Technischer Artikel

Excel-Signaturen in Delphi verifizieren und neu signieren

HotXLS verifiziert und signiert Excel-Digitalsignaturen in Delphi neu, ohne dass Excel installiert ist. Die Unit lxSignatureOpcIntegrity prüft ECMA-376 Part 2 Paket-Signaturen direkt aus den Workbook-Bytes: RSA-SHA256 über das kanonische SignedInfo, jeden SHA-256-Reference-Digest und die exakten Parts, die jede Signatur auswählt. Zertifikatsketten-Vertrauen ist ein separater, expliziter Schritt, und Editieren oder Neu-Signieren veröffentlicht nur Output, der erneut verifiziert

Der übliche Auslöser ist ein Dokumenten-Workflow, der aus dem Desktop hinausgewachsen ist. Ein Controller signiert eine Budget-Arbeitsmappe in Excel, ein Delphi-Dienst muss später die Signatur bestätigen, bevor er die Zahlen importiert, und ein dritter Schritt muss ein Sheet korrigieren und unter einem Service-Zertifikat erneut signieren. Excel dafür auf einem Server zu automatisieren ist fragil und unsupported, und die Signatur als Ja/Nein-Flag zu behandeln versteckt die beiden Fragen, die tatsächlich zählen: Zu welchen Bytes hat sich der Unterzeichner committed, und vertrauen Sie der Person, die diesen Schlüssel hält

Was deckt eine OPC-Paket-Signatur tatsächlich ab?

Eine OPC-Paket-Signatur deckt nur die Parts ab, die ihr Manifest auflistet, nicht die ganze XLSX-Datei. ECMA-376 Part 2, §13 (Digital Signatures), platziert einen Signature-Origin-Part im Paket, typischerweise /_xmlsignatures/origin.sigs, mit Relationships zu einem oder mehreren Signature-Parts wie /_xmlsignatures/sig1.xml. Jeder Signature-Part ist eine W3C-XML-DSig-Signature, deren SignedInfo auf ein internes idPackageObject verweist, und dieses Object trägt ein Manifest aus References auf einzelne Package-Parts, jeder mit eigenem Digest

HotXLS unterstützt drei Arten, wie eine Reference einen Part binden kann, und sie sind nicht austauschbar. Eine rohe Reference hasht die gespeicherten Part-Bytes. Eine Canonical-XML-Reference hasht die Form Canonical XML (without comments), sodass Attributreihenfolge und die Schreibweise von Namespace-Deklarationen keine Rolle mehr spielen. Eine Relationship-Reference wendet zuerst die OPC-RelationshipTransform an und kanonisiert dann, und diese Transform behält nur die Relationships, die ihre SourceId- oder SourceType-Selektoren treffen. Ein Selektor authentifiziert exakt die Relationships, die er auswählt: Der Rest dieses .rels-Parts darf sich frei ändern, während die Signatur gültig bleibt

HotXLS-Anatomie einer OPC-Digitalsignatur für Delphi: ein origin.sigs-Part führt zu XML-DSig-Signature-Parts, deren Manifest jeden Package-Part mit einer rohen, Canonical-XML- oder RelationshipTransform-Reference bindet
Drei Reference-Modi sind nicht austauschbar: Raw hasht gespeicherte Bytes, Canonical XML ignoriert Attributschreibweisen, und Relationship-Selektoren lassen die nicht gewählten .rels-Einträge frei änderbar

Dieser letzte Punkt ist der Grund, warum HotXLS Coverage meldet statt eines einzelnen grünen Häkchens. TXLSSignatureOpcIntegrityResult.Coverage ist ein Array aus TXLSSignatureOpcCoverage-Records, einer pro authentifizierter Manifest-Reference, jeweils mit ReferenceUri, PartName, ContentType, Transforms, RelationshipSourceIds und RelationshipSourceTypes. Wenn Ihr Import sheet3.xml liest und kein Deskriptor es nennt, sagt die Signatur nichts über die Zahlen aus, die Sie gleich laden

