Volver al blog

Dígitos Verificadores ICAO 9303: Cómo se Calculan (con Código)

El algoritmo 7-3-1 de los dígitos verificadores de la MRZ explicado paso a paso, con la aritmética completa sobre un pasaporte real e implementaciones en JavaScript y Python.

Extraer Datos de Pasaporte
ICAO 9303dígitos verificadorescheck digitMRZalgoritmo 7-3-1validación pasaporte

Un OCR que lee un pasaporte y te entrega los datos sin más te está pidiendo que confíes. Uno que valida los dígitos verificadores de la MRZ te entrega una prueba matemática de que la lectura es coherente.

Esa es la diferencia entre "creemos que el número es G12345678" y "el número es G12345678 y aquí está la aritmética que lo confirma". Este artículo explica el algoritmo completo, con los números a la vista.

Para qué sirve un dígito verificador

Un dígito verificador es un dígito extra calculado a partir de los demás. Si al recalcularlo no coincide con el impreso, algo se leyó mal.

Es exactamente el mismo principio del dígito verificador de una tarjeta bancaria o de un ISBN. Lo notable en el pasaporte es la densidad: la línea 2 lleva cuatro dígitos verificadores en 44 caracteres, cubriendo cada campo crítico y luego todos juntos otra vez.

Esto importa porque los errores clásicos de OCR son sustituciones de un solo carácter entre formas parecidas: 0/O, 1/I, 5/S, 8/B, 2/Z.

Vale la pena ser exacto sobre qué garantiza el esquema, porque suele exagerarse. Una sustitución pasa inadvertida solo si los valores de los dos caracteres difieren en un múltiplo exacto de 10 — y esa condición se puede verificar par por par. Los pares que confunde un OCR real no la cumplen:

ConfusiónValoresDiferencia¿Se detecta?
0 / O0 / 2424
1 / I1 / 1817
5 / S5 / 2823
8 / B8 / 113
2 / Z2 / 3533
G / 616 / 610No, en su propio campo

Es decir: todas las confusiones tipográficas habituales quedan cubiertas. El punto ciego son los pares que difieren en exactamente 10, 20 o 30 — A/0, G/6, K/A, U/K — y es un punto ciego genuino, no un caso que otro dígito rescate. Volvemos sobre él más abajo, porque conviene conocerlo.

El algoritmo, en tres reglas

Regla 1 — Cada carácter vale un número.

CarácterValor
0-9Su propio valor, 0 a 9
A-Z10 a 35 (A=10, B=11, … Z=35)
<0

Regla 2 — Los pesos se repiten 7, 3, 1.

Al primer carácter le corresponde 7; al segundo, 3; al tercero, 1; al cuarto, 7 otra vez, y así indefinidamente.

Regla 3 — El dígito es la suma módulo 10.

Multiplica cada valor por su peso, suma todo, y toma el residuo de dividir entre 10.

La aritmética, sobre un pasaporte real

Tomemos el número de pasaporte G12345678 de nuestra MRZ de ejemplo, cuyo dígito verificador impreso es 6:

CarácterG12345678
Valor1612345678
Peso731731731
Producto112322112542218

Suma: 112 + 3 + 2 + 21 + 12 + 5 + 42 + 21 + 8 = 226

226 mód 10 = 6 ✓ Coincide con el dígito impreso.

Ahora supongamos que el OCR leyó 5 donde había 8, en la última posición (peso 1). La suma baja 3, queda en 223, y el dígito calculado pasa a ser 3 en lugar de 6. El error salta de inmediato.

El punto ciego, explicado

Sustituyamos en cambio la G inicial por un 6. G vale 16, 6 vale 6: la suma cae 10 × 7 = 70, hasta 156, y 156 mód 10 sigue siendo 6. El dígito coincide y el error pasa.

Esto no es mala suerte, es aritmética: un cambio de valor Δ en un carácter con peso w altera la suma en Δ·w, y pasa inadvertido cuando Δ·w ≡ 0 (mod 10). Como 7, 3 y 1 son todos coprimos con 10, la condición se reduce a Δ ≡ 0 (mod 10), sin importar la posición ni el peso.

De ahí se sigue algo que conviene saber y que suele contarse mal: el dígito compuesto no rescata estos casos. Si una sustitución es invisible para el dígito de su campo, lo es también para el compuesto, porque la condición no depende del peso. Un A leído como 0 atraviesa los dos.

Lo que el compuesto sí aporta es real, pero es otra cosa: cubre los propios dígitos verificadores (posiciones 10, 20, 28 y 43), que ningún otro cálculo protege, y detecta desalineaciones donde un campo entero se corrió una posición.

Los otros tres dígitos

El mismo cálculo se aplica a:

  • Fecha de nacimiento (800705 → dígito 0): 56 + 0 + 0 + 49 + 0 + 5 = 110 → 0
  • Fecha de vencimiento (330705 → dígito 4): 21 + 9 + 0 + 49 + 0 + 5 = 84 → 4
  • Número personal (14 caracteres, aquí todos < → dígito 0): suma 0 → 0

El dígito compuesto

El cuarto es distinto y es el más útil. Se calcula sobre la concatenación de los campos ya verificados, incluyendo sus propios dígitos:

  • Posiciones 1-10 (número de pasaporte + su dígito)
  • Posiciones 14-20 (fecha de nacimiento + su dígito)
  • Posiciones 22-43 (vencimiento + su dígito + número personal + su dígito)

