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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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).
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.
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.
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.
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.
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.
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.
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.
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.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:
| matrix | fails today when latency ≥ | fails with fix when latency ≥ | worst observed so far | with-fix outlook |
|---|---|---|---|---|
| 42 wheels | ~7.1 s/file | ~14.4 s/file | 6.66 s/file | clears worst case |
| 73 wheels | ~4.1 s/file | ~8.2 s/file | 6.66 s/file | clears, thin margin |
| 119 wheels | ~2.5 s/file | ~5.0 s/file | 6.66 s/file | still fails at worst case |
| 150 wheels | ~2.0 s/file | ~4.0 s/file | 6.66 s/file | exposed 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.
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.
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.
The triggering report: ExpiredIdentity during publish; triage from token suspicion to client fix.
April 2026 issue documenting cert caching without renewal; hosts the incident's root-cause discussion, live token decode, and prior-art links.
The reorder that extends the effective budget from token lifetime (300 s) to certificate lifetime (600 s). Open, unreleased as of July 25.
May 2025 request establishing the 5-minute lifetime long before this incident; see also community discussions #190629 and #160973.
The documented token structure; the example's iat/exp are five minutes apart.
Direction for moving token handling into the upload tool, enabling refresh instead of a single fixed-lifetime fetch.
Rekor v1 stays the default log for the foreseeable future; v1 is in maintenance mode.
v2 batches writes, adds seconds of per-entry integration latency, and recommends client-side parallelism.
No incidents reported July 17–23, the window in which the measured degradation began.
The provenance endpoints from which every timing in this page was reconstructed.