1pub const DATA_INTEGRITY_PROOF_TYPE: &str = "DataIntegrityProof";
51
52#[derive(
54 std::fmt::Debug,
55 std::clone::Clone,
56 std::marker::Copy,
57 std::cmp::PartialEq,
58 std::cmp::Eq,
59)]
60pub enum ProofRole {
61 Principal,
63 Notary,
65}
66
67pub fn signing_base(
79 envelope: &crate::envelope::NotarizationEnvelope,
80 role: ProofRole,
81) -> std::result::Result<std::string::String, crate::errors::AphError> {
82 let proofs = base_proofs(&envelope.proof, role)?;
85 let mut working = envelope.clone();
86 working.proof = proofs;
87 canonicalize(&working)
88}
89
90pub fn mandate_signing_base(
107 mandate: &crate::delegation_mandate::DelegationMandate,
108 role: ProofRole,
109) -> std::result::Result<std::string::String, crate::errors::AphError> {
110 let mut value = match serde_json::to_value(mandate) {
111 std::result::Result::Ok(v) => v,
112 std::result::Result::Err(_) => {
114 return std::result::Result::Err(crate::errors::AphError::InvalidEnvelopeSignature);
115 }
116 };
117 match value.as_object_mut() {
118 std::option::Option::Some(object) => {
119 object.remove("notarySignature");
120 if role == ProofRole::Principal {
121 object.remove("principalSignature");
122 }
123 }
124 std::option::Option::None => {
127 return std::result::Result::Err(crate::errors::AphError::InvalidEnvelopeSignature);
128 }
129 }
130 std::result::Result::Ok(super::jcs::canonicalize_rfc8785(&value))
131}
132
133pub(crate) fn proof_mut(
139 envelope: &mut crate::envelope::NotarizationEnvelope,
140 role: ProofRole,
141) -> std::result::Result<&mut crate::envelope::EnvelopeProof, crate::errors::AphError> {
142 let count = envelope.proof.all().len();
145 let is_chain = envelope.proof.is_chain();
146 match &mut envelope.proof {
147 crate::envelope::EnvelopeProofs::Single(proof) => match role {
148 ProofRole::Notary => std::result::Result::Ok(proof),
149 ProofRole::Principal => {
152 std::result::Result::Err(wrong_shape(role, is_chain, count))
153 }
154 },
155 crate::envelope::EnvelopeProofs::Chain(proofs) => {
156 if proofs.len() != CHAIN_LENGTH {
157 return std::result::Result::Err(wrong_shape(role, is_chain, count));
158 }
159 let index = match role {
160 ProofRole::Principal => 0,
161 ProofRole::Notary => 1,
162 };
163 match proofs.get_mut(index) {
164 std::option::Option::Some(proof) => std::result::Result::Ok(proof),
165 std::option::Option::None => {
168 std::result::Result::Err(wrong_shape(role, is_chain, count))
169 }
170 }
171 }
172 }
173}
174
175pub(crate) fn require_signed_principal(
186 envelope: &crate::envelope::NotarizationEnvelope,
187) -> std::result::Result<(), crate::errors::AphError> {
188 let principal_is_signed = match envelope.proof.principal() {
189 std::option::Option::Some(proof) => !proof.proof_value.is_empty(),
190 std::option::Option::None => {
191 return std::result::Result::Err(crate::errors::AphError::proof_chain_invalid(
192 "a notary countersignature requires a two-element proof chain (§7.1.11)",
193 ));
194 }
195 };
196 if !principal_is_signed {
197 return std::result::Result::Err(crate::errors::AphError::proof_chain_invalid(
198 "the principal proof carries no proofValue; a countersignature over an unsigned principal proof attests nothing (§7.1.11)",
199 ));
200 }
201 std::result::Result::Ok(())
202}
203
204pub(crate) fn signature_failure(role: ProofRole) -> crate::errors::AphError {
212 match role {
213 ProofRole::Principal => crate::errors::AphError::PrincipalSignatureInvalid,
214 ProofRole::Notary => crate::errors::AphError::InvalidEnvelopeSignature,
215 }
216}
217
218pub(crate) fn proof_of(
224 envelope: &crate::envelope::NotarizationEnvelope,
225 role: ProofRole,
226) -> std::result::Result<&crate::envelope::EnvelopeProof, crate::errors::AphError> {
227 let proof = match role {
228 ProofRole::Principal => envelope.proof.principal(),
229 ProofRole::Notary => envelope.proof.notary(),
230 };
231 match proof {
232 std::option::Option::Some(proof) => std::result::Result::Ok(proof),
233 std::option::Option::None => {
235 std::result::Result::Err(crate::errors::AphError::proof_chain_invalid(
236 "the envelope carries no proof in the requested chain position (§7.1.11)",
237 ))
238 }
239 }
240}
241
242const CHAIN_LENGTH: usize = 2;
244
245fn base_proofs(
247 proofs: &crate::envelope::EnvelopeProofs,
248 role: ProofRole,
249) -> std::result::Result<crate::envelope::EnvelopeProofs, crate::errors::AphError> {
250 let count = proofs.all().len();
251 let is_chain = proofs.is_chain();
252 match role {
253 ProofRole::Principal => {
254 let principal = match proofs.principal() {
255 std::option::Option::Some(proof) => proof,
256 std::option::Option::None => {
257 return std::result::Result::Err(wrong_shape(role, is_chain, count));
258 }
259 };
260 let mut head = principal.clone();
261 head.proof_value = std::string::String::new();
262 std::result::Result::Ok(crate::envelope::EnvelopeProofs::Chain(std::vec![head]))
266 }
267 ProofRole::Notary => match proofs {
268 crate::envelope::EnvelopeProofs::Single(proof) => {
269 let mut lone = proof.clone();
270 lone.proof_value = std::string::String::new();
271 std::result::Result::Ok(crate::envelope::EnvelopeProofs::Single(lone))
272 }
273 crate::envelope::EnvelopeProofs::Chain(_) => {
274 let principal = match proofs.principal() {
275 std::option::Option::Some(proof) => proof,
276 std::option::Option::None => {
277 return std::result::Result::Err(wrong_shape(role, is_chain, count));
278 }
279 };
280 let notary = match proofs.notary() {
281 std::option::Option::Some(proof) => proof,
282 std::option::Option::None => {
283 return std::result::Result::Err(wrong_shape(role, is_chain, count));
284 }
285 };
286 let mut tail = notary.clone();
287 tail.proof_value = std::string::String::new();
288 std::result::Result::Ok(crate::envelope::EnvelopeProofs::Chain(std::vec![
292 principal.clone(),
293 tail,
294 ]))
295 }
296 },
297 }
298}
299
300fn canonicalize(
302 envelope: &crate::envelope::NotarizationEnvelope,
303) -> std::result::Result<std::string::String, crate::errors::AphError> {
304 match serde_json::to_value(envelope) {
305 std::result::Result::Ok(value) => {
306 std::result::Result::Ok(super::jcs::canonicalize_rfc8785(&value))
307 }
308 std::result::Result::Err(_) => {
310 std::result::Result::Err(crate::errors::AphError::InvalidEnvelopeSignature)
311 }
312 }
313}
314
315fn wrong_shape(role: ProofRole, is_chain: bool, count: usize) -> crate::errors::AphError {
318 let form = if is_chain { "array" } else { "object" };
319 let expected = match role {
320 ProofRole::Principal => "a two-element proof chain",
321 ProofRole::Notary => "a single proof or a two-element proof chain",
322 };
323 crate::errors::AphError::proof_chain_invalid(std::format!(
324 "the {} canonicalization base (§7.2.1) requires {}; this envelope carries {} proof(s) in {} form",
325 role_label(role),
326 expected,
327 count,
328 form
329 ))
330}
331
332fn role_label(role: ProofRole) -> &'static str {
334 match role {
335 ProofRole::Principal => "principal",
336 ProofRole::Notary => "notary",
337 }
338}
339
340#[cfg(test)]
350pub(crate) mod test_support {
351 pub(crate) const PRINCIPAL_P256_SCALAR: [u8; 32] = [
366 0xc9, 0xaf, 0xa9, 0xd8, 0x45, 0xba, 0x75, 0x16, 0x6b, 0x5c, 0x21, 0x57, 0x67, 0xb1, 0xd6, 0x93,
367 0x4e, 0x50, 0xc3, 0xdb, 0x36, 0xe8, 0x9b, 0x12, 0x7b, 0x8a, 0x62, 0x2b, 0x12, 0x0f, 0x67, 0x21,
368 ];
369
370 pub(crate) const NOTARY_P256_SCALAR: [u8; 32] = [
377 0x8e, 0x9b, 0x10, 0x9e, 0x71, 0x90, 0x98, 0xbf, 0x98, 0x04, 0x87, 0xdf, 0x1f, 0x5d, 0x77, 0xe9,
378 0xcb, 0x29, 0x60, 0x6e, 0xbe, 0xd2, 0x26, 0x3b, 0x5f, 0x57, 0xc2, 0x13, 0xdf, 0x84, 0xf4, 0xb2,
379 ];
380
381 pub(crate) fn p256_key(scalar: &[u8; 32]) -> p256::ecdsa::SigningKey {
383 p256::ecdsa::SigningKey::from_slice(scalar).expect("a published P-256 test scalar is in 1..n")
384 }
385
386 pub(crate) fn single_proof_envelope() -> crate::envelope::NotarizationEnvelope {
389 let raw = include_str!("../../tests/golden/slack_reply_envelope.json");
390 serde_json::from_str(raw).expect("golden fixture parses")
391 }
392
393 pub(crate) const PRINCIPAL_PROOF_ID: &str = "urn:uuid:00000000-0000-4000-8000-0000000000a1";
395 pub(crate) const NOTARY_PROOF_ID: &str = "urn:uuid:00000000-0000-4000-8000-0000000000a2";
397
398 pub(crate) fn chain_envelope() -> crate::envelope::NotarizationEnvelope {
407 let mut envelope = single_proof_envelope();
408 let template = envelope
409 .proof
410 .notary()
411 .expect("the golden fixture carries a lone notary proof")
412 .clone();
413
414 let mut principal = template.clone();
415 principal.id = std::option::Option::Some(std::string::String::from(PRINCIPAL_PROOF_ID));
416 principal.previous_proof = std::option::Option::None;
417 principal.proof_purpose = std::string::String::from("assertionMethod");
418 principal.verification_method = std::format!(
419 "{did}#{fragment}",
420 did = envelope.credential_subject.human_principal.id,
421 fragment = envelope
422 .credential_subject
423 .human_principal
424 .id
425 .trim_start_matches("did:key:")
426 );
427 principal.proof_value = std::string::String::new();
428
429 let mut notary = template;
430 notary.id = std::option::Option::Some(std::string::String::from(NOTARY_PROOF_ID));
431 notary.previous_proof =
432 std::option::Option::Some(std::string::String::from(PRINCIPAL_PROOF_ID));
433 notary.proof_purpose = std::string::String::from("authentication");
434 notary.proof_value = std::string::String::new();
435
436 envelope.credential_subject.policy.attestation_mode =
437 std::option::Option::Some(crate::envelope::AttestationMode::PrincipalSigned);
438 envelope.proof = crate::envelope::EnvelopeProofs::Chain(std::vec![principal, notary]);
439 envelope
440 }
441}
442
443#[cfg(test)]
444mod tests {
445 fn single() -> crate::envelope::NotarizationEnvelope {
446 super::test_support::single_proof_envelope()
447 }
448
449 fn chain() -> crate::envelope::NotarizationEnvelope {
450 super::test_support::chain_envelope()
451 }
452
453 fn sample_mandate() -> crate::delegation_mandate::DelegationMandate {
454 crate::delegation_mandate::DelegationMandate {
455 id: std::string::String::from("urn:uuid:00000000-0000-4000-8000-000000000001"),
456 human_principal_did: std::string::String::from("did:key:zHuman"),
457 agent_did: std::string::String::from("did:web:agent.squillo.com"),
458 allowed_channels: std::vec![crate::envelope::ChannelKind::Slack],
459 allowed_recipient_classes: std::option::Option::None,
460 rate_limit_per_hour: std::option::Option::Some(30),
461 valid_from: std::string::String::from("2026-05-21T00:00:00Z"),
462 valid_until: std::string::String::from("2026-06-21T00:00:00Z"),
463 principal_signature: std::string::String::from("zPrincipalSignatureBytes"),
464 notary_signature: std::string::String::from("zNotarySignatureBytes"),
465 }
466 }
467
468 #[test]
469 fn the_three_bases_are_pairwise_distinct() {
470 let lone = super::signing_base(&single(), super::ProofRole::Notary)
475 .expect("a lone notary base is always constructible");
476 let chained = chain();
477 let principal = super::signing_base(&chained, super::ProofRole::Principal)
478 .expect("a two-element chain has a principal base");
479 let notary = super::signing_base(&chained, super::ProofRole::Notary)
480 .expect("a two-element chain has a notary base");
481
482 std::assert_ne!(lone, principal);
483 std::assert_ne!(lone, notary);
484 std::assert_ne!(principal, notary);
485 }
486
487 #[test]
488 fn principal_base_is_a_one_element_array_not_an_object() {
489 let base = super::signing_base(&chain(), super::ProofRole::Principal)
495 .expect("a two-element chain has a principal base");
496 std::assert!(
497 base.contains(r#""proof":[{"#),
498 "principal base must carry a one-element ARRAY: {}",
499 base
500 );
501 }
502
503 #[test]
504 fn principal_base_discards_the_notary_proof_entirely() {
505 let base = super::signing_base(&chain(), super::ProofRole::Principal)
509 .expect("a two-element chain has a principal base");
510 std::assert!(
511 !base.contains(super::test_support::NOTARY_PROOF_ID),
512 "principal base must not mention the notary proof: {}",
513 base
514 );
515 }
516
517 #[test]
518 fn notary_base_keeps_both_proofs() {
519 let base = super::signing_base(&chain(), super::ProofRole::Notary)
523 .expect("a two-element chain has a notary base");
524 std::assert!(
525 base.contains(super::test_support::PRINCIPAL_PROOF_ID)
526 && base.contains(super::test_support::NOTARY_PROOF_ID),
527 "notary base must carry both proofs: {}",
528 base
529 );
530 }
531
532 #[test]
533 fn every_envelope_base_empties_rather_than_removes_proof_value() {
534 let chained = chain();
539 for (label, base) in [
540 (
541 "lone",
542 super::signing_base(&single(), super::ProofRole::Notary).expect("lone base"),
543 ),
544 (
545 "principal",
546 super::signing_base(&chained, super::ProofRole::Principal).expect("principal base"),
547 ),
548 (
549 "notary",
550 super::signing_base(&chained, super::ProofRole::Notary).expect("notary base"),
551 ),
552 ] {
553 std::assert!(
554 base.contains(r#""proofValue":"""#),
555 "{} base must keep proofValue present-but-empty: {}",
556 label,
557 base
558 );
559 }
560 }
561
562 #[test]
563 fn principal_base_of_a_single_proof_envelope_is_aph_e013() {
564 let error = super::signing_base(&single(), super::ProofRole::Principal)
568 .expect_err("a single proof has no principal base");
569 std::assert_eq!(error.code(), "APH_E013");
570 }
571
572 #[test]
573 fn principal_base_of_a_one_element_chain_is_aph_e013() {
574 let mut envelope = chain();
578 let principal = envelope
579 .proof
580 .principal()
581 .expect("the chain fixture has a principal proof")
582 .clone();
583 envelope.proof = crate::envelope::EnvelopeProofs::Chain(std::vec![principal]);
584 let error = super::signing_base(&envelope, super::ProofRole::Principal)
585 .expect_err("a one-element chain is not a chain");
586 std::assert_eq!(error.code(), "APH_E013");
587 }
588
589 #[test]
590 fn notary_base_of_a_three_element_chain_is_aph_e013() {
591 let mut envelope = chain();
595 let mut proofs = envelope.proof.all().to_vec();
596 let extra = proofs
597 .last()
598 .expect("the chain fixture is non-empty")
599 .clone();
600 proofs.push(extra);
601 envelope.proof = crate::envelope::EnvelopeProofs::Chain(proofs);
602 let error = super::signing_base(&envelope, super::ProofRole::Notary)
603 .expect_err("a three-element chain has no defined base");
604 std::assert_eq!(error.code(), "APH_E013");
605 }
606
607 #[test]
608 fn mandate_principal_base_removes_both_signature_fields() {
609 let base = super::mandate_signing_base(&sample_mandate(), super::ProofRole::Principal)
613 .expect("a mandate always has a principal base");
614 std::assert!(
615 !base.contains("principalSignature") && !base.contains("notarySignature"),
616 "principal mandate base must carry neither signature: {}",
617 base
618 );
619 }
620
621 #[test]
622 fn mandate_notary_base_keeps_the_principal_signature() {
623 let base = super::mandate_signing_base(&sample_mandate(), super::ProofRole::Notary)
627 .expect("a mandate always has a notary base");
628 std::assert!(
629 base.contains("zPrincipalSignatureBytes") && !base.contains("notarySignature"),
630 "notary mandate base must keep principalSignature and drop its own: {}",
631 base
632 );
633 }
634
635 #[test]
636 fn the_two_mandate_bases_are_distinct() {
637 let mandate = sample_mandate();
641 let principal = super::mandate_signing_base(&mandate, super::ProofRole::Principal)
642 .expect("principal mandate base");
643 let notary =
644 super::mandate_signing_base(&mandate, super::ProofRole::Notary).expect("notary mandate base");
645 std::assert_ne!(principal, notary);
646 }
647
648 #[test]
649 fn bases_are_deterministic() {
650 let chained = chain();
654 let once = super::signing_base(&chained, super::ProofRole::Notary).expect("notary base");
655 let twice = super::signing_base(&chained, super::ProofRole::Notary).expect("notary base");
656 std::assert_eq!(once, twice);
657 }
658}