Tekninen artikkeli

Muutosanalyysi PDF:n allekirjoituksen jälkeen PDFiumilla

Selvittääkseen mitä PDF:ssä muuttui sen allekirjoituksen jälkeen, Delphille ja Lazarukselle tarkoitettu PDFium Component tarjoaa TPdf.AnalyzeSignatureRevisionsin, allekirjoituksen jälkeisen revisiomuutosanalyysin joka rakentaa jokaisen inkrementaalisen revision uudelleen alkuperäisistä tiedostotavuista, arvostelee jokaisen myöhemmän objektimuutoksen kyseisen allekirjoituksen DocMDP- ja FieldMDP-sääntöjä vasten ja raportoi varjo-objektimäärittelyt erillisenä riskinä. Tilanne jota se tavoittelee on tuttu kaikille jotka käsittelevät sopimuksia: sertisoitu lomake lähtee matkaan, palaa kahden inkrementaalisen tallennuksen kera, ja jokainen allekirjoitus yhä verifioituu. Se on odotettua, koska allekirjoitus kattaa vain oman revisionsinsa tavut. Oikea kysymys on sallittiinko ne myöhemmät tallennukset, eikä vihreä rasti allekirjoituksessa vastaa siihen

Miksi PDFiumin allekirjoitus-API ei voi näyttää mitä allekirjoituksen jälkeen muuttui?

PDFiumin allekirjoitus-API ei voi näyttää allekirjoituksen jälkeisiä muutoksia koska se lukee vain allekirjoitussanakirjan: /Contents, /ByteRange, /SubFilter ja DocMDP-luvan arvon. PDFiumilla ei ole inkrementaalista revisiograafia, se ei jäsentä FieldMDP-transformin parametreja eikä tarjoa objektitason diffiä revisioiden välillä, joten FPdfPades.pasin analyysaattori toimii suoraan raakatavuilla sen sijaan. Sillä on käytännön seuraus jonka ympärille kannattaa suunnitella. TPdf.AnalyzeSignatureRevisions lukee tavut jotka säilytettiin asiakirjan latautuessa, ei koskaan SaveAsin tuottamaa kopiota, koska uudelleenkirjoitettu tiedosto on menettänyt juuri sen revisiorakenteen jota analysoidaan. Jos asiakirja tuli progressiivisesta lähteestä jonka lataus ei ole valmistunut, raportti palauttaa arvot SourceStatus = pvssIncomplete ja Status = prasIndeterminate sen sijaan että analysoisi katkaistua tiedostoa

Revisiorajojen rakentaminen uudelleen startxrefistä, xref-streamsista ja /Prev-ketjusta

Analyysaattori rakentaa revisiorajat uudelleen seuraamalla jokaisen startxrefin taakse klassisten xref-taulujen, ristiviittausstreamien, hybridi-viittauksen /XRefStm-merkintöjen ja /Prev-ketjun kautta, kuten inkrementaalisille päivityksille on määritelty ISO 32000-1:n §7.5.6:ssa ja §7.5.8:ssa. Jokaisen allekirjoituksen kattama pituus on sen toisen ByteRange-välin loppu, ja analyysaattori kuvaa kyseisen pituuden siihen revisioon jonka xref-osaan se osuu. Kun mikään revisio ei täsmää, allekirjoitus saa prrCoveredRevisionNotFoundin ja Indeterminate-tilan. Jokaisen objektin tila toistetaan sitten kattavaan revisioon asti, ja jokaista myöhempää xref-merkintää verrataan kyseiseen tilaan. Tällä on enemmän väliä kuin kuulostaa: jotkut kirjoittajat toistavat koko xref-taulun jokaisella inkrementaalisella tallennuksella, ja merkintä joka yhä osoittaa samaan muuttumattomaan objektiin ohitetaan sen sijaan että raportoitaisiin muutoksena. Ilman kyseistä vertailua täysin laillinen lomakkeen täyttö hukkuisi satoihin valemuutoksiin

