feat: Add backup verify with a live decrypt probe
A fingerprint comparison proves two archives agree about a key. Only opening real ciphertext from the target proves the key in hand reads the data, which is the question an operator actually has.
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
package backup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"gitea.hostxtra.co.uk/mrhid6/vantage/shared/cryptobox"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
func TestVerifyMatchingKey(t *testing.T) {
|
||||
client, _ := testDB(t)
|
||||
archive := archiveOf(t, client, validKeyHex, false)
|
||||
|
||||
rep, err := Verify(context.Background(), VerifyOptions{Archive: archive, KeyHex: validKeyHex})
|
||||
if err != nil {
|
||||
t.Fatalf("Verify: %v", err)
|
||||
}
|
||||
if !rep.KeyMatchesArchive {
|
||||
t.Fatal("matching key reported as a mismatch")
|
||||
}
|
||||
if rep.ProbeAttempted {
|
||||
t.Fatal("probe ran with no client supplied")
|
||||
}
|
||||
if !rep.OK() {
|
||||
t.Fatalf("report not OK: %v", rep.Problems)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyMismatchedKeyIsNotOK(t *testing.T) {
|
||||
client, _ := testDB(t)
|
||||
archive := archiveOf(t, client, validKeyHex, false)
|
||||
other := "0000000000000000000000000000000000000000000000000000000000000002"
|
||||
|
||||
rep, err := Verify(context.Background(), VerifyOptions{Archive: archive, KeyHex: other})
|
||||
if err != nil {
|
||||
t.Fatalf("Verify: %v", err)
|
||||
}
|
||||
if rep.KeyMatchesArchive {
|
||||
t.Fatal("mismatched key reported as matching")
|
||||
}
|
||||
if rep.OK() {
|
||||
t.Fatal("a mismatch must not report OK")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyProbeDecryptsLiveCiphertext(t *testing.T) {
|
||||
client, dbName := testDB(t)
|
||||
ctx := context.Background()
|
||||
|
||||
key, err := ParseKey(validKeyHex)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseKey: %v", err)
|
||||
}
|
||||
sealed, err := cryptobox.Seal(key, "s3cret")
|
||||
if err != nil {
|
||||
t.Fatalf("Seal: %v", err)
|
||||
}
|
||||
if _, err := client.Database(dbName).Collection("secrets").InsertOne(ctx, bson.M{
|
||||
"instance_id": "i1",
|
||||
"values": bson.M{"TOKEN": sealed},
|
||||
}); err != nil {
|
||||
t.Fatalf("insert: %v", err)
|
||||
}
|
||||
|
||||
path, _ := dumpToFile(t, DumpOptions{Client: client, Database: dbName, KeyHex: validKeyHex})
|
||||
archive, err := Open(path)
|
||||
if err != nil {
|
||||
t.Fatalf("Open: %v", err)
|
||||
}
|
||||
defer archive.Close()
|
||||
|
||||
rep, err := Verify(ctx, VerifyOptions{
|
||||
Archive: archive, KeyHex: validKeyHex, Client: client, Database: dbName,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Verify: %v", err)
|
||||
}
|
||||
if !rep.ProbeAttempted {
|
||||
t.Fatal("probe did not run with a client supplied")
|
||||
}
|
||||
if !rep.ProbeDecrypted {
|
||||
t.Fatalf("probe failed to decrypt live ciphertext: %v", rep.Problems)
|
||||
}
|
||||
if rep.ProbeCollection != "secrets" {
|
||||
t.Fatalf("probe collection %q, want secrets", rep.ProbeCollection)
|
||||
}
|
||||
if !rep.OK() {
|
||||
t.Fatalf("report not OK: %v", rep.Problems)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyProbeFailsWithWrongKey(t *testing.T) {
|
||||
client, dbName := testDB(t)
|
||||
ctx := context.Background()
|
||||
|
||||
key, err := ParseKey(validKeyHex)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseKey: %v", err)
|
||||
}
|
||||
sealed, err := cryptobox.Seal(key, "s3cret")
|
||||
if err != nil {
|
||||
t.Fatalf("Seal: %v", err)
|
||||
}
|
||||
if _, err := client.Database(dbName).Collection("secrets").InsertOne(ctx, bson.M{
|
||||
"values": bson.M{"TOKEN": sealed},
|
||||
}); err != nil {
|
||||
t.Fatalf("insert: %v", err)
|
||||
}
|
||||
|
||||
other := "0000000000000000000000000000000000000000000000000000000000000002"
|
||||
path, _ := dumpToFile(t, DumpOptions{Client: client, Database: dbName, KeyHex: other})
|
||||
archive, err := Open(path)
|
||||
if err != nil {
|
||||
t.Fatalf("Open: %v", err)
|
||||
}
|
||||
defer archive.Close()
|
||||
|
||||
rep, err := Verify(ctx, VerifyOptions{
|
||||
Archive: archive, KeyHex: other, Client: client, Database: dbName,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Verify: %v", err)
|
||||
}
|
||||
if rep.ProbeDecrypted {
|
||||
t.Fatal("probe decrypted with the wrong key")
|
||||
}
|
||||
if rep.OK() {
|
||||
t.Fatal("a failed probe must not report OK")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyProbeAbsentCiphertextIsNotAFailure(t *testing.T) {
|
||||
client, dbName := testDB(t)
|
||||
seed(t, client, dbName)
|
||||
|
||||
path, _ := dumpToFile(t, DumpOptions{Client: client, Database: dbName, KeyHex: validKeyHex})
|
||||
archive, err := Open(path)
|
||||
if err != nil {
|
||||
t.Fatalf("Open: %v", err)
|
||||
}
|
||||
defer archive.Close()
|
||||
|
||||
rep, err := Verify(context.Background(), VerifyOptions{
|
||||
Archive: archive, KeyHex: validKeyHex, Client: client, Database: dbName,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Verify: %v", err)
|
||||
}
|
||||
if rep.ProbeAttempted {
|
||||
t.Fatal("probe claims to have run against a database with no ciphertext")
|
||||
}
|
||||
if !rep.OK() {
|
||||
t.Fatalf("a database storing no secrets must still verify: %v", rep.Problems)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user