diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1fab55e3b..404f63d55 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -115,6 +115,7 @@ jobs: cargo run -p xtask -- check-admission-policy cargo run -p xtask -- check-command-drift cargo run -p xtask -- check-verify-path-completeness + cargo run -p xtask -- check-binding-boundary cargo run -p xtask -- gen-error-docs --check - name: Architecture boundary checks run: | diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 83fc56827..688194a2e 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -129,6 +129,25 @@ graph TD Violations are caught by `cargo deny check bans` and `cargo clippy` (`disallowed-methods`). +## The Report Is the Only API (relying parties) + +A relying party — the market, any server authenticating `Auths-Presentation` +requests, any consumer of a verify entrypoint — **never inspects evidence**. +Anything an RP needs to act on (subject, the proven root, capabilities, expiry, +freshness) must be a field on the verdict/report, set by the verifier only after +it proved the fact. If an RP finds itself parsing a KEL, an attachment, or an +ACDC out of the evidence it was handed, that is a missing verdict field — fix it +in `auths-verifier`, never in the consumer. Precedent: `subjectRoot` (the +delegator a seal-checked replay proved) was added to the Valid presentation +verdict for exactly this reason. + +The companion rule: **RPs re-implement no wire.** Header parsing, base64 +conventions, verify-request assembly, and the verdict→denial doctrine live once, +behind the SDK's relying-party surface (`packages/auths-node` +`presentationNonce` / `authenticatePresentation`). An RP keeps only what is +irreducibly its own: storage (the single-use challenge store, principal +records) and grant policy (which capabilities admit which action). + ## Port Inventory Ports are trait interfaces that decouple domain logic from infrastructure. Production adapters live in the appropriate infra crate under `src/adapters/`. Test doubles live in `auths-test-utils/src/fakes/` or `auths-test-utils/src/mocks/`. diff --git a/crates/auths-api/tests/cases/rp_auth.rs b/crates/auths-api/tests/cases/rp_auth.rs index 3ac19490e..fa90dd628 100644 --- a/crates/auths-api/tests/cases/rp_auth.rs +++ b/crates/auths-api/tests/cases/rp_auth.rs @@ -69,6 +69,7 @@ fn principal(caps: &[&str]) -> VerifiedPrincipal { let verdict = PresentationVerdict::Valid { issuer: IdentityDID::parse("did:keri:Eissuer").expect("issuer"), subject: CanonicalDid::parse("did:keri:Eagent").expect("subject"), + subject_root: CanonicalDid::parse("did:keri:Eagent").expect("subject"), caps: caps .iter() .map(|c| Capability::parse(c).expect("cap")) diff --git a/crates/auths-cli/src/commands/artifact/verify.rs b/crates/auths-cli/src/commands/artifact/verify.rs index c0760cd8e..2f38fa7ab 100644 --- a/crates/auths-cli/src/commands/artifact/verify.rs +++ b/crates/auths-cli/src/commands/artifact/verify.rs @@ -700,7 +700,7 @@ fn resolve_identity_key( // The identity_did is now PROVEN equal to the authenticated KEL's // inception (RT-005), so it is safe to return as the certified signer. - let (root_did, _kel) = trust.into_parts(); + let (root_did, _kel, _device_kels) = trust.into_parts(); Ok((pk, CanonicalDid::new_unchecked(root_did))) } else { // Resolve public key from the issuer DID diff --git a/crates/auths-cli/src/commands/credential.rs b/crates/auths-cli/src/commands/credential.rs index 827680ee6..084a24aba 100644 --- a/crates/auths-cli/src/commands/credential.rs +++ b/crates/auths-cli/src/commands/credential.rs @@ -143,6 +143,14 @@ pub enum CredentialSubcommand { help = "The base64url challenge nonce from /v1/auth/challenge." )] nonce: String, + + /// Also emit the wire-ready evidence (credential + attachment-carrying KELs + TEL) + /// a relying party without a registry replica verifies the presentation against. + #[arg( + long = "with-evidence", + help = "Emit JSON with the header AND the verifiable evidence bundle (for relying parties that hold no replica of your KEL)." + )] + with_evidence: bool, }, } @@ -231,6 +239,7 @@ pub fn handle_credential( said, audience, nonce, + with_evidence, } => { let ctx = build_auths_context(&repo_path, env_config, Some(passphrase_provider))?; let subject_alias = KeyAlias::new_unchecked(subject); @@ -250,14 +259,37 @@ pub fn handle_credential( .map_err(anyhow::Error::new)?; let header = format!("Auths-Presentation {token}"); - if is_json_mode() { - JsonResponse::success( - "credential present", - serde_json::json!({ "authorization": header }), + let evidence = if with_evidence { + Some( + auths_sdk::domains::credentials::load_presentation_evidence( + &ctx, + &subject_alias, + &said, + ) + .map_err(anyhow::Error::new)?, ) - .print()?; } else { - println!("{header}"); + None + }; + + match evidence { + Some(evidence) => { + // --with-evidence is machine-facing: always JSON, one object a + // relying party splices into its verify request. + JsonResponse::success( + "credential present", + serde_json::json!({ "authorization": header, "evidence": evidence }), + ) + .print()?; + } + None if is_json_mode() => { + JsonResponse::success( + "credential present", + serde_json::json!({ "authorization": header }), + ) + .print()?; + } + None => println!("{header}"), } Ok(()) } diff --git a/crates/auths-cli/src/commands/id/identity.rs b/crates/auths-cli/src/commands/id/identity.rs index 19c91e4f5..a55483408 100644 --- a/crates/auths-cli/src/commands/id/identity.rs +++ b/crates/auths-cli/src/commands/id/identity.rs @@ -857,6 +857,53 @@ pub fn handle_id( kel_attachments.push(hex::encode(att)); } + // Delegated device KELs: commits are signed by a DEVICE AID, so a + // stateless verifier needs each device KEL the root anchored — not + // only the root's. Enumerate the registry for dip-rooted KELs whose + // delegator is this identity, exporting each with its attachments. + let root_prefix_str = prefix.as_str().to_string(); + let mut device_prefixes: Vec = Vec::new(); + let _ = registry.visit_identities(&mut |candidate| { + if candidate != root_prefix_str { + device_prefixes.push(candidate.to_string()); + } + std::ops::ControlFlow::Continue(()) + }); + let mut device_kels = Vec::new(); + for device in device_prefixes { + let device_prefix = auths_keri::Prefix::new_unchecked(device.clone()); + let mut device_events: Vec = Vec::new(); + let _ = registry.visit_events(&device_prefix, 0, &mut |e| { + device_events.push(e.clone()); + std::ops::ControlFlow::Continue(()) + }); + let delegated_by_this_root = device_events + .first() + .and_then(|e| e.delegator()) + .is_some_and(|d| d.as_str() == root_prefix_str); + if !delegated_by_this_root { + continue; + } + let mut device_attachments = Vec::with_capacity(device_events.len()); + for e in &device_events { + let seq = e.sequence().value(); + let att = registry + .get_attachment(&device_prefix, seq) + .map_err(|err| anyhow::anyhow!("failed to read device KEL signature: {err}"))? + .ok_or_else(|| { + anyhow::anyhow!( + "device {device} KEL event at seq {seq} has no stored signature attachment; cannot export a verifiable bundle" + ) + })?; + device_attachments.push(hex::encode(att)); + } + device_kels.push(auths_verifier::BundleDeviceKel { + did: format!("did:keri:{device}"), + kel: device_events, + kel_attachments: device_attachments, + }); + } + // Create the bundle. Curve flows in-band from the typed keychain // extraction so verifiers never re-derive it from byte length. let bundle = IdentityBundle { @@ -866,6 +913,7 @@ pub fn handle_id( attestation_chain: attestations, kel, kel_attachments, + device_kels, bundle_timestamp: now, max_valid_for_secs: max_age_secs, }; diff --git a/crates/auths-cli/src/commands/verify_commit.rs b/crates/auths-cli/src/commands/verify_commit.rs index c5a329ecc..f8faba30e 100644 --- a/crates/auths-cli/src/commands/verify_commit.rs +++ b/crates/auths-cli/src/commands/verify_commit.rs @@ -170,14 +170,14 @@ pub async fn handle_verify_commit( { pinned_roots.push(own_root); } - let mut bundle_kel: Option = None; + let mut bundle_kels: Vec = Vec::new(); let bundle_path = cmd .identity_bundle .clone() .or_else(super::verify_helpers::discover_project_bundle); if let Some(bundle_path) = &bundle_path { match load_bundle_trust(bundle_path, chrono::Utc::now()) { - Ok((root, kel)) => { + Ok((root, kel, device_kels)) => { // Evidence-only (RT-005): the bundle is *evidence for* a root that // must be pinned independently (`.auths/roots` or self-trust). It // never becomes its own trust anchor — otherwise the anchor and the @@ -198,11 +198,18 @@ pub async fn handle_verify_commit( ); } if !kel.is_empty() { - bundle_kel = Some(BundleKel { + bundle_kels.push(BundleKel { did: root, events: kel, }); } + // Device KELs were seal-checked against the root by the parse; + // each resolves its own delegated signer DID statelessly. + bundle_kels.extend( + device_kels + .into_iter() + .map(|(did, events)| BundleKel { did, events }), + ); } Err(e) => return handle_error(&cmd, 2, &e), } @@ -226,7 +233,7 @@ pub async fn handle_verify_commit( &receipt_lookup, &sdk_ctx, &cmd, - bundle_kel.as_ref(), + &bundle_kels, commit_ref, ) .await, @@ -256,15 +263,22 @@ struct BundleKel { fn load_bundle_trust( path: &std::path::Path, now: chrono::DateTime, -) -> std::result::Result<(String, Vec), String> { +) -> std::result::Result< + ( + String, + Vec, + Vec, + ), + String, +> { let content = fs::read_to_string(path) .map_err(|e| format!("could not read identity bundle {path:?}: {e}"))?; let bundle: IdentityBundle = serde_json::from_str(&content) .map_err(|e| format!("identity bundle {path:?} is not valid JSON: {e}"))?; let trust = BundleTrust::parse(&bundle, now) .map_err(|e| format!("identity bundle {path:?} is not a usable trust anchor: {e}"))?; - let (root, kel) = trust.into_parts(); - Ok((root, kel)) + let (root, kel, device_kels) = trust.into_parts(); + Ok((root, kel, device_kels)) } /// Resolve the commit spec to a list of commit SHAs. @@ -366,19 +380,18 @@ fn extract_oidc_binding_display(attestation: &Attestation) -> Option, + bundle_kels: &[BundleKel], did: &str, ) -> Result, String> { - // Stateless first: a bundle that carries the signer's KEL satisfies - // resolution without any identity store (CI runners). Prefix binding is - // still enforced, so a tampered bundle cannot smuggle a foreign KEL. - if let Some(bundle) = bundle_kel - && bundle.did == did - { + // Stateless first: a bundle that carries the signer's KEL (the root's or a + // delegated device's) satisfies resolution without any identity store (CI + // runners). Prefix binding is still enforced, so a tampered bundle cannot + // smuggle a foreign KEL. + if let Some(bundle) = bundle_kels.iter().find(|b| b.did == did) { let prefix = auths_sdk::keri::parse_did_keri(did).map_err(|e| e.to_string())?; auths_sdk::keri::verify_prefix_binding(&prefix, &bundle.events) .map_err(|e| e.to_string())?; @@ -403,7 +416,7 @@ async fn verify_one_commit( receipt_lookup: &dyn WitnessReceiptLookup, sdk_ctx: &auths_sdk::context::AuthsContext, cmd: &VerifyCommitCommand, - bundle_kel: Option<&BundleKel>, + bundle_kels: &[BundleKel], commit_ref: &str, ) -> VerifyCommitResult { let sha = match resolve_commit_sha(commit_ref) { @@ -440,7 +453,7 @@ async fn verify_one_commit( // KEL sourcing is an SDK/adapter concern: the committed bundle or the local // registry — never a network fetch. The prefix-binding guard is applied // regardless of source. The command stays presentation-thin. - let device_kel = match resolve_signer_kel(registry, bundle_kel, &device_did).await { + let device_kel = match resolve_signer_kel(registry, bundle_kels, &device_did).await { Ok(events) => events, Err(e) => { return VerifyCommitResult::failure( @@ -463,7 +476,7 @@ async fn verify_one_commit( Some(delegator) => format!("did:keri:{delegator}"), None => root_did, }; - let root_kel = match resolve_signer_kel(registry, bundle_kel, &root_did).await { + let root_kel = match resolve_signer_kel(registry, bundle_kels, &root_did).await { Ok(events) => events, Err(e) => { return VerifyCommitResult::failure( @@ -501,11 +514,12 @@ async fn verify_one_commit( // no source it can trust to confirm freshness from them. Absent a verifier-supplied fresher // tip, the strongest honest grade is Unknown — the policy decides whether to tolerate it // (ADR 009 D5). A direct verify carries no such signal and likewise stays Unknown. - let verdict = match bundle_kel { - Some(_) => witnessed + let verdict = if bundle_kels.is_empty() { + witnessed.verdict + } else { + witnessed .verdict - .with_freshness(&FreshnessPolicy::default(), FreshnessEvidence::Offline), - None => witnessed.verdict, + .with_freshness(&FreshnessPolicy::default(), FreshnessEvidence::Offline) }; let mut result = verdict_to_result(sha.clone(), verdict); match witnessed.witness { diff --git a/crates/auths-id/src/policy/mod.rs b/crates/auths-id/src/policy/mod.rs index 799c543a9..9fb3dca7c 100644 --- a/crates/auths-id/src/policy/mod.rs +++ b/crates/auths-id/src/policy/mod.rs @@ -712,6 +712,8 @@ mod tests { PresentationVerdict::Valid { issuer: auths_verifier::IdentityDID::parse(CRED_ISSUER).expect("valid test issuer"), subject: auths_verifier::CanonicalDid::parse(CRED_SUBJECT).expect("valid test subject"), + subject_root: auths_verifier::CanonicalDid::parse(CRED_SUBJECT) + .expect("valid test subject"), caps: caps .iter() .map(|c| auths_verifier::Capability::parse(c).expect("valid test capability")) diff --git a/crates/auths-keri/src/events.rs b/crates/auths-keri/src/events.rs index 9e460e3c9..bbc33b4c8 100644 --- a/crates/auths-keri/src/events.rs +++ b/crates/auths-keri/src/events.rs @@ -1615,10 +1615,39 @@ pub fn pair_kel_attachments( events .into_iter() .zip(attachments) - .map(|(event, att)| Ok(SignedEvent::new(event, parse_attachment(att.as_ref())?))) + .map(|(event, att)| { + // A delegated (`dip`/`drt`) event's attachment is `-A ++ -G + // `; a plain event's is just `-A `. The JSON body + // of a delegated event carries no source seal (it lives in the + // attachment), so restore it here — the bilateral delegation + // binding then re-verifies it against the delegator KEL; it is + // never trusted from the attachment alone. + let (signatures, seals) = parse_delegated_attachment(att.as_ref())?; + let event = rehydrate_source_seal(event, seals.into_iter().next()); + Ok(SignedEvent::new(event, signatures)) + }) .collect() } +/// Re-attach a delegated event's source seal from its parsed attachment; plain +/// events and sealless attachments pass through unchanged. +fn rehydrate_source_seal(event: Event, seal: Option) -> Event { + let Some(seal) = seal else { + return event; + }; + match event { + Event::Dip(mut e) => { + e.source_seal = Some(seal); + Event::Dip(e) + } + Event::Drt(mut e) => { + e.source_seal = Some(seal); + Event::Drt(e) + } + other => other, + } +} + /// Error shape for attachment encode/decode. #[derive(Debug, thiserror::Error)] #[non_exhaustive] diff --git a/crates/auths-mcp-server/src/keri_auth.rs b/crates/auths-mcp-server/src/keri_auth.rs index 74286d4e7..b8c08fea4 100644 --- a/crates/auths-mcp-server/src/keri_auth.rs +++ b/crates/auths-mcp-server/src/keri_auth.rs @@ -223,6 +223,7 @@ mod tests { let verdict = PresentationVerdict::Valid { issuer: IdentityDID::parse("did:keri:Eissuer").expect("valid test issuer"), subject: CanonicalDid::parse("did:keri:Eagent").expect("valid test subject"), + subject_root: CanonicalDid::parse("did:keri:Eagent").expect("valid test subject"), caps: caps .iter() .map(|c| Capability::parse(c).expect("valid test capability")) diff --git a/crates/auths-rp/src/principal.rs b/crates/auths-rp/src/principal.rs index 22753528f..28c4dbd58 100644 --- a/crates/auths-rp/src/principal.rs +++ b/crates/auths-rp/src/principal.rs @@ -177,6 +177,7 @@ mod tests { PresentationVerdict::Valid { issuer: IdentityDID::parse("did:keri:Eissuer").expect("valid test issuer"), subject: CanonicalDid::parse(subject).expect("valid test subject"), + subject_root: CanonicalDid::parse(subject).expect("valid test subject"), caps: caps .iter() .map(|c| Capability::parse(c).expect("valid test capability")) @@ -193,6 +194,7 @@ mod tests { let fresh = PresentationVerdict::Valid { issuer: IdentityDID::parse("did:keri:Eissuer").expect("issuer"), subject: CanonicalDid::parse("did:keri:Eagent").expect("subject"), + subject_root: CanonicalDid::parse("did:keri:Eagent").expect("subject"), caps: vec![], role: None, expires_at: None, diff --git a/crates/auths-rp/tests/cases/verdict.rs b/crates/auths-rp/tests/cases/verdict.rs index fb4ee1afc..724df5a7a 100644 --- a/crates/auths-rp/tests/cases/verdict.rs +++ b/crates/auths-rp/tests/cases/verdict.rs @@ -7,6 +7,7 @@ fn valid_verdict(subject: &str, caps: &[&str]) -> PresentationVerdict { PresentationVerdict::Valid { issuer: IdentityDID::parse("did:keri:Eissuer").unwrap(), subject: CanonicalDid::parse(subject).unwrap(), + subject_root: CanonicalDid::parse(subject).unwrap(), caps: caps.iter().map(|c| Capability::parse(c).unwrap()).collect(), role: None, expires_at: None, diff --git a/crates/auths-sdk/src/domains/credentials/mod.rs b/crates/auths-sdk/src/domains/credentials/mod.rs index eed2ee6c3..59ad7fb02 100644 --- a/crates/auths-sdk/src/domains/credentials/mod.rs +++ b/crates/auths-sdk/src/domains/credentials/mod.rs @@ -37,7 +37,10 @@ pub use freshness::{ }; pub use issue::{CredentialIssuance, CredentialSummary, issue, list, revoke}; pub use present::{ChallengeSession, PresentationChallenge, present_credential}; -pub use present_inputs::{PresentationInputs, load_presentation_inputs}; +pub use present_inputs::{ + EvidenceCredential, PresentationEvidence, PresentationInputs, load_presentation_evidence, + load_presentation_inputs, +}; pub use stored::StoredCredential; pub use usage_ledger::{UsageDecision, UsageLedger, UsageObservation}; pub use verify::{ diff --git a/crates/auths-sdk/src/domains/credentials/present_inputs.rs b/crates/auths-sdk/src/domains/credentials/present_inputs.rs index 29381de0d..c9468d00e 100644 --- a/crates/auths-sdk/src/domains/credentials/present_inputs.rs +++ b/crates/auths-sdk/src/domains/credentials/present_inputs.rs @@ -116,6 +116,115 @@ pub fn load_presentation_inputs( }) } +/// The wire-ready evidence a relying party needs beside the `Auths-Presentation` +/// header: the credential, every KEL slice with per-event CESR signature +/// attachments (base64), and the TEL. Field names match the verifier's JSON +/// contract (`VerifyPresentationRequest`), so a relying party splices this +/// straight into a verify request without re-mapping. +#[derive(Debug, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PresentationEvidence { + /// The credential body + issuer signature, contract-shaped. + pub credential: EvidenceCredential, + /// The issuer identity's KEL (oldest first). + pub issuer_kel: Vec, + /// One base64 CESR signature attachment per issuer event. + pub issuer_kel_attachments_b64: Vec, + /// The subject (holder) AID's KEL. + pub subject_kel: Vec, + /// One base64 CESR signature attachment per subject event. + pub subject_kel_attachments_b64: Vec, + /// The subject's delegator KEL, or empty for a root subject. + pub delegator_kel: Vec, + /// One base64 CESR signature attachment per delegator event. + pub delegator_kel_attachments_b64: Vec, + /// The credential's TEL (`vcp`/`iss`/optional `rev`). + pub tel: Vec, +} + +/// The contract's `credential` object: ACDC body + base64 issuer signature. +#[derive(Debug, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct EvidenceCredential { + /// The ACDC body. + pub acdc: auths_keri::Acdc, + /// The issuer's detached signature over the ACDC wire bytes, base64. + pub signature_b64: String, +} + +/// Load the full wire-ready presentation evidence for `credential_said`. +/// +/// Same resolution as [`load_presentation_inputs`], plus the per-event signature +/// attachments each KEL carries in the registry — the parts a relying party's +/// verifier authenticates before any replay. A KEL event without a stored +/// attachment fails closed: evidence that cannot be authenticated must not be +/// exported as if it could. +/// +/// Args: +/// * `ctx`: The SDK context (registry + clock + keychain). +/// * `issuer_alias`: The keychain alias of the credential's issuer. +/// * `credential_said`: The SAID of the credential to export evidence for. +/// +/// Usage: +/// ```ignore +/// let evidence = load_presentation_evidence(&ctx, &alias, &said)?; +/// let json = serde_json::to_string(&evidence)?; +/// ``` +pub fn load_presentation_evidence( + ctx: &AuthsContext, + issuer_alias: &KeyAlias, + credential_said: &str, +) -> Result { + use base64::Engine as _; + let b64 = base64::engine::general_purpose::STANDARD; + + let inputs = load_presentation_inputs(ctx, issuer_alias, credential_said)?; + let issuer_prefix = + crate::domains::credentials::issue::resolve_issuer_prefix(ctx, issuer_alias)?; + let subject_prefix = Prefix::new_unchecked(inputs.signed.acdc.a.i.to_string()); + let delegator_prefix = subject_delegator_prefix(&inputs.subject_kel); + + let attachments = |prefix: &Prefix, kel: &[Event]| -> Result, CredentialError> { + kel.iter() + .map(|event| { + let seq = event.sequence().value(); + ctx.registry + .get_attachment(prefix, seq) + .map_err(|e| CredentialError::StaleOrUnresolvable { + reason: format!("attachment read failed for {prefix} seq {seq}: {e}"), + })? + .map(|bytes| b64.encode(bytes)) + .ok_or_else(|| CredentialError::StaleOrUnresolvable { + reason: format!( + "no signature attachment stored for {prefix} seq {seq} — this KEL cannot be exported as authenticatable evidence" + ), + }) + }) + .collect() + }; + + let issuer_kel_attachments_b64 = attachments(&issuer_prefix, &inputs.issuer_kel)?; + let subject_kel_attachments_b64 = attachments(&subject_prefix, &inputs.subject_kel)?; + let delegator_kel_attachments_b64 = match &delegator_prefix { + Some(delegator) => attachments(delegator, &inputs.subject_delegator_kel)?, + None => Vec::new(), + }; + + Ok(PresentationEvidence { + credential: EvidenceCredential { + acdc: inputs.signed.acdc.clone(), + signature_b64: b64.encode(&inputs.signed.signature), + }, + issuer_kel: inputs.issuer_kel, + issuer_kel_attachments_b64, + subject_kel: inputs.subject_kel, + subject_kel_attachments_b64, + delegator_kel: inputs.subject_delegator_kel, + delegator_kel_attachments_b64, + tel: inputs.tel, + }) +} + /// The delegator prefix of a delegated subject (from its `dip`/`drt`), or `None` when the /// subject is a non-delegated root identity. fn subject_delegator_prefix(subject_kel: &[Event]) -> Option { diff --git a/crates/auths-sdk/src/workflows/commit_hooks.rs b/crates/auths-sdk/src/workflows/commit_hooks.rs index 3fb655c7d..dfb7a03ca 100644 --- a/crates/auths-sdk/src/workflows/commit_hooks.rs +++ b/crates/auths-sdk/src/workflows/commit_hooks.rs @@ -504,16 +504,22 @@ mod tests { .expect("re-running init over our own config must be fine"); } - /// A registry mirror must never block a code push. + /// The pre-push hook chains and never blocks: no mirror, no network, no + /// side effects — its whole job is keeping repo-local hooks alive under + /// core.hooksPath. #[test] - fn pre_push_is_non_fatal_and_opt_outable() { + fn pre_push_only_chains_and_exits_clean() { assert!( - PRE_PUSH_HOOK.contains("|| true"), - "registry push failure must not fail the code push" + PRE_PUSH_HOOK.contains("hooks/pre-push"), + "the managed hook must chain to the repo-local pre-push" ); assert!( - PRE_PUSH_HOOK.contains("auths.autopush"), - "pushing to a remote you do not own must stay refusable" + PRE_PUSH_HOOK.trim_end().ends_with("exit 0"), + "a missing repo hook must exit clean, never fail the push" + ); + assert!( + !PRE_PUSH_HOOK.contains("registry push"), + "the mirror is gone — KEL distribution is the committed bundle, not a pushed ref" ); } } diff --git a/crates/auths-verifier/src/commit_bundle.rs b/crates/auths-verifier/src/commit_bundle.rs index 68a6d9630..2cf1b969a 100644 --- a/crates/auths-verifier/src/commit_bundle.rs +++ b/crates/auths-verifier/src/commit_bundle.rs @@ -12,7 +12,9 @@ //! `verifyCommitJson` WASM export), so the verdict cannot drift between //! transports. -use auths_keri::{Event, compute_event_said, pair_kel_attachments, validate_signed_kel}; +use auths_keri::{ + Event, KelSealIndex, compute_event_said, pair_kel_attachments, validate_signed_kel, +}; use chrono::{DateTime, Utc}; use serde::Serialize; @@ -61,8 +63,13 @@ pub enum BundleTrustError { pub struct BundleTrust { root_did: String, kel: Vec, + device_kels: Vec, } +/// One authenticated device KEL from a bundle: the device's `did:keri:` and its +/// seal-checked events, oldest first. +pub type AuthenticatedDeviceKel = (String, Vec); + impl BundleTrust { /// Parse a bundle into a trust anchor: freshness + RT-005 + RT-002. /// @@ -135,9 +142,63 @@ impl BundleTrust { .map_err(|e| BundleTrustError::KelUnauthenticated(e.to_string()))?; } + // Device KELs authenticate the same way, with the just-authenticated + // root KEL resolving each delegation seal. Self-certification first: a + // bundle cannot pair a victim's device DID with an attacker-authored + // KEL any more than it can for the root. + let root_seals = KelSealIndex::from_events(&bundle.kel); + let mut device_kels = Vec::with_capacity(bundle.device_kels.len()); + for device in &bundle.device_kels { + let prefix = device + .did + .strip_prefix("did:keri:") + .unwrap_or(device.did.as_str()); + let Some(inception) = device.kel.first() else { + return Err(BundleTrustError::KelUnauthenticated(format!( + "device {} carries an empty KEL", + device.did + ))); + }; + let said = compute_event_said(inception).map_err(|e| { + BundleTrustError::NotSelfCertifying(format!( + "device {} KEL inception has no computable SAID: {e}", + device.did + )) + })?; + if prefix != said.as_str() { + return Err(BundleTrustError::NotSelfCertifying(format!( + "device did {} does not self-certify to its inception SAID did:keri:{said}", + device.did + ))); + } + let attachment_bytes: Vec> = device + .kel_attachments + .iter() + .map(|att_hex| { + hex::decode(att_hex).map_err(|e| { + BundleTrustError::KelUnauthenticated(format!( + "non-hex device KEL signature attachment: {e}" + )) + }) + }) + .collect::>()?; + let signed = pair_kel_attachments(device.kel.clone(), &attachment_bytes) + .map_err(|e| BundleTrustError::KelUnauthenticated(e.to_string()))?; + validate_signed_kel(&signed, Some(&root_seals)) + .map_err(|e| BundleTrustError::KelUnauthenticated(e.to_string()))?; + // Keep the rehydrated events: pairing restored each delegated + // event's -G source seal, which downstream structural replays + // re-enforce against the root KEL. + device_kels.push(( + device.did.clone(), + signed.into_iter().map(|se| se.event).collect(), + )); + } + Ok(Self { root_did: bundle.identity_did.to_string(), kel: bundle.kel.clone(), + device_kels, }) } @@ -152,10 +213,16 @@ impl BundleTrust { &self.kel } - /// Consume the anchor into `(root_did, kel)` for callers that thread the - /// parts separately (the CLI's stateless resolver). - pub fn into_parts(self) -> (String, Vec) { - (self.root_did, self.kel) + /// The authenticated device KELs — `(did, events)` per delegated device the + /// bundle carries, each seal-checked against the root KEL. + pub fn device_kels(&self) -> &[AuthenticatedDeviceKel] { + &self.device_kels + } + + /// Consume the anchor into `(root_did, kel, device_kels)` for callers that + /// thread the parts separately (the CLI's stateless resolver). + pub fn into_parts(self) -> (String, Vec, Vec) { + (self.root_did, self.kel, self.device_kels) } } @@ -412,6 +479,7 @@ mod tests { attestation_chain: Vec::new(), kel: Vec::new(), kel_attachments: Vec::new(), + device_kels: Vec::new(), bundle_timestamp: ts, max_valid_for_secs: ttl, } diff --git a/crates/auths-verifier/src/contract.rs b/crates/auths-verifier/src/contract.rs index 8233d5888..8cb2581b1 100644 --- a/crates/auths-verifier/src/contract.rs +++ b/crates/auths-verifier/src/contract.rs @@ -104,6 +104,7 @@ enum WirePresentationVerdict { Valid { issuer: String, subject: String, + subject_root: String, caps: Vec, role: Option, expires_at: Option, @@ -195,6 +196,7 @@ impl From for WirePresentationVerdict { PresentationVerdict::Valid { issuer, subject, + subject_root, caps, role, expires_at, @@ -203,6 +205,7 @@ impl From for WirePresentationVerdict { } => WirePresentationVerdict::Valid { issuer: issuer.as_str().to_string(), subject: subject.as_str().to_string(), + subject_root: subject_root.as_str().to_string(), caps: caps.iter().map(|c| c.as_str().to_string()).collect(), role, expires_at: expires_at.map(|t| t.to_rfc3339()), @@ -509,7 +512,9 @@ fn serialize_verdict(verdict: V) -> String { /// Authenticate one untrusted wire KEL: decode its base64 CESR signature attachments, /// pair them per event, and fold per-event signature verification into the replay /// (`validate_signed_kel`). This is the RT-002 ingestion boundary for the JSON -/// contract — after it, the events are safe for the structural replays downstream. +/// contract — the returned events are the ones downstream replays must use: pairing +/// rehydrates each delegated event's `-G` source seal from its attachment, and a +/// dip/drt without its seal cannot re-validate its delegation binding. /// /// An empty KEL with no attachments passes through (the verify cores report the /// precise domain verdict for a missing KEL); any count mismatch, undecodable @@ -519,9 +524,9 @@ fn authenticate_kel( events: &[Event], attachments_b64: &[String], lookup: Option<&dyn DelegatorKelLookup>, -) -> Result<(), RequestError> { +) -> Result, RequestError> { if events.is_empty() && attachments_b64.is_empty() { - return Ok(()); + return Ok(Vec::new()); } let mut attachment_bytes = Vec::with_capacity(attachments_b64.len()); for (i, attachment) in attachments_b64.iter().enumerate() { @@ -539,7 +544,7 @@ fn authenticate_kel( field, detail: e.to_string(), })?; - Ok(()) + Ok(signed.into_iter().map(|se| se.event).collect()) } /// Parse, bound-check, and run the presentation verify; `Err` carries a request-layer error. @@ -560,20 +565,20 @@ fn run_presentation(request_json: &str) -> Result Result Result Result, + /// Delegated device KELs anchored under this identity's root. Commits are + /// signed by a DEVICE AID, so a stateless verifier needs the device's KEL, + /// not only the root's. Each entry authenticates exactly like the root KEL + /// (per-event hex CESR attachments), with the root's seal index resolving + /// the delegation. Empty in bundles exported before this field existed — + /// and for identities whose signing DID IS the root. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub device_kels: Vec, /// UTC timestamp when this bundle was created pub bundle_timestamp: DateTime, /// Maximum age in seconds before this bundle is considered stale @@ -971,6 +979,18 @@ impl IdentityBundle { } } +/// One delegated device KEL carried inside an [`IdentityBundle`]. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] +pub struct BundleDeviceKel { + /// The device's `did:keri:` AID (the `dip` prefix the root anchored). + pub did: String, + /// The device KEL events, oldest first. + pub kel: Vec, + /// The CESR signature attachment (hex) for each `kel` event, in order. + pub kel_attachments: Vec, +} + /// Represents a 2-way key attestation between a primary identity and a device key. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] @@ -1858,6 +1878,7 @@ mod tests { attestation_chain: vec![], kel: vec![], kel_attachments: vec![], + device_kels: vec![], bundle_timestamp: DateTime::parse_from_rfc3339("2099-01-01T00:00:00Z") .unwrap() .with_timezone(&Utc), @@ -1909,6 +1930,7 @@ mod tests { attestation_chain: vec![attestation], kel: vec![], kel_attachments: vec![], + device_kels: vec![], bundle_timestamp: DateTime::parse_from_rfc3339("2099-01-01T00:00:00Z") .unwrap() .with_timezone(&Utc), diff --git a/crates/auths-verifier/src/lib.rs b/crates/auths-verifier/src/lib.rs index cd4bd6286..654d41a09 100644 --- a/crates/auths-verifier/src/lib.rs +++ b/crates/auths-verifier/src/lib.rs @@ -101,13 +101,13 @@ pub use authorization_summary::AuthorizationSummary; // Re-export core types pub use core::{ - ATTESTATION_VERSION, Attestation, Capability, CapabilityError, CommitOid, CommitOidError, - DevicePublicKey, EcdsaP256Error, EcdsaP256PublicKey, EcdsaP256Signature, Ed25519KeyError, - Ed25519PublicKey, Ed25519Signature, IdentityBundle, InvalidKeyError, MAX_ATTESTATION_JSON_SIZE, - MAX_JSON_BATCH_SIZE, OidcBinding, PolicyId, PublicKeyDecodeError, PublicKeyHex, - PublicKeyHexError, ResourceId, Role, RoleParseError, SignatureAlgorithm, SignatureLengthError, - SignatureVerifyError, ThresholdPolicy, TypedSignature, VerifiedAttestation, - decode_public_key_bytes, decode_public_key_hex, + ATTESTATION_VERSION, Attestation, BundleDeviceKel, Capability, CapabilityError, CommitOid, + CommitOidError, DevicePublicKey, EcdsaP256Error, EcdsaP256PublicKey, EcdsaP256Signature, + Ed25519KeyError, Ed25519PublicKey, Ed25519Signature, IdentityBundle, InvalidKeyError, + MAX_ATTESTATION_JSON_SIZE, MAX_JSON_BATCH_SIZE, OidcBinding, PolicyId, PublicKeyDecodeError, + PublicKeyHex, PublicKeyHexError, ResourceId, Role, RoleParseError, SignatureAlgorithm, + SignatureLengthError, SignatureVerifyError, ThresholdPolicy, TypedSignature, + VerifiedAttestation, decode_public_key_bytes, decode_public_key_hex, }; // Re-export the OIDC policy join (keyless CI verify-time exchange) @@ -156,7 +156,9 @@ pub use auths_keri::{ // Re-export commit verification types pub use commit::{VerifiedCommit, commit_object_is_signed}; -pub use commit_bundle::{BundleTrust, BundleTrustError, verify_commit_with_bundle_json}; +pub use commit_bundle::{ + AuthenticatedDeviceKel, BundleTrust, BundleTrustError, verify_commit_with_bundle_json, +}; pub use commit_kel::{ ANCHOR_SEQ_TRAILER, CommitVerdict, DEVICE_TRAILER, ID_TRAILER, SCOPE_TRAILER, VerifierWitnessPolicy, WitnessGateStatus, WitnessedVerdict, anchor_seq_trailer, diff --git a/crates/auths-verifier/src/presentation.rs b/crates/auths-verifier/src/presentation.rs index 5ce463c97..b67d36bbf 100644 --- a/crates/auths-verifier/src/presentation.rs +++ b/crates/auths-verifier/src/presentation.rs @@ -148,6 +148,11 @@ pub enum PresentationVerdict { issuer: IdentityDID, /// The subject (holder) AID (`did:keri:`) whose current key signed the presentation. subject: CanonicalDid, + /// The subject's proven root: for a delegated subject (dip-rooted KEL) the + /// delegator AID whose anchoring seal the replay verified; for a root subject, + /// the subject itself. Relying parties credit accounts to THIS identity — + /// never to a delegator parsed out of unverified input. + subject_root: CanonicalDid, /// The capabilities the now-bound credential grants (`a.capability`). caps: Vec, /// The optional informational role claim (`a.role`). @@ -469,6 +474,18 @@ fn verify_holder_signature( let Ok(subject) = CanonicalDid::parse(&format!("did:keri:{}", signed.acdc.a.i)) else { return PresentationVerdict::SubjectKelInvalid; }; + // The proven root: a dip-rooted subject KEL only replays after its delegator's + // anchoring seal checked out (the KelSealIndex lookup above), so the delegator + // named by the KEL is verified, not claimed. A root subject is its own root. + let subject_root = match subject_kel.iter().find_map(|event| event.delegator()) { + Some(delegator) => { + let Ok(root) = CanonicalDid::parse(&format!("did:keri:{delegator}")) else { + return PresentationVerdict::SubjectKelInvalid; + }; + root + } + None => subject.clone(), + }; let message = PresentationEnvelope::signed_message( &envelope.credential_said, @@ -483,6 +500,7 @@ fn verify_holder_signature( return PresentationVerdict::Valid { issuer: grant.issuer, subject, + subject_root, caps: grant.caps, role: grant.role, expires_at: grant.expires_at, diff --git a/crates/auths-verifier/tests/cases/freshness_honesty.rs b/crates/auths-verifier/tests/cases/freshness_honesty.rs index 9f7d160c7..d971aca44 100644 --- a/crates/auths-verifier/tests/cases/freshness_honesty.rs +++ b/crates/auths-verifier/tests/cases/freshness_honesty.rs @@ -42,6 +42,7 @@ fn presentation_valid_names_as_of_and_freshness() { let verdict = PresentationVerdict::Valid { issuer: did("did:keri:Eissuer"), subject: subject("did:keri:Eholder"), + subject_root: subject("did:keri:Eholder"), caps: vec![Capability::parse("acme:read").expect("cap")], role: None, expires_at: None, diff --git a/crates/auths-verifier/tests/cases/presentation.rs b/crates/auths-verifier/tests/cases/presentation.rs index b43620290..92afd0b8d 100644 --- a/crates/auths-verifier/tests/cases/presentation.rs +++ b/crates/auths-verifier/tests/cases/presentation.rs @@ -461,6 +461,7 @@ async fn valid_challenge_presentation_verifies() { PresentationVerdict::Valid { issuer, subject: s, + subject_root, caps, role, expires_at, @@ -468,6 +469,10 @@ async fn valid_challenge_presentation_verifies() { .. } => { assert_eq!(s.as_str(), format!("did:keri:{}", subject.aid)); + assert_eq!( + subject_root, s, + "a root subject is its own proven root on the Valid verdict" + ); assert_eq!( caps.iter().map(|c| c.as_str()).collect::>(), ["sign"] @@ -971,6 +976,10 @@ fn presentation_json_contract_matches_sync() { format!("did:keri:{}", subject.aid), "valid verdict carries the holder subject DID" ); + assert_eq!( + verdict["subjectRoot"], verdict["subject"], + "a root subject is its own proven root on the wire" + ); assert_eq!(verdict["caps"], serde_json::json!(["sign"])); assert_eq!( verdict["freshness"], "unknown", diff --git a/crates/auths-verifier/ts/verdict.ts b/crates/auths-verifier/ts/verdict.ts index 71b35d564..13a59d488 100644 --- a/crates/auths-verifier/ts/verdict.ts +++ b/crates/auths-verifier/ts/verdict.ts @@ -58,6 +58,8 @@ export type PresentationVerdict = kind: "valid"; issuer: CanonicalDid; subject: CanonicalDid; + /** The subject's proven root: its delegator for a delegated subject, itself otherwise. */ + subjectRoot: CanonicalDid; caps: readonly Capability[]; role: string | null; expiresAt: string | null; diff --git a/crates/xtask/src/check_binding_boundary.rs b/crates/xtask/src/check_binding_boundary.rs new file mode 100644 index 000000000..864c1e606 --- /dev/null +++ b/crates/xtask/src/check_binding_boundary.rs @@ -0,0 +1,83 @@ +//! Binding-boundary lint: the Node bindings are a presentation layer. +//! +//! `packages/auths-node` exists to marshal — it calls `auths_sdk` workflows +//! (and the contract/wire crates `auths_verifier`, `auths_keri`, `auths_rp`, +//! `auths_crypto`), it does not reach into `auths_id` / `auths_core` / +//! `auths_storage` primitives. Every such reach is a place where the binding +//! can drift from the SDK's orchestration — the `delegateAgent` incident +//! (bindings kept minting attestation-linked agents long after the SDK went +//! KERI-native) is the class this lint prevents. +//! +//! Existing reaches are grandfathered with a trailing +//! `// binding-boundary-allow: ` and shrink over time; a NEW deep +//! import without the annotation fails CI. + +use std::path::PathBuf; + +use anyhow::{bail, Context}; + +const BINDINGS_SRC: &str = "packages/auths-node/src"; +const DEEP_PREFIXES: &[&str] = &["use auths_id::", "use auths_core::", "use auths_storage::"]; +const ALLOW_MARKER: &str = "binding-boundary-allow:"; + +/// Run the lint over the Node bindings source. +/// +/// Args: +/// * `workspace_root`: The repository root. +/// +/// Usage: +/// ```ignore +/// check_binding_boundary::run(workspace_root())?; +/// ``` +pub fn run(workspace_root: PathBuf) -> anyhow::Result<()> { + let src = workspace_root.join(BINDINGS_SRC); + let mut violations = Vec::new(); + let mut scanned = 0usize; + + for entry in std::fs::read_dir(&src).with_context(|| format!("read {}", src.display()))? { + let path = entry?.path(); + if path.extension().and_then(|e| e.to_str()) != Some("rs") { + continue; + } + scanned += 1; + let content = + std::fs::read_to_string(&path).with_context(|| format!("read {}", path.display()))?; + let lines: Vec<&str> = content.lines().collect(); + for (idx, line) in lines.iter().enumerate() { + let trimmed = line.trim_start(); + let is_deep = DEEP_PREFIXES.iter().any(|p| trimmed.starts_with(p)); + // rustfmt may float a trailing annotation to the next line (inside a + // brace group) — accept the marker on the line, the one above, or below. + let annotated = line.contains(ALLOW_MARKER) + || idx + .checked_sub(1) + .is_some_and(|i| lines[i].contains(ALLOW_MARKER)) + || lines.get(idx + 1).is_some_and(|l| l.contains(ALLOW_MARKER)); + if is_deep && !annotated { + violations.push(format!( + "{}:{}: {}", + path.strip_prefix(&workspace_root) + .unwrap_or(&path) + .display(), + idx + 1, + trimmed + )); + } + } + } + + if !violations.is_empty() { + for v in &violations { + eprintln!("BINDING-BOUNDARY {v}"); + } + bail!( + "{} deep import(s) in the Node bindings without a `{ALLOW_MARKER}` annotation. \ + Bindings are a presentation layer: call an auths_sdk workflow instead, or \ + annotate the line with the reason and a migration note.", + violations.len() + ); + } + + println!("binding-boundary check: {scanned} binding files scanned, 0 violations"); + Ok(()) +} diff --git a/crates/xtask/src/main.rs b/crates/xtask/src/main.rs index b35ae331c..95db1ed12 100644 --- a/crates/xtask/src/main.rs +++ b/crates/xtask/src/main.rs @@ -8,6 +8,7 @@ )] mod check_admission_policy; mod check_anchor_discipline; +mod check_binding_boundary; mod check_clippy_sync; mod check_command_drift; mod check_constant_time; @@ -33,6 +34,8 @@ struct Cli { enum Command { /// Generate docs/cloud-ci/telemetry/schema.json and schema.md from AuditEvent GenSchema, + /// Lint the Node bindings: presentation layer only, no deep crate reaches + CheckBindingBoundary, /// Regenerate CLI flag tables in docs from `auths --help` output. /// Pass `--check` to fail if any table is out of date (CI gate). GenDocs { @@ -95,6 +98,9 @@ fn main() -> anyhow::Result<()> { }; match cli.command { Command::GenSchema => gen_schema::run(workspace_root()), + Command::CheckBindingBoundary => { + check_binding_boundary::run(workspace_root().to_path_buf()) + } Command::GenDocs { check } => gen_docs::run(workspace_root(), check), Command::GenerateSchemas => schemas::generate(workspace_root()), Command::ValidateSchemas => schemas::validate(workspace_root()), diff --git a/docs/sdk/node/api/index.md b/docs/sdk/node/api/index.md index a0b71395f..b8cc93582 100644 --- a/docs/sdk/node/api/index.md +++ b/docs/sdk/node/api/index.md @@ -325,7 +325,7 @@ service properties. #### Example ```typescript -import { Auths } from '@auths-dev/node' +import { Auths } from '@auths-dev/sdk' const auths = new Auths() @@ -642,7 +642,10 @@ The signature and signer DID. verify(opts): Promise; ``` -Verifies a single attestation with optional capability and time constraints. +Verifies a single attestation's authenticity, with an optional time constraint. + +Capability/role authority is no longer checked here — that grant comes from a +holder-verified ACDC credential, not the attestation. ###### Parameters @@ -678,7 +681,10 @@ console.log(result.valid) verifyChain(opts): Promise; ``` -Verifies an attestation chain with optional capability and witness constraints. +Verifies an attestation chain's authenticity, with optional witness quorum. + +Capability authority is no longer checked here — that grant comes from a +holder-verified ACDC credential. ###### Parameters @@ -698,6 +704,20 @@ The verification report. [VerificationError](#verificationerror) if verification encounters an error. +##### availableChecks() + +```ts +static availableChecks(): string[]; +``` + +Returns the list of known diagnostic check names. + +###### Returns + +`string`[] + +Array of check name strings. + *** ### AuthsError @@ -711,7 +731,7 @@ machine-readable [code](#code) and human-readable #### Example ```typescript -import { Auths, AuthsError } from '@auths-dev/node' +import { Auths, AuthsError } from '@auths-dev/sdk' try { auths.signAs({ message: data, identityDid: did }) @@ -1031,7 +1051,7 @@ Common codes: `'invalid_key'`, `'key_not_found'`, `'signing_failed'`. #### Example ```typescript -import { Auths, CryptoError } from '@auths-dev/node' +import { Auths, CryptoError } from '@auths-dev/sdk' try { auths.signAs({ message: data, identityDid: did }) @@ -1392,6 +1412,144 @@ auths.devices.revoke({ *** +### EphemeralIdentity + +In-memory identity for tests, demos, and CI. + +Generates a fresh Ed25519 keypair on construction. The resulting DID is +`did:key:z...` (not `did:keri:`), which is valid for `signActionRaw` +and `verifyActionEnvelope` but cannot be used with KERI operations. + +#### Example + +```ts +import { EphemeralIdentity } from '@auths/node/testing' + +const alice = new EphemeralIdentity() +const sig = alice.sign(Buffer.from('hello')) +const envelope = alice.signAction('tool_call', '{"tool": "web_search"}') +const result = alice.verifyAction(envelope) +console.log(result.valid) // true +``` + +#### Constructors + +##### Constructor + +```ts +new EphemeralIdentity(): EphemeralIdentity; +``` + +###### Returns + +[`EphemeralIdentity`](#ephemeralidentity) + +#### Accessors + +##### did + +###### Get Signature + +```ts +get did(): string; +``` + +The `did:key:z...` identifier for this ephemeral identity. + +###### Returns + +`string` + +##### privateKeyHex + +###### Get Signature + +```ts +get privateKeyHex(): string; +``` + +Hex-encoded 32-byte Ed25519 seed (private key). + +###### Returns + +`string` + +##### publicKeyHex + +###### Get Signature + +```ts +get publicKeyHex(): string; +``` + +Hex-encoded 32-byte Ed25519 public key. + +###### Returns + +`string` + +#### Methods + +##### sign() + +```ts +sign(message): string; +``` + +Sign arbitrary bytes. Returns hex-encoded signature. + +###### Parameters + +###### message + +`Buffer` + +###### Returns + +`string` + +##### signAction() + +```ts +signAction(actionType, payloadJson): string; +``` + +Sign an action envelope. Returns JSON envelope string. + +###### Parameters + +###### actionType + +`string` + +###### payloadJson + +`string` + +###### Returns + +`string` + +##### verifyAction() + +```ts +verifyAction(envelopeJson): NapiVerificationResult; +``` + +Verify an action envelope against this identity's public key. + +###### Parameters + +###### envelopeJson + +`string` + +###### Returns + +`NapiVerificationResult` + +*** + ### IdentityError Raised when an identity or device operation fails. @@ -1401,7 +1559,7 @@ Common codes: `'identity_not_found'`, `'unknown'`. #### Example ```typescript -import { Auths, IdentityError } from '@auths-dev/node' +import { Auths, IdentityError } from '@auths-dev/sdk' try { auths.devices.link({ identityDid: did, capabilities: ['sign'] }) @@ -1834,7 +1992,7 @@ Common codes: `'keychain_locked'`. #### Example ```typescript -import { Auths, KeychainError } from '@auths-dev/node' +import { Auths, KeychainError } from '@auths-dev/sdk' try { auths.identities.create({ label: 'main' }) @@ -2058,7 +2216,7 @@ Common codes: `'server_error'`. #### Example ```typescript -import { NetworkError } from '@auths-dev/node' +import { NetworkError } from '@auths-dev/sdk' try { // network operation @@ -2079,8 +2237,8 @@ try { ```ts new NetworkError( - message, - code, + message, + code, shouldRetry?): NetworkError; ``` @@ -2297,7 +2455,7 @@ Common codes: `'org_error'`. #### Example ```typescript -import { Auths, OrgError } from '@auths-dev/node' +import { Auths, OrgError } from '@auths-dev/sdk' try { auths.orgs.addMember({ orgDid, memberDid, role: 'member' }) @@ -2516,6 +2674,9 @@ https://v8.dev/docs/stack-trace-api#customizing-stack-traces Manages organizations and their membership. +The organization mints a fresh delegated key for each member from a label; +callers do not provide member public keys. + Access via [Auths.orgs](#orgs). #### Example @@ -2524,9 +2685,8 @@ Access via [Auths.orgs](#orgs). const org = auths.orgs.create({ label: 'my-team' }) auths.orgs.addMember({ orgDid: org.orgDid, - memberDid: dev.did, + memberLabel: 'alice', role: 'member', - memberPublicKeyHex: dev.publicKey, }) ``` @@ -2556,7 +2716,8 @@ new OrgService(client): OrgService; addMember(opts): OrgMember; ``` -Adds a member to an organization. +Adds a member to an organization. The org mints a fresh delegated key for +the member from `memberLabel`; the returned `memberDid` is the new AID. ###### Parameters @@ -2581,9 +2742,8 @@ The new member record. ```typescript const member = auths.orgs.addMember({ orgDid: org.orgDid, - memberDid: dev.did, + memberLabel: 'alice', role: 'member', - memberPublicKeyHex: dev.publicKey, }) ``` @@ -2690,7 +2850,7 @@ Common codes: `'pairing_error'`, `'timeout'`. #### Example ```typescript -import { PairingError } from '@auths-dev/node' +import { PairingError } from '@auths-dev/sdk' try { await auths.pairing.createSession({ bindAddress: '127.0.0.1' }) @@ -2711,8 +2871,8 @@ try { ```ts new PairingError( - message, - code, + message, + code, shouldRetry?): PairingError; ``` @@ -2925,7 +3085,7 @@ https://v8.dev/docs/stack-trace-api#customizing-stack-traces Handles device pairing for cross-device identity authorization. The pairing flow: controller creates a session, device joins with the -short code, controller completes pairing to authorize the device. +short code, the controller waits for the device's response. Access via [Auths.pairing](#pairing). @@ -2986,32 +3146,6 @@ dispose: void; `void` -##### complete() - -```ts -complete(opts): Promise; -``` - -Completes pairing by authorizing the connected device. - -###### Parameters - -###### opts - -[`CompletePairingOptions`](#completepairingoptions) - -Completion options with device identity and capabilities. - -###### Returns - -`Promise`\<[`PairingResult`](#pairingresult)\> - -The pairing result with the device's authorization attestation. - -###### Throws - -[PairingError](#pairingerror) if no session is active or completion fails. - ##### createSession() ```ts @@ -3138,7 +3272,7 @@ against an attestation context to produce an allow/deny decision. #### Example ```typescript -import { PolicyBuilder } from '@auths-dev/node' +import { PolicyBuilder } from '@auths-dev/sdk' // Quick standard policy const policy = PolicyBuilder.standard('sign_commit') @@ -3171,6 +3305,24 @@ new PolicyBuilder(): PolicyBuilder; [`PolicyBuilder`](#policybuilder) +#### Properties + +##### AVAILABLE\_PREDICATES + +```ts +readonly static AVAILABLE_PREDICATES: string[]; +``` + +All available predicate method names. + +##### AVAILABLE\_PRESETS + +```ts +readonly static AVAILABLE_PRESETS: string[]; +``` + +Built-in preset policy names. + #### Methods ##### attrEquals() @@ -3786,6 +3938,52 @@ Policies to OR together. A new builder combining the policies. +##### availablePredicates() + +```ts +static availablePredicates(): string[]; +``` + +Returns the list of available predicate method names. + +###### Returns + +`string`[] + +##### availablePresets() + +```ts +static availablePresets(): string[]; +``` + +Returns the list of available preset policy names. + +###### Returns + +`string`[] + +##### fromJson() + +```ts +static fromJson(jsonStr): PolicyBuilder; +``` + +Reconstructs a PolicyBuilder from a JSON policy expression. + +###### Parameters + +###### jsonStr + +`string` + +JSON string from `toJson()` or config files. + +###### Returns + +[`PolicyBuilder`](#policybuilder) + +A new builder with the parsed predicates. + ##### standard() ```ts @@ -4006,7 +4204,7 @@ Common codes: `'repo_not_found'`, `'trust_error'`, `'witness_error'`. #### Example ```typescript -import { Auths, StorageError } from '@auths-dev/node' +import { Auths, StorageError } from '@auths-dev/sdk' try { auths.trust.pin({ did: 'did:keri:ENOTREAL' }) @@ -4374,7 +4572,7 @@ Common codes: `'invalid_signature'`, `'expired_attestation'`, #### Example ```typescript -import { verifyAttestation, VerificationError } from '@auths-dev/node' +import { verifyAttestation, VerificationError } from '@auths-dev/sdk' try { await verifyAttestation(json, publicKey) @@ -4651,7 +4849,10 @@ The witness entry. ###### Example ```typescript -const w = auths.witnesses.add({ url: 'http://witness.example.com:3333' }) +const w = auths.witnesses.add({ + url: 'http://witness.example.com:3333', + aid: 'did:keri:EWitness...', +}) console.log(w.url) // http://witness.example.com:3333 ``` @@ -4743,31 +4944,23 @@ Options for [OrgService.addMember](#addmember). optional capabilities: string[]; ``` -Capabilities to grant the member. - -##### memberDid - -```ts -memberDid: string; -``` - -DID of the member to add. +Capabilities to grant the member. If omitted, role defaults are used. -##### memberPublicKeyHex? +##### expiresAt? ```ts -optional memberPublicKeyHex: string; +optional expiresAt: number; ``` -Hex-encoded public key of the member (required for cross-repo adds). +Optional Unix timestamp (seconds) at which the membership expires. -##### note? +##### memberLabel ```ts -optional note: string; +memberLabel: string; ``` -Optional note for the membership record. +Human-readable alias for the member key minted by the org. ##### orgDid @@ -4801,6 +4994,14 @@ Options for [WitnessService.add](#add). #### Properties +##### aid + +```ts +aid: string; +``` + +AID (`did:keri:` prefix) of the witness. + ##### label? ```ts @@ -4905,14 +5106,6 @@ An attestation record from the local registry. #### Properties -##### capabilities - -```ts -capabilities: string[]; -``` - -List of capabilities granted (e.g. `['sign']`). - ##### createdAt ```ts @@ -5145,6 +5338,14 @@ optional author: string; Only include commits by this author. +##### identityBundlePath? + +```ts +optional identityBundlePath: string; +``` + +Path to an Auths identity-bundle JSON file for signer DID resolution. + ##### limit? ```ts @@ -5251,131 +5452,190 @@ Number of signatures that passed verification. *** -### ChainLink +### BundleAttestation -A single link in a verified attestation chain. +An attestation in the identity bundle's chain. + +Represents a 2-way key attestation between a primary identity and a device key. +Matches `auths/schemas/attestation-v1.json`. #### Properties -##### error? +##### capabilities? ```ts -optional error: string | null; +optional capabilities: string[]; ``` -Error message if this link failed, or `null`. +Capabilities this attestation grants. -##### issuer +##### delegated\_by? ```ts -issuer: string; +optional delegated_by: string | null; ``` -DID of the issuer at this link. +DID of the attestation that delegated authority. -##### subject +##### device\_public\_key ```ts -subject: string; +device_public_key: string; ``` -DID of the subject at this link. +Ed25519 public key of the device (32 bytes, hex-encoded). -##### valid +##### device\_signature ```ts -valid: boolean; +device_signature: string; ``` -Whether this link verified successfully. - -*** +Device's Ed25519 signature over the canonical attestation data (hex-encoded). -### ClientConfig +##### expires\_at? -Configuration for the [Auths](#auths) client. +```ts +optional expires_at: string | null; +``` -#### Properties +Expiration timestamp (ISO 8601). -##### passphrase? +##### identity\_signature? ```ts -optional passphrase: string; +optional identity_signature: string; ``` -Passphrase for key encryption. Can also be set via `AUTHS_PASSPHRASE` env var. +Issuer's Ed25519 signature over the canonical attestation data (hex-encoded). -##### repoPath? +##### issuer ```ts -optional repoPath: string; +issuer: string; ``` -Path to the Auths Git registry. Defaults to `'~/.auths'`. - -*** +DID of the issuing identity (`did:keri:...`). -### CommitSignResult +##### note? -Result of signing a Git commit. +```ts +optional note: string | null; +``` -#### Properties +Optional human-readable note. -##### method +##### payload? ```ts -method: string; +optional payload: unknown; ``` -Signing method identifier. +Optional arbitrary JSON payload. -##### namespace +##### revoked\_at? ```ts -namespace: string; +optional revoked_at: string | null; ``` -Namespace for the signature (e.g. `'auths'`). +Timestamp when the attestation was revoked (ISO 8601). -##### signaturePem +##### rid ```ts -signaturePem: string; +rid: string; ``` -PEM-encoded signature for the commit. +Record identifier linking this attestation to its storage ref. + +##### role? + +```ts +optional role: Role | null; +``` + +Role for org membership attestations. + +##### signer\_type? + +```ts +optional signer_type: SignerType | null; +``` + +The type of entity that produced this signature. + +##### subject + +```ts +subject: string; +``` + +DID of the device being attested (`did:key:z...`). + +##### timestamp? + +```ts +optional timestamp: string | null; +``` + +Creation timestamp (ISO 8601). + +##### version + +```ts +version: number; +``` + +Schema version. *** -### CompletePairingOptions +### ChainLink -Options for [PairingService.complete](#complete). +A single link in a verified attestation chain. #### Properties -##### capabilities? +##### error? ```ts -optional capabilities: string[]; +optional error: string | null; ``` -Capabilities to grant the device. +Error message if this link failed, or `null`. -##### deviceDid +##### issuer ```ts -deviceDid: string; +issuer: string; ``` -DID of the device to authorize. +DID of the issuer at this link. -##### devicePublicKeyHex +##### subject ```ts -devicePublicKeyHex: string; +subject: string; ``` -Hex-encoded Ed25519 public key of the device. +DID of the subject at this link. + +##### valid + +```ts +valid: boolean; +``` + +Whether this link verified successfully. + +*** + +### ClientConfig + +Configuration for the [Auths](#auths) client. + +#### Properties ##### passphrase? @@ -5383,7 +5643,79 @@ Hex-encoded Ed25519 public key of the device. optional passphrase: string; ``` -Override the client's passphrase. +Passphrase for key encryption. Can also be set via `AUTHS_PASSPHRASE` env var. + +##### repoPath? + +```ts +optional repoPath: string; +``` + +Path to the Auths Git registry. Defaults to `'~/.auths'`. + +*** + +### CommitResultLike + +A commit verification result (from Git commit verification). + +#### Properties + +##### commitSha + +```ts +commitSha: string; +``` + +Git commit SHA. + +##### isValid + +```ts +isValid: boolean; +``` + +Whether the commit signature is valid. + +##### signer? + +```ts +optional signer: string | null; +``` + +Hex-encoded public key of the signer, if identified. + +*** + +### CommitSignResult + +Result of signing a Git commit. + +#### Properties + +##### method + +```ts +method: string; +``` + +Signing method identifier. + +##### namespace + +```ts +namespace: string; +``` + +Namespace for the signature (e.g. `'auths'`). + +##### signaturePem + +```ts +signaturePem: string; +``` + +PEM-encoded signature for the commit. *** @@ -5585,21 +5917,13 @@ An agent delegated under an existing identity. #### Properties -##### attestation - -```ts -attestation: string; -``` - -JSON-serialized delegation attestation signed by the parent identity. - ##### did ```ts did: string; ``` -The delegated agent's DID (typically `did:key:z...`). +The agent's `did:keri:` AID — a delegated identifier the parent root anchored. ##### keyAlias @@ -5609,13 +5933,21 @@ keyAlias: string; Keychain alias for the agent's signing key. +##### prefix + +```ts +prefix: string; +``` + +The agent's KEL prefix (the delegated inception SAID). + ##### publicKey ```ts publicKey: string; ``` -Hex-encoded Ed25519 public key. +Hex-encoded public key. *** @@ -5679,6 +6011,26 @@ Previous expiration timestamp, or `null` if there was none. Context for policy evaluation. +**DID format requirements:** +- `issuer`: Must be a valid DID. Typically `did:keri:E...` for identity DIDs + (organizations, individuals) or `did:key:z...` for device DIDs. +- `subject`: Same format rules as `issuer`. For device attestations, this is + usually a `did:key:z...` device DID. + +Both `issuer` and `subject` are parsed into `CanonicalDid` values by the +Rust policy engine. The engine accepts both `did:keri:` and `did:key:` formats. +Invalid DID strings will cause evaluation to fail with a parse error. + +#### Example + +```typescript +const ctx: EvalContextOpts = { + issuer: 'did:keri:EOrg123', // organization identity + subject: 'did:key:z6MkDevice', // device key + capabilities: ['sign_commit'], +} +``` + #### Properties ##### capabilities? @@ -5729,6 +6081,9 @@ issuer: string; DID of the attestation issuer. +Must be a valid `did:keri:` or `did:key:` DID string. +Typically the organization or identity that issued the attestation. + ##### repo? ```ts @@ -5769,6 +6124,9 @@ subject: string; DID of the attestation subject. +Must be a valid `did:keri:` or `did:key:` DID string. +For device attestations, this is the device's `did:key:z...` DID. + *** ### ExtendDeviceOptions @@ -5883,6 +6241,135 @@ Path to the Git registry that stores this identity. *** +### IdentityBundle + +Identity bundle for stateless verification in CI/CD environments. + +Contains all the information needed to verify commit signatures without +requiring access to the identity repository or daemon. + +Matches `auths/schemas/identity-bundle-v1.json`. + +#### Example + +```typescript +import { readFileSync } from 'node:fs' +import type { IdentityBundle } from '@auths-dev/sdk' + +const bundle: IdentityBundle = JSON.parse( + readFileSync('.auths/identity-bundle.json', 'utf-8') +) +console.log(bundle.identity_did) // did:keri:E... +``` + +#### Properties + +##### attestation\_chain + +```ts +attestation_chain: BundleAttestation[]; +``` + +Chain of attestations linking the signing key to the identity. + +##### bundle\_timestamp + +```ts +bundle_timestamp: string; +``` + +UTC timestamp when this bundle was created (ISO 8601). + +##### identity\_did + +```ts +identity_did: string; +``` + +The DID of the identity (`did:keri:...`). + +##### max\_valid\_for\_secs + +```ts +max_valid_for_secs: number; +``` + +Maximum age in seconds before this bundle is considered stale. + +##### public\_key\_hex + +```ts +public_key_hex: string; +``` + +The public key in hex format for signature verification (32 bytes, hex). + +*** + +### IdentityBundleInfo + +Parsed identity bundle metadata. + +#### Properties + +##### deviceCount + +```ts +deviceCount: number; +``` + +Number of device attestations in the chain. + +##### did + +```ts +did: string; +``` + +Identity DID (`did:keri:...`). + +##### label + +```ts +label: string | null; +``` + +Human-readable identity label. + +##### publicKeyHex + +```ts +publicKeyHex: string; +``` + +Hex-encoded Ed25519 public key. + +*** + +### InMemoryKeypair + +#### Properties + +##### did + +```ts +did: string; +``` + +##### privateKeyHex + +```ts +privateKeyHex: string; +``` + +##### publicKeyHex + +```ts +publicKeyHex: string; +``` + +*** + ### JoinPairingOptions Options for [PairingService.join](#join). @@ -6001,14 +6488,6 @@ An organization member record. #### Properties -##### attestationRid - -```ts -attestationRid: string; -``` - -Resource identifier of the membership attestation. - ##### capabilities ```ts @@ -6025,26 +6504,34 @@ expiresAt: string | null; Expiration timestamp (RFC 3339), or `null` if no expiry. -##### issuerDid +##### memberDid ```ts -issuerDid: string; +memberDid: string; ``` -DID of the admin who added this member. +Delegated `did:keri:` AID minted by the org for this member. -##### memberDid +##### memberPrefix ```ts -memberDid: string; +memberPrefix: string; ``` -DID of the member. +KERI prefix of the member's delegated identity. -##### revoked +##### orgDid ```ts -revoked: boolean; +orgDid: string; +``` + +DID of the organization that owns this membership. + +##### revoked + +```ts +revoked: boolean; ``` Whether the membership has been revoked. @@ -6131,38 +6618,6 @@ Hex-encoded Ed25519 public key of the device. *** -### PairingResult - -Result of completing a pairing and authorizing the device. - -#### Properties - -##### attestationRid - -```ts -attestationRid: string; -``` - -Resource identifier of the authorization attestation. - -##### deviceDid - -```ts -deviceDid: string; -``` - -DID of the paired device. - -##### deviceName - -```ts -deviceName: string | null; -``` - -Optional name of the device, or `null`. - -*** - ### PairingSession An active pairing session awaiting a device connection. @@ -6399,23 +6854,7 @@ Options for [OrgService.revokeMember](#revokemember). memberDid: string; ``` -DID of the member to revoke. - -##### memberPublicKeyHex? - -```ts -optional memberPublicKeyHex: string; -``` - -Hex-encoded public key of the member. - -##### note? - -```ts -optional note: string; -``` - -Optional revocation note. +Delegated DID of the member to revoke. ##### orgDid @@ -6593,6 +7032,14 @@ Options for [ArtifactService.signBytes](#signbytes). #### Properties +##### commitSha? + +```ts +optional commitSha: string; +``` + +Optional commit SHA to bind the attestation to. + ##### data ```ts @@ -6641,6 +7088,14 @@ Options for [ArtifactService.sign](#sign). #### Properties +##### commitSha? + +```ts +optional commitSha: string; +``` + +Optional commit SHA to bind the attestation to. + ##### expiresInDays? ```ts @@ -6937,14 +7392,6 @@ attestations: string[]; Array of JSON-serialized attestations (leaf to root). -##### requiredCapability? - -```ts -optional requiredCapability: string; -``` - -Optional capability the leaf attestation must grant. - ##### rootKey ```ts @@ -6993,14 +7440,6 @@ issuerKey: string; Hex-encoded Ed25519 public key of the issuer. -##### requiredCapability? - -```ts -optional requiredCapability: string; -``` - -Optional capability the attestation must grant. - *** ### WaitForPairingResponseOptions @@ -7105,333 +7544,935 @@ publicKeyHex: string; Hex-encoded Ed25519 public key of the witness. -## Variables +## Type Aliases -### version() +### CanonicalDid ```ts -const version: () => string = native.version; +type CanonicalDid = Brand; ``` -#### Returns +Device DID — always `did:key:z...` format. -`string` +Represents a device's ephemeral key-based identity. Used for device +attestations and signing keys. Parse with [parseDeviceDid](#parsedevicedid). -## Functions +*** -### compilePolicy() +### IdentityDID ```ts -function compilePolicy(policyJson): string; +type IdentityDID = Brand; ``` -Compiles a raw policy JSON string for use with [evaluatePolicy](#evaluatepolicy). +Identity DID — always `did:keri:...` format. -#### Parameters +Represents a KERI-based decentralized identity. Used for organizations +and individual identities. Parse with [parseIdentityDid](#parseidentitydid). -##### policyJson +*** -`string` +### Outcome -JSON string of the policy expression. +```ts +type Outcome = typeof Outcome[keyof typeof Outcome]; +``` -#### Returns +Authorization outcome from a policy evaluation. -`string` +Values match the Rust `Outcome` enum in `auths-policy/src/decision.rs`. -Compiled policy JSON. +*** -#### Throws +### ReasonCode -[AuthsError](#authserror) if the policy is invalid. +```ts +type ReasonCode = typeof ReasonCode[keyof typeof ReasonCode]; +``` + +Machine-readable reason code for stable logging and alerting. + +Values match the Rust `ReasonCode` enum in `auths-policy/src/decision.rs`. *** -### evaluatePolicy() +### Role ```ts -function evaluatePolicy(compiledPolicyJson, context): PolicyDecision; +type Role = typeof Role[keyof typeof Role]; ``` -Evaluates a compiled policy against an attestation context. - -#### Parameters +Organization member role. -##### compiledPolicyJson +*** -`string` +### SignerType -Compiled policy from [compilePolicy](#compilepolicy) or [PolicyBuilder.build](#build). +```ts +type SignerType = typeof SignerType[keyof typeof SignerType]; +``` -##### context +The type of entity that produced a signature. -[`EvalContextOpts`](#evalcontextopts) +*** -The evaluation context. +### TrustLevel -#### Returns +```ts +type TrustLevel = typeof TrustLevel[keyof typeof TrustLevel]; +``` -[`PolicyDecision`](#policydecision) +Trust level for a pinned identity. -The policy decision with `allowed`/`denied` convenience booleans. +Values match the Rust `TrustLevel` enum in `auths-core/src/trust/pinned.rs`. -#### Throws +## Variables -[AuthsError](#authserror) if evaluation fails. +### generateInmemoryKeypair() -#### Example +```ts +const generateInmemoryKeypair: () => InMemoryKeypair = native.generateInmemoryKeypair; +``` -```typescript -import { compilePolicy, evaluatePolicy } from '@auths-dev/node' +#### Returns -const compiled = compilePolicy(policyJson) -const decision = evaluatePolicy(compiled, { - issuer: 'did:keri:EOrg', - subject: 'did:key:zDevice', -}) -``` +[`InMemoryKeypair`](#inmemorykeypair) *** -### mapNativeError() +### Outcome ```ts -function mapNativeError(err, defaultCls?): AuthsError; +const Outcome: object; ``` -Maps a native napi-rs error into a typed [AuthsError](#authserror) subclass. +Authorization outcome from a policy evaluation. -Parses the `[AUTHS_CODE] message` format emitted by the Rust layer -and instantiates the appropriate error class with a machine-readable code. +Values match the Rust `Outcome` enum in `auths-policy/src/decision.rs`. -#### Parameters +#### Type Declaration -##### err +##### Allow -`unknown` +```ts +readonly Allow: "Allow" = 'Allow'; +``` -The raw error from the native binding. +##### Deny -##### defaultCls? +```ts +readonly Deny: "Deny" = 'Deny'; +``` -(`message`, `code`) => [`AuthsError`](#authserror) +##### Indeterminate -Fallback error class when the code is unrecognized. +```ts +readonly Indeterminate: "Indeterminate" = 'Indeterminate'; +``` -#### Returns +##### MissingCredential -[`AuthsError`](#authserror) +```ts +readonly MissingCredential: "MissingCredential" = 'MissingCredential'; +``` -A typed [AuthsError](#authserror) instance. +##### RequiresApproval + +```ts +readonly RequiresApproval: "RequiresApproval" = 'RequiresApproval'; +``` *** -### verifyAttestation() +### ReasonCode ```ts -function verifyAttestation(attestationJson, issuerPkHex): Promise; +const ReasonCode: object; ``` -Verifies a single attestation against an issuer's public key. - -#### Parameters +Machine-readable reason code for stable logging and alerting. -##### attestationJson +Values match the Rust `ReasonCode` enum in `auths-policy/src/decision.rs`. -`string` +#### Type Declaration -JSON-serialized attestation. +##### AllChecksPassed -##### issuerPkHex +```ts +readonly AllChecksPassed: "AllChecksPassed" = 'AllChecksPassed'; +``` -`string` +##### ApprovalAlreadyUsed -Hex-encoded Ed25519 public key of the issuer. +```ts +readonly ApprovalAlreadyUsed: "ApprovalAlreadyUsed" = 'ApprovalAlreadyUsed'; +``` -#### Returns +##### ApprovalExpired -`Promise`\<[`VerificationResult`](#verificationresult)\> +```ts +readonly ApprovalExpired: "ApprovalExpired" = 'ApprovalExpired'; +``` -The verification result. +##### ApprovalGranted -#### Throws +```ts +readonly ApprovalGranted: "ApprovalGranted" = 'ApprovalGranted'; +``` -[VerificationError](#verificationerror) if verification encounters an error. +##### ApprovalRequestMismatch -#### Example +```ts +readonly ApprovalRequestMismatch: "ApprovalRequestMismatch" = 'ApprovalRequestMismatch'; +``` -```typescript -import { verifyAttestation } from '@auths-dev/node' +##### ApprovalRequired -const result = await verifyAttestation(attestationJson, publicKeyHex) -console.log(result.valid) // true +```ts +readonly ApprovalRequired: "ApprovalRequired" = 'ApprovalRequired'; ``` -*** - -### verifyAttestationWithCapability() +##### AttrMismatch ```ts -function verifyAttestationWithCapability( - attestationJson, - issuerPkHex, -requiredCapability): Promise; +readonly AttrMismatch: "AttrMismatch" = 'AttrMismatch'; ``` -Verifies a single attestation with a required capability check. - -#### Parameters +##### CapabilityMissing -##### attestationJson +```ts +readonly CapabilityMissing: "CapabilityMissing" = 'CapabilityMissing'; +``` -`string` +##### CapabilityPresent -JSON-serialized attestation. +```ts +readonly CapabilityPresent: "CapabilityPresent" = 'CapabilityPresent'; +``` -##### issuerPkHex +##### ChainTooDeep -`string` +```ts +readonly ChainTooDeep: "ChainTooDeep" = 'ChainTooDeep'; +``` -Hex-encoded Ed25519 public key of the issuer. +##### CombinatorResult -##### requiredCapability +```ts +readonly CombinatorResult: "CombinatorResult" = 'CombinatorResult'; +``` -`string` +##### DelegationMismatch -Capability the attestation must grant. +```ts +readonly DelegationMismatch: "DelegationMismatch" = 'DelegationMismatch'; +``` -#### Returns +##### Expired -`Promise`\<[`VerificationResult`](#verificationresult)\> +```ts +readonly Expired: "Expired" = 'Expired'; +``` -The verification result. +##### InsufficientTtl -#### Throws +```ts +readonly InsufficientTtl: "InsufficientTtl" = 'InsufficientTtl'; +``` -[VerificationError](#verificationerror) if verification fails. +##### IssuedTooLongAgo -*** +```ts +readonly IssuedTooLongAgo: "IssuedTooLongAgo" = 'IssuedTooLongAgo'; +``` -### verifyAtTime() +##### IssuerMatch ```ts -function verifyAtTime( - attestationJson, - issuerPkHex, -atRfc3339): Promise; +readonly IssuerMatch: "IssuerMatch" = 'IssuerMatch'; ``` -Verifies a single attestation at a specific point in time. - -#### Parameters +##### IssuerMismatch -##### attestationJson +```ts +readonly IssuerMismatch: "IssuerMismatch" = 'IssuerMismatch'; +``` -`string` +##### MissingField -JSON-serialized attestation. +```ts +readonly MissingField: "MissingField" = 'MissingField'; +``` -##### issuerPkHex +##### RecursionExceeded -`string` +```ts +readonly RecursionExceeded: "RecursionExceeded" = 'RecursionExceeded'; +``` -Hex-encoded Ed25519 public key of the issuer. +##### Revoked -##### atRfc3339 +```ts +readonly Revoked: "Revoked" = 'Revoked'; +``` -`string` +##### RoleMismatch -RFC 3339 timestamp to verify at. +```ts +readonly RoleMismatch: "RoleMismatch" = 'RoleMismatch'; +``` -#### Returns +##### ScopeMismatch -`Promise`\<[`VerificationResult`](#verificationresult)\> +```ts +readonly ScopeMismatch: "ScopeMismatch" = 'ScopeMismatch'; +``` -The verification result. +##### ShortCircuit -#### Throws +```ts +readonly ShortCircuit: "ShortCircuit" = 'ShortCircuit'; +``` -[VerificationError](#verificationerror) if verification fails. +##### SignerTypeMatch -*** +```ts +readonly SignerTypeMatch: "SignerTypeMatch" = 'SignerTypeMatch'; +``` -### verifyAtTimeWithCapability() +##### SignerTypeMismatch ```ts -function verifyAtTimeWithCapability( - attestationJson, - issuerPkHex, - atRfc3339, -requiredCapability): Promise; +readonly SignerTypeMismatch: "SignerTypeMismatch" = 'SignerTypeMismatch'; ``` -Verifies an attestation at a specific time with a required capability. +##### Unconditional -#### Parameters +```ts +readonly Unconditional: "Unconditional" = 'Unconditional'; +``` + +##### WitnessQuorumNotMet + +```ts +readonly WitnessQuorumNotMet: "WitnessQuorumNotMet" = 'WitnessQuorumNotMet'; +``` + +##### WorkloadMismatch + +```ts +readonly WorkloadMismatch: "WorkloadMismatch" = 'WorkloadMismatch'; +``` + +*** + +### Role + +```ts +const Role: object; +``` + +Organization member role. + +#### Type Declaration + +##### Admin + +```ts +readonly Admin: "admin" = 'admin'; +``` + +##### Member + +```ts +readonly Member: "member" = 'member'; +``` + +##### Readonly + +```ts +readonly Readonly: "readonly" = 'readonly'; +``` + +*** + +### signActionRaw() + +```ts +const signActionRaw: (privateKeyHex, actionType, payloadJson, identityDid) => string = native.signActionRaw; +``` + +#### Parameters + +##### privateKeyHex + +`string` + +##### actionType + +`string` + +##### payloadJson + +`string` + +##### identityDid + +`string` + +#### Returns + +`string` + +*** + +### signArtifactBytesRaw() + +```ts +const signArtifactBytesRaw: (data, privateKeyHex, identityDid, expiresIn?, note?) => NapiArtifactResult = native.signArtifactBytesRaw; +``` + +#### Parameters + +##### data + +`Buffer` + +##### privateKeyHex + +`string` + +##### identityDid + +`string` + +##### expiresIn? + +`number` | `null` + +##### note? + +`string` | `null` + +#### Returns + +`NapiArtifactResult` + +*** + +### signBytesRaw() + +```ts +const signBytesRaw: (privateKeyHex, message) => string = native.signBytesRaw; +``` + +#### Parameters + +##### privateKeyHex + +`string` + +##### message + +`Buffer` + +#### Returns + +`string` + +*** + +### SignerType + +```ts +const SignerType: object; +``` + +The type of entity that produced a signature. + +#### Type Declaration + +##### Agent + +```ts +readonly Agent: "Agent" = 'Agent'; +``` + +##### Human + +```ts +readonly Human: "Human" = 'Human'; +``` + +##### Workload + +```ts +readonly Workload: "Workload" = 'Workload'; +``` + +*** + +### TrustLevel + +```ts +const TrustLevel: object; +``` + +Trust level for a pinned identity. + +Values match the Rust `TrustLevel` enum in `auths-core/src/trust/pinned.rs`. + +#### Type Declaration + +##### Manual + +```ts +readonly Manual: "manual" = 'manual'; +``` + +Manually pinned via CLI or `--issuer-pk`. + +##### OrgPolicy + +```ts +readonly OrgPolicy: "org_policy" = 'org_policy'; +``` + +Loaded from roots.json org policy file. + +##### Tofu + +```ts +readonly Tofu: "tofu" = 'tofu'; +``` + +Accepted on first use (interactive prompt). + +*** + +### verifyActionEnvelope() + +```ts +const verifyActionEnvelope: (envelopeJson, publicKeyHex) => object = native.verifyActionEnvelope; +``` + +#### Parameters + +##### envelopeJson + +`string` + +##### publicKeyHex + +`string` + +#### Returns + +`object` + +##### error? + +```ts +optional error: string | null; +``` + +##### errorCode? + +```ts +optional errorCode: string | null; +``` + +##### valid + +```ts +valid: boolean; +``` + +*** + +### version() + +```ts +const version: () => string = native.version; +``` + +#### Returns + +`string` + +*** + +### WellKnownCapability + +```ts +const WellKnownCapability: object; +``` + +Well-known capability identifiers. + +Custom capabilities can be any valid string (alphanumeric + `:` + `-` + `_`, max 64 chars). +The `auths:` prefix is reserved. + +#### Type Declaration + +##### ManageMembers + +```ts +readonly ManageMembers: "manage_members" = 'manage_members'; +``` + +##### RotateKeys + +```ts +readonly RotateKeys: "rotate_keys" = 'rotate_keys'; +``` + +##### SignCommit + +```ts +readonly SignCommit: "sign_commit" = 'sign_commit'; +``` + +##### SignRelease + +```ts +readonly SignRelease: "sign_release" = 'sign_release'; +``` + +## Functions + +### compilePolicy() + +```ts +function compilePolicy(policyJson): string; +``` + +Compiles a raw policy JSON string for use with [evaluatePolicy](#evaluatepolicy). + +#### Parameters + +##### policyJson + +`string` + +JSON string of the policy expression. + +#### Returns + +`string` + +Compiled policy JSON. + +#### Throws + +[AuthsError](#authserror) if the policy is invalid. + +*** + +### evalContextFromCommitResult() + +```ts +function evalContextFromCommitResult( + commitResult, + issuer, + capabilities?): EvalContextOpts; +``` + +Build an EvalContext options object from a commit verification result. + +Extracts the signer hex from the commit result and converts it to a +`did:key:` DID for use as the `subject` field. + +#### Parameters + +##### commitResult + +[`CommitResultLike`](#commitresultlike) + +A commit verification result with a `signer` hex field. + +##### issuer + +`string` + +The issuer DID (`did:keri:...`). + +##### capabilities? + +`string`[] + +Optional capability list to include. + +#### Returns + +[`EvalContextOpts`](#evalcontextopts) + +An EvalContextOpts suitable for `evaluatePolicy()`. + +#### Example + +```typescript +const ctx = evalContextFromCommitResult(cr, org.orgDid, ['sign_commit']) +const decision = evaluatePolicy(compiled, ctx) +``` -##### attestationJson +*** + +### evaluatePolicy() + +```ts +function evaluatePolicy(compiledPolicyJson, context): PolicyDecision; +``` + +Evaluates a compiled policy against an attestation context. + +#### Parameters + +##### compiledPolicyJson `string` -JSON-serialized attestation. +Compiled policy from [compilePolicy](#compilepolicy) or [PolicyBuilder.build](#build). -##### issuerPkHex +##### context + +[`EvalContextOpts`](#evalcontextopts) + +The evaluation context. + +#### Returns + +[`PolicyDecision`](#policydecision) + +The policy decision with `allowed`/`denied` convenience booleans. + +#### Throws + +[AuthsError](#authserror) if evaluation fails. + +#### Example + +```typescript +import { compilePolicy, evaluatePolicy } from '@auths-dev/sdk' + +const compiled = compilePolicy(policyJson) +const decision = evaluatePolicy(compiled, { + issuer: 'did:keri:EOrg', + subject: 'did:key:zDevice', +}) +``` + +*** + +### isAdmin() + +```ts +function isAdmin(member): boolean; +``` + +Check whether an organization member has admin role. + +#### Parameters + +##### member + +[`OrgMember`](#orgmember) + +The organization member to check. + +#### Returns + +`boolean` + +`true` if the member's role is `'admin'`. + +*** + +### mapNativeError() + +```ts +function mapNativeError(err, defaultCls?): AuthsError; +``` + +Maps a native napi-rs error into a typed [AuthsError](#authserror) subclass. + +Parses the `[AUTHS_CODE] message` format emitted by the Rust layer +and instantiates the appropriate error class with a machine-readable code. + +#### Parameters + +##### err + +`unknown` + +The raw error from the native binding. + +##### defaultCls? + +(`message`, `code`) => [`AuthsError`](#authserror) + +Fallback error class when the code is unrecognized. + +#### Returns + +[`AuthsError`](#authserror) + +A typed [AuthsError](#authserror) instance. + +*** + +### parseDeviceDid() + +```ts +function parseDeviceDid(raw): CanonicalDid; +``` + +Parse and validate a device DID string. + +#### Parameters + +##### raw `string` -Hex-encoded Ed25519 public key of the issuer. +A DID string that should start with `did:key:z`. -##### atRfc3339 +#### Returns + +[`CanonicalDid`](#canonicaldid) + +The validated DID as a `CanonicalDid` branded type. + +#### Throws + +Error if the string does not start with `did:key:z`. + +#### Example + +```typescript +const did = parseDeviceDid('did:key:z6MkDevice...') +// did is typed as CanonicalDid +``` + +*** + +### parseIdentityBundle() + +```ts +function parseIdentityBundle(path): Record; +``` + +Parse an Auths identity-bundle JSON file. + +#### Parameters + +##### path `string` -RFC 3339 timestamp to verify at. +Path to the identity-bundle JSON file. + +#### Returns + +`Record`\<`string`, `unknown`\> + +The parsed bundle object. + +#### Example + +```typescript +const bundle = parseIdentityBundle('.auths/identity-bundle.json') +console.log(bundle.did) +``` + +*** + +### parseIdentityBundleInfo() + +```ts +function parseIdentityBundleInfo(path): IdentityBundleInfo; +``` -##### requiredCapability +Parse an identity bundle into a typed [IdentityBundleInfo](#identitybundleinfo). + +#### Parameters + +##### path `string` -Capability the attestation must grant. +Path to the identity-bundle JSON file. #### Returns -`Promise`\<[`VerificationResult`](#verificationresult)\> +[`IdentityBundleInfo`](#identitybundleinfo) -The verification result. +Typed bundle metadata. + +*** + +### parseIdentityDid() + +```ts +function parseIdentityDid(raw): IdentityDID; +``` + +Parse and validate an identity DID string. + +#### Parameters + +##### raw + +`string` + +A DID string that should start with `did:keri:`. + +#### Returns + +[`IdentityDID`](#identitydid-12) + +The validated DID as an `IdentityDID` branded type. #### Throws -[VerificationError](#verificationerror) if verification fails. +Error if the string does not start with `did:keri:`. + +#### Example + +```typescript +const did = parseIdentityDid('did:keri:EOrg123') +// did is typed as IdentityDID +``` *** -### verifyChain() +### verifyAttestation() ```ts -function verifyChain(attestationsJson, rootPkHex): Promise; +function verifyAttestation(attestationJson, issuerPkHex): Promise; ``` -Verifies an attestation chain from leaf to root. +Verifies a single attestation against an issuer's public key. #### Parameters -##### attestationsJson +##### attestationJson -`string`[] +`string` -Array of JSON-serialized attestations (leaf to root). +JSON-serialized attestation. -##### rootPkHex +##### issuerPkHex `string` -Hex-encoded Ed25519 public key of the root identity. +Hex-encoded Ed25519 public key of the issuer. #### Returns -`Promise`\<[`VerificationReport`](#verificationreport)\> +`Promise`\<[`VerificationResult`](#verificationresult)\> -The verification report with chain link details. +The verification result. #### Throws @@ -7440,24 +8481,64 @@ The verification report with chain link details. #### Example ```typescript -import { verifyChain } from '@auths-dev/node' +import { verifyAttestation } from '@auths-dev/sdk' -const report = await verifyChain(attestationChain, rootPublicKeyHex) -console.log(report.status.statusType) // 'Valid' +const result = await verifyAttestation(attestationJson, publicKeyHex) +console.log(result.valid) // true +``` + +*** + +### verifyAtTime() + +```ts +function verifyAtTime( + attestationJson, + issuerPkHex, +atRfc3339): Promise; ``` +Verifies a single attestation at a specific point in time. + +#### Parameters + +##### attestationJson + +`string` + +JSON-serialized attestation. + +##### issuerPkHex + +`string` + +Hex-encoded Ed25519 public key of the issuer. + +##### atRfc3339 + +`string` + +RFC 3339 timestamp to verify at. + +#### Returns + +`Promise`\<[`VerificationResult`](#verificationresult)\> + +The verification result. + +#### Throws + +[VerificationError](#verificationerror) if verification fails. + *** -### verifyChainWithCapability() +### verifyChain() ```ts -function verifyChainWithCapability( - attestationsJson, - rootPkHex, -requiredCapability): Promise; +function verifyChain(attestationsJson, rootPkHex): Promise; ``` -Verifies an attestation chain with a required capability at the leaf. +Verifies an attestation chain from leaf to root. #### Parameters @@ -7473,21 +8554,24 @@ Array of JSON-serialized attestations (leaf to root). Hex-encoded Ed25519 public key of the root identity. -##### requiredCapability - -`string` - -Capability the leaf attestation must grant. - #### Returns `Promise`\<[`VerificationReport`](#verificationreport)\> -The verification report. +The verification report with chain link details. #### Throws -[VerificationError](#verificationerror) if verification fails. +[VerificationError](#verificationerror) if verification encounters an error. + +#### Example + +```typescript +import { verifyChain } from '@auths-dev/sdk' + +const report = await verifyChain(attestationChain, rootPublicKeyHex) +console.log(report.status.statusType) // 'Valid' +``` *** @@ -7495,8 +8579,8 @@ The verification report. ```ts function verifyChainWithWitnesses( - attestationsJson, - rootPkHex, + attestationsJson, + rootPkHex, witnesses): Promise; ``` @@ -7535,7 +8619,7 @@ The verification report. #### Example ```typescript -import { verifyChainWithWitnesses } from '@auths-dev/node' +import { verifyChainWithWitnesses } from '@auths-dev/sdk' const report = await verifyChainWithWitnesses(chain, rootKey, { receipts: witnessReceipts, @@ -7550,9 +8634,9 @@ const report = await verifyChainWithWitnesses(chain, rootKey, { ```ts function verifyDeviceAuthorization( - identityDid, - deviceDid, - attestationsJson, + identityDid, + deviceDid, + attestationsJson, identityPkHex): Promise; ``` diff --git a/packages/auths-node/Cargo.toml b/packages/auths-node/Cargo.toml index 772d49a91..32ed2ebe2 100644 --- a/packages/auths-node/Cargo.toml +++ b/packages/auths-node/Cargo.toml @@ -21,6 +21,8 @@ auths-core = { path = "../../crates/auths-core", features = ["keychain-file-fall auths-id = { path = "../../crates/auths-id" } auths-crypto = { path = "../../crates/auths-crypto" } auths-verifier = { path = "../../crates/auths-verifier" } +auths-keri = { path = "../../crates/auths-keri" } +auths-rp = { path = "../../crates/auths-rp" } auths-storage = { path = "../../crates/auths-storage", features = ["backend-git"] } auths-policy = { path = "../../crates/auths-policy" } auths-pairing-daemon = { path = "../../crates/auths-pairing-daemon" } @@ -29,6 +31,8 @@ auths-infra-git = { path = "../../crates/auths-infra-git" } # Utilities serde = { version = "1", features = ["derive"] } serde_json = "1" +base64 = "0.22" +rand = "0.8" tokio = { version = "1", features = ["sync", "net"] } reqwest = { version = "0.13.2", default-features = false, features = ["rustls", "json"] } hex = "0.4" diff --git a/packages/auths-node/__test__/integration.spec.ts b/packages/auths-node/__test__/integration.spec.ts index 7838c90f6..f5ebd9727 100644 --- a/packages/auths-node/__test__/integration.spec.ts +++ b/packages/auths-node/__test__/integration.spec.ts @@ -64,9 +64,9 @@ describe('identity lifecycle', () => { name: 'ci-bot', capabilities: ['sign'], }) - expect(agent.did).toMatch(/^did:key:/) + expect(agent.did).toMatch(/^did:keri:/) + expect(agent.prefix).toBeDefined() expect(agent.keyAlias).toBeDefined() - expect(agent.attestation).toBeDefined() }) it('creates standalone agent', () => { diff --git a/packages/auths-node/conformance/credential_revoked.json b/packages/auths-node/conformance/credential_revoked.json new file mode 100644 index 000000000..95936597b --- /dev/null +++ b/packages/auths-node/conformance/credential_revoked.json @@ -0,0 +1 @@ +{"schemaVersion":1,"credential":{"acdc":{"v":"ACDC10JSON00018d_","d":"ECMW25uAAgYIRpJdH5Cu_upswgwdXr-w0T39T8cwUBHB","i":"EAe5o2mlyMDBdE2M7smLqnbELCpDCpG-2s-W6CaYRSqy","ri":"EOeqXxxstDjRzGgPmyXEssdrUDi0sAQHycWK4obwLatj","s":"ECJN2IjRhuWpNRFJaIhLbYAAbB-DjILDaQ3dEX7rfA9_","a":{"d":"EBKAMIY1Gy3FE-jIzKPkwTU6Q2aON3sxgnctvvWoHi2S","i":"EHolder000000000000000000000000000000000000","dt":"2025-01-01T00:00:00.000000+00:00","capability":"sign"}},"signatureB64":"QJx5OMheSb26g5H0rmG16gsg6o/S9uQ6JAW4gkeHsyFT4GAB9DiFSssZQiOQ1NUHFd9HyXZCkA1MMITwRRZ7XQ=="},"issuerKel":[{"v":"KERI10JSON00012f_","t":"icp","d":"EAe5o2mlyMDBdE2M7smLqnbELCpDCpG-2s-W6CaYRSqy","i":"EAe5o2mlyMDBdE2M7smLqnbELCpDCpG-2s-W6CaYRSqy","s":"0","kt":"1","k":["1AAJAm_wO5SSQc4drdQ1GeaWDgqFtBppoFwygQOqK84VlMoW"],"nt":"1","n":["EGn7g8vcW7roa2PjmfRqlRlLrEeov5JabW2-z9EFnt4Q"],"bt":"0","b":[],"c":[],"a":[]},{"v":"KERI10JSON00013a_","t":"ixn","d":"EH3BdhrtRpJT9GOXg5CTmYbRk-2gp6Jvv0L5boJJ6jA6","i":"EAe5o2mlyMDBdE2M7smLqnbELCpDCpG-2s-W6CaYRSqy","s":"1","p":"EAe5o2mlyMDBdE2M7smLqnbELCpDCpG-2s-W6CaYRSqy","a":[{"i":"EOeqXxxstDjRzGgPmyXEssdrUDi0sAQHycWK4obwLatj","s":"0","d":"EOeqXxxstDjRzGgPmyXEssdrUDi0sAQHycWK4obwLatj"}]},{"v":"KERI10JSON00013a_","t":"ixn","d":"EEVK_HJGjiUXEzH6Jqf8z1wiEqKu79WD6i_UJjDnKgHo","i":"EAe5o2mlyMDBdE2M7smLqnbELCpDCpG-2s-W6CaYRSqy","s":"2","p":"EH3BdhrtRpJT9GOXg5CTmYbRk-2gp6Jvv0L5boJJ6jA6","a":[{"i":"EOeqXxxstDjRzGgPmyXEssdrUDi0sAQHycWK4obwLatj","s":"0","d":"ELJZXPOYRZMpuTXnyZAy9wutcqgCc5LEytVClQB2EILq"}]},{"v":"KERI10JSON00013a_","t":"ixn","d":"EFG5QpWFuF1lGouvFO_eKv072CfxOntIe7Nl9E-WrtNA","i":"EAe5o2mlyMDBdE2M7smLqnbELCpDCpG-2s-W6CaYRSqy","s":"3","p":"EEVK_HJGjiUXEzH6Jqf8z1wiEqKu79WD6i_UJjDnKgHo","a":[{"i":"EOeqXxxstDjRzGgPmyXEssdrUDi0sAQHycWK4obwLatj","s":"1","d":"EMy4efrAVOyNkhvxek2N5xmOnnqeK6GhP9Fqthh8UYKh"}]}],"issuerKelAttachmentsB64":["LUFBQkFBQ3p4LXVzWm41TlNhdzlXREx3RDBuOW1LN085ODFoZUpOdm95MFZqelhWV0J3RGhrY3laYTh2cmtYWnBvcnFTdFNGNVMzV3pCdDNkdkhpZWs1akoyYjg=","LUFBQkFBRElTY1JLU1poeTgxU2Fkc3ExS3pwc1B2RHBud01nTkIxMGhjdnBRMWxlelhPalFLbGx4ODFkZndyektvcEJ3U0haalN1bkhBUWRlZnhfeVAxaFB1Y2o=","LUFBQkFBQXVBNnB4QUpaMWZnMV8xNFd4ZENUSWNuTGJ1UFBGWm16bkRVaFhkSEJDeHhrSjVXV3N1V19xVmEwNkFFZzRRUHpzbUI3MWUxRC0tNWlvUC1qdkp6TUo=","LUFBQkFBQ3hLUXozV2FYWTZmQU1RRFh1a2xZcUxVLWZsd3EyeHZIb2ZybE9kRnhQYVZCbVBYVFpMUzFHMjAzLXBNY3lzdk5BOWRpSE5QNVF6N3ZvS2J3dTRxNjQ="],"tel":[{"v":"KERI10JSON0000ff_","t":"vcp","d":"EOeqXxxstDjRzGgPmyXEssdrUDi0sAQHycWK4obwLatj","i":"EOeqXxxstDjRzGgPmyXEssdrUDi0sAQHycWK4obwLatj","ii":"EAe5o2mlyMDBdE2M7smLqnbELCpDCpG-2s-W6CaYRSqy","s":"0","c":["NB"],"bt":"0","b":[],"n":"0AAHBwcHBwcHBwcHBwcHBwcH"},{"v":"KERI10JSON0000ed_","t":"iss","d":"ELJZXPOYRZMpuTXnyZAy9wutcqgCc5LEytVClQB2EILq","i":"ECMW25uAAgYIRpJdH5Cu_upswgwdXr-w0T39T8cwUBHB","s":"0","ri":"EOeqXxxstDjRzGgPmyXEssdrUDi0sAQHycWK4obwLatj","dt":"2025-01-01T00:00:00.000000+00:00"},{"v":"KERI10JSON000120_","t":"rev","d":"EMy4efrAVOyNkhvxek2N5xmOnnqeK6GhP9Fqthh8UYKh","i":"ECMW25uAAgYIRpJdH5Cu_upswgwdXr-w0T39T8cwUBHB","s":"1","ri":"EOeqXxxstDjRzGgPmyXEssdrUDi0sAQHycWK4obwLatj","p":"ELJZXPOYRZMpuTXnyZAy9wutcqgCc5LEytVClQB2EILq","dt":"2025-01-01T00:00:00.000000+00:00"}],"receipts":[],"witnessPolicy":"warn","now":"2025-06-01T00:00:00+00:00"} diff --git a/packages/auths-node/conformance/credential_valid.json b/packages/auths-node/conformance/credential_valid.json new file mode 100644 index 000000000..dc7cfaaec --- /dev/null +++ b/packages/auths-node/conformance/credential_valid.json @@ -0,0 +1 @@ +{"schemaVersion":1,"credential":{"acdc":{"v":"ACDC10JSON00018d_","d":"ECMW25uAAgYIRpJdH5Cu_upswgwdXr-w0T39T8cwUBHB","i":"EAe5o2mlyMDBdE2M7smLqnbELCpDCpG-2s-W6CaYRSqy","ri":"EOeqXxxstDjRzGgPmyXEssdrUDi0sAQHycWK4obwLatj","s":"ECJN2IjRhuWpNRFJaIhLbYAAbB-DjILDaQ3dEX7rfA9_","a":{"d":"EBKAMIY1Gy3FE-jIzKPkwTU6Q2aON3sxgnctvvWoHi2S","i":"EHolder000000000000000000000000000000000000","dt":"2025-01-01T00:00:00.000000+00:00","capability":"sign"}},"signatureB64":"QJx5OMheSb26g5H0rmG16gsg6o/S9uQ6JAW4gkeHsyFT4GAB9DiFSssZQiOQ1NUHFd9HyXZCkA1MMITwRRZ7XQ=="},"issuerKel":[{"v":"KERI10JSON00012f_","t":"icp","d":"EAe5o2mlyMDBdE2M7smLqnbELCpDCpG-2s-W6CaYRSqy","i":"EAe5o2mlyMDBdE2M7smLqnbELCpDCpG-2s-W6CaYRSqy","s":"0","kt":"1","k":["1AAJAm_wO5SSQc4drdQ1GeaWDgqFtBppoFwygQOqK84VlMoW"],"nt":"1","n":["EGn7g8vcW7roa2PjmfRqlRlLrEeov5JabW2-z9EFnt4Q"],"bt":"0","b":[],"c":[],"a":[]},{"v":"KERI10JSON00013a_","t":"ixn","d":"EH3BdhrtRpJT9GOXg5CTmYbRk-2gp6Jvv0L5boJJ6jA6","i":"EAe5o2mlyMDBdE2M7smLqnbELCpDCpG-2s-W6CaYRSqy","s":"1","p":"EAe5o2mlyMDBdE2M7smLqnbELCpDCpG-2s-W6CaYRSqy","a":[{"i":"EOeqXxxstDjRzGgPmyXEssdrUDi0sAQHycWK4obwLatj","s":"0","d":"EOeqXxxstDjRzGgPmyXEssdrUDi0sAQHycWK4obwLatj"}]},{"v":"KERI10JSON00013a_","t":"ixn","d":"EEVK_HJGjiUXEzH6Jqf8z1wiEqKu79WD6i_UJjDnKgHo","i":"EAe5o2mlyMDBdE2M7smLqnbELCpDCpG-2s-W6CaYRSqy","s":"2","p":"EH3BdhrtRpJT9GOXg5CTmYbRk-2gp6Jvv0L5boJJ6jA6","a":[{"i":"EOeqXxxstDjRzGgPmyXEssdrUDi0sAQHycWK4obwLatj","s":"0","d":"ELJZXPOYRZMpuTXnyZAy9wutcqgCc5LEytVClQB2EILq"}]}],"issuerKelAttachmentsB64":["LUFBQkFBQ3p4LXVzWm41TlNhdzlXREx3RDBuOW1LN085ODFoZUpOdm95MFZqelhWV0J3RGhrY3laYTh2cmtYWnBvcnFTdFNGNVMzV3pCdDNkdkhpZWs1akoyYjg=","LUFBQkFBRElTY1JLU1poeTgxU2Fkc3ExS3pwc1B2RHBud01nTkIxMGhjdnBRMWxlelhPalFLbGx4ODFkZndyektvcEJ3U0haalN1bkhBUWRlZnhfeVAxaFB1Y2o=","LUFBQkFBQXVBNnB4QUpaMWZnMV8xNFd4ZENUSWNuTGJ1UFBGWm16bkRVaFhkSEJDeHhrSjVXV3N1V19xVmEwNkFFZzRRUHpzbUI3MWUxRC0tNWlvUC1qdkp6TUo="],"tel":[{"v":"KERI10JSON0000ff_","t":"vcp","d":"EOeqXxxstDjRzGgPmyXEssdrUDi0sAQHycWK4obwLatj","i":"EOeqXxxstDjRzGgPmyXEssdrUDi0sAQHycWK4obwLatj","ii":"EAe5o2mlyMDBdE2M7smLqnbELCpDCpG-2s-W6CaYRSqy","s":"0","c":["NB"],"bt":"0","b":[],"n":"0AAHBwcHBwcHBwcHBwcHBwcH"},{"v":"KERI10JSON0000ed_","t":"iss","d":"ELJZXPOYRZMpuTXnyZAy9wutcqgCc5LEytVClQB2EILq","i":"ECMW25uAAgYIRpJdH5Cu_upswgwdXr-w0T39T8cwUBHB","s":"0","ri":"EOeqXxxstDjRzGgPmyXEssdrUDi0sAQHycWK4obwLatj","dt":"2025-01-01T00:00:00.000000+00:00"}],"receipts":[],"witnessPolicy":"warn","now":"2025-06-01T00:00:00+00:00"} diff --git a/packages/auths-node/conformance/presentation_valid.json b/packages/auths-node/conformance/presentation_valid.json new file mode 100644 index 000000000..b1287dfa1 --- /dev/null +++ b/packages/auths-node/conformance/presentation_valid.json @@ -0,0 +1 @@ +{"schemaVersion":1,"envelope":{"credentialSaid":"EIHIVHyV6fFHf_DJdBKQL6nPnZYg7p_bTRm-eBo-W3Qt","audience":"audience.example","binding":{"mode":"challenge","nonceB64":"BQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQU="},"signatureB64":"ES29qyTATtdDVdQaxF+OWfxcTmIH/K+/mMpFDgWuOlUv0sxYG1cf8Z83ZgVuvbimS0btcJ4+a75WuD3JtFyq2Q=="},"credential":{"acdc":{"v":"ACDC10JSON00018e_","d":"EIHIVHyV6fFHf_DJdBKQL6nPnZYg7p_bTRm-eBo-W3Qt","i":"ENNmy_HLw5dGgfij4BHImeVY__QCrcIJ-kAWyHBtux2T","ri":"EH-PM17GEG5c3dw-7iyipW5NMHDd45DmfGnoRrAgH40K","s":"ECJN2IjRhuWpNRFJaIhLbYAAbB-DjILDaQ3dEX7rfA9_","a":{"d":"EFVPZndW9OMDowHy-JSwooEGRKVxXQRRURJoL1BDnKTw","i":"EH4vavhiH-UtDihmkP53OKOKEID422tE10tCKLQRlbzf","dt":"2025-01-01T00:00:00.000000+00:00","capability":"sign"}},"signatureB64":"wpp/Pnh1BW1aw2uobLmeEYpbBfMvOibBdcczBoMbZiA0pRxDLtevyuLMsd1+ct89MWDuKRtDFTSrtyTO1K5/BA=="},"issuerKel":[{"v":"KERI10JSON00012b_","t":"icp","d":"ENNmy_HLw5dGgfij4BHImeVY__QCrcIJ-kAWyHBtux2T","i":"ENNmy_HLw5dGgfij4BHImeVY__QCrcIJ-kAWyHBtux2T","s":"0","kt":"1","k":["DIqI4910CfGV_VLbLTy6XXLKZwm_HZQSG_N0iAG0D29c"],"nt":"1","n":["EGn7g8vcW7roa2PjmfRqlRlLrEeov5JabW2-z9EFnt4Q"],"bt":"0","b":[],"c":[],"a":[]},{"v":"KERI10JSON00013a_","t":"ixn","d":"ENxyBrMxp1YOwaMEQibWJRmz0ej_LxCrWJf9-fXR114S","i":"ENNmy_HLw5dGgfij4BHImeVY__QCrcIJ-kAWyHBtux2T","s":"1","p":"ENNmy_HLw5dGgfij4BHImeVY__QCrcIJ-kAWyHBtux2T","a":[{"i":"EH-PM17GEG5c3dw-7iyipW5NMHDd45DmfGnoRrAgH40K","s":"0","d":"EH-PM17GEG5c3dw-7iyipW5NMHDd45DmfGnoRrAgH40K"}]},{"v":"KERI10JSON00013a_","t":"ixn","d":"ELDwLMIYAo_4_aM5xM_QXcMVp3BBREx_n4Cn2Ll36Th2","i":"ENNmy_HLw5dGgfij4BHImeVY__QCrcIJ-kAWyHBtux2T","s":"2","p":"ENxyBrMxp1YOwaMEQibWJRmz0ej_LxCrWJf9-fXR114S","a":[{"i":"EH-PM17GEG5c3dw-7iyipW5NMHDd45DmfGnoRrAgH40K","s":"0","d":"EOFt2wXDs3k5_NnPYJgQNWQJqmfK7Sj1k6l5_6Bawwxf"}]}],"issuerKelAttachmentsB64":["LUFBQkFBQUw2S2xWVXlieHZpQkZNZDVjSWNjRFRKLTBJbzdrd3B0LWE1aUM2ekExZVBHQlpEaXktbUhFcEpycUdGRGF5YjB6OHNxRk9QVUNhaGVxZjFsc3MyQUg=","LUFBQkFBQjkwR0ZJTEdfdEYwem9Sekgyb2NvTzVxb0g2OFVpakFENUdQQ01wek1Edm5Fa05lWUsyM3A1bEUzOE5DYTRiWmp1RWJzMU90Mkl5N0lUcElmZ2pIY0U=","LUFBQkFBQjgyaHhWSXAzQW90UDdEMFZmQzI4ZzhTQUxNaGxyMktPTzFuUmNzQ0dfT3RxQklRWURkNlZrN0NjSk9ZWF9wRzRLOElMY25wNHQyVVEyN0VkdFpqc0g="],"subjectKel":[{"v":"KERI10JSON00012f_","t":"icp","d":"EH4vavhiH-UtDihmkP53OKOKEID422tE10tCKLQRlbzf","i":"EH4vavhiH-UtDihmkP53OKOKEID422tE10tCKLQRlbzf","s":"0","kt":"1","k":["1AAJAz734yusG1Gn3MusNCMeLwR1cO6mqviykufH400tPRf1"],"nt":"1","n":["ED-seG4sYP8IP6nXr6-1hdtqToWLaevdXRklPysEH-Dr"],"bt":"0","b":[],"c":[],"a":[]}],"subjectKelAttachmentsB64":["LUFBQkFBQ2J0cG14V3R3VDlUWVZrazBXRTlmUjZ5VEFJWEgyM2daY1BsSlZkMVZtbDZYLWpfdWNBbjNwMWl0bjA1RFdRLXFIMGMza0hCSE92SmxQY3J6aG5FWmU="],"tel":[{"v":"KERI10JSON0000ff_","t":"vcp","d":"EH-PM17GEG5c3dw-7iyipW5NMHDd45DmfGnoRrAgH40K","i":"EH-PM17GEG5c3dw-7iyipW5NMHDd45DmfGnoRrAgH40K","ii":"ENNmy_HLw5dGgfij4BHImeVY__QCrcIJ-kAWyHBtux2T","s":"0","c":["NB"],"bt":"0","b":[],"n":"0AAHBwcHBwcHBwcHBwcHBwcH"},{"v":"KERI10JSON0000ed_","t":"iss","d":"EOFt2wXDs3k5_NnPYJgQNWQJqmfK7Sj1k6l5_6Bawwxf","i":"EIHIVHyV6fFHf_DJdBKQL6nPnZYg7p_bTRm-eBo-W3Qt","s":"0","ri":"EH-PM17GEG5c3dw-7iyipW5NMHDd45DmfGnoRrAgH40K","dt":"2025-01-01T00:00:00.000000+00:00"}],"receipts":[],"witnessPolicy":"warn","audience":"audience.example","expectedChallengeB64":"BQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQU=","now":"2025-06-01T00:00:00+00:00"} diff --git a/packages/auths-node/conformance/statuses.json b/packages/auths-node/conformance/statuses.json new file mode 100644 index 000000000..852420807 --- /dev/null +++ b/packages/auths-node/conformance/statuses.json @@ -0,0 +1,32 @@ +{ + "presentationStatuses": [ + "Valid", + "HolderNotCurrentKey", + "WrongAudience", + "NonceMismatchOrConsumed", + "Expired", + "SubjectKelInvalid", + "CredentialNotValid", + "MalformedRequest", + "KelUnauthenticated", + "InputTooLarge", + "UnsupportedSchemaVersion", + "Unknown" + ], + "credentialStatuses": [ + "Valid", + "SaidMismatch", + "SchemaInvalid", + "IssuerSignatureInvalid", + "RegistryNotEstablished", + "CredentialRevoked", + "Expired", + "WitnessQuorumNotMet", + "IssuerKelDuplicitous", + "MalformedRequest", + "KelUnauthenticated", + "InputTooLarge", + "UnsupportedSchemaVersion", + "Unknown" + ] +} diff --git a/packages/auths-node/index.d.ts b/packages/auths-node/index.d.ts index 1ff71dddd..6f4a7d4c8 100644 --- a/packages/auths-node/index.d.ts +++ b/packages/auths-node/index.d.ts @@ -11,6 +11,31 @@ export declare function addOrgMember(orgDid: string, memberLabel: string, role: export declare function addWitness(urlStr: string, aid: string, repoPath: string, label?: string | undefined | null): NapiWitnessResult +/** + * Authenticate a presentation request end-to-end. + * + * The caller has already consumed the single-use nonce (via + * [`presentation_nonce`] + its own store); this performs everything else: + * audience binding, nonce equality, the evidence-carrying verify request + * (every supplied KEL is signature-authenticated before replay), and the + * verdict→denial mapping. Capability policy is deliberately NOT here — the + * report carries `caps` and the relying party decides what they admit. + * + * Args: + * * `authorization_header`: The full `Authorization` header value. + * * `evidence_json`: The evidence bundle from `credential present --with-evidence`. + * * `expected_audience`: The relying party's own audience identifier. + * * `expected_nonce`: The base64url nonce the caller just consumed. + * * `now_iso`: Verification time (RFC 3339); defaults to the current time. + * + * Usage: + * ```ignore + * const report = authenticatePresentation(header, evidenceJson, 'market.auths.dev', peek.nonce); + * if (report.authorized) { /* report.subject, report.subjectRoot, report.caps *\/ } + * ``` + */ +export declare function authenticatePresentation(authorizationHeader: string, evidenceJson: string, expectedAudience: string, expectedNonce: string, nowIso?: string | undefined | null): NapiAgentAuthReport + export declare function compilePolicy(policyJson: string): string export declare function createAgentIdentity(agentName: string, capabilities: Array, repoPath: string, passphrase?: string | undefined | null): NapiAgentIdentityBundle @@ -63,6 +88,8 @@ export declare const enum CredentialStatus { IssuerKelDuplicitous = 'IssuerKelDuplicitous', /** The request JSON could not be parsed; see `message`. */ MalformedRequest = 'MalformedRequest', + /** A supplied KEL failed signature authentication (forged or missing attachments). */ + KelUnauthenticated = 'KelUnauthenticated', /** A request slice exceeded its bound; see `field`. */ InputTooLarge = 'InputTooLarge', /** The request schema version is not understood by this build. */ @@ -113,12 +140,47 @@ export declare function listPinnedIdentities(repoPath: string): string export declare function listWitnesses(repoPath: string): string +/** + * Mint a single-use challenge nonce (32 random bytes, base64url) for the + * relying party to store and hand to an agent. Format and length are the + * auths-rp contract — a relying party never invents its own nonce shape. + * + * Args: (none) + * + * Usage: + * ```ignore + * const nonce = mintChallengeNonce(); // store with a TTL, consume exactly once + * ``` + */ +export declare function mintChallengeNonce(): string + export interface NapiActionEnvelope { envelopeJson: string signatureHex: string signerDid: string } +/** The outcome of authenticating a presentation request. */ +export interface NapiAgentAuthReport { + /** True only when the presentation verified end-to-end. */ + authorized: boolean + /** + * `"ok"` when authorized; otherwise the kebab-case denial code + * (e.g. `"presentation-kel-unauthenticated"`, `"wrong-audience"`). + */ + code: string + /** The proven subject AID — present when authorized. */ + subject?: string + /** The subject's proven root (its delegator when delegated) — present when authorized. */ + subjectRoot?: string + /** The credential's issuer AID — present when authorized. */ + issuer?: string + /** The capabilities the verified credential grants — present when authorized. */ + caps?: Array + /** Failure detail for diagnostics — present on some denials. */ + detail?: string +} + export interface NapiAgentIdentityBundle { agentDid: string keyAlias: string @@ -166,9 +228,14 @@ export interface NapiCommitSignResult { } export interface NapiDelegatedAgentBundle { + /** + * The agent's `did:keri:` AID — a real delegated identifier whose KEL the + * parent root anchored, not an attestation-linked `did:key`. + */ agentDid: string + /** The agent's KEL prefix (the `dip` SAID). */ + agentPrefix: string keyAlias: string - attestationJson: string publicKeyHex: string repoPath?: string } @@ -242,6 +309,19 @@ export interface NapiPolicyDecision { message: string } +/** + * The parse-only view of an incoming presentation header: what the relying + * party needs to consume its challenge, before anything is trusted. + */ +export interface NapiPresentationPeek { + /** The base64url challenge nonce the subject signed over. */ + nonce: string + /** The audience the presentation claims to bind to (verified later). */ + audience: string + /** The SAID of the credential being presented (verified later). */ + credentialSaid: string +} + export interface NapiRotationResult { controllerDid: string newKeyFingerprint: string @@ -278,6 +358,24 @@ export interface NapiWitnessResult { export declare function pinIdentity(did: string, repoPath: string, label?: string | undefined | null, trustLevel?: string | undefined | null): NapiPinnedIdentity +/** + * Parse-only peek at an `Authorization: Auths-Presentation` header. + * + * Nothing here is trusted — it exists so the relying party can consume the + * single-use nonce from its store before verification runs. Only the + * interactive challenge binding is accepted. + * + * Args: + * * `authorization_header`: The full `Authorization` header value. + * + * Usage: + * ```ignore + * const peek = presentationNonce(header); + * const consumed = await store.consume(peek.nonce); + * ``` + */ +export declare function presentationNonce(authorizationHeader: string): NapiPresentationPeek + /** A typed presentation verdict report (holder-binding outcome). */ export interface PresentationReport { /** The discriminated status. */ @@ -286,6 +384,8 @@ export interface PresentationReport { issuer?: string /** Subject (holder) AID whose current key signed — present on `Valid`. */ subject?: string + /** The subject's proven root (its delegator when delegated, itself otherwise) — present on `Valid`. */ + subjectRoot?: string /** Granted capabilities — present on `Valid`. Never silently dropped (fn-153.2). */ caps?: Array /** Optional informational role claim — present on `Valid`. */ @@ -318,6 +418,8 @@ export declare const enum PresentationStatus { CredentialNotValid = 'CredentialNotValid', /** The request JSON could not be parsed; see `message`. */ MalformedRequest = 'MalformedRequest', + /** A supplied KEL failed signature authentication (forged or missing attachments). */ + KelUnauthenticated = 'KelUnauthenticated', /** A request slice exceeded its bound; see `field`. */ InputTooLarge = 'InputTooLarge', /** The request schema version is not understood by this build. */ diff --git a/packages/auths-node/index.js b/packages/auths-node/index.js index 78b0504b3..60cee6676 100644 --- a/packages/auths-node/index.js +++ b/packages/auths-node/index.js @@ -579,6 +579,7 @@ module.exports = nativeBinding module.exports.NapiPairingHandle = nativeBinding.NapiPairingHandle module.exports.addOrgMember = nativeBinding.addOrgMember module.exports.addWitness = nativeBinding.addWitness +module.exports.authenticatePresentation = nativeBinding.authenticatePresentation module.exports.compilePolicy = nativeBinding.compilePolicy module.exports.createAgentIdentity = nativeBinding.createAgentIdentity module.exports.createIdentity = nativeBinding.createIdentity @@ -599,7 +600,9 @@ module.exports.listAttestationsByDevice = nativeBinding.listAttestationsByDevice module.exports.listOrgMembers = nativeBinding.listOrgMembers module.exports.listPinnedIdentities = nativeBinding.listPinnedIdentities module.exports.listWitnesses = nativeBinding.listWitnesses +module.exports.mintChallengeNonce = nativeBinding.mintChallengeNonce module.exports.pinIdentity = nativeBinding.pinIdentity +module.exports.presentationNonce = nativeBinding.presentationNonce module.exports.PresentationStatus = nativeBinding.PresentationStatus module.exports.removePinnedIdentity = nativeBinding.removePinnedIdentity module.exports.removeWitness = nativeBinding.removeWitness diff --git a/packages/auths-node/lib/identity.ts b/packages/auths-node/lib/identity.ts index b93e84984..2fd77a70c 100644 --- a/packages/auths-node/lib/identity.ts +++ b/packages/auths-node/lib/identity.ts @@ -30,13 +30,13 @@ export interface AgentIdentity { /** An agent delegated under an existing identity. */ export interface DelegatedAgent { - /** The delegated agent's DID (typically `did:key:z...`). */ + /** The agent's `did:keri:` AID — a delegated identifier the parent root anchored. */ did: string + /** The agent's KEL prefix (the delegated inception SAID). */ + prefix: string /** Keychain alias for the agent's signing key. */ keyAlias: string - /** JSON-serialized delegation attestation signed by the parent identity. */ - attestation: string - /** Hex-encoded Ed25519 public key. */ + /** Hex-encoded public key. */ publicKey: string } @@ -209,13 +209,13 @@ export class IdentityService { opts.capabilities, this.client.repoPath, pp, - opts.expiresInDays ?? null, + opts.expiresInDays != null ? opts.expiresInDays * 86_400 : null, opts.identityDid, ) return { did: bundle.agentDid, + prefix: bundle.agentPrefix, keyAlias: bundle.keyAlias, - attestation: bundle.attestationJson, publicKey: bundle.publicKeyHex, } } catch (err) { diff --git a/packages/auths-node/lib/native.ts b/packages/auths-node/lib/native.ts index 4d707d0e4..7ac5cdc58 100644 --- a/packages/auths-node/lib/native.ts +++ b/packages/auths-node/lib/native.ts @@ -44,9 +44,11 @@ export interface NapiAgentIdentityBundle { } export interface NapiDelegatedAgentBundle { + /** The agent's `did:keri:` AID — a delegated identifier the parent root anchored. */ agentDid: string + /** The agent's KEL prefix (the delegated inception SAID). */ + agentPrefix: string keyAlias: string - attestationJson: string publicKeyHex: string repoPath?: string | null } @@ -177,7 +179,7 @@ export interface NativeBindings { generateInmemoryKeypair(): NapiInMemoryKeypair createIdentity(keyAlias: string, repoPath: string, passphrase?: string | null): NapiIdentityResult createAgentIdentity(agentName: string, capabilities: string[], repoPath: string, passphrase?: string | null): NapiAgentIdentityBundle - delegateAgent(agentName: string, capabilities: string[], parentRepoPath: string, passphrase?: string | null, expiresInDays?: number | null, identityDid?: string | null): NapiDelegatedAgentBundle + delegateAgent(agentName: string, capabilities: string[], parentRepoPath: string, passphrase?: string | null, expiresInSeconds?: number | null, identityDid?: string | null): NapiDelegatedAgentBundle rotateIdentityKeys(repoPath: string, identityKeyAlias?: string | null, nextKeyAlias?: string | null, passphrase?: string | null): NapiRotationResult getIdentityPublicKey(identityDid: string, repoPath: string, passphrase?: string | null): string diff --git a/packages/auths-node/package.json b/packages/auths-node/package.json index d9d949a20..da5af1548 100644 --- a/packages/auths-node/package.json +++ b/packages/auths-node/package.json @@ -32,9 +32,9 @@ }, "scripts": { "artifacts": "napi artifacts", - "build": "napi build --platform --release", - "build:debug": "napi build --platform", - "prepublishOnly": "napi prepublish -t npm --no-gh-release", + "build": "napi build --platform --release && node scripts/sync-conformance.mjs", + "build:debug": "napi build --platform && node scripts/sync-conformance.mjs", + "prepublishOnly": "node scripts/sync-conformance.mjs && napi prepublish -t npm --no-gh-release", "docs": "typedoc", "test": "vitest run", "universal": "napi universal -t darwin" @@ -46,5 +46,11 @@ "typedoc-plugin-markdown": "^4.10.0", "typescript": "^5.7.0", "vitest": "^3.0.0" - } + }, + "files": [ + "index.js", + "index.d.ts", + "conformance/", + "README.md" + ] } diff --git a/packages/auths-node/scripts/sync-conformance.mjs b/packages/auths-node/scripts/sync-conformance.mjs new file mode 100644 index 000000000..2e202e23a --- /dev/null +++ b/packages/auths-node/scripts/sync-conformance.mjs @@ -0,0 +1,44 @@ +#!/usr/bin/env node +/** + * Sync the conformance surface into the package: the golden verify vectors + * (the exact fixtures the Rust verifier tests against) and the canonical + * status lists (parsed from the generated declarations, so they cannot drift + * from the enums). + * + * Runs on build and prepublish; consumers assert against `conformance/` from + * the installed package, which pins the contract to the version they run. + */ + +import { copyFileSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import { join, resolve } from 'node:path'; + +const pkg = resolve(import.meta.dirname, '..'); +const fixtures = resolve(pkg, '../../crates/auths-verifier/tests/fixtures'); +const out = join(pkg, 'conformance'); +mkdirSync(out, { recursive: true }); + +const VECTORS = ['presentation_valid.json', 'credential_valid.json', 'credential_revoked.json']; +for (const name of VECTORS) { + copyFileSync(join(fixtures, name), join(out, name)); +} + +const dts = readFileSync(join(pkg, 'index.d.ts'), 'utf8'); +function enumValues(name) { + const match = dts.match(new RegExp(`export declare const enum ${name} \\{([\\s\\S]*?)\\n\\}`)); + if (!match) throw new Error(`enum ${name} not found in index.d.ts`); + return [...match[1].matchAll(/= '([^']+)'/g)].map((m) => m[1]); +} + +writeFileSync( + join(out, 'statuses.json'), + JSON.stringify( + { + presentationStatuses: enumValues('PresentationStatus'), + credentialStatuses: enumValues('CredentialStatus'), + }, + null, + 2, + ) + '\n', +); + +console.log(`conformance synced: ${VECTORS.length} vectors + statuses.json`); diff --git a/packages/auths-node/src/artifact.rs b/packages/auths-node/src/artifact.rs index 59d8bcf6d..c33c12a50 100644 --- a/packages/auths-node/src/artifact.rs +++ b/packages/auths-node/src/artifact.rs @@ -2,7 +2,9 @@ use std::io::Read; use std::path::PathBuf; use std::sync::Arc; +// binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow use auths_core::signing::PrefilledPassphraseProvider; +// binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow use auths_core::storage::keychain::{IdentityDID, KeyAlias, get_platform_keychain_with_config}; use auths_crypto::decode_seed_hex; use auths_sdk::context::AuthsContext; @@ -11,6 +13,7 @@ use auths_sdk::signing::{ ArtifactSigningParams, SigningKeyMaterial, sign_artifact as sdk_sign_artifact, sign_artifact_raw, }; +// binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow use auths_storage::git::{ GitRegistryBackend, RegistryAttestationStorage, RegistryConfig, RegistryIdentityStorage, }; diff --git a/packages/auths-node/src/attestation_query.rs b/packages/auths-node/src/attestation_query.rs index 412e50e97..8177eb61e 100644 --- a/packages/auths-node/src/attestation_query.rs +++ b/packages/auths-node/src/attestation_query.rs @@ -1,8 +1,11 @@ use std::path::PathBuf; use std::sync::Arc; +// binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow use auths_id::attestation::group::AttestationGroup; +// binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow use auths_id::storage::attestation::AttestationSource; +// binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow use auths_storage::git::{GitRegistryBackend, RegistryAttestationStorage, RegistryConfig}; use auths_verifier::core::Attestation; use auths_verifier::types::CanonicalDid; diff --git a/packages/auths-node/src/commit_sign.rs b/packages/auths-node/src/commit_sign.rs index 0f09e9db9..dbba9b0c8 100644 --- a/packages/auths-node/src/commit_sign.rs +++ b/packages/auths-node/src/commit_sign.rs @@ -1,7 +1,9 @@ use std::path::PathBuf; use std::sync::Arc; +// binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow use auths_core::signing::PrefilledPassphraseProvider; +// binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow use auths_core::storage::keychain::get_platform_keychain_with_config; use auths_sdk::workflows::signing::{ CommitSigningContext, CommitSigningParams, CommitSigningWorkflow, diff --git a/packages/auths-node/src/device.rs b/packages/auths-node/src/device.rs index 056a80dcf..604a14772 100644 --- a/packages/auths-node/src/device.rs +++ b/packages/auths-node/src/device.rs @@ -1,12 +1,15 @@ use std::path::PathBuf; use std::sync::Arc; +// binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow use auths_core::signing::PrefilledPassphraseProvider; +// binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow use auths_core::storage::keychain::get_platform_keychain_with_config; use auths_sdk::context::AuthsContext; use auths_sdk::device::extend_device; use auths_sdk::device::{link_device, revoke_device}; use auths_sdk::types::{DeviceExtensionConfig, DeviceLinkConfig}; +// binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow use auths_storage::git::{ GitRegistryBackend, RegistryAttestationStorage, RegistryConfig, RegistryIdentityStorage, }; diff --git a/packages/auths-node/src/helpers.rs b/packages/auths-node/src/helpers.rs index 61c9b039e..1e6d6b858 100644 --- a/packages/auths-node/src/helpers.rs +++ b/packages/auths-node/src/helpers.rs @@ -1,6 +1,8 @@ use std::path::PathBuf; +// binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow use auths_core::config::{EnvironmentConfig, KeychainConfig}; +// binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow use auths_core::storage::keychain::{ IdentityDID, KeyAlias, KeyRole, KeyStorage, get_platform_keychain_with_config, }; diff --git a/packages/auths-node/src/identity.rs b/packages/auths-node/src/identity.rs index a0b2f3586..175cb89d6 100644 --- a/packages/auths-node/src/identity.rs +++ b/packages/auths-node/src/identity.rs @@ -1,15 +1,19 @@ use std::path::PathBuf; use std::sync::Arc; -use auths_core::crypto::signer::encrypt_keypair; +// binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow use auths_core::signing::PrefilledPassphraseProvider; +// binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow use auths_core::storage::keychain::{KeyAlias, KeyRole, get_platform_keychain_with_config}; +// binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow use auths_id::identity::initialize::initialize_registry_identity; +// binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow use auths_id::storage::attestation::AttestationSource; use auths_sdk::context::AuthsContext; use auths_sdk::device::link_device; use auths_sdk::types::{DeviceLinkConfig, IdentityRotationConfig}; use auths_sdk::workflows::rotation::rotate_identity; +// binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow use auths_storage::git::{ GitRegistryBackend, RegistryAttestationStorage, RegistryConfig, RegistryIdentityStorage, }; @@ -279,37 +283,19 @@ pub fn delegate_agent( Some("Ed25519") | Some("ed25519") => auths_crypto::CurveType::Ed25519, _ => auths_crypto::CurveType::P256, }; - let generated = auths_id::keri::inception::generate_keypair_for_init(curve_choice) - .map_err(|e| format_error("AUTHS_CRYPTO_ERROR", format!("Key generation failed: {e}")))?; - let agent_pubkey = generated.public_key.clone(); - - let (parent_did, _, _) = keychain - .load_key(&parent_alias) - .map_err(|e| format_error("AUTHS_KEY_NOT_FOUND", format!("Key load failed: {e}")))?; - - // Store the PKCS8 blob directly — it's already curve-aware from generate_keypair_for_init. - // No need to extract-then-re-encode the seed (which was Ed25519-only). - let encrypted = encrypt_keypair(generated.pkcs8.as_ref(), &passphrase_str) - .map_err(|e| format_error("AUTHS_CRYPTO_ERROR", format!("Key encryption failed: {e}")))?; - keychain - .store_key( - &agent_alias, - &parent_did, - KeyRole::DelegatedAgent, - &encrypted, - ) - .map_err(|e| format_error("AUTHS_KEYCHAIN_ERROR", format!("Key storage failed: {e}")))?; - - validate_capabilities(&capabilities)?; - - let link_config = DeviceLinkConfig { - identity_key_alias: parent_alias, - device_key_alias: Some(agent_alias.clone()), - device_did: None, - expires_in: expires_in.map(|s| s as u64), - note: Some(format!("Agent: {}", agent_name)), - payload: None, - }; + let scope: Vec = capabilities + .iter() + .map(|c| { + auths_keri::Capability::parse(c).map_err(|e| { + format_error( + "AUTHS_INVALID_INPUT", + format!("Invalid capability '{c}': {e}"), + ) + }) + }) + .collect::>()?; + #[allow(clippy::disallowed_methods)] // presentation boundary: wall clock is injected here + let expires_at = expires_in.map(|secs| chrono::Utc::now().timestamp() + secs); let keychain: Arc = Arc::from(keychain); @@ -326,42 +312,40 @@ pub fn delegate_agent( .passphrase_provider(provider) .build(); - let result = link_device(link_config, &ctx, clock.as_ref()).map_err(|e| { + // KERI-native delegation: the agent gets its own dip-rooted KEL the parent + // anchors (with the scope/expiry as a delegator-anchored seal) — never an + // attestation-linked did:key. + let result = auths_sdk::domains::agents::delegation::add_scoped( + &ctx, + &parent_alias, + &agent_alias, + curve_choice, + &scope, + expires_at, + ) + .map_err(|e| { format_error( "AUTHS_IDENTITY_ERROR", - format!("Agent provisioning failed: {e}"), + format!("Agent delegation failed: {e}"), ) })?; - #[allow(clippy::disallowed_methods)] // INVARIANT: device_did from SDK setup result - let device_did = CanonicalDid::new_unchecked(result.device_did.to_string()); - let attestations = attestation_storage - .load_attestations_for_device(&device_did) - .map_err(|e| { - format_error( - "AUTHS_REGISTRY_ERROR", - format!("Failed to load attestation: {e}"), - ) - })?; - - let attestation = attestations.last().ok_or_else(|| { - format_error( - "AUTHS_REGISTRY_ERROR", - "No attestation found after provisioning", - ) - })?; - - let attestation_json = serde_json::to_string(attestation).map_err(|e| { + let (agent_pubkey, _curve) = auths_core::storage::keychain::extract_public_key_bytes( + ctx.key_storage.as_ref(), + &agent_alias, + ctx.passphrase_provider.as_ref(), + ) + .map_err(|e| { format_error( - "AUTHS_SERIALIZATION_ERROR", - format!("Serialization failed: {e}"), + "AUTHS_KEY_NOT_FOUND", + format!("Agent key readback failed: {e}"), ) })?; Ok(NapiDelegatedAgentBundle { - agent_did: result.device_did.to_string(), + agent_did: result.agent_did, + agent_prefix: result.agent_prefix, key_alias: agent_alias.to_string(), - attestation_json, public_key_hex: hex::encode(&agent_pubkey), repo_path: Some(repo.to_string_lossy().to_string()), }) diff --git a/packages/auths-node/src/lib.rs b/packages/auths-node/src/lib.rs index 407398dd6..214cfb8dc 100644 --- a/packages/auths-node/src/lib.rs +++ b/packages/auths-node/src/lib.rs @@ -13,6 +13,7 @@ pub mod org; pub mod pairing; pub mod policy; pub mod presentation; +pub mod rp; pub mod sign; pub mod trust; pub mod types; diff --git a/packages/auths-node/src/org.rs b/packages/auths-node/src/org.rs index 16e8f407a..4fd9550d7 100644 --- a/packages/auths-node/src/org.rs +++ b/packages/auths-node/src/org.rs @@ -1,17 +1,27 @@ use std::path::PathBuf; use std::sync::Arc; +// binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow use auths_core::ports::id::SystemUuidProvider; +// binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow use auths_core::signing::{DidResolver, PrefilledPassphraseProvider, StorageSigner}; +// binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow use auths_core::storage::keychain::{IdentityDID, KeyAlias, KeyStorage}; +// binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow use auths_id::attestation::create::create_signed_attestation; +// binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow use auths_id::identity::initialize::initialize_registry_identity; +// binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow use auths_id::identity::resolve::RegistryDidResolver; +// binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow use auths_id::keri::types::Prefix; +// binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow use auths_id::storage::git_refs::AttestationMetadata; +// binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow use auths_id::storage::registry::RegistryBackend; use auths_sdk::context::AuthsContext; use auths_sdk::workflows::org::{add_member, list_members, revoke_member}; +// binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow use auths_storage::git::{ GitRegistryBackend, RegistryAttestationStorage, RegistryConfig, RegistryIdentityStorage, }; diff --git a/packages/auths-node/src/pairing.rs b/packages/auths-node/src/pairing.rs index a792263b7..c2667d223 100644 --- a/packages/auths-node/src/pairing.rs +++ b/packages/auths-node/src/pairing.rs @@ -4,13 +4,16 @@ use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::sync::Arc; use std::time::Duration; +// binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow use auths_core::storage::keychain::{IdentityDID, KeyRole}; +// binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow use auths_id::storage::identity::IdentityStorage; use auths_pairing_daemon::{ MockNetworkDiscovery, MockNetworkInterfaces, PairingDaemonBuilder, PairingDaemonHandle, RateLimiter, }; use auths_sdk::pairing::{PairingSessionParams, build_pairing_session_request}; +// binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow use auths_storage::git::RegistryIdentityStorage; use chrono::Utc; use tokio::sync::Mutex; diff --git a/packages/auths-node/src/presentation.rs b/packages/auths-node/src/presentation.rs index 9c13e140a..b8aa0b460 100644 --- a/packages/auths-node/src/presentation.rs +++ b/packages/auths-node/src/presentation.rs @@ -31,6 +31,8 @@ pub enum PresentationStatus { CredentialNotValid, /// The request JSON could not be parsed; see `message`. MalformedRequest, + /// A supplied KEL failed signature authentication (forged or missing attachments). + KelUnauthenticated, /// A request slice exceeded its bound; see `field`. InputTooLarge, /// The request schema version is not understood by this build. @@ -62,6 +64,8 @@ pub enum CredentialStatus { IssuerKelDuplicitous, /// The request JSON could not be parsed; see `message`. MalformedRequest, + /// A supplied KEL failed signature authentication (forged or missing attachments). + KelUnauthenticated, /// A request slice exceeded its bound; see `field`. InputTooLarge, /// The request schema version is not understood by this build. @@ -102,6 +106,8 @@ pub struct PresentationReport { pub issuer: Option, /// Subject (holder) AID whose current key signed — present on `Valid`. pub subject: Option, + /// The subject's proven root (its delegator when delegated, itself otherwise) — present on `Valid`. + pub subject_root: Option, /// Granted capabilities — present on `Valid`. Never silently dropped (fn-153.2). pub caps: Option>, /// Optional informational role claim — present on `Valid`. @@ -193,6 +199,7 @@ fn presentation_status(kind: &str) -> PresentationStatus { "subjectKelInvalid" => PresentationStatus::SubjectKelInvalid, "credentialNotValid" => PresentationStatus::CredentialNotValid, "malformedRequest" => PresentationStatus::MalformedRequest, + "kelUnauthenticated" => PresentationStatus::KelUnauthenticated, "inputTooLarge" => PresentationStatus::InputTooLarge, "unsupportedSchemaVersion" => PresentationStatus::UnsupportedSchemaVersion, _ => PresentationStatus::Unknown, @@ -211,6 +218,7 @@ fn credential_status(kind: &str) -> CredentialStatus { "witnessQuorumNotMet" => CredentialStatus::WitnessQuorumNotMet, "issuerKelDuplicitous" => CredentialStatus::IssuerKelDuplicitous, "malformedRequest" => CredentialStatus::MalformedRequest, + "kelUnauthenticated" => CredentialStatus::KelUnauthenticated, "inputTooLarge" => CredentialStatus::InputTooLarge, "unsupportedSchemaVersion" => CredentialStatus::UnsupportedSchemaVersion, _ => CredentialStatus::Unknown, @@ -226,7 +234,7 @@ fn credential_report(value: &Value) -> CredentialReport { as_of: number_field(value, "asOf"), revoked_at: number_field(value, "revokedAt"), expired_at: string_field(value, "expiredAt"), - message: string_field(value, "message"), + message: string_field(value, "message").or_else(|| string_field(value, "detail")), field: string_field(value, "field"), } } @@ -236,11 +244,12 @@ fn presentation_report(value: &Value) -> PresentationReport { status: presentation_status(kind_of(value)), issuer: string_field(value, "issuer"), subject: string_field(value, "subject"), + subject_root: string_field(value, "subjectRoot"), caps: caps_field(value), role: string_field(value, "role"), expires_at: string_field(value, "expiresAt"), credential: value.get("credential").map(credential_report), - message: string_field(value, "message"), + message: string_field(value, "message").or_else(|| string_field(value, "detail")), field: string_field(value, "field"), } } diff --git a/packages/auths-node/src/rp.rs b/packages/auths-node/src/rp.rs new file mode 100644 index 000000000..35a99d7f6 --- /dev/null +++ b/packages/auths-node/src/rp.rs @@ -0,0 +1,251 @@ +//! The relying-party surface — everything a server needs to authenticate an +//! `Auths-Presentation` request lives HERE, in Rust, behind one call. +//! +//! A relying party (the market, any future API) must never re-implement the +//! wire: no header parsing, no base64 juggling, no verify-request assembly, no +//! verdict-to-denial mapping in application code. The flow is: +//! +//! 1. [`mint_challenge_nonce`] — mint the single-use nonce the RP stores. +//! 2. [`presentation_nonce`] — parse-only peek at an incoming header, so the +//! RP can consume the nonce from its store BEFORE any verification (a +//! failed attempt must still burn its challenge). +//! 3. [`authenticate_presentation`] — the full check: wire parse, audience and +//! nonce binding, evidence-carrying verify request, KEL-authenticated +//! verification, and the verdict→denial doctrine. Returns a typed report; +//! grants (which capabilities admit which action) stay the RP's policy. + +use base64::Engine as _; +use base64::engine::general_purpose::STANDARD as B64_STD; +use base64::engine::general_purpose::URL_SAFE_NO_PAD as B64_URL; +use napi_derive::napi; + +use auths_rp::{AUTHS_PRESENTATION_SCHEME, Nonce, WireBinding, WirePresentation}; + +/// The parse-only view of an incoming presentation header: what the relying +/// party needs to consume its challenge, before anything is trusted. +#[napi(object)] +pub struct NapiPresentationPeek { + /// The base64url challenge nonce the subject signed over. + pub nonce: String, + /// The audience the presentation claims to bind to (verified later). + pub audience: String, + /// The SAID of the credential being presented (verified later). + pub credential_said: String, +} + +/// The outcome of authenticating a presentation request. +#[napi(object)] +pub struct NapiAgentAuthReport { + /// True only when the presentation verified end-to-end. + pub authorized: bool, + /// `"ok"` when authorized; otherwise the kebab-case denial code + /// (e.g. `"presentation-kel-unauthenticated"`, `"wrong-audience"`). + pub code: String, + /// The proven subject AID — present when authorized. + pub subject: Option, + /// The subject's proven root (its delegator when delegated) — present when authorized. + pub subject_root: Option, + /// The credential's issuer AID — present when authorized. + pub issuer: Option, + /// The capabilities the verified credential grants — present when authorized. + pub caps: Option>, + /// Failure detail for diagnostics — present on some denials. + pub detail: Option, +} + +fn denied(code: &str, detail: Option) -> NapiAgentAuthReport { + NapiAgentAuthReport { + authorized: false, + code: code.to_string(), + subject: None, + subject_root: None, + issuer: None, + caps: None, + detail, + } +} + +/// Mint a single-use challenge nonce (32 random bytes, base64url) for the +/// relying party to store and hand to an agent. Format and length are the +/// auths-rp contract — a relying party never invents its own nonce shape. +/// +/// Args: (none) +/// +/// Usage: +/// ```ignore +/// const nonce = mintChallengeNonce(); // store with a TTL, consume exactly once +/// ``` +#[napi] +pub fn mint_challenge_nonce() -> String { + use rand::RngCore as _; + let mut bytes = [0u8; 32]; + rand::rngs::OsRng.fill_bytes(&mut bytes); + Nonce::from_bytes(bytes).to_b64url() +} + +fn parse_wire(authorization_header: &str) -> Result { + let scheme = format!("{AUTHS_PRESENTATION_SCHEME} "); + let token = authorization_header + .strip_prefix(&scheme) + .ok_or("bad-authorization-header")? + .trim(); + let json = B64_URL + .decode(token) + .map_err(|_| "bad-authorization-header")?; + serde_json::from_slice::(&json).map_err(|_| "bad-authorization-header") +} + +/// Parse-only peek at an `Authorization: Auths-Presentation` header. +/// +/// Nothing here is trusted — it exists so the relying party can consume the +/// single-use nonce from its store before verification runs. Only the +/// interactive challenge binding is accepted. +/// +/// Args: +/// * `authorization_header`: The full `Authorization` header value. +/// +/// Usage: +/// ```ignore +/// const peek = presentationNonce(header); +/// const consumed = await store.consume(peek.nonce); +/// ``` +#[napi] +pub fn presentation_nonce(authorization_header: String) -> napi::Result { + let wire = parse_wire(&authorization_header) + .map_err(|code| napi::Error::from_reason(code.to_string()))?; + let WireBinding::Challenge { nonce } = wire.binding else { + return Err(napi::Error::from_reason( + "challenge-binding-required".to_string(), + )); + }; + Ok(NapiPresentationPeek { + nonce, + audience: wire.audience, + credential_said: wire.credential_said, + }) +} + +/// Authenticate a presentation request end-to-end. +/// +/// The caller has already consumed the single-use nonce (via +/// [`presentation_nonce`] + its own store); this performs everything else: +/// audience binding, nonce equality, the evidence-carrying verify request +/// (every supplied KEL is signature-authenticated before replay), and the +/// verdict→denial mapping. Capability policy is deliberately NOT here — the +/// report carries `caps` and the relying party decides what they admit. +/// +/// Args: +/// * `authorization_header`: The full `Authorization` header value. +/// * `evidence_json`: The evidence bundle from `credential present --with-evidence`. +/// * `expected_audience`: The relying party's own audience identifier. +/// * `expected_nonce`: The base64url nonce the caller just consumed. +/// * `now_iso`: Verification time (RFC 3339); defaults to the current time. +/// +/// Usage: +/// ```ignore +/// const report = authenticatePresentation(header, evidenceJson, 'market.auths.dev', peek.nonce); +/// if (report.authorized) { /* report.subject, report.subjectRoot, report.caps */ } +/// ``` +#[napi] +pub fn authenticate_presentation( + authorization_header: String, + evidence_json: String, + expected_audience: String, + expected_nonce: String, + now_iso: Option, +) -> napi::Result { + let wire = match parse_wire(&authorization_header) { + Ok(wire) => wire, + Err(code) => return Ok(denied(code, None)), + }; + let WireBinding::Challenge { nonce } = &wire.binding else { + return Ok(denied("challenge-binding-required", None)); + }; + if wire.audience != expected_audience { + return Ok(denied("wrong-audience", None)); + } + if *nonce != expected_nonce { + return Ok(denied("challenge-mismatch", None)); + } + let Ok(nonce_bytes) = Nonce::parse_b64url(nonce).map(|n| n.as_bytes().to_vec()) else { + return Ok(denied( + "bad-authorization-header", + Some("malformed nonce".into()), + )); + }; + let Ok(signature_bytes) = B64_URL.decode(&wire.signature_b64) else { + return Ok(denied( + "bad-authorization-header", + Some("malformed signature".into()), + )); + }; + let evidence: serde_json::Value = match serde_json::from_str(&evidence_json) { + Ok(serde_json::Value::Object(map)) => serde_json::Value::Object(map), + _ => return Ok(denied("evidence-required", None)), + }; + + #[allow(clippy::disallowed_methods)] // presentation boundary: wall clock injected here + let now = now_iso.unwrap_or_else(|| chrono::Utc::now().to_rfc3339()); + let nonce_std = B64_STD.encode(&nonce_bytes); + let field = |name: &str| evidence.get(name).cloned().unwrap_or(serde_json::json!([])); + let request = serde_json::json!({ + "schemaVersion": 1, + "envelope": { + "credentialSaid": wire.credential_said, + "audience": wire.audience, + "binding": { "mode": "challenge", "nonceB64": nonce_std }, + "signatureB64": B64_STD.encode(&signature_bytes), + }, + "credential": evidence.get("credential").cloned().unwrap_or(serde_json::Value::Null), + "issuerKel": field("issuerKel"), + "issuerKelAttachmentsB64": field("issuerKelAttachmentsB64"), + "subjectKel": field("subjectKel"), + "subjectKelAttachmentsB64": field("subjectKelAttachmentsB64"), + "delegatorKel": field("delegatorKel"), + "delegatorKelAttachmentsB64": field("delegatorKelAttachmentsB64"), + "tel": field("tel"), + "receipts": [], + "witnessPolicy": "warn", + "audience": expected_audience, + "expectedChallengeB64": nonce_std, + "now": now, + }); + + let verdict_json = auths_verifier::verify_presentation_json(&request.to_string()); + let verdict: serde_json::Value = serde_json::from_str(&verdict_json) + .unwrap_or_else(|_| serde_json::json!({ "kind": "malformedRequest" })); + let kind = verdict["kind"].as_str().unwrap_or("malformedRequest"); + + if kind != "valid" { + let kebab: String = kind + .chars() + .flat_map(|c| { + if c.is_ascii_uppercase() { + vec!['-', c.to_ascii_lowercase()] + } else { + vec![c] + } + }) + .collect(); + let detail = verdict["detail"] + .as_str() + .or_else(|| verdict["message"].as_str()) + .or_else(|| verdict["field"].as_str()) + .map(str::to_string); + return Ok(denied(&format!("presentation-{kebab}"), detail)); + } + + Ok(NapiAgentAuthReport { + authorized: true, + code: "ok".to_string(), + subject: verdict["subject"].as_str().map(str::to_string), + subject_root: verdict["subjectRoot"].as_str().map(str::to_string), + issuer: verdict["issuer"].as_str().map(str::to_string), + caps: verdict["caps"].as_array().map(|a| { + a.iter() + .filter_map(|c| c.as_str().map(str::to_string)) + .collect() + }), + detail: None, + }) +} diff --git a/packages/auths-node/src/sign.rs b/packages/auths-node/src/sign.rs index 79e950472..2bee2307f 100644 --- a/packages/auths-node/src/sign.rs +++ b/packages/auths-node/src/sign.rs @@ -1,4 +1,6 @@ +// binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow use auths_core::signing::{PrefilledPassphraseProvider, SecureSigner, StorageSigner}; +// binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow use auths_core::storage::keychain::{KeyAlias, get_platform_keychain_with_config}; use auths_verifier::action::ActionEnvelope; use auths_verifier::core::MAX_ATTESTATION_JSON_SIZE; diff --git a/packages/auths-node/src/trust.rs b/packages/auths-node/src/trust.rs index fe37e4929..6141c1d37 100644 --- a/packages/auths-node/src/trust.rs +++ b/packages/auths-node/src/trust.rs @@ -1,8 +1,11 @@ use std::path::PathBuf; use std::sync::Arc; +// binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow use auths_core::trust::pinned::{PinnedIdentity, PinnedIdentityStore, TrustLevel}; +// binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow use auths_id::identity::resolve::{DefaultDidResolver, DidResolver, RegistryDidResolver}; +// binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow use auths_storage::git::{GitRegistryBackend, RegistryConfig}; use auths_verifier::PublicKeyHex; use napi_derive::napi; diff --git a/packages/auths-node/src/types.rs b/packages/auths-node/src/types.rs index db5d6819c..26dddb0ac 100644 --- a/packages/auths-node/src/types.rs +++ b/packages/auths-node/src/types.rs @@ -154,9 +154,12 @@ pub struct NapiAgentIdentityBundle { #[napi(object)] #[derive(Clone)] pub struct NapiDelegatedAgentBundle { + /// The agent's `did:keri:` AID — a real delegated identifier whose KEL the + /// parent root anchored, not an attestation-linked `did:key`. pub agent_did: String, + /// The agent's KEL prefix (the `dip` SAID). + pub agent_prefix: String, pub key_alias: String, - pub attestation_json: String, pub public_key_hex: String, pub repo_path: Option, } diff --git a/packages/auths-node/src/witness.rs b/packages/auths-node/src/witness.rs index 5093b5dd8..8c36818ae 100644 --- a/packages/auths-node/src/witness.rs +++ b/packages/auths-node/src/witness.rs @@ -1,8 +1,12 @@ use std::path::PathBuf; +// binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow use auths_id::keri::types::Prefix; +// binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow use auths_id::storage::identity::IdentityStorage; +// binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow use auths_id::witness_config::{WitnessConfig, WitnessRef}; +// binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow use auths_storage::git::RegistryIdentityStorage; use napi_derive::napi; diff --git a/schemas/identity-bundle-v1.json b/schemas/identity-bundle-v1.json index 6dd0ab90c..00a0e7508 100644 --- a/schemas/identity-bundle-v1.json +++ b/schemas/identity-bundle-v1.json @@ -28,6 +28,13 @@ "default": "p256", "type": "string" }, + "device_kels": { + "description": "Delegated device KELs anchored under this identity's root. Commits are signed by a DEVICE AID, so a stateless verifier needs the device's KEL, not only the root's. Each entry authenticates exactly like the root KEL (per-event hex CESR attachments), with the root's seal index resolving the delegation. Empty in bundles exported before this field existed — and for identities whose signing DID IS the root.", + "type": "array", + "items": { + "$ref": "#/definitions/BundleDeviceKel" + } + }, "identity_did": { "description": "The DID of the identity (e.g., `\"did:keri:...\"`)", "allOf": [ @@ -209,6 +216,35 @@ } } }, + "BundleDeviceKel": { + "description": "One delegated device KEL carried inside an [`IdentityBundle`].", + "type": "object", + "required": [ + "did", + "kel", + "kel_attachments" + ], + "properties": { + "did": { + "description": "The device's `did:keri:` AID (the `dip` prefix the root anchored).", + "type": "string" + }, + "kel": { + "description": "The device KEL events, oldest first.", + "type": "array", + "items": { + "$ref": "#/definitions/Event" + } + }, + "kel_attachments": { + "description": "The CESR signature attachment (hex) for each `kel` event, in order.", + "type": "array", + "items": { + "type": "string" + } + } + } + }, "CesrKey": { "description": "A CESR-encoded public key (e.g., `D` + base64url Ed25519, `1AAI` + base64url P-256).\n\nWraps the qualified string form. Use `parse()` to extract the curve-tagged `KeriPublicKey` for cryptographic operations.\n\nUsage: ``` use auths_keri::CesrKey; let key = CesrKey::new_unchecked(\"DAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\".into()); assert!(key.parse().is_ok()); ```", "type": "string"