Teknisk artikel

ECDSA-signaturverifiering i Delphi: DER till P1363

Ett ECDSA-signatureValue inuti en CMS-behållare är en DER SEQUENCE { INTEGER r, INTEGER s }. Windows CNG-funktionen BCryptVerifySignature accepterar inget av detta: den vill ha IEEE P1363 fast bredd r || s, utan taggar och utan längder. HotPDF, den native VCL-PDF-komponenten för Delphi och C++Builder, konverterar mellan de två enligt strikta DER-regler innan en nyckel importeras

Felet detta förhindrar är specifikt och nedslående. Acrobat öppnar dokumentet och visar en grön bock. Din egen verifierare, som går igenom samma byte, returnerar ogiltig, eller CNG lämnar tillbaka STATUS_INVALID_SIGNATURE utan vidare förklaring. Det är inget fel på signaturen. Det som är fel är att ungefär sjuttio byte ASN.1 skickades till ett API som förväntade sig sextiofyra byte rått heltal, och missmatchningen är osynlig om du inte vet att leta efter den

Varför avvisar BCryptVerifySignature en giltig ECDSA-signatur?

Därför att de två sidorna av anropet talar olika signaturkodningar, och ingen av dem meddelar det. ISO 32000-1 §12.8 säger att en signaturordbok bär en CMS-blob i /Contents; RFC 5652 §5.3 säger att signatureValue i varje SignerInfo är en OCTET STRING vars innehåll är vad signaturalgoritmen definierar. För ECDSA är det innehållet SEC 1 DER-strukturen: en SEQUENCE som håller två INTEGER. Den har variabel längd by design, eftersom r och s är heltal och DER strippar inledande nollbyte från heltal

IEEE P1363 tar motsatt ståndpunkt. Den definierar signaturen som sammanslagningen av de två koordinaterna, var och en vänsterutfylld med nollor till exakt kurvfältets bytebredd. En P-256-signatur är alltid 64 byte. En DER-kodning av samma signatur är normalt 70 eller 71 byte och kan vara någonstans mellan cirka 8 och 72. Skicka DER-formen till BCryptVerifySignature och enbart längdkontrollen dömer ut anropet, vilket är varför HotPDF normaliserar innan den verifierar snarare än efteråt

uses
  HPDFECDSA;

// Converts a CMS signatureValue into the fixed-width form CNG expects.
// ARaw comes back as 64 bytes for P-256, 96 for P-384, 132 for P-521.
function ToP1363(const ADerSig: TBytes; ACurve: THPDFECDSACurve;
  out ARaw: TBytes): Boolean;
begin
  Result := HPDFECDSANormalizeSignature(ADerSig, ACurve, eseDER, ARaw);
end;

DER-reglerna en signaturparser inte får slappna av på

Varje avvisning som listas här är en avvisning HotPDF utför medvetet, och var och en stänger en väg som en överseende parser skulle lämna öppen. Frestelsen när man skriver en konverterare är att hitta de två INTEGER-noderna, kopiera deras innehåll och gå vidare. Det fungerar på välformad indata och accepterar tyst en hel familj av manipulerbara omkodningar på fientlig indata. Så HPDFECDSANormalizeSignature avvisar ett negativt heltal, det vill säga varje r eller s vars första innehållsbyte har den höga biten satt, eftersom en giltig ECDSA-skalär är positiv. Den avvisar ett värde som helt är noll, eftersom r = 0 eller s = 0 aldrig är en legitim signatur. Den avvisar ett överflödigt inledande nollbyte: X.690 §8.3 tillåter exakt ett, och bara när nästa byte annars skulle läsas som negativt, så ett 00 följt av ett byte under 0x80 är en omkodning, inte en signatur. Den avvisar ett icke-minimalt längdhuvud, eftersom X.690 §10.1 kräver den bestämda formen kodad i så få byte som möjligt, och en långform-längd som kunde varit kortform är en annan bytesträng med samma betydelse. Den avvisar ett heltal bredare än kurvans koordinatstorlek, eftersom det värdet inte kan vara ett fältelement. Och den avvisar varje efterföljande nod efter s, tillsammans med en yttre SEQUENCE vars totala längd inte motsvarar längden på hela blobben

De två sista spelar större roll än de ser ut att göra. Byte som följer efter SEQUENCE är det klassiska tricket för signaturmanipulerbarhet: lägg till skräp, och en tillåtande verifierare säger ändå giltig medan bytesträngen den validerade inte är den bytesträng som signerades. Samma instinkt driver den ASN.1-längdhärdning som beskrivs i anteckningen om PKCS#12-tolkning, och det är samma instinkt här. I en verifieringsväg är en accepterad struktur som aldrig sändes ut av en efterlevande signerare ett fel, inte en artighet

Koordinatbredden hör till kurvan, inte till signaturen

