Digital Document Checker
Biblioteca Python modular para parsing, verificação e validação de documentos digitais brasileiros codificados em QRCode — o padrão SERPRO / VIO / Carteira Digital de Trânsito e a CIN (Carteira de Identidade Nacional).
Tipos de documento já com submódulo próprio:
| Tipo | Módulo | Templates | Emissor | Status |
|---|---|---|---|---|
| CNH | documents/cnh.py |
2, 4, 83 | SENATRAN | verificado |
| DNI | documents/dni.py |
8, 9, 73 | TSE | experimental¹ |
| RG Digital | documents/rg.py |
91, 92 | GovBr | experimental¹ |
| CIN | documents/cin.py |
— (JWT) | MJSP | experimental¹ |
A arquitetura permite adicionar outros tipos (CRLV, identidades funcionais, ...) como submódulos independentes — veja Adicionando um novo tipo de documento.
¹ Experimental: parsing implementado a partir da engenharia reversa do app oficial, ainda não validado contra amostras reais desses documentos.
⚠️ Leia o Disclaimer / Aviso Legal antes de usar. Projeto pessoal, sem qualquer garantia. O uso é por sua conta e risco.
Objetivo
O objetivo deste projeto é oferecer uma forma programática e aberta de:
- Extrair (parsing) todos os dados contidos no QRCode de um documento digital brasileiro — nome, CPF, categoria, validade, foto, etc.
- Verificar a autenticidade da assinatura digital contra as chaves públicas oficiais (SENATRAN/SERPRO), quando disponíveis.
- Validar o estado do documento (autêntico, vencido, sandbox, template conhecido) devolvendo um resultado estruturado e fácil de consumir.
O parsing sempre acontece — mesmo que a assinatura não confira ou o documento esteja vencido —, de modo que você sempre recebe os dados disponíveis acompanhados dos indicadores de estado.
Princípios de projeto
- O parsing sempre ocorre, independentemente de validade ou autenticidade. Um documento com assinatura inválida ou vencido ainda tem todos os seus campos extraídos.
- O resultado (
DocumentResult) traz todos os dados disponíveis mais campos de estado padronizados:is_parsed,is_authentic,is_signature_checked,is_expired,is_valid, além deerrorsewarnings. - Modular por tipo: cada documento tem seu submódulo em
digital_document_checker/documents/com umDocumentHandler.
Instalação
pip install -e . # núcleo (parsing + verificação de assinatura)
pip install -e ".[full]" # + leitura de QRCode em imagens e PDFs
Dependências opcionais:
| Recurso | Pacotes |
|---|---|
| Verificação de assinatura | cryptography (núcleo) |
| Ler QRCode de imagem | pyzbar, Pillow |
| Ler QRCode de PDF | PyMuPDF (ou pdf2image) |
| Converter a foto BPG para PNG | binário bpgdec (libbpg) |
pyzbarrequer a biblioteca nativa zbar (brew install zbarno macOS,apt-get install libzbar0no Linux).
Uso rápido
from digital_document_checker import DigitalDocumentChecker
checker = DigitalDocumentChecker()
result = checker.parse_pdf("cnh.pdf") # ou parse_image / parse_bytes / parse_base64
print(result.document_type) # "cnh"
print(result.is_authentic, result.is_expired) # True/False
print(result.fields["nome"], result.fields["cpf"])
if result.photo:
result.photo.save("foto.png", as_png=True) # BPG -> PNG (requer bpgdec)
Linha de comando:
python -m digital_document_checker cnh.pdf
python -m digital_document_checker cnh.pdf --json --save-photo foto.png
python -m digital_document_checker cin.jpg # CIN: detectada automaticamente
CIN — Carteira de Identidade Nacional
O QRCode impresso no verso da CIN não usa o envelope binário do padrão VIO:
seu conteúdo é um JWT assinado (ES512, curva P-521) emitido pelo MJSP. A
detecção é automática — o mesmo DigitalDocumentChecker reconhece o formato e
escolhe o caminho certo.
result = checker.parse_image("cin_verso.jpg") # ou parse_qr_text(<token JWT>)
result.document_type # "cin"
result.header_format # "jws"
result.is_authentic # assinatura ES512 conferida contra a chave do app
result.signature.algorithm # "ES512"
result.data.cpf # "12345678901"
result.data.cpf_formatado # "123.456.789-01"
result.data.data_nascimento # "07/08/1981" (claim 'dns')
result.data.data_validade # "02/02/2036" (claim 'dvd')
result.data.url # "https://cin.mj.gov.br/cidadao/<uuid>"
result.data.uuid # identificador usado pelo app na consulta online
result.data.outras_claims # demais claims do JWT
O QRCode carrega CPF, data de nascimento e data de validade. Nome, filiação e sexo só existem na leitura online, autenticada e com consentimento do titular — esta biblioteca não faz chamadas de rede. Veja Leitura online.
A
urldo QRCode não é um link para abrir no navegador (devolve 404): ela só transporta ouuid. Detalhes em A claimurlnão é um link.
As checagens reproduzem processQrCode do app: cpf presente, iss == "MJSP",
url presente e terminando em UUID, e assinatura válida. Cada checagem
reprovada vira um item em result.errors — os campos continuam sendo extraídos.
As chaves públicas ficam em data/cin_keys.json (ambientes PROD, HML e
TST do app; PROD é o padrão, como em CURRENT_ENV):
python -m digital_document_checker cin.jpg --cin-env '*' # tenta todos os ambientes
from digital_document_checker import CINKeyStore, DigitalDocumentChecker
checker = DigitalDocumentChecker(cin_keys=CINKeyStore.from_file("minhas_chaves.json"))
Uso com Docker
A imagem já traz o projeto instalado (clonado no build) com todas as
dependências de leitura de QRCode e PDF (pyzbar/zbar, Pillow, PyMuPDF)
e o bpgdec (libbpg) compilado, habilitando a conversão da foto BPG → PNG
dentro do container.
Build:
docker build -t digital-document-checker .
# opcional: fixar um branch ou tag do repositório
docker build --build-arg GIT_REF=main -t digital-document-checker .
Execução — monte o diretório com seus arquivos em /data:
# saída legível
docker run --rm -v "$PWD:/data" digital-document-checker /data/cnh.pdf
# saída JSON
docker run --rm -v "$PWD:/data" digital-document-checker /data/cnh.pdf --json
# salvar a foto embarcada, convertida para PNG (bpgdec incluso na imagem)
docker run --rm -v "$PWD:/data" digital-document-checker \
/data/cnh.pdf --save-photo /data/foto.png
# ajuda
docker run --rm digital-document-checker --help
A conversão da foto BPG → PNG (
--save-photo *.png) usa obpgdec, que já vem compilado na imagem Docker. Fora do Docker, essa conversão específica exige obpgdec(libbpg) noPATH; sem ele, a foto ainda é extraída em BPG e a extração dos dados e a verificação da assinatura não dependem disso.
O que é verificado
Padrão VIO/SERPRO (CNH, DNI, RG Digital):
- Envelope — timestamp de emissão + versão (cabeçalho hexadecimal de 10 bytes ou binário de 5 bytes).
- Formato — o parser correto é escolhido pela versão (CNH = versão 2).
- Template —
template_id→ tipo de documento, emissor e grupo de certificados (data/templates.json). - Assinatura digital —
SHA256withRSAouSHA256withECDSAcontra as chaves públicas dedata/certificates.json, considerando a janela de validade do certificado na data de emissão. - Expiração — a partir do campo de validade do próprio documento.
CIN:
- Formato — JWS compacto de três segmentos (
header.payload.signature). - Claims —
cpfpresente,iss == "MJSP",urlpresente e terminando em um UUID. - Assinatura digital —
ES512(P-521) sobreheader.payload, contra a chave pública JWK dedata/cin_keys.json. - Expiração — pela claim
dvd(a validade impressa no cartão) ou, na falta dela, peloexpdo JWT.
Estrutura
digital_document_checker/
├── checker.py # orquestrador (parse -> verifica -> valida)
├── models.py # DocumentResult, Certificate, Template, Photo, ...
├── registry.py # CertificateStore / TemplateStore / CINKeyStore
├── crypto.py # verificação RSA / ECDSA / JWS (ES256-512)
├── images.py, qr.py # foto embarcada e leitura de QRCode
├── codecs/ # bits, alfabetos 6/7 bits, basE91
├── formats/ # parsers por versão de envelope (2 = CNH, 3 = DNI, ...)
│ ├── envelope.py
│ ├── jws.py # JWS compacto (CIN)
│ ├── v2_cnh.py # CNH (verificado)
│ ├── v3_dni.py # DNI (experimental)
│ ├── multiblock.py # versões 4/5/6 (experimental)
│ └── v1_text.py # versão 1 legada (experimental)
├── documents/ # submódulos por tipo de documento
│ ├── cnh.py # CNH <- verificado
│ ├── dni.py # DNI (experimental)
│ ├── rg.py # RG Digital (experimental)
│ ├── cin.py # CIN (experimental)
│ └── generic.py
└── data/ # certificates.json, templates.json, cin_keys.json
Adicionando um novo tipo de documento
from digital_document_checker.documents import register_handler
from digital_document_checker.documents.base import DocumentHandler, DocumentData
class MeuDocHandler(DocumentHandler):
type_name = "meu_doc"
display_name = "Meu Documento"
template_ids = (123,)
def build(self, payload, template):
# payload.field_values -> valores brutos
return DocumentData(raw_fields=payload.field_values, fields={...})
register_handler(MeuDocHandler(), prepend=True)
Notas sobre o formato (engenharia reversa)
- O conteúdo do QRCode é binário transportado como texto Latin-1 (ISO-8859-1).
- A CNH (versão 2) empacota os campos em 6 bits por caractere (alfabeto ASCII 32–95), seguidos de 256 bytes de assinatura e da foto em BPG.
- O decodificador de bits desta biblioteca foi validado byte-a-byte contra o decodificador do aplicativo oficial de referência.
- O dado assinado nunca usa os bytes originais do cabeçalho: o app sempre o reconstrói como 8 dígitos hexadecimais de timestamp + 2 de versão, em minúsculas — inclusive quando o QRCode traz o cabeçalho binário de 5 bytes.
- Campos por versão: 1 basE91, 2 e 5 6 bits, 4 7 bits, 3 e 6 basE91. As versões 4, 5 e 6 também transportam a foto no bloco intermediário.
- As versões 1, 4, 5 e 6 (outros tipos de documento) têm parsing estrutural implementado, porém ainda não validados contra amostras reais — marcados como experimentais no código.
- Os arquivos
data/certificates.jsonedata/templates.jsonacompanham o app oficial (última sincronização: VIO 2.4.5). A origem remota éhttps://vio.serpro.gov.br/api/v2/{certificates,templates}.
CIN
- Origem: app
identidade-nacional1.19.0 (React Native / Expo SDK 50). A lógica fica no bundle Hermesassets/index.android.bundle, não no DEX. - O QRCode é um JWS compacto (
header.payload.signatureem base64url) — nada de envelope binário, timestamp outemplate_id. - Fluxo de
processQrCode: decodifica o JWT → exigecpf,iss == "MJSP"eurlstring → valida a assinatura → extrai o UUID do fim daurlcom/\/([0-9a-fA-F]{8}-...-[0-9a-fA-F]{12})$/. - A verificação da assinatura é delegada ao módulo nativo
IoReactNativeJwtModule.verify(@pagopa/io-react-native-jwt), que usa oECDSAVerifierdo Nimbus com BouncyCastle. Consequências práticas: a assinatura vem no formato JOSE (R || S, 132 bytes para P-521) e não em DER, e nenhuma claim padrão é validada — nemexp, nemnbf. - As chaves de
data/cin_keys.jsonsão as deENVIRONMENTS[*].publicKeyJWT; o app usaCURRENT_ENV = PROD.HMLeTSTcompartilham o mesmo par. Validado contra uma CIN real: a assinatura confere com a chavePROD. - Claims observadas em documento real:
iss,url,cpf,dns(nascimento) edvd(validade). Não háiat,expnemkid. - A claim
dvdnão é usada pelo app 1.19.0 — ele só renderiza CPF ednsna tela offline —, mas o rótulodateValid("DATA DE VALIDADE") já existe emcommonStrings.qrCodeScreen.validateOff. Aqui ela é lida e usada na expiração.
A claim url não é um link
Abrir a url do QRCode no navegador devolve HTTP 404 — e isso é o
comportamento esperado, não um erro de leitura. O app nunca navega até ela
nem faz requisição a esse endereço. Em processQrCode a url serve para
exatamente duas coisas:
- verificar
typeof payload.url === 'string'; - extrair o UUID do final com a regex acima.
Ou seja, ela é um carregador de identificador, não um destino. O host de
API do app é outro caminho: CURRENT_ENV.baseUrl em PROD é
https://cin.mj.gov.br/api/, e não /cidadao/.
Leitura online — fora do escopo desta biblioteca
Documentado apenas para explicar o que não está implementado aqui: esta biblioteca valida a assinatura localmente e não faz nenhuma chamada de rede.
Ambientes (ENVIRONMENTS):
| Ambiente | baseUrl |
SSO | clientId |
|---|---|---|---|
PROD |
https://cin.mj.gov.br/api/ |
sso.acesso.gov.br |
mobile.cin.mj.gov.br |
HML |
https://hmlcinapi.mj.gov.br/api/v1/ |
sso.staging.acesso.gov.br |
identidadenacionaldsv |
TST |
https://tstcin.mj.gov.br/api/ |
sso.staging.acesso.gov.br |
identidadenacionaldsv |
Endpoints (relativos ao baseUrl):
| Constante | Caminho |
|---|---|
POST_DATA |
dados |
GET_SIGNATURE_VALIDATION |
carteiras/jwt/validar |
GET_IDENTITY |
identidade |
GET_HISTORY |
historico |
PERMISSIONS |
permissao |
POST_DEVICE_ID |
dispositivoid |
POST_NOTIFICATION |
notification |
POST_NOTIFICATION_APPROVED |
notificacoes-aprovado |
| — | justificativas |
As duas chamadas do fluxo de leitura:
POST {baseUrl}dados
{cpfConsultado, idJustificativa, nomeConsultante, uuid}
POST {baseUrl}carteiras/jwt/validar
{jwt: "<token lido do QRCode>"}
Nada disso é anônimo. O app exige login gov.br via OAuth2 + PKCE
(response_type=code, code_challenge_method=S256, scopes openid, email,
profile, govbr_confiabilidades, redirect_uri=identidadenacional://logged),
e a consulta ainda pede justificativa e nome de quem consulta. Há também
uma etapa de consentimento — a tela "Aguardando a aprovação do acesso aos
dados", em que o titular aprova a consulta no próprio celular.
Por isso a leitura offline entrega apenas CPF, data de nascimento e validade: é tudo o que viaja no QRCode. O restante (nome, filiação, sexo, naturalidade, situação do documento) é servidor, autenticado e com consentimento do titular.
Leitura do QRCode em digitalizações
Duas armadilhas encontradas em documentos reais e tratadas em qr.py:
- A CIN traz códigos de barras 1D (CODE39) ao lado do QRCode. Aceitar
qualquer simbologia faz o leitor devolver o conteúdo errado — por isso a
decodificação é restrita a
ZBarSymbol.QRCODE. - Em PDFs digitalizados o QRCode ocupa poucos pixels e o zbar falha na
resolução original. A leitura escalona a imagem (1×, 2×, 3×, 4×) alternando
LANCZOS(suaviza o ruído do scanner) eNEAREST(preserva QRCodes nítidos de um pixel por módulo), mais uma variante com contraste automático. PDFs ainda são re-renderizados no dobro do DPI antes de desistir.
Disclaimer / Aviso Legal
Leia com atenção antes de utilizar.
Este é um projeto pessoal, desenvolvido de forma independente e sem vínculo com o SERPRO, o SENATRAN, o DENATRAN, o MJSP ou qualquer órgão governamental. Não se trata de uma ferramenta oficial de validação.
O software é fornecido "COMO ESTÁ" ("AS IS"), sem garantias de qualquer natureza, expressas ou implícitas, incluindo — mas não se limitando a — garantias de comercialização, adequação a uma finalidade específica, exatidão, integridade ou não violação.
- O projeto pode conter falhas e bugs. O resultado da verificação pode gerar falsos-negativos e/ou falsos-positivos (por exemplo, apontar como autêntico um documento fraudado, ou como inválido um documento legítimo).
- Os autores e colaboradores NÃO se responsabilizam por qualquer dano, prejuízo, perda financeira, fraude, decisão equivocada ou consequência de qualquer natureza decorrente, direta ou indiretamente, do uso — ou da impossibilidade de uso — desta biblioteca ou de seus resultados.
- O uso é por sua conta e risco. Não utilize este software como única fonte para decisões que envolvam dinheiro, segurança, identidade, obrigações legais ou qualquer situação crítica. Para validação com valor legal, utilize sempre os canais e aplicativos oficiais.
- Os metadados em
data/certificates.json,data/templates.jsonedata/cin_keys.jsonsão os dados públicos distribuídos com os aplicativos oficiais de validação e podem estar desatualizados. Mantê-los atualizados é responsabilidade do usuário.
Ao utilizar este projeto, você declara estar ciente e de acordo com todos os termos deste aviso. Ferramenta destinada a fins de estudo, pesquisa e verificação técnica.
Testes
pip install -e ".[dev]"
pytest
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
File details
Details for the file digital_document_checker-0.1.3.tar.gz.
File metadata
- Download URL: digital_document_checker-0.1.3.tar.gz
- Upload date:
- Size: 85.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e5c220173f3db128839421fea25f33141105b2a03fb066baef0e5ec8c3dfcb12
|
|
| MD5 |
712495677b54007047038b49a3628da0
|
|
| BLAKE2b-256 |
160905721b2f093732ef0f5bfd21f0b39a1d0c017ec0dd0fcec2a279ba06e8e9
|