Skip to content

Latest commit

 

History

History
350 lines (275 loc) · 16.5 KB

SPEC.md

File metadata and controls

350 lines (275 loc) · 16.5 KB

Aithos Request Authentication

Version: 1.0

Date: 2026-09-05

1. Protocol

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.

2. Agent resolution

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.

Aithos resolver

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:

  1. Read /v1/agents/{agent_id} from its configured registry origin.
  2. Require the returned agentId to match, status to be ACTIVE, and key_thumbprint to belong to authorizedKids.
  3. Read /v1/agents/{agent_id}/jwks.json from that origin and select exactly one matching kid. Require kty="EC", crv="P-256", valid coordinates, and a recomputed RFC 7638 thumbprint equal to key_thumbprint.
  4. Return that key, the record's agentId, and its domains, mapping certifiedAt and lastCheckedAt to 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.

3. Access policy

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.

4. Signed HTTP request

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.

5. Freshness and replay

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.

6. Request processing

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:

  1. Require HTTPS at the externally visible boundary.
  2. Validate headers, signature selection, coverage, parameters and freshness.
  3. Resolve keyid, validate the returned key and verify the signature.
  4. Read the bounded content and compare its SHA-256 digest to Content-Digest.
  5. Recheck freshness and evaluate the access policy.
  6. Atomically consume the nonce until created + 61.
  7. 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.

7. Failures

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.

8. A2A integration

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.

9. Python integration

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.

10. Conformance

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.

11. References