Centralized Authenticode signing service for iyulab Windows artifacts.
This repository provides two ways to sign Windows binaries with the iyulab GlobalSign EV code signing certificate:
Call the composite action directly from any iyulab repo's workflow. Signs files in place on the caller's runner:
jobs:
build:
runs-on: windows-latest
environment: production # Required for OIDC federated credential subject match
permissions:
id-token: write # Required for OIDC
contents: read
steps:
- run: dotnet publish ... -o out
- uses: iyulab/code-sign@main
with:
files: |
out/MyApp.exe
out/MyApp.dllPrerequisites for this path:
- Caller repo must be granted an Azure AD federated credential with subject
repo:iyulab/<caller>:environment:production - Caller repo must have a
productionenvironment id-token: writepermission on the calling job- The
productionenvironment must be declared on the job
Send a repository_dispatch event with asset names already attached to a draft GitHub Release. The service downloads, signs, and replaces the assets. Useful when the caller doesn't want to modify its build jobs (only its publish step).
The Azure Key Vault credentials never leave this repository. Caller repos do not need their own Azure AD configuration for path (2).
- Azure AD federated credentials (one per caller repo):
repo:iyulab/code-sign:environment:production— for the dispatch service pathrepo:iyulab/Filer-releases:environment:production— for the composite action path- Additional credentials added per caller repo as needed
- Every federated credential requires the caller to declare
environment: productionon the signing job — this is the OIDC claim gate - Composite action path: no secrets needed on caller side, only the federated credential
- Dispatch service path: explicit allowlist in
.github/workflows/sign-on-dispatch.yml+ iyulab org-levelRELEASE_TOKENfor cross-repo asset access - Payload validation (dispatch path only): tag format, asset name whitelist, path traversal prevention
Caller sends a repository_dispatch event with event_type: sign-request and the following payload:
{
"event_type": "sign-request",
"client_payload": {
"caller_repo": "iyulab/Filer-releases",
"release_tag": "v0.1.95",
"assets": ["Filer-setup.exe", "MyApp.exe"],
"request_id": "filer-releases-12345"
}
}| Field | Type | Validation |
|---|---|---|
caller_repo |
string | Must be in allowlist |
release_tag |
string | Must match ^v\d+\.\d+\.\d+(-[a-zA-Z0-9.]+)?$ |
assets |
string[] | Each must match ^[A-Za-z0-9._-]+\.(exe|dll|msi)$ |
request_id |
string | Alphanumeric + _-, max 64 chars (used for tracing) |
Each asset must already exist on the caller's release (typically a draft) before the dispatch is sent.
For each signed asset, the service uploads a companion marker file named <asset>.signed to the same release. Callers should poll for the marker to know when signing is complete.
Marker contents (small JSON):
{"asset":"Filer-setup.exe","signedAt":"2026-04-07T03:40:12.1234567+00:00","requestId":"filer-releases-12345"}Example: signing Filer-setup.exe → after completion, the release contains both Filer-setup.exe (now signed, with new SHA256) and Filer-setup.exe.signed.
Important: The signed asset's SHA256 differs from the unsigned original. Callers that publish hash manifests (e.g.
latest.json) must compute hashes AFTER the marker appears.
Open a PR to .github/workflows/sign-on-dispatch.yml, adding the new repo:
ALLOWED=("iyulab/Filer-releases" "iyulab/your-new-repo")The iyulab org secret RELEASE_TOKEN is used for both directions:
code-signuses it to download/upload release assets on the caller repo- Caller repo uses it to send
repository_dispatchtocode-sign
Confirm the new caller repo has access to the org secret (check Settings → Secrets → Organization secrets in the new caller).
Copy the canonical implementation from iyulab/Filer-releases — specifically these four jobs in release.yml:
draft-release— creates a draft GitHub release with unsigned artifacts attachedrequest-sign— sendsrepository_dispatchto this servicewait-for-signed— polls for.signedmarkers on the draft releasefinalize-release— re-computes hashes from signed binaries, regenerates manifests, promotes draft → published
Minimal example of the dispatch step in the caller's request-sign job:
- uses: peter-evans/repository-dispatch@v4
with:
token: ${{ secrets.RELEASE_TOKEN }}
repository: iyulab/code-sign
event-type: sign-request
client-payload: >-
{
"caller_repo": "iyulab/your-repo",
"release_tag": "${{ needs.version.outputs.tag }}",
"assets": ["MyApp-setup.exe"],
"request_id": "your-repo-${{ github.run_id }}"
}And the polling loop in wait-for-signed:
- env:
GH_TOKEN: ${{ secrets.RELEASE_TOKEN }}
TAG: ${{ needs.version.outputs.tag }}
run: |
for i in $(seq 1 60); do
if gh release view "$TAG" --repo iyulab/your-repo --json assets \
| jq -e '.assets[] | select(.name == "MyApp-setup.exe.signed")' > /dev/null; then
echo "Signed marker detected"; exit 0
fi
sleep 15
done
echo "::error::Timeout waiting for signature"; exit 1.exe— Windows executables (signed by AzureSignTool / signtool compatible).dll— Native and managed DLLs.msi— Windows Installer packages
For artifacts nested inside zip files, use the composite action path to sign binaries in place on the build runner BEFORE packaging. The dispatch service path only operates on top-level release assets.
The dispatching repo is not in ALLOWED array. See onboarding Step 1.
The release tag must follow strict semver: v1.2.3 or v1.2.3-prerelease. Numeric segments only, optional pre-release suffix with alphanumerics and dots.
Asset names must be alphanumeric + ._- with extension .exe, .dll, or .msi. No path separators, no .., no spaces.
The asset does not exist on the caller's release at the time of dispatch. Confirm the caller's draft-release job completed before request-sign runs.
Federated credential subject mismatch. Verify the credential in Azure AD has subject repo:iyulab/code-sign:environment:production and that the workflow job declares environment: production.
The Azure AD service principal lacks Key Vault Certificate User role on kv-codesign-iyulab.
GitHub rejects 0-byte uploads. The service workflow ensures markers contain JSON metadata; caller-side uploads must also be non-empty.
Inspect the most recent sign-on-dispatch.yml run in this repo for the actual error. Common causes: RELEASE_TOKEN access issue on the new caller repo, transient Azure timestamp server issue (re-run), AzureSignTool installation failure.
iyulab/Filer-releases is the first and canonical caller. See its .github/workflows/release.yml for the full integration pattern.
| File | Purpose |
|---|---|
action.yml |
Composite action for in-build signing (uses: iyulab/code-sign@main) |
.github/workflows/sign-on-dispatch.yml |
Dispatch service workflow (repository_dispatch handler) |
.github/workflows/_sign-test.yml |
Standalone signing smoke test (workflow_dispatch) |
.github/workflows/_self-test.yml |
E2E self-test of the dispatch handler (workflow_dispatch) |
The two _*.yml workflows are diagnostic tools and are not triggered automatically. They are safe to run manually to validate the signing infrastructure or debug issues.