MRZ ICAO 9303: como validar cartões e passaportes
Formatos TD1, TD2 e TD3, dígitos de controlo com pesos 7-3-1 e módulo 10, exemplo com o espécime oficial da ICAO e código JavaScript para validar a MRZ.
Por Equipa Constaia8 min de leitura
A zona de leitura ótica (MRZ, do inglês machine readable zone) são aquelas duas ou três linhas cheias de < no fundo da página do passaporte ou no verso de um cartão de identificação. Foi pensada para ser lida por máquinas sem erros, e por isso inclui dígitos de controlo: se o OCR confundir um carácter, as contas deixam de bater certo.
Este artigo explica os formatos, o algoritmo dos dígitos de controlo com um exemplo real do documento da ICAO, código para a validar e, sobretudo, o que uma MRZ válida não demonstra.
A norma: ICAO Doc 9303
A MRZ é definida pelo Doc 9303 da ICAO (8.ª edição). A Parte 3 fixa o que é comum a todos os documentos; as Partes 4 a 7 fixam cada formato. O essencial:
- Só se usam A–Z, 0–9 e o carácter de preenchimento
<, impressos no tipo de letra OCR-B. - As datas escrevem-se como AAMMDD: 12 de julho de 1942 fica
420712. - Os nomes são transliterados sem acentos. Por exemplo, a tabela da ICAO permite escrever o Ñ espanhol como
NouNXX.
Três formatos
| Formato | Linhas × caracteres | Uso típico | Parte do Doc 9303 |
|---|---|---|---|
| TD1 | 3 × 30 | Cartões de identificação (tamanho cartão bancário), como o DNI espanhol | Parte 5 |
| TD2 | 2 × 36 | Alguns documentos oficiais de tamanho intermédio | Parte 6 |
| TD3 | 2 × 44 | Passaportes | Parte 4 |
O TD2 é parecido com o TD3, com menos espaço: o campo do nome é mais curto e tem menos um dígito de controlo. Na prática, o que mais vai encontrar em formulários europeus é TD1 (cartões de identificação) e TD3 (passaportes).
O dígito de controlo: pesos 7-3-1 e módulo 10
A Parte 3 (secção 4.9) descreve o cálculo:
- Cada carácter tem um valor: os algarismos valem o que indicam,
<vale 0 e as letras vão de A = 10 a Z = 35. - Multiplica-se cada valor por um peso que se repete: 7, 3, 1, 7, 3, 1…
- Somam-se os produtos.
- O dígito de controlo é o resto da divisão por 10.
| A | B | C | D | E | F | G | H | I | J | K | L | M |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
| N | O | P | Q | R | S | T | U | V | W | X | Y | Z |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 |
O exemplo do apêndice A da Parte 3: a data de 27 de julho de 1952 (520727).
| Carácter | 5 | 2 | 0 | 7 | 2 | 7 |
|---|---|---|---|---|---|---|
| Peso | 7 | 3 | 1 | 7 | 3 | 1 |
| Produto | 35 | 6 | 0 | 49 | 6 | 7 |
Soma: 103. Resto da divisão por 10: 3. Na MRZ escreve-se 5207273.
Exemplo resolvido: o passaporte espécime da ICAO (TD3)
O apêndice A da Parte 4 mostra um passaporte fictício do estado da "Utopia" (código UTO) em nome de Anna Maria Eriksson:
PPUTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<
L898902C36UTO7408122F3404159ZE184226B<<<<<16A primeira linha contém o tipo de documento (PP), o estado emissor (UTO) e o nome: apelido, <<, nomes próprios separados por <. A segunda linha tem os dados com os respetivos dígitos de controlo:
| Posições | Campo | Valor | Controlo |
|---|---|---|---|
| 1–9 | Número do documento | L898902C3 | 10: 6 |
| 11–13 | Nacionalidade | UTO | — |
| 14–19 | Data de nascimento | 740812 | 20: 2 |
| 21 | Sexo | F | — |
| 22–27 | Data de validade | 340415 | 28: 9 |
| 29–42 | Número pessoal / dados opcionais | ZE184226B<<<<< | 43: 1 |
| 44 | Controlo composto | — | 6 |
Vamos verificar o número do documento, L898902C3:
| Carácter | L | 8 | 9 | 8 | 9 | 0 | 2 | C | 3 |
|---|---|---|---|---|---|---|---|---|---|
| Valor | 21 | 8 | 9 | 8 | 9 | 0 | 2 | 12 | 3 |
| Peso | 7 | 3 | 1 | 7 | 3 | 1 | 7 | 3 | 1 |
| Produto | 147 | 24 | 9 | 56 | 27 | 0 | 14 | 36 | 3 |
Soma: 316 → resto 6. Coincide com a posição 10.
Da mesma forma, a data de nascimento 740812 soma 122 (controlo 2), a validade 340415 soma 69 (controlo 9) e o número pessoal soma 401 (controlo 1).
O dígito composto
O último carácter protege a linha inteira. Calcula-se sobre as posições 1–10, 14–20 e 22–43 da segunda linha, ou seja, os campos com os seus próprios dígitos de controlo, saltando a nacionalidade e o sexo:
L898902C36 + 7408122 + 3404159ZE184226B<<<<<1A soma ponderada é 896 → controlo composto 6, o último carácter da linha.
Se o campo do número pessoal estiver vazio (só <), o seu dígito de controlo pode ser < ou 0. Tenha isso em conta ao validar.
TD1: o formato dos cartões de identificação
O espécime da Parte 5 é um cartão da mesma titular:
I<UTOD231458907<<<<<<<<<<<<<<<
7408122F1204159UTO<<<<<<<<<<<6
ERIKSSON<<ANNA<MARIA<<<<<<<<<<- Linha 1: tipo (
I<), estado emissor (UTO), número do documento nas posições 6–14 (D23145890) e o seu controlo na 15 (7); da 16 à 30, dados opcionais. - Linha 2: nascimento (1–6) e controlo (7), sexo (8), validade (9–14) e controlo (15), nacionalidade (16–18), dados opcionais (19–29) e controlo composto (30).
- Linha 3: o nome.
O composto do TD1 calcula-se sobre as posições 6–30 da linha 1 e 1–7, 9–15 e 19–29 da linha 2. No espécime dá 6.
Um pormenor: se o número do documento tiver mais de 9 caracteres, na posição 15 vai um < e o número continua na zona de dados opcionais, com o dígito de controlo no fim.
Código em JavaScript
const WEIGHTS = [7, 3, 1];
function charValue(c: string): number {
if (c === "<") return 0;
if (c >= "0" && c <= "9") return c.charCodeAt(0) - 48; // 0–9
if (c >= "A" && c <= "Z") return c.charCodeAt(0) - 55; // A=10 … Z=35
throw new Error(`Carácter inválido na MRZ: ${c}`);
}
export function checkDigit(data: string): string {
let sum = 0;
for (let i = 0; i < data.length; i++) sum += charValue(data[i]) * WEIGHTS[i % 3];
return String(sum % 10);
}
export function validateTd3(line1: string, line2: string) {
if (line1.length !== 44 || line2.length !== 44) throw new Error("TD3: duas linhas de 44");
const fields: Record<string, [string, string]> = {
document_number: [line2.slice(0, 9), line2[9]],
birth_date: [line2.slice(13, 19), line2[19]],
expiry_date: [line2.slice(21, 27), line2[27]],
personal_number: [line2.slice(28, 42), line2[42]],
composite: [line2.slice(0, 10) + line2.slice(13, 20) + line2.slice(21, 43), line2[43]],
};
return Object.fromEntries(
Object.entries(fields).map(([name, [data, digit]]) => [name, checkDigit(data) === digit]),
);
}
validateTd3(
"PPUTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<",
"L898902C36UTO7408122F3404159ZE184226B<<<<<16",
);
// { document_number: true, birth_date: true, expiry_date: true,
// personal_number: true, composite: true }Se mudar a validade para 340416, falham o controlo da validade e o composto. É assim que se deteta um erro de leitura (ou uma edição desajeitada).
O que o código acima não cobre
- Erros de OCR:
O/0,I/1,B/8eS/5confundem-se com frequência. Nos campos que só podem ser numéricos (datas, dígitos de controlo) pode corrigir com segurança; no número do documento, que é alfanumérico, pode testar substituições e ficar com a que bate certo. - O século:
74pode ser 1974 ou 2074. Para o nascimento, escolha o ano que não fica no futuro; para a validade, o que for coerente com a validade do documento. - Linhas cortadas: se uma fotografia corta o fim da linha, preencha com
<apenas se souber que faltavam caracteres de preenchimento, e marque o resultado como duvidoso.
Coerência entre a MRZ e a zona visual
Uma MRZ coerente não basta: tem de coincidir com o que está impresso na zona de inspeção visual (VIZ). Compare pelo menos:
- número do documento,
- datas de nascimento e de validade,
- sexo e nacionalidade,
- apelidos e nomes, tendo em conta a transliteração (sem acentos,
<como espaço, Ñ comoNouNXX) e que os nomes longos podem aparecer truncados.
Uma MRZ correta com dados diferentes dos impressos é um sinal claro para rever o documento manualmente.
Aviso: um checksum não prova autenticidade
O algoritmo é público e simples. Existem geradores públicos que produzem MRZ com todos os dígitos de controlo corretos. Por isso:
- Uma MRZ válida só demonstra que os caracteres são coerentes entre si.
- Não demonstra que o documento existe, que não foi anulado nem que pertence a quem o apresenta.
- A autenticidade verifica-se por outras vias (por exemplo, os elementos de segurança físicos ou o chip do documento), que ficam fora do que uma fotografia mostra.
Como o Constaia o faz
Quando analisa um DNI, um NIE ou um passaporte, o Constaia lê a MRZ com OCR, corrige as confusões típicas e devolve duas verificações deterministas: mrz_checksums (todos os dígitos de controlo) e mrz_matches_visual (a MRZ coincide com os dados impressos). Se algo não bater certo, o veredicto passa a invalid ou review, com o motivo.
curl https://api.constaia.com/v1/analyze \
-H "Authorization: Bearer ck_test_..." \
-F file=@passport.jpg \
-F 'options={"expect":"passport","checks":{"not_expired":true},"storage":"none"}'Com uma chave ck_test_… e o ficheiro passport.jpg obtém uma resposta simulada sem gastar créditos. O validador de MRZ faz parte do pacote open source @constaia/validators. E repetimos: o Constaia não é KYC biométrico e um checksum correto não demonstra que um documento é autêntico. Mais informação em Checks.
Resumo
- TD1 = 3 × 30 (cartões de identificação), TD2 = 2 × 36, TD3 = 2 × 44 (passaportes).
- Dígito de controlo: valores (A = 10 … Z = 35,
<= 0), pesos 7-3-1, soma, módulo 10. - Valide cada campo e o composto, e compare a MRZ com a zona visual.
- Um checksum correto não prova autenticidade.
Quer testar uma MRZ concreta? Use o leitor de MRZ gratuito. Para o integrar no seu produto, crie uma conta gratuita com 250 créditos por mês.