mirror of
https://github.com/saymrwulf/proof-aware-crypto-tooling-agent.git
synced 2026-09-03 19:53:43 +00:00
Dogfood cryptography: pacta verifies signatures through the PROVEN code path
"Eat your own dogfood": pacta consumes certificates about a verified
Ed25519 implementation while checking those certificates' signatures
with OpenSSL. Now it can use the object of its own evidence:
- dogfood/pacta-verified-verify: a ~90-line Rust binary built against
the PINNED proven source workspace (saymrwulf/curve25519-dalek-source
at the exact commit the dalek certificates pin - the build records it:
aa0f6ab...) with the serial backend pinned via RUSTFLAGS exactly as
the verified extraction pins it. Cargo.toml is committed as a template
({{SOURCE}} placeholder) so no machine path is hardcoded; the rendered
file, target/, and the built binary are gitignored.
- pacta dogfood-build --source <workspace>: renders, builds, installs
to dogfood/state/, and writes a provenance sidecar (source commit,
backend cfg, rustc, and an honest coverage note: the certificates
cover verify_sha512, the extraction-refactored image of this verify
path; SHA-512 and the wire glue remain the theorems' documented
boundary). pacta dogfood-status reports the active backend.
- signing.verify_payload_ed25519_detailed: dispatch - the dogfood
binary when present (backend "verified-dalek-serial"), OpenSSL
fallback otherwise, and the backend that ACTUALLY ran is recorded in
receipt signature statuses and attestation evidence. Fallback is
never silent.
- --require-verified-verifier (receipt-verify + agent): policy fails
closed when verification did not run on the certificate-covered
path.
- ML-DSA is deliberately unchanged: no proven implementation exists,
so the slot stays fail-closed "unavailable" - the honest hybrid-PQC
posture is one proven-classical signature plus one required-but-
unproven PQC slot, never a pretend backend.
Validated live: receipt verification through the proven verifier
(backend recorded), a corrupted signature bit rejected BY the proven
binary, tampered attestations rejected, and the policy failing closed
when the binary is absent. 49/49 tests green (incl. PEM-SPKI raw-key
cross-check against openssl, dispatch/backend recording with a stub,
and a real-binary roundtrip that skips gracefully where unbuilt).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
7c717d03fc
commit
d331ba17d9
9 changed files with 444 additions and 11 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -14,3 +14,7 @@ __pycache__/
|
|||
.lake/
|
||||
*.olean
|
||||
*.ilean
|
||||
dogfood/state/
|
||||
dogfood/pacta-verified-verify/target/
|
||||
dogfood/pacta-verified-verify/Cargo.toml
|
||||
dogfood/pacta-verified-verify/Cargo.lock
|
||||
|
|
|
|||
14
dogfood/pacta-verified-verify/Cargo.toml.template
Normal file
14
dogfood/pacta-verified-verify/Cargo.toml.template
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# Rendered by `pacta dogfood-build` - the {{SOURCE}} placeholder is replaced
|
||||
# with the local checkout of the PINNED proven source workspace
|
||||
# (saymrwulf/curve25519-dalek-source). Committed as a template so the repo
|
||||
# never hardcodes a machine-specific path.
|
||||
[package]
|
||||
name = "pacta-verified-verify"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
ed25519-dalek = { path = "{{SOURCE}}/ed25519-dalek", default-features = false, features = ["fast", "zeroize"] }
|
||||
|
||||
[workspace]
|
||||
77
dogfood/pacta-verified-verify/src/main.rs
Normal file
77
dogfood/pacta-verified-verify/src/main.rs
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
//! pacta's dogfood Ed25519 verifier.
|
||||
//!
|
||||
//! This binary is built against the PINNED, PROVEN source workspace
|
||||
//! (saymrwulf/curve25519-dalek-source at the commit named in the build
|
||||
//! provenance) with the serial backend pinned - the exact code path whose
|
||||
//! correctness certificates pacta consumes. When pacta checks a provider
|
||||
//! signature or a signed tree head through this binary, the agent is
|
||||
//! eating its own dogfood: the arithmetic under the verification is
|
||||
//! certificate-covered (field, group law, scalars, decompression, and the
|
||||
//! four-tier signature apex), and the residual trusted base is exactly the
|
||||
//! theorems' documented boundary (SHA-512 as an oracle, the wire glue).
|
||||
//!
|
||||
//! Usage: pacta-verified-verify <pubkey-hex-32B> <sig-hex-64B> <payload-file>
|
||||
//! Exit 0 = signature valid; exit 1 = invalid; exit 2 = usage/format error.
|
||||
|
||||
use ed25519_dalek::{Signature, Verifier, VerifyingKey};
|
||||
use std::process::ExitCode;
|
||||
|
||||
fn hex_decode(s: &str) -> Result<Vec<u8>, String> {
|
||||
if s.len() % 2 != 0 {
|
||||
return Err("odd-length hex".into());
|
||||
}
|
||||
(0..s.len() / 2)
|
||||
.map(|i| u8::from_str_radix(&s[2 * i..2 * i + 2], 16).map_err(|e| e.to_string()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
if args.len() != 4 {
|
||||
eprintln!("usage: pacta-verified-verify <pubkey-hex> <sig-hex> <payload-file>");
|
||||
return ExitCode::from(2);
|
||||
}
|
||||
let pk_bytes = match hex_decode(&args[1]) {
|
||||
Ok(b) if b.len() == 32 => b,
|
||||
_ => {
|
||||
eprintln!("error: public key must be 32 bytes of hex");
|
||||
return ExitCode::from(2);
|
||||
}
|
||||
};
|
||||
let sig_bytes = match hex_decode(&args[2]) {
|
||||
Ok(b) if b.len() == 64 => b,
|
||||
_ => {
|
||||
eprintln!("error: signature must be 64 bytes of hex");
|
||||
return ExitCode::from(2);
|
||||
}
|
||||
};
|
||||
let payload = match std::fs::read(&args[3]) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
eprintln!("error: cannot read payload: {e}");
|
||||
return ExitCode::from(2);
|
||||
}
|
||||
};
|
||||
let mut pk_array = [0u8; 32];
|
||||
pk_array.copy_from_slice(&pk_bytes);
|
||||
let verifying_key = match VerifyingKey::from_bytes(&pk_array) {
|
||||
Ok(k) => k,
|
||||
Err(e) => {
|
||||
eprintln!("error: invalid public key: {e}");
|
||||
return ExitCode::from(2);
|
||||
}
|
||||
};
|
||||
let mut sig_array = [0u8; 64];
|
||||
sig_array.copy_from_slice(&sig_bytes);
|
||||
let signature = Signature::from_bytes(&sig_array);
|
||||
match verifying_key.verify(&payload, &signature) {
|
||||
Ok(()) => {
|
||||
println!("OK");
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
Err(_) => {
|
||||
println!("INVALID");
|
||||
ExitCode::from(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@ from typing import Any
|
|||
|
||||
from .config import RepoConfig
|
||||
from .profiles import get_profile
|
||||
from .signing import verify_attestation_signature
|
||||
from .signing import verify_attestation_signature_detailed
|
||||
from .sthstore import check_sth_against_store, check_sth_freshness
|
||||
from .transparency import load_receipt, verify_receipt
|
||||
from .yamlio import load_data
|
||||
|
|
@ -45,6 +45,7 @@ def validate_attestation(
|
|||
sth_store_path: str | Path | None = None,
|
||||
consistency_proof_path: str | Path | None = None,
|
||||
max_sth_age_seconds: int | None = None,
|
||||
require_verified_verifier: bool = False,
|
||||
) -> AttestationResult:
|
||||
provider = raw.get("provider")
|
||||
subject = raw.get("subject") or {}
|
||||
|
|
@ -82,12 +83,18 @@ def validate_attestation(
|
|||
signature = raw.get("signature") or {}
|
||||
environment = raw.get("environment") or {}
|
||||
signature_status = signature.get("status", "not_checked")
|
||||
signature_backend = "none"
|
||||
if public_key_path:
|
||||
ok, error = verify_attestation_signature(raw, public_key_path)
|
||||
ok, error, signature_backend = verify_attestation_signature_detailed(raw, public_key_path)
|
||||
if ok:
|
||||
signature_status = "verified"
|
||||
else:
|
||||
diagnostics.append(f"Attestation signature verification failed: {error}")
|
||||
if require_verified_verifier and signature_backend != "verified-dalek-serial":
|
||||
diagnostics.append(
|
||||
"Policy requires the dogfood (certificate-covered) Ed25519 verifier, but verification ran on "
|
||||
f"backend '{signature_backend}'. Build it with: pacta dogfood-build --source <pinned-workspace>."
|
||||
)
|
||||
elif signature_status == "signed":
|
||||
diagnostics.append("Signed attestation requires --attestation-public-key.")
|
||||
elif signature_status == "not_implemented":
|
||||
|
|
@ -142,6 +149,7 @@ def validate_attestation(
|
|||
"attestation_provider": provider,
|
||||
"attestation_path": str(path) if path else None,
|
||||
"attestation_signature_status": signature_status,
|
||||
"attestation_signature_backend": signature_backend,
|
||||
"attestation_log_url": raw.get("log_url") or signature.get("log_url"),
|
||||
"attestation_issued_at": raw.get("issued_at"),
|
||||
"check_log_path": (raw.get("replay") or {}).get("check_log_path"),
|
||||
|
|
|
|||
|
|
@ -139,8 +139,17 @@ def build_parser() -> argparse.ArgumentParser:
|
|||
receipt_verify.add_argument("--sth-store", help="Path to the local STH pin store (split-view/rollback defense).")
|
||||
receipt_verify.add_argument("--consistency-proof", help="File with a hex consistency proof from the pinned tree size (provider: log-consistency).")
|
||||
receipt_verify.add_argument("--max-sth-age-seconds", type=int, help="Reject signed tree heads older than this (freshness policy).")
|
||||
receipt_verify.add_argument("--require-verified-verifier", action="store_true", help="Fail closed unless Ed25519 verification ran on the dogfood (certificate-covered) verifier.")
|
||||
receipt_verify.set_defaults(func=cmd_receipt_verify)
|
||||
|
||||
dogfood_build = sub.add_parser("dogfood-build", help="Build the dogfood Ed25519 verifier from the pinned proven source workspace.")
|
||||
dogfood_build.add_argument("--source", required=True, help="Local checkout of saymrwulf/curve25519-dalek-source (the pinned proven workspace).")
|
||||
dogfood_build.add_argument("--timeout", type=int, default=900)
|
||||
dogfood_build.set_defaults(func=cmd_dogfood_build)
|
||||
|
||||
dogfood_status = sub.add_parser("dogfood-status", help="Show which Ed25519 verification backend pacta will use, with provenance.")
|
||||
dogfood_status.set_defaults(func=cmd_dogfood_status)
|
||||
|
||||
agent = sub.add_parser("agent", help="Apply a policy-gated consequence to verification evidence.")
|
||||
agent.add_argument("--claims", help="Existing claim card to act on.")
|
||||
agent.add_argument("--config", help="Repository config used to generate a claim card.")
|
||||
|
|
@ -169,6 +178,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|||
agent.add_argument("--sth-store", help="Path to the local STH pin store (split-view/rollback defense).")
|
||||
agent.add_argument("--consistency-proof", help="File with a hex consistency proof from the pinned tree size.")
|
||||
agent.add_argument("--max-sth-age-seconds", type=int, help="Reject signed tree heads older than this.")
|
||||
agent.add_argument("--require-verified-verifier", action="store_true", help="Fail closed unless Ed25519 verification ran on the dogfood (certificate-covered) verifier.")
|
||||
agent.set_defaults(func=cmd_agent)
|
||||
return parser
|
||||
|
||||
|
|
@ -385,6 +395,11 @@ def cmd_receipt_verify(args: argparse.Namespace) -> int:
|
|||
consistency_proof_path=args.consistency_proof,
|
||||
max_sth_age_seconds=args.max_sth_age_seconds,
|
||||
)
|
||||
if args.require_verified_verifier and result.signatures.get("ed25519_backend") != "verified-dalek-serial":
|
||||
accountability_diagnostics.append(
|
||||
"Policy requires the dogfood (certificate-covered) Ed25519 verifier, but verification ran on backend "
|
||||
f"'{result.signatures.get('ed25519_backend', 'none')}'."
|
||||
)
|
||||
if accountability_diagnostics:
|
||||
result.accepted = False
|
||||
result.diagnostics.extend(accountability_diagnostics)
|
||||
|
|
@ -402,6 +417,38 @@ def cmd_receipt_verify(args: argparse.Namespace) -> int:
|
|||
return 0 if result.accepted else 1
|
||||
|
||||
|
||||
def cmd_dogfood_build(args: argparse.Namespace) -> int:
|
||||
from .dogfood import build_verifier
|
||||
|
||||
result = build_verifier(args.source, timeout=args.timeout)
|
||||
for diagnostic in result.diagnostics:
|
||||
print(diagnostic)
|
||||
if not result.built:
|
||||
print("dogfood verifier NOT built; Ed25519 verification falls back to OpenSSL (recorded as a downgrade).")
|
||||
return 1
|
||||
print(f"binary: {result.binary_path}")
|
||||
for key in ("source_commit", "backend_cfg", "rustc_version"):
|
||||
print(f"{key}: {result.provenance.get(key)}")
|
||||
print("coverage: " + str(result.provenance.get("coverage_note")))
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_dogfood_status(args: argparse.Namespace) -> int:
|
||||
from .dogfood import BACKEND_OPENSSL, BACKEND_VERIFIED, load_provenance, locate_verifier
|
||||
|
||||
binary = locate_verifier()
|
||||
if binary is None:
|
||||
print(f"backend: {BACKEND_OPENSSL} (fallback)")
|
||||
print("dogfood verifier not built. Build with: pacta dogfood-build --source <pinned-workspace>")
|
||||
return 1
|
||||
print(f"backend: {BACKEND_VERIFIED}")
|
||||
print(f"binary: {binary}")
|
||||
provenance = load_provenance(binary)
|
||||
for key in ("source_workspace", "source_commit", "backend_cfg", "rustc_version"):
|
||||
print(f"{key}: {provenance.get(key)}")
|
||||
return 0
|
||||
|
||||
|
||||
def _log_accountability_checks(
|
||||
receipt: dict,
|
||||
sth_store: str | None,
|
||||
|
|
@ -553,4 +600,5 @@ def _attestation_for_args(args: argparse.Namespace, repo: RepoConfig):
|
|||
sth_store_path=getattr(args, "sth_store", None),
|
||||
consistency_proof_path=getattr(args, "consistency_proof", None),
|
||||
max_sth_age_seconds=getattr(args, "max_sth_age_seconds", None),
|
||||
require_verified_verifier=bool(getattr(args, "require_verified_verifier", False)),
|
||||
)
|
||||
|
|
|
|||
184
src/pacta/dogfood.py
Normal file
184
src/pacta/dogfood.py
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import binascii
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
DOGFOOD_ENV = "PACTA_DOGFOOD_VERIFIER"
|
||||
DEFAULT_STATE_DIR = Path("dogfood") / "state"
|
||||
BACKEND_VERIFIED = "verified-dalek-serial"
|
||||
BACKEND_OPENSSL = "openssl"
|
||||
|
||||
# Ed25519 SubjectPublicKeyInfo (RFC 8410): a fixed 12-byte DER prefix then
|
||||
# the raw 32-byte key. Parsing by prefix is exact for this OID, not a
|
||||
# heuristic.
|
||||
_ED25519_SPKI_PREFIX = bytes.fromhex("302a300506032b6570032100")
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class DogfoodBuildResult:
|
||||
built: bool
|
||||
binary_path: Path | None
|
||||
provenance: dict[str, Any] = field(default_factory=dict)
|
||||
diagnostics: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
def pem_public_key_to_raw(public_key_path: str | Path) -> bytes:
|
||||
"""Extract the raw 32-byte Ed25519 key from an OpenSSL PEM SPKI file."""
|
||||
text = Path(public_key_path).read_text(encoding="utf-8")
|
||||
body = "".join(
|
||||
line.strip()
|
||||
for line in text.splitlines()
|
||||
if line.strip() and not line.startswith("-----")
|
||||
)
|
||||
try:
|
||||
der = base64.b64decode(body, validate=True)
|
||||
except (binascii.Error, ValueError) as exc:
|
||||
raise ValueError(f"Not a PEM public key: {public_key_path}: {exc}") from exc
|
||||
if not der.startswith(_ED25519_SPKI_PREFIX) or len(der) != len(_ED25519_SPKI_PREFIX) + 32:
|
||||
raise ValueError(
|
||||
f"Not an Ed25519 SubjectPublicKeyInfo: {public_key_path} "
|
||||
f"(got {len(der)} DER bytes)"
|
||||
)
|
||||
return der[len(_ED25519_SPKI_PREFIX):]
|
||||
|
||||
|
||||
def default_binary_path(state_dir: str | Path | None = None) -> Path:
|
||||
return Path(state_dir or DEFAULT_STATE_DIR) / "pacta-verified-verify"
|
||||
|
||||
|
||||
def locate_verifier(state_dir: str | Path | None = None) -> Path | None:
|
||||
"""Find the dogfood verifier binary: explicit env var first, then the
|
||||
default build location. Returns None when unavailable (callers fall
|
||||
back to OpenSSL and must record the downgrade)."""
|
||||
env = os.environ.get(DOGFOOD_ENV)
|
||||
if env:
|
||||
path = Path(env)
|
||||
return path if path.exists() else None
|
||||
path = default_binary_path(state_dir)
|
||||
return path if path.exists() else None
|
||||
|
||||
|
||||
def load_provenance(binary_path: str | Path) -> dict[str, Any]:
|
||||
sidecar = Path(binary_path).with_suffix(".provenance.json")
|
||||
if sidecar.exists():
|
||||
return json.loads(sidecar.read_text(encoding="utf-8"))
|
||||
return {}
|
||||
|
||||
|
||||
def build_verifier(
|
||||
source_workspace: str | Path,
|
||||
crate_dir: str | Path = Path("dogfood") / "pacta-verified-verify",
|
||||
state_dir: str | Path | None = None,
|
||||
timeout: int = 900,
|
||||
) -> DogfoodBuildResult:
|
||||
"""Build the dogfood verifier against the pinned proven source workspace.
|
||||
|
||||
The serial backend is pinned via RUSTFLAGS exactly as the verified
|
||||
extraction pins it; provenance (source path, source commit, rustc,
|
||||
backend cfg) is recorded next to the binary and surfaces in evidence.
|
||||
"""
|
||||
source = Path(source_workspace).expanduser().resolve()
|
||||
crate = Path(crate_dir).resolve()
|
||||
diagnostics: list[str] = []
|
||||
cargo = shutil.which("cargo")
|
||||
if not cargo:
|
||||
return DogfoodBuildResult(False, None, {}, ["cargo is not available on PATH; cannot build the dogfood verifier."])
|
||||
if not (source / "ed25519-dalek" / "Cargo.toml").exists():
|
||||
return DogfoodBuildResult(
|
||||
False,
|
||||
None,
|
||||
{},
|
||||
[f"{source} does not look like the pinned curve25519-dalek source workspace (no ed25519-dalek/Cargo.toml)."],
|
||||
)
|
||||
template = (crate / "Cargo.toml.template").read_text(encoding="utf-8")
|
||||
(crate / "Cargo.toml").write_text(template.replace("{{SOURCE}}", str(source)), encoding="utf-8")
|
||||
env = dict(os.environ)
|
||||
backend_cfg = 'curve25519_dalek_backend="serial"'
|
||||
env["RUSTFLAGS"] = (env.get("RUSTFLAGS", "") + f" --cfg {backend_cfg}").strip()
|
||||
completed = subprocess.run(
|
||||
[cargo, "build", "--release", "--quiet"],
|
||||
cwd=str(crate),
|
||||
env=env,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=timeout,
|
||||
)
|
||||
if completed.returncode != 0:
|
||||
tail = (completed.stderr or completed.stdout or "").strip().splitlines()[-12:]
|
||||
return DogfoodBuildResult(False, None, {}, ["cargo build failed:"] + tail)
|
||||
built = crate / "target" / "release" / "pacta-verified-verify"
|
||||
if not built.exists():
|
||||
return DogfoodBuildResult(False, None, {}, [f"cargo reported success but {built} does not exist."])
|
||||
out = default_binary_path(state_dir)
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(built, out)
|
||||
provenance = {
|
||||
"type": "pacta.dogfood.verifier_provenance.v1",
|
||||
"source_workspace": str(source),
|
||||
"source_commit": _git_commit(source),
|
||||
"backend_cfg": backend_cfg,
|
||||
"rustc_version": _tool_version("rustc"),
|
||||
"cargo_version": _tool_version("cargo"),
|
||||
"entry_point": "ed25519_dalek::VerifyingKey::verify (pinned workspace)",
|
||||
"coverage_note": (
|
||||
"The certificates cover verify_sha512, the extraction-refactored image of this verify path "
|
||||
"(same internals; the delta is the documented hash-wrapper refactor in the pinned source). "
|
||||
"Field, group law, scalars, encoding/decompression, and the four apex tiers are certificate-covered; "
|
||||
"SHA-512 and the ~15 lines of wire glue are the theorems' documented trusted base."
|
||||
),
|
||||
}
|
||||
out.with_suffix(".provenance.json").write_text(json.dumps(provenance, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||
return DogfoodBuildResult(True, out, provenance, diagnostics)
|
||||
|
||||
|
||||
def verify_payload_dogfood(
|
||||
payload: bytes,
|
||||
signature: bytes,
|
||||
public_key_path: str | Path,
|
||||
binary: str | Path,
|
||||
timeout: int = 30,
|
||||
) -> tuple[bool, str | None]:
|
||||
try:
|
||||
raw_key = pem_public_key_to_raw(public_key_path)
|
||||
except ValueError as exc:
|
||||
return False, str(exc)
|
||||
with tempfile.TemporaryDirectory(prefix="pacta-dogfood-") as tmp:
|
||||
payload_path = Path(tmp) / "payload.bin"
|
||||
payload_path.write_bytes(payload)
|
||||
completed = subprocess.run(
|
||||
[str(binary), raw_key.hex(), signature.hex(), str(payload_path)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=timeout,
|
||||
)
|
||||
if completed.returncode == 0:
|
||||
return True, None
|
||||
if completed.returncode == 1:
|
||||
return False, "signature invalid (verified-path verifier)"
|
||||
return False, (completed.stderr or completed.stdout or "dogfood verifier error").strip()
|
||||
|
||||
|
||||
def _git_commit(path: Path) -> str | None:
|
||||
git = shutil.which("git")
|
||||
if not git:
|
||||
return None
|
||||
completed = subprocess.run(
|
||||
[git, "rev-parse", "HEAD"], cwd=str(path), capture_output=True, text=True, timeout=15
|
||||
)
|
||||
return completed.stdout.strip() if completed.returncode == 0 else None
|
||||
|
||||
|
||||
def _tool_version(tool: str) -> str | None:
|
||||
path = shutil.which(tool)
|
||||
if not path:
|
||||
return None
|
||||
completed = subprocess.run([path, "--version"], capture_output=True, text=True, timeout=15)
|
||||
return completed.stdout.strip() if completed.returncode == 0 else None
|
||||
|
|
@ -58,22 +58,27 @@ def sign_attestation(attestation: dict[str, Any], private_key_path: str | Path,
|
|||
|
||||
|
||||
def verify_attestation_signature(attestation: dict[str, Any], public_key_path: str | Path) -> tuple[bool, str | None]:
|
||||
ok, error, _backend = verify_attestation_signature_detailed(attestation, public_key_path)
|
||||
return ok, error
|
||||
|
||||
|
||||
def verify_attestation_signature_detailed(attestation: dict[str, Any], public_key_path: str | Path) -> tuple[bool, str | None, str]:
|
||||
signature = attestation.get("signature") or {}
|
||||
if signature.get("scheme") != "openssl-ed25519":
|
||||
return False, f"Unsupported attestation signature scheme: {signature.get('scheme')}"
|
||||
return False, f"Unsupported attestation signature scheme: {signature.get('scheme')}", "none"
|
||||
encoded = signature.get("signature_base64")
|
||||
if not encoded:
|
||||
return False, "Attestation signature is missing signature_base64."
|
||||
return False, "Attestation signature is missing signature_base64.", "none"
|
||||
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."
|
||||
return False, "Attestation payload digest does not match signature metadata.", "none"
|
||||
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."
|
||||
return verify_payload_ed25519(canonical_attestation_payload(attestation), encoded, public_key_path)
|
||||
return False, "Attestation public key fingerprint does not match signature metadata.", "none"
|
||||
return verify_payload_ed25519_detailed(canonical_attestation_payload(attestation), encoded, public_key_path)
|
||||
|
||||
|
||||
def sign_payload_ed25519(payload: bytes, private_key_path: str | Path) -> str:
|
||||
|
|
@ -96,11 +101,37 @@ def sign_payload_ed25519(payload: bytes, private_key_path: str | Path) -> str:
|
|||
|
||||
|
||||
def verify_payload_ed25519(payload: bytes, signature_base64: str, public_key_path: str | Path) -> tuple[bool, str | None]:
|
||||
openssl = _openssl()
|
||||
ok, error, _backend = verify_payload_ed25519_detailed(payload, signature_base64, public_key_path)
|
||||
return ok, error
|
||||
|
||||
|
||||
def verify_payload_ed25519_detailed(
|
||||
payload: bytes,
|
||||
signature_base64: str,
|
||||
public_key_path: str | Path,
|
||||
) -> tuple[bool, str | None, str]:
|
||||
"""Verify an Ed25519 signature, preferring pacta's DOGFOOD verifier -
|
||||
a binary built from the pinned, certificate-covered dalek source with
|
||||
the serial backend pinned - and falling back to OpenSSL when the
|
||||
dogfood binary is unavailable. The third element names the backend that
|
||||
actually ran so callers can record (and policies can require) the
|
||||
verified path."""
|
||||
from .dogfood import BACKEND_OPENSSL, BACKEND_VERIFIED, locate_verifier, verify_payload_dogfood
|
||||
|
||||
try:
|
||||
signature_bytes = base64.b64decode(signature_base64, validate=True)
|
||||
except ValueError as exc:
|
||||
return False, f"Invalid base64 signature: {exc}"
|
||||
return False, f"Invalid base64 signature: {exc}", "none"
|
||||
binary = locate_verifier()
|
||||
if binary is not None:
|
||||
ok, error = verify_payload_dogfood(payload, signature_bytes, public_key_path, binary)
|
||||
return ok, error, BACKEND_VERIFIED
|
||||
ok, error = _verify_payload_openssl(payload, signature_bytes, public_key_path)
|
||||
return ok, error, BACKEND_OPENSSL
|
||||
|
||||
|
||||
def _verify_payload_openssl(payload: bytes, signature_bytes: bytes, public_key_path: str | Path) -> tuple[bool, str | None]:
|
||||
openssl = _openssl()
|
||||
with tempfile.TemporaryDirectory(prefix="pacta-verify-") as tmp:
|
||||
payload_path = Path(tmp) / "payload.bin"
|
||||
signature_path = Path(tmp) / "payload.sig"
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ from pathlib import Path
|
|||
from typing import Any
|
||||
|
||||
from .postquantum import detect_ml_dsa
|
||||
from .signing import canonical_json, public_key_fingerprint, sign_payload_ed25519, verify_payload_ed25519
|
||||
from .signing import canonical_json, public_key_fingerprint, sign_payload_ed25519, verify_payload_ed25519_detailed
|
||||
from .yamlio import load_data
|
||||
|
||||
HASH_ALGORITHM = "RFC9162_SHA256"
|
||||
|
|
@ -206,8 +206,9 @@ def verify_signed_tree_head(
|
|||
diagnostics.append("Signed tree head Ed25519 public-key fingerprint mismatch.")
|
||||
statuses["ed25519"] = "key_mismatch"
|
||||
else:
|
||||
ok, error = verify_payload_ed25519(payload, str(ed25519.get("signature_base64") or ""), public_key_path)
|
||||
ok, error, backend = verify_payload_ed25519_detailed(payload, str(ed25519.get("signature_base64") or ""), public_key_path)
|
||||
statuses["ed25519"] = "verified" if ok else "failed"
|
||||
statuses["ed25519_backend"] = backend
|
||||
if not ok:
|
||||
diagnostics.append(f"Signed tree head Ed25519 verification failed: {error}")
|
||||
|
||||
|
|
|
|||
66
tests/test_dogfood.py
Normal file
66
tests/test_dogfood.py
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import os
|
||||
import stat
|
||||
import subprocess
|
||||
|
||||
from pacta.dogfood import BACKEND_VERIFIED, locate_verifier, pem_public_key_to_raw
|
||||
from pacta.signing import generate_ed25519_keypair, sign_payload_ed25519, verify_payload_ed25519_detailed
|
||||
|
||||
|
||||
def test_pem_spki_to_raw_roundtrip(tmp_path):
|
||||
private_key = tmp_path / "k.key"
|
||||
public_key = tmp_path / "k.pub"
|
||||
generate_ed25519_keypair(private_key, public_key)
|
||||
raw = pem_public_key_to_raw(public_key)
|
||||
assert len(raw) == 32
|
||||
# cross-check against openssl's own raw dump
|
||||
dumped = subprocess.run(
|
||||
["openssl", "pkey", "-pubin", "-in", str(public_key), "-outform", "DER"],
|
||||
capture_output=True, check=True,
|
||||
).stdout
|
||||
assert dumped.endswith(raw)
|
||||
|
||||
|
||||
def test_pem_rejects_non_ed25519(tmp_path):
|
||||
bogus = tmp_path / "bogus.pub"
|
||||
bogus.write_text("-----BEGIN PUBLIC KEY-----\nAAAA\n-----END PUBLIC KEY-----\n")
|
||||
import pytest
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
pem_public_key_to_raw(bogus)
|
||||
|
||||
|
||||
def test_dispatch_prefers_dogfood_binary_and_records_backend(tmp_path, monkeypatch):
|
||||
# a fake verifier that accepts everything: proves dispatch + backend label
|
||||
fake = tmp_path / "fake-verify"
|
||||
fake.write_text("#!/bin/sh\necho OK\nexit 0\n")
|
||||
fake.chmod(fake.stat().st_mode | stat.S_IEXEC)
|
||||
private_key = tmp_path / "k.key"
|
||||
public_key = tmp_path / "k.pub"
|
||||
generate_ed25519_keypair(private_key, public_key)
|
||||
payload = b"dogfood dispatch test"
|
||||
signature = sign_payload_ed25519(payload, private_key)
|
||||
monkeypatch.setenv("PACTA_DOGFOOD_VERIFIER", str(fake))
|
||||
ok, error, backend = verify_payload_ed25519_detailed(payload, signature, public_key)
|
||||
assert ok and backend == BACKEND_VERIFIED
|
||||
monkeypatch.setenv("PACTA_DOGFOOD_VERIFIER", str(tmp_path / "missing"))
|
||||
assert locate_verifier() is None
|
||||
ok, error, backend = verify_payload_ed25519_detailed(payload, signature, public_key)
|
||||
assert ok and backend == "openssl"
|
||||
|
||||
|
||||
def test_real_dogfood_binary_if_built(tmp_path):
|
||||
import pytest
|
||||
|
||||
binary = locate_verifier()
|
||||
if binary is None or "fake" in str(binary):
|
||||
pytest.skip("dogfood verifier not built on this host")
|
||||
private_key = tmp_path / "k.key"
|
||||
public_key = tmp_path / "k.pub"
|
||||
generate_ed25519_keypair(private_key, public_key)
|
||||
payload = b"the proven path verifies this"
|
||||
signature = sign_payload_ed25519(payload, private_key)
|
||||
ok, error, backend = verify_payload_ed25519_detailed(payload, signature, public_key)
|
||||
assert ok and backend == BACKEND_VERIFIED
|
||||
# flip one payload byte: the proven path must reject
|
||||
ok, error, backend = verify_payload_ed25519_detailed(payload + b"x", signature, public_key)
|
||||
assert not ok and backend == BACKEND_VERIFIED
|
||||
Loading…
Reference in a new issue