Tehnički članak

OpenSSL AIA i CRL dohvatanje za PDF potpise u Delphi-ju

PDFium VCL sada može na svom OpenSSL backendu da zaokruži lanac PDF potpisa i da proveri revokaciju preko mreže: kada je OnlineRetrieval uključen, ConfigureSslCmsVerifier ugrađuje verifikator koji dovlači nedostajuće intermediate sertifikate sa AIA caIssuers URL-ova i CRL-ove sa CRL distribution points, unutar fiksnog budžeta vremena, zahteva i bajtova po pozivu verifikacije. Dovučeni sertifikati su isključivo materijal za lanac. Poverenje i dalje dolazi jedino iz sistemskog spremišta i sidara koje vi podesite

Jaz koji se ovim zatvara pokazuje se prvog trenutka kad realne PDF-ove validirate na Linux serveru. Veliki deo potpisnika u CMS ugrađuje samo svoj leaf sertifikat, pa OpenSSL ne doseže do root-a, TrustStatus se vraća kao nevalidan, i revokacija se nikada ne pokrene jer lanac nikada nije postao pouzdan. Pre v3.121.0 OpenSSL backend opisan u tekstu o verifikaciji PDF potpisa OpenSSL-om u PDFium VCL bio je strogo oflajn i OnlineRetrieval na njega nije imao uticaja. Jedno vredi reći na početku: PDFium engine sam uopšte ne radi CMS verifikaciju, pa svako pravilo ispod živi u PAdES sloju komponente i njenom OpenSSL povezivanju, gde ga možete i pročitati

Kojim redosledom OpenSSL backend verifikuje, dovlači i proverava?

Prvo integritet, pa poverenje, pa revokacija, i mreža se dodiruje samo između koraka kojima treba. VerifyCmsWithSsl proverava CMS potpis i potpisane atribute (RFC 5652) uz pritisnutu proveru lanca, i ako to padne vraća se odmah, pre nego što fetch sesija uopšte postoji, pa dokument sa pokvarenim bajtovima ne okida nijedan odlazni zahtev. Tek ako lanac potom padne a OnlineRetrieval je uključen, prati AIA veze i verifikuje ponovo. CRL distribution points dovlače se tek kad je lanac pouzdan, jer CRL koja visi sa nepouzdane putanje ništa ne dokazuje. Tri presude ostaju odvojene kroz sve: validan potpis sa nepotpunim lancem i dalje se prijavljuje kao validan potpis

Redosled VerifyCmsWithSsl u OpenSSL backendu PDFium Component: CMS provera potpisa ide uz pritisnutu proveru lanca, pa pokvareni bajtovi nikada ne diraju mrežu; RetrieveIntermediates prati AIA caIssuers URL-ove tek posle pada lanca sa uključenim OnlineRetrieval, a RetrieveCrls dovlači distribution point CRL-ove u posebno spremište tek kad je lanac pouzdan
Integritet, pa poverenje, pa revokacija: mreža se dodiruje samo između koraka kojima treba, a CRL koja visi sa nepouzdane putanje ništa ne dokazuje
uses
  PDFium, FPdfCrypto, FPdfCryptoSsl, FPdfPades;

var
  Pdf: TPdf;
  Probe: TPdfCmsVerifyOptions;
  Diags: TPdfSslVerifyDiagnostics;
  Trust: TPadesTrustValidationOptions;
  Verdict: TPadesValidationResult;
  I: Integer;
begin
  ConfigureSslTrustAnchors(LoadCorporateRoots);   // DER; jedino dodatno poverenje
  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;  // ptnpOffline po podrazumevanju
  Trust.NetworkPolicy := ptnpOnline;
  Trust.CheckRevocation := True;
  Trust.UrlRetrievalTimeoutMs := 10000;           // po pozivu verifikacije

  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;

Zašto se dovučeni sertifikati nikada ne dodaju u trust spremište?

Jer URL-ovi dolaze iz sertifikata koji se validira, a birao ih je potpisnik. authorityInfoAccess caIssuers unos (RFC 5280 §4.2.2.1) je naznaka gde stanuje izdavalac, i ništa više. Da bi u trust-anchor spremište otišlo bilo šta što odgovori na taj URL, svako bi mogao da potpiše svojim samodiranim ključem, uperi AIA u svoj server i dobije zeleni verdikt. Zato RetrieveIntermediates svaki parsiran sertifikat predaje CMS_add1_cert-u, koji ga smešta u skup nepouzdanih ove jedne CMS strukture, i OpenSSL i dalje mora od njega da izgradi putanju do sidra koje ste podesili ili koje sistemsko spremište već drži. Ima i tiši razlog: certificate argument CMS_verify-a nije zamena od štaka za sertifikate ugrađene u CMS, pa je dodavanje u sam CMS pouzdan put

