PDFlibPas implémente SHA-3 et SHAKE256 sur une seule permutation Keccak-f[1600] en Object Pascal. Les deux familles diffèrent exactement en deux endroits : l'octet de domaine pad10*1, $06 pour SHA-3 et $1F pour SHAKE selon FIPS 202 §6.2 et §6.3, et la règle de rate. SHAKE256 épingle sa rate à 136 octets quelle que soit la longueur de sortie
Cette seconde différence est celle que les gens se trompent, parce qu'elle ne ressemble pas du tout à une différence. SHA3-256 tourne aussi à une rate de 136, donc un SHAKE256 construit en copiant le chemin de code SHA-3 passe chaque test de 32 octets que vous lui lancez puis produit du charabia la première fois que quelqu'un demande 64. Ce qui suit, c'est comment les deux familles ont fini par partager un seul enregistrement de contexte dans PDFlibDigest, où s'arrête le point d'entrée one-shot, et pourquoi le XOF en flux a dû exister avant que Ed448 ou ML-DSA ne puissent
Pourquoi SHA-3 et SHAKE256 partagent-ils un seul contexte Keccak ?
Parce que le seul état par famille est un unique octet, et PDFlibPas le stocke dans TSHA3Context comme Suffix. Une fois que le séparateur de domaine vit dans le contexte plutôt que d'être codé en dur dans la routine de finalisation, SHA3AbsorbBlock, SHA3Update et SHA3Final servent les deux familles inchangées, et il y a exactement une copie de la permutation à 24 tours dans la bibliothèque. Les initialiseurs sont là où les deux chemins divergent, et ils divergent complètement
Type
TSHA3Context= Record
State: Array [0.. 24] Of UInt64;
Buffer: Array [0.. 135] Of Byte;
BufferLength: Integer;
Rate: Integer;
OutputLength: Integer;
// Séparateur de domaine : $06 pour SHA3, $1F pour 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; // la rate suit la taille du digest
Context.Suffix:= $06;
End;
// SHAKE256 : la rate est fixée à 136 octets quoi que demande l'appelant,
// parce que SHAKE est une fonction à sortie extensible
Procedure SHAKE256Init(Var Context: TSHA3Context; OutputBytes: Integer);
Begin
FillChar(Context, SizeOf(Context), 0);
Context.OutputLength:= OutputBytes;
Context.Rate:= 136;
Context.Suffix:= $1F;
End;
La formule dans SHA3Init donne 136, 104 et 72 octets de rate pour SHA3-256, SHA3-384 et SHA3-512, donc la rate et le niveau de sécurité sont soudés ensemble pour la famille à longueur fixe. SHAKE256Init casse délibérément ce lien : OutputLength devient une demande d'octets, pas une déclaration de force de sécurité, et la capacité de l'éponge reste à 512 bits quelle que soit la quantité de sortie tirée. Ce découplage est la définition même d'une fonction à sortie extensible, et l'écrire comme deux initialiseurs sur un contexte rend la distinction visible dans la source plutôt qu'enterrée dans un commentaire
Le plafond de 136 octets sur le point d'entrée one-shot
Les fonctions de commodité dans PDFlibPas refusent tout ce qui sort de 1..136 octets, et cette limite est honnête plutôt qu'arbitraire. SHAKE256StreamRange et SHAKE256ByteArray passent toutes les deux par SHA3Final, qui padde le dernier bloc, permute une fois, et lit la sortie directement depuis l'état. Seuls les premiers Rate octets de cet état sont une sortie légale, donc une seule squeeze donne au plus 136 octets ; au-delà il vous faut une permutation fraîche par bloc. Les deux vrais consommateurs tiennent de toute façon dans la fenêtre : le profil de signature PDF digère 32 octets, et Ed448 en a besoin de 114 selon RFC 8032 §5.2. SHAKE256StreamRange prend le même triplet FirstLength, SecondStart, SecondLength que SHA256StreamRange et SHA3_256StreamRange, donc le code de validation parcourt un /ByteRange de signature de la même façon pour chaque algorithme et ne fait jamais de cas spécial pour le XOF
Qu'est-ce qui cloche dans pad10*1 quand la position tombe sur rate-1 ?
Rien, à condition de XORer les deux octets de pad dans l'état plutôt que de les assigner. FIPS 202 §B.2 met le suffixe de domaine à la position d'absorption courante et $80 au dernier octet du bloc. Quand le message laisse par hasard la position à Rate- 1, ces deux écritures visent le même octet et doivent se combiner en $9F. Stockez plutôt que XORez et l'un des deux gagne, le padding est silencieusement faux pour exactement une longueur de message sur 136, et tous les vecteurs de régression que vous possédez passent parce qu'aucun n'y tombe. PLShakeXOFFinalize dans PDFlibPas XOR les deux, ce qui fait que le cas de chevauchement sort du cas général sans aucune branche
// Padde le dernier bloc partiel et entre dans la phase de squeeze
Procedure PLShakeXOFFinalize(Var Ctx: TPLShakeXOF);
Begin
If Not Ctx.Absorbing Then
Exit;
Ctx.Absorbing:= False;
// suffixe à SqueezePos, $80 à Rate-1 ; quand SqueezePos = Rate-1 les deux
// XOR atterrissent sur le même octet et se combinent correctement en $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 : absorber directement dans l'éponge
PDFlibPas expose une sortie non bornée à travers un enregistrement séparé, TPLShakeXOF, tenant seulement State comme 25 mots UInt64, Rate, Suffix, un drapeau Absorbing et SqueezePos. Il n'y a pas de buffer de message : PLShakeXOFAbsorb XOR les octets entrants directement dans l'état et permute dès qu'un bloc est rempli, avec SqueezePos servant d'offset dans les deux phases. PLShakeXOFInit choisit la variante avec un booléen, True pour SHAKE128 à rate 168 et False pour SHAKE256 à rate 136, et PLShakeXOFSqueeze continue de permuter jusqu'à ce que l'appelant ait le nombre d'octets demandé. La sortie transversale aux blocs a été vérifiée à 200 octets, au-delà de la première frontière de bloc dans les deux variantes
Var
Ctx: TPLShakeXOF;
Digest: Array [0.. 113] Of Byte; // Ed448 veut 114 octets, 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, quand un buffer en entrée et un buffer en sortie suffisent :
// PLShakeXOF(False, Input, Output, 114)
Absorber en plusieurs appels compte plus qu'il n'y paraît. Ed448 préfixe le message avec une chaîne dom4 et une valeur de 57 octets avant de hacher, et un échantillonneur de rejet veut nourrir une graine et des octets d'index sans concaténer de chaînes d'abord. Avoir l'enregistrement en flux posé avant l'un ou l'autre consommateur était le but : le travail Ed448 et courbes Brainpool en Pascal pur a besoin de 114 octets en un coup, et le port FIPS 204 ML-DSA squeeze indéfiniment depuis les deux variantes. Ni l'un ni l'autre n'auraient pu être écrits contre l'API one-shot
Pourquoi PDFlibPas refuse-t-il d'exposer SHAKE256 comme sélecteur de digest ?
Parce qu'il n'y a aucun moyen légal de le choisir. TPDFlibDigestAlgorithm dans PDFlibDigitalSign exécute daAuto, daSHA1, daSHA256, daSHA384, daSHA512, daSHA3_256, daSHA3_384, daSHA3_512, et s'arrête. Il n'y a pas de daSHAKE256, et l'omission est une décision, pas un oubli. RFC 8702 enregistre SHAKE dans CMS, mais à l'intérieur d'une signature PDF, le seul endroit où ISO/TS 32002 met SHAKE256 est comme digest interne d'Ed448, où l'algorithme de signature le choisit et l'appelant ne le fait pas. Le publier comme valeur DigestAlgorithm libre laisserait n'importe qui apparier SHAKE256 avec RSA ou ECDSA et produire un dictionnaire de signature qu'aucun validateur n'acceptera. La capacité est livrée, le sélecteur ne l'est pas, et le code côté vérification appelle SHAKE256StreamRange sur le layout /ByteRange directement. Si vous choisissez un digest pour des signatures qu'une contrepartie doit valider aujourd'hui, daSHA256 à daSHA3_512 dans un workflow PAdES B-B à B-LTA avec horodatage et données de validation long terme reste la réponse ; ISO/TS 32001 est ce qui gouverne quand SHA-3 devient routinier dans le PDF du tout
Tester un XOF : le vecteur qui a l'air de passer
Comparez la longueur de sortie complète, et utilisez plus d'un vecteur. Le résultat SHAKE256 de la chaîne vide est celui que tout le monde mémorise, et c'est un test faible précisément parce qu'il est si largement cité. La sortie SHAKE256 de 32 octets pour abc est pire : ses 16 premiers octets, 483366601360a8771c6863080cc4114d, coïncident avec plusieurs vecteurs voisins, donc une comparaison tronquée rapporte un succès pendant que la moitié discriminante, 8db44530f8f1e1ee4f94ea37e78b5739, reste non vérifiée. La régression PDFlibPas compare les deux vecteurs sur toute leur longueur. Ajoutez aussi un cas transversal aux blocs, parce qu'une boucle de squeeze qui ne franchit jamais une frontière de rate n'a pas été testée du tout
La leçon plus large du pliage de SHAKE dans une implémentation SHA-3 existante, c'est que le code partagé n'a jamais été le risque. Keccak-f[1600] est la même permutation pour les deux familles et elle correspond aux vecteurs ou pas. Le risque vit dans les deux octets et un entier qui diffèrent, et dans le plafond que vous devez énoncer honnêtement plutôt que laisser les appelants le découvrir. Pour voir comment la couche digest s'insère à côté des outils de signature, de chiffrement et de PDF/A dans une base de code Object Pascal native, la page produit de la bibliothèque PDF Delphi PDFlibPas liste le jeu complet de composants et la matrice de compilateurs supportés