revert: remove dual-key license verification

Restored original license signing key from backup - key was never
compromised (private repo). Removes unnecessary dual-key complexity:

- Remove legacyPublicKey and SetLegacyPublicKey from license.go
- Simplify signature verification to single key
- Remove EmbeddedLegacyPublicKey from pubkey.go
- Remove PULSE_LICENSE_LEGACY_PUBLIC_KEY from Dockerfile and workflows
- Remove dual-key test
- Simplify mock.env
This commit is contained in:
rcourtman 2026-02-03 21:29:21 +00:00
parent 6e034a343a
commit 1490a6e6e3
11 changed files with 16 additions and 219 deletions

View file

@ -184,7 +184,6 @@ jobs:
cache-to: type=registry,ref=ghcr.io/${{ github.repository_owner }}/pulse:buildcache,mode=max
build-args: |
PULSE_LICENSE_PUBLIC_KEY=${{ secrets.PULSE_LICENSE_PUBLIC_KEY }}
PULSE_LICENSE_LEGACY_PUBLIC_KEY=${{ secrets.PULSE_LICENSE_LEGACY_PUBLIC_KEY }}
VERSION=${{ needs.extract_version.outputs.tag }}
tags: |
ghcr.io/${{ github.repository_owner }}/pulse:staging-${{ needs.extract_version.outputs.tag }}
@ -202,7 +201,6 @@ jobs:
cache-to: type=registry,ref=ghcr.io/${{ github.repository_owner }}/pulse-docker-agent:buildcache,mode=max
build-args: |
PULSE_LICENSE_PUBLIC_KEY=${{ secrets.PULSE_LICENSE_PUBLIC_KEY }}
PULSE_LICENSE_LEGACY_PUBLIC_KEY=${{ secrets.PULSE_LICENSE_LEGACY_PUBLIC_KEY }}
VERSION=${{ needs.extract_version.outputs.tag }}
tags: |
ghcr.io/${{ github.repository_owner }}/pulse-docker-agent:staging-${{ needs.extract_version.outputs.tag }}
@ -210,10 +208,8 @@ jobs:
- name: Build Docker images for integration tests
run: |
docker build -t pulse-mock-github:test tests/integration/mock-github-server
docker build -t pulse:test -f Dockerfile --target runtime --cache-from ghcr.io/${{ github.repository_owner }}/pulse:buildcache --build-arg BUILDKIT_INLINE_CACHE=1 --build-arg PULSE_LICENSE_PUBLIC_KEY="$PULSE_LICENSE_PUBLIC_KEY" --build-arg PULSE_LICENSE_LEGACY_PUBLIC_KEY="$PULSE_LICENSE_LEGACY_PUBLIC_KEY" --build-arg VERSION="${{ needs.extract_version.outputs.tag }}" .
env:
PULSE_LICENSE_PUBLIC_KEY: ${{ secrets.PULSE_LICENSE_PUBLIC_KEY }}
PULSE_LICENSE_LEGACY_PUBLIC_KEY: ${{ secrets.PULSE_LICENSE_LEGACY_PUBLIC_KEY }}
- name: Run update integration smoke tests
working-directory: tests/integration
@ -320,7 +316,6 @@ jobs:
./scripts/build-release.sh ${{ needs.extract_version.outputs.version }}
env:
PULSE_LICENSE_PUBLIC_KEY: ${{ secrets.PULSE_LICENSE_PUBLIC_KEY }}
PULSE_LICENSE_LEGACY_PUBLIC_KEY: ${{ secrets.PULSE_LICENSE_LEGACY_PUBLIC_KEY }}
- name: Post-build health check
run: |

View file

@ -77,7 +77,6 @@ jobs:
cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/pulse:buildcache
build-args: |
PULSE_LICENSE_PUBLIC_KEY=${{ secrets.PULSE_LICENSE_PUBLIC_KEY }}
PULSE_LICENSE_LEGACY_PUBLIC_KEY=${{ secrets.PULSE_LICENSE_LEGACY_PUBLIC_KEY }}
VERSION=${{ steps.version.outputs.tag }}
tags: |
rcourtman/pulse:${{ steps.version.outputs.tag }}
@ -99,7 +98,6 @@ jobs:
cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/pulse-docker-agent:buildcache
build-args: |
PULSE_LICENSE_PUBLIC_KEY=${{ secrets.PULSE_LICENSE_PUBLIC_KEY }}
PULSE_LICENSE_LEGACY_PUBLIC_KEY=${{ secrets.PULSE_LICENSE_LEGACY_PUBLIC_KEY }}
VERSION=${{ steps.version.outputs.tag }}
tags: |
rcourtman/pulse-docker-agent:${{ steps.version.outputs.tag }}

