diff --git a/sigstore/_internal/oidc/ambient.py b/sigstore/_internal/oidc/ambient.py index fdd47485c..96c0a04f1 100644 --- a/sigstore/_internal/oidc/ambient.py +++ b/sigstore/_internal/oidc/ambient.py @@ -45,7 +45,7 @@ def detect_credential() -> Optional[str]: Raises `AmbientCredentialError` if any detector fails internally (i.e. detects a credential, but cannot retrieve it). """ - detectors: List[Callable[..., Optional[str]]] = [detect_github] + detectors: List[Callable[..., Optional[str]]] = [detect_github, detect_circleci] for detector in detectors: credential = detector() if credential is not None: @@ -76,7 +76,7 @@ def detect_github() -> Optional[str]: req_url = os.getenv("ACTIONS_ID_TOKEN_REQUEST_URL") if not req_token or not req_url: raise AmbientCredentialError( - "GitHub: missing or insufficient OIDC token permissions?" + "GitHub: missing or insufficient OIDC token permissions" ) resp = requests.get( @@ -96,3 +96,19 @@ def detect_github() -> Optional[str]: return _GitHubTokenPayload(**body).value except Exception as e: raise AmbientCredentialError("GitHub: malformed or incomplete JSON") from e + + +def detect_circleci() -> Optional[str]: + logger.debug("CircleCI: looking for OIDC credentials") + + if not os.getenv("CIRCLECI"): + logger.debug("CircleCI: environment doesn't look right; giving up") + return None + + token = os.getenv("CIRCLE_OIDC_TOKEN") + if not token: + raise AmbientCredentialError( + "CircleCI: missing or insufficient OIDC token permissions" + ) + + return token diff --git a/test/internal/oidc/test_ambient.py b/test/internal/oidc/test_ambient.py index 3c551e4c8..a8bbe0335 100644 --- a/test/internal/oidc/test_ambient.py +++ b/test/internal/oidc/test_ambient.py @@ -20,8 +20,9 @@ def test_detect_credential_none(monkeypatch): - detect_github = pretend.call_recorder(lambda: None) - monkeypatch.setattr(ambient, "detect_github", detect_github) + detect_noop = pretend.call_recorder(lambda: None) + for detector in ["detect_github", "detect_circleci"]: + monkeypatch.setattr(ambient, detector, detect_noop) assert ambient.detect_credential() is None @@ -32,7 +33,7 @@ def test_detect_credential(monkeypatch): assert ambient.detect_credential() == "fakejwt" -def test_detect_github_bad_env(monkeypatch): +def test_detect_github_wrong_env(monkeypatch): # We might actually be running in a CI, so explicitly remove this. monkeypatch.delenv("GITHUB_ACTIONS", raising=False) @@ -56,7 +57,7 @@ def test_detect_github_bad_permissions(monkeypatch): with pytest.raises( ambient.AmbientCredentialError, - match="GitHub: missing or insufficient OIDC token permissions?", + match="GitHub: missing or insufficient OIDC token permissions", ): ambient.detect_github() assert logger.debug.calls == [ @@ -136,3 +137,21 @@ def test_detect_github(monkeypatch): ) ] assert resp.json.calls == [pretend.call()] + + +def test_detect_circleci_wrong_env(monkeypatch): + # We might actually be running in a CI, so explicitly remove this. + monkeypatch.delenv("CIRCLECI", raising=False) + + assert ambient.detect_circleci() is None + + +def test_detect_circleci_bad_permissions(monkeypatch): + monkeypatch.setenv("CIRCLECI", "true") + monkeypatch.delenv("CIRCLE_OIDC_TOKEN", raising=False) + + with pytest.raises( + ambient.AmbientCredentialError, + match="CircleCI: missing or insufficient OIDC token permissions", + ): + ambient.detect_circleci()