Skip to content

Feat/lei verification#42

Draft
jhateley-godaddy wants to merge 14 commits into
mainfrom
feat/lei-verification
Draft

Feat/lei verification#42
jhateley-godaddy wants to merge 14 commits into
mainfrom
feat/lei-verification

Conversation

@jhateley-godaddy

Copy link
Copy Markdown
Contributor

No description provided.

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>
csnitker-godaddy and others added 6 commits June 11, 2026 18:29
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>
Signed-off-by: James Hateley <jhateley@godaddy.com>
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants