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>
126 lines
4.7 KiB
Python
126 lines
4.7 KiB
Python
"""
|
|
Regenerator for test-vectors/identities.json.
|
|
|
|
Takes a list of fixed (X25519_priv || Ed25519_priv) inputs and emits the
|
|
expected `public_key`, `identity_hash`, and `destination_hash` (for
|
|
`lxmf.delivery`) that upstream RNS derives. Verifies the round-trip:
|
|
load -> derive -> reload-from-output and confirms outputs match.
|
|
|
|
Run from the repo root:
|
|
|
|
python tools/regen_identities.py
|
|
|
|
Updates `test-vectors/identities.json` in place. Exit 0 on success.
|
|
"""
|
|
|
|
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__)))
|
|
OUT_PATH = os.path.join(REPO_ROOT, "test-vectors", "identities.json")
|
|
|
|
|
|
# Fixed test-vector inputs. The X25519 and Ed25519 private bytes are 32
|
|
# arbitrary but stable bytes; we use SHA-256 of a label so the values are
|
|
# reproducible in any environment without a private blob in the repo.
|
|
def vector_inputs():
|
|
def derive(label: str, suffix: str) -> bytes:
|
|
return hashlib.sha256(f"{label}.{suffix}".encode("utf-8")).digest()
|
|
|
|
return [
|
|
{
|
|
"label": "alice",
|
|
"x25519_priv_hex": derive("alice", "x25519").hex(),
|
|
"ed25519_priv_hex": derive("alice", "ed25519").hex(),
|
|
"destination_full_name": "lxmf.delivery",
|
|
},
|
|
{
|
|
"label": "bob",
|
|
"x25519_priv_hex": derive("bob", "x25519").hex(),
|
|
"ed25519_priv_hex": derive("bob", "ed25519").hex(),
|
|
"destination_full_name": "lxmf.delivery",
|
|
},
|
|
]
|
|
|
|
|
|
def derive_with_upstream(spec: dict) -> dict:
|
|
prv_bytes = bytes.fromhex(spec["x25519_priv_hex"]) + bytes.fromhex(spec["ed25519_priv_hex"])
|
|
if len(prv_bytes) != 64:
|
|
raise ValueError(f"prv_bytes for {spec['label']} must be 64 B, got {len(prv_bytes)}")
|
|
|
|
identity = RNS.Identity.from_bytes(prv_bytes)
|
|
if identity is None:
|
|
raise RuntimeError(f"Identity.from_bytes returned None for {spec['label']}")
|
|
|
|
public_key = identity.get_public_key() # 64 B: X25519_pub || Ed25519_pub
|
|
identity_hash = identity.hash # 16 B
|
|
full_name = spec["destination_full_name"]
|
|
name_hash = hashlib.sha256(full_name.encode("utf-8")).digest()[:10]
|
|
destination_hash = hashlib.sha256(name_hash + identity_hash).digest()[:16]
|
|
|
|
# Cross-check: RNS.Destination.hash with bytes-style identity argument
|
|
# (no Identity instance — just the raw identity_hash) must agree.
|
|
rns_dest_hash = RNS.Destination.hash(identity_hash, *full_name.split("."))
|
|
if rns_dest_hash != destination_hash:
|
|
raise RuntimeError(
|
|
f"destination_hash mismatch for {spec['label']!r}: "
|
|
f"hand-computed {destination_hash.hex()} vs RNS.Destination.hash "
|
|
f"{rns_dest_hash.hex()}"
|
|
)
|
|
|
|
# Round-trip: rebuild identity from its private-key bytes again.
|
|
rebuilt = RNS.Identity.from_bytes(identity.get_private_key())
|
|
if rebuilt.hash != identity_hash:
|
|
raise RuntimeError(
|
|
f"private-key round-trip mismatch for {spec['label']!r}"
|
|
)
|
|
|
|
return {
|
|
"label": spec["label"],
|
|
"destination_full_name": full_name,
|
|
"inputs": {
|
|
"x25519_priv_hex": spec["x25519_priv_hex"],
|
|
"ed25519_priv_hex": spec["ed25519_priv_hex"],
|
|
"private_key_hex": identity.get_private_key().hex(), # X25519 || Ed25519
|
|
},
|
|
"expected": {
|
|
"public_key_hex": public_key.hex(), # X25519_pub || Ed25519_pub
|
|
"identity_hash_hex": identity_hash.hex(),
|
|
"name_hash_hex": name_hash.hex(),
|
|
"destination_hash_hex": destination_hash.hex(),
|
|
},
|
|
"rns_version_at_generation": RNS.__version__,
|
|
"generator_script": "tools/regen_identities.py",
|
|
"verifies_spec_sections": ["1.1", "1.2"],
|
|
}
|
|
|
|
|
|
def main():
|
|
print(f"regen_identities.py against RNS {RNS.__version__}")
|
|
vectors = [derive_with_upstream(v) for v in vector_inputs()]
|
|
payload = {
|
|
"_about": (
|
|
"Identity test vectors. Load via RNS.Identity.from_bytes("
|
|
"bytes.fromhex(inputs.private_key_hex)) and confirm the four "
|
|
"expected.* fields match. Regenerate by running this script "
|
|
"against the upstream RNS shown in rns_version_at_generation."
|
|
),
|
|
"vectors": vectors,
|
|
}
|
|
os.makedirs(os.path.dirname(OUT_PATH), exist_ok=True)
|
|
with open(OUT_PATH, "w", encoding="utf-8", newline="\n") as f:
|
|
json.dump(payload, f, indent=2, sort_keys=False)
|
|
f.write("\n")
|
|
print(f"Wrote {OUT_PATH} with {len(vectors)} vectors")
|
|
print("ALL PASS")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|