mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
Fix v6 demo release signing key deployment
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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") + `
|
||||
|
||||
@@ -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"`,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user