Podpisz duży instalator bez przesyłania go na serwer

· Komponenty
Podpisz duży instalator bez przesyłania go na serwer

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łaniaTrasa skrótu
Pakiet1 075 355 648 bajtów1 075 355 648 bajtów
Wysłano do serwera1 075 356 521 bajtów477 bajtów
Odebrano z serwera1 075 364 108 bajtów11 044 bajtów
Czas w sieci14,1 s0,18 s
Całe polecenie16,3 s4,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.