dogfood: anchor the signer path to the package, not the working directory

DEFAULT_STATE_DIR was Path("dogfood")/"state" -- a relative path, so
locate_verifier() resolved against whatever directory the process started in.
The consequence was not a crash but something quieter: run the provider from
the repository root and it signs with the attested dalek build; run it from
anywhere else and the binary is not found, signing falls back to OpenSSL, and
the head records `signing_backend: openssl`. Which implementation signs the
transparency log was an accident of the current directory.

Found by re-signing published head 13 as a reproducibility check. The byte
comparison passed -- the reconstructed payload re-signed to signature_base64
exactly -- but the backend came back `openssl` while head 13 records
`verified-dalek-serial`. The swap is invisible precisely BECAUSE Ed25519 is
deterministic: both implementations emit identical bytes, nothing downstream
differs, no test fails. A silent substitution that changes no output is one
nobody notices until the outputs differ, which for a signing key is late.

(The byte-identity is also a good result in its own right: independent
cross-implementation agreement on the SIGNING side, alongside the five-way
agreement already demonstrated on the verifying side.)

Fix: anchor to the package via Path(__file__).resolve().parents[2]. Resolution
no longer depends on cwd -- demonstrated from /, /tmp and the repo root, all
three now select verified-dalek where before only the repo root did.

Added PACTA_REQUIRE_VERIFIED_SIGNER. Recording a downgrade truthfully, which
this code already did, tells you afterwards which implementation signed; it
does not let you DECIDE which one will. For signing a transparency-log head
that choice should be stated up front and enforced, not discovered in a
provenance field once the head exists. Set it and signing raises instead of
substituting OpenSSL. Off by default: every existing caller keeps the
fall-back-and-record behaviour.

Negative-tested both ways (refuses, naming the path it searched; default still
records `openssl`). Suite: 145 passed, 0 failed, 0 skipped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
mrwulf 2026-08-04 18:10:59 +02:00
parent cd3b1bc921
commit 5e35a533e1
2 changed files with 45 additions and 2 deletions

View file

@ -12,7 +12,27 @@ from pathlib import Path
from typing import Any
DOGFOOD_ENV = "PACTA_DOGFOOD_VERIFIER"
DEFAULT_STATE_DIR = Path("dogfood") / "state"
REQUIRE_VERIFIED_ENV = "PACTA_REQUIRE_VERIFIED_SIGNER"
# Anchored to the PACKAGE, not to the caller's working directory.
#
# This was `Path("dogfood") / "state"` — a relative path, so it resolved against
# whatever directory the process happened to start in. The consequence was not a
# crash but something quieter and worse: run the provider from the repository
# root and it signs with the attested dalek build; run it from anywhere else and
# locate_verifier() finds nothing, signing falls back to OpenSSL, and the head
# records `signing_backend: openssl`. WHICH IMPLEMENTATION SIGNS THE
# TRANSPARENCY LOG WAS AN ACCIDENT OF THE CURRENT DIRECTORY.
#
# Found 2026-08-04 by re-signing the published head 13 as a reproducibility
# check: the byte comparison passed, but the backend came back `openssl` while
# the published head says `verified-dalek-serial`. Both produced identical bytes
# — Ed25519 is deterministic, so that is expected and is itself useful evidence
# — which is exactly why the substitution was invisible. A silent backend swap
# that changes no output is one nobody notices until the outputs differ.
#
# __file__ is <repo>/src/pacta/dogfood.py, so parents[2] is the repo root.
DEFAULT_STATE_DIR = Path(__file__).resolve().parents[2] / "dogfood" / "state"
BACKEND_VERIFIED = "verified-dalek-serial"
BACKEND_OPENSSL = "openssl"
@ -112,6 +132,21 @@ def locate_verifier(state_dir: str | Path | None = None) -> Path | None:
return path if path.exists() else None
def require_verified_signer() -> bool:
"""Whether a downgrade to OpenSSL is forbidden for this process.
Recording a downgrade truthfully, which this code already does, tells you
afterwards which implementation signed. It does not let you DECIDE which
one will. For an operation as consequential as signing a transparency-log
head, the choice should be stated up front and enforced, not discovered in
the provenance field once the head exists.
Off by default: every existing caller keeps the fall-back-and-record
behaviour. Set PACTA_REQUIRE_VERIFIED_SIGNER=1 and signing raises instead
of quietly substituting OpenSSL."""
return os.environ.get(REQUIRE_VERIFIED_ENV, "").strip().lower() in {"1", "true", "yes", "on"}
def load_provenance(binary_path: str | Path) -> dict[str, Any]:
sidecar = Path(binary_path).with_suffix(".provenance.json")
if sidecar.exists():

View file

@ -92,12 +92,20 @@ def sign_payload_ed25519_detailed(payload: bytes, private_key_path: str | Path)
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
from .dogfood import (BACKEND_OPENSSL, BACKEND_VERIFIED, REQUIRE_VERIFIED_ENV,
default_binary_path, locate_verifier, require_verified_signer,
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
if require_verified_signer():
raise SignatureError(
f"{REQUIRE_VERIFIED_ENV} is set, so falling back to OpenSSL is refused, but the "
f"attested signer was not found at {default_binary_path()}. "
f"Build it, or point {'PACTA_DOGFOOD_VERIFIER'} at it, or unset "
f"{REQUIRE_VERIFIED_ENV} to accept the recorded downgrade.")
return _sign_payload_openssl(payload, private_key_path), BACKEND_OPENSSL