Incident analysis · onset 2026-07-19 · still elevated 2026-07-25

The five-minute wall

In late July 2026, PyPI publish workflows across the ecosystem began failing with ExpiredIdentity while signing attestations. The immediate suspicion — that GitHub had quietly shortened its OIDC tokens — turns out to be wrong. The token was always five minutes. What changed is how long each attestation takes to write into Sigstore's transparency log: per-artifact signing latency rose 4–13× starting July 19, and projects with large wheel matrices ran out of clock.

signing latency ↑ 4–13× 300 s hard budget 34 releases measured reconstructed from public transparency logs

Each tick is one distribution file receiving its transparency-log entry, plotted in real seconds after the OIDC token was issued. The red wall at 300 s is where the token expires and ExpiredIdentity is raised. These are real, second-resolution timelines recovered from each file's published attestation.

01

What happened

Starting July 23–24, maintainers of several projects reported that their long-standing, unchanged release workflows using the standard PyPI publishing action failed during the attestation step with sigstore.oidc.ExpiredIdentity. One project with 73 wheels was blocked entirely and remains unreleased. Another, with 119 wheels, needed twelve attempts before one squeaked through. A third project's release passed unnoticed with roughly 91% of its signing budget consumed — one more wheel and it would have joined the thread.

5:00

The budget. GitHub's Actions OIDC token is valid for exactly five minutes from issuance (exp − iat = 300 s). The entire attestation signing loop must finish inside it.

4–13×

The change. Per-artifact transparency-log write latency rose from a ~0.5 s/file baseline to 2–6.7 s/file beginning July 19, with high variance and no acknowledged incident.

0

Client-side changes. The publishing action, its pinned Sigstore stack, workflow files, and wheel counts were all unchanged for weeks to months before the failures began.

The failures look like a GitHub token problem, and were initially debugged as one. The data shows they are a Sigstore write-path latency problem colliding with a token lifetime that was never generous — and a client-side check that gives up earlier than it cryptographically needs to.
02

Mechanism: two clocks, one loop

When the publishing action runs with attestations enabled, it performs these phases in order. Only one phase is governed by the 5-minute OIDC clock — and it is not the phase people watch.

phase A
Checks & token exchange
Dists are validated; a first OIDC token is exchanged for a PyPI upload token.
⏱ no attestation clock
phase B · t=0
Fresh OIDC token + Fulcio cert
The attestation step fetches its own Sigstore-audience token, then immediately requests one signing certificate (cached for the whole loop).
⏱ token clock starts · 300 s
⏱ cert clock starts · 600 s
phase C
Serial signing loop
For each file: sign locally, then 1–2 network round trips — an optional RFC 3161 timestamp and a Rekor transparency-log entry. One file at a time.
⏱ must finish < 300 s
phase D
Upload to PyPI
Twine uploads files + attestations using the PyPI token from phase A. Can take many minutes; doesn't touch the OIDC clock.
⏱ no attestation clock

The failure is triggered by a specific ordering in the Sigstore Python client: before every signature it checks token validity first, and only then would it return the still-valid cached certificate. Because the token (300 s) expires before the certificate (600 s), the client raises ExpiredIdentity at t=300 s even while holding a certificate that is cryptographically valid until t=600 s.

Why "our 7-minute workflow used to pass" was never a contradiction. Total publish time ≈ phase C + phase D. Historically phase C was 30–100 seconds for even the largest matrices; the minutes people saw were mostly upload. A 7-minute publish and a 5-minute token were always compatible — the margin was simply invisible.
03

Evidence: 34 releases, reconstructed to the second

Every published attestation carries its transparency-log inclusion time, and the shared signing certificate records when the loop began. That makes the signing loop of any past release publicly reconstructable. Each point below is one real release: its mean per-file signing latency, sized by wheel count. Dashed lines mark where the 300-second budget runs out for a given matrix size. Hover for detail; click a highlighted point to load it into the replay track above.

baseline (<1 s/file) degraded past a failure threshold budget-exhaustion thresholds point size ∝ number of files

Stable for weeks

June 1 → July 18: 18 consecutive measurements across nine projects sit in a tight 0.43–0.96 s/file band (one outlier at 1.7). Daily dev builds from a single constant-config project form an unbroken flat baseline.

