commit 5f094b213b982a2e1693618d202c9e2058a2c65f
parent 5edb693348d8656e844f4fad5a89be32dd0aa5e6
Author: triesap <tyson@radroots.org>
Date: Wed, 5 Aug 2026 17:28:44 +0000
feat(secrets): add authenticated envelope contexts
Diffstat:
7 files changed, 563 insertions(+), 1 deletion(-)
diff --git a/crates/secrets/src/context.rs b/crates/secrets/src/context.rs
@@ -0,0 +1,341 @@
+//! Validated semantic authority for encrypted envelopes.
+
+use crate::error::{ContextField, ContextValueError, Error};
+use alloc::string::String;
+use alloc::vec::Vec;
+use core::fmt;
+
+/// Version of the canonical authenticated context encoding.
+pub const ENVELOPE_CONTEXT_VERSION: u16 = 1;
+/// Domain separator included in every canonical context encoding.
+pub const ENVELOPE_CONTEXT_DOMAIN: &[u8] = b"radroots.envelope_context.v1";
+/// Maximum UTF-8 length of a purpose identifier.
+pub const ENVELOPE_PURPOSE_MAX_BYTES: usize = 128;
+/// Maximum UTF-8 length of a subject type discriminator.
+pub const ENVELOPE_SUBJECT_TYPE_MAX_BYTES: usize = 64;
+/// Maximum length of a subject's canonical bytes.
+pub const ENVELOPE_SUBJECT_VALUE_MAX_BYTES: usize = 128;
+/// Maximum UTF-8 length of a payload schema identifier.
+pub const PAYLOAD_SCHEMA_MAX_BYTES: usize = 128;
+
+/// Validated, namespaced use-case identifier for protected plaintext.
+#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
+pub struct EnvelopePurpose(String);
+
+impl EnvelopePurpose {
+ /// Parses a canonical lower-case namespaced purpose.
+ pub fn parse(value: impl Into<String>) -> Result<Self, Error> {
+ let value = value.into();
+ validate_namespaced(
+ value.as_str(),
+ ENVELOPE_PURPOSE_MAX_BYTES,
+ ContextField::Purpose,
+ )?;
+ Ok(Self(value))
+ }
+
+ /// Returns the validated identifier.
+ #[must_use]
+ pub fn as_str(&self) -> &str {
+ self.0.as_str()
+ }
+}
+
+impl fmt::Debug for EnvelopePurpose {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter.write_str("EnvelopePurpose(<validated>)")
+ }
+}
+
+impl fmt::Display for EnvelopePurpose {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter.write_str("<validated envelope purpose>")
+ }
+}
+
+/// Validated, typed identity of the protected object.
+#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
+pub struct EnvelopeSubject {
+ subject_type: String,
+ value: String,
+}
+
+impl EnvelopeSubject {
+ /// Parses a subject type and its canonical, non-secret identity bytes.
+ pub fn parse(subject_type: impl Into<String>, value: impl Into<String>) -> Result<Self, Error> {
+ let subject_type = subject_type.into();
+ let value = value.into();
+ validate_label(
+ subject_type.as_str(),
+ ENVELOPE_SUBJECT_TYPE_MAX_BYTES,
+ ContextField::SubjectType,
+ )?;
+ validate_subject_value(value.as_str())?;
+ Ok(Self {
+ subject_type,
+ value,
+ })
+ }
+
+ /// Returns the validated type discriminator.
+ #[must_use]
+ pub fn subject_type(&self) -> &str {
+ self.subject_type.as_str()
+ }
+
+ /// Returns the validated canonical subject value.
+ #[must_use]
+ pub fn value(&self) -> &str {
+ self.value.as_str()
+ }
+}
+
+impl fmt::Debug for EnvelopeSubject {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter
+ .debug_struct("EnvelopeSubject")
+ .field("subject_type", &self.subject_type)
+ .field("value", &"<redacted>")
+ .finish()
+ }
+}
+
+impl fmt::Display for EnvelopeSubject {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(formatter, "{}:<redacted>", self.subject_type)
+ }
+}
+
+/// Validated, version-bearing schema identifier for protected plaintext.
+#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
+pub struct PayloadSchemaId(String);
+
+impl PayloadSchemaId {
+ /// Parses a canonical lower-case namespaced payload schema identifier.
+ pub fn parse(value: impl Into<String>) -> Result<Self, Error> {
+ let value = value.into();
+ validate_namespaced(
+ value.as_str(),
+ PAYLOAD_SCHEMA_MAX_BYTES,
+ ContextField::PayloadSchema,
+ )?;
+ Ok(Self(value))
+ }
+
+ /// Returns the validated identifier.
+ #[must_use]
+ pub fn as_str(&self) -> &str {
+ self.0.as_str()
+ }
+}
+
+impl fmt::Debug for PayloadSchemaId {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter.write_str("PayloadSchemaId(<validated>)")
+ }
+}
+
+impl fmt::Display for PayloadSchemaId {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter.write_str("<validated payload schema>")
+ }
+}
+
+/// Independently validated semantic authority authenticated by an envelope.
+#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
+pub struct EnvelopeContext {
+ purpose: EnvelopePurpose,
+ subject: EnvelopeSubject,
+ payload_schema: PayloadSchemaId,
+}
+
+impl EnvelopeContext {
+ /// Constructs a context only from independently validated parts.
+ #[must_use]
+ pub const fn new(
+ purpose: EnvelopePurpose,
+ subject: EnvelopeSubject,
+ payload_schema: PayloadSchemaId,
+ ) -> Self {
+ Self {
+ purpose,
+ subject,
+ payload_schema,
+ }
+ }
+
+ /// Returns the authenticated use-case identifier.
+ #[must_use]
+ pub const fn purpose(&self) -> &EnvelopePurpose {
+ &self.purpose
+ }
+
+ /// Returns the authenticated typed subject.
+ #[must_use]
+ pub const fn subject(&self) -> &EnvelopeSubject {
+ &self.subject
+ }
+
+ /// Returns the authenticated payload schema identifier.
+ #[must_use]
+ pub const fn payload_schema(&self) -> &PayloadSchemaId {
+ &self.payload_schema
+ }
+
+ /// Encodes the deterministic context wire representation used by envelope v2.
+ #[must_use]
+ pub fn to_canonical_bytes(&self) -> Vec<u8> {
+ let purpose = self.purpose.as_str().as_bytes();
+ let subject_type = self.subject.subject_type().as_bytes();
+ let subject_value = self.subject.value().as_bytes();
+ let payload_schema = self.payload_schema.as_str().as_bytes();
+ let mut encoded = Vec::with_capacity(
+ 2 + ENVELOPE_CONTEXT_DOMAIN.len()
+ + 2
+ + purpose.len()
+ + 2
+ + subject_type.len()
+ + 2
+ + subject_value.len()
+ + 2
+ + payload_schema.len(),
+ );
+ encoded.extend_from_slice(&ENVELOPE_CONTEXT_VERSION.to_be_bytes());
+ encoded.extend_from_slice(ENVELOPE_CONTEXT_DOMAIN);
+ push_bounded(&mut encoded, purpose);
+ push_bounded(&mut encoded, subject_type);
+ push_bounded(&mut encoded, subject_value);
+ push_bounded(&mut encoded, payload_schema);
+ encoded
+ }
+}
+
+impl fmt::Debug for EnvelopeContext {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter
+ .debug_struct("EnvelopeContext")
+ .field("purpose", &self.purpose)
+ .field("subject", &self.subject)
+ .field("payload_schema", &self.payload_schema)
+ .finish()
+ }
+}
+
+fn push_bounded(encoded: &mut Vec<u8>, value: &[u8]) {
+ let length = u16::try_from(value.len()).unwrap_or_else(|_| {
+ unreachable!("validated envelope context fields are bounded below u16::MAX")
+ });
+ encoded.extend_from_slice(&length.to_be_bytes());
+ encoded.extend_from_slice(value);
+}
+
+fn validate_namespaced(value: &str, max: usize, field: ContextField) -> Result<(), Error> {
+ validate_length(value, max, field)?;
+ if !value.contains('.') || !value.split('.').all(valid_segment) {
+ return Err(invalid(field, ContextValueError::NonCanonical));
+ }
+ Ok(())
+}
+
+fn validate_label(value: &str, max: usize, field: ContextField) -> Result<(), Error> {
+ validate_length(value, max, field)?;
+ if !valid_segment(value) {
+ return Err(invalid(field, ContextValueError::NonCanonical));
+ }
+ Ok(())
+}
+
+fn validate_subject_value(value: &str) -> Result<(), Error> {
+ validate_length(
+ value,
+ ENVELOPE_SUBJECT_VALUE_MAX_BYTES,
+ ContextField::SubjectValue,
+ )?;
+ if !value.bytes().all(|byte| {
+ byte.is_ascii_lowercase()
+ || byte.is_ascii_digit()
+ || matches!(byte, b'_' | b'-' | b'.' | b':' | b'/')
+ }) {
+ return Err(invalid(
+ ContextField::SubjectValue,
+ ContextValueError::NonCanonical,
+ ));
+ }
+ Ok(())
+}
+
+fn validate_length(value: &str, max: usize, field: ContextField) -> Result<(), Error> {
+ if value.is_empty() {
+ return Err(invalid(field, ContextValueError::Empty));
+ }
+ if value.len() > max {
+ return Err(invalid(
+ field,
+ ContextValueError::TooLong {
+ actual_bytes: value.len(),
+ max_bytes: max,
+ },
+ ));
+ }
+ if !value.is_ascii() || value != value.trim() || value.chars().any(char::is_control) {
+ return Err(invalid(field, ContextValueError::NonCanonical));
+ }
+ Ok(())
+}
+
+fn valid_segment(segment: &str) -> bool {
+ let mut bytes = segment.bytes();
+ matches!(bytes.next(), Some(first) if first.is_ascii_lowercase())
+ && bytes.all(|byte| {
+ byte.is_ascii_lowercase() || byte.is_ascii_digit() || matches!(byte, b'_' | b'-')
+ })
+}
+
+const fn invalid(field: ContextField, reason: ContextValueError) -> Error {
+ Error::InvalidContextValue { field, reason }
+}
+
+#[cfg(feature = "serde")]
+mod serde_impl {
+ use super::{EnvelopeContext, EnvelopePurpose, EnvelopeSubject, PayloadSchemaId};
+ use alloc::string::String;
+
+ #[derive(serde::Serialize, serde::Deserialize)]
+ #[serde(deny_unknown_fields)]
+ struct WireContext {
+ purpose: String,
+ subject_type: String,
+ subject: String,
+ payload_schema: String,
+ }
+
+ impl serde::Serialize for EnvelopeContext {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: serde::Serializer,
+ {
+ WireContext {
+ purpose: String::from(self.purpose().as_str()),
+ subject_type: String::from(self.subject().subject_type()),
+ subject: String::from(self.subject().value()),
+ payload_schema: String::from(self.payload_schema().as_str()),
+ }
+ .serialize(serializer)
+ }
+ }
+
+ impl<'de> serde::Deserialize<'de> for EnvelopeContext {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: serde::Deserializer<'de>,
+ {
+ let wire = WireContext::deserialize(deserializer)?;
+ Ok(Self::new(
+ EnvelopePurpose::parse(wire.purpose).map_err(serde::de::Error::custom)?,
+ EnvelopeSubject::parse(wire.subject_type, wire.subject)
+ .map_err(serde::de::Error::custom)?,
+ PayloadSchemaId::parse(wire.payload_schema).map_err(serde::de::Error::custom)?,
+ ))
+ }
+ }
+}
diff --git a/crates/secrets/src/error.rs b/crates/secrets/src/error.rs
@@ -23,6 +23,37 @@ pub enum SecretIdError {
},
}
+/// Authenticated context field rejected by validation.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+#[non_exhaustive]
+pub enum ContextField {
+ /// Envelope use-case identifier.
+ Purpose,
+ /// Subject type discriminator.
+ SubjectType,
+ /// Canonical subject identity.
+ SubjectValue,
+ /// Plaintext schema identifier.
+ PayloadSchema,
+}
+
+/// Secret-safe reason an authenticated context field was rejected.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+#[non_exhaustive]
+pub enum ContextValueError {
+ /// The field was empty.
+ Empty,
+ /// The field exceeded its explicit bound.
+ TooLong {
+ /// Observed UTF-8 byte length.
+ actual_bytes: usize,
+ /// Maximum accepted UTF-8 byte length.
+ max_bytes: usize,
+ },
+ /// The field did not use its canonical portable representation.
+ NonCanonical,
+}
+
/// A security property requested by a host but unsupported by a provider.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
@@ -63,6 +94,13 @@ pub enum Operation {
pub enum Error {
/// A secret identifier failed validation.
InvalidSecretId(SecretIdError),
+ /// An authenticated envelope context field failed validation.
+ InvalidContextValue {
+ /// Rejected semantic field without its value.
+ field: ContextField,
+ /// Secret-safe validation class.
+ reason: ContextValueError,
+ },
/// Key versions start at one; zero is never a valid version.
InvalidKeyVersion,
/// Secret material was empty or exceeded the bounded input limit.
@@ -190,6 +228,9 @@ impl fmt::Display for Error {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidSecretId(reason) => reason.fmt(formatter),
+ Self::InvalidContextValue { field, reason } => {
+ write!(formatter, "envelope context {field:?} is {reason}")
+ }
Self::InvalidKeyVersion => formatter.write_str("secret key version must be non-zero"),
Self::InvalidSecretLength {
actual_bytes,
@@ -290,6 +331,22 @@ impl fmt::Display for Error {
}
}
+impl fmt::Display for ContextValueError {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Self::Empty => formatter.write_str("empty"),
+ Self::TooLong {
+ actual_bytes,
+ max_bytes,
+ } => write!(
+ formatter,
+ "too long ({actual_bytes} bytes; maximum is {max_bytes})"
+ ),
+ Self::NonCanonical => formatter.write_str("not canonical"),
+ }
+ }
+}
+
#[cfg(feature = "std")]
impl std::error::Error for Error {}
@@ -312,6 +369,10 @@ mod tests {
}
let errors = [
Error::InvalidSecretId(SecretIdError::Empty),
+ Error::InvalidContextValue {
+ field: ContextField::SubjectValue,
+ reason: ContextValueError::NonCanonical,
+ },
Error::InvalidKeyVersion,
Error::InvalidSecretLength {
actual_bytes: 0,
diff --git a/crates/secrets/src/lib.rs b/crates/secrets/src/lib.rs
@@ -6,6 +6,7 @@
extern crate alloc;
+pub mod context;
pub mod envelope;
pub mod error;
#[cfg(feature = "file")]
diff --git a/crates/secrets/tests/context_contract.rs b/crates/secrets/tests/context_contract.rs
@@ -0,0 +1,93 @@
+use radroots_secrets::Error;
+use radroots_secrets::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 radroots_secrets::error::{ContextField, ContextValueError};
+
+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"),
+ )
+}
+
+#[test]
+fn context_encoding_is_canonical_and_domain_separated() {
+ let encoded = context().to_canonical_bytes();
+ assert_eq!(&encoded[..2], &ENVELOPE_CONTEXT_VERSION.to_be_bytes());
+ assert_eq!(
+ &encoded[2..2 + ENVELOPE_CONTEXT_DOMAIN.len()],
+ ENVELOPE_CONTEXT_DOMAIN
+ );
+ assert_eq!(encoded, context().to_canonical_bytes());
+}
+
+#[test]
+fn context_parts_accept_exact_boundaries() {
+ let purpose = format!("a.{}", "b".repeat(ENVELOPE_PURPOSE_MAX_BYTES - 2));
+ let subject_type = format!("a{}", "b".repeat(ENVELOPE_SUBJECT_TYPE_MAX_BYTES - 1));
+ let subject_value = "a".repeat(ENVELOPE_SUBJECT_VALUE_MAX_BYTES);
+ let schema = format!("a.{}", "b".repeat(PAYLOAD_SCHEMA_MAX_BYTES - 2));
+ assert!(EnvelopePurpose::parse(purpose).is_ok());
+ assert!(EnvelopeSubject::parse(subject_type, subject_value).is_ok());
+ assert!(PayloadSchemaId::parse(schema).is_ok());
+}
+
+#[test]
+fn invalid_context_is_rejected_without_echoing_values() {
+ let cases = [
+ (
+ EnvelopePurpose::parse("").err(),
+ ContextField::Purpose,
+ ContextValueError::Empty,
+ ),
+ (
+ EnvelopePurpose::parse("Not.namespaced").err(),
+ ContextField::Purpose,
+ ContextValueError::NonCanonical,
+ ),
+ (
+ EnvelopeSubject::parse("private artifact", "subject").err(),
+ ContextField::SubjectType,
+ ContextValueError::NonCanonical,
+ ),
+ (
+ EnvelopeSubject::parse("private_artifact", "SUBJECT-SECRET").err(),
+ ContextField::SubjectValue,
+ ContextValueError::NonCanonical,
+ ),
+ (
+ PayloadSchemaId::parse("schema\n.v1").err(),
+ ContextField::PayloadSchema,
+ ContextValueError::NonCanonical,
+ ),
+ ];
+ for (error, field, reason) in cases {
+ let error = error.expect("invalid context");
+ assert_eq!(error, Error::InvalidContextValue { field, reason });
+ assert!(!error.to_string().contains("SUBJECT-SECRET"));
+ }
+}
+
+#[test]
+fn diagnostics_redact_semantic_values() {
+ let context = context();
+ let debug = format!("{context:?}");
+ assert!(debug.contains("private_artifact"));
+ assert!(!debug.contains("01010101010101010101010101010101"));
+ assert!(!debug.contains("trade.private_terms.v1"));
+}
+
+#[cfg(feature = "serde")]
+#[test]
+fn serde_revalidates_context_parts() {
+ let encoded = serde_json::to_vec(&context()).expect("serialize");
+ let decoded: EnvelopeContext = serde_json::from_slice(&encoded).expect("deserialize");
+ assert_eq!(decoded, context());
+ let invalid = br#"{"purpose":"radroots.private_artifact","subject_type":"private_artifact","subject":"INVALID","payload_schema":"trade.private_terms.v1"}"#;
+ assert!(serde_json::from_slice::<EnvelopeContext>(invalid).is_err());
+}
diff --git a/crates/secrets/tests/package_boundary.rs b/crates/secrets/tests/package_boundary.rs
@@ -88,7 +88,8 @@ fn crate_root_contains_only_the_approved_module_skeleton() {
assert_eq!(
declarations(ROOT, "pub mod "),
BTreeSet::from([
- "envelope", "error", "file", "id", "keyring", "memory", "provider", "wrapping",
+ "context", "envelope", "error", "file", "id", "keyring", "memory", "provider",
+ "wrapping",
])
);
assert_eq!(
diff --git a/crates/secrets/tests/security_contract.rs b/crates/secrets/tests/security_contract.rs
@@ -11,6 +11,7 @@ const PUBLIC_API: &str = include_str!("../../../docs/api/radroots_secrets.txt");
#[test]
fn reviewed_api_forbids_secret_bearing_clone_serialize_and_byte_access() {
for required in [
+ "pub mod radroots_secrets::context",
"pub mod radroots_secrets::envelope",
"pub mod radroots_secrets::error",
"pub mod radroots_secrets::id",
diff --git a/docs/api/radroots_secrets.txt b/docs/api/radroots_secrets.txt
@@ -1,4 +1,49 @@
pub mod radroots_secrets
+pub mod radroots_secrets::context
+pub struct radroots_secrets::context::EnvelopeContext
+impl radroots_secrets::context::EnvelopeContext
+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
+pub const fn radroots_secrets::context::EnvelopeContext::subject(&self) -> &radroots_secrets::context::EnvelopeSubject
+pub fn radroots_secrets::context::EnvelopeContext::to_canonical_bytes(&self) -> alloc::vec::Vec<u8>
+impl core::fmt::Debug for radroots_secrets::context::EnvelopeContext
+pub fn radroots_secrets::context::EnvelopeContext::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+impl serde_core::ser::Serialize for radroots_secrets::context::EnvelopeContext
+pub fn radroots_secrets::context::EnvelopeContext::serialize<S>(&self, S) -> core::result::Result<<S as serde_core::ser::Serializer>::Ok, <S as serde_core::ser::Serializer>::Error> where S: serde_core::ser::Serializer
+impl<'de> serde_core::de::Deserialize<'de> for radroots_secrets::context::EnvelopeContext
+pub fn radroots_secrets::context::EnvelopeContext::deserialize<D>(D) -> core::result::Result<Self, <D as serde_core::de::Deserializer>::Error> where D: serde_core::de::Deserializer<'de>
+pub struct radroots_secrets::context::EnvelopePurpose(_)
+impl radroots_secrets::context::EnvelopePurpose
+pub fn radroots_secrets::context::EnvelopePurpose::as_str(&self) -> &str
+pub fn radroots_secrets::context::EnvelopePurpose::parse(impl core::convert::Into<alloc::string::String>) -> core::result::Result<Self, radroots_secrets::error::Error>
+impl core::fmt::Debug for radroots_secrets::context::EnvelopePurpose
+pub fn radroots_secrets::context::EnvelopePurpose::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+impl core::fmt::Display for radroots_secrets::context::EnvelopePurpose
+pub fn radroots_secrets::context::EnvelopePurpose::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+pub struct radroots_secrets::context::EnvelopeSubject
+impl radroots_secrets::context::EnvelopeSubject
+pub fn radroots_secrets::context::EnvelopeSubject::parse(impl core::convert::Into<alloc::string::String>, impl core::convert::Into<alloc::string::String>) -> core::result::Result<Self, radroots_secrets::error::Error>
+pub fn radroots_secrets::context::EnvelopeSubject::subject_type(&self) -> &str
+pub fn radroots_secrets::context::EnvelopeSubject::value(&self) -> &str
+impl core::fmt::Debug for radroots_secrets::context::EnvelopeSubject
+pub fn radroots_secrets::context::EnvelopeSubject::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+impl core::fmt::Display for radroots_secrets::context::EnvelopeSubject
+pub fn radroots_secrets::context::EnvelopeSubject::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+pub struct radroots_secrets::context::PayloadSchemaId(_)
+impl radroots_secrets::context::PayloadSchemaId
+pub fn radroots_secrets::context::PayloadSchemaId::as_str(&self) -> &str
+pub fn radroots_secrets::context::PayloadSchemaId::parse(impl core::convert::Into<alloc::string::String>) -> core::result::Result<Self, radroots_secrets::error::Error>
+impl core::fmt::Debug for radroots_secrets::context::PayloadSchemaId
+pub fn radroots_secrets::context::PayloadSchemaId::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+impl core::fmt::Display for radroots_secrets::context::PayloadSchemaId
+pub fn radroots_secrets::context::PayloadSchemaId::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+pub const radroots_secrets::context::ENVELOPE_CONTEXT_DOMAIN: &[u8]
+pub const radroots_secrets::context::ENVELOPE_CONTEXT_VERSION: u16
+pub const radroots_secrets::context::ENVELOPE_PURPOSE_MAX_BYTES: usize
+pub const radroots_secrets::context::ENVELOPE_SUBJECT_TYPE_MAX_BYTES: usize
+pub const radroots_secrets::context::ENVELOPE_SUBJECT_VALUE_MAX_BYTES: usize
+pub const radroots_secrets::context::PAYLOAD_SCHEMA_MAX_BYTES: usize
pub mod radroots_secrets::envelope
#[non_exhaustive] pub enum radroots_secrets::envelope::Cipher
pub radroots_secrets::envelope::Cipher::XChaCha20Poly1305
@@ -39,6 +84,19 @@ pub fn radroots_secrets::envelope::SealRequest<'_>::fmt(&self, &mut core::fmt::F
pub const radroots_secrets::envelope::ENVELOPE_MAX_BYTES: usize
pub const radroots_secrets::envelope::ENVELOPE_VERSION: u16
pub mod radroots_secrets::error
+#[non_exhaustive] pub enum radroots_secrets::error::ContextField
+pub radroots_secrets::error::ContextField::PayloadSchema
+pub radroots_secrets::error::ContextField::Purpose
+pub radroots_secrets::error::ContextField::SubjectType
+pub radroots_secrets::error::ContextField::SubjectValue
+#[non_exhaustive] pub enum radroots_secrets::error::ContextValueError
+pub radroots_secrets::error::ContextValueError::Empty
+pub radroots_secrets::error::ContextValueError::NonCanonical
+pub radroots_secrets::error::ContextValueError::TooLong
+pub radroots_secrets::error::ContextValueError::TooLong::actual_bytes: usize
+pub radroots_secrets::error::ContextValueError::TooLong::max_bytes: usize
+impl core::fmt::Display for radroots_secrets::error::ContextValueError
+pub fn radroots_secrets::error::ContextValueError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
#[non_exhaustive] pub enum radroots_secrets::error::Error
pub radroots_secrets::error::Error::BackendFailure
pub radroots_secrets::error::Error::BackendFailure::backend: radroots_secrets::id::BackendKind
@@ -55,6 +113,9 @@ pub radroots_secrets::error::Error::EnvelopeTooLarge
pub radroots_secrets::error::Error::EnvelopeTooLarge::actual_bytes: usize
pub radroots_secrets::error::Error::EnvelopeTooLarge::max_bytes: usize
pub radroots_secrets::error::Error::InsecurePermissions
+pub radroots_secrets::error::Error::InvalidContextValue
+pub radroots_secrets::error::Error::InvalidContextValue::field: radroots_secrets::error::ContextField
+pub radroots_secrets::error::Error::InvalidContextValue::reason: radroots_secrets::error::ContextValueError
pub radroots_secrets::error::Error::InvalidDataKeyLength
pub radroots_secrets::error::Error::InvalidDataKeyLength::actual_bytes: usize
pub radroots_secrets::error::Error::InvalidKeyVersion
@@ -296,6 +357,9 @@ pub radroots_secrets::Error::EnvelopeTooLarge
pub radroots_secrets::Error::EnvelopeTooLarge::actual_bytes: usize
pub radroots_secrets::Error::EnvelopeTooLarge::max_bytes: usize
pub radroots_secrets::Error::InsecurePermissions
+pub radroots_secrets::Error::InvalidContextValue
+pub radroots_secrets::Error::InvalidContextValue::field: radroots_secrets::error::ContextField
+pub radroots_secrets::Error::InvalidContextValue::reason: radroots_secrets::error::ContextValueError
pub radroots_secrets::Error::InvalidDataKeyLength
pub radroots_secrets::Error::InvalidDataKeyLength::actual_bytes: usize
pub radroots_secrets::Error::InvalidKeyVersion