Skip to content

NuGet Package Signing

Sign and verify NuGet packages using QCecuring-managed certificates backed by HSM-protected private keys.


┌──────────────┐ ┌──────────────────┐ ┌──────────────┐
│ NuGet CLI │──────►│ QCecuring Agent │──────►│ QCecuring │
│ / dotnet │ │ (KSP/PKCS#11) │ │ Platform │
└──────────────┘ └──────────────────┘ └──────────────┘
┌──────────────┐
│ HSM │
│ (Private │
│ Keys) │
└──────────────┘

Key points:

  • Private keys never leave the HSM
  • Signing operations are performed remotely via the QCecuring Agent
  • The agent exposes keys through Windows KSP or Linux PKCS#11
  • NuGet CLI uses the certificate as if it were locally available

Step 1: Configure Signing Certificate in QCecuring

Section titled “Step 1: Configure Signing Certificate in QCecuring”
  1. Navigate to Certificates in the QCecuring platform
  2. Import or generate a code signing certificate:
    • Subject: Your organization’s code signing identity
    • Key Usage: Digital Signature
    • Enhanced Key Usage: Code Signing (1.3.6.1.5.5.7.3.3)
    • Algorithm: RSA 4096 or ECDSA P-256
  3. Note the certificate thumbprint for use in signing commands
RequirementValue
Key UsageDigital Signature
Enhanced Key UsageCode Signing (1.3.6.1.5.5.7.3.3)
Minimum Key SizeRSA 2048 / ECDSA P-256
Hash AlgorithmSHA-256 or higher
ValidityMust be valid at signing time
Trust ChainMust chain to a trusted root

Step 2: Export Certificate or Configure PKCS#11

Section titled “Step 2: Export Certificate or Configure PKCS#11”
Section titled “Option A: Windows KSP (Recommended for Windows)”

The QCecuring Agent registers a Key Storage Provider (KSP) on Windows. The certificate appears in the Windows Certificate Store.

Terminal window
# Verify the certificate is available in the store
Get-ChildItem Cert:\CurrentUser\My | Where-Object { $_.Subject -like "*YourOrg*" }
# Output:
# Thumbprint Subject
# ---------- -------
# A1B2C3D4E5F6... CN=YourOrg Code Signing

Configure the QCecuring PKCS#11 module:

Terminal window
# Verify the token is accessible
pkcs11-tool --module /usr/lib/qcecuring-pkcs11.so --list-objects --type cert
# Export the certificate (public part only) for NuGet
pkcs11-tool --module /usr/lib/qcecuring-pkcs11.so \
--read-object --type cert --label "Code Signing" \
--output-file code-signing.cer

Option C: PFX Export (for CI/CD environments)

Section titled “Option C: PFX Export (for CI/CD environments)”

Export a PFX with a reference to the HSM key (no private key material exported):

Terminal window
qcecuring-agent export-cert \
--cert-id "code-signing-prod" \
--format pfx \
--output code-signing.pfx \
--password "$PFX_PASSWORD"

Terminal window
# Sign a single package
nuget sign MyPackage.1.0.0.nupkg \
-CertificateFingerprint A1B2C3D4E5F6... \
-Timestamper http://timestamp.digicert.com \
-HashAlgorithm SHA256 \
-TimestampHashAlgorithm SHA256 \
-OutputDirectory ./signed/
# Sign multiple packages
nuget sign *.nupkg \
-CertificateFingerprint A1B2C3D4E5F6... \
-Timestamper http://timestamp.digicert.com \
-HashAlgorithm SHA256 \
-TimestampHashAlgorithm SHA256 \
-OutputDirectory ./signed/
Terminal window
# Sign with dotnet nuget sign
dotnet nuget sign MyPackage.1.0.0.nupkg \
--certificate-fingerprint A1B2C3D4E5F6... \
--timestamper http://timestamp.digicert.com \
--hash-algorithm SHA256 \
--timestamp-hash-algorithm SHA256 \
--output ./signed/
Terminal window
nuget sign MyPackage.1.0.0.nupkg \
-CertificatePath ./code-signing.pfx \
-CertificatePassword "$PFX_PASSWORD" \
-Timestamper http://timestamp.digicert.com \
-HashAlgorithm SHA256 \
-TimestampHashAlgorithm SHA256 \
-OutputDirectory ./signed/

Terminal window
# Verify a signed package
nuget verify -Signatures MyPackage.1.0.0.nupkg
# Verify with verbose output
nuget verify -Signatures -Verbosity detailed MyPackage.1.0.0.nupkg
# Using dotnet CLI
dotnet nuget verify MyPackage.1.0.0.nupkg

Expected output for a valid signature:

Verifying MyPackage.1.0.0
...
Signature type: Author
...
Signing certificate:
Subject Name: CN=YourOrg Code Signing
SHA256 hash: A1B2C3D4E5F6...
...
Successfully verified package 'MyPackage.1.0.0'.

Timestamping ensures signatures remain valid after the certificate expires.

Timestamp ServerURL
DigiCerthttp://timestamp.digicert.com
Sectigohttp://timestamp.sectigo.com
GlobalSignhttp://timestamp.globalsign.com/tsa/r6advanced1
SSL.comhttp://ts.ssl.com

Always use timestamping in production. Without a timestamp, the signature becomes invalid when the certificate expires.


name: Build and Sign NuGet Package
on:
push:
tags: ['v*']
jobs:
build-and-sign:
runs-on: self-hosted # QCecuring Agent installed
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: Build
run: dotnet build --configuration Release
- name: Pack
run: dotnet pack --configuration Release --output ./packages
- name: Sign NuGet Packages
run: |
for pkg in ./packages/*.nupkg; do
dotnet nuget sign "$pkg" \
--certificate-fingerprint ${{ secrets.CERT_FINGERPRINT }} \
--timestamper http://timestamp.digicert.com \
--hash-algorithm SHA256 \
--timestamp-hash-algorithm SHA256 \
--output ./signed/
done
- name: Verify Signatures
run: |
for pkg in ./signed/*.nupkg; do
dotnet nuget verify "$pkg"
done
- name: Push to NuGet
run: |
dotnet nuget push ./signed/*.nupkg \
--source https://api.nuget.org/v3/index.json \
--api-key ${{ secrets.NUGET_API_KEY }}
trigger:
tags:
include: ['v*']
pool:
name: 'SigningPool' # Self-hosted with QCecuring Agent
steps:
- task: DotNetCoreCLI@2
displayName: 'Build'
inputs:
command: 'build'
arguments: '--configuration Release'
- task: DotNetCoreCLI@2
displayName: 'Pack'
inputs:
command: 'pack'
arguments: '--configuration Release --output $(Build.ArtifactStagingDirectory)'
- script: |
for pkg in $(Build.ArtifactStagingDirectory)/*.nupkg; do
dotnet nuget sign "$pkg" \
--certificate-fingerprint $(CertFingerprint) \
--timestamper http://timestamp.digicert.com \
--hash-algorithm SHA256 \
--timestamp-hash-algorithm SHA256 \
--output $(Build.ArtifactStagingDirectory)/signed/
done
displayName: 'Sign Packages'
- task: NuGetCommand@2
displayName: 'Push to Feed'
inputs:
command: 'push'
packagesToPush: '$(Build.ArtifactStagingDirectory)/signed/*.nupkg'
nuGetFeedType: 'external'
publishFeedCredentials: 'NuGetServiceConnection'

IssueCauseResolution
No certificate foundCertificate not in store or agent not runningVerify QCecuring Agent is running and certificate is accessible
The certificate chain is not trustedMissing intermediate or root CAImport full chain into trusted store
Timestamp verification failedTimestamp server unreachableCheck network connectivity; try alternate TSA
Package already signedAttempting to re-signUse --overwrite flag or sign unsigned package
Key usage not valid for signingWrong certificate typeEnsure certificate has Code Signing EKU
Access denied to private keyAgent permissionsRun signing process with correct user context

  • Always timestamp signatures for long-term validity
  • Use SHA-256 or higher for both signature and timestamp hash
  • Store certificate fingerprints in CI/CD secrets, not in code
  • Verify packages after signing and before publishing
  • Use separate certificates for development and production
  • Enable audit logging in QCecuring for all signing operations