Files
nexarch/cmd/account-api/modulekeys.go
T
patrickandClaude Sonnet 5 e57b0d23b6
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (push) Canceled after 0s
CI / Core-Modul (Build/Vet/Test) (push) Canceled after 0s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (push) Canceled after 0s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (push) Canceled after 0s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (push) Canceled after 0s
CI / Web (dms-browser) (push) Canceled after 0s
CI / Web (pdf-viewer) (push) Canceled after 0s
CI / Web (shl) (push) Canceled after 0s
Core-Schnittstellen-Vertragstests / contract-tests (push) Canceled after 0s
Supply-Chain-Scan (Go) / govulncheck (push) Canceled after 0s
API-16: persistente modul-signaturschlüssel und rotation
Signaturschluessel der Modul-JWTs liegen verschluesselt in der Mandanten-Datenbank
(module_signing_keys), werden von mehreren Instanzen geteilt und rotieren mit
Ueberlappung; nicht lesbarer Speicher ergibt 503 statt Token/JWKS.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 23:42:00 +02:00

110 lines
3.2 KiB
Go

package main
import (
"context"
"fmt"
"strconv"
"time"
"github.com/jackc/pgx/v5/pgxpool"
"gitea.perlbach24.de/scripte/nexarch/internal/kek"
"gitea.perlbach24.de/scripte/nexarch/internal/moduletrust"
)
const (
envKeyEncryptionKey = "NEXARCH_ACCOUNT_MODULE_KEY_ENCRYPTION_KEY"
envKeysEphemeral = "NEXARCH_ACCOUNT_MODULE_KEYS_EPHEMERAL"
envKeyRotationDays = "NEXARCH_ACCOUNT_MODULE_KEY_ROTATION_DAYS"
envKeyActivationSecs = "NEXARCH_ACCOUNT_MODULE_KEY_ACTIVATION_DELAY_SECONDS"
envKeyRefreshSecs = "NEXARCH_ACCOUNT_MODULE_KEY_REFRESH_SECONDS"
defaultRotationDays = 30
defaultActivationSecs = 120
defaultRefreshSecs = 30
)
type moduleKeyConfig struct {
ephemeral bool
rotationInterval time.Duration
activationDelay time.Duration
refreshInterval time.Duration
}
func intEnv(getenv func(string) string, name string, def, min, max int) (int, error) {
raw := getenv(name)
if raw == "" {
return def, nil
}
n, err := strconv.Atoi(raw)
if err != nil {
return 0, fmt.Errorf("%s: %q ist keine ganze zahl", name, raw)
}
if n < min || n > max {
return 0, fmt.Errorf("%s: %d ausserhalb von %d..%d", name, n, min, max)
}
return n, nil
}
func parseModuleKeyConfig(getenv func(string) string) (moduleKeyConfig, error) {
var c moduleKeyConfig
switch getenv(envKeysEphemeral) {
case "", "false":
case "true":
c.ephemeral = true
default:
return c, fmt.Errorf("%s: erlaubt sind true oder false", envKeysEphemeral)
}
days, err := intEnv(getenv, envKeyRotationDays, defaultRotationDays, 0, 3650)
if err != nil {
return c, err
}
act, err := intEnv(getenv, envKeyActivationSecs, defaultActivationSecs, 60, 3600)
if err != nil {
return c, err
}
refresh, err := intEnv(getenv, envKeyRefreshSecs, defaultRefreshSecs, 1, 300)
if err != nil {
return c, err
}
if act < refresh {
return c, fmt.Errorf("%s (%ds) darf nicht kuerzer sein als %s (%ds)", envKeyActivationSecs, act, envKeyRefreshSecs, refresh)
}
c.rotationInterval = time.Duration(days) * 24 * time.Hour
c.activationDelay = time.Duration(act) * time.Second
c.refreshInterval = time.Duration(refresh) * time.Second
return c, nil
}
type kekCipher struct{ key kek.MasterKey }
func (c kekCipher) Encrypt(plaintext []byte) ([]byte, error) {
return kek.EncryptWithTenantKEK(c.key, plaintext)
}
func (c kekCipher) Decrypt(ciphertext []byte) ([]byte, error) {
return kek.DecryptWithTenantKEK(c.key, ciphertext)
}
func newModuleKeyManager(ctx context.Context, pool *pgxpool.Pool, getenv func(string) string, tokenTTL time.Duration) (*moduletrust.KeyManager, error) {
cfg, err := parseModuleKeyConfig(getenv)
if err != nil {
return nil, err
}
if cfg.ephemeral {
return moduletrust.NewKeyManager()
}
key, err := kek.LoadMasterKeyFromEnv(envKeyEncryptionKey)
if err != nil {
return nil, fmt.Errorf("%s: %w (ohne persistente schluessel nur mit %s=true, dann ungueltig nach jedem neustart)", envKeyEncryptionKey, err, envKeysEphemeral)
}
return moduletrust.NewPersistentKeyManager(ctx, moduletrust.PersistentConfig{
Store: moduletrust.NewPGKeyStore(pool),
Cipher: kekCipher{key: key},
TokenTTL: tokenTTL,
ActivationDelay: cfg.activationDelay,
RotationInterval: cfg.rotationInterval,
RefreshInterval: cfg.refreshInterval,
})
}