1# Copyright 2025 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"""Sigstore based signature, signers and verifiers, using protobuf.
16
17The difference between this module and `sign_sigstore` is that here we use
18Sigstore via the protobuf specs instead of `sigstore-python`. This is to enable
19support for traditional signing and verification. These require additional data
20to be stored in the sigstore bundle used for the signature but `sigstore-python`
21validation does not allow those.
22"""
23
24import abc
25import json
26import pathlib
27import sys
28from typing import cast
29
30from sigstore_models.bundle import v1 as bundle_pb
31from typing_extensions import override
32
33from model_signing._signing import signing
34
35
36if sys.version_info >= (3, 11):
37 from typing import Self
38else:
39 from typing_extensions import Self
40
41
42# The media type to use when creating the protobuf based Sigstore bundle
43_BUNDLE_MEDIA_TYPE: str = "application/vnd.dev.sigstore.bundle.v0.3+json"
44
45
46def pae(raw_payload: bytes) -> bytes:
47 """Generates the PAE encoding of statement from the payload.
48
49 This is an internal of `sigstore_python`, but since in this module and
50 classes derived from the signer and verifier defined here we cannot use
51 `sigstore_python`, we have to reimplement this.
52
53 See https://github.com/secure-systems-lab/dsse/blob/v1.0.0/protocol.md
54 for details.
55
56 Args:
57 payload: The raw payload to encode.
58
59 Returns:
60 The encoded statement from the payload.
61 """
62 payload_type = signing._IN_TOTO_JSON_PAYLOAD_TYPE
63 payload_type_length = len(payload_type)
64 payload_length = len(raw_payload)
65 pae_str = f"DSSEv1 {payload_type_length} {payload_type} {payload_length}"
66 return b" ".join([pae_str.encode("utf-8"), raw_payload])
67
68
69def pae_compat(raw_payload: bytes) -> bytes:
70 """Generates the PAE encoding of statement from the payload.
71
72 This is the same as `pae`, but using the version as defined in v0.2.0 of the
73 `model_signing` library. Due to a bug in that implementation, signatures
74 generated at that version have to be verified using this compat patch. The
75 issue is that the raw payload, which is bytes, is added to a string and then
76 encoded back as bytes, so we get additional escape characters included.
77
78 Args:
79 payload: The raw payload to encode.
80
81 Returns:
82 The encoded statement from the payload.
83 """
84 payload_type = signing._IN_TOTO_JSON_PAYLOAD_TYPE
85 payload_type_length = len(payload_type)
86 payload_length = len(raw_payload)
87 # Notice bug here!
88 pae_str = (
89 f"DSSEV1 {payload_type_length} {payload_type} "
90 f"{payload_length} {raw_payload}"
91 )
92 return pae_str.encode("utf-8")
93
94
95class Signature(signing.Signature):
96 """Sigstore signature support, wrapping around `bundle_pb.Bundle`."""
97
98 def __init__(self, bundle: bundle_pb.Bundle):
99 """Builds an instance of this signature.
100
101 Args:
102 bundle: the sigstore bundle (in `bundle_pb.Bundle` format).
103 """
104 self.bundle = bundle
105
106 @override
107 def write(self, path: pathlib.Path) -> None:
108 path.write_text(self.bundle.to_json(), encoding="utf-8")
109
110 @classmethod
111 @override
112 def read(cls, path: pathlib.Path) -> Self:
113 content = path.read_text(encoding="utf-8")
114 parsed_dict = json.loads(content)
115
116 # adjust parsed_dict due to previous usage of protobufs
117 if "tlogEntries" not in parsed_dict["verificationMaterial"]:
118 parsed_dict["verificationMaterial"]["tlogEntries"] = []
119 if "publicKey" in parsed_dict["verificationMaterial"]:
120 if "hint" not in parsed_dict["verificationMaterial"]["publicKey"]:
121 parsed_dict["verificationMaterial"]["publicKey"]["hint"] = None
122 for k in ["rawBytes", "keyDetails"]:
123 if k in parsed_dict["verificationMaterial"]["publicKey"]:
124 del parsed_dict["verificationMaterial"]["publicKey"][k]
125
126 return cls(bundle_pb.Bundle.from_dict(parsed_dict))
127
128
129class Signer(signing.Signer):
130 """Signer for traditional signing.
131
132 This is subclassed for each traditional signing method we support.
133 """
134
135
136class Verifier(signing.Verifier):
137 """Verifier for traditional signature verification.
138
139 This is subclassed for each traditional signing method we support.
140 """
141
142 @override
143 def _verify_signed_content(
144 self, signature: signing.Signature
145 ) -> tuple[str, bytes]:
146 # We are guaranteed to only use the local signature type
147 signature = cast(Signature, signature)
148 bundle = signature.bundle
149
150 # Since the bundle is done via protobuf, check media type first
151 if bundle.media_type != _BUNDLE_MEDIA_TYPE:
152 raise ValueError(
153 f"Invalid sigstore bundle, got media type {bundle.media_type} "
154 f"but expected {_BUNDLE_MEDIA_TYPE}"
155 )
156
157 return self._verify_bundle(bundle)
158
159 @abc.abstractmethod
160 def _verify_bundle(self, bundle: bundle_pb.Bundle) -> tuple[str, bytes]:
161 """Verifies the bundle to extract the payload type and payload.
162
163 Since the bundle is generated via proto, we need to do more checks to
164 replace what `verify_dsse` from `sigstore_python` does.
165 """