Feat/lei verification#42
Draft
jhateley-godaddy wants to merge 14 commits into
Draft
Conversation
Add the IDENTITY_* event family to the Transparency Log — the "who"
behind agents, sealed on its own read stream over the same single
Merkle tree as agent events.
Event family (internal/tl/event/identity): IDENTITY_VERIFIED,
IDENTITY_UPDATED, IDENTITY_REVOKED, IDENTITY_LINKED,
IDENTITY_UNLINKED — a closed enum mirroring the agent envelope's
JCS + RFC 6962 leaf rules. Sealed proof events quote the DID
document's verification method VERBATIM ({verificationMethod,
signedProof}); nothing derived, re-encoded, or normalized enters a
seal. Thumbprints are compute-at-read conveniences.
Ingest: POST /v1/internal/identities/event — a third codec on the
same producer-signature lane. The closed enums are the cross-lane
guard: agent bodies fail the identity codec and vice versa, and the
V1 lane stays frozen.
Storage: tl_events gains a nullable identity_id read index (streams
are read indexes over one log, never separate trees) plus a
tl_identity_event_agents fan-out table so link batches — ONE sealed
event carrying ansIds[] — join back to agents at read time.
Reads: /v1/identities/{id}{,/audit,/receipt,/agents}, agent badges
gain a computed identities[] join (link live ∧ identity stream
state; provenKeyIds name the current proven set), and
/v1/agents/{id}/identities{,/history} serve the agent-side views in
the standard audit envelope. Identity operations never write to an
agent's stream; all propagation is read-time.
Receipt cache lookups move from (agent, treeSize) to the table's
natural (leafIndex, treeSize) key so identity leaves reuse the same
COSE_Sign1 machinery.
Signed-off-by: Connor Snitker <csnitker@godaddy.com>
…, links Add the /v2/ans/identities surface: owner-level Verified Identities (the "who") proven through challenge-bound key proofs, sealed onto their own TL stream, and linked to any number of the owner's agents. The agent registration path is byte-for-byte untouched — agents carry no identity fields; the association is the links sub-resource. Proof gate (the single security invariant): the RA seals only after control is PROVEN — every submitted compact JWS must carry the served JCS IdentityProofInput verbatim as its payload (clients never canonicalize) and verify against the identifier's AUTHORITATIVE key. The nonce is single-use, consumed only inside the success transaction via a conditional UPDATE (TOCTOU guard); failed attempts never consume it, and recovery is the idempotent re-add. Kinds are pluggable behind the controlVerifier registry (identitykinds.go) — THE extension seam for did:plc, did:ion, did:ethr, and lei (vLEI). did:web and did:key ship enabled: - did:web — multi-key possession of the document's assertionMethod keys, any host. The did.json fetch is a port with two adapters (identity.resolver.type): "noop" synthesizes the document from the proofs' embedded jwk headers (quickstart — real signature verification, waived live-document binding) and "web" is the hardened fetcher: WebPKI, SSRF egress denylist with per-call IP pinning, 5s timeout, size cap, same-registrable-domain redirects. - did:key — the key IS the identifier; zero I/O. Ed25519 (z6Mk…) and P-256 (zDn…) forms. Key support matches exactly what the JWS layer verifies: EdDSA (Ed25519, raw-signing-input per RFC 8037), ES256 (P-256), RS256 (RSA ≥ 2048) — with precise rejections for X25519 (key agreement, cannot sign) and curves without a verifier. Links are a single owner-gated call with no challenge and no signature: the caller must own the identity AND every named agent. A batch seals as ONE IDENTITY_LINKED event (fleet link = O(1) events); rotation and revocation are likewise one event each, with propagation to linked badges left to the TL's read-time join. Agent detail responses gain the additive computed identities[] view. Outbox grows a third IDENTITY lane (migration 007 rebuilds the CHECK; tlclient maps it to /v1/internal/identities/event) under the same sign-once/replay-verbatim invariant. Per-owner register/rotate rate limiting bounds the outbound fetches. Signed-off-by: Connor Snitker <csnitker@godaddy.com>
scripts/demo/identity-lifecycle.sh exercises EVERY identity operation against the live stack: register → idempotent re-add → multi-key verify-control (one ES256 + one EdDSA proof over the same nonce, both verification methods sealed verbatim) → an Ed25519 did:key registered, proven, and linked alongside the did:web (one agent carrying two identities) → a two-agent batch link sealing ONE IDENTITY_LINKED with ansIds[2] → RA + TL computed views in both directions → rotation (proven set 2→1, visible on every linked badge from one sealed event) → identity SCITT receipt → per-pair unlink (agent keeps its other identity) → revocation propagating to the still-linked badge at read time while agents stay ACTIVE. run-lifecycle.sh gains steps 16-19: register the who, prove control, link the agent, and read the badge's identities[] join — one hop answers "who is behind this agent". scripts/demo/signproof is the registrant-side tool (keys minted and proofs signed locally; private keys never touch the RA): keygen emits did:key identifiers for P-256 or Ed25519 keypairs, sign emits the compact JWS with kid + embedded jwk headers, alg following the key type. The coverage gate now excludes scripts/* alongside cmd/* — demo tooling is exercised by the end-to-end lifecycle runs, not unit tests. Signed-off-by: Connor Snitker <csnitker@godaddy.com>
The repo-adapted implementation plan derived from the DESIGN-multi-identity-anchors rev-4 design of record, including the as-built deviations: the single-table TL read index, the multi-algorithm key allowlist, verbatim verification-method sealing, and the noop/web resolver split. Signed-off-by: Connor Snitker <csnitker@godaddy.com>
9859d77 to
3375835
Compare
Implements the 2026-06-11 design-review deltas on the RA side (design §5.6.1 item 6, §4.3, §3.2, §3.6): - Identity operations (verify-control, revoke, link, unlink) now seal SYNCHRONOUSLY and report success only after the TL acknowledges the seal; the row transition commits with the acknowledgment, so the RA row can never be ahead of the log. The IDENTITY lane leaves the outbox entirely: sign once, submit once — a failed delivery IS a failed operation (503 TL_UNAVAILABLE, retryable, nothing consumed). A nil/disabled TL client fails identity sealing closed. - A short-TTL provisional nonce claim (migration 008) serializes concurrent verify attempts across the seal round trip (VERIFICATION_IN_FLIGHT to the loser); failed attempts release it, preserving the failed-attempts-don't-consume rule. - Conditional commits replace every blind save a racing operation could clobber: MarkRevoked flips VERIFIED->REVOKED only while still VERIFIED; Link re-checks the identity in its commit tx (a revoke landing during the seal gains no live link rows); StageChallenge persists a re-add/rotate challenge conditional on the load-time status+nonce with no live claim, and never writes status. - Link liveness gate: every agent in the batch must be ACTIVE or DEPRECATED — terminal or pre-activation agents fail the whole batch with 422 AGENT_NOT_LINKABLE. New per-owner link/unlink rate limit (identity.link-rate-limit, default 60/min). - RA reads follow the visibility predicate: AgentDetails.identities[] is empty for a terminal agent; the identity detail's linked list drops links to terminal agents and surfaces (not swallows) agent lookup failures. - GET /v2/ans/identities adopts the v2 limit + opaque-cursor envelope; JWS proof decoding rejects crit headers (RFC 7515 $4.1.11); did:web path segments additionally reject '.', '..', and control bytes. Race-pinning tests simulate a concurrent revoke committing inside the seal round trip via a sealer hook; spec/api-spec-v2.yaml documents the new codes and seal-before-success semantics. Signed-off-by: Connor Snitker <csnitker@godaddy.com>
…ation The TL half of the 2026-06-11 review-pass deltas (design §5.6.3): - Computed views carry the keys: the badge identities[] join and the identity badge quote the CURRENT proven key set verbatim from the latest sealed proof event (json.RawMessage end to end), with keysLogId pointing at that seal — a verifier checks operator signatures from the badge alone. provenKeyIds/identityLogId are replaced. writeJSON disables HTML escaping so the quoted sealed bytes survive byte-verbatim. - One visibility predicate on every current view: an entry appears while the link is LINKED and the agent is live (ACTIVE/DEPRECATED/ WARNING). REVOKED identities STAY VISIBLE with identityStatus REVOKED and keys withheld — a verifier must see the who behind a still-linked agent was revoked. Terminal agents drop out of the badge join and the reverse join; history routes are untouched. - Revocation is terminal at read time: status derives REVOKED from ANY IDENTITY_REVOKED leaf on the stream, never tail-only — a racing operation's leaf landing after the revocation can never resurrect the identity on the public surface. - Join failure is explicit, never silent: the badge serves the agent material with identitiesUnavailable: true when the join cannot be computed; the reverse join propagates non-not-found failures instead of shrinking the answer. - Badge identities[] carries a small safety cap with identitiesTotal; the standalone per-agent route and the reverse join are paginated (TL limit/offset convention) as the overflow targets. Signed-off-by: Connor Snitker <csnitker@godaddy.com>
…views - TL reads after identity operations no longer poll for records: the 200 already guarantees the seal (assert_tl_identity_audit fails immediately on a missing record as a seal-before-success regression; only Merkle-proof coverage may briefly wait on the checkpoint cadence). - New negative step: linking a REVOKED agent fails 422 AGENT_NOT_LINKABLE all-or-nothing and seals nothing. - Badge asserts move to the new shape: verbatim keys[] lengths + keysLogId presence, rotation visible as the quoted set flipping 2 -> 1, and the revoked did:web staying VISIBLE on the still-linked agent with identityStatus REVOKED and no quoted keys. - Agents' TL presence is polled right after activation: the AGENT lane still seals via the async outbox (flagged in the design as a bug to fix separately), and the identity joins need the AGENT_REGISTERED leaves present. Signed-off-by: Connor Snitker <csnitker@godaddy.com>
…rastructure Standalone proof-of-concept CLI (own Go module under scripts/poc/ — go-ethereum and miekg/dns never touch the production go.mod; not built by make) answering whether the future eth identity kinds are implementable from plain Go with no funded wallet and no paid APIs: - did:ethr: offline proof-of-control (EIP-191 personal_sign -> ecrecover -> address compare — zero RPC for the bare form) plus full ERC-1056 resolution: the changed() linked-list event walk, delegate/attribute document construction matching the reference resolver, validTo/revocation semantics. - ENS from first principles: namehash, registry parent-walk, ENSIP-10 wildcard, and the EIP-3668 CCIP-Read gateway loop — resolves offchain names (cb.id, base.eth subnames) that no maintained Go ENS library can. - ENSIP-25: bidirectional ENS <-> ERC-8004 verification against the official IdentityRegistry deployments, with ERC-7930 key encode/decode and probing of the CAIP-style key forms observed in the wild (spec-vs-wild drift surfaced explicitly). - ENSIP-26: agent-context / agent-endpoint discovery with real-DNS checks (A/AAAA/HTTPS/TXT + DNSSEC AD bit) and well-known document probes. Validated live against mainnet (enswhois.eth — a real ERC-8004 agent, jesse.base.eth via Coinbase's production CCIP gateway) and Sepolia. The README maps each finding onto the controlVerifier seam these kinds will eventually plug into. Signed-off-by: Connor Snitker <csnitker@godaddy.com>
The delta plan against the as-built branch: what the three 2026-06-11 design-review passes changed, which items were already implemented, the eight workstreams, the resolved doc inconsistencies (revoked identities stay visible on still-linked agents; the agent lane's return-before-seal flagged as a bug to fix separately), and the as-built notes on the race guards the adversarial review added (read-side terminal revocation, conditional commits, the seal claim). Signed-off-by: Connor Snitker <csnitker@godaddy.com>
Signed-off-by: James Hateley <jhateley@godaddy.com>
3375835 to
7fb2cbd
Compare
Signed-off-by: James Hateley <jhateley@godaddy.com>
7fb2cbd to
7e3e424
Compare
Replace the literal {"v":"ACDC marker in presentedCredentialSAID with a
regexp tolerating insignificant JSON whitespace around the brace, key,
and colon, so a pretty-printing serializer does not cause the leaf-SAID
scan to miss credential frames. Anchored on the version member being
first, which the KERI/ACDC serialization rules guarantee.
Trim the vlei demo README to remove three-way repetition while keeping
every script-verified fact, and tidy the demo scripts, docker-compose,
and start.sh vlei config comment. Ignore scripts/demo/vlei/signify/out/.
Signed-off-by: James Hateley <jhateley@godaddy.com>
The noop LEIControlVerifier required a bespoke base64url-JSON payload
({publicKey, lei}) while the real verifier consumes a full-chain
KERI/CESR export. Presenting the demo's real CESR to a noop-mode RA
therefore failed base64url decoding and returned 422
LEI_PRESENTATION_INVALID — diverging from the DNS and did:web noops,
which accept the same client payload as their real counterparts and
waive only the external-world binding.
The noop now consumes the same cesr/cesrSignature the verifier does: it
pins the real subject AID read from the presented leaf credential (a.i),
echoes the credential's LEI (a.LEI), and structurally accepts a
well-formed qb64 signature — waiving the GLEIF authorization, the live
AID↔LEI binding, and the cryptographic signature check (no KEL key-state
oracle in the quickstart), mirroring the noop DNS verifier.
The ACDC frame scan in verifier.go is refactored into a shared
scanACDCChain/leafFrame so the noop reads the leaf's a.i/a.LEI;
presentedCredentialSAID is preserved as a thin wrapper. No wire-contract
change — the OpenAPI cesr field already specifies the full-chain export.
Stale "runs real Ed25519 crypto"/"base64url JSON" docs updated.
Signed-off-by: James Hateley <jhateley@godaddy.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.