mirror of
https://github.com/pnpm/pnpm.git
synced 2026-07-14 17:52:44 -04:00
feat(release): auto-refresh embedded signing keys in the release PR (#12798)
* feat(release): auto-refresh embedded signing keys in the release PR The create-release-pr workflow previously failed when the embedded npm signing keys or Node.js release keys drifted from their canonical sources, requiring a manual update PR before a release could proceed. Now the workflow runs the update scripts in --update mode and commits the refreshed keys together with the version bumps, so the new trust roots are reviewed as part of the release PR diff. A synthesized changeset records the refresh in the changelogs of the affected packages. Also embeds the Node.js release team's new signing key 655F3B5C1FB3FA8D1A0CA6BDE4A7D232B936D2FD (Stewart X Addison), which the last release run reported as missing, and fixes the Rust renderer in update-node-release-keys.mjs so its output is lint-clean (angle-bracket URL for dylint's bare-url lint, raw-string hashes only when the key contains a quote for clippy's needless_raw_string_hashes) and matches the committed file byte-for-byte on a no-drift run. * fix(release): canonicalize npm key order and flag trust-root refreshes Review follow-ups: sort the merged npm signing keys by keyid so a reordering of npm's response cannot churn the generated file or synthesize a spurious changeset, and leave an explicit warning comment on the release PR whenever embedded trust roots were refreshed, so a key change cannot slip through buried in a large version-bump diff.
This commit is contained in:
6
.changeset/add-sxa-node-release-key.md
Normal file
6
.changeset/add-sxa-node-release-key.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@pnpm/crypto.shasums-file": patch
|
||||
"pnpm": patch
|
||||
---
|
||||
|
||||
Added the Node.js release team's new signing key (Stewart X Addison, `655F3B5C1FB3FA8D1A0CA6BDE4A7D232B936D2FD`) to the embedded Node.js release keys, so runtimes whose `SHASUMS256.txt` is signed by the new releaser verify successfully.
|
||||
71
.github/workflows/create-release-pr.yml
vendored
71
.github/workflows/create-release-pr.yml
vendored
@@ -50,19 +50,53 @@ jobs:
|
||||
git fetch origin "$TARGET"
|
||||
git checkout -B "release-pr/$TARGET" FETCH_HEAD
|
||||
|
||||
# Fail the release if the npm registry signing keys embedded in
|
||||
# @pnpm/deps.security.signatures have drifted from what npm advertises. pnpm
|
||||
# verifies package-manager binaries (pacquet, the version-switch pnpm) against
|
||||
# these keys, so a stale set could break verification after a key rotation.
|
||||
- name: Check embedded npm signing keys are up to date
|
||||
run: node pnpm11/deps/security/signatures/scripts/update-npm-signing-keys.mjs
|
||||
# Refresh the npm registry signing keys embedded in
|
||||
# @pnpm/deps.security.signatures from what npm advertises. pnpm verifies
|
||||
# package-manager binaries (pacquet, the version-switch pnpm) against these
|
||||
# keys, so a stale set could break verification after a key rotation. Any
|
||||
# drift is committed below along with the version bumps, so the new trust
|
||||
# roots are reviewed as part of the release PR diff.
|
||||
- name: Update embedded npm signing keys
|
||||
run: node pnpm11/deps/security/signatures/scripts/update-npm-signing-keys.mjs --update
|
||||
|
||||
# Fail the release if the embedded Node.js release keys (used to verify the
|
||||
# signature of a downloaded runtime's SHASUMS256.txt) have drifted from the
|
||||
# canonical nodejs/release-keys list, so a new release signer cannot silently
|
||||
# break Node.js runtime verification.
|
||||
- name: Check embedded Node.js release keys are up to date
|
||||
run: node pnpm11/crypto/shasums-file/scripts/update-node-release-keys.mjs
|
||||
# Refresh the embedded Node.js release keys (used to verify the signature of
|
||||
# a downloaded runtime's SHASUMS256.txt) from the canonical
|
||||
# nodejs/release-keys list, so a new release signer cannot break Node.js
|
||||
# runtime verification. Reviewed in the release PR diff like the npm keys.
|
||||
- name: Update embedded Node.js release keys
|
||||
run: node pnpm11/crypto/shasums-file/scripts/update-node-release-keys.mjs --update
|
||||
|
||||
# A refreshed trust root must show up in the changelogs, so synthesize a
|
||||
# changeset for whichever key sets drifted before `pnpm bump` consumes the
|
||||
# pending changesets. The `drifted` output drives an explicit review signal
|
||||
# on the PR, so a trust-root change cannot hide in a large version-bump diff.
|
||||
- name: Add changesets for refreshed keys
|
||||
id: keys
|
||||
run: |
|
||||
drifted=""
|
||||
if ! git diff --quiet -- pnpm11/deps/security/signatures/src/npmSigningKeys.ts; then
|
||||
drifted="npm registry signing keys"
|
||||
cat > .changeset/release-refresh-npm-signing-keys.md <<'EOF'
|
||||
---
|
||||
"@pnpm/deps.security.signatures": patch
|
||||
"pnpm": patch
|
||||
---
|
||||
|
||||
Updated the embedded npm registry signing keys to the set currently advertised by npm.
|
||||
EOF
|
||||
fi
|
||||
if ! git diff --quiet -- pnpm11/crypto/shasums-file/src/nodeReleaseKeys.ts; then
|
||||
drifted="${drifted:+$drifted and }Node.js release keys"
|
||||
cat > .changeset/release-refresh-node-release-keys.md <<'EOF'
|
||||
---
|
||||
"@pnpm/crypto.shasums-file": patch
|
||||
"pnpm": patch
|
||||
---
|
||||
|
||||
Updated the embedded Node.js release keys to the current canonical `nodejs/release-keys` list.
|
||||
EOF
|
||||
fi
|
||||
echo "drifted=$drifted" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Consumes the pending changesets: bumps versions, writes changelogs, updates
|
||||
# the ledger, and syncs manifests. A no-op (no pending changesets) leaves the
|
||||
@@ -117,3 +151,16 @@ jobs:
|
||||
--base "$TARGET" \
|
||||
--head "$BRANCH"
|
||||
fi
|
||||
|
||||
# Embedded trust roots changed in this release, so leave a hard-to-miss
|
||||
# comment: the refreshed keys need deliberate review, not a scroll-past
|
||||
# among the version bumps and changelogs.
|
||||
- name: Flag refreshed trust roots on the PR
|
||||
if: steps.changes.outputs.changed == 'true' && steps.keys.outputs.drifted != ''
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.UPDATE_LOCKFILE_TOKEN }}
|
||||
TARGET: ${{ github.event.inputs.target }}
|
||||
DRIFTED: ${{ steps.keys.outputs.drifted }}
|
||||
run: |
|
||||
PR=$(gh pr list --head "release-pr/$TARGET" --state open --json number --jq '.[0].number')
|
||||
gh pr comment "$PR" --body "⚠️ This release refreshes embedded trust roots: **${DRIFTED}**. Review those key diffs deliberately before merging — they gate signature verification of Node.js runtimes and package-manager binaries."
|
||||
|
||||
@@ -2117,6 +2117,23 @@ sWjTVgUCaGA63AIbDAAKCRAgsaOQsWjTVu8oAP9Bc+QY+9FikX3YvMgWAqiDlVOy
|
||||
o0y6UIZGBMSQlF80wAD/d34LqtVIVe9oe5NO3xA75+6Ew8tGeAjUq/ovagr5dAU=
|
||||
=JsVv
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
",
|
||||
},
|
||||
NodeReleaseKey {
|
||||
fingerprint: "655F3B5C1FB3FA8D1A0CA6BDE4A7D232B936D2FD",
|
||||
armored_key: r"-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mDMEakIjcxYJKwYBBAHaRw8BAQdAYRFWvBCAd9dDjKTePuAvvzAWxhvojAXco0m4
|
||||
2AMh/MC0H1N0ZXdhcnQgWCBBZGRpc29uIDxzeGFAaWJtLmNvbT6ImQQTFgoAQRYh
|
||||
BGVfO1wfs/qNGgymveSn0jK5NtL9BQJqQiNzAhsDBQkDwmcABQsJCAcCAiICBhUK
|
||||
CQgLAgQWAgMBAh4HAheAAAoJEOSn0jK5NtL9FOEBAMpIjknm4fnEQvTmIlzy0kDu
|
||||
VplF4HR78+lBef2i4590AP9i82wtP4pH1/vynoSBMkCauFIPmF1c9MYLFF0f53zq
|
||||
Arg4BGpCI3MSCisGAQQBl1UBBQEBB0AVa1WJ4P8ir/M7NaCGrX7PDs0QU9qzpzme
|
||||
a5TZ44b/YgMBCAeIfgQYFgoAJhYhBGVfO1wfs/qNGgymveSn0jK5NtL9BQJqQiNz
|
||||
AhsMBQkDwmcAAAoJEOSn0jK5NtL9EzYA/Arr4hbKLaU6OUjAVbH7HGLEl0HSvxE8
|
||||
5lEgWfuNbqOtAQCF9UT0wQEfiIj2R358Ce62zV43w2yaD8Xu0M/S+hz8AQ==
|
||||
=DcLq
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
// signature of SHASUMS256.txt) from the canonical nodejs/release-keys repo into
|
||||
// src/nodeReleaseKeys.ts and pacquet's matching Rust key module.
|
||||
//
|
||||
// node update-node-release-keys.mjs # check (CI / release gate)
|
||||
// node update-node-release-keys.mjs # check
|
||||
// node update-node-release-keys.mjs --update # rewrite the embedded keys
|
||||
//
|
||||
// `--check` fails when the authoritative keys.list contains a fingerprint that
|
||||
// is not embedded, so a newly added release signer cannot silently break Node
|
||||
// runtime verification.
|
||||
// The create-release-pr workflow runs `--update`, so any drift from the
|
||||
// authoritative keys.list is committed into the release PR and reviewed there —
|
||||
// a newly added release signer cannot silently break Node runtime verification.
|
||||
// Check mode reports the drift without writing.
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
@@ -97,10 +98,13 @@ ${entries}
|
||||
}
|
||||
|
||||
function renderRust (keys) {
|
||||
// Hashed delimiters only when the key itself contains a quote, or clippy's
|
||||
// needless_raw_string_hashes rejects the generated file.
|
||||
const rustRawString = (s) => s.includes('"') ? `r#"${s}"#` : `r"${s}"`
|
||||
const entries = keys.map(({ fingerprint, armored }) =>
|
||||
` NodeReleaseKey {\n fingerprint: "${fingerprint}",\n armored_key: r#"${armored}\n"#,\n },`).join('\n')
|
||||
` NodeReleaseKey {\n fingerprint: "${fingerprint}",\n armored_key: ${rustRawString(`${armored}\n`)},\n },`).join('\n')
|
||||
return `// GENERATED - the Node.js release team's OpenPGP public keys, mirrored from
|
||||
// https://github.com/nodejs/release-keys (keys.list + keys/<fingerprint>.asc).
|
||||
// <https://github.com/nodejs/release-keys> (keys.list + keys/<fingerprint>.asc).
|
||||
//
|
||||
// Used to verify the signature of a Node.js release's SHASUMS256.txt before
|
||||
// trusting its hashes. Refresh with:
|
||||
|
||||
@@ -119,4 +119,8 @@ export const NODE_RELEASE_KEYS = [
|
||||
fingerprint: '5BE8A3F6C8A5C01D106C0AD820B1A390B168D356',
|
||||
armoredKey: "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmDMEaGA63BYJKwYBBAHaRw8BAQdAo/yU+MutacFmmn0CEX495goNrBxR24235XLM\ncvHYjfq0L0FudG9pbmUgZHUgSGFtZWwgPGR1aGFtZWxhbnRvaW5lMTk5NUBnbWFp\nbC5jb20+iI4EExYKADYWIQRb6KP2yKXAHRBsCtggsaOQsWjTVgUCaGA63AIbAwQL\nCQgHBBUKCQgFFgIDAQACHgUCF4AACgkQILGjkLFo01afgwEA/sLHqsj7ml2vyDoT\nKDPE8n9a80ZOh14OfnlOe0cCZA8BAMEOOk7QFI69DIlV1nMiqcFCqQFoSzBU2LkI\nR17p/j4NtDNBbnRvaW5lIGR1IEhhbWVsIDxhbnRvaW5lLmR1aGFtZWxAcGxhdGZv\ncm1hdGljLmRldj6IjgQTFgoANhYhBFvoo/bIpcAdEGwK2CCxo5CxaNNWBQJpsCMx\nAhsDBAsJCAcEFQoJCAUWAgMBAAIeAQIXgAAKCRAgsaOQsWjTVr/sAPwIBsG8g6ND\nzoNRTX1wPKBvfZg1NP7tYCyM5sxQfrpuLAEA05AhG4xBILfhL/f0pqR5jXfxg6gz\nT6WfeVeS6zeHZwe4OARoYDrcEgorBgEEAZdVAQUBAQdAQVmtih8AO3ryBQMR/22x\nWHVKLjAbCiH2cMxNH+iy1RQDAQgHiHgEGBYKACAWIQRb6KP2yKXAHRBsCtggsaOQ\nsWjTVgUCaGA63AIbDAAKCRAgsaOQsWjTVu8oAP9Bc+QY+9FikX3YvMgWAqiDlVOy\no0y6UIZGBMSQlF80wAD/d34LqtVIVe9oe5NO3xA75+6Ew8tGeAjUq/ovagr5dAU=\n=JsVv\n-----END PGP PUBLIC KEY BLOCK-----\n",
|
||||
},
|
||||
{
|
||||
fingerprint: '655F3B5C1FB3FA8D1A0CA6BDE4A7D232B936D2FD',
|
||||
armoredKey: "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmDMEakIjcxYJKwYBBAHaRw8BAQdAYRFWvBCAd9dDjKTePuAvvzAWxhvojAXco0m4\n2AMh/MC0H1N0ZXdhcnQgWCBBZGRpc29uIDxzeGFAaWJtLmNvbT6ImQQTFgoAQRYh\nBGVfO1wfs/qNGgymveSn0jK5NtL9BQJqQiNzAhsDBQkDwmcABQsJCAcCAiICBhUK\nCQgLAgQWAgMBAh4HAheAAAoJEOSn0jK5NtL9FOEBAMpIjknm4fnEQvTmIlzy0kDu\nVplF4HR78+lBef2i4590AP9i82wtP4pH1/vynoSBMkCauFIPmF1c9MYLFF0f53zq\nArg4BGpCI3MSCisGAQQBl1UBBQEBB0AVa1WJ4P8ir/M7NaCGrX7PDs0QU9qzpzme\na5TZ44b/YgMBCAeIfgQYFgoAJhYhBGVfO1wfs/qNGgymveSn0jK5NtL9BQJqQiNz\nAhsMBQkDwmcAAAoJEOSn0jK5NtL9EzYA/Arr4hbKLaU6OUjAVbH7HGLEl0HSvxE8\n5lEgWfuNbqOtAQCF9UT0wQEfiIj2R358Ce62zV43w2yaD8Xu0M/S+hz8AQ==\n=DcLq\n-----END PGP PUBLIC KEY BLOCK-----\n",
|
||||
},
|
||||
] as const satisfies ReadonlyArray<{ fingerprint: string, armoredKey: string }>
|
||||
|
||||
@@ -2,14 +2,15 @@
|
||||
// Keeps the embedded npm registry signing keys (src/npmSigningKeys.ts) in sync
|
||||
// with https://registry.npmjs.org/-/npm/v1/keys.
|
||||
//
|
||||
// node update-npm-signing-keys.mjs # check (CI / release gate)
|
||||
// node update-npm-signing-keys.mjs # check
|
||||
// node update-npm-signing-keys.mjs --update # rewrite the embedded keys
|
||||
//
|
||||
// `--check` fails when npm advertises a signing key that is not embedded
|
||||
// verbatim, so a key rotation cannot silently break (or weaken) pnpm's
|
||||
// signature verification. `--update` writes the union of npm's keys and any
|
||||
// embedded keys npm no longer lists (older keys are kept so packages published
|
||||
// before a rotation still verify).
|
||||
// The create-release-pr workflow runs `--update`, so a key rotation is
|
||||
// committed into the release PR and reviewed there rather than silently
|
||||
// breaking (or weakening) pnpm's signature verification. `--update` writes the
|
||||
// union of npm's keys and any embedded keys npm no longer lists (older keys are
|
||||
// kept so packages published before a rotation still verify). Check mode
|
||||
// reports the drift without writing.
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
@@ -37,11 +38,13 @@ async function main () {
|
||||
}
|
||||
|
||||
// Union: every npm key, plus embedded keys npm no longer lists (kept for
|
||||
// verifying packages published before a rotation).
|
||||
// verifying packages published before a rotation). Sorted so a reordering of
|
||||
// npm's response cannot churn the generated file.
|
||||
const merged = [...npmKeys]
|
||||
for (const e of embedded) {
|
||||
if (!merged.some((m) => m.keyid === e.keyid)) merged.push(e)
|
||||
}
|
||||
merged.sort((a, b) => a.keyid.localeCompare(b.keyid))
|
||||
fs.writeFileSync(KEYS_FILE, render(merged))
|
||||
console.log(missing.length === 0
|
||||
? '✓ Embedded npm signing keys already current; rewrote file.'
|
||||
@@ -87,8 +90,8 @@ function render (keys) {
|
||||
// ${KEYS_URL}
|
||||
//
|
||||
// Refresh with: node deps/security/signatures/scripts/update-npm-signing-keys.mjs --update
|
||||
// The release workflow runs \`--check\` and fails if these drift from npm, so a
|
||||
// rotated key cannot silently break (or weaken) signature verification.
|
||||
// The create-release-pr workflow refreshes these automatically, so a rotated
|
||||
// key cannot silently break (or weaken) signature verification.
|
||||
export const NPM_SIGNING_KEYS = ${body} as const satisfies ReadonlyArray<{
|
||||
expires: string | null
|
||||
keyid: string
|
||||
|
||||
@@ -3,22 +3,22 @@
|
||||
// https://registry.npmjs.org/-/npm/v1/keys
|
||||
//
|
||||
// Refresh with: node deps/security/signatures/scripts/update-npm-signing-keys.mjs --update
|
||||
// The release workflow runs `--check` and fails if these drift from npm, so a
|
||||
// rotated key cannot silently break (or weaken) signature verification.
|
||||
// The create-release-pr workflow refreshes these automatically, so a rotated
|
||||
// key cannot silently break (or weaken) signature verification.
|
||||
export const NPM_SIGNING_KEYS = [
|
||||
{
|
||||
"expires": "2025-01-29T00:00:00.000Z",
|
||||
"keyid": "SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA",
|
||||
"keytype": "ecdsa-sha2-nistp256",
|
||||
"scheme": "ecdsa-sha2-nistp256",
|
||||
"key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE1Olb3zMAFFxXKHiIkQO5cJ3Yhl5i6UPp+IhuteBJbuHcA5UogKo0EWtlWwW6KSaKoTNEYL7JlCQiVnkhBktUgg=="
|
||||
},
|
||||
{
|
||||
"expires": null,
|
||||
"keyid": "SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U",
|
||||
"keytype": "ecdsa-sha2-nistp256",
|
||||
"scheme": "ecdsa-sha2-nistp256",
|
||||
"key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEY6Ya7W++7aUPzvMTrezH6Ycx3c+HOKYCcNGybJZSCJq/fd7Qa8uuAKtdIkUQtQiEKERhAmE5lMMJhP8OkDOa2g=="
|
||||
},
|
||||
{
|
||||
"expires": "2025-01-29T00:00:00.000Z",
|
||||
"keyid": "SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA",
|
||||
"keytype": "ecdsa-sha2-nistp256",
|
||||
"scheme": "ecdsa-sha2-nistp256",
|
||||
"key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE1Olb3zMAFFxXKHiIkQO5cJ3Yhl5i6UPp+IhuteBJbuHcA5UogKo0EWtlWwW6KSaKoTNEYL7JlCQiVnkhBktUgg=="
|
||||
}
|
||||
] as const satisfies ReadonlyArray<{
|
||||
expires: string | null
|
||||
|
||||
Reference in New Issue
Block a user