Skip to main content

eid_icao 0.1.2 copy "eid_icao: ^0.1.2" to clipboard
eid_icao: ^0.1.2 copied to clipboard

Reads ICAO 9303 passports and identity cards over any eid transport: BAC and PACE, MRZ, face image and data groups, with Passive, Chip and Active Authentication.

eid_icao #

Pub Version Build Maintainer Licence Platforms Donate with PayPal

Reads the contactless chip of passports and identity cards, ICAO 9303: passports worldwide, and the identity cards of the European Union issued since August 2021, which Regulation (EU) 2019/1157 requires to carry it. In pure Dart, over any eid transport.

  • Opens the chip with BAC or PACE, from the MRZ or the card access number (CAN), then talks to it under secure messaging.
  • Reads the MRZ (DG1), the face (DG2, ISO/IEC 19794-5 or 39794-5), the displayed signature (DG7), the additional personal and document details (DG11, DG12) and the persons to notify (DG16).
  • Checks that the issuing state signed the data (Passive Authentication) and that the chip is not a copy (Chip Authentication, Chip Authentication Mapping or Active Authentication).
  • Reads the MRZ in the frames of a camera, for the key, in pure Dart.
  • Gives the photo as JPEG or PNG, ready to show: JPEG 2000, which most identity cards use, is decoded in pure Dart.

Reading a document takes a transport: eid_nfc for the NFC of an Android phone or an iPhone, eid_ccid for a contactless PC/SC reader on Android, iOS, macOS, Windows and Linux. No browser lets a page talk to an identity chip: on the web, the package checks documents read elsewhere (IcaoDocument.fromJson, IcaoPassiveAuthenticator) and runs the simulator.

Which packages for which document: a Belgian card in a USB reader takes eid_belgium and eid_ccid and needs no key; a passport or an EU identity card takes eid_icao, with eid_nfc on a phone or eid_ccid on a contactless USB reader, and needs the CAN or the MRZ

Install #

dependencies:
  eid_icao: ^0.1.0
  eid_ccid: ^0.1.0 # a contactless PC/SC reader, in a Flutter application

Read a document #

import 'package:eid_ccid/eid_ccid.dart';
import 'package:eid_icao/eid_icao.dart';

final readers = await CcidTransport.listReaders();
final transport = await CcidTransport.connect(readers.first);

final document = await IcaoReader(transport).read(
  access: IcaoAccessKey.can('123456'),
);
document.mrz.lastName;      // SPECIMEN
document.mrz.birthDate;     // 1990-05-15
document.photo;             // JPEG or PNG, for Image.memory
document.authenticity;      // chipAuthentication

await transport.disconnect();

The chip opens only with a key printed on the document:

IcaoAccessKey.can('123456');                // on EU identity cards
IcaoAccessKey.mrz(
  documentNumber: 'UT1234567',
  birthDate: PartialDate(1990, 5, 15),
  expiryDate: DateTime.utc(2034, 3, 13),
);
IcaoAccessKey.fromMrz(scannedLines);        // check digits verified

PACE is used when the chip offers it, BAC otherwise. Identity cards issued since 2021 open with PACE only; older passports with BAC only, from the MRZ. A key the chip refuses throws an IcaoAccessException whose reason is wrongKey.

Scan the MRZ with a camera #

Rather than having the document number and the dates typed, package:eid_icao/mrz_scanner.dart reads them in the frames of a camera, in pure Dart: no native code, no machine learning library, nothing to download. The OCR-B characters are matched against their templates, the check digits put a near tie right (0 and O, 8 and B), and a zone is given once two frames read it the same.

import 'package:eid_icao/mrz_scanner.dart';

final scanner = MrzScanner();

// For each frame of the camera, its luminance plane:
final result = scanner.add(MrzImage.luminance(
  width: width,
  height: height,
  bytes: luminance,
  rotation: MrzRotation.clockwise90, // as a phone's camera sees it
));
if (result != null) {
  await IcaoReader(transport).read(access: result.accessKey);
}

In Flutter, universal_barcode_scanner hands its camera frames over on Android, iOS, macOS, Windows, Linux and the web:

