ci-oidc-provider Logo

ci-oidc-provider

Supply chain attestation for open source projects on self-hosted CI

GoCI/CDDockerSecuritySigstoreOpen Source

The XZ Utils Problem

In March 2024, a malicious maintainer embedded a backdoor in the XZ Utils release tarball — a file that was never present in git. The attack reached Fedora 40 and Debian Sid before being discovered by accident. No existing tool detected the discrepancy between the git repository and the release artifact.

The root cause is structural: most open source projects build release tarballs on a maintainer's laptop, sign them with a personal GPG key, and upload. GPG proves who signed the tarball, not where or how it was built. A compromised maintainer can inject arbitrary files, and the signature remains valid.

The Goal

Every file in every open source package should be verified and traced back to a single, specific commit in its repository. Release artifacts must be built by auditable CI, not on developer workstations. Each artifact must carry a cryptographic attestation proving which CI system built it, from which repository, at which commit.

GitHub Actions provides this via Sigstore for GitHub-hosted projects. But projects that value sovereignty and self-hosting — on Gitea, Forgejo, Jenkins, Woodpecker, or Drone — have had no equivalent path. ci-oidc-provider fills that gap.

Three Tools, One Mission

ci-oidc-provider

source

Producer — the certificate authority

A composite CI action that acts as its own certificate authority. During a CI build, it reads the build environment, creates an X.509 certificate embedding the commit SHA, repository, workflow ref, and other provenance claims as Fulcio-compatible OID extensions, then signs the certificate with a CA key. Cosign uses this certificate when signing artifacts — binding the signature to the exact build that produced it.

No Fulcio server. No Rekor transparency log. No sidecar container. The action is the CA.

Gitea ActionsGitHub ActionsForgejoSidecar mode (Jenkins, Woodpecker, Drone)

ci-oidc-verify

source

Consumer — verification CLI

A single-command tool that verifies Sigstore-attested artifacts and produces machine-readable provenance receipts. Supports constraint flags, YAML policy files, multi-attestation cross-checks, and tarball-to-git content diffing. Designed so distro packagers can verify upstream tarballs and chain the provenance into their own builds.

ci-oidc-deps

source

Ecosystem — dependency attestation scanner

Scans your dependency lockfiles and reports which dependencies have attestations and which don't. Parses 10 lockfile formats across Go, Rust, npm, Python, Ruby, .NET, Java, yarn, and pnpm. Generates coverage badges, SARIF reports for code scanning, and PR comments that make attestation gaps visible — creating ecosystem pressure for adoption.

3

Open Source Tools

10

Lockfile Parsers

221

Unit Tests

L3

SLSA Build Level

How It Works

1

Generate keys once

Run ci-oidc-provider keygen to create a CA keypair and a cosign signing keypair. Store both as CI secrets. Commit the OIDC discovery files (.well-known/jwks) to your repo — your forge's existing HTTPS serves them.

2

Add the action to your CI workflow

One step invokes the composite action. It reads the CI environment (commit, repo, ref, workflow), creates an X.509 certificate with those provenance claims embedded as Fulcio OID extensions, and signs it with the CA key. The certificate's public key matches your cosign key.

3

Sign artifacts with cosign

Cosign signs Docker images and binary blobs using the cosign key and the provenance certificate. The Sigstore bundle ties the artifact's cryptographic signature to the certificate that proves where and how it was built. No external Fulcio server or Rekor transparency log required.

4

Anyone can verify

Consumers run cosign verify --key cosign.pub or use ci-oidc-verify for richer provenance extraction. The certificate chain proves: CA key signed a cert that says "this artifact was built from commit X in repo Y by workflow Z." No accounts, no API keys, no vendor trust.

Design Principles

No external infrastructure

No Fulcio server, no Rekor transparency log, no new domains, no new TLS certificates. The action is the CA. OIDC discovery files are static JSON served by your forge's existing raw file endpoint.

Self-contained verification

Every release tarball includes the binary, its cosign signature, and the public key. Verification requires only cosign — no network calls, no third-party services, no accounts.

Fulcio-compatible certificates

Provenance claims use the same OID extensions as Fulcio (1.3.6.1.4.1.57264.1.*). Existing Sigstore verification tooling reads the certificates without modification.

Two modes, one codebase

Composite action for Gitea/GitHub/Forgejo (runs in-step, zero infrastructure). Sidecar mode for Jenkins/Woodpecker/Drone (long-running container with API-based context resolution).

What This Prevents

Tarball injection attacks — release artifacts must be built by CI. Hand-crafted tarballs have no attestation and fail verification.

Compromised maintainer keys — GPG signatures are replaced by CI-issued, commit-bound attestations. No long-lived key to steal.

Silent workflow tampering — the attestation includes the workflow ref and SHA, so any change to the build process is captured.

