Criptografia Pós-Quântica

Um computador quântico suficientemente grande quebraria o RSA, o X25519/X448 e o ECDSA/ECDH em todas as curvas da página Criptografia de Chave Pública: nenhuma dessa matemática resiste ao algoritmo de Shor. Ele não precisa existir hoje para ser um problema hoje, um adversário pode gravar tráfego criptografado agora e descriptografá-lo assim que tal máquina existir, uma estratégia geralmente chamada de "colher agora, descriptografar depois". Qualquer coisa que precise permanecer confidencial por anos, não por dias, já deveria estar migrando para uma troca de chaves pós-quântica.

 

O NIST padronizou três esquemas, todos implementados aqui, e todos construídos sobre nada além de SHAKE (veja Hashing & Autenticação de Mensagens), de modo que a segurança deles depende apenas de o hash ser sólido: nenhuma suposição de reticulados (lattice) ou de teoria dos números a ser quebrada.

 

Esquema Finalidade Padrão Unit
ML-KEM Encapsulamento de chave, substitui o Diffie-Hellman FIPS 203 sgcCrypto_MLKEM
ML-DSA Assinaturas, baseado em reticulados, a escolha padrão FIPS 204 sgcCrypto_MLDSA
SLH-DSA Assinaturas, baseado apenas em hash, alternativa conservadora FIPS 205 sgcCrypto_SLHDSA

 

ML-KEM e a construção híbrida (recomendado)

 

O ML-KEM substitui uma troca Diffie-Hellman: o remetente encapsula um segredo compartilhado para a chave pública do destinatário, o destinatário desencapsula esse segredo com a chave privada, e ambos terminam com os mesmos 32 bytes. Ele vem em três conjuntos de parâmetros, quanto maior, maior a margem de segurança e maiores as chaves.

 


procedure sgcMLKEM_GenerateKeyPair(aParams: TsgcMLKEMParams; out aPublicKey, aPrivateKey: TBytes);
procedure sgcMLKEM_Encapsulate(aParams: TsgcMLKEMParams; const aPublicKey: TBytes; out aCiphertext, aSharedSecret: TBytes);
function sgcMLKEM_Decapsulate(aParams: TsgcMLKEMParams; const aPrivateKey, aCiphertext: TBytes): TBytes;

var
  oPub, oPriv, oCiphertext, oSecretA, oSecretB: TBytes;
begin
  sgcMLKEM_GenerateKeyPair(mlkem768, oPub, oPriv);
  sgcMLKEM_Encapsulate(mlkem768, oPub, oCiphertext, oSecretA);   { sender }
  oSecretB := sgcMLKEM_Decapsulate(mlkem768, oPriv, oCiphertext); { recipient }
  { oSecretA = oSecretB }
end;

 

A desencapsulação da FIPS 203 nunca falha.

Um texto cifrado corrompido ou malicioso não gera um erro, ele silenciosamente retorna um segredo diferente, derivado de um valor de rejeição implícito embutido na chave privada. Retornar um erro em vez disso entregaria a um atacante um oráculo de descriptografia para sondar a chave privada; o padrão evita isso por design. O que quer que o segredo derivado proteja tem que detectar a divergência por conta própria, por exemplo uma tag AEAD que falha ao ser verificada mais adiante.

 

O que de fato implantar hoje é a construção híbrida: X25519 combinado com ML-KEM-768, de modo que o segredo compartilhado dependa dos dois. Ela continua segura se o ML-KEM algum dia for quebrado por um avanço de criptoanálise, e continua segura se um computador quântico quebrar o X25519; um atacante precisa que os dois falhem ao mesmo tempo.

 


procedure sgcHybrid_GenerateKeyPair(out aPublicKey, aPrivateKey: TBytes);
procedure sgcHybrid_Encapsulate(const aPublicKey: TBytes; out aCiphertext, aSharedSecret: TBytes);
function sgcHybrid_Decapsulate(const aPrivateKey, aCiphertext: TBytes; out aSharedSecret: TBytes): Boolean;

 

X-Wing e os grupos híbridos do TLS 1.3

 

