HotPDF validuje PDF MAC podle ISO/TS 32004 na revizi, ne na soubor. THotPDF.ValidatePDFMACChain projde každý inkrementální update od kotvy řetězce vpřed a ověří každý MAC proti read-only prefixovému proudu končícímu na vlastním startxref a %%EOF dané revize. Jeden validní MAC na nejnovější revizi nic nedokáže o revizích pod ní
Toto je scénář, který to všechno motivuje. Expedujete AES-256 šifrované PDF s PDF MAC. Někdo otevře soubor v hex editoru, překlopí bajt uvnitř první revize chráněné MACem, pak připojí úplně novou revizi nesoucí dokonalý validní MAC svůj vlastní. Každý prohlížeč otevře soubor bez reptání a naivní kontrolor, který hashuje aktuální bajtový rozsah proti MACu v aktivním traileru, hlásí úspěch — protože ten MAC je skutečně správný pro bajty, které kryje. Škoda sedí o dvě revize níž, v oblasti, kterou nikdo znovu nezkontroloval
Proč validní MAC nejvyšší úrovně nedokazuje, že je soubor v pořádku?
Protože PDF MAC kryje prefix, ne dokument. Inkrementální update je prvotřídní část formátu: každé uložení připojí nové tělo, novou sekci křížových odkazů a nový trailer, zatímco starší bajty zůstávají přesně tam, kde byly. ISO/TS 32004 jede na tom modelu, takže každá revize nese vlastní slovník /AuthCode autentizující soubor, jak stál v tu chvíli, a ověření jen toho nejnovějšího nechá každou dřívější revizi neprozkoumanou. HotPDF proto vystavuje obě otázky jako dvě volání a rozdíl mezi nimi je celý smysl tohoto článku. ValidatePDFMAC odpovídá „je aktuální revize autentická" a plní záznam THPDFPDFMACValidationInfo; ValidatePDFMACChain odpovídá „je každá revize chráněná MACem v tomto souboru autentická" a plní THPDFPDFMACChainValidationInfo polem na revizi plus strojově čitelným důvodem selhání. Na výše popsaném souboru překlopeném a znovu MACnutém vrátí první volání True a druhé False proti indexu revize 1
var
Pdf: THotPDF;
Chain: THPDFPDFMACChainValidationInfo;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if not Pdf.ValidatePDFMACChain('incoming.pdf', 'user', Chain) then
begin
// Selhání je jedno 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ždý MAC se ověřuje na vlastním prefixovém proudu, nikdy na délce finálního souboru
Nejdražší chyba v téhle oblasti je brát finální velikost souboru jako horní mez při opětovném hashování starší revize, což zalije koncové bajty do každého digestu kromě nejnovějšího a hlásí manipulaci na zdravém souboru. HotPDF místo toho pro každou revizi rekonstruuje ohraničený read-only proud končící na vlastní hodnotě startxref dané revize následované jejím %%EOF a hashuje jen to. Najít hranici je zajímavější, než vypadá: literál %%EOF se může objevit uvnitř content streamu nebo řetězce, takže kandidát se přijímá jen tehdy, když bezprostředně předcházející startxref parsuje na číslo rovné offsetu křížových odkazů sekce, která se validuje, s ničím mezitím kromě mezer. Revize pak absorbuje přesně jednu sekvenci konce řádku po značce — jediný CR, jediný LF nebo jednu dvojici CRLF — a nic víc. To poslední pravidlo kousne v praxi, protože zapisovač, který emituje mezi dvěma revizemi prázdný řádek navíc, vyrobil bajty patřící další revizi a polykání veškerého koncového whitespace do předchozí tiše změní oba digesty. Výčet sekcí dodržuje tutéž disciplínu: HotPDF projde sekce křížových odkazů od nejstarší k nejnovější přesně jednou, přehrávající volné, přímé i objektoproudové položky, takže pozdější sekce přepisují dřívější stav, což je opak sémantiky první-vidí-vítězí, kterou aplikuje parser aktivního xrefu
Kde se řetězec kotví a co ho zlomí?
První revize nesoucí validní /AuthCode je kotva a FirstMACRevisionIndex hlásí, kde ochrana začíná; všechno před ní je konstručně nechráněno, což je normální. Všechno po ní musí být chráněno MACem, takže připojení jednoho prostého inkrementálního update k souboru chráněnému MACem selže s pmcfRequiredRevisionMissing a indexem provinilé revize — tolerování mezery by dovolilo útočníkovi odnést ochranu prostým uložením ještě jednou. Další tři invarianty drží napříč řetězcem, každý se svým kódem selhání
pmcfKDFSaltChanged—/KDFSaltmusí zůstat stabilní od kotvy dál, protože rotující sůl by dovolila padělateli odvozovat klíče znovu pod parametry vlastní volbypmcfDigestDowngrade— síla digestu se srovnává s posledním ověřeným MACem, ne s bezprostředně předcházející revizí, takže řetězec startující pod profilem Modern na SHA-384 nemůže tiše pokračovat na SHA-256pmcfPermissionDowngrade— revize nesmí vymazat požadavek PDF MAC, který dřívější revize autentizovala
Důsledek, který stojí za vstřebání, je, že historické MACy se ověřují nezávisle, i když už nejsou aktivním trailerem. Proto útok uprav-starší-revizi-a-připoj-čerstvý-MAC z úvodu nepřežije: nejnovější MAC projde na svém vlastním vzorku, ValidatePDFMAC je spokojen a řetězec i tak dopadne na revizi 1 s pmcfRevisionInvalid
Pořadí podpisu: klíče traileru nejdřív, signatureDigest naposledy
Když je MAC připojen k CMS podpisu místo toho, aby stál sám, přestává pořadí zápisu být stylistickou otázkou. HotPDF vyžaduje, aby /AuthCode, /KDFSalt, rozšíření vývojáře ISO 32004 a /SigObjRef byly zapsány do téže revize před spočítáním /ByteRange podpisu; připojíte-li cokoli z toho potom, ty bajty dopadnou mimo rozsah, který podpis kryje, a vznikne soubor, jehož podpis projde, zatímco vazba MACu je nepodepsaná. Dva digesty pak běží opačným směrem, což vypadá cyklicky na první pohled a není. PDF MAC signatureDigest váže syrové obsahové oktety CMS SignerInfo.signature OCTET STRING — ne celý CMS DER a ne podepsané atributy — takže se konstruuje poté, co syrová hodnota podpisu existuje, a vstřikuje se jako nepodepsaný atribut id-attr-pdfMacData. Protože /Contents je z podpisového ByteRange vyloučeno a nepodepsané atributy nikdy nekrmí výpočet podpisu, sekvence vyrob-podpis, postav-MAC, zabal-CMS se uzavírá čistě bez kryptografické smyčky. Z toho plynou dva důsledky: sentinel /ByteRange a zástupný symbol /Contents musejí zůstat v čistém textu a mimo objektové proudy, i v šifrovaném souboru, jinak je patcher pevné šířky nenajde; a když je MAC digest rovněž SHA-256, podepisovací digest se použije rovnou znovu, jinak se oba digest kontexty aktualizují v jediném průchodu výstupním proudem
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; // SHA-384 digest dokumentu
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 dva digesty
// se hlásí odděleně
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;
Validace sleduje tutéž cestu z opačného konce: přečte přímé /AuthCode z právě aktivního klasického traileru křížových odkazů, následuje generaci-respektující nepřímé /SigObjRef, potvrdí, že váže /V jediného pole podpisu, a hlásí selhání digestu dokumentu odděleně od selhání digestu podpisu. To jsou odlišné diagnózy a slepení do jediného booleanu vyhodí jedinou informaci, která říká, zda byl dotčen obsah stránky, nebo hodnota podpisu. Děláte-li už práci s CMS, sedí to vedle článku o podepisování PAdES a průvodce ověřováním podpisů v načtených dokumentech
Nikdy nevěřte /P: nejdřív dešifrujte 16bajtové /Perms
ISO/TS 32004 signalizuje „tento dokument vyžaduje PDF MAC" bitem oprávnění 13 a zjevný způsob, jak ho číst, je ten špatný, protože celé číslo /P ve šifrovacím slovníku je čistý text bez autentizace — kdokoli může ten bit překlopit v textovém editoru a downgradeovat požadavek. ISO 32000-2 §7.6 dodává odpověď v položce /Perms a HotPDF ji používá: dešifrujte 16bajtový řetězec /Perms souborovým šifrovacím klíčem pod AES-256 CBC, nulovým IV, bez paddingu, pak zkontrolujte každé pole čistého textu, než něčemu uvěříte. Bajty 1 až 4 drží hodnotu oprávnění v little-endian pořadí a musí se rovnat celému číslu /P přesně; bajty 5 až 8 jsou 0xFF; bajt 9 je příznak šifrování metadat T nebo F; bajty 10 až 12 jsou literální značka adb. Jen když všechno toto drží, stává se PermissionsAuthenticated True a bit 13 se čte — a dejte pozor na jeho polaritu, protože požadavek MACu se tvrdí, když je bit 0x1000 vypnutý. Nesoulad mezi /P a dešifrovanými oprávněními není varování, které se zaloguje a přejde; je to padělaná sada oprávnění a správnou odpovědí je selhání zavřeně
Agilita algoritmů končí u digestu
ISO/TS 32004 vám nechá zvolit digest dokumentu a jen digest dokumentu. HotPDF drží HMAC-SHA-256 pro autentizaci, HKDF-SHA-256 podle RFC 5869 pro odvozování klíčů a AES-256 key wrap podle RFC 3394 pevně pod proměnným THPDFPDFMACDigestAlgorithm rozpínajícím pmdaSHA256 až pmdaSHA3_512, protože přirozená chyba je brát „profil SHA3-512" jako licenci vyměnit i HMAC, což vyrábí soubor, který už není PDF MAC v žádném interoperabilním smyslu. Jeden implementační detail stojí za okopírování, píšete-li vlastní verifikátor: přečtěte digest OID z CMS AuthenticatedData před hashováním bajtového rozsahu, protože zadrátování SHA-256 a smířování potom promění agilitu v etiketu a nechá nepřátelský soubor nechat vás streamovat celý dokument, než zjistíte, že algoritmus nebyl nikdy podporován. CMSAlgorithmProtection, digest algoritmus AuthenticatedData, integrity-info messageDigest a digest bajtového rozsahu musejí všechny pojmenovávat jeden algoritmus a jakýkoli nesoulad selhává zavřeně
var
Options: THPDFPDFMACOptions;
begin
Options := THPDFPDFMACOptions.Compatibility; // SHA-256, přijímá všech šest
Options := THPDFPDFMACOptions.Modern; // SHA-384, odmítá 256bitové
Options := THPDFPDFMACOptions.HighAssurance; // jen SHA3-512, AES-GCM
// Vlastní profil je legální, ale algoritmus, který generuje s,
// musí být rovněž v allowlistu validace, jinak je konfigurace
// odmítnuta, než se zapíše jediný bajt
Options.Profile := pmppCustom;
Options.DigestAlgorithm := pmdaSHA512;
Options.AllowedDigestAlgorithms := [pmdaSHA512, pmdaSHA3_512];
Options.RequireAESGCM := True;
end;
Co PDF MAC dokazuje a nedokazuje
Ověřený řetězec PDF MAC dokazuje, že každá chráněná revize je bajt za bajt identická s tím, co zapsal někdo držící souborový šifrovací klíč, že žádná chráněná revize nebyla odstraněna ani přeřazena a že po kotvě nebyla připojena žádná nechráněná revize — přesně ta třída útoků, kterou otevřenou nechává prosté šifrování AES-256, protože důvěrnost neříká nic o integritě a šifrované PDF s napíchanou revizí dešifruje stejně spokojeně jako neporušené. Co nedokazuje, je autorství. MAC klíč se odvozuje ze souborového šifrovacího klíče, takže kdokoli, kdo může otevřít dokument, může vyrobit i validní MAC přes upravenou verzi, každý legitimní příjemce nevyjímaje; je to symetrická primitiva a symetrická primitiva nepřipisují. Potřebujete-li vědět kdo něco změnil, potřebujete digitální podpis s certifikátem za ním a PDF MAC ho pak doplňuje tím, že chrání inkrementální strukturu, kterou samotný podpis nekryje. Berte je jako vrstvy a nechte oba rozsudky hlásit nezávisle místo slepení do jedné ikonky stavu
Vstupní body PDF MAC popsané zde — AddStandalonePDFMAC, SignPDFWithPFXAndAttachedPDFMAC, ValidatePDFMAC a ValidatePDFMACChain — putují se standardní HotPDF Delphi Component pro Delphi a C++Builder, kde produktová stránka nese kompletní referenci záznamu voleb, výčtů stavů a pole validace na revizi