Une facture signée se vérifie octet pour octet, son signataire a été révoqué le mois dernier, et la CRL que vous avez récupérée ne liste jamais le numéro de série : elle parle pour une autre partition. La validation de chemin dans HotPDF est conçue autour de tels cas : règles de périmètre et d'ancre RFC 5280, horloge d'évaluation en UTC, et horodatages RFC 3161 dont le temps ne compte qu'une fois la chaîne TSA de confiance
L'horloge d'évaluation tourne en UTC
Chaque horodatage qu'un validateur de chemin compare est stocké en UTC. Le RFC 5280 §4.1.2.5 exige que les dates de validité des certificats soient encodées en UTCTime ou GeneralizedTime en heure moyenne de Greenwich, les thisUpdate et nextUpdate des CRL utilisent les mêmes encodages, et les réponses OCSP du RFC 6960 portent producedAt, thisUpdate et nextUpdate en GeneralizedTime UTC. Le TDateTime de Delphi n'a pas de fuseau et Now renvoie l'heure locale du mur, si bien qu'une vérification écrite if Now > NotAfter mélange deux horloges sans avertissement du compilateur. L'erreur se divise par géographie. À l'ouest de Greenwich, elle court en avance. Sur un serveur de New York à 09:00 locale, quand l'horloge mondiale indique 14:00Z, une réponse OCSP produite à 13:59Z se trouve cinq heures dans le futur de l'horloge naïve, loin d'une tolérance de cinq minutes comme le OCSPAllowedClockSkewMinutes par défaut de HotPDF, et se fait refuser comme pas encore valide. À Shanghai le signe se retourne : à 22:00 locale, c'est-à-dire 14:00Z, un certificat de signature dont le notAfter est 20:00Z paraît déjà expiré, huit heures en avance. Aucun des deux échecs n'apparaît sur un agent de build en UTC — le défaut sur la plupart des runners CI hébergés
HotPDF traite le THPDFCertificateValidationOptions.ValidationTime comme UTC. Laissé à son défaut de zéro, le temps d'évaluation devient l'heure UTC courante, sauf si un horodatage de confiance fournit l'heure de signature (plus bas), et l'instant réellement utilisé revient dans Info.CertificateValidation.EvaluationTime. VerifyLoadedSignatureWithTrust exécute la vérification de plage d'octets derrière svValid avant tout cela, puis construit le chemin. Quand vous épinglez vous-même un moment historique, disons pour un auditeur demandant si la signature d'une facture était bonne à la clôture du trimestre, la conversion est votre affaire. Le TTimeZone.Local.ToUniversalTime de System.DateUtils le fait. Appliquez-le là où l'horodatage entre dans votre système plutôt qu'à l'appel de validation : dans les zones à heure d'été, une heure locale se répète chaque automne et une autre n'existe jamais au printemps, si bien qu'une colonne de base de données d'horodatages locaux ne peut pas faire l'aller-retour de chaque instant
uses
System.SysUtils, System.DateUtils, System.IOUtils,
HPDFDoc, HPDFSignVerify;
var
Pdf: THotPDF;
Options: THPDFCertificateValidationOptions;
Info: THPDFSignatureInfo;
Status: THPDFCertificateValidationStatus;
ClosingLocal: TDateTime;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.LoadFromFile('invoices-2026-q1.pdf');
Options := THPDFCertificateValidationOptions.Default;
SetLength(Options.TrustAnchors, 1);
Options.TrustAnchors[0] := TFile.ReadAllBytes('issuing-root.cer');
Options.RevocationMode := rvmRequire;
// Clôture de trimestre telle que l'ERP l'a enregistrée, dans le fuseau local du serveur
ClosingLocal := EncodeDateTime(2026, 3, 31, 23, 59, 59, 0);
// Certificats, CRL et réponses OCSP portent tous des heures UTC
Options.ValidationTime := TTimeZone.Local.ToUniversalTime(ClosingLocal);
Status := Pdf.VerifyLoadedSignatureWithTrust(0, Options, Info);
Writeln('Evaluated at (UTC): ',
DateTimeToStr(Info.CertificateValidation.EvaluationTime));
case Status of
cvsTrusted:
Writeln('Trusted via ',
string(Info.CertificateValidation.TrustAnchorSubjectDN));
cvsRevoked:
Writeln('Revoked at (UTC): ',
DateTimeToStr(Info.CertificateValidation.RevokedAt));
else
Writeln('Not trusted: ', string(Info.CertificateValidation.Issue));
end;
finally
Pdf.Free;
end;
end;
Quels certificats une CRL couvre-t-elle vraiment ?
Une CRL ne parle que pour les certificats dans son périmètre, et le périmètre est déclaré dans l'extension Issuing Distribution Point du RFC 5280 §5.2.5. Une AC qui partitionne ses données de révocation publie plusieurs CRL, toutes signées avec la même clé et toutes fraîches, et l'absence d'un certificat dans l'une d'elles ne signifie rien tant que cette CRL ne prétend pas le couvrir. Les champs qui resserrent le périmètre sont :
distributionPoint: la CRL ne couvre que les certificats dont l'extension CRL Distribution Points nomme ce pointonlyContainsUserCertsetonlyContainsCACerts: certificats d'entité finale seulement, ou certificats d'AC seulementonlySomeReasons: révocations pour un sous-ensemble de codes de motif, comme keyCompromiseonlyContainsAttributeCerts: la CRL concerne les certificats d'attributs et ne dit rien des certificats à clé publiqueindirectCRL: les entrées peuvent appartenir à des émetteurs autres que le signataire de la CRL
La recherche naïve vérifie la signature de la CRL, contrôle thisUpdate et nextUpdate, cherche le numéro de série et déclare le certificat bon quand le numéro manque. Cette dernière étape ne tient qu'après le test de périmètre du RFC 5280 §6.3.3 (b)(2). La CRL de la partition 7 ne liste jamais un certificat de la partition 3. Une CRL restreinte aux certificats d'AC ne liste jamais un signataire d'entité finale. Toutes deux répondent "non trouvé" pour un certificat révoqué. HotPDF ne consulte une CRL de base pour un certificat que si la CRL n'est limitée ni aux certificats d'attributs ni à certains motifs, si sa restriction user ou CA correspond au certificat, et si tout point de distribution qu'elle nomme correspond à l'un des URI de la propre extension CRL Distribution Points du certificat, comparée sans tenir compte de la casse — un point donné comme nom relatif à l'émetteur de la CRL ne correspond à rien. Une CRL limitée à des motifs n'est pas consultée du tout, si bien que les révocations qu'elle liste ne comptent pas non plus. Le §6.3.3 permet bien d'assembler une couverture complète depuis des partitions de motifs, en suivant un masque de motifs jusqu'à ce que chaque motif soit couvert, mais pour les signatures de document une réponse OCSP ou une CRL à périmètre complet est une preuve plus simple : collectez une de celles-là. Sans preuve dans le périmètre, le statut de révocation est inconnu, et sous rvmRequire l'appel renvoie cvsRevocationUnknown
Les ancres de confiance qui ferment un chemin doivent être des AC
Le RFC 5280 §6.1.1 remet à l'algorithme de validation de chemin son ancre de confiance comme simple information, un nom d'émetteur et une clé publique, et l'algorithme ne demande jamais si le propre certificat de l'ancre peut émettre quoi que ce soit ; le test basicConstraints du §6.1.4 tourne sur les certificats intermédiaires du chemin. Cela va bien quand les ancres sont des AC racines d'un magasin trié sur le volet. Les flux documentaires cassent l'hypothèse, parce que les gens épinglent des certificats d'entité finale : un système d'approvisionnement recevant des contrats de trois fournisseurs connus fait souvent confiance directement au certificat de signature de chaque fournisseur. L'épinglage marche pour un chemin d'un certificat, où signataire et ancre sont le même certificat. Le danger est un chemin de deux certificats. La clé privée d'un fournisseur peut signer un certificat frais portant n'importe quel sujet, le nom de votre DAF par exemple, et un validateur qui traite chaque clé d'ancre comme clé émettrice construit un chemin propre depuis ce faux jusqu'à la feuille épinglée
HotPDF applique les exigences d'émetteur du RFC 5280 à l'ancre chaque fois que le chemin dépasse un certificat. L'ancre exige alors basicConstraints avec cA=TRUE (§4.2.1.9), et si elle porte une extension keyUsage, le bit keyCertSign doit être posé (§4.2.1.3). Une racine sans extension basicConstraints compte comme émettrice seulement si elle est auto-émise, avec sujet et émetteur égaux, ce qui garde fonctionnelles les vieilles racines de version 1 antérieures aux extensions. L'épinglage continue de marcher. Un certificat d'entité finale épinglé valide toujours ses propres signatures, tandis qu'un certificat qu'il a signé revient en cvsPathValidationFailed, avec une chaîne Issue déclarant que l'ancre qui a émis le chemin n'est pas une autorité de certification
Quels horodatages peuvent fixer le temps d'évaluation ?
Seuls ceux de confiance. Un jeton RFC 3161 embarqué dans le CMS prétend que la valeur de signature existait à genTime, et les validateurs jugent le certificat du signataire à ce moment — c'est ainsi qu'une signature survit à son certificat. Le jeton n'est aussi qu'une autre signature CMS. Le vérifier contre le certificat qu'il porte prouve la cohérence interne et rien sur qui le produit : quiconque détient une clé de signature expirée ou révoquée peut faire tourner son propre service d'horodatage, horodater une nouvelle signature avec le genTime de l'année dernière et la faire marcher en arrière dans la période de validité du certificat. Le RFC 3161 §2.3 exige que le certificat TSA porte l'extended key usage id-kp-timeStamping comme unique finalité, marquée critique. HotPDF adopte le Info.Timestamp.GenerationTime comme temps d'évaluation seulement après que le certificat du jeton a validé contre vos ancres, à l'heure UTC courante, pour la finalité d'horodatage. Un horodatage de document, /SubFilter ETSI.RFC3161, est un autre cas. Là, le jeton est la signature en cours de validation, et son genTime s'applique à son propre chemin
Comme le jeton est contrôlé avec vos propres options, la racine TSA doit se trouver dans la même liste TrustAnchors que la racine du signataire, et sous rvmRequire la chaîne TSA a besoin de preuves de révocation pour elle-même. Rater l'un des deux et l'évaluation retombe à l'heure courante. La durée de vie du certificat TSA est l'autre limite : le jeton est jugé à l'heure courante, si bien qu'une fois ce certificat expiré, l'évaluation à l'heure de signature s'en va avec, et les archives ajoutent un horodatage de document avant ce point, l'étape couverte dans les preuves LTV PAdES et le renouvellement d'horodatage B-LTA. Du code qui détient les octets CMS bruts peut poser la même question via HPDFCMSTimestampTimeIsTrusted(CMSDER, Options), et ValidateLoadedSignatureBatch l'applique à chaque signature d'un lot. La fonction ci-dessous rapporte sur quelle horloge un verdict a été pris
// Attendu : avec SignerRoot seul, une signature dont le certificat a expiré
// en 2025 est évaluée à l'heure UTC courante et ne revient pas
// cvsTrusted, bien que son jeton d'horodatage se vérifie ; avec TsaRoot ajouté,
// le même appel est évalué à genTime
function DescribeVerdictClock(Pdf: THotPDF; SigIndex: Integer;
const SignerRoot, TsaRoot: TBytes): string;
var
Options: THPDFCertificateValidationOptions;
Info: THPDFSignatureInfo;
Status: THPDFCertificateValidationStatus;
Clock: string;
begin
Options := THPDFCertificateValidationOptions.Default;
// La chaîne TSA est jugée contre la même liste d'ancres que le signataire
SetLength(Options.TrustAnchors, 2);
Options.TrustAnchors[0] := SignerRoot;
Options.TrustAnchors[1] := TsaRoot;
Options.RevocationMode := rvmBestEffort;
Options.ValidationTime := 0; // un horodatage de confiance peut fournir le temps
Status := Pdf.VerifyLoadedSignatureWithTrust(SigIndex, Options, Info);
if Info.Timestamp.Kind <> tskSignature then
Clock := 'no signature timestamp, current UTC time'
else if (Info.Timestamp.Status = svValid) and
(Info.CertificateValidation.EvaluationTime =
Info.Timestamp.GenerationTime) then
Clock := 'trusted timestamp genTime'
else
Clock := 'timestamp not trusted, current UTC time';
Result := Format('%s at %s (UTC), status %d', [Clock,
DateTimeToStr(Info.CertificateValidation.EvaluationTime), Ord(Status)]);
end;
Des entrées hostiles avant la première décision de confiance
Une partie du travail de validation tourne sur des données qu'un attaquant contrôle entièrement, avant que quoi que ce soit ait été cru. Les intermédiaires manquants se récupèrent via les URL caIssuers de l'Authority Information Access du RFC 5280 §4.2.2.1, et chaque intermédiaire récupéré peut annoncer à son tour l'URL de son propre émetteur, si bien que la file de travail grossit pendant que vous la parcourez. En Delphi, cela écarte une boucle for. La valeur finale d'un for est évaluée une fois, avant que la boucle ne démarre — les URL ajoutées en cours de marche ne sont jamais visitées, et une chaîne manquante de deux niveaux reste incomplète. Relisez Count à chaque passe avec une boucle while à la place. Dans HotPDF, MaxFetch sur HPDFFetchAIAIntermediates plafonne combien de certificats une marche télécharge, et MaxFetchedIntermediates fixe le même plafond quand FetchMissingIntermediates laisse la validation de chemin récupérer toute seule
L'indice Retry-After est une autre valeur qu'un inconnu choisit. Son serveur a été nommé par un certificat non vérifié, et il peut demander une heure de pause à chaque requête. Bridez-le. THPDFSignatureEvidenceRetryPolicy.MaxRetryAfterMS fixe ce plafond pour les récupérations de preuves et d'AIA de HotPDF, 30 000 ms par défaut. La routine ci-dessous applique les deux règles pour les équipes qui rampent l'AIA elles-mêmes, disons pour préchauffer le cache d'intermédiaires d'un étage de signature hors ligne
uses
System.SysUtils, System.Generics.Collections;
type
// Votre transport : télécharge une cible caIssuers et renvoie les
// URL caIssuers annoncées par le certificat reçu
TFetchIssuer = reference to function(const URL: string;
out NextURLs: TArray<string>; out RetryAfterMS: Cardinal): Boolean;
function WalkCAIssuers(const SeedURLs: TArray<string>;
const Fetch: TFetchIssuer; MaxFetch: Integer;
MaxRetryAfterMS: Cardinal): Integer;
var
Queue: TList<string>;
Seen: TDictionary<string, Boolean>;
NextURLs: TArray<string>;
URL: string;
I, Attempt: Integer;
RetryAfter: Cardinal;
Ok: Boolean;
procedure Enqueue(const AURL: string);
begin
if not Seen.ContainsKey(LowerCase(AURL)) then
begin
Seen.Add(LowerCase(AURL), True);
Queue.Add(AURL);
end;
end;
begin
Result := 0;
Queue := TList<string>.Create;
Seen := TDictionary<string, Boolean>.Create;
try
for URL in SeedURLs do
Enqueue(URL);
I := 0;
// Une boucle for figerait Queue.Count à l'entrée et n'atteindrait jamais les
// URL que les certificats récupérés ajoutent plus bas
while (I < Queue.Count) and (Result < MaxFetch) do
begin
Ok := False;
for Attempt := 1 to 3 do
begin
NextURLs := nil;
RetryAfter := 0;
Ok := Fetch(Queue[I], NextURLs, RetryAfter);
if Ok or (Attempt = 3) then
Break;
// Le délai est le souhait du serveur, et un certificat non vérifié
// a choisi le serveur
if RetryAfter > MaxRetryAfterMS then
RetryAfter := MaxRetryAfterMS;
Sleep(RetryAfter);
end;
Inc(I);
Inc(Result);
if Ok then
for URL in NextURLs do
Enqueue(URL);
end;
finally
Seen.Free;
Queue.Free;
end;
end;
La vérification de signature CMS elle-même tourne avant la validation de chemin, parce que construire un chemin pour une signature qui ne se vérifie pas est du travail gaspillé, et elle utilise la clé publique du certificat incrusté dans la signature. Cet ordre met une taille de clé choisie par l'attaquant aux commandes au moment où tourne l'arithmétique la plus coûteuse. Rien n'est encore cru. Le coût de vérification RSA croît avec la longueur du module et celle de l'exposant, si bien qu'un certificat avec un module énorme et un exposant tout aussi long peut occuper un thread en exponentiation modulaire sur un serveur qui vérifie des fichiers téléversés. HotPDF refuse un module RSA au-dessus de 16 384 bits, refuse un exposant plus long que le module, et au-dessus de 3 072 bits n'accepte que des exposants d'au plus 64 bits. Les vraies clés de signature se tiennent entre 2 048 et 4 096 bits avec e = 65537, un exposant de 17 bits, si bien que le plafond ne les touche jamais ; le plancher est une politique séparée et configurable, AlgorithmPolicy.MinimumRSAKeyBits, qui prend défaut à 2 048
La même signature peut être cvsTrusted au genTime de son horodatage et échouer à la date du jour. Journalisez Info.CertificateValidation.EvaluationTime et TrustAnchorSubjectDN à côté de chaque verdict que le composant PDF Delphi HotPDF renvoie, et un auditeur relisant la trace plus tard verra sur quelle horloge et quelle ancre la réponse repose