Artigo Técnico

Verifique e reassine assinaturas digitais do Excel em Delphi

O HotXLS verifica e reassina assinaturas digitais do Excel em Delphi sem o Excel instalado. A unit lxSignatureOpcIntegrity confere assinaturas de pacote do ECMA-376 Part 2 diretamente dos bytes da pasta de trabalho: RSA-SHA256 sobre o SignedInfo canônico, todo digest SHA-256 de Reference, 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 verifica de novo

O gatilho habitual é um fluxo de documentos que cresceu além do desktop. Um controlador assina uma pasta de orçamento no Excel, um serviço Delphi depois precisa confirmar a assinatura antes de importar os números, e um terceiro passo precisa corrigir uma planilha e assinar de novo sob um certificado de serviço. Automatizar o Excel em um 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: a que bytes o signatário se comprometeu, e se você confia na pessoa que segura aquela chave

O que uma assinatura de pacote OPC realmente cobre?

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

O HotXLS suporta três formas de uma Reference atar uma parte, e elas não são intercambiáveis. Uma Reference raw faz o hash dos bytes armazenados da parte. Uma Reference XML canônico faz o hash da forma Canonical XML (sem comentários), então a ordem de atributos e a grafia de declarações de namespace deixam de importar. Uma Reference de relationship aplica primeiro a OPC RelationshipTransform e então canoniza, e essa transformação mantém apenas as relationships casadas por seus seletores SourceId ou SourceType. Um seletor autentica exatamente as relationships que seleciona: o resto dessa parte .rels pode mudar livremente enquanto a assinatura permanece válida

Anatomia de assinatura digital OPC do HotXLS para Delphi: uma parte origin.sigs leva a partes de assinatura XML-DSig cujo Manifest ata cada parte do pacote com uma referência raw, XML canônico ou RelationshipTransform
Os três modos de Reference não são intercambiáveis: raw hasheia bytes armazenados, XML canônico ignora a grafia de atributos, e seletores de relationship deixam as entradas .rels não selecionadas livres para mudar

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

Verificando assinaturas a partir de um snapshot ZIP imutável

O HotXLS verifica um pacote a partir de um único snapshot de bytes imutável, nunca do arquivo no disco ou de um TWorkbook vivo. O XlsVerifySignatureOpcPackage e seus irmãos de múltiplas assinaturas recebem um array TBytes, leem apenas esse array, e nunca buscam alvos externos, certificados, arquivos locais ou recursos de rede. O ponto é que os bytes que você verificou são os bytes que você analisa depois: se o arquivo for relido mais tarde, outro processo pode tê-lo substituído no meio do caminho

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

Para pacotes que podem carregar mais de uma assinatura, o XlsVerifyAllSignaturesOpcPackage verifica toda assinatura anexada e guarda os resultados individuais. O status agregado é xsoiIntegrityVerified apenas quando toda assinatura admitida verifica; caso contrário é a primeira falha na ordem do inventário, e cada relatório ainda diz qual 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);  // um snapshot, verificado e analisado
  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 é um passe
end;

Os valores de status são específicos o bastante para agir sobre eles. xsoiDigestMismatch significa que uma parte selecionada mudou depois da assinatura, xsoiInvalidSignature significa que o valor RSA não verifica contra o SignedInfo com a chave do certificado embutido, xsoiPartUnavailable significa que uma parte selecionada está ausente, e xsoiUnsupportedProfile ou xsoiResourceLimit significam que o verificador recusou em vez de chutar. A camada ZIP por baixo aplica a mesma leitura fail-closed descrita no artigo sobre validação de EOCD ZIP para arquivos XLSX não confiáveis, e a busca de partes segue as regras da resolução de relationships OPC do XLSX

Por que editar uma pasta assinada invalida a assinatura?

Editar uma pasta assinada invalida a assinatura assim que um byte alterado cai dentro de uma seleção assinada, porque o digest SHA-256 armazenado para aquela Reference não casa mais. Um writer de planilhas que carrega a pasta e salva de novo quase sempre toca partes assinadas: ele resserializa XML, renumera shared strings, regrava o workbook.xml e o manifest de content types. Mesmo quando os valores das células são idênticos, os bytes não são, e a próxima verificação devolve xsoiDigestMismatch

