Décider qu'une signature PDF est qualifiée au sens eIDAS signifie répondre à une question qui n'a rien à voir avec la cryptographie : le certificat a-t-il été émis par un service de confiance qu'un État membre a listé comme qualifié, au moment où la signature a été faite. La réponse vit dans une liste de confiance, un document XML publié par territoire, et toute la valeur de ce document dépend de son authenticité. Le composant PDFium refuse donc d'y regarder avant que quelqu'un l'ait garantie. TPdfEuropeanTrustedList.ParseAuthenticated remet les octets bruts complets à un IPdfTrustedListAuthenticator fourni par l'appelant avant d'analyser un seul service, et il ne crée un instantané que si cet authentificateur passe explicitement
Cet ordonnancement est la conception. Tout le reste de cette fonctionnalité en découle, y compris les parties qui paraissent incommodes
Analyser ne suffit pas à faire confiance
Une liste de confiance qui s'analyse proprement vous dit que le XML est bien formé. Elle ne vous dit rien sur qui l'a écrite. Comme la liste est ce sur quoi votre décision entière de statut qualifié repose, en accepter une parce qu'elle s'est analysée rendrait la décision dénuée de sens : un attaquant qui peut substituer la liste peut déclarer sa propre autorité de certification qualifiée
Le même raisonnement s'applique à la mise en cache, et c'est le piège qui vaut d'être nommé. Le cache d'instantanés stocke le XML d'origine avec un condensé SHA-256, et il serait facile de traiter un condensé correspondant au chargement comme preuve que la liste est authentique. Ce n'en est pas une. Un condensé calculé par le même processus qui a stocké le fichier, sans aucune clé impliquée, ne vérifie que les octets n'ont pas changé depuis que vous les avez écrits ; si la liste était frauduleuse au moment de sa mise en cache, le condensé confirme que c'est la même liste frauduleuse. Charger un instantané en cache passe donc par le même authentificateur qu'analyser une liste fraîche. Intégrité et authenticité sont des propriétés différentes et une seule d'elles a besoin d'une clé
uses
FPdfTrustedList;
type
TListAuthenticator = class(TInterfacedObject, IPdfTrustedListAuthenticator)
public
function Authenticate(const XmlData: TBytes;
out AuthenticationDetails: string): Boolean;
end;
function TListAuthenticator.Authenticate(const XmlData: TBytes;
out AuthenticationDetails: string): Boolean;
begin
// Votre politique vit ici : vérifiez la signature enveloppée XMLDSIG
// contre le certificat de signature de liste que vous avez épinglé hors bande,
// et décrivez ce que vous avez vérifié pour la piste d'audit
Result := VerifyEnvelopedXmlSignature(XmlData, FPinnedListSigner);
if Result then
AuthenticationDetails := 'XMLDSIG verified against pinned LOTL signer';
end;
var
List: TPdfEuropeanTrustedList;
Cache: TFileStream;
begin
List := TPdfEuropeanTrustedList.ParseAuthenticated(RawXml,
TListAuthenticator.Create, TPdfTrustedListOptions.Default);
// L'instantané existe seulement parce que l'authentificateur a dit oui
Cache := TFileStream.Create('tl-de.snapshot', fmCreate);
try
List.SaveCache(Cache);
finally
Cache.Free;
end;
end;
Le validateur ne possède pas la politique réseau
Un validateur PAdES n'a pas à décider comment rejoindre la liste des listes de confiance, s'il faut passer par un proxy, à quelle fréquence réessayer, ou quoi faire quand un territoire est injoignable. Ce sont des décisions d'application et de déploiement, et dans les environnements réglementés elles sont fréquemment auditées. Les mises à jour arrivent donc par IPdfTrustedListSource, à qui l'on remet un URI et un plafond d'octets et qui renvoie des octets
Ce que le composant impose, ce sont les invariants qui font d'une mise à jour une mise à jour plutôt qu'une substitution. Update exige que le territoire soit inchangé, que le numéro de séquence croisse strictement, et que l'heure d'émission ne recule pas. Ces trois vérifications défont les attaques par rétrogradation les plus évidentes : rejouer une liste plus ancienne qui liste encore un service retiré depuis, ou échanger la liste d'un autre territoire dont vous n'avez jamais eu l'intention de faire confiance aux services
Limites de l'analyseur, et aucun DTD du tout
TPdfTrustedListOptions plafonne la taille du XML, le compte de jetons, la profondeur d'imbrication, le nombre de services, le nombre de certificats et la taille d'un certificat individuel, avec une fonction de classe Default fournissant des valeurs utilisables. Les listes de confiance sont des documents publiés de taille prévisible, donc les bornes sont bon marché à poser et aucune liste légitime n'a besoin de les dépasser
Séparément et inconditionnellement, l'analyseur rejette les déclarations DTD et d'entités. Cela ferme d'un seul refus à la fois le déni de service par expansion d'entités et la voie de divulgation par entités externes, et cela ne coûte rien car les listes de confiance n'utilisent pas d'entités. Tout analyseur XML atteignable depuis une entrée non fiable devrait être configuré ainsi ; la différence ici est que le refus n'est pas configurable, donc il ne peut pas être désactivé par un changement d'option bien intentionné
Le statut qualifié est consigné à côté de la confiance de chaîne, pas fondu dedans
Le côté évaluation est délibérément séparé. TPadesTrustValidationOptions.QualifiedTrustEvaluator prend un IPdfQualifiedTrustEvaluator, que l'instantané de liste de confiance implémente. Pendant la validation, l'évaluateur reçoit le certificat feuille, la chaîne et une heure de validation, fait correspondre les certificats de services par comparaison DER exacte contre le signataire et la chaîne, combine le statut du service, l'identifiant de type de service et les URIs de qualificatif à ce moment-là, et renvoie un enregistrement d'évaluation
Le résultat atterrit à deux endroits sur chaque signature : QualifiedTrustStatus comme statut grossier, et QualifiedTrust comme évaluation complète avec territoire, nom de fournisseur, nom de service, identifiant de type, statut et heure de début de statut. Ce qu'il ne fait pas est changer CertificateTrustStatus. La confiance de chaîne système et le statut qualifié répondent à des questions différentes, et un rapport qui les fusionne ne peut pas distinguer « fiable mais non qualifié » de « qualifié mais la chaîne ne valide pas », qui sont tous deux réels et tous deux demandent des traitements différents
var
Options: TPadesTrustValidationOptions;
Report: TPadesValidationResult;
I: Integer;
begin
Options := TPadesTrustValidationOptions.Default;
Options.CheckRevocation := True;
Options.QualifiedTrustEvaluator := List; // l'instantané authentifié
Options.QualifiedValidationTime := SigningTime; // pas Now
Report := Pdf.ValidatePadesTrust(Options);
for I := 0 to High(Report.Signatures) do
if Report.Signatures[I].QualifiedTrustStatus = pcsValid then
Writeln(Format('signature %d qualified by %s / %s (%s)',
[I, Report.Signatures[I].QualifiedTrust.Territory,
Report.Signatures[I].QualifiedTrust.ProviderName,
Report.Signatures[I].QualifiedTrust.ServiceName]))
else if Report.Signatures[I].QualifiedTrustStatus = pcsIndeterminate then
// Aucun service correspondant, ou l'instantané ne peut répondre pour ce moment
Writeln(Format('signature %d: qualified status undetermined', [I]));
end;
Pourquoi l'heure de validation n'est pas maintenant
Parce que la qualification est une propriété d'un moment. Un service de confiance peut se voir accorder le statut qualifié, le voir retiré plus tard, puis être réintégré encore plus tard, et chacune de ces transitions porte une heure de début dans la liste. Une signature faite pendant que le service était qualifié reste qualifiée ensuite ; une signature faite avant l'octroi ne devient pas qualifiée rétroactivement. Évaluer contre l'heure courante donne donc la mauvaise réponse dans les deux sens
La liste porte ce qui est nécessaire pour cela : chaque enregistrement de service a une heure de début de statut et un drapeau distinguant les entrées historiques des courantes, et l'évaluateur les combine contre l'heure que vous fournissez. En pratique, cette heure vient d'un horodatage de confiance sur la signature plutôt que de l'heure de signature revendiquée dans le CMS, ce qui est pourquoi le matériel de validation à long terme compte même pour une question qui ressemble à une consultation de politique ; le côté horodatage et DSS est couvert dans l'article sur les signatures à long terme
Ce que vous devez encore construire
Trois choses, et aucune n'appartient à une bibliothèque PDF. L'authentificateur, c'est-à-dire une vraie vérification XMLDSIG contre un certificat de signature de liste que vous avez obtenu par un canal auquel vous faites confiance. La politique de récupération, c'est-à-dire comment et à quelle fréquence vous rafraîchissez, et ce que fait votre application quand un rafraîchissement échoue. Et le périmètre territorial, c'est-à-dire quelles listes vous portez du tout, ce qui est une décision commerciale sur dans quels États membres vos contreparties signent
Ce que vous obtenez du composant est la partie facile à rater subtilement : l'ordonnancement authentifier avant d'analyser, une analyse XML bornée et sans entités, des invariants de mise à jour monotones, la correspondance de services par DER exact, l'évaluation de statut historique, et un résultat qui reste séparé de la confiance de chaîne ordinaire. Si votre problème immédiat est plus élémentaire, qu'un validateur rejette une signature que vous croyez bonne, les causes habituelles sont cataloguées dans pourquoi les validateurs rejettent les signatures PAdES, et la surface d'inspection des signatures est décrite dans l'inspection des signatures et des niveaux PAdES. Les capacités du composant sont listées sur la page produit du PDFium Delphi component