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 }