💡 ตัวอย่างทำงานเต็มที่มีบน GitHub:
pdf-signing-certificate-checks-python

บทนำ

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

การปฏิเสธนี้มีชื่อเรียก การตรวจสอบความถูกต้องของใบรับรองเป็นพฤติกรรมของ GroupDocs.Signature สำหรับ Python ที่ปฏิเสธการเซ็นเมื่อช่วงเวลาความถูกต้องของใบรับรองหมดลงหรือยังไม่เริ่มต้น มันมาพร้อมกับเวอร์ชัน 26.9 พร้อมกับการเปลี่ยนแปลงสองอย่างที่มีรูปแบบเดียวกัน: SHA‑256 กลายเป็นค่าเริ่มต้นของดิจสต์สำหรับลายเซ็น PDF, และ SignatureSettings.log_level เริ่มทำการกรองแทนที่จะถูกละเลยอย่างเงียบ ๆ แต่ละอย่างทำให้ผลลัพธ์ที่เคยเกิดขึ้นโดยเงียบ ๆ ปรากฏต่อหน้าคุณ

บทความนี้เปรียบเทียบสามการควบคุมเหล่านี้เมื่อทำงานจาก Python ผ่าน .NET – สิ่งที่แต่ละอย่างเปลี่ยนแปลงในผลลัพธ์, เวลาใดที่ควรใช้, และรายละเอียดสองประการของการเชื่อมต่อที่ทำให้คนต้องเสียบ่ายหนึ่ง ผลลัพธ์ทั้งหมดที่อ้างอิงมาจากการรันตัวอย่างกับ PDF หนึ่งหน้า

ทำไมเรื่องนี้สำคัญกว่าการบันทึกเวอร์ชัน

การเปลี่ยนแปลงทั้งสามมีคุณสมบัติที่ควรตั้งชื่อ: ทั้งหมดทำให้ความล้มเหลวที่คุณจะค้นพบภายหลังกลายเป็นความล้มเหลวที่คุณค้นพบทันที

  • ใบรับรองที่หมดอายุ: การเรียกเซ็นล้มเหลวในจุดที่ใครสักคนสามารถต่ออายุใบรับรองได้, แทนที่จะสร้างเอกสารที่ล้มเหลวในการตรวจสอบหลังการแจกจ่าย
  • ค่าเริ่มต้นของดิจสต์: ลายเซ็นใหม่ใช้ SHA‑256 โดยที่ไม่มีใครต้องจำให้ถาม, ดังนั้นตัวเลือกที่อ่อนแอต้องการการตัดสินใจแทนการละเลย
  • ระดับบันทึก: บริการที่ตั้งค่าให้แสดงคำเตือนเท่านั้นจะได้รับคำเตือนเท่านั้น, ทำให้คำเตือนอ่านได้, ซึ่งหมายความว่าคำเตือนจะถูกอ่าน

ข้อสุดท้ายนั้นไม่ใช่แค่เรื่องรูปลักษณ์ ความสำคัญของคำเตือนใบรับรองหมดอายุคือให้ใครสักคนเห็นมัน, และคำเตือนที่ซ่อนอยู่ในข้อความ trace สิบบรรทัดต่อการรันเซ็นหนึ่งครั้งคือคำเตือนที่ไม่มีใครเห็น

ข้อกำหนดเบื้องต้น

ก่อนเริ่ม, ตรวจสอบว่าคุณมี:

  • Python 3.9 หรือใหม่กว่า บนตัวแปล 64‑bit – แพ็กเกจมาพร้อมกับ .NET runtime ที่บรรจุไว้และไม่มี wheel 32‑bit
  • GroupDocs.Signature สำหรับ Python ผ่าน .NET เวอร์ชัน 26.10.0, พร้อม ใบอนุญาตชั่วคราวฟรี หากต้องการลบข้อจำกัดการประเมิน
  • PDF ที่ต้องการเซ็น, และแพ็กเกจ cryptography หากต้องการสร้างใบรับรองทดสอบแบบใช้ครั้งเดียวตามตัวอย่าง

