feat: verified identities — the who behind agents (did:web + did:key)#41
Open
csnitker-godaddy wants to merge 8 commits into
Open
feat: verified identities — the who behind agents (did:web + did:key)#41csnitker-godaddy wants to merge 8 commits into
csnitker-godaddy wants to merge 8 commits into
Conversation
964ca27 to
b07a1ce
Compare
There was a problem hiding this comment.
Pull request overview
Adds Verified Identities (“who”) as a first-class, provider-owned object that can be control-proved (did:web + did:key), sealed as an IDENTITY_* event stream in the existing Transparency Log (single Merkle tree), and linked to agents with read-time joins (no agent re-sealing for identity lifecycle changes).
Changes:
- Introduces TL support for
IDENTITY_*producer events (new ingest lane + identity read/join endpoints) and agent badge identity joins. - Adds RA
/v2/ans/identitieslifecycle (register → verify-control → rotate/revoke + link/unlink), including JWS proof machinery, did:web resolution (noop + hardened web), and did:key verification. - Updates demos/tooling (signproof, lifecycle scripts) and adds an Ethereum identity PoC CLI module (
scripts/poc/ethid).
Reviewed changes
Copilot reviewed 81 out of 82 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| scripts/poc/ethid/README.md | Documents the Ethereum identity PoC CLI goals/recipes and observed ENSIP drift. |
| scripts/poc/ethid/out.go | Adds terminal-safe output helpers with control-character stripping. |
| scripts/poc/ethid/main.go | Adds ethid CLI entrypoint + subcommand dispatch. |
| scripts/poc/ethid/keys.go | Implements did:ethr keygen/sign/verify and DID parsing helpers. |
| scripts/poc/ethid/go.mod | Declares the standalone Go module/deps for the ethid PoC. |
| scripts/poc/ethid/ensip26.go | Implements ENSIP-26 discovery, optional DNS checks, and well-known probing. |
| scripts/poc/ethid/dnscheck.go | Adds real DNS querying logic via miekg/dns with DNSSEC AD reporting. |
| scripts/poc/ethid/chains.go | Adds chain presets + shared RPC flag plumbing and chain-id sanity checks. |
| scripts/poc/ethid/ccip.go | Implements EIP-3668 CCIP-Read gateway loop for ENS offchain resolution. |
| scripts/poc/ethid/.gitignore | Ignores the built ethid binary. |
| scripts/demo/start.sh | Adds RA demo config block for identity resolver + challenge TTL. |
| scripts/demo/signproof/main.go | Adds demo-side keygen/sign tooling for identity control proofs (EdDSA/ES256). |
| scripts/demo/run-lifecycle.sh | Extends lifecycle demo to register/verify/link an identity and validate TL join. |
| scripts/demo/common.sh | Adds assert_tl_identity_audit to enforce seal-before-success invariants in demos. |
| Makefile | Excludes scripts/* from coverage instrumentation. |
| internal/tl/service/receipt.go | Adds identity receipt support and switches receipt cache lookup to (leafIndex, treeSize). |
| internal/tl/service/log.go | Adds identity ingest + identity stream/index query helpers and link join helpers. |
| internal/tl/service/codec.go | Adds the identity ingest codec (identityCodec) for IDENTITY_* events. |
| internal/tl/service/codec_identity_test.go | Adds unit tests for identity codec + cross-lane rejection. |
| internal/tl/service/badge.go | Extends badge model with computed identity join fields and identity-key view fields. |
| internal/tl/handler/paginate_internal_test.go | Tests computed-view pagination helper edge cases. |
| internal/tl/handler/handler.go | Mounts identity routes, adds agent identity computed view routes, and adjusts JSON encoding. |
| internal/tl/handler/handler_test.go | Wires identity badge service into TL handler testbed. |
| internal/tl/handler/empty_param_test.go | Updates handler constructor usage for new dependency. |
| internal/tl/handler/empty_param_identity_test.go | Adds empty-param guard tests for identity/agent-identity routes. |
| internal/tl/event/identity/event_test.go | Adds tests for identity event/envelope validation + signing lifecycle. |
| internal/ra/service/identityratelimit.go | Adds per-owner fixed-window rate limiter for identity operations. |
| internal/ra/service/identitykinds.go | Adds control-verifier registry and did:web + did:key proof verification + shared JWS parsing/sealing. |
| internal/ra/handler/lifecycle.go | Optionally decorates agent detail responses with computed identities[] view. |
| internal/ra/handler/identity.go | Implements /v2/ans/identities handlers (register/rotate/verify/revoke/link/unlink/list/detail). |
| internal/ra/handler/errors.go | Maps transient ErrUnavailable to HTTP 503. |
| internal/ra/handler/errors_internal_test.go | Adds tests pinning the error→HTTP mapping including 503 behavior. |
| internal/ra/handler/dto.go | Adds DTOs/mapping for agent detail identities[] computed view. |
| internal/port/store.go | Adds IdentityStore + IdentityLinkStore interfaces for persistence and link caching. |
| internal/port/didresolver.go | Adds DIDResolver port + DID document/verification method shapes for did:web control proof. |
| internal/domain/identity_test.go | Adds identity domain tests (kind inference, challenge lifecycle, rotation/revoke invariants). |
| internal/domain/errors.go | Introduces ErrUnavailable + constructor for retryable upstream failures. |
| internal/crypto/proofinput.go | Adds canonical identity proof input + signing-input encoding utilities. |
| internal/crypto/proofinput_test.go | Tests canonicalization determinism and signing-input round trip. |
| internal/crypto/jws.go | Adds EdDSA support, crit header rejection, and helper to decode standard JWS without verifying. |
| internal/crypto/jws_crit_test.go | Tests rejection of JWS crit headers. |
| internal/crypto/jwk.go | Adds JWK parsing/allowlist + multibase key encoding/decoding for did:key. |
| internal/crypto/jwk_test.go | Tests JWK/multibase round-trips and rejection cases, including EdDSA verification. |
| internal/config/defaults.go | Adds default RA identity config (resolver, TTL, rate limits, seal timeout). |
| internal/config/config.go | Adds RA identity config types and validation hooks. |
| internal/adapter/tlclient/client.go | Adds IDENTITY ingest lane support and synchronous seal helper with domain error mapping. |
| internal/adapter/tlclient/client_test.go | Adds tests for seal-before-success mapping behavior. |
| internal/adapter/store/sqlitetl/receipts.go | Switches receipt cache lookup to leafIndex-based query (supports identity leaves). |
| internal/adapter/store/sqlitetl/migrations/004_identity_events.sql | Adds tl_events.identity_id + identity↔agent fan-out index table for link joins. |
| internal/adapter/store/sqlitetl/identityevents_test.go | Adds tests for identity indexing, link fan-out, proof selection, pagination, and receipt lookup. |
| internal/adapter/store/sqlite/outbox.go | Allows outbox schema_version to include IDENTITY (lane support). |
| internal/adapter/store/sqlite/migrations/008_identity_seal_claim.sql | Adds challenge-claim column for seal-before-success concurrency control. |
| internal/adapter/store/sqlite/migrations/007_outbox_identity_lane.sql | Widens outbox schema_version CHECK to include IDENTITY via table rebuild. |
| internal/adapter/store/sqlite/migrations/006_identities.sql | Adds identities + identity_links tables and uniqueness constraints. |
| internal/adapter/didresolver/web.go | Adds hardened did:web resolver with SSRF guards, redirect constraints, and size/time bounds. |
| internal/adapter/didresolver/noop.go | Adds noop did:web resolver that synthesizes documents from JWK hints (dev-only). |
| go.mod | Promotes golang.org/x/net to a direct dependency. |
| config/ra-local.yaml | Adds local RA identity config block documentation + defaults. |
| cmd/ans-tl/main.go | Wires identity badge service into TL server. |
| cmd/ans-ra/main.go | Wires identity service, DID resolver selection, and mounts /v2/ans/identities routes. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+374
to
+384
| switch c.Identity.Resolver.Type { | ||
| case "noop", "web": | ||
| default: | ||
| return fmt.Errorf("identity.resolver.type %q not supported (expected 'noop' or 'web')", c.Identity.Resolver.Type) | ||
| } | ||
| if c.Identity.ChallengeTTL < 0 { | ||
| return errors.New("identity.challenge-ttl must not be negative") | ||
| } | ||
| if c.Identity.RegisterRateLimit < 0 { | ||
| return errors.New("identity.register-rate-limit must not be negative") | ||
| } |
Comment on lines
+192
to
+199
| type didEthr struct { | ||
| DID string | ||
| Network string // "" = mainnet per the method spec | ||
| ChainID uint64 | ||
| // Exactly one of Address / PublicKey is the subject form. | ||
| Address common.Address | ||
| PublicKey []byte // 33-byte compressed, nil for address form | ||
| } |
b07a1ce to
9ecfd38
Compare
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>
Signed-off-by: Connor Snitker <csnitker@godaddy.com>
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>
9ecfd38 to
a599ea8
Compare
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.
Summary
Implements Verified Identities — the "who" behind an agent (the "what") — per the team's
DESIGN-multi-identity-anchors.mdrev 4 design of record (who/what pivot, 2026-06-10), including the two post-design corrections: multi-algorithm key support and verbatim verification-method sealing.An identity is a first-class object owned by the
providerId, proven through a challenge-bound control proof, sealed onto its own Transparency Log stream, and linked to any number of that owner's agents. Prove once, link the fleet; rotation/revocation is one sealed event regardless of linked-agent count — propagation is the TL's read-time join. The agent registration path is byte-for-byte untouched.What's in here (4 commits, reviewable independently)
IDENTITY_*event family (closed 5-token enum, same JCS + RFC 6962 leaf rules), thePOST /v1/internal/identities/eventingest lane on the existing producer-signature discipline, the identity read surface (/v1/identities/{id}{,/audit,/receipt,/agents}), and the agent badge's computedidentities[]join. One Merkle tree; "streams" are read indexes (tl_events.identity_id+ a link fan-out table)./v2/ans/identitiessurface: register → verify-control → rotate/revoke + the owner-gated links. Per-kind logic lives behind thecontrolVerifierregistry (internal/ra/service/identitykinds.go) — the extension seam fordid:plc,did:ion,did:ethr, andlei(vLEI).did:web+did:keyship enabled.scripts/demo/identity-lifecycle.shexercises every operation live (multi-key ES256+EdDSA proof, Ed25519 did:key side-by-side with the did:web on one agent, two-agent batch link sealing one event, rotation 2→1 keys, per-pair unlink, read-time revocation propagation);run-lifecycle.shgains the identity steps;signproofis the registrant-side key/JWS tool.Security invariant (stated once)
The RA seals an identity attestation only after control is proven — never on resolution alone. Every submitted compact JWS must carry the served JCS
IdentityProofInput({identifier, identityId, nonce, purpose:"ans:identity-proof:v1", raId, scheme}) verbatim as its payload, and verify against the identifier's authoritative key (the re-fetcheddid.jsonfor did:web; the identifier itself for did:key). One bad proof fails the call closed. The nonce is single-use, consumed only inside the success transaction (conditional-UPDATE TOCTOU guard); failed attempts never consume it.Pre-implementation shape diff (per CLAUDE.md)
The canonical contracts under
spec/are authored in this PR (both validate as OpenAPI 3.0.3; docsui copies synced viamake docs-sync):spec/api-spec-v2.yaml— 8 identity routes (/ans/identities{,/{id}},verify-control,revoke(a POST — identities are never deleted),links{,/{agentId}}) +AgentDetails.identities[](additive, computed).spec/api-spec-tl-v2.yaml— the identity ingest + read surface,IdentityProducerEvent,ProvenKey = {verificationMethod (verbatim), signedProof},LinkedIdentityView.provenKeyIds.Deliberate deltas from the rev-4 design text, all discussed with Connor/team:
Type.IsValid()enuminternal/tl/event/identitypackage, own closed enumValidate()tl_identity_eventstableidentity_idcolumn ontl_events+ link fan-out tablekeyThumbprint+ normalizedpublicKeyJwkverificationMethodverbatim; thumbprints compute-at-readExtensibility (the vlei/did:ion/did:plc path)
Adding a kind = one
controlVerifierimplementation + one registry entry + a lexical arm indomain.InferIdentifierKind. JWS-scheme kinds reuse the shared proof machinery wholesale with a different fetcher port (noop/real adapter split, same as the DNS verifier). Per-kind wire shapes are additive members (signedProofstoday;cesrSignature/ethSignaturelater — exactly one family per kind). Unregistered kinds →IDENTIFIER_KIND_UNSUPPORTED; unknown did methods name the method in the error. NokindCHECK in storage — the domain dispatcher + registry are the single source of truth.Verification
make checkgreen: fmt, vet, golangci-lint (0 issues), coverage 90.1% ≥ 90% gate (internal/domainadditions at 100%)make test-raceclean (incl. concurrent nonce-consumption)IDENTITY_*token sealed, COSE receipts verify,ans-verifypasses on the agent pathNotes for reviewers
dns.type: noop.🤖 Generated with Claude Code