Een ECDSA-signatureValue binnen een CMS-container is een DER SEQUENCE { INTEGER r, INTEGER s }. De Windows CNG-functie BCryptVerifySignature accepteert geen van beide: hij wil IEEE P1363 fixed-width r || s, zonder tags en zonder lengtes. HotPDF, het native VCL PDF-component voor Delphi en C++Builder, converteert tussen de twee volgens strikte DER-regels voordat een sleutel geïmporteerd wordt
De fout die dit voorkomt is een specifieke en demoraliserende. Acrobat opent het document en toont een groen vinkje. Je eigen verifier, die dezelfde bytes doorloopt, retourneert ongeldig, of CNG geeft STATUS_INVALID_SIGNATURE terug zonder verdere uitleg. Er is niets mis met de handtekening. Wat wel mis is, is dat ongeveer zeventig bytes ASN.1 doorgegeven werden aan een API die vierenzestig bytes ruwe integer verwachtte, en de mismatch is onzichtbaar tenzij je weet dat je ernaar moet zoeken
Waarom weigert BCryptVerifySignature een geldige ECDSA-handtekening?
Omdat de twee kanten van de aanroep verschillende handtekeningcoderingen spreken, en geen van beide kondigt dat aan. ISO 32000-1 §12.8 zegt dat een signature-dictionary een CMS-blob draagt in /Contents; RFC 5652 §5.3 zegt dat de signatureValue in elke SignerInfo een OCTET STRING is waarvan de inhoud is wat het handtekeningalgoritme ook definieert. Voor ECDSA is die inhoud de SEC 1 DER-structuur: een SEQUENCE die twee INTEGERs bevat. Het heeft bewust een variabele lengte, omdat r en s integers zijn en DER leidende nul-octetten van integers strippt
IEEE P1363 neemt het tegenovergestelde standpunt in. Het definieert de handtekening als de aaneenschakeling van de twee coördinaten, elk links opgevuld met nullen tot precies de bytebreedte van het curveveld. Een P-256-handtekening is altijd 64 bytes. Een DER-codering van dezelfde handtekening is normaliter 70 of 71 bytes en kan overal tussen ongeveer 8 en 72 liggen. Geef de DER-vorm door aan BCryptVerifySignature en de lengtecontrole alleen al doemt de aanroep, wat is waarom HotPDF normaliseert vóórdat het verifieert in plaats van erna
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;
De DER-regels die een handtekeningparser niet mag versoepelen
Elke afwijzing die hier genoemd wordt, is een afwijzing die HotPDF bewust uitvoert, en elk daarvan sluit een pad af dat een toegeeflijke parser open zou laten. De verleiding bij het schrijven van een converter is om de twee INTEGER-nodes te vinden, hun inhoud te kopiëren, en verder te gaan. Dat werkt op welgevormde input en accepteert stilzwijgend een familie van manipuleerbare herencoderingen op vijandige input. Dus wijst HPDFECDSANormalizeSignature een negatieve integer af, wat betekent elke r of s waarvan het eerste inhoudsoctet de hoge bit heeft ingesteld, omdat een geldige ECDSA-scalar positief is. Het wijst een waarde af die volledig nul is, aangezien r = 0 of s = 0 nooit een legitieme handtekening is. Het wijst een overbodig leidend nul-octet af: X.690 §8.3 staat precies één toe, en alleen wanneer het volgende octet anders als negatief gelezen zou worden, dus is een 00 gevolgd door een octet onder 0x80 een herencodering, geen handtekening. Het wijst een niet-minimale lengte-header af, omdat X.690 §10.1 de definite-vorm vereist gecodeerd in het minst aantal octetten, en een long-form lengte die short-form had kunnen zijn, is een andere byte-string die dezelfde betekenis draagt. Het wijst een integer af die breder is dan de curve-coördinaatgrootte, aangezien die waarde geen veldelement kan zijn. En het wijst elke opvolgende node na s af, samen met een buitenste SEQUENCE waarvan de totale lengte niet gelijk is aan de lengte van de hele blob
Die laatste twee zijn belangrijker dan ze lijken. Bytes na de SEQUENCE zijn de klassieke handtekening-malleability-truc: voeg rommel toe, en een toegeeflijke verifier zegt nog steeds geldig terwijl de byte-string die hij gevalideerd heeft niet de byte-string is die ondertekend werd. Hetzelfde instinct drijft de ASN.1-lengteverharding beschreven in de notitie over PKCS#12-parsing, en het is hetzelfde instinct hier. In een verificatiepad is een geaccepteerde structuur die nooit door een conforme signer uitgegeven werd, een defect, geen beleefdheid
Coördinaatbreedte hoort bij de curve, niet bij de handtekening
HotPDF leidt de outputbreedte af van de benoemde curve-OID, nooit van de lengte van de DER die het zojuist geparst heeft. Dit is de tweede helft van de conversie en de helft die makkelijk subtiel fout gaat. RFC 5480 §2.1.1 identificeert de curve in de certificaat-SubjectPublicKeyInfo-parameters, en HPDFECDSACurveFromOID mapt de drie OID's die HotPDF ondersteunt: 1.2.840.10045.3.1.7 voor P-256, 1.3.132.0.34 voor P-384, en 1.3.132.0.35 voor P-521. HPDFECDSACoordinateSize retourneert vervolgens 32, 48, of 66 bytes, en de P1363-buffer is het dubbele: 64, 96, of 132. Elke gedecodeerde integer wordt rechts uitgelijnd in zijn helft, dus wordt een korte r links met nullen opgevuld in plaats van verschoven. P-521 is degene die mensen verrast, omdat 521 bits 65,125 bytes is en afgerond wordt naar 66, wat een handtekening van 132 bytes oplevert die geen macht-van-twee-intuïtie voorspeld zou hebben. De publieke sleutel reist mee als een ongecomprimeerd EC-punt volgens RFC 5480 §2.2, wat 0x04 gevolgd door X en Y is, dus controleert HotPDF dat het precies 1 + 2 * CoordinateSize bytes is en begint met 0x04 voordat het CNG aanraakt
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;
Let op de laatste parameter. HPDFECDSAVerifyDigest accepteert ook eseP1363 voor aanroepers die al een fixed-width handtekening bezitten, van een hardware-token of een externe ondertekeningsservice die ruwe r || s teruggeeft. Dat pad handhaaft nog steeds de lengte- en niet-nul-controle op beide helften, dus een buffer van de juiste grootte vol nullen wordt geweigerd in plaats van doorgegeven aan de provider
Waarom faalt de generieke ECDSA-algoritmenaam op oudere Windows?
Omdat de generieke naam nieuwer is dan de deploymentbasis waarnaar je uitlevert. CNG stelt een algoritme-identifier ECDSA beschikbaar die de curve afleidt uit de geïmporteerde sleutel, en het is de nette manier om deze code te schrijven, maar BCryptOpenAlgorithmProvider wordt alleen gegarandeerd opgelost op recentere Windows-versies. Op een oudere machine faalt de open-aanroep, blijft de providerhandle nil, en rapporteert elke ECDSA-verificatie in je applicatie niet-ondersteund bij een handtekening die volkomen goed is. HotPDF vermijdt die afgrond door in plaats daarvan de per-curve-identifiers te openen. Het lost ECDSA_P256, ECDSA_P384, en ECDSA_P521 eenmalig op, cachet één providerhandle per curve, en sluit ze bij unit-finalisatie. Elke verificatie doet dan alleen het goedkope werk: importeer een tijdelijke publieke sleutel uit een ECCPUBLICBLOB, roep BCryptVerifySignature aan, vernietig de sleutel. Geen herhaalde LoadLibrary, geen herhaalde GetProcAddress, geen provider open en sluiten per handtekening. Batchverificatie van een paar honderd documenten voelt het verschil, en een serviceproces dat anders providerhandles zou opjagen onder belasting ook
De resultaatcodes blijven eerlijk over het onderscheid. evrProviderUnavailable betekent dat de machine HotPDF geen provider kon geven; evrInvalid betekent dat CNG STATUS_INVALID_SIGNATURE antwoordde. Die twee samenvoegen tot één fout is hoe een deploymentprobleem verkeerd gerapporteerd wordt als een vervalst document. Dezelfde scheiding tussen omgevingsfout en cryptografische fout loopt door de CNG- en CAPI-afhandeling aan de ondertekeningskant, behandeld in het artikel over certificaatopslag-ondertekening en byte-volgorde
Welk certificaat heeft dit ondertekend? SignerIdentifier is twee verschillende dingen
RFC 5652 §5.3 maakt SignerIdentifier een CHOICE, en een verifier die slechts één tak afhandelt, verifieert stilzwijgend tegen de verkeerde sleutel. De eerste tak is issuerAndSerialNumber, een SEQUENCE die de issuer-Name in ruwe DER en het serial-INTEGER bevat, en het matchen ervan is een bytevergelijking tegen elk certificaat in de CMS-certificates-set. De tweede tak is [0] subjectKeyIdentifier, een impliciet getagde OCTET STRING, en het matchen ervan vereist graven in het certificaat in plaats van het vergelijken van zijn headervelden
Het graven heeft een laag die mensen verrast. De sleutelidentifier leeft in een X.509v3-extensie, dus loopt HotPDF door het [3]-extensieveld van de tbsCertificate, vindt de extensie waarvan de OID 2.5.29.14 is, slaat de optionele critical-BOOLEAN over, en neemt de extnValue-OCTET STRING. Die octet-string is niet de identifier. Volgens RFC 5280 §4.2.1.2 is de inhoud ervan zelf DER, en het KeyIdentifier-type is nog een OCTET STRING, dus parse je een tweede keer om bij de daadwerkelijke bytes te komen. Stop je één laag te vroeg, dan vergelijk je een wrapper van 22 bytes met een identifier van 20 bytes, matcht geen enkel certificaat, en valt de verifier terug op welke heuristiek je vervolgens geschreven hebt, wat het echte gevaar is. Het eerste certificaat in de set nemen is een verleidelijke sluiproute en het is fout wanneer de CMS een keten draagt, wat meestal het geval is, omdat de leaf niet verplicht is als eerste te komen. HotPDF accepteert alleen een niet-gematcht certificaat wanneer de container er precies één bevat; bij meerdere aanwezige certificaten is een exacte SignerIdentifier-match verplicht. Een digest verifiëren tegen een intermediaire-CA-publieke-sleutel produceert geen vriendelijke foutmelding, het produceert een zelfverzekerd ongeldig bij een document dat prima is
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 rapporteert P-256, P-384, of P-521, zodat een auditlog vastlegt welke curve daadwerkelijk gebruikt werd in plaats van alleen het woord ECDSA. De documentniveau-loodgieterij rond deze aanroep, in het bijzonder hoe de /ByteRange-segmenten gehasht worden en waarom de digest berekend moet worden over het bestand in plaats van over de geparste objectboom, is het onderwerp van het begeleidende artikel over het verifiëren van PDF-handtekeningen
Wat dit je niet geeft
Een groen resultaat van HPDFECDSAVerifyDigest beantwoordt slechts één vraag: deze bytes zijn ondertekend door de privésleutel die bij deze publieke sleutel hoort. Het zegt niets over of die sleutel toebehoort aan iemand die je zou moeten vertrouwen. Kettingopbouw naar een trust anchor, herroeping via CRL of OCSP, en beleidscontroles zijn apart werk, en elk product dat een geldige handtekening rapporteert zonder die zaken, rapporteert minder dan de gebruiker aanneemt. Certificaatgeldigheidsdata worden apart weergegeven in THPDFSignatureInfo precies om die reden: een handtekening kan cryptografisch verifiëren terwijl het certificaat dat haar maakte twee jaar geleden verlopen is. De curve-ondersteuning is ook bewust beperkt. Drie NIST prime curves worden afgehandeld, en een handtekening over elke andere curve retourneert niet-ondersteund in plaats van een gok. Het CNG-pad is Windows-only, wat de juiste afweging is voor een VCL-component maar het waard is om te vermelden voordat je een cross-platform service eromheen plant. En de striktheid is niet configureerbaar: er is geen soepele modus die een niet-minimale DER-lengte accepteert omdat een legacy-signer er ooit een uitzond. Als je zo'n bestand tegenkomt in productie, is het eerlijke antwoord om het vast te leggen en de producent aan te spreken, niet om de parser te verruimen totdat het bestand slaagt
Het ECDSA-verificatiepad dat hier beschreven wordt, wordt geleverd als onderdeel van de standaard HotPDF Component voor Delphi en C++Builder, naast de RSA PKCS#1 v1.5- en RSA-PSS-paden en de volledige signature-informatie-record; de productpagina bevat de complete referentie voor digitale handtekeningen