Delphi에서 MSI 또는 MSIX 설치 프로그램에 서명하는 방법

· 컴포넌트
Delphi에서 MSI 또는 MSIX 설치 프로그램에 서명하는 방법

서명되지 않은 설치 프로그램은 고객이 가장 먼저 문제를 알아차리는 지점입니다. SmartScreen은 알 수 없는 게시자에 대해 경고를 표시하고, UAC 프롬프트에는 이름이 표시되지 않으며, MSIX 패키지는 아예 설치조차 되지 않습니다. Windows는 유효한 서명을 가진 패키지만 설치하기 때문입니다.

sgcSign 2026.10에는 이를 위한 두 가지 컴포넌트가 추가되었습니다. TsgcMSISigner는 Windows Installer의 .msi.msp 파일에 서명하고, TsgcAppxSigner.msix.appx 패키지와 그 번들에 서명합니다. 두 컴포넌트 모두 이미 EXE용으로 사용해 보셨을 Authenticode 서명자와 똑같이 작동합니다. 키 공급자, 선택적 타임스탬프 기관, 그리고 단 하나의 호출입니다.

Delphi 프로그램에서 서명된 MSI와 MSIX, 그리고 설치될 수 없는 패키지를 막아내는 게시자 확인 기능입니다. YouTube에서도 시청하세요.

프로그램

파일 확장자에 따라 서명자를 선택하고 SHA-256으로 서명하며, 타임스탬프 기관이 지정된 경우 RFC 3161 타임스탬프를 추가하는 하나의 콘솔 프로그램입니다.

program SignInstaller;

{$APPTYPE CONSOLE}

uses
  SysUtils, StrUtils, sgcSign_Authenticode, sgcSign_MSI, sgcSign_APPX,
  sgcSign_KeyProvider_PFX, sgcSign_TSA;

var
  PFX: TsgcPFXKeyProvider;
  TSA: TsgcTSAClient;
  Signer: TsgcAuthenticodeSigner;
begin
  if ParamCount < 4 then
  begin
    WriteLn('usage: SignInstaller <input.msi|input.msix> <output> ' +
      '<file.pfx> <password> [tsa-url]');
    Halt(1);
  end;
  if MatchText(ExtractFileExt(ParamStr(1)), ['.msi', '.msp']) then
    Signer := TsgcMSISigner.Create(nil)
  else
    Signer := TsgcAppxSigner.Create(nil); // .msix .appx and bundles
  PFX := TsgcPFXKeyProvider.Create(nil);
  TSA := TsgcTSAClient.Create(nil);
  try
    try
      PFX.LoadFromFile(ParamStr(3), ParamStr(4));
      Signer.KeyProvider := PFX;
      Signer.Hash := ahSHA256;
      Signer.Level := alBES;
      if ParamCount > 4 then
      begin
        TSA.URL := ParamStr(5);
        Signer.TSAClient := TSA;
        Signer.Level := alT;
      end;
      if Signer is TsgcAppxSigner then
      begin
        WriteLn('Publisher  : ', sgcAppxReadPublisher(ParamStr(1)));
        WriteLn('Certificate: ', PFX.Certificate.SubjectRFC2253);
        TsgcAppxSigner(Signer).SignFile(ParamStr(1), ParamStr(2));
      end
      else
        TsgcMSISigner(Signer).SignFile(ParamStr(1), ParamStr(2));
      WriteLn('Signed: ', ParamStr(2));
    except
      on E: Exception do
      begin
        WriteLn('Error: ', E.Message);
        ExitCode := 1;
      end;
    end;
  finally
    Signer.Free;
    TSA.Free;
    PFX.Free;
  end;
end.

두 서명자 모두 TsgcAuthenticodeSigner를 상속하므로, 키 공급자, 다이제스트, 타임스탬프 레벨, 설명은 EXE와 마찬가지로 공통 베이스에서 설정되며, SignFile은 구체적인 클래스뿐만 아니라 베이스 타입을 통해서도 호출할 수 있습니다.

Windows Installer 패키지 서명하기

.msi는 PE 파일이 아닙니다. 스트림과 스토리지로 이루어진 작은 파일 시스템인 복합 파일이며, Windows가 확인하는 다이제스트는 서명 스트림을 제외한 채 정확한 순서로 이 스트림들에 대해 계산됩니다. TsgcMSISigner는 이 다이제스트를 계산하고 PKCS#7 서명을 생성하여 패키지의 DigitalSignature 스트림에 기록합니다. SHA-1, SHA-256, SHA-384, SHA-512가 모두 지원되며, RFC 3161 타임스탬프는 인증서가 만료된 후에도 서명을 유효하게 유지합니다.

패치 패키지(.msp)도 동일한 방식으로 서명됩니다. AppendSignature를 설정하면, signtool /as가 EXE에 서명을 추가하는 방식과 마찬가지로 이미 서명된 패키지가 첫 번째 서명 옆에 두 번째 서명을 받게 됩니다.

MSIX 또는 APPX 패키지 서명하기

