Skip to content

feat(apt): verify OpenPGP signatures hermetically when sqv/gpgv are missing - #287

Closed
devPalacio wants to merge 2 commits into
bazel-contrib:mainfrom
devPalacio:hermetic-pgp-verifier
Closed

devPalacio wants to merge 2 commits into
bazel-contrib:mainfrom
devPalacio:hermetic-pgp-verifier

Conversation

@devPalacio

@devPalacio devPalacio commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Problem

apt signature verification needs sqv or gpgv on PATH (plus gpg or sq to dearmor .asc keyrings). Without them, extension evaluation fails:

Error in fail: GPG verification requested for dist 'bullseye', but neither `gpgv` nor `sqv` was found on PATH.

Stock macOS has none of these tools. This is what fails the macOS jobs of the BCR presubmit for 0.11.0 (bazel-central-registry#10868, build 42895). It also affects any macOS user of gpg_keys, and it is a host-tool dependency of the kind #185, #141 and #283 removed.

Change

When neither sqv nor gpgv is on PATH, fall back to a hermetic verifier instead of failing. sqv and gpgv are still preferred when present, so nothing changes for anyone who has them.

  • apt/private/Verify.java runs on the host's rules_java remotejdk21_* JDK, with the three BouncyCastle 1.86 jars downloaded from Maven Central and pinned by sha256.
  • OpenPGP semantics are left to BouncyCastle's high-level API (org.bouncycastle.openpgp.api, default policy): certificate validity, revocation, expiry, key capabilities, hash and key-strength policy, and text-mode canonicalisation for detached signatures. Armored and binary keyrings, including several concatenated armored blocks, are read directly, so there is no gpg --dearmor step.
  • What Verify.java adds is small: an allowlist of document signature types (the library otherwise accepts any signature class, so a key certification could pass as a signature over attacker-chosen bytes), verification at the current time, and the RFC 4880 section 7.1 reduction of a cleartext-signed InRelease to the text that was signed, since the library has no cleartext support. The text that is returned is exactly the text that was verified.
  • Success follows the sqv rule: at least one valid signature from a key in the keyrings. Exit code 1 means not verified, 2 means the verifier could not process its inputs, and pgp.bzl reports the two differently.
  • pgp.bzl clears JAVA_TOOL_OPTIONS, JDK_JAVA_OPTIONS and _JAVA_OPTIONS for the verifier JVM, and does not call the host rm for it.
  • MODULE.bazel: use_extension(@rules_java//java:extensions.bzl, "toolchains") and use_repo for the nine remotejdk21_* repos (all nine exist in rules_java 8.8.0).

It is stricter than gpgv in a few places on purpose: SHA-1 and MD5 signatures, expired or revoked keys, and expired signatures are rejected, as sqv does.

Testing

  • apt/tests/pgp: 35 cases with checked-in signed fixtures (good, trailing whitespace, dash escaping, no final newline, CRLF, tampered text, lone CR, truncated, SHA-1, MD5, expired signature, expired key, revoked key, wrong key, certification-signature replay, standalone and certification classes, encryption-only subkey, concatenated and invalid keyrings). They run as of a fixed time, so they do not depend on the clock. I mutation-tested the suite: removing the signature-type allowlist, the whitespace stripping, the lone-CR rejection, or the check at the current time each makes exactly the expected cases fail.
  • New hermetic-pgp CI job: the fixture suite, e2e/smoke, and e2e/invalid_key, with a PATH that has no gpg*, sq or sqv. I ran the same steps locally; the workflow itself has not run yet.
  • Locally, with no OpenPGP tools on the repo PATH and a fresh Bazel cache: e2e/smoke builds and analyzes on macOS (arm64) with Bazel 7.x and 8.5.1, and on Ubuntu 24.04 (analysis only) with both; e2e/invalid_key fails with "GPG verification failed" on all of them. bazel test //apt/tests/...: 44 of 44 pass. main without this change fails with the error above.
  • Combined with fix: bump tar.bzl to 0.10.7 #286 (the tar.bzl bump) and BCR's incompatible flags, as in the presubmit, it also builds on macOS.

Not tested: Windows and the non-host JDK platforms (declared, not exercised), and the real BCR presubmit run.

Review

The first version of this PR used BouncyCastle's low-level signature API and hand-written policy. An adversarial review by four reviewers found, among other things, that a key certification was accepted as a valid Release.gpg, that expired signatures and keys, MD5 and SHA-1 were accepted, and that the whitespace and CR handling differed from gpgv. This version replaces that code with the library's verifier and covers each finding with a fixture.

Open questions

  • Fallback vs. default. The fallback keeps behavior stable. Making the hermetic verifier the default would give identical results on every machine, which the README already asks for, but it would be a behavior change.
  • Cost: a JVM start per verification (about 0.5 to 1 s each) and a JDK download on hosts without sqv/gpgv. Verify.java is compiled by the source launcher on every call.
  • The cleartext adapter is the only hand-written OpenPGP-adjacent logic left. A library with cleartext support (for example PGPainless) could remove it; I have not tried one.

Prior art

  • rules_jvm_external uses BouncyCastle OpenPGP classes in a Bazel-launched Java tool (MavenSigning.java), for signing.
  • bazeldnf verifies RPMs with golang.org/x/crypto/openpgp inside a Bazel-built binary.
  • envoyproxy/toolshed (bazel/pgp) ships a pinned prebuilt sq, for Linux x86_64 and arm64 only.

Sequoia does not publish prebuilt sqv or sq binaries, and BCR has no OpenPGP module, so I used the JDK this repo already depends on.

Related: #280 (the BCR PRs for 0.9.5, 0.10.0 and 0.11.0 not published) and #286 (tar.bzl bump, the other cause of the 0.11.0 presubmit failure).

🤖 Generated with Claude Code

…issing

Fall back to a BouncyCastle-based verifier (apt/private/Verify.java) run on
a rules_java remote JDK when neither sqv nor gpgv is on PATH. The BouncyCastle
jars are pinned by sha256. sqv and gpgv are still preferred when present.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@loosebazooka

Copy link
Copy Markdown
Contributor

I think I would prefer to not merge this (maintain our own verifier), for now we can get presubmit to be green by adding gnupg to the build.

…CI job

Rework the hermetic verifier after an adversarial review of the first version:

- Use org.bouncycastle.openpgp.api (certificate validity, revocation, expiry,
  key capabilities, hash and key-strength policy) instead of the low-level
  signature API. Only a signature type allowlist, verification at the current
  time and the RFC 4880 7.1 reduction of cleartext messages are written here.
- Reject a lone CR in cleartext messages, strip trailing blanks, and return the
  exact text that was verified.
- Exit 2 for parse and I/O errors so they are not reported as bad signatures.
- Clear JAVA_TOOL_OPTIONS and friends for the verifier JVM; skip the host rm
  for it.
- apt/tests/pgp: signed fixtures and a runner (35 cases, including forgery,
  weak digest, expiry, revocation and lone-CR cases).
- CI: run the fixtures and the e2e suites with no OpenPGP tools on PATH.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@devPalacio

Copy link
Copy Markdown
Contributor Author

I think I would prefer to not merge this (maintain our own verifier), for now we can get presubmit to be green by adding gnupg to the build.

yea based on how this is going, just adding gnupg seems much more pragmatic :)

@devPalacio devPalacio closed this Oct 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants