mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-07-09 16:00:59 +00:00
Guard forward release signing against trust-root drift
This commit is contained in:
parent
df04b9b1ff
commit
f58840e8a8
11 changed files with 207 additions and 6 deletions
|
|
@ -52,6 +52,7 @@ jobs:
|
|||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
PULSE_UPDATE_SIGNING_KEY: ${{ secrets.PULSE_UPDATE_SIGNING_KEY }}
|
||||
PULSE_UPDATE_SIGNING_PUBLIC_KEY: ${{ vars.PULSE_UPDATE_SIGNING_PUBLIC_KEY }}
|
||||
run: |
|
||||
./scripts/backfill-release-assets.sh --tag "${{ inputs.tag }}" --repo "${{ github.repository }}"
|
||||
|
||||
|
|
|
|||
6
.github/workflows/create-release.yml
vendored
6
.github/workflows/create-release.yml
vendored
|
|
@ -302,6 +302,7 @@ jobs:
|
|||
cache-to: type=registry,ref=ghcr.io/${{ github.repository_owner }}/pulse:buildcache,mode=max
|
||||
build-args: |
|
||||
VERSION=${{ needs.prepare.outputs.tag }}
|
||||
PULSE_UPDATE_SIGNING_PUBLIC_KEY=${{ vars.PULSE_UPDATE_SIGNING_PUBLIC_KEY }}
|
||||
secrets: |
|
||||
pulse_license_public_key=${{ secrets.PULSE_LICENSE_PUBLIC_KEY }}
|
||||
pulse_update_signing_key=${{ secrets.PULSE_UPDATE_SIGNING_KEY }}
|
||||
|
|
@ -320,6 +321,7 @@ jobs:
|
|||
cache-to: type=registry,ref=ghcr.io/${{ github.repository_owner }}/pulse-agent:buildcache,mode=max
|
||||
build-args: |
|
||||
VERSION=${{ needs.prepare.outputs.tag }}
|
||||
PULSE_UPDATE_SIGNING_PUBLIC_KEY=${{ vars.PULSE_UPDATE_SIGNING_PUBLIC_KEY }}
|
||||
secrets: |
|
||||
pulse_license_public_key=${{ secrets.PULSE_LICENSE_PUBLIC_KEY }}
|
||||
pulse_update_signing_key=${{ secrets.PULSE_UPDATE_SIGNING_KEY }}
|
||||
|
|
@ -343,12 +345,14 @@ jobs:
|
|||
DOCKER_BUILDKIT: 1
|
||||
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 }}
|
||||
run: |
|
||||
docker build \
|
||||
--target runtime \
|
||||
--secret id=pulse_license_public_key,env=PULSE_LICENSE_PUBLIC_KEY \
|
||||
--secret id=pulse_update_signing_key,env=PULSE_UPDATE_SIGNING_KEY \
|
||||
--build-arg VERSION="${{ needs.prepare.outputs.tag }}" \
|
||||
--build-arg PULSE_UPDATE_SIGNING_PUBLIC_KEY="${PULSE_UPDATE_SIGNING_PUBLIC_KEY}" \
|
||||
-t pulse-helm-smoke:${{ needs.prepare.outputs.version }} \
|
||||
.
|
||||
|
||||
|
|
@ -605,6 +609,7 @@ jobs:
|
|||
env:
|
||||
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 }}
|
||||
|
||||
- name: Post-build health check
|
||||
run: |
|
||||
|
|
@ -899,6 +904,7 @@ jobs:
|
|||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
PULSE_UPDATE_SIGNING_KEY: ${{ secrets.PULSE_UPDATE_SIGNING_KEY }}
|
||||
PULSE_UPDATE_SIGNING_PUBLIC_KEY: ${{ vars.PULSE_UPDATE_SIGNING_PUBLIC_KEY }}
|
||||
run: |
|
||||
./scripts/backfill-release-assets.sh --tag "${{ needs.prepare.outputs.tag }}" --repo "${{ github.repository }}"
|
||||
|
||||
|
|
|
|||
2
.github/workflows/publish-docker.yml
vendored
2
.github/workflows/publish-docker.yml
vendored
|
|
@ -140,6 +140,7 @@ jobs:
|
|||
cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/pulse:buildcache
|
||||
build-args: |
|
||||
VERSION=${{ steps.version.outputs.tag }}
|
||||
PULSE_UPDATE_SIGNING_PUBLIC_KEY=${{ vars.PULSE_UPDATE_SIGNING_PUBLIC_KEY }}
|
||||
secrets: |
|
||||
pulse_license_public_key=${{ secrets.PULSE_LICENSE_PUBLIC_KEY }}
|
||||
pulse_update_signing_key=${{ secrets.PULSE_UPDATE_SIGNING_KEY }}
|
||||
|
|
@ -181,6 +182,7 @@ jobs:
|
|||
cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/pulse-agent:buildcache
|
||||
build-args: |
|
||||
VERSION=${{ steps.version.outputs.tag }}
|
||||
PULSE_UPDATE_SIGNING_PUBLIC_KEY=${{ vars.PULSE_UPDATE_SIGNING_PUBLIC_KEY }}
|
||||
secrets: |
|
||||
pulse_license_public_key=${{ secrets.PULSE_LICENSE_PUBLIC_KEY }}
|
||||
pulse_update_signing_key=${{ secrets.PULSE_UPDATE_SIGNING_KEY }}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ FROM --platform=linux/amd64 golang:1.25.9-alpine@sha256:5caaf1cca9dc351e13deafbc
|
|||
|
||||
ARG BUILD_AGENT
|
||||
ARG VERSION
|
||||
ARG PULSE_UPDATE_SIGNING_PUBLIC_KEY
|
||||
WORKDIR /app
|
||||
|
||||
# Install build dependencies
|
||||
|
|
@ -68,6 +69,8 @@ RUN --mount=type=cache,id=pulse-go-mod,target=/go/pkg/mod \
|
|||
if [ -f /run/secrets/pulse_license_public_key ]; then LICENSE_PUBLIC_KEY="$(tr -d '\r\n' < /run/secrets/pulse_license_public_key)"; fi && \
|
||||
if [ -f /run/secrets/pulse_update_signing_key ]; then UPDATE_SIGNING_KEY="$(tr -d '\r\n' < /run/secrets/pulse_update_signing_key)"; fi && \
|
||||
if [ -n "${UPDATE_SIGNING_KEY}" ]; then UPDATE_PUBLIC_KEYS="$(go run ./scripts/release_update_key.go public-key --private-key "${UPDATE_SIGNING_KEY}")"; fi && \
|
||||
if [ -n "${PULSE_UPDATE_SIGNING_PUBLIC_KEY:-}" ] && [ -z "${UPDATE_PUBLIC_KEYS}" ]; then echo "Error: PULSE_UPDATE_SIGNING_PUBLIC_KEY was provided but no update signing key was mounted." >&2; exit 1; fi && \
|
||||
if [ -n "${PULSE_UPDATE_SIGNING_PUBLIC_KEY:-}" ] && [ "${UPDATE_PUBLIC_KEYS}" != "${PULSE_UPDATE_SIGNING_PUBLIC_KEY}" ]; then echo "Error: mounted update signing key does not match PULSE_UPDATE_SIGNING_PUBLIC_KEY." >&2; echo "Expected public key: ${PULSE_UPDATE_SIGNING_PUBLIC_KEY}" >&2; echo "Actual public key: ${UPDATE_PUBLIC_KEYS}" >&2; exit 1; fi && \
|
||||
SERVER_LDFLAGS="$(./scripts/release_ldflags.sh server --version "${VERSION}" --build-time "${BUILD_TIME}" --git-commit "${GIT_COMMIT}" $(if [ -n "${LICENSE_PUBLIC_KEY}" ]; then printf '%s %s' --license-public-key "${LICENSE_PUBLIC_KEY}"; fi) $(if [ -n "${UPDATE_PUBLIC_KEYS}" ]; then printf '%s %s' --update-public-keys "${UPDATE_PUBLIC_KEYS}"; fi))" && \
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
|
||||
-tags release \
|
||||
|
|
@ -91,6 +94,8 @@ RUN --mount=type=cache,id=pulse-go-mod,target=/go/pkg/mod \
|
|||
UPDATE_PUBLIC_KEYS="" && \
|
||||
if [ -f /run/secrets/pulse_update_signing_key ]; then UPDATE_SIGNING_KEY="$(tr -d '\r\n' < /run/secrets/pulse_update_signing_key)"; fi && \
|
||||
if [ -n "${UPDATE_SIGNING_KEY}" ]; then UPDATE_PUBLIC_KEYS="$(go run ./scripts/release_update_key.go public-key --private-key "${UPDATE_SIGNING_KEY}")"; fi && \
|
||||
if [ -n "${PULSE_UPDATE_SIGNING_PUBLIC_KEY:-}" ] && [ -z "${UPDATE_PUBLIC_KEYS}" ]; then echo "Error: PULSE_UPDATE_SIGNING_PUBLIC_KEY was provided but no update signing key was mounted." >&2; exit 1; fi && \
|
||||
if [ -n "${PULSE_UPDATE_SIGNING_PUBLIC_KEY:-}" ] && [ "${UPDATE_PUBLIC_KEYS}" != "${PULSE_UPDATE_SIGNING_PUBLIC_KEY}" ]; then echo "Error: mounted update signing key does not match PULSE_UPDATE_SIGNING_PUBLIC_KEY." >&2; echo "Expected public key: ${PULSE_UPDATE_SIGNING_PUBLIC_KEY}" >&2; echo "Actual public key: ${UPDATE_PUBLIC_KEYS}" >&2; exit 1; fi && \
|
||||
AGENT_LDFLAGS="$(./scripts/release_ldflags.sh agent --version "${VERSION}" $(if [ -n "${UPDATE_PUBLIC_KEYS}" ]; then printf '%s %s' --update-public-keys "${UPDATE_PUBLIC_KEYS}"; fi))" && \
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
|
||||
-ldflags="${AGENT_LDFLAGS}" \
|
||||
|
|
@ -147,7 +152,11 @@ RUN --mount=type=cache,id=pulse-go-mod,target=/go/pkg/mod \
|
|||
mkdir -p /app/rendered-installers && \
|
||||
UPDATE_SIGNING_KEY="" && \
|
||||
INSTALLER_SSH_PUBLIC_KEY="" && \
|
||||
UPDATE_PUBLIC_KEYS="" && \
|
||||
if [ -f /run/secrets/pulse_update_signing_key ]; then UPDATE_SIGNING_KEY="$(tr -d '\r\n' < /run/secrets/pulse_update_signing_key)"; fi && \
|
||||
if [ -n "${UPDATE_SIGNING_KEY}" ]; then UPDATE_PUBLIC_KEYS="$(go run ./scripts/release_update_key.go public-key --private-key "${UPDATE_SIGNING_KEY}")"; fi && \
|
||||
if [ -n "${PULSE_UPDATE_SIGNING_PUBLIC_KEY:-}" ] && [ -z "${UPDATE_PUBLIC_KEYS}" ]; then echo "Error: PULSE_UPDATE_SIGNING_PUBLIC_KEY was provided but no update signing key was mounted." >&2; exit 1; fi && \
|
||||
if [ -n "${PULSE_UPDATE_SIGNING_PUBLIC_KEY:-}" ] && [ "${UPDATE_PUBLIC_KEYS}" != "${PULSE_UPDATE_SIGNING_PUBLIC_KEY}" ]; then echo "Error: mounted update signing key does not match PULSE_UPDATE_SIGNING_PUBLIC_KEY." >&2; echo "Expected public key: ${PULSE_UPDATE_SIGNING_PUBLIC_KEY}" >&2; echo "Actual public key: ${UPDATE_PUBLIC_KEYS}" >&2; exit 1; fi && \
|
||||
if [ -n "${UPDATE_SIGNING_KEY}" ]; then INSTALLER_SSH_PUBLIC_KEY="$(go run ./scripts/release_update_key.go public-key-ssh --private-key "${UPDATE_SIGNING_KEY}" --comment pulse-installer)"; fi && \
|
||||
go run ./scripts/render_installers.go --source-dir ./scripts --output-dir /app/rendered-installers --installer-ssh-public-key "${INSTALLER_SSH_PUBLIC_KEY}" && \
|
||||
if [ -n "${UPDATE_SIGNING_KEY}" ]; then \
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ Use this as the final gate before cutting a Pulse v6 pre-release.
|
|||
|
||||
## Promotion Policy
|
||||
- [x] Record the previous stable tag and exact rollback pin command before publishing a new prerelease or stable release.
|
||||
- [ ] For any prerelease or stable publication, confirm the repo variable `PULSE_UPDATE_SIGNING_PUBLIC_KEY` is set to the intended active update signer public key and that the release workflows are consuming it alongside `PULSE_UPDATE_SIGNING_KEY`, so accidental trust-root rotation fails closed before publication.
|
||||
- [ ] For the GA/stable candidate, confirm the release pipeline has already been exercised on a real prerelease tag, not only linted or YAML-parsed.
|
||||
- [x] For stable promotion, confirm the candidate commit has already shipped on `rc`.
|
||||
- [x] For stable promotion, confirm the chosen `promoted_from_tag` is a prerelease that was actually published through the governed prerelease path, not an accidental git tag.
|
||||
|
|
|
|||
|
|
@ -8,21 +8,24 @@
|
|||
|
||||
1. The latest shipped Pulse v6 prerelease tag is `v6.0.0-rc.2`.
|
||||
2. That shipped prerelease tag resolves to commit `7b0bdebdff8127228708e84eb836cfd3d7214c08`.
|
||||
3. The governed release profile in `docs/release-control/control_plane.json`
|
||||
3. The selected remote ref `origin/pulse/v6-release` is still behind the current
|
||||
local governed branch state, so `Release Dry Run` would exercise stale remote
|
||||
control-plane metadata instead of the intended candidate.
|
||||
4. The governed release profile in `docs/release-control/control_plane.json`
|
||||
currently declares both `prerelease_branch` and `stable_branch` as
|
||||
`pulse/v6-release`.
|
||||
4. The active control-plane target is still `v6-product-lane-expansion`, not
|
||||
5. The active control-plane target is still `v6-product-lane-expansion`, not
|
||||
`v6-ga-promotion`.
|
||||
5. The active local `pulse/v6-release` branch currently reports `VERSION=6.0.0`, so a
|
||||
6. The active local `pulse/v6-release` branch currently reports `VERSION=6.0.0`, so a
|
||||
local GA candidate exists on the governed stable line.
|
||||
6. There is still no governed `Prerelease-to-GA Rehearsal Record` proving a successful
|
||||
7. There is still no governed `Prerelease-to-GA Rehearsal Record` proving a successful
|
||||
non-publish `Release Dry Run` for the current `6.0.0` candidate.
|
||||
7. `docs/releases/RELEASE_NOTES_v6.md` and
|
||||
8. `docs/releases/RELEASE_NOTES_v6.md` and
|
||||
`docs/release-control/v6/internal/V5_MAINTENANCE_SUPPORT_POLICY.md` now carry the
|
||||
currently proposed exact dates for the eventual GA notice:
|
||||
- `v6` GA date: `2026-04-20`
|
||||
- `v5` end-of-support date: `2026-07-19`
|
||||
8. There is still no governed `Release Dry Run` artifact or rehearsal record
|
||||
9. There is still no governed `Release Dry Run` artifact or rehearsal record
|
||||
exercising stable inputs for:
|
||||
- `version=6.0.0`
|
||||
- `promoted_from_tag=v6.0.0-rc.2`
|
||||
|
|
|
|||
|
|
@ -221,6 +221,11 @@ server-side update execution surfaces.
|
|||
Whenever that policy changes, update the owning workflow/install proof files
|
||||
in `scripts/installtests/build_release_assets_test.go` and
|
||||
`scripts/release_control/release_promotion_policy_*` in the same slice.
|
||||
10. Keep forward release signing pinned to an explicit trust root. Governed
|
||||
release scripts, Docker release builds, and historical backfill paths must
|
||||
accept the active private signing key only alongside a non-secret expected
|
||||
public key or equivalent pinned identity, and they must fail closed before
|
||||
publication if the signer drifts from that expected trust root.
|
||||
|
||||
## Current State
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
package installtests
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
|
@ -66,9 +71,13 @@ func TestBuildReleaseUsesV6InstallScripts(t *testing.T) {
|
|||
`: "${PULSE_REPO_ROOT:=$(cd "${PULSE_SCRIPTS_DIR}/.." && pwd)}"`,
|
||||
`go -C "${PULSE_REPO_ROOT}" run "${PULSE_SCRIPTS_DIR}/release_update_key.go" "$@"`,
|
||||
`pulse_release_go_run_update_key public-key --private-key "${PULSE_UPDATE_SIGNING_KEY}"`,
|
||||
`pulse_release_go_run_update_key fingerprint --public-key "${PULSE_RELEASE_UPDATE_PUBLIC_KEY}"`,
|
||||
`pulse_release_go_run_update_key public-key-ssh --private-key "${PULSE_UPDATE_SIGNING_KEY}"`,
|
||||
`pulse_release_go_run_update_key openssh-private-key --private-key "${PULSE_UPDATE_SIGNING_KEY}"`,
|
||||
`pulse_release_go_run_update_key sign --private-key "${PULSE_UPDATE_SIGNING_KEY}" --file "${absolute_file}"`,
|
||||
`PULSE_UPDATE_SIGNING_PUBLIC_KEY`,
|
||||
`PULSE_UPDATE_SIGNING_PUBLIC_KEY_FINGERPRINT`,
|
||||
`Verified update signing public key fingerprint: ${PULSE_RELEASE_UPDATE_PUBLIC_KEY_FINGERPRINT}`,
|
||||
`ssh-keygen -q -Y sign`,
|
||||
`"${resolved_tool}" "dir:${release_dir}" -o "spdx-json=${tmp_sbom}"`,
|
||||
`if compgen -G "pulse-*.sbom.spdx.json" > /dev/null; then`,
|
||||
|
|
@ -184,6 +193,7 @@ func TestBackfillReleaseWorkflowRepairsPublishedAssetsWithoutRebuilds(t *testing
|
|||
`SYFT_SHA256="590650c2743b83f327d1bf9bec64f6f83b7fec504187bb84f500c862bf8f2a0f"`,
|
||||
`./scripts/backfill-release-assets.sh --tag "${{ inputs.tag }}" --repo "${{ github.repository }}"`,
|
||||
`PULSE_UPDATE_SIGNING_KEY: ${{ secrets.PULSE_UPDATE_SIGNING_KEY }}`,
|
||||
`PULSE_UPDATE_SIGNING_PUBLIC_KEY: ${{ vars.PULSE_UPDATE_SIGNING_PUBLIC_KEY }}`,
|
||||
`./scripts/validate-published-release.sh "${{ inputs.tag }}" "${{ github.repository }}"`,
|
||||
}
|
||||
for _, needle := range workflowRequired {
|
||||
|
|
@ -284,8 +294,10 @@ func TestDockerAndDemoBuildsUseCanonicalReleaseLdflags(t *testing.T) {
|
|||
`COPY scripts/render_installers.go ./scripts/render_installers.go`,
|
||||
`--mount=type=secret,id=pulse_license_public_key,required=false`,
|
||||
`--mount=type=secret,id=pulse_update_signing_key,required=false`,
|
||||
`ARG PULSE_UPDATE_SIGNING_PUBLIC_KEY`,
|
||||
`LICENSE_PUBLIC_KEY="$(tr -d '\r\n' < /run/secrets/pulse_license_public_key)"`,
|
||||
`UPDATE_PUBLIC_KEYS="$(go run ./scripts/release_update_key.go public-key --private-key "${UPDATE_SIGNING_KEY}")"`,
|
||||
`mounted update signing key does not match PULSE_UPDATE_SIGNING_PUBLIC_KEY.`,
|
||||
`./scripts/release_ldflags.sh server --version "${VERSION}" --build-time "${BUILD_TIME}" --git-commit "${GIT_COMMIT}"`,
|
||||
`./scripts/release_ldflags.sh agent --version "${VERSION}"`,
|
||||
`go run ./scripts/render_installers.go --source-dir ./scripts --output-dir /app/rendered-installers`,
|
||||
|
|
@ -334,11 +346,14 @@ func TestReleaseWorkflowsUseSecretSafeAttestedImageBuilds(t *testing.T) {
|
|||
`provenance: mode=max`,
|
||||
`sbom: true`,
|
||||
`secrets: |`,
|
||||
`PULSE_UPDATE_SIGNING_PUBLIC_KEY=${{ vars.PULSE_UPDATE_SIGNING_PUBLIC_KEY }}`,
|
||||
`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 }}`,
|
||||
`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`,
|
||||
`--build-arg PULSE_UPDATE_SIGNING_PUBLIC_KEY="${PULSE_UPDATE_SIGNING_PUBLIC_KEY}"`,
|
||||
`id-token: write`,
|
||||
`attestations: write`,
|
||||
`uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4`,
|
||||
|
|
@ -361,6 +376,7 @@ func TestReleaseWorkflowsUseSecretSafeAttestedImageBuilds(t *testing.T) {
|
|||
`provenance: mode=max`,
|
||||
`sbom: true`,
|
||||
`secrets: |`,
|
||||
`PULSE_UPDATE_SIGNING_PUBLIC_KEY=${{ vars.PULSE_UPDATE_SIGNING_PUBLIC_KEY }}`,
|
||||
`pulse_license_public_key=${{ secrets.PULSE_LICENSE_PUBLIC_KEY }}`,
|
||||
`pulse_update_signing_key=${{ secrets.PULSE_UPDATE_SIGNING_KEY }}`,
|
||||
`subject-name: docker.io/rcourtman/pulse`,
|
||||
|
|
@ -549,6 +565,58 @@ func TestDockerfileStagesShippedDocsForEmbeddedFrontendBuild(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestReleaseUpdateKeyFingerprintUsesCanonicalRawPublicKeyHash(t *testing.T) {
|
||||
publicKey, privateKey, 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", "fingerprint", "--private-key", base64.StdEncoding.EncodeToString(privateKey))
|
||||
cmd.Dir = repoFile()
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("release_update_key.go fingerprint failed: %v\n%s", err, output)
|
||||
}
|
||||
|
||||
sum := sha256.Sum256(publicKey)
|
||||
expected := "SHA256:" + base64.StdEncoding.EncodeToString(sum[:])
|
||||
if got := strings.TrimSpace(string(output)); got != expected {
|
||||
t.Fatalf("fingerprint mismatch: got %q want %q", got, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReleaseAssetCommonRejectsUnexpectedUpdateSigningPublicKey(t *testing.T) {
|
||||
if _, err := exec.LookPath("bash"); err != nil {
|
||||
t.Skip("bash not installed")
|
||||
}
|
||||
if _, err := exec.LookPath("go"); err != nil {
|
||||
t.Skip("go not installed")
|
||||
}
|
||||
|
||||
_, privateKey, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatalf("generate signing key: %v", err)
|
||||
}
|
||||
unexpectedPublicKey, _, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatalf("generate unexpected public key: %v", err)
|
||||
}
|
||||
|
||||
cmd := exec.Command("bash", "-lc", "source ./scripts/release_asset_common.sh; pulse_release_prepare_signing_state pulse-installer pulse-install")
|
||||
cmd.Dir = repoFile()
|
||||
cmd.Env = append(os.Environ(),
|
||||
"PULSE_UPDATE_SIGNING_KEY="+base64.StdEncoding.EncodeToString(privateKey),
|
||||
"PULSE_UPDATE_SIGNING_PUBLIC_KEY="+base64.StdEncoding.EncodeToString(unexpectedPublicKey),
|
||||
)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err == nil {
|
||||
t.Fatalf("expected release_asset_common.sh to reject a mismatched signing public key:\n%s", output)
|
||||
}
|
||||
if !strings.Contains(string(output), "does not match PULSE_UPDATE_SIGNING_PUBLIC_KEY") {
|
||||
t.Fatalf("expected mismatched signing public key error, got:\n%s", output)
|
||||
}
|
||||
}
|
||||
|
||||
func repoFile(parts ...string) string {
|
||||
root := filepath.Join("..", "..")
|
||||
segments := append([]string{root}, parts...)
|
||||
|
|
|
|||
|
|
@ -12,10 +12,14 @@ pulse_release_go_run_update_key() {
|
|||
pulse_release_prepare_signing_state() {
|
||||
local signer_identity="${1:-pulse-installer}"
|
||||
local signer_namespace="${2:-pulse-install}"
|
||||
local actual_fingerprint=""
|
||||
local expected_public_key=""
|
||||
local expected_fingerprint=""
|
||||
|
||||
PULSE_RELEASE_SIGNER_IDENTITY="${signer_identity}"
|
||||
PULSE_RELEASE_SIGNER_NAMESPACE="${signer_namespace}"
|
||||
PULSE_RELEASE_UPDATE_PUBLIC_KEY=""
|
||||
PULSE_RELEASE_UPDATE_PUBLIC_KEY_FINGERPRINT=""
|
||||
PULSE_RELEASE_UPDATE_SSH_PUBLIC_KEY=""
|
||||
PULSE_RELEASE_UPDATE_SSH_PRIVATE_KEY_FILE=""
|
||||
|
||||
|
|
@ -35,6 +39,36 @@ pulse_release_prepare_signing_state() {
|
|||
exit 1
|
||||
fi
|
||||
|
||||
PULSE_RELEASE_UPDATE_PUBLIC_KEY_FINGERPRINT="$(pulse_release_go_run_update_key fingerprint --public-key "${PULSE_RELEASE_UPDATE_PUBLIC_KEY}")"
|
||||
if [[ -z "${PULSE_RELEASE_UPDATE_PUBLIC_KEY_FINGERPRINT}" ]]; then
|
||||
echo "Error: failed to derive update signing public key fingerprint." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
expected_public_key="$(printf '%s' "${PULSE_UPDATE_SIGNING_PUBLIC_KEY:-}" | tr -d '\r\n[:space:]')"
|
||||
if [[ -n "${expected_public_key}" && "${PULSE_RELEASE_UPDATE_PUBLIC_KEY}" != "${expected_public_key}" ]]; then
|
||||
echo "Error: PULSE_UPDATE_SIGNING_KEY does not match PULSE_UPDATE_SIGNING_PUBLIC_KEY." >&2
|
||||
echo "Expected public key: ${expected_public_key}" >&2
|
||||
echo "Actual public key: ${PULSE_RELEASE_UPDATE_PUBLIC_KEY}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
expected_fingerprint="$(printf '%s' "${PULSE_UPDATE_SIGNING_PUBLIC_KEY_FINGERPRINT:-}" | tr -d '\r\n[:space:]')"
|
||||
if [[ -n "${expected_fingerprint}" ]]; then
|
||||
expected_fingerprint="${expected_fingerprint#SHA256:}"
|
||||
actual_fingerprint="${PULSE_RELEASE_UPDATE_PUBLIC_KEY_FINGERPRINT#SHA256:}"
|
||||
if [[ "${actual_fingerprint}" != "${expected_fingerprint}" ]]; then
|
||||
echo "Error: PULSE_UPDATE_SIGNING_KEY fingerprint mismatch." >&2
|
||||
echo "Expected: SHA256:${expected_fingerprint}" >&2
|
||||
echo "Actual: ${PULSE_RELEASE_UPDATE_PUBLIC_KEY_FINGERPRINT}" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -n "${expected_public_key}" || -n "${expected_fingerprint}" ]]; then
|
||||
echo "Verified update signing public key fingerprint: ${PULSE_RELEASE_UPDATE_PUBLIC_KEY_FINGERPRINT}"
|
||||
fi
|
||||
|
||||
if ! command -v ssh-keygen >/dev/null 2>&1; then
|
||||
echo "Error: ssh-keygen is required to sign installer release assets." >&2
|
||||
exit 1
|
||||
|
|
|
|||
|
|
@ -277,6 +277,8 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
|
|||
self.assertIn('./scripts/backfill-release-assets.sh --tag "${{ needs.prepare.outputs.tag }}" --repo "${{ github.repository }}"', content)
|
||||
self.assertIn('./scripts/validate-published-release.sh "${{ needs.prepare.outputs.tag }}" "${{ github.repository }}"', content)
|
||||
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('SYFT_VERSION="1.42.4"', content)
|
||||
self.assertIn('SYFT_ARCHIVE="syft_${SYFT_VERSION}_linux_amd64.tar.gz"', content)
|
||||
self.assertIn('SYFT_SHA256="590650c2743b83f327d1bf9bec64f6f83b7fec504187bb84f500c862bf8f2a0f"', content)
|
||||
|
|
@ -306,9 +308,19 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
|
|||
self.assertIn(': "${PULSE_REPO_ROOT:=$(cd "${PULSE_SCRIPTS_DIR}/.." && pwd)}"', release_asset_helper)
|
||||
self.assertIn('go -C "${PULSE_REPO_ROOT}" run "${PULSE_SCRIPTS_DIR}/release_update_key.go" "$@"', release_asset_helper)
|
||||
self.assertIn('pulse_release_go_run_update_key public-key --private-key "${PULSE_UPDATE_SIGNING_KEY}"', release_asset_helper)
|
||||
self.assertIn(
|
||||
'pulse_release_go_run_update_key fingerprint --public-key "${PULSE_RELEASE_UPDATE_PUBLIC_KEY}"',
|
||||
release_asset_helper,
|
||||
)
|
||||
self.assertIn('pulse_release_go_run_update_key public-key-ssh --private-key "${PULSE_UPDATE_SIGNING_KEY}"', release_asset_helper)
|
||||
self.assertIn('pulse_release_go_run_update_key openssh-private-key --private-key "${PULSE_UPDATE_SIGNING_KEY}"', release_asset_helper)
|
||||
self.assertIn('pulse_release_go_run_update_key sign --private-key "${PULSE_UPDATE_SIGNING_KEY}" --file "${absolute_file}"', release_asset_helper)
|
||||
self.assertIn("PULSE_UPDATE_SIGNING_PUBLIC_KEY", release_asset_helper)
|
||||
self.assertIn("PULSE_UPDATE_SIGNING_PUBLIC_KEY_FINGERPRINT", release_asset_helper)
|
||||
self.assertIn(
|
||||
"Verified update signing public key fingerprint: ${PULSE_RELEASE_UPDATE_PUBLIC_KEY_FINGERPRINT}",
|
||||
release_asset_helper,
|
||||
)
|
||||
self.assertIn('"${resolved_tool}" "dir:${release_dir}" -o "spdx-json=${tmp_sbom}"', release_asset_helper)
|
||||
self.assertIn('find . -maxdepth 1 -type f \\( -name \'*.sig\' -o -name \'*.sshsig\' \\) -delete', release_asset_helper)
|
||||
self.assertIn('source "${SCRIPT_DIR}/release_asset_common.sh"', backfill_script)
|
||||
|
|
@ -321,6 +333,7 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
|
|||
self.assertIn('SYFT_VERSION="1.42.4"', backfill_workflow)
|
||||
self.assertIn('./scripts/backfill-release-assets.sh --tag "${{ inputs.tag }}" --repo "${{ github.repository }}"', backfill_workflow)
|
||||
self.assertIn('./scripts/validate-published-release.sh "${{ inputs.tag }}" "${{ github.repository }}"', backfill_workflow)
|
||||
self.assertIn("PULSE_UPDATE_SIGNING_PUBLIC_KEY: ${{ vars.PULSE_UPDATE_SIGNING_PUBLIC_KEY }}", backfill_workflow)
|
||||
self.assertIn("pulse_license_public_key=${{ secrets.PULSE_LICENSE_PUBLIC_KEY }}", content)
|
||||
self.assertIn("pulse_update_signing_key=${{ secrets.PULSE_UPDATE_SIGNING_KEY }}", content)
|
||||
self.assertIn("--secret id=pulse_license_public_key,env=PULSE_LICENSE_PUBLIC_KEY", content)
|
||||
|
|
@ -397,6 +410,7 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
|
|||
self.assertIn("subject-name: ghcr.io/${{ github.repository_owner }}/pulse-agent", publish)
|
||||
self.assertIn("create-storage-record: false", publish)
|
||||
self.assertIn("pulse_license_public_key=${{ secrets.PULSE_LICENSE_PUBLIC_KEY }}", publish)
|
||||
self.assertIn("PULSE_UPDATE_SIGNING_PUBLIC_KEY=${{ vars.PULSE_UPDATE_SIGNING_PUBLIC_KEY }}", publish)
|
||||
self.assertNotIn("provenance: false", publish)
|
||||
self.assertIn("control_plane.py --branch-for-version", promote)
|
||||
self.assertIn("control_plane.py --branch-for-version", demo)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
|
|
@ -13,6 +17,7 @@ func 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 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>")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
|
@ -67,6 +72,39 @@ func main() {
|
|||
fail(err)
|
||||
}
|
||||
fmt.Print(encoded)
|
||||
case "fingerprint":
|
||||
fingerprintCmd := flag.NewFlagSet("fingerprint", flag.ExitOnError)
|
||||
privateKey := fingerprintCmd.String("private-key", "", "base64-encoded Ed25519 private key or seed")
|
||||
publicKey := fingerprintCmd.String("public-key", "", "base64-encoded Ed25519 public key or PKIX public key")
|
||||
_ = fingerprintCmd.Parse(os.Args[2:])
|
||||
|
||||
if (*privateKey == "") == (*publicKey == "") {
|
||||
usage()
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
sum := sha256.Sum256(derivedPublicKey)
|
||||
fmt.Printf("SHA256:%s\n", base64.StdEncoding.EncodeToString(sum[:]))
|
||||
case "sign":
|
||||
signCmd := flag.NewFlagSet("sign", flag.ExitOnError)
|
||||
privateKey := signCmd.String("private-key", "", "base64-encoded Ed25519 private key or seed")
|
||||
|
|
@ -94,3 +132,23 @@ func fail(err error) {
|
|||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func decodePublicKey(encoded string) (ed25519.PublicKey, error) {
|
||||
decoded, err := base64.StdEncoding.DecodeString(encoded)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid base64 public key: %w", err)
|
||||
}
|
||||
if len(decoded) == ed25519.PublicKeySize {
|
||||
return ed25519.PublicKey(decoded), nil
|
||||
}
|
||||
|
||||
publicKey, err := x509.ParsePKIXPublicKey(decoded)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse Ed25519 public key: %w", err)
|
||||
}
|
||||
ed25519PublicKey, ok := publicKey.(ed25519.PublicKey)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("trusted key is not an Ed25519 public key")
|
||||
}
|
||||
return ed25519PublicKey, nil
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue