PDFium VCL은 macOS Keychain에 있는 개인 키로 PAdES 문서에 서명합니다. 모든 Security와 CoreFoundation 심볼을 런타임에 dlopen과 dlsym으로 해석하는 백엔드를 통해서입니다. 링크 타임 바인딩이 전혀 없으므로 오타 심볼 이름은 링커 에러나 크래시가 아니라 KeychainAvailable이 False를 돌려주고 KeychainMissingSymbols가 범인을 지목하는 형태로 드러납니다
그 선택은 불편한 제약에 의해 강제됐고, 그 처리 방식은 일반화됩니다. 유닛은 macOS SDK가 없는 머신에서 작성됐으므로 모든 프레임워크 심볼 이름과 모든 상수는 문서에서 왔고 헤더와 대조할 수 없었습니다. 그 상황에 대한 잘못된 반응은 코드를 신중하게 쓰고 소망하는 것입니다. 옳은 반응은 불가피한 실수가 최대한 위치를 찾기 쉬운 형태로 자신을 알리도록 배치하는 것입니다
타깃 플랫폼에서조차 동적 바인딩이 옳은 선택인 이유
프로그램을 멈추는 실패 부류를 자신을 보고하는 실패 부류로 바꾸기 때문입니다. 잘못된 정적 링크 프레임워크 참조는 타깃에서 링크 타임에 실패하고 다른 어디서도 링크되지 않습니다. 잘못된 동적 바인딩은 사용 불가 백엔드와 해석되지 않은 이름 목록을 만들고, Mac에서의 첫 실행은 질문을 왜 사용 불가인가에서 오타 하나를 지목하는 한 줄로 바꿉니다
한 번이 아니라 매일 보수를 지불하는 두 번째 이점도 있습니다. 유닛은 프레임워크를 링크하지 않으므로 모든 플랫폼에서 컴파일되고, 평범한 Windows 빌드가 그 문법, 타입, uses 절을 계속 검사합니다. 팀에 아무도 갖고 있지 않은 플랫폼에서만 컴파일되는 유닛은 컴파일러가 전혀 들여다보지 않는 유닛이며, 공유 타입의 모든 리팩터링마다 조용히 부식합니다
uses
FPdfCrypto, FPdfCryptoMac;
var
Options: TPadesSignerOptions;
begin
if not KeychainAvailable then
raise Exception.Create('Keychain backend unavailable, unresolved: ' +
KeychainMissingSymbols);
ConfigureKeychainSignerProvider; // PAdES 서명자 백엔드로 설치
ConfigureKeychainCmsVerifier; // 그리고 검증 백엔드로도
Writeln('signer backend : ', PadesCryptoBackendName);
Writeln('verify backend : ', PadesCmsVerificationBackendName);
Options := TPadesSignerOptions.Default;
Options.CertificateThumbprint := 'B1 3F 9C ...'; // SHA-1, 대소문자 무관
Options.PaddingScheme := psRsaPss;
end;
두 종류의 익스포트 심볼, 두 가지 읽는 방법
이것은 바인딩 전체에서 가장 헷갈리는 디테일 하나이며, 거꾸로 이해하면 깨끗하게 컴파일되고 런타임에 실패합니다. CoreFoundation과 Security는 같은 dlsym 호출을 통해 범주적으로 다른 두 가지를 익스포트하고, 코드는 어느 것이 무엇인지 알아야 합니다
키체인 아이템 클래스 키와 CoreFoundation 불리언 싱글턴 같은 명명된 상수는 내용물이 여러분이 원하는 CFStringRef나 CFBooleanRef인 익스포트 변수입니다. dlsym은 그 변수의 주소를 돌려주므로 값을 얻으려면 한 번 역참조해야 합니다. 딕셔너리 키·값 콜백 같은 콜백 테이블 구조는 익스포트 구조이고, dlsym은 구조의 주소를 돌려주는데 그것이 바로 딕셔너리 생성 함수가 기대하는 포인터입니다. 그것을 역참조하면 구조의 첫 머신 워드를 포인터인 것처럼 넘기게 됩니다
어느 실수도 컴파일 에러를 내지 않고, 명확한 런타임 에러도 내지 않습니다. 하류 어딘가에서 실패하는 쓰레기 포인터를 얻습니다. 구분을 틀리게 만드는 것을 불가능하게 하는 방법은 기억에 의존하는 것을 그만두는 것입니다. 헬퍼 함수 둘, 하나는 바인딩하고 역참조하고 하나는 바인딩만 하도록 해서, 호출 지점이 어떤 종류의 심볼을 요구하는지 선언하면 헬퍼가 나머지를 강제합니다
// 익스포트 변수: dlsym은 CFTypeRef를 담은 변수의 주소를 주므로
// 한 번 역참조합니다
FSecClassKey := BindConstant(SecurityLib, 'kSecClass');
// 익스포트 구조: dlsym은 구조 그 자체의 주소를 주며 그것이 API가
// 원하는 것입니다. 역참조하지 마세요
FKeyCallbacks := BindStruct(CoreFoundationLib,
'kCFTypeDictionaryKeyCallBacks');
RSA-PSS 서명에 별도의 폴백 둘이 필요한 이유는?
그 알고리즘은 서로 독립적인 두 가지 방식으로 빠질 수 있고 둘 중 하나만 버전 문제이기 때문입니다. PSS 다이제스트 서명 알고리즘 상수는 macOS 10.13에 등장했으므로 더 오래된 시스템에서는 그 심볼이 그저 거기 없고 바인딩은 nil을 받습니다. 그것이 버전 검사입니다. 별도로, 상수가 존재하는 시스템에서도 특정 키는 그것을 거부할 수 있고, 프레임워크는 그 질문을 그 키에 대해 SecKeyIsAlgorithmSupported로 답합니다. 하드웨어 기반 키나 제한적인 속성을 실은 키는 같은 머신의 소프트웨어 키가 PSS를 받아들이는 동안 거부할 수 있습니다
두 경로 모두 같은 폴백, 즉 PKCS#1 v1.5로의 전환으로 이끌어져야 합니다. 그리고 결정적인 부분은 폴백이 서명 호출만이 아니라 CMS 구조에 쓰이는 알고리즘 식별자도 바꿔야 한다는 것입니다. 실제로 v1.5 서명을 만들면서 PSS 알고리즘 식별자를 내보내면 모든 검증기가 완전히 거부하는 문서가 나오는데, PSS 미지원을 보고하는 것보다 엄격히 나쁩니다. 다운그레이드는 받아들일 수 있지만 선언한 것과 한 것 사이의 불일치는 받아들일 수 없으며, 이것은 macOS 기질이 아니라 서명 코드에 대한 일반 규칙입니다. 서명 수준의 함의는 PAdES B-B로 PDF 서명하기에 정리되어 있습니다
ECDSA 서명 인코딩, 그리고 주목할 만한 반전
타원곡선 경로는 macOS에서 변환이 전혀 필요 없는데, 이것은 PKCS#11 바인딩이 요구하는 것의 정반대입니다. ECDSA에 대한 Security 프레임워크 다이제스트 서명 알고리즘은 이미 X9.62 DER 형태의 서명을 돌려주며, 그것이 정확히 CMS가 원하는 것입니다. PKCS#11 토큰은 대신 날 것의 고정 폭 P1363 쌍을 돌려주고, 그것은 서명 구조에 들어가기 전에 재인코딩되어야 합니다
그래서 같은 인터페이스를 구현하는 두 백엔드가 같은 알고리즘에 대해 정반대의 취급을 필요로 하고, 어느 쪽도 틀리지 않습니다. 이것이 바로 추상화가 노출하는 것이 아니라 흡수해야 하는 종류의 차이입니다. PAdES 계층은 프로바이더에게 서명을 요청하고, 인코딩 관습은 프로바이더 안에 머뭅니다. 그것들이 위로 새면 모든 호출자가 백엔드별 조건부를 떠안게 됩니다. 같은 형태가 HSM을 상대로 한 원격 PAdES 서명 세션에서 기술된 원격 서명 이야기에도 나타납니다
// 프로바이더 인터페이스는 모든 플랫폼에서 같으므로 선택은 호출마다가
// 아니라 시작 시의 결정입니다
{$IFDEF DARWIN}
if KeychainAvailable then
ConfigureKeychainSignerProvider;
{$ENDIF}
{$IFDEF MSWINDOWS}
// Windows CNG 프로바이더는 플랫폼 유닛이 설치합니다
{$ENDIF}
if not PadesCryptoAvailable then
raise Exception.Create('no signing backend on this platform');
// 여기서부터 서명 코드는 플랫폼 중립입니다
Signer := ResolvePadesSigner(Options);
세 줄 차이로 나란히 앉는 참조 카운팅 규칙
Core Foundation 메모리 관리는 이름 규약을 따르는데, 여기서의 함정은 다른 규약을 가진 함수들이 같은 짧은 블록 안에서 나란히 나타난다는 것입니다. 트러스트 오브젝트에서 인증서를 가져오는(get) 함수는 해제해서는 안 되는 빌린 참조를 돌려줍니다. 서명자 인증서를 복사하거나 그 데이터를 복사하는 함수들은 해제해야 하는 소유 참조를 돌려줍니다. 연속된 세 호출, 두 개의 소유 규칙, 그리고 빌린 것을 해제하는 것은 그 줄에서 실패하지 않습니다. retain 카운트를 망가뜨리고 나중에 무관한 무언가를 끌어내립니다
완화책은 정리 코드를 쓰기 전에 매번, 예외 없이, 모든 프레임워크 함수 이름의 동사를 읽는 것입니다. API가 복사본을 돌려주는지 뷰를 돌려주는지 확인하는 것의 CoreFoundation판이며, 틀렸을 때의 비용은 에러가 아니라 간헐적 크래시입니다
이 백엔드가 주장하지 않는 것
집필 시점까지 macOS에서 단 한 번도 돌지 않았고, 그것을 담백하게 말하는 것이 암시된 보증보다 쓸모 있습니다. 입증 가능한 것은 더 좁고 여전히 가치 있습니다. 유닛은 일일 빌드의 일부로 Windows에서 컴파일되고, 모든 프레임워크 심볼은 런타임에 이름으로 바인딩되며 실패는 열거되고, 두 PSS 폴백을 포함한 알고리즘 선택 로직은 검토하고 추론할 수 있는 평범한 Pascal입니다. Mac에서의 첫 실행은 동작하거나 고칠 이름 목록을 낳거나 둘 중 하나입니다
손으로 CMS 구조를 조립하기보다 더 상위의 CMS 디코더를 쓰는 검증 쪽 짝은 macOS에서 SecTrust로 PDF 서명 검증하기에서 다루며, 같은 바인딩 인프라와 같은 진단 접근을 공유합니다
여기서 가져갈 수 있는 아이디어는 macOS가 아니라 리스크의 배치에 관한 것입니다. 검증할 수 없는 인터페이스를 향해 코드를 써야 한다면, 실수를 찾는 데 가장 값싼 구성을 고르세요. 해석되지 않은 이름의 명시적 목록을 갖춘 동적 바인딩은 검증 불가능한 스무 개의 전제를 한 줄의 진단으로 바꿉니다. 두 백엔드는 소스로 PDFium Delphi 컴포넌트에 실려 나오므로, 심볼 이름을 고쳐야 한다면 지원 티켓이 아니라 여러분 자신의 트리에서 한 줄 바꾸는 일입니다