EUDI Wallet: como validar credenciais SD-JWT VC em produção

EUDI Wallet: como validar credenciais SD-JWT VC em produção

>A carteira física vai morrer — não em 2026, mas para quem constrói software de identidade, autenticação e KYC, ela já morreu. A União Europeia está finalizando o European Digital Identity Wallet (EUDI Wallet), um sistema baseado em W3C Verifiable Credentials que vai unificar identidade digital em 27 países. E a maioria dos devs nem percebeu por onde esse assunto vai entrar na sua stack.

Segundo o Sapo.pt, o regulamento eIDAS 2.0 entrou em vigor em maio de 2024 e estabelece o final de 2026 como prazo para disponibilização das carteiras aos cidadãos. Mas o detalhe que mata para quem programa é outro: os prestadores de serviços digitais serão obrigados a aceitar a carteira se o cidadão quiser usá-la. E aí começa a parte interessante.

O que muda com o eIDAS 2.0 — e por que devs devem prestar atenção agora

O eIDAS original (2014) já regulava assinaturas eletrônicas e identidade digital na UE, mas era fragmentado: cada país tinha seu sistema nacional (Cartão de Cidadão em Portugal, SPID na Itália, FranceConnect na França). O eIDAS 2.0 impõe interoperabilidade técnica via especificações comuns publicadas no EUDI Wallet Architecture Reference Framework. Isso muda o jogo para qualquer dev que atende clientes europeus.

Na prática, três pilares técnicos vão definir o ecossistema:

  • W3C Verifiable Credentials (VC) — modelo de dados para credenciais verificáveis criptograficamente.
  • SD-JWT VC — variante de JWT com disclosure seletiva, que permite revelar só os atributos necessários (provar que tem mais de 18 anos sem mostrar a data de nascimento).
  • ISO 18013-5 (mDoc) — padrão mobile usado também como base técnica em vários países.

O fluxo segue o modelo Issuer → Holder → Verifier. Um governo emite uma credencial (“João tem 32 anos, NIS 123…”), o cidadão guarda no app da wallet, e quando precisa provar algo para um serviço (banco, loja, hospital) apresenta apenas os atributos necessários com prova criptográfica. É KYC sem fricção e sem upload de documento.

A stack técnica por trás da carteira europeia

Quando comecei a ler os documentos do EUDI Wallet com olho de dev, percebi que o consórcio escolheu tecnologias pragmáticas — não reinventou a roda. O padrão de emissão é o OpenID for Verifiable Credential Issuance (OpenID4VCI) e o de apresentação é o OpenID for Verifiable Presentations (OpenID4VP). Ambos são extensões do OpenID Connect que você já conhece — só que com semântica de credenciais em vez de tokens de sessão.

Por que SD-JWT e não blockchain?

Pergunta justa, e a resposta me surpreendeu pela honestidade. Blockchain resolveria revogação distribuída, mas custaria caro em privacidade (tudo público) e em latência. A escolha por SD-JWT com status lists é um trade-off consciente: PKI tradicional funciona, e a UE já tem infraestruturas de chaves públicas robustas nos países-membros.

O esquema de disclosure seletiva funciona com salt-based hashing: cada campo tem um hash com salt, e o holder revela o salt apenas dos campos que quer expor. O verifier confirma o hash sem ver os outros campos. É elegante, e a biblioteca de referência está em openwallet-foundation-labs.

Key binding: o detalhe que ninguém fala

Uma credencial SD-JWT sem key binding é só um JSON bonito. Para evitar replay attacks, a credencial precisa ser vinculada à chave privada do holder via proof of possession no momento da apresentação. Isso usa normalmente ES256 ou EdDSA. Se você está implementando um verifier e ignora isso, qualquer um que copiar o JWT consegue apresentá-lo. Cuidado com essa armadilha.

Na Prática: como um Verifier valida uma SD-JWT VC

Imagine que você está construindo um e-commerce que precisa verificar a idade do cliente para vender álcool. Em vez de pedir upload de documento, você pede a apresentação via EUDI Wallet. O fluxo cross-device funciona via dynamic QR code ou deep link com um request_uri seguindo OpenID4VP.

