seal/open (bzw. wrap/unwrap) in allen vier betroffenen Stellen binden ein Additional Authenticated Data an Chiffretext/DEK-Wrapper: ein Austausch zwischen zwei Kontexten faellt beim Entschluesseln auf, statt still zu funktionieren. - dms/internal/crypto/envelope.go + service.go: AAD = Tenant-Slug (bereits vorhandener Parameter in Service.Seal/Open, Signatur unveraendert -- keine der rund 30 externen Aufrufstellen musste angepasst werden) - archive/internal/backupcrypto/envelope.go + reposecret.go/file.go: gleiches Muster, AAD = Tenant-Slug - mail/internal/crypto/envelope.go + service.go + kekversions.go: gleiches Muster, AAD = Tenant-Slug (inkl. ARC-08 KEK-Versionsverwaltung) - internal/kek/master.go + store.go: wrap/unwrap sowie EncryptWithTenantKEK/DecryptWithTenantKEK binden AAD. Die drei echten externen Aufrufer angepasst: ldapsync (Tenant-ID + Zweck), webhook (Ziel-URL des Abonnements), account-api modulekeys (fester Zweck-Bezeichner -- SecretCipher-Interface aus internal/moduletrust reicht keine Datensatz-ID durch, eine Erweiterung wuerde in andere Module hineinwirken, bewusst nicht Teil dieser Kachel) Rueckwaertskompatibel: jedes open/unwrap versucht zuerst mit AAD, faellt bei Fehlschlag auf kein-AAD zurueck -- vor dieser Aenderung versiegelte Datensaetze bleiben lesbar. Bekannte, dokumentierte Grenze: ein Austausch zwischen zwei Datensaetzen DESSELBEN Mandanten/Kontexts bleibt unerkannt (identisches AAD) -- volle Bindung an den einzelnen Datensatz braeuchte eine Datensatz-ID an rund 30 Aufrufstellen, bewusst nicht Teil dieser kleinstmoeglichen Loesung. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
143 lines
4.9 KiB
Go
143 lines
4.9 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"
|
|
// envKeyEncryptionKeyOld ist nur fuer `-rewrap-module-keys` (QA-15 F-25)
|
|
// relevant: der VORHERIGE Verschluesselungsschluessel, mit dem die
|
|
// gespeicherten Modul-Signaturschluessel gerade noch entschluesselbar
|
|
// sind, bevor sie auf envKeyEncryptionKey neu verpackt werden.
|
|
envKeyEncryptionKeyOld = "NEXARCH_ACCOUNT_MODULE_KEY_ENCRYPTION_KEY_OLD"
|
|
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 }
|
|
|
|
// AAD (F-21) ist ein fester Zweck-Bezeichner statt einer Datensatz-ID: das
|
|
// SecretCipher-Interface (internal/moduletrust) reicht keine Kennung des
|
|
// einzelnen Schluessels durch, eine Erweiterung wuerde in alle Module
|
|
// hineinwirken, die dasselbe Interface fuer Modul-/Sitzungs-Signaturschluessel
|
|
// verwenden (ausserhalb dieser Kachel). Bindet den Chiffretext zumindest an
|
|
// seinen Verwendungszweck.
|
|
const moduleSigningKeyAAD = "module-signing-key"
|
|
|
|
func (c kekCipher) Encrypt(plaintext []byte) ([]byte, error) {
|
|
return kek.EncryptWithTenantKEK(c.key, plaintext, []byte(moduleSigningKeyAAD))
|
|
}
|
|
|
|
func (c kekCipher) Decrypt(ciphertext []byte) ([]byte, error) {
|
|
return kek.DecryptWithTenantKEK(c.key, ciphertext, []byte(moduleSigningKeyAAD))
|
|
}
|
|
|
|
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,
|
|
})
|
|
}
|
|
|
|
// rewrapModuleSigningKeys verpackt alle gespeicherten Modul-Signaturschluessel
|
|
// von envKeyEncryptionKeyOld auf envKeyEncryptionKey um (QA-15 F-25). Beide
|
|
// Umgebungsvariablen muessen gesetzt sein; ephemere (nicht persistente)
|
|
// Schluesselverwaltung hat nichts zum Neuverpacken.
|
|
func rewrapModuleSigningKeys(ctx context.Context, pool *pgxpool.Pool) (int, error) {
|
|
oldKey, err := kek.LoadMasterKeyFromEnv(envKeyEncryptionKeyOld)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("%s: %w", envKeyEncryptionKeyOld, err)
|
|
}
|
|
newKey, err := kek.LoadMasterKeyFromEnv(envKeyEncryptionKey)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("%s: %w", envKeyEncryptionKey, err)
|
|
}
|
|
res, err := moduletrust.RewrapAll(ctx, moduletrust.NewPGKeyStore(pool), kekCipher{key: oldKey}, kekCipher{key: newKey})
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return res.Rewrapped, nil
|
|
}
|