Teknik Makale

macOS'ta SecTrust ile PDF İmzalarını Doğrulama

PDFium Delphi Component, macOS'ta PDF imzalarını hand-parsed CMS yerine Apple CMSDecoder ve SecTrust üzerine kurulu bir CMS verification backend'i olan TPdfKeychainCmsVerifier üzerinden doğrular. ConfigureKeychainCmsVerifier onu kurar ve tek bir CMSDecoderCopySignerStatus çağrısı signature verdict'ini, bir SecTrust handle'ını ve certificate result code'u geri verir; bu, TPdfCmsVerifyResult'ın Windows'ta zaten taşıdığı iki sütunun tam karşılığıdır

Çalışmayı zorlayan senaryo sıkıcı ve yaygındır. Bir document archive'ın Lazarus build'i Mac üzerinde çalışır, imzalı bir contract açar ve her signature pcsUnsupported olarak döner. Dosyada sorun yoktur. Signature verification, Windows dışında backend olmadığı için yalnızca desteklenmiyordu ve PAdES validator backend yokken tahmin etmeyi reddediyordu. PDFiumPas'ın 3.111.0 sürümü IPdfCmsVerifier ve ConfigurePadesCmsVerifier ile seam'i açtı; 3.113.0 sürümü macOS'ta doldurdu. Bu portun ilginç tarafı plumbing değil, Apple API'nin Windows API ile aynı şekle sahip olmadığı üç yerdir

Bir PDF signature neden iki byte range'i kapsar?

Çünkü bir signature onu taşıyan baytları kapsayamaz. ISO 32000-1 §12.8.1, CMS SignedData blob'unu signature dictionary içindeki /Contents string'ine koyar ve imzalanmış alanı /ByteRange ile açıklar; bu, ortadaki boşluğun iki yanındaki her şeyi kapsayan offset ve length çiftleri kümesidir. Her platformda iki segment, ortada tek bir boşluk vardır

Platformlar bu segmentlerin crypto layer'a nasıl ulaştığı konusunda ayrışır ve fark memory maliyeti doğurur. Windows'ta CryptVerifyDetachedMessageSignature pointer ve length array'ini kabul eder; iki span buffer'da olduğu gibi gider ve hiçbir şey çoğaltılmaz. Apple CMSDecoderSetDetachedContent tek bir CFData kabul eder ve multi-segment biçimi yoktur; bu yüzden macOS backend decode etmeden önce iki range'i contiguous bir buffer'da birleştirir. Bu, imzalanmış baytların tam ikinci bir kopyasıdır. 400 MB'lık taranmış bir arşivde gerçek bir memory peak'tir, belgeyle birlikte signature'dan değil document'tan ölçeklenir ve başvurulacak alternatif API yoktur. Bunu müşteri makinesinde keşfetmek yerine batch worker'ı buna göre boyutlandırın

Tek çağrı TPdfCmsVerifyResult'ın iki sütununu doldurur

CMSDecoderCopySignerStatus, Security.framework entry point'i için alışılmadık derecede cömerttir: tek çağrı signer status'ü, kurduğu chain için bir SecTrustRef'i ve certificate evaluation için bir OSStatus'u döndürür. Bunlar doğrudan PAdES validator'ın zaten tükettiği record'a gider; signer status SignatureStatus, certificate result TrustStatus olur ve raw değerler SignatureError ile TrustError içinde korunur; böylece support ticket bir sıfat değil sayı aktarabilir. Çağıranlar IPdfCmsVerifier'a hiç dokunmaz: ValidatePadesCompliance ve ValidatePadesTrust her verification'ı hangi backend kuruluysa onun üzerinden yönlendirir, bu yüzden TPadesSignatureValidation okuyan kod her iki platformda byte byte aynıdır; bu, Delphi'de PDF signature dictionary'lerini ve PAdES seviyelerini inceleme walkthrough'unda da anlatılır

uses
  FPdfCrypto, FPdfCryptoMac, FPdfPades;

procedure InstallMacVerifier;
begin
  // Signing ve verification farklı framework symbol'lerini çözer; bu nedenle
  // biri mevcutken diğeri mevcut olmayabilir
  if not KeychainVerificationAvailable then
    raise Exception.CreateFmt('Security.framework symbols missing: %s',
      [KeychainMissingSymbols]);

  ConfigureKeychainCmsVerifier;

  // PadesCmsVerificationBackendName artık 'macOS Security.framework' döndürür
  if not PadesCmsVerificationAvailable then
    raise Exception.Create('No CMS verification backend is installed');
end;

kCMSSignerInvalidCert neden geçerli bir signature bildirir?

Çünkü Apple bu değere adının düşündürdüğünden daha dar bir anlam verir: signature'ın kendisi doğrulanmıştır, yalnızca certificate chain kurulamaz. TPdfKeychainCmsVerifier bu nedenle kCMSSignerInvalidCert'i SignatureStatus sütununda pcvsValid'e eşler ve certificate probleminin ait olduğu TrustStatus üzerinden görünmesini sağlar. Bunu signature verdict'ine katmak, component'in operatöre değiştirilmemiş bir belgenin değiştirildiğini söylemesine yol açardı; bu, bir signature validator'ın üretebileceği en kötü false alarmdır

function MapSignerStatus(Status: LongWord): TPdfCmsVerifyStatus;
begin
  case Status of
  kCMSSignerValid:
    Result:= pcvsValid;
  // Signature doğrulandı; yalnızca chain kurulamadı ve bunu trust
  // status kendi başına bildirir
  kCMSSignerInvalidCert:
    Result:= pcvsValid;
  kCMSSignerInvalidSignature, kCMSSignerUnsigned:
    Result:= pcvsInvalid;
  else
    Result:= pcvsIndeterminate;
  end;
