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>
182 lines
6.1 KiB
Python
182 lines
6.1 KiB
Python
"""
|
|
Verifier for SPEC.md S6.5 (regular PROOF packet wire form).
|
|
|
|
Three scenarios against upstream RNS 1.2.0:
|
|
|
|
1. Implicit-mode opportunistic DATA proof: when Reticulum is
|
|
configured with use_implicit_proof = True (the upstream default
|
|
per Reticulum.py:259), Identity.prove emits a 64-byte body
|
|
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()
|