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