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 Route | Hash Route | |
|---|---|---|
| Paket | 1.075.355.648 bytes | 1.075.355.648 bytes |
| An den Server gesendet | 1.075.356.521 bytes | 477 bytes |
| Vom Server empfangen | 1.075.364.108 bytes | 11.044 bytes |
| Zeit auf der Leitung | 14,1 s | 0,18 s |
| Gesamter Befehl | 16,3 s | 4,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.
