Pitch: A última barreira pra migrar a clínica pro Linux era o token que assina receituários. Derrubar ela me levou a PKCS#11, native messaging e uma briga com o Flatpak
Sou o TI de uma clínica médica e venho migrando as máquinas pro Linux. As únicas que faltavam eram justamente as que mais precisavam: as da recepção, que desde a atualização pro Windows 11 sofriam com travamentos e consumo absurdo de recursos. E eram as que eu não conseguia migrar, por causa de um token GD Burti: é nas máquinas da recepção que se assina receituário (via Lacuna Web PKI) e que se emite nota fiscal na plataforma da prefeitura, com login por certificado digital.
Esse post é a história de como esse token deixou de ser barreira, e do buraco técnico que isso me fez descer: PKCS#11, native messaging, NSS e uma linha de configuração que o Flatpak escreve dentro de todo sandbox e que quase enterrou a ideia.
A primeira tentativa, e por que eu desisti dela
Quem já precisou de certificado A3 no Linux conhece o garimpo: SafeSign numa versão específica pro token GD Burti, SafeNet pro 5100, e por cima os assinadores que cada sistema exige. A comunidade resolveu isso do jeito certo, com container: o distrobox-adv-br, do Pedro HQB, põe drivers, assinadores e dois navegadores dentro de uma distrobox Debian e exporta tudo pro menu. O trabalho dele de garimpar versões e remendos é a fundação de tudo que vem abaixo.
Eu estudei usar ele na clínica. E desisti. O container resolve o problema dos drivers criando outro: o navegador que enxerga o token é o de dentro da box. Pra recepção, isso significava abandonar o navegador que elas já usam, com os perfis, as senhas salvas e os atalhos de anos, e se acostumar com um Chromium ou Firefox novos em folha, mais a regra de "quando for assinar, abre o outro navegador". Pra usuário não técnico, isso é uma receita de chamado de suporte por dia. Deixei pra lá, e a recepção ficou no Windows.
Meses depois, a ideia voltou por outro caminho
Enquanto desenvolvia a Kuuhaku OS (uma distro imutável baseada em fedora-bootc), criei o sora, um CLI que já apresentei aqui: ele roda comandos de dentro de uma box como se fossem nativos do host. E isso reabriu a pergunta da clínica com outro sinal: se um comando da box pode aparecer no host, por que um driver da box não poderia aparecer pro navegador do host?
Fui pesquisar e a primeira descoberta é que "usar certificado no navegador" são dois problemas sem relação entre si, e a clínica usa os dois todo dia:
| O site pede | Quem usa o token | Transporte |
|---|---|---|
| autenticação (nota fiscal na prefeitura, gov.br, Projudi, eproc) | o próprio navegador, carregando o módulo PKCS#11 dentro do processo dele | RPC do p11-kit sobre stdin/stdout |
| assinatura (receituário via Lacuna Web PKI, SAJ, PJe) | um programa à parte, que o navegador executa e conversa por native messaging | stdin/stdout, que o protocolo já usa |
Não é "exportar a .so da box"
A tentação inicial é óbvia: copiar a biblioteca pra fora, ou fazer um exportador genérico de libs. Não funciona.
Um módulo PKCS#11 não é um programa: é uma .so que o navegador carrega dentro do próprio processo via dlopen. Uma biblioteca compilada contra a glibc e as dependências do Debian presente no container não tem contrato nenhum de funcionar carregada num Firefox do Fedora do host. E a alternativa "genérica", remotar qualquer .so por RPC, esbarra na ABI do C: serializar ponteiros, structs aninhadas, callbacks e semântica de threads sem nenhum contrato dizendo o que é entrada, o que é saída e quem é dono de cada alocação. Precisaria de uma IDL por interface.
Só que o PKCS#11 é remotável por um acidente feliz de design: tabela de funções estável, chamadas de granularidade grossa, sem callbacks pra memória de quem chama, e dono da memória bem definido em cada função. Por isso alguém conseguiu escrever um protocolo de marshalling pra ele, função por função: o p11-kit tem remoting desde 2017, criado pra encaminhar um token por SSH. A configuração de módulo (~/.config/pkcs11/modules/*.module) aceita:
remote: |<comando que fala o protocolo em stdin/stdout>
O exemplo da documentação é |ssh user@remote p11-kit remote /path/module.so. Trocar o ssh por distrobox enter resolve o problema inteiro:
remote: |/usr/bin/distrobox enter --name adv-br -- p11-kit remote /usr/lib/libaetpkss.so
O navegador do host carrega o p11-kit-proxy.so (que já vem com o p11-kit de qualquer distro), o proxy lê o .module, sobe o comando, e o driver de verdade roda dentro da box, conversando com a leitora através do pcscd do host, cujo socket o distrobox monta na box.
Coisas que eu já verifiquei, e que podem te poupar um tempo:
- O canal binário atravessa o
distrobox enterintacto. Comparei byte a byte uma mensagem contendo\r\neNUL: hash idêntico dos dois lados. E odistrobox-entermanda as mensagens de progresso pra stderr, então o stdout fica limpo pro protocolo (o native messaging depende da mesma propriedade). - Diferença de versão do p11-kit não importa. Debian 0.25.5 servindo pro Fedora 0.26.5, sem ajuste nenhum. A versão reportada vira até um jeito de saber qual lado respondeu (Cryptoki 3.0 da box, 3.2 do host).
- Objetos, slots e tokens atravessam de verdade. No teste com o módulo de trust do Debian no lugar do driver (pra não depender do hardware), o host enumerou 150 objetos de certificado através do RPC, com o slot rotulado com um caminho do Debian que não existe no host.
C_LogineC_Signusam o mesmo canal.
A metade da assinatura não é um template
O assinador de receituário é outro bicho. O Lacuna Web PKI (e o WebSigner da Softplan, e o da Certisign) não é uma lib no navegador: é um programa separado que o navegador executa e com quem conversa por native messaging (4 bytes de tamanho + JSON, no caso do Chromium). Se o programa roda na box, ele usa o driver da box e alcança o token; o navegador do host só precisa saber executá-lo.
Quem diz ao navegador o que executar é um manifesto JSON no perfil, com um campo path. A ponte é reapontar esse path pra um wrapper que entra na box. Até parece trivial, certo? Errado:
- o manifesto precisa ser copiado de dentro da box com exatamente um campo reescrito: o
allowed_originsamarra o manifesto ao ID da extensão e não pode ser inventado; - Chromium e Firefox usam formatos diferentes (
allowed_originsvsallowed_extensions) e diretórios diferentes; - o
pathtem que ser absoluto, porque o navegador aberto pelo menu tem umPATHque não inclui o diretório do distrobox.
De gambiarra a mecanismo: o sora provide
A primeira versão de tudo isso foi na unha, em shell script dentro do fork, porque o sora ainda não tinha nada parecido. Quando funcionou, olhei pro que tinha escrito e percebi que eram três pontes com o mesmo formato:
| Ponte | O que se escreve no host | Campo que aponta pra box |
|---|---|---|
atalho .desktop | ~/.local/share/applications/*.desktop | Exec= |
| assinador (native messaging) | manifesto no perfil do navegador | path |
| driver de token (PKCS#11) | ~/.config/pkcs11/modules/*.module | remote: |
O método é idêntico: um arquivo de configuração no host cujo campo "execute isto" aponta pra um comando que entra na box. Só mudam o formato do arquivo e o lugar. Então o que virou o comando sora provide (lançado na 0.3.0) não é um exportador de libs: é esse mecanismo uma vez só (criar o wrapper, escrever o arquivo, saber desfazer), com um adaptador por ponto de integração, hoje pkcs11 e native-messaging. Candidatos futuros seguem o mesmo formato: units de socket do systemd de usuário, handlers de MIME, SSH agent, serviços D-Bus.
Reescrevi o fork em cima disso e apaguei a versão na unha. Com navegador nativo, tudo funcionava, e pra clínica isso já bastava: as máquinas de lá usam navegador instalado por pacote. Mas fiquei com uma pergunta atravessada: e se eu usasse dentro do Flatpak, teria como?
O plot twist do Flatpak
Aviso honesto: essa parte não era requisito de ninguém, era desafio que eu me impus. E a resposta inicial era não. Todo sandbox Flatpak recebe um /etc/pkcs11/pkcs11.conf com isto dentro:
# Disable user pkcs11 config, because the host modules don't work in the runtime
user-config: none
E quem escreve esse arquivo é o binário do flatpak, não o runtime. Vale pra qualquer aplicativo, qualquer runtime, qualquer distribuição. Nenhum módulo PKCS#11 de usuário é lido lá dentro, então o caminho óbvio (levar o .module pra dentro do sandbox) está morto antes de começar. Não existe configuração que resolva.
A parte irônica: durante o design do adaptador PKCS#11 eu tinha avaliado e rejeitado uma alternativa, o p11-kit server com socket unix, porque ela exigia um daemon vivo com ciclo de vida próprio e um caminho de socket singular, enquanto o remote: sobe sob demanda e morre com o consumidor. Pois foi exatamente o design rejeitado que voltou como a única porta de entrada no sandbox, com três peças:
- um
p11-kit serverno host, escutando em$XDG_RUNTIME_DIR/p11-kit/pkcs11. A maioria das distros já traz socket activation pronta (p11-kit-server.socket), então nem daemon próprio precisa: ele sobe sob demanda e agrega todos os módulos configurados no host, inclusive osremote:que apontam pra dentro da box; - um
flatpak override --filesystem=xdg-run/p11-kit/pkcs11, que coloca esse socket dentro do sandbox; - o
p11-kit-client.so, que já existe no runtime do Flatpak, registrado no banco NSS do perfil do navegador. Por ser uma biblioteca carregada pelo NSS, e não um módulo lido da configuração PKCS#11, ele escapa douser-config: none.
A cadeia completa, pra quem gosta de ver o encanamento:
navegador Flatpak ─ NSS ─ p11-kit-client.so
│ (socket montado no sandbox)
host ─ p11-kit server ─ módulo "remote: |distrobox enter ..."
│
box ─ p11-kit remote ─ libaetpkss.so ─ pcscd do host ─ leitora USB
Testei no Firefox Flatpak de ponta a ponta. Primeiro a sondagem de dentro do sandbox, carregando o client por ctypes: C_Initialize devolveu CKR_OK e C_GetSlotList enumerou os tokens, um deles servido por um módulo que só existe dentro do container. Depois o teste de verdade, com o token e o certificado reais: o navegador enxerga o certificado e a autenticação funciona de dentro do sandbox. Navegadores da família Chromium em Flatpak devem atravessar pelo mesmo caminho, já que também consomem PKCS#11 via NSS, mas não verifiquei: fica o convite pra quem quiser testar.
A assinatura em Flatpak atravessa por outro caminho: o sandbox não tem distrobox, então o manifesto ganha um shim flatpak-spawn --host, e isso pede uma permissão de portal (--talk-name=org.freedesktop.Flatpak) que é decisão de quem usa a máquina. Os scripts imprimem os comandos de permissão em vez de executá-los.
Armadilhas que custaram horas (ou sessões)
- A box compartilha o namespace de PID com o host. Um
pkill -f "p11-kit server"rodado dentro da box mata processos do host, inclusive o próprio shell cuja linha de comando contém aquele texto. Isso me custou duas sessões de terminal até cair a ficha. Qualquer rotina de parada tem que casar por PID, nunca por padrão de linha de comando. modutil -addcarrega a biblioteca;-rawaddnão. Registrar op11-kit-proxy.sodo host usando omodutilda box falha, porque ele tenta dar load numa.sodo Fedora dentro do Debian. O-rawaddsó escreve a linha nopkcs11.txt, que é o que se quer, e de quebra dispensa instalarnss-toolsno host: omodutilda box escreve no banco NSS do host, montado no mesmo caminho absoluto.- A biblioteca PKCS#11 do SerproID derruba com SIGSEGV qualquer processo que a carregar enquanto um diretório de configuração não existir. Como as ferramentas de diagnóstico carregam todos os drivers instalados, tudo morre junto, mesmo quem nunca usou SerproID. Adaptador nenhum deve validar módulo dando load nele.
- O Firefox 147 mudou o perfil pra
$XDG_CONFIG_HOME/mozilla/firefox, mas os manifestos de native messaging continuam sendo lidos de~/.mozillaincondicionalmente (bug 2005167). Os scripts varrem os dois caminhos. - Latência: cada uso começa com um
distrobox enter, que na minha máquina custa 2,3 s com o container parado e 1,4 s quente. Pro ciclo de vida de um navegador, irrelevante; pra uma ferramenta de linha de comando que abre e fecha o recurso a cada invocação, dá pra sentir. - O
pcscd2.4.1 do Fedora fala numa boa com a lib cliente do Debian trixie da box; o 2.0.3 do Ubuntu 24.04 (Mint 22, Zorin 18) não fala, e é por isso que o projeto original mantém uma variante "legado".
Onde a clínica está hoje
Metade da recepção já roda Fedora Workstation, com Brave Origin nativo, assinando receituário pelo Lacuna e emitindo nota na prefeitura com o token, tudo pelo navegador delas, sem saberem que existe um container no meio. A outra metade ainda está no Windows com Chrome, e a migração segue em andamento; a tendência é todas ficarem com navegador nativo, e o caminho Flatpak fica pronto pra quem precisar dele. Os travamentos do Windows 11, nas máquinas migradas, viraram história.
O projeto
O fork virou o sora-adv-br. O nome vem do original, que nasceu pra advocacia, mas o encanamento serve pra qualquer certificado A3 (a clínica é a prova). O que muda em relação ao original:
- nenhum navegador é instalado: o Firefox, Chrome, Brave ou Vivaldi que a pessoa já usa continua sendo o dela, inclusive em Flatpak;
- só os três drivers por padrão (SafeSign, SafeNet, SerproID); cada assinador é uma opção (
--with-webpki,--with-pjeoffice...). Quem só autentica nunca baixa o Java; - instalação idempotente (falhou no meio, roda de novo e ele pula o que já está pronto) e um
diagnostico.shque confere o encanamento inteiro, do socket do PC/SC ao manifesto de cada assinador.
curl -fsSL https://raw.githubusercontent.com/LLawli/sora-adv-br/main/packaging/install.sh | sh
Custos
- Publicar um recurso da box dá a qualquer aplicativo do host o mesmo acesso que ele teria com o driver instalado localmente. É equivalente a ter o driver no host, não pior, mas vale dizer, porque a expectativa de quem usa container costuma ser a oposta. O PIN continua sendo pedido pelo driver a cada operação.
- O socket do p11-kit expõe ao sandbox todos os módulos PKCS#11 do host, e a permissão de filesystem afrouxa o confinamento do aplicativo. Por isso nada é concedido automaticamente: os scripts imprimem os comandos e a decisão é sua.
- Em Ubuntu 24.04 e derivados, o
pcscddo host é velho demais: atualize-o ou use a variante legada do original.
Feedback que procuro
- Em produção eu só tenho o GD Burti (SafeSign); a cobertura de outros tokens e leitoras depende de quem testar. Se você tiver um A3 na gaveta, o
diagnostico.shfoi feito exatamente pra transformar "não funciona" num relato útil - O mesmo encanamento serve pra advocacia (Projudi, eproc, PJe), contador com e-CNPJ, médico com certificado do CFM. Alguém tem um caso desses pra validar?
- Que outro ponto de integração mereceria um adaptador no
provide? Units de socket do systemd, MIME handlers e SSH agent estão na lista de candidatos, mas caso de uso real vale mais que taxonomia.
Repositório: https://github.com/LLawli/sora-adv-br (GPL-3.0, fork do distrobox-adv-br com o histórico do Pedro preservado)