기술 문서

PDF 서명 후 무엇이 바뀌었는지 분류하기

PDF에 대한 서명은 이후 변경을 금지하지 않습니다. 서명은 바이트 범위를 고정할 뿐이고, 증분 업데이트는 그 뒤에 새 바이트를 덧붙이므로, 문서에 새 콘텐츠가 생기는 동안에도 서명은 수학적으로 유효하게 남습니다. 그 콘텐츠가 받아들일 만한 것인지는 정책 문제이며, DocMDP는 저자가 정책을 밝히는 자리입니다. 전혀 변경 없음, 양식 작성과 서명만, 또는 여기에 주석까지입니다. 정책을 강제한다는 것은 실제로 무엇이 바뀌었는지 분류하는 것이고, 그것이 AnalyzeModifications가 하는 일입니다. 이전 리비전을 가리키게 한 뒤, 전체 판정은 GetModificationLevel에서 읽고 각 차이의 레벨, 오브젝트 번호, 설명은 개별 finding 접근자에서 읽습니다

이것이 갖춰지면 DocMDP 강제는 비교 한 번으로 수렴합니다. 계산된 레벨이 정책이 허용하는 레벨 이하인가의 문제입니다

mlNone부터 mlUnclassified까지 PDFlibPas 수정 레벨 사다리를 보여 주고 DocMDP 정책 강제가 Delphi에서 한 번의 비교임을 나타내는 도식
TPLModificationLevel 사다리는 mlNone부터 mlUnclassified까지 이어지며, DocMDP 강제는 계산된 레벨을 정책과 비교하는 일로 줄어듭니다

서명된 PDF가 바뀌는 것이 예상되는 이유

적법한 사례는 세 가지이며, 여러분이 보게 될 대부분을 커버합니다. 두 번째 서명자가 자신의 서명을 추가합니다. 수신자가 저자가 열어 둔 양식 필드를 작성합니다. 그리고 장기 검증 자료가 덧붙여집니다. OCSP 응답과 CRL이 문서 보안 저장소(document security store)에 기록되어 응답기가 사라진 뒤에도 서명이 검증 가능하게 유지됩니다. 마지막 사례는 허용될 뿐이 아니라, 잘 관리된 아카이브가 서명 문서에 의도적으로 수행하는 바로 그 작업입니다

그러므로 "서명 후 파일이 커졌다"는 것은 아무 정보도 주지 않습니다. 질문은 언제나 무엇이 추가되었는가이고, 답은 바이트를 감시하는 것이 아니라 문서 상태를 비교하는 데서 나와야 합니다. 증분 추가(append)의 역학 자체는 증분 업데이트 문서에서 다룹니다

만들어진 경로가 아니라 오브젝트의 형태로 분류합니다

분류기는 변경 후 오브젝트가 무엇인지를 보지, 어떤 라이브러리 호출이 만들었는지를 보지 않습니다. 이는 의도된 설계입니다. 분석은 다른 소프트웨어가 만든 파일을 대상으로 돌아가며, 그런 파일에는 들여다볼 호출 경로가 없기 때문입니다

네 가지 형태를 인식합니다. 문서 보안 저장소와 검증 관련 정보 사전, 크로스 레퍼런스 스트림 오브젝트, 카탈로그 metadata 엔트리, 바이트 범위를 담은 서명 사전은 장기 아카이브 자료입니다. 필드 타입과 필드 값을 함께 담은 오브젝트는 양식 작성입니다. 타입이 annotation인 오브젝트, 또는 서브타입이 ISO 32000-2 Table 168에 나열된 것 중 하나인 오브젝트는 주석 변경입니다. 그 밖의 모든 것은 미분류(unclassified)입니다

PDFlibPas가 변경된 각 PDF 오브젝트에 적용하는 결정 트리로, 업데이트를 archive, form filling, annotation, unclassified 레벨로 분류
변경된 각 오브젝트는 그것이 무엇인지, 즉 보안 저장소, xref 스트림, 필드, 주석인지로 분류되며 만들어낸 호출로 분류되는 일은 없습니다

