Compare commits
No commits in common. "2e0900e15b65cb78492908ed6112a7d19f4f8e16" and "d4af46cc1b034f610e4dfd3e46052f6be56e29c8" have entirely different histories.
2e0900e15b
...
d4af46cc1b
@ -88,8 +88,6 @@ def enrol(
|
|||||||
fingerprint = keys.fingerprint(public)
|
fingerprint = keys.fingerprint(public)
|
||||||
if not store.put_agent(agent, keys.public_bytes(public), fingerprint):
|
if not store.put_agent(agent, keys.public_bytes(public), fingerprint):
|
||||||
raise errors.EnrolmentFailed(f"{agent} is already enrolled")
|
raise errors.EnrolmentFailed(f"{agent} is already enrolled")
|
||||||
# Spend any sibling seeds left over from double-seed / unused chat leaks.
|
|
||||||
store.clear_bootstrap_for_agent(agent)
|
|
||||||
store.record(
|
store.record(
|
||||||
"identity.enrolled",
|
"identity.enrolled",
|
||||||
actor=agent,
|
actor=agent,
|
||||||
@ -272,8 +270,6 @@ def enrol_rekey(store: Store, agent: str, public_b64: str, secret: str) -> str:
|
|||||||
# A new key must not leave old sessions alive, or rotating after a
|
# A new key must not leave old sessions alive, or rotating after a
|
||||||
# compromise buys nothing.
|
# compromise buys nothing.
|
||||||
killed = store.revoke_sessions_for_agent(agent, "key rotated")
|
killed = store.revoke_sessions_for_agent(agent, "key rotated")
|
||||||
# Leftover bootstrap seeds must not remain a standing rekey capability.
|
|
||||||
store.clear_bootstrap_for_agent(agent)
|
|
||||||
store.record(
|
store.record(
|
||||||
"identity.rekeyed",
|
"identity.rekeyed",
|
||||||
actor=agent,
|
actor=agent,
|
||||||
|
|||||||
@ -312,44 +312,10 @@ class Store:
|
|||||||
|
|
||||||
# -- bootstrap ---------------------------------------------------------
|
# -- bootstrap ---------------------------------------------------------
|
||||||
def put_bootstrap(self, secret_hash: str, agent: str) -> None:
|
def put_bootstrap(self, secret_hash: str, agent: str) -> None:
|
||||||
"""Register one unspent bootstrap secret for an agent.
|
self._exec(
|
||||||
|
"INSERT INTO bootstrap (secret_hash, agent, created_at) VALUES (?, ?, ?)",
|
||||||
Any prior unspent secrets for the same agent are spent first so only
|
(secret_hash, agent, int(time.time())),
|
||||||
one live seed exists at a time (ops double-seed or a chat leak of an
|
)
|
||||||
unused seed must not leave a standing rekey capability).
|
|
||||||
"""
|
|
||||||
with self._write_lock:
|
|
||||||
now = int(time.time())
|
|
||||||
self.conn.execute(
|
|
||||||
"UPDATE bootstrap SET spent_at = ? WHERE agent = ? AND spent_at IS NULL",
|
|
||||||
(now, agent),
|
|
||||||
)
|
|
||||||
self.conn.execute(
|
|
||||||
"INSERT INTO bootstrap (secret_hash, agent, created_at) VALUES (?, ?, ?)",
|
|
||||||
(secret_hash, agent, now),
|
|
||||||
)
|
|
||||||
self.conn.commit()
|
|
||||||
|
|
||||||
def clear_bootstrap_for_agent(self, agent: str, *, keep_hash: Optional[str] = None) -> int:
|
|
||||||
"""Spend (or delete) every unspent bootstrap secret for an agent.
|
|
||||||
|
|
||||||
After a successful enrol / enrol_rekey, leftover seeds must not remain
|
|
||||||
usable. keep_hash, when set, is left alone (already consumed by CAS).
|
|
||||||
"""
|
|
||||||
with self._write_lock:
|
|
||||||
if keep_hash is None:
|
|
||||||
cur = self.conn.execute(
|
|
||||||
"UPDATE bootstrap SET spent_at = ? WHERE agent = ? AND spent_at IS NULL",
|
|
||||||
(int(time.time()), agent),
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
cur = self.conn.execute(
|
|
||||||
"UPDATE bootstrap SET spent_at = ? WHERE agent = ? AND spent_at IS NULL"
|
|
||||||
" AND secret_hash != ?",
|
|
||||||
(int(time.time()), agent, keep_hash),
|
|
||||||
)
|
|
||||||
self.conn.commit()
|
|
||||||
return cur.rowcount
|
|
||||||
|
|
||||||
def spend_bootstrap(self, secret_hash: str) -> Optional[str]:
|
def spend_bootstrap(self, secret_hash: str) -> Optional[str]:
|
||||||
"""Atomically consume an enrolment secret. Returns the agent or None.
|
"""Atomically consume an enrolment secret. Returns the agent or None.
|
||||||
|
|||||||
@ -6,8 +6,6 @@ import unittest
|
|||||||
from sigil import errors, identity, keys
|
from sigil import errors, identity, keys
|
||||||
from sigil.store import Store
|
from sigil.store import Store
|
||||||
|
|
||||||
import helpers
|
|
||||||
|
|
||||||
|
|
||||||
class Base(unittest.TestCase):
|
class Base(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
@ -146,59 +144,5 @@ class TestRekey(Base):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestBootstrapHygiene(unittest.TestCase):
|
|
||||||
"""R2: only one live bootstrap secret; leftovers unusable after enrol."""
|
|
||||||
|
|
||||||
def test_leftover_seed_fails_after_enrol(self):
|
|
||||||
store = Store(":memory:")
|
|
||||||
private, public = keys.generate()
|
|
||||||
s1 = identity.seed_bootstrap(store, "alice")
|
|
||||||
# Direct insert of a second hash simulates a pre-hygiene double-seed
|
|
||||||
# (put_bootstrap now clears siblings; plant one after s1 is recorded).
|
|
||||||
import hashlib
|
|
||||||
s2 = "leftover-seed-that-must-die"
|
|
||||||
store.conn.execute(
|
|
||||||
"INSERT INTO bootstrap (secret_hash, agent, created_at) VALUES (?, ?, ?)",
|
|
||||||
(hashlib.sha256(s2.encode()).hexdigest(), "alice", 1),
|
|
||||||
)
|
|
||||||
store.conn.commit()
|
|
||||||
identity.enrol(store, "alice", keys.export_public_b64(public), s1)
|
|
||||||
with self.assertRaises(errors.EnrolmentFailed):
|
|
||||||
identity.enrol_rekey(
|
|
||||||
store, "alice", keys.export_public_b64(public), s2
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_put_bootstrap_keeps_only_one_live_secret(self):
|
|
||||||
store = Store(":memory:")
|
|
||||||
s1 = identity.seed_bootstrap(store, "bob")
|
|
||||||
s2 = identity.seed_bootstrap(store, "bob")
|
|
||||||
private, public = keys.generate()
|
|
||||||
with self.assertRaises(errors.EnrolmentFailed):
|
|
||||||
identity.enrol(store, "bob", keys.export_public_b64(public), s1)
|
|
||||||
# Only the latest seed works.
|
|
||||||
identity.enrol(store, "bob", keys.export_public_b64(public), s2)
|
|
||||||
|
|
||||||
def test_leftover_seed_fails_after_rekey(self):
|
|
||||||
store = Store(":memory:")
|
|
||||||
helpers.enrol(store, "carol")
|
|
||||||
import hashlib
|
|
||||||
leftover = "another-unused-seed"
|
|
||||||
new_pub = keys.generate()[1]
|
|
||||||
secret = identity.seed_bootstrap(store, "carol")
|
|
||||||
# Plant a sibling after the live seed (simulates a concurrent leak).
|
|
||||||
store.conn.execute(
|
|
||||||
"INSERT INTO bootstrap (secret_hash, agent, created_at) VALUES (?, ?, ?)",
|
|
||||||
(hashlib.sha256(leftover.encode()).hexdigest(), "carol", 2),
|
|
||||||
)
|
|
||||||
store.conn.commit()
|
|
||||||
identity.enrol_rekey(
|
|
||||||
store, "carol", keys.export_public_b64(new_pub), secret
|
|
||||||
)
|
|
||||||
newer_pub = keys.generate()[1]
|
|
||||||
with self.assertRaises(errors.EnrolmentFailed):
|
|
||||||
identity.enrol_rekey(
|
|
||||||
store, "carol", keys.export_public_b64(newer_pub), leftover
|
|
||||||
)
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
@ -345,17 +345,10 @@ class TestDualBootstrapEnrolRace(unittest.TestCase):
|
|||||||
import threading
|
import threading
|
||||||
|
|
||||||
store = Store(":memory:")
|
store = Store(":memory:")
|
||||||
# Plant two live secrets directly: put_bootstrap now keeps only one
|
secrets = [
|
||||||
# live seed, so the dual-enrol race must bypass that hygiene.
|
identity.seed_bootstrap(store, "racer"),
|
||||||
import hashlib, time as _time
|
identity.seed_bootstrap(store, "racer"),
|
||||||
secrets = ["race-secret-alpha", "race-secret-beta"]
|
]
|
||||||
now = int(_time.time())
|
|
||||||
for s in secrets:
|
|
||||||
store.conn.execute(
|
|
||||||
"INSERT INTO bootstrap (secret_hash, agent, created_at) VALUES (?, ?, ?)",
|
|
||||||
(hashlib.sha256(s.encode()).hexdigest(), "racer", now),
|
|
||||||
)
|
|
||||||
store.conn.commit()
|
|
||||||
pubs = [keys.generate()[1] for _ in range(2)]
|
pubs = [keys.generate()[1] for _ in range(2)]
|
||||||
results = []
|
results = []
|
||||||
lock = threading.Lock()
|
lock = threading.Lock()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user