Teknisk artikkel

PDF MAC-revisjonskjedevalidering i Delphi (ISO 32004)

HotPDF validerer en ISO/TS 32004 PDF MAC per revisjon snarere enn per fil. THotPDF.ValidatePDFMACChain går gjennom hver inkrementelle oppdatering fra kjedeankeret fremover og verifiserer hver MAC mot en skrivebeskyttet prefiksstrøm som slutter ved revisjonens egen startxref og %%EOF. Én gyldig MAC på nyeste revisjon beviser ingenting om revisjonene under den

Her er scenariet som motiverer alt dette. Du leverer en AES-256-kryptert PDF med en PDF MAC på den. Noen åpner filen i en hex-editor, snur en byte inne i den første MAC-beskyttede revisjonen, og legger så til en helt ny revisjon som bærer en perfekt gyldig MAC av deres eget. Hvert visningsprogram åpner filen uten klage, og en naiv sjekker som hasher gjeldende byte-range mot MAC-en i den aktive traileren rapporterer suksess — for den MAC-en er virkelig korrekt for bytene den dekker. Skaden sitter to revisjoner ned, i en region ingen sjekket på nytt

Hvorfor beviser en gyldig MAC på toppnivå ikke at filen er intakt?

Fordi en PDF MAC dekker et prefiks, ikke et dokument. Inkrementell oppdatering er en førsteklasses del av formatet: hver lagring legger til en ny kropp, en ny kryssreferanseseksjon og en ny trailer, mens de eldre bytene blir nøyaktig der de var. ISO/TS 32004 rir på den modellen, så hver revisjon bærer sin egen /AuthCode-ordbok som autentiserer filen slik den sto i det øyeblikket, og å verifisere bare den nyeste lar hver tidligere revisjon uundersøkt. HotPDF eksponerer derfor de to spørsmålene som to kall, og forskjellen mellom dem er hele poenget med denne artikkelen. ValidatePDFMAC svarer på om gjeldende revisjon er autentisk, og fyller en THPDFPDFMACValidationInfo-post; ValidatePDFMACChain svarer på om hver MAC-beskyttet revisjon i denne filen er autentisk, og fyller THPDFPDFMACChainValidationInfo med en per revisjon-matrise pluss en maskinlesbar feilårsak. På den manipulerte-og-re-MACede filen over, returnerer det første kallet True og det andre returnerer False mot revisjonsindeks 1

var
  Pdf: THotPDF;
  Chain: THPDFPDFMACChainValidationInfo;
  I: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if not Pdf.ValidatePDFMACChain('incoming.pdf', 'user', Chain) then
    begin
      // Feilen er én 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;

Hver MAC verifiseres på sin egen prefiksstrøm, aldri på endelig fillengde

Den dyreste feilen på dette området er å bruke endelig filstørrelse som øvre grense ved re-hashing av en eldre revisjon, noe som føy er etterfølgende byte inn i hver digest unntatt den nyeste og rapporterer manipulering på en sunn fil. HotPDF rekonstruerer i stedet, for hver revisjon, en avgrenset skrivebeskyttet strøm som slutter ved den revisjonens egen startxref-verdi fulgt av dens %%EOF, og hasher bare den. Å lokalisere grensen er kresenere enn det ser ut: den bokstavelige %%EOF kan opptre inne i en innholdsstrøm eller en streng, så en kandidat aksepteres bare når den umiddelbart foregående startxref parser til et tall likt kryssreferanseoffseten til seksjonen som valideres, uten annet enn mellomrom mellom dem. Revisjonen absorberer deretter nøyaktig én slutt-på-linje-sekvens etter markøren — en enkelt CR, en enkelt LF, eller ett CRLF-par — og ingenting mer. Den siste regelen biter i praksis, for en skriver som avgir en ekstra tom linje mellom to revisjoner har produsert byte tilhørende neste revisjon, og å svelge alt etterfølgende mellomrom inn i den forrige endrer stille begge digests. Seksjonsoppregning følger samme disiplin: HotPDF går kryssreferanseseksjonene eldste til nyeste nøyaktig én gang, og spiller av frie, direkte og objektstrøm-oppføringer slik at senere seksjoner overskriver tidligere tilstand, noe som er det motsatte av first-seen-wins-semantikken en aktiv-xref-parser anvender

HotPDF verifiserer hver ISO 32004 PDF MAC mot en prefiksstrøm som slutter ved den revisjonens egen startxref og slutt-på-fil-markør, så en byte snudd inne i revisjon 1 feiler kjeden selv om nyeste MAC fremdeles validerer rent
Hver revisjons MAC re-hasheres over sitt eget avgrensede prefiks, så å redigere revisjon 1 og legge til en ferskt MAC-et revisjon fortsatt tilfredsstiller ValidatePDFMAC mens ValidatePDFMACChain lander på revisjon 1

Hvor anker kjeden seg, og hva bryter den?

