""" Verifier for SPEC.md S10 (Resource fragmentation protocol). Exercises upstream RNS 1.2.4 Resource construction with a deterministic fake Link and verifies: 1. Default direct-LXMF single-packet content limit is 319 bytes. 2. Resource encrypts one complete stream, then slices that ciphertext into RESOURCE packet bodies without packet-level re-encryption. 3. Resource hash, expected proof, and map-hash formulas. 4. RESOURCE_ADV fields and flags. 5. Multi-segment advertisement `d` is total logical-resource size. 6. An exhausted RESOURCE_REQ may carry part requests; upstream fulfils the 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 import RNS from LXMF.LXMessage import LXMessage from RNS.Cryptography.Token import Token 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: print(f"FAIL: {msg}") sys.exit(1) class FakeLink: """Minimum Link surface needed by Resource and Packet construction.""" def __init__(self, mtu: int = RNS.Reticulum.MTU): self.type = RNS.Destination.LINK self.status = RNS.Link.ACTIVE self.mtu = 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) self.cancelled_incoming = [] self.cancelled_outgoing = [] self.concluded = [] self.torn_down = False def encrypt(self, data: bytes) -> bytes: return self._token.encrypt(data) def decrypt(self, data: bytes) -> bytes: return self._token.decrypt(data) def cancel_incoming_resource(self, resource) -> None: self.cancelled_incoming.append(resource) def cancel_outgoing_resource(self, resource) -> None: self.cancelled_outgoing.append(resource) def resource_concluded(self, resource) -> None: self.concluded.append(resource) def teardown(self) -> None: self.torn_down = True def verify_default_lxmf_threshold() -> None: expected = RNS.Link.MDU - LXMessage.LXMF_OVERHEAD if LXMessage.LINK_PACKET_MAX_CONTENT != expected: fail( "S10 direct-LXMF threshold formula mismatch: " f"{LXMessage.LINK_PACKET_MAX_CONTENT} != {expected}" ) if expected != 319: fail(f"S10 default direct-LXMF threshold changed: got {expected}, want 319") print("PASS S10 default direct-LXMF Resource threshold: computed content_size > 319") def verify_preparation_and_advertisement() -> None: link = FakeLink() plaintext = (b"resource-verifier-" * 70) + bytes(range(64)) resource = Resource(plaintext, link, advertise=False, auto_compress=False) stream = b"".join(part.data for part in resource.parts) decrypted = link.decrypt(stream) prefix = decrypted[:Resource.RANDOM_HASH_SIZE] recovered = decrypted[Resource.RANDOM_HASH_SIZE:] if recovered != plaintext: fail("S10.2 whole-stream decrypt did not recover original plaintext") if len(prefix) != Resource.RANDOM_HASH_SIZE: fail("S10.2 throwaway prefix has wrong length") for index, part in enumerate(resource.parts): if part.context != RNS.Packet.RESOURCE: fail(f"S10.3 part {index} has context {part.context:#x}, want RESOURCE") if part.raw[19:] != part.data: fail(f"S10.6 part {index} was packet-level encrypted or altered") expected_hash = RNS.Identity.full_hash(plaintext + resource.random_hash) expected_proof = RNS.Identity.full_hash(plaintext + expected_hash) if resource.hash != expected_hash: fail("S10.2 resource hash formula mismatch") if resource.expected_proof != expected_proof: fail("S10.2 expected proof formula mismatch") for index, part in enumerate(resource.parts): expected_map_hash = RNS.Identity.full_hash(part.data + resource.random_hash)[:Resource.MAPHASH_LEN] if part.map_hash != expected_map_hash: fail(f"S10.2 map-hash formula mismatch for part {index}") adv = ResourceAdvertisement.unpack(ResourceAdvertisement(resource).pack()) if adv.t != len(stream): fail(f"S10.4 ADV t mismatch: {adv.t} != {len(stream)}") if adv.d != len(plaintext): fail(f"S10.4 ADV d mismatch for single segment: {adv.d} != {len(plaintext)}") if adv.n != len(resource.parts): fail(f"S10.4 ADV n mismatch: {adv.n} != {len(resource.parts)}") if adv.h != resource.hash or adv.r != resource.random_hash: fail("S10.4 ADV hash or random-hash field mismatch") if not adv.e or adv.c or adv.s or adv.u or adv.p or adv.x: fail(f"S10.4 ADV flags unexpected: {adv.f:#x}") print( "PASS S10.2/S10.4/S10.6 whole-stream encryption, ciphertext slicing, " "hash formulas, and ADV fields" ) def verify_multisegment_total_size() -> None: link = FakeLink() logical_size = Resource.MAX_EFFICIENT_SIZE + 257 resource = Resource(b"M" * logical_size, link, advertise=False, auto_compress=False) adv = ResourceAdvertisement.unpack(ResourceAdvertisement(resource).pack()) if resource.total_segments != 2 or not resource.split: fail("S10.11 expected a two-segment Resource") if adv.d != logical_size: fail(f"S10.4 multi-segment ADV d is {adv.d}, want total logical size {logical_size}") if resource.uncompressed_size >= logical_size: fail("S10.4 first-segment plaintext unexpectedly equals total logical size") print( "PASS S10.4/S10.11 multi-segment ADV d is total logical-resource size, " "not current-segment plaintext size" ) def verify_exhausted_request_with_parts() -> None: link = FakeLink() payload_size = ResourceAdvertisement.HASHMAP_MAX_LEN * (link.mtu - RNS.Reticulum.HEADER_MAXSIZE - RNS.Reticulum.IFAC_MIN_SIZE) resource = Resource(b"R" * payload_size, link, advertise=False, auto_compress=False) if len(resource.parts) <= ResourceAdvertisement.HASHMAP_MAX_LEN: fail("S10.7 fixture did not produce a multi-segment hashmap") resource.status = Resource.TRANSFERRING resource.adv_sent = time.time() resource.rtt = 0.1 requested_part = resource.parts[0] last_known_index = ResourceAdvertisement.HASHMAP_MAX_LEN - 1 last_known_hash = resource.parts[last_known_index].map_hash request_data = ( bytes([Resource.HASHMAP_IS_EXHAUSTED]) + last_known_hash + resource.hash + requested_part.map_hash ) captured: list[RNS.Packet] = [] real_outbound = RNS.Transport.outbound def fake_outbound(packet): captured.append(packet) return True RNS.Transport.outbound = staticmethod(fake_outbound) try: resource.request(request_data) finally: RNS.Transport.outbound = real_outbound contexts = [packet.context for packet in captured] if RNS.Packet.RESOURCE not in contexts: fail("S10.7 exhausted RESOURCE_REQ did not fulfil bundled part request") if RNS.Packet.RESOURCE_HMU not in contexts: fail("S10.7 exhausted RESOURCE_REQ did not emit RESOURCE_HMU") print("PASS S10.7 exhausted RESOURCE_REQ fulfils bundled parts and emits RESOURCE_HMU") def verify_receiver_reject_vs_cancel() -> None: link = FakeLink() outgoing = Resource(b"reject-me", link, advertise=False, auto_compress=False) advertisement_plaintext = ResourceAdvertisement(outgoing).pack() class AdvertisementPacket: plaintext = advertisement_plaintext link = None AdvertisementPacket.link = link captured: list[RNS.Packet] = [] real_outbound = RNS.Transport.outbound def fake_outbound(packet): captured.append(packet) return True RNS.Transport.outbound = staticmethod(fake_outbound) try: Resource.reject(AdvertisementPacket) if [packet.context for packet in captured] != [RNS.Packet.RESOURCE_RCL]: fail("S10.9 advertisement rejection did not emit exactly one RESOURCE_RCL") captured.clear() incoming = Resource(None, link) incoming.status = Resource.TRANSFERRING incoming.initiator = False incoming.callback = None incoming.cancel() if captured: fail("S10.9 ordinary receiver-side cancel unexpectedly emitted a packet") if incoming not in link.cancelled_incoming: fail("S10.9 ordinary receiver-side cancel did not remove incoming Resource") finally: RNS.Transport.outbound = real_outbound 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() verify_preparation_and_advertisement() 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") if __name__ == "__main__": main()