Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions Sources/JWTKit/ECDSA/ECDSA.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Curve>: ECDSAKey, Equatable where Curve: ECDSACurveType {
typealias Signature = Curve.Signature
Expand Down Expand Up @@ -199,4 +207,32 @@ extension ECDSA {
lhs.parameters?.x == rhs.parameters?.x && lhs.parameters?.y == rhs.parameters?.y
}
}

public struct EnclavePrivateKey<EnclaveCurve, Curve>: 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<Curve> {
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
}
}
}
9 changes: 9 additions & 0 deletions Sources/JWTKit/ECDSA/ECDSACurveType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,12 @@ public protocol ECDSACurveType: Sendable {
static var curve: ECDSACurve { get }
static var byteRanges: (x: Range<Int>, y: Range<Int>) { get }
}

public protocol ECDSAEnclaveCurveType: Sendable {
associatedtype Signature: ECDSASignature
associatedtype PrivateKey: ECDSAEnclavePrivateKey
associatedtype SigningAlgorithm: ECDSASigningAlgorithm

static var curve: ECDSACurve { get }
static var byteRanges: (x: Range<Int>, y: Range<Int>) { get }
}
50 changes: 50 additions & 0 deletions Sources/JWTKit/ECDSA/ECDSAEnclaveSigner.swift
Original file line number Diff line number Diff line change
@@ -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<Key: ECDSAEnclaveKey>: JWTAlgorithm, CryptoSigner {
let privateKey: ECDSA.EnclavePrivateKey<Key.EnclaveCurve, Key.Curve>?
let publicKey: ECDSA.PublicKey<Key.Curve>
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<Key.EnclaveCurve, Key.Curve>:
self.privateKey = privateKey
self.publicKey = privateKey.publicKey
case let publicKey as ECDSA.PublicKey<Key.Curve>:
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)
}
}
11 changes: 7 additions & 4 deletions Sources/JWTKit/ECDSA/ECDSAKeyTypes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Bytes>(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
}

Expand Down
33 changes: 33 additions & 0 deletions Sources/JWTKit/ECDSA/JWTKeyCollection+ECDSA.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
}
}
23 changes: 23 additions & 0 deletions Sources/JWTKit/ECDSA/P256+CurveType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Int>, y: Range<Int>) = (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<P256>
public typealias ES256PrivateKey = ECDSA.PrivateKey<P256>
public typealias ES256EnclavePrivateKey = ECDSA.EnclavePrivateKey<SecureEnclave.P256, P256>
Loading