Perci
2026-08-17 15:44:17 -04:00
committed by Eva
parent 9f2cb82ff3
commit 0c283a558d
+371 -8
View File
@@ -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 <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:
```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.