← Voltar ao blog

Validar CNH em JavaScript: algoritmo DENATRAN passo a passo

·12 min de leitura

O número de registro da CNH é tratado por muitos sistemas como texto livre. Isso deixa passar documentos inválidos, fantasmas ou digitados errado. A validação correta exige reproduzir o algoritmo que o DENATRAN (hoje SENATRAN) usa para gerar os dois dígitos verificadores. Este artigo mostra o algoritmo passo a passo e uma implementação TypeScript pronta para produção.

O número de registro da CNH tem estrutura definida por norma DENATRAN

O DENATRAN estabeleceu o formato do número de registro na Portaria 1.606/1998, depois consolidado em resoluções do CONTRAN. A estrutura é fixa: 11 dígitos numéricos, sem letras, sem pontuação obrigatória no campo de banco de dados.

O que é o RENACH e por que ele importa para validação

O RENACH (Registro Nacional de Carteiras de Habilitação) unifica os dados de habilitação de todos os estados. Cada registro recebe um identificador composto pelo código de UF (dois caracteres) seguido de 9 dígitos numéricos. O número que aparece no campo "Número de Registro" da CNH física corresponde apenas à parte numérica do RENACH, sem o prefixo de UF. Para validar o segundo dígito verificador com precisão, o estado emissor precisa ser conhecido. Quando o sistema armazena apenas os 11 dígitos, é possível validar o primeiro verificador com certeza e o segundo condicionalmente.

Composição dos 11 dígitos: prefixo de UF, sequencial e dois dígitos verificadores

Posições:  0  1  2  3  4  5  6  7  8   9   10
Conteúdo: [d0 d1 d2 d3 d4 d5 d6 d7 d8  D1  D2]
           \_________base 9 dígitos_______/ DV1 DV2

Posições 0–8 formam a base numérica atribuída pelo DETRAN estadual. D1 é o primeiro dígito verificador e D2 o segundo. O índice do estado emissor (DSC — Dígito de Segurança de Controle) varia de 1 (AC) a 27 (TO) e participa do cálculo de D2.

Diferença entre número de registro, espelho e código de segurança

O número de registro (11 dígitos) é o identificador primário. O número de espelho identifica a emissão física da CNH — pode mudar quando o documento é reimpresso sem alteração do RENACH. O código de segurança é um token de consulta no portal SENATRAN, sem algoritmo público de verificação. Para validação de formulários e APIs, só o número de registro interessa.

O algoritmo oficial usa mod-11 em duas passagens sequenciais

O mod-11 é o mesmo padrão do CPF e do CNPJ: soma ponderada dos dígitos, divisão por 11, e o resto determina o verificador. A CNH aplica duas passagens sobre os mesmos 9 dígitos base, com pesos diferentes.

Passagem 1: pesos 2 a 10 sobre os 9 primeiros dígitos

PosiçãoPesoProduto
02d0 × 2
13d1 × 3
24d2 × 4
35d3 × 5
46d4 × 6
57d5 × 7
68d6 × 8
79d7 × 9
810d8 × 10

Soma todos os produtos. Calcula rem = soma % 11.

Regras de resto: quando o DSC (Dígito de Segurança de Controle) é zero

Se rem < 2 (resto 0 ou 1), D1 = 0. Caso contrário, D1 = 11 - rem. O resultado sempre cabe em um dígito — não há caso especial de "10" nessa passagem.

Passagem 2: pesos 2 a 11 incluindo o primeiro dígito verificador

A segunda passagem usa os 9 dígitos base mais D1, com pesos 2 a 11: posições 0–8 recebem pesos 2–10 (idênticos à passagem 1), e D1 recebe peso 11. O DSC do estado emissor é somado ao total antes do mod-11.

Como o estado emissor afeta o segundo cálculo (DSC ≥ 10)

O índice de estado vai de 1 a 27. Quando (total + DSC) % 11 produz 11 - rem = 10, o resultado é ajustado para 0, evitando um verificador de dois dígitos. Estados com DSC ≥ 10 (a partir de MA=10) têm maior probabilidade de acionar esse ajuste.

Implementação do primeiro dígito verificador em TypeScript

Função calcPrimeiroDV(digits: string): number

function calcPrimeiroDV(digits: string): number {
  let sum = 0;
  for (let i = 0; i < 9; i++) {
    sum += Number(digits[i]) * (2 + i); // pesos 2, 3, ..., 10
  }
  const rem = sum % 11;
  return rem < 2 ? 0 : 11 - rem;
}

