Compare commits

..

No commits in common. "cd87be6b8dbb4305d3ac985a0870d5d7a326a5e8" and "7e0ac5020c6d010f6a6284630148693ea84320f6" have entirely different histories.

4 changed files with 4 additions and 82 deletions

View File

@ -241,8 +241,7 @@ Leora's contracted prod pass (Quarry 18a5d762) closed three remaining review blo
rewrite that in-DB checkpoints alone would miss after deletion. Leave it unset
and the tip stays in-DB only.
- **The service is unwritten.** TLS, CSRF, transport, routes. DB file seal is
available (`SealedStore`, plaintext working copy `0600`); SQLCipher
page-level encryption is deferred (not yet available).
available (`SealedStore`); SQLCipher page-level encryption is not.
- **Rate limits are per-process**, so N processes means N times the quota.
- **No key recovery.** Rotate and old sessions die. Deliberate.
- **Humans remain unattributable** to themselves, which is accepted and is a

View File

@ -137,9 +137,7 @@ Still open, and these are the reasons not to ship it yet:
- **The service around this is unwritten.** TLS, CSRF, session transport and the
HTTP routes. DB encryption at rest is available via `SealedStore` /
`SIGIL_SEAL_PASS` (AES-256-GCM file seal; not SQLCipher page-level). The
plaintext working copy is mode `0600` in a private temp dir while open;
SQLCipher remains deferred for hostile co-resident hosts. The library
refuses to mint authority from a name, so a route that forgets to
library refuses to mint authority from a name, so a route that forgets to
authenticate fails closed, but that is only as good as the integration.
- **Off-box anchor is opt-in.** Set `SIGIL_ANCHOR` (or `Store(..., anchor_path=)`)
and every checkpoint also writes a signed tip outside the SQLite file.

View File

@ -11,11 +11,6 @@ working copy is plaintext in memory / a temp file while open). It is a real
working seal: the file on disk is ciphertext, and tests assert a stolen seal
has no SQLite header and no recoverable agent rows without the passphrase.
The plaintext working copy is created mode 0600 inside a private temp dir.
SQLCipher (or equivalent page-level encryption) remains a deferred prod
follow-up for multi-tenant / hostile co-resident hosts — never treat seal-on
as "encrypted DB" while the process is running.
Format on disk (version 1):
b"SIGILSEAL1" + 16-byte salt + 12-byte nonce + AESGCM(ciphertext)
@ -88,30 +83,6 @@ def is_sealed_file(path: str) -> bool:
return handle.read(len(MAGIC)) == MAGIC
def _write_private_file(path: str, data: bytes) -> None:
"""Write `data` to `path` with mode 0600, fsync, unlink on failure."""
flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
fd = os.open(path, flags, 0o600)
try:
with os.fdopen(fd, "wb") as handle:
fd = -1 # ownership transferred
handle.write(data)
handle.flush()
os.fsync(handle.fileno())
os.chmod(path, 0o600)
except Exception:
if fd >= 0:
try:
os.close(fd)
except OSError:
pass
try:
os.unlink(path)
except OSError:
pass
raise
class SealedStore(Store):
"""Store whose on-disk file is AES-GCM sealed.
@ -136,8 +107,6 @@ class SealedStore(Store):
self.seal_path = path
self._passphrase = passphrase
self._tmpdir = tempfile.TemporaryDirectory(prefix="sigil-seal-")
# TemporaryDirectory is already 0700; pin the dir mode explicitly.
os.chmod(self._tmpdir.name, 0o700)
self._plain_path = os.path.join(self._tmpdir.name, "store.db")
if os.path.exists(path):
with open(path, "rb") as handle:
@ -148,16 +117,9 @@ class SealedStore(Store):
"refusing to treat a cleartext DB as sealed"
)
plain = unseal_bytes(blob, passphrase)
_write_private_file(self._plain_path, plain)
else:
# Touch an empty 0600 file so SQLite does not create a world-readable DB.
_write_private_file(self._plain_path, b"")
with open(self._plain_path, "wb") as handle:
handle.write(plain)
super().__init__(self._plain_path, anchor_path=anchor_path)
# Re-pin after SQLite open (WAL/SHM sidecars inherit umask otherwise).
try:
os.chmod(self._plain_path, 0o600)
except OSError:
pass
def flush(self) -> str:
"""Checkpoint WAL, read plaintext bytes, write sealed blob to seal_path."""
@ -168,24 +130,14 @@ class SealedStore(Store):
dest_path = os.path.join(self._tmpdir.name, "export.db")
if os.path.exists(dest_path):
os.unlink(dest_path)
# Pre-create 0600 so SQLite backup does not leave a world-readable export.
_write_private_file(dest_path, b"")
dest = sqlite3.connect(dest_path)
try:
self.conn.backup(dest)
dest.commit()
finally:
dest.close()
try:
os.chmod(dest_path, 0o600)
except OSError:
pass
with open(dest_path, "rb") as handle:
plain = handle.read()
try:
os.unlink(dest_path)
except OSError:
pass
blob = seal_bytes(plain, self._passphrase)
directory = os.path.dirname(os.path.abspath(self.seal_path))
if directory:

View File

@ -124,30 +124,3 @@ class TestSealedStore(unittest.TestCase):
if __name__ == "__main__":
unittest.main()
class TestPlaintextWorkingCopyHardening(unittest.TestCase):
"""R5: plaintext tmp must be 0600; SQLCipher remains deferred."""
def test_plaintext_working_copy_is_owner_only(self):
import tempfile
from sigil.sealed import SealedStore
from sigil import identity, keys
d = tempfile.mkdtemp()
path = os.path.join(d, "sealed.db")
store = SealedStore(path, "test-passphrase-ok")
try:
mode = os.stat(store._plain_path).st_mode & 0o777
self.assertEqual(mode, 0o600, oct(mode))
dir_mode = os.stat(store._tmpdir.name).st_mode & 0o777
self.assertEqual(dir_mode, 0o700, oct(dir_mode))
secret = identity.seed_bootstrap(store, "alice")
_, pub = keys.generate()
identity.enrol(store, "alice", keys.export_public_b64(pub), secret)
store.flush()
# export.db must not linger world-readable after flush
export = os.path.join(store._tmpdir.name, "export.db")
self.assertFalse(os.path.exists(export))
finally:
store.close()