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
48
49from model_signing import hashing
50from model_signing._signing import sign_certificate as certificate
51from model_signing._signing import sign_ec_key as ec_key
52from model_signing._signing import sign_sigstore as sigstore
53from model_signing._signing import signing
54
55
56if sys.version_info >= (3, 11):
57 from typing import Self
58else:
59 from typing_extensions import Self
60
61
62def sign(model_path: hashing.PathLike, signature_path: hashing.PathLike):
63 """Signs a model using the default configuration.
64
65 In this default configuration we sign using Sigstore and the default hashing
66 configuration from `model_signing.hashing`.
67
68 The resulting signature is in the Sigstore bundle format.
69
70 Args:
71 model_path: the path to the model to sign.
72 signature_path: the path of the resulting signature.
73 """
74 Config().sign(model_path, signature_path)
75
76
77def sign_to_bytes(model_path: hashing.PathLike) -> bytes:
78 """Signs a model using the default configuration and returns the signature.
79
80 In this default configuration we sign using Sigstore and the default hashing
81 configuration from `model_signing.hashing`.
82
83 The resulting signature is the Sigstore bundle, returned in memory as UTF-8
84 encoded JSON bytes instead of being written to disk.
85
86 Args:
87 model_path: the path to the model to sign.
88
89 Returns:
90 The signature, as a Sigstore bundle encoded in UTF-8 JSON bytes.
91 """
92 return Config().sign_to_bytes(model_path)
93
94
95class Config:
96 """Configuration to use when signing models.
97
98 Currently, we support signing with Sigstore (both the public
99 instance and staging instance), signing with private keys,
100 signing with signing certificates, and signing with custom
101 PKI configurations using the `--trust_config` option.
102 This allows users to bring their own trust configuration
103 to sign and verify models. Other signing modes may be
104 added in the future.
105 """
106
107 def __init__(self):
108 """Initializes the default configuration for signing."""
109 self._hashing_config = hashing.Config()
110 # lazy initialize default signer at signing to avoid network calls
111 self._signer = None
112
113 def sign(
114 self, model_path: hashing.PathLike, signature_path: hashing.PathLike
115 ):
116 """Signs a model using the current configuration.
117
118 Args:
119 model_path: The path to the model to sign.
120 signature_path: The path of the resulting signature.
121 """
122 signature = self._sign(model_path)
123 signature.write(pathlib.Path(signature_path))
124
125 def sign_to_bytes(self, model_path: hashing.PathLike) -> bytes:
126 """Signs a model and returns the signature as bytes.
127
128 This mirrors `sign`, but instead of writing the signature to disk it
129 returns the Sigstore bundle in memory. This is useful in serverless or
130 pipeline contexts where writing to the filesystem is undesirable or
131 impossible, and the bundle needs to be streamed, persisted to an object
132 store, or passed directly to another process.
133
134 The returned bytes are the UTF-8 encoded Sigstore bundle, identical to
135 what `sign` would have written to the signature path.
136
137 Args:
138 model_path: The path to the model to sign.
139
140 Returns:
141 The signature, as a Sigstore bundle encoded in UTF-8 JSON bytes.
142 """
143 return self._sign(model_path).to_bytes()
144
145 def _sign(self, model_path: hashing.PathLike) -> signing.Signature:
146 """Hashes and signs a model, returning the in-memory signature.
147
148 Args:
149 model_path: The path to the model to sign.
150
151 Returns:
152 The `Signature` produced by the configured signer.
153 """
154 if self._signer is None:
155 self.use_sigstore_signer()
156 manifest = self._hashing_config.hash(model_path)
157 payload = signing.Payload(manifest)
158 return self._signer.sign(payload)
159
160 def set_hashing_config(self, hashing_config: hashing.Config) -> Self:
161 """Sets the new configuration for hashing models.
162
163 Args:
164 hashing_config: The new hashing configuration.
165
166 Returns:
167 The new signing configuration.
168 """
169 self._hashing_config = hashing_config
170 return self
171
172 def use_sigstore_signer(
173 self,
174 *,
175 oidc_issuer: str | None = None,
176 use_ambient_credentials: bool = False,
177 use_staging: bool = False,
178 force_oob: bool = False,
179 identity_token: str | None = None,
180 client_id: str | None = None,
181 client_secret: str | None = None,
182 trust_config: pathlib.Path | None = None,
183 ) -> Self:
184 """Configures the signing to be performed with Sigstore.
185
186 The signer in this configuration is changed to one that performs signing
187 with Sigstore.
188
189 Args:
190 oidc_issuer: An optional OpenID Connect issuer to use instead of the
191 default production one. Only relevant if `use_staging = False`.
192 Default is empty, relying on the Sigstore configuration.
193 use_ambient_credentials: Use ambient credentials (also known as
194 Workload Identity). Default is False. If ambient credentials
195 cannot be used (not available, or option disabled), a flow to get
196 signer identity via OIDC will start.
197 use_staging: Use staging configurations, instead of production. This
198 is supposed to be set to True only when testing. Default is False.
199 force_oob: If True, forces an out-of-band (OOB) OAuth flow. If set,
200 the OAuth authentication will not attempt to open the default web
201 browser. Instead, it will display a URL and code for manual
202 authentication. Default is False, which means the browser will be
203 opened automatically if possible.
204 identity_token: An explicit identity token to use when signing,
205 taking precedence over any ambient credential or OAuth workflow.
206 client_id: An optional client ID to use when performing OIDC-based
207 authentication. This is typically used to identify the
208 application making the request to the OIDC provider. If not
209 provided, the default client ID configured by Sigstore will be
210 used.
211 client_secret: An optional client secret to use along with the
212 client ID when authenticating with the OIDC provider. This is
213 required for confidential clients that need to prove their
214 identity to the OIDC provider. If not provided, it is assumed
215 that the client is public or the provider does not require a
216 secret.
217 trust_config: A path to a custom trust configuration. When provided,
218 the signature verification process will rely on the supplied
219 PKI and trust configurations, instead of the default Sigstore
220 setup. If not specified, the default Sigstore configuration
221 is used.
222
223 Return:
224 The new signing configuration.
225 """
226 self._signer = sigstore.Signer(
227 oidc_issuer=oidc_issuer,
228 use_ambient_credentials=use_ambient_credentials,
229 use_staging=use_staging,
230 identity_token=identity_token,
231 force_oob=force_oob,
232 client_id=client_id,
233 client_secret=client_secret,
234 trust_config=trust_config,
235 )
236 return self
237
238 def use_elliptic_key_signer(
239 self, *, private_key: hashing.PathLike, password: str | None = None
240 ) -> Self:
241 """Configures the signing to be performed using elliptic curve keys.
242
243 The signer in this configuration is changed to one that performs signing
244 using a private key based on elliptic curve cryptography.
245
246 Args:
247 private_key: The path to the private key to use for signing.
248 password: An optional password for the key, if encrypted.
249
250 Return:
251 The new signing configuration.
252 """
253 self._signer = ec_key.Signer(pathlib.Path(private_key), password)
254 return self
255
256 def use_certificate_signer(
257 self,
258 *,
259 private_key: hashing.PathLike,
260 signing_certificate: hashing.PathLike,
261 certificate_chain: Iterable[hashing.PathLike],
262 ) -> Self:
263 """Configures the signing to be performed using signing certificates.
264
265 The signer in this configuration is changed to one that performs signing
266 using cryptographic signing certificates.
267
268 Args:
269 private_key: The path to the private key to use for signing.
270 signing_certificate: The path to the signing certificate.
271 certificate_chain: Optional paths to other certificates to establish
272 a chain of trust.
273
274 Return:
275 The new signing configuration.
276 """
277 self._signer = certificate.Signer(
278 pathlib.Path(private_key),
279 pathlib.Path(signing_certificate),
280 [pathlib.Path(c) for c in certificate_chain],
281 )
282 return self
283
284 def use_pkcs11_signer(
285 self, *, pkcs11_uri: str, module_paths: Iterable[str] = frozenset()
286 ) -> Self:
287 """Configures the signing to be performed using PKCS #11.
288
289 The signer in this configuration is changed to one that performs signing
290 using a private key based on elliptic curve cryptography.
291
292 Args:
293 pkcs11_uri: The PKCS11 URI.
294 module_paths: Optional list of paths of PKCS #11 modules.
295
296 Return:
297 The new signing configuration.
298 """
299 try:
300 from model_signing._signing import sign_pkcs11 as pkcs11
301 except ImportError as e:
302 raise RuntimeError(
303 "PKCS #11 functionality requires the 'pkcs11' extra. "
304 "Install with 'pip install model-signing[pkcs11]'."
305 ) from e
306 self._signer = pkcs11.Signer(pkcs11_uri, module_paths)
307 return self
308
309 def use_pkcs11_certificate_signer(
310 self,
311 *,
312 pkcs11_uri: str,
313 signing_certificate: pathlib.Path,
314 certificate_chain: Iterable[pathlib.Path],
315 module_paths: Iterable[str] = frozenset(),
316 ) -> Self:
317 """Configures the signing to be performed using signing certificates.
318
319 The signer in this configuration is changed to one that performs signing
320 using cryptographic certificates.
321
322 Args:
323 pkcs11_uri: The PKCS #11 URI.
324 signing_certificate: The path to the signing certificate.
325 certificate_chain: Optional paths to other certificates to establish
326 a chain of trust.
327 module_paths: Optional list of paths of PKCS #11 modules.
328
329 Return:
330 The new signing configuration.
331 """
332 try:
333 from model_signing._signing import sign_pkcs11 as pkcs11
334 except ImportError as e:
335 raise RuntimeError(
336 "PKCS #11 functionality requires the 'pkcs11' extra. "
337 "Install with 'pip install model-signing[pkcs11]'."
338 ) from e
339
340 self._signer = pkcs11.CertSigner(
341 pkcs11_uri,
342 signing_certificate,
343 certificate_chain,
344 module_paths=module_paths,
345 )
346 return self