Artículo técnico

Verificar y refirmar firmas digitales de Excel en Delphi

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

El disparador habitual es un flujo documental que se quedó pequeño para el escritorio. Un controlador firma un libro de presupuesto en Excel, un servicio Delphi tiene luego que confirmar la firma antes de importar los números, y un tercer paso necesita corregir una hoja y firmar de nuevo con un certificado de servicio. Automatizar Excel en un servidor para eso es frágil y no soportado, y tratar la firma como una marca sí/no esconde las dos preguntas que importan de verdad: a qué bytes se comprometió el firmante, y si confiáis en la persona que guarda 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 fichero 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 W3C XML-DSig 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 un Reference puede ligar una parte, y no son intercambiables. Un Reference pelado hashea los bytes guardados de la parte. Un Reference de XML canónico hashea la forma Canonical XML (sin comentarios), así que el orden de atributos y la grafía de las declaraciones de namespace dejan de importar. Un Reference de relación aplica primero la RelationshipTransform de OPC y luego canoniza, y esa transformación conserva solo las relaciones que casan con sus selectores SourceId o SourceType. Un selector autentica exactamente las relaciones que selecciona: el resto de esa parte .rels puede cambiar libremente mientras la firma sigue válida

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

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

Verificar firmas desde una instantánea ZIP inmutable

HotXLS verifica un paquete desde una única instantánea de bytes inmutable, nunca desde el fichero en disco ni un TWorkbook vivo. XlsVerifySignatureOpcPackage y sus hermanos multi-firma toman un array TBytes, leen solo ese array, y nunca obtienen objetivos externos, certificados, ficheros locales ni recursos de red. La gracia es que los bytes que verificasteis son los bytes que parseáis después: si el fichero se relee más tarde, otro proceso puede haberlo sustituido mientras

La canonización es donde los verificadores caseros suelen torcerse. 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 sitio, así que sus bytes C14N difieren, el digest difiere, y una firma perfectamente válida falla. HotXLS usa el contexto del envolvente completo en el lado de verificación y en el de emisión

Para paquetes que pueden llevar más de una firma, XlsVerifyAllSignaturesOpcPackage verifica cada firma adjunta y conserva los resultados individuales. El estado agregado es xsoiIntegrityVerified solo cuando cada firma admitida verifica; si no, es el primer fallo en orden de inventario, y cada informe sigue diciendo 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 aprobado
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 embebido, xsoiPartUnavailable significa que falta una parte seleccionada, y xsoiUnsupportedProfile o xsoiResourceLimit significan que el verificador se negó en lugar de adivinar. La capa ZIP de debajo aplica la misma lectura de fallo cerrado descrita en el artículo sobre la validación de EOCD ZIP para ficheros XLSX no fiables, y la búsqueda de partes sigue las reglas de la resolución de relaciones OPC de XLSX

Por qué editar un libro firmado invalida la firma

Editar un libro firmado invalida la firma en cuanto un byte cambiado cae dentro de una selección firmada, porque el digest SHA-256 guardado para ese Reference deja de casar. Un writer de hojas de cálculo que carga el libro y lo vuelve a guardar 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 os deja hacer la pregunta antes de comprometeros a una edición. XlsEditVerifiedOpcPackage sustituye 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 todas las selecciones firmadas, y xsoeWouldInvalidate en caso contrario. Con xsoeInvalidateChanged quita el enlace 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 entregaros un fichero 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 frente a confianza: qué prueba una firma verificada

Una firma verificada prueba que las partes seleccionadas no han cambiado 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 comprobaciones exitosas, y ClaimedSignatureTime son datos suministrados por el firmante, no una marca de tiempo de fiar. Cualquiera puede generar un certificado autofirmado, firmar un libro y pasar la comprobación de integridad

HotXLS verifica las firmas digitales de paquete OPC de Excel en Delphi en dos capas: comprobaciones de digest de integridad sobre una instantánea de bytes inmutable que siempre informan xsstUntrusted, y después XlsVerifySignatureOpcPackageWithTrust con una política del llamador
Una firma verificada prueba que las partes seleccionadas no han cambiado, mientras que la confianza exige vuestros propios anchors, una hora 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 de fallo cerrado sin anchors, sin hora 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 ejecuta primero el verificador de integridad completo y luego evalúa el certificado que de verdad 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 llamáis a WinCrypt vosotros mismos. Que CertVerifyCertificateChainPolicy devuelva True solo significa que la llamada se ejecutó; aceptar sigue exigiendo un dwError cero en el estado de 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 sin key usage puede pasar la política base ordinaria, así que comprueba DigitalSignatureUsage y RequiredEkuPresent él mismo. La segunda trampa: combinar un almacén de raíz exclusivo con hRestrictedOther en la configuración del motor de cadena devuelve E_INVALIDARG en el backend actual, así que HotXLS usa el almacén de raíz exclusivo del llamador y luego autoriza cada elemento de la cadena primaria devuelta contra los bytes DER que suministrasteis. Un emisor que simplemente resulta estar en un almacén del sistema local nunca se vuelve de fiar

