Compare commits

...

2 commits

Author SHA1 Message Date
7ffbb0ef5e Completed the full link-delivered LXMF unit:
Tier 1 audit: `link-lxmf-tier1-rns-1.2.4-lxmf-0.9.7.md`
Tier 2 vectors/verifier: link-lxmf.json, regen_link_lxmf.py, and verify_link_lxmf.py
Tier 3 promotion: updated SPEC.md, flows, status, and documentation
Key correction: the 319/320 boundary uses upstream’s computed LXMF content_size, not simply raw message content length.
Also corrected stale flow descriptions for KEEPALIVE (0xFA) and encrypted LINKCLOSE teardown (0xFC).
Verification:
Deterministic vector regeneration: identical SHA-256
Portable-path and formatting checks: pass
Full pinned suite: 17 passed, 0 failed
2026-06-08 13:54:27 -07:00
7433063bfb Completed the Resource three-tier work unit.
Added deterministic `resources.json` and `regen_resources.py`.
Extended `verify_resource.py` with receiver assembly/proof and requested negative cases.
Updated specification, audit, status, and tool documentation.
Fixed an unrelated nondeterministic wrong-ticket test in verify_stamps.py.
Confirmed vector regeneration is byte-identical.
Confirmed no tracked reliance on specenv or user-specific paths.
git diff --check: pass.
Complete pinned suite: 16 passed, 0 failed.
2026-06-08 13:38:24 -07:00
16 changed files with 1108 additions and 38 deletions

18
SPEC.md
View file

