musehub_auth_models.py
python
sha256:bba4b69de173366aeb7d490d687ccffb6db9726374033addece89cf2b87642d8
docs: add production launch discovery report and infra laun…
Sonnet 5
8 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
2 commits
sha256:bba4b69de173366aeb7d490d687ccffb6db9726374033addece89cf2b87642d8
docs: add production launch discovery report and infra laun…
Sonnet 5
8 days ago
sha256:eb0928124669c933c0ef852bea1ad0c56649cf21bd7e9663c3b51004771b0591
docs: add MuseHub cloud identity/AWS operating model and Go…
Sonnet 5
patch
8 days ago