Skip to main content

One post tagged with "ios"

View All Tags

Ce înseamnă să integrezi CEI prin NFC. Un ghid pentru ingineri.

· 8 min read
Cătălin Toma
Founder, EidKit

Acesta este al treilea articol din seria noastră despre carta electronică de identitate. Articolele anterioare acoperă problema adresei care strică fluxurile KYC și ce poate semna buletinul tău electronic.

Dacă ai mai integrat un pașaport electronic sau un alt document de identitate cu cip, vei intra în acest proiect cu un set de presupuneri rezonabile. Cele mai multe dintre ele sunt parțial greșite pentru CEI.

Nu este că standardele ICAO nu se aplică — se aplică, ca punct de plecare. Problema este că CEI este un card de identitate național cu extensii specifice care nu apar nicăieri documentate complet în public. Ce urmează este o hartă a terenului bazată pe implementări complete pentru Android și iOS, testate pe carduri CEI reale.


Ce știi deja — și ce se aplică

Orice card de identitate electronic bazat pe standardele ICAO folosește PACE (Password Authenticated Connection Establishment) pentru a stabili un canal securizat înainte de a permite citirea oricărui datum. CEI face același lucru, cu codul CAN — 6 cifre tipărite pe fața cardului — ca parolă.

Rezultatul PACE este un canal Secure Messaging (SM) care împachetează toate comenzile APDU ulterioare. Orice comandă trimisă raw după stabilirea canalului este respinsă de card — comportament standard, nu specific CEI.

Autentificarea pasivă funcționează după principii cunoscute pe ambele platforme: cipul conține un Security Object Document care înlănțuie hash-urile SHA-256 ale grupelor de date până la certificatul rădăcină CSCA al MAI. Verificarea arată diferit în funcție de platformă, dar principiul este același:

Pe Android, jMRTD gestionează parsarea SOD-ului. Verificarea lanțului se face manual față de certificatul CSCA distribuit în bundle cu SDK-ul:

val sod = SODFile(sodRaw.inputStream())
val dsc = sod.docSigningCertificate
val csca = assets.open("csca_romania.der").use {
CertificateFactory.getInstance("X.509").generateCertificate(it) as X509Certificate
}
dsc.verify(csca.publicKey) // aruncă excepție dacă invalid

Autentificarea pasivă trebuie să ruleze întotdeauna înainte de a folosi datele citite. Până aici, un inginer cu experiență în documente ICAO va fi confortabil. Aceasta este și cam limita terenului familiar.


Unde presupunerile încep să se destrame

Cipul are patru applet-uri, nu unul

Documentele de călătorie ICAO standard au o structură de applet relativ predictibilă. CEI nu urmează același tipar. Cipul conține patru applet-uri cu roluri distincte:

AppletRol
AID1 / National AppPunct de intrare PACE, găzduiește parametrii de securitate
ICAO LDSFotografie, semnătură olografă digitizată, SOD pentru autentificare pasivă
EDATADate personale: nume, CNP, adresă de domiciliu
GenPKIChei și certificate pentru autentificare activă și semnare

Applet-ul ESIGN există pe cip și apare în unele documente de referință. Nu este folosit. Semnarea se face prin GenPKI, printr-o comandă diferită de ce ai presupune din lectura standardelor.

Fiecare applet urmează propriul flux de selecție și autentificare. Nu selectezi un applet și citești ce ai nevoie.

Două faze, cerințe diferite

Citirea datelor de pe CEI se împarte natural în două faze:

Faza 1 — doar CAN: accesează datele disponibile fără PIN — fotografia titularului, semnătura olografă digitizată și datele necesare pentru autentificarea pasivă. Această fază folosește applet-ul ICAO standard.

Faza 2 — CAN + PIN de 4 cifre: accesează datele personale complete din applet-ul EDATA, inclusiv adresa de domiciliu — care nu mai apare tipărită pe cardul fizic.

Ordinea operațiilor înainte de PACE nu este documentată — și contează

