-
-
Notifications
You must be signed in to change notification settings - Fork 90
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
requirements: Add initial support for uploading PEP 740 attestations
Signed-off-by: William Woodruff <[email protected]>
- Loading branch information
1 parent
699cd61
commit b526ff8
Showing
7 changed files
with
239 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
import logging | ||
import os | ||
import sys | ||
from pathlib import Path | ||
from typing import NoReturn | ||
|
||
from pypi_attestation_models import AttestationPayload | ||
from sigstore.oidc import IdentityError, IdentityToken, detect_credential | ||
from sigstore.sign import Signer, SigningContext | ||
|
||
# Be very verbose. | ||
sigstore_logger = logging.getLogger("sigstore") | ||
sigstore_logger.setLevel(logging.DEBUG) | ||
sigstore_logger.addHandler(logging.StreamHandler()) | ||
|
||
_GITHUB_STEP_SUMMARY = Path(os.getenv("GITHUB_STEP_SUMMARY")) | ||
|
||
# The top-level error message that gets rendered. | ||
# This message wraps one of the other templates/messages defined below. | ||
_ERROR_SUMMARY_MESSAGE = """ | ||
Attestation generation failure: | ||
{message} | ||
You're seeing this because the action attempted to generated PEP 740 | ||
attestations for its inputs, but failed to do so. | ||
""" | ||
|
||
# Rendered if OIDC identity token retrieval fails for any reason. | ||
_TOKEN_RETRIEVAL_FAILED_MESSAGE = """ | ||
OpenID Connect token retrieval failed: {identity_error} | ||
This generally indicates a workflow configuration error, such as insufficient | ||
permissions. Make sure that your workflow has `id-token: write` configured | ||
at the job level, e.g.: | ||
```yaml | ||
permissions: | ||
id-token: write | ||
``` | ||
Learn more at https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#adding-permissions-settings. | ||
""" | ||
|
||
|
||
def die(msg: str) -> NoReturn: | ||
with _GITHUB_STEP_SUMMARY.open("a", encoding="utf-8") as io: | ||
print(_ERROR_SUMMARY_MESSAGE.format(message=msg), file=io) | ||
|
||
# HACK: GitHub Actions' annotations don't work across multiple lines naively; | ||
# translating `\n` into `%0A` (i.e., HTML percent-encoding) is known to work. | ||
# See: https://github.com/actions/toolkit/issues/193 | ||
msg = msg.replace("\n", "%0A") | ||
print(f"::error::Attestation generation failure: {msg}", file=sys.stderr) | ||
sys.exit(1) | ||
|
||
|
||
def debug(msg: str): | ||
print(f"::debug::{msg}", file=sys.stderr) | ||
|
||
|
||
# pylint: disable=redefined-outer-name | ||
def attest_dist(dist: Path, signer: Signer) -> None: | ||
# We are the publishing step, so there should be no pre-existing publish | ||
# attestation. The presence of one indicates user confusion. | ||
attestation_path = Path(f"{dist}.publish.attestation") | ||
if attestation_path.is_file(): | ||
die(f"{dist} already has a publish attestation: {attestation_path}") | ||
|
||
payload = AttestationPayload.from_dist(dist) | ||
attestation = payload.sign(signer) | ||
|
||
attestation_path.write_text(attestation.model_dump_json(), encoding="utf-8") | ||
debug(f"saved publish attestation: {dist=} {attestation_path=}") | ||
|
||
|
||
packages_dir = Path(sys.argv[1]) | ||
|
||
try: | ||
# NOTE: audience is always sigstore. | ||
oidc_token = detect_credential() | ||
identity = IdentityToken(oidc_token) | ||
except IdentityError as identity_error: | ||
# NOTE: We only perform attestations in trusted publishing flows, so we | ||
# don't need to re-check for the "PR from fork" error mode, only | ||
# generic token retrieval errors. | ||
cause = _TOKEN_RETRIEVAL_FAILED_MESSAGE.format(identity_error=identity_error) | ||
die(cause) | ||
|
||
# Collect all sdists and wheels. | ||
dists = [sdist.absolute() for sdist in packages_dir.glob("*.tar.gz")] | ||
dists.extend(whl.absolute() for whl in packages_dir.glob("*.whl")) | ||
|
||
with SigningContext.production().signer(identity, cache=True) as signer: | ||
for dist in dists: | ||
# This should never really happen, but some versions of GitHub's | ||
# download-artifact will create a subdirectory with the same name | ||
# as the artifact being downloaded, e.g. `dist/foo.whl/foo.whl`. | ||
if not dist.is_file(): | ||
die(f"Path looks like a distribution but is not a file: {dist}") | ||
|
||
attest_dist(dist, signer) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,14 @@ | ||
twine | ||
|
||
# NOTE: Used to detect an ambient OIDC credential for OIDC publishing. | ||
# NOTE: Used to detect an ambient OIDC credential for OIDC publishing, | ||
# as well as PEP 740 attestations. | ||
id ~= 1.0 | ||
|
||
# NOTE: This is pulled in transitively through `twine`, but we also declare | ||
# NOTE: it explicitly here because `oidc-exchange.py` uses it. | ||
# Ref: https://github.com/di/id | ||
requests | ||
|
||
# NOTE: Used to generate attestations. | ||
pypi-attestation-models == 0.0.2 | ||
sigstore ~= 3.0.0 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters