|
| 1 | +"""SAN-496: RFC 8032 Section 7.1 Ed25519 algorithm-conformance tests. |
| 2 | +
|
| 3 | +Verifies the SDK's Ed25519 implementation (via the `cryptography` library |
| 4 | +imported at src/sanna/crypto.py:27) produces byte-correct outputs against |
| 5 | +the IETF RFC 8032 reference vectors. This is the ALGORITHM-conformance |
| 6 | +test surface; the PROTOCOL-conformance surface lives in the existing |
| 7 | +constitution_signature.json + receipt_signature.json vectors using the |
| 8 | +SAN-489-relabeled fixed seed. |
| 9 | +
|
| 10 | +Source: https://datatracker.ietf.org/doc/html/rfc8032#section-7.1 |
| 11 | +
|
| 12 | +For each RFC 8032 vector: |
| 13 | + 1. Derive raw public key from secret seed; assert byte-exact match with RFC pubkey |
| 14 | + 2. Sign the message; assert byte-exact match with RFC signature |
| 15 | + 3. Verify the signature against the public key (round-trip) |
| 16 | +
|
| 17 | +A sanity test asserts the JSON file contains exactly the 5 RFC 8032 |
| 18 | +Section 7.1 vectors -- no more, no less. |
| 19 | +
|
| 20 | +Runs on Python 3.10, 3.11, 3.12 (CI matrix). |
| 21 | +""" |
| 22 | + |
| 23 | +import json |
| 24 | +from pathlib import Path |
| 25 | + |
| 26 | +import pytest |
| 27 | +from cryptography.hazmat.primitives.asymmetric.ed25519 import ( |
| 28 | + Ed25519PrivateKey, |
| 29 | + Ed25519PublicKey, |
| 30 | +) |
| 31 | +from cryptography.hazmat.primitives import serialization |
| 32 | + |
| 33 | +VECTORS_FILE = Path(__file__).parent / "vectors" / "ed25519_rfc8032.json" |
| 34 | + |
| 35 | + |
| 36 | +def _load_vectors(): |
| 37 | + with VECTORS_FILE.open(encoding="utf-8") as f: |
| 38 | + data = json.load(f) |
| 39 | + return data["vectors"] |
| 40 | + |
| 41 | + |
| 42 | +@pytest.mark.parametrize( |
| 43 | + "vector", _load_vectors(), ids=lambda v: v["name"] |
| 44 | +) |
| 45 | +def test_ed25519_rfc8032_public_key_derivation(vector): |
| 46 | + """Derived public key matches the RFC 8032 expected pubkey byte-for-byte.""" |
| 47 | + seed = bytes.fromhex(vector["secret_key_hex"]) |
| 48 | + expected_pubkey = bytes.fromhex(vector["public_key_hex"]) |
| 49 | + |
| 50 | + sk = Ed25519PrivateKey.from_private_bytes(seed) |
| 51 | + derived_pubkey = sk.public_key().public_bytes( |
| 52 | + encoding=serialization.Encoding.Raw, |
| 53 | + format=serialization.PublicFormat.Raw, |
| 54 | + ) |
| 55 | + |
| 56 | + assert derived_pubkey == expected_pubkey, ( |
| 57 | + f"Public key derivation mismatch for {vector['name']}: " |
| 58 | + f"expected {expected_pubkey.hex()}, got {derived_pubkey.hex()}. " |
| 59 | + f"See RFC 8032 Section 7.1." |
| 60 | + ) |
| 61 | + |
| 62 | + |
| 63 | +@pytest.mark.parametrize( |
| 64 | + "vector", _load_vectors(), ids=lambda v: v["name"] |
| 65 | +) |
| 66 | +def test_ed25519_rfc8032_signature_matches(vector): |
| 67 | + """Signing the RFC message with the RFC seed produces the RFC signature byte-for-byte.""" |
| 68 | + seed = bytes.fromhex(vector["secret_key_hex"]) |
| 69 | + message = bytes.fromhex(vector["message_hex"]) |
| 70 | + expected_signature = bytes.fromhex(vector["signature_hex"]) |
| 71 | + |
| 72 | + sk = Ed25519PrivateKey.from_private_bytes(seed) |
| 73 | + signature = sk.sign(message) |
| 74 | + |
| 75 | + assert signature == expected_signature, ( |
| 76 | + f"Signature mismatch for {vector['name']}: " |
| 77 | + f"expected {expected_signature.hex()}, got {signature.hex()}. " |
| 78 | + f"See RFC 8032 Section 7.1." |
| 79 | + ) |
| 80 | + |
| 81 | + |
| 82 | +@pytest.mark.parametrize( |
| 83 | + "vector", _load_vectors(), ids=lambda v: v["name"] |
| 84 | +) |
| 85 | +def test_ed25519_rfc8032_signature_verifies(vector): |
| 86 | + """The RFC signature verifies against the RFC public key + message (round-trip).""" |
| 87 | + pubkey_raw = bytes.fromhex(vector["public_key_hex"]) |
| 88 | + message = bytes.fromhex(vector["message_hex"]) |
| 89 | + signature = bytes.fromhex(vector["signature_hex"]) |
| 90 | + |
| 91 | + pk = Ed25519PublicKey.from_public_bytes(pubkey_raw) |
| 92 | + pk.verify(signature, message) # raises InvalidSignature on mismatch |
| 93 | + |
| 94 | + |
| 95 | +def test_ed25519_rfc8032_vectors_file_has_all_five(): |
| 96 | + """Sanity-check: tests/vectors/ed25519_rfc8032.json contains exactly the 5 RFC 8032 Section 7.1 vectors.""" |
| 97 | + vectors = _load_vectors() |
| 98 | + names = {v["name"] for v in vectors} |
| 99 | + expected = {"TEST 1", "TEST 2", "TEST 3", "TEST 1024", "TEST SHA(abc)"} |
| 100 | + assert names == expected, ( |
| 101 | + f"Expected exactly the 5 RFC 8032 Section 7.1 vectors {sorted(expected)}; " |
| 102 | + f"got {sorted(names)}." |
| 103 | + ) |
0 commit comments