gabriel / musehub public
keys.py python
241 lines 9.3 KB
Raw
sha256:e519738f2102d625d47828a17d113283cd744577b43551013cb668fd04f1c3fe chore(mwp8/phase5): tick all RG checkboxes; close-out doc (… Sonnet 4.6 18 days ago
1 """Algorithm-agnostic public-key signature verification for MuseHub auth.
2
3 Design for quantum readiness
4 -----------------------------
5 Ed25519 (based on elliptic-curve Diffie-Hellman) is broken by Shor's algorithm
6 running on a sufficiently powerful quantum computer. NIST finalised three
7 post-quantum signature standards in 2024:
8
9 FIPS 203 ML-KEM (key encapsulation — not needed here)
10 FIPS 204 ML-DSA-65 (signatures — the drop-in replacement for Ed25519)
11 FIPS 205 SLH-DSA (signatures — hash-based, conservative option)
12
13 This module implements a single interface for all signature algorithms so that
14 migrating from Ed25519 → ML-DSA-65 requires changing exactly this file.
15
16 Current status
17 --------------
18 ``ed25519`` IMPLEMENTED via PyCA ``cryptography`` (hazmat Ed25519PublicKey)
19 ``ml-dsa-65`` DEFINED awaiting stable Python library support.
20 The ``cryptography`` library (PyCA) plans ML-DSA support;
21 ``liboqs-python`` (Open Quantum Safe) supports it today but
22 requires native compilation. Uncomment the ML-DSA block below
23 when the dependency is available in the container.
24
25 Algorithm wire identifiers follow the IANA/NIST naming convention:
26 "ed25519" — RFC 8032 / FIPS 186-5
27 "ml-dsa-65" — FIPS 204 (formerly CRYSTALS-Dilithium-3)
28
29 Key and signature sizes (raw bytes)
30 ------------------------------------
31 Algorithm Public key Signature
32 ed25519 32 64
33 ml-dsa-65 1952 3309
34
35 Usage
36 -----
37 from musehub.crypto.keys import KeyAlgorithm, verify_signature, key_fingerprint
38
39 # Verification raises InvalidKeyError or SignatureError on failure
40 verify_signature(
41 algorithm=KeyAlgorithm.ED25519,
42 public_key_bytes=raw_pub,
43 message=nonce_bytes,
44 signature_bytes=sig,
45 )
46
47 fp = key_fingerprint(raw_pub) # "sha256:<64-hex>", length 71
48 """
49
50 import hashlib
51 import hmac
52 from enum import Enum
53 from typing import Final
54
55 from muse.core.types import DEFAULT_SIGN_ALGO, b64url_encode, b64url_decode
56
57 # ---------------------------------------------------------------------------
58 # Algorithm registry
59 # ---------------------------------------------------------------------------
60
61 class KeyAlgorithm(str, Enum):
62 """Supported key algorithm identifiers (wire strings)."""
63
64 ED25519 = DEFAULT_SIGN_ALGO
65 ML_DSA_65 = "ml-dsa-65"
66
67 # Expected sizes in raw bytes for each algorithm.
68 # Used for pre-validation before calling into the crypto library.
69 PUBLIC_KEY_SIZES: Final[dict[KeyAlgorithm, int]] = {
70 KeyAlgorithm.ED25519: 32,
71 KeyAlgorithm.ML_DSA_65: 1952,
72 }
73
74 SIGNATURE_SIZES: Final[dict[KeyAlgorithm, int]] = {
75 KeyAlgorithm.ED25519: 64,
76 KeyAlgorithm.ML_DSA_65: 3309,
77 }
78
79 # Human-readable descriptions for documentation and error messages
80 ALGORITHM_DESCRIPTIONS: Final[dict[KeyAlgorithm, str]] = {
81 KeyAlgorithm.ED25519: "Ed25519 (RFC 8032 / FIPS 186-5) — classical, not quantum-safe",
82 KeyAlgorithm.ML_DSA_65: "ML-DSA-65 (FIPS 204) — NIST post-quantum standard, quantum-safe",
83 }
84
85 # Algorithms that are currently implemented and callable.
86 # ML_DSA_65 is defined above but implementation is pending stable Python library.
87 IMPLEMENTED_ALGORITHMS: Final[frozenset[KeyAlgorithm]] = frozenset({
88 KeyAlgorithm.ED25519,
89 # KeyAlgorithm.ML_DSA_65, # Uncomment when liboqs-python or cryptography adds support
90 })
91
92 # Default algorithm for new registrations
93 DEFAULT_ALGORITHM: Final[KeyAlgorithm] = KeyAlgorithm.ED25519
94
95 # ---------------------------------------------------------------------------
96 # Exceptions
97 # ---------------------------------------------------------------------------
98
99 class InvalidKeyError(ValueError):
100 """Raised when a public key is malformed, wrong size, or unsupported."""
101
102 class SignatureError(ValueError):
103 """Raised when a signature fails verification or is malformed."""
104
105 class AlgorithmNotImplementedError(NotImplementedError):
106 """Raised when the requested algorithm is defined but not yet implemented."""
107
108 def __init__(self, algorithm: KeyAlgorithm) -> None:
109 super().__init__(
110 f"Algorithm '{algorithm.value}' is defined but not yet implemented. "
111 f"See musehub/crypto/keys.py for the upgrade path."
112 )
113
114 # ---------------------------------------------------------------------------
115 # Core functions
116 # ---------------------------------------------------------------------------
117
118 def key_fingerprint(public_key_bytes: bytes) -> str:
119 """Return the canonical ``sha256:``-prefixed fingerprint of raw public key bytes.
120
121 Returns ``sha256:<64-hex>`` (71 chars total). Delegates to the single
122 canonical implementation in ``muse.core.types``.
123 """
124 from muse.core.types import public_key_fingerprint # noqa: PLC0415
125 return public_key_fingerprint(public_key_bytes)
126
127 def fingerprints_equal(a: str, b: str) -> bool:
128 """Constant-time comparison of two ``sha256:``-prefixed fingerprint strings.
129
130 Uses ``hmac.compare_digest`` to prevent timing-based side-channel attacks.
131 """
132 return hmac.compare_digest(a.lower(), b.lower())
133
134 def verify_signature(
135 *,
136 algorithm: KeyAlgorithm,
137 public_key_bytes: bytes,
138 message: bytes,
139 signature_bytes: bytes,
140 ) -> None:
141 """Verify a digital signature. Raises on any failure.
142
143 Args:
144 algorithm: Which signing algorithm to use for verification.
145 public_key_bytes: Raw (unencoded) public key bytes.
146 message: The exact bytes that were signed.
147 signature_bytes: The raw signature bytes.
148
149 Raises:
150 AlgorithmNotImplementedError: Algorithm is defined but not yet available.
151 InvalidKeyError: Public key is malformed or the wrong size.
152 SignatureError: Signature verification failed or signature is malformed.
153 """
154 if algorithm not in IMPLEMENTED_ALGORITHMS:
155 raise AlgorithmNotImplementedError(algorithm)
156
157 expected_key_size = PUBLIC_KEY_SIZES[algorithm]
158 if len(public_key_bytes) != expected_key_size:
159 raise InvalidKeyError(
160 f"Algorithm '{algorithm.value}' requires a {expected_key_size}-byte public key; "
161 f"got {len(public_key_bytes)} bytes."
162 )
163
164 expected_sig_size = SIGNATURE_SIZES[algorithm]
165 if len(signature_bytes) != expected_sig_size:
166 raise SignatureError(
167 f"Algorithm '{algorithm.value}' requires a {expected_sig_size}-byte signature; "
168 f"got {len(signature_bytes)} bytes."
169 )
170
171 if algorithm is KeyAlgorithm.ED25519:
172 _verify_ed25519(public_key_bytes, message, signature_bytes)
173 elif algorithm is KeyAlgorithm.ML_DSA_65: # pragma: no cover — not yet implemented
174 _verify_ml_dsa_65(public_key_bytes, message, signature_bytes)
175
176 # ---------------------------------------------------------------------------
177 # Per-algorithm implementations
178 # ---------------------------------------------------------------------------
179
180 def _verify_ed25519(
181 public_key_bytes: bytes,
182 message: bytes,
183 signature_bytes: bytes,
184 ) -> None:
185 """Verify an Ed25519 signature using PyCA cryptography (hazmat layer).
186
187 Ed25519 is a deterministic Schnorr signature over Curve25519. It is NOT
188 quantum-safe — a cryptographically-relevant quantum computer running Shor's
189 algorithm can recover the private key from the public key.
190
191 When to migrate: when `IMPLEMENTED_ALGORITHMS` includes ML_DSA_65, update
192 the CLI key-generation command to produce ML-DSA-65 keys and advise users
193 to re-register. Old Ed25519 keys continue to work until manually revoked.
194 """
195 from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
196 from cryptography.exceptions import InvalidSignature
197
198 try:
199 pub_key = Ed25519PublicKey.from_public_bytes(public_key_bytes)
200 except (ValueError, TypeError) as exc:
201 raise InvalidKeyError(f"Invalid Ed25519 public key: {exc}") from exc
202
203 try:
204 pub_key.verify(signature_bytes, message)
205 except InvalidSignature as exc:
206 raise SignatureError("Ed25519 signature verification failed") from exc
207
208 def _verify_ml_dsa_65(
209 public_key_bytes: bytes, # noqa: ARG001
210 message: bytes, # noqa: ARG001
211 signature_bytes: bytes, # noqa: ARG001
212 ) -> None: # pragma: no cover
213 """Verify an ML-DSA-65 signature (FIPS 204 / formerly CRYSTALS-Dilithium-3).
214
215 ML-DSA-65 is a lattice-based signature scheme standardised by NIST in
216 August 2024. It is believed to be secure against both classical and
217 quantum adversaries.
218
219 Implementation status: NOT YET AVAILABLE in stable Python builds.
220
221 To enable:
222 1. Add ``liboqs-python`` to ``requirements.txt`` and rebuild the Docker image.
223 (Requires ``cmake`` and C build tools in the container.)
224 OR wait for ``cryptography`` (PyCA) to add ML-DSA support.
225
226 2. Uncomment ``KeyAlgorithm.ML_DSA_65`` in ``IMPLEMENTED_ALGORITHMS``.
227
228 3. Remove this ``raise`` and implement the verification:
229
230 import oqs
231 verifier = oqs.Signature("Dilithium3")
232 is_valid = verifier.verify(message, signature_bytes, public_key_bytes)
233 if not is_valid:
234 raise SignatureError("ML-DSA-65 signature verification failed")
235
236 4. Update DEFAULT_ALGORITHM to ``KeyAlgorithm.ML_DSA_65`` and document
237 the migration path for existing Ed25519 users.
238 """
239 raise AlgorithmNotImplementedError(KeyAlgorithm.ML_DSA_65)
240
241 # b64url_encode and b64url_decode live in muse.core.types — re-exported above.
File History 2 commits
sha256:e519738f2102d625d47828a17d113283cd744577b43551013cb668fd04f1c3fe chore(mwp8/phase5): tick all RG checkboxes; close-out doc (… Sonnet 4.6 18 days ago
sha256:0e5174da777684df43b2db71f282079030ef28bacffb93e19c29ee712b2a8bc4 test(mwp8/phase0): audit — repair+force-push leave stale ca… Sonnet 4.6 20 days ago