package license import ( "crypto/ed25519" "encoding/base64" "encoding/json" "fmt" "math/rand/v2" "strings" "time" ) // Laufzeiten der Leases (Spec 4.1a). const ( CommunityLease = 14 * 24 * time.Hour CommunityRenewAfter = 7 * 24 * time.Hour CommunityRenewJitter = 12 * time.Hour CommercialLease = 7 * 24 * time.Hour CommercialMaintenance = 10 * 24 * time.Hour ) // LeaseTerms sind Erneuerungs- und Ablaufzeitpunkt einer Lease. type LeaseTerms struct { RenewAfter time.Time ValidUntil time.Time } // TermsFor berechnet die Lease-Zeiten. Kommerziell: 7 Tage (in Wartung 10), // Erneuerung nach 2/3, höchstens bis zum Vertragsende; nach Vertragsende // gibt es keine Lease (ok=false). Community: 14 Tage, Erneuerung nach // 7 Tagen ± Streuung (jitter liefert einen Wert in [-max, +max]; nil = // zufällig), Vertragsende ohne Bedeutung. override > 0 setzt eine feste // Laufzeit (Tests, Betrieb), Erneuerung dann nach 2/3. func TermsFor(tier Tier, maintenance bool, licenseUntil, now time.Time, override time.Duration, jitter func(max time.Duration) time.Duration) (LeaseTerms, bool) { commercial := tier != TierCommunity if commercial && !now.Before(licenseUntil) { return LeaseTerms{}, false } var t LeaseTerms switch { case override > 0: t.ValidUntil = now.Add(override) case !commercial: if jitter == nil { jitter = randomJitter } return LeaseTerms{ ValidUntil: now.Add(CommunityLease), RenewAfter: now.Add(CommunityRenewAfter + jitter(CommunityRenewJitter)), }, true case maintenance: t.ValidUntil = now.Add(CommercialMaintenance) default: t.ValidUntil = now.Add(CommercialLease) } if commercial && t.ValidUntil.After(licenseUntil) { t.ValidUntil = licenseUntil } t.RenewAfter = now.Add(t.ValidUntil.Sub(now) * 2 / 3) return t, true } func randomJitter(max time.Duration) time.Duration { return time.Duration(rand.Int64N(int64(2*max)+1)) - max } // Lease ist die signierte, kurzlaufende Kopie der Lizenz für einen Knoten. type Lease struct { NodeID string `json:"node_id"` TenantID string `json:"tenant_id"` Tier Tier `json:"tier"` MaxRoutes int `json:"max_routes"` AllowedDomains []string `json:"allowed_domains"` Features []Feature `json:"features"` IssuedAt time.Time `json:"issued_at"` RenewAfter time.Time `json:"renew_after"` ValidUntil time.Time `json:"valid_until"` Signature string `json:"signature"` } // Eigenes Präfix: Eine Lease-Signatur gilt nie als Lizenz-Signatur und umgekehrt. const leaseSignatureVersion = "SULTAN-LEASE-V1\n" func (l Lease) message() []byte { c := l c.Signature = "" c.IssuedAt, c.RenewAfter, c.ValidUntil = c.IssuedAt.UTC(), c.RenewAfter.UTC(), c.ValidUntil.UTC() b, _ := json.Marshal(c) return append([]byte(leaseSignatureVersion), b...) } // IssueLease stellt eine Lease für nodeID aus den Werten von l aus. func (i *Issuer) IssueLease(l License, nodeID string, terms LeaseTerms, now time.Time) (Lease, error) { if len(i.Key) != ed25519.PrivateKeySize { return Lease{}, fmt.Errorf("license: Issuer ohne gültigen Master-Key") } if strings.TrimSpace(nodeID) == "" { return Lease{}, fmt.Errorf("%w: node_id fehlt", ErrInvalid) } utc := func(t time.Time) time.Time { return t.Round(0).UTC() } lease := Lease{ NodeID: nodeID, TenantID: l.TenantID, Tier: l.Tier, MaxRoutes: l.MaxRoutes, AllowedDomains: append([]string{}, l.AllowedDomains...), Features: append([]Feature{}, l.Features...), IssuedAt: utc(now), RenewAfter: utc(terms.RenewAfter), ValidUntil: utc(terms.ValidUntil), } lease.Signature = base64.StdEncoding.EncodeToString(ed25519.Sign(i.Key, lease.message())) return lease, nil } // Verify prüft die Signatur mit dem Public Key des Master-Keys. func (l Lease) Verify(pub ed25519.PublicKey) error { sig, err := base64.StdEncoding.DecodeString(l.Signature) if err != nil || len(sig) != ed25519.SignatureSize || len(pub) != ed25519.PublicKeySize { return ErrInvalidSignature } if !ed25519.Verify(pub, l.message(), sig) { return ErrInvalidSignature } return nil } // Expired meldet, ob die Lease zum Zeitpunkt now abgelaufen ist. func (l Lease) Expired(now time.Time) bool { return !now.Before(l.ValidUntil) } // ParseLease liest und prüft eine Lease (unbekannte Felder sind Fehler). func ParseLease(data []byte, pub ed25519.PublicKey) (Lease, error) { dec := json.NewDecoder(strings.NewReader(string(data))) dec.DisallowUnknownFields() var l Lease if err := dec.Decode(&l); err != nil { return Lease{}, fmt.Errorf("%w: %v", ErrInvalid, err) } if l.NodeID == "" || l.ValidUntil.IsZero() { return Lease{}, fmt.Errorf("%w: node_id oder valid_until fehlt", ErrInvalid) } if l.AllowedDomains == nil { l.AllowedDomains = []string{} } if l.Features == nil { l.Features = []Feature{} } if err := l.Verify(pub); err != nil { return Lease{}, err } return l, nil }