Artigo Técnico

Verificar e reassinar assinaturas digitais Excel no Delphi

O HotXLS verifica e reassina assinaturas digitais Excel no Delphi sem Excel instalado. A unidade lxSignatureOpcIntegrity verifica assinaturas de pacote ECMA-376 Parte 2 diretamente a partir dos bytes do workbook: RSA-SHA256 sobre o SignedInfo canónico, cada digest de Reference SHA-256, e as partes exatas que cada assinatura seleciona. A confiança na cadeia de certificados é um passo separado e explícito, e editar ou reassinar só publica saída que volta a verificar

O gatilho habitual é um fluxo de documentos que ultrapassou o desktop. Um controlador assina um workbook de orçamento no Excel, um serviço Delphi depois tem de confirmar a assinatura antes de importar os números, e um terceiro passo precisa de corrigir uma folha e assinar de novo sob um certificado de serviço. Automatizar o Excel num servidor para isso é frágil e não suportado, e tratar a assinatura como uma flag sim/não esconde as duas perguntas que realmente importam: que bytes o assinante comprometeu, e se confia na pessoa que detém essa chave

O que cobre afinal uma assinatura de pacote OPC?

Uma assinatura de pacote OPC cobre só as partes que o seu Manifest lista, não o ficheiro XLSX inteiro. A ECMA-376 Parte 2, §13 (Digital Signatures) coloca uma parte de origem de assinatura no pacote, tipicamente /_xmlsignatures/origin.sigs, com relações para uma ou mais partes de assinatura como /_xmlsignatures/sig1.xml. Cada parte de assinatura é uma Signature W3C XML-DSig cujo SignedInfo referencia um idPackageObject interno, e esse Object transporta um Manifest de References para partes individuais do pacote, cada uma com o seu próprio digest

O HotXLS suporta três formas de uma Reference poder vincular uma parte, e não são intercambiáveis. Uma Reference bruta faz hash dos bytes guardados da parte. Uma Reference de XML canónico faz hash da forma Canonical XML (sem comentários), pelo que a ordem de atributos e a grafia das declarações de namespaces já não importam. Uma Reference de relação aplica primeiro a OPC RelationshipTransform e depois canoniza, e essa transformação mantém só as relações correspondidas pelos seus seletores SourceId ou SourceType. Um seletor autentica exatamente as relações que seleciona: o resto dessa parte .rels pode mudar livremente enquanto a assinatura permanece válida

Anatomia da assinatura digital OPC do HotXLS para Delphi: uma parte origin.sigs conduz a partes de assinatura XML-DSig cujo Manifest vincula cada parte do pacote com uma referência raw, canonical XML ou RelationshipTransform
Os três modos de Reference não são intercambiáveis: raw faz hash dos bytes guardados, canonical XML ignora a grafia dos atributos, e os seletores de relação deixam as entradas .rels não selecionadas livres para mudar

Esse último ponto é por isso que o HotXLS reporta cobertura em vez de um único visto verde. O TXLSSignatureOpcIntegrityResult.Coverage é uma matriz de registos TXLSSignatureOpcCoverage, um por Manifest Reference autenticada, cada um com ReferenceUri, PartName, ContentType, Transforms, RelationshipSourceIds e RelationshipSourceTypes. Se a sua importação lê sheet3.xml e nenhum descritor o nomeia, a assinatura não diz nada sobre os números que está prestes a carregar

Verificar assinaturas a partir de uma fotografia ZIP imutável

O HotXLS verifica um pacote a partir de uma única fotografia de bytes imutável, nunca a partir do ficheiro em disco ou de um TWorkbook em vivo. O XlsVerifySignatureOpcPackage e os seus irmãos multi-assinatura recebem uma matriz TBytes, leem só essa matriz, e nunca vão buscar alvos externos, certificados, ficheiros locais ou recursos de rede. O ponto é que os bytes que verificou são os bytes que analisa depois: se o ficheiro for relido mais tarde, outro processo pode tê-lo substituído entretanto

