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. Sem biblioteca criptográfica externa, sem provedor de plataforma, sem DLL: o PDFlibEd448 implementa o PureEdDSA da RFC 8032 sobre edwards448, e o PDFlibBrainpool implementa brainpoolP256r1, brainpoolP384r1 e brainpoolP512r1 da RFC 5639. Ambos foram construídos da mesma forma, contra vetores de resposta conhecida gerados de forma independente antes que qualquer Pascal fosse escrito, e ambos valem o registro principalmente pelos bugs

A aritmética de campo é um código incomumente honesto. Ela ou corresponde aos vetores publicados byte a byte ou não corresponde, então não há espaço para um "quase funcionando". O que a torna difícil é que uma implementação errada ainda produz assinaturas, ainda verifica as próprias assinaturas e ainda parece completamente plausível

Por que essas curvas, e por que em Pascal

As curvas Brainpool aparecem em perfis europeus de assinatura qualificada, então uma biblioteca que assina documentos para esse mercado não pode tratá-las como exóticas. O Ed448 está no conjunto de algoritmos que a ISO/TS 32002 traz para o PDF, no qual o digest interno é SHAKE256 em vez de SHA-2. Nenhuma das famílias está disponível nas bibliotecas criptográficas Pascal de uso comum, então uma biblioteca PDF que as queira precisa implementá-las por conta própria

O argumento de implantação é o mesmo que se aplica a toda a criptografia desta biblioteca: um aplicativo que distribui um único binário sem dependência criptográfica não tem provedor a detectar, versão a igualar nem comportamento que mude quando o host recebe patches. Assinar é exatamente a área em que você menos quer uma dependência móvel

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

A primeira tentativa do ponto base do 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 autocoerente: sua geração de chaves, assinatura e verificação concordam entre si e discordam do resto do mundo

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

Os parâmetros de domínio 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 que qualquer Pascal rode
Os parâmetros de domínio do edwards448 e das curvas Brainpool vêm do texto das RFCs, são 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 para a curva 25519 e não funciona para o edwards448, no qual esse valor não tem raiz quadrada. Um script o desmentiu em segundos, o que é muito mais barato do que descobrir isso por meio de um debugger

O método: um espelho no nível de limb antes de qualquer Pascal

A técnica que tornou ambas as unidades tratáveis é uma implementação espelho em uma linguagem com inteiros sem limites, construída de baixo para cima. Primeiro apenas a camada aritmética: multiplicação de campo, subtração e propagação de carry, testadas contra seus invariantes algébricos em algumas centenas de casos aleatórios. Depois a geração de chaves completa dentro do espelho, que é onde os bugs semânticos vivem 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 validando a aritmética de campo Pascal e a geração de chaves para Ed448 e Brainpool
O fluxo de trabalho do espelho de baixo para cima: primeiro a aritmética, depois a geração de chaves dentro do espelho, então a transcrição Pascal e a comparação de valores intermediários

O ganho é diagnóstico em vez de desenvolvimento. Uma vez que se sabe que o espelho está correto, qualquer divergência entre espelho e Pascal é um deslize de transcrição, e sondar o mesmo valor intermediário nas duas implementações o localiza imediatamente. Isso converte uma classe de bug que de outra forma é quase indepurável, um único limb errado no fundo de uma multiplicação escalar, em uma comparação de cinco minutos

Quatro causas-raiz no Ed448

Todas as quatro foram encontradas sondando valores intermediários, e todas as quatro são do tipo que produz uma saída de aparência válida

A primeira é uma armadilha de notação. A maioria das fórmulas publicadas para a adição unificada de Edwards assume uma constante de curva igual a menos um, e o edwards448 tem mais um. Transportada sem alteração, o numerador da coordenada y é escrito como uma soma onde deveria ser uma diferença. A correção não é remendar o sinal, mas rederivar 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 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 ainda é uma representação projetiva válida e é a coordenada afim errada, então 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 quanto o escalar de desafio devem ser reduzidos do digest completo, que para o Ed448 é de 114 bytes, e não de seus primeiros 57. A curva de 32 bytes também usa seu digest completo de 64 bytes, então a regra é consistente; apenas a suposição de que "metade do digest é a largura do escalar" está errada

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 isso produz assinaturas que verificam contra a sua própria implementação e nada mais, que é a falha mais enganosa possível

