Skip to content

feat: Ed25519 license signing + offline verification + cosign verification#211

Merged
intel352 merged 2 commits intomainfrom
license-signing
Feb 28, 2026
Merged

feat: Ed25519 license signing + offline verification + cosign verification#211
intel352 merged 2 commits intomainfrom
license-signing

Conversation

@intel352
Copy link
Contributor

Summary

  • Ed25519 license tokens (pkg/license/): Signed wflic.v1.<payload>.<signature> format tokens with offline verification — no HTTP call-home required
  • OfflineValidator + CompositeValidator (licensing/): Token-based offline license validation with automatic fallback to HTTP validator
  • License plugin wiring (plugins/license/): Adapter connecting licensing.Validatorplugin.LicenseValidator interfaces, embedded public key, WORKFLOW_LICENSE_TOKEN env var support
  • Cosign binary verification (plugin/cosign.go, plugin/loader.go): Verifies premium plugin binary signatures via sigstore before loading

Test plan

  • go test ./pkg/license/... — 10 tests (round-trip, expiry, tampering, invalid formats)
  • go test ./licensing/... — 19 tests (offline validator, composite fallback, HTTP compatibility)
  • go test ./plugins/license/... — 11 tests (wiring hook, adapter, token scenarios)
  • go build ./plugin/... — cosign verifier compiles cleanly
  • go test ./... — all 102 packages pass, zero failures

🤖 Generated with Claude Code

… binary verification

Add cryptographic license protection for premium plugins:

- pkg/license: Ed25519 token signing/verification (wflic.v1 format), PEM key management
- licensing: OfflineValidator for token-based offline license checks, CompositeValidator
  combining offline + HTTP validation with automatic fallback
- plugins/license: Wiring hook connecting license validator to plugin loader via adapter,
  embedded dev public key, WORKFLOW_LICENSE_TOKEN env var support
- plugin/cosign.go: CosignVerifier for verifying plugin binary signatures via sigstore
- plugin/loader.go: SetCosignVerifier + LoadBinaryPlugin with premium tier verification

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings February 28, 2026 12:52
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces an offline-capable licensing mechanism (Ed25519-signed tokens) and wires it into the engine’s plugin licensing checks, along with an initial cosign-based verification utility intended to validate premium plugin binaries before loading.

Changes:

  • Added pkg/license Ed25519 token/key utilities (+ tests) for wflic.v1.<payload>.<signature> license tokens.
  • Added licensing.OfflineValidator and licensing.CompositeValidator (+ tests) to validate tokens offline with optional HTTP fallback.
  • Added a new license plugin wiring hook that reads WORKFLOW_LICENSE_TOKEN and sets a plugin.LicenseValidator on the PluginLoader; added an embedded public key.
  • Added plugin.CosignVerifier and PluginLoader.LoadBinaryPlugin hook point for premium plugin binary verification.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
plugins/license/plugin.go Embeds Ed25519 public key and adds wiring hook to set loader license validator from env token / HTTP validator.
plugins/license/plugin_test.go Adds wiring hook + adapter tests using a stub modular app/engine loader.
plugins/license/keys/license.pub Adds embedded Ed25519 public key PEM.
plugins/license/keys/genkey_main.go Adds build-ignored helper to generate keypair and write license.pub.
plugin/loader.go Adds cosign verifier field, setter, and LoadBinaryPlugin verification path.
plugin/cosign.go Implements a CLI-based cosign verifier (cosign verify-blob).
pkg/license/token.go Implements token signing/parsing/verification and helpers.
pkg/license/token_test.go Adds coverage for token format, tampering, expiry helpers, and PEM round-trips.
pkg/license/keygen.go Adds Ed25519 key generation and PEM marshal/unmarshal helpers.
licensing/offline_validator.go Implements offline validator based on embedded public key + signed token.
licensing/offline_validator_test.go Adds tests for offline validator behaviors and composite fallback.
licensing/composite_validator.go Adds composite validator that prefers offline validation and falls back to HTTP.

Comment on lines +35 to +50
// Validate implements licensing.Validator. It returns a valid result when key
// matches the stored token string, and an invalid result otherwise.
func (v *OfflineValidator) Validate(_ context.Context, key string) (*ValidationResult, error) {
if key != v.tokenStr {
return &ValidationResult{
Valid: false,
Error: "license key does not match token",
CachedUntil: time.Now().Add(DefaultCacheTTL),
}, nil
}
return &ValidationResult{
Valid: true,
License: *v.licenseInfo(),
CachedUntil: time.Now().Add(DefaultCacheTTL),
}, nil
}
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OfflineValidator.Validate currently returns Valid=true whenever the provided key matches the stored token string, even if the token is expired. That makes an expired offline license look valid to callers that rely on ValidationResult.Valid (matching HTTPValidator behavior). Consider checking token expiration here and returning Valid=false with an appropriate Error when expired.

Copilot uses AI. Check for mistakes.
}

