Articolo tecnico

Verificare e rifirmare le firme digitali Excel in Delphi

HotXLS verifica e rifirma le firme digitali Excel in Delphi senza Excel installato. L'unità lxSignatureOpcIntegrity controlla le firme del pacchetto ECMA-376 Part 2 direttamente dai byte della cartella di lavoro: RSA-SHA256 sul SignedInfo canonico, ogni digest SHA-256 dei Reference, e le parti esatte che ogni firma seleziona. Il trust sulla catena di certificati è un passo separato ed esplicito, e la modifica o la rifirma pubblica solo output che verifica di nuovo

Il grilletto usuale è un flusso documentale cresciuto oltre il desktop. Un controller firma una cartella di budget in Excel, un servizio Delphi in seguito deve confermare la firma prima di importare i numeri, e un terzo passo deve correggere un foglio e rifirmare sotto un certificato di servizio. Automatizzare Excel su un server per questo è fragile e non supportato, e trattare la firma come un flag sì/no nasconde le due domande che contano davvero: a quali byte si è impegnato il firmatario, e se vi fidate della persona che detiene quella chiave

Cosa copre davvero una firma del pacchetto OPC?

Una firma del pacchetto OPC copre solo le parti che il suo Manifest elenca, non l'intero file XLSX. ECMA-376 Part 2, §13 (Digital Signatures) colloca una parte di origine firma nel pacchetto, tipicamente /_xmlsignatures/origin.sigs, con relationship verso una o più parti firma come /_xmlsignatures/sig1.xml. Ogni parte firma è una Signature W3C XML-DSig il cui SignedInfo referenzia un idPackageObject interno, e quell'Object porta un Manifest di Reference verso singole parti del pacchetto, ciascuna con il proprio digest

HotXLS supporta tre modi in cui un Reference può legare una parte, e non sono intercambiabili. Un Reference raw fa l'hash dei byte memorizzati della parte. Un Reference XML canonico fa l'hash della forma Canonical XML (senza commenti), così l'ordine degli attributi e l'ortografia delle dichiarazioni di namespace non contano più. Un Reference relationship applica prima la OPC RelationshipTransform e poi canonizza, e quella transform conserva solo le relationship selezionate dai suoi selettori SourceId o SourceType. Un selettore autentica esattamente le relationship che seleziona: il resto di quella parte .rels può cambiare liberamente mentre la firma resta valida

Anatomia delle firme digitali OPC di HotXLS per Delphi: una parte origin.sigs porta a parti firma XML-DSig il cui Manifest lega ogni parte del pacchetto con un reference raw, XML canonico o RelationshipTransform
Le tre modalità di Reference non sono intercambiabili: raw fa l'hash dei byte memorizzati, il XML canonico ignora l'ortografia degli attributi, e i selettori relationship lasciano le voci .rels non selezionate libere di cambiare

Quell'ultimo punto è perché HotXLS riporta la copertura invece di un singolo segno di spunta verde. TXLSSignatureOpcIntegrityResult.Coverage è un array di record TXLSSignatureOpcCoverage, uno per Reference autenticato del Manifest, ciascuno con ReferenceUri, PartName, ContentType, Transforms, RelationshipSourceIds e RelationshipSourceTypes. Se il vostro import legge sheet3.xml e nessun descrittore la nomina, la firma non dice nulla sui numeri che state per caricare

Verificare le firme da uno snapshot ZIP immutabile

HotXLS verifica un pacchetto da un solo snapshot di byte immutabile, mai dal file su disco o da un TWorkbook in vita. XlsVerifySignatureOpcPackage e i suoi fratelli multi-firma prendono un array TBytes, leggono solo quell'array, e non recuperano mai target esterni, certificati, file locali o risorse di rete. Il punto è che i byte che avete verificato sono i byte che poi analizzate: se il file viene riletto dopo, un altro processo può averlo sostituito nel frattempo

