Artigo Técnico

Ed448 e Brainpool ECDSA em Pascal puro para PDF

O PDFlibPas assina e verifica com Ed448 e com as três curvas ECDSA Brainpool em Object Pascal puro. Nenhuma biblioteca criptográfica externa, nenhum fornecedor de plataforma, nenhuma DLL: o PDFlibEd448 implementa o PureEdDSA do RFC 8032 sobre edwards448, e o PDFlibBrainpool implementa brainpoolP256r1, brainpoolP384r1 e brainpoolP512r1 do RFC 5639. Ambos foram construídos da mesma forma, contra vetores de resposta conhecida gerados de forma independente antes de qualquer linha de Pascal ser escrita, e ambos merecem ser relatados sobretudo pelos bugs

A aritmética de corpo é código invulgarmente honesto. Ou corresponde byte a byte aos vetores publicados, ou não corresponde, pelo que não há espaço para um "quase a funcionar". O que a torna difícil é que uma implementação errada continua a produzir assinaturas, continua a verificar as próprias assinaturas e continua a parecer completamente plausível

Porquê estas curvas, e porquê em Pascal

As curvas Brainpool aparecem em perfis europeus de assinatura qualificada, pelo que uma biblioteca que assine documentos para esse mercado não as pode tratar como exóticas. O Ed448 faz parte do conjunto de algoritmos que a ISO/TS 32002 traz para o PDF, em que o resumo interno é SHAKE256 e não SHA-2. Nenhuma das famílias está disponível nas bibliotecas criptográficas Pascal de uso comum, pelo que uma biblioteca PDF que as queira tem de as implementar

O argumento da implantação é o mesmo que se aplica a toda a criptografia desta biblioteca: uma aplicação que distribui um único binário sem dependência criptográfica não tem fornecedor para detetar, nem versão para corresponder, nem comportamento que mude quando o anfitrião é atualizado. Assinar é precisamente a área em que menos se quer uma dependência em movimento

As constantes vêm do texto da especificação, nunca da memória

A primeira tentativa do ponto base de edwards448 foi escrita de memória e estava errada. Não é um erro notável, mas é muito caro, porque um ponto base errado produz um sistema autoconsistente: a sua geração de chaves, assinatura e verificação concordam entre si e discordam do resto do mundo

O procedimento funcional é retirar cada parâmetro de domínio do texto da especificação e depois verificar cruzadamente. Para edwards448, isso significa o primo, a constante da curva, a ordem do grupo e ambas as coordenadas decimais do ponto base extraídos do RFC 8032, convertidos para a representação interna em limbs, e depois confrontados com os vetores de teste publicados no mesmo documento. Para as curvas Brainpool, significa os parâmetros do RFC 5639, uma implementação independente escrita para gerar vetores, e uma verificação cruzada contra uma biblioteca de sistema nos dois sentidos antes de qualquer Pascal correr

Os parâmetros de domínio de Ed448 e Brainpool fluem do texto das especificações RFC 8032 e RFC 5639 para a forma em limbs e são verificados cruzadamente antes de qualquer Pascal correr
Os parâmetros de domínio de edwards448 e das curvas Brainpool são retirados do texto dos RFC, convertidos para limbs e verificados cruzadamente contra vetores independentes

Um atalho de derivação merece um aviso porque parece universal e não é: recuperar o ponto base a partir de um valor fixo de y funciona na curva 25519 e não funciona em edwards448, onde esse valor não tem raiz quadrada. Um script refutou-o em segundos, o que é muito mais barato do que descobri-lo através de um depurador

O método: uma réplica ao nível de limbs antes de qualquer Pascal

A técnica que tornou ambas as unidades tratáveis é uma implementação espelho numa linguagem com inteiros sem limites, construída de baixo para cima. Primeiro a camada aritmética sozinha: multiplicação de corpo, subtração e propagação de carries, testada sob carga contra os seus invariantes algébricos ao longo de algumas centenas de casos aleatórios. Depois a geração de chaves completa dentro do espelho, que é onde vivem os bugs semânticos e onde são baratos de encontrar. Só então a transcrição para Pascal

Fluxo de trabalho de uma implementação espelho com inteiros sem limites a validar a aritmética de corpo Pascal e a geração de chaves para Ed448 e Brainpool
O fluxo espelho de baixo para cima: primeiro a aritmética, depois a geração de chaves dentro do espelho, depois a transcrição Pascal e a comparação de valores intermédios

