ADR 008 — Credential-grade capabilities via ACDC + TEL (Epic F)¶
Status: Accepted Context: Epic F ("ACDC + TEL credentials"). Capabilities and roles become verifiable credentials — ACDC (Authentic Chained Data Containers) — with KERI-native per-credential revocation — TEL (Transaction Event Log) — anchored to the issuer's KEL. This is the credential-grade upgrade to Epic E's advisory, delegator-anchored scope seal: per-capability revocation (revoke one credential without rotating keys or revoking the identity), third-party verifiability, and a holder-bound presentation model. It is not full vLEI/IPEX interop.
Context¶
Epic F is optional to the core thesis — device-bound commit signing, provable by KEL
replay, needs none of it. It was nonetheless built first-class, not as a deferred
afterthought: when we ship credential-grade authorization it must be robust —
minimal trust surface, maximal trust guarantees. The non-negotiable v1 properties:
credentials are holder-bound, never bearer; revocation is witnessed
(transitively, via the KEL anchor) and freshness-checked, never
trust-on-first-sight-silently; both curves (P-256 default + Ed25519) are exercised
from day one; and the SAID layout is forward-compatible for e so edges are additive.
We defer features (full IPEX, edge/rule content, OIDC, escrow). We do not defer
security properties.
Exhaustive search at the start of Epic F found no ACDC/TEL/credential/registry code —
only forward-reference comments — but the exact KERI primitives needed already existed
and were keripy-1.3.4 byte-aligned (SAID-ification, CESR encoders, TEL→KEL anchoring as
a Seal::KeyEvent in an ixn, signing-time key recovery, KEL-position revocation
ordering, the policy-context bridge template). Epic F built the credential layer on
that substrate.
The decisions below were surfaced during gap analysis and are recorded here as the load-bearing scoping and authority choices.
Decisions¶
-
D1 — ACDC is ADDITIVE, not a commit-time replacement. Epic E's
agentscope:seal +Auths-Scopetrailer remain the commit-time signing fast path. ACDC subsumes the attestation authority fields (capabilities/role) — the migration target the roadmap names. Commit verification is not rewritten in v1; anAuths-Credentialtrailer that makes ACDC the commit-time authority is the deferred integration point (see Deferrals). This keeps blast radius sane and matches "not required for the core thesis." -
D2 — Backerless (
NB) registry, with witness-enforced revocation. Event typesvcp(registry inception, once per issuer),iss(issuance),rev(revocation). Neverbis/brv/vrt/TEL backers — a parallel trust system is the wrong mechanism. Robustness comes from witnessing the KEL anchor, not from TEL backers: everyvcp/iss/revis anchored by aSeal::KeyEventin the issuer's KELixn, which post-Epic-D is witness-receipted. The F.9 pre-flight finding (proven by test) is that the Epic-D KEL gate quorum-gates establishment events only — "ixn never gates" — so the verifier (F.5) itself quorum-checks the lifecycle anchoring ixns (vcp/iss/rev) via the KAWAWitnessAgreementalgorithm (Option A). See the composed witness claim below. -
D3 — Holder-bound minimal ACDC
{v,d,i,ri,s,a}(NOT a bearer token). The subjecta.iis a KERI AID (D5 guarantees the issuee has a KEL); authority is honored only when the presenter proves current control of that AID. Possession of the ACDC alone grants nothing — this avoids the bearer-token red flag. For commit signing this is automatic (the signer's KEL). For third-party presentation, v1 requires a presentation signature by the subject's signing-time key over(credential-SAID, audience, nonce)(F.8) — a thin precursor to IPEX, not full IPEX. One pinned JSON-Schema-2020-12 capability schema; the schema SAID is embedded and immutable. -
D4 — Fail-closed + revocation-freshness (v1). A TEL event whose KEL anchor is not present locally is rejected, never accepted (no escrow of out-of-order events). Plus: freshness is a first-class verdict — owned by the SDK resolution layer, not the pure verifier. The pure verifier (F.5) cannot resolve a KEL tip or judge staleness (no network, no clock of its own); it checks what it is handed and reports the "as-of" position of that input plus the witness-quorum status. The SDK
credentials::verify(F.4) resolves the issuer KEL/TEL/receipts to the witnessed tip, then judges freshness: "not revoked" means norevanchored at or before the resolved tip, and an unresolvable/stale tip yieldsStaleOrUnresolvable(fail-closed), never a silent valid. -
D5 — Separate issuance step. Delegate first (
agents::add/org::add_member→ issuee gets a KEL), THENcredentials::issue(issuer, issuee_did, caps, …); hard-fail if the issuee prefix has no KEL. Thevcpregistry is lazily incepted on first issuance per issuer. -
D6 — Revocation is per-CREDENTIAL (TEL
rev), distinct from the coarseagents::revoke(whole delegate). This per-capability granularity is what ACDC buys. -
D7 — SAID protocol tag parameterized.
compute_saidpreviously hardcodedKERI10JSON; F.1 parameterized the version/protocol tag (aProtocolenum) so ACDC emitsACDC10JSON…for keripy interop. The 17-char version-string layout and all KEL SAIDs are unchanged. -
D8 — Dual-curve is a v1 acceptance gate, not a follow-up. The agents suite was once Ed25519-only and hid a P-256 break for an entire epic. Epic F does not repeat that: every credential/TEL signature and every embedded key is curve-tagged in-band (CESR prefix / multicodec / explicit field — never dispatch on byte length), and the keripy fixtures and unit tests run issue → verify → revoke for both P-256 (default) and Ed25519.
check-curve-agnosticstays at 0 violations.
RegistryBackend freeze-touch resolution (F.3)¶
TEL storage required touching the frozen RegistryBackend trait. The resolution was
to extend it — append_tel_event / visit_tel_events / store_credential, plus an
AtomicWriteOp::AppendTelEvent and a refs/.../tel/<reg>/<cred>/<sn> layout. The
documented atomicity justification: the ACDC blob, the TEL event, and the KEL anchoring
ixn must land in one commit, mirroring the existing attestation write-batch
exception. anchor_tel_event reuses the staged single-author ixn path
(author_root_anchor_ixn was refactored into a stage_root_anchor_ixn), and
ensure_registry lazily incepts an idempotent backerless vcp per issuer. A kt≥2
issuer is rejected with a typed error (single-author anchoring only — same limit as Epic
E org delegation).
agentscope: seal vs ACDC capability precedence (F.6)¶
There are two on-chain encodings of a capability/role grant, serving different decision
grades, reconciled by a documented CapsSource precedence rule
(auths-id/src/policy/mod.rs):
agentscope:scope seal — the Epic-ESeal::Digestanchored in the delegator'sixn. It is commit-time advisory: the offline fast path a verifier reads straight off the KEL without a live presentation. It is the low-latency convenience source, not an authority of record.- ACDC credential — the authoritative caps/role source for credential-grade
decisions. Authority derived from it is honored only through a holder-verified
presentation (F.8) at the policy seam (
context_from_credential).
Anti-divergence rule: the same grant MUST NOT be authored into both encodings with
diverging caps/role. When both exist for one grant, CapsSource::governing selects the
ACDC — the credential governs the credential-grade decision; the agentscope: seal
remains valid only as the advisory commit-time fast path.
The composed witness claim (F.9) — canonical assurance¶
This is the precise, honest statement of what Valid means under each witness policy.
It is quoted here verbatim as the canonical assurance:
Under
RequireWitnesses, a credential isValidonly if (a) the issuer's KEL establishment events reached quorum [Epic D], (b) itsvcpandissanchoring ixns reached quorum [F.5/A], and (c) no quorum-reachingrevanchor exists at/before the presentation's KEL position. UnderWarn(default), under-quorum is a warning (TOFS) anddetect_duplicitystill catches revocation-hiding-via-fork.
What shipped (NOT deferred)¶
These v1 properties shipped in Epic F and must not be mistaken for future work:
- Holder-binding (F.8) — credential-derived authority is honored only on proof of
current control of the subject AID by KEL replay plus a fresh presentation signature
over
(credential-SAID, audience, nonce). The interactive challenge-response path (verifier-issued single-use nonce) is the v1 default; a non-interactive(audience, purpose, short-TTL)path exists with a documented within-TTL residual.PresentationVerdictdistinguishesHolderNotCurrentKey/WrongAudience/NonceMismatchOrConsumed/Expired/SubjectKelInvalid/CredentialNotValid. - Lifecycle witness-quorum (F.5/F.9) — the verifier enforces witness quorum over the
vcp/iss/revanchoring ixns (not just establishment events) via KAWA, per the composed claim above.WitnessQuorumNotMetnames which lifecycle anchor missed. - Revocation freshness (F.4) — the SDK resolves to the witnessed tip and owns the
StaleOrUnresolvablefail-closed verdict; absence-of-revis never silently treated as valid.
Threat model¶
Each attack is paired with its mitigation. The single residual is stated honestly.
- Revocation-hiding / equivocation → the
revevent is KEL-anchored; hiding it requires forking the issuer's KEL. The anchoring ixns are witnessed under the F.5/F.9 lifecycle witness-quorum (composed claim, depends on Epic D), anddetect_duplicityflags the fork in both witness modes. - Credential theft / replay → holder-binding (F.8): authority needs proof of current subject-key control via challenge-response (verifier-issued single-use nonce), not mere possession. A stolen ACDC blob grants nothing without the subject's current signing key.
- Issuer key compromise + rotation → the issuer signs with its signing-time key;
verification recovers the key in force at the
issanchor position, so aissforged with a post-rotation key does not verify. Recovery = rotate the issuer key andrevthe affected credentials (or use thevcp-level kill-switch). Credentials issued before the compromise remain valid until explicitly revoked. - Registry / TEL fork →
detect_duplicityon the issuer KEL/TEL surfaces the divergence; the no-witness stance is first-valid-seen + refuse (verdictIssuerKelDuplicitous), never silent acceptance of a forked branch. - Downgrade / staleness → the SDK freshness verdict
StaleOrUnresolvable(F.4) plus the witness policy: an unresolvable/stale tip fails closed; the verifier never treats absence-of-revagainst a stale view as "not revoked." - Revocation latency under
RequireWitnesses(residual — stated honestly) → arevonly revokes once it reaches quorum (claim (c) above), so the window between authoring arevand its receipts reaching quorum leaves the credentialValidto aRequireWitnessesverifier. The hiding of thatrevis still fork-detectable viadetect_duplicity, and underWarn(default) a seenrevrevokes immediately. This is the deliberate trade-off of witnessed fail-closed revocation; it is not a silent gap. - Schema substitution → the capability schema SAID is pinned and embedded; an
unknown or substituted schema SAID is rejected (
SchemaInvalid).
Epic-D dependency¶
The lifecycle witness-quorum (the witnessing of the vcp/iss/rev anchoring ixns, per
the composed claim) requires Epic D landed for its fail-closed mode. Until then, the
default Warn mode is honest trust-on-first-sight — the same caveat as the commit
path — with under-quorum surfaced as a non-fatal warning and detect_duplicity still
catching revocation-hiding-via-fork.
Forward-compatibility honesty (no over-claim)¶
The most-compact SAID makes a future top-level e (edges) block additive: a v1
credential SAID is unchanged for present fields, so edges can be layered without a
breaking change. Selective / graduated disclosure (u/A) is NOT additive — u
lives inside the attributes block and changes a.d and d, so selective disclosure is
a SAID-breaking v2 (a new schema/version). We claim forward-compatibility only for e,
never for SD.
Consequences (assurance, stated precisely)¶
- A capability is a first-class, holder-bound, KEL-anchored credential: a third party verifies it purely by replay (SAID + embedded schema + issuer signing-time key + TEL status by KEL position + witness-quorum) and honors its authority only against a holder-verified presentation. No bearer token anywhere on the path.
- Per-credential revocation is independent of key rotation and identity revocation.
- Attestation-borne
capabilities+roleare no longer the authority of record — every reader was migrated off them (F.10) and the write path that stamped them was removed (F.11). OIDC +delegated_byremain on the attestation (deferred). - Under
RequireWitnessesthe assurance is exactly the composed claim above, with the documented revocation-latency residual.
Deferrals (tracked)¶
Each item below is genuinely out of Epic F v1 scope and has a tracking GitHub issue on
auths-dev/auths (each back-referencing this ADR). Holder-binding, lifecycle
witness-quorum, and freshness are NOT deferred — they shipped (see "What shipped").
- Backed registries (
bis/brv/vrt/TEL backers) — #221. v1 is backerlessNB; trust derives from the witnessed KEL anchor, not TEL backers. - ACDC edge (
e) + rule (r) content — #222. The SAID stays forward-compatible for a top-levele, so edges are additive; the content (chaining semantics, rule sections) is deferred. - Selective / graduated disclosure (
u/A) content — #223. This is a SAID-breaking v2 (new schema/version), not additive — see "Forward-compatibility honesty." - Full IPEX grant/admit choreography —
#224. The v1 presentation signature
(holder-binding over
(cred-SAID, audience, nonce)) shipped in F.8; the full grant/admit protocol is deferred. - TEL escrow of out-of-order events — #225. v1 rejects an unanchored TEL event rather than escrowing it.
Auths-Credentialcommit trailer (ACDC as commit-time authority) — #226. The two-layer seam (D1) is documented; making ACDC the commit-time authority is the deferred integration point.- OIDC → ACDC migration —
#227. F.10/F.11 migrated
capabilities roleonly; OIDC binding stays on the attestation for v1.- Dynamic /
oneOfschema registry — #228. v1 pins one embedded JSON-Schema-2020-12 capability schema. delegated_by→ ACDC edge — #229.delegated_bystays on the attestation; modeling delegation provenance as an ACDC edge depends on (2).
Already filed from F.10:
- ACDC-sourced capability gate for artifact/device verification —
#220. F.10 removed the legacy
attestation-borne capability gates on
auths artifact verifyandauths device verify-attestation; re-introducing a capability gate sourced from a holder-verified credential needs an issuer flow.
Archived auths-cloud crates — reuse assessment¶
When the deferred server/integration items above are scheduled, four archived crates under
_archived/auths-cloud/crates/ were assessed for reuse. Cross-cutting finding: each splits
cleanly into a standards-driven transport/protocol half (reusable, stable) and a domain half
built on the pre-Epic-E/F model — attestation capabilities/role, bearer/session tokens, the
removed add_organization_member path — which conflicts with the current KERI/ACDC/KEL-signature
direction and must be rewritten. Reuse the protocol layers; rewrite the domain layers.
| Archived crate | Verdict | Salvage | Tracking issue |
|---|---|---|---|
auths-oidc-bridge |
Partial reuse — high value | The OIDC verification half (resilient JWKS client w/ circuit-breaker + stale fallback, GitHub-OIDC claim verification, RFC 8693 delegation math) drops into the empty [auths-oidc-port] trait impls. The minting half issues Bearer JWTs — rewrite to mint ACDC instead. |
#227 (and vision #119) |
auths-scim-server |
Reuse protocol crate, rewrite server | Port the auths-scim crate as-is (RFC 7643/7644 types, filter parser, PATCH ops, discovery endpoints, SCIM errors — already separated from business logic). Rewrite /Users handlers off the agent-stored capabilities column onto the KEL-native org::delegation (add_member/revoke_member), add an /Orgs/{id}/Members route, replace bearer auth with KEL signatures. |
#215 |
auths-witness |
Do not reuse — build fresh | It is a C2SP transparency-log checkpoint cosigner, not a KERI event-receipt witness — zero protocol overlap with WitnessAgreement/StoredReceipt/rct. The repo already has the witness primitives + an HTTP client expecting /witness/{prefix}/event; a fresh ~500-line server against those is cheaper than retrofitting. |
#221, #202 |
auths-registry-server (~18.5k LOC) |
Rewrite, salvage HTTP scaffold | Reusable plumbing only: error→RFC 9457 mapping, middleware/rate-limit/CORS, pairing-store ports, multi-tenant resolver, Stripe billing glue. Rewrite all org/device/verify/artifact routes to call the SDK credential domain (they embed business logic + the old attestation model); auth → KEL signatures; the 2.5k-LOC sequencer belongs in auths-transparency. Reference, not a drop-in. |
(no dedicated issue — credential-server work) |
References¶
docs/architecture/keri-only-roadmap.md§"Epic F"docs/getting-started/credentials.md(issue / verify / present / revoke guide)docs/getting-started/delegation.md(the Epic-E advisory scope seal this upgrades)docs/architecture/cryptography.md→ "Wire-format Curve Tagging" (D8 in-band tagging)docs/architecture/multi_device_accepted_risks.md(kt=1, no-witness baseline)- ADR 006 (witness receipting & duplicity — the Epic-D substrate the composed claim rests on)
- ADR 007 (agent identity via delegation — the advisory scope seal; same deferral convention)
- ACDC spec (ToIP): https://trustoverip.github.io/kswg-acdc-specification/
- PTEL (Public TEL) spec: https://trustoverip.github.io/tswg-ptel-specification/draft-pfeairheller-ptel.html
- Code anchors:
auths-keri/src/{acdc.rs,tel.rs,said.rs};auths-id/src/keri/credential_registry.rs,auths-id/src/policy/mod.rs(context_from_credential,CapsSource);auths-verifier/src/{credential.rs,presentation.rs};auths-sdk/src/domains/credentials/(issue/revoke/list/verify/present).