proof-aware-crypto-tooling-.../src/pacta/signing.py

125 lines
4.9 KiB
Python
Raw Normal View History

2026-07-03 11:03:58 +00:00
from __future__ import annotations
import base64
import hashlib
import json
import shutil
import subprocess
import tempfile
from pathlib import Path
from typing import Any
class SignatureError(RuntimeError):
pass
def canonical_attestation_payload(attestation: dict[str, Any]) -> bytes:
unsigned = {key: value for key, value in attestation.items() if key != "signature"}
return json.dumps(unsigned, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
def payload_digest(attestation: dict[str, Any]) -> str:
return hashlib.sha256(canonical_attestation_payload(attestation)).hexdigest()
def public_key_fingerprint(public_key_path: str | Path) -> str:
data = Path(public_key_path).read_bytes()
return hashlib.sha256(data).hexdigest()
def generate_ed25519_keypair(private_key_path: str | Path, public_key_path: str | Path) -> None:
openssl = _openssl()
private_path = Path(private_key_path)
public_path = Path(public_key_path)
private_path.parent.mkdir(parents=True, exist_ok=True)
public_path.parent.mkdir(parents=True, exist_ok=True)
subprocess.run([openssl, "genpkey", "-algorithm", "ed25519", "-out", str(private_path)], check=True, timeout=30)
subprocess.run([openssl, "pkey", "-in", str(private_path), "-pubout", "-out", str(public_path)], check=True, timeout=30)
def sign_attestation(attestation: dict[str, Any], private_key_path: str | Path, public_key_path: str | Path | None = None) -> dict[str, Any]:
openssl = _openssl()
payload = canonical_attestation_payload(attestation)
with tempfile.TemporaryDirectory(prefix="pacta-sign-") as tmp:
payload_path = Path(tmp) / "payload.json"
signature_path = Path(tmp) / "payload.sig"
payload_path.write_bytes(payload)
completed = subprocess.run(
[openssl, "pkeyutl", "-sign", "-inkey", str(private_key_path), "-rawin", "-in", str(payload_path), "-out", str(signature_path)],
check=False,
capture_output=True,
text=True,
timeout=30,
)
if completed.returncode != 0:
raise SignatureError((completed.stderr or completed.stdout or "openssl signing failed").strip())
signature_bytes = signature_path.read_bytes()
signed = dict(attestation)
signed["signature"] = {
"scheme": "openssl-ed25519",
"status": "signed",
"payload_digest_sha256": hashlib.sha256(payload).hexdigest(),
"signature_base64": base64.b64encode(signature_bytes).decode("ascii"),
}
if public_key_path:
signed["signature"]["public_key_fingerprint_sha256"] = public_key_fingerprint(public_key_path)
return signed
def verify_attestation_signature(attestation: dict[str, Any], public_key_path: str | Path) -> tuple[bool, str | None]:
openssl = _openssl()
signature = attestation.get("signature") or {}
if signature.get("scheme") != "openssl-ed25519":
return False, f"Unsupported attestation signature scheme: {signature.get('scheme')}"
encoded = signature.get("signature_base64")
if not encoded:
return False, "Attestation signature is missing signature_base64."
expected_digest = signature.get("payload_digest_sha256")
actual_digest = payload_digest(attestation)
if expected_digest and expected_digest != actual_digest:
return False, "Attestation payload digest does not match signature metadata."
expected_fingerprint = signature.get("public_key_fingerprint_sha256")
if expected_fingerprint:
actual_fingerprint = public_key_fingerprint(public_key_path)
if expected_fingerprint != actual_fingerprint:
return False, "Attestation public key fingerprint does not match signature metadata."
try:
signature_bytes = base64.b64decode(encoded, validate=True)
except ValueError as exc:
return False, f"Invalid base64 signature: {exc}"
with tempfile.TemporaryDirectory(prefix="pacta-verify-") as tmp:
payload_path = Path(tmp) / "payload.json"
signature_path = Path(tmp) / "payload.sig"
payload_path.write_bytes(canonical_attestation_payload(attestation))
signature_path.write_bytes(signature_bytes)
completed = subprocess.run(
[
openssl,
"pkeyutl",
"-verify",
"-pubin",
"-inkey",
str(public_key_path),
"-rawin",
"-in",
str(payload_path),
"-sigfile",
str(signature_path),
],
check=False,
capture_output=True,
text=True,
timeout=30,
)
if completed.returncode != 0:
return False, (completed.stderr or completed.stdout or "openssl verification failed").strip()
return True, None
def _openssl() -> str:
path = shutil.which("openssl")
if not path:
raise SignatureError("openssl is not available on PATH")
return path