Aller au contenu

POC MRZ parser — ICAO 9303 (TD1, TD2, TD3) + checksums

POC : poc-mrz-parser/ (~500 lignes Kotlin pur, 0 dépendance externe). Référence pour l’implémentation prod de mrz-svc dans la pipeline biométrique.

Status : 18/18 tests passants sur les vecteurs de référence ICAO 9303 + cas dégradés.

Ce POC démontre qu’un parser MRZ conforme ICAO 9303 peut être construit en Kotlin pur, sans dépendance ML, sans appel commercial. La MRZ est déterministe : son intégrité est vérifiée par 5 chiffres de contrôle (algorithme 7-3-1), et tout échec d’un seul checksum suffit à rejeter la zone — avec gradation lenient/strict.


ArgumentDétail
Standard publicICAO 9303 est public, stable depuis 2015, 8e édition. Aucun différenciateur commercial.
Source de véritéSur un passeport, la MRZ est la zone que les portes d’embarquement et les bornes douanières lisent. Plus fiable que l’OCR de la zone visible (typo possible, dégradation imprimée).
Cross-checkComparée avec l’OCR de la zone visible, elle détecte ~70 % des falsifications grossières (nom modifié, photo recollée).
Hors-ligneAucun appel réseau requis — fonctionne en air-gap.
Coût0 USD de coût marginal. Fournisseurs commerciaux facturent 0,02–0,08 USD/parsing pour quasiment rien.

poc-mrz-parser/
├── build.gradle.kts // Kotlin 2.0, JUnit 5, AssertJ — pas d'autre dep
├── src/main/kotlin/io/vitakyc/mrz/
│ ├── Model.kt // MrzFormat, MrzResult, MrzDate, Sex, MrzError, MrzWarning
│ ├── Checksum.kt // ICAO 7-3-1 (compute + verify + valueOf)
│ ├── MrzParser.kt // détection format, parseTd1/td2/td3, helpers
│ └── Main.kt // démo CLI 5 cas
└── src/test/kotlin/io/vitakyc/mrz/
└── MrzParserTest.kt // 18 tests

3. Algorithme du chiffre de contrôle ICAO (7-3-1)

Section intitulée « 3. Algorithme du chiffre de contrôle ICAO (7-3-1) »
fun compute(input: String): Int {
var sum = 0
for ((i, c) in input.withIndex()) {
sum += valueOf(c) * WEIGHTS[i % 3] // WEIGHTS = [7, 3, 1]
}
return sum % 10
}
fun valueOf(c: Char): Int = when {
c in '0'..'9' -> c - '0'
c in 'A'..'Z' -> 10 + (c - 'A')
c == '<' -> 0
else -> error("invalid MRZ character: '$c'")
}

Tests reference values validés directement :

InputCheck expectedTest
L898902C36doc# du passeport TD3 standard ICAO
7408122DOB du passeport TD3 standard ICAO
1204159expiry du passeport TD3 standard ICAO
ZE184226B<<<<<1personal number TD3

4.1 TD1 — cartes ID-1 (CNI biométriques, incl. CIN tunisienne 2017+)

Section intitulée « 4.1 TD1 — cartes ID-1 (CNI biométriques, incl. CIN tunisienne 2017+) »

3 lignes × 30 caractères. Champs (1-based) :

Ligne 1: doc_code(2) issuing_state(3) doc_number(9) check(1) optional1(15)
Ligne 2: dob(6) check(1) sex(1) expiry(6) check(1) nationality(3) optional2(11) composite(1)
Ligne 3: surname << given_names (padded with '<')

5 chiffres de contrôle : doc#, dob, expiry, composite + (pas de personal number en TD1, donc 4 effectifs).

4.2 TD2 — titres de séjour (visas, titres séjour UE)

Section intitulée « 4.2 TD2 — titres de séjour (visas, titres séjour UE) »

2 lignes × 36 caractères :

Ligne 1: doc_code(2) issuing_state(3) name(31)
Ligne 2: doc_number(9) check(1) nationality(3) dob(6) check(1) sex(1) expiry(6) check(1) optional(7) composite(1)

2 lignes × 44 caractères :

Ligne 1: doc_code(2) issuing_state(3) name(39)
Ligne 2: doc_number(9) check(1) nationality(3) dob(6) check(1) sex(1) expiry(6) check(1) personal(14) check(1) composite(1)

5 chiffres de contrôle : doc#, dob, expiry, personal_number, composite.

FormatComposite input
TD1line1[5..29] (25 chars) + line2[0..6] (7) + line2[8..14] (7) + line2[18..28] (11)
TD2line2[0..9] + line2[13..19] + line2[21..27] + line2[28..34]
TD3line2[0..9] + line2[13..19] + line2[21..27] + line2[28..42]

