Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 120 additions & 0 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,26 @@ on:
type: string
default: ""
description: "Path to a custom WXS file in the caller repo for MSI generation (relative to repo root). If not provided, uses default template."
windows_authenticode_signing:
required: false
type: boolean
default: false
description: "Whether to Authenticode-sign Windows .exe and .msi artifacts with Azure Trusted Signing. When true, the AZURE_CLIENT_ID/AZURE_TENANT_ID/AZURE_SUBSCRIPTION_ID secrets and the trusted_signing_* inputs are required."
trusted_signing_endpoint:
required: false
type: string
default: ""
description: "Azure Trusted Signing region endpoint (e.g. https://wus2.codesigning.azure.net/). Required when windows_authenticode_signing is true."
trusted_signing_account_name:
required: false
type: string
default: ""
description: "Azure Trusted Signing account name. Required when windows_authenticode_signing is true."
trusted_signing_certificate_profile:
required: false
type: string
default: ""
description: "Azure Trusted Signing certificate profile name. Required when windows_authenticode_signing is true."
secrets:
RELENG_GITHUB_TOKEN:
required: true
Expand All @@ -68,6 +88,15 @@ on:
GORELEASER_PRO_KEY:
required: false
description: "GoReleaser Pro license key for MSI builds. Required when msi is true."
AZURE_CLIENT_ID:
required: false
description: "Client ID of the Azure AD app used for Trusted Signing OIDC login. Required when windows_authenticode_signing is true."
AZURE_TENANT_ID:
required: false
description: "Azure AD tenant ID for Trusted Signing OIDC login. Required when windows_authenticode_signing is true."
AZURE_SUBSCRIPTION_ID:
required: false
description: "Azure subscription ID hosting the Trusted Signing account. Required when windows_authenticode_signing is true."

env:
CDN_BASE_URL: "https://dist.conductorone.com"
Expand Down Expand Up @@ -164,6 +193,53 @@ jobs:
exit 1
fi

- name: Validate Authenticode signing configuration
if: inputs.windows_authenticode_signing == true
env:
HAS_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID != '' }}
HAS_TENANT_ID: ${{ secrets.AZURE_TENANT_ID != '' }}
HAS_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID != '' }}
TS_ENDPOINT: ${{ inputs.trusted_signing_endpoint }}
TS_ACCOUNT: ${{ inputs.trusted_signing_account_name }}
TS_PROFILE: ${{ inputs.trusted_signing_certificate_profile }}
MSI_ENABLED: ${{ inputs.msi }}
run: |
if [ "$MSI_ENABLED" != "true" ]; then
echo "::error::windows_authenticode_signing requires msi to be true (the Windows build job is gated on msi)"
exit 1
fi

missing=0
require_secret() {
if [ "$2" != "true" ]; then
echo "::error::$1 secret is required when windows_authenticode_signing is true"
missing=1
fi
}
require_input() {
if [ -z "$2" ]; then
echo "::error::$1 input is required when windows_authenticode_signing is true"
missing=1
fi
}

require_secret "AZURE_CLIENT_ID" "$HAS_CLIENT_ID"
require_secret "AZURE_TENANT_ID" "$HAS_TENANT_ID"
require_secret "AZURE_SUBSCRIPTION_ID" "$HAS_SUBSCRIPTION_ID"
require_input "trusted_signing_endpoint" "$TS_ENDPOINT"
require_input "trusted_signing_account_name" "$TS_ACCOUNT"
require_input "trusted_signing_certificate_profile" "$TS_PROFILE"

