💡 전체 작동 예제는 GitHub에서 확인할 수 있습니다:
pdf-서명-인증서-검사-파이썬

소개

서비스는 매일 밤 업로드된 PDF에 서명합니다. 어느 날 아침, 사용 중인 인증서가 만료일을 지나게 되었지만, 아무 변화도 없는 것처럼 보였습니다: 작업은 실행되고, 파일은 기록되며, 로그는 정상으로 보였습니다. 몇 주 후 누군가가 Acrobat에서 해당 문서 중 하나를 열었을 때 경고 배너가 표시되었습니다. 만료된 인증서로 만든 서명은 약한 서명이 아니라 검증자가 무효로 보고하는 서명입니다. 승인된 것으로 보였던 문서는 서명되지 않은 문서보다 가치가 떨어집니다. 사람들은 그 문서를 신뢰했기 때문입니다.

이 거부에는 이름이 있습니다. 인증서 유효성 검사란 Python용 GroupDocs.Signature 동작으로, 인증서의 유효 기간이 지나거나 시작되기 전이면 서명을 거부합니다. 이 기능은 버전 26.9에 도입되었으며, 같은 형태의 두 가지 변경과 함께 제공되었습니다: PDF 서명의 기본 다이제스트가 SHA-256으로 바뀌었고, SignatureSettings.log_level이 조용히 무시되는 대신 필터링을 시작했습니다. 각각은 이전에 조용히 발생하던 결과를 눈앞에 드러냅니다.

이 문서는 Python을 통해 .NET에서 동작하는 세 가지 제어를 비교합니다—각 제어가 출력에 어떤 변화를 주는지, 언제 사용해야 하는지, 그리고 바인딩의 두 가지 세부 사항이 사람들에게 오후 한 날을 소비하게 만든 이유를 설명합니다. 모든 결과는 한 페이지 PDF에 샘플을 실행한 결과입니다.

왜 이것이 버전 노트보다 더 중요한가

세 가지 변경은 하나의 공통된 특성을 가지고 있습니다: 나중에 발견하게 될 실패를 지금 바로 발견하게 만든다는 점입니다.

  • 만료된 인증서: 누군가가 인증서를 갱신할 수 있는 상황에서 서명 호출이 실패합니다. 배포 후 검증에 실패하는 문서를 만들지 않습니다.
  • 다이제스트 기본값: 새로운 서명은 SHA-256을 사용하므로, 약한 옵션을 선택하려면 의도적인 결정을 해야 합니다.
  • 로그 레벨: 경고만 출력하도록 구성된 서비스는 이제 경고만 받게 되며, 이는 경고를 읽을 수 있게 만들어 줍니다.

마지막 항목은 겉보기에만 중요한 것이 아닙니다. 만료된 인증서 경고의 전체 가치는 누군가가 이를 확인하는 데 있으며, 서명 실행당 10개의 추적 메시지에 파묻힌 경고는 아무도 보지 못합니다.

전제 조건

시작하기 전에 다음을 확인하십시오:

  • 64비트 인터프리터에서 Python 3.9 이상 — 패키지는 번들된 .NET 런타임을 포함하고 32비트 휠을 제공하지 않습니다.
  • .NET 26.10.0용 GroupDocs.Signature for Python, 평가 제한을 해제하려면 무료 임시 라이선스를 사용하십시오.
  • 서명할 PDF와, 샘플이 임시 테스트 인증서를 생성하는 데 사용하는 cryptography 패키지

설치

pip install groupdocs-signature-net cryptography

제어 1 – 서명에 기록되는 다이제스트

DigitalSignOptions의 hash_algorithm이 다이제스트를 선택합니다. 26.9 이후 기본값은 현재 검증자가 기대하는 adbe.pkcs7.detached 형식의 SHA-256이며, 이전에는 새 서명이 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)

두 가지 세부 사항을 강조합니다. 인증서는 파일 경로가 아니라 io.BytesIO 형태의 certificate_stream을 통해 전달됩니다. 이는 메모리 내에서 만든 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

메시지는 인증서 이름, 만료 날짜, 지문, 그리고 허용될 수 있는 속성을 표시합니다. 이는 애플리케이션이 운영자에게 무엇을 갱신해야 하는지 알려주기에 충분합니다. 파이썬에서는 첫 번째 줄만 사용하는 것이 중요합니다. 예외 텍스트 뒤에 바인딩 뒤쪽의 .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은 플래그 값입니다. 샘플은 동일한 문서를 세 번 서명합니다—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)

결과는 전혀 없고, 경고 하나, 그리고 그 경고와 10개의 추적 메시지 순입니다. 26.9 이전에는 세 행 모두 동일했는데, 레벨이 받아들여지고 무시되었기 때문입니다—레벨을 설정했지만 변화가 없고 코드를 오해한 경우에 알아두면 좋습니다.