UniversalBarcodeScanner(
  scanFormat: ScanFormat.none,
  continuous: true,
  onFrame: (ScanFrame frame) {
    final result = scanner.add(MrzImage.luminance(
      width: frame.width,
      height: frame.height,
      bytes: frame.bytes,
      rotation: MrzRotation.values[frame.quarterTurns],
    ));
    if (result != null) Navigator.pop(context, result.accessKey);
  },
  onCreated: (ScannerController controller) {},
);

TD1 (identity cards), TD2 and TD3 (passports) are read, slanted, turned a little or upside down, the long document numbers of Belgian cards included. A frame takes some 100 ms on a laptop: to keep an interface smooth, read it in an isolate with MrzRecognizer().read(image) and hand the reading to MrzScanner.addReading. Nothing of the scanner is compiled into an application that does not import it.

Read on arrival #

final watcher = IcaoWatcher(
  CcidTerminal.any(),         // every reader, those plugged in later included
  accessKey: null,            // a key known in advance, tried first
  accessPrompt: (request) => askForCan(request), // null when the user gives up
  autoRead: true,             // read as soon as the document is put down
  parts: IcaoPart.all,        // face, signature, personalDetails, ...
  acceptExpired: false,       // turn down expired documents
  acceptedTypes: null,        // every document type
  verifySignatures: true,     // Passive Authentication
  trustedRoots: null,         // the CSCAs, see below
  verifyCard: true,           // have the chip prove it is genuine
  showPrivateData: false,     // the personal numbers, see Personal data
  onProgress: null,           // 0 to 1, for a progress bar
  onApdu: null,               // every command and answer, before protection
)..start();

watcher.events.listen((event) {
  switch (event) {
    case IcaoChipRead read:
      show(read.document);
    case IcaoChipReadFailed failed:
      showError(icaoErrorMessage(failed.error, IcaoLanguage.fr));
    case IcaoChipRemoved _:
      clear();
    case IcaoChipInserted _:
      break;
  }
});

request.acceptsCan tells whether the CAN opens this chip, and request.refused why the previous key was turned down. The prompt is asked again until the chip opens or it returns null.

The watcher checks that the document is still there with a harmless command. It never sends one during a read, which would end secure messaging.

Read less #

final document = await reader.read(access: key, parts: {IcaoPart.face});
document.personalDetails;   // null

DG1 is always read. DG2 is the largest file and most of the reading time: leave IcaoPart.face out when the photo is not needed.

Turn documents down #

Three checks are on by default: an expired document, data the issuing state did not sign and a cloned chip are turned down. acceptedTypes adds a fourth:

await reader.read(access: key, acceptedTypes: {IcaoDocumentType.passport});

A document turned down throws an IcaoDocumentRejectedException whose reason is expired, documentType, signature or notGenuine.

  • verifySignatures runs Passive Authentication: every data group read must match its hash in EF.SOD, which the document signer signed. With trustedRoots, a CSCA among them must have issued the document signer.
  • verifyCard has the chip prove it holds a private key that a copy of the files would not: Chip Authentication from DG14, Chip Authentication Mapping during PACE, or Active Authentication from DG15. It needs verifySignatures, which vouches for those keys. A chip offering none of them, as on some older passports, gives notSupported.

Trust the issuing states #

The certificates of the issuing states (CSCAs) are not bundled: they change, and states publish them.

final list = IcaoMasterList.parse(bytes);
await reader.read(access: key, trustedRoots: list.cscas);

IcaoMasterList.parse reads:

Without trustedRoots, the hashes and the signature are still checked, and passiveAuthentication.chain is unverified: the data is consistent, but nothing ties its signer to a state. Anyone can make a chip that passes every check that way, its own proof included: to establish an identity, give the CSCAs.

A CSCA of any state in the list is accepted for any document. To require the issuing state's own, pass only its CSCAs (list.ofCountry('BE')), or compare passiveAuthentication.countrySigningCa?.subject.country with the issuing state of the MRZ. Revocation lists are not checked.

Check on a server #

// On the device.
send(document.toJson());

// On the server.
final copy = IcaoDocument.fromJson(json);
final check = IcaoPassiveAuthenticator(trustedRoots: list.cscas)
    .verify(sod: copy.sod, dataGroups: copy.dataGroups);
check.isValid;

Passive Authentication needs no chip. The chip's own proof, though, only happens while it is on the reader.

Images #