삭제는 추가보다 엄격하게 다룹니다. 삭제된 오브젝트는 오래된 쪽 오브젝트가 스스로 아카이브 자료였을 때만 화이트리스트에 들어가며, 이는 보안 저장소가 더 새것으로 교체되는 정상 사례를 커버합니다. 그 외 모든 삭제는 미분류입니다. 서명된 문서에서 콘텐츠를 지우는 것은 어떤 권한 레벨도 승인하지 않는 행위이기 때문입니다. 문서 수준 차이는 더 엄격합니다. 페이지 수 변화는 개별 오브젝트를 들여다보지 않고 바로 미분류로 갑니다. 어떤 DocMDP 레벨도 페이지 추가나 삭제를 허용하지 않기 때문입니다

화이트리스트는 거부하는 쪽으로 오류를 저지릅니다

이것이 모든 애매한 판단을 지배하는 설계 규칙입니다. 허용으로 잘못 분류된 변경은 저자가 결코 승인하지 않은 콘텐츠 위에서 통과되는 서명입니다. 미분류로 잘못 분류된 변경은 플래그가 붙고 사람이 검토하게 되는 문서입니다. 두 오류는 대칭이 아니므로 화이트리스트는 좁게 유지되고, 인식되지 않는 형태는 추측하지 않고 미분류로 떨어집니다

여기에는 미리 예상할 가치가 있는 실무적 결과가 있습니다. 특이한 생산자의 파일은 들여다보면 무해한 미분류 변경을 가끔 보고합니다. 올바른 대응은 화이트리스트를 넓히는 것이 아니라 finding 상세와 오브젝트 번호를 확인하는 것입니다. 개별 보고를 잠재우려고 커지는 화이트리스트는 더 이상 보안 통제가 아니기 때문입니다

uses
  PDFlibrary, PDFlibCompare;

var
  Pdf: TPDFlib;
  I, Level: Integer;
begin
  Pdf := TPDFlib.Create(nil);
  try
    Pdf.LoadFromFile('contract-countersigned.pdf', '');
    if Pdf.AnalyzeModifications('contract-as-signed.pdf', '') < 0 then
      raise Exception.Create('the earlier revision could not be loaded');

    // TPLModificationLevel은 mlNone, mlLTAUpdates, mlFormFilling,
    // mlAnnotations, mlUnclassified 순서이며 getter는 서수를 반환합니다
    Level := Pdf.GetModificationLevel;
    // DocMDP 강제는 이제 정책에 대한 비교 한 번입니다
    if Level > Ord(mlFormFilling) then
      for I := 0 to Pdf.GetModificationFindingCount - 1 do
        Report.Add(Format('object %d, level %d: %s',
          [Pdf.GetModificationFindingObjNum(I),
           Pdf.GetModificationFindingLevel(I),
           Pdf.GetModificationFindingDetail(I)]));
  finally
    Pdf.Free;
  end;
end;

전체 레벨은 모든 finding의 최댓값이며, 이것이 유일하게 방어 가능한 집계입니다. 아카이브 추가 99개와 미분류 변경 하나를 담은 문서는 미분류 변경이 있는 문서입니다

그 아래: 암호학적 해시가 아니라 지문(fingerprint)

CompareWith가 노출하고 수정 분석이 기반으로 삼는 비교 엔진은 SHA-256이 아니라 비암호학적 64비트 해시를 써서 정규화된 본문의 지문(fingerprint)으로 오브젝트를 식별합니다. 숙고된 선택입니다. 구조 비교에 필요한 것은 결정성입니다. 같은 오브젝트 본문은 실행 안에서 항상 같은 지문을 내야 합니다. 충돌 저항은 필요하지 않습니다. 비교의 양쪽을 모두 통제하는 공격자는 이미 다른 수단으로 이겼고, 백만 오브젝트 문서의 모든 오브젝트에 완전한 암호학적 해시 비용을 치르는 것은 이득 없는 실비용입니다

