commit a82499a6523ee68a224f32588a5e4f98b47a2fce parent 36aeec6ed6190fd16046dc10cc6926e5fbb2cc07 Author: triesap <tyson@radroots.org> Date: Wed, 5 Aug 2026 17:48:29 +0000 feat(secrets): require context-bound envelope v2 Diffstat:
16 files changed, 421 insertions(+), 97 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock @@ -2683,6 +2683,7 @@ dependencies = [ "keyring", "serde", "serde_json", + "sha2", "tempfile", "zeroize", ] diff --git a/crates/secrets/Cargo.toml b/crates/secrets/Cargo.toml @@ -36,6 +36,7 @@ serde = { workspace = true, default-features = false, features = [ "alloc", "derive", ], optional = true } +sha2 = { workspace = true, default-features = false } tempfile = { workspace = true, optional = true } zeroize = { workspace = true } diff --git a/crates/secrets/README.md b/crates/secrets/README.md @@ -39,6 +39,9 @@ serializes the envelope, and opens it again: # fn main() -> Result<(), Box<dyn std::error::Error>> { use futures_executor::block_on; use radroots_secrets::EncryptedEnvelope; +use radroots_secrets::context::{ + EnvelopeContext, EnvelopePurpose, EnvelopeSubject, PayloadSchemaId, +}; use radroots_secrets::envelope::{Nonce, SealMaterial, SealRequest}; use radroots_secrets::id::{BackendKind, KeyVersion}; use radroots_secrets::memory::MemoryProvider; @@ -58,14 +61,20 @@ provider.provision( let plaintext = SecretMaterial::from_slice(b"private profile value")?; let data_key = SecretMaterial::from_slice(&[0x41; 32])?; +let context = EnvelopeContext::new( + EnvelopePurpose::parse("radroots.private_profile")?, + EnvelopeSubject::parse("profile", "example-profile")?, + PayloadSchemaId::parse("radroots.private_profile.v1")?, +); let request = SealRequest::new( reference, + context.clone(), &plaintext, SealMaterial::new(data_key, Nonce::new([0x24; 24])), ); let encoded = block_on(EncryptedEnvelope::seal(&provider, request))?.encode()?; let decoded = EncryptedEnvelope::decode(&encoded)?; -let opened = block_on(decoded.open(&provider))?; +let opened = block_on(decoded.open(&provider, &context))?; opened.expose_secret(|bytes| assert_eq!(bytes, b"private profile value")); # Ok(()) @@ -104,12 +113,14 @@ cloneable or serializable. Secret identifiers are serialized only where a documented wire or storage format requires them; their ordinary diagnostics remain redacted. -`EncryptedEnvelope` authenticates its format version, cipher, key source, -backend, key version, secret identifier, nonce, wrapped data key, and -ciphertext length. Decode validates all lengths and enum values before provider -access. Envelope serialization is a persistence contract; Rust layout and -debug output are not. Unknown versions, ciphers, key sources, malformed -lengths, backend mismatches, and authentication failures fail closed. +`EncryptedEnvelope` v2 authenticates its format version, cipher, key source, +backend, key version, secret identifier, purpose, typed subject, payload +schema, nonce, wrapped data key, and ciphertext length. Normal open requires an +independently derived expected context and rejects legacy v1. Decode validates +all lengths and enum values before provider access. Envelope serialization is +a persistence contract; Rust layout and debug output are not. Unknown versions, +ciphers, key sources, malformed lengths, context mismatches, backend mismatches, +and authentication failures fail closed. Provider-native error strings are normalized before crossing the public boundary. Callers must still avoid logging plaintext, serialized identifiers, diff --git a/crates/secrets/examples/explicit_memory_provider.rs b/crates/secrets/examples/explicit_memory_provider.rs @@ -1,5 +1,8 @@ use futures_executor::block_on; use radroots_secrets::EncryptedEnvelope; +use radroots_secrets::context::{ + EnvelopeContext, EnvelopePurpose, EnvelopeSubject, PayloadSchemaId, +}; use radroots_secrets::envelope::{Nonce, SealMaterial, SealRequest}; use radroots_secrets::id::{BackendKind, KeyVersion}; use radroots_secrets::memory::MemoryProvider; @@ -17,14 +20,20 @@ fn main() -> Result<(), Box<dyn std::error::Error>> { let plaintext = SecretMaterial::from_slice(b"private profile value")?; let data_key = SecretMaterial::from_slice(&[0x41; 32])?; + let context = EnvelopeContext::new( + EnvelopePurpose::parse("radroots.private_profile")?, + EnvelopeSubject::parse("profile", "example-profile")?, + PayloadSchemaId::parse("radroots.private_profile.v1")?, + ); let request = SealRequest::new( reference, + context.clone(), &plaintext, SealMaterial::new(data_key, Nonce::new([0x24; 24])), ); let encoded = block_on(EncryptedEnvelope::seal(&provider, request))?.encode()?; let decoded = EncryptedEnvelope::decode(&encoded)?; - let opened = block_on(decoded.open(&provider))?; + let opened = block_on(decoded.open(&provider, &context))?; opened.expose_secret(|bytes| assert_eq!(bytes, b"private profile value")); println!("opened one explicitly provisioned in-memory envelope"); diff --git a/crates/secrets/src/context.rs b/crates/secrets/src/context.rs @@ -4,6 +4,7 @@ use crate::error::{ContextField, ContextValueError, Error}; use alloc::string::String; use alloc::vec::Vec; use core::fmt; +use sha2::{Digest, Sha256}; /// Version of the canonical authenticated context encoding. pub const ENVELOPE_CONTEXT_VERSION: u16 = 1; @@ -208,6 +209,12 @@ impl EnvelopeContext { push_bounded(&mut encoded, payload_schema); encoded } + + /// Returns the SHA-256 identity used to bind provider wrapping requests. + #[must_use] + pub fn authentication_digest(&self) -> [u8; 32] { + Sha256::digest(self.to_canonical_bytes()).into() + } } impl fmt::Debug for EnvelopeContext { diff --git a/crates/secrets/src/envelope.rs b/crates/secrets/src/envelope.rs @@ -1,9 +1,15 @@ -//! Versioned encrypted-envelope contracts. +//! Versioned context-bound encrypted-envelope contracts. +use crate::context::{ + ENVELOPE_CONTEXT_DOMAIN, ENVELOPE_CONTEXT_VERSION, ENVELOPE_PURPOSE_MAX_BYTES, + ENVELOPE_SUBJECT_TYPE_MAX_BYTES, ENVELOPE_SUBJECT_VALUE_MAX_BYTES, EnvelopeContext, + EnvelopePurpose, EnvelopeSubject, PAYLOAD_SCHEMA_MAX_BYTES, PayloadSchemaId, +}; use crate::error::Error; use crate::id::{BackendKind, KeyVersion}; use crate::wrapping::{KeyWrapping, SecretMaterial, UnwrapRequest, WrapRequest, WrappedSecret}; use crate::{SecretId, SecretRef}; +use alloc::string::String; use alloc::vec::Vec; use chacha20poly1305::aead::{Aead, KeyInit, Payload}; use chacha20poly1305::{Key, XChaCha20Poly1305, XNonce}; @@ -14,8 +20,10 @@ const DATA_KEY_BYTES: usize = 32; const AEAD_TAG_BYTES: usize = 16; const NONCE_BYTES: usize = 24; -/// Current authenticated envelope format version. -pub const ENVELOPE_VERSION: u16 = 1; +/// Legacy structurally authenticated envelope format. +pub const LEGACY_ENVELOPE_VERSION: u16 = 1; +/// Current context-bound authenticated envelope format. +pub const ENVELOPE_VERSION: u16 = 2; /// Maximum encoded envelope size accepted from storage. pub const ENVELOPE_MAX_BYTES: usize = 256 * 1024; @@ -109,9 +117,10 @@ impl fmt::Debug for SealMaterial { } } -/// Complete input for one envelope sealing operation. +/// Complete input for one context-bound envelope sealing operation. pub struct SealRequest<'a> { reference: SecretRef, + context: EnvelopeContext, plaintext: &'a SecretMaterial, material: SealMaterial, } @@ -121,11 +130,13 @@ impl<'a> SealRequest<'a> { #[must_use] pub const fn new( reference: SecretRef, + context: EnvelopeContext, plaintext: &'a SecretMaterial, material: SealMaterial, ) -> Self { Self { reference, + context, plaintext, material, } @@ -144,22 +155,24 @@ pub struct EncryptedEnvelope { cipher: Cipher, key_source: KeySource, reference: SecretRef, + context: Option<EnvelopeContext>, nonce: Nonce, wrapped_key: WrappedSecret, ciphertext: Vec<u8>, } impl EncryptedEnvelope { - /// Seals plaintext using only explicit host-supplied key and nonce material. + /// Seals plaintext as v2 using explicit context, key, and nonce material. pub async fn seal(wrapping: &dyn KeyWrapping, request: SealRequest<'_>) -> Result<Self, Error> { let SealRequest { reference, + context, plaintext, material, } = request; validate_data_key(&material.data_key)?; let wrapped_key = wrapping - .wrap(WrapRequest::new(&reference, &material.data_key)) + .wrap(WrapRequest::new(&reference, &context, &material.data_key)) .await?; let ciphertext_len = plaintext @@ -174,6 +187,7 @@ impl EncryptedEnvelope { Cipher::XChaCha20Poly1305, KeySource::ProviderWrapped, &reference, + Some(&context), material.nonce, &wrapped_key, ciphertext_len, @@ -197,6 +211,7 @@ impl EncryptedEnvelope { cipher: Cipher::XChaCha20Poly1305, key_source: KeySource::ProviderWrapped, reference, + context: Some(context), nonce: material.nonce, wrapped_key, ciphertext, @@ -205,11 +220,26 @@ impl EncryptedEnvelope { Ok(envelope) } - /// Authenticates and decrypts into a single-owner zeroizing value. - pub async fn open(&self, wrapping: &dyn KeyWrapping) -> Result<SecretMaterial, Error> { + /// Authenticates v2 using independently expected context before releasing plaintext. + pub async fn open( + &self, + wrapping: &dyn KeyWrapping, + expected_context: &EnvelopeContext, + ) -> Result<SecretMaterial, Error> { self.validate()?; + if self.version == LEGACY_ENVELOPE_VERSION { + return Err(Error::LegacyEnvelopeDenied); + } + let stored_context = self.context.as_ref().ok_or(Error::EnvelopeMalformed)?; + if stored_context != expected_context { + return Err(Error::ContextMismatch); + } let data_key = wrapping - .unwrap(UnwrapRequest::new(&self.reference, &self.wrapped_key)) + .unwrap(UnwrapRequest::new( + &self.reference, + expected_context, + &self.wrapped_key, + )) .await?; validate_data_key(&data_key)?; let aad = self.encoded_header()?; @@ -240,6 +270,12 @@ impl EncryptedEnvelope { self.version } + /// Returns the authenticated v2 context, or `None` for a legacy v1 envelope. + #[must_use] + pub const fn context(&self) -> Option<&EnvelopeContext> { + self.context.as_ref() + } + /// Returns the authenticated cipher identifier. #[must_use] pub const fn cipher(&self) -> Cipher { @@ -266,7 +302,7 @@ impl EncryptedEnvelope { Ok(encoded) } - /// Decodes and validates an envelope without accessing a provider. + /// Decodes and validates v1 or v2 without accessing a provider. pub fn decode(encoded: &[u8]) -> Result<Self, Error> { if encoded.len() > ENVELOPE_MAX_BYTES { return Err(Error::EnvelopeTooLarge { @@ -279,22 +315,24 @@ impl EncryptedEnvelope { return Err(Error::EnvelopeMalformed); } let version = decoder.u16()?; - if version != ENVELOPE_VERSION { + if !matches!(version, LEGACY_ENVELOPE_VERSION | ENVELOPE_VERSION) { return Err(Error::UnsupportedEnvelopeVersion { version }); } let cipher = Cipher::from_code(decoder.u8()?)?; let key_source = KeySource::from_code(decoder.u8()?)?; let backend = BackendKind::from_code(decoder.u8()?)?; let key_version = KeyVersion::new(decoder.u32()?)?; - let id_len = usize::from(decoder.u16()?); - let id_bytes = decoder.take(id_len)?; - let id = core::str::from_utf8(id_bytes).map_err(|_| Error::EnvelopeMalformed)?; + let id = decoder.bounded_string(u16::MAX.into())?; let reference = SecretRef::new(SecretId::parse(id)?, backend, key_version); + let context = if version == ENVELOPE_VERSION { + Some(decode_context(&mut decoder)?) + } else { + None + }; let nonce = Nonce::new(decoder.take_array::<NONCE_BYTES>()?); - let wrapped_len = usize::try_from(decoder.u32()?).map_err(|_| Error::EnvelopeMalformed)?; + let wrapped_len = decoder.u32_usize()?; let wrapped_key = WrappedSecret::from_bytes(decoder.take(wrapped_len)?.to_vec())?; - let ciphertext_len = - usize::try_from(decoder.u32()?).map_err(|_| Error::EnvelopeMalformed)?; + let ciphertext_len = decoder.u32_usize()?; let ciphertext = decoder.take(ciphertext_len)?.to_vec(); if !decoder.is_empty() { return Err(Error::EnvelopeMalformed); @@ -304,6 +342,7 @@ impl EncryptedEnvelope { cipher, key_source, reference, + context, nonce, wrapped_key, ciphertext, @@ -318,6 +357,7 @@ impl EncryptedEnvelope { self.cipher, self.key_source, &self.reference, + self.context.as_ref(), self.nonce, &self.wrapped_key, self.ciphertext.len(), @@ -325,10 +365,12 @@ impl EncryptedEnvelope { } fn validate(&self) -> Result<(), Error> { - if self.version != ENVELOPE_VERSION { - return Err(Error::UnsupportedEnvelopeVersion { - version: self.version, - }); + match (self.version, self.context.is_some()) { + (LEGACY_ENVELOPE_VERSION, false) | (ENVELOPE_VERSION, true) => {} + (LEGACY_ENVELOPE_VERSION | ENVELOPE_VERSION, _) => { + return Err(Error::EnvelopeMalformed); + } + (version, _) => return Err(Error::UnsupportedEnvelopeVersion { version }), } if self.ciphertext.len() < AEAD_TAG_BYTES { return Err(Error::EnvelopeMalformed); @@ -352,6 +394,7 @@ impl fmt::Debug for EncryptedEnvelope { .field("cipher", &self.cipher) .field("key_source", &self.key_source) .field("reference", &self.reference) + .field("context", &self.context) .field("nonce", &"<redacted>") .field("wrapped_key", &"<redacted>") .field("ciphertext", &"<redacted>") @@ -381,6 +424,22 @@ impl<'de> serde::Deserialize<'de> for EncryptedEnvelope { } } +fn decode_context(decoder: &mut Decoder<'_>) -> Result<EnvelopeContext, Error> { + let version = decoder.u16()?; + if version != ENVELOPE_CONTEXT_VERSION { + return Err(Error::UnsupportedContextVersion { version }); + } + if decoder.take(ENVELOPE_CONTEXT_DOMAIN.len())? != ENVELOPE_CONTEXT_DOMAIN { + return Err(Error::EnvelopeMalformed); + } + let purpose = EnvelopePurpose::parse(decoder.bounded_string(ENVELOPE_PURPOSE_MAX_BYTES)?)?; + let subject_type = decoder.bounded_string(ENVELOPE_SUBJECT_TYPE_MAX_BYTES)?; + let subject_value = decoder.bounded_string(ENVELOPE_SUBJECT_VALUE_MAX_BYTES)?; + let subject = EnvelopeSubject::parse(subject_type, subject_value)?; + let payload_schema = PayloadSchemaId::parse(decoder.bounded_string(PAYLOAD_SCHEMA_MAX_BYTES)?)?; + Ok(EnvelopeContext::new(purpose, subject, payload_schema)) +} + fn validate_data_key(data_key: &SecretMaterial) -> Result<(), Error> { if data_key.len() != DATA_KEY_BYTES { return Err(Error::InvalidDataKeyLength { @@ -390,11 +449,13 @@ fn validate_data_key(data_key: &SecretMaterial) -> Result<(), Error> { Ok(()) } +#[allow(clippy::too_many_arguments)] fn encode_header( version: u16, cipher: Cipher, key_source: KeySource, reference: &SecretRef, + context: Option<&EnvelopeContext>, nonce: Nonce, wrapped_key: &WrappedSecret, ciphertext_len: usize, @@ -407,8 +468,20 @@ fn encode_header( actual_bytes: ciphertext_len, max_bytes: ENVELOPE_MAX_BYTES, })?; - let capacity = - 4 + 2 + 1 + 1 + 1 + 4 + 2 + id.len() + NONCE_BYTES + 4 + wrapped_key.as_bytes().len() + 4; + let context_len = context.map_or(0, |value| value.to_canonical_bytes().len()); + let capacity = 4 + + 2 + + 1 + + 1 + + 1 + + 4 + + 2 + + id.len() + + context_len + + NONCE_BYTES + + 4 + + wrapped_key.as_bytes().len() + + 4; let mut encoded = Vec::with_capacity(capacity); encoded.extend_from_slice(&MAGIC); encoded.extend_from_slice(&version.to_be_bytes()); @@ -418,6 +491,16 @@ fn encode_header( encoded.extend_from_slice(&reference.key_version().get().to_be_bytes()); encoded.extend_from_slice(&id_len.to_be_bytes()); encoded.extend_from_slice(id); + match (version, context) { + (LEGACY_ENVELOPE_VERSION, None) => {} + (ENVELOPE_VERSION, Some(context)) => { + encoded.extend_from_slice(&context.to_canonical_bytes()); + } + (LEGACY_ENVELOPE_VERSION | ENVELOPE_VERSION, _) => { + return Err(Error::EnvelopeMalformed); + } + (version, _) => return Err(Error::UnsupportedEnvelopeVersion { version }), + } encoded.extend_from_slice(nonce.as_bytes()); encoded.extend_from_slice(&wrapped_len.to_be_bytes()); encoded.extend_from_slice(wrapped_key.as_bytes()); @@ -461,6 +544,20 @@ impl<'a> Decoder<'a> { Ok(u32::from_be_bytes(self.take_array()?)) } + fn u32_usize(&mut self) -> Result<usize, Error> { + usize::try_from(self.u32()?).map_err(|_| Error::EnvelopeMalformed) + } + + fn bounded_string(&mut self, max: usize) -> Result<String, Error> { + let length = usize::from(self.u16()?); + if length > max { + return Err(Error::EnvelopeMalformed); + } + let value = + core::str::from_utf8(self.take(length)?).map_err(|_| Error::EnvelopeMalformed)?; + Ok(String::from(value)) + } + const fn is_empty(&self) -> bool { self.remaining.is_empty() } @@ -470,6 +567,15 @@ impl<'a> Decoder<'a> { mod tests { use super::*; + fn context() -> EnvelopeContext { + EnvelopeContext::new( + EnvelopePurpose::parse("radroots.private_artifact").expect("purpose"), + EnvelopeSubject::parse("private_artifact", "01010101010101010101010101010101") + .expect("subject"), + PayloadSchemaId::parse("trade.private_terms.v1").expect("schema"), + ) + } + fn envelope() -> EncryptedEnvelope { EncryptedEnvelope { version: ENVELOPE_VERSION, @@ -480,6 +586,7 @@ mod tests { BackendKind::Memory, KeyVersion::new(1).expect("version"), ), + context: Some(context()), nonce: Nonce::new([7; NONCE_BYTES]), wrapped_key: WrappedSecret::from_bytes(vec![8; 32]).expect("wrapped"), ciphertext: vec![9; AEAD_TAG_BYTES], @@ -510,29 +617,25 @@ mod tests { EncryptedEnvelope::decode(&malformed).err(), Some(Error::EnvelopeMalformed) ); + for (offset, expected) in [ + (5, Error::UnsupportedEnvelopeVersion { version: 3 }), + (6, Error::UnsupportedCipher { cipher: 9 }), + (7, Error::UnsupportedKeySource { key_source: 9 }), + (8, Error::UnsupportedBackend { backend: 9 }), + ] { + let mut unsupported = encoded.clone(); + unsupported[offset] = if offset == 5 { 3 } else { 9 }; + assert_eq!( + EncryptedEnvelope::decode(&unsupported).err(), + Some(expected) + ); + } + let context_version_offset = 4 + 2 + 1 + 1 + 1 + 4 + 2 + "coverage-key".len(); let mut unsupported = encoded.clone(); - unsupported[5] = 2; - assert_eq!( - EncryptedEnvelope::decode(&unsupported).err(), - Some(Error::UnsupportedEnvelopeVersion { version: 2 }) - ); - let mut unsupported = encoded.clone(); - unsupported[6] = 9; - assert_eq!( - EncryptedEnvelope::decode(&unsupported).err(), - Some(Error::UnsupportedCipher { cipher: 9 }) - ); - let mut unsupported = encoded.clone(); - unsupported[7] = 9; - assert_eq!( - EncryptedEnvelope::decode(&unsupported).err(), - Some(Error::UnsupportedKeySource { key_source: 9 }) - ); - let mut unsupported = encoded.clone(); - unsupported[8] = 9; + unsupported[context_version_offset + 1] = 2; assert_eq!( EncryptedEnvelope::decode(&unsupported).err(), - Some(Error::UnsupportedBackend { backend: 9 }) + Some(Error::UnsupportedContextVersion { version: 2 }) ); let mut trailing = encoded; trailing.push(0); @@ -542,12 +645,15 @@ mod tests { ); let mut invalid = envelope(); - invalid.version = 2; + invalid.version = 3; assert_eq!( invalid.encode().err(), - Some(Error::UnsupportedEnvelopeVersion { version: 2 }) + Some(Error::UnsupportedEnvelopeVersion { version: 3 }) ); let mut invalid = envelope(); + invalid.context = None; + assert_eq!(invalid.encode().err(), Some(Error::EnvelopeMalformed)); + let mut invalid = envelope(); invalid.ciphertext.clear(); assert_eq!(invalid.encode().err(), Some(Error::EnvelopeMalformed)); let mut invalid = envelope(); diff --git a/crates/secrets/src/error.rs b/crates/secrets/src/error.rs @@ -101,6 +101,8 @@ pub enum Error { /// Secret-safe validation class. reason: ContextValueError, }, + /// The independently expected context did not match authenticated metadata. + ContextMismatch, /// Key versions start at one; zero is never a valid version. InvalidKeyVersion, /// Secret material was empty or exceeded the bounded input limit. @@ -179,6 +181,13 @@ pub enum Error { /// Observed version number. version: u16, }, + /// The authenticated context encoding version is unsupported. + UnsupportedContextVersion { + /// Observed context encoding version. + version: u16, + }, + /// A v1 envelope was presented to the normal v2-only open API. + LegacyEnvelopeDenied, /// The encoded cipher identifier is not supported. UnsupportedCipher { /// Observed cipher identifier. @@ -231,6 +240,7 @@ impl fmt::Display for Error { Self::InvalidContextValue { field, reason } => { write!(formatter, "envelope context {field:?} is {reason}") } + Self::ContextMismatch => formatter.write_str("encrypted envelope context mismatch"), Self::InvalidKeyVersion => formatter.write_str("secret key version must be non-zero"), Self::InvalidSecretLength { actual_bytes, @@ -305,6 +315,15 @@ impl fmt::Display for Error { "encrypted envelope version {version} is unsupported" ) } + Self::UnsupportedContextVersion { version } => { + write!( + formatter, + "envelope context version {version} is unsupported" + ) + } + Self::LegacyEnvelopeDenied => { + formatter.write_str("legacy encrypted envelope requires migration authority") + } Self::UnsupportedCipher { cipher } => { write!( formatter, @@ -373,6 +392,7 @@ mod tests { field: ContextField::SubjectValue, reason: ContextValueError::NonCanonical, }, + Error::ContextMismatch, Error::InvalidKeyVersion, Error::InvalidSecretLength { actual_bytes: 0, @@ -415,6 +435,8 @@ mod tests { }, Error::EnvelopeMalformed, Error::UnsupportedEnvelopeVersion { version: 2 }, + Error::UnsupportedContextVersion { version: 2 }, + Error::LegacyEnvelopeDenied, Error::UnsupportedCipher { cipher: 9 }, Error::UnsupportedKeySource { key_source: 9 }, Error::UnsupportedBackend { backend: 9 }, diff --git a/crates/secrets/src/keyring.rs b/crates/secrets/src/keyring.rs @@ -323,6 +323,7 @@ const fn backend_failure(operation: Operation) -> Error { mod tests { use super::*; use crate::SecretId; + use crate::context::{EnvelopeContext, EnvelopePurpose, EnvelopeSubject, PayloadSchemaId}; use crate::id::KeyVersion; use alloc::collections::BTreeMap; use std::sync::Arc; @@ -388,6 +389,14 @@ mod tests { ) } + fn context() -> EnvelopeContext { + EnvelopeContext::new( + EnvelopePurpose::parse("radroots.keyring_test").expect("purpose"), + EnvelopeSubject::parse("keyring_test", "fixture").expect("subject"), + PayloadSchemaId::parse("radroots.keyring_test.v1").expect("schema"), + ) + } + #[test] fn construction_has_no_store_side_effect() { let store = MockStore::default(); @@ -408,23 +417,29 @@ mod tests { let next = reference("keyring-key", 2); let current_material = SecretMaterial::from_slice(&[0x11; 32]).expect("material"); let next_material = SecretMaterial::from_slice(&[0x22; 32]).expect("material"); + let context = context(); assert!(matches!( - futures_executor::block_on( - provider.wrap(WrapRequest::new(¤t, ¤t_material)) - ), + futures_executor::block_on(provider.wrap(WrapRequest::new( + ¤t, + &context, + ¤t_material + ))), Err(Error::SecretNotFound { .. }) )); provider .provision(¤t, ¤t_material) .expect("provision"); - let wrapped = futures_executor::block_on( - provider.wrap(WrapRequest::new(¤t, ¤t_material)), - ) + let wrapped = futures_executor::block_on(provider.wrap(WrapRequest::new( + ¤t, + &context, + ¤t_material, + ))) .expect("wrap"); - let opened = - futures_executor::block_on(provider.unwrap(UnwrapRequest::new(¤t, &wrapped))) - .expect("unwrap"); + let opened = futures_executor::block_on( + provider.unwrap(UnwrapRequest::new(¤t, &context, &wrapped)), + ) + .expect("unwrap"); opened.expose_secret(|bytes| assert_eq!(bytes, &[0x11; 32])); provider diff --git a/crates/secrets/src/wrapping.rs b/crates/secrets/src/wrapping.rs @@ -1,6 +1,7 @@ //! Data-key wrapping contracts. use crate::SecretRef; +use crate::context::EnvelopeContext; use crate::error::Error; use alloc::boxed::Box; use alloc::vec::Vec; @@ -115,15 +116,21 @@ impl fmt::Debug for WrappedSecret { #[derive(Debug, Clone, Copy)] pub struct WrapRequest<'a> { reference: &'a SecretRef, + context: &'a EnvelopeContext, plaintext: &'a SecretMaterial, } impl<'a> WrapRequest<'a> { /// Creates an explicit wrapping request. #[must_use] - pub const fn new(reference: &'a SecretRef, plaintext: &'a SecretMaterial) -> Self { + pub const fn new( + reference: &'a SecretRef, + context: &'a EnvelopeContext, + plaintext: &'a SecretMaterial, + ) -> Self { Self { reference, + context, plaintext, } } @@ -134,6 +141,12 @@ impl<'a> WrapRequest<'a> { self.reference } + /// Returns the independently validated semantic wrapping authority. + #[must_use] + pub const fn context(&self) -> &'a EnvelopeContext { + self.context + } + /// Returns the single-owner plaintext wrapper. #[must_use] pub const fn plaintext(&self) -> &'a SecretMaterial { @@ -145,14 +158,23 @@ impl<'a> WrapRequest<'a> { #[derive(Debug, Clone, Copy)] pub struct UnwrapRequest<'a> { reference: &'a SecretRef, + context: &'a EnvelopeContext, wrapped: &'a WrappedSecret, } impl<'a> UnwrapRequest<'a> { /// Creates an explicit unwrapping request. #[must_use] - pub const fn new(reference: &'a SecretRef, wrapped: &'a WrappedSecret) -> Self { - Self { reference, wrapped } + pub const fn new( + reference: &'a SecretRef, + context: &'a EnvelopeContext, + wrapped: &'a WrappedSecret, + ) -> Self { + Self { + reference, + context, + wrapped, + } } /// Returns the provider capability reference. @@ -161,6 +183,12 @@ impl<'a> UnwrapRequest<'a> { self.reference } + /// Returns the independently validated semantic unwrapping authority. + #[must_use] + pub const fn context(&self) -> &'a EnvelopeContext { + self.context + } + /// Returns the provider-wrapped value. #[must_use] pub const fn wrapped(&self) -> &'a WrappedSecret { diff --git a/crates/secrets/tests/envelope_contract.rs b/crates/secrets/tests/envelope_contract.rs @@ -1,4 +1,7 @@ use futures_executor::block_on; +use radroots_secrets::context::{ + EnvelopeContext, EnvelopePurpose, EnvelopeSubject, PayloadSchemaId, +}; use radroots_secrets::envelope::{ Cipher, ENVELOPE_VERSION, KeySource, Nonce, SealMaterial, SealRequest, }; @@ -57,6 +60,15 @@ fn reference() -> SecretRef { ) } +fn context() -> EnvelopeContext { + EnvelopeContext::new( + EnvelopePurpose::parse("radroots.private_artifact").expect("purpose"), + EnvelopeSubject::parse("private_artifact", "01010101010101010101010101010101") + .expect("subject"), + PayloadSchemaId::parse("trade.private_terms.v1").expect("schema"), + ) +} + fn seal(plaintext: &[u8]) -> EncryptedEnvelope { let plaintext = SecretMaterial::from_slice(plaintext).expect("plaintext"); let data_key = SecretMaterial::from_slice(&[0x11; 32]).expect("data key"); @@ -64,6 +76,7 @@ fn seal(plaintext: &[u8]) -> EncryptedEnvelope { &VectorWrapping, SealRequest::new( reference(), + context(), &plaintext, SealMaterial::new(data_key, Nonce::new([0x22; 24])), ), @@ -82,10 +95,10 @@ fn deterministic_envelope_vector_round_trips() { let encoded = envelope.encode().expect("encode"); assert_eq!( hex::encode(&encoded), - "52525331000101010100000007000c656e76656c6f70652d6b6579222222222222222222222222222222222222222222222222000000204b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b00000028f106837e33d690e7c5287abdd815ce9257b7b5b176ea9596abf3b7fe745aec5a8c2487a553d4659d" + "52525331000201010100000007000c656e76656c6f70652d6b65790001726164726f6f74732e656e76656c6f70655f636f6e746578742e76310019726164726f6f74732e707269766174655f61727469666163740010707269766174655f617274696661637400203031303130313031303130313031303130313031303130313031303130313031001674726164652e707269766174655f7465726d732e7631222222222222222222222222222222222222222222222222000000204b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b00000028f106837e33d690e7c5287abdd815ce9257b7b5b176ea9596f762615d1c221c5c10964c4f799d5d59" ); let decoded = EncryptedEnvelope::decode(&encoded).expect("decode"); - let opened = block_on(decoded.open(&VectorWrapping)).expect("open"); + let opened = block_on(decoded.open(&VectorWrapping, &context())).expect("open"); opened.expose_secret(|bytes| assert_eq!(bytes, b"radroots envelope vector")); } @@ -94,11 +107,12 @@ fn tamper_nonce_and_ciphertext_are_rejected() { let encoded = seal(b"authenticated plaintext").encode().expect("encode"); let mut nonce_tamper = encoded.clone(); - let nonce_offset = 4 + 2 + 1 + 1 + 1 + 4 + 2 + "envelope-key".len(); + let nonce_offset = + 4 + 2 + 1 + 1 + 1 + 4 + 2 + "envelope-key".len() + context().to_canonical_bytes().len(); nonce_tamper[nonce_offset] ^= 0x01; let envelope = EncryptedEnvelope::decode(&nonce_tamper).expect("decode nonce tamper"); assert!(matches!( - block_on(envelope.open(&VectorWrapping)), + block_on(envelope.open(&VectorWrapping, &context())), Err(Error::DecryptFailed) )); @@ -107,12 +121,54 @@ fn tamper_nonce_and_ciphertext_are_rejected() { *last ^= 0x01; let envelope = EncryptedEnvelope::decode(&ciphertext_tamper).expect("decode ciphertext tamper"); assert!(matches!( - block_on(envelope.open(&VectorWrapping)), + block_on(envelope.open(&VectorWrapping, &context())), Err(Error::DecryptFailed) )); } #[test] +fn context_mismatch_and_authenticated_context_tamper_fail_closed() { + let encoded = seal(b"context-bound plaintext").encode().expect("encode"); + let different = EnvelopeContext::new( + EnvelopePurpose::parse("radroots.different_artifact").expect("purpose"), + EnvelopeSubject::parse("private_artifact", "01010101010101010101010101010101") + .expect("subject"), + PayloadSchemaId::parse("trade.private_terms.v1").expect("schema"), + ); + let envelope = EncryptedEnvelope::decode(&encoded).expect("decode"); + assert_eq!( + block_on(envelope.open(&VectorWrapping, &different)).err(), + Some(Error::ContextMismatch) + ); + + let purpose = b"radroots.private_artifact"; + let purpose_offset = encoded + .windows(purpose.len()) + .position(|window| window == purpose) + .expect("purpose offset"); + let mut tampered = encoded; + tampered[purpose_offset] = b's'; + let envelope = EncryptedEnvelope::decode(&tampered).expect("valid altered context"); + let altered = envelope.context().expect("v2 context").clone(); + assert_eq!( + block_on(envelope.open(&VectorWrapping, &altered)).err(), + Some(Error::DecryptFailed) + ); +} + +#[test] +fn normal_open_denies_the_frozen_v1_corpus() { + let legacy = hex::decode("52525331000101010100000007000c656e76656c6f70652d6b6579222222222222222222222222222222222222222222222222000000204b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b00000028f106837e33d690e7c5287abdd815ce9257b7b5b176ea9596abf3b7fe745aec5a8c2487a553d4659d").expect("legacy hex"); + let envelope = EncryptedEnvelope::decode(&legacy).expect("legacy decode"); + assert_eq!(envelope.version(), 1); + assert!(envelope.context().is_none()); + assert_eq!( + block_on(envelope.open(&VectorWrapping, &context())).err(), + Some(Error::LegacyEnvelopeDenied) + ); +} + +#[test] fn wrong_key_slot_and_invalid_version_fail_closed() { let encoded = seal(b"slot-bound plaintext").encode().expect("encode"); let id_offset = 4 + 2 + 1 + 1 + 1 + 4 + 2; @@ -120,7 +176,7 @@ fn wrong_key_slot_and_invalid_version_fail_closed() { wrong_slot[id_offset] = b'x'; let envelope = EncryptedEnvelope::decode(&wrong_slot).expect("decode slot tamper"); assert!(matches!( - block_on(envelope.open(&VectorWrapping)), + block_on(envelope.open(&VectorWrapping, &context())), Err(Error::BackendFailure { backend: BackendKind::Memory, operation: Operation::Unwrap, @@ -128,10 +184,10 @@ fn wrong_key_slot_and_invalid_version_fail_closed() { )); let mut bad_version = encoded; - bad_version[5] = 2; + bad_version[5] = 3; assert!(matches!( EncryptedEnvelope::decode(&bad_version), - Err(Error::UnsupportedEnvelopeVersion { version: 2 }) + Err(Error::UnsupportedEnvelopeVersion { version: 3 }) )); } @@ -149,6 +205,7 @@ fn malformed_lengths_and_wrong_data_key_lengths_are_rejected() { &VectorWrapping, SealRequest::new( reference(), + context(), &plaintext, SealMaterial::new(short_key, Nonce::new([0x22; 24])), ), @@ -163,7 +220,7 @@ fn serde_uses_the_validated_binary_envelope() { let envelope = seal(b"serde envelope"); let json = serde_json::to_vec(&envelope).expect("serialize envelope"); let decoded: EncryptedEnvelope = serde_json::from_slice(&json).expect("deserialize envelope"); - let opened = block_on(decoded.open(&VectorWrapping)).expect("open"); + let opened = block_on(decoded.open(&VectorWrapping, &context())).expect("open"); opened.expose_secret(|bytes| assert_eq!(bytes, b"serde envelope")); } diff --git a/crates/secrets/tests/file_adapter.rs b/crates/secrets/tests/file_adapter.rs @@ -1,6 +1,9 @@ #![cfg(feature = "file")] use futures_executor::block_on; +use radroots_secrets::context::{ + EnvelopeContext, EnvelopePurpose, EnvelopeSubject, PayloadSchemaId, +}; use radroots_secrets::envelope::Nonce; use radroots_secrets::file::{FileOpenMode, FileProvider}; use radroots_secrets::id::{BackendKind, KeyVersion}; @@ -16,6 +19,14 @@ fn reference(id: &str, version: u32) -> SecretRef { ) } +fn context() -> EnvelopeContext { + EnvelopeContext::new( + EnvelopePurpose::parse("radroots.file_test").expect("purpose"), + EnvelopeSubject::parse("file_test", "fixture").expect("subject"), + PayloadSchemaId::parse("radroots.file_test.v1").expect("schema"), + ) +} + fn provider(root: &std::path::Path, mode: FileOpenMode) -> FileProvider { FileProvider::open( root, @@ -40,9 +51,11 @@ fn file_provider_round_trips_encrypted_material_with_atomic_creation() { assert!(!persisted.windows(32).any(|window| window == [0x41; 32])); assert_eq!(fs::read_dir(&root).expect("read root").count(), 1); - let wrapped = block_on(provider.wrap(WrapRequest::new(&reference, &material))).expect("wrap"); - let opened = - block_on(provider.unwrap(UnwrapRequest::new(&reference, &wrapped))).expect("unwrap"); + let context = context(); + let wrapped = + block_on(provider.wrap(WrapRequest::new(&reference, &context, &material))).expect("wrap"); + let opened = block_on(provider.unwrap(UnwrapRequest::new(&reference, &context, &wrapped))) + .expect("unwrap"); opened.expose_secret(|bytes| assert_eq!(bytes, &[0x41; 32])); assert_eq!(provider.backend_kind(), BackendKind::File); assert_eq!(format!("{provider:?}"), "FileProvider(<redacted>)"); @@ -75,7 +88,7 @@ fn traversal_existing_replacement_and_truncated_files_fail_closed() { fs::write(root.join("66696c652d6b6579.v1.rrk"), b"truncated").expect("truncate"); assert!(matches!( - block_on(provider.wrap(WrapRequest::new(&reference, &material))), + block_on(provider.wrap(WrapRequest::new(&reference, &context(), &material))), Err(Error::EnvelopeMalformed) )); } @@ -103,8 +116,10 @@ fn rotation_resumes_after_new_version_commit_and_removes_old_version() { assert!(!provider.contains(¤t).expect("old contains")); assert!(provider.contains(&next).expect("new contains")); - let wrapped = block_on(provider.wrap(WrapRequest::new(&next, &next_material))).expect("wrap"); - assert!(block_on(provider.unwrap(UnwrapRequest::new(&next, &wrapped))).is_ok()); + let context = context(); + let wrapped = + block_on(provider.wrap(WrapRequest::new(&next, &context, &next_material))).expect("wrap"); + assert!(block_on(provider.unwrap(UnwrapRequest::new(&next, &context, &wrapped))).is_ok()); } #[cfg(unix)] diff --git a/crates/secrets/tests/memory_adapter.rs b/crates/secrets/tests/memory_adapter.rs @@ -1,6 +1,9 @@ #![cfg(feature = "memory")] use futures_executor::block_on; +use radroots_secrets::context::{ + EnvelopeContext, EnvelopePurpose, EnvelopeSubject, PayloadSchemaId, +}; use radroots_secrets::id::{BackendKind, KeyVersion}; use radroots_secrets::memory::MemoryProvider; use radroots_secrets::wrapping::{SecretMaterial, UnwrapRequest, WrapRequest}; @@ -14,6 +17,14 @@ fn reference(id: &str, version: u32) -> SecretRef { ) } +fn context() -> EnvelopeContext { + EnvelopeContext::new( + EnvelopePurpose::parse("radroots.memory_test").expect("purpose"), + EnvelopeSubject::parse("memory_test", "fixture").expect("subject"), + PayloadSchemaId::parse("radroots.memory_test.v1").expect("schema"), + ) +} + #[test] fn memory_provider_has_explicit_lifecycle_and_round_trips() { let provider = MemoryProvider::new(); @@ -29,9 +40,11 @@ fn memory_provider_has_explicit_lifecycle_and_round_trips() { assert!(provider.contains(&reference).expect("contains")); let plaintext = SecretMaterial::from_slice(&[0x41; 32]).expect("material"); - let wrapped = block_on(provider.wrap(WrapRequest::new(&reference, &plaintext))).expect("wrap"); - let opened = - block_on(provider.unwrap(UnwrapRequest::new(&reference, &wrapped))).expect("unwrap"); + let context = context(); + let wrapped = + block_on(provider.wrap(WrapRequest::new(&reference, &context, &plaintext))).expect("wrap"); + let opened = block_on(provider.unwrap(UnwrapRequest::new(&reference, &context, &wrapped))) + .expect("unwrap"); opened.expose_secret(|bytes| assert_eq!(bytes, &[0x41; 32])); assert!(provider.remove(&reference).expect("remove")); @@ -45,7 +58,7 @@ fn missing_and_mismatched_material_fail_closed() { let reference = reference("missing-key", 1); let plaintext = SecretMaterial::from_slice(&[0x11; 32]).expect("material"); assert!(matches!( - block_on(provider.wrap(WrapRequest::new(&reference, &plaintext))), + block_on(provider.wrap(WrapRequest::new(&reference, &context(), &plaintext))), Err(Error::SecretNotFound { backend: BackendKind::Memory, key_version: 1, @@ -59,7 +72,7 @@ fn missing_and_mismatched_material_fail_closed() { ) .expect("provision"); assert!(matches!( - block_on(provider.wrap(WrapRequest::new(&reference, &plaintext))), + block_on(provider.wrap(WrapRequest::new(&reference, &context(), &plaintext))), Err(Error::BackendFailure { .. }) )); assert!(matches!( @@ -96,7 +109,7 @@ fn rotation_is_monotonic_atomic_and_invalidates_the_old_version() { assert!(!provider.contains(¤t).expect("old contains")); assert!(provider.contains(&next).expect("new contains")); let new_material = SecretMaterial::from_slice(&[0x22; 32]).expect("material"); - assert!(block_on(provider.wrap(WrapRequest::new(&next, &new_material))).is_ok()); + assert!(block_on(provider.wrap(WrapRequest::new(&next, &context(), &new_material))).is_ok()); let invalid = reference("different-key", 3); assert_eq!( diff --git a/crates/secrets/tests/package_boundary.rs b/crates/secrets/tests/package_boundary.rs @@ -42,6 +42,7 @@ fn manifest_has_final_identity_features_and_no_radroots_dependencies() { "chacha20poly1305", "keyring", "serde", + "sha2", "tempfile", "zeroize" ]) @@ -129,7 +130,7 @@ fn package_documentation_covers_the_security_and_host_contract() { "provider.provision(", "EncryptedEnvelope::seal", "EncryptedEnvelope::decode", - "decoded.open(&provider)", + "decoded.open(&provider, &context)", ] { assert!( EXAMPLE.contains(required), diff --git a/crates/secrets/tests/provider_contract.rs b/crates/secrets/tests/provider_contract.rs @@ -1,4 +1,7 @@ use futures_executor::block_on; +use radroots_secrets::context::{ + EnvelopeContext, EnvelopePurpose, EnvelopeSubject, PayloadSchemaId, +}; use radroots_secrets::error::{Operation, PolicyRequirement}; use radroots_secrets::id::{BackendKind, KeyVersion}; use radroots_secrets::provider::{ @@ -94,6 +97,14 @@ fn reference(backend: BackendKind) -> SecretRef { ) } +fn context() -> EnvelopeContext { + EnvelopeContext::new( + EnvelopePurpose::parse("radroots.provider_test").expect("purpose"), + EnvelopeSubject::parse("provider_test", "fixture").expect("subject"), + PayloadSchemaId::parse("radroots.provider_test.v1").expect("schema"), + ) +} + #[test] fn provider_traits_are_dyn_compatible_and_round_trip_opaque_material() { fn accept_dyn(_: &dyn SecretProvider) {} @@ -110,9 +121,11 @@ fn provider_traits_are_dyn_compatible_and_round_trip_opaque_material() { let reference = reference(BackendKind::Memory); let plaintext = SecretMaterial::from_slice(b"caller-owned-data-key").expect("material"); - let wrapped = block_on(provider.wrap(WrapRequest::new(&reference, &plaintext))).expect("wrap"); - let opened = - block_on(provider.unwrap(UnwrapRequest::new(&reference, &wrapped))).expect("unwrap"); + let context = context(); + let wrapped = + block_on(provider.wrap(WrapRequest::new(&reference, &context, &plaintext))).expect("wrap"); + let opened = block_on(provider.unwrap(UnwrapRequest::new(&reference, &context, &wrapped))) + .expect("unwrap"); opened.expose_secret(|bytes| assert_eq!(bytes, b"caller-owned-data-key")); assert_eq!(format!("{plaintext:?}"), "SecretMaterial(<redacted>)"); @@ -217,7 +230,7 @@ fn provider_errors_are_normalized_and_secret_safe() { }; let reference = reference(BackendKind::External); let plaintext = SecretMaterial::from_slice(b"must-not-appear").expect("material"); - let error = block_on(provider.wrap(WrapRequest::new(&reference, &plaintext))) + let error = block_on(provider.wrap(WrapRequest::new(&reference, &context(), &plaintext))) .expect_err("backend failure"); assert_eq!( error, @@ -243,7 +256,7 @@ fn reference_backend_mismatch_fails_before_wrapping() { let reference = reference(BackendKind::File); let plaintext = SecretMaterial::from_slice(b"data-key").expect("material"); assert_eq!( - block_on(provider.wrap(WrapRequest::new(&reference, &plaintext))), + block_on(provider.wrap(WrapRequest::new(&reference, &context(), &plaintext))), Err(Error::BackendMismatch { provider: BackendKind::Memory, reference: BackendKind::File, diff --git a/crates/secrets/tests/security_contract.rs b/crates/secrets/tests/security_contract.rs @@ -1,3 +1,6 @@ +use radroots_secrets::context::{ + EnvelopeContext, EnvelopePurpose, EnvelopeSubject, PayloadSchemaId, +}; use radroots_secrets::envelope::{Nonce, SealMaterial, SealRequest}; use radroots_secrets::error::{Operation, SecretIdError}; use radroots_secrets::id::{BackendKind, KeyVersion}; @@ -49,6 +52,8 @@ fn reviewed_api_forbids_secret_bearing_clone_serialize_and_byte_access() { "SecretMaterial::into_bytes", "SecretMaterial::to_vec", "SecretRef::clone", + "EncryptedEnvelope::open(&self, &dyn radroots_secrets::wrapping::KeyWrapping) ->", + "SealRequest<'a>::new(radroots_secrets::id::SecretRef, &'a radroots_secrets::wrapping::SecretMaterial", ] { assert!( !PUBLIC_API.contains(forbidden), @@ -98,8 +103,14 @@ fn diagnostics_snapshot_is_redacted_and_plaintext_free() { let wrapped = WrappedSecret::from_bytes(b"wrapped-material-sentinel".to_vec()) .expect("valid wrapped material"); let sealing_key = SecretMaterial::from_slice(&[0x42; 32]).expect("valid sealing key"); + let context = EnvelopeContext::new( + EnvelopePurpose::parse("radroots.security_test").expect("purpose"), + EnvelopeSubject::parse("security_test", "plaintext-secret-id-sentinel").expect("subject"), + PayloadSchemaId::parse("radroots.security_test.v1").expect("schema"), + ); let request = SealRequest::new( reference, + context, &plaintext, SealMaterial::new(sealing_key, Nonce::new([0x24; 24])), ); diff --git a/docs/api/radroots_secrets.txt b/docs/api/radroots_secrets.txt @@ -2,6 +2,7 @@ pub mod radroots_secrets pub mod radroots_secrets::context pub struct radroots_secrets::context::EnvelopeContext impl radroots_secrets::context::EnvelopeContext +pub fn radroots_secrets::context::EnvelopeContext::authentication_digest(&self) -> [u8; 32] pub const fn radroots_secrets::context::EnvelopeContext::new(radroots_secrets::context::EnvelopePurpose, radroots_secrets::context::EnvelopeSubject, radroots_secrets::context::PayloadSchemaId) -> Self pub const fn radroots_secrets::context::EnvelopeContext::payload_schema(&self) -> &radroots_secrets::context::PayloadSchemaId pub const fn radroots_secrets::context::EnvelopeContext::purpose(&self) -> &radroots_secrets::context::EnvelopePurpose @@ -52,10 +53,11 @@ pub radroots_secrets::envelope::KeySource::ProviderWrapped pub struct radroots_secrets::envelope::EncryptedEnvelope impl radroots_secrets::envelope::EncryptedEnvelope pub const fn radroots_secrets::envelope::EncryptedEnvelope::cipher(&self) -> radroots_secrets::envelope::Cipher +pub const fn radroots_secrets::envelope::EncryptedEnvelope::context(&self) -> core::option::Option<&radroots_secrets::context::EnvelopeContext> pub fn radroots_secrets::envelope::EncryptedEnvelope::decode(&[u8]) -> core::result::Result<Self, radroots_secrets::error::Error> pub fn radroots_secrets::envelope::EncryptedEnvelope::encode(&self) -> core::result::Result<alloc::vec::Vec<u8>, radroots_secrets::error::Error> pub const fn radroots_secrets::envelope::EncryptedEnvelope::key_source(&self) -> radroots_secrets::envelope::KeySource -pub async fn radroots_secrets::envelope::EncryptedEnvelope::open(&self, &dyn radroots_secrets::wrapping::KeyWrapping) -> core::result::Result<radroots_secrets::wrapping::SecretMaterial, radroots_secrets::error::Error> +pub async fn radroots_secrets::envelope::EncryptedEnvelope::open(&self, &dyn radroots_secrets::wrapping::KeyWrapping, &radroots_secrets::context::EnvelopeContext) -> core::result::Result<radroots_secrets::wrapping::SecretMaterial, radroots_secrets::error::Error> pub const fn radroots_secrets::envelope::EncryptedEnvelope::reference(&self) -> &radroots_secrets::id::SecretRef pub async fn radroots_secrets::envelope::EncryptedEnvelope::seal(&dyn radroots_secrets::wrapping::KeyWrapping, radroots_secrets::envelope::SealRequest<'_>) -> core::result::Result<Self, radroots_secrets::error::Error> pub const fn radroots_secrets::envelope::EncryptedEnvelope::version(&self) -> u16 @@ -78,11 +80,12 @@ impl core::fmt::Debug for radroots_secrets::envelope::SealMaterial pub fn radroots_secrets::envelope::SealMaterial::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result pub struct radroots_secrets::envelope::SealRequest<'a> impl<'a> radroots_secrets::envelope::SealRequest<'a> -pub const fn radroots_secrets::envelope::SealRequest<'a>::new(radroots_secrets::id::SecretRef, &'a radroots_secrets::wrapping::SecretMaterial, radroots_secrets::envelope::SealMaterial) -> Self +pub const fn radroots_secrets::envelope::SealRequest<'a>::new(radroots_secrets::id::SecretRef, radroots_secrets::context::EnvelopeContext, &'a radroots_secrets::wrapping::SecretMaterial, radroots_secrets::envelope::SealMaterial) -> Self impl core::fmt::Debug for radroots_secrets::envelope::SealRequest<'_> pub fn radroots_secrets::envelope::SealRequest<'_>::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result pub const radroots_secrets::envelope::ENVELOPE_MAX_BYTES: usize pub const radroots_secrets::envelope::ENVELOPE_VERSION: u16 +pub const radroots_secrets::envelope::LEGACY_ENVELOPE_VERSION: u16 pub mod radroots_secrets::error #[non_exhaustive] pub enum radroots_secrets::error::ContextField pub radroots_secrets::error::ContextField::PayloadSchema @@ -106,6 +109,7 @@ pub radroots_secrets::error::Error::BackendMismatch::provider: radroots_secrets: pub radroots_secrets::error::Error::BackendMismatch::reference: radroots_secrets::id::BackendKind pub radroots_secrets::error::Error::BackendUnavailable pub radroots_secrets::error::Error::BackendUnavailable::backend: radroots_secrets::id::BackendKind +pub radroots_secrets::error::Error::ContextMismatch pub radroots_secrets::error::Error::DecryptFailed pub radroots_secrets::error::Error::EncryptFailed pub radroots_secrets::error::Error::EnvelopeMalformed @@ -128,6 +132,7 @@ pub radroots_secrets::error::Error::InvalidServiceName pub radroots_secrets::error::Error::InvalidWrappedLength pub radroots_secrets::error::Error::InvalidWrappedLength::actual_bytes: usize pub radroots_secrets::error::Error::InvalidWrappedLength::max_bytes: usize +pub radroots_secrets::error::Error::LegacyEnvelopeDenied pub radroots_secrets::error::Error::PolicyUnsupported pub radroots_secrets::error::Error::PolicyUnsupported::backend: radroots_secrets::id::BackendKind pub radroots_secrets::error::Error::PolicyUnsupported::requirement: radroots_secrets::error::PolicyRequirement @@ -142,6 +147,8 @@ pub radroots_secrets::error::Error::UnsupportedBackend pub radroots_secrets::error::Error::UnsupportedBackend::backend: u8 pub radroots_secrets::error::Error::UnsupportedCipher pub radroots_secrets::error::Error::UnsupportedCipher::cipher: u8 +pub radroots_secrets::error::Error::UnsupportedContextVersion +pub radroots_secrets::error::Error::UnsupportedContextVersion::version: u16 pub radroots_secrets::error::Error::UnsupportedEnvelopeVersion pub radroots_secrets::error::Error::UnsupportedEnvelopeVersion::version: u16 pub radroots_secrets::error::Error::UnsupportedKeySource @@ -312,12 +319,14 @@ impl core::fmt::Debug for radroots_secrets::wrapping::SecretMaterial pub fn radroots_secrets::wrapping::SecretMaterial::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result pub struct radroots_secrets::wrapping::UnwrapRequest<'a> impl<'a> radroots_secrets::wrapping::UnwrapRequest<'a> -pub const fn radroots_secrets::wrapping::UnwrapRequest<'a>::new(&'a radroots_secrets::id::SecretRef, &'a radroots_secrets::wrapping::WrappedSecret) -> Self +pub const fn radroots_secrets::wrapping::UnwrapRequest<'a>::context(&self) -> &'a radroots_secrets::context::EnvelopeContext +pub const fn radroots_secrets::wrapping::UnwrapRequest<'a>::new(&'a radroots_secrets::id::SecretRef, &'a radroots_secrets::context::EnvelopeContext, &'a radroots_secrets::wrapping::WrappedSecret) -> Self pub const fn radroots_secrets::wrapping::UnwrapRequest<'a>::reference(&self) -> &'a radroots_secrets::id::SecretRef pub const fn radroots_secrets::wrapping::UnwrapRequest<'a>::wrapped(&self) -> &'a radroots_secrets::wrapping::WrappedSecret pub struct radroots_secrets::wrapping::WrapRequest<'a> impl<'a> radroots_secrets::wrapping::WrapRequest<'a> -pub const fn radroots_secrets::wrapping::WrapRequest<'a>::new(&'a radroots_secrets::id::SecretRef, &'a radroots_secrets::wrapping::SecretMaterial) -> Self +pub const fn radroots_secrets::wrapping::WrapRequest<'a>::context(&self) -> &'a radroots_secrets::context::EnvelopeContext +pub const fn radroots_secrets::wrapping::WrapRequest<'a>::new(&'a radroots_secrets::id::SecretRef, &'a radroots_secrets::context::EnvelopeContext, &'a radroots_secrets::wrapping::SecretMaterial) -> Self pub const fn radroots_secrets::wrapping::WrapRequest<'a>::plaintext(&self) -> &'a radroots_secrets::wrapping::SecretMaterial pub const fn radroots_secrets::wrapping::WrapRequest<'a>::reference(&self) -> &'a radroots_secrets::id::SecretRef pub struct radroots_secrets::wrapping::WrappedSecret(_) @@ -350,6 +359,7 @@ pub radroots_secrets::Error::BackendMismatch::provider: radroots_secrets::id::Ba pub radroots_secrets::Error::BackendMismatch::reference: radroots_secrets::id::BackendKind pub radroots_secrets::Error::BackendUnavailable pub radroots_secrets::Error::BackendUnavailable::backend: radroots_secrets::id::BackendKind +pub radroots_secrets::Error::ContextMismatch pub radroots_secrets::Error::DecryptFailed pub radroots_secrets::Error::EncryptFailed pub radroots_secrets::Error::EnvelopeMalformed @@ -372,6 +382,7 @@ pub radroots_secrets::Error::InvalidServiceName pub radroots_secrets::Error::InvalidWrappedLength pub radroots_secrets::Error::InvalidWrappedLength::actual_bytes: usize pub radroots_secrets::Error::InvalidWrappedLength::max_bytes: usize +pub radroots_secrets::Error::LegacyEnvelopeDenied pub radroots_secrets::Error::PolicyUnsupported pub radroots_secrets::Error::PolicyUnsupported::backend: radroots_secrets::id::BackendKind pub radroots_secrets::Error::PolicyUnsupported::requirement: radroots_secrets::error::PolicyRequirement @@ -386,6 +397,8 @@ pub radroots_secrets::Error::UnsupportedBackend pub radroots_secrets::Error::UnsupportedBackend::backend: u8 pub radroots_secrets::Error::UnsupportedCipher pub radroots_secrets::Error::UnsupportedCipher::cipher: u8 +pub radroots_secrets::Error::UnsupportedContextVersion +pub radroots_secrets::Error::UnsupportedContextVersion::version: u16 pub radroots_secrets::Error::UnsupportedEnvelopeVersion pub radroots_secrets::Error::UnsupportedEnvelopeVersion::version: u16 pub radroots_secrets::Error::UnsupportedKeySource @@ -396,10 +409,11 @@ pub fn radroots_secrets::error::Error::fmt(&self, &mut core::fmt::Formatter<'_>) pub struct radroots_secrets::EncryptedEnvelope impl radroots_secrets::envelope::EncryptedEnvelope pub const fn radroots_secrets::envelope::EncryptedEnvelope::cipher(&self) -> radroots_secrets::envelope::Cipher +pub const fn radroots_secrets::envelope::EncryptedEnvelope::context(&self) -> core::option::Option<&radroots_secrets::context::EnvelopeContext> pub fn radroots_secrets::envelope::EncryptedEnvelope::decode(&[u8]) -> core::result::Result<Self, radroots_secrets::error::Error> pub fn radroots_secrets::envelope::EncryptedEnvelope::encode(&self) -> core::result::Result<alloc::vec::Vec<u8>, radroots_secrets::error::Error> pub const fn radroots_secrets::envelope::EncryptedEnvelope::key_source(&self) -> radroots_secrets::envelope::KeySource -pub async fn radroots_secrets::envelope::EncryptedEnvelope::open(&self, &dyn radroots_secrets::wrapping::KeyWrapping) -> core::result::Result<radroots_secrets::wrapping::SecretMaterial, radroots_secrets::error::Error> +pub async fn radroots_secrets::envelope::EncryptedEnvelope::open(&self, &dyn radroots_secrets::wrapping::KeyWrapping, &radroots_secrets::context::EnvelopeContext) -> core::result::Result<radroots_secrets::wrapping::SecretMaterial, radroots_secrets::error::Error> pub const fn radroots_secrets::envelope::EncryptedEnvelope::reference(&self) -> &radroots_secrets::id::SecretRef pub async fn radroots_secrets::envelope::EncryptedEnvelope::seal(&dyn radroots_secrets::wrapping::KeyWrapping, radroots_secrets::envelope::SealRequest<'_>) -> core::result::Result<Self, radroots_secrets::error::Error> pub const fn radroots_secrets::envelope::EncryptedEnvelope::version(&self) -> u16