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_protobuf_specs.dev.sigstore.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())
109
110 @classmethod
111 @override
112 def read(cls, path: pathlib.Path) -> Self:
113 content = path.read_text()
114 parsed_dict = json.loads(content)
115 return cls(bundle_pb.Bundle().from_dict(parsed_dict))
116
117
118class Signer(signing.Signer):
119 """Signer for traditional signing.
120
121 This is subclassed for each traditional signing method we support.
122 """
123
124
125class Verifier(signing.Verifier):
126 """Verifier for traditional signature verification.
127
128 This is subclassed for each traditional signing method we support.
129 """
130
131 @override
132 def _verify_signed_content(
133 self, signature: signing.Signature
134 ) -> tuple[str, bytes]:
135 # We are guaranteed to only use the local signature type
136 signature = cast(Signature, signature)
137 bundle = signature.bundle
138
139 # Since the bundle is done via protobuf, check media type first
140 if bundle.media_type != _BUNDLE_MEDIA_TYPE:
141 raise ValueError(
142 f"Invalid sigstore bundle, got media type {bundle.media_type} "
143 f"but expected {_BUNDLE_MEDIA_TYPE}"
144 )
145
146 return self._verify_bundle(bundle)
147
148 @abc.abstractmethod
149 def _verify_bundle(self, bundle: bundle_pb.Bundle) -> tuple[str, bytes]:
150 """Verifies the bundle to extract the payload type and payload.
151
152 Since the bundle is generated via proto, we need to do more checks to
153 replace what `verify_dsse` from `sigstore_python` does.
154 """