การตรวจสอบลายเซ็น PAdES หนึ่งลายเซ็นหมายถึงการตรวจสามเรื่องที่เป็นอิสระต่อกัน และเครื่องหมายถูกสีเขียวในโปรแกรมอ่านบอกคุณได้เพียงเรื่องที่สามเท่านั้น เรื่องแรก อาร์เรย์ /ByteRange ต้องครอบคลุมไบต์ที่ถูกต้อง ช่วงที่มันระบุไว้ต้องประกอบกลับขึ้นมาเป็นอินพุตชุดเดียวกันเป๊ะ ๆ กับที่ CMS digest ถูกคำนวณไว้ โดยไม่มีไบต์ที่ลงนามแล้วหลงเหลืออยู่นอกช่วงเหล่านั้น เรื่องที่สอง ใบรับรองภายใน CMS ต้องต่อสายไปถึงรากที่คุณเชื่อถือ และต้องพกแอตทริบิวต์ signing-certificate ที่ลงนามแล้วตามที่ PAdES กำหนด เรื่องที่สาม ถ้าโปรไฟล์อ้างว่ามี timestamp ก็ต้องมีโทเคน RFC 3161 ผูกค่าลายเซ็นไว้กับจุดเวลาก่อนที่ใบรับรองจะหมดอายุ Acrobat ยุบทั้งสามเรื่องนี้ให้เหลือไอคอนเดียว ขณะที่ตัวตรวจความสอดคล้องแยกทั้งสามออกจากกัน และโค้ดที่ผลิตไฟล์เหล่านี้ก็ควรทำเช่นเดียวกัน losLab PDF Library (PDF Library for Delphi) ให้คุณทั้งฝั่งการลงลายเซ็น การฝัง timestamp กลับเข้าไป และการเรียกเพื่อตรวจสอบ ByteRange ก่อนที่คุณจะเชื่อถือมัน
มีข้อแยกแยะหนึ่งที่ทำให้การอิมพลีเมนต์ PAdES ครั้งแรกสะดุดแทบทุกราย จึงควรพูดไว้ก่อนจะถึงโค้ดใด ๆ ลายเซ็นที่เขียนด้วย /SubFilter /adbe.pkcs7.detached เป็นลายเซ็นตาม ISO 32000-1 §12.8 ที่สมบูรณ์ทุกประการและ Acrobat จะรายงานว่าใช้ได้ แต่มันก็ไม่ใช่ลายเซ็น PAdES เพราะ ETSI EN 319 142-1 กำหนดให้ใช้ ETSI.CAdES.detached ในทุกระดับ baseline ตัวตรวจความสอดคล้องตาม eIDAS จะปฏิเสธแบบแรกและรับแบบหลัง ทั้งที่ส่วนวิทยาการรหัสลับเหมือนกันทุกประการ โปรไฟล์คือสิ่งที่เอกสารประกาศเกี่ยวกับตัวเอง และการทำให้คำประกาศนั้นถูกต้องก็คือการเรียกเพียงครั้งเดียวใน PDF Library for Delphi
อะไรที่เปลี่ยนลายเซ็น PDF ให้กลายเป็นลายเซ็น PAdES
ETSI EN 319 142-1 กำหนดระดับ baseline ไว้สี่ระดับ ซ้อนอยู่บนรูปแบบ CMS ระดับ PAdES-B-B คือจุดเริ่มต้น ได้แก่ลายเซ็น CAdES ในฟิลด์ลายเซ็นของ PDF ที่มี SubFilter เป็น ETSI.CAdES.detached พร้อมแอตทริบิวต์ signing-certificate ที่ลงนามแล้ว ระดับ PAdES-B-T เพิ่ม timestamp ตาม RFC 3161 คร่อมค่าลายเซ็น เพื่อพิสูจน์ว่าลายเซ็นมีอยู่ก่อนจุดเวลาที่ไม่มีใครย้อนวันได้ ระดับ PAdES-B-LT ฝังใบรับรอง CRL และคำตอบ OCSP ที่จำเป็นต่อการตรวจสอบไว้ใน Document Security Store ไฟล์จึงยังตรวจสอบได้แม้หลังจาก CA ผู้ออกใบรับรองปลดระวางโครงสร้างพื้นฐานของตนไปแล้ว ส่วน PAdES-B-LTA ปิดยอดกองด้วย document timestamp ที่คุ้มครองหลักฐานสะสมทั้งหมดอีกชั้นเมื่ออัลกอริทึมอ่อนแอลงตามกาลเวลา
PDF Library for Delphi จับแนวคิดเหล่านี้ลงบน sign-process API ของตน ตัวระบุโปรไฟล์คือ SetSignProcessCustomSubFilter หากนโยบายของคุณต้องการ commitment-type indication (proof of origin, proof of approval หรือหนึ่งในตัวระบุอื่นของ ETSI ที่มีหมายเลข 1 ถึง 6) เรื่องนั้นทำผ่าน SetSignProcessCommitmentType ส่วนนโยบายลายเซ็นแบบระบุชัดแนบเข้าไปด้วย SetSignProcessSignaturePolicy ซึ่งรับ OID ของนโยบายพร้อมค่า digest ของมัน มีค่าเริ่มต้นหนึ่งค่าที่ควรใส่ใจ เมื่อปล่อยอัลกอริทึม digest ไว้ที่โหมดอัตโนมัติ ไลบรารีจะเลือก SHA-256 สำหรับลายเซ็นแบบ ETSI และ adbe.pkcs7.detached และถอยไปใช้ SHA-1 เฉพาะบนเส้นทางเก่า adbe.pkcs7.sha1 เท่านั้น ถึงอย่างนั้นก็ควรกำหนดค่าให้ชัดเจนอยู่ดี ผู้ตรวจสอบจะถามว่าคุณใช้แฮชตัวใด และค่าที่ระบุไว้ชัดในโค้ดย่อมปกป้องได้ง่ายกว่าค่าเริ่มต้นที่คุณต้องไปเปิดคู่มืออ่านเพื่อมาอธิบาย
การสร้างลายเซ็นระดับ baseline
flat API ขับเคลื่อนการลงลายเซ็นเหมือนเครื่องสถานะแบบยิงครั้งเดียว คือเปิดกระบวนการบนไฟล์ต้นทาง ตั้งค่า ปิดท้ายลงไฟล์เอาต์พุต แล้วอ่านรหัสผลลัพธ์ ลำดับด้านล่างสร้างลายเซ็น PAdES-B-B ด้วย SHA-256 บรรทัดที่สำคัญที่สุดกลับไม่เกี่ยวกับตัวลายเซ็นเลย มันคือการจอง /Contents ไว้ใหญ่เกินตัวโดยตั้งใจ เพราะนั่นคือสิ่งเดียวที่คุณเปลี่ยนภายหลังไม่ได้ หากวันหนึ่งต้องเพิ่ม timestamp ให้ลายเซ็นนี้
var
Pdf: TPDFlib;
SignId: Integer;
begin
Pdf := TPDFlib.Create;
try
SignId := Pdf.NewSignProcessFromFile('invoice.pdf', '');
if SignId = 0 then
raise Exception.Create('cannot open source PDF');
Pdf.SetSignProcessField(SignId, 'Sig1');
Pdf.SetSignProcessPFXFromFile(SignId, 'company.pfx', PfxPassword);
Pdf.SetSignProcessInfo(SignId, 'Approved', 'Vienna', 'billing@example.com');
Pdf.SetSignProcessCustomSubFilter(SignId, 'ETSI.CAdES.detached');
Pdf.SetSignProcessDigestAlgorithm(SignId, 2); // SHA-256
Pdf.SetSignProcessReserveContentsBytes(SignId, 8192); // เผื่อที่ไว้ให้ timestamp ภายหลัง
Pdf.EndSignProcessToFile(SignId, 'invoice-signed.pdf');
if Pdf.GetSignProcessResult(SignId) <> 1 then
raise Exception.CreateFmt('signing failed, code %d',
[Pdf.GetSignProcessResult(SignId)]);
Pdf.ReleaseSignProcess(SignId);
finally
Pdf.Free;
end;
end;
NewSignProcessFromFile คืนค่า 0 เมื่อเปิดไฟล์ต้นทางไม่ได้เลย หลังจากนั้น GetSignProcessResult จะแยกโหมดความล้มเหลวที่เกิดขึ้นจริงในระบบใช้งานจริงออกจากกัน โดย 4 หมายถึงรหัสผ่าน PDF ผิด 7 คือรหัสผ่าน PFX ผิด 9 คือไฟล์ใบรับรองที่ไม่มีคีย์ส่วนตัว 10 คือพาธเอาต์พุตที่เขียนไม่ได้ และ 11 คือความล้มเหลวขณะลงไบต์ของลายเซ็น การบันทึกรหัสตัวเลขไว้ข้างชื่อไฟล์อินพุตเปลี่ยนทิกเก็ตสนับสนุนที่คลุมเครือให้กลายเป็นการวินิจฉัยภายในหนึ่งนาที
การเพิ่ม timestamp ตาม RFC 3161 ที่ไลบรารีจะไม่ไปดึงมาให้คุณ
PDF Library for Delphi ไม่ได้มาพร้อมไคลเอนต์ TSA และนั่นเป็นการขีดขอบเขตโดยตั้งใจ ไม่ใช่ช่องโหว่ ไลบรารีคำนวณแฮชที่หน่วยงานออก timestamp ต้องลงนามกำกับ และฝัง CMS ฉบับเสริมกลับเข้าไปให้ในภายหลัง ส่วนการแลกเปลี่ยนผ่าน HTTP และการผ่าตัด CMS ที่อยู่ตรงกลางเป็นหน้าที่ของผู้เรียก มีเหตุผลทางเทคนิคที่หนักแน่นรองรับการแบ่งเช่นนี้ ตัวควบคุมของ Windows CryptoAPI ที่ควรจะใช้เพิ่มแอตทริบิวต์แบบไม่ลงนาม คือ CMSG_CTRL_ADD_SIGNER_UNAUTH_ATTR ล้มเหลวด้วย CRYPT_E_INVALID_INDEX บนโครงสร้าง SignedData แบบ detached ที่ PAdES ใช้ ดังนั้น CMS ฉบับเสริมจึงต้องมาจากตัวเข้ารหัส CMS ที่อยู่ใต้การควบคุมของคุณเอง ไม่มีไลบรารีใดพับโทเคนเข้าไปเงียบ ๆ ด้วยการเรียกระบบครั้งเดียวได้ และตัวใดที่อ้างว่าทำได้ ก็กำลังผ่าตัดอยู่ในที่ที่คุณมองไม่เห็น
var
Pdf: TPDFlib;
StsId: Integer;
HashHex, TstDer, TsAttr, AugmentedCms: AnsiString;
begin
Pdf := TPDFlib.Create;
try
StsId := Pdf.NewPAdESSignatureTimeStampProcessFromFile('invoice-signed.pdf', '');
Pdf.SetPAdESSignatureTimeStampField(StsId, 'Sig1');
Pdf.SetPAdESSignatureTimeStampDigestAlgorithm(StsId, 2);
HashHex := Pdf.GetPAdESSignatureValueHashHex(StsId);
// สองการเรียกด้านล่างเป็นโค้ดฝั่งแอปพลิเคชัน: HTTP POST ไปยัง TSA ของคุณ
// และการเข้ารหัส CMS ใหม่ที่แนบโทเคนเข้าไปเป็นแอตทริบิวต์แบบไม่ลงนาม
TstDer := RequestTimeStampToken(HashHex);
TsAttr := Pdf.BuildPAdESSignatureTimeStampAttribute(TstDer);
AugmentedCms := AttachUnsignedAttribute(Pdf.GetPAdESSignatureCMSBytes(StsId), TsAttr);
Pdf.SetPAdESSignatureCMSBytes(StsId, AugmentedCms);
Pdf.EndPAdESSignatureTimeStampProcessToFile(StsId, 'invoice-bt.pdf');
if Pdf.GetPAdESSignatureTimeStampProcessResult(StsId) <> 1 then
raise Exception.Create('timestamp embedding failed');
Pdf.ReleasePAdESSignatureTimeStampProcess(StsId);
finally
Pdf.Free;
end;
end;
จับตารหัสผลลัพธ์ตรงนี้ให้ดี 12 หมายถึงไม่มีฟิลด์ลายเซ็นชื่อนั้นอยู่ 11 คือแจง CMS ที่มีอยู่ไม่ได้ และ 13 คือ CMS ฉบับเสริมใหญ่เกินกว่าจะลงในที่ว่าง /Contents ที่จองไว้ รหัส 13 คือตัวที่เจ็บ เพราะทางแก้เดียวคือลงลายเซ็นใหม่ทั้งหมด โทเคน timestamp ทั่วไปพร้อมสายใบรับรองมีขนาดราว 4 ถึง 6 KB และการจองไว้ 8192 ไบต์ในขั้นตอน B-B ก็มีอยู่ด้วยเหตุผลนี้พอดี คือให้ขั้นตอนนี้มีที่ลง
การตรวจสอบเริ่มที่ ByteRange ไม่ใช่ที่สายใบรับรอง
เครื่องหมายถูกสีเขียวในโปรแกรมอ่านคือการตัดสินใจเรื่องความเชื่อถือเทียบกับคลังใบรับรองของเครื่องนั้น ไม่ใช่คำตัดสินเชิงโครงสร้างเกี่ยวกับตัวไฟล์ การตรวจสอบด้วยโปรแกรมควรเริ่มที่ระดับต่ำกว่านั้น ด้วยคำถามที่ incremental update ทำให้ละเอียดอ่อนขึ้น คือลายเซ็นแต่ละอันครอบคลุมไบต์ใดบ้างจริง ๆ ส่วนเสริมทุกอย่างที่พูดถึงในบทความนี้ ไม่ว่าจะเป็นลายเซ็นที่สอง DSS dictionary หรือ document timestamp ล้วนเข้ามาทาง incremental update และการอัปเดตแต่ละครั้งก็ต่อไบต์เพิ่มไว้นอก /ByteRange ของลายเซ็นก่อนหน้า ไบต์ที่ต่อเพิ่มเหล่านั้นชอบธรรมดี แต่ตัวตรวจสอบก็ยังต้องจำแนกมันเทียบกับนโยบายการแก้ไขของเอกสาร และระดับ DocMDP รายฟิลด์ที่นโยบายนั้นอาศัยอยู่ก็อ่านได้ด้วย GetSignatureDocMDPLevelByName
var
Doc: TPDFlibSignDoc;
Names: TStringList;
I: Integer;
B0, B1, B2, B3, FileSize: Int64;
begin
FileSize := TFile.GetSize('invoice-bt.pdf'); // ก่อน Open: SignDoc ถือ share lock ไว้
Doc := TPDFlibSignDoc.Create;
try
if not Doc.Open('invoice-bt.pdf', '', False) then
raise Exception.Create('cannot open for audit');
Names := TStringList.Create;
try
Doc.GetSignatureFieldNames(Names);
for I := 0 to Names.Count - 1 do
if Doc.GetSignatureValueObjNum(Names[I]) > 0 then // >0 หมายถึงลงนามแล้วจริง
begin
B0 := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 11)));
B1 := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 12)));
B2 := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 13)));
B3 := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 14)));
if (B0 = 0) and (B2 + B3 = FileSize) then
Writeln(Names[I], ': covers the file to EOF')
else
Writeln(Names[I], ': earlier revision, or unexpected ByteRange layout');
end;
finally
Names.Free;
end;
Doc.Close;
finally
Doc.Free;
end;
end;
มีกับดักสองอย่างอยู่ในเส้นทางการตรวจสอบนี้ TPDFlibSignDoc.Open ถือไฟล์ไว้ด้วย share lock แบบผูกขาด ตัวตรวจสอบที่ต้องการแฮชไบต์ดิบของไฟล์เพื่อยืนยัน CMS ด้วยจึงต้องอ่านไฟล์เข้าหน่วยความจำก่อนจะเปิดมันเพื่อตรวจสอบ หากสลับลำดับนี้ การอ่านจะล้มเหลวเพราะล็อกที่คุณตั้งขึ้นเอง กับดักที่สองเงียบมากกว่าจะดัง คือคู่ขนานฝั่ง flat API อย่าง GetSignProcessByteRange คืนค่าเป็น Integer ขณะที่ offset เบื้องหลังเป็น Int64 เมื่อเลย 2 GB ไปแล้วการเรียกแบบ flat จึงตัดค่าทิ้งโดยไม่บ่นสักคำ ซึ่งเป็นเหตุผลที่ตัวอย่างนี้ดึง offset ผ่านคลาสตรวจสอบแทน มีสิ่งที่ขาดหายไปอย่างหนึ่งที่ควรเอ่ยถึงด้วย เลเยอร์ flat ไม่มีตัวห่อ VerifySignature เลยแม้แต่น้อย คำตัดสินเชิงวิทยาการรหัสลับมาจาก TPDFlibSignatureVerifier ที่ระดับคลาส ซึ่งคืนค่า vsValid, vsInvalid หรือ vsUnknown หรือไม่ก็มาจากตัวตรวจสอบภายนอกที่นโยบายการปฏิบัติตามข้อกำหนดของคุณเชื่อถืออยู่แล้ว
การตรวจสอบระยะยาว: DSS, VRI และ document timestamp
PAdES-B-LT มีอยู่เพราะโครงสร้างพื้นฐานของการเพิกถอนใบรับรองก็มีวันตาย ETSI EN 319 142-1 §5.4.2.2 กำหนด Document Security Store ไว้ คือ dictionary ระดับเอกสารที่บรรจุใบรับรอง CRL และคำตอบ OCSP โดยจะทำดัชนีรายลายเซ็นผ่านรายการ VRI ที่ใช้แฮชของ /Contents ของแต่ละลายเซ็นเป็นคีย์ก็ได้ กระแสงานใน PDF Library for Delphi สะท้อนการออกแบบของ timestamp NewPAdESDSSProcessFromFile เปิดกระบวนการ ส่วน AddPAdESDSSCertificate, AddPAdESDSSCRL และ AddPAdESDSSOCSP รับข้อมูล DER แบบ blob ขณะที่ AddPAdESDSSVRI ผูกวัตถุดิบที่เลือกไว้เข้ากับลายเซ็นหนึ่งอัน แล้ว EndPAdESDSSProcessToFile เขียนทุกอย่างลงไปเป็น incremental update ส่วนที่ยากยังคงอยู่ฝั่งคุณ การไปดึงวัตถุดิบการเพิกถอนมา และการตัดสินว่ามันสดใหม่พอจะคุ้มค่าฝังหรือไม่ เป็นงานของผู้เรียก ไลบรารีรับประกันว่า dictionary เหล่านั้นสอดคล้องกันเชิงโครงสร้าง แต่รับประกันไม่ได้ว่าตัวตอบ OCSP ของคุณพูดความจริง
ปลายทางฝั่งการจัดเก็บถาวรอย่าง B-LTA เพิ่ม document timestamp เข้ามา คือฟิลด์ลายเซ็นแยกต่างหากที่มีชนิดเป็น DocTimeStamp แทนที่จะเป็น Sig สร้างขึ้นผ่าน SetSignProcessDocTimeStamp พร้อมความยาวลายเซ็นที่จองไว้ มันไม่ได้มาแทนที่ signature timestamp จากขั้นตอน B-T โดย signature timestamp พิสูจน์ว่าลายเซ็นหนึ่งอันมีอยู่เมื่อใด ส่วน document timestamp คุ้มครองทั้งไฟล์รวมถึงหลักฐานใน DSS ด้วย และเป็นองค์ประกอบที่คลังเก็บระยะยาวจะต่ออายุทุกไม่กี่ปีเมื่ออัลกอริทึมอ่อนแอลง โปรไฟล์การจัดเก็บถาวรที่สมบูรณ์จะมีทั้งสองอย่าง สำหรับโปรแกรมอ่านที่เก่ากว่าโครงสร้างเหล่านี้ TPDFlibSignDoc.EnsurePAdESExtensions จะบันทึกส่วนขยายนักพัฒนา ESIC ไว้ในแค็ตตาล็อกของเอกสาร เป็นการประกาศว่าไฟล์นี้ใช้ฟีเจอร์ที่ ETSI กำหนดไว้
มีปฏิกิริยาหนึ่งต่อเรื่องทั้งหมดนี้ที่ควรตัดไฟแต่ต้นลม เพราะมันดูเหมือนบั๊กแต่ไม่ใช่ โปรแกรมอ่านมักรายงานว่า "ไม่ทราบความถูกต้อง" กับไฟล์ที่มีโครงสร้าง PAdES ถูกต้องครบถ้วน ความเชื่อถือกับโครงสร้างเป็นคนละแกนกัน โปรแกรมอ่านเพียงแต่ต่อสายจากผู้ลงนามไปยังรากที่มันเชื่อถือบนเครื่องนั้นไม่ได้ ซึ่งเป็นเรื่องปกติกับ CA ส่วนตัวและใบรับรองสำหรับทดสอบ แม้ว่าการตรวจสอบ ByteRange และการยืนยัน CMS จะผ่านทั้งคู่ก็ตาม ทางแก้คือกระจายใบรับรองรากให้ถูกต้อง หรือประเมินเทียบกับ trusted list ของสหภาพยุโรปเมื่อเป้าหมายจริงคือสถานะ qualified ตาม eIDAS ไม่ใช่การไปแก้โค้ดลงลายเซ็น
สำหรับมุมมองฝั่งการตรวจสอบ อันหมายถึงการไล่รายชื่อฟิลด์ลายเซ็นทั้งคลังเอกสาร การถ่ายผัง ByteRange ออกมา และการอ่านระดับ DocMDP เป็นชุด ดูได้ที่บทความคู่กันว่าด้วยเวิร์กเบนช์การปฏิบัติตามข้อกำหนดและการลงลายเซ็น ส่วนเอกสารที่ลงลายเซ็นแล้วและต้องผ่านนโยบายการจัดเก็บถาวรด้วย เป็นของเวิร์กโฟลว์ที่อธิบายไว้ในการพรีไฟลต์ PDF/A และ PDF/UA ใน Delphi เอกสาร API ฉบับเต็มและไฟล์ดาวน์โหลดสำหรับประเมินอยู่ที่หน้าผลิตภัณฑ์ losLab PDF Library for Delphi