HotPDF valideert een ISO/TS 32004 PDF MAC per revisie in plaats van per bestand. THotPDF.ValidatePDFMACChain loopt elke incrementele update vanaf het ketenanker vooruit en verifieert elke MAC tegen een alleen-lezen prefixstream die eindigt op de eigen startxref en %%EOF van die revisie. Eén geldige MAC op de nieuwste revisie bewijst niets over de revisies eronder
Hier is het scenario dat dit alles motiveert. U levert een AES-256-versleutelde PDF met een PDF MAC erop. Iemand opent het bestand in een hex-editor, wijzigt een byte binnen de eerste MAC-beschermde revisie, en plakt er een splinternieuwe revisie achter met een volkomen geldige MAC van eigen makelij. Elke viewer opent het bestand zonder klachten, en een naïeve checker die de huidige byterange hasht tegen de MAC in de actieve trailer meldt succes — want die MAC is echt correct voor de bytes die hij dekt. De schade zit twee revisies lager, in een gebied dat niemand opnieuw heeft gecontroleerd
Waarom bewijst een geldige MAC op topniveau niet dat het bestand intact is?
Omdat een PDF MAC een prefix dekt, geen document. Incrementele update is een eersteklas deel van het formaat: elke save voegt een nieuwe body, een nieuwe kruisverwijzingssectie en een nieuwe trailer toe, terwijl de oudere bytes precies blijven waar ze stonden. ISO/TS 32004 rijdt op dat model, dus elke revisie draagt zijn eigen /AuthCode-dictionary dat het bestand authenticeert zoals het op dat moment was, en alleen de nieuwste verifiëren laat elke eerdere revisie ongecontroleerd. HotPDF legt de twee vragen daarom bloot als twee aanroepen, en het verschil ertussen is het hele punt van dit artikel. ValidatePDFMAC beantwoordt "is de huidige revisie authentiek" en vult een THPDFPDFMACValidationInfo-record; ValidatePDFMACChain beantwoordt "is elke MAC-beschermde revisie in dit bestand authentiek" en vult THPDFPDFMACChainValidationInfo met een array per revisie plus een machineleesbare faalreden. Op het hierboven gemanipuleerde en opnieuw van MAC voorziene bestand geeft de eerste aanroep True en de tweede False bij revisie-index 1
var
Pdf: THotPDF;
Chain: THPDFPDFMACChainValidationInfo;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if not Pdf.ValidatePDFMACChain('incoming.pdf', 'user', Chain) then
begin
// De failure is één van 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;
Elke MAC verifieert op zijn eigen prefixstream, nooit op de uiteindelijke bestandsgrootte
De duurste bug in dit gebied is de uiteindelijke bestandsgrootte als bovengrens gebruiken bij het opnieuw hashen van een oudere revisie, wat achterliggende bytes in elke digest vouwt behalve de nieuwste en manipulatie meldt op een gezond bestand. HotPDF reconstrueert in plaats daarvan per revisie een begrensde alleen-lezen stream die eindigt op de eigen startxref-waarde van die revisie gevolgd door zijn %%EOF, en hasht alleen dat. De grens vinden is lastiger dan het lijkt: de letterlijke %%EOF kan binnen een contentstream of een string voorkomen, dus een kandidaat wordt alleen geaccepteerd wanneer de direct voorafgaande startxref parseert naar een getal gelijk aan de kruisverwijzings-offset van de sectie die wordt gevalideerd, met niets dan witruimte ertussen. De revisie absorbeert daarna exact één end-of-line-sequentie na de marker — één enkele CR, één enkele LF, of één CRLF-paar — en niets meer. Die laatste regel bijt in de praktijk, want een writer die een extra lege regel tussen twee revisies emitteert heeft bytes geproduceerd die toebehoren aan de volgende revisie, en alle volg witruimte in de vorige inslikken verandert stilletjes beide digests. Sectie-opsomming volgt dezelfde discipline: HotPDF loopt de kruisverwijzingssecties precies één keer van oud naar nieuw af, en speelt free-, directe en object-stream-entries opnieuw af zodat latere secties eerdere toestand overschrijven, het tegenovergestelde van de first-seen-wins-semantiek die een actieve-xref-parser toepast
Waar legt de keten het anker, en wat breekt hem?
De eerste revisie met een geldige /AuthCode is het anker, en FirstMACRevisionIndex rapporteert waar de bescherming begint; alles ervoor is by construction onbeschermd, wat normaal is. Alles erna moet MAC-beschermd zijn, dus één gewone incrementele update toevoegen aan een MAC-beschermd bestand faalt met pmcfRequiredRevisionMissing en de betreffende revisie-index — een gat tolereren zou een aanvaller de bescherming laten strippen door simpelweg nog een keer te saven. Drie verdere invarianten gelden over de keten, elk met zijn eigen faalcode
pmcfKDFSaltChanged— de/KDFSaltmoet stabiel blijven vanaf het anker, want een roterende salt zou een vervalser sleutels opnieuw kunnen laten afleiden onder parameters naar eigen keuzepmcfDigestDowngrade— digeststerkte wordt vergeleken met de laatst geverifieerde MAC in plaats van de direct voorafgaande revisie, dus een keten die onder het Modern-profiel op SHA-384 begint kan niet stilletjes met SHA-256 doorgaanpmcfPermissionDowngrade— een revisie mag een PDF MAC-vereiste die een eerdere revisie had geauthenticeerd niet wissen
De consequentie die u moet internaliseren is dat historische MAC's onafhankelijk worden geverifieerd, ook als ze niet langer de actieve trailer zijn. Daarom overleeft de aanval uit de inleiding — een oude revisie bewerken en er een verse MAC achteraan plakken — niet: de nieuwste MAC klopt op zichzelf, ValidatePDFMAC is tevreden, en de keten komt desondanks op revisie 1 uit met pmcfRevisionInvalid
Signature-volgorde: trailer-sleutels eerst, signatureDigest als laatste
Wanneer de MAC aan een CMS-signature hangt in plaats van zelfstandig te staan, houdt de schrijfvolgorde op een stilistische vraag te zijn. HotPDF vereist dat /AuthCode, /KDFSalt, de ISO 32004 developer extension en /SigObjRef in dezelfde revisie worden geschreven voordat de signature-/ByteRange wordt berekend; voeg er een van toe achteraf en die bytes belanden buiten de range die de signature dekt, wat een bestand oplevert waarvan de signature verifieert terwijl de MAC-binding unsigned is. De twee digests lopen daarna de andere kant op, wat op het eerste gezicht circulair oogt en het niet is. De PDF MAC signatureDigest bindt de ruwe content-octetten van de CMS SignerInfo.signature OCTET STRING — niet de hele CMS DER, en niet de signed attributes — dus hij wordt geconstrueerd nadat de ruwe signature-waarde bestaat en geïnjecteerd als een unsigned attribute id-attr-pdfMacData. Omdat /Contents buiten de signature-ByteRange valt en unsigned attributes nooit de signatureberekening voeden, sluit de volgorde signature produceren, MAC bouwen, CMS wikkelen netjes zonder cryptografische lus. Twee corollairen volgen: de /ByteRange-sentinel en de /Contents-placeholder moeten platte tekst blijven en buiten objectstreams, ook in een versleuteld bestand, anders kan de fixed-width patcher ze niet vinden; en wanneer de MAC-digest ook SHA-256 is, wordt de signing-digest gewoon hergebruikt, anders worden beide digest-contexten bijgewerkt in één passage over de uitvoerstream
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 documentdigest
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, en de twee digests
// worden apart gerapporteerd
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;
Validatie loopt hetzelfde pad vanaf de andere kant terug: lees de directe /AuthCode uit de momenteel actieve klassieke kruisverwijzingstrailer, volg de generatiebewuste indirecte /SigObjRef, bevestig dat die bindt aan de /V van het ene signatureveld, en rapporteer een documentdigest-faaling apart van een signature-digest-faaling. Dat zijn verschillende diagnoses, en ze samenvouwen in één boolean gooit de enige informatie weg die zegt of de paginacontent of de signaturewaarde is aangeraakt. Als u al CMS-werk doet, staat dit naast het PAdES-signeringartikel en de gids voor het verifiëren van signatures in geladen documenten
Vertrouw /P nooit: ontsleutel eerst de 16-byte /Perms
ISO/TS 32004 signaleert "dit document vereist een PDF MAC" via permissiebit 13, en de voor de hand liggende manier om dat te lezen is de verkeerde, want de /P-integer in het encryption dictionary is platte tekst en ongeauthenticeerd — iedereen kan die bit in een teksteditor omdraaien en de vereiste afwaarderen. ISO 32000-2 §7.6 levert het antwoord in de /Perms-entry, en HotPDF gebruikt het: ontsleutel de 16-byte /Perms-string met de bestandssleutel onder AES-256 CBC, nul-IV, geen padding, en controleer daarna elk veld van de platte tekst voordat u iets gelooft. Bytes 1 tot 4 bevatten de permissiewaarde in little-endian-volgorde en moeten exact gelijk zijn aan de /P-integer; bytes 5 tot 8 zijn 0xFF; byte 9 is de T- of F-vlag voor metadata-encryptie; bytes 10 tot 12 zijn de letterlijke marker adb. Pas wanneer dat alles klopt wordt PermissionsAuthenticated True en wordt bit 13 gelezen — en let op zijn polariteit, want de MAC-vereiste geldt wanneer de 0x1000-bit niet is gezet. Een mismatch tussen /P en de ontsleutelde permissies is geen waarschuwing om te loggen en voorbij te gaan; het is een vervalste permissieset, en het juiste antwoord is fail closed
Algoritme-agility stopt bij de digest
ISO/TS 32004 laat u de documentdigest kiezen, en alleen de documentdigest. HotPDF houdt HMAC-SHA-256 voor authenticatie, HKDF-SHA-256 volgens RFC 5869 voor sleutelafleiding en AES-256 key wrap volgens RFC 3394 vast onder een variabele THPDFPDFMACDigestAlgorithm die loopt van pmdaSHA256 tot pmdaSHA3_512, want de natuurlijke vergissing is een SHA3-512-profiel te behandelen als de licentie om ook de HMAC te wisselen, wat een bestand oplevert dat in geen enkele interoperabele zin nog een PDF MAC is. Eén implementatiedetail is het kopiëren waard als u uw eigen verifier schrijft: lees de digest-OID uit de CMS AuthenticatedData voordat u de byterange hasht, want SHA-256 hardcoden en daarna reconciliëren maakt van agility een label en laat een vijandig bestand u het hele document laten streamen voordat u ontdekt dat het algoritme nooit is ondersteund. CMSAlgorithmProtection, het AuthenticatedData-digestalgoritme, de integrity-info messageDigest en de byte-range-digest moeten allemaal één algoritme noemen, en elke afwijking leidt tot een fail closed
var
Options: THPDFPDFMACOptions;
begin
Options := THPDFPDFMACOptions.Compatibility; // SHA-256, accepteert alle zes
Options := THPDFPDFMACOptions.Modern; // SHA-384, weigert 256-bit
Options := THPDFPDFMACOptions.HighAssurance; // alleen SHA3-512, AES-GCM
// Een custom profiel is legaal, maar het algoritme waarmee het
// genereert moet ook in de validatie-allowlist staan, anders wordt
// de configuratie geweigerd voordat er één byte is geschreven
Options.Profile := pmppCustom;
Options.DigestAlgorithm := pmdaSHA512;
Options.AllowedDigestAlgorithms := [pmdaSHA512, pmdaSHA3_512];
Options.RequireAESGCM := True;
end;
Wat een PDF MAC bewijst en wat niet
Een geverifieerde PDF MAC-keten bewijst dat elke beschermde revisie byte-identiek is aan wat iemand met de bestandssleutel heeft geschreven, dat er geen beschermde revisie is verwijderd of herschikt, en dat er na het anker geen onbeschermde revisie is toegevoegd — precies de klasse aanvallen die gewone AES-256-encryptie openlaat, want vertrouwelijkheid zegt niets over integriteit en een versleutelde PDF met een ingelaste revisie ontsleutelt even graag als een intacte. Wat hij niet bewijst is auteurschap. De MAC-sleutel is afgeleid van de bestandssleutel, dus iedereen die het document kan openen kan ook een geldige MAC produceren over een gewijzigde versie, iedere legitieme ontvanger inbegrepen; het is een symmetrische primitief, en symmetrische primitieven kunnen niet attribueren. Als u moet weten wie iets heeft gewijzigd hebt u een digitale signature met een certificaat erachter nodig, en de PDF MAC vult die dan aan door de incrementele structuur te beschermen die de signature alleen niet dekt. Behandel ze als lagen en laat de twee vonnissen onafhankelijk rapporteren in plaats van samengevouwen in één statusicoon
De hier beschreven PDF MAC-entrypoints — AddStandalonePDFMAC, SignPDFWithPFXAndAttachedPDFMAC, ValidatePDFMAC en ValidatePDFMACChain — worden geleverd met de standaard HotPDF Delphi Component voor Delphi en C++Builder, waar de productpagina de volledige referentie draagt voor het options-record, de statusenumeraties en de validatie-array per revisie