gabriel / musehub public
musehub_auth_models.py python
91 lines 3.5 KB
Raw
sha256:a10adeeb7a0169cb9900f9806ed7a973047258abb6283724fe55e8eb68ff3f0a init: musehub initial commit Human 171 days ago
1 """SQLAlchemy ORM model for public-key authentication.
2
3 Each row represents one registered public key that can authenticate as a
4 MuseHub identity. A single identity (handle) may have multiple keys — e.g.
5 a personal laptop key, a desktop key, and an agent key.
6
7 Algorithm support
8 -----------------
9 ``algorithm`` identifies the signing algorithm used by this key. Supported
10 values are defined in ``musehub.crypto.keys.KeyAlgorithm``:
11
12 "ed25519" — RFC 8032 / FIPS 186-5; classical, not quantum-safe.
13 "ml-dsa-65" — FIPS 204 (formerly CRYSTALS-Dilithium-3); NIST post-quantum
14 standard. Implementation pending stable Python library support.
15 See ``musehub/crypto/keys.py`` for the upgrade path.
16
17 Key material
18 ------------
19 ``public_key_b64`` stores the raw public key as a URL-safe base64 string
20 (no padding). The column is ``Text`` to accommodate larger post-quantum keys:
21 Ed25519 public key: 32 bytes → 43 base64 chars
22 ML-DSA-65 public key: 1952 bytes → 2604 base64 chars
23
24 ``fingerprint`` is the lowercase hex SHA-256 digest of the raw key bytes.
25 SHA-256 provides 128-bit post-quantum security via Grover's algorithm, which
26 is within NIST's current acceptable threshold for symmetric/hash operations.
27
28 Challenge-response is stateless: the server stores nonces in memory with a
29 short TTL. The client signs the raw nonce bytes with their private key to
30 prove ownership. No server secret, no ``ACCESS_TOKEN_SECRET``.
31 """
32 from __future__ import annotations
33
34 import uuid
35 from datetime import datetime, timezone
36
37 from sqlalchemy import DateTime, ForeignKey, Index, String, Text, UniqueConstraint
38 from sqlalchemy.orm import Mapped, mapped_column
39
40 from musehub.db.database import Base
41
42
43 def _new_uuid() -> str:
44 return str(uuid.uuid4())
45
46
47 def _utc_now() -> datetime:
48 return datetime.now(timezone.utc)
49
50
51 class MusehubAuthKey(Base):
52 """One registered public key for a MuseHub identity.
53
54 An identity can own many keys (one per device / agent instance) across any
55 supported algorithm. Deleting a row immediately revokes that key.
56 """
57
58 __tablename__ = "musehub_auth_keys"
59 __table_args__ = (
60 UniqueConstraint("fingerprint", name="uq_musehub_auth_keys_fingerprint"),
61 Index("ix_musehub_auth_keys_identity_id", "identity_id"),
62 Index("ix_musehub_auth_keys_algorithm", "algorithm"),
63 )
64
65 key_id: Mapped[str] = mapped_column(String(36), primary_key=True, default=_new_uuid)
66
67 # FK to musehub_identities.id — CASCADE so deleting an identity revokes all keys
68 identity_id: Mapped[str] = mapped_column(
69 String(36),
70 ForeignKey("musehub_identities.id", ondelete="CASCADE"),
71 nullable=False,
72 )
73
74 # Signing algorithm — matches KeyAlgorithm enum values in musehub.crypto.keys
75 algorithm: Mapped[str] = mapped_column(String(32), nullable=False, default="ed25519")
76
77 # URL-safe base64-encoded raw public key (no padding). Text to handle PQ keys.
78 public_key_b64: Mapped[str] = mapped_column(Text, nullable=False)
79
80 # Lowercase hex SHA-256 of the raw key bytes — used for O(1) lookup
81 fingerprint: Mapped[str] = mapped_column(String(64), nullable=False)
82
83 # Optional human-readable label ("MacBook Pro", "CI agent", etc.)
84 label: Mapped[str] = mapped_column(String(255), nullable=False, default="")
85
86 created_at: Mapped[datetime] = mapped_column(
87 DateTime(timezone=True), nullable=False, default=_utc_now
88 )
89 last_used_at: Mapped[datetime | None] = mapped_column(
90 DateTime(timezone=True), nullable=True
91 )
File History 1 commit
sha256:a10adeeb7a0169cb9900f9806ed7a973047258abb6283724fe55e8eb68ff3f0a init: musehub initial commit Human 171 days ago