기술 문서

Delphi PDF 인증서 암호화: RSA-OAEP와 ECDH

인증서 암호화 PDF는 파일이 누구를 위해 쓰였든 암호화 딕셔너리에 /P 값을 하나만 실습니다. 이 글의 뒤에서 만들 파일들에서는 -64입니다. 세 명의 수신자가 서로 다른 권한 세트를 가져도 그 하나의 숫자를 공유합니다. /P에서 "이 사용자가 인쇄할 수 있나?"를 답하는 코드는 자리표시자를 읽고 있는 것인데, 공개키 security handler 아래에서 진짜 권한 단어는 각 수신자의 암호화 엔벨로프 안을 여행하며 그 수신자의 개인 키가 열어야만 나타나기 때문입니다

이 설계가 인증서 암호화의 수고를 감수할 이유입니다. 분기 감사 패키지가 외부 리뷰어 셋에게 갑니다. 법무는 셋 모두 읽기를 원하고 한 명만 인쇄가 허용되며, 누구도 첨부 파일 곁의 메일 스레드에 비밀번호가 앉기를 원하지 않습니다. 비밀번호는 그것을 표현할 수 없습니다. 인증서는 됩니다. 각 수신자는 이미 쥐고 있는 키로 파일을 열고 각 엔벨로프는 자기 권한 단어를 실어 다닙니다

인증서 기반 PDF 암호화는 비밀번호와 무엇이 다를까?

공개키 암호화 PDF는 파일 키를 무작위 시드에 모든 수신자 엔벨로프의 정확한 바이트를 더한 것에서 파생하며, 사람이 타이핑하는 것은 계산에 들어가지 않습니다. 핸들러는 ISO 32000-1 §7.6.4(ISO 32000-2에서는 §7.6.5)에 규정되어 있고 엔벨로프는 RFC 5652의 CMS EnvelopedData 구조입니다. HotPDF는 /Filter /Adobe.PubSec를 /SubFilter /adbe.pkcs7.s5와 함께 씁니다. AES-256에서는 /V 5, /CF 아래 /CFM /AESV3를 갖는 /DefaultCryptFilter 항목, 그리고 최상위 딕셔너리가 아니라 그 crypt filter 안에 놓이는 /Recipients 배열을 뜻합니다

각 엔벨로프는 24바이트를 암호화합니다. 20바이트 시드에 그 수신자의 32비트 권한 단어가 이어집니다. 로드 시 리더는 하나의 엔벨로프를 감싸 풀어 시드를 회복하고, 시드를 /Recipients 순서의 모든 엔벨로프와 함께 해시해(AES-256은 SHA-256, 구형 암호는 SHA-1) 파일 키를 다시 만듭니다. 리뷰어 B는 리뷰어 A가 무엇을 할 수 있는지 절대 배우지 못하지만, B의 리더는 여전히 A의 엔벨로프 바이트를 해시하므로 배열 전체가 바이트 단위로 살아남아야 합니다. 이 모델과 평범한 비밀번호 사이에서 아직 고르고 있다면 AES-256 비밀번호 암호화와 권한 플래그 가이드가 그 절충의 반대편을 다룹니다

HotPDF 공개키 암호화 다이어그램: EnablePubKeyEncryption이 20바이트 시드를 고정하고, 각 CMS EnvelopedData 엔벨로프는 /Filter /Adobe.PubSec에 /SubFilter /adbe.pkcs7.s5와 /CFM /AESV3로 그 20바이트와 32비트 권한 단어 하나를 암호화하며, 리더는 하나의 엔벨로프를 풀어 시드를 회복하고 /Recipients의 모든 항목을 배열 순서로 해시해 파일 키를 다시 만듭니다
암호화 딕셔너리의 /P 값은 자리표시자일 뿐입니다. 진짜 권한은 각 엔벨로프 안을 여행하므로, 다이제스트가 도는 배열을 하류의 무엇도 재정렬하거나 재인코딩해서는 안 됩니다

EnablePubKeyEncryption으로 RSA 수신자 쓰기

RSA 인증서라면 순서가 짧습니다. aes256으로 EnablePubKeyEncryption을 부르고, DER 인코딩된 인증서마다 AddPubKeyRecipientCertificate를 한 번씩 부릅니다. 모두 BeginDoc 전에입니다. 헬퍼는 RSAES-OAEP 엔벨로프를 프로세스 안에서 만듭니다. 마지막 두 매개변수가 THPDFRSAOAEPHash(rohSHA256, rohSHA384, rohSHA512)에서 OAEP 다이제스트와 MGF1 다이제스트를 고르며 둘 다 기본값은 rohSHA256이고, 엔벨로프 콘텐츠는 AES-256-CBC로 암호화됩니다

