Files
felhom.eu/hub/internal/store/r133_recovery_seal_test.go
T

97 lines
3.6 KiB
Go

package store
import (
"strings"
"testing"
)
// R-133 (hub v0.135.0): the break-glass console password is sealed at rest with the off-site seal
// (offsite_seal.go). RED-PROOF (audits/hub-safety-2026-10-05/partB/red-proof.txt): make
// SaveHostRecoveryCredential store `secret` instead of `sealed` → TestR133_RawRowHoldsNoPassword fails.
func rawRecovery(t *testing.T, st *Store, hostID string) string {
t.Helper()
var v string
if err := st.db.QueryRow(`SELECT secret FROM host_recovery WHERE host_id = ?`, hostID).Scan(&v); err != nil {
t.Fatal(err)
}
return v
}
// A copy of hub.db alone holds no console password: asserted on the raw column, and the value still opens.
func TestR133_RawRowHoldsNoPassword(t *testing.T) {
st := sealTestStore(t)
if err := st.SaveHostRecoveryCredential("h1", "root@pam", "Console-Pw-7741"); err != nil {
t.Fatal(err)
}
raw := rawRecovery(t, st, "h1")
if strings.Contains(raw, "Console-Pw-7741") || !strings.HasPrefix(raw, sealPrefix) {
t.Fatalf("raw host_recovery.secret is not sealed: %q", raw)
}
c, err := st.GetHostRecoveryCredential("h1")
if err != nil || c == nil || c.Secret != "Console-Pw-7741" || c.Username != "root@pam" {
t.Fatalf("GetHostRecoveryCredential = %+v, %v", c, err)
}
// The page path never carried the secret, and still does not.
m, err := st.GetHostRecoveryMeta("h1")
if err != nil || m == nil || m.Username != "root@pam" {
t.Fatalf("meta = %+v, %v", m, err)
}
}
// The migration: a row written in plaintext before v0.135.0 is sealed in place, once; the value survives.
func TestR133_SealLegacyRecoverySecrets(t *testing.T) {
st := sealTestStore(t)
if _, err := st.db.Exec(`INSERT INTO host_recovery (host_id, username, secret) VALUES ('old', 'root@pam', 'Legacy-Plain-1')`); err != nil {
t.Fatal(err)
}
if err := st.SaveHostRecoveryCredential("new", "root@pam", "Fresh-2"); err != nil {
t.Fatal(err)
}
if _, err := st.GetHostRecoveryCredential("old"); err == nil {
t.Fatal("a plaintext row was handed out before the migration sealed it")
}
n, err := st.SealLegacyRecoverySecrets()
if err != nil || n != 1 {
t.Fatalf("SealLegacyRecoverySecrets = %d, %v — want exactly the one plaintext row", n, err)
}
if raw := rawRecovery(t, st, "old"); strings.Contains(raw, "Legacy-Plain-1") || !strings.HasPrefix(raw, sealPrefix) {
t.Fatalf("legacy row not sealed: %q", raw)
}
if c, err := st.GetHostRecoveryCredential("old"); err != nil || c.Secret != "Legacy-Plain-1" {
t.Fatalf("after migration = %+v, %v", c, err)
}
if n, err := st.SealLegacyRecoverySecrets(); err != nil || n != 0 {
t.Fatalf("second run sealed %d (%v) — want 0 (idempotent)", n, err)
}
}
// A wrong key fails CLOSED: an error, never the column's bytes.
func TestR133_WrongKeyFailsClosed(t *testing.T) {
st := sealTestStore(t)
if err := st.SaveHostRecoveryCredential("h1", "root@pam", "Console-Pw-7741"); err != nil {
t.Fatal(err)
}
if err := st.SetOffsiteSecretKey([]byte("another-key-of-exactly-32-bytes!")); err != nil {
t.Fatal(err)
}
c, err := st.GetHostRecoveryCredential("h1")
if err == nil || c != nil {
t.Fatalf("a wrong key returned %+v (err %v) — want an error and nothing", c, err)
}
}
// No key → the save is refused; nothing is written in the clear.
func TestR133_NoKeyRefusesToSave(t *testing.T) {
st := sealTestStore(t)
st.sealer = nil
if err := st.SaveHostRecoveryCredential("h1", "root@pam", "Console-Pw-7741"); err != ErrNoSealKey {
t.Fatalf("save without a key = %v, want ErrNoSealKey", err)
}
var n int
_ = st.db.QueryRow(`SELECT COUNT(*) FROM host_recovery`).Scan(&n)
if n != 0 {
t.Fatalf("%d row(s) written without a key", n)
}
}