Bài viết kỹ thuật

Verify chữ ký PDF trên macOS bằng SecTrust trong Delphi

PDFium Delphi Component verify chữ ký PDF trên macOS qua TPdfKeychainCmsVerifier, một CMS verification backend xây trên Apple CMSDecoder và SecTrust thay vì tự parse CMS. ConfigureKeychainCmsVerifier cài backend đó, còn một lời gọi CMSDecoderCopySignerStatus trả về signature verdict, SecTrust handle và certificate result code, đúng cặp column mà TPdfCmsVerifyResult vốn đã có trên Windows

Tình huống buộc phải làm việc này khá buồn tẻ và phổ biến. Một Lazarus build của document archive chạy trên Mac, mở signed contract, rồi mọi signature đều trả pcsUnsupported. File không có gì sai. Signature verification đơn giản là chưa có backend ngoài Windows, còn PAdES validator từ chối đoán khi không có backend. Version 3.111.0 của PDFiumPas mở seam bằng IPdfCmsVerifierConfigurePadesCmsVerifier; version 3.113.0 lấp seam đó trên macOS. Phần thú vị của port không nằm ở plumbing mà ở ba nơi Apple API không có shape giống Windows API

Vì sao một PDF signature bao phủ hai byte range?

Vì signature không thể bao phủ các byte chứa chính nó. ISO 32000-1 §12.8.1 đặt CMS SignedData blob trong string /Contents của signature dictionary và mô tả phần được ký bằng /ByteRange, một tập offset và length pair bao phủ mọi thứ ở hai phía của lỗ trống đó. Hai segment, một khoảng gap ở giữa, trên mọi platform

Platform bất đồng về cách đưa các segment đó vào crypto layer, và sự bất đồng này tốn memory. Trên Windows, CryptVerifyDetachedMessageSignature nhận array pointer và length, nên cả hai span đi vào đúng như nằm trong buffer mà không duplicate. Apple CMSDecoderSetDetachedContent nhận một CFData và không có dạng multi-segment, nên macOS backend nối hai range thành buffer liên tục trước khi decode. Đó là một bản copy thứ hai đầy đủ của signed byte. Với scanned archive 400 MB, đây là memory peak thật, tăng theo document thay vì theo signature, và không có API thay thế để dùng. Hãy sizing batch worker tương ứng thay vì chỉ phát hiện trên máy customer

Một call điền hai column của TPdfCmsVerifyResult

CMSDecoderCopySignerStatus hào phóng khác thường đối với một entry point của Security.framework: một call trả signer status, SecTrustRef cho chain nó dựng và OSStatus cho certificate evaluation. Chúng đi thẳng vào record mà PAdES validator đã consume, signer status thành SignatureStatus, certificate result thành TrustStatus, còn raw value được giữ trong SignatureErrorTrustError để support ticket có thể trích số thay vì adjective. Caller không bao giờ tự chạm IPdfCmsVerifierValidatePadesComplianceValidatePadesTrust route mọi verification qua backend đã cài, nên code đọc TPadesSignatureValidation giống từng byte trên cả hai platform, như walkthrough về inspect signature dictionary và PAdES level trong Delphi mô tả

uses
  FPdfCrypto, FPdfCryptoMac, FPdfPades;

procedure InstallMacVerifier;
begin
  // Symbol cho signing và verification resolve khác nhau, nên một bên
  // có thể tồn tại trong khi bên kia không có
  if not KeychainVerificationAvailable then
    raise Exception.CreateFmt('Security.framework symbols missing: %s',
      [KeychainMissingSymbols]);

  ConfigureKeychainCmsVerifier;

  // PadesCmsVerificationBackendName giờ trả về 'macOS Security.framework'
  if not PadesCmsVerificationAvailable then
    raise Exception.Create('No CMS verification backend is installed');
end;

Vì sao kCMSSignerInvalidCert báo signature hợp lệ?

Vì Apple gán value đó cho ý nghĩa hẹp hơn tên gọi gợi ý: signature tự verify thành công, chỉ certificate chain không establish được. Vì vậy TPdfKeychainCmsVerifier map kCMSSignerInvalidCert thành pcvsValid trong column SignatureStatus và để vấn đề certificate xuất hiện qua TrustStatus, nơi chain problem thuộc về. Gộp nó vào signature verdict sẽ khiến component nói với operator rằng document không bị tamper thực ra đã bị sửa, là false alarm tệ nhất mà signature validator có thể tạo

function MapSignerStatus(Status: LongWord): TPdfCmsVerifyStatus;
begin
  case Status of
  kCMSSignerValid:
    Result:= pcvsValid;
  // Signature đã verify, chỉ chain không verify, còn
  // trust status tự báo phần đó
  kCMSSignerInvalidCert:
    Result:= pcvsValid;
  kCMSSignerInvalidSignature, kCMSSignerUnsigned:
    Result:= pcvsInvalid;
  else
    Result:= pcvsIndeterminate;
  end;
end;

Đọc hai status như một ordered pair thì reporting logic tự hiện ra. SignatureStatus = pcvsValid cùng TrustStatus = pcvsInvalid mô tả document có byte nguyên vẹn nhưng issuer của nó không được Mac này trust: anchor thiếu trong Keychain, intermediate hết hạn hoặc chain không thể complete offline. Đó là câu hỏi policy của operator chứ không phải câu hỏi integrity của document, và distinction này chính là lý do đứng sau phần lớn case trong ghi chú về vì sao validator reject PAdES signature dù crypto vẫn đúng

