Version: 1.0
Date: 2026-09-05
The server authenticates an HTTP request using a key authorized for an agent, then applies access rules to the agent identifier and verified domains.
The protocol uses HTTP Message Signatures (RFC 9421), SHA-256 Content-Digest (RFC 9530), and ECDSA P-256 with SHA-256. The profile identifier is also the signature tag and A2A extension URI:
https://aithos-protocol.github.io/agent-request-auth/v1
The key words MUST, MUST NOT, REQUIRED, SHOULD and MAY have their BCP 14 meaning when they appear in all capitals.
The signed keyid parameter is a string of 1–512 visible ASCII characters,
interpreted by the server's configured AgentResolver.
The resolver returns an authorized public key, a stable agent_id, and
verified_domains, or rejects the identifier. Each verified domain contains
its name, certified_at, and last_checked_at as timezone-aware UTC timestamps.
An empty domain collection is valid. Agent identifiers use 1–512 visible ASCII
characters and are compared exactly within the configured resolver's namespace.
Domain timestamps MUST satisfy certified_at <= last_checked_at.
The middleware MUST derive the authenticated identity and domain attributes from this result and MUST validate the key before signature verification. The key MUST be a valid, finite point on P-256. Invalid points, the point at infinity and other curves MUST be rejected, including for custom resolvers.
The resolver is selected by the server. It defines identifier interpretation, accepted key bindings, verified domain assertions and their freshness. Its implementation may use any source that satisfies this contract.
The default resolver uses https://registry.aithos.world; its HTTPS origin is
configurable by the server. Its keyid format is:
<agent_id>:<key_thumbprint>
Both components are canonical unpadded base64url encodings of 32 bytes
(43 characters). agent_id identifies the entry; key_thumbprint is the
RFC 7638 thumbprint of the current signing key.
For each resolution, the resolver MUST:
- Read
/v1/agents/{agent_id}from its configured registry origin. - Require the returned
agentIdto match,statusto beACTIVE, andkey_thumbprintto belong toauthorizedKids. - Read
/v1/agents/{agent_id}/jwks.jsonfrom that origin and select exactly one matchingkid. Requirekty="EC",crv="P-256", valid coordinates, and a recomputed RFC 7638 thumbprint equal tokey_thumbprint. - Return that key, the record's
agentId, and itsdomains, mappingcertifiedAtandlastCheckedAtto the corresponding timestamp fields.
Unknown or inactive agents and unauthorized or unsupported keys are rejected. Network failures, malformed responses, and a JWKS missing a key authorized by the record are resolution failures. The resolver MUST bound response sizes and request durations. The default resolver performs a new record read for every request without a local response cache. Registry caching and revalidation govern the state observed; key material is accepted only with the record's authorization.
A verified Aithos domain records a DNS declaration naming the agent and a signed confirmation by an authorized agent key. Domain policy uses the registry's published verification state under its Domain Certification Profile. Timestamps report registry observations; matching uses membership in the returned domain set.
| Parameter | Value |
|---|---|
allowed_agent_ids |
Required: "*" or a list of agent identifiers. An empty list denies every agent. |
required_domains |
Optional list of domain names; omission applies only the agent rule. A supplied list must be nonempty. |
domain_operator |
"or" by default; "and" requires every listed domain. |
"*" matches every authenticated agent. Otherwise the resolved agent_id MUST
belong to allowed_agent_ids. A wildcard mixed into an identifier list is invalid.
When required_domains is supplied, "or" requires at least one listed domain
in the resolved agent's verified_domains; "and" requires all of them. All
matching domains MUST belong to that same resolved agent. The agent rule and
domain rule MUST both pass. A policy denial after authentication returns 403.
Domain names MUST be valid ASCII DNS hostnames in IDNA2008 A-label form. For comparison, lowercase the name and remove one final dot if present. Matching is exact. Invalid names or operators MUST cause a configuration error before serving requests. Resolver-provided domain names undergo the same validation.
The initial headers MUST contain:
Content-Digest: sha-256=:...:
Signature-Input: sig1=...
Signature: sig1=:...:Content-Digest MUST contain exactly one sha-256 byte-sequence member of
32 bytes without parameters. Other digest members MAY be present. The complete
field is signed; every member value MUST be a byte sequence. The digest covers
content bytes after content encoding and
before decoding, excluding HTTP transfer framing. Empty content uses:
Content-Digest: sha-256=:47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=:The application supplies the final content bytes and chooses the content type. The signer sends those bytes unchanged. The verifier reads them before content decoding and passes them unchanged to the application after successful checks.
Signature-Input and Signature follow RFC 9421 dictionary syntax and label
matching. Labels are arbitrary. The verifier MUST select exactly one signature
input whose tag is the profile identifier and its matching signature. Other
signature members and authentication headers MUST be preserved.
Required coverage is "@method", "@target-uri", and "content-digest".
The following fields MUST also be covered whenever present in the received
request: content-type, content-encoding, a2a-extensions, a2a-version.
Additional ordinary HTTP fields MAY be covered; the server MUST require any
additional coverage its application relies on. Components use RFC 9421 default
field extraction, without component parameters. The supported derived components
are @method and @target-uri. Component order is chosen by the signer and
preserved by the verifier.
| Signature parameter | Value |
|---|---|
created |
Nonnegative integer Unix timestamp in whole seconds. |
nonce |
String: 16 cryptographically random bytes, canonical unpadded base64url (22 characters). Fresh for each request signed with the key. |
keyid |
Resolver identifier from Section 2, as a string. |
tag |
Profile identifier from Section 1, as a string. |
These four parameters are REQUIRED. The optional alg MUST equal
"ecdsa-p256-sha256"; other parameters MUST be rejected. The verifier MUST use
this algorithm regardless of any request assertion.
The signature is RFC 9421 ECDSA P-256/SHA-256: two unsigned integers r and s,
each encoded as 32 big-endian bytes and concatenated into 64 bytes. Verification
MUST check 1 <= r, s < n, where n is the P-256 group order, and verify the
ECDSA signature over SHA-256 of the RFC 9421 signature base. That base includes
@signature-params and MUST be reconstructed from the received HTTP message.
Before resolution, reject malformed structured fields, duplicate dictionary keys, duplicate selected signature parameters or components, and repeated Content-Type or A2A-Version fields. Repeated list/dictionary fields are combined according to HTTP and RFC 9421; duplicate keys MUST be detected before parsing discards them. The selected Signature item MUST be a parameter-free byte sequence.
Using integer Unix seconds, freshness requires abs(now - created) <= 60.
Server clocks MUST be synchronized to UTC. The verifier MUST check freshness
both before resolution and after reading the body.
The nonce store exposes an atomic operation:
consume(key_thumbprint, nonce, retain_until) -> accepted | rejected | unavailable
key_thumbprint is computed from the validated P-256 key under RFC 7638. Its
thumbprint input contains exactly crv, kty, x and y; each coordinate is
32 bytes encoded as canonical unpadded base64url.
The operation MUST reject an existing (key_thumbprint, nonce) tuple or an
already reached retain_until. Otherwise it records the tuple until at least
retain_until = created + 61, an exclusive expiration timestamp. Consumption
occurs only after successful signature, content and access-policy checks.
All instances accepting the same requests MUST share replay state and preserve it across restarts. Records MUST remain available until their deadline. Store failure or capacity exhaustion MUST prevent the application from running.
The signer calculates Content-Digest from the final content, generates created
and a fresh nonce, sets keyid and tag, constructs the covered components and
RFC 9421 base, then adds the matching signature. The transport MUST preserve
the signed method, target URI, fields and content.
Before invoking a protected application, the verifier MUST:
- Require HTTPS at the externally visible boundary.
- Validate headers, signature selection, coverage, parameters and freshness.
- Resolve
keyid, validate the returned key and verify the signature. - Read the bounded content and compare its SHA-256 digest to Content-Digest.
- Recheck freshness and evaluate the access policy.
- Atomically consume the nonce until
created + 61. - Pass the authenticated agent, key identifier and unchanged content to the application through server-side request context.
Content is held until all checks pass and MAY be hashed incrementally. The HTTP integration MUST enforce finite header/body limits and deadlines for content reading, resolution and nonce storage before unbounded work or allocation. Body limits count actual received bytes.
Both sides MUST derive the same externally visible @target-uri under RFC 9421.
Proxy deployments MUST use trusted configuration for scheme, authority and any
path prefix, preserving the path and query representation. Client-supplied
forwarding headers MUST NOT override that configuration.
| Failure | HTTP status |
|---|---|
| Missing, malformed, stale, replayed, rejected or invalid authentication | 401 |
| Authenticated agent does not satisfy the access policy | 403 |
| Resolver or nonce-store failure, timeout or capacity exhaustion | 503 |
| Body size limit exceeded | 413 |
| Header size limit exceeded | 431 |
| Body-read deadline exceeded | 408 |
A 401 MUST include this profile's challenge:
WWW-Authenticate: AgentSignature realm="aithos"The client responds using the signed fields from Section 4. Unknown keys and invalid signatures MUST have the same public error representation. Error body format and logging are defined by the server. Every failure stops the protected application. Invalid server configuration fails at startup.
An A2A 1.0 server using this authentication declares the required extension in its Agent Card. This fragment describes that declaration:
{
"capabilities": {
"extensions": [
{
"uri": "https://aithos-protocol.github.io/agent-request-auth/v1",
"description": "Authenticate HTTP requests with an authorized agent key.",
"required": true
}
]
}
}Public Agent Card discovery remains accessible. The client MUST include the
URI in A2A-Extensions before signing. A2A defines the media type and service
parameter values; Section 4 authenticates their received values.
The A2A integration MUST enforce required extension activation before executing
agent operations, using ExtensionSupportRequiredError for missing activation
(HTTP+JSON: 400; JSON-RPC: -32008). Authentication and policy failures use
Section 7. The A2A server owns binding-specific error serialization. Successful
responses MUST include the activated URI in A2A-Extensions, preserving other
activated extension URIs.
Language implementations share this protocol and conformance vectors. Each
package has its own directory, dependencies, tests, version and release. The
Python package lives in python/.
The Python API in README.md requires Python 3.11 or newer, uses ASGI and exposes
AithosAuthMiddleware,
AithosResolver, AgentResolver, ResolvedAgent, VerifiedDomain,
NonceStore, SQLiteNonceStore, sign_request, key_thumbprint and
PROFILE_TAG. The middleware accepts the policy from
Section 3 and optional resolver and nonce_store objects.
The default resolver is AithosResolver(). The default nonce store is
SQLiteNonceStore(path=".aithos-auth/nonces.sqlite3"), relative to the process
working directory. The SQLite store MUST use an atomic transaction and durable
commits. Initialization failure prevents startup. Deployments MUST retain the
database across restarts and share replay state as required by Section 5.
The default SQLite capacity is 100,000 live entries; max_entries configures it.
The Python nonce-store interface is asynchronous:
async def consume(key_thumbprint: str, nonce: str, retain_until: int) -> bool:
...It returns True only for a new unexpired tuple, False on rejection, and raises
on failure. Resolution returns ResolvedAgent or None, and raises on failure.
Domain timestamps are timezone-aware UTC datetimes. Successful middleware
processing sets scope["aithos.auth"] to
{"agent": resolved_agent, "key_id": signed_key_id} before invoking the next
application.
sign_request accepts keyword arguments method, url, headers, body,
key_id, and private_key. Headers are an ordered sequence of string pairs;
body is bytes; the private key is a P-256 key. It returns a new header-pair list,
preserving unrelated headers and signatures, with fresh signature parameters.
An existing signature carrying this profile's tag is replaced together with its
matching signature input. The signer uses an available label for the new pair.
If Content-Digest already contains the correct SHA-256 value, the signer MUST
preserve the original field lines. Both signer and middleware accept
additional_covered_headers, an optional sequence of extra required HTTP fields.
key_thumbprint(public_key) returns the RFC 7638 identifier from Section 5;
PROFILE_TAG contains the URI from Section 1.
The ASGI adapter uses raw_path and query_string. Its URLs MUST omit an
empty trailing query delimiter, which ASGI cannot distinguish from an absent
query; sign_request rejects this form. Proxy deployments configure
external_origin and, when a prefix is stripped, external_path_prefix.
The adapter MUST reject requests whose original target cannot be reconstructed.
Python limits default to 16 KiB of headers, 1 MiB of content, 10 seconds for
reading content, and 5 seconds each for resolution and nonce storage. Registry
responses are bounded to 1 MiB. The server MAY configure other finite limits.
Middleware options are max_header_bytes, max_body_bytes, body_timeout,
resolver_timeout and nonce_timeout. Callback deadlines MUST also be checked
after their results become available.
Shared vectors MUST cover signed requests, signature bases, digests and expected verification results. Each language package MUST test:
- Valid requests, empty content, and changes to signed fields or content.
- Invalid keys, signatures, identifiers, encodings and duplicate fields.
- Agent rules, domain rules, default
or,and, and exact domain matching. - Freshness boundaries, concurrent replay and replay after restart.
- Resolver and nonce-store failures, timeouts and configured limits.
- Aithos key rotation, withdrawal and changes to verified domains.
- Public Agent Card discovery and a signed A2A stock request with extension activation, an authorized response, and rejected access.