PDFiumPas는 개인 키가 절대 여러분의 프로세스 안에 있을 필요가 없도록 PAdES 서명을 두 번의 호출로 나눕니다. PreparePadesRemoteSignature는 고정 너비의 빈 /Contents 자리표시자를 담은 증분 업데이트를 작성하고, SHA-256 문서 다이제스트, 정확한 ByteRange, 준비된 파일의 지문을 담은 요청 레코드를 돌려줍니다. CompletePadesRemoteSignature는 여러분의 서명 서비스가 반환한 분리형(detached) CMS를 받아 그 예약된 자리에 넣습니다
이 두 호출 사이에는 몇 분, 몇 시간이 지날 수 있고, 프로세스가 재시작될 수 있으며, 작업이 다른 머신으로 옮겨갈 수도 있습니다. 바로 그 간극이 이 API가 이런 모양을 하고 있는 이유의 전부입니다
원격 키는 왜 일반적인 서명 호출을 쓸 수 없는가?
SignPadesBytes는 서명 연산이 호출 안에서 일어난다고 가정하기 때문입니다. 이 메서드는 증분 업데이트를 만들고, ByteRange에 대한 다이제스트를 계산하고, 서명하고, 결과를 기록하는 것을 모두 반환하기 전에 끝냅니다. 키가 Windows 인증서 저장소나 여러분이 로드한 PKCS#12 파일 안에 있을 때는 이것으로 정확히 맞습니다
키가 네트워크 HSM, 신뢰 서비스 제공자가 운영하는 적격 서명 생성 장치, 또는 사용자가 휴대폰에서 확인해야 하는 클라우드 서명 API 안에 있을 때는 불가능합니다. 이런 경우 그 순서는 함수 호출이 아니라 대화입니다. 여러분은 다이제스트를 보내고, 다른 무언가가 사람을 인증하며, CMS는 나중에 돌아옵니다. 동기 API는 2단계 인증이 필요할지도 모르는 연산에서 스레드를 블록하지 않고서는 "나중에"를 표현할 수 없습니다
2단계 프로토콜
1단계는 문서를 준비합니다. PDFiumPas는 서명 필드와 값 딕셔너리를 덧붙이고, /Contents 안에 16진수로 인코딩된 공간을 ContentsSize바이트만큼 예약하고, 그 예약된 공간 주변의 ByteRange를 계산한 다음, FormatVersion, PreparedFingerprint, DocumentDigest, 네 요소로 된 ByteRange, ContentsHexOffset, ContentsSize를 담은 TPadesRemoteSigningRequest를 만들어냅니다
여러분의 서명 서비스가 필요로 하는 값은 DocumentDigest 하나뿐입니다. 반환되는 CAdES SignedData가 메시지 다이제스트로 담아야 하는 SHA-256입니다. 레코드 안의 나머지 모든 것은 2단계가 자신이 완성하는 파일이 그 다이제스트가 계산된 바로 그 파일임을 증명할 수 있도록 존재합니다
uses
FPdfPades;
var
Options: TPadesRemoteSignOptions;
Request: TPadesRemoteSigningRequest;
Source, Prepared, Session: TFileStream;
begin
Options := TPadesRemoteSignOptions.Default;
Options.Reason := 'Approved by finance';
Options.Location := 'Lisbon';
Options.Name := 'A. Moreira';
Options.SigningTimeUtc := NowUtc;
Options.ContentsSize := 16384; // CMS를 위해 예약된 16진수 바이트
Source := TFileStream.Create('contract.pdf', fmOpenRead or fmShareDenyWrite);
Prepared := TFileStream.Create('contract.prepared.pdf', fmCreate);
try
PreparePadesRemoteSignature(Source, Prepared, Options, Request);
finally
Prepared.Free;
Source.Free;
end;
// 이후 실행이나 다른 머신이 마무리할 수 있도록 세션을 영속화
Session := TFileStream.Create('contract.signreq', fmCreate);
try
SavePadesRemoteSigningRequest(Session, Request);
finally
Session.Free;
end;
SendDigestToSigningService(Request.DocumentDigest);
end;
Complete는 무엇을 거부하며, 각 검사는 왜 존재하는가?
완성 단계는 원격 서명 설계가 보통 잘못되는 지점이므로, 검증은 의도적으로 가차 없습니다. CompletePadesRemoteSignature는 다음을 거부합니다. 지문이 더 이상 요청과 일치하지 않는 준비된 PDF, 기록된 자리표시자 좌표와 일치하지 않는 ByteRange, 수정된 /Contents 구분자, 더 이상 비어 있지 않은 자리표시자, 예약된 공간보다 큰 CMS, 정확히 하나의 DER 값이 아닌 CMS, 지원되지 않는 SignedData 형태, 누락된 signing-certificate-v2 속성, 그리고 메시지 다이제스트가 준비된 문서 다이제스트와 일치하지 않는 CMS입니다
이 검사들 각각은 실제 실패에 대응됩니다. 지문과 ByteRange 검사는 누군가 두 단계 사이에서 준비된 파일을 다시 생성한 경우를 잡아냅니다. 그런 경우 아무도 가지고 있지 않은 바이트에 대해 검증되는 서명이 만들어질 것입니다. 빈 자리표시자 검사는 이중 완성, 즉 이미 존재하는 서명 위에 두 번째 CMS가 덮어써지는 경우를 잡아냅니다. 메시지 다이제스트 검사는 그중 가장 위험한 경우, 즉 다른 문서에 대해 올바르게 형성된 CMS로 서명한 경우를 잡아냅니다. 이는 큐가 동시에 진행 중인 두 서명 세션을 뒤섞을 때 벌어지는 일입니다. 이 검사가 없다면 서명된 것처럼 보이지만 어디서나 검증에 실패하는 파일, 또는 더 나쁘게는 다른 누군가의 승인을 담은 파일이 만들어질 것입니다
signing-certificate-v2 요구사항은 무결성 문제라기보다는 PAdES 규격 준수의 문제입니다. ETSI EN 319 142는 서명 인증서가 서명된 속성 안에 결합되어 있을 것을 요구하며, 그 속성이 없는 CMS는 암호학적으로는 검증되더라도 PAdES 서명이 아닙니다. 완성 단계에서 이를 거부한다는 것은 고객의 검증기 보고서가 아니라 바로 여기서 문제를 발견한다는 뜻이며, 이 주제는 검증기가 PAdES 서명을 거부하는 이유에서 더 자세히 다룹니다
var
Request: TPadesRemoteSigningRequest;
Session, Prepared, Dest: TFileStream;
CmsDer: TBytes;
begin
Session := TFileStream.Create('contract.signreq', fmOpenRead);
try
Request := LoadPadesRemoteSigningRequest(Session);
finally
Session.Free;
end;
CmsDer := FetchDetachedCmsFromService; // HSM이나 TSP가 반환함
Prepared := TFileStream.Create('contract.prepared.pdf', fmOpenRead);
Dest := TFileStream.Create('contract.signed.pdf', fmCreate);
try
try
CompletePadesRemoteSignature(Prepared, Dest, Request, CmsDer);
except
on E: EPadesCrypto do
// 모든 거부는 구체적인 사유를 담고 있으니 그대로 로그에 남길 것
FailSession(E.Message);
end;
finally
Dest.Free;
Prepared.Free;
end;
end;
프로세스와 머신 경계를 넘나들기
SavePadesRemoteSigningRequest와 LoadPadesRemoteSigningRequest는 세션을 안정적인 버전 관리 바이너리 형식으로 직렬화하는데, 바로 이것이 이 설계를 단지 올바를 뿐 아니라 실용적으로 만들어줍니다. 웹 애플리케이션은 한 요청에서 문서를 준비하고, 준비된 PDF와 세션 블롭을 저장하고, 스마트카드 서명을 위해 브라우저에 다이제스트를 돌려준 다음, 완전히 다른 요청 핸들러에서 파일을 완성할 수 있습니다
FormatVersion 필드는 업그레이드 전반에 걸쳐 이를 안전하게 유지해 주는 요소입니다. 이전 빌드가 작성한 세션을 더 새 빌드가 로드하면, 다른 모양의 레코드로 잘못 해석되는 대신 명시적으로 인식되거나 거부됩니다. 여러분의 큐가 며칠씩 세션을 보관할 수 있다면, 포맷 버전을 구현 세부 사항이 아니라 로그에 남길 가치가 있는 운영상의 사실로 취급하십시오
자리표시자 크기 정하기
ContentsSize는 여러분이 반드시 고민해야 할 유일한 매개변수입니다. CMS가 존재하기도 전에 고정되기 때문입니다. 이 값은 16진수로 인코딩된 예약 공간을 셉니다. 그래서 6KB짜리 DER CMS는 최소 12KB의 공간이 필요하며, 구현은 이 예약을 64MiB로 제한합니다
너무 적게 예약하면, 여러분의 서명 서비스가 이미 작업을 마친 뒤에 완성 단계가 CMS 초과 오류로 실패하며, 이는 종량제 적격 서명 서비스에서는 낭비된 연산을 뜻합니다. 너무 많이 예약하면 모든 서명된 문서가 영원히 그 패딩을 짊어지게 됩니다. 합리적인 접근법은 측정하는 것입니다. 실제 인증서 체인으로 문서 하나에 서명하여 DER 길이를 확인하고, 16진수로 두 배로 늘린 다음, T-레벨 서명으로 업그레이드할 계획이라면 타임스탬프 토큰을 위한 여유 공간을 넉넉히 더하십시오. 여러 중간 인증서와 긴 OCSP 응답을 가진 체인은 사람들이 예상하는 것보다 빠르게 커집니다
서명 이후에 오는 것
완성된 원격 서명은 PAdES B-B입니다. 장기 검증에는 타임스탬프와 검증 자료가 필요한데, 이는 DSS와 서명별 VRI 딕셔너리를 추가하는 별도의 증분 업데이트이며, RFC 3161 타임스탬프와 DSS를 활용한 장기 서명에서 설명합니다. 이 단계는 로컬에서 이루어집니다. 인증서, OCSP 응답, CRL을 추가할 뿐이며, 그 어느 것도 개인 키를 필요로 하지 않습니다
배포하기 전에, 신뢰 당사자(relying party)가 사용할 것과 같은 코드 경로로 여러분이 만들어낸 것을 검증하십시오. 디지털 서명과 PAdES 레벨 검사하기에서 다룹니다. 서명과 검증은 서로 다른 코드이며, 원격 서명 파이프라인은 바로 외부 검증기가 지적할 때까지 아무도 눈치채지 못한 채 이 둘이 서로 어긋날 수 있는 지점입니다
PDFiumPas는 PDFium 엔진을 감싸는 Delphi와 Lazarus용 컴포넌트로, 네이티브 Pascal PAdES 스택을 갖추고 있어서 서명, 타임스탬프, 검증이 외부 명령줄 도구 없이 작동합니다. 전체 API 문서와 평가판은 PDFium Delphi 컴포넌트 페이지에 있습니다