Voltar ao blog

26 de junho de 2026 · Por Montte · 8 min de leitura

SignatureKit × Licitei: assinar com A1 no navegador, sem cobrar por assinatura

Como a Montte e a Licitei levaram a assinatura A1 para o navegador: custo marginal zero, fluxo intacto e a chave privada nunca sai do dispositivo.

A Licitei é uma das maiores plataformas de licitação do Brasil. Para disputar, o usuário precisa assinar: cada proposta e cada declaração de habilitação exige uma assinatura digital de verdade, com o certificado A1 do licitante.

Parece um problema resolvido — é só plugar uma API de assinatura e seguir. Só que a conta não fecha.

O custo de uma assinatura

Os serviços de assinatura pagos cobram por documento, por envelope ou por assinatura. Para um documento, tudo bem. Mas uma licitação nunca é um documento só: o licitante assina dezenas de documentos de uma vez, toda vez que entra numa disputa. E o modelo de preço é, por construção, indexado a essa contagem.

Não é especulação, é o que as tabelas dizem. A Clicksign tem uma base mensal a partir de R$39/mês e cobra cada documento que estoura a franquia — de R$2,40 a R$6,90 por documento, sendo que o plano com mais recursos chega a custar mais caro por documento na franquia de entrada (R$3,00 no Start contra R$6,90 no Automação, a 20 documentos) (Clicksign, preços). A ZapSign cobra um adicional explícito de R$0,50 por assinatura com certificado digital, que se soma ao limite de documentos do plano (ZapSign, tabela de funcionalidades).

Multiplique qualquer um desses por dezenas de documentos, por milhares de licitantes, por cada disputa que eles entram, e aquele item de custo deixa de ser o custo de uma funcionalidade — vira o custo do produto.

A saída óbvia — "deixa o usuário assinar por fora e subir o resultado" — é pior. Ela quebra o fluxo: o licitante sai da Licitei no meio da disputa, briga com outra ferramenta, volta e sobe tudo de novo. E não há folga para isso. A Lei 14.133/2021 determina que as licitações sejam realizadas preferencialmente sob a forma eletrônica (art. 17, §2º), com fases ordenadas de apresentação de propostas e de habilitação (TCE-SP, art. 17). No pregão eletrônico federal, propostas e documentos de habilitação são enviados juntos e exclusivamente pelo sistema, só podem ser substituídos até a abertura da sessão pública, e a disputa corre em tempos automáticos fixos — dez minutos no modo aberto (Portal de Compras, pregão eletrônico). Num processo que fecha num minuto exato, esse atrito faz perder licitação.

O que é, de fato, assinar com A1

O A1 não é uma assinatura genérica de PDF. É um certificado ICP-Brasil, e isso tem peso jurídico definido: documentos assinados com certificado ICP-Brasil têm a mesma validade jurídica que documentos em papel com assinatura manuscrita, nos termos do art. 10 da MP 2.200-2/2001 (ITI, certificação digital). É o que a Lei 14.063/2020 chama de assinatura eletrônica qualificada — o nível mais alto entre simples, avançada e qualificada, justamente o que usa certificado ICP-Brasil (§1º do art. 10 da MP 2.200-2/2001) (Governo Digital, assinatura eletrônica).

O que distingue o A1 dos outros é onde a chave mora. O A1 é um certificado em software, um arquivo guardado no próprio computador ou celular do titular, com validade de um ano. O A3 vive em mídia de hardware — cartão, token ou HSM remoto — com validade de um a cinco anos (ITI, certificação digital). O A1 ser um arquivo é exatamente o que torna possível assiná-lo no navegador, sem token físico, e por isso ele é o formato que o licitante leva para uma disputa eletrônica.

Por que "assinatura com A1" ainda custa tanto

Aqui está a parte fácil de não enxergar. A maioria dos serviços que aceitam certificado A1 ainda cobra caro por isso, e o motivo é arquitetural: eles assinam no servidor. O usuário entrega o .pfx (ou um cofre de chaves gerenciado o guarda) junto com o documento; o serviço assina no servidor e devolve o resultado.

O preço por assinatura não está pagando a criptografia. A operação criptográfica em si é barata: toda assinatura de chave pública é calculada sobre um hash de tamanho fixo do documento, não sobre o documento inteiro — é o que a torna rápida (MDN, SubtleCrypto.sign()). O que custa é o servidor e, principalmente, a guarda da chave que ele implica.

E essa guarda é cara de verdade. Assinar no servidor com custódia gerenciada significa pagar por operação: no AWS KMS, assinatura assimétrica custa US$0,15 a cada 10.000 requisições, está explicitamente fora da franquia gratuita, e cada chave ainda custa US$1/mês (AWS KMS, preços). Querer custódia em hardware piora: um custom key store com CloudHSM cobra US$1,60 por HSM por hora, e a AWS recomenda no mínimo dois para disponibilidade — cerca de US$2.380/mês só de cluster, antes de qualquer assinatura (AWS KMS, preços).

Custódia também é responsabilidade, não só custo. A recomendação do NIST é direta: a chave privada deve permanecer sob controle exclusivo de seu titular e ser protegida contra divulgação não autorizada (NIST SP 800-57 Part 1 Rev. 5). E o que está em jogo se isso falha é tudo: o próprio NIST alerta que a divulgação de uma chave privada de assinatura torna suspeitas a integridade e a irretratabilidade de todos os dados já assinados por aquela chave (NIST SP 800-57 Part 1 Rev. 5). Concentrar milhares de .pfx de licitantes num servidor é concentrar exatamente esse risco.

