HotPDF waliduje PDF MAC z ISO/TS 32004 per rewizja, a nie per plik. THotPDF.ValidatePDFMACChain przechodzi każdą przyrostową aktualizację od kotwicy łańcucha w przód i weryfikuje każdy MAC względem strumienia prefiksu tylko do odczytu, kończącego się na własnym startxref i %%EOF danej rewizji. Jeden poprawny MAC na najnowszej rewizji nie mówi nic o rewizjach pod nią
Oto scenariusz, który motywuje wszystko to. Wysyłasz zaszyfrowany AES-256 PDF z PDF MAC na pokładzie. Ktoś otwiera plik w edytorze hex, podmienia bajt wewnątrz pierwszej chronionej MAC rewizji, a potem dokłada zupełnie nową rewizję niosącą w pełni poprawny, własny MAC. Każda przeglądarka otwiera plik bez zastrzeżeń, a naiwny kontroler haszujący bieżący zakres bajtów względem MAC w aktywnym trailerze zgłasza sukces — bo ten MAC naprawdę jest poprawny dla bajtów, które obejmuje. Szkoda siedzi dwie rewizje niżej, w regionie, którego nikt ponownie nie sprawdził
Dlaczego poprawny MAC najwyższego poziomu nie dowodzi, że plik jest nienaruszony?
Bo PDF MAC obejmuje prefiks, nie dokument. Aktualizacja przyrostowa jest pełnoprawną częścią formatu: każdy zapis dokłada nowe ciało, nową sekcję odwołań krzyżowych i nowy trailer, podczas gdy starsze bajty zostają dokładnie tam, gdzie były. ISO/TS 32004 jeździ na tym modelu, więc każda rewizja niesie własny słownik /AuthCode uwierzytelniający plik w stanie z tej chwili, a weryfikowanie wyłącznie najnowszej zostawia każdą wcześniejszą rewizję bez oględzin. HotPDF wystawia więc oba pytania jako dwa wywołania, a różnica między nimi to cały sens tego artykułu. ValidatePDFMAC odpowiada na pytanie, czy bieżąca rewizja jest autentyczna, wypełniając rekord THPDFPDFMACValidationInfo; ValidatePDFMACChain odpowiada, czy każda chroniona MAC rewizja w tym pliku jest autentyczna, wypełniając THPDFPDFMACChainValidationInfo tablicą per rewizja plus maszynowo czytelnym powodem awarii. Na pliku z naruszeniem i ponownym MAC-em z góry pierwsze wywołanie zwraca True, a drugie False na indeksie rewizji 1
var
Pdf: THotPDF;
Chain: THPDFPDFMACChainValidationInfo;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if not Pdf.ValidatePDFMACChain('incoming.pdf', 'user', Chain) then
begin
// Awaria to jedna z pmcfRevisionBoundary, pmcfNoPDFMAC,
// pmcfRequiredRevisionMissing, pmcfRevisionInvalid,
// pmcfKDFSaltChanged, pmcfDigestDowngrade, pmcfPermissionDowngrade
Writeln('chain rejected: ', Chain.Message);
Writeln('revision ', Chain.FailureRevisionIndex,
' at xref offset ', Chain.FailureXRefOffset);
Exit;
end;
for I := 0 to High(Chain.Revisions) do
Writeln(I, ' len=', Chain.Revisions[I].RevisionLength,
' mac=', Chain.Revisions[I].HasPDFMAC,
' perms=', Chain.Revisions[I].PermissionsAuthenticated);
finally
Pdf.Free;
end;
end;
Każdy MAC weryfikuje się na własnym strumieniu prefiksu, nigdy na końcowej długości pliku
Najdroższy błąd w tym obszarze to użycie końcowego rozmiaru pliku jako górnej granicy przy ponownym haszowaniu starszej rewizji, co wchłania końcowe bajty do każdego skrótu poza najnowszym i zgłasza naruszenie na zdrowym pliku. HotPDF zamiast tego odtwarza, dla każdej rewizji, ograniczony strumień tylko do odczytu kończący się na własnej wartości startxref tej rewizji, po której następuje jej %%EOF, i haszuje tylko to. Zlokalizowanie granicy jest bardziej wybredne, niż wygląda: literał %%EOF może pojawić się wewnątrz strumienia treści albo napisu, więc kandydat jest przyjmowany tylko wtedy, gdy bezpośrednio poprzedzający startxref parsuje się do liczby równej offsetowi odwołań krzyżowych walidowanej sekcji, bez niczego poza białymi znakami między nimi. Rewizja pochłania potem dokładnie jedną sekwencję końca wiersza po znaczniku — pojedyncze CR, pojedyncze LF albo jedną parę CRLF — i nic więcej. Ta ostatnia reguła kąsi w praktyce, bo zapisujący, który wyemituje dodatkową pustą linię między dwiema rewizjami, wyprodukował bajty należące do następnej rewizji, a pochłonięcie całego końcowego białego znaku do poprzedniej po cichu zmienia oba skróty. Enumeracja sekcji trzyma tę samą dyscyplinę: HotPDF przechodzi sekcje odwołań krzyżowych od najstarszej do najnowszej dokładnie raz, odtwarzając wpisy wolne, bezpośrednie i strumieni obiektów, tak by późniejsze sekcje nadpisywały wcześniejszy stan, co jest przeciwieństwem semantyki pierwszy-widziany-wygrywa stosowanej przez parser aktywnego xref
Gdzie kotwiczy się łańcuch i co go łamie?
Pierwsza rewizja niosąca poprawny /AuthCode jest kotwicą, a FirstMACRevisionIndex zgłasza, gdzie zaczyna się ochrona; wszystko przed nią jest z konstrukcji niechronione, co jest normalne. Wszystko po niej musi być chronione MAC, więc doklejenie jednego zwykłego przyrostu do pliku chronionego MAC zawodzi z pmcfRequiredRevisionMissing i indeksem winnej rewizji — tolerowanie luki pozwoliłoby atakującemu zerwać ochronę po prostu zapisując jeszcze raz. Trzy dalsze niezmienności obowiązują wzdłuż łańcucha, każda z własnym kodem awarii
pmcfKDFSaltChanged—/KDFSaltmusi pozostać stabilny od kotwicy w przód, bo rotująca sól pozwoliłaby fałszerzowi wyprowadzić klucze ponownie pod parametrami własnego wyborupmcfDigestDowngrade— siła skrótu jest porównywana z ostatnim zweryfikowanym MAC-em, a nie z bezpośrednio poprzedzającą rewizją, więc łańcuch zaczynający się pod profilem Modern na SHA-384 nie może po cichu ciągnąć się na SHA-256pmcfPermissionDowngrade— rewizja nie może skasować wymogu PDF MAC, który wcześniejsza rewizja uwierzytelniła
Konsekwencją wartą przyswojenia jest to, że historyczne MAC-i są weryfikowane niezależnie, nawet gdy przestają być aktywnym trailerem. Dlatego atak edytuj-starą-rewizję-i-doklej-świeży-MAC z wstępu nie przetrwa: najnowszy MAC zgadza się sam w sobie, ValidatePDFMAC jest zadowolony, a łańcuch i tak ląduje na rewizji 1 z pmcfRevisionInvalid
Kolejność podpisu: klucze trailera najpierw, signatureDigest na końcu
Gdy MAC jest przypięty do podpisu CMS, a nie stoi sam, kolejność zapisu przestaje być pytaniem stylistycznym. HotPDF wymaga, by /AuthCode, /KDFSalt, rozszerzenie deweloperskie ISO 32004 i /SigObjRef zostały zapisane do tej samej rewizji przed policzeniem /ByteRange podpisu; doklejenie czegokolwiek z nich potem ląduje tymi bajtami poza zakresem obejmowanym przez podpis, produkując plik, którego podpis się weryfikuje, podczas gdy powiązanie MAC jest niepodpisane. Oba skróty biegną potem w przeciwnych kierunkach, co na pierwszy rzut oka wygląda na cyrkularność i nią nie jest. signatureDigest PDF MAC wiąże surowe oktety treści CMS SignerInfo.signature OCTET STRING — nie cały DER CMS i nie podpisane atrybuty — więc jest konstruowany po tym, jak istnieje już surowa wartość podpisu, i wstrzykiwany jako niepodpisany atrybut id-attr-pdfMacData. Ponieważ /Contents jest wykluczony z ByteRange podpisu, a niepodpisane atrybuty nigdy nie zasilają obliczania podpisu, sekwencja wytwórz-podpis, zbuduj-MAC, opakuj-CMS domyka się czysto bez kryptograficznej pętli. Płyną dwa wnioski: wartownik /ByteRange i placeholder /Contents muszą pozostać w czystym tekście i poza strumieniami obiektów nawet w zaszyfrowanym pliku, bo łatacz o stałej szerokości nie znajdzie ich inaczej; a gdy skrót MAC to również SHA-256, skrót podpisujący jest wykorzystywany wprost ponownie, w przeciwnym razie oba konteksty skrótu są aktualizowane w jednym przebiegu nad strumieniem wyjściowym
var
Pdf: THotPDF;
Options: THPDFPDFMACOptions;
Info: THPDFPDFMACValidationInfo;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.AutoLaunch := False;
Pdf.FileName := 'unsigned.pdf';
Pdf.ActivateProtection := True;
Pdf.CryptKeyLength := aesgcm;
Pdf.OwnerPassword := 'owner';
Pdf.UserPassword := 'user';
Pdf.ProtectOptions := [prPrint, prExtractContent];
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 12);
Pdf.CurrentPage.AddSignedSignatureField('Approval',
Rect(72, 120, 280, 160), 16384);
Pdf.EndDoc;
Options := THPDFPDFMACOptions.Modern; // skrót dokumentu SHA-384
if THotPDF.SignPDFWithPFXAndAttachedPDFMAC('unsigned.pdf',
'signed.pdf', 'signer.pfx', 'pfx-secret', 'user', Options) then
if Pdf.ValidatePDFMAC('signed.pdf', 'user', Options, Info) then
begin
// Location = pmlAttachedToSignature, a oba skróty
// są zgłaszane osobno
Writeln('signature object : ', Info.SignatureObjectNumber);
Writeln('signature digest : ', Info.SignatureDigestMatched);
Writeln('full file coverage: ', Info.FullFileCoverage);
Writeln('perms authentic : ', Info.PermissionsAuthenticated);
end;
finally
Pdf.Free;
end;
end;
Walidacja wraca tą samą ścieżką z drugiego końca: czyta bezpośredni /AuthCode z aktualnie aktywnego klasycznego trailera odwołań krzyżowych, podąża za generacyjnie świadomym pośrednim /SigObjRef, potwierdza, że wiąże /V jedynego pola podpisu, i zgłasza awarię skrótu dokumentu osobno od awarii skrótu podpisu. To różne diagnozy, a zgniecenie ich w jedną wartość boolean wyrzuca jedyną informację mówiącą, czy dotknięto treści strony, czy wartości podpisu. Jeśli już robisz rzeczy z CMS, to siedzi obok artykułu o podpisywaniu PAdES i poradnika weryfikacji podpisów w wczytanych dokumentach
Nigdy nie ufaj /P: najpierw odszyfruj 16-bajtowe /Perms
ISO/TS 32004 sygnalizuje, że ten dokument wymaga PDF MAC, przez bit uprawnień 13, a oczywisty sposób odczytania go jest zły, bo liczba całkowita /P w słowniku szyfrowania jest w czystym tekście i nieuwerzytelniona — ktokolwiek może podmienić ten bit w edytorze tekstu i zdjąć wymóg o poziom. ISO 32000-2 §7.6 dostarcza odpowiedzi we wpisie /Perms, a HotPDF z niej korzysta: odszyfruj 16-bajtowy napis /Perms kluczem szyfrowania pliku pod AES-256 CBC, zerowym IV, bez dopełnienia, potem sprawdź każde pole tekstu jawnego, zanim uwierzysz w cokolwiek. Bajty od 1 do 4 trzymają wartość uprawnień w kolejności little-endian i muszą być dokładnie równe liczbie /P; bajty od 5 do 8 to 0xFF; bajt 9 to flaga szyfrowania metadanych T albo F; bajty od 10 do 12 to literał znacznik adb. Dopiero gdy wszystko to się trzyma, PermissionsAuthenticated staje się True i bit 13 zostaje odczytany — i uważaj na jego polarność, bo wymóg MAC jest asertowany, gdy bit 0x1000 jest wyczyszczony. Niezgodność między /P a odszyfrowanymi uprawnieniami nie jest ostrzeżeniem do zalogowania i przejścia dalej; to podrobiony zbiór uprawnień, a właściwą odpowiedzią jest zawieść bezpiecznie
Zwinność algorytmów zatrzymuje się na skrócie
ISO/TS 32004 pozwala wybrać skrót dokumentu i tylko skrót dokumentu. HotPDF trzyma HMAC-SHA-256 do uwierzytelniania, HKDF-SHA-256 według RFC 5869 do wyprowadzania kluczy i AES-256 key wrap według RFC 3394 na stałe pod zmiennym THPDFPDFMACDigestAlgorithm rozpiętym od pmdaSHA256 do pmdaSHA3_512, bo naturalny błąd to potraktowanie "profilu SHA3-512" jako licencji na wymianę również HMAC, co daje plik, który nie jest już PDF MAC w żadnym międzyoperacyjnym sensie. Jeden detal implementacyjny jest wart skopiowania, jeśli piszesz własny weryfikator: odczytaj OID skrótu z CMS AuthenticatedData przed haszowaniem zakresu bajtów, bo zaszyty na sztywno SHA-256 i uzgadnianie później zamienia zwinność w etykietę i pozwala wrogiemu plikowi zmusić cię do przepuszczenia całego dokumentu przez strumień, zanim odkryjesz, że algorytm nigdy nie był wspierany. CMSAlgorithmProtection, algorytm skrótu AuthenticatedData, integrity-info messageDigest i skrót zakresu bajtów muszą wszystkie wskazywać jeden algorytm, a każda niezgoda zawodzi bezpiecznie
var
Options: THPDFPDFMACOptions;
begin
Options := THPDFPDFMACOptions.Compatibility; // SHA-256, przyjmuje wszystkie sześć
Options := THPDFPDFMACOptions.Modern; // SHA-384, odrzuca 256-bit
Options := THPDFPDFMACOptions.HighAssurance; // wyłącznie SHA3-512, AES-GCM
// Własny profil jest legalny, ale algorytm, którym generuje,
// też musi widnieć na liście dozwolonych walidacji, bo konfiguracja
// jest odrzucana, zanim choć jeden bajt zostanie zapisany
Options.Profile := pmppCustom;
Options.DigestAlgorithm := pmdaSHA512;
Options.AllowedDigestAlgorithms := [pmdaSHA512, pmdaSHA3_512];
Options.RequireAESGCM := True;
end;
Co PDF MAC dowodzi, a czego nie
Zweryfikowany łańcuch PDF MAC dowodzi, że każda chroniona rewizja jest bajt w bajt identyczna z tym, co zapisał ktoś trzymający klucz szyfrowania pliku, że żadna chroniona rewizja nie została usunięta ani przestawiona i że żadna niechroniona rewizja nie została doklejona za kotwicą — dokładnie ta klasa ataków, którą zostawia otwarte zwykłe szyfrowanie AES-256, bo poufność nie mówi nic o integralności, a zaszyfrowany PDF ze wklejoną rewizją deszyfruje się równie chętnie jak nienaruszony. Czego nie dowodzi, to autorstwa. Klucz MAC wyprowadza się z klucza szyfrowania pliku, więc ktokolwiek potrafi otworzyć dokument, potrafi też wyprodukować poprawny MAC nad zmodyfikowaną wersją, włącznie z każdym prawowitym odbiorcą; to symetryczny prymityw, a symetryczne prymitywy nie atrybuują. Jeśli musisz wiedzieć, kto coś zmienił, potrzebujesz podpisu cyfrowego z certyfikatem za nim, a PDF MAC dopełnia go wtedy, chroniąc strukturę przyrostową, której sam podpis nie obejmuje. Traktuj je jako warstwy i każ zgłaszać oba werdykty niezależnie, zamiast zgniatać je w jedną ikonę statusu
Punkty wejścia PDF MAC opisane tutaj — AddStandalonePDFMAC, SignPDFWithPFXAndAttachedPDFMAC, ValidatePDFMAC i ValidatePDFMACChain — przychodzą ze standardowym HotPDF Delphi Component dla Delphi i C++Builder, gdzie strona produktu niesie pełną referencję rekordu opcji, enumeracji statusów i tablicy walidacji per rewizja