From 26d83e39adb1c5d01692f3a5050e2e57d567c716 Mon Sep 17 00:00:00 2001 From: Paul Toffoloni Date: Mon, 11 Nov 2024 14:11:25 +0100 Subject: [PATCH 1/2] Start adding JWK representation --- .../JWTKit/ECDSA/ECDSA+JWKRepresentable.swift | 20 + Sources/JWTKit/ECDSA/ECDSA.swift | 44 ++- Sources/JWTKit/ECDSA/ECDSAKeyTypes.swift | 5 +- Sources/JWTKit/JWK/JWK.swift | 368 ++++++++---------- Sources/JWTKit/JWK/JWKRepresentable.swift | 25 ++ Sources/JWTKit/JWK/JWKSigner.swift | 6 +- Tests/JWTKitTests/ECDSATests.swift | 37 +- 7 files changed, 288 insertions(+), 217 deletions(-) create mode 100644 Sources/JWTKit/ECDSA/ECDSA+JWKRepresentable.swift create mode 100644 Sources/JWTKit/JWK/JWKRepresentable.swift diff --git a/Sources/JWTKit/ECDSA/ECDSA+JWKRepresentable.swift b/Sources/JWTKit/ECDSA/ECDSA+JWKRepresentable.swift new file mode 100644 index 00000000..fd858508 --- /dev/null +++ b/Sources/JWTKit/ECDSA/ECDSA+JWKRepresentable.swift @@ -0,0 +1,20 @@ +import Crypto + +extension ECDSA.PublicKey: JWKRepresentable { + public func toJWKRepresentation(kid: String? = nil) -> JWK { + let algorithm: JWK.Algorithm = + switch self.curve { + case .p256: .es256 + case .p384: .es384 + case .p521: .es512 + default: fatalError("Unsupported curve") + } + return .ecdsa( + algorithm, + identifier: kid.map { .init(string: $0) }, + x: self.coordinates.x, + y: self.coordinates.y, + curve: self.curve + ) + } +} diff --git a/Sources/JWTKit/ECDSA/ECDSA.swift b/Sources/JWTKit/ECDSA/ECDSA.swift index 5f7124de..eeb2f03a 100644 --- a/Sources/JWTKit/ECDSA/ECDSA.swift +++ b/Sources/JWTKit/ECDSA/ECDSA.swift @@ -13,7 +13,10 @@ public protocol ECDSAKey: Sendable { associatedtype Curve: ECDSACurveType var curve: ECDSACurve { get } + + @available(*, deprecated, renamed: "coordinates") var parameters: ECDSAParameters? { get } + var coordinates: ECDSACoordinates { get } } extension ECDSA { @@ -23,12 +26,20 @@ extension ECDSA { public private(set) var curve: ECDSACurve = Curve.curve + @available(*, deprecated, renamed: "coordinates") public var parameters: ECDSAParameters? { // 0x04 || x || y let x = self.backing.x963Representation[Curve.byteRanges.x].base64EncodedString() let y = self.backing.x963Representation[Curve.byteRanges.y].base64EncodedString() return (x, y) } + + public var coordinates: ECDSACoordinates { + // 0x04 || x || y + let x = self.backing.x963Representation[Curve.byteRanges.x].base64EncodedString() + let y = self.backing.x963Representation[Curve.byteRanges.y].base64EncodedString() + return (x, y) + } var backing: PublicKey @@ -87,7 +98,7 @@ extension ECDSA { try self.init(pem: String(decoding: data, as: UTF8.self)) } - /// Initializes a new ``ECDSA.PublicKey` with ECDSA parameters. + /// Initializes a new ``ECDSA.PublicKey`` with ECDSA parameters. /// /// - Parameters: /// - parameters: The ``ECDSAParameters`` tuple containing the x and y coordinates of the public key. These coordinates should be base64 URL encoded strings. @@ -98,6 +109,7 @@ extension ECDSA { /// /// - Note: /// The ``ECDSAParameters`` tuple is assumed to have x and y properties that are base64 URL encoded strings representing the respective coordinates of an ECDSA public key. + @available(*, deprecated, renamed: "init(coordinates:)") public init(parameters: ECDSAParameters) throws { guard let x = parameters.x.base64URLDecodedData(), @@ -107,13 +119,34 @@ extension ECDSA { } self.backing = try PublicKey(x963Representation: [0x04] + x + y) } + + /// Initializes a new ``ECDSA.PublicKey`` with ECDSA coordinates. + /// + /// - Parameters: + /// - parameters: The ``ECDSACoordinates`` tuple containing the x and y coordinates of the public key. These coordinates should be base64 URL encoded strings. + /// + /// - Throws: + /// - ``JWTError/generic`` with the identifier `ecCoordinates` if the x and y coordinates from `parameters` cannot be interpreted as base64 encoded data. + /// - ``JWTError/generic`` with the identifier `ecPrivateKey` if the provided `privateKey` is non-nil but cannot be interpreted as a valid `PrivateKey`. + /// + /// - Note: + /// The ``ECDSACoordinates`` tuple is assumed to have x and y properties that are base64 URL encoded strings representing the respective coordinates of an ECDSA public key. + public init(coordinates: ECDSACoordinates) throws { + guard + let x = coordinates.x.base64URLDecodedData(), + let y = coordinates.y.base64URLDecodedData() + else { + throw JWTError.generic(identifier: "ecCoordinates", reason: "Unable to interpret x or y as base64 encoded data") + } + self.backing = try PublicKey(x963Representation: [0x04] + x + y) + } init(backing: PublicKey) { self.backing = backing } public static func == (lhs: Self, rhs: Self) -> Bool { - lhs.parameters?.x == rhs.parameters?.x && lhs.parameters?.y == rhs.parameters?.y + lhs.coordinates.x == rhs.coordinates.x && lhs.coordinates.y == rhs.coordinates.y } } } @@ -125,9 +158,14 @@ extension ECDSA { public private(set) var curve: ECDSACurve = Curve.curve + @available(*, deprecated, renamed: "coordinates") public var parameters: ECDSAParameters? { self.publicKey.parameters } + + public var coordinates: ECDSACoordinates { + self.publicKey.coordinates + } var backing: PrivateKey @@ -196,7 +234,7 @@ extension ECDSA { } public static func == (lhs: Self, rhs: Self) -> Bool { - lhs.parameters?.x == rhs.parameters?.x && lhs.parameters?.y == rhs.parameters?.y + lhs.coordinates.x == rhs.coordinates.x && lhs.coordinates.y == rhs.coordinates.y } } } diff --git a/Sources/JWTKit/ECDSA/ECDSAKeyTypes.swift b/Sources/JWTKit/ECDSA/ECDSAKeyTypes.swift index 3626946e..e066d202 100644 --- a/Sources/JWTKit/ECDSA/ECDSAKeyTypes.swift +++ b/Sources/JWTKit/ECDSA/ECDSAKeyTypes.swift @@ -7,6 +7,9 @@ import X509 import Foundation #endif +@available(*, deprecated, renamed: "ECDSACoordinates") +public typealias ECDSAParameters = (x: String, y: String) + /// A typealias representing the parameters of an ECDSA (Elliptic Curve Digital Signature Algorithm) key. /// /// This tuple consists of two strings representing the x and y coordinates on the elliptic curve. @@ -19,7 +22,7 @@ import X509 /// - Parameters: /// - x: A `String` representing the x-coordinate on the elliptic curve. /// - y: A `String` representing the y-coordinate on the elliptic curve. -public typealias ECDSAParameters = (x: String, y: String) +public typealias ECDSACoordinates = (x: String, y: String) public protocol ECDSASignature: Sendable { var rawRepresentation: Data { get set } diff --git a/Sources/JWTKit/JWK/JWK.swift b/Sources/JWTKit/JWK/JWK.swift index 849af7bd..02284508 100644 --- a/Sources/JWTKit/JWK/JWK.swift +++ b/Sources/JWTKit/JWK/JWK.swift @@ -1,224 +1,49 @@ import struct Foundation.Data import class Foundation.JSONDecoder -/// A JSON Web Key. -/// -/// Read specification (RFC 7517) https://tools.ietf.org/html/rfc7517. +/// A JSON Web Key, according to RFC 7517. +/// See: https://tools.ietf.org/html/rfc7517. public struct JWK: Codable, Sendable { - public struct Curve: Codable, RawRepresentable, Equatable, Sendable { - let backing: Backing - - public var rawValue: String { - switch self.backing { - case .ecdsa(let ecdsaCurve): - ecdsaCurve.rawValue - case .eddsa(let eddsaCurve): - eddsaCurve.rawValue - } - } - - /// Represents an ECDSA curve. - public static func ecdsa(_ curve: ECDSACurve) -> Self { .init(.ecdsa(curve)) } - - /// Represents an EdDSA curve. - public static func eddsa(_ curve: EdDSACurve) -> Self { .init(.eddsa(curve)) } - - enum Backing: Codable { - case ecdsa(ECDSACurve) - case eddsa(EdDSACurve) - } - - init(_ backing: Backing) { - self.backing = backing - } - - public init?(rawValue: String) { - if let ecdsaCurve = ECDSACurve(rawValue: rawValue) { - self.init(.ecdsa(ecdsaCurve)) - } else if let eddsaCurve = EdDSACurve(rawValue: rawValue) { - self.init(.eddsa(eddsaCurve)) - } else { - return nil - } - } - - public init(from decoder: any Decoder) throws { - let container = try decoder.singleValueContainer() - if let ecdsaCurve = try? container.decode(ECDSACurve.self) { - self = .ecdsa(ecdsaCurve) - } else if let eddsaCurve = try? container.decode(EdDSACurve.self) { - self = .eddsa(eddsaCurve) - } else { - throw DecodingError.dataCorruptedError( - in: container, debugDescription: "Curve type not supported") - } - } - - public func encode(to encoder: any Encoder) throws { - switch self.backing { - case .ecdsa(let ecdsaCurve): - try ecdsaCurve.encode(to: encoder) - case .eddsa(let eddsaCurve): - try eddsaCurve.encode(to: encoder) - } - } - } - - /// Supported `kty` key types. - public struct KeyType: Codable, RawRepresentable, Equatable, Sendable { - public typealias RawValue = String - - let backing: Backing - - public var rawValue: String { - self.backing.rawValue - } - - /// RSA - public static let rsa = Self(backing: .rsa) - /// ECDSA - public static let ecdsa = Self(backing: .ecdsa) - /// Octet Key Pair - public static let octetKeyPair = Self(backing: .octetKeyPair) - - enum Backing: String, Codable { - case rsa = "RSA" - case ecdsa = "EC" - case octetKeyPair = "OKP" - } - - init(backing: Backing) { - self.backing = backing - } - - public init?(rawValue: String) { - guard let backing = Backing(rawValue: rawValue) else { - return nil - } - self.init(backing: backing) - } - - public init(from decoder: any Decoder) throws { - try self.init(backing: decoder.singleValueContainer().decode(Backing.self)) - } - - public func encode(to encoder: any Encoder) throws { - var container = encoder.singleValueContainer() - try container.encode(self.backing) - } - } + // MARK: Properties /// The `kty` (key type) parameter identifies the cryptographic algorithm - /// family used with the key, such as `RSA` or `ECDSA`. The `kty` value - /// is a case-sensitive string. + /// family used with the key, such as `RSA` or `ECDSA`. public var keyType: KeyType - /// Supported `alg` algorithms - public struct Algorithm: Codable, RawRepresentable, Equatable, Sendable { - public typealias RawValue = String - - let backing: Backing - - public var rawValue: String { - self.backing.rawValue - } - - /// RSA with SHA256 - public static let rs256 = Self(backing: .rs256) - /// RSA with SHA384 - public static let rs384 = Self(backing: .rs384) - /// RSA with SHA512 - public static let rs512 = Self(backing: .rs512) - /// RSA-PSS with SHA256 - public static let ps256 = Self(backing: .ps256) - /// RSA-PSS with SHA384 - public static let ps384 = Self(backing: .ps384) - /// RSA-PSS with SHA512 - public static let ps512 = Self(backing: .ps512) - /// EC with SHA256 - public static let es256 = Self(backing: .es256) - /// EC with SHA384 - public static let es384 = Self(backing: .es384) - /// EC with SHA512 - public static let es512 = Self(backing: .es512) - /// EdDSA - public static let eddsa = Self(backing: .eddsa) - - enum Backing: String, Codable { - case rs256 = "RS256" - case rs384 = "RS384" - case rs512 = "RS512" - case ps256 = "PS256" - case ps384 = "PS384" - case ps512 = "PS512" - case es256 = "ES256" - case es384 = "ES384" - case es512 = "ES512" - case eddsa = "EdDSA" - } - - init(backing: Backing) { - self.backing = backing - } - - public init?(rawValue: String) { - guard let backing = Backing(rawValue: rawValue) else { - return nil - } - self.init(backing: backing) - } - - public init(from decoder: any Decoder) throws { - try self.init(backing: decoder.singleValueContainer().decode(Backing.self)) - } - - public func encode(to encoder: any Encoder) throws { - var container = encoder.singleValueContainer() - try container.encode(self.backing) - } - } - /// The `alg` (algorithm) parameter identifies the algorithm intended for - /// use with the key. The `alg` value is a case-sensitive ASCII string. + /// use with the key. 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. - /// - /// The structure of the `kid` value is unspecified. When `kid` values - /// are used within a JWK Set, different keys within the JWK set should - /// use distinct `kid` values. - /// - /// (One example in which different keys might use the same `kid` value - /// is if they have different `kty` (key type) values but are considered to be - /// equivalent alternatives by the application using them.) - /// - /// The `kid` value is a case-sensitive string. + /// The `kid` (key ID) parameter is used to identify a specific key, + /// often in a set of keys. 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. + // MARK: RSA keys + + // RSA modulus as a Base64 URL encoded string. public var modulus: String? - /// `e` Exponent. + /// RSA public exponent as a Base64 URL encoded string. public var exponent: String? - /// `d` Private exponent. + /// RSA private exponent as a Base64 URL encoded string. public var privateExponent: String? - /// `p` First prime factor. + /// RSA first prime factor as a Base64 URL encoded string. public var prime1: String? - /// `q` Second prime factor. + /// RSA second prime factor as a Base64 URL encoded string. public var prime2: String? - // ECDSA keys + // MARK: ECDSA keys + + /// ECDSA x-coordinate as a Base64 URL encoded string. public var x: String? + /// ECDSA y-coordinate as a Base64 URL encoded string. public var y: String? + /// `crv` (curve) parameter identifying the cryptographic curve used. public var curve: Curve? private enum CodingKeys: String, CodingKey { @@ -235,10 +60,6 @@ public struct JWK: Codable, Sendable { 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?, @@ -252,9 +73,7 @@ public struct JWK: Codable, Sendable { keyIdentifier: identifier, modulus: modulus, exponent: exponent, - privateExponent: privateExponent, - prime1: nil, - prime2: nil + privateExponent: privateExponent ) } @@ -295,6 +114,10 @@ public struct JWK: Codable, Sendable { ) } + public init(json: String) throws { + self = try JSONDecoder().decode(JWK.self, from: Data(json.utf8)) + } + private init( keyType: KeyType, algorithm: Algorithm? = nil, @@ -321,3 +144,146 @@ public struct JWK: Codable, Sendable { self.curve = curve } } + +extension JWK { + /// Supported `kty` key types. + public struct KeyType: Codable, RawRepresentable, Equatable, Sendable { + enum Backing: String, Codable { + case rsa = "RSA" + case ecdsa = "EC" + case octetKeyPair = "OKP" + } + + let backing: Backing + + public var rawValue: String { self.backing.rawValue } + + public static let rsa = Self(backing: .rsa) + public static let ecdsa = Self(backing: .ecdsa) + public static let octetKeyPair = Self(backing: .octetKeyPair) + + init(backing: Backing) { + self.backing = backing + } + + public init?(rawValue: String) { + guard let backing = Backing(rawValue: rawValue) else { + return nil + } + self.init(backing: backing) + } + } +} + +extension JWK { + /// Supported `alg` algorithms + public struct Algorithm: Codable, RawRepresentable, Equatable, Sendable { + enum Backing: String, Codable { + case rs256 = "RS256" + case rs384 = "RS384" + case rs512 = "RS512" + case ps256 = "PS256" + case ps384 = "PS384" + case ps512 = "PS512" + case es256 = "ES256" + case es384 = "ES384" + case es512 = "ES512" + case eddsa = "EdDSA" + } + + let backing: Backing + + public var rawValue: String { self.backing.rawValue } + + /// RSA with SHA256 + public static let rs256 = Self(backing: .rs256) + /// RSA with SHA384 + public static let rs384 = Self(backing: .rs384) + /// RSA with SHA512 + public static let rs512 = Self(backing: .rs512) + /// RSA-PSS with SHA256 + public static let ps256 = Self(backing: .ps256) + /// RSA-PSS with SHA384 + public static let ps384 = Self(backing: .ps384) + /// RSA-PSS with SHA512 + public static let ps512 = Self(backing: .ps512) + /// EC with SHA256 + public static let es256 = Self(backing: .es256) + /// EC with SHA384 + public static let es384 = Self(backing: .es384) + /// EC with SHA512 + public static let es512 = Self(backing: .es512) + /// EdDSA + public static let eddsa = Self(backing: .eddsa) + + init(backing: Backing) { + self.backing = backing + } + + public init?(rawValue: String) { + guard let backing = Backing(rawValue: rawValue) else { + return nil + } + self.init(backing: backing) + } + } +} + +extension JWK { + public struct Curve: Codable, RawRepresentable, Equatable, Sendable { + enum Backing: Codable { + case ecdsa(ECDSACurve) + case eddsa(EdDSACurve) + } + + let backing: Backing + + public var rawValue: String { + switch self.backing { + case .ecdsa(let ecdsaCurve): ecdsaCurve.rawValue + case .eddsa(let eddsaCurve): eddsaCurve.rawValue + } + } + + /// Represents an ECDSA curve. + public static func ecdsa(_ curve: ECDSACurve) -> Self { .init(.ecdsa(curve)) } + + /// Represents an EdDSA curve. + public static func eddsa(_ curve: EdDSACurve) -> Self { .init(.eddsa(curve)) } + + init(_ backing: Backing) { + self.backing = backing + } + + public init?(rawValue: String) { + if let ecdsaCurve = ECDSACurve(rawValue: rawValue) { + self.init(.ecdsa(ecdsaCurve)) + } else if let eddsaCurve = EdDSACurve(rawValue: rawValue) { + self.init(.eddsa(eddsaCurve)) + } else { + return nil + } + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + if let ecdsaCurve = try? container.decode(ECDSACurve.self) { + self = .ecdsa(ecdsaCurve) + } else if let eddsaCurve = try? container.decode(EdDSACurve.self) { + self = .eddsa(eddsaCurve) + } else { + throw DecodingError.dataCorruptedError(in: container, debugDescription: "Curve type not supported") + } + } + + public func encode(to encoder: any Encoder) throws { + switch self.backing { + case .ecdsa(let ecdsaCurve): + try ecdsaCurve.encode(to: encoder) + case .eddsa(let eddsaCurve): + try eddsaCurve.encode(to: encoder) + } + } + } +} + diff --git a/Sources/JWTKit/JWK/JWKRepresentable.swift b/Sources/JWTKit/JWK/JWKRepresentable.swift new file mode 100644 index 00000000..9ed39c8d --- /dev/null +++ b/Sources/JWTKit/JWK/JWKRepresentable.swift @@ -0,0 +1,25 @@ +#if !canImport(Foundation) + import FoundationEssentials +#else + import Foundation +#endif + +public protocol JWKRepresentable { + func toJWKRepresentation(kid: String?) -> JWK +} + +extension JWK { + public func toJSONString() throws -> String { + let data = try JSONEncoder().encode(self) + guard let string = String(data: data, encoding: .utf8) else { + throw EncodingError.invalidValue( + self, EncodingError.Context(codingPath: [], debugDescription: "Failed to encode JWK to JSON string") + ) + } + return string + } + + public func toJSONData() throws -> Data { + try JSONEncoder().encode(self) + } +} diff --git a/Sources/JWTKit/JWK/JWKSigner.swift b/Sources/JWTKit/JWK/JWKSigner.swift index 6682d332..540482df 100644 --- a/Sources/JWTKit/JWK/JWKSigner.swift +++ b/Sources/JWTKit/JWK/JWKSigner.swift @@ -101,19 +101,19 @@ extension JWK { if let privateExponent = self.privateExponent { return try ECDSASigner(key: ES256PrivateKey(key: privateExponent)) } else { - return try ECDSASigner(key: ES256PublicKey(parameters: (x, y))) + return try ECDSASigner(key: ES256PublicKey(coordinates: (x, y))) } case .es384: if let privateExponent = self.privateExponent { return try ECDSASigner(key: ES384PrivateKey(key: privateExponent)) } else { - return try ECDSASigner(key: ES384PublicKey(parameters: (x, y))) + return try ECDSASigner(key: ES384PublicKey(coordinates: (x, y))) } case .es512: if let privateExponent = self.privateExponent { return try ECDSASigner(key: ES512PrivateKey(key: privateExponent)) } else { - return try ECDSASigner(key: ES512PublicKey(parameters: (x, y))) + return try ECDSASigner(key: ES512PublicKey(coordinates: (x, y))) } default: return nil diff --git a/Tests/JWTKitTests/ECDSATests.swift b/Tests/JWTKitTests/ECDSATests.swift index abd94598..bd1aa94d 100644 --- a/Tests/JWTKitTests/ECDSATests.swift +++ b/Tests/JWTKitTests/ECDSATests.swift @@ -273,9 +273,8 @@ struct ECDSATests { // Sign the message with the initial private key let signature = try await keys.getKey(for: "initial").sign(message) - // Extract the EC parameters and create a public key from it - let params = ec.parameters! - try await keys.add(ecdsa: ES256PublicKey(parameters: params), kid: "params") + // Extract the EC coordinates and create a public key from them + try await keys.add(ecdsa: ES256PublicKey(coordinates: ec.coordinates), kid: "params") // Verify the signature using the public key created from the parameters #expect(try await keys.getKey(for: "params").verify(signature, signs: message)) @@ -295,9 +294,8 @@ struct ECDSATests { // Sign the message with the initial private key let signature = try await keys.getKey(for: "initial").sign(message) - // Extract the EC parameters and create a public key from it - let params = ec.parameters! - try await keys.add(ecdsa: ES384PublicKey(parameters: params), kid: "params") + // Extract the EC coordinates and create a public key from them + try await keys.add(ecdsa: ES384PublicKey(coordinates: ec.coordinates), kid: "params") // Verify the signature using the public key created from the parameters #expect(try await keys.getKey(for: "params").verify(signature, signs: message)) @@ -317,9 +315,8 @@ struct ECDSATests { // Sign the message with the initial private key let signature = try await keys.getKey(for: "initial").sign(message) - // Extract the EC parameters and create a public key from it - let params = ec.parameters! - try await keys.add(ecdsa: ES512PublicKey(parameters: params), kid: "params") + // Extract the EC coordinates and create a public key from them + try await keys.add(ecdsa: ES512PublicKey(coordinates: ec.coordinates), kid: "params") // Verify the signature using the public key created from the parameters #expect(try await keys.getKey(for: "params").verify(signature, signs: message)) @@ -327,6 +324,28 @@ struct ECDSATests { // Ensure the curve is p521 #expect(ec.curve == .p521) } + + @Test("Generate JWK from ECDSA Public Key") + func generateJWKFromECDSAPublicKey() async throws { + let key = try ES256PublicKey(pem: ecdsaPublicKey) + let jwkString = try key.toJWKRepresentation().toJSONString() + + let dict = try JSONSerialization.jsonObject(with: jwkString.data(using: .utf8)!, options: []) as! [String: Any] + #expect(dict["kty"] as? String == "EC") + #expect(dict["crv"] as? String == "P-256") + #expect(dict["alg"] as? String == "ES256") + #expect(dict["x"] as? String == key.coordinates.x) + #expect(dict["y"] as? String == key.coordinates.y) + + let jwkData = try key.toJWKRepresentation().toJSONData() + let dict2 = try JSONSerialization.jsonObject(with: jwkData, options: []) as! [String: Any] + + #expect(dict2["kty"] as? String == "EC") + #expect(dict2["crv"] as? String == "P-256") + #expect(dict2["alg"] as? String == "ES256") + #expect(dict2["x"] as? String == key.coordinates.x) + #expect(dict2["y"] as? String == key.coordinates.y) + } } let ecdsaPrivateKey = """ From d7218b54b5fbefc6d641ad1f21fb73f438a2bc0e Mon Sep 17 00:00:00 2001 From: Paul Toffoloni Date: Wed, 18 Dec 2024 23:37:36 +0100 Subject: [PATCH 2/2] Add missing JWK parameters --- .gitignore | 1 + .../JWTKit/ECDSA/ECDSA+JWKRepresentable.swift | 25 +++- .../JWTKit/EdDSA/EdDSA+JWKRepresentable.swift | 27 ++++ Sources/JWTKit/JWK/JWK+Parameters.swift | 114 +++++++++++++++ Sources/JWTKit/JWK/JWK.swift | 137 ++++++------------ Sources/JWTKit/JWK/JWKRepresentable.swift | 10 +- 6 files changed, 219 insertions(+), 95 deletions(-) create mode 100644 Sources/JWTKit/EdDSA/EdDSA+JWKRepresentable.swift create mode 100644 Sources/JWTKit/JWK/JWK+Parameters.swift diff --git a/.gitignore b/.gitignore index f724f106..f57967e6 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ Tests/LinuxMain.swift .benchmarkBaselines/ Benchmarks/.benchmarkBaselines/ x5c_test_certs +.swift-format diff --git a/Sources/JWTKit/ECDSA/ECDSA+JWKRepresentable.swift b/Sources/JWTKit/ECDSA/ECDSA+JWKRepresentable.swift index fd858508..b1f9b125 100644 --- a/Sources/JWTKit/ECDSA/ECDSA+JWKRepresentable.swift +++ b/Sources/JWTKit/ECDSA/ECDSA+JWKRepresentable.swift @@ -1,7 +1,15 @@ import Crypto extension ECDSA.PublicKey: JWKRepresentable { - public func toJWKRepresentation(kid: String? = nil) -> JWK { + public func toJWKRepresentation( + keyIdentifier: JWKIdentifier? = nil, + use: JWK.Usage? = nil, + keyOperations: [JWK.KeyOperation]? = nil, + x509URL: String? = nil, + x509CertificateChain: [String]? = nil, + x509SHA1Thumbprint: String? = nil, + x509SHA256Thumbprint: String? = nil + ) -> JWK { let algorithm: JWK.Algorithm = switch self.curve { case .p256: .es256 @@ -9,12 +17,19 @@ extension ECDSA.PublicKey: JWKRepresentable { case .p521: .es512 default: fatalError("Unsupported curve") } - return .ecdsa( - algorithm, - identifier: kid.map { .init(string: $0) }, + return .init( + keyType: .ecdsa, + algorithm: algorithm, + keyIdentifier: keyIdentifier, + use: use, + keyOperations: keyOperations, + x509URL: x509URL, + x509CertificateChain: x509CertificateChain, + x509CertificateSHA1Thumbprint: x509SHA1Thumbprint, + x509CertificateSHA256Thumbprint: x509SHA256Thumbprint, x: self.coordinates.x, y: self.coordinates.y, - curve: self.curve + curve: .ecdsa(self.curve) ) } } diff --git a/Sources/JWTKit/EdDSA/EdDSA+JWKRepresentable.swift b/Sources/JWTKit/EdDSA/EdDSA+JWKRepresentable.swift new file mode 100644 index 00000000..1786039d --- /dev/null +++ b/Sources/JWTKit/EdDSA/EdDSA+JWKRepresentable.swift @@ -0,0 +1,27 @@ +import Crypto + +extension EdDSA.PublicKey: JWKRepresentable { + public func toJWKRepresentation( + keyIdentifier: JWKIdentifier? = nil, + use: JWK.Usage? = nil, + keyOperations: [JWK.KeyOperation]? = nil, + x509URL: String? = nil, + x509CertificateChain: [String]? = nil, + x509SHA1Thumbprint: String? = nil, + x509SHA256Thumbprint: String? = nil + ) -> JWK { + .init( + keyType: .octetKeyPair, + algorithm: .eddsa, + keyIdentifier: keyIdentifier, + use: use, + keyOperations: keyOperations, + x509URL: x509URL, + x509CertificateChain: x509CertificateChain, + x509CertificateSHA1Thumbprint: x509SHA1Thumbprint, + x509CertificateSHA256Thumbprint: x509SHA256Thumbprint, + x: self.rawRepresentation.base64EncodedString(), + curve: .eddsa(self.curve) + ) + } +} diff --git a/Sources/JWTKit/JWK/JWK+Parameters.swift b/Sources/JWTKit/JWK/JWK+Parameters.swift new file mode 100644 index 00000000..9a5a2055 --- /dev/null +++ b/Sources/JWTKit/JWK/JWK+Parameters.swift @@ -0,0 +1,114 @@ +extension JWK { + /// Supported `kty` key types. + public struct KeyType: Codable, RawRepresentable, Equatable, Sendable { + enum Backing: String, Codable { + case rsa = "RSA" + case ecdsa = "EC" + case octetKeyPair = "OKP" + } + + let backing: Backing + + public var rawValue: String { self.backing.rawValue } + + public static let rsa = Self(backing: .rsa) + public static let ecdsa = Self(backing: .ecdsa) + public static let octetKeyPair = Self(backing: .octetKeyPair) + + init(backing: Backing) { + self.backing = backing + } + + public init?(rawValue: String) { + guard let backing = Backing(rawValue: rawValue) else { + return nil + } + self.init(backing: backing) + } + } +} + +extension JWK { + /// Intended use of the public key. + /// https://datatracker.ietf.org/doc/html/rfc7517#section-4.2 + public enum Usage: String, Codable, Sendable { + case signature + case encryption + + enum CodingKeys: String, CodingKey { + case signature = "sig" + case encryption = "enc" + } + } +} + +extension JWK { + /// Operations that the key is intended to be used for. + /// https://datatracker.ietf.org/doc/html/rfc7517#section-4.3 + public enum KeyOperation: String, Codable, Sendable { + case sign + case verify + case encrypt + case decrypt + case wrapKey + case unwrapKey + case deriveKey + case deriveBits + } +} + +extension JWK { + /// The cryptographic algorithm family used with the key. + /// https://datatracker.ietf.org/doc/html/rfc7517#section-4.4 + public struct Algorithm: Codable, RawRepresentable, Equatable, Sendable { + enum Backing: String, Codable { + case rs256 = "RS256" + case rs384 = "RS384" + case rs512 = "RS512" + case ps256 = "PS256" + case ps384 = "PS384" + case ps512 = "PS512" + case es256 = "ES256" + case es384 = "ES384" + case es512 = "ES512" + case eddsa = "EdDSA" + } + + let backing: Backing + + public var rawValue: String { self.backing.rawValue } + + /// RSA with SHA256 + public static let rs256 = Self(backing: .rs256) + /// RSA with SHA384 + public static let rs384 = Self(backing: .rs384) + /// RSA with SHA512 + public static let rs512 = Self(backing: .rs512) + /// RSA-PSS with SHA256 + public static let ps256 = Self(backing: .ps256) + /// RSA-PSS with SHA384 + public static let ps384 = Self(backing: .ps384) + /// RSA-PSS with SHA512 + public static let ps512 = Self(backing: .ps512) + /// EC with SHA256 + public static let es256 = Self(backing: .es256) + /// EC with SHA384 + public static let es384 = Self(backing: .es384) + /// EC with SHA512 + public static let es512 = Self(backing: .es512) + /// EdDSA + public static let eddsa = Self(backing: .eddsa) + + init(backing: Backing) { + self.backing = backing + } + + public init?(rawValue: String) { + guard let backing = Backing(rawValue: rawValue) else { + return nil + } + self.init(backing: backing) + } + } +} + diff --git a/Sources/JWTKit/JWK/JWK.swift b/Sources/JWTKit/JWK/JWK.swift index 02284508..c0d0e982 100644 --- a/Sources/JWTKit/JWK/JWK.swift +++ b/Sources/JWTKit/JWK/JWK.swift @@ -17,7 +17,31 @@ public struct JWK: Codable, Sendable { /// The `kid` (key ID) parameter is used to identify a specific key, /// often in a set of keys. public var keyIdentifier: JWKIdentifier? - + + /// The `use` parameter identifies the intended use of the key. + public var use: Usage? + + /// The `key_ops` (key operations) parameter identifies the operation(s) + /// for which the key is intended to be used. + public var keyOperations: [KeyOperation]? + + /// The `x5u` (X.509 URL) parameter is a URI that refers to a resource + /// for an X.509 public key certificate or certificate chain. + public var x509URL: String? + + /// The `x5c` (X.509 certificate chain) parameter contains a chain of one + /// or more PKIX certificates. + public var x509CertificateChain: [String]? + + /// The `x5t` (X.509 certificate SHA-1 thumbprint) parameter is a base64url-encoded + /// SHA-1 thumbprint (a.k.a. digest) of the DER encoding of an X.509 certificate. + public var x509CertificateSHA1Thumbprint: String? + + /// The `x5t#S256` (X.509 certificate SHA-256 thumbprint) parameter is a + /// base64url-encoded SHA-256 thumbprint (a.k.a. digest) of the DER encoding + /// of an X.509 certificate. + public var x509CertificateSHA256Thumbprint: String? + // MARK: RSA keys // RSA modulus as a Base64 URL encoded string. @@ -58,6 +82,12 @@ public struct JWK: Codable, Sendable { case curve = "crv" case x case y + case use = "use" + case keyOperations = "key_ops" + case x509URL = "x5u" + case x509CertificateChain = "x5c" + case x509CertificateSHA1Thumbprint = "x5t" + case x509CertificateSHA256Thumbprint = "x5t#S256" } public static func rsa( @@ -118,10 +148,16 @@ public struct JWK: Codable, Sendable { self = try JSONDecoder().decode(JWK.self, from: Data(json.utf8)) } - private init( + init( keyType: KeyType, algorithm: Algorithm? = nil, keyIdentifier: JWKIdentifier? = nil, + use: JWK.Usage? = nil, + keyOperations: [JWK.KeyOperation]? = nil, + x509URL: String? = nil, + x509CertificateChain: [String]? = nil, + x509CertificateSHA1Thumbprint: String? = nil, + x509CertificateSHA256Thumbprint: String? = nil, modulus: String? = nil, exponent: String? = nil, privateExponent: String? = nil, @@ -134,6 +170,12 @@ public struct JWK: Codable, Sendable { self.keyType = keyType self.algorithm = algorithm self.keyIdentifier = keyIdentifier + self.use = use + self.keyOperations = keyOperations + self.x509URL = x509URL + self.x509CertificateChain = x509CertificateChain + self.x509CertificateSHA1Thumbprint = x509CertificateSHA1Thumbprint + self.x509CertificateSHA256Thumbprint = x509CertificateSHA256Thumbprint self.modulus = modulus self.exponent = exponent self.privateExponent = privateExponent @@ -145,90 +187,6 @@ public struct JWK: Codable, Sendable { } } -extension JWK { - /// Supported `kty` key types. - public struct KeyType: Codable, RawRepresentable, Equatable, Sendable { - enum Backing: String, Codable { - case rsa = "RSA" - case ecdsa = "EC" - case octetKeyPair = "OKP" - } - - let backing: Backing - - public var rawValue: String { self.backing.rawValue } - - public static let rsa = Self(backing: .rsa) - public static let ecdsa = Self(backing: .ecdsa) - public static let octetKeyPair = Self(backing: .octetKeyPair) - - init(backing: Backing) { - self.backing = backing - } - - public init?(rawValue: String) { - guard let backing = Backing(rawValue: rawValue) else { - return nil - } - self.init(backing: backing) - } - } -} - -extension JWK { - /// Supported `alg` algorithms - public struct Algorithm: Codable, RawRepresentable, Equatable, Sendable { - enum Backing: String, Codable { - case rs256 = "RS256" - case rs384 = "RS384" - case rs512 = "RS512" - case ps256 = "PS256" - case ps384 = "PS384" - case ps512 = "PS512" - case es256 = "ES256" - case es384 = "ES384" - case es512 = "ES512" - case eddsa = "EdDSA" - } - - let backing: Backing - - public var rawValue: String { self.backing.rawValue } - - /// RSA with SHA256 - public static let rs256 = Self(backing: .rs256) - /// RSA with SHA384 - public static let rs384 = Self(backing: .rs384) - /// RSA with SHA512 - public static let rs512 = Self(backing: .rs512) - /// RSA-PSS with SHA256 - public static let ps256 = Self(backing: .ps256) - /// RSA-PSS with SHA384 - public static let ps384 = Self(backing: .ps384) - /// RSA-PSS with SHA512 - public static let ps512 = Self(backing: .ps512) - /// EC with SHA256 - public static let es256 = Self(backing: .es256) - /// EC with SHA384 - public static let es384 = Self(backing: .es384) - /// EC with SHA512 - public static let es512 = Self(backing: .es512) - /// EdDSA - public static let eddsa = Self(backing: .eddsa) - - init(backing: Backing) { - self.backing = backing - } - - public init?(rawValue: String) { - guard let backing = Backing(rawValue: rawValue) else { - return nil - } - self.init(backing: backing) - } - } -} - extension JWK { public struct Curve: Codable, RawRepresentable, Equatable, Sendable { enum Backing: Codable { @@ -246,10 +204,12 @@ extension JWK { } /// Represents an ECDSA curve. - public static func ecdsa(_ curve: ECDSACurve) -> Self { .init(.ecdsa(curve)) } + public static func ecdsa(_ curve: ECDSACurve) -> Self { .init(.ecdsa(curve)) + } /// Represents an EdDSA curve. - public static func eddsa(_ curve: EdDSACurve) -> Self { .init(.eddsa(curve)) } + public static func eddsa(_ curve: EdDSACurve) -> Self { .init(.eddsa(curve)) + } init(_ backing: Backing) { self.backing = backing @@ -286,4 +246,3 @@ extension JWK { } } } - diff --git a/Sources/JWTKit/JWK/JWKRepresentable.swift b/Sources/JWTKit/JWK/JWKRepresentable.swift index 9ed39c8d..3cc3e15e 100644 --- a/Sources/JWTKit/JWK/JWKRepresentable.swift +++ b/Sources/JWTKit/JWK/JWKRepresentable.swift @@ -5,7 +5,15 @@ #endif public protocol JWKRepresentable { - func toJWKRepresentation(kid: String?) -> JWK + func toJWKRepresentation( + keyIdentifier: JWKIdentifier?, + use: JWK.Usage?, + keyOperations: [JWK.KeyOperation]?, + x509URL: String?, + x509CertificateChain: [String]?, + x509SHA1Thumbprint: String?, + x509SHA256Thumbprint: String? + ) -> JWK } extension JWK {