Teknisk artikel

Validering av PDF MAC-revisionskedja i Delphi (ISO 32004)

HotPDF validerar en PDF MAC enligt ISO/TS 32004 per revision i stället för per fil. THotPDF.ValidatePDFMACChain går igenom varje inkrementell uppdatering från kedjans ankare och framåt och verifierar varje MAC mot en skrivskyddad prefixström som slutar vid den revisionens egen startxref och %%EOF. En giltig MAC på den senaste revisionen bevisar ingenting om revisionerna under den

Här är scenariot som motiverar alltsammans. Du skeppar en AES-256-krypterad PDF med en PDF MAC på sig. Någon öppnar filen i en hexeditor, flippar en byte inuti den första MAC-skyddade revisionen och lägger sedan till en helt ny revision som bär en fullständigt giltig egen MAC. Varje visare öppnar filen utan protester, och en naiv kontrollant som hashar det aktuella byteintervallet mot MAC:en i den aktiva trailern rapporterar framgång — eftersom den MAC:en verkligen är korrekt för de byte den täcker. Skadan sitter två revisioner ner, i en region som ingen kontrollerade om

Varför bevisar inte en giltig toppnivå-MAC att filen är intakt?

Eftersom en PDF MAC täcker ett prefix, inte ett dokument. Inkrementell uppdatering är en förstklassig del av formatet: varje sparning lägger till en ny kropp, en ny korsreferenssektion och en ny trailer, medan de äldre byten ligger kvar exakt där de var. ISO/TS 32004 vilar på den modellen, så varje revision bär sin egen /AuthCode-ordlista som autentiserar filen som den såg ut just då, och att bara verifiera den senaste lämnar varje tidigare revision oundersökt. HotPDF exponerar därför de två frågorna som två anrop, och skillnaden mellan dem är hela poängen med den här artikeln. ValidatePDFMAC svarar på ”är den aktuella revisionen autentisk” och fyller en THPDFPDFMACValidationInfo-post; ValidatePDFMACChain svarar på ”är varje MAC-skyddad revision i den här filen autentisk” och fyller THPDFPDFMACChainValidationInfo med en array per revision plus en maskinläsbar felorsak. På den manipulerade och sedan om-MACade filen ovan returnerar det första anropet True och det andra False mot revisionsindex 1

var
  Pdf: THotPDF;
  Chain: THPDFPDFMACChainValidationInfo;
  I: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if not Pdf.ValidatePDFMACChain('incoming.pdf', 'user', Chain) then
    begin
      // Felet är ett av 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;

Varje MAC verifieras mot sin egen prefixström, aldrig mot den slutliga fillängden

Den dyraste buggen i det här området är att använda den slutliga filstorleken som övre gräns vid omhashning av en äldre revision, vilket viker in avslutande byte i varje digest utom den senaste och rapporterar manipulation på en sund fil. HotPDF rekonstruerar i stället, för varje revision, en avgränsad skrivskyddad ström som slutar vid den revisionens eget startxref-värde följt av dess %%EOF, och hashar bara den. Att lokalisera gränsen är krångligare än det ser ut: den litterala %%EOF kan förekomma inuti en innehållsström eller en sträng, så en kandidat accepteras bara när den omedelbart föregående startxref tolkas till ett tal som är lika med korsreferensoffseten för sektionen som valideras, med inget annat än blanksteg mellan dem. Revisionen absorberar sedan exakt en radslutssekvens efter markören — en enda CR, en enda LF eller ett CRLF-par — och inget mer. Den sista regeln biter i praktiken, eftersom en skrivare som skriver ut en extra tomrad mellan två revisioner har producerat byte som tillhör nästa revision, och att svälja allt efterföljande blanksteg i den föregående ändrar tyst båda digesterna. Sektionsuppräkningen följer samma disciplin: HotPDF går igenom korsreferenssektionerna äldst till nyast exakt en gång och spelar upp fria, direkta och objektströmsposter så att senare sektioner skriver över tidigare tillstånd, vilket är motsatsen till den först-kom-först-semantik som en aktiv-xref-parser tillämpar

HotPDF verifierar varje ISO 32004 PDF MAC mot en prefixström som slutar vid den revisionens egen startxref och filslutsmarkör, så en byte som flippats inuti revision 1 fäller kedjan även om den senaste MAC:en fortfarande validerar rent
Varje revisions MAC omhashas över sitt eget avgränsade prefix, så att redigera revision 1 och lägga till en ny-MACad revision fortfarande tillfredsställer ValidatePDFMAC medan ValidatePDFMACChain landar på revision 1

Var förankras kedjan, och vad bryter den?