Signaturen aus einem unveränderlichen ZIP-Snapshot verifizieren

HotXLS verifiziert ein Paket aus einem einzigen unveränderlichen Byte-Snapshot, nie aus der Datei auf der Platte oder einem lebenden TWorkbook. XlsVerifySignatureOpcPackage und seine Multi-Signature-Geschwister nehmen ein TBytes-Array, lesen nur dieses Array und holen niemals externe Ziele, Zertifikate, lokale Dateien oder Netzwerkressourcen. Der Punkt: Die Bytes, die Sie verifiziert haben, sind die Bytes, die Sie danach parsen — wird die Datei später neu gelesen, kann ein anderer Prozess sie zwischenzeitlich ersetzt haben

Die Kanonisierung ist der Ort, an dem selbst gebaute Verifizierer meist scheitern. SignedInfo und das Package-Object müssen im Namespace-Kontext der vollständigen Signature-Hülle kanonisiert werden. Ein isoliertes Fragment, aus dem Dokument herausgeschnitten, erbt andere Namespace-Deklarationen als der Verifizierer an Ort und Stelle sieht, also unterscheiden sich seine C14N-Bytes, der Digest unterscheidet sich, und eine vollkommen gültige Signatur scheitert. HotXLS verwendet den vollen Hüllen-Kontext auf der verifizierenden wie auf der ausstellenden Seite

Für Pakete, die mehr als eine Signatur tragen können, verifiziert XlsVerifyAllSignaturesOpcPackage jede angehängte Signatur und behält die Einzelergebnisse. Der Aggregatstatus ist xsoiIntegrityVerified nur, wenn jede zugelassene Signatur verifiziert; sonst ist er der erste Fehlschlag in Inventarreihenfolge, und jeder Report sagt weiterhin, welcher Signature-Part scheiterte

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);  // ein Snapshot, verifiziert und geparst
  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 ist kein Bestehen
end;

Die Statuswerte sind konkret genug, um darauf zu handeln. xsoiDigestMismatch bedeutet, dass ein gewählter Part sich nach dem Signieren geändert hat, xsoiInvalidSignature, dass der RSA-Wert sich gegen SignedInfo mit dem eingebetteten Zertifikatsschlüssel nicht verifiziert, xsoiPartUnavailable, dass ein gewählter Part fehlt, und xsoiUnsupportedProfile oder xsoiResourceLimit, dass der Verifizierer sich geweigert hat, statt zu raten. Die darunterliegende ZIP-Schicht wendet dasselbe Fail-closed-Lesen an, das im Artikel zur ZIP-EOCD-Validierung für nicht vertrauenswürdige XLSX-Dateien beschrieben ist, und die Part-Suche folgt den Regeln der XLSX-OPC-Relationship-Auflösung

Warum macht das Editieren einer signierten Arbeitsmappe die Signatur ungültig?

Das Editieren einer signierten Arbeitsmappe macht die Signatur ungültig, sobald ein geändertes Byte in eine signierte Auswahl fällt, denn der gespeicherte SHA-256-Digest für diese Reference passt nicht mehr. Ein Spreadsheet-Writer, der die Arbeitsmappe lädt und erneut speichert, fasst fast immer signierte Parts an: Er serialisiert XML neu, nummeriert Shared Strings um, schreibt workbook.xml und das Content-Type-Manifest neu. Selbst wenn die Zellwerte identisch sind, sind die Bytes es nicht, und die nächste Verifikation liefert xsoiDigestMismatch

