Skip to content

fix(verification): use timestamp time for TSA cert chain validation instead of current time#2893

Open
sonarly[bot] wants to merge 1 commit intomainfrom
sonarly-16857-tsa-certificate-expiry-causes-all-attestation
Open

fix(verification): use timestamp time for TSA cert chain validation instead of current time#2893
sonarly[bot] wants to merge 1 commit intomainfrom
sonarly-16857-tsa-certificate-expiry-causes-all-attestation

Conversation

@sonarly
Copy link

@sonarly sonarly bot commented Mar 20, 2026

Automated fix for bug 16857

Severity: high

Summary

Attestation timestamp verification fails for all attestations using the FreeTSA timestamp authority because the TSA leaf certificate expired on March 11, 2026, and the verification code validates the cert chain at current system time instead of at the timestamp's issue time.

User Impact

Any organization using FreeTSA (or any TSA with an expired leaf cert) as their timestamp authority cannot store new attestations. The AttestationService.Store endpoint returns a 400 error, blocking CI/CD attestation workflows. The affected org is chainloop-test (service account c5693f09), but this affects all orgs using the same TSA.

Root Cause

TSA Certificate Expiry Breaks Timestamp Verification

The VerifyTimestamps function in pkg/attestation/verifier/timestamp.go calls verification.VerifyTimestampResponse() from the sigstore/timestamp-authority/v2 library (line 71-76):

ts, err := verification.VerifyTimestampResponse(st, bytes.NewReader(sigBytes),
    verification.VerifyOpts{
        TSACertificate: tsaCert,
        Intermediates:  intermediates,
        Roots:          roots,
    })
if err != nil {
    continue
}

The upstream VerifyTimestampResponse validates the TSA certificate chain using Go's x509.VerifyOptions without setting CurrentTime, which defaults to time.Now(). This means the certificate chain must be valid at the current system time — not at the time the timestamp was issued.

The FreeTSA leaf certificate used in production expired on March 11, 2026:

notBefore=Mar 13 01:57:39 2016 GMT
notAfter=Mar 11 01:57:39 2026 GMT
subject=O = Free TSA, OU = TSA, CN = www.freetsa.org

After March 11, 2026, every call to VerifyTimestampResponse fails the x509 chain validation because Go sees the leaf cert as expired. Since ALL TSA verifications fail, len(verifiedTimestamps) < len(signedTimestamps) is true at line 96, producing the error "some timestamps verification failed".

This propagates up through VerifyBundle (line 93-95) → verifyBundle in workflowrun.go (line 475-486) → SaveAttestation (line 326-337), where it becomes a NewErrValidation (400 error) that blocks the attestation from being stored.

Triggering Cause

The FreeTSA leaf certificate expired on March 11, 2026. The Sentry error's firstEvent is March 17, 2026 — the first attestation store attempt after expiry by the chainloop-test org's service account running a scheduled GitHub Actions workflow.

The Design Flaw

For timestamp verification, the correct behavior is to check that the TSA certificate was valid at the time the timestamp was issued (which the code already does at line 81), not at the current time. The purpose of a TSA timestamp is to prove a signature existed at a specific point in time — the TSA cert only needs to have been valid then. The upstream library's use of current time for chain validation is semantically incorrect for this use case, and the Chainloop code doesn't compensate for it.

The fix needs to either:

  1. Pass a CurrentTime to the verification options set to the timestamp's time (requires library support or custom chain validation)
  2. Perform the chain validation separately with CurrentTime set to the parsed timestamp time
  3. The immediate operational fix is to update the TSA certificate chain in the Helm deployment configuration

Introduced by: jiparis on 2025-02-20 in commit 3b67e0e

feat(signing): implement timestamp authorities for signature and verification (#1843)

Suggested Fix

What changed

Replaced verification.VerifyTimestampResponse from sigstore/timestamp-authority/v2 with a new verifyTimestampAtTime function that validates the TSA certificate chain at the timestamp's time rather than the current system time.

Why

The upstream verification.VerifyTimestampResponse delegates certificate chain validation to Go's x509.VerifyOptions without setting CurrentTime, which defaults to time.Now(). When a TSA certificate expires (as FreeTSA's leaf cert did on March 11, 2026), all timestamp verification fails — even for timestamps that were validly issued while the cert was still active.

For timestamp verification, the semantically correct behavior is to validate the TSA certificate chain at the time the timestamp was issued, not at the current time. A timestamp proves a signature existed at a specific point in time; the TSA cert only needs to have been valid at that moment.

How

The new verifyTimestampAtTime function:

  1. Parses the timestamp response using timestamp.ParseResponse (from digitorus/timestamp, already a direct dependency), which also verifies the PKCS7 signature
  2. Verifies the hashed message matches the provided signature bytes using the hash algorithm specified in the timestamp
  3. Validates the TSA certificate chain using x509.Certificate.Verify with CurrentTime set to the parsed timestamp's time

This removes the dependency on sigstore/timestamp-authority/v2/pkg/verification from this file.

Additional note

While this code fix handles verification of timestamps signed by expired TSA certs, the production deployment should also update the TSA configuration if the FreeTSA service itself is no longer issuing valid timestamps with an expired cert. New attestations would need a TSA with a valid certificate to obtain timestamps.

A new test file timestamp_test.go covers the key scenarios including the exact bug case (cert expired at current time but valid at timestamp time).


Generated by Sonarly

…nstead of current time

https://sonarly.com/issue/16857?type=bug

Attestation timestamp verification fails for all attestations using the FreeTSA timestamp authority because the TSA leaf certificate expired on March 11, 2026, and the verification code validates the cert chain at current system time instead of at the timestamp's issue time.

Fix: ## What changed

