Files
nexarch/internal/license/license.go
sysopsandClaude Sonnet 5 d63db93a4c LIC-01: lizenzmodell-lizenzschluessel-pruefung
internal/license: Ed25519-signierte Lizenzschluessel (stdlib crypto/ed25519,
keine neue Abhaengigkeit). Issuer haelt den privaten Schluessel (lebt beim
Lizenzgeber), Validator nur den oeffentlichen (lebt im Core-Prozess) — klare
Trennung Ausstellung/Pruefung nach Unleash-Vorbild (Flag-Verwaltung vs.
Flag-Auswertung).

Store.Install prueft NUR die Signatur und persistiert den Lizenzumfang
(Plan, Modul-Liste, Laufzeit) in tenant_licenses (Registry-DB, 1:1 zu
tenants). Eine bereits abgelaufene, aber korrekt signierte Lizenz laesst
sich trotzdem einspielen — der Ablauf wird erst bei Store.RequireActive
bewertet (liefert ErrLicenseExpired statt Panic/Absturz), waehrend
Store.Status den Umfang unabhaengig vom Ablauf weiterhin liefert.

Neu: scripts/run-checks.sh buendelt reset-test-env.sh + go build/vet/test
(-p 1) zu einem Ein-Kommando-Check fuer den Testhost.

Pruefungen (ausgefuehrt auf root@192.168.1.131, go build/vet/test PASS):
1. Manipulierter Lizenzschluessel zuverlaessig erkannt —
   TestParse_RejectsTamperedKey, TestParse_RejectsWrongKeyPair,
   TestStore_InstallRejectsInvalidSignature. PASS.
2. Ablauf loest definierten eingeschraenkten Zustand aus, kein harter
   Systemausfall — TestStore_RequireActive_DetectsExpiry (inkl. recover()-
   Absicherung im Test, dass kein Panic auftritt), ErrLicenseExpired statt
   Absturz; Status bleibt trotzdem abfragbar. PASS.
3. Signaturpruefung von zweiter Person gegen Dokumentation nachvollzogen —
   NICHT durchgefuehrt (keine zweite Person in dieser Session verfuegbar).
   Offen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 19:12:13 +02:00

107 lines
3.3 KiB
Go

// 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
}