uses
  System.SysUtils, System.IOUtils, HPDFDoc, HPDFCrypt, HPDFRSA;

procedure WriteAuditPack(const OutFile: string);
var
  Pdf: THotPDF;
  Seed: AnsiString;
begin
  SetLength(Seed, 20);                      // AES-256이라도 정확히 20바이트
  AESGenerateRandomBytes(@Seed[1], Length(Seed));
  Pdf := THotPDF.Create(nil);
  try
    Pdf.AutoLaunch := False;
    Pdf.FileName := OutFile;
    Pdf.EnablePubKeyEncryption(Seed, aes256, True);   // 기본 키 타입은 aes128
    // 리뷰어 A는 인쇄 가능, 리뷰어 B는 읽기와 추출만 가능
    Pdf.AddPubKeyRecipientCertificate(TFile.ReadAllBytes('reviewer-a.cer'),
      [prPrint, prPrint12bit, prExtractContent], rohSHA256, rohSHA256);
    Pdf.AddPubKeyRecipientCertificate(TFile.ReadAllBytes('reviewer-b.cer'),
      [prExtractContent]);
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Arial', [], 11);
    Pdf.CurrentPage.TextOut(72, 720, 0, 'Q3 audit pack');
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

그 코드에서 하중을 지는 세부가 셋 있습니다. 시드는 AES-256을 포함해 모든 키 유형에서 20바이트이고, 다른 길이면 EnablePubKeyEncryption이 예외를 raise합니다. 키 유형 기본값은 aes128인데 두 인증서 헬퍼는 aes256만 고집하므로, 두 번째 인자를 빠뜨리면 첫 AddPubKeyRecipientCertificate 호출이 "certificate envelopes require aes256"으로 떨어집니다. 레거시 암호(k40, k128, aes128)는 다른 도구가 만든 완전한 DER 엔벨로프를 받는 AddPubKeyRecipient로 계속 이용할 수 있습니다

세 번째 세부는 버전입니다. 공개키 핸들러 아래의 AES-256은 PDF 2.0 기능이고 HotPDF는 문서 버전을 스스로 2.0으로 올립니다. 낮은 버전에 StrictVersionLock이 설정되어 있으면 그 올림이 허용되지 않습니다. EnablePubKeyEncryption은 아무것도 켜지 않고 돌아오고, 오류는 한 줄 뒤에 수신자 헬퍼가 "call EnablePubKeyEncryption first"를 보고할 때 표면화됩니다. 그 메시지는 사람을 호출 순서 실마리로 보내지만, 대신 버전 잠금을 확인하십시오. 증분 업데이트 도중의 암호화 변경은 EInvalidOpException으로 그 자리에서 거부됩니다

P-256, P-384, P-521, X25519, X448에 ECDH 수신자 추가하기

타원곡선 인증서는 AddPubKeyAgreementRecipientWithSecret을 지나는데, 이것은 CMS key-agreement 수신자(RFC 5753의 KeyAgreeRecipientInfo, X25519와 X448 프로파일은 RFC 8418)를 쓰고 ECDH 공유 비밀을 프로세스 안에서 계산합니다. THPDFPubKeyAgreementScheme 값이 곡선을 이름 짓습니다. pkasECDHP256, pkasECDHP384, pkasECDHP521, pkasX25519, pkasX448입니다. 방식은 인증서의 키 알고리즘, 곡선, 키 길이와 일치해야 하며, 그렇지 않으면 호출이 "Certificate key does not match the requested agreement scheme"를 raise합니다

모든 엔벨로프는 새 무작위 32바이트 UKM과 stdDH KDF의 키 암호화 키(P-256과 X25519는 SHA-256, P-384는 SHA-384, P-521과 X448는 SHA-512), RFC 3394에 정의된 AES-256 키 랩을 받습니다. 곡선 산술은 플랫폼 암호 제공자 밑바닥 없는 평범한 Pascal이고, 순수 Pascal NIST 곡선 산술 문서가 그 층이 어떻게 짜였는지 걸어 보여줍니다. Montgomery 곡선에서는 임시 키 쌍 전체를 로컬에서 만들 수 있습니다:

uses
  System.SysUtils, System.IOUtils, HPDFDoc, HPDFCrypt, HPDFPubSec,
  HPDFKeyAgreement;

