Teknisk artikkel

ECDSA-verifisering av PDF-signaturer i Delphi: DER til P1363

En ECDSA signatureValue inne i en CMS-beholder er en DER SEQUENCE { INTEGER r, INTEGER s }. Windows CNG-funksjonen BCryptVerifySignature aksepterer ingen av delene: den vil ha IEEE P1363 fastbredde r || s, uten tagger og uten lengder. HotPDF, den native VCL PDF-komponenten for Delphi og C++Builder, konverterer mellom de to under strenge DER-regler før en nøkkel importeres

Feilen dette forhindrer er en spesifikk og demoraliserende en. Acrobat åpner dokumentet og viser et grønt hakemerke. Din egen verifiserer, som går gjennom de samme bytene, returnerer ugyldig, eller CNG gir tilbake STATUS_INVALID_SIGNATURE uten videre forklaring. Det er ingenting galt med signaturen. Det som er galt, er at omtrent sytti byte med ASN.1 ble sendt til et API som forventet sekstifire byte med rått heltall, og mismatchet er usynlig med mindre du vet å lete etter det

Hvorfor avviser BCryptVerifySignature en gyldig ECDSA-signatur?

Fordi de to sidene av kallet snakker forskjellige signaturkodinger, og ingen av dem kunngjør det. ISO 32000-1 §12.8 sier at en signaturordbok bærer en CMS-blob i /Contents; RFC 5652 §5.3 sier at signatureValue i hver SignerInfo er en OCTET STRING hvis innhold er hva signaturalgoritmen definerer. For ECDSA er det innholdet SEC 1 DER-strukturen: en SEQUENCE som holder to INTEGER-er. Den har variabel lengde by design, fordi r og s er heltall og DER stripper ledende null-oktetter fra heltall

IEEE P1363 tar det motsatte synet. Den definerer signaturen som sammenkjedingen av de to koordinatene, hver venstrepolstret med nuller til nøyaktig bytebredden til kurvefeltet. En P-256-signatur er alltid 64 byte. En DER-koding av samme signatur er normalt 70 eller 71 byte og kan være hvor som helst fra omtrent 8 til 72. Gi DER-formen til BCryptVerifySignature, og lengdesjekken alene dømmer kallet, og det er derfor HotPDF normaliserer før den verifiserer fremfor etterpå

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-reglene en signaturparser ikke må lempe på

Hver avvisning listet her er en avvisning HotPDF utfører bevisst, og hver av dem lukker en vei en overbærende parser ville latt stå åpen. Fristelsen når man skriver en konverterer er å finne de to INTEGER-nodene, kopiere innholdet deres, og gå videre. Det fungerer på velformet input og aksepterer stille en hel familie av formbare re-kodinger på fiendtlig input. Så HPDFECDSANormalizeSignature avviser et negativt heltall, altså enhver r eller s hvis første innholdsoktett har den høye biten satt, fordi en gyldig ECDSA-skalar er positiv. Den avviser en verdi som er utelukkende null, siden r = 0 eller s = 0 aldri er en legitim signatur. Den avviser en overflødig ledende null-oktett: X.690 §8.3 tillater nøyaktig én, og bare når neste oktett ellers ville leses som negativ, så en 00 etterfulgt av en oktett under 0x80 er en re-koding, ikke en signatur. Den avviser en ikke-minimal lengdeheader, fordi X.690 §10.1 krever den definitive formen kodet i færrest mulig oktetter, og en langform-lengde som kunne vært kortform er en annen bytestreng som bærer samme mening. Den avviser et heltall bredere enn kurvens koordinatstørrelse, siden den verdien ikke kan være et feltelement. Og den avviser enhver etterfølgende node etter s, sammen med en ytre SEQUENCE hvis totale lengde ikke er lik lengden på hele blobben

De to siste betyr mer enn de ser ut til. Etterfølgende byte etter SEQUENCE-en er det klassiske signatur-formbarhetstrikset: legg til søppel, og en overbærende verifiserer sier fortsatt gyldig mens bytestrengen den validerte ikke er bytestrengen som ble signert. Samme instinkt driver den ASN.1-lengdeherdingen beskrevet i notatet om PKCS#12-parsing, og det er samme instinkt her. I en verifiseringsvei er en akseptert struktur som aldri ble utstedt av en konform signatar en defekt, ikke en høflighet

