Abychom zjistili, co se v PDF změnilo po podepsání, poskytuje PDFium Component pro Delphi a Lazarus TPdf.AnalyzeSignatureRevisions, analyzátor změn revizí po podpisu, který znovu sestaví každou inkrementální revizi z původních bytů souboru, ohodnotí každou pozdější objektovou změnu podle DocMDP a FieldMDP pravidel toho podpisu a hlásí shadow definice objektů jako samostatné riziko. Situace, na kterou cílí, je povědomá každému, kdo zachází se smlouvami: certifikovaný formulář odejde, vrátí se se dvěma dalšími inkrementálními úložbami a každý podpis stále projde verifikací. To je očekávané, protože podpis pokrývá jen byty své vlastní revize. Skutečná otázka je, zda ty pozdější úložby byly povolené — a zelená fajfka na podpisu na ni neodpoví
Proč signature API PDFium neukáže, co se změnilo po podpisu?
Signature API PDFium nedokáže ukázat změny po podpisu, protože čte jen slovník podpisu: /Contents, /ByteRange, /SubFilter a hodnotu oprávnění DocMDP. PDFium nemá graf inkrementálních revizí, neparsuje transform parametry FieldMDP a nenabízí žádný diff mezi revizemi na úrovni objektů, takže analyzátor v FPdfPades.pas pracuje rovnou na syrových bytech. Má to praktický důsledek, kolem kterého byste měli navrhovat. TPdf.AnalyzeSignatureRevisions čte byty uchované při načtení dokumentu, nikdy kopii vyprodukovanou SaveAs, protože přepsaný soubor přišel právě o revizní strukturu, která se analyzuje. Pokud dokument přišel z progresivního zdroje, který nedoběhl se stahováním, vrátí report SourceStatus = pvssIncomplete a Status = prasIndeterminate místo analýzy useknutého souboru
Sestavení hranic revizí z startxref, xref streamů a /Prev
Analyzátor sestaví hranice revizí tak, že sleduje každý startxref zpět skrz klasické xref tabulky, cross-reference streamy, hybridní položky /XRefStm a řetěz /Prev, jak jsou definovány pro inkrementální update v ISO 32000-1 §7.5.6 a §7.5.8. Pokrytá délka každého podpisu je konec jeho druhého rozpětí ByteRange a analyzátor namapuje tu délku na revizi, do jejíž xref sekce spadá. Když žádná revize nesedí, podpis dostane prrCoveredRevisionNotFound a status Indeterminate. Stav každého objektu se pak přehraje až do pokryté revize a každá pozdější xref položka se porovná s tím stavem. Tohle má větší váhu, než zní: někteří writery znovu zapisují kompletní xref tabulku při každé inkrementální úložbě a položka, která dál míří na tentýž nezměněný objekt, se přeskočí místo nahlášení jako modifikace. Bez toho porovnání by zcela legální vyplnění formuláře se utopilo ve stovkách falešných změn
Shadow definice jsou případ, který si zaslouží nejvíc pozornosti. Tělo objektu, které se objeví uvnitř bajtového rozpětí pozdější revize, ale není odkazované xrefem té revize, je pro obyčejný prohlížeč neviditelné — a je to přesně ten druh přípravy, na kterém stojí shadow útoky: skrytý obsah se zasadí před nebo po podpisu a později se aktivuje překlopením reference. AnalyzePadesSignatureRevisionsBytes zaznamená takový objekt jako non-authoritative změnu s IsAuthoritative = False, ohodnotí ji prdSuspicious bez ohledu na úroveň oprávnění a přidá prrUnreferencedObjectDefinition do sady rizik. Dvě související rizika pokrývají další strukturální triky: prrDuplicateObjectDefinition vystřelí, když jedna xref sekce vyjmenuje tentýž objekt víc než jednou, a prrSignatureObjectRedefined vystřelí, když pozdější revize předefinuje existující 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;
Jak se DocMDP a FieldMDP vynucují pro každý podpis?
DocMDP a FieldMDP se vynucují odděleně pro každý podpis, na jeho vlastní pokryté revizi, takže certifikační podpis a pozdější schvalovací podpis v témže souboru mohou dospět k různým verdiktům nad tutéž editaci. Každý pozdější objekt se nejdřív klasifikuje do TPadesRevisionChangeKind podle svých položek /Type, /Subtype a /FT a podle role, kterou hraje v grafech stránek, formulářů, anotací a DSS. Cokoli nesoucí /JavaScript, /JS, /Launch, /OpenAction, /AA, /RichMedia nebo /EmbeddedFile se stane prckActiveContent. Rozhodnutí pak následuje ISO 32000-1 §12.8.2.2: s P=1 je zakázáno všechno kromě cross-reference dat a validačního materiálu; P=2 povoluje vyplňování formulářů a další podpisy, ale odmítá změny anotací; P=3 povoluje i anotace. Obsah stránek, struktura dokumentu, metadata, aktivní obsah a smazané objekty jsou zakázané pod jakoukoli úrovní DocMDP a ohodnotí se prdSuspicious, když podpis nenese DocMDP vůbec, protože schvalovací podpis formálně nezakazuje nic, ale čtenář už nevidí, co bylo podepsáno
FieldMDP z ISO 32000-1 §12.8.2.4 zužuje rozhodnutí o polích formuláře dál. pfmaAll zamkne každé pole, pfmaInclude zamkne jen vyjmenovaná pole a pfmaExclude zamkne všechno kromě vyjmenovaných. Aby se Include nebo Exclude aplikoval, rozliší analyzátor každé změněné pole na jeho plně kvalifikované jméno přes řetěz /Parent a porovná ho se seznamem zámků exaktní shodou, takže vyjmenovávejte terminální názvy polí, místo abyste čekali, že rodičovský název pokryje děti. Když se název rozlišit nepodaří nebo transform používá akci, kterou parser nepozná, změna se stane prdIndeterminate a vyvolá se prrFieldMdpUnresolved. Rozhodnutí per změna se pak sbalí nejhorší-dřív, se Suspicious nad Disallowed, Disallowed nad Indeterminate a Indeterminate nad Allowed, takže jeden shadow objekt přebije jakýkoli počet legitimních aktualizací polí
Proč některé změny vychází jako Indeterminate místo bezpečné?
Změny vychází jako Indeterminate vždy, když analyzátor nedokáže prokázat, že změna je povolená, protože ve verifikaci podpisu se neznámé nikdy nesmí hlásit jako povolené. Jeden obvyklý případ se řeší přesně naopak: long-term validace přidá /DSS a přepíše katalog, což by jinak pod P=1 počítalo jako strukturální změna. Analyzátor odřízne /DSS a /Extensions ze starého i nového katalogového slovníku a porovná zbytek; když se nic dalšího neliší, přepis se bere jako aktualizace validačního materiálu a povolí se, takže augmentace B-LT a B-LTA nerozbije certifikační podpis. Jiné mezery zůstávají záměrně otevřené. Položky Type-2 v cross-reference streamu míří do komprimovaných object streamů a analyzátor object streamy uvnitř této bezpečnostní hranice neexpanduje, takže tyhle změny vyjdou jako prckCompressedObject s prrCompressedObjectUnresolved, zakázané pod P=1 a jinde Indeterminate. Tvrdé rozpočty 1024 revizí, 1 000 000 objektových čísel a 2 000 000 hlášených změn produkují prrResourceLimitExceeded a rozbitý řetěz xref produkuje prrMalformedRevisionChain; obojí končí jako Indeterminate, nikdy jako průchod
const
StructuralRisks: TPadesRevisionRisks = [prrMalformedRevisionChain,
prrDuplicateObjectDefinition, prrUnreferencedObjectDefinition,
prrSignatureObjectRedefined, prrResourceLimitExceeded];
function RevisionVerdict(const R: TPadesRevisionAnalysisReport): string;
begin
// Některá rizika se zaznamenají beze změny Status, takže je testujte nejdřív
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;
Pořadí v té bráně je záměrné. prrDuplicateObjectDefinition se do sady rizik přidá bez toho, že by sám snižoval Status, a FieldMDP transform, který nejde parsovat, ovlivní status až ve chvíli, kdy se reálně změní pole formuláře, takže brána koukající jen na Status může přehlédnout důkaz, který report už obsahuje. Mějte na paměti i to, co report netvrdí. TPadesRevisionAnalysisReport neříká nic o tom, zda je CMS podpis kryptograficky validní, nebo zda certifikát podepisujícího řetězí ke kořeni, kterému věříte. Analýza revizí odpovídá na otázku, co se stalo po podpisu, a patří vedle strukturální a trust validace, ne na její místo
Zápis seed hodnot a zámků MDP při podepisování
Tytéž pravidla lze zapsat při podepisování přes TPadesSignatureFieldOptions, což je člen FieldOptions jak TPadesSignOptions, tak TPadesRemoteSignOptions. PDFium umí vytvořit widget, ale nedokáže zapsat /SV, /Lock, transform FieldMDP nebo DocMDP ani katalogový slovník /Perms, takže vlastní inkrementální PAdES writer komponenty produkuje tyhle objekty uvnitř téhož xref updatu jako podpis. FieldName nastaví kořenový název pole, RequiredSeedValues se stane bity /Ff slovníku seed hodnot popsaného v ISO 32000-1 §12.7.4.5, Reasons, LegalAttestations a AcceptableCertificates omezí, co si pozdější podepisující může vybrat, LockAction s LockFields zapíše nepřímý /SigFieldLock a CertificationPermission od 1 do 3 udělá z podpisu certifikační podpis. Transformy DocMDP a FieldMDP míří obě do jednoho pole /Reference na hodnotě podpisu, každá s /Data mířícím na katalog
var
Options: TPadesSignOptions;
begin
Options := TPadesSignOptions.Default;
Options.CertificateThumbprint := 'A1B2C3D4E5F60718293A4B5C6D7E8F9012345678';
Options.Reason := 'Approved for release';
Options.FieldOptions.FieldName := 'Certification';
Options.FieldOptions.CertificationPermission := 2; // jen vyplňování formuláře a podepisování
Options.FieldOptions.RequiredSeedValues := [psvcSubFilter, psvcDigestMethod];
Options.FieldOptions.LockAction := pfmaInclude; // zamkne jen tato pole
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 detailů se snadno pokazí, když tohle stavíte ručně. Katalogový /Perms /DocMDP musí odkazovat na slovník hodnoty podpisu, ne na widget anotaci, a writer proto drží hodnotu podpisu jako vlastní nepřímý objekt. Existující slovník /Perms už může nést usage rights /UR3, takže writer ho zkopíruje a vloží /DocMDP místo výměny, dle slovníku oprávnění v ISO 32000-1 §12.8.4. Dokument, který už nese /DocMDP, odmítne druhý certifikační podpis s EPadesCrypto a odmítnou i nekonzistentní volby: zámek Include nebo Exclude bez názvů polí, zámek All se seznamem polí, legal attestation na necertifikačním podpisu nebo tečka v kořenovém názvu pole. Vzdálené podepisování přidává další pravidlo, protože podepisující certifikát není známý, když běží PreparePadesRemoteSignature: nastavení CertificateRequired tam vyžaduje explicitní seznam AcceptableCertificates, zatímco lokální podepisování může jako zálohu použít rozlišený certifikát podepisujícího
Analýza revizí doplňuje sadu nástrojů kolem podpisů, místo aby cokoli z ní nahradila. Začněte článkem o prohlížení PDF podpisů a úrovní PAdES s PDFium Component, abyste přečetli slovník a baseline úroveň, podívejte se na proč validátory odmítají PAdES podpisy kvůli strukturálním selháním, která přicházejí dřív než jakákoli otázka revizí, a zapojte verdikt do širšího auditu PDF bezpečnostních rizik vedle kontrol JavaScriptu a embedded souborů. TPdf.AnalyzeSignatureRevisions, TPadesSignatureFieldOptions a inkrementální PAdES writer ukázaný tady dodává PDFium Component pro Delphi, C++Builder a Lazarus