HotXLS lässt Sie die Frage stellen, bevor Sie sich auf eine Bearbeitung festlegen. XlsEditVerifiedOpcPackage ersetzt bestehende Parts in einem verifizierten Paket und verifiziert dann den Kandidaten. Mit xsoePreserve liefert es xsoePreserved nur, wenn die Signatur weiterhin verifiziert — was passiert, wenn die editierten Parts außerhalb jeder signierten Auswahl liegen —, und sonst xsoeWouldInvalidate. Mit xsoeInvalidateChanged entfernt es die Signaturbindung aus den Root-Relationships und [Content_Types].xml und gibt ein explizit unsigniertes Paket als xsoeInvalidated zurück, statt Ihnen eine Datei zu reichen, die noch signiert aussieht

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 lag außerhalb der signierten Auswahl
    xsoeWouldInvalidate: Result := False;  // ein signierter Digest deckt sheet2 ab
  else
    raise Exception.CreateFmt('Edit refused, original integrity %d',
      [Ord(Details.OriginalIntegrity)]);
  end;
end;

Integrität gegenüber Vertrauen: Was beweist eine verifizierte Signatur?

Eine verifizierte Signatur beweist, dass die ausgewählten Parts unverändert sind, seit jemand mit dem privaten Schlüssel sie signiert hat — und nichts darüber, wer dieser jemand ist. HotXLS hält die beiden Schichten absichtlich auseinander: Jedes TXLSSignatureOpcIntegrityResult trägt Trust = xsstUntrusted, eingeschlossen erfolgreicher Prüfungen, und ClaimedSignatureTime ist vom Unterzeichner gelieferte Angabe, kein vertrauenswürdiger Zeitstempel. Jeder kann ein selbstsigniertes Zertifikat erzeugen, eine Arbeitsmappe signieren und die Integritätsprüfung bestehen

HotXLS verifiziert Excel-OPC-Paket-Digitalsignaturen in Delphi in zwei Schichten: Integritäts-Digest-Prüfungen über einen unveränderlichen Byte-Snapshot, die stets xsstUntrusted melden, dann XlsVerifySignatureOpcPackageWithTrust mit einer Policy im Besitz des Aufrufers
Eine verifizierte Signatur beweist, dass die gewählten Parts unverändert sind, während Vertrauen eigene Anker, eine UTC-Verifikationszeit und die Document-Signing-EKU braucht, bevor xsotTrustedSelection etwas bedeutet

Vertrauen ist ein separater Aufruf mit einer Policy im Besitz des Aufrufers. XlsDefaultSignatureOpcTrustPolicy liefert eine Fail-closed-Policy ohne Anker, ohne Verifikationszeit, mit der Document-Signing-EKU 1.3.6.1.4.1.311.10.3.12 und xsrvRequireCached-Revocation. XlsVerifySignatureOpcPackageWithTrust fährt zuerst den vollständigen Integritätsverifizierer und evaluiert dann das Zertifikat, das ihn tatsächlich bestanden hat; ein Aufrufer kann kein anderes Leaf-Zertifikat unterschieben

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;

Das Windows-Backend hat zwei Fallen, die leicht übersehen werden, wenn man WinCrypt selbst aufruft. Dass CertVerifyCertificateChainPolicy True zurückgibt, bedeutet nur, dass der Aufruf ausgeführt wurde; die Annahme verlangt weiterhin ein null dwError im Policy-Status, null aggregierte Chain-Fehler, eine explizite digitalSignature-Key-Usage am Leaf und die geforderte EKU. HotXLS stellte fest, dass ein Zertifikat ohne Key-Usage die gewöhnliche Base-Policy bestehen kann, also prüft es DigitalSignatureUsage und RequiredEkuPresent selbst. Die zweite Falle: Eine exklusive Root-Store mit hRestrictedOther in der Chain-Engine-Konfiguration zu kombinieren liefert E_INVALIDARG auf dem aktuellen Backend, also verwendet HotXLS die exklusive Caller-Root-Store und autorisiert danach jedes Element der zurückgegebenen primären Kette gegen die von Ihnen gelieferten DER-Bytes. Ein Issuer, der zufällig in einem lokalen System-Store sitzt, wird dadurch nie vertrauenswürdig