La canonicalizzazione è dove i verifier fatti in casa di solito sbagliano. SignedInfo e l'Object del pacchetto vanno canonizzati nel contesto di namespace della busta Signature completa. Un frammento isolato ritagliato dal documento eredita dichiarazioni di namespace diverse da quelle che il verifier vede sul posto, così i suoi byte C14N differiscono, il digest differisce, e una firma perfettamente valida fallisce. HotXLS usa il contesto completo della busta sia sul lato verifica sia su quello di emissione

Per i pacchetti che possono portare più di una firma, XlsVerifyAllSignaturesOpcPackage verifica ogni firma allegata e conserva gli esiti individuali. Lo stato aggregato è xsoiIntegrityVerified solo quando ogni firma ammessa verifica; altrimenti è il primo fallimento in ordine di inventario, e ogni report vi dice comunque quale parte firma è fallita

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);  // uno snapshot, verificato e analizzato
  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 non è un superamento
end;

I valori di stato sono abbastanza specifici da agire. xsoiDigestMismatch significa che una parte selezionata è cambiata dopo la firma, xsoiInvalidSignature significa che il valore RSA non verifica contro SignedInfo con la chiave del certificato incorporato, xsoiPartUnavailable significa che una parte selezionata manca, e xsoiUnsupportedProfile o xsoiResourceLimit significano che il verifier ha rifiutato invece di tirare a indovinare. Il layer ZIP sottostante applica la stessa lettura fail-closed descritta nell'articolo sulla validazione ZIP EOCD per file XLSX non fidati, e la ricerca delle parti segue le regole della risoluzione delle relationship OPC di XLSX

Perché modificare una cartella firmata invalida la firma?

Modificare una cartella firmata invalida la firma non appena un byte cambiato cade dentro una selezione firmata, perché il digest SHA-256 memorizzato per quel Reference non combacia più. Un writer spreadsheet che carica la cartella e la salva di nuovo tocca quasi sempre parti firmate: riserializza l'XML, rinumera le shared strings, riscrive workbook.xml e il manifest dei content type. Anche quando i valori delle celle sono identici, i byte non lo sono, e la verifica successiva restituisce xsoiDigestMismatch

HotXLS vi lascia porre la domanda prima di impegnarvi in una modifica. XlsEditVerifiedOpcPackage sostituisce parti esistenti in un pacchetto verificato e poi verifica il candidato. Con xsoePreserve restituisce xsoePreserved solo quando la firma verifica ancora, il che accade quando le parti modificate stanno fuori da ogni selezione firmata, e xsoeWouldInvalidate altrimenti. Con xsoeInvalidateChanged rimuove il legame di firma dalle relationship della radice e da [Content_Types].xml e restituisce un pacchetto esplicitamente non firmato come xsoeInvalidated, invece di consegnarvi un file che sembra ancora firmato

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 stava fuori dalla selezione firmata
    xsoeWouldInvalidate: Result := False;  // un digest firmato copre sheet2
  else
    raise Exception.CreateFmt('Edit refused, original integrity %d',
      [Ord(Details.OriginalIntegrity)]);
  end;
end;

Integrità contro trust: cosa prova una firma verificata?

Una firma verificata prova che le parti selezionate sono invariate da quando qualcuno in possesso della chiave privata le ha firmate, e nulla su chi sia quel qualcuno. HotXLS tiene i due livelli separati di proposito: ogni TXLSSignatureOpcIntegrityResult porta Trust = xsstUntrusted, controlli riusciti compresi, e ClaimedSignatureTime sono dati forniti dal firmatario, non un timestamp fidato. Chiunque può generare un certificato autofirmato, firmare una cartella di lavoro, e passare il controllo di integrità

HotXLS verifica le firme digitali del pacchetto OPC Excel in Delphi su due livelli: controlli di digest di integrità su uno snapshot di byte immutabile che riportano sempre xsstUntrusted, poi XlsVerifySignatureOpcPackageWithTrust con una policy del chiamante
Una firma verificata prova che le parti selezionate sono invariate, mentre il trust richiede i vostri anchor, un tempo di verifica UTC e l'EKU di firma documento prima che xsotTrustedSelection significhi qualcosa

