Parallel to #280 (macOS code-signing + notarization). Today's Windows installer (DisplayXRSetup-X.Y.Z.NNN.exe, built by installer/DisplayXRInstaller.nsi) ships unsigned. Users get Microsoft SmartScreen's "Windows protected your PC — unrecognized app" prompt on double-click and need "More info" → "Run anyway" to proceed. installer /S (silent install from a script with admin context) works regardless of signing.
This issue tracks Authenticode signing + (eventually) SmartScreen reputation to ship a clickable installer.
Prerequisites (not in repo today)
-
Code Signing certificate from a Microsoft-trusted CA. Two tiers, both viable:
- Standard Code Signing ($200-500/yr — DigiCert, Sectigo, SSL.com). P12 file, easy CI import. SmartScreen still warns until the cert builds "reputation" (~3000+ download-and-runs over weeks-to-months). Useful as a stepping stone.
- EV (Extended Validation) Code Signing ($400-700/yr — same CAs). Reputation pre-baked: clean run from the first signed binary. Requires hardware token (HSM) by default; for CI use one of:
- Azure Key Vault with a code-signing certificate (Microsoft's recommended modern path; SignTool 6.3+ supports
-fd sha256 -dlib AzureSignTool.dll)
- DigiCert KeyLocker (their cloud HSM service)
- SSL.com eSigner CSC (similar)
Locally-held tokens don't work from CI without a self-hosted runner.
-
GitHub Actions secrets (depending on which path):
- Standard cert path:
WINDOWS_CERT_P12 (base64-encoded), WINDOWS_CERT_PASSWORD
- Azure Key Vault path:
AZURE_KEY_VAULT_URI, AZURE_CERT_NAME, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_TENANT_ID (plus AzureSignTool installed via dotnet tool install)
-
Forks: same as macOS — gracefully fall back to unsigned builds on fork PRs.
Implementation sketch — Standard cert path (simpler, ships unsigned-with-warning-until-reputation)
- name: Import code signing cert
if: needs.DetectChanges.outputs.docs_only != 'true' && github.event_name != 'pull_request'
shell: pwsh
env:
CERT_P12: ${{ secrets.WINDOWS_CERT_P12 }}
CERT_PASSWORD: ${{ secrets.WINDOWS_CERT_PASSWORD }}
run: |
$bytes = [Convert]::FromBase64String($env:CERT_P12)
$certPath = "$env:RUNNER_TEMP\cert.p12"
[IO.File]::WriteAllBytes($certPath, $bytes)
$pwd = ConvertTo-SecureString -String $env:CERT_PASSWORD -AsPlainText -Force
Import-PfxCertificate -FilePath $certPath -CertStoreLocation Cert:\CurrentUser\My -Password $pwd
Remove-Item $certPath
# (NSIS produces _package\DisplayXRSetup-*.exe unsigned here.)
- name: Sign installer + dependent binaries
if: needs.DetectChanges.outputs.docs_only != 'true' && github.event_name != 'pull_request'
shell: pwsh
run: |
$signtool = "$env:ProgramFiles(x86)\Windows Kits\10\bin\10.0.22621.0\x64\signtool.exe"
# Sign every .exe / .dll inside _package, then the installer itself.
# Microsoft recommends SHA-256 + RFC3161 timestamping.
Get-ChildItem -Recurse _package -Include *.exe,*.dll | ForEach-Object {
& $signtool sign /fd SHA256 /tr http://timestamp.digicert.com /td SHA256 /a $_.FullName
}
Implementation sketch — Azure Key Vault / EV path
- name: Install AzureSignTool
if: needs.DetectChanges.outputs.docs_only != 'true' && github.event_name != 'pull_request'
run: dotnet tool install --global AzureSignTool
- name: Sign with EV cert from Azure Key Vault
if: needs.DetectChanges.outputs.docs_only != 'true' && github.event_name != 'pull_request'
shell: pwsh
env:
AKV_URI: ${{ secrets.AZURE_KEY_VAULT_URI }}
AKV_CERT: ${{ secrets.AZURE_CERT_NAME }}
AKV_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
AKV_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
AKV_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
run: |
Get-ChildItem -Recurse _package -Include *.exe,*.dll | ForEach-Object {
AzureSignTool sign `
--azure-key-vault-url "$env:AKV_URI" `
--azure-key-vault-client-id "$env:AKV_CLIENT_ID" `
--azure-key-vault-tenant-id "$env:AKV_TENANT_ID" `
--azure-key-vault-client-secret "$env:AKV_CLIENT_SECRET" `
--azure-key-vault-certificate "$env:AKV_CERT" `
--file-digest sha256 --timestamp-rfc3161 http://timestamp.digicert.com `
$_.FullName
}
What to sign
NSIS installers wrap their content; signing only DisplayXRSetup-*.exe is not sufficient — the unpacked DLLs/EXEs that ship inside trigger SmartScreen warnings when the user's apps load them. Sign:
_package\bin\DisplayXRClient.dll (runtime)
_package\bin\displayxr-service.exe
_package\bin\displayxr-cli.exe (if shipped)
_package\bin\plugins\DisplayXR-SimDisplay.dll
- The NSIS installer's
DisplayXRSetup-*.exe itself
- (Future) Leia SR plug-in DLL — signed independently by the LeiaSR installer pipeline
Acceptance
When to schedule
Same trigger as #280: when the audience shifts from developers (who tolerate the warning) to end users. The macOS .pkg and Windows .exe are roughly symmetric — sign them together so users on either platform see the same UX, not one polished and one warning-y.
References
Parallel to #280 (macOS code-signing + notarization). Today's Windows installer (
DisplayXRSetup-X.Y.Z.NNN.exe, built byinstaller/DisplayXRInstaller.nsi) ships unsigned. Users get Microsoft SmartScreen's "Windows protected your PC — unrecognized app" prompt on double-click and need "More info" → "Run anyway" to proceed.installer /S(silent install from a script with admin context) works regardless of signing.This issue tracks Authenticode signing + (eventually) SmartScreen reputation to ship a clickable installer.
Prerequisites (not in repo today)
Code Signing certificate from a Microsoft-trusted CA. Two tiers, both viable:
-fd sha256 -dlib AzureSignTool.dll)Locally-held tokens don't work from CI without a self-hosted runner.
GitHub Actions secrets (depending on which path):
WINDOWS_CERT_P12(base64-encoded),WINDOWS_CERT_PASSWORDAZURE_KEY_VAULT_URI,AZURE_CERT_NAME,AZURE_CLIENT_ID,AZURE_CLIENT_SECRET,AZURE_TENANT_ID(plusAzureSignToolinstalled viadotnet tool install)Forks: same as macOS — gracefully fall back to unsigned builds on fork PRs.
Implementation sketch — Standard cert path (simpler, ships unsigned-with-warning-until-reputation)
Implementation sketch — Azure Key Vault / EV path
What to sign
NSIS installers wrap their content; signing only
DisplayXRSetup-*.exeis not sufficient — the unpacked DLLs/EXEs that ship inside trigger SmartScreen warnings when the user's apps load them. Sign:_package\bin\DisplayXRClient.dll(runtime)_package\bin\displayxr-service.exe_package\bin\displayxr-cli.exe(if shipped)_package\bin\plugins\DisplayXR-SimDisplay.dllDisplayXRSetup-*.exeitselfAcceptance
mainsigns every shipped.exe+.dlland the NSIS installersigntool verify /pa _package\DisplayXRSetup-*.exereturnsSuccessfully verifieddocs/getting-started/building.mdupdated to drop the "More info → Run anyway" workaround noteWhen to schedule
Same trigger as #280: when the audience shifts from developers (who tolerate the warning) to end users. The macOS .pkg and Windows .exe are roughly symmetric — sign them together so users on either platform see the same UX, not one polished and one warning-y.
References
.github/workflows/build-windows.yml— workflow to extendinstaller/DisplayXRInstaller.nsi— NSIS script that produces the unsigned.exeinstaller/DisplayXRLeiaSRInstaller.nsi— sibling Leia installer (same signing requirement when it lands)