Dovlačenje je namerno uzano. RetrieveIntermediates ide najviše 4 kruga, pri čemu svaki skuplja caIssuers URL-ove sa svakog sertifikata koji je tada u CMS-u, i staje čim krug ništa ne doda ili se potroši vremenski budžet. Odgovor se mora dekodovati sa d2i_X509 kao jedan DER sertifikat koji troši ceo sadržaj; bajtovi na kraju su odbijeni, i PKCS#7 certs-only paket poslužen sa .p7c URL-a preskače se umesto da se raspakuje. OCSP pristupni metod u istom AIA proširenju ignoriše se, jer ovaj backend ne govori OCSP. Na strani revokacije, RetrieveCrls čita samo fullName URI-jeve svakog DistributionPoint-a (RFC 5280 §4.2.1.13) iz CMS sertifikata i podešenih sidara, a dovučene CRL-ove sliva u drugo, nezavisno X509_STORE spremište sa CRL proverom celog lanca, pa nedostajuća ili zastarela CRL menja RevocationStatus a TrustStatus nikada ne dodiruje

// Skraćeno iz VerifyCmsWithSsl (FPdfCryptoSsl.pas); BIO priprema izostavljena.
// Svaki _CMS_verify poziv dobija svež content BIO
if _CMS_verify(Cms, nil, nil, Bio, nil,
  CMS_NO_SIGNER_CERT_VERIFY or CMS_BINARY) <> 1 then
  Exit;                                   // pokvaren potpis: bez mreže u potpunosti
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, samo nepouzdano
  // verifikuj lanac ponovo nad istim spremištem sidara
end;

if Options.CheckRevocation and (FetchSession <> nil) and
   (Result.TrustStatus = pcvsValid) then
  RetrievedCrls := RetrieveCrls(Cms, FetchSession);
// drugo, odvojeno spremište: podešene CRL-ove plus dovučene
Store := BuildStore(True, RetrievedCrls, RevocationChecked, CrlsMalformed);

Šta jedan poziv verifikacije košta najviše?

Fiksni plafon, koji sprovodi jedan TPdfCryptoFetchSession koji AIA i CRL koraci jednog poziva verifikacije dele. Granice su konstante u FPdfCryptoHttp-u, a ne sugestije:

  • Vreme: UrlRetrievalTimeoutMs, koje se u TPdfCmsVerifyOptions.Default i TPadesTrustValidationOptions.Default podrazumeva na 15000; sesija napravljena sa 0 vraća se na 30000, a sat kreće čim potpis prođe, pokrivajući svaki kasniji zahtev
  • Zahtevi: najviše 8 po sesiji, brojani pre nego što se transport okuša, pa mrtav host i dalje potroši mesto
  • Bajtovi: 1 MiB po odgovoru i 4 MiB ukupno, uz odbijanje URL-ova dužih od 2048 znakova pre bilo koje konekcije
Čvrsti plafoni jednog TPdfCryptoFetchSession koji dele AIA i CRL koraci poziva verifikacije u PDFium Component: UrlRetrievalTimeoutMs se podrazumeva na 15000 ms sa povratkom na 30000 kod nule, najviše 8 zahteva po sesiji, 1 MiB po odgovoru i 4 MiB ukupno uz neusphele odgovore koji se i dalje računaju, i odbijanje URL-ova preko 2048 znakova
Granice su konstante, ne sugestije: bajtovi iz neuspehog odgovora i dalje troše budžet, i pošto se svaki potpis i svaki timestamp verifikuje odvojeno najgori slučaj raste sa brojem potpisa

Knjigovodstvo je strože nego što na prvi pogled deluje. Bajtovi primljeni iz neuspehog odgovora i dalje ulaze u ukupan zbir, pa server koji odgovori 404 velikom stranicom ne može besplatno da isprazni budžet. Čitanje koje pređe limit po odgovoru prekida preuzimanje umesto da ASN.1 parseru prosledi odsečeno telo, i HTTP 200 sa praznim telom odbija se odmah, jer bi AIA putanja u suprotnom indeksirala Data[0] praznog niza. Samo obični http:// i https:// URL-ovi prolaze, bez preusmerenja, kolačića, akreditiva i automatske pretrage proksija, dok HTTPS zadržava svoje uobičajene provere sertifikata i imena hosta. URL deduplikacija je namerno ograničena na jedan poziv: sledeća validacija mora moći da vidi sveže objavljenu CRL. Budžet je i po pozivu, ne po dokumentu, a ValidatePadesTrust verifikuje svaki potpis i svaki timestamp token odvojeno, pa najgori slučaj raste sa brojem potpisa