O retorno é diagnóstico em vez de desenvolvimental. Uma vez conhecida a correção do espelho, qualquer divergência entre espelho e Pascal é um lapso de transcrição, e sondar o mesmo valor intermédio nas duas implementações localiza-o imediatamente. Isso converte uma classe de bug que de outra forma é quase indepurável, um único limb errado no interior profundo de uma multiplicação escalar, numa comparação de cinco minutos

Quatro causas raiz no Ed448

As quatro foram encontradas sondando valores intermédios, e as quatro são do tipo que produz saída de aparência válida

A primeira é uma armadilha de notação. A maioria das fórmulas publicadas para a adição de Edwards unificada assume uma constante de curva de menos um, e edwards448 tem mais um. Transportada sem alterações, o numerador da coordenada y é escrito como uma soma quando deveria ser uma diferença. A correção não é remendar o sinal, mas re-derivar a forma de produto sem inversão a partir da lei de adição afim da curva correta, o que produz as quatro expressões de coordenadas e não deixa espaço para um sinal ser herdado da fonte errada

A segunda está na descompressão de pontos. Recuperar o x afim a partir de coordenadas projetivas exige uma multiplicação pelo inverso de Z. Multiplicar pelo inverso ao quadrado produz um valor que continua a ser uma representação projetiva válida e é a coordenada afim errada, pelo que o sintoma é um y correto com um x errado. Sempre que uma coordenada está certa e a outra não, o bug está na normalização, não na aritmética

A terceira é um hábito importado da curva mais curta. Tanto o escalar por assinatura como o escalar de desafio têm de ser reduzidos a partir do resumo completo, que para Ed448 são 114 bytes, e não dos primeiros 57. A curva de 32 bytes também usa o seu resumo completo de 64 bytes, pelo que a regra é consistente; é apenas o pressuposto de que "metade do resumo é a largura do escalar" que está errado

A quarta é a ordenação. O prefixo de separação de domínio vem primeiro, antes do prefixo de contexto e da mensagem, o que não é a ordem que a leitura intuitiva de R e A na especificação sugere. Errar isto produz assinaturas que verificam contra a sua própria implementação e contra mais nada, que é a falha mais enganadora possível

// Carry de corpo: propagação pura com semântica de piso (floor),
// pelo que funcionam tanto os limbs positivos como os negativos e a
// subtração dispensa viés. O carry superior volta através de
// 2^448 = 2^224 + 1 (mod p), que toca o limb 0 e o limb 8. Limitado
// a quatro rondas; duas observadas na prática
procedure FeCarry(var A: TFe448);
var
  I, Round: Integer;
  Carry: Int64;
begin
  for Round := 1 to 4 do
  begin
    Carry := 0;
    for I := 0 to 15 do
    begin
      A[I] := A[I] + Carry;
      Carry := Floor28(A[I]);          // piso (floor), não truncatura
      A[I] := A[I] - (Carry shl 28);
    end;
    if Carry = 0 then
      Break;
    A[0] := A[0] + Carry;              // 2^448 == 1
    A[8] := A[8] + Carry;              // 2^448 == 2^224
  end;
end;

Uma versão anterior dessa rotina aplicava um viés antes de propagar, e com entradas grandes dobrava um carry espúrio de magnitude errada nos limbs baixos. Esquemas de carry baseados em viés são uma fonte persistente desta classe de defeito; a semântica de piso com um ciclo repeat limitado é mais fácil de raciocinar e mensuravelmente suficientemente rápida

Duas causas raiz no Brainpool

A primeira nem sequer é criptografia. A representação funcional são 33 limbs, pelo que o produto de dois valores precisa de 66, e o array de produtos foi declarado com 64. Escrever para além do fim corrompia memória adjacente, o que se manifestou primeiro como resultados errados e só se tornou um crash depois de adicionado um varrimento mais amplo. A regra que daí saiu vale a pena aplicar a todos os buffers numéricos de tamanho fixo: dimensioná-los pela largura do produto no pior caso e adicionar margem, e depois nunca mais pensar nisso. O array no código de produção tem 68 limbs

