mirror of
https://github.com/saymrwulf/proof-aware-crypto-tooling-agent.git
synced 2026-09-04 20:03:40 +00:00
127 lines
5.3 KiB
Python
127 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"}
|