A PDFlibPas implementa SHA-3 e SHAKE256 numa única permutação Keccak-f[1600] em Object Pascal. As duas famílias diferem em exatamente dois sítios: o byte de domínio pad10*1, $06 para SHA-3 e $1F para SHAKE segundo a FIPS 202 §6.2 e §6.3, e a regra do rate. O SHAKE256 fixa o seu rate em 136 bytes independentemente do comprimento da saída
Essa segunda diferença é a que as pessoas erram, porque não parece diferença nenhuma. O SHA3-256 também corre a rate 136, pelo que um SHAKE256 construído copiando o caminho de código do SHA-3 passa em todos os testes de 32 bytes que lhe atirar e depois produz lixo na primeira vez que alguém pede 64. O que se segue é como as duas famílias acabaram por partilhar um record de contexto em PDFlibDigest, onde o ponto de entrada one-shot para, e porque é que o XOF em streaming teve de existir antes de o Ed448 ou o ML-DSA poderem
Porque é que SHA-3 e SHAKE256 partilham um contexto Keccak?
Porque o único estado por família é um único byte, e a PDFlibPas guarda-o em TSHA3Context como Suffix. Uma vez que o separador de domínio vive no contexto em vez de estar codificado à força na rotina de finalização, SHA3AbsorbBlock, SHA3Update e SHA3Final servem as duas famílias sem alterações, e há exatamente uma cópia da permutação de 24 rondas na biblioteca. Os inicializadores são onde os dois caminhos divergem, e divergem por completo
Type
TSHA3Context= Record
State: Array [0.. 24] Of UInt64;
Buffer: Array [0.. 135] Of Byte;
BufferLength: Integer;
Rate: Integer;
OutputLength: Integer;
// Separador de domínio: $06 para SHA3, $1F para SHAKE (FIPS 202 §6.2/6.3)
Suffix: Byte;
End;
Procedure SHA3Init(Var Context: TSHA3Context; OutputBits: Integer);
Begin
FillChar(Context, SizeOf(Context), 0);
Context.OutputLength:= OutputBits div 8;
Context.Rate:= 200- 2* Context.OutputLength; // o rate segue o tamanho do digest
Context.Suffix:= $06;
End;
// SHAKE256: o rate está fixo em 136 bytes seja o que o chamador pedir,
// porque o SHAKE é uma função de saída extensível
Procedure SHAKE256Init(Var Context: TSHA3Context; OutputBytes: Integer);
Begin
FillChar(Context, SizeOf(Context), 0);
Context.OutputLength:= OutputBytes;
Context.Rate:= 136;
Context.Suffix:= $1F;
End;
A fórmula em SHA3Init dá 136, 104 e 72 bytes de rate para SHA3-256, SHA3-384 e SHA3-512, pelo que rate e nível de segurança estão soldados na família de comprimento fixo. SHAKE256Init quebra deliberadamente essa ligação: OutputLength passa a ser um pedido de bytes, não uma declaração de força de segurança, e a capacidade da esponja fica em 512 bits independentemente de quanto saída se tirar. Esse desacoplamento é a definição de uma função de saída extensível, e escrevê-lo como dois inicializadores sobre um contexto torna a distinção visível no código em vez de enterrada num comentário
O teto de 136 bytes no ponto de entrada one-shot
As funções de conveniência na PDFlibPas recusam qualquer coisa fora de 1..136 bytes, e esse limite é honesto em vez de arbitrário. SHAKE256StreamRange e SHAKE256ByteArray passam ambas por SHA3Final, que faz o padding do último bloco, permuta uma vez e lê a saída diretamente do estado. Apenas os primeiros Rate bytes desse estado são saída legal, pelo que um squeeze produz no máximo 136 bytes; para lá disso precisa de uma permutação nova por bloco. Os dois consumidores reais cabem na mesma na janela: o perfil de assinatura PDF digere 32 bytes, e o Ed448 precisa de 114 segundo a RFC 8032 §5.2. SHAKE256StreamRange recebe o mesmo trio FirstLength, SecondStart, SecondLength que SHA256StreamRange e SHA3_256StreamRange, pelo que o código de validação percorre um /ByteRange de assinatura da mesma forma para todos os algoritmos e nunca faz caso especial do XOF
O que corre mal no pad10*1 quando a posição aterra em rate-1?
Nada, desde que faça XOR de ambos os bytes de pad no estado em vez de os atribuir. A FIPS 202 §B.2 coloca o suffix de domínio na posição atual de absorb e $80 no último byte do bloco. Quando a mensagem deixa por acaso a posição em Rate- 1, essas duas escritas apontam para o mesmo byte e têm de combinar em $9F. Armazenar em vez de fazer XOR e um deles ganha, o padding fica silenciosamente errado para exatamente um comprimento de mensagem em 136, e todos os vetores de regressão que tem passam porque nenhum aterra lá. O PLShakeXOFFinalize na PDFlibPas faz XOR de ambos, o que faz o caso de sobreposição sair do caso geral sem nenhum branch
// Faz o padding do último bloco parcial e entra na fase de squeeze
Procedure PLShakeXOFFinalize(Var Ctx: TPLShakeXOF);
Begin
If Not Ctx.Absorbing Then
Exit;
Ctx.Absorbing:= False;
// suffix em SqueezePos, $80 em Rate-1; quando SqueezePos = Rate-1 os dois
// XORs caem no mesmo byte e combinam corretamente em $9F
Ctx.State[Ctx.SqueezePos div 8]:= Ctx.State[Ctx.SqueezePos div 8]xor
(UInt64(Ctx.Suffix)shl (8* (Ctx.SqueezePos and 7)));
Ctx.State[(Ctx.Rate- 1)div 8]:= Ctx.State[(Ctx.Rate- 1)div 8]xor
(UInt64($80)shl (8* ((Ctx.Rate- 1)and 7)));
SHA3Permute(Ctx.State);
Ctx.SqueezePos:= 0;
End;
TPLShakeXOF: absorver diretamente na esponja
A PDFlibPas expõe saída sem limite através de um record separado, TPLShakeXOF, que guarda apenas State como 25 palavras UInt64, Rate, Suffix, um flag Absorbing e SqueezePos. Não há buffer de mensagem: PLShakeXOFAbsorb faz XOR dos bytes de entrada diretamente no estado e permuta sempre que enche um bloco, com SqueezePos a servir de deslocamento nas duas fases. PLShakeXOFInit escolhe a variante com um boolean, True para SHAKE128 a rate 168 e False para SHAKE256 a rate 136, e PLShakeXOFSqueeze continua a permutar até o chamador ter a contagem de bytes que pediu. A saída entre blocos foi verificada em 200 bytes, para lá da primeira fronteira de bloco nas duas variantes
Var
Ctx: TPLShakeXOF;
Digest: Array [0.. 113] Of Byte; // O Ed448 quer 114 bytes, RFC 8032 §5.2
Begin
PLShakeXOFInit(Ctx, False); // False = SHAKE256, rate 136
PLShakeXOFAbsorb(Ctx, @Dom4[1], Length(Dom4));
PLShakeXOFAbsorb(Ctx, @Prefix[1], Length(Prefix));
PLShakeXOFAbsorb(Ctx, @Msg[1], Length(Msg));
PLShakeXOFFinalize(Ctx);
PLShakeXOFSqueeze(Ctx, @Digest[0], SizeOf(Digest));
End;
// Ou, quando um buffer de entrada e um de saída é tudo o que precisa:
// PLShakeXOF(False, Input, Output, 114)
Absorver em várias chamadas importa mais do que parece. O Ed448 prefixa a mensagem com uma string dom4 e um valor de 57 bytes antes de fazer hash, e um rejection sampler quer alimentar uma seed e bytes de índice sem concatenar strings primeiro. O record de streaming chegar antes de qualquer consumidor era o ponto: o trabalho de curvas Ed448 e Brainpool em Pascal puro precisa de 114 bytes de uma vez, e o port ML-DSA FIPS 204 faz squeeze indefinidamente das duas variantes. Nenhum podia ter sido escrito contra a API one-shot
Porque é que a PDFlibPas se recusa a expor SHAKE256 como seletor de digest?
Porque não há forma legal de o escolher. TPDFlibDigestAlgorithm em PDFlibDigitalSign corre daAuto, daSHA1, daSHA256, daSHA384, daSHA512, daSHA3_256, daSHA3_384, daSHA3_512, e para. Não há daSHAKE256, e a omissão é uma decisão, não um descuido. A RFC 8702 regista o SHAKE em CMS, mas dentro de uma assinatura PDF o único sítio onde a ISO/TS 32002 coloca SHAKE256 é como digest interno do Ed448, onde o algoritmo de assinatura o escolhe e o chamador não. Publicá-lo como um valor DigestAlgorithm autónomo deixaria qualquer um emparelhar SHAKE256 com RSA ou ECDSA e produzir um dicionário de assinatura que nenhum validador aceita. A capacidade vem, o seletor não, e o código do lado da verificação chama SHAKE256StreamRange sobre o layout /ByteRange diretamente. Se está a escolher um digest para assinaturas que uma contraparte tem de validar hoje, daSHA256 até daSHA3_512 dentro de um workflow PAdES B-B a B-LTA com timestamping e dados de validação de longo prazo continua a ser a resposta; a ISO/TS 32001 é o que governa quando o SHA-3 se torna rotina no PDF de todo
Testar um XOF: o vetor que parece que passa
Compare o comprimento completo da saída e use mais do que um vetor. O resultado SHAKE256 da string vazia é o que toda a gente memoriza, e é um teste fraco precisamente por ser tão citado. A saída SHAKE256 de 32 bytes para abc é pior: os seus primeiros 16 bytes, 483366601360a8771c6863080cc4114d, coincidem com vários vetores vizinhos, pelo que uma comparação truncada reporta sucesso enquanto a metade discriminante, 8db44530f8f1e1ee4f94ea37e78b5739, fica por verificar. A regressão da PDFlibPas compara os dois vetores em todo o seu comprimento. Acrescente também um caso entre blocos, porque um loop de squeeze que nunca cruza uma fronteira de rate não foi testado de todo
A lição mais ampla de dobrar o SHAKE numa implementação SHA-3 existente é que o código partilhado nunca foi o risco. O Keccak-f[1600] é a mesma permutação para as duas famílias, e ou corresponde aos vetores ou não. O risco vive nos dois bytes e num inteiro que diferem, e no teto que tem de afirmar honestamente em vez de deixar os chamadores descobrirem. Para ver como a camada de digest se encaixa ao lado das ferramentas de assinatura, encriptação e PDF/A num codebase Object Pascal nativo, a página do produto PDF Library para Delphi lista o conjunto completo de componentes e a matriz de compiladores suportados