Skip to content

GitHub Actions 自动化签名与可信发布 ​

使用 GitHub 托管的 Windows Runner 构建应用,通过 VSign 云端密钥签名,验证发布者和时间戳,再交付同一份已验证文件。本教程提供可下载的签名 Action、两种工作流和最小 .NET 测试程序。

流程为:构建 → 校验 CLI → 临时恢复操作员 → 云签 → 补齐时间戳并验签 → 生成报告与摘要 → 打包发布。任何签名、时间戳或验签错误都会阻止制品上传。

与 GitLab CI 常驻 Runner 不同,本方案每次使用新的 windows-2022 Runner,临时恢复操作员 PFX。操作员 PFX 是云签认证身份,不是云端代码签名私钥的导出文件,仍须作为敏感凭据保管。

典型架构:构建、远程签名与发布 ​

GitHub Actions 编排客户的流水线,构建与验签在 Windows Runner 中执行;VSign 由 TrustAsia 远程提供,代码签名私钥保留在云端。

  • Runner 部署位置可选:本文下载示例使用 GitHub 云端的 windows-2022。自托管 Runner 可部署在客户自己的云主机或本地服务器,需另行配置 Windows 工具链、网络和凭据隔离,不直接套用临时 Runner 的清理假设。GitHub 自托管 Runner 说明
  • 签名服务独立于构建环境:Runner 安装 CLI 并持有操作员认证凭据,通过 HTTPS 调用 VSign;代码签名私钥不导出到 Runner。时间戳服务使用独立地址。
  • 验签通过才交付:签名、时间戳或验签失败即停止上传。图中虚线表示后续 CD 发布,本文示例止于 Artifact 上传;Release 发布按第 6 节接入,复用同一份已签名制品,不重新构建。

1. 下载示例 ​

下载 完整示例包,解压到测试仓库根目录。保留 .github 目录层级;包内不含 CLI 二进制、PFX、密码或 PIN。

文件保存位置用途
action.yml.github/actions/vsign-sign/action.yml下载工具、恢复身份、签名、验签和清理
verify-vsign-exe.yml.github/workflows/verify-vsign-exe.yml首次测试:生成自包含单文件 EXE
build-sign.yml.github/workflows/build-sign.yml多文件 .NET 应用:签名后打包完整发布目录
Program.cs 与 项目文件src/VSignSmokeTest/最小 .NET 8 控制台测试程序

运行前提

示例沿用配套 CLI 的 vsign 子命令和文件操作员存储接口,适用于支持 SSIGNCODE_OPERATOR_STORE=file 的交付版本。需要准备匹配版本的 ssigncode.exe、可用的云签证书与操作员、RFC 3161 时间戳服务和 Windows SDK SignTool。

请先运行测试工作流完成环境验收,再接入正式发布。CLI SHA-256 校验确认工具文件未变,不代表不同版本的命令接口自动兼容。

2. 准备云签身份与 CLI ​

在 CertCloud 找到已签发的云签代码签名证书订单,创建用于 CI 的操作员,设置云签密钥 PIN。按交付流程在可信 Windows 电脑上安装操作员,取得操作员 PFX、解密密码及其证书指纹。

信息用途
安装 Token初次安装操作员,不是证书指纹
操作员 PFX 和密码Runner 恢复认证身份;PFX 应包含私钥且处于有效期内
操作员证书指纹验证恢复的是正确操作员
代码签名证书指纹指定云签证书,并核对最终软件发布者
云签密钥 PIN授权云端签名操作

从 VSign 官网 获取客户端。交付手册中的 CLI 路径为 %LOCALAPPDATA%\Programs\vsigntoolui\binaries\ssigncode.exe;以实际安装目录和交付版本为准。

在可信电脑上计算工具摘要:

powershell
Get-FileHash -LiteralPath 'C:\tools\ssigncode.exe' -Algorithm SHA256

