13
13
# limitations under the License.
14
14
15
15
"""
16
- Common (base) models for the verification APIs .
16
+ Common models shared between signing and verification .
17
17
"""
18
18
19
19
from __future__ import annotations
20
20
21
21
import base64
22
22
import logging
23
+ import typing
23
24
from textwrap import dedent
25
+ from typing import Any , List , Optional
24
26
27
+ import rfc8785
25
28
from cryptography .hazmat .primitives .serialization import Encoding
26
29
from cryptography .x509 import (
27
30
Certificate ,
28
31
load_der_x509_certificate ,
29
32
)
33
+ from pydantic import (
34
+ BaseModel ,
35
+ ConfigDict ,
36
+ Field ,
37
+ StrictInt ,
38
+ StrictStr ,
39
+ ValidationInfo ,
40
+ field_validator ,
41
+ )
42
+ from pydantic .dataclasses import dataclass
30
43
from sigstore_protobuf_specs .dev .sigstore .bundle import v1 as bundle_v1
31
44
from sigstore_protobuf_specs .dev .sigstore .bundle .v1 import (
32
45
Bundle as _Bundle ,
39
52
)
40
53
41
54
from sigstore import dsse
55
+ from sigstore ._internal .merkle import verify_merkle_inclusion
56
+ from sigstore ._internal .rekor .checkpoint import verify_checkpoint
42
57
from sigstore ._utils import (
43
58
B64Str ,
44
59
BundleType ,
60
+ KeyID ,
45
61
cert_is_leaf ,
46
62
cert_is_root_ca ,
47
63
)
48
- from sigstore .errors import Error
49
- from sigstore .transparency import LogEntry , LogInclusionProof
64
+ from sigstore .errors import Error , VerificationError
65
+
66
+ if typing .TYPE_CHECKING :
67
+ from sigstore ._internal .trustroot import RekorKeyring
68
+
50
69
51
70
_logger = logging .getLogger (__name__ )
52
71
53
72
73
+ class LogInclusionProof (BaseModel ):
74
+ """
75
+ Represents an inclusion proof for a transparency log entry.
76
+ """
77
+
78
+ model_config = ConfigDict (populate_by_name = True )
79
+
80
+ checkpoint : StrictStr = Field (..., alias = "checkpoint" )
81
+ hashes : List [StrictStr ] = Field (..., alias = "hashes" )
82
+ log_index : StrictInt = Field (..., alias = "logIndex" )
83
+ root_hash : StrictStr = Field (..., alias = "rootHash" )
84
+ tree_size : StrictInt = Field (..., alias = "treeSize" )
85
+
86
+ @field_validator ("log_index" )
87
+ def _log_index_positive (cls , v : int ) -> int :
88
+ if v < 0 :
89
+ raise ValueError (f"Inclusion proof has invalid log index: { v } < 0" )
90
+ return v
91
+
92
+ @field_validator ("tree_size" )
93
+ def _tree_size_positive (cls , v : int ) -> int :
94
+ if v < 0 :
95
+ raise ValueError (f"Inclusion proof has invalid tree size: { v } < 0" )
96
+ return v
97
+
98
+ @field_validator ("tree_size" )
99
+ def _log_index_within_tree_size (
100
+ cls , v : int , info : ValidationInfo , ** kwargs : Any
101
+ ) -> int :
102
+ if "log_index" in info .data and v <= info .data ["log_index" ]:
103
+ raise ValueError (
104
+ "Inclusion proof has log index greater than or equal to tree size: "
105
+ f"{ v } <= { info .data ['log_index' ]} "
106
+ )
107
+ return v
108
+
109
+
110
+ @dataclass (frozen = True )
111
+ class LogEntry :
112
+ """
113
+ Represents a transparency log entry.
114
+
115
+ Log entries are retrieved from the transparency log after signing or verification events,
116
+ or loaded from "Sigstore" bundles provided by the user.
117
+
118
+ This representation allows for either a missing inclusion promise or a missing
119
+ inclusion proof, but not both: attempting to construct a `LogEntry` without
120
+ at least one will fail.
121
+ """
122
+
123
+ uuid : Optional [str ]
124
+ """
125
+ This entry's unique ID in the log instance it was retrieved from.
126
+
127
+ For sharded log deployments, IDs are unique per-shard.
128
+
129
+ Not present for `LogEntry` instances loaded from Sigstore bundles.
130
+ """
131
+
132
+ body : B64Str
133
+ """
134
+ The base64-encoded body of the transparency log entry.
135
+ """
136
+
137
+ integrated_time : int
138
+ """
139
+ The UNIX time at which this entry was integrated into the transparency log.
140
+ """
141
+
142
+ log_id : str
143
+ """
144
+ The log's ID (as the SHA256 hash of the DER-encoded public key for the log
145
+ at the time of entry inclusion).
146
+ """
147
+
148
+ log_index : int
149
+ """
150
+ The index of this entry within the log.
151
+ """
152
+
153
+ inclusion_proof : LogInclusionProof
154
+ """
155
+ An inclusion proof for this log entry.
156
+ """
157
+
158
+ inclusion_promise : Optional [B64Str ]
159
+ """
160
+ An inclusion promise for this log entry, if present.
161
+
162
+ Internally, this is a base64-encoded Signed Entry Timestamp (SET) for this
163
+ log entry.
164
+ """
165
+
166
+ @classmethod
167
+ def _from_response (cls , dict_ : dict [str , Any ]) -> LogEntry :
168
+ """
169
+ Create a new `LogEntry` from the given API response.
170
+ """
171
+
172
+ # Assumes we only get one entry back
173
+ entries = list (dict_ .items ())
174
+ if len (entries ) != 1 :
175
+ raise ValueError ("Received multiple entries in response" )
176
+
177
+ uuid , entry = entries [0 ]
178
+ return LogEntry (
179
+ uuid = uuid ,
180
+ body = entry ["body" ],
181
+ integrated_time = entry ["integratedTime" ],
182
+ log_id = entry ["logID" ],
183
+ log_index = entry ["logIndex" ],
184
+ inclusion_proof = LogInclusionProof .model_validate (
185
+ entry ["verification" ]["inclusionProof" ]
186
+ ),
187
+ inclusion_promise = entry ["verification" ]["signedEntryTimestamp" ],
188
+ )
189
+
190
+ def encode_canonical (self ) -> bytes :
191
+ """
192
+ Returns a canonicalized JSON (RFC 8785) representation of the transparency log entry.
193
+
194
+ This encoded representation is suitable for verification against
195
+ the Signed Entry Timestamp.
196
+ """
197
+ payload : dict [str , int | str ] = {
198
+ "body" : self .body ,
199
+ "integratedTime" : self .integrated_time ,
200
+ "logID" : self .log_id ,
201
+ "logIndex" : self .log_index ,
202
+ }
203
+
204
+ return rfc8785 .dumps (payload )
205
+
206
+ def _verify_set (self , keyring : RekorKeyring ) -> None :
207
+ """
208
+ Verify the inclusion promise (Signed Entry Timestamp) for a given transparency log
209
+ `entry` using the given `keyring`.
210
+
211
+ Fails if the given log entry does not contain an inclusion promise.
212
+ """
213
+
214
+ if self .inclusion_promise is None :
215
+ raise VerificationError ("SET: invalid inclusion promise: missing" )
216
+
217
+ signed_entry_ts = base64 .b64decode (self .inclusion_promise )
218
+
219
+ try :
220
+ keyring .verify (
221
+ key_id = KeyID (bytes .fromhex (self .log_id )),
222
+ signature = signed_entry_ts ,
223
+ data = self .encode_canonical (),
224
+ )
225
+ except VerificationError as exc :
226
+ raise VerificationError (f"SET: invalid inclusion promise: { exc } " )
227
+
228
+ def _verify (self , keyring : RekorKeyring ) -> None :
229
+ """
230
+ Verifies this log entry.
231
+
232
+ This method performs steps (5), (6), and optionally (7) in
233
+ the top-level verify API:
234
+
235
+ * Verifies the consistency of the entry with the given bundle;
236
+ * Verifies the Merkle inclusion proof and its signed checkpoint;
237
+ * Verifies the inclusion promise, if present.
238
+ """
239
+
240
+ verify_merkle_inclusion (self )
241
+ verify_checkpoint (keyring , self )
242
+
243
+ _logger .debug (f"successfully verified inclusion proof: index={ self .log_index } " )
244
+
245
+ if self .inclusion_promise :
246
+ self ._verify_set (keyring )
247
+ _logger .debug (
248
+ f"successfully verified inclusion promise: index={ self .log_index } "
249
+ )
250
+
251
+
54
252
class InvalidBundle (Error ):
55
253
"""
56
254
Raised when the associated `Bundle` is invalid in some way.
@@ -72,20 +270,6 @@ def diagnostics(self) -> str:
72
270
)
73
271
74
272
75
- class InvalidRekorEntry (InvalidBundle ):
76
- """
77
- Raised if the effective Rekor entry in `VerificationMaterials.rekor_entry()`
78
- does not match the other materials in `VerificationMaterials`.
79
-
80
- This can only happen in two scenarios:
81
-
82
- * A user has supplied the wrong offline entry, potentially maliciously;
83
- * The Rekor log responded with the wrong entry, suggesting a server error.
84
- """
85
-
86
- pass
87
-
88
-
89
273
class Bundle :
90
274
"""
91
275
Represents a Sigstore bundle.
0 commit comments