Artykuł techniczny

Listy zaufane eIDAS i kwalifikowane podpisy PDF

Stwierdzenie, że podpis PDF jest kwalifikowany według eIDAS, znaczy odpowiedzenie na pytanie, które nie ma nic do rzeczy z kryptografią: czy certyfikat został wystawiony przez usługę zaufania wpisaną przez państwo członkowskie jako kwalifikowana w chwili złożenia podpisu. Odpowiedź mieszka na liście zaufanej, dokumencie XML publikowanym per terytorium, i cała wartość tego dokumentu zależy od jego autentyczności. Komponent PDFium odmawia więc zajrzenia do środka, dopóki ktoś się za nią nie poręczy. TPdfEuropeanTrustedList.ParseAuthenticated wręcza kompletne surowe bajty dostarczonemu przez wywołującego IPdfTrustedListAuthenticator, zanim sparsuje jedną usługę, i tworzy migawkę tylko wtedy, gdy ten uwierzytelniający jawnie przepuści

Przepływ uwierzytelnij-przed-sparsowaniem dla europejskiej listy zaufanej w Delphi: surowy XML ze świeżego pobrania albo zbuforowanej migawki przechodzi przez IPdfTrustedListAuthenticator, zanim TPdfEuropeanTrustedList cokolwiek sparsuje
Świeże i zbuforowane listy spotykają ten sam uwierzytelniający, a migawka istnieje dopiero po jego przepuszczeniu

Ta kolejność jest projektem. Wszystko inne w tej funkcji wynika z niej, łącznie z częściami, które wyglądają na niewygodne

Sparsowane to nie zaufane

Lista zaufana parsująca się czysto mówi ci, że XML jest dobrze uformowany. Nie mówi nic o tym, kto ją napisał. Skoro lista jest tym, na czym opiera się cała decyzja o statusie kwalifikowanym, przyjęcie jej dlatego, że się sparsowała, uczyniłoby decyzję bezsensowną: atakujący potrafiący podmienić listę może ogłosić własny urząd certyfikacji kwalifikowanym

To samo rozumowanie dotyczy buforowania i to jest pułapka warta nazwania. Cache migawek przechowuje oryginalny XML razem z haszem SHA-256 i łatwo byłoby traktować zgodny hasz przy wczytywaniu jako dowód, że lista jest autentyczna. Nie jest. Hasz liczony przez ten sam proces, który zapisał plik, bez udziału klucza, dowodzi tylko, że bajty nie zmieniły się od ich zapisania; jeśli lista była fałszywa w chwili zbuforowania, hasz potwierdza, że to ta sama fałszywa lista. Wczytywanie zbuforowanej migawki przechodzi więc przez ten sam uwierzytelniający co parsowanie świeżej. Integralność i autentyczność to różne własności i tylko jedna z nich potrzebuje klucza

uses
  FPdfTrustedList;

type
  TListAuthenticator = class(TInterfacedObject, IPdfTrustedListAuthenticator)
  public
    function Authenticate(const XmlData: TBytes;
      out AuthenticationDetails: string): Boolean;
  end;

function TListAuthenticator.Authenticate(const XmlData: TBytes;
  out AuthenticationDetails: string): Boolean;
begin
  // Twoja polityka mieszka tutaj: zweryfikuj otoczoną sygnaturę XMLDSIG
  // względem przypiętego pozapasmowo certyfikatu podpisującego listę
  // i opisz, co zostało sprawdzone, dla ścieżki audytu
  Result := VerifyEnvelopedXmlSignature(XmlData, FPinnedListSigner);
  if Result then
    AuthenticationDetails := 'XMLDSIG verified against pinned LOTL signer';
end;

var
  List: TPdfEuropeanTrustedList;
  Cache: TFileStream;