macOS thực sự check revocation ở đâu?

Trong trust evaluation, đó là lý do TPdfCmsVerifyResult.RevocationStatus đi sau TrustStatus thay vì mang verdict riêng. SecPolicyCreateRevocation tạo policy, policy đó join SecPolicyCreateBasicX509 trong array truyền vào CMSDecoderCopySignerStatus, còn OCSP hoặc CRL diễn ra tại nơi chain được dựng. Không có câu trả lời riêng trả về nên báo một câu như vậy sẽ là bịa. Bản thân array có một ownership rule nhỏ đáng gọi tên: CFArrayCreate retain cả hai policy, nên hai local reference được release ngay sau đó; case một policy thì bỏ qua array và truyền policy trực tiếp, dạng mà API cũng nhận

Offline operation là flag tường minh chứ không phải tai nạn connectivity. Khi TPdfCmsVerifyOptions.OnlineRetrieval là False, backend thêm kSecRevocationNetworkAccessDisabled, giới hạn evaluation vào response đã cache trên machine, còn checkpoint callback vẫn fire pcvstCryptographicSignature, pcvstChainBuildpcvstRevocationCheck theo cùng thứ tự Windows backend báo cáo. Application code set tất cả qua higher-level options record

var
  Options: TPadesTrustValidationOptions;
  Report: TPadesValidationResult;
  Stream: TFileStream;
begin
  Options:= TPadesTrustValidationOptions.Default;
  Options.CheckRevocation:= True;
  Options.NetworkPolicy:= ptnpOffline;   // chỉ dùng response đã cache
  Options.CheckTimeStamps:= True;

  Stream:= TFileStream.Create('contract.pdf', fmOpenRead or fmShareDenyWrite);
  try
    Report:= ValidatePadesTrust(Stream, Options);
  finally
    Stream.Free;
  end;

  if Report.SignatureCount= 0 then
    Log('No signature dictionary in this document')
  else if Report.Signatures[0].CmsSignatureStatus <> pcsValid then
    Log('Document integrity failed')
  else if Report.Signatures[0].CertificateTrustStatus <> pcsValid then
    Log('Bytes intact, chain not trusted on this Mac');
end;

Get và copy: bản release fail ở một nơi khác

SecTrustGetCertificateAtIndex có get semantics và reference nó trả về tuyệt đối không được release, trong khi CMSDecoderCopySignerCertSecCertificateCopyData, nằm cách đó vài dòng trong cùng routine, có copy semantics và bắt buộc phải release. Core Foundation encode toàn bộ quy tắc vào một verb trong tên function còn type system không enforce gì. Release borrowed reference không hỏng gì tại call site: trust object chỉ trở nên unsound, còn crash xuất hiện sau đó ở nơi không có liên hệ dễ thấy với certificate chain

ChainCount:= _SecTrustGetCertificateCount(Trust);
SetLength(Result.ChainCertificates, ChainCount);
for I:= 0 to ChainCount- 1 do
begin
  // Get semantics: reference này borrowed và không release ở đây
  Cert:= _SecTrustGetCertificateAtIndex(Trust, I);
  if Cert= nil then
    Continue;
  // Copy semantics: object này owned và phải trả lại
  CertData:= _SecCertificateCopyData(Cert);
  if CertData= nil then
    Continue;
  try
    Result.ChainCertificates[I]:= CFDataToBytes(CertData);
  finally
    _CFRelease(CertData);
  end;
end;

Verifier bảo đảm gì khi không backend nào trả lời?

Rằng câu trả lời là unsupported, không bao giờ là pass im lặng. Khi ConfigurePadesCmsVerifier chưa cài gì và platform default cũng không giúp được, TPdfCmsVerifyResult trả về với mọi column là unavailable, còn PAdES validator map nó thành pcsUnsupported, nên build không có crypto backend báo trung thực thay vì tuyên bố gì đó về signature. macOS binding cũng conservative theo hướng đó: Security.framework và CoreFoundation được tiếp cận qua dlopendlsym, nên framework vắng hoặc symbol name binding viết sai sẽ xuất hiện dưới dạng KeychainVerificationAvailable trả False cùng KeychainMissingSymbols nêu thủ phạm, không phải link failure và không phải verdict sai. Đây là cùng fail-closed posture component dùng khi tìm native library, được mô tả trong bài về load native PDFium library trên mọi target

Signature verification là phần của PDF stack nơi sai một cách im lặng tệ hơn unavailable một cách rõ ràng, còn macOS đưa cho bạn API đủ rộng để dễ rơi vào cả hai. Hãy nối các byte range và chấp nhận bản copy, giữ signature verdict và chain verdict ở hai column riêng, tôn trọng verb get và copy, rồi để backend thiếu tự nói nó thiếu. Nếu bạn đưa Delphi hoặc Free Pascal document workflow lên Mac và cần PAdES signing cùng validation ở cả hai phía, PDFium Delphi Component ship Keychain backend cạnh backend Windows sau một interface duy nhất