diff --git a/Sources/JWTKit/ECDSA/ECDSA.swift b/Sources/JWTKit/ECDSA/ECDSA.swift index 885a9a3a..58cb1fb4 100644 --- a/Sources/JWTKit/ECDSA/ECDSA.swift +++ b/Sources/JWTKit/ECDSA/ECDSA.swift @@ -16,6 +16,14 @@ public protocol ECDSAKey: Sendable { var parameters: ECDSAParameters? { get } } +public protocol ECDSAEnclaveKey: Sendable { + associatedtype EnclaveCurve: ECDSAEnclaveCurveType + associatedtype Curve: ECDSACurveType + + var curve: ECDSACurve { get } + var parameters: ECDSAParameters? { get } +} + extension ECDSA { public struct PublicKey: ECDSAKey, Equatable where Curve: ECDSACurveType { typealias Signature = Curve.Signature @@ -199,4 +207,32 @@ extension ECDSA { lhs.parameters?.x == rhs.parameters?.x && lhs.parameters?.y == rhs.parameters?.y } } + + public struct EnclavePrivateKey: ECDSAEnclaveKey, Equatable where EnclaveCurve: ECDSAEnclaveCurveType, Curve: ECDSACurveType { + public typealias PrivateKey = EnclaveCurve.PrivateKey + typealias Signature = PrivateKey.Signature + + public private(set) var curve: ECDSACurve = EnclaveCurve.curve + + public var parameters: ECDSAParameters? { + self.publicKey.parameters + } + + var backing: PrivateKey + + public var publicKey: PublicKey { + try! .init(backing: self.backing.publicKey) + } + + /// Creates an ``ECDSA.EnclavePrivateKey`` instance from SwiftCrypto SecureEnclave PrivateKey. + /// + /// - Parameter backing: The SwiftCrypto SecureEnclave PrivateKey. + public init(backing: PrivateKey) { + self.backing = backing + } + + public static func == (lhs: Self, rhs: Self) -> Bool { + lhs.parameters?.x == rhs.parameters?.x && lhs.parameters?.y == rhs.parameters?.y + } + } } diff --git a/Sources/JWTKit/ECDSA/ECDSACurveType.swift b/Sources/JWTKit/ECDSA/ECDSACurveType.swift index 9ddafd43..678e4cfc 100644 --- a/Sources/JWTKit/ECDSA/ECDSACurveType.swift +++ b/Sources/JWTKit/ECDSA/ECDSACurveType.swift @@ -24,3 +24,12 @@ public protocol ECDSACurveType: Sendable { static var curve: ECDSACurve { get } static var byteRanges: (x: Range, y: Range) { get } } + +public protocol ECDSAEnclaveCurveType: Sendable { + associatedtype Signature: ECDSASignature + associatedtype PrivateKey: ECDSAEnclavePrivateKey + associatedtype SigningAlgorithm: ECDSASigningAlgorithm + + static var curve: ECDSACurve { get } + static var byteRanges: (x: Range, y: Range) { get } +} diff --git a/Sources/JWTKit/ECDSA/ECDSAEnclaveSigner.swift b/Sources/JWTKit/ECDSA/ECDSAEnclaveSigner.swift new file mode 100644 index 00000000..12a51412 --- /dev/null +++ b/Sources/JWTKit/ECDSA/ECDSAEnclaveSigner.swift @@ -0,0 +1,50 @@ +// +// ECDSAEnclaveSigner.swift +// jwt-kit +// +// Created by Jeff Seibert on 10/21/25. +// + + +#if !canImport(Darwin) +import FoundationEssentials +#else +import Foundation +#endif + +struct ECDSAEnclaveSigner: JWTAlgorithm, CryptoSigner { + let privateKey: ECDSA.EnclavePrivateKey? + let publicKey: ECDSA.PublicKey + let algorithm: DigestAlgorithm = Key.Curve.SigningAlgorithm.digestAlgorithm + let name: String = Key.Curve.SigningAlgorithm.name + + init(key: Key) { + switch key { + case let privateKey as ECDSA.EnclavePrivateKey: + self.privateKey = privateKey + self.publicKey = privateKey.publicKey + case let publicKey as ECDSA.PublicKey: + self.publicKey = publicKey + self.privateKey = nil + default: + // This should never happen + fatalError("Unexpected key type: \(type(of: key))") + } + } + + func sign(_ plaintext: some DataProtocol) throws -> [UInt8] { + let digest = try self.digest(plaintext) + guard let privateKey else { + throw JWTError.signingAlgorithmFailure(ECDSAError.noPrivateKey) + } + let signature = try privateKey.backing.signature(for: digest) + return [UInt8](signature.rawRepresentation) + } + + public func verify(_ signature: some DataProtocol, signs plaintext: some DataProtocol) throws + -> Bool + { + let digest = try self.digest(plaintext) + return try publicKey.backing.isValidSignature(signature, for: digest) + } +} diff --git a/Sources/JWTKit/ECDSA/ECDSAKeyTypes.swift b/Sources/JWTKit/ECDSA/ECDSAKeyTypes.swift index abef0991..505299d7 100644 --- a/Sources/JWTKit/ECDSA/ECDSAKeyTypes.swift +++ b/Sources/JWTKit/ECDSA/ECDSAKeyTypes.swift @@ -25,19 +25,22 @@ public protocol ECDSASignature: Sendable { var rawRepresentation: Data { get set } } -public protocol ECDSAPrivateKey: Sendable { - associatedtype PublicKey: ECDSAPublicKey - associatedtype Signature: ECDSASignature +public protocol ECDSAPrivateKey: ECDSAEnclavePrivateKey { init(compactRepresentable: Bool) init(x963Representation: some ContiguousBytes) throws init(rawRepresentation: some ContiguousBytes) throws init(pemRepresentation: String) throws init(derRepresentation: Bytes) throws where Bytes: RandomAccessCollection, Bytes.Element == UInt8 - var publicKey: PublicKey { get } var rawRepresentation: Data { get } var x963Representation: Data { get } var derRepresentation: Data { get } var pemRepresentation: String { get } +} + +public protocol ECDSAEnclavePrivateKey: Sendable { + associatedtype PublicKey: ECDSAPublicKey + associatedtype Signature: ECDSASignature + var publicKey: PublicKey { get } func signature(for data: some Digest) throws -> Signature } diff --git a/Sources/JWTKit/ECDSA/JWTKeyCollection+ECDSA.swift b/Sources/JWTKit/ECDSA/JWTKeyCollection+ECDSA.swift index 1c7d1bb5..7f17f8a1 100644 --- a/Sources/JWTKit/ECDSA/JWTKeyCollection+ECDSA.swift +++ b/Sources/JWTKit/ECDSA/JWTKeyCollection+ECDSA.swift @@ -33,4 +33,37 @@ extension JWTKeyCollection { ), for: kid ) } + + /// 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. + /// - jsonEncoder: An optional custom JSON encoder conforming to ``JWTJSONEncoder``, used for encoding JWTs. + /// If `nil`, a default encoder is used. + /// - jsonDecoder: An optional custom JSON decoder conforming to ``JWTJSONDecoder``, used for decoding JWTs. + /// If `nil`, a default decoder is used. + /// - Returns: The same instance of the collection (`Self`), which allows for method chaining. + @discardableResult + public func add( + ecdsa key: some ECDSAEnclaveKey, + kid: JWKIdentifier? = nil, + parser: some JWTParser = DefaultJWTParser(), + serializer: some JWTSerializer = DefaultJWTSerializer() + ) -> Self { + add( + .init( + algorithm: ECDSAEnclaveSigner(key: key), + parser: parser, + serializer: serializer + ), for: kid + ) + } } diff --git a/Sources/JWTKit/ECDSA/P256+CurveType.swift b/Sources/JWTKit/ECDSA/P256+CurveType.swift index 117b5b20..737b0523 100644 --- a/Sources/JWTKit/ECDSA/P256+CurveType.swift +++ b/Sources/JWTKit/ECDSA/P256+CurveType.swift @@ -41,8 +41,31 @@ extension P256.Signing.PublicKey: ECDSAPublicKey { } } +extension SecureEnclave.P256: ECDSAEnclaveCurveType { + public typealias Signature = P256.Signing.ECDSASignature + public typealias PrivateKey = SecureEnclave.P256.Signing.PrivateKey + + public static let curve: ECDSACurve = .p256 + + /// Specifies the byte ranges in which the X and Y coordinates of an ECDSA public key appear for the P256 curve. + /// For P256, the public key is typically 65 bytes long: a single byte prefix (usually 0x04 for uncompressed keys), followed by + /// 32 bytes for the X coordinate, and then 32 bytes for the Y coordinate. + /// + /// Thus: + /// - The X coordinate spans bytes 1 through 32 (byte 0 is for the prefix). + /// - The Y coordinate spans bytes 33 through 64. + public static let byteRanges: (x: Range, y: Range) = (1..<33, 33..<65) + + public struct SigningAlgorithm: ECDSASigningAlgorithm { + public static let name = "ES256" + public static let digestAlgorithm: DigestAlgorithm = .sha256 + } +} + extension P256.Signing.PrivateKey: ECDSAPrivateKey {} extension P256.Signing.ECDSASignature: ECDSASignature {} +extension SecureEnclave.P256.Signing.PrivateKey: ECDSAEnclavePrivateKey {} public typealias ES256PublicKey = ECDSA.PublicKey public typealias ES256PrivateKey = ECDSA.PrivateKey +public typealias ES256EnclavePrivateKey = ECDSA.EnclavePrivateKey