Bỏ qua để đến nội dung

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

Evidence: Standard-backed

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 revision Header, body, xref section, trailer — the original bytes.
  2. Sign A /ByteRange digest covers the whole revision except the signature value itself.
  3. Edit and save Changed objects + a new xref section are appended; originals are untouched.
  4. New trailer chains back The appended trailer carries /Prev = offset of the previous xref section.
  5. Verify The 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ử.

Quy tắc chỉ-nối-thêm là một yêu cầu mang tính quy phạm. Spec: ISO 32000-2, §7.5.6 nêu rằng nội dung của một tệp PDF có thể được cập nhật theo cách gia tăng mà không cần ghi lại toàn bộ tệp, và khi làm vậy, các thay đổi phải được nối thêm vào cuối tệp, giữ nguyên vẹn nội dung gốc. Evidence: Standard-backed

Cùng điều khoản đó cũng định nghĩa cơ chế. Một mục cross-reference cho bản cập nhật gia tăng chỉ chứa các entry cho những đối tượng đã bị thay đổi, thay thế hoặc xóa. Các đối tượng đã xóa vẫn được giữ lại trong tệp nhưng được đánh dấu là đã xóa thông qua entry cross-reference của chúng. Trailer được thêm vào phải chứa một entry /Prev cho biết vị trí của mục cross-reference trước đó. Entry của bản cập nhật cho một đối tượng đã thay đổi mang byte offset của bản sao mới, ghi đè lên offset cũ. Một trình đọc xây dựng thông tin cross-reference sao cho bản sao mới nhất của mỗi đối tượng là bản được truy cập.

Hệ quả đối với chữ ký được nêu trực tiếp bởi Spec: ISO 32000-2, §12.8.1 : một digest theo dải byte được tính trên một dải của tệp — thường là toàn bộ tệp, ngoại trừ giá trị chữ ký (entry /Contents). Tiêu chuẩn sau đó lưu ý rằng nếu một tài liệu đã ký bị sửa đổi và lưu bằng cập nhật gia tăng, thì dữ liệu tương ứng với dải byte của chữ ký gốc được bảo toàn, nên nếu chữ ký hợp lệ thì trạng thái của tài liệu tại thời điểm ký có thể được tái tạo lại. Chỉ-nối-thêm không phải là một điểm cộng tùy chọn. Nó là thuộc tính mà mô hình chữ ký phụ thuộc vào.

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 đè.