pypi_attestations

The pypi-attestations APIs.

 1"""The `pypi-attestations` APIs."""
 2
 3__version__ = "0.0.16"
 4
 5from ._impl import (
 6    Attestation,
 7    AttestationBundle,
 8    AttestationError,
 9    AttestationType,
10    ConversionError,
11    Distribution,
12    Envelope,
13    GitHubPublisher,
14    GitLabPublisher,
15    Provenance,
16    Publisher,
17    TransparencyLogEntry,
18    VerificationError,
19    VerificationMaterial,
20)
21
22__all__ = [
23    "Attestation",
24    "AttestationBundle",
25    "AttestationError",
26    "AttestationType",
27    "ConversionError",
28    "Distribution",
29    "Envelope",
30    "GitHubPublisher",
31    "GitLabPublisher",
32    "Provenance",
33    "Publisher",
34    "TransparencyLogEntry",
35    "VerificationError",
36    "VerificationMaterial",
37]
class Attestation(pydantic.main.BaseModel):
135class Attestation(BaseModel):
136    """Attestation object as defined in PEP 740."""
137
138    version: Literal[1]
139    """
140    The attestation format's version, which is always 1.
141    """
142
143    verification_material: VerificationMaterial
144    """
145    Cryptographic materials used to verify `message_signature`.
146    """
147
148    envelope: Envelope
149    """
150    The enveloped attestation statement and signature.
151    """
152
153    @property
154    def statement(self) -> dict[str, Any]:
155        """Return the statement within this attestation's envelope.
156
157        The value returned here is a dictionary, in the shape of an
158        in-toto statement.
159        """
160        return json.loads(self.envelope.statement)  # type: ignore[no-any-return]
161
162    @classmethod
163    def sign(cls, signer: Signer, dist: Distribution) -> Attestation:
164        """Create an envelope, with signature, from the given Python distribution.
165
166        On failure, raises `AttestationError`.
167        """
168        try:
169            stmt = (
170                StatementBuilder()
171                .subjects(
172                    [
173                        Subject(
174                            name=dist.name,
175                            digest=DigestSet(root={"sha256": dist.digest}),
176                        )
177                    ]
178                )
179                .predicate_type(AttestationType.PYPI_PUBLISH_V1)
180                .build()
181            )
182        except DsseError as e:
183            raise AttestationError(str(e))
184
185        try:
186            bundle = signer.sign_dsse(stmt)
187        except (ExpiredCertificate, ExpiredIdentity) as e:
188            raise AttestationError(str(e))
189
190        try:
191            return Attestation.from_bundle(bundle)
192        except ConversionError as e:
193            raise AttestationError(str(e))
194
195    def verify(
196        self,
197        identity: VerificationPolicy | Publisher,
198        dist: Distribution,
199        *,
200        staging: bool = False,
201    ) -> tuple[str, Optional[dict[str, Any]]]:
202        """Verify against an existing Python distribution.
203
204        The `identity` can be an object confirming to
205        `sigstore.policy.VerificationPolicy` or a `Publisher`, which will be
206        transformed into an appropriate verification policy.
207
208        By default, Sigstore's production verifier will be used. The
209        `staging` parameter can be toggled to enable the staging verifier
210        instead.
211
212        On failure, raises an appropriate subclass of `AttestationError`.
213        """
214        # NOTE: Can't do `isinstance` with `Publisher` since it's
215        # a `_GenericAlias`; instead we punch through to the inner
216        # `_Publisher` union.
217        # Use of typing.get_args is needed for Python < 3.10
218        if isinstance(identity, get_args(_Publisher)):
219            policy = identity._as_policy()  # noqa: SLF001
220        else:
221            policy = identity
222
223        if staging:
224            verifier = Verifier.staging()
225        else:
226            verifier = Verifier.production()
227
228        bundle = self.to_bundle()
229        try:
230            type_, payload = verifier.verify_dsse(bundle, policy)
231        except sigstore.errors.VerificationError as err:
232            raise VerificationError(str(err)) from err
233
234        if type_ != DsseEnvelope._TYPE:  # noqa: SLF001
235            raise VerificationError(f"expected JSON envelope, got {type_}")
236
237        try:
238            statement = _Statement.model_validate_json(payload)
239        except ValidationError as e:
240            raise VerificationError(f"invalid statement: {str(e)}")
241
242        if len(statement.subjects) != 1:
243            raise VerificationError("too many subjects in statement (must be exactly one)")
244        subject = statement.subjects[0]
245
246        if not subject.name:
247            raise VerificationError("invalid subject: missing name")
248
249        try:
250            # We always ultranormalize when signing, but other signers may not.
251            subject_name = _ultranormalize_dist_filename(subject.name)
252        except ValueError as e:
253            raise VerificationError(f"invalid subject: {str(e)}")
254
255        if subject_name != dist.name:
256            raise VerificationError(
257                f"subject does not match distribution name: {subject_name} != {dist.name}"
258            )
259
260        digest = subject.digest.root.get("sha256")
261        if digest is None or digest != dist.digest:
262            raise VerificationError("subject does not match distribution digest")
263
264        try:
265            AttestationType(statement.predicate_type)
266        except ValueError:
267            raise VerificationError(f"unknown attestation type: {statement.predicate_type}")
268
269        return statement.predicate_type, statement.predicate
270
271    def to_bundle(self) -> Bundle:
272        """Convert a PyPI attestation object as defined in PEP 740 into a Sigstore Bundle."""
273        cert_bytes = self.verification_material.certificate
274        statement = self.envelope.statement
275        signature = self.envelope.signature
276
277        evp = DsseEnvelope(
278            _Envelope(
279                payload=statement,
280                payload_type=DsseEnvelope._TYPE,  # noqa: SLF001
281                signatures=[_Signature(sig=signature)],
282            )
283        )
284
285        tlog_entry = self.verification_material.transparency_entries[0]
286        try:
287            certificate = x509.load_der_x509_certificate(cert_bytes)
288        except ValueError as err:
289            raise ConversionError("invalid X.509 certificate") from err
290
291        try:
292            log_entry = LogEntry._from_dict_rekor(tlog_entry)  # noqa: SLF001
293        except (ValidationError, sigstore.errors.Error) as err:
294            raise ConversionError("invalid transparency log entry") from err
295
296        return Bundle._from_parts(  # noqa: SLF001
297            cert=certificate,
298            content=evp,
299            log_entry=log_entry,
300        )
301
302    @classmethod
303    def from_bundle(cls, sigstore_bundle: Bundle) -> Attestation:
304        """Convert a Sigstore Bundle into a PyPI attestation as defined in PEP 740."""
305        certificate = sigstore_bundle.signing_certificate.public_bytes(
306            encoding=serialization.Encoding.DER
307        )
308
309        envelope = sigstore_bundle._inner.dsse_envelope  # noqa: SLF001
310
311        if len(envelope.signatures) != 1:
312            raise ConversionError(f"expected exactly one signature, got {len(envelope.signatures)}")
313
314        return cls(
315            version=1,
316            verification_material=VerificationMaterial(
317                certificate=base64.b64encode(certificate),
318                transparency_entries=[
319                    sigstore_bundle.log_entry._to_rekor().to_dict()  # noqa: SLF001
320                ],
321            ),
322            envelope=Envelope(
323                statement=base64.b64encode(envelope.payload),
324                signature=base64.b64encode(envelope.signatures[0].sig),
325            ),
326        )