HotPDF härleder utdatabredden från den namngivna kurv-OID:n, aldrig från längden på DER:et den just tolkat. Detta är den andra halvan av konverteringen och den halva som är lätt att få subtilt fel. RFC 5480 §2.1.1 identifierar kurvan i certifikatets SubjectPublicKeyInfo-parametrar, och HPDFECDSACurveFromOID mappar de tre OID:erna HotPDF stödjer: 1.2.840.10045.3.1.7 för P-256, 1.3.132.0.34 för P-384, och 1.3.132.0.35 för P-521. HPDFECDSACoordinateSize returnerar sedan 32, 48 eller 66 byte, och P1363-bufferten är dubbelt så stor: 64, 96 eller 132. Varje avkodat heltal högerjusteras in i sin halva, så ett kort r nollutfylls till vänster snarare än förskjuts. P-521 är den som fångar folk, eftersom 521 bitar är 65,125 byte och avrundas uppåt till 66, vilket ger en 132-byte-signatur som ingen tvåpotensintuition skulle ha förutspått. Den publika nyckeln medföljer som en okomprimerad EC-punkt enligt RFC 5480 §2.2, vilket är 0x04 följt av X och Y, så HotPDF kontrollerar att den är exakt 1 + 2 * CoordinateSize byte och börjar med 0x04 innan CNG rörs vid

var
  Digest, SigDER, PublicPoint: TBytes;
  Curve: THPDFECDSACurve;
  Res: THPDFECDSAVerifyResult;
begin
  // secp256r1, taken from the certificate SubjectPublicKeyInfo parameters
  Curve := HPDFECDSACurveFromOID('1.2.840.10045.3.1.7');

  // PublicPoint must be $04 || X || Y, so 1 + 2 * 32 = 65 bytes for P-256
  Res := HPDFECDSAVerifyDigest(Digest, SigDER, PublicPoint, Curve, eseDER);

  case Res of
    evrValid:
      Memo1.Lines.Add('signature verifies');
    evrInvalid:
      Memo1.Lines.Add('signature does not match the digest');
    evrMalformed:
      Memo1.Lines.Add('DER encoding or public point rejected');
    evrUnsupported:
      Memo1.Lines.Add('curve or algorithm not supported here');
    evrProviderUnavailable:
      Memo1.Lines.Add('bcrypt.dll or the curve provider is missing');
    evrProviderError:
      Memo1.Lines.Add('CNG returned an unexpected status');
  end;
end;

Lägg märke till den sista parametern. HPDFECDSAVerifyDigest accepterar också eseP1363 för anropare som redan håller en signatur med fast bredd, från en hårdvarutoken eller en fjärrsigneringstjänst som returnerar rå r || s. Den vägen upprätthåller fortfarande längd- och icke-noll-kontrollen på båda halvorna, så en buffert av rätt storlek full av nollor avvisas i stället för att skickas vidare till leverantören

Varför misslyckas det generiska ECDSA-algoritmnamnet på äldre Windows?

Därför att det generiska namnet är nyare än den utrullningsbas du levererar till. CNG exponerar en algoritmidentifierare ECDSA som härleder kurvan från den importerade nyckeln, och det är det rena sättet att skriva den här koden, men BCryptOpenAlgorithmProvider garanteras bara lösa upp den på nyare Windows-versioner. På en äldre maskin misslyckas öppningsanropet, leverantörshandtaget förblir nil, och varje ECDSA-verifiering i din applikation rapporterar ostödd på en signatur som är helt korrekt. HotPDF undviker stupet genom att öppna de kurvspecifika identifierarna i stället. Den löser upp ECDSA_P256, ECDSA_P384 och ECDSA_P521 en gång, cachar ett leverantörshandtag per kurva, och stänger dem i enhetsfinalisering. Varje verifiering gör då bara det billiga arbetet: importera en tillfällig publik nyckel från en ECCPUBLICBLOB, anropa BCryptVerifySignature, förstöra nyckeln. Ingen upprepad LoadLibrary, ingen upprepad GetProcAddress, ingen öppning och stängning av leverantör per signatur. Batch-verifiering av några hundra dokument märker skillnaden, och det gör även en tjänsteprocess som annars skulle churn:a leverantörshandtag under belastning

Returkoderna förblir ärliga om distinktionen. evrProviderUnavailable betyder att maskinen inte kunde ge HotPDF en leverantör; evrInvalid betyder att CNG svarade STATUS_INVALID_SIGNATURE. Att slå ihop de två till ett enda fel är hur ett utrullningsproblem felrapporteras som ett förfalskat dokument. Samma separation mellan miljöfel och kryptografiskt fel går genom CNG- och CAPI-hanteringen på signeringssidan, som täcks i artikeln om certifikatlagersignering och byteordning

