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"""High level API for the signing interface of `model_signing` library.
16
17The module allows signing a model with a default configuration:
18
19```python
20model_signing.signing.sign("finbert", "finbert.sig")
21```
22
23The module allows customizing the signing configuration before signing:
24
25```python
26model_signing.signing.Config().use_elliptic_key_signer(private_key="key").sign(
27 "finbert", "finbert.sig"
28)
29```
30
31The same signing configuration can be used to sign multiple models:
32
33```python
34signing_config = model_signing.signing.Config().use_elliptic_key_signer(
35 private_key="key"
36)
37
38for model in all_models:
39 signing_config.sign(model, f"{model}_sharded.sig")
40```
41
42The API defined here is stable and backwards compatible.
43"""
44
45from collections.abc import Iterable
46import pathlib
47import sys
48from typing import Optional
49
50from model_signing import hashing
51from model_signing._signing import sign_certificate as certificate
52from model_signing._signing import sign_ec_key as ec_key
53from model_signing._signing import sign_sigstore as sigstore
54from model_signing._signing import signing
55
56
57if sys.version_info >= (3, 11):
58 from typing import Self
59else:
60 from typing_extensions import Self
61
62
63def sign(model_path: hashing.PathLike, signature_path: hashing.PathLike):
64 """Signs a model using the default configuration.
65
66 In this default configuration we sign using Sigstore and the default hashing
67 configuration from `model_signing.hashing`.
68
69 The resulting signature is in the Sigstore bundle format.
70
71 Args:
72 model_path: the path to the model to sign.
73 signature_path: the path of the resulting signature.
74 """
75 Config().sign(model_path, signature_path)
76
77
78class Config:
79 """Configuration to use when signing models.
80
81 Currently, we support signing with Sigstore (both the public
82 instance and staging instance), signing with private keys,
83 signing with signing certificates, and signing with custom
84 PKI configurations using the `--trust_config` option.
85 This allows users to bring their own trust configuration
86 to sign and verify models. Other signing modes may be
87 added in the future.
88 """
89
90 def __init__(self):
91 """Initializes the default configuration for signing."""
92 self._hashing_config = hashing.Config()
93 # lazy initialize default signer at signing to avoid network calls
94 self._signer = None
95
96 def sign(
97 self, model_path: hashing.PathLike, signature_path: hashing.PathLike
98 ):
99 """Signs a model using the current configuration.
100
101 Args:
102 model_path: The path to the model to sign.
103 signature_path: The path of the resulting signature.
104 """
105 if not self._signer:
106 self._signer = self.use_sigstore_signer()
107 manifest = self._hashing_config.hash(model_path)
108 payload = signing.Payload(manifest)
109 signature = self._signer.sign(payload)
110 signature.write(pathlib.Path(signature_path))
111
112 def set_hashing_config(self, hashing_config: hashing.Config) -> Self:
113 """Sets the new configuration for hashing models.
114
115 Args:
116 hashing_config: The new hashing configuration.
117
118 Returns:
119 The new signing configuration.
120 """
121 self._hashing_config = hashing_config
122 return self
123
124 def use_sigstore_signer(
125 self,
126 *,
127 oidc_issuer: Optional[str] = None,
128 use_ambient_credentials: bool = False,
129 use_staging: bool = False,
130 force_oob: bool = False,
131 identity_token: Optional[str] = None,
132 client_id: Optional[str] = None,
133 client_secret: Optional[str] = None,
134 trust_config: Optional[pathlib.Path] = None,
135 ) -> Self:
136 """Configures the signing to be performed with Sigstore.
137
138 The signer in this configuration is changed to one that performs signing
139 with Sigstore.
140
141 Args:
142 oidc_issuer: An optional OpenID Connect issuer to use instead of the
143 default production one. Only relevant if `use_staging = False`.
144 Default is empty, relying on the Sigstore configuration.
145 use_ambient_credentials: Use ambient credentials (also known as
146 Workload Identity). Default is False. If ambient credentials
147 cannot be used (not available, or option disabled), a flow to get
148 signer identity via OIDC will start.
149 use_staging: Use staging configurations, instead of production. This
150 is supposed to be set to True only when testing. Default is False.
151 force_oob: If True, forces an out-of-band (OOB) OAuth flow. If set,
152 the OAuth authentication will not attempt to open the default web
153 browser. Instead, it will display a URL and code for manual
154 authentication. Default is False, which means the browser will be
155 opened automatically if possible.
156 identity_token: An explicit identity token to use when signing,
157 taking precedence over any ambient credential or OAuth workflow.
158 client_id: An optional client ID to use when performing OIDC-based
159 authentication. This is typically used to identify the
160 application making the request to the OIDC provider. If not
161 provided, the default client ID configured by Sigstore will be
162 used.
163 client_secret: An optional client secret to use along with the
164 client ID when authenticating with the OIDC provider. This is
165 required for confidential clients that need to prove their
166 identity to the OIDC provider. If not provided, it is assumed
167 that the client is public or the provider does not require a
168 secret.
169 trust_config: A path to a custom trust configuration. When provided,
170 the signature verification process will rely on the supplied
171 PKI and trust configurations, instead of the default Sigstore
172 setup. If not specified, the default Sigstore configuration
173 is used.
174
175 Return:
176 The new signing configuration.
177 """
178 self._signer = sigstore.Signer(
179 oidc_issuer=oidc_issuer,
180 use_ambient_credentials=use_ambient_credentials,
181 use_staging=use_staging,
182 identity_token=identity_token,
183 force_oob=force_oob,
184 client_id=client_id,
185 client_secret=client_secret,
186 trust_config=trust_config,
187 )
188 return self
189
190 def use_elliptic_key_signer(
191 self, *, private_key: hashing.PathLike, password: Optional[str] = None
192 ) -> Self:
193 """Configures the signing to be performed using elliptic curve keys.
194
195 The signer in this configuration is changed to one that performs signing
196 using a private key based on elliptic curve cryptography.
197
198 Args:
199 private_key: The path to the private key to use for signing.
200 password: An optional password for the key, if encrypted.
201
202 Return:
203 The new signing configuration.
204 """
205 self._signer = ec_key.Signer(pathlib.Path(private_key), password)
206 return self
207
208 def use_certificate_signer(
209 self,
210 *,
211 private_key: hashing.PathLike,
212 signing_certificate: hashing.PathLike,
213 certificate_chain: Iterable[hashing.PathLike],
214 ) -> Self:
215 """Configures the signing to be performed using signing certificates.
216
217 The signer in this configuration is changed to one that performs signing
218 using cryptographic signing certificates.
219
220 Args:
221 private_key: The path to the private key to use for signing.
222 signing_certificate: The path to the signing certificate.
223 certificate_chain: Optional paths to other certificates to establish
224 a chain of trust.
225
226 Return:
227 The new signing configuration.
228 """
229 self._signer = certificate.Signer(
230 pathlib.Path(private_key),
231 pathlib.Path(signing_certificate),
232 [pathlib.Path(c) for c in certificate_chain],
233 )
234 return self
235
236 def use_pkcs11_signer(
237 self, *, pkcs11_uri: str, module_paths: Iterable[str] = frozenset()
238 ) -> Self:
239 """Configures the signing to be performed using PKCS #11.
240
241 The signer in this configuration is changed to one that performs signing
242 using a private key based on elliptic curve cryptography.
243
244 Args:
245 pkcs11_uri: The PKCS11 URI.
246 module_paths: Optional list of paths of PKCS #11 modules.
247
248 Return:
249 The new signing configuration.
250 """
251 try:
252 from model_signing._signing import sign_pkcs11 as pkcs11
253 except ImportError as e:
254 raise RuntimeError(
255 "PKCS #11 functionality requires the 'pkcs11' extra. "
256 "Install with 'pip install model-signing[pkcs11]'."
257 ) from e
258 self._signer = pkcs11.Signer(pkcs11_uri, module_paths)
259 return self
260
261 def use_pkcs11_certificate_signer(
262 self,
263 *,
264 pkcs11_uri: str,
265 signing_certificate: pathlib.Path,
266 certificate_chain: Iterable[pathlib.Path],
267 module_paths: Iterable[str] = frozenset(),
268 ) -> Self:
269 """Configures the signing to be performed using signing certificates.
270
271 The signer in this configuration is changed to one that performs signing
272 using cryptographic certificates.
273
274 Args:
275 pkcs11_uri: The PKCS #11 URI.
276 signing_certificate: The path to the signing certificate.
277 certificate_chain: Optional paths to other certificates to establish
278 a chain of trust.
279 module_paths: Optional list of paths of PKCS #11 modules.
280
281 Return:
282 The new signing configuration.
283 """
284 try:
285 from model_signing._signing import sign_pkcs11 as pkcs11
286 except ImportError as e:
287 raise RuntimeError(
288 "PKCS #11 functionality requires the 'pkcs11' extra. "
289 "Install with 'pip install model-signing[pkcs11]'."
290 ) from e
291
292 self._signer = pkcs11.CertSigner(
293 pkcs11_uri,
294 signing_certificate,
295 certificate_chain,
296 module_paths=module_paths,
297 )
298 return self