Bỏ qua để đến nội dung
getnextpdf.com

Cập nhật gia tăng và vì sao chúng quan trọng

Khi một tệp PDF thay đổi sau khi đã được ghi, cách lưu an toàn không phải là ghi lại toàn bộ tệp. Thay vào đó, bạn nối thêm các đối tượng đã thay đổi và một mục cross-reference mới vào cuối tệp, giữ nguyên mọi byte gốc ở đúng vị trí ban đầu. Trang này giải thích cơ chế đó hoạt động ra sao và vì sao nhờ vậy một chữ ký số có thể tồn tại qua một lần chỉnh sửa sau đó.

Chữ ký bảo vệ một dải byte. Nếu việc lưu một thay đổi chỉ sửa một từ mà lại ghi lại toàn bộ tệp, thì mọi byte offset đều sẽ dịch chuyển. Dải đã ký sẽ không còn khớp với nội dung ban đầu nữa. Chữ ký sẽ hỏng, ngay cả khi bản thân nội dung đã ký không hề bị động đến.

Cập nhật gia tăng tồn tại để điều đó không xảy ra. Các byte gốc, bao gồm cả những byte mà một chữ ký bao phủ, vẫn giữ nguyên vị trí. Người kiểm tra có thể lấy một tài liệu đã ký rồi mới chỉnh sửa và xác minh chữ ký đầu tiên dựa trên bản sửa đổi gốc. Họ thấy chính xác những gì đã được ký và, tách biệt với phần đó, những gì đã thay đổi về sau. Làm sai điều này thì bạn hoặc sẽ vô hiệu hóa các chữ ký hợp lệ, hoặc tệ hơn, mất khả năng chứng minh chữ ký thực sự đã xác nhận điều gì.

  • Một bản cập nhật gia tăng nối thêm: các đối tượng mới và đã thay đổi, rồi một mục cross-reference mới, rồi một trailer mới, tất cả đều ở cuối tệp.
  • Nội dung gốc của tệp được giữ nguyên vẹn — không bị chỉnh sửa tại chỗ.
  • Trailer mới mang một mục /Prev: byte offset của mục cross-reference trước đó. Các mục này tạo thành một chuỗi liên kết ngược.
  • Một trình đọc xây dựng chỉ mục bằng cách đi theo chuỗi đó, bắt đầu từ mục mới nhất. Với bất kỳ số đối tượng nào, mục mới nhất sẽ thắng.
  • Vì không có gì bị ghi đè, dải byte mà một chữ ký trước đó bao phủ vẫn giống hệt từng byte một — nên chữ ký vẫn xác minh được, và bạn có thể khôi phục tài liệu đúng như khi nó được ký.

NextPDF ghi tài liệu nền như đã mô tả ở trang trước, rồi cung cấp ba thứ mà một bản cập nhật gia tăng cần có.

Sau build(), trình ghi (src/Writer/PdfWriter.php) giữ lại:

  • bộ đệm đầu ra, có thể lấy qua getBuffer(), để một bản cập nhật có thể được nối thêm đúng vào điểm cuối của các byte hiện có;
  • byte offset của mục cross-reference cuối cùng, qua getLastXrefOffset(), sẽ trở thành giá trị /Prev của mục mới;
  • các mục trong dictionary của catalog, qua getCatalogEntries(), để một bản cập nhật buộc phải phát lại catalog (ví dụ để gắn một tham chiếu chữ ký) sẽ không làm mất bất kỳ khóa nào trước đó.

Một bản sửa đổi được nối thêm cấp phát số đối tượng mới (hoặc tái sử dụng số hiện có cho các đối tượng mà nó thay thế) dựa trên cùng một ObjectRegistry, nhờ vậy việc đánh số đối tượng vẫn nhất quán qua các bản sửa đổi. Mục cross-reference mới chỉ liệt kê những đối tượng mà bản sửa đổi này tác động đến. Trailer mới lặp lại các mục của trailer trước đó và thêm /Prev, trỏ ngược về mục trước đó. Chuỗi đó chính là thứ mà một trình đọc đi theo.

Điều này thể hiện rõ nhất khi ký. ByteRangeCalculator của NextPDF (src/Security/Signature/ByteRangeCalculator.php) tính mảng /ByteRange thành hai đoạn: mọi thứ trước giá trị chữ ký, và mọi thứ sau nó — nhờ vậy chữ ký bao phủ toàn bộ bản sửa đổi trừ chính các byte của nó. Vì một lần chỉnh sửa về sau được nối thêm chứ không ghi đè lên những byte đó, nên dải này không bao giờ dịch chuyển.

  1. Write base revisionHeader, body, xref section, trailer — the original bytes.
  2. SignA /ByteRange digest covers the whole revision except the signature value itself.
  3. Edit and saveChanged objects + a new xref section are appended; originals are untouched.
  4. New trailer chains backThe appended trailer carries /Prev = offset of the previous xref section.
  5. VerifyThe first signature still covers the same unchanged bytes; the chain shows what came after.
Cách một tệp PDF đã ký mà sau đó được chỉnh sửa vẫn có thể xác minh: mỗi lần lưu đều nối thêm, nên các byte đã ký ban đầu không bao giờ bị ghi đè và chuỗi trailer ghi lại lịch sử.

Một tệp PDF đã ký rồi mới chỉnh sửa, nhìn từ góc độ cấu trúc. Bản sửa đổi gốc kết thúc tại %%EOF của riêng nó. Bản sửa đổi thứ hai được nối thêm bên dưới nó.

%PDF-2.0
... original objects, including the signature dictionary ...
xref
0 8
... entries for the original revision ...
trailer
<< /Size 8 /Root 1 0 R >>
startxref
920
%%EOF
<-- end of revision 1: the signed bytes stop here
9 0 obj <-- revision 2, appended
<< /Type /Annot /Subtype /Text /Contents (added after signing) >>
endobj
xref
0 1
9 0 obj-entry...
8 9
0000001740 00000 n
trailer
<< /Size 10 /Root 1 0 R /Prev 920 >>
startxref
1980
%%EOF

Một trình xác thực đọc trailer cuối cùng, thấy /Prev 920, và từ đó có toàn bộ chuỗi. Nó có thể xác minh chữ ký dựa trên các byte tính đến %%EOF đầu tiên, vốn không thay đổi. Sau đó nó có thể báo cáo riêng rằng bản sửa đổi 2 đã thêm một annotation. Lịch sử nằm ngay trong tệp. Không có gì bị che giấu bằng cách ghi đè.

Điểm dễ mắc bẫy là “cập nhật gia tăng nghĩa là thay đổi nhỏ, nên nó vô hại.” Nối thêm nói về bảo toàn byte, không phải về kích thước. Một bản cập nhật gia tăng có thể thêm rất nhiều nội dung. Điều khiến nó là một bản cập nhật gia tăng là nó không động đến những byte đã có sẵn ở đó. Hệ quả đi kèm cũng dễ khiến nhiều người nhầm: một công cụ “tối ưu hóa” hoặc “tuyến tính hóa” một tệp PDF đã ký bằng cách ghi lại từ đầu sẽ tạo ra một tệp nhỏ hơn, gọn gàng hơn, kèm theo một chữ ký bị hỏng, vì dải byte đã ký không còn tồn tại nữa. Lưu gia tăng một tệp PDF đã ký và ghi lại nó từ đầu không phải là cùng một thao tác.

Chỉ-nối-thêm bảo vệ các byte. Tự thân nó không cho bạn biết liệu các thay đổi được nối thêm có được cho phép hay không. Một bản sửa đổi thứ hai có thể thêm một chữ ký thứ hai một cách hợp lệ, hoặc cũng có thể thêm nội dung mà người ký đầu tiên không hề có ý định. Quyết định đó là việc của quá trình xác thực chữ ký và chính sách phát hiện sửa đổi (DocMDP). Nối thêm là nền tảng giúp việc phân tích đó có thể thực hiện được, chứ không phải bản thân việc phân tích.

Trang này cũng không đề cập cách hai dải byte của một chữ ký được tính và ghép lại, hay những gì một quá trình xác thực đầy đủ kiểm tra. Đó là những chủ đề riêng. Bảo đảm ở đây áp dụng cho các tệp được ghi và cập nhật bởi một trình ghi tuân thủ: một tệp có các bản sửa đổi trước đó vốn đã sai định dạng thì không trở nên đúng định dạng chỉ nhờ được nối thêm vào.

Làm sao tôi biết một tệp PDF có bao nhiêu bản sửa đổi? Đếm các dấu %%EOF và đi theo chuỗi /Prev từ trailer cuối cùng. Mỗi mục cross-reference mà bạn truy theo được tương ứng với một bản sửa đổi đã lưu.

Việc xóa một đối tượng có loại bỏ nó khỏi tệp không? Không. Một bản cập nhật gia tăng đánh dấu đối tượng là đã xóa trong entry cross-reference của nó, nhưng các byte của đối tượng vẫn còn ở các bản sửa đổi trước đó. “Đã xóa” nghĩa là “không được bản sửa đổi hiện tại tham chiếu đến”, chứ không phải “đã bị xóa bỏ.”

Một bản cập nhật gia tăng có thể thay đổi phiên bản PDF không? Có, bằng cách đặt entry /Version trong catalog ở bản sửa đổi được nối thêm. Phần header vẫn giữ nguyên như đã ghi. /Version trong catalog được ưu tiên khi nó chỉ định một phiên bản mới hơn.

  • Cập nhật gia tăng — lưu thay đổi bằng cách nối thêm các đối tượng đã thay đổi, một mục cross-reference mới và một trailer mới vào cuối tệp, mà không thay đổi các byte hiện có.
  • /Prev — entry trong trailer (hoặc cross-reference stream) giữ byte offset của mục cross-reference trước đó. Nó liên kết các bản sửa đổi thành một chuỗi ngược.
  • Bản sửa đổi (Revision) — trạng thái của tệp được ghi lại bởi một mục cross-reference và trailer của nó. Một tệp có N mục cross-reference thì có N bản sửa đổi.
  • /ByteRange — mảng trong một signature dictionary cho biết hai đoạn byte mà digest của chữ ký bao phủ (mọi thứ trừ chính giá trị chữ ký).
  • Dải byte đã ký (Signed byte range) — đúng những byte mà digest của một chữ ký được tính trên đó. Cập nhật gia tăng tồn tại để những byte này không bao giờ bị dịch chuyển hay ghi đè.