xsotTrustedSelection beschreibt weiterhin nur diese Signatur und ihre gemeldete Coverage. Unsignierte Zellen und nicht gewählte Relationships dürfen sich ändern, während die gewählte Signatur vertrauenswürdig bleibt — deshalb liefert das Trust-Ergebnis auch PackageSha256 und CertificateSha256 für Ihr Audit-Log

Wie signiert oder gegensigniert man eine Arbeitsmappe ohne Excel neu?

Auf Byte-Array-Ebene signiert HotXLS über explizite, autorisierte Transaktionen neu, und die signierten Save-Pfade des Workbooks wiederverwenden dieselben verifizierten Pipelines. XlsReplaceVerifiedSignatureOpcPackage tauscht eine verifizierte Signatur gegen eine neue, XlsEditAndReplaceSelectedSignatureOpcPackage wendet Part-Edits an und ersetzt eine gewählte Signatur in derselben Transaktion, und XlsAppendVerifiedSignatureOpcPackage ergänzt eine Signatur, während es jedes verifizierte Original Byte für Byte hält. Die Replace-Aufrufe nehmen eine TXLSSignatureOpcReplaceAuthorization, der Append-Aufruf verifiziert jedes Original zuerst, und alle nehmen eine TXLSSignatureOpcIssueOptions-Auswahl und einen Signer-Callback; die Bibliothek berührt Ihren privaten Schlüssel nie

Der Autorisierungs-Record bindet die Operation an das exakte Original, das Sie freigegeben haben: ExpectedPackageSha256, ExpectedSignaturePart und ExpectedCertificateSha256, alle erst verglichen, nachdem die alte Signatur vollständig verifiziert ist. Jeder editierte Part muss von der neuen Auswahl abgedeckt sein, jede nicht gewählte Signatur muss sich weiterhin gegen das gestaffelte Paket verifizieren, bevor der Signer gerufen wird, und der Aufruf liefert sonst xspsWouldInvalidate

HotXLS signiert ein Excel-XLSX-Paket in Delphi über XlsEditAndReplaceSelectedSignatureOpcPackage neu, das die alte Signatur verifiziert, die erwarteten Paket-, Part- und Zertifikats-Hashes prüft und einen Signer-Callback aufruft
Der Autorisierungs-Record bindet den Replace an das freigegebene Original, nicht gewählte Signaturen müssen sich weiterhin auf dem gestaffelten Paket verifizieren, und der private Schlüssel bleibt in Ihrem CNG, HSM oder Ihrer Smartcard
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 ist der 32-Byte-SHA-256, den die Bibliothek berechnete;
  // RsaSignSha256Pkcs1 steht für Ihren eigenen CNG-, HSM- oder Smartcard-Wrapper und
  // muss die volle Big-Endian-RSASSA-PKCS1-v1_5-Signatur zurückgeben
  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); // oder der bei der Freigabe gespeicherte Hash
  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;

Zwei Details in diesem Code sind absichtlich so. Output und Details müssen getrennte Variablen sein; ein Feld von Details als Output zu übergeben ist explizit nicht unterstützt, denn die Bibliothek kann kein Paket veröffentlichen und einen Zertifikatsreport im selben Speicher halten. Und die alte Coverage wird nicht geerbt: Die neue Signatur authentifiziert exakt die References, die Sie auflisten — entscheiden Sie also erneut, auf welche Parts ein nachgelagerter Konsument sich stützen wird

Gegensignieren verwendet denselben Append-Aufruf. Seit HotXLS 2.384.296 darf TXLSSignatureOpcIssueOptions.References, übergeben an XlsAppendVerifiedSignatureOpcPackage, Parts aus dem verifizierten Original-Signatur-Inventar nennen, sodass ein Prüfer über einen bestehenden Signature-Part signieren, mehrere Counter-Signatures ketten oder Signature-Parts und Content-Parts in einer Auswahl mischen kann. Jede angehängte Signatur verzeichnet nur ihre direkten Auswahlen. Die Qualifikation für dieses Release lief 222 Boundary-Checks und 122 unabhängige Checks mit dem .NET-WindowsBase-Packaging-Verifizierer über 7 Ausgabepakete, mit Bestätigung von genau einer neuen Signatur sowie byte-identischen Original-Signaturen und Zertifikaten