Il trust è una chiamata separata con una policy del chiamante. XlsDefaultSignatureOpcTrustPolicy restituisce una policy fail-closed senza anchor, senza tempo di verifica, con l'EKU di firma documento 1.3.6.1.4.1.311.10.3.12 e revoca xsrvRequireCached. XlsVerifySignatureOpcPackageWithTrust esegue prima il verifier di integrità completo e poi valuta il certificato che effettivamente l'ha superato; un chiamante non può sostituire un certificato foglia diverso

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;

Il backend Windows ha due trappole facili da perdere quando chiamate WinCrypt da soli. CertVerifyCertificateChainPolicy che restituisce True significa solo che la chiamata è stata eseguita; l'accettazione richiede comunque uno dwError a zero nello stato della policy, zero errori aggregati di catena, un key usage digitalSignature esplicito sul foglia e l'EKU richiesto. HotXLS ha scoperto che un certificato sprovvisto di key usage può passare la policy base ordinaria, quindi controlla DigitalSignatureUsage e RequiredEkuPresent da sé. La seconda trappola: combinare un root store esclusivo con hRestrictedOther nella configurazione del chain engine restituisce E_INVALIDARG sul backend attuale, così HotXLS usa lo store radice esclusivo del chiamante e poi autorizza ogni elemento della catena primaria restituita contro i byte DER che avete fornito. Un emittente che capita semplicemente di stare in uno store di sistema locale non diventa mai fidato

xsotTrustedSelection descrive comunque solo quella firma e la copertura che riporta. Celle non firmate e relationship non selezionate possono cambiare mentre la firma selezionata resta fidata, ed è per questo che il risultato di trust restituisce anche PackageSha256 e CertificateSha256 per il vostro log di audit

Come si rifirma o controfirma una cartella di lavoro senza Excel?

Al livello di array di byte HotXLS rifirma attraverso transazioni esplicite e autorizzate, e i percorsi di salvataggio firmato del Workbook riusano le stesse pipeline verificate. XlsReplaceVerifiedSignatureOpcPackage scambia una firma verificata con una nuova, XlsEditAndReplaceSelectedSignatureOpcPackage applica modifiche a parti e sostituisce una firma selezionata nella stessa transazione, e XlsAppendVerifiedSignatureOpcPackage aggiunge una firma conservando ogni originale verificato byte per byte. Le chiamate di replace prendono un TXLSSignatureOpcReplaceAuthorization, la chiamata di append verifica ogni originale prima, e tutte prendono una selezione TXLSSignatureOpcIssueOptions e una callback firmataria; la libreria non tocca mai la vostra chiave privata

Il record di autorizzazione lega l'operazione all'esatto originale che avete approvato: ExpectedPackageSha256, ExpectedSignaturePart e ExpectedCertificateSha256, tutti confrontati solo dopo che la vecchia firma ha verificato completamente. Ogni parte modificata deve essere coperta dalla nuova selezione, ogni firma non selezionata deve ancora verificare contro il pacchetto in scena prima che il firmatario venga chiamato, e la chiamata restituisce xspsWouldInvalidate altrimenti

HotXLS rifirma un pacchetto XLSX Excel in Delphi attraverso XlsEditAndReplaceSelectedSignatureOpcPackage, che verifica la vecchia firma, controlla gli hash attesi di pacchetto, parte e certificato, e chiama una callback firmataria
Il record di autorizzazione lega il replace all'originale approvato, le firme non selezionate devono ancora verificare sul pacchetto in scena, e la chiave privata resta dentro il vostro CNG, HSM o smart card
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 è lo SHA-256 di 32 byte calcolato dalla libreria;
  // RsaSignSha256Pkcs1 sta per il vostro wrapper CNG, HSM o smart card e
  // deve restituire la firma RSASSA-PKCS1-v1_5 completa 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); // o l'hash memorizzato all'approvazione
  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;