View file

@ -78,10 +78,8 @@ jobs:
run: |
VERSION="v$(cat VERSION | tr -d '\n')"
docker build -t pulse-mock-github:test tests/integration/mock-github-server
docker build -t pulse:test -f Dockerfile --target runtime --cache-from ghcr.io/${{ github.repository_owner }}/pulse:buildcache --build-arg BUILDKIT_INLINE_CACHE=1 --build-arg PULSE_LICENSE_PUBLIC_KEY="$PULSE_LICENSE_PUBLIC_KEY" --build-arg PULSE_LICENSE_LEGACY_PUBLIC_KEY="$PULSE_LICENSE_LEGACY_PUBLIC_KEY" --build-arg VERSION="$VERSION" .
env:
PULSE_LICENSE_PUBLIC_KEY: ${{ secrets.PULSE_LICENSE_PUBLIC_KEY }}
PULSE_LICENSE_LEGACY_PUBLIC_KEY: ${{ secrets.PULSE_LICENSE_LEGACY_PUBLIC_KEY }}
- name: Run integration diagnostics
working-directory: tests/integration

View file

@ -53,10 +53,8 @@ jobs:
- name: Build Docker images for test environment
run: |
docker build -t pulse-mock-github:test ./tests/integration/mock-github-server
docker build -t pulse:test -f Dockerfile --build-arg PULSE_LICENSE_PUBLIC_KEY="$PULSE_LICENSE_PUBLIC_KEY" --build-arg PULSE_LICENSE_LEGACY_PUBLIC_KEY="$PULSE_LICENSE_LEGACY_PUBLIC_KEY" .
env:
PULSE_LICENSE_PUBLIC_KEY: ${{ secrets.PULSE_LICENSE_PUBLIC_KEY }}
PULSE_LICENSE_LEGACY_PUBLIC_KEY: ${{ secrets.PULSE_LICENSE_LEGACY_PUBLIC_KEY }}
- name: Start test containers
working-directory: tests/integration

View file

@ -70,10 +70,8 @@ jobs:
# Build Pulse test image
cd ../../
docker build -t pulse:test -f Dockerfile --build-arg PULSE_LICENSE_PUBLIC_KEY="$PULSE_LICENSE_PUBLIC_KEY" --build-arg PULSE_LICENSE_LEGACY_PUBLIC_KEY="$PULSE_LICENSE_LEGACY_PUBLIC_KEY" .
env:
PULSE_LICENSE_PUBLIC_KEY: ${{ secrets.PULSE_LICENSE_PUBLIC_KEY }}
PULSE_LICENSE_LEGACY_PUBLIC_KEY: ${{ secrets.PULSE_LICENSE_LEGACY_PUBLIC_KEY }}
- name: Run diagnostic smoke test
working-directory: tests/integration

View file

