Skip to content

Commit

Permalink
Add AttestationPayload model
Browse files Browse the repository at this point in the history
  • Loading branch information
facutuesca authored and woodruffw committed May 2, 2024
1 parent 3eefb9e commit 8fc177d
Show file tree
Hide file tree
Showing 4 changed files with 49 additions and 3 deletions.
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,11 @@ target-version = "py39"

[tool.ruff.lint]
select = ["ALL"]
# ANN102 is deprecated
# D203 and D213 are incompatible with D211 and D212 respectively.
# COM812 and ISC001 can cause conflicts when using ruff as a formatter.
# See https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules.
ignore = ["D203", "D213", "COM812", "ISC001"]
ignore = ["ANN102", "D203", "D213", "COM812", "ISC001"]

[tool.ruff.lint.per-file-ignores]

Expand Down
2 changes: 2 additions & 0 deletions src/pypi_attestation_models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from ._impl import (
Attestation,
AttestationPayload,
ConversionError,
InvalidAttestationError,
VerificationMaterial,
Expand All @@ -13,6 +14,7 @@

__all__ = [
"Attestation",
"AttestationPayload",
"ConversionError",
"InvalidAttestationError",
"VerificationMaterial",
Expand Down
35 changes: 33 additions & 2 deletions src/pypi_attestation_models/_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,19 @@

import binascii
from base64 import b64decode, b64encode
from typing import Annotated, Any, Literal, NewType
from hashlib import sha256
from typing import TYPE_CHECKING, Annotated, Any, Literal, NewType

import rfc8785
from annotated_types import MinLen # noqa: TCH002
from cryptography import x509
from cryptography.hazmat.primitives import serialization
from pydantic import BaseModel
from sigstore.models import Bundle, LogEntry

if TYPE_CHECKING:
from pathlib import Path # pragma: no cover


class ConversionError(ValueError):
"""The base error for all errors during conversion."""
Expand Down Expand Up @@ -62,8 +67,34 @@ class Attestation(BaseModel):
message_signature: str
"""
The attestation's signature, as `base64(raw-sig)`, where `raw-sig`
is the raw bytes of the signing operation.
is the raw bytes of the signing operation over the attestation payload.
"""


class AttestationPayload(BaseModel):
"""Attestation Payload object as defined in PEP 740."""

distribution: str
"""
The file name of the Python package distribution.
"""

digest: str
"""
The SHA-256 digest of the distribution's contents, as a hexadecimal string.
"""

@classmethod
def from_dist(cls, dist: Path) -> AttestationPayload:
"""Create an `AttestationPayload` from a distribution file."""
return AttestationPayload(
distribution=dist.name,
digest=sha256(dist.read_bytes()).hexdigest(),
)

def __bytes__(self: AttestationPayload) -> bytes:
"""Convert to bytes using a canonicalized JSON representation (from RFC8785)."""
return rfc8785.dumps(self.model_dump())


def sigstore_to_pypi(sigstore_bundle: Bundle) -> Attestation:
Expand Down
12 changes: 12 additions & 0 deletions test/test_impl.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Internal implementation tests."""

import hashlib
import json
from pathlib import Path

Expand Down Expand Up @@ -80,3 +81,14 @@ def test_verification_roundtrip() -> None:
identity="facundo.tuesca@trailofbits.com", issuer="https://accounts.google.com"
),
)


def test_attestation_payload() -> None:
payload = impl.AttestationPayload.from_dist(artifact_path)

assert payload.digest == hashlib.sha256(artifact_path.read_bytes()).hexdigest()
assert payload.distribution == artifact_path.name

expected = f'{{"digest":"{payload.digest}","distribution":"{payload.distribution}"}}'

assert bytes(payload) == bytes(expected, "utf-8")

0 comments on commit 8fc177d

Please sign in to comment.