From 5e35a533e1e11fa90d85738ca6c655368020352b Mon Sep 17 00:00:00 2001 From: mrwulf Date: Tue, 4 Aug 2026 18:10:59 +0200 Subject: [PATCH] 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 --- src/pacta/dogfood.py | 37 ++++++++++++++++++++++++++++++++++++- src/pacta/signing.py | 10 +++++++++- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/src/pacta/dogfood.py b/src/pacta/dogfood.py index 6c0d2c5..69a06ec 100644 --- a/src/pacta/dogfood.py +++ b/src/pacta/dogfood.py @@ -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 /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(): diff --git a/src/pacta/signing.py b/src/pacta/signing.py index fc46ae4..dcafa15 100644 --- a/src/pacta/signing.py +++ b/src/pacta/signing.py @@ -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