
Apagando segredos em Go
Quando você trabalha com chaves criptográficas, senhas ou outros dados sensíveis nos seus programas, existe um problema que tem ficado na sombra há décadas: mesmo depois que você termina de usar esses dados, eles ainda estão lá na memória. Stack frames, registradores, alocações no heap. Tudo vira uma espécie de registro arqueológico dos seus segredos, esperando alguém com um dump de memória ou um debugger aparecer e desenterrá-los.
O Go vai introduzir um package experimental runtime/secret que tenta resolver esse problema (de um jeito que é realmente prático). É o tipo de feature que faz você pensar “por que demorou tanto?”, até você começar a pensar nos detalhes de implementação e perceber, “ah, foi por isso.”
O problema: memória é uma tatuagem, não um quadro branco
Vamos começar pelo que estamos tentando resolver. Quando você escreve código que manipula dados sensíveis, digamos que você está implementando TLS ou processando números de cartão de crédito, os dados fluem pelo seu programa: eles vivem nos registradores quando a CPU está trabalhando ativamente com eles, na stack como variáveis locais, e no heap quando você aloca memória. Quando você termina de usar, você pode até colocar essas variáveis como zero ou nil e considerar o trabalho feito.
Mas o lance é que isso não apaga os dados de verdade. Quando você reatribui uma variável, você está só dizendo para o programa parar de se preocupar com aquela posição de memória. Os bytes em si? Eles continuam lá, de boa, esperando algo sobrescrever eles. E se alguém conseguir acesso à memória do seu processo, através de um core dump, uma vulnerabilidade de memória, ou simplesmente acesso root, eles podem ir “pescando” esses valores antigos.
Isso importa muito pra forward secrecy. Se você está fazendo key exchange e alguém depois faz dump da memória do seu processo, você não quer que eles consigam reconstruir chaves de sessões antigas. Você quer essas chaves sumidas. Não “tecnicamente desalocadas”, mas “sobrescritas com zeros e impossível de recuperar”, sumidas mesmo.
O pessoal da segurança se preocupa com isso há anos, e com razão. Você pode escrever o código crypto mais à prova de balas do mundo, mas se seus segredos estão lá na memória tipo jornal velho no sótão, você tem um problema.
A solução: secret.Do
O novo package runtime/secret te dá uma função chamada Do que envolve suas operações sensíveis. A ideia básica é:
import "runtime/secret"
secret.Do(func() {
// Manipule seus dados sensíveis aqui
key := deriveEncryptionKey(...)
plaintext := decrypt(ciphertext, key)
process(plaintext)
// Quando essa função retornar, o Go vai apagar tudo
})
Quando secret.Do retorna, o runtime do Go ativamente apaga:
- Todos os registradores que foram usados pela sua função.
- A stack inteira que sua função usou.
- Alocações no heap feitas durante a função (quando o GC perceber que não são mais alcançáveis).
Isso não é aquela abordagem típica de “seta pra zero e torce pro melhor”. O runtime está literalmente passando e limpando esses valores antes de devolver o controle pro seu programa.
Cavando mais fundo
Agora é que a coisa fica interessante. O time do Go não simplesmente espalhou uns memset por aí e considerou o trabalho feito. Eles foram fundo no runtime mesmo.
Quando você chama secret.Do(f), aqui está o que acontece:
- Marca a goroutine como “modo secreto” - O runtime incrementa um contador na sua goroutine pra rastrear que você está manipulando segredos.
- Chama sua função - Bem direto, exceto que está envolvida num handler de panic.
- Apaga a stack - Limpa toda a memória da stack que foi usada pela sua função.
- Apaga os registradores - Zera todos os registradores da CPU que podem ter guardado seus dados.
- Marca alocações no heap - Tageia quaisquer objetos alocados durante a função pra que o GC saiba zerar eles imediatamente quando liberar.
O código assembly pra isso é meio que bonito num jeito “caramba, que completo”. No x86-64, eles estão explicitamente limpando registradores XMM, registradores de propósito geral, tudo. Eles até estão lidando com edge cases tipo o que acontece se sua função secreta for preemptada ou receber um signal.
E sim, antes de você perguntar: se sua função entra em panic, eles ainda assim apagam tudo antes de re-lançar o panic. Eles até modificam o stack trace pra parecer que o panic veio do próprio
secret.Do, removendo qualquer traço do que aconteceu dentro da sua função secreta. Agora isso é compromisso com a causa.
As limitações
Ou: Por que isso não é uma varinha mágica.
Claro, nada na vida é de graça, e certamente nada em programação de sistemas é de graça. O package runtime/secret vem com algumas limitações importantes que você precisa saber. Atualmente só funciona em linux/amd64 e linux/arm64 (me pergunto por quê…). Se você está no macOS ou Windows, ou rodando em alguma outra arquitetura, secret.Do simplesmente chama sua função diretamente e não faz nada de especial. Afinal, É um experimento.
Você não pode lançar uma nova goroutine de dentro do secret.Do. Se tentar, vai ter um panic. Isso faz sentido quando você pensa sobre isso. Como o runtime rastrearia qual memória pertence à sua operação secreta se ela está espalhada por múltiplas goroutines? A complexidade seria um pesadelo. Além disso, se você escrever segredos em variáveis globais, você está por sua conta. O package não pode te ajudar porque, bem, elas são globais. Elas ficam por aí.
Alocações no heap só são apagadas depois que o garbage collector roda e determina que não são mais alcançáveis. Se seu programa está segurando referências (até mesmo acidentalmente), esses segredos vão ficar por aí. O runtime não consegue ler sua mente sobre quando você está realmente terminado com algo. Como a documentação meio que nota ominosamente, “Endereços de ponteiros podem vazar em buffers de dados usados pelo runtime pra fazer garbage collection.” Então se você está codificando segredos nos próprios valores de ponteiro (o que você provavelmente não deveria estar fazendo de qualquer maneira), eles podem vazar.
Lendo essas limitações, você pode estar pensando: “Cara, são muitas ressalvas.” E você estaria certo! Isso é coisa genuinamente difícil. O fato de funcionar de qualquer jeito, e funcionar de forma confiável pros casos que suporta, é bem impressionante. O perfeito é inimigo do bom, e isso é bom o suficiente pra muitos casos de uso do mundo real.
Eu preciso disso?
À primeira vista, você pode pensar: “Com que frequência alguém realmente consegue acesso à memória do meu processo?” E se você está construindo um CRUD que fala com banco de dados, talvez a resposta seja “não muito frequentemente.” Mas no mundo real, vulnerabilidades de disclosure de memória aparecem com uma regularidade alarmante. Lembra do Heartbleed? Ou qualquer um das dúzias de bugs de buffer overflow que são encontrados todo ano?
Especialmente pra código criptográfico, isso importa. Se você está implementando TLS, você quer forward secrecy: mesmo se um atacante depois comprometer seu servidor e fazer dump de toda sua memória, eles não deveriam conseguir descriptografar sessões antigas. Mas se suas chaves efêmeras estão simplesmente lá na memória indefinidamente, você não tem forward secrecy de verdade, tem? Você tem “forward secrecy com asterisco”.
O package runtime/secret te dá uma forma de realmente alcançar as propriedades de segurança que você está alegando ter. Ele transforma “a gente para de usar a chave” em “a gente ativamente destrói a chave.” Essa é uma diferença significativa.
A implementação é a parte interessante
O que eu acho fascinante sobre essa feature é o quão fundo ela vai no runtime. O time do Go não simplesmente adicionou uma nova função de biblioteca; eles modificaram o garbage collector, a lógica de crescimento de stack, signal handling, preempção, até as interações com vDSO (virtual dynamic shared object).
Quando sua função secreta cresce sua stack, o runtime agora limpa a stack antiga antes de desalocar ela. Quando objetos são alocados em modo secreto, eles recebem uma marca especial, e quando são liberados, são imediatamente zerados ao invés de simplesmente serem adicionados à free list. O panic handler foi modificado pra apagar o stack trace de funções secretas.
Esse é o tipo de feature que requer entender o runtime num nível realmente profundo. Uma coisa é dizer “a gente deveria apagar segredos da memória.” Outra coisa completamente diferente é descobrir todos os lugares no runtime onde esses segredos podem vazar e tapar cada um deles.
Olhando o commit, esse trabalho foi inicialmente começado pelo Keith Randall lá em 2023 (CL 600635), depois foi continuado e aprimorado pelo Daniel Morsing, que adicionou suporte ARM64, signal handling, e lidou com vários edge cases. O fato de ter levado dois anos e múltiplos engenheiros experientes de runtime pra fazer isso direito deveria te dizer algo sobre a complexidade envolvida.
Código
Então quando você deveria realmente usar isso? O caso de uso óbvio é operações criptográficas: derivação de chave, criptografia, descriptografia, assinatura. Em qualquer lugar que você esteja manipulando dados que realmente não deveriam ficar na memória.
func decryptPayload(encrypted []byte, keyMaterial []byte) ([]byte, error) {
var plaintext []byte
var err error
secret.Do(func() {
key := deriveKey(keyMaterial)
plaintext, err = decrypt(encrypted, key)
// key é apagada quando isso retornar
})
return plaintext, err
}
Você também pode usar pra manipular senhas, tokens de API, ou outras credenciais. Basicamente em qualquer lugar que você esteja pensando “eu queria que esses dados simplesmente sumissem quando eu terminasse com eles”.
Mas não exagere. Você não precisa envolver toda função em secret.Do. O overhead não é enorme, mas também não é zero, o runtime está fazendo trabalho de verdade pra limpar toda aquela memória. Use onde importa.
Sete anos de trabalho
Essa feature foi solicitada na issue #21865, que foi aberta em setembro de 2017. Sete anos atrás. Foi quanto tempo levou pra ir de “ei, seria legal se o Go pudesse apagar segredos da memória” pra “aqui está uma implementação funcionando que lida com todos os edge cases.”
Algumas pessoas podem olhar pra essa timeline e pensar que o time do Go é lento. Mas eu acho que mostra cautela apropriada. Fazer isso errado seria pior do que não ter, porque as pessoas iriam confiar nisso e ter uma falsa sensação de segurança. Melhor levar o tempo pra fazer direito.
E “direito” nesse caso significa pensar em todas as interações com o resto do runtime, lidar com panics e crescimento de stack e preempção e signals, testar minuciosamente, e documentar as limitações claramente. O fato de ainda estar marcado como experimento (requerendo GOEXPERIMENT=runtimesecret pra habilitar) mostra que eles estão sendo cuidadosos sobre o rollout.
“É o que tem para hoje”
O package runtime/secret não vai resolver todos os seus problemas de segurança. Ele não vai te proteger de SQL injection ou XSS ou uma porção de outras vulnerabilidades. O que ele vai fazer é te dar uma forma prática de garantir que dados sensíveis não fiquem na memória mais tempo do que o necessário.
É o tipo de feature que me faz apreciar a atenção aos detalhes do time de runtime do Go. Eles poderiam ter lançado algo pela metade que só lidasse com os casos fáceis. Ao invés disso, eles construíram algo que realmente funciona, documentaram as limitações honestamente, e fizeram integrar de forma limpa com o resto da linguagem.
Pra quem está construindo sistemas criptográficos, isso vai ser uma ferramenta muito útil. Pra todo mundo mais, é um bom lembrete de que segurança é difícil, e que às vezes a melhor solução é ir fundo no runtime e consertar as coisas no nível da fundação.
E ei, se nada mais, é divertido ler código assembly que é explicitamente projetado pra garantir que seus segredos não possam ser recuperados da memória. Tem algo estranhamente satisfatório em ver todas aquelas instruções XOR que estão ali só pra zerar registradores.
Agora se me dá licença, eu preciso ir envolver meu código de manipulação de senha em secret.Do. Melhor sete anos atrasado do que nunca, né?
Pra usar, compile o Go com GOEXPERIMENT=runtimesecret. Atualmente suportado em linux/amd64 e linux/arm64.
Referências
- Issue: #21865 - proposal: Go 2: allow cryptographic packages to wipe stack memory
- Implementação: Commit a3fb92a710
- Code Review: CL 704615 (implementação final)
- CL Original: CL 600635 (trabalho inicial do Keith Randall)
- Documentação do Package: src/runtime/secret/secret.go
- Exemplos de Teste: src/runtime/secret/secret_test.go