MSIX는 블록 맵이 있는 ZIP 파일이며, Windows가 서명하는 것은 파일의 해시가 아니라 그 구성 요소들에 대한 다이제스트로 이루어진 작은 구조체입니다. TsgcAppxSigner는 이 구조체를 생성하고 서명한 뒤 결과를 패키지 안에 AppxSignature.p7x로 기록합니다. 패키지가 아직 서명 파트를 선언하지 않았다면 새로 추가됩니다. Windows는 패키지에 대해 SHA-256만 허용하므로 제공되는 다이제스트는 이것뿐이며, 이미 서명된 패키지에 다시 서명하면 이전 서명을 대체합니다.

게시자 확인

이것은 오후 시간을 통째로 날리게 만드는 실수입니다. 패키지 매니페스트의 Publisher는 서명 인증서의 주체(subject)와 정확히 일치해야 합니다. 두 값이 다르면 Windows는 서명이 잘못되었다고 보고하지 않습니다. 대신 패키지를 인식하지 못하게 되고, 확인할 수 없는 파일 형식이라고 알리는데, 이는 마치 빌드가 손상된 것처럼 보입니다.

TsgcAppxSigner는 서명하기 전에 이를 확인하며, 두 값을 모두 표시하는 메시지와 함께 중단됩니다.

> SignInstaller.exe DemoApp-wrongpublisher.msix out.msix ..\certs\demo.pfx demo
Publisher  : CN=Someone Else
Certificate: CN=sgcSign Demo Code Signing,O=eSeGeCe Demo
Error: APPX: the signing certificate subject "O=eSeGeCe Demo, CN=sgcSign Demo Code Signing"
does not match the Publisher declared in AppxManifest.xml ("CN=Someone Else"). Windows
requires them to be identical and refuses a package where they differ; signtool rejects
the same combination with 0x8007000B. Sign with a certificate whose subject matches the
manifest, or rebuild the package with the Publisher set to the certificate subject.

이 비교는 문자열이 아니라 속성 유형과 값으로 이루어지므로, 매니페스트와 인증서 사이의 공백이나 순서 차이로 인해 잘못된 경고가 발생하지 않습니다. ValidatePublisher(기본값 True)를 사용하면 이 확인을 끌 수 있으며, sgcAppxReadPublishersgcAppxDNMatches를 사용하면 예를 들어 서명 단계에 도달하기 전에 빌드를 실패시키는 등 동일한 비교를 직접 실행할 수 있습니다.

결과 확인하기

> SignInstaller.exe DemoApp.msi DemoApp-signed.msi ..\certs\demo.pfx demo http://timestamp.digicert.com
Signed: DemoApp-signed.msi

> SignInstaller.exe DemoApp.msix DemoApp-signed.msix ..\certs\demo.pfx demo http://timestamp.digicert.com
Publisher  : CN=sgcSign Demo Code Signing, O=eSeGeCe Demo
Certificate: CN=sgcSign Demo Code Signing,O=eSeGeCe Demo
Signed: DemoApp-signed.msix

> Get-AuthenticodeSignature DemoApp-signed.msi, DemoApp-signed.msix |
    Format-List Path, @{n='Signer';e={$_.SignerCertificate.Subject}},
                      @{n='TSA';e={$_.TimeStamperCertificate.Subject}}

Path   : DemoApp-signed.msi
Signer : CN=sgcSign Demo Code Signing, O=eSeGeCe Demo
TSA    : CN=DigiCert SHA256 RSA4096 Timestamp Responder 2026 1, O="DigiCert, Inc.", C=US

Path   : DemoApp-signed.msix
Signer : CN=sgcSign Demo Code Signing, O=eSeGeCe Demo
TSA    : CN=DigiCert SHA256 RSA4096 Timestamp Responder 2026 1, O="DigiCert, Inc.", C=US

Windows에서 signtool verify /pa /v를 실행하면 두 파일 모두에서 동일한 서명자와 DigiCert 타임스탬프를 찾아내며, 계산되는 다이제스트는 실제로 서명된 것과 일치합니다. 유일한 불만은 0x800B010A인데, 이는 데모 체인이 해당 머신에 설치되지 않은 자체 서명 테스트 루트로 끝나기 때문입니다. 다이제스트나 서명이 손상되었다면 대신 0x80096010이 나타났을 것입니다. TsgcAuthenticodeVerifier는 설치 프로그램 패키지와 MSIX 패키지를 스스로 인식하므로, EXE를 확인하는 것과 동일한 호출로 이 파일들도 확인할 수 있습니다.

서버와 명령줄에서

sgcSign Server는 /api/v1/sign/msi/api/v1/sign/appx에서 두 형식 모두에 서명하며, 명령줄 도구는 --format msi--format appx를 사용합니다. 두 방식 모두 해시 전용 경로도 제공하므로 대용량 설치 프로그램이 네트워크를 거치지 않아도 되며, 이는 이 시리즈의 다음 글에서 다룰 주제입니다.

제공 현황

TsgcMSISignerTsgcAppxSigner는 서버 경로 및 명령줄 형식과 함께 Delphi와 C++Builder용 sgcSign 2026.10에 포함되어 제공됩니다. Windows뿐만 아니라 Linux, macOS, iOS, Android에서도 빌드하고 실행할 수 있습니다. 모든 속성은 sgcSign 온라인 도움말에 문서화되어 있습니다.

서명되지 않거나 설치되지 않는 패키지에 대해 궁금한 점이 있으신가요? 문의하기를 이용해 주시면 코드를 작성한 사람들이 직접 답변해 드립니다.