The provider eats its own dogfood: root signatures via the merkleized library

The dogfood principle now runs in BOTH directions. Agents already
verified signatures through the proven dalek path; now the provider
SIGNS with it too, and proves to itself that the signing code is in its
own log before every signature:

- dogfood binary gains a `sign` mode (seed over stdin, never argv;
  ed25519_dalek::SigningKey from the same pinned merkleized workspace).
  Honesty ledger unchanged: the library's VERIFY path is
  certificate-covered; its signing path is declared trusted base - but
  it is the ATTESTED artifact, not an un-attested third implementation.
- sign_payload_ed25519_detailed: signing dispatch mirroring the verify
  dispatch; the backend that actually signed is recorded in every
  attestation signature block and STH.
- THE SELF-REFERENTIAL CHECK: before signing any tree head, the
  provider runs the SAME Merkle inclusion verification an agent runs -
  against the very tree it is about to sign - for the newest leaf
  attesting the signing library itself, and embeds the result in the
  signature block:
    signing_provenance:
      signing_backend: verified-dalek-serial
      signing_library_component: dalek-ed25519-verified
      signing_library_source_commit: aa0f6ab...
      self_inclusion: verified
      signing_library_leaf_index: 4
      signing_library_certificates_proven: 16/16
  A root signature that names the leaf vouching for the code that
  produced it. First-append chicken-and-egg is handled honestly
  (self_inclusion: library_not_in_log).
- Evidence refreshed: all four receipts re-issued under dogfood-signed
  STHs; the full agent verify loop re-run green.

50/50 tests (new signing roundtrip test, skip-safe where unbuilt).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
mrwulf 2026-07-06 15:21:17 +02:00
parent 71c670527c
commit b8ffbafa7f
11 changed files with 242 additions and 20 deletions

View file

