Skip to content

Add Rekor v2 client #1422

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 39 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
376361a
add RekorV2Client and types
ramonpetgrave64 May 20, 2025
2983b4a
add rekorv2 client + tests
ramonpetgrave64 May 20, 2025
3f94e7a
add lots of docstrings
ramonpetgrave64 May 20, 2025
d043256
xfail on local and staging
ramonpetgrave64 May 20, 2025
1a55117
add cahngelog
ramonpetgrave64 May 20, 2025
65ecf4e
Merge branch 'main' into rekov2-client
ramonpetgrave64 May 20, 2025
3730364
remove staging and production methods
ramonpetgrave64 May 21, 2025
1e78607
add @pytest.mark.ambient_oidc
ramonpetgrave64 May 21, 2025
53c9113
send the cert, not only the public key
ramonpetgrave64 May 21, 2025
79f967c
abstract the signer fixture
ramonpetgrave64 May 21, 2025
832f87d
reorganize fixtures
ramonpetgrave64 May 21, 2025
e6a6fe3
add tiemout
ramonpetgrave64 May 21, 2025
c546aea
Merge branch 'main' into rekov2-client
ramonpetgrave64 May 23, 2025
5baeb8f
no V002 workaround
ramonpetgrave64 May 23, 2025
4b2a03a
merge updates
ramonpetgrave64 May 23, 2025
73eaf0e
no V002 workaround
ramonpetgrave64 May 23, 2025
34043a2
regorganize tests
ramonpetgrave64 May 23, 2025
a733d5a
use new methods for building requests
ramonpetgrave64 May 23, 2025
90d8244
changelog
ramonpetgrave64 May 23, 2025
b49d8a7
cleanup comment
ramonpetgrave64 May 23, 2025
80422f2
future import
ramonpetgrave64 May 23, 2025
9b60fb6
Rekor: Tweak the log submitter abstraction
jku Jun 4, 2025
d80c25c
tests: Simplify rekorv2 tests
jku Jun 4, 2025
5746d0c
rekor: remove timeout for now
jku Jun 4, 2025
cba9507
Use rekor types from (unreleased) protobuf-specs
jku Jun 6, 2025
5c503a6
Merge remote-tracking branch 'origin/main' into rekov2-client
jku Jun 6, 2025
3d9a1b8
rekor v2: Cope without CreateEntryRequest
jku Jun 6, 2025
324647c
rekor v2: Use the correct keytype in the entry request
jku Jun 6, 2025
68633d4
rekor v2: Workaround mypys issue with enums
jku Jun 6, 2025
9cad1d1
tests: Add missing license
jku Jun 6, 2025
b942f90
Revert "rekor v2: Cope without CreateEntryRequest"
jku Jun 7, 2025
4e9c733
test: remove unnecessary change
jku Jun 7, 2025
afdebab
rekor: Re-use the error class for both clients
jku Jun 7, 2025
f0b79f9
sign: Tweak rekor annotation
jku Jun 7, 2025
1f71c8b
change PR number
ramonpetgrave64 Jun 9, 2025
29ba622
ranem to EntryRequestBody
ramonpetgrave64 Jun 9, 2025
d5b407b
docstring formatting
ramonpetgrave64 Jun 9, 2025
983bfa6
Merge remote-tracking branch 'origin/main' into rekov2-client
jku Jun 10, 2025
4d27fe0
Merge branch 'main' into rekov2-client
jku Jun 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ All versions prior to 0.9.0 are untracked.
[#1402](https://github.com/sigstore/sigstore-python/pull/1402)


* Added a `RekorV2Client` for posting new entries to a Rekor V2 instance.
[#1400](https://github.com/sigstore/sigstore-python/pull/1422)

### Fixed

* Avoid instantiation issues with `TransparencyLogEntry` when `InclusionPromise` is not present.
Expand Down
68 changes: 68 additions & 0 deletions sigstore/_internal/rekor/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,86 @@
APIs for interacting with Rekor.
"""

from __future__ import annotations

import base64
from abc import ABC, abstractmethod
from typing import Any, NewType

import rekor_types
import requests
from cryptography.x509 import Certificate

from sigstore._utils import base64_encode_pem_cert
from sigstore.dsse import Envelope
from sigstore.hashes import Hashed
from sigstore.models import LogEntry

__all__ = [
"_hashedrekord_from_parts",
]

EntryRequestBody = NewType("EntryRequestBody", dict[str, Any])


class RekorClientError(Exception):
"""
A generic error in the Rekor client.
"""

def __init__(self, http_error: requests.HTTPError):
"""
Create a new `RekorClientError` from the given `requests.HTTPError`.
"""
if http_error.response is not None:
try:
error = rekor_types.Error.model_validate_json(http_error.response.text)
super().__init__(f"{error.code}: {error.message}")
except Exception:
super().__init__(
f"Rekor returned an unknown error with HTTP {http_error.response.status_code}"
)
else:
super().__init__(f"Unexpected Rekor error: {http_error}")


class RekorLogSubmitter(ABC):
"""
Abstract class to represent a Rekor log entry submitter.

Intended to be implemented by RekorClient and RekorV2Client.
"""

@abstractmethod
def create_entry(
self,
request: EntryRequestBody,
) -> LogEntry:
"""
Submit the request to Rekor.
"""
pass

@classmethod
@abstractmethod
def _build_hashed_rekord_request(
self, hashed_input: Hashed, signature: bytes, certificate: Certificate
) -> EntryRequestBody:
"""
Construct a hashed rekord request to submit to Rekor.
"""
pass

@classmethod
@abstractmethod
def _build_dsse_request(
self, envelope: Envelope, certificate: Certificate
) -> EntryRequestBody:
"""
Construct a dsse request to submit to Rekor.
"""
pass


# TODO: This should probably live somewhere better.
def _hashedrekord_from_parts(
Expand Down
96 changes: 72 additions & 24 deletions sigstore/_internal/rekor/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

from __future__ import annotations

import base64
import json
import logging
from abc import ABC
Expand All @@ -26,8 +27,17 @@

import rekor_types
import requests
from cryptography.hazmat.primitives import serialization
from cryptography.x509 import Certificate

from sigstore._internal import USER_AGENT
from sigstore._internal.rekor import (
EntryRequestBody,
RekorClientError,
RekorLogSubmitter,
)
from sigstore.dsse import Envelope
from sigstore.hashes import Hashed
from sigstore.models import LogEntry

_logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -62,27 +72,6 @@ def from_response(cls, dict_: dict[str, Any]) -> RekorLogInfo:
)


class RekorClientError(Exception):
"""
A generic error in the Rekor client.
"""

def __init__(self, http_error: requests.HTTPError):
"""
Create a new `RekorClientError` from the given `requests.HTTPError`.
"""
if http_error.response is not None:
try:
error = rekor_types.Error.model_validate_json(http_error.response.text)
super().__init__(f"{error.code}: {error.message}")
except Exception:
super().__init__(
f"Rekor returned an unknown error with HTTP {http_error.response.status_code}"
)
else:
super().__init__(f"Unexpected Rekor error: {http_error}")


class _Endpoint(ABC):
def __init__(self, url: str, session: requests.Session) -> None:
self.url = url
Expand Down Expand Up @@ -145,13 +134,12 @@ def get(

def post(
self,
proposed_entry: rekor_types.Hashedrekord | rekor_types.Dsse,
payload: EntryRequestBody,
) -> LogEntry:
"""
Submit a new entry for inclusion in the Rekor log.
"""

payload = proposed_entry.model_dump(mode="json", by_alias=True)
_logger.debug(f"proposed: {json.dumps(payload)}")

resp: requests.Response = self.session.post(self.url, json=payload)
Expand Down Expand Up @@ -216,7 +204,7 @@ def post(
return oldest_entry


class RekorClient:
class RekorClient(RekorLogSubmitter):
"""The internal Rekor client"""

def __init__(self, url: str) -> None:
Expand Down Expand Up @@ -261,3 +249,63 @@ def log(self) -> RekorLog:
Returns a `RekorLog` adapter for making requests to a Rekor log.
"""
return RekorLog(f"{self.url}/log", session=self.session)

def create_entry(self, request: EntryRequestBody) -> LogEntry:
"""
Submit the request to Rekor.
"""
return self.log.entries.post(request)

def _build_hashed_rekord_request( # type: ignore[override]
self, hashed_input: Hashed, signature: bytes, certificate: Certificate
) -> EntryRequestBody:
"""
Construct a hashed rekord payload to submit to Rekor.
"""
rekord = rekor_types.Hashedrekord(
spec=rekor_types.hashedrekord.HashedrekordV001Schema(
signature=rekor_types.hashedrekord.Signature(
content=base64.b64encode(signature).decode(),
public_key=rekor_types.hashedrekord.PublicKey(
content=base64.b64encode(
certificate.public_bytes(
encoding=serialization.Encoding.PEM
)
).decode()
),
),
data=rekor_types.hashedrekord.Data(
hash=rekor_types.hashedrekord.Hash(
algorithm=hashed_input._as_hashedrekord_algorithm(),
value=hashed_input.digest.hex(),
)
),
),
)
return EntryRequestBody(rekord.model_dump(mode="json", by_alias=True))

def _build_dsse_request( # type: ignore[override]
self, envelope: Envelope, certificate: Certificate
) -> EntryRequestBody:
"""
Construct a dsse request to submit to Rekor.
"""
dsse = rekor_types.Dsse(
spec=rekor_types.dsse.DsseSchema(
# NOTE: mypy can't see that this kwarg is correct due to two interacting
# behaviors/bugs (one pydantic, one datamodel-codegen):
# See: <https://github.com/pydantic/pydantic/discussions/7418#discussioncomment-9024927>
# See: <https://github.com/koxudaxi/datamodel-code-generator/issues/1903>
proposed_content=rekor_types.dsse.ProposedContent( # type: ignore[call-arg]
envelope=envelope.to_json(),
verifiers=[
base64.b64encode(
certificate.public_bytes(
encoding=serialization.Encoding.PEM
)
).decode()
],
),
),
)
return EntryRequestBody(dsse.model_dump(mode="json", by_alias=True))
Loading