// Package license implementiert Core LIC-01: Lizenzmodell je Tenant (Plan, // Modul-Umfang, Laufzeit) und die kryptographische Pruefung signierter // Lizenzschluessel. Feature-Flag-AUSWERTUNG zur Laufzeit (LIC-02) und die // Verwaltungsoberflaeche (LIC-04) sind ausdruecklich nicht Teil dieses Pakets // — hier geht es nur um Ausstellung/Validierung/Persistenz (Unleash-Vorbild: // klare Trennung Flag-Verwaltung vs. Flag-Auswertung). package license import ( "crypto/ed25519" "encoding/base64" "encoding/json" "errors" "fmt" "time" ) var ( ErrInvalidSignature = errors.New("license: signatur ungueltig") ErrMalformedKey = errors.New("license: lizenzschluessel hat ungueltiges format") ) // Payload ist der signierte Lizenzinhalt (Akzeptanzkriterium 3: Plan, // Modul-Liste, Laufzeit). type Payload struct { TenantSlug string `json:"tenant_slug"` Plan string `json:"plan"` Modules []string `json:"modules"` IssuedAt time.Time `json:"issued_at"` ValidUntil time.Time `json:"valid_until"` } // Issuer stellt signierte Lizenzschluessel aus. Haelt den PRIVATEN // Ed25519-Schluessel — lebt in der Praxis beim Lizenzgeber, nicht im // laufenden Core-Prozess (der nur den Validator mit dem oeffentlichen // Schluessel braucht). type Issuer struct { priv ed25519.PrivateKey } func NewIssuer(priv ed25519.PrivateKey) *Issuer { return &Issuer{priv: priv} } // Issue liefert den Lizenzschluessel im Format base64(payload-json) "." base64(signatur). func (i *Issuer) Issue(payload Payload) (string, error) { raw, err := json.Marshal(payload) if err != nil { return "", fmt.Errorf("payload serialisieren: %w", err) } sig := ed25519.Sign(i.priv, raw) return base64.RawURLEncoding.EncodeToString(raw) + "." + base64.RawURLEncoding.EncodeToString(sig), nil } // Validator prueft Lizenzschluessel gegen den OEFFENTLICHEN Ed25519-Schluessel // — das ist alles, was der laufende Core-Prozess kennen muss. type Validator struct { pub ed25519.PublicKey } func NewValidator(pub ed25519.PublicKey) *Validator { return &Validator{pub: pub} } // Parse prueft die Signatur (Akzeptanzkriterium 1 / Pruefung 1) und liefert // bei Erfolg den entschluesselten Payload. Ein manipulierter Schluessel wird // hier zuverlaessig erkannt, unabhaengig davon, ob die Laufzeit noch gueltig // waere — Signaturpruefung und Ablaufpruefung sind bewusst getrennt // (Signatur bei Einspielen, Ablauf bei jeder Nutzung, siehe Store.RequireActive). func (v *Validator) Parse(key string) (Payload, error) { rawPart, sigPart, ok := splitOnce(key, '.') if !ok { return Payload{}, ErrMalformedKey } raw, err := base64.RawURLEncoding.DecodeString(rawPart) if err != nil { return Payload{}, ErrMalformedKey } sig, err := base64.RawURLEncoding.DecodeString(sigPart) if err != nil { return Payload{}, ErrMalformedKey } if !ed25519.Verify(v.pub, raw, sig) { return Payload{}, ErrInvalidSignature } var p Payload if err := json.Unmarshal(raw, &p); err != nil { // Signatur war gueltig, aber Payload nicht mehr parsebar — sollte bei // unveraenderten Schluesseln nie vorkommen, trotzdem kein Panic. return Payload{}, fmt.Errorf("%w: payload nicht lesbar", ErrMalformedKey) } return p, nil } func splitOnce(s string, sep byte) (before, after string, ok bool) { for i := 0; i < len(s); i++ { if s[i] == sep { return s[:i], s[i+1:], true } } return "", "", false }