Article technique

Signer PAdES avec une identité du trousseau macOS en Delphi

PDFium VCL signe des documents PAdES avec une clé privée détenue dans le trousseau macOS via un backend qui résout chaque symbole Security et CoreFoundation au chargement avec dlopen et dlsym. Rien n'est lié au moment de l'édition de liens, ce qui signifie qu'un nom de symbole mal épelé fait surface sous la forme de KeychainAvailable renvoyant False et de KeychainMissingSymbols nommant le coupable, plutôt que sous la forme d'une erreur d'éditeur de liens ou d'un plantage

Ce choix a été imposé par une contrainte inconfortable, et la façon dont il a été traité se généralise. L'unité a été écrite sur une machine sans SDK macOS, donc chaque nom de symbole de framework et chaque constante venait de la documentation et rien ne pouvait être contrôlé contre un en-tête. La mauvaise réponse à cette situation est d'écrire le code soigneusement et d'espérer. La bonne est d'organiser les choses pour que les inévitables erreurs s'annoncent elles-mêmes sous la forme la plus localisable possible

Pourquoi la liaison dynamique est le bon choix même sur la plateforme cible

Parce qu'elle convertit une classe d'échec qui arrête le programme en une classe d'échec qui se rapporte lui-même. Une référence de framework liée statiquement qui est fausse échoue à l'édition de liens sur la cible et ne link nulle part ailleurs. Une liaison dynamique fausse produit un backend indisponible et une liste de noms non résolus, et la première exécution sur un Mac transforme la question de pourquoi c'est indisponible en une seule ligne nommant une coquille

Il y a un second bénéfice qui paie chaque jour plutôt qu'une fois. Comme l'unité ne link aucun framework, elle compile sur chaque plateforme, si bien que la construction Windows ordinaire continue de contrôler sa syntaxe, ses types et sa clause uses. Une unité qui ne compile que sur une plateforme que personne de l'équipe ne possède est une unité qu'aucun compilateur ne regarde, et elle se dégrade en silence à chaque refactorisation d'un type partagé

uses
  FPdfCrypto, FPdfCryptoMac;

var
  Options: TPadesSignerOptions;
begin
  if not KeychainAvailable then
    raise Exception.Create('Keychain backend unavailable, unresolved: ' +
      KeychainMissingSymbols);

  ConfigureKeychainSignerProvider;   // installer comme backend signataire PAdES
  ConfigureKeychainCmsVerifier;      // et comme backend de vérification

  Writeln('signer backend  : ', PadesCryptoBackendName);
  Writeln('verify backend  : ', PadesCmsVerificationBackendName);

  Options := TPadesSignerOptions.Default;
  Options.CertificateThumbprint := 'B1 3F 9C ...';   // SHA-1, toute casse
  Options.PaddingScheme := psRsaPss;
end;

Deux sortes de symboles exportés, deux façons de les lire

C'est le détail le plus déroutant de toute la liaison, et le prendre à l'envers compile proprement et échoue à l'exécution. CoreFoundation et Security exportent deux choses catégoriquement différentes par le même appel dlsym, et le code doit savoir laquelle est laquelle

Les constantes nommées comme les clés de classe d'éléments du trousseau et les singletons booléens CoreFoundation sont des variables exportées dont le contenu est le CFStringRef ou le CFBooleanRef voulu. dlsym renvoie l'adresse de cette variable, donc il faut déréférencer une fois pour obtenir la valeur. Les structures de tables de callbacks comme les callbacks de clé et de valeur de dictionnaire sont des structures exportées, et dlsym renvoie l'adresse de la structure, qui est précisément le pointeur que la fonction de création de dictionnaire attend. Déréférencez celle-ci et vous passez le premier mot machine de la structure comme si c'était un pointeur

Aucune des deux erreurs ne produit une erreur de compilation, et aucune ne produit une erreur d'exécution claire. Vous obtenez un pointeur parasite qui échoue quelque part en aval. La façon de rendre la distinction impossible à rater est d'arrêter de compter sur la mémoire : deux fonctions d'aide, une qui lie et déréférence et une qui lie sans déréférencer, si bien que le site d'appel déclare quel genre de symbole il demande et l'aide impose le reste

