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`.
|
||||
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`)
|
||||
|
||||
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_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
|
||||
def link_established(self, link):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue