En ECDSA signatureValue inde i en CMS-container er en DER SEQUENCE { INTEGER r, INTEGER s }. Windows CNG-funktionen BCryptVerifySignature accepterer ingen af delene: den vil have IEEE P1363 fast-bredde r || s, uden tags og uden længder. HotPDF, den native VCL PDF-komponent til Delphi og C++Builder, konverterer mellem de to under strenge DER-regler før en nøgle importeres
Fejlen dette forhindrer er en specifik og demoraliserende en. Acrobat åbner dokumentet og viser et grønt flueben. Din egen verifikator, der gennemgår de samme bytes, returnerer ugyldig, eller CNG giver STATUS_INVALID_SIGNATURE tilbage uden yderligere forklaring. Der er intet galt med signaturen. Det, der er galt, er at omkring halvfjerds bytes ASN.1 blev overgivet til en API, der forventede fireogtres bytes rå heltal, og misforholdet er usynligt, medmindre man ved man skal kigge efter det
Hvorfor afviser BCryptVerifySignature en gyldig ECDSA-signatur?
Fordi de to sider af kaldet taler forskellige signaturkodninger, og ingen af dem annoncerer det. ISO 32000-1 §12.8 siger at en signaturdictionary bærer en CMS-blob i /Contents; RFC 5652 §5.3 siger at signatureValue i hver SignerInfo er en OCTET STRING, hvis indhold er hvad signaturalgoritmen definerer. For ECDSA er det indhold SEC 1 DER-strukturen: en SEQUENCE der holder to INTEGERs. Den er variabel i længden by design, fordi r og s er heltal, og DER strips indledende nul-oktetter fra heltal
IEEE P1363 tager det modsatte synspunkt. Det definerer signaturen som sammenkædningen af de to koordinater, hver venstre-paddet med nuller til nøjagtigt kurvefeltets bytebredde. En P-256-signatur er altid 64 bytes. En DER-kodning af samme signatur er normalt 70 eller 71 bytes og kan være hvor som helst fra omkring 8 til 72. Overgiv DER-formen til BCryptVerifySignature, og længdetjekket alene dømmer kaldet, hvilket er hvorfor HotPDF normaliserer før den verificerer frem for efter
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-reglerne en signaturparser ikke må lempe
Hver afvisning nævnt her er en afvisning, HotPDF udfører bevidst, og hver enkelt lukker en sti, som en overbærende parser ville lade stå åben. Fristelsen, når man skriver en konverter, er at finde de to INTEGER-noder, kopiere deres indhold og gå videre. Det virker på velformet input og accepterer stille en familie af formbare genkodninger på fjendtligt input. Så HPDFECDSANormalizeSignature afviser et negativt heltal, altså enhver r eller s, hvis første indholdsoktet har den høje bit sat, fordi en gyldig ECDSA-skalar er positiv. Den afviser en værdi, der er helt nul, siden r = 0 eller s = 0 aldrig er en legitim signatur. Den afviser en redundant indledende nul-oktet: X.690 §8.3 tillader præcis én, og kun når den næste oktet ellers ville læses som negativ, så en 00 efterfulgt af en oktet under 0x80 er en genkodning, ikke en signatur. Den afviser en ikke-minimal længde-header, fordi X.690 §10.1 kræver den definitive form kodet i de færrest mulige oktetter, og en lang-form-længde, der kunne have været kort-form, er en anden bytestreng, der bærer den samme betydning. Den afviser et heltal bredere end kurvens koordinatstørrelse, siden den værdi ikke kan være et feltelement. Og den afviser enhver efterfølgende node efter s, sammen med en ydre SEQUENCE, hvis totale længde ikke svarer til længden af hele blob'en
De sidste to betyder mere, end de ser ud til. Efterfølgende bytes efter SEQUENCE er det klassiske signatur-malleability-trick: tilføj junk, og en overbærende verifikator siger stadig gyldig, mens den bytestreng, den validerede, ikke er den bytestreng, der blev signeret. Den samme instinkt driver den ASN.1-længdehærdning, der beskrives i notatet om PKCS#12-parsing, og det er det samme instinkt her. I en verifikationssti er en accepteret struktur, der aldrig blev udsendt af en konform signer, en defekt, ikke en kulance
Koordinatbredde hører til kurven, ikke til signaturen
HotPDF udleder outputbredden fra det navngivne kurve-OID, aldrig fra længden af den DER, den lige har parset. Dette er den anden halvdel af konverteringen og den halvdel, der er let at få subtilt forkert. RFC 5480 §2.1.1 identificerer kurven i certifikatets SubjectPublicKeyInfo-parametre, og HPDFECDSACurveFromOID mapper de tre OID'er, HotPDF understø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 derefter 32, 48 eller 66 bytes, og P1363-bufferen er det dobbelte: 64, 96 eller 132. Hvert dekodet heltal er højre-justeret ind i sin halvdel, så en kort r nul-paddes til venstre frem for at blive forskudt. P-521 er den, der fanger folk, fordi 521 bit er 65,125 bytes og runder op til 66, hvilket giver en 132-byte signatur, som ingen potens-af-to-intuition ville have forudsagt. Den offentlige nøgle rejser med som et ukomprimeret EC-punkt per RFC 5480 §2.2, som er 0x04 efterfulgt af X og Y, så HotPDF tjekker at det er nøjagtigt 1 + 2 * CoordinateSize bytes og begynder 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;
Bemærk den sidste parameter. HPDFECDSAVerifyDigest accepterer også eseP1363 for kaldere, der allerede holder en fast-bredde-signatur, fra en hardware-token eller en fjern-signeringstjeneste, der returnerer rå r || s. Den sti håndhæver stadig længdetjekket og ikke-nul-tjekket på begge halvdele, så en buffer af den rigtige størrelse fuld af nuller afvises frem for at blive sendt videre til provideren
Hvorfor fejler det generiske ECDSA-algoritmenavn på ældre Windows?
Fordi det generiske navn er nyere end den udrulningsbase, du leverer til. CNG eksponerer en algoritme-identifikator ECDSA, der udleder kurven fra den importerede nøgle, og det er den rene måde at skrive denne kode på, men BCryptOpenAlgorithmProvider er kun garanteret at kunne opløse den på nyere Windows-versioner. På en ældre maskine fejler open-kaldet, provider-håndtaget forbliver nil, og hver ECDSA-verifikation i din applikation rapporterer ikke-understøttet på en signatur, der er helt fin. HotPDF undgår klippen ved at åbne de per-kurve-identifikatorer i stedet. Den opløser ECDSA_P256, ECDSA_P384 og ECDSA_P521 én gang, cacher ét provider-håndtag per kurve, og lukker dem i unit-finalisering. Hver verifikation gør derefter kun det billige arbejde: importer en midlertidig offentlig nøgle fra en ECCPUBLICBLOB, kald BCryptVerifySignature, destruér nøglen. Ingen gentaget LoadLibrary, ingen gentaget GetProcAddress, ingen provider åbnet og lukket per signatur. Batch-verifikation af nogle få hundrede dokumenter mærker forskellen, og det gør en tjenesteproces, der ellers ville kværne provider-håndtag under belastning
Resultatkoderne forbliver ærlige om skelnen. evrProviderUnavailable betyder, at maskinen ikke kunne give HotPDF en provider; evrInvalid betyder, at CNG svarede STATUS_INVALID_SIGNATURE. At kollapse de to til én fejl er, hvordan et udrulningsproblem bliver fejlrapporteret som et forfalsket dokument. Den samme adskillelse mellem miljøfejl og kryptografisk fejl løber gennem CNG- og CAPI-håndteringen på signeringssiden, dækket i artiklen om certifikatlager-signering og byte-rækkefølge
Hvilket certifikat signerede dette? SignerIdentifier er to forskellige ting
RFC 5652 §5.3 gør SignerIdentifier til en CHOICE, og en verifikator, der kun håndterer den ene arm, vil stille verificere mod den forkerte nøgle. Den første arm er issuerAndSerialNumber, en SEQUENCE der holder issuer-navnet i rå DER og serie-INTEGER'et, og at matche den er en bytesammenligning mod hvert certifikat i CMS'ens certificates-sæt. Den anden arm er [0] subjectKeyIdentifier, en implicit tagget OCTET STRING, og at matche den kræver at grave ind i certifikatet frem for at sammenligne dets header-felter
Gravningen har et lag, der overrasker folk. Nøgleidentifikatoren bor i en X.509v3-udvidelse, så HotPDF gennemgår [3]-udvidelsesfeltet i tbsCertificate, finder udvidelsen hvis OID er 2.5.29.14, springer den valgfrie kritiske BOOLEAN over, og tager extnValue OCTET STRING'en. Den oktetstreng er ikke identifikatoren. Ifølge RFC 5280 §4.2.1.2 er dens indhold selv DER, og KeyIdentifier-typen er endnu en OCTET STRING, så man parser en anden gang for at nå de faktiske bytes. Stop ét lag for tidligt, og man sammenligner en 22-byte-wrapper mod en 20-byte-identifikator, intet certifikat matcher nogensinde, og verifikatoren falder tilbage på hvilken heuristik man så skrev, hvilket er den egentlige fare. At tage det første certifikat i sættet er en fristende genvej, og det er forkert, når CMS'en bærer en kæde, hvilket er de fleste gange, fordi løvbladet ikke er påkrævet at komme først. HotPDF accepterer kun et umatchet certifikat, når containeren holder præcis ét; med flere certifikater til stede er en eksakt SignerIdentifier-match obligatorisk. At verificere en digest mod en mellemliggende CA-offentlig nøgle producerer ikke en venlig fejl, den producerer en selvsikker ugyldig på et dokument, der er fint
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, så en revisionslog registrerer hvilken kurve, der faktisk blev brugt, frem for bare ordet ECDSA. Den dokumentniveau-VVS omkring dette kald, især hvordan /ByteRange-segmenterne hashes og hvorfor digesten skal beregnes over filen frem for det parsede objekttræ, er emnet for følgeartiklen om verifikation af PDF-signaturer
Hvad dette ikke giver dig
Et grønt resultat fra HPDFECDSAVerifyDigest besvarer kun ét spørgsmål: disse bytes blev signeret af den private nøgle, der matcher denne offentlige nøgle. Det siger intet om, hvorvidt den nøgle tilhører nogen, du burde stole på. Kædebygning til et tillidsanker, tilbagekaldelse gennem CRL eller OCSP, og politiktjek er separat arbejde, og ethvert produkt, der rapporterer en gyldig signatur uden dem, rapporterer mindre end brugeren antager. Certifikatgyldighedsdatoer eksponeres separat i THPDFSignatureInfo af netop den grund: en signatur kan verificere kryptografisk, mens certifikatet, der lavede den, udløb for to år siden. Kurve-understøttelsen er også bevidst snæver. Tre NIST prime-kurver håndteres, og en signatur over enhver anden kurve returnerer ikke-understøttet frem for et gæt. CNG-stien er kun til Windows, hvilket er den rigtige afvejning for en VCL-komponent, men værd at nævne, før man planlægger en cross-platform-tjeneste omkring den. Og strengheden er ikke konfigurerbar: der er ingen overbærende tilstand, der accepterer en ikke-minimal DER-længde, fordi en legacy-signer udsendte en. Møder man en sådan fil i produktion, er det ærlige svar at registrere den og forfølge producenten, ikke at udvide parseren, indtil filen består
ECDSA-verifikationsstien beskrevet her leveres som del af standard-HotPDF Component til Delphi og C++Builder, sammen med RSA PKCS#1 v1.5- og RSA-PSS-stierne og den fulde signaturinformationsrecord; produktsiden bærer den komplette digitale signaturreference