Verification
NFC verification
Read the ePassport / eID chip on the device, validate it on the server — BAC/PACE, DG1/DG2/SOD and passive authentication.
The NFC_VERIFICATION module reads the contactless chip embedded in an e-passport or biometric ID and validates its cryptographically-signed contents. Chip data is signed by the issuing authority, so a successful read is far stronger evidence than an OCR'd photo — and it yields a high-quality chip portrait for face match.
Reading the chip needs the device NFC radio, which a WebView cannot reach. So NFC is a native-SDK-only capability: the iOS, Android and Flutter SDKs read the chip, and the Obsidian backend does the cryptography. The pattern is read on device, validate on server — the SDK returns raw data groups, the backend performs passive authentication.
Info
On a web host (iframe / popup) the flow advertises no NFC capability, so NFC_VERIFICATION falls back to a server-side manual review (nfc_not_available) rather than blocking the applicant. Native hosts advertise capabilities: ["nfc"] and run the real chip read.
Why NFC beats OCR
OCR reads printed ink and a photographed portrait — both of which a forger can alter on a fake booklet. NFC reads the chip the issuing authority personalised, whose contents are protected by a signature (the SOD) that a tamper would break:
| OCR (document capture) | NFC (chip read) | |
|---|---|---|
| Source of truth | Printed page + photo | Issuer-signed chip |
| Portrait quality | Photo of a photo | Full-resolution DG2 image ✅ |
| Tamper evidence | Forensics heuristics ⚠️ | Cryptographic hash check ✅ |
| Proof of issuer | None | CSCA trust chain (when configured) ✅ |
| Works in a WebView | ✅ | ❌ (needs native radio) |
NFC doesn't replace document capture — the OCR'd MRZ is what derives the chip key and what the chip is cross-checked against. They run in sequence.
End-to-end flow
Document scanned in the flow
The applicant photographs the document; the engine OCRs the MRZ (document number, date of birth, date of expiry).
Native SDK reads the chip
When the flow reaches the NFC step and the host advertises the nfc capability, it issues nfc.scan over the capability bridge with the MRZ fields. The SDK derives the BAC key from the MRZ (or a CAN for PACE), opens secure messaging, and reads DG1 (MRZ), DG2 (face) and EF.SOD (the signed hash set) — returning them base64, raw bytes only.
Server validates
The client posts the data groups to POST /api/verify/<token>/nfc/, which runs passive authentication and resolves the NFC_VERIFICATION module.
The SDKs also expose a standalone readNfc(...) API for reading a chip outside the hosted flow. Per-SDK setup (iOS entitlements, Android/Flutter permissions, deps) is in the NFC SDK guide. Under the hood: iOS NFCPassportReader, Android JMRTD + BouncyCastle, Flutter dmrtd.
What the SDK sends the server
The native read returns three base64 blobs, which the client posts unchanged:
{
"sod_b64": "<EF.SOD, base64>",
"dg1_b64": "<DG1 (MRZ), base64>",
"dg2_b64": "<DG2 (face), base64>"
}What the server validates
The backend (nfc_service.passive_authenticate) does three things:
- Passive authentication (tamper check). Parse EF.SOD (CMS
SignedData→LDSSecurityObject), recompute the hash of each uploaded data group (SHA-1 or SHA-256, per the SOD), and compare it to the SOD's stored value. Any mismatch means the chip data was altered after signing. - MRZ read-back. Parse DG1 and compare the document number to the document the applicant already scanned — this catches a genuine chip lifted from another booklet. A mismatch caps the verdict at review.
- Trust chain (optional). Verifying that the SOD signer chains to a trusted Country Signing CA requires an ICAO CSCA masterlist. Set
NFC_CSCA_DIRto a masterlist directory to enable it.
Warning
Integrity alone does not prove authenticity. Without a configured CSCA masterlist, an integrity-verified chip resolves to review, not approved — an attacker can forge a chip whose internal hashes are self-consistent; only the CSCA chain proves the chip was signed by a real issuing authority.
Decision mapping
| Condition | NFC_VERIFICATION result |
|---|---|
| A data-group hash doesn't match the SOD (tamper) | declined ❌ |
| SOD unparseable / no data groups to check | in_review ⚠️ |
| MRZ read-back mismatch (chip vs scanned document) | in_review ⚠️ |
| Integrity OK, no CSCA masterlist configured | in_review ⚠️ |
| Integrity OK and CSCA chain verified | approved ✅ |
| Host has no NFC capability (web fallback) | in_review (nfc_not_available) ⚠️ |
Common failure reasons
If the native read fails, the SDK surfaces a stable code the flow can act on — the read never silently succeeds:
| Code | What happened | What to do |
|---|---|---|
wrong_key | The BAC/PACE key derived from the MRZ (or CAN) didn't open the chip — usually a mistyped or mis-OCR'd document number, DOB or expiry. | Re-confirm the MRZ fields and retry; offer manual entry. |
tag_lost | The phone lost contact with the chip mid-read (moved too soon). | Ask the applicant to hold the phone still against the document. |
nfc_disabled | NFC is off (or unavailable) on the device. | Prompt to enable NFC, or fall back to document-only. |
user_cancelled | The applicant dismissed the native scan sheet. | Re-prompt or let them skip to review. |
timeout | No chip detected within the read window. | Reposition the phone over the chip (passports: rear cover; ID cards: centre) and retry. |
| Unsupported document | The document has no readable chip (a non-biometric ID or an unsupported booklet). | Fall through to the document-only path; the module reviews rather than blocks. |
A wrong_key after a good OCR read most often means the MRZ was misread — the chip key is derived from exactly those three fields, so a single wrong digit fails key derivation. Let the applicant correct the prefilled MRZ before retrying.
Device support
NFC reading depends on the device having an NFC radio and the OS exposing it to apps: modern iPhones (via Core NFC / NFCPassportReader) and most Android phones with NFC hardware. Web browsers expose no passport-chip API, which is why the hosted flow only offers the NFC step on native hosts that advertise the nfc capability and otherwise falls through to the server-side review.