ข้ามไปยังเนื้อหา

การอัปเดตแบบเพิ่มหน่วยและเหตุผลที่สำคัญ

Evidence: Standard-backed

เมื่อ PDF มีการเปลี่ยนแปลงหลังจากเขียนเสร็จแล้ว วิธีบันทึกที่ปลอดภัยคือไม่เขียนไฟล์ใหม่ทั้งหมด แต่จะ ต่อท้าย วัตถุที่เปลี่ยนแปลงและส่วน cross-reference ใหม่ไว้ที่ท้ายไฟล์แทน โดยปล่อยให้ไบต์เดิมทุกไบต์อยู่ที่ตำแหน่งเดิมทุกประการ หน้านี้อธิบายว่ากลไกนี้ทำงานอย่างไร และเหตุใดลายเซ็นดิจิทัลจึงยังคงอยู่ได้แม้มีการแก้ไขในภายหลัง

ลายเซ็นปกป้องช่วงไบต์หนึ่งช่วง หากการบันทึกการแก้ไขเพียงคำเดียวทำให้ต้องเขียนไฟล์ใหม่ ตำแหน่ง byte offset ทุกตำแหน่งก็จะเลื่อน ช่วงที่ถูกเซ็นจะไม่อธิบายเนื้อหาเดิมอีกต่อไป ลายเซ็นจะใช้การไม่ได้ แม้ว่าเนื้อหาที่ถูกเซ็นเองจะไม่ถูกแตะต้องก็ตาม

การอัปเดตแบบเพิ่มหน่วยมีไว้เพื่อป้องกันเหตุการณ์เช่นนั้น ไบต์เดิม รวมถึงไบต์ที่ลายเซ็นครอบคลุม จะคงอยู่ที่เดิม ผู้ตรวจสอบสามารถนำเอกสารที่ถูกเซ็นแล้วและมีการแก้ไขภายหลังมาตรวจสอบลายเซ็นแรกเทียบกับการแก้ไขฉบับดั้งเดิมได้ ผู้ตรวจสอบจะเห็นได้อย่างแม่นยำว่าสิ่งใดถูกเซ็น และเห็นแยกต่างหากว่าสิ่งใดเปลี่ยนแปลงไปในภายหลัง หากจัดการเรื่องนี้ผิดพลาด อาจทำให้ลายเซ็นที่ถูกต้องใช้การไม่ได้ หรือที่แย่กว่านั้นคือสูญเสียความสามารถในการพิสูจน์ว่าลายเซ็นนั้นรับรองสิ่งใดไว้จริง

  • การอัปเดตแบบเพิ่มหน่วยจะ ต่อท้าย วัตถุที่เพิ่มใหม่และที่เปลี่ยนแปลง ตามด้วยส่วน cross-reference ใหม่และ trailer ใหม่ ทั้งหมดอยู่ที่ท้ายไฟล์
  • เนื้อหาไฟล์เดิมจะ คงสภาพเดิมไว้ และไม่ถูกแก้ไขในตำแหน่งเดิม
  • trailer ใหม่จะมีรายการ /Prev ซึ่งคือ byte offset ของส่วน cross-reference ก่อนหน้า ส่วนต่าง ๆ เหล่านี้ประกอบกันเป็นห่วงโซ่ที่ชี้ย้อนหลัง
  • โปรแกรมอ่านจะสร้างดัชนีโดยไล่ตามห่วงโซ่นั้นจากรายการใหม่ที่สุดก่อน สำหรับหมายเลขวัตถุใด ๆ รายการที่ ใหม่ที่สุด จะเป็นรายการที่ใช้
  • เนื่องจากไม่มีสิ่งใดถูกเขียนทับ ช่วงไบต์ที่ลายเซ็นก่อนหน้าครอบคลุมจึงยังคงเหมือนเดิมแบบไบต์ต่อไบต์ ลายเซ็นจึงยังตรวจสอบผ่าน และสามารถกู้คืนเอกสารในสภาพเดียวกับตอนที่ถูกเซ็นได้อย่างแม่นยำ

NextPDF เขียนเอกสารฐานตามที่อธิบายไว้ในหน้าก่อนหน้า จากนั้นจึงเปิดเผยข้อมูลสามอย่างที่การอัปเดตแบบเพิ่มหน่วยต้องใช้

