Skip to content

Commit fba14c1

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

File tree

9 files changed

+664
-2
lines changed

9 files changed

+664
-2
lines changed

Sources/JWTKit/ECDSA/ECDSAKey.swift

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

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

Sources/JWTKit/JWTPayload.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
/// A JWT payload is a Publically Readable set of claims
22
/// Each variable represents a claim.
3+
/// - Warning: Requirements changed in v5 to be async. Please also conform to ``AsyncJWTPayload`` while on v4, and remove the ``AsyncJWTPayload`` conformance once you do update to v5.
34
public protocol JWTPayload: Codable {
45
/// Verifies that the payload's claims are correct or throws an error.
56
func verify(using signer: JWTSigner) throws
67
}
8+
9+
/// A transitionary protocol with sync and async requirements.
10+
///
11+
/// This protocol should be dropped once you are finished migrating to v5, as it'll have been renamed back to ``JWTPayload``, but with a single async requirement. In order to support both versions v4 and v5 in a library, do not implement the requirements of ``JWTPayload`` as ``JWTSigner`` is no longer available in v5.
12+
public protocol AsyncJWTPayload: Codable {
13+
func verify<Algorithm: JWTAlgorithm>(using signer: Algorithm) throws
14+
15+
/// Verifies that the payload's claims are correct or throws an error.
16+
func verify<Algorithm: JWTAlgorithm>(using algorithm: Algorithm) async throws
17+
}

0 commit comments

Comments
 (0)