diff --git a/.changeset/add-sxa-node-release-key.md b/.changeset/add-sxa-node-release-key.md new file mode 100644 index 0000000000..bc5eef87b4 --- /dev/null +++ b/.changeset/add-sxa-node-release-key.md @@ -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. diff --git a/.github/workflows/create-release-pr.yml b/.github/workflows/create-release-pr.yml index b3e5cde801..cd1fee7aa5 100644 --- a/.github/workflows/create-release-pr.yml +++ b/.github/workflows/create-release-pr.yml @@ -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." diff --git a/pacquet/crates/crypto-shasums-file/src/node_release_keys.rs b/pacquet/crates/crypto-shasums-file/src/node_release_keys.rs index 46cb4c75b7..a5fd10b3f2 100644 --- a/pacquet/crates/crypto-shasums-file/src/node_release_keys.rs +++ b/pacquet/crates/crypto-shasums-file/src/node_release_keys.rs @@ -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----- ", }, ]; diff --git a/pnpm11/crypto/shasums-file/scripts/update-node-release-keys.mjs b/pnpm11/crypto/shasums-file/scripts/update-node-release-keys.mjs index a3d0ceb1c5..93588c7c3d 100644 --- a/pnpm11/crypto/shasums-file/scripts/update-node-release-keys.mjs +++ b/pnpm11/crypto/shasums-file/scripts/update-node-release-keys.mjs @@ -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/.asc). +// (keys.list + keys/.asc). // // Used to verify the signature of a Node.js release's SHASUMS256.txt before // trusting its hashes. Refresh with: diff --git a/pnpm11/crypto/shasums-file/src/nodeReleaseKeys.ts b/pnpm11/crypto/shasums-file/src/nodeReleaseKeys.ts index 43967ae5fe..524d15dc4d 100644 --- a/pnpm11/crypto/shasums-file/src/nodeReleaseKeys.ts +++ b/pnpm11/crypto/shasums-file/src/nodeReleaseKeys.ts @@ -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 }> diff --git a/pnpm11/deps/security/signatures/scripts/update-npm-signing-keys.mjs b/pnpm11/deps/security/signatures/scripts/update-npm-signing-keys.mjs index 98568ae3f9..a3bc755b65 100644 --- a/pnpm11/deps/security/signatures/scripts/update-npm-signing-keys.mjs +++ b/pnpm11/deps/security/signatures/scripts/update-npm-signing-keys.mjs @@ -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 diff --git a/pnpm11/deps/security/signatures/src/npmSigningKeys.ts b/pnpm11/deps/security/signatures/src/npmSigningKeys.ts index 9a2f8632e3..c7869ecce8 100644 --- a/pnpm11/deps/security/signatures/src/npmSigningKeys.ts +++ b/pnpm11/deps/security/signatures/src/npmSigningKeys.ts @@ -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