Skip to content

Commit 2198c9d

Browse files
Add machinery for model signing and verification. (#259)
* 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> * Handle internal review Signed-off-by: Mihai Maruseac <mihaimaruseac@google.com> * Add missing `__init__.py` file Signed-off-by: Mihai Maruseac <mihaimaruseac@google.com> --------- Signed-off-by: Mihai Maruseac <mihaimaruseac@google.com>
1 parent 1e1c503 commit 2198c9d

5 files changed

Lines changed: 419 additions & 2 deletions

File tree

.github/workflows/lint.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ jobs:
6666
pip install -r model_signing/install/requirements_test_Linux.txt
6767
pip install -r model_signing/install/requirements_dev_Linux.txt
6868
# TODO: https://github.com/sigstore/model-transparency/issues/231 - Support all repo
69-
pytype --keep-going model_signing/{hashing,manifest,serialization,signature}
69+
pytype --keep-going model_signing/{hashing,manifest,serialization,signature,signing}
7070
7171
pylint-lint:
7272
runs-on: ubuntu-latest
@@ -90,4 +90,4 @@ jobs:
9090
pylint \
9191
--max-line-length 80 \
9292
--disable C0114,C0115,C0116,R0801,R0903,R0904,R0913,R0914,R1721,R1737,W0107,W0212,W0223,W0231,W0511,W0621 \
93-
model_signing/{hashing,manifest,serialization,signature}
93+
model_signing/{hashing,manifest,serialization,signature,signing}

model_signing/signing/__init__.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
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.
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
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+
"""Empty signing infrastructure.
16+
17+
This is only used to test the signing and verification machinery. It can also be
18+
used as a default implementation in cases where some of the machinery doesn't
19+
need to do anything (e.g., in testing or in cases where verification is being
20+
done from outside the library).
21+
"""
22+
23+
import pathlib
24+
from typing import Self
25+
from typing_extensions import override
26+
27+
from model_signing.manifest import manifest
28+
from model_signing.signing import signing
29+
30+
31+
class EmptySigningPayload(signing.SigningPayload):
32+
"""An empty signing payload, mostly just for testing."""
33+
34+
@classmethod
35+
@override
36+
def from_manifest(cls, manifest: manifest.Manifest) -> Self:
37+
"""Converts a manifest to the signing payload used for signing.
38+
39+
Args:
40+
manifest: the manifest to convert to signing payload.
41+
42+
Returns:
43+
An instance of `EmptySigningPayload`.
44+
"""
45+
del manifest # unused
46+
return cls()
47+
48+
def __eq__(self, other: object) -> bool:
49+
if not isinstance(other, type(self)):
50+
return NotImplemented
51+
52+
return True # all instances are equal
53+
54+
55+
class EmptySignature(signing.Signature):
56+
"""Empty signature, mostly for testing.
57+
58+
Can also be used in cases where the signing result does not need to
59+
follow the rest of the signing machinery in this library (e.g., it is
60+
verified only by tooling that assume a different flow, or the existing
61+
signing machinery already manages writing signatures as a side effect of the
62+
signing process).
63+
"""
64+
65+
@override
66+
def write(self, path: pathlib.Path) -> None:
67+
"""Writes the signature to disk, to the given path.
68+
69+
Since the signature is empty this function actually does nothing, it's
70+
here just to match the API.
71+
72+
Args:
73+
path: the path to write the signature to. Ignored.
74+
"""
75+
del path # unused
76+
77+
@classmethod
78+
@override
79+
def read(cls, path: pathlib.Path) -> Self:
80+
"""Reads the signature from disk.
81+
82+
Since the signature is empty, this does nothing besides just returning
83+
an instance of `EmptySignature`.
84+
85+
Args:
86+
path: the path to read the signature from. Ignored.
87+
88+
Returns:
89+
An instance of `EmptySignature`.
90+
"""
91+
del path # unused
92+
return cls()
93+
94+
def __eq__(self, other: object) -> bool:
95+
if not isinstance(other, type(self)):
96+
return NotImplemented
97+
98+
return True # all instances are equal
99+
100+
101+
class EmptySigner(signing.Signer):
102+
"""A signer that only produces `EmptySignature` objects, for testing."""
103+
104+
@override
105+
def sign(self, payload: signing.SigningPayload) -> EmptySignature:
106+
"""Signs the provided signing payload.
107+
108+
Args:
109+
payload: the `SigningPayload` instance that should be signed.
110+
111+
Returns:
112+
An `EmptySignature` object.
113+
"""
114+
del payload # unused
115+
return EmptySignature()
116+
117+
118+
class EmptyVerifier(signing.Verifier):
119+
"""Verifier that accepts only `EmptySignature` objects.
120+
121+
Rather than producing a manifest out of thin air, the verifier also fails to
122+
verify the signature, even if it is in the accepted `EmptySignature` format.
123+
"""
124+
125+
@override
126+
def verify(self, signature: signing.Signature) -> manifest.Manifest:
127+
"""Verifies the signature.
128+
129+
Args:
130+
signature: the signature to verify.
131+
132+
Raises:
133+
TypeError: If the signature is not an `EmptySignature` instance.
134+
ValueError: If the signature is an `EmptySignature` instance. This
135+
simulates failing signature verification.
136+
"""
137+
if isinstance(signature, EmptySignature):
138+
raise ValueError("Signature verification failed")
139+
raise TypeError("Only `EmptySignature` instances are supported")
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
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+
import pathlib
16+
from typing import Self
17+
import pytest
18+
from typing_extensions import override
19+
20+
from model_signing import test_support
21+
from model_signing.hashing import hashing
22+
from model_signing.manifest import manifest
23+
from model_signing.signing import empty_signing
24+
from model_signing.signing import signing
25+
26+
27+
class TestEmptySigningPayload:
28+
29+
def test_build_from_digest_manifest(self):
30+
digest = hashing.Digest("test", b"test_digest")
31+
manifest_file = manifest.DigestManifest(digest)
32+
33+
payload = empty_signing.EmptySigningPayload.from_manifest(manifest_file)
34+
35+
assert payload == empty_signing.EmptySigningPayload()
36+
37+
def test_build_from_itemized_manifest(self):
38+
path1 = pathlib.PurePath("file1")
39+
digest1 = hashing.Digest("test", b"abcd")
40+
item1 = manifest.FileManifestItem(path=path1, digest=digest1)
41+
42+
path2 = pathlib.PurePath("file2")
43+
digest2 = hashing.Digest("test", b"efgh")
44+
item2 = manifest.FileManifestItem(path=path2, digest=digest2)
45+
46+
manifest_file = manifest.FileLevelManifest([item1, item2])
47+
payload = empty_signing.EmptySigningPayload.from_manifest(manifest_file)
48+
49+
assert payload == empty_signing.EmptySigningPayload()
50+
51+
52+
class TestEmptySignature:
53+
54+
def test_write_and_read(self):
55+
signature = empty_signing.EmptySignature()
56+
signature.write(test_support.UNUSED_PATH)
57+
58+
new_signature = empty_signing.EmptySignature.read(
59+
test_support.UNUSED_PATH
60+
)
61+
62+
assert new_signature == signature
63+
64+
65+
class TestEmptySigner:
66+
67+
def test_sign_gives_empty_signature(self):
68+
payload = empty_signing.EmptySigningPayload()
69+
signer = empty_signing.EmptySigner()
70+
71+
signature = signer.sign(payload)
72+
73+
assert isinstance(signature, empty_signing.EmptySignature)
74+
75+
76+
class _FakeSignature(signing.Signature):
77+
"""A test only signature that does nothing."""
78+
79+
@override
80+
def write(self, path: pathlib.Path) -> None:
81+
del path # unused, do nothing
82+
83+
@classmethod
84+
@override
85+
def read(cls, path: pathlib.Path) -> Self:
86+
del path # unused, do nothing
87+
return cls()
88+
89+
90+
class TestEmptyVerifier:
91+
92+
def test_only_empty_signatures_allowed(self):
93+
signature = _FakeSignature()
94+
verifier = empty_signing.EmptyVerifier()
95+
96+
with pytest.raises(
97+
TypeError,
98+
match="Only `EmptySignature` instances are supported",
99+
):
100+
verifier.verify(signature)
101+
102+
def test_verification_always_fails(self):
103+
signature = empty_signing.EmptySignature()
104+
verifier = empty_signing.EmptyVerifier()
105+
106+
with pytest.raises(
107+
ValueError,
108+
match="Signature verification failed",
109+
):
110+
verifier.verify(signature)

0 commit comments

Comments
 (0)