Anchor device attestations as ixn seals so the KEL becomes the authoritative device-event log¶
Problem¶
Auths' KEL today carries only icp (inception) and rot (rotation) events. Device link/unlink — the lifecycle that authorizes which devices can sign on behalf of a controller — happens entirely outside the KEL: attestation JSON blobs are written to refs/auths/registry under v1/devices/<shard>/did_key_<subject>/attestation.json, with a parallel history/<ts>_.auths.json audit trail. Linking emits a Git commit; unlinking emits another.
This works as a Git-native audit trail but means:
- The KEL is not the authoritative ordering of device events. A consumer who walks just the KEL has no record of which devices were authorized when.
- Witnesses don't receipt device events. Witness servers receipt
icp/rot/ixn. Since auths never emitsixn, witnesses have no evidence of any device change. Compromise + retroactive device-link tampering would be invisible to a witness-quorum verifier. - No cryptographic chain on device events. Git commit lineage gives ordering but not the same SAID-chain integrity that KEL events have.
The KERI-spec-aligned answer: emit an ixn event on every device link/unlink, with an a[] seal containing the SAID of the canonicalized attestation. The attestation blob continues to live in Git (it's the off-event payload the seal commits to) but the authoritative link/unlink record becomes the sealed ixn event in the KEL.
This is the ixn-anchoring conversation raised by @rank5.syzygy in our KERI-team comparison: in his (kels) model, every recovery / link / unlink is an event in the chain, not a side-channel artifact. Auths should match that property — without changing its Git-native storage philosophy.
Current state¶
What auths has¶
icpandrotevents: emitted, validated, witness-receiptable. Seebash launch-repo/checks/cli/check_e2e_identity_lifecycle.shfor a working end-to-end example.ixnevent type: defined and validated incrates/auths-keri/src/events.rsandcrates/auths-keri/src/validate.rs:215-218— but never emitted by any caller. The validator handlesEvent::Ixn(ixn)correctly; production code just doesn't construct one.- Device attestations: stored as JSON blobs at
refs/auths/registry. Reference example produced byauths init:
{
"version": 1,
"rid": ".auths",
"issuer": "did:keri:E…",
"subject": "did:key:zDna…",
"device_public_key": { "curve": "p256", "key": "03…" },
"identity_signature": "…",
"device_signature": "…",
"timestamp": "2026-04-14T11:23:48.546747Z",
"note": "Linked by auths-sdk setup"
}
Sealtype: defined incrates/auths-keri/src/events.rs(search forpub struct Seal/digest_value());validate.rsalready searches for seals viafind_seal_in_kel(events: &[Event], digest: &str)(validate.rs:764-782).- Attestation storage code path:
crates/auths-storage/src/git/adapter.rs:89(REGISTRY_REF = "refs/auths/registry").
What's missing¶
- An
ixnevent emitter for device link/unlink. - A canonical-attestation hash function whose output goes into
seal.d. - Wiring in
auths-id::device::link/auths-id::device::revoke(or wherever those flows live inauths-sdk::workflows::device::*) so each link/unlink builds and writes anixnto the KEL alongside the existing Git ref blob write. - Witness-receipt path for
ixn(assuming witness server is wired into the flow at all — see "open question" below).
Required changes¶
Phase 1 — emit ixn on device link¶
- Add
auths_id::keri::interaction::emit_link_event(controller_signer, attestation: &DeviceAttestation, kel: &mut GitKel) -> Result<IxnEvent, …>. Implementation: - Canonicalize the attestation JSON (use
json-canon, same as today) - Compute Blake3-256 SAID of the canonical bytes
- Build
IxnEvent { v, t: "ixn", d: <SAID-of-this-event>, i: controller_aid, s: next_seq, p: prev_event_d, a: vec![Seal::Digest { d: <attestation_SAID> }] }(use whicheverSealvariant inauths-keri::events::Sealrepresents a digest seal — confirm by readingseal.digest_value()) - Run
finalize_ixn_event(crates/auths-keri/src/validate.rs:747-762) to compute the event's own SAID + version-string size - Sign with the controller's current key (Phase 1 of the spec-compliance issue; if that hasn't landed, fall back to in-body
xfor now and migrate later) - Append to KEL via
GitKel::append_event(or whatever theauths-infra-git::GitEventLog::appendequivalent is) - Wire this into
auths-sdk::workflows::device::link— every successful device authorization writes the attestation blob AND emits theixn. Both writes should land in the same Git commit if possible (samerefs/auths/registryref); if not, emit theixnfirst so a partial failure leaves a sealed-but-missing-payload state (recoverable: rewrite the payload), not the inverse (orphaned attestation a verifier can't tie to anything). - Update
auths device linkCLI to surface the newixnSAID in--jsonoutput for scriptability.
Phase 2 — emit ixn on device unlink / revoke¶
- Same shape, different intent. The
ixnfor unlinking commits to a "revocation attestation" — same canonical attestation shape with an additional field (e.g.revoked: true,revocation_timestamp: …, signed by the controller). Hash that revocation attestation, put its SAID in theixn'sa[]. - Wire into
auths-sdk::workflows::device::revoke.
Phase 3 — KEL-walk replaces Git-ref-walk for device queries¶
- Add
auths-id::device::resolve_authorized_devices(controller_aid, at_seq: Option<u64>) -> Vec<DeviceAuthorization>. Implementation: - Load the controller's KEL
- Walk events; collect every
ixn's seals - Resolve each seal's SAID to its on-disk attestation blob
- Apply link/unlink semantics in seal order (later unlink overrides earlier link)
- Filter to the snapshot at
at_seqif specified - Switch
auths device listand the device-resolution path used during signature verification to call this function instead of doing a direct Git-ref scan. Old Git-ref scan can stay as a fallback / cross-check during a deprecation window.
Phase 4 — witness receipts cover device events¶
- Confirm
auths-core::witness::serveracceptsixnevents (it should — the validator does). If not, extend. - Update the
WitnessConfigflow so device link/unlink, when witnesses are configured, sends the newixnto witnesses for receipt — same path asicp/rotalready use. - Add a verification rule that, given a controller config requiring
bt > 0for device events, rejects device authorizations whoseixnlacks the witness threshold's worth of receipts.
Phase 5 — backfill & migration¶
- One-off migration tool: scan existing
refs/auths/registrydevice blobs, compute their SAIDs, emit oneixnper blob in chronological order (use the existinghistory/<ts>_.auths.jsontimestamps to order). This brings the KEL into sync with already-linked devices. - Document that pre-migration attestations have no KEL anchor and require operator action to migrate.
Acceptance criteria¶
bash launch-repo/checks/cli/check_e2e_identity_lifecycle.shshows anixnevent in the KEL for the bootstrap device-A link (currently shows onlyicp).auths device link(Phase 3 of the existing e2e check) emits anixnwith seq = 2; KEL now has icp(0) → rot(1) → ixn(2).auths device listproduces identical output whether it walks the KEL (ixnseals) or the legacy Git-ref scan, on any pre-existing identity.cargo test -p auths-id --lib device::resolve_authorized_devicespasses with cases covering: link, link-then-revoke, link-revoke-relink, link with witness threshold unmet (rejected).- A witness-quorum-required identity rejects device-link sigs that lack receipts (integration test in
crates/auths-core/tests/cases/witness_quorum.rs).
Open questions for design review (resolve before Phase 1)¶
- One
ixnper device event, or batch? Spec allows multiple seals ina[]. Batching is cheaper but couples link/unlink ordering across devices. Recommend: one event per action for clarity, optimize later if KEL bloat is real. - Same KEL for identity + device events, or separate KELs? auths today has one KEL per controller. ACDC / TEL designs use a separate Transaction Event Log per registry. For device events, one KEL is fine because every device-event is controller-issued. If we ever support delegated device authorization (a paired admin device authorizes a third device), revisit — that's when a TEL becomes useful.
- Pre-Phase-1 spec-compliance work (sister issue) vs this issue: which lands first? Recommend sister issue lands first (
xexternalization,drt,u128). This issue depends on signed-event-attachment shape being stable. If we land this issue first, every emittedixnwill have an in-bodyxand need migrating later.
Same-shape gaps across the codebase¶
The device-link gap is one instance of a general pattern: a trust-affecting decision recorded as a Git-ref attestation but not sealed in the controller's KEL. The same architectural fix (ixn + seal) applies to every operation below. Phases 1-5 above cover device link/unlink; these are the follow-on surfaces that need the same treatment.
Org membership (high priority)¶
add_organization_member and revoke_organization_member produce attestations stored at refs/auths/registry under org/<prefix>/members/. The org's controller KEL has zero record that member X was added at time T. An attacker who compromises the Git registry can forge or backdate org memberships with no KEL evidence to contradict them.
Fix: ixn with a digest seal of the membership attestation SAID, emitted by the org controller's KEL. Same pattern as device-link Phases 1-2.
Files: crates/auths-sdk/src/domains/org/service.rs, crates/auths-sdk/src/workflows/org.rs.
Agent delegation (medium priority — architectural choice)¶
provision_agent_identity creates a standalone icp for the agent, then links it to the parent via an attestation. The parent's KEL doesn't record "I delegated agent X."
KERI has a purpose-built mechanism: dip (delegated inception). If agents were provisioned via dip instead of standalone icp + side-channel attestation, the parent's KEL would natively contain the delegation event. This is architecturally cleaner than ixn sealing — the delegation relationship lives in the event type itself, not in a seal's payload.
Decision: use dip/drt for agent provisioning (KERI-native), or ixn + seal (consistent with device-link pattern). Recommend dip/drt for agents since the delegation is a first-class KERI concept.
Files: crates/auths-id/src/agent_identity.rs.
Artifact/release signing (medium priority)¶
sign_artifact_bytes_raw and sign_action_raw produce signed attestations stored in Git (and optionally submitted to the transparency log via Rekor). The signer's KEL has no record of "I signed artifact X at time T."
For supply-chain security, an ixn anchoring each signing event would let anyone replay the signer's KEL and see every artifact they ever attested to — a tamper-evident signing log baked into the identity itself. This is particularly valuable for CI identities where the signing history IS the audit trail.
Files: crates/auths-sdk/src/domains/signing/service.rs.
Capability lifecycle (low priority)¶
When a device's capabilities are narrowed (removing sign:release) or extended (adding manage:members), a new attestation replaces the old one. There's no KEL record of the capability change. An attacker could forge an attestation granting capabilities the controller never intended.
This is lower priority because capability changes are a subset of device re-attestation — if device-link ixn seals land (Phases 1-2), capability changes are implicitly covered since they produce new attestations that would also be sealed.
Emergency freeze (speculative)¶
There's no ixn event type for "I'm temporarily suspending all operations on this identity." KERI doesn't define one natively, but an ixn with a well-known seal type (e.g., {"t": "freeze"}) would let a controller broadcast "stop trusting my signatures until further notice" in a way witnesses can receipt and verifiers can detect.
Currently the only way to "freeze" is to rotate to a null next-key commitment (abandon), which is permanent. A reversible freeze via ixn would be useful for incident response — "my laptop was stolen, freeze everything, I'll rotate keys from my phone once I verify."
Cross-identity attestation chains (low priority, high value)¶
When identity A attests to identity B's device (e.g., an org admin authorizing a member's device), that attestation is in A's registry but not anchored in A's KEL. If A is compromised and the attacker forges attestations for B's devices, A's KEL is silent — the forgeries look identical to legitimate attestations.
Anchoring cross-identity attestations in the issuer's KEL would let B verify "did A's KEL actually record this authorization at the claimed sequence?"
Transparency log submission as a KEL-visible fact (speculative)¶
When an attestation is submitted to Rekor, the inclusion proof comes back but isn't recorded in the signer's KEL. An ixn sealing "I submitted attestation X to log Y, got inclusion proof Z" would make the transparency-log submission itself part of the tamper-evident record. This closes the gap between "I signed something" and "I provably published that I signed it."
The general principle¶
If a controller makes a decision that affects trust — authorize, revoke, delegate, sign, freeze — and that decision isn't an ixn seal (or dip/drt for delegation) in the KEL, then the KEL is an incomplete history of the controller's actions. The phases above fix the most critical instance (device link/unlink). The gaps listed here are the same pattern applied to every trust-affecting operation auths supports, ordered by priority.
Out of scope¶
- TEL (Transaction Event Log) registries. A more general mechanism than
ixnseals; the right answer if we ever ship credential issuance (ACDC). Not needed for device-link anchoring. - ACDC migration of device attestations. Separate question — would change the payload shape, not the anchoring mechanism. Can land independently.
- Multi-key or weighted-threshold inception. Different epic (kels-comparison work).
References¶
- KERI IETF draft, §6 (
ixnevents) and §7 (anchored seals): https://datatracker.ietf.org/doc/draft-ssmith-keri/ - Existing
Ixnvalidation:crates/auths-keri/src/validate.rs:215-218 - Existing seal lookup:
crates/auths-keri/src/validate.rs:764-782(find_seal_in_kel) - Existing finalizer:
crates/auths-keri/src/validate.rs:747-762(finalize_ixn_event) - Current device-attestation storage:
crates/auths-storage/src/git/adapter.rs:89 - Working device-attestation reference:
bash launch-repo/checks/cli/check_e2e_identity_lifecycle.shand the JSON inlaunch-repo/collab_bordumb_rotation.md - KERI-team conversation context:
launch-repo/collab_bordumb_rotation.mdand the rank5.syzygysign_v1/rec/3example - Sister issue (must land first):
keri-spec-compliance-roundtrip.md