Skip to content

Commit c44df49

Browse files
DarkaMaulfacutuescawoodruffw
authored
Add the PEP 740 related objects (#36)
Co-authored-by: Facundo Tuesca <facundo.tuesca@trailofbits.com> Co-authored-by: William Woodruff <william@trailofbits.com>
1 parent 196f2bc commit c44df49

File tree

5 files changed

+170
-6
lines changed

5 files changed

+170
-6
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212
- The minimum Python version required has been bumped to `3.11`
1313
([#37](https://github.com/trailofbits/pypi-attestations/pull/37))
1414

15+
- The `Provenance`, `Publisher`, `GitHubPublisher`, `GitLabPublisher`, and
16+
`AttestationBundle` types have been added
17+
([#36](https://github.com/trailofbits/pypi-attestations/pull/36)).
18+
1519
## [0.0.9]
1620

1721
### Added

Makefile

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,7 @@ dev: $(VENV)/pyvenv.cfg
4646
$(VENV)/pyvenv.cfg: pyproject.toml
4747
# Create our Python 3 virtual environment
4848
python3 -m venv env
49-
# NOTE(ekilmer): interrogate v1.5.0 needs setuptools when using Python 3.12+.
50-
# This should be fixed when the next release is made
51-
$(VENV_BIN)/python -m pip install --upgrade pip setuptools
49+
$(VENV_BIN)/python -m pip install --upgrade pip
5250
$(VENV_BIN)/python -m pip install -e .[$(INSTALL_EXTRA)]
5351

5452
.PHONY: lint

src/pypi_attestations/__init__.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,33 @@
44

55
from ._impl import (
66
Attestation,
7+
AttestationBundle,
78
AttestationError,
89
AttestationType,
910
ConversionError,
1011
Distribution,
1112
Envelope,
13+
GitHubPublisher,
14+
GitLabPublisher,
15+
Provenance,
16+
Publisher,
1217
TransparencyLogEntry,
1318
VerificationError,
1419
VerificationMaterial,
1520
)
1621

1722
__all__ = [
1823
"Attestation",
24+
"AttestationBundle",
1925
"AttestationError",
2026
"AttestationType",
21-
"Envelope",
2227
"ConversionError",
2328
"Distribution",
29+
"Envelope",
30+
"GitHubPublisher",
31+
"GitLabPublisher",
32+
"Provenance",
33+
"Publisher",
2434
"TransparencyLogEntry",
2535
"VerificationError",
2636
"VerificationMaterial",

src/pypi_attestations/_impl.py

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
from cryptography import x509
1515
from cryptography.hazmat.primitives import serialization
1616
from packaging.utils import parse_sdist_filename, parse_wheel_filename
17-
from pydantic import Base64Bytes, BaseModel, field_validator
17+
from pydantic import Base64Bytes, BaseModel, ConfigDict, Field, field_validator
18+
from pydantic.alias_generators import to_snake
1819
from pydantic_core import ValidationError
1920
from sigstore._utils import _sha256_streaming
2021
from sigstore.dsse import DigestSet, StatementBuilder, Subject, _Statement
@@ -331,3 +332,83 @@ def _ultranormalize_dist_filename(dist: str) -> str:
331332
return f"{name}-{ver}.tar.gz"
332333
else:
333334
raise ValueError(f"unknown distribution format: {dist}")
335+
336+
337+
class _PublisherBase(BaseModel):
338+
model_config = ConfigDict(alias_generator=to_snake)
339+
340+
kind: str
341+
claims: dict[str, Any] | None = None
342+
343+
344+
class GitHubPublisher(_PublisherBase):
345+
"""A GitHub-based Trusted Publisher."""
346+
347+
kind: Literal["GitHub"] = "GitHub"
348+
349+
repository: str
350+
"""
351+
The fully qualified publishing repository slug, e.g. `foo/bar` for
352+
repository `bar` owned by `foo`.
353+
"""
354+
355+
workflow: str
356+
"""
357+
The filename of the GitHub Actions workflow that performed the publishing
358+
action.
359+
"""
360+
361+
environment: str | None = None
362+
"""
363+
The optional name GitHub Actions environment that the publishing
364+
action was performed from.
365+
"""
366+
367+
368+
class GitLabPublisher(_PublisherBase):
369+
"""A GitLab-based Trusted Publisher."""
370+
371+
kind: Literal["GitLab"] = "GitLab"
372+
373+
repository: str
374+
"""
375+
The fully qualified publishing repository slug, e.g. `foo/bar` for
376+
repository `bar` owned by `foo` or `foo/baz/bar` for repository
377+
`bar` owned by group `foo` and subgroup `baz`.
378+
"""
379+
380+
environment: str | None = None
381+
"""
382+
The optional environment that the publishing action was performed from.
383+
"""
384+
385+
386+
Publisher = Annotated[GitHubPublisher | GitLabPublisher, Field(discriminator="kind")]
387+
388+
389+
class AttestationBundle(BaseModel):
390+
"""AttestationBundle object as defined in PEP 740."""
391+
392+
publisher: Publisher
393+
"""
394+
The publisher associated with this set of attestations.
395+
"""
396+
397+
attestations: list[Attestation]
398+
"""
399+
The list of attestations included in this bundle.
400+
"""
401+
402+
403+
class Provenance(BaseModel):
404+
"""Provenance object as defined in PEP 740."""
405+
406+
version: Literal[1] = 1
407+
"""
408+
The provenance object's version, which is always 1.
409+
"""
410+
411+
attestation_bundles: list[AttestationBundle]
412+
"""
413+
One or more attestation "bundles".
414+
"""

test/test_impl.py

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Internal implementation tests."""
22

3+
import json
34
import os
45
from hashlib import sha256
56
from pathlib import Path
@@ -8,7 +9,7 @@
89
import pypi_attestations._impl as impl
910
import pytest
1011
import sigstore
11-
from pydantic import ValidationError
12+
from pydantic import TypeAdapter, ValidationError
1213
from sigstore.dsse import DigestSet, StatementBuilder, Subject
1314
from sigstore.models import Bundle
1415
from sigstore.oidc import IdentityToken
@@ -462,3 +463,73 @@ def test_ultranormalize_dist_filename(input: str, normalized: str) -> None:
462463
def test_ultranormalize_dist_filename_invalid(input: str) -> None:
463464
with pytest.raises(ValueError):
464465
impl._ultranormalize_dist_filename(input)
466+
467+
468+
class TestPublisher:
469+
def test_discriminator(self) -> None:
470+
gh_raw = {"kind": "GitHub", "repository": "foo/bar", "workflow": "publish.yml"}
471+
gh = TypeAdapter(impl.Publisher).validate_python(gh_raw)
472+
473+
assert isinstance(gh, impl.GitHubPublisher)
474+
assert gh.repository == "foo/bar"
475+
assert gh.workflow == "publish.yml"
476+
assert TypeAdapter(impl.Publisher).validate_json(json.dumps(gh_raw)) == gh
477+
478+
gl_raw = {"kind": "GitLab", "repository": "foo/bar/baz", "environment": "publish"}
479+
gl = TypeAdapter(impl.Publisher).validate_python(gl_raw)
480+
assert isinstance(gl, impl.GitLabPublisher)
481+
assert gl.repository == "foo/bar/baz"
482+
assert gl.environment == "publish"
483+
assert TypeAdapter(impl.Publisher).validate_json(json.dumps(gl_raw)) == gl
484+
485+
def test_wrong_kind(self) -> None:
486+
with pytest.raises(ValueError, match="Input should be 'GitHub'"):
487+
impl.GitHubPublisher(kind="wrong", repository="foo/bar", workflow="publish.yml")
488+
489+
with pytest.raises(ValueError, match="Input should be 'GitLab'"):
490+
impl.GitLabPublisher(kind="GitHub", repository="foo/bar")
491+
492+
def test_claims(self) -> None:
493+
raw = {
494+
"kind": "GitHub",
495+
"repository": "foo/bar",
496+
"workflow": "publish.yml",
497+
"claims": {
498+
"this": "is-preserved",
499+
"this-too": 123,
500+
},
501+
}
502+
pub = TypeAdapter(impl.Publisher).validate_python(raw)
503+
504+
assert pub.claims == {
505+
"this": "is-preserved",
506+
"this-too": 123,
507+
}
508+
509+
510+
class TestProvenance:
511+
def test_version(self) -> None:
512+
attestation = impl.Attestation.model_validate_json(dist_attestation_path.read_bytes())
513+
provenance = impl.Provenance(
514+
attestation_bundles=[
515+
impl.AttestationBundle(
516+
publisher=impl.GitHubPublisher(repository="foo/bar", workflow="publish.yml"),
517+
attestations=[attestation],
518+
)
519+
]
520+
)
521+
assert provenance.version == 1
522+
523+
# Setting any other version doesn't work.
524+
with pytest.raises(ValueError):
525+
provenance = impl.Provenance(
526+
version=2,
527+
attestation_bundles=[
528+
impl.AttestationBundle(
529+
publisher=impl.GitHubPublisher(
530+
repository="foo/bar", workflow="publish.yml"
531+
),
532+
attestations=[attestation],
533+
)
534+
],
535+
)

0 commit comments

Comments
 (0)