pydantic-cryptography 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,18 @@
1
+ """Pydantic types for cryptographic keys: load and validate them in settings, get JWKs and JWKS."""
2
+
3
+ from ._jwk import JWKS, AlgName, ECPublicJWK, OKPPublicJWK, PublicJWK, RSAPublicJWK
4
+ from ._keys import Alg, PrivateKey, PublicKey
5
+ from ._kinds import KindName
6
+
7
+ __all__ = [
8
+ "JWKS",
9
+ "Alg",
10
+ "AlgName",
11
+ "ECPublicJWK",
12
+ "KindName",
13
+ "OKPPublicJWK",
14
+ "PrivateKey",
15
+ "PublicJWK",
16
+ "PublicKey",
17
+ "RSAPublicJWK",
18
+ ]
@@ -0,0 +1,361 @@
1
+ """JWK (RFC 7517) representations of `cryptography` keys, and their thumbprints (RFC 7638)."""
2
+
3
+ import json
4
+ from base64 import urlsafe_b64encode
5
+ from hashlib import sha256
6
+ from typing import TYPE_CHECKING, Annotated, Literal, NamedTuple, Self, TypeAlias
7
+
8
+ from cryptography.hazmat.primitives.asymmetric.ec import (
9
+ EllipticCurve,
10
+ EllipticCurvePublicKey,
11
+ )
12
+ from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey
13
+ from pydantic import BaseModel, ConfigDict, Field, TypeAdapter
14
+
15
+ from ._kinds import KindName, SupportedPublicKey, kind_of
16
+
17
+ if TYPE_CHECKING: # (_keys imports this module)
18
+ from ._keys import PrivateKey, PublicKey
19
+
20
+ # The JWA algorithms ("alg") that the supported keys can be used with, from the IANA JOSE registry
21
+ # (a test checks that they match the tables below). Left out: RSA1_5 (insecure) and the symmetric
22
+ # ones.
23
+ AlgName: TypeAlias = Literal[
24
+ "RS256",
25
+ "RS384",
26
+ "RS512",
27
+ "PS256",
28
+ "PS384",
29
+ "PS512",
30
+ "RSA-OAEP",
31
+ "RSA-OAEP-256",
32
+ "RSA-OAEP-384",
33
+ "RSA-OAEP-512",
34
+ "ES256",
35
+ "ES384",
36
+ "ES512",
37
+ "ES256K",
38
+ "Ed25519",
39
+ "Ed448",
40
+ "EdDSA",
41
+ "ECDH-ES",
42
+ "ECDH-ES+A128KW",
43
+ "ECDH-ES+A192KW",
44
+ "ECDH-ES+A256KW",
45
+ "HPKE-0",
46
+ "HPKE-1",
47
+ "HPKE-2",
48
+ "HPKE-3",
49
+ "HPKE-4",
50
+ "HPKE-5",
51
+ "HPKE-6",
52
+ "HPKE-7",
53
+ "HPKE-0-KE",
54
+ "HPKE-1-KE",
55
+ "HPKE-2-KE",
56
+ "HPKE-3-KE",
57
+ "HPKE-5-KE",
58
+ "HPKE-7-KE",
59
+ ]
60
+
61
+ _ECDH: tuple[AlgName, ...] = (
62
+ "ECDH-ES",
63
+ "ECDH-ES+A128KW",
64
+ "ECDH-ES+A192KW",
65
+ "ECDH-ES+A256KW",
66
+ )
67
+
68
+ # The algorithms of each kind of key other than EC; the first one is the default.
69
+ ALGS: dict[KindName, tuple[AlgName, ...]] = {
70
+ "RSA": (
71
+ "RS256",
72
+ "RS384",
73
+ "RS512",
74
+ "PS256",
75
+ "PS384",
76
+ "PS512",
77
+ "RSA-OAEP",
78
+ "RSA-OAEP-256",
79
+ "RSA-OAEP-384",
80
+ "RSA-OAEP-512",
81
+ ),
82
+ # EdDSA is deprecated by RFC 9864, but PyJWT (as of 2.15) only knows EdDSA, not Ed25519 and
83
+ # Ed448: https://github.com/jpadilla/pyjwt/issues/1190
84
+ "Ed25519": (
85
+ "Ed25519",
86
+ "EdDSA",
87
+ ),
88
+ "Ed448": (
89
+ "Ed448",
90
+ "EdDSA",
91
+ ),
92
+ "X25519": (
93
+ *_ECDH,
94
+ "HPKE-3",
95
+ "HPKE-4",
96
+ "HPKE-3-KE",
97
+ ),
98
+ "X448": (
99
+ *_ECDH,
100
+ "HPKE-5",
101
+ "HPKE-6",
102
+ "HPKE-5-KE",
103
+ ),
104
+ }
105
+
106
+
107
+ # The JWK "crv" of the EC curves (the IANA JOSE registry has no others), and the JWK "use"
108
+ ECCurveName: TypeAlias = Literal["P-256", "P-384", "P-521", "secp256k1"]
109
+ Use: TypeAlias = Literal["sig", "enc"]
110
+
111
+
112
+ class ECCurve(NamedTuple):
113
+ """An EC curve with a JWK representation."""
114
+
115
+ crv: ECCurveName
116
+ algs: tuple[AlgName, ...] # the first one is the default
117
+
118
+
119
+ # By cryptography's curve name. Other curves have no JWK representation (the keys refuse them).
120
+ EC_CURVES = {
121
+ "secp256r1": ECCurve(
122
+ crv="P-256",
123
+ algs=(
124
+ "ES256",
125
+ *_ECDH,
126
+ "HPKE-0",
127
+ "HPKE-7",
128
+ "HPKE-0-KE",
129
+ "HPKE-7-KE",
130
+ ),
131
+ ),
132
+ "secp384r1": ECCurve(
133
+ crv="P-384",
134
+ algs=(
135
+ "ES384",
136
+ *_ECDH,
137
+ "HPKE-1",
138
+ "HPKE-1-KE",
139
+ ),
140
+ ),
141
+ "secp521r1": ECCurve(
142
+ crv="P-521",
143
+ algs=(
144
+ "ES512",
145
+ *_ECDH,
146
+ "HPKE-2",
147
+ "HPKE-2-KE",
148
+ ),
149
+ ),
150
+ "secp256k1": ECCurve(
151
+ crv="secp256k1",
152
+ algs=("ES256K",),
153
+ ),
154
+ }
155
+
156
+ # The algorithms that encrypt (the JWK "use" is "enc"); the others sign ("sig").
157
+ _ENCRYPTION_ALG_PREFIXES = (
158
+ "RSA-OAEP",
159
+ "ECDH-ES",
160
+ "HPKE-",
161
+ )
162
+
163
+
164
+ def b64url(data: bytes) -> str:
165
+ """Base64url encoding without padding (RFC 7515 section 2)."""
166
+ return urlsafe_b64encode(data).decode("ascii").rstrip("=")
167
+
168
+
169
+ def _uint(value: int, length: int | None = None) -> str:
170
+ """
171
+ An unsigned integer as base64url, big-endian.
172
+
173
+ Without `length`, in as few octets as possible (RFC 7518 section 2, for the RSA members);
174
+ otherwise padded to `length` octets (for the EC members, whose length is set by the curve).
175
+ """
176
+ if length is None:
177
+ length = max(1, (value.bit_length() + 7) // 8)
178
+ return b64url(value.to_bytes(length, "big"))
179
+
180
+
181
+ def _ec_size(curve: EllipticCurve) -> int:
182
+ return (curve.key_size + 7) // 8
183
+
184
+
185
+ def public_members(key: SupportedPublicKey) -> dict[str, str]:
186
+ """
187
+ The JWK members of the public key: "kty" and the key's parameters, nothing else.
188
+
189
+ These are exactly the members the thumbprint is computed from (RFC 7638 section 3.2).
190
+ """
191
+ if isinstance(key, RSAPublicKey):
192
+ numbers = key.public_numbers()
193
+ return {
194
+ "kty": "RSA",
195
+ "n": _uint(numbers.n),
196
+ "e": _uint(numbers.e),
197
+ }
198
+ if isinstance(key, EllipticCurvePublicKey):
199
+ crv = EC_CURVES[key.curve.name].crv
200
+ size = _ec_size(key.curve)
201
+ ec_numbers = key.public_numbers()
202
+ return {
203
+ "kty": "EC",
204
+ "crv": crv,
205
+ "x": _uint(ec_numbers.x, size),
206
+ "y": _uint(ec_numbers.y, size),
207
+ }
208
+ return {
209
+ "kty": "OKP",
210
+ "crv": kind_of(key).name,
211
+ "x": b64url(key.public_bytes_raw()),
212
+ }
213
+
214
+
215
+ def thumbprint(key: SupportedPublicKey) -> str:
216
+ """The SHA-256 JWK thumbprint of the key (RFC 7638), base64url encoded."""
217
+ canonical = json.dumps(public_members(key), sort_keys=True, separators=(",", ":"))
218
+ return b64url(sha256(canonical.encode("utf-8")).digest())
219
+
220
+
221
+ def algs(key: SupportedPublicKey) -> tuple[AlgName, ...]:
222
+ """The JWA algorithms the key can be used with; the first one is the default."""
223
+ if isinstance(key, EllipticCurvePublicKey):
224
+ return EC_CURVES[key.curve.name].algs
225
+ return ALGS[kind_of(key).name]
226
+
227
+
228
+ def kind_algs(kind: KindName) -> frozenset[AlgName]:
229
+ """The algorithms a kind of key can be used with; for EC, with any of the curves."""
230
+ if kind == "EC":
231
+ return frozenset(alg for curve in EC_CURVES.values() for alg in curve.algs)
232
+ return frozenset(ALGS[kind])
233
+
234
+
235
+ def use(alg: AlgName) -> Use:
236
+ """The JWK "use" of a key used with the algorithm: "enc" or "sig"."""
237
+ return "enc" if alg.startswith(_ENCRYPTION_ALG_PREFIXES) else "sig"
238
+
239
+
240
+ # The algorithms of each JWK key type; a test checks that they match the tables above.
241
+ RSAAlgName: TypeAlias = Literal[
242
+ "RS256",
243
+ "RS384",
244
+ "RS512",
245
+ "PS256",
246
+ "PS384",
247
+ "PS512",
248
+ "RSA-OAEP",
249
+ "RSA-OAEP-256",
250
+ "RSA-OAEP-384",
251
+ "RSA-OAEP-512",
252
+ ]
253
+ ECAlgName: TypeAlias = Literal[
254
+ "ES256",
255
+ "ES384",
256
+ "ES512",
257
+ "ES256K",
258
+ "ECDH-ES",
259
+ "ECDH-ES+A128KW",
260
+ "ECDH-ES+A192KW",
261
+ "ECDH-ES+A256KW",
262
+ "HPKE-0",
263
+ "HPKE-1",
264
+ "HPKE-2",
265
+ "HPKE-7",
266
+ "HPKE-0-KE",
267
+ "HPKE-1-KE",
268
+ "HPKE-2-KE",
269
+ "HPKE-7-KE",
270
+ ]
271
+ OKPAlgName: TypeAlias = Literal[
272
+ "Ed25519",
273
+ "Ed448",
274
+ "EdDSA",
275
+ "ECDH-ES",
276
+ "ECDH-ES+A128KW",
277
+ "ECDH-ES+A192KW",
278
+ "ECDH-ES+A256KW",
279
+ "HPKE-3",
280
+ "HPKE-4",
281
+ "HPKE-5",
282
+ "HPKE-6",
283
+ "HPKE-3-KE",
284
+ "HPKE-5-KE",
285
+ ]
286
+
287
+
288
+ _Kid: TypeAlias = Annotated[
289
+ str, Field(description="The key ID: the key's SHA-256 JWK thumbprint (RFC 7638)")
290
+ ]
291
+
292
+
293
+ class _PublicJWK(BaseModel):
294
+ model_config = ConfigDict(frozen=True, extra="forbid")
295
+
296
+
297
+ class RSAPublicJWK(_PublicJWK):
298
+ """The JWK of an RSA public key."""
299
+
300
+ kty: Literal["RSA"]
301
+ kid: _Kid
302
+ use: Use
303
+ alg: RSAAlgName
304
+ n: str
305
+ e: str
306
+
307
+
308
+ class ECPublicJWK(_PublicJWK):
309
+ """The JWK of an EC public key."""
310
+
311
+ kty: Literal["EC"]
312
+ kid: _Kid
313
+ use: Use
314
+ alg: ECAlgName
315
+ crv: ECCurveName
316
+ x: str
317
+ y: str
318
+
319
+
320
+ class OKPPublicJWK(_PublicJWK):
321
+ """The JWK of an Ed25519, Ed448, X25519 or X448 public key (RFC 8037)."""
322
+
323
+ kty: Literal["OKP"]
324
+ kid: _Kid
325
+ use: Use
326
+ alg: OKPAlgName
327
+ crv: Literal["Ed25519", "Ed448", "X25519", "X448"]
328
+ x: str
329
+
330
+
331
+ PublicJWK: TypeAlias = Annotated[
332
+ RSAPublicJWK | ECPublicJWK | OKPPublicJWK, Field(discriminator="kty")
333
+ ]
334
+
335
+
336
+ class JWKS(BaseModel):
337
+ """A JWK Set (RFC 7517 section 5) of public keys, e.g. for a `jwks.json` endpoint."""
338
+
339
+ model_config = ConfigDict(frozen=True, extra="forbid")
340
+
341
+ keys: list[PublicJWK]
342
+
343
+ @classmethod
344
+ def from_keys(cls, *keys: "PrivateKey | PublicKey | None") -> Self:
345
+ """
346
+ The JWK Set of the public keys of the keys.
347
+
348
+ None is skipped, so that an optional setting (like the previous key during a key rotation)
349
+ can be passed as is, and so are repeats. The same key with two different algorithms is
350
+ refused.
351
+ """
352
+ by_kid: dict[str, PublicJWK] = {}
353
+ for key in keys:
354
+ if key is None:
355
+ continue
356
+ if by_kid.setdefault(key.kid, key.public_jwk) != key.public_jwk:
357
+ raise ValueError(f"two different JWKs with the kid {key.kid!r}")
358
+ return cls(keys=list(by_kid.values()))
359
+
360
+
361
+ PUBLIC_JWK_ADAPTER: TypeAdapter[PublicJWK] = TypeAdapter(PublicJWK)