nfc reader

This commit is contained in:
0xturboblitz
2023-10-10 14:46:20 +02:00
parent 82bcf3efce
commit 815fd9434b
23 changed files with 1215 additions and 0 deletions

View File

@@ -0,0 +1,193 @@
//
// BACHandler.swift
// NFCTest
//
// Created by Andy Qua on 07/06/2019.
// Copyright © 2019 Andy Qua. All rights reserved.
//
import Foundation
#if !os(macOS)
import CoreNFC
@available(iOS 15, *)
public class BACHandler {
let KENC : [UInt8] = [0,0,0,1]
let KMAC : [UInt8] = [0,0,0,2]
public var ksenc : [UInt8] = []
public var ksmac : [UInt8] = []
var rnd_icc : [UInt8] = []
var rnd_ifd : [UInt8] = []
public var kifd : [UInt8] = []
var tagReader : TagReader?
public init() {
// For testing only
}
public init(tagReader: TagReader) {
self.tagReader = tagReader
}
public func performBACAndGetSessionKeys( mrzKey : String ) async throws {
guard let tagReader = self.tagReader else {
throw NFCPassportReaderError.NoConnectedTag
}
Log.debug( "BACHandler - deriving Document Basic Access Keys" )
_ = try self.deriveDocumentBasicAccessKeys(mrz: mrzKey)
// Make sure we clear secure messaging (could happen if we read an invalid DG or we hit a secure error
tagReader.secureMessaging = nil
// get Challenge
Log.debug( "BACHandler - Getting initial challenge" )
let response = try await tagReader.getChallenge()
Log.verbose( "DATA - \(response.data)" )
Log.debug( "BACHandler - Doing mutual authentication" )
let cmd_data = self.authentication(rnd_icc: [UInt8](response.data))
let maResponse = try await tagReader.doMutualAuthentication(cmdData: Data(cmd_data))
Log.debug( "DATA - \(maResponse.data)" )
guard maResponse.data.count > 0 else {
throw NFCPassportReaderError.InvalidMRZKey
}
let (KSenc, KSmac, ssc) = try self.sessionKeys(data: [UInt8](maResponse.data))
tagReader.secureMessaging = SecureMessaging(ksenc: KSenc, ksmac: KSmac, ssc: ssc)
Log.debug( "BACHandler - complete" )
}
func deriveDocumentBasicAccessKeys(mrz: String) throws -> ([UInt8], [UInt8]) {
let kseed = generateInitialKseed(kmrz:mrz)
Log.verbose("Calculate the Basic Access Keys (Kenc and Kmac) using TR-SAC 1.01, 4.2")
let smskg = SecureMessagingSessionKeyGenerator()
self.ksenc = try smskg.deriveKey(keySeed: kseed, mode: .ENC_MODE)
self.ksmac = try smskg.deriveKey(keySeed: kseed, mode: .MAC_MODE)
return (ksenc, ksmac)
}
///
/// Calculate the kseed from the kmrz:
/// - Calculate a SHA-1 hash of the kmrz
/// - Take the most significant 16 bytes to form the Kseed.
/// @param kmrz: The MRZ information
/// @type kmrz: a string
/// @return: a 16 bytes string
///
/// - Parameter kmrz: mrz key
/// - Returns: first 16 bytes of the mrz SHA1 hash
///
func generateInitialKseed(kmrz : String ) -> [UInt8] {
Log.verbose("Calculate the SHA-1 hash of MRZ_information")
Log.verbose("\tMRZ KEY - \(kmrz)")
let hash = calcSHA1Hash( [UInt8](kmrz.data(using:.utf8)!) )
Log.verbose("\tsha1(MRZ_information): \(binToHexRep(hash))")
let subHash = Array(hash[0..<16])
Log.verbose("Take the most significant 16 bytes to form the Kseed")
Log.verbose("\tKseed: \(binToHexRep(subHash))" )
return Array(subHash)
}
/// Construct the command data for the mutual authentication.
/// - Request an 8 byte random number from the MRTD's chip (rnd.icc)
/// - Generate an 8 byte random (rnd.ifd) and a 16 byte random (kifd)
/// - Concatenate rnd.ifd, rnd.icc and kifd (s = rnd.ifd + rnd.icc + kifd)
/// - Encrypt it with TDES and the Kenc key (eifd = TDES(s, Kenc))
/// - Compute the MAC over eifd with TDES and the Kmax key (mifd = mac(pad(eifd))
/// - Construct the APDU data for the mutualAuthenticate command (cmd_data = eifd + mifd)
///
/// @param rnd_icc: The challenge received from the ICC.
/// @type rnd_icc: A 8 bytes binary string
/// @return: The APDU binary data for the mutual authenticate command
func authentication( rnd_icc : [UInt8]) -> [UInt8] {
self.rnd_icc = rnd_icc
Log.verbose("Request an 8 byte random number from the MRTD's chip")
Log.verbose("\tRND.ICC: " + binToHexRep(self.rnd_icc))
self.rnd_icc = rnd_icc
let rnd_ifd = generateRandomUInt8Array(8)
let kifd = generateRandomUInt8Array(16)
Log.verbose("Generate an 8 byte random and a 16 byte random")
Log.verbose("\tRND.IFD: \(binToHexRep(rnd_ifd))" )
Log.verbose("\tRND.Kifd: \(binToHexRep(kifd))")
let s = rnd_ifd + rnd_icc + kifd
Log.verbose("Concatenate RND.IFD, RND.ICC and Kifd")
Log.verbose("\tS: \(binToHexRep(s))")
let iv : [UInt8] = [0, 0, 0, 0, 0, 0, 0, 0]
let eifd = tripleDESEncrypt(key: ksenc,message: s, iv: iv)
Log.verbose("Encrypt S with TDES key Kenc as calculated in Appendix 5.2")
Log.verbose("\tEifd: \(binToHexRep(eifd))")
let mifd = mac(algoName: .DES, key: ksmac, msg: pad(eifd, blockSize:8))
Log.verbose("Compute MAC over eifd with TDES key Kmac as calculated in-Appendix 5.2")
Log.verbose("\tMifd: \(binToHexRep(mifd))")
// Construct APDU
let cmd_data = eifd + mifd
Log.verbose("Construct command data for MUTUAL AUTHENTICATE")
Log.verbose("\tcmd_data: \(binToHexRep(cmd_data))")
self.rnd_ifd = rnd_ifd
self.kifd = kifd
return cmd_data
}
/// Calculate the session keys (KSenc, KSmac) and the SSC from the data
/// received by the mutual authenticate command.
/// @param data: the data received from the mutual authenticate command send to the chip.
/// @type data: a binary string
/// @return: A set of two 16 bytes keys (KSenc, KSmac) and the SSC
public func sessionKeys(data : [UInt8] ) throws -> ([UInt8], [UInt8], [UInt8]) {
Log.verbose("Decrypt and verify received data and compare received RND.IFD with generated RND.IFD \(binToHexRep(self.ksmac))" )
let response = tripleDESDecrypt(key: self.ksenc, message: [UInt8](data[0..<32]), iv: [0,0,0,0,0,0,0,0] )
let response_kicc = [UInt8](response[16..<32])
let Kseed = xor(self.kifd, response_kicc)
Log.verbose("Calculate XOR of Kifd and Kicc")
Log.verbose("\tKseed: \(binToHexRep(Kseed))" )
let smskg = SecureMessagingSessionKeyGenerator()
let KSenc = try smskg.deriveKey(keySeed: Kseed, mode: .ENC_MODE)
let KSmac = try smskg.deriveKey(keySeed: Kseed, mode: .MAC_MODE)
// let KSenc = self.keyDerivation(kseed: Kseed,c: KENC)
// let KSmac = self.keyDerivation(kseed: Kseed,c: KMAC)
Log.verbose("Calculate Session Keys (KSenc and KSmac) using Appendix 5.1")
Log.verbose("\tKSenc: \(binToHexRep(KSenc))" )
Log.verbose("\tKSmac: \(binToHexRep(KSmac))" )
let ssc = [UInt8](self.rnd_icc.suffix(4) + self.rnd_ifd.suffix(4))
Log.verbose("Calculate Send Sequence Counter")
Log.verbose("\tSSC: \(binToHexRep(ssc))" )
return (KSenc, KSmac, ssc)
}
}
#endif