procedure AddLegalRecipient(Pdf: THotPDF);
var
  Scalar, OriginatorPublic: TBytes;
begin
  // 엔벨로프마다 새 에페머럴 스칼라, 클램핑은 ladder 안에서 일어남
  SetLength(Scalar, 32);
  AESGenerateRandomBytes(@Scalar[0], Length(Scalar));
  try
    OriginatorPublic := HPDFX25519PublicFromScalar(Scalar);
    Pdf.AddPubKeyAgreementRecipientWithSecret(
      TFile.ReadAllBytes('legal-x25519.cer'),
      [prPrint, prExtractContent], pkasX25519,
      OriginatorPublic, Scalar,
      []);   // OwnPublicPoint: NIST 곡선에서만 의미 있음
  finally
    HPDFSecureClearBytes(Scalar);
  end;
end;

NIST 곡선은 호출자에게 더 요구합니다. HotPDF는 X25519와 X448에만(HPDFX25519PublicFromScalar, HPDFX448PublicFromScalar) originator 공개키를 유도해 주므로, P-256, P-384, P-521에서는 임시 쌍이 여러분 도구의 몫입니다. 필드 크기와 정확히 같은 빅엔디안 스칼라(32, 48, 66바이트)와 짝이 맞는 비압축 0x04||X||Y 점(65, 97, 133바이트)을 OriginatorPublicKey로 넘기고, 같은 점을 NIST 방식을 위해 예약된 매개변수인 OwnPublicPoint에도 넘기십시오. 틀린 길이는 즉시 raise합니다. 인증서에서 가져온 수신자 점은 곱셈이 시작되기 전에 곡선 방정식과 대조됩니다

어떤 라이브러리도 검사할 수 없는 것은 여러분의 originator 공개키가 여러분의 스칼라에 속하는지입니다. 어긋난 절반들도 잘 구성된 엔벨로프를 만들어 냅니다. 기록자는 스칼라 곱하기 수신자 점에서 키를 유도하고, 수신자는 자기 개인 키 곱하기 잘못된 originator 점에서 유도하며, 수신자 쪽의 RFC 3394 무결성 검사가 가장 먼저 알아차리는 시점에는 파일이 이미 출하된 뒤입니다. 같은 곡선의 테스트 키를 상대로 한 왕복 로드가 빌드에 속합니다. 성공한 EndDoc과 그럴듯한 파일 크기는 여기서 아무것도 증명하지 않습니다

이 짝 짓기 단계가 실무에서 곡선 선택이 중요한 이유입니다. 인증서 발급을 통제한다면 X25519가 Delphi에서 제대로 다루기 쉬운 곡선입니다. 32바이트 무작위 스칼라 하나, 헬퍼 호출 하나, 손으로 동기화해야 할 점이 없습니다. NIST 곡선이 제 자리를 얻는 경우는 수신자 인증서들이 P-256이나 P-384만 발급하는 PKI에서 올 때이고, 그때는 왕복 로드가 선택이 아니게 됩니다

HotPDF ECDH 합의 다이어그램: AddPubKeyAgreementRecipientWithSecret이 순수 Pascal 곡선 코드로 공유 비밀을 유도하고, 새 32바이트 UKM을 stdDH KDF에 섞습니다. P-256과 X25519는 SHA-256, P-384는 SHA-384, P-521과 X448는 SHA-512이며, 이어서 RFC 3394 AES-256 키 랩이 콘텐츠 키를 감싸 KeyAgreeRecipientInfo 엔벨로프를 만듭니다
pkasECDHP256부터 pkasX448까지의 방식 값은 인증서 키와 일치해야 하며, 어긋난 스칼라와 공개 절반도 잘 구성된 엔벨로프를 만들어 내지만 어떤 수신자도 열지 못합니다

/Recipients의 순서는 왜 중요한가?

