기술 문서

PDFium VCL에서 OpenSSL로 PDF 서명 검증하기

PDFium VCL은 CMS 검증을 IPdfCmsVerifier 인터페이스 뒤의 교체 가능한 백엔드로 다룹니다. 그래서 PAdES 검증기는 Windows에서는 CryptoAPI로, macOS에서는 Keychain으로, OpenSSL이 있는 어디서든 ConfigureSslCmsVerifier로 돌아갑니다. 인터페이스는 작습니다. 그 아래의 세 가지 OpenSSL 동작은 나이브하게 구현하면 자신만만한 잘못된 답을 냅니다

동기는 Delphi 애플리케이션이 Windows를 떠나면 단순해집니다. 서명 검증은 플랫폼 암호 스택이 구현 디테일이 아닌 몇 안 되는 영역입니다. 어떤 인증서를 신뢰하는지, 어떤 알고리즘이 존재하는지, 폐기가 무엇을 의미하는지를 결정합니다. 하나를 하드코딩하면 코드가 포트되지 않습니다. 추상화를 나쁘게 하면 모든 플랫폼이 호출자가 비교할 수 없는 제각각의 모양을 한 답을 내놓습니다

추상화가 실제로 실어야 하는 것

두 가지 검증 형태와 세 개의 독립 판정입니다. PDF 서명은 분리(detached) 형태입니다. 서명된 콘텐츠는 /Contents 구멍 양옆의 두 바이트 범위이므로 VerifyDetached는 버퍼 하나가 아니라 두 세그먼트를 받습니다. 타임스탬프 토큰은 첨부(attached) 형태로 자기 콘텐츠를 실으므로 VerifyAttached는 DER만 받습니다

결과는 세 상태로 나뉩니다. 셋이 서로 다른 질문에 답하고 어긋날 수 있기 때문입니다. SignatureStatus는 바이트가 서명자 인증서의 키로 서명됐는지를 말합니다. TrustStatus는 그 인증서가 여러분이 신뢰하는 무언가로 체인되는지를 말합니다. RevocationStatus는 인증서가 관련 시점에 여전히 유효했는지를 말합니다. 여러분이 들어 본 적 없는 인증서의 수학적으로 완벽한 서명을 실은 문서는 valid이고 untrusted이고 unknown이며, 그것을 단일 불리언으로 뭉개는 것이 검증기가 사용자에게 거짓말하게 되는 길입니다

uses
  FPdfCrypto, FPdfCryptoSsl;

var
  Options: TPdfCmsVerifyOptions;
begin
  if not SslAvailable then
    raise Exception.Create('libcrypto not usable: ' + SslMissingSymbols);

  ConfigureSslTrustAnchors(LoadCorporateRoots);   // DER, 비어 있을 수 있음
  ConfigureSslCrls(LoadFreshCrls);                // DER, 비어 있을 수 있음
  ConfigureSslCmsVerifier;                        // 백엔드 설치

  Writeln('backend  : ', PadesCmsVerificationBackendName);
  Writeln('library  : ', SslLibraryPath, ' ', SslLibraryVersion);
  Writeln('ABI      : ', SslAbiLayout);           // ulong=<n> long=<n>

  Options := TPdfCmsVerifyOptions.Default;
  Options.CheckRevocation := True;
  Options.CollectChainCertificates := True;
end;

SslAbiLayout은 호기심 거리처럼 보이지만 그렇지 않습니다. 모든 OpenSSL 에러 코드와 모든 스토어 플래그는 C unsigned long으로 경계를 넘는데, 이것은 Windows에서 4바이트이고 Linux와 macOS에서 8바이트입니다. 고정 32비트 타입으로 선언하면 코드는 Windows에서 동작하다가 LP64에서 값의 절반을 조용히 읽습니다. 가정된 폭들을 테스트에서 단언할 수 있는 문자열로 보고하는 것이 부류 전체의 플랫폼 ABI 드리프트를 한 줄 검사로 바꿉니다. PKCS#11 바인딩에서 CK_ULONG으로 같은 문제를 겪어 본 사람이라면 즉시 알아볼 것입니다. 그 이야기는 PKCS#11 구조체 패킹과 CK_ULONG 폭에 있습니다

두 번째 검증 패스가 빈 콘텐츠를 보는 이유는?

CMS_verify가 분리 콘텐츠 BIO를 파일 끝까지 읽고, 읽힌 BIO가 여러분을 위해 되감기지 않기 때문입니다. 두 패스로 검증하는 것은 합리적인 설계입니다. 먼저 체인 평가를 억제한 채 암호학적 서명만, 그다음 전체 평가. 그런데 두 패스가 하나의 BIO를 공유하면 비정상적으로 기만적인 방식으로 실패합니다