Miten AnalyzeSignatureRevisions rakentaa inkrementaaliset revisiot uudelleen raakoista PDF-tavuista Delphissä: allekirjoituksen ByteRange päättyy kattavan revision sisälle, xrefin Prev-ketju kulkee taaksepäin jokaisen myöhemmän tallennuksen läpi, objektin tila toistetaan kattavaan revisioon, ja muuttumattomat toistetut merkinnät ohitetaan sen sijaan että raportoitaisiin muutoksina
Allekirjoitus kattaa vain oman revisionsinsa tavut, joten analyysaattori kuvaa toisen ByteRange-välin revisioon ja arvostelee jokaisen myöhemmän xref-merkinnän toistettua objektitilaa vasten

Varjomäärittelyt ovat tapaus joka ansaitsee eniten huomiota. Objektirunko joka ilmestyy myöhemmän revision tavualueen sisälle mutta johon kyseisen revision xref ei viittaa, on näkymätön tavalliselle katselimelle, ja se on juuri sellaista lavastusta johon varjohyökkäykset nojaavat: piilotettu sisältö istutetaan ennen tai jälkeen allekirjoituksen ja aktivoidaan myöhemmin kääntämällä viittaus. AnalyzePadesSignatureRevisionsBytes kirjaa tällaisen objektin ei-auktoritatiiviseksi muutokseksi arvolla IsAuthoritative = False, arvostelee sen prdSuspiciousiksi riippumatta lupatasosta ja lisää prrUnreferencedObjectDefinitionin riskijoukkoon. Kaksi sukua olevaa riskiä kattaa muut rakennetemput: prrDuplicateObjectDefinition laukeaa kun yksi xref-osa luettelee saman objektin useammin kuin kerran, ja prrSignatureObjectRedefined laukeaa kun myöhempi revisio määrittelee olemassa olevan allekirjoitusobjektin uudelleen

Varjo-objektimäärittely myöhemmän PDF-revision tavualueen sisällä: objektirunko on olemassa mutta yksikään xref-merkintä ei viittaa siihen, joten katselimet eivät koskaan näytä sitä, ja AnalyzeSignatureRevisions PDFium Componentissa kirjaa sen ei-auktoritatiiviseksi, arvostelee sen prdSuspiciousiksi ja nostaa prrUnreferencedObjectDefinitionin tupla- ja uudelleenmääriteltyjen allekirjoitusten riskien rinnalle
Piilotettu sisältö istutetaan ennen tai jälkeen allekirjoituksen ja aktivoidaan myöhemmin kääntämällä viittaus, mistä syystä viittaamaton runko arvostellaan epäilyttäväksi riippumatta DocMDP-lupatasosta
uses
  SysUtils, TypInfo, PDFium, FPdfPades;

const
  ShadowTag: array[Boolean] of string = ('', ' (shadow)');
var
  Pdf: TPdf;
  Report: TPadesRevisionAnalysisReport;
  i, j: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'contract-returned.pdf';
    Pdf.Active := True;
    Report := Pdf.AnalyzeSignatureRevisions;
    Writeln('Revisions: ', Report.RevisionCount,
      '  Signatures: ', Report.SignatureCount,
      '  Overall: ', GetEnumName(TypeInfo(TPadesRevisionAnalysisStatus),
        Ord(Report.Status)));
    for i := 0 to High(Report.Signatures) do
      with Report.Signatures[i] do
      begin
        Writeln(Format('Signature %d covers revision %d, %d later, P=%d, FieldMDP=%s',
          [SignatureIndex, CoveredRevisionIndex, LaterRevisionCount,
           DocMdpPermission,
           GetEnumName(TypeInfo(TPadesFieldMdpAction), Ord(FieldMdpAction))]));
        for j := 0 to High(Changes) do
          Writeln(Format('  rev %d  obj %d  %s -> %s%s',
            [Changes[j].RevisionIndex, Changes[j].ObjectNumber,
             GetEnumName(TypeInfo(TPadesRevisionChangeKind), Ord(Changes[j].Kind)),
             GetEnumName(TypeInfo(TPadesRevisionDecision), Ord(Changes[j].Decision)),
             ShadowTag[not Changes[j].IsAuthoritative]]));
      end;
  finally
    Pdf.Free;
  end;
