คุณได้รับไฟล์ PDF ที่เซ็นชื่อแล้ว และจำเป็นต้องแสดงผลในโปรแกรมเปิดอ่านของคุณว่าใครเป็นผู้เซ็น, เซ็นเมื่อใด, ลายเซ็นนั้นครอบคลุมไฟล์ทั้งหมดหรือไม่ และสอดคล้องตามมาตรฐานการใช้งานระยะยาวระดับใด PDFium Component สำหรับ Delphi และ Lazarus สามารถตอบคำถามทั้งสี่ข้อนี้ได้ด้วยฟังก์ชันการอ่านอย่างเดียว: เมธอด FPDF_GetSignatureCount และฟังก์ชันในตระกูล FPDFSignatureObj_* จะทำหน้าที่เปิดข้อมูลพจนานุกรมของลายเซ็น และ TPdf.ValidatePades จะจัดประเภทระดับมาตรฐานของ PAdES นี่คือบทความแรกจากซีรีส์สามตอนเกี่ยวกับการจัดการลายเซ็น PDF ด้วย PDFium ส่วนบทความอีกสองตอนที่เหลือจะนำเสนอขั้นตอนการสร้างลายเซ็นระดับ B-B และการเพิ่มการระบุเวลาใช้งานระยะยาว อย่างไรก็ตาม มีขอบเขตข้อจำกัดประการหนึ่งที่ต้องชี้แจงก่อน: นั่นคือเนื้อหาทั้งหมดในบทความนี้จะเน้นไปที่ขั้นตอนการตรวจสอบ และการอ่านสิ่งที่ลายเซ็นระบุไว้นั้นเป็นงานคนละส่วนกับการยืนยันความถูกต้องของคริปโตกราฟีหรือการตัดสินใจว่าคุณจะไว้วางใจผู้เซ็นชื่อหรือไม่
สิ่งนี้มีความสำคัญเนื่องจากไฟล์ DLL ดั้งเดิมสามารถล้มเหลวได้ในสามรูปแบบที่แตกต่างกัน และข้อความดิบจากระบบปฏิบัติการมักจะนำสาเหตุทั้งหมดมารวมกัน สถาปัตยกรรมอาจจะไม่ถูกต้อง, ไฟล์ไบนารีที่จัดส่งอาจจะเก่าเกินไปสำหรับตัวอ้างอิง Pascal ที่เรียกใช้ หรือเกิดปัญหา Race Condition ระหว่างเธรดสองเธรดที่พยายามโหลดและผูกไฟล์ในเวลาเดียวกัน แต่ละสาเหตุมีแนวทางแก้ไขที่แตกต่างกัน และจุดประสงค์ของการเพิ่มระบบวินิจฉัยในการปรับปรุงรอบการโหลดเวอร์ชัน v2.11.0 คือการชี้แจงให้คุณทราบอย่างชัดเจนว่าคุณกำลังเผชิญกับปัญหาใดอยู่
เหตุใดพจนานุกรมลายเซ็น PDF จึงไม่ใช่แค่ข้อมูลไบต์ชุดหนึ่ง
ลายเซ็น PDF คือระเบียนพจนานุกรม ไม่ใช่เพียงไฟล์แนบปกติ และรายการข้อมูลที่สำคัญที่สุดสองรายการจะทำหน้าที่แจ้งให้ทราบว่าเอกสารได้รับการปกป้องอยู่มากน้อยเพียงใด ข้อกำหนด ISO 32000-1 §12.8 กำหนดโครงสร้างพจนานุกรมของลายเซ็นโดยมีรายการ /ByteRange และ /Contents โดย /Contents จะเก็บโครงสร้างข้อมูล CMS SignedData ที่เข้ารหัสแบบฐานสิบหก (hex-encoded) ตามข้อกำหนด RFC 5652 ซึ่งเป็นโครงสร้างคริปโตกราฟีที่เก็บใบรับรองของผู้เซ็น, แอตทริบิวต์ที่เซ็นชื่อ และค่าลายเซ็นเอง ส่วน /ByteRange คือส่วนที่นักพัฒนามักประเมินค่าต่ำเกินไป: ซึ่งเป็นอาร์เรย์ที่เก็บช่วงความยาวและออฟเซตสองช่วง ซึ่งรวมกันจะครอบคลุมเนื้อหาไฟล์ทั้งหมด ยกเว้นเพียงส่วนสตริงฐานสิบหก /Contents ช่องว่างนั้นคือตำแหน่งที่ข้อมูลลายเซ็นเก็บอยู่ และช่วงข้อมูลทั้งสองข้างคือส่วนเนื้อหาที่ลายเซ็นรับรองอย่างถูกต้อง
การออกแบบ ByteRange คือสิ่งที่ทำให้การบันทึกแบบเพิ่มพูน (incremental save) สามารถตรวจสอบได้ เนื่องจากผู้เซ็นจะไม่สามารถคำนวณแฮชของไบต์ลายเซ็นที่ยังไม่เกิดขึ้นได้ ไฟล์จึงถูกแยกส่วนรอบๆ พื้นที่ตัวแทน /Contents และส่วนที่เหลือทั้งหมดจะถูกนำไปคำนวณแฮชเพื่อสร้างลายเซ็น ลายเซ็นที่มีขอบเขต ByteRange ไม่ครอบคลุมไปถึงท้ายไฟล์เป็นสัญญาณเตือนภัย: เนื้อหาที่เขียนเพิ่มหลังขอบเขตที่รับรองผ่านการบันทึกแบบเพิ่มพูนในภายหลัง จะไม่ส่งผลให้ลายเซ็นเกิดความเสียหาย แม้ว่าเนื้อหานั้นจะเปลี่ยนสิ่งที่ผู้อ่านเห็นก็ตาม ดังนั้นสิ่งแรกที่ผู้ตรวจสอบที่รอบคอบควรตรวจหาจึงไม่ใช่ว่าใครเป็นผู้เซ็น แต่คือการตรวจสอบว่าขอบเขตลายเซ็นครอบคลุมไบต์ข้อมูลที่รับรองครบถ้วนหรือไม่
การอ่านพจนานุกรมลายเซ็นด้วย API การอ่านอย่างเดียวของ PDFium
PDFium Component จะแสดงข้อมูลพจนานุกรมลายเซ็นผ่านสมาชิกแบบอ่านอย่างเดียวสองตัว ได้แก่: SignatureCount และระเบียน Signature[Index] เบื้องหลังการทำงานจะส่งไปเรียก FPDF_GetSignatureCount, FPDF_GetSignatureObject และฟังก์ชันเข้าถึง FPDFSignatureObj_* สำหรับรายการ /SubFilter, /ByteRange, /Contents, /Reason และเวลาเซ็นชื่อ คำสำคัญคือ "อ่านอย่างเดียว": PDFium สามารถระบุและอ่านข้อมูลลายเซ็นได้แต่ไม่มีฟังก์ชัน API เพื่อสร้างหรือเขียนลายเซ็นใหม่ ซึ่งเป็นเหตุผลที่ขั้นตอนการเซ็นชื่อในซีรีส์นี้จะถูกพัฒนาจากตัวไลบรารีเองไม่ใช่จาก PDFium
var
Pdf: TPdf;
i: Integer;
Sig: TPdfSignature;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'contract-signed.pdf';
Pdf.Active := True;
for i := 0 to Pdf.SignatureCount - 1 do
begin
Sig := Pdf.Signature[i];
Writeln('SubFilter : ', Sig.Encoding); // ETSI.CAdES.detached, adbe.pkcs7.detached, ...
Writeln('Signed at : ', Sig.Time); // signer date string, e.g. D:20260708120000+02'00'
Writeln('Reason : ', Sig.Reason);
Writeln('CMS length: ', Length(Sig.Content)); // raw DER SignedData taken from /Contents
Writeln('DocMDP : ', Sig.Permission); // 0 = no certification, 1..3 = MDP level
end;
finally
Pdf.Free;
end;
end;
ระเบียน TPdfSignatureแต่ละรายการจะจับคู่กับข้อมูลพจนานุกรมโดยตรง แอตทริบิวต์ Encoding คือค่า /SubFilter ซึ่งเป็นฟิลด์วินิจฉัยที่สำคัญที่สุด เนื่องจากระบุชื่อตัวจัดการลายเซ็นและแยกแยะลายเซ็นแบบ ETSI.CAdES.detached สมัยใหม่ออกจากลายเซ็นรุ่นเก่าหรือลายเซ็นต้องห้ามได้ทันที Time คือเวลาเซ็นชื่อที่ระบุโดยผู้เขียนในรูปแบบสตริงวันที่ของ PDF ซึ่งเป็นเพียงคำกล่าวอ้างของผู้เซ็นไม่ใช่เวลาที่เชื่อถือได้ Content คือข้อมูล CMS SignedData ดิบ และ Permission ระดับการรับรอง DocMDP (0 สำหรับลายเซ็นอนุมัติทั่วไป, 1 ถึง 3 สำหรับลายเซ็นการรับรองที่จำกัดการเปลี่ยนแปลงในภายหลัง) ฟิลด์หนึ่งที่ระเบียนไม่ได้แสดงออกมาคือข้อมูล ByteRange ที่วิเคราะห์แล้ว และการละเว้นนี้เป็นความตั้งใจ เนื่องจากเมธอด ValidatePades จะทำหน้าที่คำนวณความครอบคลุมของ ByteRange ให้คุณแล้วโดยไม่ต้องเสียเวลาคำนวณเอง
ความแตกต่างระหว่าง PAdES B-B, B-T, B-LT และ B-LTA คืออะไร?
ระดับมาตรฐาน PAdES ทั้งสี่ระดับเปรียบเสมือนบันไดขั้นต่างๆ จากลายเซ็นที่ถูกต้องขั้นต่ำไปจนถึงระดับโครงสร้างที่ออกแบบมาเพื่อให้คงอยู่สำหรับงานจดหมายเหตุหลายสิบปี และระดับที่สูงกว่าจะครอบคลุมคุณสมบัติระดับที่ต่ำกว่าทั้งหมด ข้อกำหนด ETSI EN 319 142-1 กำหนดระดับเหล่านั้นเป็น B-B, B-T, B-LT และ B-LTA ระดับ B-B (Basic) คือลายเซ็นพร้อมด้วยแอตทริบิวต์บังคับและไม่มีข้อมูลอื่นเพิ่มเติม ระดับ B-T (Timestamp) จะเพิ่มข้อมูลการระบุเวลา (timestamp) ที่เชื่อถือได้ตามข้อกำหนด RFC 3161 เหนือลายเซ็น เพื่อให้เวลาที่เซ็นชื่อได้รับการรับรองจากหน่วยงานรับรองเวลาภายนอก แทนที่จะเป็นเวลาจากนาฬิกาของผู้เซ็น ระดับ B-LT (Long-Term) จะฝังข้อมูลยืนยันความถูกต้อง — ได้แก่ ห่วงโซ่ใบรับรอง และตัวเลือกคำตอบ OCSP หรือ CRL — ไว้ภายในไฟล์ เพื่อให้ยังคงสามารถตรวจสอบความถูกต้องของลายเซ็นได้ในหลายปีถัดไปเมื่อผู้ให้บริการออกใบรับรองเดิมปิดตัวลงแล้ว ส่วนระดับ B-LTA (Long-Term with Archive timestamp) จะหุ้มข้อมูลยืนยันทั้งหมดด้วยการระบุเวลาเอกสาร (document timestamp) เพื่อปกป้องข้อมูลระยะยาวและช่วยให้คุณสามารถระบุเวลาใหม่ได้ก่อนที่ระบบคริปโตกราฟีจะเก่าเกินไป
การทำความเข้าใจในทางปฏิบัติเกี่ยวข้องกับระยะเวลาการใช้งาน ลายเซ็น B-B จะตอบคำถามว่า "มีผู้เซ็นเอกสารนี้หรือไม่" ลายเซ็น B-T จะตอบว่า "เซ็นเมื่อใดแบบพิสูจน์ได้" ลายเซ็น B-LT ตอบว่า "ยังตรวจสอบลายเซ็นได้หรือไม่หลังจากใบรับรองหมดอายุ" และลายเซ็น B-LTA ตอบว่า "การตรวจสอบนั้นจะยังคงใช้ได้ในอีก 20 ปีข้างหน้าหรือไม่" ข้อกำหนดทางกฎหมายในแต่ละกรณีจะเลือกระดับที่ต้องการ: เช่น โครงสร้างใบกำกับภาษีอิเล็กทรอนิกส์ (e-invoicing) และ eIDAS ส่วนมากต้องการขั้นต่ำระดับ B-T และข้อกำหนดเรื่องงานจดหมายเหตุต้องการระดับ B-LT หรือ B-LTA การทราบว่าเอกสารผ่านเกณฑ์ระดับใดก่อนที่คุณจะกดยอมรับหรือปฏิเสธ คือเป้าหมายของการตรวจสอบความถูกต้อง
การตรวจหาระดับมาตรฐานด้วย TPdf.ValidatePades
PDFium Component ช่วยให้การตรวจสอบระดับเป็นเรื่องง่ายด้วยการเรียกใช้งานเพียงครั้งเดียว เมธอด TPdf.ValidatePades จะส่งคืนระเบียน TPadesValidationResult ที่มีฟิลด์ Level เป็นประเภท TPadesLevel — ได้แก่ plNone, plUnknown, plB_B, plB_T, plB_LT หรือ plB_LTA — ควบคู่ไปกับกลุ่มปัญหาการทำงาน, จำนวนลายเซ็น และจำนวนการระบุเวลาเอกสาร ระดับจะได้รับการประเมินตามลำดับ: ตัวตรวจสอบจะยืนยัน B-B ก่อน จากนั้นขยับเป็น B-T หากตรวจพบข้อมูลระบุเวลาลายเซ็นหรือเอกสาร และขยับเป็น B-LT หากแค็ตตาล็อกเก็บค่า /DSS พร้อมใบรับรองและสัญลักษณ์ /Extensions /ESIC ระดับ 1 และขยับเป็น B-LTA หากตรวจพบระบุเวลาเอกสารและสัญลักษณ์ ESIC ระดับ 2 ร่วมกัน มีตัวช่วยสองตัวเพื่ออำนวยความสะดวก: IsCompliant จะเป็นจริงเมื่อระดับแตะขั้นต่ำ B-B และไม่มีปัญหาใดๆ และ IsCompliantAt ช่วยให้คุณตรวจสอบระดับขั้นต่ำตามนโยบาย เช่น plB_T ได้
เหตุใด adbe.pkcs7.sha1 จึงเป็น SubFilter ต้องห้าม?
เนื่องจากฟังก์ชัน SHA-1 มีความไม่ปลอดภัยและตัวจัดการ adbe.pkcs7.sha1 เรียกใช้งานคำสั่งนี้อย่างถาวร ตัว SubFilter นี้จะประมวลผลแฮชเอกสารล่วงหน้าด้วย SHA-1 ก่อนจัดเก็บในรูปแบบ PKCS#7 และฟังก์ชัน SHA-1 มีช่องโหว่เรื่องความขัดแย้งของแฮช (collision-vulnerable) มาหลายปีแล้ว ข้อกำหนด EN 319 142-1 ข้อ 6.3 จึงห้ามไม่ให้ใช้งานสัญลักษณ์นี้สำหรับลายเซ็นระดับมาตรฐานเด็ดขาด เมธอด ValidatePades จะแจ้งรหัสปัญหา ppeiForbiddenSubFilter เมื่อตรวจพบ adbe.pkcs7.sha1 หรือ adbe.x509.rsa_sha1 และแจ้ง ppeiBadDigestAlgorithm เมื่อตัวข้อมูล CMS เรียกใช้ MD5 หรือ SHA-1 เป็นขั้นตอนแฮชเนื้อหา (ข้อ 6.2.1) ซึ่งเป็นการตรวจสอบสองจุดที่ช่วยสกัดปัญหาความอ่อนแอแบบเดียวกันในส่วนการทำงานคนละชั้น
กลุ่มปัญหามีสมาชิกทั้งหมด 26 ตัว และปัญหาที่พบบ่อยจะเกี่ยวข้องกับโครงสร้างและความครอบคลุมข้อมูล ปัญหา ppeiByteRangeNotCoveringFile คือตัวตรวจสอบขอบเขตความครอบคลุมข้อมูลที่กล่าวไว้ก่อนหน้า ปัญหา ppeiForbiddenCertKey จะทำงานเมื่อพจนานุกรมลายเซ็นเก็บค่า /Cert ซึ่งมาตรฐาน PAdES ห้ามใช้งานเพราะห่วงโซ่ใบรับรองจะต้องจัดเก็บภายใน CMS SignedData.certificates เท่านั้น ส่วนปัญหา ppeiMissingSigningCertificate, ppeiMissingContentType และ ppeiMissingMessageDigest จะเตือนกรณีขาดแอตทริบิวต์บังคับ และ ppeiDetachedContentViolation ตรวจจับกรณีลายเซ็นฝังเนื้อหาที่เซ็นไว้แทนที่จะแยกออกมาต่างหาก การไล่ดูรายการปัญหาจะช่วยให้คุณบันทึกวินิจฉัยปัญหาได้อย่างละเอียด
var
R: TPadesValidationResult;
Issue: TPadesValidationIssue;
begin
R := Pdf.ValidatePades;
if R.Issues <> [] then
for Issue := Low(TPadesValidationIssue) to High(TPadesValidationIssue) do
if Issue in R.Issues then
Writeln('Issue: ',
GetEnumName(TypeInfo(TPadesValidationIssue), Ord(Issue)));
end;
สิ่งที่ ValidatePades ไม่ได้ตรวจสอบ
เมธอด ValidatePades ตรวจสอบเฉพาะโครงสร้าง ไม่ใช่การตรวจสอบความน่าเชื่อถือ และการสับสนสองเรื่องนี้เป็นข้อผิดพลาดที่เป็นอันตราย ผลลัพธ์ plB_LTA หมายความว่าเอกสารมีลายเซ็น B-LTA ที่ถูกต้องตามโครงสร้างพร้อมด้วยคุณสมบัติ, ข้อมูล และข้อมูลระบุเวลาในตำแหน่งที่ถูกต้อง — แต่ไม่ได้การันตีว่าลายเซ็นนั้นถูกต้องตามหลักคริปโตกราฟี, ห่วงโซ่ใบรับรองเชื่อมโยงกับใบรับรองรากที่คุณเชื่อถือ หรือไม่มีใบรับรองใดในห่วงโซ่ถูกยกเลิก ตัวตรวจสอบตั้งใจที่จะไม่ทำการยืนยันความถูกต้องของคริปโตกราฟี: ไม่มีการคำนวณลายเซ็นใหม่เหนือ ByteRange, ไม่มีการสร้างหรือประเมินห่วงโซ่ความน่าเชื่อถือ และไม่มีการตรวจสอบสถานะการเพิกถอน OCSP หรือ CRL การแบ่งงานนี้มีเหตุผลและเป็นประโยชน์ เพราะการตรวจสอบโครงสร้างทำงานได้รวดเร็ว ทำงานสอดคล้องกัน และไม่ต้องใช้คีย์, การเชื่อมต่อเครือข่าย หรือระบบคริปโตกราฟีของแพลตฟอร์ม ทำให้ ValidatePades รันงานได้เหมือนกันหมดบน Windows, Linux และ macOS ในรูปแบบโค้ด Pascal บริสุทธิ์เหนือไบต์ไฟล์ ในทางตรงกันข้าม การตรวจสอบห่วงโซ่ความน่าเชื่อถือไม่สามารถแยกออกจากนโยบายความปลอดภัยได้ — เช่น ใบรับรองรากใดที่คุณเชื่อถือ, คุณดึงข้อมูลการเพิกถอนอย่างไร, ความเข้มงวดของเวลาเป็นอย่างไร — และจัดอยู่ในขั้นตอนถัดไปที่ต้องพึ่งพาคลังเก็บใบรับรองของแพลตฟอร์ม ดังนั้นขอแนะนำให้พิจารณาว่าผลลัพธ์ผ่านของ ValidatePades คือเกตเวย์ด่านแรกว่าลายเซ็นมีโครงสร้างที่ถูกต้อง จากนั้นจึงส่งต่อลายเซ็นที่ผ่านด่านไปยังกระบวนการตรวจสอบความถูกต้องคริปโตกราฟีจริงก่อนที่คุณจะเชื่อถือข้อมูล
ขั้นตอนการตรวจสอบโครงสร้างคือจุดเริ่มต้นที่เหมาะสม และทำงานคู่ขนานกับตัวตรวจสอบแบบอ่านอย่างเดียวในหัวข้อ auditing PDF security risks with the PDFium Component และสอดคล้องกับการทำงานด้านรูปแบบเอกสาร เช่น validating print-ready PDF/X documents เมื่อคุณสามารถอ่านและจัดประเภทของลายเซ็นได้แล้ว ขั้นตอนถัดไปคือการสร้างลายเซ็นขึ้นมา: โดยบทความตอนที่สองจะนำเสนอหัวข้อ signing PDFs with PAdES B-B และบทความตอนที่สามจะขยายขอบเขตไปยังการระบุเวลาที่เชื่อถือได้และการระบุเวลาลายเซ็นระยะยาวระดับ B-LT และ B-LTA ระบบตรวจสอบลายเซ็นแบบอ่านอย่างเดียวและตัววิเคราะห์ ValidatePades ที่แสดง ณ ที่นี้เป็นส่วนหนึ่งของ PDFium Component สำหรับ Delphi, C++Builder และ Lazarus