Attestation object as defined in PEP 740.

version: Literal[1]

The attestation format's version, which is always 1.

verification_material: VerificationMaterial

Cryptographic materials used to verify message_signature.

envelope: Envelope

The enveloped attestation statement and signature.

statement: dict[str, typing.Any]
153    @property
154    def statement(self) -> dict[str, Any]:
155        """Return the statement within this attestation's envelope.
156
157        The value returned here is a dictionary, in the shape of an
158        in-toto statement.
159        """
160        return json.loads(self.envelope.statement)  # type: ignore[no-any-return]

Return the statement within this attestation's envelope.

The value returned here is a dictionary, in the shape of an in-toto statement.

@classmethod
def sign( cls, signer: sigstore.sign.Signer, dist: Distribution) -> Attestation:
162    @classmethod
163    def sign(cls, signer: Signer, dist: Distribution) -> Attestation:
164        """Create an envelope, with signature, from the given Python distribution.
165
166        On failure, raises `AttestationError`.
167        """
168        try:
169            stmt = (
170                StatementBuilder()
171                .subjects(
172                    [
173                        Subject(
174                            name=dist.name,
175                            digest=DigestSet(root={"sha256": dist.digest}),
176                        )
177                    ]
178                )
179                .predicate_type(AttestationType.PYPI_PUBLISH_V1)
180                .build()
181            )
182        except DsseError as e:
183            raise AttestationError(str(e))
184
185        try:
186            bundle = signer.sign_dsse(stmt)
187        except (ExpiredCertificate, ExpiredIdentity) as e:
188            raise AttestationError(str(e))
189
190        try:
191            return Attestation.from_bundle(bundle)
192        except ConversionError as e:
193            raise AttestationError(str(e))

