Article technique

Vérifier et re-signer des signatures Excel en Delphi

HotXLS vérifie et re-signe les signatures numériques Excel en Delphi sans Excel installé. Lunité lxSignatureOpcIntegrity vérifie les signatures de paquet ECMA-376 Part 2 directement depuis les octets du classeur : RSA-SHA256 sur le SignedInfo canonique, chaque digest SHA-256 de Reference, et les parties exactes que chaque signature sélectionne. La confiance de chaîne de certificats est une étape séparée et explicite, et éditer ou re-signer ne publie quune sortie qui se vérifie à nouveau

Le déclencheur habituel est un workflow documentaire qui a dépassé le bureau. Un contrôleur signe un classeur budgétaire dans Excel, un service Delphi doit ensuite confirmer la signature avant dimporter les nombres, et une troisième étape doit corriger une feuille et signer à nouveau sous un certificat de service. Automatiser Excel sur un serveur pour cela est fragile et non pris en charge, et traiter la signature comme un drapeau oui/non cache les deux questions qui comptent réellement : quels octets le signataire a-t-il engagés, et faites-vous confiance à la personne qui tient cette clé

Que couvre réellement une signature de paquet OPC ?

Une signature de paquet OPC ne couvre que les parties que son Manifest liste, pas le fichier XLSX entier. ECMA-376 Part 2, §13 (Digital Signatures), place une partie origine de signature dans le paquet, typiquement /_xmlsignatures/origin.sigs, avec des relations vers une ou plusieurs parties de signature telles que /_xmlsignatures/sig1.xml. Chaque partie de signature est une Signature W3C XML-DSig dont le SignedInfo référence un idPackageObject interne, et cet Object porte un Manifest de References vers des parties individuelles du paquet, chacune avec son propre digest

HotXLS prend en charge trois façons pour une Reference de lier une partie, et elles ne sont pas interchangeables. Une Reference brute hache les octets stockés de la partie. Une Reference XML canonique hache la forme Canonical XML (sans commentaires), si bien que lordre des attributs et lorthographe des déclarations despaces de noms ne comptent plus. Une Reference de relation applique dabord lOPC RelationshipTransform puis canonise, et cette transformée ne garde que les relations appariées par ses sélecteurs SourceId ou SourceType. Un sélecteur authentifie exactement les relations quil sélectionne : le reste de cette partie .rels peut changer librement pendant que la signature reste valide

Anatomie de la signature numérique OPC HotXLS pour Delphi : une partie origin.sigs mène à des parties de signature XML-DSig dont le Manifest lie chaque partie du paquet avec une référence brute, XML canonique ou RelationshipTransform
Trois modes de Reference ne sont pas interchangeables : le brut hache les octets stockés, le XML canonique ignore lorthographe des attributs, et les sélecteurs de relations laissent les entrées .rels non sélectionnées libres de changer

Ce dernier point est pourquoi HotXLS rapporte une couverture au lieu dun simple coche vert. TXLSSignatureOpcIntegrityResult.Coverage est un tableau denregistrements TXLSSignatureOpcCoverage, un par Manifest Reference authentifiée, chacun avec ReferenceUri, PartName, ContentType, Transforms, RelationshipSourceIds et RelationshipSourceTypes. Si votre import lit sheet3.xml et quaucun descripteur ne le nomme, la signature ne dit rien des nombres que vous allez charger

Vérifier des signatures depuis une photo ZIP immuable

HotXLS vérifie un paquet depuis une seule photo doctets immuable, jamais depuis le fichier sur disque ou un TWorkbook vivant. XlsVerifySignatureOpcPackage et ses frères multi-signatures prennent un tableau TBytes, ne lisent que ce tableau, et ne vont jamais chercher des cibles externes, des certificats, des fichiers locaux ou des ressources réseau. Le point est que les octets que vous avez vérifiés sont les octets que vous analysez ensuite : si le fichier est relu plus tard, un autre processus a pu le remplacer entre-temps

La canonisation est là où les vérificateurs maison se trompent dordinaire. SignedInfo et lObject du paquet doivent être canonisés dans le contexte despaces de noms de lenveloppe Signature complète. Un fragment isolé découpé du document hérite de déclarations despaces de noms différentes de ce que le vérificateur voit en place, si bien que ses octets C14N diffèrent, le digest diffère, et une signature parfaitement valide échoue. HotXLS emploie le contexte denveloppe complet côté vérification comme côté émission

