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>
107 lines
3.3 KiB
Go
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
|
|
}
|