HotPDF validerer en ISO/TS 32004 PDF MAC pr. revision i stedet for pr. fil. THotPDF.ValidatePDFMACChain gennemgår hver inkrementel opdatering frem fra kædens anker og verificerer hver MAC mod en skrivebeskyttet præfiksstrøm, der slutter ved revisionens egen startxref og %%EOF. Én gyldig MAC på den nyeste revision beviser ingenting om revisionerne under den
Her er scenariet, der motiverer det hele. Du afskiber en AES-256-krypteret PDF med en PDF MAC på. Nogen åbner filen i en hex-editor, vender en byte inde i den første MAC-beskyttede revision og tilføjer derefter en helt ny revision med en fuldstændig gyldig MAC af deres egen. Alle fremvisere åbner filen uden brok, og en naiv tjekker, der hasher det aktuelle byteinterval mod den MAC, der ligger i den aktive trailer, melder succes — fordi den MAC virkelig er korrekt for de bytes, den dækker. Skaden sidder to revisioner længere nede, i et område, som ingen genkontrollerede
Hvorfor beviser en gyldig MAC på øverste niveau ikke, at filen er intakt?
Fordi en PDF MAC dækker et præfiks, ikke et dokument. Inkrementel opdatering er en first-class del af formatet: hver gemning tilføjer en ny body, en ny krydsreferencesection og en ny trailer, mens de ældre bytes forbliver præcis, hvor de var. ISO/TS 32004 bygger på den model, så hver revision bærer sin egen /AuthCode-ordbog, der autentificerer filen, som den så ud i det øjeblik, og at verificere kun den nyeste efterlader hver tidligere revision uundersøgt. HotPDF eksponerer derfor de to spørgsmål som to kald, og forskellen mellem dem er hele pointen med denne artikel. ValidatePDFMAC svarer på, om den aktuelle revision er autentisk, og udfylder en THPDFPDFMACValidationInfo-record; ValidatePDFMACChain svarer på, om hver MAC-beskyttet revision i filen er autentisk, og udfylder THPDFPDFMACChainValidationInfo med et array pr. revision plus en maskinlæsbar fejlårsag. På den manipulerede og derefter ny-MACede fil ovenfor returnerer det første kald True, og det andet returnerer False for revisionsindeks 1
var
Pdf: THotPDF;
Chain: THPDFPDFMACChainValidationInfo;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if not Pdf.ValidatePDFMACChain('incoming.pdf', 'user', Chain) then
begin
// Fejlen er én af 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;
Hver MAC verificeres på sin egen præfiksstrøm, aldrig på filens endelige længde
Den dyreste fejl på dette område er at bruge den endelige filstørrelse som øvre grænse, når man re-hasher en ældre revision, hvilket folder efterfølgende bytes ind i hver digest undtagen den nyeste og melder manipulation på en sund fil. HotPDF rekonstruerer i stedet for hver revision en afgrænset skrivebeskyttet strøm, der slutter ved revisionens egen startxref-værdi efterfulgt af dens %%EOF, og hasherer kun den. At lokalisere grænsen er mere bøvlet, end det ser ud: den literale %%EOF kan optræde inde i en indholdsstrøm eller en streng, så en kandidat accepteres kun, når den umiddelbart foregående startxref parser til et tal, der er lig med krydsreferenceoffsettet for den section, der valideres, med intet andet end whitespace imellem. Revisionen optager derefter præcis én slut-på-linje-sekvens efter markøren — et enkelt CR, et enkelt LF eller ét CRLF-par — og ikke mere. Den sidste regel bider i praksis, fordi en skriver, der emitterer en ekstra tom linje mellem to revisioner, har produceret bytes, der tilhører den næste revision, og at synke alt efterfølgende whitespace ned i den forrige ændrer stille begge digests. Sectionoptælling følger samme disciplin: HotPDF går krydsreferencesectionerne igennem fra ældste til nyeste præcis én gang og afspiller free-, direkte- og object-stream-indgange, så senere sections overskriver tidligere tilstand, hvilket er det modsatte af den first-seen-wins-semantik, en aktiv-xref-parser anvender
Hvor ankrer kæden, og hvad bryder den?
Den første revision med en gyldig /AuthCode er ankeret, og FirstMACRevisionIndex rapporterer, hvor beskyttelsen begynder; alt før den er ubeskyttet af konstruktion, hvilket er normalt. Alt efter den skal være MAC-beskyttet, så at tilføje én almindelig inkrementel opdatering til en MAC-beskyttet fil fejler med pmcfRequiredRevisionMissing og det pågældende revisionsindeks — at tolerere et hul ville lade en angriber fjerne beskyttelse blot ved at gemme én gang mere. Yderligere tre invarianter gælder på tværs af kæden, hver med sin egen fejlkode
pmcfKDFSaltChanged—/KDFSaltskal forblive stabilt fra ankeret og frem, da et roterende salt ville lade en forfalsker re-aflede nøgler under parametre af eget valgpmcfDigestDowngrade— digest-styrke sammenlignes med den sidst verificerede MAC frem for den umiddelbart foregående revision, så en kæde, der starter under Modern-profilen ved SHA-384, ikke kan stille fortsætte med SHA-256pmcfPermissionDowngrade— en revision må ikke rydde et PDF MAC-krav, som en tidligere revision havde autentificeret
Konsekvensen, der er værd at internalisere, er, at historiske MACer verificeres uafhængigt, selv når de ikke længere er den aktive trailer. Det er derfor, angrebet fra indledningen — rediger en gammel revision og tilføj en frisk MAC — ikke overlever: den nyeste MAC tjekker fint på egen hånd, ValidatePDFMAC er tilfreds, og kæden lander stadig på revision 1 med pmcfRevisionInvalid
Signaturrækkefølge: trailer-nøgler først, signatureDigest sidst
Når MACen er knyttet til en CMS-signatur i stedet for at stå alene, holder skriverækkefølgen op med at være et stilspørgsmål. HotPDF kræver, at /AuthCode, /KDFSalt, ISO 32004-udviklerudvidelsen og /SigObjRef skrives ind i samme revision inden signaturens /ByteRange beregnes; tilføjes nogen af dem bagefter, lander de bytes uden for det interval, signaturen dækker, hvilket giver en fil, hvis signatur verificerer, mens MAC-bindingen er usigneret. De to digests kører derefter den anden vej, hvilket ved første øjekast ser cirkulært ud og ikke er. PDF MAC signatureDigest binder de rå indholdsoctetter af CMS SignerInfo.signature OCTET STRING — ikke hele CMS DER og ikke de signede attributter — så den konstrueres, efter den rå signaturværdi findes, og injiceres som en id-attr-pdfMacData-usigneret attribut. Da /Contents er ekskluderet fra signaturens ByteRange, og usignerede attributter aldrig indgår i signaturberegningen, lukker sekvensen fremstil-signatur, byg-MAC, pak-CMS rent uden kryptografisk loop. To følgeslutninger følger: /ByteRange-sentinellen og /Contents-pladsholderen skal forblive i klartekst og uden for objektstrømme, selv i en krypteret fil, ellers kan patcheren med fast bredde ikke finde dem; og når MAC-digesten også er SHA-256, genbruges signatur-digesten direkte, ellers opdateres begge digest-kontekster i ét enkelt gennemløb af outputstrømmen
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-dokumentdigest
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, og de to digests
// rapporteres separat
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;
Validering sporer samme vej fra den anden ende: læs den direkte /AuthCode fra den aktuelt aktive klassiske krydsreference-trailer, følg den generationsbevidste indirekte /SigObjRef, bekræft at den binder det ene signaturfelts /V, og rapportér en dokumentdigest-fejl separat fra en signaturdigest-fejl. Det er to forskellige diagnoser, og at skjule dem i én enkelt boolean smider den eneste information væk, der siger, om sideindholdet eller signaturværdien blev rørt. Hvis du allerede arbejder med CMS, placerer dette sig ved siden af PAdES-signeringsartiklen og vejledningen til verificering af signaturer i indlæste dokumenter
Stol aldrig på /P: dekryptér først den 16 byte lange /Perms
ISO/TS 32004 signalerer, at dette dokument kræver en PDF MAC, gennem tilladelsesbit 13, og den indlysende måde at læse den på er den forkerte, fordi /P-integeren i krypteringsordbogen er klartekst og uautentificeret — enhver kan vende den bit i en teksteditor og nedgradere kravet. ISO 32000-2 §7.6 leverer svaret i /Perms-indgangen, og HotPDF bruger den: dekryptér 16-byte /Perms-strengen med filens krypteringsnøgle under AES-256 CBC, nul-IV, ingen padding, og kontrollér derefter alle felter i klarteksten, før du tror på noget. Byte 1 til 4 holder tilladelsesværdien i little-endian-rækkefølge og skal svare præcis til /P-integeren; byte 5 til 8 er 0xFF; byte 9 er metadatakrypteringsflagget T eller F; byte 10 til 12 er den literale markør adb. Først når alt det holder, bliver PermissionsAuthenticated True, og bit 13 læses — og husk dens polaritet, for MAC-kravet er fremsat, når 0x1000-bitten er ryddet. Et misforhold mellem /P og de dekrypterede tilladelser er ikke en advarsel, man logger og går videre forbi; det er et forfalsket tilladelsessæt, og det rigtige svar er at fejle lukket
Algoritmeagilitet stopper ved digesten
ISO/TS 32004 lader dig vælge dokumentdigesten, og kun dokumentdigesten. HotPDF holder HMAC-SHA-256 til autentificering, HKDF-SHA-256 pr. RFC 5869 til nøgleafledning og AES-256 key wrap pr. RFC 3394 fast under en variabel THPDFPDFMACDigestAlgorithm, der spænder fra pmdaSHA256 til pmdaSHA3_512, fordi den naturlige fejl er at behandle en SHA3-512-profil som licens til også at skifte HMAC, hvilket giver en fil, der ikke længere er en PDF MAC i nogen interoperabel forstand. Én implementeringsdetalje er værd at kopiere, hvis du skriver din egen verifikator: læs digest-OID ud af CMS AuthenticatedData inden du hasherer byte-intervallet, for at hardkode SHA-256 og forlige bagefter gør agiliteten til en etiket og lader en fjendtlig fil få dig til at streame hele dokumentet, før du opdager, at algoritmen aldrig var understøttet. CMSAlgorithmProtection, digest-algoritmen i AuthenticatedData, integrity-info messageDigest og byte-interval-digesten skal alle navngive én algoritme, og enhver uenighed fejler lukket
var
Options: THPDFPDFMACOptions;
begin
Options := THPDFPDFMACOptions.Compatibility; // SHA-256, accepterer alle seks
Options := THPDFPDFMACOptions.Modern; // SHA-384, afviser 256-bit
Options := THPDFPDFMACOptions.HighAssurance; // kun SHA3-512, AES-GCM
// En tilpasset profil er lovlig, men algoritmen, den genererer med,
// skal også optræde i valideringstilladelseslisten, ellers afvises
// konfigurationen, før en eneste byte skrives
Options.Profile := pmppCustom;
Options.DigestAlgorithm := pmdaSHA512;
Options.AllowedDigestAlgorithms := [pmdaSHA512, pmdaSHA3_512];
Options.RequireAESGCM := True;
end;
Hvad en PDF MAC beviser og ikke beviser
En verificeret PDF MAC-kæde beviser, at hver beskyttet revision er byte-identisk med, hvad der blev skrevet af nogen med filens krypteringsnøgle, at ingen beskyttet revision blev fjernet eller omrokeret, og at ingen ubeskyttet revision blev tilføjet efter ankeret — præcis den klasse af angreb, som almindelig AES-256-kryptering efterlader åben, da fortrolighed ikke siger noget om integritet, og en krypteret PDF med en indsat revision dekrypteres lige så gladelig som en intakt. Hvad den ikke beviser, er forfatterskab. MAC-nøglen afledes af filens krypteringsnøgle, så enhver, der kan åbne dokumentet, også kan producere en gyldig MAC over en ændret version, alle legitime modtagere indregnet; det er en symmetrisk primitiv, og symmetriske primitiver kan ikke attribuere. Hvis du behøver at vide, hvem der ændrede noget, behøver du en digital signatur med et certifikat bag, og PDF MACen komplementerer den derefter ved at beskytte den inkrementelle struktur, signaturen alene ikke dækker. Behandl dem som lag, og lad de to domme rapporteres uafhængigt i stedet for at skjules i ét statusikon
PDF MAC-indgangspunkterne beskrevet her — AddStandalonePDFMAC, SignPDFWithPFXAndAttachedPDFMAC, ValidatePDFMAC og ValidatePDFMACChain — følger med den standard HotPDF Delphi Component til Delphi og C++Builder, hvor produktsiden bærer den fulde reference til options-recorden, status-enummerationerne og valideringsarrayet pr. revision