Skip to content

Commit c48e25d

Browse files
Make the API fully configurable (#416)
* Make the library API fully configurable Signed-off-by: Mihai Maruseac <[email protected]> * Remove `verify`, document exception. Signed-off-by: Mihai Maruseac <[email protected]> --------- Signed-off-by: Mihai Maruseac <[email protected]>
1 parent 2c9652a commit c48e25d

File tree

4 files changed

+116
-85
lines changed

4 files changed

+116
-85
lines changed

src/model_signing/hashing.py

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -90,9 +90,7 @@ def __init__(self):
9090
symbolic link in the model directory results in an error.
9191
"""
9292
self._ignored_paths = frozenset()
93-
self._serializer = file.Serializer(
94-
self._build_file_hasher_factory(), allow_symlinks=False
95-
)
93+
self.use_file_serialization()
9694

9795
def hash(self, model_path: os.PathLike) -> manifest.Manifest:
9896
"""Hashes a model using the current configuration."""
@@ -137,11 +135,11 @@ def _build_file_hasher_factory(
137135
method.
138136
"""
139137

140-
def factory(path: pathlib.Path) -> io.SimpleFileHasher:
138+
def _factory(path: pathlib.Path) -> io.SimpleFileHasher:
141139
hasher = self._build_stream_hasher(hashing_algorithm)
142140
return io.SimpleFileHasher(path, hasher, chunk_size=chunk_size)
143141

144-
return factory
142+
return _factory
145143

146144
def _build_sharded_file_hasher_factory(
147145
self,
@@ -162,23 +160,23 @@ def _build_sharded_file_hasher_factory(
162160
The hasher factory that should be used by the active serialization
163161
method.
164162
"""
165-
algorithm = self._build_stream_hasher(hashing_algorithm)
166163

167-
def factory(
164+
def _factory(
168165
path: pathlib.Path, start: int, end: int
169166
) -> io.ShardedFileHasher:
167+
hasher = self._build_stream_hasher(hashing_algorithm)
170168
return io.ShardedFileHasher(
171169
path,
172-
algorithm,
170+
hasher,
173171
start=start,
174172
end=end,
175173
chunk_size=chunk_size,
176174
shard_size=shard_size,
177175
)
178176

179-
return factory
177+
return _factory
180178

181-
def set_serialize_by_file_to_manifest(
179+
def use_file_serialization(
182180
self,
183181
*,
184182
hashing_algorithm: Literal["sha256", "blake2"] = "sha256",
@@ -213,7 +211,7 @@ def set_serialize_by_file_to_manifest(
213211
)
214212
return self
215213

216-
def set_serialize_by_file_shard_to_manifest(
214+
def use_shard_serialization(
217215
self,
218216
*,
219217
hashing_algorithm: Literal["sha256", "blake2"] = "sha256",

src/model_signing/signing.py

Lines changed: 52 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,15 @@
1919
`hashing.py`, `signing.py` and `verifying.py` at the root level of the library.
2020
"""
2121

22-
from collections.abc import Callable
22+
from collections.abc import Iterable
2323
import os
2424
import pathlib
2525
import sys
2626
from typing import Optional
2727

2828
from model_signing import hashing
29-
from model_signing import manifest
29+
from model_signing._signing import sign_certificate as certificate
30+
from model_signing._signing import sign_ec_key as ec_key
3031
from model_signing._signing import sign_sigstore as sigstore
3132
from model_signing._signing import signing
3233

@@ -60,10 +61,7 @@ class Config:
6061
def __init__(self):
6162
"""Initializes the default configuration for signing."""
6263
self._hashing_config = hashing.Config()
63-
self._payload_generator = signing.Payload
64-
self._signer = sigstore.Signer(
65-
use_ambient_credentials=False, use_staging=False
66-
)
64+
self.use_sigstore_signer()
6765

6866
def sign(self, model_path: os.PathLike, signature_path: os.PathLike):
6967
"""Signs a model using the current configuration.
@@ -73,7 +71,7 @@ def sign(self, model_path: os.PathLike, signature_path: os.PathLike):
7371
signature_path: the path of the resulting signature.
7472
"""
7573
manifest = self._hashing_config.hash(model_path)
76-
payload = self._payload_generator(manifest)
74+
payload = signing.Payload(manifest)
7775
signature = self._signer.sign(payload)
7876
signature.write(pathlib.Path(signature_path))
7977

@@ -89,25 +87,7 @@ def set_hashing_config(self, hashing_config: hashing.Config) -> Self:
8987
self._hashing_config = hashing_config
9088
return self
9189

92-
def set_payload_generator(
93-
self, generator: Callable[[manifest.Manifest], signing.Payload]
94-
) -> Self:
95-
"""Sets the conversion from manifest to signing payload.
96-
97-
Since we want to support multiple serialization formats and multiple
98-
signing solutions, we use a payload generator to relax the coupling
99-
between the two.
100-
101-
Args:
102-
generator: the conversion from a manifest to a signing payload.
103-
104-
Return:
105-
The new signing configuration.
106-
"""
107-
self._payload_generator = generator
108-
return self
109-
110-
def set_sigstore_signer(
90+
def use_sigstore_signer(
11191
self,
11292
*,
11393
oidc_issuer: Optional[str] = None,
@@ -117,9 +97,8 @@ def set_sigstore_signer(
11797
) -> Self:
11898
"""Configures the signing to be performed with Sigstore.
11999
120-
Only one signer can be configured. Currently, we only support Sigstore
121-
in the API, but the CLI supports signing with PKI, BYOK and no signing.
122-
We will merge the configurations in a subsequent change.
100+
The signer in this configuration is changed to one that performs signing
101+
with Sigstore, as configured.
123102
124103
Args:
125104
oidc_issuer: An optional OpenID Connect issuer to use instead of the
@@ -144,3 +123,47 @@ def set_sigstore_signer(
144123
identity_token=identity_token,
145124
)
146125
return self
126+
127+
def use_elliptic_key_signer(
128+
self, *, private_key: pathlib.Path, password: Optional[str] = None
129+
) -> Self:
130+
"""Configures the signing to be performed using elliptic curve keys.
131+
132+
The signer in this configuration is changed to one that performs signing
133+
using a private key based on elliptic curve cryptography.
134+
135+
Args:
136+
private_key: The path to the private key to use for signing.
137+
password: An optional password for the key, if encrypted.
138+
139+
Return:
140+
The new signing configuration.
141+
"""
142+
self._signer = ec_key.Signer(private_key, password)
143+
return self
144+
145+
def use_certificate_signer(
146+
self,
147+
*,
148+
private_key: pathlib.Path,
149+
signing_certificate: pathlib.Path,
150+
certificate_chain: Iterable[pathlib.Path],
151+
) -> Self:
152+
"""Configures the signing to be performed using signing certificates.
153+
154+
The signer in this configuration is changed to one that performs signing
155+
using cryptographic certificates.
156+
157+
Args:
158+
private_key: The path to the private key to use for signing.
159+
signing_certificate: The path to the signing certificate.
160+
certificate_chain: Optional paths to other certificates to establish
161+
a chain of trust.
162+
163+
Return:
164+
The new signing configuration.
165+
"""
166+
self._signer = certificate.Signer(
167+
private_key, signing_certificate, certificate_chain
168+
)
169+
return self

src/model_signing/verifying.py

Lines changed: 50 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,14 @@
2020
of the library.
2121
"""
2222

23+
from collections.abc import Iterable
2324
import os
2425
import pathlib
2526
import sys
26-
from typing import Optional
2727

2828
from model_signing import hashing
29+
from model_signing._signing import sign_certificate as certificate
30+
from model_signing._signing import sign_ec_key as ec_key
2931
from model_signing._signing import sign_sigstore as sigstore
3032

3133

@@ -35,33 +37,6 @@
3537
from typing_extensions import Self
3638

3739

38-
def verify(
39-
model_path: os.PathLike,
40-
signature_path: os.PathLike,
41-
*,
42-
identity: str,
43-
oidc_issuer: Optional[str] = None,
44-
use_staging: bool = False,
45-
):
46-
"""Verifies that a model conforms to a signature.
47-
48-
Currently, this assumes signatures over DSSE, using Sigstore. We will add
49-
support for more cases in a future change.
50-
51-
Args:
52-
model_path: the path to the model to verify.
53-
signature_path: the path to the signature to check.
54-
identity: The expected identity that has signed the model.
55-
oidc_issuer: The expected OpenID Connect issuer that provided the
56-
certificate used for the signature.
57-
use_staging: Use staging configurations, instead of production. This
58-
is supposed to be set to True only when testing. Default is False.
59-
"""
60-
Config().set_sigstore_dsse_verifier(
61-
identity=identity, oidc_issuer=oidc_issuer, use_staging=use_staging
62-
).verify(model_path, signature_path)
63-
64-
6540
class Config:
6641
"""Configuration to use when verifying models against signatures.
6742
@@ -81,8 +56,13 @@ def verify(self, model_path: os.PathLike, signature_path: os.PathLike):
8156
8257
Args:
8358
model_path: the path to the model to verify.
84-
signature_path: the path to the signature to check.
59+
60+
Raises:
61+
ValueError: No verifier has been configured.
8562
"""
63+
if self._verifier is None:
64+
raise ValueError("Attempting to verify with no configured verifier")
65+
8666
signature = sigstore.Signature.read(pathlib.Path(signature_path))
8767
expected_manifest = self._verifier.verify(signature)
8868
actual_manifest = self._hashing_config.hash(model_path)
@@ -102,19 +82,14 @@ def set_hashing_config(self, hashing_config: hashing.Config) -> Self:
10282
self._hashing_config = hashing_config
10383
return self
10484

105-
def set_sigstore_dsse_verifier(
106-
self,
107-
*,
108-
identity: str,
109-
oidc_issuer: Optional[str] = None,
110-
use_staging: bool = False,
85+
def use_sigstore_verifier(
86+
self, *, identity: str, oidc_issuer: str, use_staging: bool = False
11187
) -> Self:
112-
"""Configures the verification of a Sigstore signature over DSSE.
88+
"""Configures the verification of signatures produced by Sigstore.
11389
114-
Only one verifier can be configured. Currently, we only support Sigstore
115-
in the API, but the CLI supports signing with PKI, BYOK and no
116-
signing/verification. We will merge the configurations in a subsequent
117-
change.
90+
The verifier in this configuration is changed to one that performs
91+
verification of Sigstore signatures (sigstore bundles signed by
92+
keyless signing via Sigstore).
11893
11994
Args:
12095
identity: The expected identity that has signed the model.
@@ -130,3 +105,38 @@ def set_sigstore_dsse_verifier(
130105
identity=identity, oidc_issuer=oidc_issuer, use_staging=use_staging
131106
)
132107
return self
108+
109+
def use_elliptic_key_verifier(self, *, public_key: pathlib.Path) -> Self:
110+
"""Configures the verification of signatures generated by a private key.
111+
112+
The verifier in this configuration is changed to one that performs
113+
verification of sgistore bundles signed by an elliptic curve private
114+
key. The public key used in the configuration must match the private key
115+
used during signing.
116+
117+
Args:
118+
public_key: The path to the public key to verify with.
119+
120+
Return:
121+
The new verification configuration.
122+
"""
123+
self._verifier = ec_key.Verifier(public_key)
124+
return self
125+
126+
def use_certificate_verifier(
127+
self, *, certificate_chain: Iterable[pathlib.Path] = frozenset()
128+
) -> Self:
129+
"""Configures the verification of signatures generated by a certificate.
130+
131+
The verifier in this configuration is changed to one that performs
132+
verification of sgistore bundles signed by a signing certificate.
133+
134+
Args:
135+
certificate_chain: Certificate chain to establish root of trust. If
136+
empty, the operating system's one is used.
137+
138+
Return:
139+
The new verification configuration.
140+
"""
141+
self._verifier = certificate.Verifier(certificate_chain)
142+
return self

tests/api_test.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -88,16 +88,16 @@ def test_sign_and_verify(
8888
self, sigstore_oidc_beacon_token, sample_model_folder, tmp_path
8989
):
9090
sc = signing.Config()
91-
sc.set_sigstore_signer(
91+
sc.use_sigstore_signer(
9292
use_staging=True, identity_token=sigstore_oidc_beacon_token
9393
)
9494
signature_path = tmp_path / "model.sig"
9595
sc.sign(sample_model_folder, signature_path)
9696

9797
expected_identity = "https://github.com/sigstore-conformance/extremely-dangerous-public-oidc-beacon/.github/workflows/extremely-dangerous-oidc-beacon.yml@refs/heads/main"
98-
verifying.verify(
99-
sample_model_folder,
100-
signature_path,
98+
expected_oidc_issuer = "https://token.actions.githubusercontent.com"
99+
verifying.Config().use_sigstore_verifier(
101100
identity=expected_identity,
101+
oidc_issuer=expected_oidc_issuer,
102102
use_staging=True,
103-
)
103+
).verify(sample_model_folder, signature_path)

0 commit comments

Comments
 (0)