// Design do carry de campo: propagação pura com semântica de floor,
// de modo que limbs positivos e negativos funcionam e a subtração
// não precisa de bias. O carry superior dobra de volta por
// 2^448 = 2^224 + 1 (mod p), o que toca o limb 0 e o limb 8.
// Limitado a quatro rodadas; 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]);          // floor, não truncamento
      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 bias antes de propagar, e com entradas grandes ela dobrava um carry espúrio de magnitude errada nos limbs baixos. Esquemas de carry baseados em bias são uma fonte persistente dessa classe de defeito; a semântica de floor com um loop repeat limitado é mais fácil de raciocinar e mensuravelmente rápida o suficiente

Duas causas-raiz no Brainpool

A primeira não é criptografia de forma alguma. A representação em uso tem 33 limbs, então o produto de dois valores precisa de 66, e o array de produtos foi declarado com 64. Escrever além do fim corrompeu memória adjacente, o que apareceu primeiro como resultados errados e só se tornou um crash depois que uma varredura mais ampla foi adicionada. A regra que saiu disso vale para todo buffer numérico de tamanho fixo: dimensione pela largura do produto no pior caso e adicione margem, e depois nunca mais pense nisso. O array no código de produção tem 68 limbs

A segunda é um formato de exponenciação confuso. Existem duas formas corretas de square-and-multiply e elas consomem o expoente em direções opostas: a forma da direita para a esquerda multiplica e depois eleva a base ao quadrado e deve 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 loop de inversão modular tinha um corpo da direita para a esquerda com uma varredura de bits começando pelo mais significativo. Ambas as metades são de livro-texto, a combinação não é, e o resultado é um inverso errado que ainda parece um elemento de campo plausível

Duas formas de exponenciação square-and-multiply com direções de bits opostas e a forma mista que calculava inversos modulares Brainpool errados
Ambas as formas square-and-multiply estão corretas por si só; combinar um corpo da direita para a esquerda com uma varredura do mais significativo primeiro produz um inverso errado plausível
// Duplicação e adição de Jacobian em que o registro de destino pode
// ser a mesma variável que uma origem. Uma cópia integral do registro
// na entrada é a única defesa confiá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;        // copie primeiro, depois calcule 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 que os bugs

Correções incrementais quentes não convergem em uma unidade criptográfica. Um rascunho foi remendado repetidamente até carregar 32 rotinas duplicadas e uma estrutura danificada, e só foi consertado sendo reescrito. O padrão a adotar é escrever uma vez a partir de um espelho validado ou reescrever; uma sequência de correções locais em aritmética que você ainda não entende acumula mais rápido do que corrige

E verifique o timestamp do executável antes de acreditar em um resultado de teste. Um build incremental que compila mas não relinka executa o binário anterior, o que fabricou uma rodada inteira de pistas falsas sobre sondas ausentes e saída duplicada. Ao depurar criptografia, um resultado inexplicável deve levantar "é este o binário que acabei de construir" antes de "o algoritmo está errado"

Desempenho, escopo e como chamar

A redução modular na unidade Brainpool é shift-subtract bit a bit em série a partir do bit mais alto definido do produto, então uma multiplicação custa aproximadamente na ordem da largura em bits. Uma verificação P-256 cai nas poucas centenas de milissegundos, o que é insignificante para assinar ou verificar documentos e seria inadequado para um terminador TLS. A redução de Barrett é a atualização óbvia e precisa de um valor de trabalho mais largo do que a representação atual carrega, então é uma mudança a fazer quando uma carga de trabalho 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, então a
  // política de nonce fica com o aplicativo
  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 do Brainpool recebe o nonce em vez de gerar um. Isso é deliberado: a geração de nonce é a coisa mais catastrófica de errar no ECDSA, já que um valor repetido ou previsível revela a chave privada, e a decisão sobre de onde vem a aleatoriedade pertence ao aplicativo e ao seu regime de conformidade, não a uma biblioteca PDF

Essas curvas ficam ao lado do trabalho pós-quântico descrito em o artigo sobre FIPS 204 ML-DSA, e se conectam ao mesmo pipeline de assinatura e validação coberto em assinatura e validação PAdES. Para certificados de teste nessas curvas, a rota de geração local é descrita em certificados autoassinados com CryptoAPI. A matriz completa de algoritmos está listada na página de produto da losLab PDF Developer Library