View File

@@ -0,0 +1,223 @@
//
// ChipAuthenticationHandler.swift
// NFCPassportReader
//
// Created by Andy Qua on 25/02/2021.
//
import Foundation
import OpenSSL
#if !os(macOS)
import CoreNFC
import CryptoKit
@available(iOS 15, *)
class ChipAuthenticationHandler {
private static let NO_PACE_KEY_REFERENCE : UInt8 = 0x00
private static let ENC_MODE : UInt8 = 0x1
private static let MAC_MODE : UInt8 = 0x2
private static let PACE_MODE : UInt8 = 0x3
private static let COMMAND_CHAINING_CHUNK_SIZE = 224
var tagReader : TagReader?
var gaSegments = [[UInt8]]()
var chipAuthInfos = [Int:ChipAuthenticationInfo]()
var chipAuthPublicKeyInfos = [ChipAuthenticationPublicKeyInfo]()
var isChipAuthenticationSupported : Bool = false
public init(dg14 : DataGroup14, tagReader: TagReader) {
self.tagReader = tagReader
for secInfo in dg14.securityInfos {
if let cai = secInfo as? ChipAuthenticationInfo {
let keyId = cai.getKeyId()
chipAuthInfos[keyId] = cai
} else if let capki = secInfo as? ChipAuthenticationPublicKeyInfo {
chipAuthPublicKeyInfos.append(capki)
}
}
if chipAuthPublicKeyInfos.count > 0 {
isChipAuthenticationSupported = true
}
}
public func doChipAuthentication() async throws {
Log.info( "Performing Chip Authentication - number of public keys found - \(chipAuthPublicKeyInfos.count)" )
guard isChipAuthenticationSupported else {
throw NFCPassportReaderError.NotYetSupported( "ChipAuthentication not supported" )
}
var success = false
for pubKey in chipAuthPublicKeyInfos {
do {
success = try await self.doChipAuthentication( with: pubKey)
if success {
break
}
} catch {
// try next key
}
}
if !success {
throw NFCPassportReaderError.ChipAuthenticationFailed
}
}
private func doChipAuthentication( with chipAuthPublicKeyInfo : ChipAuthenticationPublicKeyInfo ) async throws -> Bool {
// So it turns out that some passports don't have ChipAuthInfo items.
// So if we do have a ChipAuthInfo the we take the keyId (if present) and OID from there,
// BUT if we don't then we will try to infer the OID from the public key
let keyId = chipAuthPublicKeyInfo.keyId
let chipAuthInfoOID : String
if let chipAuthInfo = chipAuthInfos[keyId ?? 0] {
chipAuthInfoOID = chipAuthInfo.oid
} else {
if let oid = inferOID( fromPublicKeyOID:chipAuthPublicKeyInfo.oid) {
chipAuthInfoOID = oid
} else {
return false
}
}
try await self.doCA( keyId: keyId, encryptionDetailsOID: chipAuthInfoOID, publicKey: chipAuthPublicKeyInfo.pubKey )
return true
}
/// Infer OID from public key type - Best guess seems to be to use 3DES_CBC_CBC for both ECDH and DH keys
/// Apparently works for French passports
private func inferOID(fromPublicKeyOID: String ) -> String? {
if fromPublicKeyOID == SecurityInfo.ID_PK_ECDH_OID {
Log.warning("No ChipAuthenticationInfo - guessing its id-CA-ECDH-3DES-CBC-CBC");
return SecurityInfo.ID_CA_ECDH_3DES_CBC_CBC_OID
} else if fromPublicKeyOID == SecurityInfo.ID_PK_DH_OID {
Log.warning("No ChipAuthenticationInfo - guessing its id-CA-DH-3DES-CBC-CBC");
return SecurityInfo.ID_CA_DH_3DES_CBC_CBC_OID
}
Log.warning("No ChipAuthenticationInfo and unsupported ChipAuthenticationPublicKeyInfo public key OID \(fromPublicKeyOID)")
return nil;
}
private func doCA( keyId: Int?, encryptionDetailsOID oid: String, publicKey: OpaquePointer) async throws {
// Generate Ephemeral Keypair from parameters from DG14 Public key
// This should work for both EC and DH keys
var ephemeralKeyPair : OpaquePointer? = nil
let pctx = EVP_PKEY_CTX_new(publicKey, nil)
EVP_PKEY_keygen_init(pctx)
EVP_PKEY_keygen(pctx, &ephemeralKeyPair)
EVP_PKEY_CTX_free(pctx)
// Send the public key to the passport
try await sendPublicKey(oid: oid, keyId: keyId, pcdPublicKey: ephemeralKeyPair!)
Log.debug( "Public Key successfully sent to passport!" )
// Use our ephemeral private key and the passports public key to generate a shared secret
// (the passport with do the same thing with their private key and our public key)
let sharedSecret = OpenSSLUtils.computeSharedSecret(privateKeyPair:ephemeralKeyPair!, publicKey:publicKey)
// Now try to restart Secure Messaging using the new shared secret and
try restartSecureMessaging( oid : oid, sharedSecret : sharedSecret, maxTranceiveLength : 1, shouldCheckMAC : true)
}
private func sendPublicKey(oid : String, keyId : Int?, pcdPublicKey : OpaquePointer) async throws {
let cipherAlg = try ChipAuthenticationInfo.toCipherAlgorithm(oid: oid)
guard let keyData = OpenSSLUtils.getPublicKeyData(from: pcdPublicKey) else {
throw NFCPassportReaderError.InvalidDataPassed("Unable to get public key data from public key" )
}
if cipherAlg.hasPrefix("DESede") {
var idData : [UInt8] = []
if let keyId = keyId {
idData = intToBytes( val:keyId, removePadding:true)
idData = wrapDO( b:0x84, arr:idData)
}
let wrappedKeyData = wrapDO( b:0x91, arr:keyData)
_ = try await self.tagReader?.sendMSEKAT(keyData: Data(wrappedKeyData), idData: Data(idData))
} else if cipherAlg.hasPrefix("AES") {
_ = try await self.tagReader?.sendMSESetATIntAuth(oid: oid, keyId: keyId)
let data = wrapDO(b: 0x80, arr:keyData)
gaSegments = self.chunk(data: data, segmentSize: ChipAuthenticationHandler.COMMAND_CHAINING_CHUNK_SIZE )
try await self.handleGeneralAuthentication()
} else {
throw NFCPassportReaderError.InvalidDataPassed("Cipher Algorithm \(cipherAlg) not supported")
}
}
private func handleGeneralAuthentication() async throws {
repeat {
// Pull next segment from list
let segment = gaSegments.removeFirst()
let isLast = gaSegments.isEmpty
// send it
_ = try await self.tagReader?.sendGeneralAuthenticate(data: segment, isLast: isLast)
} while ( !gaSegments.isEmpty )
}
private func restartSecureMessaging( oid : String, sharedSecret : [UInt8], maxTranceiveLength : Int, shouldCheckMAC : Bool) throws {
let cipherAlg = try ChipAuthenticationInfo.toCipherAlgorithm(oid: oid)
let keyLength = try ChipAuthenticationInfo.toKeyLength(oid: oid)
// Start secure messaging.
let smskg = SecureMessagingSessionKeyGenerator()
let ksEnc = try smskg.deriveKey(keySeed: sharedSecret, cipherAlgName: cipherAlg, keyLength: keyLength, mode: .ENC_MODE)
let ksMac = try smskg.deriveKey(keySeed: sharedSecret, cipherAlgName: cipherAlg, keyLength: keyLength, mode: .MAC_MODE)
let ssc = withUnsafeBytes(of: 0.bigEndian, Array.init)
if (cipherAlg.hasPrefix("DESede")) {
Log.info( "Restarting secure messaging using DESede encryption")
let sm = SecureMessaging(encryptionAlgorithm: .DES, ksenc: ksEnc, ksmac: ksMac, ssc: ssc)
tagReader?.secureMessaging = sm
} else if (cipherAlg.hasPrefix("AES")) {
Log.info( "Restarting secure messaging using AES encryption")
let sm = SecureMessaging(encryptionAlgorithm: .AES, ksenc: ksEnc, ksmac: ksMac, ssc: ssc)
tagReader?.secureMessaging = sm
} else {
Log.error( "Not restarting secure messaging as unsupported cipher algorithm requested - \(cipherAlg)")
throw NFCPassportReaderError.InvalidDataPassed("Unsupported cipher algorithm \(cipherAlg)" )
}
}
func inferDigestAlgorithmFromCipherAlgorithmForKeyDerivation( cipherAlg : String, keyLength : Int) throws -> String {
if cipherAlg == "DESede" || cipherAlg == "AES-128" {
return "SHA1"
}
if cipherAlg == "AES" && keyLength == 128 {
return "SHA1"
}
if cipherAlg == "AES-256" || cipherAlg == "AES-192" {
return "SHA256"
}
if cipherAlg == "AES" && (keyLength == 192 || keyLength == 256) {
return "SHA256"
}
throw NFCPassportReaderError.InvalidDataPassed("Unsupported cipher algorithm or key length")
}
/// Chunks up a byte array into a number of segments of the given size,
/// and a final segment if there is a remainder.
/// - Parameter segmentSize the number of bytes per segment
/// - Parameter data the data to be partitioned
/// - Parameter a list with the segments
func chunk( data : [UInt8], segmentSize: Int ) -> [[UInt8]] {
return stride(from: 0, to: data.count, by: segmentSize).map {
Array(data[$0 ..< Swift.min($0 + segmentSize, data.count)])
}
}
}
#endif

