Artículo técnico

Firmas digitales de Excel en Delphi: verificar y refirmar

HotXLS verifica y vuelve a firmar firmas digitales de Excel en Delphi sin Excel instalado. La unidad lxSignatureOpcIntegrity revisa firmas de paquete de ECMA-376 Part 2 directamente desde los bytes del libro: RSA-SHA256 sobre el SignedInfo canónico, cada digest SHA-256 de las References, y las partes exactas que cada firma selecciona. La confianza de la cadena de certificados es un paso separado y explícito, y editar o volver a firmar solo publica salida que vuelve a verificar

El disparador habitual es un flujo de documentos que se quedó chico para el escritorio. Un controlador firma un libro de presupuesto en Excel, un servicio Delphi después tiene que confirmar la firma antes de importar los números, y un tercer paso necesita corregir una hoja y volver a firmar bajo un certificado de servicio. Automatizar Excel en un servidor para eso es frágil y no soportado, y tratar la firma como una bandera sí/no esconde las dos preguntas que realmente importan: a qué bytes se comprometió el firmante, y si usted confía en la persona que tiene esa clave

¿Qué cubre realmente una firma de paquete OPC?

Una firma de paquete OPC cubre solo las partes que su Manifest lista, no el archivo XLSX entero. ECMA-376 Part 2, §13 (Digital Signatures) coloca una parte de origen de firma en el paquete, típicamente /_xmlsignatures/origin.sigs, con relaciones a una o más partes de firma como /_xmlsignatures/sig1.xml. Cada parte de firma es una Signature XML-DSig del W3C cuyo SignedInfo referencia un idPackageObject interno, y ese Object lleva un Manifest de References a partes individuales del paquete, cada una con su propio digest

HotXLS soporta tres formas en que una Reference puede atar una parte, y no son intercambiables. Una Reference cruda aplica hash a los bytes guardados de la parte. Una Reference de XML canónico aplica hash a la forma Canonical XML (sin comentarios), así que el orden de atributos y la grafía de las declaraciones de namespace dejan de importar. Una Reference de relación aplica primero la RelationshipTransform de OPC y luego canoniza, y esa transformación conserva solo las relaciones que sus selectores SourceId o SourceType emparejan. Un selector autentica exactamente las relaciones que selecciona: el resto de esa parte .rels puede cambiar libremente mientras la firma siga válida

Anatomía de la firma digital OPC de HotXLS para Delphi: una parte origin.sigs lleva a partes de firma XML-DSig cuyo Manifest ata cada parte del paquete con una referencia cruda, de XML canónico o de RelationshipTransform
Los tres modos de Reference no son intercambiables: la cruda aplica hash a los bytes guardados, el XML canónico ignora la grafía de atributos, y los selectores de relación dejan libres a cambiar las entradas .rels no seleccionadas

Ese último punto es la razón por la que HotXLS reporta cobertura en lugar de una única marca verde. TXLSSignatureOpcIntegrityResult.Coverage es un arreglo de registros TXLSSignatureOpcCoverage, uno por cada Manifest Reference autenticada, cada uno con ReferenceUri, PartName, ContentType, Transforms, RelationshipSourceIds y RelationshipSourceTypes. Si su importación lee sheet3.xml y ningún descriptor lo nombra, la firma no dice nada sobre los números que está por cargar

Verificar firmas desde una instantánea ZIP inmutable

HotXLS verifica un paquete desde una única instantánea de bytes inmutable, nunca desde el archivo en disco o un TWorkbook vivo. XlsVerifySignatureOpcPackage y sus hermanos de firma múltiple toman un arreglo TBytes, leen solo ese arreglo, y nunca buscan destinos externos, certificados, archivos locales o recursos de red. La gracia es que los bytes que usted verificó son los bytes que parsea después: si el archivo se relee más tarde, otro proceso puede haberlo reemplazado en el medio