在当前私有仓库创建工具 Release,标签例如 vsign-cli-v1,只上传 ssigncode.exe。将经核对的 SHA-256 保存为下一节的环境变量。Action 下载后先校验摘要,再执行工具;下载失败或摘要不符立即停止。

本例使用当前仓库的 GITHUB_TOKEN 读取 Release。若把 CLI 放在另一个私有仓库,需要另外配置访问方式,当前仓库 Token 不会自动获得跨仓库权限。GitHub Token 权限

3. 配置受保护的签名环境 ​

在 Settings → Environments 创建 release-signing,并完成:

  1. 将 main 配置为受保护分支,限制合并和工作流修改权限。
  2. Environment 仅允许计划用于签名的 main 分支和应用发布标签。
  3. 正式标签设置保护规则,只允许发布负责人为经过审查的提交创建标签。
  4. 套餐支持时,配置 Required reviewers 和禁止自审。

两个示例都检查 github.ref_protected。 手动测试只允许受保护的 main;标签签名只接受受保护的应用标签。未保护引用时,作业会跳过,不会尝试取得签名凭据。

私有仓库的 Environment 和 Secrets 需要相应套餐;Free、Pro、Team 的 Required reviewers 仅适用于公开仓库。没有此功能时,应使用组织已有的审批流程,不能把点击 Run workflow 当作独立审批。环境与审批能力

Environment Secrets ​

名称内容
VSIGN_OPERATOR_PFX_B64操作员 PFX 的 Base64
VSIGN_OPERATOR_PWDPFX 解密密码
VSIGN_KEY_PIN云签密钥 PIN

在保存 PFX 的可信 Windows 电脑上执行,直接复制 Base64 到剪贴板:

powershell
$pfxPath = 'C:\vsign\operator.pfx'
if (-not (Test-Path -LiteralPath $pfxPath -PathType Leaf)) { throw '未找到操作员 PFX。' }
[Convert]::ToBase64String([IO.File]::ReadAllBytes($pfxPath)) | Set-Clipboard

粘贴到 Secret 后清理剪贴板。Base64 不是加密,不打印到终端、不写入仓库。PFX 密码与云签 PIN 即使相同,也分别维护;检查单个 Secret 的大小限制。Secrets 使用说明

Environment Variables ​

名称内容
VSIGN_SERVER实际 HTTPS 云签地址;本交付方案使用 https://vsign.trustasia.com
VSIGN_OPERATOR_SHA1操作员证书指纹,不是安装 Token
VSIGN_CERT_HASH代码签名证书指纹
VSIGN_TOOL_RELEASE工具 Release 标签,例如 vsign-cli-v1
VSIGN_TOOL_SHA256经核对的 CLI SHA-256,64 位十六进制
VSIGN_TIMESTAMP_URL可访问的 RFC 3161 时间戳地址,不能填云签 API 地址

两类证书指纹都是 40 位十六进制标识;文件签名和时间戳使用 SHA-256。不要复制其他项目的指纹或工具摘要。

4. 先运行单文件测试 ​

将示例包提交到受保护的 main 后,在 Actions → Verify standalone VSign EXE → Run workflow 选择 main。环境需要审批时,先完成审批。

单文件示例生成自包含的 Windows x64 EXE,并纳入本机运行库,避免只上传 EXE 却缺少运行依赖。签名后运行这个最小测试程序,再次确认文件摘要与验签时一致,最后上传:

  • VSignSmokeTest.exe:已签名的自包含测试程序。
  • VSignSmokeTest.exe.signature.json:签名者、时间戳、最终摘要、工具摘要、源码提交和运行链接。
  • SHA256SUMS.txt:最终 EXE 的 SHA-256。
查看完整单文件测试工作流
yaml
name: Verify standalone VSign EXE
'on':
  workflow_dispatch: null
permissions:
  contents: read
concurrency:
  group: vsign-release-signing
  cancel-in-progress: false