Pour les paquets qui peuvent porter plus dune signature, XlsVerifyAllSignaturesOpcPackage vérifie chaque signature attachée et garde les issues individuelles. Le statut agrégé est xsoiIntegrityVerified seulement quand chaque signature admise se vérifie ; sinon cest le premier échec dans lordre dinventaire, et chaque rapport dit toujours quelle partie de signature a échoué

uses
  System.SysUtils, System.IOUtils, lxSignatureOpcIntegrity;

function AllSignaturesIntact(const FileName: string): Boolean;
var
  Bytes: TBytes;
  Reports: TXLSSignatureOpcVerifications;
  Status: TXLSSignatureOpcIntegrityStatus;
  I, J: Integer;
begin
  Bytes := TFile.ReadAllBytes(FileName);  // une photo, vérifiée et analysée
  Status := XlsVerifyAllSignaturesOpcPackage(Bytes, Reports);
  for I := 0 to High(Reports) do
  begin
    Writeln(String(Reports[I].SignaturePart), ': ', Ord(Reports[I].Status));
    if Reports[I].Status = xsoiIntegrityVerified then
      for J := 0 to High(Reports[I].Integrity.Coverage) do
        Writeln('  covers ', String(Reports[I].Integrity.Coverage[J].PartName));
  end;
  Result := Status = xsoiIntegrityVerified;  // xsoiUnsigned nest pas un succès
end;

Les valeurs de statut sont assez spécifiques pour agir. xsoiDigestMismatch signifie quune partie sélectionnée a changé après signature, xsoiInvalidSignature signifie que la valeur RSA ne se vérifie pas contre le SignedInfo avec la clé du certificat incorporé, xsoiPartUnavailable signifie quune partie sélectionnée manque, et xsoiUnsupportedProfile ou xsoiResourceLimit signifient que le vérificateur a refusé plutôt que deviné. La couche ZIP en dessous applique la même lecture à échec fermé décrite dans larticle sur la validation ZIP EOCD pour des fichiers XLSX non fiables, et la recherche de parties suit les règles de la résolution de relations OPC XLSX

Pourquoi éditer un classeur signé invalide-t-il la signature ?

Éditer un classeur signé invalide la signature dès quun octet changé tombe dans une sélection signée, parce que le digest SHA-256 stocké pour cette Reference ne correspond plus. Un rédacteur de tableur qui charge le classeur et le sauvegarde à nouveau touche presque toujours des parties signées : il resérialise du XML, renumérote les chaînes partagées, réécrit workbook.xml et le manifeste de types de contenu. Même quand les valeurs de cellules sont identiques, les octets ne le sont pas, et la vérification suivante renvoie xsoiDigestMismatch

HotXLS vous laisse poser la question avant de vous engager dans une édition. XlsEditVerifiedOpcPackage remplace des parties existantes dans un paquet vérifié puis vérifie le candidat. Avec xsoePreserve il renvoie xsoePreserved seulement quand la signature se vérifie encore, ce qui arrive quand les parties éditées se trouvent hors de toute sélection signée, et xsoeWouldInvalidate sinon. Avec xsoeInvalidateChanged il retire le lien de signature des relations racine et de [Content_Types].xml et renvoie un paquet explicitement non signé comme xsoeInvalidated, au lieu de vous tendre un fichier qui a encore lair signé

function EditKeepsSignature(const Signed, NewSheetXml: TBytes;
  out Edited: TBytes): Boolean;
var
  Edits: TXLSSignatureOpcEdits;
  Details: TXLSSignatureOpcEditResult;
begin
  SetLength(Edits, 1);
  Edits[0].PartName := '/xl/worksheets/sheet2.xml';
  Edits[0].Data := NewSheetXml;
  case XlsEditVerifiedOpcPackage(Signed, Edits, xsoePreserve, Edited, Details) of
    xsoePreserved:       Result := True;   // sheet2 était hors de la sélection signée
    xsoeWouldInvalidate: Result := False;  // un digest signé couvre sheet2
  else
    raise Exception.CreateFmt('Edit refused, original integrity %d',
      [Ord(Details.OriginalIntegrity)]);
  end;
end;

Intégrité face à confiance : que prouve une signature vérifiée ?