Schéma du backend trousseau macOS de PDFium VCL résolvant les symboles Security et CoreFoundation par dlsym : kSecClass est une variable exportée que BindConstant déréférence une fois pour obtenir la valeur CFStringRef, tandis que kCFTypeDictionaryKeyCallBacks est une structure exportée que BindStruct passe par adresse, et mélanger les deux règles produit des pointeurs parasites en aval
Un seul appel dlsym renvoie deux choses catégoriquement différentes : l'adresse d'une variable portant un CFTypeRef et l'adresse d'une structure de callbacks. Deux aides prennent la décision de déréférencer ou non au site de liaison au lieu de dans la mémoire
// Variable exportée : dlsym donne l'adresse d'une variable portant le
// CFTypeRef, donc déréférencer une fois
FSecClassKey := BindConstant(SecurityLib, 'kSecClass');

// Structure exportée : dlsym donne l'adresse DE la structure, qui est
// ce que l'API veut. Ne pas déréférencer
FKeyCallbacks := BindStruct(CoreFoundationLib,
  'kCFTypeDictionaryKeyCallBacks');

Pourquoi une signature RSA-PSS exige-t-elle deux replis distincts ?

Parce que l'algorithme peut être manquant de deux façons indépendantes, et qu'une seule d'elles est une question de version. La constante d'algorithme de signature de condensé PSS est apparue dans macOS 10.13, donc sur un système plus ancien le symbole simplement n'y est pas et la liaison obtient nil. C'est le contrôle de version. Séparément, sur un système où la constante existe, une clé particulière peut quand même la refuser, et le framework répond à cette question via SecKeyIsAlgorithmSupported pour cette clé. Une clé adossée au matériel ou une clé aux attributs restrictifs peut décliner PSS pendant qu'une clé logicielle sur la même machine l'accepte

Les deux chemins doivent mener au même repli : basculer vers PKCS#1 v1.5. Et le point critique est que le repli doit aussi changer l'identifiant d'algorithme écrit dans la structure CMS, pas seulement l'appel de signature. Émettre un identifiant d'algorithme PSS tout en produisant réellement une signature v1.5 donne un document que tout vérificateur rejette d'emblée, ce qui est strictement pire que de rapporter que PSS n'est pas pris en charge. Un déclassement est acceptable, un décalage entre ce que vous déclarez et ce que vous avez fait ne l'est pas, et c'est une règle générale du code de signature plutôt qu'une bizarrerie macOS. Les implications au niveau de la signature sont exposées dans la signature de PDF avec PAdES B-B

Chaîne de décision montrant pourquoi la signature RSA-PSS dans le backend trousseau de PDFium VCL exige deux replis indépendants : dlsym renvoie nil pour la constante de signature de condensé sur les versions de macOS antérieures à 10.13, SecKeyIsAlgorithmSupported peut décliner une clé adossée au matériel, et les deux portes se déversent dans le même déclassement PKCS#1 v1.5 dont l'identifiant d'algorithme CMS doit changer avec lui
PSS peut être indisponible deux fois, une fois par version de macOS et une fois par clé, et seule la porte de version est une question de système. Les deux portes se déversent dans le même déclassement v1.5, et l'identifiant CMS suit

L'encodage de signature ECDSA, et un renversement à noter

Le chemin à courbes elliptiques n'exige aucune conversion du tout sur macOS, et c'est l'opposé de ce qu'exige une liaison PKCS#11. L'algorithme de signature de condensé du framework Security pour ECDSA renvoie la signature déjà en forme X9.62 DER, qui est exactement ce que CMS veut. Un jeton PKCS#11 renvoie à la place la paire brute à largeur fixe P1363, qui doit être réencodée avant d'entrer dans une structure de signature

