Pular para o conteúdo principal

Integração com o Lacuna Signer

Passo a passo para o operador fazer um perfil assinar pelo Lacuna Signer, em vez de usar um certificado mantido localmente. A assinatura com certificado local (PFX / PKCS#11 / repositório do Windows) e a assinatura pelo Lacuna Signer coexistem, cada uma em seu perfil — pastas monitoradas diferentes podem usar métodos de assinatura diferentes na mesma instância.

Quando usar isto​

Escolha Method = LacunaSigner para um perfil quando:

  • Uma pessoa (e não um certificado mantido pelo servidor) precisa assinar cada documento — por exemplo, contratos com contra-assinatura, contratos de trabalho, documentação de admissão de RH.
  • A identidade do signatário é a do participante, e não a do serviço. Cada documento despachado pertence ao participante, do lado do Signer.
  • A trilha de auditoria que você quer é a que o Signer mantém (identidade do signatário, evidência da assinatura, motivos de recusa, expiração).

Escolha Method = Local (o padrão) quando:

  • A assinatura é do serviço — assinatura automatizada de notas fiscais com o certificado de assinatura da empresa, assinatura de NF-e em tempo de execução em um token PKCS#11, contra-assinatura em lote.
  • O certificado fica no host (PFX / HSM / repositório do Windows) e não há uma pessoa no circuito.

Os dois podem rodar lado a lado. Uma única instância pode monitorar input/contracts/ (LacunaSigner) e input/nfe/ (PKCS#11 local) ao mesmo tempo.

Escolha Method = SignerFolder — a direção oposta — quando uma pessoa cria documentos no Lacuna Signer e indica como participante o titular do certificado da organização, e a assinatura desse participante deve chegar sem que ninguém precise tocar em um token. Veja Assinando em uma pasta do Lacuna Signer abaixo.

Resumo da arquitetura​

input/ ─▶ Observador ─▶ Queued ─▶ worker reivindica
│
profile.Method? ───┤
│
Local ───────────────────────────────▶ assina no slot ─▶ Verifying ─▶ Completed
│
LacunaSigner ─▶ upload + cria documento ─▶ AwaitingSigner (slot de concorrência LIBERADO)
│
tique do worker de consulta ──┤
│
Pending → continua AwaitingSigner
Concluded → baixa os bytes ─▶ Verifying ─▶ Completed
Refused/Expired/Canceled → Failed
timeout → Failed

São dois workers que cooperam, em vez de um:

  1. O worker do pipeline reivindica jobs Queued e, em perfis LacunaSigner, apenas os despacha ao Signer (upload + criação do documento) e os passa para AwaitingSigner. O slot do pipeline é liberado imediatamente após o despacho — o job agora está retido do lado remoto, e o worker fica livre para pegar o próximo item.
  2. Um worker de polling separado acorda a cada Signer:PollIntervalSeconds (padrão 30 s) e percorre todas as linhas AwaitingSigner. Para cada linha, ele verifica o status do documento na API do Signer; os documentos concluídos são baixados e passam pela mesma etapa final de verificar → criptografar → promover que o caminho Local usa.

Essa divisão importa: ocupar um slot de Pipeline:MaxConcurrency enquanto uma pessoa leva dias para assinar anularia completamente o propósito da fila.

A máquina de estados, estendida​

Em perfis LacunaSigner, o AwaitingSigner se encaixa entre Processing e Verifying:

Queued ─▶ Processing ─┬─ assinatura local ok ────────▶ Verifying ─▶ Completed
│ └▶ Failed
└─ despachado ao Signer ─▶ AwaitingSigner
│
concluído → download ───────┼──▶ Verifying ─▶ Completed
recusado/expirado/timeout ──┴──▶ Failed
cancel do operador ────────────▶ Canceled (cancelamento remoto em melhor esforço)

Perfis que usam apenas o método Local nunca entram em AwaitingSigner. Perfis LacunaSigner nunca seguem o caminho local direto Processing → Verifying.

Configuração​

Signer:* — um tenant por host​

A conexão com o Signer é global — um endpoint + uma chave de API para o host, compartilhados por todos os perfis que usam Method = LacunaSigner ou Method = SignerFolder. Os dois métodos leem metades diferentes do restante do bloco:

ChaveTipoPadrãoOverride por envUsada porObrigatória quando
Signer:Endpointstring""Signer__Endpointos dois métodosAlgum perfil usa LacunaSigner ou SignerFolder, ou qualquer outra parte de Signer: está definida. Padrão na nuvem: https://signer.lacunasoftware.com.
Signer:ApiKeystring""Signer__ApiKeyos dois métodosREQUIRED, SECRET, mesma condição. Formato: application-id|secret.
Signer:PollIntervalSecondsint30Signer__PollIntervalSecondsLacunaSigneropcional — a frequência com que o worker de polling percorre as linhas AwaitingSigner. 1–3600.
Signer:TimeoutHoursint168 (7 dias)Signer__TimeoutHoursLacunaSigneropcional — quanto tempo um documento despachado pode esperar pelo participante. 1–8760.
Signer:MaxConsecutiveApiFailuresint5Signer__MaxConsecutiveApiFailuresLacunaSigneropcional — o limite por documento do polling, descrito abaixo. 1–100.
Signer:FolderSweepIntervalSecondsint300 (5 minutos)Signer__FolderSweepIntervalSecondsSignerFolderopcional — a frequência com que cada pasta do Signer vinculada é listada; é o máximo que um documento espera até este host percebê-lo. 60–86400.
Signer:WebhookSecretstring"" (notificações desligadas)Signer__WebhookSecretSignerFolderopcional, SECRET — o token Bearer que o Signer apresenta em uma notificação. Pelo menos 32 caracteres. Defini-lo conta como definir o bloco, então Signer:Endpoint e Signer:ApiKey passam a ser obrigatórias.

Um host que precisa de apenas um método pode deixar as chaves do outro nos valores padrão: nada as lê. Todas as chaves, com seus limites e recusas, também estão em Configuração.

O validador só age sobre esta seção quando ela existe — omita Signer:* por inteiro e nada aqui é exigido, que é o que acontece em uma implantação puramente Local. Escreva qualquer parte dela e o bloco inteiro é validado.

Mudou na 2.1.0 — o bloco Signer:* é validado por si só

Até a 2.0.x, o bloco só era validado quando algum perfil selecionava Method = LacunaSigner. Os perfis agora ficam no banco de dados operacional e podem ser trocados para o Lacuna Signer pelo dashboard sem reinicialização, então a regra passou a valer ao contrário:

  • Um bloco Signer: preenchido pela metade impede o boot, mesmo que nenhum perfil o use — um endpoint sem chave de API deixado para depois, por exemplo. A mensagem cita as duas chaves e sugere remover a seção como solução.
  • Selecionar Method = LacunaSigner é recusado quando o host não tem configurações Signer:* — no boot, para um perfil ainda importado da configuração pelo seed (carga inicial), e na página do perfil, para um perfil que está sendo salvo.
  • É a presença das configurações Signer:* no host que inicia o gateway do assinador remoto e o worker de polling, então um perfil trocado para o Lacuna Signer depois do boot começa a despachar sem reinicialização. Em um host com o bloco inteiro definido e nenhum perfil que o use, o worker de polling roda e não encontra nada a fazer a cada intervalo — remova a seção se este host assina tudo localmente.
A chave de API e o segredo do webhook são segredos.

Defina-os como Signer__ApiKey e Signer__WebhookSecret no bulksigner.env (Linux) / em uma variável de ambiente de máquina (Windows) / no .env (Docker). Os valores literais são mascarados nos logs.

Um perfil que seleciona Method = SignerFolder é recusado em um host sem configurações Signer:* exatamente como um perfil LacunaSigner, e são essas mesmas configurações que iniciam a varredura de pastas do Signer. Um perfil SignerFolder já armazenado, em um host que depois perdeu as configurações, fica degradado — nunca impede o boot.

Escolhendo o método pelo dashboard​

É aqui que você escolhe o método em uma implantação em execução. O Signing:Profiles[] é um seed de uso único, importado no primeiro boot (veja Configuração); por isso, a próxima seção descreve como é um seed, e não onde você muda um perfil.

O painel Certificado em /profiles/{name} contém o método, e o formulário em /profiles/_new também. Ele fica nesse painel, e não no de Comportamento, porque o método decide se o perfil tem ou não uma chave local: escolha Lacuna Signer, e a origem do certificado e os dados dela são substituídos pelos três campos do participante — nome, e-mail e identificador —, que são tudo o que define de onde vem a assinatura de um perfil assim. Escolha Pasta do Lacuna Signer, e o certificado continua, com um seletor de Pasta do Lacuna Signer acima dele — veja Assinando em uma pasta do Lacuna Signer abaixo.

As direções da troca são diferentes, e o formulário informa qual se aplica antes de você salvar. A regra por trás cabe em uma frase: uma mudança espera uma reinicialização exatamente quando deixa o perfil precisando de uma chave que nenhuma instância em execução abriu.

TrocaQuando passa a valerO que acontece com o outro bloco
Local ou pasta do Signer → Lacuna SignerNo próximo job reivindicado. Sem reinicialização — o gateway roda em todo host que tem configurações Signer:*, e não só para os perfis que existiam no boot.Os dados do certificado são apagados, inclusive a senha: uma credencial armazenada para uma chave que fica no serviço remoto é uma credencial que nada jamais usará. O vínculo com uma pasta do Signer é desfeito.
Lacuna Signer → Local ou pasta do SignerNa próxima reinicialização, porque uma chave privada precisa ser aberta, e salvar o formulário não abre nenhuma. Até lá, o perfil aparece como degradado na própria página, e os jobs roteados para ele falham com profile.degraded.O participante é apagado.
Local ↔ pasta do Signer, mesmo certificadoNo próximo job reivindicado. Sem reinicialização — os dois assinam com a chave que este host já abriu, e ela é mantida.Sair da pasta do Signer desfaz o vínculo com a pasta.
Qualquer salvamento que também mude um dado do certificadoNa próxima reinicialização, qualquer que seja o método — a regra do certificado não muda.—

As recusas acontecem ao salvar, no seu idioma de exibição: um participante sem algum dos três campos, um e-mail sem @, e selecionar o Lacuna Signer em um host sem configurações Signer:* — a única recusa cuja solução é uma mudança de configuração e uma reinicialização, e não um campo do formulário; por isso a mensagem cita as chaves.

Os dígitos verificadores do identificador do participante não são validados. Diferentemente do CPF de um aprovador — que este produto grava nos próprios registros de auditoria —, este é entregue ao serviço remoto, e é o serviço que decide se conhece o participante.

Signing:Profiles[].Method + bloco Signer​

Seleção do método por perfil como seed, importada no primeiro boot com a tabela de perfis vazia. O padrão é Method = Local, então os perfis que já existiam não precisam de mudança.

"Signing": {
"Profiles": [
{
"Name": "contracts",
"Format": "Pades",
"Method": "LacunaSigner",
"Verify": true,
"Encrypt": false,
"ValidateCertificate": false,
"Signer": {
"Name": "Jack Bauer",
"Email": "jack.bauer@example.com",
"Identifier": "75502846369"
}
}
]
}

Validação no nível do perfil:

  • Method = LacunaSigner exige um bloco Signer:{Name, Email, Identifier} não vazio. O validador recusa blocos parciais.
  • Method = LacunaSigner proíbe um bloco Certificate:* (não há certificado local envolvido).
  • Method = LacunaSigner proíbe ValidateCertificate = true (não há certificado local a validar).
  • As regras de Method = Local não mudam: o bloco de certificado é obrigatório, e o bloco Signer é ignorado se estiver presente.
  • Method = LacunaSigner não pode ser combinado com uma regra de aprovação cujo conjunto de assinantes seja ProfileKeyAndApprovers — o assinador remoto receberia um envelope de assinaturas de aprovadores, e não o arquivo de pagamento. Veja Aprovações.

As mesmas regras fazem um salvamento ser recusado na página do perfil. O perfil default derivado (criado pelo seed quando Signing:Profiles[] é omitido) é sempre Method = Local.

Fluxo do operador​

  1. O operador coloca um arquivo em uma pasta monitorada por um perfil LacunaSigner (ou usa POST /api/files?profile=contracts).
  2. O observador / endpoint enfileira o job; Status = Queued.
  3. O worker do pipeline reivindica o próximo slot, passa o job para Processing e então faz o upload e cria o documento no Signer. Em caso de sucesso, o job passa para AwaitingSigner, com o id do documento remoto registrado; o slot é liberado.
  4. O Signer envia um e-mail ao participante, que assina pela interface do Signer quando puder.
  5. O worker de polling executa um tique a cada Signer:PollIntervalSeconds. Em cada tique, ele carrega todas as linhas AwaitingSigner, das mais antigas para as mais novas, e, para cada uma:
    • Pending → não mexe na linha.
    • Concluded → baixa os bytes assinados, passa o job para Verifying, executa a mesma etapa final de verificar → opcionalmente criptografar → promover e passa o job para Completed.
    • Refused / Expired / Canceled → passa o job para Failed com signer.document-rejected.
    • Timeout local (AwaitingSigner por mais tempo que Signer:TimeoutHours) → passa o job para Failed com signer.timeout. O documento remoto é deixado como está, do lado do Signer.

O dashboard exibe AwaitingSigner como um status próprio (chip amarelo, ícone de ampulheta). A página de detalhe do job mostra o id do documento remoto e a hora do despacho, e um card de estatística Aguardando assinatura aparece quando algum perfil LacunaSigner está configurado.

Semântica do cancelamento​

Em perfis LacunaSigner, o cancelamento pelo operador passa a valer para {Queued, AwaitingSigner}. Processing e Verifying continuam intocáveis.

Quando um operador cancela um job AwaitingSigner:

  1. O job passa para Canceled localmente — mesmo handler, mesma trilha de auditoria.
  2. Em seguida, o handler faz uma chamada de cancelamento remoto ao Signer, em melhor esforço. As falhas são registradas no log como Warning, mas não desfazem o cancelamento local.
  3. Se o cancelamento remoto falhou, o participante ainda pode ver o documento na caixa de entrada do Signer. O job local está corretamente Canceled de qualquer forma.

O botão Cancelar na página do job pede confirmação antes: o diálogo mostra o nome do arquivo e explica o que o cancelamento faz a partir do status atual do job — no caso de um job aguardando o Lacuna Signer, que o documento remoto dele é cancelado em melhor esforço. O POST /api/jobs/{id}/cancel não pede confirmação.

O Limpar Jobs não cancela documentos remotos

O Limpar Jobs apaga todos os registros de jobs, qualquer que seja o status, inclusive AwaitingSigner, mas não faz nenhuma chamada ao Lacuna Signer: um documento já despachado continua na caixa de entrada do participante. Cancele esses jobs antes se o participante não deve assiná-los. Veja Operação.

O cancelamento em melhor esforço é uma escolha deliberada.

Desfazer o cancelamento local porque uma ida e volta de rede falhou deixaria o operador sem saber em que estado o job ficou e contradiria o princípio de que "cancelar encerra a questão". O caso de um documento remoto órfão é raro e inofensivo — o participante pode ignorar o e-mail, ou o operador pode fazer a limpeza na administração do Signer.

Falhas de API e o limite por job​

A integração com o Signer distingue dois tipos de falha:

  • Transitória — uma conexão recusada ou um timeout, um 5xx, 408 ou 429, ou uma resposta que não vem da API do Signer (a página de erro de um proxy). O worker de polling incrementa um contador de falhas por documento e segue para a próxima linha. O contador zera na primeira chamada bem-sucedida. Quando o Signer:MaxConsecutiveApiFailures é excedido para um único documento, aquele job falha com code = signer.unreachable. As outras linhas não são afetadas.
  • Permanente — um 4xx que não se resolve com novas tentativas (chave de API inválida, documento desconhecido, requisição malformada). O job falha imediatamente com code = signer.unreachable.

Um reinício do processo zera os contadores de falha, que ficam em memória. Se a indisponibilidade de origem foi resolvida entre as falhas e o reinício, o polling é retomado normalmente no próximo boot.

Mudou na 2.16.0 — toda forma de "Signer fora do alcance" conta para o limite

Até a 2.15.x, só uma falha de conexão pura era reconhecida como o Signer estando inacessível. Um 5xx, um 408 ou 429, a página de erro de um proxy ou o timeout do próprio cliente REST durante um polling ou download era registrado no log e tentado de novo a cada tique, sem contar, até o Signer:TimeoutHours. A partir da 2.16.0, cada um deles conta para o Signer:MaxConsecutiveApiFailures e para o bulksigner_signer_api_errors_total; assim, uma indisponibilidade mais longa que o limite faz o job falhar com signer.unreachable — use Tentar novamente quando o Signer voltar. Um erro HTTP que a API do Signer não formatou, como um 4xx puro vindo de algo no caminho, agora faz o job falhar na hora como permanente, como as recusas do próprio Signer sempre fizeram.

Assimetria entre despacho e polling.

O Signer:MaxConsecutiveApiFailures protege apenas o caminho de polling. Uma falha transitória durante o despacho faz o job falhar no primeiro erro, em vez de ser repetida até um limite — por design, já que o despacho é uma única chamada curta no início do job. Se o seu endpoint do Signer é instável a ponto de as falhas de despacho serem um problema, faça uma nova tentativa pelo dashboard ou por REST (POST /api/jobs/{id}/retry) quando o serviço remoto voltar.

Recuperação após reinício — linhas AwaitingSigner NÃO são varridas​

A varredura de recuperação na inicialização passa para Failed qualquer job travado em Processing / Verifying (eles estavam em andamento quando o processo anterior morreu) — exceto um job SignerFolder, que não tem arquivo e é encerrado pela resposta do Lacuna Signer (veja Quando a execução que o assinava termina). As linhas AwaitingSigner são explicitamente excluídas — o trabalho está retido do lado remoto; varrê-las localmente perderia dados que não cabe ao host invalidar. O worker de polling retoma o polling dessas linhas no próximo boot, exatamente de onde parou.

O que chega a output/​

Em perfis LacunaSigner, os bytes promovidos para output/ são os bytes que o Signer assinou — a assinatura do participante sobre o documento original, baixada depois que o documento é concluído. As etapas de verificação e criptografia rodam sobre esses bytes exatamente como rodariam em um perfil Local; então:

  • Verify = true (padrão) — a assinatura é verificada com base na política configurada, após o download.
  • Encrypt = true + Encryption:Enabled = true — os bytes baixados são criptografados com AES-256-GCM em um envelope BSENC v1; o texto claro nunca é gravado em output/.

Os arquivos de entrada originais só são apagados de input/ depois que a etapa de verificação é concluída com sucesso — a mesma garantia do caminho Local.

Métricas​

Instrumentos Prometheus específicos do Signer são expostos em /api/metrics:

MétricaTipoO que ela acompanha
bulksigner_jobs_dispatched_to_signer_total{profile}CounterDespachos bem-sucedidos ao Signer, rotulados pelo nome do perfil.
bulksigner_jobs_awaiting_signerGaugeContagem atual de linhas AwaitingSigner.
bulksigner_signer_poll_duration_secondsHistogramDuração, por tique, de uma passada completa sobre as linhas AwaitingSigner.
bulksigner_signer_api_errors_total{op}CounterFalhas da API do Signer encontradas pelo worker de polling, rotuladas por operação (poll, download). Falhas de despacho e de cancelamento remoto ficam no registro do job — veja a nota sobre a assimetria acima.

O método SignerFolder tem instrumentos próprios — veja Métricas e auditoria abaixo.

No modo cluster​

Com o modo cluster ligado, cada instância só faz polling no Lacuna Signer dos documentos que ela mesma despachou, de modo que duas instâncias nunca baixam os mesmos bytes assinados. Há duas consequências para dashboards e alertas: o bulksigner_jobs_awaiting_signer é por instância — some os valores de toda a frota —, e um job que uma instância despachou antes de morrer é reatribuído a uma sobrevivente pela varredura de assunção. Uma linha sem dono algum não recebe polling de ninguém. Veja Alta disponibilidade para os detalhes e a solução.

Assinando em uma pasta do Lacuna Signer — SignerFolder​

Novo na 2.16.0

Um terceiro método de assinatura. Nada muda para um host sem perfil SignerFolder.

Tudo o que está acima segue uma direção: este host envia um arquivo ao Signer, e uma pessoa o assina lá. Method = SignerFolder é a outra: uma pessoa cria um documento no Lacuna Signer, em uma pasta vinculada a um perfil do Bulk Signer, com o titular do certificado do perfil como participante; este host o assina com o certificado do próprio perfil, e o Signer incorpora a assinatura ao documento e o guarda. Nenhum arquivo entra em input/, nada chega a output/, e o documento deixa de ser acompanhado assim que o Signer aceita a assinatura.

Pessoa Lacuna Signer Bulk Signer
│ cria um documento na │ │
│ pasta vinculada, indicando │ │
│ o titular do certificado ▶ │ ── notificação (opcional) ────────▶ │
│ │ ◀── varredura / checagem: relê ──── │
│ │ │ vez do titular? → job
│ │ ◀── inicia com o certificado ────── │
│ │ ─── hash a assinar ───────────────▶ │
│ │ │ assina com a chave do perfil
│ │ ◀── conclui com a assinatura ────── │
│ │ incorpora, guarda o documento │

O perfil​

Ele mantém um certificado — qualquer origem, inclusive um PKCS#12 enviado e o Azure Key Vault, com a recusa por host do modo cluster inalterada — e ganha uma pasta do Lacuna Signer, pelo id da pasta no Signer, no lugar de uma pasta de entrada. O vínculo é um para um e com a própria pasta: dois perfis não podem compartilhar uma pasta, e um documento em uma subpasta não é do perfil. O titular do certificado é identificado pelo CPF que o certificado traz — certificados e-CPF e e-CNPJ trazem um —, nunca por um campo digitado. O perfil reaproveita o Signer:Endpoint e a Signer:ApiKey deste host (um tenant por host), e é recusado em um host sem eles. O seed pode declarar um perfil assim — o formato da configuração e todas as recusas estão em Configuração —, e o dashboard também, como descrito abaixo.

Escolhendo a pasta pelo dashboard​

Em /profiles/_new, ou no painel Certificado de /profiles/{name}, escolha Pasta do Lacuna Signer como método. Os campos do certificado continuam — este host assina com aquela chave — e um seletor de Pasta do Lacuna Signer aparece acima deles: ele lista as pastas da organização do Signer a que pertence a chave de API deste host, com busca pelo nome, inclusive pastas aninhadas, e grava o id da pasta junto com o nome com que você a escolheu, que é o que a página do perfil mostra depois. Apenas a própria pasta é vinculada, nunca as subpastas. Se não for possível contatar o Signer, o seletor avisa e mantém a pasta já escolhida; nada mais no formulário é afetado. A página também informa, ao lado do seletor, que o Signer escolhe o formato e que a política ICP-Brasil em um PDF é uma configuração da organização no Signer.

Nesse método, o painel Comportamento esconde o que o Signer decide — a pasta de entrada, a verificação, a criptografia e a checagem CNAB240 — e as salva desligadas; o formato continua visível, mas é ignorado, guardado para uma mudança futura para um método que o leia. O salvamento é recusado, no seu idioma, quando há:

  • nenhuma pasta, ou uma pasta em que outro perfil já assina — com o nome desse perfil. Dois salvamentos que escolhem a mesma pasta no mesmo instante são decididos pelo banco operacional, e o perdedor recebe a mesma mensagem;
  • um host sem configurações Signer:*;
  • o perfil default — é para ele que vão os uploads sem perfil e as pastas sem vínculo, e este método não recebe arquivos;
  • a mudança de um perfil para o método enquanto ele ainda tem verificação, criptografia, validação do certificado ou a checagem CNAB240 ligadas, uma pasta de entrada ou uma regra de aprovação — desligue, limpe a pasta ou remova a regra nos respectivos painéis antes;
  • a mudança de um perfil para fora do método quando ele não tem formato escolhido — escolha um no painel Comportamento antes;
  • em /profiles/_new, que abre o certificado enquanto você espera, um certificado sem CPF: nenhuma ação do fluxo no Signer poderia ser associada a ele. Use um e-CPF ou e-CNPJ ICP-Brasil.

O Signer escolhe o formato, não o perfil​

Um PDF é assinado em PAdES, um documento XML em XAdES, e qualquer outro em CAdES (AD-RB) — o Format do perfil é ignorado, e o job registra o formato que o Signer vai usar. Em um PDF, a aplicação da política ICP-Brasil depende da configuração UseBrazilianPdfSigningPolicies da organização no Signer, e não do Bulk Signer: o padrão ADR-Básica do produto não alcança uma assinatura que o Signer monta. Verificação, criptografia, validação do certificado e a checagem CNAB240 são do Signer ou não se aplicam; por isso o perfil precisa ter Verify, Encrypt, ValidateCertificate e CheckCnab240 desligados, e não tem regra de aprovação — o fluxo, aprovadores inclusive, é decidido no Signer.

Como um documento é encontrado​

A cada Signer:FolderSweepIntervalSeconds (padrão: cinco minutos), uma varredura lista os documentos pendentes da pasta de cada perfil habilitado e relê cada um com a própria chave de API deste host. Um documento vira um job quando ainda está exatamente naquela pasta, ainda está pendente, e a ação do fluxo pendente atual é uma ação de assinatura cujo identificador do participante é o CPF do titular. Um documento cujos participantes anteriores ainda não agiram não gera nada — ele é encontrado de novo quando chega a vez do titular, já que é o Signer, e não o Bulk Signer, que controla essa espera.

Cada par (documento, ação do fluxo) é definitivo depois de ter tido um job: um documento encontrado de novo — pela próxima varredura, ou por outra instância no modo cluster — não cria nada, qualquer que seja o status do job existente, e Tentar novamente é o único caminho de volta (abaixo). O par fica registrado como descoberto em um registro próprio, que excluir o job e o Limpar Jobs mantêm; assim, nenhum dos dois faz a varredura assinar o documento de novo.

Um documento pendente na pasta sem nenhuma ação de assinatura para o titular — nem mais adiante no fluxo, nem já concluída — é algo que uma pasta vinculada um para um não deveria conter: ele é registrado uma vez como um job Failed com signer-folder.no-holder-action, para que alguém o veja, e todas as varreduras seguintes o deixam em paz.

A varredura roda com o pipeline pausado ou não — a descoberta só enfileira jobs; a pausa impede que eles sejam reivindicados — e a pasta de um perfil desabilitado é ignorada. Uma notificação do Signer reduz a espera para segundos — abaixo —, e a varredura continua sendo a garantia de qualquer forma.

Como ele é assinado​

O job é reivindicado como qualquer outro — sob a chave de pausa e o Pipeline:MaxConcurrency, e é por isso que cada assinatura é um job — e o worker:

  1. relê o documento e confere se a ação do fluxo ainda é do titular e ainda é a atual;
  2. pede ao Signer a URL de ação do titular (CPF e o e-mail do participante da ação, e o id da ação quando o titular tem mais de uma pendente) e extrai dela a chave do documento e o ticket;
  3. inicia uma assinatura pública com o certificado, recebendo o hash a assinar e o algoritmo de digest com que ele foi calculado;
  4. recusa qualquer coisa que não seja SHA-256, SHA-384 ou SHA-512 — SHA-1 e um digest desconhecido ou ausente, pelo nome, e um hash com o tamanho errado — antes de tocar na chave;
  5. assina o hash como hash com a chave do perfil e devolve a assinatura ao Signer, que a incorpora ao documento.

O job passa por Processing, depois por Verifying enquanto o Signer confere a assinatura, e então Completed quando o Signer a aceita. A página do job e a lista /jobs mostram o nome do documento e o tipo MIME dele.

Quando ele não é assinado​

Qualquer outro desfecho é um de dois, e o documento nunca é cancelado nem recusado no Signer em nenhum deles: este host não o criou, e ele continua pendente lá.

  • Canceled — o documento seguiu adiante antes de este host assiná-lo. Ele foi cancelado, recusado ou expirou no Signer, a ação do titular já foi concluída, ou o Signer respondeu à URL de ação ou ao início que a ação não está pendente (NoPendingActionFoundForEmailAndIdentifier, SignatureNotPending, FlowActionNotPending) e uma segunda leitura confirma — uma segunda leitura que ainda mostra a ação pendente significa que o Signer não associou o participante, e o job termina Failed com signer-folder.signature-failed. Nada deu errado aqui, então o job não tem código; o histórico diz No longer pending at Lacuna Signer: <status>. Uma ação que volta a esperar uma etapa anterior não "seguiu adiante" — é um fluxo editado por baixo deste host, e falha com signer-folder.signature-failed, já que um par que teve job nunca é redescoberto.
  • Failed com um código — todo o resto. signer-folder.signature-refused quando o Signer recusou a assinatura na resposta (os resultados da validação ficam no histórico do job); signer-folder.key-unavailable quando a chave do perfil não assinou; signer.unreachable quando não foi possível contatar o Signer ou ele não respondeu de forma útil — uma conexão recusada, um timeout, um 5xx, 408 ou 429, ou uma resposta que não vem da API do Signer, como a página de erro de um proxy; signer-folder.action-url-malformed quando a URL de ação do Signer não trazia chave ou ticket; signer-folder.profile-changed quando o perfil do job saiu do método; signer-folder.interrupted quando a execução que o assinava terminou no meio da assinatura (abaixo); signer-folder.signature-failed para o resto — o documento saiu da pasta, a ação deixou de ser do titular, um digest sob o qual este host não assina —, com o motivo no histórico. Cada um está em Diagnóstico de problemas.

Uma falha depois de a assinatura ter sido entregue ao Signer — uma conclusão que deu timeout — informa no job que o Signer pode tê-la aceitado; um Tentar novamente então relê o documento e termina Canceled em vez de assinar duas vezes. Uma falha depois de a assinatura ter sido calculada, mas antes de ser entregue, informa isso: o documento está intacto no Signer.

Quando a execução que o assinava termina​

Uma reinicialização, ou uma instância que parou de responder no modo cluster: a recuperação na inicialização, e a sobrevivente que assume o job, não podem olhar em output/ para um job assim; por isso elas perguntam ao Lacuna Signer — uma leitura do documento com a chave de API deste host, nada mais — e encerram o job pela resposta:

O job estavaO Lacuna Signer informaO job termina
Processing, a chave nunca foi acionada— (não é perguntado)De volta à fila: nada foi tentado, então isto é recuperação, não uma nova tentativa. A próxima reivindicação relê o documento, como toda reivindicação
Verifying — a assinatura foi entregue ao Signera ação do fluxo do titular CompletedCompleted, com o histórico dizendo que o Signer informa a ação como concluída. Nada é gravado em disco; não há o que gravar
Processing, depois da chave — a assinatura nunca saiu deste hosto documento ou a ação do titular seguiu adianteCanceled, No longer pending at Lacuna Signer: … — o mesmo desfecho da etapa de assinatura, já que a assinatura deste job nunca chegou ao Signer
qualquer um, depois da chavea ação do titular ainda pendente, ou qualquer estado que este host não interpreta como um desfechoFailed, signer-folder.interrupted, com o histórico dizendo se a assinatura tinha sido entregue e o que o Signer informou
qualquer um, depois da chave— o Signer não está configurado neste host, não responde ou responde com erroFailed, signer-folder.interrupted, com o histórico dizendo que não foi possível perguntar ao Signer, e por quê

Um job que passou da chave nunca volta à fila, diga o Signer o que disser: isso seria uma segunda assinatura que ninguém decidiu. Confira o documento no Lacuna Signer antes de usar Tentar novamente em um job signer-folder.interrupted — a nova tentativa o relê e termina Canceled se a ação do titular já estiver concluída, então ela nunca assina duas vezes, de qualquer forma.

A consulta é limitada para que o Signer não consiga segurar um boot: cada leitura é interrompida após dez segundos, a recuperação inteira para de perguntar após trinta, e, depois que o Signer deixa de responder, os jobs restantes daquela recuperação seguem o caminho alternativo sem perguntar — o host sobe de qualquer forma. Um job cuja conclusão este host não conseguiu registrar depois de o Signer aceitar a assinatura é exatamente a linha Verifying que a tabela conclui no próximo início.

Tentar novamente é o único caminho de volta​

A partir de Failed e também de Canceled — inclusive o cancelamento de um job Queued pelo operador, que é só local e nunca chega ao Signer. Uma nova tentativa é um job novo que aponta o antigo como pai, para o mesmo documento e a mesma ação do fluxo, e ela recomeça do zero: a reivindicação relê o documento, pede uma nova URL de ação e abre um novo início, cujo token é de uso único. Ela assina enquanto a ação do titular ainda está pendente e termina Canceled quando não está mais. Só um job por (documento, ação do fluxo) trabalha por vez; por isso, uma segunda tentativa enquanto a primeira está na fila é recusada com job.already-processing. Um documento registrado com signer-folder.no-holder-action não aceita nova tentativa — não há nada deste host a repetir, e, quando o titular é adicionado no Signer, a próxima varredura encontra a nova ação sozinha.

Quando o próprio perfil não consegue trabalhar​

Três condições param um perfil SignerFolder sem parar o host nem nenhum outro perfil, e cada uma é informada como um certificado que não abre: na própria página do perfil e em /profiles como Este perfil não consegue assinar, como degraded: true no GET /api/profiles, e como uma linha signing-profile:<name> em /api/ready que fica vermelha, mas não faz a resposta falhar. Um job já enfileirado para o perfil falha com profile.degraded, citando o motivo; a varredura e as notificações não criam job novo para ele, e um Tentar novamente cria, mas falha da mesma forma ao ser reivindicado.

  • Este host não tem configurações Signer:*. Todo salvamento e o seed recusam o método em um host assim; então este é um perfil armazenado cujo host perdeu o Signer:Endpoint e a Signer:ApiKey depois — ou um irmão do cluster configurado de outro jeito. Sem credencial, a varredura nem chega a iniciar, e nada na pasta seria encontrado; em vez de não fazer nada em silêncio, o perfil é degradado citando as duas chaves. Configure as chaves e reinicie, ou tire o perfil do método na página dele.
  • O certificado não tem CPF. Nada no Signer poderia ser associado ao titular. Isso é detectado onde quer que o perfil seja resolvido — na inicialização e na próxima atualização depois de um salvamento que passou o perfil para o método com um certificado assim —, nunca impedindo o boot. Dê ao perfil um e-CPF ou e-CNPJ ICP-Brasil (lido na próxima reinicialização, como toda mudança de certificado), ou tire o perfil do método.
  • A pasta vinculada não existe mais no Signer — foi excluída lá. Nada na inicialização pergunta ao Signer sobre uma pasta: é a varredura que descobre isso, e ela só pergunta ao Signer se a pasta ainda existe quando a listagem de pendentes da pasta volta vazia, já que o Signer lista uma pasta inexistente como vazia. Só a resposta do próprio Signer de que a pasta não existe (FolderNotFound) degrada o perfil; uma indisponibilidade durante a pergunta não degrada — é uma falha daquela varredura, avisada e perguntada de novo na próxima. Vincule o perfil a outra pasta na página dele: a degradação fica com a pasta antiga, na hora, sem reinicialização. A varredura também pergunta de novo a cada passada; assim, uma resposta errada — Signer:ApiKey ou Signer:Endpoint apontando por um instante para outra organização — se resolve sozinha quando o Signer encontra a pasta. Uma reinicialização esquece a degradação, e a primeira varredura depois dela a descobre de novo.

A varredura ignora a pasta de um perfil degradado — sem listagem, sem leitura, sem job, e uma notificação para ela é descartada — e avisa uma vez, no console e no log durável, em vez de a cada passada. Veja Diagnóstico de problemas.

Nenhum arquivo chega a um perfil SignerFolder​

Um upload que o nomeia, um reescaneamento ou uma pasta monitorada cujos arquivos são roteados para ele, ou uma nova tentativa de um job de arquivo cujo perfil passou depois para o método é recusado na entrada com profile.takes-no-files, antes de qualquer coisa ser preparada.

Métricas e auditoria​

MétricaTipoO que ela acompanha
bulksigner_signer_folder_jobs_enqueued_total{profile}CounterJobs criados para documentos encontrados à espera, e para novas tentativas deles. Um documento encontrado de novo não cria nada e não é contado.
bulksigner_signer_folder_signatures_total{outcome}CounterTentativas de assinatura: signed, refused (pelo Signer, na resposta), no-longer-pending (seguiu adiante antes de existir uma assinatura; terminou Canceled) ou failed.
bulksigner_signer_folder_sweep_errors_total{op}CounterChamadas ao Signer que a varredura não conseguiu concluir (list, folder — a pergunta se uma pasta cuja listagem voltou vazia ainda existe —, read, enqueue) e passadas que falharam por inteiro (pass). Uma varredura que falha não perde nada; a próxima pergunta de novo.
bulksigner_signer_notifications_total{outcome}CounterNotificações recebidas — veja abaixo.

Uma varredura que continua falhando é uma linha de prontidão: signer-folder-sweep em /api/ready fica vermelha depois de três passadas seguidas com alguma falha cada — uma Signer:ApiKey revogada faz todas falharem — e /api/ready/details diz desde quando e o que falhou por último, por etapa e tipo (nunca a mensagem do Signer, que pode citar a chave de um documento). Ela nunca muda o veredito: um 503 não traria o Signer de volta e pararia todo o resto que este host faz. Veja Diagnóstico de problemas.

Os eventos operacionais são SignerDocumentEnqueued, SignerDocumentWithoutHolderAction, JobSignedAtSigner e JobNoLongerPendingAtSigner.

Notificações — o webhook​

A varredura encontra um documento dentro de Signer:FolderSweepIntervalSeconds. Uma notificação o encontra em segundos: o Signer chama este host quando um documento é criado, ou quando um participante assina ou aprova — a etapa anterior à do titular é como se descobre que "chegou a vez do titular", já que o Signer não tem evento para uma ação que passa a ficar pendente. Ela é opcional, e só vale a pena onde o Signer consegue alcançar este host; um host sem entrada pública perde minutos, nunca documentos.

Uma notificação é uma campainha, não uma ordem. Nada no corpo dela é levado em conta como verdade. Este host lê quais documentos ela cita e em que pasta ela diz que cada um está, enfileira uma checagem para cada um que está na pasta vinculada a um perfil habilitado e responde — sem chamar o Signer dentro da requisição. A checagem então faz exatamente o que a varredura faz para um documento: relê o documento no Signer com a própria chave de API deste host, e só enfileira um job se ele ainda está exatamente naquela pasta, pendente, e é a vez do titular. Uma notificação forjada pode, no máximo, fazer este host perguntar ao Signer sobre um documento; uma repetida não cria nada, porque a checagem segue o caminho da varredura e a idempotência dela. Os documentos de um perfil desabilitado são confirmados e descartados.

Configurando.

  1. Gere um segredo aleatório longo — pelo menos 32 caracteres — e defina-o neste host como Signer:WebhookSecret (a variável de ambiente Signer__WebhookSecret; nunca no appsettings.json). Reinicie. Sem segredo, a rota responde 404.
  2. No Lacuna Signer, abra a página de integrações da organização e adicione um webhook:
    • URL: o endereço público deste host seguido de /api/signer/notifications — por exemplo, https://bulksigner.example.com/api/signer/notifications. Atrás de um proxy reverso, o endereço que o proxy publica.
    • Autenticação: Bearer, com o segredo do passo 1 como token. Não X-API-Key — esse header é a credencial REST do próprio produto, e uma notificação que chega com ele é recusada.
    • Eventos: o Signer entrega todo evento a todo webhook da organização; este host age sobre DocumentsCreated, DocumentSigned e DocumentApproved e apenas confirma o recebimento dos demais.
  3. Use a entrega de teste do Signer, ou crie um documento na pasta vinculada, e leia /api/ready/details: a linha signer-notifications diz quando a última notificação chegou àquela instância. No modo cluster, cada instância informa só o que chegou a ela.

O que ela responde. 401 para um token Bearer ausente ou errado (signer-notification.unauthorized); 202 quando um documento em uma pasta vinculada foi enfileirado para checagem, ou já estava esperando por ela; 204 para toda outra entrega autenticada — um evento que não pode deixar um documento à espera, uma pasta sem vínculo, a pasta de um perfil desabilitado, um corpo que este host não consegue ler —, porque o Signer tenta de novo qualquer resposta que não seja 2xx, e uma nova tentativa não mudaria nada disso; 503 só quando este host falhou (signer-notification.unavailable): o banco operacional não está utilizável nesta instância, ou há checagens demais esperando. 429 é o limite de requisições da própria rota (RateLimiting:SignerNotification), que o Signer também tenta de novo. As checagens ficam em memória: uma reinicialização esquece as que ainda não foram feitas, e a próxima varredura encontra os documentos delas. O contrato completo está em API REST.

Referências cruzadas de diagnóstico​

Veja em Diagnóstico de problemas os passos de diagnóstico para:


A seguir: Arquivos de pagamento CNAB240. Anterior: Criptografia.