Quando a wallet apresenta a credencial, seu backend recebe algo assim (formato compactado do SD-JWT):

eyJhbGciOiJFUzI1NiIsInR5cCI6Im1kb2Mrc2QtanQifQ.eyJpc3MiOiJodHRwczovL2lzc3Vlci5leGFtcGxlLmNvbSIsInN1YiI6ImRpZDpleGFtcGxlOmFiY2RlMTIzIiwiaWF0IjoxNzE3MDk1MjAwLCJpc3N1ZWRfYXQiOjE3MTcwOTUyMDAsImV4cGlyZXNfYXQiOjE5MjUwOTUyMDAsInN0YXR1cyI6eyJzdGF0dXNfbGlzdCI6eyJpZCI6Imh0dHBzOi8vZXhhbXBsZS5jb20vc3RhdHVzIiwiaW5kZXgiOjEzNH19LCJzZCI6eyJnaXZlbl9uYW1lIjoiX3BkUWd0VGFsN2VEemZ0RkpCejg1Wnd0bkMwUTcyVHJDSkQiLCJmYW1pbHlfbmFtZSI6Il9wZFFndFRhbDdZRGpNd2tHRnlvV0tVcW5oN05wNHRnYU4ifX0.WqU3Oi48H1nG5X5X9lJ2Fg~WyJnZW9yZ2UiLCJEZW1vIiwxOF0~WyJnaXZlbl9uYW1lIiwiSm9obyJd

Esse token tem três partes concatenadas por ~: o JWT base, as disclosures (cada uma é um array [salt, claim_name, claim_value] codificado em base64url) e a key binding JWT. Para validar tudo, o esqueleto fica assim:

import json, base64, hashlib
from typing import List

def decode_disclosure(disclosure_b64: str):
    raw = base64.urlsafe_b64decode(disclosure_b64 + "==")
    salt, name, value = json.loads(raw)
    digest = hashlib.sha256(disclosure_b64.encode()).digest()
    return name, value, digest.hex()

def verify_age_credential(
    sd_jwt_compact: str,
    issuer_public_key: bytes,
    expected_min_age: int = 18
) -> dict:
    parts = sd_jwt_compact.split("~")
    jwt_part = parts[0]
    disclosures = [p for p in parts[1:-1] if p]
    kb_jwt = parts[-1] if parts[-1].startswith("eyJ") else None

    # 1. Verifica assinatura do JWT contra chave do issuer (via JWKS em prod)
    # payload = PyJWSClient.verify(jwt_part, issuer_public_key)
    payload = {"_sd_algoritm": [hashlib.sha256(disclosures[0].encode()).digest().hex()]}

    # 2. Calcula os hashes das disclosures e confere com o claim _sd_algoritm
    revealed = {}
    for disc in disclosures:
        name, value, digest_hex = decode_disclosure(disc)
        if digest_hex in payload["_sd_algoritm"]:
            revealed[name] = value

    # 3. Aplica a regra de negócio (apenas o necessário!)
    birth_year = int(revealed.get("birthdate", "0")[:4])
    if 2026 - birth_year < expected_min_age:
        raise ValueError("Idade insuficiente")

    # 4. Verifica key binding (ESSENCIAL, nunca pule isso)
    if kb_jwt and not verify_key_binding(kb_jwt, jwt_part, expected_min_age):
        raise ValueError("Falha no key binding")

    return revealed

def verify_key_binding(kb_jwt: str, parent_jwt: str, nonce: int) -> bool:
    # Pseudocódigo: verificar assinatura do kb_jwt contra pub key do holder
    # e checar nonce + aud + cnf.hash do parent_jwt
    return True  # substitua pela verificação real

# Uso:
# claims = verify_age_credential(token, issuer_pub_key)
# print(claims)  # {'given_name': 'João'}

Esse é o esqueleto de qualquer validador. Em produção, use a biblioteca oficial sd-jwt-python ou sua porta em TypeScript, sd-jwt-ts. Testei isso em ambiente de homologação e a lib oficial evita umas cinco classes de bugs sutis de padding base64 e ordenação de disclosures.

Erros comuns que devs vão cometer (e como evitar)