begin
  List := TPdfEuropeanTrustedList.ParseAuthenticated(RawXml,
    TListAuthenticator.Create, TPdfTrustedListOptions.Default);
  // Migawka istnieje tylko dlatego, że uwierzytelniający powiedział tak
  Cache := TFileStream.Create('tl-de.snapshot', fmCreate);
  try
    List.SaveCache(Cache);
  finally
    Cache.Free;
  end;
end;

Walidator nie posiada polityki sieciowej

Walidatorowi PAdES nie wypada decydować, jak dotrzeć do listy list zaufanych, czy iść przez proxy, jak często ponawiać ani co robić, gdy terytorium jest nieosiągalne. To są decyzje aplikacji i wdrożenia, a w środowiskach regulowanych bywają często audytowane. Aktualizacje przychodzą więc przez IPdfTrustedListSource, któremu wręcza się URI i limit bajtów i który zwraca bajty

To, co komponent faktycznie egzekwuje, to niezmienniki czyniące z aktualizacji aktualizację, a nie podmianę. Update wymaga, by terytorium było niezmienione, by numer sekwencyjny ściśle rósł i by czas wydania nie cofał się. Te trzy kontrole łamią najbardziej oczywiste ataki obniżające: odtworzenie starszej listy, która wciąż wypisuje od tego czasu wycofaną usługę, albo wstawienie listy innego terytorium, której usługom nigdy nie zamierzano ufać

Niezmienniki aktualizacji egzekwowane przez komponent listy zaufanej PDFium: niezmienione terytorium, ściśle rosnący numer sekwencyjny i czas wydania nigdy cofający się, co razem blokuje ataki obniżające
Trzy monotoniczne kontrole rozdzielają prawdziwą aktualizację od odtworzonej albo podmienionej listy

Limity parsera i zero DTD

TPdfTrustedListOptions sufituje rozmiar XML, liczbę tokenów, głębokość zagnieżdżenia, liczbę usług, liczbę certyfikatów i rozmiar pojedynczego certyfikatu, a klasowa funkcja Default dostarcza używalnych wartości. Listy zaufane to publikowane dokumenty przewidywalnego rozmiaru, więc granice są tanie w ustawieniu i nie ma legalnej listy, która musiałaby je przekroczyć

Osobno i bezwarunkowo parser odrzuca deklaracje DTD i encji. To zamyka jedną odmową i rozszerzanie encji jako denial of service, i drogę ujawnienia przez encje zewnętrzne, i nic nie kosztuje, bo listy zaufane nie używają encji. Każdy parser XML osiągalny z niezaufanego wejścia powinien być tak skonfigurowany; różnica polega na tym, że ta odmowa nie jest konfigurowalna, więc nie da się jej wyłączyć dobrze intencjonowaną zmianą opcji

Status kwalifikowany jest zapisywany obok zaufania łańcucha, nie scalany z nim

Strona ewaluacji jest celowo osobna. TPadesTrustValidationOptions.QualifiedTrustEvaluator przyjmuje IPdfQualifiedTrustEvaluator, który migawka listy zaufanej implementuje. Podczas walidacji ewaluator dostaje certyfikat liścia, łańcuch i czas walidacji, dopasowuje certyfikaty usług dokładnym porównaniem DER względem podpisującego i łańcucha, łączy status usługi, identyfikator typu usługi i URI kwalifikatorów w tym punkcie czasu i zwraca rekord ewaluacji

Wynik ląduje w dwóch miejscach na każdym podpisie: QualifiedTrustStatus jako zgrubny status i QualifiedTrust jako pełna ewaluacja z terytorium, nazwą dostawcy, nazwą usługi, identyfikatorem typu, statusem i czasem rozpoczęcia statusu. Tego, czego nie robi, to zmiana CertificateTrustStatus. Zaufanie łańcucha systemu i status kwalifikowany odpowiadają na różne pytania, a raport, który je scali, nie odróżni „zaufany, ale niekwalifikowany” od „kwalifikowany, ale łańcuch się nie waliduje”, a oba są realne i oba wymagają innego traktowania