xsotTrustedSelection describe aún solo esa firma y su cobertura informada. Las celdas sin firmar y las relaciones no seleccionadas pueden cambiar mientras la firma seleccionada sigue de fiar, que es por lo que el resultado de confianza devuelve también PackageSha256 y CertificateSha256 para vuestro log de auditoría

Cómo refirmar o contrafirmar un libro sin Excel

A nivel de array de bytes HotXLS refirma mediante transacciones explícitas y autorizadas, y las rutas de guardado firmado del Workbook reutilizan los mismos pipelines verificados. XlsReplaceVerifiedSignatureOpcPackage intercambia una firma verificada por una nueva, XlsEditAndReplaceSelectedSignatureOpcPackage aplica ediciones de partes y sustituye una firma seleccionada en la misma transacción, y XlsAppendVerifiedSignatureOpcPackage añade una firma manteniendo cada byte original verificado igual. Las llamadas de sustitución toman un TXLSSignatureOpcReplaceAuthorization, la llamada de append verifica primero cada original, y todas toman una selección TXLSSignatureOpcIssueOptions y un callback de firmante; la biblioteca nunca toca vuestra clave privada

El registro de autorización liga la operación al original exacto que aprobasteis: ExpectedPackageSha256, ExpectedSignaturePart y ExpectedCertificateSha256, todos comparados solo después de que la firma vieja haya verificado del todo. Cada parte editada debe estar 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 refirma un paquete XLSX de Excel en Delphi mediante XlsEditAndReplaceSelectedSignatureOpcPackage, que verifica la firma vieja, comprueba los hashes esperados de paquete, parte y certificado, y llama a un callback de firmante
El registro de autorización liga la sustitución al original aprobado, las firmas no seleccionadas deben seguir verificando sobre el paquete preparado, y la clave privada se queda dentro de vuestro 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 calculó la biblioteca;
  // RsaSignSha256Pkcs1 representa vuestro propio envoltorio CNG, HSM o smart card y
  // debe devolver la firma 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); // 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 de 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 informe de certificado en el mismo almacenamiento. Y la cobertura vieja no se hereda: la nueva firma autentica exactamente las References que listéis, así que decidid de nuevo en qué partes va a confiar un consumidor posterior

La contrafirma usa la misma llamada de append. Desde HotXLS 2.384.296, las TXLSSignatureOpcIssueOptions.References pasadas a XlsAppendVerifiedSignatureOpcPackage pueden nombrar partes del inventario de firmas originales verificado, 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 selección. Cada firma añadida registra solo sus selecciones directas. La calificación de esa versión corrió 222 comprobaciones de frontera y 122 comprobaciones independientes con el verificador de empaquetado .NET WindowsBase sobre 7 paquetes de salida, confirmando exactamente una firma nueva y firmas y certificados originales byte-idénticos

Trampas y límites que conviene 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 codificó en porcentaje el + literal como %2B. Las propias comprobaciones de RSA, C14N y Reference de HotXLS aceptaron el resultado, pero WindowsBase informó InvalidSignature; una muestra controlada que cambiaba solo esa grafía pasó de InvalidSignature a Success. El emisor conserva ahora 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 vuestro

  • El backend de confianza es solo Windows; otros objetivos devuelven xsotTrustUnavailable después de que la integridad tenga éxito
  • El verificador acepta paquetes de hasta 64 MiB, 16 MiB por parte, y como máximo 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 externos devuelven xsoiUnsupportedProfile
  • xsrvRequireCached usa datos de revocación solo de caché; un estado ausente rechaza, y xsrvNotChecked no da ninguna garantía de revocación
  • Firmar un libro que Excel también tenga que aceptar choca todavía con las reglas de paquete de por qué Excel repara un XLSX válido; un Override o un Id de relación duplicados rompen el fichero antes de que se compruebe cualquier firma

FAQ

¿Una comprobación de integridad de HotXLS significa que el firmante es de fiar?

No. XlsVerifySignatureOpcPackage prueba que las partes seleccionadas casan con sus digests bajo la clave del certificado embebido y siempre informa xsstUntrusted. Usad XlsVerifySignatureOpcPackageWithTrust con vuestros propios anchors y una hora 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 un Reference cambia. Usad XlsEditVerifiedOpcPackage con xsoePreserve para probar una edición primero, o XlsEditAndReplaceSelectedSignatureOpcPackage para editar y refirmar en una transacción verificada

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

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

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