การติดตั้ง

pip install groupdocs-signature-net cryptography

การควบคุม 1 – ดิจสต์ที่เขียนลงในลายเซ็น

hash_algorithm บน DigitalSignOptions เลือกดิจสต์ ค่าเริ่มต้นตั้งแต่เวอร์ชัน 26.9 คือ SHA‑256, ในรูปแบบ adbe.pkcs7.detached ที่ตัวตรวจสอบปัจจุบันคาดหวัง; ก่อนหน้านั้นลายเซ็นใหม่ใช้ SHA‑1

with signature.Signature(source_path) as sign:
    options = DigitalSignOptions()
    options.certificate_stream = io.BytesIO(pfx)
    options.password = PASSWORD
    options.hash_algorithm = HashAlgorithm.SHA512
    options.reason = "Approved"

    result = sign.sign(output_path, options)
    return len(result.succeeded)

มีสองรายละเอียดที่ควรอธิบาย ใบรับรองมาผ่าน certificate_stream เป็น io.BytesIO แทนการระบุพาธไฟล์, ซึ่งเป็นวิธีที่ PKCS#12 ที่สร้างในหน่วยความจำส่งถึงไลบรารีโดยไม่ต้องเขียนลงดิสก์ – ตัวอย่างพึ่งพาวิธีนี้เพื่อไม่ต้องจัดส่งคีย์ส่วนตัวเลย และ HashAlgorithm มีค่า AUTO, SHA1, SHA256, SHA384 และ SHA512, โดยที่ตราประทับเวลา (หากคุณเพิ่ม) จะใช้ดิจสต์ใดก็ตามที่ลายเซ็นใช้

โดยปฏิบัติการนี้เป็นการควบคุมที่คุณสัมผัสน้อยที่สุด ค่าเริ่มต้นคือคำตอบที่ถูกต้องแล้ว, SHA384 และ SHA512 มีไว้สำหรับนโยบายที่ระบุไว้, ส่วน SHA1 เป็นการตั้งค่าความเข้ากันได้สำหรับตัวตรวจสอบที่คุณไม่สามารถเปลี่ยนได้

การควบคุม 2 – ใบรับรองที่หมดอายุจะหยุดคุณหรือไม่

หากไม่มีการแทนที่, การเซ็นด้วยใบรับรองที่ช่วงเวลาความถูกต้องหมดแล้ว – หรือยังไม่เริ่ม – จะทำให้เกิด GroupDocsSignatureException และไม่เขียนไฟล์ใด ๆ

try:
    sign.sign(output_path, options)
    return True
except signature.GroupDocsSignatureException as error:
    print(f"Rejected: {str(error).splitlines()[0]}")
    return False

ข้อความจะระบุชื่อใบรับรอง, วันที่หมดอายุ, thumbprint และคุณสมบัติที่อาจทำให้ใบรับรองผ่านได้, ซึ่งเพียงพอให้แอปพลิเคชันบอกผู้ดำเนินการว่าต้องต่ออายุอะไร ส่วนการดึงเฉพาะบรรทัดแรกสำคัญใน Python โดยเฉพาะ: ข้อความของ exception จะต่อด้วย stack trace ของ .NET จากการเชื่อมต่อ, ซึ่งไม่ควรแสดงต่อผู้ใช้

เมื่อคุณต้องการเซ็นต่อไปจริง ๆ – เช่นการทดสอบกับใบรับรองที่เก็บไว้, หรือชุดงานที่ต้องรันคืนนี้ขณะรอการต่ออายุ – การแทนที่ทำได้ต่อการเรียก:

settings = signature.SignatureSettings(ConsoleLogger())
settings.log_level = LogLevel.WARNING | LogLevel.ERROR

with signature.Signature(source_path, settings=settings) as sign:
    options.allow_expired = True
    result = sign.sign(output_path, options)