หลังจาก build() โปรแกรมเขียน (src/Writer/PdfWriter.php) จะเก็บสิ่งต่อไปนี้ไว้

  • บัฟเฟอร์เอาต์พุตที่ดึงได้ผ่าน getBuffer() เพื่อให้สามารถต่อท้ายการอัปเดตไว้ที่ตำแหน่งท้ายสุดของไบต์ที่มีอยู่ได้อย่างแม่นยำ
  • byte offset ของส่วน cross-reference ล่าสุด ผ่าน getLastXrefOffset() ซึ่งจะกลายเป็นค่า /Prev ของส่วนใหม่
  • รายการใน catalog dictionary ผ่าน getCatalogEntries() เพื่อให้การอัปเดตที่ต้องเขียน catalog ใหม่ (เช่น เพื่อแนบการอ้างอิงลายเซ็น) ไม่สูญเสียคีย์เดิมใด ๆ

การแก้ไขที่ถูกต่อท้ายจะจัดสรรหมายเลขวัตถุใหม่ (หรือนำหมายเลขเดิมกลับมาใช้สำหรับวัตถุที่แทนที่) โดยใช้ ObjectRegistry ตัวเดียวกัน การกำหนดหมายเลขวัตถุจึงสอดคล้องกันตลอดทุกการแก้ไข ส่วน cross-reference ใหม่จะแสดงรายการเฉพาะวัตถุที่การแก้ไขครั้งนี้แตะต้องเท่านั้น trailer ใหม่จะคงรายการจาก trailer ก่อนหน้าไว้ และเพิ่ม /Prev ที่ชี้ย้อนกลับไปยังส่วนก่อนหน้า ห่วงโซ่นี้คือสิ่งที่โปรแกรมอ่านไล่ตาม

ประเด็นนี้เห็นชัดที่สุดในการเซ็น NextPDF ใช้ ByteRangeCalculator (src/Security/Signature/ByteRangeCalculator.php) คำนวณอาร์เรย์ /ByteRange เป็นสองช่วง ได้แก่ทุกอย่างก่อนค่าลายเซ็นและทุกอย่างหลังค่าลายเซ็น ลายเซ็นจึงครอบคลุมการแก้ไขทั้งหมด ยกเว้นไบต์ของตัวลายเซ็นเอง เนื่องจากการแก้ไขในภายหลังถูกต่อท้ายแทนที่จะเขียนทับไบต์เหล่านั้น ช่วงดังกล่าวจึงไม่เคยเลื่อน

  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.
วิธีที่ PDF ซึ่งถูกเซ็นแล้วแก้ไขในภายหลังยังคงตรวจสอบได้ ทุกครั้งที่บันทึกจะเป็นการต่อท้าย ไบต์ที่ถูกเซ็นไว้ดั้งเดิมจึงไม่เคยถูกเขียนทับ และห่วงโซ่ trailer จะบันทึกประวัติไว้

กฎแบบต่อท้ายเท่านั้นเป็นข้อกำหนดเชิงบรรทัดฐาน Spec: ISO 32000-2, §7.5.6 ระบุว่าเนื้อหาของ PDF สามารถอัปเดตแบบเพิ่มหน่วยได้โดยไม่ต้องเขียนไฟล์ใหม่ทั้งหมด และเมื่อทำเช่นนั้น การเปลี่ยนแปลง ต้อง ถูกต่อท้ายไว้ที่ท้ายไฟล์ โดยปล่อยให้เนื้อหาเดิมคงสภาพไว้ Evidence: Standard-backed

ข้อกำหนดเดียวกันนี้นิยามกลไกการทำงานไว้ ส่วน cross-reference สำหรับการอัปเดตแบบเพิ่มหน่วยจะมีรายการเฉพาะวัตถุที่ถูกเปลี่ยนแปลง แทนที่ หรือลบเท่านั้น วัตถุที่ถูกลบจะยังคงอยู่ในไฟล์ แต่ถูกทำเครื่องหมายว่าลบแล้วผ่านรายการ cross-reference ของวัตถุนั้น trailer ที่ถูกเพิ่มเข้ามา ต้อง มีรายการ /Prev ที่ระบุตำแหน่งของส่วน cross-reference ก่อนหน้า รายการ cross-reference ของการอัปเดตสำหรับวัตถุที่เปลี่ยนแปลงจะมี byte offset ของสำเนา ใหม่ ซึ่งจะแทนที่ offset เดิม โปรแกรมอ่านจะสร้างข้อมูล cross-reference ขึ้นมาเพื่อให้เข้าถึงสำเนาล่าสุดของแต่ละวัตถุ

