Files
nexarch/internal/moduletrust/pgstore.go
patrickandClaude Sonnet 5 74c052a3e4
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
OPS-10: F-25 Neuverpackung der Modul-Signaturschluessel bei Schluesselwechsel (RewrapAll, -rewrap-module-keys)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 10:24:50 +02:00

102 lines
2.8 KiB
Go

package moduletrust
import (
"context"
"fmt"
"time"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
// advisoryLockKey serialisiert Schluesselaenderungen aller Instanzen einer Mandanten-Datenbank.
const advisoryLockKey int64 = 0x4e5841524d4b53 // "NXARMKS"
type PGKeyStore struct {
pool *pgxpool.Pool
}
func NewPGKeyStore(pool *pgxpool.Pool) *PGKeyStore {
return &PGKeyStore{pool: pool}
}
const selectKeys = `SELECT kid, public_key, private_key_ciphertext, created_at, active_from, retire_after
FROM module_signing_keys ORDER BY active_from, created_at`
type querier interface {
Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error)
}
func listKeys(ctx context.Context, q querier) ([]StoredKey, error) {
rows, err := q.Query(ctx, selectKeys)
if err != nil {
return nil, err
}
defer rows.Close()
var out []StoredKey
for rows.Next() {
var k StoredKey
var pub []byte
if err := rows.Scan(&k.KID, &pub, &k.PrivateCiphertext, &k.CreatedAt, &k.ActiveFrom, &k.RetireAfter); err != nil {
return nil, err
}
k.Public = pub
out = append(out, k)
}
return out, rows.Err()
}
func (s *PGKeyStore) Load(ctx context.Context) ([]StoredKey, error) {
return listKeys(ctx, s.pool)
}
func (s *PGKeyStore) Update(ctx context.Context, fn func(tx KeyTx) error) error {
tx, err := s.pool.Begin(ctx)
if err != nil {
return err
}
defer func() { _ = tx.Rollback(ctx) }()
if _, err := tx.Exec(ctx, `SELECT pg_advisory_xact_lock($1)`, advisoryLockKey); err != nil {
return err
}
if err := fn(&pgKeyTx{tx: tx}); err != nil {
return err
}
return tx.Commit(ctx)
}
type pgKeyTx struct {
tx pgx.Tx
}
func (t *pgKeyTx) List(ctx context.Context) ([]StoredKey, error) {
return listKeys(ctx, t.tx)
}
func (t *pgKeyTx) Insert(ctx context.Context, k StoredKey) error {
_, err := t.tx.Exec(ctx, `INSERT INTO module_signing_keys (kid, public_key, private_key_ciphertext, created_at, active_from, retire_after)
VALUES ($1, $2, $3, $4, $5, $6)`, k.KID, []byte(k.Public), k.PrivateCiphertext, k.CreatedAt, k.ActiveFrom, k.RetireAfter)
return err
}
func (t *pgKeyTx) UpdateCiphertext(ctx context.Context, kid string, ciphertext []byte) error {
tag, err := t.tx.Exec(ctx, `UPDATE module_signing_keys SET private_key_ciphertext = $2 WHERE kid = $1`, kid, ciphertext)
if err != nil {
return err
}
if tag.RowsAffected() == 0 {
return fmt.Errorf("kid %q unbekannt", kid)
}
return nil
}
func (t *pgKeyTx) SetRetireAfter(ctx context.Context, kid string, at time.Time) error {
_, err := t.tx.Exec(ctx, `UPDATE module_signing_keys SET retire_after = $2 WHERE kid = $1`, kid, at)
return err
}
func (t *pgKeyTx) DeleteRetiredBefore(ctx context.Context, at time.Time) error {
_, err := t.tx.Exec(ctx, `DELETE FROM module_signing_keys WHERE retire_after IS NOT NULL AND retire_after <= $1`, at)
return err
}