/Recipients의 순서가 중요한 이유는 파일 키가 시드에 배열 순서대로 모든 엔벨로프를 이은 것의 다이제스트이기 때문입니다. 기록자와 읽는 자가 동일한 바이트를 동일한 순서로 해시해야 합니다. HotPDF는 엔벨로프를 여러분이 더한 순서로 유지하고 그대로 쓰므로 순서는 자유입니다. 위험한 것은 그 뒤에 배열에 벌어지는 일입니다. 후처리 도구, 서명 파이프라인, 자작 엔벨로프 빌더가 그것이 깨지는 자리이고, 실패들은 한 모양을 공유합니다. 양쪽이 살짝 다른 바이트를 해시하는 것입니다:

  • Delphi의 타입 없는 TList는 날 포인터를 저장합니다. List.Add(Pointer(Bytes))로 엔벨로프를 모으면 참조 카운트가 지역 변수에 남아, 그 변수의 다음 SetLength가 버퍼를 해제하거나 재사용하고 모든 슬롯이 마지막 엔벨로프를 별칭으로 가리킬 수 있습니다. TList<TBytes>나 평범한 TArray<TBytes>는 참조를 카운트합니다
  • 제자리에서 동작하는 DER 파서는 파싱하는 버퍼를 수정합니다. 어떤 감싸 풀기 시도가 손대기 전에, 각 엔벨로프의 순수한 복사본을 해시하십시오
  • 유니코드 TStringList는 바이너리 DER의 자리가 아닙니다. $80 이상의 바이트가 들어오는 길에서 코드 페이지로 재인코딩됩니다. 엔벨로프는 TBytes로 유지하거나 먼저 헥스 인코딩하십시오
  • 암호문은 헥스 문자열에 속합니다. literal string 안에서는 CR, LF, CRLF가 모두 하나의 LF로 읽히므로(ISO 32000-1 §7.3.4.2) 우연히 $0D $0A를 담은 엔벨로프는 바이트 하나를 잃습니다. HotPDF는 모든 /Recipients 항목을 헥스 문자열로 쓰고 string encryption 밖에 둡니다. 리더는 어떤 키를 쥐기도 전에 엔벨로프가 필요하기 때문입니다
  • DER BIT STRING의 첫 콘텐츠 바이트는 미사용 비트 수를 세며 바이트 정렬 키에서는 $00이어야 합니다. 이 바이트를 빠뜨리고 0x04||X||Y 점을 콘텐츠에 곧장 넣은 손작성 DER은 키 전체를 한 바이트 밀어내고, 엄격한 파서는 originator 키를 그 자리에서 거부합니다
  • 동작해야 할 키가 여전히 복호화하지 못하면 이 순서로 계층별로 비교하십시오. 파일 키, 암호문 앞머리의 IV, 객체별 키, 평문입니다. 고장은 첫 불일치 계층의 바로 뒤에 앉아 있습니다

개인 키로 인증서 암호화 PDF 열기

개인 키는 LoadFromFile이 돌기 전에 등록되어야 합니다. HotPDF는 구조 패스 동안 파일 키를 회복하기 때문입니다. HPDFParsePFX로 파싱한 RSA 신원은 PubSecKeyMaterial로 가고, 추가 RSA 키는 AddPubSecKeyMaterial로 가는데 이것은 모듈러스와 개인 지수 없는 자료를 거부합니다. 두 키 슬롯 모두 RSA 모듈러스와 개인 지수로 매칭되므로, PFX 기반을 포함한 EC 키는 세 번째 길을 택합니다. AddPubSecAgreementKeyMaterial(CurveOID, PrivateScalar, OwnPublicPoint)에 HPDFOIDX25519, HPDFOIDX448, HPDFOIDECP256, HPDFOIDECP384, HPDFOIDECP521 상수 중 하나를 넘기는 것입니다. NIST 곡선은 거기에 수신자 자신의 비압축 공개 점을 요구하고, Montgomery 곡선은 그것을 무시합니다

PFX 파일로 도착한 EC 신원도 같은 호출에 들어맞습니다. HPDFParsePFX는 그것을 KeyAlgorithm = pfxkaECDSA(X25519와 X448 신원 포함)로 보고하고 CurveOID와, PrivateKey의 개인 스칼라, PublicKey의 인증서 점을 채워 세 매개변수에 곧장 대응시킵니다

uses
  System.SysUtils, System.IOUtils, HPDFDoc, HPDFPFX, HPDFKeyAgreement;

procedure OpenAuditPack(const LegalScalar: TBytes);
var
  Reader: THotPDF;
  EcKey: THPDFPFXKeyMaterial;
