Code Signing Certificate Setup Guide: Secure Software Distribution
Code signing digitally signs software to prove authenticity and integrity. When users download signed applications, they can verify the software comes from the claimed publisher and hasn't been modified since signing. Operating systems increasingly require code signing—Windows SmartScreen warns users about unsigned software, macOS Gatekeeper blocks it, and mobile platforms require it for app store distribution.
This guide covers obtaining code signing certificates, signing on Windows, macOS, and Linux, CI/CD integration, and the 2025 regulatory changes requiring HSM-based key storage.
2025 Code Signing Requirements
The CA/Browser Forum has mandated significant changes effective in 2025:
┌─────────────────────────────────────────────────────────────────────────┐
│ 2025 Code Signing Certificate Changes │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ EFFECTIVE FEBRUARY 1, 2025 │
│ ───────────────────────────────────────────────────────────────────── │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ MANDATORY HSM STORAGE │ │
│ │ • All code signing private keys MUST be stored in: │ │
│ │ - Hardware Security Module (HSM) │ │
│ │ - Hardware token (USB, smart card) │ │
│ │ • FIPS 140-2 Level 2 minimum certification required │ │
│ │ • Software key storage NO LONGER PERMITTED │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ SHORTER VALIDITY PERIODS │ │
│ │ • Maximum certificate validity: 460 days (was 3 years) │ │
│ │ • More frequent renewals required │ │
│ │ • Automation becomes essential │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ STRONGER KEY REQUIREMENTS │ │
│ │ • Minimum RSA key size: 3072 bits (was 2048) │ │
│ │ • ECDSA P-256 or P-384 also acceptable │ │
│ │ • SHA-256 or stronger hash algorithm required │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ WHY THESE CHANGES? │
│ • High-profile key theft incidents (Nvidia, Samsung, Microsoft) │
│ • Software-stored keys too easily compromised │
│ • Shorter validity limits exposure from compromised certificates │
│ • Stronger cryptography for long-term security │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Action required: If you currently store code signing keys in software (PFX file, certificate store), you must transition to HSM or hardware token before your next certificate renewal.
Code Signing Certificate Types
Standard vs EV Code Signing
| Feature | Standard Code Signing | EV Code Signing |
|---|---|---|
| Identity Verification | Organization verified | Extended verification (like EV SSL) |
| SmartScreen Reputation | Builds over time | Immediate trusted reputation |
| User Experience | May show warnings initially | No SmartScreen warnings |
| Key Storage | HSM required (2025+) | HSM required (always) |
| Price Range | $200-500/year | $400-700/year |
| Best For | Internal/limited distribution | Commercial software distribution |
Recommendation: For commercial software, EV code signing is worth the premium to avoid SmartScreen warnings that significantly reduce download completion rates.
Certificate Providers
| Provider | Standard CS | EV CS | HSM Options |
|---|---|---|---|
| DigiCert | $474/year | $619/year | Cloud, Token, Network HSM |
| Sectigo | $329/year | $399/year | Cloud, Token |
| GlobalSign | $249/year | $419/year | Cloud, Token |
| SSL.com | $229/year | $319/year | Cloud, Token |
| Microsoft Trusted Signing | Pay per signature | N/A | Managed HSM |
Microsoft Trusted Signing (Preview)
Microsoft's managed code signing service provides EV-equivalent reputation without managing certificates:
# Azure CLI setup
az extension add --name trustedsigning
# Create Trusted Signing account
az trustedsigning account create \
--resource-group myRG \
--name mySigningAccount \
--location eastus
# Create certificate profile
az trustedsigning certificate-profile create \
--account-name mySigningAccount \
--profile-name Production \
--profile-type PublicTrust \
--include-street true \
--include-city true
# Sign using SignTool with Trusted Signing
signtool sign /v /debug /fd SHA256 \
/tr http://timestamp.acs.microsoft.com \
/td SHA256 \
/dlib "C:\Program Files\Azure\TrustedSigning\bin\Azure.CodeSigning.Dlib.dll" \
/dmdf metadata.json \
myapp.exe
Windows Code Signing (Authenticode)
Prerequisites
# Install Windows SDK (includes signtool.exe)
# Download from: https://developer.microsoft.com/windows/downloads/windows-sdk/
# Or install via winget
winget install Microsoft.WindowsSDK
# Verify signtool is available
& "C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x64\signtool.exe" /?
Signing with Hardware Token (EV Certificate)
# EV certificate on SafeNet token (automatic detection)
signtool sign /v /fd SHA256 `
/tr http://timestamp.digicert.com `
/td SHA256 `
/sha1 ABC123DEF456... `
MyApplication.exe
# For unattended signing, some tokens support PIN caching
# Or use SafeNet Authentication Client with stored PIN
# Sign multiple files
Get-ChildItem -Recurse -Include *.exe, *.dll | ForEach-Object {
signtool sign /v /fd SHA256 `
/tr http://timestamp.digicert.com `
/td SHA256 `
/sha1 ABC123DEF456... `
$_.FullName
}
Signing with Cloud HSM (Azure Key Vault)
# Install Azure Sign Tool
dotnet tool install --global AzureSignTool
# Sign using Azure Key Vault
AzureSignTool sign `
--azure-key-vault-url "https://myvault.vault.azure.net" `
--azure-key-vault-client-id "client-id" `
--azure-key-vault-client-secret "client-secret" `
--azure-key-vault-tenant-id "tenant-id" `
--azure-key-vault-certificate "code-signing-cert" `
--timestamp-rfc3161 "http://timestamp.digicert.com" `
--timestamp-digest sha256 `
--file-digest sha256 `
--verbose `
MyApplication.exe
Signing Windows Drivers
Kernel-mode drivers have additional requirements:
# Drivers require EV certificate and cross-certificate
# Microsoft attestation signing for Windows 10 1607+
# Sign with EV certificate (includes cross-cert)
signtool sign /v /fd SHA256 `
/ac "DigiCert High Assurance EV Root CA.crt" `
/tr http://timestamp.digicert.com `
/td SHA256 `
/sha1 ABC123DEF456... `
/ph `
MyDriver.sys
# Submit to Microsoft Partner Center for attestation signing
# (Required for Windows 10 version 1607 and later)
Verify Signatures
# Verify signature
signtool verify /pa /v MyApplication.exe
# Verify with detailed output
signtool verify /pa /v /debug MyApplication.exe
# Check timestamp
signtool verify /pa /v /tw MyApplication.exe
# Expected output for valid signature:
# Successfully verified: MyApplication.exe
# Signing Certificate Chain:
# Issued to: Example Corp
# Issued by: DigiCert Code Signing CA
# The signature is timestamped: [date]
macOS Code Signing
Prerequisites
# Requires Apple Developer Program membership ($99/year)
# Obtain Developer ID certificate from Apple Developer portal
# List available signing identities
security find-identity -v -p codesigning
# Expected output:
# 1) ABC123... "Developer ID Application: Example Corp (TEAM123)"
# 2) DEF456... "Developer ID Installer: Example Corp (TEAM123)"
Sign Applications
# Sign application bundle
codesign --sign "Developer ID Application: Example Corp (TEAM123)" \
--timestamp \
--options runtime \
--force \
--deep \
MyApp.app
# Verify signature
codesign --verify --verbose=4 MyApp.app
spctl --assess --verbose=4 --type execute MyApp.app
Notarization (Required for macOS 10.15+)
# Create ZIP for notarization
ditto -c -k --keepParent MyApp.app MyApp.zip
# Submit for notarization
xcrun notarytool submit MyApp.zip \
--apple-id "[email protected]" \
--team-id "TEAM123" \
--password "@keychain:AC_PASSWORD" \
--wait
# Check status
xcrun notarytool log <submission-id> \
--apple-id "[email protected]" \
--team-id "TEAM123" \
--password "@keychain:AC_PASSWORD"
# Staple notarization ticket to app
xcrun stapler staple MyApp.app
# Verify notarization
spctl --assess --verbose=4 --type execute MyApp.app
# Should show: source=Notarized Developer ID
Sign Disk Images (DMG)
# Sign the DMG
codesign --sign "Developer ID Application: Example Corp (TEAM123)" \
--timestamp \
MyApp.dmg
# Notarize the DMG
xcrun notarytool submit MyApp.dmg \
--apple-id "[email protected]" \
--team-id "TEAM123" \
--password "@keychain:AC_PASSWORD" \
--wait
# Staple
xcrun stapler staple MyApp.dmg
Sign Installer Packages (PKG)
# Sign package
productsign --sign "Developer ID Installer: Example Corp (TEAM123)" \
--timestamp \
unsigned.pkg \
signed.pkg
# Notarize
xcrun notarytool submit signed.pkg \
--apple-id "[email protected]" \
--team-id "TEAM123" \
--password "@keychain:AC_PASSWORD" \
--wait
# Staple
xcrun stapler staple signed.pkg
Linux Package Signing
GPG Signing for DEB Packages
# Generate GPG key (if not exists)
gpg --full-generate-key
# Export public key for repository
gpg --armor --export [email protected] > public.gpg
# Sign .deb package
dpkg-sig --sign builder mypackage.deb
# Verify signature
dpkg-sig --verify mypackage.deb
RPM Package Signing
# Configure RPM macros
cat >> ~/.rpmmacros << 'EOF'
%_signature gpg
%_gpg_name Developer <[email protected]>
%_gpg_path /home/developer/.gnupg
EOF
# Sign RPM
rpm --addsign mypackage.rpm
# Verify signature
rpm --checksig mypackage.rpm
rpm -K mypackage.rpm
Sign AppImage
# Sign with GPG
gpg --detach-sign --armor MyApp.AppImage
# Users verify with
gpg --verify MyApp.AppImage.asc MyApp.AppImage
CI/CD Integration
GitHub Actions (Windows)
name: Build and Sign
on:
push:
tags:
- 'v*'
jobs:
build-and-sign:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Build Application
run: dotnet build -c Release
- name: Decode Certificate
run: |
$certBytes = [Convert]::FromBase64String("${{ secrets.CODE_SIGNING_CERT }}")
[IO.File]::WriteAllBytes("cert.pfx", $certBytes)
- name: Sign Application
run: |
& "C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x64\signtool.exe" sign `
/f cert.pfx `
/p "${{ secrets.CERT_PASSWORD }}" `
/fd SHA256 `
/tr http://timestamp.digicert.com `
/td SHA256 `
.\bin\Release\MyApp.exe
- name: Clean Up Certificate
if: always()
run: Remove-Item cert.pfx -ErrorAction SilentlyContinue
GitHub Actions (Azure Key Vault)
name: Build and Sign with Azure Key Vault
on:
push:
tags:
- 'v*'
jobs:
build-and-sign:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Azure Login
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Build Application
run: dotnet build -c Release
- name: Install Azure Sign Tool
run: dotnet tool install --global AzureSignTool
- name: Sign Application
run: |
AzureSignTool sign `
--azure-key-vault-url "${{ secrets.AZURE_KEY_VAULT_URL }}" `
--azure-key-vault-client-id "${{ secrets.AZURE_CLIENT_ID }}" `
--azure-key-vault-client-secret "${{ secrets.AZURE_CLIENT_SECRET }}" `
--azure-key-vault-tenant-id "${{ secrets.AZURE_TENANT_ID }}" `
--azure-key-vault-certificate "${{ secrets.CERT_NAME }}" `
--timestamp-rfc3161 "http://timestamp.digicert.com" `
--timestamp-digest sha256 `
--file-digest sha256 `
.\bin\Release\MyApp.exe
Azure DevOps Pipeline
trigger:
tags:
include:
- v*
pool:
vmImage: 'windows-latest'
steps:
- task: DotNetCoreCLI@2
inputs:
command: 'build'
configuration: 'Release'
- task: AzureKeyVault@2
inputs:
azureSubscription: 'Azure-Connection'
KeyVaultName: 'MyKeyVault'
SecretsFilter: 'CodeSigningCert'
- task: DotNetCoreCLI@2
displayName: 'Install AzureSignTool'
inputs:
command: 'custom'
custom: 'tool'
arguments: 'install --global AzureSignTool'
- script: |
AzureSignTool sign ^
--azure-key-vault-url "$(KeyVaultUrl)" ^
--azure-key-vault-managed-identity ^
--azure-key-vault-certificate "CodeSigningCert" ^
--timestamp-rfc3161 "http://timestamp.digicert.com" ^
--file-digest sha256 ^
"$(Build.ArtifactStagingDirectory)\MyApp.exe"
displayName: 'Sign Application'
Jenkins Pipeline
pipeline {
agent { label 'windows' }
environment {
AZURE_CLIENT_ID = credentials('azure-client-id')
AZURE_CLIENT_SECRET = credentials('azure-client-secret')
AZURE_TENANT_ID = credentials('azure-tenant-id')
}
stages {
stage('Build') {
steps {
bat 'dotnet build -c Release'
}
}
stage('Sign') {
steps {
bat '''
AzureSignTool sign ^
--azure-key-vault-url "https://myvault.vault.azure.net" ^
--azure-key-vault-client-id "%AZURE_CLIENT_ID%" ^
--azure-key-vault-client-secret "%AZURE_CLIENT_SECRET%" ^
--azure-key-vault-tenant-id "%AZURE_TENANT_ID%" ^
--azure-key-vault-certificate "code-signing-cert" ^
--timestamp-rfc3161 "http://timestamp.digicert.com" ^
--file-digest sha256 ^
bin\\Release\\MyApp.exe
'''
}
}
}
}
Security Best Practices
Pre-Signing Verification
┌─────────────────────────────────────────────────────────────────────────┐
│ Code Signing Security Workflow │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. BUILD │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ • Build from verified source control │ │
│ │ • Use reproducible builds │ │
│ │ • Lock dependency versions │ │
│ │ • Scan dependencies for vulnerabilities │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 2. PRE-SIGN VERIFICATION │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ • Static analysis (SAST) │ │
│ │ • Malware scan (multiple engines) │ │
│ │ • License compliance check │ │
│ │ • Binary verification (no unexpected changes) │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 3. SIGNING │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ • HSM-protected signing key │ │
│ │ • Multi-party authorization for production │ │
│ │ • Timestamp from multiple TSAs │ │
│ │ • Audit logging of all signatures │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 4. POST-SIGN VERIFICATION │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ • Verify signature is valid │ │
│ │ • Verify timestamp is present │ │
│ │ • Test on clean system │ │
│ │ • Record hash for future verification │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Malware Scanning Before Signing
# Scan with multiple engines before signing
# Example: VirusTotal API
#!/bin/bash
FILE="$1"
API_KEY="${VIRUSTOTAL_API_KEY}"
# Upload file
RESPONSE=$(curl -s --request POST \
--url 'https://www.virustotal.com/api/v3/files' \
--header "x-apikey: ${API_KEY}" \
--form "file=@${FILE}")
ANALYSIS_ID=$(echo "$RESPONSE" | jq -r '.data.id')
# Wait for analysis
sleep 60
# Get results
RESULT=$(curl -s --request GET \
--url "https://www.virustotal.com/api/v3/analyses/${ANALYSIS_ID}" \
--header "x-apikey: ${API_KEY}")
MALICIOUS=$(echo "$RESULT" | jq '.data.attributes.stats.malicious')
if [ "$MALICIOUS" -gt 0 ]; then
echo "WARNING: $MALICIOUS engines detected malware!"
exit 1
fi
echo "Clean: proceeding with signing"
Timestamping Best Practices
# Use multiple timestamp servers for redundancy
$timestampServers = @(
"http://timestamp.digicert.com",
"http://timestamp.sectigo.com",
"http://timestamp.globalsign.com",
"http://tsa.starfieldtech.com"
)
$signed = $false
foreach ($ts in $timestampServers) {
try {
signtool sign /v /fd SHA256 `
/tr $ts `
/td SHA256 `
/sha1 ABC123... `
MyApp.exe
$signed = $true
Write-Host "Signed with timestamp from: $ts"
break
}
catch {
Write-Host "Timestamp failed from $ts, trying next..."
}
}
if (-not $signed) {
throw "All timestamp servers failed!"
}
Separation of Test and Production
┌─────────────────────────────────────────────────────────────────────────┐
│ Test vs Production Signing │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ TEST ENVIRONMENT PRODUCTION ENVIRONMENT │
│ ───────────────────────────────────────────────────────────────────── │
│ │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ Self-Signed Cert │ │ EV Code Signing │ │
│ │ or Test CA │ │ Certificate │ │
│ └─────────────────────┘ └─────────────────────┘ │
│ │
│ • Different key from prod • HSM-protected key │
│ • No SmartScreen impact • Multi-person authorization │
│ • Automated in CI • Approval gate required │
│ • Short validity • Full audit logging │
│ • Test systems only • Customer-facing releases │
│ │
│ NEVER use production signing key for: │
│ • Development builds │
│ • Testing/QA builds │
│ • Internal tools not for distribution │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Internal Code Signing (Private PKI)
For internal enterprise deployment:
# Create code signing CA (using private PKI)
# See: PKI Certificate Authority Setup Guide
# OpenSSL config for code signing template
cat > codesigning.cnf << 'EOF'
[req]
distinguished_name = req_dn
x509_extensions = v3_code
[req_dn]
CN = Internal Code Signing
[v3_code]
basicConstraints = CA:FALSE
keyUsage = critical, digitalSignature
extendedKeyUsage = critical, codeSigning
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer
EOF
# Generate code signing certificate
openssl req -new -nodes \
-keyout codesigning.key \
-out codesigning.csr \
-subj "/CN=Internal Code Signing/O=Example Corp"
openssl ca -config /etc/pki/issuing-ca/openssl.cnf \
-extensions v3_code \
-in codesigning.csr \
-out codesigning.crt
# Create PFX for Windows
openssl pkcs12 -export \
-inkey codesigning.key \
-in codesigning.crt \
-certfile ca-chain.crt \
-out codesigning.pfx
Deploy internal CA trust:
# Windows: Deploy via Group Policy
# Import to Trusted Publishers and Root
# PowerShell deployment
$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2("root-ca.crt")
$store = New-Object System.Security.Cryptography.X509Certificates.X509Store("Root", "LocalMachine")
$store.Open("ReadWrite")
$store.Add($cert)
$store.Close()
$signingCert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2("codesigning.crt")
$pubStore = New-Object System.Security.Cryptography.X509Certificates.X509Store("TrustedPublisher", "LocalMachine")
$pubStore.Open("ReadWrite")
$pubStore.Add($signingCert)
$pubStore.Close()
Common Pitfalls
| Pitfall | Impact | Prevention |
|---|---|---|
| Missing timestamp | Signatures invalid after cert expires | Always use timestamping with redundant TSAs |
| Software key storage | Key theft, signing malware | HSM or hardware token (required 2025+) |
| Signing without scanning | Signing malware as legitimate | Pre-sign malware scanning pipeline |
| Single person authorization | Insider threat | Multi-person approval for production |
| Using prod key for test | Key exposure, SmartScreen contamination | Separate test and production keys |
| Weak algorithms | Future signature invalidation | SHA-256+, RSA 3072+ or ECDSA |
| Missing chain certificates | Verification failures | Include full certificate chain |
| Delayed revocation response | Extended malware trust | Incident response plan, CA communication |
| No signature verification | Ship unsigned builds | Post-sign verification in CI |
| Hardcoded credentials | Key exposure in source control | Secrets management (Key Vault, etc.) |
Conclusion
Code signing is essential for secure software distribution, building user trust, and meeting platform requirements. The 2025 regulatory changes requiring HSM storage and stronger keys reflect the industry's recognition that code signing key protection is critical—stolen keys have been used to sign malware impersonating major vendors.
Key takeaways:
- Transition to HSM before your next certificate renewal (required February 2025)
- Use EV certificates for commercial software to avoid SmartScreen warnings
- Always timestamp signatures with multiple TSA servers
- Integrate scanning into CI/CD to prevent signing compromised builds
- Separate test and production signing keys and processes
- Implement multi-person authorization for production signing
For related topics, see our guides on HSM Certificate Storage and PKI Certificate Authority Setup.