Une signature vérifiée prouve que les parties sélectionnées sont inchangées depuis que quelquun tenant la clé privée les a signées, et rien sur qui est ce quelquun. HotXLS garde les deux couches séparées à dessein : chaque TXLSSignatureOpcIntegrityResult porte Trust = xsstUntrusted, contrôles réussis compris, et ClaimedSignatureTime est une donnée fournie par le signataire, pas un horodatage de confiance. Nimporte qui peut générer un certificat auto-signé, signer un classeur, et passer le contrôle dintégrité

HotXLS vérifie les signatures numériques de paquet OPC Excel en Delphi en deux couches : des contrôles de digest dintégrité sur une photo doctets immuable qui rapportent toujours xsstUntrusted, puis XlsVerifySignatureOpcPackageWithTrust avec une politique détenue par lappelant
Une signature vérifiée prouve que les parties sélectionnées sont inchangées, tandis que la confiance exige vos propres ancres, un temps de vérification UTC et lEKU de signature de document avant que xsotTrustedSelection ne signifie quoi que ce soit

La confiance est un appel séparé avec une politique détenue par lappelant. XlsDefaultSignatureOpcTrustPolicy renvoie une politique à échec fermé sans ancres, sans temps de vérification, avec lEKU de signature de document 1.3.6.1.4.1.311.10.3.12 et la révocation xsrvRequireCached. XlsVerifySignatureOpcPackageWithTrust exécute dabord le vérificateur dintégrité complet puis évalue le certificat qui la réellement passé ; un appelant ne peut pas substituer un certificat feuille différent

uses
  System.SysUtils, System.DateUtils, lxSignatureOpcIntegrity;

function SignedUnderCorporateCA(const Bytes, RootDer, IssuingCaDer: TBytes): Boolean;
var
  Policy: TXLSSignatureOpcTrustPolicy;
  Trust: TXLSSignatureOpcTrustResult;
begin
  Policy := XlsDefaultSignatureOpcTrustPolicy;
  SetLength(Policy.TrustAnchors, 1);
  Policy.TrustAnchors[0] := RootDer;
  SetLength(Policy.Intermediates, 1);
  Policy.Intermediates[0] := IssuingCaDer;
  Policy.VerificationTimeUtc := TTimeZone.Local.ToUniversalTime(Now);

  Result := XlsVerifySignatureOpcPackageWithTrust(Bytes, Policy, Trust)
    = xsotTrustedSelection;
  if not Result then
    Writeln(Format('trust %d, integrity %d, policy error %.8x, chain errors %.8x',
      [Ord(Trust.Status), Ord(Trust.IntegrityStatus),
       Trust.NativePolicyError, Trust.NativeChainErrors]));
end;

Le backend Windows a deux pièges faciles à rater quand vous appelez WinCrypt vous-même. CertVerifyCertificateChainPolicy renvoyant True signifie seulement que lappel a exécuté ; lacceptation exige encore un dwError nul dans le statut de politique, zéro erreurs de chaîne agrégées, un usage de clé digitalSignature explicite sur la feuille et lEKU requis. HotXLS a constaté quun certificat manquant dusage de clé peut passer la politique de base ordinaire, donc il vérifie DigitalSignatureUsage et RequiredEkuPresent lui-même. Le second piège : combiner un magasin racine exclusif avec hRestrictedOther dans la config du moteur de chaîne renvoie E_INVALIDARG sur le backend actuel, si bien que HotXLS emploie le magasin racine exclusif de lappelant puis autorise chaque élément de la chaîne primaire renvoyée contre les octets DER que vous avez fournis. Un émetteur qui se trouve simplement dans un magasin système local ne devient jamais de confiance

xsotTrustedSelection ne décrit toujours que cette signature et sa couverture rapportée. Des cellules non signées et des relations non sélectionnées peuvent changer pendant que la signature sélectionnée reste de confiance, voilà pourquoi le résultat de confiance renvoie aussi PackageSha256 et CertificateSha256 pour votre journal daudit

Comment re-signer ou contre-signer un classeur sans Excel ?

