ssign Version française

Code signing in GitHub Actions with Certum SimplySign

ssign runs on a Linux runner and signs Windows binaries there: no Windows runner, no SimplySign Desktop, no container. The account e-mail and the TOTP seed come from environment secrets.

A signing workflow

name: Sign

on:
  workflow_dispatch:        # manual only — each run performs a real cloud signature

jobs:
  sign:
    runs-on: ubuntu-latest
    environment: signing    # ← protected environment; see next section
    steps:
      - uses: actions/checkout@v4
      - run: cargo install ssign
      - run: ssign dist/*.exe
        env:
          CERTUM_EMAIL: ${{ secrets.CERTUM_EMAIL }}
          CERTUM_OTP:   ${{ secrets.CERTUM_OTP }}
      - uses: actions/upload-artifact@v4
        with: { name: signed, path: dist/ }

CERTUM_OTP holds your TOTP seed, the long-lived secret that lets anyone sign as you. Store it as described below, not as a plain repository secret.

Require the owner's approval for every signing run

Signing uses your certificate and, if mishandled, can expose the seed. Gate it behind a GitHub Environment, so that the repository owner must approve every signing run before the job can even read the secrets:

  1. Settings → Environments → New environment, name it signing.
  2. Add a Deployment protection rule → Required reviewers, and add yourself (the owner). Any job that targets this environment now pauses for your approval.
  3. Store CERTUM_EMAIL and CERTUM_OTP as environment secrets of signing (not repository secrets). They are readable only inside an approved run of a job that declares environment: signing.
  4. In the workflow, the signing job declares environment: signing, as above.

Result: a pull request, even a malicious one, cannot trigger a signature or read the seed; every run waits for the owner's click. Also keep the signing job on workflow_dispatch or protected-branch pushes, never on pull_request from forks.

In a release pipeline

Make signing a job in the same workflow as the build and the release, gated by environment: signing, with the release job listing it in needs:. A tag then becomes build → approve → signed release. Do not rely on a separate workflow triggered by release: published: a release created by the built-in GITHUB_TOKEN does not trigger other workflows.

A real example: ssign signs its own Windows release

ssign's release workflow does exactly this. On each version tag, a job on ubuntu-latest takes the Linux ssign binary built earlier in the same run, waits for approval through the signing environment, signs the Windows ssign.exe and ssign_pkcs11.dll with a single Certum login, then checks both with osslsigncode verify. The release job waits for it. Abridged:

  sign-windows:
    needs: build
    if: startsWith(github.ref, 'refs/tags/v')
    runs-on: ubuntu-latest
    environment: signing
    steps:
      # …download the Linux ssign and the Windows artifacts built earlier in the run…
      - name: Sign the Windows binary + module with ssign (via Certum)
        env:
          CERTUM_EMAIL: ${{ secrets.CERTUM_EMAIL }}
          CERTUM_OTP: ${{ secrets.CERTUM_OTP }}
        run: |
          unzip -o dl/ssign-windows-x86_64.zip -d dl
          unzip -o dl/ssign-pkcs11-windows-x86_64.zip -d dl
          # One Certum login signs both the CLI and the module.
          ssign --verbose \
            -n "ssign" -u "https://github.com/${{ github.repository }}" \
            dl/ssign.exe dl/ssign_pkcs11.dll

  release:
    needs: [build, sign-windows, sign-apple]

The full file: .github/workflows/ci.yml.

Several files, one login

Pass every file to one ssign call, as above: it logs in once for the whole batch. If a later step signs other formats with the PKCS#11 module in the same job, the module reuses the cached session instead of logging in again (see the session cache).