eMRTD SDK for iOS

This SDK handles reading an NFC enabled passport using iOS 13 CoreNFC APIs to read and verify the chip of an eMRTD (electronic machine readable travel document).

Preconditions

Sample App

In Example there is a minimal full functional demo, that shows the usage of the SDK. To run it on the device, please adjust the bundle identifier and the development team accordingly.

Framework Installation

Apple offers XCFrameworks file format (from Xcode 11 and Swift 5.1 on) for packaging module-stable frameworks. The folder sdk contains two .xcframework files: emrtd_sdk.xcframework and openssl.xcframework.

Direct Installation in Xcode

Just put the two xcframework files into your target's dependencies via drag&drop: Xcode XCFramework usage

Via Cocoapods (1.9+ required)

There is also a emrtd-sdk.podspec file in folder sdk. If you already use Cocoapods for dependency management, you can simply add the sdks with one statement in your Podfile:

platform :ios, '13.0'

target 'emrtd-sdk-sample' do
  use_frameworks!

  # local Pod for NFC passport reading
  pod 'emrtd-sdk', :path => '../sdk/'

  # ... could be also managed your own git
  #pod 'emrtd-sdk', :git => 'https://git.yourdomain.com/emrtd-sdk-ios.git'
end

Then, run the following command:

$ pod install

Additional hints

1. Changes in Info.plist

  <key>com.apple.developer.nfc.readersession.iso7816.select-identifiers</key>
  <array>
    <string>A0000002471001</string>
  </array>
  <key>NFCReaderUsageDescription</key>
  <string>This app uses NFC to scan passports</string>

2. Entitlement

...
<dict>
  <key>com.apple.developer.nfc.readersession.formats</key>
  <array>
    <string>TAG</string>  // Application specific tag, including ISO 7816 Tags
  </array>
</dict>
...

Usage

EmrtdPassportReader instance

Pass a masterlist-file-url to the EmrtdPassportReader to verify the document certificate with a country certificate (CSCA).

A quite extensive and up to date Masterlist can be downloaded from the german Federal Cyber Security Authority BSI: German Masterlist

Generate an access key using the document number, date of birth and date of expiry. Or generate an access key using the CAN (6 digit number, printed on the front of the document)

On an instance of EmrtdPassportReader, call readAndVerify(accessKey:completedCallback:)
to read and verify the passport.

Or call read(accessKey:completedCallback:) to just read data from the passport without verifying.

let passportReader = EmrtdPassportReader()
// Set the masterListURL on the Passport Reader to allow auto passport verification
let masterListURL = Bundle.main.url(forResource: "20200409_DEMasterList", withExtension: "ml")!
try! passportReader.readMasterlist(from: masterListURL)

let mrzKey = MRZKey(documentNumber: "123456789", birthDateYYMMdd: "970101", expiryDateYYMMdd: "201212")

passportReader.readAndVerify(accessKey: mrzKey, completedCallback: { (passport, error) in
   if let passport = passport {
       // All good, we got a passport
   } else if let error = error {
       // Error occurred
   }
})

Using the CAN Key:

let canKey = CANKey(keyString: "123456")
passportReader.readAndVerify(accessKey: canKey, completedCallback: ...)

The messages displayed during the NFC Session can be customized:

func errorLocalization(error: EmrtdPassportReaderError) -> String {
    switch error {
    case .NFCNotSupported(_):
        return ""
    case .FailedToReadMasterlistFile(_):
        return ""
    case .MoreThanOneTagFound:
        return ""
    case .WrongTag:
        return ""
    case .UserInvalidatedSession:
        return ""
    case .SessionInvalidated(let errorCode):
        return ""
    case .ConnectingFailed(let error):
        return ""
    case .ConnectionLost:
      return "Lost Connection to chip"
    case .PaceOrBacFailed(let error):
        return ""
    case .FileReadFailed(let error, let files):
        return "Failed to read file(s) \(files.map({ "\($0)" }))"
    case .IncorrectAccessKey:
        return ""
    @unknown default:
        return "Unknown Error"
    }
}

func stepLocalization(step: ReadAndVerifyStep) -> String {
    switch step {
    case .waitingForPassport:
        return "Hold your iPhone near a passport"
    case .readFileAtrInfo:
        return "Reading elementary file Atr/Info"
    case .readFileCardAccess:
        return ""
    case .doPaceOrBac(_):
        return ""
    case .readFileSOD:
        return ""
    case .readFileDG14:
        return ""
    case .doChipAuthentication(_):
        return ""
    case .readFileDG15:
        return ""
    case .doActiveAuthentication(_):
        return ""
    case .readRemainingElementaryFiles:
        return ""
    case .doPassiveAuthentication:
        return ""
    case .done:
        return "Done"
    @unknown default:
        return "Unknown Step"
    }
}

func fileReadProgressLocalization(fileName: ElementaryFileName,
                                  readBytes: Int,
                                  totalBytes: Int) -> String {
    return "Reading File \(fileName) (\(readBytes)/\(totalBytes)Bytes)"
}

let passportReader = EmrtdPassportReader(errorLocalization: errorLocalization,
                                         stepLocalization: stepLocalization,
                                         fileReadProgressLocalization: fileReadProgressLocalization)

EmrtdPassport instance

When EmrtdPassportReader finishes, the completed callback will be called. The EmrtdPassport instance holds the information about the passport-chip.

The supported Files are: Atr/Info, CardAccess, SOD, DG1, DG2, DG7, DG11, DG12, DG14, DG15

Supported Protocols are: BAC, PACE, Chip Authentication, Active Authentication, Passive Authentication

Some examples are given below:

passportReader.readAndVerify(accessKey: mrzKey) { passport, error in
   guard let passport = passport, error == nil else {
       // Error occurred
       return
   }

   /* Extracted Data */

   // DG1 is always present according to specification
   // DG1 contains the MRZ of the passport
   if let dg1File: DataGroup1File = passport.dg1File {
       let documentNumber: String = dg1File.documentNumber
   } else {
       // Failed to parse DG1 apparently :(
   }

   // One face-info in DG2 must be present according to specification
   if let faceInfo: BiometricFaceImageInfo = passport.dg2File?.faceInfos?.first {
       let hairColor: HairColor = faceInfo.hairColor // Value may be unspecified
       let faceUIImage: UIImage? = faceInfo.uiImage
   }

   // DG11 is optional
   if let dg11File: DataGroup11File = passport.dg11File {
       let placeOfBirth: String? = dg11File.placeOfBirth // Value is optional
   }

   let notCorrectlyParsedFiles: [File] = passport.notCorrectlyParsedFiles

   let documentCertificate = passport.sodFile?.documentCertificate

   /* Verification */

   // If a masterlist-file-url was provided, you can check:
   if passport.passiveAuthenticationResult == true {
       // Data was not tampered
   }

   /*
    You can additionally verify that the chip was not cloned.
    Note that ActiveAuthentication and/or ChipAuthentication
    may not be supported by all passports.

    If the result for either one of these checks is `.Failed`,
    you need to assume that the chip was cloned.
   */
   if passport.activeAuthenticationResult == .Success {
       // Chip was not cloned
   }
   if passport.chipAuthenticationResult == .Success {
       // Chip was not cloned
   }
}

Credits