Handle 409s from Rekor by fetching the entry by Hayden-IO · Pull Request #647 · sigstore/sigstore-go · GitHub
Skip to content

Handle 409s from Rekor by fetching the entry - #647

Merged
Hayden-IO merged 1 commit into
sigstore:mainfrom
Hayden-IO:retry-rekor
Jul 28, 2026
Merged

Hayden-IO merged 1 commit into
sigstore:mainfrom
Hayden-IO:retry-rekor

Conversation

@Hayden-IO

@Hayden-IO Hayden-IO commented Jun 16, 2026 •

Copy link
Copy Markdown
Contributor

When a CI pipeline fails midway, a client may try to upload the same
signed entry again. This will result in Rekor returning a 409.
Additionally, occasionally Rekor v1 fails with a 409 - the root cause
isn't known, it's a flake in Trillian.

This change gracefully handles a 409 Conflict by retrieving the
transparency log entry. For Rekor v1, this is simple, just use the UUID
in the response to lookup the entry by UUID.

Rekor v2 is not handled because we aren't able to fully construct the
TLE without the log's public key, which is the TLE's LogID. A comment
has been added with details on how someone could implement it in the
future.

Comment thread pkg/sign/transparency.go Fixed
Comment thread pkg/sign/transparency.go Fixed
Comment thread pkg/sign/transparency.go Outdated

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@steiza @cmurphy @kommendorkapten Wanted to get some input on options here.

With Rekor v1, when we fetch an entry from one of the log's read APIs, the log returns a complete TLE structure. For Rekor v2, the client must reconstruct the TLE, since there is no endpoint. This is almost doable:

  • The index is returned in both a custom header and the error message when responding with a 409
  • The inclusion proof is constructed by fetching the relevant tiles computed with the index
  • The canonicalized body and kind/version come from the entry bundle computed with the index
  • The checkpoint comes from the log

Unfortunately, the log ID cannot be recomputed with anything from a Rekor v2 log. We've defined the log ID as the encoded public key hash, which comes from the trusted root (or in Rekor v1's case, it's either returned by the log or can be fetched from /api/v1/log). We aren't guaranteed to have the trusted root here.

The way we represent a log according to C2SP is a combination of the log's origin (e.g. log2025-1.rekor.sigstore.dev) and the first 4 bytes of its key ID as per c2sp.org/signed-note. Those values are included in the checkpoint.

So this will all be resolved going forward if we standardize on c2sp.org/tlog-proof and get rid of these redundant fields in the TLE. But what do we now?

Options:

  • Rekor v2 uploads simply don't handle 409s
  • Change the key lookup logic in pkg/verify/tlog to use the origin/key ID if there is no log ID present in the bundle. We also have to relax some of the constraints when parsing the bundle that mandate a log ID. I'd add a conformance test to verify if other clients can handle no log ID as well - some might, IIRC some just try to verify with all log keys.
  • Inject the trusted root into the context higher up the call stack. If a trusted root is available, then fetch the entry and populate the log ID when the response is a 409.

Thoughts? Other options?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ignoring 409 will break certain flows, as the same entry can't be uploaded again, but the expected failure rate is low. How bad would it be if we resign then try again from a client side? For ECDSA this would work as the signatures are non-determinisitc, and getting a new signature would so create a new TLE, and so not hit by the deduplication. How bad of an idea is that?

If we think that is a terrible idea (to leave stray entries in the log) I would vote on injecting the trusted root, as verifying the received proof is before constructing the bundle is a good thing anyways.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that's a significant ask. Unfortunately, I don't have an example of a pipeline where this is coming up, I assume it's from bespoke pipelines. Curious if anyone has hit this with artifact attentions or a trusted publishing workflow. Also, this hasn't come up for v2 yet since there is no real world usage.

My biggest concern is this ends up being a bigger effort because we produce bundles that aren't compatible across clients. I think it's most important to resolve in cosign,

