Skip to content

Commit

Permalink
Add ECDSAKey support to JWKS (vapor#44)
Browse files Browse the repository at this point in the history
* WIP: port required Katalysis changes to latest vapor/jwt-kit version (4.0.1)
Check the TODO in Source/JWTKit
Some tests are failing

* fix tests and todos

* remove key ops

* small fixes

* fixes

* fixed a warning, and removed stuff

* cleanup

* more cleanup

* small fix

* small fix

* add initializers for keys

* add JWKS initializer

* apply fixes

* make parameters property public

* make static funcs public

* fix ecdsa key decoding

* fix bug and add test

* dirty fix applied

* improve api

* cleanup

* cleanup

* cleanup

* remove warning

* fix encoding label for ecdsa keys

* fix KeyType to match official spec

* update all algorithm and curve labels to specification

* fix test since spec update

* make convenience init public

Co-authored-by: Alex Tran Qui <[email protected]>
  • Loading branch information
JaapWijnen and ratranqu authored Sep 8, 2020
1 parent 8abeddb commit 6055fe8
Show file tree
Hide file tree
Showing 11 changed files with 296 additions and 63 deletions.
1 change: 1 addition & 0 deletions Sources/CJWTKitBoringSSL/include/CJWTKitBoringSSL.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#include "CJWTKitBoringSSL_base.h"
#include "CJWTKitBoringSSL_bio.h"
#include "CJWTKitBoringSSL_blowfish.h"
#include "CJWTKitBoringSSL_bn.h"
#include "CJWTKitBoringSSL_boringssl_prefix_symbols.h"
#include "CJWTKitBoringSSL_boringssl_prefix_symbols_asm.h"
#include "CJWTKitBoringSSL_cast.h"
Expand Down
54 changes: 50 additions & 4 deletions Sources/JWTKit/ECDSA/ECDSAKey.swift
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import CJWTKitBoringSSL

public final class ECDSAKey: OpenSSLKey {
public enum Curve {
case p256
case p384
case p521
public enum Curve: String, Codable {
case p256 = "P-256"
case p384 = "P-384"
case p521 = "P-521"

var cName: Int32 {
switch self {
Expand Down Expand Up @@ -59,8 +59,54 @@ public final class ECDSAKey: OpenSSLKey {
init(_ c: OpaquePointer) {
self.c = c
}

public convenience init(parameters: Parameters, curve: Curve = .p521, privateKey: String? = nil) throws {
guard let c = CJWTKitBoringSSL_EC_KEY_new_by_curve_name(curve.cName) else {
throw JWTError.signingAlgorithmFailure(ECDSAError.newKeyByCurveFailure)
}

guard let bnX = BigNumber.convert(parameters.x) else {
throw JWTError.generic(identifier: "ecCoordinates", reason: "Unable to interpret x as BN")
}
guard let bnY = BigNumber.convert(parameters.y) else {
throw JWTError.generic(identifier: "ecCoordinates", reason: "Unable to interpret y as BN")
}

if CJWTKitBoringSSL_EC_KEY_set_public_key_affine_coordinates(c, bnX.c, bnY.c) != 1 {
throw JWTError.generic(identifier: "ecCoordinates", reason: "Unable to set public key")
}

if let privateKey = privateKey {
guard let bnPrivate = BigNumber.convert(privateKey) else {
throw JWTError.generic(identifier: "ecPrivateKey", reason: "Unable to interpret privateKey as BN")
}
if CJWTKitBoringSSL_EC_KEY_set_private_key(c, bnPrivate.c) != 1 {
throw JWTError.generic(identifier: "ecPrivateKey", reason: "Unable to set private key")
}
}

self.init(c)
}

deinit {
CJWTKitBoringSSL_EC_KEY_free(self.c)
}

public var parameters: Parameters? {
let group: OpaquePointer = CJWTKitBoringSSL_EC_KEY_get0_group(self.c)
let pubKey: OpaquePointer = CJWTKitBoringSSL_EC_KEY_get0_public_key(self.c)

let bnX = BigNumber()
let bnY = BigNumber()
if (CJWTKitBoringSSL_EC_POINT_get_affine_coordinates_GFp(group, pubKey, bnX.c, bnY.c, nil) != 1) {
return nil
}

return Parameters(x: bnX.toBase64(), y: bnY.toBase64())
}

public struct Parameters {
public let x: String
public let y: String
}
}
4 changes: 3 additions & 1 deletion Sources/JWTKit/ECDSA/ECDSASigner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ internal struct ECDSASigner: JWTAlgorithm, OpenSSLSigner {
var curveResultSize: Int {
let curveName = CJWTKitBoringSSL_EC_GROUP_get_curve_name(CJWTKitBoringSSL_EC_KEY_get0_group(key.c))
switch curveName {
case NID_X9_62_prime256v1, NID_secp384r1:
case NID_X9_62_prime256v1:
return 32
case NID_secp384r1:
return 48
case NID_secp521r1:
return 66
default:
Expand Down
2 changes: 1 addition & 1 deletion Sources/JWTKit/HMAC/JWTSigner+HMAC.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ extension JWTSigner {
return .init(algorithm: HMACSigner<SHA384>(key: key, name: "HS384"))
}

// MARK: 384
// MARK: 512

public static func hs512(key: String) -> JWTSigner {
self.hs512(key: [UInt8](key.utf8))
Expand Down
123 changes: 69 additions & 54 deletions Sources/JWTKit/JWK/JWK.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,67 +4,40 @@ import class Foundation.JSONDecoder
/// A JSON Web Key.
///
/// Read specification (RFC 7517) https://tools.ietf.org/html/rfc7517.
public struct JWK: Decodable {
public struct JWK: Codable {
/// Supported `kty` key types.
public enum KeyType: Decodable {
public enum KeyType: String, Codable {
/// RSA
case rsa

/// Decodes from a lowercased string.
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let value = try container.decode(String.self).lowercased()
switch value {
case "rsa":
self = .rsa
default:
throw JWTError.invalidJWK
}
}
case rsa = "RSA"
/// ECDSA
case ecdsa = "EC"
}

/// The `kty` (key type) parameter identifies the cryptographic algorithm
/// family used with the key, such as `RSA` or `EC`. The `kty` value
/// is a case-sensitive string.
/// family used with the key, such as `RSA` or `ECDSA`. The `kty` value
/// is a case-sensitive string.
public var keyType: KeyType

/// Supported `alg` algorithms
public enum Algorithm: Decodable {
public enum Algorithm: String, Codable {
/// RSA with SHA256
case rs256
case rs256 = "RS256"
/// RSA with SHA384
case rs384
case rs384 = "RS384"
/// RSA with SHA512
case rs512

init?(string: String) {
switch string.lowercased() {
case "rs256":
self = .rs256
case "rs384":
self = .rs384
case "rs512":
self = .rs512
default:
return nil
}
}

/// Decodes from a lowercased string.
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let string = try container.decode(String.self)
guard let algorithm = Self(string: string) else {
throw JWTError.invalidJWK
}
self = algorithm
}
case rs512 = "RS512"
/// EC with SHA256
case es256 = "ES256"
/// EC with SHA384
case es384 = "ES384"
/// EC with SHA512
case es512 = "ES512"
}

/// The `alg` (algorithm) parameter identifies the algorithm intended for
/// use with the key. The `alg` value is a case-sensitive ASCII string.
public var algorithm: Algorithm?

/// The `alg` (algorithm) parameter identifies the algorithm intended for
/// use with the key. The `alg` value is a case-sensitive ASCII string.
public var algorithm: Algorithm?
/// The `kid` (key ID) parameter is used to match a specific key. This is
/// used, for instance, to choose among a set of keys within a JWK Set
/// during key rollover.
Expand All @@ -79,26 +52,68 @@ public struct JWK: Decodable {
///
/// The `kid` value is a case-sensitive string.
public var keyIdentifier: JWKIdentifier?


// RSA keys
// Represented as the base64url encoding of the value’s unsigned big endian representation as an octet sequence.
/// `n` Modulus.
public var modulus: String?

/// `e` Exponent.
public var exponent: String?

/// `d` Private exponent.
public var privateExponent: String?

// ECDSA keys
public var x: String?

public var y: String?

public var curve: ECDSAKey.Curve?

private enum CodingKeys: String, CodingKey {
case keyType = "kty"
case algorithm = "alg"
case keyIdentifier = "kid"
case modulus = "n"
case exponent = "e"
case privateExponent = "d"
case curve = "crv"
case x
case y
}

public init(json: String) throws {
self = try JSONDecoder().decode(JWK.self, from: Data(json.utf8))
}

public static func rsa(_ algorithm: Algorithm?, identifier: JWKIdentifier?, modulus: String?, exponent: String?, privateExponent: String? = nil) -> JWK {
JWK(keyType: .rsa, algorithm: algorithm, keyIdentifier: identifier, n: modulus, e: exponent, d: privateExponent)
}

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)
}

private init(
keyType: KeyType,
algorithm: Algorithm? = nil,
keyIdentifier: JWKIdentifier? = nil,
n: String? = nil,
e: String? = nil,
d: String? = nil,
x: String? = nil,
y: String? = nil,
curve: ECDSAKey.Curve? = nil
) {
self.keyType = keyType
self.algorithm = algorithm
self.keyIdentifier = keyIdentifier
self.modulus = n
self.exponent = e
self.privateExponent = d
self.x = x
self.y = y
self.curve = curve
}
}
6 changes: 5 additions & 1 deletion Sources/JWTKit/JWK/JWKS.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@
///
/// A JSON object that represents a set of JWKs.
/// Read specification (RFC 7517) https://tools.ietf.org/html/rfc7517.
public struct JWKS: Decodable {
public struct JWKS: Codable {
/// All JSON Web Keys
public var keys: [JWK]

public init(keys: [JWK]) {
self.keys = keys
}

/// Retrieves the desired key from the JSON Web Key Set
/// - Parameters:
Expand Down
3 changes: 3 additions & 0 deletions Sources/JWTKit/JWTError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public enum JWTError: Error, CustomStringConvertible, LocalizedError {
case unknownKID(JWKIdentifier)
case invalidJWK
case invalidBool(String)
case generic(identifier: String, reason: String)

public var reason: String {
switch self {
Expand All @@ -28,6 +29,8 @@ public enum JWTError: Error, CustomStringConvertible, LocalizedError {
return "invalid JWK"
case .invalidBool(let str):
return "invalid boolean value: \(str)"
case .generic(let identifier, let reason):
return "missing '\(identifier). \(reason)"
}
}

Expand Down
49 changes: 48 additions & 1 deletion Sources/JWTKit/JWTSigners.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ public final class JWTSigners {
case .jwt(let jwt):
return jwt
case .jwk(let jwk):
return jwk.signer(for: alg.flatMap(JWK.Algorithm.init))
return jwk.signer(for: alg.flatMap({ JWK.Algorithm.init(rawValue: $0) }))
}
}

Expand Down Expand Up @@ -180,7 +180,54 @@ private struct JWKSigner {
return JWTSigner.rs384(key: rsaKey)
case .rs512:
return JWTSigner.rs512(key: rsaKey)
default:
return nil
}

case .ecdsa:
guard let x = self.jwk.x else {
return nil
}
guard let y = self.jwk.y else {
return nil
}

guard let algorithm = algorithm ?? self.jwk.algorithm else {
return nil
}

let curve: ECDSAKey.Curve

if let jwkCurve = self.jwk.curve {
curve = jwkCurve
} else {
switch algorithm {
case .es256:
curve = .p256
case .es384:
curve = .p384
case .es512:
curve = .p521
default:
return nil
}
}

guard let ecKey = try? ECDSAKey(parameters: .init(x: x, y: y), curve: curve, privateKey: self.jwk.privateExponent) else {
return nil
}

switch algorithm {
case .es256:
return JWTSigner.es256(key: ecKey)
case .es384:
return JWTSigner.es384(key: ecKey)
case .es512:
return JWTSigner.es512(key: ecKey)
default:
return nil
}
}
}
}

38 changes: 38 additions & 0 deletions Sources/JWTKit/Utilities/BigNumber.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import Foundation
import CJWTKitBoringSSL

class BigNumber {
let c: UnsafeMutablePointer<BIGNUM>?;

public init() {
self.c = CJWTKitBoringSSL_BN_new();
}

init(_ ptr: OpaquePointer) {
self.c = UnsafeMutablePointer<BIGNUM>(ptr);
}

deinit {
CJWTKitBoringSSL_BN_free(self.c);
}

public static func convert(_ bnBase64: String) -> BigNumber? {
guard let data = Data(base64Encoded: bnBase64) else {
return nil
}

let c = data.withUnsafeBytes { (p: UnsafeRawBufferPointer) -> OpaquePointer in
return OpaquePointer(CJWTKitBoringSSL_BN_bin2bn(p.baseAddress?.assumingMemoryBound(to: UInt8.self), p.count, nil))
}
return BigNumber(c)
}

public func toBase64(_ size: Int = 1000) -> String {
let pBuffer = UnsafeMutablePointer<UInt8>.allocate(capacity: size)
defer { pBuffer.deallocate() }

let actualBytes = Int(CJWTKitBoringSSL_BN_bn2bin(self.c, pBuffer))
let data = Data(bytes: pBuffer, count: actualBytes)
return data.base64EncodedString()
}
}
Loading

0 comments on commit 6055fe8

Please sign in to comment.