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
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
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
// 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