qredential

SD-JWT · Token Status List · base45

A resposta já está dentro do código

Uma credencial que prova a si mesma. Assinatura, validade, revogação e claims, tudo conferido sem uma única requisição sair do dispositivo.

TypeScript, zero dependências de runtime, só WebCrypto. Node, navegadores e React Native, porque quem verifica costuma ser um celular.

assinando uma credencial de demonstração…
 
 
requisições de rede· verificado em·

O que isto faz, na prática

Uma credencial é uma afirmação que alguém assinou. “Esta pessoa tem mais de 18 anos.” “Esta pessoa pode dirigir.” A assinatura é o que dá valor à frase: ela diz que uma autoridade responde por aquilo e que ninguém alterou nada depois.

No papel, e num PDF, uma assinatura cobre o documento inteiro. Para provar uma única linha, você entrega tudo. Mostre a carteira de motorista só para provar a idade e a outra pessoa fica sabendo também o seu endereço, o número do documento e a sua data de nascimento exata. Ela não pediu nada disso. E agora tem.

A divulgação seletiva desfaz essa troca. O emissor assina cada dado separadamente, e a credencial carrega apenas uma impressão digital de cada um. Você escolhe o que revelar, e o resto continua sendo impressão digital, que não diz nada sobre o conteúdo. A assinatura continua válida, porque nunca foi uma assinatura sobre um bloco indivisível.

Tudo isso cabe dentro do código que o verificador lê: a assinatura, as impressões digitais e os dados que você escolheu mostrar. Nada é buscado durante a verificação, então uma porta, um ônibus, um posto de saúde no interior ou um avião em pleno voo conferem a credencial sem rede nenhuma.

Falta uma peça, e é justamente a que costuma ser esquecida. Tudo o que pode ser lido pode ser fotografado. Por isso quem apresenta também precisa assinar um desafio novo com uma chave privada que nunca sai do aparelho dele. Sem esse passo, o print da credencial de outra pessoa passaria. Esta biblioteca recusa qualquer apresentação que não traga isso.

Feita para o momento em que não há sinal

Eu construí o eCNH, a habilitação digital brasileira, usada por mais de 40 milhões de pessoas. A parte que mais me ensinou não foi o aplicativo. Foi a beira da estrada: um agente lendo uma habilitação numa rodovia com uma barra de sinal, ou nenhuma, precisando de um sim ou não em menos de um segundo.

Tudo de que você precisa para essa resposta cabe no código. A assinatura prova o emissor, os claims estão ali mesmo, e a única coisa que vem de fora é a chave pública do emissor, que muda tão raramente que dá para distribuir junto e atualizar uma vez por semana.

Bibliotecas para isso existem. São SDKs corporativos: pesados, amarrados ao perfil de um país, e escritos como se você já trabalhasse no setor de identidade. Esta é a versão que um engenheiro de produto adiciona numa terça-feira.

Três partes, e nenhuma delas é um servidor

01 · EMITIR

Assinar cada claim uma vez

O emissor decide quais claims poderão ser omitidos depois, e assina um digest para cada um deles.

02 · APRESENTAR

Enviar só o que foi pedido

A carteira descarta os claims que vai guardar. A assinatura do emissor continua válida sobre o que restou.

03 · VERIFICAR

Responder sem perguntar a ninguém

Assinatura, validade, revogação e cada disclosure, conferidos contra uma chave fixada no dispositivo.

Prove que tem mais de 18 sem entregar sua data de nascimento

As leis de verificação de idade estão chegando mais rápido que as ferramentas. A implementação de sempre faz o cliente enviar foto do documento para um terceiro, o que é um desastre de privacidade e um vazamento à espera de acontecer.

A divulgação seletiva resolve isso direito. O verificador não consegue aprender a data de nascimento nem se quiser, porque aquele valor nunca saiu da carteira. A propriedade é criptográfica, não é promessa em política de privacidade.

// A credencial guarda nome, endereço, data de nascimento e número do documento.
// O bar recebe um booleano.
const presentation = await present(credential, { disclose: ['over_18'] })

const result = await verify(presentation, { trust })
result.claims             // { over_18: true }
result.claims.birth_date  // undefined, e nunca foi transmitido
result.withheld           // 4, e ele não consegue dizer quais quatro