allow_not_yet_valid มีรูปแบบเดียวกันสำหรับใบรับรองที่ออกในอนาคต, และสองแฟล็กนี้ทำงานอิสระกัน: การอนุญาตใบรับรองที่หมดอายุไม่ได้หมายความว่าจะอนุญาตใบรับรองที่ยังไม่ถึงวันเริ่มต้น ใบรับรองที่ยังไม่ถึงวันเริ่มต้นมักบ่งบอกว่าเครื่องมีนาฬิกาผิดพลาด ไม่ใช่ใบรับรองแปลก, และนาฬิกาผิดทำให้ลายเซ็นทุกอันที่เครื่องสร้างขึ้นเป็นที่สงสัย, ดังนั้นตรวจสอบนาฬิกาก่อนแทนที่อะไรเลย

การแทนที่ทั้งสองจะส่งคำเตือนแทนการผ่านอย่างเงียบ ๆ, ซึ่งเป็นส่วนที่เชื่อมต่อกับการควบคุมที่สาม

การควบคุม 3 – ใครจะได้รู้บ้าง

SignatureSettings.log_level เป็นค่าฝีมือ (flags). ตัวอย่างทำการเซ็นเอกสารเดียวกันสามครั้ง, ภายใต้ LogLevel.NONE, LogLevel.WARNING | LogLevel.ERROR และ LogLevel.ALL, แล้วนับจำนวนข้อความที่มาถึง:

logger = CollectingLogger()
settings = signature.SignatureSettings(logger)
settings.log_level = level

with signature.Signature(source_path, settings=settings) as sign:
    options.allow_expired = True
    sign.sign(output_path, options)

ผลลัพธ์คือไม่มีอะไรเลย, จากนั้นหนึ่งคำเตือน, แล้วคำเตือนนั้นบวกกับ trace สิบบรรทัด ก่อนเวอร์ชัน 26.9 ทั้งสามแถวจะเหมือนกัน, เพราะระดับถูกยอมรับแล้วละเลย – สิ่งนี้ควรทราบหากคุณเคยตั้งค่าแล้วไม่เห็นการเปลี่ยนแปลงและสรุปว่าคุณอ่านโค้ดของตัวเองผิด

มีรายละเอียดการเชื่อมต่อสองประการที่ทำให้ฉันเสียบ่ายหนึ่ง, ดังนั้นจึงควรอธิบายอย่างชัดเจน SignatureSettings.logger เป็นแบบอ่าน‑อย่างเดียว, ดังนั้น logger ต้องส่งเป็นอาร์กิวเมนต์ของคอนสตรัคเตอร์และการกำหนดค่าให้มันจะทำให้เกิด AttributeError; log_level ตั้งตามปกติหลังจากนั้น ส่วน logger ที่กำหนดเองต้องไม่สืบทอดจาก groupdocs.signature.logging.ILogger – คลาสฐานนั้นห่อวัตถุเนทีฟที่คอนสตรัคเตอร์ต้องการ handle ที่ไลบรารีเป็นเจ้าของ, ดังนั้นการสืบทอดทำให้เกิด TypeError. การเชื่อมต่อจะรับออบเจ็กต์ใด ๆ ที่มีสามเมธอดต่อไปนี้:

class StdlibLogger:
    def error(self, message, exception=None):
        logging.getLogger("groupdocs").error(message, exc_info=exception)

    def warning(self, message, exception=None):
        logging.getLogger("groupdocs").warning(message)

    def trace(self, message):
        logging.getLogger("groupdocs").debug(message)

ให้ error และ warning มีพารามิเตอร์ exception แบบเลือกได้. ไลบรารีไม่ได้ส่งค่าเสมอ, และ logger ที่บังคับให้ต้องมีค่านั้นจะทำให้การทำงานล้มเหลวเมื่อข้อความไม่มี exception

การเปรียบเทียบสามอย่าง: เมื่อใดควรใช้แต่ละอย่าง

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

พวกมันไม่ใช่ทางเลือกแทนกัน – การเรียกเซ็นหนึ่งครั้งใช้ทั้งสามพร้อมกัน ลำดับที่ควรคิดคือผลลัพธ์: การตรวจสอบความถูกต้องตัดสินว่าไฟล์จะมีหรือไม่, ดิจสต์ตัดสินว่าเนื้อหาภายในเป็นอะไร, และระดับบันทึกตัดสินว่าใครจะรู้

