A PDFlibPas implementa SHA-3 e SHAKE256 sobre uma única permutação Keccak-f[1600] em Object Pascal. As duas famílias diferem em exatamente dois lugares: o byte de domínio pad10*1, $06 para SHA-3 e $1F para SHAKE conforme FIPS 202 §6.2 e §6.3, e a regra de rate. O SHAKE256 fixa seu rate em 136 bytes independentemente do comprimento da saída
Essa segunda diferença é a que as pessoas erram, porque ela não parece uma diferença de jeito nenhum. O SHA3-256 também roda em rate 136, então um SHAKE256 construído copiando o caminho de código do SHA-3 passa em todo teste de 32 bytes que você jogar nele e depois produz lixo na primeira vez que alguém pede 64. O que vem a seguir é como as duas famílias acabaram compartilhando um único record de contexto em PDFlibDigest, onde o ponto de entrada one-shot para, e por que o XOF streaming precisava existir antes de Ed448 ou ML-DSA poderem
Por que SHA-3 e SHAKE256 compartilham um único contexto Keccak?
Porque o único estado por família é um único byte, e a PDFlibPas o armazena em TSHA3Context como Suffix. Uma vez que o separador de domínio vive no contexto em vez de estar hard-coded na rotina de finalização, SHA3AbsorbBlock, SHA3Update e SHA3Final servem às duas famílias sem mudanças, e há exatamente uma cópia da permutação de 24 rodadas na biblioteca. Os initializers são onde os dois caminhos divergem, e divergem completamente
Type
TSHA3Context= Record
State: Array [0.. 24] Of UInt64;
Buffer: Array [0.. 135] Of Byte;
BufferLength: Integer;
Rate: Integer;
OutputLength: Integer;
// Domain separator: $06 for SHA3, $1F for 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 acompanha o tamanho do digest
Context.Suffix:= $06;
End;
// SHAKE256: o rate é fixo em 136 bytes, não importa o que o chamador peça,
// porque 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, então rate e nível de segurança ficam soldados juntos na família de comprimento fixo. SHAKE256Init deliberadamente quebra esse vínculo: OutputLength vira um pedido de bytes, não uma declaração de força de segurança, e a capacidade do sponge fica em 512 bits não importa quanta saída você extraia. Esse desacoplamento é a definição de uma função de saída extensível, e escrevê-la como dois initializers sobre um contexto torna a distinção visível no código-fonte em vez de enterrada em um 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 ambas passam por SHA3Final, que faz o pad do último bloco, permuta uma vez e lê a saída direto do estado. Apenas os primeiros Rate bytes desse estado são saída legal, então um squeeze rende no máximo 136 bytes; além disso você precisa de uma permutação nova por bloco. Os dois consumidores reais cabem na janela de qualquer forma: o perfil de assinatura de PDF digere 32 bytes, e o Ed448 precisa de 114 conforme RFC 8032 §5.2. SHAKE256StreamRange recebe a mesma trinca FirstLength, SecondStart, SecondLength de SHA256StreamRange e SHA3_256StreamRange, então o código de validação percorre um /ByteRange de assinatura do mesmo jeito para todo algoritmo e nunca faz special-case do XOF
O que dá errado no pad10*1 quando a posição cai em rate-1?
Nada, desde que você faça XOR dos dois bytes de pad no estado em vez de atribuí-los. 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 por acaso deixa a posição em Rate- 1, essas duas escritas miram o mesmo byte e precisam se combinar em $9F. Armazene em vez de fazer XOR e um deles vence, o padding fica silenciosamente errado para exatamente um comprimento de mensagem em 136, e todo vetor de regressão que você tem passa porque nenhum deles cai lá. PLShakeXOFFinalize na PDFlibPas faz XOR dos dois, o que faz o caso de sobreposição sair do caso geral sem branch
// Aplica padding no último bloco parcial e entra na fase de squeeze
Procedure PLShakeXOFFinalize(Var Ctx: TPLShakeXOF);
Begin
If Not Ctx.Absorbing Then
Exit;
Ctx.Absorbing:= False;
// sufixo em SqueezePos, $80 em Rate-1; quando SqueezePos = Rate-1 os dois
// Os XORs caem no mesmo byte e se 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: absorvendo direto no sponge
A PDFlibPas expõe saída ilimitada através de um record separado, TPLShakeXOF, guardando apenas State como 25 palavras UInt64, Rate, Suffix, uma flag Absorbing e SqueezePos. Não há buffer de mensagem: PLShakeXOFAbsorb faz XOR dos bytes de entrada diretamente no estado e permuta sempre que preenche um bloco, com SqueezePos servindo de offset nas duas fases. PLShakeXOFInit escolhe a variante com um boolean, True para SHAKE128 em rate 168 e False para SHAKE256 em rate 136, e PLShakeXOFSqueeze continua permutando até quem chama ter a contagem de bytes que pediu. Saída cruzando blocos foi checada em 200 bytes, além da primeira fronteira de bloco nas duas variantes
Var
Ctx: TPLShakeXOF;
Digest: Array [0.. 113] Of Byte; // Ed448 wants 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 que você 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 hashear, e um rejection sampler quer alimentar uma seed e bytes de índice sem concatenar strings antes. Ter o record streaming pronto antes de qualquer um dos consumidores era o ponto: o trabalho de curvas Ed448 e Brainpool em Pascal puro precisa de 114 bytes de uma vez, e o port de ML-DSA FIPS 204 faz squeeze indefinidamente das duas variantes. Nenhum dos dois poderia ter sido escrito contra a API one-shot
Por que a PDFlibPas se recusa a expor SHAKE256 como seletor de digest?
Porque não há um jeito legal de escolhê-lo. TPDFlibDigestAlgorithm em PDFlibDigitalSign roda daAuto, daSHA1, daSHA256, daSHA384, daSHA512, daSHA3_256, daSHA3_384, daSHA3_512, e para. Não existe daSHAKE256, e a omissão é uma decisão, não um descuido. A RFC 8702 registra SHAKE em CMS, mas dentro de uma assinatura de PDF o único lugar onde a ISO/TS 32002 coloca SHAKE256 é como o digest interno do Ed448, onde o algoritmo de assinatura o escolhe e quem chama não escolhe. Publicá-lo como um valor DigestAlgorithm autônomo deixaria qualquer um combinar SHAKE256 com RSA ou ECDSA e produzir um dicionário de assinatura que nenhum validador vai aceitar. A capacidade vem, o seletor não, e o código do lado da verificação chama SHAKE256StreamRange sobre o layout /ByteRange diretamente. Se você está escolhendo um digest para assinaturas que uma contraparte precisa validar hoje, daSHA256 até daSHA3_512 dentro de um workflow PAdES B-B até B-LTA com timestamping e dados de validação de longo prazo continua sendo a resposta; a ISO/TS 32001 é o que governa quando SHA-3 vira rotina em PDF de qualquer forma
Testando um XOF: o vetor que parece passar
Compare o comprimento completo da saída, e use mais de um vetor. O resultado SHAKE256 da string vazia é o que todo mundo memoriza, e é um teste fraco precisamente porque é tão citado. A saída SHAKE256 de 32 bytes para abc é pior: seus primeiros 16 bytes, 483366601360a8771c6863080cc4114d, coincidem com vários vetores vizinhos, então uma comparação truncada reporta sucesso enquanto a metade discriminante, 8db44530f8f1e1ee4f94ea37e78b5739, fica sem checagem. A regressão da PDFlibPas compara os dois vetores no comprimento inteiro. Adicione também um caso cruzando blocos, porque um loop de squeeze que nunca cruza uma fronteira de rate não foi testado de jeito nenhum
A lição mais ampla de dobrar o SHAKE em uma implementação SHA-3 existente é que o código compartilhado nunca foi o risco. Keccak-f[1600] é a mesma permutação para as duas famílias e ou ela bate com os vetores ou não bate. O risco vive nos dois bytes e no inteiro que diferem, e no teto que você precisa enunciar honestamente em vez de deixar quem chama descobrir. Para ver como a camada de digest se encaixa junto com o tooling de assinatura, criptografia e PDF/A em um codebase nativo em Object Pascal, a página do produto da biblioteca PDF Delphi PDFlibPas lista o conjunto completo de componentes e a matriz de compiladores suportados