The GentID Protocol
Version: 0.1 (draft) Status: Draft for implementation feedback License: This specification is released under CC-BY-4.0. Reference implementations are MIT.
GentID is a federated identity and authorization protocol for AI agents. It lets any organization issue cryptographic identities for its agents under its own domain, and lets any relying party verify those identities — and the financial authorizations they carry — with pure cryptography, offline after caching, with no central registry.
The trust model is DKIM's, applied to agents: anyone can run a GentID node, but only the controller of delta.com's DNS can issue identities under delta.com.
Requirement language. The key words MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY are to be interpreted as described in RFC 2119.
1. Overview and Terminology
| Term | Meaning |
|---|---|
| Org | A legal or operational entity anchored to a DNS domain. Controls a root key. |
| Node | Software an org runs (or has hosted) that issues certificates and serves protocol endpoints for its domain. |
| Delegation | A signed certificate by which a key grants a narrower set of powers to a child key (gentid.delegation.v1, §4.1). |
| Agent certificate | The leaf certificate binding an agent's key to its identifier (gentid.agent.v1, §4.2). |
| Bundle | The set of certificates (and optional attestations / revocation lists) a relying party needs to verify a chain offline (§5.1). |
| Envelope | A signed, timestamped, nonced wrapper an agent puts around a message or request (gentid.msg.v1, §5.1). |
| Mandate | A signed, verifiable spending authorization (gentid.mandate.v1, §7.1). |
| Enforcer | A settlement institution that holds funds and enforces mandates. GentID verifies mandates; enforcers enforce them (§7.3). |
| Relying party (RP) | Any service that receives an envelope and verifies it. |
| Anchor | The set of root public keys for a domain, established via DNS TXT (§3.1) and/or /.well-known/gentid.json (§3.2). |
A conforming verifier implements §3.3, §5.2, and §6.4. A conforming issuer additionally implements §3.1–§3.2, §4, and §6.1–§6.3. Mandate support (§7) and the HTTP binding (§8) are separately conformable modules.
2. Identifiers
2.1 Syntax
gentid-id = "gentic:agent:" domain *( ":" path-seg ) ":" name
domain = <DNS domain, lowercase, punycode for IDNs>
path-seg = 1*32( ALPHA / DIGIT / "-" / "_" ) ; delegation path segment
name = 1*64( ALPHA / DIGIT / "-" / "_" ) ; leaf agent nameExamples:
gentic:agent:delta.com:support-bot
gentic:agent:delta.com:ops:booking:rebooker-7The path segments mirror the delegation chain (§4.1): each path-seg is the name of one delegation link between the org root and the agent. The final component is the agent's own name. Identifiers are case-sensitive after the domain; the domain is always lowercase.
Legacy identifiers. IDs of the form gentic:agent:<hex> where <hex> is 6–64 lowercase hex characters and contains no further : separators are legacy registry-era IDs. Parsers MUST accept them and flag them legacy: true. They carry no domain anchor and are verifiable only through the legacy compatibility node (§10).
Disambiguation rule: an ID whose first component after gentic:agent: matches ^[0-9a-f]{6,64}$ and has no following components is legacy. A domain always contains at least one ., so the grammars do not collide.
2.2 did:web projection
Every non-legacy GentID identifier projects to a did:web DID and back:
gentic:agent:delta.com:ops:rebooker-7
⇄ did:web:delta.com:gentid:ops:rebooker-7Rule: did:web:<domain>:gentid[:path-seg…]:<name>. The gentid segment is fixed and reserved. A DID Document generator MUST produce a document whose verificationMethod contains the agent's current Ed25519 public key (as publicKeyJwk) with id = <did>#<kid>, and whose service array includes the org's GentID endpoint (type: "GentIDNode", serviceEndpoint from §3.2).
3. Trust Anchoring and Cryptography
3.1 DNS TXT anchor
An org anchors its root key(s) by publishing a TXT record at _gentid.<domain>:
_gentid.delta.com. IN TXT "v=gentid1; k=ed25519; p=<base64url raw 32-byte public key>"- Multiple
_gentidTXT records MAY exist (key rotation, offline roots). Each valid record contributes one anchor key. v=gentid1is mandatory and MUST be first. Unknowntag=valuepairs MUST be ignored.- Verifiers SHOULD note whether the DNS response was DNSSEC-authenticated and MAY expose this as a signal; absence of DNSSEC does not fail verification.
3.2 /.well-known/gentid.json
An org (or its node) MUST serve https://<domain>/.well-known/gentid.json:
{
"type": "gentid.node.v1",
"domain": "delta.com",
"protocolVersion": "0.1",
"rootKeys": [
{ "kid": "…", "kty": "OKP", "crv": "Ed25519", "x": "…" }
],
"endpoints": {
"agents": "https://gentid.delta.com/v1/agents/{id}",
"delegations": "https://gentid.delta.com/v1/delegations/{kid}",
"revocations": "https://gentid.delta.com/v1/revocations"
},
"revocation": {
"maxAge": 300,
"refreshInterval": 3600
},
"policy": {
"mandateEnforcers": []
},
"sig": { "alg": "EdDSA", "kid": "…", "sig": "…" }
}- The document MUST be signed (§3.3) by a root key. Verifiers MUST check the signature against a key that appears in
rootKeysand, when DNS is resolvable, require at least onerootKeysentry to match a_gentidTXT anchor (§3.1). When DNS is unavailable, a cached, previously DNS-corroborated document MAY be used within its cache TTL. endpointsvalues are URI templates (RFC 6570 level 1). The three endpoints above are mandatory for issuers. Responses:agents/{id}returns the agent's current bundle (§5.1bundleobject);delegations/{kid}returns a single delegation certificate;revocationsreturns the current signed revocation list (§6.1).revocation.maxAge(seconds) is the org's declared bound for financial-class freshness (§6.4). Default 300.revocation.refreshIntervalis the re-publish cadence (§6.3).policy.mandateEnforcerslists the domains of settlement institutions the org designates for its mandates (§7.4). Default empty.
3.3 Canonicalization, key identifiers, signatures
- Algorithm: Ed25519 (EdDSA) only, in v0.1.
algMUST be"EdDSA". - Canonicalization: RFC 8785 (JCS). To sign an object: remove its
sigmember, canonicalize with JCS, sign the UTF-8 bytes. To verify: removesig, canonicalize, verify. - Key identifier (`kid`): the RFC 7638 JWK thumbprint (SHA-256, base64url, no padding) of the key's OKP JWK
{"crv":"Ed25519","kty":"OKP","x":"…"}. - Signature object: every signed protocol object carries
"sig": { "alg": "EdDSA", "kid": "<signer kid>", "sig": "<base64url signature>" }. - All base64 in this spec is base64url without padding (RFC 4648 §5).
- All timestamps are integer Unix seconds (UTC).
Private keys MUST be generated locally by the party that will control them. No conforming API transmits a private key across a network boundary.
4. Certificates
4.1 gentid.delegation.v1
A delegation certificate grants a child key a subset of the signer's powers:
{
"type": "gentid.delegation.v1",
"domain": "delta.com",
"name": "ops",
"path": [],
"subject": { "kid": "<child kid>", "kty": "OKP", "crv": "Ed25519", "x": "…" },
"grants": {
"scopes": ["booking:*", "support:read"],
"mandateCeiling": { "currency": "USD", "perTransaction": 500, "perDay": 2000, "total": 10000 },
"maxDepth": 2
},
"nbf": 1750000000,
"exp": 1781536000,
"sig": { "alg": "EdDSA", "kid": "<parent kid>", "sig": "…" }
}pathis the list of ancestor delegation names between the root and this link (empty when signed by the root).nameis this link's own segment. The concatenationpath + [name]becomes thepathof any child.subjectembeds the child's full public key, so bundles verify without key lookups.- Monotonic narrowing (issuers MUST enforce at issuance; verifiers MUST enforce at verification): -
scopes⊆ parent's effective scopes. Scope matching is exact-segment with*as a trailing wildcard segment (booking:*coversbooking:create, notbookingx). The root's implicit scope set is["*"]. - Every field ofmandateCeiling≤ the parent's corresponding field, in the same currency. A field absent on the parent means unlimited for the parent; a field absent on the child inherits the parent's value. The root's implicit ceiling is unlimited. If the parent has nomandateCeilingat all and the child declares none, the effective ceiling is unlimited — issuers SHOULD always set explicit ceilings. -exp≤ parentexp;nbf≥ parentnbf. - Chain length below this link ≤maxDepth(root implicitmaxDepth= 8; a child'smaxDepthMUST be ≤ parent'smaxDepth− 1). - A delegation signed directly by a root key is a root delegation; its
sig.kidMUST match an anchor key (§3.1/§3.2).
4.2 gentid.agent.v1
The leaf certificate binding an agent key to its identifier:
{
"type": "gentid.agent.v1",
"id": "gentic:agent:delta.com:ops:rebooker-7",
"domain": "delta.com",
"path": ["ops"],
"name": "rebooker-7",
"subject": { "kid": "<agent kid>", "kty": "OKP", "crv": "Ed25519", "x": "…" },
"grants": {
"scopes": ["booking:rebook"],
"mandateCeiling": { "currency": "USD", "perTransaction": 200 }
},
"nbf": 1750000000,
"exp": 1755184000,
"sig": { "alg": "EdDSA", "kid": "<issuing delegation's subject kid>", "sig": "…" }
}idMUST equalgentic:agent:+domain+:+ eachpathsegment +:+name.pathMUST equal the issuing delegation'spath + [name-of-issuing-delegation], or[]if issued directly by the root.grantsnarrow monotonically from the issuing link exactly as in §4.1.- Agent certificates SHOULD be short-lived (RECOMMENDED ≤ 60 days).
5. Messages and Verification
5.1 gentid.msg.v1 envelope and the bundle
An agent proves authorship of a request by attaching an envelope:
{
"type": "gentid.msg.v1",
"id": "b64url-128-bit-random-nonce",
"ts": 1752570000,
"agent": "gentic:agent:delta.com:ops:rebooker-7",
"kid": "<agent kid>",
"payloadHash": "<base64url SHA-256 of the request payload>",
"mandate": null,
"sig": { "alg": "EdDSA", "kid": "<agent kid>", "sig": "…" }
}idis a nonce: ≥ 128 bits of randomness, base64url. RPs MUST reject a repeated(agent, id)pair within the acceptance window.tsMUST be within ±300 seconds of the RP's clock.payloadHashbinds the envelope to the request body (for HTTP: the raw request body bytes; empty body hashes the empty string). Binding of method/URL is profile-specific (§8.1).mandate, when present, is agentid.mandate.v1object (§7.1) or itsidif the mandate was delivered in the bundle.
The bundle delivers the verification material:
{
"type": "gentid.bundle.v1",
"agentCert": { …gentid.agent.v1… },
"delegations": [ …root-first list of gentid.delegation.v1… ],
"attestations": [ …optional, §9… ],
"revocations": { …optional gentid.revocations.v1 snapshot, §6.1… }
}5.2 Chain verification procedure
verifyChain(bundle, anchors, opts) — anchors is the set of root keys for the claimed domain (resolved per §3.1/§3.2 by the caller or an injected resolver); opts includes now, operationClass (§6.4), and the envelope under test. Steps:
- Parse the envelope and bundle; reject unknown
typevalues or malformed objects. - Freshness of envelope: check
tswindow (±300 s) and nonce non-reuse. - Anchor the chain root: the first delegation in
delegations(or the agent cert itself ifdelegationsis empty) MUST be signed by akidpresent inanchors. If not ⇒ reject (E_ANCHOR). - Walk the chain root→leaf: for each link, verify (a)
sigagainst the previous link'ssubjectkey (or anchor key for the first), (b)nbf ≤ now ≤ exp, (c)path/nameconsistency (§4.1–§4.2), (d) monotonic narrowing ofgrants(scopes, ceilings, depth, validity windows). Any failure ⇒ reject (E_CHAIN, with the failing link index). - Bind the leaf: the agent cert's
subject.kidMUST equal the envelope'skid, itsidMUST equal the envelope'sagent, and the envelopesigMUST verify against the agent cert'ssubjectkey. Failure ⇒ reject (E_LEAF). - Revocation: evaluate §6.4 for
opts.operationClassagainst the freshest available revocation list for the domain. Any revokedkidoridanywhere in the chain ⇒ reject (E_REVOKED); stale/unknown revocation state is handled per class (§6.4). - Output:
{ id, domain, path, name, grants (effective, fully narrowed), chain, assurance }(assurance per §9).
Steps 3–6 are the normative core; a verifier MUST NOT reorder them in a way that skips any check. verifyChain is a pure function over its inputs: all network resolution happens outside it.
6. Revocation
6.1 gentid.revocations.v1
{
"type": "gentid.revocations.v1",
"domain": "delta.com",
"serial": 42,
"issuedAt": 1752570000,
"nextUpdateBy": 1752573600,
"revoked": [
{ "kid": "…", "at": 1752569000, "reason": "key-compromise" },
{ "id": "gentic:agent:delta.com:ops:rebooker-7", "at": 1752569500 }
],
"sig": { "alg": "EdDSA", "kid": "<root kid>", "sig": "…" }
}serialMUST increase monotonically with every publication (including empty re-publications).- Anti-rollback: a verifier that has seen serial n for a domain MUST reject any list with serial < n (
E_ROLLBACK) and MUST NOT let it overwrite the cached list. - Revoking a
kidinvalidates every certificate signed by that kid and, transitively, the entire subtree below it. Revoking anidinvalidates that agent only. - The list MUST be signed by a root key.
6.2 Propagation and webhooks
A node MUST update its served list within 5 seconds of a revocation command. Nodes SHOULD support registered webhooks: on publication, POST the new list to each registered URL with an X-GentID-Serial header. Webhook delivery is best-effort; the pull path (§6.3) is authoritative.
6.3 Serving
The revocations endpoint (§3.2) serves the current list with standard HTTP caching (ETag = serial). Even with no revocations, a node MUST re-publish (new serial, fresh issuedAt/nextUpdateBy) at least every revocation.refreshInterval seconds, so that nextUpdateBy in the wild never lapses while the node is healthy.
6.4 Freshness tiers
Callers declare an operationClass; the verifier enforces:
| Class | Requirement on the revocation list used |
|---|---|
read | Best effort. A cached list of any age MAY be used; missing list ⇒ proceed, flag revocationChecked: false. |
commit | List MUST satisfy now ≤ nextUpdateBy (grace: + 1 × refreshInterval). Otherwise ⇒ reject (E_STALE). |
financial | List MUST satisfy now − issuedAt ≤ revocation.maxAge (§3.2). Stale, missing, or unverifiable list ⇒ reject (E_STALE). There is no override parameter. |
financial + unknown = deny is a normative invariant. Implementations MUST NOT expose a configuration flag that relaxes it.
7. Mandates
7.1 gentid.mandate.v1
A mandate is a signed spending authorization issued down the same delegation chain:
{
"type": "gentid.mandate.v1",
"id": "b64url-128-bit-random",
"domain": "delta.com",
"agent": "gentic:agent:delta.com:ops:rebooker-7",
"limits": { "currency": "USD", "perTransaction": 200, "perDay": 1000, "total": 5000 },
"scope": { "payees": ["gentic:agent:united.com:*", "acme-travel.com"], "categories": ["travel"] },
"approval": { "aboveAmount": 150, "method": "human" },
"escrow": { "required": true, "releaseOn": "receipt" },
"enforcers": ["atheries.com"],
"nbf": 1752500000,
"exp": 1755184000,
"sig": { "alg": "EdDSA", "kid": "<a kid in the agent's chain>", "sig": "…" }
}sig.kidMUST be a key in the agent's verified chain (root, a delegation subject, or — discouraged — the agent itself). The mandate's authority derives from that key's position.- Every
limitsfield MUST be ≤ the effective `mandateCeiling` at the signing key's chain position (§4.1), same currency. Exceeding any ceiling ⇒ invalid. enforcersMUST be a subset of the domain'spolicy.mandateEnforcers(§3.2). An empty policy list means the org has designated no enforcers and no valid mandate can name one.scope.payeesentries are GentID id patterns (trailing*wildcard on the name/path) or bare domains.approval.aboveAmountmarks the threshold above which the enforcer MUST obtain out-of-band human approval — enforcement of this is the enforcer's duty, not the verifier's.
7.2 Mandate verification
verifyMandate(mandate, bundle, anchors, opts):
- Run
verifyChain(bundle, anchors, opts)— withopts.operationClass = "financial"whenever the mandate will be acted on. Failure fails the mandate. mandate.agentMUST equal the verified chain'sid;mandate.domainMUST equal its domain.mandate.sig.kidMUST appear in the verified chain; verifysigagainst that key.nbf ≤ now ≤ exp.limitsMUST fit within the effectivemandateCeilingat the signing key's position.enforcers⊆ the domain's publishedpolicy.mandateEnforcers(from the anchored.well-knowndocument supplied inopts).- Neither the mandate's
idnor any chain key may be revoked (financial freshness, §6.4).
Output: { valid, mandate, effectiveCeiling, enforcers }.
7.3 Verified ≠ enforceable
Verification proves a mandate is authentic: really issued, within ceilings, not expired, not revoked. It does not prove funds exist, budgets have room, or approvals happened — that is the enforcer's job, against live state GentID does not hold. Conforming implementations MUST preserve this distinction in API naming and documentation. GentID software MUST NOT maintain balances, escrow state, or budget accounting.
7.4 Enforcers
An enforcer is identified by its domain and MUST itself be a GentID org (anchored per §3). Orgs designate acceptable enforcers in policy.mandateEnforcers. The enforcer client interface (MandateEnforcerClient: quote, escrow, release, status) is normatively named but its wire protocol is out of scope for v0.1; the HTTP 402 binding (§8.2) defines the RP-visible flow.
7.7 gentid.receipt.v1
Enforcers emit signed settlement receipts; GentID implementations parse and verify them (they never emit them):
{
"type": "gentid.receipt.v1",
"id": "…",
"enforcer": "atheries.com",
"mandate": "<mandate id>",
"agent": "gentic:agent:delta.com:ops:rebooker-7",
"payee": "acme-travel.com",
"amount": 142.50,
"currency": "USD",
"outcome": "settled",
"ts": 1752571000,
"sig": { "alg": "EdDSA", "kid": "<enforcer key>", "sig": "…" }
}outcome ∈ settled | refunded | disputed | failed. Verification anchors sig.kid in the enforcer's domain (§3). Receipts are the evidence base for assurance tier 3 (§9).
8. HTTP Binding
8.1 Request headers
GentID-Envelope: <base64url(JSON gentid.msg.v1)>
GentID-Bundle: <base64url(JSON gentid.bundle.v1)>payloadHashcovers the raw request body. For GET/HEAD (no body), the hash of the empty string. The envelope's canonicalpayloadHashinput for HTTP additionally prepends the line<METHOD> <path+query>\nto the body bytes, binding the route.- RPs SHOULD cache bundles per agent (keyed by agent id + cert
exp) and accept envelopes without aGentID-Bundleheader when a cached, still-valid bundle exists; the responseGentID-Bundle-Required: trueheader requests a re-send.
8.2 HTTP 402 payment binding
A service that requires payment responds:
HTTP/1.1 402 Payment Required
GentID-Price: 12.50 USD
GentID-Payee: acme-travel.com
GentID-Enforcers-Accepted: atheries.com, other-settler.exampleClient flow: (1) hold a verified mandate whose enforcers intersects GentID-Enforcers-Accepted and whose limits/scope cover price and payee; (2) request escrow from the enforcer via its MandateEnforcerClient; (3) retry the original request adding GentID-Escrow: <base64url enforcer-signed escrow proof>; (4) the service verifies the escrow proof against the enforcer's anchors, fulfils, and the enforcer releases per the mandate's escrow.releaseOn rule, emitting a gentid.receipt.v1.
9. Assurance Tiers
| Tier | Name | Established by |
|---|---|---|
| 0 | self-declared | Chain verifies but no DNS/.well-known anchor corroborated (e.g. offline, unanchored dev keys). |
| 1 | domain-verified | Chain anchors to the domain per §3. Proves control of the domain, nothing else. |
| 2 | org-verified | A gentid.attestation.v1 of kind org-verified, signed by a recognized attestation authority, attached to the org root and carried in the bundle. |
| 3 | transaction-proven | Tier 2 plus verifiable gentid.receipt.v1 history from recognized enforcers. |
{
"type": "gentid.attestation.v1",
"kind": "org-verified",
"subjectDomain": "delta.com",
"subjectKid": "<org root kid>",
"authority": "gentid.com",
"claims": { "legalName": "Delta Air Lines, Inc.", "jurisdiction": "US-GA" },
"issuedAt": 1752000000,
"exp": 1783536000,
"sig": { "alg": "EdDSA", "kid": "<authority key>", "sig": "…" }
}Verifiers compute tiers 0–1 locally; tiers 2–3 require the verifier to recognize the attestation authority / enforcer (a local trust decision, like a browser root store).
Honesty caveat (normative for documentation): domain-verified proves an agent belongs to a domain — not that the domain is honest. de1ta.com can be domain-verified. Lookalike-domain risk is mitigated by tiers 2–3, not tier 1, and conforming documentation MUST say so.
10. Legacy Compatibility and Aliases
- Legacy hash IDs (§2.1) verify only against the designated legacy node (
legacy.gentid.com), which acts as the anchor domain for all registry-era identities during the deprecation window. - Alias record — emitted at migration so history remains verifiable:
{
"type": "gentid.alias.v1",
"legacyId": "gentic:agent:a3f9d2e8b1c4",
"id": "gentic:agent:delta.com:ops:rebooker-7",
"issuedAt": 1752570000,
"sig": { "alg": "EdDSA", "kid": "<legacy node root kid>", "sig": "…" }
}Verifiers presented with a legacy signature MAY resolve the alias and report the new identity.
11. Security Considerations
- Clock skew: the ±300 s envelope window is a hard bound; RPs with looser clocks MUST fix their clocks, not widen the window.
- Nonce storage: RPs MUST retain seen
(agent, id)pairs for at least the acceptance window (600 s) to prevent replay. - Key rotation: publish the new root TXT record alongside the old, re-sign
.well-known, re-issue root delegations, then remove the old record. Verifiers accept any current anchor. - Offline roots: orgs SHOULD keep root keys offline and operate day-to-day from a root delegation; revoking that delegation's kid is then the org-level kill switch.
- Downgrade: verifiers MUST ignore any object whose
alg≠EdDSAin v0.1. - Rollback: §6.1's serial monotonicity MUST be enforced with persisted state per domain.
12. Test Vectors
Conformance vectors live in spec/test-vectors/ as JSON files, each { "name", "description", "input": …, "expect": "valid" | "<error code>" }. The suite MUST cover: a valid 3-link chain; grant-widening chains (E_CHAIN); expired links; revoked kids; rollback lists (E_ROLLBACK); tampered envelopes (E_LEAF); stale financial-class checks (E_STALE); mandates exceeding ceilings; mandates naming undesignated enforcers. A verifier passing all vectors and implementing §5.2/§6.4/§7.2 is conformant.
