Artykuł techniczny

DocMDP i FieldMDP: audyt rewizji PDF w Delphi

Podpisany PDF, który zmienił się po podpisaniu, nie jest automatycznie zepsuty. ISO 32000-1 dopuszcza aktualizacje przyrostowe ponad podpisem, a tylko niektóre z nich łamią politykę ustaloną przez podpisującego. HotPDF Component dla Delphi i C++Builder odpowiada na to pytanie za pomocą AnalyzeLoadedSignatureRevisions, który klasyfikuje każdą rewizję po podpisaniu i ocenia ją wobec DocMDP i FieldMDP. Scenariusz jest znany każdemu, kto dostarcza oprogramowanie do obsługi umów: Twój klient podpisuje umowę zakupu, wysyła ją i dostaje z powrotem z dołączoną stroną aneksu. Czytnik pokazuje żółty pasek mówiący, że podpis jest nienaruszony, ale dokument został zmieniony po podpisaniu, a nikt w pokoju nie potrafi powiedzieć, czy to normalny przepływ kontrasygnaty, czy ktoś po cichu edytuje podpisaną umowę

Co liczy się jako legalna zmiana po podpisaniu?

Zmiana jest legalna, gdy jej kategoria semantyczna mieści się w uprawnieniu zadeklarowanym przez podpis certyfikujący. ISO 32000-1 §12.8.2.2 definiuje transformację DocMDP z wartością /P równą 1, 2 lub 3: 1 nie zezwala na żadne zmiany, 2 zezwala na wypełnianie formularzy i podpisywanie, 3 zezwala na wypełnianie formularzy, podpisywanie i adnotacje. HotPDF udostępnia je jako wartości THPDFDocMDPPermission: dmpNoChanges, dmpFormFillAndSign i dmpFormFillSignAndAnnotate, z dmpNone zarezerwowanym dla wyników inspekcji niezawierających żadnej transformacji DocMDP

Kategorie są uporządkowane, a to uporządkowanie jest silnikiem całego sprawdzenia. THPDFRevisionModificationLevel przebiega rmlNone, rmlLongTermValidation, rmlFormFillAndSign, rmlAnnotations, rmlOther, celowo ułożone tak, by większa liczba porządkowa nigdy nie oznaczała mniejszej restrykcyjności. Cały dokument redukuje się do maksymalnego poziomu zaobserwowanego we wszystkich rewizjach po podpisie, a porównanie DocMDP staje się pojedynczym testem liczb całkowitych. Jedna niuansowa sprawa liczy się wcześnie: przy dmpNoChanges analiza wciąż akceptuje rmlLongTermValidation. Dodanie materiału walidacyjnego DSS i VRI albo znacznika czasu dokumentu do certyfikowanego pliku to utrzymanie podpisu, nie modyfikacja dokumentu, a potraktowanie tego jako naruszenia zepsułoby każdy istniejący przepływ archiwizacji długoterminowej

Jak HotPDF odbudowuje łańcuch rewizji?

Strukturalnie, nie heurystycznie. Zgodnie z ISO 32000-1 §7.5.6 aktualizacja przyrostowa dołącza nową sekcję cross-reference, której /Prev wskazuje na poprzednią, więc HotPDF czyta startxref od końca, parsuje sekcję tam wskazaną, podąża wstecz za /Prev i powtarza, zwracając sekcje od najstarszej. W tej pętli siedzą dwa limity bezpieczeństwa i oba warto znać przy diagnozowaniu pliku, który zawodzi: /Prev wskazujący na przesunięcie już odwiedzone kończy przechodzenie z jawną diagnostyką cyklu zamiast kręcić się w pętli, a łańcuch dłuższy niż tysiąc rewizji jest odrzucany od razu. Oba przypadki pojawiają się w Analysis.Issue, przy czym funkcja zwraca False, i żadnego z nich nie należy zamiatać pod dywan, ponieważ cykliczne /Prev to plik zniekształcony albo wrogi, a nie po prostu nietypowy

W realnych dokumentach pojawiają się cztery historyczne postaci i wszystkie cztery są obsługiwane: tradycyjne tablice xref parsowane linia po linii, strumienie cross-reference dekompresowane i dekodowane przez ich pola /W i /Index, pliki z hybrydowym cross-reference, których tradycyjny trailer niesie klucz /XRefStm, parsowany i scalany do tej samej rewizji (przypadek producentów Office, omówiony w artykule o hybrydowych strumieniach cross-reference), oraz obiekty żyjące wewnątrz kontenera ObjStm, które mają znaczenie, ponieważ nowoczesna aktualizacja zwykle umieszcza zmieniony słownik w skompresowanym strumieniu zamiast zapisywać go bezpośrednio, jak opisano w tekście o strumieniach obiektów i aktualizacjach przyrostowych. Podpis kotwiczy podział: /ByteRange[2] + /ByteRange[3] staje się SignedRevisionLength, a każda sekcja przy tym przesunięciu lub poza nim jest po podpisaniu. Czy zakres bajtów wciąż haszuje się poprawnie, to osobne pytanie, na które odpowiada VerifyLoadedSignature, omówione w artykule o weryfikacji podpisów cyfrowych PDF

