From d72b93b92bfb07322c1f1771415378920f2d246c Mon Sep 17 00:00:00 2001 From: Undline <103777919+Undline@users.noreply.github.com> Date: Tue, 7 Apr 2026 20:28:00 -0400 Subject: [PATCH 1/2] feat(genesis): challenge v1 persistence, service, and apex domain rules - Add migration 008: core_genesis.instance_id and genesis_challenge table (one-shot, 5m TTL). - Add modulr_core.genesis challenge builder/verify and GenesisChallengeService; GenesisChallengeRepository. - CoreGenesisRepository: touch, get_or_create_instance_id; validate modulr_apex_domain with dotted DNS-style rules (same as register_org), max 253 chars. - Tests for challenge issue/verify/replay/expiry and apex validation. Made-with: Cursor --- src/modulr_core/genesis/__init__.py | 23 +++ src/modulr_core/genesis/challenge.py | 182 ++++++++++++++++++ .../migrations/008_genesis_challenge.sql | 16 ++ src/modulr_core/repositories/__init__.py | 2 + src/modulr_core/repositories/core_genesis.py | 54 +++++- .../repositories/genesis_challenge.py | 102 ++++++++++ tests/test_core_genesis_repository.py | 15 ++ tests/test_genesis_challenge.py | 180 +++++++++++++++++ 8 files changed, 571 insertions(+), 3 deletions(-) create mode 100644 src/modulr_core/genesis/__init__.py create mode 100644 src/modulr_core/genesis/challenge.py create mode 100644 src/modulr_core/persistence/migrations/008_genesis_challenge.sql create mode 100644 src/modulr_core/repositories/genesis_challenge.py create mode 100644 tests/test_genesis_challenge.py diff --git a/src/modulr_core/genesis/__init__.py b/src/modulr_core/genesis/__init__.py new file mode 100644 index 0000000..b416140 --- /dev/null +++ b/src/modulr_core/genesis/__init__.py @@ -0,0 +1,23 @@ +"""Genesis challenge format (v1) and verify helpers.""" + +from modulr_core.genesis.challenge import ( + CHALLENGE_PURPOSE, + CHALLENGE_TTL_SECONDS, + GENESIS_CHALLENGE_FORMAT_VERSION, + GenesisChallengeError, + GenesisChallengeService, + IssuedGenesisChallenge, + build_genesis_challenge_v1_body, + verify_genesis_challenge_signature, +) + +__all__ = [ + "CHALLENGE_PURPOSE", + "CHALLENGE_TTL_SECONDS", + "GENESIS_CHALLENGE_FORMAT_VERSION", + "GenesisChallengeError", + "GenesisChallengeService", + "IssuedGenesisChallenge", + "build_genesis_challenge_v1_body", + "verify_genesis_challenge_signature", +] diff --git a/src/modulr_core/genesis/challenge.py b/src/modulr_core/genesis/challenge.py new file mode 100644 index 0000000..ea7a6b0 --- /dev/null +++ b/src/modulr_core/genesis/challenge.py @@ -0,0 +1,182 @@ +"""Genesis challenge v1: multiline UTF-8 body, Ed25519 verify, 5-minute TTL.""" + +from __future__ import annotations + +import re +import secrets +from collections.abc import Callable +from dataclasses import dataclass + +from cryptography.exceptions import InvalidSignature +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey + +from modulr_core.repositories.core_genesis import CoreGenesisRepository +from modulr_core.repositories.genesis_challenge import GenesisChallengeRepository +from modulr_core.validation.hex_codec import InvalidHexEncoding, decode_hex_fixed + +GENESIS_CHALLENGE_FORMAT_VERSION = "modulr-genesis-challenge-v1" +CHALLENGE_PURPOSE = "prove_bootstrap_operator" +CHALLENGE_TTL_SECONDS = 300 + +_NONCE_HEX_RE = re.compile(r"^[0-9a-f]{64}$") +_INSTANCE_ID_RE = re.compile( + r"^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + re.IGNORECASE, +) + + +class GenesisChallengeError(Exception): + """Invalid challenge, signature, expiry, or bootstrap state.""" + + +@dataclass(frozen=True, slots=True) +class IssuedGenesisChallenge: + """A newly issued challenge (store `body` and sign it with the operator key).""" + + challenge_id: str + body: str + issued_at_unix: int + expires_at_unix: int + + +def _validate_instance_id(instance_id: str) -> None: + s = instance_id.strip() + if not s or len(s) > 128: + raise GenesisChallengeError("invalid instance_id") + if not _INSTANCE_ID_RE.match(s): + raise GenesisChallengeError("invalid instance_id") + + +def _validate_nonce_hex(nonce_hex: str) -> None: + if not _NONCE_HEX_RE.match(nonce_hex): + raise GenesisChallengeError("invalid challenge nonce") + + +def _validate_bootstrap_pubkey_hex(pubkey_hex: str) -> bytes: + try: + raw = decode_hex_fixed(pubkey_hex, byte_length=32) + except InvalidHexEncoding as e: + raise GenesisChallengeError("invalid subject_signing_pubkey_hex") from e + try: + Ed25519PublicKey.from_public_bytes(raw) + except ValueError as e: + raise GenesisChallengeError("invalid Ed25519 public key") from e + return raw + + +def build_genesis_challenge_v1_body( + *, + instance_id: str, + nonce_hex: str, + issued_at_unix: int, + expires_at_unix: int, + subject_signing_pubkey_hex: str, +) -> str: + """Build the canonical multiline UTF-8 challenge body (no trailing newline). + + Keymaster must sign ``body.encode("utf-8")`` with the bootstrap operator key. + """ + _validate_instance_id(instance_id) + _validate_nonce_hex(nonce_hex) + pk = subject_signing_pubkey_hex.strip().lower() + _validate_bootstrap_pubkey_hex(pk) + if issued_at_unix < 0 or expires_at_unix < 0: + raise GenesisChallengeError("invalid unix timestamps") + if expires_at_unix <= issued_at_unix: + raise GenesisChallengeError("expires_at_unix must be after issued_at_unix") + + lines = [ + GENESIS_CHALLENGE_FORMAT_VERSION, + f"instance_id: {instance_id.strip()}", + f"nonce: {nonce_hex}", + f"issued_at_unix: {issued_at_unix}", + f"expires_at_unix: {expires_at_unix}", + f"subject_signing_pubkey_hex: {pk}", + f"purpose: {CHALLENGE_PURPOSE}", + ] + return "\n".join(lines) + + +def verify_genesis_challenge_signature( + *, + body: str, + signature_hex: str, + expected_subject_pubkey_hex: str, +) -> None: + """Verify Ed25519 signature over UTF-8 bytes of ``body``.""" + pk_hex = expected_subject_pubkey_hex.strip().lower() + pk_raw = _validate_bootstrap_pubkey_hex(pk_hex) + try: + sig = decode_hex_fixed(signature_hex, byte_length=64) + except InvalidHexEncoding as e: + raise GenesisChallengeError("invalid signature hex") from e + pub = Ed25519PublicKey.from_public_bytes(pk_raw) + try: + pub.verify(sig, body.encode("utf-8")) + except InvalidSignature as e: + raise GenesisChallengeError("signature verification failed") from e + + +class GenesisChallengeService: + """Issue and verify one-shot genesis challenges (SQLite-backed).""" + + def __init__( + self, + *, + genesis_repo: CoreGenesisRepository, + challenge_repo: GenesisChallengeRepository, + clock: Callable[[], int], + ) -> None: + self._genesis = genesis_repo + self._challenges = challenge_repo + self._clock = clock + + def issue(self, *, subject_signing_pubkey_hex: str) -> IssuedGenesisChallenge: + snap = self._genesis.get() + if snap.genesis_complete: + raise GenesisChallengeError("genesis already complete") + now = int(self._clock()) + self._genesis.touch(updated_at=now) + instance_id = self._genesis.get_or_create_instance_id(updated_at=now) + challenge_id = secrets.token_hex(32) + expires_at = now + CHALLENGE_TTL_SECONDS + body = build_genesis_challenge_v1_body( + instance_id=instance_id, + nonce_hex=challenge_id, + issued_at_unix=now, + expires_at_unix=expires_at, + subject_signing_pubkey_hex=subject_signing_pubkey_hex, + ) + self._challenges.insert( + challenge_id=challenge_id, + subject_signing_pubkey_hex=subject_signing_pubkey_hex.strip().lower(), + body=body, + issued_at=now, + expires_at=expires_at, + ) + return IssuedGenesisChallenge( + challenge_id=challenge_id, + body=body, + issued_at_unix=now, + expires_at_unix=expires_at, + ) + + def verify_and_consume(self, *, challenge_id: str, signature_hex: str) -> None: + snap = self._genesis.get() + if snap.genesis_complete: + raise GenesisChallengeError("genesis already complete") + now = int(self._clock()) + row = self._challenges.get_by_id(challenge_id) + if row is None: + raise GenesisChallengeError("unknown challenge_id") + if row.consumed_at is not None: + raise GenesisChallengeError("challenge already consumed") + if now >= row.expires_at: + raise GenesisChallengeError("challenge expired") + verify_genesis_challenge_signature( + body=row.body, + signature_hex=signature_hex, + expected_subject_pubkey_hex=row.subject_signing_pubkey_hex, + ) + self._challenges.mark_consumed(challenge_id, consumed_at=now) + self._genesis.touch(updated_at=now) diff --git a/src/modulr_core/persistence/migrations/008_genesis_challenge.sql b/src/modulr_core/persistence/migrations/008_genesis_challenge.sql new file mode 100644 index 0000000..782b1c4 --- /dev/null +++ b/src/modulr_core/persistence/migrations/008_genesis_challenge.sql @@ -0,0 +1,16 @@ +-- Stable Core instance id (UUID) for binding challenges to this deployment. +-- genesis_challenge: one-shot Ed25519 proof records (anti-replay, TTL). +PRAGMA foreign_keys = ON; + +ALTER TABLE core_genesis ADD COLUMN instance_id TEXT; + +CREATE TABLE IF NOT EXISTS genesis_challenge ( + challenge_id TEXT PRIMARY KEY, + subject_signing_pubkey_hex TEXT NOT NULL, + body TEXT NOT NULL, + issued_at INTEGER NOT NULL, + expires_at INTEGER NOT NULL, + consumed_at INTEGER +); + +CREATE INDEX IF NOT EXISTS idx_genesis_challenge_expires ON genesis_challenge (expires_at); diff --git a/src/modulr_core/repositories/__init__.py b/src/modulr_core/repositories/__init__.py index 110d7e3..9483aaa 100644 --- a/src/modulr_core/repositories/__init__.py +++ b/src/modulr_core/repositories/__init__.py @@ -2,6 +2,7 @@ from modulr_core.repositories.core_genesis import CoreGenesisRepository from modulr_core.repositories.dial_route_entry import DialRouteEntryRepository +from modulr_core.repositories.genesis_challenge import GenesisChallengeRepository from modulr_core.repositories.heartbeat import HeartbeatRepository from modulr_core.repositories.message_dedup import MessageDedupRepository from modulr_core.repositories.modules import ModulesRepository @@ -10,6 +11,7 @@ __all__ = [ "CoreGenesisRepository", "DialRouteEntryRepository", + "GenesisChallengeRepository", "HeartbeatRepository", "MessageDedupRepository", "ModulesRepository", diff --git a/src/modulr_core/repositories/core_genesis.py b/src/modulr_core/repositories/core_genesis.py index 132edd5..3507087 100644 --- a/src/modulr_core/repositories/core_genesis.py +++ b/src/modulr_core/repositories/core_genesis.py @@ -3,11 +3,14 @@ from __future__ import annotations import sqlite3 +import uuid from dataclasses import dataclass from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey +from modulr_core.errors.exceptions import WireValidationError from modulr_core.validation.hex_codec import InvalidHexEncoding, decode_hex_fixed +from modulr_core.validation.names import validate_modulr_org_domain _MODULR_APEX_DOMAIN_MAX_LEN = 253 @@ -19,6 +22,7 @@ class CoreGenesisSnapshot: genesis_complete: bool bootstrap_signing_pubkey_hex: str | None modulr_apex_domain: str | None + instance_id: str | None updated_at: int @@ -34,17 +38,27 @@ def _validate_bootstrap_pubkey_hex(pub_hex: str) -> None: def _validate_apex_domain(domain: str) -> str: + """Enforce dotted DNS-style apex (``validate_modulr_org_domain``), max 253 chars.""" d = domain.strip() if not d: raise ValueError("modulr_apex_domain must be non-empty when set") if len(d) > _MODULR_APEX_DOMAIN_MAX_LEN: mx = _MODULR_APEX_DOMAIN_MAX_LEN raise ValueError(f"modulr_apex_domain must be at most {mx} characters") - return d + try: + return validate_modulr_org_domain(d) + except WireValidationError as e: + raise ValueError( + "modulr_apex_domain must be a dotted DNS-style domain " + "(same label rules as register_org; e.g. modulr.network)", + ) from e class CoreGenesisRepository: - """Read/update the single ``core_genesis`` row (migration ``007`` seeds it).""" + """Read/update the single ``core_genesis`` row. + + Migration ``007`` seeds the row; ``008`` adds ``instance_id``. + """ def __init__(self, conn: sqlite3.Connection) -> None: self._conn = conn @@ -53,7 +67,7 @@ def get(self) -> CoreGenesisSnapshot: cur = self._conn.execute( """ SELECT genesis_complete, bootstrap_signing_pubkey_hex, - modulr_apex_domain, updated_at + modulr_apex_domain, instance_id, updated_at FROM core_genesis WHERE singleton = 1 """, @@ -66,13 +80,47 @@ def get(self) -> CoreGenesisSnapshot: pk_s = str(pk) if pk is not None else None apex = row["modulr_apex_domain"] apex_s = str(apex).strip() if apex is not None and str(apex).strip() else None + iid = row["instance_id"] + iid_s = str(iid).strip() if iid is not None and str(iid).strip() else None return CoreGenesisSnapshot( genesis_complete=complete, bootstrap_signing_pubkey_hex=pk_s, modulr_apex_domain=apex_s, + instance_id=iid_s, updated_at=int(row["updated_at"]), ) + def touch(self, *, updated_at: int) -> None: + """Bump ``updated_at`` on the singleton row (activity / wizard progress).""" + self._conn.execute( + """ + UPDATE core_genesis SET updated_at = ? WHERE singleton = 1 + """, + (updated_at,), + ) + + def get_or_create_instance_id(self, *, updated_at: int) -> str: + """Return stable Core ``instance_id`` (UUID); allocate once on first use.""" + cur = self._conn.execute( + "SELECT instance_id FROM core_genesis WHERE singleton = 1", + ) + row = cur.fetchone() + if row is None: + raise RuntimeError("core_genesis singleton missing; run apply_migrations") + existing = row["instance_id"] + if existing is not None and str(existing).strip(): + return str(existing).strip() + new_id = str(uuid.uuid4()) + self._conn.execute( + """ + UPDATE core_genesis + SET instance_id = ?, updated_at = ? + WHERE singleton = 1 + """, + (new_id, updated_at), + ) + return new_id + def set_genesis_complete(self, *, complete: bool, updated_at: int) -> None: self._conn.execute( """ diff --git a/src/modulr_core/repositories/genesis_challenge.py b/src/modulr_core/repositories/genesis_challenge.py new file mode 100644 index 0000000..54fdb2e --- /dev/null +++ b/src/modulr_core/repositories/genesis_challenge.py @@ -0,0 +1,102 @@ +"""SQLite persistence for one-shot genesis challenges.""" + +from __future__ import annotations + +import sqlite3 +from dataclasses import dataclass +from typing import Any + + +@dataclass(frozen=True, slots=True) +class GenesisChallengeRow: + """Stored challenge row (body is exact UTF-8 text for signing).""" + + challenge_id: str + subject_signing_pubkey_hex: str + body: str + issued_at: int + expires_at: int + consumed_at: int | None + + +class GenesisChallengeRepository: + """CRUD for ``genesis_challenge`` (singleton row table, keyed by challenge_id).""" + + def __init__(self, conn: sqlite3.Connection) -> None: + self._conn = conn + + def insert( + self, + *, + challenge_id: str, + subject_signing_pubkey_hex: str, + body: str, + issued_at: int, + expires_at: int, + ) -> None: + self._conn.execute( + """ + INSERT INTO genesis_challenge ( + challenge_id, + subject_signing_pubkey_hex, + body, + issued_at, + expires_at, + consumed_at + ) VALUES (?, ?, ?, ?, ?, NULL) + """, + (challenge_id, subject_signing_pubkey_hex, body, issued_at, expires_at), + ) + + def get_by_id(self, challenge_id: str) -> GenesisChallengeRow | None: + cur = self._conn.execute( + """ + SELECT + challenge_id, + subject_signing_pubkey_hex, + body, + issued_at, + expires_at, + consumed_at + FROM genesis_challenge WHERE challenge_id = ? + """, + (challenge_id,), + ) + row = cur.fetchone() + if row is None: + return None + return _row_to_challenge(row) + + def mark_consumed(self, challenge_id: str, *, consumed_at: int) -> None: + cur = self._conn.execute( + """ + UPDATE genesis_challenge SET consumed_at = ? + WHERE challenge_id = ? AND consumed_at IS NULL + """, + (consumed_at, challenge_id), + ) + if cur.rowcount != 1: + raise RuntimeError("challenge not found or already consumed") + + +def _row_to_challenge(row: sqlite3.Row | tuple[Any, ...]) -> GenesisChallengeRow: + if isinstance(row, sqlite3.Row): + cid = row["challenge_id"] + pk = row["subject_signing_pubkey_hex"] + body = row["body"] + issued_at = int(row["issued_at"]) + expires_at = int(row["expires_at"]) + consumed = row["consumed_at"] + else: + cid, pk, body, issued_at, expires_at, consumed = row + issued_at = int(issued_at) + expires_at = int(expires_at) + consumed_at: int | None = None if consumed is None else int(consumed) + return GenesisChallengeRow( + challenge_id=str(cid), + subject_signing_pubkey_hex=str(pk), + body=str(body), + issued_at=issued_at, + expires_at=expires_at, + consumed_at=consumed_at, + ) diff --git a/tests/test_core_genesis_repository.py b/tests/test_core_genesis_repository.py index bfddd02..53f0f48 100644 --- a/tests/test_core_genesis_repository.py +++ b/tests/test_core_genesis_repository.py @@ -30,6 +30,7 @@ def test_core_genesis_default_after_migration() -> None: assert s.genesis_complete is False assert s.bootstrap_signing_pubkey_hex is None assert s.modulr_apex_domain is None + assert s.instance_id is None assert s.updated_at == 0 @@ -81,6 +82,10 @@ def test_core_genesis_apex_domain_validation() -> None: repo.set_modulr_apex_domain(apex_domain=" ", updated_at=1) with pytest.raises(ValueError, match="at most"): repo.set_modulr_apex_domain(apex_domain="x" * 254, updated_at=1) + with pytest.raises(ValueError, match="modulr_apex_domain must be a dotted"): + repo.set_modulr_apex_domain(apex_domain="not a domain", updated_at=1) + with pytest.raises(ValueError, match="modulr_apex_domain must be a dotted"): + repo.set_modulr_apex_domain(apex_domain="singlelabel", updated_at=1) def test_schema_migrations_includes_007() -> None: @@ -89,3 +94,13 @@ def test_schema_migrations_includes_007() -> None: "SELECT 1 FROM schema_migrations WHERE version = 7", ) assert cur.fetchone() is not None + + +def test_schema_migrations_includes_008_genesis_challenge() -> None: + conn = _conn() + cur = conn.execute("SELECT 1 FROM schema_migrations WHERE version = 8") + assert cur.fetchone() is not None + cur2 = conn.execute( + "SELECT name FROM sqlite_master WHERE type='table' AND name='genesis_challenge'", + ) + assert cur2.fetchone() is not None diff --git a/tests/test_genesis_challenge.py b/tests/test_genesis_challenge.py new file mode 100644 index 0000000..b0031f7 --- /dev/null +++ b/tests/test_genesis_challenge.py @@ -0,0 +1,180 @@ +"""Genesis challenge v1 body, Ed25519 verify, and one-shot SQLite service.""" + +from __future__ import annotations + +import hashlib +import sqlite3 + +import pytest +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey +from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat + +from modulr_core.genesis.challenge import ( + CHALLENGE_PURPOSE, + CHALLENGE_TTL_SECONDS, + GENESIS_CHALLENGE_FORMAT_VERSION, + GenesisChallengeError, + GenesisChallengeService, + build_genesis_challenge_v1_body, + verify_genesis_challenge_signature, +) +from modulr_core.persistence import apply_migrations, connect_memory +from modulr_core.repositories.core_genesis import CoreGenesisRepository +from modulr_core.repositories.genesis_challenge import GenesisChallengeRepository + + +def _test_keypair() -> tuple[Ed25519PrivateKey, str]: + seed = hashlib.sha256(b"genesis-challenge-test-vector").digest() + priv = Ed25519PrivateKey.from_private_bytes(seed) + pub_hex = priv.public_key().public_bytes( + encoding=Encoding.Raw, + format=PublicFormat.Raw, + ).hex() + return priv, pub_hex + + +def _conn() -> sqlite3.Connection: + c = connect_memory(check_same_thread=False) + apply_migrations(c) + return c + + +def test_build_genesis_challenge_v1_body_golden() -> None: + _, pk = _test_keypair() + body = build_genesis_challenge_v1_body( + instance_id="11111111-1111-4111-8111-111111111111", + nonce_hex="a" * 64, + issued_at_unix=1_700_000_000, + expires_at_unix=1_700_000_000 + CHALLENGE_TTL_SECONDS, + subject_signing_pubkey_hex=pk, + ) + expected = ( + f"{GENESIS_CHALLENGE_FORMAT_VERSION}\n" + "instance_id: 11111111-1111-4111-8111-111111111111\n" + f"nonce: {'a' * 64}\n" + "issued_at_unix: 1700000000\n" + f"expires_at_unix: {1_700_000_000 + CHALLENGE_TTL_SECONDS}\n" + f"subject_signing_pubkey_hex: {pk}\n" + f"purpose: {CHALLENGE_PURPOSE}" + ) + assert body == expected + assert not body.endswith("\n") + priv, _ = _test_keypair() + sig = priv.sign(body.encode("utf-8")).hex() + verify_genesis_challenge_signature( + body=body, + signature_hex=sig, + expected_subject_pubkey_hex=pk, + ) + + +def test_verify_rejects_wrong_signature() -> None: + _, pk = _test_keypair() + other_priv = Ed25519PrivateKey.generate() + body = build_genesis_challenge_v1_body( + instance_id="22222222-2222-4222-8222-222222222222", + nonce_hex="b" * 64, + issued_at_unix=100, + expires_at_unix=100 + CHALLENGE_TTL_SECONDS, + subject_signing_pubkey_hex=pk, + ) + bad_sig = other_priv.sign(body.encode("utf-8")).hex() + with pytest.raises(GenesisChallengeError, match="signature verification failed"): + verify_genesis_challenge_signature( + body=body, + signature_hex=bad_sig, + expected_subject_pubkey_hex=pk, + ) + + +def test_genesis_challenge_service_happy_path() -> None: + conn = _conn() + g_repo = CoreGenesisRepository(conn) + c_repo = GenesisChallengeRepository(conn) + priv, pk = _test_keypair() + t = {"now": 1_000} + + svc = GenesisChallengeService( + genesis_repo=g_repo, + challenge_repo=c_repo, + clock=lambda: t["now"], + ) + issued = svc.issue(subject_signing_pubkey_hex=pk) + conn.commit() + assert len(issued.challenge_id) == 64 + assert issued.expires_at_unix == t["now"] + CHALLENGE_TTL_SECONDS + sig = priv.sign(issued.body.encode("utf-8")).hex() + svc.verify_and_consume(challenge_id=issued.challenge_id, signature_hex=sig) + conn.commit() + + with pytest.raises(GenesisChallengeError, match="already consumed"): + svc.verify_and_consume(challenge_id=issued.challenge_id, signature_hex=sig) + + +def test_genesis_challenge_service_expired() -> None: + conn = _conn() + g_repo = CoreGenesisRepository(conn) + c_repo = GenesisChallengeRepository(conn) + priv, pk = _test_keypair() + t = {"now": 500} + + svc = GenesisChallengeService( + genesis_repo=g_repo, + challenge_repo=c_repo, + clock=lambda: t["now"], + ) + issued = svc.issue(subject_signing_pubkey_hex=pk) + conn.commit() + sig = priv.sign(issued.body.encode("utf-8")).hex() + t["now"] = issued.expires_at_unix + 1 + with pytest.raises(GenesisChallengeError, match="expired"): + svc.verify_and_consume(challenge_id=issued.challenge_id, signature_hex=sig) + + +def test_genesis_challenge_service_blocks_when_genesis_complete() -> None: + conn = _conn() + g_repo = CoreGenesisRepository(conn) + c_repo = GenesisChallengeRepository(conn) + priv, pk = _test_keypair() + g_repo.set_genesis_complete(complete=True, updated_at=1) + conn.commit() + + svc = GenesisChallengeService( + genesis_repo=g_repo, + challenge_repo=c_repo, + clock=lambda: 100, + ) + with pytest.raises(GenesisChallengeError, match="genesis already complete"): + svc.issue(subject_signing_pubkey_hex=pk) + + g_repo.set_genesis_complete(complete=False, updated_at=2) + conn.commit() + issued = svc.issue(subject_signing_pubkey_hex=pk) + conn.commit() + sig = priv.sign(issued.body.encode("utf-8")).hex() + g_repo.set_genesis_complete(complete=True, updated_at=3) + conn.commit() + with pytest.raises(GenesisChallengeError, match="genesis already complete"): + svc.verify_and_consume(challenge_id=issued.challenge_id, signature_hex=sig) + + +def test_genesis_challenge_service_unknown_id() -> None: + conn = _conn() + g_repo = CoreGenesisRepository(conn) + c_repo = GenesisChallengeRepository(conn) + svc = GenesisChallengeService( + genesis_repo=g_repo, + challenge_repo=c_repo, + clock=lambda: 1, + ) + with pytest.raises(GenesisChallengeError, match="unknown challenge_id"): + svc.verify_and_consume(challenge_id="f" * 64, signature_hex="a" * 128) + + +def test_get_or_create_instance_id_stable() -> None: + conn = _conn() + repo = CoreGenesisRepository(conn) + a = repo.get_or_create_instance_id(updated_at=10) + b = repo.get_or_create_instance_id(updated_at=20) + assert a == b + assert repo.get().instance_id == a From 21aaa88ba3e0600a1f4351ee1decd1945fed7f4e Mon Sep 17 00:00:00 2001 From: Undline <103777919+Undline@users.noreply.github.com> Date: Tue, 7 Apr 2026 20:35:41 -0400 Subject: [PATCH 2/2] Fixed to meet Ruff --- tests/test_core_genesis_repository.py | 28 +++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/tests/test_core_genesis_repository.py b/tests/test_core_genesis_repository.py index 53f0f48..b1f9f33 100644 --- a/tests/test_core_genesis_repository.py +++ b/tests/test_core_genesis_repository.py @@ -1,4 +1,13 @@ -"""Singleton ``core_genesis`` persistence (migration 007).""" +""" +Tests for ``CoreGenesisRepository`` and related schema migrations. + +Covers the singleton ``core_genesis`` row (migration 007) and migration 008 +(``instance_id``, ``genesis_challenge`` table). + +Note: + Apex domain and pubkey validation live on the repository; migration ordering + is asserted against ``schema_migrations`` and ``sqlite_master``. +""" from __future__ import annotations @@ -24,6 +33,11 @@ def _conn() -> sqlite3.Connection: def test_core_genesis_default_after_migration() -> None: + """ + Default singleton row matches migration seed. + + Expects incomplete genesis and unset pubkey, apex, and instance id. + """ conn = _conn() repo = CoreGenesisRepository(conn) s = repo.get() @@ -51,6 +65,7 @@ def test_core_genesis_set_pubkey_and_complete() -> None: def test_core_genesis_rejects_invalid_pubkey_hex() -> None: + """Non-hex pubkey strings are rejected before write.""" conn = _conn() repo = CoreGenesisRepository(conn) with pytest.raises(ValueError, match="expected 64 hex"): @@ -66,6 +81,7 @@ def test_core_genesis_rejects_uppercase_pubkey_hex() -> None: def test_core_genesis_clear_pubkey() -> None: + """Setting bootstrap pubkey to ``None`` clears the column.""" conn = _conn() repo = CoreGenesisRepository(conn) k = _valid_pubkey_hex() @@ -76,6 +92,7 @@ def test_core_genesis_clear_pubkey() -> None: def test_core_genesis_apex_domain_validation() -> None: + """Empty, overlong, and non-dotted apex values raise ``ValueError``.""" conn = _conn() repo = CoreGenesisRepository(conn) with pytest.raises(ValueError, match="non-empty"): @@ -89,6 +106,7 @@ def test_core_genesis_apex_domain_validation() -> None: def test_schema_migrations_includes_007() -> None: + """Assert migration 007 is recorded (``core_genesis`` seed).""" conn = _conn() cur = conn.execute( "SELECT 1 FROM schema_migrations WHERE version = 7", @@ -97,10 +115,16 @@ def test_schema_migrations_includes_007() -> None: def test_schema_migrations_includes_008_genesis_challenge() -> None: + """ + Assert migration 008 applied and ``genesis_challenge`` table exists. + + Migration 008 adds ``genesis_challenge`` and ``core_genesis.instance_id``. + """ conn = _conn() cur = conn.execute("SELECT 1 FROM schema_migrations WHERE version = 8") assert cur.fetchone() is not None cur2 = conn.execute( - "SELECT name FROM sqlite_master WHERE type='table' AND name='genesis_challenge'", + "SELECT name FROM sqlite_master " + "WHERE type='table' AND name='genesis_challenge'", ) assert cur2.fetchone() is not None