PDFium VCL третира CMS верификацията като сменяем backend зад интерфейса IPdfCmsVerifier, така че PAdES валидаторът може да върви на Windows през CryptoAPI, на macOS през Keychain и навсякъде, където има OpenSSL, през ConfigureSslCmsVerifier. Интерфейсът е малък. Три OpenSSL поведения под него произвеждат уверени грешни отговори, ако го имплементирате наивно
Мотивацията е съвсем ясна щом Delphi приложение напусне Windows. Валидацията на подписи е една от малкото области, където платформеният crypto стек не е имплементационен детайл: той решава кои сертификати са доверени, кои алгоритъми съществуват и какво значи отмяна. Закодирайте един и кодът не се пренася. Абстрахирайте го зле и всяка платформа докладва различно оформен отговор, който викащият не може да сравни
Какво реално трябва да носи абстракцията
Две форми на верификация и три независими присъди. PDF подпис е detached: подписаното съдържание са двата байтови диапазона от двете страни на дупката /Contents, така че VerifyDetached взима два сегмента вместо един буфер. Timestamp токен е attached, носещ собствено съдържание, така че VerifyAttached взима само DER-а
Резултатът се разделя на три статуса, защото отговарят на три различни въпроса и могат да не са съгласни. SignatureStatus казва дали байтовете са подписани с ключа в сертификата на подписвача. TrustStatus казва дали този сертификат изгражда верига към нещо, на което вярвате. RevocationStatus казва дали сертификатът е бил валиден в съответния момент. Документ с математически перфектен подпис от сертификат, за който никога не сте чували, е валиден, недоверен и неизвестен, а свиването на това в един булев е начинът, по който валидаторите свършват, лъжейки потребителите си
uses
FPdfCrypto, FPdfCryptoSsl;
var
Options: TPdfCmsVerifyOptions;
begin
if not SslAvailable then
raise Exception.Create('libcrypto not usable: ' + SslMissingSymbols);
ConfigureSslTrustAnchors(LoadCorporateRoots); // DER, може да е празно
ConfigureSslCrls(LoadFreshCrls); // DER, може да е празно
ConfigureSslCmsVerifier; // инсталира backend-а
Writeln('backend : ', PadesCmsVerificationBackendName);
Writeln('library : ', SslLibraryPath, ' ', SslLibraryVersion);
Writeln('ABI : ', SslAbiLayout); // ulong=<n> long=<n>
Options := TPdfCmsVerifyOptions.Default;
Options.CheckRevocation := True;
Options.CollectChainCertificates := True;
end;
SslAbiLayout изглежда като любопитство и не е. Всеки OpenSSL код за грешка и всеки store флаг пресича границата като C unsigned long, което е четири байта на Windows и осем на Linux и macOS. Обявите ли го като фиксиран 32-битов тип, кодът работи на Windows, после безшумно чете половин стойност на LP64. Докладването на допусканите широчини като низ, който можете да утвърждавате в тест, превръща целия клас платформен ABI drift в проверка на един ред. Всеки, минал през същия проблем с CK_ULONG в PKCS#11 binding, ще го разпознае мигновено; тази история е в PKCS#11 struct packing и CK_ULONG широчина
Защо вторият верификационен пас вижда празно съдържание?
Защото CMS_verify чете detached content BIO-то до край на файла, а BIO, което е било четено, не се превърта обратно за вас. Верифицирането на два паса е разумно дизайнерско решение – първо само криптографският подпис с подтисната оценка на веригата, после пълната оценка – и се проваля по необичайно измамен начин, ако и двата паса споделят едно BIO
Вторият пас получава нула байта съдържание. В detached режим това не е грешка, защото празен content буфер е легален вход. Дайджестът просто не съвпада, а провалът се проявява като провал на изграждане на верига, а не като провал на съдържанието, което ви праща да оглеждате сертификати и trust store-ове, докато истинският проблем е позиция в stream. Възстановявайте memory BIO-то с BIO_new_mem_buf за всеки пас. Струва една алокация и премахва възможността изцяло
Какво флагът no-verify потиска и какво не
CMS_NO_SIGNER_CERT_VERIFY подтиска оценката на веригата, не търсенето на сертификата на подписвача. Вътрешно OpenSSL разрешава и прикача сертификатите на подписвача, преди да се допита до флага, така че след първи пас, носещ този флаг, подписвачът вече е наличен и алгоритмичните му идентификатори могат да бъдат прочетени веднага. Няма нужда да пускате второ пълно верифициране само за да получите сертификата на подписвача – това, което името на флага ви искушава да допуснете
Едно правило за собственост върви с това. Референцията към подписвача принадлежи на CMS структурата и не бива да бъде освобождавана независимо. Валидна е, докато съществува структурата, а освобождаването ѝ произвежда разваляне, чийто симптом се появява съвсем другаде, обикновено при изчистване на несвързан обект
Защо включването на CRL проверка отхвърля всеки подпис?
Защото OpenSSL проверява CRL-ите само срещу това, което store-ът вече държи, и сам не извлича нищо. Не следва CRL distribution точки и не говори OCSP. Задайте ли X509_V_FLAG_CRL_CHECK на store без нито един CRL, всяка верига се проваля с неспособност да получи certificate CRL. Резултатът изглежда като проверка за отмяна, работеща и намираща проблеми. Това е проверка за отмяна, никога не пускана изобщо
Backend-ът затова задава флага само когато ConfigureSslCrls действително е доставила поне един CRL. Без такъв RevocationStatus се връща като pcvsUnsupported, което е честно твърдение, че въпросът не е бил отговорен. По същата причина OnlineRetrieval няма ефект върху този backend и никакъв pcvstOnlineRetrieval checkpoint не се излъчва: няма път за извличане, от който да докладва прогрес
Това е дизайнерска позиция, заслужаваща защита изобщо. Валидатор, който не може да провери отмяна, трябва да го каже. Докладването на непроверен сертификат като неотменен е единственият най-чест начин инструментите за валидация на подписи да подвеждат потребителите си, и е точно класът объркване, проучен в защо валидаторите отхвърлят PAdES подписи
// Checkpoint-ите позволяват на UI да показва кой етап върви, и ви казват
// кои етапи един backend реално извършва
type
TSignatureProbe = class
procedure Checkpoint(Stage: TPdfCmsVerifyStage);
end;
procedure TSignatureProbe.Checkpoint(Stage: TPdfCmsVerifyStage);
begin
case Stage of
pcvstCryptographicSignature: Status('checking the signature');
pcvstChainBuild: Status('building the certificate chain');
pcvstOnlineRetrieval: Status('fetching validation data');
pcvstRevocationCheck: Status('checking revocation');
end;
end;
// Прочетете трите присъди поотделно; позволено им е да не са съгласни
if Result.SignatureStatus = pcvsValid then
case Result.TrustStatus of
pcvsValid: Report('signed and trusted');
pcvsInvalid: Report('signed, chain rejected');
pcvsUnsupported,
pcvsIndeterminate: Report('signed, trust not established');
end;
if Result.RevocationStatus = pcvsUnsupported then
Report('revocation was not checked on this backend');
Свързване към библиотека, която не можете да заковете
OpenSSL преименува своите stack accessors между 1.0 и 1.1, така че същата логическа функция има две възможни експортни имена в зависимост от build-а, който host-ът случайно има. Binding-ът разрешава първо по-новото име и пада обратно на по-старото, и записва липсващ символ само когато нито едното не се разреши. Това е правилната форма за всяко динамично свързване към библиотека, която не ship-вате: предпочитайте актуални имена, търпейте исторически, и докладвайте само истинско отсъствие
SslMissingSymbols е това, което превръща провалено зареждане в диагностично събитие. Непразен резултат на host, който ясно има инсталиран libcrypto, значи, че инсталираната версия е по-стара от API-то, което този build цели – съвсем различен support разговор от липсваща библиотека. ConfigureSslLibraryPath покрива другия чест случай: host с няколко OpenSSL build-а, където този по подразбиращия се search път не е този, който искате
Избор на backend по платформа
Практическата уредба е да изберете при стартиране и да запишете кой е отговорил. На Windows платформеният backend се интегрира със certificate store-овете, които едно предприятие вече управлява, което обикновено е това, което искате. На macOS Keychain backend-ът пасва на същото разсъждение и е описан в верифициране на подписи със SecTrust на macOS. OpenSSL е портативният вариант и е също правилният избор, когато ви трябва политика за валидация, идентична из платформите, вместо такава, следваща trust store-а на всяка платформа
Който и да инсталирате, логвайте PadesCmsVerificationBackendName до всяка присъда, която записвате. Съхранен резултат от валидация без backend-а, произвел го, не може да бъде възпроизведен после, защото трите статус стойности значат фино различни неща в зависимост от това кой стек е отговорил. Слоят за оглед на подписи върху всичко това, включително как се докладват PAdES нивата, е обхванат в оглед на PDF цифрови подписи и PAdES нива
Всичко това ship-ва като source с PDFium Delphi компонента, което тук значи повече от обикновено: за валидатор на подписи възможността да прочетете точно кои флагове задава един backend и кои проверки прескача не е приятна добавка, а единственият начин да знаете какво реално твърди зелена отметка във вашето приложение