proof-aware-crypto-tooling-.../tests/test_slhdsa.py
mrwulf a03662438a slhdsa: the post-quantum signing path (deterministic, parameter-locked, additive)
Phase 2b+3 of the step-3 rehearsal, under the four operator decisions of
2026-08-06: deterministic signing, separate slh_dsa block, additive posture,
keygen executed same day (key in provider state, 0600, git-ignored — verified
before generation, not after).

src/pacta/slhdsa.py — the module that did not exist (register:
pq-slot-names-unproven-algorithm). Parameter set LOCKED to SLH-DSA-SHA2-128s:
every entry point asserts the key's reported algorithm and refuses anything
else, because any other set sits outside all eleven certificates while looking
like dogfood. Deterministic via -pkeyopt deterministic:1, so the byte-level
reproducibility check that caught a real defect on the Ed25519 side survives
here. Verification runs two ways: OpenSSL, and pacta-verify-slhdsa built from
the pinned proven source — the one signature check in the estate performed by
code whose verify path the certificates cover. The proven-verifier path is
package-anchored, not cwd-relative: the lesson of signer-backend-depends-on-cwd
applied on day one, not retrofitted.

make_signed_tree_head grows optional slhdsa key parameters. With them, the head
carries a signed slh_dsa block; without, an honest not-configured slot exactly
as ml_dsa always has. ml_dsa itself is untouched. Signatures stay outside the
signed payload for both algorithms — tested by asserting the payload is
byte-identical with and without the slh_dsa key.

Honesty carried in the artifact: signing_backend says "openssl" because no
proven signer exists for any algorithm; the module docstring states that
nothing here is Lean-proven and that the certificates cover the verify path of
the extracted model only.

Tests: 7 new, suite 152 passed, 0 failed, 0 skipped — including determinism
(two signings, identical bytes), the foreign-key refusal (Ed25519 key raises),
corruption rejected by both verifiers, and the proven/OpenSSL agreement.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-06 22:03:39 +02:00

126 lines
5.3 KiB
Python

"""SLH-DSA signing path: deterministic, parameter-locked, two-verifier checked.
Every test uses THROWAWAY keys generated into tmp_path. No test touches the
provider state directory or any long-lived key.
"""
from __future__ import annotations
import base64
import json
def _keypair(tmp_path):
from pacta.slhdsa import generate_slhdsa_keypair
priv, pub = tmp_path / "t.key", tmp_path / "t.pub"
generate_slhdsa_keypair(priv, pub)
return priv, pub
def test_keygen_shape_and_permissions(tmp_path):
priv, pub = _keypair(tmp_path)
assert priv.exists() and pub.exists()
assert (priv.stat().st_mode & 0o777) == 0o600
def test_deterministic_signing_reproduces_bytes(tmp_path):
"""Operator decision 2026-08-06: same payload + key => identical bytes.
This is the property the Ed25519 reproducibility check relies on, and the
reason the deterministic variant was chosen over the FIPS 205 default."""
from pacta.slhdsa import sign_payload_slhdsa
priv, _pub = _keypair(tmp_path)
payload = b"the same head payload"
assert sign_payload_slhdsa(payload, priv) == sign_payload_slhdsa(payload, priv)
def test_sign_verify_roundtrip_both_verifiers(tmp_path):
from pacta.slhdsa import (locate_proven_verifier, sign_payload_slhdsa,
verify_payload_slhdsa, verify_payload_slhdsa_proven)
priv, pub = _keypair(tmp_path)
payload = b"a transparency log head payload"
sig = sign_payload_slhdsa(payload, priv)
ok, err = verify_payload_slhdsa(payload, sig, pub)
assert ok, err
if locate_proven_verifier() is None:
import pytest
pytest.skip("pacta-verify-slhdsa not built on this host")
ok, err = verify_payload_slhdsa_proven(payload, sig, pub)
assert ok, f"proven-source verifier disagrees with OpenSSL: {err}"
def test_corruption_rejected_by_both(tmp_path):
from pacta.slhdsa import (locate_proven_verifier, sign_payload_slhdsa,
verify_payload_slhdsa, verify_payload_slhdsa_proven)
priv, pub = _keypair(tmp_path)
payload = b"payload"
raw = bytearray(base64.b64decode(sign_payload_slhdsa(payload, priv)))
raw[0] ^= 1
bad = base64.b64encode(bytes(raw)).decode()
ok, _ = verify_payload_slhdsa(payload, bad, pub)
assert not ok
if locate_proven_verifier() is not None:
ok, _ = verify_payload_slhdsa_proven(payload, bad, pub)
assert not ok
def test_parameter_set_lock_refuses_foreign_key(tmp_path):
"""An Ed25519 key must be refused outright — a signature under any other
algorithm would look like dogfood while sitting outside every proof."""
import pytest
from pacta.signing import generate_ed25519_keypair
from pacta.slhdsa import SlhDsaError, sign_payload_slhdsa
priv, pub = tmp_path / "ed.key", tmp_path / "ed.pub"
generate_ed25519_keypair(priv, pub)
with pytest.raises(SlhDsaError):
sign_payload_slhdsa(b"x", priv)
def test_head_carries_separate_slh_dsa_block(tmp_path):
"""make_signed_tree_head with an SLH-DSA key: both signatures verify, the
ml_dsa slot is UNTOUCHED, and without a key the slot degrades honestly."""
from pacta.signing import generate_ed25519_keypair, verify_payload_ed25519_detailed
from pacta.slhdsa import verify_payload_slhdsa
from pacta.transparency import make_signed_tree_head, signed_tree_head_payload
ed_priv, ed_pub = tmp_path / "ed.key", tmp_path / "ed.pub"
generate_ed25519_keypair(ed_priv, ed_pub)
slh_priv, slh_pub = _keypair(tmp_path)
sth = make_signed_tree_head("00" * 32, 19, "11" * 32, "2026-08-06T00:00:00Z",
ed_priv, ed_pub,
slhdsa_private_key_path=slh_priv,
slhdsa_public_key_path=slh_pub)
payload = signed_tree_head_payload(sth)
ed = sth["signatures"]["ed25519"]
ok, err, _backend = verify_payload_ed25519_detailed(payload, ed["signature_base64"], ed_pub)
assert ok, err
slh = sth["signatures"]["slh_dsa"]
assert slh["status"] == "signed"
assert slh["parameter_set"] == "SLH-DSA-SHA2-128s"
assert slh["mode"] == "deterministic"
ok, err = verify_payload_slhdsa(payload, slh["signature_base64"], slh_pub)
assert ok, err
# ml_dsa stays exactly the honest disclosure it always was
assert sth["signatures"]["ml_dsa"]["status"] in {"not_configured", "unavailable"}
assert "signature_base64" not in sth["signatures"]["ml_dsa"]
# additive: no key => honest not-configured slot, never an error
bare = make_signed_tree_head("00" * 32, 19, "11" * 32, "2026-08-06T00:00:00Z",
ed_priv, ed_pub)
assert bare["signatures"]["slh_dsa"]["status"] == "not_configured"
# and the payload is unchanged by the slh_dsa presence: signatures are
# outside the signed bytes for BOTH algorithms
assert signed_tree_head_payload(bare) == payload
def test_block_is_json_serialisable(tmp_path):
from pacta.slhdsa import slh_dsa_signature_block
priv, pub = _keypair(tmp_path)
block = slh_dsa_signature_block(b"payload", priv, pub)
json.dumps(block)
assert set(block) >= {"scheme", "standard", "parameter_set", "mode", "status",
"payload_digest_sha256", "signature_base64",
"public_key_fingerprint_sha256"}