Koordinatbredde tilhører kurven, ikke signaturen

HotPDF utleder utdatabredden fra den navngitte kurve-OID-en, aldri fra lengden på DER-en den nettopp parset. Dette er den andre halvparten av konverteringen og halvparten som er lett å få subtilt feil. RFC 5480 §2.1.1 identifiserer kurven i sertifikatets SubjectPublicKeyInfo-parametere, og HPDFECDSACurveFromOID mapper de tre OID-ene HotPDF støtter: 1.2.840.10045.3.1.7 for P-256, 1.3.132.0.34 for P-384, og 1.3.132.0.35 for P-521. HPDFECDSACoordinateSize returnerer deretter 32, 48, eller 66 byte, og P1363-bufferen er dobbelt så stor: 64, 96, eller 132. Hvert dekodet heltall høyrejusteres inn i sin halvdel, så en kort r nullpolstres til venstre fremfor å skiftes. P-521 er den som fanger folk, fordi 521 bit er 65,125 byte og rundes opp til 66, som gir en 132-byte signatur ingen potens-av-to-intuisjon ville forutsett. Den offentlige nøkkelen følger med som et ukomprimert EC-punkt per RFC 5480 §2.2, som er 0x04 etterfulgt av X og Y, så HotPDF sjekker at den er nøyaktig 1 + 2 * CoordinateSize byte og begynner med 0x04 før den rører CNG

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;

Legg merke til den siste parameteren. HPDFECDSAVerifyDigest aksepterer også eseP1363 for de som allerede har en fastbredde-signatur, fra en maskinvaretoken eller en ekstern signeringstjeneste som returnerer rå r || s. Den veien håndhever fortsatt lengde- og ikke-null-sjekken på begge halvdeler, så en buffer med riktig størrelse full av nuller avvises fremfor å slippes gjennom til provideren

Hvorfor feiler det generiske ECDSA-algoritmenavnet på eldre Windows?

Fordi det generiske navnet er nyere enn utrullingsgrunnlaget du leverer til. CNG eksponerer en algoritmeidentifikator ECDSA som utleder kurven fra den importerte nøkkelen, og det er den rene måten å skrive denne koden på, men BCryptOpenAlgorithmProvider er bare garantert å løse den opp på nyere Windows-versjoner. På en eldre maskin feiler åpne-kallet, provider-håndtaket forblir nil, og hver ECDSA-verifisering i applikasjonen din rapporterer ikke-støttet på en signatur som er helt fin. HotPDF unngår stupet ved å åpne per-kurve-identifikatorene i stedet. Den løser opp ECDSA_P256, ECDSA_P384, og ECDSA_P521 én gang, mellomlagrer ett provider-håndtak per kurve, og lukker dem ved enhetens finalisering. Hver verifisering gjør deretter bare det billige arbeidet: importer en midlertidig offentlig nøkkel fra en ECCPUBLICBLOB, kall BCryptVerifySignature, ødelegg nøkkelen. Ingen gjentatt LoadLibrary, ingen gjentatt GetProcAddress, ingen provider åpnet og lukket per signatur. Batch-verifisering av noen hundre dokumenter merker forskjellen, og det gjør også en tjenesteprosess som ellers ville sluke provider-håndtak under last

Returkodene holder seg ærlige om skillet. evrProviderUnavailable betyr at maskinen ikke kunne gi HotPDF en provider; evrInvalid betyr at CNG svarte STATUS_INVALID_SIGNATURE. Å slå de to sammen til én feil er hvordan et utrullingsproblem blir feilrapportert som et forfalsket dokument. Det samme skillet mellom miljøfeil og kryptografisk feil går gjennom CNG- og CAPI-håndteringen på signeringssiden, dekket i artikkelen om sertifikatlager-signering og byte-rekkefølge

Hvilket sertifikat signerte dette? SignerIdentifier er to forskjellige ting

