Technischer Artikel

Ed448 und Brainpool-ECDSA in Pure Pascal für PDF

PDFlibPas signiert und verifiziert mit Ed448 und mit den drei Brainpool-ECDSA-Kurven in reinem Object Pascal. Keine externe Kryptografiebibliothek, kein Plattform-Provider, keine DLL: PDFlibEd448 implementiert RFC 8032 PureEdDSA auf edwards448, und PDFlibBrainpool implementiert RFC 5639 brainpoolP256r1, brainpoolP384r1 und brainpoolP512r1. Beide wurden auf dieselbe Weise aufgebaut, gegen Known-Answer-Vektoren, die unabhängig generiert wurden, bevor auch nur eine Zeile Pascal geschrieben war, und beide sind vor allem wegen ihrer Fehler einen Artikel wert

Körperarithmetik ist ungewöhnlich ehrlicher Code. Sie stimmt entweder byteweise mit veröffentlichten Vektoren überein oder eben nicht, sodass es keinen Raum für „funktioniert größtenteils“ gibt. Schwierig ist, dass eine falsche Implementierung dennoch Signaturen erzeugt, die eigenen Signaturen verifiziert und dabei völlig plausibel aussieht

Warum diese Kurven und warum in Pascal

Brainpool-Kurven tauchen in europäischen Profilen für qualifizierte Signaturen auf, sodass eine Bibliothek, die Dokumente für diesen Markt signiert, sie nicht als Exoten abtun kann. Ed448 gehört zum Algorithmusset, das ISO/TS 32002 in PDF einführt, wobei der interne Digest SHAKE256 statt SHA-2 ist. Keine der beiden Familien ist in den gebräuchlichen Pascal-Kryptografiebibliotheken verfügbar, also muss eine PDF-Bibliothek, die sie haben will, sie selbst mitbringen

Das Deployment-Argument ist dasselbe wie bei der gesamten Kryptografie dieser Bibliothek: Eine Anwendung, die eine einzige Binärdatei ohne kryptografische Abhängigkeit ausliefert, hat keinen Provider zu erkennen, keine Version abzugleichen und kein Verhalten, das sich ändert, wenn der Host gepatcht wird. Signieren ist genau der Bereich, in dem Sie eine wandelbare Abhängigkeit am wenigsten brauchen

Konstanten stammen aus dem Spezifikationstext, niemals aus dem Gedächtnis

Der erste Versuch des edwards448-Basispunkts wurde aus dem Gedächtnis notiert und war falsch. Das ist kein bemerkenswerter Fehler, aber ein sehr teurer, denn ein falscher Basispunkt erzeugt ein selbstkonsistentes System: Ihre Schlüsselgenerierung, Ihr Signieren und Ihre Verifizierung stimmen untereinander überein und widersprechen der gesamten übrigen Welt

Das funktionierende Vorgehen lautet: Jeden Domänenparameter dem Spezifikationstext entnehmen und dann kreuzweise verifizieren. Für edwards448 bedeutet das Primzahl, Kurvenkonstante, Gruppenordnung und beide Dezimalkoordinaten des Basispunkts aus RFC 8032, konvertiert in die interne Limb-Darstellung, und dann abgeglichen gegen die veröffentlichten Testvektoren aus demselben Dokument. Für die Brainpool-Kurven heißt es Parameter aus RFC 5639, eine unabhängige Implementierung, die Vektoren generiert, und ein gegenläufiger Abgleich mit einer Systembibliothek, bevor irgendein Pascal lief

Die Domänenparameter von Ed448 und Brainpool fließen aus den Spezifikationstexten von RFC 8032 und RFC 5639 in Limb-Form und werden kreuzverifiziert, bevor Pascal läuft
Die Domänenparameter für edwards448 und die Brainpool-Kurven werden aus dem RFC-Text übernommen, in Limbs konvertiert und gegen unabhängige Vektoren kreuzverifiziert

Ein Ableitungs-Kürzel verdient eine Warnung, weil es universell aussieht und es nicht ist: Den Basispunkt aus einem festen y-Wert zurückzugewinnen funktioniert bei der 25519-Kurve und nicht bei edwards448, wo dieser Wert keine Quadratwurzel besitzt. Ein Skript widerlegte das in Sekunden, was viel billiger ist, als es im Debugger zu entdecken

Die Methode: ein Spiegel auf Limb-Ebene vor jeder Pascal-Transkription

Die Technik, die beide Units beherrschbar machte, ist eine Spiegelimplementierung in einer Sprache mit unbeschränkten Integern, von unten nach oben aufgebaut. Zuerst allein die Arithmetikschicht: Körper-Multiplikation, Subtraktion und Übertragsfortpflanzung, Stresstests gegen ihre algebraischen Invarianten über ein paar hundert Zufallsfälle. Dann die vollständige Schlüsselgenerierung im Spiegel, denn dort wohnen die semantischen Fehler, und dort sind sie billig zu finden. Erst danach die Pascal-Transkription

