From 76710937175cabe8ae152c186af4320cee5a5ed0 Mon Sep 17 00:00:00 2001 From: Tristan Miller Date: Fri, 1 Mar 2024 08:52:53 -0700 Subject: [PATCH 1/2] Add private key abstraction --- Sources/WalletSdk/Base64URL.swift | 23 +++++ Sources/WalletSdk/Keys.swift | 141 ++++++++++++++++++++++++++++++ Sources/WalletSdk/MDoc.swift | 41 ++------- Sources/WalletSdk/SoftECKey.swift | 98 +++++++++++++++++++++ 4 files changed, 269 insertions(+), 34 deletions(-) create mode 100644 Sources/WalletSdk/Base64URL.swift create mode 100644 Sources/WalletSdk/Keys.swift create mode 100644 Sources/WalletSdk/SoftECKey.swift diff --git a/Sources/WalletSdk/Base64URL.swift b/Sources/WalletSdk/Base64URL.swift new file mode 100644 index 0000000..4b33a84 --- /dev/null +++ b/Sources/WalletSdk/Base64URL.swift @@ -0,0 +1,23 @@ +import Foundation + +extension Data { + var base64EncodedUrlSafe: String { + let string = self.base64EncodedString() + + // Make this URL safe and remove padding + return string + .replacingOccurrences(of: "+", with: "-") + .replacingOccurrences(of: "/", with: "_") + .replacingOccurrences(of: "=", with: "") + } +} + +extension Data { + init?(base64EncodedURLSafe string: String, options: Base64DecodingOptions = []) { + let string = string + .replacingOccurrences(of: "-", with: "+") + .replacingOccurrences(of: "_", with: "/") + + self.init(base64Encoded: string, options: options) + } +} diff --git a/Sources/WalletSdk/Keys.swift b/Sources/WalletSdk/Keys.swift new file mode 100644 index 0000000..6679859 --- /dev/null +++ b/Sources/WalletSdk/Keys.swift @@ -0,0 +1,141 @@ +import Foundation + +protocol Jwa { + var jwaRepresentation: String { get } +} + +enum Alg : Jwa { + case ellipticCurve + + var jwaRepresentation: String { + switch self { + case .ellipticCurve: + "EC" + } + } +} + +protocol Jwk { + var jwkRepresentation: [String: String] { get } +} + +protocol SigningKey { + func signature(data: Data) throws -> Data +} + +protocol PrivateKey { + associatedtype PublicKeyKind: PublicKey + var publicKey: PublicKeyKind { get } +} + +protocol PublicKey : Jwk { +} + +enum JwkParseError : Error { + case missingProperty(String) + case unknownProperty(String, String) + case base64Parse(String) + case wrongKeyLength +} + +enum Curve : Jwa { + case p256 + + init?(jwaRepresentation: String) { + switch jwaRepresentation { + case "P-256": + self = .p256 + default: + return nil + } + } + + var jwaRepresentation: String { + switch self { + case .p256: + return "P-256" + } + } + + var keyComponentOctets: Int { + switch self { + case .p256: + return 32 + } + } +} + +internal class ECPrivateKeyComponents { + var curve: Curve + var x: Data + var y: Data + var d: Data + + let x963Header: UInt8 = 0x04; + let x963HeaderLen: Int = 1; + + init?(curve: Curve, x: Data, y: Data, d: Data) { + if x.count != curve.keyComponentOctets { + return nil + } + + if y.count != curve.keyComponentOctets { + return nil + } + + if d.count != curve.keyComponentOctets { + return nil + } + + self.curve = curve + self.x = x + self.y = y + self.d = d + } + + init?(x963Representation: Data, curve: Curve) { + let x963BufSize = x963HeaderLen + (3 * curve.keyComponentOctets) + if x963Representation.count != x963BufSize { + return nil + } + + let xOffset = x963HeaderLen; + let xEnd = xOffset + curve.keyComponentOctets; + let yOffset = xEnd; + let yEnd = yOffset + curve.keyComponentOctets; + let dOffset = yEnd; + let dEnd = dOffset + curve.keyComponentOctets; + + self.curve = curve + self.x = x963Representation.subdata(in: xOffset.. Data { + guard let base64String = jwk[propName] else { + throw JwkParseError.missingProperty(propName) + } + + guard let data = Data(base64EncodedURLSafe: base64String) else { + throw JwkParseError.base64Parse(propName) + } + + return data +} diff --git a/Sources/WalletSdk/MDoc.swift b/Sources/WalletSdk/MDoc.swift index c889316..a81f701 100644 --- a/Sources/WalletSdk/MDoc.swift +++ b/Sources/WalletSdk/MDoc.swift @@ -9,14 +9,12 @@ public typealias ItemsRequest = SpruceIDWalletSdkRs.ItemsRequest public class MDoc: Credential { var inner: SpruceIDWalletSdkRs.MDoc - var keyAlias: String /// issuerAuth is the signed MSO (i.e. CoseSign1 with MSO as payload) /// namespaces is the full set of namespaces with data items and their value /// IssuerSignedItemBytes will be bytes, but its composition is defined here /// https://github.com/spruceid/isomdl/blob/f7b05dfa/src/definitions/issuer_signed.rs#L18 - public init?(fromMDoc issuerAuth: Data, namespaces: [Namespace: [IssuerSignedItemBytes]], keyAlias: String) { - self.keyAlias = keyAlias + public init?(fromMDoc issuerAuth: Data, namespaces: [Namespace: [IssuerSignedItemBytes]]) { do { try self.inner = SpruceIDWalletSdkRs.MDoc.fromCbor(value: issuerAuth) } catch { @@ -43,12 +41,14 @@ public class BLESessionManager { var sessionManager: SessionManager? var itemsRequests: [ItemsRequest]? var mdoc: MDoc + var privateKey: SigningKey var bleManager: MDocHolderBLECentral! - init?(mdoc: MDoc, engagement: DeviceEngagement, callback: BLESessionStateDelegate) { + init?(mdoc: MDoc, privateKey: SigningKey, engagement: DeviceEngagement, callback: BLESessionStateDelegate) { self.callback = callback self.uuid = UUID() self.mdoc = mdoc + self.privateKey = privateKey do { let sessionData = try SpruceIDWalletSdkRs.initialiseSession(document: mdoc.inner, uuid: self.uuid.uuidString) @@ -71,37 +71,10 @@ public class BLESessionManager { let responseData = try SpruceIDWalletSdkRs.submitResponse(sessionManager: sessionManager!, itemsRequests: itemsRequests!, permittedItems: items) - let query = [kSecClass: kSecClassKey, - kSecAttrApplicationLabel: self.mdoc.keyAlias, - kSecReturnRef: true] as [String: Any] - - // Find and cast the result as a SecKey instance. - var item: CFTypeRef? - var secKey: SecKey - switch SecItemCopyMatching(query as CFDictionary, &item) { - case errSecSuccess: - // swiftlint:disable force_cast - secKey = item as! SecKey - // swiftlint:enable force_cast - case errSecItemNotFound: - self.callback.update(state: .error("Key not found")) - self.cancel() - return - case let status: - self.callback.update(state: .error("Keychain read failed: \(status)")) - self.cancel() - return - } - var error: Unmanaged? - guard let data = SecKeyCopyExternalRepresentation(secKey, &error) as Data? else { - self.callback.update(state: .error("Failed to cast key: \(error.debugDescription)")) - self.cancel() - return - } - let privateKey = try P256.Signing.PrivateKey(x963Representation: data) - let signature = try privateKey.signature(for: responseData.payload) + + let signature = try self.privateKey.signature(data: responseData.payload) let signatureData = try SpruceIDWalletSdkRs.submitSignature(sessionManager: sessionManager!, - signature: signature.derRepresentation) + signature: signature) self.state = signatureData.state self.bleManager.writeOutgoingValue(data: signatureData.response) } catch { diff --git a/Sources/WalletSdk/SoftECKey.swift b/Sources/WalletSdk/SoftECKey.swift new file mode 100644 index 0000000..0c43724 --- /dev/null +++ b/Sources/WalletSdk/SoftECKey.swift @@ -0,0 +1,98 @@ +import CryptoKit +import Foundation + +public class SoftECDSAPrivateSigningKey: PrivateKey, SigningKey, Jwk { + private var innerKey: P256.Signing.PrivateKey + var curve: Curve + var x: Data + var y: Data + var d: Data + + var publicKey: some ECDSAPublicVerifyingKey { + ECDSAPublicVerifyingKey(x: x, y: y, curve: curve) + } + + var x963Represention: Data { + self.innerKey.x963Representation + } + + var jwkRepresentation: [String : String] { + return [ + "alg": Alg.ellipticCurve.jwaRepresentation, + "crv": curve.jwaRepresentation, + "x": x.base64EncodedUrlSafe, + "y": y.base64EncodedUrlSafe, + "d": d.base64EncodedUrlSafe, + ] + } + + @available(iOS 14, *) + init?(pkcs8Representation: String, curve: Curve) throws { + self.curve = curve + self.innerKey = try P256.Signing.PrivateKey(pemRepresentation: pkcs8Representation) + guard let components = ECPrivateKeyComponents(x963Representation: self.innerKey.x963Representation, curve: curve) else { + return nil + } + + self.x = components.x; + self.y = components.y; + self.d = components.d; + } + + init(jwkRepresentation: [String: String]) throws { + guard let alg = jwkRepresentation["alg"] else { + throw JwkParseError.missingProperty("alg") + } + + if alg != Alg.ellipticCurve.jwaRepresentation { + throw JwkParseError.unknownProperty("alg", alg) + } + + guard let crv = jwkRepresentation["crv"] else { + throw JwkParseError.missingProperty("crv") + } + + guard let curve = Curve.init(jwaRepresentation: crv) else { + throw JwkParseError.unknownProperty("crv", crv) + } + + let x = try parseBase64Bytes(jwk: jwkRepresentation, propName: "x"); + let y = try parseBase64Bytes(jwk: jwkRepresentation, propName: "y"); + let d = try parseBase64Bytes(jwk: jwkRepresentation, propName: "d"); + + guard let components = ECPrivateKeyComponents(curve: curve, x: x, y: y, d: d) else { + throw JwkParseError.wrongKeyLength + } + + self.innerKey = try P256.Signing.PrivateKey(x963Representation: components.x963Representation) + self.curve = curve + self.x = x + self.y = y + self.d = d + } + + func signature(data: Data) throws -> Data { + try self.innerKey.signature(for: data).rawRepresentation + } +} + +public class ECDSAPublicVerifyingKey: PublicKey { + var x: Data + var y: Data + var curve: Curve + + init(x: Data, y: Data, curve: Curve) { + self.x = x + self.y = y + self.curve = curve + } + + var jwkRepresentation: [String: String] { + return [ + "alg": Alg.ellipticCurve.jwaRepresentation, + "crv": curve.jwaRepresentation, + "x": x.base64EncodedUrlSafe, + "y": y.base64EncodedUrlSafe, + ] + } +} From 1cb0c51bc882df03b3f9ad3332628a0260e6ade8 Mon Sep 17 00:00:00 2001 From: Tristan Miller Date: Fri, 1 Mar 2024 09:42:12 -0700 Subject: [PATCH 2/2] Formatting --- Sources/WalletSdk/Base64URL.swift | 2 +- Sources/WalletSdk/Credentials.swift | 4 +- Sources/WalletSdk/Keys.swift | 108 ++++++++++++++-------------- Sources/WalletSdk/MDoc.swift | 2 +- Sources/WalletSdk/SoftECKey.swift | 89 +++++++++++------------ 5 files changed, 104 insertions(+), 101 deletions(-) diff --git a/Sources/WalletSdk/Base64URL.swift b/Sources/WalletSdk/Base64URL.swift index 4b33a84..2b2be35 100644 --- a/Sources/WalletSdk/Base64URL.swift +++ b/Sources/WalletSdk/Base64URL.swift @@ -3,7 +3,7 @@ import Foundation extension Data { var base64EncodedUrlSafe: String { let string = self.base64EncodedString() - + // Make this URL safe and remove padding return string .replacingOccurrences(of: "+", with: "-") diff --git a/Sources/WalletSdk/Credentials.swift b/Sources/WalletSdk/Credentials.swift index 3fd16dd..62df6ce 100644 --- a/Sources/WalletSdk/Credentials.swift +++ b/Sources/WalletSdk/Credentials.swift @@ -9,11 +9,13 @@ public class CredentialStore { // swiftlint:disable force_cast public func presentMdocBLE(deviceEngagement: DeviceEngagement, + privateKey: SigningKey, callback: BLESessionStateDelegate // , trustedReaders: TrustedReaders ) -> BLESessionManager? { if let firstMdoc = self.credentials.first(where: {$0 is MDoc}) { - return BLESessionManager(mdoc: firstMdoc as! MDoc, engagement: DeviceEngagement.QRCode, callback: callback) + return BLESessionManager(mdoc: firstMdoc as! MDoc, privateKey: privateKey, + engagement: DeviceEngagement.QRCode, callback: callback) } else { return nil } diff --git a/Sources/WalletSdk/Keys.swift b/Sources/WalletSdk/Keys.swift index 6679859..2011039 100644 --- a/Sources/WalletSdk/Keys.swift +++ b/Sources/WalletSdk/Keys.swift @@ -4,9 +4,9 @@ protocol Jwa { var jwaRepresentation: String { get } } -enum Alg : Jwa { +enum Alg: Jwa { case ellipticCurve - + var jwaRepresentation: String { switch self { case .ellipticCurve: @@ -15,32 +15,32 @@ enum Alg : Jwa { } } -protocol Jwk { +public protocol Jwk { var jwkRepresentation: [String: String] { get } } -protocol SigningKey { +public protocol SigningKey { func signature(data: Data) throws -> Data } -protocol PrivateKey { +public protocol PrivateKey { associatedtype PublicKeyKind: PublicKey var publicKey: PublicKeyKind { get } } -protocol PublicKey : Jwk { +public protocol PublicKey: Jwk { } -enum JwkParseError : Error { +enum JwkParseError: Error { case missingProperty(String) case unknownProperty(String, String) case base64Parse(String) case wrongKeyLength } -enum Curve : Jwa { +public enum Curve: Jwa { case p256 - + init?(jwaRepresentation: String) { switch jwaRepresentation { case "P-256": @@ -56,7 +56,7 @@ enum Curve : Jwa { return "P-256" } } - + var keyComponentOctets: Int { switch self { case .p256: @@ -67,64 +67,64 @@ enum Curve : Jwa { internal class ECPrivateKeyComponents { var curve: Curve - var x: Data - var y: Data - var d: Data - - let x963Header: UInt8 = 0x04; - let x963HeaderLen: Int = 1; - - init?(curve: Curve, x: Data, y: Data, d: Data) { - if x.count != curve.keyComponentOctets { + var xData: Data + var yData: Data + var dData: Data + + let x963Header: UInt8 = 0x04 + let x963HeaderLen: Int = 1 + + init?(curve: Curve, xData: Data, yData: Data, dData: Data) { + if xData.count != curve.keyComponentOctets { return nil } - - if y.count != curve.keyComponentOctets { + + if yData.count != curve.keyComponentOctets { return nil } - - if d.count != curve.keyComponentOctets { + + if dData.count != curve.keyComponentOctets { return nil } - + self.curve = curve - self.x = x - self.y = y - self.d = d + self.xData = xData + self.yData = yData + self.dData = dData } - + init?(x963Representation: Data, curve: Curve) { let x963BufSize = x963HeaderLen + (3 * curve.keyComponentOctets) if x963Representation.count != x963BufSize { return nil } - - let xOffset = x963HeaderLen; - let xEnd = xOffset + curve.keyComponentOctets; - let yOffset = xEnd; - let yEnd = yOffset + curve.keyComponentOctets; - let dOffset = yEnd; - let dEnd = dOffset + curve.keyComponentOctets; - + + let xOffset = x963HeaderLen + let xEnd = xOffset + curve.keyComponentOctets + let yOffset = xEnd + let yEnd = yOffset + curve.keyComponentOctets + let dOffset = yEnd + let dEnd = dOffset + curve.keyComponentOctets + self.curve = curve - self.x = x963Representation.subdata(in: xOffset.. Data { + + public func signature(data: Data) throws -> Data { try self.innerKey.signature(for: data).rawRepresentation } } public class ECDSAPublicVerifyingKey: PublicKey { - var x: Data - var y: Data + var xData: Data + var yData: Data var curve: Curve - - init(x: Data, y: Data, curve: Curve) { - self.x = x - self.y = y + + init(xData: Data, yData: Data, curve: Curve) { + self.xData = xData + self.yData = yData self.curve = curve } - var jwkRepresentation: [String: String] { + public var jwkRepresentation: [String: String] { return [ "alg": Alg.ellipticCurve.jwaRepresentation, "crv": curve.jwaRepresentation, - "x": x.base64EncodedUrlSafe, - "y": y.base64EncodedUrlSafe, + "x": xData.base64EncodedUrlSafe, + "y": yData.base64EncodedUrlSafe ] } }