Zašto WinHTTP zahtev kome je isteklo vreme i dalje može da piše u vašu memoriju?

Jer povratak po isteku vremena ne otkazuje povratne pozive koji su već u letu. Windows transport pogoni WinHTTP asinhrono i čeka na događaj sa preostalim vremenom sesije, i kada to čekanje odustane, zahtev i dalje može dovršiti čitanje i javiti se kasnije. Uperite asinhrono čitanje u bafer na steku i taj kasni završetak upisaće u okvir koji tada pripada nekoj sasvim drugoj funkciji. Popravka je vlasništvo, ne tajming: događaj i bafer za čitanje od 16 KB žive u heap zapisu sa dve reference, jednu drži pozivalac a drugu otpušta samo finalni HANDLE_CLOSING povratni poziv, pa koja strana poslednja završi, ta oslobađa memoriju

Zašto WinHTTP zahtev kome je isteklo vreme i dalje može pisati u memoriju: povratak po isteku ostavlja povratne pozive u letu, pa PDFium Component asinhrono čitanje uperi u heap alociran THttpState zapis čiji se 16 KB bafer i dve reference, jedna kod pozivaoca i jedna otpuštena od finalnog HANDLE_CLOSING povratnog poziva, oslobađaju tek kad poslednja strana završi
Kasni završetak može dovršiti svoje čitanje posle toga što je vaše čekanje odustalo; heap vlasništvo sa dve reference znači da taj upis sleti u memoriju koja je još živa
type
  PHttpState = ^THttpState;
  THttpState = record
    References: LongInt;               // pozivalac + finalni HANDLE_CLOSING povratni poziv
    Event: THandle;
    Status, Count: DWORD;
    Buffer: array[0..16383] of Byte;   // asinhrona čitanja sleću ovde, nikada na stek
  end;

procedure ReleaseState(State: PHttpState);
begin
  if InterlockedDecrement(State.References) = 0 then
  begin
    CloseHandle(State.Event);
    Dispose(State);
  end;
end;

// U statusnom povratnom pozivu: HANDLE_CLOSING je poslednja najava koju WinHTTP
// šalje za zahtev, pa ona spušta drugu referencu
if Status = HttpHandleClosing then
begin
  ReleaseState(State);
  Exit;
end;

Šta libcurl mora da obezbedi na FPC Unix-u

Asinhroni rezolver i build bezbedan za niti, ili onlajn dovlačenje ostaje isključeno. Na FPC Unix-u transport ide preko libcurl-a, iste zavisnosti koja stoji iza libcurl timestamp backend-a za ne-Windows ciljeve, i povezivanje odbija svaku biblioteku čija feature maska nema ili CURL_VERSION_ASYNCHDNS ili CURL_VERSION_THREADSAFE. Razlog je što CURLOPT_NOSIGNAL, koje biblioteka u tuđem procesu mora postaviti, u paru sa sinhronim rezolverom znači da DNS upit može slobodno da preživi istek vremena. Druga zamka je gašenje: curl_global_cleanup ne čeka asinhrona DNS niti, pa jednom inicijalizovan libcurl ostaje mapiran dok proces ne izađe, umesto da pozadinska nit otrči u istovareni kod. Kad bilo koji od ta dva uslova padne, SslCapabilities.OnlineRetrieval je False a SslVerifyOptionsDiagnostics prijavljuje psvdOnlineRetrievalIgnored umesto da se pravi da je mreža bila pitana

Šta rezultat garantuje, a šta ne

Validan RevocationStatus sa ovog backenda znači da su pronađene, podešene ili dovučene tekuće CRL-ove koje pokrivaju ceo lanac i da nijedna nije upisala sertifikat iz njega; i ništa više. OCSP nema, pa CA koja revokaciju objavljuje samo kroz OCSP ostavlja rezultat nepodržanim, i mrežni kvar izgleda potpuno isto kao CA koja ništa ne objavljuje. Primetite i da psvdNoCrlsConfigured opisuje samo CRL-ove koje ste vi podesili, pa je uz onlajn dovlačenje to naznaka, a ne prognoza pada. Kad audit trag mora biti reprodukovan bez pristupa mreži, ostavite NetworkPolicy na podrazumevanom ptnpOffline-u: fetch sesija se ne pravi i backend nikada ne otvara konekciju, što se poklapa sa oflajn ugovorom na CryptoAPI strani opisanim u tekstu o oflajn proverama revokacije PDF potpisa na Windows-u

Dovlačenje, budžeti i transport povezivanja stižu kao source uz PDFium Delphi komponentu, pa možete tačno da potvrdite koje URL-ove validacija sme da kontaktira i koliko sme da preuzme pre nego što uključite ptnpOnline na serveru koji rukuje nepouzdanim dokumentima