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