Safeguard
Open Source Security

NuGet package signing, source mapping, and verifying pack...

A practical guide to NuGet package signing, source mapping, and provenance verification for .NET teams — with commands, config, and a troubleshooting checklist.

Karan Patel
Cloud Security Engineer
8 min read

Every dotnet add package command is an act of trust: you're pulling compiled code from a public feed and running it inside your build. When that trust is misplaced — a compromised maintainer account, a typosquatted package name, an unsigned artifact swapped in transit — the blast radius extends straight into production. NuGet package signing is the primary control that lets you verify a package actually came from the publisher you think it did, and hasn't been tampered with since. This guide walks through generating and applying a signing certificate, configuring trust policies, mapping packages to their expected sources, and verifying provenance end-to-end for a .NET build pipeline. By the end, you'll have a repeatable process for signing your own packages and enforcing verification on everything you consume — including how Safeguard extends these checks continuously across your software supply chain.

Step 1: Understand What NuGet Package Signing Actually Verifies

Before touching tooling, it's worth being precise about what signing does and doesn't guarantee. A signed .nupkg carries an author signature (proving who published it) and, when pushed through nuget.org, a repository signature (proving it passed through the official feed unmodified). Signature verification confirms:

  • The package hasn't been altered since it was signed.
  • The signing certificate chains to a trusted root and hasn't been revoked.
  • The timestamp on the signature is valid, so an expired certificate doesn't retroactively invalidate old packages.

What it does not verify is that the code inside is safe — signing proves identity and integrity, not intent. That's why signing has to be paired with source verification and provenance checks, which we'll cover in later steps. If you only take one thing from this section, take this: signing answers "who published this and was it changed," not "should I trust what it does."

Step 2: Generate or Obtain a Code Signing Certificate

You'll need a certificate from a Certificate Authority that issues code-signing certs trusted by NuGet's chain (DigiCert, Sectigo, GlobalSign are common choices). Self-signed certificates work for internal testing but won't be trusted by consumers outside your organization.

For CI pipelines, store the certificate in a secrets manager or an HSM-backed key vault rather than committing it to the repo. If you're on Azure DevOps or GitHub Actions, use a secure file or encrypted secret and pull it at build time:

# GitHub Actions snippet
- name: Restore signing cert
  run: |
    echo "${{ secrets.NUGET_SIGNING_CERT_B64 }}" | base64 -d > signcert.pfx

Never let the private key touch disk in plaintext for longer than the build step requires, and scrub it in a cleanup step afterward.

Step 3: Sign the Package

With the .NET SDK, signing is a single CLI call against the .nupkg you've already packed:

dotnet nuget sign MyLibrary.1.4.0.nupkg \
  --certificate-path signcert.pfx \
  --certificate-password $CERT_PASSWORD \
  --timestamper http://timestamp.digicert.com \
  --output ./signed

The --timestamper flag is not optional in practice — without an RFC 3161 timestamp, your signature becomes invalid the moment the certificate expires, even for packages published years earlier. Verify the signature locally before you push anything:

dotnet nuget verify ./signed/MyLibrary.1.4.0.nupkg

This should report a valid author signature and, once published, a valid repository signature layered on top of it.

Step 4: Configure Signature Verification on the Consuming Side

Signing your own output is half the job; the other half is requiring signatures on everything you consume. In NuGet.Config, enable signature validation and pin the certificate fingerprints you trust:

<configuration>
  <config>
    <add key="signatureValidationMode" value="require" />
  </config>
  <trustedSigners>
    <author name="ContosoEngineering">
      <certificate fingerprint="3F8B...A21C" hashAlgorithm="SHA256" allowUntrustedRoot="false" />
    </author>
    <repository name="nuget.org" serviceIndex="https://api.nuget.org/v3/index.json">
      <certificate fingerprint="0E5F...9B44" hashAlgorithm="SHA256" allowUntrustedRoot="false" />
    </repository>
  </trustedSigners>
</configuration>

Setting signatureValidationMode to require means any unsigned or invalidly signed package fails restore outright — this is the setting that turns signing from a nice-to-have into an enforced gate. Roll it out to CI first, in report-only mode if your tooling supports it, before flipping it to blocking on developer machines, since a poorly scoped rollout will break restores for legitimate internal packages that predate your signing policy.

Step 5: Map Packages to Trusted NuGet Feeds

