A PDFium VCL ma már képes befejezni egy PDF alágírás láncát és hálózaton ellenőrizni a visszavonást az OpenSSL backendjén: ha az OnlineRetrieval be van kapcsolva, a ConfigureSslCmsVerifier olyan ellenőrzőt telepít, amely a hiányzó köztes tanúsítványokat az AIA caIssuers URL-ekről, a CRL-eket pedig a CRL disztribúciós pontokról tölti le, minden ellenőrzési hívásra rászabott fix idő-, kérés- és bájtkereten belül. A letöltött tanúsítványok legfeljebb láncanyagok. A bizalom továbbra is kizárólag a rendszertárolóból és az Ön által beállított horgonyokból jön
A bezárt rés először akkor mutatkozik meg, amikor valós világú PDF-eket érvényesít Linux szerveren. Az aláírók nagy része csak a saját levéltanúsítványát ágyazza be a CMS-be, így az OpenSSL nem ér el gyökérig, a TrustStatus érvénytelenül jön vissza, a visszavonás-ellenőrzés pedig sosem fut el, mert a lánc sosem lett megbízható. A v3.121.0 előtt az PDF aláírások OpenSSL-lel való ellenőrzéséről a PDFium VCL-ben szóló cikkben ismertetett OpenSSL backend szigorúan offline volt, az OnlineRetrieval pedig nem hatott rá. Egy dolgot érdemes előre kimondani: maga a PDFium motor semmilyen CMS-ellenőrzést nem végez, tehát az alábbi minden szabály a komponens PAdES rétegében és annak OpenSSL-kötésében él, ahol olvasható is
Milyen sorrendben ellenőriz, tölt le és vizsgál az OpenSSL backend?
Először az integritás, aztán a bizalom, végül a visszavonás, és a hálózatra csak a szükséges lépések között ér hozzá. A VerifyCmsWithSsl a CMS alágírást és az aláírt attribútumokat (RFC 5652) lánchelyettesítés letiltásával vizsgálja, és ha ez elbukik, azonnal visszaad, még mielőtt bármilyen letöltési szekció létezne, így egy törött bájtos dokumentum kimenő kérést sem vált ki. Csak ha a lánc ezután bukik meg, és az OnlineRetrieval be van kapcsolva, követi az AIA-hivatkozásokat, és újraellenőrzi. A CRL disztribúciós pontokat csak azután tölti le, hogy a lánc megbízhatóvá vált, mert egy megbízhatatlan útra akasztott CRL semmit nem bizonyít. A három ítélet végig külön áll: hiányos lánccal bíró érvényes alágírást továbbra is érvényes alágírásként jelentenek
uses
PDFium, FPdfCrypto, FPdfCryptoSsl, FPdfPades;
var
Pdf: TPdf;
Probe: TPdfCmsVerifyOptions;
Diags: TPdfSslVerifyDiagnostics;
Trust: TPadesTrustValidationOptions;
Verdict: TPadesValidationResult;
I: Integer;
begin
ConfigureSslTrustAnchors(LoadCorporateRoots); // DER; az egyetlen extra bizalom
ConfigureSslCmsVerifier;
Probe := TPdfCmsVerifyOptions.Default;
Probe.OnlineRetrieval := True;
Probe.CheckRevocation := True;
Diags := SslVerifyOptionsDiagnostics(Probe);
if psvdOnlineRetrievalIgnored in Diags then
Log('no HTTP transport or CMS_add1_cert: validation stays offline');
Trust := TPadesTrustValidationOptions.Default; // alapból ptnpOffline
Trust.NetworkPolicy := ptnpOnline;
Trust.CheckRevocation := True;
Trust.UrlRetrievalTimeoutMs := 10000; // ellenőrzési hívásonként
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'signed-contract.pdf';
Pdf.Active := True;
Verdict := Pdf.ValidatePadesTrust(Trust);
for I := 0 to High(Verdict.Signatures) do
Log(Format('#%d trust=%d revocation=%d', [I,
Ord(Verdict.Signatures[I].CertificateTrustStatus),
Ord(Verdict.Signatures[I].RevocationStatus)]));
finally
Pdf.Free;
end;
end;
Miért kerül sosem a bizalmi tárolóba a letöltött tanúsítvány?
Mert az URL-ek az éppen érvényesített tanúsítványból jönnek, és az aláíró választotta őket. Az authorityInfoAccess caIssuers bejegyzés (RFC 5280 §4.2.2.1) csupán jelzés arról, hol lakik a kibocsátó, és semmi több. Ha bárki, aki arra az URL-re válaszol, a bizalmi horgonyok tárolójába kerülne, bárki aláírhatna saját készítésű kulccsal, a saját szerverére irányíthatná az AIA-t, és zöld ítéletet kapna. A RetrieveIntermediates ezért minden feldolgozott tanúsítványt a CMS_add1_cert-nek ad át, amely az adott CMS-struktúra nem megbízható halmazába teszi, és az OpenSSL-nek továbbra is útvonalat kell építenie onnan az Ön által beállított horgonyig vagy a rendszertárolóban meglévőig. Van egy halkabb ok is: a CMS_verify tanúsítványargumentuma nem cserélhető egy-az-egyben a CMS-be ágyazott tanúsítványokra, így magába a CMS-be adni a megbízható út
A letöltési hurok szándékosan szűk. A RetrieveIntermediates legfeljebb 4 kört fut, mindegyik a CMS-be frissen került összes tanúsítványból gyűjti a caIssuers URL-eket, és abban a pillanatban áll le, amikor egy kör semmit nem ad hozzá, vagy elfogy az időkeret. Egy válasznak d2i_X509-cel kell dekódolódnia egyetlen DER tanúsítványként, amely a teljes törzset felemészti; a maradékbájtokat elutasítja, és a .p7c URL-ről kiszolgált PKCS#7, csak tanúsítványos köteget kihagyja kicsomagolás helyett. Ugyanezen AIA-kiterjesztés OCSP hozzáférési módszerét figyelmen kívül hagyja, mert ez a backend nem beszél OCSP-t. A visszavonás oldalán a RetrieveCrls csak minden DistributionPoint fullName URI-jait olvassa (RFC 5280 §4.2.1.13) a CMS tanúsítványaiból és a beállított horgonyokból, a letöltött CRL-ek pedig egy második, független X509_STORE-ba kerülnek teljes láncre CRL-ellenőrzéssel, így egy hiányzó vagy avas CRL a RevocationStatus-ot változtatja meg anélkül, hogy a TrustStatus-hoz érne
// A VerifyCmsWithSsl-ből (FPdfCryptoSsl.pas) sűrítve; a BIO beállítás kimaradt.
// Minden _CMS_verify hívás friss content BIO-t kap
if _CMS_verify(Cms, nil, nil, Bio, nil,
CMS_NO_SIGNER_CERT_VERIFY or CMS_BINARY) <> 1 then
Exit; // törött aláírás: semmi hálózat
if Options.OnlineRetrieval and SslCapabilities.OnlineRetrieval then
FetchSession := TPdfCryptoFetchSession.Create(Options.UrlRetrievalTimeoutMs);
Store := BuildStore(False, nil, RevocationChecked, CrlsMalformed);
if (_CMS_verify(Cms, nil, Store, Bio, nil, CMS_BINARY) <> 1) and
(FetchSession <> nil) then
begin
RetrieveIntermediates(Cms, FetchSession); // CMS_add1_cert, csak nem megbízható
// a lánc újraellenőrzése ugyanazon horgonytárolón
end;
if Options.CheckRevocation and (FetchSession <> nil) and
(Result.TrustStatus = pcvsValid) then
RetrievedCrls := RetrieveCrls(Cms, FetchSession);
// második, külön tároló: a beállított CRL-ek plusz a letöltöttek
Store := BuildStore(True, RetrievedCrls, RevocationChecked, CrlsMalformed);
Mennyibe kerül legfeljebb egy ellenőrzési hívás?
Fix plafonba ütközik, amelyet egyetlen TPdfCryptoFetchSession érvényesít, és amelyet egyetlen ellenőrzési hívás AIA- és CRL-lépései osztoznak. A korlátok konstansok a FPdfCryptoHttp-ban, nem javaslatok:
- Idő:
UrlRetrievalTimeoutMs, amely mind aTPdfCmsVerifyOptions.Default, mind aTPadesTrustValidationOptions.Defaultesetén 15000-re alapértelmezett; a 0-val kreált szekció 30000-re esik vissza, és az óra akkor indul el, amikor az alágírás átment, minden későbbi kérést lefedve - Kérések: legfeljebb 8 szekciónként, a transzport megkísérlése előtt számolva, tehát egy halott gép is felhasznál egy helyet
- Bájtok: válaszonként 1 MiB és összesen 4 MiB, az 2048 karakternél hosszabb URL-eket minden kapcsolódás előtt visszautasítva
A könyvelés szigorúbb, mint elsőre fest. Egy elbukó válaszból érkező bájtok is beleszámítanak az összesítésbe, így egy nagy oldallal 404-et válaszoló szerver nem merítheti ki ingyen a keretet. A válaszonkénti határt átlépő olvasás megszakítja a letöltést, ahelyett hogy csonka törzset adna tovább az ASN.1-elemzőnek, az üres törzsű HTTP 200 pedig lapból elutasításra kerül, mert az AIA út egyébként egy üres tömb Data[0]-jába indexelne. Csak sima http:// és https:// URL megy át, átirányítás, sütik, hitelesítő adatok és automatikus proxy-felderítés nélkül, miközben a HTTPS megtartja a megszokott tanúsítvány- és gépnév-ellenőrzést. Az URL-deduplikáció szándékosan egyetlen hívásra szól: a következő érvényesítésnek látnia kell a frissen kipublikált CRL-t. A keret hívásonkénti is, nem dokumentumonkénti, és a ValidatePadesTrust minden alágírást és minden időbélyeg-tokenet külön ellenőriz, így a legrosszabb eset az alágírások számával nő
Miért írhat még a memóriájába egy időtúllépéses WinHTTP kérés?
Mert az időkorláton való visszatérés nem mondja le a már repülésben lévő callbackeket. A Windows transzport a WinHTTP-t aszinkronosan hajtja, és a szekció hátralévő idejével vár eseményre; amikor ez a várás feladja, a kérés még befejezhet egy olvasást, és utána szignálhat. Mutassa az aszinkron olvasást verempufferre, és ez a késői befejezés olyan keretbe ír, amely akkorra valamilyen idegen függvényé. A javítás a tulajdonlás, nem az időzítés: az esemény és a 16 KB-os olvasáspuffer egy két hivatkozással bíró heap-rekordban lakik, amelyet az egyik a hívó tart, a másikat csak a végső HANDLE_CLOSING callback enged el, így bármelyik oldal fejezi be utoljára, az szabadítja fel a memóriát
type
PHttpState = ^THttpState;
THttpState = record
References: LongInt; // hívó + végső HANDLE_CLOSING callback
Event: THandle;
Status, Count: DWORD;
Buffer: array[0..16383] of Byte; // ide érkeznek az aszinkron olvasások, sosem veremre
end;
procedure ReleaseState(State: PHttpState);
begin
if InterlockedDecrement(State.References) = 0 then
begin
CloseHandle(State.Event);
Dispose(State);
end;
end;
// A status callbackben: a HANDLE_CLOSING az utolsó értesítés, amelyet a
// WinHTTP küld a kérésért, így ez elengedi a második hivatkozást
if Status = HttpHandleClosing then
begin
ReleaseState(State);
Exit;
end;
Mit kell szolgáltatnia a libcurlnek FPC Unixon
Aszinkron feloldót és szálbiztos buildet, vagy az online letöltés kikapcsolva marad. FPC Unixon a transzport libcurlen megy át — ugyanazon a függőségen, amely a nem Windows célok libcurl időbélyeg-backendjének is a háta mögött áll —, és a kötés minden olyan könyvtárat elutasít, amelynek feature maszkjából hiányzik a CURL_VERSION_ASYNCHDNS vagy a CURL_VERSION_THREADSAFE. Az ok: a CURLOPT_NOSIGNAL, amelyet egy idegen folyamatban élő könyvtárnak állítania kell, szinkron feloldóval kombinálva azt jelenti, hogy egy DNS-keresés egyszerűen túlélheti az időkorlátot. A második csapda a leállítás: a curl_global_cleanup nem várja meg az aszinkron DNS-szálakat, így ha a libcurl egyszer inicializálódott, a modul a folyamat kilépéséig betöltve marad, ahelyett hogy egy háttérszál már kirakott kódba futna. Ha valamelyik követelmény elbukik, a SslCapabilities.OnlineRetrieval False, és a SslVerifyOptionsDiagnostics psvdOnlineRetrievalIgnored-ot jelent ahelyett, hogy úgy tett volna, mintha a hálózathoz ért volna
Mit garantál az eredmény, és mit nem
Egy érvényes RevocationStatus ettől a backendtől azt jelenti, hogy találtak, beállítottak vagy letöltöttek aktuális, a teljes láncot lefedő CRL-eket, és egyik sem sorolt fel benne tanúsítványt; és ennél többet nem. OCSP nincs, így egy csak OCSP-n publikáló CA az eredményt nem támogatottnak hagyja, és egy hálózati hiba pontosan úgy néz ki, mint egy semmit nem publikáló CA. Vegye figyelembe azt is, hogy a psvdNoCrlsConfigured csak az Ön által beállított CRL-eket írja le, tehát online letöltéssel jelzés, nem a bukás előrejelzése. Ha az audit nyomvonalának hálózat nélkül is reprodukálhatónak kell lennie, hagyja a NetworkPolicy-t a ptnpOffline alapértelmezésén: nem keletkezik letöltési szekció, a backend pedig sosem nyit kapcsolatot, ami egybevág a CryptoAPI oldal offline szerződésével, amelyet az offline PDF alágírás-visszavonás-ellenőrzésről Windowson szóló cikk ír le
A letöltési kód, a keretek és a transzportkötések forrással együtt szállulnak a PDFium Delphi komponenssel, így pontosan meggyőződhet arról, mely URL-eket érinthet egy érvényesítés és mennyit tölthet le, mielőtt bekapcsolja a ptnpOnline-ot egy megbízhatatlan dokumentumokat kezelő szerveren