View File

@@ -0,0 +1,16 @@
//
// DataGroupHash.swift
// NFCPassportReader
//
// Created by Andy Qua on 09/02/2021.
// Copyright © 2021 Andy Qua. All rights reserved.
//
@available(iOS 13, macOS 10.15, *)
public struct DataGroupHash {
public var id: String
public var sodHash: String
public var computedHash : String
public var match : Bool
}

View File

@@ -0,0 +1,36 @@
//
// DataGroupParser.swift
//
// Created by Andy Qua on 14/06/2019.
//
import OpenSSL
@available(iOS 13, macOS 10.15, *)
class DataGroupParser {
static let dataGroupNames = ["Common", "DG1", "DG2", "DG3", "DG4", "DG5", "DG6", "DG7", "DG8", "DG9", "DG10", "DG11", "DG12", "DG13", "DG14", "DG15", "DG16", "SecurityData"]
static let tags : [UInt8] = [0x60, 0x61, 0x75, 0x63, 0x76, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, 0x70, 0x77]
static let classes : [DataGroup.Type] = [COM.self, DataGroup1.self, DataGroup2.self,
NotImplementedDG.self, NotImplementedDG.self, NotImplementedDG.self,
NotImplementedDG.self, DataGroup7.self, NotImplementedDG.self,
NotImplementedDG.self, NotImplementedDG.self, DataGroup11.self,
DataGroup12.self, NotImplementedDG.self, DataGroup14.self,
DataGroup15.self, NotImplementedDG.self, SOD.self]
func parseDG( data : [UInt8] ) throws -> DataGroup {
let header = data[0..<4]
let dg = try tagToDG(header[0])
return try dg.init(data)
}
func tagToDG( _ tag : UInt8 ) throws -> DataGroup.Type {
guard let index = DataGroupParser.tags.firstIndex(of: tag) else { throw NFCPassportReaderError.UnknownTag}
return DataGroupParser.classes[index]
}
}