Provenance forgery — the CA key is a CI secret, inaccessible to build steps in sidecar mode. In action mode, the certificate is issued in an isolated step before artifact signing.

Don't Trust Us — Build It Yourself

Signatures prove that our CI built a binary. But what if our CI was compromised? The strongest guarantee comes from independent reproduction: clone the repository at the attested commit, build it in your own CI, and compare the result against the binary we provide. If the outputs match, you know the source code is what produced the artifact — regardless of whether you trust our infrastructure.

Every tool in this suite is open source and designed to be reproducibly built. The attestation gives you the exact commit SHA, so you always know which source to build from. We believe independent building is what gives even more confidence than any signature alone.

Technical Deep Dive

Signing Architecture

The core insight is that Fulcio's role — issuing short-lived certificates with provenance claims — can be performed by the CI action itself. The action reads the build environment, extracts the cosign public key from the encrypted cosign key, creates an X.509 certificate containing both the public key and provenance OID extensions, and signs it with the CA key:

CA key (OIDC_SIGNING_KEY)
  └─ signs X.509 certificate containing:
        ├─ cosign public key (extracted from COSIGN_KEY)
        └─ Fulcio OID extensions:
            ├─ 1.3.6.1.4.1.57264.1.1   issuer URL
            ├─ 1.3.6.1.4.1.57264.1.12  source repository URI
            ├─ 1.3.6.1.4.1.57264.1.13  source repository digest (commit SHA)
            ├─ 1.3.6.1.4.1.57264.1.14  source repository ref
            ├─ 1.3.6.1.4.1.57264.1.9   build signer URI (workflow)
            ├─ 1.3.6.1.4.1.57264.1.20  build trigger
            └─ 1.3.6.1.4.1.57264.1.21  run invocation URI

cosign key (COSIGN_KEY)
  └─ signs Docker images (cosign sign --key --certificate)
  └─ signs binary blobs (cosign sign-blob --key)