@ -686,6 +686,16 @@ destination_hash(16) || source_hash(16) || signature(64) || msgpack_payload(...)
Full layout. The Link's session key encrypts the whole blob.
For DIRECT delivery, upstream computes
`content_size = len(msgpack_payload) - TIMESTAMP_SIZE - STRUCT_OVERHEAD` and
selects a single Link DATA packet when `content_size <=
LINK_PACKET_MAX_CONTENT`; otherwise it sends the complete body as a Resource.
With default RNS 1.2.4 / LXMF 0.9.7 parameters the boundary is 319/320. The
threshold applies to this computed LXMF content size, not simply the raw
`content` field and not the complete signed body. At the 319 boundary, the
complete canonical body is 431 bytes (`Link.MDU`) and the Link-encrypted wire
packet is 499 bytes. (verified by `tools/verify_link_lxmf.py`)
### 5.3 `msgpack_payload`
A msgpack array of 4 elements (5th optional):
@ -2411,7 +2421,7 @@ The repeater repo's `pre_build.py` patches several other microReticulum protocol
## 10. Resource fragmentation protocol
A **Resource** is the standard RNS mechanism for transferring a payload that exceeds the per-packet content limit of an established Reticulum Link. LXMF, Link REQUEST/RESPONSE, NomadNet, and file-transfer utilities use it for large bodies; an application can also define its own sequencing protocol or use Channel, so Resource is not the only possible large-data mechanism. With default RNS 1.2.4 / LXMF 0.9.7 parameters, DIRECT LXMF selects Resource when content exceeds **319 bytes** (`LINK_PACKET_MAX_CONTENT = Link.MDU(431) - LXMF_OVERHEAD(112)`, verified by `tools/verify_resource.py`). Resource is built **on top of** an active Link — it relies on the Link's session key for encryption (§3.1 link-derived form) and on the Link's bidirectional DATA channel for control traffic.
A **Resource** is the standard RNS mechanism for transferring a payload that exceeds the per-packet content limit of an established Reticulum Link. LXMF, Link REQUEST/RESPONSE, NomadNet, and file-transfer utilities use it for large bodies; an application can also define its own sequencing protocol or use Channel, so Resource is not the only possible large-data mechanism. With default RNS 1.2.4 / LXMF 0.9.7 parameters, DIRECT LXMF selects Resource when its **computed LXMF content size** exceeds **319 bytes** (`LINK_PACKET_MAX_CONTENT = Link.MDU(431) - LXMF_OVERHEAD(112)`, verified by `tools/verify_link_lxmf.py`). This is `len(msgpack_payload) - TIMESTAMP_SIZE - STRUCT_OVERHEAD`, not necessarily `len(content)` and not the complete signed body. Resource is built **on top of** an active Link — it relies on the Link's session key for encryption (§3.1 link-derived form) and on the Link's bidirectional DATA channel for control traffic.
The complete reference is `RNS/Resource.py` (1380 lines in RNS 1.2.4); `RNS/Packet.py:74-79` defines the context constants. This section describes the wire-level invariants a clean-room implementation must respect; many implementation choices (window sizing heuristics, watchdog timers, EIFR computation) are private and listed only when their absence would cause an interop break.
@ -2419,7 +2429,7 @@ The complete reference is `RNS/Resource.py` (1380 lines in RNS 1.2.4); `RNS/Pack
Three triggers in upstream:
1. **`LXMessage.send()` for `DIRECT` method with `representation == RESOURCE`.** Set automatically when the encrypted-form LXMF body exceeds `LINK_PACKET_MAX_CONTENT` (`LXMF/LXMessage.py:415-421`).
1. **`LXMessage.send()` for `DIRECT` method with `representation == RESOURCE`.** Set automatically when the computed LXMF `content_size` exceeds `LINK_PACKET_MAX_CONTENT` (`LXMF/LXMessage.py:405-421`; verified by `tools/verify_link_lxmf.py`).
2. **NomadNet page request fulfillment** — a server returning a page whose body exceeds the link MTU.
3. **Direct file transfers** via `rncp` and similar utilities.
@ -3769,8 +3779,10 @@ See [`test-vectors/`](test-vectors/). Currently populated:
- **`announces.json`** — signed announce packets, with and without ratchet material. Verified by `tools/verify_announce_roundtrip.py`; regenerated by `tools/regen_announces.py`. Covers SPEC.md §4.1, §4.2, and §4.5.
- **`lxmf.json`** — deterministic opportunistic LXMF plaintext and Token ciphertext vectors. Verified by `tools/verify_lxmf_opportunistic.py`; regenerated by `tools/regen_lxmf.py`. Covers SPEC.md §3 and §5.
- **`links.json`** — Link handshake and LRRTT vectors, including LINKREQUEST, LRPROOF, derived keys, and the activation packet. Verified by `tools/verify_link_handshake.py` and `tools/verify_link_lrrtt.py`; regenerated by `tools/regen_links.py`. Covers SPEC.md §6.1-§6.4 and §6.6.
- **`resources.json`** — deterministic multi-part Resource ciphertext, part packets, hashmap, advertisement, and proof body. Verified by `tools/verify_resource.py`; regenerated by `tools/regen_resources.py`. Covers SPEC.md §10.2, §10.4, §10.8, and §10.12.
- **`link-lxmf.json`** — deterministic DIRECT LXMF vectors at the exact PACKET/RESOURCE boundary, using the session key from `links.json`. Verified by `tools/verify_link_lxmf.py`; regenerated by `tools/regen_link_lxmf.py`. Covers SPEC.md §5.2, §5.5, §5.6, §6.4.3, and §10.1.
Remaining vector work should focus on negative/rejection cases and link-delivered LXMF bodies rather than the original bootstrap categories.
Remaining vector work should focus on broader negative/rejection cases rather than the original bootstrap categories.
An implementation that round-trips every test vector — both directions — should be wire-compatible with upstream Reticulum and LXMF for the covered operations.

View file

@ -167,8 +167,8 @@ Initial confidence assessment (subjective, not authoritative — re-do this audi
| §7.6 `TCPServerInterface.OUT` override | Source-cited; matches behavior observed in the mobile-app's local-transport experiments. |
| §8 KISS / HDLC framing | High — both work in production on the reference clients |
| §9.1§9.8 Implementation gotchas | Each was a real bug that bit a real implementation. High confidence each is real; some lack formal test scripts. |
| §10 Resource fragmentation | Source-cited from `RNS/Resource.py` against RNS 1.2.4; not yet runtime-verified in this repo's `tools/`. |
| §11 Test vectors | Historical bootstrap item. `test-vectors/` is now populated with identities, announces, opportunistic LXMF, and Link vectors. Future work should add negative vectors and link-delivered LXMF coverage. |
| §10 Resource fragmentation | Source-audited against RNS 1.2.4 and runtime-verified by `tools/verify_resource.py`, including deterministic vectors, receiver assembly/proof, multi-segment sizing, and negative cases. |
| §11 Test vectors | Historical bootstrap item. `test-vectors/` now covers identities, announces, opportunistic LXMF, Link establishment, link-delivered LXMF, and Resource. Future work should add broader negative vectors. |
| §12 Source map | High |
**Historical bootstrap tasks from the initial audit, now mostly complete:**

View file

@ -0,0 +1,75 @@
# Tier 1 Audit: Link-Delivered LXMF
Question: How does upstream LXMF 0.9.7 select, emit, receive, and acknowledge
DIRECT LXMF over an established RNS 1.2.4 Link?
Evidence baseline:
- RNS package: `rns==1.2.4`
- LXMF package: `lxmf==0.9.7`
- Sources: `LXMF/LXMessage.py`, `LXMF/LXMRouter.py`, `RNS/Packet.py`,
`RNS/Link.py`, and `RNS/Resource.py`
- Audit date: 2026-06-08
The Tier 2 evidence is `tools/verify_link_lxmf.py` and the deterministic
`test-vectors/link-lxmf.json`. Confirmed findings are promoted into `SPEC.md`
and the Link-LXMF flow documents.
## Confirmed Model
1. `LXMessage.pack()` computes:
```
content_size = len(packed_payload) - TIMESTAMP_SIZE - STRUCT_OVERHEAD
```
DIRECT selects PACKET when `content_size <= LINK_PACKET_MAX_CONTENT`
and Resource otherwise (`LXMessage.py:405-421`). The threshold applies to
this computed value, not simply `len(content)` and not the complete signed
LXMF body.
2. With default RNS 1.2.4 / LXMF 0.9.7 parameters,
`LINK_PACKET_MAX_CONTENT = 319`. The deterministic boundary fixtures use
empty title/fields, so their raw content lengths also happen to be 319 and
320. The PACKET fixture's complete canonical LXMF body is 431 bytes, equal
to `Link.MDU`; after Link Token framing its complete wire packet is 499
bytes.
3. DIRECT/PACKET passes the complete canonical body
`destination_hash || source_hash || signature || msgpack_payload` to
`RNS.Packet(link, packed)` (`LXMessage.py:627-635`). Packet packing emits
HEADER_1 DATA/LINK with context NONE and Link-derived Token encryption
(`Packet.py:176-219`).
4. DIRECT/RESOURCE passes that same complete canonical body to
`RNS.Resource(packed, link, ...)` (`LXMessage.py:643-653`). Resource then
applies its own whole-stream Link encryption and fragmentation.
5. On receive, `LXMRouter.delivery_packet()` proves the Link DATA packet,
classifies `destination_type == LINK` as DIRECT, and passes the decrypted
body unchanged to `lxmf_delivery()` (`LXMRouter.py:1822-1850`). Unlike the
opportunistic path, it does not prepend a destination hash.
6. `delivery_resource_concluded()` passes the assembled Resource body to
`lxmf_delivery(..., method=DIRECT)` (`LXMRouter.py:1876-1885`).
7. PACKET completion is the regular Link DATA proof callback; Resource
completion is the Resource callback. Both converge on
`LXMessage.__mark_delivered()` (`LXMessage.py:471-490, 594-603`).
## Tier 2 Scope
`tools/verify_link_lxmf.py` verifies:
1. Exact computed-content boundary: 319 selects PACKET, 320 selects Resource.
2. Deterministic Link DATA bytes using the session key from `links.json`.
3. DIRECT/PACKET decrypts to the complete canonical LXMF body and validates
Alice's signature.
4. DIRECT/RESOURCE decrypts and reassembles to the same canonical form.
5. A wrong Link key cannot decrypt the DIRECT/PACKET ciphertext.
6. `LXMRouter.delivery_packet()` proves Link DATA, classifies it DIRECT, and
forwards the full plaintext unchanged.
This work unit does not run a live threaded Link exchange. Link establishment,
LRRTT activation, generic Link proof format, and generic Resource behavior are
covered by their existing focused verifiers.

View file

@ -10,9 +10,9 @@ Evidence baseline:
`RNS/Link.py`, and `LXMF/LXMessage.py`
- Audit date: 2026-06-08
This began as a Tier 1 source-analysis report. The focused Tier 2 checks now
live in `tools/verify_resource.py`; the confirmed F1-F6 corrections were
promoted into `SPEC.md` §10 on 2026-06-08.
This began as a Tier 1 source-analysis report. The completed Tier 2 checks
live in `tools/verify_resource.py` and `test-vectors/resources.json`; the
confirmed F1-F6 corrections were promoted into `SPEC.md` §10 on 2026-06-08.
## Confirmed Core Model
@ -117,8 +117,8 @@ later-version note only when documenting an actual version change.
## Tier 2 Verifier Scope
The first focused verifier is implemented in `tools/verify_resource.py` and
avoids a live threaded transfer. It currently covers items 1-6 below:
The focused verifier is implemented in `tools/verify_resource.py` and avoids
a live threaded transfer. It covers:
1. Construct a Resource with a deterministic fake Link encryption key, fixed
throwaway prefix, and fixed advertisement `r`.
@ -131,11 +131,11 @@ avoids a live threaded transfer. It currently covers items 1-6 below:
including simultaneous part fulfilment and RESOURCE_HMU generation.
7. Verify receiver assembly strips the throwaway prefix, decrypts once,
validates the hash, and emits the expected RESOURCE_PRF bytes.
8. Add a multi-segment fixture proving `d` remains total logical size while
8. Verify a multi-segment fixture proving `d` remains total logical size while
`t`, `n`, `h`, and `r` describe the current segment.
9. Add negative cases: malformed ADV, wrong `r`, corrupt part, invalid HMU
9. Verify negative cases: malformed ADV, wrong `r`, corrupt part, invalid HMU
boundary, and oversized decompression.
Remaining Tier 2 work is a deterministic `test-vectors/resources.json` plus
items 7-9. The confirmed first claim set has already been promoted into
`SPEC.md` and the Resource flow documents.
The deterministic fixture is regenerated by `tools/regen_resources.py`.
Tier 2 Resource work is complete for this audit scope; confirmed claims have
been promoted into `SPEC.md` and the Resource flow documents.

View file

@ -70,6 +70,11 @@ A regular DATA packet on the link (`context = NONE`, Token-encrypted with link s
The handler calls `packet.prove()` immediately (mandatory PROOF receipt per §6.5), then dispatches the body to `LXMessage.unpack_from_bytes` and `lxmf_delivery` exactly like the opportunistic flow's steps 10-12.
`delivery_packet` passes Link plaintext through unchanged and marks the method
DIRECT; it does **not** prepend a destination hash. This distinction and the
319/320 PACKET/Resource boundary are verified by
`tools/verify_link_lxmf.py`.
### 6. Inbound LXMF DATA — Resource representation
A larger LXMF body arrives as a Resource transfer per `flows/receive-resource.md`. The Link's `resource_strategy = ACCEPT_APP` triggers `delivery_resource_advertised(resource)` (`LXMF/LXMRouter.py:1867-1874`):

View file

@ -13,9 +13,9 @@ Out of scope: opportunistic delivery (see [`send-opportunistic-lxmf.md`](send-op
DIRECT is reached two ways:
1. **App-requested:** `LXMessage(desired_method=LXMessage.DIRECT, …)`. Used for messages too large for one packet, or for sessions where the app wants the link's full-duplex DATA channel (e.g. an interactive chat). The router opens or reuses an `RNS.Link` to the recipient and sends the message over it.
2. **Auto-downgrade from OPPORTUNISTIC:** `LXMessage.pack` at `LXMF/LXMessage.py:394-398` falls back to `DIRECT` if the encrypted-form content size exceeds `ENCRYPTED_PACKET_MAX_CONTENT`. The originator may or may not surface this transition to the user; it's silent at the protocol layer.
2. **Auto-downgrade from OPPORTUNISTIC:** `LXMessage.pack` at `LXMF/LXMessage.py:394-398` falls back to `DIRECT` if the computed LXMF `content_size` exceeds `ENCRYPTED_PACKET_MAX_CONTENT`. The originator may or may not surface this transition to the user; it's silent at the protocol layer.
Within DIRECT there are two **representations** decided at pack time (`LXMF/LXMessage.py:414-421`):
Within DIRECT there are two **representations** decided at pack time (`LXMF/LXMessage.py:405-421`). Upstream compares its computed `content_size = len(msgpack_payload) - TIMESTAMP_SIZE - STRUCT_OVERHEAD`, not simply `len(content)` or the full signed body:
- **PACKET** — the body fits in a single `LINK_PACKET_MAX_CONTENT`-sized DATA packet on the link.
- **RESOURCE** — the body is larger than that and must be sent as an `RNS.Resource` (multi-packet, fragmented, with its own checksum / progress / retransmit machinery). The Resource fragmentation protocol is documented in [`../SPEC.md`](../SPEC.md) §10 and the dedicated send/receive Resource flows. This flow document covers PACKET in full and links to those details for RESOURCE.
@ -147,11 +147,11 @@ Same as `send-opportunistic-lxmf.md` step 9 — the framed link DATA packet leav
### 7. PROOF receipt arrives → `__mark_delivered` fires
The recipient (per the receive flow — TODO `receive-link-lxmf.md`) decrypts the link DATA, parses the LXMF body via `LXMessage.unpack_from_bytes`, validates the signature, and emits a PROOF for this packet (`Packet.prove` from inside `LXMRouter.delivery_packet` at line 1820, same as the opportunistic receive). The PROOF travels back along the link, `PacketReceipt.proven` resolves on the sender, and `__mark_delivered` puts the LXMessage in `DELIVERED`.
The recipient (per [`receive-link-lxmf.md`](receive-link-lxmf.md)) decrypts the link DATA, parses the LXMF body via `LXMessage.unpack_from_bytes`, validates the signature, and emits a PROOF for this packet (`Packet.prove` from inside `LXMRouter.delivery_packet` at line 1820, same as the opportunistic receive). The PROOF travels back along the link, `PacketReceipt.proven` resolves on the sender, and `__mark_delivered` puts the LXMessage in `DELIVERED`.
### 8. (Optional) RESOURCE representation for large bodies
If the packed body exceeds `LINK_PACKET_MAX_CONTENT` (`LXMF/LXMessage.py:415-421`), `representation` is set to `RESOURCE` and step 5 instead constructs an `RNS.Resource` (`__as_resource`, line 651):
If computed `content_size` exceeds `LINK_PACKET_MAX_CONTENT` (`LXMF/LXMessage.py:405-421`), `representation` is set to `RESOURCE` and step 5 instead constructs an `RNS.Resource` (`__as_resource`, line 651):
```python
RNS.Resource(self.packed, self.__delivery_destination, callback=..., progress_callback=..., auto_compress=...)
@ -173,9 +173,9 @@ This enables an interactive conversation over a single link rather than each mes
### 10. Link teardown
A link stays in `ACTIVE` until either side calls `link.teardown()`, the watchdog times out (no inbound activity within `Link.STALE_GRACE` after the keepalive interval), or the next-hop interface goes down. `RNS/Link.py` keepalives are `CTX_KEEPALIVE` (0xfd) packets; the cadence is `Link.KEEPALIVE` seconds.
A link stays in `ACTIVE` until either side calls `link.teardown()`, the watchdog times out, or the next-hop interface goes down. `RNS/Link.py` keepalives use `KEEPALIVE (0xFA)`; their cadence is dynamically clamped between `KEEPALIVE_MIN = 5s` and `KEEPALIVE_MAX = 360s`.
Teardown sends `RNS.Packet(link, b"", context=Link.PROOF, ...)` informing the peer; once teardown completes, the link is removed from `Transport.active_links` and from `LXMRouter.direct_links`, and the next DIRECT message to the same destination has to repeat the full handshake from step 3.
Teardown sends an encrypted `LINKCLOSE (0xFC)` packet whose plaintext body is `link_id`; once teardown completes, the link is removed from `Transport.active_links` and from `LXMRouter.direct_links`, and the next DIRECT message to the same destination has to repeat the full handshake from step 3.
---
@ -198,6 +198,10 @@ DATA on link (either direction), Token-encrypted (no eph_pub prefix):
(plaintext = full LXMF body: dest_hash || src_hash || signature || msgpack_payload)
```
At the default 319-byte computed-content boundary, the full LXMF plaintext is
431 bytes and the complete Link DATA wire packet is 499 bytes. At 320,
upstream switches to Resource. See `test-vectors/link-lxmf.json`.
Per SPEC.md §6.5 the receiver of any CTX_NONE DATA packet on the link MUST emit a PROOF receipt back; this is the mandatory `Packet.prove_packet` step on the receiving side, and is what resolves the sender's `PacketReceipt`.
---

View file

@ -10,8 +10,10 @@ Populated against RNS 1.2.4 / LXMF 0.9.7:
- ✅ `announces.json` — two announce vectors (no-ratchet + with-ratchet) signed by Alice (regenerator: `../tools/regen_announces.py`, verifier: `../tools/verify_announce_roundtrip.py`).
- ✅ `lxmf.json` — two opportunistic-LXMF vectors Alice → Bob (regenerator: `../tools/regen_lxmf.py`, verifier: `../tools/verify_lxmf_opportunistic.py`).
- ✅ `links.json` — full Link handshake vector (LINKREQUEST + LRPROOF + derived session key) Alice → Bob, plus an LRRTT packet (§6.4.2) emitted from the initiator with pinned IV and `rtt_seconds = 0.05` (regenerator: `../tools/regen_links.py`, verifiers: `../tools/verify_link_handshake.py`, `../tools/verify_link_lrrtt.py`).
- ✅ `resources.json` — deterministic multi-part Resource ciphertext, part packets, hashmap, advertisement, and proof body (regenerator: `../tools/regen_resources.py`, verifier: `../tools/verify_resource.py`).
- ✅ `link-lxmf.json` — DIRECT LXMF PACKET and Resource vectors at the exact 319/320 computed-content boundary (regenerator: `../tools/regen_link_lxmf.py`, verifier: `../tools/verify_link_lxmf.py`).
All four files are byte-deterministic across runs: regenerators pin every random source (ephemeral keys, IVs, `random_hash` prefix + timestamp, LXMF timestamp) so the output is reproducible against a fixed upstream RNS / LXMF version.
All six files are byte-deterministic across runs: regenerators pin every random source (ephemeral keys, IVs, `random_hash` values, timestamps) so the output is reproducible against a fixed upstream RNS / LXMF version.
See [`../agent.md`](../agent.md) §3 and [`../todo.md`](../todo.md) for the evidence model and remaining task list.
@ -23,6 +25,8 @@ Each vector lives in a per-domain JSON file, e.g.:
- `announces.json` — full hex of a signed announce packet, plus the inputs that produced it (display_name, ratchetPub, etc.)
- `lxmf.json` — sender + recipient identity, plaintext, expected ciphertext bytes
- `links.json` — LINKREQUEST + LRPROOF + derived session keys
- `resources.json` — Resource plaintext, encrypted stream, parts, hashmap, advertisement, and proof
- `link-lxmf.json` — DIRECT LXMF Link DATA and Resource boundary forms
Each entry should include:
@ -47,6 +51,7 @@ For the spec to claim "an implementation that passes all test vectors interopera
3. **Token encrypt + decrypt** — bidirectional, with both ratchet and long-term keys.
4. **Opportunistic LXMF** — full plaintext → ciphertext → plaintext round-trip, signature valid both ways.
5. **Link handshake** — LINKREQUEST built by client A, LRPROOF computed by upstream as B, both arrive at the same `link_id` and session keys.
6. **Link-delivered LXMF** — body packed by client, decrypted + parsed by upstream.
6. **Link-delivered LXMF** — body packed by client, decrypted + parsed by upstream. Covered by `link-lxmf.json`.
7. **Resource transfer** — encrypt once, split into parts, validate ADV/hashmap, assemble, and emit the expected proof.
A separate vector set for FAILURE cases is also useful: malformed announces, expired ratchets, mismatched signatures. An implementation should reject those as a regression-prevention measure.

View file

@ -0,0 +1,80 @@
{
"_about": "DIRECT LXMF vectors at the exact upstream PACKET/RESOURCE boundary. Both carry the complete canonical LXMF body over the deterministic Link session from links.json. `content_size` is upstream's computed LXMF size, not necessarily the raw content field length.",
"vectors": [
{
"label": "alice_to_bob_direct_packet_boundary",
"inputs": {
"src_identity_label": "alice",
"dst_identity_label": "bob",
"content_size": 319,
"lxmf_timestamp": 1700000000.0,
"link_vector_label": "alice_to_bob_aes256cbc",
"token_iv_hex": "5152535455565758595a5b5c5d5e5f60"
},
"expected": {
"method": 2,
"representation": 1,
"lxmf_packed_hex": "9695d17f22fa6e45d2b0cd3439a7ca7ec33c40a5b030596d95617dc4ca163aae05036402d951b1c68131b516673bcc04206ca5adb586ac7da3e63226e0febd19de6563326d055adbeb519e1d3418f887e0ef5d0ef5099c980e5e2b603555180094cb41d954fc40000000c400c5013f7878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787880",
"link_packet_ciphertext_hex": "5152535455565758595a5b5c5d5e5f6013989c7e9cb3ad3d547b14028cb2c8cb6d253f5e0de613cfa9ee588413147738ab5810b8e24c5174bf4d2f8e06a127c29703cd7ab276e020bdef730d1df6aac088518a1be128b927401a3998454207ff967f64bbe2cf58e8aea7759e3dd8d197b086d70ba654fe656bb7196b825f080c0edbe793d0b34c188fe0f56ea4f9c6912c01647c78fefe1f9b5cde08a322b7a5c86570018b7f4ea55ed8a07f1f4d6b34dbe75f3009965de388286f3971558fb7d0f0baae1ebafe0423aab50742d83d6042a25781553d808228f16a479c5d41fa23fee3cd01ab88b4d36c0fe9546baf6954c9a393026b04d0397692221f931548ec48afe7e6553c9e2f3c142eb38e692cb1cecaa880fe7372fd494d4dc9bd4852b172755074aff59634959e45338e90d3d7066480ab410a70dafaaaf579e953698dd7dab6ec2968e51e4f77550a1b7392c731066d8b53625cc163a3155f71b4a2201ffb7c7fc31d397e9140402afd7d722e447ef2e723e158c37ebf92828f0383a8eed02f233e1996a07d9d2ca572a1dbf49d58780d15c829fd3d53cc27b5016ce4fdd696c384b8d0ef71d1aba605c14320b5b0e0c20899e65e80cb35e80fa20fe5947055b2ad173bd1e58297affb8d4af5f1c1b27e1959b17e27c53625ddd264",
"link_packet_raw_hex": "0c007ee5fe3e4952c9ac4519b537f6278474005152535455565758595a5b5c5d5e5f6013989c7e9cb3ad3d547b14028cb2c8cb6d253f5e0de613cfa9ee588413147738ab5810b8e24c5174bf4d2f8e06a127c29703cd7ab276e020bdef730d1df6aac088518a1be128b927401a3998454207ff967f64bbe2cf58e8aea7759e3dd8d197b086d70ba654fe656bb7196b825f080c0edbe793d0b34c188fe0f56ea4f9c6912c01647c78fefe1f9b5cde08a322b7a5c86570018b7f4ea55ed8a07f1f4d6b34dbe75f3009965de388286f3971558fb7d0f0baae1ebafe0423aab50742d83d6042a25781553d808228f16a479c5d41fa23fee3cd01ab88b4d36c0fe9546baf6954c9a393026b04d0397692221f931548ec48afe7e6553c9e2f3c142eb38e692cb1cecaa880fe7372fd494d4dc9bd4852b172755074aff59634959e45338e90d3d7066480ab410a70dafaaaf579e953698dd7dab6ec2968e51e4f77550a1b7392c731066d8b53625cc163a3155f71b4a2201ffb7c7fc31d397e9140402afd7d722e447ef2e723e158c37ebf92828f0383a8eed02f233e1996a07d9d2ca572a1dbf49d58780d15c829fd3d53cc27b5016ce4fdd696c384b8d0ef71d1aba605c14320b5b0e0c20899e65e80cb35e80fa20fe5947055b2ad173bd1e58297affb8d4af5f1c1b27e1959b17e27c53625ddd264",
"link_id_hex": "7ee5fe3e4952c9ac4519b537f6278474"
},
"rns_version_at_generation": "1.2.4",
"lxmf_version_at_generation": "0.9.7",
"generator_script": "tools/regen_link_lxmf.py",
"verifies_spec_sections": [
"5.2",
"5.5",
"5.6",
"6.4.3",
"10.1"
]
},
{
"label": "alice_to_bob_direct_resource_boundary",
"inputs": {
"src_identity_label": "alice",
"dst_identity_label": "bob",
"content_size": 320,
"lxmf_timestamp": 1700000000.0,
"link_vector_label": "alice_to_bob_aes256cbc",
"token_iv_hex": "6162636465666768696a6b6c6d6e6f70",
"throwaway_prefix_hex": "71727374",
"resource_random_hash_hex": "81828384",
"auto_compress": false
},
"expected": {
"method": 2,
"representation": 2,
"lxmf_packed_hex": "9695d17f22fa6e45d2b0cd3439a7ca7ec33c40a5b030596d95617dc4ca163aae1c7919c2ec56920071ec83d67bc4eecf89164eb35d124c70d083ea76c5f8439154586dcbbdfb9ea6a7697085a2d0ddb07f735a849c8267adc157064ed9cd8b0294cb41d954fc40000000c400c50140787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787880",
"resource_hash_hex": "28adad95727aa613d7728c82e3e9a6027c407b51396677d6f6378ed1e0f44b64",
"resource_expected_proof_hex": "9ec0ee7a5aaae728c1480bbe2b449e07328ef223caed65f386b84209749efe4d",
"resource_advertisement_plaintext_hex": "8ba174cd01f0a164cd01b0a16e02a168c42028adad95727aa613d7728c82e3e9a6027c407b51396677d6f6378ed1e0f44b64a172c40481828384a16fc42028adad95727aa613d7728c82e3e9a6027c407b51396677d6f6378ed1e0f44b64a16901a16c01a171c0a16601a16dc40837cd98014cd2769c",
"resource_parts": [
{
"body_hex": "6162636465666768696a6b6c6d6e6f70951cef742577ef844c26d338a9b873db7510b7733b592bf353089ef09d06dceccf084587a257101f5db226c1270441f50c799b7e79d09a8dce6ed3b6dc40447607e56756b9bb6164abb87f7c0d9fa51cc46f8ff7dc99e76b3adddf6d045e2b3ffc575ccc97123b9bf7d206eb7b67d6f3c9f8575a796a5401813ce5f0a66269dfc3693e8edc4b749a845cb0ae6e12bbc6a2e0dc00f448fec88998303ae77dc0b4d53e3b8bb4912ddff0a4f456df3c5ca3f227f7e764bb5e264211a224929cc8da96160b5589f366ab2ee08f10d3851f94b88fcd3de232665b1c1d1bd55cec99665dd9b667c72b8fe2908bffa6d5264fd21a7f3809c70cbb7342536fab74fb855103b7375630b724d4abd6a3f36864ddf15f4c6e6bb6f554a75fdb4f286cc089455589021320abef585e80caee8ea94f0c705ce928b8088055e1f98c441238bcac024eea36bc7728e7b81d34fa3f2d1d0c722dc77d234e271c9ece67361400db88775899039de948548f3826f6fed969915f041d404a42c3e56bcdb0f2d8e57f1be27e128ad73a4011745dca50ef042cd9eb853e9fe3d7cc1ab9b8809ff073700dabd9c14a71fb08980750b68d970ec9853cc37d8b179c436abf26bbbf7477891e",
"map_hash_hex": "37cd9801",
"raw_hex": "0c007ee5fe3e4952c9ac4519b537f6278474016162636465666768696a6b6c6d6e6f70951cef742577ef844c26d338a9b873db7510b7733b592bf353089ef09d06dceccf084587a257101f5db226c1270441f50c799b7e79d09a8dce6ed3b6dc40447607e56756b9bb6164abb87f7c0d9fa51cc46f8ff7dc99e76b3adddf6d045e2b3ffc575ccc97123b9bf7d206eb7b67d6f3c9f8575a796a5401813ce5f0a66269dfc3693e8edc4b749a845cb0ae6e12bbc6a2e0dc00f448fec88998303ae77dc0b4d53e3b8bb4912ddff0a4f456df3c5ca3f227f7e764bb5e264211a224929cc8da96160b5589f366ab2ee08f10d3851f94b88fcd3de232665b1c1d1bd55cec99665dd9b667c72b8fe2908bffa6d5264fd21a7f3809c70cbb7342536fab74fb855103b7375630b724d4abd6a3f36864ddf15f4c6e6bb6f554a75fdb4f286cc089455589021320abef585e80caee8ea94f0c705ce928b8088055e1f98c441238bcac024eea36bc7728e7b81d34fa3f2d1d0c722dc77d234e271c9ece67361400db88775899039de948548f3826f6fed969915f041d404a42c3e56bcdb0f2d8e57f1be27e128ad73a4011745dca50ef042cd9eb853e9fe3d7cc1ab9b8809ff073700dabd9c14a71fb08980750b68d970ec9853cc37d8b179c436abf26bbbf7477891e"
},
{
"body_hex": "ffa6985bcc6b2709ea244b5951c90828c7758f63bf7ebf9c92d029aec4c32b24",
"map_hash_hex": "4cd2769c",
"raw_hex": "0c007ee5fe3e4952c9ac4519b537f627847401ffa6985bcc6b2709ea244b5951c90828c7758f63bf7ebf9c92d029aec4c32b24"
}
],
"link_id_hex": "7ee5fe3e4952c9ac4519b537f6278474"
},
"rns_version_at_generation": "1.2.4",
"lxmf_version_at_generation": "0.9.7",
"generator_script": "tools/regen_link_lxmf.py",
"verifies_spec_sections": [
"5.2",
"5.5",
"5.6",
"10.1",
"10.2",
"10.4"
]
}
]
}

View file

@ -0,0 +1,52 @@
{
"_about": "Deterministic Resource transfer vector over a fixed Link Token key. The Resource is uncompressed and spans multiple RESOURCE packets. A clean-room implementation should encrypt `throwaway_prefix || plaintext` once with the recorded Token key and IV, slice the ciphertext at the Resource SDU, reproduce every part and map hash, pack the advertisement, then assemble and emit the recorded PRF body.",
"vectors": [
{
"label": "fixed_link_uncompressed_multi_part",
"inputs": {
"plaintext_hex": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff",
"token_key_hex": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f",
"token_iv_hex": "404142434445464748494a4b4c4d4e4f",
"throwaway_prefix_hex": "10111213",
"random_hash_hex": "20212223",
"link_hash_hex": "00112233445566778899aabbccddeeff",
"mtu": 500,
"auto_compress": false
},
"expected": {
"encrypted_stream_hex": "404142434445464748494a4b4c4d4e4f2e9d382a37451544df61eee81f38350c4883c306763ae9491e392fc51bb2579c295964e92e36cd787d899e959ae7c3bd83607c4a45ebabd30a4de196a2f7ba9d77aabd00ca89fc711c072fbef55dc346c51754955bf4616427b2917de04247985af831059d14a52654ed50a0b01e99daa097f369609cfd31957dc8d2737e603f954e7e0ea70a65f7fee60c9b8b8f2482cb00469a155a9e0601111de46004ae4cde1fc14ac9f3be5b37a8308f47a7a7524e9638af44100573cb35a3d55a436c6080ccdcac5bcce07fae741f6049b4f06c011abba11f4647917ecbd7da7071a7936606bfbfd69a4c264060e7e1a2e093b704c76e0b09dc6200e761fd711489fda28679a375fad63df1e732f66ac47fc2650a6db3ee818b201e93b8b7325d375c0dfd850db44215c5fdc4189d199a2758e067c8d2ce29737acc4b81e9d22604b1a3f6d6315b3003b11dcbad4b761fbc4b43a9e3446c6083a4a32293458c843aa57fd829b21afaa600fc438d292a00c1bc651f95291586414bc4e67fa2740da4def4ed2caaac74f39d8cecf00e957f848c1adea9bf78556e55d17c94fd550a99d328bacf6704d7db090a9759aabac010322f539019b28cea0238992638a358582386065fa79a46947f5028d9e884d9fc98304a43f318b13b2a9ac14ddbf7d68d898be03d7881bf347e5cdc209ab155d23cbe922430c71ae14902642310c50d7d9617f462b4674517abf1430b6bc7763299c6556e52187d6950f0ff71ca6a1dcffbce2415f074492dc511c6c489b1ea8ed5d34c46d8ce32cffb71567411b7f8da3a84b53b16bc4384bfbe3238e5487b18f4faf3b09ee83e53f537c699be3f9167fc27a7df92bea5ec4a20646100016cb6728a7a94d51184acbcfffab0b1ccf7f6ee61fc34df1dbfc98cc801e141111a88c5713748d929cbbd14d69613bacf12d60df4f4da0799f7b9b6a1200ad84732789ff629f63aef6e5ffd79741e6d9160a1350882de9a1c76aae13a2c70ed972200af3f4078837882a06501b514081cb465c6b69661c9a82459a59086d6bbda247d4eac9656d413fe88558ae6a4e6d94ed1d2bb87bfc75b68256291d7208bb5e890d30f4c53226e222288de73106b1d4ba3fb01addfb7b0b1b9fed386cfa5afdb8789f22e7cfe7f0b39c8c797d0b70fd33a89293df048820c86b80cfd6f8ed829483d9fadbb05b8b6b4c8680931d74ffc263a23ab7845fe92ab850638fae9c267a623e78afc26055864e0504a951451933f60a357e17d8471653dca6cc97b3ef92c1715c959c4df40bdacf257b106d1fc6e635c82208f4235621458afd2ad23ce1e799f596df9be514f25cfff1f65bebf46ff52c2f43cf7c2edae2873e70a938cb417aea136c2a3b9521863eb0ae97e7093b99ad9059b00ba08463ad3b68bf2a91b053c6119117aee992020cc288f0f1ab9ae3ee3618f83d03378a62282bc9fa93a47dfa5426114551bbf610b5549ba57d548001e8b67bf09a40878799321832ef266b588a4226854366f22822fd36a79443c17f52bfc88341405eef4b6f821ef255270e14a21ae11570f205b550268c69e9ea071f21cd599774ef2e0fe20ec7ab2146d36237b922b7e59b8b78d621872ff682d703829e627b04f23315c93d5304e991e584d29facf6704c4d444418b3af4c6514cc9aa63e396e976713077755f721ebb022013bb75b071308a71ac578dd5c124a0af521c7404f166c26a63116b377928cc5d971d230f1bdf445202869d5af5304fd5e743165c1d65f86cf46247898b43d6cbf65f2be6217421c6a6ba95df7fd0fda278855fcea67916d710511da70eaa8cfb0d630b21e725f5cb304d13d5ca39440edefecc3f394482c08399a6d62cf8",
"resource_hash_hex": "a60281a93eccd1a9473ee0e72e3224bcf76792857bc003f1374facb56b0f3991",
"expected_proof_hex": "1c0e68442e09a54939291962b45ff7a10c9871aee1c7a769c338ba483bd1a256",
"resource_prf_body_hex": "a60281a93eccd1a9473ee0e72e3224bcf76792857bc003f1374facb56b0f39911c0e68442e09a54939291962b45ff7a10c9871aee1c7a769c338ba483bd1a256",
"hashmap_hex": "94abb0d50fcf34cdd9769774",
"advertisement_plaintext_hex": "8ba174cd0540a164cd0500a16e03a168c420a60281a93eccd1a9473ee0e72e3224bcf76792857bc003f1374facb56b0f3991a172c40420212223a16fc420a60281a93eccd1a9473ee0e72e3224bcf76792857bc003f1374facb56b0f3991a16901a16c01a171c0a16601a16dc40c94abb0d50fcf34cdd9769774",
"parts": [
{
"body_hex": "404142434445464748494a4b4c4d4e4f2e9d382a37451544df61eee81f38350c4883c306763ae9491e392fc51bb2579c295964e92e36cd787d899e959ae7c3bd83607c4a45ebabd30a4de196a2f7ba9d77aabd00ca89fc711c072fbef55dc346c51754955bf4616427b2917de04247985af831059d14a52654ed50a0b01e99daa097f369609cfd31957dc8d2737e603f954e7e0ea70a65f7fee60c9b8b8f2482cb00469a155a9e0601111de46004ae4cde1fc14ac9f3be5b37a8308f47a7a7524e9638af44100573cb35a3d55a436c6080ccdcac5bcce07fae741f6049b4f06c011abba11f4647917ecbd7da7071a7936606bfbfd69a4c264060e7e1a2e093b704c76e0b09dc6200e761fd711489fda28679a375fad63df1e732f66ac47fc2650a6db3ee818b201e93b8b7325d375c0dfd850db44215c5fdc4189d199a2758e067c8d2ce29737acc4b81e9d22604b1a3f6d6315b3003b11dcbad4b761fbc4b43a9e3446c6083a4a32293458c843aa57fd829b21afaa600fc438d292a00c1bc651f95291586414bc4e67fa2740da4def4ed2caaac74f39d8cecf00e957f848c1adea9bf78556e55d17c94fd550a99d328bacf6704d7db090a9759aabac010322f539019b28cea0238992638a358582386",
"map_hash_hex": "94abb0d5",
"raw_hex": "0c0000112233445566778899aabbccddeeff01404142434445464748494a4b4c4d4e4f2e9d382a37451544df61eee81f38350c4883c306763ae9491e392fc51bb2579c295964e92e36cd787d899e959ae7c3bd83607c4a45ebabd30a4de196a2f7ba9d77aabd00ca89fc711c072fbef55dc346c51754955bf4616427b2917de04247985af831059d14a52654ed50a0b01e99daa097f369609cfd31957dc8d2737e603f954e7e0ea70a65f7fee60c9b8b8f2482cb00469a155a9e0601111de46004ae4cde1fc14ac9f3be5b37a8308f47a7a7524e9638af44100573cb35a3d55a436c6080ccdcac5bcce07fae741f6049b4f06c011abba11f4647917ecbd7da7071a7936606bfbfd69a4c264060e7e1a2e093b704c76e0b09dc6200e761fd711489fda28679a375fad63df1e732f66ac47fc2650a6db3ee818b201e93b8b7325d375c0dfd850db44215c5fdc4189d199a2758e067c8d2ce29737acc4b81e9d22604b1a3f6d6315b3003b11dcbad4b761fbc4b43a9e3446c6083a4a32293458c843aa57fd829b21afaa600fc438d292a00c1bc651f95291586414bc4e67fa2740da4def4ed2caaac74f39d8cecf00e957f848c1adea9bf78556e55d17c94fd550a99d328bacf6704d7db090a9759aabac010322f539019b28cea0238992638a358582386"
},
{
"body_hex": "065fa79a46947f5028d9e884d9fc98304a43f318b13b2a9ac14ddbf7d68d898be03d7881bf347e5cdc209ab155d23cbe922430c71ae14902642310c50d7d9617f462b4674517abf1430b6bc7763299c6556e52187d6950f0ff71ca6a1dcffbce2415f074492dc511c6c489b1ea8ed5d34c46d8ce32cffb71567411b7f8da3a84b53b16bc4384bfbe3238e5487b18f4faf3b09ee83e53f537c699be3f9167fc27a7df92bea5ec4a20646100016cb6728a7a94d51184acbcfffab0b1ccf7f6ee61fc34df1dbfc98cc801e141111a88c5713748d929cbbd14d69613bacf12d60df4f4da0799f7b9b6a1200ad84732789ff629f63aef6e5ffd79741e6d9160a1350882de9a1c76aae13a2c70ed972200af3f4078837882a06501b514081cb465c6b69661c9a82459a59086d6bbda247d4eac9656d413fe88558ae6a4e6d94ed1d2bb87bfc75b68256291d7208bb5e890d30f4c53226e222288de73106b1d4ba3fb01addfb7b0b1b9fed386cfa5afdb8789f22e7cfe7f0b39c8c797d0b70fd33a89293df048820c86b80cfd6f8ed829483d9fadbb05b8b6b4c8680931d74ffc263a23ab7845fe92ab850638fae9c267a623e78afc26055864e0504a951451933f60a357e17d8471653dca6cc97b3ef92c1715",
"map_hash_hex": "0fcf34cd",
"raw_hex": "0c0000112233445566778899aabbccddeeff01065fa79a46947f5028d9e884d9fc98304a43f318b13b2a9ac14ddbf7d68d898be03d7881bf347e5cdc209ab155d23cbe922430c71ae14902642310c50d7d9617f462b4674517abf1430b6bc7763299c6556e52187d6950f0ff71ca6a1dcffbce2415f074492dc511c6c489b1ea8ed5d34c46d8ce32cffb71567411b7f8da3a84b53b16bc4384bfbe3238e5487b18f4faf3b09ee83e53f537c699be3f9167fc27a7df92bea5ec4a20646100016cb6728a7a94d51184acbcfffab0b1ccf7f6ee61fc34df1dbfc98cc801e141111a88c5713748d929cbbd14d69613bacf12d60df4f4da0799f7b9b6a1200ad84732789ff629f63aef6e5ffd79741e6d9160a1350882de9a1c76aae13a2c70ed972200af3f4078837882a06501b514081cb465c6b69661c9a82459a59086d6bbda247d4eac9656d413fe88558ae6a4e6d94ed1d2bb87bfc75b68256291d7208bb5e890d30f4c53226e222288de73106b1d4ba3fb01addfb7b0b1b9fed386cfa5afdb8789f22e7cfe7f0b39c8c797d0b70fd33a89293df048820c86b80cfd6f8ed829483d9fadbb05b8b6b4c8680931d74ffc263a23ab7845fe92ab850638fae9c267a623e78afc26055864e0504a951451933f60a357e17d8471653dca6cc97b3ef92c1715"
},
{
"body_hex": "c959c4df40bdacf257b106d1fc6e635c82208f4235621458afd2ad23ce1e799f596df9be514f25cfff1f65bebf46ff52c2f43cf7c2edae2873e70a938cb417aea136c2a3b9521863eb0ae97e7093b99ad9059b00ba08463ad3b68bf2a91b053c6119117aee992020cc288f0f1ab9ae3ee3618f83d03378a62282bc9fa93a47dfa5426114551bbf610b5549ba57d548001e8b67bf09a40878799321832ef266b588a4226854366f22822fd36a79443c17f52bfc88341405eef4b6f821ef255270e14a21ae11570f205b550268c69e9ea071f21cd599774ef2e0fe20ec7ab2146d36237b922b7e59b8b78d621872ff682d703829e627b04f23315c93d5304e991e584d29facf6704c4d444418b3af4c6514cc9aa63e396e976713077755f721ebb022013bb75b071308a71ac578dd5c124a0af521c7404f166c26a63116b377928cc5d971d230f1bdf445202869d5af5304fd5e743165c1d65f86cf46247898b43d6cbf65f2be6217421c6a6ba95df7fd0fda278855fcea67916d710511da70eaa8cfb0d630b21e725f5cb304d13d5ca39440edefecc3f394482c08399a6d62cf8",
"map_hash_hex": "d9769774",
"raw_hex": "0c0000112233445566778899aabbccddeeff01c959c4df40bdacf257b106d1fc6e635c82208f4235621458afd2ad23ce1e799f596df9be514f25cfff1f65bebf46ff52c2f43cf7c2edae2873e70a938cb417aea136c2a3b9521863eb0ae97e7093b99ad9059b00ba08463ad3b68bf2a91b053c6119117aee992020cc288f0f1ab9ae3ee3618f83d03378a62282bc9fa93a47dfa5426114551bbf610b5549ba57d548001e8b67bf09a40878799321832ef266b588a4226854366f22822fd36a79443c17f52bfc88341405eef4b6f821ef255270e14a21ae11570f205b550268c69e9ea071f21cd599774ef2e0fe20ec7ab2146d36237b922b7e59b8b78d621872ff682d703829e627b04f23315c93d5304e991e584d29facf6704c4d444418b3af4c6514cc9aa63e396e976713077755f721ebb022013bb75b071308a71ac578dd5c124a0af521c7404f166c26a63116b377928cc5d971d230f1bdf445202869d5af5304fd5e743165c1d65f86cf46247898b43d6cbf65f2be6217421c6a6ba95df7fd0fda278855fcea67916d710511da70eaa8cfb0d630b21e725f5cb304d13d5ca39440edefecc3f394482c08399a6d62cf8"
}
]
},
"rns_version_at_generation": "1.2.4",
"generator_script": "tools/regen_resources.py",
"verifies_spec_sections": [
"10.2",
"10.3",
"10.4",
"10.8",
"10.12"
]
}
]
}

17
todo.md
View file

@ -21,12 +21,13 @@ Outstanding work for the spec repo.
## Test infrastructure
- [ ] **Deterministic Resource vectors and negative cases.** The Tier 1 audit
- [x] **Deterministic Resource vectors and negative cases.** The Tier 1 audit
is recorded in `audits/resource-tier1-rns-1.2.4.md`;
`tools/verify_resource.py` now runtime-locks the first focused claim
set, and confirmed corrections are promoted into SPEC.md §10. Add
`test-vectors/resources.json` plus malformed ADV, wrong-`r`, corrupt
part, invalid-HMU-boundary, and oversized-decompression rejection cases.
`tools/verify_resource.py` runtime-locks sender and receiver behavior,
proof validation, multi-segment sizing, and malformed ADV, wrong-`r`,
corrupt-part, invalid-HMU-boundary, and oversized-decompression
rejection cases. `tools/regen_resources.py` regenerates the deterministic
`test-vectors/resources.json`.
- [x] **Bootstrap `test-vectors/identities.json`** — Alice + Bob
identities populated against RNS 1.2.0. Regenerator at
`tools/regen_identities.py`.
@ -50,6 +51,12 @@ Outstanding work for the spec repo.
`verify_stamps.py`, `verify_ratchet_dedup.py`). Status table
lives in `tools/README.md`.
- [x] **Deterministic link-delivered LXMF vectors.** Added
`test-vectors/link-lxmf.json`, `tools/regen_link_lxmf.py`, and
`tools/verify_link_lxmf.py`. Covers the exact computed-content
PACKET/Resource boundary, Link decrypt/parse, wrong-key rejection, and
DIRECT receive dispatch.
## Open `⚠️ UNVERIFIED` items in SPEC.md
These need either a runtime test or a stronger upstream source citation

View file

@ -51,15 +51,18 @@ Populated against RNS 1.2.4 / LXMF 0.9.7:
| `verify_proof_packet.py` | §6.5 — implicit (64B) and explicit (96B) proof body forms, validator length-dispatch | ✅ |
| `verify_link_handshake.py` | §6.1, §6.2, §6.3, §6.6 — LINKREQUEST/LRPROOF body order, link_id derivation, signalling | ✅ |
| `verify_link_lrrtt.py` | §6.4.2, §6.4.3 — LRRTT wire form, HEADER_1 header, dest_type=LINK, ctx=0xfe, link-form Token body, msgpack float64 plaintext | ✅ |
| `verify_link_lxmf.py` | §5.2, §5.5, §5.6, §6.4.3, §10.1 — DIRECT PACKET/Resource boundary, Link decrypt/parse, wrong-key rejection, receive dispatch | ✅ |
| `verify_path_request.py` | §1.2 well-known hashes, §7.1 LXMF path-preamble gating | ✅ |
| `verify_rnode_split.py` | §8.3 — RNode air-frame split-packet TX/RX state machines | ✅ |
| `verify_msgpack_quirk.py` | §9.3 — encoding name as bytes vs str affects upstream parsing | ✅ |
| `verify_stamps.py` | §5.7 — workblock determinism, PoW stamp search/validate, ticket shortcut | ✅ |
| `verify_ratchet_dedup.py` | §7.3 / §4.5 step 6.3 — confirms replay defence is keyed on `random_blob`, NOT on `(dest_hash, ratchet_pub)` | ✅ |
| `verify_resource.py` | §10.2, §10.4, §10.6, §10.7, §10.9, §10.11 — whole-stream encryption/slicing, hashes, ADV fields, exhausted REQ behavior, rejection/cancel distinction, multi-segment total size | ✅ |
| `verify_resource.py` | §10.2, §10.4, §10.6-§10.9, §10.11, §10.12 — vectors, whole-stream encryption/slicing, receiver assembly/proof, control behavior, multi-segment size, and negative cases | ✅ |
| `regen_identities.py` | regenerates `test-vectors/identities.json` | ✅ |
| `regen_announces.py` | regenerates `test-vectors/announces.json` (deterministic announce wire bytes, with and without ratchet) | ✅ |
| `regen_lxmf.py` | regenerates `test-vectors/lxmf.json` (deterministic opportunistic-LXMF plaintext + Token ciphertext) | ✅ |
| `regen_links.py` | regenerates `test-vectors/links.json` (deterministic LINKREQUEST + LRPROOF + derived session key) | ✅ |
| `regen_link_lxmf.py` | regenerates `test-vectors/link-lxmf.json` (deterministic DIRECT PACKET and Resource boundary vectors) | ✅ |
| `regen_resources.py` | regenerates `test-vectors/resources.json` (deterministic Resource ciphertext, parts, ADV, and PRF body) | ✅ |
See [`../agent.md`](../agent.md) §3 and [`../todo.md`](../todo.md) for the evidence model and remaining priority order.

268
tools/regen_link_lxmf.py Normal file
View file

@ -0,0 +1,268 @@
"""
Regenerator for test-vectors/link-lxmf.json.
Builds deterministic DIRECT LXMF vectors at the PACKET/RESOURCE boundary:
- computed LXMF content_size 319 -> one encrypted Link DATA packet
- computed LXMF content_size 320 -> one Resource over the same Link
The vectors reuse the deterministic Link session key from links.json and the
Alice/Bob identities from identities.json.
"""
from __future__ import annotations
import json
import os
import sys
import tempfile
import LXMF
import RNS
from LXMF.LXMessage import LXMessage
from RNS.Cryptography.Token import Token
from RNS.Resource import Resource, ResourceAdvertisement
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
OUT_PATH = os.path.join(REPO_ROOT, "test-vectors", "link-lxmf.json")
IDS_PATH = os.path.join(REPO_ROOT, "test-vectors", "identities.json")
LINKS_PATH = os.path.join(REPO_ROOT, "test-vectors", "links.json")
FIXED_TIMESTAMP = 1700000000.0
PACKET_IV = bytes.fromhex("5152535455565758595a5b5c5d5e5f60")
RESOURCE_IV = bytes.fromhex("6162636465666768696a6b6c6d6e6f70")
RESOURCE_PREFIX = bytes.fromhex("71727374")
RESOURCE_RANDOM_HASH = bytes.fromhex("81828384")
def fail(message: str) -> None:
print(f"FAIL: {message}")
sys.exit(1)
def init_minimal_rns():
config_dir = tempfile.mkdtemp(prefix="rns-regen-link-lxmf-")
config_path = os.path.join(config_dir, "config")
with open(config_path, "w", encoding="utf-8") as config:
config.write("[reticulum]\nenable_transport = No\nshare_instance = No\n")
return RNS.Reticulum(configdir=config_dir, loglevel=0)
def load_inputs():
with open(IDS_PATH, "r", encoding="utf-8") as identities_file:
identities = json.load(identities_file)["vectors"]
with open(LINKS_PATH, "r", encoding="utf-8") as links_file:
link_vector = json.load(links_file)["vectors"][0]
alice = next(vector for vector in identities if vector["label"] == "alice")
bob = next(vector for vector in identities if vector["label"] == "bob")
alice_identity = RNS.Identity.from_bytes(bytes.fromhex(alice["inputs"]["private_key_hex"]))
bob_identity = RNS.Identity.from_bytes(bytes.fromhex(bob["inputs"]["private_key_hex"]))
derived_key = bytes.fromhex(link_vector["expected"]["derived_key_hex"])
link_id = bytes.fromhex(link_vector["expected"]["link_id_hex"])
return alice_identity, bob_identity, derived_key, link_id
class FakeLink:
"""Active Link surface needed by LXMessage, Packet, and Resource."""
def __init__(self, derived_key: bytes, link_id: bytes):
self.type = RNS.Destination.LINK
self.status = RNS.Link.ACTIVE
self.hash = link_id
self.link_id = link_id
self.mtu = RNS.Reticulum.MTU
self.mdu = RNS.Link.MDU
self.rtt = 0.1
self.traffic_timeout_factor = 1
self.last_outbound = 0
self.tx = 0
self.txbytes = 0
self._token = Token(derived_key)
def encrypt(self, data: bytes) -> bytes:
return self._token.encrypt(data)
def decrypt(self, data: bytes) -> bytes:
return self._token.decrypt(data)
def make_message(destination, source, content_size: int) -> LXMessage:
message = LXMessage(
destination=destination,
source=source,
title=b"",
content=b"x" * content_size,
fields={},
desired_method=LXMessage.DIRECT,
)
message.timestamp = FIXED_TIMESTAMP
message.pack()
computed_content_size = (
len(message.packed[2 * LXMessage.DESTINATION_LENGTH + LXMessage.SIGNATURE_LENGTH :])
- LXMessage.TIMESTAMP_SIZE
- LXMessage.STRUCT_OVERHEAD
)
if computed_content_size != content_size:
fail(f"fixture computed content_size {computed_content_size}, want {content_size}")
return message
def patch_token_iv(iv: bytes):
token_mod = sys.modules["RNS.Cryptography.Token"]
real_urandom = token_mod.os.urandom
def fixed_urandom(length: int) -> bytes:
if length == 16:
return iv
return real_urandom(length)
token_mod.os.urandom = fixed_urandom
return token_mod, real_urandom
def build_packet_vector(message: LXMessage, link: FakeLink) -> dict:
message.set_delivery_destination(link)
token_mod, real_urandom = patch_token_iv(PACKET_IV)
try:
packet = message._LXMessage__as_packet()
packet.pack()
finally:
token_mod.os.urandom = real_urandom
return {
"label": "alice_to_bob_direct_packet_boundary",
"inputs": {
"src_identity_label": "alice",
"dst_identity_label": "bob",
"content_size": 319,
"lxmf_timestamp": FIXED_TIMESTAMP,
"link_vector_label": "alice_to_bob_aes256cbc",
"token_iv_hex": PACKET_IV.hex(),
},
"expected": {
"method": message.method,
"representation": message.representation,
"lxmf_packed_hex": message.packed.hex(),
"link_packet_ciphertext_hex": packet.ciphertext.hex(),
"link_packet_raw_hex": packet.raw.hex(),
"link_id_hex": link.link_id.hex(),
},
"rns_version_at_generation": RNS.__version__,
"lxmf_version_at_generation": LXMF.__version__,
"generator_script": "tools/regen_link_lxmf.py",
"verifies_spec_sections": ["5.2", "5.5", "5.6", "6.4.3", "10.1"],
}
def build_resource_vector(message: LXMessage, link: FakeLink) -> dict:
message.set_delivery_destination(link)
message.auto_compress = False
token_mod, real_urandom = patch_token_iv(RESOURCE_IV)
real_get_random_hash = RNS.Identity.get_random_hash
real_advertise = Resource.advertise
hashes = [
RESOURCE_PREFIX + bytes(28),
RESOURCE_RANDOM_HASH + bytes(28),
]
def fixed_random_hash() -> bytes:
if not hashes:
raise RuntimeError("unexpected extra Resource random-hash request")
return hashes.pop(0)
RNS.Identity.get_random_hash = staticmethod(fixed_random_hash)
Resource.advertise = lambda self: None
try:
resource = message._LXMessage__as_resource()
finally:
token_mod.os.urandom = real_urandom
RNS.Identity.get_random_hash = staticmethod(real_get_random_hash)
Resource.advertise = real_advertise
return {
"label": "alice_to_bob_direct_resource_boundary",
"inputs": {
"src_identity_label": "alice",
"dst_identity_label": "bob",
"content_size": 320,
"lxmf_timestamp": FIXED_TIMESTAMP,
"link_vector_label": "alice_to_bob_aes256cbc",
"token_iv_hex": RESOURCE_IV.hex(),
"throwaway_prefix_hex": RESOURCE_PREFIX.hex(),
"resource_random_hash_hex": RESOURCE_RANDOM_HASH.hex(),
"auto_compress": False,
},
"expected": {
"method": message.method,
"representation": message.representation,
"lxmf_packed_hex": message.packed.hex(),
"resource_hash_hex": resource.hash.hex(),
"resource_expected_proof_hex": resource.expected_proof.hex(),
"resource_advertisement_plaintext_hex": ResourceAdvertisement(resource).pack().hex(),
"resource_parts": [
{
"body_hex": part.data.hex(),
"map_hash_hex": part.map_hash.hex(),
"raw_hex": part.raw.hex(),
}
for part in resource.parts
],
"link_id_hex": link.link_id.hex(),
},
"rns_version_at_generation": RNS.__version__,
"lxmf_version_at_generation": LXMF.__version__,
"generator_script": "tools/regen_link_lxmf.py",
"verifies_spec_sections": ["5.2", "5.5", "5.6", "10.1", "10.2", "10.4"],
}
def main() -> None:
print(f"regen_link_lxmf.py against RNS {RNS.__version__} / LXMF {LXMF.__version__}")
init_minimal_rns()
try:
alice_identity, bob_identity, derived_key, link_id = load_inputs()
alice = RNS.Destination(
alice_identity, RNS.Destination.IN, RNS.Destination.SINGLE, "lxmf", "delivery"
)
bob = RNS.Destination(
bob_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, "lxmf", "delivery"
)
RNS.Identity.remember(bytes(32), alice.hash, alice_identity.get_public_key(), None)
RNS.Identity.remember(bytes(32), bob.hash, bob_identity.get_public_key(), None)
link = FakeLink(derived_key, link_id)
packet_message = make_message(bob, alice, 319)
resource_message = make_message(bob, alice, 320)
if packet_message.representation != LXMessage.PACKET:
fail("content_size 319 did not select PACKET")
if resource_message.representation != LXMessage.RESOURCE:
fail("content_size 320 did not select RESOURCE")
payload = {
"_about": (
"DIRECT LXMF vectors at the exact upstream PACKET/RESOURCE boundary. "
"Both carry the complete canonical LXMF body over the deterministic "
"Link session from links.json. `content_size` is upstream's computed "
"LXMF size, not necessarily the raw content field length."
),
"vectors": [
build_packet_vector(packet_message, link),
build_resource_vector(resource_message, link),
],
}
with open(OUT_PATH, "w", encoding="utf-8", newline="\n") as output:
json.dump(payload, output, indent=2, sort_keys=False)
output.write("\n")
print(f"Wrote {OUT_PATH} with 2 vectors")
print("ALL PASS")
finally:
try:
RNS.Reticulum.exit_handler()
except Exception:
pass
if __name__ == "__main__":
main()

146
tools/regen_resources.py Normal file
View file

@ -0,0 +1,146 @@
"""
Regenerator for test-vectors/resources.json.
Builds a deterministic multi-part Resource over a fixed fake Link. The link
Token key, Token IV, throwaway prefix, and Resource random-hash salt are pinned
so every ciphertext byte, part packet, map hash, advertisement, and proof is
reproducible against the pinned RNS version.
Run from repo root:
python tools/regen_resources.py
Updates `test-vectors/resources.json` in place. Exit 0 on success.
"""
from __future__ import annotations
import json
import os
import sys
import RNS
from RNS.Cryptography.Token import Token
from RNS.Resource import Resource, ResourceAdvertisement
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
OUT_PATH = os.path.join(REPO_ROOT, "test-vectors", "resources.json")
FIXED_TOKEN_KEY = bytes(range(64))
FIXED_TOKEN_IV = bytes.fromhex("404142434445464748494a4b4c4d4e4f")
FIXED_PREFIX = bytes.fromhex("10111213")
FIXED_RANDOM_HASH = bytes.fromhex("20212223")
PLAINTEXT = bytes(range(256)) * 5
class FakeLink:
"""Minimum deterministic Link surface needed by Resource construction."""
def __init__(self):
self.type = RNS.Destination.LINK
self.status = RNS.Link.ACTIVE
self.mtu = RNS.Reticulum.MTU
self.mdu = RNS.Link.MDU
self.hash = bytes.fromhex("00112233445566778899aabbccddeeff")
self.link_id = self.hash
self.rtt = 0.1
self.traffic_timeout_factor = 1
self.last_outbound = 0
self.tx = 0
self.txbytes = 0
self._token = Token(FIXED_TOKEN_KEY)
def encrypt(self, data: bytes) -> bytes:
return self._token.encrypt(data)
def decrypt(self, data: bytes) -> bytes:
return self._token.decrypt(data)
def main() -> None:
print(f"regen_resources.py against RNS {RNS.__version__}")
token_mod = sys.modules["RNS.Cryptography.Token"]
real_urandom = token_mod.os.urandom
real_get_random_hash = RNS.Identity.get_random_hash
random_hashes = [
FIXED_PREFIX + bytes(28),
FIXED_RANDOM_HASH + bytes(28),
]
def fixed_urandom(length: int) -> bytes:
if length == 16:
return FIXED_TOKEN_IV
return real_urandom(length)
def fixed_random_hash() -> bytes:
if not random_hashes:
raise RuntimeError("unexpected additional Resource random-hash request")
return random_hashes.pop(0)
token_mod.os.urandom = fixed_urandom
RNS.Identity.get_random_hash = staticmethod(fixed_random_hash)
try:
link = FakeLink()
resource = Resource(PLAINTEXT, link, advertise=False, auto_compress=False)
finally:
token_mod.os.urandom = real_urandom
RNS.Identity.get_random_hash = staticmethod(real_get_random_hash)
encrypted_stream = b"".join(part.data for part in resource.parts)
advertisement = ResourceAdvertisement(resource).pack()
proof = resource.hash + resource.expected_proof
vector = {
"label": "fixed_link_uncompressed_multi_part",
"inputs": {
"plaintext_hex": PLAINTEXT.hex(),
"token_key_hex": FIXED_TOKEN_KEY.hex(),
"token_iv_hex": FIXED_TOKEN_IV.hex(),
"throwaway_prefix_hex": FIXED_PREFIX.hex(),
"random_hash_hex": FIXED_RANDOM_HASH.hex(),
"link_hash_hex": link.hash.hex(),
"mtu": link.mtu,
"auto_compress": False,
},
"expected": {
"encrypted_stream_hex": encrypted_stream.hex(),
"resource_hash_hex": resource.hash.hex(),
"expected_proof_hex": resource.expected_proof.hex(),
"resource_prf_body_hex": proof.hex(),
"hashmap_hex": resource.hashmap.hex(),
"advertisement_plaintext_hex": advertisement.hex(),
"parts": [
{
"body_hex": part.data.hex(),
"map_hash_hex": part.map_hash.hex(),
"raw_hex": part.raw.hex(),
}
for part in resource.parts
],
},
"rns_version_at_generation": RNS.__version__,
"generator_script": "tools/regen_resources.py",
"verifies_spec_sections": ["10.2", "10.3", "10.4", "10.8", "10.12"],
}
payload = {
"_about": (
"Deterministic Resource transfer vector over a fixed Link Token key. "
"The Resource is uncompressed and spans multiple RESOURCE packets. "
"A clean-room implementation should encrypt `throwaway_prefix || "
"plaintext` once with the recorded Token key and IV, slice the "
"ciphertext at the Resource SDU, reproduce every part and map hash, "
"pack the advertisement, then assemble and emit the recorded PRF body."
),
"vectors": [vector],
}
with open(OUT_PATH, "w", encoding="utf-8", newline="\n") as output:
json.dump(payload, output, indent=2, sort_keys=False)
output.write("\n")
print(f"Wrote {OUT_PATH} with 1 vector")
print("ALL PASS")
if __name__ == "__main__":
main()

