From ce7d7c1956e258a8c59db08a7e4fc81dd6a87e5b Mon Sep 17 00:00:00 2001 From: rcourtman Date: Tue, 12 May 2026 10:30:42 +0100 Subject: [PATCH] Fix stale README signature key and guard against future drift The README's secure-install snippet has pinned the wrong ed25519 key since commit a60fa03d7 (April 22, 2026), so v6 rc.2 through rc.5 all shipped with a documented verification step that does not work. I downloaded the published rc.5 install.sh + install.sh.sshsig and ran ssh-keygen -Y verify with both candidate keys: Ds21c5... (README's pinned key) -> Could not verify signature MZd/DaH... (key embedded in install.sh and pulse-auto-update.sh) -> OK Customers who actually followed the README's secure-install path saw "Could not verify signature" and aborted. Most users curl-pipe the script unverified so the drift went unreported. Replace the stale key in README.md and docs/INSTALL.md with the actual pipeline signing key (MZd/...). Add a validate-release.sh smoke that extracts the README's pinned key and runs the exact ssh-keygen -Y verify command against the signed install.sh.sshsig. Any future drift between documented key and actual signing key fails the release before publish. Lock both the correct-key presence and the stale-key absence in build_release_assets_test.go for README and docs/INSTALL.md so a manual edit cannot regress the docs back to the broken state. --- README.md | 2 +- docs/INSTALL.md | 4 +- .../installtests/build_release_assets_test.go | 39 +++++++++++++++++++ scripts/validate-release.sh | 39 +++++++++++++++++++ 4 files changed, 81 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 732879e49..bdde57ab5 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ export PULSE_VERSION=vX.Y.Z curl -fsSLO "https://github.com/rcourtman/Pulse/releases/download/${PULSE_VERSION}/install.sh" curl -fsSLO "https://github.com/rcourtman/Pulse/releases/download/${PULSE_VERSION}/install.sh.sshsig" ssh-keygen -Y verify \ - -f <(printf '%s\n' 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDs21c5oPk2khrdHlsw1aZ9EJKoTsyalGzhb0hdwJrkV pulse-installer') \ + -f <(printf '%s\n' 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMZd/DaH+BldzOkq1A8KVTcFk73nAyrE8aJOyf7i00jm pulse-installer') \ -I pulse-installer \ -n pulse-install \ -s install.sh.sshsig < install.sh diff --git a/docs/INSTALL.md b/docs/INSTALL.md index 914538b43..2ff76d5b7 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -23,7 +23,7 @@ export PULSE_VERSION=vX.Y.Z curl -fsSLO "https://github.com/rcourtman/Pulse/releases/download/${PULSE_VERSION}/install.sh" curl -fsSLO "https://github.com/rcourtman/Pulse/releases/download/${PULSE_VERSION}/install.sh.sshsig" ssh-keygen -Y verify \ - -f <(printf '%s\n' 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDs21c5oPk2khrdHlsw1aZ9EJKoTsyalGzhb0hdwJrkV pulse-installer') \ + -f <(printf '%s\n' 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMZd/DaH+BldzOkq1A8KVTcFk73nAyrE8aJOyf7i00jm pulse-installer') \ -I pulse-installer \ -n pulse-install \ -s install.sh.sshsig < install.sh @@ -98,7 +98,7 @@ export PULSE_VERSION=vX.Y.Z curl -fsSLO "https://github.com/rcourtman/Pulse/releases/download/${PULSE_VERSION}/install.sh" curl -fsSLO "https://github.com/rcourtman/Pulse/releases/download/${PULSE_VERSION}/install.sh.sshsig" ssh-keygen -Y verify \ - -f <(printf '%s\n' 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDs21c5oPk2khrdHlsw1aZ9EJKoTsyalGzhb0hdwJrkV pulse-installer') \ + -f <(printf '%s\n' 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMZd/DaH+BldzOkq1A8KVTcFk73nAyrE8aJOyf7i00jm pulse-installer') \ -I pulse-installer \ -n pulse-install \ -s install.sh.sshsig < install.sh diff --git a/scripts/installtests/build_release_assets_test.go b/scripts/installtests/build_release_assets_test.go index 007b16b18..e087583b7 100644 --- a/scripts/installtests/build_release_assets_test.go +++ b/scripts/installtests/build_release_assets_test.go @@ -283,6 +283,45 @@ func TestReleaseValidationRequiresSignedSidecars(t *testing.T) { `Pulse Unified Agent Installer`, `bash "$install_sh_path" --help`, `Install specific version (e.g.`, + // README key drift guard — across v6 rc.2 → rc.5 the README pinned a + // stale ed25519 key that did not verify install.sh.sshsig, so anyone + // following the secure-install path saw "Could not verify signature". + // validate-release.sh must extract the README's pinned key and actually + // run ssh-keygen -Y verify against the signed installer. + `Validating README pinned signature key matches install.sh.sshsig`, + `grep -oE "ssh-ed25519 [A-Za-z0-9+/=]+ pulse-installer" "$readme_path"`, + `ssh-keygen -Y verify \`, + `README's pinned signature key does not verify install.sh.sshsig`, + } + + readmeBytes, err := os.ReadFile(repoFile("README.md")) + if err != nil { + t.Fatalf("read README.md: %v", err) + } + readme := string(readmeBytes) + // Lock in the actual signing key documented to customers. This is the public + // counterpart of PULSE_UPDATE_SIGNING_KEY and matches what install.sh and + // scripts/pulse-auto-update.sh have embedded. A future edit cannot silently + // regress to the stale Ds21c5 key without tripping this assertion. + const correctReadmeKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMZd/DaH+BldzOkq1A8KVTcFk73nAyrE8aJOyf7i00jm pulse-installer" + if !strings.Contains(readme, correctReadmeKey) { + t.Fatalf("README.md must pin the correct pulse-installer ed25519 key for install.sh signature verification") + } + const staleReadmeKey = "Ds21c5oPk2khrdHlsw1aZ9EJKoTsyalGzhb0hdwJrkV" + if strings.Contains(readme, staleReadmeKey) { + t.Fatalf("README.md still references the stale pulse-installer key Ds21c5...; rc.2 → rc.5 shipped this drift") + } + + installDocsBytes, err := os.ReadFile(repoFile("docs", "INSTALL.md")) + if err != nil { + t.Fatalf("read docs/INSTALL.md: %v", err) + } + installDocs := string(installDocsBytes) + if !strings.Contains(installDocs, correctReadmeKey) { + t.Fatalf("docs/INSTALL.md must pin the correct pulse-installer ed25519 key") + } + if strings.Contains(installDocs, staleReadmeKey) { + t.Fatalf("docs/INSTALL.md still references the stale pulse-installer key Ds21c5...") } for _, needle := range localRequired { if !strings.Contains(localValidator, needle) { diff --git a/scripts/validate-release.sh b/scripts/validate-release.sh index c761c2c97..65a26bb23 100755 --- a/scripts/validate-release.sh +++ b/scripts/validate-release.sh @@ -517,6 +517,45 @@ while IFS= read -r line; do done < checksums.txt success "SSH signature sidecars validated" +# Actually run the README's documented verification step against install.sh.sshsig. +# The README ships a hardcoded ed25519 pubkey and tells customers to verify +# install.sh with it before running. Across v6 rc.2 → rc.5 (~20 days) the README +# pinned a stale key (Ds21c5...) that didn't match the actual pipeline signing +# key (MZd/...), so any customer who followed the secure-install path got +# "Could not verify signature" and aborted. This check extracts the README's +# pinned key and runs the exact verification command, so any drift between +# documented key and actual signing key fails the release. +info "Validating README pinned signature key matches install.sh.sshsig..." +readme_path="$(cd "$(dirname "$0")/.." && pwd)/README.md" +if [ ! -f "$readme_path" ]; then + error "README.md not found at $readme_path — cannot validate documented signature key" + exit 1 +fi +readme_signing_key=$(grep -oE "ssh-ed25519 [A-Za-z0-9+/=]+ pulse-installer" "$readme_path" | head -1) +if [ -z "$readme_signing_key" ]; then + error "Could not extract ed25519 pulse-installer key from README.md secure-install snippet" + exit 1 +fi +if ! command -v ssh-keygen >/dev/null 2>&1; then + error "ssh-keygen not found — required to validate the README-documented signature path" + exit 1 +fi +readme_allowed_signers=$(mktemp) +printf 'pulse-installer %s\n' "$readme_signing_key" > "$readme_allowed_signers" +if ! ssh-keygen -Y verify \ + -f "$readme_allowed_signers" \ + -I pulse-installer \ + -n pulse-install \ + -s install.sh.sshsig < install.sh >/dev/null 2>&1; then + rm -f "$readme_allowed_signers" + error "README's pinned signature key does not verify install.sh.sshsig" + error "Customers who follow the README's secure-install ssh-keygen step will see 'Could not verify signature' and abort" + error "Either update README.md/docs/INSTALL.md with the correct pulse-installer pubkey, or fix the release signing key" + exit 1 +fi +rm -f "$readme_allowed_signers" +success "README pinned signature key verifies install.sh.sshsig" + # Validate individual .sha256 files exist and match checksums.txt info "Validating individual .sha256 files..." while IFS= read -r line; do