@ -13,7 +13,8 @@
//! Usage: pacta-verified-verify <pubkey-hex-32B> <sig-hex-64B> <payload-file> //! Usage: pacta-verified-verify <pubkey-hex-32B> <sig-hex-64B> <payload-file>
//! Exit 0 = signature valid; exit 1 = invalid; exit 2 = usage/format error. //! Exit 0 = signature valid; exit 1 = invalid; exit 2 = usage/format error.
use ed25519_dalek::{Signature, Verifier, VerifyingKey}; use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
use std::io::Read;
use std::process::ExitCode; use std::process::ExitCode;
fn hex_decode(s: &str) -> Result<Vec<u8>, String> { fn hex_decode(s: &str) -> Result<Vec<u8>, String> {
@ -25,10 +26,53 @@ fn hex_decode(s: &str) -> Result<Vec<u8>, String> {
.collect() .collect()
} }
fn sign_mode(payload_path: &str) -> ExitCode {
// Signing mode: the 32-byte seed arrives on STDIN as hex (never argv -
// argv is world-readable in /proc). The signature is written to stdout
// as hex. NOTE the honesty ledger: this library's SOURCE is merkleized
// and attested in the transparency log, and its VERIFY path is
// certificate-covered; the signing path itself is NOT proven - it is
// declared trusted base, chosen because it is at least the attested
// artifact rather than an un-attested third implementation.
let mut seed_hex = String::new();
if std::io::stdin().read_to_string(&mut seed_hex).is_err() {
eprintln!("error: cannot read seed from stdin");
return ExitCode::from(2);
}
let seed = match hex_decode(seed_hex.trim()) {
Ok(b) if b.len() == 32 => b,
_ => {
eprintln!("error: seed must be 32 bytes of hex on stdin");
return ExitCode::from(2);
}
};
let payload = match std::fs::read(payload_path) {
Ok(p) => p,
Err(e) => {
eprintln!("error: cannot read payload: {e}");
return ExitCode::from(2);
}
};
let mut seed_array = [0u8; 32];
seed_array.copy_from_slice(&seed);
let signing_key = SigningKey::from_bytes(&seed_array);
let signature = signing_key.sign(&payload);
println!("{}", hex_encode(&signature.to_bytes()));
ExitCode::SUCCESS
}
fn hex_encode(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{b:02x}")).collect()
}
fn main() -> ExitCode { fn main() -> ExitCode {
let args: Vec<String> = std::env::args().collect(); let args: Vec<String> = std::env::args().collect();
if args.len() == 3 && args[1] == "sign" {
return sign_mode(&args[2]);
}
if args.len() != 4 { if args.len() != 4 {
eprintln!("usage: pacta-verified-verify <pubkey-hex> <sig-hex> <payload-file>"); eprintln!("usage: pacta-verified-verify <pubkey-hex> <sig-hex> <payload-file>");
eprintln!(" pacta-verified-verify sign <payload-file> (32-byte seed hex on stdin)");
return ExitCode::from(2); return ExitCode::from(2);
} }
let pk_bytes = match hex_decode(&args[1]) { let pk_bytes = match hex_decode(&args[1]) {

View file

@ -21,16 +21,24 @@ sth:
type: pacta.transparency.signed_tree_head.v1 type: pacta.transparency.signed_tree_head.v1
log_id: 205e4c389cb143e08f0d2d58bdc8e425e47e3cbe7f2108cc58bbe835d2cc41d7 log_id: 205e4c389cb143e08f0d2d58bdc8e425e47e3cbe7f2108cc58bbe835d2cc41d7
tree_size: 8 tree_size: 8
timestamp: '2026-07-06T12:53:08Z' timestamp: '2026-07-06T13:20:39Z'
root_hash: 9a15b9a1379edc07ae43d3fc61b52dc4446b56770bff6538e88ed98746ac2283 root_hash: 9a15b9a1379edc07ae43d3fc61b52dc4446b56770bff6538e88ed98746ac2283
hash_algorithm: RFC9162_SHA256 hash_algorithm: RFC9162_SHA256
signatures: signatures:
ed25519: ed25519:
scheme: openssl-ed25519 scheme: openssl-ed25519
signing_backend: verified-dalek-serial
status: signed status: signed
payload_digest_sha256: d783b70d6124bd2206bed6671ce83e9b292dec41761527326695d5e23b12eb8b payload_digest_sha256: d915e3fd22b755768bc91e088259f42e6011e8fb98910aaa6ae4b93ce793255b
signature_base64: 0Si0Mb46p5xmPu8EJp9wm/Cle3xClHYYRLBFksjnygVci8U+RE4uBNydU0dSQT7FrnJB+xdwCn5NBRnUnjaECg== signature_base64: 30Dz9i50AsM9L44fQCGQJcnk84fzpIUuJznbJiumQuREZ2QdAfsY9EpnXiC+uHcTnN8yJ928WIeBcarsKebXBw==
public_key_fingerprint_sha256: 874c8a008a607021528b2493fa1caf059f9d5c123d29193dfabc09a6d1e7a56a public_key_fingerprint_sha256: 874c8a008a607021528b2493fa1caf059f9d5c123d29193dfabc09a6d1e7a56a
signing_provenance:
signing_backend: verified-dalek-serial
signing_library_component: dalek-ed25519-verified
signing_library_source_commit: aa0f6abc327ba2a54a534b21608ca8996cf73682
self_inclusion: verified
signing_library_leaf_index: 4
signing_library_certificates_proven: 16/16
ml_dsa: ml_dsa:
scheme: ML-DSA-65 scheme: ML-DSA-65
standard: FIPS 204 standard: FIPS 204

View file

@ -21,16 +21,24 @@ sth:
type: pacta.transparency.signed_tree_head.v1 type: pacta.transparency.signed_tree_head.v1
log_id: 205e4c389cb143e08f0d2d58bdc8e425e47e3cbe7f2108cc58bbe835d2cc41d7 log_id: 205e4c389cb143e08f0d2d58bdc8e425e47e3cbe7f2108cc58bbe835d2cc41d7
tree_size: 8 tree_size: 8
timestamp: '2026-07-06T12:53:09Z' timestamp: '2026-07-06T13:20:40Z'
root_hash: 9a15b9a1379edc07ae43d3fc61b52dc4446b56770bff6538e88ed98746ac2283 root_hash: 9a15b9a1379edc07ae43d3fc61b52dc4446b56770bff6538e88ed98746ac2283
hash_algorithm: RFC9162_SHA256 hash_algorithm: RFC9162_SHA256
signatures: signatures:
ed25519: ed25519:
scheme: openssl-ed25519 scheme: openssl-ed25519
signing_backend: verified-dalek-serial
status: signed status: signed
payload_digest_sha256: d5d25db2c90f0246cc10d10f912085b81c60d2a324354b5abecfdbeb8bd17ee2 payload_digest_sha256: 72d9abbc5720f1c6f03ad6f6363a6c5587a2bfd46f67248282cdcfa8eeb8e1dd
signature_base64: sI4rWnWMrX0zDaN8jofBwSuExKg1e0cmZV3OPaulbXDV97yz95aSo0Gnrfd1En2vyyV1qoEXdTAY9NDgvA8xBA== signature_base64: Yz9T8akHXeiMJBKLgpoKTInfdV5WeuZgYzgWYEMkdaYS9NVKLpDjp1AcapJxUZ9Ft+2O2+n010yN7flODKqBBw==
public_key_fingerprint_sha256: 874c8a008a607021528b2493fa1caf059f9d5c123d29193dfabc09a6d1e7a56a public_key_fingerprint_sha256: 874c8a008a607021528b2493fa1caf059f9d5c123d29193dfabc09a6d1e7a56a
signing_provenance:
signing_backend: verified-dalek-serial
signing_library_component: dalek-ed25519-verified
signing_library_source_commit: aa0f6abc327ba2a54a534b21608ca8996cf73682
self_inclusion: verified
signing_library_leaf_index: 4
signing_library_certificates_proven: 16/16
ml_dsa: ml_dsa:
scheme: ML-DSA-65 scheme: ML-DSA-65
standard: FIPS 204 standard: FIPS 204

View file

@ -21,16 +21,24 @@ sth:
type: pacta.transparency.signed_tree_head.v1 type: pacta.transparency.signed_tree_head.v1
log_id: 205e4c389cb143e08f0d2d58bdc8e425e47e3cbe7f2108cc58bbe835d2cc41d7 log_id: 205e4c389cb143e08f0d2d58bdc8e425e47e3cbe7f2108cc58bbe835d2cc41d7
tree_size: 8 tree_size: 8
timestamp: '2026-07-06T12:53:08Z' timestamp: '2026-07-06T13:20:39Z'
root_hash: 9a15b9a1379edc07ae43d3fc61b52dc4446b56770bff6538e88ed98746ac2283 root_hash: 9a15b9a1379edc07ae43d3fc61b52dc4446b56770bff6538e88ed98746ac2283
hash_algorithm: RFC9162_SHA256 hash_algorithm: RFC9162_SHA256
signatures: signatures:
ed25519: ed25519:
scheme: openssl-ed25519 scheme: openssl-ed25519
signing_backend: verified-dalek-serial
status: signed status: signed
payload_digest_sha256: d783b70d6124bd2206bed6671ce83e9b292dec41761527326695d5e23b12eb8b payload_digest_sha256: d915e3fd22b755768bc91e088259f42e6011e8fb98910aaa6ae4b93ce793255b
signature_base64: 0Si0Mb46p5xmPu8EJp9wm/Cle3xClHYYRLBFksjnygVci8U+RE4uBNydU0dSQT7FrnJB+xdwCn5NBRnUnjaECg== signature_base64: 30Dz9i50AsM9L44fQCGQJcnk84fzpIUuJznbJiumQuREZ2QdAfsY9EpnXiC+uHcTnN8yJ928WIeBcarsKebXBw==
public_key_fingerprint_sha256: 874c8a008a607021528b2493fa1caf059f9d5c123d29193dfabc09a6d1e7a56a public_key_fingerprint_sha256: 874c8a008a607021528b2493fa1caf059f9d5c123d29193dfabc09a6d1e7a56a
signing_provenance:
signing_backend: verified-dalek-serial
signing_library_component: dalek-ed25519-verified
signing_library_source_commit: aa0f6abc327ba2a54a534b21608ca8996cf73682
self_inclusion: verified
signing_library_leaf_index: 4
signing_library_certificates_proven: 16/16
ml_dsa: ml_dsa:
scheme: ML-DSA-65 scheme: ML-DSA-65
standard: FIPS 204 standard: FIPS 204

View file

@ -2,16 +2,24 @@ schema_version: 1
type: pacta.transparency.signed_tree_head.v1 type: pacta.transparency.signed_tree_head.v1
log_id: 205e4c389cb143e08f0d2d58bdc8e425e47e3cbe7f2108cc58bbe835d2cc41d7 log_id: 205e4c389cb143e08f0d2d58bdc8e425e47e3cbe7f2108cc58bbe835d2cc41d7
tree_size: 8 tree_size: 8
timestamp: '2026-07-06T12:53:09Z' timestamp: '2026-07-06T13:20:40Z'
root_hash: 9a15b9a1379edc07ae43d3fc61b52dc4446b56770bff6538e88ed98746ac2283 root_hash: 9a15b9a1379edc07ae43d3fc61b52dc4446b56770bff6538e88ed98746ac2283
hash_algorithm: RFC9162_SHA256 hash_algorithm: RFC9162_SHA256
signatures: signatures:
ed25519: ed25519:
scheme: openssl-ed25519 scheme: openssl-ed25519
signing_backend: verified-dalek-serial
status: signed status: signed
payload_digest_sha256: d5d25db2c90f0246cc10d10f912085b81c60d2a324354b5abecfdbeb8bd17ee2 payload_digest_sha256: 72d9abbc5720f1c6f03ad6f6363a6c5587a2bfd46f67248282cdcfa8eeb8e1dd
signature_base64: sI4rWnWMrX0zDaN8jofBwSuExKg1e0cmZV3OPaulbXDV97yz95aSo0Gnrfd1En2vyyV1qoEXdTAY9NDgvA8xBA== signature_base64: Yz9T8akHXeiMJBKLgpoKTInfdV5WeuZgYzgWYEMkdaYS9NVKLpDjp1AcapJxUZ9Ft+2O2+n010yN7flODKqBBw==
public_key_fingerprint_sha256: 874c8a008a607021528b2493fa1caf059f9d5c123d29193dfabc09a6d1e7a56a public_key_fingerprint_sha256: 874c8a008a607021528b2493fa1caf059f9d5c123d29193dfabc09a6d1e7a56a
signing_provenance:
signing_backend: verified-dalek-serial
signing_library_component: dalek-ed25519-verified
signing_library_source_commit: aa0f6abc327ba2a54a534b21608ca8996cf73682
self_inclusion: verified
signing_library_leaf_index: 4
signing_library_certificates_proven: 16/16
ml_dsa: ml_dsa:
scheme: ML-DSA-65 scheme: ML-DSA-65
standard: FIPS 204 standard: FIPS 204

View file

@ -21,16 +21,24 @@ sth:
type: pacta.transparency.signed_tree_head.v1 type: pacta.transparency.signed_tree_head.v1
log_id: 205e4c389cb143e08f0d2d58bdc8e425e47e3cbe7f2108cc58bbe835d2cc41d7 log_id: 205e4c389cb143e08f0d2d58bdc8e425e47e3cbe7f2108cc58bbe835d2cc41d7
tree_size: 8 tree_size: 8
timestamp: '2026-07-06T12:53:09Z' timestamp: '2026-07-06T13:20:39Z'
root_hash: 9a15b9a1379edc07ae43d3fc61b52dc4446b56770bff6538e88ed98746ac2283 root_hash: 9a15b9a1379edc07ae43d3fc61b52dc4446b56770bff6538e88ed98746ac2283
hash_algorithm: RFC9162_SHA256 hash_algorithm: RFC9162_SHA256
signatures: signatures:
ed25519: ed25519:
scheme: openssl-ed25519 scheme: openssl-ed25519
signing_backend: verified-dalek-serial
status: signed status: signed
payload_digest_sha256: d5d25db2c90f0246cc10d10f912085b81c60d2a324354b5abecfdbeb8bd17ee2 payload_digest_sha256: d915e3fd22b755768bc91e088259f42e6011e8fb98910aaa6ae4b93ce793255b
signature_base64: sI4rWnWMrX0zDaN8jofBwSuExKg1e0cmZV3OPaulbXDV97yz95aSo0Gnrfd1En2vyyV1qoEXdTAY9NDgvA8xBA== signature_base64: 30Dz9i50AsM9L44fQCGQJcnk84fzpIUuJznbJiumQuREZ2QdAfsY9EpnXiC+uHcTnN8yJ928WIeBcarsKebXBw==
public_key_fingerprint_sha256: 874c8a008a607021528b2493fa1caf059f9d5c123d29193dfabc09a6d1e7a56a public_key_fingerprint_sha256: 874c8a008a607021528b2493fa1caf059f9d5c123d29193dfabc09a6d1e7a56a
signing_provenance:
signing_backend: verified-dalek-serial
signing_library_component: dalek-ed25519-verified
signing_library_source_commit: aa0f6abc327ba2a54a534b21608ca8996cf73682
self_inclusion: verified
signing_library_leaf_index: 4
signing_library_certificates_proven: 16/16
ml_dsa: ml_dsa:
scheme: ML-DSA-65 scheme: ML-DSA-65
standard: FIPS 204 standard: FIPS 204

View file

@ -7,6 +7,7 @@ from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
from pacta.dogfood import BACKEND_OPENSSL, BACKEND_VERIFIED, load_provenance, locate_verifier
from pacta.signing import canonical_json from pacta.signing import canonical_json
from pacta.transparency import ( from pacta.transparency import (
HASH_ALGORITHM, HASH_ALGORITHM,
@ -19,6 +20,7 @@ from pacta.transparency import (
make_signed_tree_head, make_signed_tree_head,
merkle_root, merkle_root,
proof_to_hex, proof_to_hex,
verify_inclusion,
) )
from pacta.yamlio import dump_data, load_data from pacta.yamlio import dump_data, load_data
@ -95,7 +97,8 @@ class TransparencyLog:
timestamp: str | None = None, timestamp: str | None = None,
) -> dict[str, Any]: ) -> dict[str, Any]:
metadata = self.metadata() metadata = self.metadata()
leaves = [entry.leaf_bytes() for entry in self.entries()] entries = self.entries()
leaves = [entry.leaf_bytes() for entry in entries]
sth = make_signed_tree_head( sth = make_signed_tree_head(
metadata["log_id"], metadata["log_id"],
len(leaves), len(leaves),
@ -103,6 +106,7 @@ class TransparencyLog:
timestamp or _now(), timestamp or _now(),
private_key_path, private_key_path,
public_key_path, public_key_path,
signing_provenance=self.signing_provenance(entries),
) )
dump_data(sth, self.sth_path) dump_data(sth, self.sth_path)
return sth return sth
@ -144,6 +148,7 @@ class TransparencyLog:
_now(), _now(),
private_key_path, private_key_path,
public_key_path, public_key_path,
signing_provenance=self.signing_provenance(entries),
) )
dump_data(sth, self.sth_path) dump_data(sth, self.sth_path)
consistency = [] consistency = []
@ -174,6 +179,45 @@ class TransparencyLog:
return receipt return receipt
def signing_provenance(self, entries: list[LogEntry], signing_library_component: str = "dalek-ed25519-verified") -> dict[str, Any]:
"""THE PROVIDER EATS ITS OWN DOGFOOD: before signing a tree head,
verify that the attestation of the SIGNING LIBRARY ITSELF (the
merkleized dalek source whose build produced the signing binary) is
included in the very tree about to be signed - by running the same
Merkle inclusion verification an agent runs. The result is recorded
inside the signature block: a root signature that names the leaf
vouching for the code that produced it."""
binary = locate_verifier()
backend = BACKEND_VERIFIED if binary is not None else BACKEND_OPENSSL
provenance: dict[str, Any] = {
"signing_backend": backend,
"signing_library_component": signing_library_component,
}
if binary is not None:
build = load_provenance(binary)
if build.get("source_commit"):
provenance["signing_library_source_commit"] = build["source_commit"]
matches = [
entry
for entry in entries
if ((entry.leaf.get("attestation") or {}).get("subject") or {}).get("component") == signing_library_component
]
if not matches:
provenance["self_inclusion"] = "library_not_in_log"
return provenance
newest = matches[-1]
leaves = [entry.leaf_bytes() for entry in entries]
proof = inclusion_proof(leaves, newest.index)
verified = verify_inclusion(
newest.leaf_bytes(), newest.index, len(leaves), proof, merkle_root(leaves)
)
provenance["self_inclusion"] = "verified" if verified else "FAILED"
provenance["signing_library_leaf_index"] = newest.index
attested = (newest.leaf.get("attestation") or {}).get("certificates") or []
clean = sum(1 for c in attested if c.get("status") == "proven" and c.get("axiom_status") == "clean")
provenance["signing_library_certificates_proven"] = f"{clean}/{len(attested)}"
return provenance
def consistency_from(self, old_tree_size: int) -> dict[str, Any]: def consistency_from(self, old_tree_size: int) -> dict[str, Any]:
"""Consistency proof from an arbitrary earlier tree size - what a """Consistency proof from an arbitrary earlier tree size - what a
pinning agent requests when its pin is older than the receipt's pinning agent requests when its pin is older than the receipt's

View file

@ -20,6 +20,9 @@ BACKEND_OPENSSL = "openssl"
# the raw 32-byte key. Parsing by prefix is exact for this OID, not a # the raw 32-byte key. Parsing by prefix is exact for this OID, not a
# heuristic. # heuristic.
_ED25519_SPKI_PREFIX = bytes.fromhex("302a300506032b6570032100") _ED25519_SPKI_PREFIX = bytes.fromhex("302a300506032b6570032100")
# Ed25519 PKCS#8 private key (RFC 8410): fixed 16-byte DER prefix then the
# 32-byte seed wrapped as an OCTET STRING.
_ED25519_PKCS8_PREFIX = bytes.fromhex("302e020100300506032b657004220420")
@dataclass(slots=True) @dataclass(slots=True)
@ -50,6 +53,49 @@ def pem_public_key_to_raw(public_key_path: str | Path) -> bytes:
return der[len(_ED25519_SPKI_PREFIX):] return der[len(_ED25519_SPKI_PREFIX):]
def pem_private_key_to_seed(private_key_path: str | Path) -> bytes:
"""Extract the raw 32-byte Ed25519 seed from an OpenSSL PKCS#8 PEM."""
text = Path(private_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 private key: {private_key_path}: {exc}") from exc
if not der.startswith(_ED25519_PKCS8_PREFIX) or len(der) != len(_ED25519_PKCS8_PREFIX) + 32:
raise ValueError(f"Not an Ed25519 PKCS#8 key: {private_key_path} ({len(der)} DER bytes)")
return der[len(_ED25519_PKCS8_PREFIX):]
def sign_payload_dogfood(
payload: bytes,
private_key_path: str | Path,
binary: str | Path,
timeout: int = 30,
) -> bytes:
"""Sign via the dogfood binary (the merkleized, attested library). The
seed travels over STDIN, never argv. Honesty: the library's verify path
is certificate-covered; its signing path is declared trusted base - but
it is the ATTESTED artifact, not an un-attested third implementation."""
seed = pem_private_key_to_seed(private_key_path)
with tempfile.TemporaryDirectory(prefix="pacta-dogfood-sign-") as tmp:
payload_path = Path(tmp) / "payload.bin"
payload_path.write_bytes(payload)
completed = subprocess.run(
[str(binary), "sign", str(payload_path)],
input=seed.hex(),
capture_output=True,
text=True,
timeout=timeout,
)
if completed.returncode != 0:
raise RuntimeError((completed.stderr or "dogfood signing failed").strip())
return bytes.fromhex(completed.stdout.strip())
def default_binary_path(state_dir: str | Path | None = None) -> Path: def default_binary_path(state_dir: str | Path | None = None) -> Path:
return Path(state_dir or DEFAULT_STATE_DIR) / "pacta-verified-verify" return Path(state_dir or DEFAULT_STATE_DIR) / "pacta-verified-verify"

View file

@ -44,10 +44,11 @@ def generate_ed25519_keypair(private_key_path: str | Path, public_key_path: str
def sign_attestation(attestation: dict[str, Any], private_key_path: str | Path, public_key_path: str | Path | None = None) -> dict[str, Any]: def sign_attestation(attestation: dict[str, Any], private_key_path: str | Path, public_key_path: str | Path | None = None) -> dict[str, Any]:
payload = canonical_attestation_payload(attestation) payload = canonical_attestation_payload(attestation)
signature_base64 = sign_payload_ed25519(payload, private_key_path) signature_base64, signing_backend = sign_payload_ed25519_detailed(payload, private_key_path)
signed = dict(attestation) signed = dict(attestation)
signed["signature"] = { signed["signature"] = {
"scheme": "openssl-ed25519", "scheme": "openssl-ed25519",
"signing_backend": signing_backend,
"status": "signed", "status": "signed",
"payload_digest_sha256": hashlib.sha256(payload).hexdigest(), "payload_digest_sha256": hashlib.sha256(payload).hexdigest(),
"signature_base64": signature_base64, "signature_base64": signature_base64,
@ -82,6 +83,25 @@ def verify_attestation_signature_detailed(attestation: dict[str, Any], public_ke
def sign_payload_ed25519(payload: bytes, private_key_path: str | Path) -> str: def sign_payload_ed25519(payload: bytes, private_key_path: str | Path) -> str:
signature, _backend = sign_payload_ed25519_detailed(payload, private_key_path)
return signature
def sign_payload_ed25519_detailed(payload: bytes, private_key_path: str | Path) -> tuple[str, str]:
"""Sign, preferring the dogfood binary (the merkleized, attested dalek
library) and falling back to OpenSSL. Returns (base64 signature, the
backend that actually signed) - the backend is recorded next to every
signature so the provenance is never silent."""
from .dogfood import BACKEND_OPENSSL, BACKEND_VERIFIED, locate_verifier, sign_payload_dogfood
binary = locate_verifier()
if binary is not None:
signature_bytes = sign_payload_dogfood(payload, private_key_path, binary)
return base64.b64encode(signature_bytes).decode("ascii"), BACKEND_VERIFIED
return _sign_payload_openssl(payload, private_key_path), BACKEND_OPENSSL
def _sign_payload_openssl(payload: bytes, private_key_path: str | Path) -> str:
openssl = _openssl() openssl = _openssl()
with tempfile.TemporaryDirectory(prefix="pacta-sign-") as tmp: with tempfile.TemporaryDirectory(prefix="pacta-sign-") as tmp:
payload_path = Path(tmp) / "payload.bin" payload_path = Path(tmp) / "payload.bin"

View file

@ -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_detailed from .signing import canonical_json, public_key_fingerprint, sign_payload_ed25519_detailed, verify_payload_ed25519_detailed
from .yamlio import load_data from .yamlio import load_data
HASH_ALGORITHM = "RFC9162_SHA256" HASH_ALGORITHM = "RFC9162_SHA256"
@ -152,6 +152,7 @@ def make_signed_tree_head(
timestamp: str, timestamp: str,
private_key_path: str | Path, private_key_path: str | Path,
public_key_path: str | Path, public_key_path: str | Path,
signing_provenance: dict[str, Any] | None = None,
) -> dict[str, Any]: ) -> dict[str, Any]:
sth: dict[str, Any] = { sth: dict[str, Any] = {
"schema_version": 1, "schema_version": 1,
@ -163,13 +164,16 @@ def make_signed_tree_head(
"hash_algorithm": HASH_ALGORITHM, "hash_algorithm": HASH_ALGORITHM,
} }
payload = signed_tree_head_payload(sth) payload = signed_tree_head_payload(sth)
signature_base64, signing_backend = sign_payload_ed25519_detailed(payload, private_key_path)
sth["signatures"] = { sth["signatures"] = {
"ed25519": { "ed25519": {
"scheme": "openssl-ed25519", "scheme": "openssl-ed25519",
"signing_backend": signing_backend,
"status": "signed", "status": "signed",
"payload_digest_sha256": hashlib.sha256(payload).hexdigest(), "payload_digest_sha256": hashlib.sha256(payload).hexdigest(),
"signature_base64": sign_payload_ed25519(payload, private_key_path), "signature_base64": signature_base64,
"public_key_fingerprint_sha256": public_key_fingerprint(public_key_path), "public_key_fingerprint_sha256": public_key_fingerprint(public_key_path),
**({"signing_provenance": signing_provenance} if signing_provenance else {}),
}, },
"ml_dsa": detect_ml_dsa().to_signature_slot(), "ml_dsa": detect_ml_dsa().to_signature_slot(),
} }

View file

@ -64,3 +64,27 @@ def test_real_dogfood_binary_if_built(tmp_path):
# flip one payload byte: the proven path must reject # flip one payload byte: the proven path must reject
ok, error, backend = verify_payload_ed25519_detailed(payload + b"x", signature, public_key) ok, error, backend = verify_payload_ed25519_detailed(payload + b"x", signature, public_key)
assert not ok and backend == BACKEND_VERIFIED assert not ok and backend == BACKEND_VERIFIED
def test_dogfood_signing_roundtrip_if_built(tmp_path):
import pytest
from pacta.dogfood import locate_verifier, pem_private_key_to_seed, sign_payload_dogfood
from pacta.signing import verify_payload_ed25519_detailed
import base64
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"
from pacta.signing import generate_ed25519_keypair
generate_ed25519_keypair(private_key, public_key)
assert len(pem_private_key_to_seed(private_key)) == 32
payload = b"signed by the merkleized library"
signature = sign_payload_dogfood(payload, private_key, binary)
ok, error, backend = verify_payload_ed25519_detailed(
payload, base64.b64encode(signature).decode(), public_key
)
assert ok and backend == "verified-dalek-serial"