如何在 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,这是因为演示证书链的末端是一个未安装在本机上的自签名测试根证书。如果摘要或签名被破坏,则会显示 0x80096010TsgcAuthenticodeVerifier 能够自行识别安装程序包和 MSIX 包,因此验证 EXE 的同一个调用也可以验证这些文件。

在服务器和命令行中

sgcSign Server 在 /api/v1/sign/msi/api/v1/sign/appx 上为这两种格式签名,命令行工具则使用 --format msi--format appx。两者都还提供仅哈希路由,因此大型安装程序永远不需要经过网络传输,这是本系列下一篇文章的主题。

可用性

TsgcMSISignerTsgcAppxSigner 随 sgcSign 2026.10 一起为 Delphi 和 C++Builder 提供,同时提供服务器路由和命令行格式。它们不仅可在 Windows 上构建和运行,也可在 Linux、macOS、iOS 和 Android 上构建和运行。每个属性都记录在 sgcSign 在线帮助中。

对无法签名或无法安装的包有疑问吗?请与我们联系,编写代码的人员将亲自回复您。