← Blog

· 17 min de leitura #rust#mqtt#iot#unikernel#open source#security

urMQTT v0.3.0: cifragem do disco, um arranque que recusa uma imagem alterada e aquilo que estragámos nós próprios

O que aconteceu depois do terceiro texto sobre o urMQTT: cifragem do disco em repouso com um custo medido, um teste que altera um byte no kernel e mostra que o carregador o recusa, um pacote de auditoria com 889 pontos unsafe justificados, os erros que encontrámos nós próprios (e uma afirmação que escrevemos mal), e a versão v0.3.0, com a lista do que ainda não está demonstrado, incluindo um CI vermelho.

O terceiro texto, urMQTT depois do VPS, terminava com uma lista "O que se segue": um broker de demonstração público (D1), uma segunda ronda sobre o fluxo único lento, o caminho em texto simples contra o Mosquitto no VPS e, "depois da versão", bare metal, IOMMU, Secure Boot e cifragem do disco. Este texto diz o que disso foi feito, o que não foi, e o que encontrámos pelo caminho. A cifragem do disco e uma parte do Secure Boot entraram, ainda assim, antes da versão, porque estavam no caminho para a auditoria. A segunda ronda sobre o fluxo único e o caminho em texto simples contra o Mosquitto não foram feitos.

O aviso prévio mantém-se: o projeto está em fase pré-alfa, a versão destina-se a VPS com KVM, e os números medem código, testes e experiências concretas, não maturidade em produção.

Como foi ao longo do tempo

