diff --git a/Sources/WalletSdk/Base64URL.swift b/Sources/WalletSdk/Base64URL.swift new file mode 100644 index 0000000..2b2be35 --- /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/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 new file mode 100644 index 0000000..2011039 --- /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" + } + } +} + +public protocol Jwk { + var jwkRepresentation: [String: String] { get } +} + +public protocol SigningKey { + func signature(data: Data) throws -> Data +} + +public protocol PrivateKey { + associatedtype PublicKeyKind: PublicKey + var publicKey: PublicKeyKind { get } +} + +public protocol PublicKey: Jwk { +} + +enum JwkParseError: Error { + case missingProperty(String) + case unknownProperty(String, String) + case base64Parse(String) + case wrongKeyLength +} + +public 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 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 yData.count != curve.keyComponentOctets { + return nil + } + + if dData.count != curve.keyComponentOctets { + return nil + } + + self.curve = curve + 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 + + self.curve = curve + self.xData = 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..478e6a2 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..2bc291f --- /dev/null +++ b/Sources/WalletSdk/SoftECKey.swift @@ -0,0 +1,99 @@ +import CryptoKit +import Foundation + +public class SoftECDSAPrivateSigningKey: PrivateKey, SigningKey, Jwk { + private var innerKey: P256.Signing.PrivateKey + var curve: Curve + var xData: Data + var yData: Data + var dData: Data + + public var publicKey: some ECDSAPublicVerifyingKey { + ECDSAPublicVerifyingKey(xData: xData, yData: yData, curve: curve) + } + + var x963Represention: Data { + self.innerKey.x963Representation + } + + public var jwkRepresentation: [String: String] { + return [ + "alg": Alg.ellipticCurve.jwaRepresentation, + "crv": curve.jwaRepresentation, + "x": xData.base64EncodedUrlSafe, + "y": yData.base64EncodedUrlSafe, + "d": dData.base64EncodedUrlSafe + ] + } + + @available(iOS 14, *) + public 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.xData = components.xData + self.yData = components.yData + self.dData = components.dData + } + + public 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 xData = try parseBase64Bytes(jwk: jwkRepresentation, propName: "x") + let yData = try parseBase64Bytes(jwk: jwkRepresentation, propName: "y") + let dData = try parseBase64Bytes(jwk: jwkRepresentation, propName: "d") + + guard let components = ECPrivateKeyComponents(curve: curve, xData: xData, yData: yData, dData: dData) else { + throw JwkParseError.wrongKeyLength + } + + self.innerKey = try P256.Signing.PrivateKey(x963Representation: components.x963Representation) + self.curve = curve + self.xData = xData + self.yData = yData + self.dData = dData + } + + public func signature(data: Data) throws -> Data { + try self.innerKey.signature(for: data).rawRepresentation + } +} + +public class ECDSAPublicVerifyingKey: PublicKey { + var xData: Data + var yData: Data + var curve: Curve + + init(xData: Data, yData: Data, curve: Curve) { + self.xData = xData + self.yData = yData + self.curve = curve + } + + public var jwkRepresentation: [String: String] { + return [ + "alg": Alg.ellipticCurve.jwaRepresentation, + "crv": curve.jwaRepresentation, + "x": xData.base64EncodedUrlSafe, + "y": yData.base64EncodedUrlSafe + ] + } +}