end;

Miten DocMDP ja FieldMDP täytäntöönpanotaan jokaisen allekirjoituksen kohdalla?

DocMDP ja FieldMDP täytäntöönpanotaan erikseen jokaiselle allekirjoitukselle kyseisen allekirjoituksen omassa kattamassa versiossa, joten sertifiointiallekirjoitus ja myöhempi hyväksyntäallekirjoitus samassa tiedostossa voivat päätyä eri tuomioihin samasta muokkauksesta. Jokainen myöhempi objekti luokitellaan ensin TPadesRevisionChangeKindiksi sen /Type-, /Subtype- ja /FT-merkintöjen sekä sen roolin perusteella jota se näyttelee sivun, lomakkeen, annotaation ja DSS-graafien joukossa. Mikä tahansa joka kantaa /JavaScriptia, /JS:ä, /Launchia, /OpenActionia, /AA:ta, /RichMediaa tai /EmbeddedFileia muuttuu prckActiveContentiksi. Päätös noudattaa sitten ISO 32000-1 §12.8.2.2:ta: arvolla P=1 kaikki paitsi ristiviittausdata ja validointimateriaali on kiellettyä; P=2 sallii lomakkeiden täytön ja lisäallekirjoitukset mutta hylkää annotaatiomuutokset; P=3 sallii myös annotaatiot. Sivun sisältö, asiakirjan rakenne, metadata, aktiivinen sisältö ja poistetut objektit ovat kiellettyjä millä tahansa DocMDP-tasolla, ja arvotellaan prdSuspiciousiksi kun allekirjoitus ei kanna lainkaan DocMDP:tä, koska hyväksyntäallekirjoitus ei muodollisesti kiellä mitään mutta lukija ei enää näe sitä mitä allekirjoitettiin

FieldMDP, ISO 32000-1:n §12.8.2.4:stä, kaventaa lomakekenttäpäätöstä lisää. pfmaAll lukitsee jokaisen kentän, pfmaInclude lukitsee vain luetellut kentät, ja pfmaExclude lukitsee kaiken paitsi luetellut kentät. Soveltaakseen Includea tai Excludea analyysaattori ratkaisee jokaisen muuttuneen kentän täydellisesti kvalifioituun nimeensä /Parent-ketjun kautta ja vertaa sitä lukituslistaan täsmällisellä vertailulla, joten luetteloi päätekenttien nimet sen sijaan että odottaisit vanhemman nimen kattavan sen lapset. Kun nimeä ei voida ratkaista tai transformi käyttää toimintoa jota jäsentin ei tunnista, muutos muuttuu prdIndeterminateiksi ja prrFieldMdpUnresolved nostetaan. Muutosta kohti tehdyt päätökset kääritään sitten huonoimman ensin -periaatteella, Suspicious sijoittuu Disallowedin yläpuolelle, Disallowed Indeterminaten yläpuolelle ja Indeterminate Allowedin yläpuolelle, joten yksi varjo-objekti painaa enemmän kuin mikä tahansa määrä laillisia kenttäpäivityksiä

Arvosteluputki jonka AnalyzeSignatureRevisions soveltaa jokaiseen allekirjoituksen jälkeiseen muutokseen Delphissä: TPadesRevisionChangeKind Type- ja Subtype-merkintöistä, DocMDP-päätös kattavassa versiossa arvosta P=1 arvoon P=3, FieldMDP-lukitustarkistus täydellisesti kvalifioituihin kenttänimiin ja huonoimman ensin -kierros prdSuspiciousista alas prdAllowediin
Yksi varjo-objekti painaa enemmän kuin mikä tahansa määrä laillisia kenttäpäivityksiä koska Suspicious sijoittuu Disallowedin, Indeterminaten ja Allowedin yläpuolelle, kun taas jotkut riskit kirjataan tilan rinnalle laskematta sitä alas

Miksi jotkut muutokset palaavat Indeterminatena turvallisen sijaan?

