Skip to content

KeylessVerifier cannot verify bundles that use an RFC 3161 timestamp and no transparency-log entry #1251

Description

@anthonydahanne

I used help from AI to create this issue, but I humanly read it and reviewed/corrected it before submitting it

Why did I create this issue

The concrete case that surfaced this: GitHub artifact attestations produced for a private repository. Those are signed by GitHub's own (non–public-good) Sigstore instance, which does not write to a public transparency log — the bundle carries a TSA timestamp instead. gh attestation verify and cosign (with --use-signed-timestamps --insecure-ignore-tlog) both verify these bundles; sigstore-java cannot.

Summary

KeylessVerifier.verify(...) unconditionally requires at least one Rekor transparency-log entry in the bundle. Sigstore bundles whose verification material contains only an RFC 3161 signed timestamp timestampVerificationData) and no tlogEntries are valid per the bundle spec, but verification fails before the timestamp or signature is ever checked.

Related issues (this is not a duplicate)

Steps to reproduce

  1. Obtain a bundle with timestampVerificationData and no tlogEntries — e.g. an actions/attest-build-provenance attestation from a private GitHub repo (gh attestation download …), plus the matching trust root (gh attestation trusted-root).
  2. sigstore-cli verify <artifact> --bundle <bundle> --trusted-root <root> --certificate-identity <san> --certificate-oidc-issuer https://token.actions.githubusercontent.com

Actual behavior

Verification throws while fetching the (absent) Rekor entry — bundle.getEntries().get(0) on an empty list — so it never reaches signature/timestamp verification.

Root cause

In sigstore-java/src/main/java/dev/sigstore/KeylessVerifier.java, the Rekor entry is treated as mandatory:

// findHashAlgorithm(...)
var rekorEntry = bundle.getEntries().get(0);
...
// verify(...)
var rekorEntry = bundle.getEntries().get(0);
...
rekorVerifier.verifyEntry(rekorEntry);   // "Transparency log entry could not be verified"

verifyTimestamps(...) already knows how to verify RFC 3161 timestamps and even guards if (timestamps.isEmpty() && entryTime == null), but it is only reached after the
mandatory Rekor entry has been obtained and verified — so a TSA-only bundle can never get there.

Expected behavior

When a bundle has no tlogEntries but does carry a valid timestampVerificationData (RFC 3161 timestamp from a TSA in the trusted root), KeylessVerifier should verify the Fulcio chain + DSSE/message signature and establish signing time from the TSA timestamp — i.e. treat the transparency-log entry as optional when signed timestamps are present, rather than requiring one. This matches the Sigstore bundle spec, whose verification material permits tlog entries and/or signed timestamps.

Suggested fix

  • Make findHashAlgorithm / verify tolerate an empty bundle.getEntries():
  • derive the hash algorithm without a Rekor entry when none is present (e.g. from the DSSE payload / message-signature algorithm), and
  • skip rekorVerifier.verifyEntry(...) when there is no entry, relying on verifyTimestamps(...) (TSA) for the trusted signing time.
  • Keep requiring at least one source of trusted time (tlog SET or RFC 3161 timestamp), which verifyTimestamps already enforces.

References / prior art

  • Sigstore bundle spec: verification material may be a transparency-log entry, a signed timestamp, or both.
  • cosign verify-blob-attestation verifies the same bundles with --use-signed-timestamps --insecure-ignore-tlog (+ --trusted-root); gh attestation verify verifies them natively.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions