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
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
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
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
xsotTrustUnavailabledepois 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
xsrvRequireCachedusa dados de revogação só de cache; estado em falta rejeita, e oxsrvNotCheckednã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