Signature checks matter less if a build can silently pull the same package name from an unexpected source. Source mapping in NuGet.Config pins which feed is authoritative for which package prefixes, closing off dependency-confusion attacks where an attacker publishes a same-named package to a public feed hoping your internal build resolves it there instead:

<packageSourceMapping>
  <packageSource key="contoso-internal">
    <package pattern="Contoso.*" />
    <package pattern="Internal.*" />
  </packageSource>
  <packageSource key="nuget.org">
    <package pattern="*" />
  </packageSource>
</packageSourceMapping>

This is the practical form of nuget source verification: instead of trusting whichever feed answers first, you explicitly declare that Contoso.* packages only ever resolve from your internal feed. Combined with restricting your list of configured sources to a small set of trusted NuGet feeds, this closes one of the most common real-world NuGet supply chain attack paths, and it costs nothing beyond a one-time config change per repository.

Step 6: Establish Package Provenance for .NET Builds

Signing and source mapping tell you who published a package and where it came from. Provenance goes a step further: it ties a specific published artifact back to the exact source commit, build system, and build steps that produced it. For package provenance dotnet projects, the emerging standard is SLSA-style build provenance combined with Sigstore/dotnet nuget sign metadata, plus reproducible build attestations where your build system supports them.

At minimum, capture and store alongside every release:

  • The commit SHA and repository URL that produced the build.
  • The CI job ID and runner identity (self-hosted vs. GitHub-hosted, vs. ephemeral cloud runner).
  • A hash manifest of build inputs (SDK version, restored package versions, lock file).
dotnet restore --locked-mode  # fails if lock file doesn't match resolved graph

--locked-mode combined with a committed packages.lock.json gives you a verifiable, reproducible input set — if two builds from the same commit and lock file produce packages with different hashes, that's a signal worth investigating immediately, not after an incident.

Step 7: Automate Verification in CI, Not Just at Release Time

A signing and verification policy that only runs at publish time misses the more common attack window: a compromised dependency getting pulled into a routine build days or weeks before release. Add a verification gate as an early pipeline step:

dotnet restore
dotnet nuget verify $(find ~/.nuget/packages -name '*.nupkg') --all

Fail the build on any unsigned, expired, or untrusted-root package, and alert on any package resolving from a source outside your configured package source mapping. Treat these failures the same severity as a failed unit test — because a compromised transitive dependency is functionally the same risk.

Troubleshooting and Verification Checklist

  • "Certificate chain not trusted" errors: usually means the CA's intermediate certificate isn't in the trust store used by your build agents. Import the full chain, not just the leaf cert, and confirm the same trust store is used across all runners.
  • Signature valid locally but fails in CI: check clock skew on build agents — timestamp validation is sensitive to system time, and containers with drifted clocks will reject otherwise-valid signatures.
  • Restore breaks after enabling signatureValidationMode=require: audit your dependency tree for unsigned internal or legacy packages first with dotnet nuget verify in report mode; re-sign or replace them before enforcing.
  • Package source mapping silently not applied: confirm you're on a NuGet client version that supports packageSourceMapping (5.9+) and that the config file is actually being picked up — run dotnet nuget config paths to confirm precedence across machine, user, and repo-level configs.
  • Verifying provenance after the fact: for any package already in production, cross-reference the package's published hash against the commit history and CI logs of the source repo. A mismatch, or an inability to reproduce the artifact from source, is a red flag worth treating as a potential compromise until proven otherwise.
  • Quick sanity check across a whole solution: dotnet list package --vulnerable --include-transitive won't check signatures, but pairing it with a signature-verification pass gives you both integrity and known-CVE coverage in one CI stage.

How Safeguard Helps

Manually maintaining certificate trust lists, source mappings, and provenance records across dozens of repositories doesn't scale, and it's exactly the kind of control that quietly rots the moment the person who set it up moves teams. Safeguard continuously monitors your NuGet dependency graph — flagging unsigned packages, unexpected feed resolution, and provenance gaps before they reach a build, not after a postmortem. It maps every package in your .NET projects back to its source repository and build history, verifies signatures and source mappings against the policies you've defined, and surfaces drift the moment a dependency starts resolving from an untrusted feed or arrives without the expected attestation. Instead of scattering NuGet.Config enforcement and CI scripts across every repo and hoping they stay in sync, Safeguard gives you one place to see supply chain trust posture across your entire portfolio — so the controls in this guide keep working long after the initial rollout.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.