use std::ops::ControlFlow;
use std::path::Path;
use std::sync::Arc;
use auths_core::PrefilledPassphraseProvider;
use auths_core::signing::{PassphraseProvider, StorageSigner};
use auths_core::storage::keychain::KeyAlias;
use auths_core::testing::IsolatedKeychainHandle;
use auths_crypto::CurveType;
use auths_id::keri::credential_registry::find_registry;
use auths_id::keri::types::Prefix;
use auths_id::keri::{Event, Seal};
use auths_id::storage::receipts::{GitReceiptStorage, ReceiptStorage};
use auths_id::storage::registry::backend::RegistryBackend;
use auths_keri::witness::{Receipt, ReceiptTag, SignedReceipt, StoredReceipt};
use auths_keri::{
KeriPublicKey, KeriSequence, Said, TelEvent, Threshold, VersionString,
compute_capability_schema_said,
};
use auths_sdk::context::AuthsContext;
use auths_sdk::domains::credentials::{
CredentialError, CredentialVerdict, StoredCredential, VerifierWitnessPolicy, issue, list,
revoke, verify,
};
use auths_sdk::domains::device::add_device;
use auths_sdk::domains::identity::service::initialize;
use auths_sdk::domains::identity::types::{
CreateDeveloperIdentityConfig, IdentityConfig, InitializeResult,
};
use auths_sdk::domains::signing::types::GitSigningScope;
use ring::signature::{Ed25519KeyPair, KeyPair};
use crate::cases::helpers::build_test_context_with_provider;
const PASS: &str = "Test-passphrase1!";
fn setup_test_identity(registry_path: &Path) -> (KeyAlias, IsolatedKeychainHandle) {
let keychain = IsolatedKeychainHandle::new();
let signer = StorageSigner::new(keychain.clone());
let provider = PrefilledPassphraseProvider::new(PASS);
let config = CreateDeveloperIdentityConfig::builder(KeyAlias::new_unchecked("issuer-key"))
.with_git_signing_scope(GitSigningScope::Skip)
.build();
let ctx = build_test_context_with_provider(registry_path, Arc::new(keychain.clone()), None);
let result = match initialize(
IdentityConfig::Developer(config),
&ctx,
Arc::new(keychain.clone()),
&signer,
&provider,
None,
)
.unwrap()
{
InitializeResult::Developer(r) => r,
_ => unreachable!(),
};
(result.key_alias, keychain)
}
struct Harness {
ctx: AuthsContext,
issuer_alias: KeyAlias,
issuer_prefix: Prefix,
_tmp: tempfile::TempDir,
}
fn setup() -> Harness {
let tmp = tempfile::tempdir().unwrap();
let (issuer_alias, keychain) = setup_test_identity(tmp.path());
let provider: Arc<dyn PassphraseProvider + Send + Sync> =
Arc::new(PrefilledPassphraseProvider::new(PASS));
let ctx = build_test_context_with_provider(tmp.path(), Arc::new(keychain), Some(provider));
let managed = ctx
.identity_storage
.load_identity()
.expect("issuer identity");
let issuer_prefix = Prefix::new_unchecked(
managed
.controller_did
.as_str()
.strip_prefix("did:keri:")
.unwrap()
.to_string(),
);
Harness {
ctx,
issuer_alias,
issuer_prefix,
_tmp: tmp,
}
}
fn make_issuee(h: &Harness, label: &str) -> String {
add_device(
&h.ctx,
&h.issuer_alias,
&KeyAlias::new_unchecked(label),
CurveType::Ed25519,
)
.expect("delegate issuee device")
.device_did
}
fn collect_kel(backend: &(dyn RegistryBackend + Send + Sync), prefix: &Prefix) -> Vec<Event> {
let mut events = Vec::new();
backend
.visit_events(prefix, 0, &mut |e| {
events.push(e.clone());
ControlFlow::Continue(())
})
.expect("walk KEL");
events
}
fn load_stored(h: &Harness, credential_said: &str) -> StoredCredential {
let blob = h
.ctx
.registry
.load_credential(
&h.issuer_prefix,
&Said::new_unchecked(credential_said.to_string()),
)
.expect("load credential")
.expect("credential blob present");
StoredCredential::from_bytes(&blob).expect("parse stored credential")
}
#[test]
fn issue_creates_anchored_acdc() {
let h = setup();
let issuee = make_issuee(&h, "deploy-bot");
let issued = issue(
&h.ctx,
&h.issuer_alias,
&issuee,
&["sign".to_string()],
Some("deployer"),
None,
)
.expect("issue credential");
assert!(issued.credential_said.starts_with('E'));
assert_eq!(issued.issuee_did, issuee);
let cred = issued.credential_said.clone();
let kel = collect_kel(h.ctx.registry.as_ref(), &h.issuer_prefix);
let anchored = kel.iter().any(|e| {
e.is_interaction()
&& e.anchors().iter().any(|s| {
matches!(
s,
Seal::KeyEvent { i, s: sn, .. }
if i.as_str() == cred && sn.value() == 0
)
})
});
assert!(
anchored,
"issuer KEL must anchor the iss for the credential"
);
let stored = load_stored(&h, &issued.credential_said);
assert!(stored.acdc.verify_said().is_ok());
assert_eq!(stored.acdc.d.as_str(), issued.credential_said);
}
#[test]
fn issue_to_nonexistent_issuee_rejected() {
let h = setup();
let phantom = "did:keri:EPhantomIssueeAID00000000000000000000000000";
let err = issue(
&h.ctx,
&h.issuer_alias,
phantom,
&["sign".to_string()],
None,
None,
)
.expect_err("issuing to an issuee with no KEL must hard-fail");
assert!(
matches!(err, CredentialError::IssueeNotFound { .. }),
"expected IssueeNotFound, got {err:?}"
);
}
#[test]
fn revoke_marks_credential_revoked_in_tel() {
let h = setup();
let issuee = make_issuee(&h, "rev-bot");
let issued = issue(
&h.ctx,
&h.issuer_alias,
&issuee,
&["sign".to_string()],
None,
None,
)
.expect("issue");
revoke(&h.ctx, &h.issuer_alias, &issued.credential_said).expect("revoke");
let live = list(&h.ctx, &h.issuer_alias).expect("list");
let entry = live
.iter()
.find(|c| c.credential_said == issued.credential_said)
.expect("credential still present in the full set");
assert!(entry.revoked, "revoked credential must be flagged revoked");
}
#[test]
fn revoke_already_revoked_idempotent() {
let h = setup();
let issuee = make_issuee(&h, "idem-bot");
let issued = issue(
&h.ctx,
&h.issuer_alias,
&issuee,
&["sign".to_string()],
None,
None,
)
.expect("issue");
revoke(&h.ctx, &h.issuer_alias, &issued.credential_said).expect("first revoke");
revoke(&h.ctx, &h.issuer_alias, &issued.credential_said)
.expect("re-revoking is idempotent (Ok)");
let registry = find_registry(h.ctx.registry.as_ref(), &h.issuer_prefix)
.unwrap()
.expect("registry exists");
let cred = Said::new_unchecked(issued.credential_said.clone());
let tel = auths_id::keri::credential_registry::read_credential_tel(
h.ctx.registry.as_ref(),
&h.issuer_prefix,
®istry,
&cred,
)
.unwrap();
let rev_count = tel
.iter()
.filter(|e| matches!(e, TelEvent::Rev(rev) if rev.i == cred))
.count();
assert_eq!(
rev_count, 1,
"idempotent revoke must not author a second rev"
);
}
#[test]
fn credential_list_shows_live() {
let h = setup();
let issuee_a = make_issuee(&h, "issuee-a");
let issuee_b = make_issuee(&h, "issuee-b");
let a = issue(
&h.ctx,
&h.issuer_alias,
&issuee_a,
&["sign".to_string()],
None,
None,
)
.expect("issue a");
let b = issue(
&h.ctx,
&h.issuer_alias,
&issuee_b,
&["read".to_string()],
None,
None,
)
.expect("issue b");
let live: Vec<_> = list(&h.ctx, &h.issuer_alias)
.expect("list")
.into_iter()
.filter(|c| !c.revoked)
.collect();
assert_eq!(live.len(), 2, "two live credentials");
revoke(&h.ctx, &h.issuer_alias, &a.credential_said).expect("revoke a");
let live: Vec<_> = list(&h.ctx, &h.issuer_alias)
.expect("list")
.into_iter()
.filter(|c| !c.revoked)
.collect();
assert_eq!(live.len(), 1, "one live credential after revoke");
assert_eq!(live[0].credential_said, b.credential_said);
}
#[tokio::test]
async fn verify_collects_lifecycle_anchor_receipts() {
let h = setup();
let issuee = make_issuee(&h, "verify-bot");
let issued = issue(
&h.ctx,
&h.issuer_alias,
&issuee,
&["sign".to_string()],
None,
None,
)
.expect("issue");
let stored = load_stored(&h, &issued.credential_said);
let now = chrono::Utc::now();
let verdict = verify(&h.ctx, &stored, VerifierWitnessPolicy::Warn, now)
.await
.expect("verify");
assert!(
verdict.is_valid(),
"freshly issued credential must verify, got {verdict:?}"
);
match verdict {
CredentialVerdict::Resolved { as_of, .. } => {
let kel = collect_kel(h.ctx.registry.as_ref(), &h.issuer_prefix);
assert_eq!(as_of.seq, kel.last().unwrap().sequence().value());
}
other => panic!("expected Resolved, got {other:?}"),
}
}
#[tokio::test]
async fn verify_stale_tip_is_unresolvable() {
let h = setup();
let issuee = make_issuee(&h, "stale-bot");
let issued = issue(
&h.ctx,
&h.issuer_alias,
&issuee,
&["sign".to_string()],
None,
None,
)
.expect("issue");
let mut stored = load_stored(&h, &issued.credential_said);
let witness = witness_aid(99);
let mut state = h.ctx.registry.get_key_state(&h.issuer_prefix).unwrap();
state.backers = vec![witness];
state.backer_threshold = Threshold::Simple(1);
h.ctx
.registry
.write_key_state(&h.issuer_prefix, &state)
.unwrap();
let _ = &mut stored;
let now = chrono::Utc::now();
let verdict = verify(
&h.ctx,
&stored,
VerifierWitnessPolicy::RequireWitnesses,
now,
)
.await
.expect("verify");
assert!(
matches!(verdict, CredentialVerdict::StaleOrUnresolvable { .. }),
"no reachable witnessed tip must fail closed, got {verdict:?}"
);
}
#[tokio::test]
async fn verify_require_witnesses_fails_closed_on_under_quorum_iss() {
let tmp = tempfile::tempdir().unwrap();
let ctx =
build_test_context_with_provider(tmp.path(), Arc::new(IsolatedKeychainHandle::new()), None);
ctx.registry.init_if_needed().expect("init registry");
let w1 = TestWitness::new(40);
let w2 = TestWitness::new(41);
let fixture = WitnessedFixture::build(&[w1.aid.clone(), w2.aid.clone()], 2);
fixture.persist(&ctx);
let storage = GitReceiptStorage::new(tmp.path());
store_receipts(
&storage,
&fixture.issuer_prefix,
&fixture.registry,
&[
w1.receipt(&fixture.issuer_prefix, &fixture.registry, 0),
w2.receipt(&fixture.issuer_prefix, &fixture.registry, 0),
],
);
store_receipts(
&storage,
&fixture.issuer_prefix,
&fixture.iss_said,
&[w1.receipt(&fixture.issuer_prefix, &fixture.iss_said, 0)],
);
let now = chrono::Utc::now();
let verdict = verify(
&ctx,
&fixture.stored,
VerifierWitnessPolicy::RequireWitnesses,
now,
)
.await
.expect("verify");
assert!(
!verdict.is_valid(),
"under-quorum iss must not be Valid under RequireWitnesses, got {verdict:?}"
);
match verdict {
CredentialVerdict::Resolved { verdict, .. } => assert!(
matches!(
verdict,
auths_verifier::CredentialVerdict::WitnessQuorumNotMet {
event: auths_verifier::LifecycleEvent::Iss,
..
}
),
"expected WitnessQuorumNotMet(iss), got {verdict:?}"
),
other => panic!("expected a Resolved WitnessQuorumNotMet, got {other:?}"),
}
}
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 witness_aid(seed_byte: u8) -> Prefix {
let verkey = KeriPublicKey::ed25519(&ed25519_pubkey(&[seed_byte; 32])).expect("verkey");
Prefix::new_unchecked(verkey.to_qb64().expect("qb64"))
}
struct TestWitness {
seed: [u8; 32],
aid: Prefix,
}
impl TestWitness {
fn new(seed_byte: u8) -> Self {
let seed = [seed_byte; 32];
Self {
aid: witness_aid(seed_byte),
seed,
}
}
fn receipt(&self, controller: &Prefix, said: &Said, seq: u128) -> StoredReceipt {
let receipt = Receipt {
v: VersionString::placeholder(),
t: ReceiptTag,
d: said.clone(),
i: controller.clone(),
s: KeriSequence::new(seq),
};
let payload = serde_json::to_vec(&receipt).expect("receipt json");
let kp = Ed25519KeyPair::from_seed_unchecked(&self.seed).expect("kp");
let signature = kp.sign(&payload).as_ref().to_vec();
StoredReceipt {
signed: SignedReceipt { receipt, signature },
witness: self.aid.clone(),
}
}
}
fn store_receipts(
storage: &GitReceiptStorage,
issuer: &Prefix,
event_said: &Said,
receipts: &[StoredReceipt],
) {
use auths_id::keri::EventReceipts;
let event_receipts = EventReceipts::new(event_said.as_str(), receipts.to_vec());
storage
.store_receipts(issuer, &event_receipts, chrono::Utc::now())
.expect("store receipts");
}
struct WitnessedFixture {
issuer_prefix: Prefix,
issuer_kel: Vec<Event>,
vcp: TelEvent,
iss: TelEvent,
registry: Said,
iss_said: Said,
credential_said: Said,
stored: StoredCredential,
}
impl WitnessedFixture {
fn build(witnesses: &[Prefix], threshold: u64) -> Self {
use auths_keri::{
CesrKey, IcpEvent, IcpEventInit, Iss, IxnEvent, Seal as KeriSeal, TelAnchorSeal, Vcp,
compute_next_commitment, encode_tel_nonce, finalize_icp_event, finalize_ixn_event,
};
let issuer_seed = [1u8; 32];
let issuer_verkey =
KeriPublicKey::ed25519(&ed25519_pubkey(&issuer_seed)).expect("issuer verkey");
let issuer_cesr = CesrKey::new_unchecked(issuer_verkey.to_qb64().expect("qb64"));
let next_key = KeriPublicKey::ed25519(&[2u8; 32]).expect("next key");
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(&next_key)],
bt: Threshold::Simple(threshold),
b: witnesses.to_vec(),
c: vec![],
a: vec![],
}))
.expect("issuer icp");
let issuer_prefix = icp.i.clone();
let nonce = encode_tel_nonce(&[7u8; 16]).expect("nonce");
let vcp = Vcp::new(issuer_prefix.clone(), nonce)
.saidify()
.expect("vcp");
let registry = vcp.registry().clone();
let schema = compute_capability_schema_said().expect("schema");
let mut data = serde_json::Map::new();
data.insert(
"capability".to_string(),
serde_json::Value::String("sign".to_string()),
);
let acdc = auths_keri::Acdc::new(
issuer_prefix.clone(),
registry.clone(),
schema,
Prefix::new_unchecked("EHolder000000000000000000000000000000000000".to_string()),
"2025-01-01T00:00:00.000000+00:00".to_string(),
data,
)
.saidify()
.expect("acdc");
let credential_said = acdc.d.clone();
let wire = acdc.to_wire_bytes().expect("wire");
let kp = Ed25519KeyPair::from_seed_unchecked(&issuer_seed).expect("kp");
let signature = kp.sign(&wire).as_ref().to_vec();
let iss = Iss::new(
credential_said.clone(),
registry.clone(),
"2025-01-01T00:00:00.000000+00:00".to_string(),
)
.saidify()
.expect("iss");
let iss_said = iss.d.clone();
let anchor =
|seq: u128, prior: &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_prefix.clone(),
s: KeriSequence::new(seq),
p: prior.clone(),
a: vec![KeriSeal::KeyEvent {
i: seal.i,
s: seal.s,
d: seal.d,
}],
})
.expect("anchor ixn")
};
let vcp_ixn = anchor(1, &icp.d, vcp.s, &vcp.d);
let iss_ixn = anchor(2, &vcp_ixn.d, iss.s, &iss.d);
let issuer_kel = vec![Event::Icp(icp), Event::Ixn(vcp_ixn), Event::Ixn(iss_ixn)];
WitnessedFixture {
issuer_prefix,
issuer_kel,
vcp: TelEvent::Vcp(vcp),
iss: TelEvent::Iss(iss),
registry,
iss_said,
credential_said,
stored: StoredCredential { acdc, signature },
}
}
fn persist(&self, ctx: &AuthsContext) {
for event in &self.issuer_kel {
ctx.registry
.append_event(&self.issuer_prefix, event)
.expect("append KEL event");
}
let vcp_bytes = auths_keri::tel_to_wire_bytes(match &self.vcp {
TelEvent::Vcp(v) => v,
_ => unreachable!(),
})
.expect("vcp bytes");
ctx.registry
.append_tel_event(
&self.issuer_prefix,
&self.registry,
&self.registry,
0,
&vcp_bytes,
)
.expect("append vcp");
let iss_bytes = auths_keri::tel_to_wire_bytes(match &self.iss {
TelEvent::Iss(i) => i,
_ => unreachable!(),
})
.expect("iss bytes");
ctx.registry
.append_tel_event(
&self.issuer_prefix,
&self.registry,
&self.credential_said,
0,
&iss_bytes,
)
.expect("append iss");
let blob = self.stored.to_bytes().expect("blob");
ctx.registry
.store_credential(&self.issuer_prefix, &self.credential_said, &blob)
.expect("store credential");
}
}