Một PDF đã ký mà thay đổi sau khi ký không tự động là hỏng. ISO 32000-1 cho phép cập nhật gia tăng trên một chữ ký, và chỉ một số trong đó phá vỡ chính sách mà người ký đặt ra. HotPDF Component cho Delphi và C++Builder trả lời câu hỏi đó bằng AnalyzeLoadedSignatureRevisions, hàm này phân loại mọi revision sau khi ký và chấm điểm nó theo DocMDP và FieldMDP. Tình huống này quen thuộc với bất kỳ ai làm phần mềm hợp đồng: khách hàng của bạn ký một thỏa thuận mua bán, gửi đi, và nhận lại với một trang phụ lục được đính kèm. Trình đọc hiện thanh vàng nói rằng chữ ký vẫn nguyên vẹn nhưng tài liệu đã bị thay đổi kể từ khi ký, và không ai trong phòng có thể nói đó là một quy trình đồng ký bình thường hay ai đó đang âm thầm sửa một hợp đồng đã ký
Điều gì tính là một thay đổi hợp lệ sau khi ký?
Một thay đổi hợp lệ khi phạm trù ngữ nghĩa của nó nằm trong quyền hạn mà chữ ký chứng nhận đã khai báo. ISO 32000-1 §12.8.2.2 định nghĩa phép biến đổi DocMDP với giá trị /P là 1, 2 hoặc 3: 1 không cho phép thay đổi nào, 2 cho phép điền biểu mẫu và ký, 3 cho phép điền biểu mẫu, ký và chú thích. HotPDF phơi bày những giá trị đó dưới dạng THPDFDocMDPPermission gồm dmpNoChanges, dmpFormFillAndSign và dmpFormFillSignAndAnnotate, với dmpNone dành cho các kết quả thanh tra không mang phép biến đổi DocMDP nào cả
Các phạm trù được sắp thứ tự, và trật tự đó là động cơ của toàn bộ phép kiểm tra. THPDFRevisionModificationLevel chạy rmlNone, rmlLongTermValidation, rmlFormFillAndSign, rmlAnnotations, rmlOther, được sắp xếp có chủ đích sao cho một số thứ tự lớn hơn không bao giờ ít hạn chế hơn. Một tài liệu hoàn chỉnh quy về mức tối đa quan sát được trên mọi revision sau chữ ký, và phép so sánh DocMDP trở thành một phép kiểm tra số nguyên duy nhất. Có một sắc thái quan trọng cần biết sớm: ở dmpNoChanges, phép phân tích vẫn chấp nhận rmlLongTermValidation. Thêm tài liệu xác thực DSS và VRI hoặc một document timestamp vào một file đã được chứng nhận là bảo trì chữ ký, không phải sửa đổi tài liệu, và coi đó là vi phạm sẽ phá vỡ mọi quy trình lưu trữ dài hạn đang tồn tại
HotPDF xây lại chuỗi revision như thế nào?
Về mặt cấu trúc, không phải theo phương pháp phỏng đoán. Theo ISO 32000-1 §7.5.6, một cập nhật gia tăng nối thêm một đoạn cross-reference mới mà /Prev của nó trỏ về đoạn trước đó, nên HotPDF đọc startxref từ cuối file, phân tích đoạn ở đó, theo /Prev lùi lại và lặp lại, trả về các đoạn theo thứ tự cũ nhất trước. Hai giới hạn an toàn nằm trong vòng lặp đó và cả hai đều đáng biết khi phân loại một file bị lỗi: một /Prev trỏ tới một offset đã được ghé thăm sẽ dừng bước duyệt với một chẩn đoán vòng lặp tường minh thay vì quay vòng vô hạn, và một chuỗi dài hơn một nghìn revision bị từ chối thẳng. Cả hai đều hiện ra trong Analysis.Issue với hàm trả về False, và không nên bỏ qua điều nào, vì một /Prev tuần hoàn là dấu hiệu của một file bị hỏng hoặc thù địch chứ không phải một file bất thường
Bốn dạng lịch sử xuất hiện trong tài liệu thực tế và cả bốn đều được xử lý: bảng xref truyền thống phân tích theo từng dòng, cross-reference stream được giải nén và giải mã qua các trường /W và /Index của nó, file hybrid-reference mà trailer truyền thống của nó mang một khóa /XRefStm được phân tích và gộp vào cùng một revision (trường hợp trình sinh của Office, được nói trong bài về hybrid cross-reference stream), và các object nằm trong một container ObjStm, quan trọng vì một bản cập nhật hiện đại thường đặt dictionary đã thay đổi vào một stream nén thay vì ghi trực tiếp, như mô tả trong bài về object stream và cập nhật gia tăng. Chữ ký làm mốc phân tách: /ByteRange[2] + /ByteRange[3] trở thành SignedRevisionLength, và mọi đoạn tại hoặc sau offset đó là sau khi ký. Việc dải byte đó có còn băm đúng hay không là một câu hỏi khác, được trả lời bởi VerifyLoadedSignature và trình bày trong bài về xác minh chữ ký số PDF
Từng object thay đổi được phân loại như thế nào
Phân loại chạy theo từng object, rồi lan truyền theo các tham chiếu. Với mỗi số hiệu object mà một đoạn sau khi ký chạm vào, HotPDF đọc phần thân mới và phần thân như nó đã tồn tại trong snapshot đã ký; một phần thân giống hệt là rmlNone, vì các trình sinh đôi khi ghi lại object mà không thay đổi gì. Các bộ nhận diện được thiết kế hẹp có chủ đích. Một object /Type /DocTimeStamp, hoặc một object có /SubFilter là ETSI.RFC3161, là rmlLongTermValidation, cũng như bất cứ thứ gì có thể với tới từ cây /DSS của catalog; một dictionary /Type /Sig là rmlFormFillAndSign. Đối với container, phép kiểm tra là khóa nào đã di chuyển, không phải object là gì: catalog chỉ được phép thêm hoặc thay /DSS, /Extensions hoặc /AcroForm; dictionary AcroForm chỉ /Fields, /SigFlags, /NeedAppearances, /DR, /DA hoặc /Q; một trang chỉ /Annots; một trường hoặc widget chỉ /V, /AP, /AS hoặc /M. Bất cứ thứ gì ngoài những tập đó rơi xuống rmlOther, và đó chính xác là cách trang phụ lục được đính kèm bị bắt: thêm một trang sắp xếp lại cây trang theo cách mà không danh sách trắng nào bao phủ, và không có việc điền biểu mẫu hợp lệ nào giống với nó cả
Rồi các mức lan truyền, mỗi container thừa hưởng mức tối đa của các con đã thay đổi mà nó trỏ tới, lặp cho đến khi phép gán ổn định. Đây là điều khiến appearance stream hoạt động. Một trường văn bản được điền lại ghi lại /V và trỏ tới một stream /AP mới, và bản thân stream đó là một khối toán tử nội dung vô danh không có kiểu để nhận diện; vì trường sở hữu nó là rmlFormFillAndSign, stream thừa hưởng cùng mức thay vì rơi xuống rmlOther. Cùng cơ chế lan truyền đó mang ngữ cảnh DSS lên các stream chứng chỉ và thu hồi mà nếu không sẽ không thể phân loại được
Vì sao một object không đọc được lại tính là vi phạm?
Vì phương án thay thế là một trình xác thực bị đánh bại bằng cách ghi thứ nó không hiểu. Ba tình huống kết thúc ở rmlOther không có ngoại lệ trong HotPDF: một object mà phần thân của nó không đọc được từ revision, một object mà revision đánh dấu là đã giải phóng, và một object không khớp bất kỳ bộ nhận diện nào ở trên. Mỗi trường hợp ghi một chẩn đoán cụ thể vào trường Issue của revision, để người vận hành thấy object nào đã tạo ra kết luận đó
Giải phóng là trường hợp gay gắt nhất trong ba. Một revision sau khi ký đánh dấu một object đã được định nghĩa trước đó là free nghĩa là đã xóa nội dung khỏi một tài liệu đã ký, và không mức quyền hạn nào theo §12.8.2.2 cho phép điều đó; các số hiệu object rơi vào FreedObjectNumbers và revision được nâng lên rmlOther. Các object không đọc được đi theo cùng logic vì một lý do khác. Một trình xác thực không thể phân tích một object thì không có cơ sở nào để gọi nó vô hại, và phản ứng trung thực với điều đó không phải là im lặng. Báo cáo một cấu trúc bất thường nhưng vô hại như một vi phạm tốn một lần con người xem xét; sai lầm ngược lại là gửi đi một hợp đồng đã ký với một chỉnh sửa không ai để ý bên trong
Đọc kết luận trong Delphi
Lệnh gọi ngắn gọn. Tải tài liệu, chọn chỉ số chữ ký, đọc bản ghi; overload không tham số mở lại file mà tài liệu được tải từ đó, và overload TStream nhận byte do người gọi cung cấp và khôi phục vị trí stream trước khi trả về. PolicyCompliant là giá trị boolean duy nhất mà hầu hết bên gọi muốn, gộp ba quyết định độc lập: tính hợp lệ về cấu trúc của các dictionary quyền hạn, DocMDPCompliant, và FieldMDPCompliant. Hãy giữ các thành phần đó hiển thị riêng trong giao diện của bạn thay vì gộp chung, và lưu ý rằng một tài liệu không có phép biến đổi DocMDP để lại DocMDPCompliant là True, vì một chữ ký phê duyệt thông thường không khai báo chính sách nào để vi phạm, và ModificationLevel tổng hợp khi đó mang tính mô tả chứ không phải một kết luận
var
Pdf: THotPDF;
Analysis: THPDFSignatureRevisionAnalysis;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('contract-countersigned.pdf') > 0 then
begin
if Pdf.AnalyzeLoadedSignatureRevisions(0, Analysis) then
begin
if Analysis.PolicyCompliant then
Writeln('Post-signature changes stay inside the signing policy')
else
Writeln('Policy violation: ', string(Analysis.Issue));
end
else
Writeln('Analysis could not run: ', string(Analysis.Issue));
end;
finally
Pdf.Free;
end;
end;
Để phân loại triage, bạn thường muốn bảng phân tích theo từng revision thay vì bản tóm tắt, vì nó cho biết mọi thứ đi sai vào lúc nào trong lịch sử tài liệu. Mỗi mục trong Analysis.Revisions mang chỉ số của nó trong chuỗi, offset cross-reference mà nó được ghi tại đó, mức thay đổi của riêng nó, và các số hiệu object liên quan
const
LevelNames: array[THPDFRevisionModificationLevel] of string =
('none', 'long-term validation', 'form fill and sign',
'annotations', 'other');
var
I: Integer;
begin
Writeln(Format('%d revisions in chain, signature sits at index %d',
[Analysis.TotalRevisionCount, Analysis.SignedRevisionIndex]));
for I := 0 to High(Analysis.Revisions) do
Writeln(Format(' rev %d at offset %d: %s (%d changed, %d freed) %s',
[Analysis.Revisions[I].RevisionIndex,
Analysis.Revisions[I].XRefOffset,
LevelNames[Analysis.Revisions[I].ModificationLevel],
Length(Analysis.Revisions[I].ChangedObjectNumbers),
Length(Analysis.Revisions[I].FreedObjectNumbers),
string(Analysis.Revisions[I].Issue)]));
end;
FieldMDP được đánh giá riêng, và đó là có chủ đích
Một tài liệu có thể thỏa DocMDP mà vẫn không hợp lệ, đó là lý do FieldMDPCompliant là một boolean riêng biệt thay vì gộp vào phép so sánh mức. ISO 32000-1 §12.8.2.4 định nghĩa phép biến đổi FieldMDP, và §12.7.5.5 mục /SigFieldLock liên quan, để đóng băng các trường biểu mẫu được đặt tên tại thời điểm ký ngay cả khi tài liệu nói chung vẫn cho phép điền biểu mẫu. Điền một trường là hành động mức 2; điền một trường mà người ký đã khóa là một vi phạm bất kể mức nào. HotPDF đọc phạm vi vào THPDFFieldLockAction dưới dạng flaAll, flaInclude hoặc flaExclude, với flaNone dành cho các kết quả không mang chính sách khóa, và các tên vào Permissions.FieldNames: flaAll khóa mọi thứ, flaInclude khóa các tên được liệt kê, flaExclude khóa mọi thứ trừ chúng. Một chi tiết cần lưu ý khi đọc kết quả là chỉ những trường đã có mặt trong snapshot đã ký mới được báo cáo trong ChangedFieldNames, vì một trường được tạo hoàn toàn sau khi ký không có trạng thái đã ký nào để mâu thuẫn và bị bắt bởi đường DocMDP thay vào đó
var
Source: TFileStream;
Analysis: THPDFSignatureRevisionAnalysis;
I: Integer;
begin
Source := TFileStream.Create('contract.pdf', fmOpenRead or fmShareDenyWrite);
try
if Pdf.AnalyzeLoadedSignatureRevisions(0, Source, Analysis) then
if Analysis.Permissions.HasFieldMDP and (not Analysis.FieldMDPCompliant) then
for I := 0 to High(Analysis.ChangedFieldNames) do
Writeln('modified after locking: ',
string(Analysis.ChangedFieldNames[I]));
finally
Source.Free; // stream position was restored before the call returned
end;
end;
Điều mà phân tích này sẽ không cho bạn biết
Nó không xác minh chữ ký. AnalyzeLoadedSignatureRevisions suy luận về cấu trúc và quyền hạn; liệu dải byte đã ký có còn băm ra đúng giá trị trong blob CMS hay không, và liệu chứng chỉ của người ký có nối chuỗi tới thứ gì bạn tin cậy hay không, được trả lời bởi VerifyLoadedSignature và VerifyLoadedSignatureWithTrust. Một file có thể hoàn toàn tuân thủ chính sách và vô giá trị về mặt mật mã, nên hai phép kiểm tra thuộc về nhau, song hành trong bất kỳ cổng chấp nhận thực tế nào. Nó cũng không đọc ý định bên trong content stream: một trang mà content stream của nó bị thay thế toàn bộ bị bắt như một thay đổi ngoài danh sách trắng, nhưng phân tích sẽ không nói cho bạn biết bản thay thế đã đổi một con số thanh toán. Một kết luận rmlOther nghĩa là con người nên xem xét, không phải là gian lận đã xảy ra, và một kết luận tuân thủ nghĩa là thay đổi khớp một phạm trù được cho phép, không phải là thay đổi được mong muốn. Khi tất cả những gì bạn cần là điều người ký đã khai báo, mà không cần duyệt qua revision, GetLoadedSignaturePermissions trả về các dictionary chính sách một cách riêng biệt
Mọi thứ mô tả ở đây chạy hoàn toàn bản địa trong Delphi và C++Builder mà không cần dịch vụ ký bên ngoài nào trong vòng lặp, đó là điều khiến nó thực tế để chạy trên mọi tài liệu đến thay vì chỉ những tài liệu đã bị nghi ngờ từ trước. API chữ ký và revision đầy đủ, bao gồm các phương thức quyền hạn và xác minh mà nó kết hợp cùng, là một phần của HotPDF Component cho Delphi và C++Builder