Τεχνικό Άρθρο

Επαλήθευση PDF signatures στο macOS με SecTrust στο Delphi

Το PDFium Delphi Component επαληθεύει PDF signatures στο macOS μέσω του TPdfKeychainCmsVerifier, ενός CMS verification backend χτισμένου πάνω στα Apple CMSDecoder και SecTrust αντί για hand-parsed CMS. Το ConfigureKeychainCmsVerifier το εγκαθιστά, και ένα μόνο call στο CMSDecoderCopySignerStatus επιστρέφει το signature verdict, ένα SecTrust handle και certificate result code, ακριβώς το ζεύγος στηλών που ήδη μετέφερε το TPdfCmsVerifyResult στα Windows

Το σενάριο που επέβαλε τη δουλειά είναι βαρετό και συνηθισμένο. Lazarus build ενός document archive τρέχει σε Mac, ανοίγει signed contract και κάθε signature επιστρέφει pcsUnsupported. Τίποτε δεν είναι λάθος με το file. Απλώς η signature verification δεν είχε backend εκτός Windows, και ο PAdES validator αρνούνταν να μαντέψει χωρίς ένα. Η version 3.111.0 του PDFiumPas άνοιξε το seam με IPdfCmsVerifier και ConfigurePadesCmsVerifier· η version 3.113.0 το συμπλήρωσε στο macOS. Το ενδιαφέρον σε αυτό το port δεν είναι το plumbing αλλά τα τρία σημεία όπου το Apple API δεν έχει το ίδιο σχήμα με το Windows API

Γιατί μια PDF signature καλύπτει δύο byte ranges

Επειδή μια signature δεν μπορεί να καλύψει τα bytes που την περιέχουν. Το ISO 32000-1 §12.8.1 τοποθετεί το CMS SignedData blob στο /Contents string του signature dictionary και περιγράφει το signed extent με /ByteRange, ένα σύνολο από offset και length pairs που καλύπτουν όλα όσα βρίσκονται εκατέρωθεν εκείνης της τρύπας. Δύο segments, ένα gap στη μέση, σε κάθε platform

Οι platforms διαφωνούν για το πώς φτάνουν αυτά τα segments στο crypto layer, και η διαφωνία κοστίζει memory. Στα Windows το CryptVerifyDetachedMessageSignature δέχεται array από pointers και lengths, άρα και τα δύο spans περνούν όπως βρίσκονται στο buffer χωρίς duplication. Το Apple CMSDecoderSetDetachedContent δέχεται ένα CFData και δεν έχει multi-segment form, οπότε το macOS backend ενώνει τα δύο ranges σε contiguous buffer πριν από το decoding. Αυτό είναι πλήρες δεύτερο copy των signed bytes. Σε scanned archive 400 MB είναι πραγματικό memory peak, κλιμακώνεται με το document και όχι με τη signature και δεν υπάρχει εναλλακτικό API. Κάνε sizing στο batch worker ανάλογα αντί να το ανακαλύψεις σε machine customer

Ένα call γεμίζει δύο στήλες του TPdfCmsVerifyResult

Το CMSDecoderCopySignerStatus είναι ασυνήθιστα γενναιόδωρο για entry point του Security.framework: ένα call επιστρέφει signer status, ένα SecTrustRef για την chain που έχτισε και ένα OSStatus για την αξιολόγηση του certificate. Αυτά καταλήγουν απευθείας στο record που καταναλώνει ήδη ο PAdES validator, με το signer status να γίνεται SignatureStatus, το certificate result να γίνεται TrustStatus και τις raw values να διατηρούνται στα SignatureError και TrustError, ώστε ένα support ticket να μπορεί να παραθέσει αριθμό αντί για adjective. Οι callers δεν αγγίζουν ποτέ μόνοι τους το IPdfCmsVerifier — τα ValidatePadesCompliance και ValidatePadesTrust δρομολογούν κάθε verification μέσω όποιου backend έχει εγκατασταθεί, οπότε ο code που διαβάζει TPadesSignatureValidation είναι byte-for-byte ίδιος και στις δύο platforms, όπως περιγράφεται στον οδηγό για inspecting PDF signature dictionaries και PAdES levels στο Delphi

