Modern Windows applications often need more than a successful build. Before an application package can be distributed through an organization's deployment process, package signing must also be handled securely.
For WinUI 3 applications, signing is especially important because Windows uses package signatures to establish trust and verify package integrity.
The Windows App Development CLI provides command-line tooling that can help automate Windows application development workflows. When package signing is incorporated into CI/CD, teams need to consider not only how to sign an application, but also how to protect the certificate and signing credentials used by the pipeline.
A secure architecture separates the application build from sensitive signing operations:
Source Code
↓
Build
↓
Test
↓
Package
↓
Secure Signing
↓
Release Artifact
The signing certificate should never be treated like an ordinary source-code file.
Why WinUI Package Signing Matters
Windows application packages can contain executable code and other application resources.
Signing provides an integrity and trust mechanism for the package.
A simplified process looks like:
WinUI Application
↓
MSIX Package
↓
Digital Signature
↓
Signed Package
↓
Verification
↓
Installation / Distribution
If the package is modified after signing, its signature can no longer be trusted as proof that the package remains unchanged.
This makes signing a security boundary in the release pipeline.
What Is the Windows App Development CLI?
The Windows App Development CLI is a command-line tool intended to simplify Windows application development workflows.
A CLI-based workflow is useful in CI/CD because build and packaging operations can be executed without relying on an interactive development environment.
A typical pipeline can therefore look like:
Developer Commit
↓
GitHub Actions
↓
Windows Runner
↓
Windows App Development CLI
↓
Build / Package
↓
Signing
The exact commands and capabilities available in a particular CLI release should be verified against the installed version.
Do not hard-code assumptions about command names or options without validating them in the CI environment.
Signing Should Be a Separate Security Step
A common mistake is treating signing as just another build command.
Instead, consider it a privileged operation:
Build Job
|
+-- Compile
+-- Test
+-- Package
|
v
Signing Job
|
+-- Access certificate
+-- Sign package
+-- Publish signed artifact
This separation reduces the number of steps that require access to sensitive signing material.
The Signing Certificate Is Sensitive
A package-signing certificate can be used to establish trust for application packages.
Therefore, avoid storing a private signing certificate directly in the source repository:
repository/
|
+-- signing-cert.pfx ← Avoid
Instead, store sensitive material in an appropriate secret-management system.
For Azure-based environments, teams commonly use services such as:
Azure Key Vault
to manage sensitive cryptographic material.
The exact signing architecture depends on certificate type, pipeline tooling, and organizational requirements.
Azure-Based Signing Architecture
A secure conceptual design is:
GitHub Actions
|
| Federated Identity
v
Azure Identity
|
v
Azure Key Vault
|
v
Signing Credential
|
v
MSIX Package
The important principle is that the pipeline should receive only the access required to perform its signing operation.
Avoid embedding:
Client secrets
Passwords
Private keys
Long-lived access tokens
directly into workflow files.
GitHub Actions and Azure Authentication
GitHub Actions can authenticate to Azure without storing a long-lived Azure password in the repository.
A common approach uses OpenID Connect (OIDC).
The conceptual flow is:
GitHub Actions
|
| OIDC Token
v
Azure Entra ID
|
| Federated Identity
v
Azure Resource
The workflow identity can then receive narrowly scoped permissions.
This is preferable to putting a long-lived cloud credential directly into the workflow.
Example Azure Login Step
A GitHub Actions workflow can use an Azure login action:
- name: Azure Login
uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
For OIDC-based authentication, the workflow also needs the appropriate permissions:
permissions:
id-token: write
contents: read
The Azure identity must be configured to trust the GitHub repository and workflow conditions being used.
Do Not Give the Workflow Excessive Azure Permissions
The signing workflow should not automatically receive permissions to:
All Azure resources
All Key Vault secrets
All subscriptions
Production infrastructure
Use least privilege.
Conceptually:
GitHub Workflow
|
v
Signing Identity
|
+-- Required Key Vault operation
|
X-- Unrelated resources
This limits the impact if the workflow is compromised.
Protecting the Certificate
There are several possible certificate-handling approaches.
Certificate Download
A pipeline can retrieve a certificate from a protected store and use it during signing.
The sensitive material should be:
Retrieved only when required
Stored temporarily
Used for signing
Removed after use
Remote Signing
Another architecture keeps the private key protected and performs signing through a controlled signing service.
This can reduce private-key exposure on the CI runner.
The appropriate approach depends on the organization's certificate infrastructure and supported tooling.
Temporary Files Need Protection
Suppose a certificate must temporarily exist on the runner:
Key Vault
↓
Temporary certificate
↓
Signing operation
↓
Delete certificate
The workflow should clean up the file after signing.
For example:
$certificatePath = Join-Path $env:RUNNER_TEMP "signing.pfx"
try {
# Retrieve certificate securely
# Perform signing
}
finally {
if (Test-Path $certificatePath) {
Remove-Item $certificatePath -Force
}
}
Cleanup should happen even when the signing operation fails.
Never Print Secrets
Avoid commands such as:
Write-Host $certificatePassword
or:
Write-Host $certificatePath
when the path or command could expose sensitive information.
GitHub Actions masks registered secrets in logs, but developers should still design workflows so sensitive values are never unnecessarily printed.
Signing With a Certificate Password
If a PFX file is used, the private key may be protected by a password.
The password should be supplied through a protected secret or secure credential mechanism.
Do not write:
env:
SIGNING_PASSWORD: "MyPassword123"
Instead, use a protected secret:
env:
SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }}
Even then, minimize how often the secret is exposed to commands and processes.
Signing the MSIX Package
The exact signing command depends on the packaging workflow and installed Windows tooling.
A common Windows signing tool is SignTool.
A conceptual command looks like:
signtool sign `
/fd SHA256 `
/f $certificatePath `
/p $env:SIGNING_PASSWORD `
$packagePath
The certificate path, password, package path, and signing options should come from the secure pipeline environment.
Before using this in production, verify the installed Windows SDK and SignTool version and use the supported signing options for the certificate and package.
Verify the Signature
Signing should always be followed by verification.
Conceptually:
Package
↓
Sign
↓
Verify
↓
Publish
For example:
signtool verify `
/pa `
$packagePath
A successful signing command is not the final validation.
The pipeline should confirm that the resulting package has a valid signature.
Verify Before Publishing
A secure release workflow can enforce:
Build
↓
Test
↓
Package
↓
Sign
↓
Verify
↓
Publish
Do not publish the package if signature verification fails.
For example:
- name: Verify package signature
shell: pwsh
run: |
signtool verify /pa "${{ env.PACKAGE_PATH }}"
If the command returns a failure status, the workflow should stop.
Certificate Subject and Identity
A signing certificate contains identity information.
During verification, teams may want to check:
Subject
Issuer
Expiration
Thumbprint
Signature algorithm
This can help detect accidental use of the wrong certificate.
For example:
Expected certificate
|
v
Verify package
|
v
Certificate identity matches
Do not rely only on the fact that "a certificate" was used.
The pipeline should use the expected signing identity.
Certificate Expiration
Certificates expire.
This can cause an otherwise healthy release pipeline to fail unexpectedly.
A release process should monitor:
Certificate expiration date
well before the release pipeline needs the certificate.
A practical workflow is:
Certificate
↓
Expiration Monitoring
↓
Renew / Replace
↓
Update Secure Store
↓
Test Signing
↓
Release
Certificate rotation should be tested before the existing certificate becomes unusable.
Protecting Production Signing
Production signing deserves stronger controls than development signing.
For example:
Development
↓
Development Certificate
Test
↓
Test Certificate
Production
↓
Production Certificate
Do not use the same private signing credential everywhere unless there is a deliberate security reason.
Separating environments reduces the impact of a compromised development workflow.
Pull Requests Should Not Automatically Access Production Signing Keys
This is particularly important in GitHub Actions.
A pull request from an untrusted source should not automatically receive access to production signing credentials.
Prefer:
Pull Request
↓
Build
↓
Test
↓
Security Analysis
and then:
Trusted Release
↓
Production Signing
↓
Publish
This creates a clear trust boundary.
GitHub Environments
GitHub Environments can be used to separate deployment and release configuration.
For example:
development
testing
production
The production environment can have additional protection requirements.
The conceptual pipeline becomes:
Pull Request
↓
Build + Test
↓
Merge
↓
Production Environment
↓
Approval / Policy
↓
Signing
↓
Release
This is stronger than allowing every workflow execution to access the production certificate.
Code Signing vs Package Signing
These concepts should not be confused.
An application may contain executable binaries that have their own code-signing requirements, while the MSIX package has package-level signing.
Conceptually:
Application Binary
↓
Code Signature
MSIX Package
↓
Package Signature
Depending on the application's distribution requirements, both can be relevant.
The pipeline should clearly document which artifacts are being signed.
Testing the Signing Pipeline
A good test should verify the complete workflow.
Step 1: Build
dotnet build --configuration Release
Step 2: Package
Create the MSIX package using the application's packaging workflow.
Step 3: Retrieve Signing Credential
Access the approved secure certificate source.
Step 4: Sign
Sign the package using the required certificate.
Step 5: Verify
Run signature verification.
Step 6: Inspect Identity
Confirm that the expected certificate identity was used.
Step 7: Test Installation
Install the signed package in a controlled environment.
Step 8: Clean Up
Remove temporary signing material from the runner.
Testing Failure Scenarios
Do not test only successful signing.
Also test:
Expired certificate
Wrong certificate
Incorrect password
Missing certificate
Insufficient Azure permission
Invalid package
Modified package
Signature verification failure
For example:
Package
↓
Modify after signing
↓
Verify
↓
Expected: Failure
This confirms that the verification stage is actually detecting tampering.
Common Mistakes
Storing PFX Files in Git
A private signing certificate should not be committed to source control.
Hardcoding Certificate Passwords
Passwords belong in secure secret-management systems.
Giving CI Broad Azure Permissions
The signing identity should have only the permissions required.
Signing Every Pull Request
Production signing should generally be restricted to trusted release workflows.
Skipping Signature Verification
Always verify the resulting artifact.
Ignoring Certificate Expiration
A certificate can expire even when the application code has not changed.
Publishing Before Verification
The unsigned or incorrectly signed artifact should never proceed to release.
Using One Certificate Everywhere
Separate development and production signing identities when the security model requires it.
Troubleshooting
Signing Fails
Check:
Certificate availability
Certificate password
Certificate validity
Package path
Signing tool version
Runner architecture
Azure Authentication Fails
Verify:
Client ID
Tenant ID
Subscription
OIDC permissions
Federated credential
Azure role assignment
Key Vault Access Is Denied
Check the identity's permissions and whether the workflow is using the intended Azure identity.
Signature Verification Fails
Inspect:
Certificate validity
Certificate chain
Package modification
Signing algorithm
Timestamp configuration
Package Installs on One Machine but Not Another
Check certificate trust and the signing identity available on the target system.
Best Practices
Treat package signing as a privileged release operation.
Keep private signing credentials outside source control.
Use secure certificate storage.
Prefer short-lived or federated CI authentication where supported.
Apply least-privilege Azure permissions.
Restrict production signing to trusted workflows.
Use separate signing identities for different environments where appropriate.
Verify every signed package before publishing.
Monitor certificate expiration.
Test certificate rotation before production expiration.
Clean up temporary certificate files from CI runners.
Never print signing credentials in workflow logs.
Test failure and tampering scenarios.
Keep build and signing responsibilities separated when practical.
Advantages and Disadvantages
Advantages
Automates package signing as part of CI/CD.
Reduces manual release steps.
Provides repeatable signature verification.
Azure-based identity and secret-management services can reduce credential exposure.
Separating signing from normal builds creates a clearer security boundary.
Disadvantages
Signing infrastructure adds configuration complexity.
Certificate management requires ongoing maintenance.
Incorrect Azure permissions can block releases.
Production signing credentials require stronger controls.
Native Windows signing tools and packaging requirements need to be maintained on CI runners.
Conclusion
Securing WinUI package signing is not simply a matter of adding a signing command to a build script.
A production-oriented pipeline should establish a clear trust boundary:
Source
↓
Build
↓
Test
↓
Package
↓
Trusted Release Workflow
↓
Secure Certificate Access
↓
Sign
↓
Verify
↓
Publish
The Windows App Development CLI can be part of an automated Windows application workflow, while Azure services can provide the identity and secret-management infrastructure around sensitive signing operations.
The most important security principle is simple: the application build should not automatically have unrestricted access to the production signing credential.
Keep signing credentials protected, use least-privilege access, restrict production signing to trusted workflows, verify signatures before publishing, and test certificate rotation and failure scenarios.
That approach turns package signing from a manual release task into a controlled security step within the Windows application delivery pipeline.