use super::*;
use crate::runtime::{ExternalEpoch, ProcessGeneration};
fn context() -> Cx {
Cx::for_testing()
}
fn binding(value: u8) -> EnvelopeBinding {
EnvelopeBinding {
purpose: EnvelopePurpose::Continuation,
digest: [value; 32],
}
}
fn owner(policy: EnvelopePolicy) -> EphemeralEnvelopeProtector {
EphemeralEnvelopeProtector::new(
&context(),
ProcessGenerationGuard::install().unwrap(),
SnapshotCloneStance::NoLiveMemoryCloning,
EnvelopePurpose::Continuation,
policy,
)
.unwrap()
}
fn ttl() -> Duration {
Duration::from_secs(60)
}
#[test]
fn ephemeral_envelope_round_trip_includes_binary_and_empty_payloads() {
let cx = context();
let mut owner = owner(EnvelopePolicy::default());
for payload in [
b"".as_slice(),
b"private credential material".as_slice(),
&[0, 1, 255, 0, 128],
] {
let sealed = owner.seal(&cx, &binding(1), payload, ttl()).unwrap();
let opened = owner.open(&cx, &binding(1), &sealed).unwrap();
assert_eq!(opened.as_bytes(), payload);
assert_eq!(sealed.len(), HEADER_BYTES + payload.len() + TAG_BYTES);
}
}
#[test]
fn ephemeral_envelope_tampered_header_ciphertext_or_tag_never_opens() {
let cx = context();
let mut owner = owner(EnvelopePolicy::default());
let sealed = owner
.seal(&cx, &binding(1), b"confidential", ttl())
.unwrap();
for index in 0..sealed.len() {
let mut changed = sealed.clone();
changed[index] ^= 1;
assert!(
matches!(
owner.open(&cx, &binding(1), &changed),
Err(EnvelopeError::InvalidEnvelope)
),
"changed byte {index} must not authenticate"
);
}
assert_eq!(
owner.open(&cx, &binding(1), &sealed).unwrap().as_bytes(),
b"confidential"
);
}
#[test]
fn ephemeral_envelope_rejects_truncation_extension_and_forged_lengths() {
let cx = context();
let mut owner = owner(EnvelopePolicy::default());
let sealed = owner.seal(&cx, &binding(1), b"private", ttl()).unwrap();
for end in 0..sealed.len() {
assert!(owner.open(&cx, &binding(1), &sealed[..end]).is_err());
}
let mut changed = sealed.clone();
changed.push(0);
assert!(owner.open(&cx, &binding(1), &changed).is_err());
let mut changed = sealed;
changed[66..70].copy_from_slice(&u32::MAX.to_be_bytes());
assert!(owner.open(&cx, &binding(1), &changed).is_err());
}
#[test]
fn ephemeral_envelope_owner_purpose_and_authorization_binding_are_separate() {
let cx = context();
let mut one = owner(EnvelopePolicy::default());
let two = owner(EnvelopePolicy::default());
let sealed = one.seal(&cx, &binding(1), b"private", ttl()).unwrap();
assert!(two.open(&cx, &binding(1), &sealed).is_err());
assert!(one.open(&cx, &binding(2), &sealed).is_err());
let other_purpose = EnvelopeBinding {
purpose: EnvelopePurpose::Credential,
digest: [1; 32],
};
assert!(one.open(&cx, &other_purpose, &sealed).is_err());
assert!(matches!(
one.seal(&cx, &other_purpose, b"private", ttl()),
Err(EnvelopeError::InvalidBinding)
));
assert_eq!(
one.open(&cx, &binding(1), &sealed).unwrap().as_bytes(),
b"private"
);
}
#[test]
fn catalog_cursor_envelope_binds_context_expires_and_cannot_cross_purposes() {
let cx = context();
let guard = ProcessGenerationGuard::install().unwrap();
let mut catalog = EphemeralEnvelopeProtector::new(
&cx,
guard,
SnapshotCloneStance::NoLiveMemoryCloning,
EnvelopePurpose::CatalogCursor,
EnvelopePolicy::new(8, ttl(), 1).unwrap(),
)
.unwrap();
let digest = sha256_bounded(b"catalog/context/one", 128).unwrap();
let binding = EnvelopeBinding::catalog_cursor(&digest).unwrap();
let offset = 7_u64.to_be_bytes();
let sealed = catalog.seal(&cx, &binding, &offset, ttl()).unwrap();
assert_eq!(
catalog.open(&cx, &binding, &sealed).unwrap().as_bytes(),
offset
);
let other =
EnvelopeBinding::catalog_cursor(&sha256_bounded(b"catalog/context/two", 128).unwrap())
.unwrap();
assert!(matches!(
catalog.open(&cx, &other, &sealed),
Err(EnvelopeError::InvalidEnvelope)
));
let wrong_purpose = EnvelopeBinding {
purpose: EnvelopePurpose::Continuation,
digest: binding.digest,
};
assert!(matches!(
catalog.open(&cx, &wrong_purpose, &sealed),
Err(EnvelopeError::InvalidEnvelope)
));
assert_eq!(
catalog.open(&cx, &binding, &sealed).unwrap().as_bytes(),
offset
);
catalog.started = catalog
.started
.checked_sub(ttl() + Duration::from_secs(1))
.unwrap();
assert!(matches!(
catalog.open(&cx, &binding, &sealed),
Err(EnvelopeError::InvalidEnvelope)
));
assert_eq!(
catalog.generation(),
1,
"expiry does not mint keys or mutate generations"
);
}
#[test]
fn ephemeral_envelope_nonce_allocation_is_unique_and_never_wraps() {
let cx = context();
let mut owner = owner(EnvelopePolicy::default());
let a = owner.seal(&cx, &binding(1), b"same", ttl()).unwrap();
let b = owner.seal(&cx, &binding(1), b"same", ttl()).unwrap();
assert_eq!(u64::from_be_bytes(a[50..58].try_into().unwrap()), 0);
assert_eq!(u64::from_be_bytes(b[50..58].try_into().unwrap()), 1);
assert_ne!(a[HEADER_BYTES..], b[HEADER_BYTES..]);
owner.keys.last_mut().unwrap().next_nonce = u64::MAX;
assert!(matches!(
owner.seal(&cx, &binding(1), b"next", ttl()),
Err(EnvelopeError::NonceExhausted)
));
assert_eq!(owner.keys.last().unwrap().next_nonce, u64::MAX);
assert_eq!(
owner.open(&cx, &binding(1), &a).unwrap().as_bytes(),
b"same"
);
}
#[test]
fn ephemeral_envelope_rotates_without_invalidating_live_previous_generations() {
let cx = context();
let mut owner = owner(EnvelopePolicy::new(64, ttl(), 2).unwrap());
let previous = owner.seal(&cx, &binding(1), b"old", ttl()).unwrap();
assert_eq!(owner.rotate(&cx).unwrap(), 2);
let current = owner.seal(&cx, &binding(1), b"new", ttl()).unwrap();
assert_eq!(
owner.open(&cx, &binding(1), &previous).unwrap().as_bytes(),
b"old"
);
assert_eq!(
owner.open(&cx, &binding(1), ¤t).unwrap().as_bytes(),
b"new"
);
assert!(matches!(owner.rotate(&cx), Err(EnvelopeError::KeyCapacity)));
assert_eq!(owner.generation(), 2);
assert_eq!(owner.keys.len(), 2);
assert_eq!(
owner.open(&cx, &binding(1), &previous).unwrap().as_bytes(),
b"old"
);
}
#[test]
fn ephemeral_envelope_expired_generations_can_be_retired_without_sleeping() {
let cx = context();
let mut owner = owner(EnvelopePolicy::new(64, ttl(), 1).unwrap());
let previous = owner.seal(&cx, &binding(1), b"old", ttl()).unwrap();
owner.started = owner.started.checked_sub(Duration::from_secs(61)).unwrap();
assert!(matches!(
owner.open(&cx, &binding(1), &previous),
Err(EnvelopeError::InvalidEnvelope)
));
assert_eq!(owner.rotate(&cx).unwrap(), 2);
assert_eq!(owner.keys.len(), 1);
let current = owner.seal(&cx, &binding(1), b"new", ttl()).unwrap();
assert_eq!(
owner.open(&cx, &binding(1), ¤t).unwrap().as_bytes(),
b"new"
);
}
#[test]
fn ephemeral_envelope_rejected_input_does_not_consume_a_nonce_or_mutate_keys() {
let cx = context();
let mut owner = owner(EnvelopePolicy::new(4, ttl(), 2).unwrap());
assert!(matches!(
owner.seal(&cx, &binding(1), b"large", ttl()),
Err(EnvelopeError::TooLarge)
));
assert!(matches!(
owner.seal(&cx, &binding(1), b"four", Duration::ZERO),
Err(EnvelopeError::InvalidLifetime)
));
assert!(matches!(
owner.seal(&cx, &binding(1), b"four", ttl() + Duration::from_secs(1)),
Err(EnvelopeError::InvalidLifetime)
));
assert_eq!(owner.keys[0].next_nonce, 0);
assert_eq!(owner.keys[0].latest_expiry, 0);
let exact = owner.seal(&cx, &binding(1), b"four", ttl()).unwrap();
assert_eq!(
owner.open(&cx, &binding(1), &exact).unwrap().as_bytes(),
b"four"
);
}
#[test]
fn ephemeral_envelope_generation_exhaustion_keeps_existing_material() {
let cx = context();
let mut owner = owner(EnvelopePolicy::default());
let sealed = owner.seal(&cx, &binding(1), b"private", ttl()).unwrap();
owner.generation = u64::MAX;
assert!(matches!(
owner.rotate(&cx),
Err(EnvelopeError::GenerationExhausted)
));
assert_eq!(owner.keys.len(), 1);
assert_eq!(
owner.open(&cx, &binding(1), &sealed).unwrap().as_bytes(),
b"private"
);
}
#[test]
fn ephemeral_envelope_close_is_terminal_and_retires_keys() {
let cx = context();
let mut owner = owner(EnvelopePolicy::default());
let sealed = owner.seal(&cx, &binding(1), b"private", ttl()).unwrap();
owner.close();
assert!(owner.keys.is_empty());
assert!(matches!(
owner.open(&cx, &binding(1), &sealed),
Err(EnvelopeError::Closed)
));
assert!(matches!(
owner.seal(&cx, &binding(1), b"private", ttl()),
Err(EnvelopeError::Closed)
));
assert!(matches!(owner.rotate(&cx), Err(EnvelopeError::Closed)));
}
#[test]
fn ephemeral_envelope_process_change_precedes_nonce_or_key_access() {
let cx = context();
let mut owner = owner(EnvelopePolicy::default());
let installed = ProcessGenerationGuard::installed().unwrap().generation();
owner.process = ProcessBoundToken {
minted_in: ProcessGeneration::observed(
installed.pid().wrapping_add(1),
*installed.nonce(),
installed.generation(),
),
};
assert!(matches!(
owner.seal(&cx, &binding(1), b"private", ttl()),
Err(EnvelopeError::ProcessChanged)
));
assert_eq!(owner.keys[0].next_nonce, 0);
assert!(matches!(
owner.rotate(&cx),
Err(EnvelopeError::ProcessChanged)
));
assert_eq!(owner.generation(), 1);
}
#[test]
fn ephemeral_envelope_cannot_claim_external_epoch_or_live_clone_support() {
let cx = context();
let guard = ProcessGenerationGuard::install().unwrap();
for external_epoch in [None, Some(ExternalEpoch::new(true, true))] {
for ephemeral_protected_state_disabled in [false, true] {
let result = EphemeralEnvelopeProtector::new(
&cx,
guard,
SnapshotCloneStance::LiveMemoryCloningPermitted {
ephemeral_protected_state_disabled,
external_epoch,
},
EnvelopePurpose::Continuation,
EnvelopePolicy::default(),
);
assert!(matches!(result, Err(EnvelopeError::CloningUnsupported)));
}
}
}
#[test]
fn ephemeral_envelope_policy_has_finite_hard_ceilings() {
assert!(EnvelopePolicy::new(1, Duration::from_secs(1), 1).is_ok());
for (bytes, lifetime, keys) in [
(0, ttl(), 1),
(MAX_PLAINTEXT + 1, ttl(), 1),
(1, Duration::ZERO, 1),
(1, MAX_LIFETIME + Duration::from_secs(1), 1),
(1, ttl(), 0),
(1, ttl(), 9),
] {
assert!(matches!(
EnvelopePolicy::new(bytes, lifetime, keys),
Err(EnvelopeError::InvalidPolicy)
));
}
}
#[test]
fn ephemeral_envelope_diagnostics_contain_no_secret_or_binding_bytes() {
let owner = owner(EnvelopePolicy::default());
let diagnostics = format!(
"{owner:?} {:?} {}",
binding(7),
EnvelopeError::InvalidEnvelope
);
assert!(!diagnostics.contains("private"));
assert!(!diagnostics.contains("[7, 7"));
assert!(!diagnostics.contains("nonce"));
}