ระดับบันทึกเปลี่ยนข้อยกเว้นที่ฉันได้รับหรือไม่?

ไม่. มันกำหนดว่าข้อความใดจะถึง logger ของคุณและไม่มีผลอื่น ๆ ใบรับรองที่หมดอายุยังคงทำให้เกิด GroupDocsSignatureException ภายใต้ LogLevel.NONE, และ allow_expired ยังเซ็นได้ภายใต้ LogLevel.ALL; ค่าที่คืนและข้อยกเว้นเหมือนกันทุกระดับ สิ่งที่เปลี่ยนคือคำเตือนที่อธิบายลายเซ็นที่น่าสงสัยจะถูกอ่านโดยคนหรือไม่

การตรวจสอบย้ายไปในทิศทางเดียวกัน

ควรกล่าวถึงเพราะเป็นอีกครึ่งของการปล่อยเวอร์ชันเดียวกัน verify กับ DigitalVerifyOptions ที่ว่างเปล่า ตอนนี้จะตรวจสอบลายเซ็นดิจิทัล PDF ทุกอันแบบคริปโตกราฟิก, ดังนั้นเอกสารที่ถูกแก้ไขหลังการเซ็นจะกลับเป็นไม่ถูกต้องแทนที่จะเป็นแค่ไม่มีคำอธิบาย:

with signature.Signature(signed_path) as sign:
    result = sign.verify(DigitalVerifyOptions())
    return result.is_valid

สองบรรทัด, และควรเพิ่มเข้าไปใน pipeline ใด ๆ ที่ทำการเซ็นแล้วเก็บไว้ โปรดสังเกตว่า True ไม่ได้สัญญาว่า: ลายเซ็นตรงกับเอกสาร, ไม่ได้สัญญาว่าผู้ออกใบรับรองเชื่อถือได้ ใบรับรองที่เซ็นด้วยตนเองของตัวอย่างตรวจสอบได้ที่นี่แต่ยังคงถูก PDF reader ปฏิเสธ, ซึ่งตอบคำถามเรื่องความเชื่อถือแยกต่างหาก

แนวทางปฏิบัติที่ดีที่สุดและเคล็ดลับ

  • ให้การปฏิเสธเป็นค่าเริ่มต้น ในทุกสิ่งที่เซ็นในนามผู้ใช้, และทำการแทนที่ต่อการเรียกแต่ละครั้งแทนที่จะทำทั่วระบบ. ข้อยกเว้นนั้นมีค่าใช้จ่ายต่ำ; ชุดลายเซ็นที่ไม่ถูกต้องหลายรายการมีค่าใช้จ่ายสูง
  • บันทึกข้อความคำเตือน, ไม่ใช่แค่ตัวนับ. ข้อความจะระบุใบรับรองและวันที่, ซึ่งเป็นส่วนเดียวที่ผู้ดำเนินการสามารถดำเนินการได้
  • ตรวจสอบนาฬิกาก่อนอนุญาตใบรับรองที่ยังไม่ถึงวันเริ่มต้น. ใบรับรองมักถูกต้องและเครื่องมักผิด, ซึ่งส่งผลต่อมากกว่าการเรียกเซ็นหนึ่งครั้ง
  • อย่าให้ trace ปรากฏใน production. ประมาณสิบบรรทัดต่อการรันเซ็นจะสะสมเร็ว; เปิดใช้ขณะวินิจฉัยและปิดเมื่อเสร็จ
  • ตรวจสอบหลังการเซ็น ใน pipeline ใด ๆ, ตอนนี้การตรวจสอบเป็นคริปโตกราฟิก, ดังนั้นผลลัพธ์ที่เสียหายจะถูกจับก่อนที่ผู้รับจะพบ

สรุป

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

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

แหล่งข้อมูลเพิ่มเติม