PDFlibPas ondertekent en verifieert met Ed448 en met de drie Brainpool ECDSA-curven in puur Object Pascal. Geen externe cryptografische bibliotheek, geen platformprovider, geen DLL: PDFlibEd448 implementeert RFC 8032 PureEdDSA op edwards448 en PDFlibBrainpool implementeert RFC 5639 brainpoolP256r1, brainpoolP384r1 en brainpoolP512r1. Beide zijn op dezelfde manier gebouwd, tegen known-answer vectors die onafhankelijk zijn gegenereerd voordat er ook maar één regel Pascal was geschreven, en beide zijn vooral het schrijven waard vanwege de bugs
Rekenkunde op veldniveau is ongebruikelijk eerlijke code. Die komt ofwel byte voor byte overeen met gepubliceerde vectors, ofwel niet, dus er is geen ruimte voor "grotendeels werkend". Wat het lastig maakt, is dat een foutieve implementatie nog steeds handtekeningen produceert, nog steeds de eigen handtekeningen verifieert en nog steeds volkomen plausibel oogt
Waarom deze curven, en waarom in Pascal
Brainpool-curven komen voor in Europese profielen voor gekwalificeerde handtekeningen, dus een bibliotheek die documenten voor die markt ondertekent kan ze niet als exotisch beschouwen. Ed448 zit in de verzameling algoritmen die ISO/TS 32002 aan PDF toevoegt, waarbij de interne digest SHAKE256 is in plaats van SHA-2. Geen van beide families is beschikbaar in de gangbare Pascal-cryptografiebibliotheken, dus een PDF-bibliotheek die ze wil gebruiken moet ze zelf bezitten
Het argument voor de uitrol is hetzelfde als bij alle cryptografie van deze bibliotheek: een applicatie die één binair bestand levert zonder cryptografische afhankelijkheid heeft geen provider om te detecteren, geen versie om te matchen en geen gedrag dat verandert zodra de host wordt gepatcht. Ondertekenen is precies het gebied waar u het minst behoefte hebt aan een wisselende afhankelijkheid
Constanten komen uit de specificatietekst, nooit uit het geheugen
De eerste poging tot het basispunt van edwards448 is uit het geheugen opgeschreven en was fout. Dat is geen opmerkelijke fout, maar wel een zeer dure, want een verkeerd basispunt levert een zelfconsistent systeem op: uw sleutelgeneratie, ondertekening en verificatie zijn het onderling eens en oneens met de rest van de wereld
De werkende procedure is om elke domeinparameter uit de specificatietekst te halen en die vervolgens kruislings te verifiëren. Voor edwards448 betekent dat de priem, de curveconstante, de groepenorde en beide decimale coördinaten van het basispunt uit RFC 8032, omgezet naar de interne limb-representatie en daarna gecontroleerd tegen gepubliceerde testvectoren uit hetzelfde document. Voor de Brainpool-curven betekent het de parameters uit RFC 5639, een onafhankelijke implementatie die vectoren genereerde, en een kruiscontrole tegen een systeembibliotheek in beide richtingen voordat er ook maar één regel Pascal draaide
Eén afkorting bij de afleiding verdient een waarschuwing, omdat die universeel oogt en dat niet is: het basispunt terughalen uit een vaste y-waarde werkt voor de 25519-curve en niet voor edwards448, waar die waarde geen wortel heeft. Een script weerlegde dat in seconden, wat veel goedkoper is dan het via een debugger te ontdekken
De methode: een mirror op limb-niveau vóór elke regel Pascal
De techniek die beide units hanteerbaar maakte, is een mirror-implementatie in een taal met onbegrensde integers, bottom-up opgebouwd. Eerst alleen de rekenlaag: vermenigvuldigen in het veld, aftrekken en carry-propagatie, getest tegen hun algebraïsche invarianten over een paar honderd willekeurige gevallen. Daarna de volledige sleutelgeneratie binnen de mirror, want daar huizen de semantische bugs en daar zijn ze goedkoop te vinden. Pas daarna de transscriptie naar Pascal
Het rendement is diagnostisch in plaats van constructief. Zodra de mirror als correct bekendstaat, is elke discrepantie tussen mirror en Pascal een transscriptiefout, en het opvragen van dezelfde tussenwaarde in beide implementaties lokaliseert die onmiddellijk. Daarmee wordt een categorie bugs die vrijwel niet te debuggen is, één verkeerde limb diep in een scalaire vermenigvuldiging, teruggebracht tot een vergelijking van vijf minuten
Vier oorzaken in Ed448
Alle vier zijn gevonden door tussenwaarden te inspecteren, en alle vier zijn van het soort dat geldig ogende uitvoer oplevert
De eerste is een valkuil in de notatie. De meeste gepubliceerde formules voor unified Edwards-additie gaan uit van een curveconstante van min één, terwijl edwards448 plus één heeft. Onveranderd overgenomen staat de teller van de y-coördinaat als een som waar het een verschil had moeten zijn. De oplossing is niet het teken te patchen, maar de vorm zonder inversie opnieuw af te leiden uit de affiene optelwet voor de juiste curve, wat de vier coördinaatuitdrukkingen oplevert en geen ruimte laat voor een teken dat uit de verkeerde bron is overgenomen
De tweede zit in de puntdecompressie. Het affiene x terughalen uit projectieve coördinaten vereist één vermenigvuldiging met de inverse van Z. Vermenigvuldigen met het kwadraat van de inverse levert een waarde op die nog steeds een geldige projectieve representatie is, maar de verkeerde affiene coördinaat, dus het symptoom is een correcte y met een verkeerde x. Wanneer de ene coördinaat klopt en de andere niet, zit de bug in de normalisatie en niet in de rekenkunde
De derde is een gewoonte overgeërfd van de kortere curve. Zowel de scalaire waarde per handtekening als de challenge-scalar moet gereduceerd worden vanuit de volledige digest, die bij Ed448 114 bytes telt, en niet vanuit de eerste 57. De curve van 32 bytes gebruikt ook haar volledige digest van 64 bytes, dus de regel is consistent; alleen de aanname dat "de helft van de digest de breedte van de scalar is", is fout
De vierde is de volgorde. Het domeinscheidingsprefix komt eerst, vóór het contextprefix en het bericht, wat niet de volgorde is die de intuïtieve lezing van R en A in de specificatie suggereert. Als dit fout gaat, ontstaan handtekeningen die verifiëren tegen uw eigen implementatie en niets anders, en dat is de meest misleidende vorm van falen die denkbaar is
// Ontwerp van de veld-carry: pure propagatie met floor-semantics, zodat
// zowel positieve als negatieve limbs werken en aftrekken geen bias nodig heeft.
// De carry bovenaan loopt via 2^448 = 2^224 + 1 (mod p) terug, wat
// limb 0 en limb 8 raakt. Begrensd op vier rondes; twee waargenomen
// in de praktijk
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]); // floor, geen afkapping
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;
Een eerdere versie van die routine paste een bias toe vóór het propageren, en bij grote inputs vouwde die een spookcarry van verkeerde grootte in de lage limbs. Carry-schema's op basis van bias zijn een hardnekkige bron van dit soort defecten; floor-semantics met een begrensde repeat-lus is overzichtelijker om over te redeneren en meetbaar snel genoeg
Twee oorzaken in Brainpool
De eerste is helemaal geen cryptografie. De gebruikte representatie telt 33 limbs, dus het product van twee waarden vraagt er 66, en de productarray was gedeclareerd met 64. Schrijven voorbij het einde corrumpeerde het aangrenzende geheugen, wat zich eerst openbaarde als verkeerde resultaten en pas in een crash veranderde toen een bredere scan werd toegevoegd. De regel die daaruit voortkwam is het toepassen waard op elke numerieke buffer met vaste grootte: dimensioneer die op de productbreedte in het slechtste geval, tel marge toe en denk er daarna nooit meer over na. De array in de uitgeleverde code telt 68 limbs
De tweede is een door elkaar gehaalde machtsverheffingsvorm. Er zijn twee correcte square-and-multiply-vormen en ze consumeren de exponent in tegengestelde richtingen: de right-to-left-vorm vermenigvuldigt eerst en verheft daarna de basis, en moet bits vanaf de minst significante kant lezen, terwijl de left-to-right-vorm eerst kwadrateert en dan vermenigvuldigt en vanaf de meest significante kant leest. De lus voor modulaire inversie had een right-to-left-body met een bitwandeling die bij de meest significante bit begon. Beide helften zijn textbook, de combinatie niet, en het resultaat is een verkeerde inverse die nog steeds oogt als een plausibel veldelement
// Jacobiaans verdubbelen en optellen waarbij het doelrecord dezelfde
// variabele kan zijn als een bron. Een volledige recordkopie bij binnenkomst
// is de enige betrouwbare verdediging: het wegschrijven van de limbs van R
// vervuilt latere lezingen van P
procedure BPPointDouble(var R: TBPPoint; const P: TBPPoint;
const Curve: TBPCurve);
var
Pin: TBPPoint;
begin
Pin := P; // eerst kopiëren, daarna alleen vanuit Pin rekenen
// ... M = 3X^2 + A*Z^4, S = 4*X*Y^2, X3 = M^2 - 2S, ...
end;
Twee proceslessen die meer kostten dan de bugs
Incrementeel hotfixen convergeert niet bij een cryptografische unit. Eén versie werd herhaaldelijk gepatcht tot die 32 gedupliceerde routines en een beschadigde structuur telde, en alleen een herschrijving loste het op. Het patroon om aan te houden is: of schrijf het één keer vanuit een gevalideerde mirror, of herschrijf het; een reeks lokale fixes aan rekenkunde die u nog niet doorgrondt stapelt zich sneller op dan dat die corrigeert
En controleer de tijdstempel van het uitvoerbare bestand voordat u een testresultaat gelooft. Een incrementele build die compileert maar niet linkt, draait de vorige binary, wat een complete ronde valse sporen opleverde over ontbrekende probes en gedupliceerde uitvoer. Bij het debuggen van cryptografie hoort een onverklaard resultaat eerst de vraag op te roepen "is dit de binary die ik net heb gebouwd" en pas daarna "klopt het algoritme niet"
Prestaties, reikwijdte en hoe u het aanroept
Modulaire reductie in de Brainpool-unit gebeurt bitserieel met shift-subtract vanaf de hoogst gezette bit van het product, dus een vermenigvuldiging kost grofweg een veelvoud van de bitbreedte. Een P-256-verificatie landt in de lage honderden milliseconden, wat voor het ondertekenen of verifiëren van documenten onopvallend is en voor een TLS-terminator ontoereikend zou zijn. Barrett-reductie is de voor de hand liggende upgrade en vraagt een werkwaarde die breder is dan de huidige representatie voert, dus dat is een wijziging om te maken zodra een workload erom vraagt, niet preventief
uses
PDFlibEd448, PDFlibBrainpool;
var
PublicKey, Signature: AnsiString;
Curve: TBPCurve;
R, S, PubX, PubY: TBPValue;
begin
// Ed448: PureEdDSA, intern SHAKE256, sleutels van 57 bytes
if Ed448PublicKeyFromSeed(Seed, PublicKey) and
Ed448Sign(DocumentDigest, Seed, Signature) then
Assert(Ed448Verify(DocumentDigest, PublicKey, Signature));
// Brainpool: de aanroeper levert de nonce per handtekening, dus het
// nonce-beleid blijft bij de applicatie
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;
Merk op dat het ondertekeningstoegangspunt van Brainpool de nonce als parameter neemt in plaats van er een te genereren. Dat is opzettelijk: nonce-generatie is het enige meest catastrofale dat u in ECDSA verkeerd kunt doen, want een herhaalde of voorspelbare waarde legt de privésleutel bloot, en de beslissing waar willekeur vandaan komt, hoort bij de applicatie en haar compliance-regime, niet bij een PDF-bibliotheek
Deze curven staan naast het post-quantumwerk dat in het artikel over FIPS 204 ML-DSA wordt beschreven, en ze koppelen aan dezelfde pijplijn voor ondertekening en validatie die in PAdES-ondertekening en -validatie wordt behandeld. Voor testcertificaten op deze curven wordt de route voor lokale generatie beschreven in zelfondertekende certificaten met CryptoAPI. De volledige algoritmematrix staat op de productpagina van de losLab PDF Developer Library