Create an envelope, with signature, from the given Python distribution.

On failure, raises AttestationError.

def verify( self, identity: Union[sigstore.verify.policy.VerificationPolicy, Annotated[Union[GitHubPublisher, GitLabPublisher], FieldInfo(annotation=NoneType, required=True, discriminator='kind')]], dist: Distribution, *, staging: bool = False) -> tuple[str, typing.Optional[dict[str, typing.Any]]]:
195    def verify(
196        self,
197        identity: VerificationPolicy | Publisher,
198        dist: Distribution,
199        *,
200        staging: bool = False,
201    ) -> tuple[str, Optional[dict[str, Any]]]:
202        """Verify against an existing Python distribution.
203
204        The `identity` can be an object confirming to
205        `sigstore.policy.VerificationPolicy` or a `Publisher`, which will be
206        transformed into an appropriate verification policy.
207
208        By default, Sigstore's production verifier will be used. The
209        `staging` parameter can be toggled to enable the staging verifier
210        instead.
211
212        On failure, raises an appropriate subclass of `AttestationError`.
213        """
214        # NOTE: Can't do `isinstance` with `Publisher` since it's
215        # a `_GenericAlias`; instead we punch through to the inner
216        # `_Publisher` union.
217        # Use of typing.get_args is needed for Python < 3.10
218        if isinstance(identity, get_args(_Publisher)):
219            policy = identity._as_policy()  # noqa: SLF001
220        else:
221            policy = identity
222
223        if staging:
224            verifier = Verifier.staging()
225        else:
226            verifier = Verifier.production()
227
228        bundle = self.to_bundle()
229        try:
230            type_, payload = verifier.verify_dsse(bundle, policy)
231        except sigstore.errors.VerificationError as err:
232            raise VerificationError(str(err)) from err
233
234        if type_ != DsseEnvelope._TYPE:  # noqa: SLF001
235            raise VerificationError(f"expected JSON envelope, got {type_}")
236
237        try:
238            statement = _Statement.model_validate_json(payload)
239        except ValidationError as e:
240            raise VerificationError(f"invalid statement: {str(e)}")
241
242        if len(statement.subjects) != 1:
243            raise VerificationError("too many subjects in statement (must be exactly one)")
244        subject = statement.subjects[0]
245
246        if not subject.name:
247            raise VerificationError("invalid subject: missing name")
248
249        try:
250            # We always ultranormalize when signing, but other signers may not.
251            subject_name = _ultranormalize_dist_filename(subject.name)
252        except ValueError as e:
253            raise VerificationError(f"invalid subject: {str(e)}")
254
255        if subject_name != dist.name:
256            raise VerificationError(
257                f"subject does not match distribution name: {subject_name} != {dist.name}"
258            )
259
260        digest = subject.digest.root.get("sha256")
261        if digest is None or digest != dist.digest:
262            raise VerificationError("subject does not match distribution digest")
263
264        try:
265            AttestationType(statement.predicate_type)
266        except ValueError:
267            raise VerificationError(f"unknown attestation type: {statement.predicate_type}")
268
269        return statement.predicate_type, statement.predicate