Números reais, inclusive o incômodo

Uma habilitação realista, oito claims, validade de cinco anos, ponteiro de lista de status. Medido, não estimado:

CredencialCaracteresVersão do QR
tudo visível~74018, escaneia bem
os oito claims divulgáveis~159027, denso demais
presenting only over_18~111522, ainda denso
A divulgação seletiva praticamente dobra a credencial. Cada claim divulgável custa um salt mais um digest assinado, e os digests ficam no payload quer o portador revele o claim ou não. Isso é proposital, já que uma contagem de digest que encolhesse conforme o que você revela vazaria o que você escondeu. O efeito é que a economia na apresentação é de trinta por cento aqui, não os oitenta que a intuição promete. Torne dois ou três claims divulgáveis, não todos, e deixe o fits() te dizer onde você está antes de imprimir qualquer coisa.

Não acredite em mim

O playground roda a biblioteca inteira no seu navegador e dispara oito ataques reais contra ela. Cada um imprime o código de recusa que espera, então dá para conferir a biblioteca contra as próprias afirmações em vez de confiar num README.

Virar um claim no payload
bad_signature
Inventar um claim nunca emitido
digest_mismatch
Enviar uma disclosure duas vezes
digest_mismatch
Assinar com a chave errada
bad_signature
Alegar ser outra autoridade
unknown_issuer
Usar daqui a dez anos
expired
Usar depois de revogada
revoked
Se esconder atrás de lista velha
status_list_stale
Repetir uma apresentação gravada
holder_proof_invalid
Apresentar foto do código de outro
holder_proof_missing

Por que esta, e quando não

Cinco motivos para escolher, e depois a parte honesta.

  • Cerca de 1.300 linhas que dá para lerPequena o bastante para uma pessoa ler tudo numa tarde e entender o que faz. Segurança que você não consegue ler é segurança que você aceita por fé.
  • Zero dependênciasNada vindo do npm em tempo de execução. Não há cadeia de suprimentos para auditar além desta, e nada que possa mudar debaixo de você na semana que vem.
  • Recusa o que não consegue checar por inteiroSem aprovação parcial, sem aviso que dá para ignorar. Toda falha volta como um motivo nomeado, que você registra e trata, nunca como um falso seco.
  • Roda onde a verificação aconteceNode, Deno, Bun, navegadores, React Native e edge workers. O mesmo código em todos, sobre a criptografia padrão que já vem na plataforma.
  • Testada de três formasTestes unitários, os vetores oficiais publicados junto com a RFC 9901, e testes de propriedade que jogam entrada aleatória e malformada nela procurando um caso que escape.

E quando esta é a ferramenta errada.

  • Você precisa de carteira de motorista ISO 18013-5É outra codificação, CBOR e COSE, e outro padrão. Esta biblioteca fala SD-JWT, o formato que as carteiras europeias e as especificações OpenID usam. Problema parecido, trabalho diferente.
  • Você quer uma carteira inteiraEla verifica e apresenta. Não guarda credenciais, não gerencia dispositivos e não cuida de cadastro. É uma peça, feita para morar dentro do seu produto.
  • Você precisa de auditoria externa antes de um uso reguladoAinda não existe. Os testes e os vetores da RFC são públicos, e cada linha também, mas para uma implantação regulada vale reservar orçamento para a sua própria revisão.

Instalar

npm i qredential

Node 20 ou mais novo, todos os navegadores atuais, React Native. Cerca de 1.300 linhas de código sem dependência de runtime, exatamente para que ler antes de confiar seja realista.

Padrões, não invenções

  • SD-JWT, RFC 9901divulgação seletiva e key binding, aninhada e recursiva, o mecanismo que a carteira de identidade europeia usa
  • SD-JWT VCo formato da credencial
  • Token Status Listrevogação que funciona a partir de uma cópia em cache
  • base45, RFC 9285o envelope do QR, escolhido por compatibilidade de leitor

Não é mDL ISO 18013-5, que é CBOR e COSE em vez de JWT. Está no roteiro, e uma credencial que use algo não suportado é recusada em vez de entendida pela metade. Afirmar metade de um padrão de conformidade é pior que não afirmar nada.