fix(verification): use timestamp time for TSA cert chain validation instead of current time#2893
Conversation
…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).
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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>
|
@jiparis is this correct? |
Automated fix for bug 16857
Severity:
highSummary
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.Storeendpoint returns a 400 error, blocking CI/CD attestation workflows. The affected org ischainloop-test(service accountc5693f09), but this affects all orgs using the same TSA.Root Cause
TSA Certificate Expiry Breaks Timestamp Verification
The
VerifyTimestampsfunction inpkg/attestation/verifier/timestamp.gocallsverification.VerifyTimestampResponse()from thesigstore/timestamp-authority/v2library (line 71-76):The upstream
VerifyTimestampResponsevalidates the TSA certificate chain using Go'sx509.VerifyOptionswithout settingCurrentTime, which defaults totime.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:
After March 11, 2026, every call to
VerifyTimestampResponsefails 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) →verifyBundleinworkflowrun.go(line 475-486) →SaveAttestation(line 326-337), where it becomes aNewErrValidation(400 error) that blocks the attestation from being stored.Triggering Cause
The FreeTSA leaf certificate expired on March 11, 2026. The Sentry error's
firstEventis March 17, 2026 — the first attestation store attempt after expiry by thechainloop-testorg'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:
CurrentTimeto the verification options set to the timestamp's time (requires library support or custom chain validation)CurrentTimeset to the parsed timestamp timeIntroduced by: jiparis on 2025-02-20 in commit
3b67e0eSuggested Fix
What changed
Replaced
verification.VerifyTimestampResponsefromsigstore/timestamp-authority/v2with a newverifyTimestampAtTimefunction that validates the TSA certificate chain at the timestamp's time rather than the current system time.Why
The upstream
verification.VerifyTimestampResponsedelegates certificate chain validation to Go'sx509.VerifyOptionswithout settingCurrentTime, which defaults totime.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
verifyTimestampAtTimefunction:timestamp.ParseResponse(fromdigitorus/timestamp, already a direct dependency), which also verifies the PKCS7 signaturex509.Certificate.VerifywithCurrentTimeset to the parsed timestamp's timeThis removes the dependency on
sigstore/timestamp-authority/v2/pkg/verificationfrom 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.gocovers the key scenarios including the exact bug case (cert expired at current time but valid at timestamp time).Generated by Sonarly