Repository: vapor/jwt-kit (swift)

Issue:
EdDSA Support
Please consider supporting Ed25519 signature.

Pull request:
This implements support for Octet Key Pairs ([RFC8037](https://www.rfc-editor.org/rfc/rfc8037.html#section-2)) with the EdDSA algorithm and ED25519 curves. It closes #50.

Patch:
diff --git a/Sources/JWTKit/ECDSA/ECDSAKey.swift b/Sources/JWTKit/ECDSA/ECDSAKey.swift
index f02cf575..e8b5d0e3 100644
--- a/Sources/JWTKit/ECDSA/ECDSAKey.swift
+++ b/Sources/JWTKit/ECDSA/ECDSAKey.swift
@@ -1,34 +1,13 @@
 @_implementationOnly import CJWTKitBoringSSL
 
 public final class ECDSAKey: OpenSSLKey {
+    
     public enum Curve: String, Codable {
         case p256 = "P-256"
         case p384 = "P-384"
         case p521 = "P-521"
-
-        var cName: Int32 {
-            switch self {
-            case .p256:
-                return NID_X9_62_prime256v1
-            case .p384:
-                return NID_secp384r1
-            case .p521:
-                return NID_secp521r1
-            }
-        }
-      
-        init?(cName: Int32) {
-            switch cName {
-            case NID_X9_62_prime256v1:
-                self = .p256
-            case NID_secp384r1:
-                self = .p384
-            case NID_secp521r1:
-                self = .p521
-            default:
-                return nil
-            }
-        }
+        case ed25519 = "Ed25519"
+        case ed448 = "Ed448"
     }
     
     public static func generate(curve: Curve = .p521) throws -> ECDSAKey {
@@ -129,3 +108,37 @@ public final class ECDSAKey: OpenSSLKey {
         public let y: String
     }
 }
+
+extension ECDSAKey.Curve {
+    var cName: Int32 {
+        switch self {
+        case .p256:
+            return NID_X9_62_prime256v1
+        case .p384:
+            return NID_secp384r1
+        case .p521:
+            return NID_secp521r1
+        case .ed25519:
+            return NID_ED25519
+        case .ed448:
+            return NID_ED448
+        }
+    }
+  
+    init?(cName: Int32) {
+        switch cName {
+        case NID_X9_62_prime256v1:
+            self = .p256
+        case NID_secp384r1:
+            self = .p384
+        case NID_secp521r1:
+            self = .p521
+        case NID_ED25519:
+            self = .ed25519
+        case NID_ED448:
+            self = .ed448
+        default:
+            return nil
+        }
+    }
+}
diff --git a/Sources/JWTKit/EdDSA/EdDSAError.swift b/Sources/JWTKit/EdDSA/EdDSAError.swift
new file mode 100644
index 00000000..bcc267c1
--- /dev/null
+++ b/Sources/JWTKit/EdDSA/EdDSAError.swift
@@ -0,0 +1,5 @@
+internal enum EdDSAError: Error {
+    case publicAndPrivateKeyMissing
+    case privateKeyMissing
+    case publicKeyMissing
+}
diff --git a/Sources/JWTKit/EdDSA/EdDSAKey.swift b/Sources/JWTKit/EdDSA/EdDSAKey.swift
new file mode 100644
index 00000000..acb0ef9f
--- /dev/null
+++ b/Sources/JWTKit/EdDSA/EdDSAKey.swift
@@ -0,0 +1,70 @@
+import Foundation
+import Crypto
+
+public struct EdDSAKey {
+            
+    public enum Curve: String, Codable {
+        case ed25519 = "Ed25519"
+    }
+    
+    let keyPair: OctetKeyPair
+    var publicKey: Data {
+        keyPair.publicKey
+    }
+    var privateKey: Data? {
+        keyPair.privateKey
+    }
+    let curve: Curve
+         
+    public static func `public`(x: String, curve: Curve) throws -> EdDSAKey {
+        
+        guard let xData = x.data(using: .utf8), !xData.isEmpty else {
+            throw EdDSAError.publicKeyMissing
+        }
+        
+        return try EdDSAKey(
+            keyPair: .`public`(
+                x: Data(xData.base64URLDecodedBytes())
+            ),
+            curve: curve
+        )
+    }
+    
+    public static func `private`(x: String, d: String, curve: Curve) throws -> EdDSAKey {
+        guard let xData = x.data(using: .utf8), !xData.isEmpty else {
+            throw EdDSAError.publicKeyMissing
+        }
+        
+        guard let dData = d.data(using: .utf8), !dData.isEmpty else {
+            throw EdDSAError.privateKeyMissing
+        }
+        
+        
+        return try EdDSAKey(
+            keyPair: .`private`(
+                x: Data(xData.base64URLDecodedBytes()),
+                d: Data(dData.base64URLDecodedBytes())
+            ),
+            curve: curve
+        )
+    }
+    
+    init(keyPair: OctetKeyPair, curve: Curve) throws {
+        self.keyPair = keyPair
+        self.curve = curve
+    }
+    
+    public static func generate(curve: Curve) throws -> EdDSAKey {
+        switch curve {
+        case .ed25519:            
+            let key = Curve25519.Signing.PrivateKey()
+            return try .init(
+                keyPair: .`private`(
+                    x: key.publicKey.rawRepresentation,
+                    d: key.rawRepresentation),
+                curve: curve
+            )
+        }
+    }
+}
+
diff --git a/Sources/JWTKit/EdDSA/EdDSASigner.swift b/Sources/JWTKit/EdDSA/EdDSASigner.swift
new file mode 100644
index 00000000..8e0fbee5
--- /dev/null
+++ b/Sources/JWTKit/EdDSA/EdDSASigner.swift
@@ -0,0 +1,33 @@
+import Crypto
+
+internal struct EdDSASigner: JWTAlgorithm {
+    let key: EdDSAKey
+    let name = "EdDSA"
+    
+    func sign<Plaintext>(_ plaintext: Plaintext) throws -> [UInt8] where Plaintext : DataProtocol {                
+        guard let privateKey = key.privateKey else {
+            throw  JWTError.signingAlgorithmFailure(EdDSAError.privateKeyMissing)
+        }
+        
+        switch key.curve {
+        case .ed25519:
+            return try Curve25519.Signing.PrivateKey(
+                rawRepresentation: privateKey
+            ).signature(
+                for: plaintext
+            ).copyBytes()
+        }
+    }
+    
+    func verify<Signature, Plaintext>(_ signature: Signature, signs plaintext: Plaintext) throws -> Bool where Signature : DataProtocol, Plaintext : DataProtocol {
+        switch key.curve {
+        case .ed25519:
+            return try Curve25519.Signing.PublicKey(
+                rawRepresentation: key.publicKey
+            ).isValidSignature(
+                signature,
+                for: plaintext
+            )
+        }
+    }
+}
diff --git a/Sources/JWTKit/EdDSA/JWTSigner+EdDSA.swift b/Sources/JWTKit/EdDSA/JWTSigner+EdDSA.swift
new file mode 100644
index 00000000..eb196c3b
--- /dev/null
+++ b/Sources/JWTKit/EdDSA/JWTSigner+EdDSA.swift
@@ -0,0 +1,8 @@
+import Foundation
+import Crypto
+
+extension JWTSigner {
+    public static func eddsa(_ key: EdDSAKey) -> JWTSigner {
+        .init(algorithm: EdDSASigner(key: key))
+    }
+}
diff --git a/Sources/JWTKit/EdDSA/OctetKeyPair.swift b/Sources/JWTKit/EdDSA/OctetKeyPair.swift
new file mode 100644
index 00000000..29e4c8f4
--- /dev/null
+++ b/Sources/JWTKit/EdDSA/OctetKeyPair.swift
@@ -0,0 +1,32 @@
+import Foundation
+
+// https://www.rfc-editor.org/rfc/rfc8037.html#appendix-A
+enum OctetKeyPair {
+    case `public`(x: Data)
+    case `private`(x: Data, d: Data)
+    
+    init(x: Data, d: Data) throws {
+        self = .`private`(x: x, d: d)
+    }
+    
+    init(x: Data) throws {
+        self = .`public`(x: x)
+    }
+    
+    var publicKey: Data {
+        switch self {
+            case .`public`(let x), .`private`(let x, _):
+                return x
+        }
+    }
+    
+    var privateKey: Data? {
+        switch self {
+            case .`private`(_ , let d):
+                return d
+                
+            case .`public`:
+                return nil
+        }
+    }
+}
diff --git a/Sources/JWTKit/JWK/JWK.swift b/Sources/JWTKit/JWK/JWK.swift
index a7e3cecc..c388f0a0 100644
--- a/Sources/JWTKit/JWK/JWK.swift
+++ b/Sources/JWTKit/JWK/JWK.swift
@@ -5,12 +5,23 @@ import class Foundation.JSONDecoder
 ///
 /// Read specification (RFC 7517) https://tools.ietf.org/html/rfc7517.
 public struct JWK: Codable {
+    
+    public enum Curve: String, Codable {
+        case p256 = "P-256"
+        case p384 = "P-384"
+        case p521 = "P-521"
+        case ed25519 = "Ed25519"
+        case ed448 = "Ed448"
+    }
+    
     /// Supported `kty` key types.
     public enum KeyType: String, Codable {
         /// RSA
         case rsa = "RSA"
         /// ECDSA
         case ecdsa = "EC"
+        /// Octet Key Pair
+        case octetKeyPair = "OKP"
     }
      
     /// The `kty` (key type) parameter identifies the cryptographic algorithm
@@ -32,6 +43,8 @@ public struct JWK: Codable {
         case es384 = "ES384"
         /// EC with SHA512
         case es512 = "ES512"
+        /// EdDSA
+        case eddsa = "EdDSA"
     }
      
      /// The `alg` (algorithm) parameter identifies the algorithm intended for
@@ -69,7 +82,7 @@ public struct JWK: Codable {
 
     public var y: String?
     
-    public var curve: ECDSAKey.Curve?
+    public var curve: Curve?
         
     private enum CodingKeys: String, CodingKey {
         case keyType = "kty"
@@ -92,7 +105,11 @@ public struct JWK: Codable {
     }
     
     public static func ecdsa(_ algorithm: Algorithm?, identifier: JWKIdentifier?, x: String?, y: String?, curve: ECDSAKey.Curve?, privateKey: String? = nil) -> JWK {
-        return JWK(keyType: .ecdsa, algorithm: algorithm, keyIdentifier: identifier, d: privateKey, x: x, y: y, curve: curve)
+        return JWK(keyType: .ecdsa, algorithm: algorithm, keyIdentifier: identifier, d: privateKey, x: x, y: y, curve: curve.flatMap { Curve(rawValue: $0.rawValue) })
+    }
+    
+    public static func octetKeyPair(_ algorithm: Algorithm?, identifier: JWKIdentifier?, x: String?, y: String?, curve: EdDSAKey.Curve?, privateKey: String? = nil) -> JWK {
+        return JWK(keyType: .octetKeyPair, algorithm: algorithm, keyIdentifier: identifier, d: privateKey, x: x, curve: curve.flatMap { Curve(rawValue: $0.rawValue) })
     }
     
     private init(
@@ -104,7 +121,7 @@ public struct JWK: Codable {
         d: String? = nil,
         x: String? = nil,
         y: String? = nil,
-        curve: ECDSAKey.Curve? = nil
+        curve: Curve? = nil
     ) {
         self.keyType = keyType
         self.algorithm = algorithm
diff --git a/Sources/JWTKit/JWTSigners.swift b/Sources/JWTKit/JWTSigners.swift
index 592432c1..4b94647a 100644
--- a/Sources/JWTKit/JWTSigners.swift
+++ b/Sources/JWTKit/JWTSigners.swift
@@ -200,7 +200,7 @@ private struct JWKSigner {
             
             let curve: ECDSAKey.Curve
             
-            if let jwkCurve = self.jwk.curve {
+            if let jwkCurve = (self.jwk.curve.flatMap { ECDSAKey.Curve(rawValue: $0.rawValue) }) {
                 curve = jwkCurve
             } else {
                 switch algorithm {
@@ -229,6 +229,28 @@ private struct JWKSigner {
             default:
                 return nil
             }
+
+        case .octetKeyPair:
+            guard let algorithm = algorithm ?? self.jwk.algorithm else {
+                return nil
+            }
+                
+            guard let curve = self.jwk.curve.flatMap({ EdDSAKey.Curve(rawValue: $0.rawValue) }) else {
+                return nil
+            }
+            
+            switch (algorithm, self.jwk.x, jwk.privateExponent) {
+            case (.eddsa, .some(let x), .some(let d)):
+                let key = try? EdDSAKey.private(x: x, d: d, curve: curve)
+                return key.map(JWTSigner.eddsa(_:))
+                
+            case (.eddsa, .some(let x), .none):
+                let key = try? EdDSAKey.public(x: x, curve: curve)
+                return key.map(JWTSigner.eddsa(_:))
+                
+            default:
+                return nil
+            }
         }
     }
 }


Test patch:
diff --git a/Tests/JWTKitTests/JWTKitTests.swift b/Tests/JWTKitTests/JWTKitTests.swift
index b0533e0d..a34e89f8 100644
--- a/Tests/JWTKitTests/JWTKitTests.swift
+++ b/Tests/JWTKitTests/JWTKitTests.swift
@@ -670,6 +670,152 @@ class JWTKitTests: XCTestCase {
             .verify(firebaseJWT, as: FirebasePayload.self)
         XCTAssertEqual(payload.userID, "y8wiKThXGKM88xxrQWDZzKnBuqv2")
     }
+    
+    // MARK: - EdDSA
+    
+    func testEdDSAGenerate() throws {
+        let payload = TestPayload(
+            sub: "vapor",
+            name: "Foo",
+            admin: false,
+            exp: .init(value: .init(timeIntervalSince1970: 2_000_000_000))
+        )
+        
+        let signer = try JWTSigner.eddsa(.generate(curve: .ed25519))
+        let token = try signer.sign(payload)
+        try XCTAssertEqual(signer.verify(token, as: TestPayload.self), payload)
+    }
+    
+    func testEdDSAPublicPrivate() throws {
+        
+        let publicSigner = try JWTSigner.eddsa(
+            .public(x: eddsaPublicKeyBase64, curve: .ed25519)
+        )
+        let privateSigner = try JWTSigner.eddsa(
+            .private(x: eddsaPublicKeyBase64, d: eddsaPrivateKeyBae64, curve: .ed25519)
+        )
+
+        let payload = TestPayload(
+            sub: "vapor",
+            name: "Foo",
+            admin: false,
+            exp: .init(value: .init(timeIntervalSince1970: 2_000_000_000))
+        )
+        for _ in 0..<1_000 {
+            let token = try privateSigner.sign(payload)
+            // test public signer decoding
+            try XCTAssertEqual(publicSigner.verify(token, as: TestPayload.self), payload)
+        }
+    }
+        
+    func testVerifyingEdDSAKeyUsingJWK() throws {
+        struct Foo: JWTPayload {
+            var bar: Int
+            func verify(using signer: JWTSigner) throws { }
+        }
+                
+        // ecdsa key in base64 format
+        let x = eddsaPublicKeyBase64
+        let d = eddsaPrivateKeyBae64
+        
+        // sign jwt
+        let signer = try JWTSigner.eddsa(.private(x: x, d: d, curve: .ed25519))
+        let jwt = try signer.sign(Foo(bar: 42), kid: "vapor")
+
+        // verify using jwks
+        let jwksString = """
+        {
+            "keys": [
+                {
+                    "kty": "OKP",
+                    "crv": "Ed25519",
+                    "use": "sig",
+                    "kid": "vapor",
+                    "x": "\(x)",
+                    "d": "\(d)"
+                 }
+            ]
+        }
+        """
+
+        let signers = JWTSigners()
+        try signers.use(jwksJSON: jwksString)
+        let foo = try signers.verify(jwt, as: Foo.self)
+        XCTAssertEqual(foo.bar, 42)
+    }
+    
+    func testVerifyingEdDSAKeyUsingJWKBase64URL() throws {
+        struct Foo: JWTPayload {
+            var bar: Int
+            func verify(using signer: JWTSigner) throws { }
+        }
+                
+        // eddsa key in base64url format
+        let x = eddsaPublicKeyBase64Url
+        let d = eddsaPrivateKeyBae64Url
+
+        // sign jwt
+        let signer = try JWTSigner.eddsa(.private(x: x, d: d, curve: .ed25519))
+        let jwt = try signer.sign(Foo(bar: 42), kid: "vapor")
+
+        // verify using jwks without alg
+        let jwksString = """
+        {
+            "keys": [
+                {
+                 "kty": "OKP",
+                 "crv": "Ed25519",
+                 "use": "sig",
+                 "kid": "vapor",
+                 "x": "\(x)",
+                 "d": "\(d)"
+                 }
+            ]
+        }
+        """
+
+        let signers = JWTSigners()
+        try signers.use(jwksJSON: jwksString)
+        let foo = try signers.verify(jwt, as: Foo.self)
+        XCTAssertEqual(foo.bar, 42)
+    }
+        
+    func testVerifyingEdDSAKeyUsingJWKWithMixedBase64Formats() throws {
+        struct Foo: JWTPayload {
+            var bar: Int
+            func verify(using signer: JWTSigner) throws { }
+        }
+                
+        // eddsa key in base64url format
+        let x = eddsaPublicKeyBase64Url
+        let d = eddsaPrivateKeyBae64
+
+        // sign jwt
+        let signer = try JWTSigner.eddsa(.private(x: x, d: d, curve: .ed25519))
+        let jwt = try signer.sign(Foo(bar: 42), kid: "vapor")
+
+        // verify using jwks without alg
+        let jwksString = """
+        {
+            "keys": [
+                {
+                  "kty": "OKP",
+                  "crv": "Ed25519",
+                  "use": "sig",
+                  "kid": "vapor",
+                  "x": "\(x)",
+                  "d": "\(d)"
+                 }
+            ]
+        }
+        """
+
+        let signers = JWTSigners()
+        try signers.use(jwksJSON: jwksString)
+        let foo = try signers.verify(jwt, as: Foo.self)
+        XCTAssertEqual(foo.bar, 42)
+    }
+
 }
 
 struct AudiencePayload: Codable {
@@ -835,6 +981,11 @@ F3IAwlztc7uFj589k/M+xO4TGdrEVlMyiVdC5/B0MLa8LQ==
 -----END CERTIFICATE-----
 """
 
+let eddsaPublicKeyBase64 = "0ZcEvMCSYqSwR8XIkxOoaYjRQSAO8frTMSCpNbUl4lE="
+let eddsaPrivateKeyBae64 = "d1H3/dcg0V3XyAuZW2TE5Z3rhY20M+4YAfYu/HUQd8w="
+let eddsaPublicKeyBase64Url = "0ZcEvMCSYqSwR8XIkxOoaYjRQSAO8frTMSCpNbUl4lE"
+let eddsaPrivateKeyBae64Url = "d1H3_dcg0V3XyAuZW2TE5Z3rhY20M-4YAfYu_HUQd8w"
+
 struct FirebasePayload: JWTPayload, Equatable {
     enum CodingKeys: String, CodingKey {
         case providerID = "provider_id"