@ -1,7 +1,6 @@
# syntax=docker/dockerfile:1.7-labs
ARG BUILD_AGENT=1
ARG PULSE_LICENSE_PUBLIC_KEY
ARG PULSE_LICENSE_LEGACY_PUBLIC_KEY
# Build stage for frontend (must be built first for embedding)
# Force amd64 platform to avoid slow QEMU emulation during multi-arch builds
@ -28,7 +27,6 @@ FROM --platform=linux/amd64 golang:1.24-alpine AS backend-builder
ARG BUILD_AGENT
ARG PULSE_LICENSE_PUBLIC_KEY
ARG PULSE_LICENSE_LEGACY_PUBLIC_KEY
ARG VERSION
WORKDIR /app
@ -61,9 +59,6 @@ RUN --mount=type=cache,id=pulse-go-mod,target=/go/pkg/mod \
if [ -n "${PULSE_LICENSE_PUBLIC_KEY}" ]; then \
LICENSE_LDFLAGS="-X github.com/rcourtman/pulse-go-rewrite/internal/license.EmbeddedPublicKey=${PULSE_LICENSE_PUBLIC_KEY}"; \
fi && \
if [ -n "${PULSE_LICENSE_LEGACY_PUBLIC_KEY}" ]; then \
LICENSE_LDFLAGS="${LICENSE_LDFLAGS} -X github.com/rcourtman/pulse-go-rewrite/internal/license.EmbeddedLegacyPublicKey=${PULSE_LICENSE_LEGACY_PUBLIC_KEY}"; \
fi && \
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
-ldflags="-s -w -X main.Version=${VERSION} -X main.BuildTime=${BUILD_TIME} -X main.GitCommit=${GIT_COMMIT} -X github.com/rcourtman/pulse-go-rewrite/internal/dockeragent.Version=${VERSION} ${LICENSE_LDFLAGS}" \
-trimpath \

View file

