HotXLS ověřuje a znovu podepisuje digitální podpisy Excelu v Delphi bez instalovaného Excelu. Unit lxSignatureOpcIntegrity kontroluje podpisy balíčku ECMA-376 Part 2 přímo z bajtů sešitu: RSA-SHA256 přes kanonický SignedInfo, každý digest Reference SHA-256 a přesné části, které každý podpis vybírá. Důvěra v řetěz certifikátů je samostatný, explicitní krok a editace nebo znovupodepsání publikují jen výstup, který projde ověřením znovu
Obvyklý spouštěč je dokumentový workflow, který přerostl desktop. Kontrolor podepíše rozpočtový sešit v Excelu, služba v Delphi má poté potvrdit podpis před importem čísel a třetí krok potřebuje opravit jeden list a znovu podepsat pod servisním certifikátem. Automatizovat na to Excel na serveru je křehké a nepodporované a brát podpis jako ano/ne příznak schovává dvě otázky, které doopravdy záleží: kterým bajtům se podepisující zavázal a zda důvěřujete osobě, která ten klíč drží
Co podpis balíčku OPC doopravdy pokrývá?
Podpis balíčku OPC pokrývá jen části, které jeho Manifest vypíše, ne celý soubor XLSX. ECMA-376 Part 2, §13 (Digital Signatures) dává do balíčku signature origin part, typicky /_xmlsignatures/origin.sigs, s relacemi na jednu či více signature parts jako /_xmlsignatures/sig1.xml. Každá signature part je W3C XML-DSig Signature, jejíž SignedInfo odkazuje na interní idPackageObject a ten Object nese Manifest z References na jednotlivé části balíčku, každý s vlastním digestem
HotXLS podporuje tři způsoby, jak může Reference svázat část, a nejsou zaměnitelné. Syrová Reference hashuje uložené bajty části. Kanonická XML Reference hashuje tvar Canonical XML (bez komentářů), takže pořadí atributů a pravopis deklarací namespace už nezáleží. Reference relací nejdřív aplikuje OPC RelationshipTransform a pak kanonizuje a ta transformace drží jen relace spárované svými selektory SourceId nebo SourceType. Selektor autentizuje přesně relace, které vybere: zbytek té .rels části se může měnit libovolně, zatímco podpis zůstává platný
Ta poslední věc je důvod, proč HotXLS hlásí pokrytí místo jediné zelené fajfky. TXLSSignatureOpcIntegrityResult.Coverage je pole záznamů TXLSSignatureOpcCoverage, jeden na autentizovanou Manifest Reference, každý s ReferenceUri, PartName, ContentType, Transforms, RelationshipSourceIds a RelationshipSourceTypes. Čte-li váš import sheet3.xml a žádný deskriptor ho nejmenuje, podpis o číslech, která se chystáte načíst, neříká nic
Ověřování podpisů z neměnného ZIP snapshotu
HotXLS ověřuje balíček z jednoho neměnného bajtového snapshotu, nikdy ze souboru na disku ani z živého TWorkbook. XlsVerifySignatureOpcPackage a jeho multi-podpisoví sourozenci berou pole TBytes, čtou jen to pole a nikdy nenačítají externí cíle, certifikáty, lokální soubory ani síťové zdroje. Smysl je v tom, že bajty, které jste ověřili, jsou bajty, které parseujete poté: přečte-li se soubor znovu později, jiný proces ho mezitím mohl vyměnit
Kanonizace je místo, kde domácí verifikátory obvykle selhávají. SignedInfo i Object balíčku se musí kanonizovat v namespace kontextu kompletní obálky Signature. Izolovaný fragment vyříznutý z dokumentu dědí jiné deklarace namespace, než jaké verifikátor vidí na místě, takže jeho C14N bajty se liší, digest se liší a naprosto platný podpis propadne. HotXLS používá plný kontext obálky na ověřovací i vydávající straně
Pro balíčky, které mohou nést víc než jeden podpis, ověří XlsVerifyAllSignaturesOpcPackage každý připojený podpis a drží jednotlivé výsledky. Agregátní status je xsoiIntegrityVerified jen tehdy, když projde každý přijatý podpis; jinak je to první selhání v inventárním pořadí a každý report pořád říká, která signature part selhala
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); // jeden snapshot, ověřený i parsovaný
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 není úspěch
end;
Hodnoty statusu jsou dost specifické na to, aby se dalo jednat. xsoiDigestMismatch znamená, že se vybraná část po podpisu změnila, xsoiInvalidSignature, že hodnota RSA neověřuje proti SignedInfo klíčem embedded certifikátu, xsoiPartUnavailable, že vybraná část chybí, a xsoiUnsupportedProfile nebo xsoiResourceLimit, že se verifikátor odmítl, místo aby hádal. ZIP vrstva pod tím aplikuje totéž fail-closed čtení popsané v článku o validaci ZIP EOCD pro nedůvěryhodné soubory XLSX a hledání částí následuje pravidla z řešení relací OPC v XLSX
Proč editace podepsaného sešitu zneplatní podpis?
Editace podepsaného sešitu zneplatní podpis, jakmile změněný bajt spadne do podepsaného výběru, protože uložený digest SHA-256 pro tu Reference už nesedí. Tabulkový writer, který sešit načte a znovu uloží, se téměř vždy dotkne podepsaných částí: reserializuje XML, přečísluje sdílené řetězce, přepíše workbook.xml a manifest content type. I když jsou hodnoty buněk identické, bajty nejsou a další ověření vrátí xsoiDigestMismatch
HotXLS vám dovolí tu otázku položit dřív, než se editaci zavážete. XlsEditVerifiedOpcPackage vymění existující části v ověřeném balíčku a potom ověří kandidáta. S xsoePreserve vrací xsoePreserved jen tehdy, když podpis stále projde, což nastává, když leží upravené části mimo každý podepsaný výběr, a xsoeWouldInvalidate jinak. S xsoeInvalidateChanged odstraní vazbu podpisu z kořenových relací a [Content_Types].xml a vrátí explicitně nepodepsaný balíček jako xsoeInvalidated, místo aby vám podal soubor, který pořád vypadá podepsaně
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 byl mimo podepsaný výběr
xsoeWouldInvalidate: Result := False; // podepsaný digest pokrývá sheet2
else
raise Exception.CreateFmt('Edit refused, original integrity %d',
[Ord(Details.OriginalIntegrity)]);
end;
end;
Integrita versus důvěra: co ověřený podpis dokazuje?
Ověřený podpis dokazuje, že vybrané části se od okamžiku, kdy je podepsal někdo držící privátní klíč, nezměnily, a nic o tom, kdo tím někdo je. HotXLS drží ty dvě vrstvy oddělené úmyslně: každý TXLSSignatureOpcIntegrityResult nese Trust = xsstUntrusted, včetně úspěšných kontrol, a ClaimedSignatureTime jsou data dodaná podepisujícím, ne důvěryhodné časové razítko. Kdokoli si může vygenerovat self-signed certifikát, podepsat sešit a projít kontrolou integrity
Důvěra je samostatné volání s politikou vlastněnou volajícím. XlsDefaultSignatureOpcTrustPolicy vrací fail-closed politiku bez kotev, bez ověřovacího času, s EKU podepisování dokumentů 1.3.6.1.4.1.311.10.3.12 a revokací xsrvRequireCached. XlsVerifySignatureOpcPackageWithTrust nejdřív pustí kompletní integrity verifikátor a pak vyhodnotí certifikát, který jím skutečně prošel; volající nemůže dosadit jiný listový certifikát
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;
Windows backend má dvě pasti, snadno přehlédnutelné, když voláte WinCrypt sami. Vrácení True z CertVerifyCertificateChainPolicy znamená jen, že volání proběhlo; přijetí stále vyžaduje nulové dwError ve statusu politiky, nulové agregátní chyby řetězce, explicitní key usage digitalSignature na listu a požadované EKU. HotXLS zjistil, že certifikát postrádající key usage může projít obyčejnou base politikou, takže DigitalSignatureUsage a RequiredEkuPresent kontroluje sám. Druhá past: kombinace exkluzivního root store s hRestrictedOther v konfiguraci chain enginu vrací na aktuálním backendu E_INVALIDARG, takže HotXLS používá exkluzivní root store volajícího a pak autorizuje každý element vráceného primárního řetězce proti DER bajtům, které jste dodali. Vystavitel, který jen náhodou sedí v lokálním systémovém store, se nikdy nestane důvěryhodným
xsotTrustedSelection stále popisuje jen ten podpis a jeho hlášené pokrytí. Nepodepsané buňky a nevybrané relace se mohou měnit, zatímco vybraný podpis zůstává důvěryhodný, proto trust výsledek vrací i PackageSha256 a CertificateSha256 pro váš audit log
Jak znovu podepsat nebo protipodepsat sešit bez Excelu?
Na úrovni bajtových polí se HotXLS znovu podepisuje přes explicitní, autorizované transakce a podepsané save cesty Workbooku znovu používají tytéž ověřené pipeline. XlsReplaceVerifiedSignatureOpcPackage vymění jeden ověřený podpis za nový, XlsEditAndReplaceSelectedSignatureOpcPackage aplikuje editace částí a vymění jeden vybraný podpis ve stejné transakci a XlsAppendVerifiedSignatureOpcPackage přidá podpis, přičemž drží každý ověřený původní bajt za bajt. Replace volání berou TXLSSignatureOpcReplaceAuthorization, append volání ověří nejdřív každý původní a všechna berou výběr TXLSSignatureOpcIssueOptions a callback podepisujícího; knihovna se nikdy nedotkne vašeho privátního klíče
Autorizační záznam váže operaci na přesný původní, který jste schválili: ExpectedPackageSha256, ExpectedSignaturePart a ExpectedCertificateSha256, všechna srovnávaná jen poté, co se starý podpis plně ověří. Každá upravená část musí být pokrytá novým výběrem, každý nevybraný podpis musí stále projít proti připravenému balíčku dřív, než se zavolá podepisující, a volání jinak vrátí xspsWouldInvalidate
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 je 32bajtové SHA-256, které knihovna spočítala;
// RsaSignSha256Pkcs1 zastupuje váš vlastní wrapper CNG, HSM nebo smart card a
// musí vrátit kompletní podpis RSASSA-PKCS1-v1_5 big-endian
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); // nebo hash uložený při schválení
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;
Dva detaily v tom kódu jsou záměrné. Output a Details musí být oddělené proměnné; předání pole Details jako Output je explicitně nepodporované, protože knihovna nemůže publikovat balíček a držet report certifikátu ve stejném úložišti. A staré pokrytí se nedědí: nový podpis autentizuje přesně Reference, které vypíšete, takže se znovu rozhodněte, na které části se bude spoléhat konzument po proudu
Protipodepisování používá totéž append volání. Od HotXLS 2.384.296 může TXLSSignatureOpcIssueOptions.References předávané do XlsAppendVerifiedSignatureOpcPackage jmenovat části z inventáře ověřeného původního podpisu, takže reviewer může podepsat nad existující signature part, zřetězit několik protipodpisů nebo smíchat signature parts a content parts v jednom výběru. Každý přidaný podpis zaznamenává jen své přímé výběry. Kvalifikace pro to vydání běžela 222 hraničních kontrol a 122 nezávislých kontrol s .NET WindowsBase packaging verifikátorem přes 7 výstupních balíčků a potvrdila přesně jeden nový podpis a bajtově identické původní podpisy i certifikáty
Pasti a limity, které stojí za znalost
Nejpoučnější bug přišel od znaku, který je naprosto legální. URI OPC Reference nesou content type jako query, například /xl/worksheets/sheet1.xml?ContentType=application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml. Raný issuer HotXLS procentově zakódoval literál + jako %2B. Vlastní kontroly RSA, C14N a Reference HotXLS výsledek přijaly, ale WindowsBase hlásil InvalidSignature; kontrolovaný vzorek, který změnil jen ten pravopis, přešel z InvalidSignature na Success. Issuer teď drží podporovanou literální ASCII query interpunkci, zatímco XML atributy pořád escapuje, a lekce pro jakéhokoli podepisujícího je testovat proti nezávislému nativnímu verifikátoru, ne jen proti svému
- Trust backend je jen Windows; ostatní cíle vrátí
xsotTrustUnavailablepoté, co integrita projde - Verifikátor přijímá balíčky do 64 MiB, 16 MiB na část a nejvýš 32 signature parts pod jedním origin
- Vydávání přijme nejvýš 256 References a 4096 selektorů celkem na podpis
- Podporovaný profil je RSA-SHA256, digesty SHA-256 a Canonical XML bez komentářů; jiné algoritmy, extra application Objects a externí References vrátí
xsoiUnsupportedProfile xsrvRequireCachedpoužívá jen cachovaná revokační data; chybějící status odmítá axsrvNotCheckednedává žádnou záruku revokace- Podepsání sešitu, který má přijmout i Excel, stále naráží na pravidla balíčku z proč Excel opravuje validní XLSX; duplikovaný Override nebo relace Id rozbije soubor dřív, než se zkontroluje jakýkoli podpis
FAQ
Znamená integrity kontrola HotXLS, že podepisující je důvěryhodný?
Ne. XlsVerifySignatureOpcPackage dokazuje, že vybrané části odpovídají svým digestům pod klíčem embedded certifikátu a vždy hlásí xsstUntrusted. Použijte XlsVerifySignatureOpcPackageWithTrust s vlastními kotvami a UTC ověřovacím časem k vyhodnocení řetězce certifikátů
Proč ukazuje Excel můj podpis sešitu jako neplatný po editaci v Delphi?
Uložený sešit přepisuje podepsané XML části, takže se změní SHA-256 digest aspoň jedné Reference. Použijte XlsEditVerifiedOpcPackage s xsoePreserve k otestování editace předem, nebo XlsEditAndReplaceSelectedSignatureOpcPackage k editaci a znovupodepsání v jedné ověřené transakci
Může HotXLS podepsat klíčem uloženým na smart card nebo HSM?
Ano, protože podepisování je callback volajícího. Knihovna předá kanonický SignedInfo a jeho 32bajtový digest SHA-256, váš kód vrátí podpis RSASSA-PKCS1-v1_5 a privátní klíč nikdy neopustí provider, který ovládáte
Ověřování podpisů OPC, vyhodnocování důvěry, protipodepisování i ověřené znovupodepisování jsou součástí HotXLS, nativní komponenty Excel tabulek pro Delphi a C++Builder, která čte a zapisuje XLS, XLSX, XLSB a ODS bez Excelu nebo COM automatizace