uses
  FPdfCrypto, FPdfCryptoMac, FPdfPades;

procedure InstallMacVerifier;
begin
  // Signing και verification επιλύουν διαφορετικά framework symbols, άρα το ένα
  // μπορεί να υπάρχει ενώ το άλλο όχι
  if not KeychainVerificationAvailable then
    raise Exception.CreateFmt('Security.framework symbols missing: %s',
      [KeychainMissingSymbols]);

  ConfigureKeychainCmsVerifier;

  // Το PadesCmsVerificationBackendName απαντά πλέον 'macOS Security.framework'
  if not PadesCmsVerificationAvailable then
    raise Exception.Create('No CMS verification backend is installed');
end;

Γιατί το kCMSSignerInvalidCert αναφέρει valid signature

Επειδή η Apple δίνει σε αυτή την τιμή στενότερη σημασία από εκείνη που υπονοεί το όνομά της: η signature επαληθεύτηκε και μόνο το certificate chain δεν μπόρεσε να established. Γι’ αυτό το TPdfKeychainCmsVerifier αντιστοιχίζει το kCMSSignerInvalidCert σε pcvsValid στη στήλη SignatureStatus και αφήνει το certificate problem να εμφανιστεί μέσω TrustStatus, όπου ανήκει ένα chain problem. Αν το συγχώνευες με το signature verdict, θα έκανες το component να λέει σε operator ότι ένα document χωρίς tampering τροποποιήθηκε, το χειρότερο false alarm που μπορεί να σηκώσει ένας signature validator

function MapSignerStatus(Status: LongWord): TPdfCmsVerifyStatus;
begin
  case Status of
  kCMSSignerValid:
    Result:= pcvsValid;
  // Η signature επαληθεύτηκε και μόνο η chain απέτυχε· το trust
  // status το αναφέρει ξεχωριστά
  kCMSSignerInvalidCert:
    Result:= pcvsValid;
  kCMSSignerInvalidSignature, kCMSSignerUnsigned:
    Result:= pcvsInvalid;
  else
    Result:= pcvsIndeterminate;
  end;
end;

Διάβασε τα δύο statuses ως ordered pair και η reporting logic γράφεται μόνη της. Το SignatureStatus = pcvsValid μαζί με TrustStatus = pcvsInvalid περιγράφει document του οποίου τα bytes είναι intact και του οποίου ο issuer δεν είναι trusted από το συγκεκριμένο Mac: λείπει anchor από το Keychain, έχει λήξει intermediate ή η chain δεν μπορεί να ολοκληρωθεί offline. Αυτό είναι operator policy question και όχι document integrity question, και η διάκριση είναι ακριβώς εκείνη που βρίσκεται πίσω από τις περισσότερες περιπτώσεις στη σημείωση για γιατί validators απορρίπτουν PAdES signatures που είναι cryptographically sound

Πού ελέγχει πράγματι το macOS τη revocation

Μέσα στο trust evaluation, γι’ αυτό το TPdfCmsVerifyResult.RevocationStatus ακολουθεί το TrustStatus αντί να έχει δικό του verdict. Το SecPolicyCreateRevocation παράγει policy, εκείνη η policy ενώνεται με τη SecPolicyCreateBasicX509 στο array που περνά στο CMSDecoderCopySignerStatus και η OCSP ή CRL δουλειά γίνεται εκεί όπου χτίζεται η chain. Δεν επιστρέφεται ξεχωριστή απάντηση, οπότε η αναφορά μίας θα σήμαινε ότι την επινοούμε. Το array μεταφέρει επίσης μικρό rule ownership που αξίζει να ονομαστεί: το CFArrayCreate κρατά και τις δύο policies, οπότε τα δύο local references γίνονται release αμέσως μετά, ενώ η περίπτωση single policy παρακάμπτει εντελώς το array και περνά την policy απευθείας, μορφή που δέχεται επίσης το API

Η offline λειτουργία είναι explicit flag και όχι ατύχημα συνδεσιμότητας. Όταν το TPdfCmsVerifyOptions.OnlineRetrieval είναι False, το backend προσθέτει kSecRevocationNetworkAccessDisabled, περιορίζοντας την αξιολόγηση σε responses που έχουν ήδη γίνει cache στη machine, και το checkpoint callback εξακολουθεί να ενεργοποιεί τα pcvstCryptographicSignature, pcvstChainBuild και pcvstRevocationCheck με την ίδια σειρά που αναφέρει το Windows backend. Ο application code τα ρυθμίζει όλα μέσω του higher-level options record

