Article technique

Un backend d'horodatage libcurl pour PDFium VCL sur FPC

PDFium VCL envoie les requêtes d'horodatage RFC 3161 via libcurl sur les cibles non Windows, lié dynamiquement à huit symboles, en miroir de la forme du backend Windows qui se lie à WinHTTP. Deux réglages d'options décident si le transport est fiable sous charge, et l'unité entière a été validée sur une machine qui ne pouvait pas la compiler pour sa plateforme cible

L'horodatage est ce qui transforme une signature en quelque chose qui survit à l'expiration du certificat, et c'est une opération réseau assise à l'intérieur d'une opération de signature. Cette combinaison rend le choix de transport conséquent d'une façon inhabituelle : il tourne sur un thread de travail, il parle à un serveur que vous ne contrôlez pas, et un blocage là-bas immobilise un pipeline de signature plutôt qu'un chargement de page

Pourquoi libcurl plutôt que le client HTTP de FPC ?

Parce que l'alternative traîne une pile TLS dans le dépôt puis vous fait entretenir sa détection de version. La route évidente sur Free Pascal est fphttpclient avec la couche socket OpenSSL, et elle échoue sur les détails : les bindings OpenSSL de FPC 3.2.2 détectent OpenSSL 3.x de façon peu fiable sur la plupart des distributions actuelles, et macOS ajoute des différences LibreSSL par-dessus. Ce qui commence comme un petit appel HTTP devient la maintenance continue de l'ABI TLS de quelqu'un d'autre

libcurl résout son propre backend TLS et valide les chaînes contre le magasin de confiance de la plateforme, donc le côté Pascal n'a besoin d'aucun de cela. La couche de liaison est de huit symboles. Ce compte est l'argument : une surface plus petite entre votre code et une dépendance mouvante laisse moins d'endroits où une montée de version d'une distribution peut vous casser, et cela colle au backend Windows existant, qui lie de la même façon une poignée de points d'entrée WinHTTP

uses
  FPdfTsaFpc;

var
  ReqDer, RespDer: TBytes;
begin
  if not TsaHttpAvailable then
    raise Exception.Create('no HTTP transport for timestamping');

  Writeln('TSA transport: ', TsaHttpBackendName);

  ReqDer := BuildTimeStampQuery(DocumentDigest);
  if PostTimeStampQuery('https://tsa.example.org/tsr', ReqDer, RespDer) then
    AttachTimeStampToken(RespDer)
  else
    raise Exception.Create('timestamp request failed');
end;

Déclarer une fonction C variadique en Pascal

curl_easy_setopt et curl_easy_getinfo sont variadiques côté C, et Object Pascal n'a aucun moyen de l'exprimer. L'approche qui fonctionne est de déclarer plusieurs prototypes fixes, un par classe d'argument, tous pointant le même symbole exporté : une variante prenant un long, une variante prenant un pointeur, et ainsi de suite, choisie au site d'appel selon ce que vous passez réellement

C'est sûr pour une raison précise qu'il vaut mieux comprendre que copier. Chacun de ces types d'argument est passé dans un registre entier sous les conventions d'appel de plateforme en jeu, ce qui est exactement là où l'implémentation C lit avec va_arg. L'astuce tient donc pour les entiers, les pointeurs et les handles, et elle ne tient pas pour les arguments en virgule flottante, qui voyagent dans des registres différents. N'ajoutez pas de variante prenant un double en supposant que le schéma se généralise

// Un symbole exporté, plusieurs prototypes fixes. Chaque variante passe son argument dans
// un registre entier, ce qui est là où le côté C le lit. Une variante en virgule flottante
// ne marcherait pas et ne doit pas être ajoutée
type
  TCurlSetOptLong = function(Handle: Pointer; Option: Integer;
    Value: NativeInt): Integer; cdecl;
  TCurlSetOptPtr  = function(Handle: Pointer; Option: Integer;
    Value: Pointer): Integer; cdecl;

var
  curl_easy_setopt_long: TCurlSetOptLong;
  curl_easy_setopt_ptr:  TCurlSetOptPtr;

Deux réglages qui décident si la requête aboutit

Le premier est un en-tête Expect: vide explicite. libcurl active la poignée de main HTTP 100-continue pour les corps de requête au-delà d'environ un kilooctet, et une requête d'horodatage avec une demande de certificat dépasse en général ce seuil. Certains serveurs TSA ne répondent jamais à la continuation, si bien que le client attend un timeout complet avant d'envoyer un corps que le serveur aurait accepté immédiatement. Envoyer un en-tête Expect: vide supprime la poignée de main, et la requête passe en un seul aller-retour

Le second est CURLOPT_NOSIGNAL, qui doit être posé. Sans lui, libcurl implémente son timeout de résolution de noms avec SIGALRM, et ce mécanisme n'est pas thread-safe. La signature tourne sur un thread de travail, donc le comportement par défaut est un crash latent qui apparaît sous concurrence et jamais dans un test monothread. Poser le drapeau désactive le chemin à signal et ne coûte que la granularité du timeout du résolveur

Les deux défauts partagent un profil qui les rend chers à trouver plus tard. Aucun n'apparaît dans un test fonctionnel contre un serveur bien élevé sur un seul thread. Les deux apparaissent en production, contre un TSA particulier, sous charge. Quand vous liez une bibliothèque réseau, lisez ce que ses défauts supposent de votre processus avant de supposer qu'ils collent

