PDFium VCL traktuje weryfikację CMS jako wymienny backend za interfejsem IPdfCmsVerifier, więc walidator PAdES może chodzić na Windows przez CryptoAPI, na macOS przez Keychain i wszędzie, gdzie jest OpenSSL, przez ConfigureSslCmsVerifier. Interfejs jest mały. Trzy zachowania OpenSSL pod spodem produkują pewne siebie, złe odpowiedzi, jeśli zaimplementujesz go naiwnie
Motywacja jest jasna, skoro aplikacja Delphi opuszcza Windows. Walidacja podpisów to jedna z niewielu dziedzin, w których platformowy stos kryptografii nie jest detalem implementacyjnym: rozstrzyga, które certyfikaty są zaufane, jakie algorytmy istnieją i co znaczy unieważnienie. Zaszyj jedną na sztywno, a kod się nie przeproci. Zaabstrahuj ją źle, a każda platforma raportuje inaczej ukształtowaną odpowiedź, której wołający nie umie porównać
Co abstrakcja faktycznie musi nieść
Dwa kształty weryfikacji i trzy niezależne wyroki. Podpis PDF jest detached: podpisana zawartość to dwa zakresy bajtów po obu stronach dziury /Contents, więc VerifyDetached bierze dwa segmenty, a nie jeden bufor. Token znacznika czasu jest attached, wozi własną zawartość, więc VerifyAttached bierze tylko DER
Wynik rozdziela się na trzy statusy, bo odpowiadają na trzy różne pytania i mogą się różnić. SignatureStatus mówi, czy bajty zostały podpisane kluczem z certyfikatu podpisującego. TrustStatus mówi, czy ten certyfikat chainuje do czegoś zaufanego. RevocationStatus mówi, czy certyfikat był wciąż ważny w istotnym momencie. Dokument z matematycznie doskonałym podpisem od certyfikatu, o którym nigdy nie słyszałeś, jest ważny, niezaufany i nieznany, a zgniecenie tego do jednego booleana to sposób, w jaki walidatory skończą okłamywać użytkowników
uses
FPdfCrypto, FPdfCryptoSsl;
var
Options: TPdfCmsVerifyOptions;
begin
if not SslAvailable then
raise Exception.Create('libcrypto not usable: ' + SslMissingSymbols);
ConfigureSslTrustAnchors(LoadCorporateRoots); // DER, może być puste
ConfigureSslCrls(LoadFreshCrls); // DER, może być puste
ConfigureSslCmsVerifier; // instaluje 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 wygląda na ciekawostkę i nią nie jest. Każdy kod błędu OpenSSL i każda flaga magazynu przekracza granicę jako C-owe unsigned long, czyli cztery bajty na Windows i osiem na Linuxie i macOS. Zadeklarujesz to jako stały typ 32-bitowy, kod działa na Windows, a potem po cichu czyta pół wartości na LP64. Raportowanie założonych szerokości jako ciągu, na którym możesz zrobić asercję w teście, zamienia całą klasę dryfu ABI platform w jednolinijkową kontrolę. Każdy, kto przegrał z tym samym problemem przy CK_ULONG w bindingu PKCS#11, rozpozna to natychmiast; ta historia jest w artykule pakowanie struktur PKCS#11 i szerokość CK_ULONG
Dlaczego drugi przebieg weryfikacji widzi pustą zawartość?
Bo CMS_verify czyta BIO zawartości detached do końca pliku, a BIO, które zostało przeczytane, nie jest przewijane za ciebie. Weryfikacja w dwóch przebiegach to rozsądny projekt: najpierw sam podpis kryptograficzny z wyciszoną oceną łańcucha, potem pełna ocena, i polegnie w wyjątkowo zwodniczy sposób, jeśli oba przebiegi dzielą jedno BIO
Drugi przebieg dostaje zero bajtów zawartości. W trybie detached to nie jest błąd, bo pusty bufor zawartości to legalne wejście. Digest po prostu się nie zgadza, a porażka wychodzi jako niepowodzenie budowania łańcucha, a nie zawartości, co wysyła cię do inspekcji certyfikatów i magazynów zaufania, podczas gdy prawdziwy problem to pozycja strumienia. Buduj pamięciowe BIO przez BIO_new_mem_buf na każdy przebieg. Kosztuje jedną alokację i usuwa tę możliwość całkowicie
Co flaga no-verify wycisza, a czego nie
CMS_NO_SIGNER_CERT_VERIFY wycisza ocenę łańcucha, a nie wyszukiwanie certyfikatu podpisującego. Wewnętrznie OpenSSL rozwiązuje i doczepia certyfikaty podpisującego, zanim zajrzy do flagi, więc po pierwszym przebiegu z tą flagą podpisujący jest już dostępny, a jego identyfikatory algorytmów można czytać od razu. Nie trzeba odpalać drugiej pełnej weryfikacji tylko po to, by zdobyć certyfikat podpisującego, co kusi z nazwy flagi
Jedna reguła własności idzie z tym w pakiecie. Referencja podpisującego należy do struktury CMS i nie wolno jej zwalniać niezależnie. Jest ważna tak długo, jak struktura, a jej zwolnienie produkuje uszkodzenie, którego objaw pokazuje się gdziekolwiek indziej, zwykle przy sprzątaniu niezwiązanego obiektu
Dlaczego włączenie sprawdzania CRL odrzuca każdy podpis?
Bo OpenSSL sprawdza CRL-y wyłącznie względem tego, co magazyn już ma, i sam niczego nie pobiera. Nie podąża za punktami dystrybucji CRL i nie mówi OCSP. Ustawisz X509_V_FLAG_CRL_CHECK na magazynie bez CRL-ów i każdy łańcuch polegnie z niezdolnością zdobycia CRL certyfikatu. Wynik wygląda jak sprawdzanie unieważnień, które działa i znajduje problemy. To sprawdzanie unieważnień, które nigdy w ogóle nie chodziło
Backend ustawia więc flagę tylko wtedy, gdy ConfigureSslCrls faktycznie dostarczył przynajmniej jeden CRL. Bez niego RevocationStatus wraca jako pcvsUnsupported, co jest szczerym stwierdzeniem, że pytanie nie zostało odpowiedziane. Z tego samego powodu OnlineRetrieval nie ma na tym backendzie efektu i żaden checkpoint pcvstOnlineRetrieval nie jest emitowany: nie ma ścieżki pobierania, z której można raportować postęp
To pozycja projektowa warta obrony w ogóle. Walidator, który nie umie sprawdzić unieważnień, powinien tak powiedzieć. Raportowanie niesprawdzonego certyfikatu jako nieunieważnionego to najczęstszy pojedynczy sposób, w jaki narzędzia walidacji podpisów wprowadzają swoich użytkowników w błąd, i dokładnie ta klasa zamieszania jest eksplorowana w artykule dlaczego walidatory odrzucają podpisy PAdES
// Checkpointy pozwalają UI pokazać, który etap chodzi, i mówią ci,
// które etapy backend faktycznie wykonuje
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;
// Czytaj trzy wyroki osobno; wolno im się różnić
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');
Bindowanie do biblioteki, której nie przypniesz
OpenSSL przemianował swoje akcesory stosu między 1.0 a 1.1, więc ta sama logiczna funkcja ma dwie możliwe nazwy eksportu, zależnie od builda, jaki akurat ma host. Binding rozwiązuje najpierw nowszą nazwę i schodzi na starszą, a brakujący symbol odnotowuje dopiero, gdy żadna się nie rozwiąże. To właściwy kształt każdego bindowania dynamicznego do biblioteki, której nie dystrybuujesz: preferuj bieżące nazwy, toleruj historyczne i raportuj wyłącznie rzeczywisty brak
SslMissingSymbols to to, co zamienia nieudane załadowanie w zdarzenie dające się zdiagnozować. Niepusty wynik na hoście, który wyraźnie ma zainstalowane libcrypto, znaczy, że zainstalowana wersja jest starsza niż API, pod które celuje ten build, czyli zupełnie inna rozmowa supportowa niż brakująca biblioteka. ConfigureSslLibraryPath pokrywa drugi częsty przypadek: host z kilkoma buildami OpenSSL, gdzie ta na domyślnej ścieżce wyszukiwania nie jest tą, którą chcesz
Wybór backendu per platforma
Praktyczny układ to wybór przy starcie i zapisanie, który odpowiedział. Na Windows backend platformowy integruje się z magazynami certyfikatów, które przedsiębiorstwo już prowadzi, i to zwykle jest to, czego chcesz. Na macOS backend Keychain pasuje do tego samego rozumowania i jest opisany w artykule weryfikacja podpisów z SecTrust na macOS. OpenSSL to opcja przenośna i to też właściwy wybór, gdy potrzebujesz polityki walidacji identycznej między platformami, a nie takiej, która podąża za magazynem zaufania każdej z nich
Cokolwiek zainstalujesz, loguj PadesCmsVerificationBackendName obok każdego wyroku, jaki zapisujesz. Zapisany wynik walidacji bez backendu, który go wyprodukował, nie da się później odtworzyć, bo trzy wartości statusu znaczą subtelnie różne rzeczy, zależnie od tego, który stos odpowiedział. Warstwa inspekcji podpisów na tym wszystkim, łącznie ze sposobem raportowania poziomów PAdES, jest opisana w artykule inspekcja cyfrowych podpisów PDF i poziomów PAdES
Całość jest dostarczana jako źródła z komponentem PDFium dla Delphi, co ma tu większe znaczenie niż zwykle: dla walidatora podpisów możliwość przeczytania dokładnie, jakie flagi backend ustawia i które kontrole pomija, to nie nice-to-have, to jedyny sposób, by wiedzieć, co zielony ptaszek w twojej aplikacji faktycznie twierdzi