두 번째 패스는 0바이트의 콘텐츠를 받습니다. 분리 모드에서 그것은 에러가 아닙니다. 빈 콘텐츠 버퍼는 합법적인 입력이니까요. 다이제스트는 그저 맞지 않고, 실패는 콘텐츠 실패가 아니라 체인 구축 실패로 드러나서, 실제 문제가 스트림 위치인 동안 여러분을 인증서와 신뢰 저장소 들여다보기로 보냅니다. 매 패스마다 BIO_new_mem_buf로 메모리 BIO를 다시 만드세요. 할당 하나의 비용으로 가능성 자체를 제거합니다

no-verify 플래그가 억제하는 것과 하지 않는 것

CMS_NO_SIGNER_CERT_VERIFY는 체인 평가를 억제하지 서명자 인증서 조회를 억제하지 않습니다. 내부적으로 OpenSSL은 플래그를 참조하기 전에 서명자 인증서를 해석해 붙이므로, 그 플래그를 실은 첫 패스 뒤에는 서명자가 이미 사용 가능하고 그 알고리즘 식별자를 바로 읽을 수 있습니다. 서명자 인증서를 얻으려고 두 번째 전체 검증을 돌릴 필요가 없는데, 플래그 이름이 여러분을 그렇게 가정하게 유혹합니다

그것에 딸린 소유 규칙이 하나 있습니다. 서명자 참조는 CMS 구조에 속하며 독립적으로 해제되어서는 안 됩니다. 구조가 살아 있는 동안 유효하고, 해제하면 증상이 전혀 다른 곳, 보통 무관한 오브젝트의 정리 중에 나타나는 손상이 됩니다

CRL 검사를 켜면 모든 서명이 거부되는 이유는?

OpenSSL은 CRL을 스토어가 이미 갖고 있는 것에 대해서만 검사하고 스스로 아무것도 가져오지 않기 때문입니다. CRL 배포 지점을 따라가지 않고 OCSP도 말하지 않습니다. CRL이 하나도 없는 스토어에 X509_V_FLAG_CRL_CHECK를 설정하면 모든 체인이 인증서 CRL을 얻을 수 없다는 이유로 실패합니다. 그 결과는 폐기 검사가 동작해서 문제를 찾은 것처럼 보입니다. 실제로는 폐기 검사가 전혀 실행되지 않은 것입니다

그래서 백엔드는 ConfigureSslCrls가 실제로 최소한 하나의 CRL을 공급했을 때만 플래그를 설정합니다. 하나도 없으면 RevocationStatuspcvsUnsupported로 돌아오는데, 질문이 답되지 않았다는 정직한 진술입니다. 같은 이유로 OnlineRetrieval은 이 백엔드에 효과가 없고 pcvstOnlineRetrieval 체크포인트도 발생하지 않습니다. 진행을 보고할 가져오기 경로가 없으니까요

PDFium VCL OpenSSL CMS 검증기의 세 함정 다이어그램: 파일 끝까지 읽힌 공유 콘텐츠 BIO가 두 번째 검증 패스에 0바이트를 남기고, CMS_NO_SIGNER_CERT_VERIFY는 체인 평가를 억제하지 서명자 조회는 억제하지 않으며, 빈 스토어에서의 CRL 검사는 폐기 검사가 한 번도 돌지 않은 채 모든 체인을 거부한다
각 함정은 자신만만한 잘못된 판정을 냅니다: 스트림 위치가 신뢰 실패로 가장하고, no-verify 플래그는 이름이 시사하는 것보다 적게 억제하며, 한 번도 돌지 않은 폐기 검사는 문제를 찾은 폐기 검사처럼 보입니다

이것은 일반적으로 방어할 가치가 있는 설계 입장입니다. 폐기를 검사할 수 없는 검증기는 그렇게 말해야 합니다. 검사하지 않은 인증서를 폐기되지 않음으로 보고하는 것이 서명 검증 도구가 사용자를 오도하는 단연 가장 흔한 방식이며, 검증기가 PAdES 서명을 거부하는 이유에서 탐구하는 바로 그 부류의 혼란입니다

// 체크포인트는 UI에게 어느 단계가 도는지 보여 주고, 백엔드가 실제로
// 수행하는 단계들을 알려 줍니다
type
  TSignatureProbe = class
    procedure Checkpoint(Stage: TPdfCmsVerifyStage);
  end;

