From d91ffe78ae2360878d2216a51b63e03500d5fdf6 Mon Sep 17 00:00:00 2001 From: Facundo Tuesca Date: Fri, 24 Jul 2026 16:24:26 +0200 Subject: [PATCH 1/2] Return cached cert first when signing Signed-off-by: Facundo Tuesca --- sigstore/sign.py | 37 ++++++++++++++++++------------------- test/unit/test_sign.py | 13 +++++++++++++ 2 files changed, 31 insertions(+), 19 deletions(-) diff --git a/sigstore/sign.py b/sigstore/sign.py index 2c0fe17c7..2036e4807 100644 --- a/sigstore/sign.py +++ b/sigstore/sign.py @@ -151,36 +151,35 @@ def _signing_cert( the returned certificate is present in Fulcio's CT log. """ - # Our CSR cannot possibly succeed if our underlying identity token - # is expired. - if not self._identity_token.in_validity_period(): - raise ExpiredIdentity - - # If it exists, verify if the current certificate is expired + # If a cached certificate exists, use it until it expires. if self.__cached_signing_certificate: not_valid_after = self.__cached_signing_certificate.not_valid_after_utc if datetime.now(timezone.utc) > not_valid_after: raise ExpiredCertificate return self.__cached_signing_certificate - else: - _logger.debug("Retrieving signed certificate...") + # Our CSR cannot possibly succeed if our underlying identity token + # is expired. + if not self._identity_token.in_validity_period(): + raise ExpiredIdentity + + _logger.debug("Retrieving signed certificate...") - certificate_request = self._build_csr() + certificate_request = self._build_csr() - certificate_response = self._signing_ctx._fulcio.signing_cert.post( - certificate_request, self._identity_token - ) + certificate_response = self._signing_ctx._fulcio.signing_cert.post( + certificate_request, self._identity_token + ) - verify_sct( - certificate_response.cert, - certificate_response.chain, - self._signing_ctx._trusted_root.ct_keyring(KeyringPurpose.SIGN), - ) + verify_sct( + certificate_response.cert, + certificate_response.chain, + self._signing_ctx._trusted_root.ct_keyring(KeyringPurpose.SIGN), + ) - _logger.debug("Successfully verified SCT...") + _logger.debug("Successfully verified SCT...") - return certificate_response.cert + return certificate_response.cert def _finalize_sign( self, diff --git a/test/unit/test_sign.py b/test/unit/test_sign.py index d488d756f..2f6ade772 100644 --- a/test/unit/test_sign.py +++ b/test/unit/test_sign.py @@ -13,6 +13,7 @@ # limitations under the License. import hashlib import secrets +from datetime import datetime, timedelta, timezone import cryptography.x509 as x509 import pretend @@ -72,6 +73,18 @@ def test_build_csr_non_ascii_identity_produces_valid_csr(): assert len(reparsed.subject) == 0 +def test_signer_uses_valid_cached_certificate_with_expired_identity(): + cert = pretend.stub( + not_valid_after_utc=datetime.now(timezone.utc) + timedelta(minutes=5) + ) + + signer = Signer.__new__(Signer) + signer._identity_token = pretend.stub(in_validity_period=lambda: False) + signer._Signer__cached_signing_certificate = cert + + assert signer._signing_cert() is cert + + # only check the log contents for production: staging is already on # rekor v2 and we don't currently support log lookups on rekor v2. # This test can likely be removed once prod also uses rekor v2 From 7425bb12aebe04a65e01d90280939d132af9b9ef Mon Sep 17 00:00:00 2001 From: Facundo Tuesca Date: Sat, 25 Jul 2026 21:33:56 +0200 Subject: [PATCH 2/2] Test actual signing flow instead of stubbing Signed-off-by: Facundo Tuesca --- test/unit/test_sign.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/test/unit/test_sign.py b/test/unit/test_sign.py index 2f6ade772..49e389bbf 100644 --- a/test/unit/test_sign.py +++ b/test/unit/test_sign.py @@ -13,7 +13,6 @@ # limitations under the License. import hashlib import secrets -from datetime import datetime, timedelta, timezone import cryptography.x509 as x509 import pretend @@ -73,16 +72,20 @@ def test_build_csr_non_ascii_identity_produces_valid_csr(): assert len(reparsed.subject) == 0 -def test_signer_uses_valid_cached_certificate_with_expired_identity(): - cert = pretend.stub( - not_valid_after_utc=datetime.now(timezone.utc) + timedelta(minutes=5) - ) - - signer = Signer.__new__(Signer) - signer._identity_token = pretend.stub(in_validity_period=lambda: False) - signer._Signer__cached_signing_certificate = cert +@pytest.mark.staging +@pytest.mark.ambient_oidc +def test_signer_uses_valid_cached_certificate_with_expired_identity(staging): + ctx_cls, _, identity = staging + ctx: SigningContext = ctx_cls() + assert identity is not None - assert signer._signing_cert() is cert + # Constructing a cached signer obtains its certificate while the token is + # valid. Expire the local token afterward, then perform a real signing + # operation: it must reuse the still-valid cached certificate. + with ctx.signer(identity) as signer: + identity._exp = 0 + assert not signer._identity_token.in_validity_period() + signer.sign_artifact(secrets.token_bytes(32)) # only check the log contents for production: staging is already on