Ein großes Installationspaket signieren, ohne es hochzuladen

· Komponenten
Ein großes Installationspaket signieren, ohne es hochzuladen

Ein Signaturserver hält den privaten Schlüssel an einem einzigen Ort, und genau das ist der Sinn davon. Der übliche Preis dafür ist die Datei selbst: Der Build Agent lädt das Installationspaket hoch, der Server signiert es und schickt das Ganze wieder zurück. Bei einem Installationspaket von mehreren Gigabyte bedeutet das, das Paket zweimal durch das Netzwerk zu schicken, nur um eine Signatur von wenigen Kilobyte hinzuzufügen.

Das muss nicht sein. Was Authenticode signiert, ist nie die Datei, sondern ein Digest der Datei, und dieser Digest kann dort berechnet werden, wo die Datei bereits liegt. Ab sgcSign 2026.10 signiert der sgcSign Server ein Windows Installer Paket sowie ein MSIX oder APPX Paket allein anhand dieses Digests, so wie er es für eine EXE bereits tat.

Ein 1 GB großes Installationspaket, zweimal über den sgcSign Server signiert, einmal per Upload und einmal anhand seines Hashes, mit jedem gezählten Byte auf der Leitung. Auch auf YouTube.

Was tatsächlich signiert wird

Eine Authenticode Signatur deckt einen Digest ab, und jedes Format definiert seinen eigenen. Bei einer EXE ist es der PE Image Hash, ohne die Prüfsumme und die Zertifikatstabelle. Bei einer MSI ist es ein Digest über die Streams der Compound File in einer festen Reihenfolge. Bei einer MSIX ist es ein kleiner Block aus SHA-256 Digests über die Teile des Pakets. Der Server benötigt nur diesen Wert und sonst nichts: Er signiert den Digest mit dem Schlüssel, den er besitzt, und der Client schreibt die Signatur in die Datei.

Über die Befehlszeile

Das Kommandozeilenwerkzeug nimmt standardmäßig die Hash Route für --format msi und --format appx, wie es das bereits für --format authenticode tat, und --upload sendet stattdessen die ganze Datei. Die Demo signiert dasselbe 1 GB große Paket auf beide Arten gegen einen Server auf derselben Maschine, über ein kleines Relay, das jedes vom Werkzeug gesendete und empfangene Byte zählt. Das Paket enthält 1 GiB Zufallsdaten, sodass unterwegs nichts komprimiert werden kann.

> 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

Beide Dateien kommen signiert zurück und tragen denselben Digest: signtool verify /v zeigt bei beiden Hash of file (sha256): 9151F2EE…5A0CDDD, den Wert aus der Zeile [prehash], mit dem Demo Signierer und dem DigiCert Zeitstempel.

Die Zahlen

Upload RouteHash Route
Paket1.075.355.648 bytes1.075.355.648 bytes
An den Server gesendet1.075.356.521 bytes477 bytes
Vom Server empfangen1.075.364.108 bytes11.044 bytes
Zeit auf der Leitung14,1 s0,18 s
Gesamter Befehl16,3 s4,8 s

Das lief über Loopback, wo das Bewegen von Bytes fast nichts kostet, und trotzdem brauchte die Upload Route dafür 14 Sekunden. Über eine echte Leitung ist die Übertragung der gesamte Kostenfaktor: Bei 100 Mbit/s brauchen die 2,15 GB der Upload Route fast drei Minuten, während die Hash Route weiterhin nur 477 Bytes sendet. Was von den 4,8 Sekunden der Hash Route übrig bleibt, ist das Vorbereiten und Hashen des 1 GB großen Pakets auf dem Build Agent, eine Arbeit, die die Upload Route ebenfalls erledigt, nur eben auf dem Server.

Dasselbe in Delphi

In einem Delphi Build Werkzeug besteht der Ablauf aus vier Schritten: das Paket vorbereiten, das vorbereitete Paket hashen, den Hash senden und die zurückkommende Signatur einbetten. Der HTTP Aufruf erfolgt mit dem Client, den Ihr Projekt bereits verwendet. Dieses Beispiel nutzt THTTPClient und System.JSON aus der 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.

Vom selben Server kam ein PKCS#7 mit 7.941 Bytes zurück, und signtool liest das Ergebnis genau wie bei den beiden Dateien oben.

Erst vorbereiten, dann hashen

Ein Installationspaket wird in der Form signiert, in der es ausgeliefert wird, deshalb muss es in diese Form gebracht werden, bevor es gehasht wird. sgcMSIPrepareForSigning und sgcAppxPrepareForSigning erledigen das, und die Regel ist einfach: Hashen Sie, was prepare geliefert hat, und betten Sie die Signatur in genau dieses vorbereitete Paket ein, nicht in das Original. Hashen Sie stattdessen das Original, deckt die Signatur Bytes ab, die es nicht mehr gibt.

MSIX: den Publisher selbst prüfen

Auf der Hash Route sieht keine Seite beide Hälften der einen Prüfung, bei der Windows streng ist. Der Server besitzt das Zertifikat und sieht nie das Manifest, und der Client besitzt das Manifest und sieht nie das Zertifikat. Deshalb enthält jede Hash Antwort signer_subject, das Subject des Zertifikats, das signiert hat. Vergleichen Sie es mit dem Publisher des Pakets, mit sgcAppxReadPublisher und sgcAppxDNMatches, bevor Sie die Signatur einbetten. Das Kommandozeilenwerkzeug tut dies von selbst und weigert sich, das Paket zu schreiben, wenn beide voneinander abweichen.

Wann trotzdem hochgeladen werden sollte

Die Routen für die vollständige Datei sind weiterhin vorhanden. Verwenden Sie sie, wenn der Client den Digest nicht selbst berechnen kann, oder wenn eine Signatur eine Freigabe benötigt, weil der Freigabe Workflow nur mit vollständigen Datei Uploads arbeitet. --upload schaltet das Kommandozeilenwerkzeug für einen einzelnen Lauf auf diese Route zurück.

Verfügbarkeit

Die Hash Routen für Installationspakete, /api/v1/sign/msi/hash und /api/v1/sign/appx/hash, der standardmäßige Hash zuerst Ansatz des Kommandozeilenwerkzeugs sowie die Funktionen zum Vorbereiten, Hashen und Einbetten sind ab sgcSign 2026.10 für Delphi und C++Builder verfügbar. Die Routen sind in der sgcSign Online Hilfe dokumentiert.

Signieren Sie große Pakete aus einer Build Farm? Nehmen Sie Kontakt auf, und Sie erhalten eine Antwort von den Leuten, die den Code geschrieben haben.