lib

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

context.rs (11129B)


      1 //! Validated semantic authority for encrypted envelopes.
      2 
      3 use crate::error::{ContextField, ContextValueError, Error};
      4 use alloc::string::String;
      5 use alloc::vec::Vec;
      6 use core::fmt;
      7 use sha2::{Digest, Sha256};
      8 
      9 /// Version of the canonical authenticated context encoding.
     10 pub const ENVELOPE_CONTEXT_VERSION: u16 = 1;
     11 /// Domain separator included in every canonical context encoding.
     12 pub const ENVELOPE_CONTEXT_DOMAIN: &[u8] = b"radroots.envelope_context.v1";
     13 /// Maximum UTF-8 length of a purpose identifier.
     14 pub const ENVELOPE_PURPOSE_MAX_BYTES: usize = 128;
     15 /// Maximum UTF-8 length of a subject type discriminator.
     16 pub const ENVELOPE_SUBJECT_TYPE_MAX_BYTES: usize = 64;
     17 /// Maximum length of a subject's canonical bytes.
     18 pub const ENVELOPE_SUBJECT_VALUE_MAX_BYTES: usize = 128;
     19 /// Maximum UTF-8 length of a payload schema identifier.
     20 pub const PAYLOAD_SCHEMA_MAX_BYTES: usize = 128;
     21 
     22 /// Validated, namespaced use-case identifier for protected plaintext.
     23 #[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
     24 pub struct EnvelopePurpose(String);
     25 
     26 impl EnvelopePurpose {
     27     /// Parses a canonical lower-case namespaced purpose.
     28     pub fn parse(value: impl Into<String>) -> Result<Self, Error> {
     29         let value = value.into();
     30         validate_namespaced(
     31             value.as_str(),
     32             ENVELOPE_PURPOSE_MAX_BYTES,
     33             ContextField::Purpose,
     34         )?;
     35         Ok(Self(value))
     36     }
     37 
     38     /// Returns the validated identifier.
     39     #[must_use]
     40     pub fn as_str(&self) -> &str {
     41         self.0.as_str()
     42     }
     43 }
     44 
     45 impl fmt::Debug for EnvelopePurpose {
     46     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
     47         formatter.write_str("EnvelopePurpose(<validated>)")
     48     }
     49 }
     50 
     51 impl fmt::Display for EnvelopePurpose {
     52     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
     53         formatter.write_str("<validated envelope purpose>")
     54     }
     55 }
     56 
     57 /// Validated, typed identity of the protected object.
     58 #[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
     59 pub struct EnvelopeSubject {
     60     subject_type: String,
     61     value: String,
     62 }
     63 
     64 impl EnvelopeSubject {
     65     /// Parses a subject type and its canonical, non-secret identity bytes.
     66     pub fn parse(subject_type: impl Into<String>, value: impl Into<String>) -> Result<Self, Error> {
     67         let subject_type = subject_type.into();
     68         let value = value.into();
     69         validate_label(
     70             subject_type.as_str(),
     71             ENVELOPE_SUBJECT_TYPE_MAX_BYTES,
     72             ContextField::SubjectType,
     73         )?;
     74         validate_subject_value(value.as_str())?;
     75         Ok(Self {
     76             subject_type,
     77             value,
     78         })
     79     }
     80 
     81     /// Returns the validated type discriminator.
     82     #[must_use]
     83     pub fn subject_type(&self) -> &str {
     84         self.subject_type.as_str()
     85     }
     86 
     87     /// Returns the validated canonical subject value.
     88     #[must_use]
     89     pub fn value(&self) -> &str {
     90         self.value.as_str()
     91     }
     92 }
     93 
     94 impl fmt::Debug for EnvelopeSubject {
     95     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
     96         formatter
     97             .debug_struct("EnvelopeSubject")
     98             .field("subject_type", &self.subject_type)
     99             .field("value", &"<redacted>")
    100             .finish()
    101     }
    102 }
    103 
    104 impl fmt::Display for EnvelopeSubject {
    105     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    106         write!(formatter, "{}:<redacted>", self.subject_type)
    107     }
    108 }
    109 
    110 /// Validated, version-bearing schema identifier for protected plaintext.
    111 #[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
    112 pub struct PayloadSchemaId(String);
    113 
    114 impl PayloadSchemaId {
    115     /// Parses a canonical lower-case namespaced payload schema identifier.
    116     pub fn parse(value: impl Into<String>) -> Result<Self, Error> {
    117         let value = value.into();
    118         validate_namespaced(
    119             value.as_str(),
    120             PAYLOAD_SCHEMA_MAX_BYTES,
    121             ContextField::PayloadSchema,
    122         )?;
    123         Ok(Self(value))
    124     }
    125 
    126     /// Returns the validated identifier.
    127     #[must_use]
    128     pub fn as_str(&self) -> &str {
    129         self.0.as_str()
    130     }
    131 }
    132 
    133 impl fmt::Debug for PayloadSchemaId {
    134     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    135         formatter.write_str("PayloadSchemaId(<validated>)")
    136     }
    137 }
    138 
    139 impl fmt::Display for PayloadSchemaId {
    140     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    141         formatter.write_str("<validated payload schema>")
    142     }
    143 }
    144 
    145 /// Independently validated semantic authority authenticated by an envelope.
    146 #[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
    147 pub struct EnvelopeContext {
    148     purpose: EnvelopePurpose,
    149     subject: EnvelopeSubject,
    150     payload_schema: PayloadSchemaId,
    151 }
    152 
    153 impl EnvelopeContext {
    154     /// Constructs a context only from independently validated parts.
    155     #[must_use]
    156     pub const fn new(
    157         purpose: EnvelopePurpose,
    158         subject: EnvelopeSubject,
    159         payload_schema: PayloadSchemaId,
    160     ) -> Self {
    161         Self {
    162             purpose,
    163             subject,
    164             payload_schema,
    165         }
    166     }
    167 
    168     /// Returns the authenticated use-case identifier.
    169     #[must_use]
    170     pub const fn purpose(&self) -> &EnvelopePurpose {
    171         &self.purpose
    172     }
    173 
    174     /// Returns the authenticated typed subject.
    175     #[must_use]
    176     pub const fn subject(&self) -> &EnvelopeSubject {
    177         &self.subject
    178     }
    179 
    180     /// Returns the authenticated payload schema identifier.
    181     #[must_use]
    182     pub const fn payload_schema(&self) -> &PayloadSchemaId {
    183         &self.payload_schema
    184     }
    185 
    186     /// Encodes the deterministic context wire representation used by envelope v2.
    187     #[must_use]
    188     pub fn to_canonical_bytes(&self) -> Vec<u8> {
    189         let purpose = self.purpose.as_str().as_bytes();
    190         let subject_type = self.subject.subject_type().as_bytes();
    191         let subject_value = self.subject.value().as_bytes();
    192         let payload_schema = self.payload_schema.as_str().as_bytes();
    193         let mut encoded = Vec::with_capacity(
    194             2 + ENVELOPE_CONTEXT_DOMAIN.len()
    195                 + 2
    196                 + purpose.len()
    197                 + 2
    198                 + subject_type.len()
    199                 + 2
    200                 + subject_value.len()
    201                 + 2
    202                 + payload_schema.len(),
    203         );
    204         encoded.extend_from_slice(&ENVELOPE_CONTEXT_VERSION.to_be_bytes());
    205         encoded.extend_from_slice(ENVELOPE_CONTEXT_DOMAIN);
    206         push_bounded(&mut encoded, purpose);
    207         push_bounded(&mut encoded, subject_type);
    208         push_bounded(&mut encoded, subject_value);
    209         push_bounded(&mut encoded, payload_schema);
    210         encoded
    211     }
    212 
    213     /// Returns the SHA-256 identity used to bind provider wrapping requests.
    214     #[must_use]
    215     pub fn authentication_digest(&self) -> [u8; 32] {
    216         Sha256::digest(self.to_canonical_bytes()).into()
    217     }
    218 }
    219 
    220 impl fmt::Debug for EnvelopeContext {
    221     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    222         formatter
    223             .debug_struct("EnvelopeContext")
    224             .field("purpose", &self.purpose)
    225             .field("subject", &self.subject)
    226             .field("payload_schema", &self.payload_schema)
    227             .finish()
    228     }
    229 }
    230 
    231 fn push_bounded(encoded: &mut Vec<u8>, value: &[u8]) {
    232     let length = u16::try_from(value.len()).unwrap_or_else(|_| {
    233         unreachable!("validated envelope context fields are bounded below u16::MAX")
    234     });
    235     encoded.extend_from_slice(&length.to_be_bytes());
    236     encoded.extend_from_slice(value);
    237 }
    238 
    239 fn validate_namespaced(value: &str, max: usize, field: ContextField) -> Result<(), Error> {
    240     validate_length(value, max, field)?;
    241     if !value.contains('.') || !value.split('.').all(valid_segment) {
    242         return Err(invalid(field, ContextValueError::NonCanonical));
    243     }
    244     Ok(())
    245 }
    246 
    247 fn validate_label(value: &str, max: usize, field: ContextField) -> Result<(), Error> {
    248     validate_length(value, max, field)?;
    249     if !valid_segment(value) {
    250         return Err(invalid(field, ContextValueError::NonCanonical));
    251     }
    252     Ok(())
    253 }
    254 
    255 fn validate_subject_value(value: &str) -> Result<(), Error> {
    256     validate_length(
    257         value,
    258         ENVELOPE_SUBJECT_VALUE_MAX_BYTES,
    259         ContextField::SubjectValue,
    260     )?;
    261     if !value.bytes().all(|byte| {
    262         byte.is_ascii_lowercase()
    263             || byte.is_ascii_digit()
    264             || matches!(byte, b'_' | b'-' | b'.' | b':' | b'/')
    265     }) {
    266         return Err(invalid(
    267             ContextField::SubjectValue,
    268             ContextValueError::NonCanonical,
    269         ));
    270     }
    271     Ok(())
    272 }
    273 
    274 fn validate_length(value: &str, max: usize, field: ContextField) -> Result<(), Error> {
    275     if value.is_empty() {
    276         return Err(invalid(field, ContextValueError::Empty));
    277     }
    278     if value.len() > max {
    279         return Err(invalid(
    280             field,
    281             ContextValueError::TooLong {
    282                 actual_bytes: value.len(),
    283                 max_bytes: max,
    284             },
    285         ));
    286     }
    287     if !value.is_ascii() || value != value.trim() || value.chars().any(char::is_control) {
    288         return Err(invalid(field, ContextValueError::NonCanonical));
    289     }
    290     Ok(())
    291 }
    292 
    293 fn valid_segment(segment: &str) -> bool {
    294     let mut bytes = segment.bytes();
    295     matches!(bytes.next(), Some(first) if first.is_ascii_lowercase())
    296         && bytes.all(|byte| {
    297             byte.is_ascii_lowercase() || byte.is_ascii_digit() || matches!(byte, b'_' | b'-')
    298         })
    299 }
    300 
    301 const fn invalid(field: ContextField, reason: ContextValueError) -> Error {
    302     Error::InvalidContextValue { field, reason }
    303 }
    304 
    305 #[cfg(feature = "serde")]
    306 mod serde_impl {
    307     use super::{EnvelopeContext, EnvelopePurpose, EnvelopeSubject, PayloadSchemaId};
    308     use alloc::string::String;
    309 
    310     #[derive(serde::Serialize, serde::Deserialize)]
    311     #[serde(deny_unknown_fields)]
    312     struct WireContext {
    313         purpose: String,
    314         subject_type: String,
    315         subject: String,
    316         payload_schema: String,
    317     }
    318 
    319     impl serde::Serialize for EnvelopeContext {
    320         fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    321         where
    322             S: serde::Serializer,
    323         {
    324             WireContext {
    325                 purpose: String::from(self.purpose().as_str()),
    326                 subject_type: String::from(self.subject().subject_type()),
    327                 subject: String::from(self.subject().value()),
    328                 payload_schema: String::from(self.payload_schema().as_str()),
    329             }
    330             .serialize(serializer)
    331         }
    332     }
    333 
    334     impl<'de> serde::Deserialize<'de> for EnvelopeContext {
    335         fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    336         where
    337             D: serde::Deserializer<'de>,
    338         {
    339             let wire = WireContext::deserialize(deserializer)?;
    340             Ok(Self::new(
    341                 EnvelopePurpose::parse(wire.purpose).map_err(serde::de::Error::custom)?,
    342                 EnvelopeSubject::parse(wire.subject_type, wire.subject)
    343                     .map_err(serde::de::Error::custom)?,
    344                 PayloadSchemaId::parse(wire.payload_schema).map_err(serde::de::Error::custom)?,
    345             ))
    346         }
    347     }
    348 }