HotPDF는 파일 단위가 아니라 리비전 단위로 ISO/TS 32004 PDF MAC을 검증합니다. THotPDF.ValidatePDFMACChain은 체인 앵커부터 모든 증분 업데이트를 순회하며 각 MAC을 그 리비전 자신의 startxref와 %%EOF에서 끝나는 읽기 전용 프리픽스 스트림으로 검증합니다. 최신 리비전의 MAC이 하나 유효하더라도 그 아래 리비전에 대해서는 아무것도 증명하지 않습니다
이 모든 것의 동기가 되는 시나리오는 다음과 같습니다. PDF MAC을 붙인 AES-256 암호화 PDF를 배포합니다. 누군가 헥스 에디터로 파일을 열어 첫 번째 MAC 보호 리비전 안의 바이트 하나를 뒤집고, 자기 자신의 완전히 유효한 MAC을 실은 새 리비전을 맨 뒤에 덧붙입니다. 모든 뷰어는 아무 불평 없이 파일을 열고, 활성 트레일러의 MAC에 대해 현재 바이트 범위를 해시하는 순진한 검사기는 성공을 보고합니다. 그 MAC이 자신이 덮는 바이트에 대해서는 실제로 올바르기 때문입니다. 손상은 두 리비전 아래, 아무도 다시 검사하지 않은 영역에 앉아 있습니다
유효한 최상위 MAC이 파일 무결성을 증명하지 못하는 이유
PDF MAC은 문서가 아니라 프리픽스를 덮기 때문입니다. 증분 업데이트는 포맷의 일급 시민입니다. 저장할 때마다 새 본문, 새 크로스 레퍼런스 섹션, 새 트레일러가 덧붙여지고 오래된 바이트는 정확히 제자리에 남습니다. ISO/TS 32004는 그 모델 위에 얹혀 있어서 각 리비전이 그 순간의 파일을 인증하는 자기 자신의 /AuthCode 사전을 실으며, 최신 것만 검증하면 이전 리비전은 전부 검사되지 않은 채로 남습니다. 그래서 HotPDF는 두 질문을 두 호출로 노출하는데, 그 차이가 이 글의 전부입니다. ValidatePDFMAC는 “현재 리비전이 진본인가”에 답하며 THPDFPDFMACValidationInfo 레코드를 채웁니다. ValidatePDFMACChain은 “이 파일의 모든 MAC 보호 리비전이 진본인가”에 답하며 리비전별 배열과 기계 판독 가능한 실패 이유를 실은 THPDFPDFMACChainValidationInfo를 채웁니다. 위의 변조 후 재 MAC 파일에서 첫 호출은 True를, 둘째 호출은 리비전 인덱스 1에 대해 False를 돌려줍니다
var
Pdf: THotPDF;
Chain: THPDFPDFMACChainValidationInfo;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if not Pdf.ValidatePDFMACChain('incoming.pdf', 'user', Chain) then
begin
// 실패는 pmcfRevisionBoundary, pmcfNoPDFMAC,
// pmcfRequiredRevisionMissing, pmcfRevisionInvalid,
// pmcfKDFSaltChanged, pmcfDigestDowngrade, pmcfPermissionDowngrade 중 하나다
Writeln('chain rejected: ', Chain.Message);
Writeln('revision ', Chain.FailureRevisionIndex,
' at xref offset ', Chain.FailureXRefOffset);
Exit;
end;
for I := 0 to High(Chain.Revisions) do
Writeln(I, ' len=', Chain.Revisions[I].RevisionLength,
' mac=', Chain.Revisions[I].HasPDFMAC,
' perms=', Chain.Revisions[I].PermissionsAuthenticated);
finally
Pdf.Free;
end;
end;
각 MAC은 자신의 프리픽스 스트림으로 검증된다
이 영역에서 가장 비싼 버그는 오래된 리비전을 재해시할 때 최종 파일 크기를 상한으로 쓰는 것입니다. 그러면 뒤따르는 바이트가 최신을 제외한 모든 다이제스트에 접혀 들어가고 정상 파일에서 변조를 보고합니다. HotPDF는 대신 각 리비전마다 그 리비전 자신의 startxref 값과 그 뒤의 %%EOF에서 끝나는 경계가 있는 읽기 전용 스트림을 재구성하고 그것만 해시합니다. 경계 찾기는 보이는 것보다 성가십니다. 리터럴 %%EOF는 콘텐츠 스트림이나 문자열 안에 나타날 수 있으므로, 후보는 바로 앞의 startxref가 검증 중인 섹션의 크로스 레퍼런스 오프셋과 같은 숫자로 파싱되고 둘 사이에 공백 외에 아무것도 없을 때만 받아들여집니다. 리비전은 그 다음에 마커 뒤의 줄바꿈 시퀀스 하나 — CR 하나, LF 하나, 또는 CRLF 한 쌍 — 를 정확히 흡수하고 그 이상은 흡수하지 않습니다. 마지막 규칙은 실무에서 물어 뜯습니다. 두 리비전 사이에 빈 줄을 하나 더 내보내는 기록기는 다음 리비전에 속하는 바이트를 만들어 낸 것이고, 뒤따르는 모든 공백을 이전 리비전에 삼켜 버리면 두 다이제스트가 조용히 바뀝니다. 섹션 나열은 같은 규율을 따릅니다. HotPDF는 크로스 레퍼런스 섹션을 오래된 것부터 최신까지 정확히 한 번 순회하며 free, direct, object-stream 항목을 재생해서 뒤 섹션이 앞 상태를 덮어쓰게 하는데, 이는 활성 xref 파서가 적용하는 먼저 본 것이 이기는 의미론의 정반대입니다
체인은 어디에 앵커되고 무엇이 그것을 깨는가
유효한 /AuthCode를 실은 첫 리비전이 앵커이고, FirstMACRevisionIndex가 보호가 시작되는 지점을 보고합니다. 그 이전은 구조상 보호되지 않은 것이며 그것이 정상입니다. 그 이후는 전부 MAC 보호여야 하므로, MAC 보호 파일에 평범한 증분 업데이트 하나를 덧붙이면 pmcfRequiredRevisionMissing과 문제의 리비전 인덱스로 실패합니다. 간격을 용납하면 공격자가 한 번 더 저장하는 것만으로 보호를 벗겨낼 수 있기 때문입니다. 세 가지 불변식이 체인 전체에 걸쳐 성립하며, 각자 자기 실패 코드를 갖습니다
pmcfKDFSaltChanged—/KDFSalt는 앵커 이후로 안정을 유지해야 합니다. 돌아가는 솔트는 위조자가 자기가 고른 매개변수로 키를 다시 유도하게 해 주므로pmcfDigestDowngrade— 다이제스트 강도는 바로 앞 리비전이 아니라 마지막으로 검증된 MAC과 비교되므로, Modern 프로파일로 SHA-384에서 시작한 체인이 조용히 SHA-256으로 이어질 수 없습니다pmcfPermissionDowngrade— 리비전은 이전 리비전이 인증했던 PDF MAC 요구를 지울 수 없습니다
새겨둘 가치가 있는 귀결은, 역사적 MAC은 더 이상 활성 트레일러가 아니게 된 뒤에도 독립적으로 검증된다는 것입니다. 그래서 도입부의 오래된 리비전을 고치고 새 MAC을 덧붙이는 공격은 살아남지 못합니다. 최신 MAC은 자기 것으로 통과하고 ValidatePDFMAC는 기뻐하지만, 체인은 여전히 pmcfRevisionInvalid로 리비전 1에 걸립니다
시그니처 순서: 트레일러 키가 먼저, signatureDigest가 마지막
MAC이 단독이 아니라 CMS 시그니처에 붙을 때는 기록 순서가 취향의 문제가 아니게 됩니다. HotPDF는 /AuthCode, /KDFSalt, ISO 32004 개발자 확장, /SigObjRef가 시그니처 /ByteRange가 계산되기 전에 같은 리비전에 기록되기를 요구합니다. 그중 하나라도 나중에 덧붙이면 그 바이트들은 시그니처가 덮는 범위 밖에 착지하고, 시그니처는 검증되는데 MAC 바인딩은 서명되지 않은 파일이 만들어집니다. 두 다이제스트는 그 다음 반대 방향으로 흐르는데, 언뜻 순환처럼 보이고 아닙니다. PDF MAC signatureDigest는 CMS SignerInfo.signature OCTET STRING의 날 콘텐츠 옥텟을 묶습니다. CMS DER 전체가 아니고 서명된 속성도 아닙니다. 그래서 날 시그니처 값이 존재한 뒤에 구성되고 id-attr-pdfMacData 비서명 속성으로 주입됩니다. /Contents는 시그니처 ByteRange에서 제외되고 비서명 속성은 시그니처 계산에 결코 먹히지 않으므로, 시그니처 생성, MAC 구축, CMS 포장의 순서는 암호학적 루프 없이 깨끗하게 닫힙니다. 두 가지 따름정리가 따릅니다. /ByteRange 센티널과 /Contents 플레이스홀더는 암호화 파일에서도 평문으로, 객체 스트림 밖에 남아 있어야 합니다. 그렇지 않으면 고정폭 패처가 그것들을 찾을 수 없습니다. 그리고 MAC 다이제스트도 SHA-256일 때는 서명 다이제스트를 그대로 재사용하고, 그렇지 않으면 두 다이제스트 컨텍스트가 출력 스트림 한 번의 패스로 갱신됩니다
var
Pdf: THotPDF;
Options: THPDFPDFMACOptions;
Info: THPDFPDFMACValidationInfo;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.AutoLaunch := False;
Pdf.FileName := 'unsigned.pdf';
Pdf.ActivateProtection := True;
Pdf.CryptKeyLength := aesgcm;
Pdf.OwnerPassword := 'owner';
Pdf.UserPassword := 'user';
Pdf.ProtectOptions := [prPrint, prExtractContent];
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 12);
Pdf.CurrentPage.AddSignedSignatureField('Approval',
Rect(72, 120, 280, 160), 16384);
Pdf.EndDoc;
Options := THPDFPDFMACOptions.Modern; // SHA-384 문서 다이제스트
if THotPDF.SignPDFWithPFXAndAttachedPDFMAC('unsigned.pdf',
'signed.pdf', 'signer.pfx', 'pfx-secret', 'user', Options) then
if Pdf.ValidatePDFMAC('signed.pdf', 'user', Options, Info) then
begin
// Location = pmlAttachedToSignature이고 두 다이제스트는
// 따로 보고된다
Writeln('signature object : ', Info.SignatureObjectNumber);
Writeln('signature digest : ', Info.SignatureDigestMatched);
Writeln('full file coverage: ', Info.FullFileCoverage);
Writeln('perms authentic : ', Info.PermissionsAuthenticated);
end;
finally
Pdf.Free;
end;
end;
검증은 같은 경로를 반대쪽 끝에서 다시 밟습니다. 현재 활성인 클래식 크로스 레퍼런스 트레일러에서 direct /AuthCode를 읽고, 세대를 인식하는 indirect /SigObjRef를 따라가며, 그것이 하나뿐인 시그니처 필드의 /V를 묶는지 확인하고, 문서 다이제스트 실패를 시그니처 다이제스트 실패와 갈라 보고합니다. 둘은 다른 진단이고, 하나의 불리언으로 뭉개면 페이지 콘텐츠가 건드렸는지 시그니처 값이 건드렸는지 알려 주는 유일한 정보를 버리는 셈입니다. 이미 CMS 작업을 하고 있다면 이것은 PAdES 서명 글과 불러들인 문서에서 시그니처를 검증하는 가이드 곁에 놓입니다
/P를 믿지 마라: 16바이트 /Perms를 먼저 복호화하라
ISO/TS 32004는 “이 문서는 PDF MAC을 요구한다”를 권한 비트 13으로 알리는데, 눈에 띄는 읽는 방법이 틀린 방법입니다. 암호화 사전의 /P 정수는 평문이고 인증되지 않아서, 누구든 텍스트 에디터로 그 비트를 뒤집고 요구를 다운그레이드할 수 있기 때문입니다. ISO 32000-2 §7.6은 /Perms 항목에 답을 공급하고 HotPDF는 그것을 씁니다. 16바이트 /Perms 문자열을 파일 암호화 키로 AES-256 CBC, 제로 IV, 패딩 없이 복호화한 다음, 아무것도 믿기 전에 평문의 모든 필드를 검사합니다. 바이트 1부터 4는 권한 값을 리틀 엔디안 순서로 담고 /P 정수와 정확히 같아야 합니다. 바이트 5부터 8은 0xFF입니다. 바이트 9는 T 또는 F 메타데이터 암호화 플래그입니다. 바이트 10부터 12는 리터럴 마커 adb입니다. 그 모두가 성립할 때만 PermissionsAuthenticated가 True가 되고 비트 13이 읽힙니다. 그리고 극성을 조심하십시오. MAC 요구는 0x1000 비트가 꺼져 있을 때 주장됩니다. /P와 복호화된 권한 사이의 불일치는 기록하고 넘어갈 경고가 아닙니다. 그것은 위조된 권한 집합이고 올바른 응답은 fail close입니다
알고리즘 민첩성은 다이제스트에서 멈춘다
ISO/TS 32004는 문서 다이제스트를, 그리고 오직 문서 다이제스트만 고르게 허용합니다. HotPDF는 pmdaSHA256부터 pmdaSHA3_512에 이르는 가변 THPDFPDFMACDigestAlgorithm 아래에 HMAC-SHA-256 인증, RFC 5869의 HKDF-SHA-256 키 유도, RFC 3394의 AES-256 키 랩을 고정해 둡니다. “SHA3-512 프로파일”을 HMAC까지 바꿔도 되는 허가로 다루는 것이 자연스러운 실수인데, 그러면 상호 운용 가능한 어떤 의미에서도 더 이상 PDF MAC이 아닌 파일이 나오기 때문입니다. 자기 검증기를 쓴다면 복사할 가치가 있는 구현 세부가 하나 있습니다. 바이트 범위를 해시하기 전에 CMS AuthenticatedData에서 다이제스트 OID를 읽으십시오. SHA-256을 하드코딩하고 나중에 맞춰 보면 민첩성이 라벨로 전락하고, 적대적 파일이 알고리즘이 처음부터 지원되지 않았다는 걸 발견하기 전에 문서 전체를 스트리밍하게 만들 수 있습니다. CMSAlgorithmProtection, AuthenticatedData 다이제스트 알고리즘, integrity-info messageDigest, 바이트 범위 다이제스트가 모두 하나의 알고리즘을 이름 지어야 하고, 어떤 불일치든 fail close입니다
var
Options: THPDFPDFMACOptions;
begin
Options := THPDFPDFMACOptions.Compatibility; // SHA-256, 여섯 개 모두 수용
Options := THPDFPDFMACOptions.Modern; // SHA-384, 256비트 거부
Options := THPDFPDFMACOptions.HighAssurance; // SHA3-512만, AES-GCM
// 커스텀 프로파일은 합법이지만 그것이 생성에 쓰는 알고리즘은
// 검증 허용 목록에도 나타나야 하고, 그렇지 않으면 설정은
// 바이트 하나 기록되기 전에 거부된다
Options.Profile := pmppCustom;
Options.DigestAlgorithm := pmdaSHA512;
Options.AllowedDigestAlgorithms := [pmdaSHA512, pmdaSHA3_512];
Options.RequireAESGCM := True;
end;
PDF MAC이 증명하는 것과 증명하지 않는 것
검증된 PDF MAC 체인은 모든 보호 리비전이 파일 암호화 키를 쥔 누군가가 기록한 것과 바이트 단위로 동일하다는 것, 보호 리비전이 하나도 제거되거나 재배치되지 않았다는 것, 그리고 앵커 뒤에 보호되지 않은 리비전이 덧붙여지지 않았다는 것을 증명합니다. 바로 평범한 AES-256 암호화가 열어 두는 공격 부류입니다. 기밀성은 무결성에 관해 아무 말도 하지 않고, 봉합된 리비전을 가진 암호화 PDF는 온전한 것만큼 쾌활하게 복호화되기 때문입니다. 증명하지 않는 것은 저작입니다. MAC 키는 파일 암호화 키에서 유도되므로, 문서를 열 수 있는 사람이라면 누구든 합법적 수신자 전원을 포함해서 수정된 버전 위에 유효한 MAC을 만들 수 있습니다. 대칭 프리미티브이고, 대칭 프리미티브는 귀속할 수 없습니다. 무엇을 누가 바꿨는지 알아야 한다면 인증서가 뒷받침하는 디지털 시그니처가 필요하고, PDF MAC은 시그니처만으로는 덮지 못하는 증분 구조를 보호하며 그것을 보완합니다. 둘을 계층으로 다루고 두 판정을 하나의 상태 아이콘으로 뭉개지 말고 독립적으로 보고하게 하십시오
여기서 기술한 PDF MAC 진입점들 — AddStandalonePDFMAC, SignPDFWithPFXAndAttachedPDFMAC, ValidatePDFMAC, ValidatePDFMACChain — 은 델파이와 C++Builder용 표준 HotPDF Delphi Component와 함께 배송되며, 제품 페이지는 옵션 레코드, 상태 열거형, 리비전별 검증 배열의 전체 레퍼런스를 싣습니다