begin
  Reader := THotPDF.Create(nil);
  try
    Reader.AutoLaunch := False;
    // RSA identity becomes the primary key
    Reader.PubSecKeyMaterial :=
      HPDFParsePFX(TFile.ReadAllBytes('reviewer-a.pfx'), 'pfx-password');
    // EC identity from a PFX: curve, scalar and point go to the agreement list
    EcKey := HPDFParsePFX(TFile.ReadAllBytes('reviewer-c-p256.pfx'), 'pfx-password');
    if EcKey.KeyAlgorithm = pfxkaECDSA then
      Reader.AddPubSecAgreementKeyMaterial(EcKey.CurveOID,
        EcKey.PrivateKey, EcKey.PublicKey);
    // Raw X25519 scalar held by the legal team; no point needed
    Reader.AddPubSecAgreementKeyMaterial(HPDFOIDX25519, LegalScalar, nil);
    Reader.LoadFromFile('audit-pack.pdf', '');
    Writeln('Pages: ', Reader.GetLoadedPageCount);
  finally
    Reader.Free;
  end;
end;

그대로 두면 HotPDF는 모든 엔벨로프를 모든 등록된 키에 대조해 봅니다. 기본 RSA 키 먼저, 그다음 각 추가 RSA 키, 그다음 EC 자료입니다. PubSecRecipientQuery는 그것을 단일 엔벨로프로 좁힙니다. 콜백은 엔벨로프 수를 받고 배열 안의 0 기반 인덱스를 돌려주어야 하며, 그 외의 값은 -1을 포함해 로드 도중 예외를 raise합니다. 모든 엔벨로프를 시도하려면 속성을 지정하지 않고 두십시오. Delphi 아래에서는 그 타입이 함수 참조이므로 익명 메서드가 동작하고, FPC 아래에서는 평범한 함수 포인터로 Context 인자가 PubSecRecipientQueryContext에서 옵니다

하나의 동작에는 자체 테스트가 어울립니다. 어떤 등록된 키도 엔벨로프를 풀지 못하면 키 회복은 파일 키 없이 끝나고 그 이유로는 예외를 raise하지 않습니다. 문서가 틀림없이 갖고 있는 콘텐츠, 곧 텍스트 문자열이나 메타데이터 값에 어서트하십시오. 평범하게 돌아온 LoadFromFile 호출은 파일 구조가 파싱되었다는 것만 보여줍니다

HotPDF 개인 키 로딩 다이어그램: PubSecKeyMaterial이 HPDFParsePFX에서 온 기본 RSA 키를 실고, AddPubSecKeyMaterial은 RSA 키만 더하며, AddPubSecAgreementKeyMaterial은 HPDFOIDX25519부터 HPDFOIDECP521까지의 곡선 OID 아래 날 ECDH 스칼라를 등록합니다. LoadFromFile에서 제공자는 기본 키, 각 추가 RSA 키, EC 자료 순으로 모든 엔벨로프를 시도합니다
어떤 키도 엔벨로프를 풀지 못하면 회복 단계는 예외 대신 파일 키 없이 돌아오므로, 실제 복호화된 콘텐츠를 검증하거나 PubSecRecipientQuery로 엔벨로프를 고정하십시오

대상 뷰어에서 무엇을 확인해야 하나?

HotPDF의 기록자와 읽는 자는 바이트 단위로 합의하고 엔벨로프는 위에 인용한 CMS 구조를 따릅니다. 서드파티 뷰어는 다른 질문입니다. RSA-OAEP 키 전송 지원은 물론 그 이상으로 X25519나 X448 수신자 지원은 리더마다, 같은 리더의 버전마다 다릅니다. 문서가 특정 뷰어에서 열려야 한다면 같은 키 유형의 테스트 인증서용 테스트 파일을 암호화해 방식을 확정하기 전에 거기서 열어 보십시오. 뷰어가 여러분 통제 밖이라면 뷰어의 지원 목록이 방식을 고르고, 그것은 앞서 말한 X25519 편의 논거를 능가합니다

엔벨로프 권한은 규격을 지키는 소프트웨어가 존중하는 정책으로 남습니다. 비밀번호 암호화 아래에서와 정확히 같습니다. 시드 품질은 여러분 몫입니다. AESGenerateRandomBytes가 그 일을 위해 존재하고, HotPDF는 파일 키가 유도된 뒤 자기 시드 복사본을 닦아냅니다. 문자열, 스트림, 첨부가 다른 crypt filter를 필요로 한다면 StmF, StrF, EFF crypt filter 정책 가이드가 지정 방법을 보여주는데, 공개키 핸들러는 DefaultCryptFilter나 Identity를 받습니다

RSA와 타원곡선 인증서의 수신자별 엔벨로프와 돌아오는 길의 키 로딩은 비밀번호 암호화와 디지털 서명 곁에서 Delphi와 C++Builder용 HotPDF PDF 컴포넌트의 일부입니다