Den første revisjonen som bærer en gyldig /AuthCode er ankeret, og FirstMACRevisionIndex rapporterer hvor beskyttelsen begynner; alt før den er ubeskyttet ved konstruksjon, noe som er normalt. Alt etter den må være MAC-beskyttet, så å legge til én vanlig inkrementell oppdatering til en MAC-beskyttet fil feiler med pmcfRequiredRevisionMissing og den krenkende revisjonsindeksen — å tolerere et hull ville la en angriper strippe beskyttelse bare ved å lagre én gang til. Tre ytterligere invarianter gjelder på tvers av kjeden, hver med sin egen feilkode

  • pmcfKDFSaltChanged/KDFSalt må forbli stabil fra ankeret fremover, siden en roterende salt ville la en forfalsker re-derivere nøkler under parametere av deres eget valg
  • pmcfDigestDowngrade — digest-styrke sammenlignes mot den siste verifiserte MAC-en snarere enn den umiddelbart foregående revisjonen, så en kjede som starter under Modern-profilen ved SHA-384 kan ikke stille fortsette med SHA-256
  • pmcfPermissionDowngrade — en revisjon kan ikke rydde et PDF MAC-krav som en tidligere revisjon hadde autentisert

Konsekvensen verdt å internalisere er at historiske MAC-er verifiseres uavhengig selv når de ikke lenger er den aktive traileren. Det er derfor angrepet rediger-en-gammel-revisjon-og-legg-til-en-fersk-MAC fra åpningen ikke overlever: den nyeste MAC-en sjekker ut på egen hånd, ValidatePDFMAC er fornøyd, og kjeden lander fremdeles på revisjon 1 med pmcfRevisionInvalid

Signaturordning: trailer-nøkler først, signatureDigest sist

Når MAC-en er festet til en CMS-signatur snarere enn å stå alene, slutter skriveorden å være et stilistisk spørsmål. HotPDF krever at /AuthCode, /KDFSalt, ISO 32004 utviklerutvidelsen og /SigObjRef skrives inn i samme revisjon før signaturens /ByteRange beregnes; legger du til noen av dem etterpå lander de bytene utenfor rangen signaturen dekker, og produserer en fil hvis signatur verifiserer mens MAC-bindingen er usignert. De to digestene kjører så den andre veien, noe som ser sirkulært ut ved første blikk og ikke er det. PDF MAC signatureDigest binder de rå innholdsoktettene av CMS SignerInfo.signature OCTET STRING — ikke hele CMS DER, og ikke de signerte attributtene — så den konstrueres etter at rå signaturverdi finnes og injiseres som en id-attr-pdfMacData usignert attributt. Siden /Contents er ekskludert fra signaturens ByteRange og usignerte attributter aldri mater signaturberegningen, lukker sekvensen produser-signatur, bygg-MAC, pakk-CMS rent uten kryptografisk løkke. To følgeslutninger følger: /ByteRange-sentinellen og /Contents-plassholderen må forbli klartekst og utenfor objektstrømmer selv i en kryptert fil, ellers kan patcheren med fast bredde ikke finne dem; og når MAC-digesten også er SHA-256 gjenbrukes signeringsdigesten fullt ut, ellers oppdateres begge digest-kontekstene i én gjennomgang over utdatastrømmen

HotPDF skriveorden for en PDF MAC festet til en CMS-signatur: MAC-nøklene går inn i revisjonen før ByteRange måles, og signaturdigesten bygges etterpå fra de rå SignerInfo signaturokettene
Å skrive AuthCode, KDFSalt, SigObjRef og utviklerutvidelsen før ByteRange måles, er det som holder MAC-bindingen innenfor rangen signaturen dekker
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 digestene
        // 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 bane fra den andre enden: les den direkte /AuthCode fra den gjeldende aktive klassiske kryssreferanse-traileren, følg den generasjonsbevisste indirekte /SigObjRef, bekreft at den binder det ene signaturfeltets /V, og rapporter en dokumentdigest-feil separat fra en signaturdigest-feil. Det er forskjellige diagnoser, og å kollapse dem til én enkelt boolsk kaster den eneste informasjonen som sier om sideinnholdet eller signaturverdien ble rørt. Driver du allerede CMS-arbeid, sitter dette ved siden av PAdES-signeringsartikkelen og guiden til å verifisere signaturer i innlastede dokumenter

Stol aldri på /P: dekrypter 16-byte /Perms først

