콘텐츠로 이동

PDF 안에서 서명이 자리 잡는 방식

Spec: ETSI EN 319 142-1 Spec: RFC 5652 Evidence: Standard-backed

PDF 서명은 파일을 감싸는 형태가 아닙니다. 서명은 파일 내부에 내장됩니다. 서명을 명시하는 딕셔너리와, 서명 값 자체를 의도적으로 건너뛰도록 선언된 바이트 범위에 대해 계산한 다이제스트로 구성됩니다. 이 페이지에서는 그 메커니즘과, 그만큼 중요한, 이 메커니즘이 보장하지 않는 것을 설명합니다.

“이 문서는 서명되었다”는 사람들이 행동의 근거로 삼는 문장입니다. 사람들은 이 문장을 결제, 승인, 법적 의무와 연결합니다. 서명이 정확히 어떤 바이트를 다루는지 알지 못하면, 유효하다는 결과가 실제로 무엇을 증명하는지 말할 수 없습니다. PDF는 서명이 완전히 유효한 상태에서도, 서명자가 본 적 없는 콘텐츠를 독자에게 보여줄 수 있습니다. 그 콘텐츠는 서명 이후에, 서명이 다룬다고 주장하지 않은 영역에 추가된 것이기 때문입니다. 서명의 권한이 어디서 시작하고 어디서 끝나는지 아는 것이 방어 가능한 결정과 막연한 기대에 머무는 결정을 가르는 차이입니다.

  • PDF 서명은 외부 봉투가 아니라 문서 내부의 서명 딕셔너리서명 필드 안에 자리합니다.
  • 서명된 바이트는 ByteRange 배열로 선언됩니다. 두 개의 (offset, length) 세그먼트가 함께 파일 전체를 포괄하되, Contents 항목에 들어 있는 16진수 서명 값만은 제외합니다.
  • 두 세그먼트를 이어 붙인 바이트의 다이제스트가 바로 암호화 서명이 실제로 보호하는 대상입니다.
  • 나중에 새 리비전으로 추가된 것은 무엇이든 원래 바이트 범위 바깥에 있습니다. 원래 서명은 유효한 상태로 유지됩니다. 그 서명은 새 바이트에 대해 어떤 주장도 하지 않았기 때문입니다.
  • 승인 서명과 인증 서명은 범위가 다릅니다. 인증(DocMDP)은 이후에 어떤 변경이 허용되는지를 제약하지만, 승인은 그렇지 않습니다.

NextPDF는 포맷이 의도한 방식에 맞춰 고정된 순서로 서명을 구성하므로, 바이트 범위가 어림값이 아니라 정확한 값이 됩니다.

엔진이 서명을 기록할 때, 먼저 Contents 값을 위한 고정 크기 슬롯을 예약하고, 고정 폭의 ByteRange 자리표시자를 기록합니다. 엔진은 상호 참조 테이블과 파일 끝 표시자까지 포함하여 전체 문서가 기록될 때까지 기다립니다. 그제야 두 개의 실제 오프셋을 계산하고, 단 한 바이트도 이동시키지 않은 채 그 값을 자리표시자에 다시 기록하며, 두 세그먼트를 해시한 뒤 그 결과로 만들어진 CMS 객체를 예약된 슬롯에 넣습니다. 이 자리표시자는 일정한 길이로 0 패딩되며, 이는 실제 숫자를 채워 넣더라도 해시되는 바로 그 바이트가 움직이지 않도록 하기 위함입니다. 이것이 자체적으로 정합적인 서명을 만들어 내는 유일한 순서입니다. 엔진은 이 순서에서 발생하는 어떤 실패든 조용한 폴백이 아니라 치명적 오류로 처리합니다.

PDF 2.0 프로파일에서 서명 객체 자체는 분리형(detached) CMS SignedData 구조입니다. PDF 딕셔너리는 어디서어떻게를 말하고, CMS 객체는 누가와 암호학적 증거를 담습니다.

  1. Step 1 of 4: ISO 32000-2 §12.8.1 — ByteRange digest & signature dictionary
  2. Step 2 of 4: ISO 32000-2 §12.8.3.3 — ETSI.CAdES.detached SubFilter
  3. Step 3 of 4: ETSI EN 319 142-1 PAdES baseline profile
  4. Step 4 of 4: RFC 5652 CMS SignedData in Contents
