O que aprendi tentando portar uma distrobox para o Flatpak
Há duas semanas contei aqui como fiz o navegador do host enxergar um token de certificado digital que só funciona dentro de um container (o sora-adv-br). A tese daquele projeto era: não leve o navegador para dentro do container, publique o que está dentro para o navegador de fora. A box do distrobox era só o veículo onde os drivers funcionam.
Se a box é só o veículo, dá para trocar o veículo. Foi o que eu tentei: o mesmo projeto, com um Flatpak no lugar da distrobox. O resultado é o flatpak-adv-br, mas este post não é sobre o resultado: é sobre o que o porte me ensinou de Flatpak. Eu uso Flatpak há anos e achava que conhecia; empacotar drivers PKCS#11, assinadores .NET e um programa Java dentro dele me mostrou o quanto eu só tinha arranhado. Tudo abaixo foi medido executando, não deduzido de documentação.
1. A ideia atravessou em uma linha
A ponte inteira do projeto original é um arquivo de configuração no host cujo campo "execute isto" aponta para um comando que entra no container. Para autenticação por certificado, é um .module do p11-kit:
remote: |/usr/bin/distrobox enter --name adv-br -- p11-kit remote /usr/lib/libaetpkss.so
O porte dessa parte foi trocar o comando:
remote: |flatpak run --command=adv-br-pkcs11 io.github.llawli.AdvBr /caminho/do/driver.so
E funciona, porque o flatpak run tem a mesma propriedade que o distrobox enter: é transparente para stdin/stdout, então o RPC do p11-kit atravessa o pipe sem sujeira. Não precisa de p11-kit server, de socket em xdg-run, nem de unidade systemd: o p11-kit do host sobe o processo sob demanda e ele morre com o consumidor. O native messaging dos assinadores (Lacuna Web PKI, WebSigner) atravessa pelo mesmo motivo: o protocolo é stdin/stdout, e o wrapper que o manifesto aponta troca distrobox enter por flatpak run --command=.
A lição que eu levo disso: quando a integração inteira se resume a "arquivo de configuração + comando que fala um protocolo em stdin/stdout", o container vira detalhe de implementação. Distrobox, Flatpak, e provavelmente qualquer coisa que execute um processo com pipes limpos.
Se fosse só isso, não haveria post. O resto é onde o Flatpak deixou de ser um distrobox com outro nome.
2. O Flatpak escreve dentro do seu sandbox, e não deixa você apagar
Todo sandbox Flatpak recebe um /etc/pkcs11/pkcs11.conf com user-config: none (a string mora no binário do flatpak, não no runtime, então vale para qualquer app e qualquer distribuição). E recebe também um /etc/pkcs11/modules/p11-kit-trust.module. O /etc do sandbox é um tmpfs gravável, então a intuição diz que dá para remover o que incomoda. Não dá: esses dois arquivos são binds somente-leitura em cima do tmpfs. rm devolve "Dispositivo ou recurso está ocupado" e sobrescrever devolve "Sistema de arquivos somente para leitura".
Isso teve uma consequência de projeto real. O jeito preguiçoso de exportar os drivers seria um processo só, servindo o p11-kit-proxy.so do sandbox, que agrega tudo. Mas "tudo" incluiria o módulo de trust que o Flatpak injetou e que não sai: o host receberia de volta, como se fossem tokens, as âncoras de confiança "System Trust" e "Default Trust" que ele já tem. Por isso a ponte exporta um processo por driver, o que de quebra isola o driver que derruba o processo que o carregou (e existe driver que derruba: o do SafeNet dá SIGSEGV no encerramento, dentro de uma thread que ele mesmo criou).
O outro lado dessa moeda me surpreendeu mais: a raiz do sandbox é gravável. /usr pertence ao runtime e é somente-leitura, mas / é tmpfs onde o processo pode escrever. Duas coisas do projeto existem por causa disso:
- as extensões de navegador (Lacuna, WebSigner) pedem que a pessoa digite o caminho de um driver, e as opções prontas apontam para
/usr/lib, onde um Flatpak não pode instalar nada. A saída foi o lançador criar, a cada execução, um/pkcs11/adv-br.sona raiz do sandbox: um caminho curto, digitável, que responde por todos os drivers instalados, inclusive os que forem instalados depois; - o Certisign WebSigner abre
/opt/certisign-websigner/res/*.gladepor caminho absoluto gravado no binário.mkdir -p /optdentro do sandbox funciona, e um symlink para dentro de/appresolve sem remendar string em binário.
3. A lição do JRE: a imagem não é o lugar do que é opcional
Essa é a lição que eu mais quero contar, porque ela mudou como eu penso pacote.
O PJeOffice Pro, o assinador do CNJ para o PJe, é um programa Java. O .deb dele não traz JVM nenhuma; ele precisa de um Java 11, e um JRE são quase 300 MB. A tentação de quem empacota é óbvia: builda o JRE na imagem e pronto, todo mundo que instalar o pacote tem tudo funcionando.
Só que a maioria de quem usa certificado digital nunca vai abrir o PJe. Médico assinando receituário, contador com e-CNPJ, gente autenticando no gov.br: nenhum deles deveria baixar 300 MB de Java para descobrir que não precisava. E o mesmo raciocínio vale para cada driver e cada assinador, por um segundo motivo que se soma ao tamanho: licença. SafeSign, SafeNet, SerproID e os assinadores permitem ao licenciado usar e guardar cópia, não redistribuir. Embutir qualquer um deles na imagem seria redistribuição.
A resposta do Flatpak para isso são as extensões: pacotes que se montam dentro do sandbox de outro. O pacote base do projeto ficou com o que todo mundo usa (OpenSC e a ponte para o host) e cada coisa opcional virou uma extensão que só é construída, e baixada da URL do próprio fabricante, na máquina de quem pediu. O JRE não é buildado na imagem de ninguém: ele entra como parte da extensão do PJeOffice, e só existe no disco de quem instalou --with-pjeoffice.
O número justifica sozinho: o pacote base saiu de 597 MB para 4,2 MB. E ganhou de graça a instalação incremental: dá para instalar um driver hoje e um assinador amanhã, com o mesmo comando, sem refazer nada.
O que a documentação não conta sobre extensões, e o porte me ensinou:
- O Flatpak não exporta arquivos de extensão. O
.desktopque uma extensão instala nunca vira atalho de menu: o Flatpak exporta o que estava no aplicativo no momento do build dele, e extensão instalada depois não passa por lá. O projeto precisou de um passo de publicação que lê o atalho e o ícone de dentro do sandbox e os escreve em~/.local/share. - O que uma extensão instala fora do próprio prefixo não é coletado. O
install.shda extensão openjdk11 do SDK copia o JRE para/app/jre, um caminho fixo, e esse conteúdo simplesmente some do pacote final. Tive que replicar o que ele faz, com o destino dentro do prefixo da extensão. - Extensão de um app pode ser montada por outro. O Flatpak não exige que o prefixo de uma extensão seja o id de quem a declara: qualquer aplicativo que escreva o
add-extensionscerto recebe as extensões instaladas na máquina. Verifiquei montando os drivers deste projeto dentro do PJeOffice de outro empacotador, e op11-kit list-modulesde lá listou tudo. Um driver proprietário instalado uma vez servindo a vários Flatpaks. O cuidado éautodelete: falsedo lado de quem consome, senão desinstalar um app leva junto um driver que os outros usam.
4. A mesma lição, aprendida de novo: extensão exige builder
A história do JRE tem um segundo capítulo. A versão de linha de comando constrói as extensões na máquina de quem instala, e isso exige flatpak-builder, o SDK do GNOME e o SDK de Java: mais de 1 GB de cache para um pacote de poucos megabytes. Para mim, ok. Para o advogado que só quer assinar petição, isso é o problema de novo, com outra roupa.
Quando fui fazer a versão com interface gráfica (instalável de um repositório Flatpak, sem terminal), a extensão deixou de servir: uma janela que constrói extensões precisaria da permissão org.freedesktop.Flatpak, que é literalmente "pode rodar qualquer comando fora da caixa". Pedir essa permissão para instalar um driver é matar o propósito do sandbox.
O que resolveu foi um degrau abaixo na stack: os componentes viraram dados do aplicativo. A janela baixa o pacote do fabricante, confere o sha256 e extrai para ~/.var/app/<id>/data/componentes/. Antes de escolher esse caminho eu medi os dois fatos de que ele depende: um .so colocado nos dados do aplicativo carrega dentro do sandbox, e um binário colocado lá executa. E a JVM virou só mais uma fonte do componente PJeOffice: o assinador vem do pacote do CNJ, a máquina virtual vem da Azul (Zulu 11, GPLv2 com classpath exception, a mesma linha que o lançador oficial do CNJ usa). 155 MB baixados, 382 MB em disco, só para quem clicou em instalar.
A lição, nas duas versões dela: o lugar do que é opcional nunca é a imagem. Na linha de comando isso se escreve "extensão"; na distribuição binária se escreve "componente baixado sob demanda"; o princípio é o mesmo, e o JRE foi quem me ensinou duas vezes.
5. O pior modo de falha que eu já depurei: tudo funciona, menos o último passo
O runtime do Flatpak traz o p11-kit da série 0.26. Debian trixie e Ubuntu 24.04 trazem a 0.25. O que trafega no pipe da ponte é a tabela de funções PKCS#11 serializada, e as duas séries discordam sobre ela (a 0.26.0 atualizou os headers para o PKCS#11 3.2).
O modo de falha é cruel: nada recusa a conexão. Os slots enumeram, o token aparece, o PIN é aceito, a lista de certificados vem inteira. E toda assinatura falha com CKR_DEVICE_ERROR. Pior: autenticar por certificado também assina (no CertificateVerify do handshake TLS), então o login no Projudi e no eproc para de funcionar com o certificado aparecendo normalmente na tela. Cada peça parece certa até o último passo, que é exatamente onde a pessoa está com o prazo correndo.
O Flatpak não escolhe runtime conforme o host, e o runtime não vai voltar de série. A saída foi o instalador comparar as duas séries antes de construir e, quando divergem, compilar dentro do pacote um p11-kit da série do host, isolado, que só o processo da ponte usa. Dois detalhes que custaram uma rodada cada:
- o campo
library-versiondop11-kit list-modulesé a versão reportada pelo módulo carregado, não pela biblioteca do processo que pergunta. Perguntar ao módulo errado devolve a resposta certa do lado errado, que é o engano perfeito; - o soname não serve para distinguir série: é
0.4.1na 0.25 e0.4.10/0.4.11na 0.26, o que ordena errado como texto, e as séries 0.23 e 0.24 compartilham o mesmo soname.
6. Medir em vez de deduzir
Um apanhado do que só apareceu executando, cada item com o sintoma enganoso na frente:
- "O assinador não respondeu" pode ser Wayland. Lacuna e WebSigner são .NET com Avalonia, que só tem backend X11.
--socket=fallback-x11entrega o socket só quando não há Wayland, que é exatamente quando eles não precisariam dele; morre comXOpenDisplay failedantes de ler a primeira mensagem. Tem que ser--socket=x11. - "O navegador demora um minuto para abrir" pode ser um driver que você não usa. Com o driver SafeNet instalado e sem token SafeNet espetado,
C_GetSlotListleva mais de 60 segundos dentro do sandbox (medi as condições uma a uma: é a conversa com o pcscd pelo socket, e acontece igual em outro Flatpak que não é o meu). Instale só o driver do seu token. lddverde não significa que a biblioteca carrega. A lib do SerproID usa símbolos dalibgcc_se não a declara emDT_NEEDED; olddlista o que falta entre as dependências declaradas, então fica feliz. Quem descobre é odlopen, na hora de assinar, com o token na mão. E se outra coisa já trouxe alibgcc_spara o processo, funciona por acidente, e o mesmo pacote passa numa máquina e falha na outra.patchelf --add-neededno build, e o build passou a carregar cada biblioteca, não só a rodarldd.- Eu mesmo errei um diagnóstico por generalizar de uma amostra. Vi um teste travar 150 segundos num caminho e recomendei trocar pelo outro. Medindo de novo: o caminho "lento" respondeu em 3,5 s e o "rápido" em 27 s. O que variava era contenção da leitora (o p11-kit mantém um processo por módulo e por cliente, e cinco deles disputam o cartão), não o caminho. Número colhido uma vez, num sistema com contenção, é anedota com cara de medição.
- O bug que só o uso real achou: o manifesto de native messaging escrito para o Brave estava no formato do Firefox (
allowed_extensionsem vez deallowed_origins). O navegador ignora o arquivo em silêncio, e a extensão conclui que o assinador não está instalado. A causa era umlocalde bash sombreando a variável do laço de fora. Todos os meus testes passavam; a primeira pessoa de fora caiu nisso em minutos.
7. Licença é decisão de arquitetura
"Nada proprietário é redistribuído" parece uma frase de rodapé e na prática desenhou o projeto inteiro: é por isso que cada build baixa da URL do fabricante na máquina de quem instala, que o JRE é extensão, que existe repositório próprio só para o que é livre (o aplicativo, GPL-3.0, e os p11-kit de compatibilidade, BSD-3-Clause). Ter um servidor não muda nada: quem dita é a licença de cada binário, não onde ele estaria hospedado.
E baixar do fabricante tem seu próprio folclore. O servidor da Softplan entrega uma cadeia TLS incompleta (repete o certificado do servidor no lugar do intermediário), e a solução que circula é curl -k, que desliga a verificação inteira. A alternativa é completar a cadeia: um --cacert com o intermediário que falta, emitido por uma raiz que o sistema já confia, e sha256sum -c em seguida. De quebra descobri, testando, que o downloader do flatpak-builder ignora SSL_CERT_FILE e CURL_CA_BUNDLE, então esse é o único módulo que baixa dentro do build.
8. Onde isso foi parar
O porte virou o flatpak-adv-br: drivers e assinadores num Flatpak, usados pelo Firefox, Chrome, Brave ou Vivaldi que a pessoa já tem, inclusive em Flatpak, e pelo Papers para assinar PDF. Dois caminhos: ./instalar.sh para quem quer construir na própria máquina e escolher cada peça, ou um aplicativo com janela, instalado de repositório próprio, para quem não quer ver terminal.
O estado honesto: o SafeSign (token GD Burti) está funcionando ponta a ponta, com testadores reais assinando de verdade. SafeNet e SerproID estão implementados e não testados, porque nenhum testador usa esses tokens, e prontidão não é o mesmo que funcionar na mão de alguém. E o VIDaaS (certificado em nuvem da Valid, que não tem cliente Linux oficial) está em desenvolvimento: a chave mora num HSM na nuvem e toda assinatura é uma chamada de API aprovada no celular, então não há nada de específico de Windows no que importa; falta gente com certificado VIDaaS de verdade para exercitar os três passos finais.
Como no anterior, a fundação é o garimpo de versões e remendos do Pedro HQB no distrobox-adv-br: empacotar é fácil quando alguém já respondeu o que empacotar.
Se você tem um token A3 na gaveta, um SafeNet, um SerproID ou um VIDaaS, o projeto tem um diagnostico.sh e um botão de relatar problema (que mostra o texto sanitizado antes de enviar) feitos exatamente para transformar "não funcionou" num relato útil.
E duas perguntas para quem já esteve num porte parecido. Quem empacota para Flatpak: como você lida com o que é opcional e grande, extensão, componente baixado, ou embute e aceita o tamanho? E quem já portou algo entre distrobox, Flatpak, Snap ou Nix: qual armadilha te custou mais horas e não está nesta lista?