ISO/TS 32004 signaliserer at dette dokumentet krever en PDF MAC gjennom tillatelsesbit 13, og den opplagte måten å lese det på er den gale, for /P-heltallet i krypteringsordboken er klartekst og uautentisert — hvem som helst kan snu den biten i en teksteditor og nedgradere kravet. ISO 32000-2 §7.6 leverer svaret i /Perms-oppføringen, og HotPDF bruker den: dekrypter 16-byte /Perms-strengen med filkrypteringsnøkkelen under AES-256 CBC, null IV, ingen utfylling, og sjekk så hvert felt av klarteksten før du tror noe. Byte 1 til 4 holder tillatelsesverdien i little-endian orden og må være likt /P-heltallet nøyaktig; byte 5 til 8 er 0xFF; byte 9 er T- eller F-metadatakrypteringsflagget; byte 10 til 12 er den bokstavelige markøren adb. Bare når alt dette holder blir PermissionsAuthenticated True og bit 13 lest — og merk dens polaritet, for MAC-kravet hevdes når 0x1000-biten er ren. Et avvik mellom /P og de dekrypterte tillatelsene er ikke en advarsel å logge og passere; det er et forfalsket tillatelsessett, og det riktige svaret er å feile lukket

HotPDF autentiserer PDF-tillatelser ved å dekryptere seksten-byte Perms-strengen med filkrypteringsnøkkelen og sjekke little-endian tillatelsesverdien, FF-fyllbytene, metadataflagget og adb-markøren før bit 13 leses
Klartekst /P-heltallet er uautentisert, så PDF MAC-kravet leses først etter at hvert felt av den dekrypterte /Perms er sjekket

Algoritme-smidighet stopper ved digesten

ISO/TS 32004 lar deg velge dokumentdigesten, og bare dokumentdigesten. HotPDF beholder HMAC-SHA-256 for autentisering, HKDF-SHA-256 etter RFC 5869 for nøkkelderivasjon og AES-256 key wrap etter RFC 3394 fast under en variabel THPDFPDFMACDigestAlgorithm som spenner pmdaSHA256 gjennom pmdaSHA3_512, for den naturlige feilen er å behandle en SHA3-512-profil som lisens til å bytte HMAC-en også, noe som gir en fil som ikke lenger er en PDF MAC i noen interoperabel forstand. Én implementasjonsdetalj er verdt å kopiere hvis du skriver din egen verifiserer: les digest-OID-en ut av CMS AuthenticatedData før du hasher byte-rangen, for å hardkode SHA-256 og forsones etterpå gjør smidighet om til en etikett og lar en fiendtlig fil få deg til å strømme hele dokumentet før du oppdager at algoritmen aldri var støttet. CMSAlgorithmProtection, AuthenticatedData digest-algoritmen, integrity-info messageDigest og byte-range digesten må alle navngi én algoritme, og ethvert avvik feiler lukket

var
  Options: THPDFPDFMACOptions;
begin
  Options := THPDFPDFMACOptions.Compatibility;  // SHA-256, aksepterer alle seks
  Options := THPDFPDFMACOptions.Modern;         // SHA-384, avviser 256-bit
  Options := THPDFPDFMACOptions.HighAssurance;  // bare SHA3-512, AES-GCM

  // En tilpasset profil er lovlig, men algoritmen den genererer med
  // må også opptre i valideringens tillatelsesliste, ellers avvises
  // konfigurasjonen før en enkelt byte er skrevet
  Options.Profile := pmppCustom;
  Options.DigestAlgorithm := pmdaSHA512;
  Options.AllowedDigestAlgorithms := [pmdaSHA512, pmdaSHA3_512];
  Options.RequireAESGCM := True;
end;

Hva en PDF MAC beviser og ikke beviser

En verifisert PDF MAC-kjede beviser at hver beskyttet revisjon er byte-identisk med det som ble skrevet av noen som holdt filkrypteringsnøkkelen, at ingen beskyttet revisjon ble fjernet eller omordnet, og at ingen ubeskyttet revisjon ble lagt til etter ankeret — nøyaktig den klassen av angrep som ren AES-256-kryptering lar stå åpen, siden konfidensialitet ikke sier noe om integritet og en kryptert PDF med en innflettet revisjon dekrypterer like glad som en intakt. Det den ikke beviser, er forfatterskap. MAC-nøkkelen deriveres fra filkrypteringsnøkkelen, så hvem som helst som kan åpne dokumentet også kan produsere en gyldig MAC over en endret versjon, hver legitime mottaker inkludert; den er en symmetrisk primitiv, og symmetriske primitiver kan ikke attribuere. Trenger du å vite hvem som endret noe, trenger du en digital signatur med et sertifikat bak, og PDF MAC-en komplementerer den da ved å beskytte den inkrementelle strukturen signaturen alene ikke dekker. Behandle dem som lag og la de to dommene rapporteres uavhengig snarere enn kollapset til ett statusikon

PDF MAC-inngangspunktene beskrevet her — AddStandalonePDFMAC, SignPDFWithPFXAndAttachedPDFMAC, ValidatePDFMAC og ValidatePDFMACChain — leveres med standard HotPDF Delphi Component for Delphi og C++Builder, der produktsiden bærer den fullstendige referansen for opsjonsposten, statusopplistingene og per revisjon-valideringsmatrisen