La canonización es donde los verificadores caseros suelen equivocarse. El SignedInfo y el Object del paquete deben canonizarse en el contexto de namespaces del envolvente Signature completo. Un fragmento aislado cortado del documento hereda declaraciones de namespace distintas de las que el verificador ve en su lugar, así que sus bytes C14N difieren, el digest difiere, y una firma perfectamente válida falla. HotXLS usa el contexto completo del envolvente tanto del lado de verificación como del de emisión

Para paquetes que pueden llevar más de una firma, XlsVerifyAllSignaturesOpcPackage verifica cada firma adjunta y conserva los desenlaces individuales. El estado agregado es xsoiIntegrityVerified solo cuando cada firma admitida verifica; de lo contrario es la primera falla en orden de inventario, y cada reporte sigue diciéndole qué parte de firma falló

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);  // una instantánea, verificada y parseada
  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 no es un pase
end;

Los valores de estado son lo bastante específicos para actuar. xsoiDigestMismatch significa que una parte seleccionada cambió después de firmar, xsoiInvalidSignature significa que el valor RSA no verifica contra el SignedInfo con la clave del certificado incrustado, xsoiPartUnavailable significa que falta una parte seleccionada, y xsoiUnsupportedProfile o xsoiResourceLimit significan que el verificador se rehusó en lugar de adivinar. La capa ZIP de abajo aplica la misma lectura fail-closed descrita en el artículo sobre validación de EOCD ZIP para archivos XLSX no confiables, y la búsqueda de partes sigue las reglas de resolución de relaciones OPC de XLSX

¿Por qué editar un libro firmado invalida la firma?

Editar un libro firmado invalida la firma tan pronto como un byte cambiado cae dentro de una selección firmada, porque el digest SHA-256 guardado para esa Reference ya no coincide. Un escritor de hojas de cálculo que carga el libro y lo guarda de nuevo casi siempre toca partes firmadas: reserializa XML, renumera shared strings, reescribe workbook.xml y el manifiesto de content types. Aunque los valores de celda sean idénticos, los bytes no lo son, y la siguiente verificación devuelve xsoiDigestMismatch

HotXLS le deja hacerse la pregunta antes de comprometerse con una edición. XlsEditVerifiedOpcPackage reemplaza partes existentes en un paquete verificado y luego verifica el candidato. Con xsoePreserve devuelve xsoePreserved solo cuando la firma todavía verifica, lo que pasa cuando las partes editadas quedan fuera de toda selección firmada, y xsoeWouldInvalidate en caso contrario. Con xsoeInvalidateChanged quita el vínculo de firma de las relaciones de la raíz y de [Content_Types].xml y devuelve un paquete explícitamente sin firmar como xsoeInvalidated, en lugar de entregarle un archivo que todavía parece firmado

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 estaba fuera de la selección firmada
    xsoeWouldInvalidate: Result := False;  // un digest firmado cubre sheet2
  else
    raise Exception.CreateFmt('Edit refused, original integrity %d',
      [Ord(Details.OriginalIntegrity)]);
  end;
end;

Integridad contra confianza: ¿qué prueba una firma verificada?

Una firma verificada prueba que las partes seleccionadas no cambiaron desde que alguien con la clave privada las firmó, y nada sobre quién es ese alguien. HotXLS mantiene las dos capas separadas a propósito: cada TXLSSignatureOpcIntegrityResult lleva Trust = xsstUntrusted, incluidas las verificaciones exitosas, y ClaimedSignatureTime son datos suministrados por el firmante, no una marca de tiempo confiable. Cualquiera puede generar un certificado self-signed, firmar un libro y pasar la verificación de integridad

HotXLS verifica firmas digitales de paquete OPC de Excel en Delphi en dos capas: verificaciones de digest de integridad sobre una instantánea de bytes inmutable que siempre reportan xsstUntrusted, luego XlsVerifySignatureOpcPackageWithTrust con una política del llamador
Una firma verificada prueba que las partes seleccionadas no cambiaron, mientras que la confianza exige sus propios anchors, un tiempo de verificación UTC y el EKU de firma de documentos antes de que xsotTrustedSelection signifique algo