@ -17,22 +17,12 @@ import (
// For development, leave empty to skip validation.
var publicKey ed25519.PublicKey
// Legacy public key for validating licenses signed with the previous key.
// Used during key rotation to maintain compatibility with existing licenses.
var legacyPublicKey ed25519.PublicKey
// SetPublicKey sets the public key for license validation.
// This should be called during initialization with the production key.
func SetPublicKey(key ed25519.PublicKey) {
publicKey = key
}
// SetLegacyPublicKey sets the legacy public key for validating old licenses.
// This enables dual-key verification during key rotation periods.
func SetLegacyPublicKey(key ed25519.PublicKey) {
legacyPublicKey = key
}
// License errors
var (
ErrInvalidLicense = errors.New("invalid license key")
@ -427,24 +417,11 @@ func ValidateLicense(licenseKey string) (*License, error) {
// Verify signature
// In production, public key MUST be set. In dev mode, we skip signature validation.
// Dual-key support: try primary key first, then legacy key for old licenses.
devMode := os.Getenv("PULSE_LICENSE_DEV_MODE") == "true"
signedData := []byte(parts[0] + "." + parts[1])
if len(publicKey) > 0 || len(legacyPublicKey) > 0 {
verified := false
// Try primary key first (new licenses)
if len(publicKey) > 0 && ed25519.Verify(publicKey, signedData, signature) {
verified = true
}
// Fall back to legacy key (old licenses signed before key rotation)
if !verified && len(legacyPublicKey) > 0 && ed25519.Verify(legacyPublicKey, signedData, signature) {
verified = true
}
if !verified {
if len(publicKey) > 0 {
if !ed25519.Verify(publicKey, signedData, signature) {
return nil, ErrSignatureInvalid
}
} else if !devMode {

View file

@ -727,113 +727,3 @@ func TestValidateLicense_RealSignature(t *testing.T) {
t.Error("Expected error for invalid signature")
}
}
func TestValidateLicense_DualKeyVerification(t *testing.T) {
// Generate two key pairs: "new" primary key and "old" legacy key
newPub, newPriv, _ := ed25519.GenerateKey(nil)
oldPub, oldPriv, _ := ed25519.GenerateKey(nil)
os.Setenv("PULSE_LICENSE_DEV_MODE", "false")
defer os.Setenv("PULSE_LICENSE_DEV_MODE", "true")
// Helper to create a signed license token
createSignedToken := func(priv ed25519.PrivateKey, email string) string {
claims := Claims{
LicenseID: "test-dual-key",
Email: email,
Tier: TierPro,
IssuedAt: time.Now().Unix(),
ExpiresAt: time.Now().Add(30 * 24 * time.Hour).Unix(),
}
header := base64.RawURLEncoding.EncodeToString([]byte(`{"alg":"EdDSA","typ":"JWT"}`))
payloadBytes, _ := json.Marshal(claims)
payload := base64.RawURLEncoding.EncodeToString(payloadBytes)
signedData := header + "." + payload
signature := ed25519.Sign(priv, []byte(signedData))
return signedData + "." + base64.RawURLEncoding.EncodeToString(signature)
}
newKeyToken := createSignedToken(newPriv, "new@test.com")
oldKeyToken := createSignedToken(oldPriv, "old@test.com")
t.Run("primary key only validates new token", func(t *testing.T) {
SetPublicKey(newPub)
SetLegacyPublicKey(nil)
defer SetPublicKey(nil)
lic, err := ValidateLicense(newKeyToken)
if err != nil {
t.Fatalf("Expected new token to validate with primary key: %v", err)
}
if lic.Claims.Email != "new@test.com" {
t.Error("Email mismatch")
}
// Old token should fail
_, err = ValidateLicense(oldKeyToken)
if err == nil {
t.Error("Expected old token to fail with only primary key")
}
})
t.Run("legacy key only validates old token", func(t *testing.T) {
SetPublicKey(nil)
SetLegacyPublicKey(oldPub)
defer SetLegacyPublicKey(nil)
lic, err := ValidateLicense(oldKeyToken)
if err != nil {
t.Fatalf("Expected old token to validate with legacy key: %v", err)
}
if lic.Claims.Email != "old@test.com" {
t.Error("Email mismatch")
}
// New token should fail
_, err = ValidateLicense(newKeyToken)
if err == nil {
t.Error("Expected new token to fail with only legacy key")
}
})
t.Run("dual key validates both tokens", func(t *testing.T) {
SetPublicKey(newPub)
SetLegacyPublicKey(oldPub)
defer SetPublicKey(nil)
defer SetLegacyPublicKey(nil)
// New token should validate
lic, err := ValidateLicense(newKeyToken)
if err != nil {
t.Fatalf("Expected new token to validate with dual keys: %v", err)
}
if lic.Claims.Email != "new@test.com" {
t.Error("Email mismatch for new token")
}
// Old token should also validate
lic, err = ValidateLicense(oldKeyToken)
if err != nil {
t.Fatalf("Expected old token to validate with dual keys: %v", err)
}
if lic.Claims.Email != "old@test.com" {
t.Error("Email mismatch for old token")
}
})
t.Run("neither key rejects invalid token", func(t *testing.T) {
SetPublicKey(newPub)
SetLegacyPublicKey(oldPub)
defer SetPublicKey(nil)
defer SetLegacyPublicKey(nil)
// Create token signed with a completely different key
_, otherPriv, _ := ed25519.GenerateKey(nil)
otherToken := createSignedToken(otherPriv, "other@test.com")
_, err := ValidateLicense(otherToken)
if err == nil {
t.Error("Expected token signed with unknown key to fail")
}
})
}

View file

@ -14,79 +14,41 @@ import (
// Example: go build -ldflags "-X github.com/rcourtman/pulse-go-rewrite/internal/license.EmbeddedPublicKey=BASE64_KEY"
var EmbeddedPublicKey string = ""
// EmbeddedLegacyPublicKey is the previous production public key (base64 encoded).
// Used for dual-key verification during key rotation to validate old licenses.
// Set at build time via -ldflags alongside EmbeddedPublicKey.
var EmbeddedLegacyPublicKey string = ""
// InitPublicKey initializes the public key(s) for license validation.
// Primary key priority:
// InitPublicKey initializes the public key for license validation.
// Priority:
// 1. PULSE_LICENSE_PUBLIC_KEY environment variable (base64 encoded)
// 2. EmbeddedPublicKey (set at compile time via -ldflags)
// 3. If PULSE_LICENSE_DEV_MODE=true, skip validation (development only)
//
// Legacy key priority (for dual-key verification during key rotation):
// 1. PULSE_LICENSE_LEGACY_PUBLIC_KEY environment variable (base64 encoded)
// 2. EmbeddedLegacyPublicKey (set at compile time via -ldflags)
//
// If PULSE_LICENSE_DEV_MODE=true, skip validation (development only).
// Call this during application startup before any license operations.
func InitPublicKey() {
devMode := os.Getenv("PULSE_LICENSE_DEV_MODE") == "true"
primaryLoaded := false
legacyLoaded := false
// Load primary public key
// Priority 1: Environment variable
if envKey := os.Getenv("PULSE_LICENSE_PUBLIC_KEY"); envKey != "" {
key, err := decodePublicKey(envKey)
if err != nil {
log.Error().Err(err).Msg("Failed to decode PULSE_LICENSE_PUBLIC_KEY")
log.Error().Err(err).Msg("Failed to decode PULSE_LICENSE_PUBLIC_KEY, trying embedded key")
// Fall through to try embedded key instead of returning
} else {
SetPublicKey(key)
log.Info().Msg("License public key loaded from environment")
primaryLoaded = true
return
}
}
if !primaryLoaded && EmbeddedPublicKey != "" {
// Priority 2: Embedded key (set at compile time)
if EmbeddedPublicKey != "" {
key, err := decodePublicKey(EmbeddedPublicKey)
if err != nil {
log.Error().Err(err).Msg("Failed to decode embedded public key")
} else {
SetPublicKey(key)
log.Info().Msg("License public key loaded from embedded key")
primaryLoaded = true
return
}
}
// Load legacy public key (for dual-key verification)
if envKey := os.Getenv("PULSE_LICENSE_LEGACY_PUBLIC_KEY"); envKey != "" {
key, err := decodePublicKey(envKey)
if err != nil {
log.Error().Err(err).Msg("Failed to decode PULSE_LICENSE_LEGACY_PUBLIC_KEY")
} else {
SetLegacyPublicKey(key)
log.Info().Msg("Legacy license public key loaded from environment")
legacyLoaded = true
}
}
if !legacyLoaded && EmbeddedLegacyPublicKey != "" {
key, err := decodePublicKey(EmbeddedLegacyPublicKey)
if err != nil {
log.Error().Err(err).Msg("Failed to decode embedded legacy public key")
} else {
SetLegacyPublicKey(key)
log.Info().Msg("Legacy license public key loaded from embedded key")
legacyLoaded = true
}
}
// Log status
if primaryLoaded && legacyLoaded {
log.Info().Msg("Dual-key license verification enabled (primary + legacy)")
} else if primaryLoaded {
log.Info().Msg("Single-key license verification enabled")
} else if legacyLoaded {
log.Warn().Msg("Only legacy key loaded - new licenses will not validate")
} else if devMode {
// No key available
if os.Getenv("PULSE_LICENSE_DEV_MODE") == "true" {
log.Warn().Msg("License validation running in DEV MODE - signatures not verified")
} else {
log.Warn().Msg("No license public key configured - license activation will fail")

View file

@ -7,13 +7,5 @@ PULSE_MOCK_DOCKER_HOSTS=3
PULSE_MOCK_DOCKER_CONTAINERS=12
PULSE_MOCK_RANDOM_METRICS=true
PULSE_MOCK_STOPPED_PERCENT=20
# License verification keys (Ed25519 public keys, base64 encoded)
# For local dev, use the legacy key as primary since existing test licenses use it.
# For production releases, set the new key as primary and old key as legacy.
#
# Key rotation (2026-02-03):
# - Legacy key: OzbVzmg+TaSGt0eWzDVpn0QkqhOzJqUbOFvSF3AmuRU= (signs existing licenses)
# - New key: Set via PULSE_LICENSE_PUBLIC_KEY at build time
#
# License verification key (Ed25519 public key, base64 encoded)
PULSE_LICENSE_PUBLIC_KEY="OzbVzmg+TaSGt0eWzDVpn0QkqhOzJqUbOFvSF3AmuRU="
# PULSE_LICENSE_LEGACY_PUBLIC_KEY="" # Set in production builds for dual-key verification

View file

@ -27,12 +27,6 @@ else
echo "Warning: PULSE_LICENSE_PUBLIC_KEY not set; Pulse Pro license activation will fail for release binaries."
fi
# Optional legacy public key for dual-key verification during key rotation
if [[ -n "${PULSE_LICENSE_LEGACY_PUBLIC_KEY:-}" ]]; then
LICENSE_LDFLAGS="${LICENSE_LDFLAGS} -X github.com/rcourtman/pulse-go-rewrite/internal/license.EmbeddedLegacyPublicKey=${PULSE_LICENSE_LEGACY_PUBLIC_KEY}"
echo "Legacy public key configured for dual-key verification."
fi
# Clean previous builds
rm -rf $BUILD_DIR $RELEASE_DIR
mkdir -p $BUILD_DIR $RELEASE_DIR