View File

@@ -0,0 +1,145 @@
//
// Errors.swift
// NFCPassportReader
//
// Created by Andy Qua on 09/02/2021.
// Copyright © 2021 Andy Qua. All rights reserved.
//
import Foundation
// MARK: TagError
@available(iOS 13, macOS 10.15, *)
public enum NFCPassportReaderError: Error {
case ResponseError(String, UInt8, UInt8)
case InvalidResponse
case UnexpectedError
case NFCNotSupported
case NoConnectedTag
case D087Malformed
case InvalidResponseChecksum
case MissingMandatoryFields
case CannotDecodeASN1Length
case InvalidASN1Value
case UnableToProtectAPDU
case UnableToUnprotectAPDU
case UnsupportedDataGroup
case DataGroupNotRead
case UnknownTag
case UnknownImageFormat
case NotImplemented
case TagNotValid
case ConnectionError
case UserCanceled
case InvalidMRZKey
case MoreThanOneTagFound
case InvalidHashAlgorithmSpecified
case UnsupportedCipherAlgorithm
case UnsupportedMappingType
case PACEError(String,String)
case ChipAuthenticationFailed
case InvalidDataPassed(String)
case NotYetSupported(String)
var value: String {
switch self {
case .ResponseError(let errMsg, _, _): return errMsg
case .InvalidResponse: return "InvalidResponse"
case .UnexpectedError: return "UnexpectedError"
case .NFCNotSupported: return "NFCNotSupported"
case .NoConnectedTag: return "NoConnectedTag"
case .D087Malformed: return "D087Malformed"
case .InvalidResponseChecksum: return "InvalidResponseChecksum"
case .MissingMandatoryFields: return "MissingMandatoryFields"
case .CannotDecodeASN1Length: return "CannotDecodeASN1Length"
case .InvalidASN1Value: return "InvalidASN1Value"
case .UnableToProtectAPDU: return "UnableToProtectAPDU"
case .UnableToUnprotectAPDU: return "UnableToUnprotectAPDU"
case .UnsupportedDataGroup: return "UnsupportedDataGroup"
case .DataGroupNotRead: return "DataGroupNotRead"
case .UnknownTag: return "UnknownTag"
case .UnknownImageFormat: return "UnknownImageFormat"
case .NotImplemented: return "NotImplemented"
case .TagNotValid: return "TagNotValid"
case .ConnectionError: return "ConnectionError"
case .UserCanceled: return "UserCanceled"
case .InvalidMRZKey: return "InvalidMRZKey"
case .MoreThanOneTagFound: return "MoreThanOneTagFound"
case .InvalidHashAlgorithmSpecified: return "InvalidHashAlgorithmSpecified"
case .UnsupportedCipherAlgorithm: return "UnsupportedCipherAlgorithm"
case .UnsupportedMappingType: return "UnsupportedMappingType"
case .PACEError(let step, let reason): return "PACEError (\(step)) - \(reason)"
case .ChipAuthenticationFailed: return "ChipAuthenticationFailed"
case .InvalidDataPassed(let reason) : return "Invalid data passed - \(reason)"
case .NotYetSupported(let reason) : return "Not yet supported - \(reason)"
}
}
}
@available(iOS 13, macOS 10.15, *)
extension NFCPassportReaderError: LocalizedError {
public var errorDescription: String? {
return NSLocalizedString(value, comment: "My error")
}
}
// MARK: OpenSSLError
@available(iOS 13, macOS 10.15, *)
public enum OpenSSLError: Error {
case UnableToGetX509CertificateFromPKCS7(String)
case UnableToVerifyX509CertificateForSOD(String)
case VerifyAndReturnSODEncapsulatedData(String)
case UnableToReadECPublicKey(String)
case UnableToExtractSignedDataFromPKCS7(String)
case VerifySignedAttributes(String)
case UnableToParseASN1(String)
case UnableToDecryptRSASignature(String)
}
@available(iOS 13, macOS 10.15, *)
extension OpenSSLError: LocalizedError {
public var errorDescription: String? {
switch self {
case .UnableToGetX509CertificateFromPKCS7(let reason):
return NSLocalizedString("Unable to read the SOD PKCS7 Certificate. \(reason)", comment: "UnableToGetPKCS7CertificateForSOD")
case .UnableToVerifyX509CertificateForSOD(let reason):
return NSLocalizedString("Unable to verify the SOD X509 certificate. \(reason)", comment: "UnableToVerifyX509CertificateForSOD")
case .VerifyAndReturnSODEncapsulatedData(let reason):
return NSLocalizedString("Unable to verify the SOD Datagroup hashes. \(reason)", comment: "UnableToGetSignedDataFromPKCS7")
case .UnableToReadECPublicKey(let reason):
return NSLocalizedString("Unable to read ECDSA Public key \(reason)!", comment: "UnableToReadECPublicKey")
case .UnableToExtractSignedDataFromPKCS7(let reason):
return NSLocalizedString("Unable to extract Signer data from PKCS7 \(reason)!", comment: "UnableToExtractSignedDataFromPKCS7")
case .VerifySignedAttributes(let reason):
return NSLocalizedString("Unable to Verify the SOD SignedAttributes \(reason)!", comment: "UnableToExtractSignedDataFromPKCS7")
case .UnableToParseASN1(let reason):
return NSLocalizedString("Unable to parse ASN1 \(reason)!", comment: "UnableToParseASN1")
case .UnableToDecryptRSASignature(let reason):
return NSLocalizedString("DatUnable to decrypt RSA Signature \(reason)!", comment: "UnableToDecryptRSSignature")
}
}
}
// MARK: PassiveAuthenticationError
public enum PassiveAuthenticationError: Error {
case UnableToParseSODHashes(String)
case InvalidDataGroupHash(String)
case SODMissing(String)
}
extension PassiveAuthenticationError: LocalizedError {
public var errorDescription: String? {
switch self {
case .UnableToParseSODHashes(let reason):
return NSLocalizedString("Unable to parse the SOD Datagroup hashes. \(reason)", comment: "UnableToParseSODHashes")
case .InvalidDataGroupHash(let reason):
return NSLocalizedString("DataGroup hash not present or didn't match \(reason)!", comment: "InvalidDataGroupHash")
case .SODMissing(let reason):
return NSLocalizedString("DataGroup SOD not present or not read \(reason)!", comment: "SODMissing")
}
}
}

