From 72fff6c969fe8b75c7de830585b90dbe36c92253 Mon Sep 17 00:00:00 2001 From: bordumb Date: Sat, 18 Jul 2026 02:41:02 +0100 Subject: [PATCH 01/21] =?UTF-8?q?security:=20close=20#374=20by=20removal?= =?UTF-8?q?=20=E2=80=94=20the=20KEL=20travels=20in=20the=20committed=20bun?= =?UTF-8?q?dle,=20not=20a=20mirrored=20ref?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pre-push registry mirror silently no-oped against every real remote (git2 links no ssh/tls transport), so the published ref drifted until the release could not verify its own binaries. Instead of giving the mirror a transport, remove the distribution channel it fed: - pre-push hook: chaining only (core.hooksPath still must not eat repo hooks) - auths registry push/pull + storage push_registry/pull_registry: deleted - verify: a committed .auths/ci-bundle.json is auto-discovered, so a plain clone still verifies with no flags — the KEL rides the clone like the pin - release.yml: artifact verify runs on the committed bundle; a guard step verifies HEAD through the bundle first and fails with a regenerate hint when a key rotation staled it - auths-rp registry_sync: docs now state the file://-only transport scope Closes #374. Auths-Id: did:keri:EB5cPHY0t-ejNC_rUzPS1dclTvd6kG-R9mQzjozCuGgd Auths-Device: did:keri:EB5cPHY0t-ejNC_rUzPS1dclTvd6kG-R9mQzjozCuGgd Auths-Anchor-Seq: 1 --- .github/workflows/release.yml | 37 ++- crates/auths-cli/src/cli.rs | 3 - crates/auths-cli/src/commands/mod.rs | 1 - crates/auths-cli/src/commands/registry.rs | 153 ----------- .../auths-cli/src/commands/verify_commit.rs | 10 +- .../auths-cli/src/commands/verify_helpers.rs | 25 ++ crates/auths-cli/src/main.rs | 1 - .../tests/cases/third_party_verify.rs | 68 +++-- crates/auths-rp/src/registry_sync.rs | 10 +- .../src/domains/identity/registration.rs | 8 +- crates/auths-sdk/src/storage.rs | 7 +- .../auths-sdk/src/workflows/commit_hooks.rs | 35 +-- crates/auths-storage/src/git/mod.rs | 2 - crates/auths-storage/src/git/sync.rs | 254 ------------------ crates/auths-storage/tests/cases/mod.rs | 1 - .../tests/cases/registry_sync.rs | 241 ----------------- 16 files changed, 134 insertions(+), 722 deletions(-) delete mode 100644 crates/auths-cli/src/commands/registry.rs delete mode 100644 crates/auths-storage/src/git/sync.rs delete mode 100644 crates/auths-storage/tests/cases/registry_sync.rs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cc9d3a73..5419fcbb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -187,24 +187,41 @@ jobs: --note "Release ${{ github.ref_name }}" # The ephemeral attestation anchors HEAD, whose trust chain ends at the - # maintainer's KEL. The repo publishes that KEL under refs/auths/registry - # (KEL-native distribution) — seed the runner's ~/.auths so the verifier - # can resolve it. - - name: Seed maintainer registry for verification - shell: bash + # maintainer's KEL. Resolve that KEL from the committed identity bundle + # (.auths/ci-bundle.json) — the same stateless bundle the commit-verify + # job uses — instead of seeding ~/.auths from refs/auths/registry. The + # git-ref mirror was unreliable (transportless push → drift, see #374); + # the bundle travels with the repo and needs no network fetch. + # Staleness tripwire: the committed bundle is a snapshot, so a key + # rotation silently invalidates it. Verifying HEAD's own commit through + # the bundle exercises the exact trust chain the artifact check needs and + # fails fast with an actionable message instead of at the last step. + - name: Guard — committed bundle still covers the signer (Unix) + if: matrix.ext == '.tar.gz' + run: | + ./staging/auths verify HEAD --identity-bundle .auths/ci-bundle.json || { + echo "::error::.auths/ci-bundle.json no longer verifies HEAD — the maintainer key likely rotated after the bundle was exported. Regenerate with: auths id export-bundle --alias main --output .auths/ci-bundle.json --max-age-secs 31536000, commit, and re-tag." + exit 1 + } + + - name: Guard — committed bundle still covers the signer (Windows) + if: matrix.ext == '.zip' + shell: pwsh run: | - git init -q "$HOME/.auths" - git -C "$HOME/.auths" fetch -q "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY" 'refs/auths/*:refs/auths/*' - git -C "$HOME/.auths" for-each-ref refs/auths/ + .\staging\auths.exe verify HEAD --identity-bundle .auths/ci-bundle.json + if ($LASTEXITCODE -ne 0) { + Write-Host "::error::.auths/ci-bundle.json no longer verifies HEAD - regenerate with 'auths id export-bundle --alias main --output .auths/ci-bundle.json --max-age-secs 31536000', commit, and re-tag." + exit 1 + } - name: Verify own attestation (Unix) if: matrix.ext == '.tar.gz' - run: ./staging/auths artifact verify "${{ matrix.asset_name }}${{ matrix.ext }}" + run: ./staging/auths artifact verify "${{ matrix.asset_name }}${{ matrix.ext }}" --identity-bundle .auths/ci-bundle.json - name: Verify own attestation (Windows) if: matrix.ext == '.zip' shell: pwsh - run: .\staging\auths.exe artifact verify "${{ matrix.asset_name }}${{ matrix.ext }}" + run: .\staging\auths.exe artifact verify "${{ matrix.asset_name }}${{ matrix.ext }}" --identity-bundle .auths/ci-bundle.json - name: Upload artifact uses: actions/upload-artifact@v4 diff --git a/crates/auths-cli/src/cli.rs b/crates/auths-cli/src/cli.rs index d1f04500..d3d57da2 100644 --- a/crates/auths-cli/src/cli.rs +++ b/crates/auths-cli/src/cli.rs @@ -38,7 +38,6 @@ use crate::commands::oobi::OobiCommand; use crate::commands::org::OrgCommand; use crate::commands::policy::PolicyCommand; use crate::commands::publish::PublishCommand; -use crate::commands::registry::RegistryCommand; use crate::commands::reset::ResetCommand; use crate::commands::scim::ScimCommand; use crate::commands::sign::SignCommand; @@ -156,8 +155,6 @@ pub enum RootCommand { Audit(AuditCommand), #[command(hide = true)] Auth(AuthCommand), - #[command(hide = true)] - Registry(RegistryCommand), // ── Internal (visible via --help-all) ── #[command(hide = true)] diff --git a/crates/auths-cli/src/commands/mod.rs b/crates/auths-cli/src/commands/mod.rs index dd2bfa27..372ef06c 100644 --- a/crates/auths-cli/src/commands/mod.rs +++ b/crates/auths-cli/src/commands/mod.rs @@ -40,7 +40,6 @@ pub mod org; pub mod policy; pub mod provision; pub mod publish; -pub mod registry; pub mod reset; pub mod scim; pub mod sign; diff --git a/crates/auths-cli/src/commands/registry.rs b/crates/auths-cli/src/commands/registry.rs deleted file mode 100644 index 01b1701e..00000000 --- a/crates/auths-cli/src/commands/registry.rs +++ /dev/null @@ -1,153 +0,0 @@ -//! Registry propagation commands: push/pull `refs/auths/registry` over a git -//! remote, so a second machine sees this machine's identity events (rotations, -//! revocations, new devices) over a wire instead of a shared filesystem. - -use std::path::PathBuf; - -use anyhow::{Result, anyhow}; -use clap::{Parser, Subcommand}; -use serde::Serialize; - -use auths_sdk::storage::{ - MergeOutcome, MergedCredentials, MergedKel, PushOutcome, pull_registry, push_registry, -}; - -use crate::commands::executable::ExecutableCommand; -use crate::config::CliConfig; -use crate::ux::format::{JsonResponse, Output, is_json_mode}; - -/// Sync the local identity registry with a git remote. -#[derive(Parser, Debug, Clone)] -#[command( - name = "registry", - about = "Push/pull the identity registry to/from a git remote", - after_help = "The registry travels as the packed git ref refs/auths/registry, so any -git remote (a bare repo over file://, git://, ssh://, https://) can carry it -between machines. Push is fast-forward-only (the registry is append-only). -Pull authenticates every fetched KEL (prefix binding, signature replay, -fork refusal) before merging it into the local registry. - -Examples: - auths registry push git://wire.local/registry.git # publish this machine's registry - auths registry pull git://wire.local/registry.git # merge another machine's events - -Related: - auths id show — The identity whose events the registry carries - auths device list — Devices learned from merged registries" -)] -pub struct RegistryCommand { - #[command(subcommand)] - pub subcommand: RegistrySubcommand, -} - -/// Registry sync subcommands. -#[derive(Subcommand, Debug, Clone)] -pub enum RegistrySubcommand { - /// Publish the local registry ref to a git remote (fast-forward only). - Push { - /// Git remote URL (file://, git://, ssh://, https://). - #[arg(value_name = "REMOTE")] - remote: String, - }, - /// Fetch a remote registry and merge its authenticated KELs locally. - Pull { - /// Git remote URL (file://, git://, ssh://, https://). - #[arg(value_name = "REMOTE")] - remote: String, - }, -} - -#[derive(Debug, Serialize)] -struct PushReport { - remote: String, - outcome: PushOutcome, -} - -#[derive(Debug, Serialize)] -struct PullReport { - remote: String, - merged: Vec, - #[serde(flatten)] - credentials: MergedCredentials, -} - -impl ExecutableCommand for RegistryCommand { - fn execute(&self, ctx: &CliConfig) -> Result<()> { - let registry_root = resolve_registry_root(ctx)?; - match &self.subcommand { - RegistrySubcommand::Push { remote } => { - let outcome = push_registry(®istry_root, remote)?; - if is_json_mode() { - JsonResponse::success( - "registry push", - PushReport { - remote: remote.clone(), - outcome, - }, - ) - .print()?; - } else { - let out = Output::new(); - match outcome { - PushOutcome::Updated => { - out.print_success(&format!("Registry pushed to {remote}")); - } - PushOutcome::AlreadyCurrent => { - out.print_info(&format!("Remote {remote} is already current")); - } - } - } - Ok(()) - } - RegistrySubcommand::Pull { remote } => { - let report = pull_registry(®istry_root, remote)?; - if is_json_mode() { - JsonResponse::success( - "registry pull", - PullReport { - remote: remote.clone(), - merged: report.merged, - credentials: report.credentials, - }, - ) - .print()?; - } else { - let out = Output::new(); - out.print_success(&format!("Registry pulled from {remote}")); - for kel in &report.merged { - let line = match &kel.outcome { - MergeOutcome::Imported { events } => { - format!("{} imported ({events} events)", kel.prefix) - } - MergeOutcome::Advanced { events } => { - format!("{} advanced (+{events} events)", kel.prefix) - } - MergeOutcome::AlreadyCurrent => { - format!("{} already current", kel.prefix) - } - }; - out.print_info(&line); - } - let creds = &report.credentials; - if creds.credentials_imported > 0 || creds.tel_events_imported > 0 { - out.print_info(&format!( - "{} credential(s) and {} status event(s) imported", - creds.credentials_imported, creds.tel_events_imported - )); - } - } - Ok(()) - } - } - } -} - -/// The registry root the sync operates on: the global `--repo` override when -/// given, otherwise the configured Auths home. -fn resolve_registry_root(ctx: &CliConfig) -> Result { - if let Some(repo) = &ctx.repo_path { - return Ok(repo.clone()); - } - auths_sdk::paths::auths_home_with_config(&ctx.env_config) - .map_err(|e| anyhow!("Could not locate ~/.auths: {e}")) -} diff --git a/crates/auths-cli/src/commands/verify_commit.rs b/crates/auths-cli/src/commands/verify_commit.rs index e7fbbc7b..57112ef5 100644 --- a/crates/auths-cli/src/commands/verify_commit.rs +++ b/crates/auths-cli/src/commands/verify_commit.rs @@ -63,7 +63,9 @@ pub struct VerifyCommitCommand { /// Path to an identity bundle JSON whose root `did:keri:` is pinned as a trusted /// root for this verification (CI/stateless commit verification). The bundle is - /// freshness-checked; an unreadable or stale bundle fails closed. + /// freshness-checked; an unreadable or stale bundle fails closed. When absent, + /// a committed `.auths/ci-bundle.json` at the repo root is discovered and used + /// the same way — that is how a plain clone verifies with no flags. #[arg(long, value_parser)] pub identity_bundle: Option, } @@ -185,7 +187,11 @@ pub async fn handle_verify_commit( pinned_roots.push(own_root); } let mut bundle_kel: Option = None; - if let Some(bundle_path) = &cmd.identity_bundle { + 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)) => { // Evidence-only (RT-005): the bundle is *evidence for* a root that diff --git a/crates/auths-cli/src/commands/verify_helpers.rs b/crates/auths-cli/src/commands/verify_helpers.rs index 428bf5aa..c0522d6b 100644 --- a/crates/auths-cli/src/commands/verify_helpers.rs +++ b/crates/auths-cli/src/commands/verify_helpers.rs @@ -24,6 +24,31 @@ pub fn load_project_pinned_roots() -> Vec { .unwrap_or_default() } +/// The committed identity bundle, when the repository carries one. +/// +/// A repo that commits `.auths/ci-bundle.json` (produced by +/// `auths id export-bundle`) ships its signer's KEL with the code, so a fresh +/// clone verifies with no flags and no network. The bundle stays evidence-only: +/// its root must still match an independently pinned root (`.auths/roots` or +/// self-trust), exactly as with an explicit `--identity-bundle`, which always +/// wins over discovery. +/// +/// Usage: +/// ```ignore +/// let bundle = cmd.identity_bundle.clone().or_else(discover_project_bundle); +/// ``` +pub fn discover_project_bundle() -> Option { + let output = crate::subprocess::git_command(&["rev-parse", "--show-toplevel"]) + .output() + .ok()?; + if !output.status.success() { + return None; + } + let root = std::path::PathBuf::from(String::from_utf8_lossy(&output.stdout).trim()); + let bundle = root.join(".auths").join("ci-bundle.json"); + bundle.is_file().then_some(bundle) +} + /// The current repository's `origin` URL, when there is one. /// /// Used as the default KEL transport: a repo carries its signer's KEL on the same diff --git a/crates/auths-cli/src/main.rs b/crates/auths-cli/src/main.rs index eab20aaa..39c20d27 100644 --- a/crates/auths-cli/src/main.rs +++ b/crates/auths-cli/src/main.rs @@ -119,7 +119,6 @@ fn run() -> Result<()> { RootCommand::Credential(cmd) => cmd.execute(&ctx), RootCommand::Audit(cmd) => cmd.execute(&ctx), RootCommand::Auth(cmd) => cmd.execute(&ctx), - RootCommand::Registry(cmd) => cmd.execute(&ctx), // Internal RootCommand::Emergency(cmd) => cmd.execute(&ctx), RootCommand::Agent(cmd) => cmd.execute(&ctx), diff --git a/crates/auths-cli/tests/cases/third_party_verify.rs b/crates/auths-cli/tests/cases/third_party_verify.rs index 8ede7ab3..91eef04d 100644 --- a/crates/auths-cli/tests/cases/third_party_verify.rs +++ b/crates/auths-cli/tests/cases/third_party_verify.rs @@ -4,9 +4,10 @@ //! Every other verification test in this suite runs as the signer, with a populated //! `~/.auths`. That is exactly why a total failure of this property shipped //! undetected: `auths init` wrote `/.auths/roots` but never staged it, so the -//! trust pin never reached a cloner; and nothing mirrored `refs/auths/registry` to -//! the remote, so the KEL never travelled either. A fresh clone failed with "KEL not -//! found" while the engine itself was correct the whole time. +//! trust pin never reached a cloner. A fresh clone failed with "KEL not found" +//! while the engine itself was correct the whole time. The KEL travels the same +//! way the pin does: committed in the clone, as `.auths/ci-bundle.json` +//! (`auths id export-bundle`) — no side-channel ref mirror, no network fetch. //! //! These tests play the *second person*: a machine with no auths identity, a plain //! `git clone`, and no flags. They must keep failing closed when trust is absent. @@ -20,9 +21,9 @@ use super::helpers::TestEnv; /// An identity that has signed HEAD and pushed it to a bare origin. /// -/// Returns the signer's env and the origin path. The push goes through a plain -/// `git push`: the managed pre-push hook is what mirrors `refs/auths/registry`, and -/// this test would be worthless if it pushed the registry by hand. +/// Returns the signer's env and the origin path. The signer's KEL travels inside +/// the committed `.auths/ci-bundle.json` — the same clone that carries the code +/// and the trust pin carries the evidence, so verification needs no network. fn signer_with_pushed_commit() -> (TestEnv, PathBuf) { let alice = TestEnv::new(); alice.init_identity(); @@ -36,7 +37,33 @@ fn signer_with_pushed_commit() -> (TestEnv, PathBuf) { assert!(init_bare.status.success(), "git init --bare failed"); std::fs::write(alice.repo_path.join("main.rs"), "fn main() {}").unwrap(); - let add = alice.git_cmd().args(["add", "main.rs"]).output().unwrap(); + + let bundle = alice.repo_path.join(".auths").join("ci-bundle.json"); + let export = alice + .cmd("auths") + .args([ + "id", + "export-bundle", + "--alias", + "main", + "--output", + bundle.to_str().unwrap(), + "--max-age-secs", + "3600", + ]) + .output() + .unwrap(); + assert!( + export.status.success(), + "id export-bundle failed: {}", + String::from_utf8_lossy(&export.stderr) + ); + + let add = alice + .git_cmd() + .args(["add", "main.rs", ".auths/ci-bundle.json"]) + .output() + .unwrap(); assert!(add.status.success(), "git add failed"); // A plain signed commit — no `auths sign`. The pin must ride along on its own. @@ -147,22 +174,23 @@ fn the_trust_pin_travels_with_the_repository() { ); } -/// The KEL must reach the remote the code went to, via the managed pre-push hook — -/// not via a manual `auths registry push`. +/// The KEL must be *in the clone* — the committed bundle, like the pin, travels +/// with the code instead of over a side-channel ref mirror. #[test] -fn a_plain_push_mirrors_the_registry_to_the_remote() { - let (alice, origin) = signer_with_pushed_commit(); +fn the_committed_bundle_carries_the_kel_in_the_clone() { + let (_alice, origin) = signer_with_pushed_commit(); + let bob = observer_cloning(&origin); - let refs = alice - .git_cmd() - .arg("ls-remote") - .arg(&origin) - .output() - .unwrap(); - let refs = String::from_utf8_lossy(&refs.stdout); + let bundle = bob.repo_path.join(".auths").join("ci-bundle.json"); + assert!( + bundle.is_file(), + ".auths/ci-bundle.json must be committed, or a cloner has no KEL to verify against" + ); + let content = std::fs::read_to_string(&bundle).unwrap(); assert!( - refs.contains("refs/auths/registry"), - "a plain `git push` must mirror the signer's KEL to the remote, got:\n{refs}" + content.contains("kel_attachments"), + "the bundle must carry signature attachments, got: {}", + &content[..content.len().min(200)] ); } diff --git a/crates/auths-rp/src/registry_sync.rs b/crates/auths-rp/src/registry_sync.rs index dcad6b61..cd7f0865 100644 --- a/crates/auths-rp/src/registry_sync.rs +++ b/crates/auths-rp/src/registry_sync.rs @@ -18,6 +18,13 @@ //! **non-forced**, so a remote that rewinds its history (e.g. to resurrect a revoked //! credential by serving an older tip) is rejected and the replica keeps the newest state it //! has seen. +//! +//! **Transport scope (deliberate):** the shipped `GitRegistrySync` adapter links git2 +//! without network transports, so it syncs from **local paths / `file://` remotes only** +//! (an operator-local mirror kept fresh by whatever moves bytes on that host). Network +//! distribution is not this crate's job; relying parties that receive KELs over a wire +//! get them in-band (signed presentation bundles) and enforce freshness per request. +//! [`RegistrySync`] is the port where a network adapter would land if that changes. use std::sync::Arc; use std::time::Duration; @@ -35,7 +42,8 @@ pub const AUTHS_REFS_GLOB: &str = "refs/auths/*"; pub struct RemoteUrl(String); impl RemoteUrl { - /// Parse a non-empty remote URL (e.g. `https://…`, `ssh://…`, `file:///srv/registry`). + /// Parse a non-empty remote URL. The shipped `GitRegistrySync` adapter reaches + /// local paths and `file://` remotes only (no network transport is linked). /// /// Args: /// * `s`: The git remote URL of the authoritative registry. diff --git a/crates/auths-sdk/src/domains/identity/registration.rs b/crates/auths-sdk/src/domains/identity/registration.rs index d15884dd..5632b634 100644 --- a/crates/auths-sdk/src/domains/identity/registration.rs +++ b/crates/auths-sdk/src/domains/identity/registration.rs @@ -20,10 +20,10 @@ use crate::domains::identity::types::RegistrationOutcome; // possible failure shape: it looks configured and fails at the network. // // Nothing needs it. Identity, signing, and verification are entirely local and -// git-native — a signer's KEL travels to a relying party over the same git remote -// as the code (see `workflows::commit_hooks::PRE_PUSH_HOOK`). "No central server" -// is the product's headline claim, and a default that contradicts it is worse than -// no default. +// git-native — a signer's KEL travels to a relying party inside the committed +// identity bundle (`auths id export-bundle` → `.auths/ci-bundle.json`). "No +// central server" is the product's headline claim, and a default that +// contradicts it is worse than no default. // // A registry is now opt-in and explicit: pass `--registry ` or set // `AUTHS_REGISTRY_URL`. Callers surface `RegistrationError::NoRegistryConfigured` diff --git a/crates/auths-sdk/src/storage.rs b/crates/auths-sdk/src/storage.rs index d7da9467..52ec8ab1 100644 --- a/crates/auths-sdk/src/storage.rs +++ b/crates/auths-sdk/src/storage.rs @@ -2,13 +2,10 @@ //! //! Gated behind the `backend-git` feature. -#[cfg(feature = "backend-git")] -pub use auths_id::keri::sync::{MergeOutcome, MergedCredentials, MergedKel}; #[cfg(feature = "backend-git")] pub use auths_id::storage::GitWitnessReceiptLookup; #[cfg(feature = "backend-git")] pub use auths_storage::git::{ - GitAttestationStorage, GitIdentityStorage, GitRegistryBackend, PushOutcome, - RegistryAttestationStorage, RegistryConfig, RegistryIdentityStorage, RegistryPullReport, - RegistrySyncError, pull_registry, push_registry, + GitAttestationStorage, GitIdentityStorage, GitRegistryBackend, + RegistryAttestationStorage, RegistryConfig, RegistryIdentityStorage, }; diff --git a/crates/auths-sdk/src/workflows/commit_hooks.rs b/crates/auths-sdk/src/workflows/commit_hooks.rs index cc4e08e8..3fb655c7 100644 --- a/crates/auths-sdk/src/workflows/commit_hooks.rs +++ b/crates/auths-sdk/src/workflows/commit_hooks.rs @@ -86,33 +86,20 @@ fi exit 0 "#; -/// The managed `pre-push` hook. Mirrors `refs/auths/registry` to the remote the -/// code is going to, so a clone of that remote can resolve the signer's KEL. +/// The managed `pre-push` hook. Exists only to chain: `core.hooksPath` +/// *replaces* `.git/hooks`, so a managed hook that did not chain would silently +/// disable every repo-local pre-push (husky, pre-commit, prek) on the machine. /// -/// Three properties are deliberate: -/// -/// * **Non-fatal.** A registry mirror must never block a code push. Every failure -/// path exits 0 — no write access to the remote is the common, expected case. -/// * **Opt-out via `auths.autopush=false`.** The registry is public by -/// construction (a KEL is public keys and events), but pushing to a remote you -/// do not own should stay refusable. -/// * **Chains to the repo's own `pre-push`.** `core.hooksPath` *replaces* -/// `.git/hooks`, so a managed hook that did not chain would silently disable -/// every repo-local pre-push (husky, pre-commit, prek) on the machine. +/// It used to mirror `refs/auths/registry` to the push remote, but that mirror +/// silently no-oped against real (ssh/https) remotes — the CLI's git layer links +/// no network transport — so the published ref drifted from the signer's real +/// KEL. KEL distribution is the committed identity bundle now +/// (`auths id export-bundle` → `.auths/ci-bundle.json`), which travels with the +/// code and needs no side-channel push. pub const PRE_PUSH_HOOK: &str = r#"#!/bin/sh # auths pre-push hook v1 — managed by `auths init`, checked by `auths doctor`. -# Mirrors refs/auths/registry to the remote being pushed to, so a clone of that -# remote carries the KEL `auths verify` needs. Fast-forward-only and non-fatal: -# a registry mirror must never block a code push. Disable with: -# git config auths.autopush false - -REMOTE_URL="$2" - -if [ -n "$REMOTE_URL" ] && - [ "$(git config --get auths.autopush 2>/dev/null)" != "false" ] && - command -v auths >/dev/null 2>&1; then - auths registry push "$REMOTE_URL" >/dev/null 2>&1 || true -fi +# Chains to the repo's own pre-push hook: core.hooksPath replaces .git/hooks, +# so without this the managed hooks would disable repo-local ones. REPO_HOOK="$(git rev-parse --git-dir 2>/dev/null)/hooks/pre-push" if [ -x "$REPO_HOOK" ]; then diff --git a/crates/auths-storage/src/git/mod.rs b/crates/auths-storage/src/git/mod.rs index a60fc164..da5ce929 100644 --- a/crates/auths-storage/src/git/mod.rs +++ b/crates/auths-storage/src/git/mod.rs @@ -9,7 +9,6 @@ pub mod remote; pub mod standalone_attestation; pub mod standalone_export; pub mod standalone_identity; -pub mod sync; mod tree_ops; pub mod vfs; @@ -28,5 +27,4 @@ pub use remote::{ pub use standalone_attestation::GitAttestationStorage; pub use standalone_export::GitRefSink; pub use standalone_identity::GitIdentityStorage; -pub use sync::{PushOutcome, RegistryPullReport, RegistrySyncError, pull_registry, push_registry}; pub use vfs::{FixedClock, OsVfs, Vfs}; diff --git a/crates/auths-storage/src/git/sync.rs b/crates/auths-storage/src/git/sync.rs deleted file mode 100644 index c611c5df..00000000 --- a/crates/auths-storage/src/git/sync.rs +++ /dev/null @@ -1,254 +0,0 @@ -//! Registry propagation over a git remote — push/pull of `refs/auths/registry`. -//! -//! The wire is plain git, so any git remote (a bare repo over `file://`, -//! `git://`, `ssh://`, `https://`) can carry a registry between machines — -//! no shared filesystem. -//! -//! **Push** publishes the local packed registry ref, fast-forward only: the -//! registry is append-only, so a non-fast-forward means the histories -//! diverged and the push is refused ([`RegistrySyncError::Diverged`]) rather -//! than forced. -//! -//! **Pull** fetches the remote's ref into a throwaway snapshot and runs the -//! validated KEL merge ([`merge_registries`]) into the local registry — -//! prefix binding, authenticated replay, rollback floor, fork refusal. -//! Nothing from the remote is ever persisted unvalidated. - -use std::cell::RefCell; -use std::path::Path; - -use std::collections::HashSet; - -use auths_id::keri::sync::{ - KelCaps, MergedCredentials, MergedKel, RegistryMergeError, merge_credentials_and_tel, - merge_registries, -}; -use auths_id::ports::registry::RegistryError; -use git2::Repository; - -use super::remote::{MAX_KEL_BYTES, MAX_KEL_EVENTS, RemoteKelError, RemoteKelSource}; -use super::{GitRegistryBackend, REGISTRY_REF, RegistryConfig}; - -/// Errors pushing or pulling a registry over a git remote. -#[derive(Debug, thiserror::Error)] -#[non_exhaustive] -pub enum RegistrySyncError { - /// The local registry repo could not be opened. - #[error("could not open the local registry at '{path}': {source}")] - OpenLocal { - /// The local registry path. - path: String, - /// The underlying git2 error. - #[source] - source: git2::Error, - }, - - /// The local registry has no packed registry ref to push. - #[error("local registry has no '{reference}' to push (run `auths init` first)")] - NothingToPush { - /// The expected ref name. - reference: String, - }, - - /// A git transport operation (connect / fetch / push) failed. - #[error("git transport with '{url}' failed: {source}")] - Transport { - /// The remote URL. - url: String, - /// The underlying git2 error. - #[source] - source: git2::Error, - }, - - /// The remote's registry history is not an ancestor of the local one — - /// pushing would discard remote events. The registry is append-only; - /// reconcile by pulling first. - #[error( - "remote registry has diverged from the local one (non-fast-forward); \ - pull first, then push" - )] - Diverged, - - /// The remote refused the ref update. - #[error("remote rejected the push of {reference}: {status}")] - PushRejected { - /// The ref the remote refused. - reference: String, - /// The remote's status message. - status: String, - }, - - /// Fetching the remote registry snapshot failed. - #[error(transparent)] - Fetch(#[from] RemoteKelError), - - /// The validated merge refused the fetched registry. - #[error(transparent)] - Merge(#[from] RegistryMergeError), - - /// The local registry backend failed. - #[error("local registry error: {0}")] - Storage(#[from] RegistryError), -} - -/// The result of a registry push. -#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)] -#[serde(rename_all = "snake_case")] -pub enum PushOutcome { - /// The remote's registry ref was advanced (or created). - Updated, - /// The remote already holds the local tip — nothing to send. - AlreadyCurrent, -} - -/// Push the local packed registry ref to `url`, fast-forward only. -/// -/// Refuses with [`RegistrySyncError::Diverged`] when the remote holds history -/// the local registry does not — the registry is append-only, so a push never -/// rewrites a remote. -/// -/// Args: -/// * `local_root`: The local registry root (the `~/.auths` repo). -/// * `url`: The git remote to publish to. -/// -/// Usage: -/// ```ignore -/// let outcome = push_registry(&auths_home, "git://wire.local/registry.git")?; -/// ``` -pub fn push_registry(local_root: &Path, url: &str) -> Result { - let repo = Repository::open(local_root).map_err(|e| RegistrySyncError::OpenLocal { - path: local_root.display().to_string(), - source: e, - })?; - let local_oid = repo - .find_reference(REGISTRY_REF) - .ok() - .and_then(|r| r.target()) - .ok_or_else(|| RegistrySyncError::NothingToPush { - reference: REGISTRY_REF.to_string(), - })?; - - let transport = |source: git2::Error| RegistrySyncError::Transport { - url: url.to_string(), - source, - }; - let mut remote = repo.remote_anonymous(url).map_err(transport)?; - - // Learn the remote tip (if any) before deciding what a push would do: - // fetch the remote's registry ref into a throwaway local ref. A remote - // without the ref is a fresh wire — the fetch is a no-op and the temp ref - // never appears. (Deliberately not `Remote::list()`: that needs a second - // connection and an empty remote advertises zero refs.) - const INCOMING_REF: &str = "refs/auths/sync/incoming"; - remote - .fetch(&[format!("+{REGISTRY_REF}:{INCOMING_REF}")], None, None) - .map_err(transport)?; - let remote_oid = repo - .find_reference(INCOMING_REF) - .ok() - .and_then(|r| r.target()); - if let Ok(mut incoming) = repo.find_reference(INCOMING_REF) { - let _ = incoming.delete(); - } - - if let Some(remote_oid) = remote_oid { - if remote_oid == local_oid { - return Ok(PushOutcome::AlreadyCurrent); - } - // Fast-forward check — the remote commit is in the local odb from the - // fetch above. - let fast_forward = repo - .graph_descendant_of(local_oid, remote_oid) - .map_err(transport)?; - if !fast_forward { - return Err(RegistrySyncError::Diverged); - } - } - - // Plain (non-force) refspec: the ancestry was proven above, and the remote - // side's own fast-forward rule stays in force as a second guard. - let rejection: RefCell> = RefCell::new(None); - { - let mut callbacks = git2::RemoteCallbacks::new(); - callbacks.push_update_reference(|reference, status| { - if let Some(status) = status { - *rejection.borrow_mut() = Some((reference.to_string(), status.to_string())); - } - Ok(()) - }); - let mut options = git2::PushOptions::new(); - options.remote_callbacks(callbacks); - remote - .push( - &[format!("{REGISTRY_REF}:{REGISTRY_REF}")], - Some(&mut options), - ) - .map_err(transport)?; - } - if let Some((reference, status)) = rejection.into_inner() { - return Err(RegistrySyncError::PushRejected { reference, status }); - } - Ok(PushOutcome::Updated) -} - -/// The full report of a registry pull: the per-identity KEL merge plus the -/// credential/TEL artifacts that ride on those authenticated KELs. -#[derive(Debug, Clone, serde::Serialize)] -pub struct RegistryPullReport { - /// Per-identity KEL merge outcomes (the authenticated trust core). - pub merged: Vec, - /// Credential body + TEL chain import counts (re-verified at verify time). - #[serde(flatten)] - pub credentials: MergedCredentials, -} - -/// Pull the remote registry at `url` and merge it into the local registry under -/// the validated-merge guards. -/// -/// Two layers, in dependency order: -/// -/// 1. **KELs** — [`merge_registries`] authenticates every identity's KEL -/// (prefix binding, signed replay, rollback floor, fork refusal) and appends -/// only the strictly-newer authenticated suffix. This is the trust core. -/// 2. **Credentials + TEL** — [`merge_credentials_and_tel`] then materializes the -/// ACDC bodies and TEL chains for the issuers just authenticated, so the local -/// machine has everything a `credential verify` needs. These artifacts are -/// re-verified at verify time against the authenticated KEL (issuer signature -/// + KEL-anchored TEL), and a tampered body/chain is refused on import. -/// -/// Provisions the local registry if it does not exist yet, so a fresh machine -/// can pull before it ever runs `auths init`. This is what makes a cold-machine -/// import reconstruct a WHOLE fleet (artifacts AND credentials), not KELs alone. -/// -/// Args: -/// * `local_root`: The local registry root (the `~/.auths` repo). -/// * `url`: The git remote to fetch from. -/// -/// Usage: -/// ```ignore -/// let report = pull_registry(&auths_home, "git://wire.local/registry.git")?; -/// ``` -pub fn pull_registry( - local_root: &Path, - url: &str, -) -> Result { - let snapshot = RemoteKelSource::new(url).fetch_snapshot()?; - let local = - GitRegistryBackend::from_config_unchecked(RegistryConfig::single_tenant(local_root)); - local.init_if_needed()?; - let caps = KelCaps { - max_events: MAX_KEL_EVENTS, - max_bytes: MAX_KEL_BYTES, - }; - let merged = merge_registries(snapshot.backend(), &local, &caps)?; - - // Only issuers whose KEL was authenticated above are eligible to carry - // credentials/TEL onto this machine — a dangling artifact is never imported. - let authenticated: HashSet<_> = merged.iter().map(|m| m.prefix.clone()).collect(); - let credentials = merge_credentials_and_tel(snapshot.backend(), &local, &authenticated)?; - - Ok(RegistryPullReport { - merged, - credentials, - }) -} diff --git a/crates/auths-storage/tests/cases/mod.rs b/crates/auths-storage/tests/cases/mod.rs index e71207be..718f5ded 100644 --- a/crates/auths-storage/tests/cases/mod.rs +++ b/crates/auths-storage/tests/cases/mod.rs @@ -6,4 +6,3 @@ mod concurrent_batch; mod concurrent_writes; mod credential_registry; mod registry_contract; -mod registry_sync; diff --git a/crates/auths-storage/tests/cases/registry_sync.rs b/crates/auths-storage/tests/cases/registry_sync.rs deleted file mode 100644 index f578c1e4..00000000 --- a/crates/auths-storage/tests/cases/registry_sync.rs +++ /dev/null @@ -1,241 +0,0 @@ -//! Registry push/pull over a git remote: fast-forward-only publish, and the -//! validated merge on pull (authenticated import/advance, unsigned refusal, -//! fork refusal, idempotence). - -use auths_id::keri::sync::{MergeOutcome, RegistryMergeError}; -use auths_id::ports::registry::RegistryBackend; -use auths_keri::{ - Event, IndexedSignature, IxnEvent, KeriSequence, Prefix, Said, Seal, VersionString, - finalize_ixn_event, serialize_attachment, serialize_for_signing, -}; -use auths_storage::git::sync::{PushOutcome, RegistrySyncError, pull_registry, push_registry}; -use auths_storage::git::{GitRegistryBackend, RegistryConfig}; -use ring::signature::Ed25519KeyPair; -use tempfile::TempDir; - -use super::mock_ed25519_keypairs::{mock_inception_event, mock_keypair}; - -/// Sign `event` with `signer`; return the CESR attachment bytes. -fn attachment_for(signer: &Ed25519KeyPair, event: &Event) -> Vec { - let sig = signer - .sign(&serialize_for_signing(event).unwrap()) - .as_ref() - .to_vec(); - serialize_attachment(&[IndexedSignature { - index: 0, - prior_index: None, - sig, - }]) - .unwrap() -} - -fn fresh_registry() -> (GitRegistryBackend, TempDir) { - let dir = tempfile::tempdir().unwrap(); - let backend = - GitRegistryBackend::from_config_unchecked(RegistryConfig::single_tenant(dir.path())); - backend.init_if_needed().unwrap(); - (backend, dir) -} - -/// A registry holding identity `index`'s fully-signed inception. -fn signed_registry(index: usize) -> (GitRegistryBackend, TempDir, Ed25519KeyPair, Event, Prefix) { - let (backend, dir) = fresh_registry(); - let signer = mock_keypair(index * 2); - let event = mock_inception_event(index); - let prefix = event.prefix().clone(); - let attachment = attachment_for(&signer, &event); - backend - .append_signed_event(&prefix, &event, &attachment) - .unwrap(); - (backend, dir, signer, event, prefix) -} - -/// A signed interaction event extending `prior` at sequence `seq`. -fn signed_ixn( - signer: &Ed25519KeyPair, - prefix: &Prefix, - prior: &Said, - seq: u128, - anchors: Vec, -) -> (Event, Vec) { - let ixn = IxnEvent { - v: VersionString::placeholder(), - d: Said::default(), - i: prefix.clone(), - s: KeriSequence::new(seq), - p: prior.clone(), - a: anchors, - }; - let event = Event::Ixn(finalize_ixn_event(ixn).unwrap()); - let attachment = attachment_for(signer, &event); - (event, attachment) -} - -fn bare_remote() -> (TempDir, String) { - let dir = tempfile::tempdir().unwrap(); - git2::Repository::init_bare(dir.path()).unwrap(); - let url = format!("file://{}", dir.path().display()); - (dir, url) -} - -#[test] -fn push_then_pull_imports_authenticated_kel() { - let (_src, src_dir, _signer, event, prefix) = signed_registry(0); - let (_remote_dir, url) = bare_remote(); - - assert_eq!( - push_registry(src_dir.path(), &url).unwrap(), - PushOutcome::Updated - ); - // Re-pushing the same tip is a no-op, not an error. - assert_eq!( - push_registry(src_dir.path(), &url).unwrap(), - PushOutcome::AlreadyCurrent - ); - - let dest_dir = tempfile::tempdir().unwrap(); - let merged = pull_registry(dest_dir.path(), &url).unwrap().merged; - assert_eq!(merged.len(), 1); - assert_eq!(merged[0].prefix, prefix); - assert!(matches!( - merged[0].outcome, - MergeOutcome::Imported { events: 1 } - )); - - // The destination's copy is the same event WITH its signature attachment — - // still authenticatable, not merely structurally replayable. - let dest = - GitRegistryBackend::from_config_unchecked(RegistryConfig::single_tenant(dest_dir.path())); - assert_eq!(dest.get_event(&prefix, 0).unwrap(), event); - assert!(dest.get_attachment(&prefix, 0).unwrap().is_some()); -} - -#[test] -fn pull_advances_then_reports_already_current() { - let (src, src_dir, signer, event, prefix) = signed_registry(1); - let (_remote_dir, url) = bare_remote(); - push_registry(src_dir.path(), &url).unwrap(); - - let dest_dir = tempfile::tempdir().unwrap(); - pull_registry(dest_dir.path(), &url).unwrap(); - - // The source advances (a signed interaction) and re-publishes (fast-forward). - let (ixn, ixn_att) = signed_ixn(&signer, &prefix, event.said(), 1, vec![]); - src.append_signed_event(&prefix, &ixn, &ixn_att).unwrap(); - assert_eq!( - push_registry(src_dir.path(), &url).unwrap(), - PushOutcome::Updated - ); - - let merged = pull_registry(dest_dir.path(), &url).unwrap().merged; - assert!(matches!( - merged[0].outcome, - MergeOutcome::Advanced { events: 1 } - )); - - // Pulling again changes nothing — the merge is idempotent. - let merged = pull_registry(dest_dir.path(), &url).unwrap().merged; - assert!(matches!(merged[0].outcome, MergeOutcome::AlreadyCurrent)); -} - -#[test] -fn push_refuses_diverged_remote() { - let (_a, a_dir, _sa, _ea, _pa) = signed_registry(2); - let (_b, b_dir, _sb, _eb, _pb) = signed_registry(3); - let (_remote_dir, url) = bare_remote(); - - push_registry(a_dir.path(), &url).unwrap(); - // B's registry shares no history with A's — pushing it would discard A. - let err = push_registry(b_dir.path(), &url).unwrap_err(); - assert!(matches!(err, RegistrySyncError::Diverged)); -} - -#[test] -fn push_without_local_registry_fails_actionably() { - let empty = tempfile::tempdir().unwrap(); - let (_remote_dir, url) = bare_remote(); - let err = push_registry(empty.path(), &url).unwrap_err(); - assert!(matches!(err, RegistrySyncError::OpenLocal { .. })); -} - -#[test] -fn pull_refuses_unsigned_kel() { - // An event persisted WITHOUT its signature attachment cannot be - // authenticated — the pull refuses the whole registry (fail closed). - let (backend, src_dir) = fresh_registry(); - let event = mock_inception_event(4); - let prefix = event.prefix().clone(); - backend.append_event(&prefix, &event).unwrap(); - let (_remote_dir, url) = bare_remote(); - push_registry(src_dir.path(), &url).unwrap(); - - let dest_dir = tempfile::tempdir().unwrap(); - let err = pull_registry(dest_dir.path(), &url).unwrap_err(); - assert!(matches!( - err, - RegistrySyncError::Merge(RegistryMergeError::MissingSignature { .. }) - )); -} - -#[test] -fn pull_refuses_forked_kel() { - // Source and destination share an inception but diverge at sequence 1 — - // the merge must refuse, never silently pick a side. - let (src, src_dir, signer, event, prefix) = signed_registry(5); - let (ixn_src, att_src) = signed_ixn(&signer, &prefix, event.said(), 1, vec![]); - src.append_signed_event(&prefix, &ixn_src, &att_src) - .unwrap(); - let (_remote_dir, url) = bare_remote(); - push_registry(src_dir.path(), &url).unwrap(); - - let dest_dir = tempfile::tempdir().unwrap(); - let dest = - GitRegistryBackend::from_config_unchecked(RegistryConfig::single_tenant(dest_dir.path())); - dest.init_if_needed().unwrap(); - dest.append_signed_event(&prefix, &event, &attachment_for(&signer, &event)) - .unwrap(); - let (ixn_dest, att_dest) = signed_ixn( - &signer, - &prefix, - event.said(), - 1, - vec![Seal::Digest { - d: event.said().clone(), - }], - ); - dest.append_signed_event(&prefix, &ixn_dest, &att_dest) - .unwrap(); - - let err = pull_registry(dest_dir.path(), &url).unwrap_err(); - assert!(matches!( - err, - RegistrySyncError::Merge(RegistryMergeError::Forked { sequence: 1, .. }) - )); -} - -#[test] -fn pull_into_unprovisioned_root_provisions_it() { - // A fresh machine (no ~/.auths at all) can pull before `auths init`. - let (_src, src_dir, _signer, _event, prefix) = signed_registry(6); - let (_remote_dir, url) = bare_remote(); - push_registry(src_dir.path(), &url).unwrap(); - - let dest_root = tempfile::tempdir().unwrap(); - let dest_path = dest_root.path().join("never-initialized"); - let merged = pull_registry(&dest_path, &url).unwrap().merged; - assert_eq!(merged.len(), 1); - - let dest = GitRegistryBackend::from_config_unchecked(RegistryConfig::single_tenant(&dest_path)); - assert!(matches!( - dest.get_tip(&prefix), - Ok(tip) if tip.sequence == 0 - )); -} - -#[test] -fn pull_from_remote_without_registry_fails_actionably() { - let (_remote_dir, url) = bare_remote(); - let dest_dir = tempfile::tempdir().unwrap(); - let err = pull_registry(dest_dir.path(), &url).unwrap_err(); - assert!(matches!(err, RegistrySyncError::Fetch(_))); -} From 1132c4a97db9620e68e9389efee9a1a0b6af31f3 Mon Sep 17 00:00:00 2001 From: bordumb Date: Sat, 18 Jul 2026 03:00:32 +0100 Subject: [PATCH 02/21] =?UTF-8?q?security:=20close=20#262=20=E2=80=94=20si?= =?UTF-8?q?gned=20KELs=20at=20every=20remaining=20untrusted=20boundary?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Residuals 1+2 close by removal, residual 3 by authentication: - --oobi (HttpOobiResolver) and --remote (RemoteKelSource, resolver reconcile/rollback machinery) are deleted: both fed bare unsigned events to a structural replay, and the mirror that would have populated their remotes is gone (#374). Verify resolution is committed-bundle or local registry — never a network fetch. - The JSON verify contract (WASM + FFI, one shared layer) now requires a parallel base64 CESR attachments array per KEL slice and authenticates each via validate_signed_kel before any downstream replay: delegator standalone, subject against the delegator's seals, issuer standalone. Forged or stripped attachments refuse with the new kelUnauthenticated verdict kind (TS union + exhaustiveness guard + Go kinds updated). - WASM verifyDeviceLink takes the same attachments input and authenticates before the link check, mirroring validateKelJson. - rt-002-allow annotations rewritten to the ingestion-boundary justification; the stale --remote/--oobi residual clauses are gone; check-verify-path-completeness passes with 0 violations. - Cross-surface parity battery gains forged-subject-kel-attachment, forged-issuer-kel-attachment, and stripped-attachment cases (asserted byte-identical native vs FFI); fixtures regenerated with attachments, which the Node/Python/Go/TS binding tests consume as-is. Closes #262. Auths-Id: did:keri:EB5cPHY0t-ejNC_rUzPS1dclTvd6kG-R9mQzjozCuGgd Auths-Device: did:keri:EB5cPHY0t-ejNC_rUzPS1dclTvd6kG-R9mQzjozCuGgd Auths-Anchor-Seq: 1 --- .../auths-cli/src/commands/unified_verify.rs | 15 - .../auths-cli/src/commands/verify_commit.rs | 71 +--- .../auths-cli/src/commands/verify_helpers.rs | 27 -- crates/auths-infra-http/src/lib.rs | 2 - crates/auths-infra-http/src/oobi_resolver.rs | 329 ----------------- crates/auths-sdk/src/keri/resolver.rs | 339 +----------------- .../auths-sdk/src/workflows/commit_trust.rs | 5 +- crates/auths-storage/src/git/mod.rs | 4 - crates/auths-storage/src/git/remote.rs | 294 --------------- crates/auths-verifier/src/commit_kel.rs | 6 +- crates/auths-verifier/src/contract.rs | 111 +++++- crates/auths-verifier/src/credential.rs | 2 +- crates/auths-verifier/src/presentation.rs | 2 +- crates/auths-verifier/src/verify.rs | 2 +- crates/auths-verifier/src/wasm.rs | 24 +- .../auths-verifier/tests/cases/credential.rs | 34 +- .../tests/cases/parity_cases.rs | 35 ++ .../tests/cases/presentation.rs | 52 ++- .../tests/fixtures/credential_revoked.json | 2 +- .../tests/fixtures/credential_valid.json | 2 +- .../tests/fixtures/presentation_valid.json | 2 +- crates/auths-verifier/ts/exhaustiveness.ts | 4 + crates/auths-verifier/ts/verdict.ts | 1 + docs/architecture/kel-distribution.md | 15 +- docs/cli/commands/primary.md | 2 - docs/guides/git/signing-configuration.md | 2 +- docs/guides/git/team-workflows.md | 2 +- docs/guides/git/verifying-commits.md | 6 +- packages/auths-verifier-go/verifier.go | 1 + 29 files changed, 300 insertions(+), 1093 deletions(-) delete mode 100644 crates/auths-infra-http/src/oobi_resolver.rs delete mode 100644 crates/auths-storage/src/git/remote.rs diff --git a/crates/auths-cli/src/commands/unified_verify.rs b/crates/auths-cli/src/commands/unified_verify.rs index 366bad65..1527c1f1 100644 --- a/crates/auths-cli/src/commands/unified_verify.rs +++ b/crates/auths-cli/src/commands/unified_verify.rs @@ -130,19 +130,6 @@ pub struct UnifiedVerifyCommand { #[arg(long, value_name = "PATH")] pub signature: Option, - /// Override the git remote a signer's KEL is fetched from when it is absent - /// locally. Defaults to the repo's own `origin`. The local registry stays the - /// trusted floor — a remote can only advance the key-state, never roll it - /// back, and nothing is fetched when the KEL is already local. - #[arg(long)] - pub remote: Option, - - /// Fetch signer KELs over HTTP from this OOBI base URL (SSRF-hardened: - /// HTTPS-only, no redirects, private/loopback hosts blocked). Takes - /// precedence over `--remote`. - #[arg(long)] - pub oobi: Option, - /// Fail verification when the signer's root KEL has not reached witness /// quorum (fail-closed). Default: warn and continue. #[arg(long = "require-witnesses")] @@ -166,8 +153,6 @@ pub async fn handle_verify_unified( witness_receipts: cmd.witness_receipts, witness_threshold: cmd.witness_threshold, witness_keys: cmd.witness_keys, - remote: cmd.remote, - oobi: cmd.oobi, require_witnesses: cmd.require_witnesses, // Honor --identity-bundle for commit/git-ref targets (previously dropped // here, silently verifying against the wrong/default trust root). diff --git a/crates/auths-cli/src/commands/verify_commit.rs b/crates/auths-cli/src/commands/verify_commit.rs index 57112ef5..c5a329ec 100644 --- a/crates/auths-cli/src/commands/verify_commit.rs +++ b/crates/auths-cli/src/commands/verify_commit.rs @@ -1,6 +1,5 @@ use crate::ux::format::is_json_mode; use anyhow::{Context, Result, anyhow}; -use auths_infra_http::HttpOobiResolver; use auths_keri::Event; use auths_keri::witness::{SignedReceipt, WitnessReceiptLookup}; use auths_sdk::core_config::EnvironmentConfig; @@ -41,21 +40,6 @@ pub struct VerifyCommitCommand { #[arg(long, num_args = 1..)] pub witness_keys: Vec, - /// Override the git remote a signer's KEL is fetched from when it is absent - /// locally. Defaults to the repo's own `origin`, which is where the managed - /// pre-push hook mirrors it. The local registry stays the trusted floor — a - /// remote can only advance the key-state, never roll it back, and nothing is - /// fetched when the KEL is already local. - #[arg(long)] - pub remote: Option, - - /// Fetch signer KELs over HTTP from this OOBI base URL (e.g. - /// `https://registry.example`). SSRF-hardened: HTTPS-only, no redirect - /// following, private/loopback hosts blocked. Takes precedence over - /// `--remote`; the resolved KEL is still prefix-bound + replayed locally. - #[arg(long)] - pub oobi: Option, - /// Fail verification when the signer's root KEL has not reached witness /// quorum (fail-closed). Default: warn and continue (trust-on-first-sight). #[arg(long = "require-witnesses")] @@ -374,20 +358,18 @@ fn extract_oidc_binding_display(attestation: &Attestation) -> Option, did: &str, ) -> Result, String> { @@ -402,33 +384,9 @@ async fn resolve_signer_kel( .map_err(|e| e.to_string())?; return Ok(bundle.events.clone()); } - if let Some(oobi_base) = &cmd.oobi { - let prefix = auths_sdk::keri::parse_did_keri(did).map_err(|e| e.to_string())?; - let resolver = HttpOobiResolver::new(oobi_base.clone()).map_err(|e| e.to_string())?; - let events = resolver - .fetch_kel(&prefix) - .await - .map_err(|e| e.to_string())?; - auths_sdk::keri::verify_prefix_binding(&prefix, &events).map_err(|e| e.to_string())?; - Ok(events) - } else { - // Default the transport to the repo's own `origin`. The managed pre-push - // hook mirrors refs/auths/registry there, but `git clone` fetches only - // refs/heads/* and refs/tags/* — so in a fresh clone the KEL sits on the - // remote and not in the working copy. Resolution stays local-first: the - // chain only reaches for the remote when the KEL is absent locally, so - // this costs nothing in the common case and fires exactly where - // verification would otherwise fail. - let remote = cmd - .remote - .clone() - .or_else(crate::commands::verify_helpers::repo_origin_url); - let chain = match remote { - Some(url) => auths_sdk::keri::KelResolverChain::with_remote(registry, url), - None => auths_sdk::keri::KelResolverChain::local(registry), - }; - chain.resolve_kel(did).map_err(|e| e.to_string()) - } + auths_sdk::keri::KelResolverChain::local(registry) + .resolve_kel(did) + .map_err(|e| e.to_string()) } /// Verify a single commit against the replayed KEL. @@ -479,11 +437,10 @@ async fn verify_one_commit( } }; - // KEL sourcing is an SDK/adapter concern: local-first, with an opt-in git - // remote (`--remote`) or an SSRF-hardened HTTP OOBI host (`--oobi`). The - // prefix-binding guard is applied regardless of transport. The command stays - // presentation-thin. - let device_kel = match resolve_signer_kel(registry, cmd, bundle_kel, &device_did).await { + // 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 { Ok(events) => events, Err(e) => { return VerifyCommitResult::failure( @@ -506,7 +463,7 @@ async fn verify_one_commit( Some(delegator) => format!("did:keri:{delegator}"), None => root_did, }; - let root_kel = match resolve_signer_kel(registry, cmd, bundle_kel, &root_did).await { + let root_kel = match resolve_signer_kel(registry, bundle_kel, &root_did).await { Ok(events) => events, Err(e) => { return VerifyCommitResult::failure( diff --git a/crates/auths-cli/src/commands/verify_helpers.rs b/crates/auths-cli/src/commands/verify_helpers.rs index c0522d6b..1bd43ad3 100644 --- a/crates/auths-cli/src/commands/verify_helpers.rs +++ b/crates/auths-cli/src/commands/verify_helpers.rs @@ -49,33 +49,6 @@ pub fn discover_project_bundle() -> Option { bundle.is_file().then_some(bundle) } -/// The current repository's `origin` URL, when there is one. -/// -/// Used as the default KEL transport: a repo carries its signer's KEL on the same -/// remote it carries its code (the managed `pre-push` hook mirrors -/// `refs/auths/registry` there), but a plain `git clone` fetches only -/// `refs/heads/*` and `refs/tags/*` — so the ref is on the remote and not in the -/// clone. Without this, `auths verify` in a fresh clone fails with "KEL not found" -/// while the KEL sits one fetch away on the remote it was cloned from. -/// -/// Resolution stays local-first: the chain only reaches for the remote when the -/// KEL is absent locally, and the local registry remains the trusted floor. -/// -/// Usage: -/// ```ignore -/// let fallback = repo_origin_url(); -/// ``` -pub fn repo_origin_url() -> Option { - let output = crate::subprocess::git_command(&["remote", "get-url", "origin"]) - .output() - .ok()?; - if !output.status.success() { - return None; - } - let url = String::from_utf8_lossy(&output.stdout).trim().to_string(); - (!url.is_empty()).then_some(url) -} - /// The human-readable label for a surfaced freshness verdict (ADR 009). /// /// Shared by every verify command so an offline verdict renders "freshness unknown" diff --git a/crates/auths-infra-http/src/lib.rs b/crates/auths-infra-http/src/lib.rs index 0fe01bbd..8c6e8a5e 100644 --- a/crates/auths-infra-http/src/lib.rs +++ b/crates/auths-infra-http/src/lib.rs @@ -25,7 +25,6 @@ mod npm_auth; mod oidc_platforms; mod oidc_tsa_client; mod oidc_validator; -mod oobi_resolver; mod pairing_client; mod platform_context; mod registry_client; @@ -45,7 +44,6 @@ pub use oidc_platforms::{ }; pub use oidc_tsa_client::HttpTimestampClient; pub use oidc_validator::{HttpJwksClient, HttpJwtValidator, OidcTokenClaims, PinnedJwksClient}; -pub use oobi_resolver::{HttpOobiError, HttpOobiResolver, MAX_OOBI_BYTES}; pub use pairing_client::HttpPairingRelayClient; pub use platform_context::resolve_verified_platform_context; pub use registry_client::HttpRegistryClient; diff --git a/crates/auths-infra-http/src/oobi_resolver.rs b/crates/auths-infra-http/src/oobi_resolver.rs deleted file mode 100644 index 68a42d9f..00000000 --- a/crates/auths-infra-http/src/oobi_resolver.rs +++ /dev/null @@ -1,329 +0,0 @@ -//! HTTP OOBI client — fetch a `did:keri:` identity's KEL from the static -//! `.well-known/keri/oobi//keri.cesr` layout (see -//! `docs/architecture/kel-distribution.md`). -//! -//! This adapter only *fetches* and parses; it returns raw `Vec`. The -//! prefix-binding guard + monotonicity live one layer up (the SDK chain / the -//! CLI composition root) so they are not reimplemented per transport. Because an -//! OOBI URL is attacker-influenceable, the client is SSRF-hardened: HTTPS-only, -//! redirects disabled, private/loopback hosts blocked, and a response-size cap. - -use std::net::IpAddr; - -use auths_keri::{Event, Prefix}; -use url::Url; - -use crate::default_client_builder; - -/// Maximum OOBI response body size (DoS bound). -pub const MAX_OOBI_BYTES: usize = 4 * 1024 * 1024; - -/// Errors fetching a KEL over HTTP from an OOBI endpoint. -#[derive(Debug, thiserror::Error)] -#[non_exhaustive] -pub enum HttpOobiError { - /// The base URL or constructed OOBI URL is malformed. - #[error("invalid OOBI URL: {0}")] - InvalidUrl(String), - - /// The URL scheme is not `https` (SSRF guard; bypassed only when private - /// hosts are explicitly allowed). - #[error("OOBI URL scheme must be https, got '{0}'")] - InsecureScheme(String), - - /// The target host is loopback / private / link-local (SSRF guard). - #[error("refusing to fetch from a private or loopback host: {0}")] - BlockedHost(String), - - /// The HTTP request itself failed (DNS, connect, TLS, timeout). - #[error("OOBI request failed: {0}")] - Request(String), - - /// 404 — the host serves no KEL for this identifier. - #[error("identity not found at OOBI endpoint (404)")] - NotFound, - - /// 410 — the identity is gone (revoked/abandoned at the host). - #[error("identity gone at OOBI endpoint (410)")] - Gone, - - /// 429 — rate limited. - #[error("OOBI endpoint rate-limited the request (429)")] - RateLimited, - - /// 422 — the server itself reported a prefix mismatch. - #[error("OOBI endpoint reported a prefix mismatch (422)")] - ServerPrefixMismatch, - - /// Any other non-success status. - #[error("unexpected HTTP status {0} from OOBI endpoint")] - UnexpectedStatus(u16), - - /// The response exceeded the size cap. - #[error("OOBI response exceeds the {0}-byte size cap")] - Oversized(usize), - - /// The body was not a parseable KEL. - #[error("malformed KEL body from OOBI endpoint: {0}")] - Malformed(String), -} - -/// An SSRF-hardened HTTP client for the OOBI static KEL layout. -pub struct HttpOobiResolver { - client: reqwest::Client, - base_url: String, - allow_private: bool, -} - -impl HttpOobiResolver { - /// Build a resolver for `base_url`. The client follows **no** redirects - /// (so a 3xx to a private IP cannot exfiltrate) and inherits the hardened - /// timeouts / TLS floor. - /// - /// Args: - /// * `base_url`: The OOBI host base (e.g. `https://registry.example`). - pub fn new(base_url: impl Into) -> Result { - let client = default_client_builder() - .redirect(reqwest::redirect::Policy::none()) - .build() - .map_err(|e| HttpOobiError::Request(e.to_string()))?; - Ok(Self { - client, - base_url: base_url.into(), - allow_private: false, - }) - } - - /// Allow plain `http` and private/loopback hosts — for intranet registries - /// and tests against a local server. Off by default. - pub fn allow_private(mut self, allow: bool) -> Self { - self.allow_private = allow; - self - } - - /// Fetch and parse the KEL for `prefix` from the OOBI layout. - /// - /// Applies the SSRF guard, maps HTTP status to the error taxonomy, enforces - /// the size cap, and parses the `keri.cesr` body (a JSON array of events). - /// Returns raw events — the caller applies the prefix-binding guard. - /// - /// Args: - /// * `prefix`: The `did:keri:` prefix (AID) to resolve. - pub async fn fetch_kel(&self, prefix: &Prefix) -> Result, HttpOobiError> { - let url = self.oobi_url(prefix)?; - self.guard_url(&url)?; - let resp = self - .client - .get(url) - .send() - .await - .map_err(|e| HttpOobiError::Request(e.to_string()))?; - map_status(resp.status())?; - let bytes = resp - .bytes() - .await - .map_err(|e| HttpOobiError::Request(e.to_string()))?; - if bytes.len() > MAX_OOBI_BYTES { - return Err(HttpOobiError::Oversized(MAX_OOBI_BYTES)); - } - serde_json::from_slice::>(&bytes) - .map_err(|e| HttpOobiError::Malformed(e.to_string())) - } - - /// Build the OOBI URL for `prefix` under the base. - fn oobi_url(&self, prefix: &Prefix) -> Result { - let raw = format!( - "{}/.well-known/keri/oobi/{}/keri.cesr", - self.base_url.trim_end_matches('/'), - prefix.as_str() - ); - Url::parse(&raw).map_err(|e| HttpOobiError::InvalidUrl(e.to_string())) - } - - /// Enforce the SSRF policy on a resolved URL (no-op when `allow_private`). - fn guard_url(&self, url: &Url) -> Result<(), HttpOobiError> { - if self.allow_private { - return Ok(()); - } - if url.scheme() != "https" { - return Err(HttpOobiError::InsecureScheme(url.scheme().to_string())); - } - match url.host_str() { - Some(host) if is_blocked_host(host) => { - Err(HttpOobiError::BlockedHost(host.to_string())) - } - Some(_) => Ok(()), - None => Err(HttpOobiError::InvalidUrl("URL has no host".to_string())), - } - } -} - -/// Whether a host should be refused as an SSRF target. IP literals are classified -/// directly; the obvious loopback names are blocked. (DNS names that resolve to -/// private space are mitigated by redirects-disabled + HTTPS-only.) -fn is_blocked_host(host: &str) -> bool { - if let Ok(ip) = host.parse::() { - return is_blocked_ip(ip); - } - matches!(host, "localhost" | "localhost.localdomain") -} - -/// Whether an IP is loopback / private / link-local / unspecified. -fn is_blocked_ip(ip: IpAddr) -> bool { - match ip { - IpAddr::V4(v4) => { - v4.is_loopback() - || v4.is_private() - || v4.is_link_local() - || v4.is_unspecified() - || v4.is_broadcast() - } - IpAddr::V6(v6) => { - let first = v6.segments()[0]; - v6.is_loopback() - || v6.is_unspecified() - || (first & 0xfe00) == 0xfc00 // unique-local fc00::/7 - || (first & 0xffc0) == 0xfe80 // link-local fe80::/10 - } - } -} - -/// Map a non-2xx HTTP status to the OOBI error taxonomy (`Ok` for any 2xx). -fn map_status(status: reqwest::StatusCode) -> Result<(), HttpOobiError> { - use reqwest::StatusCode; - if status.is_success() { - return Ok(()); - } - Err(match status { - StatusCode::NOT_FOUND => HttpOobiError::NotFound, - StatusCode::GONE => HttpOobiError::Gone, - StatusCode::TOO_MANY_REQUESTS => HttpOobiError::RateLimited, - StatusCode::UNPROCESSABLE_ENTITY => HttpOobiError::ServerPrefixMismatch, - other => HttpOobiError::UnexpectedStatus(other.as_u16()), - }) -} - -#[cfg(test)] -#[allow(clippy::unwrap_used, clippy::expect_used)] -mod tests { - use super::*; - use auths_keri::{ - CesrKey, IcpEvent, KeriPublicKey, KeriSequence, Prefix, Said, Threshold, VersionString, - finalize_icp_event, - }; - - fn icp_and_prefix() -> (Event, Prefix) { - let key = KeriPublicKey::ed25519(&[11u8; 32]).unwrap(); - // A valid next-commitment digest (the SAID of any key works structurally). - let next = KeriPublicKey::ed25519(&[12u8; 32]).unwrap(); - let n = auths_core::crypto::said::compute_next_commitment(&next); - let icp = IcpEvent { - v: VersionString::placeholder(), - d: Said::default(), - i: Prefix::default(), - s: KeriSequence::new(0), - kt: Threshold::Simple(1), - k: vec![CesrKey::new_unchecked(key.to_qb64().unwrap())], - nt: Threshold::Simple(1), - n: vec![n], - bt: Threshold::Simple(0), - b: vec![], - c: vec![], - a: vec![], - }; - let finalized = finalize_icp_event(icp).unwrap(); - let prefix = finalized.i.clone(); - (Event::Icp(finalized), prefix) - } - - #[test] - fn blocks_loopback_and_private_and_insecure() { - assert!(is_blocked_host("127.0.0.1")); - assert!(is_blocked_host("10.0.0.5")); - assert!(is_blocked_host("192.168.1.1")); - assert!(is_blocked_host("169.254.1.1")); - assert!(is_blocked_host("localhost")); - assert!(is_blocked_host("::1")); - assert!(!is_blocked_host("93.184.216.34")); // example.com - assert!(!is_blocked_host("registry.example.com")); - - let resolver = HttpOobiResolver::new("https://127.0.0.1").unwrap(); - let (_e, prefix) = icp_and_prefix(); - let err = resolver - .guard_url(&resolver.oobi_url(&prefix).unwrap()) - .unwrap_err(); - assert!(matches!(err, HttpOobiError::BlockedHost(_))); - - let insecure = HttpOobiResolver::new("http://registry.example.com").unwrap(); - let err = insecure - .guard_url(&insecure.oobi_url(&prefix).unwrap()) - .unwrap_err(); - assert!(matches!(err, HttpOobiError::InsecureScheme(_))); - } - - #[test] - fn maps_http_status_to_taxonomy() { - use reqwest::StatusCode; - assert!(map_status(StatusCode::OK).is_ok()); - assert!(matches!( - map_status(StatusCode::NOT_FOUND), - Err(HttpOobiError::NotFound) - )); - assert!(matches!( - map_status(StatusCode::GONE), - Err(HttpOobiError::Gone) - )); - assert!(matches!( - map_status(StatusCode::TOO_MANY_REQUESTS), - Err(HttpOobiError::RateLimited) - )); - assert!(matches!( - map_status(StatusCode::UNPROCESSABLE_ENTITY), - Err(HttpOobiError::ServerPrefixMismatch) - )); - assert!(matches!( - map_status(StatusCode::INTERNAL_SERVER_ERROR), - Err(HttpOobiError::UnexpectedStatus(500)) - )); - } - - #[test] - fn builds_well_known_oobi_url() { - let resolver = HttpOobiResolver::new("https://registry.example/").unwrap(); - let prefix = Prefix::new_unchecked("EabcDEF123".to_string()); - let url = resolver.oobi_url(&prefix).unwrap(); - assert_eq!( - url.as_str(), - "https://registry.example/.well-known/keri/oobi/EabcDEF123/keri.cesr" - ); - } - - #[tokio::test] - async fn fetches_kel_from_static_layout() { - use axum::Router; - use axum::routing::get; - - let (event, prefix) = icp_and_prefix(); - let events = vec![event]; - let body = serde_json::to_vec(&events).unwrap(); - - let app = Router::new().route( - "/.well-known/keri/oobi/{aid}/keri.cesr", - get(move || { - let body = body.clone(); - async move { body } - }), - ); - let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); - let addr = listener.local_addr().unwrap(); - tokio::spawn(async move { - axum::serve(listener, app).await.unwrap(); - }); - - let resolver = HttpOobiResolver::new(format!("http://{addr}")) - .unwrap() - .allow_private(true); - let fetched = resolver.fetch_kel(&prefix).await.unwrap(); - assert_eq!(fetched, events); - } -} diff --git a/crates/auths-sdk/src/keri/resolver.rs b/crates/auths-sdk/src/keri/resolver.rs index 0d13db3a..de3f5864 100644 --- a/crates/auths-sdk/src/keri/resolver.rs +++ b/crates/auths-sdk/src/keri/resolver.rs @@ -1,42 +1,25 @@ //! KEL resolver orchestration — the seam the verifier resolves signer KELs //! through. //! -//! The chain is **local-first**: it always reads the local registry first. The -//! local registry is the trusted floor — a remote can never roll it back. +//! Resolution is **local-only**: the local registry is the trusted store, and a +//! KEL that is not local arrives in-band (a committed/explicit identity bundle, +//! authenticated via `validate_signed_kel` at its ingestion boundary) before it +//! ever reaches this chain. There is no network transport here — the removed +//! `--remote`/`--oobi` stranger feeds carried bare, unsigned events, which is +//! exactly the replay-by-structure hole the verify path bans (RT-002). //! -//! When a remote is configured (`--remote `): -//! - **stranger case** (local has no KEL): the remote result is used, after the -//! prefix-binding guard. This is what lets a verifier check a commit from an -//! identity it has never seen, with no local pre-seeding. -//! - **refresh case** (local has a KEL): the remote may only *advance* the state. -//! A remote tip older than the local tip is a rollback/withholding attempt and -//! is rejected ([`KelResolveError::Rollback`]); a strictly-newer remote is -//! accepted (a legitimate rotation/anchor the local cache hasn't seen); an -//! equal-or-shorter remote leaves local authoritative (prefer-local). -//! -//! Same-sequence forks (the `kt=1` duplicity case) are detected **across -//! sources** here (Epic D): if the local and remote streams present different -//! event SAIDs at the same sequence, resolution refuses with -//! [`KelResolveError::Diverging`] rather than silently picking a side. Within a -//! single stream, the non-fatal "warn, prefer local" duplicity signal is still -//! surfaced downstream by `verify_commit_against_kel`'s `detect_duplicity`. This -//! layer sequences sources, enforces the rollback floor, and refuses cross-source -//! forks; `auths-id` implements each read + the prefix-binding guard. +//! Within a single stream, the non-fatal "warn, prefer local" duplicity signal +//! is surfaced downstream by `verify_commit_against_kel`'s `detect_duplicity`. +//! This layer sequences the read; `auths-id` implements the registry read and +//! the prefix-binding guard. -use auths_id::keri::{ - Event, KelResolveError, KelResolver, LocalKelResolver, Prefix, parse_did_keri, - verify_prefix_binding, -}; +use auths_id::keri::{Event, KelResolveError, KelResolver, LocalKelResolver}; use auths_id::ports::registry::RegistryBackend; -use auths_storage::git::{RemoteKelError, RemoteKelSource}; -use auths_verifier::duplicity::{DuplicityReport, KelEventRef, detect_duplicity}; -/// Resolves a signer's KEL through an ordered set of sources (local, then an -/// optional git remote), enforcing the prefix-binding guard and the rollback -/// floor. +/// Resolves a signer's KEL from the local registry, enforcing the +/// prefix-binding guard. pub struct KelResolverChain<'a> { registry: &'a dyn RegistryBackend, - remote: Option, } impl<'a> KelResolverChain<'a> { @@ -50,77 +33,15 @@ impl<'a> KelResolverChain<'a> { /// let chain = KelResolverChain::local(®istry); /// ``` pub fn local(registry: &'a dyn RegistryBackend) -> Self { - Self { - registry, - remote: None, - } + Self { registry } } - /// A chain that falls back to (and refreshes from) a git remote when the - /// `--remote ` opt-in is given. Local stays the trusted floor. - /// - /// Args: - /// * `registry`: The local backend (trusted floor). - /// * `remote_url`: The git remote to fetch the packed registry from. - /// - /// Usage: - /// ```ignore - /// let chain = KelResolverChain::with_remote(®istry, "https://git.example/registry.git"); - /// let kel = chain.resolve_kel(stranger_did)?; // resolves with no local pre-seeding - /// ``` - pub fn with_remote(registry: &'a dyn RegistryBackend, remote_url: impl Into) -> Self { - Self { - registry, - remote: Some(RemoteKelSource::new(remote_url)), - } - } - - /// Resolve the full KEL for `did`, enforcing the prefix-binding guard and the - /// rollback floor. + /// Resolve the full KEL for `did`, enforcing the prefix-binding guard. /// /// Args: /// * `did`: The `did:keri:` to resolve. pub fn resolve_kel(&self, did: &str) -> Result, KelResolveError> { - let local = LocalKelResolver::new(self.registry).resolve_kel(did); - match &self.remote { - None => local, - Some(remote) => self.reconcile(did, local, remote), - } - } - - /// Reconcile a local result with a remote fetch under the local-first + - /// rollback-floor policy. - fn reconcile( - &self, - did: &str, - local: Result, KelResolveError>, - remote: &RemoteKelSource, - ) -> Result, KelResolveError> { - let prefix = - parse_did_keri(did).map_err(|_| KelResolveError::InvalidDid(did.to_string()))?; - let remote = fetch_remote_guarded(remote, &prefix); - match (local, remote) { - (Ok(local_kel), Ok(remote_kel)) => { - // Cross-source fork: same `(i,s)` with different SAIDs across the - // local and remote streams → refuse, do not silently pick a side. - if let Some((sequence, saids)) = cross_source_fork(&prefix, &local_kel, &remote_kel) - { - return Err(KelResolveError::Diverging { sequence, saids }); - } - choose_newer_no_rollback(local_kel, remote_kel) - } - // Local present, remote failed: a remote hiccup is non-fatal when we - // already hold a locally-trusted KEL. - (Ok(local_kel), Err(_)) => Ok(local_kel), - // Stranger case: nothing local, remote resolved → use the remote KEL. - (Err(KelResolveError::NotFound(_)), Ok(remote_kel)) => Ok(remote_kel), - // Local hard-errored (not a plain miss) → surface that, not the remote. - (Err(local_err), Ok(_)) => Err(local_err), - // Both failed: a bare local miss yields the (more informative) remote - // error; any other local error wins. - (Err(KelResolveError::NotFound(_)), Err(remote_err)) => Err(remote_err), - (Err(local_err), Err(_)) => Err(local_err), - } + LocalKelResolver::new(self.registry).resolve_kel(did) } } @@ -232,102 +153,15 @@ pub fn resolve_current_public_key( Ok((bytes, curve)) } -/// Fetch from a remote and immediately apply the prefix-binding guard, mapping -/// transport errors into the unified [`KelResolveError`] taxonomy. -fn fetch_remote_guarded( - remote: &RemoteKelSource, - prefix: &Prefix, -) -> Result, KelResolveError> { - let events = remote.fetch_kel(prefix).map_err(map_remote_err)?; - verify_prefix_binding(prefix, &events)?; - Ok(events) -} - -/// The rollback floor: a remote KEL must not be older than the locally-trusted -/// state. On a strictly-older remote tip → [`KelResolveError::Rollback`]. -/// Otherwise prefer the strictly-newer chain; on a tie, prefer local. -fn choose_newer_no_rollback( - local_kel: Vec, - remote_kel: Vec, -) -> Result, KelResolveError> { - let local_tip = tip_seq(&local_kel); - let remote_tip = tip_seq(&remote_kel); - if remote_tip < local_tip { - return Err(KelResolveError::Rollback { - local_tip, - remote_tip, - }); - } - Ok(if remote_tip > local_tip { - remote_kel - } else { - local_kel - }) -} - -/// The tip (highest) sequence number across a KEL (0 for a lone inception). -fn tip_seq(events: &[Event]) -> u128 { - events.last().map(|e| e.sequence().value()).unwrap_or(0) -} - -/// Detect a cross-source fork: the same `(i,s)` carrying different event SAIDs -/// across the local and remote streams. Reuses the verifier's `detect_duplicity` -/// over the union of both sources. Returns `(sequence, conflicting_saids)` on a -/// fork, or `None` when the sources agree on every shared sequence. -fn cross_source_fork( - prefix: &Prefix, - local: &[Event], - remote: &[Event], -) -> Option<(u128, Vec)> { - let refs: Vec = local - .iter() - .chain(remote.iter()) - .map(|e| KelEventRef { - prefix: prefix.as_str(), - seq: e.sequence().value() as u64, - said: e.said().as_str(), - }) - .collect(); - match detect_duplicity(&refs) { - DuplicityReport::Clean => None, - DuplicityReport::Diverging { - seq, event_saids, .. - } => Some((seq as u128, event_saids)), - } -} - -/// Map a transport-specific [`RemoteKelError`] into the unified resolver taxonomy. -fn map_remote_err(err: RemoteKelError) -> KelResolveError { - match err { - RemoteKelError::Fetch { url, source } => { - KelResolveError::Network(format!("{url}: {source}")) - } - RemoteKelError::Setup(e) => KelResolveError::Network(e.to_string()), - RemoteKelError::NoRegistry { reference } => { - KelResolveError::NotFound(format!("remote has no {reference}")) - } - RemoteKelError::NotFound(s) => KelResolveError::NotFound(s), - RemoteKelError::Oversized { what } => KelResolveError::Oversized(what), - RemoteKelError::Truncated => KelResolveError::Truncated, - RemoteKelError::Read(e) => KelResolveError::Backend(e.to_string()), - RemoteKelError::Serialize(s) => KelResolveError::Replay(s), - // `RemoteKelError` is #[non_exhaustive]; map any future transport variant - // to a generic backend error via its Display. - other => KelResolveError::Backend(other.to_string()), - } -} - #[cfg(test)] #[allow(clippy::unwrap_used, clippy::expect_used)] mod tests { use super::*; use auths_id::testing::fakes::FakeRegistryBackend; use auths_keri::{ - CesrKey, IcpEvent, IxnEvent, KeriPublicKey, KeriSequence, Said, Seal, Threshold, - VersionString, compute_next_commitment, finalize_icp_event, finalize_ixn_event, + CesrKey, IcpEvent, KeriPublicKey, KeriSequence, Prefix, Said, Threshold, VersionString, + compute_next_commitment, finalize_icp_event, }; - use auths_storage::git::{GitRegistryBackend, RegistryConfig}; - use tempfile::TempDir; fn icp_and_prefix(seed: u8) -> (Event, Prefix) { let key = KeriPublicKey::ed25519(&[seed; 32]).unwrap(); @@ -351,18 +185,6 @@ mod tests { (Event::Icp(finalized), prefix) } - fn ixn_at(prefix: &Prefix, seq: u128, prev: &Said) -> Event { - let ixn = IxnEvent { - v: VersionString::placeholder(), - d: Said::default(), - i: prefix.clone(), - s: KeriSequence::new(seq), - p: prev.clone(), - a: vec![], - }; - Event::Ixn(finalize_ixn_event(ixn).unwrap()) - } - #[test] fn local_only_resolves_from_local() { let (event, prefix) = icp_and_prefix(1); @@ -383,129 +205,4 @@ mod tests { .unwrap_err(); assert!(matches!(err, KelResolveError::NotFound(_))); } - - #[test] - fn remote_fallback_resolves_with_no_local_seeding() { - // Source registry on disk (the "remote"). - let src = TempDir::new().unwrap(); - let source = - GitRegistryBackend::from_config_unchecked(RegistryConfig::single_tenant(src.path())); - source.init_if_needed().unwrap(); - let (event, prefix) = icp_and_prefix(2); - source.append_event(&prefix, &event).unwrap(); - let url = format!("file://{}", src.path().display()); - - // Empty local registry → the chain must fall back to the remote. - let local = FakeRegistryBackend::new(); - let chain = KelResolverChain::with_remote(&local, url); - let did = format!("did:keri:{prefix}"); - assert_eq!(chain.resolve_kel(&did).unwrap(), vec![event]); - } - - #[test] - fn monotonicity_rejects_older_remote() { - let (icp, prefix) = icp_and_prefix(3); - let icp_said = match &icp { - Event::Icp(e) => e.d.clone(), - _ => unreachable!(), - }; - let local_kel = vec![icp.clone(), ixn_at(&prefix, 1, &icp_said)]; // tip 1 - let remote_kel = vec![icp]; // tip 0 — a rollback - - let err = choose_newer_no_rollback(local_kel, remote_kel).unwrap_err(); - assert!(matches!(err, KelResolveError::Rollback { .. })); - } - - #[test] - fn monotonicity_accepts_strictly_newer_remote() { - let (icp, prefix) = icp_and_prefix(4); - let icp_said = match &icp { - Event::Icp(e) => e.d.clone(), - _ => unreachable!(), - }; - let local_kel = vec![icp.clone()]; // tip 0 - let newer = vec![icp, ixn_at(&prefix, 1, &icp_said)]; // tip 1 - - let chosen = choose_newer_no_rollback(local_kel, newer.clone()).unwrap(); - assert_eq!(chosen, newer); - } - - #[test] - fn equal_tip_prefers_local() { - let (icp, _prefix) = icp_and_prefix(5); - let local_kel = vec![icp.clone()]; - let remote_kel = vec![icp]; - // Distinguish by pointer identity is overkill; equal tips → local returned. - let chosen = choose_newer_no_rollback(local_kel.clone(), remote_kel).unwrap(); - assert_eq!(chosen, local_kel); - } - - #[test] - fn remote_error_maps_into_taxonomy() { - assert!(matches!( - map_remote_err(RemoteKelError::Truncated), - KelResolveError::Truncated - )); - assert!(matches!( - map_remote_err(RemoteKelError::NotFound("x".into())), - KelResolveError::NotFound(_) - )); - assert!(matches!( - map_remote_err(RemoteKelError::Oversized { what: "big".into() }), - KelResolveError::Oversized(_) - )); - } - - /// A distinct seq-1 event (different anchors → different SAID) for fork tests. - fn conflicting_ixn_at(prefix: &Prefix, seq: u128, prev: &Said) -> Event { - let ixn = IxnEvent { - v: VersionString::placeholder(), - d: Said::default(), - i: prefix.clone(), - s: KeriSequence::new(seq), - p: prev.clone(), - a: vec![Seal::digest("EConflictingAnchor")], - }; - Event::Ixn(finalize_ixn_event(ixn).unwrap()) - } - - #[test] - fn cross_source_fork_flagged() { - let (icp, prefix) = icp_and_prefix(7); - let icp_said = match &icp { - Event::Icp(e) => e.d.clone(), - _ => unreachable!(), - }; - let local = vec![icp.clone(), ixn_at(&prefix, 1, &icp_said)]; - let remote = vec![icp, conflicting_ixn_at(&prefix, 1, &icp_said)]; - - let fork = cross_source_fork(&prefix, &local, &remote); - assert!(fork.is_some(), "a same-seq SAID mismatch must be flagged"); - let (seq, saids) = fork.unwrap(); - assert_eq!(seq, 1); - assert_eq!(saids.len(), 2); - } - - #[test] - fn clean_multi_source_resolves() { - let (icp, prefix) = icp_and_prefix(8); - let icp_said = match &icp { - Event::Icp(e) => e.d.clone(), - _ => unreachable!(), - }; - let local = vec![icp.clone(), ixn_at(&prefix, 1, &icp_said)]; - let remote = local.clone(); // identical streams — no fork - assert!(cross_source_fork(&prefix, &local, &remote).is_none()); - } - - #[test] - fn first_seen_retained_without_conflict() { - // No fork → first-seen (local) is retained at an equal tip. - let (icp, prefix) = icp_and_prefix(9); - let local = vec![icp.clone()]; - let remote = vec![icp]; - assert!(cross_source_fork(&prefix, &local, &remote).is_none()); - let chosen = choose_newer_no_rollback(local.clone(), remote).unwrap(); - assert_eq!(chosen, local); - } } diff --git a/crates/auths-sdk/src/workflows/commit_trust.rs b/crates/auths-sdk/src/workflows/commit_trust.rs index 53bf3ce4..d6a0de2a 100644 --- a/crates/auths-sdk/src/workflows/commit_trust.rs +++ b/crates/auths-sdk/src/workflows/commit_trust.rs @@ -87,9 +87,8 @@ pub fn local_self_root(ctx: &crate::context::AuthsContext) -> Option { /// `registry`, then replays them to decide whether the commit's signer is a device /// delegated under a root pinned in `pinned_roots`. This is the trust primitive the /// artifact-provenance path uses for the human-signed commit an ephemeral attestation -/// is bound to. Witnessed and transport-aware verification (the `auths verify` -/// command) layers `--remote`/`--oobi` and witness receipts on top of the same -/// `auths_verifier` KEL primitive. +/// is bound to. Witnessed verification (the `auths verify` command) layers +/// witness receipts on top of the same `auths_verifier` KEL primitive. /// /// Args: /// * `registry`: The registry backend holding identity KELs. diff --git a/crates/auths-storage/src/git/mod.rs b/crates/auths-storage/src/git/mod.rs index da5ce929..653b8ecf 100644 --- a/crates/auths-storage/src/git/mod.rs +++ b/crates/auths-storage/src/git/mod.rs @@ -5,7 +5,6 @@ mod config; mod identity_adapter; pub mod oobi; pub mod paths; -pub mod remote; pub mod standalone_attestation; pub mod standalone_export; pub mod standalone_identity; @@ -21,9 +20,6 @@ pub use oobi::{ OOBI_KEL_FILE, OOBI_RECEIPTS_FILE, OobiExportError, export_identity_oobi, export_receipts_oobi, oobi_receipts_relative_path, oobi_relative_path, parse_oobi_kel, parse_oobi_receipts, }; -pub use remote::{ - MAX_KEL_BYTES, MAX_KEL_EVENTS, RegistrySnapshot, RemoteKelError, RemoteKelSource, -}; pub use standalone_attestation::GitAttestationStorage; pub use standalone_export::GitRefSink; pub use standalone_identity::GitIdentityStorage; diff --git a/crates/auths-storage/src/git/remote.rs b/crates/auths-storage/src/git/remote.rs deleted file mode 100644 index 1b119619..00000000 --- a/crates/auths-storage/src/git/remote.rs +++ /dev/null @@ -1,294 +0,0 @@ -//! Native git-remote KEL fetch (greenfield transport). -//! -//! Given a git remote URL and a `did:keri:` prefix, fetch the remote's packed -//! registry ref (`refs/auths/registry`) into a throwaway temp repository, then -//! read the prefix's KEL via the existing [`GitRegistryBackend`] read path. -//! -//! Fetched events are returned **in memory** and are NEVER written back into the -//! caller's persistent registry: `append_event` performs no replay validation, -//! so persisting unvalidated remote events would poison later reads. The caller -//! (the SDK resolver chain) applies the prefix-binding guard and monotonicity -//! before trusting the result. Size caps bound memory against a hostile remote; -//! a truncation guard rejects a KEL whose inception (seq 0) is absent. - -use auths_id::keri::kel_resolver::{KelResolveError, collect_kel_capped}; -use auths_id::ports::registry::RegistryError; -use auths_keri::{Event, Prefix}; -use git2::Repository; -use tempfile::TempDir; - -use super::{GitRegistryBackend, REGISTRY_REF, RegistryConfig}; - -/// Maximum number of events in a single fetched KEL (DoS bound). -pub const MAX_KEL_EVENTS: usize = 10_000; - -/// Maximum total serialized size of a single fetched KEL, in bytes (DoS bound). -pub const MAX_KEL_BYTES: usize = 4 * 1024 * 1024; - -/// Errors fetching a KEL from a git remote. -#[derive(Debug, thiserror::Error)] -#[non_exhaustive] -pub enum RemoteKelError { - /// The git fetch (or temp-repo init) against the remote failed. - #[error("git fetch from '{url}' failed: {source}")] - Fetch { - /// The remote URL. - url: String, - /// The underlying git2 error. - #[source] - source: git2::Error, - }, - - /// The remote did not advertise / serve the registry ref. - #[error("remote has no registry ref ({reference})")] - NoRegistry { - /// The expected ref name. - reference: String, - }, - - /// No KEL is present on the remote for the requested identifier. - #[error("KEL not found on remote for {0}")] - NotFound(String), - - /// The fetched KEL exceeds a configured size bound. - #[error("fetched KEL exceeds size bound ({what})")] - Oversized { - /// Which bound was exceeded. - what: String, - }, - - /// The fetched KEL is missing its inception (seq 0) event. - #[error("fetched KEL is truncated: inception (seq 0) is missing")] - Truncated, - - /// Temp-repository setup failed. - #[error("temp repository setup failed: {0}")] - Setup(#[source] std::io::Error), - - /// Reading the fetched KEL out of the temp registry failed. - #[error("reading fetched KEL failed: {0}")] - Read(#[source] RegistryError), - - /// An event could not be serialized while measuring against the byte cap. - #[error("event serialization failed: {0}")] - Serialize(String), -} - -/// A KEL source backed by a configured git remote. -/// -/// Holds the remote URL (the config surface for C1); the SDK resolver chain -/// threads the `--remote`/configured URL in here and applies the prefix-binding -/// guard to the returned events. -pub struct RemoteKelSource { - remote_url: String, -} - -impl RemoteKelSource { - /// Build a remote source for the given git URL (e.g. `https://…`, `file://…`). - /// - /// Args: - /// * `remote_url`: The git remote to fetch the packed registry from. - /// - /// Usage: - /// ```ignore - /// let kel = RemoteKelSource::new("file:///srv/registry.git").fetch_kel(&prefix)?; - /// ``` - pub fn new(remote_url: impl Into) -> Self { - Self { - remote_url: remote_url.into(), - } - } - - /// The remote URL this source fetches from. - pub fn url(&self) -> &str { - &self.remote_url - } - - /// Fetch the prefix's full KEL from the remote registry. - /// - /// Fetches `refs/auths/registry` into a fresh temp repo, then reads the - /// prefix's events (from sequence 0) via [`GitRegistryBackend`]. Enforces the - /// size caps and the truncation guard. Does NOT write to any persistent - /// store — the temp repo is discarded when this returns. - /// - /// Args: - /// * `prefix`: The identifier prefix to read. - pub fn fetch_kel(&self, prefix: &Prefix) -> Result, RemoteKelError> { - let snapshot = self.fetch_snapshot()?; - read_kel_capped(snapshot.backend(), prefix, MAX_KEL_EVENTS, MAX_KEL_BYTES) - } - - /// Fetch the remote's whole packed registry into a throwaway snapshot. - /// - /// The snapshot is a read-only [`GitRegistryBackend`] over a temp repo — - /// the registry-wide primitive `registry pull` merges from. It is - /// **untrusted**: callers must run the validated merge (or the per-KEL - /// guards) before persisting anything it serves; the temp repo is deleted - /// when the snapshot drops. - pub fn fetch_snapshot(&self) -> Result { - let tmp = TempDir::new().map_err(RemoteKelError::Setup)?; - let repo = Repository::init(tmp.path()).map_err(|e| RemoteKelError::Fetch { - url: self.remote_url.clone(), - source: e, - })?; - fetch_registry_ref(&repo, &self.remote_url)?; - - let backend = - GitRegistryBackend::from_config_unchecked(RegistryConfig::single_tenant(tmp.path())); - Ok(RegistrySnapshot { _tmp: tmp, backend }) - } -} - -/// A fetched remote registry held in a temp repo — read-only, untrusted, -/// deleted on drop. -pub struct RegistrySnapshot { - /// Owns the temp repo for the snapshot's lifetime. - _tmp: TempDir, - backend: GitRegistryBackend, -} - -impl RegistrySnapshot { - /// The backend reading the fetched registry. - pub fn backend(&self) -> &GitRegistryBackend { - &self.backend - } -} - -/// Fetch only the packed registry ref from `url` into `repo`. -fn fetch_registry_ref(repo: &Repository, url: &str) -> Result<(), RemoteKelError> { - let mut remote = repo - .remote_anonymous(url) - .map_err(|e| RemoteKelError::Fetch { - url: url.to_string(), - source: e, - })?; - let refspec = format!("+{REGISTRY_REF}:{REGISTRY_REF}"); - remote - .fetch(&[refspec.as_str()], None, None) - .map_err(|e| RemoteKelError::Fetch { - url: url.to_string(), - source: e, - })?; - if repo.find_reference(REGISTRY_REF).is_err() { - return Err(RemoteKelError::NoRegistry { - reference: REGISTRY_REF.to_string(), - }); - } - Ok(()) -} - -/// Read a prefix's KEL out of a fetched snapshot under the shared untrusted-read -/// guards ([`collect_kel_capped`]: caps + truncation), mapped into this -/// adapter's error vocabulary. -/// -/// Args: -/// * `backend`: The (just-fetched) registry to read from. -/// * `prefix`: The identifier prefix. -/// * `max_events`: Hard cap on event count. -/// * `max_bytes`: Hard cap on total serialized KEL size. -fn read_kel_capped( - backend: &GitRegistryBackend, - prefix: &Prefix, - max_events: usize, - max_bytes: usize, -) -> Result, RemoteKelError> { - collect_kel_capped(backend, prefix, max_events, max_bytes).map_err(|e| match e { - KelResolveError::NotFound(id) => RemoteKelError::NotFound(id), - KelResolveError::Oversized(what) => RemoteKelError::Oversized { what }, - KelResolveError::Truncated => RemoteKelError::Truncated, - KelResolveError::Backend(reason) => RemoteKelError::Read(RegistryError::Internal(reason)), - other => RemoteKelError::Serialize(other.to_string()), - }) -} - -#[cfg(test)] -#[allow(clippy::unwrap_used, clippy::expect_used)] -mod tests { - use super::*; - use auths_id::ports::registry::RegistryBackend; - use auths_keri::{ - CesrKey, IcpEvent, KeriPublicKey, KeriSequence, Said, Threshold, VersionString, - compute_next_commitment, finalize_icp_event, - }; - - /// A finalized inception event + its self-addressing prefix. - fn icp_event() -> (Event, Prefix) { - let key = KeriPublicKey::ed25519(&[7u8; 32]).unwrap(); - let next = KeriPublicKey::ed25519(&[8u8; 32]).unwrap(); - let icp = IcpEvent { - v: VersionString::placeholder(), - d: Said::default(), - i: Prefix::default(), - s: KeriSequence::new(0), - kt: Threshold::Simple(1), - k: vec![CesrKey::new_unchecked(key.to_qb64().unwrap())], - nt: Threshold::Simple(1), - n: vec![compute_next_commitment(&next)], - bt: Threshold::Simple(0), - b: vec![], - c: vec![], - a: vec![], - }; - let finalized = finalize_icp_event(icp).unwrap(); - let prefix = finalized.i.clone(); - (Event::Icp(finalized), prefix) - } - - /// Build a source registry on disk holding one identity's inception. - fn source_registry() -> (TempDir, Event, Prefix) { - let dir = TempDir::new().unwrap(); - let backend = - GitRegistryBackend::from_config_unchecked(RegistryConfig::single_tenant(dir.path())); - backend.init_if_needed().unwrap(); - let (event, prefix) = icp_event(); - backend.append_event(&prefix, &event).unwrap(); - (dir, event, prefix) - } - - #[test] - fn fetches_kel_from_file_remote_with_no_local_seeding() { - let (src, event, prefix) = source_registry(); - let url = format!("file://{}", src.path().display()); - - let fetched = RemoteKelSource::new(url).fetch_kel(&prefix).unwrap(); - assert_eq!(fetched, vec![event]); - } - - #[test] - fn unknown_identity_on_remote_is_not_found() { - let (src, _event, _prefix) = source_registry(); - let url = format!("file://{}", src.path().display()); - let missing = - Prefix::new_unchecked("ENeverProvisionedHere000000000000000000000000".to_string()); - - let err = RemoteKelSource::new(url).fetch_kel(&missing).unwrap_err(); - assert!(matches!(err, RemoteKelError::NotFound(_))); - } - - #[test] - fn event_cap_rejects_oversized_kel() { - // A real backend with one event, read under a zero-event cap → Oversized. - let dir = TempDir::new().unwrap(); - let backend = - GitRegistryBackend::from_config_unchecked(RegistryConfig::single_tenant(dir.path())); - backend.init_if_needed().unwrap(); - let (event, prefix) = icp_event(); - backend.append_event(&prefix, &event).unwrap(); - - let err = read_kel_capped(&backend, &prefix, 0, MAX_KEL_BYTES).unwrap_err(); - assert!(matches!(err, RemoteKelError::Oversized { .. })); - } - - #[test] - fn byte_cap_rejects_oversized_kel() { - let dir = TempDir::new().unwrap(); - let backend = - GitRegistryBackend::from_config_unchecked(RegistryConfig::single_tenant(dir.path())); - backend.init_if_needed().unwrap(); - let (event, prefix) = icp_event(); - backend.append_event(&prefix, &event).unwrap(); - - let err = read_kel_capped(&backend, &prefix, MAX_KEL_EVENTS, 1).unwrap_err(); - assert!(matches!(err, RemoteKelError::Oversized { .. })); - } -} diff --git a/crates/auths-verifier/src/commit_kel.rs b/crates/auths-verifier/src/commit_kel.rs index 3e642871..15820b17 100644 --- a/crates/auths-verifier/src/commit_kel.rs +++ b/crates/auths-verifier/src/commit_kel.rs @@ -619,7 +619,7 @@ async fn verify_commit_against_kel_witnessed_at( ) -> WitnessedVerdict { // 1. Replay + witness-gate the root KEL (validates SAIDs incl. the // self-addressing icp prefix, then checks M-of-N witness agreement). - // rt-002-allow: root_kel is authenticated at the ingestion boundary before it reaches here (CI --identity-bundle → validate_signed_kel in load_bundle_trust; local registry = trusted self-owned store), and this replay additionally enforces the M-of-N witness gate. Residual: the opt-in --remote/--oobi stranger feed, whose signature-carrying transport is tracked (RT-002 follow-up). + // rt-002-allow: root_kel is authenticated at the ingestion boundary before it reaches here (CI --identity-bundle → validate_signed_kel in load_bundle_trust; local registry = trusted self-owned store), and this replay additionally enforces the M-of-N witness gate. let replay = match TrustedKel::from_trusted_source(root_kel) .replay_with_receipts(None, receipt_lookup) { @@ -713,7 +713,7 @@ pub async fn verify_commit_against_kel_scoped( provider: &dyn CryptoProvider, now: i64, ) -> CommitVerdict { - // rt-002-allow: root_kel is authenticated at the ingestion boundary (CI --identity-bundle → validate_signed_kel in load_bundle_trust; local registry = trusted self-owned store). Residual: opt-in --remote/--oobi stranger feed — signature-carrying transport tracked (RT-002 follow-up). + // rt-002-allow: root_kel is authenticated at the ingestion boundary (CI --identity-bundle → validate_signed_kel in load_bundle_trust; local registry = trusted self-owned store). let root_state = match TrustedKel::from_trusted_source(root_kel).replay() { Ok(state) => state, Err(e) => return CommitVerdict::RootKelInvalid(e.to_string()), @@ -756,7 +756,7 @@ async fn authorize_commit( // 3. Replay the device KEL (a dip needs the delegator lookup against the root). let lookup = KelSealIndex::from_events(root_kel); - // rt-002-allow: device_kel is authenticated at the ingestion boundary (CI --identity-bundle → validate_signed_kel in load_bundle_trust; local registry = trusted self-owned store); the dip's delegation is additionally bound to the already-replayed root via the root KelSealIndex. Residual: opt-in --remote/--oobi stranger feed — tracked (RT-002 follow-up). + // rt-002-allow: device_kel is authenticated at the ingestion boundary (CI --identity-bundle → validate_signed_kel in load_bundle_trust; local registry = trusted self-owned store); the dip's delegation is additionally bound to the already-replayed root via the root KelSealIndex. let device_state = match TrustedKel::from_trusted_source(device_kel).replay_with_lookup(Some(&lookup)) { Ok(s) => s, diff --git a/crates/auths-verifier/src/contract.rs b/crates/auths-verifier/src/contract.rs index 95c1ad7b..8233d588 100644 --- a/crates/auths-verifier/src/contract.rs +++ b/crates/auths-verifier/src/contract.rs @@ -30,8 +30,13 @@ //! ``` //! Other `kind`s: `holderNotCurrentKey`, `wrongAudience`, `nonceMismatchOrConsumed`, //! `expired`, `subjectKelInvalid`, `credentialNotValid` (nests a credential verdict), -//! plus the request-layer errors `malformedRequest`, `inputTooLarge`, -//! `unsupportedSchemaVersion`. Credential verdict `kind`s: `valid`, `saidMismatch`, +//! plus the request-layer errors `malformedRequest`, `kelUnauthenticated`, +//! `inputTooLarge`, `unsupportedSchemaVersion`. +//! +//! Every KEL slice travels with a parallel `…KelAttachmentsB64` array — one base64 +//! CESR signature attachment per event. The contract authenticates each KEL via +//! `validate_signed_kel` before any downstream replay (RT-002): unsigned or forged +//! events refuse the request with `kelUnauthenticated` instead of binding a key. Credential verdict `kind`s: `valid`, `saidMismatch`, //! `schemaInvalid`, `issuerSignatureInvalid`, `registryNotEstablished`, //! `credentialRevoked`, `expired`, `witnessQuorumNotMet`, `issuerKelDuplicitous`. @@ -41,7 +46,10 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use auths_keri::witness::StoredReceipt; -use auths_keri::{Acdc, Event, TelEvent}; +use auths_keri::{ + Acdc, DelegatorKelLookup, Event, KelSealIndex, TelEvent, pair_kel_attachments, + validate_signed_kel, +}; use crate::commit_kel::VerifierWitnessPolicy; use crate::credential::{CredentialVerdict, LifecycleEvent, SignedAcdc, verify_credential_sync}; @@ -113,6 +121,10 @@ enum WirePresentationVerdict { MalformedRequest { message: String, }, + KelUnauthenticated { + field: String, + detail: String, + }, InputTooLarge { field: String, count: usize, @@ -162,6 +174,10 @@ enum WireCredentialVerdict { MalformedRequest { message: String, }, + KelUnauthenticated { + field: String, + detail: String, + }, InputTooLarge { field: String, count: usize, @@ -277,9 +293,13 @@ struct VerifyPresentationRequest { envelope: WireEnvelopeIn, credential: WireSignedAcdc, issuer_kel: Vec, + issuer_kel_attachments_b64: Vec, subject_kel: Vec, + subject_kel_attachments_b64: Vec, #[serde(default)] delegator_kel: Vec, + #[serde(default)] + delegator_kel_attachments_b64: Vec, tel: Vec, #[serde(default)] receipts: Vec, @@ -299,6 +319,7 @@ struct VerifyCredentialRequest { _schema_version: u32, credential: WireSignedAcdc, issuer_kel: Vec, + issuer_kel_attachments_b64: Vec, tel: Vec, #[serde(default)] receipts: Vec, @@ -368,6 +389,10 @@ impl From for VerifierWitnessPolicy { /// A request that failed before the verify core could run — mapped to a tagged error verdict. enum RequestError { Malformed(String), + KelUnauthenticated { + field: &'static str, + detail: String, + }, TooLarge { field: &'static str, count: usize, @@ -382,6 +407,12 @@ impl From for WirePresentationVerdict { RequestError::Malformed(message) => { WirePresentationVerdict::MalformedRequest { message } } + RequestError::KelUnauthenticated { field, detail } => { + WirePresentationVerdict::KelUnauthenticated { + field: field.to_string(), + detail, + } + } RequestError::TooLarge { field, count, @@ -405,6 +436,12 @@ impl From for WireCredentialVerdict { fn from(error: RequestError) -> Self { match error { RequestError::Malformed(message) => WireCredentialVerdict::MalformedRequest { message }, + RequestError::KelUnauthenticated { field, detail } => { + WireCredentialVerdict::KelUnauthenticated { + field: field.to_string(), + detail, + } + } RequestError::TooLarge { field, count, @@ -469,6 +506,42 @@ fn serialize_verdict(verdict: V) -> String { .unwrap_or_else(|_| SERIALIZE_FALLBACK.to_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. +/// +/// An empty KEL with no attachments passes through (the verify cores report the +/// precise domain verdict for a missing KEL); any count mismatch, undecodable +/// attachment, or signature failure refuses the request as `kelUnauthenticated`. +fn authenticate_kel( + field: &'static str, + events: &[Event], + attachments_b64: &[String], + lookup: Option<&dyn DelegatorKelLookup>, +) -> Result<(), RequestError> { + if events.is_empty() && attachments_b64.is_empty() { + return Ok(()); + } + let mut attachment_bytes = Vec::with_capacity(attachments_b64.len()); + for (i, attachment) in attachments_b64.iter().enumerate() { + attachment_bytes.push(BASE64.decode(attachment).map_err(|e| { + RequestError::Malformed(format!("{field}Attachments[{i}] is not valid base64: {e}")) + })?); + } + let signed = pair_kel_attachments(events.to_vec(), &attachment_bytes).map_err(|e| { + RequestError::KelUnauthenticated { + field, + detail: e.to_string(), + } + })?; + validate_signed_kel(&signed, lookup).map_err(|e| RequestError::KelUnauthenticated { + field, + detail: e.to_string(), + })?; + Ok(()) +} + /// Parse, bound-check, and run the presentation verify; `Err` carries a request-layer error. fn run_presentation(request_json: &str) -> Result { check_request_size(request_json)?; @@ -483,6 +556,30 @@ fn run_presentation(request_json: &str) -> Result Result state, Err(_) => return CredentialVerdict::RegistryNotEstablished, diff --git a/crates/auths-verifier/src/presentation.rs b/crates/auths-verifier/src/presentation.rs index 6ac4ebbb..5ce463c9 100644 --- a/crates/auths-verifier/src/presentation.rs +++ b/crates/auths-verifier/src/presentation.rs @@ -59,7 +59,7 @@ const EXPIRY_FIELD: &str = "expiry"; /// replay error (`SubjectKelInvalid`), which is the correct fail-closed outcome. fn replay_subject(subject_kel: &[Event], subject_delegator_kel: &[Event]) -> Option { let lookup = KelSealIndex::from_events(subject_delegator_kel); - // rt-002-allow: subject_kel/delegator_kel are supplied by the caller from the local trusted registry / an authenticated bundle, and the presentation signature is bound to the resulting subject key-state. Residual: untrusted WASM presentation input — signature-carrying transport is the tracked RT-002 follow-up. + // rt-002-allow: subject_kel/delegator_kel are authenticated before they reach here — local trusted registry, authenticated bundle, or the JSON contract's authenticate_kel boundary (validate_signed_kel over the wire attachments). The presentation signature is additionally bound to the resulting subject key-state. TrustedKel::from_trusted_source(subject_kel) .replay_with_lookup(Some(&lookup)) .ok() diff --git a/crates/auths-verifier/src/verify.rs b/crates/auths-verifier/src/verify.rs index 1487af87..3908e56e 100644 --- a/crates/auths-verifier/src/verify.rs +++ b/crates/auths-verifier/src/verify.rs @@ -212,7 +212,7 @@ pub async fn verify_device_link( now: DateTime, provider: &dyn CryptoProvider, ) -> DeviceLinkVerification { - // rt-002-allow: `events` is the device-link KEL supplied by the caller from the local trusted registry / an authenticated bundle, and the attestation signature is bound to the resulting key-state. Residual: the untrusted WASM verifyDeviceLink boundary — its authenticated counterpart is validateKelJson (which now routes through validate_signed_kel); a signature-carrying verifyDeviceLink input is the tracked RT-002 follow-up. + // rt-002-allow: `events` is the device-link KEL authenticated before it reaches here — local trusted registry, authenticated bundle, or the WASM verifyDeviceLink boundary (which pairs wire attachments and runs validate_signed_kel first). The attestation signature is additionally bound to the resulting key-state. let key_state = match auths_keri::TrustedKel::from_trusted_source(events).replay() { Ok(ks) => ks, Err(e) => return DeviceLinkVerification::failure(format!("KEL verification failed: {e}")), diff --git a/crates/auths-verifier/src/wasm.rs b/crates/auths-verifier/src/wasm.rs index 537c56a2..246781bc 100644 --- a/crates/auths-verifier/src/wasm.rs +++ b/crates/auths-verifier/src/wasm.rs @@ -435,24 +435,27 @@ pub async fn wasm_validate_kel_json( /// /// Args: /// * `kel_json`: JSON array of KEL events. +/// * `attachments_json`: JSON array of hex CESR signature attachments, one per event — +/// the KEL authenticates via `validate_signed_kel` before the link check (RT-002). /// * `attestation_json`: JSON attestation linking identity to device. /// * `device_did`: Expected device DID string (e.g. `"did:key:z6Mk..."`). /// /// Usage: /// ```ignore -/// let result = verifyDeviceLink(kelJson, attestationJson, "did:key:z6Mk...").await; +/// let result = verifyDeviceLink(kelJson, attachmentsJson, attestationJson, "did:key:z6Mk...").await; /// // result: {"valid": true, "key_state": {...}, "seal_sequence": 2} /// // or: {"valid": false, "error": "..."} /// ``` #[wasm_bindgen(js_name = verifyDeviceLink)] pub async fn wasm_verify_device_link( kel_json: &str, + attachments_json: &str, attestation_json: &str, device_did: &str, ) -> Result { console_log!("WASM: Verifying device link for {}", device_did); - if kel_json.len() > MAX_JSON_BATCH_SIZE { + if kel_json.len() > MAX_JSON_BATCH_SIZE || attachments_json.len() > MAX_JSON_BATCH_SIZE { return Err(JsValue::from_str(&format!( "KEL JSON too large: {} bytes, max {}", kel_json.len(), @@ -469,6 +472,23 @@ pub async fn wasm_verify_device_link( let events = auths_keri::parse_kel_json(kel_json) .map_err(|e| JsValue::from_str(&format!("Failed to parse KEL JSON: {}", e)))?; + let attachments: Vec = serde_json::from_str(attachments_json) + .map_err(|e| JsValue::from_str(&format!("Failed to parse attachments JSON: {}", e)))?; + let attachment_bytes: Vec> = attachments + .iter() + .map(|att_hex| { + hex::decode(att_hex) + .map_err(|e| JsValue::from_str(&format!("Invalid attachment hex: {}", e))) + }) + .collect::>()?; + + // RT-002 ingestion boundary: the untrusted KEL authenticates (per-event + // signatures folded into the replay) before the structural link check runs. + let signed = auths_keri::pair_kel_attachments(events, &attachment_bytes) + .map_err(|e| JsValue::from_str(&format!("Invalid CESR attachment: {}", e)))?; + auths_keri::validate_signed_kel(&signed, None) + .map_err(|e| JsValue::from_str(&format!("KEL authentication failed: {}", e)))?; + let events: Vec = signed.into_iter().map(|se| se.event).collect(); let attestation: Attestation = serde_json::from_str(attestation_json) .map_err(|e| JsValue::from_str(&format!("Failed to parse attestation JSON: {}", e)))?; diff --git a/crates/auths-verifier/tests/cases/credential.rs b/crates/auths-verifier/tests/cases/credential.rs index 1779db61..db1c3b8d 100644 --- a/crates/auths-verifier/tests/cases/credential.rs +++ b/crates/auths-verifier/tests/cases/credential.rs @@ -8,10 +8,10 @@ use auths_crypto::{CurveType, RingCryptoProvider}; use auths_keri::witness::{Receipt, ReceiptTag, SignedReceipt, StoredReceipt}; use auths_keri::{ - Acdc, CesrKey, Event, IcpEvent, IcpEventInit, Iss, IxnEvent, KeriPublicKey, KeriSequence, - Prefix, Rev, Said, Seal, TelAnchorSeal, TelEvent, Threshold, Vcp, VersionString, + Acdc, CesrKey, Event, IcpEvent, IcpEventInit, IndexedSignature, Iss, IxnEvent, KeriPublicKey, + KeriSequence, Prefix, Rev, Said, Seal, TelAnchorSeal, TelEvent, Threshold, Vcp, VersionString, compute_capability_schema_said, compute_next_commitment, encode_tel_nonce, finalize_icp_event, - finalize_ixn_event, + finalize_ixn_event, serialize_attachment, serialize_for_signing, }; use auths_verifier::freshness::{Freshness, FreshnessEvidence, FreshnessPolicy}; use auths_verifier::{CredentialVerdict, LifecycleEvent, SignedAcdc, VerifierWitnessPolicy}; @@ -170,11 +170,34 @@ fn build_credential( (acdc.clone(), SignedAcdc { acdc, signature }) } +/// One CESR signature attachment per event, each signed by `signer` (single-signer KELs). +fn kel_attachments(signer: &Signer, kel: &[Event]) -> Vec> { + kel.iter() + .map(|event| { + let canonical = serialize_for_signing(event).expect("canonical event bytes"); + serialize_attachment(&[IndexedSignature { + index: 0, + prior_index: None, + sig: signer.sign(&canonical), + }]) + .expect("attachment") + }) + .collect() +} + +/// Base64 each raw attachment for the JSON contract's `…KelAttachmentsB64` arrays. +fn attachments_b64(attachments: &[Vec]) -> Vec { + use base64::Engine as _; + let b64 = base64::engine::general_purpose::STANDARD; + attachments.iter().map(|a| b64.encode(a)).collect() +} + /// A complete issuer KEL + TEL fixture for one credential lifecycle. struct Fixture { issuer: Signer, issuer_aid: Prefix, issuer_kel: Vec, + issuer_kel_attachments: Vec>, tel: Vec, registry: Said, iss_said: Said, @@ -287,10 +310,12 @@ fn build_fixture(spec: &FixtureSpec) -> Fixture { tel.push(TelEvent::Rev(rev)); } + let issuer_kel_attachments = kel_attachments(&issuer, &kel); Fixture { issuer, issuer_aid, issuer_kel: kel, + issuer_kel_attachments, tel, registry, iss_said: iss.d.clone(), @@ -891,6 +916,7 @@ async fn credential_json_contract_matches_sync() { "signatureB64": b64.encode(&f.signed.signature), }, "issuerKel": f.issuer_kel, + "issuerKelAttachmentsB64": attachments_b64(&f.issuer_kel_attachments), "tel": f.tel, "receipts": [], "witnessPolicy": "warn", @@ -939,6 +965,7 @@ async fn emit_credential_fixtures() { "schemaVersion": 1, "credential": { "acdc": f.signed.acdc, "signatureB64": b64.encode(&f.signed.signature) }, "issuerKel": f.issuer_kel, + "issuerKelAttachmentsB64": attachments_b64(&f.issuer_kel_attachments), "tel": f.tel, "receipts": [], "witnessPolicy": "warn", @@ -968,6 +995,7 @@ fn credential_ffi_c_abi_end_to_end() { "schemaVersion": 1, "credential": { "acdc": f.signed.acdc, "signatureB64": b64.encode(&f.signed.signature) }, "issuerKel": f.issuer_kel, + "issuerKelAttachmentsB64": attachments_b64(&f.issuer_kel_attachments), "tel": f.tel, "receipts": [], "witnessPolicy": "warn", diff --git a/crates/auths-verifier/tests/cases/parity_cases.rs b/crates/auths-verifier/tests/cases/parity_cases.rs index 2c2faeb7..8a3b4823 100644 --- a/crates/auths-verifier/tests/cases/parity_cases.rs +++ b/crates/auths-verifier/tests/cases/parity_cases.rs @@ -88,6 +88,27 @@ pub fn presentation_cases() -> Vec { }), "credentialNotValid", ), + c( + "forged-subject-kel-attachment", + tampered(PRESENTATION_VALID, |v| { + v["subjectKelAttachmentsB64"][0] = zero_sig_b64(); + }), + "kelUnauthenticated", + ), + c( + "forged-issuer-kel-attachment", + tampered(PRESENTATION_VALID, |v| { + v["issuerKelAttachmentsB64"][0] = zero_sig_b64(); + }), + "kelUnauthenticated", + ), + c( + "stripped-subject-kel-attachments", + tampered(PRESENTATION_VALID, |v| { + v["subjectKelAttachmentsB64"] = serde_json::json!([]); + }), + "kelUnauthenticated", + ), c( "truncated", PRESENTATION_VALID[..PRESENTATION_VALID.len() / 2].to_string(), @@ -134,6 +155,20 @@ pub fn credential_cases() -> Vec { }), "saidMismatch", ), + c( + "forged-issuer-kel-attachment", + tampered(CREDENTIAL_VALID, |v| { + v["issuerKelAttachmentsB64"][0] = zero_sig_b64(); + }), + "kelUnauthenticated", + ), + c( + "stripped-issuer-kel-attachments", + tampered(CREDENTIAL_VALID, |v| { + v["issuerKelAttachmentsB64"] = serde_json::json!([]); + }), + "kelUnauthenticated", + ), c( "truncated", CREDENTIAL_VALID[..CREDENTIAL_VALID.len() / 2].to_string(), diff --git a/crates/auths-verifier/tests/cases/presentation.rs b/crates/auths-verifier/tests/cases/presentation.rs index 881b0243..b4362029 100644 --- a/crates/auths-verifier/tests/cases/presentation.rs +++ b/crates/auths-verifier/tests/cases/presentation.rs @@ -9,10 +9,11 @@ use auths_crypto::{CurveType, RingCryptoProvider}; use auths_keri::{ - Acdc, CesrKey, Event, IcpEvent, IcpEventInit, Iss, IxnEvent, KeriPublicKey, KeriSequence, - Prefix, Rev, RotEvent, RotEventInit, Said, Seal, TelAnchorSeal, TelEvent, Threshold, Vcp, - VersionString, compute_capability_schema_said, compute_next_commitment, encode_tel_nonce, - finalize_icp_event, finalize_ixn_event, finalize_rot_event, + Acdc, CesrKey, Event, IcpEvent, IcpEventInit, IndexedSignature, Iss, IxnEvent, KeriPublicKey, + KeriSequence, Prefix, Rev, RotEvent, RotEventInit, Said, Seal, TelAnchorSeal, TelEvent, + Threshold, Vcp, VersionString, compute_capability_schema_said, compute_next_commitment, + encode_tel_nonce, finalize_icp_event, finalize_ixn_event, finalize_rot_event, + serialize_attachment, serialize_for_signing, }; use auths_verifier::freshness::FreshnessPolicy; use auths_verifier::{ @@ -114,6 +115,23 @@ fn presentation_message(credential_said: &str, audience: &str, nonce: &[u8]) -> message } +/// One CESR signature attachment over `event`, signed by `signer`. +fn attachment_for(signer: &Signer, event: &Event) -> Vec { + let canonical = serialize_for_signing(event).expect("canonical event bytes"); + serialize_attachment(&[IndexedSignature { + index: 0, + prior_index: None, + sig: signer.sign(&canonical), + }]) + .expect("attachment") +} + +/// Base64 each raw attachment for the JSON contract's `…KelAttachmentsB64` arrays. +fn attachments_b64(attachments: &[Vec]) -> Vec { + let b64 = base64::engine::general_purpose::STANDARD; + attachments.iter().map(|a| b64.encode(a)).collect() +} + // ── subject KEL (the holder identity that must prove current control) ──────────── /// A subject (holder) identity with its own single-key KEL. @@ -123,6 +141,8 @@ struct Subject { /// The pre-committed next signer (revealed by [`Subject::rotate`]). next_signer: Signer, kel: Vec, + /// One CESR signature attachment per KEL event (each signed by its controlling key). + kel_attachments: Vec>, } impl Subject { @@ -146,11 +166,14 @@ impl Subject { })) .expect("subject icp"); let aid = icp.i.clone(); + let event = Event::Icp(icp); + let attachment = attachment_for(&signer, &event); Self { aid, signer, next_signer, - kel: vec![Event::Icp(icp)], + kel: vec![event], + kel_attachments: vec![attachment], } } @@ -176,7 +199,10 @@ impl Subject { a: vec![], })) .expect("subject rot"); - self.kel.push(Event::Rot(rot)); + let event = Event::Rot(rot); + let attachment = attachment_for(&self.next_signer, &event); + self.kel.push(event); + self.kel_attachments.push(attachment); } /// Sign a presentation envelope for `credential_said`/`audience`/`binding`. @@ -206,6 +232,7 @@ impl Subject { /// The issuer KEL/TEL fixture credentialing a given subject AID. struct Credentialed { issuer_kel: Vec, + issuer_kel_attachments: Vec>, tel: Vec, signed: SignedAcdc, credential_said: String, @@ -288,8 +315,11 @@ fn credential_for(subject_aid: &Prefix, revoke: bool) -> Credentialed { tel.push(TelEvent::Rev(rev)); } + let issuer_kel_attachments: Vec> = + kel.iter().map(|e| attachment_for(&issuer, e)).collect(); Credentialed { issuer_kel: kel, + issuer_kel_attachments, tel, signed, credential_said, @@ -845,7 +875,7 @@ async fn sync_and_async_presentation_verdicts_match() { fn presentation_request_json( envelope: &PresentationEnvelope, cred: &Credentialed, - subject_kel: &[Event], + subject: &Subject, audience: &str, expected_challenge: Option<&[u8]>, ) -> String { @@ -874,7 +904,9 @@ fn presentation_request_json( "signatureB64": b64.encode(&cred.signed.signature), }, "issuerKel": cred.issuer_kel, - "subjectKel": subject_kel, + "issuerKelAttachmentsB64": attachments_b64(&cred.issuer_kel_attachments), + "subjectKel": subject.kel, + "subjectKelAttachmentsB64": attachments_b64(&subject.kel_attachments), "tel": cred.tel, "receipts": [], "witnessPolicy": "warn", @@ -924,7 +956,7 @@ fn presentation_json_contract_matches_sync() { (&bearer, "holderNotCurrentKey"), ] { let request = - presentation_request_json(envelope, &cred, &subject.kel, AUDIENCE, Some(&nonce)); + presentation_request_json(envelope, &cred, &subject, AUDIENCE, Some(&nonce)); let verdict: serde_json::Value = serde_json::from_str(&verify_presentation_json(&request)).expect("verdict json"); @@ -966,7 +998,7 @@ fn emit_presentation_fixture() { nonce: nonce.clone(), }, ); - let request = presentation_request_json(&envelope, &cred, &subject.kel, AUDIENCE, Some(&nonce)); + let request = presentation_request_json(&envelope, &cred, &subject, AUDIENCE, Some(&nonce)); let path = concat!( env!("CARGO_MANIFEST_DIR"), "/tests/fixtures/presentation_valid.json" diff --git a/crates/auths-verifier/tests/fixtures/credential_revoked.json b/crates/auths-verifier/tests/fixtures/credential_revoked.json index 0b42e654..46c4af33 100644 --- a/crates/auths-verifier/tests/fixtures/credential_revoked.json +++ b/crates/auths-verifier/tests/fixtures/credential_revoked.json @@ -1 +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"}]}],"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"} +{"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"} \ No newline at end of file diff --git a/crates/auths-verifier/tests/fixtures/credential_valid.json b/crates/auths-verifier/tests/fixtures/credential_valid.json index 5829b9b0..d49d20e7 100644 --- a/crates/auths-verifier/tests/fixtures/credential_valid.json +++ b/crates/auths-verifier/tests/fixtures/credential_valid.json @@ -1 +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"}]}],"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"} +{"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"} \ No newline at end of file diff --git a/crates/auths-verifier/tests/fixtures/presentation_valid.json b/crates/auths-verifier/tests/fixtures/presentation_valid.json index 9f48218c..9c9f1247 100644 --- a/crates/auths-verifier/tests/fixtures/presentation_valid.json +++ b/crates/auths-verifier/tests/fixtures/presentation_valid.json @@ -1 +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"}]}],"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":[]}],"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"} +{"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"} \ No newline at end of file diff --git a/crates/auths-verifier/ts/exhaustiveness.ts b/crates/auths-verifier/ts/exhaustiveness.ts index 91793362..bcb24dbb 100644 --- a/crates/auths-verifier/ts/exhaustiveness.ts +++ b/crates/auths-verifier/ts/exhaustiveness.ts @@ -29,6 +29,8 @@ export function describePresentation(verdict: PresentationVerdict): string { return `credential not valid: ${describeCredential(verdict.credential)}`; case "malformedRequest": return `malformed request: ${verdict.message}`; + case "kelUnauthenticated": + return `KEL unauthenticated: ${verdict.field} (${verdict.detail})`; case "inputTooLarge": return `input too large: ${verdict.field} (${verdict.count} > ${verdict.limit})`; case "unsupportedSchemaVersion": @@ -63,6 +65,8 @@ export function describeCredential(verdict: CredentialVerdict): string { return "issuer KEL is duplicitous"; case "malformedRequest": return `malformed request: ${verdict.message}`; + case "kelUnauthenticated": + return `KEL unauthenticated: ${verdict.field} (${verdict.detail})`; case "inputTooLarge": return `input too large: ${verdict.field}`; case "unsupportedSchemaVersion": diff --git a/crates/auths-verifier/ts/verdict.ts b/crates/auths-verifier/ts/verdict.ts index 688c23c6..71b35d56 100644 --- a/crates/auths-verifier/ts/verdict.ts +++ b/crates/auths-verifier/ts/verdict.ts @@ -29,6 +29,7 @@ export type Capability = string & { readonly __brand: "Capability" }; /** Request-layer failures shared by both verdict unions (never a verification outcome). */ export type RequestError = | { kind: "malformedRequest"; message: string } + | { kind: "kelUnauthenticated"; field: string; detail: string } | { kind: "inputTooLarge"; field: string; count: number; limit: number } | { kind: "unsupportedSchemaVersion"; got: number; expected: number }; diff --git a/docs/architecture/kel-distribution.md b/docs/architecture/kel-distribution.md index 96d5c538..ec5efb90 100644 --- a/docs/architecture/kel-distribution.md +++ b/docs/architecture/kel-distribution.md @@ -20,13 +20,14 @@ mutating an event breaks the self-addressing SAID / hash chain. Continuing a KEL pre-committed next key (pre-rotation commitment `n = H(next_key)`), which only the controller holds — so KEL replay is authentic even without per-event controller signatures. -## C1 — git-remote resolution (done) - -`auths verify --remote ` resolves a signer's device + root KELs from a git remote with no -local pre-seeding. Implementation: `auths_storage::git::RemoteKelSource` fetches `refs/auths/registry` -into a throwaway temp repo; `auths_sdk::keri::KelResolverChain` orchestrates **local-first + rollback -floor** (a remote KEL older than the locally-trusted tip is rejected; a strictly-newer one is accepted; -ties prefer local). Local-only by default — no network without `--remote`. +## C1 — git-remote resolution (REMOVED) + +`--remote` resolution was removed together with the registry mirror (auths#374 / auths#262): the +transport carried **bare, unsigned events** — replay-by-structure over an untrusted feed is exactly +the RT-002 class — and the mirror that would have populated remotes never worked against real +(ssh/https) hosts, so the feed had no honest producer. KEL distribution is now **in-band**: the +committed identity bundle (`.auths/ci-bundle.json`, with per-event CESR signature attachments, +authenticated via `validate_signed_kel` at ingestion) travels with the clone that carries the code. ## C2 — OOBI-style static distribution diff --git a/docs/cli/commands/primary.md b/docs/cli/commands/primary.md index 64771c20..4756c566 100644 --- a/docs/cli/commands/primary.md +++ b/docs/cli/commands/primary.md @@ -71,8 +71,6 @@ Verify a signed commit or attestation. | `--witnesses-required ` | `1` | Number of witnesses required | | `--witness-keys ...` | — | Witness public keys as DID:hex pairs | | `--signature ` | — | Path to signature file. Only used when verifying an artifact file (not a commit). Defaults to .auths.json | -| `--remote ` | — | Fetch a signer's KEL from this git remote when it is absent locally (opt-in). The local registry stays the trusted floor — a remote can only advance the key-state, never roll it back. Without it, resolution is local-only (no network) | -| `--oobi ` | — | Fetch signer KELs over HTTP from this OOBI base URL (SSRF-hardened: HTTPS-only, no redirects, private/loopback hosts blocked). Takes precedence over `--remote` | | `--require-witnesses` | — | Fail verification when the signer's root KEL has not reached witness quorum (fail-closed). Default: warn and continue | | `-j, --json` | — | Emit machine-readable JSON | | `-q, --quiet` | — | Suppress non-essential output | diff --git a/docs/guides/git/signing-configuration.md b/docs/guides/git/signing-configuration.md index 983e1c79..d80422fd 100644 --- a/docs/guides/git/signing-configuration.md +++ b/docs/guides/git/signing-configuration.md @@ -161,7 +161,7 @@ This runs `git rebase --exec` under the hood to amend each commit with a fresh s ## How Verification Finds Your Key -Verification is KEL-native: `auths verify` reads the commit's `Auths-Device` trailer and resolves the signer's current key state from their key event log — no key list file to generate, distribute, or keep in sync. Keys are resolved from the local identity store by default, with opt-in remote resolution (`--remote` / `--oobi`) and explicit trust pinning (`auths trust pin`). See [Verifying Commits](verifying-commits.md). +Verification is KEL-native: `auths verify` reads the commit's `Auths-Device` trailer and resolves the signer's current key state from their key event log — no key list file to generate, distribute, or keep in sync. Keys resolve from the local identity store, or from the committed identity bundle (`.auths/ci-bundle.json`, produced by `auths id export-bundle`) that travels with the clone — plus explicit trust pinning (`auths trust pin`). See [Verifying Commits](verifying-commits.md). > **Note on native Git verification:** Git's own `git verify-commit` / `git log --show-signature` rely on Git's `gpg.ssh.allowedSignersFile` mechanism, which Auths no longer generates. Use `auths verify` instead — it understands the identity model behind the signature, not just the raw SSH key. diff --git a/docs/guides/git/team-workflows.md b/docs/guides/git/team-workflows.md index dbaaa106..7f31dbde 100644 --- a/docs/guides/git/team-workflows.md +++ b/docs/guides/git/team-workflows.md @@ -13,7 +13,7 @@ When you verify a commit, `auths verify` reads the signer's `did:keri:` identifi If teammates' KELs are available on the repository's git remote, resolve them on demand: ```bash -auths verify origin/main..HEAD --remote origin +auths verify origin/main..HEAD ``` This is opt-in (resolution is local-only by default). A remote can only advance a signer's key state, never roll it back — your local store stays the trusted floor. diff --git a/docs/guides/git/verifying-commits.md b/docs/guides/git/verifying-commits.md index a666465c..626dcc9e 100644 --- a/docs/guides/git/verifying-commits.md +++ b/docs/guides/git/verifying-commits.md @@ -10,7 +10,7 @@ Commit verification is **KEL-native** — there is no key list file to maintain. 1. Reads the SSH signature embedded in the Git commit object 2. Reads the `Auths-Device` trailer (the signer's `did:keri:` identifier) -3. Resolves the signer's key state from their key event log (KEL) in the local identity store — or, opt-in, from a git remote (`--remote`) or an OOBI HTTP endpoint (`--oobi`) +3. Resolves the signer's key state from their key event log (KEL) in the local identity store — or from the committed identity bundle (`.auths/ci-bundle.json`) the clone carries, authenticated event-by-event before use 4. Verifies the signature cryptographically against the resolved key 5. Optionally verifies the attestation chain (when `--identity-bundle` is provided) 6. Optionally enforces witness quorum (`--require-witnesses`, `--witness-signatures`) @@ -60,7 +60,7 @@ auths verify v1.0.0 If the signer's KEL is not in your local store (e.g. a teammate's commit), fetch it from the repository's remote: ```bash -auths verify HEAD --remote origin +auths verify HEAD ``` ### Output @@ -314,7 +314,7 @@ auths trust pin --did did:keri:E... --key Or fetch their KEL from the repository's remote: ```bash -auths verify HEAD --remote origin +auths verify HEAD ``` ### "No signature found" diff --git a/packages/auths-verifier-go/verifier.go b/packages/auths-verifier-go/verifier.go index 5905485c..f93a0f81 100644 --- a/packages/auths-verifier-go/verifier.go +++ b/packages/auths-verifier-go/verifier.go @@ -247,6 +247,7 @@ const ( KindWitnessQuorumNotMet VerdictKind = "witnessQuorumNotMet" KindIssuerKelDuplicitous VerdictKind = "issuerKelDuplicitous" KindMalformedRequest VerdictKind = "malformedRequest" + KindKelUnauthenticated VerdictKind = "kelUnauthenticated" KindInputTooLarge VerdictKind = "inputTooLarge" KindUnsupportedSchemaVersion VerdictKind = "unsupportedSchemaVersion" ) From 5b88273988ba98fbb5b2cf736d75d04db77ff83a Mon Sep 17 00:00:00 2001 From: bordumb Date: Sat, 18 Jul 2026 03:17:08 +0100 Subject: [PATCH 03/21] fix: formatting Auths-Id: did:keri:EB5cPHY0t-ejNC_rUzPS1dclTvd6kG-R9mQzjozCuGgd Auths-Device: did:keri:EB5cPHY0t-ejNC_rUzPS1dclTvd6kG-R9mQzjozCuGgd Auths-Anchor-Seq: 1 --- crates/auths-sdk/src/storage.rs | 4 ++-- crates/auths-storage/tests/cases/mock_ed25519_keypairs.rs | 1 + crates/auths-verifier/tests/fixtures/credential_revoked.json | 2 +- crates/auths-verifier/tests/fixtures/credential_valid.json | 2 +- crates/auths-verifier/tests/fixtures/presentation_valid.json | 2 +- 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/crates/auths-sdk/src/storage.rs b/crates/auths-sdk/src/storage.rs index 52ec8ab1..d4af5b3c 100644 --- a/crates/auths-sdk/src/storage.rs +++ b/crates/auths-sdk/src/storage.rs @@ -6,6 +6,6 @@ pub use auths_id::storage::GitWitnessReceiptLookup; #[cfg(feature = "backend-git")] pub use auths_storage::git::{ - GitAttestationStorage, GitIdentityStorage, GitRegistryBackend, - RegistryAttestationStorage, RegistryConfig, RegistryIdentityStorage, + GitAttestationStorage, GitIdentityStorage, GitRegistryBackend, RegistryAttestationStorage, + RegistryConfig, RegistryIdentityStorage, }; diff --git a/crates/auths-storage/tests/cases/mock_ed25519_keypairs.rs b/crates/auths-storage/tests/cases/mock_ed25519_keypairs.rs index 2ceb31ac..d6b2adb0 100644 --- a/crates/auths-storage/tests/cases/mock_ed25519_keypairs.rs +++ b/crates/auths-storage/tests/cases/mock_ed25519_keypairs.rs @@ -221,6 +221,7 @@ const ALL_PKCS8: [&[u8]; 20] = [ /// The pre-generated keypair at `index` (0..19) — e.g. to SIGN the events a /// fixture appends (the current key of identity `i` is `mock_keypair(i * 2)`). +#[allow(dead_code)] // a shared test helper; not every test case in the suite uses it pub fn mock_keypair(index: usize) -> Ed25519KeyPair { assert!(index < 20, "only 20 pre-generated keypairs available"); Ed25519KeyPair::from_pkcs8(ALL_PKCS8[index]).unwrap() diff --git a/crates/auths-verifier/tests/fixtures/credential_revoked.json b/crates/auths-verifier/tests/fixtures/credential_revoked.json index 46c4af33..95936597 100644 --- a/crates/auths-verifier/tests/fixtures/credential_revoked.json +++ b/crates/auths-verifier/tests/fixtures/credential_revoked.json @@ -1 +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"} \ No newline at end of file +{"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/crates/auths-verifier/tests/fixtures/credential_valid.json b/crates/auths-verifier/tests/fixtures/credential_valid.json index d49d20e7..dc7cfaae 100644 --- a/crates/auths-verifier/tests/fixtures/credential_valid.json +++ b/crates/auths-verifier/tests/fixtures/credential_valid.json @@ -1 +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"} \ No newline at end of file +{"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/crates/auths-verifier/tests/fixtures/presentation_valid.json b/crates/auths-verifier/tests/fixtures/presentation_valid.json index 9c9f1247..b1287dfa 100644 --- a/crates/auths-verifier/tests/fixtures/presentation_valid.json +++ b/crates/auths-verifier/tests/fixtures/presentation_valid.json @@ -1 +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"} \ No newline at end of file +{"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"} From d0949e6739242ea4c9cd872c9ce6dd7e4632c6f3 Mon Sep 17 00:00:00 2001 From: bordumb Date: Sat, 18 Jul 2026 03:22:20 +0100 Subject: [PATCH 04/21] fix: format Auths-Id: did:keri:EB5cPHY0t-ejNC_rUzPS1dclTvd6kG-R9mQzjozCuGgd Auths-Device: did:keri:EB5cPHY0t-ejNC_rUzPS1dclTvd6kG-R9mQzjozCuGgd Auths-Anchor-Seq: 1 --- packages/auths-node/index.js | 104 +++++++++++++++++------------------ 1 file changed, 52 insertions(+), 52 deletions(-) diff --git a/packages/auths-node/index.js b/packages/auths-node/index.js index fa0e80e3..78b0504b 100644 --- a/packages/auths-node/index.js +++ b/packages/auths-node/index.js @@ -77,8 +77,8 @@ function requireNative() { try { const binding = require('@auths-dev/sdk-android-arm64') const bindingPackageVersion = require('@auths-dev/sdk-android-arm64/package.json').version - if (bindingPackageVersion !== '0.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.1.5' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.5 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -93,8 +93,8 @@ function requireNative() { try { const binding = require('@auths-dev/sdk-android-arm-eabi') const bindingPackageVersion = require('@auths-dev/sdk-android-arm-eabi/package.json').version - if (bindingPackageVersion !== '0.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.1.5' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.5 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -114,8 +114,8 @@ function requireNative() { try { const binding = require('@auths-dev/sdk-win32-x64-gnu') const bindingPackageVersion = require('@auths-dev/sdk-win32-x64-gnu/package.json').version - if (bindingPackageVersion !== '0.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.1.5' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.5 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -130,8 +130,8 @@ function requireNative() { try { const binding = require('@auths-dev/sdk-win32-x64-msvc') const bindingPackageVersion = require('@auths-dev/sdk-win32-x64-msvc/package.json').version - if (bindingPackageVersion !== '0.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.1.5' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.5 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -147,8 +147,8 @@ function requireNative() { try { const binding = require('@auths-dev/sdk-win32-ia32-msvc') const bindingPackageVersion = require('@auths-dev/sdk-win32-ia32-msvc/package.json').version - if (bindingPackageVersion !== '0.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.1.5' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.5 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -163,8 +163,8 @@ function requireNative() { try { const binding = require('@auths-dev/sdk-win32-arm64-msvc') const bindingPackageVersion = require('@auths-dev/sdk-win32-arm64-msvc/package.json').version - if (bindingPackageVersion !== '0.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.1.5' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.5 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -182,8 +182,8 @@ function requireNative() { try { const binding = require('@auths-dev/sdk-darwin-universal') const bindingPackageVersion = require('@auths-dev/sdk-darwin-universal/package.json').version - if (bindingPackageVersion !== '0.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.1.5' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.5 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -198,8 +198,8 @@ function requireNative() { try { const binding = require('@auths-dev/sdk-darwin-x64') const bindingPackageVersion = require('@auths-dev/sdk-darwin-x64/package.json').version - if (bindingPackageVersion !== '0.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.1.5' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.5 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -214,8 +214,8 @@ function requireNative() { try { const binding = require('@auths-dev/sdk-darwin-arm64') const bindingPackageVersion = require('@auths-dev/sdk-darwin-arm64/package.json').version - if (bindingPackageVersion !== '0.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.1.5' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.5 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -234,8 +234,8 @@ function requireNative() { try { const binding = require('@auths-dev/sdk-freebsd-x64') const bindingPackageVersion = require('@auths-dev/sdk-freebsd-x64/package.json').version - if (bindingPackageVersion !== '0.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.1.5' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.5 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -250,8 +250,8 @@ function requireNative() { try { const binding = require('@auths-dev/sdk-freebsd-arm64') const bindingPackageVersion = require('@auths-dev/sdk-freebsd-arm64/package.json').version - if (bindingPackageVersion !== '0.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.1.5' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.5 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -271,8 +271,8 @@ function requireNative() { try { const binding = require('@auths-dev/sdk-linux-x64-musl') const bindingPackageVersion = require('@auths-dev/sdk-linux-x64-musl/package.json').version - if (bindingPackageVersion !== '0.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.1.5' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.5 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -287,8 +287,8 @@ function requireNative() { try { const binding = require('@auths-dev/sdk-linux-x64-gnu') const bindingPackageVersion = require('@auths-dev/sdk-linux-x64-gnu/package.json').version - if (bindingPackageVersion !== '0.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.1.5' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.5 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -305,8 +305,8 @@ function requireNative() { try { const binding = require('@auths-dev/sdk-linux-arm64-musl') const bindingPackageVersion = require('@auths-dev/sdk-linux-arm64-musl/package.json').version - if (bindingPackageVersion !== '0.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.1.5' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.5 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -321,8 +321,8 @@ function requireNative() { try { const binding = require('@auths-dev/sdk-linux-arm64-gnu') const bindingPackageVersion = require('@auths-dev/sdk-linux-arm64-gnu/package.json').version - if (bindingPackageVersion !== '0.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.1.5' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.5 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -339,8 +339,8 @@ function requireNative() { try { const binding = require('@auths-dev/sdk-linux-arm-musleabihf') const bindingPackageVersion = require('@auths-dev/sdk-linux-arm-musleabihf/package.json').version - if (bindingPackageVersion !== '0.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.1.5' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.5 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -355,8 +355,8 @@ function requireNative() { try { const binding = require('@auths-dev/sdk-linux-arm-gnueabihf') const bindingPackageVersion = require('@auths-dev/sdk-linux-arm-gnueabihf/package.json').version - if (bindingPackageVersion !== '0.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.1.5' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.5 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -373,8 +373,8 @@ function requireNative() { try { const binding = require('@auths-dev/sdk-linux-loong64-musl') const bindingPackageVersion = require('@auths-dev/sdk-linux-loong64-musl/package.json').version - if (bindingPackageVersion !== '0.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.1.5' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.5 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -389,8 +389,8 @@ function requireNative() { try { const binding = require('@auths-dev/sdk-linux-loong64-gnu') const bindingPackageVersion = require('@auths-dev/sdk-linux-loong64-gnu/package.json').version - if (bindingPackageVersion !== '0.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.1.5' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.5 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -407,8 +407,8 @@ function requireNative() { try { const binding = require('@auths-dev/sdk-linux-riscv64-musl') const bindingPackageVersion = require('@auths-dev/sdk-linux-riscv64-musl/package.json').version - if (bindingPackageVersion !== '0.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.1.5' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.5 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -423,8 +423,8 @@ function requireNative() { try { const binding = require('@auths-dev/sdk-linux-riscv64-gnu') const bindingPackageVersion = require('@auths-dev/sdk-linux-riscv64-gnu/package.json').version - if (bindingPackageVersion !== '0.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.1.5' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.5 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -440,8 +440,8 @@ function requireNative() { try { const binding = require('@auths-dev/sdk-linux-ppc64-gnu') const bindingPackageVersion = require('@auths-dev/sdk-linux-ppc64-gnu/package.json').version - if (bindingPackageVersion !== '0.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.1.5' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.5 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -456,8 +456,8 @@ function requireNative() { try { const binding = require('@auths-dev/sdk-linux-s390x-gnu') const bindingPackageVersion = require('@auths-dev/sdk-linux-s390x-gnu/package.json').version - if (bindingPackageVersion !== '0.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.1.5' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.5 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -476,8 +476,8 @@ function requireNative() { try { const binding = require('@auths-dev/sdk-openharmony-arm64') const bindingPackageVersion = require('@auths-dev/sdk-openharmony-arm64/package.json').version - if (bindingPackageVersion !== '0.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.1.5' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.5 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -492,8 +492,8 @@ function requireNative() { try { const binding = require('@auths-dev/sdk-openharmony-x64') const bindingPackageVersion = require('@auths-dev/sdk-openharmony-x64/package.json').version - if (bindingPackageVersion !== '0.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.1.5' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.5 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -508,8 +508,8 @@ function requireNative() { try { const binding = require('@auths-dev/sdk-openharmony-arm') const bindingPackageVersion = require('@auths-dev/sdk-openharmony-arm/package.json').version - if (bindingPackageVersion !== '0.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.1.5' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.5 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { From c3ee75b75aa37c3afc2a4498aab9cb3c1117cbd6 Mon Sep 17 00:00:00 2001 From: bordumb Date: Sat, 18 Jul 2026 03:38:07 +0100 Subject: [PATCH 05/21] =?UTF-8?q?sdk:=20presentation=20evidence=20export?= =?UTF-8?q?=20=E2=80=94=20the=20agent=20hands=20its=20verifier-ready=20pro?= =?UTF-8?q?of=20to=20any=20relying=20party?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit load_presentation_evidence gathers what load_presentation_inputs resolves PLUS the per-event CESR signature attachments each KEL carries in the registry, wire-shaped to the verify contract's field names. A KEL event without a stored attachment fails closed — evidence that cannot be authenticated is not exported as if it could. auths credential present --with-evidence emits { authorization, evidence } as one JSON object: the Auths-Presentation header plus everything a relying party with no registry replica splices into its verify request. Proven end-to-end against the market's agent write API (9-check e2e: valid create, replayed nonce, forged attachment, forged signature). The Node SDK's status enums gain KelUnauthenticated so the new verdict surfaces typed instead of through the Unknown forward-compat arm. Auths-Id: did:keri:EB5cPHY0t-ejNC_rUzPS1dclTvd6kG-R9mQzjozCuGgd Auths-Device: did:keri:EB5cPHY0t-ejNC_rUzPS1dclTvd6kG-R9mQzjozCuGgd Auths-Anchor-Seq: 1 --- crates/auths-cli/src/commands/credential.rs | 44 ++++++- .../auths-sdk/src/domains/credentials/mod.rs | 5 +- .../src/domains/credentials/present_inputs.rs | 109 ++++++++++++++++++ packages/auths-node/src/presentation.rs | 6 + 4 files changed, 157 insertions(+), 7 deletions(-) diff --git a/crates/auths-cli/src/commands/credential.rs b/crates/auths-cli/src/commands/credential.rs index 827680ee..084a24ab 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-sdk/src/domains/credentials/mod.rs b/crates/auths-sdk/src/domains/credentials/mod.rs index eed2ee6c..59ad7fb0 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 29381de0..c9468d00 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/packages/auths-node/src/presentation.rs b/packages/auths-node/src/presentation.rs index 9c13e140..36b16c38 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. @@ -193,6 +197,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 +216,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, From 2638b643e93e33179b5876f7d161d145705facb7 Mon Sep 17 00:00:00 2001 From: bordumb Date: Sat, 18 Jul 2026 03:42:15 +0100 Subject: [PATCH 06/21] sdk: regenerate Node type declarations for the KelUnauthenticated status Auths-Id: did:keri:EB5cPHY0t-ejNC_rUzPS1dclTvd6kG-R9mQzjozCuGgd Auths-Device: did:keri:EB5cPHY0t-ejNC_rUzPS1dclTvd6kG-R9mQzjozCuGgd Auths-Anchor-Seq: 1 --- packages/auths-node/index.d.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/auths-node/index.d.ts b/packages/auths-node/index.d.ts index 1ff71ddd..e06562d6 100644 --- a/packages/auths-node/index.d.ts +++ b/packages/auths-node/index.d.ts @@ -63,6 +63,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. */ @@ -318,6 +320,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. */ From b18dcee41e2e5bf6ac4ab83a31fccd3be275da32 Mon Sep 17 00:00:00 2001 From: bordumb Date: Sat, 18 Jul 2026 04:04:24 +0100 Subject: [PATCH 07/21] =?UTF-8?q?fix:=20the=20two=20failures=20the=20mirro?= =?UTF-8?q?r=20removal=20left=20behind=20=E2=80=94=20and=20the=20bundle=20?= =?UTF-8?q?learns=20device=20KELs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The third-party no-flags clone test failed for a real reason: auths init delegates a DEVICE identity and commits are signed by it, but the exported bundle carried only the ROOT KEL, so a clone could not resolve the signer. The bundle now carries every delegated device KEL the root anchored (device_kels, per-event hex attachments). BundleTrust authenticates each one — self-certifying dip SAID, then validate_signed_kel with the just-authenticated root's seal index as delegator lookup — and the verify resolver serves any bundle KEL (root or device) by DID, prefix-bound. The pre-push hook test asserted the removed mirror behavior; it now pins the actual contract: chain to the repo hook, exit clean, and never name registry push again. Auths-Id: did:keri:EB5cPHY0t-ejNC_rUzPS1dclTvd6kG-R9mQzjozCuGgd Auths-Device: did:keri:EB5cPHY0t-ejNC_rUzPS1dclTvd6kG-R9mQzjozCuGgd Auths-Anchor-Seq: 1 --- .../auths-cli/src/commands/artifact/verify.rs | 2 +- crates/auths-cli/src/commands/id/identity.rs | 48 +++++++++++++ .../auths-cli/src/commands/verify_commit.rs | 58 +++++++++------ .../auths-sdk/src/workflows/commit_hooks.rs | 18 +++-- crates/auths-verifier/src/commit_bundle.rs | 71 +++++++++++++++++-- crates/auths-verifier/src/core.rs | 20 ++++++ crates/auths-verifier/src/lib.rs | 18 ++--- 7 files changed, 193 insertions(+), 42 deletions(-) diff --git a/crates/auths-cli/src/commands/artifact/verify.rs b/crates/auths-cli/src/commands/artifact/verify.rs index c0760cd8..2f38fa7a 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/id/identity.rs b/crates/auths-cli/src/commands/id/identity.rs index 19c91e4f..a5548340 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 c5a329ec..f8faba30 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-sdk/src/workflows/commit_hooks.rs b/crates/auths-sdk/src/workflows/commit_hooks.rs index 3fb655c7..dfb7a03c 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 68a6d963..e32dcc9c 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,57 @@ 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()))?; + device_kels.push((device.did.clone(), device.kel.clone())); + } + Ok(Self { root_did: bundle.identity_did.to_string(), kel: bundle.kel.clone(), + device_kels, }) } @@ -152,10 +207,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) } } diff --git a/crates/auths-verifier/src/core.rs b/crates/auths-verifier/src/core.rs index 255e127a..a6cc5cb6 100644 --- a/crates/auths-verifier/src/core.rs +++ b/crates/auths-verifier/src/core.rs @@ -943,6 +943,14 @@ pub struct IdentityBundle { /// pre-RT-002 bundles. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub kel_attachments: Vec, + /// 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))] diff --git a/crates/auths-verifier/src/lib.rs b/crates/auths-verifier/src/lib.rs index cd4bd628..654d41a0 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, From 7d5cb52695c9c0c250ec56db45d83c05d700c527 Mon Sep 17 00:00:00 2001 From: bordumb Date: Sat, 18 Jul 2026 04:04:29 +0100 Subject: [PATCH 08/21] verifier: the Valid presentation verdict names the proven subject root MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A relying party crediting an account to a delegated agent's root must get that root from the verdict, never by parsing the dip out of unverified evidence. subject_root is the delegator AID when the subject KEL is dip-rooted (proven — the seal-checked replay validated the delegation) and the subject itself otherwise. Threaded through the JSON contract (subjectRoot), the TS union, and the Node SDK report. Auths-Id: did:keri:EB5cPHY0t-ejNC_rUzPS1dclTvd6kG-R9mQzjozCuGgd Auths-Device: did:keri:EB5cPHY0t-ejNC_rUzPS1dclTvd6kG-R9mQzjozCuGgd Auths-Anchor-Seq: 1 --- crates/auths-api/tests/cases/rp_auth.rs | 1 + crates/auths-id/src/policy/mod.rs | 2 ++ crates/auths-mcp-server/src/keri_auth.rs | 1 + crates/auths-rp/src/principal.rs | 2 ++ crates/auths-rp/tests/cases/verdict.rs | 1 + crates/auths-verifier/src/contract.rs | 3 +++ crates/auths-verifier/src/presentation.rs | 18 ++++++++++++++++++ .../tests/cases/freshness_honesty.rs | 1 + .../auths-verifier/tests/cases/presentation.rs | 9 +++++++++ crates/auths-verifier/ts/verdict.ts | 2 ++ packages/auths-node/index.d.ts | 2 ++ packages/auths-node/src/presentation.rs | 3 +++ 12 files changed, 45 insertions(+) diff --git a/crates/auths-api/tests/cases/rp_auth.rs b/crates/auths-api/tests/cases/rp_auth.rs index 3ac19490..fa90dd62 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-id/src/policy/mod.rs b/crates/auths-id/src/policy/mod.rs index 799c543a..9fb3dca7 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-mcp-server/src/keri_auth.rs b/crates/auths-mcp-server/src/keri_auth.rs index 74286d4e..b8c08fea 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 22753528..28c4dbd5 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 fb4ee1af..724df5a7 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-verifier/src/contract.rs b/crates/auths-verifier/src/contract.rs index 8233d588..1bedfb44 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()), diff --git a/crates/auths-verifier/src/presentation.rs b/crates/auths-verifier/src/presentation.rs index 5ce463c9..b67d36bb 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 9f7d160c..d971aca4 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 b4362029..92afd0b8 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 71b35d56..13a59d48 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/packages/auths-node/index.d.ts b/packages/auths-node/index.d.ts index e06562d6..f9eee08b 100644 --- a/packages/auths-node/index.d.ts +++ b/packages/auths-node/index.d.ts @@ -288,6 +288,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`. */ diff --git a/packages/auths-node/src/presentation.rs b/packages/auths-node/src/presentation.rs index 36b16c38..78798389 100644 --- a/packages/auths-node/src/presentation.rs +++ b/packages/auths-node/src/presentation.rs @@ -106,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`. @@ -242,6 +244,7 @@ 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"), From 521d98051ddd93b78908cb706a9dee454c6961e9 Mon Sep 17 00:00:00 2001 From: bordumb Date: Sat, 18 Jul 2026 04:22:07 +0100 Subject: [PATCH 09/21] =?UTF-8?q?fix:=20delegated=20attachments=20authenti?= =?UTF-8?q?cate=20everywhere=20=E2=80=94=20and=20the=20schema=20catches=20?= =?UTF-8?q?up?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pair_kel_attachments now parses the full delegated attachment form (-A signatures ++ -G source seal) and rehydrates the seal onto dip/drt events before validation — the JSON body of a delegated event carries no seal, and the bilateral binding needs it. This is the same rehydration org_bundle already did locally; it now lives once, at the pairing seam, so bundle verification and the JSON contract authenticate device KELs instead of refusing on trailing -GAB bytes (the golden-path failure). Also: the three IdentityBundle test initializers gain device_kels, the identity-bundle-v1 schema is regenerated for it, and the Node report surfaces kelUnauthenticated's detail instead of dropping it. Auths-Id: did:keri:EB5cPHY0t-ejNC_rUzPS1dclTvd6kG-R9mQzjozCuGgd Auths-Device: did:keri:EB5cPHY0t-ejNC_rUzPS1dclTvd6kG-R9mQzjozCuGgd Auths-Anchor-Seq: 1 --- crates/auths-keri/src/events.rs | 31 ++++++++++++++++++- crates/auths-verifier/src/commit_bundle.rs | 1 + crates/auths-verifier/src/core.rs | 2 ++ packages/auths-node/src/presentation.rs | 4 +-- schemas/identity-bundle-v1.json | 36 ++++++++++++++++++++++ 5 files changed, 71 insertions(+), 3 deletions(-) diff --git a/crates/auths-keri/src/events.rs b/crates/auths-keri/src/events.rs index 9e460e3c..bbc33b4c 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-verifier/src/commit_bundle.rs b/crates/auths-verifier/src/commit_bundle.rs index e32dcc9c..771acceb 100644 --- a/crates/auths-verifier/src/commit_bundle.rs +++ b/crates/auths-verifier/src/commit_bundle.rs @@ -473,6 +473,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/core.rs b/crates/auths-verifier/src/core.rs index a6cc5cb6..5a0aa971 100644 --- a/crates/auths-verifier/src/core.rs +++ b/crates/auths-verifier/src/core.rs @@ -1878,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), @@ -1929,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/packages/auths-node/src/presentation.rs b/packages/auths-node/src/presentation.rs index 78798389..b8aa0b46 100644 --- a/packages/auths-node/src/presentation.rs +++ b/packages/auths-node/src/presentation.rs @@ -234,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"), } } @@ -249,7 +249,7 @@ fn presentation_report(value: &Value) -> PresentationReport { 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/schemas/identity-bundle-v1.json b/schemas/identity-bundle-v1.json index 6dd0ab90..00a0e750 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" From 3af64086732ecc86856fdb7e8e068f15388dc9fd Mon Sep 17 00:00:00 2001 From: bordumb Date: Sat, 18 Jul 2026 04:28:53 +0100 Subject: [PATCH 10/21] fix: downstream replays use the rehydrated events the boundary authenticated MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit authenticate_kel now returns the paired events — rehydration restored each delegated event's -G source seal from its attachment, and the structural replays after the boundary re-enforce that seal against the delegator KEL, so handing them the bare JSON events failed delegated subjects with SubjectKelInvalid. Same fix in BundleTrust's device KELs. Proven by the market e2e: a delegated device identity now presents, verifies, and its root credit lands on the verdict's subjectRoot (13 checks green). Auths-Id: did:keri:EB5cPHY0t-ejNC_rUzPS1dclTvd6kG-R9mQzjozCuGgd Auths-Device: did:keri:EB5cPHY0t-ejNC_rUzPS1dclTvd6kG-R9mQzjozCuGgd Auths-Anchor-Seq: 1 --- crates/auths-verifier/src/commit_bundle.rs | 8 ++++++- crates/auths-verifier/src/contract.rs | 28 ++++++++++++---------- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/crates/auths-verifier/src/commit_bundle.rs b/crates/auths-verifier/src/commit_bundle.rs index 771acceb..2cf1b969 100644 --- a/crates/auths-verifier/src/commit_bundle.rs +++ b/crates/auths-verifier/src/commit_bundle.rs @@ -186,7 +186,13 @@ impl BundleTrust { .map_err(|e| BundleTrustError::KelUnauthenticated(e.to_string()))?; validate_signed_kel(&signed, Some(&root_seals)) .map_err(|e| BundleTrustError::KelUnauthenticated(e.to_string()))?; - device_kels.push((device.did.clone(), device.kel.clone())); + // 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 { diff --git a/crates/auths-verifier/src/contract.rs b/crates/auths-verifier/src/contract.rs index 1bedfb44..8cb2581b 100644 --- a/crates/auths-verifier/src/contract.rs +++ b/crates/auths-verifier/src/contract.rs @@ -512,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 @@ -522,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() { @@ -542,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. @@ -563,20 +565,20 @@ fn run_presentation(request_json: &str) -> Result Result Result Result Date: Sat, 18 Jul 2026 04:42:57 +0100 Subject: [PATCH 11/21] sdk(node): delegateAgent mints a KERI-delegated agent, not an attestation link MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The napi surface now routes through agents::delegation::add_scoped — the agent gets its own dip-rooted KEL the parent anchors, with the requested capabilities as a delegator-anchored scope seal. The old path minted a did:key attestation agent, which no verifier in the delegated presentation flow could honor. The bundle drops attestationJson and gains agentPrefix; the TS wrapper converts expiresInDays to seconds instead of passing days where seconds were read. Proven by the market e2e: the SDK-minted agent presents with delegator evidence and its root credit lands on subjectRoot. Auths-Id: did:keri:EB5cPHY0t-ejNC_rUzPS1dclTvd6kG-R9mQzjozCuGgd Auths-Device: did:keri:EB5cPHY0t-ejNC_rUzPS1dclTvd6kG-R9mQzjozCuGgd Auths-Anchor-Seq: 1 --- packages/auths-node/Cargo.toml | 1 + .../auths-node/__test__/integration.spec.ts | 4 +- packages/auths-node/index.d.ts | 7 +- packages/auths-node/lib/identity.ts | 12 +-- packages/auths-node/src/identity.rs | 90 +++++++------------ packages/auths-node/src/types.rs | 5 +- 6 files changed, 52 insertions(+), 67 deletions(-) diff --git a/packages/auths-node/Cargo.toml b/packages/auths-node/Cargo.toml index 772d49a9..c92ccf60 100644 --- a/packages/auths-node/Cargo.toml +++ b/packages/auths-node/Cargo.toml @@ -21,6 +21,7 @@ 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-storage = { path = "../../crates/auths-storage", features = ["backend-git"] } auths-policy = { path = "../../crates/auths-policy" } auths-pairing-daemon = { path = "../../crates/auths-pairing-daemon" } diff --git a/packages/auths-node/__test__/integration.spec.ts b/packages/auths-node/__test__/integration.spec.ts index 7838c90f..f5ebd972 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/index.d.ts b/packages/auths-node/index.d.ts index f9eee08b..3b9825d2 100644 --- a/packages/auths-node/index.d.ts +++ b/packages/auths-node/index.d.ts @@ -168,9 +168,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 } diff --git a/packages/auths-node/lib/identity.ts b/packages/auths-node/lib/identity.ts index b93e8498..2fd77a70 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/src/identity.rs b/packages/auths-node/src/identity.rs index a0b2f358..b4fd47c5 100644 --- a/packages/auths-node/src/identity.rs +++ b/packages/auths-node/src/identity.rs @@ -1,7 +1,6 @@ use std::path::PathBuf; use std::sync::Arc; -use auths_core::crypto::signer::encrypt_keypair; use auths_core::signing::PrefilledPassphraseProvider; use auths_core::storage::keychain::{KeyAlias, KeyRole, get_platform_keychain_with_config}; use auths_id::identity::initialize::initialize_registry_identity; @@ -279,37 +278,16 @@ 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 +304,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/types.rs b/packages/auths-node/src/types.rs index db5d6819..26dddb0a 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, } From 12b4097e01c9b135172059b7a86295a1af53fa53 Mon Sep 17 00:00:00 2001 From: bordumb Date: Sat, 18 Jul 2026 05:00:01 +0100 Subject: [PATCH 12/21] =?UTF-8?q?sdk(node):=20the=20relying=20party=20live?= =?UTF-8?q?s=20here=20=E2=80=94=20one=20call=20authenticates=20a=20present?= =?UTF-8?q?ation=20request?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit presentationNonce parses an incoming Auths-Presentation header (parse-only, nothing trusted) so a relying party can burn its single-use challenge before verification. authenticatePresentation does everything else in Rust over the auths-rp wire types and the verifier's JSON contract: audience and nonce binding, the evidence-carrying request, KEL-authenticated verification, and the verdict-to-denial mapping. A relying party keeps storage and grant policy; it re-implements no wire, no base64, no assembly, no doctrine. Auths-Id: did:keri:EB5cPHY0t-ejNC_rUzPS1dclTvd6kG-R9mQzjozCuGgd Auths-Device: did:keri:EB5cPHY0t-ejNC_rUzPS1dclTvd6kG-R9mQzjozCuGgd Auths-Anchor-Seq: 1 --- packages/auths-node/Cargo.toml | 3 + packages/auths-node/index.d.ts | 77 +++++++++++ packages/auths-node/index.js | 2 + packages/auths-node/src/lib.rs | 1 + packages/auths-node/src/rp.rs | 230 +++++++++++++++++++++++++++++++++ 5 files changed, 313 insertions(+) create mode 100644 packages/auths-node/src/rp.rs diff --git a/packages/auths-node/Cargo.toml b/packages/auths-node/Cargo.toml index c92ccf60..32ed2ebe 100644 --- a/packages/auths-node/Cargo.toml +++ b/packages/auths-node/Cargo.toml @@ -22,6 +22,7 @@ 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" } @@ -30,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/index.d.ts b/packages/auths-node/index.d.ts index 3b9825d2..d468b58e 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 @@ -121,6 +146,27 @@ export interface NapiActionEnvelope { 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 @@ -249,6 +295,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 @@ -285,6 +344,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. */ diff --git a/packages/auths-node/index.js b/packages/auths-node/index.js index 78b0504b..83624c7b 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 @@ -600,6 +601,7 @@ module.exports.listOrgMembers = nativeBinding.listOrgMembers module.exports.listPinnedIdentities = nativeBinding.listPinnedIdentities module.exports.listWitnesses = nativeBinding.listWitnesses 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/src/lib.rs b/packages/auths-node/src/lib.rs index 407398dd..214cfb8d 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/rp.rs b/packages/auths-node/src/rp.rs new file mode 100644 index 00000000..abfe8216 --- /dev/null +++ b/packages/auths-node/src/rp.rs @@ -0,0 +1,230 @@ +//! 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, + } +} + +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, + }) +} From bac2bbba90aebd015755097e7df6e5cf07dfe3a1 Mon Sep 17 00:00:00 2001 From: bordumb Date: Sat, 18 Jul 2026 05:02:01 +0100 Subject: [PATCH 13/21] =?UTF-8?q?docs:=20the=20report=20is=20the=20only=20?= =?UTF-8?q?API=20=E2=80=94=20relying=20parties=20never=20inspect=20evidenc?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Institutionalizes the subjectRoot precedent: any fact an RP acts on is a verdict field the verifier proved, and the wire lives once behind the SDK's relying-party surface. RP-side evidence parsing is a missing verdict field, to be fixed in auths-verifier, never in the consumer. Auths-Id: did:keri:EB5cPHY0t-ejNC_rUzPS1dclTvd6kG-R9mQzjozCuGgd Auths-Device: did:keri:EB5cPHY0t-ejNC_rUzPS1dclTvd6kG-R9mQzjozCuGgd Auths-Anchor-Seq: 1 --- ARCHITECTURE.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 83fc5682..688194a2 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/`. From e5c2400107377fce3b451f83b17528fcef21e76b Mon Sep 17 00:00:00 2001 From: bordumb Date: Sat, 18 Jul 2026 05:04:19 +0100 Subject: [PATCH 14/21] =?UTF-8?q?lint:=20bindings=20are=20a=20presentation?= =?UTF-8?q?=20layer=20=E2=80=94=20check-binding-boundary?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit packages/auths-node marshals; it does not reach into auths_id/auths_core/ auths_storage primitives (the delegateAgent incident — bindings minting attestation agents long after the SDK went KERI-native — is the class this prevents). Existing reaches carry a binding-boundary-allow annotation and shrink over time; a new unannotated deep import fails CI. Auths-Id: did:keri:EB5cPHY0t-ejNC_rUzPS1dclTvd6kG-R9mQzjozCuGgd Auths-Device: did:keri:EB5cPHY0t-ejNC_rUzPS1dclTvd6kG-R9mQzjozCuGgd Auths-Anchor-Seq: 1 --- .github/workflows/ci.yml | 1 + crates/xtask/src/check_binding_boundary.rs | 73 ++++++++++++++++++++ crates/xtask/src/main.rs | 4 ++ packages/auths-node/src/artifact.rs | 6 +- packages/auths-node/src/attestation_query.rs | 6 +- packages/auths-node/src/commit_sign.rs | 4 +- packages/auths-node/src/device.rs | 6 +- packages/auths-node/src/helpers.rs | 4 +- packages/auths-node/src/identity.rs | 10 +-- packages/auths-node/src/org.rs | 20 +++--- packages/auths-node/src/pairing.rs | 6 +- packages/auths-node/src/sign.rs | 4 +- packages/auths-node/src/trust.rs | 6 +- packages/auths-node/src/witness.rs | 8 +-- 14 files changed, 118 insertions(+), 40 deletions(-) create mode 100644 crates/xtask/src/check_binding_boundary.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1fab55e3..404f63d5 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/crates/xtask/src/check_binding_boundary.rs b/crates/xtask/src/check_binding_boundary.rs new file mode 100644 index 00000000..1fc2e959 --- /dev/null +++ b/crates/xtask/src/check_binding_boundary.rs @@ -0,0 +1,73 @@ +//! 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::{Context, bail}; + +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()))?; + for (idx, line) in content.lines().enumerate() { + let trimmed = line.trim_start(); + let is_deep = DEEP_PREFIXES.iter().any(|p| trimmed.starts_with(p)); + if is_deep && !line.contains(ALLOW_MARKER) { + 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 b35ae331..71e718ce 100644 --- a/crates/xtask/src/main.rs +++ b/crates/xtask/src/main.rs @@ -7,6 +7,7 @@ clippy::expect_used )] mod check_admission_policy; +mod check_binding_boundary; mod check_anchor_discipline; mod check_clippy_sync; mod check_command_drift; @@ -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,7 @@ 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/packages/auths-node/src/artifact.rs b/packages/auths-node/src/artifact.rs index 59d8bcf6..ca6afab8 100644 --- a/packages/auths-node/src/artifact.rs +++ b/packages/auths-node/src/artifact.rs @@ -2,8 +2,8 @@ use std::io::Read; use std::path::PathBuf; use std::sync::Arc; -use auths_core::signing::PrefilledPassphraseProvider; -use auths_core::storage::keychain::{IdentityDID, KeyAlias, get_platform_keychain_with_config}; +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}; // binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow use auths_crypto::decode_seed_hex; use auths_sdk::context::AuthsContext; use auths_sdk::ports::artifact::{ArtifactDigest, ArtifactError, ArtifactMetadata, ArtifactSource}; @@ -11,7 +11,7 @@ use auths_sdk::signing::{ ArtifactSigningParams, SigningKeyMaterial, sign_artifact as sdk_sign_artifact, sign_artifact_raw, }; -use auths_storage::git::{ +use auths_storage::git::{ // binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow GitRegistryBackend, RegistryAttestationStorage, RegistryConfig, RegistryIdentityStorage, }; use auths_verifier::clock::SystemClock; diff --git a/packages/auths-node/src/attestation_query.rs b/packages/auths-node/src/attestation_query.rs index 412e50e9..97f27e56 100644 --- a/packages/auths-node/src/attestation_query.rs +++ b/packages/auths-node/src/attestation_query.rs @@ -1,9 +1,9 @@ use std::path::PathBuf; use std::sync::Arc; -use auths_id::attestation::group::AttestationGroup; -use auths_id::storage::attestation::AttestationSource; -use auths_storage::git::{GitRegistryBackend, RegistryAttestationStorage, RegistryConfig}; +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}; // binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow use auths_verifier::core::Attestation; use auths_verifier::types::CanonicalDid; use napi_derive::napi; diff --git a/packages/auths-node/src/commit_sign.rs b/packages/auths-node/src/commit_sign.rs index 0f09e9db..13130bbc 100644 --- a/packages/auths-node/src/commit_sign.rs +++ b/packages/auths-node/src/commit_sign.rs @@ -1,8 +1,8 @@ use std::path::PathBuf; use std::sync::Arc; -use auths_core::signing::PrefilledPassphraseProvider; -use auths_core::storage::keychain::get_platform_keychain_with_config; +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; // binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow 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 056a80dc..b7db45f8 100644 --- a/packages/auths-node/src/device.rs +++ b/packages/auths-node/src/device.rs @@ -1,13 +1,13 @@ use std::path::PathBuf; use std::sync::Arc; -use auths_core::signing::PrefilledPassphraseProvider; -use auths_core::storage::keychain::get_platform_keychain_with_config; +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; // binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow 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}; -use auths_storage::git::{ +use auths_storage::git::{ // binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow GitRegistryBackend, RegistryAttestationStorage, RegistryConfig, RegistryIdentityStorage, }; use auths_verifier::clock::SystemClock; diff --git a/packages/auths-node/src/helpers.rs b/packages/auths-node/src/helpers.rs index 61c9b039..8ffdf12b 100644 --- a/packages/auths-node/src/helpers.rs +++ b/packages/auths-node/src/helpers.rs @@ -1,7 +1,7 @@ use std::path::PathBuf; -use auths_core::config::{EnvironmentConfig, KeychainConfig}; -use auths_core::storage::keychain::{ +use auths_core::config::{EnvironmentConfig, KeychainConfig}; // binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow +use auths_core::storage::keychain::{ // binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow 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 b4fd47c5..fb279eed 100644 --- a/packages/auths-node/src/identity.rs +++ b/packages/auths-node/src/identity.rs @@ -1,15 +1,15 @@ use std::path::PathBuf; use std::sync::Arc; -use auths_core::signing::PrefilledPassphraseProvider; -use auths_core::storage::keychain::{KeyAlias, KeyRole, get_platform_keychain_with_config}; -use auths_id::identity::initialize::initialize_registry_identity; -use auths_id::storage::attestation::AttestationSource; +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; // binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow use auths_sdk::context::AuthsContext; use auths_sdk::device::link_device; use auths_sdk::types::{DeviceLinkConfig, IdentityRotationConfig}; use auths_sdk::workflows::rotation::rotate_identity; -use auths_storage::git::{ +use auths_storage::git::{ // binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow GitRegistryBackend, RegistryAttestationStorage, RegistryConfig, RegistryIdentityStorage, }; use auths_verifier::clock::SystemClock; diff --git a/packages/auths-node/src/org.rs b/packages/auths-node/src/org.rs index 16e8f407..a4f110a7 100644 --- a/packages/auths-node/src/org.rs +++ b/packages/auths-node/src/org.rs @@ -1,18 +1,18 @@ use std::path::PathBuf; use std::sync::Arc; -use auths_core::ports::id::SystemUuidProvider; -use auths_core::signing::{DidResolver, PrefilledPassphraseProvider, StorageSigner}; -use auths_core::storage::keychain::{IdentityDID, KeyAlias, KeyStorage}; -use auths_id::attestation::create::create_signed_attestation; -use auths_id::identity::initialize::initialize_registry_identity; -use auths_id::identity::resolve::RegistryDidResolver; -use auths_id::keri::types::Prefix; -use auths_id::storage::git_refs::AttestationMetadata; -use auths_id::storage::registry::RegistryBackend; +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; // binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow use auths_sdk::context::AuthsContext; use auths_sdk::workflows::org::{add_member, list_members, revoke_member}; -use auths_storage::git::{ +use auths_storage::git::{ // binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow GitRegistryBackend, RegistryAttestationStorage, RegistryConfig, RegistryIdentityStorage, }; use auths_verifier::clock::SystemClock; diff --git a/packages/auths-node/src/pairing.rs b/packages/auths-node/src/pairing.rs index a792263b..1b239790 100644 --- a/packages/auths-node/src/pairing.rs +++ b/packages/auths-node/src/pairing.rs @@ -4,14 +4,14 @@ use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::sync::Arc; use std::time::Duration; -use auths_core::storage::keychain::{IdentityDID, KeyRole}; -use auths_id::storage::identity::IdentityStorage; +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; // binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow use auths_pairing_daemon::{ MockNetworkDiscovery, MockNetworkInterfaces, PairingDaemonBuilder, PairingDaemonHandle, RateLimiter, }; use auths_sdk::pairing::{PairingSessionParams, build_pairing_session_request}; -use auths_storage::git::RegistryIdentityStorage; +use auths_storage::git::RegistryIdentityStorage; // binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow use chrono::Utc; use tokio::sync::Mutex; diff --git a/packages/auths-node/src/sign.rs b/packages/auths-node/src/sign.rs index 79e95047..2f37de19 100644 --- a/packages/auths-node/src/sign.rs +++ b/packages/auths-node/src/sign.rs @@ -1,5 +1,5 @@ -use auths_core::signing::{PrefilledPassphraseProvider, SecureSigner, StorageSigner}; -use auths_core::storage::keychain::{KeyAlias, get_platform_keychain_with_config}; +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}; // binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow use auths_verifier::action::ActionEnvelope; use auths_verifier::core::MAX_ATTESTATION_JSON_SIZE; use auths_verifier::types::IdentityDID; diff --git a/packages/auths-node/src/trust.rs b/packages/auths-node/src/trust.rs index fe37e492..ad20e936 100644 --- a/packages/auths-node/src/trust.rs +++ b/packages/auths-node/src/trust.rs @@ -1,9 +1,9 @@ use std::path::PathBuf; use std::sync::Arc; -use auths_core::trust::pinned::{PinnedIdentity, PinnedIdentityStore, TrustLevel}; -use auths_id::identity::resolve::{DefaultDidResolver, DidResolver, RegistryDidResolver}; -use auths_storage::git::{GitRegistryBackend, RegistryConfig}; +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}; // binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow use auths_verifier::PublicKeyHex; use napi_derive::napi; diff --git a/packages/auths-node/src/witness.rs b/packages/auths-node/src/witness.rs index 5093b5dd..0b56fff4 100644 --- a/packages/auths-node/src/witness.rs +++ b/packages/auths-node/src/witness.rs @@ -1,9 +1,9 @@ use std::path::PathBuf; -use auths_id::keri::types::Prefix; -use auths_id::storage::identity::IdentityStorage; -use auths_id::witness_config::{WitnessConfig, WitnessRef}; -use auths_storage::git::RegistryIdentityStorage; +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; // binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow use napi_derive::napi; use crate::error::format_error; From dd0e09c2b044ef2439892de1b620fd9a1e324791 Mon Sep 17 00:00:00 2001 From: bordumb Date: Sat, 18 Jul 2026 05:05:02 +0100 Subject: [PATCH 15/21] =?UTF-8?q?sdk(node):=20the=20contract=20ships=20wit?= =?UTF-8?q?h=20the=20package=20=E2=80=94=20conformance=20vectors=20+=20can?= =?UTF-8?q?onical=20statuses?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit conformance/ carries the exact golden vectors the Rust verifier tests against (valid + revoked presentations/credentials) and statuses.json parsed from the generated declarations, so a consumer asserts against the contract version it actually runs — the seam is tested from both sides, versioned together. Synced on build and prepublish; published in files. Auths-Id: did:keri:EB5cPHY0t-ejNC_rUzPS1dclTvd6kG-R9mQzjozCuGgd Auths-Device: did:keri:EB5cPHY0t-ejNC_rUzPS1dclTvd6kG-R9mQzjozCuGgd Auths-Anchor-Seq: 1 --- .../conformance/credential_revoked.json | 1 + .../conformance/credential_valid.json | 1 + .../conformance/presentation_valid.json | 1 + packages/auths-node/conformance/statuses.json | 32 ++++++++++++++ packages/auths-node/package.json | 14 ++++-- .../auths-node/scripts/sync-conformance.mjs | 44 +++++++++++++++++++ 6 files changed, 89 insertions(+), 4 deletions(-) create mode 100644 packages/auths-node/conformance/credential_revoked.json create mode 100644 packages/auths-node/conformance/credential_valid.json create mode 100644 packages/auths-node/conformance/presentation_valid.json create mode 100644 packages/auths-node/conformance/statuses.json create mode 100644 packages/auths-node/scripts/sync-conformance.mjs diff --git a/packages/auths-node/conformance/credential_revoked.json b/packages/auths-node/conformance/credential_revoked.json new file mode 100644 index 00000000..95936597 --- /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 00000000..dc7cfaae --- /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 00000000..b1287dfa --- /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 00000000..85242080 --- /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/package.json b/packages/auths-node/package.json index d9d949a2..da5af154 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 00000000..2e202e23 --- /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`); From beeed3f585c0dd98456f414682ad9c51b2a852be Mon Sep 17 00:00:00 2001 From: bordumb Date: Sat, 18 Jul 2026 05:05:51 +0100 Subject: [PATCH 16/21] =?UTF-8?q?sdk(node):=20mintChallengeNonce=20?= =?UTF-8?q?=E2=80=94=20the=20nonce=20shape=20is=20the=20contract's,=20not?= =?UTF-8?q?=20the=20caller's?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 32 random bytes in base64url via the auths-rp Nonce type, so a relying party never invents its own format and parse_b64url on the other side can never disagree with the mint. Auths-Id: did:keri:EB5cPHY0t-ejNC_rUzPS1dclTvd6kG-R9mQzjozCuGgd Auths-Device: did:keri:EB5cPHY0t-ejNC_rUzPS1dclTvd6kG-R9mQzjozCuGgd Auths-Anchor-Seq: 1 --- packages/auths-node/index.d.ts | 14 ++++++++++++++ packages/auths-node/index.js | 1 + packages/auths-node/src/rp.rs | 16 ++++++++++++++++ 3 files changed, 31 insertions(+) diff --git a/packages/auths-node/index.d.ts b/packages/auths-node/index.d.ts index d468b58e..6f4a7d4c 100644 --- a/packages/auths-node/index.d.ts +++ b/packages/auths-node/index.d.ts @@ -140,6 +140,20 @@ 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 diff --git a/packages/auths-node/index.js b/packages/auths-node/index.js index 83624c7b..60cee667 100644 --- a/packages/auths-node/index.js +++ b/packages/auths-node/index.js @@ -600,6 +600,7 @@ 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 diff --git a/packages/auths-node/src/rp.rs b/packages/auths-node/src/rp.rs index abfe8216..87ae34ca 100644 --- a/packages/auths-node/src/rp.rs +++ b/packages/auths-node/src/rp.rs @@ -65,6 +65,22 @@ fn denied(code: &str, detail: Option) -> NapiAgentAuthReport { } } +/// 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 { + let bytes: [u8; 32] = rand::random(); + Nonce::from_bytes(bytes).to_b64url() +} + fn parse_wire(authorization_header: &str) -> Result { let scheme = format!("{AUTHS_PRESENTATION_SCHEME} "); let token = authorization_header From e12bcb1fb2fc0647a53b142f12303d82ebbb1207 Mon Sep 17 00:00:00 2001 From: bordumb Date: Sat, 18 Jul 2026 05:07:57 +0100 Subject: [PATCH 17/21] style: rustfmt on the binding-boundary check Auths-Id: did:keri:EB5cPHY0t-ejNC_rUzPS1dclTvd6kG-R9mQzjozCuGgd Auths-Device: did:keri:EB5cPHY0t-ejNC_rUzPS1dclTvd6kG-R9mQzjozCuGgd Auths-Anchor-Seq: 1 --- crates/xtask/src/check_binding_boundary.rs | 6 ++++-- crates/xtask/src/main.rs | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/crates/xtask/src/check_binding_boundary.rs b/crates/xtask/src/check_binding_boundary.rs index 1fc2e959..0d46cc8d 100644 --- a/crates/xtask/src/check_binding_boundary.rs +++ b/crates/xtask/src/check_binding_boundary.rs @@ -14,7 +14,7 @@ use std::path::PathBuf; -use anyhow::{Context, bail}; +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::"]; @@ -48,7 +48,9 @@ pub fn run(workspace_root: PathBuf) -> anyhow::Result<()> { if is_deep && !line.contains(ALLOW_MARKER) { violations.push(format!( "{}:{}: {}", - path.strip_prefix(&workspace_root).unwrap_or(&path).display(), + path.strip_prefix(&workspace_root) + .unwrap_or(&path) + .display(), idx + 1, trimmed )); diff --git a/crates/xtask/src/main.rs b/crates/xtask/src/main.rs index 71e718ce..95db1ed1 100644 --- a/crates/xtask/src/main.rs +++ b/crates/xtask/src/main.rs @@ -7,8 +7,8 @@ clippy::expect_used )] mod check_admission_policy; -mod check_binding_boundary; mod check_anchor_discipline; +mod check_binding_boundary; mod check_clippy_sync; mod check_command_drift; mod check_constant_time; @@ -98,7 +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::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()), From 1a40b5d57f6d27733adb94ae93931d569ed4f710 Mon Sep 17 00:00:00 2001 From: bordumb Date: Sat, 18 Jul 2026 05:09:43 +0100 Subject: [PATCH 18/21] fix: formatting Auths-Id: did:keri:EB5cPHY0t-ejNC_rUzPS1dclTvd6kG-R9mQzjozCuGgd Auths-Device: did:keri:EB5cPHY0t-ejNC_rUzPS1dclTvd6kG-R9mQzjozCuGgd Auths-Anchor-Seq: 1 --- packages/auths-node/src/artifact.rs | 8 ++++++-- packages/auths-node/src/device.rs | 8 ++++++-- packages/auths-node/src/helpers.rs | 9 +++++++-- packages/auths-node/src/identity.rs | 13 ++++++++++--- packages/auths-node/src/org.rs | 8 ++++++-- packages/auths-node/src/rp.rs | 5 ++++- 6 files changed, 39 insertions(+), 12 deletions(-) diff --git a/packages/auths-node/src/artifact.rs b/packages/auths-node/src/artifact.rs index ca6afab8..fe89d37f 100644 --- a/packages/auths-node/src/artifact.rs +++ b/packages/auths-node/src/artifact.rs @@ -11,8 +11,12 @@ use auths_sdk::signing::{ ArtifactSigningParams, SigningKeyMaterial, sign_artifact as sdk_sign_artifact, sign_artifact_raw, }; -use auths_storage::git::{ // binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow - GitRegistryBackend, RegistryAttestationStorage, RegistryConfig, RegistryIdentityStorage, +use auths_storage::git::{ + // binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow + GitRegistryBackend, + RegistryAttestationStorage, + RegistryConfig, + RegistryIdentityStorage, }; use auths_verifier::clock::SystemClock; use chrono::Utc; diff --git a/packages/auths-node/src/device.rs b/packages/auths-node/src/device.rs index b7db45f8..a461c070 100644 --- a/packages/auths-node/src/device.rs +++ b/packages/auths-node/src/device.rs @@ -7,8 +7,12 @@ 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}; -use auths_storage::git::{ // binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow - GitRegistryBackend, RegistryAttestationStorage, RegistryConfig, RegistryIdentityStorage, +use auths_storage::git::{ + // binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow + GitRegistryBackend, + RegistryAttestationStorage, + RegistryConfig, + RegistryIdentityStorage, }; use auths_verifier::clock::SystemClock; use auths_verifier::core::Capability; diff --git a/packages/auths-node/src/helpers.rs b/packages/auths-node/src/helpers.rs index 8ffdf12b..7eee1d0f 100644 --- a/packages/auths-node/src/helpers.rs +++ b/packages/auths-node/src/helpers.rs @@ -1,8 +1,13 @@ use std::path::PathBuf; use auths_core::config::{EnvironmentConfig, KeychainConfig}; // binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow -use auths_core::storage::keychain::{ // binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow - IdentityDID, KeyAlias, KeyRole, KeyStorage, get_platform_keychain_with_config, +use auths_core::storage::keychain::{ + // binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow + IdentityDID, + KeyAlias, + KeyRole, + KeyStorage, + get_platform_keychain_with_config, }; use crate::error::format_error; diff --git a/packages/auths-node/src/identity.rs b/packages/auths-node/src/identity.rs index fb279eed..a1014225 100644 --- a/packages/auths-node/src/identity.rs +++ b/packages/auths-node/src/identity.rs @@ -9,8 +9,12 @@ use auths_sdk::context::AuthsContext; use auths_sdk::device::link_device; use auths_sdk::types::{DeviceLinkConfig, IdentityRotationConfig}; use auths_sdk::workflows::rotation::rotate_identity; -use auths_storage::git::{ // binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow - GitRegistryBackend, RegistryAttestationStorage, RegistryConfig, RegistryIdentityStorage, +use auths_storage::git::{ + // binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow + GitRegistryBackend, + RegistryAttestationStorage, + RegistryConfig, + RegistryIdentityStorage, }; use auths_verifier::clock::SystemClock; use auths_verifier::core::Capability; @@ -282,7 +286,10 @@ pub fn delegate_agent( .iter() .map(|c| { auths_keri::Capability::parse(c).map_err(|e| { - format_error("AUTHS_INVALID_INPUT", format!("Invalid capability '{c}': {e}")) + format_error( + "AUTHS_INVALID_INPUT", + format!("Invalid capability '{c}': {e}"), + ) }) }) .collect::>()?; diff --git a/packages/auths-node/src/org.rs b/packages/auths-node/src/org.rs index a4f110a7..59c82407 100644 --- a/packages/auths-node/src/org.rs +++ b/packages/auths-node/src/org.rs @@ -12,8 +12,12 @@ use auths_id::storage::git_refs::AttestationMetadata; // binding-boundary-allow: use auths_id::storage::registry::RegistryBackend; // binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow use auths_sdk::context::AuthsContext; use auths_sdk::workflows::org::{add_member, list_members, revoke_member}; -use auths_storage::git::{ // binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow - GitRegistryBackend, RegistryAttestationStorage, RegistryConfig, RegistryIdentityStorage, +use auths_storage::git::{ + // binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow + GitRegistryBackend, + RegistryAttestationStorage, + RegistryConfig, + RegistryIdentityStorage, }; use auths_verifier::clock::SystemClock; use auths_verifier::core::Role; diff --git a/packages/auths-node/src/rp.rs b/packages/auths-node/src/rp.rs index 87ae34ca..72281d62 100644 --- a/packages/auths-node/src/rp.rs +++ b/packages/auths-node/src/rp.rs @@ -166,7 +166,10 @@ pub fn authenticate_presentation( 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()))); + return Ok(denied( + "bad-authorization-header", + Some("malformed nonce".into()), + )); }; let Ok(signature_bytes) = B64_URL.decode(&wire.signature_b64) else { return Ok(denied( From fafa2eb2d8c1f4b3023dbb307cb7f84439cad6a4 Mon Sep 17 00:00:00 2001 From: bordumb Date: Sat, 18 Jul 2026 05:37:16 +0100 Subject: [PATCH 19/21] =?UTF-8?q?ci:=20the=20four=20reds=20=E2=80=94=20fmt?= =?UTF-8?q?-proof=20boundary=20annotations,=20OsRng,=20regenerated=20decla?= =?UTF-8?q?rations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The binding-boundary marker now counts on the line above or below (rustfmt floats trailing comments off brace lines), and every grandfathered annotation sits on its own line above its import. mintChallengeNonce draws from OsRng explicitly (rand::random may delegate to thread_rng — the disallowed-methods rule). The regenerated Node declarations (agentPrefix) ride along for the docs build. Auths-Id: did:keri:EB5cPHY0t-ejNC_rUzPS1dclTvd6kG-R9mQzjozCuGgd Auths-Device: did:keri:EB5cPHY0t-ejNC_rUzPS1dclTvd6kG-R9mQzjozCuGgd Auths-Anchor-Seq: 1 --- crates/xtask/src/check_binding_boundary.rs | 10 ++++-- packages/auths-node/src/artifact.rs | 13 ++++---- packages/auths-node/src/attestation_query.rs | 9 ++++-- packages/auths-node/src/commit_sign.rs | 6 ++-- packages/auths-node/src/device.rs | 13 ++++---- packages/auths-node/src/helpers.rs | 11 +++---- packages/auths-node/src/identity.rs | 19 +++++------ packages/auths-node/src/org.rs | 34 ++++++++++++-------- packages/auths-node/src/pairing.rs | 9 ++++-- packages/auths-node/src/rp.rs | 4 ++- packages/auths-node/src/sign.rs | 6 ++-- packages/auths-node/src/trust.rs | 9 ++++-- packages/auths-node/src/witness.rs | 12 ++++--- 13 files changed, 91 insertions(+), 64 deletions(-) diff --git a/crates/xtask/src/check_binding_boundary.rs b/crates/xtask/src/check_binding_boundary.rs index 0d46cc8d..7ebacb14 100644 --- a/crates/xtask/src/check_binding_boundary.rs +++ b/crates/xtask/src/check_binding_boundary.rs @@ -42,10 +42,16 @@ pub fn run(workspace_root: PathBuf) -> anyhow::Result<()> { scanned += 1; let content = std::fs::read_to_string(&path).with_context(|| format!("read {}", path.display()))?; - for (idx, line) in content.lines().enumerate() { + 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)); - if is_deep && !line.contains(ALLOW_MARKER) { + // 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) diff --git a/packages/auths-node/src/artifact.rs b/packages/auths-node/src/artifact.rs index fe89d37f..c33c12a5 100644 --- a/packages/auths-node/src/artifact.rs +++ b/packages/auths-node/src/artifact.rs @@ -2,8 +2,10 @@ use std::io::Read; use std::path::PathBuf; use std::sync::Arc; -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}; // binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow +// 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; use auths_sdk::ports::artifact::{ArtifactDigest, ArtifactError, ArtifactMetadata, ArtifactSource}; @@ -11,12 +13,9 @@ 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::{ - // binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow - GitRegistryBackend, - RegistryAttestationStorage, - RegistryConfig, - RegistryIdentityStorage, + GitRegistryBackend, RegistryAttestationStorage, RegistryConfig, RegistryIdentityStorage, }; use auths_verifier::clock::SystemClock; use chrono::Utc; diff --git a/packages/auths-node/src/attestation_query.rs b/packages/auths-node/src/attestation_query.rs index 97f27e56..8177eb61 100644 --- a/packages/auths-node/src/attestation_query.rs +++ b/packages/auths-node/src/attestation_query.rs @@ -1,9 +1,12 @@ use std::path::PathBuf; use std::sync::Arc; -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}; // binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow +// 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; use napi_derive::napi; diff --git a/packages/auths-node/src/commit_sign.rs b/packages/auths-node/src/commit_sign.rs index 13130bbc..dbba9b0c 100644 --- a/packages/auths-node/src/commit_sign.rs +++ b/packages/auths-node/src/commit_sign.rs @@ -1,8 +1,10 @@ use std::path::PathBuf; use std::sync::Arc; -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; // binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow +// 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 a461c070..604a1477 100644 --- a/packages/auths-node/src/device.rs +++ b/packages/auths-node/src/device.rs @@ -1,18 +1,17 @@ use std::path::PathBuf; use std::sync::Arc; -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; // binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow +// 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::{ - // binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow - GitRegistryBackend, - RegistryAttestationStorage, - RegistryConfig, - RegistryIdentityStorage, + GitRegistryBackend, RegistryAttestationStorage, RegistryConfig, RegistryIdentityStorage, }; use auths_verifier::clock::SystemClock; use auths_verifier::core::Capability; diff --git a/packages/auths-node/src/helpers.rs b/packages/auths-node/src/helpers.rs index 7eee1d0f..1e6d6b85 100644 --- a/packages/auths-node/src/helpers.rs +++ b/packages/auths-node/src/helpers.rs @@ -1,13 +1,10 @@ use std::path::PathBuf; -use auths_core::config::{EnvironmentConfig, KeychainConfig}; // binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow +// 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::{ - // binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow - IdentityDID, - KeyAlias, - KeyRole, - KeyStorage, - get_platform_keychain_with_config, + IdentityDID, KeyAlias, KeyRole, KeyStorage, get_platform_keychain_with_config, }; use crate::error::format_error; diff --git a/packages/auths-node/src/identity.rs b/packages/auths-node/src/identity.rs index a1014225..175cb89d 100644 --- a/packages/auths-node/src/identity.rs +++ b/packages/auths-node/src/identity.rs @@ -1,20 +1,21 @@ use std::path::PathBuf; use std::sync::Arc; -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; // binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow +// 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::{ - // binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow - GitRegistryBackend, - RegistryAttestationStorage, - RegistryConfig, - RegistryIdentityStorage, + GitRegistryBackend, RegistryAttestationStorage, RegistryConfig, RegistryIdentityStorage, }; use auths_verifier::clock::SystemClock; use auths_verifier::core::Capability; diff --git a/packages/auths-node/src/org.rs b/packages/auths-node/src/org.rs index 59c82407..4fd9550d 100644 --- a/packages/auths-node/src/org.rs +++ b/packages/auths-node/src/org.rs @@ -1,23 +1,29 @@ use std::path::PathBuf; use std::sync::Arc; -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; // binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow +// 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::{ - // binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow - GitRegistryBackend, - RegistryAttestationStorage, - RegistryConfig, - RegistryIdentityStorage, + GitRegistryBackend, RegistryAttestationStorage, RegistryConfig, RegistryIdentityStorage, }; use auths_verifier::clock::SystemClock; use auths_verifier::core::Role; diff --git a/packages/auths-node/src/pairing.rs b/packages/auths-node/src/pairing.rs index 1b239790..c2667d22 100644 --- a/packages/auths-node/src/pairing.rs +++ b/packages/auths-node/src/pairing.rs @@ -4,14 +4,17 @@ use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::sync::Arc; use std::time::Duration; -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; // binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow +// 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}; -use auths_storage::git::RegistryIdentityStorage; // binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow +// 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/rp.rs b/packages/auths-node/src/rp.rs index 72281d62..35a99d7f 100644 --- a/packages/auths-node/src/rp.rs +++ b/packages/auths-node/src/rp.rs @@ -77,7 +77,9 @@ fn denied(code: &str, detail: Option) -> NapiAgentAuthReport { /// ``` #[napi] pub fn mint_challenge_nonce() -> String { - let bytes: [u8; 32] = rand::random(); + use rand::RngCore as _; + let mut bytes = [0u8; 32]; + rand::rngs::OsRng.fill_bytes(&mut bytes); Nonce::from_bytes(bytes).to_b64url() } diff --git a/packages/auths-node/src/sign.rs b/packages/auths-node/src/sign.rs index 2f37de19..2bee2307 100644 --- a/packages/auths-node/src/sign.rs +++ b/packages/auths-node/src/sign.rs @@ -1,5 +1,7 @@ -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}; // binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow +// 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; use auths_verifier::types::IdentityDID; diff --git a/packages/auths-node/src/trust.rs b/packages/auths-node/src/trust.rs index ad20e936..6141c1d3 100644 --- a/packages/auths-node/src/trust.rs +++ b/packages/auths-node/src/trust.rs @@ -1,9 +1,12 @@ use std::path::PathBuf; use std::sync::Arc; -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}; // binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow +// 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/witness.rs b/packages/auths-node/src/witness.rs index 0b56fff4..8c36818a 100644 --- a/packages/auths-node/src/witness.rs +++ b/packages/auths-node/src/witness.rs @@ -1,9 +1,13 @@ use std::path::PathBuf; -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; // binding-boundary-allow: pre-lint reach; migrate to an auths_sdk workflow +// 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; use crate::error::format_error; From 0ec6ce997502d851e6beecf44160c88b22cb0fb5 Mon Sep 17 00:00:00 2001 From: bordumb Date: Sat, 18 Jul 2026 05:39:31 +0100 Subject: [PATCH 20/21] style: rustfmt pass on the boundary check Auths-Id: did:keri:EB5cPHY0t-ejNC_rUzPS1dclTvd6kG-R9mQzjozCuGgd Auths-Device: did:keri:EB5cPHY0t-ejNC_rUzPS1dclTvd6kG-R9mQzjozCuGgd Auths-Anchor-Seq: 1 --- crates/xtask/src/check_binding_boundary.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/xtask/src/check_binding_boundary.rs b/crates/xtask/src/check_binding_boundary.rs index 7ebacb14..864c1e60 100644 --- a/crates/xtask/src/check_binding_boundary.rs +++ b/crates/xtask/src/check_binding_boundary.rs @@ -49,7 +49,9 @@ pub fn run(workspace_root: PathBuf) -> anyhow::Result<()> { // 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)) + || 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!( From 3762b7f3b7f4e97f4cc21b951caf9e14563bcfa7 Mon Sep 17 00:00:00 2001 From: bordumb Date: Sat, 18 Jul 2026 05:49:11 +0100 Subject: [PATCH 21/21] sdk(node): the hand-maintained native shadow catches up with the KERI delegateAgent lib/native.ts redeclares the napi surface for the typed wrapper; its NapiDelegatedAgentBundle still had the attestation-era shape, so typedoc failed on agentPrefix. Shadow now matches the generated declarations (agentPrefix in, attestationJson out, expiry in seconds). typedoc: 0 errors. Auths-Id: did:keri:EB5cPHY0t-ejNC_rUzPS1dclTvd6kG-R9mQzjozCuGgd Auths-Device: did:keri:EB5cPHY0t-ejNC_rUzPS1dclTvd6kG-R9mQzjozCuGgd Auths-Anchor-Seq: 1 --- docs/sdk/node/api/index.md | 1872 +++++++++++++++++++++++------ packages/auths-node/lib/native.ts | 6 +- 2 files changed, 1482 insertions(+), 396 deletions(-) diff --git a/docs/sdk/node/api/index.md b/docs/sdk/node/api/index.md index a0b71395..b8cc9358 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/lib/native.ts b/packages/auths-node/lib/native.ts index 4d707d0e..7ac5cdc5 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