A função recebe uma string de exatamente 9 caracteres numéricos. Validação de entrada fica na função principal.

Tratamento do caso em que o resto é 0 ou 1

rem < 2 cobre os dois casos de uma vez. Diferente do CPF, a CNH não distingue entre resto 0 e resto 1 — ambos colapsam para zero. A expressão ternária é suficiente.

Testes unitários com assert puro, sem framework

import assert from "node:assert/strict";

assert.strictEqual(calcPrimeiroDV("000000000"), 0); // rem=0 → D1=0
assert.strictEqual(calcPrimeiroDV("111111111"), 11 - (99 % 11)); // rem calculado
console.log("calcPrimeiroDV: OK");

Implementação do segundo dígito verificador em TypeScript

Função calcSegundoDV(digits: string, primeiroDV: number, dsc: number): number

function calcSegundoDV(
  digits: string,
  primeiroDV: number,
  dsc: number
): number {
  let sum = 0;
  for (let i = 0; i < 9; i++) {
    sum += Number(digits[i]) * (2 + i); // pesos 2..10
  }
  sum += primeiroDV * 11; // peso 11 para D1

  const rem = (sum + dsc) % 11;
  if (rem < 2) return 0;
  const result = 11 - rem;
  return result >= 10 ? 0 : result;
}

Regra especial quando DSC atinge 10 ou 11

rem pode valer 0 a 10 (resultado do mod-11). Quando rem = 1, 11 - rem = 10 — um valor inválido como dígito único. O guard result >= 10 ? 0 : result garante que nunca se retorna dois dígitos. Estados com DSC alto deslocam o módulo para essa faixa com mais frequência.

Como o índice do estado (posição 1 do RENACH) entra no cálculo

const UF_DSC: Record<string, number> = {
  AC:  1, AL:  2, AM:  3, AP:  4, BA:  5,
  CE:  6, DF:  7, ES:  8, GO:  9, MA: 10,
  MG: 11, MS: 12, MT: 13, PA: 14, PB: 15,
  PE: 16, PI: 17, PR: 18, RJ: 19, RN: 20,
  RO: 21, RR: 22, RS: 23, SC: 24, SE: 25,
  SP: 26, TO: 27,
};

Quando o UF não está disponível, passe dsc = 0 e valide apenas D1. A maioria dos formulários coleta o estado, então o UF geralmente está disponível como contexto.

Função principal validarCNH com todos os guards de entrada

Rejeitar strings não numéricas e comprimento diferente de 11

function stripMask(input: string): string {
  return input.replace(/\D/g, "");
}

function validarCNH(
  input: string,
  uf?: string
): { valido: boolean; motivo?: string } {
  const cnh = stripMask(input);

  if (cnh.length !== 11) {
    return { valido: false, motivo: "Comprimento inválido" };
  }

  const TRIVIAIS = Array.from({ length: 10 }, (_, i) => String(i).repeat(11));
  if (TRIVIAIS.includes(cnh)) {
    return { valido: false, motivo: "Sequência trivial" };
  }

  const base = cnh.slice(0, 9);
  const d1Informado = Number(cnh[9]);
  const d2Informado = Number(cnh[10]);

  const d1Calculado = calcPrimeiroDV(base);
  if (d1Informado !== d1Calculado) {
    return { valido: false, motivo: "Primeiro dígito verificador inválido" };
  }

  if (uf) {
    const dsc = UF_DSC[uf.toUpperCase()] ?? 0;
    const d2Calculado = calcSegundoDV(base, d1Calculado, dsc);
    if (d2Informado !== d2Calculado) {
      return { valido: false, motivo: "Segundo dígito verificador inválido" };
    }
  }

  return { valido: true };
}

Rejeitar sequências triviais (00000000000 a 99999999999 repetidos)

O array TRIVIAIS cobre todas as CNHs formadas por um único dígito repetido. Esse guard é crítico: 00000000000 passa pelo algoritmo mod-11 (soma zero, rem zero, D1 zero, D2 zero), mas não representa um registro real.

Retornar objeto estruturado { valido: boolean; motivo?: string }

O campo motivo aparece apenas quando valido: false. O chamador diferencia "D1 errado" de "comprimento incorreto" sem depender de exceções ou comparar strings de erro.

Casos de borda que quebram implementações ingênuas

