HotXLS weryfikuje i ponownie podpisuje cyfrowe podpisy Excel w Delphi bez zainstalowanego Excela. Moduł lxSignatureOpcIntegrity sprawdza podpisy pakietów ECMA-376 Part 2 bezpośrednio z bajtów skoroszytu: RSA-SHA256 nad kanonicznym SignedInfo, każdy skrót SHA-256 Reference i dokładne części, które każdy podpis zaznacza. Zaufanie do łańcucha certyfikatów to osobny, jawny krok, a edycja albo ponowne podpisanie publikują wyłącznie wyjście, które znowu się weryfikuje
Zwykłym wyzwalaczem jest proces dokumentowy, który przerósł biurko. Kontroler podpisuje skoroszyt budżetowy w Excelu, usługa Delphi musi później potwierdzić podpis przed zaimportowaniem liczb, a trzeci krok potrzebuje poprawić jeden arkusz i podpisać ponownie certyfikatem serwisowym. Automatyzacja Excela na serwerze do tego jest krucha i niewspierana, a traktowanie podpisu jako flagi tak/nie chowa dwa pytania, które faktycznie się liczą: do jakich bajtów zobowiązał się podpisujący i czy ufasz osobie, która trzyma ten klucz
Co podpis pakietu OPC faktycznie obejmuje?
Podpis pakietu OPC obejmuje tylko części wypisane w jego Manifest, a nie cały plik XLSX. ECMA-376 Part 2, §13 (Digital Signatures) umieszcza w pakiecie część początkową podpisu, zwykle /_xmlsignatures/origin.sigs, z relacjami do jednej albo więcej części podpisu, jak /_xmlsignatures/sig1.xml. Każda część podpisu to W3C XML-DSig Signature, której SignedInfo referuje wewnętrzny idPackageObject, a ten Object niesie Manifest z Reference do pojedynczych części pakietu, każda z własnym skrótem
HotXLS wspiera trzy sposoby, na jakie Reference może związać część, i nie są one wymienne. Surowy Reference haszuje przechowane bajty części. Kanoniczny Reference XML haszuje formę Canonical XML (bez komentarzy), więc kolejność atrybutów i pisownia deklaracji przestrzeni nazw przestają się liczyć. Reference relacji najpierw nakłada OPC RelationshipTransform, a potem kanonizuje, i ta transformacja trzyma wyłącznie relacje dopasowane przez jego selektory SourceId albo SourceType. Selektor uwierzytelnia dokładnie te relacje, które zaznacza: reszta tej części .rels może się zmieniać swobodnie, podczas gdy podpis zostaje ważny
Ten ostatni punkt to powód, dla którego HotXLS zgłasza pokrycie zamiast pojedynczej zielonej fałki. TXLSSignatureOpcIntegrityResult.Coverage to tablica rekordów TXLSSignatureOpcCoverage, po jednym na uwierzytelniony Manifest Reference, każdy z ReferenceUri, PartName, ContentType, Transforms, RelationshipSourceIds i RelationshipSourceTypes. Jeśli twój import czyta sheet3.xml, a żaden deskryptor go nie nazywa, podpis nic nie mówi o liczbach, które właśnie zamierzasz wczytać
Weryfikacja podpisów z niezmiennej migawki ZIP
HotXLS weryfikuje pakiet z jednej niezmiennej migawki bajtów, nigdy z pliku na dysku albo żywego TWorkbook. XlsVerifySignatureOpcPackage i jego rodzeństwo wielopodpisowe przyjmują tablicę TBytes, czytają wyłącznie tę tablicę i nigdy nie pobierają zewnętrznych celów, certyfikatów, lokalnych plików ani zasobów sieciowych. Chodzi o to, że bajty, które zweryfikowałeś, to bajty, które potem parsujesz: jeśli plik zostanie później przeczytany ponownie, inny proces mógł go w międzyczasie wymienić
Kanonalizacja to miejsce, gdzie własnoręczne weryfikatory zwykle się mylą. SignedInfo i Object pakietu muszą być kanonizowane w kontekście przestrzeni nazw kompletnej koperty Signature. Wyizolowany fragment wycięty z dokumentu dziedziczy inne deklaracje przestrzeni nazw niż te, które weryfikator widzi na miejscu, więc jego bajty C14N się różnią, skrót się różni i całkiem ważny podpis zawodzi. HotXLS używa pełnego kontekstu koperty po obu stronach, weryfikującej i wystawiającej
Dla pakietów, które mogą nieść więcej niż jeden podpis, XlsVerifyAllSignaturesOpcPackage weryfikuje każdy dołączony podpis i trzyma pojedyncze wyniki. Status zagregowany to xsoiIntegrityVerified tylko, gdy każdy dopuszczony podpis się weryfikuje; inaczej jest nim pierwsza porażka w kolejności inwentarza, a każdy raport dalej mówi, która część podpisu zawiodła
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); // jedna migawka, weryfikowana i parsowana
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 to nie zaliczenie
end;
Wartości statusów są na tyle konkretne, że da się na nie reagować. xsoiDigestMismatch znaczy, że zaznaczona część zmieniła się po podpisaniu, xsoiInvalidSignature znaczy, że wartość RSA nie weryfikuje się względem SignedInfo kluczem z wbudowanego certyfikatu, xsoiPartUnavailable znaczy, że zaznaczonej części brakuje, a xsoiUnsupportedProfile albo xsoiResourceLimit znaczą, że weryfikator odmówił zamiast zgadywać. Warstwa ZIP pod spodem nakłada to samo czytanie fail-closed, opisane w artykule o walidacji EOCD ZIP dla niezaufanych plików XLSX, a wyszukiwanie części idzie za regułami z rozwiązywania relacji OPC XLSX
Dlaczego edycja podpisanego skoroszytu unieważnia podpis?
Edycja podpisanego skoroszytu unieważnia podpis, jak tylko zmieniony bajt wpadnie w podpisane zaznaczenie, bo przechowany skrót SHA-256 dla tego Reference już nie pasuje. Writer arkuszy, który wczytuje skoroszyt i zapisuje go ponownie, prawie zawsze dotyka podpisanych części: reserializuje XML, przenumerowuje współdzielone napisy, przepisuje workbook.xml i manifest typów treści. Nawet gdy wartości komórek są identyczne, bajty nie są, a następna weryfikacja zwraca xsoiDigestMismatch
HotXLS pozwala zadać pytanie, zanim zobowiążesz się do edycji. XlsEditVerifiedOpcPackage wymienia istniejące części w zweryfikowanym pakiecie, a potem weryfikuje kandydata. Z xsoePreserve zwraca xsoePreserved tylko, gdy podpis dalej się weryfikuje, co zdarza się, gdy edytowane części leżą poza każdym podpisanym zaznaczeniem, a xsoeWouldInvalidate inaczej. Z xsoeInvalidateChanged usuwa wiązanie podpisu z relacji korzenia i [Content_Types].xml i zwraca jawnie niepodpisany pakiet jako xsoeInvalidated, zamiast wręczać ci plik, który wciąż wygląda na podpisany
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 był poza podpisanym zaznaczeniem
xsoeWouldInvalidate: Result := False; // podpisany skrót obejmuje sheet2
else
raise Exception.CreateFmt('Edit refused, original integrity %d',
[Ord(Details.OriginalIntegrity)]);
end;
end;
Integralność kontra zaufanie: co udowadnia zweryfikowany podpis?
Zweryfikowany podpis dowodzi, że zaznaczone części są niezmienione od chwili, gdy ktoś trzymający klucz prywatny je podpisał, i nic o tym, kim ten ktoś jest. HotXLS trzyma te dwie warstwy osobno celowo: każdy TXLSSignatureOpcIntegrityResult niesie Trust = xsstUntrusted, łącznie z udanymi sprawdzeniami, a ClaimedSignatureTime to dane od podpisującego, nie zaufany znacznik czasu. Każdy może wygenerować certyfikat samopodpisany, podpisać skoroszyt i przejść sprawdzenie integralności
Zaufanie to osobne wywołanie z polityką należącą do wołającego. XlsDefaultSignatureOpcTrustPolicy zwraca politykę fail-closed bez kotwic, bez czasu weryfikacji, z EKU podpisywania dokumentów 1.3.6.1.4.1.311.10.3.12 i rewokacją xsrvRequireCached. XlsVerifySignatureOpcPackageWithTrust najpierw puszcza kompletny weryfikator integralności, a potem ocenia certyfikat, który faktycznie przeszedł; wołający nie może podstawić innego certyfikatu liścia
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;
Backend Windows ma dwie pułapki, łatwe do przegapienia, gdy wołasz WinCrypt sam. CertVerifyCertificateChainPolicy zwracające True znaczy tylko, że wywołanie się wykonało; przyjęcie wciąż wymaga zerowego dwError w statusie polityki, zerowych zagregowanych błędów łańcucha, jawnego użycia klucza digitalSignature na liściu i wymaganego EKU. HotXLS odkrył, że certyfikat bez użycia klucza może przejść zwykłą politykę bazową, więc sam sprawdza DigitalSignatureUsage i RequiredEkuPresent. Druga pułapka: połączenie ekskluzywnego sklepu korzeni z hRestrictedOther w konfiguracji silnika łańcucha zwraca E_INVALIDARG na obecnym backendzie, więc HotXLS używa ekskluzywnego sklepu korzeni wołającego, a potem autoryzuje każdy element zwróconego łańcucha pierwotnego względem bajtów DER, które podałeś. Wystawca, który jedynie akurat siedzi w lokalnym sklepie systemowym, nigdy nie staje się zaufany
xsotTrustedSelection opisuje dalej wyłącznie ten podpis i jego zgłoszone pokrycie. Komórki niepodpisane i relacje niezaznaczone mogą się zmieniać, podczas gdy zaznaczony podpis zostaje zaufany, dlatego wynik zaufania zwraca też PackageSha256 i CertificateSha256 dla twojego loga audytowego
Jak ponownie podpisać albo kontrpisać skoroszyt bez Excela?
Na poziomie tablicy bajtów HotXLS podpisuje ponownie przez jawne, autoryzowane transakcje, a ścieżki zapisu z podpisem Workbook reużywają tych samych zweryfikowanych potoków. XlsReplaceVerifiedSignatureOpcPackage zamienia jeden zweryfikowany podpis na nowy, XlsEditAndReplaceSelectedSignatureOpcPackage nakłada edycje części i wymienia jeden zaznaczony podpis w tej samej transakcji, a XlsAppendVerifiedSignatureOpcPackage dokłada podpis, trzymając każdą zweryfikowaną oryginalną zawartość bajt w bajt. Wywołania wymiany przyjmują TXLSSignatureOpcReplaceAuthorization, wywołanie dołączenia weryfikuje najpierw każdą oryginalną zawartość, a wszystkie przyjmują zaznaczenie TXLSSignatureOpcIssueOptions i callback podpisującego; biblioteka nigdy nie dotyka twojego klucza prywatnego
Rekord autoryzacji wiąże operację z dokładnym oryginałem, który zatwierdziłeś: ExpectedPackageSha256, ExpectedSignaturePart i ExpectedCertificateSha256, wszystkie porównywane dopiero po pełnej weryfikacji starego podpisu. Każda edytowana część musi być pokryta nowym zaznaczeniem, każdy podpis niezaznaczony musi się dalej weryfikować względem przygotowanego pakietu, zanim zostanie wołany podpisujący, a wywołanie zwraca xspsWouldInvalidate inaczej
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 to 32-bajtowy SHA-256 policzony przez bibliotekę;
// RsaSignSha256Pkcs1 oznacza twoją własną owijarkę CNG, HSM albo karty
// inteligentnej i musi zwrócić pełny 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); // albo hash przechowany przy zatwierdzeniu
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;
Dwa detale tego kodu są celowe. Output i Details muszą być osobnymi zmiennymi; przekazanie pola Details jako Output jest jawnie niewspierane, bo biblioteka nie może opublikować pakietu i trzymać raportu certyfikatu w tym samym przechowaniu. A stare pokrycie nie jest dziedziczone: nowy podpis uwierzytelnia dokładnie Reference, które wypiszesz, więc zdecyduj ponownie, na których częściach będzie polegał konsument po dalszej stronie
Kontrpisanie używa tego samego wywołania dołączającego. Od HotXLS 2.384.296 TXLSSignatureOpcIssueOptions.References przekazywane do XlsAppendVerifiedSignatureOpcPackage może nazywać części z inwentarza zweryfikowanych oryginalnych podpisów, więc recenzent może podpisać nad istniejącą częścią podpisu, skleić łańcuch kilku kontrpodpisów albo pomieszać części podpisów i części treści w jednym zaznaczeniu. Każdy dołączony podpis zapisuje wyłącznie swoje bezpośrednie zaznaczenia. Kwalifikacja dla tego wydania puściła 222 sprawdzenia brzegowe i 122 niezależne sprawdzenia z weryfikatorem pakowania .NET WindowsBase na 7 pakietach wyjściowych, potwierdzając dokładnie jeden nowy podpis i oryginalne podpisy oraz certyfikaty bajt w bajt identyczne
Pułapki i limity warte znajomości
Najbardziej pouczający błąd przyszedł od znaku całkiem legalnego. URI Reference OPC niosą typ treści jako zapytanie, na przykład /xl/worksheets/sheet1.xml?ContentType=application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml. Wczesny wystawca HotXLS kodował procentowo literalny + jako %2B. Własne sprawdzenia RSA, C14N i Reference HotXLS przyjmowały wynik, ale WindowsBase zgłaszało InvalidSignature; kontrolowana próbka, która zmieniała wyłącznie tę pisownię, przechodziła z InvalidSignature na Success. Wystawca trzyma teraz wspierana literalną interpunkcję zapytania ASCII, wciąż escapując atrybuty XML, a nauka dla każdego podpisującego: testuj względem niezależnego natywnego weryfikatora, nie tylko własnego
- Backend zaufania jest wyłącznie Windows; inne cele zwracają
xsotTrustUnavailablepo udanej integralności - Weryfikator przyjmuje pakiety do 64 MiB, 16 MiB na część i najwyżej 32 części podpisu pod jednym pochodzeniem
- Wystawianie przyjmuje do 256 Reference i 4096 połączonych selektorów na podpis
- Wspierany profil to RSA-SHA256, skróty SHA-256 i Canonical XML bez komentarzy; inne algorytmy, dodatkowe Object aplikacyjne i zewnętrzne Reference zwracają
xsoiUnsupportedProfile xsrvRequireCachedużywa danych rewokacji wyłącznie z cache; brak statusu odrzuca, axsrvNotCheckednie daje żadnej pewności rewokacji- Podpisywanie skoroszytu, który Excel też ma przyjąć, wciąż wpada w reguły pakietu z dlaczego Excel naprawia ważny XLSX; zdublowany Override albo identyfikator relacji psuje plik, zanim jakikolwiek podpis zostanie sprawdzony
FAQ
Czy sprawdzenie integralności HotXLS znaczy, że podpisujący jest zaufany?
Nie. XlsVerifySignatureOpcPackage dowodzi, że zaznaczone części pasują do swoich skrótów pod kluczem z wbudowanego certyfikatu, i zawsze zgłasza xsstUntrusted. Użyj XlsVerifySignatureOpcPackageWithTrust z własnymi kotwicami i czasem weryfikacji UTC, żeby ocenić łańcuch certyfikatów
Dlaczego Excel pokazuje podpis mojego skoroszytu jako nieważny po edycji w Delphi?
Zapisany skoroszyt przepisuje podpisane części XML, więc skrót SHA-256 co najmniej jednego Reference się zmienia. Użyj XlsEditVerifiedOpcPackage z xsoePreserve, żeby najpierw przetestować edycję, albo XlsEditAndReplaceSelectedSignatureOpcPackage, żeby edytować i podpisać ponownie w jednej zweryfikowanej transakcji
Czy HotXLS podpisze kluczem przechowywanym na karcie inteligentnej albo HSM?
Tak, bo podpisywanie to callback wołającego. Biblioteka przekazuje kanoniczne SignedInfo i jego 32-bajtowy skrót SHA-256, twój kod zwraca podpis RSASSA-PKCS1-v1_5, a klucz prywatny nigdy nie opuszcza dostawcy, którym sterujesz
Weryfikacja podpisów OPC, ocena zaufania, kontrpisanie i zweryfikowane ponowne podpisywanie są częścią HotXLS, natywnego komponentu arkuszy Excel dla Delphi i C++Builder, który czyta i pisze XLS, XLSX, XLSB i ODS bez Excela i automatyzacji COM