Workflow einer Spiegelimplementierung mit unbeschränkten Integern, die Pascal-Körperarithmetik und Schlüsselgenerierung für Ed448 und Brainpool validiert
Der Spiegel-Workflow von unten nach oben: zuerst Arithmetik, dann Schlüsselgenerierung im Spiegel, dann die Pascal-Transkription und der Vergleich von Zwischenwerten

Der Ertrag ist diagnostischer statt entwicklungsseitiger Natur. Sobald der Spiegel als korrekt bekannt ist, ist jede Diskrepanz zwischen Spiegel und Pascal ein Transkriptionsfehler, und das Abtasten desselben Zwischenwerts in beiden Implementierungen lokalisiert ihn sofort. Das verwandelt eine Fehlerklasse, die sonst kaum debuggbar ist – ein einzelner falscher Limb tief in einer Skalarmultiplikation –, in einen Fünf-Minuten-Vergleich

Vier Ursachen in Ed448

Alle vier wurden durch Abtasten von Zwischenwerten gefunden, und alle vier sind von der Sorte, die plausibel aussehende Ausgabe erzeugt

Die erste ist eine Notationsfalle. Die meisten veröffentlichten Formeln für die einheitliche Edwards-Addition gehen von einer Kurvenkonstante minus eins aus, und edwards448 hat plus eins. Unverändert übernommen wird der Zähler der y-Koordinate als Summe geschrieben, wo eine Differenz stehen sollte. Die Korrektur besteht nicht darin, das Vorzeichen zu patchen, sondern die inversionsfreie Produktform aus dem affinen Additionsgesetz der korrekten Kurve neu herzuleiten, was die vier Koordinatenausdrücke liefert und keinen Raum lässt, dass ein Vorzeichen aus der falschen Quelle geerbt wird

Die zweite steckt in der Punktdekompression. Die affine x-Koordinate aus projektiven Koordinaten zurückzugewinnen erfordert eine Multiplikation mit dem Kehrwert von Z. Multipliziert man mit dem quadrierten Kehrwert, erhält man einen Wert, der noch eine gültige projektive Darstellung ist und die falsche affine Koordinate liefert, sodass das Symptom ein korrektes y mit falschem x ist. Immer wenn eine Koordinate stimmt und die andere nicht, sitzt der Fehler in der Normalisierung, nicht in der Arithmetik

Die dritte ist eine von der kürzeren Kurve importierte Gewohnheit. Sowohl der Skalar pro Signatur als auch der Challenge-Skalar müssen aus dem vollständigen Digest reduziert werden, der bei Ed448 114 Bytes umfasst, nicht aus dessen ersten 57. Die 32-Byte-Kurve nutzt ebenfalls ihren vollen 64-Byte-Digest, also ist die Regel konsistent; falsch ist nur die Annahme, die halbe Digest-Länge sei die Skalarbreite

Die vierte ist die Reihenfolge. Das Domänentrennungs-Präfix kommt zuerst, vor dem Kontext-Präfix und der Nachricht, was nicht der Reihenfolge entspricht, die die intuitive Lesart von R und A in der Spezifikation nahelegt. Wer das verkehrt macht, erhält Signaturen, die gegen die eigene Implementierung und sonst gegen nichts verifizieren – der irreführendste denkbare Fehler

// Übertragsdesign im Körper: reine Fortpflanzung mit Floor-Semantik,
// damit positive und negative Limbs funktionieren und Subtraktion
// keinen Bias braucht. Der obere Übertrag läuft über
// 2^448 = 2^224 + 1 (mod p) zurück und berührt Limb 0 und Limb 8.
// Auf vier Runden begrenzt; in der Praxis zwei beobachtet
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, nicht Abschneiden
      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;

Eine frühere Version dieser Routine wendete vor der Fortpflanzung einen Bias an und faltete bei großen Eingaben einen Scheinübertrag falscher Größenordnung in die niedrigen Limbs. Bias-basierte Übertragsschemata sind eine hartnäckige Quelle dieser Fehlerklasse; Floor-Semantik mit begrenzter Repeat-Schleife lässt sich leichter durchdenken und ist messbar schnell genug

Zwei Ursachen in Brainpool

Die erste ist überhaupt keine Kryptografie. Die Arbeitsdarstellung umfasst 33 Limbs, das Produkt zweier Werte braucht also 66, und das Produkt-Array war mit 64 deklariert. Das Schreiben über das Ende hinaus beschädigte benachbarten Speicher, was sich zuerst als falsche Ergebnisse zeigte und erst nach Ergänzung eines breiteren Scans zum Absturz wurde. Die daraus abgeleitete Regel lohnt sich für jeden numerischen Puffer fester Größe: Er wird aus der Worst-Case-Produktbreite plus Reserve dimensioniert, und dann denkt man nie wieder darüber nach. Das Array im ausgelieferten Code umfasst 68 Limbs

Die zweite ist eine verdrehte Potenzierungsform. Es gibt zwei korrekte Square-and-Multiply-Formen, und sie konsumieren den Exponenten in entgegengesetzten Richtungen: Die Right-to-Left-Form multipliziert die Basis erst und quadriert sie dann und muss die Bits vom niederwertigen Ende lesen, während die Left-to-Right-Form erst quadriert und dann multipliziert und vom höchstwertigen Ende liest. Die Schleife zur modularen Inversion hatte einen Right-to-Left-Rumpf mit einem beim höchstwertigen Bit beginnenden Bit-Durchlauf. Beide Hälften sind Lehrbuch, die Kombination ist es nicht, und das Ergebnis ist ein falscher Kehrwert, der trotzdem wie ein plausibles Körperelement aussieht

