Fix v6 demo release signing key deployment

This commit is contained in:
rcourtman
2026-05-05 21:40:14 +01:00
parent 4aa91f6af3
commit d6e96ebeca
11 changed files with 125 additions and 12 deletions
+18
View File
@@ -629,6 +629,24 @@ jobs:
PULSE_UPDATE_SIGNING_KEY: ${{ secrets.PULSE_UPDATE_SIGNING_KEY }}
PULSE_UPDATE_SIGNING_PUBLIC_KEY: ${{ vars.PULSE_UPDATE_SIGNING_PUBLIC_KEY }}
- name: Validate installer signing key pins
env:
PULSE_UPDATE_SIGNING_PUBLIC_KEY: ${{ vars.PULSE_UPDATE_SIGNING_PUBLIC_KEY }}
run: |
set -euo pipefail
TRUSTED_SSH_PUBLIC_KEY="$(
go run ./scripts/release_update_key.go public-key-ssh \
--public-key "${PULSE_UPDATE_SIGNING_PUBLIC_KEY}" \
--comment pulse-installer
)"
for installer in install.sh scripts/pulse-auto-update.sh release/pulse-auto-update.sh; do
grep -F "PINNED_RELEASE_SSH_PUBLIC_KEY=\"${TRUSTED_SSH_PUBLIC_KEY}\"" "${installer}" >/dev/null || {
echo "::error::${installer} does not trust the configured release signing key."
exit 1
}
done
- name: Post-build health check
run: |
if [ -x ./pulse ]; then
+14
View File
@@ -188,6 +188,11 @@ jobs:
fetch-depth: 0
fetch-tags: true
- name: Set up Go
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
with:
go-version-file: go.mod
- name: Wait for release assets
run: |
set -euo pipefail
@@ -223,10 +228,19 @@ jobs:
exit 1
- name: Materialize tagged installer
env:
PULSE_UPDATE_SIGNING_PUBLIC_KEY: ${{ vars.PULSE_UPDATE_SIGNING_PUBLIC_KEY }}
run: |
set -euo pipefail
TAG="${{ needs.resolve.outputs.tag }}"
TRUSTED_SSH_PUBLIC_KEY="$(
go run ./scripts/release_update_key.go public-key-ssh \
--public-key "${PULSE_UPDATE_SIGNING_PUBLIC_KEY}" \
--comment pulse-installer
)"
git show "refs/tags/${TAG}:install.sh" > /tmp/pulse-install.sh
sed -i "s|^PINNED_RELEASE_SSH_PUBLIC_KEY=.*|PINNED_RELEASE_SSH_PUBLIC_KEY=\"${TRUSTED_SSH_PUBLIC_KEY}\"|" /tmp/pulse-install.sh
grep -F "PINNED_RELEASE_SSH_PUBLIC_KEY=\"${TRUSTED_SSH_PUBLIC_KEY}\"" /tmp/pulse-install.sh
chmod +x /tmp/pulse-install.sh
- name: Tailscale
@@ -193,6 +193,13 @@ server-side update execution surfaces.
guard fixes discovered by the release workflow itself, must still be named
in the audit record and reflected in the candidate commit hash, commit
count, and changed-scope summary before the workflow is restarted.
Release workflows and demo-update workflows must derive the OpenSSH
installer trust key from `PULSE_UPDATE_SIGNING_PUBLIC_KEY`, not from a
duplicated hand-copied key. The release workflow must fail before
publication if the repo-root server installer or auto-update script does
not trust that configured signing key, and the demo-update workflow may
patch the derived trust key into an immutable historical tagged installer
copy before executing that installer for an already-published RC.
A metadata-only packet refresh may identify the last validation commit that
introduced release risk separately from the packet-refresh commit itself,
but it must make that distinction explicit in the release notes and audit
+1 -1
View File
@@ -88,7 +88,7 @@ GITHUB_REPO="rcourtman/Pulse"
DOCKER_IMAGE_REPO="${DOCKER_IMAGE_REPO:-rcourtman/pulse}"
INSTALL_SIGNATURE_IDENTITY="pulse-installer"
INSTALL_SIGNATURE_NAMESPACE="pulse-install"
PINNED_RELEASE_SSH_PUBLIC_KEY="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDs21c5oPk2khrdHlsw1aZ9EJKoTsyalGzhb0hdwJrkV pulse-installer"
PINNED_RELEASE_SSH_PUBLIC_KEY="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMZd/DaH+BldzOkq1A8KVTcFk73nAyrE8aJOyf7i00jm pulse-installer"
BUILD_FROM_SOURCE=false
SKIP_DOWNLOAD=false
IN_CONTAINER=false
+14 -1
View File
@@ -63,7 +63,20 @@ func AuthorizedPublicKeyString(privateKey ed25519.PrivateKey, comment string) (s
if len(privateKey) != ed25519.PrivateKeySize {
return "", errors.New("invalid signing key")
}
publicKey, err := ssh.NewPublicKey(privateKey.Public())
ed25519PublicKey, ok := privateKey.Public().(ed25519.PublicKey)
if !ok {
return "", errors.New("failed to derive public key")
}
return AuthorizedPublicKeyStringFromPublicKey(ed25519PublicKey, comment)
}
// AuthorizedPublicKeyStringFromPublicKey returns the OpenSSH authorized_keys
// line for a raw Ed25519 public key.
func AuthorizedPublicKeyStringFromPublicKey(key ed25519.PublicKey, comment string) (string, error) {
if len(key) != ed25519.PublicKeySize {
return "", errors.New("invalid public key")
}
publicKey, err := ssh.NewPublicKey(key)
if err != nil {
return "", fmt.Errorf("derive ssh public key: %w", err)
}
@@ -10,6 +10,8 @@ import (
"path/filepath"
"strings"
"testing"
"golang.org/x/crypto/ssh"
)
func TestBuildReleaseUsesV6InstallScripts(t *testing.T) {
@@ -418,6 +420,10 @@ func TestReleaseWorkflowsUseSecretSafeAttestedImageBuilds(t *testing.T) {
`pulse_license_public_key=${{ secrets.PULSE_LICENSE_PUBLIC_KEY }}`,
`pulse_update_signing_key=${{ secrets.PULSE_UPDATE_SIGNING_KEY }}`,
`PULSE_UPDATE_SIGNING_PUBLIC_KEY: ${{ vars.PULSE_UPDATE_SIGNING_PUBLIC_KEY }}`,
`Validate installer signing key pins`,
`go run ./scripts/release_update_key.go public-key-ssh`,
`install.sh scripts/pulse-auto-update.sh release/pulse-auto-update.sh`,
`does not trust the configured release signing key.`,
`DOCKER_BUILDKIT: 1`,
`--secret id=pulse_license_public_key,env=PULSE_LICENSE_PUBLIC_KEY`,
`--secret id=pulse_update_signing_key,env=PULSE_UPDATE_SIGNING_KEY`,
@@ -564,6 +570,9 @@ func TestUpdateDemoWorkflowUsesGovernedNetworkPath(t *testing.T) {
`- name: Tailscale`,
`uses: tailscale/github-action@4e4c49acaa9818630ce0bd7a564372c17e33fb4d # v2`,
`authkey: ${{ secrets.TS_AUTHKEY }}`,
`uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5`,
`go run ./scripts/release_update_key.go public-key-ssh`,
`sed -i "s|^PINNED_RELEASE_SSH_PUBLIC_KEY=.*|PINNED_RELEASE_SSH_PUBLIC_KEY=\"${TRUSTED_SSH_PUBLIC_KEY}\"|" /tmp/pulse-install.sh`,
`Verify target host identity`,
`Demo environment points at host $REMOTE_HOSTNAME but expected $DEMO_EXPECTED_HOSTNAME.`,
`Verify public browser smoke`,
@@ -656,6 +665,29 @@ func TestReleaseUpdateKeyFingerprintUsesCanonicalRawPublicKeyHash(t *testing.T)
}
}
func TestReleaseUpdateKeyPublicKeySSHAcceptsPublicKey(t *testing.T) {
publicKey, _, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatalf("generate signing key: %v", err)
}
cmd := exec.Command("go", "run", "./scripts/release_update_key.go", "public-key-ssh", "--public-key", base64.StdEncoding.EncodeToString(publicKey), "--comment", "pulse-installer")
cmd.Dir = repoFile()
output, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("release_update_key.go public-key-ssh failed: %v\n%s", err, output)
}
sshPublicKey, err := ssh.NewPublicKey(publicKey)
if err != nil {
t.Fatalf("derive SSH public key: %v", err)
}
expected := strings.TrimSpace(string(ssh.MarshalAuthorizedKey(sshPublicKey))) + " pulse-installer"
if got := strings.TrimSpace(string(output)); got != expected {
t.Fatalf("SSH public key mismatch: got %q want %q", got, expected)
}
}
func TestReleaseAssetCommonRunsUpdateKeyThroughModulePath(t *testing.T) {
if _, err := exec.LookPath("bash"); err != nil {
t.Skip("bash not installed")
+2 -2
View File
@@ -1984,7 +1984,7 @@ exit 0
print_info() { :; }
INSTALL_SIGNATURE_IDENTITY="pulse-installer"
INSTALL_SIGNATURE_NAMESPACE="pulse-install"
PINNED_RELEASE_SSH_PUBLIC_KEY="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDs21c5oPk2khrdHlsw1aZ9EJKoTsyalGzhb0hdwJrkV pulse-installer"
PINNED_RELEASE_SSH_PUBLIC_KEY="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMZd/DaH+BldzOkq1A8KVTcFk73nAyrE8aJOyf7i00jm pulse-installer"
` + extractRootInstallShellFunction(t, "release_signature_key_available") + `
` + extractRootInstallShellFunction(t, "require_release_signature_verifier") + `
` + extractRootInstallShellFunction(t, "verify_release_signature") + `
@@ -2074,7 +2074,7 @@ esac
systemctl() { return 0; }
INSTALL_SIGNATURE_IDENTITY="pulse-installer"
INSTALL_SIGNATURE_NAMESPACE="pulse-install"
PINNED_RELEASE_SSH_PUBLIC_KEY="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDs21c5oPk2khrdHlsw1aZ9EJKoTsyalGzhb0hdwJrkV pulse-installer"
PINNED_RELEASE_SSH_PUBLIC_KEY="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMZd/DaH+BldzOkq1A8KVTcFk73nAyrE8aJOyf7i00jm pulse-installer"
` + extractAutoUpdateFunction(t, "release_signature_key_available") + `
` + extractAutoUpdateFunction(t, "require_release_signature_verifier") + `
` + extractAutoUpdateFunction(t, "verify_release_signature") + `
+2 -2
View File
@@ -376,7 +376,7 @@ func TestRootInstallScriptRequiresSignedReleaseDownloads(t *testing.T) {
script := string(content)
required := []string{
`PINNED_RELEASE_SSH_PUBLIC_KEY="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDs21c5oPk2khrdHlsw1aZ9EJKoTsyalGzhb0hdwJrkV pulse-installer"`,
`PINNED_RELEASE_SSH_PUBLIC_KEY="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMZd/DaH+BldzOkq1A8KVTcFk73nAyrE8aJOyf7i00jm pulse-installer"`,
`require_release_signature_verifier() {`,
`verify_release_signature() {`,
`local signature_url="${download_url}.sshsig"`,
@@ -427,7 +427,7 @@ func TestPulseAutoUpdateScriptRequiresSignedInstallerDownloads(t *testing.T) {
script := string(content)
required := []string{
`PINNED_RELEASE_SSH_PUBLIC_KEY="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDs21c5oPk2khrdHlsw1aZ9EJKoTsyalGzhb0hdwJrkV pulse-installer"`,
`PINNED_RELEASE_SSH_PUBLIC_KEY="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMZd/DaH+BldzOkq1A8KVTcFk73nAyrE8aJOyf7i00jm pulse-installer"`,
`require_release_signature_verifier() {`,
`verify_release_signature() {`,
`local install_signature_url="${install_script_url}.sshsig"`,
+1 -1
View File
@@ -16,7 +16,7 @@ LOG_TAG="${PULSE_AUTO_UPDATE_LOG_TAG:-${SERVICE_NAME}-auto-update}"
MAX_LOG_SIZE=10485760 # 10MB
INSTALL_SIGNATURE_IDENTITY="pulse-installer"
INSTALL_SIGNATURE_NAMESPACE="pulse-install"
PINNED_RELEASE_SSH_PUBLIC_KEY="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDs21c5oPk2khrdHlsw1aZ9EJKoTsyalGzhb0hdwJrkV pulse-installer"
PINNED_RELEASE_SSH_PUBLIC_KEY="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMZd/DaH+BldzOkq1A8KVTcFk73nAyrE8aJOyf7i00jm pulse-installer"
# Logging function
log() {
@@ -337,6 +337,7 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
def test_release_workflow_enforces_rc_lineage_soak_and_v5_notice(self) -> None:
content = read(".github/workflows/create-release.yml")
update_demo_workflow = read(".github/workflows/update-demo-server.yml")
validation_workflow = read(".github/workflows/validate-release-assets.yml")
helper = read("scripts/trigger-release.sh")
renderer = read("scripts/release_control/render_release_body.py")
@@ -410,6 +411,12 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
self.assertIn("PULSE_UPDATE_SIGNING_KEY: ${{ secrets.PULSE_UPDATE_SIGNING_KEY }}", content)
self.assertIn("PULSE_UPDATE_SIGNING_PUBLIC_KEY: ${{ vars.PULSE_UPDATE_SIGNING_PUBLIC_KEY }}", content)
self.assertIn("PULSE_UPDATE_SIGNING_PUBLIC_KEY=${{ vars.PULSE_UPDATE_SIGNING_PUBLIC_KEY }}", content)
self.assertIn("Validate installer signing key pins", content)
self.assertIn("go run ./scripts/release_update_key.go public-key-ssh", content)
self.assertIn("does not trust the configured release signing key", content)
self.assertIn("TRUSTED_SSH_PUBLIC_KEY", update_demo_workflow)
self.assertIn('sed -i "s|^PINNED_RELEASE_SSH_PUBLIC_KEY=.*|PINNED_RELEASE_SSH_PUBLIC_KEY=\\"${TRUSTED_SSH_PUBLIC_KEY}\\"|" /tmp/pulse-install.sh', update_demo_workflow)
self.assertIn("derive the OpenSSH installer trust key from `PULSE_UPDATE_SIGNING_PUBLIC_KEY`", normalize_ws(contract))
self.assertIn('SYFT_VERSION="1.42.4"', content)
self.assertIn('SYFT_ARCHIVE="syft_${SYFT_VERSION}_linux_amd64.tar.gz"', content)
self.assertIn('SYFT_SHA256="590650c2743b83f327d1bf9bec64f6f83b7fec504187bb84f500c862bf8f2a0f"', content)
+27 -5
View File
@@ -17,7 +17,7 @@ import (
func usage() {
fmt.Fprintln(os.Stderr, "usage:")
fmt.Fprintln(os.Stderr, " release_update_key.go public-key --private-key <base64-ed25519-key-or-seed>")
fmt.Fprintln(os.Stderr, " release_update_key.go public-key-ssh --private-key <base64-ed25519-key-or-seed> [--comment <comment>]")
fmt.Fprintln(os.Stderr, " release_update_key.go public-key-ssh (--private-key <base64-ed25519-key-or-seed> | --public-key <base64-ed25519-public-key>) [--comment <comment>]")
fmt.Fprintln(os.Stderr, " release_update_key.go openssh-private-key --private-key <base64-ed25519-key-or-seed> [--comment <comment>]")
fmt.Fprintln(os.Stderr, " release_update_key.go fingerprint (--private-key <base64-ed25519-key-or-seed> | --public-key <base64-ed25519-public-key>)")
fmt.Fprintln(os.Stderr, " release_update_key.go sign --private-key <base64-ed25519-key-or-seed> --file <path>")
@@ -47,14 +47,36 @@ func main() {
case "public-key-ssh":
publicKeyCmd := flag.NewFlagSet("public-key-ssh", flag.ExitOnError)
privateKey := publicKeyCmd.String("private-key", "", "base64-encoded Ed25519 private key or seed")
publicKey := publicKeyCmd.String("public-key", "", "base64-encoded Ed25519 public key or PKIX public key")
comment := publicKeyCmd.String("comment", "", "optional SSH key comment")
_ = publicKeyCmd.Parse(os.Args[2:])
key, err := updatesignature.DecodePrivateKey(*privateKey)
if err != nil {
fail(err)
if (*privateKey == "") == (*publicKey == "") {
usage()
}
encoded, err := updatesignature.AuthorizedPublicKeyString(key, *comment)
var (
derivedPublicKey ed25519.PublicKey
err error
)
if *privateKey != "" {
key, decodeErr := updatesignature.DecodePrivateKey(*privateKey)
if decodeErr != nil {
fail(decodeErr)
}
var ok bool
derivedPublicKey, ok = key.Public().(ed25519.PublicKey)
if !ok {
fail(fmt.Errorf("failed to derive Ed25519 public key"))
}
} else {
derivedPublicKey, err = decodePublicKey(*publicKey)
if err != nil {
fail(err)
}
}
encoded, err := updatesignature.AuthorizedPublicKeyStringFromPublicKey(derivedPublicKey, *comment)
if err != nil {
fail(err)
}