Skip to content
↑↓ to move · Enter to openEsc to close
Release notes · key discovery · security · release notes

The registry half of two-path key discovery is live

Shipped · commits 7821338, 22033b0, 81b9c6e

GET /v1/keys/<key-ulid> is deployed on www.agenid.com. It serves the registry's copy of an operator key, and it was the only undeployed half of the mechanism that makes the registry non-authoritative. A verifier can now fetch the operator key from the registry and from the operator's own domain, at https://<operator_domain>/.well-known/agenid/keys.json, and require that they agree.

Making it answer identically on every platform took follow-up work, because the security decision was first built on a framework's decoded path parameter, and in production that parameter had been decoded one more time than the code assumed.

What the endpoint does

The route is read-only, unauthenticated and has no side effects. It serves a key document already in the store, verifies nothing and reports no verification level. The spec-required query form, GET /v1/keys?key_id=<percent-encoded logical id>, returns a byte-identical document built by the same response builder.

The path segment is the wire form of the identifier, a bare ULID, while the store is keyed on the logical form agenid:key:<ULID>. The only translation between them is parseKeyReference from @agenid/core. A key that was never published is 404 key_not_found; a malformed reference or a URI fragment is 400 invalid_key_id. Documents are re-validated against the strict KeyDocument schema before being served, and error bodies do not quote the caller's input back.

Why two paths matter

If the only place to fetch an operator's key were the registry, the registry could substitute a key it controls and mint apparently valid identities. Two-path discovery removes that position. The registry can still serve a key, but a verifier who also fetches the operator's copy will see any disagreement, and disagreement is a hard fail.

The honest boundary: the .well-known copy is published by the operator. Where an operator publishes none, a verifier has one source, not two, and should not treat the missing copy as agreement.

The double-decode defect

Measured against production, a twice-encoded key ULID returned 200, while the identical code returned 400 under a local next start. The earlier guard rejected a reference that still carried a % after the transport decode, which assumed exactly one decode had happened. On Vercel the platform normalizes the path before routing and Next decodes the dynamic segment again, so the guard was inspecting a value two decodes from the wire.

The rule is now stated on the raw request target, where it needs no decode of its own: the path segment must be the literal wire form, and a bare key ULID contains no percent sign. The ?key_id= value is decoded by the registry itself, exactly once. A repeated key_id parameter is refused, in either order, because an ambiguous request cannot promise the identical document the spec requires.

One surface, not two

The reference implementation has two registries, a Fastify one and the Next one that production serves, and they had diverged: only the Next version re-validated stored documents and kept caller input out of errors. Both now call one resolveKeyDocument. A parity test drives the real handlers of both frameworks from raw request-target fixtures and requires identical status, body and reason.

Fastify framework errors no longer reflect the request target. /v1/keys is read-only on both frameworks, with every mutating method answering 405 with an Allow header, and authority key publication moved to POST /v1/authority/keys.

What it does not do, and how to try it

Key discovery serves keys. It says nothing about levels. The reference deployment at agenid.com issues nothing above L1_REGISTERED today: L2 and higher require a VerificationAssertion signed by a root authority key, and that key has not been created yet. L5 is reserved and is not issuable at all. Which agent is acting is not the same question as what it is allowed to do. AgenID answers the first. Protocol v1.1.1 defines no signed authorization object, so permission stays with the operator and with the party on the other side.

Register an agent at /issue, open its JSON envelope from the Verification Card, and follow operator_key.discovery to both paths. The two-path key discovery entry explains the comparison step by step.

Frequently asked questions

Do I have to publish a .well-known key document?

Registration works without one. Without it, a verifier has only the registry's copy, so the key-substitution defense is not in force for your agent.

Does /v1/keys tell me whether an agent is verified?

No. It serves a key document and reports no level. Verification is done by re-checking signatures against the envelope.

What happens to a retired key?

Retired and revoked keys stay resolvable so historical signatures remain checkable. There is no revocation flow deployed today.

What agenid.com issues today

The reference deployment at agenid.com issues DECLARED and L1_REGISTERED only. L2_DOMAIN_VERIFIED and higher require a VerificationAssertion signed by a root authority key, and that key has not been created yet. L5 is reserved by the specification and is not issuable at all.

Identity is not permission

Which agent is acting and who declared it is one question. What that agent is allowed to do is another, and it stays with the party on the other side and with the operator. Protocol v1.1.1 defines no signed authorization object.

Give your agent an identity

Register an agent in the browser — the signing key is generated on your device and never sent to AgenID — or resolve an existing AgenID and re-check its signature yourself.