reticiulum-specification/tools/verify_destination_hash.py
Rob cf169b2a9e Verify §2.3, §4.3, §7.1, §7.4 against upstream RNS 1.2.0 / LXMF 0.9.6
Adds tools/ verifier scripts that exercise upstream RNS / LXMF and confirm
(or correct) the SPEC.md callouts:

- §2.3 HEADER_1→HEADER_2 conversion: verified by stubbing Transport.transmit
  and seeding a multi-hop path_table entry.
- §4.3 app_data 3-element variant: producer in LXMF 0.9.6 actually emits
  2 elements only (supported_functionality at LXMRouter.py:999 is dead
  code); parser tolerates 1/2/3-element + raw UTF-8.
- §7.1 path? always-precedes claim: actually conditional on
  not has_path() AND method==OPPORTUNISTIC.
- §7.4 ratchet ring default 8: actually Destination.RATCHET_COUNT = 512
  at RNS/Destination.py:85.

Also fixes a documentation bug in §1.2: the rnstransport.path.request row
of the well-known-hash table had the dest-hash prefix where the name_hash
should be (correct name_hash is 7926bbe7dd7f9aba88b0).

Seeds test-vectors/identities.json (Alice + Bob) with a regenerator
(tools/regen_identities.py) and verifier (tools/verify_destination_hash.py)
covering §1.1 and §1.2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 10:14:51 -04:00

100 lines
3.6 KiB
Python

"""
Verifier for SPEC.md S1.1 (identity composition) and S1.2 (destination hash).
Reads test-vectors/identities.json and, for each vector:
- Loads the private-key bytes via RNS.Identity.from_bytes (the upstream API
for the on-bytes form X25519_priv || Ed25519_priv).
- Confirms identity.get_public_key() matches expected.public_key_hex
(X25519_pub || Ed25519_pub).
- Confirms identity.hash matches expected.identity_hash_hex
(= SHA256(public_key)[:16]).
- Confirms SHA256(SHA256(name)[:10] || identity_hash)[:16] equals
expected.destination_hash_hex.
- Cross-checks RNS.Destination.hash(identity_hash, *name.split(".")) ==
expected.destination_hash_hex (i.e. upstream agrees with the by-hand recipe).
Exit code 0 on PASS, non-zero on FAIL.
"""
from __future__ import annotations
import hashlib
import json
import os
import sys
import RNS
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
VEC_PATH = os.path.join(REPO_ROOT, "test-vectors", "identities.json")
def fail(msg: str) -> None:
print(f"FAIL: {msg}")
sys.exit(1)
def verify_vector(v: dict) -> None:
label = v["label"]
inputs = v["inputs"]
expect = v["expected"]
full = v["destination_full_name"]
prv = bytes.fromhex(inputs["private_key_hex"])
if prv.hex() != inputs["x25519_priv_hex"] + inputs["ed25519_priv_hex"]:
fail(f"{label}: private_key_hex != x25519_priv_hex||ed25519_priv_hex")
identity = RNS.Identity.from_bytes(prv)
if identity is None:
fail(f"{label}: RNS.Identity.from_bytes returned None")
if identity.get_public_key().hex() != expect["public_key_hex"]:
fail(f"{label}: public_key mismatch\n"
f" got: {identity.get_public_key().hex()}\n"
f" want: {expect['public_key_hex']}")
expected_idhash = hashlib.sha256(identity.get_public_key()).digest()[:16]
if expected_idhash.hex() != expect["identity_hash_hex"]:
fail(f"{label}: SHA256(public_key)[:16] != identity_hash")
if identity.hash.hex() != expect["identity_hash_hex"]:
fail(f"{label}: identity.hash != expected (RNS disagrees with manual)")
name_hash_calc = hashlib.sha256(full.encode("utf-8")).digest()[:10]
if name_hash_calc.hex() != expect["name_hash_hex"]:
fail(f"{label}: name_hash for {full!r}: got {name_hash_calc.hex()} "
f"want {expect['name_hash_hex']}")
dest_calc = hashlib.sha256(name_hash_calc + identity.hash).digest()[:16]
if dest_calc.hex() != expect["destination_hash_hex"]:
fail(f"{label}: dest_hash recipe mismatch\n"
f" got: {dest_calc.hex()}\n"
f" want: {expect['destination_hash_hex']}")
rns_dest_hash = RNS.Destination.hash(identity.hash, *full.split("."))
if rns_dest_hash.hex() != expect["destination_hash_hex"]:
fail(f"{label}: RNS.Destination.hash != expected\n"
f" RNS.Destination.hash: {rns_dest_hash.hex()}\n"
f" want: {expect['destination_hash_hex']}")
print(f"PASS {label}: identity, identity_hash, dest_hash for {full!r}")
def main():
print(f"verify_destination_hash.py against RNS {RNS.__version__}")
if not os.path.isfile(VEC_PATH):
fail(f"Missing test-vectors file: {VEC_PATH}")
with open(VEC_PATH, "r", encoding="utf-8") as f:
payload = json.load(f)
if "vectors" not in payload:
fail(f"Bad test-vectors file: missing 'vectors' key")
for vec in payload["vectors"]:
verify_vector(vec)
print(f"ALL PASS ({len(payload['vectors'])} identity vectors)")
if __name__ == "__main__":
main()