1use hpke::aead::ChaCha20Poly1305;
34use hpke::kdf::HkdfSha256;
35use hpke::kem::X25519HkdfSha256;
36use hpke::{Deserializable, Kem as KemTrait, OpModeR, OpModeS, Serializable};
37
38type Kem = X25519HkdfSha256;
39
40pub const SUITE: &str = "APH-SEAL-1";
44
45const INFO: &[u8] = b"aph sealed payload v1";
48
49pub use aph_core::{SealedPayload, SealedReader};
52
53#[derive(std::fmt::Debug, thiserror::Error, std::cmp::PartialEq, std::cmp::Eq)]
57pub enum SealError {
58 #[error("unknown seal suite `{0}`; this implementation seals only `{SUITE}`")]
61 UnknownSuite(String),
62 #[error("sealed payload field `{0}` is not unpadded base64url")]
64 MalformedEncoding(&'static str),
65 #[error("the {0} key is not a valid X25519 key for this suite")]
67 MalformedKey(&'static str),
68 #[error(
73 "the seal did not open: wrong reader key, tampered ciphertext, a seal \
74 staged under a different envelope id, or a payload relabeled about its \
75 own suite or reader"
76 )]
77 OpenRefused,
78 #[error("sealing failed: {0}")]
81 SealFailed(String),
82}
83
84#[derive(serde::Serialize)]
94struct SealContext<'a> {
95 suite: &'a str,
96 reader_id: &'a str,
97 reader_kid: &'a str,
98 envelope_id: &'a str,
99}
100
101fn context_aad(suite: &str, reader: &SealedReader, envelope_id: &str) -> Vec<u8> {
102 serde_json::to_vec(&SealContext {
103 suite,
104 reader_id: &reader.id,
105 reader_kid: &reader.kid,
106 envelope_id,
107 })
108 .expect("four string fields serialize infallibly")
109}
110
111fn b64(bytes: &[u8]) -> String {
112 use base64::Engine as _;
113 base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes)
114}
115
116fn unb64(text: &str, field: &'static str) -> std::result::Result<Vec<u8>, SealError> {
117 use base64::Engine as _;
118 base64::engine::general_purpose::URL_SAFE_NO_PAD
119 .decode(text)
120 .map_err(|_| SealError::MalformedEncoding(field))
121}
122
123pub fn seal(
137 csprng: &mut impl rand::CryptoRng,
138 reader: SealedReader,
139 reader_public_key: &[u8],
140 envelope_id: &str,
141 plaintext: &[u8],
142) -> std::result::Result<SealedPayload, SealError> {
143 let pk = <Kem as KemTrait>::PublicKey::from_bytes(reader_public_key)
144 .map_err(|_| SealError::MalformedKey("reader public"))?;
145 let aad = context_aad(SUITE, &reader, envelope_id);
146 let (encapped, ciphertext) = hpke::single_shot_seal::<ChaCha20Poly1305, HkdfSha256, Kem, _>(
147 &OpModeS::Base,
148 &pk,
149 INFO,
150 plaintext,
151 &aad,
152 csprng,
153 )
154 .map_err(|e| SealError::SealFailed(std::format!("{e}")))?;
155 std::result::Result::Ok(SealedPayload {
156 suite: SUITE.to_string(),
157 reader,
158 enc: b64(&encapped.to_bytes()),
159 ciphertext: b64(&ciphertext),
160 })
161}
162
163pub fn unseal(
171 sealed: &SealedPayload,
172 reader_private_key: &[u8],
173 envelope_id: &str,
174) -> std::result::Result<Vec<u8>, SealError> {
175 if sealed.suite != SUITE {
176 return std::result::Result::Err(SealError::UnknownSuite(sealed.suite.clone()));
177 }
178 let sk = <Kem as KemTrait>::PrivateKey::from_bytes(reader_private_key)
179 .map_err(|_| SealError::MalformedKey("reader private"))?;
180 let encapped_bytes = unb64(&sealed.enc, "enc")?;
181 let encapped = <Kem as KemTrait>::EncappedKey::from_bytes(&encapped_bytes)
182 .map_err(|_| SealError::MalformedKey("encapsulated"))?;
183 let ciphertext = unb64(&sealed.ciphertext, "ciphertext")?;
184 let aad = context_aad(&sealed.suite, &sealed.reader, envelope_id);
185 hpke::single_shot_open::<ChaCha20Poly1305, HkdfSha256, Kem>(
186 &OpModeR::Base,
187 &sk,
188 &encapped,
189 INFO,
190 &ciphertext,
191 &aad,
192 )
193 .map_err(|_| SealError::OpenRefused)
194}
195
196#[derive(std::fmt::Debug, thiserror::Error)]
200pub enum EnvelopeSealError {
201 #[error("the envelope carries no sealed payload")]
205 Absent,
206 #[error("{0}")]
211 WireUndeclared(String),
212 #[error(transparent)]
216 Refused(aph_core::AphError),
217}
218
219pub fn seal_into_envelope(
226 csprng: &mut impl rand::CryptoRng,
227 envelope: &mut aph_core::NotarizationEnvelope,
228 reader: SealedReader,
229 reader_public_key: &[u8],
230 plaintext: &[u8],
231) -> std::result::Result<(), EnvelopeSealError> {
232 if envelope.aph_version != "0.2" {
233 return std::result::Result::Err(EnvelopeSealError::WireUndeclared(std::format!(
234 "refusing to seal into an aphVersion `{}` envelope: `sealedPayload` is declared from aphVersion 0.2 (spec/aph-0.2.md), and minting it earlier produces a wire every conformant verifier refuses",
235 envelope.aph_version
236 )));
237 }
238 let sealed = seal(csprng, reader, reader_public_key, &envelope.id, plaintext)
239 .map_err(|e| EnvelopeSealError::WireUndeclared(std::format!("sealing failed: {e}")))?;
240 envelope.credential_subject.sealed_payload = std::option::Option::Some(sealed);
241 std::result::Result::Ok(())
242}
243
244pub fn unseal_from_envelope(
251 envelope: &aph_core::NotarizationEnvelope,
252 reader_private_key: &[u8],
253) -> std::result::Result<Vec<u8>, EnvelopeSealError> {
254 aph_core::sealed_payload_is_declared(envelope).map_err(EnvelopeSealError::WireUndeclared)?;
255 let sealed = envelope
256 .credential_subject
257 .sealed_payload
258 .as_ref()
259 .ok_or(EnvelopeSealError::Absent)?;
260 unseal(sealed, reader_private_key, &envelope.id).map_err(|e| match e {
261 SealError::UnknownSuite(suite) => {
262 EnvelopeSealError::Refused(aph_core::AphError::seal_suite_unknown(suite, SUITE))
263 }
264 _ => EnvelopeSealError::Refused(aph_core::AphError::seal_unopenable(&envelope.id)),
265 })
266}
267
268#[cfg(test)]
269mod tests {
270 fn derive_keypair_for_tests(ikm: &[u8]) -> (Vec<u8>, Vec<u8>) {
275 use hpke::{Kem as KemTrait, Serializable};
276 let (sk, pk) = super::Kem::derive_keypair(ikm);
277 (sk.to_bytes().to_vec(), pk.to_bytes().to_vec())
278 }
279 const RECEIVER_IKM: &[u8] = b"APH-SEALED-TEST-RECEIVER-IKM-0001";
283 const SENDER_IKM: &[u8] = b"APH-SEALED-TEST-SENDER-IKM---0001";
284 const ENVELOPE_ID: &str = "urn:uuid:00000000-0000-4000-8000-0000000000e1";
285
286 fn rng() -> impl rand::CryptoRng {
287 <rand::rngs::StdRng as rand::SeedableRng>::seed_from_u64(7)
290 }
291
292 fn reader(id: &str) -> super::SealedReader {
293 super::SealedReader { id: id.to_string(), kid: std::string::String::from("enc-1") }
294 }
295
296 #[test]
297 fn scenario_one_the_carrier_cannot_read_but_the_receiver_can() {
298 let (receiver_sk, receiver_pk) = derive_keypair_for_tests(RECEIVER_IKM);
303 let sealed = super::seal(
304 &mut rng(),
305 reader("did:web:receiver.example.com"),
306 &receiver_pk,
307 ENVELOPE_ID,
308 b"the order: attack at dawn",
309 )
310 .expect("sealing to a valid reader key succeeds");
311
312 let opened = super::unseal(&sealed, &receiver_sk, ENVELOPE_ID)
313 .expect("the named reader opens the seal");
314 std::assert_eq!(opened, b"the order: attack at dawn");
315
316 let (sender_sk, _) = derive_keypair_for_tests(SENDER_IKM);
320 std::assert_eq!(
321 super::unseal(&sealed, &sender_sk, ENVELOPE_ID).unwrap_err(),
322 super::SealError::OpenRefused,
323 );
324 }
325
326 #[test]
327 fn scenario_two_the_sender_seals_to_itself_and_the_counterparty_cannot_read() {
328 let (sender_sk, sender_pk) = derive_keypair_for_tests(SENDER_IKM);
332 let sealed = super::seal(
333 &mut rng(),
334 reader("did:web:sender.example.com"),
335 &sender_pk,
336 ENVELOPE_ID,
337 b"guardrail overlay the counterparty must hold but not read",
338 )
339 .expect("sealing to one's own key is the same operation");
340
341 let (receiver_sk, _) = derive_keypair_for_tests(RECEIVER_IKM);
342 std::assert_eq!(
343 super::unseal(&sealed, &receiver_sk, ENVELOPE_ID).unwrap_err(),
344 super::SealError::OpenRefused,
345 );
346 std::assert!(super::unseal(&sealed, &sender_sk, ENVELOPE_ID).is_ok());
347 }
348
349 #[test]
350 fn a_seal_lifted_into_a_different_envelope_refuses_even_for_its_reader() {
351 let (receiver_sk, receiver_pk) = derive_keypair_for_tests(RECEIVER_IKM);
356 let sealed = super::seal(
357 &mut rng(),
358 reader("did:web:receiver.example.com"),
359 &receiver_pk,
360 ENVELOPE_ID,
361 b"bound to one envelope",
362 )
363 .expect("seal succeeds");
364 std::assert_eq!(
365 super::unseal(&sealed, &receiver_sk, "urn:uuid:00000000-0000-4000-8000-0000000000e2")
366 .unwrap_err(),
367 super::SealError::OpenRefused,
368 );
369 }
370
371 #[test]
372 fn a_tampered_ciphertext_refuses() {
373 let (receiver_sk, receiver_pk) = derive_keypair_for_tests(RECEIVER_IKM);
377 let mut sealed = super::seal(
378 &mut rng(),
379 reader("did:web:receiver.example.com"),
380 &receiver_pk,
381 ENVELOPE_ID,
382 b"integrity or refusal",
383 )
384 .expect("seal succeeds");
385 let mut bytes = super::unb64(&sealed.ciphertext, "ciphertext").expect("decodes");
386 bytes[0] ^= 0x01;
387 sealed.ciphertext = super::b64(&bytes);
388 std::assert_eq!(
389 super::unseal(&sealed, &receiver_sk, ENVELOPE_ID).unwrap_err(),
390 super::SealError::OpenRefused,
391 );
392 }
393
394 #[test]
395 fn an_unknown_suite_is_refused_before_any_cryptography() {
396 let (receiver_sk, receiver_pk) = derive_keypair_for_tests(RECEIVER_IKM);
400 let mut sealed = super::seal(
401 &mut rng(),
402 reader("did:web:receiver.example.com"),
403 &receiver_pk,
404 ENVELOPE_ID,
405 b"x",
406 )
407 .expect("seal succeeds");
408 sealed.suite = std::string::String::from("APH-SEAL-99");
409 std::assert_eq!(
410 super::unseal(&sealed, &receiver_sk, ENVELOPE_ID).unwrap_err(),
411 super::SealError::UnknownSuite(std::string::String::from("APH-SEAL-99")),
412 );
413 }
414
415 #[test]
416 fn the_wire_shape_round_trips_and_refuses_unknown_members() {
417 let (_, receiver_pk) = derive_keypair_for_tests(RECEIVER_IKM);
421 let sealed = super::seal(
422 &mut rng(),
423 reader("did:web:receiver.example.com"),
424 &receiver_pk,
425 ENVELOPE_ID,
426 b"round trip",
427 )
428 .expect("seal succeeds");
429 let json = serde_json::to_string(&sealed).expect("serializes");
430 std::assert!(json.contains("\"suite\":\"APH-SEAL-1\""));
431 std::assert!(json.contains("\"reader\""));
432 let back: super::SealedPayload = serde_json::from_str(&json).expect("parses");
433 std::assert_eq!(back, sealed);
434
435 let smuggled = json.replacen('{', "{\"extra\":1,", 1);
436 std::assert!(
437 serde_json::from_str::<super::SealedPayload>(&smuggled).is_err(),
438 "an unknown member is refused at strict parse, like every wire struct"
439 );
440 }
441
442 #[test]
443 fn a_relabeled_reader_or_suite_refuses_even_with_the_right_key() {
444 let (receiver_sk, receiver_pk) = derive_keypair_for_tests(RECEIVER_IKM);
451 let sealed = super::seal(
452 &mut rng(),
453 reader("did:web:receiver.example.com"),
454 &receiver_pk,
455 ENVELOPE_ID,
456 b"context-bound",
457 )
458 .expect("seal succeeds");
459
460 let mut relabeled = sealed.clone();
461 relabeled.reader.id = std::string::String::from("did:web:attacker.example.com");
462 std::assert_eq!(
463 super::unseal(&relabeled, &receiver_sk, ENVELOPE_ID).unwrap_err(),
464 super::SealError::OpenRefused,
465 "a relabeled reader must refuse: the seal authenticates its context"
466 );
467
468 let mut rekidded = sealed.clone();
469 rekidded.reader.kid = std::string::String::from("enc-2");
470 std::assert_eq!(
471 super::unseal(&rekidded, &receiver_sk, ENVELOPE_ID).unwrap_err(),
472 super::SealError::OpenRefused,
473 "a relabeled kid must refuse for the same reason"
474 );
475 }
476
477 fn envelope_v02() -> aph_core::NotarizationEnvelope {
480 let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
484 .join("../../../examples/audience_bound_envelope.json");
485 let raw = std::fs::read_to_string(&path)
486 .unwrap_or_else(|e| std::panic!("failed to read {:?}: {}", path, e));
487 let mut envelope: aph_core::NotarizationEnvelope =
488 serde_json::from_str(&raw).expect("the golden parses");
489 envelope.aph_version = std::string::String::from("0.2");
490 envelope
491 }
492
493 #[test]
494 fn seal_into_and_unseal_from_an_envelope_round_trip() {
495 let (receiver_sk, receiver_pk) = derive_keypair_for_tests(RECEIVER_IKM);
499 let mut envelope = envelope_v02();
500 super::seal_into_envelope(
501 &mut rng(),
502 &mut envelope,
503 reader("did:web:receiver.example.com"),
504 &receiver_pk,
505 b"carried, verified, unread until here",
506 )
507 .expect("sealing into a 0.2 envelope succeeds");
508 std::assert!(envelope.credential_subject.sealed_payload.is_some());
509
510 let opened = super::unseal_from_envelope(&envelope, &receiver_sk)
511 .expect("the named reader opens the envelope's seal");
512 std::assert_eq!(opened, b"carried, verified, unread until here");
513 }
514
515 #[test]
516 fn sealing_into_a_v01_envelope_refuses_up_front() {
517 let (_, receiver_pk) = derive_keypair_for_tests(RECEIVER_IKM);
520 let mut envelope = envelope_v02();
521 envelope.aph_version = std::string::String::from("0.1");
522 let err = super::seal_into_envelope(
523 &mut rng(),
524 &mut envelope,
525 reader("did:web:receiver.example.com"),
526 &receiver_pk,
527 b"x",
528 )
529 .expect_err("a 0.1 wire cannot carry the member");
530 std::assert!(std::matches!(err, super::EnvelopeSealError::WireUndeclared(_)));
531 }
532
533 #[test]
534 fn a_sealed_member_on_a_v01_wire_refuses_at_the_declaration_rule() {
535 let (receiver_sk, receiver_pk) = derive_keypair_for_tests(RECEIVER_IKM);
540 let mut envelope = envelope_v02();
541 super::seal_into_envelope(
542 &mut rng(),
543 &mut envelope,
544 reader("did:web:receiver.example.com"),
545 &receiver_pk,
546 b"x",
547 )
548 .expect("seal succeeds on 0.2");
549 envelope.aph_version = std::string::String::from("0.1");
550 let err = super::unseal_from_envelope(&envelope, &receiver_sk)
551 .expect_err("the declaration rule refuses first");
552 std::assert!(std::matches!(err, super::EnvelopeSealError::WireUndeclared(_)));
553 }
554
555 #[test]
556 fn envelope_unseal_failures_carry_their_draft_codes() {
557 let (receiver_sk, receiver_pk) = derive_keypair_for_tests(RECEIVER_IKM);
562 let mut envelope = envelope_v02();
563 super::seal_into_envelope(
564 &mut rng(),
565 &mut envelope,
566 reader("did:web:receiver.example.com"),
567 &receiver_pk,
568 b"coded refusals",
569 )
570 .expect("seal succeeds");
571
572 let mut suite_relabeled = envelope.clone();
573 suite_relabeled
574 .credential_subject
575 .sealed_payload
576 .as_mut()
577 .expect("sealed")
578 .suite = std::string::String::from("APH-SEAL-99");
579 match super::unseal_from_envelope(&suite_relabeled, &receiver_sk) {
580 std::result::Result::Err(super::EnvelopeSealError::Refused(e)) => {
581 std::assert_eq!(e.code(), "APH_E022")
582 }
583 other => std::panic!("expected an APH_E022 refusal, got {:?}", other),
584 }
585
586 let (sender_sk, _) = derive_keypair_for_tests(SENDER_IKM);
587 match super::unseal_from_envelope(&envelope, &sender_sk) {
588 std::result::Result::Err(super::EnvelopeSealError::Refused(e)) => {
589 std::assert_eq!(e.code(), "APH_E021")
590 }
591 other => std::panic!("expected an APH_E021 refusal, got {:?}", other),
592 }
593 }
594
595 #[test]
596 fn body_sha256_binds_the_raw_ciphertext_octets_so_hops_verify_blind() {
597 use sha2::Digest as _;
602 let (_, receiver_pk) = derive_keypair_for_tests(RECEIVER_IKM);
603 let mut envelope = envelope_v02();
604 super::seal_into_envelope(
605 &mut rng(),
606 &mut envelope,
607 reader("did:web:receiver.example.com"),
608 &receiver_pk,
609 b"the body no hop reads",
610 )
611 .expect("seal succeeds");
612 let sealed = envelope.credential_subject.sealed_payload.as_ref().expect("sealed");
613 let raw = super::unb64(&sealed.ciphertext, "ciphertext").expect("decodes");
614 let digest = std::format!("{:x}", sha2::Sha256::digest(&raw));
615 envelope.credential_subject.communication.body_sha256 = digest.clone();
616
617 let recomputed = std::format!(
619 "{:x}",
620 sha2::Sha256::digest(super::unb64(&sealed.ciphertext, "ciphertext").expect("decodes"))
621 );
622 std::assert_eq!(recomputed, envelope.credential_subject.communication.body_sha256);
623 }
624
625 fn draft_vector_path() -> std::path::PathBuf {
628 std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
629 .join("../../../examples/v0.2/sealed_envelope.json")
630 }
631
632 fn regenerate_draft_vector() -> String {
637 let (_, receiver_pk) = derive_keypair_for_tests(RECEIVER_IKM);
638 let mut envelope = envelope_v02();
639 envelope.id = std::string::String::from("urn:uuid:00000000-0000-4000-8000-00000000000e");
640 super::seal_into_envelope(
641 &mut rng(),
642 &mut envelope,
643 reader("did:web:receiver.example.com"),
644 &receiver_pk,
645 b"the body no hop reads: the v0.2-draft sealed vector",
646 )
647 .expect("sealing the vector succeeds");
648 use sha2::Digest as _;
649 let sealed = envelope.credential_subject.sealed_payload.as_ref().expect("sealed");
650 let raw = super::unb64(&sealed.ciphertext, "ciphertext").expect("decodes");
651 envelope.credential_subject.communication.body_sha256 =
652 std::format!("{:x}", sha2::Sha256::digest(&raw));
653 std::format!(
654 "{}\n",
655 serde_json::to_string_pretty(&envelope).expect("the vector serializes")
656 )
657 }
658
659 #[test]
660 fn the_committed_draft_vector_is_byte_identical_to_a_fresh_mint() {
661 let regenerated = regenerate_draft_vector();
665 let path = draft_vector_path();
666 let published = std::fs::read_to_string(&path).unwrap_or_default();
670 if published != regenerated {
671 std::panic!(
672 "examples/v0.2/sealed_envelope.json has drifted from the bytes \
673 this suite mints.\nTo fix in ONE step, overwrite that file with EXACTLY \
674 the content between the cut lines (the final newline is part of the \
675 content):\n----8<----\n{}----8<----",
676 regenerated
677 );
678 }
679 }
680
681 #[test]
682 fn the_committed_draft_vector_opens_for_its_reader_and_verifies_blind() {
683 use sha2::Digest as _;
688 let raw_json = std::fs::read_to_string(draft_vector_path()).expect("the vector is on disk");
689 let envelope: aph_core::NotarizationEnvelope =
690 aph_core::parse_envelope_json(&raw_json).expect("the vector strict-parses as 0.2");
691
692 let sealed = envelope.credential_subject.sealed_payload.as_ref().expect("sealed");
693 let raw = super::unb64(&sealed.ciphertext, "ciphertext").expect("decodes");
694 std::assert_eq!(
695 std::format!("{:x}", sha2::Sha256::digest(&raw)),
696 envelope.credential_subject.communication.body_sha256,
697 "the blind hop's body binding holds over the committed bytes"
698 );
699
700 let (receiver_sk, _) = derive_keypair_for_tests(RECEIVER_IKM);
701 let opened = super::unseal_from_envelope(&envelope, &receiver_sk)
702 .expect("the named reader opens the committed vector");
703 std::assert_eq!(opened, b"the body no hop reads: the v0.2-draft sealed vector");
704 }
705
706 const NOTARY_SEED: [u8; 32] = [
709 0x9d, 0x61, 0xb1, 0x9d, 0xef, 0xfd, 0x5a, 0x60, 0xba, 0x84, 0x4a, 0xf4, 0x92, 0xec, 0x2c,
712 0xc4, 0x44, 0x49, 0xc5, 0x69, 0x7b, 0x32, 0x69, 0x19, 0x70, 0x3b, 0xac, 0x03, 0x1c, 0xae,
713 0x7f, 0x60,
714 ];
715
716 fn notary_key() -> ed25519_dalek::SigningKey {
717 ed25519_dalek::SigningKey::from_bytes(&NOTARY_SEED)
718 }
719
720 fn signed_vector_path() -> std::path::PathBuf {
721 std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
722 .join("../../../examples/v0.2/sealed_signed_envelope.json")
723 }
724
725 fn regenerate_signed_vector() -> String {
731 use sha2::Digest as _;
732 let key = notary_key();
733 let did = aph_core::crypto::did_key::encode_ed25519(&key.verifying_key());
734 let fragment = did.trim_start_matches("did:key:").to_string();
735
736 let (_, receiver_pk) = derive_keypair_for_tests(RECEIVER_IKM);
737 let mut envelope = envelope_v02();
738 envelope.id = std::string::String::from("urn:uuid:00000000-0000-4000-8000-00000000000f");
739 envelope.issuer = did.clone();
740 super::seal_into_envelope(
741 &mut rng(),
742 &mut envelope,
743 reader("did:web:receiver.example.com"),
744 &receiver_pk,
745 b"the signed sealed vector: carried blind, verified by anyone, read by one",
746 )
747 .expect("sealing succeeds");
748 let raw = {
749 let sealed = envelope.credential_subject.sealed_payload.as_ref().expect("sealed");
750 super::unb64(&sealed.ciphertext, "ciphertext").expect("decodes")
751 };
752 envelope.credential_subject.communication.body_sha256 =
753 std::format!("{:x}", sha2::Sha256::digest(&raw));
754
755 envelope.proof = aph_core::EnvelopeProofs::Single(aph_core::EnvelopeProof {
756 r#type: std::string::String::from("DataIntegrityProof"),
757 cryptosuite: std::option::Option::None, verification_method: std::format!("{did}#{fragment}"),
759 created: std::string::String::from("2026-08-29T00:00:01Z"),
760 proof_purpose: std::string::String::from("assertionMethod"),
761 proof_value: std::string::String::new(),
762 id: std::option::Option::None,
763 previous_proof: std::option::Option::None,
764 });
765 aph_core::crypto::eddsa_jcs::sign_envelope(&mut envelope, &key).expect("signing succeeds");
766 std::format!(
767 "{}\n",
768 serde_json::to_string_pretty(&envelope).expect("the vector serializes")
769 )
770 }
771
772 #[test]
773 fn the_signed_sealed_vector_is_byte_identical_to_a_fresh_mint() {
774 let regenerated = regenerate_signed_vector();
775 let path = signed_vector_path();
776 let published = std::fs::read_to_string(&path).unwrap_or_default();
777 if published != regenerated {
778 std::panic!(
779 "examples/v0.2/sealed_signed_envelope.json has drifted from the \
780 bytes this suite mints.\nOverwrite it with EXACTLY the content between \
781 the cut lines (final newline included):\n----8<----\n{}----8<----",
782 regenerated
783 );
784 }
785 }
786
787 #[test]
788 fn the_signed_sealed_vector_verifies_blind_and_opens_for_its_reader() {
789 let raw_json =
793 std::fs::read_to_string(signed_vector_path()).expect("the signed vector is on disk");
794 let envelope: aph_core::NotarizationEnvelope =
795 aph_core::parse_envelope_json(&raw_json).expect("strict-parses as 0.2");
796 aph_core::crypto::eddsa_jcs::verify_envelope_did_key(&envelope)
797 .expect("the lone did:key proof verifies offline, sealed member covered");
798
799 let (receiver_sk, _) = derive_keypair_for_tests(RECEIVER_IKM);
800 let opened = super::unseal_from_envelope(&envelope, &receiver_sk)
801 .expect("the named reader opens the committed signed vector");
802 std::assert_eq!(
803 opened,
804 b"the signed sealed vector: carried blind, verified by anyone, read by one"
805 );
806 }
807
808 fn rotation_vector_path() -> std::path::PathBuf {
809 std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
810 .join("../../../examples/v0.2/rotation_attestation.json")
811 }
812
813 fn regenerate_rotation_vector() -> String {
814 let key = notary_key();
815 let did = aph_core::crypto::did_key::encode_ed25519(&key.verifying_key());
816 let successor_multibase = did.trim_start_matches("did:key:").to_string();
817 let mut attestation = aph_core::RotationAttestation {
818 aph_version: std::string::String::from("0.2"),
819 r#type: std::string::String::from(aph_core::ROTATION_ATTESTATION_TYPE),
820 id: std::string::String::from("urn:uuid:00000000-0000-4000-8000-0000000000a0"),
821 subject: std::string::String::from("did:web:notary.squillo.com"),
822 predecessor: std::string::String::from("did:web:notary.squillo.com#k1"),
823 successor: aph_core::RotationSuccessor {
824 kid: std::string::String::from("k2"),
825 alg: std::string::String::from("EdDSA"),
826 public_key_multibase: successor_multibase,
827 not_before: std::string::String::from("2027-01-01T00:00:00Z"),
828 not_after: std::string::String::from("2029-01-01T00:00:00Z"),
829 },
830 created: std::string::String::from("2026-08-29T00:00:00Z"),
831 proof: aph_core::EnvelopeProof {
832 r#type: std::string::String::from("DataIntegrityProof"),
833 cryptosuite: std::option::Option::Some(std::string::String::from("eddsa-jcs-2022")),
834 verification_method: std::string::String::from("did:web:notary.squillo.com#k1"),
835 created: std::string::String::from("2026-08-29T00:00:00Z"),
836 proof_purpose: std::string::String::from("assertionMethod"),
837 proof_value: std::string::String::new(),
838 id: std::option::Option::None,
839 previous_proof: std::option::Option::None,
840 },
841 };
842 aph_core::sign_rotation_attestation(&mut attestation, &key).expect("signing succeeds");
843 std::format!(
844 "{}\n",
845 serde_json::to_string_pretty(&attestation).expect("the vector serializes")
846 )
847 }
848
849 #[test]
850 fn the_rotation_attestation_vector_is_byte_identical_to_a_fresh_mint() {
851 let regenerated = regenerate_rotation_vector();
852 let path = rotation_vector_path();
853 let published = std::fs::read_to_string(&path).unwrap_or_default();
854 if published != regenerated {
855 std::panic!(
856 "examples/v0.2/rotation_attestation.json has drifted from the \
857 bytes this suite mints.\nOverwrite it with EXACTLY the content between \
858 the cut lines (final newline included):\n----8<----\n{}----8<----",
859 regenerated
860 );
861 }
862 }
863
864 #[test]
865 fn the_committed_rotation_vector_verifies_against_its_predecessor_key() {
866 let raw = std::fs::read_to_string(rotation_vector_path()).expect("the vector is on disk");
867 let attestation: aph_core::RotationAttestation =
868 serde_json::from_str(&raw).expect("strict-parses");
869 aph_core::verify_rotation_attestation(&attestation, ¬ary_key().verifying_key())
870 .expect("the predecessor's committed statement verifies");
871 }
872}