Skip to content

Commit

Permalink
Add machinery for model signing and verification.
Browse files Browse the repository at this point in the history
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
mihaimaruseac committed Jul 25, 2024
1 parent f409c9a commit 6ef25b6
Show file tree
Hide file tree
Showing 6 changed files with 406 additions and 4 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ jobs:
pip install -r model_signing/install/requirements_test_Linux.txt
pip install -r model_signing/install/requirements_dev_Linux.txt
# TODO: https://github.com/sigstore/model-transparency/issues/231 - Support all repo
pytype --keep-going model_signing/{hashing,manifest,serialization,signature}
pytype --keep-going model_signing/{hashing,manifest,serialization,signature,signing}
pylint-lint:
runs-on: ubuntu-latest
Expand All @@ -90,4 +90,4 @@ jobs:
pylint \
--max-line-length 80 \
--disable C0114,C0115,C0116,R0801,R0903,R0904,R0913,R0914,R1721,R1737,W0107,W0212,W0223,W0231,W0511,W0621 \
model_signing/{hashing,manifest,serialization,signature}
model_signing/{hashing,manifest,serialization,signature,signing}
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@
import pathlib
import pytest

from model_signing import test_support
from model_signing.hashing import file
from model_signing.hashing import memory
from model_signing.manifest import manifest
from model_signing.serialization import serialize_by_file_shard
from model_signing import test_support


class TestDigestSerializer:
Expand Down
2 changes: 1 addition & 1 deletion model_signing/serialization/serialize_by_file_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@
import pathlib
import pytest

from model_signing import test_support
from model_signing.hashing import file
from model_signing.hashing import memory
from model_signing.manifest import manifest
from model_signing.serialization import serialize_by_file
from model_signing import test_support


class TestDigestSerializer:
Expand Down
135 changes: 135 additions & 0 deletions model_signing/signing/empty_signing.py
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")
110 changes: 110 additions & 0 deletions model_signing/signing/empty_signing_test.py
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)
Loading

0 comments on commit 6ef25b6

Please sign in to comment.