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 demrz-svcdans 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.
1. Pourquoi un parser MRZ interne
Section intitulée « 1. Pourquoi un parser MRZ interne »| Argument | Détail |
|---|---|
| Standard public | ICAO 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-check | Comparée avec l’OCR de la zone visible, elle détecte ~70 % des falsifications grossières (nom modifié, photo recollée). |
| Hors-ligne | Aucun appel réseau requis — fonctionne en air-gap. |
| Coût | 0 USD de coût marginal. Fournisseurs commerciaux facturent 0,02–0,08 USD/parsing pour quasiment rien. |
2. Architecture
Section intitulée « 2. Architecture »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 tests3. 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 :
| Input | Check expected | Test |
|---|---|---|
L898902C3 | 6 | doc# du passeport TD3 standard ICAO |
740812 | 2 | DOB du passeport TD3 standard ICAO |
120415 | 9 | expiry du passeport TD3 standard ICAO |
ZE184226B<<<<< | 1 | personal number TD3 |
4. Formats supportés
Section intitulée « 4. Formats supportés »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)4.3 TD3 — passeports
Section intitulée « 4.3 TD3 — passeports »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.
4.4 Composite — formules par format
Section intitulée « 4.4 Composite — formules par format »| Format | Composite input |
|---|---|
| TD1 | line1[5..29] (25 chars) + line2[0..6] (7) + line2[8..14] (7) + line2[18..28] (11) |
| TD2 | line2[0..9] + line2[13..19] + line2[21..27] + line2[28..34] |
| TD3 | line2[0..9] + line2[13..19] + line2[21..27] + line2[28..42] |
5. Output démo CLI
Section intitulée « 5. Output démo CLI »=== 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 ===6. Couverture des tests
Section intitulée « 6. Couverture des tests »| # | Test | Vérifie |
|---|---|---|
| 1 | ICAO checksum reference values | algorithme 7-3-1 sur 4 inputs ICAO publics |
| 2 | ICAO checksum char values | conversion lettres / chiffres / < |
| 3 | TD3 standard parse — full success | parse complet du passeport ICAO Eriksson |
| 4 | TD1 standard parse — full success | parse complet TD1 |
| 5 | TD2 standard parse — full success | parse complet TD2 |
| 6 | format detection by line count and length | 3 cas TD1/TD2/TD3 |
| 7 | format detection fails on unknown shape | jet MrzError.FormatNotDetected |
| 8 | invalid character rejected | MrzError.InvalidCharacter |
| 9 | invalid line length rejected | MrzError.FormatNotDetected (longueur ≠ standard) |
| 10 | corrupted document number — strict throws, lenient warns | gradation strict/lenient |
| 11 | sex parsing — M F X and chevron | M, F, < → X |
| 12 | date of birth century inference — sliding window | yy=74 → 1974 ; yy=05 → 2005 (ref 2026) |
| 13 | expiry date is always future century | yy=12 → 2012 |
| 14 | checksum verify — chevron is not a valid digit | < rejette comme check digit (sauf personal vide) |
| 15 | TD3 personal number empty allows chevron check digit | spec ICAO §4.4 |
| 16 | name parsing — multiple given names with single chevron | < séparateur intra-prénoms |
| 17 | surname only without given names | nom seul |
| 18 | MRZ checksum score reflects partial corruption | score = passed/total |
7. Comment ce POC se branche dans mrz-svc
Section intitulée « 7. Comment ce POC se branche dans mrz-svc »mrz-svc est un microservice Kotlin/Ktor exposant :
POST /v1/mrz/parseContent-Type: text/plainBody: <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-svcetmrz-svc - Métriques Prometheus : count par format, latence p95, distribution
checksumScore - Logs traceables avec
traceIdTemporal - Rate limit par tenant
- Endpoint OpenAPI versionné
Latence cible p99 : ≤ 5 ms (parsing pur, sans réseau).
8. Hors scope POC (à ajouter en prod)
Section intitulée « 8. Hors scope POC (à ajouter en prod) »- 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-svcou 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-svccross-check, pas dans le parser.
9. Reproduire le POC
Section intitulée « 9. Reproduire le POC »cd poc-mrz-parser./gradlew test # 18/18 tests verts./gradlew run # démo CLI 5 casDépendances : Kotlin 2.0.20, JVM 17, JUnit 5.11, AssertJ 3.26. Aucune dépendance externe runtime.
10. Références
Section intitulée « 10. Références »- 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)