Onset: July 19, ~07:00 UTC

Between a fast run at 08:14 on July 18 and the next daily build at 06:56 on July 19, latency quadruples. A second project confirms independently at 11:13 the same day (2.34 s/file).

Volatile, and not load

Latency swings 0.45 → 6.7 s/file through July 19–25, with a full recovery window on July 20 evening. Global log write volume during slow windows was no higher than during fast June windows — this is a slower write path, not a busier one.

04

Hypotheses, ruled out and in

ruled out GitHub shortened OIDC tokens

A public feature request from May 2025 already describes the token as expiring in 5 minutes; GitHub's own documentation example shows iat→exp of 5 minutes; and a live token decoded during this incident shows exp − iat = 300 s with nbf = iat − 300 s. The lifetime long predates the failures.

ruled in Transparency-log write latency

The only quantity that moved. Per-entry latency is elevated 4–13× since July 19, spread across nearly every file in slow loops (2–5 s gaps) rather than a few stalls, fluctuating hour to hour, and still elevated on the morning of July 25. The service status page reports no incident for July 17–23.

ruled out The publishing action changed

The action's release branch last changed May 21, 2026 (a CI dependency bump). Its Sigstore client stack was pinned in December 2025. Affected projects published successfully on this exact code on June 7, June 17, and July 20.

ruled out Bigger wheel matrices

File counts are flat: the 119-wheel project has shipped exactly 119 files every release since June 1; the 73-wheel project has shipped 73 since March. Nobody's matrix grew into the wall — the wall moved.

ruled out A demand spike on the log

From consecutive log indices, global write throughput during fast June windows reached 160–210 entries/s; during the slowest measured July window it was ~89 entries/s. Slowness does not track load in this data.

context December's client bump raised the stakes

A December 2025 upgrade of the pinned Sigstore client (3.6 → 4.1) added per-signature timestamp-authority support alongside the log write — more serial network per file. It didn't cause the incident (seven clean months followed), but it means each unit of backend latency now costs more.

05

Timeline

2025-05-08
Token lifetime documented at 5 minutes
A public request asks GitHub for OIDC tokens longer than the current 5-minute expiry — establishing the lifetime 14 months before this incident.
2025-12-19
Publishing action pins Sigstore client 4.1
Rekor v2 readiness and RFC 3161 timestamp support arrive in the pinned stack. Per-file signing now includes 1–2 serial network calls.
2026-04-01
Client's cert-expiry handling flagged
A Sigstore client issue notes the cached signing certificate is never renewed on expiry — filed as a UX improvement, low priority at the time.
2026-06-07 → 07-18
Healthy baseline
18 measured releases sign at 0.43–0.96 s/file. A 119-wheel matrix completes its loop in 98 s — 33% of budget.
2026-07-19 ~07:00 UTC
Latency degradation begins
Daily dev builds jump from 0.50 to 1.91 s/file overnight; an unrelated project measures 2.34 s/file four hours later. No status-page incident.
2026-07-20 evening
Temporary recovery
Two large releases (104 and 119 wheels) sign at full baseline speed — 0.45–0.47 s/file. The 119-wheel loop takes 55 s. Its maintainers, watching total step time, believe they passed "with ~10 seconds to spare"; the real signing margin was 245 seconds.
2026-07-22 22:13 UTC
A near miss nobody noticed
A 42-wheel release signs at 6.66 s/file — 273 s of a 300 s budget, 91% consumed. It passes; no issue is filed.
2026-07-23 01:47 UTC
12-attempt release
The 119-wheel project's release fails repeatedly at the wall; the attempt that finally lands signs at 2.11 s/file, finishing with 51 s to spare.
2026-07-24
73-wheel project fully blocked; issues filed
Repeated ExpiredIdentity failures ~5 minutes into the publish step. The report lands on the publishing action's tracker and is triaged to the Sigstore client within hours.
2026-07-24 14:28 UTC
Client fix proposed
A one-ordering-change PR: return the valid cached certificate before checking token expiry — extending the effective budget from the token's 300 s to the certificate's 600 s.
2026-07-25 (this analysis)
Degradation ongoing, fix unreleased
Morning builds measure 2.35 and 3.32 s/file. The PR is open; no client release, dependency bump, or action release has shipped. The 73-wheel project remains unreleased.
06

