lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

error.rs (16836B)


      1 //! Normalized secret-operation errors.
      2 
      3 use crate::id::BackendKind;
      4 use core::fmt;
      5 
      6 /// Why a [`crate::SecretId`] failed validation.
      7 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
      8 #[non_exhaustive]
      9 pub enum SecretIdError {
     10     /// The identifier was empty.
     11     Empty,
     12     /// The identifier exceeded the package limit.
     13     TooLong {
     14         /// Observed UTF-8 byte length.
     15         actual_bytes: usize,
     16         /// Maximum accepted UTF-8 byte length.
     17         max_bytes: usize,
     18     },
     19     /// The identifier contained a character outside its portable alphabet.
     20     InvalidCharacter {
     21         /// UTF-8 byte offset of the invalid character.
     22         byte_offset: usize,
     23     },
     24 }
     25 
     26 /// Authenticated context field rejected by validation.
     27 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
     28 #[non_exhaustive]
     29 pub enum ContextField {
     30     /// Envelope use-case identifier.
     31     Purpose,
     32     /// Subject type discriminator.
     33     SubjectType,
     34     /// Canonical subject identity.
     35     SubjectValue,
     36     /// Plaintext schema identifier.
     37     PayloadSchema,
     38 }
     39 
     40 /// Secret-safe reason an authenticated context field was rejected.
     41 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
     42 #[non_exhaustive]
     43 pub enum ContextValueError {
     44     /// The field was empty.
     45     Empty,
     46     /// The field exceeded its explicit bound.
     47     TooLong {
     48         /// Observed UTF-8 byte length.
     49         actual_bytes: usize,
     50         /// Maximum accepted UTF-8 byte length.
     51         max_bytes: usize,
     52     },
     53     /// The field did not use its canonical portable representation.
     54     NonCanonical,
     55 }
     56 
     57 /// A security property requested by a host but unsupported by a provider.
     58 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
     59 #[non_exhaustive]
     60 pub enum PolicyRequirement {
     61     /// The secret must remain device-local.
     62     DeviceLocal,
     63     /// The provider must require user presence.
     64     UserPresence,
     65     /// The provider must use hardware-backed protection.
     66     HardwareBacked,
     67 }
     68 
     69 /// A normalized provider operation.
     70 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
     71 #[non_exhaustive]
     72 pub enum Operation {
     73     /// Create or open a provider root.
     74     Open,
     75     /// Provision caller-supplied material.
     76     Provision,
     77     /// Rotate caller-supplied material.
     78     Rotate,
     79     /// Remove provider-owned material.
     80     Remove,
     81     /// Read protected provider state.
     82     Read,
     83     /// Persist protected provider state.
     84     Write,
     85     /// Wrap plaintext key material.
     86     Wrap,
     87     /// Unwrap protected key material.
     88     Unwrap,
     89 }
     90 
     91 /// A normalized, secret-safe package failure.
     92 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
     93 #[non_exhaustive]
     94 pub enum Error {
     95     /// A secret identifier failed validation.
     96     InvalidSecretId(SecretIdError),
     97     /// An authenticated envelope context field failed validation.
     98     InvalidContextValue {
     99         /// Rejected semantic field without its value.
    100         field: ContextField,
    101         /// Secret-safe validation class.
    102         reason: ContextValueError,
    103     },
    104     /// The independently expected context did not match authenticated metadata.
    105     ContextMismatch,
    106     /// Key versions start at one; zero is never a valid version.
    107     InvalidKeyVersion,
    108     /// Secret material was empty or exceeded the bounded input limit.
    109     InvalidSecretLength {
    110         /// Observed byte length.
    111         actual_bytes: usize,
    112         /// Maximum accepted byte length.
    113         max_bytes: usize,
    114     },
    115     /// Wrapped material was empty or exceeded the bounded input limit.
    116     InvalidWrappedLength {
    117         /// Observed byte length.
    118         actual_bytes: usize,
    119         /// Maximum accepted byte length.
    120         max_bytes: usize,
    121     },
    122     /// No explicitly selected provider was available.
    123     BackendUnavailable {
    124         /// Requested adapter family.
    125         backend: BackendKind,
    126     },
    127     /// A provider cannot satisfy a required security property.
    128     PolicyUnsupported {
    129         /// Provider that rejected the policy.
    130         backend: BackendKind,
    131         /// Unsupported property.
    132         requirement: PolicyRequirement,
    133     },
    134     /// A reference was sent to the wrong provider family.
    135     BackendMismatch {
    136         /// Provider selected by the host.
    137         provider: BackendKind,
    138         /// Provider recorded by the reference.
    139         reference: BackendKind,
    140     },
    141     /// A provider operation failed without exposing its native diagnostic.
    142     BackendFailure {
    143         /// Provider that failed.
    144         backend: BackendKind,
    145         /// Normalized operation that failed.
    146         operation: Operation,
    147     },
    148     /// The referenced provider-owned key was not found.
    149     SecretNotFound {
    150         /// Provider that did not contain the key.
    151         backend: BackendKind,
    152         /// Missing key revision.
    153         key_version: u32,
    154     },
    155     /// The referenced provider-owned key already exists.
    156     SecretAlreadyExists {
    157         /// Provider that already contains the key.
    158         backend: BackendKind,
    159         /// Existing key revision.
    160         key_version: u32,
    161     },
    162     /// A key rotation did not preserve identity or advance the version.
    163     InvalidRotation,
    164     /// A filesystem path was relative, traversing, symlinked, or not a file.
    165     UnsafePath,
    166     /// Filesystem permissions allowed access outside the current user.
    167     InsecurePermissions,
    168     /// An OS keyring service identifier failed portable validation.
    169     InvalidServiceName,
    170     /// Envelope data exceeded the package-wide bound.
    171     EnvelopeTooLarge {
    172         /// Observed byte length.
    173         actual_bytes: usize,
    174         /// Maximum accepted byte length.
    175         max_bytes: usize,
    176     },
    177     /// Envelope bytes were truncated or structurally invalid.
    178     EnvelopeMalformed,
    179     /// The encoded envelope version is not supported.
    180     UnsupportedEnvelopeVersion {
    181         /// Observed version number.
    182         version: u16,
    183     },
    184     /// The authenticated context encoding version is unsupported.
    185     UnsupportedContextVersion {
    186         /// Observed context encoding version.
    187         version: u16,
    188     },
    189     /// A v1 envelope was presented to the normal v2-only open API.
    190     LegacyEnvelopeDenied,
    191     /// The expected legacy provider reference did not match authenticated v1 metadata.
    192     ProviderReferenceMismatch,
    193     /// A legacy payload failed its owning schema validator.
    194     LegacyPayloadValidationFailed,
    195     /// Legacy key or nonce material was reused for a v2 reseal.
    196     LegacyEntropyReuse,
    197     /// The encoded cipher identifier is not supported.
    198     UnsupportedCipher {
    199         /// Observed cipher identifier.
    200         cipher: u8,
    201     },
    202     /// The encoded key-source identifier is not supported.
    203     UnsupportedKeySource {
    204         /// Observed key-source identifier.
    205         key_source: u8,
    206     },
    207     /// The encoded backend identifier is not supported.
    208     UnsupportedBackend {
    209         /// Observed backend identifier.
    210         backend: u8,
    211     },
    212     /// A data-encryption key had an invalid length.
    213     InvalidDataKeyLength {
    214         /// Observed byte length.
    215         actual_bytes: usize,
    216     },
    217     /// Authenticated encryption failed.
    218     EncryptFailed,
    219     /// Authentication or decryption failed.
    220     DecryptFailed,
    221 }
    222 
    223 impl fmt::Display for SecretIdError {
    224     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    225         match self {
    226             Self::Empty => formatter.write_str("secret identifier is empty"),
    227             Self::TooLong {
    228                 actual_bytes,
    229                 max_bytes,
    230             } => write!(
    231                 formatter,
    232                 "secret identifier is too long: {actual_bytes} bytes; maximum is {max_bytes}"
    233             ),
    234             Self::InvalidCharacter { byte_offset } => write!(
    235                 formatter,
    236                 "secret identifier contains an invalid character at byte offset {byte_offset}"
    237             ),
    238         }
    239     }
    240 }
    241 
    242 impl fmt::Display for Error {
    243     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    244         match self {
    245             Self::InvalidSecretId(reason) => reason.fmt(formatter),
    246             Self::InvalidContextValue { field, reason } => {
    247                 write!(formatter, "envelope context {field:?} is {reason}")
    248             }
    249             Self::ContextMismatch => formatter.write_str("encrypted envelope context mismatch"),
    250             Self::InvalidKeyVersion => formatter.write_str("secret key version must be non-zero"),
    251             Self::InvalidSecretLength {
    252                 actual_bytes,
    253                 max_bytes,
    254             } => write!(
    255                 formatter,
    256                 "secret material length is invalid: {actual_bytes} bytes; maximum is {max_bytes}"
    257             ),
    258             Self::InvalidWrappedLength {
    259                 actual_bytes,
    260                 max_bytes,
    261             } => write!(
    262                 formatter,
    263                 "wrapped material length is invalid: {actual_bytes} bytes; maximum is {max_bytes}"
    264             ),
    265             Self::BackendUnavailable { backend } => {
    266                 write!(formatter, "secret backend {backend:?} is unavailable")
    267             }
    268             Self::PolicyUnsupported {
    269                 backend,
    270                 requirement,
    271             } => write!(
    272                 formatter,
    273                 "secret backend {backend:?} does not satisfy {requirement:?}"
    274             ),
    275             Self::BackendMismatch {
    276                 provider,
    277                 reference,
    278             } => write!(
    279                 formatter,
    280                 "secret reference backend {reference:?} does not match provider {provider:?}"
    281             ),
    282             Self::BackendFailure { backend, operation } => write!(
    283                 formatter,
    284                 "secret backend {backend:?} failed during {operation:?}"
    285             ),
    286             Self::SecretNotFound {
    287                 backend,
    288                 key_version,
    289             } => write!(
    290                 formatter,
    291                 "secret backend {backend:?} has no key at version {key_version}"
    292             ),
    293             Self::SecretAlreadyExists {
    294                 backend,
    295                 key_version,
    296             } => write!(
    297                 formatter,
    298                 "secret backend {backend:?} already has key version {key_version}"
    299             ),
    300             Self::InvalidRotation => {
    301                 formatter.write_str("secret rotation must preserve identity and advance version")
    302             }
    303             Self::UnsafePath => formatter.write_str("secret provider path is unsafe"),
    304             Self::InsecurePermissions => {
    305                 formatter.write_str("secret provider permissions are insecure")
    306             }
    307             Self::InvalidServiceName => {
    308                 formatter.write_str("secret provider service name is invalid")
    309             }
    310             Self::EnvelopeTooLarge {
    311                 actual_bytes,
    312                 max_bytes,
    313             } => write!(
    314                 formatter,
    315                 "encrypted envelope is too large: {actual_bytes} bytes; maximum is {max_bytes}"
    316             ),
    317             Self::EnvelopeMalformed => formatter.write_str("encrypted envelope is malformed"),
    318             Self::UnsupportedEnvelopeVersion { version } => {
    319                 write!(
    320                     formatter,
    321                     "encrypted envelope version {version} is unsupported"
    322                 )
    323             }
    324             Self::UnsupportedContextVersion { version } => {
    325                 write!(
    326                     formatter,
    327                     "envelope context version {version} is unsupported"
    328                 )
    329             }
    330             Self::LegacyEnvelopeDenied => {
    331                 formatter.write_str("legacy encrypted envelope requires migration authority")
    332             }
    333             Self::ProviderReferenceMismatch => {
    334                 formatter.write_str("encrypted envelope provider reference mismatch")
    335             }
    336             Self::LegacyPayloadValidationFailed => {
    337                 formatter.write_str("legacy encrypted payload failed schema validation")
    338             }
    339             Self::LegacyEntropyReuse => {
    340                 formatter.write_str("legacy envelope cryptographic material cannot be reused")
    341             }
    342             Self::UnsupportedCipher { cipher } => {
    343                 write!(
    344                     formatter,
    345                     "encrypted envelope cipher {cipher} is unsupported"
    346                 )
    347             }
    348             Self::UnsupportedKeySource { key_source } => write!(
    349                 formatter,
    350                 "encrypted envelope key source {key_source} is unsupported"
    351             ),
    352             Self::UnsupportedBackend { backend } => {
    353                 write!(
    354                     formatter,
    355                     "encrypted envelope backend {backend} is unsupported"
    356                 )
    357             }
    358             Self::InvalidDataKeyLength { actual_bytes } => write!(
    359                 formatter,
    360                 "envelope data key must be 32 bytes; got {actual_bytes}"
    361             ),
    362             Self::EncryptFailed => formatter.write_str("encrypted envelope sealing failed"),
    363             Self::DecryptFailed => formatter.write_str("encrypted envelope authentication failed"),
    364         }
    365     }
    366 }
    367 
    368 impl fmt::Display for ContextValueError {
    369     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    370         match self {
    371             Self::Empty => formatter.write_str("empty"),
    372             Self::TooLong {
    373                 actual_bytes,
    374                 max_bytes,
    375             } => write!(
    376                 formatter,
    377                 "too long ({actual_bytes} bytes; maximum is {max_bytes})"
    378             ),
    379             Self::NonCanonical => formatter.write_str("not canonical"),
    380         }
    381     }
    382 }
    383 
    384 #[cfg(feature = "std")]
    385 impl std::error::Error for Error {}
    386 
    387 #[cfg(test)]
    388 mod tests {
    389     use super::*;
    390     use alloc::format;
    391     use alloc::string::ToString;
    392 
    393     #[test]
    394     fn every_normalized_error_has_a_secret_safe_message() {
    395         let id_errors = [
    396             SecretIdError::Empty,
    397             SecretIdError::TooLong {
    398                 actual_bytes: 2,
    399                 max_bytes: 1,
    400             },
    401             SecretIdError::InvalidCharacter { byte_offset: 3 },
    402         ];
    403         for error in id_errors {
    404             assert!(!error.to_string().is_empty());
    405         }
    406         let errors = [
    407             Error::InvalidSecretId(SecretIdError::Empty),
    408             Error::InvalidContextValue {
    409                 field: ContextField::SubjectValue,
    410                 reason: ContextValueError::NonCanonical,
    411             },
    412             Error::ContextMismatch,
    413             Error::InvalidKeyVersion,
    414             Error::InvalidSecretLength {
    415                 actual_bytes: 0,
    416                 max_bytes: 1,
    417             },
    418             Error::InvalidWrappedLength {
    419                 actual_bytes: 0,
    420                 max_bytes: 1,
    421             },
    422             Error::BackendUnavailable {
    423                 backend: BackendKind::Memory,
    424             },
    425             Error::PolicyUnsupported {
    426                 backend: BackendKind::File,
    427                 requirement: PolicyRequirement::DeviceLocal,
    428             },
    429             Error::BackendMismatch {
    430                 provider: BackendKind::Memory,
    431                 reference: BackendKind::File,
    432             },
    433             Error::BackendFailure {
    434                 backend: BackendKind::Keyring,
    435                 operation: Operation::Open,
    436             },
    437             Error::SecretNotFound {
    438                 backend: BackendKind::External,
    439                 key_version: 1,
    440             },
    441             Error::SecretAlreadyExists {
    442                 backend: BackendKind::Memory,
    443                 key_version: 2,
    444             },
    445             Error::InvalidRotation,
    446             Error::UnsafePath,
    447             Error::InsecurePermissions,
    448             Error::InvalidServiceName,
    449             Error::EnvelopeTooLarge {
    450                 actual_bytes: 2,
    451                 max_bytes: 1,
    452             },
    453             Error::EnvelopeMalformed,
    454             Error::UnsupportedEnvelopeVersion { version: 2 },
    455             Error::UnsupportedContextVersion { version: 2 },
    456             Error::LegacyEnvelopeDenied,
    457             Error::ProviderReferenceMismatch,
    458             Error::LegacyPayloadValidationFailed,
    459             Error::LegacyEntropyReuse,
    460             Error::UnsupportedCipher { cipher: 9 },
    461             Error::UnsupportedKeySource { key_source: 9 },
    462             Error::UnsupportedBackend { backend: 9 },
    463             Error::InvalidDataKeyLength { actual_bytes: 1 },
    464             Error::EncryptFailed,
    465             Error::DecryptFailed,
    466         ];
    467         for error in errors {
    468             assert!(!error.to_string().is_empty());
    469         }
    470         for operation in [
    471             Operation::Open,
    472             Operation::Provision,
    473             Operation::Rotate,
    474             Operation::Remove,
    475             Operation::Read,
    476             Operation::Write,
    477             Operation::Wrap,
    478             Operation::Unwrap,
    479         ] {
    480             assert!(!format!("{operation:?}").is_empty());
    481         }
    482     }
    483 }