签名服务器把私钥集中保存在一处,这正是使用签名服务器的意义所在。通常为此付出的代价是文件本身:构建代理上传安装程序,服务器对其签名后再把整个文件传回。对于几个 GB 大小的安装程序来说,这意味着为了添加几 KB 的签名,文件要在网络上往返两次。
其实不必如此。Authenticode 签名的对象从来都不是文件本身,而是文件的摘要,而摘要可以在文件已经所在的地方就地计算。从 sgcSign 2026.10 起,sgcSign Server 像此前对 EXE 所做的那样,仅凭摘要即可为 Windows Installer 包以及 MSIX 或 APPX 包签名。
通过 sgcSign Server 对一个 1 GB 的安装程序签名两次,一次通过上传,一次仅凭其哈希,并统计了线路上传输的每一个字节。YouTube 上同步发布。
实际被签名的是什么
Authenticode 签名覆盖的是摘要,每种格式各自定义自己的摘要方式。对 EXE 而言,是排除校验和与证书表之后的 PE 映像哈希。对 MSI 而言,是按固定顺序对复合文件各个流计算得到的摘要。对 MSIX 而言,是对包内各部分分别计算的一组 SHA-256 摘要构成的小数据块。服务器只需要这个值,别无其他:它用自己持有的密钥对摘要签名,再由客户端把签名写入文件。
从命令行使用
命令行工具默认对 --format msi 和 --format appx 使用哈希路由,就像此前对 --format authenticode 所做的那样,而 --upload 会改为发送整个文件。演示对同一台机器上的服务器,用两种方式对同一个 1 GB 的包签名,中间经过一个小型中继,统计工具收发的每一个字节。该包内含 1 GiB 随机数据,因此传输途中无法被压缩。
> 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
两个文件都会带着签名返回,并且携带相同的摘要:signtool verify /v 在两者上都显示 Hash of file (sha256): 9151F2EE…5A0CDDD,这与 [prehash] 行中的值一致,同时还带有演示签名者信息与 DigiCert 时间戳。
数据对比
| 上传路由 | 哈希路由 | |
|---|---|---|
| 包大小 | 1,075,355,648 字节 | 1,075,355,648 字节 |
| 发送到服务器 | 1,075,356,521 字节 | 477 字节 |
| 从服务器接收 | 1,075,364,108 字节 | 11,044 字节 |
| 线路耗时 | 14.1 s | 0.18 s |
| 整个命令耗时 | 16.3 s | 4.8 s |
这次测试通过回环网络进行,在回环网络上传输字节几乎不产生成本,即便如此,上传路由仍然为此花费了 14 秒。在真实网络链路上,传输本身就是全部成本:以 100 Mbit/s 的速度,上传路由的 2.15 GB 数据需要接近三分钟,而哈希路由仍然只发送 477 字节。哈希路由 4.8 秒中剩下的部分,是在构建代理上准备并计算 1 GB 包的哈希所花的时间,这项工作上传路由同样要做,只不过是在服务器上完成的。
在 Delphi 中实现同样的流程
在 Delphi 构建工具中,这一流程分为四步:准备包、计算已准备包的哈希、发送哈希,以及嵌入返回的签名。HTTP 调用可以使用项目中已经在用的任何客户端。这里使用的是 RTL 的 THTTPClient 和 System.JSON。
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.
同一台服务器返回了一个 7,941 字节的 PKCS#7,signtool 读取该结果的方式与上面两个文件完全相同。
先准备,再计算哈希
安装包是以最终交付时的形态被签名的,因此在计算哈希之前必须先把它转换成那个形态。sgcMSIPrepareForSigning 和 sgcAppxPrepareForSigning 完成这一步骤,规则很简单:对 prepare 生成的结果计算哈希,并把签名嵌入到同一个已准备的包中,而不是原始文件中。如果改为对原始文件计算哈希,签名所覆盖的字节就已经不存在了。
MSIX:自行核对发布者
在哈希路由中,Windows 严格检查的这一项校验,双方都只能看到其中一半。服务器持有证书,但从不看到清单文件;客户端持有清单文件,但从不看到证书。因此每个哈希响应都会携带 signer_subject,也就是签名证书的主体。在嵌入签名之前,请用 sgcAppxReadPublisher 和 sgcAppxDNMatches 将其与包的 Publisher 进行比对。命令行工具会自动完成这一步,一旦两者不一致就会拒绝写入该包。
什么情况下仍需要上传
完整文件路由依然存在。当客户端自身无法计算摘要,或者签名需要审批时,请使用这些路由,因为审批流程只支持完整文件上传。--upload 会让命令行工具在这一次运行中改用该路由。
适用范围
面向安装包的哈希路由 /api/v1/sign/msi/hash 和 /api/v1/sign/appx/hash、命令行工具默认优先使用哈希路由的设置,以及 prepare、hash、embed 相关函数,均已在面向 Delphi 和 C++Builder 的 sgcSign 2026.10 中提供。这些路由已在 sgcSign 在线帮助中列出文档。
正在从构建集群中为大型软件包签名?联系我们,您会收到编写这段代码的人的回复。