Jak klasyfikowany jest każdy zmieniony obiekt

Klasyfikacja przebiega per obiekt, a potem propaguje się wzdłuż referencji. Dla każdego numeru obiektu, którego dotyka sekcja po podpisaniu, HotPDF czyta nowe ciało oraz ciało takie, jakim było w podpisanym zrzucie; identyczne ciało to rmlNone, ponieważ producenci faktycznie przepisują obiekty bez ich zmieniania. Rozpoznawacze są celowo wąskie. Obiekt /Type /DocTimeStamp, albo taki, którego /SubFilter to ETSI.RFC3161, to rmlLongTermValidation, tak jak wszystko osiągalne z drzewa /DSS katalogu; słownik /Type /Sig to rmlFormFillAndSign. Dla kontenerów testem jest to, które klucze się przesunęły, nie czym jest sam obiekt: katalog może jedynie zyskać lub zmienić /DSS, /Extensions lub /AcroForm; słownik AcroForm tylko /Fields, /SigFlags, /NeedAppearances, /DR, /DA lub /Q; strona tylko /Annots; pole lub widget tylko /V, /AP, /AS lub /M. Cokolwiek poza tymi zbiorami spada do rmlOther, i dokładnie tak zostaje złapana dołączona strona aneksu: dodanie strony przekłada drzewo stron w sposób, którego żadna biała lista nie obejmuje, a żadne legalne wypełnianie formularza tego nie przypomina

Potem poziomy propagują się, przy czym każdy kontener dziedziczy maksymalny poziom zmienionych dzieci, na które wskazuje, iterowane aż przypisanie się ustabilizuje. To właśnie sprawia, że strumienie wyglądu działają. Wypełnione pole tekstowe przepisuje /V i wskazuje na świeży strumień /AP, a ten strumień sam w sobie jest anonimowym blobem operatorów treści bez typu do rozpoznania; ponieważ pole, do którego należy, jest rmlFormFillAndSign, strumień dziedziczy ten sam poziom zamiast wpaść do rmlOther. Ta sama propagacja przenosi kontekst DSS na strumienie certyfikatów i unieważnień, które inaczej byłyby nieklasyfikowalne

Dlaczego nieczytelny obiekt liczy się jako naruszenie?

Ponieważ alternatywą jest walidator pokonany przez zapisanie czegoś, czego nie rozumie. Trzy sytuacje kończą się w HotPDF na rmlOther bez odwołania: obiekt, którego ciała nie dało się odczytać z rewizji, obiekt, który rewizja oznacza jako zwolniony, oraz obiekt niepasujący do żadnego z powyższych rozpoznawaczy. Każda zapisuje konkretną diagnostykę w polu Issue rewizji, więc operator widzi, który numer obiektu wygenerował werdykt

Zwalnianie jest najostrzejsze z tej trójki. Rewizja po podpisaniu, która oznacza wcześniej zdefiniowany obiekt jako wolny, usunęła treść z podpisanego dokumentu, i żaden poziom uprawnień w rozumieniu §12.8.2.2 na to nie zezwala; numery obiektów lądują w FreedObjectNumbers, a rewizja jest podnoszona do rmlOther. Nieczytelne obiekty podążają za tą samą logiką z innego powodu. Walidator, który nie potrafi sparsować obiektu, nie ma podstaw, by nazwać go nieszkodliwym, a uczciwą odpowiedzią na to nie jest milczenie. Zgłoszenie nietypowej, lecz łagodnej konstrukcji jako naruszenia kosztuje przegląd przez człowieka; przeciwny błąd wysyła podpisaną umowę z niezauważoną edycją w środku

Odczytywanie werdyktu w Delphi

Wywołanie jest krótkie. Wczytaj dokument, wybierz indeks podpisu, przeczytaj rekord; przeciążenie bez parametrów ponownie otwiera plik, z którego dokument został wczytany, a przeciążenie TStream przyjmuje bajty dostarczone przez wywołującego i przywraca pozycję strumienia przed zwróceniem wyniku. PolicyCompliant to pojedynczy boolean, którego chce większość wywołujących, łączący trzy niezależne decyzje: strukturalną poprawność słowników uprawnień, DocMDPCompliant i FieldMDPCompliant. Trzymaj te składowe widoczne w swoim UI zamiast je zwijać, i zauważ, że dokument bez transformacji DocMDP zostawia DocMDPCompliant na True, ponieważ zwykły podpis zatwierdzający nie deklaruje żadnej polityki do naruszenia, a zbiorczy ModificationLevel jest wtedy opisowy, a nie werdyktem

var
  Pdf: THotPDF;
  Analysis: THPDFSignatureRevisionAnalysis;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('contract-countersigned.pdf') > 0 then
    begin
      if Pdf.AnalyzeLoadedSignatureRevisions(0, Analysis) then
      begin
        if Analysis.PolicyCompliant then
          Writeln('Post-signature changes stay inside the signing policy')
        else
          Writeln('Policy violation: ', string(Analysis.Issue));
      end
      else
        Writeln('Analysis could not run: ', string(Analysis.Issue));
    end;
  finally
    Pdf.Free;
  end;