Verify against an existing Python distribution.

The identity can be an object confirming to sigstore.policy.VerificationPolicy or a Publisher, which will be transformed into an appropriate verification policy.

By default, Sigstore's production verifier will be used. The staging parameter can be toggled to enable the staging verifier instead.

On failure, raises an appropriate subclass of AttestationError.

def to_bundle(self) -> sigstore.models.Bundle:
271    def to_bundle(self) -> Bundle:
272        """Convert a PyPI attestation object as defined in PEP 740 into a Sigstore Bundle."""
273        cert_bytes = self.verification_material.certificate
274        statement = self.envelope.statement
275        signature = self.envelope.signature
276
277        evp = DsseEnvelope(
278            _Envelope(
279                payload=statement,
280                payload_type=DsseEnvelope._TYPE,  # noqa: SLF001
281                signatures=[_Signature(sig=signature)],
282            )
283        )
284
285        tlog_entry = self.verification_material.transparency_entries[0]
286        try:
287            certificate = x509.load_der_x509_certificate(cert_bytes)
288        except ValueError as err:
289            raise ConversionError("invalid X.509 certificate") from err
290
291        try:
292            log_entry = LogEntry._from_dict_rekor(tlog_entry)  # noqa: SLF001
293        except (ValidationError, sigstore.errors.Error) as err:
294            raise ConversionError("invalid transparency log entry") from err
295
296        return Bundle._from_parts(  # noqa: SLF001
297            cert=certificate,
298            content=evp,
299            log_entry=log_entry,
300        )

Convert a PyPI attestation object as defined in PEP 740 into a Sigstore Bundle.

@classmethod
def from_bundle( cls, sigstore_bundle: sigstore.models.Bundle) -> Attestation:
302    @classmethod
303    def from_bundle(cls, sigstore_bundle: Bundle) -> Attestation:
304        """Convert a Sigstore Bundle into a PyPI attestation as defined in PEP 740."""
305        certificate = sigstore_bundle.signing_certificate.public_bytes(
306            encoding=serialization.Encoding.DER
307        )
308
309        envelope = sigstore_bundle._inner.dsse_envelope  # noqa: SLF001
310
311        if len(envelope.signatures) != 1:
312            raise ConversionError(f"expected exactly one signature, got {len(envelope.signatures)}")
313
314        return cls(
315            version=1,
316            verification_material=VerificationMaterial(
317                certificate=base64.b64encode(certificate),
318                transparency_entries=[
319                    sigstore_bundle.log_entry._to_rekor().to_dict()  # noqa: SLF001
320                ],
321            ),
322            envelope=Envelope(
323                statement=base64.b64encode(envelope.payload),
324                signature=base64.b64encode(envelope.signatures[0].sig),
325            ),
326        )

Convert a Sigstore Bundle into a PyPI attestation as defined in PEP 740.

model_config: ClassVar[pydantic.config.ConfigDict] = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

model_fields: ClassVar[Dict[str, pydantic.fields.FieldInfo]] = {'version': FieldInfo(annotation=Literal[1], required=True), 'verification_material': FieldInfo(annotation=VerificationMaterial, required=True), 'envelope': FieldInfo(annotation=Envelope, required=True)}

