บทความเทคนิค

Trusted List ของ eIDAS กับลายเซ็น PDF แบบ qualified

การตัดสินว่าลายเซ็น PDF เป็น qualified ตาม eIDAS หมายถึงการตอบคำถามที่ไม่เกี่ยวกับ cryptography เลย: ใบรับรองถูกออกโดย trust service ที่ประเทศสมาชิกจดทะเบียนไว้ว่า qualified หรือไม่ ณ ขณะที่ลายเซ็นเกิดขึ้น คำตอบอยู่ใน trusted list เอกสาร XML ที่เผยแพร่แยกรายดินแดน และคุณค่าทั้งหมดของเอกสารนั้นพึ่งความจริงแท้ของมัน component PDFium จึงปฏิเสธที่จะมองข้างในจนกว่าจะมีใครออกใบรับรองมัน TPdfEuropeanTrustedList.ParseAuthenticated ส่งไบต์ดิบทั้งหมดให้ IPdfTrustedListAuthenticator ที่ผู้เรียกส่งเข้ามาก่อนมันจะ parse service แม้แต่ตัวเดียว และมันสร้าง snapshot ขึ้นก็ต่อเมื่อ authenticator นั้นผ่านอย่างชัดเจน

ขั้นตอนพิสูจน์แท้ก่อน parse สำหรับ European trusted list ใน Delphi: XML ดิบจากการดึงสดหรือ snapshot ใน cache ผ่าน IPdfTrustedListAuthenticator ก่อนที่ TPdfEuropeanTrustedList จะ parse อะไร
รายการสดและรายการใน cache เจอ authenticator ตัวเดียวกัน และ snapshot มีอยู่ก็หลังมันผ่านเท่านั้น

ลำดับนั้นคือการออกแบบ ทุกอย่างอื่นในฟีเจอร์นี้ไหลตามมา รวมถึงส่วนที่ดูไม่สะดวกด้วย

parse ผ่านไม่ได้แปลว่าเชื่อถือได้

trusted list ที่ parse ผ่านสะอาดบอกคุณแค่ว่า XML สร้างถูกหลัก มันไม่บอกว่าใครเขียนมัน เมื่อรายการนี้คือฐานที่การตัดสินสถานะ qualified ทั้งหมดของคุณยืนอยู่ การยอมรับมันเพราะมัน parse ผ่านจะทำให้การตัดสินไร้ความหมาย: ผู้โจมตีที่แทนรายการได้ก็ประกาศ certificate authority ของตัวเองให้เป็น qualified ได้

การเหตุผลชุดเดียวกันใช้กับ cache และนี่คือกับดักที่สมควรตั้งชื่อ cache snapshot เก็บ XML ต้นฉบับพร้อม digest SHA-256 และมันจะง่ายมากถ้าจะถือ digest ที่ตรงกันตอนโหลดเป็นหลักฐานว่ารายการแท้ มันไม่ใช่ digest ที่ถูกคำนวณโดย process เดียวกับที่เก็บไฟล์ โดยไม่มี key เข้ามาเกี่ยวข้อง พิสูจน์ได้แค่ว่าไบต์ไม่เปลี่ยนนับแต่คุณเขียนมัน ถ้ารายการเป็นของปลอมอยู่แล้วตอนถูก cache digest ก็ยืนยันว่ามันคือรายการปลอมชุดเดิม การโหลด snapshot จาก cache จึงวิ่งผ่าน authenticator ตัวเดียวกับการ parse รายการสด ความคงเดิมกับความแท้เป็นคุณสมบัติต่างกัน และมีเพียงตัวเดียวที่ต้องใช้ key

uses
  FPdfTrustedList;

type
  TListAuthenticator = class(TInterfacedObject, IPdfTrustedListAuthenticator)
  public
    function Authenticate(const XmlData: TBytes;
      out AuthenticationDetails: string): Boolean;
  end;

function TListAuthenticator.Authenticate(const XmlData: TBytes;
  out AuthenticationDetails: string): Boolean;
begin
  // นโยบายของคุณอยู่ตรงนี้: ตรวจลายเซ็น XMLDSIG แบบ enveloped
  // กับใบรับรองผู้ลงนามรายการที่คุณ pin ไว้นอกช่องทางไฟล์
  // และบรรยายสิ่งที่คุณเช็กไว้สำหรับ audit trail
  Result := VerifyEnvelopedXmlSignature(XmlData, FPinnedListSigner);
  if Result then
    AuthenticationDetails := 'XMLDSIG verified against pinned LOTL signer';
end;

var
  List: TPdfEuropeanTrustedList;
  Cache: TFileStream;
