Skip to content

Commit

Permalink
Added migratory types with deprecations to guide users updating from …
Browse files Browse the repository at this point in the history
…version 4 to version 5
  • Loading branch information
dimitribouniol committed Nov 20, 2024
1 parent 13e7513 commit fba14c1
Show file tree
Hide file tree
Showing 9 changed files with 664 additions and 2 deletions.
203 changes: 203 additions & 0 deletions Sources/JWTKit/ECDSA/ECDSAKey.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Foundation
@_implementationOnly import CJWTKitBoringSSL
import Crypto

public final class ECDSAKey: OpenSSLKey {

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

@available(*, deprecated, message: "Unavailable in v5. Please use ES256PrivateKey(), ES384PrivateKey(), or ES512PrivateKey() instead.")
public static func generate(curve: Curve = .p521) throws -> ECDSAKey {
guard let c = CJWTKitBoringSSL_EC_KEY_new_by_curve_name(curve.cName) else {
throw JWTError.signingAlgorithmFailure(ECDSAError.newKeyByCurveFailure)
Expand All @@ -34,6 +36,7 @@ public final class ECDSAKey: OpenSSLKey {
///
/// - parameters:
/// - pem: Contents of pem file.
@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.")
public static func certificate(pem string: String) throws -> ECDSAKey {
try self.certificate(pem: [UInt8](string.utf8))
}
Expand All @@ -52,6 +55,7 @@ public final class ECDSAKey: OpenSSLKey {
///
/// - parameters:
/// - pem: Contents of pem file.
@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.")
public static func certificate<Data>(pem data: Data) throws -> ECDSAKey
where Data: DataProtocol
{
Expand All @@ -68,10 +72,12 @@ public final class ECDSAKey: OpenSSLKey {
return self.init(c)
}

@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.")
public static func `public`(pem string: String) throws -> ECDSAKey {
try .public(pem: [UInt8](string.utf8))
}

@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.")
public static func `public`<Data>(pem data: Data) throws -> ECDSAKey
where Data: DataProtocol
{
Expand All @@ -81,10 +87,12 @@ public final class ECDSAKey: OpenSSLKey {
return self.init(c)
}

@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.")
public static func `private`(pem string: String) throws -> ECDSAKey {
try .private(pem: [UInt8](string.utf8))
}

@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.")
public static func `private`<Data>(pem data: Data) throws -> ECDSAKey
where Data: DataProtocol
{
Expand All @@ -100,6 +108,7 @@ public final class ECDSAKey: OpenSSLKey {
self.c = c
}

@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.")
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)
Expand Down Expand Up @@ -190,3 +199,197 @@ extension ECDSAKey.Curve {
}
}
}

public protocol ECDSACurveType: Sendable {
static var curve: ECDSAKey.Curve { get }
}

extension P256: ECDSACurveType, @unchecked @retroactive Sendable {
static public var curve: ECDSAKey.Curve { .p256 }
}
public typealias ES256PublicKey = ECDSA.PublicKey<P256>
public typealias ES256PrivateKey = ECDSA.PrivateKey<P256>

extension P384: ECDSACurveType, @unchecked @retroactive Sendable {
static public var curve: ECDSAKey.Curve { .p384 }
}
public typealias ES384PublicKey = ECDSA.PublicKey<P384>
public typealias ES384PrivateKey = ECDSA.PrivateKey<P384>

extension P521: ECDSACurveType, @unchecked @retroactive Sendable {
static public var curve: ECDSAKey.Curve { .p521 }
}
public typealias ES512PublicKey = ECDSA.PublicKey<P521>
public typealias ES512PrivateKey = ECDSA.PrivateKey<P521>

public enum ECDSA: Sendable {
/// ECDSA.PublicKey was introduced in v5 and replaces ``ECDSAKey``.
///
/// - 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.
public struct PublicKey<Curve: ECDSACurveType> {
let key: ECDSAKey
init(key: ECDSAKey) { self.key = key }

public var curve: ECDSAKey.Curve? { key.curve }
public var parameters: ECDSAKey.Parameters? { key.parameters }

/// Creates an ``ECDSA.PublicKey`` instance from a PEM encoded certificate string.
///
/// - Parameter pem: The PEM encoded certificate string.
/// - Throws: If there is a problem parsing the certificate or deriving the public key.
/// - Returns: A new ``ECDSAKey`` instance with the public key from the certificate.
public init(certificate pem: String) throws {
key = try ECDSAKey.certificate(pem: pem)
}

/// Creates an ``ECDSA.PublicKey`` instance from a PEM encoded certificate data.
///
/// - Parameter pem: The PEM encoded certificate data.
/// - Throws: If there is a problem parsing the certificate or deriving the public key.
/// - Returns: A new ``ECDSA.PublicKey`` instance with the public key from the certificate.
public init<Data: DataProtocol>(certificate pem: Data) throws {
key = try ECDSAKey.certificate(pem: pem)
}

/// Creates an ``ECDSA.PublicKey`` instance from a PEM encoded public key string.
///
/// - Parameter pem: The PEM encoded public key string.
/// - Throws: If there is a problem parsing the public key.
/// - Returns: A new ``ECDSA.PublicKey`` instance with the public key from the certificate.
public init(pem string: String) throws {
key = try ECDSAKey.public(pem: string)
}

/// Creates an ``ECDSA.PublicKey`` instance from a PEM encoded public key data.
///
/// - Parameter pem: The PEM encoded public key data.
/// - Throws: If there is a problem parsing the public key.
/// - Returns: A new ``ECDSA.PublicKey`` instance with the public key from the certificate.
public init<Data: DataProtocol>(pem data: Data) throws {
key = try ECDSAKey.public(pem: data)
}

/// 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.
///
/// - 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 ``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.
public init(parameters: ECDSAKey.Parameters) throws {
key = try ECDSAKey(parameters: parameters, curve: Curve.curve, privateKey: nil)
}
}

/// ECDSA.PrivateKey was introduced in v5 and replaces ``ECDSAKey``.
///
/// - 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.
public struct PrivateKey<Curve: ECDSACurveType> {
let key: ECDSAKey
init(key: ECDSAKey) { self.key = key }

public var curve: ECDSAKey.Curve? { key.curve }
public var parameters: ECDSAKey.Parameters? { key.parameters }

/// Creates an ``ECDSA.PrivateKey`` instance from a PEM encoded private key string.
///
/// - Parameter pem: The PEM encoded private key string.
/// - Throws: If there is a problem parsing the private key.
/// - Returns: A new ``ECDSA.PrivateKey`` instance with the private key.
public init(pem string: String) throws {
key = try ECDSAKey.public(pem: string)
}

/// Creates an ``ECDSA.PrivateKey`` instance from a PEM encoded private key data.
///
/// - Parameter pem: The PEM encoded private key data.
/// - Throws: If there is a problem parsing the private key.
/// - Returns: A new ``ECDSA.PrivateKey`` instance with the private key.
public init<Data: DataProtocol>(pem data: Data) throws {
key = try ECDSAKey.public(pem: data)
}

/// Generates a new ECDSA key.
///
/// - Returns: A new ``ECDSA.PrivateKey`` instance with the generated key.
public init() {
key = try! ECDSAKey.generate(curve: Curve.curve)
}
}
}

extension ECDSA.PublicKey<P256> {
public init(backing: Curve.Signing.PublicKey) throws {
let representation = backing.rawRepresentation
try self.init(parameters: ECDSAKey.Parameters(
x: representation.prefix(representation.count/2).base64URLEncodedString(),
y: representation.suffix(representation.count/2).base64URLEncodedString()
))
}
}

extension ECDSA.PublicKey<P384> {
public init(backing: Curve.Signing.PublicKey) throws {
let representation = backing.rawRepresentation
try self.init(parameters: ECDSAKey.Parameters(
x: representation.prefix(representation.count/2).base64URLEncodedString(),
y: representation.suffix(representation.count/2).base64URLEncodedString()
))
}
}

extension ECDSA.PublicKey where Curve == P521 {
public init(backing: Curve.Signing.PublicKey) throws {
let representation = backing.rawRepresentation
try self.init(parameters: ECDSAKey.Parameters(
x: representation.prefix(representation.count/2).base64URLEncodedString(),
y: representation.suffix(representation.count/2).base64URLEncodedString()
))
}
}

extension ECDSA.PrivateKey<P256> {
public init(backing: Curve.Signing.PrivateKey) throws {
let representation = backing.publicKey.rawRepresentation
try self.init(key: ECDSAKey(
parameters: ECDSAKey.Parameters(
x: representation.prefix(representation.count/2).base64URLEncodedString(),
y: representation.suffix(representation.count/2).base64URLEncodedString()
),
curve: Curve.curve,
privateKey: backing.rawRepresentation.base64URLEncodedString()
))
}
}

extension ECDSA.PrivateKey<P384> {
public init(backing: Curve.Signing.PrivateKey) throws {
let representation = backing.publicKey.rawRepresentation
try self.init(key: ECDSAKey(
parameters: ECDSAKey.Parameters(
x: representation.prefix(representation.count/2).base64URLEncodedString(),
y: representation.suffix(representation.count/2).base64URLEncodedString()
),
curve: Curve.curve,
privateKey: backing.rawRepresentation.base64URLEncodedString()
))
}
}

extension ECDSA.PrivateKey<P521> {
public init(backing: Curve.Signing.PrivateKey) throws {
let representation = backing.publicKey.rawRepresentation
try self.init(key: ECDSAKey(
parameters: ECDSAKey.Parameters(
x: representation.prefix(representation.count/2).base64URLEncodedString(),
y: representation.suffix(representation.count/2).base64URLEncodedString()
),
curve: Curve.curve,
privateKey: backing.rawRepresentation.base64URLEncodedString()
))
}
}
70 changes: 70 additions & 0 deletions Sources/JWTKit/ECDSA/JWTSigner+ECDSA.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import class Foundation.JSONEncoder
import class Foundation.JSONDecoder

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

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

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

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

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

@available(*, deprecated, message: "Unavailable in v5. Please use JWTKeyCollection.add(ecdsa:kid:) instead.")
public static func es512(key: ECDSAKey, jsonEncoder: (any JWTJSONEncoder)?, jsonDecoder: (any JWTJSONDecoder)?) -> JWTSigner {
.init(algorithm: ECDSASigner(
key: key,
Expand All @@ -33,3 +39,67 @@ extension JWTSigner {
), jsonEncoder: jsonEncoder, jsonDecoder: jsonDecoder)
}
}

extension JWTKeyCollection {
/// Adds an ECDSA key to the collection.
///
/// Example Usage:
/// ```
/// let collection = await JWTKeyCollection()
/// .addECDSA(key: myECDSAKey)
/// ```
///
/// - Parameters:
/// - key: The ``ECDSAKey`` to be used for signing. This key should be securely stored and not exposed.
/// - kid: An optional ``JWKIdentifier`` (Key ID). If provided, this identifier will be used in the JWT `kid`
/// header field to identify the key.
/// - Returns: The same instance of the collection (`Self`), which allows for method chaining.
@discardableResult
public func add<T>(
ecdsa key: ECDSA.PublicKey<T>,
kid: JWKIdentifier? = nil
) -> Self {
switch key.curve {
case .p256:
try signers.use(.es256(key: key.key), kid: kid)
case .p384:
try signers.use(.es384(key: key.key), kid: kid)
case .p521:
try signers.use(.es512(key: key.key), kid: kid)
case .ed25519, .ed448, .none:
fatalError("Unsupported ECDSA key curve: \(key.curve?.rawValue ?? ".none")")
}
return self
}

/// Adds an ECDSA key to the collection.
///
/// Example Usage:
/// ```
/// let collection = await JWTKeyCollection()
/// .addECDSA(key: myECDSAKey)
/// ```
///
/// - Parameters:
/// - key: The ``ECDSAKey`` to be used for signing. This key should be securely stored and not exposed.
/// - kid: An optional ``JWKIdentifier`` (Key ID). If provided, this identifier will be used in the JWT `kid`
/// header field to identify the key.
/// - Returns: The same instance of the collection (`Self`), which allows for method chaining.
@discardableResult
public func add<T>(
ecdsa key: ECDSA.PrivateKey<T>,
kid: JWKIdentifier? = nil
) -> Self {
switch key.curve {
case .p256:
try signers.use(.es256(key: key.key), kid: kid)
case .p384:
try signers.use(.es384(key: key.key), kid: kid)
case .p521:
try signers.use(.es512(key: key.key), kid: kid)
case .ed25519, .ed448, .none:
fatalError("Unsupported ECDSA key curve: \(key.curve?.rawValue ?? ".none")")
}
return self
}
}
7 changes: 7 additions & 0 deletions Sources/JWTKit/JWTParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ struct JWTParser {
.decode(Payload.self, from: .init(self.encodedPayload.base64URLDecodedBytes()))
}

func payload<Payload>(as payload: Payload.Type, jsonDecoder: any JWTJSONDecoder) throws -> Payload
where Payload: AsyncJWTPayload
{
try jsonDecoder
.decode(Payload.self, from: .init(self.encodedPayload.base64URLDecodedBytes()))
}

func verify(using signer: JWTSigner) throws {
guard try signer.algorithm.verify(self.signature, signs: self.message) else {
throw JWTError.signatureVerifictionFailed
Expand Down
11 changes: 11 additions & 0 deletions Sources/JWTKit/JWTPayload.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
/// A JWT payload is a Publically Readable set of claims
/// Each variable represents a claim.
/// - 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.
public protocol JWTPayload: Codable {
/// Verifies that the payload's claims are correct or throws an error.
func verify(using signer: JWTSigner) throws
}

/// A transitionary protocol with sync and async requirements.
///
/// 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.
public protocol AsyncJWTPayload: Codable {
func verify<Algorithm: JWTAlgorithm>(using signer: Algorithm) throws

/// Verifies that the payload's claims are correct or throws an error.
func verify<Algorithm: JWTAlgorithm>(using algorithm: Algorithm) async throws
}
Loading

0 comments on commit fba14c1

Please sign in to comment.