En nuestro ejemplo esa cadena es:

G1234567868007050 3307054<<<<<<<<<<<<<<0

(sin el espacio, que solo está aquí para que se lea)

Aplicando 7-3-1 sobre los 39 caracteres, la suma da 398, y 398 mód 10 = 8 — exactamente el último carácter de la línea 2. ✓

Lo que ningún dígito cubre

Vale la pena mirar el mapa por lo que no está: el compuesto abarca las posiciones 1-10, 14-20 y 22-43. Quedan fuera:

  • La nacionalidad (posiciones 11-13).
  • El sexo (posición 21).
  • Toda la línea 1 — tipo de documento, país emisor y, sobre todo, el nombre.

Dicho de otro modo: el número de pasaporte y las fechas llegan con prueba aritmética, pero el nombre del titular es el campo menos verificado de la MRZ. No tiene dígito verificador de ninguna clase.

Es la razón práctica por la que una extracción seria no se limita a la MRZ: contrasta el nombre de la línea 1 contra el impreso en la zona visual de la página. Cuando ambos coinciden, el nombre queda respaldado por una segunda lectura independiente; cuando no, hay algo que revisar.

Implementación

En JavaScript, el algoritmo entero:

function mrzCheckDigit(input) {
  const weights = [7, 3, 1]
  let total = 0

  for (let i = 0; i < input.length; i++) {
    const c = input[i]
    let value
    if (c === '<') value = 0
    else if (c >= '0' && c <= '9') value = c.charCodeAt(0) - 48
    else if (c >= 'A' && c <= 'Z') value = c.charCodeAt(0) - 55
    else throw new Error(`Carácter inválido en MRZ: ${c}`)

    total += value * weights[i % 3]
  }

  return total % 10
}

mrzCheckDigit('G12345678') // 6
mrzCheckDigit('800705')    // 0

En Python:

def mrz_check_digit(value: str) -> int:
    weights = (7, 3, 1)
    total = 0

    for i, c in enumerate(value):
        if c == '<':
            v = 0
        elif c.isdigit():
            v = int(c)
        elif 'A' <= c <= 'Z':
            v = ord(c) - 55
        else:
            raise ValueError(f'Carácter inválido en MRZ: {c}')

        total += v * weights[i % 3]

    return total % 10


mrz_check_digit('G12345678')  # 6

Y la validación de la línea 2 completa:

function validateLine2(line) {
  if (line.length !== 44) return { valid: false, error: 'longitud incorrecta' }

  const checks = [
    { name: 'número de pasaporte', data: line.slice(0, 9),  digit: line[9]  },
    { name: 'fecha de nacimiento', data: line.slice(13, 19), digit: line[19] },
    { name: 'vencimiento',         data: line.slice(21, 27), digit: line[27] },
    { name: 'número personal',     data: line.slice(28, 42), digit: line[42] },
    {
      name: 'compuesto',
      data: line.slice(0, 10) + line.slice(13, 20) + line.slice(21, 43),
      digit: line[43],
    },
  ]

  const failed = checks.filter(
    (c) => mrzCheckDigit(c.data) !== Number(c.digit)
  )

  return { valid: failed.length === 0, failed: failed.map((c) => c.name) }
}

Un detalle del estándar que suele omitirse

ICAO 9303 permite que el carácter < sustituya al 0 en la posición de un dígito verificador cuando el campo entero es relleno. En la práctica esto aparece sobre todo en el dígito del número personal de países que no usan ese campo.

Si tu validador exige un dígito estricto ahí, rechazarás pasaportes perfectamente válidos. Pero conviene no extender esa tolerancia al dígito compuesto de la posición 44: un < ahí casi siempre significa que la transcripción perdió el último carácter, no que el emisor eligió relleno.

Qué hacer cuando un dígito no cuadra

Que falle un dígito no significa que el pasaporte sea falso. En orden de frecuencia:

  1. Error de OCR — la causa habitual. La solución es releer, no rechazar.
  2. Transcripción manual con un carácter cambiado.
  3. Recorte de la imagen que dejó fuera parte de la línea.
  4. Documento realmente alterado — el caso raro, pero el que justifica la validación.

Un motor de extracción bien construido usa el fallo como señal de reintento: si el dígito no cuadra, vuelve a leer la zona con otra estrategia antes de responder. Nosotros hacemos exactamente eso, y cuando ni así se logra una lectura que verifique, preferimos devolver un error y reembolsar el token antes que entregar datos que no podemos respaldar.

Compruébalo tú mismo

Nuestro validador MRZ es gratuito, no pide cuenta y ejecuta este mismo algoritmo: pega una MRZ y verás los cuatro dígitos recalculados uno por uno, con el que falla señalado.

Si primero necesitas el mapa de posiciones para saber qué franja alimenta cada dígito, está en Cómo leer la MRZ línea por línea. Y si quieres saltarte todo esto y recibir los campos ya validados desde una foto, la API lo hace en una sola petición.

¿Necesitas extraer datos de pasaportes automáticamente?

Prueba nuestra API con 20 extracciones gratis. Integración en minutos, resultados en segundos.

Comenzar gratis