procedure TSignatureProbe.Checkpoint(Stage: TPdfCmsVerifyStage);
begin
  case Stage of
    pcvstCryptographicSignature: Status('checking the signature');
    pcvstChainBuild:             Status('building the certificate chain');
    pcvstOnlineRetrieval:        Status('fetching validation data');
    pcvstRevocationCheck:        Status('checking revocation');
  end;
end;

// 세 판정은 따로 읽으세요. 어긋나도 됩니다
if Result.SignatureStatus = pcvsValid then
  case Result.TrustStatus of
    pcvsValid:         Report('signed and trusted');
    pcvsInvalid:       Report('signed, chain rejected');
    pcvsUnsupported,
    pcvsIndeterminate: Report('signed, trust not established');
  end;
if Result.RevocationStatus = pcvsUnsupported then
  Report('revocation was not checked on this backend');

고정할 수 없는 라이브러리에 바인딩하기

OpenSSL은 1.0과 1.1 사이에서 스택 접근자들의 이름을 바꿨으므로, 같은 논리 함수가 호스트가 갖고 있을 빌드에 따라 두 가지 가능한 익스포트 이름을 갖습니다. 바인딩은 더 새 이름을 먼저 해석하고 더 오래된 것으로 폴백하며, 둘 다 해석되지 않을 때만 빠진 심볼을 기록합니다. 여러분이 배송하지 않는 라이브러리에 대한 동적 바인딩이라면 그것이 올바른 형태입니다. 현재 이름을 선호하고, 역사적 이름을 용서하고, 진짜 부재만 보고하세요

SslMissingSymbols는 실패한 로드를 진단 가능한 사건으로 바꾸는 것입니다. libcrypto가 명백히 설치된 호스트에서 비어 있지 않은 결과는 설치된 버전이 이 빌드가 타깃하는 API보다 오래되었다는 뜻이며, 이것은 라이브러리가 아예 없는 경우와는 완전히 다른 지원 대화입니다. ConfigureSslLibraryPath는 다른 흔한 사례, 즉 기본 검색 경로 위의 것이 여러분이 원하는 것이 아닌 여러 OpenSSL 빌드를 갖춘 호스트를 다룹니다

플랫폼별 백엔드 고르기

실무적 배치는 시작 시 선택하고 누가 답했는지 기록하는 것입니다. Windows에서는 플랫폼 백엔드가 기업이 이미 관리하는 인증서 스토어와 통합되는데, 보통 그것이 여러분이 원하는 것입니다. macOS에서는 Keychain 백엔드가 같은 논리에 부합하며 macOS에서 SecTrust로 서명 검증하기에서 기술됩니다. OpenSSL은 이식 가능한 옵션이고, 각 플랫폼 신뢰 저장소를 따라가는 것이 아니라 플랫폼들 사이에서 동일한 검증 정책이 필요할 때의 올바른 선택이기도 합니다

PDFium VCL의 IPdfCmsVerifier 추상화 다이어그램: Contents 구멍 양옆 두 바이트 범위에 대한 VerifyDetached와 타임스탬프 토큰용 VerifyAttached, 세 독립 판정 SignatureStatus, TrustStatus, RevocationStatus, 그리고 CryptoAPI, SecTrust, ConfigureSslCmsVerifier를 통해 시작 시 선택되는 플랫폼별 백엔드
인터페이스는 두 검증 형태와 세 판정을 실는데, 셋이 서로 다른 질문에 답하고 어긋날 수 있기 때문입니다. 그리고 설치된 백엔드가 모든 판정 옆에 기록되어 저장된 결과를 재현할 수 있습니다

무엇을 설치하든, 기록하는 모든 판정 옆에 PadesCmsVerificationBackendName을 로그로 남기세요. 그것을 만들어 낸 백엔드 없이 저장된 검증 결과는 나중에 재현할 수 없습니다. 세 상태 값은 어느 스택이 답했는지에 따라 미묘하게 다른 것을 의미하니까요. 이 모든 것 위의 서명 검사 계층, PAdES 레벨이 어떻게 보고되는지를 포함해 PDF 디지털 서명과 PAdES 레벨 검사하기에서 다룹니다

전부는 소스로 PDFium Delphi 컴포넌트에 실려 나오며, 여기서는 평소보다 그것이 중요합니다. 서명 검증기에게 백엔드가 정확히 어떤 플래그를 설정하고 어떤 검사를 건너뛰는지 읽을 수 있는 것은 있으면 좋은 것이 아니라, 애플리케이션의 초록 체크표시가 실제로 무엇을 주장하는지 아는 유일한 방법입니다