Skip to content

Commit 6a3b93a

Browse files
committed
sigstore: flatten models into sigstore.models
Signed-off-by: William Woodruff <william@trailofbits.com>
1 parent 7e7cb04 commit 6a3b93a

14 files changed

+250
-317
lines changed

sigstore/_cli.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
from sigstore._utils import sha256_digest
4242
from sigstore.errors import Error, VerificationError
4343
from sigstore.hashes import Hashed
44+
from sigstore.models import Bundle
4445
from sigstore.oidc import (
4546
DEFAULT_OAUTH_ISSUER_URL,
4647
STAGING_OAUTH_ISSUER_URL,
@@ -54,7 +55,6 @@
5455
Verifier,
5556
policy,
5657
)
57-
from sigstore.verify.models import Bundle
5858

5959
logging.basicConfig(format="%(message)s", datefmt="[%X]", handlers=[RichHandler()])
6060
_logger = logging.getLogger(__name__)

sigstore/_internal/merkle.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
from sigstore.errors import VerificationError
3434

3535
if typing.TYPE_CHECKING:
36-
from sigstore.transparency import LogEntry
36+
from sigstore.models import LogEntry
3737

3838

3939
_LEAF_HASH_PREFIX = 0

sigstore/_internal/rekor/__init__.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,7 @@
2424
from sigstore._utils import base64_encode_pem_cert
2525
from sigstore.hashes import Hashed
2626

27-
from .checkpoint import SignedCheckpoint
28-
from .client import RekorClient
29-
3027
__all__ = [
31-
"RekorClient",
32-
"SignedCheckpoint",
3328
"_hashedrekord_from_parts",
3429
]
3530

sigstore/_internal/rekor/checkpoint.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
from sigstore.errors import VerificationError
3333

3434
if typing.TYPE_CHECKING:
35-
from sigstore.transparency import LogEntry
35+
from sigstore.models import LogEntry
3636

3737

3838
@dataclass(frozen=True)

sigstore/_internal/rekor/client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
import rekor_types
2929
import requests
3030

31-
from sigstore.transparency import LogEntry
31+
from sigstore.models import LogEntry
3232

3333
_logger = logging.getLogger(__name__)
3434

sigstore/verify/models.py renamed to sigstore/models.py

Lines changed: 201 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,33 @@
1313
# limitations under the License.
1414

1515
"""
16-
Common (base) models for the verification APIs.
16+
Common models shared between signing and verification.
1717
"""
1818

1919
from __future__ import annotations
2020

2121
import base64
2222
import logging
23+
import typing
2324
from textwrap import dedent
25+
from typing import Any, List, Optional
2426

27+
import rfc8785
2528
from cryptography.hazmat.primitives.serialization import Encoding
2629
from cryptography.x509 import (
2730
Certificate,
2831
load_der_x509_certificate,
2932
)
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
3043
from sigstore_protobuf_specs.dev.sigstore.bundle import v1 as bundle_v1
3144
from sigstore_protobuf_specs.dev.sigstore.bundle.v1 import (
3245
Bundle as _Bundle,
@@ -39,18 +52,203 @@
3952
)
4053

4154
from sigstore import dsse
55+
from sigstore._internal.merkle import verify_merkle_inclusion
56+
from sigstore._internal.rekor.checkpoint import verify_checkpoint
4257
from sigstore._utils import (
4358
B64Str,
4459
BundleType,
60+
KeyID,
4561
cert_is_leaf,
4662
cert_is_root_ca,
4763
)
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+
5069

5170
_logger = logging.getLogger(__name__)
5271

5372

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+
54252
class InvalidBundle(Error):
55253
"""
56254
Raised when the associated `Bundle` is invalid in some way.
@@ -72,20 +270,6 @@ def diagnostics(self) -> str:
72270
)
73271

74272

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-
89273
class Bundle:
90274
"""
91275
Represents a Sigstore bundle.

sigstore/sign.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@
6464
from sigstore._internal.sct import verify_sct
6565
from sigstore._internal.trustroot import KeyringPurpose, TrustedRoot
6666
from sigstore._utils import sha256_digest
67+
from sigstore.models import Bundle
6768
from sigstore.oidc import ExpiredIdentity, IdentityToken
68-
from sigstore.verify.models import Bundle
6969

7070
_logger = logging.getLogger(__name__)
7171

0 commit comments

Comments
 (0)