Files
vantage/shared/license/verify.go
T

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
}