Article technique

SHAKE256 XOF en Delphi : séparation de domaine Keccak

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

Un contexte Keccak servant deux familles dans PDFlibPas : TSHA3Context stocke le séparateur de domaine dans un unique octet Suffix, SHA3Init règle le suffixe à $06 avec Rate dérivé comme 200 moins deux fois la longueur de sortie, et SHAKE256Init règle $1F avec la rate épinglée à 136 octets, donc SHA3AbsorbBlock, SHA3Update et SHA3Final servent les deux inchangés
Parce que le seul état par famille est un octet, les deux familles de hachage partagent une seule copie de la permutation à 24 tours, et les deux initialiseurs sont le seul endroit où les chemins divergent
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

Le cas de chevauchement pad10*1 dans PDFlibPas : PLShakeXOFFinalize place le suffixe de domaine à la position d'absorption courante et $80 au dernier octet du bloc ; quand la position tombe sur Rate moins 1, les deux écritures visent un octet, le XOR les combine en $9F et un simple store corromprait une longueur de message sur 136
Stocker au lieu de XORer laisse un octet de pad gagner, et parce que le défaut apparaît pour exactement une longueur de message sur 136, tous vos vecteurs de régression peuvent passer pendant que le padding est silencieusement faux
// 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

SHAKE en flux dans PDFlibPas via TPLShakeXOF, un enregistrement avec State, Rate, Suffix, un drapeau Absorbing et SqueezePos mais sans buffer de message : PLShakeXOFInit choisit SHAKE128 à rate 168 ou SHAKE256 à rate 136, PLShakeXOFAbsorb XOR les octets directement dans l'éponge, PLShakeXOFFinalize padde et permute, et PLShakeXOFSqueeze continue de permuter jusqu'à ce que l'appelant ait chaque octet
Absorber en plusieurs appels sans concaténer d'abord est ce qui laisse Ed448 tirer ses 114 octets et le port ML-DSA squeezer indéfiniment, une couverture qu'aucun point d'entrée one-shot avec son plafond de 136 octets ne pourrait supporter
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