CNHs com prefixo de estado 0 (emissões antigas)

Emissões anteriores à padronização do RENACH usavam 0 como primeiro dígito da parte numérica. O cálculo de D1 é idêntico — a fórmula não muda. A confusão aparece ao tentar inferir o estado a partir do número sem o prefixo de UF. A solução é sempre armazenar o UF junto ao número de registro.

Segundo verificador resultando em 10 antes do ajuste

Se 11 - rem = 10, a função retorna 0. Implementações que retornam 10 diretamente falharão ao comparar com o dígito armazenado na CNH real. O guard result >= 10 ? 0 : result previne isso de forma explícita.

Entrada com máscara (pontos, hífens, espaços)

stripMask remove qualquer caractere não numérico antes de processar. Isso aceita "012.345.678-90" ou "012 345 678 90" sem erro. A verificação de comprimento ocorre após o strip.

DICA: Aplique stripMask antes de qualquer verificação de formato. Sistemas legados frequentemente persistem CNHs formatadas, e isso quebra validações que assumem entrada limpa.

Suite de testes com CNHs reais e inválidas conhecidas

Tabela de vetores de teste (número, esperado, motivo)

CNHUFEsperadoMotivo
00000000000SPinválidaSequência trivial
1234567890SPinválidaComprimento 10, não 11
1234567890XRJinválidaCaractere não numérico
12345678901SPinválidaD1 provavelmente incorreto
(gerado em /gerador-cnh)SPválidaGerado pelo algoritmo

Para vetores com resultado "válida" confirmado, use o gerador de CNH. Os números são fictícios: não correspondem a nenhum habilitado real e são seguros para fixtures e seeds de banco.

Como gerar CNHs válidas para testes sem expor dados reais

O gerador de CNH do FakeForge aplica o mesmo algoritmo DENATRAN descrito neste artigo. Use os números gerados em dados de teste, mocks de API e seeds de desenvolvimento. Eles não passam em consultas ao portal SENATRAN — são inválidos no mundo real, válidos em formato.

Executar os testes com node --test nativo (Node 20+)

// cnh.test.ts
import { test } from "node:test";
import assert from "node:assert/strict";
import { validarCNH } from "./cnh.js";

test("rejeita sequência trivial", () => {
  assert.deepEqual(validarCNH("00000000000"), {
    valido: false,
    motivo: "Sequência trivial",
  });
});

test("rejeita comprimento errado", () => {
  assert.deepEqual(validarCNH("1234567890"), {
    valido: false,
    motivo: "Comprimento inválido",
  });
});
# TypeScript direto com tsx (Node 20+)
node --import tsx --test cnh.test.ts

O runner nativo do Node 20 não exige Jest, Vitest ou Mocha.

Integrar a validação em formulários React e em APIs Next.js

Hook useCNH com debounce para campo controlado

import { useState, useEffect } from "react";
import { validarCNH } from "@/lib/validators/cnh";

export function useCNH(uf?: string) {
  const [valor, setValor] = useState("");
  const [resultado, setResultado] = useState<{
    valido: boolean;
    motivo?: string;
  } | null>(null);

  useEffect(() => {
    const timer = setTimeout(() => {
      if (stripMask(valor).length >= 11) {
        setResultado(validarCNH(valor, uf));
      } else {
        setResultado(null);
      }
    }, 400);
    return () => clearTimeout(timer);
  }, [valor, uf]);

  return { valor, setValor, resultado };
}

O debounce de 400ms evita revalidar a cada tecla. O resultado zera enquanto o campo tem menos de 11 dígitos.

Validação no route handler /api/validate/cnh com retorno padronizado

// src/app/api/validate/cnh/route.ts
import { NextRequest, NextResponse } from "next/server";
import { validarCNH } from "@/lib/validators/cnh";

export function GET(req: NextRequest) {
  const numero = req.nextUrl.searchParams.get("numero") ?? "";
  const uf = req.nextUrl.searchParams.get("uf") ?? undefined;

  const resultado = validarCNH(numero, uf);
  return NextResponse.json(resultado, { status: resultado.valido ? 200 : 422 });
}

Status 422 (Unprocessable Entity) sinaliza que a entrada chegou íntegra mas falhou na validação. Siga o mesmo padrão do validador de CPF e do validador de CNPJ para consistência de contrato.

Reutilizar a mesma lógica no cliente e no servidor sem duplicação