I can fix this just for V1, since we know these errors are a server side flake occasionally. I can just leave a comment about we could go about resolving this for v2 if it becomes an issue.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIRC this has come up when the npm registry submits the publish attestation, due to the async nature of how they are processed/ack'ed it became an issue. But this is only due to internal failure, the processing pipeline could also be updated to "retry from the start" on failure.

As Rekor V2 will be a breaking change, I'm leaning into injecting the trust root for client side reconstruction, or a resign (with a small risk of stale entries).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just coming back to this - given the amount of code needed to rebuild the TLE, I'd lean towards just getting this change in as-is for Rekor v1 only if that's alright, since that flake can happen due to no fault of the signer.

@Hayden-IO
Hayden-IO force-pushed the retry-rekor branch 2 times, most recently from e85bb06 to c443e1e Compare June 17, 2026 18:35
@Hayden-IO
Hayden-IO marked this pull request as ready for review June 17, 2026 18:42
@Hayden-IO
Hayden-IO requested a review from a team as a code owner June 17, 2026 18:42
When a CI pipeline fails midway, a client may try to upload the same
signed entry again. This will result in Rekor returning a 409.
Additionally, occasionally Rekor v1 fails with a 409 - the root cause
isn't known, it's a flake in Trillian.

This change gracefully handles a 409 Conflict by retrieving the
transparency log entry. For Rekor v1, this is simple, just use the UUID
in the response to lookup the entry by UUID.

Rekor v2 is not handled because we aren't able to fully construct the
TLE without the log's public key, which is the TLE's LogID. A comment
has been added with details on how someone could implement it in the
future.

Signed-off-by: Hayden <8418760+Hayden-IO@users.noreply.github.com>
@Hayden-IO
Hayden-IO merged commit 96c07cb into sigstore:main Jul 28, 2026
12 of 13 checks passed
StanGirard added a commit to The-Vibe-Company/quivr that referenced this pull request Oct 13, 2026
Cosign 3.1.3 aborts when Rekor v1 reports that the signed entry already
exists, before uploading the signature to the registry. Build the same
pinned Cosign source with released `sigstore-go` 1.3.0 ([upstream
fix](sigstore/sigstore-go#647)), which retrieves
the existing log entry and completes the bundle. Both image workflows
verify source checksums and the built dependency versions, then retain
mandatory signature and SBOM attestation verification.

The Grype database producer and restore actions now share a run-ID-only
cache key. Failed-only reruns reuse the successful producer's database;
cache misses, stale databases and critical vulnerabilities with an
available fix still fail.

No official fixed Cosign release exists as of 2026-10-13, so this uses
the approved isolated source-build fallback. It adds compilation time
(104 seconds for the first local build, 16 seconds cached); the overall
image-job budget accommodates this while signing deadlines stay
unchanged. The engine's dependency module is unchanged.

Validation:
- Real signing-dependency conflict owner fails with 1.2.2 and passes
with 1.3.0; upload/fetch errors and malformed evidence remain failures.
- Cache owner fails on the original keys for attempts 2/3 and passes
with stable keys and separate-run isolation.
- Shell owner covers required signing/verification operations, signer
constraints and timeout exit propagation.
- Installer rejects a tampered checksum before building; installed
binary contains the pinned versions.
- Repository guards and Python check group pass (314 script tests).

Fixes THE-1524.

Independent review: `review-code-dev` v2 / Alibaba delegation passed,
all ten files inspected, zero findings.

Latest-head CI: **35 passed, 2 skipped** in the PR check display,
including all thirteen real OCI image sign/attest/verify and
vulnerability checks. [Verification
run](https://github.com/The-Vibe-Company/quivr/actions/runs/38345125999).
Engine-job logs confirm pinned artifact versions and a 150-second
installer. Head: `a168639c913ac75c2108d81718d30455192869ca`. GitHub
reports clean and mergeable.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants