1#[derive(
45 std::fmt::Debug,
46 std::clone::Clone,
47 std::marker::Copy,
48 std::cmp::PartialEq,
49 std::cmp::Eq,
50)]
51pub enum ProofRole {
52 Principal,
54 Notary,
56}
57
58pub fn signing_base(
70 envelope: &crate::envelope::NotarizationEnvelope,
71 role: ProofRole,
72) -> std::result::Result<std::string::String, crate::errors::AphError> {
73 let proofs = base_proofs(&envelope.proof, role)?;
76 let mut working = envelope.clone();
77 working.proof = proofs;
78 canonicalize(&working)
79}
80
81pub fn mandate_signing_base(
98 mandate: &crate::delegation_mandate::DelegationMandate,
99 role: ProofRole,
100) -> std::result::Result<std::string::String, crate::errors::AphError> {
101 let mut value = match serde_json::to_value(mandate) {
102 std::result::Result::Ok(v) => v,
103 std::result::Result::Err(_) => {
105 return std::result::Result::Err(crate::errors::AphError::InvalidEnvelopeSignature);
106 }
107 };
108 match value.as_object_mut() {
109 std::option::Option::Some(object) => {
110 object.remove("notarySignature");
111 if role == ProofRole::Principal {
112 object.remove("principalSignature");
113 }
114 }
115 std::option::Option::None => {
118 return std::result::Result::Err(crate::errors::AphError::InvalidEnvelopeSignature);
119 }
120 }
121 std::result::Result::Ok(super::jcs::canonicalize_rfc8785(&value))
122}
123
124pub(crate) fn proof_mut(
130 envelope: &mut crate::envelope::NotarizationEnvelope,
131 role: ProofRole,
132) -> std::result::Result<&mut crate::envelope::EnvelopeProof, crate::errors::AphError> {
133 let count = envelope.proof.all().len();
136 let is_chain = envelope.proof.is_chain();
137 match &mut envelope.proof {
138 crate::envelope::EnvelopeProofs::Single(proof) => match role {
139 ProofRole::Notary => std::result::Result::Ok(proof),
140 ProofRole::Principal => {
143 std::result::Result::Err(wrong_shape(role, is_chain, count))
144 }
145 },
146 crate::envelope::EnvelopeProofs::Chain(proofs) => {
147 if proofs.len() != CHAIN_LENGTH {
148 return std::result::Result::Err(wrong_shape(role, is_chain, count));
149 }
150 let index = match role {
151 ProofRole::Principal => 0,
152 ProofRole::Notary => 1,
153 };
154 match proofs.get_mut(index) {
155 std::option::Option::Some(proof) => std::result::Result::Ok(proof),
156 std::option::Option::None => {
159 std::result::Result::Err(wrong_shape(role, is_chain, count))
160 }
161 }
162 }
163 }
164}
165
166const CHAIN_LENGTH: usize = 2;
168
169fn base_proofs(
171 proofs: &crate::envelope::EnvelopeProofs,
172 role: ProofRole,
173) -> std::result::Result<crate::envelope::EnvelopeProofs, crate::errors::AphError> {
174 let count = proofs.all().len();
175 let is_chain = proofs.is_chain();
176 match role {
177 ProofRole::Principal => {
178 let principal = match proofs.principal() {
179 std::option::Option::Some(proof) => proof,
180 std::option::Option::None => {
181 return std::result::Result::Err(wrong_shape(role, is_chain, count));
182 }
183 };
184 let mut head = principal.clone();
185 head.proof_value = std::string::String::new();
186 std::result::Result::Ok(crate::envelope::EnvelopeProofs::Chain(std::vec![head]))
190 }
191 ProofRole::Notary => match proofs {
192 crate::envelope::EnvelopeProofs::Single(proof) => {
193 let mut lone = proof.clone();
194 lone.proof_value = std::string::String::new();
195 std::result::Result::Ok(crate::envelope::EnvelopeProofs::Single(lone))
196 }
197 crate::envelope::EnvelopeProofs::Chain(_) => {
198 let principal = match proofs.principal() {
199 std::option::Option::Some(proof) => proof,
200 std::option::Option::None => {
201 return std::result::Result::Err(wrong_shape(role, is_chain, count));
202 }
203 };
204 let notary = match proofs.notary() {
205 std::option::Option::Some(proof) => proof,
206 std::option::Option::None => {
207 return std::result::Result::Err(wrong_shape(role, is_chain, count));
208 }
209 };
210 let mut tail = notary.clone();
211 tail.proof_value = std::string::String::new();
212 std::result::Result::Ok(crate::envelope::EnvelopeProofs::Chain(std::vec![
216 principal.clone(),
217 tail,
218 ]))
219 }
220 },
221 }
222}
223
224fn canonicalize(
226 envelope: &crate::envelope::NotarizationEnvelope,
227) -> std::result::Result<std::string::String, crate::errors::AphError> {
228 match serde_json::to_value(envelope) {
229 std::result::Result::Ok(value) => {
230 std::result::Result::Ok(super::jcs::canonicalize_rfc8785(&value))
231 }
232 std::result::Result::Err(_) => {
234 std::result::Result::Err(crate::errors::AphError::InvalidEnvelopeSignature)
235 }
236 }
237}
238
239fn wrong_shape(role: ProofRole, is_chain: bool, count: usize) -> crate::errors::AphError {
242 let form = if is_chain { "array" } else { "object" };
243 let expected = match role {
244 ProofRole::Principal => "a two-element proof chain",
245 ProofRole::Notary => "a single proof or a two-element proof chain",
246 };
247 crate::errors::AphError::proof_chain_invalid(std::format!(
248 "the {} canonicalization base (§7.2.1) requires {}; this envelope carries {} proof(s) in {} form",
249 role_label(role),
250 expected,
251 count,
252 form
253 ))
254}
255
256fn role_label(role: ProofRole) -> &'static str {
258 match role {
259 ProofRole::Principal => "principal",
260 ProofRole::Notary => "notary",
261 }
262}
263
264#[cfg(test)]
270pub(crate) mod test_support {
271 pub(crate) fn single_proof_envelope() -> crate::envelope::NotarizationEnvelope {
274 let raw = include_str!("../../tests/golden/slack_reply_envelope.json");
275 serde_json::from_str(raw).expect("golden fixture parses")
276 }
277
278 pub(crate) const PRINCIPAL_PROOF_ID: &str = "urn:uuid:00000000-0000-4000-8000-0000000000a1";
280 pub(crate) const NOTARY_PROOF_ID: &str = "urn:uuid:00000000-0000-4000-8000-0000000000a2";
282
283 pub(crate) fn chain_envelope() -> crate::envelope::NotarizationEnvelope {
292 let mut envelope = single_proof_envelope();
293 let template = envelope
294 .proof
295 .notary()
296 .expect("the golden fixture carries a lone notary proof")
297 .clone();
298
299 let mut principal = template.clone();
300 principal.id = std::option::Option::Some(std::string::String::from(PRINCIPAL_PROOF_ID));
301 principal.previous_proof = std::option::Option::None;
302 principal.proof_purpose = std::string::String::from("assertionMethod");
303 principal.verification_method = std::format!(
304 "{did}#{fragment}",
305 did = envelope.credential_subject.human_principal.id,
306 fragment = envelope
307 .credential_subject
308 .human_principal
309 .id
310 .trim_start_matches("did:key:")
311 );
312 principal.proof_value = std::string::String::new();
313
314 let mut notary = template;
315 notary.id = std::option::Option::Some(std::string::String::from(NOTARY_PROOF_ID));
316 notary.previous_proof =
317 std::option::Option::Some(std::string::String::from(PRINCIPAL_PROOF_ID));
318 notary.proof_purpose = std::string::String::from("authentication");
319 notary.proof_value = std::string::String::new();
320
321 envelope.credential_subject.policy.attestation_mode =
322 std::option::Option::Some(crate::envelope::AttestationMode::PrincipalSigned);
323 envelope.proof = crate::envelope::EnvelopeProofs::Chain(std::vec![principal, notary]);
324 envelope
325 }
326}
327
328#[cfg(test)]
329mod tests {
330 fn single() -> crate::envelope::NotarizationEnvelope {
331 super::test_support::single_proof_envelope()
332 }
333
334 fn chain() -> crate::envelope::NotarizationEnvelope {
335 super::test_support::chain_envelope()
336 }
337
338 fn sample_mandate() -> crate::delegation_mandate::DelegationMandate {
339 crate::delegation_mandate::DelegationMandate {
340 id: std::string::String::from("urn:uuid:00000000-0000-4000-8000-000000000001"),
341 human_principal_did: std::string::String::from("did:key:zHuman"),
342 agent_did: std::string::String::from("did:web:agent.squillo.com"),
343 allowed_channels: std::vec![std::string::String::from("slack")],
344 rate_limit_per_hour: std::option::Option::Some(30),
345 valid_from: std::string::String::from("2026-05-21T00:00:00Z"),
346 valid_until: std::string::String::from("2026-06-21T00:00:00Z"),
347 principal_signature: std::string::String::from("zPrincipalSignatureBytes"),
348 notary_signature: std::string::String::from("zNotarySignatureBytes"),
349 }
350 }
351
352 #[test]
353 fn the_three_bases_are_pairwise_distinct() {
354 let lone = super::signing_base(&single(), super::ProofRole::Notary)
359 .expect("a lone notary base is always constructible");
360 let chained = chain();
361 let principal = super::signing_base(&chained, super::ProofRole::Principal)
362 .expect("a two-element chain has a principal base");
363 let notary = super::signing_base(&chained, super::ProofRole::Notary)
364 .expect("a two-element chain has a notary base");
365
366 std::assert_ne!(lone, principal);
367 std::assert_ne!(lone, notary);
368 std::assert_ne!(principal, notary);
369 }
370
371 #[test]
372 fn principal_base_is_a_one_element_array_not_an_object() {
373 let base = super::signing_base(&chain(), super::ProofRole::Principal)
379 .expect("a two-element chain has a principal base");
380 std::assert!(
381 base.contains(r#""proof":[{"#),
382 "principal base must carry a one-element ARRAY: {}",
383 base
384 );
385 }
386
387 #[test]
388 fn principal_base_discards_the_notary_proof_entirely() {
389 let base = super::signing_base(&chain(), super::ProofRole::Principal)
393 .expect("a two-element chain has a principal base");
394 std::assert!(
395 !base.contains(super::test_support::NOTARY_PROOF_ID),
396 "principal base must not mention the notary proof: {}",
397 base
398 );
399 }
400
401 #[test]
402 fn notary_base_keeps_both_proofs() {
403 let base = super::signing_base(&chain(), super::ProofRole::Notary)
407 .expect("a two-element chain has a notary base");
408 std::assert!(
409 base.contains(super::test_support::PRINCIPAL_PROOF_ID)
410 && base.contains(super::test_support::NOTARY_PROOF_ID),
411 "notary base must carry both proofs: {}",
412 base
413 );
414 }
415
416 #[test]
417 fn every_envelope_base_empties_rather_than_removes_proof_value() {
418 let chained = chain();
423 for (label, base) in [
424 (
425 "lone",
426 super::signing_base(&single(), super::ProofRole::Notary).expect("lone base"),
427 ),
428 (
429 "principal",
430 super::signing_base(&chained, super::ProofRole::Principal).expect("principal base"),
431 ),
432 (
433 "notary",
434 super::signing_base(&chained, super::ProofRole::Notary).expect("notary base"),
435 ),
436 ] {
437 std::assert!(
438 base.contains(r#""proofValue":"""#),
439 "{} base must keep proofValue present-but-empty: {}",
440 label,
441 base
442 );
443 }
444 }
445
446 #[test]
447 fn principal_base_of_a_single_proof_envelope_is_aph_e013() {
448 let error = super::signing_base(&single(), super::ProofRole::Principal)
452 .expect_err("a single proof has no principal base");
453 std::assert_eq!(error.code(), "APH_E013");
454 }
455
456 #[test]
457 fn principal_base_of_a_one_element_chain_is_aph_e013() {
458 let mut envelope = chain();
462 let principal = envelope
463 .proof
464 .principal()
465 .expect("the chain fixture has a principal proof")
466 .clone();
467 envelope.proof = crate::envelope::EnvelopeProofs::Chain(std::vec![principal]);
468 let error = super::signing_base(&envelope, super::ProofRole::Principal)
469 .expect_err("a one-element chain is not a chain");
470 std::assert_eq!(error.code(), "APH_E013");
471 }
472
473 #[test]
474 fn notary_base_of_a_three_element_chain_is_aph_e013() {
475 let mut envelope = chain();
479 let mut proofs = envelope.proof.all().to_vec();
480 let extra = proofs
481 .last()
482 .expect("the chain fixture is non-empty")
483 .clone();
484 proofs.push(extra);
485 envelope.proof = crate::envelope::EnvelopeProofs::Chain(proofs);
486 let error = super::signing_base(&envelope, super::ProofRole::Notary)
487 .expect_err("a three-element chain has no defined base");
488 std::assert_eq!(error.code(), "APH_E013");
489 }
490
491 #[test]
492 fn mandate_principal_base_removes_both_signature_fields() {
493 let base = super::mandate_signing_base(&sample_mandate(), super::ProofRole::Principal)
497 .expect("a mandate always has a principal base");
498 std::assert!(
499 !base.contains("principalSignature") && !base.contains("notarySignature"),
500 "principal mandate base must carry neither signature: {}",
501 base
502 );
503 }
504
505 #[test]
506 fn mandate_notary_base_keeps_the_principal_signature() {
507 let base = super::mandate_signing_base(&sample_mandate(), super::ProofRole::Notary)
511 .expect("a mandate always has a notary base");
512 std::assert!(
513 base.contains("zPrincipalSignatureBytes") && !base.contains("notarySignature"),
514 "notary mandate base must keep principalSignature and drop its own: {}",
515 base
516 );
517 }
518
519 #[test]
520 fn the_two_mandate_bases_are_distinct() {
521 let mandate = sample_mandate();
525 let principal = super::mandate_signing_base(&mandate, super::ProofRole::Principal)
526 .expect("principal mandate base");
527 let notary =
528 super::mandate_signing_base(&mandate, super::ProofRole::Notary).expect("notary mandate base");
529 std::assert_ne!(principal, notary);
530 }
531
532 #[test]
533 fn bases_are_deterministic() {
534 let chained = chain();
538 let once = super::signing_base(&chained, super::ProofRole::Notary).expect("notary base");
539 let twice = super::signing_base(&chained, super::ProofRole::Notary).expect("notary base");
540 std::assert_eq!(once, twice);
541 }
542}