Skip to content

Commit 4270a42

Browse files
authored
SAN-496: add IETF RFC 8032 Section 7.1 Ed25519 algorithm-conformance vectors (#64)
Adds tests/vectors/ed25519_rfc8032.json with the 5 IETF RFC 8032 Section 7.1 reference vectors (TEST 1, TEST 2, TEST 3, TEST 1024, TEST SHA(abc)), hard-coded from the RFC text. Adds tests/test_ed25519_rfc8032.py with parametrized assertions for public key derivation, signature byte-match, and signature round-trip verify per vector. This covers a DIFFERENT test surface than the existing SAN-489-relabeled protocol-conformance vectors (constitution_signature.json, receipt_signature.json). Protocol conformance: does the SDK canonicalize + sign in Sanna's style? Algorithm conformance (THIS): does the SDK's Ed25519 implementation produce byte-correct outputs against IETF authoritative reference vectors? A buggy Ed25519 implementation could pass protocol conformance while failing RFC 8032; both surfaces are needed for full crypto-correctness assurance. Vectors hard-coded from RFC (not generated by tests/generate_vectors.py) to keep the authoritative-source link unambiguous. 5 vectors x 3 assertions + 1 sanity test = 16 new parametrized test cases. Runs on the full CI Python matrix (3.10, 3.11, 3.12). No SDK code changes; SDK already uses cryptography.hazmat.primitives.asymmetric.ed25519 (src/sanna/crypto.py:27) which is RFC 8032 compliant.
1 parent 39b1152 commit 4270a42

5 files changed

Lines changed: 214 additions & 14 deletions

File tree

CHANGELOG.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,37 @@
1+
## [Unreleased] -- 2026-05-11 (SAN-496)
2+
3+
### Added
4+
5+
- **`tests/vectors/ed25519_rfc8032.json`**: IETF RFC 8032 Section 7.1
6+
Ed25519 algorithm-conformance test vectors (TEST 1, TEST 2, TEST 3,
7+
TEST 1024, TEST SHA(abc)). Hard-coded from the RFC text for unambiguous
8+
authoritative-source linkage.
9+
10+
- **`tests/test_ed25519_rfc8032.py`**: parametrized tests asserting (a)
11+
public key derivation, (b) signature byte-match, (c) signature
12+
round-trip verify for each of the 5 RFC vectors. Catches the failure
13+
mode where the SDK could pass protocol-conformance (correct
14+
canonicalization + signing flow) while having a buggy Ed25519
15+
implementation that produces RFC-incompatible signatures. Orthogonal
16+
to the protocol-conformance vectors (constitution_signature.json,
17+
receipt_signature.json) which were relabeled in SAN-489 as
18+
intentionally-public fixed-seed vectors.
19+
20+
- **`tests/vectors/README.md`**: documented the two test surfaces
21+
(protocol conformance vs algorithm conformance) and updated the
22+
Files table to include the new RFC 8032 file.
23+
24+
### Notes
25+
26+
- 5 vectors x 3 assertions + 1 sanity test = 16 new parametrized test cases.
27+
- Runs on the full CI Python matrix (3.10, 3.11, 3.12).
28+
- No changes to existing protocol-conformance vectors or the
29+
`tests/generate_vectors.py` generator.
30+
- No SDK code changes. SDK already uses `cryptography.hazmat.primitives.asymmetric.ed25519`
31+
(src/sanna/crypto.py:27) which is RFC 8032 compliant.
32+
- Third-party SDK verifiers can use these vectors to independently
33+
verify their Ed25519 implementation is RFC 8032 compliant.
34+
135
## [Unreleased] -- 2026-05-10 (SAN-493 PR 2 of 3)
236

337
### Changed

docs/state.md

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<!-- auto-generated by tools/generate_state_doc.py — do not edit manually -->
2-
<!-- generated: 2026-05-10T23:04:50Z -->
2+
<!-- generated: 2026-05-11T22:48:17Z -->
33

44
# Sanna Python SDK — State
55

@@ -18,7 +18,7 @@ Package: `1.5.0` (source of truth: `src/sanna/version.py`)
1818

1919
## Test Files
2020

21-
Count: 128 (`tests/test_*.py` + `tests/reasoning/test_*.py`)
21+
Count: 129 (`tests/test_*.py` + `tests/reasoning/test_*.py`)
2222

2323
## Source Layout (`src/sanna/`)
2424

@@ -62,15 +62,15 @@ Count: 128 (`tests/test_*.py` + `tests/reasoning/test_*.py`)
6262

6363
## Latest CHANGELOG Entry
6464

65-
## [Unreleased] -- 2026-05-10 (SAN-493 PR 2 of 3)
65+
## [Unreleased] -- 2026-05-11 (SAN-496)
6666

67-
### Changed
67+
### Added
6868

69-
- **`tools/generate_state_doc.py`**: drops git-SHA from the
70-
`docs/state.md` header. The pre-fix header was
71-
`<!-- generated: TS git-sha: SHA -->`; post-fix it is
72-
`<!-- generated: TS -->`. The SHA was always one-commit-stale
73-
because regen runs pre-commit (per the sealed-gate pattern,
74-
HEAD at regen time is the parent commit), so the embedded SHA
75-
never matched the commit that landed the state.md update. Now
76-
the file contains only derived state from sources of truth
69+
- **`tests/vectors/ed25519_rfc8032.json`**: IETF RFC 8032 Section 7.1
70+
Ed25519 algorithm-conformance test vectors (TEST 1, TEST 2, TEST 3,
71+
TEST 1024, TEST SHA(abc)). Hard-coded from the RFC text for unambiguous
72+
authoritative-source linkage.
73+
74+
- **`tests/test_ed25519_rfc8032.py`**: parametrized tests asserting (a)
75+
public key derivation, (b) signature byte-match, (c) signature
76+
round-trip verify for each of the 5 RFC vectors. Catches the failure

tests/test_ed25519_rfc8032.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
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+
)

