From 7872bd92b977206df0dd3030c0eaed803bbf67f0 Mon Sep 17 00:00:00 2001 From: leora Date: Tue, 6 Oct 2026 06:14:00 +0800 Subject: [PATCH] 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. --- sigil/store.py | 9 ++++++++- tests/test_production.py | 9 +++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/sigil/store.py b/sigil/store.py index 6813795..dce41b3 100644 --- a/sigil/store.py +++ b/sigil/store.py @@ -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" diff --git a/tests/test_production.py b/tests/test_production.py index 8e40ad8..a3d3121 100644 --- a/tests/test_production.py +++ b/tests/test_production.py @@ -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( -- 2.47.3