A canonização é onde os verificadores caseiros costumam errar. O SignedInfo e o Object de pacote têm de ser canonizados no contexto de namespaces do envelope Signature completo. Um fragmento isolado cortado do documento herda declarações de namespaces diferentes das que o verificador vê no lugar, pelo que os seus bytes C14N diferem, o digest difere, e uma assinatura perfeitamente válida falha. O HotXLS usa o contexto de envelope completo tanto do lado da verificação como do da emissão

Para pacotes que possam transportar mais de uma assinatura, o XlsVerifyAllSignaturesOpcPackage verifica todas as assinaturas anexadas e mantém os resultados individuais. O estado agregado é xsoiIntegrityVerified só quando todas as assinaturas admitidas verificam; caso contrário é a primeira falha pela ordem do inventário, e cada relatório continua a dizer-lhe que parte de assinatura falhou

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);  // uma fotografia, verificada e analisada
  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 não é uma passagem
end;

Os valores de estado são específicos o suficiente para agir. xsoiDigestMismatch significa que uma parte selecionada mudou depois de assinar, xsoiInvalidSignature significa que o valor RSA não verifica contra o SignedInfo com a chave do certificado incorporado, xsoiPartUnavailable significa que falta uma parte selecionada, e xsoiUnsupportedProfile ou xsoiResourceLimit significam que o verificador recusou em vez de adivinhar. A camada ZIP por baixo aplica a mesma leitura fail-closed descrita no artigo sobre validação de EOCD ZIP para ficheiros XLSX não fidedignos, e a procura de partes segue as regras da resolução de relações OPC XLSX

Por que é que editar um workbook assinado invalida a assinatura?

Editar um workbook assinado invalida a assinatura assim que um byte alterado cai dentro de uma seleção assinada, porque o digest SHA-256 guardado para essa Reference já não coincide. Um writer de folhas de cálculo que carregue o workbook e o volte a gravar quase sempre toca em partes assinadas: resserializa XML, renumera strings partilhadas, reescreve o workbook.xml e o manifest de tipos de conteúdo. Mesmo quando os valores das células são idênticos, os bytes não são, e a verificação seguinte devolve xsoiDigestMismatch

O HotXLS deixa-o fazer a pergunta antes de se comprometer com uma edição. O XlsEditVerifiedOpcPackage substitui partes existentes num pacote verificado e depois verifica o candidato. Com xsoePreserve devolve xsoePreserved só quando a assinatura ainda verifica, o que acontece quando as partes editadas ficam fora de todas as seleções assinadas, e xsoeWouldInvalidate caso contrário. Com xsoeInvalidateChanged remove a vinculação de assinatura das relações da raiz e do [Content_Types].xml e devolve um pacote explicitamente não assinado como xsoeInvalidated, em vez de lhe entregar um ficheiro que ainda parece assinado

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 estava fora da seleção assinada
    xsoeWouldInvalidate: Result := False;  // um digest assinado cobre sheet2
  else
    raise Exception.CreateFmt('Edit refused, original integrity %d',
      [Ord(Details.OriginalIntegrity)]);
  end;
end;

Integridade versus confiança: o que prova uma assinatura verificada?

Uma assinatura verificada prova que as partes selecionadas estão inalteradas desde que alguém com a chave privada as assinou, e nada sobre quem é esse alguém. O HotXLS mantém as duas camadas separadas de propósito: todo o TXLSSignatureOpcIntegrityResult transporta Trust = xsstUntrusted, incluindo verificações bem-sucedidas, e o ClaimedSignatureTime é dados fornecidos pelo assinante, não um selo de tempo fidedigno. Qualquer um pode gerar um certificado autoassinado, assinar um workbook, e passar a verificação de integridade

O HotXLS verifica assinaturas digitais de pacote OPC Excel no Delphi em duas camadas: verificações de digest de integridade sobre uma fotografia de bytes imutável que reportam sempre xsstUntrusted, depois XlsVerifySignatureOpcPackageWithTrust com uma política propriedade do chamador
Uma assinatura verificada prova que as partes selecionadas estão inalteradas, enquanto a confiança exige os seus próprios âncoras, um tempo de verificação UTC e o EKU de assinatura de documentos antes de xsotTrustedSelection significar alguma coisa