Desde o último texto (1 de novembro, 20:41) até hoje (3 de novembro, 02:43), o ramo principal tem 37 commits novos, 215 no total. Como antes, o projeto é construído com o Claude Code (os commits trazem Co-Authored-By: Claude), e desta vez três ramos avançaram em paralelo (D1, H8, preparação da versão), pelo que a ordem dos commits não é estritamente a ordem do trabalho. Os números são calculados com o mesmo método do último texto (todos os ficheiros .rs fora de vendor/ e fuzz/, funções #[test], títulos ## ADR- em docs/DECISIONS.md) e foram verificados no commit antigo, que dá os mesmos números de então.

Momento Commit Hora Linhas de Rust Testes ADR Commits
último texto d21e3ec 1 de novembro, 20:41 112 408 926 126 178
hoje f1a7fbc 3 de novembro, 02:43 115 148 962 133 215

O crescimento do código é modesto (+2740 linhas, +36 testes), porque desta vez não houve muito código novo: a maior parte do trabalho são testes, scripts, documentação e verificações. Os scripts e o JavaScript não entram no número de linhas.

Cifragem do disco em repouso (S7)

O que planeámos. Cifragem opcional da partição de dados, desligada por omissão, com um custo medido. O que construímos (ADR-240):

  • Uma camada por baixo do disco. CryptDev envolve o dispositivo de blocos por baixo de Disk, pelo que a disposição, a tabela de tamanhos, o SQLite, a telemetria e as cópias de segurança trabalham sobre texto simples e nada sabem da cifragem. O algoritmo é AES-256-XTS (IEEE 1619), um setor de 512 bytes por unidade de dados, e não um bloco de 4 KiB como no plano: o armazenamento escreve em setores, e uma unidade de 4 KiB transformaria cada escrita de 512 bytes numa leitura-modificação-escrita que um corte de energia pode partir ao meio.
  • O superbloco fica em claro. Os dois slots do superbloco levam uma marca e uma verificação da chave (HMAC-SHA-256 de uma etiqueta e do UUID do disco), nunca a chave. Tudo o resto na partição está cifrado. Um setor que no suporte é todo zeros lê-se como zeros e não é decifrado, pelo que discard, o espaço nunca escrito e o crescimento do disco funcionam como antes.
  • Configuração. storage.encryption = "aes-256-xts" e storage.disk_key (um ficheiro com 64 bytes de chave). O formato do blob passa a ser 4 só quando a cifragem está ligada, pelo que todas as outras imagens continuam a arrancar em kernels mais antigos. Um leitor mais antigo recusa a marca desconhecida no superbloco.
  • Sem AES por hardware: recusa. O kernel recusa storage.encryption num processador sem instruções AES (ou quando [cpu] disable as desliga), em vez de executar em silêncio uma versão lenta por software.
  • A conversão é offline. O kernel nunca cifra nem decifra um disco no arranque. urmqtt-disk encrypt, decrypt e rekey fazem-no com a VM desligada: o registo de conversão é escrito primeiro e apagado por último (enquanto existir, o kernel recusa o disco), os blocos de 1 MiB são primeiro guardados num diário no anfitrião, depois escreve-se a cópia convertida, e o superbloco é trocado por último. Testado com um corte de energia após cada escrita individual nos três procedimentos, limpo e a meio da escrita, com a cache descartada e mantida: sempre um disco válido com todos os dados depois de --resume.

Medido. Débito do XTS por núcleo em Apple silicon (um MiB de setores num ciclo): 3109 MB/s a cifrar e 3103 MB/s a decifrar com AES por hardware, contra 252 e 258 MB/s por software. É por isso que não permitimos software. db-bench numa VM (aarch64, HVF, 5000 inserções em 50 transações, cinco execuções de cada variante, alternadas):

ops/s, cinco execuções mediana
cifragem desligada 49 239; 47 401; 50 802; 53 417; 46 531 49 239
cifragem ligada 57 790; 46 173; 45 939; 52 787; 49 632 49 632

A diferença das medianas é +0,8 %, e a dispersão dentro de uma mesma variante (de 46 000 a 58 000) é maior do que isso, pelo que o custo não é mensurável neste teste. É também o que esperamos: toda a execução do db-bench escreve cerca de 6 MB, o que são cerca de 2 ms de AES, e um commit espera pelo flush, não pela cifra.

A entrada de diagnóstico mostra agora também AES-XTS ao lado de AES-GCM (no HVF o kernel mede cerca de 800 MiB/s para XTS contra cerca de 560 MiB/s para AES-GCM) e indica se o AES por hardware está disponível.

A entrada de diagnóstico iniciada no emulador web em aldo.tech, num separador do navegador: no painel CRYPTO a última linha mostra AES-XTS e "hw AES". Os números descrevem o emulador (6 MiB/s), não o urMQTT.
A entrada de diagnóstico iniciada no emulador web em aldo.tech, num separador do navegador: no painel CRYPTO a última linha mostra AES-XTS e "hw AES". Os números descrevem o emulador (6 MiB/s), não o urMQTT.

O que o XTS não dá. Não há autenticação: um setor cifrado alterado decifra-se como ruído em blocos de 16 bytes (os CRC e o scrubber detetam-no), e quem puder escrever no volume pode trocar ou repor setores inteiros entre dois instantâneos. Protege um volume ou instantâneo roubado, não uma ISO roubada, porque a chave está na configuração da imagem. O que continua visível: o GPT, o superbloco, o UUID e quais os setores que alguma vez foram escritos.

O que encontrámos pelo caminho no nosso próprio trabalho. Ao escrever o capítulo sobre criptografia, encontrámos duas coisas. O passo que multiplica o tweak do XTS tinha um ramo que dependia de um bit derivado da chave; agora não tem ramos. E a afirmação "verificado com o vetor 10 do IEEE 1619" cobria apenas o primeiro bloco cifrado; agora existe também uma implementação de referência ingénua (aritmética u128, bloco a bloco) que verifica setores inteiros nos dois sentidos com 200 chaves e setores aleatórios, e a documentação diz o que foi realmente verificado.

Um arranque que recusa uma imagem alterada (H8)

O requisito da lista de auditoria era simples e, até agora, não verificado: nenhum teste mostrava que um kernel alterado não arranca. O pin do hash em limine.conf existia, mas sem prova.

  • Um teste que altera um byte. scripts/tamper.py inverte um bit em cada cópia do kernel (ou do módulo de configuração) dentro de uma ISO construída ou de uma imagem raw, e SMOKE_ONLY=integrity mostra que o carregador recusa: 24 verificações passaram, em aarch64 UEFI, x86_64 UEFI e x86_64 BIOS, com controlos que arrancam. A mensagem de recusa é lida da porta série através de um interruptor só para testes. Em BIOS a mensagem chega apenas ao ecrã, pelo que esse teste afirma apenas que a máquina não arranca.
  • A configuração do carregador registada (ADR-250). limine enroll-config funciona em BOOTAA64.EFI, BOOTX64.EFI e na fase BIOS, pelo que também um limine.conf alterado (com uma entrada de menu ou um argumento acrescentados) é recusado. urmqtt-config pack --iso volta a registar.
  • O caminho do Secure Boot (ADR-251). scripts/sign-loaders.sh (chaves de teste, assinatura, verificação, um armazenamento de variáveis OVMF), mkiso.sh --sb-key e o grupo secureboot em x86_64: uma ISO assinada arranca e constrói-se de forma reprodutível, enquanto uma não assinada, uma assinada com a chave errada e uma alterada depois de assinada recebem Access Denied (10 verificações passaram). O Limine continua a recusar um kernel alterado sob Secure Boot.
  • As regras da entrada do instalador (ADR-253). O plano falava de uma palavra na linha de comandos, install. O kernel de produção aceita na realidade três (install, live, diag), e só install escreve. O teste (9 verificações) executa-o em três máquinas com um disco em branco cujo hash não pode mudar.
  • Segredo da imagem (ADR-252). Um aviso ruidoso quando uma imagem de produção é construída fora do CI, e uma análise da imagem, dos registos de construção e dos registos série à procura de bytes de chave em muitas codificações (11 verificações).
  • TPM, apenas ADR-254. O que significaria selar a chave do servidor num vTPM ou TPM 2.0. Recomendação: não construir para uma versão centrada em VPS. Não verificámos quais os fornecedores que oferecem sequer um vTPM.

O que isto NÃO significa (registado como H8-W1 a H8-W6 em AUDIT.md): um disco instalado não arranca sob Secure Boot porque o seu módulo de configuração não tem hash (ADR-217); uma ISO assinada não pode ser configurada depois (primeiro configurar, depois assinar); o caminho BIOS não tem assinatura; não há proteção contra o regresso a uma versão mais antiga; em aarch64 o firmware daqui não aplica o Secure Boot, pelo que só foi verificada a assinatura do carregador; não há chave do projeto, pelo que as imagens da versão não estão assinadas para Secure Boot; e nenhuma máquina física foi experimentada.

O pacote de auditoria (H10)

docs/AUDIT.md é agora um pacote que um auditor externo pode usar sem perguntas: o sistema numa página, a base em que confiamos (89 612 linhas de Rust próprio na imagem, 67 pacotes de terceiros ligados), as entradas e os seus analisadores, a matriz de proteções com os respetivos testes (e as proteções sem testes), as fraquezas com o seu estado, uma tabela de cada ciclo e alocação alcançáveis pela rede, com o seu limite e o seu teste, instruções para construir e testar, um âmbito proposto para uma auditoria externa em dez áreas e uma lista de red team com 31 ataques e os seus resultados. SECURITY.md tem uma tabela STRIDE por componente.

  • Uma correção ao primeiro texto. O texto de 29 de outubro diz que cada bloco unsafe tem um comentário // SAFETY: verificado por um lint. Não era verdade: o lint (deny(unsafe_code)) apenas limita onde o unsafe pode estar, e os comentários são acompanhados desde H0 por um inventário gerado, que já então encontrou cerca de 30 pontos sem justificação e, na auditoria, 108 (porque entretanto tinham surgido novos tipos de pontos, por exemplo os atributos #[unsafe(no_mangle)] e link_section). É por isso que é o inventário, e não o lint, que agora os mantém a zero.
  • 889 pontos unsafe, cada um com uma justificação escrita. Os 108 que não tinham nenhuma (sobretudo #[unsafe(no_mangle)] e link_section, alguns unsafe fn) receberam um comentário (escreveu-o um agente de IA, apenas comentários, e eu revi uma amostra); o inventário gerado no CI regista cada ponto novo sem justificação. Uma justificação é uma afirmação a rever, não uma prova, e esse é o ponto 5 do âmbito proposto.
  • Cinco achados abertos continuam abertos, registados: não há fuzzing dos analisadores de PCI e virtio, não há um teste que escreva nas tabelas de páginas depois de bloqueadas, o tempo de execução de P-256, X25519 e Ed25519 não foi medido, não há fuzzing de ficheiros SQLite deliberadamente construídos (contido por H6), e 53 dos 67 pacotes ligados são exceções do cargo-vet, não auditorias.

Erros que encontrámos nós próprios

  • Os segredos de tráfego TLS não eram apagados (H0-W2, aberto desde o primeiro dia da auditoria): a tabela de segredos capturados era um Vec que remove e swap_remove deslocavam, deixando cópias. Agora a tabela funciona por slots: um slot é apagado no próprio sítio quando é retirado, descartado ou expulso, e nada se desloca.
  • Um ramo dependente da chave no XTS (acima).
  • log-slow-flood em x86_64 falhava ("log ring full"), também com o código-fonte da v0.2.1. A causa não era um defeito, mas o dimensionamento do teste: o cenário fazia cada núcleo trabalhar a 25 ms, enquanto o ADR-090 o tinha medido a 50 ms. Em x86_64 o COM1 é E/S por porta intercetada byte a byte, um núcleo que escreve nos destinos precisa de cerca de 9 ms por linha, e três núcleos a 25 ms pedem mais de um segundo de trabalho por segundo, pelo que os anéis enchem (o transbordo previsto). O teste corre agora a 50 ms em x86_64 (passou, 30 de 30 na execução que inclui os cenários de base); em aarch64 fica nos 25 ms.
  • A fusão dos ramos estragou os modelos antigos. O novo registo da configuração em H8 exigia um marcador final em limine.conf, que os modelos anteriores a H8 não têm, pelo que urmqtt-config pack --iso e a página do construtor recusavam todas as versões anteriores a H8, incluindo as v0.2.x que a página oferece. Apanharam-no os testes do construtor no navegador (falharam 4 de 100), não o teste do próprio H8. Corrigido (um modelo sem o marcador é configurado como antes) e acrescentado um teste que falha com o erro original quando se retira a correção.
  • A nossa documentação afirmava erradamente que o x86_64 só tinha sido verificado num emulador. O X11 executou a imagem de produção num VPS KVM real da Contabo (4 vCPU AMD EPYC) com TLS mútuo sob carga. Corrigido em todo o lado: o x86_64 foi executado num VPS KVM real no commit de H7 e num emulador, mas o código acrescentado depois (cifragem, integridade do arranque) não correu em KVM, e não foi experimentado em hardware físico.

D1 e a oferta "o seu serviço como unikernel"

O broker de demonstração público está construído, mas não publicado: a configuração (modo live, ACL demo/{id}/#, MQTT em texto simples apenas para a ponte), um script que cria uma CA de teste publicada e certificados de demonstração, uma ponte WebSocket para TCP (apenas a biblioteca padrão do Python, limites de ligações, de taxa e de tamanho, verificação da origem), uma página "Experimente o urMQTT ao vivo" com um cliente MQTT em JavaScript simples, um runbook e um modelo de ameaças. Testado em loopback e em QEMU (18 cenários, 26 testes da ponte). Não testado: um VPS real, DNS, Caddy, a imagem de contentor. Precisa de um VPS separado e de aprovação explícita para a exposição pública.

Além disso, o site tem agora uma oferta, Unikernel (15 línguas, sem preços): adaptar o seu serviço para que funcione como unikernel (avaliação de viabilidade, adaptação, piloto, suporte), com o urMQTT como referência. O texto indica expressamente os limites da base atual: IPv4, TCP apenas como servidor, KVM, Rust como escolha natural, C apenas como trabalhador isolado.

A versão v0.3.0

A etiqueta v0.3.0 foi colocada a 3 de novembro no commit f1a7fbc, e o workflow da versão teve êxito. A versão está marcada como pre-release e contém o kernel, a ISO e a imagem de desenvolvimento para aarch64, a ISO modelo (kernels para aarch64 e x86_64 e uma configuração vazia, que urmqtt-config pack --iso e a página do construtor transformam na sua imagem), um SBOM CycloneDX, SHA256SUMS e a sua assinatura minisign, e atestações de proveniência SLSA. Verificado após a publicação, num diretório vazio: a assinatura de SHA256SUMS é válida, os cinco ficheiros coincidem com as somas de verificação, ambas as atestações (o kernel, e a ISO com o predicado CycloneDX) passam, o SBOM tem 221 componentes e o modelo configura-se (pack --iso volta a registar seis cópias do carregador). A chave de assinatura continua a ser uma chave de teste.

A ordem foi importante e vale a pena registá-la: a página do construtor foi atualizada antes da etiqueta (conhece o formato 4 e todos os modelos mais antigos), e só então o espelho em aldo.tech recolheu a versão. O espelho listou-a assinada e verificada (ok, formato de configuração 4, o sha256 do modelo igual ao de SHA256SUMS), e a página em produção oferece-a como versão predefinida e constrói uma ISO no navegador em cerca de 20 segundos. As notas da versão dizem também o que raramente se escreve: o CI no GitHub não está verde (ver abaixo), o suporte é só para a última versão (a v0.2.1 e anteriores deixam de o ter), e a auditoria foi feita pelo próprio projeto.

No site, quase nos prejudicámos a nós próprios pelo caminho: o script de publicação do site (publish.sh) executa rsync --delete, e o espelho das versões pertence a outro utilizador, pelo que cada publicação terminava com o erro 23 (e teria estragado o botão "Publicar" na administração do blogue). Corrigido com uma exclusão para /urmqtt/releases/. Um achado semelhante: deploy.sh com --delete apaga a página do construtor do gerador, pelo que tem de ser reposta antes de publicar.

O que ainda não está demonstrado

  • O CI no GitHub está vermelho. Não só hoje: a última execução do CI completo antes da etiqueta teve sete tarefas falhadas, e as execuções para trás até 31 de outubro são na maioria vermelhas. As que falharam: lint (avisos do shellcheck nos scripts), interoperabilidade TLS (todos os casos do wolfSSL), a cadeia de fornecimento (a lista de exceções vet-exemptions.md não está atualizada em Linux), fuzz smoke (nessa tarefa faltam os certificados de desenvolvimento), os sanitizers (o LeakSanitizer reporta uma fuga nos testes), os testes em Linux aarch64 (um teste da ferramenta falha com "Broken pipe") e uma prova Kani para a roda de temporizadores (timer wheel) não passa. Ainda não separámos quais destas falhas são do ambiente e quais são defeitos reais; até o fazermos, não afirmamos que a versão passou no CI.
  • A chave de assinatura é uma chave de teste, e as imagens da versão são compilações de desenvolvimento com certificados de teste (exceto o modelo).
  • As traduções das páginas e deste texto para as outras 13 línguas foram escritas por IA e não foram verificadas por falantes nativos.
  • A cifragem do disco não foi medida em x86_64 (nem em KVM nem em hardware físico), e também não foram medidos o débito da telemetria nem o tempo de arranque com cifragem. Não está na página do construtor (precisa de um diálogo para a chave).
  • Não há uma auditoria de segurança independente. O âmbito está escrito; não temos auditor.
  • O Secure Boot é parcial (acima), sem uma máquina física.
  • Bare metal, IOMMU, o broker de demonstração público: não feito ou não publicado.

O que se segue

  1. Separar e corrigir o CI (primeiro o ambiente, depois a prova Kani e o teste que falha em Linux aarch64), porque não consideramos concluída uma versão sem um CI verde.
  2. Uma decisão sobre D1 (um VPS separado, DNS, aprovação para a exposição pública).
  3. Fuzzing dos analisadores de PCI e virtio e um teste para as tabelas de páginas bloqueadas (H0-W9, H0-W10).
  4. Um diálogo para a chave na página do construtor, para que a cifragem fique disponível sem a linha de comandos.
  5. Depois da versão: bare metal (X12 a X16) e IOMMU, numa ordem que depende do inventário de hardware do PC de testes.

O código está em github.com/zeljan-alduk/urMQTT; docs/PROGRESS.md continua a ser a única fonte de verdade sobre o que realmente funciona, docs/PERFORMANCE.md sobre o que foi medido e docs/AUDIT.md sobre o que ainda não está demonstrado. Pré-alfa, como antes; relatórios de erros e relatórios de segurança que sigam as instruções de SECURITY.md são bem-vindos.

Comentários

Regras dos comentários

Regras dos comentários

Os comentários servem para partilhar experiências e perguntas sobre os temas do blog. Estas regras mantêm a discussão útil para todos.

  1. Cortesia Sem insultos, ameaças, assédio, discurso de ódio ou discriminação por qualquer motivo. Critique o argumento, não a pessoa.
  2. Dentro do tema Um comentário deve dizer respeito ao artigo ou à discussão que o segue.
  3. Sem publicidade nem spam Sem anúncios, correntes de mensagens ou ligações sem contexto. Ligações e imagens são bem-vindas quando contribuem para a discussão.
  4. Privacidade dos outros Não publique dados pessoais de terceiros: nomes completos de particulares, moradas, telefones, e-mails, fotos sem consentimento.
  5. Direitos de autor Publique apenas conteúdo que possa partilhar. Ao citar, indique a fonte.
  6. Uma só identidade Use uma só alcunha e não se faça passar por outra pessoa.
  7. Confirmação por e-mail Um comentário só é publicado depois de confirmar a ligação enviada por e-mail. O endereço não é mostrado nem usado para mais nada; comentários não confirmados são apagados após 48 horas.
  8. Moderação Comentários que violem as regras são ocultados ou apagados sem aviso, e uma discussão pode ser bloqueada. As decisões dos moderadores são definitivas.
  9. Responsabilidade O autor é responsável pelo conteúdo do seu comentário. Os comentários não refletem as posições da ALDO TECH.
  10. Denúncias Denuncie conteúdo impróprio para info@aldo.tech.