O HotXLS deixa você fazer a pergunta antes de se comprometer com uma edição. O XlsEditVerifiedOpcPackage substitui partes existentes em um pacote verificado e então verifica o candidato. Com xsoePreserve ele devolve xsoePreserved apenas quando a assinatura ainda verifica, o que acontece quando as partes editadas ficam fora de toda seleção assinada, e xsoeWouldInvalidate caso contrário. Com xsoeInvalidateChanged ele remove a ligação de assinatura das relationships raiz e do [Content_Types].xml e devolve um pacote explicitamente sem assinatura como xsoeInvalidated, em vez de entregar um arquivo 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 o sheet2
  else
    raise Exception.CreateFmt('Edit refused, original integrity %d',
      [Ord(Details.OriginalIntegrity)]);
  end;
end;

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

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 TXLSSignatureOpcIntegrityResult carrega Trust = xsstUntrusted, incluindo checagens bem-sucedidas, e o ClaimedSignatureTime é dado fornecido pelo signatário, não um timestamp confiável. Qualquer um pode gerar um certificado autoassinado, assinar uma pasta, e passar na checagem de integridade

O HotXLS verifica assinaturas digitais de pacote OPC do Excel em Delphi em duas camadas: checagens de digest de integridade sobre um snapshot de bytes imutável que sempre reportam xsstUntrusted, então XlsVerifySignatureOpcPackageWithTrust com uma política de propriedade do chamador
Uma assinatura verificada prova que as partes selecionadas estão inalteradas, enquanto confiança exige seus próprios anchors, um horário de verificação UTC e o EKU de assinatura de documento antes de xsotTrustedSelection significar algo

Confiança é uma chamada separada com uma política de propriedade do chamador. O XlsDefaultSignatureOpcTrustPolicy devolve uma política fail-closed sem anchors, sem horário de verificação, com o EKU de assinatura de documento 1.3.6.1.4.1.311.10.3.12 e revogação xsrvRequireCached. O XlsVerifySignatureOpcPackageWithTrust primeiro roda o verificador de integridade completo e então avalia o certificado que de fato o passou; um chamador não pode substituir por um certificado leaf 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 você chama o WinCrypt você mesmo. O CertVerifyCertificateChainPolicy devolvendo True só significa que a chamada executou; a aceitação ainda exige um dwError zero no status da política, zero erros agregados de cadeia, um key usage digitalSignature explícito no leaf e o EKU exigido. O HotXLS descobriu que um certificado sem key usage pode passar na política base comum, então ele confere DigitalSignatureUsage e RequiredEkuPresent por conta própria. A segunda armadilha: combinar um root store exclusivo com hRestrictedOther na configuração do chain engine devolve E_INVALIDARG no backend atual, então o HotXLS usa o root store exclusivo do chamador e então autoriza cada elemento da cadeia primária devolvida contra os bytes DER que você forneceu. Um issuer que meramente por acaso senta em um store de sistema local nunca se torna confiável

O xsotTrustedSelection ainda descreve apenas aquela assinatura e a cobertura que ela reporta. Células não assinadas e relationships não selecionadas podem mudar enquanto a assinatura selecionada permanece confiável, e é por isso que o resultado de confiança também devolve PackageSha256 e CertificateSha256 para seu log de auditoria

Como reassinar ou contrassinar uma pasta sem o Excel?

No nível de array de bytes, o HotXLS reassina por meio de transações explícitas e autorizadas, e os caminhos de signed-save da 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 adiciona uma assinatura mantendo todo original verificado byte a byte. As chamadas de replace recebem um TXLSSignatureOpcReplaceAuthorization, a chamada de append verifica todo original primeiro, e todas recebem uma seleção TXLSSignatureOpcIssueOptions e um callback de signatário; a biblioteca nunca toca sua chave privada

O registro de autorização ata a operação ao exato original que você aprovou: ExpectedPackageSha256, ExpectedSignaturePart e ExpectedCertificateSha256, todos comparados apenas depois de a assinatura antiga ter verificado por completo. Toda parte editada precisa estar coberta pela nova seleção, toda assinatura não selecionada precisa ainda verificar contra o pacote preparado antes de o signatário ser chamado, e a chamada devolve xspsWouldInvalidate caso contrário

