mirror of
https://github.com/selfxyz/self.git
synced 2026-04-27 03:01:15 -04:00
nfc reader
This commit is contained in:
193
app/ios/NFCPassportReader/BACHandler.swift
Normal file
193
app/ios/NFCPassportReader/BACHandler.swift
Normal 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
|
||||
223
app/ios/NFCPassportReader/ChipAuthenticationHandler.swift
Normal file
223
app/ios/NFCPassportReader/ChipAuthenticationHandler.swift
Normal 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
|
||||
16
app/ios/NFCPassportReader/DataGroupHash.swift
Normal file
16
app/ios/NFCPassportReader/DataGroupHash.swift
Normal 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
|
||||
}
|
||||
|
||||
36
app/ios/NFCPassportReader/DataGroupParser.swift
Normal file
36
app/ios/NFCPassportReader/DataGroupParser.swift
Normal 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]
|
||||
}
|
||||
}
|
||||
145
app/ios/NFCPassportReader/Errors.swift
Normal file
145
app/ios/NFCPassportReader/Errors.swift
Normal 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")
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
68
app/ios/NFCPassportReader/Logging.swift
Normal file
68
app/ios/NFCPassportReader/Logging.swift
Normal 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 )
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
534
app/ios/NFCPassportReader/NFCPassportModel.swift
Normal file
534
app/ios/NFCPassportReader/NFCPassportModel.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user