Skip to content

Commit 83afce0

Browse files
Added migratory types with deprecations to guide users updating from version 4 to version 5
1 parent 13e7513 commit 83afce0

File tree

9 files changed

+685
-2
lines changed

9 files changed

+685
-2
lines changed

Sources/JWTKit/ECDSA/ECDSAKey.swift

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import Foundation
22
@_implementationOnly import CJWTKitBoringSSL
3+
import Crypto
34

45
public final class ECDSAKey: OpenSSLKey {
56

@@ -11,6 +12,7 @@ public final class ECDSAKey: OpenSSLKey {
1112
case ed448 = "Ed448"
1213
}
1314

15+
@available(*, deprecated, message: "Unavailable in v5. Please use ES256PrivateKey(), ES384PrivateKey(), or ES512PrivateKey() instead.")
1416
public static func generate(curve: Curve = .p521) throws -> ECDSAKey {
1517
guard let c = CJWTKitBoringSSL_EC_KEY_new_by_curve_name(curve.cName) else {
1618
throw JWTError.signingAlgorithmFailure(ECDSAError.newKeyByCurveFailure)
@@ -34,6 +36,7 @@ public final class ECDSAKey: OpenSSLKey {
3436
///
3537
/// - parameters:
3638
/// - pem: Contents of pem file.
39+
@available(*, deprecated, message: "Unavailable in v5. Please use ES256PublicKey(certificate:), ES384PublicKey(certificate:), or ES512PublicKey(certificate:) instead. Note that more interfaces for importing keys is available once you update fully to v5.")
3740
public static func certificate(pem string: String) throws -> ECDSAKey {
3841
try self.certificate(pem: [UInt8](string.utf8))
3942
}
@@ -52,6 +55,7 @@ public final class ECDSAKey: OpenSSLKey {
5255
///
5356
/// - parameters:
5457
/// - pem: Contents of pem file.
58+
@available(*, deprecated, message: "Unavailable in v5. Please use ES256PublicKey(certificate:), ES384PublicKey(certificate:), or ES512PublicKey(certificate:) instead. Note that more interfaces for importing keys is available once you update fully to v5.")
5559
public static func certificate<Data>(pem data: Data) throws -> ECDSAKey
5660
where Data: DataProtocol
5761
{
@@ -68,10 +72,12 @@ public final class ECDSAKey: OpenSSLKey {
6872
return self.init(c)
6973
}
7074

75+
@available(*, deprecated, message: "Unavailable in v5. Please use ES256PublicKey(pem:), ES384PublicKey(pem:), or ES512PublicKey(pem:) instead. Note that more interfaces for importing keys is available once you update fully to v5.")
7176
public static func `public`(pem string: String) throws -> ECDSAKey {
7277
try .public(pem: [UInt8](string.utf8))
7378
}
7479

80+
@available(*, deprecated, message: "Unavailable in v5. Please use ES256PublicKey(pem:), ES384PublicKey(pem:), or ES512PublicKey(pem:) instead. Note that more interfaces for importing keys is available once you update fully to v5.")
7581
public static func `public`<Data>(pem data: Data) throws -> ECDSAKey
7682
where Data: DataProtocol
7783
{
@@ -81,10 +87,12 @@ public final class ECDSAKey: OpenSSLKey {
8187
return self.init(c)
8288
}
8389

90+
@available(*, deprecated, message: "Unavailable in v5. Please use ES256PrivateKey(pem:), ES384PrivateKey(pem:), or ES512PrivateKey(pem:) instead. Note that more interfaces for importing keys is available once you update fully to v5.")
8491
public static func `private`(pem string: String) throws -> ECDSAKey {
8592
try .private(pem: [UInt8](string.utf8))
8693
}
8794

95+
@available(*, deprecated, message: "Unavailable in v5. Please use ES256PrivateKey(pem:), ES384PrivateKey(pem:), or ES512PrivateKey(pem:) instead. Note that more interfaces for importing keys is available once you update fully to v5.")
8896
public static func `private`<Data>(pem data: Data) throws -> ECDSAKey
8997
where Data: DataProtocol
9098
{
@@ -100,6 +108,7 @@ public final class ECDSAKey: OpenSSLKey {
100108
self.c = c
101109
}
102110

111+
@available(*, deprecated, message: "Unavailable in v5. Please use ES256PublicKey(parameters:), ES384PublicKey(parameters:), or ES512PublicKey(parameters:) instead. Note that more interfaces for importing private keys is available once you update fully to v5.")
103112
public convenience init(parameters: Parameters, curve: Curve = .p521, privateKey: String? = nil) throws {
104113
guard let c = CJWTKitBoringSSL_EC_KEY_new_by_curve_name(curve.cName) else {
105114
throw JWTError.signingAlgorithmFailure(ECDSAError.newKeyByCurveFailure)
@@ -190,3 +199,218 @@ extension ECDSAKey.Curve {
190199
}
191200
}
192201
}
202+
203+
#if compiler(>=6)
204+
public protocol ECDSACurveType: Sendable {
205+
static var curve: ECDSAKey.Curve { get }
206+
}
207+
208+
extension P256: ECDSACurveType, @unchecked @retroactive Sendable {
209+
static public var curve: ECDSAKey.Curve { .p256 }
210+
}
211+
212+
extension P384: ECDSACurveType, @unchecked @retroactive Sendable {
213+
static public var curve: ECDSAKey.Curve { .p384 }
214+
}
215+
216+
extension P521: ECDSACurveType, @unchecked @retroactive Sendable {
217+
static public var curve: ECDSAKey.Curve { .p521 }
218+
}
219+
#else
220+
public protocol ECDSACurveType {
221+
static var curve: ECDSAKey.Curve { get }
222+
}
223+
224+
extension P256: ECDSACurveType {
225+
static public var curve: ECDSAKey.Curve { .p256 }
226+
}
227+
228+
extension P384: ECDSACurveType {
229+
static public var curve: ECDSAKey.Curve { .p384 }
230+
}
231+
232+
extension P521: ECDSACurveType {
233+
static public var curve: ECDSAKey.Curve { .p521 }
234+
}
235+
#endif
236+
237+
public typealias ES256PublicKey = ECDSA.PublicKey<P256>
238+
public typealias ES256PrivateKey = ECDSA.PrivateKey<P256>
239+
240+
public typealias ES384PublicKey = ECDSA.PublicKey<P384>
241+
public typealias ES384PrivateKey = ECDSA.PrivateKey<P384>
242+
243+
public typealias ES512PublicKey = ECDSA.PublicKey<P521>
244+
public typealias ES512PrivateKey = ECDSA.PrivateKey<P521>
245+
246+
public enum ECDSA {
247+
/// ECDSA.PublicKey was introduced in v5 and replaces ``ECDSAKey``.
248+
///
249+
/// - Note: Please migrate over to ``ECDSA/PublicKey`` before updating to v5, though if you plan on remaining on v4, ``ECDSAKey`` can continue to be used.
250+
public struct PublicKey<Curve: ECDSACurveType> {
251+
let key: ECDSAKey
252+
init(key: ECDSAKey) { self.key = key }
253+
254+
public var curve: ECDSAKey.Curve? { key.curve }
255+
public var parameters: ECDSAKey.Parameters? { key.parameters }
256+
257+
/// Creates an ``ECDSA.PublicKey`` instance from a PEM encoded certificate string.
258+
///
259+
/// - Parameter pem: The PEM encoded certificate string.
260+
/// - Throws: If there is a problem parsing the certificate or deriving the public key.
261+
/// - Returns: A new ``ECDSAKey`` instance with the public key from the certificate.
262+
public init(certificate pem: String) throws {
263+
key = try ECDSAKey.certificate(pem: pem)
264+
}
265+
266+
/// Creates an ``ECDSA.PublicKey`` instance from a PEM encoded certificate data.
267+
///
268+
/// - Parameter pem: The PEM encoded certificate data.
269+
/// - Throws: If there is a problem parsing the certificate or deriving the public key.
270+
/// - Returns: A new ``ECDSA.PublicKey`` instance with the public key from the certificate.
271+
public init<Data: DataProtocol>(certificate pem: Data) throws {
272+
key = try ECDSAKey.certificate(pem: pem)
273+
}
274+
275+
/// Creates an ``ECDSA.PublicKey`` instance from a PEM encoded public key string.
276+
///
277+
/// - Parameter pem: The PEM encoded public key string.
278+
/// - Throws: If there is a problem parsing the public key.
279+
/// - Returns: A new ``ECDSA.PublicKey`` instance with the public key from the certificate.
280+
public init(pem string: String) throws {
281+
key = try ECDSAKey.public(pem: string)
282+
}
283+
284+
/// Creates an ``ECDSA.PublicKey`` instance from a PEM encoded public key data.
285+
///
286+
/// - Parameter pem: The PEM encoded public key data.
287+
/// - Throws: If there is a problem parsing the public key.
288+
/// - Returns: A new ``ECDSA.PublicKey`` instance with the public key from the certificate.
289+
public init<Data: DataProtocol>(pem data: Data) throws {
290+
key = try ECDSAKey.public(pem: data)
291+
}
292+
293+
/// Initializes a new ``ECDSA.PublicKey` with ECDSA parameters.
294+
///
295+
/// - Parameters:
296+
/// - parameters: The ``ECDSAParameters`` tuple containing the x and y coordinates of the public key. These coordinates should be base64 URL encoded strings.
297+
///
298+
/// - Throws:
299+
/// - ``JWTError/generic`` with the identifier `ecCoordinates` if the x and y coordinates from `parameters` cannot be interpreted as base64 encoded data.
300+
/// - ``JWTError/generic`` with the identifier `ecPrivateKey` if the provided `privateKey` is non-nil but cannot be interpreted as a valid `PrivateKey`.
301+
///
302+
/// - Note:
303+
/// 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.
304+
public init(parameters: ECDSAKey.Parameters) throws {
305+
key = try ECDSAKey(parameters: parameters, curve: Curve.curve, privateKey: nil)
306+
}
307+
}
308+
309+
/// ECDSA.PrivateKey was introduced in v5 and replaces ``ECDSAKey``.
310+
///
311+
/// - Note: Please migrate over to ``ECDSA/PrivateKey`` before updating to v5, though if you plan on remaining on v4, ``ECDSAKey`` can continue to be used.
312+
public struct PrivateKey<Curve: ECDSACurveType> {
313+
let key: ECDSAKey
314+
init(key: ECDSAKey) { self.key = key }
315+
316+
public var curve: ECDSAKey.Curve? { key.curve }
317+
public var parameters: ECDSAKey.Parameters? { key.parameters }
318+
319+
/// Creates an ``ECDSA.PrivateKey`` instance from a PEM encoded private key string.
320+
///
321+
/// - Parameter pem: The PEM encoded private key string.
322+
/// - Throws: If there is a problem parsing the private key.
323+
/// - Returns: A new ``ECDSA.PrivateKey`` instance with the private key.
324+
public init(pem string: String) throws {
325+
key = try ECDSAKey.public(pem: string)
326+
}
327+
328+
/// Creates an ``ECDSA.PrivateKey`` instance from a PEM encoded private key data.
329+
///
330+
/// - Parameter pem: The PEM encoded private key data.
331+
/// - Throws: If there is a problem parsing the private key.
332+
/// - Returns: A new ``ECDSA.PrivateKey`` instance with the private key.
333+
public init<Data: DataProtocol>(pem data: Data) throws {
334+
key = try ECDSAKey.public(pem: data)
335+
}
336+
337+
/// Generates a new ECDSA key.
338+
///
339+
/// - Returns: A new ``ECDSA.PrivateKey`` instance with the generated key.
340+
public init() {
341+
key = try! ECDSAKey.generate(curve: Curve.curve)
342+
}
343+
}
344+
}
345+
346+
extension ECDSA.PublicKey<P256> {
347+
public init(backing: Curve.Signing.PublicKey) throws {
348+
let representation = backing.rawRepresentation
349+
try self.init(parameters: ECDSAKey.Parameters(
350+
x: representation.prefix(representation.count/2).base64URLEncodedString(),
351+
y: representation.suffix(representation.count/2).base64URLEncodedString()
352+
))
353+
}
354+
}
355+
356+
extension ECDSA.PublicKey<P384> {
357+
public init(backing: Curve.Signing.PublicKey) throws {
358+
let representation = backing.rawRepresentation
359+
try self.init(parameters: ECDSAKey.Parameters(
360+
x: representation.prefix(representation.count/2).base64URLEncodedString(),
361+
y: representation.suffix(representation.count/2).base64URLEncodedString()
362+
))
363+
}
364+
}
365+
366+
extension ECDSA.PublicKey where Curve == P521 {
367+
public init(backing: Curve.Signing.PublicKey) throws {
368+
let representation = backing.rawRepresentation
369+
try self.init(parameters: ECDSAKey.Parameters(
370+
x: representation.prefix(representation.count/2).base64URLEncodedString(),
371+
y: representation.suffix(representation.count/2).base64URLEncodedString()
372+
))
373+
}
374+
}
375+
376+
extension ECDSA.PrivateKey<P256> {
377+
public init(backing: Curve.Signing.PrivateKey) throws {
378+
let representation = backing.publicKey.rawRepresentation
379+
try self.init(key: ECDSAKey(
380+
parameters: ECDSAKey.Parameters(
381+
x: representation.prefix(representation.count/2).base64URLEncodedString(),
382+
y: representation.suffix(representation.count/2).base64URLEncodedString()
383+
),
384+
curve: Curve.curve,
385+
privateKey: backing.rawRepresentation.base64URLEncodedString()
386+
))
387+
}
388+
}
389+
390+
extension ECDSA.PrivateKey<P384> {
391+
public init(backing: Curve.Signing.PrivateKey) throws {
392+
let representation = backing.publicKey.rawRepresentation
393+
try self.init(key: ECDSAKey(
394+
parameters: ECDSAKey.Parameters(
395+
x: representation.prefix(representation.count/2).base64URLEncodedString(),
396+
y: representation.suffix(representation.count/2).base64URLEncodedString()
397+
),
398+
curve: Curve.curve,
399+
privateKey: backing.rawRepresentation.base64URLEncodedString()
400+
))
401+
}
402+
}
403+
404+
extension ECDSA.PrivateKey<P521> {
405+
public init(backing: Curve.Signing.PrivateKey) throws {
406+
let representation = backing.publicKey.rawRepresentation
407+
try self.init(key: ECDSAKey(
408+
parameters: ECDSAKey.Parameters(
409+
x: representation.prefix(representation.count/2).base64URLEncodedString(),
410+
y: representation.suffix(representation.count/2).base64URLEncodedString()
411+
),
412+
curve: Curve.curve,
413+
privateKey: backing.rawRepresentation.base64URLEncodedString()
414+
))
415+
}
416+
}

Sources/JWTKit/ECDSA/JWTSigner+ECDSA.swift

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ import class Foundation.JSONEncoder
33
import class Foundation.JSONDecoder
44

55
extension JWTSigner {
6+
@available(*, deprecated, message: "Unavailable in v5. Please use JWTKeyCollection.add(ecdsa:kid:) instead.")
67
public static func es256(key: ECDSAKey) -> JWTSigner { .es256(key: key, jsonEncoder: nil, jsonDecoder: nil) }
78

9+
@available(*, deprecated, message: "Unavailable in v5. Please use JWTKeyCollection.add(ecdsa:kid:) instead.")
810
public static func es256(key: ECDSAKey, jsonEncoder: (any JWTJSONEncoder)?, jsonDecoder: (any JWTJSONDecoder)?) -> JWTSigner {
911
.init(algorithm: ECDSASigner(
1012
key: key,
@@ -13,8 +15,10 @@ extension JWTSigner {
1315
), jsonEncoder: jsonEncoder, jsonDecoder: jsonDecoder)
1416
}
1517

18+
@available(*, deprecated, message: "Unavailable in v5. Please use JWTKeyCollection.add(ecdsa:kid:) instead.")
1619
public static func es384(key: ECDSAKey) -> JWTSigner { .es384(key: key, jsonEncoder: nil, jsonDecoder: nil) }
1720

21+
@available(*, deprecated, message: "Unavailable in v5. Please use JWTKeyCollection.add(ecdsa:kid:) instead.")
1822
public static func es384(key: ECDSAKey, jsonEncoder: (any JWTJSONEncoder)?, jsonDecoder: (any JWTJSONDecoder)?) -> JWTSigner {
1923
.init(algorithm: ECDSASigner(
2024
key: key,
@@ -23,8 +27,10 @@ extension JWTSigner {
2327
), jsonEncoder: jsonEncoder, jsonDecoder: jsonDecoder)
2428
}
2529

30+
@available(*, deprecated, message: "Unavailable in v5. Please use JWTKeyCollection.add(ecdsa:kid:) instead.")
2631
public static func es512(key: ECDSAKey) -> JWTSigner { .es512(key: key, jsonEncoder: nil, jsonDecoder: nil) }
2732

33+
@available(*, deprecated, message: "Unavailable in v5. Please use JWTKeyCollection.add(ecdsa:kid:) instead.")
2834
public static func es512(key: ECDSAKey, jsonEncoder: (any JWTJSONEncoder)?, jsonDecoder: (any JWTJSONDecoder)?) -> JWTSigner {
2935
.init(algorithm: ECDSASigner(
3036
key: key,
@@ -33,3 +39,67 @@ extension JWTSigner {
3339
), jsonEncoder: jsonEncoder, jsonDecoder: jsonDecoder)
3440
}
3541
}
42+
43+
extension JWTKeyCollection {
44+
/// Adds an ECDSA key to the collection.
45+
///
46+
/// Example Usage:
47+
/// ```
48+
/// let collection = await JWTKeyCollection()
49+
/// .addECDSA(key: myECDSAKey)
50+
/// ```
51+
///
52+
/// - Parameters:
53+
/// - key: The ``ECDSAKey`` to be used for signing. This key should be securely stored and not exposed.
54+
/// - kid: An optional ``JWKIdentifier`` (Key ID). If provided, this identifier will be used in the JWT `kid`
55+
/// header field to identify the key.
56+
/// - Returns: The same instance of the collection (`Self`), which allows for method chaining.
57+
@discardableResult
58+
public func add<T>(
59+
ecdsa key: ECDSA.PublicKey<T>,
60+
kid: JWKIdentifier? = nil
61+
) -> Self {
62+
switch key.curve {
63+
case .p256:
64+
try signers.use(.es256(key: key.key), kid: kid)
65+
case .p384:
66+
try signers.use(.es384(key: key.key), kid: kid)
67+
case .p521:
68+
try signers.use(.es512(key: key.key), kid: kid)
69+
case .ed25519, .ed448, .none:
70+
fatalError("Unsupported ECDSA key curve: \(key.curve?.rawValue ?? ".none")")
71+
}
72+
return self
73+
}
74+
75+
/// Adds an ECDSA key to the collection.
76+
///
77+
/// Example Usage:
78+
/// ```
79+
/// let collection = await JWTKeyCollection()
80+
/// .addECDSA(key: myECDSAKey)
81+
/// ```
82+
///
83+
/// - Parameters:
84+
/// - key: The ``ECDSAKey`` to be used for signing. This key should be securely stored and not exposed.
85+
/// - kid: An optional ``JWKIdentifier`` (Key ID). If provided, this identifier will be used in the JWT `kid`
86+
/// header field to identify the key.
87+
/// - Returns: The same instance of the collection (`Self`), which allows for method chaining.
88+
@discardableResult
89+
public func add<T>(
90+
ecdsa key: ECDSA.PrivateKey<T>,
91+
kid: JWKIdentifier? = nil
92+
) -> Self {
93+
switch key.curve {
94+
case .p256:
95+
try signers.use(.es256(key: key.key), kid: kid)
96+
case .p384:
97+
try signers.use(.es384(key: key.key), kid: kid)
98+
case .p521:
99+
try signers.use(.es512(key: key.key), kid: kid)
100+
case .ed25519, .ed448, .none:
101+
fatalError("Unsupported ECDSA key curve: \(key.curve?.rawValue ?? ".none")")
102+
}
103+
return self
104+
}
105+
}

Sources/JWTKit/JWTParser.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@ struct JWTParser {
3030
.decode(Payload.self, from: .init(self.encodedPayload.base64URLDecodedBytes()))
3131
}
3232

33+
func payload<Payload>(as payload: Payload.Type, jsonDecoder: any JWTJSONDecoder) throws -> Payload
34+
where Payload: AsyncJWTPayload
35+
{
36+
try jsonDecoder
37+
.decode(Payload.self, from: .init(self.encodedPayload.base64URLDecodedBytes()))
38+
}
39+
3340
func verify(using signer: JWTSigner) throws {
3441
guard try signer.algorithm.verify(self.signature, signs: self.message) else {
3542
throw JWTError.signatureVerifictionFailed

0 commit comments

Comments
 (0)