jobs:
  build-sign-verify:
    if: github.ref_protected && (github.ref == 'refs/heads/main' || (github.event_name == 'push' && startsWith(github.ref,
      'refs/tags/v')))
    runs-on: windows-2022
    environment: release-signing
    timeout-minutes: 30
    defaults:
      run:
        shell: pwsh
    steps:
    - name: Check out reviewed source
      uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803
      with:
        persist-credentials: false
    - name: Set up .NET 8
      uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1
      with:
        dotnet-version: 8.0.x
    - name: Build application
      run: |
        $ErrorActionPreference = 'Stop'
        dotnet publish src/VSignSmokeTest/VSignSmokeTest.csproj `
          --configuration Release --runtime win-x64 --self-contained true `
          -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true `
          -p:DebugType=None -p:DebugSymbols=false `
          --output dist/unsigned
        if ($LASTEXITCODE -ne 0) { throw 'dotnet publish failed.' }
        if (-not (Test-Path -LiteralPath 'dist/unsigned/VSignSmokeTest.exe' -PathType Leaf)) { throw 'Build output not found.' }
    - name: Sign and verify EXE
      id: vsign
      uses: ./.github/actions/vsign-sign
      with:
        input-file: dist/unsigned/VSignSmokeTest.exe
        output-file: dist/signed/VSignSmokeTest.exe
        server: ${{ vars.VSIGN_SERVER }}
        signing-cert-sha1: ${{ vars.VSIGN_CERT_HASH }}
        operator-cert-sha1: ${{ vars.VSIGN_OPERATOR_SHA1 }}
        tool-release: ${{ vars.VSIGN_TOOL_RELEASE }}
        tool-sha256: ${{ vars.VSIGN_TOOL_SHA256 }}
        timestamp-url: ${{ vars.VSIGN_TIMESTAMP_URL }}
        operator-pfx-base64: ${{ secrets.VSIGN_OPERATOR_PFX_B64 }}
        operator-pfx-password: ${{ secrets.VSIGN_OPERATOR_PWD }}
        key-pin: ${{ secrets.VSIGN_KEY_PIN }}
        github-token: ${{ github.token }}
    - name: Run verified demo and prepare release files
      env:
        VERIFIED_SHA256: ${{ steps.vsign.outputs.file-sha256 }}
      run: |
        $ErrorActionPreference = 'Stop'
        $target = 'dist/signed/VSignSmokeTest.exe'
        & $target
        if ($LASTEXITCODE -ne 0) { throw 'The signed smoke-test application failed.' }
        $hash = (Get-FileHash -LiteralPath $target -Algorithm SHA256).Hash
        if ($hash -ne $env:VERIFIED_SHA256) { throw 'The signed file changed after verification.' }
        "$hash  VSignSmokeTest.exe" | Set-Content dist/signed/SHA256SUMS.txt -Encoding utf8
    - name: Upload verified release files
      uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
      with:
        name: vsign-standalone-${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }}
        path: |
          dist/signed/VSignSmokeTest.exe
          dist/signed/VSignSmokeTest.exe.signature.json
          dist/signed/SHA256SUMS.txt
        if-no-files-found: error
        retention-days: 7

外部 Actions 已固定为核对过的提交 SHA,分别对应 checkout v6、setup-dotnet v5、upload-artifact v7。升级时更新经过审查的提交,不改回浮动标签。示例使用 .NET 8.0.x;如需固定构建环境,再用 global.json 锁定 SDK 补丁版本。Actions 安全使用建议 · .NET 单文件部署

Action 如何判定成功 ​

