Completed the REQUEST/RESPONSE three-tier work unit.

Added:

Tier 1 audit: audits/request-response-tier1-rns-1.2.4.md
Deterministic test-vectors/request-response.json
tools/regen_request_response.py
tools/verify_request_response.py
Corrected two material specification errors:

ALLOW_ALL = 0x01, ALLOW_LIST = 0x02; prior values were reversed.
Resource file metadata is carried inside Resource plaintext with flag x; advertisement m remains the hashmap.
Also verified packet/Resource request-ID formulas, response correlation, wrong-ID behavior, receipt states, and malformed envelopes.

Verification:

Deterministic vector SHA-256: 5cdad638…56ab2
git diff --check: pass
Portable-path scan: pass
Full pinned suite: 18 passed, 0 failed
This commit is contained in:
John Poole 2026-06-08 14:03:50 -07:00
commit cd851dab87
10 changed files with 526 additions and 13 deletions

View file

@ -57,6 +57,9 @@ As content grows, `SPEC.md` will be split into per-layer files (packet header, i
Errata that may invalidate code built against an earlier revision of `SPEC.md`. Newest first. Feature additions and ordinary edits live in `git log` — this section is reserved for cases where the spec said one thing, that turned out to be wrong, and an implementer who pulled the bad version needs to fix their code.
- **2026-06-08 — §11.4 REQUEST authorization constants were reversed.**
Earlier §11.4 text assigned `ALLOW_LIST = 0x01` and `ALLOW_ALL = 0x02`. Upstream RNS 1.2.4 defines `ALLOW_NONE = 0x00`, `ALLOW_ALL = 0x01`, and `ALLOW_LIST = 0x02` in `RNS/Destination.py`. An implementation following the prior table would expose list-restricted handlers publicly and incorrectly restrict public handlers. §11.4 is corrected and runtime-locked by `tools/verify_request_response.py`. The same audit corrected §11.2 file metadata wording: Resource advertisement field `m` is always the hashmap; file metadata is carried inside Resource plaintext and signaled by flag `x`.
- **2026-05-17 — §10.2 Resource integrity hash: the 4-byte prefix is NOT `r`, and is NOT in the hash input.**
Bad text introduced in [`95823ad`](../../commit/95823ad); on master from 2026-05-03 to 2026-05-17. §10.2 step 3 wrongly equated the random-hash *prefix* prepended to the Resource body with the advertisement's `r` field, and step 5 wrongly fed that prefix into `hash`/`expected_proof` (claiming `hash = SHA256(random_hash || body || random_hash)`). Upstream `RNS/Resource.py` (1.2.4) uses *two distinct* `get_random_hash()[:4]` values: a throwaway prefix the receiver strips and discards (`:405`/`412`, `:682`), and `self.random_hash` — the advertisement's `r` field (`:440`, `:1285`). The integrity hash is `SHA256(uncompressed_plaintext || r)` over the prefix-stripped, decompressed body (`:441`, `:694`) — exactly as §10.8 already stated. An implementer who trusted §10.2 step 5 computes a hash no spec-compliant peer accepts; every Resource is rejected as `CORRUPT`. §10.2 corrected to agree with §10.8; §10.12's wire-layering block fixed to match. Surfaced by [issue #9](../../issues/9).

21
SPEC.md
View file

@ -2831,14 +2831,12 @@ else:
The `request_id` in element [0] of the response msgpack lets the initiator match the response to the original outbound REQUEST in `Link.pending_requests` even when several requests are in flight on the same Link (`Link.handle_response` line 906-925).
> **Security: initiators MUST verify element [0].** The request_id
> check isn't decorative — without it, a misbehaving or compromised
> transit relay can replay a stale RESPONSE from a prior request and
> the initiator accepts it as the answer to whatever's currently
> pending. An implementation that drives only one in-flight request
> per link at a time is "lucky" today (the wrong-id RESPONSE just
> happens to carry sane bytes for the application to display), but
> as soon as it adds link reuse, partials, or any kind of pipelining
> the bug becomes a silent confused-deputy.
> check isn't decorative — without it, a stale or misassociated
> RESPONSE can be accepted as the answer to a different pending request.
> Upstream `Link.handle_response()` searches for an exact ID match; a
> wrong-ID response remains unmatched and the pending request eventually
> times out. Implementations with multiple in-flight requests must preserve
> this behavior. (verified by `tools/verify_request_response.py`)
>
> **Compute `expected_id` correctly.** Server-side
> `Link.handle_request:1286` is:
@ -2879,7 +2877,7 @@ if type(response) == tuple and isinstance(response[0], io.BufferedReader):
is_response=True, auto_compress=auto_compress)
```
This is how NomadNet ships large pages with attached MIME-type / size hints — the file goes through the §10 Resource pipeline; the metadata hits the advertisement's `m` slot reserved for the resource hashmap **but** also gets a separate metadata-prefix slot per §10.2 step 1 (the 3-byte length-prefixed msgpack-packed metadata blob inserted before the random_hash).
This is how NomadNet ships large pages with attached MIME-type / size hints. The file goes through the §10 Resource pipeline; metadata is encoded as the §10.2 step-1 length-prefixed msgpack value inside Resource plaintext, and advertisement flag `x` signals its presence. Advertisement field `m` remains the Resource hashmap; it does not carry file metadata.
### 11.3 Path hash collision avoidance
@ -2900,8 +2898,8 @@ Registered via `Destination.register_request_handler(path, response_generator, a
| Mode | Constant | Effect |
|---|---|---|
| `ALLOW_NONE` | `0x00` | Reject every request (handler is a stub for testing). |
| `ALLOW_LIST` | `0x01` | Accept iff the requester has identified themselves on the link (via `link.identify(identity)`) AND their identity_hash is in `allowed_list`. |
| `ALLOW_ALL` | `0x02` | Accept any request that arrives on this Link, regardless of caller identity. |
| `ALLOW_ALL` | `0x01` | Accept any request that arrives on this Link, regardless of caller identity. |
| `ALLOW_LIST` | `0x02` | Accept iff the requester has identified themselves on the link (via `link.identify(identity)`) AND their identity_hash is in `allowed_list`. |
`Link.identify(identity)` runs `LINKIDENTIFY (context = 0xFB)` packets; this is how the requester proves which long-term identity is making the request without re-running a fresh Link handshake. Most public NomadNet pages use `ALLOW_ALL`; private pages and propagation-node operator commands use `ALLOW_LIST`.
@ -3781,6 +3779,7 @@ See [`test-vectors/`](test-vectors/). Currently populated:
- **`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.
- **`request-response.json`** — deterministic packet and Resource forms for Link REQUEST/RESPONSE, including both request-ID domains and ADV correlation flags. Verified by `tools/verify_request_response.py`; regenerated by `tools/regen_request_response.py`. Covers SPEC.md §11.1-§11.5.
Remaining vector work should focus on broader negative/rejection cases rather than the original bootstrap categories.

View file

@ -168,7 +168,8 @@ Initial confidence assessment (subjective, not authoritative — re-do this audi
| §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-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. |
| §11 REQUEST/RESPONSE | Source-audited against RNS 1.2.4 and runtime-verified by `tools/verify_request_response.py`, including packet/Resource forms, request-ID domains, correlation, and authorization constants. |
| §18 Test vectors | Populated with identities, announces, opportunistic LXMF, Link establishment, link-delivered LXMF, Resource, and REQUEST/RESPONSE. 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,69 @@
# Tier 1 Audit: Link REQUEST/RESPONSE
Question: Does `SPEC.md` §11 accurately describe the generic REQUEST/RESPONSE
RPC mechanism in upstream RNS 1.2.4?
Evidence baseline:
- RNS package: `rns==1.2.4`
- Sources: `RNS/Link.py`, `RNS/Packet.py`, `RNS/Resource.py`, and
`RNS/Destination.py`
- Audit date: 2026-06-08
Tier 2 evidence lives in `tools/verify_request_response.py` and
`test-vectors/request-response.json`. Confirmed findings and corrections are
promoted into `SPEC.md` §11.
## Confirmed Model
1. `Link.request()` packs `[time.time(), truncated_hash(path UTF-8), data]`
exactly once. It selects a REQUEST packet when the packed bytes fit
`link.mdu`, otherwise a Resource (`Link.py:478-527`).
2. Packet REQUEST IDs and Resource REQUEST IDs intentionally use different
domains:
- Packet: `packet.getTruncatedHash()`, the truncated hash of the packet's
hashable wire part.
- Resource: `truncated_hash(packed_request)`, the truncated hash of the
plaintext request envelope.
3. Packet RESPONSE plaintext is `[request_id, response]`. Large responses use
Resource with `q = request_id` and the response flag (`p`) set
(`Link.py:853-904`; `Resource.py:1278-1310`).
4. `Link.handle_response()` searches `pending_requests` for an exact
`request_id`. A wrong-ID response is not actively rejected or marked
failed; it remains unmatched and the pending request eventually times out
(`Link.py:906-925`).
5. Authorization policy constants are:
```
ALLOW_NONE = 0x00
ALLOW_ALL = 0x01
ALLOW_LIST = 0x02
```
The previous §11.4 table reversed `ALLOW_ALL` and `ALLOW_LIST`.
6. Resource file-response metadata is not stored in advertisement field `m`.
`m` remains the Resource hashmap. Metadata is a length-prefixed value inside
Resource plaintext and is indicated by advertisement flag `x`.
## Tier 2 Scope
`tools/verify_request_response.py` verifies:
1. Packet REQUEST/RESPONSE headers, Link encryption, envelopes, and packet-hash
request ID.
2. Resource REQUEST/RESPONSE encryption, ADV `q/u/p` correlation fields, and
plaintext-hash request ID.
3. Packet/Resource fixtures lie on the correct side of `link.mdu`.
4. Wrong-ID responses remain unmatched while matching responses resolve and
remove the pending request.
5. Path hash formula, authorization constants, and RequestReceipt state
constants.
Malformed REQUEST/RESPONSE payloads are caught and logged by `Link.receive()`;
no protocol error response is emitted.

View file

@ -12,8 +12,9 @@ Populated against RNS 1.2.4 / LXMF 0.9.7:
- ✅ `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`).
- ✅ `request-response.json` — Link REQUEST/RESPONSE packet and Resource forms with deterministic correlation IDs (regenerator: `../tools/regen_request_response.py`, verifier: `../tools/verify_request_response.py`).
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.
All seven 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.
@ -27,6 +28,7 @@ Each vector lives in a per-domain JSON file, e.g.:
- `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
- `request-response.json` — Link REQUEST/RESPONSE packet and Resource forms
Each entry should include:
@ -53,5 +55,6 @@ For the spec to claim "an implementation that passes all test vectors interopera
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. Covered by `link-lxmf.json`.
7. **Resource transfer** — encrypt once, split into parts, validate ADV/hashmap, assemble, and emit the expected proof.
8. **REQUEST/RESPONSE** — packet and Resource RPC forms, request-ID derivation, and response correlation.
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,74 @@
{
"_about": "Deterministic RNS Link REQUEST/RESPONSE vectors. Packet requests use the truncated packet hash as request_id; Resource requests use the truncated hash of packed_request plaintext. Resource ADV q/u/p fields carry request correlation.",
"inputs": {
"link_vector_label": "alice_to_bob_aes256cbc",
"path": "/vector/echo",
"path_hash_hex": "88bc805800abebd8961cef75c491eedd",
"timestamp": 1700000000.0,
"packet_request_iv_hex": "9192939495969798999a9b9c9d9e9fa0",
"packet_response_iv_hex": "a1a2a3a4a5a6a7a8a9aaabacadaeafb0",
"resource_request_iv_hex": "b1b2b3b4b5b6b7b8b9babbbcbdbebfc0",
"resource_response_iv_hex": "c1c2c3c4c5c6c7c8c9cacbcccdcecfd0"
},
"packet_request": {
"plaintext_hex": "93cb41d954fc40000000c41088bc805800abebd8961cef75c491eedd81a76d657373616765a568656c6c6f",
"ciphertext_hex": "42d37aae0e4d2c2a8e425bc7973abba63b3746dbd5d9b16202accb75668ce78eeba657fb1b58a81645c6452890d9cdebafb879698842c08854f91a45688d05f89b06e67ad9cc5d171ed2a6837bd4e5d6cd9c3546c17d7c64ec7a03d0a5b50857",
"raw_hex": "0c007ee5fe3e4952c9ac4519b537f62784740942d37aae0e4d2c2a8e425bc7973abba63b3746dbd5d9b16202accb75668ce78eeba657fb1b58a81645c6452890d9cdebafb879698842c08854f91a45688d05f89b06e67ad9cc5d171ed2a6837bd4e5d6cd9c3546c17d7c64ec7a03d0a5b50857",
"packet_hash_hex": "4700a9939401bbaf45cd63a0c60a6c412568d8ea38d3532a1922ece757e88c8a",
"packet_truncated_hash_hex": "4700a9939401bbaf45cd63a0c60a6c41",
"request_id_hex": "4700a9939401bbaf45cd63a0c60a6c41"
},
"packet_response": {
"plaintext_hex": "92c4104700a9939401bbaf45cd63a0c60a6c4181a6616e73776572a5776f726c64",
"ciphertext_hex": "538e359cc1fc16ccf560dd379392203369ac7ea883fc9ccd09c2eebabff7b70713150a5a3c61277337d7d6ced4f940f36564747838fa5d73465d30e576010b2f8c42b8d8fd313a4f96a54b27a499c89bef51190846a8cb5a81510a8362f54002",
"raw_hex": "0c007ee5fe3e4952c9ac4519b537f62784740a538e359cc1fc16ccf560dd379392203369ac7ea883fc9ccd09c2eebabff7b70713150a5a3c61277337d7d6ced4f940f36564747838fa5d73465d30e576010b2f8c42b8d8fd313a4f96a54b27a499c89bef51190846a8cb5a81510a8362f54002",
"packet_hash_hex": "76b8d46220b118dc0f39e6ab0245065cd2289a9a2fa8feba8d7e65b0e60add41",
"packet_truncated_hash_hex": "76b8d46220b118dc0f39e6ab0245065c",
"correlation_request_id_hex": "4700a9939401bbaf45cd63a0c60a6c41"
},
"resource_request": {
"plaintext_hex": "93cb41d954fc40000000c41088bc805800abebd8961cef75c491eeddc50300000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff",
"request_id_hex": "5e64247bfe85bd9bc95c80de8780cb2a",
"resource_hash_hex": "104551ebb56a7fcf89cf80c000615ec14622bd309b323bad2fe76284873a2644",
"advertisement_plaintext_hex": "8ba174cd0360a164cd031fa16e02a168c420104551ebb56a7fcf89cf80c000615ec14622bd309b323bad2fe76284873a2644a172c40432323232a16fc420104551ebb56a7fcf89cf80c000615ec14622bd309b323bad2fe76284873a2644a16901a16c01a171c4105e64247bfe85bd9bc95c80de8780cb2aa16609a16dc40882b712a3ebe9cea3",
"parts": [
{
"body_hex": "b1b2b3b4b5b6b7b8b9babbbcbdbebfc084c06a09ae71a7bb8f968c782a8adea75b043a250d2055be454b888fa9a07154ed14daeab5bfdb31fa020dad98644a119c5d2bbe3374645e3fd466695082066fc734e35bedb111b8d6a702731279d376352c58d5497e37906addfea6a2cc7bddb6b294d727a327bb710707e396571eafd8d75d7a22841248ffdb307289d298548da9658a417f262d06b07bfaf38ac889011567bca8dea0f2544996d20a6bfd2f1410728ed3a959c926321c12e0840363a5991e5e5f047d67d43ffe0c83d51a521e344a091be94c6cc6f5d6107c74b94ec3302c9fa62d80a8bb98155081062937ab040254d18b7b3796af391131cf16dbd365e7584b173f7b4378c48e10c2e1412428326fa9e1053bfce1272b6221727723d19cd91b412381457a51a0a68589626bcfcd048f169b2ec55939f8354de72e0531ee9348c5e1a5c11eefe7af94f500533d54b9f9b3dadcae5f39ebc4f926912764f22367969a5094f56bc216bff14c9491a404ac8f445024c1cc5ca638414cd45da5623bb26f3a3ba5cac68e12ba48656b6adbb8e14365a68321fff8c64264ea0ef61f9f7d1443c23702f116c9cf9e3c8e64b25edc29bdc4bd172f925b5e5b5c10e8d7260cc9ab31b89cf8c682b688",
"map_hash_hex": "82b712a3",
"raw_hex": "0c007ee5fe3e4952c9ac4519b537f627847401b1b2b3b4b5b6b7b8b9babbbcbdbebfc084c06a09ae71a7bb8f968c782a8adea75b043a250d2055be454b888fa9a07154ed14daeab5bfdb31fa020dad98644a119c5d2bbe3374645e3fd466695082066fc734e35bedb111b8d6a702731279d376352c58d5497e37906addfea6a2cc7bddb6b294d727a327bb710707e396571eafd8d75d7a22841248ffdb307289d298548da9658a417f262d06b07bfaf38ac889011567bca8dea0f2544996d20a6bfd2f1410728ed3a959c926321c12e0840363a5991e5e5f047d67d43ffe0c83d51a521e344a091be94c6cc6f5d6107c74b94ec3302c9fa62d80a8bb98155081062937ab040254d18b7b3796af391131cf16dbd365e7584b173f7b4378c48e10c2e1412428326fa9e1053bfce1272b6221727723d19cd91b412381457a51a0a68589626bcfcd048f169b2ec55939f8354de72e0531ee9348c5e1a5c11eefe7af94f500533d54b9f9b3dadcae5f39ebc4f926912764f22367969a5094f56bc216bff14c9491a404ac8f445024c1cc5ca638414cd45da5623bb26f3a3ba5cac68e12ba48656b6adbb8e14365a68321fff8c64264ea0ef61f9f7d1443c23702f116c9cf9e3c8e64b25edc29bdc4bd172f925b5e5b5c10e8d7260cc9ab31b89cf8c682b688"
},
{
"body_hex": "b12a395e72dd7851fbc97bd0788357586abd6ce51d55628a5dbc23be1ac3753d435454d3117bd2d1eb17741ba54a67016d26c2ad12cf4c81ac0bfc37da176c1d5b3eee7fc13c00ba45dafa4dce8220ee55817976be8b0b73f12a641193bf35c96faa9569d9306170608114c28939f1c6a430273d2deec113b7025b0227d1d5ec241eb48a9c097dcccab6d275f7e9d5fbaea2d45f8bed8c87a99229adf4daa553387081aec6be45632dcf1710166a919f279400de139940cdeb99e79130c67cdebaa9b1ea48594632a0f67b70f5cf42bfd6bef12a05c12434f72450a343f3bc3b83dbe87a41f7cf76141cc385e6b58fd493447bd6c1096a81ee0985836079c647663e17cd7bd584e197b3a196cdb2ba57b26ef3447c715864a930fc0a10c953c12c45a48c3713251b9df8316d3542f27fc6a0e3e254ebf37809ff8d37ce4e630cd2703a17f11af9681a1a3af6724fc0c2b5f3f4bbe799ef97ad30b63c2a2fb038185102a1ca487a2c5439d384c9f5c03d660a04c8b12daea5e875422c83646c9f86d0bbd4cf91b980045047d494181574",
"map_hash_hex": "ebe9cea3",
"raw_hex": "0c007ee5fe3e4952c9ac4519b537f627847401b12a395e72dd7851fbc97bd0788357586abd6ce51d55628a5dbc23be1ac3753d435454d3117bd2d1eb17741ba54a67016d26c2ad12cf4c81ac0bfc37da176c1d5b3eee7fc13c00ba45dafa4dce8220ee55817976be8b0b73f12a641193bf35c96faa9569d9306170608114c28939f1c6a430273d2deec113b7025b0227d1d5ec241eb48a9c097dcccab6d275f7e9d5fbaea2d45f8bed8c87a99229adf4daa553387081aec6be45632dcf1710166a919f279400de139940cdeb99e79130c67cdebaa9b1ea48594632a0f67b70f5cf42bfd6bef12a05c12434f72450a343f3bc3b83dbe87a41f7cf76141cc385e6b58fd493447bd6c1096a81ee0985836079c647663e17cd7bd584e197b3a196cdb2ba57b26ef3447c715864a930fc0a10c953c12c45a48c3713251b9df8316d3542f27fc6a0e3e254ebf37809ff8d37ce4e630cd2703a17f11af9681a1a3af6724fc0c2b5f3f4bbe799ef97ad30b63c2a2fb038185102a1ca487a2c5439d384c9f5c03d660a04c8b12daea5e875422c83646c9f86d0bbd4cf91b980045047d494181574"
}
]
},
"resource_response": {
"plaintext_hex": "92c4105e64247bfe85bd9bc95c80de8780cb2ac50300fffefdfcfbfaf9f8f7f6f5f4f3f2f1f0efeeedecebeae9e8e7e6e5e4e3e2e1e0dfdedddcdbdad9d8d7d6d5d4d3d2d1d0cfcecdcccbcac9c8c7c6c5c4c3c2c1c0bfbebdbcbbbab9b8b7b6b5b4b3b2b1b0afaeadacabaaa9a8a7a6a5a4a3a2a1a09f9e9d9c9b9a999897969594939291908f8e8d8c8b8a898887868584838281807f7e7d7c7b7a797877767574737271706f6e6d6c6b6a696867666564636261605f5e5d5c5b5a595857565554535251504f4e4d4c4b4a494847464544434241403f3e3d3c3b3a393837363534333231302f2e2d2c2b2a292827262524232221201f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100fffefdfcfbfaf9f8f7f6f5f4f3f2f1f0efeeedecebeae9e8e7e6e5e4e3e2e1e0dfdedddcdbdad9d8d7d6d5d4d3d2d1d0cfcecdcccbcac9c8c7c6c5c4c3c2c1c0bfbebdbcbbbab9b8b7b6b5b4b3b2b1b0afaeadacabaaa9a8a7a6a5a4a3a2a1a09f9e9d9c9b9a999897969594939291908f8e8d8c8b8a898887868584838281807f7e7d7c7b7a797877767574737271706f6e6d6c6b6a696867666564636261605f5e5d5c5b5a595857565554535251504f4e4d4c4b4a494847464544434241403f3e3d3c3b3a393837363534333231302f2e2d2c2b2a292827262524232221201f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100fffefdfcfbfaf9f8f7f6f5f4f3f2f1f0efeeedecebeae9e8e7e6e5e4e3e2e1e0dfdedddcdbdad9d8d7d6d5d4d3d2d1d0cfcecdcccbcac9c8c7c6c5c4c3c2c1c0bfbebdbcbbbab9b8b7b6b5b4b3b2b1b0afaeadacabaaa9a8a7a6a5a4a3a2a1a09f9e9d9c9b9a999897969594939291908f8e8d8c8b8a898887868584838281807f7e7d7c7b7a797877767574737271706f6e6d6c6b6a696867666564636261605f5e5d5c5b5a595857565554535251504f4e4d4c4b4a494847464544434241403f3e3d3c3b3a393837363534333231302f2e2d2c2b2a292827262524232221201f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100",
"request_id_hex": "5e64247bfe85bd9bc95c80de8780cb2a",
"resource_hash_hex": "c74320c211285d8f76a3d13c7b112d6d3b269403677f9c7c1a58d18310b8999d",
"advertisement_plaintext_hex": "8ba174cd0350a164cd0316a16e02a168c420c74320c211285d8f76a3d13c7b112d6d3b269403677f9c7c1a58d18310b8999da172c40442424242a16fc420c74320c211285d8f76a3d13c7b112d6d3b269403677f9c7c1a58d18310b8999da16901a16c01a171c4105e64247bfe85bd9bc95c80de8780cb2aa16611a16dc40807d78700f293aecd",
"parts": [
{
"body_hex": "c1c2c3c4c5c6c7c8c9cacbcccdcecfd056c0b957e1404ebe5393a2293061e245646ac70030afbc1c7d51a0a8a0653d5dd616069d8cdb92cff0f5812ebbeb8b5f219fa1fc48b06ae1aba9c48fbbc9405f94f3362a6bfe66bcb279e26093d5a14d0ad8f0982c1fc858c8ef841bc7efb1ef67ab5368b772aa05af2b77f815c56a269eaf6c2312f442ff004974d456994dc1157856eb275d14b40c506def6a071b0c6fa677ba8083c5dd1615e8d74c25ea61b84b0387ce8f40f4c5ad9a35587d33a7e7a6659bb2e48f541f080822cf03262f0b7a9c7a7f77759451fe4c925047b84a0905f68f43922fb369d8396f4a5e0c5c36b9af3501fe5ddab1028bf21c69b17cda0af87117019027bd60d88d7db28b9eb591e79fc6465d413b104f83b17b0559ec5345fa08c28f54192c6dd41bd13db6db57bbad9f5cb0a7eb4968644cdefe7586718736d77a77f0c6668965b61842f55a333ffa72d7efcb52ce0f9518fcd82c8fd2784a2a7b7d7e6d3a8f9fbb5c106e2deadf7ab4b88b1ffbb2ffc336919deba69bf6446cb0c0339e04fd09a7b2763ebe7675c8d3e8726771945d1c70ee9a3e585af46a8199f834f4abc152cb56e59983ee916366d3e5250c5be068fde57a2ad00a31c17e35b25d855f068bab2dc2e0",
"map_hash_hex": "07d78700",
"raw_hex": "0c007ee5fe3e4952c9ac4519b537f627847401c1c2c3c4c5c6c7c8c9cacbcccdcecfd056c0b957e1404ebe5393a2293061e245646ac70030afbc1c7d51a0a8a0653d5dd616069d8cdb92cff0f5812ebbeb8b5f219fa1fc48b06ae1aba9c48fbbc9405f94f3362a6bfe66bcb279e26093d5a14d0ad8f0982c1fc858c8ef841bc7efb1ef67ab5368b772aa05af2b77f815c56a269eaf6c2312f442ff004974d456994dc1157856eb275d14b40c506def6a071b0c6fa677ba8083c5dd1615e8d74c25ea61b84b0387ce8f40f4c5ad9a35587d33a7e7a6659bb2e48f541f080822cf03262f0b7a9c7a7f77759451fe4c925047b84a0905f68f43922fb369d8396f4a5e0c5c36b9af3501fe5ddab1028bf21c69b17cda0af87117019027bd60d88d7db28b9eb591e79fc6465d413b104f83b17b0559ec5345fa08c28f54192c6dd41bd13db6db57bbad9f5cb0a7eb4968644cdefe7586718736d77a77f0c6668965b61842f55a333ffa72d7efcb52ce0f9518fcd82c8fd2784a2a7b7d7e6d3a8f9fbb5c106e2deadf7ab4b88b1ffbb2ffc336919deba69bf6446cb0c0339e04fd09a7b2763ebe7675c8d3e8726771945d1c70ee9a3e585af46a8199f834f4abc152cb56e59983ee916366d3e5250c5be068fde57a2ad00a31c17e35b25d855f068bab2dc2e0"
},
{
"body_hex": "240597e65c52634ae2f630db036cc7c390125c78fb547ba855a1ee16b6d835879e895e230fe370db07d6331532883ec74be95e317ead177077328bea851227eea79a8bbc275532b78aa2aab282a497d609de8d51a7508e28e275e9788442a5ca431a982bc4f2b228bc986a37da1594dfc6bdc623680c3e5c646a5dcc1229a12ef42e003974007439ffd54adcff1980af0c506da52dc991a71530b04e798024295aa5115f655f0f5a47117bc5ee5be13238ea4f26063b465877ba861a4ad454bb0c2714249fd8a7a2b508e2e881d7ec9b10e151a9552a36d3281165cb1148213206ac759ede15c50c9b592fb8d56c298bc23dc62668654ae11f13f9058518ba1c32105766ffb95ac02530dbf949770ad5ab953b6f2ffa723c32206b7efcd18326562c46bfeddfadee5b41d9f52ce5a654e5d4bb7804bf276f86713742525cfb538f066db6f88dd4ac160dbc0ea1d214b1f4723551fa0dc613c86ce55dc1cb20df0c61f39b28ed49fc23e545eb6ccaa9d6ca2ef6dd0ca8d4a86b9941161aaa0b59",
"map_hash_hex": "f293aecd",
"raw_hex": "0c007ee5fe3e4952c9ac4519b537f627847401240597e65c52634ae2f630db036cc7c390125c78fb547ba855a1ee16b6d835879e895e230fe370db07d6331532883ec74be95e317ead177077328bea851227eea79a8bbc275532b78aa2aab282a497d609de8d51a7508e28e275e9788442a5ca431a982bc4f2b228bc986a37da1594dfc6bdc623680c3e5c646a5dcc1229a12ef42e003974007439ffd54adcff1980af0c506da52dc991a71530b04e798024295aa5115f655f0f5a47117bc5ee5be13238ea4f26063b465877ba861a4ad454bb0c2714249fd8a7a2b508e2e881d7ec9b10e151a9552a36d3281165cb1148213206ac759ede15c50c9b592fb8d56c298bc23dc62668654ae11f13f9058518ba1c32105766ffb95ac02530dbf949770ad5ab953b6f2ffa723c32206b7efcd18326562c46bfeddfadee5b41d9f52ce5a654e5d4bb7804bf276f86713742525cfb538f066db6f88dd4ac160dbc0ea1d214b1f4723551fa0dc613c86ce55dc1cb20df0c61f39b28ed49fc23e545eb6ccaa9d6ca2ef6dd0ca8d4a86b9941161aaa0b59"
}
]
},
"rns_version_at_generation": "1.2.4",
"generator_script": "tools/regen_request_response.py",
"verifies_spec_sections": [
"11.1",
"11.2",
"11.3",
"11.4",
"11.5"
]
}

View file

@ -57,6 +57,11 @@ Outstanding work for the spec repo.
PACKET/Resource boundary, Link decrypt/parse, wrong-key rejection, and
DIRECT receive dispatch.
- [x] **Deterministic REQUEST/RESPONSE vectors and verifier.** Added
`test-vectors/request-response.json`, `tools/regen_request_response.py`,
and `tools/verify_request_response.py`. Corrected §11 authorization
constants and Resource file-metadata wording.
## Open `⚠️ UNVERIFIED` items in SPEC.md
These need either a runtime test or a stronger upstream source citation

View file

@ -58,11 +58,13 @@ Populated against RNS 1.2.4 / LXMF 0.9.7:
| `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.9, §10.11, §10.12 — vectors, whole-stream encryption/slicing, receiver assembly/proof, control behavior, multi-segment size, and negative cases | ✅ |
| `verify_request_response.py` | §11.1-§11.5 — packet/Resource RPC forms, request-ID domains, correlation, authorization constants, receipt states | ✅ |
| `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) | ✅ |
| `regen_request_response.py` | regenerates `test-vectors/request-response.json` (deterministic packet and Resource RPC forms) | ✅ |
See [`../agent.md`](../agent.md) §3 and [`../todo.md`](../todo.md) for the evidence model and remaining priority order.

