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.
CryptDevenvolve o dispositivo de blocos por baixo deDisk, 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"estorage.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.encryptionnum processador sem instruções AES (ou quando[cpu] disableas 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,decrypterekeyfazem-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.

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.pyinverte 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, eSMOKE_ONLY=integritymostra 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-configfunciona emBOOTAA64.EFI,BOOTX64.EFIe na fase BIOS, pelo que também umlimine.confalterado (com uma entrada de menu ou um argumento acrescentados) é recusado.urmqtt-config pack --isovolta 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-keye o gruposecurebootem 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 recebemAccess 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óinstallescreve. 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
unsafetem um comentário// SAFETY:verificado por um lint. Não era verdade: o lint (deny(unsafe_code)) apenas limita onde ounsafepode 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)]elink_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)]elink_section, algunsunsafe 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
Vecqueremoveeswap_removedeslocavam, 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-floodem 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 queurmqtt-config pack --isoe 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.mdnã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
- 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.
- Uma decisão sobre D1 (um VPS separado, DNS, aprovação para a exposição pública).
- Fuzzing dos analisadores de PCI e virtio e um teste para as tabelas de páginas bloqueadas (H0-W9, H0-W10).
- Um diálogo para a chave na página do construtor, para que a cifragem fique disponível sem a linha de comandos.
- 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