Metadata about the fields defined on the model, mapping of field names to [FieldInfo][pydantic.fields.FieldInfo] objects.

This replaces Model.__fields__ from Pydantic V1.

model_computed_fields: ClassVar[Dict[str, pydantic.fields.ComputedFieldInfo]] = {}

A dictionary of computed field names and their corresponding ComputedFieldInfo objects.

class AttestationBundle(pydantic.main.BaseModel):
553class AttestationBundle(BaseModel):
554    """AttestationBundle object as defined in PEP 740."""
555
556    publisher: Publisher
557    """
558    The publisher associated with this set of attestations.
559    """
560
561    attestations: list[Attestation]
562    """
563    The list of attestations included in this bundle.
564    """

AttestationBundle object as defined in PEP 740.

publisher: Annotated[Union[GitHubPublisher, GitLabPublisher], FieldInfo(annotation=NoneType, required=True, discriminator='kind')]

The publisher associated with this set of attestations.

attestations: list[Attestation]

The list of attestations included in this bundle.

model_config: ClassVar[pydantic.config.ConfigDict] = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

model_fields: ClassVar[Dict[str, pydantic.fields.FieldInfo]] = {'publisher': FieldInfo(annotation=Union[GitHubPublisher, GitLabPublisher], required=True, discriminator='kind'), 'attestations': FieldInfo(annotation=list[Attestation], required=True)}

Metadata about the fields defined on the model, mapping of field names to [FieldInfo][pydantic.fields.FieldInfo] objects.

This replaces Model.__fields__ from Pydantic V1.

model_computed_fields: ClassVar[Dict[str, pydantic.fields.ComputedFieldInfo]] = {}

A dictionary of computed field names and their corresponding ComputedFieldInfo objects.

class AttestationError(builtins.ValueError):
101class AttestationError(ValueError):
102    """Base error for all APIs."""

Base error for all APIs.

class AttestationType(builtins.str, enum.Enum):
94class AttestationType(str, Enum):
95    """Attestation types known to PyPI."""
96
97    SLSA_PROVENANCE_V1 = "https://slsa.dev/provenance/v1"
98    PYPI_PUBLISH_V1 = "https://docs.pypi.org/attestations/publish/v1"

Attestation types known to PyPI.

SLSA_PROVENANCE_V1 = <AttestationType.SLSA_PROVENANCE_V1: 'https://slsa.dev/provenance/v1'>
PYPI_PUBLISH_V1 = <AttestationType.PYPI_PUBLISH_V1: 'https://docs.pypi.org/attestations/publish/v1'>
class ConversionError(pypi_attestations.AttestationError):
105class ConversionError(AttestationError):
106    """The base error for all errors during conversion."""

The base error for all errors during conversion.

class Distribution(pydantic.main.BaseModel):
66class Distribution(BaseModel):
67    """Represents a Python package distribution.
68
69    A distribution is identified by its (sdist or wheel) filename, which
70    provides the package name and version (at a minimum) plus a SHA-256
71    digest, which uniquely identifies its contents.
72    """
73
74    name: str
75    digest: str
76
77    @field_validator("name")
78    @classmethod
79    def _validate_name(cls, v: str) -> str:
80        return _ultranormalize_dist_filename(v)
81
82    @classmethod
83    def from_file(cls, dist: Path) -> Distribution:
84        """Construct a `Distribution` from the given path."""
85        name = dist.name
86        with dist.open(mode="rb", buffering=0) as io:
87            # Replace this with `hashlib.file_digest()` once
88            # our minimum supported Python is >=3.11
89            digest = _sha256_streaming(io).hex()
90
91        return cls(name=name, digest=digest)

Represents a Python package distribution.

A distribution is identified by its (sdist or wheel) filename, which provides the package name and version (at a minimum) plus a SHA-256 digest, which uniquely identifies its contents.

