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 act_classification: std::option::Option::None,
798 },
799 linked_mandate: std::option::Option::None,
800 credential_status: std::option::Option::None,
804 proof: crate::envelope::EnvelopeProofs::Single(notary_proof()),
805 }
806 }
807
808 fn chain_envelope() -> crate::envelope::NotarizationEnvelope {
811 let mut envelope = single_proof_envelope();
812 envelope.credential_subject.policy.attestation_mode =
813 std::option::Option::Some(crate::envelope::AttestationMode::PrincipalSigned);
814 envelope.proof =
815 crate::envelope::EnvelopeProofs::Chain(std::vec![principal_proof(), countersignature()]);
816 envelope
817 }
818
819 fn chain_envelope_with(
822 mutate: impl std::ops::FnOnce(
823 &mut crate::envelope::EnvelopeProof,
824 &mut crate::envelope::EnvelopeProof,
825 ),
826 ) -> crate::envelope::NotarizationEnvelope {
827 let mut principal = principal_proof();
828 let mut notary = countersignature();
829 mutate(&mut principal, &mut notary);
830 let mut envelope = chain_envelope();
831 envelope.proof = crate::envelope::EnvelopeProofs::Chain(std::vec![principal, notary]);
832 envelope
833 }
834
835 #[test]
838 fn single_proof_without_a_label_verifies_as_notary_attested() {
839 let mode = super::verify_proof_structure(&single_proof_envelope())
845 .expect("a lone notary proof is a well-formed NotaryAttested envelope");
846 std::assert_eq!(mode, crate::envelope::AttestationMode::NotaryAttested);
847 }
848
849 #[test]
850 fn a_lone_proof_carrying_chain_members_is_rejected() {
851 let mut envelope = single_proof_envelope();
857 if let crate::envelope::EnvelopeProofs::Single(proof) = &mut envelope.proof {
858 proof.previous_proof =
859 std::option::Option::Some(std::string::String::from("urn:uuid:00000000-0000-4000-8000-0000000000f1"));
860 }
861 let err = super::verify_proof_structure(&envelope)
862 .expect_err("a lone proof naming a previous proof is not well formed");
863 std::assert_eq!(err.code(), "APH_E013");
864 }
865
866 #[test]
867 fn a_lone_proof_using_the_countersignature_purpose_is_rejected() {
868 let mut envelope = single_proof_envelope();
874 if let crate::envelope::EnvelopeProofs::Single(proof) = &mut envelope.proof {
875 proof.proof_purpose = std::string::String::from("authentication");
876 }
877 let err = super::verify_proof_structure(&envelope)
878 .expect_err("a lone proof may not use the countersignature purpose");
879 std::assert_eq!(err.code(), "APH_E013");
880 }
881
882 #[test]
883 fn single_proof_labelled_notary_attested_verifies() {
884 let mut envelope = single_proof_envelope();
888 envelope.credential_subject.policy.attestation_mode =
889 std::option::Option::Some(crate::envelope::AttestationMode::NotaryAttested);
890 let mode = super::verify_proof_structure(&envelope)
891 .expect("an explicitly-labelled NotaryAttested envelope is well formed");
892 std::assert_eq!(mode, crate::envelope::AttestationMode::NotaryAttested);
893 }
894
895 #[test]
896 fn well_formed_chain_verifies_as_principal_signed() {
897 let mode = super::verify_proof_structure(&chain_envelope())
902 .expect("a correctly linked and labelled chain must verify");
903 std::assert_eq!(mode, crate::envelope::AttestationMode::PrincipalSigned);
904 }
905
906 #[test]
907 fn principal_verification_method_without_a_fragment_is_accepted() {
908 let envelope = chain_envelope_with(|principal, _notary| {
914 principal.verification_method = std::string::String::from(HUMAN_DID);
915 });
916 std::assert!(super::verify_proof_structure(&envelope).is_ok());
917 }
918
919 #[test]
922 fn a_one_element_chain_is_rejected() {
923 let mut envelope = chain_envelope();
929 envelope.proof = crate::envelope::EnvelopeProofs::Chain(std::vec![principal_proof()]);
930 let err = super::verify_proof_structure(&envelope)
931 .expect_err("a one-element chain must be rejected");
932 std::assert_eq!(err.code(), "APH_E013");
933 }
934
935 #[test]
936 fn a_three_element_chain_is_rejected() {
937 let mut envelope = chain_envelope();
942 envelope.proof = crate::envelope::EnvelopeProofs::Chain(std::vec![
943 principal_proof(),
944 countersignature(),
945 countersignature(),
946 ]);
947 let err = super::verify_proof_structure(&envelope)
948 .expect_err("a three-element chain must be rejected");
949 std::assert_eq!(err.code(), "APH_E013");
950 }
951
952 #[test]
953 fn an_empty_chain_is_rejected() {
954 let mut envelope = chain_envelope();
959 envelope.proof = crate::envelope::EnvelopeProofs::Chain(std::vec::Vec::new());
960 let err =
961 super::verify_proof_structure(&envelope).expect_err("an empty chain must be rejected");
962 std::assert_eq!(err.code(), "APH_E013");
963 }
964
965 #[test]
968 fn a_chain_with_no_attestation_mode_is_rejected() {
969 let mut envelope = chain_envelope();
975 envelope.credential_subject.policy.attestation_mode = std::option::Option::None;
976 let err = super::verify_proof_structure(&envelope)
977 .expect_err("an unlabelled chain must be rejected");
978 std::assert_eq!(err.code(), "APH_E013");
979 }
980
981 #[test]
982 fn a_chain_labelled_notary_attested_is_rejected() {
983 let mut envelope = chain_envelope();
989 envelope.credential_subject.policy.attestation_mode =
990 std::option::Option::Some(crate::envelope::AttestationMode::NotaryAttested);
991 let err = super::verify_proof_structure(&envelope)
992 .expect_err("a chain labelled NotaryAttested must be rejected");
993 std::assert_eq!(err.code(), "APH_E013");
994 }
995
996 #[test]
997 fn a_single_proof_labelled_principal_signed_is_rejected() {
998 let mut envelope = single_proof_envelope();
1004 envelope.credential_subject.policy.attestation_mode =
1005 std::option::Option::Some(crate::envelope::AttestationMode::PrincipalSigned);
1006 let err = super::verify_proof_structure(&envelope)
1007 .expect_err("a PrincipalSigned label over a single proof must be rejected");
1008 std::assert_eq!(err.code(), "APH_E013");
1009 }
1010
1011 #[test]
1014 fn a_principal_proof_with_the_wrong_purpose_is_rejected() {
1015 let envelope = chain_envelope_with(|principal, _notary| {
1020 principal.proof_purpose = std::string::String::from("authentication");
1021 });
1022 let err = super::verify_proof_structure(&envelope)
1023 .expect_err("principal proof must carry assertionMethod");
1024 std::assert_eq!(err.code(), "APH_E013");
1025 }
1026
1027 #[test]
1028 fn a_notary_proof_with_the_wrong_purpose_is_rejected() {
1029 let envelope = chain_envelope_with(|_principal, notary| {
1034 notary.proof_purpose = std::string::String::from("assertionMethod");
1035 });
1036 let err = super::verify_proof_structure(&envelope)
1037 .expect_err("notary proof must carry authentication");
1038 std::assert_eq!(err.code(), "APH_E013");
1039 }
1040
1041 #[test]
1044 fn a_principal_proof_without_an_id_is_rejected() {
1045 let envelope = chain_envelope_with(|principal, notary| {
1049 principal.id = std::option::Option::None;
1050 notary.previous_proof = std::option::Option::None;
1051 });
1052 let err = super::verify_proof_structure(&envelope)
1053 .expect_err("a chain proof without an id must be rejected");
1054 std::assert_eq!(err.code(), "APH_E013");
1055 }
1056
1057 #[test]
1058 fn a_notary_proof_without_an_id_is_rejected() {
1059 let envelope = chain_envelope_with(|_principal, notary| {
1064 notary.id = std::option::Option::None;
1065 });
1066 let err = super::verify_proof_structure(&envelope)
1067 .expect_err("the notary proof must carry an id");
1068 std::assert_eq!(err.code(), "APH_E013");
1069 }
1070
1071 #[test]
1072 fn two_proofs_sharing_one_id_are_rejected() {
1073 let envelope = chain_envelope_with(|_principal, notary| {
1077 notary.id = std::option::Option::Some(std::string::String::from(PRINCIPAL_PROOF_ID));
1078 notary.previous_proof = std::option::Option::Some(std::string::String::from(PRINCIPAL_PROOF_ID));
1079 });
1080 let err = super::verify_proof_structure(&envelope)
1081 .expect_err("duplicate proof ids must be rejected");
1082 std::assert_eq!(err.code(), "APH_E013");
1083 }
1084
1085 #[test]
1088 fn a_notary_proof_without_previous_proof_is_rejected() {
1089 let envelope = chain_envelope_with(|_principal, notary| {
1094 notary.previous_proof = std::option::Option::None;
1095 });
1096 let err = super::verify_proof_structure(&envelope)
1097 .expect_err("missing previousProof must be rejected");
1098 std::assert_eq!(err.code(), "APH_E013");
1099 std::assert!(
1100 std::string::ToString::to_string(&err).contains("previousProof"),
1101 "the reason must name the missing member: {}",
1102 err
1103 );
1104 }
1105
1106 #[test]
1107 fn a_dangling_previous_proof_is_rejected() {
1108 let envelope = chain_envelope_with(|_principal, notary| {
1113 notary.previous_proof =
1114 std::option::Option::Some(std::string::String::from("urn:uuid:not-in-this-chain"));
1115 });
1116 let err = super::verify_proof_structure(&envelope)
1117 .expect_err("a dangling previousProof must be rejected");
1118 std::assert_eq!(err.code(), "APH_E013");
1119 std::assert!(
1120 std::string::ToString::to_string(&err).contains("dangling"),
1121 "the reason must distinguish dangling from missing: {}",
1122 err
1123 );
1124 }
1125
1126 #[test]
1127 fn a_self_referential_previous_proof_is_rejected() {
1128 let envelope = chain_envelope_with(|_principal, notary| {
1133 notary.previous_proof = std::option::Option::Some(std::string::String::from(NOTARY_PROOF_ID));
1134 });
1135 let err = super::verify_proof_structure(&envelope)
1136 .expect_err("a self-referential previousProof must be rejected");
1137 std::assert_eq!(err.code(), "APH_E013");
1138 std::assert!(
1139 std::string::ToString::to_string(&err).contains("self-referential"),
1140 "the reason must distinguish a cycle from a dangling link: {}",
1141 err
1142 );
1143 }
1144
1145 #[test]
1146 fn a_principal_proof_carrying_previous_proof_is_rejected() {
1147 let envelope = chain_envelope_with(|principal, _notary| {
1152 principal.previous_proof = std::option::Option::Some(std::string::String::from(NOTARY_PROOF_ID));
1153 });
1154 let err = super::verify_proof_structure(&envelope)
1155 .expect_err("the chain head must not carry previousProof");
1156 std::assert_eq!(err.code(), "APH_E013");
1157 }
1158
1159 #[test]
1162 fn a_principal_proof_made_by_another_key_is_rejected() {
1163 let envelope = chain_envelope_with(|principal, _notary| {
1170 principal.verification_method = std::format!("{}#key-1", NOTARY_DID);
1171 });
1172 let err = super::verify_proof_structure(&envelope)
1173 .expect_err("a principal proof under a foreign DID must be rejected");
1174 std::assert_eq!(err.code(), "APH_E013");
1175 }
1176
1177 #[test]
1178 fn a_principal_verification_method_that_only_prefixes_the_human_did_is_rejected() {
1179 let envelope = chain_envelope_with(|principal, _notary| {
1184 principal.verification_method = std::format!("{}-attacker#key-1", HUMAN_DID);
1185 });
1186 let err = super::verify_proof_structure(&envelope)
1187 .expect_err("a DID that merely starts with the human's DID must be rejected");
1188 std::assert_eq!(err.code(), "APH_E013");
1189 }
1190
1191 #[test]
1194 fn require_principal_signed_refuses_a_notary_attested_envelope() {
1195 let err = super::require_mode(
1201 &single_proof_envelope(),
1202 crate::envelope::AttestationMode::PrincipalSigned,
1203 )
1204 .expect_err("a NotaryAttested envelope must not satisfy a PrincipalSigned policy");
1205 std::assert_eq!(err.code(), "APH_E012");
1206 }
1207
1208 #[test]
1209 fn require_notary_attested_accepts_a_principal_signed_envelope() {
1210 std::assert!(
1214 super::require_mode(
1215 &chain_envelope(),
1216 crate::envelope::AttestationMode::NotaryAttested,
1217 )
1218 .is_ok()
1219 );
1220 }
1221
1222 #[test]
1223 fn require_mode_accepts_an_exact_match() {
1224 std::assert!(
1227 super::require_mode(
1228 &single_proof_envelope(),
1229 crate::envelope::AttestationMode::NotaryAttested,
1230 )
1231 .is_ok()
1232 );
1233 }
1234
1235 #[test]
1236 fn require_mode_reads_absence_as_the_weaker_mode() {
1237 let mut envelope = single_proof_envelope();
1242 envelope.credential_subject.policy.attestation_mode = std::option::Option::None;
1243 let err = super::require_mode(&envelope, crate::envelope::AttestationMode::PrincipalSigned)
1244 .expect_err("an unlabelled envelope must not satisfy a PrincipalSigned policy");
1245 std::assert_eq!(err.code(), "APH_E012");
1246 }
1247
1248 #[test]
1251 fn an_absent_embedded_mandate_is_not_a_structural_error() {
1252 std::assert!(super::verify_embedded_mandate_binding(&single_proof_envelope()).is_ok());
1257 }
1258
1259 #[test]
1260 fn a_matching_embedded_mandate_binds() {
1261 let mut envelope = single_proof_envelope();
1265 envelope.credential_subject.policy.delegation_mandate_id =
1266 std::option::Option::Some(std::string::String::from(MANDATE_ID));
1267 envelope.credential_subject.policy.delegation_mandate = std::option::Option::Some(mandate());
1268 std::assert!(super::verify_embedded_mandate_binding(&envelope).is_ok());
1269 }
1270
1271 #[test]
1272 fn an_embedded_mandate_for_another_human_is_rejected() {
1273 let mut envelope = single_proof_envelope();
1279 let mut stapled = mandate();
1280 stapled.human_principal_did = std::string::String::from("did:key:z6MkSomeoneElse");
1281 envelope.credential_subject.policy.delegation_mandate = std::option::Option::Some(stapled);
1282 let err = super::verify_embedded_mandate_binding(&envelope)
1283 .expect_err("a mandate for another human must not bind");
1284 std::assert_eq!(err.code(), "APH_E011");
1285 }
1286
1287 #[test]
1288 fn an_embedded_mandate_for_another_agent_is_rejected() {
1289 let mut envelope = single_proof_envelope();
1294 let mut stapled = mandate();
1295 stapled.agent_did = std::string::String::from("did:web:other-agent.example");
1296 envelope.credential_subject.policy.delegation_mandate = std::option::Option::Some(stapled);
1297 let err = super::verify_embedded_mandate_binding(&envelope)
1298 .expect_err("a mandate for another agent must not bind");
1299 std::assert_eq!(err.code(), "APH_E011");
1300 }
1301
1302 #[test]
1303 fn an_embedded_mandate_disagreeing_with_the_declared_id_is_rejected() {
1304 let mut envelope = single_proof_envelope();
1309 envelope.credential_subject.policy.delegation_mandate_id =
1310 std::option::Option::Some(std::string::String::from("urn:uuid:some-other-mandate"));
1311 envelope.credential_subject.policy.delegation_mandate = std::option::Option::Some(mandate());
1312 let err = super::verify_embedded_mandate_binding(&envelope)
1313 .expect_err("an embedded mandate must be the one the envelope names");
1314 std::assert_eq!(err.code(), "APH_E011");
1315 }
1316
1317 #[test]
1318 fn an_embedded_mandate_not_covering_the_channel_is_rejected() {
1319 let mut envelope = single_proof_envelope();
1325 envelope.credential_subject.channel.kind = crate::envelope::ChannelKind::Discord;
1326 envelope.credential_subject.policy.delegation_mandate = std::option::Option::Some(mandate());
1327 let err = super::verify_embedded_mandate_binding(&envelope)
1328 .expect_err("a channel outside allowedChannels must not bind");
1329 std::assert_eq!(err.code(), "APH_E005");
1330 }
1331
1332 #[test]
1333 fn an_envelope_outside_the_mandate_window_is_rejected() {
1334 let mut envelope = single_proof_envelope();
1339 envelope.valid_from = std::string::String::from("2026-07-01T00:00:00Z");
1340 envelope.valid_until = std::string::String::from("2026-07-02T00:00:00Z");
1341 envelope.credential_subject.policy.delegation_mandate = std::option::Option::Some(mandate());
1342 let err = super::verify_embedded_mandate_binding(&envelope)
1343 .expect_err("an envelope outside the mandate window must not bind");
1344 std::assert_eq!(err.code(), "APH_E003");
1345 }
1346
1347 #[test]
1348 fn an_envelope_expiring_after_the_mandate_is_rejected() {
1349 let mut envelope = single_proof_envelope();
1354 envelope.valid_from = std::string::String::from("2026-05-31T00:00:00Z");
1355 envelope.valid_until = std::string::String::from("2026-06-30T00:00:00Z");
1356 envelope.credential_subject.policy.delegation_mandate = std::option::Option::Some(mandate());
1357 let err = super::verify_embedded_mandate_binding(&envelope)
1358 .expect_err("an envelope outliving the mandate must not bind");
1359 std::assert_eq!(err.code(), "APH_E003");
1360 }
1361
1362 #[test]
1363 fn an_unparseable_envelope_window_fails_closed() {
1364 let mut envelope = single_proof_envelope();
1369 envelope.valid_from = std::string::String::from("not-a-timestamp");
1370 envelope.credential_subject.policy.delegation_mandate = std::option::Option::Some(mandate());
1371 let err = super::verify_embedded_mandate_binding(&envelope)
1372 .expect_err("an unparseable window must not bind");
1373 std::assert_eq!(err.code(), "APH_E003");
1374 }
1375
1376 #[test]
1379 fn a_single_proof_envelope_has_nothing_to_order() {
1380 std::assert!(super::verify_timestamp_order(&single_proof_envelope()).is_ok());
1384 }
1385
1386 #[test]
1387 fn issuance_order_decision_then_principal_then_notary_is_accepted() {
1388 std::assert!(super::verify_timestamp_order(&chain_envelope()).is_ok());
1392 }
1393
1394 #[test]
1395 fn equal_timestamps_are_accepted() {
1396 let mut envelope = chain_envelope_with(|principal, notary| {
1401 principal.created = std::string::String::from("2026-05-21T00:00:01Z");
1402 notary.created = std::string::String::from("2026-05-21T00:00:01Z");
1403 });
1404 envelope.credential_subject.notarization.decision_timestamp =
1405 std::string::String::from("2026-05-21T00:00:01Z");
1406 std::assert!(super::verify_timestamp_order(&envelope).is_ok());
1407 }
1408
1409 #[test]
1410 fn a_principal_proof_predating_the_decision_is_rejected() {
1411 let envelope = chain_envelope_with(|principal, _notary| {
1417 principal.created = std::string::String::from("2026-05-21T00:00:00Z");
1418 });
1419 let err = super::verify_timestamp_order(&envelope)
1420 .expect_err("a principal proof predating the decision must be rejected");
1421 std::assert_eq!(err.code(), "APH_E013");
1422 }
1423
1424 #[test]
1425 fn a_countersignature_predating_the_principal_proof_is_rejected() {
1426 let envelope = chain_envelope_with(|_principal, notary| {
1431 notary.created = std::string::String::from("2026-05-21T00:00:01Z");
1432 });
1433 let err = super::verify_timestamp_order(&envelope)
1434 .expect_err("a countersignature predating its principal proof must be rejected");
1435 std::assert_eq!(err.code(), "APH_E013");
1436 }
1437
1438 #[test]
1439 fn an_unparseable_proof_timestamp_is_rejected() {
1440 let envelope = chain_envelope_with(|principal, _notary| {
1444 principal.created = std::string::String::from("yesterday");
1445 });
1446 let err = super::verify_timestamp_order(&envelope)
1447 .expect_err("an unparseable timestamp must be rejected");
1448 std::assert_eq!(err.code(), "APH_E013");
1449 }
1450
1451 #[test]
1452 fn timestamps_are_compared_as_instants_not_strings() {
1453 let envelope = chain_envelope_with(|_principal, notary| {
1458 notary.created = std::string::String::from("2026-05-20T20:00:03-04:00");
1459 });
1460 std::assert!(
1461 super::verify_timestamp_order(&envelope).is_ok(),
1462 "an equivalent instant written with an offset must order correctly"
1463 );
1464 }
1465
1466 #[test]
1467 fn a_malformed_chain_cannot_have_its_order_checked() {
1468 let mut envelope = chain_envelope();
1472 envelope.proof = crate::envelope::EnvelopeProofs::Chain(std::vec![principal_proof()]);
1473 let err = super::verify_timestamp_order(&envelope)
1474 .expect_err("a one-element chain has no checkable order");
1475 std::assert_eq!(err.code(), "APH_E013");
1476 }
1477 fn audience(id: &str) -> crate::envelope::Audience {
1480 crate::envelope::Audience {
1481 id: id.to_string(),
1482 channel_binding: std::option::Option::None,
1483 }
1484 }
1485
1486 fn binding(
1487 kind: crate::envelope::ChannelKind,
1488 coordinates: &[(&str, &str)],
1489 ) -> crate::envelope::AudienceChannelBinding {
1490 crate::envelope::AudienceChannelBinding {
1491 kind,
1492 coordinates: coordinates
1493 .iter()
1494 .map(|(k, v)| (k.to_string(), serde_json::json!(v)))
1495 .collect(),
1496 }
1497 }
1498
1499 #[test]
1500 fn an_envelope_with_no_audience_is_a_bearer_credential_by_choice() {
1501 let envelope = single_proof_envelope();
1505 super::verify_audience(&envelope, "did:web:anyone.example.com", std::option::Option::None)
1506 .expect("absence is not a constraint");
1507 }
1508
1509 #[test]
1510 fn an_audience_naming_another_verifier_is_refused() {
1511 let mut envelope = single_proof_envelope();
1515 envelope.credential_subject.audience =
1516 std::option::Option::Some(audience("did:web:intended.example.com"));
1517 let err =
1518 super::verify_audience(&envelope, "did:web:other.example.com", std::option::Option::None)
1519 .expect_err("a mismatched audience must refuse");
1520 std::assert_eq!(err.code(), "APH_E017");
1521 let text = std::format!("{}", err);
1522 std::assert!(text.contains("did:web:intended.example.com"));
1523 std::assert!(text.contains("did:web:other.example.com"));
1524 }
1525
1526 #[test]
1527 fn the_named_verifier_is_admitted() {
1528 let mut envelope = single_proof_envelope();
1531 envelope.credential_subject.audience =
1532 std::option::Option::Some(audience("did:web:intended.example.com"));
1533 super::verify_audience(&envelope, "did:web:intended.example.com", std::option::Option::None)
1534 .expect("the named audience is exactly who may accept");
1535 }
1536
1537 #[test]
1538 fn a_channel_binding_with_no_act_coordinates_is_refused_not_skipped() {
1539 let mut envelope = single_proof_envelope();
1544 let mut aud = audience("did:web:intended.example.com");
1545 aud.channel_binding = std::option::Option::Some(binding(
1546 crate::envelope::ChannelKind::Slack,
1547 &[("teamId", "T01234567")],
1548 ));
1549 envelope.credential_subject.audience = std::option::Option::Some(aud);
1550 let err = super::verify_audience(
1551 &envelope,
1552 "did:web:intended.example.com",
1553 std::option::Option::None,
1554 )
1555 .expect_err("unknown act coordinates must refuse, not skip");
1556 std::assert_eq!(err.code(), "APH_E017");
1557 }
1558
1559 #[test]
1560 fn a_channel_binding_member_mismatch_is_refused_naming_the_member() {
1561 let mut envelope = single_proof_envelope();
1565 let mut aud = audience("did:web:intended.example.com");
1566 aud.channel_binding = std::option::Option::Some(binding(
1567 crate::envelope::ChannelKind::Slack,
1568 &[("teamId", "T01234567"), ("channelId", "C01234567")],
1569 ));
1570 envelope.credential_subject.audience = std::option::Option::Some(aud);
1571 let act = binding(
1572 crate::envelope::ChannelKind::Slack,
1573 &[("teamId", "T01234567"), ("channelId", "C_ELSEWHERE")],
1574 );
1575 let err = super::verify_audience(
1576 &envelope,
1577 "did:web:intended.example.com",
1578 std::option::Option::Some(&act),
1579 )
1580 .expect_err("a differing coordinate must refuse");
1581 std::assert_eq!(err.code(), "APH_E017");
1582 std::assert!(std::format!("{}", err).contains("channelId"));
1583 }
1584
1585 #[test]
1586 fn a_matching_channel_binding_is_admitted_and_extra_act_members_are_fine() {
1587 let mut envelope = single_proof_envelope();
1592 let mut aud = audience("did:web:intended.example.com");
1593 aud.channel_binding = std::option::Option::Some(binding(
1594 crate::envelope::ChannelKind::Slack,
1595 &[("teamId", "T01234567")],
1596 ));
1597 envelope.credential_subject.audience = std::option::Option::Some(aud);
1598 let act = binding(
1599 crate::envelope::ChannelKind::Slack,
1600 &[("teamId", "T01234567"), ("threadTs", "1727000000.000100")],
1601 );
1602 super::verify_audience(
1603 &envelope,
1604 "did:web:intended.example.com",
1605 std::option::Option::Some(&act),
1606 )
1607 .expect("every bound member matches");
1608 }
1609
1610 #[test]
1611 fn a_kind_mismatch_in_the_binding_is_refused() {
1612 let mut envelope = single_proof_envelope();
1616 let mut aud = audience("did:web:intended.example.com");
1617 aud.channel_binding =
1618 std::option::Option::Some(binding(crate::envelope::ChannelKind::Slack, &[]));
1619 envelope.credential_subject.audience = std::option::Option::Some(aud);
1620 let act = binding(crate::envelope::ChannelKind::Email, &[]);
1621 let err = super::verify_audience(
1622 &envelope,
1623 "did:web:intended.example.com",
1624 std::option::Option::Some(&act),
1625 )
1626 .expect_err("kind is a bound member like any other");
1627 std::assert_eq!(err.code(), "APH_E017");
1628 }
1629
1630 #[test]
1631 fn the_envelope_window_admits_inside_and_refuses_both_sides_with_e019() {
1632 let envelope = single_proof_envelope();
1638 super::verify_envelope_window(&envelope, "2026-05-21T12:00:00Z").expect("inside the window");
1639 let early = super::verify_envelope_window(&envelope, "2026-05-20T23:59:59Z")
1640 .expect_err("not yet valid");
1641 std::assert_eq!(early.code(), "APH_E019");
1642 let late = super::verify_envelope_window(&envelope, "2026-05-22T00:00:01Z")
1643 .expect_err("expired");
1644 std::assert_eq!(late.code(), "APH_E019");
1645 std::assert!(std::format!("{}", late).contains("2026-05-22T00:00:00Z"));
1646 }
1647
1648 #[test]
1649 fn an_unparseable_window_fails_closed_to_e019() {
1650 let mut envelope = single_proof_envelope();
1655 envelope.valid_until = std::string::String::from("not-a-timestamp");
1656 let err = super::verify_envelope_window(&envelope, "2026-05-21T12:00:00Z")
1657 .expect_err("garbage must refuse");
1658 std::assert_eq!(err.code(), "APH_E019");
1659 }
1660
1661 #[test]
1662 fn an_envelope_is_spent_by_acceptance_and_a_second_presentation_refuses() {
1663 let mut ledger = super::InMemoryConsumedEnvelopeLedger::default();
1668 let envelope = single_proof_envelope();
1669 super::enforce_single_use(&mut ledger, &envelope).expect("first acceptance spends");
1670 let err = super::enforce_single_use(&mut ledger, &envelope)
1671 .expect_err("second presentation is a replay");
1672 std::assert_eq!(err.code(), "APH_E018");
1673 std::assert!(std::format!("{}", err).contains(&envelope.id));
1674 let again =
1675 super::enforce_single_use(&mut ledger, &envelope).expect_err("and it stays spent");
1676 std::assert_eq!(again.code(), "APH_E018");
1677 }
1678
1679 #[test]
1680 fn distinct_envelope_ids_spend_independently() {
1681 let mut ledger = super::InMemoryConsumedEnvelopeLedger::default();
1685 let first = single_proof_envelope();
1686 let mut second = single_proof_envelope();
1687 second.id = std::string::String::from("urn:uuid:00000000-0000-4000-8000-000000000002");
1688 super::enforce_single_use(&mut ledger, &first).expect("first id is fresh");
1689 super::enforce_single_use(&mut ledger, &second).expect("second id is fresh too");
1690 }
1691 #[test]
1694 fn an_unconstrained_mandate_ignores_recipient_class_entirely() {
1695 let mut envelope = single_proof_envelope();
1699 envelope.credential_subject.policy.delegation_mandate = std::option::Option::Some(mandate());
1700 super::verify_embedded_mandate_binding(&envelope)
1701 .expect("no constraint, nothing to check");
1702 }
1703
1704 #[test]
1705 fn a_declared_class_inside_the_grant_is_admitted() {
1706 let mut envelope = single_proof_envelope();
1707 envelope.credential_subject.channel.recipient_class =
1708 std::option::Option::Some(crate::envelope::RecipientClass::Human);
1709 let mut constrained = mandate();
1710 constrained.allowed_recipient_classes =
1711 std::option::Option::Some(std::vec![crate::envelope::RecipientClass::Human]);
1712 envelope.credential_subject.policy.delegation_mandate =
1713 std::option::Option::Some(constrained);
1714 super::verify_embedded_mandate_binding(&envelope)
1715 .expect("human is exactly what the grant covers");
1716 }
1717
1718 #[test]
1719 fn a_declared_class_outside_the_grant_is_refused_with_e020() {
1720 let mut envelope = single_proof_envelope();
1723 envelope.credential_subject.channel.recipient_class =
1724 std::option::Option::Some(crate::envelope::RecipientClass::Agent);
1725 let mut constrained = mandate();
1726 constrained.allowed_recipient_classes =
1727 std::option::Option::Some(std::vec![crate::envelope::RecipientClass::Human]);
1728 envelope.credential_subject.policy.delegation_mandate =
1729 std::option::Option::Some(constrained);
1730 let err = super::verify_embedded_mandate_binding(&envelope)
1731 .expect_err("agent traffic under a human-only grant must refuse");
1732 std::assert_eq!(err.code(), "APH_E020");
1733 std::assert!(std::format!("{}", err).contains("agent"));
1734 }
1735
1736 #[test]
1737 fn declaring_nothing_under_a_constrained_grant_is_refused_not_waved() {
1738 let mut envelope = single_proof_envelope();
1742 let mut constrained = mandate();
1743 constrained.allowed_recipient_classes =
1744 std::option::Option::Some(std::vec![crate::envelope::RecipientClass::Human]);
1745 envelope.credential_subject.policy.delegation_mandate =
1746 std::option::Option::Some(constrained);
1747 let err = super::verify_embedded_mandate_binding(&envelope)
1748 .expect_err("an unverifiable constraint refuses");
1749 std::assert_eq!(err.code(), "APH_E020");
1750 std::assert!(std::format!("{}", err).contains("nothing"));
1751 }
1752}