我如何将远程代码签名集成到团队CI/CD:经验分享

0 阅读6分钟

告别UKey插拔与手动签名,用云端HSM实现全自动签名流水线

为什么我要放弃传统UKey?

作为团队里的运维兼开发,我最头疼的事情之一就是代码签名。每次发版,都要从抽屉里翻出那个UKey,插到编译服务器上,输密码,手动拖文件签名……更崩溃的是,证书一年一换,UKey还容易丢。别的办公室要还要进行邮寄 。 去年CA/B论坛新规把证书有效期砍到460天,我彻底受不了了。

后来我们换成了远程代码签名服务——私钥保存在云端FIPS 140-3认证的硬件安全模块(HSM)里,永不导出。CI/CD流水线可以直接调用API签名,彻底解放双手。经过几个月的折腾,我把踩过的坑和总结的经验分享出来,希望对大家有帮助。


第一步:购买与申请 – 几个容易忽略的细节

购买时的“签名次数”怎么选?

锐成信息控制台,进入“远程代码签名”购买页面。除了选品牌(Sectigo、GlobalSign等),关键是要选签名次数

经验:我们项目每天构建20次左右,每次签名1个exe+2个dll,一年大约需要 20*3*365 ≈ 21900 次。建议按年构建次数 × 平均签名文件数 × 1.2(冗余)来买。买少了后续加购麻烦,买多了不会过期(有效期内用完为止)。

申请证书:CSR只能在密码机里生成,别想手动填

提交申请资料时,我发现一个和传统证书完全不同的地方:CSR只能由云端密码机自动生成,根本没有手动填写的选项。

一开始我还担心是不是功能不全,后来才知道这是为了私钥永不接触网络——密码机内部生成密钥对,只把CSR提交给CA,私钥永远出不来。你只需要填好组织信息、联系人,剩下的交给系统。

避坑:提交后状态会变成“待审核”,记得去邮箱完成企业验证(不同品牌流程略有差异)。我们第一次用Sectigo,差点漏掉一封要求盖章的确认邮件。


第二步:锐安信签名工具 – 运维日常签名够用了

证书签发后,我下载了官方的桌面签名工具——锐安信签名工具(支持Windows/Mac/Linux)。这个工具适合临时手动签名,比如测试版本、内部工具。

配置只需 AK/SK,不需要 Cert Code

打开工具,进入“设置”页面,只填两样东西:

  • Access Key (AK)
  • Access Secret (SK)

注意:工具会自动识别你账户下的有效证书。

这一点当时让我困惑,后来明白:锐安信签名工具以“用户”身份登录,直接关联账户里所有可用的远程签名证书;而命令行工具需要指定用哪张证书(因为一个账户可能有多个)。

使用体验

  • 拖拽文件到界面,支持批量。
  • 选择算法:我们只选 SHA256(Windows 7以上都支持),时间戳服务器用微软的 http://timestamp.acs.microsoft.com
  • 点“开始签名”,大概2-3秒一个文件。
  • 输出文件默认加 _signed 后缀,不会覆盖原文件。

小技巧:如果签名失败,检查网络是否能访问云端HSM的API端点(公司内网可能需加白名单)。


第三步:SignTool命令行 – 真正实现CI/CD自动化的核心

锐安信签名工具虽好,但我们的Jenkins流水线不能等人工操作。SignTool 是命令行版本,可以集成到任何脚本或编程语言中。

下载与环境变量

从同一页面下载 signtool 可执行文件(Linux下是二进制,Windows下是exe)。我们把它放在 /opt/signtool/ 目录,加入PATH。

安全第一,不要硬编码凭证。在Jenkins中配置凭据,然后在流水线中注入环境变量:

bash

export SIGNTOOL_ACCESS_KEY='AK...'
export SIGNTOOL_ACCESS_SECRET='SK...'
export SIGNTOOL_CERT_CODE='your-cert-code'   # 命令行必须指定证书

基础签名命令(我最常用的模板)

bash

./signtool sign \
  -k "$SIGNTOOL_ACCESS_KEY" \
  -s "$SIGNTOOL_ACCESS_SECRET" \
  -c "$SIGNTOOL_CERT_CODE" \
  -f unsigned.exe \
  -o signed.exe \
  --sha2=true \
  --timestamp-rfc3161 http://timestamp.acs.microsoft.com

参数解析

  • -c:证书订阅号,在控制台的“证书详情”里能找到。
  • --timestamp-rfc3161必须加,否则证书过期后签名会失效。
  • 布尔值参数(如 --sha2)必须用 =true/false 格式,不能只用空格。

集成到Node.js脚本(我们的Jenkins pipeline用这个)

const { execSync } = require('child_process');
const path = require('path');

function signBinary(filePath) {
  const signtool = '/opt/signtool/signtool';
  const cmd = `${signtool} sign \
    -k "${process.env.SIGNTOOL_ACCESS_KEY}" \
    -s "${process.env.SIGNTOOL_ACCESS_SECRET}" \
    -c "${process.env.SIGNTOOL_CERT_CODE}" \
    -f "${filePath}" \
    --sha2=true \
    --timestamp-rfc3161 http://timestamp.acs.microsoft.com`;
  
  console.log(`Signing ${filePath}...`);
  execSync(cmd, { stdio: 'inherit' });
}