O HotXLS reassina um pacote XLSX do Excel em Delphi por meio do XlsEditAndReplaceSelectedSignatureOpcPackage, que verifica a assinatura antiga, confere os hashes esperados de pacote, parte e certificado, e chama um callback de signatário
O registro de autorização ata o replace ao original aprovado, assinaturas não selecionadas precisam ainda verificar no pacote preparado, 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 computou;
  // RsaSignSha256Pkcs1 representa seu próprio wrapper CNG, HSM ou smart card e
  // precisa 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 armazenado 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 daquele código são deliberados. Output e Details precisam 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 você listar, então decida de novo em que partes um consumidor a jusante vai se apoiar

Contrassinar usa a mesma chamada de append. Desde o HotXLS 2.384.296, o TXLSSignatureOpcIssueOptions.References passado ao XlsAppendVerifiedSignatureOpcPackage pode nomear partes do inventário de assinaturas originais verificado, então um revisor pode assinar sobre uma parte de assinatura existente, encadear várias contrassinaturas, ou misturar partes de assinatura e partes de conteúdo em uma seleção. Cada assinatura anexada registra apenas suas seleções diretas. A qualificação daquela versão rodou 222 checagens de fronteira e 122 checagens independentes com o verificador de packaging do .NET WindowsBase em 7 pacotes de saída, confirmando exatamente uma assinatura nova e assinaturas e certificados originais byte-idênticos

Armadilhas e limites que valem conhecer

O bug mais instrutivo veio de um caractere perfeitamente legal. URIs de Reference OPC carregam o content type como query, por exemplo /xl/worksheets/sheet1.xml?ContentType=application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml. Um issuer antigo do HotXLS fazia percent-encoding do + literal como %2B. As próprias checagens de RSA, C14N e Reference do HotXLS aceitavam o resultado, mas o WindowsBase reportava InvalidSignature; uma amostra controlada que mudava apenas aquela grafia ia de InvalidSignature para Success. O issuer agora mantém pontuação ASCII literal de query suportada enquanto ainda escapa atributos XML, e a lição para qualquer signatário é testar contra um verificador nativo independente, não apenas o seu

  • O backend de confiança é apenas Windows; outros alvos devolvem xsotTrustUnavailable depois de a integridade ter sucesso
  • O verificador aceita pacotes de 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 extras e References externas devolvem xsoiUnsupportedProfile
  • O xsrvRequireCached usa dados de revogação apenas de cache; status ausente rejeita, e o xsrvNotChecked não dá nenhuma garantia de revogação
  • Assinar uma pasta que o Excel também precisa aceitar ainda esbarra nas regras de pacote de por que o Excel repara um XLSX válido; um Override duplicado ou um Id de relationship quebra o arquivo antes de qualquer assinatura ser conferida

FAQ

Uma checagem de integridade do HotXLS significa que o signatário é confiável?

Não. O XlsVerifySignatureOpcPackage prova que as partes selecionadas casam com seus digests sob a chave do certificado embutido e sempre reporta xsstUntrusted. Use o XlsVerifySignatureOpcPackageWithTrust com seus próprios anchors e um horário de verificação UTC para avaliar a cadeia de certificados

Por que o Excel mostra a assinatura da minha pasta como inválida após uma edição em Delphi?

Uma pasta salva regrava partes XML assinadas, então o digest SHA-256 de ao menos uma Reference muda. Use o XlsEditVerifiedOpcPackage com xsoePreserve para testar uma edição primeiro, ou o XlsEditAndReplaceSelectedSignatureOpcPackage para editar e reassinar em uma transação verificada

O HotXLS assina com uma chave guardada em smart card ou HSM?

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

Verificação de assinaturas OPC, avaliação de confiança, contrassinatura e reassinatura verificada fazem parte do HotXLS, o componente de planilhas Excel nativo para Delphi e C++Builder, que lê e grava XLS, XLSX, XLSB e ODS sem Excel nem automação COM