Để tìm ra một PDF đã đổi gì sau khi được ký, PDFium Component cho Delphi và Lazarus cung cấp TPdf.AnalyzeSignatureRevisions, một bộ phân tích thay đổi revision hậu ký dựng lại mọi revision tăng dần từ các byte file gốc, chấm điểm mỗi thay đổi object về sau theo luật DocMDP và FieldMDP của chữ ký đó, và báo các định nghĩa object bóng dưới dạng một rủi ro riêng. Tình huống nó nhắm tới thì ai từng xử lý hợp đồng đều quen: một biểu mẫu được chứng nhận gửi đi, quay về kèm thêm hai lần save tăng dần, và mọi chữ ký vẫn verify ngon. Điều đó là đúng ý, vì một chữ ký chỉ phủ các byte của revision của chính nó. Câu hỏi thật là những lần save sau đó có được phép hay không, và một dấu kiểm xanh trên chữ ký không trả lời câu đó
Vì sao signature API của PDFium không cho thấy thay đổi sau khi ký?
Signature API của PDFium không thể cho thấy thay đổi hậu ký vì nó chỉ đọc signature dictionary: /Contents, /ByteRange, /SubFilter và giá trị quyền DocMDP. PDFium không có đồ thị revision tăng dần, không parse tham số transform FieldMDP, và không cung cấp diff cấp object giữa các revision, nên bộ phân tích trong FPdfPades.pas làm việc trực tiếp trên byte thô thay vì thế. Điều đó có một hệ quả thực dụng bạn nên thiết kế xoay quanh. TPdf.AnalyzeSignatureRevisions đọc các byte được giữ lại lúc tài liệu được nạp, không bao giờ đọc một bản sao do SaveAs sinh ra, vì một file được viết lại đã mất đúng cấu trúc revision đang được phân tích. Nếu tài liệu đến từ một nguồn progressive chưa tải xong, báo cáo trả SourceStatus = pvssIncomplete và Status = prasIndeterminate thay vì phân tích một file bị cắt cụt
Dựng lại biên revision từ startxref, xref stream và /Prev
Bộ phân tích dựng lại biên revision bằng cách lần theo mọi startxref ngược qua các bảng xref cổ điển, cross-reference stream, các entry hybrid-reference /XRefStm và chuỗi /Prev, như được định nghĩa cho incremental update trong ISO 32000-1 §7.5.6 và §7.5.8. Độ dài được phủ của mỗi chữ ký là điểm kết thúc của span ByteRange thứ hai, và bộ phân tích ánh xạ độ dài đó về revision mà section xref của nó nằm bên trong. Khi không revision nào khớp, chữ ký nhận prrCoveredRevisionNotFound và trạng thái Indeterminate. Trạng thái của mọi object sau đó được replay tới revision được phủ, và mỗi entry xref về sau được đối chiếu với trạng thái đó. Điều này quan trọng hơn nghe qua: vài writer nhắc lại toàn bộ bảng xref ở mỗi lần save tăng dần, và một entry vẫn trỏ vào cùng object không đổi bị bỏ qua thay vì bị báo là một sửa đổi. Thiếu phép đối chiếu đó, một lần điền form hợp pháp hoàn toàn sẽ chết đuối trong hàng trăm thay đổi giả
Định nghĩa bóng là ca đáng chú ý nhất. Một object body xuất hiện bên trong byte range của một revision về sau nhưng không được xref của revision đó tham chiếu thì vô hình với một viewer bình thường, mà nó lại đúng là kiểu dàn sẵn mà shadow attack dựa vào: nội dung ẩn được trồng trước hay sau khi ký rồi sau đó kích hoạt bằng cách lật một tham chiếu. AnalyzePadesSignatureRevisionsBytes ghi một object kiểu đó như một thay đổi không chính thức với IsAuthoritative = False, chấm nó prdSuspicious bất kể mức quyền, và thêm prrUnreferencedObjectDefinition vào tập rủi ro. Hai rủi ro liên quan phủ các thủ thuật cấu trúc khác: prrDuplicateObjectDefinition bắn khi một section xref liệt kê cùng object quá một lần, và prrSignatureObjectRedefined bắn khi một revision về sau định nghĩa lại một object chữ ký đã có
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;
DocMDP và FieldMDP được thi hành cho mỗi chữ ký ra sao?
DocMDP và FieldMDP được thi hành tách rời cho mỗi chữ ký, tại revision được phủ của chính chữ ký đó, nên một chữ ký chứng nhận và một chữ ký phê duyệt về sau trong cùng một file có thể đưa ra phán quyết khác nhau về cùng một chỉnh sửa. Mọi object về sau trước hết được xếp loại vào một TPadesRevisionChangeKind từ các entry /Type, /Subtype và /FT của nó và từ vai trò nó đóng trong các đồ thị page, form, annotation và DSS. Bất cứ thứ gì mang /JavaScript, /JS, /Launch, /OpenAction, /AA, /RichMedia hay /EmbeddedFile trở thành prckActiveContent. Phán quyết sau đó đi theo ISO 32000-1 §12.8.2.2: với P=1, mọi thứ ngoài dữ liệu cross-reference và vật liệu validation đều bị cấm; P=2 cho phép điền form và ký thêm nhưng chối thay đổi annotation; P=3 còn cho phép annotation cả. Nội dung page, cấu trúc tài liệu, metadata, active content và object bị xóa bị cấm dưới mọi mức DocMDP, và được chấm prdSuspicious khi chữ ký chẳng mang DocMDP nào, vì một chữ ký phê duyệt không cấm gì một cách chính thức nhưng người đọc không còn thấy những gì đã được ký
FieldMDP, từ ISO 32000-1 §12.8.2.4, siết thêm phán quyết về form field. pfmaAll khóa mọi field, pfmaInclude chỉ khóa các field được liệt kê, và pfmaExclude khóa mọi thứ trừ các field được liệt kê. Để áp Include hay Exclude, bộ phân tích phân giải mỗi field bị đổi về tên đủ qualified của nó qua chuỗi /Parent và so với danh sách khóa bằng khớp chính xác, nên hãy liệt kê các tên field terminal thay vì trông chờ một tên cha che phủ các con của nó. Khi một tên không phân giải được hay transform dùng một action parser không nhận diện, thay đổi trở thành prdIndeterminate và prrFieldMdpUnresolved được raise. Các phán quyết từng thay đổi sau đó cuộn lên theo kiểu tệ-trước, với Suspicious xếp trên Disallowed, Disallowed trên Indeterminate, và Indeterminate trên Allowed, nên một object bóng outweigh bao nhiêu cập nhật field hợp pháp cũng được
Vì sao vài thay đổi trả về Indeterminate thay vì an toàn?
Thay đổi trả về Indeterminate bất cứ khi nào bộ phân tích không chứng minh nổi một thay đổi là được phép, vì trong một phép kiểm chữ ký, thứ không biết không bao giờ được báo là được phép. Một ca phổ biến được xử lý chính xác thay vì thế: long-term validation thêm một /DSS và viết lại catalog, thứ nếu không sẽ bị tính là thay đổi cấu trúc dưới P=1. Bộ phân tích gỡ /DSS và /Extensions khỏi hai dictionary catalog cũ mới rồi so phần còn lại; khi chẳng còn gì khác lệch, phép viết lại được coi là một cập nhật vật liệu validation và được cho phép, nên việc nâng cấp B-LT và B-LTA không làm vỡ một chữ ký chứng nhận. Các khoảng trống khác được để hở một cách chủ ý. Các entry Type-2 trong một cross-reference stream trỏ vào các object stream nén, và bộ phân tích không trải object stream bên trong biên an ninh này, nên những thay đổi đó lộ diện dưới dạng prckCompressedObject với prrCompressedObjectUnresolved, bị cấm dưới P=1 và Indeterminate với các trường hợp khác. Các ngân sách cứng 1024 revision, 1.000.000 số object và 2.000.000 thay đổi được báo sinh prrResourceLimitExceeded, và một chuỗi xref vỡ sinh prrMalformedRevisionChain; cả hai kết thúc bằng Indeterminate, không bao giờ là pass
const
StructuralRisks: TPadesRevisionRisks = [prrMalformedRevisionChain,
prrDuplicateObjectDefinition, prrUnreferencedObjectDefinition,
prrSignatureObjectRedefined, prrResourceLimitExceeded];
function RevisionVerdict(const R: TPadesRevisionAnalysisReport): string;
begin
// Một số risk được ghi mà không đổi Status, nên hãy thử chúng trước
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;
Thứ tự trong cổng đó là có chủ ý. prrDuplicateObjectDefinition được thêm vào tập rủi ro mà tự thân không hạ Status, và một transform FieldMDP không parse được chỉ ảnh hưởng trạng thái khi một form field thật sự đổi, nên một cổng chỉ nhìn Status có thể bỏ lỡ bằng chứng mà báo cáo đã chứa. Cũng hãy nhớ những gì báo cáo không khẳng định. TPadesRevisionAnalysisReport chẳng nói gì về việc chữ ký CMS có hợp lệ về mặt mật mã hay chứng chỉ người ký có chain tới một root bạn tin hay không. Phân tích revision trả lời câu hỏi chuyện gì xảy ra sau khi ký, và nó nằm cạnh validation cấu trúc và tin cậy, chứ không thay thế chúng
Ghi seed value và khóa MDP ngay lúc ký
Cùng các luật đó có thể được soạn thảo khi ký qua TPadesSignatureFieldOptions, là member FieldOptions của cả TPadesSignOptions lẫn TPadesRemoteSignOptions. PDFium tạo được một widget nhưng không ghi được /SV, /Lock, một transform FieldMDP hay DocMDP, hay dictionary /Perms của catalog, nên PAdES writer tăng dần của riêng component sinh các object đó bên trong cùng một cập nhật xref với chữ ký. FieldName đặt tên field gốc, RequiredSeedValues thành các bit /Ff của seed-value dictionary mô tả trong ISO 32000-1 §12.7.4.5, Reasons, LegalAttestations và AcceptableCertificates ràng buộc điều một người ký về sau được chọn, LockAction cùng LockFields ghi một /SigFieldLock gián tiếp, và CertificationPermission từ 1 tới 3 biến chữ ký thành một chữ ký chứng nhận. Cả hai transform DocMDP và FieldMDP đi vào một mảng /Reference trên signature value, mỗi cái với /Data trỏ vào catalog
var
Options: TPadesSignOptions;
begin
Options := TPadesSignOptions.Default;
Options.CertificateThumbprint := 'A1B2C3D4E5F60718293A4B5C6D7E8F9012345678';
Options.Reason := 'Approved for release';
Options.FieldOptions.FieldName := 'Certification';
Options.FieldOptions.CertificationPermission := 2; // chỉ điền form và ký
Options.FieldOptions.RequiredSeedValues := [psvcSubFilter, psvcDigestMethod];
Options.FieldOptions.LockAction := pfmaInclude; // khóa chỉ những trường này
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;
Vài chi tiết dễ sai nếu bạn tự gõ tay chỗ này. Catalog /Perms /DocMDP phải tham chiếu signature value dictionary, chứ không phải widget annotation, và writer giữ signature value như một object gián tiếp riêng vì đúng lý do đó. Một dictionary /Perms hiện có có thể đã giữ các usage right /UR3, nên writer sao chép nó rồi chèn /DocMDP thay vì thay thế, theo đúng permissions dictionary trong ISO 32000-1 §12.8.4. Một tài liệu đã mang /DocMDP từ chối một chữ ký chứng nhận thứ hai bằng EPadesCrypto, và các option mâu thuẫn cũng vậy: một khóa Include hay Exclude mà không có tên field, một khóa All mà lại kèm danh sách field, một legal attestation trên một chữ ký không chứng nhận, hay một dấu chấm trong tên field gốc. Remote signing thêm một luật nữa, vì chứng chỉ ký chưa được biết khi PreparePadesRemoteSignature chạy: đặt CertificateRequired ở đó đòi một danh sách AcceptableCertificates tường minh, trong khi ký local có thể fallback về chứng chỉ người ký đã phân giải
Phân tích revision hoàn thiện hộp công cụ chữ ký chứ không thay thế bất kỳ phần nào của nó. Hãy bắt đầu với soi chữ ký PDF và các mức PAdES với PDFium Component để đọc dictionary và mức baseline, xem vì sao validator chối chữ ký PAdES cho các thất bại cấu trúc đến trước bất kỳ câu hỏi revision nào, rồi gấp phán quyết vào một audit rủi ro an ninh PDF rộng hơn cùng các phép kiểm JavaScript và embedded file. TPdf.AnalyzeSignatureRevisions, TPadesSignatureFieldOptions và PAdES writer tăng dần trình bày ở trên đi kèm PDFium Component cho Delphi, C++Builder và Lazarus