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.
- pydantic_cryptography/__init__.py +18 -0
- pydantic_cryptography/_jwk.py +361 -0
- pydantic_cryptography/_keys.py +800 -0
- pydantic_cryptography/_kinds.py +74 -0
- pydantic_cryptography/py.typed +0 -0
- pydantic_cryptography-0.1.0.dist-info/METADATA +412 -0
- pydantic_cryptography-0.1.0.dist-info/RECORD +9 -0
- pydantic_cryptography-0.1.0.dist-info/WHEEL +4 -0
- pydantic_cryptography-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -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)
|