Au niveau tableau doctets, HotXLS re-signe par des transactions explicites et autorisées, et les chemins de sauvegarde signée du Workbook réemploient les mêmes pipelines vérifiés. XlsReplaceVerifiedSignatureOpcPackage échange une signature vérifiée contre une nouvelle, XlsEditAndReplaceSelectedSignatureOpcPackage applique des éditions de parties et remplace une signature sélectionnée dans la même transaction, et XlsAppendVerifiedSignatureOpcPackage ajoute une signature en gardant chaque original vérifié octet pour octet. Les appels de remplacement prennent un TXLSSignatureOpcReplaceAuthorization, lappel dajout vérifie chaque original dabord, et tous prennent une sélection TXLSSignatureOpcIssueOptions et un callback de signataire ; la bibliothèque ne touche jamais votre clé privée

Lenregistrement dautorisation lie lopération à lexact original que vous avez approuvé : ExpectedPackageSha256, ExpectedSignaturePart et ExpectedCertificateSha256, tous comparés seulement après que lancienne signature sest entièrement vérifiée. Chaque partie éditée doit être couverte par la nouvelle sélection, chaque signature non sélectionnée doit encore se vérifier contre le paquet monté avant que le signataire soit appelé, et lappel renvoie xspsWouldInvalidate sinon

HotXLS re-signer un paquet XLSX Excel en Delphi via XlsEditAndReplaceSelectedSignatureOpcPackage, qui vérifie lancienne signature, contrôle les hash attendus de paquet, de partie et de certificat, et appelle un callback de signataire
Lenregistrement dautorisation lie le remplacement à loriginal approuvé, les signatures non sélectionnées doivent encore se vérifier sur le paquet monté, et la clé privée reste dans votre CNG, HSM ou carte à puce
uses
  System.SysUtils, System.DateUtils, lxHash, lxSignatureOpcIntegrity;

type
  TWorkbookResigner = class
  private
    function SignDigest(const Request: TXLSSignatureOpcSelectedReplaceRequest;
      var Signature: TBytes): TXLSSignatureOpcSignerStatus;
  public
    function Resign(const Approved, NewSheetXml, NewCertDer: TBytes;
      const OldCertSha256: WideString; var Output: TBytes): Boolean;
  end;

function TWorkbookResigner.SignDigest(
  const Request: TXLSSignatureOpcSelectedReplaceRequest;
  var Signature: TBytes): TXLSSignatureOpcSignerStatus;
begin
  // Request.NewSignature.Digest est le SHA-256 de 32 octets que la bibliothèque a calculé ;
  // RsaSignSha256Pkcs1 représente votre propre wrapper CNG, HSM ou carte à puce et
  // doit renvoyer la signature RSASSA-PKCS1-v1_5 big-endian complète
  if RsaSignSha256Pkcs1(Request.NewSignature.Digest, Signature) then
    Result := xsosSigned
  else
    Result := xsosFailed;
end;

function TWorkbookResigner.Resign(const Approved, NewSheetXml, NewCertDer: TBytes;
  const OldCertSha256: WideString; var Output: TBytes): Boolean;
var
  Auth: TXLSSignatureOpcReplaceAuthorization;
  Options: TXLSSignatureOpcIssueOptions;
  Edits: TXLSSignatureOpcEdits;
  Details: TXLSSignatureOpcSelectedReplaceResult;
begin
  Auth.ExpectedPackageSha256 := XLSComputeSHA256Hex(Approved); // ou le hash stocké à lapprobation
  Auth.ExpectedSignaturePart := '/_xmlsignatures/sig1.xml';
  Auth.ExpectedCertificateSha256 := OldCertSha256;

  SetLength(Edits, 1);
  Edits[0].PartName := '/xl/worksheets/sheet1.xml';
  Edits[0].Data := NewSheetXml;

  SetLength(Options.References, 2);
  Options.References[0].PartName := '/xl/workbook.xml';
  Options.References[0].Mode := xsimCanonicalXml;
  Options.References[1].PartName := '/xl/worksheets/sheet1.xml';
  Options.References[1].Mode := xsimCanonicalXml;
  Options.ClaimedSignatureTimeUtc := FormatDateTime('yyyy"-"mm"-"dd"T"hh":"nn":"ss"Z"',
    TTimeZone.Local.ToUniversalTime(Now));

  Result := XlsEditAndReplaceSelectedSignatureOpcPackage(Approved, NewCertDer,
    Auth, Options, Edits, SignDigest, Output, Details) = xspsReplaced;
end;

