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