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 the Example folder of the framework there is a minimal 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

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()
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
   }

   if passport.activeAuthenticationResult == .Success {
       // Chip not cloned
   }

   if passport.chipAuthenticationResult == .Success {
       // Chip not cloned
   }
}

Verify a document certificate using the Document Validation Service:

To validate the document signer certificate using the document validation service, you need to send a request to

https://kinegramdocval.lkis.de/v2/validate

with the Content-Type set to multipart/form-data and the following fields:

client_id      text/plain                Client ID
dsc            application/octet-stream  Document Signer Certificate
idb            application/json          IDB data from the SDK

All fields are mandatory. client_id is a literal provided by your OVDK contact. dsc is the CardData.docSigningCertificate. idb data can be generated with the IDBMessageBuilder:

let idbMessageBuilder = IntelligentDatabaseMessageBuilder(app: "Your app name")
let idbJSON = try idbMessageBuilder.encodeAsJSON(passport: passport, error: error)

Or simply:

private func documentCertificateValidationRequest(passport: EmrtdPassport?,
                                                  error: EmrtdPassportReaderError?) {
    let app = "Your app name"
    let clientId = "RN53sEEGTvpSfk9b"
    let documentValidationHost = URL(string: "https://kinegramdocval.lkis.de/v2/validate")!

    try! DocumentCertificateValidationRequest(app: app, clientId: clientId, host: documentValidationHost)
        .validate(passport: passport, error: error) { validation, error in
            guard let validation = validation, error == nil else {
                if let error = error {
                    print("Document Certificate Validation Error \(error)")
                } else {
                    print("Document Certificate Validation Error")
                }
                return
            }

            let successful = validation.successful
            let id = validation.validation_id
            print("Document Certificate Validation: Success \(successful), ID \(id)")

            if passport?.sodFile?.signatureValid == true &&
                validation.successful &&
                passport?.allHashesValid == true &&
                passport?.cardAccessFileAuthenticityConfirmed != false {
                print("Document Passive Authentication Success. Data was not tampered with.")
            } else {
                print("Document Passive Authentication Failed. Data was tampered with!!")
            }
    }
}

Credits