From 0424c5ae3bbfe86bdacf703bee4510c7e2709c06 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Sat, 7 Feb 2026 14:18:44 +0000 Subject: [PATCH] fix(license): harden release key validation and fingerprint logging (cherry picked from commit f253ed2778c0a85783e9ccc14e0eb42ffb8089af) --- internal/license/pubkey.go | 19 +++++++++++++++-- internal/license/pubkey_test.go | 21 +++++++++++++++++++ scripts/build-release.sh | 37 +++++++++++++++++++++++++++++---- 3 files changed, 71 insertions(+), 6 deletions(-) diff --git a/internal/license/pubkey.go b/internal/license/pubkey.go index d9342bd6a..2aff9113b 100644 --- a/internal/license/pubkey.go +++ b/internal/license/pubkey.go @@ -2,6 +2,7 @@ package license import ( "crypto/ed25519" + "crypto/sha256" "encoding/base64" "os" "strings" @@ -14,6 +15,14 @@ import ( // Example: go build -ldflags "-X github.com/rcourtman/pulse-go-rewrite/internal/license.EmbeddedPublicKey=BASE64_KEY" var EmbeddedPublicKey string = "" +func publicKeyFingerprint(key ed25519.PublicKey) string { + if len(key) == 0 { + return "" + } + sum := sha256.Sum256(key) + return "SHA256:" + base64.StdEncoding.EncodeToString(sum[:]) +} + // InitPublicKey initializes the public key for license validation. // Priority: // 1. PULSE_LICENSE_PUBLIC_KEY environment variable (base64 encoded) @@ -30,7 +39,10 @@ func InitPublicKey() { // Fall through to try embedded key instead of returning } else { SetPublicKey(key) - log.Info().Msg("License public key loaded from environment") + log.Info(). + Str("source", "environment"). + Str("fingerprint", publicKeyFingerprint(key)). + Msg("License public key loaded") return } } @@ -42,7 +54,10 @@ func InitPublicKey() { log.Error().Err(err).Msg("Failed to decode embedded public key") } else { SetPublicKey(key) - log.Info().Msg("License public key loaded from embedded key") + log.Info(). + Str("source", "embedded"). + Str("fingerprint", publicKeyFingerprint(key)). + Msg("License public key loaded") return } } diff --git a/internal/license/pubkey_test.go b/internal/license/pubkey_test.go index b08312b83..dbc3881a6 100644 --- a/internal/license/pubkey_test.go +++ b/internal/license/pubkey_test.go @@ -2,8 +2,10 @@ package license import ( "crypto/ed25519" + "crypto/sha256" "encoding/base64" "os" + "strings" "testing" ) @@ -115,3 +117,22 @@ func TestDecodePublicKey(t *testing.T) { }) } } + +func TestPublicKeyFingerprint(t *testing.T) { + pub, _, _ := ed25519.GenerateKey(nil) + + got := publicKeyFingerprint(pub) + if !strings.HasPrefix(got, "SHA256:") { + t.Fatalf("expected SHA256 prefix, got %q", got) + } + + sum := sha256.Sum256(pub) + want := "SHA256:" + base64.StdEncoding.EncodeToString(sum[:]) + if got != want { + t.Fatalf("fingerprint mismatch: got %q want %q", got, want) + } + + if empty := publicKeyFingerprint(nil); empty != "" { + t.Fatalf("expected empty fingerprint for nil key, got %q", empty) + } +} diff --git a/scripts/build-release.sh b/scripts/build-release.sh index e482909b5..c45e94414 100755 --- a/scripts/build-release.sh +++ b/scripts/build-release.sh @@ -19,12 +19,41 @@ RELEASE_DIR="release" echo "Building Pulse v${VERSION}..." -# Optional public key embedding for license validation +# Require public key embedding for release-grade license validation. +# Explicitly opt out with PULSE_ALLOW_MISSING_LICENSE_KEY=true (not recommended). LICENSE_LDFLAGS="" -if [[ -n "${PULSE_LICENSE_PUBLIC_KEY:-}" ]]; then - LICENSE_LDFLAGS="-X github.com/rcourtman/pulse-go-rewrite/internal/license.EmbeddedPublicKey=${PULSE_LICENSE_PUBLIC_KEY}" +if [[ -z "${PULSE_LICENSE_PUBLIC_KEY:-}" ]]; then + if [[ "${PULSE_ALLOW_MISSING_LICENSE_KEY:-false}" == "true" ]]; then + echo "Warning: PULSE_LICENSE_PUBLIC_KEY not set; continuing because PULSE_ALLOW_MISSING_LICENSE_KEY=true." + else + echo "Error: PULSE_LICENSE_PUBLIC_KEY is required for release builds." >&2 + echo "Set PULSE_ALLOW_MISSING_LICENSE_KEY=true only for local non-release debugging." >&2 + exit 1 + fi else - echo "Warning: PULSE_LICENSE_PUBLIC_KEY not set; Pulse Pro license activation will fail for release binaries." + decoded_key_len=$(printf '%s' "${PULSE_LICENSE_PUBLIC_KEY}" | openssl base64 -d -A 2>/dev/null | wc -c | tr -d ' ') + if [[ "${decoded_key_len}" != "32" ]]; then + echo "Error: PULSE_LICENSE_PUBLIC_KEY must decode to 32 bytes (Ed25519 public key)." >&2 + exit 1 + fi + + if [[ -n "${PULSE_LICENSE_PUBLIC_KEY_FINGERPRINT:-}" ]]; then + expected_fingerprint="${PULSE_LICENSE_PUBLIC_KEY_FINGERPRINT#SHA256:}" + actual_fingerprint=$(printf '%s' "${PULSE_LICENSE_PUBLIC_KEY}" | openssl base64 -d -A 2>/dev/null | openssl dgst -sha256 -binary | openssl base64 -A) + if [[ -z "${actual_fingerprint}" ]]; then + echo "Error: Failed to compute fingerprint for PULSE_LICENSE_PUBLIC_KEY." >&2 + exit 1 + fi + if [[ "${actual_fingerprint}" != "${expected_fingerprint}" ]]; then + echo "Error: PULSE_LICENSE_PUBLIC_KEY fingerprint mismatch." >&2 + echo "Expected: SHA256:${expected_fingerprint}" >&2 + echo "Actual: SHA256:${actual_fingerprint}" >&2 + exit 1 + fi + echo "Verified license public key fingerprint: SHA256:${actual_fingerprint}" + fi + + LICENSE_LDFLAGS="-X github.com/rcourtman/pulse-go-rewrite/internal/license.EmbeddedPublicKey=${PULSE_LICENSE_PUBLIC_KEY}" fi # Clean previous builds