Fallstricke und Grenzen, die man kennen sollte

Der lehrreichste Bug kam von einem Zeichen, das vollkommen legal ist. OPC-Reference-URIs tragen den Content-Type als Query, etwa /xl/worksheets/sheet1.xml?ContentType=application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml. Ein früher HotXLS-Issuer prozent-kodierte das literale + als %2B. HotXLS' eigene RSA-, C14N- und Reference-Prüfungen akzeptierten das Ergebnis, aber WindowsBase meldete InvalidSignature; eine kontrollierte Probe, die nur diese Schreibweise änderte, ging von InvalidSignature zu Success. Der Issuer behält jetzt unterstützte literale ASCII-Query-Interpunktion bei und escaped weiterhin XML-Attribute, und die Lehre für jeden Signer: gegen einen unabhängigen nativen Verifizierer testen, nicht nur gegen den eigenen

  • Das Trust-Backend ist Windows-only; andere Ziele liefern xsotTrustUnavailable, nachdem die Integrität bestanden ist
  • Der Verifizierer akzeptiert Pakete bis 64 MiB, 16 MiB pro Part und höchstens 32 Signature-Parts unter einem Origin
  • Das Ausstellen akzeptiert bis zu 256 References und 4096 kombinierte Selektoren pro Signatur
  • Das unterstützte Profil ist RSA-SHA256, SHA-256-Digests und Canonical XML without comments; andere Algorithmen, zusätzliche Application-Objects und externe References liefern xsoiUnsupportedProfile
  • xsrvRequireCached nutzt rein gecachte Revocation-Daten; fehlender Status weist ab, und xsrvNotChecked gibt keine Revocation-Zusage
  • Eine Arbeitsmappe zu signieren, die Excel ebenfalls akzeptieren muss, stößt weiterhin auf die Paketregeln aus warum Excel ein gültiges XLSX repariert; ein duplizierter Override oder eine duplizierte Relationship-Id bricht die Datei, bevor irgendeine Signatur geprüft wird

FAQ

Bedeutet eine HotXLS-Integritätsprüfung, dass der Unterzeichner vertrauenswürdig ist?

Nein. XlsVerifySignatureOpcPackage beweist, dass die gewählten Parts unter dem eingebetteten Zertifikatsschlüssel zu ihren Digests passen, und meldet stets xsstUntrusted. Verwenden Sie XlsVerifySignatureOpcPackageWithTrust mit Ihren eigenen Ankern und einer UTC-Verifikationszeit, um die Zertifikatskette zu bewerten

Warum zeigt Excel meine Arbeitsmappen-Signatur nach einem Delphi-Edit als ungültig?

Ein gespeichertes Workbook schreibt signierte XML-Parts neu, also ändert sich der SHA-256-Digest mindestens einer Reference. Verwenden Sie XlsEditVerifiedOpcPackage mit xsoePreserve, um eine Bearbeitung zuerst zu testen, oder XlsEditAndReplaceSelectedSignatureOpcPackage, um in einer verifizierten Transaktion zu editieren und neu zu signieren

Kann HotXLS mit einem Schlüssel auf Smartcard oder HSM signieren?

Ja, denn Signieren ist ein Caller-Callback. Die Bibliothek übergibt das kanonische SignedInfo und seinen 32-Byte-SHA-256-Digest, Ihr Code liefert die RSASSA-PKCS1-v1_5-Signatur zurück, und der private Schlüssel verlässt nie den Provider, den Sie kontrollieren

OPC-Signaturverifikation, Trust-Bewertung, Gegensignieren und verifiziertes Neu-Signieren sind Teil von HotXLS, der nativen Excel-Spreadsheet-Komponente für Delphi und C++Builder, die XLS, XLSX, XLSB und ODS ohne Excel oder COM-Automatisierung liest und schreibt