Aby ste zistili, čo sa v PDF zmenilo po podpise, poskytuje PDFium Component pre Delphi a Lazarus TPdf.AnalyzeSignatureRevisions, analyzátor zmien revízií po podpise, ktorý prestaví každú inkrementálnu revíziu z pôvodných bajtov súboru, ohodnotí každú neskoršiu zmenu objektu podľa pravidiel DocMDP a FieldMDP toho podpisu a hlási shadow definície objektov ako samostatné riziko. Situácia, na ktorú cielí, je povedomá každému, kto manipuluje so zmluvami: certifikovaný formulár odíde, vráti sa s ďalšími dvomi inkrementálnymi uložkami a každý podpis stále verifikuje. To sa očakáva, pretože podpis pokrýva len bajty vlastnej revízie. Skutočná otázka znie, či boli tie neskoršie uložky povolené, a zelená fajka na podpise na ňu neodpovedá
Prečo PDFium signature API neukáže, čo sa zmenilo po podpise?
PDFium signature API nedokáže ukázať zmeny po podpise, pretože číta len slovník podpisu: /Contents, /ByteRange, /SubFilter a hodnotu povolení DocMDP. PDFium nemá graf inkrementálnych revízií, neparsovať transform parametre FieldMDP nevie a nenabíza žiadny diff na úrovni objektov medzi revíziami, takže analyzátor v FPdfPades.pas pracuje namiesto toho priamo na surových bajtoch. To má praktický dôsledok, ktorý treba zohľadniť pri návrhu aplikácie. TPdf.AnalyzeSignatureRevisions číta bajty uschované pri načítaní dokumentu, nikdy nie kópiu vyrobenú SaveAs, pretože prepísaný súbor stratil práve tú revíznu štruktúru, ktorá sa analyzuje. Ak dokument prišiel z progresívneho zdroja, ktorý nedokončil sťahovanie, vráti report SourceStatus = pvssIncomplete a Status = prasIndeterminate namiesto analýzy useknutého súboru
Prestavba hraníc revízií zo startxref, xref streamov a /Prev
Analyzátor prestaví hranice revízií nasledovaním každého startxref späť cez klasické xref tabuľky, cross-reference streamy, hybrid-reference záznamy /XRefStm a reťaz /Prev, ako je definované pre inkrementálne aktualizácie v ISO 32000-1 §7.5.6 a §7.5.8. Pokrytá dĺžka každého podpisu je koniec jeho druhého spanu ByteRange a analyzátor namapuje tú dĺžku na revíziu, ktorej xref sekcia ju zahŕňa. Keď žiadna revízia nesedí, dostane podpis prrCoveredRevisionNotFound a status Indeterminate. Stav každého objektu sa potom prehrá až po pokrytú revíziu a každý neskorší xref záznam sa porovná s tým stavom. To má väčší význam, než znie: niektoré writery zopakujú kompletnú xref tabuľku pri každej inkrementálnej uložke a záznam, ktorý stále ukazuje na ten istý nezmenený objekt, sa preskočí namiesto toho, aby bol hlásený ako modifikácia. Bez toho porovnania by sa úplne legálne vyplnenie formulára utopilo v stovkách falošných zmien
Shadow definície sú prípad, ktorý si zaslúži najväčšiu pozornosť. Telo objektu, ktoré sa objaví vnútri bajtového range neskoršej revízie, ale nie je odkazované xrefom tej revízie, je neviditeľné pre obyčajný viewer, a pritom je to presne ten druh prípravy, na ktorý sa shadow útoky spoliehajú: skrytý obsah sa zasadí pred podpisom alebo po ňom a neskôr sa aktivuje preklopením referencie. AnalyzePadesSignatureRevisionsBytes zaznamená taký objekt ako neautoritatívnu zmenu s IsAuthoritative = False, ohodnotí ho prdSuspicious bez ohľadu na úroveň povolení a pridá prrUnreferencedObjectDefinition do sady rizík. Dve súvisiace riziká pokrývajú ďalšie štrukturálne triky: prrDuplicateObjectDefinition vystrelí, keď jedna xref sekcia vypíše ten istý objekt viackrát, a prrSignatureObjectRedefined vystrelí, keď neskoršia revízia nanovo definuje existujúci objekt podpisu
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;
Ako sa DocMDP a FieldMDP vynucujú pre každý podpis?
DocMDP a FieldMDP sa vynucujú oddelene pre každý podpis, na vlastnej pokrytej revízii toho podpisu, takže certifikačný podpis a neskorší schvaľovací podpis v tom istom súbore môžu dospieť k odlišným verdiktom o tej istej úprave. Každý neskorší objekt sa najprv klasifikuje do TPadesRevisionChangeKind podľa svojich záznamov /Type, /Subtype a /FT a podľa role, ktorú hrá v grafoch stránky, formulára, anotácií a DSS. Čokoľvek nesúce /JavaScript, /JS, /Launch, /OpenAction, /AA, /RichMedia alebo /EmbeddedFile sa stane prckActiveContent. Rozhodnutie potom nasleduje ISO 32000-1 §12.8.2.2: s P=1 je zakázané všetko okrem cross-reference dát a validačného materiálu; P=2 povoľuje vyplňovanie formulárov a ďalšie podpisy, ale odmieta zmeny anotácií; P=3 povoľuje aj anotácie. Obsah stránky, štruktúra dokumentu, metadáta, aktívny obsah a zmazané objekty sú zakázané pod akoukoľvek úrovňou DocMDP a ohodnotia sa prdSuspicious, keď podpis nenesie žiadne DocMDP, keďže schvaľovací podpis formálne nezakazuje nič, ale čitateľ už nevidí, čo bolo podpísané
FieldMDP z ISO 32000-1 §12.8.2.4 ešte zúži rozhodnutie o formulárových poliach. pfmaAll zamkne každé pole, pfmaInclude zamkne len vypísané polia a pfmaExclude zamkne všetko okrem vypísaných polí. Na aplikovanie Include alebo Exclude dorieši analyzátor každé zmenené pole na jeho plne kvalifikované meno cez reťaz /Parent a porovná ho so zoznamom zámkov exact matchom, takže vypisujte terminálne mená polí, namiesto očakávania, že meno rodiča pokryje deti. Keď sa meno nedá doriešiť alebo transform používa akciu, ktorú parser nerozpozná, zmena sa stane prdIndeterminate a vyvolá sa prrFieldMdpUnresolved. Rozhodnutia na zmenu sa potom zhrnú najhoršie prvé, so Suspicious nad Disallowed, Disallowed nad Indeterminate a Indeterminate nad Allowed, takže jeden shadow objekt prevaží ľubovoľný počet legitímnych aktualizácií polí
Prečo sa niektoré zmeny vracajú ako Indeterminate namiesto bezpečné?
Zmeny sa vracajú ako Indeterminate vždy, keď analyzátor nedokáže dokázať, že zmena je povolená, pretože v kontrole podpisu sa neznáme nikdy nesmie hlásiť ako povolené. Jeden bežný prípad sa spracúva presne namiesto toho: dlhodobá validácia pridá /DSS a prepíše katalóg, čo by inak počítalo ako štrukturálna zmena pod P=1. Analyzátor odstráni /DSS a /Extensions zo starého aj nového katalógového slovníka a porovná zvyšok; keď sa nič iné nelíši, prepis sa berie ako aktualizácia validačného materiálu a povolí sa, takže augmentácia B-LT a B-LTA nerozbije certifikačný podpis. Ostatné medzery zostávajú otvorené zámerne. Záznamy Type-2 v cross-reference streame ukazujú do komprimovaných object streamov a analyzátor neexpanduje object streamy vnútri tejto bezpečnostnej hranice, takže tie zmeny vystúpia ako prckCompressedObject s prrCompressedObjectUnresolved, zakázané pod P=1 a inak Indeterminate. Tvrdé rozpočty 1024 revízií, 1 000 000 objektových čísel a 2 000 000 hlásených zmien produkujú prrResourceLimitExceeded a rozbitý xref reťaz produkuje prrMalformedRevisionChain; oboje končí ako Indeterminate, nikdy ako pass
const
StructuralRisks: TPadesRevisionRisks = [prrMalformedRevisionChain,
prrDuplicateObjectDefinition, prrUnreferencedObjectDefinition,
prrSignatureObjectRedefined, prrResourceLimitExceeded];
function RevisionVerdict(const R: TPadesRevisionAnalysisReport): string;
begin
// Niektoré riziká sa zaznamenajú bez zmeny Status, testujte ich preto najprv
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;
Poradie v tej bráne je zámerné. prrDuplicateObjectDefinition sa pridá do sady rizík bez toho, že by sám znížil Status, a FieldMDP transform, ktorý sa nedá sparsovať, ovplyvní status až vtedy, keď sa reálne zmení formulárové pole, takže brána pozerajúca len na Status môže prehliadnuť dôkaz, ktorý report už obsahuje. Majte na pamäti aj to, čo report netvrdí. TPadesRevisionAnalysisReport nič nehovorí o tom, či je CMS podpis kryptograficky platný, alebo či certifikát podpisujúceho reťazí ku koreňu, ktorému dôverujete. Analýza revízií odpovedá na otázku, čo sa stalo po podpise, a patrí vedľa štrukturálnej a trust validácie, nie na ich miesto
Zápis seed hodnôt a zámkov MDP pri podpisovaní
Rovnaké pravidlá sa dajú napísať pri podpisovaní cez TPadesSignatureFieldOptions, ktorý je člen FieldOptions oboch TPadesSignOptions a TPadesRemoteSignOptions. PDFium dokáže vytvoriť widget, ale nedokáže zapísať /SV, /Lock, transform FieldMDP alebo DocMDP, ani katalógový slovník /Perms, takže vlastný inkrementálny PAdES writer komponentu produkuje tieto objekty vnútri tej istej xref aktualizácie ako podpis. FieldName nastaví root meno poľa, RequiredSeedValues sa stane bitmi /Ff slovníka seed hodnôt popísaného v ISO 32000-1 §12.7.4.5, Reasons, LegalAttestations a AcceptableCertificates obmedzia, čo si neskorší podpisujúci môže vybrať, LockAction s LockFields zapisuje nepriamy /SigFieldLock a CertificationPermission od 1 po 3 urobí z podpisu certifikačný podpis. Transformy DocMDP aj FieldMDP idú obe do jedného poľa /Reference na hodnote podpisu, každá s /Data ukazujúcim na katalóg
var
Options: TPadesSignOptions;
begin
Options := TPadesSignOptions.Default;
Options.CertificateThumbprint := 'A1B2C3D4E5F60718293A4B5C6D7E8F9012345678';
Options.Reason := 'Approved for release';
Options.FieldOptions.FieldName := 'Certification';
Options.FieldOptions.CertificationPermission := 2; // len vyplňovanie formulárov a podpisovanie
Options.FieldOptions.RequiredSeedValues := [psvcSubFilter, psvcDigestMethod];
Options.FieldOptions.LockAction := pfmaInclude; // zamknúť len tieto polia
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;
Pár detailov je ľahké pokaziť, ak si toto píšete po svojom. Katalógové /Perms /DocMDP musí odkazovať na slovník hodnoty podpisu, nie na widget anotáciu, a writer preto drží hodnotu podpisu ako vlastný nepriamy objekt. Existujúci slovník /Perms môže už držať usage práva /UR3, takže writer ho skopíruje a vloží /DocMDP namiesto nahradenia, podľa slovníka povolení v ISO 32000-1 §12.8.4. Dokument, ktorý už nesie /DocMDP, odmietne druhý certifikačný podpis s EPadesCrypto a rovnako nekonzistentné options: zámok Include alebo Exclude bez mien polí, zámok All so zoznamom polí, legal attestation na necertifikačnom podpise alebo bodka v root mene poľa. Remote podpisovanie pridáva jedno pravidlo navyše, pretože podpisujúci certifikát je neznámy, keď beží PreparePadesRemoteSignature: nastavenie CertificateRequired tam vyžaduje explicitný zoznam AcceptableCertificates, kým lokálne podpisovanie sa môže chytiť doriešeného certifikátu podpisujúceho
Analýza revízií dotvára toolbox podpisov namiesto toho, aby nahradila ktorúkoľvek jeho časť. Začnite prezeraním PDF podpisov a úrovní PAdES s PDFium Component, aby ste prečítali slovník a baseline úroveň, pozrite prečo validátory odmietajú PAdES podpisy kvôli štrukturálnym zlyhaniam, ktoré prichádzajú skôr než akákoľvek revízna otázka, a zapojte verdikt do širšieho auditu PDF bezpečnostných rizík vedľa kontrol JavaScriptu a embedded súborov. TPdf.AnalyzeSignatureRevisions, TPadesSignatureFieldOptions a tu ukázaný inkrementálny PAdES writer vozí PDFium Component pre Delphi, C++Builder a Lazarus