sgcCrypto_MLKEM_Hybrid contém três famílias híbridas. As funções sgcHybrid_ acima são o formato original do sgcCrypto e são mantidas por compatibilidade. O X-Wing é o KEM híbrido padronizado de uso geral, X25519 combinado com ML-KEM-768, e é o que se deve usar em código novo fora do TLS. A terceira família constrói os key shares híbridos do TLS 1.3 e nada mais.

 

O X-Wing (draft-connolly-cfrg-xwing-kem-10) armazena um único segredo: a chave privada é uma semente de 32 bytes, e cada operação a expande no material ML-KEM-768 e X25519 de que precisa. A chave pública tem 1216 bytes, o texto cifrado 1120 bytes e o segredo compartilhado 32 bytes. O encapsulamento executa a verificação da chave de encapsulamento da FIPS 203 e gera um erro quando a chave pública não passa nela.

 


procedure sgcXWing_GenerateKeyPair(out aPublicKey, aPrivateKey: TBytes);
procedure sgcXWing_Encapsulate(const aPublicKey: TBytes; out aCiphertext, aSharedSecret: TBytes);
function sgcXWing_Decapsulate(const aPrivateKey, aCiphertext: TBytes; out aSharedSecret: TBytes): Boolean;

var
  oPub, oPriv, oCiphertext, oSecretA, oSecretB: TBytes;
begin
  sgcXWing_GenerateKeyPair(oPub, oPriv);
  sgcXWing_Encapsulate(oPub, oCiphertext, oSecretA);      { sender }
  if sgcXWing_Decapsulate(oPriv, oCiphertext, oSecretB) then
    { oSecretA = oSecretB };
end;

 

Os key shares híbridos do TLS 1.3 da RFC 10024 são X25519MLKEM768, SecP256r1MLKEM768 e SecP384r1MLKEM1024, codepoints IANA 0x11EC, 0x11EB e 0x11ED. Eles não são KEMs que você possa usar por conta própria. O segredo compartilhado é uma simples concatenação das duas metades, o que só é seguro porque o TLS o alimenta em seu key schedule junto com o transcript do handshake, portanto use-os para construir um key share de TLS e nada mais. O mecanismo TLS Nativo oferece X25519MLKEM768 primeiro por padrão, de modo que um cliente ou um servidor que o utilize já negocia uma troca de chaves pós-quântica sem código adicional.

 

Os grupos híbridos existem apenas no TLS 1.3. O mecanismo nativo também fala TLS 1.2, e uma conexão que recorre a ele porque o peer não consegue TLS 1.3 usa um grupo clássico, X25519, secp256r1 ou secp384r1, sem proteção pós-quântica. Defina TLSOptions.Version em um cliente ou SSLOptions.Version em um servidor como tls1_3, e liste apenas grupos híbridos em NativeTLS_Options.Groups, quando a troca de chaves precisar ser sempre pós-quântica.

 

O demo Post-Quantum TLS, Demos\18.Cryptography\08.PostQuantum_TLS, tem um seletor de versão TLS com as opções TLS 1.3 e TLS 1.2, apenas TLS 1.3 e apenas TLS 1.2, e um seletor de conjunto de cifras, e sua linha de status mostra a versão, o grupo e o conjunto de cifras negociados. O demo do servidor pode assinar com um certificado ML-DSA-44, que atende apenas clientes TLS 1.3, porque o TLS 1.2 não tem esquema de assinatura ML-DSA.

 


type
  TsgcTLSHybridGroup = (thgX25519MLKEM768, thgSecP256r1MLKEM768, thgSecP384r1MLKEM1024);

function sgcTLSHybrid_GroupID(aGroup: TsgcTLSHybridGroup): Word;
function sgcTLSHybrid_GroupName(aGroup: TsgcTLSHybridGroup): string;
function sgcTLSHybrid_Available(aGroup: TsgcTLSHybridGroup): Boolean;

procedure sgcTLSHybrid_ClientKeyShare(aGroup: TsgcTLSHybridGroup; out aKeyShare, aPrivateState: TBytes);
function sgcTLSHybrid_ServerKeyShare(aGroup: TsgcTLSHybridGroup; const aClientKeyShare: TBytes;
  out aKeyShare, aSharedSecret: TBytes): Boolean;