=== VitaKYC POC MRZ parser — démo ICAO 9303 ===
[1] Passeport TD3 ICAO standard (Anna Maria Eriksson)
format=TD3, country=UTO, nationality=UTO
doc#=L898902C3, ERIKSSON, ANNA MARIA
DOB=1974-08-12, sex=F, expiry=2012-04-15
checksumScore=1,00, warnings=0
[2] CNI TD1 ICAO standard (Anna Maria Eriksson)
format=TD1, country=UTO, nationality=UTO
doc#=D23145890, ERIKSSON, ANNA MARIA
DOB=1974-08-12, sex=F, expiry=2012-04-15
checksumScore=1,00, warnings=0
[3] Titre séjour TD2 ICAO standard
format=TD2, country=UTO, nationality=UTO
doc#=D23145890, ERIKSSON, ANNA MARIA
DOB=1974-08-12, sex=F, expiry=2012-04-15
checksumScore=1,00, warnings=0
[4] MRZ corrompue (1 chiffre changé) — non strict → warning ; strict → exception
warnings : [CHECKSUM_MISMATCH documentNumber expected=6 actual=0,
CHECKSUM_MISMATCH composite expected=8 actual=0]
checksumScore : 0,60
strict mode: check digit mismatch on 'documentNumber': expected 6, got 0
[5] Format non détecté
cannot detect MRZ format (lines=1, length=7)
=== Démo terminée ===

#TestVérifie
1ICAO checksum reference valuesalgorithme 7-3-1 sur 4 inputs ICAO publics
2ICAO checksum char valuesconversion lettres / chiffres / <
3TD3 standard parse — full successparse complet du passeport ICAO Eriksson
4TD1 standard parse — full successparse complet TD1
5TD2 standard parse — full successparse complet TD2
6format detection by line count and length3 cas TD1/TD2/TD3
7format detection fails on unknown shapejet MrzError.FormatNotDetected
8invalid character rejectedMrzError.InvalidCharacter
9invalid line length rejectedMrzError.FormatNotDetected (longueur ≠ standard)
10corrupted document number — strict throws, lenient warnsgradation strict/lenient
11sex parsing — M F X and chevronM, F, < → X
12date of birth century inference — sliding windowyy=74 → 1974 ; yy=05 → 2005 (ref 2026)
13expiry date is always future centuryyy=12 → 2012
14checksum verify — chevron is not a valid digit< rejette comme check digit (sauf personal vide)
15TD3 personal number empty allows chevron check digitspec ICAO §4.4
16name parsing — multiple given names with single chevron< séparateur intra-prénoms
17surname only without given namesnom seul
18MRZ checksum score reflects partial corruptionscore = passed/total

mrz-svc est un microservice Kotlin/Ktor exposant :

POST /v1/mrz/parse
Content-Type: text/plain
Body: <raw MRZ as 2 or 3 lines>
→ 200 { format, documentNumber, surname, ..., checksumScore, warnings }
→ 400 { error: "InvalidCharacter" | "FormatNotDetected" | "CheckDigitMismatch", details }

Le parser présenté ici est extrait tel quel ; le service ajoute :

  • Authentification mTLS entre bio-svc et mrz-svc
  • Métriques Prometheus : count par format, latence p95, distribution checksumScore
  • Logs traceables avec traceId Temporal
  • Rate limit par tenant
  • Endpoint OpenAPI versionné

Latence cible p99 : ≤ 5 ms (parsing pur, sans réseau).


  • NFC : lecture du chip biométrique passport via BAC/PACE puis DG1 (qui contient la MRZ stockée). Validation croisée NFC ↔ MRZ visible. Reporté V2 (cf ADR-028).
  • MRZ visa type A (1 ligne × 44) : rare en KYC banque, reporté V2.
  • OCR de la MRZ : ce POC suppose la MRZ déjà extraite en string. L’OCR de la zone MRZ (font OCR-B, fixe) est typiquement délégué à un détecteur dédié dans ocr-svc ou Regula DR SDK.
  • Country alpha-3 dictionary : pour valider que UTO, TUN, FRA, etc. sont des codes ISO 3166 valides. Trivial mais hors scope POC.
  • Date of birth post-validation : DOB > today → erreur. À ajouter dans bio-svc cross-check, pas dans le parser.

Fenêtre de terminal
cd poc-mrz-parser
./gradlew test # 18/18 tests verts
./gradlew run # démo CLI 5 cas

Dépendances : Kotlin 2.0.20, JVM 17, JUnit 5.11, AssertJ 3.26. Aucune dépendance externe runtime.


  • Pipeline biométrique — architecture complète
  • ADR-028 — pipeline biométrique
  • Standard : ICAO Doc 9303 (parties 3, 4, 5 spécifiques aux MRZ TD1/TD2/TD3)
  • Algorithme check digit ICAO : ICAO 9303-3 §4.9
  • Vecteurs de test publics : ICAO 9303-3 Appendix A (Anna Maria Eriksson)