A confiança é uma chamada separada com uma política propriedade do chamador. O XlsDefaultSignatureOpcTrustPolicy devolve uma política fail-closed sem âncoras, sem tempo de verificação, com o EKU de assinatura de documentos 1.3.6.1.4.1.311.10.3.12 e revogação xsrvRequireCached. O XlsVerifySignatureOpcPackageWithTrust primeiro corre o verificador de integridade completo e depois avalia o certificado que realmente o passou; um chamador não pode substituir um certificado folha diferente

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;

O backend Windows tem duas armadilhas fáceis de perder quando chama o WinCrypt você mesmo. O CertVerifyCertificateChainPolicy devolver True só significa que a chamada executou; a aceitação ainda exige um dwError zero no estado da política, erros agregados de cadeia zero, um uso de chave digitalSignature explícito na folha e o EKU exigido. O HotXLS descobriu que um certificado sem uso de chave pode passar a política base ordinária, por isso verifica DigitalSignatureUsage e RequiredEkuPresent ele próprio. A segunda armadilha: combinar um arquivo de raízes exclusivo com hRestrictedOther na configuração do motor de cadeias devolve E_INVALIDARG no backend atual, pelo que o HotXLS usa o arquivo de raízes exclusivo do chamador e depois autoriza cada elemento da cadeia primária devolvida contra os bytes DER que forneceu. Um emissor que por acaso se encontre num arquivo do sistema local nunca se torna fidedigno

O xsotTrustedSelection continua a descrever só essa assinatura e a cobertura que reporta. Células não assinadas e relações não selecionadas podem mudar enquanto a assinatura selecionada permanece fidedigna, razão pela qual o resultado de confiança também devolve PackageSha256 e CertificateSha256 para o seu log de auditoria

Como reassinar ou contra-assinar um workbook sem Excel?

Ao nível da matriz de bytes o HotXLS reassina através de transações explícitas e autorizadas, e os caminhos de gravação assinada do Workbook reutilizam os mesmos pipelines verificados. O XlsReplaceVerifiedSignatureOpcPackage troca uma assinatura verificada por uma nova, o XlsEditAndReplaceSelectedSignatureOpcPackage aplica edições de partes e substitui uma assinatura selecionada na mesma transação, e o XlsAppendVerifiedSignatureOpcPackage acrescenta uma assinatura mantendo todos os originais verificados byte a byte. As chamadas de substituição recebem um TXLSSignatureOpcReplaceAuthorization, a chamada de acréscimo verifica todos os originais primeiro, e todas recebem uma seleção TXLSSignatureOpcIssueOptions e um callback de assinante; a biblioteca nunca toca na sua chave privada

O registo de autorização vincula a operação ao exato original que aprovou: ExpectedPackageSha256, ExpectedSignaturePart e ExpectedCertificateSha256, todos comparados só depois de a assinatura antiga ter verificado por completo. Toda a parte editada tem de estar coberta pela nova seleção, toda assinatura não selecionada tem de continuar a verificar contra o pacote em estágio antes de o assinante ser chamado, e a chamada devolve xspsWouldInvalidate caso contrário

O HotXLS reassina um pacote XLSX Excel no Delphi através do XlsEditAndReplaceSelectedSignatureOpcPackage, que verifica a assinatura antiga, confere os hashes esperados de pacote, parte e certificado, e chama um callback de assinante
O registo de autorização vincula a substituição ao original aprovado, as assinaturas não selecionadas têm de continuar a verificar no pacote em estágio, e a chave privada fica dentro do seu CNG, HSM ou 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 é o SHA-256 de 32 bytes que a biblioteca calculou;
  // RsaSignSha256Pkcs1 representa o seu próprio invólucro CNG, HSM ou smart card e
  // tem de devolver a assinatura RSASSA-PKCS1-v1_5 big-endian completa
  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 o hash guardado na aprovação
  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;

Dois detalhes nesse código são deliberados. Output e Details têm de ser variáveis separadas; passar um campo de Details como Output é explicitamente não suportado, porque a biblioteca não pode publicar um pacote e guardar um relatório de certificado no mesmo armazenamento. E a cobertura antiga não é herdada: a nova assinatura autentica exatamente as References que lista, por isso decida de novo em que partes um consumidor a jusante vai confiar

