Saltar para o conteúdo
Constaia

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

Também em: Español, English, Français

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 N ou NXX.

Três formatos

FormatoLinhas × caracteresUso típicoParte do Doc 9303
TD13 × 30Cartões de identificação (tamanho cartão bancário), como o DNI espanholParte 5
TD22 × 36Alguns documentos oficiais de tamanho intermédioParte 6
TD32 × 44PassaportesParte 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:

  1. 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.
  2. Multiplica-se cada valor por um peso que se repete: 7, 3, 1, 7, 3, 1…
  3. Somam-se os produtos.
  4. O dígito de controlo é o resto da divisão por 10.
ABCDEFGHIJKLM
10111213141516171819202122
NOPQRSTUVWXYZ
23242526272829303132333435

O exemplo do apêndice A da Parte 3: a data de 27 de julho de 1952 (520727).

Carácter520727
Peso731731
Produto35604967

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<<<<<16

A 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çõesCampoValorControlo
1–9Número do documentoL898902C310: 6
11–13NacionalidadeUTO—
14–19Data de nascimento74081220: 2
21SexoF—
22–27Data de validade34041528: 9
29–42Número pessoal / dados opcionaisZE184226B<<<<<43: 1
44Controlo composto—6

Vamos verificar o número do documento, L898902C3:

CarácterL898902C3
Valor21898902123
Peso731731731
Produto1472495627014363

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<<<<<1

A 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

mrz.ts
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/8 e S/5 confundem-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: 74 pode 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, Ñ como N ou NXX) 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.

Fontes

  1. 01ICAO Doc 9303, 8.ª ed., Parte 3: especificações comuns a todos os MRTD
  2. 02ICAO Doc 9303, 8.ª ed., Parte 4: passaportes (TD3)
  3. 03ICAO Doc 9303, 8.ª ed., Parte 5: documentos de formato TD1
  4. 04Wikipedia — Machine-readable passport
  5. 05Exemplo público de gerador de MRZ (GitHub)