Ce trebuie să faci înainte de PACE depinde de ce vrei să faci după PACE, iar regulile sunt asimetrice în funcție de scenariu. Faza 1 cere o pregătire diferită față de Faza 2 și GenPKI. Dacă pregătirea nu este cea corectă, eșuările apar în puncte neașteptate cu coduri de eroare care nu indică problema reală.

Situația este diferită pe cele două platforme din motive arhitecturale:

Stack-ul NFC Android pornește în contextul MF — niciun AID nu este pre-selectat. Aceasta este starea corectă pentru Faza 1: PACE funcționează direct în context MF, după care SM SELECT ICAO oferă acces la DG2/DG7/SOD.

// Faza 1: nicio pre-selecție necesară — stack-ul Android pornește în MF
isoDep.timeout = 20_000 // timeout-ul implicit este insuficient pentru CEI

val paceResult = ps.doPACE(canKey, paceOid, paceParams, null)
val wrapper = paceResult.wrapper
// toate comenzile de acum înainte trec prin wrapper

// Faza 2: SELECT AID1 înainte de PACE — chip-ul cere context AID1 pentru EDATA/GenPKI

Comportamentul asimetric al pregătirii pre-PACE nu este documentat nicăieri — a fost descoperit prin eliminare pe ambele platforme.


Formatele de date: unde implementarea românească diverge

DG1 nu este MRZ

Acesta este momentul în care codul care funcționează perfect la pașapoarte se rupe complet, pe ambele platforme. Datele de identitate returnate de applet-ul EDATA nu sunt în formatul MRZ pe care îl parsează bibliotecile ICAO standard — sunt într-un format ASN.1 specific implementării românești, cu diacritice corecte și câmpuri structurate diferit:

SEQUENCE (0x30)
[0] (0x80) lastName UTF-8 ex. "TOMA"
[1] (0x81) firstName UTF-8 ex. "CĂTĂLIN" (cu diacritice)
[2] (0x82) sex UTF-8 "M" sau "F"
[3] (0x83) dateOfBirth UTF-8 DDMMYYYY
[4] (0x84) cnp UTF-8 13 cifre
[5] (0x85) nationality UTF-8 "ROU"

jMRTD's DG1File nu poate parsa acest format. Este nevoie de un parser propriu:

// jMRTD nu ajută aici — format ASN.1 specific românesc
val tags = parseContextTags(dg1Bytes) // parser propriu
val lastName = tags[0x80]?.toString(Charsets.UTF_8)
val firstName = tags[0x81]?.toString(Charsets.UTF_8) // diacriticele sunt corecte
val cnp = tags[0x84]?.toString(Charsets.UTF_8)

Formatul nu este documentat public — a fost determinat prin inspecție directă a bytes returnați de card.

Cele două chei criptografice din GenPKI

GenPKI conține două chei distincte, pe curbe eliptice diferite, cu comportamente interne diferite la semnare:

OperațiunePINComportament intern
Autentificare activă4 cifreCheie pe secp384r1, referință 0x81
Semnare document6 cifreCheie pe brainpoolP384r1, referință 0x8E

A le confunda produce semnături incorecte fără niciun mesaj de eroare care să indice cauza. Comportamentul este identic pe Android și iOS — aceasta este o constrângere a cipului, nu a platformei.


Lucruri care se rup înainte să ajungi la logica de business

Furnizorul criptografic trebuie înregistrat explicit, în ordinea corectă, înainte de orice operație pe cip. Ordinea greșită produce eșuări silențioase:

Security.removeProvider("BC")
Security.insertProviderAt(BouncyCastleProvider(), 1)

Android 13+ a schimbat API-ul pentru interceptarea tag-urilor NFC. Dacă suporți versiuni mai vechi, gestionezi două variante cu comportamente ușor diferite.

PIN counter query nu funcționează în modul SM, pe nicio platformă. Nu există cale să interoghezi numărul de încercări rămase înainte de a trimite PIN-ul efectiv. Tratezi SW=63CX în răspunsul la VERIFY (X = încercări rămase) și SW=6983 pentru card blocat — detaliu care afectează direct UX-ul aplicației.


Scriem despre CEI — capabilitățile sale, provocările de integrare și contextul reglementar din jurul său. Dacă un subiect de aici este relevant pentru ce construiești, scrie-ne.