Então a Montte e a Licitei fizeram a pergunta óbvia: e se o servidor não estiver no caminho?

Assine no navegador

O @signature-kit roda o fluxo de assinatura inteiro no navegador. Os bytes do PDF, o container A1 .p12/.pfx e a senha ficam na memória local da página e nunca fazem uma requisição de rede. O PAdES é calculado no cliente; a aplicação recebe de volta os bytes assinados, prontos para baixar ou guardar.

Isso é possível porque o navegador já traz a primitiva certa. A Web Crypto API expõe operações criptográficas direto no cliente — inclusive em Web Workers — e o SubtleCrypto.sign() gera a assinatura digital com a chave privada usando RSASSA-PKCS1-v1_5, RSA-PSS, ECDSA ou Ed25519 (MDN, Web Crypto API). É exatamente a operação necessária para assinar o hash de um PDF sem servidor.

npm install @signature-kit/pdf @signature-kit/a1

Nada sai do dispositivo

O documento, o .pfx e a senha vivem só na aba do navegador. A senha é embrulhada como Redacted antes de chegar ao a1SignaturesLayer, então nunca aparece num log ou numa mensagem de erro. Nenhuma requisição é feita — a chave privada não viaja.

O fluxo

readBrowserFileBytes transforma um File/Blob em Uint8Array, tudo em memória. Falhas são tipadas (react.FILE_READ_FAILED), nunca uma exceção solta.

read.ts
import { readBrowserFileBytes } from "@signature-kit/pdf/browser"

const bytes = await Effect.runPromise(readBrowserFileBytes(file))

createBrowserPdfSignatureBuilderState carrega as páginas do PDF com o pdf-lib e produz um template validado, com o campo de assinatura posicionado — de forma explícita ou automática. O template dele é o que o assinador consome.

build.ts
import { createBrowserPdfSignatureBuilderState } from "@signature-kit/pdf/browser"

const state = await Effect.runPromise(
  createBrowserPdfSignatureBuilderState({
    id: "proposta",
    name: file.name,
    documentId: "proposta",
    documentName: file.name,
    pdf: bytes,
    role: { id: "signer-1", label: "Licitante", required: true },
    draft: { id: "sig", type: "signature", roleId: "signer-1", width: 180, height: 48 },
    placement: { pageIndex: 0, x: 360, y: 96, anchor: "center" },
  }),
)

signBrowserPdfBatch assina o conjunto inteiro sob um a1SignaturesLayer — o certificado é destravado uma vez e reusado em cada documento. Ele assina estritamente um por um, e um documento que falha vira { ok: false, error } em vez de abortar a execução, então você sempre recebe um resultado por entrada, na ordem. O onItemSettled dispara a cada documento concluído, então a UI mostra o progresso e devolve cada arquivo assinado direto para a disputa.

sign-batch.ts
import { signBrowserPdfBatch } from "@signature-kit/pdf/browser"
import { a1SignaturesLayer } from "@signature-kit/a1/signer"
import { Effect, Redacted } from "effect"

// `items` = um { id, input } por documento enviado, montado com o passo acima.
const results = await Effect.runPromise(
  signBrowserPdfBatch(items, {
    onItemSettled: (result, index, total) => {
      if (result.ok) attachToBid(result.id, result.signedPdf)
      else markFailed(result.id, result.error.message)
    },
  }).pipe(
    // O certificado é provido UMA vez, para o lote inteiro, e nunca sai da aba.
    Effect.provide(a1SignaturesLayer({ pfx, password: Redacted.make(password) })),
  ),
)

É isso: dezenas de documentos, um certificado, uma passada — e nenhum byte enviado para um servidor.

E ainda atende às exigências brasileiras

A saída é PAdES. Precisa da rubrica em todas as páginas? Carimbe antes de assinar com stampPdfRubric({ pages: "all" }), para que o byte range de uma única assinatura cubra todas as páginas. Precisa da política ICP-Brasil embutida? Passe icpBrasil no input de assinatura. Nada disso muda onde a assinatura acontece — continua sendo o navegador.

A escolha de PAdES não é casual: é o padrão ETSI EN 319 142 para assinaturas eletrônicas avançadas embutidas no próprio PDF, com perfis baseline (B-B, B-T, B-LT, B-LTA) que viajam junto com o documento (PAdES, Wikipedia). A assinatura fica dentro do arquivo entregue na disputa, sem depender de nenhum servidor da Licitei para ser verificada depois.

O que isso mudou para a Licitei

  • Custo marginal zero por assinatura. A parte cara era o servidor e a custódia da chave, e não existe servidor. Assinar dezenas de documentos por disputa custa o mesmo que assinar um: nada.
  • O certificado nunca sai do dispositivo. O .pfx e a senha ficam na aba, Redacted de ponta a ponta — a chave sob controle exclusivo do titular, como o NIST recomenda. É a melhor resposta possível para "para onde foi minha chave privada?": ela não foi a lugar nenhum.
  • Não quebra o fluxo. O licitante nunca sai da Licitei. Sobe, posiciona, assina o lote, anexa — na mesma tela, contra o mesmo prazo de dez minutos.
  • Um documento ruim não derruba o lote. Uma assinatura que falha é um resultado tipado, não uma exceção. Os outros documentos continuam assinando; a falha aparece com um código que dá para tratar.

A criptografia nunca foi a parte difícil — é um hash assinado em microssegundos. Tirar isso do servidor — e manter tipado, em lote e no navegador — é o que tornou a assinatura com certificado digital algo que a Licitei pôde incluir, em vez de tarifar.

Continue lendo