205
tools/verify_link_lxmf.py Normal file
View file

@ -0,0 +1,205 @@
"""
Verifier for DIRECT LXMF delivery over an established Reticulum Link.
Validates deterministic PACKET and Resource vectors, the exact upstream
content_size boundary (319/320), full-body DIRECT parsing, wrong-link-key
rejection, and LXMRouter's DIRECT receive dispatch.
"""
from __future__ import annotations
import json
import os
import sys
import tempfile
import LXMF
import RNS
from LXMF.LXMessage import LXMessage
from LXMF.LXMRouter import LXMRouter
from RNS.Cryptography.Token import Token
from RNS.Resource import Resource, ResourceAdvertisement
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
VECTORS_PATH = os.path.join(REPO_ROOT, "test-vectors", "link-lxmf.json")
IDS_PATH = os.path.join(REPO_ROOT, "test-vectors", "identities.json")
LINKS_PATH = os.path.join(REPO_ROOT, "test-vectors", "links.json")
def fail(message: str) -> None:
print(f"FAIL: {message}")
sys.exit(1)
def init_minimal_rns():
config_dir = tempfile.mkdtemp(prefix="rns-verify-link-lxmf-")
config_path = os.path.join(config_dir, "config")
with open(config_path, "w", encoding="utf-8") as config:
config.write("[reticulum]\nenable_transport = No\nshare_instance = No\n")
return RNS.Reticulum(configdir=config_dir, loglevel=0)
def load_json(path: str):
with open(path, "r", encoding="utf-8") as input_file:
return json.load(input_file)
def content_size(packed: bytes) -> int:
payload = packed[2 * LXMessage.DESTINATION_LENGTH + LXMessage.SIGNATURE_LENGTH :]
return len(payload) - LXMessage.TIMESTAMP_SIZE - LXMessage.STRUCT_OVERHEAD
def parse_and_check(packed: bytes, expected_size: int) -> LXMessage:
parsed = LXMessage.unpack_from_bytes(packed)
if not parsed.signature_validated:
fail(f"DIRECT LXMF signature invalid at content_size {expected_size}")
if len(parsed.content) != expected_size or parsed.content != b"x" * expected_size:
fail(f"DIRECT LXMF content mismatch at content_size {expected_size}")
if parsed.method != LXMessage.UNKNOWN:
fail("unpack_from_bytes unexpectedly assigned a delivery method")
return parsed
def verify_vectors(derived_key: bytes, link_id: bytes) -> tuple[bytes, bytes]:
vectors = load_json(VECTORS_PATH)["vectors"]
packet_vector = next(vector for vector in vectors if vector["expected"]["representation"] == LXMessage.PACKET)
resource_vector = next(vector for vector in vectors if vector["expected"]["representation"] == LXMessage.RESOURCE)
token = Token(derived_key)
packet_packed = bytes.fromhex(packet_vector["expected"]["lxmf_packed_hex"])
packet_raw = bytes.fromhex(packet_vector["expected"]["link_packet_raw_hex"])
packet_ciphertext = bytes.fromhex(packet_vector["expected"]["link_packet_ciphertext_hex"])
if content_size(packet_packed) != 319 or len(packet_packed) != RNS.Link.MDU:
fail("319 boundary vector does not fill the Link MDU exactly")
if len(packet_raw) != 19 + len(packet_ciphertext) or len(packet_raw) > RNS.Reticulum.MTU:
fail("319 boundary vector has an invalid Link packet wire length")
if packet_raw[:2] != bytes([RNS.Destination.LINK << 2, 0]):
fail("DIRECT PACKET vector flags/hops are not HEADER_1 DATA LINK")
if packet_raw[2:18] != link_id or packet_raw[18] != RNS.Packet.NONE:
fail("DIRECT PACKET vector link_id/context mismatch")
if packet_raw[19:] != packet_ciphertext:
fail("DIRECT PACKET raw body differs from recorded ciphertext")
if token.decrypt(packet_ciphertext) != packet_packed:
fail("DIRECT PACKET did not decrypt to the complete LXMF body")
parse_and_check(packet_packed, 319)
resource_packed = bytes.fromhex(resource_vector["expected"]["lxmf_packed_hex"])
parts = [bytes.fromhex(part["body_hex"]) for part in resource_vector["expected"]["resource_parts"]]
stream = b"".join(parts)
decrypted = token.decrypt(stream)
prefix = bytes.fromhex(resource_vector["inputs"]["throwaway_prefix_hex"])
if decrypted[: Resource.RANDOM_HASH_SIZE] != prefix:
fail("DIRECT Resource throwaway prefix mismatch")
if decrypted[Resource.RANDOM_HASH_SIZE :] != resource_packed:
fail("DIRECT Resource did not decrypt to the complete LXMF body")
if content_size(resource_packed) != 320:
fail("Resource boundary vector computed content_size is not 320")
parse_and_check(resource_packed, 320)
adv = ResourceAdvertisement.unpack(
bytes.fromhex(resource_vector["expected"]["resource_advertisement_plaintext_hex"])
)
if adv.d != len(resource_packed) or adv.h.hex() != resource_vector["expected"]["resource_hash_hex"]:
fail("DIRECT Resource advertisement size/hash mismatch")
try:
Token(bytes(reversed(derived_key))).decrypt(packet_ciphertext)
except Exception:
pass
else:
fail("DIRECT packet decrypted under the wrong Link key")
print("PASS S5.2/S10.1 DIRECT PACKET/RESOURCE boundary vectors: 319 -> PACKET, 320 -> RESOURCE")
print("PASS S3/S5.5/S5.6 DIRECT Link decrypt and signed full-body parse; wrong Link key rejected")
return packet_packed, resource_packed
def verify_upstream_boundary(alice_identity, bob_identity) -> None:
alice = RNS.Destination(
alice_identity, RNS.Destination.IN, RNS.Destination.SINGLE, "verify_link_lxmf", "alice"
)
bob = RNS.Destination(
bob_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, "verify_link_lxmf", "bob"
)
for size, expected in [(319, LXMessage.PACKET), (320, LXMessage.RESOURCE)]:
message = LXMessage(bob, alice, b"x" * size, b"", {}, LXMessage.DIRECT)
message.timestamp = 1700000000.0
message.pack()
if content_size(message.packed) != size or message.representation != expected:
fail(f"upstream boundary mismatch at {size}")
print("PASS S10.1 upstream selection uses computed LXMF content_size at the 319/320 boundary")
def verify_router_direct_dispatch(packet_packed: bytes) -> None:
router = object.__new__(LXMRouter)
captured = {}
router.lxmf_delivery = lambda data, destination_type, **kwargs: captured.update(
data=data, destination_type=destination_type, method=kwargs.get("method")
)
class ImmediateThread:
def __init__(self, target, daemon=None):
self.target = target
def start(self):
self.target()
class FakePacket:
destination_type = RNS.Destination.LINK
rssi = -80
snr = 7
q = 90
ratchet_id = bytes.fromhex("00112233445566778899aabbccddeeff")
proved = False
def prove(self):
self.proved = True
packet = FakePacket()
router_mod = sys.modules["LXMF.LXMRouter"]
real_thread = router_mod.threading.Thread
router_mod.threading.Thread = ImmediateThread
try:
router.delivery_packet(packet_packed, packet)
finally:
router_mod.threading.Thread = real_thread
if not packet.proved:
fail("LXMRouter.delivery_packet did not prove DIRECT Link DATA")
if captured.get("data") != packet_packed:
fail("LXMRouter.delivery_packet altered or prepended DIRECT LXMF data")
if captured.get("destination_type") != RNS.Destination.LINK:
fail("LXMRouter.delivery_packet lost LINK destination type")
if captured.get("method") != LXMessage.DIRECT:
fail("LXMRouter.delivery_packet did not classify LINK data as DIRECT")
print("PASS S5.2 DIRECT receive dispatch proves DATA and passes full LXMF body unchanged")
def main() -> None:
print(f"verify_link_lxmf.py against RNS {RNS.__version__} / LXMF {LXMF.__version__}")
init_minimal_rns()
try:
identities = load_json(IDS_PATH)["vectors"]
links = load_json(LINKS_PATH)["vectors"][0]
alice = next(vector for vector in identities if vector["label"] == "alice")
bob = next(vector for vector in identities if vector["label"] == "bob")
alice_identity = RNS.Identity.from_bytes(bytes.fromhex(alice["inputs"]["private_key_hex"]))
bob_identity = RNS.Identity.from_bytes(bytes.fromhex(bob["inputs"]["private_key_hex"]))
RNS.Identity.remember(bytes(32), bytes.fromhex(alice["expected"]["destination_hash_hex"]), alice_identity.get_public_key(), None)
derived_key = bytes.fromhex(links["expected"]["derived_key_hex"])
link_id = bytes.fromhex(links["expected"]["link_id_hex"])
packet_packed, _ = verify_vectors(derived_key, link_id)
verify_upstream_boundary(alice_identity, bob_identity)
verify_router_direct_dispatch(packet_packed)
print("ALL PASS")
finally:
try:
RNS.Reticulum.exit_handler()
except Exception:
pass
if __name__ == "__main__":
main()