View File

@@ -0,0 +1,68 @@
//
// Logging.swift
// NFCTest
//
// Created by Andy Qua on 11/06/2019.
// Copyright © 2019 Andy Qua. All rights reserved.
//
import Foundation
// TODO: Quick log functions - will move this to something better
public enum LogLevel : Int, CaseIterable {
case verbose = 0
case debug = 1
case info = 2
case warning = 3
case error = 4
case none = 5
}
public class Log {
public static var logLevel : LogLevel = .info
public static var storeLogs = false
public static var logData = [String]()
private static let df = DateFormatter()
private static var dfInit = false
public class func verbose( _ msg : @autoclosure () -> String ) {
log( .verbose, msg )
}
public class func debug( _ msg : @autoclosure () -> String ) {
log( .debug, msg )
}
public class func info( _ msg : @autoclosure () -> String ) {
log( .info, msg )
}
public class func warning( _ msg : @autoclosure () -> String ) {
log( .warning, msg )
}
public class func error( _ msg : @autoclosure () -> String ) {
log( .error, msg )
}
public class func clearStoredLogs() {
logData.removeAll()
}
class func log( _ logLevel : LogLevel, _ msg : () -> String ) {
guard logLevel != .none else { return }
if !dfInit {
df.dateFormat = "y-MM-dd H:m:ss.SSSS"
dfInit = true
}
if self.logLevel.rawValue <= logLevel.rawValue {
let message = msg()
print( "\(df.string(from:Date())) - \(message)" )
if storeLogs {
logData.append( message )
}
}
}
}

View File