ผลที่เกิดกับลายเซ็นระบุไว้โดยตรงใน Spec: ISO 32000-2, §12.8.1 กล่าวคือ ค่า digest ของช่วงไบต์ จะถูกคำนวณจากช่วงหนึ่งของไฟล์ ซึ่งโดยปกติคือทั้งไฟล์ ยกเว้น ค่าลายเซ็น (รายการ /Contents) จากนั้นมาตรฐานระบุไว้ว่า หากเอกสารที่ถูกเซ็นถูกแก้ไขและบันทึกด้วยการอัปเดตแบบเพิ่มหน่วย ข้อมูล ที่สอดคล้องกับช่วงไบต์ของลายเซ็นดั้งเดิมจะถูกรักษาไว้ ดังนั้นหาก ลายเซ็นนั้นถูกต้อง สถานะของเอกสาร ณ เวลาที่เซ็นก็สามารถ สร้างขึ้นใหม่ได้ การต่อท้ายเท่านั้นไม่ใช่เพียงคุณสมบัติเสริมที่ดี แต่เป็นคุณสมบัติที่แบบจำลองลายเซ็น ต้องพึ่งพา

เมื่อมอง PDF ที่ถูกเซ็นแล้วและแก้ไขภายหลังในเชิงโครงสร้าง การแก้ไขฉบับดั้งเดิมจะสิ้นสุดที่ %%EOF ของตัวเอง การแก้ไขครั้งที่สองถูกต่อท้ายไว้ด้านล่าง

%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

ตัวตรวจสอบจะอ่าน trailer สุดท้าย เห็น /Prev 920 และจึงได้ห่วงโซ่ทั้งหมด ตัวตรวจสอบสามารถตรวจสอบลายเซ็นเทียบกับไบต์จนถึง %%EOF แรก ซึ่งไม่มีการเปลี่ยนแปลง จากนั้นจึงรายงานแยกต่างหากได้ว่าการแก้ไขครั้งที่ 2 เพิ่ม annotation เข้ามา ประวัติทั้งหมดอยู่ในไฟล์ ไม่มีสิ่งใดถูกซ่อนด้วยการเขียนทับ

ความเข้าใจผิดที่พบบ่อยคือ “การอัปเดตแบบเพิ่มหน่วยหมายความว่าการเปลี่ยนแปลงมีขนาดเล็ก จึงไม่เป็นอันตราย” การต่อท้ายเกี่ยวข้องกับ การรักษาไบต์ ไม่ใช่ขนาด การอัปเดตแบบเพิ่มหน่วยสามารถเพิ่มเนื้อหาได้เป็นจำนวนมาก สิ่งที่ทำให้เป็นการอัปเดตแบบเพิ่มหน่วยคือการไม่แตะต้องไบต์ที่มีอยู่แล้ว ผลที่ตามมาข้อนี้ทำให้หลายคนพลาดเช่นกัน เครื่องมือที่ “เพิ่มประสิทธิภาพ” หรือ “linearize” PDF ที่ถูกเซ็นโดยเขียนใหม่ตั้งแต่ต้นจะได้ไฟล์ที่เล็กลงและสะอาดขึ้น และลายเซ็นที่ใช้การไม่ได้ เพราะช่วงไบต์ที่ถูกเซ็นไม่มีอยู่อีกต่อไป การบันทึก PDF ที่ถูกเซ็นแล้วกับการบันทึกซ้ำไม่ใช่การดำเนินการเดียวกัน

การต่อท้ายเท่านั้นปกป้อง ไบต์ แต่การต่อท้ายเพียงลำพังไม่ได้บอกว่าการเปลี่ยนแปลงที่ต่อท้ายเข้ามานั้นได้รับ การอนุญาต หรือไม่ การแก้ไขครั้งที่สองอาจเพิ่มลายเซ็นที่สองอย่างถูกต้องตามกฎ หรืออาจเพิ่มเนื้อหาที่ผู้เซ็นรายแรกไม่เคยตั้งใจก็ได้ การตัดสินว่าเป็นกรณีใดเป็นหน้าที่ของการตรวจสอบลายเซ็นและนโยบายการตรวจจับการแก้ไข (DocMDP) การต่อท้ายเป็นรากฐานที่ทำให้การวิเคราะห์นั้น เป็นไปได้ ไม่ใช่ตัวการวิเคราะห์เอง

