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
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
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
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
xsotTrustUnavailabledepois 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
xsrvRequireCachedusa dados de revogação apenas de cache; status ausente rejeita, e oxsrvNotCheckednã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