name: str
digest: str
@classmethod
def from_file(cls, dist: pathlib.Path) -> Distribution:
82    @classmethod
83    def from_file(cls, dist: Path) -> Distribution:
84        """Construct a `Distribution` from the given path."""
85        name = dist.name
86        with dist.open(mode="rb", buffering=0) as io:
87            # Replace this with `hashlib.file_digest()` once
88            # our minimum supported Python is >=3.11
89            digest = _sha256_streaming(io).hex()
90
91        return cls(name=name, digest=digest)

Construct a Distribution from the given path.

model_config: ClassVar[pydantic.config.ConfigDict] = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

model_fields: ClassVar[Dict[str, pydantic.fields.FieldInfo]] = {'name': FieldInfo(annotation=str, required=True), 'digest': FieldInfo(annotation=str, required=True)}

Metadata about the fields defined on the model, mapping of field names to [FieldInfo][pydantic.fields.FieldInfo] objects.

This replaces Model.__fields__ from Pydantic V1.

model_computed_fields: ClassVar[Dict[str, pydantic.fields.ComputedFieldInfo]] = {}

A dictionary of computed field names and their corresponding ComputedFieldInfo objects.

class Envelope(pydantic.main.BaseModel):
329class Envelope(BaseModel):
330    """The attestation envelope, containing the attested-for payload and its signature."""
331
332    statement: Base64Bytes
333    """
334    The attestation statement.
335
336    This is represented as opaque bytes on the wire (encoded as base64),
337    but it MUST be an JSON in-toto v1 Statement.
338    """
339
340    signature: Base64Bytes
341    """
342    A signature for the above statement, encoded as base64.
343    """

The attestation envelope, containing the attested-for payload and its signature.

statement: Annotated[bytes, EncodedBytes(encoder=<class 'pypi_attestations._impl.Base64EncoderSansNewline'>)]

The attestation statement.

This is represented as opaque bytes on the wire (encoded as base64), but it MUST be an JSON in-toto v1 Statement.

signature: Annotated[bytes, EncodedBytes(encoder=<class 'pypi_attestations._impl.Base64EncoderSansNewline'>)]

A signature for the above statement, encoded as base64.

model_config: ClassVar[pydantic.config.ConfigDict] = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

model_fields: ClassVar[Dict[str, pydantic.fields.FieldInfo]] = {'statement': FieldInfo(annotation=bytes, required=True, metadata=[EncodedBytes(encoder=<class 'pypi_attestations._impl.Base64EncoderSansNewline'>)]), 'signature': FieldInfo(annotation=bytes, required=True, metadata=[EncodedBytes(encoder=<class 'pypi_attestations._impl.Base64EncoderSansNewline'>)])}

Metadata about the fields defined on the model, mapping of field names to [FieldInfo][pydantic.fields.FieldInfo] objects.

This replaces Model.__fields__ from Pydantic V1.

model_computed_fields: ClassVar[Dict[str, pydantic.fields.ComputedFieldInfo]] = {}

A dictionary of computed field names and their corresponding ComputedFieldInfo objects.

class GitHubPublisher(pypi_attestations._impl._PublisherBase):
484class GitHubPublisher(_PublisherBase):
485    """A GitHub-based Trusted Publisher."""
486
487    kind: Literal["GitHub"] = "GitHub"
488
489    repository: str
490    """
491    The fully qualified publishing repository slug, e.g. `foo/bar` for
492    repository `bar` owned by `foo`.
493    """
494
495    workflow: str
496    """
497    The filename of the GitHub Actions workflow that performed the publishing
498    action.
499    """
500
501    environment: Optional[str] = None
502    """
503    The optional name GitHub Actions environment that the publishing
504    action was performed from.
505    """
506
507    def _as_policy(self) -> VerificationPolicy:
508        return _GitHubTrustedPublisherPolicy(self.repository, self.workflow)

