musehub_auth_models.py
python
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