From 459ea1c49e5db5745b1fa2986baa9f8bf79acfac Mon Sep 17 00:00:00 2001 From: Mihai Maruseac Date: Mon, 5 Aug 2024 13:43:05 -0700 Subject: [PATCH] Sign models with Sigstore, generate Sigstore bundles 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 --- model_signing/signing/in_toto.py | 1 + model_signing/signing/sigstore.py | 127 ++++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+) diff --git a/model_signing/signing/in_toto.py b/model_signing/signing/in_toto.py index 055d3e09..5a2c1a09 100644 --- a/model_signing/signing/in_toto.py +++ b/model_signing/signing/in_toto.py @@ -40,6 +40,7 @@ class IntotoPayload(signing.SigningPayload): """ predicate_type: Final[str] + statement: Final[statement.Statement] class SingleDigestIntotoPayload(IntotoPayload): diff --git a/model_signing/signing/sigstore.py b/model_signing/signing/sigstore.py index f30e5343..d9172a27 100644 --- a/model_signing/signing/sigstore.py +++ b/model_signing/signing/sigstore.py @@ -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 @@ -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)