เครื่องมือตรวจสอบจะปฏิเสธลายเซ็น PAdES ด้วยเหตุผลหนึ่งในสามประการในเกือบทุกกรณีที่เราได้ดีบั๊กด้วย PDFium Component ใน Delphi: อาร์เรย์ /ByteRange ยังคงเก็บตัวยึดตำแหน่งที่เป็นศูนย์, เวลาลงนาม /M ไม่ใช่สตริงวันที่ PDF ที่ถูกต้อง หรือการอัปเดตส่วนเพิ่ม (incremental update) ได้ละทิ้งรายการ /Encrypt จากเอกสารที่เข้ารหัส ทั้งสามประการนี้สร้างไฟล์ที่เปิดใช้งานได้ดี เรนเดอร์ได้ดี แต่ล้มเหลวทันทีที่เครื่องมือตรวจสอบที่สอดคล้องกับมาตรฐานอ่านพจนานุกรมลายเซ็น
สถานการณ์ที่เป็นแรงบันดาลใจให้เขียนบทความนี้มีความเฉพาะเจาะจงมาก คุณลงนามในสัญญาด้วยเวิร์กโฟลว์จากบทความการลงนาม PAdES B-B โค้ดตรวจสอบของคุณเองรายงานว่ามีลายเซ็นและโครงสร้างที่สมบูรณ์ การทดสอบในเครื่องทั้งหมดผ่านเกณฑ์ — จากนั้นคู่สัญญาอัปโหลดไฟล์ไปยังแพลตฟอร์มการตรวจสอบของพวกเขาและพบกากบาทสีแดง ไม่มีส่วนใดของความล้มเหลวที่มองเห็นได้ในโปรแกรมดูเอกสาร เนื่องจากข้อบกพร่องทั้งสามประการนี้ไม่กระทบต่อเนื้อหาหน้าเว็บ พวกมันอาศัยอยู่ภายในพจนานุกรมลายเซ็นและส่วนท้ายไฟล์อย่างสมบูรณ์ ซึ่งเป็นจุดที่เครื่องมือตรวจสอบค้นหาข้อมูลในขณะที่โปรแกรมดูเอกสารส่วนใหญ่ไม่ได้สแกน
ทำไม ByteRange ของลายเซ็น PDF ของฉันจึงไม่ถูกต้อง?
การปฏิเสธ ByteRange มักหมายความว่าฟิลด์ทั้งสี่ฟิลด์ไม่เคยถูกเติมข้อมูล ไม่ใช่ว่าช่วงข้อมูลนั้นคลาดเคลื่อนอย่างละเอียดอ่อน อาร์เรย์ /ByteRange [A B C D] ประกาศช่วงข้อมูลสองช่วง คือ ไบต์ A ถึง A+B และไบต์ C ถึง C+D และข้อกำหนด EN 319 142-1 §6.3 (ข้อกำหนด k) กำหนดให้ข้อมูลทั้งสองรวมกันครอบคลุมทั้งไฟล์ ยกเว้นสตริง /Contents ที่เข้ารหัสฐานสิบหก ในเชิงตัวเลข: A คือ 0, C >= A+B, C+D เท่ากับความยาวไฟล์ และช่องว่างระหว่าง B และ C เก็บสตริงฐานสิบหก <...> พอดี — วงเล็บสองไบต์บวกอักขระฐานสิบหกสองตัวต่อไบต์ของข้อมูล CMS เครื่องมือตรวจสอบที่อ่านค่า [0 0 0 0] จะสรุปว่าลายเซ็นไม่ครอบคลุมอะไรเลยและปฏิเสธมัน โดยไม่คำนึงว่าโครงสร้าง CMS ภายใน /Contents จะถูกต้องเพียงใด
กลไกของการเกิดขึ้นนี้เป็นเรื่องที่คุ้มค่าแก่การทำความเข้าใจ เนื่องจากมีรูปแบบเดียวกันในทุกระบบการลงนาม ผู้ลงนามไม่สามารถทราบค่าออฟเซ็ตสุดท้ายได้จนกว่าไฟล์จะถูกจัดวางโครงสร้าง ดังนั้นตัวเขียนจึงสร้างอาร์เรย์ด้วยตัวยึดตำแหน่งศูนย์ที่มีความกว้างคงที่ และเติมข้อมูลกลับหลังจากจัดวางโครงสร้าง ในรุ่นของ PDFium Component ก่อนเวอร์ชัน 2.14.1 การเติมข้อมูลกลับนั้นค้นหารูปแบบตัวยึดตำแหน่งโดยเริ่มจากตำแหน่ง /Contents — แต่ /ByteRange อยู่ก่อน /Contents ในพจนานุกรม ดังนั้นการค้นหาจึงไม่พบอะไรเลยและการแทนที่ทั้งสามครั้งจึงล้มเหลวอย่างเงียบเชียบ ผลสรุปข้อมูล CMS ถูกคำนวณบนช่วงข้อมูลที่ถูกต้อง ดังนั้นวิทยาการเข้ารหัสลับจึงสมบูรณ์แบบ; แต่การประกาศช่วงข้อมูลเหล่านั้นยังคงเป็นศูนย์ ดังนั้นเครื่องมือตรวจสอบที่เป็นไปตามมาตรฐานทุกตัวจึงปฏิเสธไฟล์นี้ เอกสารประทับเวลา PAdES B-LTA ซึ่งใช้รูปแบบพจนานุกรมเดียวกันก็ล้มเหลวในลักษณะเดียวกัน — ซึ่งเกี่ยวข้องหากคุณพัฒนาต่อยอดบนลายเซ็นระยะยาว B-LT และ B-LTA เวอร์ชัน 2.14.1 จะเติมข้อมูลกลับโดยเริ่มจากจุดเริ่มต้นของวัตถุลายเซ็น และการแก้ไขนี้ครอบคลุมโดยการทดสอบการถดถอย (regression test) ซึ่งแยกวิเคราะห์ไฟล์ที่สร้างขึ้นและยืนยันการคำนวณทางคณิตศาสตร์ด้านล่าง
function SignedByteRangeCoversFile(const FileName: string): Boolean;
var
Raw: TBytes;
Text: AnsiString;
P, N: Integer;
F: array[0..3] of Int64;
begin
Raw := TFile.ReadAllBytes(FileName);
SetString(Text, PAnsiChar(@Raw[0]), Length(Raw));
P := Pos('/ByteRange', Text); // first signature only
Result := P > 0;
if not Result then Exit;
Inc(P, Length('/ByteRange'));
for N := 0 to 3 do
begin
while (P <= Length(Text)) and not (Text[P] in ['0'..'9']) do Inc(P);
F[N] := 0;
while (P <= Length(Text)) and (Text[P] in ['0'..'9']) do
begin
F[N] := F[N] * 10 + Ord(Text[P]) - Ord('0');
Inc(P);
end;
end;
// EN 319 142-1 §6.3 req k: spans cover everything except /Contents
Result := (F[0] = 0) and (F[2] >= F[0] + F[1]) and
(F[2] + F[3] = Int64(Length(Raw)));
end;
โค้ด RTL ทั่วไปเพียงยี่สิบบรรทัด โดยไม่มีการเรียกใช้ไลบรารีภายนอก และมันสามารถจับคลาสความล้มเหลวทั้งหมดได้ ณ จุดที่ผลิตงาน หากคุณจะเก็บการยืนยัน (assertion) ข้อใดข้อหนึ่งจากบทความนี้ไว้ โปรดเก็บข้อนี้ไว้: แยกวิเคราะห์ผลลัพธ์ที่ลงนามของคุณเองและตรวจสอบความเท่ากันทั้งสามประการก่อนที่ไฟล์จะออกจากกระบวนการของคุณ
ทำไมเครื่องมือตรวจสอบจึงตั้งธงว่าเวลาลงนาม /M ผิดรูปแบบ?
เครื่องมือตรวจสอบที่เข้มงวดจะปฏิเสธเวลาลงนามที่ไม่ใช่สตริงวันที่ PDF ที่สมบูรณ์ และส่วนที่ขาดหายไปบ่อยที่สุดสองส่วนคือคำนำหน้า D: และเครื่องหมายระบุเวลา UTC ส่วน §7.9.4 ของ ISO 32000-1 กำหนดรูปแบบวันที่เป็น D:YYYYMMDDHHmmSS ตามด้วยออฟเซ็ต — Z สำหรับ UTC หรือความสัมพันธ์ +HH'mm' ที่มีเครื่องหมายกำกับ ค่า 20260709143000 เปล่า ๆ จะได้รับการแยกวิเคราะห์เป็นวันที่สำหรับตัวอ่านที่ผ่อนปรน แต่เครื่องมือตรวจสอบที่ใช้หลักไวยากรณ์ตามตัวอักษรจะมองเห็นสตริงที่ผิดรูปแบบในฟิลด์รูปแบบบังคับและตั้งธงปฏิเสธลายเซ็น รุ่นของ PDFium Component ก่อน 2.14.4 เขียนรายการ /M ของ TPdf.SignPades และ SignPadesBytes ในรูปแบบเปลือยเปล่าเช่นนั้น; ตั้งแต่ 2.14.4 รายการจะเก็บคำนำหน้า D: และเครื่องหมาย Z ดังนั้นเวลาลงนามที่ประกาศจะอ่านว่า D:20260709143000Z
ข้อสังเกตในทางปฏิบัติสองประการแนบมากับฟิลด์นี้ ประการแรก เขียน UTC และระบุให้ชัดเจน: ประทับเวลาที่ไม่มีเครื่องหมายออฟเซ็ตจะบีบให้เครื่องมือตรวจสอบต้องคาดเดาความสัมพันธ์ของเขตเวลา และส่วน §7.9.4 ถือว่าออฟเซ็ตเป็นส่วนหนึ่งของรูปแบบข้อมูล ไม่ใช่ส่วนเสริมที่เลือกได้ ประการที่สอง จำไว้ว่า /M คืออะไร — มันคือเวลาที่ผู้ลงนามประกาศ ซึ่งเป็นการอ้างสิทธิ์ไม่ใช่หลักฐานการพิสูจน์ เครื่องมือตรวจสอบตรวจสอบรูปแบบข้อมูล ณ จุดนี้ ไม่ใช่ความจริงของมัน; เวลาที่พิสูจน์ได้มาจากประทับเวลา RFC 3161 ในรุ่น PAdES B-T ขึ้นไป การจัดรูปแบบฟิลด์อย่างถูกต้องและการเชื่อถือมันเป็นการตัดสินใจที่แยกจากกัน และเครื่องมือตรวจสอบจะบังคับใช้เฉพาะข้อแรกเท่านั้น
ทำไม SignPades จึงยกเลิกการทำงานด้วยข้อยกเว้น EPadesCrypto บน PDF ที่เข้ารหัส?
PDFium Component ปฏิเสธที่จะลงนามในเอกสารที่เข้ารหัส เนื่องจากทางเลือกอื่นคือการสร้างไฟล์ที่โปรแกรมอ่านที่เป็นไปตามมาตรฐานจะทำลายเมื่อเปิด ลายเซ็น PAdES ถูกแนบเข้ามาเป็นการอัปเดตส่วนเพิ่ม และ ISO 32000-1 §7.5.6 กำหนดให้ส่วนท้ายใหม่ของส่วนอัปเดตต้องเก็บทุกรายการของส่วนท้ายก่อนหน้า ยกเว้น /Prev — ซึ่งในเอกสารที่เข้ารหัสจะรวมถึง /Encrypt ด้วย หากทิ้งมันไป ส่วนท้ายล่าสุดจะประกาศว่าไฟล์นั้นไม่ได้เข้ารหัส ดังนั้นโปรแกรมอ่านที่สอดคล้องกับมาตรฐานจะแยกวิเคราะห์เนื้อหาที่เข้ารหัสเสมือนเป็นข้อความธรรมดาและได้รับข้อมูลขยะ ที่แย่กว่านั้นคือ สตรีมและสตริงที่ผู้ลงนามแนบมาจะต้องได้รับการเข้ารหัสด้วยคีย์เอกสารเพื่อให้ถูกต้องตามกฎหมาย ซึ่งตัวฉีดลายเซ็นข้อความธรรมดาไม่สามารถดำเนินการได้ ไม่มีวิธีใดที่จะแนบลายเซ็นข้อความธรรมดาที่ถูกต้องเข้ากับไฟล์ที่เข้ารหัสได้ ดังนั้นตั้งแต่เวอร์ชัน 2.14.2 ฟังก์ชัน TPdf.SignPades, SignPadesBytes และ InjectPadesDssMarkers จะส่งข้อยกเว้น EPadesCrypto แทนที่จะปล่อยผลลัพธ์ที่เสียหายหรือไม่สามารถตรวจสอบได้ออกมา
try
if not Pdf.SignPades('contract-signed.pdf', AThumbprint) then
Writeln('Signing reported failure');
except
on E: EPadesCrypto do
begin
// e.g. 'SignPadesBytes: the source document is encrypted;
// remove encryption before signing'
Writeln('Cannot sign: ', E.Message);
end;
end;
ข้อยกเว้นคือผลลัพธ์ที่ถูกต้อง ดังนั้นโปรดออกแบบเวิร์กโฟลว์รอบเรื่องนี้แทนที่จะดักจับและลองใหม่ ถถอดรหัสก่อนด้วยสิทธิ์ของเจ้าของเอกสาร ลงนามในสำเนาข้อความธรรมดา และหากช่องทางการเผยแพร่ต้องการการเข้ารหัส ให้ยอมรับว่าการเข้ารหัสแล้วลงนามและการลงนามแล้วเข้ารหัสสร้างสิ่งประดิษฐ์ที่แตกต่างกันพร้อมเรื่องราวการตรวจสอบที่แตกต่างกัน ลายเซ็นที่คำนวณจากไบต์ข้อความธรรมดาไม่สามารถคงอยู่ได้เมื่อมีการเข้ารหัสไบต์เหล่านั้นซ้ำ ดังนั้นทางเลือกที่ตรงไปตรงมาคือไฟล์ข้อความธรรมดาที่ลงนามแล้ว หรือการตัดสินใจเชิงนโยบายที่จัดทำเป็นเอกสารไว้ถัดจากโค้ด
ทำไมบั๊กสองตัวจึงสามารถรับรองความถูกต้องซึ่งกันและกันได้
ข้อบกพร่องของ ByteRange ซ่อนตัวอยู่นานเนื่องจากเครื่องมือตรวจสอบของเราเองผิดพลาดในลักษณะที่เกื้อหนุนกัน และนั่นคือบทเรียนที่สามารถถ่ายทอดได้มากที่สุดในบทความนี้ การตรวจสอบความครอบคลุมใน ValidatePadesCompliance กำหนดให้ช่วงที่สองต้องเริ่มต้นที่ A+B พอดี — ช่องว่างเป็นศูนย์ — ซึ่งจำแนกประเภทเค้าโครงมาตรฐานผิดไป เนื่องจากช่องว่างดังกล่าวเป็นที่เก็บสตริงฐานสิบหก /Contents ดังนั้นเครื่องมือตรวจสอบภายในจึงปฏิเสธเค้าโครงที่เป็นไปตามมาตรฐาน และตัวสร้างภายในก็ไม่เคยผลิตเค้าโครงดังกล่าวเลย และไปป์ไลน์การลงนามแล้วตรวจสอบแบบต้นทางถึงปลายทางก็บังเอิญทำงานผ่านเกลียวสีเขียว บั๊กแต่ละตัวปฏิเสธไม่ให้ชุดทดสอบมีตัวอย่างที่ขัดแย้งซึ่งจะเปิดเผยบั๊กอีกตัวหนึ่ง เวอร์ชัน 2.14.1 แก้ไขทั้งสองฝั่งในการเปิดตัวเดียวกัน: ตัวสร้างจะเติมข้อมูลกลับทั้งสี่ฟิลด์ และตัวตรวจสอบยอมรับ C >= A+B โดยมี C+D เท่ากับความยาวไฟล์
การแก้ไขในเชิงระเบียบวิธีคือการตรวจสอบข้ามกับซอฟต์แวร์อื่นที่คุณไม่ได้เขียนขึ้นมา ตัวสร้างและตัวตรวจสอบที่แชร์ฐานโค้ดเดียวกัน ผู้เขียนคนเดียวกัน หรือแม้กระทั่งโมเดลความคิดในรูปแบบเดียวกัน สามารถเห็นพ้องกับความเข้าใจผิดร่วมกันได้อย่างไม่มีกำหนด; ตัวตรวจสอบที่เป็นอิสระจะทำลายความสมมาตรนั้น รันผลลัพธ์ที่ลงนามของคุณผ่านเครื่องมือตรวจสอบภายนอกอย่างน้อยหนึ่งตัวก่อนจะเปิดตัว และรักษาการตรวจสอบโครงสร้างตัวเองในการสร้างงานเป็นด่านแรกที่รวดเร็ว — ฟังก์ชัน TPdf.ValidatePades ซึ่งอธิบายไว้ในบทความตรวจสอบลายเซ็น จะรายงานความล้มเหลวของความครอบคลุมเป็นปัญหาที่ระบุชื่อ
var
R: TPadesValidationResult;
begin
Pdf.FileName := 'contract-signed.pdf';
Pdf.Active := True;
R := Pdf.ValidatePades;
if ppeiByteRangeNotCoveringFile in R.Issues then
Writeln('ByteRange does not cover the file');
if not R.IsCompliant then
Writeln('Structural issues present: do not ship this file');
end;
รายการตรวจสอบสำหรับการปฏิเสธผลลัพธ์ที่ลงนาม
เมื่อแพลตฟอร์มปฏิเสธลายเซ็น PAdES ของคุณ ให้ตรวจสอบสาเหตุทางโครงสร้างก่อนที่จะสงสัยเกี่ยวกับใบรับรองหรือห่วงโซ่ความเชื่อถือ อ่านอาร์เรย์ /ByteRange และยืนยันความเท่ากันสามประการ: ฟิลด์แรกเป็นศูนย์ ช่วงที่สองเริ่มต้นที่หรือหลังจุดสิ้นสุดของช่วงแรก และช่วงที่สองสิ้นสุดลงที่จุดสิ้นสุดของไฟล์พอดี อ่านรายการ /M และยืนยันว่าเป็นสตริงวันที่ตามส่วน §7.9.4 ที่สมบูรณ์พร้อมคำนำหน้า D: และเครื่องหมายออฟเซ็ต ยืนยันว่าเอกสารต้นทางไม่ได้ถูกเข้ารหัสเมื่อแนบลายเซ็น — และหากระบบของคุณลงนามไปอยู่ดีโดยไม่มีการท้วงติงใด ๆ ให้ถือว่าความเงียบนั้นเป็นบั๊กในระบบ การตรวจสอบทั้งสามรายการนี้ทำงานบนไบต์ดิบ in ระดับมิลลิวินาที และจากประสบการณ์ของเรา สิ่งเหล่านี้อธิบายการปฏิเสธได้บ่อยกว่าสาเหตุทางวิทยาการเข้ารหัสลับใด ๆ
การเรียกใช้การลงนาม การตรวจสอบ และการรับรองความถูกต้องที่ใช้ที่นี่ — SignPades, ValidatePades และการตรวจสอบความเข้ากันได้ของ PAdES พร้อมการคำนวณทางคณิตศาสตร์ ByteRange ที่แก้ไขแล้ว การจัดรูปแบบวันที่ และการตรวจสอบการเข้ารหัส — จัดส่งพร้อมกับ PDFium Component สำหรับ Delphi, C++Builder และ Lazarus