const { execSync } = require('child_process');
const path = require('path');

function signBinary(filePath) {
  const signtool = '/opt/signtool/signtool';
  const cmd = `${signtool} sign \
    -k "${process.env.SIGNTOOL_ACCESS_KEY}" \
    -s "${process.env.SIGNTOOL_ACCESS_SECRET}" \
    -c "${process.env.SIGNTOOL_CERT_CODE}" \
    -f "${filePath}" \
    --sha2=true \
    --timestamp-rfc3161 http://timestamp.acs.microsoft.com`;
  
  console.log(`Signing ${filePath}...`);
  execSync(cmd, { stdio: 'inherit' });
}

踩坑记录:嵌套签名

有一次我们给一个已经签过名的DLL再次签名(因为要打包到新的EXE里),结果签名失败。后来查文档发现需要用 --nest=true

--nest=true   # 保留原有签名,添加新签名

如果不用这个参数,默认会清除旧签名,导致文件签名链断裂。


第四步:与CI/CD工具集成的实战经验

Electron Builder – 自动签名Windows安装包

我们的Electron应用以前是构建后手动用UKey签名,改成远程签名后,只需在 electron-builder.mjs 里加一个自定义签名函数:

async function customSign(configuration) {
  const { SIGNTOOL_ACCESS_KEY, SIGNTOOL_ACCESS_SECRET, SIGNTOOL_CERT_CODE } = process.env;
  const signtoolPath = './signtool/signtool';  // 相对路径
  
  const cmd = `${signtoolPath} sign \
    -k "${SIGNTOOL_ACCESS_KEY}" \
    -s "${SIGNTOOL_ACCESS_SECRET}" \
    -c "${SIGNTOOL_CERT_CODE}" \
    -f "${configuration.path}" \
    --nest=true \
    --sha2=true \
    --timestamp-rfc3161 http://timestamp.acs.microsoft.com`;
  
  execSync(cmd, { stdio: 'inherit' });
}

export default {
  win: { signtoolOptions: { sign: customSign } }
};

效果:每次执行 npm run dist,生成的NSIS安装包和主程序都会自动签名,全程无人工干预。

Advanced Installer – 制作MSI安装包

我们用Advanced Installer打包Windows服务。集成时需要注意几个细节:

  1. 数字签名设置

    • 签名工具选“定制”

    • 路径指向 signtool.exe

    • 命令行输入(注意开头是两个 sign):

      sign sign --access-key="xxx" --access-secret="xxx" --cert-code="xxx" --nest=true --sha1=false --sha2=true --timestamp-rfc3161=http://timestamp.acs.microsoft.com --desc="[|ProductName]" --override=true --file
      

      为什么是 sign sign?第一个 sign 是子命令,第二个 sign 是动作。这是工具的设计,照着写就行。

  2. 修改压缩方式:在“构建”页面,把“将安装文件归档为CAB文件”改成 “使用LZMA压缩归档安装文件” 。不这么做的话,签名后MSI会损坏。

  3. 验证:在“数字签名” → “已配置用于签名的文件”里,能看到待签名的文件列表,签名次数也会相应消耗。


第五步:审计与记录 

以前用UKey,谁签了什么文件、什么时候签的,全靠Excel手工记。远程签名自带审计功能:

登录控制台,进入“远程代码签名” → “签名记录”,可以看到:

  • 每次签名的时间戳
  • 签名文件的哈希值(SHA256)
  • 使用的是哪张证书
  • 签名结果(成功/失败)

支持导出CSV/JSON,财务对账、安全审计都很方便。


注意事项

  1. 时间戳服务器一定要稳:我们试过用Sectigo自己的时间戳,有时会很慢。换成微软的 http://timestamp.acs.microsoft.com 后稳定很多。
  2. 网络问题:云端HSM的API需要外网访问。如果编译服务器在隔离内网,记得申请开通白名单(端口443)。
  3. 签名次数优化:不要每个文件都单独签名一次。如果一次构建要签10个文件,就会消耗10次。我们改成只签最终产物(安装包),内部的DLL不做签名(或只做一次缓存)。
  4. 证书订阅号别弄混:如果你有多个证书(比如测试证书和生产证书),命令行里的 -c 参数一定要区分。我们通过Jenkins的参数化构建来动态传入。
  5. 布尔值参数格式:再次强调,--sha1=false 不能写成 --sha1 false,后者会被解析成两个参数导致错误。

总结

从插拔UKey的痛苦,到全自动化的远程签名,这半年的转变让我深刻体会到:工具选对了,效率提升十倍。远程代码签名不仅解决了硬件管理的麻烦,还让CI/CD真正实现了“从代码提交到发布签名”的一键完成。

如果你也在为传统代码签名发愁,不妨试试这条路。更多集成场景可以看他们官方的集成指南。希望我的经验能让你少踩一些坑!