if [ -n "$TS_ENDPOINT" ] && [[ ! "$TS_ENDPOINT" =~ ^https://[A-Za-z0-9.-]+/?$ ]]; then
echo "::error::trusted_signing_endpoint must be an https URL (e.g. https://wus2.codesigning.azure.net/). Got: $TS_ENDPOINT"
missing=1
fi

if [ "$missing" -ne 0 ]; then
exit 1
fi
echo "✅ Authenticode signing configuration valid"

determine-workflows-ref:
needs: validate-inputs
runs-on: ubuntu-latest
Expand Down Expand Up @@ -589,15 +665,54 @@ jobs:
WXS_PATH: ${{ steps.wxs.outputs.wxs_path }}
WORKFLOWS_REF: ${{ needs.determine-workflows-ref.outputs.ref }}
RELEASE_TAG: ${{ inputs.tag }}
AUTHENTICODE: ${{ inputs.windows_authenticode_signing }}
run: |
export BUILD_STARTED_ON=$(date -u +"%Y-%m-%dT%H:%M:%SZ")

# Authenticode signing is opt-in. When enabled, inject two blocks into
# the GoReleaser config (same envsubst block pattern used for Docker
# extra_files), so the template stays valid YAML when signing is off:
# - WINDOWS_BUILD_SIGN_HOOK: a build post-hook that signs the raw
# .exe BEFORE it is packaged into the zip and the MSI.
# - WINDOWS_AUTHENTICODE_MSI_SIGN: a `signs` entry that signs the
# .msi in place, ordered ahead of the cosign signature so the
# Sigstore bundle covers the Authenticode-signed bytes.
export WINDOWS_BUILD_SIGN_HOOK=""
export WINDOWS_AUTHENTICODE_MSI_SIGN=""
if [ "${AUTHENTICODE}" = "true" ]; then
echo "Authenticode signing enabled: injecting signing config"
SIGN_SCRIPT="../_workflows/scripts/sign-windows-authenticode.ps1"

WINDOWS_BUILD_SIGN_HOOK=$(printf '\n hooks:\n post:\n - cmd: pwsh -NoProfile -File %s -Path "{{ .Path }}"\n output: true' "$SIGN_SCRIPT")

WINDOWS_AUTHENTICODE_MSI_SIGN=$(printf '\n - id: authenticode-msi\n cmd: pwsh\n artifacts: installer\n ids:\n - windows-msi\n signature: "{{ .Env.artifact }}.authenticode.json"\n output: true\n args:\n - "-NoProfile"\n - "-File"\n - "%s"\n - "-Path"\n - "{{ .Env.artifact }}"\n - "-EmitMarker"' "$SIGN_SCRIPT")
fi

# Generate GoReleaser config
envsubst < templates/.goreleaser-windows-template.yaml.tmpl | tee "_generated/.goreleaser.windows.yaml"

# Generate provenance predicate
envsubst < templates/.slsa-provenance-predicate-template.json.tmpl | tee "_generated/predicate.json"

- name: Azure login for Trusted Signing (OIDC)
if: inputs.windows_authenticode_signing == true
uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

- name: Install Microsoft sign tool
if: inputs.windows_authenticode_signing == true
shell: pwsh
run: |
# The `sign` tool currently ships pre-release only. Pin a specific
# version here once validated against the provisioned Trusted Signing
# account for reproducible, supply-chain-stable signing.
dotnet tool install --global sign --prerelease
# Surface the global tools directory for subsequent steps / hooks.
"$env:USERPROFILE\.dotnet\tools" | Out-File -FilePath $env:GITHUB_PATH -Append -Encoding utf8

- name: Run GoReleaser for Windows
uses: goreleaser/goreleaser-action@v6
with:
Expand All @@ -608,6 +723,11 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.RELENG_GITHUB_TOKEN }}
GORELEASER_KEY: ${{ secrets.GORELEASER_PRO_KEY }}
# Consumed by scripts/sign-windows-authenticode.ps1 (invoked from the
# build post-hook and the MSI signs entry). Empty when signing is off.
TRUSTED_SIGNING_ENDPOINT: ${{ inputs.trusted_signing_endpoint }}
TRUSTED_SIGNING_ACCOUNT_NAME: ${{ inputs.trusted_signing_account_name }}
TRUSTED_SIGNING_CERTIFICATE_PROFILE: ${{ inputs.trusted_signing_certificate_profile }}

- name: Flatten MSI directory structure
shell: pwsh
Expand Down
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ The release workflow accepts the following input parameters:
| `docker_extra_files` | No | `""` | Comma-separated list of extra files/dirs to include in Docker build context |
| `msi` | No | `true` | Whether to build MSI Windows installers |
| `msi_wxs_path` | No | `""` | Path to custom WXS template for MSI installer (uses default if not set) |
| `windows_authenticode_signing` | No | `false` | Authenticode-sign Windows `.exe` and `.msi` via Azure Trusted Signing (requires the `trusted_signing_*` inputs and `AZURE_*` secrets) |
| `trusted_signing_endpoint` | No | `""` | Azure Trusted Signing region endpoint (e.g. `https://wus2.codesigning.azure.net/`); required when `windows_authenticode_signing: true` |
| `trusted_signing_account_name` | No | `""` | Azure Trusted Signing account name; required when `windows_authenticode_signing: true` |
| `trusted_signing_certificate_profile` | No | `""` | Azure Trusted Signing certificate profile name; required when `windows_authenticode_signing: true` |

