1# Copyright 2024 The Sigstore Authors
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Sigstore based signature, signers and verifiers."""
16
17import pathlib
18import sys
19from typing import Optional, cast
20
21from google.protobuf import json_format
22from sigstore import dsse as sigstore_dsse
23from sigstore import models as sigstore_models
24from sigstore import oidc as sigstore_oidc
25from sigstore import sign as sigstore_signer
26from sigstore import verify as sigstore_verifier
27from typing_extensions import override
28
29from model_signing._signing import signing
30
31
32if sys.version_info >= (3, 11):
33 from typing import Self
34else:
35 from typing_extensions import Self
36
37_DEFAULT_CLIENT_ID = "sigstore"
38_DEFAULT_CLIENT_SECRET = ""
39
40
41class Signature(signing.Signature):
42 """Sigstore signature support, wrapping around `sigstore_models.Bundle`."""
43
44 def __init__(self, bundle: sigstore_models.Bundle):
45 """Builds an instance of this signature.
46
47 Args:
48 bundle: the sigstore bundle (in `bundle_pb.Bundle` format).
49 """
50 self.bundle = bundle
51
52 @override
53 def write(self, path: pathlib.Path) -> None:
54 path.write_text(self.bundle.to_json())
55
56 @classmethod
57 @override
58 def read(cls, path: pathlib.Path) -> Self:
59 content = path.read_text()
60 return cls(sigstore_models.Bundle.from_json(content))
61
62
63class Signer(signing.Signer):
64 """Signing using Sigstore."""
65
66 def __init__(
67 self,
68 *,
69 oidc_issuer: Optional[str] = None,
70 use_ambient_credentials: bool = True,
71 use_staging: bool = False,
72 identity_token: Optional[str] = None,
73 force_oob: bool = False,
74 client_id: Optional[str] = None,
75 client_secret: Optional[str] = None,
76 ):
77 """Initializes Sigstore signers.
78
79 Needs to set-up a signing context to use the public goods instance and
80 machinery for getting an identity token to use in signing.
81
82 Args:
83 oidc_issuer: An optional OpenID Connect issuer to use instead of the
84 default production one. Only relevant if `use_staging = False`.
85 Default is empty, relying on the Sigstore configuration.
86 use_ambient_credentials: Use ambient credentials (also known as
87 Workload Identity). Default is True. If ambient credentials cannot
88 be used (not available, or option disabled), a flow to get signer
89 identity via OIDC will start.
90 use_staging: Use staging configurations, instead of production. This
91 is supposed to be set to True only when testing. Default is False.
92 force_oob: If True, forces an out-of-band (OOB) OAuth flow. If set,
93 the OAuth authentication will not attempt to open the default web
94 browser. Instead, it will display a URL and code for manual
95 authentication. Default is False, which means the browser will be
96 opened automatically if possible.
97 identity_token: An explicit identity token to use when signing,
98 taking precedence over any ambient credential or OAuth workflow.
99 client_id: An optional client ID to use when performing OIDC-based
100 authentication. This is typically used to identify the
101 application making the request to the OIDC provider. If not
102 provided, the default client ID configured by Sigstore will be
103 used.
104 client_secret: An optional client secret to use along with the
105 client ID when authenticating with the OIDC provider. This is
106 required for confidential clients that need to prove their
107 identity to the OIDC provider. If not provided, it is assumed
108 that the client is public or the provider does not require a
109 secret.
110 """
111 if use_staging:
112 self._signing_context = sigstore_signer.SigningContext.staging()
113 self._issuer = sigstore_oidc.Issuer.staging()
114 else:
115 self._signing_context = sigstore_signer.SigningContext.production()
116 if oidc_issuer is not None:
117 self._issuer = sigstore_oidc.Issuer(oidc_issuer)
118 else:
119 self._issuer = sigstore_oidc.Issuer.production()
120
121 self._use_ambient_credentials = use_ambient_credentials
122 self._identity_token = identity_token
123 self._force_oob = force_oob
124 self._client_id = client_id or _DEFAULT_CLIENT_ID
125 self._client_secret = client_secret or _DEFAULT_CLIENT_SECRET
126
127 def _get_identity_token(self) -> sigstore_oidc.IdentityToken:
128 """Obtains an identity token to use in signing.
129
130 The precedence matches that of sigstore-python:
131 1) Explicitly supplied identity token
132 2) Ambient credential detected in the environment, if enabled
133 3) Interactive OAuth flow
134 """
135 if self._identity_token:
136 return sigstore_oidc.IdentityToken(self._identity_token)
137 if self._use_ambient_credentials:
138 token = sigstore_oidc.detect_credential()
139 if token:
140 return sigstore_oidc.IdentityToken(token)
141
142 return self._issuer.identity_token(
143 force_oob=self._force_oob,
144 client_id=self._client_id,
145 client_secret=self._client_secret,
146 )
147
148 @override
149 def sign(self, payload: signing.Payload) -> Signature:
150 # We need to convert from in-toto statement to Sigstore's DSSE
151 # version. They both contain the same contents, but there is no way
152 # to coerce one type to the other.
153 # See also: https://github.com/sigstore/sigstore-python/issues/1076
154 statement = sigstore_dsse.Statement(
155 json_format.MessageToJson(payload.statement.pb).encode("utf-8")
156 )
157
158 token = self._get_identity_token()
159 with self._signing_context.signer(token) as signer:
160 bundle = signer.sign_dsse(statement)
161
162 return Signature(bundle)
163
164
165class Verifier(signing.Verifier):
166 """Signature verification using Sigstore."""
167
168 def __init__(
169 self, *, identity: str, oidc_issuer: str, use_staging: bool = False
170 ):
171 """Initializes Sigstore verifiers.
172
173 When verifying a signature, we also check an identity policy: the
174 certificate must belong to a given "identity", and must be issued by a
175 given OpenID Connect issuer.
176
177 Args:
178 identity: The expected identity that has signed the model.
179 oidc_issuer: The expected OpenID Connect issuer that provided the
180 certificate used for the signature.
181 use_staging: Use staging configurations, instead of production. This
182 is supposed to be set to True only when testing. Default is False.
183 """
184 if use_staging:
185 self._verifier = sigstore_verifier.Verifier.staging()
186 else:
187 self._verifier = sigstore_verifier.Verifier.production()
188
189 self._policy = sigstore_verifier.policy.Identity(
190 identity=identity, issuer=oidc_issuer
191 )
192
193 @override
194 def _verify_signed_content(
195 self, signature: signing.Signature
196 ) -> tuple[str, bytes]:
197 # We are guaranteed to only use the local signature type
198 signature = cast(Signature, signature)
199 bundle = signature.bundle
200 return self._verifier.verify_dsse(bundle=bundle, policy=self._policy)