133 lines
3.5 KiB
Go
133 lines
3.5 KiB
Go
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
|
|
}
|