RFC 5652 §5.3 gjør SignerIdentifier til en CHOICE, og en verifiserer som bare håndterer den ene armen vil stille verifisere mot feil nøkkel. Den første armen er issuerAndSerialNumber, en SEQUENCE som holder utsteder-Name i rå DER og serienummer-INTEGER, og å matche den er en bytesammenligning mot hvert sertifikat i CMS-ens certificates-sett. Den andre armen er [0] subjectKeyIdentifier, en implisitt tagget OCTET STRING, og å matche den krever å grave inn i sertifikatet fremfor å sammenligne dets headerfelt

Gravingen har et lag som overrasker folk. Nøkkelidentifikatoren bor i en X.509v3-utvidelse, så HotPDF traverserer [3]-utvidelsesfeltet til tbsCertificate, finner utvidelsen hvis OID er 2.5.29.14, hopper over den valgfrie kritiske BOOLEAN, og tar extnValue OCTET STRING. Den oktettstrengen er ikke identifikatoren. Per RFC 5280 §4.2.1.2 er innholdet dens selv DER, og KeyIdentifier-typen er en annen OCTET STRING, så du parser en gang til for å nå de faktiske bytene. Stopp ett lag for tidlig, og du sammenligner en 22-byte innpakning mot en 20-byte identifikator, intet sertifikat matcher noensinne, og verifisereren faller tilbake på hvilken som helst heuristikk du skrev deretter, som er den ekte faren. Å ta det første sertifikatet i settet er en fristende snarvei og det er feil hver gang CMS-en bærer en kjede, som er det meste av tiden, fordi bladet ikke er påkrevd å komme først. HotPDF aksepterer bare et umatchet sertifikat når beholderen holder nøyaktig ett; med flere sertifikater til stede er en eksakt SignerIdentifier-match obligatorisk. Å verifisere et digest mot en mellomliggende CA-offentlig nøkkel produserer ikke en vennlig feilmelding, den produserer en sikker ugyldig på et dokument som er helt i orden

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 rapporterer P-256, P-384, eller P-521, slik at en revisjonslogg registrerer hvilken kurve som faktisk ble brukt fremfor bare ordet ECDSA. Den dokumentnivå-rørleggingen rundt dette kallet, spesielt hvordan /ByteRange-segmentene hashes og hvorfor digestet må beregnes over filen fremfor over det parsede objekttreet, er temaet for følgeartikkelen om verifisering av PDF-signaturer

Hva dette ikke gir deg

Et grønt resultat fra HPDFECDSAVerifyDigest svarer på ett eneste spørsmål: disse bytene ble signert av den private nøkkelen som matcher denne offentlige nøkkelen. Den sier ingenting om hvorvidt den nøkkelen tilhører noen du burde stole på. Kjedebygging til et tillitsanker, tilbakekalling gjennom CRL eller OCSP, og policysjekker er separat arbeid, og ethvert produkt som rapporterer en gyldig signatur uten dem, rapporterer mindre enn brukeren antar. Sertifikatets gyldighetsdatoer synliggjøres separat i THPDFSignatureInfo av nettopp den grunnen: en signatur kan verifisere kryptografisk mens sertifikatet som laget den utløp for to år siden. Kurvestøtten er også bevisst snever. Tre NIST prime-kurver håndteres, og en signatur over enhver annen kurve returnerer ikke-støttet fremfor en gjetning. CNG-veien er bare for Windows, som er den riktige avveiningen for en VCL-komponent, men verdt å nevne før du planlegger en plattformuavhengig tjeneste rundt den. Og strengheten er ikke konfigurerbar: det finnes ingen overbærende modus som aksepterer en ikke-minimal DER-lengde fordi en eller annen legacy-signatar utstedte en slik. Hvis du møter en slik fil i produksjon, er det ærlige svaret å registrere den og etterforske produsenten, ikke å utvide parseren til filen består

ECDSA-verifiseringsveien beskrevet her leveres som en del av standard HotPDF Component for Delphi og C++Builder, sammen med RSA PKCS#1 v1.5- og RSA-PSS-veiene og hele signaturinformasjonsrecorden; produktsiden inneholder den komplette referansen for digitale signaturer