Muutokset palaavat Indeterminatena aina kun analyysaattori ei voi todistaa muutoksen olevan sallittu, koska allekirjoitustarkistuksessa tuntematon ei saa koskaan raportoitua sallituksi. Yksi yleinen tapaus käsitellään sen sijaan täsmällisesti: pitkäaikaisvalidointi lisää /DSSin ja kirjoittaa katalogin uudelleen, mikä muuten laskettaisiin rakennemuutokseksi arvolla P=1. Analyysaattori irrottaa /DSSin ja /Extensionsin vanhasta ja uudesta katalogisanakirjasta ja vertaa loppua; kun mikään muu ei eroa, uudelleenkirjoitus käsitellään validointimateriaalin päivityksenä ja sallitaan, joten B-LT- ja B-LTA-laajennus ei riko sertifiointiallekirjoitusta. Muut aukot jätetään tarkoituksella auki. Tyyppi-2-merkinnät ristiviittausstreamissa osoittavat pakattuihin objektistreamsuihin, ja analyysaattori ei laajenna objektistreamsia tämän turvarajan sisällä, joten nuo muutokset nousevat pintaan prckCompressedObjectina prrCompressedObjectUnresolvedin kera, kiellettyinä arvolla P=1 ja muuten Indeterminatena. Kovat budjetit 1024 revisiosta, 1 000 000 objektinumerosta ja 2 000 000 raportoidusta muutoksesta tuottavat prrResourceLimitExceededin, ja rikki mennyt xref-ketju tuottaa prrMalformedRevisionChainin; molemmat päättyvät Indeterminatena, ei koskaan läpimenona

const
  StructuralRisks: TPadesRevisionRisks = [prrMalformedRevisionChain,
    prrDuplicateObjectDefinition, prrUnreferencedObjectDefinition,
    prrSignatureObjectRedefined, prrResourceLimitExceeded];

function RevisionVerdict(const R: TPadesRevisionAnalysisReport): string;
begin
  // Jotkut riskit kirjataan muuttamatta Statusta, joten testaa ne ensin
  if R.Risks * StructuralRisks <> [] then
    Exit('review: structural risk in the revision chain');
  case R.Status of
    prasNoLaterChanges: Result := 'accept: nothing was added after signing';
    prasAllowed:        Result := 'accept: every later change is permitted';
    prasDisallowed:     Result := 'reject: a change violates DocMDP or FieldMDP';
    prasSuspicious:     Result := 'reject: shadow or unconstrained content change';
    prasIndeterminate:  Result := 'review: the analyzer could not decide';
  else
    Result := 'not checked: no signatures or no original bytes';
  end;
end;

Järjestys kyseisessä portissa on tahallinen. prrDuplicateObjectDefinition lisätään riskijoukkoon laskematta Statusia itse alas, ja FieldMDP-transformi jota ei voi jäsentää vaikuttaa tilaan vasta kun lomakekenttä oikeasti muuttuu, joten portti joka katsoo pelkkää Statusia voi missata todistusaineiston jota raportti jo sisältää. Pidä mielessä myös se mitä raportti ei väitä. TPadesRevisionAnalysisReport ei sano mitään siitä onko CMS-allekirjoitus kryptografisesti kelvollinen vai ketjuntuuko allekirjoittajan sertifikaatti juureen johon luotat. Revisioanalyysi vastaa kysymykseen mitä allekirjoituksen jälkeen tapahtui, ja se kuuluu rakenteellisen ja luottamusvalidoinnin rinnalle, ei niiden tilalle

Seed-arvojen ja MDP-lukitusten kirjoittaminen allekirjoitushetkellä