View file

@ -14,14 +14,21 @@ Link and verifies:
requested parts and emits RESOURCE_HMU.
7. RESOURCE_RCL is emitted for advertisement rejection, while an ordinary
receiver-side cancel is local-only.
8. Deterministic Resource vector bytes round-trip.
9. Receiver assembly decrypts once, strips the prefix, handles metadata,
validates integrity, and emits RESOURCE_PRF.
10. Malformed ADV, wrong-r, corrupt-part, invalid-HMU-boundary,
oversized-decompression, and incorrect-proof cases are rejected.
Exit code 0 on PASS, non-zero on FAIL.
"""
from __future__ import annotations
import json
import os
import sys
import tempfile
import time
import LXMF
@ -32,6 +39,8 @@ from RNS.Resource import Resource, ResourceAdvertisement
FIXED_TOKEN_KEY = bytes(range(64))
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
VECTORS_PATH = os.path.join(REPO_ROOT, "test-vectors", "resources.json")
def fail(msg: str) -> None:
@ -57,6 +66,8 @@ class FakeLink:
self._token = Token(FIXED_TOKEN_KEY)
self.cancelled_incoming = []
self.cancelled_outgoing = []
self.concluded = []
self.torn_down = False
def encrypt(self, data: bytes) -> bytes:
return self._token.encrypt(data)
@ -71,7 +82,10 @@ class FakeLink:
self.cancelled_outgoing.append(resource)
def resource_concluded(self, resource) -> None:
pass
self.concluded.append(resource)
def teardown(self) -> None:
self.torn_down = True
def verify_default_lxmf_threshold() -> None:
@ -83,7 +97,7 @@ def verify_default_lxmf_threshold() -> None:
)
if expected != 319:
fail(f"S10 default direct-LXMF threshold changed: got {expected}, want 319")
print("PASS S10 default direct-LXMF Resource threshold: content > 319 bytes")
print("PASS S10 default direct-LXMF Resource threshold: computed content_size > 319")
def verify_preparation_and_advertisement() -> None:
@ -239,6 +253,190 @@ def verify_receiver_reject_vs_cancel() -> None:
print("PASS S10.9 RESOURCE_RCL rejects advertisements; ordinary receiver cancel is local-only")
def load_vector() -> dict:
try:
with open(VECTORS_PATH, "r", encoding="utf-8") as vector_file:
return json.load(vector_file)["vectors"][0]
except (OSError, KeyError, IndexError, json.JSONDecodeError) as exc:
fail(f"S10 Resource vector could not be loaded: {exc}")
def verify_deterministic_vector() -> None:
vector = load_vector()
inputs = vector["inputs"]
expected = vector["expected"]
plaintext = bytes.fromhex(inputs["plaintext_hex"])
prefix = bytes.fromhex(inputs["throwaway_prefix_hex"])
random_hash = bytes.fromhex(inputs["random_hash_hex"])
encrypted_stream = bytes.fromhex(expected["encrypted_stream_hex"])
token = Token(bytes.fromhex(inputs["token_key_hex"]))
if token.decrypt(encrypted_stream) != prefix + plaintext:
fail("S10 vector ciphertext did not decrypt to prefix || plaintext")
resource_hash = RNS.Identity.full_hash(plaintext + random_hash)
proof = RNS.Identity.full_hash(plaintext + resource_hash)
if resource_hash.hex() != expected["resource_hash_hex"]:
fail("S10 vector resource hash mismatch")
if proof.hex() != expected["expected_proof_hex"]:
fail("S10 vector expected proof mismatch")
if (resource_hash + proof).hex() != expected["resource_prf_body_hex"]:
fail("S10 vector RESOURCE_PRF body mismatch")
part_bodies = [bytes.fromhex(part["body_hex"]) for part in expected["parts"]]
if b"".join(part_bodies) != encrypted_stream:
fail("S10 vector part bodies do not concatenate to encrypted stream")
map_hashes = b"".join(
RNS.Identity.full_hash(body + random_hash)[:Resource.MAPHASH_LEN]
for body in part_bodies
)
if map_hashes.hex() != expected["hashmap_hex"]:
fail("S10 vector hashmap mismatch")
adv = ResourceAdvertisement.unpack(bytes.fromhex(expected["advertisement_plaintext_hex"]))
if adv.h != resource_hash or adv.r != random_hash or adv.m != map_hashes:
fail("S10 vector advertisement hash, random-hash, or hashmap mismatch")
if adv.n != len(part_bodies) or adv.d != len(plaintext):
fail("S10 vector advertisement part count or logical size mismatch")
print("PASS S10.2/S10.4/S10.8 deterministic Resource vector round-trip")
def make_incoming(outgoing: Resource, link: FakeLink, directory: str, parts=None) -> Resource:
incoming = Resource(None, link)
incoming.status = Resource.TRANSFERRING
incoming.initiator = False
incoming.callback = None
incoming.parts = parts if parts is not None else [part.data for part in outgoing.parts]
incoming.encrypted = outgoing.encrypted
incoming.compressed = outgoing.compressed
incoming.random_hash = outgoing.random_hash
incoming.hash = outgoing.hash
incoming.has_metadata = outgoing.has_metadata
incoming.segment_index = outgoing.segment_index
incoming.total_segments = outgoing.total_segments
incoming.storagepath = os.path.join(directory, "resource.data")
incoming.meta_storagepath = os.path.join(directory, "resource.meta")
incoming.max_decompressed_size = Resource.AUTO_COMPRESS_MAX_SIZE
incoming.data = None
class AdvertisementPacket:
pass
AdvertisementPacket.link = link
AdvertisementPacket.plaintext = ResourceAdvertisement(outgoing).pack()
incoming.advertisement_packet = AdvertisementPacket
return incoming
def verify_receiver_assembly_and_proof() -> None:
link = FakeLink()
payload = b"receiver-assembly-" * 160
metadata = {"name": "fixture.bin", "kind": "resource"}
outgoing = Resource(payload, link, metadata=metadata, advertise=False, auto_compress=True)
captured: list[RNS.Packet] = []
callback_result = {}
real_outbound = RNS.Transport.outbound
real_cache = RNS.Transport.cache
def fake_outbound(packet):
captured.append(packet)
return True
with tempfile.TemporaryDirectory(prefix="verify-resource-") as directory:
incoming = make_incoming(outgoing, link, directory)
def assembled(resource):
callback_result["data"] = resource.data.read()
callback_result["metadata"] = resource.metadata
incoming.callback = assembled
RNS.Transport.outbound = staticmethod(fake_outbound)
RNS.Transport.cache = staticmethod(lambda packet, force_cache=False: None)
try:
incoming.assemble()
finally:
RNS.Transport.outbound = real_outbound
RNS.Transport.cache = real_cache
proofs = [packet for packet in captured if packet.context == RNS.Packet.RESOURCE_PRF]
if incoming.status != Resource.COMPLETE:
fail(f"S10.8 receiver assembly status is {incoming.status}, want COMPLETE")
if callback_result != {"data": payload, "metadata": metadata}:
fail("S10.8 receiver assembly did not recover payload and metadata")
if len(proofs) != 1 or proofs[0].data != outgoing.hash + outgoing.expected_proof:
fail("S10.8 receiver did not emit the expected RESOURCE_PRF body")
outgoing.status = Resource.AWAITING_PROOF
outgoing.validate_proof(bytes(64))
if outgoing.status != Resource.AWAITING_PROOF:
fail("S10.8 initiator accepted an incorrect Resource proof")
outgoing.validate_proof(outgoing.hash + outgoing.expected_proof)
if outgoing.status != Resource.COMPLETE:
fail("S10.8 initiator rejected the correct Resource proof")
print("PASS S10.8 receiver assembly, metadata recovery, PRF emission, and proof validation")
def verify_negative_cases() -> None:
try:
ResourceAdvertisement.unpack(b"\x81\xa1t\x01")
except Exception:
pass
else:
fail("S10.4 malformed Resource advertisement was accepted")
link = FakeLink()
outgoing = Resource(b"integrity-check-" * 100, link, advertise=False, auto_compress=False)
real_outbound = RNS.Transport.outbound
captured: list[RNS.Packet] = []
RNS.Transport.outbound = staticmethod(lambda packet: captured.append(packet) or True)
try:
with tempfile.TemporaryDirectory(prefix="verify-resource-negative-") as directory:
wrong_r = make_incoming(outgoing, link, directory)
wrong_r.random_hash = bytes(Resource.RANDOM_HASH_SIZE)
wrong_r.assemble()
if wrong_r.status != Resource.CORRUPT:
fail("S10.8 Resource with wrong r was not marked CORRUPT")
with tempfile.TemporaryDirectory(prefix="verify-resource-negative-") as directory:
corrupt_parts = [part.data for part in outgoing.parts]
corrupt_parts[0] = bytes([corrupt_parts[0][0] ^ 1]) + corrupt_parts[0][1:]
corrupt = make_incoming(outgoing, link, directory, corrupt_parts)
corrupt.assemble()
if corrupt.status != Resource.CORRUPT:
fail("S10.8 Resource with corrupt part was not marked CORRUPT")
compressed = Resource(b"Z" * 5000, link, advertise=False, auto_compress=True)
with tempfile.TemporaryDirectory(prefix="verify-resource-negative-") as directory:
oversized = make_incoming(compressed, link, directory)
oversized.max_decompressed_size = 128
oversized.assemble()
if oversized.status != Resource.CORRUPT or not link.torn_down:
fail("S10.8 oversized decompression was not rejected and link torn down")
payload_size = ResourceAdvertisement.HASHMAP_MAX_LEN * outgoing.sdu
boundary = Resource(b"B" * payload_size, link, advertise=False, auto_compress=False)
boundary.status = Resource.TRANSFERRING
boundary.adv_sent = time.time()
boundary.rtt = 0.1
invalid_request = (
bytes([Resource.HASHMAP_IS_EXHAUSTED])
+ boundary.parts[0].map_hash
+ boundary.hash
)
captured.clear()
boundary.request(invalid_request)
if boundary.status != Resource.FAILED:
fail("S10.7 invalid HMU boundary did not cancel the Resource")
if any(packet.context == RNS.Packet.RESOURCE_HMU for packet in captured):
fail("S10.7 invalid HMU boundary emitted RESOURCE_HMU")
finally:
RNS.Transport.outbound = real_outbound
print("PASS S10.4/S10.7/S10.8 malformed and corrupt Resource cases are rejected")
def main() -> None:
print(f"verify_resource.py against RNS {RNS.__version__} / LXMF {LXMF.__version__}")
verify_default_lxmf_threshold()
@ -246,6 +444,9 @@ def main() -> None:
verify_multisegment_total_size()
verify_exhausted_request_with_parts()
verify_receiver_reject_vs_cancel()
verify_deterministic_vector()
verify_receiver_assembly_and_proof()
verify_negative_cases()
print("ALL PASS")

View file

@ -151,10 +151,17 @@ def verify_ticket_shortcut(target_cost=4):
lxm = LXMessage(destination=dst, source=src, content=b"x", title=b"",
fields={}, desired_method=LXMessage.OPPORTUNISTIC)
lxm.message_id = hashlib.sha256(b"ticket-shortcut-test").digest()
ticket = os.urandom(LXMessage.TICKET_LENGTH) # 16B
expected_stamp = RNS.Identity.truncated_hash(ticket + lxm.message_id)
ticket = bytes(range(LXMessage.TICKET_LENGTH))
nonce = 0
while True:
lxm.message_id = hashlib.sha256(
b"ticket-shortcut-test" + nonce.to_bytes(4, "big")
).digest()
expected_stamp = RNS.Identity.truncated_hash(ticket + lxm.message_id)
workblock = LXStamper.stamp_workblock(lxm.message_id)
if not LXStamper.stamp_valid(expected_stamp, target_cost, workblock):
break
nonce += 1
lxm.stamp = expected_stamp
if not lxm.validate_stamp(target_cost, tickets=[ticket]):
@ -164,9 +171,9 @@ def verify_ticket_shortcut(target_cost=4):
print("PASS S5.7.3 LXMessage.validate_stamp accepts ticket stamp shortcut")
# With wrong ticket — must NOT match the ticket shortcut, and the
# PoW path won't validate either (because expected_stamp wasn't
# generated against the workblock). validate_stamp returns False.
wrong_ticket = os.urandom(LXMessage.TICKET_LENGTH)
# PoW path is known not to validate either; the deterministic fixture
# above explicitly selects a ticket stamp below target_cost.
wrong_ticket = bytes(reversed(ticket))
lxm.stamp_value = None
if lxm.validate_stamp(target_cost, tickets=[wrong_ticket]):
fail("S5.7.3 validate_stamp accepted with wrong ticket")