A segunda é uma forma de exponenciação trocada. Há duas formas corretas de square-and-multiply e consomem o expoente em sentidos opostos: a forma da direita para a esquerda multiplica e depois eleva a base ao quadrado e tem de ler os bits a partir da extremidade menos significativa, enquanto a forma da esquerda para a direita eleva ao quadrado e depois multiplica e lê a partir da extremidade mais significativa. O ciclo de inversão modular tinha um corpo da direita para a esquerda com um percurso de bits do mais significativo primeiro. Ambas as metades são de livro, a combinação não é, e o resultado é um inverso errado que ainda parece um elemento de corpo plausível

Duas formas de exponenciação square-and-multiply com sentidos de bits opostos e a forma mista que calculava inversos modulares Brainpool errados
Ambas as formas square-and-multiply são corretas por si; combinar um corpo da direita para a esquerda com um percurso do mais significativo primeiro produz um inverso errado plausível
// Duplicação e adição Jacobian em que o registo de destino pode ser
// a mesma variável que uma origem. Uma cópia integral do registo à
// entrada é a única defesa fiável: escrever os limbs de R polui as
// leituras posteriores de P
procedure BPPointDouble(var R: TBPPoint; const P: TBPPoint;
  const Curve: TBPCurve);
var
  Pin: TBPPoint;
begin
  Pin := P;        // copiar primeiro, e depois calcular apenas a partir de Pin
  // ... M = 3X^2 + A*Z^4, S = 4*X*Y^2, X3 = M^2 - 2S, ...
end;

Duas lições de processo que custaram mais do que os bugs

Corrigir incrementalmente a quente não converge numa unidade criptográfica. Um rascunho foi remendado repetidamente até acumular 32 rotinas duplicadas e uma estrutura danificada, e só foi resolvido reescrevendo-o. O padrão a adotar é escrever uma única vez a partir de um espelho validado ou reescrever; uma sequência de correções locais a aritmética que ainda não compreende acumula mais depressa do que corrige

E verifique o carimbo temporal no executável antes de acreditar num resultado de teste. Uma compilação incremental que compila mas não refaz a ligação corre o binário anterior, o que fabricou uma ronda inteira de pistas falsas sobre sondas em falta e saída duplicada. Ao depurar criptografia, um resultado inexplicado deve sugerir "será este o binário que acabei de compilar" antes de "estar o algoritmo errado"

Desempenho, âmbito e como chamar

A redução modular na unidade Brainpool é subtração por deslocamento em série de bits a partir do bit ativo mais elevado do produto, pelo que uma multiplicação custa aproximadamente pela ordem da largura de bits. Uma verificação P-256 fica nas primeiras centenas de milissegundos, o que é irrelevante para assinar ou verificar documentos e seria inadequado para um terminador TLS. A redução de Barrett é a melhoria óbvia e precisa de um valor de trabalho mais largo do que a representação atual transporta, pelo que é uma alteração a fazer quando uma carga de trabalho o pedir, e não preventivamente

uses
  PDFlibEd448, PDFlibBrainpool;

var
  PublicKey, Signature: AnsiString;
  Curve: TBPCurve;
  R, S, PubX, PubY: TBPValue;
begin
  // Ed448: PureEdDSA, SHAKE256 internamente, chaves de 57 bytes
  if Ed448PublicKeyFromSeed(Seed, PublicKey) and
     Ed448Sign(DocumentDigest, Seed, Signature) then
    Assert(Ed448Verify(DocumentDigest, PublicKey, Signature));

  // Brainpool: o chamador fornece o nonce por assinatura, pelo que a
  // política de nonce fica com a aplicação
  Curve := BPLoadCurve(bpP256r1);
  if BPKeyGen(PubX, PubY, PrivateD, Curve) and
     BPSignFixedK(R, S, Hash, PrivateD, Nonce, Curve) then
    Assert(BPVerify(R, S, Hash, PubX, PubY, Curve));
end;

Note que o ponto de entrada de assinatura Brainpool recebe o nonce em vez de gerar um. Isso é deliberado: a geração de nonce é a única coisa mais catastrófica de errar em ECDSA, dado que um valor repetido ou previsível divulga a chave privada, e a decisão sobre a origem da aleatoriedade pertence à aplicação e ao seu regime de conformidade, não a uma biblioteca PDF

Estas curvas juntam-se ao trabalho pós-quântico descrito no artigo sobre FIPS 204 ML-DSA, e ligam-se à mesma pipeline de assinatura e validação coberta em assinatura e validação PAdES. Para certificados de teste nestas curvas, a via de geração local está descrita em certificados autoassinados com CryptoAPI. A matriz completa de algoritmos está listada na página de produto da losLab PDF Developer Library