sigil/tests/test_sealed.py
leora 73f802a5e6 Sealed store: AES-256-GCM encryption at rest for the DB file
Tokens/codes/secrets were already digests; a stolen cleartext SQLite file
still leaked the agent roster, revocation state and audit history.
SealedStore / SIGIL_SEAL_PASS wraps on-disk bytes in AES-256-GCM
(PBKDF2-HMAC-SHA256, 480k iters). Working copy is plaintext only while
open (temp file); flush/close re-seals. Incremental path short of
SQLCipher page-level encryption.

11 new tests in test_sealed.py (stolen seal has no SQLite header / no
agent rows; wrong passphrase fails; works with off-box anchor). Suite
130 green.

Part of Quarry 18a5d762. Independent crypto review + platform glue remain.
2026-10-06 06:28:01 +08:00

127 lines
4.6 KiB
Python

"""Sealed store: encryption at rest for the Sigil database file.
Finding: digests stop a stolen DB from yielding live credentials, but the file
still holds the agent roster, revocation state and audit history in the clear.
SealedStore wraps on-disk bytes in AES-256-GCM so that material is unreadable
without the passphrase. Short of SQLCipher (no page-level encryption while
open); real working seal of the sensitive file.
"""
from __future__ import annotations
import os
import sqlite3
import tempfile
import unittest
from sigil import identity, keys
from sigil.sealed import (
MAGIC,
SealedStore,
is_sealed_file,
seal_bytes,
unseal_bytes,
)
from sigil.store import Store
from tests import helpers
PASS = "test-seal-passphrase-ok"
class TestSealBytes(unittest.TestCase):
def test_round_trip(self):
blob = seal_bytes(b"SQLite format 3\x00hello", PASS)
self.assertTrue(blob.startswith(MAGIC))
self.assertEqual(unseal_bytes(blob, PASS), b"SQLite format 3\x00hello")
def test_wrong_passphrase_refused(self):
blob = seal_bytes(b"secret-db-bytes", PASS)
with self.assertRaises(ValueError):
unseal_bytes(blob, "wrong-passphrase!!")
def test_short_passphrase_refused(self):
with self.assertRaises(ValueError):
seal_bytes(b"x", "short")
def test_corrupt_magic_refused(self):
with self.assertRaises(ValueError):
unseal_bytes(b"NOTSEALED" + b"\x00" * 40, PASS)
class TestSealedStore(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.addCleanup(self.tmp.cleanup)
self.path = os.path.join(self.tmp.name, "sigil.seal")
def test_flush_writes_ciphertext_without_sqlite_header(self):
store = SealedStore(self.path, PASS)
helpers.enrol(store, "bigpickle")
store.flush()
with open(self.path, "rb") as handle:
raw = handle.read()
self.assertTrue(raw.startswith(MAGIC))
self.assertNotIn(b"SQLite format 3", raw)
self.assertTrue(is_sealed_file(self.path))
# Agent id must not appear in the clear on disk.
self.assertNotIn(b"bigpickle", raw)
store.close()
def test_reopen_with_passphrase_recovers_agents(self):
with SealedStore(self.path, PASS) as store:
private, _ = helpers.enrol(store, "bigpickle")
store.flush()
with SealedStore(self.path, PASS) as store:
row = store.get_agent("bigpickle")
self.assertIsNotNone(row)
self.assertEqual(row["fingerprint"], keys.fingerprint(
keys.public_from_private(private)
))
def test_reopen_with_wrong_passphrase_fails(self):
with SealedStore(self.path, PASS) as store:
helpers.enrol(store, "bigpickle")
store.flush()
with self.assertRaises(ValueError):
SealedStore(self.path, "definitely-wrong-passphrase")
def test_stolen_seal_is_not_a_sqlite_database(self):
with SealedStore(self.path, PASS) as store:
helpers.enrol(store, "bigpickle")
identity.challenge(store, "bigpickle")
store.flush()
with self.assertRaises(sqlite3.DatabaseError):
sqlite3.connect(self.path).execute("SELECT name FROM sqlite_master")
def test_refuses_to_open_cleartext_db_as_sealed(self):
clear = os.path.join(self.tmp.name, "clear.db")
Store(clear) # creates a normal sqlite file
with self.assertRaises(ValueError) as ctx:
SealedStore(clear, PASS)
self.assertIn("not a Sigil sealed store", str(ctx.exception))
def test_seal_permissions_are_owner_only(self):
with SealedStore(self.path, PASS) as store:
helpers.enrol(store, "bigpickle")
store.flush()
mode = os.stat(self.path).st_mode & 0o777
self.assertEqual(mode, 0o600)
def test_sealed_store_works_with_offbox_anchor(self):
tip = os.path.join(self.tmp.name, "chain.tip")
with SealedStore(self.path, PASS, anchor_path=tip) as store:
private, _ = helpers.enrol(store, "bigpickle")
identity.challenge(store, "bigpickle")
result = store.checkpoint(private, "bigpickle", "sealed-daily")
self.assertEqual(result.get("anchor_path"), tip)
self.assertTrue(os.path.exists(tip))
self.assertTrue(store.verify_external_anchors()["ok"])
store.flush()
self.assertTrue(is_sealed_file(self.path))
self.assertTrue(os.path.exists(tip))
if __name__ == "__main__":
unittest.main()