> ## Documentation Index
> Fetch the complete documentation index at: https://docs.fau.run/agent-docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Request signing

> RFC 9421 signatures every /mcp call must carry.

Every `POST /mcp` must be signed. Unsigned, expired, replayed, or wrongly keyed
requests never reach a tool. `GET /.well-known/fau-node` is **not** signed.

## Covered components

Always cover:

* `@method`
* `@target-uri` (full public URL, including scheme — e.g. `https://n….relay.fau.run/mcp`)
* `@authority` (hostname only)
* `content-digest`

## Signature parameters

| Param     | Requirement                                          |
| --------- | ---------------------------------------------------- |
| `created` | Unix seconds; future skew ≤ 60 s                     |
| `expires` | ≤ `created + 300`                                    |
| `keyid`   | `"<agent-domain>#<kid>"`                             |
| `alg`     | `ed25519`                                            |
| `nonce`   | Unique; node rejects reuse inside the expires window |
| `tag`     | `"fau-agent"`                                        |

Default lifetime in the reference signer is **120 s** (still ≤ 300).

## Content-Digest

SHA-256 of the **raw body bytes**, RFC 9530 form with **standard Base64** (not
base64url):

```http theme={null}
Content-Digest: sha-256=:DIGEST_BASE64:
```

## Example headers

Label is `sig1` in the reference implementation:

```http theme={null}
Content-Type: application/json
Accept: application/json, text/event-stream
Content-Digest: sha-256=:47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=:
Signature-Input: sig1=("@method" "@target-uri" "@authority" "content-digest");created=1760000100;expires=1760000220;keyid="assistant.example#k1";alg="ed25519";nonce="abc…";tag="fau-agent"
Signature: sig1=:BASE64_SIGNATURE_BYTES:
```

`Signature` / digest use **standard Base64** with surrounding colons. Key documents and
queue envelopes use **base64url**.

## Pseudocode

```text theme={null}
body = utf8(compact_json)
digest_b64 = standard_base64(sha256(body))
content_digest = "sha-256=:" + digest_b64 + ":"

created = now_unix()
expires = created + 120
nonce = random_urlsafe(16)
keyid = domain + "#" + kid

signature_base = rfc9421_base(
  components: ["@method", "@target-uri", "@authority", "content-digest"],
  params: {created, expires, keyid, alg: "ed25519", nonce, tag: "fau-agent"},
  values: {
    "@method": "POST",
    "@target-uri": "https://n….relay.fau.run/mcp",
    "@authority": "n….relay.fau.run",
    "content-digest": content_digest
  }
)

sig = ed25519_sign(private_key, signature_base)
```

Normative edge cases live next to the verifier in
`node/src/fedagent/crypto/signatures.py` (`sign_request`).

## Rules that bite

* Agent domain must contain at least one dot (e.g. `assistant.example`), and must
  **not** equal the node hostname.
* Sign the **public** URL you will send on the wire. Redirects break signatures —
  call `/mcp` (or `/mcp/`) directly; do not follow 3xx.
* Body larger than **256 KiB** → HTTP `413 {"error":"payload_too_large"}`.
* Missing/bad signature → HTTP `401 {"error":"signature_invalid",…}` (not JSON-RPC).

## Capability token (optional)

```http theme={null}
Authorization: Bearer <token>
```

Tokens are optional. After phone approval the node usually remembers a **grant by
domain**; retry the signed tool call **without** a Bearer header. If you do send a
token, it must be bound to your domain and key fingerprint; an invalid token is
typically **ignored**, not a hard 401.
