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
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
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ä
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