Skip to content

Commit 58a0c27

Browse files
authored
cli: move oidc token into pytest (sigstore#91)
* cli: move oidc token into pytest Signed-off-by: Jack Leightcap <jack.leightcap@trailofbits.com> * cli: github token threading Signed-off-by: Jack Leightcap <jack.leightcap@trailofbits.com> --------- Signed-off-by: Jack Leightcap <jack.leightcap@trailofbits.com>
1 parent ac0a71a commit 58a0c27

File tree

4 files changed

+115
-111
lines changed

4 files changed

+115
-111
lines changed

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ ALL_PY_SRCS := action.py \
77
all:
88
@echo "Run my targets individually!"
99

10-
env/pyvenv.cfg: dev-requirements.txt
10+
env/pyvenv.cfg: dev-requirements.txt requirements.txt
1111
python3 -m venv env
1212
./env/bin/python -m pip install --upgrade pip
13-
./env/bin/python -m pip install --requirement dev-requirements.txt
13+
./env/bin/python -m pip install --requirement dev-requirements.txt --requirement requirements.txt
1414

1515
.PHONY: dev
1616
dev: env/pyvenv.cfg

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,16 @@ In the example above, the workflow is installing [sigstore-python](https://githu
5757
and providing `sigstore` as the `entrypoint` since this is the command used to
5858
invoke the client.
5959

60+
## Development
61+
62+
Running the conformance suite locally,
63+
64+
```sh
65+
(env) $ pytest --entrypoint=SIGSTORE_CLIENT --identity-token=$(gh auth token)
66+
```
67+
68+
Using the [`gh` CLI](https://cli.github.com/) and noting SIGSTORE_CLIENT is the absolute path to a client implementing the [CLI specification](https://github.com/sigstore/sigstore-conformance/blob/main/docs/cli_protocol.md).
69+
6070
## Licensing
6171

6272
`sigstore-conformance` is licensed under the Apache 2.0 License.

action.py

Lines changed: 3 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -6,114 +6,16 @@
66

77
import os
88
import sys
9-
import time
10-
from datetime import datetime, timedelta
11-
from io import BytesIO
129
from pathlib import Path
13-
from typing import Optional
14-
from zipfile import ZipFile
1510

1611
import pytest
17-
import requests
1812

1913
_SUMMARY = Path(os.getenv("GITHUB_STEP_SUMMARY")).open("a") # type: ignore
2014
_RENDER_SUMMARY = os.getenv("GHA_SIGSTORE_CONFORMANCE_SUMMARY", "true") == "true"
2115
_DEBUG = (
2216
os.getenv("GHA_SIGSTORE_CONFORMANCE_INTERNAL_BE_CAREFUL_DEBUG", "false") != "false"
2317
)
2418
_ACTION_PATH = Path(os.getenv("GITHUB_ACTION_PATH")) # type: ignore
25-
_OIDC_BEACON_API_URL = (
26-
"https://api.github.com/repos/sigstore-conformance/extremely-dangerous-public-oidc-beacon/"
27-
"actions"
28-
)
29-
_OIDC_BEACON_WORKFLOW_ID = 55399612
30-
31-
32-
class OidcTokenError(Exception):
33-
pass
34-
35-
36-
def _get_oidc_token() -> str:
37-
gh_token = os.getenv("GHA_SIGSTORE_GITHUB_TOKEN")
38-
if gh_token is None:
39-
raise OidcTokenError(
40-
"`GHA_SIGSTORE_GITHUB_TOKEN` environment variable not found"
41-
)
42-
43-
session = requests.Session()
44-
headers = {
45-
"Accept": "application/vnd.github+json",
46-
"X-GitHub-Api-Version": "2022-11-28",
47-
"Authorization": f"Bearer {gh_token}",
48-
}
49-
50-
workflow_time: Optional[datetime] = None
51-
run_id: str
52-
53-
# We need a token that was generated in the last 5 minutes. Keep checking until we find one.
54-
while workflow_time is None or datetime.now() - workflow_time >= timedelta(
55-
minutes=5
56-
):
57-
# If there's a lot of traffic in the GitHub Actions cron queue, we might not have a valid
58-
# token to use. In that case, wait for 30 seconds and try again.
59-
if workflow_time is not None:
60-
_log("Couldn't find a recent token, waiting...")
61-
time.sleep(30)
62-
63-
resp: requests.Response = session.get(
64-
url=_OIDC_BEACON_API_URL + f"/workflows/{_OIDC_BEACON_WORKFLOW_ID}/runs",
65-
headers=headers,
66-
)
67-
resp.raise_for_status()
68-
69-
resp_json = resp.json()
70-
workflow_runs = resp_json["workflow_runs"]
71-
if not workflow_runs:
72-
raise OidcTokenError(f"Found no workflow runs: {resp_json}")
73-
74-
workflow_run = workflow_runs[0]
75-
76-
# If the job is still running, the token artifact won't have been generated yet.
77-
if workflow_run["status"] != "completed":
78-
continue
79-
80-
run_id = workflow_run["id"]
81-
workflow_time = datetime.strptime(
82-
workflow_run["run_started_at"], "%Y-%m-%dT%H:%M:%SZ"
83-
)
84-
85-
resp = session.get(
86-
url=_OIDC_BEACON_API_URL + f"/runs/{run_id}/artifacts",
87-
headers=headers,
88-
)
89-
resp.raise_for_status()
90-
91-
resp_json = resp.json()
92-
artifacts = resp_json["artifacts"]
93-
if len(artifacts) != 1:
94-
raise OidcTokenError(
95-
f"Found unexpected number of artifacts on OIDC beacon run: {artifacts}"
96-
)
97-
98-
oidc_artifact = artifacts[0]
99-
if oidc_artifact["name"] != "oidc-token":
100-
raise OidcTokenError(
101-
f"Found unexpected artifact on OIDC beacon run: {oidc_artifact['name']}"
102-
)
103-
artifact_id = oidc_artifact["id"]
104-
105-
# Download the OIDC token artifact and unzip the archive.
106-
resp = session.get(
107-
url=_OIDC_BEACON_API_URL + f"/artifacts/{artifact_id}/zip",
108-
headers=headers,
109-
)
110-
resp.raise_for_status()
111-
112-
with ZipFile(BytesIO(resp.content)) as artifact_zip:
113-
artifact_file = artifact_zip.open("oidc-token.txt")
114-
115-
# Strip newline.
116-
return artifact_file.read().decode().rstrip()
11719

11820

11921
def _summary(msg):
@@ -152,11 +54,9 @@ def _fatal_help(msg):
15254
if skip_signing:
15355
sigstore_conformance_args.extend(["--skip-signing"])
15456

155-
try:
156-
oidc_token = _get_oidc_token()
157-
except OidcTokenError as e:
158-
_fatal_help(f"Could not retrieve OIDC token: {str(e)}")
159-
sigstore_conformance_args.extend(["--identity-token", oidc_token])
57+
gh_token = os.getenv("GHA_SIGSTORE_GITHUB_TOKEN")
58+
if gh_token:
59+
sigstore_conformance_args.extend(["--github-token", gh_token])
16060

16161
_debug(f"running: sigstore-conformance {[str(a) for a in sigstore_conformance_args]}")
16262

test/conftest.py

Lines changed: 100 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
import os
22
import shutil
33
import tempfile
4+
import time
5+
from datetime import datetime, timedelta
6+
from io import BytesIO
47
from pathlib import Path
5-
from typing import Callable, Tuple, TypeVar
8+
from typing import Callable, Optional, Tuple, TypeVar
9+
from zipfile import ZipFile
610

711
import pytest # type: ignore
12+
import requests
813

914
from .client import (BundleMaterials, SignatureCertificateMaterials,
1015
SigstoreClient, VerificationMaterials)
@@ -13,10 +18,20 @@
1318
_MakeMaterialsByType = Callable[[str, _M], Tuple[Path, _M]]
1419
_MakeMaterials = Callable[[str], Tuple[Path, VerificationMaterials]]
1520

21+
_OIDC_BEACON_API_URL = (
22+
"https://api.github.com/repos/sigstore-conformance/extremely-dangerous-public-oidc-beacon/"
23+
"actions"
24+
)
25+
_OIDC_BEACON_WORKFLOW_ID = 55399612
26+
27+
28+
class OidcTokenError(Exception):
29+
pass
30+
1631

1732
def pytest_addoption(parser):
1833
"""
19-
Add the `--entrypoint`, `--identity-token`, and `--skip-signing` flags to
34+
Add the `--entrypoint`, `--github-token`, and `--skip-signing` flags to
2035
the `pytest` CLI.
2136
"""
2237
parser.addoption(
@@ -27,9 +42,9 @@ def pytest_addoption(parser):
2742
type=str,
2843
)
2944
parser.addoption(
30-
"--identity-token",
45+
"--github-token",
3146
action="store",
32-
help="the OIDC token to supply to the Sigstore client under test",
47+
help="the GitHub token to supply to the Sigstore client under test",
3348
required=True,
3449
type=str,
3550
)
@@ -54,12 +69,91 @@ def pytest_configure(config):
5469

5570

5671
@pytest.fixture
57-
def client(pytestconfig):
72+
def identity_token(pytestconfig):
73+
gh_token = pytestconfig.getoption("--github-token")
74+
session = requests.Session()
75+
headers = {
76+
"Accept": "application/vnd.github+json",
77+
"X-GitHub-Api-Version": "2022-11-28",
78+
"Authorization": f"Bearer {gh_token}",
79+
}
80+
81+
workflow_time: Optional[datetime] = None
82+
run_id: str
83+
84+
# We need a token that was generated in the last 5 minutes. Keep checking until we find one.
85+
while workflow_time is None or datetime.now() - workflow_time >= timedelta(
86+
minutes=5
87+
):
88+
# If there's a lot of traffic in the GitHub Actions cron queue, we might not have a valid
89+
# token to use. In that case, wait for 30 seconds and try again.
90+
if workflow_time is not None:
91+
# FIXME(jl): logging in pytest?
92+
# _log("Couldn't find a recent token, waiting...")
93+
time.sleep(30)
94+
95+
resp: requests.Response = session.get(
96+
url=_OIDC_BEACON_API_URL + f"/workflows/{_OIDC_BEACON_WORKFLOW_ID}/runs",
97+
headers=headers,
98+
)
99+
resp.raise_for_status()
100+
101+
resp_json = resp.json()
102+
workflow_runs = resp_json["workflow_runs"]
103+
if not workflow_runs:
104+
raise OidcTokenError(f"Found no workflow runs: {resp_json}")
105+
106+
workflow_run = workflow_runs[0]
107+
108+
# If the job is still running, the token artifact won't have been generated yet.
109+
if workflow_run["status"] != "completed":
110+
continue
111+
112+
run_id = workflow_run["id"]
113+
workflow_time = datetime.strptime(
114+
workflow_run["run_started_at"], "%Y-%m-%dT%H:%M:%SZ"
115+
)
116+
117+
resp = session.get(
118+
url=_OIDC_BEACON_API_URL + f"/runs/{run_id}/artifacts",
119+
headers=headers,
120+
)
121+
resp.raise_for_status()
122+
123+
resp_json = resp.json()
124+
artifacts = resp_json["artifacts"]
125+
if len(artifacts) != 1:
126+
raise OidcTokenError(
127+
f"Found unexpected number of artifacts on OIDC beacon run: {artifacts}"
128+
)
129+
130+
oidc_artifact = artifacts[0]
131+
if oidc_artifact["name"] != "oidc-token":
132+
raise OidcTokenError(
133+
f"Found unexpected artifact on OIDC beacon run: {oidc_artifact['name']}"
134+
)
135+
artifact_id = oidc_artifact["id"]
136+
137+
# Download the OIDC token artifact and unzip the archive.
138+
resp = session.get(
139+
url=_OIDC_BEACON_API_URL + f"/artifacts/{artifact_id}/zip",
140+
headers=headers,
141+
)
142+
resp.raise_for_status()
143+
144+
with ZipFile(BytesIO(resp.content)) as artifact_zip:
145+
artifact_file = artifact_zip.open("oidc-token.txt")
146+
147+
# Strip newline.
148+
return artifact_file.read().decode().rstrip()
149+
150+
151+
@pytest.fixture
152+
def client(pytestconfig, identity_token):
58153
"""
59154
Parametrize each test with the client under test.
60155
"""
61156
entrypoint = pytestconfig.getoption("--entrypoint")
62-
identity_token = pytestconfig.getoption("--identity-token")
63157
return SigstoreClient(entrypoint, identity_token)
64158

65159

0 commit comments

Comments
 (0)