use hpke::aead::ChaCha20Poly1305;
use hpke::kdf::HkdfSha256;
use hpke::kem::X25519HkdfSha256;
use hpke::{Deserializable, Kem as KemTrait, OpModeR, OpModeS, Serializable};
type Kem = X25519HkdfSha256;
pub const SUITE: &str = "APH-SEAL-1";
const INFO: &[u8] = b"aph sealed payload v1";
pub use aph_core::{SealedPayload, SealedReader};
#[derive(std::fmt::Debug, thiserror::Error, std::cmp::PartialEq, std::cmp::Eq)]
pub enum SealError {
#[error("unknown seal suite `{0}`; this implementation seals only `{SUITE}`")]
UnknownSuite(String),
#[error("sealed payload field `{0}` is not unpadded base64url")]
MalformedEncoding(&'static str),
#[error("the {0} key is not a valid X25519 key for this suite")]
MalformedKey(&'static str),
#[error(
"the seal did not open: wrong reader key, tampered ciphertext, a seal \
staged under a different envelope id, or a payload relabeled about its \
own suite or reader"
)]
OpenRefused,
#[error("sealing failed: {0}")]
SealFailed(String),
}
#[derive(serde::Serialize)]
struct SealContext<'a> {
suite: &'a str,
reader_id: &'a str,
reader_kid: &'a str,
envelope_id: &'a str,
}
fn context_aad(suite: &str, reader: &SealedReader, envelope_id: &str) -> Vec<u8> {
serde_json::to_vec(&SealContext {
suite,
reader_id: &reader.id,
reader_kid: &reader.kid,
envelope_id,
})
.expect("four string fields serialize infallibly")
}
fn b64(bytes: &[u8]) -> String {
use base64::Engine as _;
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes)
}
fn unb64(text: &str, field: &'static str) -> std::result::Result<Vec<u8>, SealError> {
use base64::Engine as _;
base64::engine::general_purpose::URL_SAFE_NO_PAD
.decode(text)
.map_err(|_| SealError::MalformedEncoding(field))
}
pub fn seal(
csprng: &mut impl rand::CryptoRng,
reader: SealedReader,
reader_public_key: &[u8],
envelope_id: &str,
plaintext: &[u8],
) -> std::result::Result<SealedPayload, SealError> {
let pk = <Kem as KemTrait>::PublicKey::from_bytes(reader_public_key)
.map_err(|_| SealError::MalformedKey("reader public"))?;
let aad = context_aad(SUITE, &reader, envelope_id);
let (encapped, ciphertext) = hpke::single_shot_seal::<ChaCha20Poly1305, HkdfSha256, Kem, _>(
&OpModeS::Base,
&pk,
INFO,
plaintext,
&aad,
csprng,
)
.map_err(|e| SealError::SealFailed(std::format!("{e}")))?;
std::result::Result::Ok(SealedPayload {
suite: SUITE.to_string(),
reader,
enc: b64(&encapped.to_bytes()),
ciphertext: b64(&ciphertext),
})
}
pub fn unseal(
sealed: &SealedPayload,
reader_private_key: &[u8],
envelope_id: &str,
) -> std::result::Result<Vec<u8>, SealError> {
if sealed.suite != SUITE {
return std::result::Result::Err(SealError::UnknownSuite(sealed.suite.clone()));
}
let sk = <Kem as KemTrait>::PrivateKey::from_bytes(reader_private_key)
.map_err(|_| SealError::MalformedKey("reader private"))?;
let encapped_bytes = unb64(&sealed.enc, "enc")?;
let encapped = <Kem as KemTrait>::EncappedKey::from_bytes(&encapped_bytes)
.map_err(|_| SealError::MalformedKey("encapsulated"))?;
let ciphertext = unb64(&sealed.ciphertext, "ciphertext")?;
let aad = context_aad(&sealed.suite, &sealed.reader, envelope_id);
hpke::single_shot_open::<ChaCha20Poly1305, HkdfSha256, Kem>(
&OpModeR::Base,
&sk,
&encapped,
INFO,
&ciphertext,
&aad,
)
.map_err(|_| SealError::OpenRefused)
}
#[derive(std::fmt::Debug, thiserror::Error)]
pub enum EnvelopeSealError {
#[error("the envelope carries no sealed payload")]
Absent,
#[error("{0}")]
WireUndeclared(String),
#[error(transparent)]
Refused(aph_core::AphError),
}
pub fn seal_into_envelope(
csprng: &mut impl rand::CryptoRng,
envelope: &mut aph_core::NotarizationEnvelope,
reader: SealedReader,
reader_public_key: &[u8],
plaintext: &[u8],
) -> std::result::Result<(), EnvelopeSealError> {
if envelope.aph_version != "0.2" {
return std::result::Result::Err(EnvelopeSealError::WireUndeclared(std::format!(
"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",
envelope.aph_version
)));
}
let sealed = seal(csprng, reader, reader_public_key, &envelope.id, plaintext)
.map_err(|e| EnvelopeSealError::WireUndeclared(std::format!("sealing failed: {e}")))?;
envelope.credential_subject.sealed_payload = std::option::Option::Some(sealed);
std::result::Result::Ok(())
}
pub fn unseal_from_envelope(
envelope: &aph_core::NotarizationEnvelope,
reader_private_key: &[u8],
) -> std::result::Result<Vec<u8>, EnvelopeSealError> {
aph_core::sealed_payload_is_declared(envelope).map_err(EnvelopeSealError::WireUndeclared)?;
let sealed = envelope
.credential_subject
.sealed_payload
.as_ref()
.ok_or(EnvelopeSealError::Absent)?;
unseal(sealed, reader_private_key, &envelope.id).map_err(|e| match e {
SealError::UnknownSuite(suite) => {
EnvelopeSealError::Refused(aph_core::AphError::seal_suite_unknown(suite, SUITE))
}
_ => EnvelopeSealError::Refused(aph_core::AphError::seal_unopenable(&envelope.id)),
})
}
#[cfg(test)]
mod tests {
fn derive_keypair_for_tests(ikm: &[u8]) -> (Vec<u8>, Vec<u8>) {
use hpke::{Kem as KemTrait, Serializable};
let (sk, pk) = super::Kem::derive_keypair(ikm);
(sk.to_bytes().to_vec(), pk.to_bytes().to_vec())
}
const RECEIVER_IKM: &[u8] = b"APH-SEALED-TEST-RECEIVER-IKM-0001";
const SENDER_IKM: &[u8] = b"APH-SEALED-TEST-SENDER-IKM---0001";
const ENVELOPE_ID: &str = "urn:uuid:00000000-0000-4000-8000-0000000000e1";
fn rng() -> impl rand::CryptoRng {
<rand::rngs::StdRng as rand::SeedableRng>::seed_from_u64(7)
}
fn reader(id: &str) -> super::SealedReader {
super::SealedReader { id: id.to_string(), kid: std::string::String::from("enc-1") }
}
#[test]
fn scenario_one_the_carrier_cannot_read_but_the_receiver_can() {
let (receiver_sk, receiver_pk) = derive_keypair_for_tests(RECEIVER_IKM);
let sealed = super::seal(
&mut rng(),
reader("did:web:receiver.example.com"),
&receiver_pk,
ENVELOPE_ID,
b"the order: attack at dawn",
)
.expect("sealing to a valid reader key succeeds");
let opened = super::unseal(&sealed, &receiver_sk, ENVELOPE_ID)
.expect("the named reader opens the seal");
std::assert_eq!(opened, b"the order: attack at dawn");
let (sender_sk, _) = derive_keypair_for_tests(SENDER_IKM);
std::assert_eq!(
super::unseal(&sealed, &sender_sk, ENVELOPE_ID).unwrap_err(),
super::SealError::OpenRefused,
);
}
#[test]
fn scenario_two_the_sender_seals_to_itself_and_the_counterparty_cannot_read() {
let (sender_sk, sender_pk) = derive_keypair_for_tests(SENDER_IKM);
let sealed = super::seal(
&mut rng(),
reader("did:web:sender.example.com"),
&sender_pk,
ENVELOPE_ID,
b"guardrail overlay the counterparty must hold but not read",
)
.expect("sealing to one's own key is the same operation");
let (receiver_sk, _) = derive_keypair_for_tests(RECEIVER_IKM);
std::assert_eq!(
super::unseal(&sealed, &receiver_sk, ENVELOPE_ID).unwrap_err(),
super::SealError::OpenRefused,
);
std::assert!(super::unseal(&sealed, &sender_sk, ENVELOPE_ID).is_ok());
}
#[test]
fn a_seal_lifted_into_a_different_envelope_refuses_even_for_its_reader() {
let (receiver_sk, receiver_pk) = derive_keypair_for_tests(RECEIVER_IKM);
let sealed = super::seal(
&mut rng(),
reader("did:web:receiver.example.com"),
&receiver_pk,
ENVELOPE_ID,
b"bound to one envelope",
)
.expect("seal succeeds");
std::assert_eq!(
super::unseal(&sealed, &receiver_sk, "urn:uuid:00000000-0000-4000-8000-0000000000e2")
.unwrap_err(),
super::SealError::OpenRefused,
);
}
#[test]
fn a_tampered_ciphertext_refuses() {
let (receiver_sk, receiver_pk) = derive_keypair_for_tests(RECEIVER_IKM);
let mut sealed = super::seal(
&mut rng(),
reader("did:web:receiver.example.com"),
&receiver_pk,
ENVELOPE_ID,
b"integrity or refusal",
)
.expect("seal succeeds");
let mut bytes = super::unb64(&sealed.ciphertext, "ciphertext").expect("decodes");
bytes[0] ^= 0x01;
sealed.ciphertext = super::b64(&bytes);
std::assert_eq!(
super::unseal(&sealed, &receiver_sk, ENVELOPE_ID).unwrap_err(),
super::SealError::OpenRefused,
);
}
#[test]
fn an_unknown_suite_is_refused_before_any_cryptography() {
let (receiver_sk, receiver_pk) = derive_keypair_for_tests(RECEIVER_IKM);
let mut sealed = super::seal(
&mut rng(),
reader("did:web:receiver.example.com"),
&receiver_pk,
ENVELOPE_ID,
b"x",
)
.expect("seal succeeds");
sealed.suite = std::string::String::from("APH-SEAL-99");
std::assert_eq!(
super::unseal(&sealed, &receiver_sk, ENVELOPE_ID).unwrap_err(),
super::SealError::UnknownSuite(std::string::String::from("APH-SEAL-99")),
);
}
#[test]
fn the_wire_shape_round_trips_and_refuses_unknown_members() {
let (_, receiver_pk) = derive_keypair_for_tests(RECEIVER_IKM);
let sealed = super::seal(
&mut rng(),
reader("did:web:receiver.example.com"),
&receiver_pk,
ENVELOPE_ID,
b"round trip",
)
.expect("seal succeeds");
let json = serde_json::to_string(&sealed).expect("serializes");
std::assert!(json.contains("\"suite\":\"APH-SEAL-1\""));
std::assert!(json.contains("\"reader\""));
let back: super::SealedPayload = serde_json::from_str(&json).expect("parses");
std::assert_eq!(back, sealed);
let smuggled = json.replacen('{', "{\"extra\":1,", 1);
std::assert!(
serde_json::from_str::<super::SealedPayload>(&smuggled).is_err(),
"an unknown member is refused at strict parse, like every wire struct"
);
}
#[test]
fn a_relabeled_reader_or_suite_refuses_even_with_the_right_key() {
let (receiver_sk, receiver_pk) = derive_keypair_for_tests(RECEIVER_IKM);
let sealed = super::seal(
&mut rng(),
reader("did:web:receiver.example.com"),
&receiver_pk,
ENVELOPE_ID,
b"context-bound",
)
.expect("seal succeeds");
let mut relabeled = sealed.clone();
relabeled.reader.id = std::string::String::from("did:web:attacker.example.com");
std::assert_eq!(
super::unseal(&relabeled, &receiver_sk, ENVELOPE_ID).unwrap_err(),
super::SealError::OpenRefused,
"a relabeled reader must refuse: the seal authenticates its context"
);
let mut rekidded = sealed.clone();
rekidded.reader.kid = std::string::String::from("enc-2");
std::assert_eq!(
super::unseal(&rekidded, &receiver_sk, ENVELOPE_ID).unwrap_err(),
super::SealError::OpenRefused,
"a relabeled kid must refuse for the same reason"
);
}
fn envelope_v02() -> aph_core::NotarizationEnvelope {
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../../examples/audience_bound_envelope.json");
let raw = std::fs::read_to_string(&path)
.unwrap_or_else(|e| std::panic!("failed to read {:?}: {}", path, e));
let mut envelope: aph_core::NotarizationEnvelope =
serde_json::from_str(&raw).expect("the golden parses");
envelope.aph_version = std::string::String::from("0.2");
envelope
}
#[test]
fn seal_into_and_unseal_from_an_envelope_round_trip() {
let (receiver_sk, receiver_pk) = derive_keypair_for_tests(RECEIVER_IKM);
let mut envelope = envelope_v02();
super::seal_into_envelope(
&mut rng(),
&mut envelope,
reader("did:web:receiver.example.com"),
&receiver_pk,
b"carried, verified, unread until here",
)
.expect("sealing into a 0.2 envelope succeeds");
std::assert!(envelope.credential_subject.sealed_payload.is_some());
let opened = super::unseal_from_envelope(&envelope, &receiver_sk)
.expect("the named reader opens the envelope's seal");
std::assert_eq!(opened, b"carried, verified, unread until here");
}
#[test]
fn sealing_into_a_v01_envelope_refuses_up_front() {
let (_, receiver_pk) = derive_keypair_for_tests(RECEIVER_IKM);
let mut envelope = envelope_v02();
envelope.aph_version = std::string::String::from("0.1");
let err = super::seal_into_envelope(
&mut rng(),
&mut envelope,
reader("did:web:receiver.example.com"),
&receiver_pk,
b"x",
)
.expect_err("a 0.1 wire cannot carry the member");
std::assert!(std::matches!(err, super::EnvelopeSealError::WireUndeclared(_)));
}
#[test]
fn a_sealed_member_on_a_v01_wire_refuses_at_the_declaration_rule() {
let (receiver_sk, receiver_pk) = derive_keypair_for_tests(RECEIVER_IKM);
let mut envelope = envelope_v02();
super::seal_into_envelope(
&mut rng(),
&mut envelope,
reader("did:web:receiver.example.com"),
&receiver_pk,
b"x",
)
.expect("seal succeeds on 0.2");
envelope.aph_version = std::string::String::from("0.1");
let err = super::unseal_from_envelope(&envelope, &receiver_sk)
.expect_err("the declaration rule refuses first");
std::assert!(std::matches!(err, super::EnvelopeSealError::WireUndeclared(_)));
}
#[test]
fn envelope_unseal_failures_carry_their_draft_codes() {
let (receiver_sk, receiver_pk) = derive_keypair_for_tests(RECEIVER_IKM);
let mut envelope = envelope_v02();
super::seal_into_envelope(
&mut rng(),
&mut envelope,
reader("did:web:receiver.example.com"),
&receiver_pk,
b"coded refusals",
)
.expect("seal succeeds");
let mut suite_relabeled = envelope.clone();
suite_relabeled
.credential_subject
.sealed_payload
.as_mut()
.expect("sealed")
.suite = std::string::String::from("APH-SEAL-99");
match super::unseal_from_envelope(&suite_relabeled, &receiver_sk) {
std::result::Result::Err(super::EnvelopeSealError::Refused(e)) => {
std::assert_eq!(e.code(), "APH_E022")
}
other => std::panic!("expected an APH_E022 refusal, got {:?}", other),
}
let (sender_sk, _) = derive_keypair_for_tests(SENDER_IKM);
match super::unseal_from_envelope(&envelope, &sender_sk) {
std::result::Result::Err(super::EnvelopeSealError::Refused(e)) => {
std::assert_eq!(e.code(), "APH_E021")
}
other => std::panic!("expected an APH_E021 refusal, got {:?}", other),
}
}
#[test]
fn body_sha256_binds_the_raw_ciphertext_octets_so_hops_verify_blind() {
use sha2::Digest as _;
let (_, receiver_pk) = derive_keypair_for_tests(RECEIVER_IKM);
let mut envelope = envelope_v02();
super::seal_into_envelope(
&mut rng(),
&mut envelope,
reader("did:web:receiver.example.com"),
&receiver_pk,
b"the body no hop reads",
)
.expect("seal succeeds");
let sealed = envelope.credential_subject.sealed_payload.as_ref().expect("sealed");
let raw = super::unb64(&sealed.ciphertext, "ciphertext").expect("decodes");
let digest = std::format!("{:x}", sha2::Sha256::digest(&raw));
envelope.credential_subject.communication.body_sha256 = digest.clone();
let recomputed = std::format!(
"{:x}",
sha2::Sha256::digest(super::unb64(&sealed.ciphertext, "ciphertext").expect("decodes"))
);
std::assert_eq!(recomputed, envelope.credential_subject.communication.body_sha256);
}
fn draft_vector_path() -> std::path::PathBuf {
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../../examples/v0.2/sealed_envelope.json")
}
fn regenerate_draft_vector() -> String {
let (_, receiver_pk) = derive_keypair_for_tests(RECEIVER_IKM);
let mut envelope = envelope_v02();
envelope.id = std::string::String::from("urn:uuid:00000000-0000-4000-8000-00000000000e");
super::seal_into_envelope(
&mut rng(),
&mut envelope,
reader("did:web:receiver.example.com"),
&receiver_pk,
b"the body no hop reads: the v0.2-draft sealed vector",
)
.expect("sealing the vector succeeds");
use sha2::Digest as _;
let sealed = envelope.credential_subject.sealed_payload.as_ref().expect("sealed");
let raw = super::unb64(&sealed.ciphertext, "ciphertext").expect("decodes");
envelope.credential_subject.communication.body_sha256 =
std::format!("{:x}", sha2::Sha256::digest(&raw));
std::format!(
"{}\n",
serde_json::to_string_pretty(&envelope).expect("the vector serializes")
)
}
#[test]
fn the_committed_draft_vector_is_byte_identical_to_a_fresh_mint() {
let regenerated = regenerate_draft_vector();
let path = draft_vector_path();
let published = std::fs::read_to_string(&path).unwrap_or_default();
if published != regenerated {
std::panic!(
"examples/v0.2/sealed_envelope.json has drifted from the bytes \
this suite mints.\nTo fix in ONE step, overwrite that file with EXACTLY \
the content between the cut lines (the final newline is part of the \
content):\n----8<----\n{}----8<----",
regenerated
);
}
}
#[test]
fn the_committed_draft_vector_opens_for_its_reader_and_verifies_blind() {
use sha2::Digest as _;
let raw_json = std::fs::read_to_string(draft_vector_path()).expect("the vector is on disk");
let envelope: aph_core::NotarizationEnvelope =
aph_core::parse_envelope_json(&raw_json).expect("the vector strict-parses as 0.2");
let sealed = envelope.credential_subject.sealed_payload.as_ref().expect("sealed");
let raw = super::unb64(&sealed.ciphertext, "ciphertext").expect("decodes");
std::assert_eq!(
std::format!("{:x}", sha2::Sha256::digest(&raw)),
envelope.credential_subject.communication.body_sha256,
"the blind hop's body binding holds over the committed bytes"
);
let (receiver_sk, _) = derive_keypair_for_tests(RECEIVER_IKM);
let opened = super::unseal_from_envelope(&envelope, &receiver_sk)
.expect("the named reader opens the committed vector");
std::assert_eq!(opened, b"the body no hop reads: the v0.2-draft sealed vector");
}
const NOTARY_SEED: [u8; 32] = [
0x9d, 0x61, 0xb1, 0x9d, 0xef, 0xfd, 0x5a, 0x60, 0xba, 0x84, 0x4a, 0xf4, 0x92, 0xec, 0x2c,
0xc4, 0x44, 0x49, 0xc5, 0x69, 0x7b, 0x32, 0x69, 0x19, 0x70, 0x3b, 0xac, 0x03, 0x1c, 0xae,
0x7f, 0x60,
];
fn notary_key() -> ed25519_dalek::SigningKey {
ed25519_dalek::SigningKey::from_bytes(&NOTARY_SEED)
}
fn signed_vector_path() -> std::path::PathBuf {
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../../examples/v0.2/sealed_signed_envelope.json")
}
fn regenerate_signed_vector() -> String {
use sha2::Digest as _;
let key = notary_key();
let did = aph_core::crypto::did_key::encode_ed25519(&key.verifying_key());
let fragment = did.trim_start_matches("did:key:").to_string();
let (_, receiver_pk) = derive_keypair_for_tests(RECEIVER_IKM);
let mut envelope = envelope_v02();
envelope.id = std::string::String::from("urn:uuid:00000000-0000-4000-8000-00000000000f");
envelope.issuer = did.clone();
super::seal_into_envelope(
&mut rng(),
&mut envelope,
reader("did:web:receiver.example.com"),
&receiver_pk,
b"the signed sealed vector: carried blind, verified by anyone, read by one",
)
.expect("sealing succeeds");
let raw = {
let sealed = envelope.credential_subject.sealed_payload.as_ref().expect("sealed");
super::unb64(&sealed.ciphertext, "ciphertext").expect("decodes")
};
envelope.credential_subject.communication.body_sha256 =
std::format!("{:x}", sha2::Sha256::digest(&raw));
envelope.proof = aph_core::EnvelopeProofs::Single(aph_core::EnvelopeProof {
r#type: std::string::String::from("DataIntegrityProof"),
cryptosuite: std::option::Option::None, verification_method: std::format!("{did}#{fragment}"),
created: std::string::String::from("2026-08-29T00:00:01Z"),
proof_purpose: std::string::String::from("assertionMethod"),
proof_value: std::string::String::new(),
id: std::option::Option::None,
previous_proof: std::option::Option::None,
});
aph_core::crypto::eddsa_jcs::sign_envelope(&mut envelope, &key).expect("signing succeeds");
std::format!(
"{}\n",
serde_json::to_string_pretty(&envelope).expect("the vector serializes")
)
}
#[test]
fn the_signed_sealed_vector_is_byte_identical_to_a_fresh_mint() {
let regenerated = regenerate_signed_vector();
let path = signed_vector_path();
let published = std::fs::read_to_string(&path).unwrap_or_default();
if published != regenerated {
std::panic!(
"examples/v0.2/sealed_signed_envelope.json has drifted from the \
bytes this suite mints.\nOverwrite it with EXACTLY the content between \
the cut lines (final newline included):\n----8<----\n{}----8<----",
regenerated
);
}
}
#[test]
fn the_signed_sealed_vector_verifies_blind_and_opens_for_its_reader() {
let raw_json =
std::fs::read_to_string(signed_vector_path()).expect("the signed vector is on disk");
let envelope: aph_core::NotarizationEnvelope =
aph_core::parse_envelope_json(&raw_json).expect("strict-parses as 0.2");
aph_core::crypto::eddsa_jcs::verify_envelope_did_key(&envelope)
.expect("the lone did:key proof verifies offline, sealed member covered");
let (receiver_sk, _) = derive_keypair_for_tests(RECEIVER_IKM);
let opened = super::unseal_from_envelope(&envelope, &receiver_sk)
.expect("the named reader opens the committed signed vector");
std::assert_eq!(
opened,
b"the signed sealed vector: carried blind, verified by anyone, read by one"
);
}
fn rotation_vector_path() -> std::path::PathBuf {
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../../examples/v0.2/rotation_attestation.json")
}
fn regenerate_rotation_vector() -> String {
let key = notary_key();
let did = aph_core::crypto::did_key::encode_ed25519(&key.verifying_key());
let successor_multibase = did.trim_start_matches("did:key:").to_string();
let mut attestation = aph_core::RotationAttestation {
aph_version: std::string::String::from("0.2"),
r#type: std::string::String::from(aph_core::ROTATION_ATTESTATION_TYPE),
id: std::string::String::from("urn:uuid:00000000-0000-4000-8000-0000000000a0"),
subject: std::string::String::from("did:web:notary.squillo.com"),
predecessor: std::string::String::from("did:web:notary.squillo.com#k1"),
successor: aph_core::RotationSuccessor {
kid: std::string::String::from("k2"),
alg: std::string::String::from("EdDSA"),
public_key_multibase: successor_multibase,
not_before: std::string::String::from("2027-01-01T00:00:00Z"),
not_after: std::string::String::from("2029-01-01T00:00:00Z"),
},
created: std::string::String::from("2026-08-29T00:00:00Z"),
proof: aph_core::EnvelopeProof {
r#type: std::string::String::from("DataIntegrityProof"),
cryptosuite: std::option::Option::Some(std::string::String::from("eddsa-jcs-2022")),
verification_method: std::string::String::from("did:web:notary.squillo.com#k1"),
created: std::string::String::from("2026-08-29T00:00:00Z"),
proof_purpose: std::string::String::from("assertionMethod"),
proof_value: std::string::String::new(),
id: std::option::Option::None,
previous_proof: std::option::Option::None,
},
};
aph_core::sign_rotation_attestation(&mut attestation, &key).expect("signing succeeds");
std::format!(
"{}\n",
serde_json::to_string_pretty(&attestation).expect("the vector serializes")
)
}
#[test]
fn the_rotation_attestation_vector_is_byte_identical_to_a_fresh_mint() {
let regenerated = regenerate_rotation_vector();
let path = rotation_vector_path();
let published = std::fs::read_to_string(&path).unwrap_or_default();
if published != regenerated {
std::panic!(
"examples/v0.2/rotation_attestation.json has drifted from the \
bytes this suite mints.\nOverwrite it with EXACTLY the content between \
the cut lines (final newline included):\n----8<----\n{}----8<----",
regenerated
);
}
}
#[test]
fn the_committed_rotation_vector_verifies_against_its_predecessor_key() {
let raw = std::fs::read_to_string(rotation_vector_path()).expect("the vector is on disk");
let attestation: aph_core::RotationAttestation =
serde_json::from_str(&raw).expect("strict-parses");
aph_core::verify_rotation_attestation(&attestation, ¬ary_key().verifying_key())
.expect("the predecessor's committed statement verifies");
}
}