diff --git a/sigstore/models.py b/sigstore/models.py index 2237b772e..19d12e8d3 100644 --- a/sigstore/models.py +++ b/sigstore/models.py @@ -63,7 +63,7 @@ ) from sigstore._internal.tuf import DEFAULT_TUF_URL, STAGING_TUF_URL, TrustUpdater from sigstore._utils import KeyID, cert_is_leaf, cert_is_root_ca, is_timerange_valid -from sigstore.errors import Error, MetadataError, TUFError, VerificationError +from sigstore.errors import Error, MetadataError, VerificationError # Versions supported by this client REKOR_VERSIONS = [1, 2] @@ -965,11 +965,8 @@ def from_tuf( tr_path = updater.get_trusted_root_path() inner_tr = trustroot_v1.TrustedRoot.from_json(Path(tr_path).read_bytes()) - try: - sc_path = updater.get_signing_config_path() - inner_sc = trustroot_v1.SigningConfig.from_json(Path(sc_path).read_bytes()) - except TUFError as e: - raise e + sc_path = updater.get_signing_config_path() + inner_sc = trustroot_v1.SigningConfig.from_json(Path(sc_path).read_bytes()) return cls( trustroot_v1.ClientTrustConfig( diff --git a/sigstore/sign.py b/sigstore/sign.py index 8a517e7b7..c0b0248c3 100644 --- a/sigstore/sign.py +++ b/sigstore/sign.py @@ -141,14 +141,21 @@ def _signing_cert( else: _logger.debug("Retrieving signed certificate...") - # Build an X.509 Certificate Signing Request + # Build an X.509 Certificate Signing Request. + # Fulcio ignores the CSR subject entirely; it derives the + # certificate's identity from the OIDC token, not from this + # field. We use a fixed stub value here instead of the actual + # identity claim so that non-ASCII characters (e.g. emojis in + # GitHub Actions environment names) don't produce an invalid + # IA5String-encoded email attribute and cause a Fulcio 400. + # See: https://github.com/sigstore/fulcio/issues/863 builder = ( x509.CertificateSigningRequestBuilder() .subject_name( x509.Name( [ x509.NameAttribute( - NameOID.EMAIL_ADDRESS, self._identity_token._identity + NameOID.EMAIL_ADDRESS, "user@example.com" ), ] ) diff --git a/test/unit/test_sign.py b/test/unit/test_sign.py index 0b039b119..6a52c15d5 100644 --- a/test/unit/test_sign.py +++ b/test/unit/test_sign.py @@ -11,24 +11,88 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +import datetime import hashlib import logging import secrets import pretend import pytest +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.x509.oid import NameOID from sigstore_models.common.v1 import HashAlgorithm import sigstore.oidc +import sigstore.sign +from sigstore._internal.fulcio.client import FulcioCertificateSigningResponse from sigstore._internal.timestamp import TimestampAuthorityClient from sigstore.dsse import StatementBuilder, Subject from sigstore.errors import VerificationError from sigstore.hashes import Hashed from sigstore.models import ClientTrustConfig -from sigstore.sign import SigningContext +from sigstore.sign import Signer, SigningContext from sigstore.verify.policy import UnsafeNoOp +def test_csr_subject_is_stub_for_non_ascii_identity(dummy_jwt, monkeypatch): + # Regression test for https://github.com/sigstore/sigstore-python/issues/1507. + # The CSR subject must be a fixed ASCII stub regardless of the identity + # claim value, because Fulcio ignores the CSR subject entirely and + # non-ASCII values (e.g. emojis in GHA environment names) produce an + # invalid IA5String encoding that Fulcio rejects with a 400. + now = int(datetime.datetime.now().timestamp()) + token_str = dummy_jwt( + { + "aud": "sigstore", + "sub": "repo:org/repo:environment:deploy\U0001f680", + "iat": now, + "nbf": now, + "exp": now + 600, + "iss": "hxxps://unknown.issuer.example.com/auth", + } + ) + identity_token = sigstore.oidc.IdentityToken(token_str) + # The identity contains a non-ASCII emoji character. + assert "\U0001f680" in identity_token.identity + + captured_csrs: list = [] + + def fake_post(csr, token): + captured_csrs.append(csr) + # Return a minimal fake response; _signing_cert will try to call + # verify_sct on it, which we also patch out below. + fake_cert = pretend.stub( + not_valid_after_utc=datetime.datetime( + 9999, 1, 1, tzinfo=datetime.timezone.utc + ) + ) + return FulcioCertificateSigningResponse(cert=fake_cert, chain=[]) + + monkeypatch.setattr(sigstore.sign, "verify_sct", lambda *a, **kw: None) + + fake_signing_cert_endpoint = pretend.stub(post=fake_post) + fake_fulcio = pretend.stub(signing_cert=fake_signing_cert_endpoint) + fake_trusted_root = pretend.stub( + ct_keyring=lambda *a: pretend.stub(verify=lambda *a: None) + ) + fake_rekor = pretend.stub() + ctx = SigningContext( + fulcio=fake_fulcio, + rekor=fake_rekor, + trusted_root=fake_trusted_root, + ) + + signer = Signer(identity_token, ctx, cache=False) + signer._Signer__cached_private_key = ec.generate_private_key(ec.SECP256R1()) + signer._signing_cert() + + assert len(captured_csrs) == 1 + csr = captured_csrs[0] + email_attrs = csr.subject.get_attributes_for_oid(NameOID.EMAIL_ADDRESS) + assert len(email_attrs) == 1 + assert email_attrs[0].value == "user@example.com" + + # 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