Files
patrick 95f60eddea
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
Merge-Fix: pgSessionKeyTx implementiert UpdateCiphertext (KeyTx-Interface-Erweiterung aus F-25 kollidierte still mit F-10 Sitzungsschluesseln)
2026-09-22 11:23:43 +02:00

105 lines
3.3 KiB
Go

package moduletrust
import (
"context"
"fmt"
"time"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
// advisorySessionLockKey serialisiert Schluesselaenderungen der
// Sitzungstokens (QA-15 F-10) — eigener Schluessel, damit eine Rotation der
// Sitzungs- und der Modul-Signaturschluessel (advisoryLockKey) einander
// nicht gegenseitig blockiert.
const advisorySessionLockKey int64 = 0x4e5841524d534b // "NXARMSK"
// SessionPGKeyStore ist PGKeyStore fuer die Tabelle session_signing_keys
// (Migration 0010) — gleiche Logik wie PGKeyStore/module_signing_keys, aber
// eigene Tabelle und eigene Advisory-Lock-Kennung, weil Sitzungs- und
// Modul-Signaturschluessel getrennte Sicherheitsdomaenen sind.
type SessionPGKeyStore struct {
pool *pgxpool.Pool
}
func NewSessionPGKeyStore(pool *pgxpool.Pool) *SessionPGKeyStore {
return &SessionPGKeyStore{pool: pool}
}
const selectSessionKeys = `SELECT kid, public_key, private_key_ciphertext, created_at, active_from, retire_after
FROM session_signing_keys ORDER BY active_from, created_at`
func listSessionKeys(ctx context.Context, q querier) ([]StoredKey, error) {
rows, err := q.Query(ctx, selectSessionKeys)
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 *SessionPGKeyStore) Load(ctx context.Context) ([]StoredKey, error) {
return listSessionKeys(ctx, s.pool)
}
func (s *SessionPGKeyStore) 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)`, advisorySessionLockKey); err != nil {
return err
}
if err := fn(&pgSessionKeyTx{tx: tx}); err != nil {
return err
}
return tx.Commit(ctx)
}
type pgSessionKeyTx struct {
tx pgx.Tx
}
func (t *pgSessionKeyTx) List(ctx context.Context) ([]StoredKey, error) {
return listSessionKeys(ctx, t.tx)
}
func (t *pgSessionKeyTx) Insert(ctx context.Context, k StoredKey) error {
_, err := t.tx.Exec(ctx, `INSERT INTO session_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 *pgSessionKeyTx) SetRetireAfter(ctx context.Context, kid string, at time.Time) error {
_, err := t.tx.Exec(ctx, `UPDATE session_signing_keys SET retire_after = $2 WHERE kid = $1`, kid, at)
return err
}
func (t *pgSessionKeyTx) DeleteRetiredBefore(ctx context.Context, at time.Time) error {
_, err := t.tx.Exec(ctx, `DELETE FROM session_signing_keys WHERE retire_after IS NOT NULL AND retire_after <= $1`, at)
return err
}
func (t *pgSessionKeyTx) UpdateCiphertext(ctx context.Context, kid string, ciphertext []byte) error {
tag, err := t.tx.Exec(ctx, `UPDATE session_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
}