PDF 서명이 정의되는 위치를 컨테이너 포맷부터 암호화 객체까지 짚어 보면 다음과 같습니다. ISO 32000-2 는 딕셔너리와 바이트 범위 메커니즘을 규정하고, ETSI EN 319 142-1 은 이를 PAdES 용으로 프로파일링하며, RFC 5652 는 Contents 에 들어가는 CMS SignedData 객체를 정의합니다.

Evidence: Standard-backed 이 메커니즘은 다음에서 정의됩니다: Spec: ISO 32000-2, §12.8.1 . 바이트 범위 다이제스트는 ByteRange 항목이 가리키는 바이트 범위에 대해 계산됩니다. 그 범위는 서명 딕셔너리를 포함하되 서명 값, 즉 Contents 항목은 제외한 파일 전체여야 합니다. ByteRange는 정수 쌍, 즉 시작 오프셋과 길이로 이루어진 배열입니다. 서로 떨어진 범위를 사용하는 이유는 다이제스트 계산에서 서명 값 자체를 건너뛰기 위해서입니다.

PDF 2.0 프로파일에서, Spec: ISO 32000-2, §12.8.3.3 SubFilterETSI.CAdES.detached일 때, Contents 값이 DER로 인코딩된 CMS SignedData 객체임을 규정합니다. 이는 Spec: RFC 5652 에서 정의하는 구조와 동일하며, 그 객체의 PAdES 프로파일은 바로 Spec: ETSI EN 319 142-1 가 설명합니다.

서명이 모두 같은 범위를 갖는 것은 아닙니다. Spec: ISO 32000-2, §12.7.4.5 MDP 권한을 정의합니다. 값이 0이면 서명은 승인 서명이 되고, 값이 13이면 이후 어떤 수정이 문서를 적합한 상태로 유지할 수 있는지를 제약하는 인증 서명이 됩니다. 동일한 바이트 범위 메커니즘이지만, 이후 변경에 대한 약속은 다릅니다.

NextPDF의 엔진은 바로 이를 구현합니다. 고정 폭의 ByteRange 자리표시자, 두 세그먼트를 이어 붙인 다이제스트, 그리고 예약된 Contents 슬롯에 들어가는 분리형 CMS 객체로 구성되며, 파일이 완성된 뒤에야 비로소 확정됩니다.

ByteRange를 손으로 직접 만드는 경우는 드뭅니다. 이 예시의 목적은 결과의 형태를 보여 주고, 서명된 파일을 살펴볼 때 그것을 알아볼 수 있도록 하는 데 있습니다.

<?php
declare(strict_types=1);
use NextPDF\Security\Signature\ByteRangeCalculator;
// Offsets the engine knows only after the whole PDF is written:
// $contentsStart — byte just before the '<' of the hex signature
// $contentsEnd — byte just after the '>' that closes it
// $fileLength — total file size in bytes
$range = ByteRangeCalculator::calculate(
contentsStart: $contentsStart,
contentsEnd: $contentsEnd,
fileLength: $fileLength,
);
// $range === [0, $contentsStart, $contentsEnd, $fileLength - $contentsEnd]
// Segment 1: file start → just before the signature value
// Segment 2: just after the signature value → end of file
// The signature value itself is the gap. It is never hashed.
$signedMessage = ByteRangeCalculator::extractSignedData($pdfBytes, $range);
// $signedMessage is segment 1 concatenated with segment 2 — exactly the
// bytes the cryptographic digest is computed over.

두 세그먼트 사이의 간격이 바로 서명 값입니다. 서명 값은 자기 자신의 다이제스트에 포함될 수 없으므로, 범위는 하나가 아니라 두 조각으로 나뉩니다.