Due dettagli in quel codice sono deliberati. Output e Details devono essere variabili separate; passare un campo di Details come Output è esplicitamente non supportato, perché la libreria non può pubblicare un pacchetto e conservare un report di certificato nello stesso storage. E la vecchia copertura non si eredita: la nuova firma autentica esattamente i Reference che elencate, quindi decidete di nuovo su quali parti farà affidamento un consumatore a valle

La controfirma usa la stessa chiamata di append. Da HotXLS 2.384.296, TXLSSignatureOpcIssueOptions.References passato a XlsAppendVerifiedSignatureOpcPackage può nominare parti dall'inventario verificato delle firme originali, così un revisore può firmare sopra una parte firma esistente, incatenare parecchie controfirme, o mischiare parti firma e parti contenuto in una sola selezione. Ogni firma aggiunta registra solo le proprie selezioni dirette. La qualifica per quella release ha eseguito 222 controlli al bordo e 122 controlli indipendenti con il verifier di packaging .NET WindowsBase su 7 pacchetti di output, confermando esattamente una nuova firma e firme e certificati originali byte-identici

Insidie e limiti da conoscere

Il bug più istruttivo è venuto da un carattere perfettamente legale. Gli URI dei Reference OPC portano il content type come query, per esempio /xl/worksheets/sheet1.xml?ContentType=application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml. Un emittente HotXLS precoce codificava in percentuale la + letterale come %2B. I controlli RSA, C14N e Reference dello stesso HotXLS accettavano il risultato, ma WindowsBase riportava InvalidSignature; un campione controllato che cambiava solo quell'ortografia passava da InvalidSignature a Success. L'emittente ora conserva la punteggiatura di query ASCII letterale supportata continuando comunque a fare l'escape degli attributi XML, e la lezione per qualunque firmatario è di testare contro un verifier nativo indipendente, non solo il proprio

  • Il backend di trust è solo Windows; gli altri target restituiscono xsotTrustUnavailable dopo che l'integrità è riuscita
  • Il verifier accetta pacchetti fino a 64 MiB, 16 MiB per parte, e al massimo 32 parti firma sotto un'origine
  • L'emissione accetta fino a 256 Reference e 4096 selettori combinati per firma
  • Il profilo supportato è RSA-SHA256, digest SHA-256 e Canonical XML senza commenti; altri algoritmi, Object applicativi extra e Reference esterni restituiscono xsoiUnsupportedProfile
  • xsrvRequireCached usa dati di revoca solo da cache; uno stato mancante rifiuta, e xsrvNotChecked non dà alcuna garanzia di revoca
  • Firmare una cartella che Excel deve pure accettare incappa ancora nelle regole del pacchetto trattate in perché Excel ripara un XLSX valido; un Override duplicato o un id di relationship duplicato rompe il file prima che una firma venga controllata

FAQ

Un controllo di integrità HotXLS significa che il firmatario è fidato?

No. XlsVerifySignatureOpcPackage prova che le parti selezionate combaciano con i loro digest sotto la chiave del certificato incorporato e riporta sempre xsstUntrusted. Usate XlsVerifySignatureOpcPackageWithTrust con i vostri anchor e un tempo di verifica UTC per valutare la catena di certificati

Perché Excel mostra la firma della mia cartella come invalida dopo una modifica Delphi?

Una cartella salvata riscrive le parti XML firmate, quindi il digest SHA-256 di almeno un Reference cambia. Usate XlsEditVerifiedOpcPackage con xsoePreserve per provare prima una modifica, o XlsEditAndReplaceSelectedSignatureOpcPackage per modificare e rifirmare in una sola transazione verificata

HotXLS può firmare con una chiave su smart card o HSM?

Sì, perché la firma è una callback del chiamante. La libreria passa il SignedInfo canonico e il suo digest SHA-256 di 32 byte, il vostro codice restituisce la firma RSASSA-PKCS1-v1_5, e la chiave privata non lascia mai il provider che controllate

Verifica delle firme OPC, valutazione del trust, controfirma e rifirma verificata fanno parte di HotXLS, il componente foglio di calcolo Excel nativo per Delphi e C++Builder, che legge e scrive XLS, XLSX, XLSB e ODS senza Excel né automazione COM