begin
  List := TPdfEuropeanTrustedList.ParseAuthenticated(RawXml,
    TListAuthenticator.Create, TPdfTrustedListOptions.Default);
  // snapshot มีอยู่เพราะ authenticator ตอบตกลงเท่านั้น
  Cache := TFileStream.Create('tl-de.snapshot', fmCreate);
  try
    List.SaveCache(Cache);
  finally
    Cache.Free;
  end;
end;

validator ไม่ใช่เจ้าของนโยบายเครือข่าย

PAdES validator ไม่มีหน้าที่ตัดสินว่าจะไปถึงรายการ trusted lists อย่างไร จะผ่าน proxy หรือไม่ จะลองซ้ำกี่ครั้ง หรือจะทำอย่างไรเมื่อดินแดนหนึ่งติดต่อไม่ได้ พวกนั้นเป็นการตัดสินของแอปพลิเคชันและ deployment และในสภาพแวดล้อมที่มีกฎเกณฑ์กำกับ พวกมันมักถูก audit การอัปเดตจึงมาถึงผ่าน IPdfTrustedListSource ซึ่งได้รับ URI กับเพดานขนาดไบต์แล้วคืนไบต์กลับมา

สิ่งที่ component บังคับจริงคือ invariant ชุดที่ทำให้การอัปเดตเป็นการอัปเดต ไม่ใช่การแทนที่ Update เรียกใช้เงื่อนไขว่าดินแดนไม่เปลี่ยน เลขลำดับเพิ่มขึ้นอย่างเข้มงวด และเวลาออกไม่ไหลย้อนกลับ สามการเช็กนี้ทำลายการโจมตี downgrade ที่ชัดที่สุด: การเล่นซ้ำรายการเก่าที่ยังระบุ service ที่ถูกถอนไปแล้ว หรือการสลับเอารายการของดินแดนอื่นที่ service ของมันคุณไม่เคยตั้งใจจะเชื่อถือ

invariant ของการอัปเดตที่ component trusted list ของ PDFium บังคับ: ดินแดนไม่เปลี่ยน เลขลำดับเพิ่มขึ้นเข้มงวด และเวลาออกไม่ย้อนกลับ รวมกันบล็อกการโจมตี downgrade
สามการเช็กแบบ monotonic แยกรายการอัปเดตจริงออกจากรายการที่ถูกเล่นซ้ำหรือถูกแทนที่

ขีดจำกัดของ parser และการห้าม DTD เด็ดขาด

TPdfTrustedListOptions เพดานขนาด XML, จำนวน token, ความลึกการซ้อน, จำนวน service, จำนวนใบรับรอง และขนาดใบรับรองแต่ละใบ โดยมี class function Default ให้ค่าที่ใช้งานได้ trusted list เป็นเอกสารที่เผยแพร่และมีขนาดคาดเดาได้ การตั้งขอบเขตจึงแทบไม่มีต้นทุน และไม่มีรายการที่ชอบด้วยกฎหมายตัวใดที่ต้องการเกินขอบเหล่านั้น

แยกออกมาและไม่มีเงื่อนไข ตัว parser ปฏิเสธการประกาศ DTD และ entity สิ่งนี้ปิดทั้ง denial of service แบบ entity-expansion และเส้นทางเปิดเผยผ่าน external entity ในการปฏิเสธเดียว โดยไม่มีต้นทุน เพราะ trusted list ไม่ใช้ entity XML parser ตัวใดที่เข้าถึงได้จากอินพุตที่ไม่น่าเชื่อถือสมควรถูกตั้งค่าแบบนี้ ความต่างที่นี่คือการปฏิเสธไม่สามารถตั้งค่าได้ มันจึงถูกปิดด้วยการเปลี่ยน option ที่ตั้งใจดีแต่ทำให้ความปลอดภัยหมดไปไม่ได้

สถานะ qualified ถูกบันทึกเคียงข้างความเชื่อถือห่วงโซ่ ไม่ได้ถูกหลอมรวม

ฝั่งการประเมินถูกแยกไว้โดยตั้งใจ TPadesTrustValidationOptions.QualifiedTrustEvaluator รับ IPdfQualifiedTrustEvaluator ซึ่ง snapshot ของ trusted list เป็นผู้ implement ระหว่าง validation evaluator ได้รับใบรับรองใบปลาย ห่วงโซ่ และเวลาประเมิน จับคู่ใบรับรอง service ด้วยการเทียบ DER เป๊ะ ๆ กับผู้ลงนามและห่วงโซ่ รวมสถานะ service, identifier ชนิด service และ URI ของ qualifier ณ ช่วงเวลานั้น แล้วคืน record การประเมิน