A GitHub-based Trusted Publisher.

kind: Literal['GitHub']
repository: str

The fully qualified publishing repository slug, e.g. foo/bar for repository bar owned by foo.

workflow: str

The filename of the GitHub Actions workflow that performed the publishing action.

environment: Optional[str]

The optional name GitHub Actions environment that the publishing action was performed from.

model_config = {'alias_generator': <function to_snake>}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

model_fields: ClassVar[Dict[str, pydantic.fields.FieldInfo]] = {'kind': FieldInfo(annotation=Literal['GitHub'], required=False, default='GitHub', alias='kind', alias_priority=1), 'claims': FieldInfo(annotation=Union[dict[str, Any], NoneType], required=False, default=None, alias='claims', alias_priority=1), 'repository': FieldInfo(annotation=str, required=True, alias='repository', alias_priority=1), 'workflow': FieldInfo(annotation=str, required=True, alias='workflow', alias_priority=1), 'environment': FieldInfo(annotation=Union[str, NoneType], required=False, default=None, alias='environment', alias_priority=1)}

Metadata about the fields defined on the model, mapping of field names to [FieldInfo][pydantic.fields.FieldInfo] objects.

This replaces Model.__fields__ from Pydantic V1.

model_computed_fields: ClassVar[Dict[str, pydantic.fields.ComputedFieldInfo]] = {}

A dictionary of computed field names and their corresponding ComputedFieldInfo objects.

class GitLabPublisher(pypi_attestations._impl._PublisherBase):
511class GitLabPublisher(_PublisherBase):
512    """A GitLab-based Trusted Publisher."""
513
514    kind: Literal["GitLab"] = "GitLab"
515
516    repository: str
517    """
518    The fully qualified publishing repository slug, e.g. `foo/bar` for
519    repository `bar` owned by `foo` or `foo/baz/bar` for repository
520    `bar` owned by group `foo` and subgroup `baz`.
521    """
522
523    environment: Optional[str] = None
524    """
525    The optional environment that the publishing action was performed from.
526    """
527
528    def _as_policy(self) -> VerificationPolicy:
529        policies: list[VerificationPolicy] = [
530            policy.OIDCIssuerV2("https://gitlab.com"),
531            policy.OIDCSourceRepositoryURI(f"https://gitlab.com/{self.repository}"),
532        ]
533
534        if not self.claims:
535            raise VerificationError("refusing to build a policy without claims")
536
537        if ref := self.claims.get("ref"):
538            policies.append(
539                policy.OIDCBuildConfigURI(
540                    f"https://gitlab.com/{self.repository}//.gitlab-ci.yml@{ref}"
541                )
542            )
543        else:
544            raise VerificationError("refusing to build a policy without a ref claim")
545
546        return policy.AllOf(policies)

A GitLab-based Trusted Publisher.

kind: Literal['GitLab']
repository: str

The fully qualified publishing repository slug, e.g. foo/bar for repository bar owned by foo or foo/baz/bar for repository bar owned by group foo and subgroup baz.

environment: Optional[str]

The optional environment that the publishing action was performed from.

model_config = {'alias_generator': <function to_snake>}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

model_fields: ClassVar[Dict[str, pydantic.fields.FieldInfo]] = {'kind': FieldInfo(annotation=Literal['GitLab'], required=False, default='GitLab', alias='kind', alias_priority=1), 'claims': FieldInfo(annotation=Union[dict[str, Any], NoneType], required=False, default=None, alias='claims', alias_priority=1), 'repository': FieldInfo(annotation=str, required=True, alias='repository', alias_priority=1), 'environment': FieldInfo(annotation=Union[str, NoneType], required=False, default=None, alias='environment', alias_priority=1)}

Metadata about the fields defined on the model, mapping of field names to [FieldInfo][pydantic.fields.FieldInfo] objects.

This replaces Model.__fields__ from Pydantic V1.

