Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add private key abstraction #6

Closed
wants to merge 2 commits into from
Closed
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
23 changes: 23 additions & 0 deletions Sources/WalletSdk/Base64URL.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
4 changes: 3 additions & 1 deletion Sources/WalletSdk/Credentials.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
141 changes: 141 additions & 0 deletions Sources/WalletSdk/Keys.swift
Original file line number Diff line number Diff line change
@@ -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..<xEnd)
self.yData = x963Representation.subdata(in: yOffset..<yEnd)
self.dData = x963Representation.subdata(in: dOffset..<dEnd)
}

var x963Representation: Data {
assert(self.xData.count == curve.keyComponentOctets)
assert(self.yData.count == curve.keyComponentOctets)
assert(self.dData.count == curve.keyComponentOctets)

var buffer = Data()

buffer.append(x963Header)
buffer.append(self.xData)
buffer.append(self.yData)
buffer.append(self.dData)

return buffer
}
}

internal func parseBase64Bytes(jwk: [String: String], propName: String) throws -> Data {
guard let base64String = jwk[propName] else {
throw JwkParseError.missingProperty(propName)
}

guard let data = Data(base64EncodedURLSafe: base64String) else {
throw JwkParseError.base64Parse(propName)
}

return data
}
39 changes: 6 additions & 33 deletions Sources/WalletSdk/MDoc.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
Expand All @@ -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<CFError>?
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 {
Expand Down
99 changes: 99 additions & 0 deletions Sources/WalletSdk/SoftECKey.swift
Original file line number Diff line number Diff line change
@@ -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
]
}
}
Loading