feat(license): add offline verification
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
package license
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/hyperboloide/lk"
|
||||
)
|
||||
|
||||
type State string
|
||||
|
||||
const (
|
||||
StateValid State = "valid"
|
||||
StateExpired State = "expired"
|
||||
StateInvalid State = "invalid"
|
||||
)
|
||||
|
||||
// Reasons a licence is not valid. These are stable identifiers: the API returns
|
||||
// them and the UI maps them to messages, so do not reword them casually.
|
||||
const (
|
||||
ReasonNoLicense = "no_license"
|
||||
ReasonBadSignature = "bad_signature"
|
||||
ReasonDeploymentMismatch = "deployment_mismatch"
|
||||
ReasonInstanceMismatch = "instance_mismatch"
|
||||
ReasonExpired = "expired"
|
||||
)
|
||||
|
||||
// VerifyOpts is what the verifier knows about itself.
|
||||
type VerifyOpts struct {
|
||||
InstanceID string // this instance's own ID; required
|
||||
Deployment string // "cloud" or "self_hosted"; required
|
||||
Now time.Time // zero means time.Now()
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
License License
|
||||
State State
|
||||
Reason string
|
||||
// ClockSkewed is set when IssuedAt is in the future, which usually means
|
||||
// the host clock is wrong. It does not by itself invalidate the licence.
|
||||
ClockSkewed bool
|
||||
}
|
||||
|
||||
// Verify checks a licence blob against this instance.
|
||||
//
|
||||
// The checks run in a fixed order and stop at the first failure:
|
||||
//
|
||||
// 1. signature against a trusted public key -> bad_signature
|
||||
// 2. deployment matches this install -> deployment_mismatch
|
||||
// 3. instance ID matches this instance -> instance_mismatch
|
||||
// 4. not past ExpiresAt -> expired
|
||||
//
|
||||
// The order matters. A blob that is both expired and bound to another instance
|
||||
// reports instance_mismatch, not expired, because that is the more useful thing
|
||||
// to tell the person holding it.
|
||||
//
|
||||
// No clock tolerance is applied. Terms are a month or a year; a host whose clock
|
||||
// is wrong by enough to matter has larger problems, and a tolerance window is a
|
||||
// thing to get wrong.
|
||||
func Verify(blob string, opts VerifyOpts) Result {
|
||||
if blob == "" {
|
||||
return Result{State: StateInvalid, Reason: ReasonNoLicense}
|
||||
}
|
||||
|
||||
l, err := Parse(blob)
|
||||
if err != nil {
|
||||
return Result{State: StateInvalid, Reason: ReasonBadSignature}
|
||||
}
|
||||
|
||||
res := Result{License: l}
|
||||
|
||||
if l.Deployment != opts.Deployment {
|
||||
res.State, res.Reason = StateInvalid, ReasonDeploymentMismatch
|
||||
return res
|
||||
}
|
||||
if l.InstanceID != opts.InstanceID {
|
||||
res.State, res.Reason = StateInvalid, ReasonInstanceMismatch
|
||||
return res
|
||||
}
|
||||
|
||||
now := opts.Now
|
||||
if now.IsZero() {
|
||||
now = time.Now()
|
||||
}
|
||||
res.ClockSkewed = l.IssuedAt.After(now)
|
||||
|
||||
if !now.Before(l.ExpiresAt) {
|
||||
res.State, res.Reason = StateExpired, ReasonExpired
|
||||
return res
|
||||
}
|
||||
|
||||
res.State = StateValid
|
||||
return res
|
||||
}
|
||||
|
||||
// Parse verifies the signature only, ignoring binding and expiry.
|
||||
//
|
||||
// Used to display a licence and to inspect a blob a customer has emailed in.
|
||||
// Never use it for enforcement — it does not check who the licence is for.
|
||||
func Parse(blob string) (License, error) {
|
||||
parsed, err := lk.LicenseFromB32String(blob)
|
||||
if err != nil {
|
||||
return License{}, fmt.Errorf("licence is not readable: %w", err)
|
||||
}
|
||||
|
||||
keys, err := publicKeys()
|
||||
if err != nil {
|
||||
return License{}, err
|
||||
}
|
||||
if len(keys) == 0 {
|
||||
return License{}, fmt.Errorf("this build trusts no licence signing keys")
|
||||
}
|
||||
|
||||
verified := false
|
||||
for _, k := range keys {
|
||||
ok, err := parsed.Verify(k)
|
||||
if err == nil && ok {
|
||||
verified = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !verified {
|
||||
return License{}, fmt.Errorf("licence signature does not match any trusted key")
|
||||
}
|
||||
|
||||
var l License
|
||||
if err := json.Unmarshal(parsed.Data, &l); err != nil {
|
||||
return License{}, fmt.Errorf("licence contents are not readable: %w", err)
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
Reference in New Issue
Block a user