document.photo is ready for Flutter's Image.memory, whatever the chip holds: a JPEG as it is, or a PNG converted from JPEG 2000, which most identity cards and many passports use and Flutter does not decode. The decoder is built in, written in pure Dart from ITU-T T.800: no plugin, no server.

Image.memory(document.photo!);
document.face?.bytes;          // the image as the chip holds it
document.face?.format;         // jpeg, jpeg2000 or png

Every IcaoImage, the signature of DG7 and the images of DG12 included, has its displayBytes in the same way. The PNG is lossless: it holds the pixels of the JPEG 2000 image, decoded once, when the image is read. The bytes the issuing state signed stay in bytes.

Without a reader #

import 'package:eid_icao/testing.dart';

final chip = SimulatedIcaoChip(lastName: 'PEETERS', can: '654321');
final document = await IcaoReader(chip).read(access: chip.canKey);
chip.remove();
chip.insert();

It holds a fictional citizen of Utopia, the state of the ICAO specimens, signed by a made-up CSCA trusted for the simulator alone. access (paceAndBac, paceOnly, bacOnly), proof, faceEncoding, tamperedGroup and cloned give the documents the checks meet in the field, and those they turn down. photo: specimenPhotoJpeg2000 gives it the JPEG 2000 face of most identity cards.

Not covered #

  • DG3 and DG4, fingerprints and irises: they need Extended Access Control with terminal certificates that states issue to their own authorities.
  • PACE with Integrated Mapping, which few chips offer.
  • Revocation lists, and the LDS2 applications (travel records, visas).
  • JPEG 2000 beyond part 1: the extensions of part 2 and the high throughput coding of part 15, which ICAO documents do not use.
  • Reading the MRZ with a camera.

Personal data #

A chip holds the holder's personal data and photo, which data protection law covers. Read it only with a reason the holder knows, keep what you need, and do not store the CAN or the MRZ key longer than the read. onApdu reports the data read, though never the keys.

showPrivateData is off by default. The personal numbers are then left out: the optional data of the MRZ, where Belgium puts the national register number and passports a personal number, and the personal number of DG11. Most uses do not need them, and some states restrict who may use them, as Belgium does for its national register number.

await reader.read(access: key);                        // mrz.optionalData is empty
await reader.read(access: key, showPrivateData: true); // and here it is

The signatures are still checked on the files as read. Without the numbers, the document leaves out the raw DG1 and DG11 that hold them, so a server receiving toJson() gets the MRZ as text and cannot check DG1 again; onApdu gets their answers with the data zeroed.

Sources #

  • ICAO Doc 9303, eighth edition: parts 3 to 6 (MRZ), 10 (LDS), 11 (BAC, PACE, Chip and Active Authentication, with the worked examples this package is tested against) and 12 (PKI).
  • BSI TR-03110 and TR-03111.
  • ISO/IEC 19794-5:2005, and ISO/IEC 39794-5. This software makes use of the Schema from ISO/IEC 39794-5 within modifications permitted in the relevant ISO/IEC standard.
  • RFC 5114 (DH groups), RFC 5652 (CMS), RFC 8017 (RSA).
  • ISO 1073-2, the OCR-B font. The templates and the classifier of the MRZ scanner were made from Matthew Skala's OCR-B font, in the public domain.

License #

Released under the MIT licence.

More from COMAPPS #

Electronic identity cards in Dart:

Package What it does
eid APDUs, ISO 7816-4 file reading and the values national cards share.
eid_belgium The Belgian eID, Kids ID and residence cards, through their contact chip.
eid_ccid The transport for a USB or PC/SC card reader.
eid_nfc The transport for the NFC of an Android phone or an iPhone.

Every package COMAPPS publishes is listed at packages.comapps.be.

0
likes
160
points
277
downloads
screenshot

Documentation

API reference

Publisher

verified publishercomapps.be

Weekly Downloads

Reads ICAO 9303 passports and identity cards over any eid transport: BAC and PACE, MRZ, face image and data groups, with Passive, Chip and Active Authentication.

Homepage
Repository (GitHub)
View/report issues

Topics

#eid #icao #passport #mrz #smartcard

Funding

Consider supporting this project:

www.paypal.com

License

MIT (license)

Dependencies

crypto, eid, pointycastle

More

Packages that depend on eid_icao