Action 的认证参数全部通过 with 传入,并只在内部需要的步骤映射为环境变量,不写入跨步骤共享的 GITHUB_ENV。每次调用创建独立的随机临时目录。

  1. 输入必须是工作区内未签名的 EXE、DLL 或 MSI;输出必须是同类型、不同路径且尚不存在的文件。示例不覆盖旧产物,也不接受符号链接或目录联接。
  2. 校验 CLI 摘要、PFX 私钥、操作员证书指纹和有效期,再调用云签。
  3. 要求 Get-AuthenticodeSignature 返回 Valid,签名者等于配置的代码签名证书。
  4. CLI 没有生成时间戳时,由 SignTool 使用 VSIGN_TIMESTAMP_URL 补齐;已有时间戳则保留并验证。
  5. 执行 signtool verify /pa /all /v /tw,非零退出码包括警告都判为失败,再检查最终签名者和时间戳,写入报告。

成功后只输出已验证文件的信息。失败时删除本次生成的不合格输出,清理步骤无论成功或失败都会移除本次临时 PFX 和 CLI。Runner 被强制终止时清理不保证执行,仍需依赖临时环境销毁和操作员凭据生命周期管理。

CLI 的现有接口通过命令参数接收 PIN;示例不打印展开后的命令或原始签名日志。不要开启会记录进程参数的调试采集,或在共享 Runner 上运行此凭据方案。

5. 接入真实应用 ​

build-sign.yml 展示普通多文件 .NET 应用:保留完整 publish 目录,包括 .deps.json、运行配置和依赖,只用已验证的签名文件替换待发布 EXE,再从打包目录运行测试程序。

此示例为依赖 .NET 8 运行时的应用,接收方需要安装对应运行时;若要独立运行,使用上一节的自包含方案。应用包含其他需签名 DLL/MSI 时,为每个文件单独调用 Action;只签一个 EXE 不代表依赖文件也具有 Authenticode 签名。

多文件示例上传 VSignSmokeTest-win-x64.zip 及 ZIP 摘要。ZIP 内含完整应用、签名报告和文件摘要清单,不通过随意挑选几个文件来打包。

调用接口 ​

下列参数名与下载的 action.yml 一致;无需自行猜测 env 到 Action 的映射:

yaml
- name: Sign application
  id: vsign
  uses: ./.github/actions/vsign-sign
  with:
    input-file: dist/unsigned/YourApp.exe
    output-file: dist/signed/YourApp.exe
    server: ${{ vars.VSIGN_SERVER }}
    signing-cert-sha1: ${{ vars.VSIGN_CERT_HASH }}
    operator-cert-sha1: ${{ vars.VSIGN_OPERATOR_SHA1 }}
    tool-release: ${{ vars.VSIGN_TOOL_RELEASE }}
    tool-sha256: ${{ vars.VSIGN_TOOL_SHA256 }}
    timestamp-url: ${{ vars.VSIGN_TIMESTAMP_URL }}
    operator-pfx-base64: ${{ secrets.VSIGN_OPERATOR_PFX_B64 }}
    operator-pfx-password: ${{ secrets.VSIGN_OPERATOR_PWD }}
    key-pin: ${{ secrets.VSIGN_KEY_PIN }}
    github-token: ${{ github.token }}
Action 输出含义
signed-file已验证文件的绝对路径
signer-thumbprint已验证的发布者证书指纹
file-sha256最终文件摘要,时间戳处理已完成
report-fileJSON 验签报告路径

替换构建命令、文件名、打包及测试步骤时保持这些约束:

  • 先 checkout,再调用本地 Action;每次调用使用唯一 step id 和输出路径。
  • 只把签名输入传给 Action,不将 PFX、密码或 PIN 放在 job 全局环境或制品目录。
  • 示例中的运行步骤仅用于最小控制台程序。真实 GUI、安装程序或其他应用应替换为合适的非交互测试,不盲目运行所有输出文件。
  • 发布内容来自同一次成功签名运行,不再重新编译;打包前核对最终 EXE 摘要仍与 Action 输出相同。
  • 签名、验签和打包都不使用 continue-on-error;上传只在前序成功时执行,always() 仅用于清理。