2. Ensure your repository has the following secrets configured:

Expand All @@ -92,6 +96,7 @@ The release workflow accepts the following input parameters:
- `AC_PROVIDER`: Apple Connect provider
- `DATADOG_API_KEY`: Datadog API key for monitoring releases
- `GORELEASER_PRO_KEY`: GoReleaser Pro license key (required when `msi: true`, the default)
- `AZURE_CLIENT_ID` / `AZURE_TENANT_ID` / `AZURE_SUBSCRIPTION_ID`: Azure OIDC identity for Trusted Signing (required only when `windows_authenticode_signing: true`)

3. Remove all GoReleaser, gon files, Dockerfile, and Dockerfile.lambda files from your connector repository, if they were previously created there.

Expand Down Expand Up @@ -175,6 +180,37 @@ To disable MSI builds entirely (e.g., for connectors that don't need Windows ins

When `msi: false`, the `GORELEASER_PRO_KEY` secret is not required.

### Windows Authenticode Signing

Sigstore (cosign) signatures prove provenance but do not satisfy Windows
SmartScreen. To eliminate SmartScreen warnings on the `.exe` and `.msi`, enable
Authenticode signing via [Azure Trusted Signing](https://learn.microsoft.com/azure/trusted-signing/):

```yaml
jobs:
release:
uses: ConductorOne/github-workflows/.github/workflows/release.yaml@v4
with:
tag: ${{ github.ref_name }}
windows_authenticode_signing: true
trusted_signing_endpoint: https://wus2.codesigning.azure.net/
trusted_signing_account_name: conductorone-signing
trusted_signing_certificate_profile: conductorone
secrets:
# ... existing secrets ...
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
```

This is **opt-in** (default `false`) so connectors keep releasing unchanged
until the Azure Trusted Signing account is provisioned. Authentication uses
GitHub Actions OIDC — no signing certificate is stored in repository secrets.
The `.exe` is signed before it is packaged, so both the standalone binary (in
the `.zip`) and the copy inside the `.msi` carry the signature. See the
[release workflow docs](docs/release-workflow.md#windows-authenticode-signing-azure-trusted-signing)
for Azure setup and verification details.

## Verify Workflow

Runs linting, tests, and optional regression verification. See [detailed documentation](docs/verify-workflow.md) for jobs, regression testing, and all options.
Expand Down
5 changes: 4 additions & 1 deletion cmd/generate-windows-manifest/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,10 @@ func main() {
}

// MSI uses key "windows-amd64-msi"
// MSI has cosign signatures and attestations; Azure Trusted Signing (Windows code signing) planned for Stage 2
// MSI always has cosign signatures and attestations; Windows Authenticode
// signing (Azure Trusted Signing) is applied in-place earlier in the
// pipeline when windows_authenticode_signing is enabled, so the hash here
// covers the Authenticode-signed bytes.
assets["windows-amd64-msi"] = asset
fmt.Fprintf(os.Stderr, "✅ Added MSI asset: windows-amd64-msi -> %s\n", filename)
}
Expand Down
82 changes: 68 additions & 14 deletions docs/release-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ Builds Windows zip and MSI installer:
- MSI built using WiX Toolset with GoReleaser Pro
- Deterministic UpgradeCode via UUID v5 from repository name
- Supports custom WXS templates via `msi_wxs_path` input
- Optionally Authenticode-signs the `.exe` and `.msi` with Azure Trusted Signing
(opt-in via `windows_authenticode_signing`; see Security Properties)
- Generates SBOMs and SLSA v1 provenance attestations
- Uploads all artifacts to S3 with no-overwrite writes

Expand Down Expand Up @@ -167,7 +169,55 @@ Both Windows zip and MSI have:
- SLSA provenance attestations
- SBOM attestations

**Note:** Windows code signing via Azure Trusted Signing is planned for Stage 2.
### Windows Authenticode Signing (Azure Trusted Signing)

Sigstore signatures prove supply-chain provenance but are not recognized by
Windows SmartScreen. To remove SmartScreen warnings, the workflow can also
Authenticode-sign Windows artifacts with **Azure Trusted Signing**. This is
**opt-in** via the `windows_authenticode_signing` input (default `false`), so
releases continue to work unchanged until the Azure account is provisioned.

When enabled, the `goreleaser-windows` job:

1. Logs in to Azure via GitHub Actions OIDC (`azure/login`) — no long-lived
certificate material is stored in secrets.
2. Installs the Microsoft [`sign`](https://github.com/dotnet/sign) CLI.
3. Signs the raw `.exe` in a GoReleaser **build post-hook**, before it is
packaged — so the binary inside both the `.zip` and the `.msi` is signed.
4. Signs the `.msi` in place via a GoReleaser `signs` entry that is ordered
**ahead of** the cosign signature, so the Sigstore `.sig`/`.cert` and all
downstream hashes/attestations cover the Authenticode-signed bytes.

All signing is delegated to `scripts/sign-windows-authenticode.ps1`, which calls
`sign code trusted-signing` and then verifies the result with
`Get-AuthenticodeSignature`.

Because Azure Trusted Signing issues short-lived certificates chained to a
Microsoft-managed root, SmartScreen reputation is immediate — unlike standard OV
certificates, which must accrue reputation through download volume.

**Required configuration when `windows_authenticode_signing: true`:**

| Kind | Name | Description |
|-|-|-|
| input | `trusted_signing_endpoint` | Region endpoint, e.g. `https://wus2.codesigning.azure.net/` |
| input | `trusted_signing_account_name` | Trusted Signing account name |
| input | `trusted_signing_certificate_profile` | Certificate profile name |
| secret | `AZURE_CLIENT_ID` | App registration (federated credential) client ID |
| secret | `AZURE_TENANT_ID` | Azure AD tenant ID |
| secret | `AZURE_SUBSCRIPTION_ID` | Subscription hosting the Trusted Signing account |

The federated credential on the Azure AD app must trust the connector
repository's GitHub OIDC subject, and the identity must hold the **Trusted
Signing Certificate Profile Signer** role on the account.

**Verification** (on a Windows machine):

```powershell
Get-AuthenticodeSignature .\baton-foo_v1.0.0_windows_amd64.msi | Format-List
# Status should be 'Valid'; SignerCertificate should chain to the Microsoft root.
signtool verify /pa /v .\baton-foo_v1.0.0_windows_amd64.msi
```

### Verification

Expand Down Expand Up @@ -335,19 +385,23 @@ Test the MSI installer on an actual Windows machine:

## Future Work

### Stage 2: Windows Code Signing

Currently MSI installers have Sigstore signatures (cosign) but not Windows Authenticode signatures. Stage 2 will add:

- **Azure Trusted Signing** integration for Authenticode signatures
- MSI files will be signed with Microsoft-trusted certificate
- Windows SmartScreen warnings will be eliminated
- Users can verify publisher identity in Windows UAC prompts

This requires:
- Azure Trusted Signing account setup
- GitHub Actions OIDC integration with Azure
- Workflow updates to sign MSI after build
### Windows Code Signing (implemented, opt-in)

Windows Authenticode signing via **Azure Trusted Signing** is implemented and
gated behind the `windows_authenticode_signing` input (see
[Windows Authenticode Signing](#windows-authenticode-signing-azure-trusted-signing)).
It remains off by default pending Azure Trusted Signing account provisioning
(tracked in CE-178). To roll it out:

1. Provision an Azure Trusted Signing account and certificate profile.
2. Create an Azure AD app with a federated credential trusting the connector
repos' GitHub OIDC subjects, and grant it the **Trusted Signing Certificate
Profile Signer** role.
3. Add the `AZURE_CLIENT_ID` / `AZURE_TENANT_ID` / `AZURE_SUBSCRIPTION_ID`
secrets and set `windows_authenticode_signing: true` plus the
`trusted_signing_*` inputs in each connector's release workflow.
4. Validate on a test connector per [Testing Changes](#testing-changes) and
confirm `Get-AuthenticodeSignature` reports `Valid`.

### Other Potential Improvements

Expand Down
Loading