function sgcTLSHybrid_ClientSharedSecret(aGroup: TsgcTLSHybridGroup;
  const aPrivateState, aServerKeyShare: TBytes; out aSharedSecret: TBytes): Boolean;

 

O cliente mantém aPrivateState em segredo, passa-o inalterado para sgcTLSHybrid_ClientSharedSecret e o apaga em seguida; seu layout não faz parte da API. As duas funções Boolean retornam False, com todas as saídas vazias, diante de qualquer entrada inválida do peer: um key share com o comprimento errado, uma chave de encapsulamento ML-KEM que não passa na verificação da FIPS 203, um ponto que não está sobre a curva ou um resultado X25519 todo zerado. A RFC 10024 exige que a conexão seja abortada com um alerta illegal_parameter em cada um desses casos.

 

Tudo o que é construído sobre X25519, ou seja, as funções sgcHybrid_, o X-Wing e o grupo X25519MLKEM768, é removido da compilação em um build SGC_CRYPTO_FIPS, veja Modo FIPS. Os dois grupos SecP permanecem, porque P-256, P-384 e ML-KEM são todos algoritmos aprovados. Chame sgcTLSHybrid_Available antes de oferecer ou aceitar um grupo, em vez de presumir que ele está presente.

 

ML-DSA (recomendado para assinaturas)

 

Assinar sorteia um vetor de mascaramento e tenta construir uma assinatura, reiniciando quando a tentativa vazaria informação sobre a chave; o número esperado de tentativas é pequeno mas não fixo, de modo que assinar leva um tempo variável. Isso é inerente ao esquema e não é um canal lateral sobre a chave. As assinaturas são grandes: 2420 bytes para ML-DSA-44 contra 64 do Ed25519, e chaves públicas de 1312 bytes contra 32, portanto reserve orçamento para essa diferença antes de adotá-lo em um protocolo sensível a tamanho.

 


procedure sgcMLDSA_GenerateKeyPair(aParams: TsgcMLDSAParams; out aPublicKey, aPrivateKey: TBytes);
function sgcMLDSA_Sign(aParams: TsgcMLDSAParams; const aPrivateKey, aMessage, aContext: TBytes): TBytes;
function sgcMLDSA_Verify(aParams: TsgcMLDSAParams; const aPublicKey, aMessage, aSignature, aContext: TBytes): Boolean;

var
  oPub, oPriv, oSignature, oEmpty: TBytes;
begin
  sgcMLDSA_GenerateKeyPair(mldsa44, oPub, oPriv);
  oSignature := sgcMLDSA_Sign(mldsa44, oPriv, oMessage, oEmpty);
  if sgcMLDSA_Verify(mldsa44, oPub, oMessage, oSignature, oEmpty) then
    { valid };
end;

 

SLH-DSA (alternativa conservadora)

 

O SLH-DSA se apoia em nada além da função hash: nenhum problema de reticulados a ser quebrado, jamais, em nenhum momento no futuro. Essa garantia é cara. Uma assinatura vai de 7856 bytes (a menor, SLH-DSA-SHAKE-128s) até 49856 bytes (a maior, SLH-DSA-SHAKE-256f), e assinar com um conjunto de parâmetros s (small, pequeno) percorre centenas de milhares de operações de hash e pode levar dezenas de segundos; os conjuntos f (fast, rápido) assinam em torno de um a sete segundos, a um tamanho de assinatura aproximadamente duas vezes maior. A verificação é rápida para todo conjunto de parâmetros, o que se adequa melhor a um caso de uso de assinar raramente, verificar com frequência, como firmware ou assinatura de código, do que a um protocolo interativo.

 


procedure sgcSLHDSA_GenerateKeyPair(aParams: TsgcSLHDSAParams; out aPublicKey, aPrivateKey: TBytes);
function sgcSLHDSA_Sign(aParams: TsgcSLHDSAParams; const aPrivateKey, aMessage, aContext: TBytes;
  aDeterministic: Boolean = True): TBytes;
