Compare commits
No commits in common. "618fb5401c427c4610c9d6581874a0887b7b11c8" and "c46cd523233b43abcb9460a4a895f40d187a1ec7" have entirely different histories.
618fb5401c
...
c46cd52323
10
DESIGN.md
10
DESIGN.md
@ -123,11 +123,9 @@ means a future route cannot pass a string where a proof belongs.
|
|||||||
|
|
||||||
`revoke <agent>` with no end time is permanent: the enrolled public key is
|
`revoke <agent>` with no end time is permanent: the enrolled public key is
|
||||||
cleared and the identity stays dead until someone bootstraps a fresh secret and
|
cleared and the identity stays dead until someone bootstraps a fresh secret and
|
||||||
calls `enrol_rekey` (then `reinstate` if still banned). `enrol` will not work —
|
rekeys (or re-enrols). `reinstate` alone lifts the ban flags but does **not**
|
||||||
the agent row still exists — and would only burn the seed. `reinstate` alone
|
restore the compromised key — that was the footgun. `revoke <agent> --until
|
||||||
lifts the ban flags but does **not** restore the compromised key — that was the
|
<unix>` is a pause that keeps the key, and it lapses on its own.
|
||||||
footgun. `revoke <agent> --until <unix>` is a pause that keeps the key, and it
|
|
||||||
lapses on its own.
|
|
||||||
|
|
||||||
The distinction matters because the two failure modes are different. A key
|
The distinction matters because the two failure modes are different. A key
|
||||||
compromise wants death — a lapsing revocation would hand the attacker back in
|
compromise wants death — a lapsing revocation would hand the attacker back in
|
||||||
@ -230,7 +228,7 @@ because the review looked for it.
|
|||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
156 passing tests, and two hardening passes that closed real defects. The worst were
|
106 passing tests, and two hardening passes that closed real defects. The worst were
|
||||||
concurrency: one-time codes were redeemable repeatedly (753 sessions from 200
|
concurrency: one-time codes were redeemable repeatedly (753 sessions from 200
|
||||||
codes under load), and a bootstrap secret could enrol more than one key. Those
|
codes under load), and a bootstrap secret could enrol more than one key. Those
|
||||||
are now atomic compare-and-set operations, each covered by a test that races
|
are now atomic compare-and-set operations, each covered by a test that races
|
||||||
|
|||||||
@ -55,7 +55,7 @@ python3 sigil.py sweep # prune stale rows
|
|||||||
python3 -m unittest discover -s tests -v
|
python3 -m unittest discover -s tests -v
|
||||||
```
|
```
|
||||||
|
|
||||||
156 tests. They cover the refusals, not just the happy paths: replayed
|
130 tests. They cover the refusals, not just the happy paths: replayed
|
||||||
signatures, wrong keys, expired nonces, single-use codes, scope escalation, and
|
signatures, wrong keys, expired nonces, single-use codes, scope escalation, and
|
||||||
the invariant below. `test_hardening.py`, `test_production.py`, `test_anchor.py` and
|
the invariant below. `test_hardening.py`, `test_production.py`, `test_anchor.py` and
|
||||||
`test_sealed.py` have one test per defect found in review, so any of them
|
`test_sealed.py` have one test per defect found in review, so any of them
|
||||||
|
|||||||
@ -78,10 +78,6 @@ def enrol(
|
|||||||
_validate_agent_id(agent)
|
_validate_agent_id(agent)
|
||||||
if not store.allow(f"enrol:{agent}"):
|
if not store.allow(f"enrol:{agent}"):
|
||||||
raise errors.RateLimited(f"too many enrolment attempts for {agent}")
|
raise errors.RateLimited(f"too many enrolment attempts for {agent}")
|
||||||
# Already enrolled (e.g. after permanent revoke): refuse *before* spending
|
|
||||||
# the seed so ops following a wrong "re-enrol" path do not burn it.
|
|
||||||
if store.get_agent(agent) is not None:
|
|
||||||
raise errors.EnrolmentFailed(f"{agent} is already enrolled; use enrol_rekey")
|
|
||||||
# Spend the secret first so a burned secret cannot be retried. The agent
|
# Spend the secret first so a burned secret cannot be retried. The agent
|
||||||
# row insert below is INSERT OR IGNORE: if two secrets race for the same
|
# row insert below is INSERT OR IGNORE: if two secrets race for the same
|
||||||
# unenrolled agent, exactly one put_agent wins and the loser fails closed
|
# unenrolled agent, exactly one put_agent wins and the loser fails closed
|
||||||
|
|||||||
@ -352,9 +352,9 @@ class Store:
|
|||||||
"""Suspend an agent. `until` of None is permanent.
|
"""Suspend an agent. `until` of None is permanent.
|
||||||
|
|
||||||
A permanent revoke clears the enrolled public key so `reinstate` alone
|
A permanent revoke clears the enrolled public key so `reinstate` alone
|
||||||
cannot restore a compromised identity — ops must bootstrap + enrol_rekey
|
cannot restore a compromised identity — ops must bootstrap + rekey
|
||||||
before the agent can authenticate again. Timed suspension keeps the
|
(or enrol) before the agent can authenticate again. Timed suspension
|
||||||
key: when the pause lapses, the same key works.
|
keeps the key: when the pause lapses, the same key works.
|
||||||
|
|
||||||
Returns how many live sessions were killed.
|
Returns how many live sessions were killed.
|
||||||
"""
|
"""
|
||||||
@ -788,13 +788,9 @@ class Store:
|
|||||||
|
|
||||||
public = _keys.public_from_private(private)
|
public = _keys.public_from_private(private)
|
||||||
fingerprint = _keys.fingerprint(public)
|
fingerprint = _keys.fingerprint(public)
|
||||||
from . import identity as _identity
|
|
||||||
|
|
||||||
agent_row = self.get_agent(signer)
|
agent_row = self.get_agent(signer)
|
||||||
if agent_row is None:
|
if agent_row is None:
|
||||||
raise ValueError(f"{signer} is not enrolled, so cannot sign a checkpoint")
|
raise ValueError(f"{signer} is not enrolled, so cannot sign a checkpoint")
|
||||||
if _identity.is_revoked(agent_row):
|
|
||||||
raise ValueError(f"{signer} is revoked, so cannot sign a checkpoint")
|
|
||||||
enrolled_fp = agent_row["fingerprint"] or ""
|
enrolled_fp = agent_row["fingerprint"] or ""
|
||||||
if not enrolled_fp or fingerprint != enrolled_fp:
|
if not enrolled_fp or fingerprint != enrolled_fp:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
|
|||||||
@ -531,36 +531,3 @@ class TestKeyHistoryKeepsCheckpointsVerifiable(unittest.TestCase):
|
|||||||
self.assertNotIn("privkey", cols)
|
self.assertNotIn("privkey", cols)
|
||||||
row = store.conn.execute("SELECT * FROM agent_keys WHERE agent = ?", ("carol",)).fetchone()
|
row = store.conn.execute("SELECT * FROM agent_keys WHERE agent = ?", ("carol",)).fetchone()
|
||||||
self.assertEqual(len(row["pubkey"]), 32)
|
self.assertEqual(len(row["pubkey"]), 32)
|
||||||
|
|
||||||
|
|
||||||
class TestEnrolAfterPermanentRevoke(unittest.TestCase):
|
|
||||||
"""N4: enrol must not burn the seed when the agent row still exists."""
|
|
||||||
|
|
||||||
def test_enrol_after_permanent_revoke_does_not_burn_seed(self):
|
|
||||||
store, private = helpers.fresh()
|
|
||||||
identity.revoke(store, "bigpickle", reason="compromise")
|
|
||||||
identity.reinstate(store, "bigpickle")
|
|
||||||
secret = identity.seed_bootstrap(store, "bigpickle")
|
|
||||||
with self.assertRaises(errors.EnrolmentFailed) as ctx:
|
|
||||||
identity.enrol(
|
|
||||||
store, "bigpickle", keys.export_public_b64(keys.generate()[1]), secret
|
|
||||||
)
|
|
||||||
self.assertIn("enrol_rekey", str(ctx.exception))
|
|
||||||
# Seed still usable for the correct path.
|
|
||||||
identity.enrol_rekey(
|
|
||||||
store, "bigpickle", keys.export_public_b64(keys.generate()[1]), secret
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TestSuspendedSignerCannotCheckpoint(unittest.TestCase):
|
|
||||||
"""N5: timed-suspended signer must not checkpoint."""
|
|
||||||
|
|
||||||
def test_timed_suspend_refuses_checkpoint(self):
|
|
||||||
import time
|
|
||||||
store, private = helpers.fresh()
|
|
||||||
identity.revoke(
|
|
||||||
store, "bigpickle", until=int(time.time()) + 3600, reason="pause"
|
|
||||||
)
|
|
||||||
with self.assertRaises(ValueError) as ctx:
|
|
||||||
store.checkpoint(private, "bigpickle")
|
|
||||||
self.assertIn("revoked", str(ctx.exception).lower())
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user