View file

@ -0,0 +1,183 @@
"""
Regenerator for test-vectors/request-response.json.
Builds deterministic packet and Resource forms for the generic RNS Link
REQUEST/RESPONSE protocol using the Link session key from links.json.
"""
from __future__ import annotations
import json
import os
import sys
import RNS
from RNS.Cryptography.Token import Token
from RNS.Resource import Resource, ResourceAdvertisement
from RNS.vendor import umsgpack
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
OUT_PATH = os.path.join(REPO_ROOT, "test-vectors", "request-response.json")
LINKS_PATH = os.path.join(REPO_ROOT, "test-vectors", "links.json")
FIXED_TIME = 1700000000.0
REQUEST_PACKET_IV = bytes.fromhex("9192939495969798999a9b9c9d9e9fa0")
RESPONSE_PACKET_IV = bytes.fromhex("a1a2a3a4a5a6a7a8a9aaabacadaeafb0")
REQUEST_RESOURCE_IV = bytes.fromhex("b1b2b3b4b5b6b7b8b9babbbcbdbebfc0")
RESPONSE_RESOURCE_IV = bytes.fromhex("c1c2c3c4c5c6c7c8c9cacbcccdcecfd0")
class FakeLink:
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 with_fixed_iv(iv: bytes, callback):
token_mod = sys.modules["RNS.Cryptography.Token"]
real_urandom = token_mod.os.urandom
token_mod.os.urandom = lambda length: iv if length == 16 else real_urandom(length)
try:
return callback()
finally:
token_mod.os.urandom = real_urandom
def deterministic_resource(data: bytes, link: FakeLink, request_id: bytes, is_response: bool, iv: bytes, seed: int):
real_get_random_hash = RNS.Identity.get_random_hash
hashes = [
bytes([seed]) * 4 + bytes(28),
bytes([seed + 1]) * 4 + bytes(28),
]
RNS.Identity.get_random_hash = staticmethod(lambda: hashes.pop(0))
try:
return with_fixed_iv(
iv,
lambda: Resource(
data,
link,
request_id=request_id,
is_response=is_response,
advertise=False,
auto_compress=False,
),
)
finally:
RNS.Identity.get_random_hash = staticmethod(real_get_random_hash)
def packet_fields(packet: RNS.Packet) -> dict:
return {
"plaintext_hex": packet.data.hex(),
"ciphertext_hex": packet.ciphertext.hex(),
"raw_hex": packet.raw.hex(),
"packet_hash_hex": packet.get_hash().hex(),
"packet_truncated_hash_hex": packet.getTruncatedHash().hex(),
}
def resource_fields(resource: Resource, plaintext: bytes) -> dict:
return {
"plaintext_hex": plaintext.hex(),
"request_id_hex": resource.request_id.hex(),
"resource_hash_hex": resource.hash.hex(),
"advertisement_plaintext_hex": ResourceAdvertisement(resource).pack().hex(),
"parts": [
{"body_hex": part.data.hex(), "map_hash_hex": part.map_hash.hex(), "raw_hex": part.raw.hex()}
for part in resource.parts
],
}
def main() -> None:
print(f"regen_request_response.py against RNS {RNS.__version__}")
with open(LINKS_PATH, "r", encoding="utf-8") as links_file:
link_vector = json.load(links_file)["vectors"][0]
derived_key = bytes.fromhex(link_vector["expected"]["derived_key_hex"])
link_id = bytes.fromhex(link_vector["expected"]["link_id_hex"])
link = FakeLink(derived_key, link_id)
path = "/vector/echo"
path_hash = RNS.Identity.truncated_hash(path.encode("utf-8"))
small_request = umsgpack.packb([FIXED_TIME, path_hash, {"message": "hello"}])
packet_request = with_fixed_iv(
REQUEST_PACKET_IV,
lambda: RNS.Packet(link, small_request, context=RNS.Packet.REQUEST),
)
packet_request.pack()
packet_request_id = packet_request.getTruncatedHash()
small_response = umsgpack.packb([packet_request_id, {"answer": "world"}])
packet_response = with_fixed_iv(
RESPONSE_PACKET_IV,
lambda: RNS.Packet(link, small_response, context=RNS.Packet.RESPONSE),
)
packet_response.pack()
large_request = umsgpack.packb([FIXED_TIME, path_hash, bytes(range(256)) * 3])
resource_request_id = RNS.Identity.truncated_hash(large_request)
request_resource = deterministic_resource(
large_request, link, resource_request_id, False, REQUEST_RESOURCE_IV, 0x31
)
large_response = umsgpack.packb([resource_request_id, bytes(range(255, -1, -1)) * 3])
response_resource = deterministic_resource(
large_response, link, resource_request_id, True, RESPONSE_RESOURCE_IV, 0x41
)
payload = {
"_about": (
"Deterministic RNS Link REQUEST/RESPONSE vectors. Packet requests use "
"the truncated packet hash as request_id; Resource requests use the "
"truncated hash of packed_request plaintext. Resource ADV q/u/p fields "
"carry request correlation."
),
"inputs": {
"link_vector_label": "alice_to_bob_aes256cbc",
"path": path,
"path_hash_hex": path_hash.hex(),
"timestamp": FIXED_TIME,
"packet_request_iv_hex": REQUEST_PACKET_IV.hex(),
"packet_response_iv_hex": RESPONSE_PACKET_IV.hex(),
"resource_request_iv_hex": REQUEST_RESOURCE_IV.hex(),
"resource_response_iv_hex": RESPONSE_RESOURCE_IV.hex(),
},
"packet_request": {
**packet_fields(packet_request),
"request_id_hex": packet_request_id.hex(),
},
"packet_response": {
**packet_fields(packet_response),
"correlation_request_id_hex": packet_request_id.hex(),
},
"resource_request": resource_fields(request_resource, large_request),
"resource_response": resource_fields(response_resource, large_response),
"rns_version_at_generation": RNS.__version__,
"generator_script": "tools/regen_request_response.py",
"verifies_spec_sections": ["11.1", "11.2", "11.3", "11.4", "11.5"],
}
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}")
print("ALL PASS")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,174 @@
"""
Verifier for SPEC.md S11 generic Link REQUEST/RESPONSE protocol.
Checks packet and Resource wire forms, distinct request-ID formulas,
response correlation behavior, malformed/wrong-ID handling, path hashes, and
authorization policy constants.
"""
from __future__ import annotations
import json
import os
import sys
import RNS
from RNS.Cryptography.Token import Token
from RNS.Link import RequestReceipt
from RNS.Resource import Resource, ResourceAdvertisement
from RNS.vendor import umsgpack
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
VECTORS_PATH = os.path.join(REPO_ROOT, "test-vectors", "request-response.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 load_json(path: str):
with open(path, "r", encoding="utf-8") as input_file:
return json.load(input_file)
def verify_packet_forms(vector: dict, token: Token, link_id: bytes) -> None:
request = vector["packet_request"]
request_raw = bytes.fromhex(request["raw_hex"])
request_packet = RNS.Packet(None, request_raw)
if not request_packet.unpack():
fail("S11.1 packet REQUEST did not unpack")
if request_packet.context != RNS.Packet.REQUEST or request_packet.destination_hash != link_id:
fail("S11.1 packet REQUEST header mismatch")
plaintext = token.decrypt(request_packet.data)
if plaintext.hex() != request["plaintext_hex"]:
fail("S11.1 packet REQUEST decrypt mismatch")
decoded = umsgpack.unpackb(plaintext)
if decoded[1].hex() != vector["inputs"]["path_hash_hex"] or not isinstance(decoded[2], dict):
fail("S11.1 packet REQUEST path hash or single-packed data mismatch")
if request_packet.getTruncatedHash().hex() != request["request_id_hex"]:
fail("S11.1 packet request_id is not the truncated packet hash")
if RNS.Identity.truncated_hash(plaintext).hex() == request["request_id_hex"]:
fail("S11.1 fixture did not distinguish packet hash from plaintext hash")
if len(plaintext) > RNS.Link.MDU:
fail("S11.1 packet REQUEST fixture exceeds Link MDU")
response = vector["packet_response"]
response_packet = RNS.Packet(None, bytes.fromhex(response["raw_hex"]))
if not response_packet.unpack() or response_packet.context != RNS.Packet.RESPONSE:
fail("S11.2 packet RESPONSE header mismatch")
response_plaintext = token.decrypt(response_packet.data)
response_decoded = umsgpack.unpackb(response_plaintext)
if (
response_decoded[0].hex() != request["request_id_hex"]
or response_decoded[0].hex() != response["correlation_request_id_hex"]
):
fail("S11.2 packet RESPONSE did not carry packet request_id")
print("PASS S11.1/S11.2 packet REQUEST/RESPONSE wire forms and packet-hash request_id")
def verify_resource_form(vector: dict, key: str, token: Token, expect_response: bool) -> None:
resource = vector[key]
plaintext = bytes.fromhex(resource["plaintext_hex"])
request_id = bytes.fromhex(resource["request_id_hex"])
stream = b"".join(bytes.fromhex(part["body_hex"]) for part in resource["parts"])
decrypted = token.decrypt(stream)[Resource.RANDOM_HASH_SIZE :]
if decrypted != plaintext:
fail(f"S11 Resource {key} did not decrypt to packed plaintext")
adv = ResourceAdvertisement.unpack(bytes.fromhex(resource["advertisement_plaintext_hex"]))
if adv.q != request_id:
fail(f"S11 Resource {key} ADV q mismatch")
if expect_response and (not adv.p or adv.u):
fail("S11.2 Resource response flags mismatch")
if not expect_response and (not adv.u or adv.p):
fail("S11.1 Resource request flags mismatch")
if not expect_response and RNS.Identity.truncated_hash(plaintext) != request_id:
fail("S11.1 Resource request_id is not truncated plaintext hash")
if len(plaintext) <= RNS.Link.MDU:
fail(f"S11 Resource {key} fixture does not exceed Link MDU")
def verify_correlation_behavior(vector: dict) -> None:
expected_id = bytes.fromhex(vector["packet_request"]["request_id_hex"])
class Pending:
def __init__(self, request_id):
self.request_id = request_id
self.response_size = None
self.response_transfer_size = None
self.received = []
def response_received(self, response, metadata=None):
self.received.append((response, metadata))
class FakeLink:
status = RNS.Link.ACTIVE
def __init__(self):
self.pending_requests = [Pending(expected_id)]
link = FakeLink()
wrong_id = bytes(reversed(expected_id))
RNS.Link.handle_response(link, wrong_id, b"wrong", 5, 5)
if len(link.pending_requests) != 1 or link.pending_requests[0].received:
fail("S11.2 wrong request_id response was not ignored")
RNS.Link.handle_response(link, expected_id, b"right", 5, 5)
if link.pending_requests or not link.pending_requests == []:
fail("S11.2 matched response did not remove pending request")
print("PASS S11.2 wrong-ID response remains unmatched; matching response resolves pending request")
def verify_constants_and_receipt_states(vector: dict) -> None:
if (RNS.Destination.ALLOW_NONE, RNS.Destination.ALLOW_ALL, RNS.Destination.ALLOW_LIST) != (0, 1, 2):
fail("S11.4 authorization constants changed")
path = vector["inputs"]["path"]
if RNS.Identity.truncated_hash(path.encode("utf-8")).hex() != vector["inputs"]["path_hash_hex"]:
fail("S11.3 path hash mismatch")
if (RequestReceipt.FAILED, RequestReceipt.SENT, RequestReceipt.DELIVERED, RequestReceipt.RECEIVING, RequestReceipt.READY) != (0, 1, 2, 3, 4):
fail("S11.5 RequestReceipt states changed")
print("PASS S11.3/S11.4/S11.5 path hash, authorization constants, and receipt states")
def verify_malformed_envelopes() -> None:
class FakeLink:
status = RNS.Link.ACTIVE
try:
RNS.Link.handle_request(FakeLink(), bytes(16), [])
except (IndexError, TypeError):
pass
else:
fail("S11.1 malformed request envelope reached request handling")
for malformed in [b"", b"\x91\xc0"]:
try:
decoded = umsgpack.unpackb(malformed)
request_id = decoded[0]
response_data = decoded[1]
_ = (request_id, response_data)
except Exception:
continue
fail("S11.2 malformed response envelope exposed request_id and response")
print("PASS S11.1/S11.2 malformed request and response envelopes cannot be handled")
def main() -> None:
print(f"verify_request_response.py against RNS {RNS.__version__}")
vector = load_json(VECTORS_PATH)
link_vector = load_json(LINKS_PATH)["vectors"][0]
token = Token(bytes.fromhex(link_vector["expected"]["derived_key_hex"]))
link_id = bytes.fromhex(link_vector["expected"]["link_id_hex"])
verify_packet_forms(vector, token, link_id)
verify_resource_form(vector, "resource_request", token, False)
verify_resource_form(vector, "resource_response", token, True)
print("PASS S11.1/S11.2 Resource REQUEST/RESPONSE ADV correlation and plaintext-hash request_id")
verify_correlation_behavior(vector)
verify_constants_and_receipt_states(vector)
verify_malformed_envelopes()
print("ALL PASS")
if __name__ == "__main__":
main()