From 0c283a558dca19ffe40659d2bef50eb34e80091b Mon Sep 17 00:00:00 2001 From: Perci <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz> Date: Sat, 15 Aug 2026 07:31:58 -0400 Subject: [PATCH] docs(nips): specify trusted edge profile Co-authored-by: Perci <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz> Signed-off-by: Perci <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz> --- docs/nips/NIP-FI-EDGE.md | 379 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 371 insertions(+), 8 deletions(-) diff --git a/docs/nips/NIP-FI-EDGE.md b/docs/nips/NIP-FI-EDGE.md index bee9be15c..e0d116f1e 100644 --- a/docs/nips/NIP-FI-EDGE.md +++ b/docs/nips/NIP-FI-EDGE.md @@ -2,13 +2,376 @@ `draft` `optional` -> SKELETON — text owner: Perci. Source: RESEARCH/NIP_FI_9999_TRANSPORT_WIRE_DESIGN.md. - ## Scope -Registered edge adapters; trusted-proxy-hmac-v2 envelope + canonicalization; -authorization_domain_id derivation (exact 16 RFC 9562 UUID bytes, network order); -proof_transport_code registry + extension procedure; key rotation; nonce replay; -body acquisition bounds (deny-before-hashing, EOF-complete); normative test-vector -suite with full intermediates; proxy provenance traces. Header-trust-without- -provenance is nonconformant. +This profile lets a trusted enterprise edge deliver federated assertion evidence to +a NIP-FI verifier. It defines two constructions: + +- `trusted-proxy-hmac-v2`, a portable request-bound HMAC envelope; and +- a private authenticated-edge assertion adapter, for platforms that provide an + equivalent closed trust boundary without the stock envelope. + +NIP-FI-EDGE is optional. A deployment can implement NIP-FI core using only +`client-attached`. Claiming this profile does not weaken core assertion validation, +independent Nostr proof, binding, lifecycle, policy, final-admission, or lease rules. +The key words **MUST**, **MUST NOT**, **REQUIRED**, **SHALL**, **SHALL NOT**, +**SHOULD**, **SHOULD NOT**, **RECOMMENDED**, **NOT RECOMMENDED**, **MAY**, and +**OPTIONAL** are to be interpreted as described in BCP 14 when, and only when, +they appear in all capitals. + +`transport_contract_id` and every identifier serialized by this document are +interoperability-critical. Local adapter revision identifiers and key identifiers +are deployment-local and MUST NOT appear in public discovery. + +## Common trusted-edge requirements + +Server-owned listener, route, and authorization-domain configuration selects exactly +one edge profile before protected traffic is accepted. Request evidence cannot +select, negotiate, or downgrade that profile. Missing, repeated, comma-combined, +malformed, oversized, mixed-profile, or profile-inconsistent evidence denies without +fallback to `client-attached` or another edge profile. + +Every trusted edge MUST: + +1. strip every inbound copy of each assertion, identity, authorization, capability, + provenance, and client-peer field before inserting its own fields; +2. cryptographically authenticate the immediate edge to the accepting origin and + isolate the origin from direct or alternate ingress; +3. integrity-protect every request component used by authorization; +4. apply a positive finite provenance deadline that is included in final admission + and every resulting lease; +5. validate a closed upstream identity and authorization claim set and produce the + same normalized assertion result required by core; +6. preserve the server-resolved domain, operation, resource, method, authority, + path/query, body semantics, proof transport, and Nostr actor key through final + admission; and +7. keep assertions, credentials, signatures, MACs, raw client addresses, and private + claims out of URLs, public protocol output, logs, metrics, and traces. + +Header presence, source address, private-network location, hostname, or reachability +alone is not provenance. Accepting unsigned identity or capability headers, or +accepting signed headers without authenticating and isolating the immediate caller, +is nonconformant. + +An adapter's reviewed contract MUST identify its accepting origins, direct-origin +controls, field-stripping point, immediate-caller authentication, protected request +components, upstream assertion and policy validation, freshness bounds, independent +Nostr-proof path, compromise impact, and conformance evidence. It MUST deny when any +part of this boundary is absent or unreadable. + +### Authenticated-edge assertion adapters + +A deployment MAY install a private authenticated-edge adapter instead of HMAC-v2. +The adapter MUST satisfy all common requirements and demonstrate together: +origin isolation, cryptographically authenticated immediate caller, inbound-field +stripping, integrity of the complete authorization-relevant request, bounded +assertion and policy freshness, no direct-origin fallback, and the core final- +admission path with independent Nostr proof. + +The adapter maps only its closed, validated claim set into the normalized result. +An opaque edge token is acceptable only inside this complete contract; opacity does +not make an unchecked header authoritative. Vendor names, issuer details, caller +identities, private field names, capability semantics, and adapter identifiers MUST +NOT appear in NIP-11 or portable examples. + +## `trusted-proxy-hmac-v2` + +The stock profile identifier is `trusted-proxy-hmac-v2`. Core computes the +`transport_contract_id` from a canonical contract that includes this profile's exact +wire format, protected components, replay rules, deadline rules, configured code +meanings, and adapter semantics. Changing any of those inputs produces a different +contract identity; the profile identifier itself remains stable. The proxy strips +all inbound assertion, provenance, and client-peer fields and inserts exactly one of +each: + +```text +Nostr-Federated-Identity: Bearer +Nostr-Federated-Identity-Provenance: v2... +Nostr-Federated-Identity-Client-Peer: +``` + +The assertion field follows core's compact-JWS and size rules. `timestamp` is +canonical unsigned decimal without leading zeroes, except zero is `0`. `nonce` and +`mac` are canonical unpadded base64url. Padding, the standard base64 alphabet, +ignored whitespace, or another encoding denies. The proxy generates a fresh nonce +containing at least 128 bits from a cryptographically secure random source. The +decoded MAC is exactly 32 octets. Finite field and decoded-nonce maxima are applied +before decoding, replay lookup, hashing, or allocation. + +`client-peer` is at most 64 ASCII octets. IPv4 uses dotted decimal with no leading +zeroes. IPv6 uses lowercase RFC 5952 text. The edge converts an observed IPv4-mapped +IPv6 address to canonical IPv4 before constructing the field; a textual mapped IPv6 +field is noncanonical. Empty, repeated, comma-combined, whitespace-padded, non-IP, +or noncanonical values deny. After verification, the verifier MAY retain only a +domain-separated keyed digest of this value in bounded private state. + +The profile uses HMAC-SHA-256 with a deployment secret containing at least 256 bits. +Let `LP(x) = uint64be(len(x)) || x`, where length is in octets. The literal prefix is +14 ASCII octets and is not length-prefixed. The pre-MAC input is exactly: + +```text +"NIP-FI-PROXY-2" || +LP(timestamp_u64be) || LP(nonce_bytes) || LP(SHA256(jwt_ascii)) || +LP(authorization_domain_id) || +LP(method_ascii) || LP(authority_ascii) || LP(path_and_query_ascii) || +LP(SHA256(payload_octets)) || LP(proof_transport_octet) || LP(client_peer_ascii) +``` + +`mac = HMAC-SHA-256(secret, pre_mac_input)`. The transmitted `mac` is canonical +unpadded base64url of the raw 32-octet result. The verifier compares it in constant +time. + +### Canonical components + +- **Timestamp:** Parse canonical decimal into an unsigned 64-bit integer, rejecting + overflow, then serialize it as exactly eight-byte big-endian. Freshness checks are + separate from serialization. +- **Nonce:** Decode the exact canonical base64url field before serialization. +- **Assertion:** Hash the exact ASCII compact-JWS octets after the one space in + `Bearer `. No whitespace, Unicode, JSON, or base64 normalization is allowed. +- **Authorization domain:** Configuration contains a canonical lowercase, + hyphenated RFC 9562 UUID named `authorization_domain_uuid`. Parse its 32 displayed + hexadecimal digits into the exact 16 UUID octets in display/network order. For + example, `00112233-4455-6677-8899-aabbccddeeff` becomes + `00112233445566778899aabbccddeeff`. UTF-8 UUID text, hashing, truncation, + namespace derivation, mixed-endian GUID encoding, uppercase, and unhyphenated + configuration are forbidden. The UUID is generated once, is immutable for the + domain's lifetime, and is shared through authenticated proxy/verifier + configuration. Duplicate UUIDs among active domains MUST fail startup. +- **Method:** Use the exact uppercase ASCII method token after trusted route + resolution. Lowercase or noncanonical input denies; the verifier does not repair it. +- **Authority:** Use server-configured lowercase ASCII host plus explicit effective + decimal port. IPv6 uses brackets and RFC 5952. Userinfo, a trailing dot, an omitted + port, percent encoding, or an authority derived solely from `Host`, `Forwarded`, or + `X-Forwarded-Host` denies. +- **Path and query:** Use the exact post-rewrite ASCII origin-form. Empty path becomes + `/`; a present query includes `?`. Percent octets and hex case, an empty query, + repeated names, and parameter order are preserved. No decoding, sorting, + dot-segment removal, or re-encoding may occur after the edge snapshot. An + unaccounted rewrite denies. +- **Payload:** Hash the complete HTTP payload octets after transfer-coding removal and + before content-coding decompression. These are exactly the octets forwarded by the + edge and exposed to verification. HTTP framing, chunk delimiters, and trailers are + excluded; `Content-Encoding` is not decoded. A WebSocket upgrade uses the empty + payload. Any transformation after the protected snapshot is forbidden. +- **Proof transport:** Serialize exactly one assigned octet from the registry below. +- **Client peer:** Serialize the exact canonical ASCII field value. + +### Freshness, replay, and key rotation + +The deployment configures a positive finite `maximum_provenance_age` and a +non-negative finite `future_skew`. Evidence is live exactly when, using overflow-safe +comparisons: + +```text +timestamp <= now + future_skew +now < timestamp + maximum_provenance_age +``` + +Equality at the age bound is expired. A direct lease deadline is no later than +`timestamp + maximum_provenance_age` and every core assertion, proof, policy, and +state deadline. + +Absent, malformed, stale, future-dated, wrong-key, or mismatched provenance denies. +A v1 envelope denies. A verifier MAY try only a configured finite set of active +secrets. Rotation does not change nonce identity: replay uniqueness is scoped to +`(authorization_domain_id, trusted-proxy-hmac-v2, nonce)` and is independent of the +secret that verifies the MAC. A committed nonce is retained through at least +`timestamp + maximum_provenance_age`. + +Preparation consumes neither nonce nor Nostr-proof replay identity. Final admission +atomically consumes both with any enrollment, receipt, and authorization decision. +A failed or rolled-back admission consumes neither. Two concurrent admissions with +the same nonce commit at most one authorization. The proxy-to-verifier hop still +requires confidentiality and integrity. + +## Proof-transport code registry + +| Code | Meaning and allocation policy | +|---|---| +| `0x00` | Invalid; MUST deny. | +| `0x01` | NIP-42 connection proof. | +| `0x02` | NIP-98 HTTP proof. | +| `0x03` | Git smart-HTTP session proof profile. | +| `0x04` | Blossom proof profile. | +| `0x05`–`0x7f` | Unassigned; allocation requires a published stable specification. | +| `0x80`–`0xfe` | Private use under an explicit shared proxy/verifier contract only. | +| `0xff` | Reserved for a future extended encoding; invalid in HMAC-v2. | + +An allocation MUST define exact proof validation, request binding, freshness, replay +identity and window, and conformance vectors. Assigned semantics never change; an +incompatible meaning receives a new code. Unknown, unconfigured, or private-use +codes without the same configured contract at proxy and verifier deny. Private-use +codes MUST NOT be advertised as portable NIP-FI-EDGE interoperability. + +## Bounded payload acquisition + +Every protected `(authorization_domain_id, route, proof_transport_code)` tuple MUST +configure a finite `maximum_payload_octets` and finite per-request +`maximum_spool_octets >= maximum_payload_octets`. Zero is allowed only for a route +that requires an empty payload. Proxy and verifier configuration MUST agree and is +part of the transport contract. + +If trusted `Content-Length` exceeds the route limit, the edge denies before reading, +hashing, JWT verification, replay lookup, or authoritative mutation. For absent, +unknown, or streamed length, acquisition uses a bounded counter and spool and stops +on octet `limit + 1`. Incremental SHA-256 is allowed, but no digest or prefix can +authorize until EOF proves completeness. + +Spooling uses memory or access-controlled temporary storage with finite per-request +and aggregate quotas, cleanup on every outcome, no public or log output, and no reuse +across requests. Quota exhaustion fails closed and creates no nonce claim, proof +claim, receipt, lease, or application mutation. At or below the limit, the exact +captured payload is replayed unchanged. HMAC verification and core final admission +complete before application effects. Forwarding to a rollback-safe private spool is +not an application effect; forwarding to a parser, decoder, handler, or origin that +can act is. + +A content decoder, multipart parser, Git/Blossom handler, framework, or intermediary +that cannot expose and replay the exact stage defined above before effects cannot +claim HMAC-v2 for that route. It MUST use core `client-attached` or another specified +edge profile, never a partial-body MAC. + +## Normative HMAC-v2 vectors + +All vector integers and lengths are big-endian. Common values are: + +```text +secret_hex = 000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f +nonce_hex = 000102030405060708090a0b0c0d0e0f +nonce_base64url = AAECAwQFBgcICQoLDA0ODw +authorization_domain_uuid = 00112233-4455-6677-8899-aabbccddeeff +authorization_domain_id_hex = 00112233445566778899aabbccddeeff +jwt_ascii = eyJhbGciOiJFUzI1NiIsInR5cCI6Im5pcC1maStqd3QifQ.eyJpc3MiOiJodHRwczovL2lkLmV4YW1wbGUiLCJzdWIiOiIxMjMifQ.c2ln +assertion_digest_hex = 6103b52a52730bc065d65673247603a63c9810488c90d0ada3d8d227eee5285f +``` + +The fixture JWT represents a separately minted `nip-fi+jwt` assertion and is opaque +test input; its deliberately synthetic signature is not an assertion-validation +vector. Implementations MUST reproduce each field, complete pre-MAC input, +diagnostic input digest, raw MAC, and wire MAC exactly. + +### Vector 1: HTTP / NIP-98 / non-empty payload + +```text +timestamp_decimal = 1700000000 +timestamp_u64be_hex = 000000006553f100 +method_ascii = POST +authority_ascii = api.example:443 +path_and_query_ascii = /upload?part=1&part=2&x=%2F +payload_hex = 68656c6c6f0a +body_digest_hex = 5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03 +proof_transport_hex = 02 +client_peer_ascii = 203.0.113.9 +pre_mac_input_hex = 4e49502d46492d50524f58592d320000000000000008000000006553f1000000000000000010000102030405060708090a0b0c0d0e0f00000000000000206103b52a52730bc065d65673247603a63c9810488c90d0ada3d8d227eee5285f000000000000001000112233445566778899aabbccddeeff0000000000000004504f5354000000000000000f6170692e6578616d706c653a343433000000000000001b2f75706c6f61643f706172743d3126706172743d3226783d25324600000000000000205891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03000000000000000102000000000000000b3230332e302e3131332e39 +pre_mac_input_sha256 = df2870230d2170595dccd17d9e61a82282d8cd8b978ac18bff07419ed59091d5 +mac_hex = 761d3ecbf609f0f558b4a02a1a18a25070f3dbe89fce9cac59a80bce4436ade5 +mac_base64url = dh0-y_YJ8PVYtKAqGhiiUHDz2-ifzpysWagLzkQ2reU +provenance = v2.1700000000.AAECAwQFBgcICQoLDA0ODw.dh0-y_YJ8PVYtKAqGhiiUHDz2-ifzpysWagLzkQ2reU +``` + +### Vector 2: WebSocket / NIP-42 / empty payload / mapped peer + +The edge observed `::ffff:192.0.2.128` and emitted canonical `192.0.2.128`. + +```text +timestamp_decimal = 1 +timestamp_u64be_hex = 0000000000000001 +method_ascii = GET +authority_ascii = relay.example:443 +path_and_query_ascii = / +payload_hex = +body_digest_hex = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +proof_transport_hex = 01 +client_peer_ascii = 192.0.2.128 +pre_mac_input_hex = 4e49502d46492d50524f58592d32000000000000000800000000000000010000000000000010000102030405060708090a0b0c0d0e0f00000000000000206103b52a52730bc065d65673247603a63c9810488c90d0ada3d8d227eee5285f000000000000001000112233445566778899aabbccddeeff0000000000000003474554000000000000001172656c61792e6578616d706c653a34343300000000000000012f0000000000000020e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855000000000000000101000000000000000b3139322e302e322e313238 +pre_mac_input_sha256 = 67564d241499491b3ea53b31d6111fbc9efac37a294f6ce591519e4bf21b53e9 +mac_hex = f71a179a018637a0582cf3de39ccb7b976216c18ada312127d4c983c14af4b20 +mac_base64url = 9xoXmgGGN6BYLPPeOcy3uXYhbBitoxISfUyYPBSvSyA +``` + +### Vector 3: IPv6 authority and path/query byte preservation + +```text +timestamp_decimal = 1700000000 +timestamp_u64be_hex = 000000006553f100 +method_ascii = GET +authority_ascii = [2001:db8::1]:443 +path_and_query_ascii = /a%2Fb?b=2&a=1&a=0 +payload_hex = +body_digest_hex = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +proof_transport_hex = 02 +client_peer_ascii = 2001:db8::2 +pre_mac_input_hex = 4e49502d46492d50524f58592d320000000000000008000000006553f1000000000000000010000102030405060708090a0b0c0d0e0f00000000000000206103b52a52730bc065d65673247603a63c9810488c90d0ada3d8d227eee5285f000000000000001000112233445566778899aabbccddeeff000000000000000347455400000000000000115b323030313a6462383a3a315d3a34343300000000000000122f61253246623f623d3226613d3126613d300000000000000020e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855000000000000000102000000000000000b323030313a6462383a3a32 +pre_mac_input_sha256 = 8a93a29c4ac30b0f2551d346d0636040b639bb1f109d287e93ce44ddaed73e33 +mac_hex = df2936f81d752f3d6bac2a36d3381c38db2c9abc3570236cb121274ad34a6161 +mac_base64url = 3yk2-B11Lz1rrCo20zgcONssmrw1cCNssSEnStNKYWE +``` + +### Serialization and negative matrix + +The following timestamp values MUST serialize as shown before freshness evaluation: + +| Decimal | `uint64be` hex | +|---:|---| +| `0` | `0000000000000000` | +| `1` | `0000000000000001` | +| `255` | `00000000000000ff` | +| `256` | `0000000000000100` | +| `18446744073709551615` | `ffffffffffffffff` | + +`00`, `01`, `+1`, surrounding whitespace, negative values, and +`18446744073709551616` deny before MAC comparison. The maximum value above is an +encoding vector; ordinary freshness policy will reject it. + +Every implementation MUST run these normative negative cases: + +| Class | Required cases and result | +|---|---| +| Envelope | Absent/repeated/comma-combined fields, `v1`, missing/extra component, padding, alternate alphabet, nonce below 16 octets or above configured max, and MAC lengths 31 or 33 all deny. | +| Domain | Uppercase/nonhyphenated UUID config fails configuration; mixed-endian UUID bytes or any one-bit domain transplant fails the baseline MAC; duplicate active UUID fails startup. | +| Request | Mutating assertion, method, authority, path/query, body, proof code, or peer while retaining Vector 1's MAC denies. | +| Path | `%2F`→`%2f`, decoding to `/`, reordering repeated query values, or adding/removing an empty `?` fails the baseline MAC. | +| Authority | Unbracketed or non-RFC-5952 IPv6, uppercase host, trailing dot, or missing port denies before MAC comparison. | +| Peer | Textual `::ffff:192.0.2.128`, padded IPv4, uppercase/noncanonical IPv6, or whitespace denies before MAC comparison. | +| Proof | `0x00`, `0xff`, unknown stock code, or private code without a shared configured contract denies. | +| Body | Known and unknown lengths `0`, `limit-1`, and `limit` may proceed only after EOF; `limit+1`, disconnect before EOF, aggregate-quota exhaustion, or any post-snapshot transform denies with no replay or authoritative mutation. | +| Replay | Concurrent final admissions of one valid envelope commit at most one; preparation and failed final admission consume none; secret rotation does not create a new nonce namespace. | +| Fallback | Direct ingress, mixed evidence, and failed HMAC never retry as `client-attached` or another adapter. | + +## Discovery and conformance + +A relay that completely implements the stock profile MAY list +`trusted-proxy-hmac-v2` in NIP-11's NIP-FI transport discovery. It MUST NOT advertise +private adapters, keys, domains, field names, or code contracts. Claiming FI-EDGE +requires every configured edge profile to pass the applicable core conformance suite +and these profile traces: + +| Trace | Required oracle | +|---|---| +| `FI-TRACE-PROXY-SPOOF` | Direct ingress, unsigned/header-only identity, unauthenticated caller, or invalid provenance denies without fallback. | +| `FI-TRACE-PROXY-REPLAY` | Two HMAC-v2 final admissions using one nonce commit at most one; preparation consumes neither. A private adapter proves its declared replay semantics. | +| `FI-TRACE-PROXY-CROSS-REQUEST` | Each protected component mutation denies. HMAC-v2 covers assertion, domain, method, authority, path/query, complete body, proof transport, and peer. | +| `FI-TRACE-EDGE-BODY-BOUNDS` | Known and streamed boundary cases prove bounded work/storage, EOF completeness, cleanup, and no pre-authorization effect. | +| `FI-TRACE-EDGE-KEY-ROTATION` | A finite active-key set accepts an intended overlap without allowing nonce reuse or an unknown key. | + +The conformance record binds the exact implementation, adapter, deployment, +assertion policy, transport contract, configured code meanings, and vector revision. +Two HMAC-v2 implementations interoperate only when they reproduce all valid vector +bytes exactly, reject every negative, agree on UUID and code configuration, and +preserve atomic replay and bounded complete-body behavior. + +## Security considerations + +HMAC-v2 limits header spoofing, replay, and cross-request transplantation only when +its secret remains confidential, the edge snapshots the final routed request, the +origin authenticates that edge, and final admission atomically consumes replay state. +It does not replace TLS or independent Nostr proof. A compromised edge or shared +secret can forge federated evidence within its configured domains; use distinct +secrets and UUIDs to limit blast radius. + +Authenticated-edge adapters intentionally shift more proof to deployment controls. +A hostname, private network, or opaque token is not an equivalent construction unless +the complete boundary obligations above are demonstrated. Body buffering and replay +state are attacker-controlled resource surfaces, so all field, payload, spool, +aggregate, key-set, and retention bounds fail closed.