Depois de ler a spec e brincar com o wallet de referência da Comissão Europeia, anotei os tropeços mais prováveis:

  1. Tratar como OAuth 2.0 e parar por aí. OIDC entrega identidade do provedor para o app. EUDI entrega credenciais verificáveis que podem ser apresentadas offline e cross-organização. Modelos mentais diferentes — usar a mesma abstração leva a falhas de segurança.
  2. Esquecer o status list. Uma credencial revogada continua válida criptograficamente. Você precisa consultar o status_list no JWT ou a URL apontada. Sem isso, credenciais canceladas ainda passam — e isso é um vetor sério de fraude.
  3. Não validar a chain de confiança. O issuer não é a CA raiz. Existe uma hierarquia (Trust List da UE) que precisa ser baixada e validada via assinatura. Veja a spec de trust list.
  4. Assumir que disclosure seletiva é opcional. Se você pede a credencial inteira para validar idade, está vazando nome, NIS, foto, nacionalidade. Vai falhar compliance GDPR em poucas auditorias.
  5. Ignorar presentation definitions (PD). A spec DIF Presentation Exchange define exatamente quais campos você precisa. Pedir demais quebra UX; pedir de menos falha o fluxo inteiro.
  6. Não testar offline. A grande vantagem do modelo é apresentação presencial sem rede (NFC + BLE no padrão ISO 23220). Se sua lógica depende de internet, vai quebrar em filas de banco e portarias físicas.

Quando você vai precisar disso no seu código?

Não é teoria distante. Os casos reais vão chegar antes do que você pensa:

  • KYC em fintechs e cripto: o regulamento MiCA já cita wallets compatíveis com eIDAS como caminho preferencial de onboarding.
  • Saúde digital: prescrições e atestados europeus vão circular como VCs cross-border até 2027.
  • e-Signature qualificada: o QES (Qualified Electronic Signature) passa a poder ser emitido direto da wallet, eliminando token USB e cartão.
  • Verificação de diplomas e certificados profissionais: a European Digital Credentials for Education já usa o mesmo modelo e está ativa.

Se você atende clientes na UE e qualquer um desses fluxos toca seu sistema, 2026 é deadline de produção. Na minha experiência, integração que parece “para o ano que vem” vira incêndio em novembro — planeje agora.

FAQ — Perguntas que devs realmente fazem

O EUDI Wallet substitui autenticação por senha?

Não substitui, complementa. Você pode usar a wallet como provedor de identidade (login federado) via OpenID4VP, mas para sessões em apps você ainda vai precisar de sessão/token próprio. Pense nela como substituto do “Login com Google” e do upload de documentos ao mesmo tempo.

Funciona offline?

Sim, no fluxo presencial via NFC/BLE (ISO 23220-3 e 18013-5). Online, depende do verifier: se você aceita status list via cache local assinada, consegue validar sem rede por algumas horas.

Quanto custa integrar como relying party?

Se você já tem OIDC no stack, o esforço principal é implementar OpenID4VP no front (cross-device via DC API/QR) e adicionar validação de SD-JWT VC no back. Estimo de 2 a 6 semanas para um time sênior, dependendo do stack. A parte difícil é montar os testes — felizmente a suíte de testes de conformidade da UE existe e é bem completa.

Tem alternativa open source ao wallet de referência?

Sim. O consórcio OpenWallet Foundation (Linux Foundation) mantém implementações. O time do EUDI publicou tudo no GitHub oficial. Para issuers e verifiers, há SDKs em Python, Kotlin e Swift prontos para usar.

Como isso afeta devs fora da UE?

Indiretamente, é o maior case de identidade digital soberana do mundo. EUA, Reino Unido, Singapura e até o Brasil (gov.br com seus movimentos recentes de interoperabilidade) estão observando. Os padrões técnicos (W3C VC, SD-JWT, OpenID4VP) são globais — aprender agora te coloca à frente quando sua região decidir adotar.

Gostou? Me segue no GitHub e deixa um comentário se tiver dúvida ou quiser aprofundar algum ponto.

Y

Yuri Sousa

Front-End Developer / Designer

Desenvolvedor apaixonado por criar experiências digitais acessíveis e visualmente perfeitas. Escrevo sobre desenvolvimento web, design e tecnologia.