Komponent PDFium dla Delphi weryfikuje podpisy PDF na macOS przez TPdfKeychainCmsVerifier, backend weryfikacji CMS zbudowany na Apple CMSDecoder i SecTrust zamiast na ręcznie parsowanym CMS. ConfigureKeychainCmsVerifier go instaluje, a pojedyncze wywołanie CMSDecoderCopySignerStatus zwraca werdykt podpisu, uchwyt SecTrust i kod wyniku certyfikatu, czyli dokładnie tę parę kolumn, którą TPdfCmsVerifyResult już niósł w Windows
Przypadek, który wymusił tę pracę, jest nudny i częsty. Build Lazarusa archiwum dokumentów działa na Macu, otwiera podpisaną umowę, a każdy podpis wraca jako pcsUnsupported. Z plikiem nic nie jest nie tak. Weryfikacja podpisu po prostu nie miała backendu poza Windows, a walidator PAdES odmawiał zgadywania pod jego nieobecność. Wersja 3.111.0 PDFiumPas otworzyła miejsce przez IPdfCmsVerifier i ConfigurePadesCmsVerifier, a wersja 3.113.0 wypełniła je na macOS. Ciekawy element portu to nie plumbing, lecz trzy miejsca, w których API Apple ma inny kształt niż windowsowe
Dlaczego podpis PDF obejmuje dwa zakresy bajtów?
Ponieważ podpis nie może obejmować bajtów, które go przechowują. ISO 32000-1 §12.8.1 umieszcza blob CMS SignedData w łańcuchu /Contents słownika podpisu i opisuje podpisany zakres przez /ByteRange, czyli zestaw par offsetu i długości obejmujących wszystko po obu stronach tej dziury. Dwa segmenty, jedna luka pośrodku, na każdej platformie
Platformy różnią się tym, jak te segmenty trafiają do warstwy kryptograficznej, a różnica kosztuje pamięć. W Windows CryptVerifyDetachedMessageSignature przyjmuje tablicę wskaźników i długości, więc oba zakresy przekazuje się tak, jak leżą w buforze, bez duplikowania. Apple CMSDecoderSetDetachedContent przyjmuje jedno CFData i nie ma wariantu wielosegmentowego, więc backend macOS konkatenatuje oba zakresy do ciągłego bufora przed dekodowaniem. To pełna druga kopia podpisanych bajtów. W skanowanym archiwum 400 MB jest to prawdziwy szczyt pamięci, skaluje się z dokumentem, a nie z podpisem, i nie ma alternatywnego API, po które można sięgnąć. Odpowiednio wymiaruj worker batcha zamiast odkrywać to na maszynie klienta
Jedno wywołanie wypełnia dwie kolumny TPdfCmsVerifyResult
CMSDecoderCopySignerStatus jest niezwykle hojnym punktem wejścia Security.framework: jedno wywołanie zwraca status podpisującego, SecTrustRef dla zbudowanego łańcucha oraz OSStatus wyniku oceny certyfikatu. Wartości trafiają bezpośrednio do rekordu konsumowanego już przez walidator PAdES: status podpisującego staje się SignatureStatus, wynik certyfikatu staje się TrustStatus, a surowe wartości zachowują się w SignatureError i TrustError, aby zgłoszenie wsparcia mogło cytować liczbę zamiast przymiotnika. Wywołujący nigdy nie dotykają sami IPdfCmsVerifier — ValidatePadesCompliance i ValidatePadesTrust kierują każdą weryfikację przez zainstalowany backend, więc kod odczytujący TPadesSignatureValidation jest bajt w bajt taki sam na obu platformach, jak opisano w instrukcji inspekcji słowników podpisu PDF i poziomów PAdES w Delphi
uses
FPdfCrypto, FPdfCryptoMac, FPdfPades;
procedure InstallMacVerifier;
begin
// Podpisywanie i weryfikacja rozwiązują różne symbole frameworka, więc jeden
// może być obecny, gdy drugiego nie ma
if not KeychainVerificationAvailable then
raise Exception.CreateFmt('Security.framework symbols missing: %s',
[KeychainMissingSymbols]);
ConfigureKeychainCmsVerifier;
// PadesCmsVerificationBackendName zwraca teraz 'macOS Security.framework'
if not PadesCmsVerificationAvailable then
raise Exception.Create('No CMS verification backend is installed');
end;
Dlaczego kCMSSignerInvalidCert zgłasza poprawny podpis?
Ponieważ Apple przypisuje tej wartości węższe znaczenie, niż sugeruje jej nazwa: sam podpis został zweryfikowany i tylko łańcucha certyfikatów nie udało się ustanowić. TPdfKeychainCmsVerifier mapuje więc kCMSSignerInvalidCert na pcvsValid w kolumnie SignatureStatus, a problem certyfikatu ujawnia przez TrustStatus, gdzie problem łańcucha należy. Włączenie go do werdyktu podpisu sprawiłoby, że komponent powiedziałby operatorowi, iż nietknięty dokument został zmodyfikowany, co jest najgorszym pojedynczym fałszywym alarmem, jaki może podnieść walidator podpisu
function MapSignerStatus(Status: LongWord): TPdfCmsVerifyStatus;
begin
case Status of
kCMSSignerValid:
Result:= pcvsValid;
// Podpis został zweryfikowany, a tylko łańcuch nie, co osobno
// raportuje status zaufania
kCMSSignerInvalidCert:
Result:= pcvsValid;
kCMSSignerInvalidSignature, kCMSSignerUnsigned:
Result:= pcvsInvalid;
else
Result:= pcvsIndeterminate;
end;
end;
Odczytaj oba statusy jako uporządkowaną parę, a logika raportowania napisze się sama. SignatureStatus = pcvsValid razem z TrustStatus = pcvsInvalid opisuje dokument, którego bajty są nienaruszone, ale którego wystawcy ten konkretny Mac nie ufa: kotwica brakuje w Keychain, wygasł pośredni certyfikat albo łańcucha nie da się uzupełnić offline. To pytanie o politykę operatora, a nie o integralność dokumentu, i dokładnie to rozróżnienie stoi za większością przypadków w notatce o tym, dlaczego walidatory odrzucają kryptograficznie poprawne podpisy PAdES
Gdzie macOS faktycznie sprawdza odwołanie certyfikatu?
Wewnątrz oceny zaufania, dlatego TPdfCmsVerifyResult.RevocationStatus następuje po TrustStatus, zamiast przenosić własny werdykt. SecPolicyCreateRevocation tworzy politykę, ta polityka dołącza do SecPolicyCreateBasicX509 w tablicy przekazywanej do CMSDecoderCopySignerStatus, a praca OCSP albo CRL odbywa się podczas budowania łańcucha. Nie wraca osobna odpowiedź, więc jej raportowanie oznaczałoby wymyślanie. Sama tablica niesie małą regułę własności, którą warto nazwać: CFArrayCreate zatrzymuje obie polityki, więc dwie lokalne referencje są natychmiast zwalniane, a przypadek jednej polityki pomija tablicę całkowicie i przekazuje politykę bezpośrednio, w formie również akceptowanej przez API
Praca offline jest jawną flagą, a nie przypadkiem wynikającym z łączności. Gdy TPdfCmsVerifyOptions.OnlineRetrieval ma wartość False, backend dodaje kSecRevocationNetworkAccessDisabled, ograniczając ocenę do odpowiedzi już zapisanych w cache na maszynie, a callback checkpointu nadal wywołuje pcvstCryptographicSignature, pcvstChainBuild i pcvstRevocationCheck w tej samej kolejności, w jakiej raportuje je backend Windows. Kod aplikacji ustawia to wszystko przez rekord opcji wyższego poziomu
var
Options: TPadesTrustValidationOptions;
Report: TPadesValidationResult;
Stream: TFileStream;
begin
Options:= TPadesTrustValidationOptions.Default;
Options.CheckRevocation:= True;
Options.NetworkPolicy:= ptnpOffline; // tylko odpowiedzi z cache
Options.CheckTimeStamps:= True;
Stream:= TFileStream.Create('contract.pdf', fmOpenRead or fmShareDenyWrite);
try
Report:= ValidatePadesTrust(Stream, Options);
finally
Stream.Free;
end;
if Report.SignatureCount= 0 then
Log('No signature dictionary in this document')
else if Report.Signatures[0].CmsSignatureStatus <> pcsValid then
Log('Document integrity failed')
else if Report.Signatures[0].CertificateTrustStatus <> pcsValid then
Log('Bytes intact, chain not trusted on this Mac');
end;
Get kontra copy: zwolnienie, które wybucha gdzie indziej
SecTrustGetCertificateAtIndex ma semantykę get i zwrócona przez nie referencja nigdy nie może być zwalniana, podczas gdy CMSDecoderCopySignerCert i SecCertificateCopyData, znajdujące się kilka linii dalej w tej samej rutynie, mają semantykę copy i muszą być zwalniane. Core Foundation koduje całą regułę w jednym czasowniku nazwy funkcji, a system typów nie wymusza żadnej z niej. Zwalniaj pożyczoną referencję, a w miejscu wywołania nic się nie stanie: obiekt zaufania po prostu stanie się niespójny, a awaria nadejdzie później, w miejscu pozbawionym widocznego związku z łańcuchami certyfikatów
ChainCount:= _SecTrustGetCertificateCount(Trust);
SetLength(Result.ChainCertificates, ChainCount);
for I:= 0 to ChainCount- 1 do
begin
// Semantyka get: ta referencja jest pożyczona i tutaj się jej nie zwalnia
Cert:= _SecTrustGetCertificateAtIndex(Trust, I);
if Cert= nil then
Continue;
// Semantyka copy: ta referencja jest własnością kodu i musi wrócić
CertData:= _SecCertificateCopyData(Cert);
if CertData= nil then
Continue;
try
Result.ChainCertificates[I]:= CFDataToBytes(CertData);
finally
_CFRelease(CertData);
end;
end;
Co gwarantuje verifier, gdy żaden backend nie odpowiada?
Że odpowiedź jest nieobsługiwana, nigdy po cichu pozytywna. Gdy ConfigurePadesCmsVerifier niczego nie zainstalowało, a domyślna platformy nie może pomóc, TPdfCmsVerifyResult wraca ze wszystkimi kolumnami ustawionymi jako niedostępne, a walidator PAdES mapuje to na pcsUnsupported, więc build bez backendu kryptograficznego raportuje uczciwie zamiast twierdzić cokolwiek o podpisie. Wiązanie macOS jest celowo konserwatywne w tym samym kierunku: Security.framework i CoreFoundation są osiągane przez dlopen i dlsym, więc brak frameworka albo nazwa symbolu pomylona przez to wiązanie ujawnia się jako KeychainVerificationAvailable zwracające False z KeychainMissingSymbols nazywającym winowajcę, a nie jako błąd linkowania i nie jako zły werdykt. To ta sama postawa fail-closed, którą komponent stosuje przy szukaniu biblioteki natywnej, opisana w tekście o ładowaniu natywnej biblioteki PDFium na dowolnym celu
Weryfikacja podpisu to ta część stosu PDF, w której cicha pomyłka jest gorsza niż głośna niedostępność, a macOS daje API dość hojne, by łatwo osiągnąć oba wyniki. Połącz zakresy bajtów i zaakceptuj kopię, trzymaj werdykt podpisu oraz werdykt łańcucha w osobnych kolumnach, szanuj czasowniki get i copy, a brakujący backend pozwól zgłosić. Jeśli przenosisz workflow dokumentów Delphi albo Free Pascal na Maca i potrzebujesz podpisywania oraz walidacji PAdES po obu stronach, komponent PDFium dla Delphi dostarcza backend Keychain obok windowsowego za jednym interfejsem