Ewaluacja kwalifikowanego zaufania PAdES w Delphi: IPdfQualifiedTrustEvaluator dopasowuje certyfikaty usług dokładnym porównaniem DER i wypełnia QualifiedTrustStatus i QualifiedTrust, podczas gdy CertificateTrustStatus zostaje nietknięty
Zaufanie łańcucha i status kwalifikowany eIDAS są zapisywane obok siebie, więc oba znaleziska pozostają widoczne
var
  Options: TPadesTrustValidationOptions;
  Report: TPadesValidationResult;
  I: Integer;
begin
  Options := TPadesTrustValidationOptions.Default;
  Options.CheckRevocation := True;
  Options.QualifiedTrustEvaluator := List;      // uwierzytelniona migawka
  Options.QualifiedValidationTime := SigningTime; // nie Now
  Report := Pdf.ValidatePadesTrust(Options);

  for I := 0 to High(Report.Signatures) do
    if Report.Signatures[I].QualifiedTrustStatus = pcsValid then
      Writeln(Format('signature %d qualified by %s / %s (%s)',
        [I, Report.Signatures[I].QualifiedTrust.Territory,
         Report.Signatures[I].QualifiedTrust.ProviderName,
         Report.Signatures[I].QualifiedTrust.ServiceName]))
    else if Report.Signatures[I].QualifiedTrustStatus = pcsIndeterminate then
      // Brak pasującej usługi albo migawka nie odpowiada za ten czas
      Writeln(Format('signature %d: qualified status undetermined', [I]));
end;

Czemu czas walidacji to nie teraz

Bo kwalifikacja jest własnością momentu. Usłudze zaufania można przyznać status kwalifikowany, później go wycofać, a jeszcze później przywrócić, i każde z tych przejść niesie czas rozpoczęcia na liście. Podpis złożony, gdy usługa była kwalifikowana, pozostaje kwalifikowany potem; podpis złożony przed przyznaniem nie staje się kwalifikowany wstecz. Ewaluowanie względem bieżącego czasu daje więc złą odpowiedź w obu kierunkach

Lista niesie to, czego to potrzebuje: każdy rekord usługi ma czas rozpoczęcia statusu i flagę odróżniającą wpisy historyczne od bieżących, a ewaluator łączy je względem czasu, który dostarczysz. W praktyce ten czas pochodzi z zaufanego znacznika czasu na podpisie, a nie z czasu podpisania deklarowanego w CMS, dlatego materiał walidacji długoterminowej ma znaczenie nawet dla pytania wyglądającego na odpytanie polityki; stronę znacznika czasu i DSS omawia artykuł o podpisach długoterminowych

Co wciąż musisz zbudować

Trzy rzeczy i żadna z nich nie należy do biblioteki PDF. Uwierzytelniający, czyli faktyczna weryfikacja XMLDSIG względem certyfikatu podpisującego listę, który pozyskano kanałem godnym zaufania. Polityka pobierania, czyli jak i jak często odświeżasz oraz co robi twoja aplikacja, gdy odświeżenie zawiedzie. I zakres terytorialny, czyli które listy w ogóle nosisz, co jest decyzją biznesową o tym, w których państwach członkowskich podpisują twoi kontrahenci

To, co dostajesz od komponentu, to część, którą łatwo subtelnie zepsuć: kolejność uwierzytelnij-przed-sparsowaniem, ograniczone i wolne od encji parsowanie XML, monotoniczne niezmienniki aktualizacji, dopasowanie usług dokładnym DER, ewaluacja statusu historycznego i wynik pozostający osobny od zwykłego zaufania łańcucha. Jeśli twój bezpośredni problem jest bardziej podstawowy, że walidator odrzuca podpis, który uważasz za poprawny, typowe przyczyny są skatalogowane w czemu walidatory odrzucają podpisy PAdES, a powierzchnia inspekcji podpisów jest opisana w inspekcji podpisów i poziomów PAdES. Możliwości komponentu są wypisane na stronie produktu PDFium Delphi component