Sigstore removes the hardest part of code signing — long-lived key management — by binding signatures to short-lived certificates issued against OIDC identities, and recording every signing event in a public transparency log. npm ships Sigstore-backed provenance, PyPI accepts attestations, Kubernetes signs its releases with it. Official client libraries exist for Go, Python, Java, JavaScript, and Rust.

.NET was the gap. sigstore-dotnet (docs, Sigstore.Net on NuGet) closes it with a fully managed implementation of the Sigstore client specification. This post covers what the protocol actually does on the wire, and why a native library — not a shelled-out CLI — is the right integration point.

The keyless flow

No pre-provisioned key material exists on either side. The signer proves an identity, not possession of a long-lived key:

 Signer (CI job)          Fulcio (CA)          Rekor (log)        OIDC IdP
      │                       │                     │                 │
      │ 1. get ID token (ambient credentials)       │                 │
      │─────────────────────────────────────────────────────────────▶ │
      │ ◀───────────────────────────────────────────────────── token  │
      │                       │                     │                 │
      │ 2. generate ephemeral key pair              │                 │
      │                       │                     │                 │
      │ 3. CSR + ID token     │                     │                 │
      │──────────────────────▶│ verify token (JWKS) │                 │
      │ ◀── 10-min leaf cert ─│ log cert to CT      │                 │
      │                       │                     │                 │
      │ 4. sign artifact digest with ephemeral key  │                 │
      │                       │                     │                 │
      │ 5. upload (cert, sig, digest)               │                 │
      │────────────────────────────────────────────▶│ append entry    │
      │ ◀────────── inclusion proof + signed timestamp (SET) ─────────│
      │                       │                     │                 │
      │ 6. discard private key                      │                 │
      │ 7. emit bundle (cert + sig + log entry)     │                 │

The private key lives for the duration of one signing operation. There is nothing to rotate, store, or leak afterwards. The certificate is the interesting artifact — Fulcio maps OIDC token claims into X.509 extensions (OID arc 1.3.6.1.4.1.57264.1):

X509v3 Subject Alternative Name: critical
    URI:https://github.com/my-org/my-repo/.github/workflows/release.yml@refs/heads/main
1.3.6.1.4.1.57264.1.8  (Issuer)                 https://token.actions.githubusercontent.com
1.3.6.1.4.1.57264.1.12 (Source Repository URI)  https://github.com/my-org/my-repo
1.3.6.1.4.1.57264.1.13 (Source Repository SHA)  4f2a19b8…
1.3.6.1.4.1.57264.1.14 (Source Repository Ref)  refs/heads/main
Validity: 10 minutes

A verifier can therefore enforce policy at the granularity of workflow, repository, ref, and commit — not just “some certificate chained to some CA”.

What a verifier must actually check

The output of signing is a self-contained bundle (application/vnd.dev.sigstore.bundle.v0.3+json, protobuf-specs):

Sigstore bundle
├── verificationMaterial
│   ├── certificate                  Fulcio leaf (10-min validity)
│   ├── tlogEntries[]                Rekor: log index, Merkle inclusion
│   │                                proof, signed entry timestamp (SET)
│   └── timestampVerificationData    RFC 3161 timestamps (optional)
└── messageSignature | dsseEnvelope  raw signature, or DSSE-wrapped
                                     in-toto attestation

Signature validity is the least of it. A conformant verifier runs this pipeline:

  1. Trust root — bootstrap Fulcio CA certificates, Rekor public keys, and CT log keys via TUF, with metadata expiry and rollback protection. Hardcoding these keys forfeits revocation and rotation.
  2. Certificate path — leaf chains to the Fulcio root; the embedded SCT proves the certificate itself was logged.
  3. Signature — over the artifact digest, or over the DSSE payload (PAE encoding) for attestations.
  4. Transparency log — verify the Merkle inclusion proof and the SET against Rekor’s public key.
  5. Time — establish signing time (SET or RFC 3161 timestamp) and check it falls inside the certificate’s 10-minute validity window. This is what makes a discarded key safe: a signature produced after expiry cannot be backdated without breaking the log.
  6. Identity policy — match SAN and issuer (plus any extension claims) against expected values. Skipping this step reduces the whole scheme to “signed by anyone with a Google account”.

Steps 1–5 are mechanical but easy to get subtly wrong; step 6 is the policy decision every consumer actually cares about. Both belong in a conformance-tested library.

“NuGet already has signing”

It does — but the two NuGet signature types answer narrower questions than they are often credited with:

NuGet author sigNuGet repo sigSigstore
Key materialLong-lived cert from commercial CAnuget.org keyEphemeral, discarded
Identity boundLegal entity (cert subject)nuget.org itselfOIDC identity (workflow, email)
Build provenance (repo/ref/SHA)Cert extensions, in-toto
Transparency logRekor, append-only
Covers.nupkg.nupkg on nuget.orgAny artifact, image, SBOM, attestation

Challenging each guarantee:

  • A repository signature proves the package arrived from nuget.org unmodified. It cannot prove which build produced it, or that the package matches its claimed source repository — nuget.org signs whatever was uploaded.
  • An author signature proves possession of a certificate whose subject a commercial CA once tied to a legal name. A stolen signing certificate produces perfectly valid signatures, and with no transparency log, nothing forces its use into public view. Adoption outside Microsoft’s own packages has remained low — the cost and key-custody burden fall on every individual publisher.
  • Neither covers what modern supply-chain policy consumes: SLSA provenance, in-toto attestations, container images, SBOMs.

Sigstore does not replace NuGet signing; it answers the questions NuGet signing was never designed to answer.

Why a managed library, not a CLI

The standard workaround for .NET has been shelling out to cosign. That works in a CI script and fails as an architecture, because verification is a runtime, in-process decision in most real consumers:

  • a plugin loader deciding whether an assembly’s provenance matches a trusted publisher’s pipeline before loading it;
  • an artifact promotion service gating a staging → production feed on attestations;
  • a deployment controller validating SLSA provenance before rollout.

Forking a Go binary per check means shipping platform-specific executables, parsing CLI output as a security interface, and losing structured results. The full pipeline above — TUF bootstrap included — is what a library call should encapsulate.

Sigstore.Net

sigstore-dotnet implements the client spec in managed code only: no native binaries, no unsafe, System.Security.Cryptography plus BouncyCastle for Ed25519. Targets .NET 8/9/10. It passes the official sigstore-conformance suite (132 tests per framework) and is cross-tested against cosign and sigstore-python.

Verification is policy-first — step 6 is a required input, not an afterthought:

using Sigstore.Verification;

VerificationPolicy policy = VerificationPolicy.ForGitHubActions(
    repository: "my-org/my-repo",
    gitRef: "refs/heads/main");

VerificationResult result = await verifier.VerifyAsync(
    bundleJson, artifact, policy, CancellationToken.None);

Keyless signing picks up ambient CI credentials; no key material is configured:

SigningResult result = await signer.SignAsync(artifact, CancellationToken.None);
await File.WriteAllTextAsync("artifact.sigstore.json", result.BundleJson);

Standard DI registration:

services.AddSigstore();                 // verification only

services.AddSigstoreSigning(options =>  // + signing
{
    options.TokenProvider = new GitHubActionsTokenProvider();
});

Also supported: managed-key verification (VerifyWithKeyAsync), DSSE and message_signature bundle formats, in-toto attestation verification, digest-only (sha256) mode, RFC 3161 timestamps, and TUF bootstrap against the Sigstore Public Good Instance.

Links: GitHub · Documentation · Sigstore.Net on NuGet

Issues, conformance gaps, and PRs are welcome.