หน้านี้ยังไม่ครอบคลุมถึง วิธี คำนวณและประกอบช่วงไบต์สองช่วงของลายเซ็น และไม่ครอบคลุมถึงสิ่งที่การตรวจสอบฉบับสมบูรณ์ต้องตรวจ เรื่องเหล่านั้นเป็นหัวข้อแยกต่างหาก และการรับประกันในที่นี้เป็นเรื่องของไฟล์ที่เขียนและอัปเดตโดยโปรแกรมเขียนที่สอดคล้องกับมาตรฐาน ไฟล์ที่การแก้ไขก่อนหน้ามีรูปแบบผิดพลาดอยู่แล้วจะไม่กลายเป็นไฟล์ที่มีรูปแบบถูกต้องเพียงเพราะถูกต่อท้าย

จะรู้ได้อย่างไรว่า PDF มีการแก้ไขกี่ครั้ง นับเครื่องหมาย %%EOF และไล่ตามห่วงโซ่ /Prev จาก trailer สุดท้าย แต่ละส่วน cross-reference ที่ไล่ไปถึงคือการแก้ไขหนึ่งครั้งที่ถูกบันทึก

การลบวัตถุจะนำวัตถุนั้นออกจากไฟล์หรือไม่ ไม่ การอัปเดตแบบเพิ่มหน่วยจะทำเครื่องหมายว่าวัตถุถูกลบในรายการ cross-reference ของวัตถุนั้น แต่ไบต์ของวัตถุยังคงอยู่ในการแก้ไขก่อนหน้า “ลบ” หมายถึง “ไม่ถูกอ้างอิงโดยการแก้ไขปัจจุบัน” ไม่ใช่ “ถูกลบล้างทิ้ง”

การอัปเดตแบบเพิ่มหน่วยสามารถเปลี่ยนเวอร์ชัน PDF ได้หรือไม่ ได้ ด้วยการกำหนดรายการ /Version ใน catalog ในการแก้ไขที่ต่อท้าย ส่วนหัวจะคงสภาพเดิมตามที่เขียนไว้ /Version ใน catalog จะมีลำดับความสำคัญเหนือกว่าเมื่อระบุเวอร์ชันที่ใหม่กว่า

  • Incremental update (การอัปเดตแบบเพิ่มหน่วย) การบันทึกการเปลี่ยนแปลงด้วยการต่อท้ายวัตถุที่เปลี่ยนแปลง ส่วน cross-reference ใหม่ และ trailer ใหม่ไว้ที่ท้ายไฟล์ โดยไม่เปลี่ยนแปลงไบต์เดิมที่มีอยู่
  • /Prev รายการใน trailer (หรือ cross-reference stream) ที่เก็บ byte offset ของส่วน cross-reference ก่อนหน้า รายการนี้เชื่อมโยงการแก้ไขต่าง ๆ เข้าเป็นห่วงโซ่ที่ชี้ย้อนหลัง
  • Revision (การแก้ไข) สถานะของไฟล์ที่บันทึกไว้โดยส่วน cross-reference หนึ่งส่วนและ trailer ของส่วนนั้น ไฟล์ที่มีส่วน cross-reference จำนวน N ส่วนจะมีการแก้ไข N ครั้ง
  • /ByteRange อาร์เรย์ใน signature dictionary ที่ระบุช่วงไบต์สองช่วงที่ digest ของลายเซ็นครอบคลุม (ทุกอย่างยกเว้นค่าลายเซ็นเอง)
  • Signed byte range (ช่วงไบต์ที่ถูกเซ็น) ไบต์ที่ digest ของลายเซ็นถูกคำนวณจากไบต์เหล่านั้นโดยตรง การอัปเดตแบบเพิ่มหน่วยมีไว้เพื่อให้ไบต์เหล่านี้ไม่เคยถูกย้ายหรือเขียนทับ