var
  Options: TPadesTrustValidationOptions;
  Report: TPadesValidationResult;
  Stream: TFileStream;
begin
  Options:= TPadesTrustValidationOptions.Default;
  Options.CheckRevocation:= True;
  Options.NetworkPolicy:= ptnpOffline;   // μόνο cached responses
  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 έναντι copy: το release που αποτυγχάνει κάπου αλλού

Το SecTrustGetCertificateAtIndex έχει get semantics και το reference που επιστρέφει δεν πρέπει ποτέ να γίνει release, ενώ τα CMSDecoderCopySignerCert και SecCertificateCopyData, λίγες γραμμές μακριά στην ίδια routine, έχουν copy semantics και πρέπει να ελευθερωθούν. Το Core Foundation κωδικοποιεί ολόκληρο τον κανόνα σε μία verb μέσα στο όνομα της function και το type system δεν επιβάλλει τίποτε από αυτά. Κάνε release το borrowed reference και τίποτε δεν πάει στραβά στο call site: το trust object απλώς γίνεται unsound, και το crash έρχεται αργότερα κάπου που δεν έχει ορατή σχέση με certificate chains

ChainCount:= _SecTrustGetCertificateCount(Trust);
SetLength(Result.ChainCertificates, ChainCount);
for I:= 0 to ChainCount- 1 do
begin
  // Get semantics: αυτό το reference είναι borrowed και δεν γίνεται release εδώ
  Cert:= _SecTrustGetCertificateAtIndex(Trust, I);
  if Cert= nil then
    Continue;
  // Copy semantics: αυτό ανήκει σε εμάς και πρέπει να επιστραφεί
  CertData:= _SecCertificateCopyData(Cert);
  if CertData= nil then
    Continue;
  try
    Result.ChainCertificates[I]:= CFDataToBytes(CertData);
  finally
    _CFRelease(CertData);
  end;
end;

Τι εγγυάται ο verifier όταν κανένα backend δεν απαντά

Ότι η απάντηση είναι unsupported και ποτέ quiet pass. Όπου το ConfigurePadesCmsVerifier δεν έχει εγκαταστήσει τίποτε και το platform default δεν μπορεί να βοηθήσει, το TPdfCmsVerifyResult επιστρέφει με κάθε column unavailable και ο PAdES validator το αντιστοιχίζει σε pcsUnsupported, ώστε build χωρίς crypto backend να αναφέρει ειλικρινά την κατάσταση αντί να ισχυριστεί οτιδήποτε για τη signature. Το macOS binding είναι σκόπιμα conservative στην ίδια κατεύθυνση: Security.framework και CoreFoundation προσεγγίζονται μέσω dlopen και dlsym, οπότε absent framework ή symbol name που το binding έγραψε λάθος εμφανίζεται ως KeychainVerificationAvailable που επιστρέφει False με το KeychainMissingSymbols να ονομάζει τον ένοχο, όχι ως link failure και όχι ως wrong verdict. Είναι η ίδια fail-closed στάση που κρατά το component όταν ψάχνει native library, όπως περιγράφεται στο loading PDFium native library σε οποιοδήποτε target

Η signature verification είναι το σημείο ενός PDF stack όπου το να είσαι αθόρυβα λάθος είναι χειρότερο από το να είσαι δυνατά unavailable, και το macOS σου δίνει API αρκετά γενναιόδωρο ώστε να κάνει και τα δύο εύκολα. Κάνε concatenate τα byte ranges και δέξου το copy, κράτα χωριστά το signature verdict και το chain verdict, σεβάσου τα get και copy verbs και άφησε ένα missing backend να το πει καθαρά. Αν μεταφέρεις Delphi ή Free Pascal document workflow στο Mac και χρειάζεσαι PAdES signing και validation και στις δύο πλευρές, το PDFium Delphi Component διανέμει το Keychain backend μαζί με το Windows backend πίσω από ένα ενιαίο interface