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(), encoding="utf-8")
55
56 @classmethod
57 @override
58 def read(cls, path: pathlib.Path) -> Self:
59 content = path.read_text(encoding="utf-8")
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 trust_config: Optional[pathlib.Path] = None,
77 ):
78 """Initializes Sigstore signers.
79
80 Needs to set-up a signing context to use the public goods instance and
81 machinery for getting an identity token to use in signing.
82
83 Args:
84 oidc_issuer: An optional OpenID Connect issuer to use instead of the
85 default production one. Only relevant if `use_staging = False`.
86 Default is empty, relying on the Sigstore configuration.
87 use_ambient_credentials: Use ambient credentials (also known as
88 Workload Identity). Default is True. If ambient credentials cannot
89 be used (not available, or option disabled), a flow to get signer
90 identity via OIDC will start.
91 use_staging: Use staging configurations, instead of production. This
92 is supposed to be set to True only when testing. Default is False.
93 force_oob: If True, forces an out-of-band (OOB) OAuth flow. If set,
94 the OAuth authentication will not attempt to open the default web
95 browser. Instead, it will display a URL and code for manual
96 authentication. Default is False, which means the browser will be
97 opened automatically if possible.
98 identity_token: An explicit identity token to use when signing,
99 taking precedence over any ambient credential or OAuth workflow.
100 client_id: An optional client ID to use when performing OIDC-based
101 authentication. This is typically used to identify the
102 application making the request to the OIDC provider. If not
103 provided, the default client ID configured by Sigstore will be
104 used.
105 client_secret: An optional client secret to use along with the
106 client ID when authenticating with the OIDC provider. This is
107 required for confidential clients that need to prove their
108 identity to the OIDC provider. If not provided, it is assumed
109 that the client is public or the provider does not require a
110 secret.
111 trust_config: A path to a custom trust configuration. When
112 provided, the signature verification process will rely on the
113 supplied PKI and trust configurations, instead of the default
114 Sigstore setup. If not specified, the default Sigstore
115 configuration is used.
116 """
117 if use_staging:
118 trust_config = sigstore_models.ClientTrustConfig.staging()
119 elif trust_config:
120 trust_config = sigstore_models.ClientTrustConfig.from_json(
121 trust_config.read_text()
122 )
123 else:
124 trust_config = sigstore_models.ClientTrustConfig.production()
125
126 if not oidc_issuer:
127 oidc_issuer = trust_config.signing_config.get_oidc_url()
128
129 self._issuer = sigstore_oidc.Issuer(oidc_issuer)
130 self._signing_context = (
131 sigstore_signer.SigningContext.from_trust_config(trust_config)
132 )
133 self._use_ambient_credentials = use_ambient_credentials
134 self._identity_token = identity_token
135 self._force_oob = force_oob
136 self._client_id = client_id or _DEFAULT_CLIENT_ID
137 self._client_secret = client_secret or _DEFAULT_CLIENT_SECRET
138
139 def _get_identity_token(self) -> sigstore_oidc.IdentityToken:
140 """Obtains an identity token to use in signing.
141
142 The precedence matches that of sigstore-python:
143 1) Explicitly supplied identity token
144 2) Ambient credential detected in the environment, if enabled
145 3) Interactive OAuth flow
146 """
147 if self._identity_token:
148 return sigstore_oidc.IdentityToken(
149 self._identity_token, self._client_id
150 )
151 if self._use_ambient_credentials:
152 token = sigstore_oidc.detect_credential(self._client_id)
153 if token:
154 return sigstore_oidc.IdentityToken(token, self._client_id)
155
156 return self._issuer.identity_token(
157 force_oob=self._force_oob,
158 client_id=self._client_id,
159 client_secret=self._client_secret,
160 )
161
162 @override
163 def sign(self, payload: signing.Payload) -> Signature:
164 # We need to convert from in-toto statement to Sigstore's DSSE
165 # version. They both contain the same contents, but there is no way
166 # to coerce one type to the other.
167 # See also: https://github.com/sigstore/sigstore-python/issues/1076
168 statement = sigstore_dsse.Statement(
169 json_format.MessageToJson(payload.statement.pb).encode("utf-8")
170 )
171
172 token = self._get_identity_token()
173 with self._signing_context.signer(token) as signer:
174 bundle = signer.sign_dsse(statement)
175
176 return Signature(bundle)
177
178
179class Verifier(signing.Verifier):
180 """Signature verification using Sigstore."""
181
182 def __init__(
183 self,
184 *,
185 identity: str,
186 oidc_issuer: str,
187 use_staging: bool = False,
188 trust_config: Optional[pathlib.Path] = None,
189 ):
190 """Initializes Sigstore verifiers.
191
192 When verifying a signature, we also check an identity policy: the
193 certificate must belong to a given "identity", and must be issued by a
194 given OpenID Connect issuer.
195
196 Args:
197 identity: The expected identity that has signed the model.
198 oidc_issuer: The expected OpenID Connect issuer that provided the
199 certificate used for the signature.
200 use_staging: Use staging configurations, instead of production. This
201 is supposed to be set to True only when testing. Default is False.
202 trust_config: A path to a custom trust configuration. When provided,
203 the signature verification process will rely on the supplied
204 PKI and trust configurations, instead of the default Sigstore
205 setup. If not specified, the default Sigstore configuration
206 is used.
207 """
208 if trust_config:
209 trust_config = sigstore_models.ClientTrustConfig.from_json(
210 trust_config.read_text()
211 )
212 elif use_staging:
213 trust_config = sigstore_models.ClientTrustConfig.staging()
214 else:
215 trust_config = sigstore_models.ClientTrustConfig.production()
216
217 self._verifier = sigstore_verifier.Verifier(
218 trusted_root=trust_config.trusted_root
219 )
220
221 self._policy = sigstore_verifier.policy.Identity(
222 identity=identity, issuer=oidc_issuer
223 )
224
225 @override
226 def _verify_signed_content(
227 self, signature: signing.Signature
228 ) -> tuple[str, bytes]:
229 # We are guaranteed to only use the local signature type
230 signature = cast(Signature, signature)
231 bundle = signature.bundle
232 return self._verifier.verify_dsse(bundle=bundle, policy=self._policy)