reticiulum-specification/tools/regen_link_lxmf.py
John Poole 7ffbb0ef5e Completed the full link-delivered LXMF unit:
Tier 1 audit: `link-lxmf-tier1-rns-1.2.4-lxmf-0.9.7.md`
Tier 2 vectors/verifier: link-lxmf.json, regen_link_lxmf.py, and verify_link_lxmf.py
Tier 3 promotion: updated SPEC.md, flows, status, and documentation
Key correction: the 319/320 boundary uses upstream’s computed LXMF content_size, not simply raw message content length.
Also corrected stale flow descriptions for KEEPALIVE (0xFA) and encrypted LINKCLOSE teardown (0xFC).
Verification:
Deterministic vector regeneration: identical SHA-256
Portable-path and formatting checks: pass
Full pinned suite: 17 passed, 0 failed
2026-06-08 13:54:27 -07:00

268 lines
9.7 KiB
Python

"""
Regenerator for test-vectors/link-lxmf.json.
Builds deterministic DIRECT LXMF vectors at the PACKET/RESOURCE boundary:
- computed LXMF content_size 319 -> one encrypted Link DATA packet
- computed LXMF content_size 320 -> one Resource over the same Link
The vectors reuse the deterministic Link session key from links.json and the
Alice/Bob identities from identities.json.
"""
from __future__ import annotations
import json
import os
import sys
import tempfile
import LXMF
import RNS
from LXMF.LXMessage import LXMessage
from RNS.Cryptography.Token import Token
from RNS.Resource import Resource, ResourceAdvertisement
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
OUT_PATH = os.path.join(REPO_ROOT, "test-vectors", "link-lxmf.json")
IDS_PATH = os.path.join(REPO_ROOT, "test-vectors", "identities.json")
LINKS_PATH = os.path.join(REPO_ROOT, "test-vectors", "links.json")
FIXED_TIMESTAMP = 1700000000.0
PACKET_IV = bytes.fromhex("5152535455565758595a5b5c5d5e5f60")
RESOURCE_IV = bytes.fromhex("6162636465666768696a6b6c6d6e6f70")
RESOURCE_PREFIX = bytes.fromhex("71727374")
RESOURCE_RANDOM_HASH = bytes.fromhex("81828384")
def fail(message: str) -> None:
print(f"FAIL: {message}")
sys.exit(1)
def init_minimal_rns():
config_dir = tempfile.mkdtemp(prefix="rns-regen-link-lxmf-")
config_path = os.path.join(config_dir, "config")
with open(config_path, "w", encoding="utf-8") as config:
config.write("[reticulum]\nenable_transport = No\nshare_instance = No\n")
return RNS.Reticulum(configdir=config_dir, loglevel=0)
def load_inputs():
with open(IDS_PATH, "r", encoding="utf-8") as identities_file:
identities = json.load(identities_file)["vectors"]
with open(LINKS_PATH, "r", encoding="utf-8") as links_file:
link_vector = json.load(links_file)["vectors"][0]
alice = next(vector for vector in identities if vector["label"] == "alice")
bob = next(vector for vector in identities if vector["label"] == "bob")
alice_identity = RNS.Identity.from_bytes(bytes.fromhex(alice["inputs"]["private_key_hex"]))
bob_identity = RNS.Identity.from_bytes(bytes.fromhex(bob["inputs"]["private_key_hex"]))
derived_key = bytes.fromhex(link_vector["expected"]["derived_key_hex"])
link_id = bytes.fromhex(link_vector["expected"]["link_id_hex"])
return alice_identity, bob_identity, derived_key, link_id
class FakeLink:
"""Active Link surface needed by LXMessage, Packet, and Resource."""
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 make_message(destination, source, content_size: int) -> LXMessage:
message = LXMessage(
destination=destination,
source=source,
title=b"",
content=b"x" * content_size,
fields={},
desired_method=LXMessage.DIRECT,
)
message.timestamp = FIXED_TIMESTAMP
message.pack()
computed_content_size = (
len(message.packed[2 * LXMessage.DESTINATION_LENGTH + LXMessage.SIGNATURE_LENGTH :])
- LXMessage.TIMESTAMP_SIZE
- LXMessage.STRUCT_OVERHEAD
)
if computed_content_size != content_size:
fail(f"fixture computed content_size {computed_content_size}, want {content_size}")
return message
def patch_token_iv(iv: bytes):
token_mod = sys.modules["RNS.Cryptography.Token"]
real_urandom = token_mod.os.urandom
def fixed_urandom(length: int) -> bytes:
if length == 16:
return iv
return real_urandom(length)
token_mod.os.urandom = fixed_urandom
return token_mod, real_urandom
def build_packet_vector(message: LXMessage, link: FakeLink) -> dict:
message.set_delivery_destination(link)
token_mod, real_urandom = patch_token_iv(PACKET_IV)
try:
packet = message._LXMessage__as_packet()
packet.pack()
finally:
token_mod.os.urandom = real_urandom
return {
"label": "alice_to_bob_direct_packet_boundary",
"inputs": {
"src_identity_label": "alice",
"dst_identity_label": "bob",
"content_size": 319,
"lxmf_timestamp": FIXED_TIMESTAMP,
"link_vector_label": "alice_to_bob_aes256cbc",
"token_iv_hex": PACKET_IV.hex(),
},
"expected": {
"method": message.method,
"representation": message.representation,
"lxmf_packed_hex": message.packed.hex(),
"link_packet_ciphertext_hex": packet.ciphertext.hex(),
"link_packet_raw_hex": packet.raw.hex(),
"link_id_hex": link.link_id.hex(),
},
"rns_version_at_generation": RNS.__version__,
"lxmf_version_at_generation": LXMF.__version__,
"generator_script": "tools/regen_link_lxmf.py",
"verifies_spec_sections": ["5.2", "5.5", "5.6", "6.4.3", "10.1"],
}
def build_resource_vector(message: LXMessage, link: FakeLink) -> dict:
message.set_delivery_destination(link)
message.auto_compress = False
token_mod, real_urandom = patch_token_iv(RESOURCE_IV)
real_get_random_hash = RNS.Identity.get_random_hash
real_advertise = Resource.advertise
hashes = [
RESOURCE_PREFIX + bytes(28),
RESOURCE_RANDOM_HASH + bytes(28),
]
def fixed_random_hash() -> bytes:
if not hashes:
raise RuntimeError("unexpected extra Resource random-hash request")
return hashes.pop(0)
RNS.Identity.get_random_hash = staticmethod(fixed_random_hash)
Resource.advertise = lambda self: None
try:
resource = message._LXMessage__as_resource()
finally:
token_mod.os.urandom = real_urandom
RNS.Identity.get_random_hash = staticmethod(real_get_random_hash)
Resource.advertise = real_advertise
return {
"label": "alice_to_bob_direct_resource_boundary",
"inputs": {
"src_identity_label": "alice",
"dst_identity_label": "bob",
"content_size": 320,
"lxmf_timestamp": FIXED_TIMESTAMP,
"link_vector_label": "alice_to_bob_aes256cbc",
"token_iv_hex": RESOURCE_IV.hex(),
"throwaway_prefix_hex": RESOURCE_PREFIX.hex(),
"resource_random_hash_hex": RESOURCE_RANDOM_HASH.hex(),
"auto_compress": False,
},
"expected": {
"method": message.method,
"representation": message.representation,
"lxmf_packed_hex": message.packed.hex(),
"resource_hash_hex": resource.hash.hex(),
"resource_expected_proof_hex": resource.expected_proof.hex(),
"resource_advertisement_plaintext_hex": ResourceAdvertisement(resource).pack().hex(),
"resource_parts": [
{
"body_hex": part.data.hex(),
"map_hash_hex": part.map_hash.hex(),
"raw_hex": part.raw.hex(),
}
for part in resource.parts
],
"link_id_hex": link.link_id.hex(),
},
"rns_version_at_generation": RNS.__version__,
"lxmf_version_at_generation": LXMF.__version__,
"generator_script": "tools/regen_link_lxmf.py",
"verifies_spec_sections": ["5.2", "5.5", "5.6", "10.1", "10.2", "10.4"],
}
def main() -> None:
print(f"regen_link_lxmf.py against RNS {RNS.__version__} / LXMF {LXMF.__version__}")
init_minimal_rns()
try:
alice_identity, bob_identity, derived_key, link_id = load_inputs()
alice = RNS.Destination(
alice_identity, RNS.Destination.IN, RNS.Destination.SINGLE, "lxmf", "delivery"
)
bob = RNS.Destination(
bob_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, "lxmf", "delivery"
)
RNS.Identity.remember(bytes(32), alice.hash, alice_identity.get_public_key(), None)
RNS.Identity.remember(bytes(32), bob.hash, bob_identity.get_public_key(), None)
link = FakeLink(derived_key, link_id)
packet_message = make_message(bob, alice, 319)
resource_message = make_message(bob, alice, 320)
if packet_message.representation != LXMessage.PACKET:
fail("content_size 319 did not select PACKET")
if resource_message.representation != LXMessage.RESOURCE:
fail("content_size 320 did not select RESOURCE")
payload = {
"_about": (
"DIRECT LXMF vectors at the exact upstream PACKET/RESOURCE boundary. "
"Both carry the complete canonical LXMF body over the deterministic "
"Link session from links.json. `content_size` is upstream's computed "
"LXMF size, not necessarily the raw content field length."
),
"vectors": [
build_packet_vector(packet_message, link),
build_resource_vector(resource_message, link),
],
}
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} with 2 vectors")
print("ALL PASS")
finally:
try:
RNS.Reticulum.exit_handler()
except Exception:
pass
if __name__ == "__main__":
main()