package licensing import ( "bytes" "crypto/ed25519" "encoding/base64" "encoding/json" "errors" "io" "regexp" "time" "softbox.local/core/internal/canonicaljson" ) var ( ErrInvalidLicense = errors.New("license is invalid") ErrDuplicateField = errors.New("license has duplicate field") ErrUnsupportedNumber = errors.New("license has unsupported number") ErrSignatureMissing = errors.New("license signature is missing") ErrSignatureInvalid = errors.New("license signature is invalid") ErrPublicKeyInvalid = errors.New("license public key is invalid") ErrMachineHashInvalid = errors.New("machine hash is invalid") ErrMachineMismatch = errors.New("license machine does not match") ) var ( licenseIDPattern = regexp.MustCompile(`^lic-[a-z0-9][a-z0-9-]{0,59}$`) machineHashPattern = regexp.MustCompile(`^[0-9a-f]{64}$`) productIDPattern = regexp.MustCompile(`^[a-z0-9-]+$`) policyIDPattern = regexp.MustCompile(`^[a-z0-9][a-z0-9._:-]{0,127}$`) timestampLayout = "2006-01-02T15:04:05Z" licenseFields = map[string]struct{}{ "schema_version": {}, "license_id": {}, "machine_hash": {}, "products": {}, "issued_at": {}, "perpetual": {}, "update_policy": {}, "rebind_policy": {}, "signature": {}, } ) // Verifier validates License v1 documents using one copied Ed25519 public key. type Verifier struct { publicKey ed25519.PublicKey } // NewVerifier copies and validates a License v1 signing public key. func NewVerifier(publicKey []byte) (Verifier, error) { if len(publicKey) != ed25519.PublicKeySize { return Verifier{}, ErrPublicKeyInvalid } return Verifier{publicKey: append(ed25519.PublicKey(nil), publicKey...)}, nil } // Verify validates one License v1 document and binds it to expectedMachineHash. func (verifier Verifier) Verify(document []byte, expectedMachineHash string) (License, error) { if len(verifier.publicKey) != ed25519.PublicKeySize { return License{}, ErrPublicKeyInvalid } if !machineHashPattern.MatchString(expectedMachineHash) { return License{}, ErrMachineHashInvalid } rootValue, err := canonicaljson.Parse(document) if err != nil { return License{}, licenseCanonicalError(err) } root, ok := rootValue.(map[string]any) if !ok { return License{}, ErrInvalidLicense } signatureValue, exists := root["signature"] if !exists { return License{}, ErrSignatureMissing } signatureText, ok := signatureValue.(string) if !ok { return License{}, ErrSignatureInvalid } hasExactFields := hasExactLicenseShape(root) delete(root, "signature") signedPayload, err := canonicaljson.Marshal(root) if err != nil { return License{}, licenseCanonicalError(err) } signature, err := decodeCanonicalSignature(signatureText) if err != nil || !ed25519.Verify(verifier.publicKey, signedPayload, signature) { return License{}, ErrSignatureInvalid } if !hasExactFields { return License{}, ErrInvalidLicense } wire, err := decodeLicense(document) if err != nil { return License{}, err } license, err := validateLicense(wire) if err != nil { return License{}, err } if license.MachineHash != expectedMachineHash { return License{}, ErrMachineMismatch } return license, nil } func hasExactLicenseShape(root map[string]any) bool { if len(root) != len(licenseFields) { return false } for field := range licenseFields { if _, exists := root[field]; !exists { return false } } if _, ok := root["schema_version"].(json.Number); !ok { return false } if _, ok := root["perpetual"].(bool); !ok { return false } if _, ok := root["products"].([]any); !ok { return false } for _, field := range []string{ "license_id", "machine_hash", "issued_at", "update_policy", "rebind_policy", "signature", } { if _, ok := root[field].(string); !ok { return false } } return true } type licenseWire struct { SchemaVersion int `json:"schema_version"` LicenseID string `json:"license_id"` MachineHash string `json:"machine_hash"` Products []string `json:"products"` IssuedAt string `json:"issued_at"` Perpetual bool `json:"perpetual"` UpdatePolicy string `json:"update_policy"` RebindPolicy string `json:"rebind_policy"` Signature string `json:"signature"` } func decodeLicense(document []byte) (licenseWire, error) { decoder := json.NewDecoder(bytes.NewReader(document)) decoder.DisallowUnknownFields() decoder.UseNumber() var wire licenseWire if err := decoder.Decode(&wire); err != nil { return licenseWire{}, ErrInvalidLicense } if err := consumeLicenseEOF(decoder); err != nil { return licenseWire{}, err } return wire, nil } func consumeLicenseEOF(decoder *json.Decoder) error { var trailing any if err := decoder.Decode(&trailing); err != io.EOF { return ErrInvalidLicense } return nil } func validateLicense(wire licenseWire) (License, error) { if wire.SchemaVersion != 1 || !licenseIDPattern.MatchString(wire.LicenseID) || !machineHashPattern.MatchString(wire.MachineHash) || !policyIDPattern.MatchString(wire.UpdatePolicy) || !policyIDPattern.MatchString(wire.RebindPolicy) || wire.Signature == "" { return License{}, ErrInvalidLicense } issuedAt, err := time.Parse(timestampLayout, wire.IssuedAt) if err != nil || issuedAt.Format(timestampLayout) != wire.IssuedAt { return License{}, ErrInvalidLicense } if len(wire.Products) == 0 { return License{}, ErrInvalidLicense } products := make([]string, len(wire.Products)) seenProducts := make(map[string]struct{}, len(wire.Products)) for index, product := range wire.Products { if !productIDPattern.MatchString(product) { return License{}, ErrInvalidLicense } if _, exists := seenProducts[product]; exists { return License{}, ErrInvalidLicense } seenProducts[product] = struct{}{} products[index] = product } return License{ LicenseID: wire.LicenseID, MachineHash: wire.MachineHash, Products: products, IssuedAt: issuedAt, Perpetual: wire.Perpetual, UpdatePolicy: wire.UpdatePolicy, RebindPolicy: wire.RebindPolicy, }, nil } func decodeCanonicalSignature(value string) ([]byte, error) { signature, err := base64.StdEncoding.Strict().DecodeString(value) if err != nil || base64.StdEncoding.EncodeToString(signature) != value || len(signature) != ed25519.SignatureSize { return nil, ErrSignatureInvalid } return signature, nil } func licenseCanonicalError(err error) error { switch { case errors.Is(err, canonicaljson.ErrDuplicateField): return ErrDuplicateField case errors.Is(err, canonicaljson.ErrUnsupportedNumber): return ErrUnsupportedNumber default: return ErrInvalidLicense } }