gabriel / musehub public
musehub_auth_models.py python
117 lines 5.4 KB
Raw
sha256:d035733f21ccff27735fddebfbbe0ed24565a32a22db8de5885402262671ecd2 chore: bump version to 0.2.0rc15 for musehub#113 fix release Sonnet 4.6 patch 15 days ago
1 """SQLAlchemy ORM models for public-key authentication.
2
3 Each ``MusehubAuthKey`` row represents one registered public key that can
4 authenticate as a MuseHub identity. A single identity (handle) may have
5 multiple keys — e.g. a personal laptop key, a desktop key, and an agent key.
6
7 ``MusehubAuthChallenge`` stores in-flight challenge nonces in Postgres so
8 that any server instance (blue or green slot in a blue-green deploy) can
9 verify a challenge regardless of which instance issued it. Rows are deleted
10 immediately after use (single-use) and expire after ``CHALLENGE_TTL_SECONDS``.
11
12 Algorithm support
13 -----------------
14 ``algorithm`` identifies the signing algorithm used by this key. Supported
15 values are defined in ``musehub.crypto.keys.KeyAlgorithm``:
16
17 "ed25519" — RFC 8032 / FIPS 186-5; classical, not quantum-safe.
18 "ml-dsa-65" — FIPS 204 (formerly CRYSTALS-Dilithium-3); NIST post-quantum
19 standard. Implementation pending stable Python library support.
20 See ``musehub/crypto/keys.py`` for the upgrade path.
21
22 Key material
23 ------------
24 ``public_key_b64`` stores the raw public key as a URL-safe base64 string
25 (no padding). The column is ``Text`` to accommodate larger post-quantum keys:
26 Ed25519 public key: 32 bytes → 43 base64 chars
27 ML-DSA-65 public key: 1952 bytes → 2604 base64 chars
28
29 ``fingerprint`` is the canonical ``sha256:``-prefixed fingerprint of the raw
30 key bytes (format: ``sha256:<64-hex>``, 71 chars). SHA-256 provides 128-bit
31 post-quantum security via Grover's algorithm, which
32 is within NIST's current acceptable threshold for symmetric/hash operations.
33 """
34
35 from datetime import datetime, timezone
36
37 from sqlalchemy import DateTime, ForeignKey, Index, String, Text, UniqueConstraint
38 from sqlalchemy.orm import Mapped, MappedAsDataclass, mapped_column
39
40 from musehub.crypto.keys import KeyAlgorithm
41 from musehub.db.database import Base
42
43 def _utc_now() -> datetime:
44 return datetime.now(timezone.utc)
45
46 class MusehubAuthKey(MappedAsDataclass, Base):
47 """One registered public key for a MuseHub identity.
48
49 An identity can own many keys (one per device / agent instance) across any
50 supported algorithm. Deleting a row immediately revokes that key.
51 """
52
53 __tablename__ = "musehub_auth_keys"
54 __table_args__ = (
55 UniqueConstraint("fingerprint", name="uq_musehub_auth_keys_fingerprint"),
56 Index("ix_musehub_auth_keys_identity_id", "identity_id"),
57 Index("ix_musehub_auth_keys_algorithm", "algorithm"),
58 )
59
60 # --- Required fields ---
61 # genesis-addressed: sha256(identity_id NUL public_key_b64)
62 key_id: Mapped[str] = mapped_column(String(128), primary_key=True)
63 # FK to musehub_identities.identity_id — CASCADE so deleting an identity revokes all keys
64 identity_id: Mapped[str] = mapped_column(
65 String(128),
66 ForeignKey("musehub_identities.identity_id", ondelete="CASCADE"),
67 nullable=False,
68 )
69 # URL-safe base64-encoded raw public key (no padding). Text to handle PQ keys.
70 public_key_b64: Mapped[str] = mapped_column(Text, nullable=False)
71 # sha256:-prefixed fingerprint of the raw key bytes — used for O(1) lookup
72 fingerprint: Mapped[str] = mapped_column(String(71), nullable=False)
73
74 # --- Optional fields with Python-side defaults ---
75 # Signing algorithm — matches KeyAlgorithm enum values in musehub.crypto.keys
76 algorithm: Mapped[str] = mapped_column(String(32), nullable=False, default=KeyAlgorithm.ED25519)
77 # Optional human-readable label ("MacBook Pro", "CI agent", etc.)
78 label: Mapped[str] = mapped_column(String(255), nullable=False, default="")
79 created_at: Mapped[datetime] = mapped_column(
80 DateTime(timezone=True), nullable=False, default_factory=_utc_now
81 )
82 last_used_at: Mapped[datetime | None] = mapped_column(
83 DateTime(timezone=True), nullable=True, default=None
84 )
85
86 class MusehubAuthChallenge(Base):
87 """In-flight challenge nonce row.
88
89 Created by ``create_challenge()`` and deleted by ``verify_and_authenticate()``
90 on first use (single-use semantics). Storing nonces in Postgres — rather
91 than a process-local dict — means any server slot in a blue-green deploy
92 can verify a challenge regardless of which slot issued it.
93
94 Cleanup: expired rows are pruned by ``create_challenge()`` on each call
95 (lazy sweep) so the table stays small. A dedicated periodic cleanup job
96 is not required at current scale.
97 """
98
99 __tablename__ = "musehub_auth_challenges"
100 __table_args__ = (
101 Index("ix_musehub_auth_challenges_expires_at", "expires_at"),
102 )
103
104 # 256-bit nonce as 64-char lowercase hex string — the natural identity of a challenge.
105 nonce_hex: Mapped[str] = mapped_column(String(64), primary_key=True)
106
107 # Fingerprint of the public key that requested this challenge.
108 # Used to prevent key-substitution attacks: the verify step checks that
109 # the submitted public key matches the fingerprint bound at challenge time.
110 # Format: "sha256:<64-hex>", 71 chars.
111 fingerprint: Mapped[str] = mapped_column(String(71), nullable=False)
112
113 # Signing algorithm that the client declared at challenge time.
114 algorithm: Mapped[str] = mapped_column(String(32), nullable=False, default=KeyAlgorithm.ED25519)
115
116 # Absolute UTC expiry — row is rejected (and lazily pruned) after this time.
117 expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
File History 11 commits
sha256:d035733f21ccff27735fddebfbbe0ed24565a32a22db8de5885402262671ecd2 chore: bump version to 0.2.0rc15 for musehub#113 fix release Sonnet 4.6 patch 15 days ago
sha256:0032d6cfa33bc3c8367436ad768e7dd0e339b4332153160247da8266cb5fa352 Merge branch 'task/version-tags-phase3-server' into dev Human 17 days ago
sha256:4669620efda9ff41c55bdefd1f7bfe1c239d468428744c84ead9957e5a003a53 merge: rescue snapshot-recovery hardening (c00aa21d) into d… Opus 4.8 minor 30 days ago
sha256:a59da49c4611b970fc4b6ae48678ce4943261c213a07ddbd73ce9201df869b4a fix: remove false-positive proposal_comments index drop fro… Sonnet 4.6 patch 34 days ago
sha256:0a240d6dbff234f07d98a28a4a9a68db702f3f9ff9260196f24219bdb1c0b6f3 feat: render markdown mists as HTML with heading anchor links Sonnet 4.6 patch 35 days ago
sha256:24a7d47486ebc4ebd1832830580e177ec6f877b48dced8c000e198cdec4ce9d6 Merge 'task/bump-version-rc12' into 'dev' — proposal: Bump … Human 36 days ago
sha256:b9ff931d147e0114a1f17060f415b89ed551c170a91ff226c70437aa5c85f9ee Merge 'task/bump-version-rc12' into 'dev' — proposal: Bump … Human 36 days ago
sha256:d1122d21e73471879b460037b22c0b50fded7c423444a176f248428f75dac39c Merge 'task/fix-issue-pagination-cursor' into 'dev' — propo… Human 36 days ago
sha256:01e18975e73d2b3cd5b6db7929c895bef9aa6e0d4391dc5b2adfc548b41318dd Merge 'feat/adding-debug-logs-to-staging' into 'dev' — prop… Human 36 days ago
sha256:6b1949fc2797ca4c1936a637a4cbfec828ef56cf52398a2e74ca3c4f494e728f fix: use wire_bytes not mpack_bytes_raw in compute_object_b… Sonnet 4.6 patch 48 days ago
sha256:b99f2455dc346966d040133f5203297e6e3ef5803a93728a2c30568d0a0f7583 rename: delta_add → delta_upsert across wire format, models… Sonnet 4.6 patch 50 days ago