Zwei Square-and-Multiply-Potenzierungsformen mit entgegengesetzten Bit-Richtungen und die gemischte Form, die falsche Brainpool-Modularinversen berechnete
Beide Square-and-Multiply-Formen sind für sich korrekt; ein Right-to-Left-Rumpf mit einem beim höchstwertigen Bit beginnenden Durchlauf liefert einen plausiblen falschen Kehrwert
// Jacobische Verdopplung und Addition, wenn der Ziel-Datensatz dieselbe
// Variable wie eine Quelle sein kann. Eine Kopie des ganzen Datensatzes
// beim Einstieg ist die einzige zuverlässige Verteidigung: Das Schreiben
// von Rs Limbs verunreinigt spätere Lesezugriffe auf P
procedure BPPointDouble(var R: TBPPoint; const P: TBPPoint;
  const Curve: TBPCurve);
var
  Pin: TBPPoint;
begin
  Pin := P;        // erst kopieren, dann ausschließlich mit Pin rechnen
  // ... M = 3X^2 + A*Z^4, S = 4*X*Y^2, X3 = M^2 - 2S, ...
end;

Zwei Prozesslektionen, die mehr kosteten als die Fehler

Inkrementelles Hotfixing konvergiert bei einer kryptografischen Unit nicht. Ein Entwurf wurde wiederholt geflickt, bis er 32 duplizierte Routinen und eine beschädigte Struktur mit sich trug, und er war nur durch Neuschreiben zu retten. Das Muster, das sich etabliert: entweder einmal aus einem validierten Spiegel schreiben oder neu schreiben; eine Sequenz lokaler Fixes an Arithmetik, die man noch nicht versteht, sammelt sich schneller an, als sie korrigiert

Und prüfen Sie den Zeitstempel der ausführbaren Datei, bevor Sie einem Testergebnis glauben. Ein inkrementeller Build, der kompiliert, aber nicht relinkt, führt die vorherige Binärdatei aus, was eine ganze Runde falscher Fährten über fehlende Sonden und duplizierte Ausgaben fabrizierte. Beim Debuggen von Kryptografie sollte ein unerklärliches Ergebnis zuerst die Frage „Ist das die Binärdatei, die ich gerade gebaut habe“ aufwerfen und erst danach „Ist der Algorithmus falsch“

Leistung, Umfang und der Aufruf

Die modulare Reduktion in der Brainpool-Unit ist eine bitserielle Shift-Subtract-Prozedur ab dem höchsten gesetzten Bit des Produkts, kostet eine Multiplikation also grob in der Größenordnung der Bitbreite. Eine P-256-Verifikation landet im niedrigen dreistelligen Millisekundenbereich, was für das Signieren oder Verifizieren von Dokumenten unauffällig ist und für einen TLS-Terminator unzureichend wäre. Barrett-Reduktion ist das naheliegende Upgrade und braucht einen breiteren Arbeitswert, als die aktuelle Darstellung mitführt, also eine Änderung für den Moment, in dem eine Workload sie verlangt, nicht prophylaktisch

uses
  PDFlibEd448, PDFlibBrainpool;

var
  PublicKey, Signature: AnsiString;
  Curve: TBPCurve;
  R, S, PubX, PubY: TBPValue;
begin
  // Ed448: PureEdDSA, intern SHAKE256, 57-Byte-Schlüssel
  if Ed448PublicKeyFromSeed(Seed, PublicKey) and
     Ed448Sign(DocumentDigest, Seed, Signature) then
    Assert(Ed448Verify(DocumentDigest, PublicKey, Signature));

  // Brainpool: der Aufrufer liefert die Nonce pro Signatur, die
  // Nonce-Richtlinie bleibt also bei der Anwendung
  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;

Beachten Sie, dass der Brainpool-Signierungseinstiegspunkt die Nonce entgegennimmt, statt eine zu generieren. Das ist Absicht: Die Nonce-Generierung ist der mit Abstand katastrophalste Fehler in ECDSA, denn ein wiederholter oder vorhersagbarer Wert legt den privaten Schlüssel offen, und die Entscheidung, woher die Zufälligkeit stammt, gehört zur Anwendung und ihrem Compliance-Regime, nicht zu einer PDF-Bibliothek

Diese Kurven gesellen sich zur Post-Quantum-Arbeit, die in dem Artikel zu FIPS 204 ML-DSA beschrieben ist, und sie stecken in dieselbe Signatur- und Validierungspipeline, die unter PAdES-Signierung und -Validierung behandelt wird. Für Testzertifikate auf diesen Kurven ist der Weg der lokalen Generierung unter selbstsignierte Zertifikate mit CryptoAPI beschrieben. Die vollständige Algorithmusmatrix finden Sie auf der Produktseite zur losLab PDF Developer Library