Den första revisionen som bär en giltig /AuthCode är ankaret, och FirstMACRevisionIndex rapporterar var skyddet börjar; allt före den är oskyddat by design, vilket är normalt. Allt efter den måste vara MAC-skyddat, så att lägga till en vanlig inkrementell uppdatering på en MAC-skyddad fil misslyckas med pmcfRequiredRevisionMissing och den skyldiga revisionsindexen — att tolerera en lucka skulle låta en angripare skala av skyddet helt enkelt genom att spara en gång till. Ytterligare tre invarianter gäller över kedjan, var och en med sin egen felkod

  • pmcfKDFSaltChanged/KDFSalt måste förbli stabil från ankaret och framåt, eftersom ett roterande salt skulle låta en förfalskare härleda om nycklar under parametrar som denne själv valt
  • pmcfDigestDowngrade — digeststyrkan jämförs mot den senast verifierade MAC:en snarare än den omedelbart föregående revisionen, så en kedja som börjar under Modern-profilen på SHA-384 kan inte tyst fortsätta med SHA-256
  • pmcfPermissionDowngrade — en revision får inte nollställa ett PDF MAC-krav som en tidigare revision hade autentiserat

Konsekvensen som är värd att internalisera är att historiska MAC:er verifieras oberoende även när de inte längre är den aktiva trailern. Det är därför attacken redigera-en-gammal-revision-och-lägg-till-en-färsk-MAC från inledningen inte överlever: den senaste MAC:en klarar sig på egen hand, ValidatePDFMAC är nöjd, och kedjan landar ändå på revision 1 med pmcfRevisionInvalid

Signaturordning: trailernycklar först, signatureDigest sist

När MAC:en är fäst vid en CMS-signatur i stället för att stå ensam slutar skrivordningen vara en stilistisk fråga. HotPDF kräver att /AuthCode, /KDFSalt, ISO 32004-utvecklartillägget och /SigObjRef skrivs in i samma revision innan signaturens /ByteRange beräknas; lägg till någon av dem efteråt och de byten hamnar utanför intervallet som signaturen täcker, vilket ger en fil vars signatur verifieras medan MAC-bindningen är osignerad. De två digesterna körs sedan åt andra hållet, vilket ser cirkulärt ut vid första anblicken men inte är det. PDF MAC:s signatureDigest binder de råa innehållsoktetterna i CMS SignerInfo.signature OCTET STRING — inte hela CMS DER, och inte de signerade attributen — så den konstrueras efter att det råa signaturvärdet finns och injiceras som ett osignerat attribut id-attr-pdfMacData. Eftersom /Contents är uteslutet från signaturens ByteRange och osignerade attribut aldrig matar signaturberäkningen, sluter sekvensen producera-signatur, bygg-MAC, packa-CMS rent utan någon kryptografisk slinga. Två följder: /ByteRange-sentinellen och /Contents-platshållaren måste förbli klartext och utanför objektströmmar även i en krypterad fil, annars kan patcharen med fast bredd inte hitta dem; och när MAC-digesten också är SHA-256 återanvänds signeringsdigesten direkt, annars uppdateras båda digestkontexterna i ett enda pass över utdataströmmen

HotPDF:s skrivordning för en PDF MAC fäst vid en CMS-signatur: MAC-nycklarna kommer in i revisionen innan ByteRange mäts, och signaturdigesten byggs efteråt från de råa SignerInfo-signaturoktetterna
Att skriva AuthCode, KDFSalt, SigObjRef och utvecklartillägget innan ByteRange mäts är vad som håller MAC-bindningen inom intervallet som signaturen täcker
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, och de två digesterna
        // rapporteras 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;

Valideringen spårar samma väg från andra änden: läs den direkta /AuthCode från den för närvarande aktiva klassiska korsreferenstrailern, följ den generationsmedvetna indirekta /SigObjRef, bekräfta att den binder det enda signaturfältets /V, och rapportera ett dokumentdigestfel separat från ett signaturdigestfel. Det är olika diagnoser, och att slå ihop dem till en enda boolean kastar bort den enda information som säger om sidinnehållet eller signaturvärdet rördes. Om du redan arbetar med CMS hör detta hemma bredvid artikeln om PAdES-signering och guiden om att verifiera signaturer i inlästa dokument

Lita aldrig på /P: dekryptera 16-byte /Perms först

