Passport / eMRTD
Passport reading starts from the MRZ-derived key, establishes Secure Messaging with PACE or BAC, and reads LDS data groups from the eMRTD application.
Read Data Groups
// ICAO 9303 Part 11 Appendix D example: "L898902C<369080619406236"
let mrzKey = MRZKeyGenerator.computeMRZKey(
documentNumber: "L898902C",
dateOfBirth: "690806",
dateOfExpiry: "940623"
)
let passport = try await CoreExtendedNFC.readPassport(
mrzKey: mrzKey,
dataGroups: [.com, .dg1, .dg2, .dg14, .dg15, .sod],
performActiveAuth: true,
trustAnchorsDER: cscaCertificates, // DER CSCA certificates from your trust list
onProgress: { message in print(message) } // localized progress text
)
To read over a transport you already hold, for example from scan(pollingTargets: [.iso14443]), use the transport: overload. It runs the same workflow but neither updates the NFC sheet nor ends the session:
let (_, transport, manager) = try await CoreExtendedNFC.scan(pollingTargets: [.iso14443])
defer { manager.invalidate() }
let passport = try await CoreExtendedNFC.readPassport(
transport: transport,
mrzKey: mrzKey,
trustAnchorsDER: cscaCertificates
)
The session-owning readPassport also accepts a manager: for your own NFCSessionManager or a subclass. To change how data groups are read, subclass PassportReader and override its open methods; see Subclassing.
Read Flow
- EF.CardAccess is read. When it advertises PACE, the reader picks the first PACEInfo it supports (
PACEHandler.selectPACEInfo(from:)); ICAO 9303-11 §4.4.4.1 lets the inspection system choose any advertised parameter set, so an unsupported first entry does not rule PACE out. MSE:Set AT names that parameter set with tag84(standardized domain parameter ID). When no advertised PACEInfo is supported, the PACE stage isfallbackwith the reasons and BAC runs without any PACE APDU being sent. If PACE fails, the reader falls back to BAC; a lost tag or cancelled task ends the read. PACE fails before the mapping step when the chip's encrypted nonce is empty or not a whole number of cipher blocks (ICAO 9303-11 §4.4.3.3), or decrypts to 0 modulo the curve order. Such a nonce would map the generator to a point the chip knows without the password. BAC also needs the MRZ key, so falling back to it does not let such a chip in. - The eMRTD application (AID
A0 00 00 02 47 10 01) is selected in the order of the Chip Access Procedure (ICAO 9303-11 §4.2). PACE runs with the Master File as current DF, right after EF.CardAccess, and the application is then selected under the PACE Secure Messaging (CLA 0C). Without PACE, or when PACE fails, the application is selected in plain and BAC follows, because "the eMRTD Application MUST be selected before Basic Access Control is performed". A protected SELECT after PACE that the chip refuses with a protected status throwsNFCError.secureMessagingError, and a refusal sent as a plain status word surfaces asNFCError.unexpectedStatusWord; a refused plain SELECT throwsNFCError.bacFailed. Subclasses can overridePassportReader.selectPassportApplication(transport:). - EF.COM and EF.SOD are read first. A data group is selected when either lists it, and only the EF.SOD hash list can rule one out. When EF.COM is requested without EF.SOD and EF.COM omits a requested group, EF.SOD is read as the index too.
- DG1, then the other groups, DG15 and Active Authentication. EAC-protected DG3 and DG4 go last, so a chip that ends Secure Messaging on them cannot cost DG1, DG2, EF.SOD or AA.
- When Secure Messaging ends, the remaining groups are marked
notReadand everything read so far is kept.
readPassport throws when nothing could be read, or when DG1 was requested but not read; DG1 is mandatory. Otherwise it returns a partial model. passport.dataGroupReadStatus explains each group: read, notPresent, unavailable (absent or protected), failed, or notRead. File sizes are capped at 10 MB for DG2-DG7 and 64 KB for the rest, so a corrupt length field cannot trigger huge reads. DG2 is searched at most 8 TLV levels deep (DG2Parser.maxSearchDepth; the ICAO layout needs 3), and the parser walks slices of the file, so nesting does not copy the image at every level. When the user dismisses the sheet, the error is NFCError.userCanceled; check error.isNFCCancellation to stay silent.
Security Report
passport.securityReport has one stage each for CardAccess, PACE, BAC, Chip Authentication, Passive Authentication and Active Authentication.
- Passive Authentication is
succeededonly when the hashes match, the EF.SOD signature verifies, and the Document Signer certificate (DSC) has a validated certification path to a CSCA intrustAnchorsDER. Without trust anchors, matching hashes and a valid signature arefallback. A bad signature, failed certification path validation or hash mismatch isfailed. - EF.SOD checks (ICAO 9303-10 §4.6.2, RFC 5652): the eContentType must be
2.23.136.1.1.1(LDS security object) and the signed contentType attribute must equal it; the LDSSecurityObject version must be 0 (noldsVersionInfo) or 1; data-group hashes always use the LDSSecurityObject hash algorithm, so an unknown one isunsupportedHashAlgorithmrather than a hash mismatch.LDSVersionInfomay be a PrintableString (IA5String and OCTET STRING are also accepted). A DSC with a keyUsage extension must assert digitalSignature, otherwise the status istrustChainInvalid. - Limits of Passive Authentication: no revocation checking (CRLs are not consulted); the certification path is built only from the DSC and
trustAnchorsDER, so a CSCA link certificate must be supplied intrustAnchorsDERtogether with the CSCA that issued it; and EF.SOD must carry exactly one SignerInfo (ICAO recommends one; several are reported as an invalid signature). - PACE is
failed(possible PACE-to-BAC downgrade) when DG14 was read and lists a PACEInfo this library supports, but PACE did not complete. That happens when EF.CardAccess is missing, stripped or altered, or when PACE failed and BAC was used. ICAO 9303-11 §9.2.11 repeats the SecurityInfos of EF.CardAccess in DG14 because EF.CardAccess is unprotected, while Passive Authentication protects DG14. The data read over BAC is still returned. The detail says whether DG14 matched its EF.SOD hash. Read DG14 (and EF.SOD) if you want this check. DG14 PACEInfos the library does not support leave the stage unchanged. You can also run the check yourself withPassportReader.paceStage(_:paceCompleted:dg14SecurityInfos:passiveAuthResult:). - Active Authentication is
succeededonly when the chip signs the challenge, Passive Authentication is fully verified, and DG15 matches its hash in EF.SOD. A valid signature over an unauthenticated DG15 isfallback(ActiveAuthenticationStatus.verifiedUnauthenticatedKey), because a clone can carry its own key pair. A DG15 that does not match its hash isfailed. Once DG15 is known to exist (it was read, or EF.SOD or EF.COM lists it), anything short of a valid signature isfailed. That includes a plain status word to INTERNAL AUTHENTICATE, Secure Messaging ending before or at INTERNAL AUTHENTICATE, and a listed DG15 the chip does not return. A clone without the AA private key can do all of these on purpose.skippedmeans only that you passedperformActiveAuth: false, andnotAdvertisedthat nothing lists DG15. A strict chip can drop Secure Messaging after a plain 6A82 for an absent file. Read EF.COM or EF.SOD so that absent data groups are never selected before AA. - Active Authentication signature checks (ICAO 9303-11 §6.1): an RSA key must have a modulus of at least 1024 bits (
ActiveAuthenticationVerifier.minimumRSAModulusBits) and the ISO/IEC 9796-2 signature must be exactly the modulus length; an ECDSA signature must ber ‖ swith each half exactly the curve order length (BSI TR-03111).ActiveAuthenticationVerifier.verify(...)on its own only says the signature matches the DG15 key; pass its result throughActiveAuthenticationResult.classified(by:)with the Passive Authentication result before treating it as genuine.PassportReaderandPassportModeldo this for you. - INTERNAL AUTHENTICATE length: a short Le returns at most 256 bytes (ISO/IEC 7816-4 §5.1), and under Secure Messaging the reply also carries DO'87 padding, DO'99 and DO'8E. When the DG15 signature or its protected reply does not fit 256 bytes, the reader asks for an extended response (Ne 65 536: DO'97
00 00and outer Le00 00), as ICAO 9303-11 §6.1.4 requires for AA keys longer than 1 848 bits with 3DES or 1 792 bits with AES. The exact cut-off is RSA moduli above 1 848 bits for 3DES and above 1 784 bits for AES, so RSA-2048 and RSA-3072 keys use extended length. Smaller RSA keys and every ECDSA key keep the short Le00.PassportReader.activeAuthenticationExpectedResponseLength(for:mode:)returns the value, andCommandAPDU.internalAuthenticate(data:expectedResponseLength:)builds the command. The chip has to support extended length for those keys (ICAO 9303-11 §9.3.1.1).
Re-check a saved model offline, for example after loading new CSCA certificates:
let result = passport.reverifyPassiveAuthentication(trustAnchorsDER: cscas)
let updated = passport.reverifyingSecurity(trustAnchorsDER: cscas) // PA and AA re-judged
The status types are PassiveAuthenticationStatus (on passiveAuthResult) and ActiveAuthenticationStatus (on activeAuthResult); the DG15 key is an ActiveAuthenticationPublicKey. The 1.x names PassiveAuthStatus, ActiveAuthStatus and ActiveAuthPublicKey are deprecated typealiases.
Offline Models
To rebuild a model from data-group bytes you stored, for example in a file or database, use PassportModel(rawDataGroups:). It applies the reader's own parsers to EF.COM, DG1, DG2, DG7, DG11, DG12, DG14, DG15 and EF.SOD, and runs Passive Authentication unless performPassiveAuthentication is false. Passive Authentication verifies a CMS signature, so keep it off the main thread. Active Authentication needs the chip, so activeAuthResult is nil and the chip stages of the security report are skipped.
let model = PassportModel(
rawDataGroups: savedDataGroups, // [DataGroupId: Data], TLV-wrapped
cardAccessRaw: savedCardAccess, // optional
trustAnchorsDER: cscas
)
print(model.mrz?.documentNumber ?? "-")
Current Scope
PACE (ECDH Generic Mapping, MRZ password) is implemented with OpenSSL on the standardized domain parameters 11-18: brainpoolP224r1, brainpoolP256r1, brainpoolP320r1, brainpoolP384r1, brainpoolP512r1 and NIST P-256, P-384 and P-521 (DomainParameterID.isSupportedForPACE, isNISTCurve, isBrainpoolCurve). It rejects a chip mapping or ephemeral public key that reflects the terminal's own key. DH and Integrated / Chip Authentication Mapping, the 192-bit curves, secp224r1 and explicit domain parameters (PACEDomainParameterInfo, kept in SecurityInfos.paceDomainParameterInfos) are not supported: PACEHandler.performPACE throws unsupportedOperation before sending any APDU (PACEHandler.isSupported(_:) and unsupportedReason(for:) tell you why), and the reader falls back to BAC. performPACE accepts optional mappingPrivateKey: and ephemeralPrivateKey: scalars in [1, n-1] to reproduce published test vectors such as ICAO 9303-11 Appendix G.1; leave them nil in production so random keys are generated.
Secure Messaging uses an SSC as long as the cipher block (ICAO 9303-11 §9.8.2): 8 bytes for 3DES, 16 for AES (an 8-byte AES SSC is zero-extended); SecureMessagingTransport created with any other length throws invalidParameter on every exchange without sending anything. A protected command that needs extended length carries the extended outer Le 0000. Responses must contain DO'99 and DO'8E: §9.8.4 lists DO'99 as optional, but without it the status word is not covered by the MAC.
BAC, Secure Messaging, data-group parsing, Passive Authentication and Active Authentication are implemented. Chip Authentication is read from DG14 but not performed, because its ECDH key agreement is not implemented; the stage reports notSupported when advertised.