Skip to content

Commit

Permalink
Sign models with Sigstore, generate Sigstore bundles
Browse files Browse the repository at this point in the history
Supports both signing models serialized to digests (a la
`serialize_v0`/`serialize_v1`) and models serialized to manifests.

Suppoorts both signing digests directly and signing in-toto manifest.

There is a need to convert from in-toto's in-toto types to the ones
expected by sigstore-python, but this additional step will be removed in
the future.

Signed-off-by: Mihai Maruseac <mihaimaruseac@google.com>
  • Loading branch information
mihaimaruseac committed Aug 5, 2024
1 parent 149ba68 commit 459ea1c
Show file tree
Hide file tree
Showing 2 changed files with 128 additions and 0 deletions.
1 change: 1 addition & 0 deletions model_signing/signing/in_toto.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ class IntotoPayload(signing.SigningPayload):
"""

predicate_type: Final[str]
statement: Final[statement.Statement]


class SingleDigestIntotoPayload(IntotoPayload):
Expand Down
127 changes: 127 additions & 0 deletions model_signing/signing/sigstore.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,15 @@
import pathlib
from typing import Self

from google.protobuf import json_format
from sigstore import dsse as sigstore_dsse
from sigstore import models as sigstore_models
from sigstore import oidc as sigstore_oidc
from sigstore import sign as sigstore_signer
from typing_extensions import override

from model_signing.signing import as_bytes
from model_signing.signing import in_toto
from model_signing.signing import signing


Expand Down Expand Up @@ -66,3 +72,124 @@ def read(cls, path: pathlib.Path) -> Self:
"""
content = path.read_text()
return cls(sigstore_models.Bundle.from_json(content))


class SigstoreSigner(signing.Signer):
"""Signing machinery using Sigstore.
We want to sign both digests and in-toto statements, so we provide two
separate subclasses for the signing. This class will just handle the common
parts needed to work with Sigstore.
"""

def __init__(
self,
*,
oidc_issuer: str | None = None,
use_ambient_credentials: bool = True,
use_staging: bool = False,
):
"""Initializes Sigstore signers.
Needs to set-up a signing context to use the public goods instance and
machinery for getting an identity token to use in signing.
Args:
oidc_issuer: An optional OpenID Connect issuer to use instead of the
default production one. Only relevant if `use_staging = False`.
Default is empty, relying on the Sigstore configuration.
use_ambient_credentials: Use ambient credentials (also known as
Workload Identity). Default is True. If ambient credentials cannot
be used (not available, or option disabled), a flow to get signer
identity via OIDC will start.
use_staging: Use staging configurations, instead of production. This
is supposed to be set to True only when testing. Default is False.
"""
if use_staging:
self._signing_context = sigstore_signer.SigningContext.staging()
self._issuer = sigstore_oidc.Issuer.staging()
else:
self._signing_context = sigstore_signer.SigningContext.production()
if oidc_issuer is not None:
self._issuer = sigstore_oidc.Issuer.production(oidc_issuer)
else:
self._issuer = sigstore_oidc.Issuer.production()

self._use_ambient_credentials = use_ambient_credentials

def _get_identity_token(self) -> sigstore_oidc.IdentityToken:
"""Obtains an identity token to use in signing."""
if self._use_ambient_credentials:
token = sigstore_oidc.detect_credential()
if token:
return sigstore_oidc.IdentityToken(token)

return self._issuer.identity_token(force_oob=True)


class SigstoreArtifactSigner(SigstoreSigner):
"""A Sigstore signer that only signs artifacts.
In our case, this instance is only used to sign `as_bytes.BytesPayload`
signing payloads.
"""

@override
def sign(self, payload: signing.SigningPayload) -> SigstoreSignature:
"""Signs the provided signing payload.
Args:
payload: the payload to sign.
Returns:
A `SigstoreSignature` object.
Raises:
TypeError: If the `payload` type is not `as_bytes.BytesPayload`.
"""
if not isinstance(payload, as_bytes.BytesPayload):
raise TypeError("Only `BytesPayload` payloads are supported")

token = self._get_identity_token()
with self._signing_context.signer(token) as signer:
bundle = signer.sign_artifact(payload.digest)

return SigstoreSignature(bundle)


class SigstoreDSSESigner(SigstoreSigner):
"""A Sigstore signer that only signs DSSE statements.
In our case, this instance is only used to sign `in_toto.IntotoPayload`
signing payloads.
"""

@override
def sign(self, payload: signing.SigningPayload) -> SigstoreSignature:
"""Signs the provided signing payload.
Args:
payload: the payload to sign.
Returns:
A `SigstoreSignature` object.
Raises:
TypeError: If the `payload` type is not `as_bytes.BytesPayload`.
"""
if not isinstance(payload, in_toto.IntotoPayload):
raise TypeError("Only `IntotoPayload` payloads are supported")

# We need to convert from in-toto statement to Sigstore's DSSE
# version. They both contain the same contents, but there is no way
# to coerce one type to the other.
# See also: https://github.com/sigstore/sigstore-python/issues/1076
statement = sigstore_dsse.Statement(
json_format.MessageToJson(payload.statement.pb).encode("utf-8")
)

token = self._get_identity_token()
with self._signing_context.signer(token) as signer:
bundle = signer.sign_dsse(statement)

return SigstoreSignature(bundle)

0 comments on commit 459ea1c

Please sign in to comment.