end;

İki status'ü sıralı çift olarak okuyun; reporting logic kendini yazar. SignatureStatus = pcvsValid ile TrustStatus = pcvsInvalid birlikte, baytları sağlam ve issuer'ı bu özel Mac tarafından güvenilmeyen bir belgeyi anlatır: Keychain'de eksik bir anchor, süresi dolmuş bir intermediate veya offline tamamlanamayan bir chain olabilir. Bu operatör policy'si sorusudur, document integrity sorusu değil; ayrım, cryptographically sound PAdES signature'ların validator'lar tarafından neden reddedildiği notundaki vakaların çoğunun arkasındaki ayrımdır

macOS revocation'ı gerçekte nerede kontrol eder?

Trust evaluation içinde; bu nedenle TPdfCmsVerifyResult.RevocationStatus, kendi verdict'i olan ayrı bir alan olarak değil TrustStatus'ten sonra gelir. SecPolicyCreateRevocation bir policy üretir, bu policy SecPolicyCreateBasicX509 ile birlikte CMSDecoderCopySignerStatus'e geçirilen array'e katılır ve OCSP veya CRL işi chain'in kurulduğu yerde yapılır. Ayrı bir cevap dönmez; dönüyormuş gibi raporlamak onu uydurmak olur. Array'in kendisi küçük bir ownership kuralı taşır: CFArrayCreate her iki policy'yi retain eder, bu nedenle iki local reference hemen release edilir; tek-policy durumu ise array'i bütünüyle atlar ve API'nin kabul ettiği policy'yi doğrudan geçirir

Offline operation bağlantı kazası değil açık bir flag'dir. TPdfCmsVerifyOptions.OnlineRetrieval False olduğunda backend kSecRevocationNetworkAccessDisabled ekler ve evaluation'ı makinede zaten cache edilmiş response'larla sınırlar; checkpoint callback yine Windows backend'in raporladığı sırayla pcvstCryptographicSignature, pcvstChainBuild ve pcvstRevocationCheck durumlarında çalışır. Application code bütün bunları high-level options record üzerinden ayarlar

var
  Options: TPadesTrustValidationOptions;
  Report: TPadesValidationResult;
  Stream: TFileStream;
begin
  Options:= TPadesTrustValidationOptions.Default;
  Options.CheckRevocation:= True;
  Options.NetworkPolicy:= ptnpOffline;   // yalnızca cache edilmiş response'lar
  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 ve copy: başka yerde başarısız olan release

SecTrustGetCertificateAtIndex get semantiğine sahiptir ve döndürdüğü reference asla release edilmemelidir; aynı routine'de birkaç satır ötede bulunan CMSDecoderCopySignerCert ve SecCertificateCopyData ise copy semantiğine sahiptir ve release edilmelidir. Core Foundation bütün kuralı function name'deki tek bir verb'e kodlar ve type system bunların hiçbirini zorlamaz. Borrowed reference'ı release ederseniz call site'ta hiçbir şey bozulmaz; trust object yalnızca artık sağlam olmayan hale gelir ve crash, certificate chain'lerle görünür bağlantısı olmayan başka bir yerde daha sonra gelir

ChainCount:= _SecTrustGetCertificateCount(Trust);
SetLength(Result.ChainCertificates, ChainCount);
for I:= 0 to ChainCount- 1 do
begin
  // Get semantiği: bu reference borrowed'dur, burada release edilmez
  Cert:= _SecTrustGetCertificateAtIndex(Trust, I);
  if Cert= nil then
    Continue;
  // Copy semantiği: bu owned'dır ve geri verilmelidir
  CertData:= _SecCertificateCopyData(Cert);
  if CertData= nil then
    Continue;
  try
    Result.ChainCertificates[I]:= CFDataToBytes(CertData);
  finally
    _CFRelease(CertData);
  end;
end;

Hiçbir backend cevap vermediğinde verifier neyi garanti eder?

Cevabın quiet pass değil unsupported olması garanti edilir. ConfigurePadesCmsVerifier hiçbir şey kurmamışsa ve platform default'u yardımcı olamıyorsa TPdfCmsVerifyResult her sütunu unavailable olarak gelir ve PAdES validator bunu pcsUnsupported'e eşler; yani crypto backend'i olmayan bir build signature hakkında bir şey iddia etmek yerine dürüstçe bildirir. macOS binding de aynı yönde muhafazakârdır: Security.framework ve CoreFoundation'a dlopen ile dlsym üzerinden ulaşılır; bu nedenle eksik framework veya binding'in yanlış yazdığı bir symbol name, link failure veya yanlış verdict yerine KeychainVerificationAvailable'ın False dönmesi ve suçu KeychainMissingSymbols ile adlandırması olarak görünür. Bu, component'in native library ararken aldığı ve PDFium native library'yi herhangi bir target'ta yükleme yazısında anlatılan aynı fail-closed tavrıdır

Signature verification, bir PDF stack'te sessizce yanlış olmanın yüksek sesle kullanılamaz olmaktan daha kötü olduğu yerdir ve macOS her iki sonuca ulaşmayı kolaylaştıracak kadar cömert bir API verir. Byte range'leri birleştirin ve kopyayı kabul edin, signature verdict'i ile chain verdict'ini ayrı sütunlarda tutun, get ve copy verb'lerine uyun ve eksik backend'in bunu söylemesine izin verin. Delphi veya Free Pascal document workflow'unuzu Mac'e taşıyor ve iki tarafta da PAdES signing ile validation'a ihtiyaç duyuyorsanız PDFium Delphi Component, Keychain backend'ini Windows backend'inin yanında tek bir interface arkasında sunar