From 0ed9f0738a16688d70bdde841dbc833a8d32551d Mon Sep 17 00:00:00 2001 From: Paul Toffoloni Date: Thu, 12 Jun 2025 17:39:09 +0200 Subject: [PATCH 01/19] Add `MLDSA{65,87}` support --- .gitignore | 1 + Package.swift | 17 +++- Sources/JWTKit/JWTKeyCollection.swift | 13 ++- Sources/JWTKit/JWTSigner.swift | 7 +- .../MLDSA/JWTKeyCollection+MLDSA.swift | 16 ++++ Sources/_QuantumJWTKit/MLDSA/MLDSA.swift | 49 ++++++++++++ .../MLDSA/MLDSA65+MLDSAKey.swift | 12 +++ .../MLDSA/MLDSA87+MLDSAKey.swift | 12 +++ Sources/_QuantumJWTKit/MLDSA/MLDSAError.swift | 5 ++ Sources/_QuantumJWTKit/MLDSA/MLDSAKey.swift | 31 ++++++++ .../_QuantumJWTKit/MLDSA/MLDSASigner.swift | 47 +++++++++++ Sources/_QuantumJWTKit/MLDSA/MLDSAType.swift | 15 ++++ Tests/_QuantumJWTKitTests/MLDSATests.swift | 79 +++++++++++++++++++ 13 files changed, 296 insertions(+), 8 deletions(-) create mode 100644 Sources/_QuantumJWTKit/MLDSA/JWTKeyCollection+MLDSA.swift create mode 100644 Sources/_QuantumJWTKit/MLDSA/MLDSA.swift create mode 100644 Sources/_QuantumJWTKit/MLDSA/MLDSA65+MLDSAKey.swift create mode 100644 Sources/_QuantumJWTKit/MLDSA/MLDSA87+MLDSAKey.swift create mode 100644 Sources/_QuantumJWTKit/MLDSA/MLDSAError.swift create mode 100644 Sources/_QuantumJWTKit/MLDSA/MLDSAKey.swift create mode 100644 Sources/_QuantumJWTKit/MLDSA/MLDSASigner.swift create mode 100644 Sources/_QuantumJWTKit/MLDSA/MLDSAType.swift create mode 100644 Tests/_QuantumJWTKitTests/MLDSATests.swift diff --git a/.gitignore b/.gitignore index f724f106..6504da49 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ Packages .build +.index-build .DS_Store *.xcodeproj Package.pins diff --git a/Package.swift b/Package.swift index bc5fbb4c..893b4681 100644 --- a/Package.swift +++ b/Package.swift @@ -13,7 +13,7 @@ let package = Package( .library(name: "JWTKit", targets: ["JWTKit"]) ], dependencies: [ - .package(url: "https://github.com/apple/swift-crypto.git", "3.8.0"..<"5.0.0"), + .package(url: "https://github.com/apple/swift-crypto.git", branch: "main"), .package(url: "https://github.com/apple/swift-certificates.git", from: "1.2.0"), .package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"), ], @@ -27,11 +27,26 @@ let package = Package( .product(name: "Logging", package: "swift-log"), ] ), + .target( + name: "_QuantumJWTKit", + dependencies: [ + "JWTKit", + .product(name: "Crypto", package: "swift-crypto"), + .product(name: "_CryptoExtras", package: "swift-crypto"), + ] + ), .testTarget( name: "JWTKitTests", dependencies: [ "JWTKit" ] ), + .testTarget( + name: "_QuantumJWTKitTests", + dependencies: [ + "JWTKit", + "_QuantumJWTKit", + ] + ), ] ) diff --git a/Sources/JWTKit/JWTKeyCollection.swift b/Sources/JWTKit/JWTKeyCollection.swift index b407276a..1210c154 100644 --- a/Sources/JWTKit/JWTKeyCollection.swift +++ b/Sources/JWTKit/JWTKeyCollection.swift @@ -32,7 +32,8 @@ public actor JWTKeyCollection: Sendable { public init( defaultJWTParser: some JWTParser = DefaultJWTParser(), defaultJWTSerializer: some JWTSerializer = DefaultJWTSerializer(), - logger: Logger = Logger(label: "jwt_kit_do_not_log", factory: { _ in SwiftLogNoOpLogHandler() }) + logger: Logger = Logger( + label: "jwt_kit_do_not_log", factory: { _ in SwiftLogNoOpLogHandler() }) ) { self.storage = [:] self.defaultJWTParser = defaultJWTParser @@ -49,7 +50,7 @@ public actor JWTKeyCollection: Sendable { /// - kid: An optional ``JWKIdentifier`` to associate with the signer. /// - Returns: Self for chaining. @discardableResult - func add(_ signer: JWTSigner, for kid: JWKIdentifier? = nil) -> Self { + package func add(_ signer: JWTSigner, for kid: JWKIdentifier? = nil) -> Self { let signer = JWTSigner( algorithm: signer.algorithm, parser: signer.parser, serializer: signer.serializer) @@ -143,7 +144,9 @@ public actor JWTKeyCollection: Sendable { } else { guard let alg, let jwkAlg = JWK.Algorithm(rawValue: alg) else { throw JWTError.generic( - identifier: "Algorithm", reason: "Invalid algorithm or unable to create signer with provided algorithm." + identifier: "Algorithm", + reason: + "Invalid algorithm or unable to create signer with provided algorithm." ) } return try await jwk.makeSigner(for: jwkAlg) @@ -157,7 +160,9 @@ public actor JWTKeyCollection: Sendable { /// - alg: An optional algorithm identifier. /// - Returns: A ``JWTKey`` if one is found; otherwise, `nil`. /// - Throws: ``JWTError/generic`` if the algorithm cannot be retrieved. - public func getKey(for kid: JWKIdentifier? = nil, alg: String? = nil) async throws -> any JWTAlgorithm { + public func getKey(for kid: JWKIdentifier? = nil, alg: String? = nil) async throws + -> any JWTAlgorithm + { try await self.getSigner(for: kid, alg: alg).algorithm } diff --git a/Sources/JWTKit/JWTSigner.swift b/Sources/JWTKit/JWTSigner.swift index 97ee07af..9591d86a 100644 --- a/Sources/JWTKit/JWTSigner.swift +++ b/Sources/JWTKit/JWTSigner.swift @@ -5,13 +5,13 @@ #endif /// A JWT signer. -final class JWTSigner: Sendable { +package final class JWTSigner: Sendable { let algorithm: JWTAlgorithm let parser: any JWTParser let serializer: any JWTSerializer - init( + package init( algorithm: some JWTAlgorithm, parser: any JWTParser = DefaultJWTParser(), serializer: any JWTSerializer = DefaultJWTSerializer() @@ -25,7 +25,8 @@ final class JWTSigner: Sendable { try await serializer.sign(payload, with: header, using: self.algorithm) } - func verify(_ token: some DataProtocol) async throws -> Payload where Payload: JWTPayload { + func verify(_ token: some DataProtocol) async throws -> Payload + where Payload: JWTPayload { let (encodedHeader, encodedPayload, encodedSignature) = try parser.getTokenParts(token) let data = encodedHeader + [.period] + encodedPayload let signature = encodedSignature.base64URLDecodedBytes() diff --git a/Sources/_QuantumJWTKit/MLDSA/JWTKeyCollection+MLDSA.swift b/Sources/_QuantumJWTKit/MLDSA/JWTKeyCollection+MLDSA.swift new file mode 100644 index 00000000..be0ada34 --- /dev/null +++ b/Sources/_QuantumJWTKit/MLDSA/JWTKeyCollection+MLDSA.swift @@ -0,0 +1,16 @@ +import JWTKit + +extension JWTKeyCollection { + @discardableResult + public func add( + mldsa key: some MLDSAKey, + kid: JWKIdentifier? = nil, + parser: some JWTParser = DefaultJWTParser(), + serializer: some JWTSerializer = DefaultJWTSerializer() + ) -> Self { + self.add( + .init(algorithm: MLDSASigner(key: key), parser: parser, serializer: serializer), + for: kid + ) + } +} diff --git a/Sources/_QuantumJWTKit/MLDSA/MLDSA.swift b/Sources/_QuantumJWTKit/MLDSA/MLDSA.swift new file mode 100644 index 00000000..fee06d56 --- /dev/null +++ b/Sources/_QuantumJWTKit/MLDSA/MLDSA.swift @@ -0,0 +1,49 @@ +import _CryptoExtras + +#if !canImport(Darwin) + import FoundationEssentials +#else + import Foundation +#endif + +public enum MLDSA: Sendable {} + +extension MLDSA { + public struct PublicKey: MLDSAKey where KeyType: MLDSAType { + public typealias MLDSAType = KeyType + + typealias PublicKey = KeyType.PrivateKey.PublicKey + + let backing: any MLDSAPublicKey + + public init(backing: some MLDSAPublicKey) { + self.backing = backing + } + + public init(rawRepresentation: some DataProtocol) throws { + self.backing = try PublicKey(rawRepresentation: rawRepresentation) + } + } +} + +extension MLDSA { + public struct PrivateKey: MLDSAKey where KeyType: MLDSAType { + public typealias MLDSAType = KeyType + + typealias PrivateKey = KeyType.PrivateKey + + let backing: any MLDSAPrivateKey + + public var publicKey: MLDSA.PublicKey { + .init(backing: self.backing.publicKey) + } + + public init(backing: some MLDSAPrivateKey) { + self.backing = backing + } + + public init(seedRepresentation: some DataProtocol) throws { + self.backing = try PrivateKey(seedRepresentation: seedRepresentation) + } + } +} diff --git a/Sources/_QuantumJWTKit/MLDSA/MLDSA65+MLDSAKey.swift b/Sources/_QuantumJWTKit/MLDSA/MLDSA65+MLDSAKey.swift new file mode 100644 index 00000000..4e85c984 --- /dev/null +++ b/Sources/_QuantumJWTKit/MLDSA/MLDSA65+MLDSAKey.swift @@ -0,0 +1,12 @@ +import _CryptoExtras + +extension MLDSA65.PublicKey: MLDSAPublicKey { + public typealias MLDSAType = MLDSA65 +} + +extension MLDSA65.PrivateKey: MLDSAPrivateKey { + public typealias MLDSAType = MLDSA65 +} + +public typealias MLDSA65PublicKey = MLDSA.PublicKey +public typealias MLDSA65PrivateKey = MLDSA.PrivateKey diff --git a/Sources/_QuantumJWTKit/MLDSA/MLDSA87+MLDSAKey.swift b/Sources/_QuantumJWTKit/MLDSA/MLDSA87+MLDSAKey.swift new file mode 100644 index 00000000..5b92881c --- /dev/null +++ b/Sources/_QuantumJWTKit/MLDSA/MLDSA87+MLDSAKey.swift @@ -0,0 +1,12 @@ +import _CryptoExtras + +extension MLDSA87.PublicKey: MLDSAPublicKey { + public typealias MLDSAType = MLDSA87 +} + +extension MLDSA87.PrivateKey: MLDSAPrivateKey { + public typealias MLDSAType = MLDSA87 +} + +public typealias MLDSA87PublicKey = MLDSA.PublicKey +public typealias MLDSA87PrivateKey = MLDSA.PrivateKey diff --git a/Sources/_QuantumJWTKit/MLDSA/MLDSAError.swift b/Sources/_QuantumJWTKit/MLDSA/MLDSAError.swift new file mode 100644 index 00000000..07c3b74d --- /dev/null +++ b/Sources/_QuantumJWTKit/MLDSA/MLDSAError.swift @@ -0,0 +1,5 @@ +enum MLDSAError: Error { + case noPrivateKey + case noPublicKey + case failedToSign(Error) +} diff --git a/Sources/_QuantumJWTKit/MLDSA/MLDSAKey.swift b/Sources/_QuantumJWTKit/MLDSA/MLDSAKey.swift new file mode 100644 index 00000000..5fccae71 --- /dev/null +++ b/Sources/_QuantumJWTKit/MLDSA/MLDSAKey.swift @@ -0,0 +1,31 @@ +#if canImport(FoundationEssentials) + import FoundationEssentials +#else + import Foundation +#endif + +public protocol MLDSAKey: Sendable { + associatedtype MLDSAType: _QuantumJWTKit.MLDSAType +} + +public protocol MLDSAPublicKey: Sendable { + associatedtype MLDSAType + + init(rawRepresentation: some DataProtocol) throws + var rawRepresentation: Data { get } + func isValidSignature(_ signature: S, for data: D) -> Bool + func isValidSignature( + _ signature: S, for data: D, context: C + ) -> Bool +} + +public protocol MLDSAPrivateKey: Sendable { + associatedtype MLDSAType + associatedtype PublicKey: MLDSAPublicKey + + var seedRepresentation: Data { get } + var publicKey: PublicKey { get } + init(seedRepresentation: some DataProtocol) throws + func signature(for data: D) throws -> Data + func signature(for data: D, context: C) throws -> Data +} diff --git a/Sources/_QuantumJWTKit/MLDSA/MLDSASigner.swift b/Sources/_QuantumJWTKit/MLDSA/MLDSASigner.swift new file mode 100644 index 00000000..77182e1e --- /dev/null +++ b/Sources/_QuantumJWTKit/MLDSA/MLDSASigner.swift @@ -0,0 +1,47 @@ +import JWTKit +import _CryptoExtras + +#if canImport(FoundationEssentials) + import FoundationEssentials +#else + import Foundation +#endif + +struct MLDSASigner: JWTAlgorithm, Sendable { + let privateKey: MLDSA.PrivateKey? + let publicKey: MLDSA.PublicKey + + var name: String = Key.MLDSAType.name + + init(key: Key) { + switch key { + case let key as MLDSA.PrivateKey: + self.privateKey = key + self.publicKey = key.publicKey + case let key as MLDSA.PublicKey: + self.privateKey = nil + self.publicKey = key + default: + fatalError() + } + } + + func sign(_ plaintext: some DataProtocol) throws -> [UInt8] { + guard let privateKey else { + throw JWTError.signingAlgorithmFailure(MLDSAError.noPrivateKey) + } + + let signature: Data + do { + signature = try privateKey.backing.signature(for: plaintext) + } catch { + throw JWTError.signingAlgorithmFailure(MLDSAError.failedToSign(error)) + } + + return signature.copyBytes() + } + + func verify(_ signature: some DataProtocol, signs plaintext: some DataProtocol) throws -> Bool { + publicKey.backing.isValidSignature(signature, for: plaintext) + } +} diff --git a/Sources/_QuantumJWTKit/MLDSA/MLDSAType.swift b/Sources/_QuantumJWTKit/MLDSA/MLDSAType.swift new file mode 100644 index 00000000..2f238211 --- /dev/null +++ b/Sources/_QuantumJWTKit/MLDSA/MLDSAType.swift @@ -0,0 +1,15 @@ +import _CryptoExtras + +public protocol MLDSAType { + associatedtype PrivateKey: MLDSAPrivateKey + + static var name: String { get } +} + +extension MLDSA65: MLDSAType { + public static var name: String { "ML-DSA-65" } +} + +extension MLDSA87: MLDSAType { + public static var name: String { "ML-DSA-87" } +} diff --git a/Tests/_QuantumJWTKitTests/MLDSATests.swift b/Tests/_QuantumJWTKitTests/MLDSATests.swift new file mode 100644 index 00000000..3aedebfb --- /dev/null +++ b/Tests/_QuantumJWTKitTests/MLDSATests.swift @@ -0,0 +1,79 @@ +import Crypto +import Foundation +import JWTKit +import Testing +import _QuantumJWTKit + +@Suite("MLDSA Tests") +struct MLDSATests { + @Test("MLDSA65 Signing") + func sign65() async throws { + struct Foo: JWTPayload { + var bar: Int + func verify(using _: some JWTAlgorithm) throws {} + } + + let key = try MLDSA65PrivateKey( + seedRepresentation: Data(fromHexEncodedString: mldsa65PrivateKeySeedRepresentation)!) + + let keyCollection = JWTKeyCollection() + await keyCollection.add(mldsa: key) + + let jwt = try await keyCollection.sign(Foo(bar: 42)) + let verified = try await keyCollection.verify(jwt, as: Foo.self) + + #expect(verified.bar == 42) + } + + @Test("MLDSA87 Signing") + func sign87() async throws { + struct Foo: JWTPayload { + var bar: Int + func verify(using _: some JWTAlgorithm) throws {} + } + + let key = try MLDSA87PrivateKey( + seedRepresentation: Data(fromHexEncodedString: mldsa65PrivateKeySeedRepresentation)!) + + let keyCollection = JWTKeyCollection() + await keyCollection.add(mldsa: key) + + let jwt = try await keyCollection.sign(Foo(bar: 42)) + let verified = try await keyCollection.verify(jwt, as: Foo.self) + + #expect(verified.bar == 42) + + print(jwt) + } +} + +let mldsa65PrivateKeySeedRepresentation = + "70cefb9aed5b68e018b079da8284b9d5cad5499ed9c265ff73588005d85c225c" + +let mldsa87PrivateKeySeedRepresentation = + "19e9e5efe0c1549ddb1d72213636d16fe2faeb2428257004ae464094ca536a66" + +extension Data { + init?(fromHexEncodedString string: String) { + func decodeNibble(u: UInt8) -> UInt8? { + switch u { + case 0x30...0x39: u - 0x30 + case 0x41...0x46: u - 0x41 + 10 + case 0x61...0x66: u - 0x61 + 10 + default: nil + } + } + + self.init(capacity: string.utf8.count / 2) + + var iter = string.utf8.makeIterator() + while let c1 = iter.next() { + guard + let val1 = decodeNibble(u: c1), + let c2 = iter.next(), + let val2 = decodeNibble(u: c2) + else { return nil } + self.append(val1 << 4 + val2) + } + } +} From 9915232d1baa4392fd516605da74867580a1c9a6 Mon Sep 17 00:00:00 2001 From: Paul Toffoloni Date: Thu, 12 Jun 2025 17:53:41 +0200 Subject: [PATCH 02/19] Re-format --- Sources/JWTKit/JWTKeyCollection.swift | 13 ++++--------- Sources/JWTKit/JWTSigner.swift | 3 +-- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/Sources/JWTKit/JWTKeyCollection.swift b/Sources/JWTKit/JWTKeyCollection.swift index 1210c154..6a49246f 100644 --- a/Sources/JWTKit/JWTKeyCollection.swift +++ b/Sources/JWTKit/JWTKeyCollection.swift @@ -32,8 +32,7 @@ public actor JWTKeyCollection: Sendable { public init( defaultJWTParser: some JWTParser = DefaultJWTParser(), defaultJWTSerializer: some JWTSerializer = DefaultJWTSerializer(), - logger: Logger = Logger( - label: "jwt_kit_do_not_log", factory: { _ in SwiftLogNoOpLogHandler() }) + logger: Logger = Logger(label: "jwt_kit_do_not_log", factory: { _ in SwiftLogNoOpLogHandler() }) ) { self.storage = [:] self.defaultJWTParser = defaultJWTParser @@ -51,8 +50,7 @@ public actor JWTKeyCollection: Sendable { /// - Returns: Self for chaining. @discardableResult package func add(_ signer: JWTSigner, for kid: JWKIdentifier? = nil) -> Self { - let signer = JWTSigner( - algorithm: signer.algorithm, parser: signer.parser, serializer: signer.serializer) + let signer = JWTSigner(algorithm: signer.algorithm, parser: signer.parser, serializer: signer.serializer) if let kid { if self.storage[kid] != nil { @@ -107,8 +105,7 @@ public actor JWTKeyCollection: Sendable { guard let kid = jwk.keyIdentifier else { throw JWTError.invalidJWK(reason: "Missing KID") } - let signer = try JWKSigner( - jwk: jwk, parser: defaultJWTParser, serializer: defaultJWTSerializer) + let signer = try JWKSigner(jwk: jwk, parser: defaultJWTParser, serializer: defaultJWTSerializer) self.storage[kid] = .jwk(signer) switch (self.default, isDefault) { @@ -144,9 +141,7 @@ public actor JWTKeyCollection: Sendable { } else { guard let alg, let jwkAlg = JWK.Algorithm(rawValue: alg) else { throw JWTError.generic( - identifier: "Algorithm", - reason: - "Invalid algorithm or unable to create signer with provided algorithm." + identifier: "Algorithm", reason: "Invalid algorithm or unable to create signer with provided algorithm." ) } return try await jwk.makeSigner(for: jwkAlg) diff --git a/Sources/JWTKit/JWTSigner.swift b/Sources/JWTKit/JWTSigner.swift index 9591d86a..40fdbd19 100644 --- a/Sources/JWTKit/JWTSigner.swift +++ b/Sources/JWTKit/JWTSigner.swift @@ -25,8 +25,7 @@ package final class JWTSigner: Sendable { try await serializer.sign(payload, with: header, using: self.algorithm) } - func verify(_ token: some DataProtocol) async throws -> Payload - where Payload: JWTPayload { + func verify(_ token: some DataProtocol) async throws -> Payload where Payload: JWTPayload { let (encodedHeader, encodedPayload, encodedSignature) = try parser.getTokenParts(token) let data = encodedHeader + [.period] + encodedPayload let signature = encodedSignature.base64URLDecodedBytes() From df20a876e653c3241bdb770cee6cda7c4dbdf55f Mon Sep 17 00:00:00 2001 From: Paul Toffoloni Date: Thu, 12 Jun 2025 21:54:27 +0200 Subject: [PATCH 03/19] Switch to `_@spi(PostQuantum)` --- Package.swift | 15 --------------- Sources/JWTKit/JWTKeyCollection.swift | 2 +- Sources/JWTKit/JWTSigner.swift | 4 ++-- .../MLDSA/JWTKeyCollection+MLDSA.swift | 3 +-- .../{_QuantumJWTKit => JWTKit}/MLDSA/MLDSA.swift | 2 +- .../MLDSA/MLDSA65+MLDSAKey.swift | 6 ++++-- .../MLDSA/MLDSA87+MLDSAKey.swift | 6 ++++-- .../MLDSA/MLDSAError.swift | 0 .../MLDSA/MLDSAKey.swift | 5 ++++- .../MLDSA/MLDSASigner.swift | 1 - .../MLDSA/MLDSAType.swift | 3 +++ Tests/JWTKitTests/JWTKitTests.swift | 10 +++++++--- .../MLDSATests.swift | 3 +-- 13 files changed, 28 insertions(+), 32 deletions(-) rename Sources/{_QuantumJWTKit => JWTKit}/MLDSA/JWTKeyCollection+MLDSA.swift (94%) rename Sources/{_QuantumJWTKit => JWTKit}/MLDSA/MLDSA.swift (96%) rename Sources/{_QuantumJWTKit => JWTKit}/MLDSA/MLDSA65+MLDSAKey.swift (50%) rename Sources/{_QuantumJWTKit => JWTKit}/MLDSA/MLDSA87+MLDSAKey.swift (50%) rename Sources/{_QuantumJWTKit => JWTKit}/MLDSA/MLDSAError.swift (100%) rename Sources/{_QuantumJWTKit => JWTKit}/MLDSA/MLDSAKey.swift (90%) rename Sources/{_QuantumJWTKit => JWTKit}/MLDSA/MLDSASigner.swift (98%) rename Sources/{_QuantumJWTKit => JWTKit}/MLDSA/MLDSAType.swift (84%) rename Tests/{_QuantumJWTKitTests => JWTKitTests}/MLDSATests.swift (98%) diff --git a/Package.swift b/Package.swift index 893b4681..59f65ee2 100644 --- a/Package.swift +++ b/Package.swift @@ -27,26 +27,11 @@ let package = Package( .product(name: "Logging", package: "swift-log"), ] ), - .target( - name: "_QuantumJWTKit", - dependencies: [ - "JWTKit", - .product(name: "Crypto", package: "swift-crypto"), - .product(name: "_CryptoExtras", package: "swift-crypto"), - ] - ), .testTarget( name: "JWTKitTests", dependencies: [ "JWTKit" ] ), - .testTarget( - name: "_QuantumJWTKitTests", - dependencies: [ - "JWTKit", - "_QuantumJWTKit", - ] - ), ] ) diff --git a/Sources/JWTKit/JWTKeyCollection.swift b/Sources/JWTKit/JWTKeyCollection.swift index 6a49246f..13c70b17 100644 --- a/Sources/JWTKit/JWTKeyCollection.swift +++ b/Sources/JWTKit/JWTKeyCollection.swift @@ -49,7 +49,7 @@ public actor JWTKeyCollection: Sendable { /// - kid: An optional ``JWKIdentifier`` to associate with the signer. /// - Returns: Self for chaining. @discardableResult - package func add(_ signer: JWTSigner, for kid: JWKIdentifier? = nil) -> Self { + func add(_ signer: JWTSigner, for kid: JWKIdentifier? = nil) -> Self { let signer = JWTSigner(algorithm: signer.algorithm, parser: signer.parser, serializer: signer.serializer) if let kid { diff --git a/Sources/JWTKit/JWTSigner.swift b/Sources/JWTKit/JWTSigner.swift index 40fdbd19..97ee07af 100644 --- a/Sources/JWTKit/JWTSigner.swift +++ b/Sources/JWTKit/JWTSigner.swift @@ -5,13 +5,13 @@ #endif /// A JWT signer. -package final class JWTSigner: Sendable { +final class JWTSigner: Sendable { let algorithm: JWTAlgorithm let parser: any JWTParser let serializer: any JWTSerializer - package init( + init( algorithm: some JWTAlgorithm, parser: any JWTParser = DefaultJWTParser(), serializer: any JWTSerializer = DefaultJWTSerializer() diff --git a/Sources/_QuantumJWTKit/MLDSA/JWTKeyCollection+MLDSA.swift b/Sources/JWTKit/MLDSA/JWTKeyCollection+MLDSA.swift similarity index 94% rename from Sources/_QuantumJWTKit/MLDSA/JWTKeyCollection+MLDSA.swift rename to Sources/JWTKit/MLDSA/JWTKeyCollection+MLDSA.swift index be0ada34..131e10e7 100644 --- a/Sources/_QuantumJWTKit/MLDSA/JWTKeyCollection+MLDSA.swift +++ b/Sources/JWTKit/MLDSA/JWTKeyCollection+MLDSA.swift @@ -1,6 +1,5 @@ -import JWTKit - extension JWTKeyCollection { + @_spi(PostQuantum) @discardableResult public func add( mldsa key: some MLDSAKey, diff --git a/Sources/_QuantumJWTKit/MLDSA/MLDSA.swift b/Sources/JWTKit/MLDSA/MLDSA.swift similarity index 96% rename from Sources/_QuantumJWTKit/MLDSA/MLDSA.swift rename to Sources/JWTKit/MLDSA/MLDSA.swift index fee06d56..9f01f419 100644 --- a/Sources/_QuantumJWTKit/MLDSA/MLDSA.swift +++ b/Sources/JWTKit/MLDSA/MLDSA.swift @@ -6,7 +6,7 @@ import _CryptoExtras import Foundation #endif -public enum MLDSA: Sendable {} +@_spi(PostQuantum) public enum MLDSA: Sendable {} extension MLDSA { public struct PublicKey: MLDSAKey where KeyType: MLDSAType { diff --git a/Sources/_QuantumJWTKit/MLDSA/MLDSA65+MLDSAKey.swift b/Sources/JWTKit/MLDSA/MLDSA65+MLDSAKey.swift similarity index 50% rename from Sources/_QuantumJWTKit/MLDSA/MLDSA65+MLDSAKey.swift rename to Sources/JWTKit/MLDSA/MLDSA65+MLDSAKey.swift index 4e85c984..8a2054d0 100644 --- a/Sources/_QuantumJWTKit/MLDSA/MLDSA65+MLDSAKey.swift +++ b/Sources/JWTKit/MLDSA/MLDSA65+MLDSAKey.swift @@ -1,12 +1,14 @@ import _CryptoExtras +@_spi(PostQuantum) extension MLDSA65.PublicKey: MLDSAPublicKey { public typealias MLDSAType = MLDSA65 } +@_spi(PostQuantum) extension MLDSA65.PrivateKey: MLDSAPrivateKey { public typealias MLDSAType = MLDSA65 } -public typealias MLDSA65PublicKey = MLDSA.PublicKey -public typealias MLDSA65PrivateKey = MLDSA.PrivateKey +@_spi(PostQuantum) public typealias MLDSA65PublicKey = MLDSA.PublicKey +@_spi(PostQuantum) public typealias MLDSA65PrivateKey = MLDSA.PrivateKey diff --git a/Sources/_QuantumJWTKit/MLDSA/MLDSA87+MLDSAKey.swift b/Sources/JWTKit/MLDSA/MLDSA87+MLDSAKey.swift similarity index 50% rename from Sources/_QuantumJWTKit/MLDSA/MLDSA87+MLDSAKey.swift rename to Sources/JWTKit/MLDSA/MLDSA87+MLDSAKey.swift index 5b92881c..665ae855 100644 --- a/Sources/_QuantumJWTKit/MLDSA/MLDSA87+MLDSAKey.swift +++ b/Sources/JWTKit/MLDSA/MLDSA87+MLDSAKey.swift @@ -1,12 +1,14 @@ import _CryptoExtras +@_spi(PostQuantum) extension MLDSA87.PublicKey: MLDSAPublicKey { public typealias MLDSAType = MLDSA87 } +@_spi(PostQuantum) extension MLDSA87.PrivateKey: MLDSAPrivateKey { public typealias MLDSAType = MLDSA87 } -public typealias MLDSA87PublicKey = MLDSA.PublicKey -public typealias MLDSA87PrivateKey = MLDSA.PrivateKey +@_spi(PostQuantum) public typealias MLDSA87PublicKey = MLDSA.PublicKey +@_spi(PostQuantum) public typealias MLDSA87PrivateKey = MLDSA.PrivateKey diff --git a/Sources/_QuantumJWTKit/MLDSA/MLDSAError.swift b/Sources/JWTKit/MLDSA/MLDSAError.swift similarity index 100% rename from Sources/_QuantumJWTKit/MLDSA/MLDSAError.swift rename to Sources/JWTKit/MLDSA/MLDSAError.swift diff --git a/Sources/_QuantumJWTKit/MLDSA/MLDSAKey.swift b/Sources/JWTKit/MLDSA/MLDSAKey.swift similarity index 90% rename from Sources/_QuantumJWTKit/MLDSA/MLDSAKey.swift rename to Sources/JWTKit/MLDSA/MLDSAKey.swift index 5fccae71..82d956e7 100644 --- a/Sources/_QuantumJWTKit/MLDSA/MLDSAKey.swift +++ b/Sources/JWTKit/MLDSA/MLDSAKey.swift @@ -4,10 +4,12 @@ import Foundation #endif +@_spi(PostQuantum) public protocol MLDSAKey: Sendable { - associatedtype MLDSAType: _QuantumJWTKit.MLDSAType + associatedtype MLDSAType: JWTKit.MLDSAType } +@_spi(PostQuantum) public protocol MLDSAPublicKey: Sendable { associatedtype MLDSAType @@ -19,6 +21,7 @@ public protocol MLDSAPublicKey: Sendable { ) -> Bool } +@_spi(PostQuantum) public protocol MLDSAPrivateKey: Sendable { associatedtype MLDSAType associatedtype PublicKey: MLDSAPublicKey diff --git a/Sources/_QuantumJWTKit/MLDSA/MLDSASigner.swift b/Sources/JWTKit/MLDSA/MLDSASigner.swift similarity index 98% rename from Sources/_QuantumJWTKit/MLDSA/MLDSASigner.swift rename to Sources/JWTKit/MLDSA/MLDSASigner.swift index 77182e1e..947c8d7b 100644 --- a/Sources/_QuantumJWTKit/MLDSA/MLDSASigner.swift +++ b/Sources/JWTKit/MLDSA/MLDSASigner.swift @@ -1,4 +1,3 @@ -import JWTKit import _CryptoExtras #if canImport(FoundationEssentials) diff --git a/Sources/_QuantumJWTKit/MLDSA/MLDSAType.swift b/Sources/JWTKit/MLDSA/MLDSAType.swift similarity index 84% rename from Sources/_QuantumJWTKit/MLDSA/MLDSAType.swift rename to Sources/JWTKit/MLDSA/MLDSAType.swift index 2f238211..596ebbb3 100644 --- a/Sources/_QuantumJWTKit/MLDSA/MLDSAType.swift +++ b/Sources/JWTKit/MLDSA/MLDSAType.swift @@ -1,15 +1,18 @@ import _CryptoExtras +@_spi(PostQuantum) public protocol MLDSAType { associatedtype PrivateKey: MLDSAPrivateKey static var name: String { get } } +@_spi(PostQuantum) extension MLDSA65: MLDSAType { public static var name: String { "ML-DSA-65" } } +@_spi(PostQuantum) extension MLDSA87: MLDSAType { public static var name: String { "ML-DSA-87" } } diff --git a/Tests/JWTKitTests/JWTKitTests.swift b/Tests/JWTKitTests/JWTKitTests.swift index 0c3ffc78..5f7890e3 100644 --- a/Tests/JWTKitTests/JWTKitTests.swift +++ b/Tests/JWTKitTests/JWTKitTests.swift @@ -91,7 +91,8 @@ struct JWTKitTests { // This token was created by us but has been tampered with, so it's non-UTF-8 and invalid let corruptCrashyToken = "eyJhbGciOiJIUzI1NiIsInR5xCI6IkpXVCJ9.eyJleHAiOjE3MzExMDkyNzkuNDIwMDM3LCJmbGFnIjp0cnVlLCJzdWIiOiJoZWxsbyJ9.iFOMv8ms0ONccGisQlzEYVe90goc3TwVD_QyztGwdCE" - #expect(throws: JWTError.malformedToken(reason: "Header and payload must be UTF-8 encoded")) { + #expect(throws: JWTError.malformedToken(reason: "Header and payload must be UTF-8 encoded")) + { _ = try parser.parse([UInt8](corruptCrashyToken.utf8), as: TestPayload.self) } } @@ -354,7 +355,9 @@ struct JWTKitTests { @Test("Test Firebase JWT and Certificate") func addFirebaseJWTAndCertificate() async throws { let payload = try await JWTKeyCollection() - .add(rsa: Insecure.RSA.PublicKey(certificatePEM: firebaseCert), digestAlgorithm: .sha256) + .add( + rsa: Insecure.RSA.PublicKey(certificatePEM: firebaseCert), digestAlgorithm: .sha256 + ) .verify(firebaseJWT, as: FirebasePayload.self) #expect(payload.userID == "y8wiKThXGKM88xxrQWDZzKnBuqv2") } @@ -524,7 +527,8 @@ struct JWTKitTests { """ let jsonDecoder = JSONDecoder() - let decodedFields = try jsonDecoder.decode([String: JWTHeaderField].self, from: encodedHeader) + let decodedFields = try jsonDecoder.decode( + [String: JWTHeaderField].self, from: encodedHeader) let decodedJsonFields = try jsonDecoder.decode( [String: JWTHeaderField].self, from: jsonFields.data(using: .utf8)! ) diff --git a/Tests/_QuantumJWTKitTests/MLDSATests.swift b/Tests/JWTKitTests/MLDSATests.swift similarity index 98% rename from Tests/_QuantumJWTKitTests/MLDSATests.swift rename to Tests/JWTKitTests/MLDSATests.swift index 3aedebfb..e0553542 100644 --- a/Tests/_QuantumJWTKitTests/MLDSATests.swift +++ b/Tests/JWTKitTests/MLDSATests.swift @@ -1,8 +1,7 @@ import Crypto import Foundation -import JWTKit +@_spi(PostQuantum) import JWTKit import Testing -import _QuantumJWTKit @Suite("MLDSA Tests") struct MLDSATests { From c621ea8acec2fe09efa450a707508807f7fc6199 Mon Sep 17 00:00:00 2001 From: Paul Toffoloni Date: Thu, 12 Jun 2025 22:04:05 +0200 Subject: [PATCH 04/19] More formatting --- Tests/JWTKitTests/JWTKitTests.swift | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/Tests/JWTKitTests/JWTKitTests.swift b/Tests/JWTKitTests/JWTKitTests.swift index 5f7890e3..0c3ffc78 100644 --- a/Tests/JWTKitTests/JWTKitTests.swift +++ b/Tests/JWTKitTests/JWTKitTests.swift @@ -91,8 +91,7 @@ struct JWTKitTests { // This token was created by us but has been tampered with, so it's non-UTF-8 and invalid let corruptCrashyToken = "eyJhbGciOiJIUzI1NiIsInR5xCI6IkpXVCJ9.eyJleHAiOjE3MzExMDkyNzkuNDIwMDM3LCJmbGFnIjp0cnVlLCJzdWIiOiJoZWxsbyJ9.iFOMv8ms0ONccGisQlzEYVe90goc3TwVD_QyztGwdCE" - #expect(throws: JWTError.malformedToken(reason: "Header and payload must be UTF-8 encoded")) - { + #expect(throws: JWTError.malformedToken(reason: "Header and payload must be UTF-8 encoded")) { _ = try parser.parse([UInt8](corruptCrashyToken.utf8), as: TestPayload.self) } } @@ -355,9 +354,7 @@ struct JWTKitTests { @Test("Test Firebase JWT and Certificate") func addFirebaseJWTAndCertificate() async throws { let payload = try await JWTKeyCollection() - .add( - rsa: Insecure.RSA.PublicKey(certificatePEM: firebaseCert), digestAlgorithm: .sha256 - ) + .add(rsa: Insecure.RSA.PublicKey(certificatePEM: firebaseCert), digestAlgorithm: .sha256) .verify(firebaseJWT, as: FirebasePayload.self) #expect(payload.userID == "y8wiKThXGKM88xxrQWDZzKnBuqv2") } @@ -527,8 +524,7 @@ struct JWTKitTests { """ let jsonDecoder = JSONDecoder() - let decodedFields = try jsonDecoder.decode( - [String: JWTHeaderField].self, from: encodedHeader) + let decodedFields = try jsonDecoder.decode([String: JWTHeaderField].self, from: encodedHeader) let decodedJsonFields = try jsonDecoder.decode( [String: JWTHeaderField].self, from: jsonFields.data(using: .utf8)! ) From 89f8fd5a109152af4cd9113d68a15bd91eaf1c2e Mon Sep 17 00:00:00 2001 From: Paul Toffoloni Date: Thu, 12 Jun 2025 22:05:33 +0200 Subject: [PATCH 05/19] Even more formatting --- Sources/JWTKit/JWTKeyCollection.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Sources/JWTKit/JWTKeyCollection.swift b/Sources/JWTKit/JWTKeyCollection.swift index 13c70b17..26235846 100644 --- a/Sources/JWTKit/JWTKeyCollection.swift +++ b/Sources/JWTKit/JWTKeyCollection.swift @@ -155,9 +155,7 @@ public actor JWTKeyCollection: Sendable { /// - alg: An optional algorithm identifier. /// - Returns: A ``JWTKey`` if one is found; otherwise, `nil`. /// - Throws: ``JWTError/generic`` if the algorithm cannot be retrieved. - public func getKey(for kid: JWKIdentifier? = nil, alg: String? = nil) async throws - -> any JWTAlgorithm - { + public func getKey(for kid: JWKIdentifier? = nil, alg: String? = nil) async throws -> any JWTAlgorithm { try await self.getSigner(for: kid, alg: alg).algorithm } From 105e11c5de81c0404cf09860263e972f05319c82 Mon Sep 17 00:00:00 2001 From: Paul Toffoloni Date: Thu, 9 Oct 2025 17:40:51 +0200 Subject: [PATCH 06/19] Update MLDSA impl --- Package.swift | 2 +- .../JWTKit/MLDSA/JWTKeyCollection+MLDSA.swift | 1 + Sources/JWTKit/MLDSA/MLDSA.swift | 14 +++++++++----- Sources/JWTKit/MLDSA/MLDSA65+MLDSAKey.swift | 13 ++++++++++--- Sources/JWTKit/MLDSA/MLDSA87+MLDSAKey.swift | 13 ++++++++++--- Sources/JWTKit/MLDSA/MLDSAKey.swift | 9 ++++++--- Sources/JWTKit/MLDSA/MLDSASigner.swift | 5 +++-- Sources/JWTKit/MLDSA/MLDSAType.swift | 5 ++++- Sources/JWTKit/X5C/EmptyPolicy.swift | 16 ++++++++++++++++ Tests/JWTKitTests/MLDSATests.swift | 2 ++ 10 files changed, 62 insertions(+), 18 deletions(-) create mode 100644 Sources/JWTKit/X5C/EmptyPolicy.swift diff --git a/Package.swift b/Package.swift index 59f65ee2..bc5fbb4c 100644 --- a/Package.swift +++ b/Package.swift @@ -13,7 +13,7 @@ let package = Package( .library(name: "JWTKit", targets: ["JWTKit"]) ], dependencies: [ - .package(url: "https://github.com/apple/swift-crypto.git", branch: "main"), + .package(url: "https://github.com/apple/swift-crypto.git", "3.8.0"..<"5.0.0"), .package(url: "https://github.com/apple/swift-certificates.git", from: "1.2.0"), .package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"), ], diff --git a/Sources/JWTKit/MLDSA/JWTKeyCollection+MLDSA.swift b/Sources/JWTKit/MLDSA/JWTKeyCollection+MLDSA.swift index 131e10e7..cbb2bd43 100644 --- a/Sources/JWTKit/MLDSA/JWTKeyCollection+MLDSA.swift +++ b/Sources/JWTKit/MLDSA/JWTKeyCollection+MLDSA.swift @@ -1,5 +1,6 @@ extension JWTKeyCollection { @_spi(PostQuantum) + @available(iOS 26.0, macOS 26.0, watchOS 26.0, tvOS 26.0, *) @discardableResult public func add( mldsa key: some MLDSAKey, diff --git a/Sources/JWTKit/MLDSA/MLDSA.swift b/Sources/JWTKit/MLDSA/MLDSA.swift index 9f01f419..35d66861 100644 --- a/Sources/JWTKit/MLDSA/MLDSA.swift +++ b/Sources/JWTKit/MLDSA/MLDSA.swift @@ -1,13 +1,16 @@ import _CryptoExtras #if !canImport(Darwin) - import FoundationEssentials +import FoundationEssentials #else - import Foundation +import Foundation #endif -@_spi(PostQuantum) public enum MLDSA: Sendable {} +@_spi(PostQuantum) +@available(iOS 26.0, macOS 26.0, watchOS 26.0, tvOS 26.0, *) +public enum MLDSA: Sendable {} +@available(iOS 26.0, macOS 26.0, watchOS 26.0, tvOS 26.0, *) extension MLDSA { public struct PublicKey: MLDSAKey where KeyType: MLDSAType { public typealias MLDSAType = KeyType @@ -26,6 +29,7 @@ extension MLDSA { } } +@available(iOS 26.0, macOS 26.0, watchOS 26.0, tvOS 26.0, *) extension MLDSA { public struct PrivateKey: MLDSAKey where KeyType: MLDSAType { public typealias MLDSAType = KeyType @@ -42,8 +46,8 @@ extension MLDSA { self.backing = backing } - public init(seedRepresentation: some DataProtocol) throws { - self.backing = try PrivateKey(seedRepresentation: seedRepresentation) + public init(seedRepresentation: some DataProtocol, publicKey: KeyType.PrivateKey.PublicKey? = nil) throws { + self.backing = try PrivateKey(seedRepresentation: seedRepresentation, publicKey: publicKey) } } } diff --git a/Sources/JWTKit/MLDSA/MLDSA65+MLDSAKey.swift b/Sources/JWTKit/MLDSA/MLDSA65+MLDSAKey.swift index 8a2054d0..470c7e54 100644 --- a/Sources/JWTKit/MLDSA/MLDSA65+MLDSAKey.swift +++ b/Sources/JWTKit/MLDSA/MLDSA65+MLDSAKey.swift @@ -1,14 +1,21 @@ -import _CryptoExtras +import Crypto @_spi(PostQuantum) +@available(iOS 26.0, macOS 26.0, watchOS 26.0, tvOS 26.0, *) extension MLDSA65.PublicKey: MLDSAPublicKey { public typealias MLDSAType = MLDSA65 } @_spi(PostQuantum) +@available(iOS 26.0, macOS 26.0, watchOS 26.0, tvOS 26.0, *) extension MLDSA65.PrivateKey: MLDSAPrivateKey { public typealias MLDSAType = MLDSA65 } -@_spi(PostQuantum) public typealias MLDSA65PublicKey = MLDSA.PublicKey -@_spi(PostQuantum) public typealias MLDSA65PrivateKey = MLDSA.PrivateKey +@_spi(PostQuantum) +@available(iOS 26.0, macOS 26.0, watchOS 26.0, tvOS 26.0, *) +public typealias MLDSA65PublicKey = MLDSA.PublicKey + +@_spi(PostQuantum) +@available(iOS 26.0, macOS 26.0, watchOS 26.0, tvOS 26.0, *) +public typealias MLDSA65PrivateKey = MLDSA.PrivateKey diff --git a/Sources/JWTKit/MLDSA/MLDSA87+MLDSAKey.swift b/Sources/JWTKit/MLDSA/MLDSA87+MLDSAKey.swift index 665ae855..ce85e898 100644 --- a/Sources/JWTKit/MLDSA/MLDSA87+MLDSAKey.swift +++ b/Sources/JWTKit/MLDSA/MLDSA87+MLDSAKey.swift @@ -1,14 +1,21 @@ -import _CryptoExtras +import Crypto @_spi(PostQuantum) +@available(iOS 26.0, macOS 26.0, watchOS 26.0, tvOS 26.0, *) extension MLDSA87.PublicKey: MLDSAPublicKey { public typealias MLDSAType = MLDSA87 } @_spi(PostQuantum) +@available(iOS 26.0, macOS 26.0, watchOS 26.0, tvOS 26.0, *) extension MLDSA87.PrivateKey: MLDSAPrivateKey { public typealias MLDSAType = MLDSA87 } -@_spi(PostQuantum) public typealias MLDSA87PublicKey = MLDSA.PublicKey -@_spi(PostQuantum) public typealias MLDSA87PrivateKey = MLDSA.PrivateKey +@_spi(PostQuantum) +@available(iOS 26.0, macOS 26.0, watchOS 26.0, tvOS 26.0, *) +public typealias MLDSA87PublicKey = MLDSA.PublicKey + +@_spi(PostQuantum) +@available(iOS 26.0, macOS 26.0, watchOS 26.0, tvOS 26.0, *) +public typealias MLDSA87PrivateKey = MLDSA.PrivateKey diff --git a/Sources/JWTKit/MLDSA/MLDSAKey.swift b/Sources/JWTKit/MLDSA/MLDSAKey.swift index 82d956e7..31fdc1d9 100644 --- a/Sources/JWTKit/MLDSA/MLDSAKey.swift +++ b/Sources/JWTKit/MLDSA/MLDSAKey.swift @@ -1,15 +1,17 @@ #if canImport(FoundationEssentials) - import FoundationEssentials +import FoundationEssentials #else - import Foundation +import Foundation #endif @_spi(PostQuantum) +@available(iOS 26.0, macOS 26.0, watchOS 26.0, tvOS 26.0, *) public protocol MLDSAKey: Sendable { associatedtype MLDSAType: JWTKit.MLDSAType } @_spi(PostQuantum) +@available(iOS 26.0, macOS 26.0, watchOS 26.0, tvOS 26.0, *) public protocol MLDSAPublicKey: Sendable { associatedtype MLDSAType @@ -22,13 +24,14 @@ public protocol MLDSAPublicKey: Sendable { } @_spi(PostQuantum) +@available(iOS 26.0, macOS 26.0, watchOS 26.0, tvOS 26.0, *) public protocol MLDSAPrivateKey: Sendable { associatedtype MLDSAType associatedtype PublicKey: MLDSAPublicKey var seedRepresentation: Data { get } var publicKey: PublicKey { get } - init(seedRepresentation: some DataProtocol) throws + init(seedRepresentation: D, publicKey: PublicKey?) throws where D: DataProtocol func signature(for data: D) throws -> Data func signature(for data: D, context: C) throws -> Data } diff --git a/Sources/JWTKit/MLDSA/MLDSASigner.swift b/Sources/JWTKit/MLDSA/MLDSASigner.swift index 947c8d7b..9cfb3803 100644 --- a/Sources/JWTKit/MLDSA/MLDSASigner.swift +++ b/Sources/JWTKit/MLDSA/MLDSASigner.swift @@ -1,11 +1,12 @@ import _CryptoExtras #if canImport(FoundationEssentials) - import FoundationEssentials +import FoundationEssentials #else - import Foundation +import Foundation #endif +@available(iOS 26.0, macOS 26.0, watchOS 26.0, tvOS 26.0, *) struct MLDSASigner: JWTAlgorithm, Sendable { let privateKey: MLDSA.PrivateKey? let publicKey: MLDSA.PublicKey diff --git a/Sources/JWTKit/MLDSA/MLDSAType.swift b/Sources/JWTKit/MLDSA/MLDSAType.swift index 596ebbb3..04d895ea 100644 --- a/Sources/JWTKit/MLDSA/MLDSAType.swift +++ b/Sources/JWTKit/MLDSA/MLDSAType.swift @@ -1,6 +1,7 @@ -import _CryptoExtras +import Crypto @_spi(PostQuantum) +@available(iOS 26.0, macOS 26.0, watchOS 26.0, tvOS 26.0, *) public protocol MLDSAType { associatedtype PrivateKey: MLDSAPrivateKey @@ -8,11 +9,13 @@ public protocol MLDSAType { } @_spi(PostQuantum) +@available(iOS 26.0, macOS 26.0, watchOS 26.0, tvOS 26.0, *) extension MLDSA65: MLDSAType { public static var name: String { "ML-DSA-65" } } @_spi(PostQuantum) +@available(iOS 26.0, macOS 26.0, watchOS 26.0, tvOS 26.0, *) extension MLDSA87: MLDSAType { public static var name: String { "ML-DSA-87" } } diff --git a/Sources/JWTKit/X5C/EmptyPolicy.swift b/Sources/JWTKit/X5C/EmptyPolicy.swift new file mode 100644 index 00000000..e6038a80 --- /dev/null +++ b/Sources/JWTKit/X5C/EmptyPolicy.swift @@ -0,0 +1,16 @@ +import SwiftASN1 +import X509 + +/// This Policy acts as a placeholder. Its result is always positive. +public struct EmptyPolicy: VerifierPolicy { + @inlinable + public var verifyingCriticalExtensions: [SwiftASN1.ASN1ObjectIdentifier] { [] } + + @inlinable + init() {} + + @inlinable + public func chainMeetsPolicyRequirements(chain: UnverifiedCertificateChain) -> PolicyEvaluationResult { + .meetsPolicy + } +} diff --git a/Tests/JWTKitTests/MLDSATests.swift b/Tests/JWTKitTests/MLDSATests.swift index e0553542..865b9871 100644 --- a/Tests/JWTKitTests/MLDSATests.swift +++ b/Tests/JWTKitTests/MLDSATests.swift @@ -6,6 +6,7 @@ import Testing @Suite("MLDSA Tests") struct MLDSATests { @Test("MLDSA65 Signing") + @available(iOS 26.0, macOS 26.0, watchOS 26.0, tvOS 26.0, *) func sign65() async throws { struct Foo: JWTPayload { var bar: Int @@ -25,6 +26,7 @@ struct MLDSATests { } @Test("MLDSA87 Signing") + @available(iOS 26.0, macOS 26.0, watchOS 26.0, tvOS 26.0, *) func sign87() async throws { struct Foo: JWTPayload { var bar: Int From 04db51a2bf127a5f2b2a99fb361a38e055ec4896 Mon Sep 17 00:00:00 2001 From: Paul Toffoloni Date: Wed, 15 Oct 2025 18:46:19 +0200 Subject: [PATCH 07/19] Fix imports --- Sources/JWTKit/MLDSA/MLDSA.swift | 2 +- Sources/JWTKit/MLDSA/MLDSASigner.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/JWTKit/MLDSA/MLDSA.swift b/Sources/JWTKit/MLDSA/MLDSA.swift index 35d66861..f2c49ee0 100644 --- a/Sources/JWTKit/MLDSA/MLDSA.swift +++ b/Sources/JWTKit/MLDSA/MLDSA.swift @@ -1,4 +1,4 @@ -import _CryptoExtras +import CryptoExtras #if !canImport(Darwin) import FoundationEssentials diff --git a/Sources/JWTKit/MLDSA/MLDSASigner.swift b/Sources/JWTKit/MLDSA/MLDSASigner.swift index 9cfb3803..131573f2 100644 --- a/Sources/JWTKit/MLDSA/MLDSASigner.swift +++ b/Sources/JWTKit/MLDSA/MLDSASigner.swift @@ -1,4 +1,4 @@ -import _CryptoExtras +import CryptoExtras #if canImport(FoundationEssentials) import FoundationEssentials From bd5bfe7a38d256f838778190465cd79831b09f45 Mon Sep 17 00:00:00 2001 From: Paul Toffoloni Date: Wed, 15 Oct 2025 18:48:47 +0200 Subject: [PATCH 08/19] Minor fixes --- Sources/JWTKit/X5C/EmptyPolicy.swift | 16 ---------------- Tests/JWTKitTests/MLDSATests.swift | 2 -- 2 files changed, 18 deletions(-) delete mode 100644 Sources/JWTKit/X5C/EmptyPolicy.swift diff --git a/Sources/JWTKit/X5C/EmptyPolicy.swift b/Sources/JWTKit/X5C/EmptyPolicy.swift deleted file mode 100644 index e6038a80..00000000 --- a/Sources/JWTKit/X5C/EmptyPolicy.swift +++ /dev/null @@ -1,16 +0,0 @@ -import SwiftASN1 -import X509 - -/// This Policy acts as a placeholder. Its result is always positive. -public struct EmptyPolicy: VerifierPolicy { - @inlinable - public var verifyingCriticalExtensions: [SwiftASN1.ASN1ObjectIdentifier] { [] } - - @inlinable - init() {} - - @inlinable - public func chainMeetsPolicyRequirements(chain: UnverifiedCertificateChain) -> PolicyEvaluationResult { - .meetsPolicy - } -} diff --git a/Tests/JWTKitTests/MLDSATests.swift b/Tests/JWTKitTests/MLDSATests.swift index 865b9871..96419645 100644 --- a/Tests/JWTKitTests/MLDSATests.swift +++ b/Tests/JWTKitTests/MLDSATests.swift @@ -43,8 +43,6 @@ struct MLDSATests { let verified = try await keyCollection.verify(jwt, as: Foo.self) #expect(verified.bar == 42) - - print(jwt) } } From 46e1bbdbe11c758a984384abad00a0043b8ffb0a Mon Sep 17 00:00:00 2001 From: Paul Toffoloni <69189821+ptoffy@users.noreply.github.com> Date: Wed, 15 Oct 2025 21:19:03 +0200 Subject: [PATCH 09/19] Update Tests/JWTKitTests/MLDSATests.swift Co-authored-by: Francesco Paolo Severino <96546612+fpseverino@users.noreply.github.com> --- Tests/JWTKitTests/MLDSATests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/JWTKitTests/MLDSATests.swift b/Tests/JWTKitTests/MLDSATests.swift index 96419645..88b1bf68 100644 --- a/Tests/JWTKitTests/MLDSATests.swift +++ b/Tests/JWTKitTests/MLDSATests.swift @@ -34,7 +34,7 @@ struct MLDSATests { } let key = try MLDSA87PrivateKey( - seedRepresentation: Data(fromHexEncodedString: mldsa65PrivateKeySeedRepresentation)!) + seedRepresentation: Data(fromHexEncodedString: mldsa87PrivateKeySeedRepresentation)!) let keyCollection = JWTKeyCollection() await keyCollection.add(mldsa: key) From 531ef578163b9909d00c596baf4c0088248250f3 Mon Sep 17 00:00:00 2001 From: Paul Toffoloni Date: Wed, 15 Oct 2025 21:20:57 +0200 Subject: [PATCH 10/19] Remove unused error case --- Sources/JWTKit/MLDSA/MLDSAError.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/JWTKit/MLDSA/MLDSAError.swift b/Sources/JWTKit/MLDSA/MLDSAError.swift index 07c3b74d..21ea0cab 100644 --- a/Sources/JWTKit/MLDSA/MLDSAError.swift +++ b/Sources/JWTKit/MLDSA/MLDSAError.swift @@ -1,5 +1,4 @@ enum MLDSAError: Error { case noPrivateKey - case noPublicKey case failedToSign(Error) } From 88bbf84d812bfb7c416884b8780ee961f0cf9416 Mon Sep 17 00:00:00 2001 From: Paul Toffoloni Date: Thu, 16 Oct 2025 09:58:55 +0200 Subject: [PATCH 11/19] Update docs --- README.md | 24 ++++++++++++++++++++++++ Snippets/JWTKitExamples.swift | 12 ++++++++++++ Sources/JWTKit/Docs.docc/index.md | 15 +++++++++++++++ 3 files changed, 51 insertions(+) diff --git a/README.md b/README.md index 9f7be7ee..ed9ac1cb 100644 --- a/README.md +++ b/README.md @@ -271,6 +271,30 @@ await keys.add(eddsa: publicKey) await keys.add(eddsa: privateKey) ``` +## MLDSA + +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 without until the usage of MLDSA in JWT is finalized. + +> [!NOTE]\ +> MLDSA requires macOS 26+. + +Currently, to use MLDSA, you must import JWTKit with the `@_spi(PostQuantum)` flag enabled: + +```swift +@_spi(PostQuantum) import JWTKit +``` + +Then you can choose whether to use MLDSA65 or MLDSA87. Use them as follows: + +```swift +// Initialize an MLDSA key with its seed +let seedRepresentation = Data("...".utf8) +let privateKey = try MLDSA87PrivateKey(seedRepresentation: seedRepresentation) + +// Add private key to the key collection +await keys.add(mldsa: privateKey) +``` + ## RSA RSA is an asymmetric algorithm. It uses a public key to verify tokens and a private key to sign them. diff --git a/Snippets/JWTKitExamples.swift b/Snippets/JWTKitExamples.swift index 422edecc..1815c971 100644 --- a/Snippets/JWTKitExamples.swift +++ b/Snippets/JWTKitExamples.swift @@ -1,5 +1,7 @@ // snippet.KEY_COLLECTION import JWTKit +// snippet.MLDSA_IMPORT +@_spi(PostQuantum) import JWTKit #if !canImport(Darwin) import FoundationEssentials @@ -111,6 +113,16 @@ do { // snippet.end } +do { + // snippet.MLDSA + // Initialize an MLDSA key with its seed + let seedRepresentation = Data("...".utf8) + let privateKey = try MLDSA87PrivateKey(seedRepresentation: seedRepresentation) + + // Add private key to the key collection + await keys.add(mldsa: privateKey) +} + extension DataProtocol { func base64URLDecodedBytes() -> [UInt8] { let string = String(decoding: self, as: UTF8.self) diff --git a/Sources/JWTKit/Docs.docc/index.md b/Sources/JWTKit/Docs.docc/index.md index a4bf86a9..1410feb2 100644 --- a/Sources/JWTKit/Docs.docc/index.md +++ b/Sources/JWTKit/Docs.docc/index.md @@ -159,6 +159,21 @@ You can create an EdDSA key using its coordinates: @Snippet(path: "jwt-kit/Snippets/JWTKitExamples", slice: EDDSA) +## MLDSA + +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 without until the usage of MLDSA in JWT is finalized. + +> Note: +> MLDSA is only available on macOS 26+. + +Currently, to use MLDSA, you must import JWTKit with the `@_spi(PostQuantum)` flag enabled: + +@Snippet(path: "jwt-kit/Snippets/JWTKitExamples", slice: MLDSA_IMPORT) + +Then you can choose whether to use MLDSA65 or MLDSA87. Use them as follows: + +@Snippet(path: "jwt-kit/Snippets/JWTKitExamples", slice: MLDSA) + ## RSA RSA is an asymmetric algorithm. It uses a public key to verify tokens and a private key to sign them. From 884d3323dc4ad6fe6a26a823b69cf87ca434c055 Mon Sep 17 00:00:00 2001 From: Paul Toffoloni Date: Thu, 16 Oct 2025 10:09:39 +0200 Subject: [PATCH 12/19] Slight improvement --- README.md | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index ed9ac1cb..418f1a98 100644 --- a/README.md +++ b/README.md @@ -58,21 +58,21 @@ JWTKit provides APIs for signing and verifying JSON Web Tokens, as specified by 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: | JWS | Algorithm | Description | -| :-------------: | :-------------: | :----- | -| HS256 | HMAC256 | HMAC with SHA-256 | -| HS384 | HMAC384 | HMAC with SHA-384 | -| HS512 | HMAC512 | HMAC with SHA-512 | -| RS256 | RSA256 | RSASSA-PKCS1-v1_5 with SHA-256 | -| RS384 | RSA384 | RSASSA-PKCS1-v1_5 with SHA-384 | -| RS512 | RSA512 | RSASSA-PKCS1-v1_5 with SHA-512 | -| PS256 | RSA256PSS | RSASSA-PSS with SHA-256 | -| PS384 | RSA384PSS | RSASSA-PSS with SHA-384 | -| PS512 | RSA512PSS | RSASSA-PSS with SHA-512 | -| ES256 | ECDSA256 | ECDSA with curve P-256 and SHA-256 | -| ES384 | ECDSA384 | ECDSA with curve P-384 and SHA-384 | -| ES512 | ECDSA512 | ECDSA with curve P-521 and SHA-512 | -| EdDSA | EdDSA | EdDSA with Ed25519 | -| none | None | No digital signature or MAC | +| :---: | :---: | --- | +| `HS256` | `HMAC256` | HMAC with SHA‑256 | +| `HS384` | `HMAC384` | HMAC with SHA‑384 | +| `HS512` | `HMAC512` | HMAC with SHA‑512 | +| `RS256` | `RSA256` | RSASSA‑PKCS1‑v1_5 + SHA‑256 | +| `RS384` | `RSA384` | RSASSA‑PKCS1‑v1_5 + SHA‑384 | +| `RS512` | `RSA512` | RSASSA‑PKCS1‑v1_5 + SHA‑512 | +| `PS256` | `RSA256PSS` | RSASSA‑PSS + SHA‑256 | +| `PS384` | `RSA384PSS` | RSASSA‑PSS + SHA‑384 | +| `PS512` | `RSA512PSS` | RSASSA‑PSS + SHA‑512 | +| `ES256` | `ECDSA256` | P‑256 + SHA‑256 | +| `ES384` | `ECDSA384` | P‑384 + SHA‑384 | +| `ES512` | `ECDSA512` | P‑521 + SHA‑512 | +| `EdDSA` | `EdDSA` | Ed25519 | +| `none` | `None`| No signature / MAC | ## Vapor @@ -273,7 +273,7 @@ await keys.add(eddsa: privateKey) ## MLDSA -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 without until the usage of MLDSA in JWT is finalized. +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. > [!NOTE]\ > MLDSA requires macOS 26+. From 371e7f4dfaa52c88f9aee024e6d3f449d99e8403 Mon Sep 17 00:00:00 2001 From: Paul Toffoloni <69189821+ptoffy@users.noreply.github.com> Date: Thu, 16 Oct 2025 10:10:16 +0200 Subject: [PATCH 13/19] Update Sources/JWTKit/Docs.docc/index.md Co-authored-by: Francesco Paolo Severino <96546612+fpseverino@users.noreply.github.com> --- Sources/JWTKit/Docs.docc/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/JWTKit/Docs.docc/index.md b/Sources/JWTKit/Docs.docc/index.md index 1410feb2..0a7763f1 100644 --- a/Sources/JWTKit/Docs.docc/index.md +++ b/Sources/JWTKit/Docs.docc/index.md @@ -161,7 +161,7 @@ You can create an EdDSA key using its coordinates: ## MLDSA -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 without until the usage of MLDSA in JWT is finalized. +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. > Note: > MLDSA is only available on macOS 26+. From 35ac5d9cc122db288073423403f5321eb401ebef Mon Sep 17 00:00:00 2001 From: Paul Toffoloni Date: Thu, 16 Oct 2025 10:13:06 +0200 Subject: [PATCH 14/19] Add snippet end --- Snippets/JWTKitExamples.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Snippets/JWTKitExamples.swift b/Snippets/JWTKitExamples.swift index 1815c971..b9242608 100644 --- a/Snippets/JWTKitExamples.swift +++ b/Snippets/JWTKitExamples.swift @@ -121,6 +121,7 @@ do { // Add private key to the key collection await keys.add(mldsa: privateKey) + // snippet.end } extension DataProtocol { From a31e3c29a09470df9a3d63d4994ca67a9d45599a Mon Sep 17 00:00:00 2001 From: Paul Toffoloni Date: Thu, 16 Oct 2025 10:15:26 +0200 Subject: [PATCH 15/19] Actually add the algo to the table --- README.md | 2 ++ Sources/JWTKit/Docs.docc/index.md | 32 ++++++++++++++++--------------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 418f1a98..a791b9f1 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,8 @@ The following algorithms, as defined in [RFC 7518 § 3](https://www.rfc-editor.o | `ES384` | `ECDSA384` | P‑384 + SHA‑384 | | `ES512` | `ECDSA512` | P‑521 + SHA‑512 | | `EdDSA` | `EdDSA` | Ed25519 | +| `MLDSA_65` | `MLDSA65` | MLDSA with parameter set 65 | +| `MLDSA_87` | `MLDSA87` | MLDSA with parameter set 87 | | `none` | `None`| No signature / MAC | ## Vapor diff --git a/Sources/JWTKit/Docs.docc/index.md b/Sources/JWTKit/Docs.docc/index.md index 0a7763f1..40c3dc87 100644 --- a/Sources/JWTKit/Docs.docc/index.md +++ b/Sources/JWTKit/Docs.docc/index.md @@ -33,21 +33,23 @@ JWTKit provides APIs for signing and verifying JSON Web Tokens, as specified by 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: | JWS | Algorithm | Description | -| :-------------: | :-------------: | :----- | -| HS256 | HMAC256 | HMAC with SHA-256 | -| HS384 | HMAC384 | HMAC with SHA-384 | -| HS512 | HMAC512 | HMAC with SHA-512 | -| RS256 | RSA256 | RSASSA-PKCS1-v1_5 with SHA-256 | -| RS384 | RSA384 | RSASSA-PKCS1-v1_5 with SHA-384 | -| RS512 | RSA512 | RSASSA-PKCS1-v1_5 with SHA-512 | -| PS256 | RSA256PSS | RSASSA-PSS with SHA-256 | -| PS384 | RSA384PSS | RSASSA-PSS with SHA-384 | -| PS512 | RSA512PSS | RSASSA-PSS with SHA-512 | -| ES256 | ECDSA256 | ECDSA with curve P-256 and SHA-256 | -| ES384 | ECDSA384 | ECDSA with curve P-384 and SHA-384 | -| ES512 | ECDSA512 | ECDSA with curve P-521 and SHA-512 | -| EdDSA | EdDSA | EdDSA with Ed25519 | -| none | None | No digital signature or MAC | +| :---: | :---: | --- | +| `HS256` | `HMAC256` | HMAC with SHA‑256 | +| `HS384` | `HMAC384` | HMAC with SHA‑384 | +| `HS512` | `HMAC512` | HMAC with SHA‑512 | +| `RS256` | `RSA256` | RSASSA‑PKCS1‑v1_5 + SHA‑256 | +| `RS384` | `RSA384` | RSASSA‑PKCS1‑v1_5 + SHA‑384 | +| `RS512` | `RSA512` | RSASSA‑PKCS1‑v1_5 + SHA‑512 | +| `PS256` | `RSA256PSS` | RSASSA‑PSS + SHA‑256 | +| `PS384` | `RSA384PSS` | RSASSA‑PSS + SHA‑384 | +| `PS512` | `RSA512PSS` | RSASSA‑PSS + SHA‑512 | +| `ES256` | `ECDSA256` | P‑256 + SHA‑256 | +| `ES384` | `ECDSA384` | P‑384 + SHA‑384 | +| `ES512` | `ECDSA512` | P‑521 + SHA‑512 | +| `EdDSA` | `EdDSA` | Ed25519 | +| `MLDSA_65` | `MLDSA65` | MLDSA with parameter set 65 | +| `MLDSA_87` | `MLDSA87` | MLDSA with parameter set 87 | +| `none` | `None`| No signature / MAC | ## Vapor From 10d40e6a69b4a0526645f79911ded9345077e139 Mon Sep 17 00:00:00 2001 From: Paul Toffoloni Date: Thu, 16 Oct 2025 10:16:41 +0200 Subject: [PATCH 16/19] What am I doing --- README.md | 4 ++-- Sources/JWTKit/Docs.docc/index.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a791b9f1..9fc6c8ff 100644 --- a/README.md +++ b/README.md @@ -72,8 +72,8 @@ The following algorithms, as defined in [RFC 7518 § 3](https://www.rfc-editor.o | `ES384` | `ECDSA384` | P‑384 + SHA‑384 | | `ES512` | `ECDSA512` | P‑521 + SHA‑512 | | `EdDSA` | `EdDSA` | Ed25519 | -| `MLDSA_65` | `MLDSA65` | MLDSA with parameter set 65 | -| `MLDSA_87` | `MLDSA87` | MLDSA with parameter set 87 | +| `ML-DSA-65` | `MLDSA65` | MLDSA with parameter set 65 | +| `ML-DSA-87` | `MLDSA87` | MLDSA with parameter set 87 | | `none` | `None`| No signature / MAC | ## Vapor diff --git a/Sources/JWTKit/Docs.docc/index.md b/Sources/JWTKit/Docs.docc/index.md index 40c3dc87..02e30535 100644 --- a/Sources/JWTKit/Docs.docc/index.md +++ b/Sources/JWTKit/Docs.docc/index.md @@ -47,8 +47,8 @@ The following algorithms, as defined in [RFC 7518 § 3](https://www.rfc-editor.o | `ES384` | `ECDSA384` | P‑384 + SHA‑384 | | `ES512` | `ECDSA512` | P‑521 + SHA‑512 | | `EdDSA` | `EdDSA` | Ed25519 | -| `MLDSA_65` | `MLDSA65` | MLDSA with parameter set 65 | -| `MLDSA_87` | `MLDSA87` | MLDSA with parameter set 87 | +| `ML-DSA-65` | `MLDSA65` | MLDSA with parameter set 65 | +| `ML-DSA-87` | `MLDSA87` | MLDSA with parameter set 87 | | `none` | `None`| No signature / MAC | ## Vapor From 35563d90a65a44e5ebbecaac5990f4f431ca88f2 Mon Sep 17 00:00:00 2001 From: Paul Toffoloni Date: Thu, 16 Oct 2025 10:22:30 +0200 Subject: [PATCH 17/19] Update snippets --- README.md | 2 +- Snippets/JWTKitExamples.swift | 23 +++++++++++++---------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 9fc6c8ff..d6d9a5e2 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@


-🔑 JSON Web Token signing and verification (HMAC, RSA, PSS, ECDSA, EdDSA) using SwiftCrypto. +🔑 JSON Web Token signing and verification (HMAC, ECDSA, EdDSA, MLDSA, RSA, PSS) using SwiftCrypto. ### Supported Platforms diff --git a/Snippets/JWTKitExamples.swift b/Snippets/JWTKitExamples.swift index b9242608..58579047 100644 --- a/Snippets/JWTKitExamples.swift +++ b/Snippets/JWTKitExamples.swift @@ -1,7 +1,8 @@ -// snippet.KEY_COLLECTION -import JWTKit // snippet.MLDSA_IMPORT @_spi(PostQuantum) import JWTKit +// snippet.end +// snippet.KEY_COLLECTION +import JWTKit #if !canImport(Darwin) import FoundationEssentials @@ -113,15 +114,17 @@ do { // snippet.end } -do { - // snippet.MLDSA - // Initialize an MLDSA key with its seed - let seedRepresentation = Data("...".utf8) - let privateKey = try MLDSA87PrivateKey(seedRepresentation: seedRepresentation) +if #available(iOS 26, macOS 26, tvOS 26, watchOS 26, *) { + do { + // snippet.MLDSA + // Initialize an MLDSA key with its seed + let seedRepresentation = Data("...".utf8) + let privateKey = try MLDSA87PrivateKey(seedRepresentation: seedRepresentation) - // Add private key to the key collection - await keys.add(mldsa: privateKey) - // snippet.end + // Add private key to the key collection + await keys.add(mldsa: privateKey) + // snippet.end + } } extension DataProtocol { From 515e83dde938705567e9e29901c7923b04f00236 Mon Sep 17 00:00:00 2001 From: Paul Toffoloni Date: Sat, 25 Oct 2025 10:18:29 +0200 Subject: [PATCH 18/19] Add benchmarks --- Benchmarks/Package.swift | 7 +++++ Benchmarks/Signing/Signing.swift | 20 ++++++++----- .../TokenLifecycle/TokenLifecycle.swift | 28 ++++++++++++----- Benchmarks/Utilities/Data+hex.swift | 30 +++++++++++++++++++ Benchmarks/Utilities/Payload.swift | 10 +++++++ Benchmarks/Verifying/Verifying.swift | 21 ++++++++----- 6 files changed, 93 insertions(+), 23 deletions(-) create mode 100644 Benchmarks/Utilities/Data+hex.swift create mode 100644 Benchmarks/Utilities/Payload.swift diff --git a/Benchmarks/Package.swift b/Benchmarks/Package.swift index 52173d31..a4774142 100644 --- a/Benchmarks/Package.swift +++ b/Benchmarks/Package.swift @@ -17,6 +17,7 @@ let package = Package( dependencies: [ .product(name: "Benchmark", package: "package-benchmark"), .product(name: "JWTKit", package: "jwt-kit"), + .target(name: "Utilities"), ], path: "Signing", plugins: [ @@ -28,6 +29,7 @@ let package = Package( dependencies: [ .product(name: "Benchmark", package: "package-benchmark"), .product(name: "JWTKit", package: "jwt-kit"), + .target(name: "Utilities"), ], path: "Verifying", plugins: [ @@ -39,11 +41,16 @@ let package = Package( dependencies: [ .product(name: "Benchmark", package: "package-benchmark"), .product(name: "JWTKit", package: "jwt-kit"), + .target(name: "Utilities"), ], path: "TokenLifecycle", plugins: [ .plugin(name: "BenchmarkPlugin", package: "package-benchmark") ] ), + .target( + name: "Utilities", + path: "Utilities" + ), ] ) diff --git a/Benchmarks/Signing/Signing.swift b/Benchmarks/Signing/Signing.swift index 0b27f915..900ed691 100644 --- a/Benchmarks/Signing/Signing.swift +++ b/Benchmarks/Signing/Signing.swift @@ -1,6 +1,7 @@ import Benchmark import Foundation -import JWTKit +@_spi(PostQuantum) import JWTKit +import Utilities let benchmarks = { Benchmark.defaultConfiguration = .init( @@ -47,14 +48,16 @@ let benchmarks = { _ = try await keyCollection.sign(payload) } } -} - -struct Payload: JWTPayload { - let name: String - let admin: Bool - func verify(using signer: some JWTAlgorithm) async throws { - // nothing to verify + if #available(iOS 26, macOS 26, tvOS 26, watchOS 26, *) { + Benchmark("MLDSA65") { benchmark in + let seed = Data(fromHexEncodedString: mldsa65PrivateKeySeed)! + let key = try MLDSA65PrivateKey(seedRepresentation: seed) + let keyCollection = await JWTKeyCollection().add(mldsa: key) + for _ in benchmark.scaledIterations { + _ = try await keyCollection.sign(payload) + } + } } } @@ -102,3 +105,4 @@ let rsaPrivateKey = """ let eddsaPublicKeyBase64Url = "0ZcEvMCSYqSwR8XIkxOoaYjRQSAO8frTMSCpNbUl4lE" let eddsaPrivateKeyBase64Url = "d1H3_dcg0V3XyAuZW2TE5Z3rhY20M-4YAfYu_HUQd8w" +let mldsa65PrivateKeySeed = "70cefb9aed5b68e018b079da8284b9d5cad5499ed9c265ff73588005d85c225c" diff --git a/Benchmarks/TokenLifecycle/TokenLifecycle.swift b/Benchmarks/TokenLifecycle/TokenLifecycle.swift index 92587e21..b0decb60 100644 --- a/Benchmarks/TokenLifecycle/TokenLifecycle.swift +++ b/Benchmarks/TokenLifecycle/TokenLifecycle.swift @@ -1,6 +1,7 @@ import Benchmark import Foundation -import JWTKit +@_spi(PostQuantum) import JWTKit +import Utilities let benchmarks = { Benchmark.defaultConfiguration = .init( @@ -70,14 +71,27 @@ let benchmarks = { _ = try await keyCollection.verify(token, as: Payload.self) } } -} -struct Payload: JWTPayload { - let name: String - let admin: Bool + if #available(iOS 26, macOS 26, tvOS 26, watchOS 26, *) { + Benchmark("MLDSA65") { benchmark in + for _ in benchmark.scaledIterations { + let key = try MLDSA65PrivateKey(d: eddsaPrivateKeyBase64Url, curve: .ed25519) + let keyCollection = JWTKeyCollection() + await keyCollection.add(eddsa: key) + let token = try await keyCollection.sign(payload) + _ = try await keyCollection.verify(token, as: Payload.self) + } + } - func verify(using signer: some JWTAlgorithm) async throws { - // nothing to verify + Benchmark("MLDSA87") { benchmark in + for _ in benchmark.scaledIterations { + let key = try MLDSA87PrivateKey(d: eddsaPrivateKeyBase64Url, curve: .ed25519) + let keyCollection = JWTKeyCollection() + await keyCollection.add(eddsa: key) + let token = try await keyCollection.sign(payload) + _ = try await keyCollection.verify(token, as: Payload.self) + } + } } } diff --git a/Benchmarks/Utilities/Data+hex.swift b/Benchmarks/Utilities/Data+hex.swift new file mode 100644 index 00000000..7ebd5eb4 --- /dev/null +++ b/Benchmarks/Utilities/Data+hex.swift @@ -0,0 +1,30 @@ +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +extension Data { + package init?(fromHexEncodedString string: String) { + func decodeNibble(u: UInt8) -> UInt8? { + switch u { + case 0x30...0x39: u - 0x30 + case 0x41...0x46: u - 0x41 + 10 + case 0x61...0x66: u - 0x61 + 10 + default: nil + } + } + + self.init(capacity: string.utf8.count / 2) + + var iter = string.utf8.makeIterator() + while let c1 = iter.next() { + guard + let val1 = decodeNibble(u: c1), + let c2 = iter.next(), + let val2 = decodeNibble(u: c2) + else { return nil } + self.append(val1 << 4 + val2) + } + } +} diff --git a/Benchmarks/Utilities/Payload.swift b/Benchmarks/Utilities/Payload.swift new file mode 100644 index 00000000..a56a29ce --- /dev/null +++ b/Benchmarks/Utilities/Payload.swift @@ -0,0 +1,10 @@ +import JWTKit + +package struct Payload: JWTPayload { + package let name: String + package let admin: Bool + + func verify(using signer: some JWTAlgorithm) async throws { + // nothing to verify + } +} diff --git a/Benchmarks/Verifying/Verifying.swift b/Benchmarks/Verifying/Verifying.swift index 516f5680..7df30884 100644 --- a/Benchmarks/Verifying/Verifying.swift +++ b/Benchmarks/Verifying/Verifying.swift @@ -1,6 +1,7 @@ import Benchmark import Foundation -import JWTKit +@_spi(PostQuantum) import JWTKit +import Utilities let benchmarks = { Benchmark.defaultConfiguration = .init( @@ -69,13 +70,17 @@ let benchmarks = { _ = try await keyCollection.verify(token, as: Payload.self) } } -} - -struct Payload: JWTPayload { - let name: String - let admin: Bool - func verify(using signer: some JWTAlgorithm) async throws { - // nothing to verify + if #available(iOS 26, macOS 26, tvOS 26, watchOS 26, *) { + Benchmark("MLDSA") { benchmark in + let mldsa65PrivateKeySeed = Data(fromHexEncodedString: "70cefb9aed5b68e018b079da8284b9d5cad5499ed9c265ff73588005d85c225c") + let keyCollection = try await JWTKeyCollection() + .add(mldsa: MLDSA65PrivateKey(seedRepresentation: mldsa65PrivateKeySeed)) + let token = + "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" + for _ in benchmark.scaledIterations { + _ = try await keyCollection.verify(token, as: Payload.self) + } + } } } From 1e4642c7ca1542180adb3f09efd6041afa109226 Mon Sep 17 00:00:00 2001 From: Paul Toffoloni Date: Sat, 25 Oct 2025 10:46:10 +0200 Subject: [PATCH 19/19] Fix --- Benchmarks/Utilities/Payload.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Benchmarks/Utilities/Payload.swift b/Benchmarks/Utilities/Payload.swift index a56a29ce..501db542 100644 --- a/Benchmarks/Utilities/Payload.swift +++ b/Benchmarks/Utilities/Payload.swift @@ -4,7 +4,7 @@ package struct Payload: JWTPayload { package let name: String package let admin: Bool - func verify(using signer: some JWTAlgorithm) async throws { + package func verify(using signer: some JWTAlgorithm) async throws { // nothing to verify } }