pydantic-cryptography 0.1.0__tar.gz

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,40 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
6
+ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Before 1.0, minor versions
7
+ may contain breaking changes.
8
+
9
+ ## [Unreleased]
10
+
11
+ ## [0.1.0] - 2026-10-03
12
+
13
+ ### Added
14
+
15
+ - `PrivateKey` and `PublicKey` Pydantic types for RSA, EC (P-256, P-384, P-521, secp256k1),
16
+ Ed25519, Ed448, X25519 and X448 keys, loaded from PEM or OpenSSH text. A type argument, as in
17
+ `PrivateKey[RSAPrivateKey]`, limits the kinds of keys accepted. RSA keys must be at least 2048
18
+ bits (RFC 7518), and EC keys on one of the curves that JWKs know.
19
+ - Settings-friendly loading: indented keys (e.g. in a triple-quoted default) and keys on one line
20
+ with literal `\n`s are accepted. Works with pydantic-settings.
21
+ - Private keys stay hidden: in their `repr`, when serialized to JSON, and in validation errors.
22
+ - Key fields describe themselves in their JSON schema, and so in OpenAPI: the kinds of keys and the
23
+ formats they accept, e.g. "An RSA private key: PEM (PKCS#8, PKCS#1 or SEC 1) or OpenSSH".
24
+ - JWKs and JWK Sets as Pydantic models: `public_jwk` is an `RSAPublicJWK`, `ECPublicJWK` or
25
+ `OKPPublicJWK`, and `JWKS.from_keys()` gives a `JWKS`, e.g. to return from a FastAPI endpoint,
26
+ with its OpenAPI schema. Key IDs (`kid`) are the RFC 7638 thumbprint. Checked against the RFC
27
+ examples and against joserfc. The default `alg` of Ed25519 and Ed448 keys is `Ed25519` or
28
+ `Ed448` (RFC 9864), not the deprecated `EdDSA`.
29
+ - `Alg(...)`, to set the algorithm a key field's keys are used with, e.g.
30
+ `Annotated[PrivateKey[RSAPrivateKey], Alg("RS512")]`; the key's `alg`, `use` and JWK follow it.
31
+ It's checked against the kinds of keys the field allows when the class is defined, and against
32
+ the EC curve when a key is loaded. It works on an optional field too, e.g.
33
+ `Annotated[PrivateKey[RSAPrivateKey] | None, Alg("RS512")]`. `load()` and the constructors take
34
+ an `alg` too.
35
+ - `PrivateKey.load()` and `PublicKey.load()`, with an optional kind (`"RSA"`, `"EC"`, ...) that
36
+ type checkers understand.
37
+ - Type-checker support: mypy, pyright and ty.
38
+
39
+ [Unreleased]: https://github.com/joakimnordling/pydantic-cryptography/compare/v0.1.0...HEAD
40
+ [0.1.0]: https://github.com/joakimnordling/pydantic-cryptography/releases/tag/v0.1.0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Joakim Nordling
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,412 @@
1
+ Metadata-Version: 2.4
2
+ Name: pydantic-cryptography
3
+ Version: 0.1.0
4
+ Summary: Pydantic types for cryptographic keys: load and validate PEM keys in settings, get JWKs and JWKS
5
+ Keywords: pydantic,pydantic-settings,cryptography,pem,jwk,jwks,rsa,ecdsa,ed25519
6
+ Author: Joakim Nordling
7
+ Author-email: Joakim Nordling <joakim.nordling@gmail.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Framework :: Pydantic :: 2
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Topic :: Security :: Cryptography
21
+ Classifier: Typing :: Typed
22
+ Requires-Dist: pydantic>=2.7
23
+ Requires-Dist: cryptography>=50.0.2
24
+ Requires-Dist: typing-extensions>=4.12
25
+ Requires-Python: >=3.11
26
+ Project-URL: Homepage, https://github.com/joakimnordling/pydantic-cryptography
27
+ Project-URL: Documentation, https://github.com/joakimnordling/pydantic-cryptography#readme
28
+ Project-URL: Changelog, https://github.com/joakimnordling/pydantic-cryptography/blob/main/CHANGELOG.md
29
+ Project-URL: Issues, https://github.com/joakimnordling/pydantic-cryptography/issues
30
+ Description-Content-Type: text/markdown
31
+
32
+ # pydantic-cryptography
33
+
34
+ [![PyPI](https://img.shields.io/pypi/v/pydantic-cryptography)](https://pypi.org/project/pydantic-cryptography/)
35
+ [![Python versions](https://img.shields.io/pypi/pyversions/pydantic-cryptography)](https://pypi.org/project/pydantic-cryptography/)
36
+ [![CI](https://github.com/joakimnordling/pydantic-cryptography/actions/workflows/ci.yml/badge.svg)](https://github.com/joakimnordling/pydantic-cryptography/actions/workflows/ci.yml)
37
+ [![License: MIT](https://img.shields.io/pypi/l/pydantic-cryptography)](https://github.com/joakimnordling/pydantic-cryptography/blob/main/LICENSE)
38
+ [![Coverage: 100%](https://img.shields.io/badge/coverage-100%25-brightgreen)](https://github.com/joakimnordling/pydantic-cryptography/actions/workflows/ci.yml)
39
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
40
+
41
+ Pydantic types for cryptographic keys.
42
+
43
+ Use private and public RSA, EC (elliptic curve), Ed25519, Ed448, X25519 and X448 keys in your
44
+ settings: validated when the settings load, and ready to use, e.g. to sign and verify. Want to
45
+ publish a JWKS (JSON Web Key Set) of your keys for OpenID Connect and OAuth clients?
46
+ `JWKS.from_keys()` builds it for you, with a JWK (JSON Web Key: `kty`, `kid`, `n`, `e`, …) for
47
+ each key.
48
+
49
+ ```python
50
+ from cryptography.hazmat.primitives.asymmetric import ec, rsa
51
+ from pydantic_settings import BaseSettings
52
+
53
+ from pydantic_cryptography import PrivateKey, PublicKey
54
+
55
+
56
+ class Settings(BaseSettings):
57
+ signing_key: PrivateKey[rsa.RSAPrivateKey]
58
+ partner_key: PublicKey[ec.EllipticCurvePublicKey]
59
+ ```
60
+
61
+ Fully typed: mypy, pyright and ty know that `settings.signing_key.key` is an `rsa.RSAPrivateKey`.
62
+
63
+ ## Why
64
+
65
+ Keys are usually kept as secrets and given to the application as environment variables or `.env`
66
+ files, e.g. to sign tokens, to verify a partner's signatures, or to publish in a `jwks.json` for an
67
+ OpenID Connect or OAuth setup. A `SecretStr` setting works, but you still have to load the key
68
+ before you can use it, and keep the loaded key for reuse: loading an RSA key might take tens of
69
+ milliseconds, as it's checked on the way. And a broken key, or one of the wrong kind, only shows up
70
+ when it's first used.
71
+
72
+ These types load and check each key once, when the settings load, and the loaded key is right there
73
+ in the settings, ready to use. A mistake is reported with the field's name, and the key is kept out
74
+ of reprs, JSON and error messages, so it doesn't end up in your logs by accident. For a
75
+ `jwks.json`, the JWK members and the key ID (`kid`) are computed for you.
76
+
77
+ ## Installation
78
+
79
+ ```bash
80
+ pip install pydantic-cryptography
81
+ # or
82
+ uv add pydantic-cryptography
83
+ ```
84
+
85
+ Requires Python 3.11+, Pydantic 2.7+ and cryptography 50.0.2+. The types work in any Pydantic model;
86
+ [pydantic-settings](https://docs.pydantic.dev/latest/concepts/pydantic_settings/) isn't required,
87
+ but it's where they're most useful.
88
+
89
+ ## Quick start
90
+
91
+ Create a key and give it to the application as an environment variable:
92
+
93
+ ```bash
94
+ openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out signing-key.pem
95
+ export SIGNING_KEY="$(cat signing-key.pem)"
96
+ ```
97
+
98
+ The examples in this README continue from each other, like one script. For them, we create the key
99
+ in Python:
100
+
101
+ ```python
102
+ import os
103
+
104
+ from cryptography.hazmat.primitives import serialization
105
+ from cryptography.hazmat.primitives.asymmetric import rsa
106
+
107
+ key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
108
+ os.environ["SIGNING_KEY"] = key.private_bytes(
109
+ serialization.Encoding.PEM,
110
+ serialization.PrivateFormat.PKCS8,
111
+ serialization.NoEncryption(),
112
+ ).decode()
113
+ ```
114
+
115
+ Declare it in the settings. The type argument limits the kinds of keys accepted; here only RSA:
116
+
117
+ ```python
118
+ from cryptography.hazmat.primitives.asymmetric import rsa
119
+ from pydantic_settings import BaseSettings
120
+
121
+ from pydantic_cryptography import PrivateKey
122
+
123
+
124
+ class Settings(BaseSettings):
125
+ signing_key: PrivateKey[rsa.RSAPrivateKey]
126
+ previous_signing_key: PrivateKey[rsa.RSAPrivateKey] | None = None # during a key rotation
127
+
128
+
129
+ settings = Settings()
130
+ ```
131
+
132
+ Sign with the key, e.g. a JWT with [PyJWT](https://pyjwt.readthedocs.io/):
133
+
134
+ <!-- readme-test: needs jwt -->
135
+ ```python
136
+ import jwt
137
+
138
+ key = settings.signing_key
139
+ token = jwt.encode({"sub": "someone"}, key.key, algorithm=key.alg, headers={"kid": key.kid})
140
+
141
+ # and to verify it
142
+ claims = jwt.decode(
143
+ token,
144
+ key.public_key().key,
145
+ algorithms=[key.alg], # the key's own algorithm, never the one in the token
146
+ )
147
+ assert claims == {"sub": "someone"}
148
+ ```
149
+
150
+ And publish the public key as a JWKS, e.g. at the `jwks_uri` of your OpenID configuration. With
151
+ FastAPI, return it from an endpoint, and its OpenAPI schema shows the members of each kind of key:
152
+
153
+ <!-- readme-test: needs fastapi -->
154
+ ```python
155
+ from fastapi import FastAPI
156
+
157
+ from pydantic_cryptography import JWKS
158
+
159
+ app = FastAPI()
160
+
161
+
162
+ @app.get("/.well-known/jwks.json")
163
+ def get_jwks() -> JWKS:
164
+ return JWKS.from_keys(settings.signing_key, settings.previous_signing_key)
165
+ ```
166
+
167
+ `JWKS` is a Pydantic model, so without FastAPI, `.model_dump_json()` gives the JSON, and
168
+ `.model_dump()` a dict:
169
+
170
+ ```python
171
+ from pydantic_cryptography import JWKS
172
+
173
+ jwks = JWKS.from_keys(settings.signing_key, settings.previous_signing_key)
174
+ assert jwks.keys == [settings.signing_key.public_jwk]
175
+ json_text = jwks.model_dump_json()
176
+ # {"keys":[{"kty":"RSA","kid":"...","use":"sig","alg":"RS256","n":"...","e":"AQAB"}]}
177
+ ```
178
+
179
+ ## The types
180
+
181
+ ### `PrivateKey`
182
+
183
+ A private key. As a field, it accepts:
184
+
185
+ - PEM: PKCS#8 (`-----BEGIN PRIVATE KEY-----`), PKCS#1 (`-----BEGIN RSA PRIVATE KEY-----`) or
186
+ SEC 1 (`-----BEGIN EC PRIVATE KEY-----`);
187
+ - OpenSSH (`-----BEGIN OPENSSH PRIVATE KEY-----`), as from `ssh-keygen`;
188
+ - a `cryptography` private key object, or a `PrivateKey`.
189
+
190
+ Encrypted (password-protected) keys aren't supported.
191
+
192
+ The value is cleaned up the way settings values tend to need: the indentation of each line is
193
+ removed (a key in a triple-quoted string in a class body just works), and a key squeezed onto one
194
+ line with literal `\n`s (as some deployment tools require) gets its line breaks back.
195
+
196
+ Like Pydantic's `SecretStr`, it keeps the key out of sight: its `repr` shows only the kind of key
197
+ (`PrivateKey(RSA 2048)`), and it's serialized to JSON as `"**********"`. Validation errors don't
198
+ show the input either, so a private key put in the wrong field doesn't end up in your logs. As with
199
+ `SecretStr`, the JSON of a model with a private key can't be loaded again, as the key isn't in it.
200
+
201
+ | Attribute | What it is |
202
+ | --- | --- |
203
+ | `key` | The `cryptography` key object, e.g. `rsa.RSAPrivateKey`, to sign or decrypt with. |
204
+ | `public_key()` | The public key, as a `PublicKey`. |
205
+ | `alg` | The JWA algorithm the key is used with (see [Algorithms](#algorithms)). |
206
+ | `kid` | The key ID: the [RFC 7638](https://www.rfc-editor.org/rfc/rfc7638) SHA-256 JWK thumbprint of the key. |
207
+ | `kty` | The JWK key type: `"RSA"`, `"EC"` or `"OKP"`. |
208
+ | `public_jwk` | The public key as a JWK (see [JWKs](#jwks)). |
209
+ | `public_pem` | The public key as PEM (`-----BEGIN PUBLIC KEY-----`). |
210
+ | `private_pem` | The private key as PEM (PKCS#8, `-----BEGIN PRIVATE KEY-----`). |
211
+ | `PrivateKey(key, alg=None)` | A `PrivateKey` of a `cryptography` key object. |
212
+ | `PrivateKey.load(data, kind=None, *, alg=None)` | Loads a key from PEM or OpenSSH text (`str` or `bytes`) outside a model, e.g. `PrivateKey.load(pem, "RSA")`. The `kind` is `"RSA"`, `"EC"`, `"Ed25519"`, `"Ed448"`, `"X25519"` or `"X448"`. |
213
+
214
+ **Pass the key object to other libraries**, e.g. `settings.signing_key.key`, rather than the PEM.
215
+ PyJWT, python-jose, joserfc and jwcrypto all accept it, and loading a PEM again checks the key
216
+ again, which might take tens of milliseconds for an RSA key.
217
+
218
+ ### `PublicKey`
219
+
220
+ A public key, e.g. a partner's, to verify their signatures with. As a field, it accepts PEM
221
+ (`-----BEGIN PUBLIC KEY-----` or `-----BEGIN RSA PUBLIC KEY-----`), an OpenSSH public key line
222
+ (`ssh-ed25519 AAAA... comment`), or a `cryptography` public key object. It's serialized to JSON as
223
+ PEM. It has the same attributes as `PrivateKey` (`PublicKey(key, alg=None)`, `PublicKey.load(...)`,
224
+ `key`, `alg`, `kid`, ...), except `public_key()` and `private_pem`.
225
+
226
+ ### Limiting the kinds of keys
227
+
228
+ Without a type argument, any supported kind of key is accepted. With one, only the kinds named, and
229
+ type checkers know which `key` you get:
230
+
231
+ ```python
232
+ from cryptography.hazmat.primitives.asymmetric import ec, ed448, ed25519, rsa, x448, x25519
233
+ from pydantic import BaseModel
234
+
235
+ from pydantic_cryptography import PrivateKey, PublicKey
236
+
237
+
238
+ class Keys(BaseModel):
239
+ any_key: PrivateKey | None = None # any of the kinds below
240
+ rsa_key: PrivateKey[rsa.RSAPrivateKey] | None = None
241
+ ec_key: PrivateKey[ec.EllipticCurvePrivateKey] | None = None
242
+ ed25519_key: PrivateKey[ed25519.Ed25519PrivateKey] | None = None
243
+ ed448_key: PrivateKey[ed448.Ed448PrivateKey] | None = None
244
+ x25519_key: PrivateKey[x25519.X25519PrivateKey] | None = None
245
+ x448_key: PrivateKey[x448.X448PrivateKey] | None = None
246
+ rsa_or_ec_key: PrivateKey[rsa.RSAPrivateKey | ec.EllipticCurvePrivateKey] | None = None
247
+ partner_key: PublicKey[ed25519.Ed25519PublicKey] | None = None
248
+ ```
249
+
250
+ The public key classes are in the same modules, with `Public` in place of `Private`, e.g.
251
+ `ec.EllipticCurvePublicKey`.
252
+
253
+ A key of another kind is refused with an error such as `expected an RSA or EC private key, got an
254
+ Ed25519 private key`.
255
+
256
+ ### Algorithms
257
+
258
+ A key's `.alg` is the JWA algorithm it's used with, e.g. to sign with `algorithm=key.alg`. Its JWK
259
+ says the same. The algorithms a key can be used with depend on its kind (and for EC keys, the
260
+ curve):
261
+
262
+ | Key | Default `alg` | Others |
263
+ | --- | --- | --- |
264
+ | RSA | RS256 | RS384, RS512, PS256, PS384, PS512, RSA-OAEP, RSA-OAEP-256, RSA-OAEP-384, RSA-OAEP-512 |
265
+ | EC P-256 | ES256 | ECDH-ES, ECDH-ES+A128KW, ECDH-ES+A192KW, ECDH-ES+A256KW, HPKE-0, HPKE-7, HPKE-0-KE, HPKE-7-KE |
266
+ | EC P-384 | ES384 | ECDH-ES, ECDH-ES+A128KW, ECDH-ES+A192KW, ECDH-ES+A256KW, HPKE-1, HPKE-1-KE |
267
+ | EC P-521 | ES512 | ECDH-ES, ECDH-ES+A128KW, ECDH-ES+A192KW, ECDH-ES+A256KW, HPKE-2, HPKE-2-KE |
268
+ | EC secp256k1 | ES256K | |
269
+ | Ed25519 | Ed25519 | EdDSA |
270
+ | Ed448 | Ed448 | EdDSA |
271
+ | X25519 | ECDH-ES | ECDH-ES+A128KW, ECDH-ES+A192KW, ECDH-ES+A256KW, HPKE-3, HPKE-4, HPKE-3-KE |
272
+ | X448 | ECDH-ES | ECDH-ES+A128KW, ECDH-ES+A192KW, ECDH-ES+A256KW, HPKE-5, HPKE-6, HPKE-5-KE |
273
+
274
+ The RSA-OAEP, ECDH-ES and HPKE algorithms encrypt (the JWK's `use` is `enc`); the others sign.
275
+
276
+ Keys on other EC curves, e.g. brainpoolP256r1, are refused: JWKs have no representation for them,
277
+ so they'd have no algorithm or key ID either.
278
+
279
+ **An algorithm other than the default**, e.g. RS512 or RSA-PSS for an RSA key, is set with `Alg(...)` in the field's
280
+ annotation:
281
+
282
+ ```python
283
+ from typing import Annotated
284
+
285
+ from pydantic_cryptography import Alg
286
+
287
+
288
+ class PSSSettings(BaseSettings):
289
+ signing_key: Annotated[PrivateKey[rsa.RSAPrivateKey], Alg("PS256")]
290
+
291
+
292
+ pss_key = PSSSettings().signing_key
293
+ assert pss_key.alg == pss_key.public_jwk.alg == "PS256"
294
+ ```
295
+
296
+ The algorithm must fit every kind of key the field allows: `Alg("PS256")` on a plain `PrivateKey`
297
+ raises a `TypeError` when the class is defined. For EC keys, it's also checked against the curve
298
+ when a key is loaded, e.g. ES384 needs a P-384 key. It works on an optional field too:
299
+ `Annotated[PrivateKey[rsa.RSAPrivateKey] | None, Alg("PS256")]`. Outside a model, `load()` and the
300
+ constructor take it as well: `PrivateKey.load(pem, "RSA", alg="PS256")`.
301
+
302
+ For Ed25519 and Ed448 keys, the defaults are the fully-specified algorithms of
303
+ [RFC 9864](https://www.rfc-editor.org/rfc/rfc9864), which deprecates the older `EdDSA`. Not every
304
+ library knows them yet: PyJWT doesn't as of 2.15 ([issue](https://github.com/jpadilla/pyjwt/issues/1190),
305
+ [pull request](https://github.com/jpadilla/pyjwt/pull/1199)), and it can't use a JWK that has them.
306
+ Until your verifiers do, use `Alg("EdDSA")`, and sign with `algorithm=key.alg` as usual.
307
+
308
+ The names of the kinds of keys (`"RSA"`, `"EC"`, ...) and of the algorithms (`"RS256"`, ...) are
309
+ typed as `KindName` and `AlgName`, which you can import too, e.g. for a value from your own config.
310
+
311
+ ## JWKs
312
+
313
+ `.public_jwk` is a Pydantic model of the key's JWK: an `RSAPublicJWK` (with `n` and `e`), an
314
+ `ECPublicJWK` (`crv`, `x` and `y`) or an `OKPPublicJWK` (`crv` and `x`), each also with `kty`, the
315
+ key ID as `kid`, and `use` and `alg`. `use` follows from `alg`: `enc` for the algorithms that
316
+ encrypt, `sig` for the others. `isinstance` tells the kinds apart:
317
+
318
+ ```python
319
+ from pydantic_cryptography import RSAPublicJWK
320
+
321
+ jwk = settings.signing_key.public_jwk
322
+ assert isinstance(jwk, RSAPublicJWK)
323
+ assert (jwk.kty, jwk.use, jwk.alg, jwk.e) == ("RSA", "sig", "RS256", "AQAB")
324
+ ```
325
+
326
+ `JWKS.from_keys()` takes any number of keys (`PrivateKey` or `PublicKey`). `None` is skipped, so you
327
+ can pass an optional setting directly:
328
+
329
+ ```python
330
+ assert JWKS.from_keys(pss_key, settings.previous_signing_key).keys == [pss_key.public_jwk]
331
+ ```
332
+
333
+ A key that's already in the set is left out. The same key with two different algorithms raises an
334
+ error, as a verifier couldn't tell which one to use.
335
+
336
+ Both are Pydantic models: `.model_dump_json()` gives the JSON, and `.model_dump()` a dict.
337
+
338
+ ## Settings tips
339
+
340
+ **Multi-line values.** Environment variables can hold line breaks, and so can double-quoted values
341
+ in a `.env` file. Where that's awkward, write the key on one line with `\n` in place of the line
342
+ breaks. Kubernetes and Docker secrets mounted as files work with pydantic-settings' `secrets_dir`.
343
+
344
+ **Several keys**, e.g. for a key rotation, go in a list. As for any list in pydantic-settings, the
345
+ environment variable is then a JSON array, where the line breaks of a key are written as `\n`:
346
+ `SIGNING_KEYS='["-----BEGIN PRIVATE KEY-----\nMIIE...\n-----END PRIVATE KEY-----\n"]'`.
347
+
348
+ ```python
349
+ import json
350
+
351
+ from pydantic import Field
352
+
353
+ os.environ["SIGNING_KEYS"] = json.dumps([settings.signing_key.private_pem])
354
+
355
+
356
+ class RotatingSettings(BaseSettings):
357
+ # The first key signs, and all of them are published. To rotate, add the new key last, so it's
358
+ # published before anything is signed with it; then move it first; and remove the old key once
359
+ # nothing signed with it is in use any more.
360
+ signing_keys: list[PrivateKey[rsa.RSAPrivateKey]] = Field(min_length=1)
361
+
362
+
363
+ rotating = RotatingSettings()
364
+ signing_key = rotating.signing_keys[0]
365
+ assert JWKS.from_keys(*rotating.signing_keys).keys == [signing_key.public_jwk]
366
+ ```
367
+
368
+ A mistake in one of the keys is reported with its position, e.g. `signing_keys.1`.
369
+
370
+ **A default key**, e.g. for development, can be given as text. `PrivateKey.load()` with the kind
371
+ keeps type checkers happy, as they don't accept a string for the field. Keep such defaults out of
372
+ the production settings: there, a missing environment variable would mean signing with a key that's
373
+ in your repository.
374
+
375
+ ```python
376
+ DEVELOPMENT_KEY = settings.signing_key.private_pem # yours would be a literal PEM string
377
+
378
+
379
+ class DevelopmentSettings(BaseSettings):
380
+ signing_key: PrivateKey[rsa.RSAPrivateKey] = PrivateKey.load(DEVELOPMENT_KEY, "RSA")
381
+ ```
382
+
383
+ **A new key on each start**, when nothing needs to survive a restart. Each process gets its own
384
+ key, so with several workers or instances, one can't verify what another signed:
385
+
386
+ ```python
387
+ from cryptography.hazmat.primitives.asymmetric import ed25519
388
+
389
+
390
+ class EphemeralSettings(BaseSettings):
391
+ session_key: PrivateKey[ed25519.Ed25519PrivateKey] = PrivateKey(
392
+ ed25519.Ed25519PrivateKey.generate()
393
+ )
394
+ ```
395
+
396
+ ## Limitations
397
+
398
+ - Encrypted (password-protected) private keys aren't supported. Decrypt one with `cryptography`
399
+ (`load_pem_private_key(data, password)`) and pass the key object.
400
+ - DSA keys, and other kinds not listed above, are refused.
401
+ - RSA keys must be at least 2048 bits, as RFC 7518 requires for all the RSA algorithms.
402
+ - EC keys must be on the curves P-256, P-384, P-521 or secp256k1, the ones JWKs know.
403
+ - JWKs are output only: keys aren't loaded from JWKs, and there's no JWK of a private key.
404
+ - X.509 certificates aren't accepted: a `PublicKey` takes the key itself. Take it out of a
405
+ certificate with `cryptography` (`load_pem_x509_certificate(data).public_key()`) and pass the key
406
+ object.
407
+ - PyJWT (as of 2.15) doesn't know the Ed25519 and Ed448 algorithms; see [Algorithms](#algorithms)
408
+ for the workaround.
409
+
410
+ ## License
411
+
412
+ [MIT](LICENSE)