Files
buzz/docs/nips/NIP-FI-EDGE.md
Tyler d97780b477 docs(nips): comprehensive NIP-FI — core + claimable profiles (EDGE/LIFECYCLE/DELEG/CONF) (#5946)
## What

Comprehensive NIP-FI against `main`: one normative core plus four
separately
claimable profiles, replacing the single-document structure of #3726
(which was
based on #1485's branch, not `main`). Six documents, 1,975 lines, docs
only.

- **`NIP-FI.md` (core, 632 lines)** — issuer-qualified identity `(iss,
sub)`,
independent Nostr proof, client-attached assertion, partial bijection
with
durable tombstones, atomic final admission, bounded leases, private
denials
  with a closed response vocabulary, retire/revoke/rotate, two contract
identities (`assertion_policy_id`, `transport_contract_id`), per-policy
  `skew` / `maximum_assertion_age` / `maximum_status_age` with missing
configuration denying, a closed token-class rule (`at+jwt`,
`nip-fi+jwt`,
  named compatibility; ID tokens always deny), declared freshness class
  (`offline-jwt` | `current-status`), server-declared body authorization
  relevance (NIP-98 payload-binding fix, including a `payload` tag on an
irrelevant-body operation), BCP 14, "equivalent" defined over identity /
bounds / provenance classes, a compact non-normative worked wire
example,
  and a non-normative comparison with DPoP, mTLS-bound tokens, and HTTP
  Message Signatures. FI-INV-01..16 are normative core text. The
behavioral-oracle table lists exactly 30 oracle IDs, one per row, with
no
  shorthand.
- **`NIP-FI-EDGE.md`** — trusted-edge surface: the
`trusted-proxy-hmac-v2`
envelope + canonicalization, or a private authenticated-edge adapter
under a
reviewed contract; `authorization_domain_id` derivation (exact 16 RFC
9562
UUID bytes); `proof_transport_code` registry (0x01 NIP-42, 0x02 NIP-98;
  0x03 Git smart-HTTP and 0x04 Blossom reserved pending their transport
contracts; 0x05–0x7f unassigned pending published stable specifications)
  + extension procedure; body-acquisition bounds; three normative test
vectors. An independent Nostr proof (the NIP-98 event in
`Authorization`,
  which reaches the verifier byte-identical, or the NIP-42 event after
connect) is the only decision input outside the MAC; absent or
incomplete
  provenance on an edge-required route is `missing_evidence`,
  present-but-failing provenance is `evidence_rejected`.
  Header-trust-without-provenance is nonconformant.
- **`NIP-FI-LIFECYCLE.md`** — provision / disable / re-enable /
administrative
expiry (`binding_not_after`) / pending-replacement lineage, one
conformance
trace per privileged transition; every binding-creating transition
declares
whether it continues or establishes a grant; a private-condition table
for
  CONF enumeration agreement.
- **`NIP-FI-DELEG.md`** — delegated agents; explicit temporal boundaries
matching core's inclusive-`nbf`/exclusive-`exp` idiom, with the
delegated
`skew` configured by this profile; lease deadline anchored to the lease
  issue instant; strict path separation — a delegated request carries no
assertion or provenance field, so it cannot traverse an edge-provenance
  route and uses ingress on which NIP-FI-EDGE is not required.
- **`NIP-FI-CONF.md`** — conformance evidence: an immutable claim tuple
  including the governing document revision and exit fixture digest; the
  complete 16-row denial-fixture enumeration with three mechanical
  enumeration-agreement checks; mutation adequacy with a countable
  denominator — one retained killed mutant per literal oracle-table row
(30 core + 6 EDGE + 11 LIFECYCLE + 7 DELEG + 4 CONF = 58), rows selected
structurally by their first cell, never by section title, with the
release
gate and CONF's own oracle rows stated in the same listed-oracle terms
and
  a mutant defined for CONF's own report- and suite-subject oracles; an
  interoperability exit test compared over signing inputs (per-transport
NIP-01 serialization for the NIP-98 and NIP-42 proofs; decoded protected
header and claims as JSON values for the assertion), with a shared exit
  fixture pinning complete pre-signature header/claim JSON and complete
unsigned event fields for both transports, and mandatory negative
controls.
  `FI-CONF-INTEROP-EXIT` is `deferred` with reason
`no-independent-implementation` until a second independent
implementation
  exists; the canonical fixture is editor-authored at
`docs/nips/fixtures/nip-fi-conf-exit.json` and is **not in this PR** —
until
it is published a claim records `pending-canonical-fixture`, valid only
  while the exit test is deferred. Explicit not-applicable dispositions,
including `offline-jwt` deployments for the two current-status oracles
and
  absence of a revocation-bounded external capability projection for
  `FI-TRACE-CAPABILITY-REVOCATION`.
- **`NIP-FI-MODEL.md`** — non-normative companion; defines no
requirement or
  conformance claim and is not claimable.

## Why

The prior draft rated 9 (soundness) / 6 (minimalness) / 7 (elegance) /
7 (correctness) in adversarial + comparative review. This restructure
keeps the
two-invariant spine untouched, makes everything else a claimable
profile, and
collapses five stacked versioning mechanisms into two contract
identities.

Mutation adequacy counts one mutant per literal oracle-table row — a set
two
implementers enumerate identically — instead of "each normative
requirement,"
which had four defensible readings.

Resolved product calls (owner-approved):
1. Enrollment/denial posture is private — boolean enrollment discovery,
TOFU
extension claim not self-advertised, `key_mismatch →
authorization_denied`
   joins the denial anonymity set, and replayed evidence is classed
   `authorization_denied` so resubmission reveals nothing about commit.
2. Revocation honesty — only `current-status` deployments may advertise
an
   unconditional residual-revocation bound; `offline-jwt` advertises
unbounded/unknown. Access tokens keep RFC 9068 `at+jwt`; `nip-fi+jwt` is
   reserved for a separately minted Buzz assertion.

## Acceptance bar

- Nothing in core is deletable without losing a stated core guarantee.
- From the core document plus the CONF exit fixture, a second
implementer can
produce a valid request equal over the request compared object (signing
  inputs), and a byte-exact public denial per class — no reference
  implementation.
- Every oracle-table row ships a retained killed mutant satisfying only
the
  entry it was selected for.
- Both deployment profiles (trusted proxy = EDGE, client-held OIDC =
core
  client-attached) pass the same lifecycle conformance suite.

## Status

Ready at head e720a5cda. Every revision below is on this branch: the
2026-08-17 and 2026-08-18 review laps (Wren, Dawn, Perci, Sami, Mari,
Quinn)
closed at 513e03b06, 17d455a9b, and 4f913a813; the 2026-08-20 external
line-by-line review (R1–R10, R12) closed across 56e74141e..772ba7a72;
the
2026-08-20/21 adversarial lap (#6437) squash-merged as b8db13d88; the
round-3
external review (R13, R14), the DELEG×EDGE composition note, and three
terminology nits closed at e720a5cda; R11 is this description. Oracle
census: 58 (30 core, 6 EDGE, 11 LIFECYCLE, 7 DELEG, 4 CONF).

Known follow-ups, filed after merge and out of scope here: adapter-only
edge
deployments and FI-EDGE claimability; an EDGE private-condition table
for
CONF's enumeration-agreement check; an enumerable definition of the
positive/negative oracle sets used by the global mutation controls;
NIP-OA's clock-free verification versus NIP-FI-DELEG's wall-clock
expiry.

Supersedes #3726 as the spec vehicle; #1485 remains the design-history
anchor.

---------

Signed-off-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Signed-off-by: Max <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz>
Signed-off-by: Perci <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz>
Signed-off-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz>
Signed-off-by: Dawn <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@buzz.block.builderlab.xyz>
Signed-off-by: Tyler Longwell <tlongwell@squareup.com>
Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Co-authored-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Co-authored-by: Max <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz>
Co-authored-by: Perci <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz>
Co-authored-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz>
Co-authored-by: Dawn <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@buzz.block.builderlab.xyz>
Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
2026-08-21 17:18:24 -04:00

25 KiB
Raw Blame History

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:

  1. strip every inbound copy of each Nostr-Federated-Identity, Nostr-Federated-Identity-Provenance, and Nostr-Federated-Identity-Client-Peer field, 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 the Authorization field, which remains reserved for the independent NIP-98 proof and MUST reach final admission unmodified;
  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 other than an independent Nostr proof, which is protected by its own signature;
  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. 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-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. 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.