feat(download): verify GPG signature of release binaries on download#87
feat(download): verify GPG signature of release binaries on download#87wnoonan wants to merge 7 commits into
Conversation
| } | ||
|
|
||
| // GPG public key asset published on the release, used to verify other assets. | ||
| const PUBLIC_KEY_ASSET = 'cribl-js2bin-public.asc'; |
There was a problem hiding this comment.
Rather than fetching the public key from the release at runtime, consider committing it in the repo (e.g., keys/release-signing.pub) and referencing it from disk. This eliminates the co-location attack vector, removes a network dependency, and provides git audit history for key changes.
| function verifyGpgSignature(url, file, headers) { | ||
| const sigUrl = sigUrlForDownload(url); | ||
| if (!sigUrl) return Promise.resolve(); | ||
| if (!isGpgAvailable()) { |
There was a problem hiding this comment.
For the Windows pipeline where INFRA-7979 requires chain-of-custody enforcement, there's no way to make verification mandatory. Consider supporting a --require-signature arg with --build flag that turns these skip-paths into hard failures. Without it, a missing gpg public key or absent signature silently degrades, and enforcement depends on the caller inspecting logs
| return; | ||
| } | ||
| const keyUrl = pubKeyUrlForDownload(url); | ||
| return download(keyUrl, keyFile, headers) |
There was a problem hiding this comment.
If we commit the key in the repo instead (see earlier comment), this entire download + import becomes a local file read
| // GPG public key asset published on the release, used to verify other assets. | ||
| const PUBLIC_KEY_ASSET = 'cribl-js2bin-public.asc'; | ||
|
|
||
| let gpgReleaseHost = 'github.com'; |
There was a problem hiding this comment.
This implementation says "Does this URL look like a GitHub release? If yes, try to verify. If no, skip." and it can be confusing. Let's provide gpg signature url and gpg public key when --require-signature is passed. You can do something like below (possibly with some modifications):
if (!args['require-signature']) return Promise.resolve(); // user didn't ask, skip
// User asked — signature and key must be available
const sigUrl = args['gpg-signature-url']
|| process.env.JS2BIN_GPG_SIGNATURE_URL
|| `${binaryUrl}.asc`; // convention fallback
const keyPath = args['gpg-key-path']
|| process.env.JS2BIN_GPG_KEY_PATH
|| join(__dirname, '..', 'keys', 'release-signing.asc'); // bundled fallback
// Verify — no skipping, no "best effort", fail hard if anything goes wrong
What
Verify the GPG signature of release binaries on download, before js2bin injects the app bundle into them. This closes the chain-of-custody gap
How
download()insrc/util.jsgains a verification step. When the URL is a GitHub release download, after fetching the binary it also fetches two sibling assetsfrom the same release: the detached signature (
<asset>.asc) and our public key (cribl-js2bin-public.asc). It imports the key into an ephemeral keyring andruns
gpg --verify.We shell out to
gpgvia the existingrunCommandhelper rather than pulling in an OpenPGP library, keeping js2bin dependency-free.Behavior
Best-effort by design, so
gpgis not a hard build requirement:gpgis not installed, or no signature is published for the asset.ERR_GPG_VERIFY) only when a signature IS present but does not verify.The signature and public-key assets are themselves excluded from verification, so the check never recurses onto its own downloads.
Signing side (for context)
The signing/publishing half lives in the js2bin Jenkins job, not in this repo: it GPG-signs each built binary and uploads the binary, its
.ascsignature, andthe public key to the release. This PR is only the consumer/verification half.
Tests
test/gpg-verify.test.js:gpgis available, using a throwaway generated key against a local release server.Asset layout this expects on the release
For a binary downloaded from
.../releases/download/<tag>/<asset>, this code expects two sibling assets on the same release:<asset><asset>.asccribl-js2bin-public.asc