// Package license verifies purchased Aura CRM licenses entirely offline — // no server to phone home to, no crash loop if that server is ever // unreachable (see this repo's own history for exactly that failure mode // in a different product this business runs). A license key is a JSON // payload signed with an Ed25519 private key the vendor keeps offline // (see tools/gen-license); this package only ever holds the matching // PUBLIC key, so shipping this source (or the compiled binary) to a // customer can never leak the ability to mint new licenses — verifying and // signing use different keys, unlike an HMAC scheme where they'd be the // same secret. package license import ( "crypto/ed25519" "encoding/base64" "encoding/hex" "encoding/json" "errors" "strings" "time" ) // publicKeyHex is safe to be public — see package doc comment. Generated // once per vendor; changing it invalidates every license issued under the // old key. const publicKeyHex = "e1f8a79b84ad09cf357ae0548fd2bb77184a33eca9516dee67903b784245e99f" var ErrInvalidSignature = errors.New("license: invalid signature") var ErrMalformed = errors.New("license: malformed key") // Payload is what a license key actually asserts. ExpiresAt nil means a // perpetual license (paid in full, not a subscription) — Valid() treats // that as never expiring. type Payload struct { Customer string `json:"customer"` IssuedAt time.Time `json:"issued_at"` ExpiresAt *time.Time `json:"expires_at,omitempty"` } // Valid reports whether this payload's own terms are currently satisfied — // it does NOT re-verify the signature (Parse already did that; a Payload // only ever exists after a successful Parse). func (p *Payload) Valid() bool { return p.ExpiresAt == nil || p.ExpiresAt.After(time.Now()) } // Parse verifies a license key's Ed25519 signature and decodes its // payload. Format: base64(payload-json) + "." + base64(signature-over-the- // undecoded-payload-b64-string) — signing the base64 text rather than the // raw JSON bytes means there's exactly one canonical byte sequence to sign // and verify, no risk of a JSON re-serialization producing different bytes // on the two sides. func Parse(key string) (*Payload, error) { key = strings.TrimSpace(key) parts := strings.SplitN(key, ".", 2) if len(parts) != 2 { return nil, ErrMalformed } payloadB64, sigB64 := parts[0], parts[1] sig, err := base64.RawURLEncoding.DecodeString(sigB64) if err != nil { return nil, ErrMalformed } pubKey, err := hex.DecodeString(publicKeyHex) if err != nil { // Only possible if publicKeyHex itself was edited to something // invalid — a build-time bug, not a runtime/user-input error. return nil, errors.New("license: invalid embedded public key") } if !ed25519.Verify(ed25519.PublicKey(pubKey), []byte(payloadB64), sig) { return nil, ErrInvalidSignature } payloadJSON, err := base64.RawURLEncoding.DecodeString(payloadB64) if err != nil { return nil, ErrMalformed } var p Payload if err := json.Unmarshal(payloadJSON, &p); err != nil { return nil, ErrMalformed } return &p, nil }