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