Skip to content

Commit 156816a

Browse files
ptoffyfpseverino
andauthored
Add MLDSA{65,87} support (#229)
* Add `MLDSA{65,87}` support * Re-format * Switch to `_@spi(PostQuantum)` * More formatting * Even more formatting * Update MLDSA impl * Fix imports * Minor fixes * Update Tests/JWTKitTests/MLDSATests.swift Co-authored-by: Francesco Paolo Severino <[email protected]> * Remove unused error case * Update docs * Slight improvement * Update Sources/JWTKit/Docs.docc/index.md Co-authored-by: Francesco Paolo Severino <[email protected]> * Add snippet end * Actually add the algo to the table * What am I doing * Update snippets * Add benchmarks * Fix --------- Co-authored-by: Francesco Paolo Severino <[email protected]>
1 parent ed0532b commit 156816a

File tree

19 files changed

+483
-58
lines changed

19 files changed

+483
-58
lines changed

Benchmarks/Package.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ let package = Package(
1717
dependencies: [
1818
.product(name: "Benchmark", package: "package-benchmark"),
1919
.product(name: "JWTKit", package: "jwt-kit"),
20+
.target(name: "Utilities"),
2021
],
2122
path: "Signing",
2223
plugins: [
@@ -28,6 +29,7 @@ let package = Package(
2829
dependencies: [
2930
.product(name: "Benchmark", package: "package-benchmark"),
3031
.product(name: "JWTKit", package: "jwt-kit"),
32+
.target(name: "Utilities"),
3133
],
3234
path: "Verifying",
3335
plugins: [
@@ -39,11 +41,16 @@ let package = Package(
3941
dependencies: [
4042
.product(name: "Benchmark", package: "package-benchmark"),
4143
.product(name: "JWTKit", package: "jwt-kit"),
44+
.target(name: "Utilities"),
4245
],
4346
path: "TokenLifecycle",
4447
plugins: [
4548
.plugin(name: "BenchmarkPlugin", package: "package-benchmark")
4649
]
4750
),
51+
.target(
52+
name: "Utilities",
53+
path: "Utilities"
54+
),
4855
]
4956
)

Benchmarks/Signing/Signing.swift

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import Benchmark
22
import Foundation
3-
import JWTKit
3+
@_spi(PostQuantum) import JWTKit
4+
import Utilities
45

56
let benchmarks = {
67
Benchmark.defaultConfiguration = .init(
@@ -47,14 +48,16 @@ let benchmarks = {
4748
_ = try await keyCollection.sign(payload)
4849
}
4950
}
50-
}
51-
52-
struct Payload: JWTPayload {
53-
let name: String
54-
let admin: Bool
5551

56-
func verify(using signer: some JWTAlgorithm) async throws {
57-
// nothing to verify
52+
if #available(iOS 26, macOS 26, tvOS 26, watchOS 26, *) {
53+
Benchmark("MLDSA65") { benchmark in
54+
let seed = Data(fromHexEncodedString: mldsa65PrivateKeySeed)!
55+
let key = try MLDSA65PrivateKey(seedRepresentation: seed)
56+
let keyCollection = await JWTKeyCollection().add(mldsa: key)
57+
for _ in benchmark.scaledIterations {
58+
_ = try await keyCollection.sign(payload)
59+
}
60+
}
5861
}
5962
}
6063

@@ -102,3 +105,4 @@ let rsaPrivateKey = """
102105

103106
let eddsaPublicKeyBase64Url = "0ZcEvMCSYqSwR8XIkxOoaYjRQSAO8frTMSCpNbUl4lE"
104107
let eddsaPrivateKeyBase64Url = "d1H3_dcg0V3XyAuZW2TE5Z3rhY20M-4YAfYu_HUQd8w"
108+
let mldsa65PrivateKeySeed = "70cefb9aed5b68e018b079da8284b9d5cad5499ed9c265ff73588005d85c225c"

Benchmarks/TokenLifecycle/TokenLifecycle.swift

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import Benchmark
22
import Foundation
3-
import JWTKit
3+
@_spi(PostQuantum) import JWTKit
4+
import Utilities
45

56
let benchmarks = {
67
Benchmark.defaultConfiguration = .init(
@@ -70,14 +71,27 @@ let benchmarks = {
7071
_ = try await keyCollection.verify(token, as: Payload.self)
7172
}
7273
}
73-
}
7474

75-
struct Payload: JWTPayload {
76-
let name: String
77-
let admin: Bool
75+
if #available(iOS 26, macOS 26, tvOS 26, watchOS 26, *) {
76+
Benchmark("MLDSA65") { benchmark in
77+
for _ in benchmark.scaledIterations {
78+
let key = try MLDSA65PrivateKey(d: eddsaPrivateKeyBase64Url, curve: .ed25519)
79+
let keyCollection = JWTKeyCollection()
80+
await keyCollection.add(eddsa: key)
81+
let token = try await keyCollection.sign(payload)
82+
_ = try await keyCollection.verify(token, as: Payload.self)
83+
}
84+
}
7885

79-
func verify(using signer: some JWTAlgorithm) async throws {
80-
// nothing to verify
86+
Benchmark("MLDSA87") { benchmark in
87+
for _ in benchmark.scaledIterations {
88+
let key = try MLDSA87PrivateKey(d: eddsaPrivateKeyBase64Url, curve: .ed25519)
89+
let keyCollection = JWTKeyCollection()
90+
await keyCollection.add(eddsa: key)
91+
let token = try await keyCollection.sign(payload)
92+
_ = try await keyCollection.verify(token, as: Payload.self)
93+
}
94+
}
8195
}
8296
}
8397

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#if canImport(FoundationEssentials)
2+
import FoundationEssentials
3+
#else
4+
import Foundation
5+
#endif
6+
7+
extension Data {
8+
package init?(fromHexEncodedString string: String) {
9+
func decodeNibble(u: UInt8) -> UInt8? {
10+
switch u {
11+
case 0x30...0x39: u - 0x30
12+
case 0x41...0x46: u - 0x41 + 10
13+
case 0x61...0x66: u - 0x61 + 10
14+
default: nil
15+
}
16+
}
17+
18+
self.init(capacity: string.utf8.count / 2)
19+
20+
var iter = string.utf8.makeIterator()
21+
while let c1 = iter.next() {
22+
guard
23+
let val1 = decodeNibble(u: c1),
24+
let c2 = iter.next(),
25+
let val2 = decodeNibble(u: c2)
26+
else { return nil }
27+
self.append(val1 << 4 + val2)
28+
}
29+
}
30+
}

Benchmarks/Utilities/Payload.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import JWTKit
2+
3+
package struct Payload: JWTPayload {
4+
package let name: String
5+
package let admin: Bool
6+
7+
package func verify(using signer: some JWTAlgorithm) async throws {
8+
// nothing to verify
9+
}
10+
}

Benchmarks/Verifying/Verifying.swift

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import Benchmark
22
import Foundation
3-
import JWTKit
3+
@_spi(PostQuantum) import JWTKit
4+
import Utilities
45

56
let benchmarks = {
67
Benchmark.defaultConfiguration = .init(
@@ -69,13 +70,17 @@ let benchmarks = {
6970
_ = try await keyCollection.verify(token, as: Payload.self)
7071
}
7172
}
72-
}
73-
74-
struct Payload: JWTPayload {
75-
let name: String
76-
let admin: Bool
7773

78-
func verify(using signer: some JWTAlgorithm) async throws {
79-
// nothing to verify
74+
if #available(iOS 26, macOS 26, tvOS 26, watchOS 26, *) {
75+
Benchmark("MLDSA") { benchmark in
76+
let mldsa65PrivateKeySeed = Data(fromHexEncodedString: "70cefb9aed5b68e018b079da8284b9d5cad5499ed9c265ff73588005d85c225c")
77+
let keyCollection = try await JWTKeyCollection()
78+
.add(mldsa: MLDSA65PrivateKey(seedRepresentation: mldsa65PrivateKeySeed))
79+
let token =
80+
"eyJhbGciOiJNTC1EU0EtNjUiLCJ0eXAiOiJKV1QifQ.eyJiYXIiOjQyfQ.hsk2p8N1ScHi_Gj97WZRqm1c01MsIic8imHnvr3DjNEIfGqTScqiLqyjfd-tm1OsYQko1VI9y-g9RjA8YZPuVxXTHrQ4qOz_xXSL9nAA7_ddoknU6YOrIBQNMtoqEajvZFzfGcWVoQEniocIfXpLbR3e4fyoQjqRoQPkXNJpoE3Sw6tetMNdhL7_c_oP1ClM1sauUZTTYm6DhTvDoNV3swxh9t_duMXTLdxETN4pAb_G4xfrXqVyjkRFA5cJ9HK5Y5MXGUPRKD2VK0osuhbdFcPT3Bec4JGnVKxOYvd39p5w_WKGiTmwsRv9yE4fkXyvyDhCBHt1xUKc_6yVQl8Db60jcy5T5nLuc2TAZwTRllzSVzoFyTzlQ7edh2A3NJfiqWCF_q4y-QF3Usoiv8H4aFT22lZwIDCFpxcWQIwjt1M8WMJbJYROV6cK3c1nl0kgRfxcpq6PskAZiQ6L4wH83lwrNWGMWTGwLSwYcxtjgT3azucbrzSXBhv9JZ4nE2q2ek3sWBCdIj81qL4iVLDpOfo1jcerB1i35SWXTTTP1ROR3AfJLJR7QCLi4uEaYK1mUG8xH0CUuHL48C-ymXYMhFaz3RUPXAXMD_el3lkZDTHDQhEPf7LbXJDrId3v0-FSE187SZaW_8bAqPVMupZXU_TKvGD4juan6xQFv_0pS2sqXVLjvmMmdZPq1tj2aZuGCvReGMih0K_l8UYhAL_sm2QDt34Cjsw1ZlGkc6lJJU4ow38xl7_f3efFvuZFRT8eyoRP_s8Ld8JYOi360BL-tc5VMj510tpa5eBN3GpgnqpmhCHHPUnsiHuNdJWLmbuS4zMTlJTQ0eCkun6Kc1v2rrO1TVRIqs1aUDCTu8jsGQsZe00rdIvSU3HAJ28n6_P13sCI5JpT3pbMRdTjstzkXhGgA_D8bmjFAsV5UwugVYjTJ1u5S4hw1CtIMmV9a7uaq0MY7G58suzDzZCg14rBvj7DEWTljWZNV4OMs5m9dc42lcgZ5CC6N5ft_rNqbclkD1XJ-bbh5-halnOocQQl9J1uA7iVBoLd_tx4WOvl9RdhdEc-wVooyZ_BmXlvh1l-L8zSfmZm3r8EQcHPUhtayEAXVA7eVoNT-wAfXpV131pvjyVcxYyS2xbuHmCvL1VP5T8Ujva_aNINgZxU3w3hEzIvwZmKbkkyLpFSGPb9JmKryDDINJhu2TYJvQhhdgOkYe3IP8bEJUUiEoPI23SXj81OSgBo7GHFGooPk6FrBwnZJFOW31-SbhESYhnG0jfNeZhJhTGZe1Q6U7Ze4L_DJkiwlmebPKjUBIzSEs4HmR9-lpT05OUjuuFxOeg31MmHkuwGBbOcMxwaHZsSw40zbAYM7ktRYzPBA6MXeOojm-T3O7uCp8QCBArKlUN-rIeUSAlj76Fu6axdLzhZfT5YWzapODb0GIZAJAe4fwB_WFQuBjuR9J0JOSHt3dePL6XJAsKpBfszOY4vSjNHXdE3O3P-boAKOaOZvI4He8WKhyMENG9EaWQLlrWLnaOOIbygwDHDK6mdndvgaklyopa-jMne_ehdjTkNtsMW2uudNKisrAoUka-5kJrQZ3Rpe34DshXPj1ghRAjVk2igdozEJ1s05fgo4QzqaQPjuCbOHRLa1kOCB_G9A5ltJo88CbDRUVm-8_mR6tGiVbal5M8jMwJtYDNZV-Nsa7MWg2dVg3A63e4qpsuDgajpDxplZAFsmulGWOyeRf3tRt0GXDVMGEGdjW9iHuY5XqHN4Dz4YFPIR6NZOgfCGLrLSDyad4U6oxOmmliptmtkq6_12j_H2SV7YQT2e7rhBxTRlwsLWck6_tX3QbVfCBBtUzaxI4-FsXLKIFyFOAtE2Ng1OvYtAm5ZPE2GEwdWS35tKSaAztnKskiJ8mS9vslzo72ggoysFQeGRKC-hjlJ-EomboSrmMKjebRDQqeVq2qzFcYCe4HgPrvhEubTb0uXRLurFG8WmChBTntQc1NHI-dCsnsbDzJJoqHyuBsXQbMoY4E-Mr5QLwMKAQl0aDQ2jxdn2Y23c5oPXfKGeQy8Y6i3QIneLeGzpXbRcFZrs2BOcqAkREqs4n1qIYhwElbMgetVPVg_lIVUmY8XaH9-SCSKwncALVJm03WGCGwfEpnackyIO_i545shq9vcW_D_druZVszLbTYd7oERAOVGGL88K7u00fLpyy7rsviz_1W0HFPhlV9iTKk8uldMg4C1NMTQH2Y9IJ3D__PVfPF9hx-xMPyPK7wzC4M-nJdZop4bQYG36x4zKobYMQbNdOSSmK3poclPKzCTMH0RaMp6zynqPBVP2l4ow4gGNroH6eKVs-lVFrg3_pjnGR1fLKIvpCfkJs-TLSfw9uZrvdReMrTOvriF1wnN7uHnpCJ_Oj_NEZVGy-mH49owiNaCNu15Nt0uhqhwGQ9KlI_Einm771roQkX_XSd0if8PRvSwD427Lx8s9LMJ8JuAgu68FuAg_SiCNCnFVd7P4hTW3fI8OUNGEg40DcDxWdIRYYJ7OIeSh7Wgfa7Bk1VJ3jZs43KalQqApdTHubYdRfUjSnBG43egRO4YtZyKclQrlkQp6Lrq74tM8xPAnwr_Q5gwzoywgcnM-r3eoiZZ0qwJ40vMelEXhaoVWCWrBrjCj8Jq4VdOLMwzZBZVTHr72T3stNCECc5LWVjXwlKtVsAOP29T7cu90dJAFVwqIW-8gejMtVLLXTRiZv8k0plkPLJUB6-YyHGnIL_e-xBapj92G566Tuo-X4YDqzfuHVb7fgZit_QFcxl-fgfOlk0d1zeQc8g4oRzyw9M8NDWMBIPOlLF_CjxfrLhnmLGM52KP6j0jZEizTREsLOMS5vrTLQXq7EzBaB-7BUNGR5TjwojdUJexzKRNIGRXnmthUF587uxhIJ2EanK5ruhZX4iBuesEpMVK4B6G2WdMisSYm3M0fxOGmiIGiPzlU_k39utAvL1tqC9wD1w-l1SyZQHCmgRuZ6HiwprI4wqe5tGwqKn5fJN-Z6U1zyvq_jXfiwWnUR2GnagMMCYUlZQqlIxEBFVlJBC3-mbiPXvf-m2fy2BagmJcE3YCUU-uM_PkiO6ecqX6buraxjxexDbTdbH9VQo8u8MMSEX2IP9zZvhadTxu1k5lKvo3ZWu0jipz6Jg9HdLoJZiVN0koNoJmZXIBhXwNsVy4cpD1F8AQuI2IQDH_P8cKOrhbW_DzPcNk3Ec7bggNen6LxLC6oRHUz-mbqS5WvxBgUBXVayCiMByEX6wzwra7ZTA-9vRJhuC4phi5rM96cQFNfkTNoKj0hhvgYYdUV4XQtEDt3OC6H5Wrl3hDDqb2ZSsxZZpCly0C-VVpINBa78z9Kfegf-Mmj1akOeEUuCwJvlD55tQp2_n5BaSoIn_4qwKAiOE8JmZuEpfmk7qcjI-grh3q0bUBdikv7W0b6ny2uGa7u1dxXyGMH2C_FnLYEQZiPMO0DpD2nyOOm9Fk7vqaAmhTItB99LzqCN7PrwMz1xwidm-XtDWSc0gCcc0-c_1hYuXXHuK0qQ2mgtmY6O79MMmmR3OKEFq-FpwkmD2DFGzKqVvjTm-TRrptBcyyNuDXrRgnDfHHJoDGz-gpAiebTNTtWw3ewzWSyS1L06WihiXtHei6escdb8Rm9O_7178QkLwo6S9c1b0osuEGzOh-_4125KU2Sa27v7RfpDmFR8iNd-d1kA5rlLh8w2-VO_S6AIZ8JgFJmOQI1PjayqWYA-LtWW5Un7IOwIfK4jvAGlesGQ5dFPb_SUTtz3iW8YthqaDPW8_ybAY6Qjt6EnHdM_HDCbQn2OLF5zYKhWBo_bLPW2UiIklkVEv-dCCBZUw_QYspVESVMPlK6xG9gC_pT_MFQ0ncmNdcLXqaMsE4NjaMWMusvpgYnF2PE7uy3TdgsNpFLGGEy9mfAyFsp4YYM_WJrVyVCwTyoNex-ZcxDJ0egzy5CKwEDbqL3NzwanL3Z_1yX-4hyZWwRC-6a0WHBucNqBSSGQExwYadMHcbdcmeJRdkl6sdxnvFsArInXx1qDV-2cBkFTX5551zIHlqu-ZPBA8NmXUNzhe-R_X2mRCJh5-sEA8f8KEdfguPIDgTDQTQiQKyBo-g45cTezt239PIV6ZINDE8suB7veLIJ5Ye3XcUTqyKDgqNU6ri6BbtbhOcs2Jic2bVaLB0MkBvLCv6dprFNinKRKWZ1lUBPn6-fHYXldzJ-qCcfCpdGlTvuzB-WXuw6lgjO8i69tfkOjHMpb4r5mjkCipMreyXSbJMxSJR97MJOF3jfgndxBv4ogwPzXpjR5mOru4r0Ke--RtU5vkSGpLo7f1EbYWLnalQp7fXNEd2S09iofU9R32gr83XAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABgwQExgf"
81+
for _ in benchmark.scaledIterations {
82+
_ = try await keyCollection.verify(token, as: Payload.self)
83+
}
84+
}
8085
}
8186
}

README.md

Lines changed: 42 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
</p>
1717
<br>
1818

19-
🔑 JSON Web Token signing and verification (HMAC, RSA, PSS, ECDSA, EdDSA) using SwiftCrypto.
19+
🔑 JSON Web Token signing and verification (HMAC, ECDSA, EdDSA, MLDSA, RSA, PSS) using SwiftCrypto.
2020

2121
### Supported Platforms
2222

@@ -58,21 +58,23 @@ JWTKit provides APIs for signing and verifying JSON Web Tokens, as specified by
5858
The following algorithms, as defined in [RFC 7518 § 3](https://www.rfc-editor.org/rfc/rfc7518.html#section-3) and [RFC 8037 § 3](https://www.rfc-editor.org/rfc/rfc8037.html#section-3), are supported for both signing and verification:
5959

6060
| JWS | Algorithm | Description |
61-
| :-------------: | :-------------: | :----- |
62-
| HS256 | HMAC256 | HMAC with SHA-256 |
63-
| HS384 | HMAC384 | HMAC with SHA-384 |
64-
| HS512 | HMAC512 | HMAC with SHA-512 |
65-
| RS256 | RSA256 | RSASSA-PKCS1-v1_5 with SHA-256 |
66-
| RS384 | RSA384 | RSASSA-PKCS1-v1_5 with SHA-384 |
67-
| RS512 | RSA512 | RSASSA-PKCS1-v1_5 with SHA-512 |
68-
| PS256 | RSA256PSS | RSASSA-PSS with SHA-256 |
69-
| PS384 | RSA384PSS | RSASSA-PSS with SHA-384 |
70-
| PS512 | RSA512PSS | RSASSA-PSS with SHA-512 |
71-
| ES256 | ECDSA256 | ECDSA with curve P-256 and SHA-256 |
72-
| ES384 | ECDSA384 | ECDSA with curve P-384 and SHA-384 |
73-
| ES512 | ECDSA512 | ECDSA with curve P-521 and SHA-512 |
74-
| EdDSA | EdDSA | EdDSA with Ed25519 |
75-
| none | None | No digital signature or MAC |
61+
| :---: | :---: | --- |
62+
| `HS256` | `HMAC256` | HMAC with SHA‑256 |
63+
| `HS384` | `HMAC384` | HMAC with SHA‑384 |
64+
| `HS512` | `HMAC512` | HMAC with SHA‑512 |
65+
| `RS256` | `RSA256` | RSASSA‑PKCS1‑v1_5 + SHA‑256 |
66+
| `RS384` | `RSA384` | RSASSA‑PKCS1‑v1_5 + SHA‑384 |
67+
| `RS512` | `RSA512` | RSASSA‑PKCS1‑v1_5 + SHA‑512 |
68+
| `PS256` | `RSA256PSS` | RSASSA‑PSS + SHA‑256 |
69+
| `PS384` | `RSA384PSS` | RSASSA‑PSS + SHA‑384 |
70+
| `PS512` | `RSA512PSS` | RSASSA‑PSS + SHA‑512 |
71+
| `ES256` | `ECDSA256` | P‑256 + SHA‑256 |
72+
| `ES384` | `ECDSA384` | P‑384 + SHA‑384 |
73+
| `ES512` | `ECDSA512` | P‑521 + SHA‑512 |
74+
| `EdDSA` | `EdDSA` | Ed25519 |
75+
| `ML-DSA-65` | `MLDSA65` | MLDSA with parameter set 65 |
76+
| `ML-DSA-87` | `MLDSA87` | MLDSA with parameter set 87 |
77+
| `none` | `None`| No signature / MAC |
7678

7779
## Vapor
7880

@@ -271,6 +273,30 @@ await keys.add(eddsa: publicKey)
271273
await keys.add(eddsa: privateKey)
272274
```
273275

276+
## MLDSA
277+
278+
Hidden behind the `@_spi(PostQuantum)` flag, JWTKit supports MLDSA (Module-Lattice-Based Digital Signature Algorithm), a post-quantum signature scheme based on the CRYSTALS-DILITHIUM algorithm. It is currently behind an SPI flag because, while the MLDSA signature scheme is [standardized by NIST](https://nvlpubs.nist.gov/nistpubs/fips/nist.fips.204.pdf), its [usage in JWT](https://www.ietf.org/archive/id/draft-ietf-cose-dilithium-04.html) is still in draft state, and, while unlikely, may change before being finalized. Therefore JWTKit reserves the ability to make breaking changes to this API until the usage of MLDSA in JWT is finalized.
279+
280+
> [!NOTE]\
281+
> MLDSA requires macOS 26+.
282+
283+
Currently, to use MLDSA, you must import JWTKit with the `@_spi(PostQuantum)` flag enabled:
284+
285+
```swift
286+
@_spi(PostQuantum) import JWTKit
287+
```
288+
289+
Then you can choose whether to use MLDSA65 or MLDSA87. Use them as follows:
290+
291+
```swift
292+
// Initialize an MLDSA key with its seed
293+
let seedRepresentation = Data("...".utf8)
294+
let privateKey = try MLDSA87PrivateKey(seedRepresentation: seedRepresentation)
295+
296+
// Add private key to the key collection
297+
await keys.add(mldsa: privateKey)
298+
```
299+
274300
## RSA
275301

276302
RSA is an asymmetric algorithm. It uses a public key to verify tokens and a private key to sign them.

Snippets/JWTKitExamples.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
// snippet.MLDSA_IMPORT
2+
@_spi(PostQuantum) import JWTKit
3+
// snippet.end
14
// snippet.KEY_COLLECTION
25
import JWTKit
36

@@ -111,6 +114,19 @@ do {
111114
// snippet.end
112115
}
113116

117+
if #available(iOS 26, macOS 26, tvOS 26, watchOS 26, *) {
118+
do {
119+
// snippet.MLDSA
120+
// Initialize an MLDSA key with its seed
121+
let seedRepresentation = Data("...".utf8)
122+
let privateKey = try MLDSA87PrivateKey(seedRepresentation: seedRepresentation)
123+
124+
// Add private key to the key collection
125+
await keys.add(mldsa: privateKey)
126+
// snippet.end
127+
}
128+
}
129+
114130
extension DataProtocol {
115131
func base64URLDecodedBytes() -> [UInt8] {
116132
let string = String(decoding: self, as: UTF8.self)

Sources/JWTKit/Docs.docc/index.md

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -33,21 +33,23 @@ JWTKit provides APIs for signing and verifying JSON Web Tokens, as specified by
3333
The following algorithms, as defined in [RFC 7518 § 3](https://www.rfc-editor.org/rfc/rfc7518.html#section-3) and [RFC 8037 § 3](https://www.rfc-editor.org/rfc/rfc8037.html#section-3), are supported for both signing and verification:
3434

3535
| JWS | Algorithm | Description |
36-
| :-------------: | :-------------: | :----- |
37-
| HS256 | HMAC256 | HMAC with SHA-256 |
38-
| HS384 | HMAC384 | HMAC with SHA-384 |
39-
| HS512 | HMAC512 | HMAC with SHA-512 |
40-
| RS256 | RSA256 | RSASSA-PKCS1-v1_5 with SHA-256 |
41-
| RS384 | RSA384 | RSASSA-PKCS1-v1_5 with SHA-384 |
42-
| RS512 | RSA512 | RSASSA-PKCS1-v1_5 with SHA-512 |
43-
| PS256 | RSA256PSS | RSASSA-PSS with SHA-256 |
44-
| PS384 | RSA384PSS | RSASSA-PSS with SHA-384 |
45-
| PS512 | RSA512PSS | RSASSA-PSS with SHA-512 |
46-
| ES256 | ECDSA256 | ECDSA with curve P-256 and SHA-256 |
47-
| ES384 | ECDSA384 | ECDSA with curve P-384 and SHA-384 |
48-
| ES512 | ECDSA512 | ECDSA with curve P-521 and SHA-512 |
49-
| EdDSA | EdDSA | EdDSA with Ed25519 |
50-
| none | None | No digital signature or MAC |
36+
| :---: | :---: | --- |
37+
| `HS256` | `HMAC256` | HMAC with SHA‑256 |
38+
| `HS384` | `HMAC384` | HMAC with SHA‑384 |
39+
| `HS512` | `HMAC512` | HMAC with SHA‑512 |
40+
| `RS256` | `RSA256` | RSASSA‑PKCS1‑v1_5 + SHA‑256 |
41+
| `RS384` | `RSA384` | RSASSA‑PKCS1‑v1_5 + SHA‑384 |
42+
| `RS512` | `RSA512` | RSASSA‑PKCS1‑v1_5 + SHA‑512 |
43+
| `PS256` | `RSA256PSS` | RSASSA‑PSS + SHA‑256 |
44+
| `PS384` | `RSA384PSS` | RSASSA‑PSS + SHA‑384 |
45+
| `PS512` | `RSA512PSS` | RSASSA‑PSS + SHA‑512 |
46+
| `ES256` | `ECDSA256` | P‑256 + SHA‑256 |
47+
| `ES384` | `ECDSA384` | P‑384 + SHA‑384 |
48+
| `ES512` | `ECDSA512` | P‑521 + SHA‑512 |
49+
| `EdDSA` | `EdDSA` | Ed25519 |
50+
| `ML-DSA-65` | `MLDSA65` | MLDSA with parameter set 65 |
51+
| `ML-DSA-87` | `MLDSA87` | MLDSA with parameter set 87 |
52+
| `none` | `None`| No signature / MAC |
5153

5254
## Vapor
5355

@@ -159,6 +161,21 @@ You can create an EdDSA key using its coordinates:
159161

160162
@Snippet(path: "jwt-kit/Snippets/JWTKitExamples", slice: EDDSA)
161163

164+
## MLDSA
165+
166+
Hidden behind the `@_spi(PostQuantum)` flag, JWTKit supports MLDSA (Module-Lattice-Based Digital Signature Algorithm), a post-quantum signature scheme based on the CRYSTALS-DILITHIUM algorithm. It is currently behind an SPI flag because, while the MLDSA signature scheme is [standardized by NIST](https://nvlpubs.nist.gov/nistpubs/fips/nist.fips.204.pdf), its [usage in JWT](https://www.ietf.org/archive/id/draft-ietf-cose-dilithium-04.html) is still in draft state, and, while unlikely, may change before being finalized. Therefore JWTKit reserves the ability to make breaking changes to this API until the usage of MLDSA in JWT is finalized.
167+
168+
> Note:
169+
> MLDSA is only available on macOS 26+.
170+
171+
Currently, to use MLDSA, you must import JWTKit with the `@_spi(PostQuantum)` flag enabled:
172+
173+
@Snippet(path: "jwt-kit/Snippets/JWTKitExamples", slice: MLDSA_IMPORT)
174+
175+
Then you can choose whether to use MLDSA65 or MLDSA87. Use them as follows:
176+
177+
@Snippet(path: "jwt-kit/Snippets/JWTKitExamples", slice: MLDSA)
178+
162179
## RSA
163180

164181
RSA is an asymmetric algorithm. It uses a public key to verify tokens and a private key to sign them.

Sources/JWTKit/JWTKeyCollection.swift

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,7 @@ public actor JWTKeyCollection: Sendable {
5050
/// - Returns: Self for chaining.
5151
@discardableResult
5252
func add(_ signer: JWTSigner, for kid: JWKIdentifier? = nil) -> Self {
53-
let signer = JWTSigner(
54-
algorithm: signer.algorithm, parser: signer.parser, serializer: signer.serializer)
53+
let signer = JWTSigner(algorithm: signer.algorithm, parser: signer.parser, serializer: signer.serializer)
5554

5655
if let kid {
5756
if self.storage[kid] != nil {
@@ -106,8 +105,7 @@ public actor JWTKeyCollection: Sendable {
106105
guard let kid = jwk.keyIdentifier else {
107106
throw JWTError.invalidJWK(reason: "Missing KID")
108107
}
109-
let signer = try JWKSigner(
110-
jwk: jwk, parser: defaultJWTParser, serializer: defaultJWTSerializer)
108+
let signer = try JWKSigner(jwk: jwk, parser: defaultJWTParser, serializer: defaultJWTSerializer)
111109

112110
self.storage[kid] = .jwk(signer)
113111
switch (self.default, isDefault) {

0 commit comments

Comments
 (0)