Authentication
INK uses Ed25519 signature-based authentication, not HMAC. There is no shared secret.
Authorization Header
Every request includes an Authorization header:
Authorization: INK-Ed25519 <base64url-encoded-signature>When the sender has multiple keys (see Key Rotation), an optional keyId parameter identifies which signing key produced the signature:
Authorization: INK-Ed25519 <base64url-encoded-signature> keyId=sig-2026-03This lets the receiver verify against the correct key directly instead of trial-verifying across all candidate keys. If keyId is omitted or unknown, the search falls back to trying active keys then retired keys in order.
That search order is the general one, and live transport authentication narrows it. A signature that only a retired key verifies is rejected for live traffic with retired_key_for_live_auth unless the deployment has explicitly opted into a bounded rotation grace window. Retired keys keep verifying stored artifacts. See Key Rotation.
Signature Base
The signature covers a concatenation of a fixed domain separator, the HTTP method, request path, recipient DID, JCS-canonicalized request body and timestamp:
signatureBase = "ink/0.1" + "\n" + METHOD + "\n" + PATH + "\n" + recipientDid + "\n" + JCS(body) + "\n" + timestampsignature = Ed25519.sign(privateKey, signatureBase)The first line is always the fixed literal ink/0.1 for every request, including ink/0.2 traffic. The transport signature base never tracks the envelope protocol value. Only the body-signature domain selects on the signed protocol (see Versioning).
JCS(body) is the canonicalization of the body exactly as delivered. Nothing is removed, including the envelope’s own signature member. Stripping signature is the body-signature rule, not this one; see Canonicalization.
For example, a request posted to an endpoint whose path is /ink/v1/inbound, addressed to did:plc:recipient:
signatureBase = "ink/0.1\nPOST\n/ink/v1/inbound\ndid:plc:recipient\n{...canonical body...}\n2026-03-18T12:00:00Z"The request path
PATH is the path component of the URL the request is actually sent to, with no query string and no fragment.
INK reserves no fixed inbound path. A receiver picks its own and publishes it as the endpoint of its Agent Card; the sender takes PATH from that URL, and the receiver rebuilds the base with the path it published. /ink/v1/inbound, /ink/v1/intents and /ink/v1/<recipientDid>/intent are all conforming, and none of them is normative. Wherever a path appears in these docs it is illustrating a deployment.
This matters because PATH sits inside the frozen signature base. A sender that signs a path copied from a document instead of read from the card it just fetched does not have a routing bug, it has a signature that cannot verify anywhere. The one path a specification does pin is the card discovery path itself, GET <base>/ink/v1/<agentId>/agent.json, because a sender has to reach the card before it can learn anything else.
The query string is deliberately outside the base, so a receiver MUST NOT put authorization-relevant routing in the query. Anything the signature has to cover belongs in the path or in the body.
Why This Signature Base
Including the fixed domain separator, HTTP method, path and recipient DID prevents:
- Cross-protocol collision: The fixed
ink/0.1first line domain-separates transport signatures from other Ed25519 signing contexts. The envelopeprotocolitself is committed insideJCS(body), so a relabelled body still fails to verify. - Cross-endpoint replay: A signed intent cannot be replayed against a different endpoint of the same receiver, such as its handshake or receipt path.
- Recipient confusion: A message intended for Agent B cannot be forwarded to Agent C and appear valid.
- Method confusion: A POST body cannot be replayed as a PUT or vice versa.
Verification Steps
The receiving agent:
- Resolves the sender’s principal to a verified Agent Card and takes the card’s
keys.signingset. A key-derived ordid:webprincipal is resolvable; anything else is not, and a receiver that cannot resolve rejects rather than falling back. See Identity for what each principal form admits. This resolution is the integrator’s responsibility; the INK reference middleware (verifyInkAuth) expects the public keys to be supplied via a caller-provided resolver and then applies the key-rotation authority rule. - Reconstructs the signature base from the received request and verifies the Ed25519 signature against the resolved key set per the authority rule. Verification is strict RFC 8032: small-order public keys and non-canonical encodings are rejected.
On success verifyInkAuth returns both the raw, sender-chosen senderAgentId and a prefix-independent principal. The tulpa: and ink: spellings of one key are the same actor, so authorization, block lists, rate limits and every per-sender abuse control MUST key on principal, never on senderAgentId, or a sender can switch prefix to evade them. canonicalAgentPrincipal(agentId) exposes the same mapping for state you populate yourself.
Signature Base Anatomy
The signature base is a newline-delimited string. Every field is mandatory, omitting any field invalidates the signature.
Each field prevents a specific attack class:
| Field | Prevents |
|---|---|
| Fixed domain separator | Cross-protocol collision |
| HTTP method | Method confusion (POST replayed as PUT) |
| Request path | Cross-endpoint replay |
| Recipient DID | Recipient confusion (forward to wrong agent) |
| JCS(body) | Body tampering |
| Timestamp | Replay outside window |