흔한 함정은 유효한 서명이 지금 보고 있는 파일 전체가 서명되었다는 뜻이라고 믿는 것입니다. 그렇지 않습니다. 유효한 서명은 선언된 범위 안의 바이트가 온전하다는 뜻입니다. 나중의 리비전은 두 번째 서명, 양식 데이터, 검증 자료와 같은 콘텐츠를 그 범위 바깥에 정당하게 덧붙일 수 있습니다. 첫 번째 서명은 유효한 상태로 유지되며, 추가된 부분에 대해서는 아무 말도 하지 않습니다. 올바른 뷰어는 서명이 “서명 시점에 존재하던 문서”를 다룬다고 알려 줄 뿐, “화면에 보이는 모든 바이트”를 다룬다고 말하지 않습니다. 이 둘을 같은 것으로 취급하는 것이 바로 서명된 문서가 서명된 것처럼 보이는 미서명 콘텐츠를 갖게 되는 이유입니다.

이 페이지는 구조를 설명할 뿐 신뢰를 설명하지는 않습니다. 올바르게 구성된 ByteRange와 CMS 객체는 바이트가 온전하다는 것과 어떤 키가 그것에 서명했는지를 알려 줍니다. 하지만 그것만으로는 그 키가 사용자가 생각하는 사람의 것인지, 그 인증서가 서명 시점에 유효했는지, 또는 이후에 폐기되었는지는 알려 주지 않습니다. 그것은 인증서 경로 및 폐기 관련 작업으로, 서명을 올바르게 검증하기에서 다룹니다. 또한 이 페이지는 서명이 언제 이루어졌는지를 독립 기관이 확인해 주는 문제를 다루지 않습니다. 스스로 주장한 서명 시각은 신뢰할 수 있는 시각이 아닙니다 — 타임스탬프와 신뢰할 수 있는 시각을 참고하세요. NextPDF는 여기서 설명한 구조를 구성합니다. 인증서, 신뢰 앵커, 그리고 타임스탬프 기관은 엔진이 아니라 사용자의 배포 환경에서 제공합니다.

엔진이 티어별로 제공하는 것은 구조를 구성하는 기능입니다:

PAdES signature structure (byte range, signature dictionary, detached CMS) — edition availability
Edition Availability
Core

PAdES B-B: 서명 딕셔너리, 고정 폭의 ByteRange, 그리고 이 페이지에서 설명한 분리형 CMS SignedData 객체입니다.

Pro

동일한 구조 위에 PAdES B-T — 서명 값에 대한 신뢰할 수 있는 타임스탬프 — 를 추가합니다.

Enterprise

장기 프로파일(B-LT, B-LTA)을 추가합니다. 즉, 내장된 검증 자료와 동일한 바이트 범위 기반 위에 쌓이는 문서 타임스탬프를 더합니다.

  • 서명 딕셔너리 — 서명 핸들러, SubFilter, ByteRange, 그리고 Contents 값을 지정하는 PDF 딕셔너리.
  • ByteRange — 서명 다이제스트가 다루는 정확한 바이트를 선언하는 (offset, length) 정수 쌍의 배열.
  • Contents — 서명 값을 담고 있는 16진수 항목(PDF 2.0의 경우 분리형 CMS SignedData 객체). 이 항목은 자기 자신의 다이제스트에서 제외됩니다.
  • CMS SignedData — 서명자의 인증서와 서명 바이트를 담는 암호화 메시지 구문(Cryptographic Message Syntax, RFC 5652) 구조.
  • PAdES — PDF 고급 전자 서명(PDF Advanced Electronic Signatures): ETSI EN 319 142 시리즈에 정의된, PDF용 CMS 서명에 대한 ETSI 프로파일.
  • 승인 서명MDP 권한이 0인 서명. 콘텐츠를 단언하되 이후 변경을 제약하지는 않습니다.
  • 인증 서명DocMDP 권한(MDP 13)을 지닌 서명으로, 이후 어떤 수정이 문서를 적합한 상태로 유지할 수 있는지를 제한합니다.