model_computed_fields: ClassVar[Dict[str, pydantic.fields.ComputedFieldInfo]] = {}

A dictionary of computed field names and their corresponding ComputedFieldInfo objects.

class Provenance(pydantic.main.BaseModel):
567class Provenance(BaseModel):
568    """Provenance object as defined in PEP 740."""
569
570    version: Literal[1] = 1
571    """
572    The provenance object's version, which is always 1.
573    """
574
575    attestation_bundles: list[AttestationBundle]
576    """
577    One or more attestation "bundles".
578    """

Provenance object as defined in PEP 740.

version: Literal[1]

The provenance object's version, which is always 1.

attestation_bundles: list[AttestationBundle]

One or more attestation "bundles".

model_config: ClassVar[pydantic.config.ConfigDict] = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

model_fields: ClassVar[Dict[str, pydantic.fields.FieldInfo]] = {'version': FieldInfo(annotation=Literal[1], required=False, default=1), 'attestation_bundles': FieldInfo(annotation=list[AttestationBundle], required=True)}

Metadata about the fields defined on the model, mapping of field names to [FieldInfo][pydantic.fields.FieldInfo] objects.

This replaces Model.__fields__ from Pydantic V1.

model_computed_fields: ClassVar[Dict[str, pydantic.fields.ComputedFieldInfo]] = {}

A dictionary of computed field names and their corresponding ComputedFieldInfo objects.

Publisher = typing.Annotated[typing.Union[GitHubPublisher, GitLabPublisher], FieldInfo(annotation=NoneType, required=True, discriminator='kind')]
TransparencyLogEntry = TransparencyLogEntry
class VerificationError(pypi_attestations.AttestationError):
109class VerificationError(AttestationError):
110    """The PyPI Attestation failed verification."""
111
112    def __init__(self: VerificationError, msg: str) -> None:
113        """Initialize an `VerificationError`."""
114        super().__init__(f"Verification failed: {msg}")

The PyPI Attestation failed verification.

VerificationError(msg: str)
112    def __init__(self: VerificationError, msg: str) -> None:
113        """Initialize an `VerificationError`."""
114        super().__init__(f"Verification failed: {msg}")

Initialize an VerificationError.

class VerificationMaterial(pydantic.main.BaseModel):
120class VerificationMaterial(BaseModel):
121    """Cryptographic materials used to verify attestation objects."""
122
123    certificate: Base64Bytes
124    """
125    The signing certificate, as `base64(DER(cert))`.
126    """
127
128    transparency_entries: Annotated[list[TransparencyLogEntry], MinLen(1)]
129    """
130    One or more transparency log entries for this attestation's signature
131    and certificate.
132    """

Cryptographic materials used to verify attestation objects.

certificate: Annotated[bytes, EncodedBytes(encoder=<class 'pypi_attestations._impl.Base64EncoderSansNewline'>)]

The signing certificate, as base64(DER(cert)).

transparency_entries: Annotated[list[TransparencyLogEntry], MinLen(min_length=1)]

One or more transparency log entries for this attestation's signature and certificate.

model_config: ClassVar[pydantic.config.ConfigDict] = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

model_fields: ClassVar[Dict[str, pydantic.fields.FieldInfo]] = {'certificate': FieldInfo(annotation=bytes, required=True, metadata=[EncodedBytes(encoder=<class 'pypi_attestations._impl.Base64EncoderSansNewline'>)]), 'transparency_entries': FieldInfo(annotation=list[NewType], required=True, metadata=[MinLen(min_length=1)])}

Metadata about the fields defined on the model, mapping of field names to [FieldInfo][pydantic.fields.FieldInfo] objects.

This replaces Model.__fields__ from Pydantic V1.

model_computed_fields: ClassVar[Dict[str, pydantic.fields.ComputedFieldInfo]] = {}

A dictionary of computed field names and their corresponding ComputedFieldInfo objects.