end;

Do triażu zwykle potrzebujesz rozbicia na poszczególne rewizje zamiast podsumowania, ponieważ mówi ono, w którym momencie historii dokumentu coś poszło nie tak. Każdy wpis w Analysis.Revisions niesie swój indeks w łańcuchu, przesunięcie cross-reference, przy którym został zapisany, swój własny poziom modyfikacji i numery zaangażowanych obiektów

const
  LevelNames: array[THPDFRevisionModificationLevel] of string =
    ('none', 'long-term validation', 'form fill and sign',
     'annotations', 'other');
var
  I: Integer;
begin
  Writeln(Format('%d revisions in chain, signature sits at index %d',
    [Analysis.TotalRevisionCount, Analysis.SignedRevisionIndex]));
  for I := 0 to High(Analysis.Revisions) do
    Writeln(Format('  rev %d at offset %d: %s (%d changed, %d freed) %s',
      [Analysis.Revisions[I].RevisionIndex,
       Analysis.Revisions[I].XRefOffset,
       LevelNames[Analysis.Revisions[I].ModificationLevel],
       Length(Analysis.Revisions[I].ChangedObjectNumbers),
       Length(Analysis.Revisions[I].FreedObjectNumbers),
       string(Analysis.Revisions[I].Issue)]));
end;

FieldMDP jest oceniane osobno i to celowe

Dokument może spełniać DocMDP, a i tak być nielegalny, dlatego FieldMDPCompliant jest odrębnym boolem, a nie wtopionym w porównanie poziomów. ISO 32000-1 §12.8.2.4 definiuje transformację FieldMDP, a §12.7.5.5 powiązany wpis /SigFieldLock, by zamrozić nazwane pola formularza w momencie podpisywania, nawet gdy dokument jako całość wciąż zezwala na wypełnianie formularzy. Wypełnienie pola to akcja poziomu 2; wypełnienie pola zablokowanego przez podpisującego to naruszenie niezależnie od poziomu. HotPDF czyta zakres do THPDFFieldLockAction jako flaAll, flaInclude lub flaExclude, z flaNone dla wyników bez polityki blokady, a nazwy do Permissions.FieldNames: flaAll blokuje wszystko, flaInclude blokuje wymienione nazwy, flaExclude blokuje wszystko poza nimi. Jeden szczegół ma znaczenie przy czytaniu wyników — w ChangedFieldNames raportowane są tylko pola już obecne w podpisanym zrzucie, ponieważ pole utworzone całkowicie po podpisaniu nie ma podpisanego stanu, któremu mogłoby zaprzeczać, i jest wychwytywane zamiast tego przez ścieżkę DocMDP

var
  Source: TFileStream;
  Analysis: THPDFSignatureRevisionAnalysis;
  I: Integer;
begin
  Source := TFileStream.Create('contract.pdf', fmOpenRead or fmShareDenyWrite);
  try
    if Pdf.AnalyzeLoadedSignatureRevisions(0, Source, Analysis) then
      if Analysis.Permissions.HasFieldMDP and (not Analysis.FieldMDPCompliant) then
        for I := 0 to High(Analysis.ChangedFieldNames) do
          Writeln('modified after locking: ',
            string(Analysis.ChangedFieldNames[I]));
  finally
    Source.Free;  // stream position was restored before the call returned
  end;
end;

Czego ta analiza Ci nie powie

Nie weryfikuje podpisu. AnalyzeLoadedSignatureRevisions rozumuje o strukturze i uprawnieniach; czy podpisany zakres bajtów wciąż haszuje się do wartości w blobie CMS oraz czy certyfikat podpisującego łączy się łańcuchem z czymkolwiek, czemu ufasz, odpowiadają na to VerifyLoadedSignature i VerifyLoadedSignatureWithTrust. Plik może być doskonale zgodny z polityką i kryptograficznie bezwartościowy, więc oba sprawdzenia powinny siedzieć obok siebie w każdej realnej bramce akceptacji. Nie czyta też intencji wewnątrz strumieni treści: strona, której strumień treści został wymieniony w całości, jest wychwytywana jako zmiana poza białą listą, ale analiza nie powie Ci, że podmiana zamieniła kwotę płatności. Werdykt rmlOther oznacza, że powinien spojrzeć człowiek, nie że doszło do oszustwa, a werdykt zgodny oznacza, że zmiana mieści się w dozwolonej kategorii, nie że zmiana była pożądana. Gdy potrzebujesz tylko tego, co zadeklarował podpisujący, bez przechodzenia po rewizjach, GetLoadedSignaturePermissions zwraca same słowniki polityki

Wszystko opisane tutaj działa natywnie w Delphi i C++Builder bez żadnej zewnętrznej usługi podpisującej w pętli, co czyni praktycznym uruchamianie tego na każdym przychodzącym dokumencie zamiast tylko na tych, co do których ktoś już nabrał podejrzeń. Pełne API podpisów i rewizji, wraz z metodami uprawnień i weryfikacji, z którymi współpracuje, jest częścią HotPDF Component dla Delphi i C++Builder