ผลลัพธ์ลงสองที่บนลายเซ็นแต่ละตัว: QualifiedTrustStatus เป็นสถานะหยาบ และ QualifiedTrust เป็นการประเมินฉบับเต็มพร้อมดินแดน ชื่อ provider, ชื่อ service, identifier ชนิด, สถานะและเวลาเริ่มของสถานะ สิ่งที่มันไม่ทำคือการเปลี่ยน CertificateTrustStatus ความเชื่อถือห่วงโซ่ระดับระบบกับสถานะ qualified ตอบคนละคำถาม รายงานที่หลอมรวมสองอย่างนี้แยก "เชื่อถือได้แต่ไม่ qualified" ออกจาก "qualified แต่ห่วงโซ่ตรวจไม่ผ่าน" ไม่ได้ ทั้งคู่เป็นกรณีจริงและทั้งคู่ต้องการการจัดการต่างกัน

การประเมิน qualified trust แบบ PAdES ใน Delphi: IPdfQualifiedTrustEvaluator จับคู่ใบรับรอง service ด้วยการเทียบ DER เป๊ะและเติม QualifiedTrustStatus กับ QualifiedTrust ขณะที่ CertificateTrustStatus ไม่ถูกแตะ
ความเชื่อถือห่วงโซ่กับสถานะ qualified ตาม eIDAS ถูกบันทึกเคียงข้างกัน ข้อค้นพบทั้งสองจึงยังมองเห็น
var
  Options: TPadesTrustValidationOptions;
  Report: TPadesValidationResult;
  I: Integer;
begin
  Options := TPadesTrustValidationOptions.Default;
  Options.CheckRevocation := True;
  Options.QualifiedTrustEvaluator := List;      // snapshot ที่พิสูจน์แท้แล้ว
  Options.QualifiedValidationTime := SigningTime; // ไม่ใช่ Now
  Report := Pdf.ValidatePadesTrust(Options);

  for I := 0 to High(Report.Signatures) do
    if Report.Signatures[I].QualifiedTrustStatus = pcsValid then
      Writeln(Format('signature %d qualified by %s / %s (%s)',
        [I, Report.Signatures[I].QualifiedTrust.Territory,
         Report.Signatures[I].QualifiedTrust.ProviderName,
         Report.Signatures[I].QualifiedTrust.ServiceName]))
    else if Report.Signatures[I].QualifiedTrustStatus = pcsIndeterminate then
      // ไม่มี service ตรงกัน หรือ snapshot ตอบไม่ได้กับช่วงเวลานี้
      Writeln(Format('signature %d: qualified status undetermined', [I]));
end;

ทำไมเวลาประเมินไม่ใช่ตอนนี้

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

รายการแบกข้อมูลที่จำเป็นสำหรับเรื่องนี้: record service แต่ละตัวมีเวลาเริ่มสถานะและ flag ที่แยก entry ย้อนเวลาออกจาก entry ปัจจุบัน และ evaluator รวมพวกมันเทียบกับเวลาที่คุณส่งเข้าไป ในทางปฏิบัติเวลานั้นมาจาก trusted timestamp บนลายเซ็น ไม่ใช่เวลาลงนามที่อ้างไว้ใน CMS เหตุผลที่เนื้อหา long-term validation สำคัญแม้กับคำถามที่ดูเหมือนการค้นข้อมูลนโยบายธรรมดา ฝั่ง timestamp และ DSS อธิบายไว้ในบทความลายเซ็นระยะยาว

สิ่งที่คุณยังต้องสร้างเอง

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

สิ่งที่คุณได้จาก component คือส่วนที่ทำผิดแบบละเอียดอ่อนได้ง่าย: ลำดับพิสูจน์แท้ก่อน parse, การ parse XML ที่มีขอบเขตและไร้ entity, invariant การอัปเดตแบบ monotonic, การจับคู่ service ด้วย DER เป๊ะ, การประเมินสถานะย้อนเวลา และผลลัพธ์ที่แยกจากความเชื่อถือห่วงโซ่ธรรมดา ถ้าปัญหาเร่งด่วนของคุณพื้นฐานกว่านั้น คือ validator ปฏิเสธลายเซ็นที่คุณเชื่อว่าปกติ สาเหตุทั่วไปถูกจัดหมวดไว้ในทำไม validator ปฏิเสธลายเซ็น PAdES และพื้นผิวการตรวจลายเซ็นอธิบายไว้ในการส่องลายเซ็นและระดับ PAdES ความสามารถของ component ระบุไว้บนหน้าผลิตภัณฑ์PDFium Delphi component