Replaced `verification.VerifyTimestampResponse` from `sigstore/timestamp-authority/v2` with a new `verifyTimestampAtTime` function that validates the TSA certificate chain at the **timestamp's time** rather than the current system time.

### Why

The upstream `verification.VerifyTimestampResponse` delegates certificate chain validation to Go's `x509.VerifyOptions` without setting `CurrentTime`, which defaults to `time.Now()`. When a TSA certificate expires (as FreeTSA's leaf cert did on March 11, 2026), all timestamp verification fails — even for timestamps that were validly issued while the cert was still active.

For timestamp verification, the semantically correct behavior is to validate the TSA certificate chain at the time the timestamp was issued, not at the current time. A timestamp proves a signature existed at a specific point in time; the TSA cert only needs to have been valid at that moment.

### How

The new `verifyTimestampAtTime` function:
1. **Parses the timestamp response** using `timestamp.ParseResponse` (from `digitorus/timestamp`, already a direct dependency), which also verifies the PKCS7 signature
2. **Verifies the hashed message** matches the provided signature bytes using the hash algorithm specified in the timestamp
3. **Validates the TSA certificate chain** using `x509.Certificate.Verify` with `CurrentTime` set to the parsed timestamp's time

This removes the dependency on `sigstore/timestamp-authority/v2/pkg/verification` from this file.

### Additional note

While this code fix handles verification of timestamps signed by expired TSA certs, the production deployment should also update the TSA configuration if the FreeTSA service itself is no longer issuing valid timestamps with an expired cert. New attestations would need a TSA with a valid certificate to obtain timestamps.

A new test file `timestamp_test.go` covers the key scenarios including the exact bug case (cert expired at current time but valid at timestamp time).
Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 2 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="pkg/attestation/verifier/timestamp.go">

<violation number="1" location="pkg/attestation/verifier/timestamp.go:102">
P0: Security regression: the PKCS7 signer certificate is never verified against the trusted TSA certificate. `timestamp.ParseResponse` verifies the PKCS7 signature using the certificate *embedded in the response itself*, while `tsaCert.Verify` independently validates the expected TSA cert against roots. These two checks are disconnected — nothing confirms the timestamp was actually signed by `tsaCert`. The old `VerifyTimestampResponse` verified this binding via `pkcs7.VerifyWithChain` (signature against trusted chain) and `verifyEmbeddedLeafCert` (embedded cert matches expected cert).

To fix this, after parsing the timestamp, verify that the signing certificate in the response matches `tsaCert` (e.g., check `ts.Certificates` contains a cert equal to `tsaCert`, or re-parse `ts.RawToken` and use `pkcs7.VerifyWithChain` with the root pool and `CurrentTime` set).</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

// specific point in time — the TSA certificate only needs to have been valid then.
func verifyTimestampAtTime(tsrBytes, signature []byte, tsaCert *x509.Certificate, intermediates, roots []*x509.Certificate) (*timestamp.Timestamp, error) {
// Parse and verify the PKCS7 signature in the timestamp response
ts, err := timestamp.ParseResponse(tsrBytes)
Copy link

@cubic-dev-ai cubic-dev-ai bot Mar 20, 2026

Choose a reason for hiding this comment

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

P0: Security regression: the PKCS7 signer certificate is never verified against the trusted TSA certificate. timestamp.ParseResponse verifies the PKCS7 signature using the certificate embedded in the response itself, while tsaCert.Verify independently validates the expected TSA cert against roots. These two checks are disconnected — nothing confirms the timestamp was actually signed by tsaCert. The old VerifyTimestampResponse verified this binding via pkcs7.VerifyWithChain (signature against trusted chain) and verifyEmbeddedLeafCert (embedded cert matches expected cert).

To fix this, after parsing the timestamp, verify that the signing certificate in the response matches tsaCert (e.g., check ts.Certificates contains a cert equal to tsaCert, or re-parse ts.RawToken and use pkcs7.VerifyWithChain with the root pool and CurrentTime set).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At pkg/attestation/verifier/timestamp.go, line 102:

<comment>Security regression: the PKCS7 signer certificate is never verified against the trusted TSA certificate. `timestamp.ParseResponse` verifies the PKCS7 signature using the certificate *embedded in the response itself*, while `tsaCert.Verify` independently validates the expected TSA cert against roots. These two checks are disconnected — nothing confirms the timestamp was actually signed by `tsaCert`. The old `VerifyTimestampResponse` verified this binding via `pkcs7.VerifyWithChain` (signature against trusted chain) and `verifyEmbeddedLeafCert` (embedded cert matches expected cert).

To fix this, after parsing the timestamp, verify that the signing certificate in the response matches `tsaCert` (e.g., check `ts.Certificates` contains a cert equal to `tsaCert`, or re-parse `ts.RawToken` and use `pkcs7.VerifyWithChain` with the root pool and `CurrentTime` set).</comment>

<file context>
@@ -98,3 +92,46 @@ func VerifyTimestamps(sb *bundle.Bundle, tr *TrustedRoot) error {
+// specific point in time — the TSA certificate only needs to have been valid then.
+func verifyTimestampAtTime(tsrBytes, signature []byte, tsaCert *x509.Certificate, intermediates, roots []*x509.Certificate) (*timestamp.Timestamp, error) {
+	// Parse and verify the PKCS7 signature in the timestamp response
+	ts, err := timestamp.ParseResponse(tsrBytes)
+	if err != nil {
+		return nil, fmt.Errorf("parsing timestamp response: %w", err)
</file context>
Fix with Cubic

@migmartri
Copy link
Member

@jiparis is this correct?

@migmartri migmartri requested a review from jiparis March 20, 2026 12:38
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.

1 participant