NuGet Package Signing
NuGet Package Signing
Section titled “NuGet Package Signing”Sign and verify NuGet packages using QCecuring-managed certificates backed by HSM-protected private keys.
Architecture
Section titled “Architecture”┌──────────────┐ ┌──────────────────┐ ┌──────────────┐│ 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”- Navigate to Certificates in the QCecuring platform
- 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
- Note the certificate thumbprint for use in signing commands
Certificate Requirements for NuGet
Section titled “Certificate Requirements for NuGet”| Requirement | Value |
|---|---|
| Key Usage | Digital Signature |
| Enhanced Key Usage | Code Signing (1.3.6.1.5.5.7.3.3) |
| Minimum Key Size | RSA 2048 / ECDSA P-256 |
| Hash Algorithm | SHA-256 or higher |
| Validity | Must be valid at signing time |
| Trust Chain | Must chain to a trusted root |
Step 2: Export Certificate or Configure PKCS#11
Section titled “Step 2: Export Certificate or Configure PKCS#11”Option A: Windows KSP (Recommended for Windows)
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.
# Verify the certificate is available in the storeGet-ChildItem Cert:\CurrentUser\My | Where-Object { $_.Subject -like "*YourOrg*" }
# Output:# Thumbprint Subject# ---------- -------# A1B2C3D4E5F6... CN=YourOrg Code SigningOption B: Linux PKCS#11
Section titled “Option B: Linux PKCS#11”Configure the QCecuring PKCS#11 module:
# Verify the token is accessiblepkcs11-tool --module /usr/lib/qcecuring-pkcs11.so --list-objects --type cert
# Export the certificate (public part only) for NuGetpkcs11-tool --module /usr/lib/qcecuring-pkcs11.so \ --read-object --type cert --label "Code Signing" \ --output-file code-signing.cerOption 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):
qcecuring-agent export-cert \ --cert-id "code-signing-prod" \ --format pfx \ --output code-signing.pfx \ --password "$PFX_PASSWORD"Step 3: Sign Packages with nuget sign
Section titled “Step 3: Sign Packages with nuget sign”Using NuGet CLI
Section titled “Using NuGet CLI”# Sign a single packagenuget sign MyPackage.1.0.0.nupkg \ -CertificateFingerprint A1B2C3D4E5F6... \ -Timestamper http://timestamp.digicert.com \ -HashAlgorithm SHA256 \ -TimestampHashAlgorithm SHA256 \ -OutputDirectory ./signed/
# Sign multiple packagesnuget sign *.nupkg \ -CertificateFingerprint A1B2C3D4E5F6... \ -Timestamper http://timestamp.digicert.com \ -HashAlgorithm SHA256 \ -TimestampHashAlgorithm SHA256 \ -OutputDirectory ./signed/Using dotnet CLI
Section titled “Using dotnet CLI”# Sign with dotnet nuget signdotnet nuget sign MyPackage.1.0.0.nupkg \ --certificate-fingerprint A1B2C3D4E5F6... \ --timestamper http://timestamp.digicert.com \ --hash-algorithm SHA256 \ --timestamp-hash-algorithm SHA256 \ --output ./signed/Using Certificate from File (PFX)
Section titled “Using Certificate from File (PFX)”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/Step 4: Verify Signed Packages
Section titled “Step 4: Verify Signed Packages”# Verify a signed packagenuget verify -Signatures MyPackage.1.0.0.nupkg
# Verify with verbose outputnuget verify -Signatures -Verbosity detailed MyPackage.1.0.0.nupkg
# Using dotnet CLIdotnet nuget verify MyPackage.1.0.0.nupkgExpected 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'.Timestamp Configuration
Section titled “Timestamp Configuration”Timestamping ensures signatures remain valid after the certificate expires.
| Timestamp Server | URL |
|---|---|
| DigiCert | http://timestamp.digicert.com |
| Sectigo | http://timestamp.sectigo.com |
| GlobalSign | http://timestamp.globalsign.com/tsa/r6advanced1 |
| SSL.com | http://ts.ssl.com |
Always use timestamping in production. Without a timestamp, the signature becomes invalid when the certificate expires.
CI/CD Integration
Section titled “CI/CD Integration”GitHub Actions
Section titled “GitHub Actions”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 }}Azure DevOps Pipeline
Section titled “Azure DevOps Pipeline”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'Troubleshooting
Section titled “Troubleshooting”| Issue | Cause | Resolution |
|---|---|---|
No certificate found | Certificate not in store or agent not running | Verify QCecuring Agent is running and certificate is accessible |
The certificate chain is not trusted | Missing intermediate or root CA | Import full chain into trusted store |
Timestamp verification failed | Timestamp server unreachable | Check network connectivity; try alternate TSA |
Package already signed | Attempting to re-sign | Use --overwrite flag or sign unsigned package |
Key usage not valid for signing | Wrong certificate type | Ensure certificate has Code Signing EKU |
Access denied to private key | Agent permissions | Run signing process with correct user context |
Best Practices
Section titled “Best Practices”- 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