Ainsi, deux backends implémentant la même interface exigent un traitement opposé pour le même algorithme, et aucun des deux n'est faux. C'est précisément le genre de différence qu'une abstraction doit absorber plutôt qu'exposer : la couche PAdES demande à un fournisseur de signer, et les conventions d'encodage restent à l'intérieur du fournisseur. Si elles fuient vers le haut, chaque appelant finit par porter une conditionnelle par backend. La même forme apparaît dans l'histoire de signature à distance décrite dans les sessions de signature PAdES à distance contre un HSM

Comparaison de l'encodage de signature ECDSA à travers deux backends du signataire PAdES de PDFium VCL : le framework Security du trousseau macOS renvoie du X9.62 DER que CMS accepte sans aucune conversion, tandis qu'un jeton PKCS#11 renvoie la paire brute à largeur fixe P1363 qui doit être réencodée, si bien que ResolvePadesSigner garde les conventions d'encodage à l'intérieur du fournisseur
La même interface ECDSA exige un traitement opposé par backend : Security livre du DER fini tandis qu'un jeton PKCS#11 livre du P1363 brut, donc la conversion vit à l'intérieur du fournisseur et les appelants ne voient jamais de conditionnelle par backend
// L'interface fournisseur est la même sur chaque plateforme, donc la
// sélection est une décision de démarrage plutôt qu'un choix par appel
{$IFDEF DARWIN}
  if KeychainAvailable then
    ConfigureKeychainSignerProvider;
{$ENDIF}
{$IFDEF MSWINDOWS}
  // Le fournisseur CNG Windows est installé par l'unité plateforme
{$ENDIF}

if not PadesCryptoAvailable then
  raise Exception.Create('no signing backend on this platform');

// À partir d'ici le code de signature est neutre vis-à-vis de la plateforme
Signer := ResolvePadesSigner(Options);

Des règles de comptage de références distantes de trois lignes

La gestion mémoire de Core Foundation suit des conventions de nommage, et le piège ici est que des fonctions aux conventions différentes apparaissent côte à côte dans le même petit bloc. Une fonction qui obtient un certificat depuis un objet de confiance renvoie une référence empruntée qui ne doit pas être libérée. Les fonctions qui copient un certificat signataire ou copient ses données renvoient des références possédées qui doivent être libérées. Trois appels de suite, deux règles de possession, et libérer la référence empruntée n'échoue pas à cette ligne. Cela corrompt un compte de rétentions et abat quelque chose sans rapport plus tard

La parade est de lire le verbe dans chaque nom de fonction de framework avant d'écrire le nettoyage, chaque fois, sans exception. C'est l'équivalent CoreFoundation de vérifier si une API renvoie une copie ou une vue, et le coût de l'erreur est un crash intermittent plutôt qu'une erreur

Ce que ce backend ne prétend pas

Il n'a jamais tourné sur macOS au moment de l'écriture, et le dire franchement est plus utile qu'une assurance implicite. Ce qui est démontrablement vrai est plus étroit et reste précieux : l'unité compile sur Windows dans la construction quotidienne, chaque symbole de framework est lié par nom au chargement avec les échecs énumérés, et la logique de sélection d'algorithmes, y compris les deux replis PSS, est du Pascal ordinaire qui peut être relu et raisonné. La première exécution sur un Mac fonctionnera ou produira une liste de noms à corriger

Le pendant de vérification, qui utilise le décodeur CMS de plus haut niveau plutôt que d'assembler la structure CMS à la main, est couvert dans la vérification des signatures PDF sur macOS avec SecTrust, et il partage la même infrastructure de liaison et la même approche de diagnostic

L'idée transférable ici concerne le placement du risque plutôt que macOS. Quand vous devez écrire du code contre une interface que vous ne pouvez pas vérifier, choisissez la construction où les erreurs sont les moins chères à localiser. La liaison dynamique avec une liste explicite de noms non résolus transforme vingt hypothèses invérifiables en une ligne de diagnostic. Les deux backends sont livrés en source avec le composant Delphi PDFium, si bien que si un nom de symbole doit être corrigé, c'est un changement d'une ligne dans votre propre arbre plutôt qu'un ticket de support