// CheckFeature implements licensing.Validator.
func (v *OfflineValidator) CheckFeature(feature string) bool {
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OfflineValidator.CheckFeature returns token.HasFeature without considering expiration. For consistency with HTTPValidator.CheckFeature (which returns false when no valid license is cached), this should likely return false when the token is expired (e.g., by using GetLicenseInfo()/IsExpired guard) so expired licenses don't continue to grant features.

Suggested change
func (v *OfflineValidator) CheckFeature(feature string) bool {
func (v *OfflineValidator) CheckFeature(feature string) bool {
if v.GetLicenseInfo() == nil {
return false
}

Copilot uses AI. Check for mistakes.
Comment on lines +10 to +31
// It requires the cosign CLI to be installed; if not found, verification is
// skipped with a warning to support environments without cosign installed.
type CosignVerifier struct {
OIDCIssuer string
AllowedIdentityRegexp string
}

// NewCosignVerifier creates a CosignVerifier for the given OIDC issuer and
// identity regexp (e.g. "https://github.com/GoCodeAlone/.*").
func NewCosignVerifier(oidcIssuer, identityRegexp string) *CosignVerifier {
return &CosignVerifier{
OIDCIssuer: oidcIssuer,
AllowedIdentityRegexp: identityRegexp,
}
}

// Verify runs `cosign verify-blob` to validate the signature of a plugin binary.
// If cosign is not installed, a warning is logged and nil is returned so that
// deployments without cosign are not broken.
func (v *CosignVerifier) Verify(binaryPath, sigPath, certPath string) error {
cosignBin, err := exec.LookPath("cosign")
if err != nil {
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CosignVerifier.Verify silently skips verification (returns nil) when the cosign CLI is not installed. If the intent is to enforce signature verification for premium plugins, this should fail closed (return an error) or be guarded by an explicit configuration flag; otherwise an attacker can bypass verification simply by removing cosign from the environment.

Suggested change
// It requires the cosign CLI to be installed; if not found, verification is
// skipped with a warning to support environments without cosign installed.
type CosignVerifier struct {
OIDCIssuer string
AllowedIdentityRegexp string
}
// NewCosignVerifier creates a CosignVerifier for the given OIDC issuer and
// identity regexp (e.g. "https://github.com/GoCodeAlone/.*").
func NewCosignVerifier(oidcIssuer, identityRegexp string) *CosignVerifier {
return &CosignVerifier{
OIDCIssuer: oidcIssuer,
AllowedIdentityRegexp: identityRegexp,
}
}
// Verify runs `cosign verify-blob` to validate the signature of a plugin binary.
// If cosign is not installed, a warning is logged and nil is returned so that
// deployments without cosign are not broken.
func (v *CosignVerifier) Verify(binaryPath, sigPath, certPath string) error {
cosignBin, err := exec.LookPath("cosign")
if err != nil {
// By default, it will skip verification with a warning if the cosign CLI is
// not installed, but this behavior can be configured via AllowMissingCosign.
type CosignVerifier struct {
OIDCIssuer string
AllowedIdentityRegexp string
// AllowMissingCosign controls behavior when the cosign CLI is not found in
// PATH. If true, verification is skipped with a warning; if false, Verify
// returns an error instead of silently succeeding.
AllowMissingCosign bool
}
// NewCosignVerifier creates a CosignVerifier for the given OIDC issuer and
// identity regexp (e.g. "https://github.com/GoCodeAlone/.*").
//
// It is configured to allow missing cosign by default, preserving the
// historical behavior of skipping verification when cosign is not installed.
func NewCosignVerifier(oidcIssuer, identityRegexp string) *CosignVerifier {
return &CosignVerifier{
OIDCIssuer: oidcIssuer,
AllowedIdentityRegexp: identityRegexp,
AllowMissingCosign: true,
}
}
// NewStrictCosignVerifier creates a CosignVerifier that requires the cosign
// CLI to be installed. If cosign is not found in PATH, Verify will return an
// error instead of skipping verification.
func NewStrictCosignVerifier(oidcIssuer, identityRegexp string) *CosignVerifier {
return &CosignVerifier{
OIDCIssuer: oidcIssuer,
AllowedIdentityRegexp: identityRegexp,
AllowMissingCosign: false,
}
}
// Verify runs `cosign verify-blob` to validate the signature of a plugin binary.
// If cosign is not installed and AllowMissingCosign is true, a warning is
// logged and nil is returned so that deployments without cosign are not
// broken. If AllowMissingCosign is false, an error is returned instead.
func (v *CosignVerifier) Verify(binaryPath, sigPath, certPath string) error {
cosignBin, err := exec.LookPath("cosign")
if err != nil {
if !v.AllowMissingCosign {
return fmt.Errorf("cosign binary not found in PATH: %w", err)
}

Copilot uses AI. Check for mistakes.
Comment on lines +63 to +70
// ValidatePlugin implements plugin.LicenseValidator. It delegates to the offline
// validator, which performs the authoritative check without network calls.
func (c *CompositeValidator) ValidatePlugin(pluginName string) error {
if c.offline != nil {
return c.offline.ValidatePlugin(pluginName)
}
return nil
}
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CompositeValidator.ValidatePlugin returns nil when the offline validator is nil, which effectively approves every premium plugin in that configuration. Since plugin loading treats a nil error as licensed, this should fail closed (return an error) and/or explicitly delegate to the HTTP validator (e.g., via cached LicenseInfo) rather than silently allowing.

Copilot uses AI. Check for mistakes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@intel352 intel352 merged commit 649cc77 into main Feb 28, 2026
14 checks passed
@intel352 intel352 deleted the license-signing branch February 28, 2026 13:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants