Assinar um instalador grande sem fazer upload

· Componentes
Assinar um Instalador Grande Sem Fazer Upload

Um servidor de assinatura mantém a chave privada em um único lugar, e esse é o motivo de existir. O preço costuma ser o próprio arquivo: o agente de build faz upload do instalador, o servidor o assina e devolve o pacote inteiro. Para um instalador de alguns gigabytes, isso significa o instalador atravessando a rede duas vezes, só para acrescentar uma assinatura de alguns kilobytes.

Não precisa ser assim. O que o Authenticode assina nunca é o arquivo, é um digest do arquivo, e esse digest pode ser calculado onde o arquivo já está. A partir do sgcSign 2026.10, o sgcSign Server assina um pacote Windows Installer e um pacote MSIX ou APPX apenas a partir desse digest, do mesmo jeito que já fazia para um EXE.

Um instalador de 1 GB assinado duas vezes pelo sgcSign Server, uma vez com upload e outra a partir do seu hash, com cada byte na rede contabilizado. Também no YouTube.

O que realmente é assinado

Uma assinatura Authenticode cobre um digest, e cada formato define o seu. Para um EXE é o hash da imagem PE, sem o checksum e a tabela de certificados. Para um MSI é um digest sobre os streams do compound file em uma ordem fixa. Para um MSIX é um pequeno bloco de digests SHA-256 sobre as partes do pacote. O servidor precisa apenas desse valor: ele assina o digest com a chave que possui, e o cliente grava a assinatura no arquivo.

Pela linha de comando

A ferramenta de linha de comando usa a rota de hash por padrão para --format msi e --format appx, como já fazia para --format authenticode, e --upload envia o arquivo inteiro em vez disso. A demonstração assina o mesmo pacote de 1 GB das duas formas contra um servidor na mesma máquina, através de um pequeno relay que conta cada byte que a ferramenta envia e recebe. O pacote contém 1 GiB de dados aleatórios, de modo que nada no caminho consegue comprimi-lo.

> 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

Os dois arquivos voltam assinados, e carregam o mesmo digest: signtool verify /v mostra Hash of file (sha256): 9151F2EE…5A0CDDD em ambos, o valor da linha [prehash], com o assinante de demonstração e o timestamp da DigiCert.

Os números

Rota de uploadRota de hash
Pacote1.075.355.648 bytes1.075.355.648 bytes
Enviado ao servidor1.075.356.521 bytes477 bytes
Recebido do servidor1.075.364.108 bytes11.044 bytes
Tempo na rede14,1 s0,18 s
Comando completo16,3 s4,8 s

Isso rodou em loopback, onde mover bytes custa quase nada, e mesmo assim a rota de upload gastou 14 segundos nisso. Em um link real, a transferência é o custo inteiro: a 100 Mbit/s, os 2,15 GB da rota de upload levam quase três minutos, e a rota de hash ainda envia 477 bytes. O que resta dos 4,8 segundos da rota de hash é preparar e calcular o hash do pacote de 1 GB no agente de build, trabalho que a rota de upload também faz, só que no servidor.

A mesma coisa em Delphi

Em uma ferramenta de build em Delphi, o fluxo tem quatro etapas: preparar o pacote, calcular o hash do pacote preparado, enviar o hash e incorporar a assinatura recebida. A chamada HTTP é feita com o cliente que o seu projeto já usa. Este exemplo usa o THTTPClient e o System.JSON da 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.

No mesmo servidor, a resposta trouxe um PKCS#7 de 7.941 bytes, e o signtool lê o resultado exatamente como nos dois arquivos acima.

Prepare primeiro, depois calcule o hash

Um pacote de instalador é assinado na forma em que será distribuído, por isso precisa ser colocado nessa forma antes de ter o hash calculado. sgcMSIPrepareForSigning e sgcAppxPrepareForSigning fazem isso, e a regra é simples: calcule o hash do que o prepare devolveu, e incorpore a assinatura nesse mesmo pacote preparado, não no original. Calcule o hash do original e a assinatura vai cobrir bytes que já não existem.

MSIX: verifique o publisher você mesmo

Na rota de hash, nenhum dos dois lados vê as duas metades da verificação com a qual o Windows é rigoroso. O servidor guarda o certificado e nunca vê o manifesto, e o cliente guarda o manifesto e nunca vê o certificado. Por isso, toda resposta de hash traz o signer_subject, o subject do certificado que assinou. Compare com o Publisher do pacote, usando sgcAppxReadPublisher e sgcAppxDNMatches, antes de incorporar a assinatura. A ferramenta de linha de comando faz isso sozinha e se recusa a gravar o pacote quando eles diferem.

Quando fazer upload mesmo assim

As rotas de arquivo completo continuam disponíveis. Use-as quando o cliente não conseguir calcular o digest sozinho, ou quando uma assinatura precisar de aprovação, porque o fluxo de aprovação só funciona com uploads de arquivo completo. --upload muda a ferramenta de linha de comando de volta para essa rota em uma única execução.

Disponibilidade

As rotas de hash para pacotes de instalador, /api/v1/sign/msi/hash e /api/v1/sign/appx/hash, o padrão de hash primeiro da ferramenta de linha de comando e as funções de preparar, calcular hash e incorporar estão disponíveis no sgcSign 2026.10 para Delphi e C++Builder. As rotas estão documentadas na ajuda online do sgcSign.

Está assinando pacotes grandes a partir de um build farm? Entre em contato, e você receberá uma resposta de quem escreveu o código.