Will the proposed fix resolve it?

Mostly — but it treats the symptom, not the cause, and it has a measurable coverage gap. The fix reorders one check so a valid cached certificate is used even after the token expires. That doubles the effective signing budget from 300 s (token) to 600 s (certificate lifetime). Certificate renewal on expiry is still unimplemented, so 600 s is a hard wall of exactly the same shape. Try it against the observed latencies:

observed latencies
real matrices
today · token budget300 s
with the fix · certificate budget600 s
matrixfails today when latency ≥fails with fix when latency ≥worst observed so farwith-fix outlook
42 wheels~7.1 s/file~14.4 s/file6.66 s/fileclears worst case
73 wheels~4.1 s/file~8.2 s/file6.66 s/fileclears, thin margin
119 wheels~2.5 s/file~5.0 s/file6.66 s/filestill fails at worst case
150 wheels~2.0 s/file~4.0 s/file6.66 s/fileexposed at Jul 25 levels

So the fix is necessary and worth shipping — it would unblock the 73-wheel project even at the worst latency observed — but it is not sufficient for the largest matrices if latency holds at July-22 levels, and it does nothing for the underlying regression. It also has a delivery chain: client release → attestation library bump → publishing action release. None of those steps had shipped as of July 25, though the action's moving release/v1 tag means users inherit it automatically once they do.

Durable fixes exist and are cheap. The Actions OIDC endpoint can be called again at any point during the job — the signing step could simply re-fetch the token when it nears expiry, removing the token wall entirely. Alternatively (or additionally), parallelizing the signing loop — which the Rekor v2 launch guidance itself recommends — collapses N serial round trips into a few concurrent batches. Work toward native trusted-publishing support with adaptive token refresh in the upload tooling points in the same direction.
07

The structural risk underneath

Even after this incident resolves, the architecture retains a shear point: a serial, per-artifact write to a shared transparency log, inside a fixed small time budget, with latency owned by a third party.

08

Method, caveats & sources

How this was reconstructed. For each file of each release, PyPI's integrity API serves the file's attestation, which embeds its transparency-log entry — including the log's server-assigned inclusion time and global index. All files in a release share one signing certificate, whose notBefore marks the start of the loop (the certificate is requested immediately after the token, and its observed lifetime is exactly 10 minutes). Sorting inclusion times reconstructs the loop; gaps between consecutive entries measure per-file latency; index deltas over time estimate global log throughput. Upload timestamps from PyPI's package metadata bound the upload phase.

report
gh-action-pypi-publish #415 — Attestation error

The triggering report: ExpiredIdentity during publish; triage from token suspicion to client fix.

client issue
sigstore-python #1729 — Signer should handle ExpiredCertificate

April 2026 issue documenting cert caching without renewal; hosts the incident's root-cause discussion, live token decode, and prior-art links.

proposed fix
sigstore-python #1838 — Return cached cert first when signing

The reorder that extends the effective budget from token lifetime (300 s) to certificate lifetime (600 s). Open, unreleased as of July 25.

prior art
actions/toolkit #2048 — Request for OIDC token expiry > 5 mins

May 2025 request establishing the 5-minute lifetime long before this incident; see also community discussions #190629 and #160973.

docs
GitHub Docs — Understanding the OIDC token

The documented token structure; the example's iat/exp are five minutes apart.

alternative
gh-action-pypi-publish #360 — Use twine's built-in Trusted Publishing

Direction for moving token handling into the upload tool, enabling refresh instead of a single fixed-lifetime fetch.

infra
Sigstore blog — sigstore.dev and Rekor evolution (June 2026)

Rekor v1 stays the default log for the foreseeable future; v1 is in maintenance mode.

infra
Sigstore blog — Rekor v2 GA

v2 batches writes, adds seconds of per-entry integration latency, and recommends client-side parallelism.

status
status.sigstore.dev

No incidents reported July 17–23, the window in which the measured degradation began.

data
PyPI Integrity API · PyPI attestations docs

The provenance endpoints from which every timing in this page was reconstructed.