Vilket certifikat signerade detta? SignerIdentifier är två olika saker

RFC 5652 §5.3 gör SignerIdentifier till en CHOICE, och en verifierare som bara hanterar den ena grenen kommer tyst att verifiera mot fel nyckel. Den första grenen är issuerAndSerialNumber, en SEQUENCE som håller utfärdarens Name i rå DER och serienummer-INTEGER, och att matcha den är en bytejämförelse mot varje certifikat i CMS-certificates-mängden. Den andra grenen är [0] subjectKeyIdentifier, en implicit taggad OCTET STRING, och att matcha den kräver att gräva in i certifikatet snarare än att jämföra dess huvudfält

Grävandet har ett lager som överraskar folk. Nyckelidentifieraren bor i en X.509v3-tillägg, så HotPDF vandrar genom [3]-tilläggsfältet i tbsCertificate, hittar tillägget vars OID är 2.5.29.14, hoppar över den valfria kritiska BOOLEAN, och tar extnValue-OCTET STRING. Den bytesträngen är inte identifieraren. Enligt RFC 5280 §4.2.1.2 är dess innehåll själv DER, och typen KeyIdentifier är ännu en OCTET STRING, så man tolkar en andra gång för att nå de faktiska byten. Stanna ett lager för tidigt och du jämför en 22-bytes wrapper mot en 20-bytes identifierare, inget certifikat matchar någonsin, och verifieraren faller tillbaka på vilken heuristik du skrev härnäst, vilket är den verkliga faran. Att ta det första certifikatet i mängden är en frestande genväg och den är fel närhelst CMS:et bär en kedja, vilket är för det mesta, eftersom lövet inte krävs komma först. HotPDF accepterar bara ett omatchat certifikat när behållaren håller exakt ett; med flera certifikat närvarande är en exakt SignerIdentifier-matchning obligatorisk. Att verifiera en digest mot en publik nyckel för en mellanliggande CA ger inte ett vänligt fel, det ger en självsäker ogiltig på ett dokument som är helt i sin ordning

var
  Pdf: THotPDF;
  Info: THPDFSignatureInfo;
  I: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('signed.pdf') > 0 then
      for I := 0 to Pdf.GetLoadedSignatureFieldCount - 1 do
        if Pdf.VerifyLoadedSignatureEx(I, Info) = svValid then
          Memo1.Lines.Add(Format('%s: %s %s over %s, signer %s',
            [String(Info.FieldName), String(Info.PublicKeyAlgorithm),
             String(Info.CurveName), String(Info.HashAlgorithm),
             String(Info.SignerName)]))
        else
          Memo1.Lines.Add(Format('%s: not valid', [String(Info.FieldName)]));
  finally
    Pdf.Free;
  end;
end;

THPDFSignatureInfo.CurveName rapporterar P-256, P-384 eller P-521 så en granskningslogg registrerar vilken kurva som faktiskt användes i stället för bara ordet ECDSA. Dokumentnivå-mekaniken kring detta anrop, i synnerhet hur /ByteRange-segmenten hashas och varför digesten måste beräknas över filen snarare än det tolkade objektträdet, är ämnet för följdartikeln om att verifiera PDF-signaturer

Vad detta inte ger dig

Ett grönt resultat från HPDFECDSAVerifyDigest besvarar bara en fråga: dessa byte signerades av den privata nyckel som matchar denna publika nyckel. Det säger ingenting om huruvida den nyckeln tillhör någon du borde lita på. Kedjebyggnad till ett tillitsankare, återkallelse via CRL eller OCSP, och policykontroller är separat arbete, och varje produkt som rapporterar en giltig signatur utan dem rapporterar mindre än användaren antar. Certifikatets giltighetsdatum exponeras separat i THPDFSignatureInfo av precis det skälet: en signatur kan verifiera kryptografiskt medan certifikatet som gjorde den gick ut för två år sedan. Kurvstödet är också medvetet smalt. Tre NIST-primkurvor hanteras, och en signatur över någon annan kurva returnerar ostödd i stället för en gissning. CNG-vägen är endast för Windows, vilket är rätt avvägning för en VCL-komponent men värt att nämna innan du planerar en plattformsoberoende tjänst runt den. Och strikthetheten är inte konfigurerbar: det finns inget överseende läge som accepterar en icke-minimal DER-längd bara för att någon äldre signerare sände ut en sådan. Om du stöter på en sådan fil i produktion är det ärliga svaret att registrera den och spåra upp producenten, inte att bredda parsern tills filen godkänns

Verifieringsvägen för ECDSA som beskrivs här levereras som en del av standard-HotPDF Component för Delphi och C++Builder, tillsammans med RSA PKCS#1 v1.5- och RSA-PSS-vägarna och den fullständiga signaturinformationsposten; produktsidan innehåller den kompletta referensen för digitala signaturer