해시 선택보다 더 중요한 정규화 규칙 두 가지가 있습니다. 간접 참조는 참조된 콘텐츠로 펼쳐지는 대신 플레이스홀더 토큰으로 접힙니다. 펼치면 공유 오브젝트의 본문이 모든 참조자에게 복사되므로, 공유 폰트 디스크립터에 사소한 편집 하나가 그것에 닿는 모든 오브젝트의 지문을 무효로 만들고 보고서는 읽을 수 없게 됩니다. 그리고 오브젝트 번호 자체는 지문에서 제외됩니다. 재작성은 아무 의미론적 변화 없이 오브젝트 번호를 다시 매길 수 있기 때문입니다

매칭은 두 패스로 돕니다. 먼저 지문으로 정렬하고, 나머지는 오브젝트 번호로 짝지어 추가와 삭제의 짝이 아니라 변경으로 식별합니다. 값싼 검사가 전 과정에서 먼저 옵니다. 페이지 수 차이는 어떤 오브젝트 순회도 시작하기 전에 보고됩니다

PDFlibPas의 2패스 PDF 리비전 diff: 페이지 수 검사 먼저, 64비트 지문, 지문 정렬 후 오브젝트 번호 짝짓기
비교 엔진은 정규화된 오브젝트 본문에 지문을 매기고 페이지 수 차이를 먼저 보고한 뒤 지문과 오브젝트 번호로 매칭합니다

함정: 자기 자신과의 비교가 동일하다는 보장은 없습니다

diff 엔진의 자연스러운 첫 테스트는 파일을 자기 자신과 비교해 결과가 동일하다고 단정하는 것입니다. 이 단정은 여기서는 성립하지 않고, 이유가 교훈적입니다. 공개 로드 경로와 하위 수준 문서 로드 경로는 디코딩을 동일하게 구성하지 않으므로, 같은 파일이라도 두 경로로 로드하면 일부 오브젝트의 지문이 달라질 수 있습니다. 엔진이 틀린 것이 아닙니다. 두 로드는 실제로 서로 다른 메모리 내 상태를 만들어 냈습니다

두 경로를 억지로 맞추는 대신 비교 의미론을 좁게 밝혀 둡니다. 분석은 현재 문서 상태를 이전 리비전과 비교하고, 두 지문 집합이 정확히 일치할 때만 동일하다고 보고합니다. 이것이 사용자가 실제로 묻는 질문이며, 두 로더가 서로 바뀌어 쓸 수 있기를 요구하지 않습니다. 비교 기능을 설계할 때 "같다"가 무엇인지 정의하는 일은 그것을 계산하는 일보다 작업 비중이 큽니다

어디에 쓰는가

두 곳입니다. 검증 보고서에서 서명 검사와 나란히, 검토자가 서명이 암호학적으로 온전한지뿐 아니라 그 후 문서에 무슨 일이 있었는지도 보게 합니다. 서명 쪽은 PAdES 서명 및 검증에서 다룹니다. 그리고 수수문 게이트(intake gate)에서, 외부에서 도착한 문서를 보낸 사본과 대조해, 주석이 추가되어 돌아온 계약서와 페이지가 편집된 계약서를 다르게 취급합니다

범위에 관한 한 가지 주의입니다. 이 분석은 같은 문서 계보의 두 리비전 사이에 무엇이 바뀌었는지를 알려 줍니다. 보이는 콘텐츠가 오도적인지, 양식 필드 appearance 스트림이 값과 일치하는지, 오버레이 아래 숨겨진 텍스트가 콘텐츠 스트림에 여전히 존재하는지는 알려 주지 않습니다. 이것들은 별도의 처리가 필요하며, 콘텐츠 제거 쪽은 진짜 레닥션(redaction) 문서에서 다룹니다. 분석과 비교 진입점은 losLab PDF Developer Library 제품 페이지에 문서화되어 있습니다