Replace passcode identity with Ed25519 challenge-response. - Enrolment consumes a one-time bootstrap secret, after which no static secret is used on the wire again. - Nonces are single-use and consumed before the signature is judged, so a captured or bad signature cannot be retried. - Sessions are short-lived, scoped, and typed agent vs human_via_agent. - Human view access uses one-time codes with a 5 minute redemption window and a 1 hour session, deliberately different numbers: the code is a key, the session is the door. - Invariant: only an agent signature may act as an agent. Enforced and tested. - Append-only audit trail. 54 tests, covering refusals rather than only the happy path. Reference implementation, not reviewed or hardened. See README Status.
76 lines
2.8 KiB
Python
76 lines
2.8 KiB
Python
"""Keys: signing, verification, and the fingerprint as a safe identifier."""
|
|
|
|
import base64
|
|
import os
|
|
import tempfile
|
|
import unittest
|
|
|
|
from sigil import keys
|
|
|
|
|
|
class TestKeys(unittest.TestCase):
|
|
def test_sign_verify_roundtrip(self):
|
|
private, public = keys.generate()
|
|
message = b"the exact bytes that were challenged"
|
|
signature = keys.sign(private, message)
|
|
self.assertTrue(keys.verify(public, signature, message))
|
|
|
|
def test_wrong_message_fails(self):
|
|
private, public = keys.generate()
|
|
signature = keys.sign(private, b"nonce-1234")
|
|
self.assertFalse(keys.verify(public, signature, b"nonce-5678"))
|
|
|
|
def test_wrong_key_fails(self):
|
|
private_a, _ = keys.generate()
|
|
_, public_b = keys.generate()
|
|
signature = keys.sign(private_a, b"nonce-1234")
|
|
self.assertFalse(keys.verify(public_b, signature, b"nonce-1234"))
|
|
|
|
def test_tampered_signature_fails(self):
|
|
private, public = keys.generate()
|
|
signature = bytearray(keys.sign(private, b"nonce-1234"))
|
|
signature[0] ^= 0xFF
|
|
self.assertFalse(keys.verify(public, bytes(signature), b"nonce-1234"))
|
|
|
|
def test_garbage_signature_does_not_raise(self):
|
|
_, public = keys.generate()
|
|
self.assertFalse(keys.verify(public, b"not-a-signature", b"x"))
|
|
self.assertFalse(keys.verify(public, b"", b"x"))
|
|
|
|
def test_public_b64_roundtrip(self):
|
|
_, public = keys.generate()
|
|
restored = keys.import_public_b64(keys.export_public_b64(public))
|
|
self.assertEqual(
|
|
keys.public_bytes(restored), keys.public_bytes(public)
|
|
)
|
|
self.assertEqual(keys.fingerprint(restored), keys.fingerprint(public))
|
|
|
|
def test_fingerprint_is_stable_and_identifier_shaped(self):
|
|
_, public = keys.generate()
|
|
first = keys.fingerprint(public)
|
|
second = keys.fingerprint(public)
|
|
self.assertEqual(first, second)
|
|
self.assertTrue(first.startswith("ed25519:"))
|
|
groups = first.split(":")[1:]
|
|
self.assertEqual(len(groups), 4)
|
|
self.assertTrue(all(len(g) == 8 for g in groups))
|
|
|
|
def test_different_keys_have_different_fingerprints(self):
|
|
_, a = keys.generate()
|
|
_, b = keys.generate()
|
|
self.assertNotEqual(keys.fingerprint(a), keys.fingerprint(b))
|
|
|
|
def test_private_key_file_is_owner_only(self):
|
|
private, _ = keys.generate()
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
path = os.path.join(tmp, "key.pem")
|
|
keys.save_private(private, path)
|
|
mode = os.stat(path).st_mode & 0o777
|
|
self.assertEqual(mode, 0o600, f"expected 0600, got {oct(mode)}")
|
|
self.assertEqual(
|
|
keys.load_private(path).sign(b"x"), private.sign(b"x")
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main() |