Техническа статия

CMS верификация с OpenSSL backend в PDFium VCL

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 не се излъчва: няма път за извличане, от който да докладва прогрес

Диаграма на трите капана в OpenSSL CMS verifier-а на PDFium VCL: споделено content BIO, прочетено до край на файла, оставя втория верификационен пас с нула байта, CMS_NO_SIGNER_CERT_VERIFY подтиска оценката на веригата, но не и търсенето на подписвача, а CRL проверка на празен store отхвърля всяка верига, без отмяната изобщо да е вървяла
Всеки капан ражда уверена грешна присъда: позиция в stream се маскира като провал на доверието, флагът no-verify потиска по-малко, отколкото името му внушава, а никога не пускана отмяна изглежда като отмяна, намерила проблеми

Това е дизайнерска позиция, заслужаваща защита изобщо. Валидатор, който не може да провери отмяна, трябва да го каже. Докладването на непроверен сертификат като неотменен е единственият най-чест начин инструментите за валидация на подписи да подвеждат потребителите си, и е точно класът объркване, проучен в защо валидаторите отхвърлят 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-а на всяка платформа

Диаграма на абстракцията IPdfCmsVerifier в PDFium VCL, носеща VerifyDetached върху двата байтови диапазона около дупката Contents и VerifyAttached за timestamp токени, трите независими присъди SignatureStatus, TrustStatus и RevocationStatus, и per-platform backend-и, избирани при стартиране чрез CryptoAPI, SecTrust или ConfigureSslCmsVerifier
Интерфейсът носи две форми на верификация и три присъди, защото отговарят на различни въпроси и могат да не са съгласни, а инсталираният backend се записва до всяка присъда, така че съхранени резултати могат да бъдат възпроизведени

Който и да инсталирате, логвайте PadesCmsVerificationBackendName до всяка присъда, която записвате. Съхранен резултат от валидация без backend-а, произвел го, не може да бъде възпроизведен после, защото трите статус стойности значат фино различни неща в зависимост от това кой стек е отговорил. Слоят за оглед на подписи върху всичко това, включително как се докладват PAdES нивата, е обхванат в оглед на PDF цифрови подписи и PAdES нива

Всичко това ship-ва като source с PDFium Delphi компонента, което тук значи повече от обикновено: за валидатор на подписи възможността да прочетете точно кои флагове задава един backend и кои проверки прескача не е приятна добавка, а единственият начин да знаете какво реално твърди зелена отметка във вашето приложение