La confianza es una llamada aparte con una política del llamador. XlsDefaultSignatureOpcTrustPolicy devuelve una política fail-closed sin anchors, sin tiempo de verificación, con el EKU de firma de documentos 1.3.6.1.4.1.311.10.3.12 y revocación xsrvRequireCached. XlsVerifySignatureOpcPackageWithTrust primero corre el verificador de integridad completo y luego evalúa el certificado que realmente lo pasó; un llamador no puede sustituir un certificado hoja distinto

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;

El backend de Windows tiene dos trampas fáciles de perder cuando usted llama a WinCrypt por su cuenta. Que CertVerifyCertificateChainPolicy devuelva True solo significa que la llamada ejecutó; aceptar todavía exige un dwError cero en el estado de la política, cero errores agregados de cadena, un uso de clave digitalSignature explícito en la hoja y el EKU requerido. HotXLS encontró que un certificado al que le falta key usage puede pasar la política base ordinaria, así que verifica DigitalSignatureUsage y RequiredEkuPresent por su cuenta. La segunda trampa: combinar un store de raíces exclusivo con hRestrictedOther en la configuración del chain engine devuelve E_INVALIDARG en el backend actual, así que HotXLS usa el store de raíces exclusivo del llamador y luego autoriza cada elemento de la cadena primaria devuelta contra los bytes DER que usted suministró. Un emisor que casualmente esté en un store del sistema local nunca se vuelve confiable

xsotTrustedSelection sigue describiendo solo esa firma y su cobertura reportada. Las celdas sin firmar y las relaciones no seleccionadas pueden cambiar mientras la firma seleccionada sigue confiable, razón por la que el resultado de confianza también devuelve PackageSha256 y CertificateSha256 para su log de auditoría

¿Cómo se vuelve a firmar o contrafirma un libro sin Excel?

A nivel de arreglos de bytes HotXLS vuelve a firmar a través de transacciones explícitas y autorizadas, y las rutas de guardado firmado del Workbook reutilizan los mismos pipelines verificados. XlsReplaceVerifiedSignatureOpcPackage cambia una firma verificada por una nueva, XlsEditAndReplaceSelectedSignatureOpcPackage aplica ediciones de partes y reemplaza una firma seleccionada en la misma transacción, y XlsAppendVerifiedSignatureOpcPackage agrega una firma conservando cada byte original verificado tal cual. Las llamadas de reemplazo toman un TXLSSignatureOpcReplaceAuthorization, la llamada de anexo verifica primero cada original, y todas toman una selección TXLSSignatureOpcIssueOptions y un callback de firmante; la biblioteca nunca toca su clave privada

El registro de autorización ata la operación al original exacto que usted aprobó: ExpectedPackageSha256, ExpectedSignaturePart y ExpectedCertificateSha256, todos comparados solo después de que la firma vieja verificó por completo. Cada parte editada debe quedar cubierta por la nueva selección, cada firma no seleccionada debe seguir verificando contra el paquete preparado antes de llamar al firmante, y la llamada devuelve xspsWouldInvalidate en caso contrario

HotXLS vuelve a firmar un paquete XLSX de Excel en Delphi a través de XlsEditAndReplaceSelectedSignatureOpcPackage, que verifica la firma vieja, revisa los hashes esperados de paquete, parte y certificado, y llama a un callback de firmante
El registro de autorización ata el reemplazo al original aprobado, las firmas no seleccionadas deben seguir verificando sobre el paquete preparado, y la clave privada se queda dentro de su 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 es el SHA-256 de 32 bytes que la biblioteca calculó;
  // RsaSignSha256Pkcs1 representa su propio wrapper de CNG, HSM o smart card y
  // debe devolver 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 el hash guardado en la aprobación
  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;

Dos detalles en ese código son deliberados. Output y Details deben ser variables separadas; pasar un campo de Details como Output está explícitamente sin soporte, porque la biblioteca no puede publicar un paquete y conservar un reporte de certificados en el mismo almacenamiento. Y la cobertura vieja no se hereda: la nueva firma autentica exactamente las References que usted lista, así que decida de nuevo en qué partes confiará un consumidor aguas abajo

