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