reticiulum-specification/tools/verify_resource.py
John Poole 7433063bfb Completed the Resource three-tier work unit.
Added deterministic `resources.json` and `regen_resources.py`.
Extended `verify_resource.py` with receiver assembly/proof and requested negative cases.
Updated specification, audit, status, and tool documentation.
Fixed an unrelated nondeterministic wrong-ticket test in verify_stamps.py.
Confirmed vector regeneration is byte-identical.
Confirmed no tracked reliance on specenv or user-specific paths.
git diff --check: pass.
Complete pinned suite: 16 passed, 0 failed.
2026-06-08 13:38:24 -07:00

454 lines
18 KiB
Python

"""
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: content > 319 bytes")
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()