@@ -0,0 +1,534 @@
//
// NFCPassportModel.swift
// NFCPassportReader
//
// Created by Andy Qua on 29/10/2019.
//
import Foundation
#if os(iOS)
import UIKit
#endif
public enum PassportAuthenticationStatus {
case notDone
case success
case failed
}
@available(iOS 13, macOS 10.15, *)
public class NFCPassportModel {
public private(set) lazy var documentType : String = { return String( passportDataElements?["5F03"]?.first ?? "?" ) }()
public private(set) lazy var documentSubType : String = { return String( passportDataElements?["5F03"]?.last ?? "?" ) }()
public private(set) lazy var documentNumber : String = { return (passportDataElements?["5A"] ?? "?").replacingOccurrences(of: "<", with: "" ) }()
public private(set) lazy var issuingAuthority : String = { return passportDataElements?["5F28"] ?? "?" }()
public private(set) lazy var documentExpiryDate : String = { return passportDataElements?["59"] ?? "?" }()
public private(set) lazy var dateOfBirth : String = { return passportDataElements?["5F57"] ?? "?" }()
public private(set) lazy var gender : String = { return passportDataElements?["5F35"] ?? "?" }()
public private(set) lazy var nationality : String = { return passportDataElements?["5F2C"] ?? "?" }()
public private(set) lazy var lastName : String = {
return names[0].replacingOccurrences(of: "<", with: " " )
}()
public private(set) lazy var firstName : String = {
var name = ""
for i in 1 ..< names.count {
let fn = names[i].replacingOccurrences(of: "<", with: " " ).trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
name += fn + " "
}
return name.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
}()
public private(set) lazy var passportMRZ : String = { return passportDataElements?["5F1F"] ?? "NOT FOUND" }()
// Extract fields from DG11 if present
private lazy var names : [String] = {
guard let dg11 = dataGroupsRead[.DG11] as? DataGroup11,
let fullName = dg11.fullName?.components(separatedBy: "<<") else { return (passportDataElements?["5B"] ?? "?").components(separatedBy: "<<") }
return fullName
}()
public private(set) lazy var placeOfBirth : String? = {
guard let dg11 = dataGroupsRead[.DG11] as? DataGroup11,
let placeOfBirth = dg11.placeOfBirth else { return nil }
return placeOfBirth
}()
/// residence address
public private(set) lazy var residenceAddress : String? = {
guard let dg11 = dataGroupsRead[.DG11] as? DataGroup11,
let address = dg11.address else { return nil }
return address
}()
/// phone number
public private(set) lazy var phoneNumber : String? = {
guard let dg11 = dataGroupsRead[.DG11] as? DataGroup11,
let telephone = dg11.telephone else { return nil }
return telephone
}()
/// personal number
public private(set) lazy var personalNumber : String? = {
if let dg11 = dataGroupsRead[.DG11] as? DataGroup11,
let personalNumber = dg11.personalNumber { return personalNumber }
return (passportDataElements?["53"] ?? "?").replacingOccurrences(of: "<", with: "" )
}()
public private(set) lazy var documentSigningCertificate : X509Wrapper? = {
return certificateSigningGroups[.documentSigningCertificate]
}()
public private(set) lazy var countrySigningCertificate : X509Wrapper? = {
return certificateSigningGroups[.issuerSigningCertificate]
}()
// Extract data from COM
public private(set) lazy var LDSVersion : String = {
guard let com = dataGroupsRead[.COM] as? COM else { return "Unknown" }
return com.version
}()
public private(set) lazy var dataGroupsPresent : [String] = {
guard let com = dataGroupsRead[.COM] as? COM else { return [] }
return com.dataGroupsPresent
}()
// Parsed datagroup hashes
public private(set) var dataGroupsAvailable = [DataGroupId]()
public private(set) var dataGroupsRead : [DataGroupId:DataGroup] = [:]
public private(set) var dataGroupHashes = [DataGroupId: DataGroupHash]()
public internal(set) var cardAccess : CardAccess?
public internal(set) var BACStatus : PassportAuthenticationStatus = .notDone
public internal(set) var PACEStatus : PassportAuthenticationStatus = .notDone
public internal(set) var chipAuthenticationStatus : PassportAuthenticationStatus = .notDone
public private(set) var passportCorrectlySigned : Bool = false
public private(set) var documentSigningCertificateVerified : Bool = false
public private(set) var passportDataNotTampered : Bool = false
public private(set) var activeAuthenticationPassed : Bool = false
public private(set) var activeAuthenticationChallenge : [UInt8] = []
public private(set) var activeAuthenticationSignature : [UInt8] = []
public private(set) var verificationErrors : [Error] = []
public var isPACESupported : Bool {
get {
if cardAccess?.paceInfo != nil {
return true
} else {
// We may not have stored the cardAccess so check the DG14
if let dg14 = dataGroupsRead[.DG14] as? DataGroup14,
(dg14.securityInfos.filter { ($0 as? PACEInfo) != nil }).count > 0 {
return true
}
return false
}
}
}
public var isChipAuthenticationSupported : Bool {
get {
if let dg14 = dataGroupsRead[.DG14] as? DataGroup14,
(dg14.securityInfos.filter { ($0 as? ChipAuthenticationPublicKeyInfo) != nil }).count > 0 {
return true
} else {
return false
}
}
}
#if os(iOS)
public var passportImage : UIImage? {
guard let dg2 = dataGroupsRead[.DG2] as? DataGroup2 else { return nil }
return dg2.getImage()
}
public var signatureImage : UIImage? {
guard let dg7 = dataGroupsRead[.DG7] as? DataGroup7 else { return nil }
return dg7.getImage()
}
#endif
public var activeAuthenticationSupported : Bool {
guard let dg15 = dataGroupsRead[.DG15] as? DataGroup15 else { return false }
if dg15.ecdsaPublicKey != nil || dg15.rsaPublicKey != nil {
return true
}
return false
}
private var certificateSigningGroups : [CertificateType:X509Wrapper] = [:]
private var passportDataElements : [String:String]? {
guard let dg1 = dataGroupsRead[.DG1] as? DataGroup1 else { return nil }
return dg1.elements
}
public init() {
}
public init( from dump: [String:String] ) {
var AAChallenge : [UInt8]?
var AASignature : [UInt8]?
for (key,value) in dump {
if let data = Data(base64Encoded: value) {
let bin = [UInt8](data)
if key == "AASignature" {
AASignature = bin
} else if key == "AAChallenge" {
AAChallenge = bin
} else {
do {
let dg = try DataGroupParser().parseDG(data: bin)
let dgId = DataGroupId.getIDFromName(name:key)
self.addDataGroup( dgId, dataGroup:dg )
} catch {
Log.error("Failed to import Datagroup - \(key) from dump - \(error)" )
}
}
}
}
// See if we have Active Auth info in the dump
if let challenge = AAChallenge, let signature = AASignature {
verifyActiveAuthentication(challenge: challenge, signature: signature)
}
}
public func addDataGroup(_ id : DataGroupId, dataGroup: DataGroup ) {
self.dataGroupsRead[id] = dataGroup
if id != .COM && id != .SOD {
self.dataGroupsAvailable.append( id )
}
}
public func getDataGroup( _ id : DataGroupId ) -> DataGroup? {
return dataGroupsRead[id]
}
/// Dumps the passport data
/// - Parameters:
/// selectedDataGroups - the Data Groups to be exported (if they are present in the passport)
/// includeActiveAutheticationData - Whether to include the Active Authentication challenge and response (if supported and retrieved)
/// - Returns: dictionary of DataGroup ids and Base64 encoded data
public func dumpPassportData( selectedDataGroups : [DataGroupId], includeActiveAuthenticationData : Bool = false) -> [String:String] {
var ret = [String:String]()
for dg in selectedDataGroups {
if let dataGroup = self.dataGroupsRead[dg] {
let val = Data(dataGroup.data)
let base64 = val.base64EncodedString()
ret[dg.getName()] = base64
}
}
if includeActiveAuthenticationData && self.activeAuthenticationSupported {
ret["AAChallenge"] = Data(activeAuthenticationChallenge).base64EncodedString()
ret["AASignature"] = Data(activeAuthenticationSignature).base64EncodedString()
}
return ret
}
public func getHashesForDatagroups( hashAlgorythm: String ) -> [DataGroupId:[UInt8]] {
var ret = [DataGroupId:[UInt8]]()
for (key, value) in dataGroupsRead {
if hashAlgorythm == "SHA1" {
ret[key] = calcSHA1Hash(value.body)
} else if hashAlgorythm == "SHA224" {
ret[key] = calcSHA224Hash(value.body)
} else if hashAlgorythm == "SHA256" {
ret[key] = calcSHA256Hash(value.body)
} else if hashAlgorythm == "SHA384" {
ret[key] = calcSHA384Hash(value.body)
} else if hashAlgorythm == "SHA512" {
ret[key] = calcSHA512Hash(value.body)
}
}
return ret
}
/// This method performs the passive authentication
/// Passive Authentication : Two Parts:
/// Part 1 - Has the SOD (Security Object Document) been signed by a valid country signing certificate authority (CSCA)?
/// Part 2 - has it been tampered with (e.g. hashes of Datagroups match those in the SOD?
/// guard let sod = model.getDataGroup(.SOD) else { return }
///
/// - Parameter masterListURL: the path to the masterlist to try to verify the document signing certiifcate in the SOD
/// - Parameter useCMSVerification: Should we use OpenSSL CMS verification to verify the SOD content
/// is correctly signed by the document signing certificate OR should we do this manully based on RFC5652
/// CMS fails under certain circumstances (e.g. hashes are SHA512 whereas content is signed with SHA256RSA).
/// Currently defaulting to manual verification - hoping this will replace the CMS verification totally
/// CMS Verification currently there just in case
public func verifyPassport( masterListURL: URL?, useCMSVerification : Bool = false ) {
if let masterListURL = masterListURL {
do {
try validateAndExtractSigningCertificates( masterListURL: masterListURL )
} catch let error {
verificationErrors.append( error )
}
}
do {
try ensureReadDataNotBeenTamperedWith( useCMSVerification : useCMSVerification )
} catch let error {
verificationErrors.append( error )
}
}
public func verifyActiveAuthentication( challenge: [UInt8], signature: [UInt8] ) {
self.activeAuthenticationChallenge = challenge
self.activeAuthenticationSignature = signature
Log.verbose( "Active Authentication")
Log.verbose( " challange - \(binToHexRep(challenge))")
Log.verbose( " signature - \(binToHexRep(signature))")
// Get AA Public key
self.activeAuthenticationPassed = false
guard let dg15 = self.dataGroupsRead[.DG15] as? DataGroup15 else { return }
if let rsaKey = dg15.rsaPublicKey {
do {
var decryptedSig = try OpenSSLUtils.decryptRSASignature(signature: Data(signature), pubKey: rsaKey)
// Decrypted signature compromises of header (6A), Message, Digest hash, Trailer
// Trailer can be 1 byte (BC - SHA-1 hash) or 2 bytes (xxCC) - where xx identifies the hash algorithm used
// if the last byte of the digest is 0xBC, then this uses dedicated hash function 3 (SHA-1),
// If the last byte is 0xCC, then the preceding byte tells you which hash function
// should be used (currently not yet implemented!)
// See ISO/IEC9796-2 for details on the verification and ISO/IEC 10118-3 for the dedicated hash functions!
var hashTypeByte = decryptedSig.popLast() ?? 0x00
if hashTypeByte == 0xCC {
hashTypeByte = decryptedSig.popLast() ?? 0x00
}
var hashType : String = ""
var hashLength = 0
switch hashTypeByte {
case 0xBC, 0x33:
hashType = "SHA1"
hashLength = 20 // 160 bits for SHA-1 -> 20 bytes
case 0x34:
hashType = "SHA256"
hashLength = 32 // 256 bits for SHA-256 -> 32 bytes
case 0x35:
hashType = "SHA512"
hashLength = 64 // 512 bits for SHA-512 -> 64 bytes
case 0x36:
hashType = "SHA384"
hashLength = 48 // 384 bits for SHA-384 -> 48 bytes
case 0x38:
hashType = "SHA224"
hashLength = 28 // 224 bits for SHA-224 -> 28 bytes
default:
Log.error( "Error identifying Active Authentication RSA message digest hash algorithm" )
return
}
let message = [UInt8](decryptedSig[1 ..< (decryptedSig.count-hashLength)])
let digest = [UInt8](decryptedSig[(decryptedSig.count-hashLength)...])
// Concatenate the challenge to the end of the message
let fullMsg = message + challenge
// Then generate the hash
let msgHash : [UInt8] = try calcHash(data: fullMsg, hashAlgorithm: hashType)
// Check hashes match
if msgHash == digest {
self.activeAuthenticationPassed = true
Log.debug( "Active Authentication (RSA) successful" )
} else {
Log.error( "Error verifying Active Authentication RSA signature - Hash doesn't match" )
}
} catch {
Log.error( "Error verifying Active Authentication RSA signature - \(error)" )
}
} else if let ecdsaPublicKey = dg15.ecdsaPublicKey {
var digestType = ""
if let dg14 = dataGroupsRead[.DG14] as? DataGroup14,
let aa = dg14.securityInfos.compactMap({ $0 as? ActiveAuthenticationInfo }).first {
digestType = aa.getSignatureAlgorithmOIDString() ?? ""
}
if OpenSSLUtils.verifyECDSASignature( publicKey:ecdsaPublicKey, signature: signature, data: challenge, digestType: digestType ) {
self.activeAuthenticationPassed = true
Log.debug( "Active Authentication (ECDSA) successful" )
} else {
Log.error( "Error verifying Active Authentication ECDSA signature" )
}
}
}
// Check if signing certificate is on the revocation list
// We do this by trying to build a trust chain of the passport certificate against the ones in the revocation list
// and if we are successful then its been revoked.
// NOTE - NOT USED YET AS NOT ABLE TO TEST
func hasCertBeenRevoked( revocationListURL : URL ) -> Bool {
var revoked = false
do {
try validateAndExtractSigningCertificates( masterListURL: revocationListURL )
// Certificate chain found - which means certificate is on revocation list
revoked = true
} catch {
// No chain found - certificate not revoked
}
return revoked
}
private func validateAndExtractSigningCertificates( masterListURL: URL ) throws {
self.passportCorrectlySigned = false
guard let sod = getDataGroup(.SOD) else {
throw PassiveAuthenticationError.SODMissing("No SOD found" )
}
let data = Data(sod.body)
let cert = try OpenSSLUtils.getX509CertificatesFromPKCS7( pkcs7Der: data ).first!
self.certificateSigningGroups[.documentSigningCertificate] = cert
let rc = OpenSSLUtils.verifyTrustAndGetIssuerCertificate( x509:cert, CAFile: masterListURL )
switch rc {
case .success(let csca):
self.certificateSigningGroups[.issuerSigningCertificate] = csca
case .failure(let error):
throw error
}
Log.debug( "Passport passed SOD Verification" )
self.passportCorrectlySigned = true
}
private func ensureReadDataNotBeenTamperedWith( useCMSVerification: Bool ) throws {
guard let sod = getDataGroup(.SOD) as? SOD else {
throw PassiveAuthenticationError.SODMissing("No SOD found" )
}
// Get SOD Content and verify that its correctly signed by the Document Signing Certificate
var signedData : Data
documentSigningCertificateVerified = false
do {
if useCMSVerification {
signedData = try OpenSSLUtils.verifyAndReturnSODEncapsulatedDataUsingCMS(sod: sod)
} else {
signedData = try OpenSSLUtils.verifyAndReturnSODEncapsulatedData(sod: sod)
}
documentSigningCertificateVerified = true
} catch {
signedData = try sod.getEncapsulatedContent()
}
// Now Verify passport data by comparing compare Hashes in SOD against
// computed hashes to ensure data not been tampered with
passportDataNotTampered = false
let asn1Data = try OpenSSLUtils.ASN1Parse( data: signedData )
let (sodHashAlgorythm, sodHashes) = try parseSODSignatureContent( asn1Data )
var errors : String = ""
// pour chaque dataGroupsRead, il faut que le hash sodHashVal parsé du eContent (signedData) soit égal
// au computedHashVal qui sort du dataGroupsRead
for (id,dgVal) in dataGroupsRead {
guard let sodHashVal = sodHashes[id] else {
// SOD and COM don't have hashes so these aren't errors
if id != .SOD && id != .COM {
errors += "DataGroup \(id) is missing!\n"
}
continue
}
let computedHashVal = binToHexRep(dgVal.hash(sodHashAlgorythm))
var match = true
if computedHashVal != sodHashVal {
errors += "\(id) invalid hash:\n SOD hash:\(sodHashVal)\n Computed hash:\(computedHashVal)\n"
match = false
}
dataGroupHashes[id] = DataGroupHash(id: id.getName(), sodHash:sodHashVal, computedHash:computedHashVal, match:match)
}
if errors != "" {
Log.error( "HASH ERRORS - \(errors)" )
throw PassiveAuthenticationError.InvalidDataGroupHash(errors)
}
Log.debug( "Passport passed Datagroup Tampering check" )
passportDataNotTampered = true
}
/// Parses an text ASN1 structure, and extracts the Hash Algorythm and Hashes contained from the Octect strings
/// - Parameter content: the text ASN1 stucure format
/// - Returns: The Hash Algorythm used - either SHA1 or SHA256, and a dictionary of hashes for the datagroups (currently only DG1 and DG2 are handled)
private func parseSODSignatureContent( _ content : String ) throws -> (String, [DataGroupId : String]){
var currentDG = ""
var sodHashAlgo = ""
var sodHashes : [DataGroupId : String] = [:]
let lines = content.components(separatedBy: "\n")
let dgList : [DataGroupId] = [.COM,.DG1,.DG2,.DG3,.DG4,.DG5,.DG6,.DG7,.DG8,.DG9,.DG10,.DG11,.DG12,.DG13,.DG14,.DG15,.DG16,.SOD]
for line in lines {
if line.contains( "d=2" ) && line.contains( "OBJECT" ) {
if line.contains( "sha1" ) {
sodHashAlgo = "SHA1"
} else if line.contains( "sha224" ) {
sodHashAlgo = "SHA224"
} else if line.contains( "sha256" ) {
sodHashAlgo = "SHA256"
} else if line.contains( "sha384" ) {
sodHashAlgo = "SHA384"
} else if line.contains( "sha512" ) {
sodHashAlgo = "SHA512"
}
} else if line.contains("d=3" ) && line.contains( "INTEGER" ) {
if let range = line.range(of: "INTEGER") {
let substr = line[range.upperBound..<line.endIndex]
if let r2 = substr.range(of: ":") {
currentDG = String(line[r2.upperBound...])
}
}
} else if line.contains("d=3" ) && line.contains( "OCTET STRING" ) {
if let range = line.range(of: "[HEX DUMP]:") {
let val = line[range.upperBound..<line.endIndex]
if currentDG != "", let id = Int(currentDG, radix:16) {
sodHashes[dgList[id]] = String(val)
currentDG = ""
}
}
}
}
if sodHashAlgo == "" {
throw PassiveAuthenticationError.UnableToParseSODHashes("Unable to find hash algorythm used" )
}
if sodHashes.count == 0 {
throw PassiveAuthenticationError.UnableToParseSODHashes("Unable to extract hashes" )
}
Log.debug( "Parse SOD - Using Algo - \(sodHashAlgo)" )
Log.debug( " - Hashes - \(sodHashes)" )
return (sodHashAlgo, sodHashes)
}
}