function sgcSLHDSA_Verify(aParams: TsgcSLHDSAParams; const aPublicKey, aMessage, aSignature, aContext: TBytes): Boolean;

 

Formatos de chave: SubjectPublicKeyInfo, PKCS#8 e PEM

 

As cadeias de bytes brutas que os algoritmos FIPS consomem não são um formato de armazenamento, e nenhuma outra implementação vai lê-las. Para gravar uma chave em disco, colocá-la em um certificado ou entregá-la a outra biblioteca, exporte-a nos contêineres padrão: um SubjectPublicKeyInfo X.509 para uma chave pública, um OneAsymmetricKey PKCS#8 para uma chave privada, e armadura PEM (RFC 7468) em volta de qualquer uma das duas, rotulada como PUBLIC KEY e PRIVATE KEY. Os três esquemas expõem as mesmas oito funções, cada um seguindo a RFC que define sua codificação.

 

Esquema RFC do contêiner Funções DER Funções PEM
ML-DSA RFC 9881 sgcMLDSA_ExportSubjectPublicKeyInfo, sgcMLDSA_ImportSubjectPublicKeyInfo, sgcMLDSA_ExportPrivateKeyInfo, sgcMLDSA_ImportPrivateKeyInfo sgcMLDSA_ExportPublicKeyPEM, sgcMLDSA_ImportPublicKeyPEM, sgcMLDSA_ExportPrivateKeyPEM, sgcMLDSA_ImportPrivateKeyPEM
ML-KEM RFC 9935 sgcMLKEM_ExportSubjectPublicKeyInfo, sgcMLKEM_ImportSubjectPublicKeyInfo, sgcMLKEM_ExportPrivateKeyInfo, sgcMLKEM_ImportPrivateKeyInfo sgcMLKEM_ExportPublicKeyPEM, sgcMLKEM_ImportPublicKeyPEM, sgcMLKEM_ExportPrivateKeyPEM, sgcMLKEM_ImportPrivateKeyPEM
SLH-DSA RFC 9909 sgcSLHDSA_ExportSubjectPublicKeyInfo, sgcSLHDSA_ImportSubjectPublicKeyInfo, sgcSLHDSA_ExportPrivateKeyInfo, sgcSLHDSA_ImportPrivateKeyInfo sgcSLHDSA_ExportPublicKeyPEM, sgcSLHDSA_ImportPublicKeyPEM, sgcSLHDSA_ExportPrivateKeyPEM, sgcSLHDSA_ImportPrivateKeyPEM

 

O identificador de algoritmo carrega o object identifier do NIST com os parâmetros ausentes, de modo que o conjunto de parâmetros viaja junto com a chave e a importação o devolve. sgcMLDSA_OID, sgcMLKEM_OID e sgcSLHDSA_OID fornecem a string pontuada, e as funções ParamsFromOID correspondentes fazem o mapeamento inverso.

 

Três formas de chave privada para ML-DSA e ML-KEM.

As funções de exportação recebem um parâmetro aFormat do tipo TsgcPQCPrivateKeyFormat, declarado em sgcCrypto_DER. pqkfSeed, o padrão, grava apenas a semente, 32 bytes para ML-DSA e os 64 bytes d || z para ML-KEM; esta é a forma que as duas RFCs recomendam e é de longe a menor. pqkfExpanded grava a chave privada FIPS expandida. pqkfBoth grava as duas e gera um erro quando a chave expandida não é a expansão da semente, que é o que as RFCs chamam de chave malformada.

 

Uma semente não pode ser recuperada de uma chave expandida depois, portanto peça-a no momento da geração com sgcMLDSA_GenerateKeyPairAndSeed ou sgcMLKEM_GenerateKeyPairAndSeed quando pretender armazenar a forma recomendada. O SLH-DSA tem uma única forma de chave privada, SK.seed || SK.prf || PK.seed || PK.root, portanto sua exportação não recebe parâmetro de formato.

 


function sgcMLDSA_ExportPrivateKeyPEM(aParams: TsgcMLDSAParams; const aSeed, aPrivateKey: TBytes;
  aFormat: TsgcPQCPrivateKeyFormat = pqkfSeed): string;
