package platform import ( "crypto/ed25519" "crypto/rand" "encoding/json" "errors" "fmt" "net/url" "sort" "strconv" "strings" "time" "github.com/b1tsblog/license-platform/pkg/licensekit" ) type IssueInput struct { CustomerUserID string `json:"customerUserId,omitempty"` Customer string `json:"customer"` Product string `json:"product"` Edition string `json:"edition"` Features []string `json:"features,omitempty"` Limits map[string]int64 `json:"limits,omitempty"` Domains []string `json:"domains,omitempty"` InstanceIDs []string `json:"instanceIds,omitempty"` Mode string `json:"mode"` Days int `json:"days"` LeaseMinutes int `json:"leaseMinutes"` GraceHours int `json:"graceHours"` } type LicenseView struct { LicenseID string `json:"licenseId"` Issuer string `json:"issuer"` Customer string `json:"customer"` CustomerUserID string `json:"customerUserId,omitempty"` Product string `json:"product"` Edition string `json:"edition"` Features []string `json:"features,omitempty"` Limits map[string]int64 `json:"limits,omitempty"` Domains []string `json:"domains,omitempty"` InstanceIDs []string `json:"instanceIds,omitempty"` Mode string `json:"mode"` ServerURL string `json:"serverUrl,omitempty"` ExpiresAt int64 `json:"expiresAt"` Revoked bool `json:"revoked"` Reason string `json:"reason,omitempty"` Token string `json:"token,omitempty"` CreatedAt int64 `json:"createdAt"` UpdatedAt int64 `json:"updatedAt"` } type validateRequest struct { Token string `json:"token"` Product string `json:"product"` BaseURL string `json:"baseUrl"` Host string `json:"host,omitempty"` InstanceID string `json:"instanceId,omitempty"` ClientVersion string `json:"clientVersion,omitempty"` } type validateResponse struct { Valid bool `json:"valid"` LeaseToken string `json:"leaseToken,omitempty"` ExpiresAt string `json:"expiresAt,omitempty"` Reason string `json:"reason,omitempty"` } func (s *Server) generateKeys(issuerKeyID, leaseKeyID string) error { issuerKeyID = strings.TrimSpace(issuerKeyID) leaseKeyID = strings.TrimSpace(leaseKeyID) if !validKeyID(issuerKeyID) || !validKeyID(leaseKeyID) { return errors.New("key IDs must be 1-120 URL-safe characters") } issuerPub, issuerPriv, err := ed25519.GenerateKey(rand.Reader) if err != nil { return err } leasePub, leasePriv, err := ed25519.GenerateKey(rand.Reader) if err != nil { return err } issuerCipher, err := s.vault.Encrypt(licensekit.EncodeKey(issuerPriv)) if err != nil { return err } leaseCipher, err := s.vault.Encrypt(licensekit.EncodeKey(leasePriv)) if err != nil { return err } return s.store.SetKeysOnce(KeySet{ IssuerKeyID: issuerKeyID, IssuerPublicKey: licensekit.EncodeKey(issuerPub), IssuerPrivateCipher: issuerCipher, LeaseKeyID: leaseKeyID, LeasePublicKey: licensekit.EncodeKey(leasePub), LeasePrivateCipher: leaseCipher, }) } func (s *Server) importKeys(issuerKeyID, issuerPrivateEncoded, leaseKeyID, leasePrivateEncoded string) error { issuerKeyID = strings.TrimSpace(issuerKeyID) leaseKeyID = strings.TrimSpace(leaseKeyID) if !validKeyID(issuerKeyID) || !validKeyID(leaseKeyID) { return errors.New("key IDs must be 1-120 URL-safe characters") } issuerPrivate, err := licensekit.DecodePrivateKey(strings.TrimSpace(issuerPrivateEncoded)) if err != nil { return fmt.Errorf("issuer private key: %w", err) } leasePrivate, err := licensekit.DecodePrivateKey(strings.TrimSpace(leasePrivateEncoded)) if err != nil { return fmt.Errorf("lease private key: %w", err) } issuerCipher, err := s.vault.Encrypt(licensekit.EncodeKey(issuerPrivate)) if err != nil { return err } leaseCipher, err := s.vault.Encrypt(licensekit.EncodeKey(leasePrivate)) if err != nil { return err } return s.store.SetKeysOnce(KeySet{ IssuerKeyID: issuerKeyID, IssuerPublicKey: licensekit.EncodeKey(issuerPrivate.Public().(ed25519.PublicKey)), IssuerPrivateCipher: issuerCipher, LeaseKeyID: leaseKeyID, LeasePublicKey: licensekit.EncodeKey(leasePrivate.Public().(ed25519.PublicKey)), LeasePrivateCipher: leaseCipher, }) } func (s *Server) trustStore() (licensekit.TrustStore, error) { keys, ok := s.store.KeySet() if !ok { return licensekit.TrustStore{}, errors.New("key pairs have not been initialized") } store := licensekit.NewTrustStore() store.LicenseKeys[keys.IssuerKeyID] = keys.IssuerPublicKey store.LeaseKeys[keys.LeaseKeyID] = keys.LeasePublicKey return store, nil } func (s *Server) issueLicense(actor User, input IssueInput) (LicenseRecord, string, error) { if actor.Role != RoleAdmin && actor.Role != RoleReseller { return LicenseRecord{}, "", errors.New("issuer role required") } keys, ok := s.store.KeySet() if !ok { return LicenseRecord{}, "", errors.New("key pairs have not been initialized") } input.Customer = strings.TrimSpace(input.Customer) input.Product = strings.TrimSpace(input.Product) input.Edition = strings.TrimSpace(input.Edition) if input.Customer == "" || input.Product == "" || input.Edition == "" { return LicenseRecord{}, "", errors.New("customer, product and edition are required") } if len(input.Customer) > 300 || len(input.Product) > 200 || len(input.Edition) > 100 { return LicenseRecord{}, "", errors.New("customer, product or edition is too long") } for _, value := range append(append([]string{}, input.Features...), append(input.Domains, input.InstanceIDs...)...) { if len(strings.TrimSpace(value)) > 300 { return LicenseRecord{}, "", errors.New("feature, domain or instance value is too long") } } mode, err := licensekit.ParseMode(input.Mode) if err != nil { return LicenseRecord{}, "", err } if input.Days < 1 || input.Days > 3650 { return LicenseRecord{}, "", errors.New("days must be between 1 and 3650") } if input.LeaseMinutes <= 0 { input.LeaseMinutes = 60 } if input.LeaseMinutes > 1440 || input.GraceHours < 0 || input.GraceHours > 8760 { return LicenseRecord{}, "", errors.New("lease or grace duration is outside the allowed range") } if input.CustomerUserID != "" { customerUser, ok := s.store.GetUser(input.CustomerUserID) if !ok || customerUser.Role != RoleCustomer || !customerUser.Active { return LicenseRecord{}, "", errors.New("customer portal account was not found") } if actor.Role == RoleReseller && customerUser.ParentUserID != actor.ID { return LicenseRecord{}, "", errors.New("customer portal account is not assigned to this reseller") } } privateEncoded, err := s.vault.Decrypt(keys.IssuerPrivateCipher) if err != nil { return LicenseRecord{}, "", err } privateKey, err := licensekit.DecodePrivateKey(privateEncoded) if err != nil { return LicenseRecord{}, "", err } now := time.Now().UTC() licenseID := newID("lic") serverURL := "" if mode != licensekit.ModeOffline { serverURL = s.cfg.PublicURL } claims := licensekit.Claims{ Version: 1, LicenseID: licenseID, Issuer: actor.DisplayName, Customer: input.Customer, Product: input.Product, Edition: input.Edition, Features: unique(input.Features), Limits: input.Limits, Domains: unique(input.Domains), InstanceIDs: unique(input.InstanceIDs), IssuedAt: now.Unix(), ExpiresAt: now.Add(time.Duration(input.Days) * 24 * time.Hour).Unix(), Verification: licensekit.VerificationPolicy{Mode: mode, LeaseTTLSeconds: int64(time.Duration(input.LeaseMinutes) * time.Minute / time.Second), OfflineGraceSeconds: int64(time.Duration(input.GraceHours) * time.Hour / time.Second), ServerURL: serverURL}, Metadata: map[string]string{"issuedByUserId": actor.ID}, } token, err := licensekit.SignLicense(privateKey, keys.IssuerKeyID, claims) if err != nil { return LicenseRecord{}, "", err } ciphertext, err := s.vault.Encrypt(token) if err != nil { return LicenseRecord{}, "", err } record := LicenseRecord{ LicenseID: licenseID, TokenCipher: ciphertext, TokenHash: licensekit.TokenHash(token), Issuer: claims.Issuer, Customer: claims.Customer, CustomerUserID: input.CustomerUserID, IssuedByUserID: actor.ID, Product: claims.Product, Edition: claims.Edition, Features: claims.Features, Limits: claims.Limits, Domains: claims.Domains, InstanceIDs: claims.InstanceIDs, Mode: string(mode), ServerURL: serverURL, ExpiresAt: claims.ExpiresAt, } if err := s.store.PutLicense(record); err != nil { return LicenseRecord{}, "", err } stored, _ := s.store.GetLicense(licenseID) return stored, token, nil } func (s *Server) importLicense(actor User, token, customerUserID string) (LicenseRecord, error) { if actor.Role != RoleAdmin && actor.Role != RoleReseller { return LicenseRecord{}, errors.New("issuer role required") } token = strings.TrimSpace(token) if token == "" { return LicenseRecord{}, errors.New("license token is required") } trust, err := s.trustStore() if err != nil { return LicenseRecord{}, err } verified, err := licensekit.VerifyLicense(trust, token, time.Now().UTC()) if err != nil { return LicenseRecord{}, err } claims := verified.Claims if existing, ok := s.store.GetLicense(claims.LicenseID); ok { if existing.TokenHash == licensekit.TokenHash(token) { return existing, nil } return LicenseRecord{}, errors.New("license ID already exists with another token") } if customerUserID != "" { customer, ok := s.store.GetUser(customerUserID) if !ok || customer.Role != RoleCustomer || !customer.Active { return LicenseRecord{}, errors.New("customer portal account was not found") } if actor.Role == RoleReseller && customer.ParentUserID != actor.ID { return LicenseRecord{}, errors.New("customer portal account is not assigned to this reseller") } } ciphertext, err := s.vault.Encrypt(token) if err != nil { return LicenseRecord{}, err } record := LicenseRecord{ LicenseID: claims.LicenseID, TokenCipher: ciphertext, TokenHash: licensekit.TokenHash(token), Issuer: claims.Issuer, Customer: claims.Customer, CustomerUserID: customerUserID, IssuedByUserID: actor.ID, Product: claims.Product, Edition: claims.Edition, Features: claims.Features, Limits: claims.Limits, Domains: claims.Domains, InstanceIDs: claims.InstanceIDs, Mode: string(claims.Verification.Mode), ServerURL: strings.TrimRight(strings.TrimSpace(claims.Verification.ServerURL), "/"), ExpiresAt: claims.ExpiresAt, } if err := s.store.PutLicense(record); err != nil { return LicenseRecord{}, err } stored, _ := s.store.GetLicense(record.LicenseID) return stored, nil } func (s *Server) tokenFor(record LicenseRecord) (string, error) { return s.vault.Decrypt(record.TokenCipher) } func (s *Server) validateLicense(request validateRequest) (validateResponse, error) { store, err := s.trustStore() if err != nil { return validateResponse{Reason: err.Error()}, err } now := time.Now().UTC() verified, err := licensekit.VerifyLicense(store, request.Token, now) if err != nil { return validateResponse{Reason: err.Error()}, err } claims := verified.Claims if err := licensekit.ValidateLicenseContext(claims, request.Product, request.BaseURL, request.InstanceID); err != nil { return validateResponse{Reason: err.Error()}, err } record, ok := s.store.GetLicense(claims.LicenseID) if !ok { err := errors.New("license is not registered") return validateResponse{Reason: err.Error()}, err } if record.TokenHash != licensekit.TokenHash(request.Token) { err := errors.New("registered token does not match") return validateResponse{Reason: err.Error()}, err } if record.Revoked { reason := "license is revoked" if record.RevocationReason != "" { reason += ": " + record.RevocationReason } return validateResponse{Reason: reason}, errors.New(reason) } keys, _ := s.store.KeySet() leaseEncoded, err := s.vault.Decrypt(keys.LeasePrivateCipher) if err != nil { return validateResponse{Reason: "lease signing unavailable"}, err } leaseKey, err := licensekit.DecodePrivateKey(leaseEncoded) if err != nil { return validateResponse{Reason: "lease signing unavailable"}, err } ttl := s.cfg.DefaultLeaseTTL if claims.Verification.LeaseTTLSeconds > 0 { ttl = time.Duration(claims.Verification.LeaseTTLSeconds) * time.Second } if ttl > s.cfg.MaxLeaseTTL { ttl = s.cfg.MaxLeaseTTL } if remaining := time.Until(time.Unix(claims.ExpiresAt, 0)); ttl > remaining { ttl = remaining } if ttl <= 0 { return validateResponse{Reason: "license has expired"}, errors.New("license has expired") } host, _ := licensekit.HostFromBaseURL(request.BaseURL) leaseClaims := licensekit.LeaseClaims{Version: 1, LeaseID: newID("lease"), LicenseID: claims.LicenseID, Product: claims.Product, Customer: claims.Customer, Edition: claims.Edition, Features: claims.Features, Host: host, InstanceID: request.InstanceID, IssuedAt: now.Unix(), ExpiresAt: now.Add(ttl).Unix()} lease, err := licensekit.SignLease(leaseKey, keys.LeaseKeyID, leaseClaims) if err != nil { return validateResponse{Reason: "lease signing failed"}, err } return validateResponse{Valid: true, LeaseToken: lease, ExpiresAt: time.Unix(leaseClaims.ExpiresAt, 0).UTC().Format(time.RFC3339)}, nil } func licenseView(record LicenseRecord, token string) LicenseView { return LicenseView{LicenseID: record.LicenseID, Issuer: record.Issuer, Customer: record.Customer, CustomerUserID: record.CustomerUserID, Product: record.Product, Edition: record.Edition, Features: record.Features, Limits: record.Limits, Domains: record.Domains, InstanceIDs: record.InstanceIDs, Mode: record.Mode, ServerURL: record.ServerURL, ExpiresAt: record.ExpiresAt, Revoked: record.Revoked, Reason: record.RevocationReason, Token: token, CreatedAt: record.CreatedAt, UpdatedAt: record.UpdatedAt} } func parseIssueForm(values url.Values) (IssueInput, error) { days, err := parseBoundedInt(values.Get("days"), 1, 3650) if err != nil { return IssueInput{}, fmt.Errorf("days: %w", err) } leaseMinutes, err := parseBoundedInt(values.Get("leaseMinutes"), 1, 1440) if err != nil { return IssueInput{}, fmt.Errorf("lease minutes: %w", err) } graceHours, err := parseBoundedInt(values.Get("graceHours"), 0, 8760) if err != nil { return IssueInput{}, fmt.Errorf("grace hours: %w", err) } limits, err := parseLimits(values.Get("limits")) if err != nil { return IssueInput{}, err } return IssueInput{CustomerUserID: strings.TrimSpace(values.Get("customerUserId")), Customer: values.Get("customer"), Product: values.Get("product"), Edition: values.Get("edition"), Features: csv(values.Get("features")), Limits: limits, Domains: csv(values.Get("domains")), InstanceIDs: csv(values.Get("instances")), Mode: values.Get("mode"), Days: days, LeaseMinutes: leaseMinutes, GraceHours: graceHours}, nil } func parseBoundedInt(value string, min, max int) (int, error) { n, err := strconv.Atoi(strings.TrimSpace(value)) if err != nil || n < min || n > max { return 0, fmt.Errorf("must be between %d and %d", min, max) } return n, nil } func parseLimits(value string) (map[string]int64, error) { limits := map[string]int64{} for _, line := range strings.Split(value, "\n") { line = strings.TrimSpace(line) if line == "" { continue } parts := strings.SplitN(line, "=", 2) if len(parts) != 2 || strings.TrimSpace(parts[0]) == "" { return nil, fmt.Errorf("invalid limit %q; expected name=value", line) } value, err := strconv.ParseInt(strings.TrimSpace(parts[1]), 10, 64) if err != nil || value < 0 { return nil, fmt.Errorf("invalid limit %q", line) } limits[strings.TrimSpace(parts[0])] = value } return limits, nil } func csv(value string) []string { return unique(strings.Split(value, ",")) } func unique(values []string) []string { seen := map[string]bool{} out := make([]string, 0, len(values)) for _, value := range values { value = strings.TrimSpace(value) if value != "" && !seen[value] { seen[value] = true out = append(out, value) } } sort.Strings(out) return out } func newID(prefix string) string { raw, err := randomToken(12) if err != nil { return fmt.Sprintf("%s_%d", prefix, time.Now().UTC().UnixNano()) } return prefix + "_" + raw } func trustStoreJSON(store licensekit.TrustStore) string { data, _ := json.MarshalIndent(store, "", " ") return string(data) } func validKeyID(value string) bool { if len(value) < 1 || len(value) > 120 { return false } for _, r := range value { if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '.' || r == '-' || r == '_' || r == ':' { continue } return false } return true }