Repository: vapor/jwt-kit (swift)

Issue:
JWTKit 5.0.0-RC.1 support for Curve25519 types
JWTKit's RC uses its own types to represent common cryptographic types, and then convert into Swift-Crypto types. It would be much more practical, especially when used with other libraries, to use the swift-crypto types themselves. This way we can plug in the private keys we already have, without going through a base64 conversion step and back.

Pull request:
Fix #174

Patch:
diff --git a/Snippets/JWTKitExamples.swift b/Snippets/JWTKitExamples.swift
index 87c9613c..dc89fb69 100644
--- a/Snippets/JWTKitExamples.swift
+++ b/Snippets/JWTKitExamples.swift
@@ -10,7 +10,7 @@ struct ExamplePayload: JWTPayload {
     var exp: ExpirationClaim
     var admin: BoolClaim
 
-    func verify(using key: some JWTAlgorithm) throws {
+    func verify(using _: some JWTAlgorithm) throws {
         try self.exp.verifyNotExpired()
     }
 }
@@ -33,8 +33,7 @@ let key = try ES256PublicKey(pem: ecdsaPublicKey)
 await keys.add(ecdsa: key)
 
 // snippet.end
-do
-{
+do {
     // Create a new instance of our JWTPayload
     let payload = ExamplePayload(
         sub: "vapor",
@@ -49,8 +48,7 @@ do
     // snippet.end
 }
 
-do
-{
+do {
     // snippet.VERIFYING
     let exampleJWT = """
     eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ2YXBvciIsImV4cCI6NjQwOTIyMTEyMDAsImFkbWluIjp0cnVlfQ.lS5lpwfRNSZDvpGQk6x5JI1g40gkYCOWqbc3J_ghowo
@@ -63,14 +61,13 @@ do
     // snippet.end
 }
 
-do
-{
+do {
     // snippet.EDDSA
     // Initialize an EdDSA key with public PEM
     let publicKey = try EdDSA.PublicKey(x: "...", curve: .ed25519)
 
     // Initialize an EdDSA key with private PEM
-    let privateKey = try EdDSA.PrivateKey(x: "...", d: "...", curve: .ed25519)
+    let privateKey = try EdDSA.PrivateKey(d: "...", curve: .ed25519)
 
     // Add public key to the key collection
     await keys.add(eddsa: publicKey)
@@ -80,8 +77,7 @@ do
     // snippet.end
 }
 
-do
-{
+do {
     // snippet.RSA
     // Initialize an RSA key with components.
     let key = try Insecure.RSA.PrivateKey(
@@ -92,8 +88,8 @@ do
     // snippet.end
     _ = key
 }
-do
-{
+
+do {
     // snippet.RSA_FROM_PEM
     let rsaPublicKey = "-----BEGIN PUBLIC KEY----- ... -----END PUBLIC KEY-----"
 
@@ -136,9 +132,9 @@ struct CustomSerializer: JWTSerializer {
 
     func serialize(_ payload: some JWTPayload, header: JWTHeader) throws -> Data {
         if header.b64?.asBool == true {
-            try Data(jsonEncoder.encode(payload).base64URLEncodedBytes())
+            try Data(self.jsonEncoder.encode(payload).base64URLEncodedBytes())
         } else {
-            try jsonEncoder.encode(payload)
+            try self.jsonEncoder.encode(payload)
         }
     }
 }
@@ -146,15 +142,15 @@ struct CustomSerializer: JWTSerializer {
 struct CustomParser: JWTParser {
     var jsonDecoder: JWTJSONDecoder = .defaultForJWT
 
-    func parse<Payload>(_ token: some DataProtocol, as: Payload.Type) throws -> (header: JWTHeader, payload: Payload, signature: Data) where Payload: JWTPayload {
+    func parse<Payload>(_ token: some DataProtocol, as _: Payload.Type) throws -> (header: JWTHeader, payload: Payload, signature: Data) where Payload: JWTPayload {
         let (encodedHeader, encodedPayload, encodedSignature) = try getTokenParts(token)
 
         let header = try jsonDecoder.decode(JWTHeader.self, from: .init(encodedHeader.base64URLDecodedBytes()))
 
         let payload = if header.b64?.asBool ?? true {
-            try jsonDecoder.decode(Payload.self, from: .init(encodedPayload.base64URLDecodedBytes()))
+            try self.jsonDecoder.decode(Payload.self, from: .init(encodedPayload.base64URLDecodedBytes()))
         } else {
-            try jsonDecoder.decode(Payload.self, from: .init(encodedPayload))
+            try self.jsonDecoder.decode(Payload.self, from: .init(encodedPayload))
         }
 
         let signature = Data(encodedSignature.base64URLDecodedBytes())
@@ -162,10 +158,10 @@ struct CustomParser: JWTParser {
         return (header: header, payload: payload, signature: signature)
     }
 }
+
 // snippet.end
 
-do
-{
+do {
     // snippet.CUSTOM_SIGNING
     let keyCollection = await JWTKeyCollection()
         .add(hmac: "secret", digestAlgorithm: .sha256, parser: CustomParser(), serializer: CustomSerializer())
@@ -176,8 +172,8 @@ do
     // snippet.end
     _ = token
 }
-do
-{
+
+do {
     // snippet.CUSTOM_ENCODING
     let encoder = JSONEncoder(); encoder.dateEncodingStrategy = .iso8601
     let decoder = JSONDecoder(); decoder.dateDecodingStrategy = .iso8601
diff --git a/Sources/JWTKit/EdDSA/EdDSA.swift b/Sources/JWTKit/EdDSA/EdDSA.swift
index e47ea0c9..4e9ca798 100644
--- a/Sources/JWTKit/EdDSA/EdDSA.swift
+++ b/Sources/JWTKit/EdDSA/EdDSA.swift
@@ -13,23 +13,19 @@ public extension EdDSA {
     /// A struct representing a public key used in EdDSA (Edwards-curve Digital Signature Algorithm).
     ///
     /// In JWT, EdDSA public keys are represented as a single x-coordinate and are used for verifying signatures.
-    /// Currently, only the ``Curve/ed25519`` curve is supported.
+    /// Currently, only the ``EdDSACurve/ed25519`` curve is supported.
     struct PublicKey: EdDSAKey {
-        // https://www.rfc-editor.org/rfc/rfc8037.html#appendix-A.2
-        struct OctetKeyPair: Sendable {
-            let x: Data
-
-            init(x: Data) {
-                self.x = x
-            }
-        }
-
-        let keyPair: OctetKeyPair
-
+        let backing: Curve25519.Signing.PublicKey
         let curve: EdDSACurve
 
-        var rawRepresentation: Data {
-            keyPair.x
+        /// Creates an ``EdDSA.PublicKey`` instance using the provided public key.
+        ///
+        /// This init constructs an ``EdDSA.PublicKey`` based on the corresponding SwiftCrypto
+        /// ``Curve25519.Signing.PublicKey``.
+        /// - Parameter backing: The SwiftCrypto ``Curve25519.Signing.PublicKey``
+        public init(backing: Curve25519.Signing.PublicKey) {
+            self.backing = backing
+            self.curve = .ed25519
         }
 
         /// Creates an ``EdDSA.PublicKey`` instance using the public key x-coordinate and specified curve.
@@ -39,25 +35,28 @@ public extension EdDSA {
         ///
         /// - Parameters:
         ///   - x: A `String` representing the x-coordinate of the public key. This should be a Base64 URL encoded string.
-        ///   - curve: The ``Curve`` representing the elliptic curve used for the EdDSA public key.
+        ///   - curve: The ``EdDSACurve`` representing the elliptic curve used for the EdDSA public key.
         ///
         /// - Throws:
         ///   - ``EdDSAError/publicKeyMissing`` if the x-coordinate data is missing or cannot be properly converted.
         public init(x: String, curve: EdDSACurve) throws {
-            let xData = Data(x.utf8)
-            
-            guard !xData.isEmpty else {
+            guard
+                let xData = x.base64URLDecodedData(),
+                !xData.isEmpty
+            else {
                 throw EdDSAError.publicKeyMissing
             }
 
-            self.init(keyPair: .init(
-                x: Data(xData.base64URLDecodedBytes())
-            ), curve: curve)
+            let key = switch curve.backing {
+            case .ed25519:
+                try Curve25519.Signing.PublicKey(rawRepresentation: xData)
+            }
+
+            self.init(backing: key)
         }
 
-        fileprivate init(keyPair: OctetKeyPair, curve: EdDSACurve) {
-            self.keyPair = keyPair
-            self.curve = curve
+        var rawRepresentation: Data {
+            self.backing.rawRepresentation
         }
     }
 }
@@ -68,29 +67,9 @@ public extension EdDSA {
     /// In JWT, EdDSA private keys are represented as a pair of x-coordinate and private key (d) and are used for signing.
     /// Currently, only the ``Curve/ed25519`` curve is supported.
     struct PrivateKey: EdDSAKey {
-        // https://www.rfc-editor.org/rfc/rfc8037.html#appendix-A.1
-        struct OctetKeyPair: Sendable {
-            let x: Data
-            let d: Data
-
-            init(x: Data, d: Data) {
-                self.x = x
-                self.d = d
-            }
-        }
-
-        let keyPair: OctetKeyPair
-
+        let backing: Curve25519.Signing.PrivateKey
         let curve: EdDSACurve
 
-        var publicKey: PublicKey {
-            .init(keyPair: .init(x: keyPair.x), curve: curve)
-        }
-
-        var rawRepresentation: Data {
-            keyPair.d
-        }
-
         /// Generates a new ``EdDSAKey`` instance with both public and private key components.
         ///
         /// This method generates a new key pair suitable for signing and verifying signatures.
@@ -98,51 +77,62 @@ public extension EdDSA {
         ///
         /// - Parameter curve: The curve to be used for key generation.
         /// - Throws: An error if key generation fails.
-        /// - Returns: A new ``EdDSAKey`` instance with a freshly generated key pair.
+        /// - Returns: A new ``EdDSA.PrivateKey`` instance with a freshly generated key pair.
         public init(curve: EdDSACurve = .ed25519) throws {
-            switch curve.backing {
+            let key = switch curve.backing {
             case .ed25519:
-                let keyPair = Curve25519.Signing.PrivateKey()
-                self.init(keyPair: .init(
-                    x: keyPair.publicKey.rawRepresentation,
-                    d: keyPair.rawRepresentation
-                ), curve: curve)
+                Curve25519.Signing.PrivateKey()
             }
+
+            self.init(backing: key)
+        }
+
+        /// Creates an ``EdDSA.PrivateKey`` instance using the provided private key.
+        ///
+        /// This init constructs an ``EdDSA.PrivateKey`` based on the corresponding SwiftCrypto
+        /// ``Curve25519.Signing.PrivateKey``.
+        /// - Parameter privateKey: The SwiftCrypto ``Curve25519.Signing.PrivateKey``
+        public init(backing: Curve25519.Signing.PrivateKey) {
+            self.backing = backing
+            self.curve = .ed25519
         }
 
         /// Creates an ``EdDSA.PrivateKey`` instance using both the public and private key components along with the specified curve.
         ///
-        /// This init constructs an ``EdDSA.PrivateKey`` from the provided x-coordinate of the public key and the private key (d).
-        /// Both `x` and `d` are expected to be Base64 URL encoded strings.
+        /// This init constructs an ``EdDSA.PrivateKey`` from the provided private key (d).
+        /// `d` is expected to be a Base64 URL encoded string.
         ///
         /// - Parameters:
-        ///   - x: A `String` representing the x-coordinate of the public key, encoded in Base64 URL format.
         ///   - d: A `String` representing the private key, encoded in Base64 URL format.
-        ///   - curve: The ``Curve`` representing the elliptic curve used for the EdDSA key.
+        ///   - curve: The ``EdDSACurve`` representing the elliptic curve used for the EdDSA key.
         ///
         /// - Throws:
-        ///   - ``EdDSAError/publicKeyMissing`` if the x-coordinate data is missing or cannot be properly converted.
         ///   - ``EdDSAError/privateKeyMissing`` if the private key data is missing or cannot be properly converted.
-        public init(x: String, d: String, curve: EdDSACurve) throws {
-            let xData = Data(x.utf8)
-            guard !xData.isEmpty else {
-                throw EdDSAError.publicKeyMissing
-            }
-            
-            let dData = Data(d.utf8)
-            guard !dData.isEmpty else {
+        public init(d: String, curve: EdDSACurve) throws {
+            guard
+                let dData = d.base64URLDecodedData(),
+                !dData.isEmpty
+            else {
                 throw EdDSAError.privateKeyMissing
             }
 
-            self.init(keyPair: .init(
-                x: Data(xData.base64URLDecodedBytes()),
-                d: Data(dData.base64URLDecodedBytes())
-            ), curve: curve)
+            let key = switch curve.backing {
+            case .ed25519:
+                try Curve25519.Signing.PrivateKey(rawRepresentation: dData)
+            }
+
+            self.init(backing: key)
         }
 
-        private init(keyPair: OctetKeyPair, curve: EdDSACurve) {
-            self.keyPair = keyPair
-            self.curve = curve
+        var publicKey: PublicKey {
+            .init(backing: self.backing.publicKey)
+        }
+
+        var rawRepresentation: Data {
+            self.backing.rawRepresentation
         }
     }
 }
+
+extension Curve25519.Signing.PublicKey: @unchecked Sendable {}
+extension Curve25519.Signing.PrivateKey: @unchecked Sendable {}
diff --git a/Sources/JWTKit/JWK/JWKSigner.swift b/Sources/JWTKit/JWK/JWKSigner.swift
index 3457b0d5..09e080e0 100644
--- a/Sources/JWTKit/JWK/JWKSigner.swift
+++ b/Sources/JWTKit/JWK/JWKSigner.swift
@@ -1,9 +1,8 @@
 actor JWKSigner: Sendable {
     let jwk: JWK
-    private(set) var signer: JWTSigner?
-
     let parser: any JWTParser
     let serializer: any JWTSerializer
+    private(set) var signer: JWTSigner?
 
     init(
         jwk: JWK,
@@ -113,8 +112,8 @@ extension JWK {
             let algorithm = alg ?? self.algorithm
 
             switch (algorithm, self.x, self.privateExponent) {
-            case let (.eddsa, .some(x), .some(d)):
-                let key = try EdDSA.PrivateKey(x: x, d: d, curve: curve)
+            case let (.eddsa, .some(_), .some(d)):
+                let key = try EdDSA.PrivateKey(d: d, curve: curve)
                 return EdDSASigner(key: key)
 
             case let (.eddsa, .some(x), .none):


Test patch:
diff --git a/Tests/JWTKitTests/EdDSATests.swift b/Tests/JWTKitTests/EdDSATests.swift
index aaef50b0..1629a086 100644
--- a/Tests/JWTKitTests/EdDSATests.swift
+++ b/Tests/JWTKitTests/EdDSATests.swift
@@ -20,7 +20,7 @@ final class EdDSATests: XCTestCase {
     func testEdDSAPublicPrivate() async throws {
         let keys = try await JWTKeyCollection()
             .add(eddsa: EdDSA.PublicKey(x: eddsaPublicKeyBase64, curve: .ed25519), kid: "public")
-            .add(eddsa: EdDSA.PrivateKey(x: eddsaPublicKeyBase64, d: eddsaPrivateKeyBase64, curve: .ed25519), kid: "private")
+            .add(eddsa: EdDSA.PrivateKey(d: eddsaPrivateKeyBase64, curve: .ed25519), kid: "private")
 
         let payload = TestPayload(
             sub: "vapor",
@@ -47,7 +47,7 @@ final class EdDSATests: XCTestCase {
 
         // sign jwt
         let keyCollection = try await JWTKeyCollection()
-            .add(eddsa: EdDSA.PrivateKey(x: x, d: d, curve: .ed25519), kid: "vapor")
+            .add(eddsa: EdDSA.PrivateKey(d: d, curve: .ed25519), kid: "vapor")
 
         let jwt = try await keyCollection.sign(Foo(bar: 42))
 
@@ -84,7 +84,7 @@ final class EdDSATests: XCTestCase {
 
         // sign jwt
         let keyCollection = try await JWTKeyCollection()
-            .add(eddsa: EdDSA.PrivateKey(x: x, d: d, curve: .ed25519), kid: "vapor")
+            .add(eddsa: EdDSA.PrivateKey(d: d, curve: .ed25519), kid: "vapor")
 
         let jwt = try await keyCollection.sign(Foo(bar: 42))
 
@@ -121,7 +121,7 @@ final class EdDSATests: XCTestCase {
 
         // sign jwt
         let keyCollection = try await JWTKeyCollection()
-            .add(eddsa: EdDSA.PrivateKey(x: x, d: d, curve: .ed25519), kid: "vapor")
+            .add(eddsa: EdDSA.PrivateKey(d: d, curve: .ed25519), kid: "vapor")
 
         let jwt = try await keyCollection.sign(Foo(bar: 42), kid: "vapor")