Edit on GitHub

model_signing.verifying

High level API for the verification interface of model_signing library.

This module supports configuring the verification method used to verify a model, before performing the verification.

model_signing.verifying.Config().use_sigstore_verifier(
    identity=identity, oidc_issuer=oidc_provider
).verify("finbert", "finbert.sig")

The same verification configuration can be used to verify multiple models:

verifying_config = model_signing.signing.Config().use_elliptic_key_verifier(
    public_key="key.pub"
)

for model in all_models:
    verifying_config.verify(model, f"{model}_sharded.sig")

The API defined here is stable and backwards compatible.

  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 verification interface of `model_signing` library.
 16
 17This module supports configuring the verification method used to verify a model,
 18before performing the verification.
 19
 20```python
 21model_signing.verifying.Config().use_sigstore_verifier(
 22    identity=identity, oidc_issuer=oidc_provider
 23).verify("finbert", "finbert.sig")
 24```
 25
 26The same verification configuration can be used to verify multiple models:
 27
 28```python
 29verifying_config = model_signing.signing.Config().use_elliptic_key_verifier(
 30    public_key="key.pub"
 31)
 32
 33for model in all_models:
 34    verifying_config.verify(model, f"{model}_sharded.sig")
 35```
 36
 37The API defined here is stable and backwards compatible.
 38"""
 39
 40from collections.abc import Iterable
 41import pathlib
 42import sys
 43
 44from model_signing import hashing
 45from model_signing import manifest
 46from model_signing._signing import sign_certificate as certificate
 47from model_signing._signing import sign_ec_key as ec_key
 48from model_signing._signing import sign_sigstore as sigstore
 49from model_signing._signing import sign_sigstore_pb as sigstore_pb
 50
 51
 52if sys.version_info >= (3, 11):
 53    from typing import Self
 54else:
 55    from typing_extensions import Self
 56
 57
 58class Config:
 59    """Configuration to use when verifying models against signatures.
 60
 61    The verification configuration is needed to determine how to read and verify
 62    the signature. Given we support multiple signing format, the verification
 63    settings must match the signing ones.
 64
 65    The configuration also supports configuring the hashing configuration from
 66    `model_signing.hashing`. This should also match the configuration used
 67    during signing. However, by default, we can attempt to guess it from the
 68    signature.
 69    """
 70
 71    def __init__(self):
 72        """Initializes the default configuration for verification."""
 73        self._hashing_config = None
 74        self._verifier = None
 75        self._uses_sigstore = False
 76        self._ignore_unsigned_files = False
 77
 78    def verify(
 79        self, model_path: hashing.PathLike, signature_path: hashing.PathLike
 80    ):
 81        """Verifies that a model conforms to a signature.
 82
 83        Args:
 84            model_path: The path to the model to verify.
 85
 86        Raises:
 87            ValueError: No verifier has been configured.
 88        """
 89        if self._verifier is None:
 90            raise ValueError("Attempting to verify with no configured verifier")
 91
 92        if self._uses_sigstore:
 93            signature = sigstore.Signature.read(pathlib.Path(signature_path))
 94        else:
 95            signature = sigstore_pb.Signature.read(pathlib.Path(signature_path))
 96
 97        expected_manifest = self._verifier.verify(signature)
 98
 99        if self._hashing_config is None:
100            self._guess_hashing_config(expected_manifest)
101        if "ignore_paths" in expected_manifest.serialization_type:
102            self._hashing_config.add_ignored_paths(
103                model_path=model_path,
104                paths=expected_manifest.serialization_type["ignore_paths"],
105            )
106
107        if self._ignore_unsigned_files:
108            files_to_hash = [
109                model_path / rd.identifier
110                for rd in expected_manifest.resource_descriptors()
111            ]
112        else:
113            files_to_hash = None
114
115        actual_manifest = self._hashing_config.hash(
116            model_path, files_to_hash=files_to_hash
117        )
118
119        if actual_manifest != expected_manifest:
120            diff_message = self._get_manifest_diff(
121                actual_manifest, expected_manifest
122            )
123            raise ValueError(f"Signature mismatch: {diff_message}")
124
125    def _get_manifest_diff(self, actual, expected) -> list[str]:
126        diffs = []
127
128        actual_hashes = {
129            rd.identifier: rd.digest for rd in actual.resource_descriptors()
130        }
131        expected_hashes = {
132            rd.identifier: rd.digest for rd in expected.resource_descriptors()
133        }
134
135        extra_actual_files = set(actual_hashes.keys()) - set(
136            expected_hashes.keys()
137        )
138        if extra_actual_files:
139            diffs.append(
140                f"Extra files found in model '{actual.model_name}': "
141                f"{', '.join(sorted(extra_actual_files))}"
142            )
143
144        missing_actual_files = set(expected_hashes.keys()) - set(
145            actual_hashes.keys()
146        )
147        if missing_actual_files:
148            diffs.append(
149                f"Missing files in model '{actual.model_name}': "
150                f"{', '.join(sorted(missing_actual_files))}"
151            )
152
153        common_files = set(actual_hashes.keys()) & set(expected_hashes.keys())
154        for identifier in sorted(common_files):
155            if actual_hashes[identifier] != expected_hashes[identifier]:
156                diffs.append(
157                    f"Hash mismatch for '{identifier}': "
158                    f"Expected '{expected_hashes[identifier]}', "
159                    f"Actual '{actual_hashes[identifier]}'"
160                )
161        return diffs
162
163    def set_hashing_config(self, hashing_config: hashing.Config) -> Self:
164        """Sets the new configuration for hashing models.
165
166        After calling this method, the automatic guessing of the hashing
167        configuration used during signing is no longer possible from within one
168        instance of this class.
169
170        Args:
171            hashing_config: The new hashing configuration.
172
173        Returns:
174            The new signing configuration.
175        """
176        self._hashing_config = hashing_config
177        return self
178
179    def set_ignore_unsigned_files(self, ignore_unsigned_files: bool) -> Self:
180        """Sets whether files that were not signed are to be ignored.
181
182        This method allows to ignore those files that are not part of the
183        manifest and therefor were not originally signed.
184
185        Args:
186            ignore_unsigned_files: whether to ignore unsigned files
187        """
188        self._ignore_unsigned_files = ignore_unsigned_files
189        return self
190
191    def _guess_hashing_config(self, source_manifest: manifest.Manifest) -> None:
192        """Attempts to guess the hashing config from a manifest."""
193        args = source_manifest.serialization_type
194        method = args["method"]
195        # TODO: Once Python 3.9 support is deprecated revert to using `match`
196        if method == "files":
197            self._hashing_config = hashing.Config().use_file_serialization(
198                hashing_algorithm=args["hash_type"],
199                allow_symlinks=args["allow_symlinks"],
200                ignore_paths=args.get("ignore_paths", frozenset()),
201            )
202        elif method == "shards":
203            self._hashing_config = hashing.Config().use_shard_serialization(
204                hashing_algorithm=args["hash_type"],
205                shard_size=args["shard_size"],
206                allow_symlinks=args["allow_symlinks"],
207                ignore_paths=args.get("ignore_paths", frozenset()),
208            )
209        else:
210            raise ValueError("Cannot guess the hashing configuration")
211
212    def use_sigstore_verifier(
213        self, *, identity: str, oidc_issuer: str, use_staging: bool = False
214    ) -> Self:
215        """Configures the verification of signatures produced by Sigstore.
216
217        The verifier in this configuration is changed to one that performs
218        verification of Sigstore signatures (sigstore bundles signed by
219        keyless signing via Sigstore).
220
221        Args:
222            identity: The expected identity that has signed the model.
223            oidc_issuer: The expected OpenID Connect issuer that provided the
224              certificate used for the signature.
225            use_staging: Use staging configurations, instead of production. This
226              is supposed to be set to True only when testing. Default is False.
227
228        Return:
229            The new verification configuration.
230        """
231        self._uses_sigstore = True
232        self._verifier = sigstore.Verifier(
233            identity=identity, oidc_issuer=oidc_issuer, use_staging=use_staging
234        )
235        return self
236
237    def use_elliptic_key_verifier(
238        self, *, public_key: hashing.PathLike
239    ) -> Self:
240        """Configures the verification of signatures generated by a private key.
241
242        The verifier in this configuration is changed to one that performs
243        verification of sgistore bundles signed by an elliptic curve private
244        key. The public key used in the configuration must match the private key
245        used during signing.
246
247        Args:
248            public_key: The path to the public key to verify with.
249
250        Return:
251            The new verification configuration.
252        """
253        self._uses_sigstore = False
254        self._verifier = ec_key.Verifier(pathlib.Path(public_key))
255        return self
256
257    def use_certificate_verifier(
258        self,
259        *,
260        certificate_chain: Iterable[hashing.PathLike] = frozenset(),
261        log_fingerprints: bool = False,
262    ) -> Self:
263        """Configures the verification of signatures generated by a certificate.
264
265        The verifier in this configuration is changed to one that performs
266        verification of sgistore bundles signed by a signing certificate.
267
268        Args:
269            certificate_chain: Certificate chain to establish root of trust. If
270              empty, the operating system's one is used.
271            log_fingerprints: Log certificates' SHA256 fingerprints
272
273        Return:
274            The new verification configuration.
275        """
276        self._uses_sigstore = False
277        self._verifier = certificate.Verifier(
278            [pathlib.Path(c) for c in certificate_chain],
279            log_fingerprints=log_fingerprints,
280        )
281        return self
class Config:
 59class Config:
 60    """Configuration to use when verifying models against signatures.
 61
 62    The verification configuration is needed to determine how to read and verify
 63    the signature. Given we support multiple signing format, the verification
 64    settings must match the signing ones.
 65
 66    The configuration also supports configuring the hashing configuration from
 67    `model_signing.hashing`. This should also match the configuration used
 68    during signing. However, by default, we can attempt to guess it from the
 69    signature.
 70    """
 71
 72    def __init__(self):
 73        """Initializes the default configuration for verification."""
 74        self._hashing_config = None
 75        self._verifier = None
 76        self._uses_sigstore = False
 77        self._ignore_unsigned_files = False
 78
 79    def verify(
 80        self, model_path: hashing.PathLike, signature_path: hashing.PathLike
 81    ):
 82        """Verifies that a model conforms to a signature.
 83
 84        Args:
 85            model_path: The path to the model to verify.
 86
 87        Raises:
 88            ValueError: No verifier has been configured.
 89        """
 90        if self._verifier is None:
 91            raise ValueError("Attempting to verify with no configured verifier")
 92
 93        if self._uses_sigstore:
 94            signature = sigstore.Signature.read(pathlib.Path(signature_path))
 95        else:
 96            signature = sigstore_pb.Signature.read(pathlib.Path(signature_path))
 97
 98        expected_manifest = self._verifier.verify(signature)
 99
100        if self._hashing_config is None:
101            self._guess_hashing_config(expected_manifest)
102        if "ignore_paths" in expected_manifest.serialization_type:
103            self._hashing_config.add_ignored_paths(
104                model_path=model_path,
105                paths=expected_manifest.serialization_type["ignore_paths"],
106            )
107
108        if self._ignore_unsigned_files:
109            files_to_hash = [
110                model_path / rd.identifier
111                for rd in expected_manifest.resource_descriptors()
112            ]
113        else:
114            files_to_hash = None
115
116        actual_manifest = self._hashing_config.hash(
117            model_path, files_to_hash=files_to_hash
118        )
119
120        if actual_manifest != expected_manifest:
121            diff_message = self._get_manifest_diff(
122                actual_manifest, expected_manifest
123            )
124            raise ValueError(f"Signature mismatch: {diff_message}")
125
126    def _get_manifest_diff(self, actual, expected) -> list[str]:
127        diffs = []
128
129        actual_hashes = {
130            rd.identifier: rd.digest for rd in actual.resource_descriptors()
131        }
132        expected_hashes = {
133            rd.identifier: rd.digest for rd in expected.resource_descriptors()
134        }
135
136        extra_actual_files = set(actual_hashes.keys()) - set(
137            expected_hashes.keys()
138        )
139        if extra_actual_files:
140            diffs.append(
141                f"Extra files found in model '{actual.model_name}': "
142                f"{', '.join(sorted(extra_actual_files))}"
143            )
144
145        missing_actual_files = set(expected_hashes.keys()) - set(
146            actual_hashes.keys()
147        )
148        if missing_actual_files:
149            diffs.append(
150                f"Missing files in model '{actual.model_name}': "
151                f"{', '.join(sorted(missing_actual_files))}"
152            )
153
154        common_files = set(actual_hashes.keys()) & set(expected_hashes.keys())
155        for identifier in sorted(common_files):
156            if actual_hashes[identifier] != expected_hashes[identifier]:
157                diffs.append(
158                    f"Hash mismatch for '{identifier}': "
159                    f"Expected '{expected_hashes[identifier]}', "
160                    f"Actual '{actual_hashes[identifier]}'"
161                )
162        return diffs
163
164    def set_hashing_config(self, hashing_config: hashing.Config) -> Self:
165        """Sets the new configuration for hashing models.
166
167        After calling this method, the automatic guessing of the hashing
168        configuration used during signing is no longer possible from within one
169        instance of this class.
170
171        Args:
172            hashing_config: The new hashing configuration.
173
174        Returns:
175            The new signing configuration.
176        """
177        self._hashing_config = hashing_config
178        return self
179
180    def set_ignore_unsigned_files(self, ignore_unsigned_files: bool) -> Self:
181        """Sets whether files that were not signed are to be ignored.
182
183        This method allows to ignore those files that are not part of the
184        manifest and therefor were not originally signed.
185
186        Args:
187            ignore_unsigned_files: whether to ignore unsigned files
188        """
189        self._ignore_unsigned_files = ignore_unsigned_files
190        return self
191
192    def _guess_hashing_config(self, source_manifest: manifest.Manifest) -> None:
193        """Attempts to guess the hashing config from a manifest."""
194        args = source_manifest.serialization_type
195        method = args["method"]
196        # TODO: Once Python 3.9 support is deprecated revert to using `match`
197        if method == "files":
198            self._hashing_config = hashing.Config().use_file_serialization(
199                hashing_algorithm=args["hash_type"],
200                allow_symlinks=args["allow_symlinks"],
201                ignore_paths=args.get("ignore_paths", frozenset()),
202            )
203        elif method == "shards":
204            self._hashing_config = hashing.Config().use_shard_serialization(
205                hashing_algorithm=args["hash_type"],
206                shard_size=args["shard_size"],
207                allow_symlinks=args["allow_symlinks"],
208                ignore_paths=args.get("ignore_paths", frozenset()),
209            )
210        else:
211            raise ValueError("Cannot guess the hashing configuration")
212
213    def use_sigstore_verifier(
214        self, *, identity: str, oidc_issuer: str, use_staging: bool = False
215    ) -> Self:
216        """Configures the verification of signatures produced by Sigstore.
217
218        The verifier in this configuration is changed to one that performs
219        verification of Sigstore signatures (sigstore bundles signed by
220        keyless signing via Sigstore).
221
222        Args:
223            identity: The expected identity that has signed the model.
224            oidc_issuer: The expected OpenID Connect issuer that provided the
225              certificate used for the signature.
226            use_staging: Use staging configurations, instead of production. This
227              is supposed to be set to True only when testing. Default is False.
228
229        Return:
230            The new verification configuration.
231        """
232        self._uses_sigstore = True
233        self._verifier = sigstore.Verifier(
234            identity=identity, oidc_issuer=oidc_issuer, use_staging=use_staging
235        )
236        return self
237
238    def use_elliptic_key_verifier(
239        self, *, public_key: hashing.PathLike
240    ) -> Self:
241        """Configures the verification of signatures generated by a private key.
242
243        The verifier in this configuration is changed to one that performs
244        verification of sgistore bundles signed by an elliptic curve private
245        key. The public key used in the configuration must match the private key
246        used during signing.
247
248        Args:
249            public_key: The path to the public key to verify with.
250
251        Return:
252            The new verification configuration.
253        """
254        self._uses_sigstore = False
255        self._verifier = ec_key.Verifier(pathlib.Path(public_key))
256        return self
257
258    def use_certificate_verifier(
259        self,
260        *,
261        certificate_chain: Iterable[hashing.PathLike] = frozenset(),
262        log_fingerprints: bool = False,
263    ) -> Self:
264        """Configures the verification of signatures generated by a certificate.
265
266        The verifier in this configuration is changed to one that performs
267        verification of sgistore bundles signed by a signing certificate.
268
269        Args:
270            certificate_chain: Certificate chain to establish root of trust. If
271              empty, the operating system's one is used.
272            log_fingerprints: Log certificates' SHA256 fingerprints
273
274        Return:
275            The new verification configuration.
276        """
277        self._uses_sigstore = False
278        self._verifier = certificate.Verifier(
279            [pathlib.Path(c) for c in certificate_chain],
280            log_fingerprints=log_fingerprints,
281        )
282        return self

Configuration to use when verifying models against signatures.

The verification configuration is needed to determine how to read and verify the signature. Given we support multiple signing format, the verification settings must match the signing ones.

The configuration also supports configuring the hashing configuration from model_signing.hashing. This should also match the configuration used during signing. However, by default, we can attempt to guess it from the signature.

Config()
72    def __init__(self):
73        """Initializes the default configuration for verification."""
74        self._hashing_config = None
75        self._verifier = None
76        self._uses_sigstore = False
77        self._ignore_unsigned_files = False

Initializes the default configuration for verification.

def verify( self, model_path: Union[str, bytes, os.PathLike], signature_path: Union[str, bytes, os.PathLike]):
 79    def verify(
 80        self, model_path: hashing.PathLike, signature_path: hashing.PathLike
 81    ):
 82        """Verifies that a model conforms to a signature.
 83
 84        Args:
 85            model_path: The path to the model to verify.
 86
 87        Raises:
 88            ValueError: No verifier has been configured.
 89        """
 90        if self._verifier is None:
 91            raise ValueError("Attempting to verify with no configured verifier")
 92
 93        if self._uses_sigstore:
 94            signature = sigstore.Signature.read(pathlib.Path(signature_path))
 95        else:
 96            signature = sigstore_pb.Signature.read(pathlib.Path(signature_path))
 97
 98        expected_manifest = self._verifier.verify(signature)
 99
100        if self._hashing_config is None:
101            self._guess_hashing_config(expected_manifest)
102        if "ignore_paths" in expected_manifest.serialization_type:
103            self._hashing_config.add_ignored_paths(
104                model_path=model_path,
105                paths=expected_manifest.serialization_type["ignore_paths"],
106            )
107
108        if self._ignore_unsigned_files:
109            files_to_hash = [
110                model_path / rd.identifier
111                for rd in expected_manifest.resource_descriptors()
112            ]
113        else:
114            files_to_hash = None
115
116        actual_manifest = self._hashing_config.hash(
117            model_path, files_to_hash=files_to_hash
118        )
119
120        if actual_manifest != expected_manifest:
121            diff_message = self._get_manifest_diff(
122                actual_manifest, expected_manifest
123            )
124            raise ValueError(f"Signature mismatch: {diff_message}")

Verifies that a model conforms to a signature.

Arguments:
  • model_path: The path to the model to verify.
Raises:
  • ValueError: No verifier has been configured.
def set_hashing_config(self, hashing_config: model_signing.hashing.Config) -> Self:
164    def set_hashing_config(self, hashing_config: hashing.Config) -> Self:
165        """Sets the new configuration for hashing models.
166
167        After calling this method, the automatic guessing of the hashing
168        configuration used during signing is no longer possible from within one
169        instance of this class.
170
171        Args:
172            hashing_config: The new hashing configuration.
173
174        Returns:
175            The new signing configuration.
176        """
177        self._hashing_config = hashing_config
178        return self

Sets the new configuration for hashing models.

After calling this method, the automatic guessing of the hashing configuration used during signing is no longer possible from within one instance of this class.

Arguments:
  • hashing_config: The new hashing configuration.
Returns:

The new signing configuration.

def set_ignore_unsigned_files(self, ignore_unsigned_files: bool) -> Self:
180    def set_ignore_unsigned_files(self, ignore_unsigned_files: bool) -> Self:
181        """Sets whether files that were not signed are to be ignored.
182
183        This method allows to ignore those files that are not part of the
184        manifest and therefor were not originally signed.
185
186        Args:
187            ignore_unsigned_files: whether to ignore unsigned files
188        """
189        self._ignore_unsigned_files = ignore_unsigned_files
190        return self

Sets whether files that were not signed are to be ignored.

This method allows to ignore those files that are not part of the manifest and therefor were not originally signed.

Arguments:
  • ignore_unsigned_files: whether to ignore unsigned files
def use_sigstore_verifier( self, *, identity: str, oidc_issuer: str, use_staging: bool = False) -> Self:
213    def use_sigstore_verifier(
214        self, *, identity: str, oidc_issuer: str, use_staging: bool = False
215    ) -> Self:
216        """Configures the verification of signatures produced by Sigstore.
217
218        The verifier in this configuration is changed to one that performs
219        verification of Sigstore signatures (sigstore bundles signed by
220        keyless signing via Sigstore).
221
222        Args:
223            identity: The expected identity that has signed the model.
224            oidc_issuer: The expected OpenID Connect issuer that provided the
225              certificate used for the signature.
226            use_staging: Use staging configurations, instead of production. This
227              is supposed to be set to True only when testing. Default is False.
228
229        Return:
230            The new verification configuration.
231        """
232        self._uses_sigstore = True
233        self._verifier = sigstore.Verifier(
234            identity=identity, oidc_issuer=oidc_issuer, use_staging=use_staging
235        )
236        return self

Configures the verification of signatures produced by Sigstore.

The verifier in this configuration is changed to one that performs verification of Sigstore signatures (sigstore bundles signed by keyless signing via Sigstore).

Arguments:
  • identity: The expected identity that has signed the model.
  • oidc_issuer: The expected OpenID Connect issuer that provided the certificate used for the signature.
  • use_staging: Use staging configurations, instead of production. This is supposed to be set to True only when testing. Default is False.
Return:

The new verification configuration.

def use_elliptic_key_verifier(self, *, public_key: Union[str, bytes, os.PathLike]) -> Self:
238    def use_elliptic_key_verifier(
239        self, *, public_key: hashing.PathLike
240    ) -> Self:
241        """Configures the verification of signatures generated by a private key.
242
243        The verifier in this configuration is changed to one that performs
244        verification of sgistore bundles signed by an elliptic curve private
245        key. The public key used in the configuration must match the private key
246        used during signing.
247
248        Args:
249            public_key: The path to the public key to verify with.
250
251        Return:
252            The new verification configuration.
253        """
254        self._uses_sigstore = False
255        self._verifier = ec_key.Verifier(pathlib.Path(public_key))
256        return self

Configures the verification of signatures generated by a private key.

The verifier in this configuration is changed to one that performs verification of sgistore bundles signed by an elliptic curve private key. The public key used in the configuration must match the private key used during signing.

Arguments:
  • public_key: The path to the public key to verify with.
Return:

The new verification configuration.

def use_certificate_verifier( self, *, certificate_chain: Iterable[typing.Union[str, bytes, os.PathLike]] = frozenset(), log_fingerprints: bool = False) -> Self:
258    def use_certificate_verifier(
259        self,
260        *,
261        certificate_chain: Iterable[hashing.PathLike] = frozenset(),
262        log_fingerprints: bool = False,
263    ) -> Self:
264        """Configures the verification of signatures generated by a certificate.
265
266        The verifier in this configuration is changed to one that performs
267        verification of sgistore bundles signed by a signing certificate.
268
269        Args:
270            certificate_chain: Certificate chain to establish root of trust. If
271              empty, the operating system's one is used.
272            log_fingerprints: Log certificates' SHA256 fingerprints
273
274        Return:
275            The new verification configuration.
276        """
277        self._uses_sigstore = False
278        self._verifier = certificate.Verifier(
279            [pathlib.Path(c) for c in certificate_chain],
280            log_fingerprints=log_fingerprints,
281        )
282        return self

Configures the verification of signatures generated by a certificate.

The verifier in this configuration is changed to one that performs verification of sgistore bundles signed by a signing certificate.

Arguments:
  • certificate_chain: Certificate chain to establish root of trust. If empty, the operating system's one is used.
  • log_fingerprints: Log certificates' SHA256 fingerprints
Return:

The new verification configuration.