Serwer podpisujący przechowuje klucz prywatny w jednym miejscu i właśnie o to w nim chodzi. Zwykłą ceną za to jest sam plik: agent budujący przesyła instalator, serwer go podpisuje i odsyła całość z powrotem. Dla instalatora liczącego kilka gigabajtów oznacza to, że instalator dwukrotnie przemierza sieć, aby dodać podpis liczący zaledwie kilka kilobajtów.
Nie musi tak być. To, co podpisuje Authenticode, nigdy nie jest plikiem, lecz skrótem pliku, a ten skrót można obliczyć tam, gdzie plik już się znajduje. Od wersji sgcSign 2026.10 sgcSign Server podpisuje pakiet Windows Installer oraz pakiet MSIX lub APPX na podstawie samego tego skrótu, tak jak dotychczas robił to dla pliku EXE.
Instalator o rozmiarze 1 GB podpisany dwukrotnie przez sgcSign Server, raz przez przesłanie pliku, a raz na podstawie skrótu, z policzeniem każdego bajtu przesłanego w obu przypadkach. Także na YouTube.
Co tak naprawdę zostaje podpisane
Podpis Authenticode obejmuje skrót, a każdy format definiuje własny sposób jego obliczania. Dla pliku EXE jest to skrót obrazu PE, z pominięciem sumy kontrolnej i tabeli certyfikatów. Dla MSI jest to skrót obliczony na strumieniach pliku złożonego, w ustalonej kolejności. Dla MSIX jest to niewielki blok skrótów SHA-256 obliczonych dla poszczególnych części pakietu. Serwer potrzebuje tylko tej wartości i niczego więcej: podpisuje skrót kluczem, który posiada, a klient zapisuje podpis w pliku.
Z poziomu wiersza poleceń
Narzędzie wiersza poleceń domyślnie korzysta z trasy skrótu dla --format msi i --format appx, tak jak wcześniej robiło to dla --format authenticode, a opcja --upload powoduje zamiast tego wysłanie całego pliku. Demonstracja podpisuje ten sam pakiet o rozmiarze 1 GB na oba sposoby, wobec serwera działającego na tej samej maszynie, za pośrednictwem niewielkiego pośrednika (relay), który zlicza każdy bajt wysłany i odebrany przez narzędzie. Pakiet zawiera 1 GiB losowych danych, więc żadnym sposobem nie da się go po drodze skompresować.
> 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
Oba pliki wracają podpisane i noszą ten sam skrót: signtool verify /v pokazuje dla obu Hash of file (sha256): 9151F2EE…5A0CDDD, czyli wartość z linii [prehash], wraz z demonstracyjnym podpisującym i znacznikiem czasu DigiCert.
Liczby
| Trasa przesyłania | Trasa skrótu | |
|---|---|---|
| Pakiet | 1 075 355 648 bajtów | 1 075 355 648 bajtów |
| Wysłano do serwera | 1 075 356 521 bajtów | 477 bajtów |
| Odebrano z serwera | 1 075 364 108 bajtów | 11 044 bajtów |
| Czas w sieci | 14,1 s | 0,18 s |
| Całe polecenie | 16,3 s | 4,8 s |
Test ten przeprowadzono przez loopback, gdzie przesyłanie bajtów kosztuje niemal nic, a mimo to trasa przesyłania pliku zajęła aż 14 sekund. W przypadku prawdziwego łącza cały koszt stanowi transfer danych: przy 100 Mbit/s przesłanie 2,15 GB trasą przesyłania pliku trwa niemal trzy minuty, podczas gdy trasa skrótu nadal wysyła jedynie 477 bajtów. To, co pozostaje z 4,8 sekundy trasy skrótu, to przygotowanie i obliczenie skrótu pakietu o rozmiarze 1 GB na agencie budującym, czyli praca, którą wykonuje też trasa przesyłania pliku, tyle że na serwerze.
To samo w Delphi
W narzędziu budującym napisanym w Delphi cały przepływ składa się z czterech kroków: przygotowania pakietu, obliczenia skrótu przygotowanego pakietu, wysłania skrótu i osadzenia podpisu, który wraca z serwera. Wywołanie HTTP może korzystać z dowolnego klienta już używanego w projekcie. W tym przykładzie użyto THTTPClient i System.JSON z 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.
Z tego samego serwera otrzymano z powrotem PKCS#7 o rozmiarze 7941 bajtów, a signtool odczytuje wynik dokładnie tak samo, jak w przypadku obu plików powyżej.
Najpierw przygotuj, potem oblicz skrót
Pakiet instalacyjny jest podpisywany w postaci, w jakiej zostanie ostatecznie wydany, dlatego trzeba go doprowadzić do tej postaci, zanim zostanie obliczony jego skrót. Robią to funkcje sgcMSIPrepareForSigning i sgcAppxPrepareForSigning, a zasada jest prosta: oblicz skrót tego, co dała funkcja prepare, i osadź podpis w tym samym przygotowanym pakiecie, a nie w oryginale. Jeśli zamiast tego obliczysz skrót oryginału, podpis będzie obejmował bajty, które już nie istnieją.
MSIX: sam sprawdź wydawcę
Na trasie skrótu żadna ze stron nie widzi obu połówek jedynej kontroli, na której punkcie Windows jest rygorystyczny. Serwer posiada certyfikat, ale nigdy nie widzi manifestu, a klient posiada manifest, ale nigdy nie widzi certyfikatu. Dlatego każda odpowiedź trasy skrótu zawiera signer_subject, czyli podmiot certyfikatu, którym podpisano. Porównaj go z polem Publisher pakietu, korzystając z sgcAppxReadPublisher i sgcAppxDNMatches, zanim osadzisz podpis. Narzędzie wiersza poleceń robi to samodzielnie i odmawia zapisania pakietu, gdy wartości się różnią.
Kiedy mimo wszystko przesłać plik
Trasy dla całego pliku wciąż istnieją. Skorzystaj z nich, gdy klient nie potrafi sam obliczyć skrótu, albo gdy podpis wymaga zatwierdzenia, ponieważ proces zatwierdzania działa wyłącznie z przesyłaniem całych plików. Opcja --upload przełącza narzędzie wiersza poleceń z powrotem na tę trasę na czas jednego uruchomienia.
Dostępność
Trasy skrótu dla pakietów instalacyjnych, /api/v1/sign/msi/hash i /api/v1/sign/appx/hash, domyślne pierwszeństwo trasy skrótu w narzędziu wiersza poleceń oraz funkcje prepare, hash i embed są dostępne od wersji sgcSign 2026.10 dla Delphi i C++Builder. Trasy te są udokumentowane w pomocy online sgcSign.
Podpisujesz duże pakiety z farmy budującej? Skontaktuj się z nami, a odpowiedź otrzymasz od osób, które napisały ten kod.
