mirror of
https://github.com/saymrwulf/proof-aware-crypto-tooling-agent.git
synced 2026-09-03 19:53:43 +00:00
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:
parent
71c670527c
commit
b8ffbafa7f
11 changed files with 242 additions and 20 deletions
|
|
@ -13,7 +13,8 @@
|
|||
//! 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 ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
|
||||
use std::io::Read;
|
||||
use std::process::ExitCode;
|
||||
|
||||
fn hex_decode(s: &str) -> Result<Vec<u8>, String> {
|
||||
|
|
@ -25,10 +26,53 @@ fn hex_decode(s: &str) -> Result<Vec<u8>, String> {
|
|||
.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 {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
if args.len() == 3 && args[1] == "sign" {
|
||||
return sign_mode(&args[2]);
|
||||
}
|
||||
if args.len() != 4 {
|
||||
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);
|
||||
}
|
||||
let pk_bytes = match hex_decode(&args[1]) {
|
||||
|
|
|
|||
|
|
@ -21,16 +21,24 @@ sth:
|
|||
type: pacta.transparency.signed_tree_head.v1
|
||||
log_id: 205e4c389cb143e08f0d2d58bdc8e425e47e3cbe7f2108cc58bbe835d2cc41d7
|
||||
tree_size: 8
|
||||
timestamp: '2026-07-06T12:53:08Z'
|
||||
timestamp: '2026-07-06T13:20:39Z'
|
||||
root_hash: 9a15b9a1379edc07ae43d3fc61b52dc4446b56770bff6538e88ed98746ac2283
|
||||
hash_algorithm: RFC9162_SHA256
|
||||
signatures:
|
||||
ed25519:
|
||||
scheme: openssl-ed25519
|
||||
signing_backend: verified-dalek-serial
|
||||
status: signed
|
||||
payload_digest_sha256: d783b70d6124bd2206bed6671ce83e9b292dec41761527326695d5e23b12eb8b
|
||||
signature_base64: 0Si0Mb46p5xmPu8EJp9wm/Cle3xClHYYRLBFksjnygVci8U+RE4uBNydU0dSQT7FrnJB+xdwCn5NBRnUnjaECg==
|
||||
payload_digest_sha256: d915e3fd22b755768bc91e088259f42e6011e8fb98910aaa6ae4b93ce793255b
|
||||
signature_base64: 30Dz9i50AsM9L44fQCGQJcnk84fzpIUuJznbJiumQuREZ2QdAfsY9EpnXiC+uHcTnN8yJ928WIeBcarsKebXBw==
|
||||
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:
|
||||
scheme: ML-DSA-65
|
||||
standard: FIPS 204
|
||||
|
|
|
|||
|
|
@ -21,16 +21,24 @@ sth:
|
|||
type: pacta.transparency.signed_tree_head.v1
|
||||
log_id: 205e4c389cb143e08f0d2d58bdc8e425e47e3cbe7f2108cc58bbe835d2cc41d7
|
||||
tree_size: 8
|
||||
timestamp: '2026-07-06T12:53:09Z'
|
||||
timestamp: '2026-07-06T13:20:40Z'
|
||||
root_hash: 9a15b9a1379edc07ae43d3fc61b52dc4446b56770bff6538e88ed98746ac2283
|
||||
hash_algorithm: RFC9162_SHA256
|
||||
signatures:
|
||||
ed25519:
|
||||
scheme: openssl-ed25519
|
||||
signing_backend: verified-dalek-serial
|
||||
status: signed
|
||||
payload_digest_sha256: d5d25db2c90f0246cc10d10f912085b81c60d2a324354b5abecfdbeb8bd17ee2
|
||||
signature_base64: sI4rWnWMrX0zDaN8jofBwSuExKg1e0cmZV3OPaulbXDV97yz95aSo0Gnrfd1En2vyyV1qoEXdTAY9NDgvA8xBA==
|
||||
payload_digest_sha256: 72d9abbc5720f1c6f03ad6f6363a6c5587a2bfd46f67248282cdcfa8eeb8e1dd
|
||||
signature_base64: Yz9T8akHXeiMJBKLgpoKTInfdV5WeuZgYzgWYEMkdaYS9NVKLpDjp1AcapJxUZ9Ft+2O2+n010yN7flODKqBBw==
|
||||
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:
|
||||
scheme: ML-DSA-65
|
||||
standard: FIPS 204
|
||||
|
|
|
|||
|
|
@ -21,16 +21,24 @@ sth:
|
|||
type: pacta.transparency.signed_tree_head.v1
|
||||
log_id: 205e4c389cb143e08f0d2d58bdc8e425e47e3cbe7f2108cc58bbe835d2cc41d7
|
||||
tree_size: 8
|
||||
timestamp: '2026-07-06T12:53:08Z'
|
||||
timestamp: '2026-07-06T13:20:39Z'
|
||||
root_hash: 9a15b9a1379edc07ae43d3fc61b52dc4446b56770bff6538e88ed98746ac2283
|
||||
hash_algorithm: RFC9162_SHA256
|
||||
signatures:
|
||||
ed25519:
|
||||
scheme: openssl-ed25519
|
||||
signing_backend: verified-dalek-serial
|
||||
status: signed
|
||||
payload_digest_sha256: d783b70d6124bd2206bed6671ce83e9b292dec41761527326695d5e23b12eb8b
|
||||
signature_base64: 0Si0Mb46p5xmPu8EJp9wm/Cle3xClHYYRLBFksjnygVci8U+RE4uBNydU0dSQT7FrnJB+xdwCn5NBRnUnjaECg==
|
||||
payload_digest_sha256: d915e3fd22b755768bc91e088259f42e6011e8fb98910aaa6ae4b93ce793255b
|
||||
signature_base64: 30Dz9i50AsM9L44fQCGQJcnk84fzpIUuJznbJiumQuREZ2QdAfsY9EpnXiC+uHcTnN8yJ928WIeBcarsKebXBw==
|
||||
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:
|
||||
scheme: ML-DSA-65
|
||||
standard: FIPS 204
|
||||
|
|
|
|||
|
|
@ -2,16 +2,24 @@ schema_version: 1
|
|||
type: pacta.transparency.signed_tree_head.v1
|
||||
log_id: 205e4c389cb143e08f0d2d58bdc8e425e47e3cbe7f2108cc58bbe835d2cc41d7
|
||||
tree_size: 8
|
||||
timestamp: '2026-07-06T12:53:09Z'
|
||||
timestamp: '2026-07-06T13:20:40Z'
|
||||
root_hash: 9a15b9a1379edc07ae43d3fc61b52dc4446b56770bff6538e88ed98746ac2283
|
||||
hash_algorithm: RFC9162_SHA256
|
||||
signatures:
|
||||
ed25519:
|
||||
scheme: openssl-ed25519
|
||||
signing_backend: verified-dalek-serial
|
||||
status: signed
|
||||
payload_digest_sha256: d5d25db2c90f0246cc10d10f912085b81c60d2a324354b5abecfdbeb8bd17ee2
|
||||
signature_base64: sI4rWnWMrX0zDaN8jofBwSuExKg1e0cmZV3OPaulbXDV97yz95aSo0Gnrfd1En2vyyV1qoEXdTAY9NDgvA8xBA==
|
||||
payload_digest_sha256: 72d9abbc5720f1c6f03ad6f6363a6c5587a2bfd46f67248282cdcfa8eeb8e1dd
|
||||
signature_base64: Yz9T8akHXeiMJBKLgpoKTInfdV5WeuZgYzgWYEMkdaYS9NVKLpDjp1AcapJxUZ9Ft+2O2+n010yN7flODKqBBw==
|
||||
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:
|
||||
scheme: ML-DSA-65
|
||||
standard: FIPS 204
|
||||
|
|
|
|||
|
|
@ -21,16 +21,24 @@ sth:
|
|||
type: pacta.transparency.signed_tree_head.v1
|
||||
log_id: 205e4c389cb143e08f0d2d58bdc8e425e47e3cbe7f2108cc58bbe835d2cc41d7
|
||||
tree_size: 8
|
||||
timestamp: '2026-07-06T12:53:09Z'
|
||||
timestamp: '2026-07-06T13:20:39Z'
|
||||
root_hash: 9a15b9a1379edc07ae43d3fc61b52dc4446b56770bff6538e88ed98746ac2283
|
||||
hash_algorithm: RFC9162_SHA256
|
||||
signatures:
|
||||
ed25519:
|
||||
scheme: openssl-ed25519
|
||||
signing_backend: verified-dalek-serial
|
||||
status: signed
|
||||
payload_digest_sha256: d5d25db2c90f0246cc10d10f912085b81c60d2a324354b5abecfdbeb8bd17ee2
|
||||
signature_base64: sI4rWnWMrX0zDaN8jofBwSuExKg1e0cmZV3OPaulbXDV97yz95aSo0Gnrfd1En2vyyV1qoEXdTAY9NDgvA8xBA==
|
||||
payload_digest_sha256: d915e3fd22b755768bc91e088259f42e6011e8fb98910aaa6ae4b93ce793255b
|
||||
signature_base64: 30Dz9i50AsM9L44fQCGQJcnk84fzpIUuJznbJiumQuREZ2QdAfsY9EpnXiC+uHcTnN8yJ928WIeBcarsKebXBw==
|
||||
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:
|
||||
scheme: ML-DSA-65
|
||||
standard: FIPS 204
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ from datetime import datetime, timezone
|
|||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from pacta.dogfood import BACKEND_OPENSSL, BACKEND_VERIFIED, load_provenance, locate_verifier
|
||||
from pacta.signing import canonical_json
|
||||
from pacta.transparency import (
|
||||
HASH_ALGORITHM,
|
||||
|
|
@ -19,6 +20,7 @@ from pacta.transparency import (
|
|||
make_signed_tree_head,
|
||||
merkle_root,
|
||||
proof_to_hex,
|
||||
verify_inclusion,
|
||||
)
|
||||
from pacta.yamlio import dump_data, load_data
|
||||
|
||||
|
|
@ -95,7 +97,8 @@ class TransparencyLog:
|
|||
timestamp: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
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(
|
||||
metadata["log_id"],
|
||||
len(leaves),
|
||||
|
|
@ -103,6 +106,7 @@ class TransparencyLog:
|
|||
timestamp or _now(),
|
||||
private_key_path,
|
||||
public_key_path,
|
||||
signing_provenance=self.signing_provenance(entries),
|
||||
)
|
||||
dump_data(sth, self.sth_path)
|
||||
return sth
|
||||
|
|
@ -144,6 +148,7 @@ class TransparencyLog:
|
|||
_now(),
|
||||
private_key_path,
|
||||
public_key_path,
|
||||
signing_provenance=self.signing_provenance(entries),
|
||||
)
|
||||
dump_data(sth, self.sth_path)
|
||||
consistency = []
|
||||
|
|
@ -174,6 +179,45 @@ class TransparencyLog:
|
|||
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]:
|
||||
"""Consistency proof from an arbitrary earlier tree size - what a
|
||||
pinning agent requests when its pin is older than the receipt's
|
||||
|
|
|
|||
|
|
@ -20,6 +20,9 @@ BACKEND_OPENSSL = "openssl"
|
|||
# the raw 32-byte key. Parsing by prefix is exact for this OID, not a
|
||||
# heuristic.
|
||||
_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)
|
||||
|
|
@ -50,6 +53,49 @@ def pem_public_key_to_raw(public_key_path: str | Path) -> bytes:
|
|||
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:
|
||||
return Path(state_dir or DEFAULT_STATE_DIR) / "pacta-verified-verify"
|
||||
|
||||
|
|
|
|||
|
|
@ -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]:
|
||||
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["signature"] = {
|
||||
"scheme": "openssl-ed25519",
|
||||
"signing_backend": signing_backend,
|
||||
"status": "signed",
|
||||
"payload_digest_sha256": hashlib.sha256(payload).hexdigest(),
|
||||
"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:
|
||||
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()
|
||||
with tempfile.TemporaryDirectory(prefix="pacta-sign-") as tmp:
|
||||
payload_path = Path(tmp) / "payload.bin"
|
||||
|
|
|
|||
|
|
@ -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_detailed
|
||||
from .signing import canonical_json, public_key_fingerprint, sign_payload_ed25519_detailed, verify_payload_ed25519_detailed
|
||||
from .yamlio import load_data
|
||||
|
||||
HASH_ALGORITHM = "RFC9162_SHA256"
|
||||
|
|
@ -152,6 +152,7 @@ def make_signed_tree_head(
|
|||
timestamp: str,
|
||||
private_key_path: str | Path,
|
||||
public_key_path: str | Path,
|
||||
signing_provenance: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
sth: dict[str, Any] = {
|
||||
"schema_version": 1,
|
||||
|
|
@ -163,13 +164,16 @@ def make_signed_tree_head(
|
|||
"hash_algorithm": HASH_ALGORITHM,
|
||||
}
|
||||
payload = signed_tree_head_payload(sth)
|
||||
signature_base64, signing_backend = sign_payload_ed25519_detailed(payload, private_key_path)
|
||||
sth["signatures"] = {
|
||||
"ed25519": {
|
||||
"scheme": "openssl-ed25519",
|
||||
"signing_backend": signing_backend,
|
||||
"status": "signed",
|
||||
"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),
|
||||
**({"signing_provenance": signing_provenance} if signing_provenance else {}),
|
||||
},
|
||||
"ml_dsa": detect_ml_dsa().to_signature_slot(),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,3 +64,27 @@ def test_real_dogfood_binary_if_built(tmp_path):
|
|||
# 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
|
||||
|
||||
|
||||
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"
|
||||
|
|
|
|||
Loading…
Reference in a new issue