Firmar un instalador grande sin subirlo

· Componentes
Firmar un instalador grande sin subirlo

Un servidor de firma guarda la clave privada en un único lugar, y ese es precisamente el sentido de tenerlo. El precio habitual es el propio archivo: el agente de compilación sube el instalador, el servidor lo firma y devuelve el archivo entero. Para un instalador de varios gigabytes, eso significa que el instalador cruza la red dos veces, solo para añadir una firma de unos pocos kilobytes.

No tiene por qué ser así. Lo que Authenticode firma nunca es el archivo, sino un digest del archivo, y ese digest se puede calcular donde el archivo ya se encuentra. Desde sgcSign 2026.10, el sgcSign Server firma un paquete Windows Installer y un paquete MSIX o APPX únicamente a partir de ese digest, tal como ya hacía con un EXE.

Un instalador de 1 GB firmado dos veces a través del sgcSign Server, una vez subiéndolo y otra a partir de su hash, contando cada byte en la red. También en YouTube.

Qué se firma realmente

Una firma Authenticode cubre un digest, y cada formato define el suyo. Para un EXE es el hash de la imagen PE, sin incluir el checksum ni la tabla de certificados. Para un MSI es un digest sobre los streams del compound file en un orden fijo. Para un MSIX es un pequeño bloque de digests SHA-256 sobre las partes del paquete. El servidor solo necesita ese valor y nada más: firma el digest con la clave que posee, y el cliente escribe la firma en el archivo.

Desde la línea de comandos

La herramienta de línea de comandos toma la ruta de hash por defecto para --format msi y --format appx, tal como ya lo hacía para --format authenticode, y --upload envía el archivo completo en su lugar. La demostración firma el mismo paquete de 1 GB de las dos formas contra un servidor en la misma máquina, a través de un pequeño relay que cuenta cada byte que la herramienta envía y recibe. El paquete contiene 1 GiB de datos aleatorios, de modo que nada en el camino puede comprimirlo.

> sgcsign sign --format msi --upload --server http://127.0.0.1:18481 --apikey $k `
    --provider demo --tsa http://timestamp.digicert.com --verbose `
    --out BigApp-signed-upload.msi BigApp.msi
Signed: BigApp-signed-upload.msi | Signer: O=eSeGeCe Demo, CN=sgcSign Demo Code Signing | Duration: 3015 ms

> Get-Content .\relay.log -Tail 1
RUN 4: connections=1 bytes_up(client->server)=1075356521 bytes_down(server->client)=1075364108 wall=14.087s

> sgcsign sign --format msi --server http://127.0.0.1:18481 --apikey $k `
    --provider demo --tsa http://timestamp.digicert.com --verbose `
    --out BigApp-signed-hash.msi BigApp.msi
[prehash] alg=sha256 hash=9151f2ee34b18f80a20d4b2cec515ac344066bbed846c7efbc98d7c5e5a0cddd

> Get-Content .\relay.log -Tail 1
RUN 5: connections=1 bytes_up(client->server)=477 bytes_down(server->client)=11044 wall=0.178s

Ambos archivos vuelven firmados y llevan el mismo digest: signtool verify /v muestra en los dos Hash of file (sha256): 9151F2EE…5A0CDDD, el valor de la línea [prehash], con el firmante de demostración y la marca de tiempo de DigiCert.

Las cifras

Ruta de subidaRuta de hash
Paquete1.075.355.648 bytes1.075.355.648 bytes
Enviado al servidor1.075.356.521 bytes477 bytes
Recibido del servidor1.075.364.108 bytes11.044 bytes
Tiempo en la red14,1 s0,18 s
Comando completo16,3 s4,8 s

Esto se ejecutó por loopback, donde mover bytes cuesta casi nada, y aun así la ruta de subida tardó 14 segundos en hacerlo. En un enlace real, la transferencia es todo el costo: a 100 Mbit/s, los 2,15 GB de la ruta de subida tardan cerca de tres minutos, mientras que la ruta de hash sigue enviando 477 bytes. Lo que queda de los 4,8 segundos de la ruta de hash es preparar y calcular el hash del paquete de 1 GB en el agente de compilación, un trabajo que la ruta de subida también hace, solo que en el servidor.

Lo mismo en Delphi

En una herramienta de compilación en Delphi, el flujo tiene cuatro pasos: preparar el paquete, calcular el hash del paquete preparado, enviar el hash e incrustar la firma que llega de vuelta. La llamada HTTP es la que ya use su proyecto. Este ejemplo usa THTTPClient y System.JSON de la RTL.

program HashSignMSI;

{$APPTYPE CONSOLE}

uses
  System.SysUtils, System.Classes, System.JSON, System.Net.HttpClient,
  sgcSign_MSI, sgcSign_Base64, sgcSign_Authenticode;

const
  CS_SERVER = 'http://127.0.0.1:18480';

var
  vPrepared, vSigned, vHex, vBody: string;
  vHash, vPKCS7: TBytes;
  vI: Integer;
  oHTTP: THTTPClient;
  oFields: TStringList;
  oResponse: IHTTPResponse;
  oJSON: TJSONValue;
begin
  try
    vPrepared := ChangeFileExt(ParamStr(1), '.prepared.msi');
    vSigned := ChangeFileExt(ParamStr(1), '.signed.msi');
    // 1. Prepare: this is the package the digest covers
    sgcMSIPrepareForSigning(ParamStr(1), vPrepared);
    // 2. Hash the PREPARED package on this machine
    vHash := sgcMSIComputeHash(vPrepared, ahSHA256);
    vHex := '';
    for vI := 0 to Length(vHash) - 1 do
      vHex := vHex + LowerCase(IntToHex(vHash[vI], 2));
    Writeln('SHA-256 : ', vHex);
    // 3. Only the digest leaves the machine
    oHTTP := THTTPClient.Create;
    oFields := TStringList.Create;
    try
      oHTTP.CustomHeaders['X-API-Key'] := GetEnvironmentVariable('SGCSIGN_APIKEY');
      oFields.Add('hash=' + vHex);
      oFields.Add('alg=sha256');
      oFields.Add('provider=demo');
      oFields.Add('tsa_url=http://timestamp.digicert.com');
      oFields.Add('level=t');
      oResponse := oHTTP.Post(CS_SERVER + '/api/v1/sign/msi/hash', oFields);
      vBody := oResponse.ContentAsString(TEncoding.UTF8);
      if oResponse.StatusCode <> 200 then
        raise Exception.CreateFmt('HTTP %d: %s', [oResponse.StatusCode, vBody]);
    finally
      oFields.Free;
      oHTTP.Free;
    end;
    // 4. Base64-decode the PKCS#7 and embed it into the SAME prepared package
    oJSON := TJSONObject.ParseJSONValue(vBody);
    try
      vPKCS7 := TsgcBase64.Decode(oJSON.GetValue<string>('signature'));
    finally
      oJSON.Free;
    end;
    sgcMSIEmbedSignature(vPrepared, vSigned, vPKCS7);
    Writeln('PKCS#7  : ', Length(vPKCS7), ' bytes');
    Writeln('Signed  : ', vSigned);
  except
    on E: Exception do
    begin
      Writeln(ErrOutput, 'error: ', E.Message);
      ExitCode := 1;
    end;
  end;
end.

Contra el mismo servidor, la respuesta trajo un PKCS#7 de 7.941 bytes, y signtool lee el resultado exactamente igual que en los dos archivos anteriores.

Primero prepare, luego calcule el hash

Un paquete de instalador se firma en la forma en que se va a distribuir, así que hay que ponerlo en esa forma antes de calcular su hash. sgcMSIPrepareForSigning y sgcAppxPrepareForSigning hacen eso, y la regla es simple: calcule el hash de lo que devolvió prepare, e incruste la firma en ese mismo paquete preparado, no en el original. Calcule el hash del original y la firma cubrirá bytes que ya no existen.

MSIX: compruebe usted mismo el publisher

En la ruta de hash, ninguno de los dos lados ve las dos mitades de la única comprobación con la que Windows es estricto. El servidor tiene el certificado y nunca ve el manifiesto, y el cliente tiene el manifiesto y nunca ve el certificado. Por eso, cada respuesta de hash lleva signer_subject, el subject del certificado que firmó. Compárelo con el Publisher del paquete, con sgcAppxReadPublisher y sgcAppxDNMatches, antes de incrustar la firma. La herramienta de línea de comandos hace esto por su cuenta y se niega a escribir el paquete cuando difieren.

Cuándo subir el archivo de todos modos

Las rutas de archivo completo siguen disponibles. Úselas cuando el cliente no pueda calcular el digest por sí mismo, o cuando una firma necesite aprobación, porque el flujo de aprobación solo funciona con subidas de archivo completo. --upload hace que la herramienta de línea de comandos vuelva a esa ruta para una sola ejecución.

Disponibilidad

Las rutas de hash para paquetes de instalador, /api/v1/sign/msi/hash y /api/v1/sign/appx/hash, el comportamiento de hash por defecto de la herramienta de línea de comandos y las funciones de preparar, calcular hash e incrustar están disponibles en sgcSign 2026.10 para Delphi y C++Builder. Las rutas están documentadas en la ayuda en línea de sgcSign.

¿Firma paquetes grandes desde una granja de compilación? Póngase en contacto, y recibirá una respuesta de quienes escribieron el código.