Le composant PDFium pour Delphi vérifie les signatures PDF sous macOS via TPdfKeychainCmsVerifier, un backend de vérification CMS fondé sur les API Apple CMSDecoder et SecTrust plutôt que sur un parsing CMS manuel. ConfigureKeychainCmsVerifier l’installe, et un seul appel à CMSDecoderCopySignerStatus renvoie le verdict de signature, un handle SecTrust et un code de résultat de certificat, exactement la paire de colonnes que TPdfCmsVerifyResult porte déjà sous Windows
Le scénario qui a imposé ce travail est banal et fréquent. Un build Lazarus d’une archive documentaire s’exécute sur un Mac, ouvre un contrat signé et chaque signature revient avec pcsUnsupported. Le fichier n’a aucun problème. La vérification de signature n’avait simplement aucun backend hors Windows, et le validateur PAdES refusait de deviner en l’absence de celui-ci. La version 3.111.0 de PDFiumPas a ouvert le point d’extension avec IPdfCmsVerifier et ConfigurePadesCmsVerifier ; la version 3.113.0 l’a rempli sous macOS. La partie intéressante du portage n’est pas la plomberie, mais les trois endroits où l’API Apple n’a pas la même forme que celle de Windows
Pourquoi une signature PDF couvre-t-elle deux plages d’octets ?
Parce qu’une signature ne peut pas couvrir les octets qui la contiennent. L’ISO 32000-1 §12.8.1 place le blob CMS SignedData dans la chaîne /Contents du dictionnaire de signature et décrit l’étendue signée avec /ByteRange, ensemble de paires offset-longueur couvrant tout ce qui se trouve de part et d’autre du trou. Deux segments, un intervalle au milieu, sur toutes les plateformes
Les plateformes divergent sur la manière dont ces segments atteignent la couche cryptographique, et cette divergence coûte de la mémoire. Sous Windows, CryptVerifyDetachedMessageSignature accepte un tableau de pointeurs et de longueurs ; les deux étendues sont donc passées telles quelles dans le tampon et rien n’est dupliqué. L’API Apple CMSDecoderSetDetachedContent accepte un seul CFData et ne possède pas de forme multi-segment, si bien que le backend macOS concatène les deux plages dans un tampon contigu avant le décodage. Cela représente une seconde copie complète des octets signés. Sur une archive numérisée de 400 Mo, c’est un vrai pic mémoire ; il évolue avec le document plutôt qu’avec la signature et il n’existe pas d’API de rechange. Dimensionnez le worker de traitement en conséquence au lieu de le découvrir sur la machine d’un client
Un appel remplit deux colonnes de TPdfCmsVerifyResult
CMSDecoderCopySignerStatus est exceptionnellement généreux pour un point d’entrée de Security.framework : un appel renvoie le statut du signataire, un SecTrustRef pour la chaîne construite et un OSStatus pour l’évaluation du certificat. Ces valeurs arrivent directement dans l’enregistrement que le validateur PAdES consomme déjà : le statut du signataire devient SignatureStatus, le résultat du certificat devient TrustStatus et les valeurs brutes sont conservées dans SignatureError et TrustError, afin qu’un ticket de support puisse citer un nombre plutôt qu’un adjectif. Les appelants ne touchent jamais eux-mêmes à IPdfCmsVerifier : ValidatePadesCompliance et ValidatePadesTrust font passer chaque vérification par le backend installé, si bien que le code qui lit TPadesSignatureValidation est identique octet par octet sur les deux plateformes, comme l’explique le parcours sur l’inspection des dictionnaires de signatures PDF et des niveaux PAdES dans Delphi
uses
FPdfCrypto, FPdfCryptoMac, FPdfPades;
procedure InstallMacVerifier;
begin
// La signature et la vérification résolvent des symboles de framework différents ;
// l’un peut être présent alors que l’autre ne l’est pas
if not KeychainVerificationAvailable then
raise Exception.CreateFmt('Security.framework symbols missing: %s',
[KeychainMissingSymbols]);
ConfigureKeychainCmsVerifier;
// PadesCmsVerificationBackendName renvoie désormais 'macOS Security.framework'
if not PadesCmsVerificationAvailable then
raise Exception.Create('No CMS verification backend is installed');
end;
Pourquoi kCMSSignerInvalidCert signale-t-il une signature valide ?
Parce qu’Apple attribue à cette valeur un sens plus étroit que son nom ne le laisse croire : la signature elle-même a été vérifiée et seule la chaîne de certificats n’a pas pu être établie. TPdfKeychainCmsVerifier associe donc kCMSSignerInvalidCert à pcvsValid dans la colonne SignatureStatus et laisse le problème de certificat apparaître par TrustStatus, là où il doit se trouver. Le fondre dans le verdict de signature ferait dire au composant à l’opérateur qu’un document intact a été modifié, ce qui est la pire fausse alerte qu’un validateur de signature puisse produire
function MapSignerStatus(Status: LongWord): TPdfCmsVerifyStatus;
begin
case Status of
kCMSSignerValid:
Result:= pcvsValid;
// La signature est vérifiée et seule la chaîne ne l’est pas ; le statut
// de confiance le signale séparément
kCMSSignerInvalidCert:
Result:= pcvsValid;
kCMSSignerInvalidSignature, kCMSSignerUnsigned:
Result:= pcvsInvalid;
else
Result:= pcvsIndeterminate;
end;
end;
Lisez les deux statuts comme une paire ordonnée et la logique de rapport s’écrit toute seule. SignatureStatus = pcvsValid avec TrustStatus = pcvsInvalid décrit un document dont les octets sont intacts mais dont l’émetteur n’est pas approuvé par ce Mac : ancre absente du Keychain, intermédiaire expiré ou chaîne impossible à compléter hors ligne. C’est une question de politique opérateur, pas d’intégrité du document, et cette distinction est exactement celle qui sous-tend la plupart des cas de la note sur les raisons pour lesquelles les validateurs rejettent des signatures PAdES pourtant correctes cryptographiquement
Où macOS vérifie-t-il réellement la révocation ?
Dans l’évaluation de confiance, raison pour laquelle TPdfCmsVerifyResult.RevocationStatus suit TrustStatus plutôt que de porter son propre verdict. SecPolicyCreateRevocation produit une policy, cette policy rejoint SecPolicyCreateBasicX509 dans le tableau passé à CMSDecoderCopySignerStatus, et le travail OCSP ou CRL s’effectue au moment de construire la chaîne. Aucun résultat distinct ne revient, et en produire un reviendrait à l’inventer. Le tableau possède lui-même une petite règle de propriété qui mérite d’être nommée : CFArrayCreate retient les deux policies, si bien que les deux références locales sont libérées immédiatement après, tandis que le cas d’une seule policy évite le tableau et passe directement la policy, forme également acceptée par l’API
Le fonctionnement hors ligne est un flag explicite, pas un accident de connectivité. Lorsque TPdfCmsVerifyOptions.OnlineRetrieval vaut False, le backend ajoute kSecRevocationNetworkAccessDisabled, limitant l’évaluation aux réponses déjà mises en cache sur la machine, et le callback de checkpoint déclenche toujours pcvstCryptographicSignature, pcvstChainBuild et pcvstRevocationCheck dans le même ordre que celui signalé par le backend Windows. Le code applicatif configure tout cela via l’enregistrement d’options de niveau supérieur
var
Options: TPadesTrustValidationOptions;
Report: TPadesValidationResult;
Stream: TFileStream;
begin
Options:= TPadesTrustValidationOptions.Default;
Options.CheckRevocation:= True;
Options.NetworkPolicy:= ptnpOffline; // réponses mises en cache uniquement
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 contre copy : la libération qui échoue ailleurs
SecTrustGetCertificateAtIndex a une sémantique get et la référence qu’il renvoie ne doit jamais être libérée, tandis que CMSDecoderCopySignerCert et SecCertificateCopyData, situés quelques lignes plus loin dans la même routine, ont une sémantique copy et doivent l’être. Core Foundation encode toute la règle dans un verbe du nom de fonction, sans que le système de types ne fasse respecter quoi que ce soit. Libérez la référence empruntée et rien ne se passe au point d’appel : l’objet trust devient simplement incohérent et le crash arrive plus tard, à un endroit sans lien visible avec les chaînes de certificats
ChainCount:= _SecTrustGetCertificateCount(Trust);
SetLength(Result.ChainCertificates, ChainCount);
for I:= 0 to ChainCount- 1 do
begin
// Sémantique get : cette référence est empruntée et ne doit pas être libérée ici
Cert:= _SecTrustGetCertificateAtIndex(Trust, I);
if Cert= nil then
Continue;
// Sémantique copy : cette référence nous appartient et doit être rendue
CertData:= _SecCertificateCopyData(Cert);
if CertData= nil then
Continue;
try
Result.ChainCertificates[I]:= CFDataToBytes(CertData);
finally
_CFRelease(CertData);
end;
end;
Que garantit le vérificateur lorsqu’aucun backend ne répond ?
Que la réponse est unsupported, jamais une réussite silencieuse. Lorsque ConfigurePadesCmsVerifier n’a rien installé et que la plateforme par défaut ne peut pas aider, TPdfCmsVerifyResult revient avec toutes ses colonnes indisponibles et le validateur PAdES traduit cela en pcsUnsupported ; un build sans backend cryptographique signale donc honnêtement la situation au lieu de prétendre quoi que ce soit sur la signature. La liaison macOS est volontairement conservatrice dans le même sens : Security.framework et CoreFoundation sont atteints par dlopen et dlsym, de sorte qu’un framework absent ou un nom de symbole incorrect dans cette liaison apparaît sous la forme d’un retour False de KeychainVerificationAvailable, avec KeychainMissingSymbols qui nomme le fautif, pas comme un échec de linkage ni comme un mauvais verdict. C’est la même posture fail-closed que celle adoptée par le composant lorsqu’il cherche la bibliothèque native, décrite dans le texte sur le chargement de la bibliothèque native PDFium sur toute cible
La vérification des signatures est la partie d’une pile PDF où se tromper silencieusement est pire que d’être indisponible bruyamment, et macOS fournit une API assez généreuse pour rendre les deux issues faciles à atteindre. Concaténez les plages d’octets et acceptez la copie, gardez le verdict de signature et celui de la chaîne dans des colonnes séparées, respectez les verbes get et copy et laissez un backend absent le dire. Si vous déplacez un workflow documentaire Delphi ou Free Pascal sur Mac et avez besoin de signer et valider des PAdES des deux côtés, le PDFium Component for Delphi fournit le backend Keychain à côté de celui de Windows derrière une interface unique