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

การเซ็น PAdES ระยะไกลของ PDFium VCL: HSM และคีย์บนคลาวด์

PDFiumPas แบ่งการเซ็น PAdES ออกเป็นสองการเรียก เพื่อให้คีย์ส่วนตัวไม่จำเป็นต้องอยู่ในโพรเซสของคุณเลย PreparePadesRemoteSignature จะเขียนอัปเดตแบบเพิ่มขึ้นพร้อมพื้นที่ว่างสำรอง /Contents ที่มีความกว้างคงที่และว่างเปล่า แล้วส่งกลับเรคคอร์ดคำขอที่พก digest SHA-256 ของเอกสาร ByteRange ที่แน่นอน และลายนิ้วมือของไฟล์ที่เตรียมไว้ CompletePadesRemoteSignature จะรับ CMS แบบแยกส่วนที่บริการเซ็นของคุณส่งกลับมา แล้ววางลงในช่องที่จองไว้นั้น

ระหว่างการเรียกทั้งสองครั้งนี้ อาจผ่านไปหลายนาทีหรือหลายชั่วโมง โพรเซสอาจรีสตาร์ท และงานอาจย้ายไปยังเครื่องอื่นได้ ช่องว่างนี้เองคือเหตุผลทั้งหมดที่ API ถูกออกแบบมาในรูปแบบนี้

เหตุใดคีย์ระยะไกลจึงใช้การเรียกเซ็นแบบธรรมดาไม่ได้

เพราะ SignPadesBytes สันนิษฐานว่าการเซ็นจะเกิดขึ้นภายในการเรียกครั้งนั้น มันสร้างอัปเดตแบบเพิ่มขึ้น คำนวณ digest บน ByteRange เซ็นมัน แล้วเขียนผลลัพธ์ ทั้งหมดนี้ก่อนที่จะคืนค่ากลับมา ซึ่งถูกต้องพอดีเมื่อคีย์อยู่ใน Windows certificate store หรือในไฟล์ PKCS#12 ที่คุณโหลดไว้

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

โพรโทคอลสองเฟส

เฟสแรกเตรียมเอกสาร PDFiumPas จะต่อท้ายฟิลด์ลายเซ็นและดิกชันนารีค่า จองพื้นที่ที่เข้ารหัสฐานสิบหกจำนวน ContentsSize ไบต์ใน /Contents คำนวณ ByteRange รอบพื้นที่จองนั้น แล้วสร้าง TPadesRemoteSigningRequest ที่บรรจุ FormatVersion, PreparedFingerprint, DocumentDigest, ByteRange ที่มีสี่ส่วน ContentsHexOffset และ ContentsSize

ค่าเดียวที่บริการเซ็นของคุณต้องการคือ DocumentDigest ซึ่งเป็น SHA-256 ที่ CAdES SignedData ที่ส่งกลับมาต้องพกไว้เป็น message digest ของมัน สิ่งอื่นทั้งหมดในเรคคอร์ดนี้มีอยู่เพื่อให้เฟสที่สองพิสูจน์ได้ว่าไฟล์ที่กำลังทำให้เสร็จสมบูรณ์คือไฟล์เดียวกับที่ digest นั้นถูกคำนวณมาจาก

uses
  FPdfPades;

var
  Options: TPadesRemoteSignOptions;
  Request: TPadesRemoteSigningRequest;
  Source, Prepared, Session: TFileStream;
begin
  Options := TPadesRemoteSignOptions.Default;
  Options.Reason := 'Approved by finance';
  Options.Location := 'Lisbon';
  Options.Name := 'A. Moreira';
  Options.SigningTimeUtc := NowUtc;
  Options.ContentsSize := 16384;   // จำนวนไบต์ฐานสิบหกที่จองไว้สำหรับ CMS

  Source := TFileStream.Create('contract.pdf', fmOpenRead or fmShareDenyWrite);
  Prepared := TFileStream.Create('contract.prepared.pdf', fmCreate);
  try
    PreparePadesRemoteSignature(Source, Prepared, Options, Request);
  finally
    Prepared.Free;
    Source.Free;
  end;

  // เก็บเซสชันไว้เพื่อให้การรันครั้งถัดไป หรือเครื่องอื่น สามารถทำให้เสร็จสมบูรณ์ได้
  Session := TFileStream.Create('contract.signreq', fmCreate);
  try
    SavePadesRemoteSigningRequest(Session, Request);
  finally
    Session.Free;
  end;

  SendDigestToSigningService(Request.DocumentDigest);
end;

Complete ปฏิเสธอะไรบ้าง และแต่ละการตรวจสอบมีไว้ทำไม

ขั้นตอน Complete คือจุดที่การออกแบบการเซ็นระยะไกลมักผิดพลาด ดังนั้นการตรวจสอบจึงเข้มงวดโดยตั้งใจ CompletePadesRemoteSignature จะปฏิเสธ: PDF ที่เตรียมไว้ซึ่งลายนิ้วมือไม่ตรงกับคำขออีกต่อไป ByteRange ที่ไม่ตรงกับพิกัดพื้นที่สำรองที่บันทึกไว้ ตัวคั่น /Contents ที่ถูกแก้ไข พื้นที่สำรองที่ไม่ว่างเปล่าอีกต่อไป CMS ที่ใหญ่กว่าพื้นที่ที่จองไว้ CMS ที่ไม่ใช่ค่า DER เพียงหนึ่งค่าพอดี รูปแบบ SignedData ที่ไม่รองรับ แอตทริบิวต์ signing-certificate-v2 ที่ขาดหายไป และ CMS ที่ message digest ไม่เท่ากับ digest ของเอกสารที่เตรียมไว้

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

