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 }