La contrafirma usa la misma llamada de anexo. Desde HotXLS 2.384.296, las TXLSSignatureOpcIssueOptions.References pasadas a XlsAppendVerifiedSignatureOpcPackage pueden nombrar partes del inventario de firmas originales verificadas, así que un revisor puede firmar sobre una parte de firma existente, encadenar varias contrafirmas, o mezclar partes de firma y partes de contenido en una sola selección. Cada firma anexada registra solo sus selecciones directas. La calificación de esa versión corrió 222 verificaciones de frontera y 122 verificaciones independientes con el verificador de empaquetado de .NET WindowsBase a través de 7 paquetes de salida, confirmando exactamente una firma nueva y firmas y certificados originales byte-idénticos

Trampas y límites que vale conocer

El bug más instructivo vino de un carácter perfectamente legal. Los URIs de Reference de OPC llevan el content type como query, por ejemplo /xl/worksheets/sheet1.xml?ContentType=application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml. Un emisor temprano de HotXLS codificaba por ciento el + literal como %2B. Las propias verificaciones de RSA, C14N y Reference de HotXLS aceptaban el resultado, pero WindowsBase reportaba InvalidSignature; una muestra controlada que cambiaba solo esa grafía pasaba de InvalidSignature a Success. El emisor ahora conserva la puntuación ASCII literal de query soportada mientras sigue escapando atributos XML, y la lección para cualquier firmante es probar contra un verificador nativo independiente, no solo contra el propio

  • El backend de confianza es solo Windows; otros destinos devuelven xsotTrustUnavailable después de que la integridad tiene éxito
  • El verificador acepta paquetes de hasta 64 MiB, 16 MiB por parte, y a lo sumo 32 partes de firma bajo un origen
  • La emisión acepta hasta 256 References y 4096 selectores combinados por firma
  • El perfil soportado es RSA-SHA256, digests SHA-256 y Canonical XML sin comentarios; otros algoritmos, Objects de aplicación extra y References externas devuelven xsoiUnsupportedProfile
  • xsrvRequireCached usa datos de revocación solo de caché; un estado faltante rechaza, y xsrvNotChecked no da ninguna garantía de revocación
  • Firmar un libro que Excel también tiene que aceptar todavía choca con las reglas de paquete de por qué Excel repara un XLSX válido; un Override duplicado o un Id de relación roto daña el archivo antes de que se revise cualquier firma

FAQ

¿Una verificación de integridad de HotXLS significa que el firmante es confiable?

No. XlsVerifySignatureOpcPackage prueba que las partes seleccionadas coinciden con sus digests bajo la clave del certificado incrustado y siempre reporta xsstUntrusted. Use XlsVerifySignatureOpcPackageWithTrust con sus propios anchors y un tiempo de verificación UTC para evaluar la cadena de certificados

¿Por qué Excel muestra la firma de mi libro como inválida después de una edición en Delphi?

Un libro guardado reescribe partes XML firmadas, así que el digest SHA-256 de al menos una Reference cambia. Use XlsEditVerifiedOpcPackage con xsoePreserve para probar una edición primero, o XlsEditAndReplaceSelectedSignatureOpcPackage para editar y volver a firmar en una transacción verificada

¿Puede HotXLS firmar con una clave guardada en una smart card o HSM?

Sí, porque firmar es un callback del llamador. La biblioteca pasa el SignedInfo canónico y su digest SHA-256 de 32 bytes, su código devuelve la firma RSASSA-PKCS1-v1_5, y la clave privada nunca sale del proveedor que usted controla

La verificación de firmas OPC, la evaluación de confianza, la contrafirma y la re-firma verificada son parte de HotXLS, el componente de hojas de cálculo Excel nativo para Delphi y C++Builder, que lee y escribe XLS, XLSX, XLSB y ODS sin Excel ni automatización COM