1const ASSERTION_METHOD: &str = "assertionMethod";
38
39const AUTHENTICATION: &str = "authentication";
42
43pub fn verify_proof_structure(
71 envelope: &crate::envelope::NotarizationEnvelope,
72) -> std::result::Result<crate::envelope::AttestationMode, crate::errors::AphError> {
73 let declared = envelope.credential_subject.policy.attestation_mode;
74
75 if !envelope.proof.is_chain() {
77 if declared
78 == std::option::Option::Some(crate::envelope::AttestationMode::PrincipalSigned)
79 {
80 return std::result::Result::Err(crate::errors::AphError::proof_chain_invalid(
81 "`attestationMode` is `PrincipalSigned` but `proof` is a single object; \
82 the label MUST accompany a two-element chain (§7.1.11)",
83 ));
84 }
85 let lone = match envelope.proof.notary() {
92 std::option::Option::Some(proof) => proof,
93 std::option::Option::None => {
95 return std::result::Result::Err(crate::errors::AphError::proof_chain_invalid(
96 "`proof` is neither a chain nor a single proof",
97 ));
98 }
99 };
100 if lone.id.is_some() || lone.previous_proof.is_some() {
101 return std::result::Result::Err(crate::errors::AphError::proof_chain_invalid(
102 "a single-object `proof` MUST carry neither `id` nor `previousProof`; \
103 both belong to a chain, and a lone proof links to nothing (§7.1.11)",
104 ));
105 }
106 if lone.proof_purpose != ASSERTION_METHOD {
110 return std::result::Result::Err(crate::errors::AphError::proof_chain_invalid(
111 std::format!(
112 "a single-object `proof` MUST use `proofPurpose` `{}`; found `{}` (§7.1.11)",
113 ASSERTION_METHOD, lone.proof_purpose
114 ),
115 ));
116 }
117 return std::result::Result::Ok(crate::envelope::AttestationMode::NotaryAttested);
118 }
119
120 let proofs = envelope.proof.all();
122 if proofs.len() != 2 {
123 return std::result::Result::Err(crate::errors::AphError::proof_chain_invalid(
124 std::format!(
125 "a proof chain MUST carry exactly two proofs (principal, then notary); found {}",
126 proofs.len()
127 ),
128 ));
129 }
130
131 match declared {
136 std::option::Option::None => {
137 return std::result::Result::Err(crate::errors::AphError::proof_chain_invalid(
138 "`proof` is a chain but `attestationMode` is absent; absent means \
139 `NotaryAttested`, and a chain MUST declare `PrincipalSigned` (§7.1.11)",
140 ));
141 }
142 std::option::Option::Some(crate::envelope::AttestationMode::NotaryAttested) => {
143 return std::result::Result::Err(crate::errors::AphError::proof_chain_invalid(
144 "`proof` is a chain but `attestationMode` is `NotaryAttested`; \
145 a chain MUST declare `PrincipalSigned` (§7.1.11)",
146 ));
147 }
148 std::option::Option::Some(crate::envelope::AttestationMode::PrincipalSigned) => {}
149 }
150
151 let (principal, notary) = match (envelope.proof.principal(), envelope.proof.notary()) {
152 (std::option::Option::Some(p), std::option::Option::Some(n)) => (p, n),
153 _ => {
157 return std::result::Result::Err(crate::errors::AphError::proof_chain_invalid(
158 "`proof` is not a well-formed two-element chain",
159 ));
160 }
161 };
162
163 if principal.proof_purpose != ASSERTION_METHOD {
165 return std::result::Result::Err(crate::errors::AphError::proof_chain_invalid(
166 std::format!(
167 "chain position 1 (principal proof) MUST have `proofPurpose` \
168 `assertionMethod`; found `{}`",
169 principal.proof_purpose
170 ),
171 ));
172 }
173 if notary.proof_purpose != AUTHENTICATION {
174 return std::result::Result::Err(crate::errors::AphError::proof_chain_invalid(
175 std::format!(
176 "chain position 2 (notary proof) MUST have `proofPurpose` \
177 `authentication`; found `{}`",
178 notary.proof_purpose
179 ),
180 ));
181 }
182
183 let principal_id = match principal.id.as_deref() {
185 std::option::Option::Some(id) => id,
186 std::option::Option::None => {
187 return std::result::Result::Err(crate::errors::AphError::proof_chain_invalid(
188 "chain position 1 (principal proof) MUST carry an `id`; \
189 without one the notary proof has nothing to name (§7.1.11)",
190 ));
191 }
192 };
193 let notary_id = match notary.id.as_deref() {
194 std::option::Option::Some(id) => id,
195 std::option::Option::None => {
196 return std::result::Result::Err(crate::errors::AphError::proof_chain_invalid(
197 "chain position 2 (notary proof) MUST carry an `id`; \
198 every proof in a chain is identified (§7.1.11)",
199 ));
200 }
201 };
202 if principal_id == notary_id {
203 return std::result::Result::Err(crate::errors::AphError::proof_chain_invalid(
204 std::format!(
205 "the two proofs in a chain MUST carry distinct `id`s; both are `{}`",
206 principal_id
207 ),
208 ));
209 }
210
211 if principal.previous_proof.is_some() {
216 return std::result::Result::Err(crate::errors::AphError::proof_chain_invalid(
217 "the principal proof is the HEAD of the chain and MUST NOT carry \
218 `previousProof` (§7.1.11)",
219 ));
220 }
221 let previous = match notary.previous_proof.as_deref() {
222 std::option::Option::Some(previous) => previous,
223 std::option::Option::None => {
224 return std::result::Result::Err(crate::errors::AphError::proof_chain_invalid(
225 "the notary proof MUST carry `previousProof` naming the principal \
226 proof's `id`; a chain without linkage is only an ordered array",
227 ));
228 }
229 };
230 if previous == notary_id {
231 return std::result::Result::Err(crate::errors::AphError::proof_chain_invalid(
232 std::format!(
233 "`previousProof` is self-referential: the notary proof names its own \
234 `id` `{}`",
235 notary_id
236 ),
237 ));
238 }
239 if previous != principal_id {
240 return std::result::Result::Err(crate::errors::AphError::proof_chain_invalid(
241 std::format!(
242 "`previousProof` `{}` is dangling: it does not name a proof in this \
243 chain (the principal proof's `id` is `{}`)",
244 previous, principal_id
245 ),
246 ));
247 }
248
249 let human_did = envelope.credential_subject.human_principal.id.as_str();
256 let principal_did = match principal.verification_method.split_once('#') {
257 std::option::Option::Some((did, _fragment)) => did,
258 std::option::Option::None => principal.verification_method.as_str(),
259 };
260 if principal_did != human_did {
261 return std::result::Result::Err(crate::errors::AphError::proof_chain_invalid(
262 std::format!(
263 "the principal proof's `verificationMethod` `{}` is not a DID URL under \
264 the human principal `{}`; a proof made by any other key is not the \
265 principal's proof (§7.1.11)",
266 principal.verification_method, human_did
267 ),
268 ));
269 }
270
271 std::result::Result::Ok(crate::envelope::AttestationMode::PrincipalSigned)
272}
273
274pub fn require_mode(
289 envelope: &crate::envelope::NotarizationEnvelope,
290 required: crate::envelope::AttestationMode,
291) -> std::result::Result<(), crate::errors::AphError> {
292 let actual = envelope
293 .credential_subject
294 .policy
295 .effective_attestation_mode();
296 match (required, actual) {
297 (
298 crate::envelope::AttestationMode::PrincipalSigned,
299 crate::envelope::AttestationMode::NotaryAttested,
300 ) => std::result::Result::Err(crate::errors::AphError::attestation_mode_refused(
301 required.label(),
302 actual.label(),
303 )),
304 _ => std::result::Result::Ok(()),
305 }
306}
307
308pub fn verify_embedded_mandate_binding(
347 envelope: &crate::envelope::NotarizationEnvelope,
348) -> std::result::Result<(), crate::errors::AphError> {
349 let subject = &envelope.credential_subject;
350 let mandate = match subject.policy.delegation_mandate.as_ref() {
351 std::option::Option::Some(mandate) => mandate,
352 std::option::Option::None => return std::result::Result::Ok(()),
353 };
354
355 if mandate.human_principal_did != subject.human_principal.id {
356 return std::result::Result::Err(crate::errors::AphError::PrincipalSignatureInvalid);
357 }
358 if mandate.agent_did != subject.agent.id {
359 return std::result::Result::Err(crate::errors::AphError::PrincipalSignatureInvalid);
360 }
361 if let std::option::Option::Some(declared_id) = subject.policy.delegation_mandate_id.as_deref() {
362 if mandate.id != declared_id {
363 return std::result::Result::Err(crate::errors::AphError::PrincipalSignatureInvalid);
364 }
365 }
366
367 if !mandate.allows_channel(subject.channel.kind) {
372 return std::result::Result::Err(crate::errors::AphError::channel_not_allowed(
373 subject.channel.kind.label(),
374 ));
375 }
376
377 if let std::option::Option::Some(allowed) = &mandate.allowed_recipient_classes {
383 let allowed_labels = allowed
384 .iter()
385 .map(crate::envelope::RecipientClass::label)
386 .collect::<std::vec::Vec<&'static str>>()
387 .join(", ");
388 match envelope.credential_subject.channel.recipient_class {
389 std::option::Option::Some(declared) if allowed.contains(&declared) => {}
390 std::option::Option::Some(declared) => {
391 return std::result::Result::Err(crate::errors::AphError::recipient_class_not_allowed(
392 declared.label(),
393 &mandate.id,
394 allowed_labels,
395 ));
396 }
397 std::option::Option::None => {
398 return std::result::Result::Err(crate::errors::AphError::recipient_class_not_allowed(
399 "nothing",
400 &mandate.id,
401 allowed_labels,
402 ));
403 }
404 }
405 }
406
407 if !mandate.is_valid_at(&envelope.valid_from) || !mandate.is_valid_at(&envelope.valid_until) {
413 return std::result::Result::Err(crate::errors::AphError::mandate_expired(
414 mandate.id.as_str(),
415 ));
416 }
417
418 std::result::Result::Ok(())
419}
420
421pub fn verify_timestamp_order(
443 envelope: &crate::envelope::NotarizationEnvelope,
444) -> std::result::Result<(), crate::errors::AphError> {
445 if !envelope.proof.is_chain() {
446 return std::result::Result::Ok(());
447 }
448 let (principal, notary) = match (envelope.proof.principal(), envelope.proof.notary()) {
449 (std::option::Option::Some(p), std::option::Option::Some(n)) => (p, n),
450 _ => {
451 return std::result::Result::Err(crate::errors::AphError::proof_chain_invalid(
452 "cannot check issuance order: `proof` is not a well-formed two-element chain",
453 ));
454 }
455 };
456
457 let decision = parse_rfc3339(
458 &envelope.credential_subject.notarization.decision_timestamp,
459 "credentialSubject.notarization.decisionTimestamp",
460 )?;
461 let principal_created = parse_rfc3339(&principal.created, "the principal proof's `created`")?;
462 let notary_created = parse_rfc3339(¬ary.created, "the notary proof's `created`")?;
463
464 if principal_created < decision {
465 return std::result::Result::Err(crate::errors::AphError::proof_chain_invalid(
466 std::format!(
467 "the principal proof's `created` `{}` precedes \
468 `notarization.decisionTimestamp` `{}`; the principal signs the \
469 envelope the notary has already prepared (§7.2.1)",
470 principal.created, envelope.credential_subject.notarization.decision_timestamp
471 ),
472 ));
473 }
474 if notary_created < principal_created {
475 return std::result::Result::Err(crate::errors::AphError::proof_chain_invalid(
476 std::format!(
477 "the notary proof's `created` `{}` precedes the principal proof's \
478 `created` `{}`; a countersignature cannot predate the proof it \
479 claims to have observed (§7.2.1)",
480 notary.created, principal.created
481 ),
482 ));
483 }
484
485 std::result::Result::Ok(())
486}
487
488pub fn verify_audience(
508 envelope: &crate::envelope::NotarizationEnvelope,
509 verifier_id: &str,
510 act_coordinates: std::option::Option<&crate::envelope::AudienceChannelBinding>,
511) -> std::result::Result<(), crate::errors::AphError> {
512 let audience = match &envelope.credential_subject.audience {
513 std::option::Option::None => return std::result::Result::Ok(()),
514 std::option::Option::Some(audience) => audience,
515 };
516 if audience.id != verifier_id {
517 return std::result::Result::Err(crate::errors::AphError::audience_mismatch(
518 &audience.id,
519 verifier_id,
520 ));
521 }
522 let binding = match &audience.channel_binding {
523 std::option::Option::None => return std::result::Result::Ok(()),
524 std::option::Option::Some(binding) => binding,
525 };
526 let act = match act_coordinates {
527 std::option::Option::Some(act) => act,
528 std::option::Option::None => {
529 return std::result::Result::Err(crate::errors::AphError::audience_mismatch(
533 std::format!("{} (channelBinding present)", audience.id),
534 std::format!("{} (act delivery coordinates unknown to this verifier)", verifier_id),
535 ));
536 }
537 };
538 if binding.kind != act.kind {
539 return std::result::Result::Err(crate::errors::AphError::audience_mismatch(
540 std::format!("{} (channelBinding kind: `{}`)", audience.id, binding.kind.label()),
541 std::format!("{} (act kind: `{}`)", verifier_id, act.kind.label()),
542 ));
543 }
544 for (member, bound) in &binding.coordinates {
545 match act.coordinates.get(member) {
546 std::option::Option::Some(actual) if actual == bound => {}
547 std::option::Option::Some(actual) => {
548 return std::result::Result::Err(crate::errors::AphError::audience_mismatch(
549 std::format!("{} (channelBinding {}: `{}`)", audience.id, member, bound),
550 std::format!("{} (act {}: `{}`)", verifier_id, member, actual),
551 ));
552 }
553 std::option::Option::None => {
554 return std::result::Result::Err(crate::errors::AphError::audience_mismatch(
555 std::format!("{} (channelBinding {}: `{}`)", audience.id, member, bound),
556 std::format!("{} (act has no `{}` coordinate)", verifier_id, member),
557 ));
558 }
559 }
560 }
561 std::result::Result::Ok(())
562}
563
564pub fn verify_envelope_window(
575 envelope: &crate::envelope::NotarizationEnvelope,
576 observed_at: &str,
577) -> std::result::Result<(), crate::errors::AphError> {
578 let window_invalid = || {
579 crate::errors::AphError::envelope_window_invalid(
580 &envelope.valid_from,
581 &envelope.valid_until,
582 observed_at,
583 )
584 };
585 let from = chrono::DateTime::parse_from_rfc3339(&envelope.valid_from)
586 .map_err(|_| window_invalid())?;
587 let until = chrono::DateTime::parse_from_rfc3339(&envelope.valid_until)
588 .map_err(|_| window_invalid())?;
589 let at = chrono::DateTime::parse_from_rfc3339(observed_at).map_err(|_| window_invalid())?;
590 if at < from || at > until {
591 return std::result::Result::Err(window_invalid());
592 }
593 std::result::Result::Ok(())
594}
595
596pub trait ConsumedEnvelopeLedger {
607 fn spend(&mut self, envelope_id: &str) -> bool;
612}
613
614#[derive(std::fmt::Debug, std::default::Default)]
618pub struct InMemoryConsumedEnvelopeLedger {
619 spent: std::collections::BTreeSet<String>,
620}
621
622impl ConsumedEnvelopeLedger for InMemoryConsumedEnvelopeLedger {
623 fn spend(&mut self, envelope_id: &str) -> bool {
624 self.spent.insert(envelope_id.to_string())
625 }
626}
627
628pub fn enforce_single_use(
635 ledger: &mut dyn ConsumedEnvelopeLedger,
636 envelope: &crate::envelope::NotarizationEnvelope,
637) -> std::result::Result<(), crate::errors::AphError> {
638 if ledger.spend(&envelope.id) {
639 std::result::Result::Ok(())
640 } else {
641 std::result::Result::Err(crate::errors::AphError::envelope_already_spent(&envelope.id))
642 }
643}
644
645fn parse_rfc3339(
649 value: &str,
650 field: &str,
651) -> std::result::Result<
652 chrono::DateTime<chrono::FixedOffset>,
653 crate::errors::AphError,
654> {
655 chrono::DateTime::parse_from_rfc3339(value).map_err(|_| {
656 crate::errors::AphError::proof_chain_invalid(std::format!(
657 "{} `{}` is not a parseable RFC 3339 timestamp, so issuance order \
658 cannot be checked",
659 field, value
660 ))
661 })
662}
663
664#[cfg(test)]
665mod tests {
666 const HUMAN_DID: &str = "did:key:z6MkfAkfRZ3v9zJWh9LM2YQbWLh6hqGYDVxxC7ueoVcd5dGy";
674 const NOTARY_DID: &str = "did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSdoVTwBaPaeT1KhFmkV";
675 const AGENT_DID: &str = "did:web:agent.squillo.com";
676 const PRINCIPAL_PROOF_ID: &str = "urn:uuid:00000000-0000-4000-8000-0000000000a1";
677 const NOTARY_PROOF_ID: &str = "urn:uuid:00000000-0000-4000-8000-0000000000b1";
678 const MANDATE_ID: &str = "urn:uuid:00000000-0000-4000-8000-0000000000c1";
679
680 fn notary_proof() -> crate::envelope::EnvelopeProof {
681 crate::envelope::EnvelopeProof {
682 r#type: std::string::String::from("DataIntegrityProof"),
683 cryptosuite: std::option::Option::Some(std::string::String::from("eddsa-jcs-2022")),
684 verification_method: std::format!("{}#{}", NOTARY_DID, "z6MkpTHR8VNsBxYAAWHut2Geadd9jSdoVTwBaPaeT1KhFmkV"),
685 created: std::string::String::from("2026-05-21T00:00:01Z"),
686 proof_purpose: std::string::String::from("assertionMethod"),
687 proof_value: std::string::String::from("z-illustrative-lone-notary-proof"),
688 id: std::option::Option::None,
689 previous_proof: std::option::Option::None,
690 }
691 }
692
693 fn principal_proof() -> crate::envelope::EnvelopeProof {
694 crate::envelope::EnvelopeProof {
695 r#type: std::string::String::from("DataIntegrityProof"),
696 cryptosuite: std::option::Option::Some(std::string::String::from("eddsa-jcs-2022")),
697 verification_method: std::format!("{}#{}", HUMAN_DID, "z6MkfAkfRZ3v9zJWh9LM2YQbWLh6hqGYDVxxC7ueoVcd5dGy"),
698 created: std::string::String::from("2026-05-21T00:00:02Z"),
699 proof_purpose: std::string::String::from("assertionMethod"),
700 proof_value: std::string::String::from("z-illustrative-principal-proof"),
701 id: std::option::Option::Some(std::string::String::from(PRINCIPAL_PROOF_ID)),
702 previous_proof: std::option::Option::None,
703 }
704 }
705
706 fn countersignature() -> crate::envelope::EnvelopeProof {
707 crate::envelope::EnvelopeProof {
708 r#type: std::string::String::from("DataIntegrityProof"),
709 cryptosuite: std::option::Option::Some(std::string::String::from("eddsa-jcs-2022")),
710 verification_method: std::format!("{}#{}", NOTARY_DID, "z6MkpTHR8VNsBxYAAWHut2Geadd9jSdoVTwBaPaeT1KhFmkV"),
711 created: std::string::String::from("2026-05-21T00:00:03Z"),
712 proof_purpose: std::string::String::from("authentication"),
713 proof_value: std::string::String::from("z-illustrative-countersignature"),
714 id: std::option::Option::Some(std::string::String::from(NOTARY_PROOF_ID)),
715 previous_proof: std::option::Option::Some(std::string::String::from(PRINCIPAL_PROOF_ID)),
716 }
717 }
718
719 fn mandate() -> crate::delegation_mandate::DelegationMandate {
720 crate::delegation_mandate::DelegationMandate {
721 id: std::string::String::from(MANDATE_ID),
722 human_principal_did: std::string::String::from(HUMAN_DID),
723 agent_did: std::string::String::from(AGENT_DID),
724 allowed_channels: std::vec![crate::envelope::ChannelKind::Slack, crate::envelope::ChannelKind::Email],
725 allowed_recipient_classes: std::option::Option::None,
726 rate_limit_per_hour: std::option::Option::Some(12),
727 valid_from: std::string::String::from("2026-05-01T00:00:00Z"),
728 valid_until: std::string::String::from("2026-06-01T00:00:00Z"),
729 principal_signature: std::string::String::from("z-illustrative-principal-signature"),
730 notary_signature: std::string::String::from("z-illustrative-notary-signature"),
731 }
732 }
733
734 fn single_proof_envelope() -> crate::envelope::NotarizationEnvelope {
737 crate::envelope::NotarizationEnvelope {
738 aph_version: std::string::String::from("0.1"),
739 context: std::vec![
740 std::string::String::from("https://www.w3.org/ns/credentials/v2"),
741 std::string::String::from("https://w3id.org/aph/v1"),
742 ],
743 r#type: std::vec![
744 std::string::String::from("VerifiableCredential"),
745 std::string::String::from("AgentSendAuthorizationCredential"),
746 ],
747 id: std::string::String::from("urn:uuid:00000000-0000-4000-8000-000000000001"),
748 issuer: std::string::String::from(NOTARY_DID),
749 valid_from: std::string::String::from("2026-05-21T00:00:00Z"),
750 valid_until: std::string::String::from("2026-05-22T00:00:00Z"),
751 credential_subject: crate::envelope::CredentialSubject {
752 human_principal: crate::envelope::HumanPrincipalRef {
753 id: std::string::String::from(HUMAN_DID),
754 display_name: std::string::String::from("Scott Wyatt"),
755 },
756 agent: crate::envelope::AgentRef {
757 id: std::string::String::from(AGENT_DID),
758 agent_card_uri: std::option::Option::None,
759 display_name: std::string::String::from("Squillo Concierge"),
760 version: std::string::String::from("1.0"),
761 },
762 channel: crate::envelope::ChannelDescriptor {
763 kind: crate::envelope::ChannelKind::Slack,
764 recipient_addressing: serde_json::json!({"teamId": "T01234567"}),
765 recipient_class: std::option::Option::None,
766 },
767 communication: crate::envelope::CommunicationDescriptor {
768 content_class: crate::envelope::ContentClass::Reply,
769 body_sha256: std::string::String::from(
770 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
771 ),
772 body_size: 1842,
773 preview_lines: 3,
774 preview: std::string::String::from("hello world"),
775 },
776 policy: crate::envelope::PolicyDescriptor {
777 decision: crate::envelope::PolicyDecision::AskEveryTime,
778 matched_scope: std::string::String::from("per-channel"),
779 delegation_mandate_id: std::option::Option::None,
780 act_chain: std::vec::Vec::new(),
781 attestation_mode: std::option::Option::None,
782 delegation_mandate: std::option::Option::None,
783 },
784 notarization: crate::envelope::NotarizationMetadata {
785 notary_service: crate::envelope::NotaryServiceRef {
786 id: std::string::String::from("did:web:notary.squillo.com"),
787 name: std::string::String::from("Squillo Notary Service"),
788 version: std::string::String::from("0.1.0"),
789 attested_digest: std::option::Option::None,
790 attestation_uri: std::option::Option::None,
791 },
792 decision_timestamp: std::string::String::from("2026-05-21T00:00:01Z"),
793 decision_latency_ms: 1834,
794 },
795 apple_aur_acceptance: std::option::Option::None,
796 audience: std::option::Option::None,
797 sealed_payload: std::option::Option::None,
798 act_classification: std::option::Option::None,
799 },
800 linked_mandate: std::option::Option::None,
801 credential_status: std::option::Option::None,
805 proof: crate::envelope::EnvelopeProofs::Single(notary_proof()),
806 }
807 }
808
809 fn chain_envelope() -> crate::envelope::NotarizationEnvelope {
812 let mut envelope = single_proof_envelope();
813 envelope.credential_subject.policy.attestation_mode =
814 std::option::Option::Some(crate::envelope::AttestationMode::PrincipalSigned);
815 envelope.proof =
816 crate::envelope::EnvelopeProofs::Chain(std::vec![principal_proof(), countersignature()]);
817 envelope
818 }
819
820 fn chain_envelope_with(
823 mutate: impl std::ops::FnOnce(
824 &mut crate::envelope::EnvelopeProof,
825 &mut crate::envelope::EnvelopeProof,
826 ),
827 ) -> crate::envelope::NotarizationEnvelope {
828 let mut principal = principal_proof();
829 let mut notary = countersignature();
830 mutate(&mut principal, &mut notary);
831 let mut envelope = chain_envelope();
832 envelope.proof = crate::envelope::EnvelopeProofs::Chain(std::vec![principal, notary]);
833 envelope
834 }
835
836 #[test]
839 fn single_proof_without_a_label_verifies_as_notary_attested() {
840 let mode = super::verify_proof_structure(&single_proof_envelope())
846 .expect("a lone notary proof is a well-formed NotaryAttested envelope");
847 std::assert_eq!(mode, crate::envelope::AttestationMode::NotaryAttested);
848 }
849
850 #[test]
851 fn a_lone_proof_carrying_chain_members_is_rejected() {
852 let mut envelope = single_proof_envelope();
858 if let crate::envelope::EnvelopeProofs::Single(proof) = &mut envelope.proof {
859 proof.previous_proof =
860 std::option::Option::Some(std::string::String::from("urn:uuid:00000000-0000-4000-8000-0000000000f1"));
861 }
862 let err = super::verify_proof_structure(&envelope)
863 .expect_err("a lone proof naming a previous proof is not well formed");
864 std::assert_eq!(err.code(), "APH_E013");
865 }
866
867 #[test]
868 fn a_lone_proof_using_the_countersignature_purpose_is_rejected() {
869 let mut envelope = single_proof_envelope();
875 if let crate::envelope::EnvelopeProofs::Single(proof) = &mut envelope.proof {
876 proof.proof_purpose = std::string::String::from("authentication");
877 }
878 let err = super::verify_proof_structure(&envelope)
879 .expect_err("a lone proof may not use the countersignature purpose");
880 std::assert_eq!(err.code(), "APH_E013");
881 }
882
883 #[test]
884 fn single_proof_labelled_notary_attested_verifies() {
885 let mut envelope = single_proof_envelope();
889 envelope.credential_subject.policy.attestation_mode =
890 std::option::Option::Some(crate::envelope::AttestationMode::NotaryAttested);
891 let mode = super::verify_proof_structure(&envelope)
892 .expect("an explicitly-labelled NotaryAttested envelope is well formed");
893 std::assert_eq!(mode, crate::envelope::AttestationMode::NotaryAttested);
894 }
895
896 #[test]
897 fn well_formed_chain_verifies_as_principal_signed() {
898 let mode = super::verify_proof_structure(&chain_envelope())
903 .expect("a correctly linked and labelled chain must verify");
904 std::assert_eq!(mode, crate::envelope::AttestationMode::PrincipalSigned);
905 }
906
907 #[test]
908 fn principal_verification_method_without_a_fragment_is_accepted() {
909 let envelope = chain_envelope_with(|principal, _notary| {
915 principal.verification_method = std::string::String::from(HUMAN_DID);
916 });
917 std::assert!(super::verify_proof_structure(&envelope).is_ok());
918 }
919
920 #[test]
923 fn a_one_element_chain_is_rejected() {
924 let mut envelope = chain_envelope();
930 envelope.proof = crate::envelope::EnvelopeProofs::Chain(std::vec![principal_proof()]);
931 let err = super::verify_proof_structure(&envelope)
932 .expect_err("a one-element chain must be rejected");
933 std::assert_eq!(err.code(), "APH_E013");
934 }
935
936 #[test]
937 fn a_three_element_chain_is_rejected() {
938 let mut envelope = chain_envelope();
943 envelope.proof = crate::envelope::EnvelopeProofs::Chain(std::vec![
944 principal_proof(),
945 countersignature(),
946 countersignature(),
947 ]);
948 let err = super::verify_proof_structure(&envelope)
949 .expect_err("a three-element chain must be rejected");
950 std::assert_eq!(err.code(), "APH_E013");
951 }
952
953 #[test]
954 fn an_empty_chain_is_rejected() {
955 let mut envelope = chain_envelope();
960 envelope.proof = crate::envelope::EnvelopeProofs::Chain(std::vec::Vec::new());
961 let err =
962 super::verify_proof_structure(&envelope).expect_err("an empty chain must be rejected");
963 std::assert_eq!(err.code(), "APH_E013");
964 }
965
966 #[test]
969 fn a_chain_with_no_attestation_mode_is_rejected() {
970 let mut envelope = chain_envelope();
976 envelope.credential_subject.policy.attestation_mode = std::option::Option::None;
977 let err = super::verify_proof_structure(&envelope)
978 .expect_err("an unlabelled chain must be rejected");
979 std::assert_eq!(err.code(), "APH_E013");
980 }
981
982 #[test]
983 fn a_chain_labelled_notary_attested_is_rejected() {
984 let mut envelope = chain_envelope();
990 envelope.credential_subject.policy.attestation_mode =
991 std::option::Option::Some(crate::envelope::AttestationMode::NotaryAttested);
992 let err = super::verify_proof_structure(&envelope)
993 .expect_err("a chain labelled NotaryAttested must be rejected");
994 std::assert_eq!(err.code(), "APH_E013");
995 }
996
997 #[test]
998 fn a_single_proof_labelled_principal_signed_is_rejected() {
999 let mut envelope = single_proof_envelope();
1005 envelope.credential_subject.policy.attestation_mode =
1006 std::option::Option::Some(crate::envelope::AttestationMode::PrincipalSigned);
1007 let err = super::verify_proof_structure(&envelope)
1008 .expect_err("a PrincipalSigned label over a single proof must be rejected");
1009 std::assert_eq!(err.code(), "APH_E013");
1010 }
1011
1012 #[test]
1015 fn a_principal_proof_with_the_wrong_purpose_is_rejected() {
1016 let envelope = chain_envelope_with(|principal, _notary| {
1021 principal.proof_purpose = std::string::String::from("authentication");
1022 });
1023 let err = super::verify_proof_structure(&envelope)
1024 .expect_err("principal proof must carry assertionMethod");
1025 std::assert_eq!(err.code(), "APH_E013");
1026 }
1027
1028 #[test]
1029 fn a_notary_proof_with_the_wrong_purpose_is_rejected() {
1030 let envelope = chain_envelope_with(|_principal, notary| {
1035 notary.proof_purpose = std::string::String::from("assertionMethod");
1036 });
1037 let err = super::verify_proof_structure(&envelope)
1038 .expect_err("notary proof must carry authentication");
1039 std::assert_eq!(err.code(), "APH_E013");
1040 }
1041
1042 #[test]
1045 fn a_principal_proof_without_an_id_is_rejected() {
1046 let envelope = chain_envelope_with(|principal, notary| {
1050 principal.id = std::option::Option::None;
1051 notary.previous_proof = std::option::Option::None;
1052 });
1053 let err = super::verify_proof_structure(&envelope)
1054 .expect_err("a chain proof without an id must be rejected");
1055 std::assert_eq!(err.code(), "APH_E013");
1056 }
1057
1058 #[test]
1059 fn a_notary_proof_without_an_id_is_rejected() {
1060 let envelope = chain_envelope_with(|_principal, notary| {
1065 notary.id = std::option::Option::None;
1066 });
1067 let err = super::verify_proof_structure(&envelope)
1068 .expect_err("the notary proof must carry an id");
1069 std::assert_eq!(err.code(), "APH_E013");
1070 }
1071
1072 #[test]
1073 fn two_proofs_sharing_one_id_are_rejected() {
1074 let envelope = chain_envelope_with(|_principal, notary| {
1078 notary.id = std::option::Option::Some(std::string::String::from(PRINCIPAL_PROOF_ID));
1079 notary.previous_proof = std::option::Option::Some(std::string::String::from(PRINCIPAL_PROOF_ID));
1080 });
1081 let err = super::verify_proof_structure(&envelope)
1082 .expect_err("duplicate proof ids must be rejected");
1083 std::assert_eq!(err.code(), "APH_E013");
1084 }
1085
1086 #[test]
1089 fn a_notary_proof_without_previous_proof_is_rejected() {
1090 let envelope = chain_envelope_with(|_principal, notary| {
1095 notary.previous_proof = std::option::Option::None;
1096 });
1097 let err = super::verify_proof_structure(&envelope)
1098 .expect_err("missing previousProof must be rejected");
1099 std::assert_eq!(err.code(), "APH_E013");
1100 std::assert!(
1101 std::string::ToString::to_string(&err).contains("previousProof"),
1102 "the reason must name the missing member: {}",
1103 err
1104 );
1105 }
1106
1107 #[test]
1108 fn a_dangling_previous_proof_is_rejected() {
1109 let envelope = chain_envelope_with(|_principal, notary| {
1114 notary.previous_proof =
1115 std::option::Option::Some(std::string::String::from("urn:uuid:not-in-this-chain"));
1116 });
1117 let err = super::verify_proof_structure(&envelope)
1118 .expect_err("a dangling previousProof must be rejected");
1119 std::assert_eq!(err.code(), "APH_E013");
1120 std::assert!(
1121 std::string::ToString::to_string(&err).contains("dangling"),
1122 "the reason must distinguish dangling from missing: {}",
1123 err
1124 );
1125 }
1126
1127 #[test]
1128 fn a_self_referential_previous_proof_is_rejected() {
1129 let envelope = chain_envelope_with(|_principal, notary| {
1134 notary.previous_proof = std::option::Option::Some(std::string::String::from(NOTARY_PROOF_ID));
1135 });
1136 let err = super::verify_proof_structure(&envelope)
1137 .expect_err("a self-referential previousProof must be rejected");
1138 std::assert_eq!(err.code(), "APH_E013");
1139 std::assert!(
1140 std::string::ToString::to_string(&err).contains("self-referential"),
1141 "the reason must distinguish a cycle from a dangling link: {}",
1142 err
1143 );
1144 }
1145
1146 #[test]
1147 fn a_principal_proof_carrying_previous_proof_is_rejected() {
1148 let envelope = chain_envelope_with(|principal, _notary| {
1153 principal.previous_proof = std::option::Option::Some(std::string::String::from(NOTARY_PROOF_ID));
1154 });
1155 let err = super::verify_proof_structure(&envelope)
1156 .expect_err("the chain head must not carry previousProof");
1157 std::assert_eq!(err.code(), "APH_E013");
1158 }
1159
1160 #[test]
1163 fn a_principal_proof_made_by_another_key_is_rejected() {
1164 let envelope = chain_envelope_with(|principal, _notary| {
1171 principal.verification_method = std::format!("{}#key-1", NOTARY_DID);
1172 });
1173 let err = super::verify_proof_structure(&envelope)
1174 .expect_err("a principal proof under a foreign DID must be rejected");
1175 std::assert_eq!(err.code(), "APH_E013");
1176 }
1177
1178 #[test]
1179 fn a_principal_verification_method_that_only_prefixes_the_human_did_is_rejected() {
1180 let envelope = chain_envelope_with(|principal, _notary| {
1185 principal.verification_method = std::format!("{}-attacker#key-1", HUMAN_DID);
1186 });
1187 let err = super::verify_proof_structure(&envelope)
1188 .expect_err("a DID that merely starts with the human's DID must be rejected");
1189 std::assert_eq!(err.code(), "APH_E013");
1190 }
1191
1192 #[test]
1195 fn require_principal_signed_refuses_a_notary_attested_envelope() {
1196 let err = super::require_mode(
1202 &single_proof_envelope(),
1203 crate::envelope::AttestationMode::PrincipalSigned,
1204 )
1205 .expect_err("a NotaryAttested envelope must not satisfy a PrincipalSigned policy");
1206 std::assert_eq!(err.code(), "APH_E012");
1207 }
1208
1209 #[test]
1210 fn require_notary_attested_accepts_a_principal_signed_envelope() {
1211 std::assert!(
1215 super::require_mode(
1216 &chain_envelope(),
1217 crate::envelope::AttestationMode::NotaryAttested,
1218 )
1219 .is_ok()
1220 );
1221 }
1222
1223 #[test]
1224 fn require_mode_accepts_an_exact_match() {
1225 std::assert!(
1228 super::require_mode(
1229 &single_proof_envelope(),
1230 crate::envelope::AttestationMode::NotaryAttested,
1231 )
1232 .is_ok()
1233 );
1234 }
1235
1236 #[test]
1237 fn require_mode_reads_absence_as_the_weaker_mode() {
1238 let mut envelope = single_proof_envelope();
1243 envelope.credential_subject.policy.attestation_mode = std::option::Option::None;
1244 let err = super::require_mode(&envelope, crate::envelope::AttestationMode::PrincipalSigned)
1245 .expect_err("an unlabelled envelope must not satisfy a PrincipalSigned policy");
1246 std::assert_eq!(err.code(), "APH_E012");
1247 }
1248
1249 #[test]
1252 fn an_absent_embedded_mandate_is_not_a_structural_error() {
1253 std::assert!(super::verify_embedded_mandate_binding(&single_proof_envelope()).is_ok());
1258 }
1259
1260 #[test]
1261 fn a_matching_embedded_mandate_binds() {
1262 let mut envelope = single_proof_envelope();
1266 envelope.credential_subject.policy.delegation_mandate_id =
1267 std::option::Option::Some(std::string::String::from(MANDATE_ID));
1268 envelope.credential_subject.policy.delegation_mandate = std::option::Option::Some(mandate());
1269 std::assert!(super::verify_embedded_mandate_binding(&envelope).is_ok());
1270 }
1271
1272 #[test]
1273 fn an_embedded_mandate_for_another_human_is_rejected() {
1274 let mut envelope = single_proof_envelope();
1280 let mut stapled = mandate();
1281 stapled.human_principal_did = std::string::String::from("did:key:z6MkSomeoneElse");
1282 envelope.credential_subject.policy.delegation_mandate = std::option::Option::Some(stapled);
1283 let err = super::verify_embedded_mandate_binding(&envelope)
1284 .expect_err("a mandate for another human must not bind");
1285 std::assert_eq!(err.code(), "APH_E011");
1286 }
1287
1288 #[test]
1289 fn an_embedded_mandate_for_another_agent_is_rejected() {
1290 let mut envelope = single_proof_envelope();
1295 let mut stapled = mandate();
1296 stapled.agent_did = std::string::String::from("did:web:other-agent.example");
1297 envelope.credential_subject.policy.delegation_mandate = std::option::Option::Some(stapled);
1298 let err = super::verify_embedded_mandate_binding(&envelope)
1299 .expect_err("a mandate for another agent must not bind");
1300 std::assert_eq!(err.code(), "APH_E011");
1301 }
1302
1303 #[test]
1304 fn an_embedded_mandate_disagreeing_with_the_declared_id_is_rejected() {
1305 let mut envelope = single_proof_envelope();
1310 envelope.credential_subject.policy.delegation_mandate_id =
1311 std::option::Option::Some(std::string::String::from("urn:uuid:some-other-mandate"));
1312 envelope.credential_subject.policy.delegation_mandate = std::option::Option::Some(mandate());
1313 let err = super::verify_embedded_mandate_binding(&envelope)
1314 .expect_err("an embedded mandate must be the one the envelope names");
1315 std::assert_eq!(err.code(), "APH_E011");
1316 }
1317
1318 #[test]
1319 fn an_embedded_mandate_not_covering_the_channel_is_rejected() {
1320 let mut envelope = single_proof_envelope();
1326 envelope.credential_subject.channel.kind = crate::envelope::ChannelKind::Discord;
1327 envelope.credential_subject.policy.delegation_mandate = std::option::Option::Some(mandate());
1328 let err = super::verify_embedded_mandate_binding(&envelope)
1329 .expect_err("a channel outside allowedChannels must not bind");
1330 std::assert_eq!(err.code(), "APH_E005");
1331 }
1332
1333 #[test]
1334 fn an_envelope_outside_the_mandate_window_is_rejected() {
1335 let mut envelope = single_proof_envelope();
1340 envelope.valid_from = std::string::String::from("2026-07-01T00:00:00Z");
1341 envelope.valid_until = std::string::String::from("2026-07-02T00:00:00Z");
1342 envelope.credential_subject.policy.delegation_mandate = std::option::Option::Some(mandate());
1343 let err = super::verify_embedded_mandate_binding(&envelope)
1344 .expect_err("an envelope outside the mandate window must not bind");
1345 std::assert_eq!(err.code(), "APH_E003");
1346 }
1347
1348 #[test]
1349 fn an_envelope_expiring_after_the_mandate_is_rejected() {
1350 let mut envelope = single_proof_envelope();
1355 envelope.valid_from = std::string::String::from("2026-05-31T00:00:00Z");
1356 envelope.valid_until = std::string::String::from("2026-06-30T00:00:00Z");
1357 envelope.credential_subject.policy.delegation_mandate = std::option::Option::Some(mandate());
1358 let err = super::verify_embedded_mandate_binding(&envelope)
1359 .expect_err("an envelope outliving the mandate must not bind");
1360 std::assert_eq!(err.code(), "APH_E003");
1361 }
1362
1363 #[test]
1364 fn an_unparseable_envelope_window_fails_closed() {
1365 let mut envelope = single_proof_envelope();
1370 envelope.valid_from = std::string::String::from("not-a-timestamp");
1371 envelope.credential_subject.policy.delegation_mandate = std::option::Option::Some(mandate());
1372 let err = super::verify_embedded_mandate_binding(&envelope)
1373 .expect_err("an unparseable window must not bind");
1374 std::assert_eq!(err.code(), "APH_E003");
1375 }
1376
1377 #[test]
1380 fn a_single_proof_envelope_has_nothing_to_order() {
1381 std::assert!(super::verify_timestamp_order(&single_proof_envelope()).is_ok());
1385 }
1386
1387 #[test]
1388 fn issuance_order_decision_then_principal_then_notary_is_accepted() {
1389 std::assert!(super::verify_timestamp_order(&chain_envelope()).is_ok());
1393 }
1394
1395 #[test]
1396 fn equal_timestamps_are_accepted() {
1397 let mut envelope = chain_envelope_with(|principal, notary| {
1402 principal.created = std::string::String::from("2026-05-21T00:00:01Z");
1403 notary.created = std::string::String::from("2026-05-21T00:00:01Z");
1404 });
1405 envelope.credential_subject.notarization.decision_timestamp =
1406 std::string::String::from("2026-05-21T00:00:01Z");
1407 std::assert!(super::verify_timestamp_order(&envelope).is_ok());
1408 }
1409
1410 #[test]
1411 fn a_principal_proof_predating_the_decision_is_rejected() {
1412 let envelope = chain_envelope_with(|principal, _notary| {
1418 principal.created = std::string::String::from("2026-05-21T00:00:00Z");
1419 });
1420 let err = super::verify_timestamp_order(&envelope)
1421 .expect_err("a principal proof predating the decision must be rejected");
1422 std::assert_eq!(err.code(), "APH_E013");
1423 }
1424
1425 #[test]
1426 fn a_countersignature_predating_the_principal_proof_is_rejected() {
1427 let envelope = chain_envelope_with(|_principal, notary| {
1432 notary.created = std::string::String::from("2026-05-21T00:00:01Z");
1433 });
1434 let err = super::verify_timestamp_order(&envelope)
1435 .expect_err("a countersignature predating its principal proof must be rejected");
1436 std::assert_eq!(err.code(), "APH_E013");
1437 }
1438
1439 #[test]
1440 fn an_unparseable_proof_timestamp_is_rejected() {
1441 let envelope = chain_envelope_with(|principal, _notary| {
1445 principal.created = std::string::String::from("yesterday");
1446 });
1447 let err = super::verify_timestamp_order(&envelope)
1448 .expect_err("an unparseable timestamp must be rejected");
1449 std::assert_eq!(err.code(), "APH_E013");
1450 }
1451
1452 #[test]
1453 fn timestamps_are_compared_as_instants_not_strings() {
1454 let envelope = chain_envelope_with(|_principal, notary| {
1459 notary.created = std::string::String::from("2026-05-20T20:00:03-04:00");
1460 });
1461 std::assert!(
1462 super::verify_timestamp_order(&envelope).is_ok(),
1463 "an equivalent instant written with an offset must order correctly"
1464 );
1465 }
1466
1467 #[test]
1468 fn a_malformed_chain_cannot_have_its_order_checked() {
1469 let mut envelope = chain_envelope();
1473 envelope.proof = crate::envelope::EnvelopeProofs::Chain(std::vec![principal_proof()]);
1474 let err = super::verify_timestamp_order(&envelope)
1475 .expect_err("a one-element chain has no checkable order");
1476 std::assert_eq!(err.code(), "APH_E013");
1477 }
1478 fn audience(id: &str) -> crate::envelope::Audience {
1481 crate::envelope::Audience {
1482 id: id.to_string(),
1483 channel_binding: std::option::Option::None,
1484 }
1485 }
1486
1487 fn binding(
1488 kind: crate::envelope::ChannelKind,
1489 coordinates: &[(&str, &str)],
1490 ) -> crate::envelope::AudienceChannelBinding {
1491 crate::envelope::AudienceChannelBinding {
1492 kind,
1493 coordinates: coordinates
1494 .iter()
1495 .map(|(k, v)| (k.to_string(), serde_json::json!(v)))
1496 .collect(),
1497 }
1498 }
1499
1500 #[test]
1501 fn an_envelope_with_no_audience_is_a_bearer_credential_by_choice() {
1502 let envelope = single_proof_envelope();
1506 super::verify_audience(&envelope, "did:web:anyone.example.com", std::option::Option::None)
1507 .expect("absence is not a constraint");
1508 }
1509
1510 #[test]
1511 fn an_audience_naming_another_verifier_is_refused() {
1512 let mut envelope = single_proof_envelope();
1516 envelope.credential_subject.audience =
1517 std::option::Option::Some(audience("did:web:intended.example.com"));
1518 let err =
1519 super::verify_audience(&envelope, "did:web:other.example.com", std::option::Option::None)
1520 .expect_err("a mismatched audience must refuse");
1521 std::assert_eq!(err.code(), "APH_E017");
1522 let text = std::format!("{}", err);
1523 std::assert!(text.contains("did:web:intended.example.com"));
1524 std::assert!(text.contains("did:web:other.example.com"));
1525 }
1526
1527 #[test]
1528 fn the_named_verifier_is_admitted() {
1529 let mut envelope = single_proof_envelope();
1532 envelope.credential_subject.audience =
1533 std::option::Option::Some(audience("did:web:intended.example.com"));
1534 super::verify_audience(&envelope, "did:web:intended.example.com", std::option::Option::None)
1535 .expect("the named audience is exactly who may accept");
1536 }
1537
1538 #[test]
1539 fn a_channel_binding_with_no_act_coordinates_is_refused_not_skipped() {
1540 let mut envelope = single_proof_envelope();
1545 let mut aud = audience("did:web:intended.example.com");
1546 aud.channel_binding = std::option::Option::Some(binding(
1547 crate::envelope::ChannelKind::Slack,
1548 &[("teamId", "T01234567")],
1549 ));
1550 envelope.credential_subject.audience = std::option::Option::Some(aud);
1551 let err = super::verify_audience(
1552 &envelope,
1553 "did:web:intended.example.com",
1554 std::option::Option::None,
1555 )
1556 .expect_err("unknown act coordinates must refuse, not skip");
1557 std::assert_eq!(err.code(), "APH_E017");
1558 }
1559
1560 #[test]
1561 fn a_channel_binding_member_mismatch_is_refused_naming_the_member() {
1562 let mut envelope = single_proof_envelope();
1566 let mut aud = audience("did:web:intended.example.com");
1567 aud.channel_binding = std::option::Option::Some(binding(
1568 crate::envelope::ChannelKind::Slack,
1569 &[("teamId", "T01234567"), ("channelId", "C01234567")],
1570 ));
1571 envelope.credential_subject.audience = std::option::Option::Some(aud);
1572 let act = binding(
1573 crate::envelope::ChannelKind::Slack,
1574 &[("teamId", "T01234567"), ("channelId", "C_ELSEWHERE")],
1575 );
1576 let err = super::verify_audience(
1577 &envelope,
1578 "did:web:intended.example.com",
1579 std::option::Option::Some(&act),
1580 )
1581 .expect_err("a differing coordinate must refuse");
1582 std::assert_eq!(err.code(), "APH_E017");
1583 std::assert!(std::format!("{}", err).contains("channelId"));
1584 }
1585
1586 #[test]
1587 fn a_matching_channel_binding_is_admitted_and_extra_act_members_are_fine() {
1588 let mut envelope = single_proof_envelope();
1593 let mut aud = audience("did:web:intended.example.com");
1594 aud.channel_binding = std::option::Option::Some(binding(
1595 crate::envelope::ChannelKind::Slack,
1596 &[("teamId", "T01234567")],
1597 ));
1598 envelope.credential_subject.audience = std::option::Option::Some(aud);
1599 let act = binding(
1600 crate::envelope::ChannelKind::Slack,
1601 &[("teamId", "T01234567"), ("threadTs", "1727000000.000100")],
1602 );
1603 super::verify_audience(
1604 &envelope,
1605 "did:web:intended.example.com",
1606 std::option::Option::Some(&act),
1607 )
1608 .expect("every bound member matches");
1609 }
1610
1611 #[test]
1612 fn a_kind_mismatch_in_the_binding_is_refused() {
1613 let mut envelope = single_proof_envelope();
1617 let mut aud = audience("did:web:intended.example.com");
1618 aud.channel_binding =
1619 std::option::Option::Some(binding(crate::envelope::ChannelKind::Slack, &[]));
1620 envelope.credential_subject.audience = std::option::Option::Some(aud);
1621 let act = binding(crate::envelope::ChannelKind::Email, &[]);
1622 let err = super::verify_audience(
1623 &envelope,
1624 "did:web:intended.example.com",
1625 std::option::Option::Some(&act),
1626 )
1627 .expect_err("kind is a bound member like any other");
1628 std::assert_eq!(err.code(), "APH_E017");
1629 }
1630
1631 #[test]
1632 fn the_envelope_window_admits_inside_and_refuses_both_sides_with_e019() {
1633 let envelope = single_proof_envelope();
1639 super::verify_envelope_window(&envelope, "2026-05-21T12:00:00Z").expect("inside the window");
1640 let early = super::verify_envelope_window(&envelope, "2026-05-20T23:59:59Z")
1641 .expect_err("not yet valid");
1642 std::assert_eq!(early.code(), "APH_E019");
1643 let late = super::verify_envelope_window(&envelope, "2026-05-22T00:00:01Z")
1644 .expect_err("expired");
1645 std::assert_eq!(late.code(), "APH_E019");
1646 std::assert!(std::format!("{}", late).contains("2026-05-22T00:00:00Z"));
1647 }
1648
1649 #[test]
1650 fn an_unparseable_window_fails_closed_to_e019() {
1651 let mut envelope = single_proof_envelope();
1656 envelope.valid_until = std::string::String::from("not-a-timestamp");
1657 let err = super::verify_envelope_window(&envelope, "2026-05-21T12:00:00Z")
1658 .expect_err("garbage must refuse");
1659 std::assert_eq!(err.code(), "APH_E019");
1660 }
1661
1662 #[test]
1663 fn an_envelope_is_spent_by_acceptance_and_a_second_presentation_refuses() {
1664 let mut ledger = super::InMemoryConsumedEnvelopeLedger::default();
1669 let envelope = single_proof_envelope();
1670 super::enforce_single_use(&mut ledger, &envelope).expect("first acceptance spends");
1671 let err = super::enforce_single_use(&mut ledger, &envelope)
1672 .expect_err("second presentation is a replay");
1673 std::assert_eq!(err.code(), "APH_E018");
1674 std::assert!(std::format!("{}", err).contains(&envelope.id));
1675 let again =
1676 super::enforce_single_use(&mut ledger, &envelope).expect_err("and it stays spent");
1677 std::assert_eq!(again.code(), "APH_E018");
1678 }
1679
1680 #[test]
1681 fn distinct_envelope_ids_spend_independently() {
1682 let mut ledger = super::InMemoryConsumedEnvelopeLedger::default();
1686 let first = single_proof_envelope();
1687 let mut second = single_proof_envelope();
1688 second.id = std::string::String::from("urn:uuid:00000000-0000-4000-8000-000000000002");
1689 super::enforce_single_use(&mut ledger, &first).expect("first id is fresh");
1690 super::enforce_single_use(&mut ledger, &second).expect("second id is fresh too");
1691 }
1692 #[test]
1695 fn an_unconstrained_mandate_ignores_recipient_class_entirely() {
1696 let mut envelope = single_proof_envelope();
1700 envelope.credential_subject.policy.delegation_mandate = std::option::Option::Some(mandate());
1701 super::verify_embedded_mandate_binding(&envelope)
1702 .expect("no constraint, nothing to check");
1703 }
1704
1705 #[test]
1706 fn a_declared_class_inside_the_grant_is_admitted() {
1707 let mut envelope = single_proof_envelope();
1708 envelope.credential_subject.channel.recipient_class =
1709 std::option::Option::Some(crate::envelope::RecipientClass::Human);
1710 let mut constrained = mandate();
1711 constrained.allowed_recipient_classes =
1712 std::option::Option::Some(std::vec![crate::envelope::RecipientClass::Human]);
1713 envelope.credential_subject.policy.delegation_mandate =
1714 std::option::Option::Some(constrained);
1715 super::verify_embedded_mandate_binding(&envelope)
1716 .expect("human is exactly what the grant covers");
1717 }
1718
1719 #[test]
1720 fn a_declared_class_outside_the_grant_is_refused_with_e020() {
1721 let mut envelope = single_proof_envelope();
1724 envelope.credential_subject.channel.recipient_class =
1725 std::option::Option::Some(crate::envelope::RecipientClass::Agent);
1726 let mut constrained = mandate();
1727 constrained.allowed_recipient_classes =
1728 std::option::Option::Some(std::vec![crate::envelope::RecipientClass::Human]);
1729 envelope.credential_subject.policy.delegation_mandate =
1730 std::option::Option::Some(constrained);
1731 let err = super::verify_embedded_mandate_binding(&envelope)
1732 .expect_err("agent traffic under a human-only grant must refuse");
1733 std::assert_eq!(err.code(), "APH_E020");
1734 std::assert!(std::format!("{}", err).contains("agent"));
1735 }
1736
1737 #[test]
1738 fn declaring_nothing_under_a_constrained_grant_is_refused_not_waved() {
1739 let mut envelope = single_proof_envelope();
1743 let mut constrained = mandate();
1744 constrained.allowed_recipient_classes =
1745 std::option::Option::Some(std::vec![crate::envelope::RecipientClass::Human]);
1746 envelope.credential_subject.policy.delegation_mandate =
1747 std::option::Option::Some(constrained);
1748 let err = super::verify_embedded_mandate_binding(&envelope)
1749 .expect_err("an unverifiable constraint refuses");
1750 std::assert_eq!(err.code(), "APH_E020");
1751 std::assert!(std::format!("{}", err).contains("nothing"));
1752 }
1753}