diff --git a/README.md b/README.md index ac36eb7..00e0858 100644 --- a/README.md +++ b/README.md @@ -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). diff --git a/SPEC.md b/SPEC.md index 5f5ed90..93db9a1 100644 --- a/SPEC.md +++ b/SPEC.md @@ -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. diff --git a/agent.md b/agent.md index cf63228..bb9dab7 100644 --- a/agent.md +++ b/agent.md @@ -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:** diff --git a/audits/request-response-tier1-rns-1.2.4.md b/audits/request-response-tier1-rns-1.2.4.md new file mode 100644 index 0000000..d441feb --- /dev/null +++ b/audits/request-response-tier1-rns-1.2.4.md @@ -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. diff --git a/test-vectors/README.md b/test-vectors/README.md index ee5af70..1bbe548 100644 --- a/test-vectors/README.md +++ b/test-vectors/README.md @@ -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. diff --git a/test-vectors/request-response.json b/test-vectors/request-response.json new file mode 100644 index 0000000..0f01f56 --- /dev/null +++ b/test-vectors/request-response.json @@ -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" + ] +} diff --git a/todo.md b/todo.md index 1e9277d..0302c42 100644 --- a/todo.md +++ b/todo.md @@ -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 diff --git a/tools/README.md b/tools/README.md index 71bbf3d..3811b7f 100644 --- a/tools/README.md +++ b/tools/README.md @@ -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. diff --git a/tools/regen_request_response.py b/tools/regen_request_response.py new file mode 100644 index 0000000..37de284 --- /dev/null +++ b/tools/regen_request_response.py @@ -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() diff --git a/tools/verify_request_response.py b/tools/verify_request_response.py new file mode 100644 index 0000000..4337920 --- /dev/null +++ b/tools/verify_request_response.py @@ -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()