154 lines
4.9 KiB
Go
154 lines
4.9 KiB
Go
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
|
|
}
|