ISO/TS 32004 signalerar ”det här dokumentet kräver en PDF MAC” via behörighetsbit 13, och det uppenbara sättet att läsa det är det felaktiga, eftersom /P-heltalet i krypteringsordlistan är klartext och oautentiserat — vem som helst kan flippa den biten i en texteditor och nedgradera kravet. ISO 32000-2 §7.6 ger svaret i posten /Perms, och HotPDF använder det: dekryptera 16-byte-strängen /Perms med filkrypteringsnyckeln under AES-256 CBC, noll-IV, ingen padding, och kontrollera sedan varje fält i klartexten innan något trotts. Byte 1 till 4 håller behörighetsvärdet i little-endian-ordning och måste exakt motsvara /P-heltalet; byte 5 till 8 är 0xFF; byte 9 är flaggan T eller F för metadatakryptering; byte 10 till 12 är den litterala markören adb. Först när allt detta stämmer blir PermissionsAuthenticated True och bit 13 läses — och lägg märke till dess polaritet, eftersom MAC-kravet påstås när biten 0x1000 är nollställd. En avvikelse mellan /P och de dekrypterade behörigheterna är inte en varning att logga och gå vidare från; det är en förfalskad behörighetsuppsättning, och rätt svar är att misslyckas stängt

HotPDF autentiserar PDF-behörigheter genom att dekryptera sextonbytessträngen Perms med filkrypteringsnyckeln och kontrollera little-endian-behörighetsvärdet, FF-fyllnadsbyten, metadataflaggan och adb-markören innan bit 13 läses
Klartextheltalet /P är oautentiserat, så PDF MAC-kravet läses först efter att varje fält i den dekrypterade /Perms har kontrollerats

Algoritmflexibiliteten slutar vid digesten

ISO/TS 32004 låter dig välja dokumentdigesten, och bara dokumentdigesten. HotPDF håller HMAC-SHA-256 för autentisering, HKDF-SHA-256 enligt RFC 5869 för nyckelhärledning och AES-256 key wrap enligt RFC 3394 fixerade under en variabel THPDFPDFMACDigestAlgorithm som spänner pmdaSHA256 till pmdaSHA3_512, eftersom det naturliga misstaget är att behandla en ”SHA3-512-profil” som licens att byta ut HMAC:en också, vilket ger en fil som inte längre är en PDF MAC i någon interoperabel mening. En implementationsdetalj är värd att kopiera om du skriver din egen verifierare: läs digest-OID:n ur CMS AuthenticatedData innan du hashar byteintervallet, eftersom att hårdkoda SHA-256 och stämma av efteråt förvandlar flexibilitet till en etikett och låter en fientlig fil få dig att strömma hela dokumentet innan du upptäcker att algoritmen aldrig stöddes. CMSAlgorithmProtection, AuthenticatedData-digestalgoritmen, integritetsinformationens messageDigest och byteintervallsdigesten måste alla namnge en och samma algoritm, och varje avvikelse misslyckas stängt

var
  Options: THPDFPDFMACOptions;
begin
  Options := THPDFPDFMACOptions.Compatibility;  // SHA-256, accepterar alla sex
  Options := THPDFPDFMACOptions.Modern;         // SHA-384, avvisar 256-bit
  Options := THPDFPDFMACOptions.HighAssurance;  // endast SHA3-512, AES-GCM

  // En anpassad profil är laglig, men algoritmen den genererar med
  // måste också finnas i valideringens tillåtelselista, annars
  // avvisas konfigurationen innan en enda byte skrivs
  Options.Profile := pmppCustom;
  Options.DigestAlgorithm := pmdaSHA512;
  Options.AllowedDigestAlgorithms := [pmdaSHA512, pmdaSHA3_512];
  Options.RequireAESGCM := True;
end;

Vad en PDF MAC bevisar och inte bevisar

En verifierad PDF MAC-kedja bevisar att varje skyddad revision är byteidentisk med vad som skrevs av någon som innehade filkrypteringsnyckeln, att ingen skyddad revision togs bort eller ordnades om, och att ingen oskyddad revision lades till efter ankaret — exakt den attackklass som vanlig AES-256-kryptering lämnar öppen, eftersom konfidentialitet inte säger något om integritet och en krypterad PDF med en inskarvad revision dekrypteras lika gärna som en intakt. Vad den inte bevisar är upphovsmannaskap. MAC-nyckeln härleds från filkrypteringsnyckeln, så alla som kan öppna dokumentet kan också producera en giltig MAC över en modifierad version, inklusive varje legitim mottagare; det är en symmetrisk primitiv, och symmetriska primitiver kan inte attribuera. Om du behöver veta vem som ändrade något behöver du en digital signatur med ett certifikat bakom sig, och PDF MAC:en kompletterar den sedan genom att skydda den inkrementella struktur som signaturen ensam inte täcker. Behandla dem som lager och låt de två utslagen rapporteras oberoende i stället för att slås ihop till en statusikon

De PDF MAC-ingångspunkter som beskrivs här — AddStandalonePDFMAC, SignPDFWithPFXAndAttachedPDFMAC, ValidatePDFMAC och ValidatePDFMACChain — levereras med standardkomponenten HotPDF Delphi Component för Delphi och C++Builder, där produktsidan bär den fullständiga referensen för optionsposten, statusuppräkningarna och valideringsarrayen per revision