tests/vectors/README.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@ and key_id are committed alongside the signed output for verifier convenience.
2020
| File | Description |
2121
|------|-------------|
2222
| `canonicalization.json` | RFC 8785-style canonical JSON test cases |
23-
| `constitution_signature.json` | Constitution Ed25519 signature vector |
24-
| `receipt_signature.json` | Receipt Ed25519 signature vector |
23+
| `constitution_signature.json` | Constitution Ed25519 signature vector (protocol conformance; seed `0x01`*32) |
24+
| `receipt_signature.json` | Receipt Ed25519 signature vector (protocol conformance; seed `0x01`*32) |
25+
| `ed25519_rfc8032.json` | IETF RFC 8032 Section 7.1 Ed25519 algorithm-conformance vectors (SAN-496) |
2526

2627
## Canonicalization Rules
2728

@@ -66,3 +67,13 @@ python tests/generate_vectors.py
6667

6768
The generator uses `Ed25519PrivateKey.from_private_bytes(seed)` with
6869
the fixed seed, so output is always identical.
70+
71+
## RFC 8032 Algorithm Conformance (SAN-496)
72+
73+
`ed25519_rfc8032.json` covers a **different test surface** than the protocol-conformance vectors above. The protocol vectors (`constitution_signature.json`, `receipt_signature.json`) verify that this SDK canonicalizes + signs in Sanna's specified style. The RFC 8032 vectors verify that this SDK's underlying Ed25519 implementation produces byte-correct outputs against the IETF authoritative reference -- catching the failure mode where a buggy Ed25519 implementation could pass protocol-conformance (correct flow, wrong crypto output).
74+
75+
Source: [IETF RFC 8032 Section 7.1](https://datatracker.ietf.org/doc/html/rfc8032#section-7.1).
76+
77+
The 5 vectors covered are TEST 1 (empty message), TEST 2 (1 byte), TEST 3 (2 bytes), TEST 1024 (1023 bytes), and TEST SHA(abc) (64-byte SHA-512 of "abc"). For each: the SDK derives the public key from the RFC seed, signs the RFC message, and asserts both outputs match the RFC byte-for-byte. A round-trip verify is also asserted.
78+
79+
These vectors are **hard-coded from the RFC text**, not generated. The authoritative-source link is therefore unambiguous: if `ed25519_rfc8032.json` ever drifts from RFC 8032 Section 7.1, that's a regression worth investigating.

tests/vectors/ed25519_rfc8032.json

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
{
2+
"_meta": {
3+
"source": "IETF RFC 8032 Section 7.1 (Ed25519 test vectors)",
4+
"url": "https://datatracker.ietf.org/doc/html/rfc8032#section-7.1",
5+
"purpose": "Ed25519 ALGORITHM conformance: verifies the SDK's Ed25519 implementation produces byte-correct signatures against authoritative IETF reference vectors. Orthogonal to protocol-conformance vectors (constitution_signature.json + receipt_signature.json) which verify Sanna's canonicalization + signing flow. A buggy Ed25519 implementation could pass protocol conformance while failing RFC 8032; both surfaces are needed for full crypto-correctness assurance.",
6+
"ticket": "SAN-496",
7+
"fields": {
8+
"name": "Human-readable RFC 8032 test label",
9+
"secret_key_hex": "32-byte Ed25519 seed (private key) as lowercase hex; 64 chars",
10+
"public_key_hex": "32-byte raw Ed25519 public key as lowercase hex; 64 chars",
11+
"message_hex": "Variable-length message as lowercase hex (empty string for TEST 1)",
12+
"signature_hex": "64-byte Ed25519 signature as lowercase hex; 128 chars"
13+
}
14+
},
15+
"vectors": [
16+
{
17+
"name": "TEST 1",
18+
"secret_key_hex": "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60",
19+
"public_key_hex": "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a",
20+
"message_hex": "",
21+
"signature_hex": "e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e065224901555fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b"
22+
},
23+
{
24+
"name": "TEST 2",
25+
"secret_key_hex": "4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb",
26+
"public_key_hex": "3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c",
27+
"message_hex": "72",
28+
"signature_hex": "92a009a9f0d4cab8720e820b5f642540a2b27b5416503f8fb3762223ebdb69da085ac1e43e15996e458f3613d0f11d8c387b2eaeb4302aeeb00d291612bb0c00"
29+
},
30+
{
31+
"name": "TEST 3",
32+
"secret_key_hex": "c5aa8df43f9f837bedb7442f31dcb7b166d38535076f094b85ce3a2e0b4458f7",
33+
"public_key_hex": "fc51cd8e6218a1a38da47ed00230f0580816ed13ba3303ac5deb911548908025",
34+
"message_hex": "af82",
35+
"signature_hex": "6291d657deec24024827e69c3abe01a30ce548a284743a445e3680d7db5ac3ac18ff9b538d16f290ae67f760984dc6594a7c15e9716ed28dc027beceea1ec40a"
36+
},
37+
{
38+
"name": "TEST 1024",
39+
"secret_key_hex": "f5e5767cf153319517630f226876b86c8160cc583bc013744c6bf255f5cc0ee5",
40+
"public_key_hex": "278117fc144c72340f67d0f2316e8386ceffbf2b2428c9c51fef7c597f1d426e",
41+
"message_hex": "08b8b2b733424243760fe426a4b54908632110a66c2f6591eabd3345e3e4eb98fa6e264bf09efe12ee50f8f54e9f77b1e355f6c50544e23fb1433ddf73be84d879de7c0046dc4996d9e773f4bc9efe5738829adb26c81b37c93a1b270b20329d658675fc6ea534e0810a4432826bf58c941efb65d57a338bbd2e26640f89ffbc1a858efcb8550ee3a5e1998bd177e93a7363c344fe6b199ee5d02e82d522c4feba15452f80288a821a579116ec6dad2b3b310da903401aa62100ab5d1a36553e06203b33890cc9b832f79ef80560ccb9a39ce767967ed628c6ad573cb116dbefefd75499da96bd68a8a97b928a8bbc103b6621fcde2beca1231d206be6cd9ec7aff6f6c94fcd7204ed3455c68c83f4a41da4af2b74ef5c53f1d8ac70bdcb7ed185ce81bd84359d44254d95629e9855a94a7c1958d1f8ada5d0532ed8a5aa3fb2d17ba70eb6248e594e1a2297acbbb39d502f1a8c6eb6f1ce22b3de1a1f40cc24554119a831a9aad6079cad88425de6bde1a9187ebb6092cf67bf2b13fd65f27088d78b7e883c8759d2c4f5c65adb7553878ad575f9fad878e80a0c9ba63bcbcc2732e69485bbc9c90bfbd62481d9089beccf80cfe2df16a2cf65bd92dd597b0707e0917af48bbb75fed413d238f5555a7a569d80c3414a8d0859dc65a46128bab27af87a71314f318c782b23ebfe808b82b0ce26401d2e22f04d83d1255dc51addd3b75a2b1ae0784504df543af8969be3ea7082ff7fc9888c144da2af58429ec96031dbcad3dad9af0dcbaaaf268cb8fcffead94f3c7ca495e056a9b47acdb751fb73e666c6c655ade8297297d07ad1ba5e43f1bca32301651339e22904cc8c42f58c30c04aafdb038dda0847dd988dcda6f3bfd15c4b4c4525004aa06eeff8ca61783aacec57fb3d1f92b0fe2fd1a85f6724517b65e614ad6808d6f6ee34dff7310fdc82aebfd904b01e1dc54b2927094b2db68d6f903b68401adebf5a7e08d78ff4ef5d63653a65040cf9bfd4aca7984a74d37145986780fc0b16ac451649de6188a7dbdf191f64b5fc5e2ab47b57f7f7276cd419c17a3ca8e1b939ae49e488acba6b965610b5480109c8b17b80e1b7b750dfc7598d5d5011fd2dcc5600a32ef5b52a1ecc820e308aa342721aac0943bf6686b64b2579376504ccc493d97e6aed3fb0f9cd71a43dd497f01f17c0e2cb3797aa2a2f256656168e6c496afc5fb93246f6b1116398a346f1a641f3b041e989f7914f90cc2c7fff357876e506b50d334ba77c225bc307ba537152f3f1610e4eafe595f6d9d90d11faa933a15ef1369546868a7f3a45a96768d40fd9d03412c091c6315cf4fde7cb68606937380db2eaaa707b4c4185c32eddcdd306705e4dc1ffc872eeee475a64dfac86aba41c0618983f8741c5ef68d3a101e8a3b8cac60c905c15fc910840b94c00a0b9d0",
42+
"signature_hex": "0aab4c900501b3e24d7cdf4663326a3a87df5e4843b2cbdb67cbf6e460fec350aa5371b1508f9f4528ecea23c436d94b5e8fcd4f681e30a6ac00a9704a188a03"
43+
},
44+
{
45+
"name": "TEST SHA(abc)",
46+
"secret_key_hex": "833fe62409237b9d62ec77587520911e9a759cec1d19755b7da901b96dca3d42",
47+
"public_key_hex": "ec172b93ad5e563bf4932c70e1245034c35467ef2efd4d64ebf819683467e2bf",
48+
"message_hex": "ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f",
49+
"signature_hex": "dc2a4459e7369633a52b1bf277839a00201009a3efbf3ecb69bea2186c26b58909351fc9ac90b3ecfdfbc7c66431e0303dca179c138ac17ad9bef1177331a704"
50+
}
51+
]
52+
}

0 commit comments

Comments
 (0)