function sgcMLDSA_ImportPrivateKeyPEM(const aPEM: string; out aParams: TsgcMLDSAParams;
  out aSeed, aPrivateKey, aPublicKey: TBytes): Boolean;

var
  vParams: TsgcMLDSAParams;
  oSeed, oPub, oPriv: TBytes;
  vPublicPEM, vPrivatePEM: string;
begin
  sgcMLDSA_GenerateKeyPairAndSeed(mldsa65, oSeed, oPub, oPriv);

  vPublicPEM := sgcMLDSA_ExportPublicKeyPEM(mldsa65, oPub);
  vPrivatePEM := sgcMLDSA_ExportPrivateKeyPEM(mldsa65, oSeed, oPriv, pqkfSeed);

  { reading it back: the parameter set comes from the key itself }
  if sgcMLDSA_ImportPrivateKeyPEM(vPrivatePEM, vParams, oSeed, oPriv, oPub) then
    { vParams = mldsa65, and oPriv and oPub are the expansion of oSeed };
end;

 

A importação valida, ela não confia.

Toda função de importação retorna False em vez de gerar um erro, e retorna False diante de DER malformado, de dados sobrando ao final, de um object identifier desconhecido, de parâmetros presentes quando deveriam estar ausentes e de uma chave cujo comprimento não corresponde ao conjunto de parâmetros. Além da estrutura, uma chave pública ML-KEM precisa passar na verificação de módulo da FIPS 203, uma chave privada ML-KEM expandida precisa passar na verificação de hash e no teste de consistência do par, e uma chave privada ML-DSA expandida é reconstruída em sua chave pública, de modo que os valores armazenados t0 e tr precisam ambos concordar com ela. Uma chave pública embutida que não corresponde à chave privada é recusada. Quando uma chave privada contém uma semente, todas as três saídas voltam preenchidas, de modo que o chamador nunca precisa expandi-la separadamente.

 

sgcSLHDSA_ImportPrivateKeyInfo e sgcSLHDSA_ImportPrivateKeyPEM recebem um parâmetro extra aVerify, True por padrão, que recalcula PK.root a partir da semente privada e recusa uma chave cuja raiz armazenada seja diferente. Essa reconstrução custa tanto quanto uma geração de chave, o que significa segundos para os conjuntos de parâmetros s, portanto passe False apenas para chaves que sua própria aplicação gerou ou nas quais já confia. O object identifier, os comprimentos e a estrutura continuam sendo verificados quando ele é False.

 

Estes são os contêineres que o resto da biblioteca lê. Uma chave privada ML-DSA em PKCS#8 é o que o cliente JWT usa para assinar, um SubjectPublicKeyInfo é o que o servidor JWT usa para validar, e ambos são o que Certificados & PKI coloca em um certificado ou em uma requisição de assinatura de certificado.

 

Escolhendo um conjunto de parâmetros

 

Toda função de tamanho (sgcMLKEM_PublicKeySize, sgcMLDSA_SignatureSize, sgcSLHDSA_SignatureSize e suas contrapartes PrivateKeySize) recebe o mesmo enum de parâmetro que a própria operação, de modo que um protocolo pode dimensionar seus buffers antes de realizar qualquer operação criptográfica:

 

Conjunto de parâmetros Chave pública Chave privada Texto cifrado / Assinatura
ML-KEM-5128001632768
ML-KEM-768118424001088
ML-KEM-1024156831681568
ML-DSA-44131225602420
ML-DSA-65195240323309
ML-DSA-87259248964627
SLH-DSA-SHAKE-128s32647856
SLH-DSA-SHAKE-128f326417088
SLH-DSA-SHAKE-192s489616224
SLH-DSA-SHAKE-192f489635664
SLH-DSA-SHAKE-256s6412829792
SLH-DSA-SHAKE-256f6412849856

 

Todos os tamanhos estão em bytes. O segredo compartilhado do ML-KEM é sempre 32 bytes, independentemente do conjunto de parâmetros (sgcMLKEM_SharedSecretSize).