ข้อกำหนด signing-certificate-v2 เป็นเรื่องความสอดคล้องกับ PAdES มากกว่าเรื่องความสมบูรณ์ ETSI EN 319 142 กำหนดให้ใบรับรองผู้เซ็นต้องถูกผูกเข้ากับแอตทริบิวต์ที่เซ็นไว้ และ CMS ที่ขาดแอตทริบิวต์นั้นไม่ถือเป็นลายเซ็น PAdES แม้จะยืนยันได้ถูกต้องทางการเข้ารหัสก็ตาม การปฏิเสธมันตั้งแต่ขั้นตอน Complete หมายความว่าคุณจะพบปัญหานี้ที่นี่ ไม่ใช่ในรายงานของตัวตรวจสอบจากลูกค้า หัวข้อนี้อธิบายเพิ่มเติมไว้ใน เหตุใดตัวตรวจสอบจึงปฏิเสธลายเซ็น PAdES

var
  Request: TPadesRemoteSigningRequest;
  Session, Prepared, Dest: TFileStream;
  CmsDer: TBytes;
begin
  Session := TFileStream.Create('contract.signreq', fmOpenRead);
  try
    Request := LoadPadesRemoteSigningRequest(Session);
  finally
    Session.Free;
  end;

  CmsDer := FetchDetachedCmsFromService;   // ส่งกลับมาโดย HSM หรือ TSP

  Prepared := TFileStream.Create('contract.prepared.pdf', fmOpenRead);
  Dest := TFileStream.Create('contract.signed.pdf', fmCreate);
  try
    try
      CompletePadesRemoteSignature(Prepared, Dest, Request, CmsDer);
    except
      on E: EPadesCrypto do
        // การปฏิเสธทุกครั้งพกเหตุผลที่เจาะจงมาด้วย ให้บันทึกไว้ตามที่เป็น
        FailSession(E.Message);
    end;
  finally
    Dest.Free;
    Prepared.Free;
  end;
end;

ข้ามขอบเขตโพรเซสและเครื่อง

SavePadesRemoteSigningRequest และ LoadPadesRemoteSigningRequest ซีเรียลไลซ์เซสชันผ่านรูปแบบไบนารีที่มีเวอร์ชันกำกับและเสถียร ซึ่งเป็นสิ่งที่ทำให้การออกแบบนี้ใช้งานได้จริง ไม่ใช่แค่ถูกต้องตามทฤษฎีเท่านั้น เว็บแอปพลิเคชันสามารถเตรียมเอกสารในคำขอหนึ่ง เก็บ PDF ที่เตรียมไว้และ blob เซสชัน ส่ง digest กลับไปยังเบราว์เซอร์เพื่อเซ็นด้วยสมาร์ตการ์ด แล้วทำให้ไฟล์เสร็จสมบูรณ์ในตัวจัดการคำขออีกตัวที่ต่างออกไปโดยสิ้นเชิง

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

การกำหนดขนาดพื้นที่สำรอง

ContentsSize คือพารามิเตอร์หนึ่งเดียวที่คุณต้องคิดให้ดี เพราะมันถูกกำหนดไว้ตายตัวก่อนที่ CMS จะมีอยู่จริง มันนับพื้นที่จองที่เข้ารหัสฐานสิบหก ดังนั้น CMS แบบ DER ขนาด 6 KB จึงต้องการพื้นที่อย่างน้อย 12 KB และการใช้งานนี้จำกัดพื้นที่จองไว้สูงสุดที่ 64 MiB

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

สิ่งที่เกิดขึ้นหลังจากมีลายเซ็นแล้ว

ลายเซ็นระยะไกลที่ทำสำเร็จแล้วคือ PAdES B-B การตรวจสอบระยะยาวต้องการไทม์สแตมป์และวัสดุการตรวจสอบ ซึ่งเป็นอัปเดตแบบเพิ่มขึ้นแยกต่างหากที่เพิ่ม DSS และดิกชันนารี VRI ต่อลายเซ็น อธิบายไว้ใน ลายเซ็นระยะยาวด้วยไทม์สแตมป์ RFC 3161 และ DSS ขั้นตอนนั้นทำในเครื่องล้วน ๆ: เพิ่มใบรับรอง การตอบสนอง OCSP และ CRL ซึ่งไม่มีตัวใดต้องการคีย์ส่วนตัวเลย

ก่อนส่งมอบ ให้ตรวจสอบสิ่งที่คุณสร้างขึ้นด้วยเส้นทางโค้ดเดียวกับที่ฝ่ายที่พึ่งพาลายเซ็นจะใช้ อธิบายไว้ใน การตรวจสอบลายเซ็นดิจิทัลและระดับ PAdES การเซ็นและการตรวจสอบเป็นโค้ดคนละส่วนกัน และไปป์ไลน์การเซ็นระยะไกลก็เป็นจุดพอดีที่ทั้งสองอาจเบี่ยงเบนออกจากกันโดยไม่มีใครสังเกตเห็น จนกว่าตัวตรวจสอบภายนอกจะบอกออกมา

PDFiumPas เป็นคอมโพเนนต์สำหรับ Delphi และ Lazarus ที่ห่อหุ้มเอนจิน PDFium ไว้ พร้อมสแตก PAdES แบบเนทีฟที่เขียนด้วย Pascal ทำให้การเซ็น การประทับเวลา และการตรวจสอบทำงานได้โดยไม่ต้องพึ่งเครื่องมือบรรทัดคำสั่งภายนอก เอกสาร API ฉบับเต็มและรุ่นทดลองใช้งานอยู่ที่ หน้าคอมโพเนนต์ PDFium สำหรับ Delphi