validarCNH em @/lib/validators/cnh.ts é uma função pura sem dependências de browser ou Node.js. Importar no hook e no route handler é suficiente — não há necessidade de serializar via fetch para validação síncrona no cliente.

Considerações LGPD para sistemas que coletam número de CNH

CNH é dado pessoal sob LGPD Art. 5º, I — exige base legal (Art. 7º, IX)

O número de registro da CNH identifica uma pessoa física de forma direta (LGPD Art. 5º, I). Coletar e armazenar esse dado exige base legal explícita. A mais comum para marketplaces e sistemas B2B é execução de contrato ou legítimo interesse (LGPD Art. 7º, IX), com balancing test documentado.

AVISO: Armazenar o número de CNH "por precaução" sem finalidade específica viola o princípio da necessidade (LGPD Art. 6º, III). Se o sistema só precisa confirmar que o usuário tem habilitação, armazene um booleano ou a data de validade, não o número completo.

Princípio da minimização: validar no frontend, nunca armazenar se não for necessário

A validação de formato não exige persistência. Valide no frontend com useCNH, confirme no backend com o route handler, e persista o número apenas se ele for necessário para a finalidade declarada ao titular.

Diferença entre validar formato e cruzar com base DENATRAN/RENACH real

Este algoritmo confirma que os dígitos verificadores são matematicamente corretos. Não confirma que a CNH existe no cadastro SENATRAN, que a habilitação está ativa, nem que os dados pertencem ao usuário logado. Para confirmação real, é necessário acesso à API do SENATRAN (restrita a conveniados). O algoritmo aqui descarta entradas obviamente inválidas sem custo de I/O.

Resumo

  • O número de registro da CNH tem 11 dígitos: 9 de base e 2 verificadores calculados com mod-11 em duas passagens sequenciais.
  • Primeiro verificador usa pesos 2–10 sobre os 9 dígitos base; qualquer resto menor que 2 produz zero.
  • Segundo verificador usa pesos 2–11 (incluindo D1) mais o índice DSC do estado emissor; resultado ≥ 10 vira zero.
  • validarCNH(input, uf?) retorna { valido: boolean; motivo?: string } — o mesmo contrato serve no hook React e no route handler Next.js, sem duplicação.
  • Rejeite sequências triviais explicitamente: 00000000000 passa pelo mod-11 por construção e precisa de guard dedicado.
  • Para gerar CNHs fictícias válidas em fixtures e seeds de banco, use o gerador de CNH. Para padrões análogos em outros documentos, veja CPF, CNPJ e PIS/PASEP.

Perguntas frequentes

Se não tenho o UF disponível no formulário, consigo validar a CNH parcialmente?+

Sim. Passe `uf: undefined` e valide apenas D1. Funciona, mas é menos seguro — D2 fica inconcluso. Se o formulário coleta endereço ou estado, capture-o antes de validar. Sem UF, você rejeita a maioria das entradas inválidas, mas passa em alguns casos que SENATRAN recusaria.

Gerei uma CNH no seu site, passei pra meu código e falhou. Como debugo?+

Confirme que `stripMask` removeu máscara. Verifique que pesos em `calcPrimeiroDV` são 2–10 e em `calcSegundoDV` são 2–11 mais peso 11 para D1. Reproduza manualmente: base (9 dígitos) + D1 + D2, aplique mod-11, compare com o informado. Diferença apontará erro no algoritmo.

Validar 1 milhão de CNHs é custoso? Preciso cache?+

Não. Mod-11 é O(1) — 9 multiplicações por validação. 1 milhão em ~50ms em Node.js puro. Sem cache necessário. Se precisar mais velocidade, paralelizar com Worker Threads. CPU-bound, não I/O-bound.

Existe uma API pública e gratuita de SENATRAN para confirmar se a CNH é real?+

Não. SENATRAN restringe acesso a conveniados (bancos, seguradoras, licenciados). Este algoritmo valida apenas formato matemático, não existência. Para produção com usuários reais, negocie convênio direto com SENATRAN ou delegue a parceiro credenciado.

Por que o estado não está embutido nos 11 dígitos se ele é necessário para calcular D2?+

O RENACH completo é UF+11 dígitos (ex: SP000000001). A CNH impressa mostra só os 11 numéricos — norma DENATRAN separou propositalmente. O estado é contexto da emissão, não identificador impresso. Por isso deve vir de outro campo do formulário ou contexto.