From c0dbc51295292dd6b8feceaafe3e76802f776fa3 Mon Sep 17 00:00:00 2001 From: Tim Wilbrink Date: Mon, 3 May 2021 21:55:20 +0200 Subject: [PATCH 1/2] Adding CAN as possible key to PACE. Not falling back to BAC if PACE was tried with CAN, this is not possible. --- Sources/NFCPassportReader/PACEHandler.swift | 40 ++++++++++++++----- .../NFCPassportReader/PassportReader.swift | 37 +++++++++++------ 2 files changed, 54 insertions(+), 23 deletions(-) diff --git a/Sources/NFCPassportReader/PACEHandler.swift b/Sources/NFCPassportReader/PACEHandler.swift index f8302ff2..3d8ebe4a 100644 --- a/Sources/NFCPassportReader/PACEHandler.swift +++ b/Sources/NFCPassportReader/PACEHandler.swift @@ -37,9 +37,9 @@ extension PACEHandlerError: LocalizedError { @available(iOS 13, *) public class PACEHandler { - - private static let MRZ_PACE_KEY_REFERENCE : UInt8 = 0x01 - private static let CAN_PACE_KEY_REFERENCE : UInt8 = 0x02 // Not currently supported + public static let NO_PACE_KEY_REFERENCE : UInt8 = 0x00 + public static let MRZ_PACE_KEY_REFERENCE : UInt8 = 0x01 + public static let CAN_PACE_KEY_REFERENCE : UInt8 = 0x02 private static let PIN_PACE_KEY_REFERENCE : UInt8 = 0x03 // Not currently supported private static let CUK_PACE_KEY_REFERENCE : UInt8 = 0x04 // Not currently supported @@ -72,7 +72,7 @@ public class PACEHandler { isPACESupported = true } - public func doPACE( mrzKey : String, completed: @escaping (Bool)->() ) { + public func doPACE( paceKeySeed : String, paceKeyReference: UInt8, completed: @escaping (Bool)->() ) { guard isPACESupported else { completed( false ) return @@ -92,9 +92,18 @@ public class PACEHandler { digestAlg = try paceInfo.getDigestAlgorithm() // Either SHA-1 or SHA-256. keyLength = try paceInfo.getKeyLength() // Get key length the enc cipher. Either 128, 192, or 256. - paceKeyType = PACEHandler.MRZ_PACE_KEY_REFERENCE - paceKey = try createPaceKey( from: mrzKey ) - + switch paceKeyReference { + case PACEHandler.MRZ_PACE_KEY_REFERENCE: + paceKeyType = PACEHandler.MRZ_PACE_KEY_REFERENCE + paceKey = try createPaceKey( from: paceKeySeed ) + case PACEHandler.CAN_PACE_KEY_REFERENCE: + paceKeyType = PACEHandler.CAN_PACE_KEY_REFERENCE + paceKey = try createPaceCanKey( from: paceKeySeed ) + default: + return handleError( "PACE", "PACE Key Reference not supported (\(paceKeyReference)). Currently only 0x01 (MRZ) and 0x02 (CAN) may be used." ) + } + + // Temporary logging Log.verbose("doPace - inpit parameters" ) Log.verbose("paceOID - \(paceOID)" ) @@ -104,15 +113,14 @@ public class PACEHandler { Log.verbose("cipherAlg - \(cipherAlg)" ) Log.verbose("digestAlg - \(digestAlg)" ) Log.verbose("keyLength - \(keyLength)" ) - Log.verbose("keyLength - \(mrzKey)" ) + Log.verbose("paceKeyReference - \(paceKeyReference)" ) Log.verbose("paceKey - \(binToHexRep(paceKey, asArray:true))" ) // First start the initial auth call tagReader.sendMSESetATMutualAuth(oid: paceOID, keyType: paceKeyType, completed: { [unowned self] response, error in if let error = error { return handleError( "MSESatATMutualAuth", "Error - \(error.localizedDescription)" ) - } - + } self.doStep1() }) @@ -637,10 +645,22 @@ extension PACEHandler { let buf: [UInt8] = Array(mrzKey.utf8) let hash = calcSHA1Hash(buf) + let smskg = SecureMessagingSessionKeyGenerator() let key = try smskg.deriveKey(keySeed: hash, cipherAlgName: cipherAlg, keyLength: keyLength, nonce: nil, mode: .PACE_MODE, paceKeyReference: paceKeyType) return key } + + /// Computes a key seed based on an CAN key + /// - Parameter the CAN key + /// - Returns a encoded key based on the CAN key that can be used for PACE + func createPaceCanKey( from canKey: String ) throws -> [UInt8] { + var buf: [UInt8] = Array(canKey.utf8) + + let smskg = SecureMessagingSessionKeyGenerator() + let key = try smskg.deriveKey(keySeed: buf, cipherAlgName: cipherAlg, keyLength: keyLength, nonce: nil, mode: .PACE_MODE, paceKeyReference: paceKeyType) + return key + } /// Performs the ECDH PACE GM key agreement protocol by multiplying a private key with a public key /// - Parameters: diff --git a/Sources/NFCPassportReader/PassportReader.swift b/Sources/NFCPassportReader/PassportReader.swift index 4d06d07d..1484d3e4 100644 --- a/Sources/NFCPassportReader/PassportReader.swift +++ b/Sources/NFCPassportReader/PassportReader.swift @@ -27,7 +27,8 @@ public class PassportReader : NSObject { private var bacHandler : BACHandler? private var caHandler : ChipAuthenticationHandler? private var paceHandler : PACEHandler? - private var mrzKey : String = "" + private var accessKey : String = "" + private var paceKeyReference : UInt8? private var dataAmountToReadOverride : Int? = nil private var scanCompletedHandler: ((NFCPassportModel?, NFCPassportReaderError?)->())! @@ -58,9 +59,10 @@ public class PassportReader : NSObject { dataAmountToReadOverride = amount } - public func readPassport( mrzKey : String, tags: [DataGroupId] = [], skipSecureElements :Bool = true, customDisplayMessage: ((NFCViewDisplayMessage) -> String?)? = nil, completed: @escaping (NFCPassportModel?, NFCPassportReaderError?)->()) { + public func readPassport( accessKey : String, paceKeyReference: UInt8, tags: [DataGroupId] = [], skipSecureElements :Bool = true, customDisplayMessage: ((NFCViewDisplayMessage) -> String?)? = nil, completed: @escaping (NFCPassportModel?, NFCPassportReaderError?)->()) { self.passport = NFCPassportModel() - self.mrzKey = mrzKey + self.accessKey = accessKey + self.paceKeyReference = paceKeyReference self.dataGroupsToRead.removeAll() self.dataGroupsToRead.append( contentsOf:tags) @@ -212,7 +214,7 @@ extension PassportReader { print( "Error reading CardAccess - \(error)" ) } } - + // If we managed to read the CardAccess the PACE should be supported otherwise drop back to BAC if let cardAccess = ca { passport.cardAccess = cardAccess @@ -226,12 +228,12 @@ extension PassportReader { } func doPACEAuthentication(cardAccess:CardAccess) { - self.handlePACE(cardAccess:cardAccess, completed: { [weak self] error in + self.handlePACE(cardAccess:cardAccess, paceKeyReference: paceKeyReference ?? PACEHandler.NO_PACE_KEY_REFERENCE, completed: { [weak self] error in if error == nil { Log.info( "PACE Successful" ) self?.passport.PACEStatus = .success - // At this point, BAC Has been done and the TagReader has been set up with the SecureMessaging + // At this point, PACE has been done and the TagReader has been set up with the SecureMessaging // session keys self?.tagReader?.selectPassportApplication(completed: { response, error in @@ -241,10 +243,18 @@ extension PassportReader { } else if let error = error { Log.info( "PACE Failed - \(error.localizedDescription)" ) self?.passport.PACEStatus = .failed - self?.tagReader?.selectPassportApplication(completed: { response, error in - self?.doBACAuthentication() - }) -// let displayMessage = NFCViewDisplayMessage.error(error) + + // Try BAC after failed PACE attempt. But only if MRZ is used as Key. BAC does not support CAN or similar. + // If BAC is no option, return an error + if (self?.paceKeyReference == PACEHandler.NO_PACE_KEY_REFERENCE || self?.paceKeyReference == PACEHandler.MRZ_PACE_KEY_REFERENCE) { + self?.tagReader?.selectPassportApplication(completed: { response, error in + self?.doBACAuthentication() + }) + } else { + let displayMessage = NFCViewDisplayMessage.error(error) + self?.invalidateSession(errorMessage: displayMessage, error: error) + } + // let displayMessage = NFCViewDisplayMessage.error(error) // self?.invalidateSession(errorMessage: displayMessage, error: error) } }) @@ -344,13 +354,13 @@ extension PassportReader { Log.info( "Starting Basic Access Control (BAC)" ) self.bacHandler = BACHandler( tagReader: tagReader ) - bacHandler?.performBACAndGetSessionKeys( mrzKey: mrzKey ) { error in + bacHandler?.performBACAndGetSessionKeys( mrzKey: accessKey ) { error in self.bacHandler = nil completed(error) } } - func handlePACE( cardAccess:CardAccess, completed: @escaping (NFCPassportReaderError?)->()) { + func handlePACE( cardAccess:CardAccess, paceKeyReference: UInt8 ,completed: @escaping (NFCPassportReaderError?)->()) { guard let tagReader = self.tagReader else { completed(NFCPassportReaderError.NoConnectedTag) return @@ -360,7 +370,8 @@ extension PassportReader { do { self.paceHandler = try PACEHandler( cardAccess: cardAccess, tagReader: tagReader ) - paceHandler?.doPACE(mrzKey: mrzKey ) { paceSucceeded in + + paceHandler?.doPACE(paceKeySeed: accessKey, paceKeyReference: paceKeyReference) { paceSucceeded in if paceSucceeded { completed(nil) } else { From 6c25bdc9be9211090bfafa4f410792a08eb46655 Mon Sep 17 00:00:00 2001 From: Tim Wilbrink Date: Mon, 3 May 2021 22:23:23 +0200 Subject: [PATCH 2/2] Use CAN if entered preferred over MRZ --- .../project.pbxproj | 4 ++ .../Model/SettingsStore.swift | 9 ++- .../Views/CANEntryView.swift | 59 +++++++++++++++++++ .../NFCPassportReaderApp/Views/MainView.swift | 15 ++++- 4 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 Example/NFCPassportReaderApp/Views/CANEntryView.swift diff --git a/Example/NFCPassportReaderApp.xcodeproj/project.pbxproj b/Example/NFCPassportReaderApp.xcodeproj/project.pbxproj index 8e483842..b85cc270 100644 --- a/Example/NFCPassportReaderApp.xcodeproj/project.pbxproj +++ b/Example/NFCPassportReaderApp.xcodeproj/project.pbxproj @@ -35,6 +35,7 @@ A1FDC53225D3F19E00D22FF4 /* ViewModifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1FDC53125D3F19E00D22FF4 /* ViewModifiers.swift */; }; A1FDC53425D3F1DE00D22FF4 /* CheckBoxView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1FDC53325D3F1DE00D22FF4 /* CheckBoxView.swift */; }; A1FDC53625D43AB400D22FF4 /* MRZEntryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1FDC53525D43AB400D22FF4 /* MRZEntryView.swift */; }; + F6BB3B65264092C8008E30E7 /* CANEntryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6BB3B64264092C8008E30E7 /* CANEntryView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -87,6 +88,7 @@ A7BD003B33886ED0F3F03325 /* Pods_NFCPassportReaderApp.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_NFCPassportReaderApp.framework; sourceTree = BUILT_PRODUCTS_DIR; }; B239DD4BD3AA9ECF4F595D22 /* Pods-NFCPassportReaderApp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NFCPassportReaderApp.release.xcconfig"; path = "Target Support Files/Pods-NFCPassportReaderApp/Pods-NFCPassportReaderApp.release.xcconfig"; sourceTree = ""; }; DA39DA603B1A7EBD8FC38112 /* Pods_NFCPassportReaderAppTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_NFCPassportReaderAppTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + F6BB3B64264092C8008E30E7 /* CANEntryView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CANEntryView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -194,6 +196,7 @@ A1298C9B25D539CD00F5713E /* HelperViews */, A17134BD22C8EF7100C457C3 /* MainView.swift */, A1FDC53525D43AB400D22FF4 /* MRZEntryView.swift */, + F6BB3B64264092C8008E30E7 /* CANEntryView.swift */, A1FDC52D25D3F15D00D22FF4 /* SettingsView.swift */, A1816E9B22C9059F00F546A0 /* PassportView.swift */, A1C234C125DD19DE003FFD79 /* PassportSummaryView.swift */, @@ -404,6 +407,7 @@ A1816E9C22C9059F00F546A0 /* PassportView.swift in Sources */, A1769DF725D3E318006002D1 /* SettingsStore.swift in Sources */, A1298C9F25D53A5C00F5713E /* ViewExt.swift in Sources */, + F6BB3B65264092C8008E30E7 /* CANEntryView.swift in Sources */, A182DE6325DD730D00341204 /* FileManagerExt.swift in Sources */, A1FDC52E25D3F15D00D22FF4 /* SettingsView.swift in Sources */, A182DE6125DD6F1300341204 /* StoredPassportView.swift in Sources */, diff --git a/Example/NFCPassportReaderApp/Model/SettingsStore.swift b/Example/NFCPassportReaderApp/Model/SettingsStore.swift index a27e4ef4..3d1ba8e8 100644 --- a/Example/NFCPassportReaderApp/Model/SettingsStore.swift +++ b/Example/NFCPassportReaderApp/Model/SettingsStore.swift @@ -20,8 +20,9 @@ final class SettingsStore: ObservableObject { static let passportNumber = "passportNumber" static let dateOfBirth = "dateOfBirth" static let dateOfExpiry = "dateOfExpiry" + static let cardAccessNumber = "can" - static let allVals = [captureLog, logLevel, useNewVerification, passportNumber, dateOfBirth, dateOfExpiry] + static let allVals = [captureLog, logLevel, useNewVerification, passportNumber, dateOfBirth, dateOfExpiry, cardAccessNumber] } private let cancellable: Cancellable @@ -41,6 +42,7 @@ final class SettingsStore: ObservableObject { Keys.passportNumber: "", Keys.dateOfBirth: Date().timeIntervalSince1970, Keys.dateOfExpiry: Date().timeIntervalSince1970, + Keys.cardAccessNumber: "", ]) cancellable = NotificationCenter.default @@ -102,6 +104,11 @@ final class SettingsStore: ObservableObject { return d } } + + var cardAccessNumber: String { + set { defaults.set(newValue, forKey: Keys.cardAccessNumber) } + get { defaults.string(forKey: Keys.cardAccessNumber) ?? "" } + } @Published var passport : NFCPassportModel? } diff --git a/Example/NFCPassportReaderApp/Views/CANEntryView.swift b/Example/NFCPassportReaderApp/Views/CANEntryView.swift new file mode 100644 index 00000000..dd9a6280 --- /dev/null +++ b/Example/NFCPassportReaderApp/Views/CANEntryView.swift @@ -0,0 +1,59 @@ +// +// CANEntryView.swift +// NFCPassportReaderApp +// +// Created by Tim Wilbrink on 23.04.21. +// + +import SwiftUI + +struct CANEntryView : View { + @EnvironmentObject var settings: SettingsStore + + // These will be removed once DatePicker inline works correctly + @State private var editDOB = false + @State private var editDOE = false + @State private var editDateTitle : String = "" + + var body : some View { + let canBinding = Binding(get: { + settings.cardAccessNumber + }, set: { + settings.cardAccessNumber = $0.uppercased() + }) + VStack { + + TextField("CAN", text: canBinding) + .textCase(.uppercase) + .modifier(ClearButton(text: canBinding)) + .textContentType(.name) + .foregroundColor(Color.primary) + .padding([.leading, .trailing]) + .ignoresSafeArea(.keyboard, edges: .all) + + Divider() + + + } + .ignoresSafeArea(.keyboard, edges: .bottom) + } +} + + +#if DEBUG +struct CANEntryView_Previews : PreviewProvider { + + static var previews: some View { + let settings = SettingsStore() + + return + Group { + NavigationView { + CANEntryView() + } + .environmentObject(settings) + .environment( \.colorScheme, .light) + } + } +} +#endif diff --git a/Example/NFCPassportReaderApp/Views/MainView.swift b/Example/NFCPassportReaderApp/Views/MainView.swift index 2e07d46f..c9b725fc 100644 --- a/Example/NFCPassportReaderApp/Views/MainView.swift +++ b/Example/NFCPassportReaderApp/Views/MainView.swift @@ -52,6 +52,8 @@ struct MainView : View { }.padding([.top, .trailing]) } MRZEntryView() + Divider() + CANEntryView() Button(action: { self.scanPassport() @@ -132,6 +134,8 @@ extension MainView { let dob = df.string(from:settings.dateOfBirth) let doe = df.string(from:settings.dateOfExpiry) + let can = settings.cardAccessNumber + let passportUtils = PassportUtils() let mrzKey = passportUtils.getMRZKey( passportNumber: pptNr, dateOfBirth: dob, dateOfExpiry: doe) @@ -149,9 +153,16 @@ extension MainView { Log.logLevel = settings.logLevel Log.storeLogs = settings.shouldCaptureLogs Log.clearStoredLogs() - + + var accessKey = mrzKey + var paceKeyReference : UInt8 = PACEHandler.MRZ_PACE_KEY_REFERENCE + if (can != "") { + accessKey = can + paceKeyReference = PACEHandler.CAN_PACE_KEY_REFERENCE + } + // This is also how you can override the default messages displayed by the NFC View Controller - passportReader.readPassport(mrzKey: mrzKey, customDisplayMessage: { (displayMessage) in + passportReader.readPassport(accessKey: accessKey, paceKeyReference: paceKeyReference, customDisplayMessage: { (displayMessage) in switch displayMessage { case .requestPresentPassport: return "Hold your iPhone near an NFC enabled passport."