PDFium VCL은 비 Windows 타깃에서 RFC 3161 타임스탬프 요청을 libcurl을 통해 보내며, 여덟 개 심볼로 동적 바인딩해 WinHTTP에 바인딩하는 Windows 백엔드와 같은 형태를 이룹니다. 옵션 설정 두 개가 부하 아래에서 전송이 신뢰할 수 있는지를 결정하고, 유닛 전체는 자기 타깃 플랫폼으로 컴파일할 수 없는 머신에서 검증되었습니다
타임스탬프는 서명이 인증서 만료를 넘어 살아남는 무엇이 되게 하는 것이고, 서명 연산 안에 앉아 있는 네트워크 연산입니다. 그 조합은 전송 선택을 보통과는 다른 무게의 문제로 만듭니다. 워커 스레드에서 돌고, 여러분이 통제하지 않는 서버와 대화하고, 거기서 걸리면 페이지 로드가 아니라 서명 파이프라인이 멈춥니다
FPC HTTP 클라이언트 대신 왜 libcurl인가?
대안은 TLS 스택을 저장소로 끌어들인 뒤 그 버전 탐지를 유지보수하게 만들기 때문입니다. Free Pascal에서 눈에 보이는 경로는 OpenSSL 소켓 계층을 얹은 fphttpclient인데, 디테일에서 실패합니다. FPC 3.2.2 OpenSSL 바인딩은 대부분의 현재 배포판에서 OpenSSL 3.x를 믿을 수 없게 탐지하고, macOS는 그 위에 LibreSSL 차이를 얹습니다. 작은 HTTP 호출로 시작한 것이 남의 TLS ABI의 지속적인 유지보수가 됩니다
libcurl은 자기 TLS 백엔드를 스스로 해석하고 플랫폼 신뢰 저장소에 대해 체인을 검증하므로 Pascal 쪽에는 그런 것이 필요 없습니다. 바인딩 계층은 여덟 개 심볼입니다. 그 개수가 논거입니다. 여러분의 코드와 움직이는 의존성 사이의 표면이 작을수록 배포판 업그레이드가 여러분을 깨뜨릴 자리가 적고, 이것은 소수의 WinHTTP 진입점을 똑같은 방식으로 바인딩하는 기존 Windows 백엔드와도 맞습니다
uses
FPdfTsaFpc;
var
ReqDer, RespDer: TBytes;
begin
if not TsaHttpAvailable then
raise Exception.Create('no HTTP transport for timestamping');
Writeln('TSA transport: ', TsaHttpBackendName);
ReqDer := BuildTimeStampQuery(DocumentDigest);
if PostTimeStampQuery('https://tsa.example.org/tsr', ReqDer, RespDer) then
AttachTimeStampToken(RespDer)
else
raise Exception.Create('timestamp request failed');
end;
Pascal에서 C 가변 인자 함수 선언하기
curl_easy_setopt와 curl_easy_getinfo는 C 쪽에서 가변 인자이고 Object Pascal은 그것을 표현할 방법이 없습니다. 동작하는 접근은 고정 프로토타입을 여러 개 선언하는 것입니다. 인자 클래스당 하나씩, 모두 같은 익스포트 심볼을 가리킵니다. long을 받는 변형, 포인터를 받는 변형 등으로, 실제로 넘기는 것에 따라 호출 지점에서 고릅니다
이것이 안전한 이유는 이해하고 쓸 가치가 있지 복사할 가치가 있는 것은 아닙니다. 그 인자 타입들은 모두 놓여 있는 플랫폼 호출 규약에서 정수 레지스터로 전달되는데, 그것이 바로 C 구현의 va_arg가 읽어 내는 자리입니다. 따라서 이 트릭은 정수와 포인터와 핸들에 성립하고, 서로 다른 레지스터로 이동하는 부동소수점 인자에는 성립하지 않습니다. 패턴이 일반화될 것이라는 가정으로 double을 받는 변형을 추가하지 마세요
// 하나의 익스포트 심볼, 여러 고정 프로토타입. 모든 변형은 정수 레지스터로
// 인자를 전달하며 그것이 C 쪽이 읽는 자리입니다. 부동소수점 변형은
// 동작하지 않으므로 추가하면 안 됩니다
type
TCurlSetOptLong = function(Handle: Pointer; Option: Integer;
Value: NativeInt): Integer; cdecl;
TCurlSetOptPtr = function(Handle: Pointer; Option: Integer;
Value: Pointer): Integer; cdecl;
var
curl_easy_setopt_long: TCurlSetOptLong;
curl_easy_setopt_ptr: TCurlSetOptPtr;
요청이 완료되는지를 결정하는 두 설정
첫 번째는 명시적인 빈 Expect: 헤더입니다. libcurl은 대략 1킬로바이트를 넘는 요청 본문에 HTTP 100-continue 핸드셰이크를 켜고, 인증서 요청을 실은 타임스탬프 쿼리는 보통 그 임계를 넘습니다. 어떤 TSA 서버는 컨티뉴에이션에 절대 답하지 않아서, 클라이언트는 서버가 즉시 받아들였을 본문을 보내기 전에 풀 타임아웃을 기다립니다. 빈 Expect: 헤더를 보내면 핸드셰이크가 억제되고 요청은 왕복 한 번에 통과합니다
두 번째는 설정해야 하는 CURLOPT_NOSIGNAL입니다. 이것이 없으면 libcurl은 이름 해석 타임아웃을 SIGALRM으로 구현하는데 그 메커니즘은 스레드 안전하지 않습니다. 서명은 워커 스레드에서 돌므로 기본 동작은 동시성 아래에서 나타나고 단일 스레드 테스트에서는 절대 나타나지 않는 잠재 크래시입니다. 플래그를 설정하면 시그널 기반 경로가 꺼지고 비용은 리졸버 타임아웃의 단위가 굵어지는 것뿐입니다
두 결함 모두 나중에 찾기 값싸지 않게 만드는 프로파일을 공유합니다. 잘 동작하는 서버를 상대로 한 단일 스레드 기능 테스트에는 둘 다 나타나지 않습니다. 둘 다 프로덕션에서, 특정 TSA 하나를 상대로, 부하 아래에서 나타납니다. 네트워킹 라이브러리를 바인딩할 때는 기본값들이 여러분의 프로세스에 관해 무엇을 가정하는지 읽어 보세요. 그것들이 맞다고 가정하기 전에요
컴파일러가 절대 보지 못할 코드를 어떻게 검증하는가?
통제된 사본을 통해 어쨌든 컴파일러가 보게 만들어서입니다. 이 개발 머신에는 Linux나 macOS 크로스 컴파일러가 없으므로 타임스탬프 유닛의 비 Windows 분기는 평범한 빌드 중 코드 생성기에 도달하지 않습니다. 컴파일되지 않는 코드는 조용히 썩어가는 코드입니다. 공유 타입의 이름 변경, 바뀐 파라미터 목록, 추가된 유닛 의존성, 그리고 몇 달씩 아무도 알아차리지 못합니다
기법은 기계적입니다. 유닛을 임시 디렉터리로 복사하고, 이름을 바꾸고, 모든 Windows 조건부, 즉 {$IFDEF MSWINDOWS} 형태와 {$IF DEFINED(MSWINDOWS) 형태 모두를 절대 정의되지 않는 심볼로 교체하세요. 그다음 사본을 컴파일합니다. 3,828줄 전부가 컴파일되면, 비 Windows 경로가 존재하는 유닛을 쓰고, 일치하는 시그니처로 백엔드 함수를 호출하고, 스코프 안의 타입을 참조한다는 것을 증명한 것입니다. 전송이 동작한다는 증명은 아니고 그것은 타깃 플랫폼 말고는 얻을 수 없습니다. 분기가 이미 깨져 있지 않다는 증명이며, 실제로 쌓이는 실패 양상은 바로 그것입니다
짝 습관은 libcurl 유닛 자체를 플랫폼 가드 없이 두는 것입니다. 그러면 그것을 참조하는 것이 거기 없음에도 평범한 Windows 빌드에 참여합니다. 일일 빌드는 그 문법과 타입을 공짜로 계속 지켜 봅니다. 여러분이 갖고 있지 않은 플랫폼에서만 컴파일되는 유닛은 컴파일러가 전혀 검사하지 않는 유닛이며, 같은 논리가 Delphi와 FPC 크로스 컴파일러 함정에서 기술된 크로스 컴파일러 작업 전반에 적용됩니다
돌아오는 것에 상한 두기
타임스탬프 응답은 작은 DER 구조이고 전송은 그것을 강제하지 않습니다. 침해당했거나 잘못 설정되었거나 그저 잘못된 URL을 가리키는 서버는 임의의 스트림을 돌려줄 수 있고, 연결이 닫힐 때까지 읽는 클라이언트는 그것을 기꺼이 쌓아 올립니다. 그래서 두 전송 모두 응답에 상한을 두는데, 그것이 한도의 올바른 자리입니다. 전송에서 거부하면 지나치게 큰 본문이 할당되는 것 자체를 막지만, 파서 수준 검사는 메모리가 이미 커밋된 뒤에야 작동합니다
같은 논리가 URL에 적용됩니다. 백엔드는 의미 있게 말할 수 있는 스킴만 받아들이므로, 설정 실수는 모호한 해석 대신 명확한 메시지와 함께 즉시 실패합니다. libcurl에게 프로토콜 지원이 허용하는 무슨 방식으로든 해석하도록 넘겨지는 대신에요
전송이 서명 이야기에서 앉는 자리
타임스탬프는 장기 검증 이야기의 첫 단계이지 그 전부가 아닙니다. 토큰은 서명에 붙어야 하고, 검증 자료는 문서 보안 저장소에 기록되어야 하며, 아카이브 타임스탬프는 현재 것이 약해지기 전에 갱신되어야 합니다. 그 전체 아크는 RFC 3161 타임스탬프와 DSS를 갖춘 장기 PDF 서명에서 다룹니다
전송은 더 넓은 이식성 입장의 한 조각이기도 합니다. 어떤 타깃에서든 네이티브 라이브러리 로드하기에서 기술된 네이티브 라이브러리 로더가 PDFium 바이너리 자체에 대해 같은 부류의 문제를 다룹니다. 두 경우 모두 패턴은 동일합니다. 소수의 심볼을 동적으로 바인딩하고, 무엇이 바인딩에 실패했는지 정확히 보고하고, 빠진 의존성이 애플리케이션 시작을 멈추는 링크 타임 실패로 변하지 않게 하세요
Windows와 비 Windows 타임스탬프 백엔드는 모두 PDFium Delphi 컴포넌트에 실려 나오며, 설정이 아니라 타깃으로 고려집니다. Linux의 Lazarus 애플리케이션과 Windows의 Delphi 애플리케이션은 서로 다른 배관을 통해 같은 타임스탬프된 서명을 만들어 냅니다