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:
- 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.
- Certificate path — leaf chains to the Fulcio root; the embedded SCT proves the certificate itself was logged.
- Signature — over the artifact digest, or over the DSSE payload (PAE encoding) for attestations.
- Transparency log — verify the Merkle inclusion proof and the SET against Rekor’s public key.
- 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.
- 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 sig | NuGet repo sig | Sigstore | |
|---|---|---|---|
| Key material | Long-lived cert from commercial CA | nuget.org key | Ephemeral, discarded |
| Identity bound | Legal entity (cert subject) | nuget.org itself | OIDC identity (workflow, email) |
| Build provenance (repo/ref/SHA) | — | — | Cert extensions, in-toto |
| Transparency log | — | — | Rekor, append-only |
| Covers | .nupkg | .nupkg on nuget.org | Any 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.