A contra-assinatura usa a mesma chamada de acréscimo. Desde o HotXLS 2.384.296 que as TXLSSignatureOpcIssueOptions.References passadas ao XlsAppendVerifiedSignatureOpcPackage podem nomear partes do inventário de assinaturas originais verificado, pelo que um revisor pode assinar sobre uma parte de assinatura existente, encadear várias contra-assinaturas, ou misturar partes de assinatura e partes de conteúdo numa única seleção. Cada assinatura acrescentada regista só as suas seleções diretas. A qualificação dessa versão correu 222 verificações de fronteira e 122 verificações independentes com o verificador de empacotamento .NET WindowsBase através de 7 pacotes de saída, confirmando exatamente uma assinatura nova e assinaturas e certificados originais byte-idênticos

Armadilhas e limites que valem a pena conhecer

O bug mais instrutivo veio de um carácter perfeitamente legal. Os URIs de Reference OPC transportam o tipo de conteúdo como uma query, por exemplo /xl/worksheets/sheet1.xml?ContentType=application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml. Um emissor inicial do HotXLS codificava em percentagem o + literal como %2B. As próprias verificações de RSA, C14N e Reference do HotXLS aceitavam o resultado, mas o WindowsBase reportava InvalidSignature; uma amostra controlada que mudava só essa grafia passava de InvalidSignature para Success. O emissor agora mantém a pontuação literal ASCII de query suportada enquanto continua a escapar atributos XML, e a lição para qualquer assinante é testar contra um verificador nativo independente, não só contra o seu

  • O backend de confiança é só Windows; outros alvos devolvem xsotTrustUnavailable depois de a integridade ter sucesso
  • O verificador aceita pacotes até 64 MiB, 16 MiB por parte, e no máximo 32 partes de assinatura sob uma origem
  • A emissão aceita até 256 References e 4096 seletores combinados por assinatura
  • O perfil suportado é RSA-SHA256, digests SHA-256 e Canonical XML sem comentários; outros algoritmos, Objects de aplicação extra e References externas devolvem xsoiUnsupportedProfile
  • O xsrvRequireCached usa dados de revogação só de cache; estado em falta rejeita, e o xsrvNotChecked não dá qualquer garantia de revogação
  • Assinar um workbook que o Excel também tem de aceitar continua a esbarrar nas regras de pacote em por que o Excel repara um XLSX válido; um Override duplicado ou um Id de relação quebra o ficheiro antes de qualquer assinatura ser verificada

FAQ

Uma verificação de integridade do HotXLS significa que o assinante é fidedigno?

Não. O XlsVerifySignatureOpcPackage prova que as partes selecionadas coincidem com os seus digests sob a chave do certificado incorporado e reporta sempre xsstUntrusted. Use o XlsVerifySignatureOpcPackageWithTrust com os seus próprios âncoras e um tempo de verificação UTC para avaliar a cadeia de certificados

Por que o Excel mostra a assinatura do meu workbook como inválida depois de uma edição Delphi?

Um workbook gravado reescreve partes XML assinadas, pelo que o digest SHA-256 de pelo menos uma Reference muda. Use o XlsEditVerifiedOpcPackage com xsoePreserve para testar uma edição primeiro, ou o XlsEditAndReplaceSelectedSignatureOpcPackage para editar e reassinar numa transação verificada

Pode o HotXLS assinar com uma chave guardada num smart card ou HSM?

Sim, porque assinar é um callback do chamador. A biblioteca passa o SignedInfo canónico e o seu digest SHA-256 de 32 bytes, o seu código devolve a assinatura RSASSA-PKCS1-v1_5, e a chave privada nunca sai do fornecedor que controla

Verificação de assinaturas OPC, avaliação de confiança, contra-assinatura e reassinatura verificada fazem parte do HotXLS, o componente nativo de folhas de cálculo Excel para Delphi e C++Builder, que lê e escreve XLS, XLSX, XLSB e ODS sem Excel nem automação COM