build-sign.yml 的标签模式是 v[0-9]*,用于应用标签,如 v1.0.0。工具标签 vsign-cli-v1 不会触发应用签名。两个工作流使用相同并发组限制同时签名的任务,这不是保证先进先出的发布队列。

6. 发布与接收方核验 ​

Artifact 是有保留期限的运行产物;正式版本可通过 GitHub Release 分发同一份文件。以下三层信息作用不同:

信息验证什么
Authenticode 和可信时间戳预期发布者、签名完整性及签名时间依据
SHA-256下载文件是否与已核对文件的字节一致
提交、运行记录及可选构建证明从哪个仓库和工作流生成,关联哪个提交

报告是审计记录,不能独立充当不可伪造的来源证明。文件和摘要若同时被替换,单纯比较摘要不能建立信任;签名也不保证软件没有漏洞或已取得 SmartScreen 信誉。

发布同一份已签名文件 ​

下载成功运行的 Artifact,解压后按 验签指南 再次检查文件,并核对 SHA-256。确认报告中的提交就是发布版本后,创建指向该提交的受保护应用标签。

单文件示例可用以下命令创建 Release 草稿;替换仓库和标签:

powershell
gh release create v1.0.0 `
  .\VSignSmokeTest.exe .\VSignSmokeTest.exe.signature.json .\SHA256SUMS.txt `
  --repo OWNER/REPOSITORY --verify-tag --draft --title 'v1.0.0'
if ($LASTEXITCODE -ne 0) { throw 'Release draft creation failed.' }

多文件示例则上传 ZIP 和 ZIP 摘要。--verify-tag 只确认标签存在,仍须核对它指向报告中的提交。人工核对草稿附件后再发布。GitHub CLI Release

需要自动发布时,只给独立的发布 job 授予 contents: write;它应依赖成功签名的 job,下载该运行的指定 Artifact,再发布。签名 job 保持 contents: read。示例本身只上传 Artifact,不会自动创建 Release。

可选构建来源证明 ​

可在签名之后为最终 EXE 生成 GitHub artifact attestation。签名或添加时间戳都会改变文件摘要,签名前的证明不能直接用于最终文件。

仅给生成证明的 job 授予所需的 id-token: write、attestations: write。私有或内部仓库需要 GitHub Enterprise Cloud;按 GitHub 官方配置 接入。

接收方还可运行:

powershell
gh attestation verify .\VSignSmokeTest.exe --repo OWNER/REPOSITORY
if ($LASTEXITCODE -ne 0) { throw 'Artifact attestation verification failed.' }

检查证明中的仓库、工作流和提交符合预期,再结合 Authenticode 与摘要判断。构建证明的作用与边界

7. 验收与排查 ​

先使用授权测试证书和测试项目验证以下场景,再切换正式签名证书:

场景预期结果与处理
作业被跳过检查是否选择受保护的 main,或使用允许的受保护应用标签
CLI Release 404核对仓库、标签、附件名和 Token 权限
CLI 摘要不符工具执行前失败;重新核对供应商交付及受控摘要
PFX 密码错误、缺少私钥、过期或指纹不符签名前失败,检查操作员身份
PIN 错误或云签授权不可用签名失败,不上传发布制品
签名状态非 Valid 或发布者错误验签失败,删除本次不合格输出
时间戳不存在或服务失败尝试补齐;补齐或严格验签失败时停止
输出已存在、路径相同或越出工作区拒绝操作;使用新的、不同的输出路径
连续调用两次 Action每次使用独立临时目录,不复用上一份 PFX 或状态
发布后下载重新验签、核对文件或 ZIP 摘要,按需验证构建证明

示例包的结构与离线失败分支检查不能替代真实 Windows Runner、交付 CLI、云签服务和时间戳的联调。排查时提供版本、运行链接和脱敏错误;不上传原始 PFX、Base64、密码、PIN 或安装 Token。