Schéma du transport d'horodatage libcurl de PDFium VCL montrant curl_easy_setopt déclaré comme prototypes Pascal fixes long et pointeur qui passent les arguments dans des registres entiers, l'en-tête Expect vide qui supprime la poignée de main HTTP 100-continue, CURLOPT_NOSIGNAL qui retire le chemin SIGALRM sur les threads de travail, et le plafond de réponse au niveau transport
Deux réglages décident si la requête aboutit : un en-tête Expect vide évite les serveurs qui ne répondent jamais à la continuation, et NOSIGNAL garde les timeouts de résolution de noms hors du chemin à signal pendant que la signature tourne sur un thread de travail

Comment vérifier du code que votre compilateur ne verra jamais ?

En faisant voir le code au compilateur quand même, via une copie contrôlée. La machine de développement ici n'a ni compilateur croisé Linux ni macOS, donc les branches non Windows de l'unité d'horodatage n'atteignent jamais le générateur de code pendant une construction normale. Du code jamais compilé est du code qui pourrit en silence : un renommage dans un type partagé, une liste de paramètres changée, une dépendance d'unité ajoutée, et personne ne remarque pendant des mois

La technique est mécanique. Copiez l'unité dans un répertoire temporaire, renommez-la, et remplacez chaque conditionnelle Windows, tant la forme {$IFDEF MSWINDOWS} que la forme {$IF DEFINED(MSWINDOWS), par un symbole jamais défini. Puis compilez la copie. Quand les 3 828 lignes compilent, vous avez prouvé que le chemin non Windows utilise des unités qui existent, appelle des fonctions backend avec des signatures concordantes, et référence des types dans la portée. Ce n'est pas la preuve que le transport fonctionne, et rien de moins que la plateforme cible ne vous le donnera. C'est la preuve que la branche n'est pas déjà cassée, ce qui est le mode d'échec qui s'accumule réellement

L'habitude compagne est de laisser l'unité libcurl elle-même libre de gardes de plateforme, pour qu'elle participe à la construction Windows ordinaire même si rien là ne la référence. La construction quotidienne garde alors sa syntaxe et ses types sous garde gratuitement. Une unité qui ne compile que sur une plateforme que vous n'avez pas est une unité qu'aucun compilateur ne contrôle, et le même raisonnement s'applique à l'ensemble du travail entre compilateurs décrit dans les pièges des compilateurs croisés Delphi et FPC

Borner ce qui revient

Une réponse d'horodatage est une petite structure DER, et rien dans le transport ne l'impose. Un serveur compromis, mal configuré ou simplement pointé vers la mauvaise URL peut renvoyer un flux arbitraire, et un client qui lit jusqu'à la fermeture de la connexion l'accumulera allègrement. Les deux transports plafonnent donc la réponse, ce qui est le bon endroit pour la limite : refuser au transport empêche qu'un corps surdimensionné soit jamais alloué, tandis qu'un contrôle au niveau de l'analyseur ne se déclenche qu'après que la mémoire a été engagée

Le même raisonnement s'applique à l'URL. Le backend n'accepte que les schémas qu'il peut parler de façon sensée, si bien qu'une erreur de configuration échoue immédiatement avec un message clair au lieu d'être remise à libcurl pour interprétation à sa façon, selon ce que son support de protocoles permet

Où le transport se situe dans l'histoire de la signature

L'horodatage est la première étape de l'histoire de la validation à long terme plutôt que la totalité. Le jeton doit être attaché à la signature, le matériel de validation doit être consigné dans le magasin de sécurité du document, et les horodatages d'archive doivent être renouvelés avant que l'horodatage courant ne faiblisse. Tout cet arc est couvert dans les signatures PDF à long terme avec horodatages RFC 3161 et le DSS

Schéma PDFium VCL d'une requête d'horodatage RFC 3161 coulant depuis DocumentDigest à travers BuildTimeStampQuery et PostTimeStampQuery par libcurl vers un serveur TSA, la réponse DER plafonnée au transport, puis AttachTimeStampToken alimentant le DSS et le renouvellement des horodatages d'archive dans la validation à long terme
L'horodatage est la première étape de l'histoire de la validation à long terme : le jeton doit être attaché, le matériel de validation consigné dans le magasin de sécurité du document, et les horodatages d'archive renouvelés avant que l'horodatage courant ne faiblisse

Le transport est aussi une pièce d'une position de portabilité plus large : le chargeur de bibliothèque native décrit dans le chargement de la bibliothèque native sur toute cible traite la même classe de problème pour le binaire PDFium lui-même. Dans les deux cas le schéma est identique, lier dynamiquement un petit nombre de symboles, rapporter précisément ce qui a échoué à se lier, et ne jamais laisser une dépendance manquante devenir un échec d'édition de liens qui empêche l'application de démarrer

Les backends d'horodatage Windows et non Windows sont tous deux livrés avec le composant Delphi PDFium, choisis par cible plutôt que par configuration, si bien qu'une application Lazarus sur Linux et une application Delphi sur Windows produisent la même signature horodatée à travers des plomberies différentes