spec: §6.7.6 LINKIDENTIFY wire format + §11.6 prose
§6 specified the wire format of every Link control packet — LRRTT
(§6.4.2), KEEPALIVE (§6.7.1), LINKCLOSE (§6.7.3) — but had no
subsection for LINKIDENTIFY (`context = 0xFB`). §11.6 described it
only in prose, and called the payload "a signature over `link_id`" —
imprecise on two counts.
Per RNS 1.2.9 `RNS/Link.py:459-475` `Link.identify()`:
- Payload is `public_key(64) || signature(64)` (128 bytes total),
NOT just the signature.
- `signature` is over `link_id || public_key`, NOT over `link_id`
alone.
LINKIDENTIFY is a DATA packet on the active link, so it IS
link-encrypted like ordinary link DATA (unlike KEEPALIVE / link
PROOF, §6.7.1 / §6.5). Added explicit §6.7.6 subsection with the
wire layout, the responder-side parse path, and the two clean-room
pitfalls; corrected the §11.6 prose to match and point at the new
subsection.
Closes #12.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7a85385bb4
commit
41d2fd61ee
1 changed files with 54 additions and 1 deletions
55
SPEC.md
55
SPEC.md
|
|
@ -1599,6 +1599,59 @@ For a clean-room implementation that wants links to survive idle periods longer
|
||||||
5. On every inbound `LINKCLOSE`, decrypt, verify body equals `link_id`, transition to `CLOSED`.
|
5. On every inbound `LINKCLOSE`, decrypt, verify body equals `link_id`, transition to `CLOSED`.
|
||||||
6. On `CLOSED`, zero the session keys and cancel any in-progress Resources.
|
6. On `CLOSED`, zero the session keys and cancel any in-progress Resources.
|
||||||
|
|
||||||
|
#### 6.7.6 LINKIDENTIFY (`context = 0xFB`)
|
||||||
|
|
||||||
|
A separate Link control DATA packet — not part of the keepalive /
|
||||||
|
teardown cycle, but documented here alongside the other context-dispatched
|
||||||
|
Link control packets. Used by the initiator to prove which long-term
|
||||||
|
identity is making the request without re-running the Link handshake;
|
||||||
|
§11.6 covers the calling context on the NomadNet REQUEST path.
|
||||||
|
|
||||||
|
`Link.identify(identity)` (`RNS/Link.py:459-475` in RNS 1.2.9):
|
||||||
|
|
||||||
|
```python
|
||||||
|
def identify(self, identity):
|
||||||
|
if self.initiator and self.status == Link.ACTIVE:
|
||||||
|
signed_data = self.link_id + identity.get_public_key()
|
||||||
|
signature = identity.sign(signed_data)
|
||||||
|
proof_data = identity.get_public_key() + signature
|
||||||
|
proof = RNS.Packet(self, proof_data, RNS.Packet.DATA,
|
||||||
|
context = RNS.Packet.LINKIDENTIFY)
|
||||||
|
proof.send()
|
||||||
|
```
|
||||||
|
|
||||||
|
Wire body (128 bytes):
|
||||||
|
|
||||||
|
```
|
||||||
|
public_key(64) || signature(64)
|
||||||
|
```
|
||||||
|
|
||||||
|
- `public_key` is the initiator's **full 64-byte Identity public key**
|
||||||
|
— the same `get_public_key()` that LINKREQUEST and announces use,
|
||||||
|
the concatenation of the Ed25519 verification key and the X25519
|
||||||
|
encryption key per §3.
|
||||||
|
- `signature` is `identity.sign(link_id(16) || public_key(64))` — the
|
||||||
|
signature is over the link_id concatenated with the public_key,
|
||||||
|
NOT over `link_id` alone. The responder verifies with `public_key`
|
||||||
|
over that same concatenation.
|
||||||
|
|
||||||
|
The packet is a `DATA` packet on the active Link, so it IS
|
||||||
|
link-encrypted per §3.1 link-derived form like ordinary link DATA —
|
||||||
|
`context = 0xFB` is NOT in `RNS/Packet.py` `pack()`'s not-encrypted
|
||||||
|
set, unlike KEEPALIVE (§6.7.1) or link `PROOF` (§6.5).
|
||||||
|
|
||||||
|
The responder's `receive()` (`RNS/Link.py:1010-1029`) decrypts the
|
||||||
|
body, splits off the 64-byte public_key prefix, reconstructs
|
||||||
|
`signed_data = link_id || public_key`, verifies the signature, and on
|
||||||
|
success sets `self.remote_identity` so subsequent REQUEST handlers can
|
||||||
|
check the caller against per-page allowlists (§11.6).
|
||||||
|
|
||||||
|
A clean-room implementation MUST build the payload as
|
||||||
|
`public_key || signature` (NOT just the signature) and sign the
|
||||||
|
concatenation `link_id || public_key` (NOT `link_id` alone). Either
|
||||||
|
mistake makes every `ALLOW_LIST`-protected page return
|
||||||
|
`DEFAULT_NOTALLOWED`.
|
||||||
|
|
||||||
### 6.8 Channel mode (`CHANNEL = 0x0E`)
|
### 6.8 Channel mode (`CHANNEL = 0x0E`)
|
||||||
|
|
||||||
A Channel is a **continuous, bi-directional, message-typed stream** on top of an established Link. Distinct from §11 REQUEST/RESPONSE (single-shot, client-server) and §10 Resources (large unidirectional transfers): Channel messages are short, can flow in either direction at any time, and carry an application-defined type byte the receiver dispatches on. NomadNet uses it for its "channel" API (live chat over a Link), and any application can register custom message types via `RNS.Channel.Channel.register_message_type`.
|
A Channel is a **continuous, bi-directional, message-typed stream** on top of an established Link. Distinct from §11 REQUEST/RESPONSE (single-shot, client-server) and §10 Resources (large unidirectional transfers): Channel messages are short, can flow in either direction at any time, and carry an application-defined type byte the receiver dispatches on. NomadNet uses it for its "channel" API (live chat over a Link), and any application can register custom message types via `RNS.Channel.Channel.register_message_type`.
|
||||||
|
|
@ -2908,7 +2961,7 @@ Pages are registered with one of three allow modes (`Destination.py:35-40`):
|
||||||
- `ALLOW_LIST` — caller's identity hash must appear in the page's `.allowed` file. Server checks `remote_identity.hash` against the list at request time (`Node.py:152-154`).
|
- `ALLOW_LIST` — caller's identity hash must appear in the page's `.allowed` file. Server checks `remote_identity.hash` against the list at request time (`Node.py:152-154`).
|
||||||
- `ALLOW_NONE` — registered handlers that exist but reject all requests (rare; debug only).
|
- `ALLOW_NONE` — registered handlers that exist but reject all requests (rare; debug only).
|
||||||
|
|
||||||
For `ALLOW_LIST` the client MUST call `link.identify(identity)` immediately after the link transitions to ACTIVE and BEFORE issuing the REQUEST. This sends a `LINKIDENTIFY (context = 0xFB)` packet whose payload carries a signature over `link_id` proving the long-term identity hash. Without it, `remote_identity` is `None` server-side and every `ALLOW_LIST` page returns `DEFAULT_NOTALLOWED`. See `Browser.py:1245-1250` for the upstream call site:
|
For `ALLOW_LIST` the client MUST call `link.identify(identity)` immediately after the link transitions to ACTIVE and BEFORE issuing the REQUEST. This sends a `LINKIDENTIFY (context = 0xFB)` packet whose 128-byte payload is `public_key(64) || signature(64)`, with the signature computed over `link_id || public_key` — proving the long-term identity hash to the responder. The wire format is specified in §6.7.6. Without it, `remote_identity` is `None` server-side and every `ALLOW_LIST` page returns `DEFAULT_NOTALLOWED`. See `Browser.py:1245-1250` for the upstream call site:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def link_established(self, link):
|
def link_established(self, link):
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue