reticiulum-specification/tools/verify_proof_packet.py

182 lines
6.1 KiB
Python
Raw Normal View History

Add four more verifiers + receive-propagated flow + frontmatter version Verifiers: tools/verify_proof_packet.py — locks in §6.5. Toggles Reticulum.__use_implicit_proof to test both modes; confirms Identity.prove emits 64B (implicit) or 96B (explicit) proof body; PacketReceipt.validate_proof accepts both lengths and rejects an 80B body. tools/verify_link_handshake.py — locks in §6.1, §6.2, §6.3, §6.6. Most importantly verifies the previously-corrected §6.2 LRPROOF body order (signature(64) || responder_X25519_pub(32) || [signalling]) and §6.3 link_id offsets (N=2 for HEADER_1) by actually building a Link initiator-side, capturing the LINKREQUEST raw bytes, computing link_id by the spec recipe, running validate_request inline (since the upstream wrapper swallows exceptions), and confirming the responder's LRPROOF bytes match the spec layout. This was the single most interop-critical correction we made. tools/verify_rnode_split.py — locks in §8.3. Pure-function re-implementation of the canonical TX and RX state machines from RNode_Firmware.ino:359-446 + 716-742; tests header-byte layout, single-frame TX, split-frame TX (300B → 254+46 with shared header byte), all four RX state-machine cases (a/b/c/d from the spec table), and end-to-end TX/RX round-trip at sizes 50, 254, 255, 300, 508. tools/verify_msgpack_quirk.py — locks in §9.3. Confirms umsgpack distinguishes str (fixstr/0xa5) from bytes (bin8/0xc4); confirms LXMF.display_name_from_app_data parses bytes-encoded display names correctly and silently returns None (not crash) on str-encoded ones, matching the bug-tolerance documented in §9.3. All 11 verifiers pass against RNS 1.2.0 / LXMF 0.9.6. Plus: - SPEC.md frontmatter: 'Last verified against' line per agent.md §7. - flows/receive-propagated-lxmf.md: closing half of the propagated LXMF lifecycle. /get listing query, fetch query, ack-and-purge via the have_ids slot, message-bundle unpack and dispatch through lxmf_delivery. - tools/README.md status table refreshed; flows/README.md flips receive-propagated-lxmf.md to ✅. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 12:54:34 -04:00
"""
Verifier for SPEC.md S6.5 (regular PROOF packet wire form).
Re-anchor against RNS 1.2.4 / LXMF 0.9.7 + track upstream distribution shift Upstream RNS 1.2.4 (2026-05-07) announces it is "probably the last release that is also published to GitHub" — pip continues until rnpkg is complete and RNS is self-hosting. All 13 verifiers pass against 1.2.4 / 0.9.7; no wire-format, signing, or protocol behavior changed between 1.2.0 and 1.2.4, so the changes here are purely currency: - Pin tools/requirements.txt to rns==1.2.4 / lxmf==0.9.7 so the verifier stays reproducible if upstream stops mirroring to PyPI before the migration is ready. - Add an "Upstream distribution shift" watch-list to todo.md (local Reticulum node, repo destination hash, rnpkg install/upgrade commands, rsg signature verification, mirroring source citations). - Bump SPEC.md frontmatter and re-anchor ~50 line citations across Identity.py, Transport.py, Resource.py, Link.py, Reticulum.py, Packet.py, and LXMF/* (Identity.py drift was the heaviest at +13 to +31 lines; Transport.py was variable). Fix one numeric (MAX_RANDOM_BLOBS = 32 → 64) and one semantic (§6.6.3 LRPROOF MTU clamp citation pointed at the wrong location — corrected to point at the transit-relay clamp at Transport.py:1539-1556). - Update §10.4 decompression-bomb hazard to note upstream's 1.1.9 cap adoption, with citations to Resource.py:686-691 and Buffer.py:95-97 plus a "do not use one-shot bz2.decompress()" warning. - Re-anchor 11 flows/ files (version pins + ~30 line citations). - Bump version labels in tools/README.md, test-vectors/README.md, and 4 verifier docstrings + 2 hardcoded print strings. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 07:42:25 -04:00
Three scenarios against upstream RNS 1.2.4:
Add four more verifiers + receive-propagated flow + frontmatter version Verifiers: tools/verify_proof_packet.py — locks in §6.5. Toggles Reticulum.__use_implicit_proof to test both modes; confirms Identity.prove emits 64B (implicit) or 96B (explicit) proof body; PacketReceipt.validate_proof accepts both lengths and rejects an 80B body. tools/verify_link_handshake.py — locks in §6.1, §6.2, §6.3, §6.6. Most importantly verifies the previously-corrected §6.2 LRPROOF body order (signature(64) || responder_X25519_pub(32) || [signalling]) and §6.3 link_id offsets (N=2 for HEADER_1) by actually building a Link initiator-side, capturing the LINKREQUEST raw bytes, computing link_id by the spec recipe, running validate_request inline (since the upstream wrapper swallows exceptions), and confirming the responder's LRPROOF bytes match the spec layout. This was the single most interop-critical correction we made. tools/verify_rnode_split.py — locks in §8.3. Pure-function re-implementation of the canonical TX and RX state machines from RNode_Firmware.ino:359-446 + 716-742; tests header-byte layout, single-frame TX, split-frame TX (300B → 254+46 with shared header byte), all four RX state-machine cases (a/b/c/d from the spec table), and end-to-end TX/RX round-trip at sizes 50, 254, 255, 300, 508. tools/verify_msgpack_quirk.py — locks in §9.3. Confirms umsgpack distinguishes str (fixstr/0xa5) from bytes (bin8/0xc4); confirms LXMF.display_name_from_app_data parses bytes-encoded display names correctly and silently returns None (not crash) on str-encoded ones, matching the bug-tolerance documented in §9.3. All 11 verifiers pass against RNS 1.2.0 / LXMF 0.9.6. Plus: - SPEC.md frontmatter: 'Last verified against' line per agent.md §7. - flows/receive-propagated-lxmf.md: closing half of the propagated LXMF lifecycle. /get listing query, fetch query, ack-and-purge via the have_ids slot, message-bundle unpack and dispatch through lxmf_delivery. - tools/README.md status table refreshed; flows/README.md flips receive-propagated-lxmf.md to ✅. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 12:54:34 -04:00
1. Implicit-mode opportunistic DATA proof: when Reticulum is
configured with use_implicit_proof = True (the upstream default
Re-anchor against RNS 1.2.4 / LXMF 0.9.7 + track upstream distribution shift Upstream RNS 1.2.4 (2026-05-07) announces it is "probably the last release that is also published to GitHub" — pip continues until rnpkg is complete and RNS is self-hosting. All 13 verifiers pass against 1.2.4 / 0.9.7; no wire-format, signing, or protocol behavior changed between 1.2.0 and 1.2.4, so the changes here are purely currency: - Pin tools/requirements.txt to rns==1.2.4 / lxmf==0.9.7 so the verifier stays reproducible if upstream stops mirroring to PyPI before the migration is ready. - Add an "Upstream distribution shift" watch-list to todo.md (local Reticulum node, repo destination hash, rnpkg install/upgrade commands, rsg signature verification, mirroring source citations). - Bump SPEC.md frontmatter and re-anchor ~50 line citations across Identity.py, Transport.py, Resource.py, Link.py, Reticulum.py, Packet.py, and LXMF/* (Identity.py drift was the heaviest at +13 to +31 lines; Transport.py was variable). Fix one numeric (MAX_RANDOM_BLOBS = 32 → 64) and one semantic (§6.6.3 LRPROOF MTU clamp citation pointed at the wrong location — corrected to point at the transit-relay clamp at Transport.py:1539-1556). - Update §10.4 decompression-bomb hazard to note upstream's 1.1.9 cap adoption, with citations to Resource.py:686-691 and Buffer.py:95-97 plus a "do not use one-shot bz2.decompress()" warning. - Re-anchor 11 flows/ files (version pins + ~30 line citations). - Bump version labels in tools/README.md, test-vectors/README.md, and 4 verifier docstrings + 2 hardcoded print strings. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 07:42:25 -04:00
per Reticulum.py:256), Identity.prove emits a 64-byte body
Add four more verifiers + receive-propagated flow + frontmatter version Verifiers: tools/verify_proof_packet.py — locks in §6.5. Toggles Reticulum.__use_implicit_proof to test both modes; confirms Identity.prove emits 64B (implicit) or 96B (explicit) proof body; PacketReceipt.validate_proof accepts both lengths and rejects an 80B body. tools/verify_link_handshake.py — locks in §6.1, §6.2, §6.3, §6.6. Most importantly verifies the previously-corrected §6.2 LRPROOF body order (signature(64) || responder_X25519_pub(32) || [signalling]) and §6.3 link_id offsets (N=2 for HEADER_1) by actually building a Link initiator-side, capturing the LINKREQUEST raw bytes, computing link_id by the spec recipe, running validate_request inline (since the upstream wrapper swallows exceptions), and confirming the responder's LRPROOF bytes match the spec layout. This was the single most interop-critical correction we made. tools/verify_rnode_split.py — locks in §8.3. Pure-function re-implementation of the canonical TX and RX state machines from RNode_Firmware.ino:359-446 + 716-742; tests header-byte layout, single-frame TX, split-frame TX (300B → 254+46 with shared header byte), all four RX state-machine cases (a/b/c/d from the spec table), and end-to-end TX/RX round-trip at sizes 50, 254, 255, 300, 508. tools/verify_msgpack_quirk.py — locks in §9.3. Confirms umsgpack distinguishes str (fixstr/0xa5) from bytes (bin8/0xc4); confirms LXMF.display_name_from_app_data parses bytes-encoded display names correctly and silently returns None (not crash) on str-encoded ones, matching the bug-tolerance documented in §9.3. All 11 verifiers pass against RNS 1.2.0 / LXMF 0.9.6. Plus: - SPEC.md frontmatter: 'Last verified against' line per agent.md §7. - flows/receive-propagated-lxmf.md: closing half of the propagated LXMF lifecycle. /get listing query, fetch query, ack-and-purge via the have_ids slot, message-bundle unpack and dispatch through lxmf_delivery. - tools/README.md status table refreshed; flows/README.md flips receive-propagated-lxmf.md to ✅. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 12:54:34 -04:00
containing only the Ed25519 signature over packet.packet_hash.
2. Explicit-mode opportunistic DATA proof: when use_implicit_proof
= False, Identity.prove emits a 96-byte body of
packet_hash(32) || signature(64).
3. Receiver-side length dispatch in PacketReceipt.validate_proof:
accepts both 64- and 96-byte forms; rejects bodies of any other
length.
Reticulum doesn't support reinitialisation in one process; we toggle
use_implicit_proof via the name-mangled class variable
RNS.Reticulum._Reticulum__use_implicit_proof rather than running
two separate Reticulum instances. The toggle is read by
should_use_implicit_proof(), which is what Identity.prove dispatches on.
Exit code 0 on PASS, non-zero on FAIL.
"""
from __future__ import annotations
import os
import sys
import tempfile
import RNS
from RNS.Packet import PacketReceipt
def fail(msg: str) -> None:
print(f"FAIL: {msg}")
sys.exit(1)
def init_minimal_rns():
cfg_dir = tempfile.mkdtemp(prefix="rns-verify-proof-")
cfg_path = os.path.join(cfg_dir, "config")
with open(cfg_path, "w", encoding="utf-8") as f:
f.write("[reticulum]\nenable_transport = No\nshare_instance = No\n")
return RNS.Reticulum(configdir=cfg_dir, loglevel=0)
def set_implicit_proof(value: bool) -> None:
RNS.Reticulum._Reticulum__use_implicit_proof = value
if RNS.Reticulum.should_use_implicit_proof() != value:
fail(f"Failed to toggle use_implicit_proof to {value}")
def build_packet_to_prove(identity, name_aspect):
"""Build a regular DATA packet that we can then prove. name_aspect lets
each call create a unique destination so we don't trip the
'already registered' check."""
dest = RNS.Destination(identity, RNS.Destination.IN, RNS.Destination.SINGLE,
"verify_proof", name_aspect)
pkt = RNS.Packet(dest, b"some payload bytes", create_receipt=False)
pkt.pack()
pkt.update_hash()
pkt.fromPacked = True
pkt.destination = dest
return dest, pkt
def capture_proof_body(identity, target_packet):
"""Run identity.prove() against target_packet and capture the proof
packet's body via Packet.send monkey-patch."""
captured = {}
real_packet_class = RNS.Packet
class CapturePacket(real_packet_class):
def send(self, *a, **kw):
captured["data"] = self.data
captured["context"] = self.context
captured["packet_type"] = self.packet_type
captured["dest_hash"] = self.destination.hash
return None
RNS.Packet = CapturePacket
try:
identity.prove(target_packet)
finally:
RNS.Packet = real_packet_class
return captured
def verify_implicit_form(identity):
set_implicit_proof(True)
dest, pkt = build_packet_to_prove(identity, "implicit_test")
captured = capture_proof_body(identity, pkt)
body = captured["data"]
if len(body) != PacketReceipt.IMPL_LENGTH:
fail(f"S6.5 implicit proof body is {len(body)} bytes, want IMPL_LENGTH = "
f"{PacketReceipt.IMPL_LENGTH} (= 64)")
if not identity.validate(body, pkt.packet_hash):
fail("S6.5 implicit proof body did not validate as signature(packet_hash)")
if captured["dest_hash"] != pkt.packet_hash[:16]:
fail(f"S6.5 implicit proof dest_hash != packet_hash[:16]")
if captured["packet_type"] != RNS.Packet.PROOF:
fail(f"S6.5 implicit proof packet_type = {captured['packet_type']}, want PROOF (3)")
print("PASS S6.5.1 implicit proof form: 64B = Ed25519_sign(packet_hash) only")
def verify_explicit_form(identity):
set_implicit_proof(False)
dest, pkt = build_packet_to_prove(identity, "explicit_test")
captured = capture_proof_body(identity, pkt)
body = captured["data"]
if len(body) != PacketReceipt.EXPL_LENGTH:
fail(f"S6.5 explicit proof body is {len(body)} bytes, want EXPL_LENGTH = "
f"{PacketReceipt.EXPL_LENGTH} (= 96)")
if body[:32] != pkt.packet_hash:
fail(f"S6.5 explicit proof body[:32] != packet_hash")
if not identity.validate(body[32:], pkt.packet_hash):
fail("S6.5 explicit proof body[32:] did not validate as signature(packet_hash)")
print("PASS S6.5.1 explicit proof form: 96B = packet_hash(32) || Ed25519_sign(packet_hash)")
def verify_validator_length_dispatch(identity):
"""S6.5.5: validate_proof must accept both 64- and 96-byte bodies and
reject anything else."""
dest, pkt = build_packet_to_prove(identity, "validator_test")
pkt.create_receipt = True
pkt.receipt = PacketReceipt(pkt)
receipt = pkt.receipt
sig = identity.sign(pkt.packet_hash)
implicit_proof = sig # 64 bytes
explicit_proof = pkt.packet_hash + sig # 96 bytes
# Implicit
if not receipt.validate_proof(implicit_proof):
fail("S6.5.5 receiver rejected a valid implicit-form proof")
# Reset and try explicit
receipt.proved = False
receipt.status = PacketReceipt.SENT
if not receipt.validate_proof(explicit_proof):
fail("S6.5.5 receiver rejected a valid explicit-form proof")
# Bogus length
receipt.proved = False
receipt.status = PacketReceipt.SENT
if receipt.validate_proof(b"\x00" * 80):
fail("S6.5.5 receiver accepted an 80-byte body (must reject any non-{64,96})")
print("PASS S6.5.5 receiver length-dispatch: accepts 64B and 96B, rejects 80B")
def main():
print(f"verify_proof_packet.py against RNS {RNS.__version__}")
init_minimal_rns()
try:
identity = RNS.Identity()
verify_implicit_form(identity)
verify_explicit_form(identity)
verify_validator_length_dispatch(identity)
finally:
try: RNS.Reticulum.exit_handler()
except Exception: pass
print("ALL PASS")
if __name__ == "__main__":
main()