-
Notifications
You must be signed in to change notification settings - Fork 0
feat: Ed25519 license signing + offline verification + cosign verification #211
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| package licensing | ||
|
|
||
| import ( | ||
| "context" | ||
| ) | ||
|
|
||
| // CompositeValidator tries an offline validator first, falling back to an HTTP | ||
| // validator. This enables air-gapped or low-latency license checks while keeping | ||
| // the HTTP validator as a fallback for online validation. | ||
| type CompositeValidator struct { | ||
| offline *OfflineValidator | ||
| http *HTTPValidator | ||
| } | ||
|
|
||
| // NewCompositeValidator creates a CompositeValidator from an offline and an HTTP | ||
| // validator. Either may be nil (though at least one should be non-nil). | ||
| func NewCompositeValidator(offline *OfflineValidator, http *HTTPValidator) *CompositeValidator { | ||
| return &CompositeValidator{offline: offline, http: http} | ||
| } | ||
|
|
||
| // Validate implements licensing.Validator. It tries the offline validator first; | ||
| // if the result is valid it is returned immediately. Otherwise it falls back to | ||
| // the HTTP validator. | ||
| func (c *CompositeValidator) Validate(ctx context.Context, key string) (*ValidationResult, error) { | ||
| if c.offline != nil { | ||
| result, err := c.offline.Validate(ctx, key) | ||
| if err == nil && result.Valid { | ||
| return result, nil | ||
| } | ||
| } | ||
| if c.http != nil { | ||
| return c.http.Validate(ctx, key) | ||
| } | ||
| return &ValidationResult{Valid: false, Error: "no validator configured"}, nil | ||
| } | ||
|
|
||
| // CheckFeature implements licensing.Validator. It uses the offline validator when | ||
| // available, otherwise falls back to the HTTP validator. | ||
| func (c *CompositeValidator) CheckFeature(feature string) bool { | ||
| if c.offline != nil { | ||
| return c.offline.CheckFeature(feature) | ||
| } | ||
| if c.http != nil { | ||
| return c.http.CheckFeature(feature) | ||
| } | ||
| return false | ||
| } | ||
|
|
||
| // GetLicenseInfo implements licensing.Validator. It returns the offline license | ||
| // info when available (non-nil), otherwise falls back to the HTTP validator. | ||
| func (c *CompositeValidator) GetLicenseInfo() *LicenseInfo { | ||
| if c.offline != nil { | ||
| if info := c.offline.GetLicenseInfo(); info != nil { | ||
| return info | ||
| } | ||
| } | ||
| if c.http != nil { | ||
| return c.http.GetLicenseInfo() | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| // 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 | ||
| } | ||
|
|
||
| // CanLoadPlugin returns true when the offline validator permits the tier, or when | ||
| // there is no offline validator and the HTTP validator permits it. | ||
| func (c *CompositeValidator) CanLoadPlugin(tier string) bool { | ||
| if c.offline != nil { | ||
| return c.offline.CanLoadPlugin(tier) | ||
| } | ||
| if c.http != nil { | ||
| return c.http.CanLoadPlugin(tier) | ||
| } | ||
| return false | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,110 @@ | ||||||||||||
| package licensing | ||||||||||||
|
|
||||||||||||
| import ( | ||||||||||||
| "context" | ||||||||||||
| "fmt" | ||||||||||||
| "time" | ||||||||||||
|
|
||||||||||||
| "github.com/GoCodeAlone/workflow/pkg/license" | ||||||||||||
| ) | ||||||||||||
|
|
||||||||||||
| // OfflineValidator validates a license token using a local Ed25519 public key, | ||||||||||||
| // with no network calls required after construction. | ||||||||||||
| type OfflineValidator struct { | ||||||||||||
| tokenStr string | ||||||||||||
| token *license.LicenseToken | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // NewOfflineValidator parses publicKeyPEM and tokenStr, verifies the token | ||||||||||||
| // signature, and returns an OfflineValidator ready for use. | ||||||||||||
| func NewOfflineValidator(publicKeyPEM []byte, tokenStr string) (*OfflineValidator, error) { | ||||||||||||
| pub, err := license.UnmarshalPublicKeyPEM(publicKeyPEM) | ||||||||||||
| if err != nil { | ||||||||||||
| return nil, fmt.Errorf("parse public key: %w", err) | ||||||||||||
| } | ||||||||||||
| tok, err := license.Parse(tokenStr) | ||||||||||||
| if err != nil { | ||||||||||||
| return nil, fmt.Errorf("parse token: %w", err) | ||||||||||||
| } | ||||||||||||
| if err := tok.Verify(pub); err != nil { | ||||||||||||
| return nil, fmt.Errorf("verify token signature: %w", err) | ||||||||||||
| } | ||||||||||||
| return &OfflineValidator{tokenStr: tokenStr, token: tok}, nil | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // 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 | ||||||||||||
| } | ||||||||||||
|
Comment on lines
+35
to
+50
|
||||||||||||
|
|
||||||||||||
| // CheckFeature implements licensing.Validator. | ||||||||||||
| func (v *OfflineValidator) CheckFeature(feature string) bool { | ||||||||||||
|
||||||||||||
| func (v *OfflineValidator) CheckFeature(feature string) bool { | |
| func (v *OfflineValidator) CheckFeature(feature string) bool { | |
| if v.GetLicenseInfo() == nil { | |
| return false | |
| } |
There was a problem hiding this comment.
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.