#!/usr/bin/env -S uv run --quiet --script # /// script # requires-python = ">=3.11" # dependencies = ["noiseprotocol>=0.3.1", "pynacl>=1.5"] # /// """Generate test/vectors.json for the JS implementation. The JS crypto in web/js must agree byte for byte with the reference stack (hashlib/hmac, PyNaCl, noiseprotocol), so the ground truth is generated here with fixed inputs and consumed by test/vectors.test.mjs. Everything is derived from SHA-256 of fixed labels, so the file is reproducible. """ from __future__ import annotations import hashlib import hmac import json import struct from pathlib import Path import nacl.bindings as sodium from noise.connection import Keypair, NoiseConnection LABEL_SEAL, LABEL_MSG, LABEL_ID = b"smolmail/1 seal", b"smolmail/1 msg", b"smolmail/1 id" LABEL_IDENTITY, LABEL_ACCEPT, LABEL_MAC = b"smolmail/1 identity", b"smolmail/1 accept", b"smolmail/1 mac" TIME = 1730000000 # fixed so the envelope vector is reproducible def seed(label: str) -> bytes: return hashlib.sha256(label.encode()).digest() def hkdf_sha256(ikm: bytes, salt: bytes, info: bytes, length: int) -> bytes: prk = hmac.new(salt, ikm, hashlib.sha256).digest() out, block, counter = b"", b"", 1 while len(out) < length: block = hmac.new(prk, block + info + bytes([counter]), hashlib.sha256).digest() out, counter = out + block, counter + 1 return out[:length] def main() -> int: out: dict = {} # --- hashes over empty, one-block and multi-block inputs out["sha256"] = [ {"in": m.hex(), "out": hashlib.sha256(m).hexdigest()} for m in (b"", b"abc", bytes(range(64)), b"smolmail" * 40) ] out["sha512"] = [ {"in": m.hex(), "out": hashlib.sha512(m).hexdigest()} for m in (b"", b"abc", bytes(range(64))) ] # --- HMAC/HKDF, including RFC 5869 test case 1 out["hkdf"] = [] for ikm, salt, info, length, name in ( (bytes.fromhex("0b" * 22), bytes.fromhex("000102030405060708090a0b0c"), bytes.fromhex("f0f1f2f3f4f5f6f7f8f9"), 42, "rfc5869-1"), (b"agreement shared", b"epk" + b"recipient", b"smolmail/1 seal", 32, "seal-shaped"), ): out["hkdf"].append({"name": name, "ikm": ikm.hex(), "salt": salt.hex(), "info": info.hex(), "len": length, "out": hkdf_sha256(ikm, salt, info, length).hex()}) # --- ChaCha20-Poly1305 (RFC 8439 IETF AEAD) over varied shapes key, nonce = seed("aead key"), seed("aead nonce")[:12] aad = bytes.fromhex("50515253c0c1c2c3c4c5c6c7") out["aead"] = [] for plaintext, use_aad, name in ( (b"", False, "empty"), (b"hello", True, "short"), (b"Ladies and Gentlemen of the class of '99: if I could offer you " b"only one tip for the future, sunscreen would be it.", True, "multiline"), ): sealed = sodium.crypto_aead_chacha20poly1305_ietf_encrypt( plaintext, aad if use_aad else b"", nonce, key) out["aead"].append({"name": name, "key": key.hex(), "nonce": nonce.hex(), "aad": (aad if use_aad else b"").hex(), "plaintext": plaintext.hex(), "sealed": sealed.hex()}) # --- X25519: base multiplication and agreement, including the §2 checks out["x25519"] = [] for name, priv in (("alice", seed("x25519 alice")), ("bob", seed("x25519 bob"))): pub = sodium.crypto_scalarmult_base(priv) out["x25519"].append({"name": name, "priv": priv.hex(), "pub": pub.hex()}) out["x25519"].append({"name": "agree", "shared": sodium.crypto_scalarmult( seed("x25519 alice"), sodium.crypto_scalarmult_base(seed("x25519 bob"))).hex()}) for bad in (b"\x00" * 32, bytes.fromhex("0100" + "00" * 30)): try: sodium.crypto_scalarmult(seed("x25519 alice"), bad) raised = False except Exception: raised = True out["x25519"].append({"name": f"low-order-{bad[0]}", "peer": bad.hex(), "peer_rejected": True, "raised": raised}) assert raised, "PyNaCl must reject this low-order point for the vector to mean anything" # --- Ed25519: keypair, signature, verification, and a tampered case out["ed25519"] = [] for name, s in (("vector-seed-a", seed("ed25519 a")), ("vector-seed-b", seed("ed25519 b"))): pub, _ = sodium.crypto_sign_seed_keypair(s) for message in (b"", b"smolmail/1 auth" + bytes(32), bytes(range(64))): sig = sodium.crypto_sign(message, s + pub)[:64] out["ed25519"].append({"name": name, "seed": s.hex(), "pub": pub.hex(), "message": message.hex(), "signature": sig.hex(), "valid": True}) bad = bytes([sig[0] ^ 1]) + sig[1:] out["ed25519"].append({"name": name, "seed": s.hex(), "pub": pub.hex(), "message": message.hex(), "signature": bad.hex(), "valid": False}) # --- §2 conversions between the identity key and X25519 out["ed_to_x25519"] = [] for name, s in (("vector-seed-a", seed("ed25519 a")), ("vector-seed-b", seed("ed25519 b"))): pub, sk64 = sodium.crypto_sign_seed_keypair(s) out["ed_to_x25519"].append({ "name": name, "seed": s.hex(), "x_priv": sodium.crypto_sign_ed25519_sk_to_curve25519(sk64).hex(), "x_pub": sodium.crypto_sign_ed25519_pk_to_curve25519(pub).hex()}) # --- §5 envelope sealed with a fixed ephemeral, mirroring smolmail.py seal() sender_seed, recipient_seed, esk = seed("envelope sender"), seed("envelope recipient"), seed("envelope esk") sender_pub, _ = sodium.crypto_sign_seed_keypair(sender_seed) recipient_pub, _ = sodium.crypto_sign_seed_keypair(recipient_seed) epk = sodium.crypto_scalarmult_base(esk) shared = sodium.crypto_scalarmult(esk, sodium.crypto_sign_ed25519_pk_to_curve25519(recipient_pub)) key = hkdf_sha256(shared, epk + recipient_pub, LABEL_SEAL, 32) body = b"---\nSubject: vector\n---\nhello bob\n" sig = sodium.crypto_sign(LABEL_MSG + recipient_pub + epk + bytes([1]) + sender_pub + struct.pack(">qI", TIME, len(body)) + body, sender_seed + sender_pub)[:64] plaintext = (bytes([1]) + sender_pub + struct.pack(">qI", TIME, len(body)) + body + sig) aad = b"SMOL" + bytes([1]) + recipient_pub + epk envelope = aad + sodium.crypto_aead_chacha20poly1305_ietf_encrypt(plaintext, aad, bytes(12), key) out["envelope"] = { "sender_seed": sender_seed.hex(), "recipient_seed": recipient_seed.hex(), "esk": esk.hex(), "body": body.hex(), "time": TIME, "envelope": envelope.hex(), "id": hashlib.sha256(LABEL_ID + envelope).digest().hex(), "unpadded_plaintext_len": len(plaintext)} # --- §2/§5.8: master-derived rotation seeds and accept tokens master = seed("master") correspondent = seed("correspondent identity") accept_key = hkdf_sha256(master, b"", LABEL_ACCEPT, 32) token = hmac.new(accept_key, correspondent, hashlib.sha256).digest() mid = seed("token message id") out["tokens"] = { "master": master.hex(), "identity_seed_0": hkdf_sha256(master, b"", LABEL_IDENTITY + struct.pack(">I", 0), 32).hex(), "identity_seed_1": hkdf_sha256(master, b"", LABEL_IDENTITY + struct.pack(">I", 1), 32).hex(), "accept_key": accept_key.hex(), "correspondent": correspondent.hex(), "token": token.hex(), "message_id": mid.hex(), "accept_mac": hmac.new(token, LABEL_MAC + mid, hashlib.sha256).digest().hex(), } # --- Noise NX transcript with every key fixed i_e, r_e, s_priv = seed("nx initiator eph"), seed("nx responder eph"), seed("nx server static") s_pub = sodium.crypto_scalarmult_base(s_priv) n1 = NoiseConnection.from_name(b"Noise_NX_25519_ChaChaPoly_SHA256") n1.set_as_initiator() n1.set_prologue(b"smolmail/1") n1.set_keypair_from_private_bytes(Keypair.EPHEMERAL, i_e) n1.start_handshake() m1 = n1.write_message() n2 = NoiseConnection.from_name(b"Noise_NX_25519_ChaChaPoly_SHA256") n2.set_as_responder() n2.set_prologue(b"smolmail/1") n2.set_keypair_from_private_bytes(Keypair.STATIC, s_priv) n2.set_keypair_from_private_bytes(Keypair.EPHEMERAL, r_e) n2.start_handshake() n2.read_message(m1) m2 = n2.write_message() n1.read_message(m2) assert n1.handshake_finished and n2.handshake_finished out["noise"] = { "initiator_eph_priv": i_e.hex(), "responder_eph_priv": r_e.hex(), "server_static_priv": s_priv.hex(), "server_static_pub": s_pub.hex(), "message1": m1.hex(), "message2": m2.hex(), "handshake_hash": n1.get_handshake_hash().hex(), "assert_hash_equal": n1.get_handshake_hash() == n2.get_handshake_hash(), "initiator_frames": [{"plaintext": p.hex(), "sealed": n1.encrypt(p).hex()} for p in (b"ping", b"second frame to test the counter")], "responder_frames": [{"plaintext": p.hex(), "sealed": n2.encrypt(p).hex()} for p in (b"pong", b"x" * 300)], } path = Path(__file__).parent / "vectors.json" path.write_text(json.dumps(out, indent=2) + "\n") print(f"wrote {path}") return 0 if __name__ == "__main__": raise SystemExit(main())