Compare commits

..

2 Commits

Author SHA1 Message Date
7e0ac5020c Merge pull request 'Fix R4: checkpoint binds private key to enrolled fingerprint' (#8) from leora/fix-r4-checkpoint-key-binding into sigil-initial 2026-10-06 06:14:01 +08:00
7872bd92b9 Fix R4: checkpoint requires private key match enrolled fingerprint
checkpoint() only checked that the signer name was enrolled, so a stranger
key could write a polluting checkpoint row. Refuse unless the private key's
fingerprint equals the enrolled fingerprint for that signer.
2026-10-06 06:14:00 +08:00
2 changed files with 17 additions and 1 deletions

View File

@ -675,8 +675,15 @@ class Store:
public = _keys.public_from_private(private)
fingerprint = _keys.fingerprint(public)
if self.get_agent(signer) is None:
agent_row = self.get_agent(signer)
if agent_row is None:
raise ValueError(f"{signer} is not enrolled, so cannot sign a checkpoint")
enrolled_fp = agent_row["fingerprint"] or ""
if not enrolled_fp or fingerprint != enrolled_fp:
raise ValueError(
f"private key fingerprint does not match enrolled key for {signer}; "
"refusing to sign a checkpoint with a stranger key"
)
with self._write_lock:
last = self.conn.execute(
"SELECT id, hash FROM events ORDER BY id DESC LIMIT 1"

View File

@ -133,6 +133,15 @@ class TestSignedCheckpoints(unittest.TestCase):
with self.assertRaises(ValueError):
stranger.checkpoint(self.private, "not-enrolled")
def test_checkpoint_refuses_enrolled_name_with_wrong_key(self):
# R4: signer name is enrolled, but private key is not the enrolled one.
before = len(self.store.checkpoints())
other_private, _ = keys.generate()
with self.assertRaises(ValueError) as ctx:
self.store.checkpoint(other_private, "bigpickle")
self.assertIn("does not match enrolled key", str(ctx.exception))
self.assertEqual(len(self.store.checkpoints()), before)
def test_checkpoint_after_rekey_is_flagged(self):
secret = identity.seed_bootstrap(self.store, "bigpickle")
identity.enrol_rekey(