mirror of
https://github.com/saymrwulf/proof-aware-crypto-tooling-agent.git
synced 2026-09-06 20:20:36 +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/
|
.lake/
|
||||||
*.olean
|
*.olean
|
||||||
*.ilean
|
*.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 .config import RepoConfig
|
||||||
from .profiles import get_profile
|
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 .sthstore import check_sth_against_store, check_sth_freshness
|
||||||
from .transparency import load_receipt, verify_receipt
|
from .transparency import load_receipt, verify_receipt
|
||||||
from .yamlio import load_data
|
from .yamlio import load_data
|
||||||
|
|
@ -45,6 +45,7 @@ def validate_attestation(
|
||||||
sth_store_path: str | Path | None = None,
|
sth_store_path: str | Path | None = None,
|
||||||
consistency_proof_path: str | Path | None = None,
|
consistency_proof_path: str | Path | None = None,
|
||||||
max_sth_age_seconds: int | None = None,
|
max_sth_age_seconds: int | None = None,
|
||||||
|
require_verified_verifier: bool = False,
|
||||||
) -> AttestationResult:
|
) -> AttestationResult:
|
||||||
provider = raw.get("provider")
|
provider = raw.get("provider")
|
||||||
subject = raw.get("subject") or {}
|
subject = raw.get("subject") or {}
|
||||||
|
|
@ -82,12 +83,18 @@ def validate_attestation(
|
||||||
signature = raw.get("signature") or {}
|
signature = raw.get("signature") or {}
|
||||||
environment = raw.get("environment") or {}
|
environment = raw.get("environment") or {}
|
||||||
signature_status = signature.get("status", "not_checked")
|
signature_status = signature.get("status", "not_checked")
|
||||||
|
signature_backend = "none"
|
||||||
if public_key_path:
|
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:
|
if ok:
|
||||||
signature_status = "verified"
|
signature_status = "verified"
|
||||||
else:
|
else:
|
||||||
diagnostics.append(f"Attestation signature verification failed: {error}")
|
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":
|
elif signature_status == "signed":
|
||||||
diagnostics.append("Signed attestation requires --attestation-public-key.")
|
diagnostics.append("Signed attestation requires --attestation-public-key.")
|
||||||
elif signature_status == "not_implemented":
|
elif signature_status == "not_implemented":
|
||||||
|
|
@ -142,6 +149,7 @@ def validate_attestation(
|
||||||
"attestation_provider": provider,
|
"attestation_provider": provider,
|
||||||
"attestation_path": str(path) if path else None,
|
"attestation_path": str(path) if path else None,
|
||||||
"attestation_signature_status": signature_status,
|
"attestation_signature_status": signature_status,
|
||||||
|
"attestation_signature_backend": signature_backend,
|
||||||
"attestation_log_url": raw.get("log_url") or signature.get("log_url"),
|
"attestation_log_url": raw.get("log_url") or signature.get("log_url"),
|
||||||
"attestation_issued_at": raw.get("issued_at"),
|
"attestation_issued_at": raw.get("issued_at"),
|
||||||
"check_log_path": (raw.get("replay") or {}).get("check_log_path"),
|
"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("--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("--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("--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)
|
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 = 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("--claims", help="Existing claim card to act on.")
|
||||||
agent.add_argument("--config", help="Repository config used to generate a claim card.")
|
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("--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("--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("--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)
|
agent.set_defaults(func=cmd_agent)
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
|
|
@ -385,6 +395,11 @@ def cmd_receipt_verify(args: argparse.Namespace) -> int:
|
||||||
consistency_proof_path=args.consistency_proof,
|
consistency_proof_path=args.consistency_proof,
|
||||||
max_sth_age_seconds=args.max_sth_age_seconds,
|
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:
|
if accountability_diagnostics:
|
||||||
result.accepted = False
|
result.accepted = False
|
||||||
result.diagnostics.extend(accountability_diagnostics)
|
result.diagnostics.extend(accountability_diagnostics)
|
||||||
|
|
@ -402,6 +417,38 @@ def cmd_receipt_verify(args: argparse.Namespace) -> int:
|
||||||
return 0 if result.accepted else 1
|
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(
|
def _log_accountability_checks(
|
||||||
receipt: dict,
|
receipt: dict,
|
||||||
sth_store: str | None,
|
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),
|
sth_store_path=getattr(args, "sth_store", None),
|
||||||
consistency_proof_path=getattr(args, "consistency_proof", None),
|
consistency_proof_path=getattr(args, "consistency_proof", None),
|
||||||
max_sth_age_seconds=getattr(args, "max_sth_age_seconds", 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]:
|
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 {}
|
signature = attestation.get("signature") or {}
|
||||||
if signature.get("scheme") != "openssl-ed25519":
|
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")
|
encoded = signature.get("signature_base64")
|
||||||
if not encoded:
|
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")
|
expected_digest = signature.get("payload_digest_sha256")
|
||||||
actual_digest = payload_digest(attestation)
|
actual_digest = payload_digest(attestation)
|
||||||
if expected_digest and expected_digest != actual_digest:
|
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")
|
expected_fingerprint = signature.get("public_key_fingerprint_sha256")
|
||||||
if expected_fingerprint:
|
if expected_fingerprint:
|
||||||
actual_fingerprint = public_key_fingerprint(public_key_path)
|
actual_fingerprint = public_key_fingerprint(public_key_path)
|
||||||
if expected_fingerprint != actual_fingerprint:
|
if expected_fingerprint != actual_fingerprint:
|
||||||
return False, "Attestation public key fingerprint does not match signature metadata."
|
return False, "Attestation public key fingerprint does not match signature metadata.", "none"
|
||||||
return verify_payload_ed25519(canonical_attestation_payload(attestation), encoded, public_key_path)
|
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:
|
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]:
|
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:
|
try:
|
||||||
signature_bytes = base64.b64decode(signature_base64, validate=True)
|
signature_bytes = base64.b64decode(signature_base64, validate=True)
|
||||||
except ValueError as exc:
|
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:
|
with tempfile.TemporaryDirectory(prefix="pacta-verify-") as tmp:
|
||||||
payload_path = Path(tmp) / "payload.bin"
|
payload_path = Path(tmp) / "payload.bin"
|
||||||
signature_path = Path(tmp) / "payload.sig"
|
signature_path = Path(tmp) / "payload.sig"
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from .postquantum import detect_ml_dsa
|
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
|
from .yamlio import load_data
|
||||||
|
|
||||||
HASH_ALGORITHM = "RFC9162_SHA256"
|
HASH_ALGORITHM = "RFC9162_SHA256"
|
||||||
|
|
@ -206,8 +206,9 @@ def verify_signed_tree_head(
|
||||||
diagnostics.append("Signed tree head Ed25519 public-key fingerprint mismatch.")
|
diagnostics.append("Signed tree head Ed25519 public-key fingerprint mismatch.")
|
||||||
statuses["ed25519"] = "key_mismatch"
|
statuses["ed25519"] = "key_mismatch"
|
||||||
else:
|
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"] = "verified" if ok else "failed"
|
||||||
|
statuses["ed25519_backend"] = backend
|
||||||
if not ok:
|
if not ok:
|
||||||
diagnostics.append(f"Signed tree head Ed25519 verification failed: {error}")
|
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