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 raw_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 raw_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_bytes(self.to_bytes())
109
110 @override
111 def to_bytes(self) -> bytes:
112 return self.bundle.to_json().encode("utf-8")
113
114 @classmethod
115 @override
116 def read(cls, path: pathlib.Path) -> Self:
117 content = path.read_text(encoding="utf-8")
118 parsed_dict = json.loads(content)
119
120 # adjust parsed_dict due to previous usage of protobufs
121 if "tlogEntries" not in parsed_dict["verificationMaterial"]:
122 parsed_dict["verificationMaterial"]["tlogEntries"] = []
123 if "publicKey" in parsed_dict["verificationMaterial"]:
124 if "hint" not in parsed_dict["verificationMaterial"]["publicKey"]:
125 parsed_dict["verificationMaterial"]["publicKey"]["hint"] = None
126 for k in ["rawBytes", "keyDetails"]:
127 if k in parsed_dict["verificationMaterial"]["publicKey"]:
128 del parsed_dict["verificationMaterial"]["publicKey"][k]
129
130 return cls(bundle_pb.Bundle.from_dict(parsed_dict))
131
132
133class Signer(signing.Signer):
134 """Signer for traditional signing.
135
136 This is subclassed for each traditional signing method we support.
137 """
138
139
140class Verifier(signing.Verifier):
141 """Verifier for traditional signature verification.
142
143 This is subclassed for each traditional signing method we support.
144 """
145
146 @override
147 def _verify_signed_content(
148 self, signature: signing.Signature
149 ) -> tuple[str, bytes]:
150 # We are guaranteed to only use the local signature type
151 signature = cast(Signature, signature)
152 bundle = signature.bundle
153
154 # Since the bundle is done via protobuf, check media type first
155 if bundle.media_type != _BUNDLE_MEDIA_TYPE:
156 raise ValueError(
157 f"Invalid sigstore bundle, got media type {bundle.media_type} "
158 f"but expected {_BUNDLE_MEDIA_TYPE}"
159 )
160
161 return self._verify_bundle(bundle)
162
163 @abc.abstractmethod
164 def _verify_bundle(self, bundle: bundle_pb.Bundle) -> tuple[str, bytes]:
165 """Verifies the bundle to extract the payload type and payload.
166
167 Since the bundle is generated via proto, we need to do more checks to
168 replace what `verify_dsse` from `sigstore_python` does.
169 """