ECDSA signatureValue вътре в CMS контейнер е DER SEQUENCE { INTEGER r, INTEGER s }. Функцията на Windows CNG BCryptVerifySignature не приема нито едно от двете: тя иска IEEE P1363 с фиксирана ширина r || s, без тагове и без дължини. HotPDF, нативният VCL PDF компонент за Delphi и C++Builder, конвертира между двете под строги DER правила преди импортирането на ключ
Провалът, който това предотвратява, е специфичен и обезсърчаващ. Acrobat отваря документа и показва зелена отметка. Собственият ви verifier, обхождащ същите байтове, връща невалиден, или CNG връща STATUS_INVALID_SIGNATURE без по-нататъшно обяснение. Нищо не е грешно с подписа. Грешното е, че приблизително седемдесет байта ASN.1 са подадени на API, очакващо шестдесет и четири байта суров integer, и несъответствието е невидимо, освен ако не знаете да го търсите
Защо BCryptVerifySignature отхвърля валиден ECDSA подпис?
Защото двете страни на извикването говорят различни кодирания на подпис, и нито една не го обявява. ISO 32000-1 §12.8 казва, че речник на подпис носи CMS blob в /Contents; RFC 5652 §5.3 казва, че signatureValue във всеки SignerInfo е OCTET STRING, чието съдържание е каквото алгоритъмът за подписване дефинира. За ECDSA това съдържание е SEC 1 DER структурата: SEQUENCE, държащ два INTEGER-а. Тя е с променлива дължина по замисъл, защото r и s са цели числа, а DER изрязва водещите нулеви октети от целите числа
IEEE P1363 заема обратната гледна точка. Той дефинира подписа като конкатенация на двете координати, всяка допълнена отляво с нули до точно байтовата ширина на полето на кривата. P-256 подпис е винаги 64 байта. DER кодиране на същия подпис обикновено е 70 или 71 байта и може да бъде навсякъде от около 8 до 72. Подайте DER формата на BCryptVerifySignature и само проверката за дължина обрича извикването, поради което HotPDF нормализира преди да проверява, а не след
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 правилата, които парсър на подписи не бива да разхлабва
Всяко отхвърляне, изброено тук, е отхвърляне, което HotPDF извършва умишлено, и всяко затваря път, който снизходителен парсър би оставил отворен. Изкушението при писане на конвертор е да намерите двата INTEGER node-а, да копирате съдържанието им, и да продължите. Това работи върху добре оформен вход и тихо приема цяло семейство податливи на манипулация повторни кодирания при враждебен вход. Затова HPDFECDSANormalizeSignature отхвърля отрицателно цяло число, тоест всяко r или s, чийто първи октет на съдържание има зададен старши бит, защото валиден ECDSA скалар е положителен. Той отхвърля стойност, изцяло равна на нула, защото r = 0 или s = 0 никога не е легитимен подпис. Отхвърля излишен водещ нулев октет: X.690 §8.3 позволява точно един, и то само когато следващият октет иначе би се четял като отрицателен, така че 00, следван от октет под 0x80, е повторно кодиране, не подпис. Отхвърля не-минимален header за дължина, защото X.690 §10.1 изисква definite формата, кодирана с най-малко октети, а long-form дължина, която е могла да бъде short-form, е различен низ от байтове, носещ същото значение. Отхвърля и цяло число, по-широко от размера на координатата на кривата, защото такава стойност не може да е елемент на полето. И отхвърля всеки следващ node след s, заедно с външен SEQUENCE, чиято обща дължина не е равна на дължината на целия blob
Последните две имат по-голямо значение, отколкото изглежда. Байтове след SEQUENCE са класическият трик за податливост на подпис: добавете боклук, и снизходителен verifier все още казва валиден, докато низът от байтове, който е валидирал, не е низът от байтове, който е бил подписан. Същият инстинкт движи закаляването на дължините на ASN.1, описано в бележката за парсване на PKCS#12, и същият инстинкт стои и тук. В път за верификация приета структура, никога неизлъчена от съвместим подписващ, е дефект, не любезност
Ширината на координатите принадлежи на кривата, не на подписа
HotPDF извежда изходната ширина от именувания OID на кривата, никога от дължината на DER-а, който току-що е парснал. Това е втората половина на конверсията и половината, която е лесно да се обърка фино грешно. RFC 5480 §2.1.1 идентифицира кривата в параметрите на SubjectPublicKeyInfo на сертификата, а HPDFECDSACurveFromOID мапва трите OID-а, които HotPDF поддържа: 1.2.840.10045.3.1.7 за P-256, 1.3.132.0.34 за P-384 и 1.3.132.0.35 за P-521. HPDFECDSACoordinateSize после връща 32, 48 или 66 байта, а буферът за P1363 е двойно повече: 64, 96 или 132. Всяко декодирано цяло число се подравнява надясно в своята половина, така че кратко r се допълва с нули отляво, вместо да се измества. P-521 е този, който хваща хората, защото 521 бита са 65.125 байта и се закръгля до 66, давайки подпис от 132 байта, който никаква интуиция за степен на две не би предвидила. Публичният ключ пътува заедно като некомпресирана EC точка по RFC 5480 §2.2, тоест 0x04, следван от X и Y, така че HotPDF проверява, че е точно 1 + 2 * CoordinateSize байта и започва с 0x04, преди да докосне 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;
Отбележете последния параметър. HPDFECDSAVerifyDigest приема също eseP1363 за извикващи, които вече държат подпис с фиксирана ширина — от хардуерен token или отдалечена услуга за подписване, връщаща суров r || s. Този път все още налага проверката за дължина и не-нула и на двете половини, така че буфер с правилен размер, пълен с нули, се отхвърля, вместо да бъде подаден на доставчика
Защо генеричното име на ECDSA алгоритъма се проваля на по-стар Windows?
Защото генеричното име е по-ново от базата за разгръщане, в която доставяте. CNG излага идентификатор на алгоритъм ECDSA, който извежда кривата от импортирания ключ, и е чистият начин да се напише този код, но BCryptOpenAlgorithmProvider е гарантирано да го разреши само на по-нови версии на Windows. На по-стара машина отварящото извикване се проваля, дескрипторът на доставчика остава nil, и всяка ECDSA верификация във вашето приложение отчита неподдържан за подпис, който е напълно добър. HotPDF избягва пропастта, като отваря идентификаторите по крива вместо това. Той разрешава ECDSA_P256, ECDSA_P384 и ECDSA_P521 веднъж, кешира по един дескриптор на доставчик за крива, и ги затваря при финализация на unit-а. Всяка верификация тогава прави само евтината работа: импортира временен публичен ключ от ECCPUBLICBLOB, извиква BCryptVerifySignature, унищожава ключа. Без повторен LoadLibrary, без повторен GetProcAddress, без отваряне и затваряне на доставчик за всеки подпис. Batch верификация на няколкостотин документа усеща разликата, както и service процес, който иначе би завъртял дескрипторите на доставчик под натоварване
Кодовете за резултат остават честни за разликата. evrProviderUnavailable означава, че машината не е могла да даде на HotPDF доставчик; evrInvalid означава, че CNG е отговорил STATUS_INVALID_SIGNATURE. Свиването на тези два в един провал е начинът, по който проблем с разгръщането бива грешно докладван като подправен документ. Същото разделение между провал на средата и криптографски провал минава през CNG и CAPI обработката на страната на подписването, разгледана в статията за подписване от хранилището на сертификати и байтовия ред
Кой сертификат е подписал това? SignerIdentifier е две различни неща
RFC 5652 §5.3 прави SignerIdentifier CHOICE, а verifier, обработващ само едно рамо, тихо ще верифицира срещу грешния ключ. Първото рамо е issuerAndSerialNumber, SEQUENCE, държащ издателското Name в сурово DER и серийния INTEGER, а съвпадането е байтово сравнение спрямо всеки сертификат в множеството certificates на CMS. Второто рамо е [0] subjectKeyIdentifier, имплицитно тагнат OCTET STRING, а съвпадането изисква ровене в сертификата, а не сравняване на неговите header полета
Ровенето има слой, който изненадва хората. Идентификаторът на ключа живее в X.509v3 разширение, така че HotPDF обхожда полето [3] extensions на tbsCertificate, намира разширението, чийто OID е 2.5.29.14, пропуска опционалния critical BOOLEAN, и взима OCTET STRING extnValue. Този octet string не е идентификаторът. Според RFC 5280 §4.2.1.2 неговото съдържание само по себе си е DER, а типът KeyIdentifier е друг OCTET STRING, така че парсвате втори път, за да стигнете до истинските байтове. Спрете един слой по-рано и сравнявате 22-байтова обвивка спрямо 20-байтов идентификатор, никой сертификат не съвпада, а verifier-ът пада на каквато и heuristic да сте написали след това, което е истинската опасност. Взимането на първия сертификат в множеството е примамлив пряк път и е грешен винаги когато CMS носи верига, което е в повечето случаи, защото листото не е задължено да идва първо. HotPDF приема несъвпаднал сертификат само когато контейнерът държи точно един; с множество сертификати налице, точно съвпадение на SignerIdentifier е задължително. Верификацията на digest срещу публичен ключ на intermediate CA не произвежда приятелска грешка, тя произвежда уверено невалиден за документ, който е наред
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 отчита P-256, P-384 или P-521, така че audit лог записва коя крива действително е използвана, вместо просто думата ECDSA. Документо-нивата инсталация около това извикване, по-конкретно как сегментите на /ByteRange се хешират и защо digest-ът трябва да бъде изчислен върху файла, а не върху парснатото дърво от обекти, е предмет на съпътстващата статия за верификация на PDF подписи
Какво това не ви дава
Зелен резултат от HPDFECDSAVerifyDigest отговаря само на един въпрос: тези байтове са подписани от частния ключ, съответстващ на този публичен ключ. Той не казва нищо за това дали този ключ принадлежи на някой, на когото трябва да имате доверие. Изграждането на верига до trust anchor, отменянето чрез CRL или OCSP и проверките на политики са отделна работа, и всеки продукт, докладващ валиден подпис без тях, докладва по-малко, отколкото потребителят предполага. Датите на валидност на сертификата се показват отделно в THPDFSignatureInfo точно поради тази причина: подпис може да верифицира криптографски, докато сертификатът, направил го, е изтекъл преди две години. Поддръжката на криви също е умишлено тясна. Обработват се три NIST prime криви, а подпис върху всяка друга крива връща неподдържан, вместо предположение. Пътят на CNG е само за Windows, което е правилният компромис за VCL компонент, но си струва да се посочи преди да планирате cross-platform услуга около него. И строгостта не е конфигурируема: няма снизходителен режим, приемащ не-минимална DER дължина, защото някой legacy подписващ е излъчил такава. Ако срещнете такъв файл в production, честният отговор е да го запишете и да проследите производителя, не да разширявате парсъра, докато файлът мине
Пътят на ECDSA верификацията, описан тук, се доставя като част от стандартния HotPDF Component за Delphi и C++Builder, заедно с пътищата за RSA PKCS#1 v1.5 и RSA-PSS и пълния запис за информация за подпис; продуктовата страница носи пълния справочник за цифрови подписи