|
18 | 18 |
|
19 | 19 | from __future__ import annotations
|
20 | 20 |
|
| 21 | +import base64 |
21 | 22 | import json
|
22 | 23 | import logging
|
23 | 24 | from abc import ABC
|
|
26 | 27 |
|
27 | 28 | import rekor_types
|
28 | 29 | import requests
|
| 30 | +from cryptography.hazmat.primitives import serialization |
| 31 | +from cryptography.x509 import Certificate |
29 | 32 |
|
30 | 33 | from sigstore._internal import USER_AGENT
|
| 34 | +from sigstore._internal.rekor import ( |
| 35 | + EntryRequestBody, |
| 36 | + RekorClientError, |
| 37 | + RekorLogSubmitter, |
| 38 | +) |
| 39 | +from sigstore.dsse import Envelope |
| 40 | +from sigstore.hashes import Hashed |
31 | 41 | from sigstore.models import LogEntry
|
32 | 42 |
|
33 | 43 | _logger = logging.getLogger(__name__)
|
@@ -62,27 +72,6 @@ def from_response(cls, dict_: dict[str, Any]) -> RekorLogInfo:
|
62 | 72 | )
|
63 | 73 |
|
64 | 74 |
|
65 |
| -class RekorClientError(Exception): |
66 |
| - """ |
67 |
| - A generic error in the Rekor client. |
68 |
| - """ |
69 |
| - |
70 |
| - def __init__(self, http_error: requests.HTTPError): |
71 |
| - """ |
72 |
| - Create a new `RekorClientError` from the given `requests.HTTPError`. |
73 |
| - """ |
74 |
| - if http_error.response is not None: |
75 |
| - try: |
76 |
| - error = rekor_types.Error.model_validate_json(http_error.response.text) |
77 |
| - super().__init__(f"{error.code}: {error.message}") |
78 |
| - except Exception: |
79 |
| - super().__init__( |
80 |
| - f"Rekor returned an unknown error with HTTP {http_error.response.status_code}" |
81 |
| - ) |
82 |
| - else: |
83 |
| - super().__init__(f"Unexpected Rekor error: {http_error}") |
84 |
| - |
85 |
| - |
86 | 75 | class _Endpoint(ABC):
|
87 | 76 | def __init__(self, url: str, session: requests.Session) -> None:
|
88 | 77 | self.url = url
|
@@ -145,13 +134,12 @@ def get(
|
145 | 134 |
|
146 | 135 | def post(
|
147 | 136 | self,
|
148 |
| - proposed_entry: rekor_types.Hashedrekord | rekor_types.Dsse, |
| 137 | + payload: EntryRequestBody, |
149 | 138 | ) -> LogEntry:
|
150 | 139 | """
|
151 | 140 | Submit a new entry for inclusion in the Rekor log.
|
152 | 141 | """
|
153 | 142 |
|
154 |
| - payload = proposed_entry.model_dump(mode="json", by_alias=True) |
155 | 143 | _logger.debug(f"proposed: {json.dumps(payload)}")
|
156 | 144 |
|
157 | 145 | resp: requests.Response = self.session.post(self.url, json=payload)
|
@@ -216,7 +204,7 @@ def post(
|
216 | 204 | return oldest_entry
|
217 | 205 |
|
218 | 206 |
|
219 |
| -class RekorClient: |
| 207 | +class RekorClient(RekorLogSubmitter): |
220 | 208 | """The internal Rekor client"""
|
221 | 209 |
|
222 | 210 | def __init__(self, url: str) -> None:
|
@@ -261,3 +249,63 @@ def log(self) -> RekorLog:
|
261 | 249 | Returns a `RekorLog` adapter for making requests to a Rekor log.
|
262 | 250 | """
|
263 | 251 | return RekorLog(f"{self.url}/log", session=self.session)
|
| 252 | + |
| 253 | + def create_entry(self, request: EntryRequestBody) -> LogEntry: |
| 254 | + """ |
| 255 | + Submit the request to Rekor. |
| 256 | + """ |
| 257 | + return self.log.entries.post(request) |
| 258 | + |
| 259 | + def _build_hashed_rekord_request( # type: ignore[override] |
| 260 | + self, hashed_input: Hashed, signature: bytes, certificate: Certificate |
| 261 | + ) -> EntryRequestBody: |
| 262 | + """ |
| 263 | + Construct a hashed rekord payload to submit to Rekor. |
| 264 | + """ |
| 265 | + rekord = rekor_types.Hashedrekord( |
| 266 | + spec=rekor_types.hashedrekord.HashedrekordV001Schema( |
| 267 | + signature=rekor_types.hashedrekord.Signature( |
| 268 | + content=base64.b64encode(signature).decode(), |
| 269 | + public_key=rekor_types.hashedrekord.PublicKey( |
| 270 | + content=base64.b64encode( |
| 271 | + certificate.public_bytes( |
| 272 | + encoding=serialization.Encoding.PEM |
| 273 | + ) |
| 274 | + ).decode() |
| 275 | + ), |
| 276 | + ), |
| 277 | + data=rekor_types.hashedrekord.Data( |
| 278 | + hash=rekor_types.hashedrekord.Hash( |
| 279 | + algorithm=hashed_input._as_hashedrekord_algorithm(), |
| 280 | + value=hashed_input.digest.hex(), |
| 281 | + ) |
| 282 | + ), |
| 283 | + ), |
| 284 | + ) |
| 285 | + return EntryRequestBody(rekord.model_dump(mode="json", by_alias=True)) |
| 286 | + |
| 287 | + def _build_dsse_request( # type: ignore[override] |
| 288 | + self, envelope: Envelope, certificate: Certificate |
| 289 | + ) -> EntryRequestBody: |
| 290 | + """ |
| 291 | + Construct a dsse request to submit to Rekor. |
| 292 | + """ |
| 293 | + dsse = rekor_types.Dsse( |
| 294 | + spec=rekor_types.dsse.DsseSchema( |
| 295 | + # NOTE: mypy can't see that this kwarg is correct due to two interacting |
| 296 | + # behaviors/bugs (one pydantic, one datamodel-codegen): |
| 297 | + # See: <https://github.com/pydantic/pydantic/discussions/7418#discussioncomment-9024927> |
| 298 | + # See: <https://github.com/koxudaxi/datamodel-code-generator/issues/1903> |
| 299 | + proposed_content=rekor_types.dsse.ProposedContent( # type: ignore[call-arg] |
| 300 | + envelope=envelope.to_json(), |
| 301 | + verifiers=[ |
| 302 | + base64.b64encode( |
| 303 | + certificate.public_bytes( |
| 304 | + encoding=serialization.Encoding.PEM |
| 305 | + ) |
| 306 | + ).decode() |
| 307 | + ], |
| 308 | + ), |
| 309 | + ), |
| 310 | + ) |
| 311 | + return EntryRequestBody(dsse.model_dump(mode="json", by_alias=True)) |
0 commit comments