- O Go 1.22 troca a fonte padrão de números aleatórios em
math/randemath/rand/v2por um gerador criptograficamente forte, reduzindo bastante o impacto de erros em casos em que deveria ter sido usadocrypto/rand - O gerador do Go 1 usava um registrador de deslocamento com realimentação linear com estado de 607
uint64, de modo que, ao observar 607 saídas, já era possível reconstruir valores passados e futuros - O PCG-DXSM de
math/rand/v2melhorou a qualidade estatística dos números aleatórios e o tamanho do estado, mas ainda não garante a imprevisibilidade exigida para valores secretos - O novo ChaCha8Rand usa uma semente de 32 bytes, faz rekeying a cada 16 blocos e mantém 300 bytes de estado por núcleo, sendo aplicado em
math/rand/v2, em parte demath/rande nas seeds de hash de maps - O custo de desempenho é limitado: o ChaCha8Rand é mais lento que o gerador do Go 1, mas nunca passa de 2x, e em servidores comuns a diferença não supera 3 ns, então o ganho de segurança compensa na maioria dos programas
Os novos padrões de aleatoriedade no Go 1.22
- O Go 1.22 alterou o padrão de
math/randemath/rand/v2para usar um gerador pseudoaleatório criptograficamente forte - O objetivo é reduzir o dano quando desenvolvedores usam
math/randpor engano em lugares ondecrypto/randseria necessário - As APIs de aleatoriedade do Go tradicionalmente se dividem em duas categorias
math/rand: números aleatórios estatísticos usados em simulação, amostragem, análise numérica, algoritmos aleatórios não criptográficos, fuzzing, embaralhamento e exponential backoffcrypto/rand: números aleatórios criptográficos para casos que exigem imprevisibilidade, como chaves e tokens
Por que números aleatórios estatísticos não bastam
- Geradores estatísticos podem ser suficientes para muitos usos não criptográficos se passarem em testes estatísticos básicos
- Mas, em muitos casos, um observador que conhece o algoritmo consegue prever a sequência seguinte ao ver saídas suficientes
- O
srande oranddo Unix V3 são formas iniciais que influenciaram depois a API de aleatoriedade do C e de várias outras linguagens- Definem o estado a partir de uma única seed inteira
- Calculam o próximo valor com um gerador congruencial linear (LCG)
- Como o estado interno é simples, uma única saída já pode bastar para calcular facilmente valores futuros
- É possível escolher constantes de um LCG para que ele produza cada valor possível uma vez antes de repetir, mas há a fraqueza de que os bits menos significativos se repetem em ciclos curtos
Estrutura e vulnerabilidades do gerador do Go 1
- O gerador
math/randdo Go 1 pertence à família dos registradores de deslocamento com realimentação linear - O estado interno é um slice
veccom 607uint64vec[606]é o “tap”vec[334]é o “feed”- Para gerar o próximo valor, ele soma tap e feed para formar
x, gravaxna posição feed e então o retorna
- A implementação real não move o slice inteiro; ela apenas desloca para trás as posições de tap e feed para reduzir custo
- Gerar o próximo valor exige duas subtrações, duas somas condicionais, dois loads, uma soma e um store
- Como o valor retornado é um elemento do vetor de estado interno, ler 607 saídas expõe todo o estado
- Preenchendo o mesmo
vece executando o algoritmo, é possível prever valores futuros - Executando o algoritmo ao contrário, também é possível reconstruir valores passados
- Preenchendo o mesmo
- O gerador do Go 1 não foi feito para segurança, e a qualidade dos números gerados também depende da inicialização do
vec
O que o PCG melhorou e quais limites permanecem
- O
math/rand/v2adotou o PCG de Melissa O’Neill como gerador estatístico mais moderno - O PCG do Go se baseia em um LCG de 128 bits e usa uma função
scramblepara reduzir o estado de 128 bits a uma saída de 64 bits - Durante a discussão da proposta, o Go adotou um
scramblebaseado em multiplicação a partir de uma sugestão de O’Neill- Essa forma é chamada de PCG-DXSM
- O Numpy também usa essa forma de PCG
- O PCG tem um estado muito menor que o gerador do Go 1
- Gerador do Go 1: 607
uint64 - PCG: dois
uint64
- Gerador do Go 1: 607
- O PCG é menos sensível ao estado inicial e passa em vários testes estatísticos, mas não garante imprevisibilidade
- O PCG-XSL-RR pode ser revertido
- Não seria surpreendente que o PCG-DXSM também pudesse ser revertido
- Para gerar valores secretos, é preciso usar outro gerador, não o PCG
Números aleatórios criptográficos e o papel do sistema operacional
- Números aleatórios criptográficos precisam permanecer efetivamente imprevisíveis mesmo para um observador que conhece o método de geração e já viu muitas saídas anteriores
- Protocolos criptográficos, chaves secretas, o comércio moderno e a privacidade online dependem de números aleatórios criptográficos
- Na prática, o sistema operacional é quem fornece a aleatoriedade
- Ele coleta aleatoriedade de dispositivos físicos, como mouse, teclado, disco e temporização de rede
- Mais recentemente, também usa ruído elétrico medido diretamente pela CPU
- Quando o sistema operacional reúne aleatoriedade suficiente, por exemplo ao menos 256 bits, ele gera sequências longas com hash ou algoritmos criptográficos
- Antigamente, o uso de arquivos de dispositivo como
/dev/randomera comum, mas hoje os sistemas operacionais oferecem syscalls diretas - O
crypto/randdo Go oculta as diferenças entre sistemas operacionais e oferece a mesma interface,rand.Read
Projeto do ChaCha8Rand
- O novo ChaCha8Rand do Go 1.22 é uma forma levemente modificada da cifra de fluxo ChaCha, de Daniel J. Bernstein
- A variante mais conhecida do ChaCha é o ChaCha20, usado também em TLS e SSH
- O artigo Too Much Crypto, de Jean-Philippe Aumasson, considera segura também a variante de 8 rodadas, ChaCha8, que é cerca de 2,5 vezes mais rápida
- O ChaCha8Rand usa o ChaCha8 como
rand.Source, empregando o bloco gerado diretamente como fluxo aleatório em vez de fazer XOR com a entrada- Isso equivale a cifrar ou decifrar dados inteiramente zerados
Mudanças no ChaCha8Rand
- O ChaCha8Rand usa uma seed de 32 bytes como chave do ChaCha8
- O ChaCha8 gera blocos de 64 bytes, tratando o cálculo como 16
uint32 - Implementações comuns conseguem calcular 4 blocos ao mesmo tempo com instruções SIMD, mas para usá-los como entrada de XOR é preciso desfazer os blocos intercalados
- O ChaCha8Rand define esse próprio bloco intercalado como fluxo aleatório, eliminando o custo do unshuffle
- Na etapa final de um bloco do ChaCha8, certos valores são somados a cada
uint32- Metade deles é material de chave e a outra metade são constantes conhecidas
- O ChaCha8Rand deixa de somar novamente as constantes conhecidas, removendo metade da soma final
- A cada 16º bloco gerado, os 32 bytes finais passam a ser usados como chave dos 16 blocos seguintes
- Esse rekeying fornece uma forma de segurança futura
- Mesmo que todo o estado do gerador em memória seja exposto, só é possível recuperar valores desde o último rekeying, não os anteriores
- O Go publicou a especificação C2SP do ChaCha8Rand e casos de teste, permitindo que outras implementações compartilhem a mesma repetibilidade da implementação do Go com a mesma seed
Onde ele é aplicado na biblioteca padrão
- O runtime do Go mantém um estado ChaCha8Rand por núcleo, semeado com aleatoriedade criptográfica fornecida pelo sistema operacional
- O tamanho do estado é de 300 bytes por núcleo
- Em um sistema com 16 núcleos, isso fica em nível parecido ao único estado compartilhado de 4.872 bytes do gerador do Go 1
- O estado por núcleo permite gerar números aleatórios rapidamente sem contenção por locks
- As funções de pacote de
math/rand/v2sempre usam ChaCha8Rand- Ex.:
rand.N,rand.Float64
- Ex.:
- As funções de pacote de
math/randusam ChaCha8Rand quandorand.Seednão foi chamado- Ex.:
rand.Intn,rand.Float64 - Se
rand.Seedfor chamado, é preciso voltar ao gerador do Go 1 por compatibilidade
- Ex.:
- O runtime passou a escolher a seed de hash de novos maps com ChaCha8Rand em vez do gerador anterior baseado em wyrand
- Se um atacante souber a função de hash específica usada pela implementação de map, ele pode preparar entradas para forçar comportamento quadrático
- Usar uma seed por map, e não uma única seed global, também evita outros comportamentos degenerados
- Não está claro se a seed de map realmente exige aleatoriedade criptográfica, mas a troca foi simples e prudente
- Código que precisa de uma instância separada de ChaCha8Rand pode criar diretamente
rand.ChaCha8
Redução do impacto de erros de segurança
- O Go busca reduzir ou eliminar erros comuns com impacto de segurança, ajudando a escrever código seguro por padrão
- Quando o
Readdemath/randfoi marcado como deprecated no Go 1.20, descobriu-se que alguns desenvolvedores estavam usandomath/randem situações que exigiamcrypto/rand, como geração de material de chave - No Go 1.20, esse tipo de erro era um problema grave de segurança
- Era preciso investigar onde a chave foi usada
- Como essa chave pode ter sido exposta
- E se outras saídas aleatórias deram pistas ao atacante para derivá-la
- No Go 1.22, o mesmo erro continua sendo um erro, mas fica menos provável que ele vire uma catástrofe de segurança
- Mesmo assim, ainda é melhor usar
crypto/randpara valores secretos- O kernel do sistema operacional consegue proteger melhor os valores aleatórios
- O kernel continua adicionando nova entropia ao gerador
- A implementação do kernel recebeu mais revisão
Casos que não parecem criptográficos
- Gerar UUIDs aleatórios pode parecer um caso em que
math/randbasta, já que UUID não é um valor secreto - Mas, ao semear
math/randcom a hora atual, computadores diferentes executados no mesmo instante podem produzir o mesmo valor- Em sistemas em que a hora atual só tem precisão de milissegundos, essa possibilidade aumenta
- Mesmo com a semeadura automática baseada em entropia do sistema operacional no Go 1.20, a seed do gerador do Go 1 continua sendo apenas um inteiro de 63 bits
- Um programa que gera um UUID na inicialização fica limitado a 2⁶³ UUIDs iniciais possíveis
- Depois de cerca de 2³¹ UUIDs, colisões passam a ser uma possibilidade
- O ChaCha8Rand do Go 1.22 é semeado com 256 bits de entropia
- O número de primeiros UUIDs possíveis passa a ser 2²⁵⁶
- Não há motivo para se preocupar com colisões
- Balanceamento de carga em que um servidor frontend distribui requisições aleatoriamente entre backends também pode exigir números aleatórios imprevisíveis
- Se um atacante observar a distribuição e conhecer um algoritmo previsível, ele pode concentrar requisições caras em um backend específico
- No gerador do Go 1, isso era raro, mas possível
- No Go 1.22, isso deixa de ser problema
Características de desempenho
- Os ganhos de segurança do ChaCha8Rand têm um custo pequeno, mas o desempenho continua na mesma faixa do gerador do Go 1 e do PCG
- Duas operações foram usadas na comparação
Uint64: retorna o próximouint64do fluxo aleatórioN(1000): retorna um número aleatório no intervalo[0, 1000)
- Em chips x86 de 64 bits, ao compilar com
GOARCH=386e executar em modo 32 bits, o PCG fica mais lento que o ChaCha8Rand por causa da multiplicação de 128 bits- O ChaCha8Rand usa aritmética SIMD de 32 bits
- Em alguns sistemas,
Go 1: Uint64é mais rápido quePCG: Uint64, masGo 1: N(1000)é mais lento quePCG: N(1000)- O
N(1000)do Go 1 usa duas divisões inteiras de 64 bits para reduzir o intervalo - O
N(1000)do PCG e do ChaCha8 usa o algoritmo mais rápido demath/rand/v2, que evita divisões na maioria dos casos
- O
- No geral, o ChaCha8Rand é mais lento que o gerador do Go 1, mas não chega a ser mais de 2 vezes mais lento
- Em servidores comuns, a diferença não passa de 3 ns, e são muito poucos os programas em que isso vira gargalo
Conclusão
- O Go 1.22 aumenta a segurança dos programas sem exigir mudanças no código
- A estratégia central foi fortalecer o próprio
math/randpara reduzir um erro comum: usarmath/randpor engano no lugar decrypto/rand - Há casos parecidos no ecossistema, como o pacote npm
keypair, que tenta gerar pares de chaves RSA comMath.randomdo JavaScript quando a Web Crypto API não está disponível - A segurança de sistemas não pode depender da suposição de que desenvolvedores nunca erram
- O ChaCha8Rand do Go 1.22 mostra que usar um gerador pseudoaleatório criptograficamente forte até para aleatoriedade “matemática” pode oferecer desempenho competitivo em relação a outros geradores
1 comentários
Opiniões do Hacker News
Como mencionado no artigo, o rclone cometeu exatamente esse erro
Ao refatorar um código que usava
Readdecrypto/rand, o import foi alterado automaticamente e, provavelmente ao se misturar com código que usavamath/rand, ogoimportsparece ter mudado paramath/randComo resultado, em vez de um gerador de números aleatórios seguro, passamos a usar um gerador determinístico que o rclone semeava com o horário, e isso não foi percebido no diff :-(
https://www.cvedetails.com/cve/CVE-2020-28924/
Por isso sou muito a favor dessa mudança
goimportsfoi alterado para preferircrypto/rand, então não está claro o que aconteceu durante a refatoraçãoTalvez tenha entrado no mesmo arquivo algum código que usava uma API específica de
math/randhttps://go-review.googlesource.com/24847
De todo modo, é bom ver essa parte sendo arrumada
math/randestava sendo usado. Na prática não estava; era só uma confusão entre vários arquivos, então não foi grave, mas isso mostra como toda essa questão é confusatext/templateehtml/templatesão parecidos. Olhando em retrospecto, esse tipo de ocultação por nome de pacote foi uma má ideia"secure password generation golang", quase todos os exemplos usammath/randPior ainda: todos reinicializam a seed com o horário atual logo antes de gerar a senha
Descobri isso depois de encontrar alguém usando
math/randno nosso código e ficar curioso para saber de onde aquilo tinha sido copiadogoimportstratoumath/rand.Readecrypto/rand.Readcomo casos especiais praticamente desde o inícioMas, olhando para https://github.com/golang/tools/commit/0835c735343e0d8e375f0... de 2016, há menção a um período em que
"rand.Read"podia ser interpretado como"math/rand"Talvez tenha sido nessa época
"PredictableRand"Na semana passada, spacey também publicou em https://news.ycombinator.com/item?id=40237491, mas aquele post parece ter sido enterrado por engano como duplicata de https://news.ycombinator.com/item?id=40224864
Os dois posts do blog go.dev são duas partes da mesma série, mas são bem diferentes. Este texto é sobre algoritmos eficientes de geração de números aleatórios seguros, enquanto o anterior é sobre design de APIs em Go
Russell Cox produz de forma consistente ótimos blogs técnicos, propostas e trabalhos
Se você quer melhorar a clareza da escrita e do pensamento, começar por Russell Cox é um bom ponto de partida
Na época eu nem sabia quem era Russ Cox, mas aquela série de textos era realmente incrível
É provavelmente o material gratuito de maior qualidade sobre implementação de expressões regulares; depois disso vêm vários livros focados em compiladores, mas eles não são gratuitos nem fáceis de encontrar na web
Eu já usei
math/randem um lugar ondecrypto/randera absolutamente necessárioComo resultado, as primeiras versões do dnscrypt-proxy2 usavam chaves estáticas
A causa foi uma extensão do VSCode que adicionava imports automaticamente. Em todos os arquivos-fonte que precisavam de números aleatórios seguros, eu importava
crypto/randmanualmente com cuidado, mas esqueci em um arquivo; tudo compilou e funcionou bem, e não percebi que a extensão tinha adicionado silenciosamente um import demath/randnaquele arquivo específicoDesde então, importo
crypto/randcom o aliascryptorandpara evitar que oranderrado seja importado automaticamenteA propósito, Zig também usa um gerador de números aleatórios baseado em ChaCha8 e, em operações criptográficas, o usuário não pode fornecer seu próprio gerador: um gerador seguro é sempre usado. Para testes, algumas funções recebem uma seed explícita
Para ambientes restritos, a biblioteca padrão também inclui um gerador menor baseado na permutação Ascon e na construção Reverie
Em 2016, o
goimportsfoi alterado para preferircrypto/randem vez demath/rand(https://go-review.googlesource.com/24847), e isso foi antes de existir suporte a VSCode para GoMesmo nos anos 2020, eu pensava com frequência por que as implementações padrão de números aleatórios em várias linguagens de programação ainda usam geradores rápidos como LFSR e MT.
Parece melhor adotar uma postura conservadora, assumindo que as pessoas não sabem se precisam de um gerador de números pseudoaleatórios ou de um gerador de números pseudoaleatórios criptograficamente seguro, mudar o padrão para o segundo e fazer com que apenas quem precisa do primeiro o escolha explicitamente.
Se o desenvolvedor não escolher explicitamente o mecanismo de números aleatórios a ser usado, recebe um gerador criptograficamente seguro.
Agora a parte difícil é convencer as pessoas a migrarem para a nova API. Indo além, nem mesmo é fácil migrar de
mt_rand(), que usa uma instância global deMt19937, pararandom_int(), baseado em CSPRNG e já disponível desde o PHP 7.0.[1] https://www.php.net/releases/8.2/en.php#random_extension
No meu caso de uso havia dezenas de milhares de componentes e, ao fazer profiling, vi que uma parte considerável do tempo de inicialização da estrutura de dados era gasto no
Read()decrypto/rand, que no meu MacBook executava chamadas de sistema.Ao corrigir a biblioteca para usar o
Read()demath/rand, o desempenho melhorou bastante.Além de
math/randser mais rápido, eu também estava preocupado em esgotar sem motivo o pool de entropia do sistema. Nesse caso, a única razão para os IDs precisarem ser aleatórios era permitir adicionar mais componentes depois de serializar/desserializar a estrutura de dados, algo que eu não pretendia fazer.Não sei exatamente como o momento da mudança descrita neste blog se encaixa com a minha experiência. Talvez eu tenha usado uma versão antiga da biblioteca e, se agora
crypto/randé praticamente indistinguível demath/rand, então ótimo :-)O tamanho do estado ainda é relativamente grande (64 bytes contra 16 bytes), mas é muito melhor do que
mt19937ou o PRNG antigo do Go.Se um CSPRNG fosse muito mais lento, como costuma acontecer com CSPRNGs comuns que não são variantes de ChaCha com rodadas reduzidas, ele perderia atratividade como padrão.
Há mais um pequeno fator que empurra as pessoas para PRNGs mesmo quando elas não precisam de seed. As APIs de CSPRNG incluem erros que sempre precisam ser tratados, para o caso de falha em chamada de sistema ou falta de entropia.
Com que frequência uma leitura de
crypto/randrealmente falha? Quanto seria preciso ler em um sistema moderno para esgotar a entropia? Nunca vi falha mesmo em bilhões de requisições, eddtambém funciona bem.Fico me perguntando se, para a maioria dos casos de uso, uma API no estilo
Must/panictalvez fosse o padrão correto.Além disso, olhei o pacote
secretsdo Python (https://docs.python.org/3/library/secrets.html) e não há nenhuma menção de que ele possa lançar exceções. Na prática, isso simplesmente não acontece?Você perde um pouco de desempenho, mas ganha uma garantia muito mais forte de não causar uma catástrofe por usar o gerador de números aleatórios errado.
É uma pena que, em quase todas as linguagens, os desenvolvedores ainda tenham que se preocupar com essa aresta cortante.
Para quem não sabe,
gosece sua extensãogolangci-lintavisam sobre o uso demath/rand.https://github.com/securego/gosec/blob/d3b2359ae29fe344f4df5...
math/rand/v2é poder usá-lo na empresa sem diretivasnolinte sem a discussão de PR que vem depois.Ainda estou interpretando as recomendações sobre segurança e as novas opções da v2.
O post do blog usa frases como “valores secretos exigem outra coisa” e, em seguida, entra em detalhes sobre aleatoriedade criptográfica, ChaCha8 e o fato de ser semeado com números aleatórios do sistema, passando uma impressão muito “segura”.
Mas a documentação do pacote diz isto:
... but it should not be used for security-sensitive work ... This package's outputs might be easily predictable regardless of how it's seeded. For random numbers suitable for security-sensitive work, see the crypto/rand package.Então por que o post do blog parece insinuar o uso de
math/rand/v2para “valores secretos”?Em resumo, significa que tudo que for sensível ainda deve usar
crypto/rand, e que a melhoria descrita aqui é uma rede de segurança para quando alguém usarmath/rand/v2indevidamente?math/rand/v2não é o ideal, mas usá-lo por engano onde se deveria usarcrypto/randjá não vira imediatamente uma falha de segurança fatal.O texto também diz isto:
Ainda é melhor usar
crypto/rand. O kernel do sistema operacional consegue fazer um trabalho melhor ao manter os valores aleatórios em segredo contra vários ataques de espionagem, continua adicionando nova entropia ao gerador e passou por mais revisões. Mas usarmath/randpor engano já não é mais um desastre de segurança.Mesmo no pior benchmark, a nova estratégia é apenas cerca de metade mais lenta do que o gerador de números aleatórios não seguro, e a maioria dos benchmarks ficou muito mais próxima disso
Go consegue equilibrar bem segurança e desempenho para a biblioteca padrão e os apps construídos sobre ela. Seria bom se outros ecossistemas acompanhassem
Se uma aplicação precisar de números aleatórios rápidos e não seguros, deve implementar seu próprio gerador interno
Deixar números aleatórios não seguros ao alcance fácil é uma ferramenta para dar um tiro no próprio pé que pode ser guardada
Incentivar as pessoas a presumirem que uma primitiva
"random"é criptograficamente segura é promover más práticasTornar
math/rand/v2criptograficamente seguro pode resolver um problema, mas agora algo que não parece prometer segurança passa a estar em um estado “ok”Em geral, não existe a convenção de que as funções de
math/randsejam criptograficamente seguras. Se mudarmos isso para fazer código ruim funcionar corretamente por acaso, podemos acabar mascarando quais outros erros estamos cometendo se já estamos cometendo um erro tão óbvioO
math/randdo Go 1 seria mais precisamente chamado de gerador Fibonacci com atraso aditivoA publicação original é o artigo de Green, Smith e Klem
[1] https://doi.org/10.1145/320998.321006
Também conheço https://www.leviathansecurity.com/blog/attacking-gos-lagged-..., e ali também o chamam de gerador Fibonacci com atraso
Rob Pike e eu trocamos e-mails alguns meses atrás com Don Mitchell, que escreveu a versão original em C do gerador do Go 1, perguntando como ele descreveria esse algoritmo, e ele respondeu: “pelo que me lembro, Jim e eu implementamos um gerador parecido com LFSR, do Marsaglia”
Acho que as duas descrições, gerador Fibonacci com atraso e gerador semelhante a LFSR, são corretas a partir de perspectivas diferentes. Qualquer uma serve, mas no texto decidi usar a descrição do autor original
Se eu fosse apontar uma pequena falha, parece que aqui aleatoriedade estatística e gerador de números pseudoaleatórios foram misturados
A definição da Wikipédia para aleatoriedade estatística é: “diz-se que uma sequência de números é estatisticamente aleatória quando não contém padrões ou regularidades reconhecíveis”
Essa definição se aplica a um gerador de números aleatórios verdadeiro (TRNG)? É melhor que sim. Pelo menos no longo prazo ou no limite, deveria. Caso contrário, não é um TRNG
Um TRNG deve gerar, no longo prazo, uma “sequência de números sem padrões ou regularidades reconhecíveis”
Portanto, aleatoriedade estatística não significa PRNG, mas pode-se dizer que ela também se aplica a TRNGs
O problema parece vir do fato de existirem muitos testes de aleatoriedade estatística para verificar se um PRNG tem uma forma limitada de aleatoriedade estatística
Por isso, para identificar um PRNG, a expressão “gerador de números pseudoaleatórios” teria sido mais adequada do que “aleatoriedade estatística”. Ainda assim, é uma falha bem pequena