diff --git a/crates/mvm-core/src/mailbox.rs b/crates/mvm-core/src/mailbox.rs index 34b126da03..4687c5229c 100644 --- a/crates/mvm-core/src/mailbox.rs +++ b/crates/mvm-core/src/mailbox.rs @@ -457,8 +457,342 @@ fn verify_capability_scope( Ok(()) } -/// Verify identities, signature, capability, current state, ciphertext, and -/// replay identity before returning plaintext. +/// Authenticated header and plaintext from one successful canonical open. +/// This is verification under the supplied context, not a delivery receipt or +/// permission to skip a later admission check. +/// +/// ```compile_fail +/// use mvm_contract::protocol::mailbox::MailboxEnvelopeHeader; +/// use mvm_core::mailbox::OpenedMailboxEnvelope; +/// fn forge(header: MailboxEnvelopeHeader, payload: Vec) -> OpenedMailboxEnvelope { +/// OpenedMailboxEnvelope { header, payload } +/// } +/// ``` +pub struct OpenedMailboxEnvelope { + header: MailboxEnvelopeHeader, + payload: Vec, +} + +impl std::fmt::Debug for OpenedMailboxEnvelope { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter + .debug_struct("OpenedMailboxEnvelope") + .finish_non_exhaustive() + } +} + +impl OpenedMailboxEnvelope { + /// The exact signed header authenticated with these bytes. + #[must_use] + pub fn header(&self) -> &MailboxEnvelopeHeader { + &self.header + } + + /// The verified plaintext without copying it. + #[must_use] + pub fn payload(&self) -> &[u8] { + &self.payload + } + + /// Consume the verified result, retaining the original plaintext allocation. + #[must_use] + pub fn into_payload(self) -> Vec { + self.payload + } +} + +/// A fixed-size snapshot of the successful verifier's authenticated header and +/// supplied local context. This is historical verification data, not current +/// authorization, a journal commit, or permission to sign a durable receipt. +/// Accepted-content and revocation vectors are deliberately not retained. +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct VerifiedMailboxScope { + tenant: TenantId, + authorization_domain: AuthorizationDomainId, + mailbox: MailboxId, + sender: PrincipalId, + recipient: PrincipalId, + sender_boot: BootId, + message_id: MessageId, + capability_id: mvm_contract::protocol::mailbox::CapabilityId, + encryption_key_id: [u8; 32], + execution_epoch: ExecutionEpoch, + policy_epoch: u64, + expires_at_unix_ms: i64, + acting_principal: PrincipalId, + required_operation: MailboxOperation, + capability_issuer: PrincipalId, + current_execution_epoch: ExecutionEpoch, + current_policy_epoch: u64, + verified_at_unix_ms: i64, +} + +impl std::fmt::Debug for VerifiedMailboxScope { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter + .debug_struct("VerifiedMailboxScope") + .finish_non_exhaustive() + } +} + +impl VerifiedMailboxScope { + #[must_use] + pub fn tenant(&self) -> TenantId { + self.tenant + } + #[must_use] + pub fn authorization_domain(&self) -> AuthorizationDomainId { + self.authorization_domain + } + #[must_use] + pub fn mailbox(&self) -> MailboxId { + self.mailbox + } + #[must_use] + pub fn sender(&self) -> PrincipalId { + self.sender + } + #[must_use] + pub fn recipient(&self) -> PrincipalId { + self.recipient + } + #[must_use] + pub fn sender_boot(&self) -> BootId { + self.sender_boot + } + #[must_use] + pub fn message_id(&self) -> MessageId { + self.message_id + } + #[must_use] + pub fn capability_id(&self) -> mvm_contract::protocol::mailbox::CapabilityId { + self.capability_id + } + #[must_use] + pub fn encryption_key_id(&self) -> [u8; 32] { + self.encryption_key_id + } + #[must_use] + pub fn execution_epoch(&self) -> ExecutionEpoch { + self.execution_epoch + } + #[must_use] + pub fn policy_epoch(&self) -> u64 { + self.policy_epoch + } + #[must_use] + pub fn expires_at_unix_ms(&self) -> i64 { + self.expires_at_unix_ms + } + #[must_use] + pub fn acting_principal(&self) -> PrincipalId { + self.acting_principal + } + #[must_use] + pub fn required_operation(&self) -> MailboxOperation { + self.required_operation + } + #[must_use] + pub fn capability_issuer(&self) -> PrincipalId { + self.capability_issuer + } + #[must_use] + pub fn current_execution_epoch(&self) -> ExecutionEpoch { + self.current_execution_epoch + } + #[must_use] + pub fn current_policy_epoch(&self) -> u64 { + self.current_policy_epoch + } + #[must_use] + pub fn verified_at_unix_ms(&self) -> i64 { + self.verified_at_unix_ms + } +} + +/// Original ciphertext ownership plus exactly one canonical verified-open result. +/// The service may inspect plaintext; the journal receives only a narrow view. +/// No constructor accepts caller assertions of verification. This capsule is +/// neither Clone nor serializable/deserializable. +/// +/// ```compile_fail +/// use mvm_core::mailbox::{OpenedMailboxEnvelope, VerifiedMailboxAdmission, VerifiedMailboxScope}; +/// use mvm_contract::protocol::mailbox::MailboxEnvelope; +/// fn forge(envelope: MailboxEnvelope, opened: OpenedMailboxEnvelope, scope: VerifiedMailboxScope) -> VerifiedMailboxAdmission { +/// VerifiedMailboxAdmission { envelope, opened, scope } +/// } +/// ``` +/// +/// ```compile_fail +/// use mvm_core::mailbox::VerifiedMailboxAdmission; +/// fn requires_clone() {} +/// requires_clone::(); +/// ``` +/// +/// ```compile_fail +/// use mvm_core::mailbox::VerifiedMailboxAdmission; +/// fn requires_serialize() {} +/// requires_serialize::(); +/// ``` +/// +/// ```compile_fail +/// use mvm_core::mailbox::VerifiedMailboxAdmission; +/// fn requires_deserialize() {} +/// requires_deserialize::(); +/// ``` +pub struct VerifiedMailboxAdmission { + envelope: MailboxEnvelope, + opened: OpenedMailboxEnvelope, + scope: VerifiedMailboxScope, +} + +impl std::fmt::Debug for VerifiedMailboxAdmission { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter + .debug_struct("VerifiedMailboxAdmission") + .finish_non_exhaustive() + } +} + +impl VerifiedMailboxAdmission { + /// Borrow only verified metadata and original ciphertext, never plaintext. + #[must_use] + pub fn journal_view(&self) -> JournalAdmissionView<'_> { + JournalAdmissionView { + envelope: &self.envelope, + header: self.opened.header(), + scope: &self.scope, + replay_identity: ReplayIdentity::from_header(self.opened.header()), + } + } + + /// Service-facing plaintext access. A journal view intentionally has none. + #[must_use] + pub fn payload(&self) -> &[u8] { + self.opened.payload() + } + + /// Consume the capsule, preserving both original ciphertext and verified + /// plaintext allocations. This does not create a journal or delivery result. + #[must_use] + pub fn into_parts(self) -> (MailboxEnvelope, OpenedMailboxEnvelope) { + (self.envelope, self.opened) + } +} + +/// Ciphertext-only journal handoff borrowed from a canonical verified capsule. +/// Header and commitment are authenticated by canonical verification; the outer +/// envelope remains the exact admitted representation, including routing fields. +/// This view does not turn transport-mutable routing data into route authority. +/// A journal must still require actual destination/Receive scope, current +/// authorization, resource admission, durable commit, anchor and key custody. +/// In particular, a view verified for Send is not Receive admission authority. +/// +/// ```compile_fail +/// use mvm_core::mailbox::{JournalAdmissionView, ReplayIdentity, VerifiedMailboxScope}; +/// use mvm_contract::protocol::mailbox::{MailboxEnvelope, MailboxEnvelopeHeader}; +/// fn forge<'a>(envelope: &'a MailboxEnvelope, header: &'a MailboxEnvelopeHeader, scope: &'a VerifiedMailboxScope) -> JournalAdmissionView<'a> { +/// JournalAdmissionView { envelope, header, scope, replay_identity: ReplayIdentity::from_header(header) } +/// } +/// ``` +/// +/// ```compile_fail +/// use mvm_core::mailbox::JournalAdmissionView; +/// fn plaintext<'a>(view: &'a JournalAdmissionView<'a>) -> &'a [u8] { +/// view.payload() +/// } +/// ``` +pub struct JournalAdmissionView<'a> { + envelope: &'a MailboxEnvelope, + header: &'a MailboxEnvelopeHeader, + scope: &'a VerifiedMailboxScope, + replay_identity: ReplayIdentity, +} + +impl std::fmt::Debug for JournalAdmissionView<'_> { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter + .debug_struct("JournalAdmissionView") + .finish_non_exhaustive() + } +} + +impl JournalAdmissionView<'_> { + #[must_use] + pub fn envelope(&self) -> &MailboxEnvelope { + self.envelope + } + #[must_use] + pub fn header(&self) -> &MailboxEnvelopeHeader { + self.header + } + /// Data identity, not a replay reservation or durable admission token. + #[must_use] + pub fn replay_identity(&self) -> ReplayIdentity { + self.replay_identity + } + #[must_use] + pub fn ciphertext_commitment(&self) -> &[u8; 32] { + &self.envelope.ciphertext_commitment + } + #[must_use] + pub fn verified_scope(&self) -> &VerifiedMailboxScope { + self.scope + } +} + +/// Take ownership of an envelope and run the existing canonical verifier once, +/// including its original replay decision. No ciphertext clone, second replay +/// check, alternative crypto path or context-vector clone is performed. +/// Verification under the supplied context is historical, not current permission. +/// No journal I/O, commit evidence or receipt authority is created here. +pub fn open_verified_admission( + envelope: MailboxEnvelope, + capability: &SignedMailboxCapability, + context: &OpenContext, + encryption_key: &Key, + sender_key: &VerifyingKey, + authority_key: &VerifyingKey, + replay: &mut impl ReplayProtector, +) -> Result { + let opened = open_verified_envelope( + &envelope, + capability, + context, + encryption_key, + sender_key, + authority_key, + replay, + )?; + let header = opened.header(); + let scope = VerifiedMailboxScope { + tenant: header.tenant, + authorization_domain: header.authorization_domain, + mailbox: header.mailbox, + sender: header.sender, + recipient: header.recipient, + sender_boot: header.sender_boot, + message_id: header.message_id, + capability_id: header.capability_id, + encryption_key_id: header.encryption.key_id, + execution_epoch: header.execution_epoch, + policy_epoch: header.policy_epoch, + expires_at_unix_ms: header.expires_at_unix_ms, + acting_principal: context.acting_principal, + required_operation: context.required_operation, + capability_issuer: context.expected_capability_issuer, + current_execution_epoch: context.current_execution_epoch, + current_policy_epoch: context.current_policy_epoch, + verified_at_unix_ms: context.now_unix_ms, + }; + Ok(VerifiedMailboxAdmission { + envelope, + opened, + scope, + }) +} + +/// Verify an envelope and return its plaintext through the canonical open path. pub fn open_envelope( envelope: &MailboxEnvelope, capability: &SignedMailboxCapability, @@ -468,6 +802,29 @@ pub fn open_envelope( authority_key: &VerifyingKey, replay: &mut impl ReplayProtector, ) -> Result, MailboxError> { + open_verified_envelope( + envelope, + capability, + context, + encryption_key, + sender_key, + authority_key, + replay, + ) + .map(OpenedMailboxEnvelope::into_payload) +} + +/// Verify identities, signature, capability, current state, ciphertext, and +/// replay identity before returning the authenticated header and plaintext. +pub fn open_verified_envelope( + envelope: &MailboxEnvelope, + capability: &SignedMailboxCapability, + context: &OpenContext, + encryption_key: &Key, + sender_key: &VerifyingKey, + authority_key: &VerifyingKey, + replay: &mut impl ReplayProtector, +) -> Result { envelope.validate_bounds()?; validate_outer_versions(envelope)?; let actual_commitment: [u8; 32] = Sha256::digest(&envelope.ciphertext).into(); @@ -509,7 +866,10 @@ pub fn open_envelope( if !replay.check_and_record(ReplayIdentity::from_header(&message.header)) { return Err(MailboxError::Replay); } - Ok(message.payload) + Ok(OpenedMailboxEnvelope { + header: message.header, + payload: message.payload, + }) } /// Sign a destination receipt in a domain distinct from envelopes and grants. diff --git a/crates/mvm-core/src/mailbox/transport.rs b/crates/mvm-core/src/mailbox/transport.rs index b25512cafb..b9be6fa83a 100644 --- a/crates/mvm-core/src/mailbox/transport.rs +++ b/crates/mvm-core/src/mailbox/transport.rs @@ -14,8 +14,8 @@ use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey}; use mvm_contract::protocol::mailbox::{ AuthorizationDomainRouteLabel, DestinationRouteToken, ED25519_SIGNATURE_BYTES, ExecutionEpoch, MAILBOX_PROTOCOL_VERSION, MAX_ENCODED_ENVELOPE_BYTES, MailboxAddress, MailboxEnvelope, - MailboxEnvelopeHeader, MailboxOperation, PrincipalId, ProtocolVersion, SignatureAlgorithm, - SignedMailboxCapability, TransportBindingId, + MailboxEnvelopeHeader, MailboxMessageKind, MailboxOperation, PrincipalId, ProtocolVersion, + SignatureAlgorithm, SignedMailboxCapability, TransportBindingId, }; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; @@ -571,11 +571,21 @@ impl AuthorizedMailboxFrame { pub fn authorize_outbound( request: OutboundAuthorization<'_>, ) -> Result { - if request.context.required_operation != MailboxOperation::Send { - return Err(OutboundAuthorizationError::SendOperationRequired); - } let envelope = &request.prepared.envelope; let header = &request.prepared.header; + match header.kind { + MailboxMessageKind::Call | MailboxMessageKind::Cast + if request.context.required_operation != MailboxOperation::Send => + { + return Err(OutboundAuthorizationError::SendOperationRequired); + } + MailboxMessageKind::Reply + if request.context.required_operation != MailboxOperation::Reply => + { + return Err(OutboundAuthorizationError::ReplyOperationRequired); + } + MailboxMessageKind::Call | MailboxMessageKind::Cast | MailboxMessageKind::Reply => {} + } envelope.validate_bounds().map_err(MailboxError::from)?; validate_outer_versions(envelope)?; validate_inner_versions(header)?; @@ -615,6 +625,8 @@ pub enum OutboundAuthorizationError { Mailbox(#[from] MailboxError), #[error("outbound transport authorization requires the send operation")] SendOperationRequired, + #[error("outbound reply authorization requires the reply operation")] + ReplyOperationRequired, #[error("mailbox envelope and supplied signed header do not match")] EnvelopeHeaderMismatch, #[error("mailbox envelope does not match the authorized transport route")] diff --git a/crates/mvm-core/src/mailbox/transport/tests.rs b/crates/mvm-core/src/mailbox/transport/tests.rs index b2767de338..10cc120914 100644 --- a/crates/mvm-core/src/mailbox/transport/tests.rs +++ b/crates/mvm-core/src/mailbox/transport/tests.rs @@ -424,6 +424,131 @@ fn authenticated_connection_is_not_mailbox_authority() { assert!(transport.drain(route.route_id()).expect("drain").is_empty()); } +fn fixture_for_operation(kind: MailboxMessageKind, operation: MailboxOperation) -> MailboxFixture { + let mut fixture = mailbox_fixture(); + let mut header = fixture.prepared.header.clone(); + header.kind = kind; + header.reply_to = match kind { + MailboxMessageKind::Call => Some(mvm_contract::protocol::mailbox::ReplyRoute { + address: address(), + capability_id: header.capability_id, + }), + MailboxMessageKind::Cast | MailboxMessageKind::Reply => None, + }; + fixture.prepared = prepare_mailbox_envelope( + SealRequest { + routing: fixture.prepared.envelope().routing.clone(), + header, + plaintext: b"hello", + }, + &Key::from_bytes(id(21)), + &SigningKey::from_bytes(&id(22)), + ) + .expect("message seals"); + let mut grant = fixture.capability.grant.clone(); + grant.operations = CapabilityOperations::from_operations(&[operation]); + grant.message_kinds = vec![kind]; + fixture.capability = sign_capability(grant, &fixture.capability_authority).expect("grant"); + fixture.context.required_operation = operation; + fixture +} + +#[test] +fn reply_only_grant_authorizes_the_exact_prepared_reply() { + let route_authority = SigningKey::from_bytes(&id(61)); + let (_, route) = signed_route(TransportPath::Direct, 62, &route_authority); + let fixture = fixture_for_operation(MailboxMessageKind::Reply, MailboxOperation::Reply); + let authorized = authorize_outbound(OutboundAuthorization { + prepared: &fixture.prepared, + capability: &fixture.capability, + context: &fixture.context, + capability_authority_key: &fixture.capability_authority.verifying_key(), + route: &route, + }) + .expect("reply-only authority"); + assert_eq!( + authorized.as_bytes(), + encode_envelope(fixture.prepared.envelope()).expect("canonical bytes") + ); +} + +#[test] +fn outbound_message_kind_cannot_borrow_another_operation() { + let authority = SigningKey::from_bytes(&id(63)); + let (_, route) = signed_route(TransportPath::Direct, 64, &authority); + for kind in [ + MailboxMessageKind::Call, + MailboxMessageKind::Cast, + MailboxMessageKind::Reply, + ] { + for operation in [ + MailboxOperation::Send, + MailboxOperation::Reply, + MailboxOperation::Receive, + ] { + let fixture = fixture_for_operation(kind, operation); + let result = authorize_outbound(OutboundAuthorization { + prepared: &fixture.prepared, + capability: &fixture.capability, + context: &fixture.context, + capability_authority_key: &fixture.capability_authority.verifying_key(), + route: &route, + }); + match kind { + MailboxMessageKind::Call | MailboxMessageKind::Cast + if operation == MailboxOperation::Send => + { + assert!(result.is_ok()); + } + MailboxMessageKind::Reply if operation == MailboxOperation::Reply => { + assert!(result.is_ok()); + } + MailboxMessageKind::Reply => assert_eq!( + result, + Err(OutboundAuthorizationError::ReplyOperationRequired) + ), + MailboxMessageKind::Call | MailboxMessageKind::Cast => assert_eq!( + result, + Err(OutboundAuthorizationError::SendOperationRequired) + ), + } + } + } +} + +#[test] +fn reply_context_still_requires_a_reply_capability_and_matching_route() { + let authority = SigningKey::from_bytes(&id(65)); + let (_, route) = signed_route(TransportPath::Direct, 66, &authority); + let mut fixture = fixture_for_operation(MailboxMessageKind::Reply, MailboxOperation::Send); + fixture.context.required_operation = MailboxOperation::Reply; + let request = |current: &MailboxFixture, selected: &AuthorizedRoute| { + authorize_outbound(OutboundAuthorization { + prepared: ¤t.prepared, + capability: ¤t.capability, + context: ¤t.context, + capability_authority_key: ¤t.capability_authority.verifying_key(), + route: selected, + }) + }; + assert_eq!( + request(&fixture, &route), + Err(MailboxError::OperationNotAuthorized.into()) + ); + let valid = fixture_for_operation(MailboxMessageKind::Reply, MailboxOperation::Reply); + let mut mismatched = claims(TransportPath::Direct, 67); + mismatched.address.recipient = PrincipalId::from_bytes(id(68)); + let signed = sign_transport_binding(mismatched, &authority).expect("different route"); + let mut context = binding_context(); + context.expected_address = signed.claims.address; + let foreign_route = authorize_route(&signed, &context, &authority.verifying_key()) + .expect("valid foreign route"); + assert_eq!( + request(&valid, &foreign_route), + Err(OutboundAuthorizationError::RouteMismatch) + ); +} + #[test] fn direct_delivery_is_pass_by_value_bounded_and_cancelable() { let authority = SigningKey::from_bytes(&id(46)); diff --git a/crates/mvm-core/tests/mailbox_crypto.rs b/crates/mvm-core/tests/mailbox_crypto.rs index f555d16db8..f2f94eca9b 100644 --- a/crates/mvm-core/tests/mailbox_crypto.rs +++ b/crates/mvm-core/tests/mailbox_crypto.rs @@ -808,6 +808,97 @@ fn open_under( ) } +#[test] +fn verified_open_retains_authenticated_header_and_records_replay_once() { + #[derive(Default)] + struct CountingReplay(usize); + + impl mvm_core::mailbox::ReplayProtector for CountingReplay { + fn check_and_record(&mut self, _: mvm_core::mailbox::ReplayIdentity) -> bool { + self.0 += 1; + self.0 == 1 + } + } + + let (sender_key, authority_key, encryption_key) = contract_keys(); + let capability = sign_capability(grant(), &authority_key).expect("capability"); + let expected = header(); + let envelope = seal_with(expected.clone(), b"hello"); + let mut replay = CountingReplay::default(); + let opened = mvm_core::mailbox::open_verified_envelope( + &envelope, + &capability, + &context(), + &encryption_key, + &sender_key.verifying_key(), + &authority_key.verifying_key(), + &mut replay, + ) + .expect("verified header and bytes"); + assert_eq!(opened.header(), &expected); + assert_eq!(opened.payload(), b"hello"); + assert_eq!(replay.0, 1); + assert_eq!(opened.into_payload(), b"hello"); + assert_eq!( + open_envelope( + &envelope, + &capability, + &context(), + &encryption_key, + &sender_key.verifying_key(), + &authority_key.verifying_key(), + &mut replay, + ) + .expect_err("legacy open shares replay authority"), + MailboxError::Replay + ); + assert_eq!(replay.0, 2); +} + +#[test] +fn refused_verified_open_exposes_no_proof_and_preserves_replay_identity() { + let (sender_key, authority_key, encryption_key) = contract_keys(); + let capability = sign_capability(grant(), &authority_key).expect("capability"); + let envelope = seal_with(header(), b"hello"); + let open = |current: &OpenContext, replay: &mut InMemoryReplayProtector| { + mvm_core::mailbox::open_verified_envelope( + &envelope, + &capability, + current, + &encryption_key, + &sender_key.verifying_key(), + &authority_key.verifying_key(), + replay, + ) + }; + let mut recipient = context(); + recipient.expected_recipient = PrincipalId::from_bytes([41; 32]); + let mut epoch = context(); + epoch.current_execution_epoch = ExecutionEpoch::new(8).expect("epoch"); + let mut policy = context(); + policy.current_policy_epoch += 1; + let mut revoked = context(); + revoked.revoked_capabilities.push(grant().revocation_id); + let mut expired = context(); + expired.now_unix_ms = 2_000; + for (refused, expected) in [ + (recipient, MailboxError::WrongRecipient), + (epoch, MailboxError::StaleExecutionEpoch), + (policy, MailboxError::StalePolicyEpoch), + (revoked, MailboxError::CapabilityRevoked), + (expired, MailboxError::Expired), + ] { + let mut replay = InMemoryReplayProtector::default(); + assert_eq!(open(&refused, &mut replay).expect_err("no proof"), expected); + let accepted = open(&context(), &mut replay).expect("refusal did not consume identity"); + assert_eq!(accepted.header(), &header()); + assert_eq!(accepted.payload(), b"hello"); + let debug = format!("{accepted:?}"); + assert!(!debug.contains("hello")); + assert!(!debug.contains("capability_id")); + } +} + #[test] fn a_refused_message_does_not_consume_its_dedup_identity() { let envelope = seal_with(header(), b"hello"); @@ -1098,3 +1189,236 @@ fn only_the_destination_authority_can_sign_acceptance() { Err(MailboxError::ReceiptSignatureInvalid) )); } + +#[test] +fn verified_admission_retains_original_ciphertext_and_service_plaintext_allocations() { + use mvm_core::mailbox::{ReplayIdentity, ReplayProtector, open_verified_admission}; + #[derive(Default)] + struct CountingReplay(usize); + impl ReplayProtector for CountingReplay { + fn check_and_record(&mut self, _: ReplayIdentity) -> bool { + self.0 += 1; + self.0 == 1 + } + } + let (sender_key, authority_key, encryption_key) = contract_keys(); + let capability = sign_capability(grant(), &authority_key).unwrap(); + let expected = header(); + let envelope = seal_with(expected.clone(), b"hello"); + let ciphertext_ptr = envelope.ciphertext.as_ptr(); + let ciphertext_capacity = envelope.ciphertext.capacity(); + let mut replay = CountingReplay::default(); + let admission = open_verified_admission( + envelope, + &capability, + &context(), + &encryption_key, + &sender_key.verifying_key(), + &authority_key.verifying_key(), + &mut replay, + ) + .unwrap(); + assert_eq!(replay.0, 1); + { + let view = admission.journal_view(); + assert_eq!(view.envelope().ciphertext.as_ptr(), ciphertext_ptr); + assert_eq!(view.envelope().ciphertext.capacity(), ciphertext_capacity); + assert_eq!(view.header(), &expected); + assert_eq!( + view.replay_identity(), + ReplayIdentity::from_header(&expected) + ); + assert_eq!( + view.ciphertext_commitment(), + &view.envelope().ciphertext_commitment + ); + assert_eq!( + view.verified_scope().acting_principal(), + context().acting_principal + ); + assert_eq!( + view.verified_scope().required_operation(), + context().required_operation + ); + assert_eq!( + view.verified_scope().capability_issuer(), + context().expected_capability_issuer + ); + assert_eq!( + view.verified_scope().current_execution_epoch(), + context().current_execution_epoch + ); + assert_eq!( + view.verified_scope().current_policy_epoch(), + context().current_policy_epoch + ); + assert_eq!( + view.verified_scope().verified_at_unix_ms(), + context().now_unix_ms + ); + } + assert_eq!(admission.payload(), b"hello"); + let plaintext_ptr = admission.payload().as_ptr(); + let view = admission.journal_view(); + assert_eq!(view.header(), &expected); + assert_eq!(replay.0, 1, "views must not verify or consume replay again"); + let (envelope, opened) = admission.into_parts(); + assert_eq!(envelope.ciphertext.as_ptr(), ciphertext_ptr); + assert_eq!(opened.payload().as_ptr(), plaintext_ptr); + assert_eq!(opened.header(), &expected); + assert_eq!(opened.into_payload(), b"hello"); + assert_eq!( + open_envelope( + &envelope, + &capability, + &context(), + &encryption_key, + &sender_key.verifying_key(), + &authority_key.verifying_key(), + &mut replay + ) + .unwrap_err(), + MailboxError::Replay + ); + assert_eq!(replay.0, 2); +} + +#[test] +fn verified_admission_refusals_do_not_mint_capsule_or_record_replay() { + use mvm_core::mailbox::{ReplayIdentity, ReplayProtector, open_verified_admission}; + #[derive(Default)] + struct CountingReplay(usize); + impl ReplayProtector for CountingReplay { + fn check_and_record(&mut self, _: ReplayIdentity) -> bool { + self.0 += 1; + true + } + } + let (sender_key, authority_key, encryption_key) = contract_keys(); + let capability = sign_capability(grant(), &authority_key).unwrap(); + for failure in [ + "ciphertext", + "sender_key", + "authority_key", + "tenant", + "domain", + "recipient", + "acting_principal", + "operation", + "issuer", + "epoch", + "policy", + "expired", + "revoked", + ] { + let mut envelope = seal_with(header(), b"hello"); + let mut current = context(); + let mut sender_public = sender_key.verifying_key(); + let mut authority_public = authority_key.verifying_key(); + match failure { + "ciphertext" => envelope.ciphertext[0] ^= 1, + "sender_key" => sender_public = SigningKey::from_bytes(&[99; 32]).verifying_key(), + "authority_key" => authority_public = SigningKey::from_bytes(&[99; 32]).verifying_key(), + "tenant" => current.expected_tenant = TenantId::from_bytes([99; 32]), + "domain" => { + current.expected_authorization_domain = AuthorizationDomainId::from_bytes([99; 32]) + } + "recipient" => current.expected_recipient = PrincipalId::from_bytes([99; 32]), + "acting_principal" => current.acting_principal = PrincipalId::from_bytes([99; 32]), + "operation" => current.required_operation = MailboxOperation::Monitor, + "issuer" => current.expected_capability_issuer = PrincipalId::from_bytes([99; 32]), + "epoch" => current.current_execution_epoch = ExecutionEpoch::new(8).unwrap(), + "policy" => current.current_policy_epoch += 1, + "expired" => current.now_unix_ms = 2_000, + _ => current.revoked_capabilities.push(grant().revocation_id), + } + let mut replay = CountingReplay::default(); + assert!( + open_verified_admission( + envelope, + &capability, + ¤t, + &encryption_key, + &sender_public, + &authority_public, + &mut replay + ) + .is_err(), + "{failure}" + ); + assert_eq!(replay.0, 0, "{failure}"); + } +} + +#[test] +fn verified_admission_debug_never_contains_payload_or_ciphertext() { + use mvm_core::mailbox::open_verified_admission; + let (sender_key, authority_key, encryption_key) = contract_keys(); + let capability = sign_capability(grant(), &authority_key).unwrap(); + let admission = open_verified_admission( + seal_with(header(), b"hello"), + &capability, + &context(), + &encryption_key, + &sender_key.verifying_key(), + &authority_key.verifying_key(), + &mut InMemoryReplayProtector::default(), + ) + .unwrap(); + assert_eq!(format!("{admission:?}"), "VerifiedMailboxAdmission { .. }"); + assert_eq!( + format!("{:?}", admission.journal_view()), + "JournalAdmissionView { .. }" + ); +} + +#[test] +fn verified_admission_scope_preserves_supplied_operation_and_historical_context() { + use mvm_core::mailbox::{VerifiedMailboxScope, open_verified_admission}; + let (sender_key, authority_key, encryption_key) = contract_keys(); + let capability = sign_capability(grant(), &authority_key).unwrap(); + let mut current = context(); + let admission = open_verified_admission( + seal_with(header(), b"hello"), + &capability, + ¤t, + &encryption_key, + &sender_key.verifying_key(), + &authority_key.verifying_key(), + &mut InMemoryReplayProtector::default(), + ) + .unwrap(); + current.now_unix_ms += 100; + current.current_policy_epoch += 1; + current.required_operation = MailboxOperation::Receive; + current.revoked_capabilities.push(grant().revocation_id); + let view = admission.journal_view(); + let scope = view.verified_scope(); + assert_eq!( + scope.required_operation(), + MailboxOperation::Send, + "verification cannot become receive authority" + ); + assert_eq!(scope.verified_at_unix_ms(), 1_500); + assert_eq!(scope.current_policy_epoch(), 4); + assert_ne!(scope.current_policy_epoch(), current.current_policy_epoch); + assert_eq!(scope.tenant(), view.header().tenant); + assert_eq!( + scope.authorization_domain(), + view.header().authorization_domain + ); + assert_eq!(scope.mailbox(), view.header().mailbox); + assert_eq!(scope.sender(), view.header().sender); + assert_eq!(scope.recipient(), view.header().recipient); + assert_eq!(scope.sender_boot(), view.header().sender_boot); + assert_eq!(scope.message_id(), view.header().message_id); + assert_eq!(scope.capability_id(), view.header().capability_id); + assert_eq!(scope.encryption_key_id(), view.header().encryption.key_id); + assert_eq!(scope.execution_epoch(), view.header().execution_epoch); + assert_eq!(scope.policy_epoch(), view.header().policy_epoch); + assert_eq!(scope.expires_at_unix_ms(), view.header().expires_at_unix_ms); + assert!( + !std::mem::needs_drop::(), + "fixed scope must not retain owned context vectors" + ); +}