바인딩 세부 사항 두 가지가 오후 한 날을 소비하게 만들었습니다. SignatureSettings.logger는 읽기 전용이므로 생성자 인수로 전달해야 하며, 이후에 할당하면 AttributeError가 발생합니다; log_level은 그 뒤에 정상적으로 설정합니다. 또한 사용자 정의 로거는 groupdocs.signature.logging.ILogger를 상속해서는 안 됩니다—이 기본 클래스는 라이브러리가 소유한 핸들을 필요로 하는 네이티브 객체를 래핑하므로 상속하면 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 매개변수를 제공하십시오. 라이브러리는 항상 예외를 전달하지 않으며, 이를 요구하는 로거는 예외가 없는 메시지에서 오류가 발생합니다.

세 가지 비교: 언제 각각을 사용해야 할까

제어 가장 적합한 상황 주요 장점 제한 사항
hash_algorithm 다이제스트를 명시하는 정책을 충족해야 할 때 한 번 설정하면 출력 크기가 동일 인증서 자체가 신뢰할 수 없으면 의미 없음
유효성 검사 및 오버라이드 다른 사람을 위해 서명할 때 실패가 즉시 발견되어 수정 가능 오버라이드는 파일을 생성하지만 신뢰할 수 있는 파일은 아님
log_level 로그가 이미 복잡한 서비스 열한 개의 메시지를 하나로 축소 로그만 필터링하고 예외는 절대 필터링하지 않음

이들은 대체 관계가 아니라, 하나의 서명 호출이 세 가지를 모두 사용한다는 점을 기억하십시오. 생각하는 순서는 결과의 순서와 같습니다: 유효성 검사가 파일 존재 여부를 결정하고, 다이제스트가 내부 내용을 결정하며, 로그 레벨이 누가 알게 되는지를 결정합니다.

로그 레벨이 예외 종류에 영향을 미치나요?

아니요. 로그 레벨은 로거에 전달되는 메시지만 결정하고 그 외는 변하지 않습니다. LogLevel.NONE에서도 만료된 인증서는 GroupDocsSignatureException을 발생시키며, allow_expired는 LogLevel.ALL에서도 정상적으로 서명합니다; 반환값과 예외는 모든 레벨에서 동일합니다. 바뀌는 것은 의심스러운 서명을 설명하는 경고가 사람에게 읽히는지 여부뿐입니다.

검증도 같은 방향으로 이동

같은 릴리스의 다른 절반이므로 언급합니다. 빈 DigitalVerifyOptions를 사용한 verify는 이제 모든 PDF 디지털 서명을 암호학적으로 검사하므로, 서명 후 문서가 변경되면 단순히 설명되지 않는 것이 아니라 무효로 반환됩니다:

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

두 줄이며, 서명 후 저장 파이프라인에 추가하면 좋습니다. True가 보장하지 않는 점을 기억하십시오: 서명이 문서와 일치한다는 것일 뿐, 발급자를 신뢰한다는 뜻은 아닙니다. 샘플의 자체 서명 인증서는 여기서 검증되지만 PDF 리더에서는 여전히 거부됩니다. 이는 신뢰 문제를 별도로 해결한다는 의미입니다.

모범 사례 및 팁

  • 거부를 기본값으로 유지하십시오. 사용자를 대신해 서명하는 모든 상황에서 전역이 아닌 호출당 오버라이드하십시오. 예외는 비용이 적지만, 잘못된 서명 배치는 비용이 큽니다.
  • 경고 텍스트를 기록하고 단순히 카운터만 남기지 마십시오. 인증서와 날짜가 명시되어 있어 운영자가 조치를 취할 수 있습니다.
  • 조기 인증서를 허용하기 전에 시계를 확인하십시오. 보통 인증서는 올바르고 머신 시계가 틀린 경우가 많으며, 이는 여러 서명 호출에 영향을 줍니다.
  • 프로덕션에서는 추적을 비활성화하십시오. 서명당 약 10개의 추적은 빠르게 누적됩니다; 진단 시에만 켜고 이후에는 끄세요.
  • 서명 후 검증을 파이프라인에 포함하십시오. 이제 검증이 암호학적이므로 손상된 출력이 수신자에게 도달하기 전에 잡힙니다.

결론

세 가지 제어, 하나의 서명 호출, 그리고 모두 동일한 설계 철학을 공유합니다: 위험한 결과는 이제 결정이 필요하고, 안전한 결과는 아무 것도 할 필요가 없습니다. 유효성 검사를 유지하고, allow_expired는 호출당 예외로 로깅하며, 정책이 없으면 다이제스트는 그대로 두고, 경고가 읽히도록 로그 레벨을 설정하십시오.

샘플을 자신의 PDF에 한 번 실행하면 1분 정도 소요되며, 각 제어가 어떻게 변했는지 정확히 출력됩니다—서명된 파일 6개, 의도적인 거부 1개, 그리고 더 이상 동일하지 않은 메시지 카운트 3행이 표시됩니다.

추가 자료