Samat säännöt voi kirjoittaa allekirjoittaessa TPadesSignatureFieldOptionsin kautta, joka on FieldOptions-jäsen sekä TPadesSignOptionsissa että TPadesRemoteSignOptionsissa. PDFium voi luoda widgetin mutta ei osaa kirjoittaa /SV:tä, /Lockia, FieldMDP- tai DocMDP-transformia tai katalogin /Perms-sanakirjaa, joten komponentin oma inkrementaalinen PAdES-kirjoittaja tuottaa nämä objektit saman xref-päivityksen sisällä kuin allekirjoituksen. FieldName asettaa juurikentän nimen, RequiredSeedValues muuttuu ISO 32000-1:n §12.7.4.5:ssä kuvatun seed-value-sanakirjan /Ff-biteiksi, Reasons, LegalAttestations ja AcceptableCertificates rajoittavat mitä myöhempi allekirjoittaja voi valita, LockAction yhdessä LockFieldsin kanssa kirjoittaa epäsuoran /SigFieldLockin, ja CertificationPermission arvosta 1 arvoon 3 muuttaa allekirjoituksen sertifiointiallekirjoitukseksi. DocMDP- ja FieldMDP-transformit menevät molemmat yhteen /Reference-aulokkoon allekirjoitusarvolla, kukin /Datalla joka osoittaa katalogiin

var
  Options: TPadesSignOptions;
begin
  Options := TPadesSignOptions.Default;
  Options.CertificateThumbprint := 'A1B2C3D4E5F60718293A4B5C6D7E8F9012345678';
  Options.Reason := 'Approved for release';
  Options.FieldOptions.FieldName := 'Certification';
  Options.FieldOptions.CertificationPermission := 2;   // vain lomakkeiden täyttö ja allekirjoitus
  Options.FieldOptions.RequiredSeedValues := [psvcSubFilter, psvcDigestMethod];
  Options.FieldOptions.LockAction := pfmaInclude;      // lukitse vain nämä kentät
  SetLength(Options.FieldOptions.LockFields, 2);
  Options.FieldOptions.LockFields[0] := 'Total';
  Options.FieldOptions.LockFields[1] := 'IBAN';
  if not Pdf.SignPades('contract-certified.pdf', Options) then
    Writeln('Signing failed');
end;

Muutama yksityiskohta on helppo saada väärin jos kutoat tämän käsin. Katalogin /Perms /DocMDP:n on viitattava allekirjoitusarvon sanakirjaan, ei widget-annotaatioon, ja kirjoittaja pitää allekirjoitusarvon omana epäsuorana objektinaan juuri siksi. Olemassa oleva /Perms-sanakirja voi jo pitää /UR3-käyttöoikeuksia, joten kirjoittaja kopioi sen ja lisää /DocMDP:n sen korvaamisen sijaan, seuraten ISO 32000-1:n §12.8.4:n lupasanakirjaa. Asiakirja joka jo kantaa /DocMDP:tä kieltäytyy toisesta sertifiointiallekirjoituksesta EPadesCryptolla, samoin epäjohdonmukaiset asetukset: Include- tai Exclude-lukitus ilman kenttien nimiä, All-lukitus kenttälistalla, juridinen attestointi ei-sertifiointiallekirjoituksella tai piste juurikentän nimessä. Etäallekirjoitus lisää yhden säännön lisää, koska allekirjoittava sertifikaatti on tuntematon kun PreparePadesRemoteSignature ajetaan: CertificateRequiredin asettaminen siellä vaatii eksplisiittisen AcceptableCertificates-listan, kun taas paikallinen allekirjoitus voi palata ratkaistuun allekirjoittajan sertifikaattiin

Revisioanalyysi täydentää allekirjoitustyökalupakia korvaamatta mitään sen osaa. Aloita artikkelista PDF-allekirjoitusten ja PAdES-tasojen tarkastelu PDFium Componentilla lukeaksesi sanakirjan ja baseline-tason, katso miksi validoijat hylkäävät PAdES-allekirjoitukset rakenteellisista vioista jotka tulevat ennen mitään revisiokysymystä, ja kääri tuomio laajempaan PDF-turvallisuusriskien auditointiin JavaScript- ja upotettujen tiedostojen tarkistusten rinnalle. TPdf.AnalyzeSignatureRevisions, TPadesSignatureFieldOptions ja tässä esitelty inkrementaalinen PAdES-kirjoittaja toimitetaan PDFium Componentin mukana Delphille, C++Builderille ja Lazarukselle