## What this is An alternative shape for the NIP-FI document family, built on top of #5946 at its current head `772ba7a72` (so it carries R1–R12 and diffs cleanly against it). Thesis: the current text *argues* where a standard should *state*, and is silent where a reader needs one paragraph of *why*. This PR makes every section either a rule or labeled as not one, and fixes four correctness items found in review. Base: `772ba7a72` (= `eva/nip-fi-comprehensive`). Eight FF commits, no rebase. Three files. Oracle census unchanged at **58** (30/6/4/11/7). **Review lap (Wren, Dawn, Eva) folded in as FF commits `be72f0a5d`…`db35013c1`** — see "Review findings applied" below. ## Changes ### NIP-FI-CONF — 4,325 → 3,088 words (−29%), no obligation lost - Denial fixtures 1,516 → ~740 words. Same 16-row table. Enumeration-agreement checks 4 → 3 (checks 1 and 2 quantified over the same set). Design history ("Two of these checks shipped red…", the `Www-Authenticate` canonicalization paragraph, "one layer down", "a slow way to discover…") removed from normative text. - Interoperability exit test 934 → ~620 words. Same fixture contents, same compared objects, same deferral rule, same negative control. - Release gate restructured as a list; identical conjuncts. - **BCP 14 keyword census 37 → 36.** Every `MUST`/`MUST NOT`/`MAY`/`REQUIRED` sentence in the old text was fuzzy-matched to the new text; the two that don't map are (a) "Their public responses MUST compare byte-identical to each other, not merely equal in prefix or status" — fully subsumed by the anonymity comparison one paragraph later, and (b) the standalone "The suite MUST include a negative control:" whose obligation is retained in the **Negative control** paragraph. Reviewers: please try to find a third. ### NIP-FI core - **New non-normative section "Relationship to other work"** (~190 words): DPoP / mTLS `cnf` with the NIP-42/98 event as the proof; durable binding vs per-token claim; why 1:1 vs WebAuthn's N credentials; why two contract IDs plus dependency versions; RFC 6750 error codes; RFC 9421 / SigV4 family for the edge MAC. The "Thus two contract identities…" argument paragraph moved here from the normative snapshot section. Three source links added. - **Pin:** `skew`, `maximum_assertion_age`, `maximum_status_age` declared as configured finite values (missing → deny). Previously used at :177-178/:266/:278 and never declared; EDGE already did this for its own. - **Pin:** NIP-98 `payload` hashes one defined referent — "body bytes: the complete content after transfer decoding and before any content decoding" — the same stage EDGE MACs as `payload_octets`. Previously :315 said "bytes consumed by the application" and :320 said "body bytes received", which differ under `Content-Encoding`. **This is a semantic shift, not a clarification** (Eva's A3): under the old "consumed" reading a `Content-Encoding` flip in flight was caught by hash mismatch; under this pin the same octets hash equal, so the defense moves onto the server-resolved-body-semantics rule (core:305-308, EDGE:176-178) and the EDGE Metadata negative row (:367), which are now load-bearing for it. All four body-wording sites say "substitution of the protected octets", not "transformation", so content decoding after validation is permitted. - **Pin:** a denial decided on a WebSocket upgrade, before any NIP-42 proof exists, is the HTTP table row sent instead of `101`; post-connect denials are the Nostr text. `FI-TRACE-DENIAL-ORACLE` requires each row "on every surface where its condition can be decided" (a private-state row needing the proof is not decidable at upgrade — Wren). CONF exit list aligned. ### NIP-FI-EDGE - **R12 exception class-scoped** (Dawn's finding): "except an independent Nostr proof validated on its own signature, such as the NIP-98 event in `Authorization` or the NIP-42 event after connect" instead of naming the field. Members are illustrative ("such as") so `0x03`/`0x04` can conform once their contracts publish. Prohibition widened to "request or connection component" — Dawn's model shows this closes 6144 unprotected-connection derivations to 0. Requirement 3 gets the matching carve-out. - **`Authorization` byte-identity gains an oracle — as a two-point witness, not a denial.** EDGE:62-65 makes altering it nonconformant; no oracle named it. First attempt said "altered … denies"; Eva (A1) and Dawn's `a1_oracle.py` showed that's false as a universal — an edge substituting a fresh, valid NIP-98 event from the same actor *admits*, because `Authorization` is outside the MAC by R12's design. `FI-TRACE-PROXY-CROSS-REQUEST` now requires the bytes at final admission to equal the client-sent bytes, witnessed at both points, and names the mutant that must fail it (valid-proof substitution). The Request matrix row no longer claims a denial for it. ## Review findings applied (all FF commits on `dcd0b2ff2`) | # | Finder | Finding | Fix | |---|---|---|---| | W1 | Wren | Denial oracle demanded every private row on all three surfaces; undecidable pre-proof | core:586 "on every surface where its condition can be decided"; CONF:235-237 aligned | | W2 | Wren | EDGE:168 and :372 still forbade any post-snapshot "transformation" | "substitution of the protected octets denies" at all four sites (core:315, :573; EDGE:168, :372) | | W3/A2 | Wren, Eva | CONF dropped exit-evidence obligation (old :393) | CONF:287 "Exit evidence includes the exchanged artifacts and each implementation's statement of independence." | | W4/A6 | Wren | Two-device sentence prescribed custody outside the standard | core:605 "additional devices do not create additional active bindings." | | D1/A4 | Dawn, Eva | Em-dash members read exhaustive; `0x03` couldn't conform | "such as" | | D2 | Dawn | "a missing value denies" had no detecting oracle | `FI-TRACE-ASSERTION-VALIDATION` gains "missing-configuration" negative | | A1 | Eva, Dawn | `Authorization` oracle asserted a false denial universal | two-point byte witness + valid-substitution mutant (above) | | A5/W5 | Eva, Wren | Promotion guard retargeted by 4→3 collapse, now inert | sentence deleted; check 2's disjointness already fails promotion until the allowlist entry is removed, check 1 validates the class. CONF MUST NOT 9→8, deliberate | | D3 | Dawn | Replacement sentence said "only check 2's disjointness fails" — false in 2/4 promotion cases (check 1 fails too when classes disagree); "then" implied an ordering the independent conjuncts don't have | both words dropped (`624429bd8`) | Not taken this lap (predate this PR): EDGE:180 `maximum_provenance_age` missing-config oracle; `0x04` Blossom row says "authorization event" not "Nostr", so the class exception can't reach it until its contract is written. ## Not changed LIFECYCLE, DELEG, MODEL: zero bytes. Oracle tables, HMAC vectors, negative matrix, denial table: zero bytes beyond the two cells named above. ## Verification - `git rev-parse HEAD` == `624429bd8` in the shell that produced the census/keyword numbers below; section word counts above are from `dcd0b2ff2` (CONF now 3,1k). - Oracle census: `rg -c '^\| \`FI-[A-Z0-9-]+\` \|'` per file → 30/6/4/11/7. - Distribution markers: 0 hits across all six docs and the commit message. - Keyword census and sentence mapping: script in the PR thread on request. ## Open questions 1. ~~Two-device sentence~~ — closed by Wren's wording. 2. ~~R12 widening~~ — Dawn's model: net stricter than `772ba7a72`. 3. ~~CONF floor~~ — Eva measured Evidence rules at 317 words, every sentence an obligation. Floor reached. 4. ~~A3 body-referent shift~~ — accepted by Wren and Eva at `db35013c1`: the defense is a listed oracle row, hence inside the mutation regime. ## Verdicts At `624429bd8`: Wren 9/9/9/9 · Eva 9/9/9/9 · Meli 9/9/9/9, each re-pinned at this exact head. Dawn's three instruments (conf/a1/r12) rc=0 at this head. CI 14 pass / 0 fail / 9 skipped. Zero known falsehoods in the text. Census partition note (Dawn): "58" is unique oracle IDs per document across FI-TRACE/CONF/LC/DELEG prefixes; partitioned per prefix across all docs including FI-INV the same corpus totals 75. Quote the partition with the number. --------- Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz> Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
25 KiB
NIP-FI-EDGE: Trusted Edge Profile
draft optional
Scope
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.
Every identifier this document serializes on the wire — header names, the
profile identifier, provenance envelope fields, and proof transport codes — is
interoperability-critical. transport_contract_id remains deployment-local as
core classifies it: its value is opaque outside a deployment, while the
canonical contract semantics this profile contributes to it are normative and
fixed here. 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:
- strip every inbound copy of each
Nostr-Federated-Identity,Nostr-Federated-Identity-Provenance, andNostr-Federated-Identity-Client-Peerfield, and of every other edge profile's assertion, identity, capability, provenance, and client-peer field, before inserting its own fields. A trusted edge MUST NOT remove or modify theAuthorizationfield, which remains reserved for the independent NIP-98 proof and MUST reach final admission unmodified; - cryptographically authenticate the immediate edge to the accepting origin and isolate the origin from direct or alternate ingress;
- integrity-protect every request component used by authorization other than an independent Nostr proof, which is protected by its own signature;
- apply a positive finite provenance deadline that is included in final admission and every resulting lease;
- validate a closed upstream identity and authorization claim set and produce the same normalized assertion result required by core;
- preserve the server-resolved domain, operation, resource, method, authority, path/query, body semantics, proof transport, and Nostr actor key through final admission; and
- 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. A trusted edge that strips, rewrites, or reorders the
Authorization field, or that admits a proof-transport-0x02 request whose
Authorization field did not arrive at the verifier byte-identical to the
client-sent value, 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:
Nostr-Federated-Identity: Bearer <compact-JWS>
Nostr-Federated-Identity-Provenance: v2.<timestamp>.<nonce>.<mac>
Nostr-Federated-Identity-Client-Peer: <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:
"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-aabbccddeeffbecomes00112233445566778899aabbccddeeff. 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, orX-Forwarded-Hostdenies. - 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-Encodingis not decoded. A WebSocket upgrade uses the empty payload. Substitution of the protected octets after the snapshot denies. - Proof transport: Serialize exactly one assigned octet from the registry below.
- Client peer: Serialize the exact canonical ASCII field value.
No authorization decision, target, resource, capability, or effect selector
derives from any request or connection component outside the protected pre-MAC
components, except an independent Nostr proof validated on its own signature,
such as the NIP-98 event in Authorization or the NIP-42 event after connect,
which the MAC does not protect; body interpretation follows the server-resolved
body semantics, never unprotected transport metadata such as Content-Type or
Content-Encoding.
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:
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.
On a route that requires edge provenance, absent or incomplete provenance —
including provenance that omits the proxy-authenticated end-client peer — maps
to the missing_evidence public class, regardless of whether an assertion is
present. Provenance that is present and complete but fails verification maps to
evidence_rejected.
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: the proxy verifies a session-scoped Nostr authorization for a Git smart-HTTP request before forwarding. Reserved; allocation completes on publication of its transport contract (see below). |
0x04 |
Blossom media proof: the proxy verifies a Blossom media-HTTP authorization event for the request before forwarding. Reserved; allocation completes on publication of its transport contract (see below). |
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.
Codes 0x03 and 0x04 are reserved to fix their meanings and prevent
reassignment; their transport contracts are not yet published, so their
allocations are not complete. Until the contract for such a code is published,
the code is valid only under an explicit shared proxy/verifier contract,
exactly as for private use, and MUST NOT be presented 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:
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
(FI-TRACE-EDGE-VECTORS).
Vector 1: HTTP / NIP-98 / non-empty payload
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.
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
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. |
| Metadata | Mutating Content-Type or Content-Encoding in flight changes no authorization decision, target, capability, or effect selector; a request whose server-resolved body semantics no longer hold 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 substitution of the protected octets 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 add exactly
"edge_transports": ["trusted-proxy-hmac-v2"] inside the top-level NIP-11
federated_identity object. edge_transports is an array of unique ASCII string
profile identifiers in ascending bytewise order; this document assigns only the
single value shown. A relay that does not completely implement the stock profile
MUST omit the member. It MUST NOT advertise private adapters, keys, domains, field
names, or code contracts. No request may select behavior from this discovery
member; server-owned configuration selects the edge profile. Claiming FI-EDGE
requires every configured edge profile to pass the applicable core conformance suite
and these profile traces:
| Trace | Required oracle |
|---|---|
FI-TRACE-EDGE-VECTORS |
Reproduce all three normative vectors field-for-field, including each complete pre-MAC input, diagnostic input digest, raw MAC, and wire MAC; reproduce all five timestamp serialization rows; every listed serialization and negative-matrix case produces its required denial or configuration failure. |
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. On a 0x02 route the Authorization bytes at final admission equal the client-sent bytes, witnessed at both points; an edge that substitutes a valid proof from the same actor fails the witness. |
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.