-
Notifications
You must be signed in to change notification settings - Fork 29
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add machinery for model signing and verification.
Added a set of `Empty*` classes just to show how these would be used in OSS. I'll send another PR for the actual in-toto classes, but this one mirrors the internal changelist where the API gets introduced. For the 2 `serialize_*_test.py` files: just ordered the imports to match the internal style. Signed-off-by: Mihai Maruseac <mihaimaruseac@google.com>
- Loading branch information
1 parent
f409c9a
commit 6ef25b6
Showing
6 changed files
with
406 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
# Copyright 2024 The Sigstore Authors | ||
# | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
|
||
"""Empty signing infrastructure. | ||
This is only used to test the signing and verification machinery. It can also be | ||
used as a default implementation in cases where some of the machinery doesn't | ||
need to do anything (e.g., in testing or in cases where verification is being | ||
done from outside the library). | ||
""" | ||
|
||
import pathlib | ||
from typing import Self | ||
from typing_extensions import override | ||
|
||
from model_signing.manifest import manifest | ||
from model_signing.signing import signing | ||
|
||
|
||
class EmptySigningPayload(signing.SigningPayload): | ||
"""An empty signing payload, mostly just for testing.""" | ||
|
||
@classmethod | ||
@override | ||
def from_manifest(cls, manifest: manifest.Manifest) -> Self: | ||
"""Converts a manifest to the signing payload used for signing. | ||
Args: | ||
manifest: the manifest to convert to signing payload. | ||
Returns: | ||
An instance of `EmptySigningPayload`. | ||
""" | ||
del manifest # unused | ||
return cls() | ||
|
||
def __eq__(self, other: "EmptySigningPayload") -> bool: | ||
"""Checks that `other` is also an `EmptySigningPayload`.""" | ||
return isinstance(other, EmptySigningPayload) | ||
|
||
|
||
class EmptySignature(signing.Signature): | ||
"""Empty signature, mostly for testing. | ||
Can also be used in cases where the signing result does not need to | ||
follow the rest of the signing machinery in this library (e.g., it is | ||
verified only by tooling that assume a different flow, or the existing | ||
signing machinery already manages writing signatures as a side effect of the | ||
signing process). | ||
""" | ||
|
||
@override | ||
def write_signature(self, path: pathlib.Path) -> None: | ||
"""Writes the signature to disk, to the given path. | ||
Since the signature is empty this function actually does nothing, it's | ||
here just to match the API. | ||
Args: | ||
path: the path to write the signature to. Ignored. | ||
""" | ||
del path # unused | ||
|
||
@classmethod | ||
@override | ||
def read_signature(cls, path: pathlib.Path) -> Self: | ||
"""Reads the signature from disk. | ||
Since the signature is empty, this does nothing besides just returning | ||
an instance of `EmptySignature`. | ||
Args: | ||
path: the path to read the signature from. Ignored. | ||
Returns: | ||
An instance of `EmptySignature`. | ||
""" | ||
del path # unused | ||
return cls() | ||
|
||
def __eq__(self, other: "EmptySignature") -> bool: | ||
"""Checks that `other` is also an `EmptySignature`.""" | ||
return isinstance(other, EmptySignature) | ||
|
||
|
||
class EmptySigner(signing.Signer): | ||
"""A signer that only produces `EmptySignature` objects, for testing.""" | ||
|
||
@override | ||
def sign(self, payload: signing.SigningPayload) -> EmptySignature: | ||
"""Signs the provided signing payload. | ||
Args: | ||
payload: the `SigningPayload` instance that should be signed. | ||
Returns: | ||
An `EmptySignature` object. | ||
""" | ||
del payload # unused | ||
return EmptySignature() | ||
|
||
|
||
class EmptyVerifier(signing.Verifier): | ||
"""Verifier that accepts only `EmptySignature` objects. | ||
Rather than producing a manifest out of thin air, the verifier also fails to | ||
verify the signature, even if it is in the accepted `EmptySignature` format. | ||
""" | ||
|
||
@override | ||
def verify(self, signature: signing.Signature) -> manifest.Manifest: | ||
"""Verifies the signature. | ||
Args: | ||
signature: the signature to verify. | ||
Raises: | ||
TypeError: If the signature is not an `EmptySignature` instance. | ||
ValueError: If the signature is an `EmptySignature` instance. This | ||
simulates failing signature verification. | ||
""" | ||
if isinstance(signature, EmptySignature): | ||
raise ValueError("Signature verification failed") | ||
raise TypeError("Only `EmptySignature` instances are supported") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
# Copyright 2024 The Sigstore Authors | ||
# | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
|
||
import pathlib | ||
from typing import Self | ||
import pytest | ||
from typing_extensions import override | ||
|
||
from model_signing import test_support | ||
from model_signing.hashing import hashing | ||
from model_signing.manifest import manifest | ||
from model_signing.signing import empty_signing | ||
from model_signing.signing import signing | ||
|
||
|
||
class TestEmptySigningPayload: | ||
|
||
def test_build_from_digest_manifest(self): | ||
digest = hashing.Digest("test", b"test_digest") | ||
manifest_file = manifest.DigestManifest(digest) | ||
|
||
payload = empty_signing.EmptySigningPayload.from_manifest(manifest_file) | ||
|
||
assert payload == empty_signing.EmptySigningPayload() | ||
|
||
def test_build_from_itemized_manifest(self): | ||
path1 = pathlib.PurePath("file1") | ||
digest1 = hashing.Digest("test", b"abcd") | ||
item1 = manifest.FileManifestItem(path=path1, digest=digest1) | ||
|
||
path2 = pathlib.PurePath("file2") | ||
digest2 = hashing.Digest("test", b"efgh") | ||
item2 = manifest.FileManifestItem(path=path2, digest=digest2) | ||
|
||
manifest_file = manifest.FileLevelManifest([item1, item2]) | ||
payload = empty_signing.EmptySigningPayload.from_manifest(manifest_file) | ||
|
||
assert payload == empty_signing.EmptySigningPayload() | ||
|
||
|
||
class TestEmptySignature: | ||
|
||
def test_write_and_read(self): | ||
signature = empty_signing.EmptySignature() | ||
signature.write_signature(test_support.UNUSED_PATH) | ||
|
||
new_signature = empty_signing.EmptySignature.read_signature( | ||
test_support.UNUSED_PATH | ||
) | ||
|
||
assert new_signature == signature | ||
|
||
|
||
class TestEmptySigner: | ||
|
||
def test_sign_gives_empty_signature(self): | ||
payload = empty_signing.EmptySigningPayload() | ||
signer = empty_signing.EmptySigner() | ||
|
||
signature = signer.sign(payload) | ||
|
||
assert isinstance(signature, empty_signing.EmptySignature) | ||
|
||
|
||
class _FakeSignature(signing.Signature): | ||
"""A test only signature that does nothing.""" | ||
|
||
@override | ||
def write_signature(self, path: pathlib.Path) -> None: | ||
del path # unused, do nothing | ||
|
||
@classmethod | ||
@override | ||
def read_signature(cls, path: pathlib.Path) -> Self: | ||
del path # unused, do nothing | ||
return cls() | ||
|
||
|
||
class TestEmptyVerifier: | ||
|
||
def test_only_empty_signatures_allowed(self): | ||
signature = _FakeSignature() | ||
verifier = empty_signing.EmptyVerifier() | ||
|
||
with pytest.raises( | ||
TypeError, | ||
match="Only `EmptySignature` instances are supported", | ||
): | ||
verifier.verify(signature) | ||
|
||
def test_verification_always_fails(self): | ||
signature = empty_signing.EmptySignature() | ||
verifier = empty_signing.EmptyVerifier() | ||
|
||
with pytest.raises( | ||
ValueError, | ||
match="Signature verification failed", | ||
): | ||
verifier.verify(signature) |
Oops, something went wrong.