The certificate's public key matches cosign's public key, so consumers can verify both the signature (via cosign) and the provenance (by reading the certificate's OID extensions). This is the same certificate format that Fulcio produces — the only difference is that the CA is your own key rather than Sigstore's public CA.

Action vs. Sidecar Mode

Action mode (Gitea, GitHub, Forgejo): Runs as a composite action step inside the workflow. Reads provenance from GITHUB_* environment variables. Outputs a certificate file. Zero infrastructure — the action binary is fetched from the action repository.

Sidecar mode (Jenkins, Woodpecker, Drone): Runs as a long-lived container alongside the CI platform. Authenticates runners via native credentials, independently resolves build context by querying the CI platform's API (runners cannot supply false claims), and issues signed OIDC tokens. Supports Prometheus metrics, structured audit logging, and emergency key rotation.

# Action mode (Gitea/GitHub) — add to your workflow:
- name: Issue signing certificate
  uses: https://git.carefuldream.com/cdos/ci-oidc-provider@main
  with:
    signing-key: ${{ secrets.OIDC_SIGNING_KEY }}
    cosign-key: ${{ secrets.COSIGN_KEY }}
    issuer: https://git.carefuldream.com/cdos/my-project.git/raw/main

# Sidecar mode (Jenkins/Woodpecker/Drone) — docker-compose:
ci-oidc-sidecar:
  image: zot.carefuldream.com/cdos/ci-oidc-provider:latest
  volumes:
    - oidc-keys:/data

OIDC Discovery via Git

OIDC discovery (RFC 8414) requires a configuration document at a well-known URL. Git forges serve raw files over HTTPS. By committing the .well-known/openid-configuration and .well-known/jwks files to a repository, the forge's existing HTTPS endpoint becomes a compliant OIDC discovery URL. The JWKS is version-controlled in git, providing an auditable history of every key change.

In our infrastructure, these files are served by gitview — a lightweight Git server that hosts read-only repositories with a web UI for browsing code, commit history, and downloading signed release binaries. Gitview's raw file endpoint serves the OIDC discovery documents, and its release system verifies cosign signatures server-side before accepting uploads.

Verification Pipeline

ci-oidc-verify uses the sigstore-go library (not a cosign subprocess) for all cryptographic verification. The pipeline runs eight steps:

1

Hash the artifact locally (SHA-256 + SHA-512, never taken from the attestation)

2

Discover or load the Sigstore bundle (.bundle, .sigstore.json, or Rekor search)

3

Verify the cryptographic signature and certificate chain

4

Extract provenance claims from certificate extensions (v1 GitHub + v2 Fulcio OIDs)

5

Apply CLI constraint flags (--issuer, --commit, --repo, --ref with prefix matching)

6

Evaluate YAML policy file (trusted issuers with globs, ref regex, visibility, runner type)

7

Cross-check multiple attestations (hostname-based independence, claim agreement)

8

Format output as human-readable text or JSON provenance receipt (schema v1.0)

The JSON provenance receipt is the contract between ci-oidc-verify and downstream tooling. Distro packagers can embed it alongside source packages to create multi-hop provenance chains without depending on ci-oidc-verify itself.

Dependency Scanner

ci-oidc-deps parses lockfiles — flat, already-resolved dependency lists that every modern package manager produces. No dependency resolution engine, no build toolchain required. Each parser is a few hundred lines:

go.sum
Cargo.lock
package-lock.json
requirements.txt
poetry.lock
yarn.lock
pnpm-lock.yaml
Gemfile.lock
packages.lock.json
gradle.lockfile

For each dependency, the scanner checks attestation sources in priority order: local cache, public attestation index, package registry metadata (npm provenance API), convention-based URL probing, and transparency log search. Results are cached locally with configurable TTL.

The policy engine supports exemptions with expiry dates — critical for practical adoption. Without exemptions, the first scan of any real project produces a wall of "not attested" results for dependencies outside your control. Exemptions acknowledge reality while ensuring gaps are revisited. Output formats include human-readable, JSON, SARIF (for GitHub/Gitea code scanning integration), shields.io badge URLs, and PR comment markdown.

Trust Chain

CA key (generated once, stored as CI secret)

→ Signs X.509 certificate during CI build

→ Certificate embeds: commit SHA, repo, ref, workflow, issuer

→ Certificate's public key matches cosign's public key

→ cosign signs artifact with key + certificate

→ Signature + certificate published alongside artifact

→ Consumer verifies: cosign.pub → signature → certificate → provenance

SLSA Build L3 Compliance

SLSA L3 RequirementImplementation
Hosted build platformGitea + act_runner, Jenkins, Forgejo, Woodpecker, Drone
Provenance authenticated via digital signatureX.509 certificate with Fulcio OID extensions, cosign signature
Provenance from build platform, not tenantAction reads CI environment; sidecar resolves context via platform API
Signing key inaccessible to build stepsSidecar: key on exclusive volume. Action: CA key used only in certificate issuance step
All provenance fields from trusted control planeClaims read from CI environment variables or platform API; runner cannot inject false claims

Live Pipeline

Every push to a cdos project triggers this pipeline. All three tools are signed by their own CI — the signing infrastructure signs itself:

 1. Build test image
 2. Run tests inside container
 3. Build + push Docker image to zot.carefuldream.com
 4. Issue signing certificate        (ci-oidc-provider action)
 5. Sign Docker image with cosign    (key + certificate)
 6. Build cross-compiled binaries    (linux/amd64, linux/arm64,
                                      darwin/amd64, darwin/arm64)
 7. Sign each binary with cosign
 8. Create self-contained tarballs   (binary + sig + cosign.pub)
 9. Upload releases to gitview       (server verifies checksums)
10. Mirror git to gitview

Results are browsable at git.carefuldream.com. Docker images with signature badges at zot.carefuldream.com.

Production Features

Self-Contained Releases

Every binary release is a tarball containing the binary, its cosign signature, and the public key. Verify offline with one command: cosign verify-blob --key cosign.pub --signature *.sig binary

Sidecar: Prometheus Metrics

Counters for tokens issued/failed, key rotations, auth failures. Gauges for active keys, key age, time until expiry. Summary for token issuance latency.

Sidecar: Structured Audit Logging

Every security-relevant event (token issued, auth failed, key rotated, key expired) logged as JSON to a dedicated audit file, separate from operational logs.

Multi-Attestation Cross-Check

Verify artifacts attested by multiple independent CIs. Independence determined by issuer hostname. Attestations must agree on commit, ref, repository, and artifact hash.

Technology

Go
Sigstore / cosign
X.509 Certificates
Fulcio OID Extensions
OIDC / JWT / JWKS
Docker Swarm + k8s
Prometheus metrics
SARIF output
10 lockfile formats
YAML policy engine
4 architectures
RFC 8414 / RFC 7517

Test Coverage

221 unit tests across all three components. Coverage percentages reflect testable application logic — the uncovered remainder is stdlib error paths (crypto/rand exhaustion, disk full) and integration code that delegates to Sigstore infrastructure.

ci-oidc-provider (93 tests)

config
100%
keys/kms
100%
metrics
100%
adapter/gitea
93%
server
91%
token
90%
audit
88%
adapter/generic
87%
adapter/woodpecker
84%
keys
82%
adapter/jenkins
82%
adapter/drone
79%

ci-oidc-verify (64 tests)

policy
98%
constraint
93%
crosscheck
93%
discovery
84%
advanced
55%
receipt
51%
verify
31%

ci-oidc-deps (64 tests)

output
90%
policy
89%
baseline
88%
lockfile
74%
discovery
65%
Open Source

Browse, Clone, Verify

All three tools are open source, hosted on our own infrastructure, and signed by their own CI pipeline. Browse the source, download signed binaries, or clone and build from source.