Deux détails de ce code sont délibérés. Output et Details doivent être des variables séparées ; passer un champ de Details comme Output est explicitement non pris en charge, parce que la bibliothèque ne peut pas publier un paquet et garder un rapport de certificat dans le même stockage. Et lancienne couverture nest pas héritée : la nouvelle signature authentifie exactement les References que vous listez, donc décidez encore quelles parties un consommateur en aval utilisera

La contre-signature emploie le même appel dajout. Depuis HotXLS 2.384.296, TXLSSignatureOpcIssueOptions.References passé à XlsAppendVerifiedSignatureOpcPackage peut nommer des parties de linventaire vérifié des signatures dorigine, si bien quun relecteur peut signer par-dessus une partie de signature existante, chaîner plusieurs contre-signatures, ou mélanger parties de signatures et parties de contenu dans une seule sélection. Chaque signature ajoutée nenregistre que ses sélections directes. La qualification de cette version a exécuté 222 contrôles de bord et 122 contrôles indépendants avec le vérificateur de paquetage .NET WindowsBase sur 7 paquets de sortie, confirmant exactement une nouvelle signature et des signatures et certificats dorigine identiques octet pour octet

Pièges et limites à connaître

Le bug le plus instructif venait dun caractère parfaitement légal. Les URIs de Reference OPC portent le type de contenu comme une requête, par exemple /xl/worksheets/sheet1.xml?ContentType=application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml. Un premier émetteur HotXLS encodait en pourcentage le + littéral en %2B. Les propres contrôles RSA, C14N et Reference de HotXLS acceptaient le résultat, mais WindowsBase rapportait InvalidSignature ; un échantillon contrôlé qui ne changeait que cette orthographe passait de InvalidSignature à Success. Lémetteur garde maintenant la ponctuation de requête ASCII littérale prise en charge tout en échappant toujours les attributs XML, et la leçon pour tout signataire est de tester contre un vérificateur natif indépendant, pas seulement le vôtre

  • Le backend de confiance est Windows seul ; les autres cibles renvoient xsotTrustUnavailable après que lintégrité a réussi
  • Le vérificateur accepte des paquets jusquà 64 Mio, 16 Mio par partie, et au plus 32 parties de signature sous une origine
  • Lémission accepte jusquà 256 References et 4096 sélecteurs combinés par signature
  • Le profil pris en charge est RSA-SHA256, digests SHA-256 et Canonical XML sans commentaires ; dautres algorithmes, des Objects dapplication supplémentaires et des References externes renvoient xsoiUnsupportedProfile
  • xsrvRequireCached emploie des données de révocation en cache seul ; un statut manquant rejette, et xsrvNotChecked ne donne aucune assurance de révocation
  • Signer un classeur quExcel doit aussi accepter se heurte encore aux règles de paquet de pourquoi Excel répare un XLSX valide ; un Override dupliqué ou un Id de relation casse le fichier avant que la moindre signature soit vérifiée

FAQ

Un contrôle dintégrité HotXLS signifie-t-il que le signataire est de confiance ?

Non. XlsVerifySignatureOpcPackage prouve que les parties sélectionnées correspondent à leurs digests sous la clé du certificat incorporé et rapporte toujours xsstUntrusted. Employez XlsVerifySignatureOpcPackageWithTrust avec vos propres ancres et un temps de vérification UTC pour évaluer la chaîne de certificats

Pourquoi Excel montre-t-il ma signature de classeur comme invalide après une édition Delphi ?

Un classeur sauvegardé réécrit des parties XML signées, si bien que le digest SHA-256 dau moins une Reference change. Employez XlsEditVerifiedOpcPackage avec xsoePreserve pour tester une édition dabord, ou XlsEditAndReplaceSelectedSignatureOpcPackage pour éditer et re-signer en une transaction vérifiée

HotXLS peut-il signer avec une clé stockée sur une carte à puce ou un HSM ?

Oui, parce que la signature est un callback de lappelant. La bibliothèque passe le SignedInfo canonique et son digest SHA-256 de 32 octets, votre code renvoie la signature RSASSA-PKCS1-v1_5, et la clé privée ne quitte jamais le fournisseur que vous contrôlez

Vérification de signatures OPC, évaluation de confiance, contre-signature et re-signature vérifiée font partie de HotXLS, le composant de tableur Excel natif pour Delphi et C++Builder, qui lit et écrit XLS, XLSX, XLSB et ODS sans Excel ni automatisation COM