use auths_crypto::{CurveType, RingCryptoProvider};
use auths_keri::{
Acdc, CesrKey, Event, IcpEvent, IcpEventInit, Iss, IxnEvent, KeriPublicKey, KeriSequence,
Prefix, Rev, RotEvent, RotEventInit, Said, Seal, TelAnchorSeal, TelEvent, Threshold, Vcp,
VersionString, compute_capability_schema_said, compute_next_commitment, encode_tel_nonce,
finalize_icp_event, finalize_ixn_event, finalize_rot_event,
};
use auths_verifier::{
CredentialVerdict, PresentationBinding, PresentationEnvelope, PresentationVerdict, SignedAcdc,
VerifierWitnessPolicy, verify_presentation, verify_presentation_json, verify_presentation_sync,
};
use base64::Engine as _;
use chrono::{TimeZone, Utc};
use ring::signature::{Ed25519KeyPair, KeyPair};
const DT: &str = "2025-01-01T00:00:00.000000+00:00";
fn provider() -> RingCryptoProvider {
RingCryptoProvider
}
fn now() -> chrono::DateTime<Utc> {
Utc.with_ymd_and_hms(2025, 6, 1, 0, 0, 0).unwrap()
}
fn ed25519_sign(seed: &[u8; 32], message: &[u8]) -> Vec<u8> {
let kp = Ed25519KeyPair::from_seed_unchecked(seed).expect("ed25519 keypair");
kp.sign(message).as_ref().to_vec()
}
fn ed25519_pubkey(seed: &[u8; 32]) -> [u8; 32] {
let kp = Ed25519KeyPair::from_seed_unchecked(seed).expect("ed25519 keypair");
kp.public_key().as_ref().try_into().expect("32-byte pubkey")
}
fn p256_sign(seed: &[u8; 32], message: &[u8]) -> Vec<u8> {
use p256::ecdsa::{Signature, SigningKey, signature::Signer as _};
let sk = SigningKey::from_slice(seed).expect("p256 key");
let sig: Signature = sk.sign(message);
sig.to_bytes().to_vec()
}
fn p256_pubkey(seed: &[u8; 32]) -> Vec<u8> {
use p256::ecdsa::{SigningKey, VerifyingKey};
let sk = SigningKey::from_slice(seed).expect("p256 key");
let vk = VerifyingKey::from(&sk);
vk.to_encoded_point(true).as_bytes().to_vec()
}
struct Signer {
curve: CurveType,
seed: [u8; 32],
verkey: KeriPublicKey,
}
impl Signer {
fn new(curve: CurveType, seed_byte: u8) -> Self {
let seed = [seed_byte; 32];
let verkey = match curve {
CurveType::Ed25519 => KeriPublicKey::ed25519(&ed25519_pubkey(&seed)).expect("ed25519"),
CurveType::P256 => {
KeriPublicKey::from_verkey_bytes(&p256_pubkey(&seed), CurveType::P256)
.expect("p256")
}
};
Self {
curve,
seed,
verkey,
}
}
fn cesr(&self) -> CesrKey {
CesrKey::new_unchecked(self.verkey.to_qb64().expect("qb64"))
}
fn sign(&self, message: &[u8]) -> Vec<u8> {
match self.curve {
CurveType::Ed25519 => ed25519_sign(&self.seed, message),
CurveType::P256 => p256_sign(&self.seed, message),
}
}
}
fn dummy_key(seed: u8) -> KeriPublicKey {
KeriPublicKey::ed25519(&[seed; 32]).expect("ed25519")
}
fn schema_said() -> Said {
compute_capability_schema_said().expect("schema said")
}
fn presentation_message(credential_said: &str, audience: &str, nonce: &[u8]) -> Vec<u8> {
let mut message = Vec::with_capacity(credential_said.len() + audience.len() + nonce.len() + 2);
message.extend_from_slice(credential_said.as_bytes());
message.push(0);
message.extend_from_slice(audience.as_bytes());
message.push(0);
message.extend_from_slice(nonce);
message
}
struct Subject {
aid: Prefix,
signer: Signer,
next_signer: Signer,
kel: Vec<Event>,
}
impl Subject {
fn incept(curve: CurveType, seed_byte: u8) -> Self {
let signer = Signer::new(curve, seed_byte);
let next_signer = Signer::new(curve, seed_byte.wrapping_add(100));
let icp = finalize_icp_event(IcpEvent::new(IcpEventInit {
v: VersionString::placeholder(),
d: Said::default(),
i: Prefix::default(),
s: KeriSequence::new(0),
kt: Threshold::Simple(1),
k: vec![signer.cesr()],
nt: Threshold::Simple(1),
n: vec![compute_next_commitment(&next_signer.verkey)],
bt: Threshold::Simple(0),
b: vec![],
c: vec![],
a: vec![],
}))
.expect("subject icp");
let aid = icp.i.clone();
Self {
aid,
signer,
next_signer,
kel: vec![Event::Icp(icp)],
}
}
fn rotate(&mut self) {
let tip = self.kel.last().expect("tip");
let new_next = Signer::new(self.next_signer.curve, 251);
let rot = finalize_rot_event(RotEvent::new(RotEventInit {
v: VersionString::placeholder(),
d: Said::default(),
i: self.aid.clone(),
s: KeriSequence::new(tip.sequence().value() + 1),
p: tip.said().clone(),
kt: Threshold::Simple(1),
k: vec![self.next_signer.cesr()],
nt: Threshold::Simple(1),
n: vec![compute_next_commitment(&new_next.verkey)],
bt: Threshold::Simple(0),
br: vec![],
ba: vec![],
c: vec![],
a: vec![],
}))
.expect("subject rot");
self.kel.push(Event::Rot(rot));
}
fn present(
&self,
credential_said: &str,
audience: &str,
binding: PresentationBinding,
) -> PresentationEnvelope {
let nonce = match &binding {
PresentationBinding::Challenge { nonce } => nonce.clone(),
PresentationBinding::Ttl { nonce, .. } => nonce.clone(),
};
let message = presentation_message(credential_said, audience, &nonce);
let signature = self.signer.sign(&message);
PresentationEnvelope {
credential_said: credential_said.to_string(),
audience: audience.to_string(),
binding,
signature,
}
}
}
struct Credentialed {
issuer_kel: Vec<Event>,
tel: Vec<TelEvent>,
signed: SignedAcdc,
credential_said: String,
}
fn credential_for(subject_aid: &Prefix, revoke: bool) -> Credentialed {
let issuer = Signer::new(CurveType::Ed25519, 1);
let icp = finalize_icp_event(IcpEvent::new(IcpEventInit {
v: VersionString::placeholder(),
d: Said::default(),
i: Prefix::default(),
s: KeriSequence::new(0),
kt: Threshold::Simple(1),
k: vec![issuer.cesr()],
nt: Threshold::Simple(1),
n: vec![compute_next_commitment(&dummy_key(2))],
bt: Threshold::Simple(0),
b: vec![],
c: vec![],
a: vec![],
}))
.expect("issuer icp");
let issuer_aid = icp.i.clone();
let nonce = encode_tel_nonce(&[7u8; 16]).expect("nonce");
let vcp = Vcp::new(issuer_aid.clone(), nonce).saidify().expect("vcp");
let registry = vcp.registry().clone();
let mut data = serde_json::Map::new();
data.insert(
"capability".to_string(),
serde_json::Value::String("sign".to_string()),
);
let acdc = Acdc::new(
issuer_aid.clone(),
registry.clone(),
schema_said(),
subject_aid.clone(),
DT.to_string(),
data,
)
.saidify()
.expect("acdc");
let credential_said = acdc.d.as_str().to_string();
let wire = acdc.to_wire_bytes().expect("wire");
let signature = issuer.sign(&wire);
let signed = SignedAcdc {
acdc: acdc.clone(),
signature,
};
let iss = Iss::new(acdc.d.clone(), registry.clone(), DT.to_string())
.saidify()
.expect("iss");
let mut kel = vec![Event::Icp(icp.clone())];
let mut last = icp.d.clone();
let vcp_ixn = anchor_ixn(&issuer_aid, 1, &last, ®istry, vcp.s, &vcp.d);
last = vcp_ixn.d.clone();
kel.push(Event::Ixn(vcp_ixn));
let iss_ixn = anchor_ixn(&issuer_aid, 2, &last, ®istry, iss.s, &iss.d);
last = iss_ixn.d.clone();
kel.push(Event::Ixn(iss_ixn));
let mut tel = vec![TelEvent::Vcp(vcp), TelEvent::Iss(iss.clone())];
if revoke {
let rev = Rev::new(
acdc.d.clone(),
registry.clone(),
iss.d.clone(),
DT.to_string(),
)
.saidify()
.expect("rev");
let rev_ixn = anchor_ixn(&issuer_aid, 3, &last, ®istry, rev.s, &rev.d);
kel.push(Event::Ixn(rev_ixn));
tel.push(TelEvent::Rev(rev));
}
Credentialed {
issuer_kel: kel,
tel,
signed,
credential_said,
}
}
fn anchor_ixn(
issuer_aid: &Prefix,
seq: u128,
prior: &Said,
registry: &Said,
tel_seq: KeriSequence,
tel_said: &Said,
) -> IxnEvent {
let seal = TelAnchorSeal::for_event(
Prefix::new_unchecked(registry.as_str().to_string()),
tel_seq,
tel_said.clone(),
);
finalize_ixn_event(IxnEvent {
v: VersionString::placeholder(),
d: Said::default(),
i: issuer_aid.clone(),
s: KeriSequence::new(seq),
p: prior.clone(),
a: vec![Seal::KeyEvent {
i: seal.i,
s: seal.s,
d: seal.d,
}],
})
.expect("anchor ixn")
}
#[allow(clippy::too_many_arguments)]
async fn verify(
envelope: &PresentationEnvelope,
cred: &Credentialed,
subject_kel: &[Event],
expected_audience: &str,
expected_challenge: Option<&[u8]>,
) -> PresentationVerdict {
verify_presentation(
envelope,
&cred.signed,
&cred.issuer_kel,
&cred.tel,
&[],
VerifierWitnessPolicy::Warn,
subject_kel,
&[],
expected_audience,
expected_challenge,
now(),
&provider(),
)
.await
}
const AUDIENCE: &str = "audience.example";
#[tokio::test]
async fn bearer_acdc_without_holder_proof_is_not_honored() {
let subject = Subject::incept(CurveType::Ed25519, 30);
let cred = credential_for(&subject.aid, false);
let bearer = PresentationEnvelope {
credential_said: cred.credential_said.clone(),
audience: AUDIENCE.to_string(),
binding: PresentationBinding::Challenge {
nonce: vec![9u8; 32],
},
signature: vec![0u8; 64], };
let verdict = verify(&bearer, &cred, &subject.kel, AUDIENCE, Some(&[9u8; 32])).await;
assert_eq!(
verdict,
PresentationVerdict::HolderNotCurrentKey,
"a possessed-but-unbound ACDC must not be honored"
);
}
#[tokio::test]
async fn valid_challenge_presentation_verifies() {
for curve in [CurveType::P256, CurveType::Ed25519] {
let subject = Subject::incept(curve, 31);
let cred = credential_for(&subject.aid, false);
let nonce = vec![5u8; 32];
let envelope = subject.present(
&cred.credential_said,
AUDIENCE,
PresentationBinding::Challenge {
nonce: nonce.clone(),
},
);
let verdict = verify(&envelope, &cred, &subject.kel, AUDIENCE, Some(&nonce)).await;
match verdict {
PresentationVerdict::Valid {
issuer,
subject: s,
caps,
role,
expires_at,
} => {
assert_eq!(s.as_str(), format!("did:keri:{}", subject.aid));
assert_eq!(
caps.iter().map(|c| c.as_str()).collect::<Vec<_>>(),
["sign"]
);
assert!(
issuer.as_str().starts_with("did:keri:"),
"issuer carried on Valid, got {}",
issuer.as_str()
);
assert_eq!(role, None);
assert_eq!(expires_at, None);
}
other => panic!("expected Valid on {curve:?}, got {other:?}"),
}
}
}
#[tokio::test]
async fn consumed_or_mismatched_challenge_rejected() {
let subject = Subject::incept(CurveType::Ed25519, 32);
let cred = credential_for(&subject.aid, false);
let issued_nonce = vec![5u8; 32];
let envelope = subject.present(
&cred.credential_said,
AUDIENCE,
PresentationBinding::Challenge {
nonce: issued_nonce.clone(),
},
);
let mismatch = verify(&envelope, &cred, &subject.kel, AUDIENCE, Some(&[6u8; 32])).await;
assert_eq!(
mismatch,
PresentationVerdict::NonceMismatchOrConsumed,
"a mismatched challenge nonce is rejected"
);
let consumed = verify(&envelope, &cred, &subject.kel, AUDIENCE, None).await;
assert_eq!(
consumed,
PresentationVerdict::NonceMismatchOrConsumed,
"a consumed (no-longer-offered) challenge is rejected"
);
}
#[tokio::test]
async fn presentation_by_noncurrent_key_rejected() {
let mut subject = Subject::incept(CurveType::Ed25519, 33);
let cred = credential_for(&subject.aid, false);
let nonce = vec![5u8; 32];
let envelope = subject.present(
&cred.credential_said,
AUDIENCE,
PresentationBinding::Challenge {
nonce: nonce.clone(),
},
);
subject.rotate();
let verdict = verify(&envelope, &cred, &subject.kel, AUDIENCE, Some(&nonce)).await;
assert_eq!(
verdict,
PresentationVerdict::HolderNotCurrentKey,
"a presentation signed by a rotated-away key is not current control"
);
}
#[tokio::test]
async fn wrong_audience_rejected() {
let subject = Subject::incept(CurveType::Ed25519, 34);
let cred = credential_for(&subject.aid, false);
let nonce = vec![5u8; 32];
let envelope = subject.present(
&cred.credential_said,
"other.example",
PresentationBinding::Challenge {
nonce: nonce.clone(),
},
);
let verdict = verify(&envelope, &cred, &subject.kel, AUDIENCE, Some(&nonce)).await;
assert_eq!(verdict, PresentationVerdict::WrongAudience);
}
#[tokio::test]
async fn non_interactive_expired_ttl_rejected() {
let subject = Subject::incept(CurveType::P256, 35);
let cred = credential_for(&subject.aid, false);
let not_after = Utc.with_ymd_and_hms(2025, 3, 1, 0, 0, 0).unwrap();
let envelope = subject.present(
&cred.credential_said,
AUDIENCE,
PresentationBinding::Ttl {
nonce: vec![1u8; 32],
not_after,
},
);
let verdict = verify(&envelope, &cred, &subject.kel, AUDIENCE, None).await;
assert_eq!(verdict, PresentationVerdict::Expired);
}
#[tokio::test]
async fn non_interactive_ttl_within_window_verifies() {
let subject = Subject::incept(CurveType::Ed25519, 36);
let cred = credential_for(&subject.aid, false);
let not_after = Utc.with_ymd_and_hms(2025, 12, 31, 0, 0, 0).unwrap();
let envelope = subject.present(
&cred.credential_said,
AUDIENCE,
PresentationBinding::Ttl {
nonce: vec![1u8; 32],
not_after,
},
);
let verdict = verify(&envelope, &cred, &subject.kel, AUDIENCE, None).await;
assert!(
verdict.is_honored(),
"an in-window TTL presentation by the current key is honored, got {verdict:?}"
);
}
#[tokio::test]
async fn presentation_of_revoked_credential_rejected() {
let subject = Subject::incept(CurveType::Ed25519, 37);
let cred = credential_for(&subject.aid, true);
let nonce = vec![5u8; 32];
let envelope = subject.present(
&cred.credential_said,
AUDIENCE,
PresentationBinding::Challenge {
nonce: nonce.clone(),
},
);
let verdict = verify(&envelope, &cred, &subject.kel, AUDIENCE, Some(&nonce)).await;
match verdict {
PresentationVerdict::CredentialNotValid(CredentialVerdict::CredentialRevoked {
..
}) => {}
other => panic!("expected CredentialNotValid(CredentialRevoked), got {other:?}"),
}
}
#[tokio::test]
async fn invalid_subject_kel_rejected() {
let subject = Subject::incept(CurveType::Ed25519, 38);
let cred = credential_for(&subject.aid, false);
let nonce = vec![5u8; 32];
let envelope = subject.present(
&cred.credential_said,
AUDIENCE,
PresentationBinding::Challenge {
nonce: nonce.clone(),
},
);
let verdict = verify(&envelope, &cred, &[], AUDIENCE, Some(&nonce)).await;
assert_eq!(verdict, PresentationVerdict::SubjectKelInvalid);
}
#[tokio::test]
async fn sync_and_async_presentation_verdicts_match() {
for curve in [CurveType::P256, CurveType::Ed25519] {
let subject = Subject::incept(curve, 40);
let cred = credential_for(&subject.aid, false);
let nonce = vec![5u8; 32];
let honored = subject.present(
&cred.credential_said,
AUDIENCE,
PresentationBinding::Challenge {
nonce: nonce.clone(),
},
);
let wrong_audience = subject.present(
&cred.credential_said,
"other.example",
PresentationBinding::Challenge {
nonce: nonce.clone(),
},
);
let bearer = PresentationEnvelope {
credential_said: cred.credential_said.clone(),
audience: AUDIENCE.to_string(),
binding: PresentationBinding::Challenge {
nonce: nonce.clone(),
},
signature: vec![0u8; 64],
};
let cases = [
(&honored, AUDIENCE, Some(nonce.as_slice())),
(&wrong_audience, AUDIENCE, Some(nonce.as_slice())),
(&bearer, AUDIENCE, Some(nonce.as_slice())),
];
for (envelope, audience, challenge) in cases {
let async_verdict = verify_presentation(
envelope,
&cred.signed,
&cred.issuer_kel,
&cred.tel,
&[],
VerifierWitnessPolicy::Warn,
&subject.kel,
&[],
audience,
challenge,
now(),
&provider(),
)
.await;
let sync_verdict = verify_presentation_sync(
envelope,
&cred.signed,
&cred.issuer_kel,
&cred.tel,
&[],
VerifierWitnessPolicy::Warn,
&subject.kel,
&[],
audience,
challenge,
now(),
);
assert_eq!(
async_verdict, sync_verdict,
"sync/async verdict parity must hold on {curve:?}"
);
}
}
}
fn presentation_request_json(
envelope: &PresentationEnvelope,
cred: &Credentialed,
subject_kel: &[Event],
audience: &str,
expected_challenge: Option<&[u8]>,
) -> String {
let b64 = base64::engine::general_purpose::STANDARD;
let binding = match &envelope.binding {
PresentationBinding::Challenge { nonce } => serde_json::json!({
"mode": "challenge",
"nonceB64": b64.encode(nonce),
}),
PresentationBinding::Ttl { nonce, not_after } => serde_json::json!({
"mode": "ttl",
"nonceB64": b64.encode(nonce),
"notAfter": not_after.to_rfc3339(),
}),
};
serde_json::json!({
"schemaVersion": 1,
"envelope": {
"credentialSaid": envelope.credential_said,
"audience": envelope.audience,
"binding": binding,
"signatureB64": b64.encode(&envelope.signature),
},
"credential": {
"acdc": cred.signed.acdc,
"signatureB64": b64.encode(&cred.signed.signature),
},
"issuerKel": cred.issuer_kel,
"subjectKel": subject_kel,
"tel": cred.tel,
"receipts": [],
"witnessPolicy": "warn",
"audience": audience,
"expectedChallengeB64": expected_challenge.map(|c| b64.encode(c)),
"now": now().to_rfc3339(),
})
.to_string()
}
#[test]
fn presentation_json_contract_matches_sync() {
for curve in [CurveType::P256, CurveType::Ed25519] {
let subject = Subject::incept(curve, 42);
let cred = credential_for(&subject.aid, false);
let nonce = vec![5u8; 32];
let honored = subject.present(
&cred.credential_said,
AUDIENCE,
PresentationBinding::Challenge {
nonce: nonce.clone(),
},
);
let wrong_audience = subject.present(
&cred.credential_said,
"other.example",
PresentationBinding::Challenge {
nonce: nonce.clone(),
},
);
let bearer = PresentationEnvelope {
credential_said: cred.credential_said.clone(),
audience: AUDIENCE.to_string(),
binding: PresentationBinding::Challenge {
nonce: nonce.clone(),
},
signature: vec![0u8; 64],
};
for (envelope, expected_kind) in [
(&honored, "valid"),
(&wrong_audience, "wrongAudience"),
(&bearer, "holderNotCurrentKey"),
] {
let request =
presentation_request_json(envelope, &cred, &subject.kel, AUDIENCE, Some(&nonce));
let verdict: serde_json::Value =
serde_json::from_str(&verify_presentation_json(&request)).expect("verdict json");
assert_eq!(verdict["schemaVersion"], 1);
assert_eq!(
verdict["kind"], expected_kind,
"JSON verdict kind must match the sync outcome on {curve:?}"
);
if expected_kind == "valid" {
assert_eq!(
verdict["subject"],
format!("did:keri:{}", subject.aid),
"valid verdict carries the holder subject DID"
);
assert_eq!(verdict["caps"], serde_json::json!(["sign"]));
}
}
}
}
#[test]
fn emit_presentation_fixture() {
if std::env::var("AUTHS_EMIT_FIXTURES").is_err() {
return;
}
let subject = Subject::incept(CurveType::P256, 50);
let cred = credential_for(&subject.aid, false);
let nonce = vec![5u8; 32];
let envelope = subject.present(
&cred.credential_said,
AUDIENCE,
PresentationBinding::Challenge {
nonce: nonce.clone(),
},
);
let request = presentation_request_json(&envelope, &cred, &subject.kel, AUDIENCE, Some(&nonce));
let path = concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/presentation_valid.json"
);
std::fs::write(path, request).unwrap();
}
#[test]
fn presentation_json_typed_errors() {
let malformed: serde_json::Value =
serde_json::from_str(&verify_presentation_json("{not json")).expect("verdict json");
assert_eq!(malformed["kind"], "malformedRequest");
let huge = " ".repeat(1024 * 1024 + 1);
let too_large: serde_json::Value =
serde_json::from_str(&verify_presentation_json(&huge)).expect("verdict json");
assert_eq!(too_large["kind"], "inputTooLarge");
assert_eq!(too_large["field"], "request");
let bad_version = serde_json::json!({
"schemaVersion": 999,
"envelope": {"credentialSaid":"E","audience":"a","binding":{"mode":"challenge","nonceB64":""},"signatureB64":""},
"credential": {"acdc": {}, "signatureB64": ""},
"issuerKel": [], "subjectKel": [], "tel": [], "receipts": [],
"witnessPolicy": "warn", "audience": "a", "now": "2025-06-01T00:00:00+00:00"
})
.to_string();
let unsupported: serde_json::Value =
serde_json::from_str(&verify_presentation_json(&bad_version)).expect("verdict json");
assert_eq!(unsupported["kind"], "unsupportedSchemaVersion");
assert_eq!(unsupported["got"], 999);
}