From 376361a35a167447783a183cc54eaf44f52a91f5 Mon Sep 17 00:00:00 2001 From: Ramon Petgrave Date: Tue, 20 May 2025 16:18:13 +0000 Subject: [PATCH 01/19] add RekorV2Client and types Signed-off-by: Ramon Petgrave --- sigstore/_internal/rekor/client_v2.py | 156 +++++++++ sigstore/_internal/rekor/v2_types/README.md | 7 + sigstore/_internal/rekor/v2_types/__init__.py | 0 .../_internal/rekor/v2_types/dev/__init__.py | 0 .../rekor/v2_types/dev/sigstore/__init__.py | 0 .../v2_types/dev/sigstore/common/__init__.py | 0 .../dev/sigstore/common/v1/__init__.py | 319 ++++++++++++++++++ .../v2_types/dev/sigstore/rekor/__init__.py | 0 .../dev/sigstore/rekor/v2/__init__.py | 199 +++++++++++ .../_internal/rekor/v2_types/io/__init__.py | 0 .../rekor/v2_types/io/intoto/__init__.py | 64 ++++ 11 files changed, 745 insertions(+) create mode 100644 sigstore/_internal/rekor/client_v2.py create mode 100644 sigstore/_internal/rekor/v2_types/README.md create mode 100644 sigstore/_internal/rekor/v2_types/__init__.py create mode 100644 sigstore/_internal/rekor/v2_types/dev/__init__.py create mode 100644 sigstore/_internal/rekor/v2_types/dev/sigstore/__init__.py create mode 100644 sigstore/_internal/rekor/v2_types/dev/sigstore/common/__init__.py create mode 100644 sigstore/_internal/rekor/v2_types/dev/sigstore/common/v1/__init__.py create mode 100644 sigstore/_internal/rekor/v2_types/dev/sigstore/rekor/__init__.py create mode 100644 sigstore/_internal/rekor/v2_types/dev/sigstore/rekor/v2/__init__.py create mode 100644 sigstore/_internal/rekor/v2_types/io/__init__.py create mode 100644 sigstore/_internal/rekor/v2_types/io/intoto/__init__.py diff --git a/sigstore/_internal/rekor/client_v2.py b/sigstore/_internal/rekor/client_v2.py new file mode 100644 index 000000000..1e3906f81 --- /dev/null +++ b/sigstore/_internal/rekor/client_v2.py @@ -0,0 +1,156 @@ +# Copyright 2025 The Sigstore Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Client implementation for interacting with RekorV2. +""" + +from __future__ import annotations + +import json +import logging +import requests +from cryptography.hazmat.primitives import serialization +from cryptography.x509 import Certificate +import rekor_types + + + +from sigstore._internal import USER_AGENT +from sigstore.hashes import Hashed +from sigstore.models import LogEntry + +from sigstore._internal.rekor.v2_types.dev.sigstore.common.v1 import PublicKeyDetails +from sigstore._internal.rekor.v2_types.dev.sigstore.rekor import v2 + +_logger = logging.getLogger(__name__) + +DEFAULT_REKOR_URL = "https://rekor.sigstore.dev" +STAGING_REKOR_URL = "https://rekor.sigstage.dev" + +DEFAULT_KEY_DETAILS = PublicKeyDetails.PKIX_ECDSA_P384_SHA_256 + + +class RekorV2Client: + """The internal Rekor client for the v2 API""" + + # TODO: implement get_tile, get_entry_bundle, get_checkpoint. + + def __init__(self, base_url: str) -> None: + """ + Create a new `RekorV2Client` from the given URL. + """ + self.url = f"{base_url}/api/v2" + self.session = requests.Session() + self.session.headers.update( + { + "Content-Type": "application/json", + "Accept": "application/json", + "User-Agent": USER_AGENT, + } + ) + + def __del__(self) -> None: + """ + Terminates the underlying network session. + """ + self.session.close() + + def create_entry(self, request: v2.CreateEntryRequest) -> LogEntry: + """ + Submit a new entry for inclusion in the Rekor log. + """ + # There may be a bug in betterproto, where the V_0_0_2 is changed to V002. + # See https://github.com/sigstore/rekor-tiles/blob/bd5893730de581629a5f475923c663f776793496/api/proto/rekor_service.proto#L66. + payload = request.to_dict() + if "hashedRekordRequestV002" in payload: + payload["hashedRekordRequestV0_0_2"] = payload.pop( + "hashedRekordRequestV002" + ) + if "dsseRequestV002" in payload: + payload["dsseRequestV0_0_2"] = payload.pop("dsseRequestV002") + _logger.debug(f"request: {json.dumps(payload)}") + resp = self.session.post(f"{self.url}/log/entries", json=payload) + + try: + resp.raise_for_status() + except requests.HTTPError as http_error: + raise RekorClientError(http_error) + + integrated_entry = resp.json() + _logger.debug(f"integrated: {integrated_entry}") + return LogEntry._from_dict_rekor(integrated_entry) + + @classmethod + def _build_create_entry_request( + cls, + hashed_input: Hashed, + signature: bytes, + certificate: Certificate, + ) -> v2.CreateEntryRequest: + return v2.CreateEntryRequest( + hashed_rekord_request_v0_0_2=v2.HashedRekordRequestV002( + digest=hashed_input.digest, + signature=v2.Signature( + content=signature, + verifier=v2.Verifier( + public_key=v2.PublicKey( + raw_bytes=certificate.public_key().public_bytes( + encoding=serialization.Encoding.DER, + format=serialization.PublicFormat.SubjectPublicKeyInfo, + ) + ), + key_details=DEFAULT_KEY_DETAILS, + ), + ), + ) + ) + + @classmethod + def production(cls) -> RekorV2Client: + """ + Returns a `RekorV2Client` populated with the default Rekor production instance. + """ + return cls( + DEFAULT_REKOR_URL, + ) + + @classmethod + def staging(cls) -> RekorV2Client: + """ + Returns a `RekorV2Client` populated with the default Rekor staging instance. + """ + return cls(STAGING_REKOR_URL) + + +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}") \ No newline at end of file diff --git a/sigstore/_internal/rekor/v2_types/README.md b/sigstore/_internal/rekor/v2_types/README.md new file mode 100644 index 000000000..3d2ba2e5c --- /dev/null +++ b/sigstore/_internal/rekor/v2_types/README.md @@ -0,0 +1,7 @@ +# V2 Types + +These are types meant to be used with RekorV2. + +Generated from running `make python` in sigstore/rekor-tiles to generate (although not checked into git) and copied into here. + +Eventually, we will move these types into sigstore/protobuf-specs. diff --git a/sigstore/_internal/rekor/v2_types/__init__.py b/sigstore/_internal/rekor/v2_types/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/sigstore/_internal/rekor/v2_types/dev/__init__.py b/sigstore/_internal/rekor/v2_types/dev/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/sigstore/_internal/rekor/v2_types/dev/sigstore/__init__.py b/sigstore/_internal/rekor/v2_types/dev/sigstore/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/sigstore/_internal/rekor/v2_types/dev/sigstore/common/__init__.py b/sigstore/_internal/rekor/v2_types/dev/sigstore/common/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/sigstore/_internal/rekor/v2_types/dev/sigstore/common/v1/__init__.py b/sigstore/_internal/rekor/v2_types/dev/sigstore/common/v1/__init__.py new file mode 100644 index 000000000..4a4d9fbf7 --- /dev/null +++ b/sigstore/_internal/rekor/v2_types/dev/sigstore/common/v1/__init__.py @@ -0,0 +1,319 @@ +# Generated by the protocol buffer compiler. DO NOT EDIT! +# sources: sigstore_common.proto +# plugin: python-betterproto +# This file has been @generated + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from dataclasses import dataclass +else: + from pydantic.dataclasses import dataclass + +from datetime import datetime +from typing import ( + List, + Optional, +) + +import betterproto +from pydantic import model_validator +from pydantic.dataclasses import rebuild_dataclass + + +class HashAlgorithm(betterproto.Enum): + """ + Only a subset of the secure hash standard algorithms are supported. + See for more + details. + UNSPECIFIED SHOULD not be used, primary reason for inclusion is to force + any proto JSON serialization to emit the used hash algorithm, as default + option is to *omit* the default value of an enum (which is the first + value, represented by '0'. + """ + + UNSPECIFIED = 0 + SHA2_256 = 1 + SHA2_384 = 2 + SHA2_512 = 3 + SHA3_256 = 4 + SHA3_384 = 5 + + @classmethod + def __get_pydantic_core_schema__(cls, _source_type, _handler): + from pydantic_core import core_schema + + return core_schema.int_schema(ge=0) + + +class PublicKeyDetails(betterproto.Enum): + """ + Details of a specific public key, capturing the the key encoding method, + and signature algorithm. + PublicKeyDetails captures the public key/hash algorithm combinations + recommended in the Sigstore ecosystem. + This is modelled as a linear set as we want to provide a small number of + opinionated options instead of allowing every possible permutation. + Any changes to this enum MUST be reflected in the algorithm registry. + See: docs/algorithm-registry.md + To avoid the possibility of contradicting formats such as PKCS1 with + ED25519 the valid permutations are listed as a linear set instead of a + cartesian set (i.e one combined variable instead of two, one for encoding + and one for the signature algorithm). + """ + + UNSPECIFIED = 0 + PKCS1_RSA_PKCS1V5 = 1 + """RSA""" + + PKCS1_RSA_PSS = 2 + PKIX_RSA_PKCS1V5 = 3 + PKIX_RSA_PSS = 4 + PKIX_RSA_PKCS1V15_2048_SHA256 = 9 + """RSA public key in PKIX format, PKCS#1v1.5 signature""" + + PKIX_RSA_PKCS1V15_3072_SHA256 = 10 + PKIX_RSA_PKCS1V15_4096_SHA256 = 11 + PKIX_RSA_PSS_2048_SHA256 = 16 + """RSA public key in PKIX format, RSASSA-PSS signature""" + + PKIX_RSA_PSS_3072_SHA256 = 17 + PKIX_RSA_PSS_4096_SHA256 = 18 + PKIX_ECDSA_P256_HMAC_SHA_256 = 6 + """ECDSA""" + + PKIX_ECDSA_P256_SHA_256 = 5 + PKIX_ECDSA_P384_SHA_384 = 12 + PKIX_ECDSA_P521_SHA_512 = 13 + PKIX_ED25519 = 7 + """Ed 25519""" + + PKIX_ED25519_PH = 8 + PKIX_ECDSA_P384_SHA_256 = 19 + """ + These algorithms are deprecated and should not be used, but they + were/are being used by most Sigstore clients implementations. + """ + + PKIX_ECDSA_P521_SHA_256 = 20 + LMS_SHA256 = 14 + """ + LMS and LM-OTS + + These keys and signatures may be used by private Sigstore + deployments, but are not currently supported by the public + good instance. + + USER WARNING: LMS and LM-OTS are both stateful signature schemes. + Using them correctly requires discretion and careful consideration + to ensure that individual secret keys are not used more than once. + In addition, LM-OTS is a single-use scheme, meaning that it + MUST NOT be used for more than one signature per LM-OTS key. + If you cannot maintain these invariants, you MUST NOT use these + schemes. + """ + + LMOTS_SHA256 = 15 + + @classmethod + def __get_pydantic_core_schema__(cls, _source_type, _handler): + from pydantic_core import core_schema + + return core_schema.int_schema(ge=0) + + +class SubjectAlternativeNameType(betterproto.Enum): + UNSPECIFIED = 0 + EMAIL = 1 + URI = 2 + OTHER_NAME = 3 + """ + OID 1.3.6.1.4.1.57264.1.7 + See https://github.com/sigstore/fulcio/blob/main/docs/oid-info.md#1361415726417--othername-san + for more details. + """ + + @classmethod + def __get_pydantic_core_schema__(cls, _source_type, _handler): + from pydantic_core import core_schema + + return core_schema.int_schema(ge=0) + + +@dataclass(eq=False, repr=False) +class HashOutput(betterproto.Message): + """ + HashOutput captures a digest of a 'message' (generic octet sequence) + and the corresponding hash algorithm used. + """ + + algorithm: "HashAlgorithm" = betterproto.enum_field(1) + digest: bytes = betterproto.bytes_field(2) + """ + This is the raw octets of the message digest as computed by + the hash algorithm. + """ + + +@dataclass(eq=False, repr=False) +class MessageSignature(betterproto.Message): + """MessageSignature stores the computed signature over a message.""" + + message_digest: "HashOutput" = betterproto.message_field(1) + """ + Message digest can be used to identify the artifact. + Clients MUST NOT attempt to use this digest to verify the associated + signature; it is intended solely for identification. + """ + + signature: bytes = betterproto.bytes_field(2) + """ + The raw bytes as returned from the signature algorithm. + The signature algorithm (and so the format of the signature bytes) + are determined by the contents of the 'verification_material', + either a key-pair or a certificate. If using a certificate, the + certificate contains the required information on the signature + algorithm. + When using a key pair, the algorithm MUST be part of the public + key, which MUST be communicated out-of-band. + """ + + +@dataclass(eq=False, repr=False) +class LogId(betterproto.Message): + """LogId captures the identity of a transparency log.""" + + key_id: bytes = betterproto.bytes_field(1) + """The unique identity of the log, represented by its public key.""" + + +@dataclass(eq=False, repr=False) +class Rfc3161SignedTimestamp(betterproto.Message): + """This message holds a RFC 3161 timestamp.""" + + signed_timestamp: bytes = betterproto.bytes_field(1) + """ + Signed timestamp is the DER encoded TimeStampResponse. + See https://www.rfc-editor.org/rfc/rfc3161.html#section-2.4.2 + """ + + +@dataclass(eq=False, repr=False) +class PublicKey(betterproto.Message): + raw_bytes: Optional[bytes] = betterproto.bytes_field(1, optional=True) + """ + DER-encoded public key, encoding method is specified by the + key_details attribute. + """ + + key_details: "PublicKeyDetails" = betterproto.enum_field(2) + """Key encoding and signature algorithm to use for this key.""" + + valid_for: Optional["TimeRange"] = betterproto.message_field(3, optional=True) + """Optional validity period for this key, *inclusive* of the endpoints.""" + + +@dataclass(eq=False, repr=False) +class PublicKeyIdentifier(betterproto.Message): + """ + PublicKeyIdentifier can be used to identify an (out of band) delivered + key, to verify a signature. + """ + + hint: str = betterproto.string_field(1) + """ + Optional unauthenticated hint on which key to use. + The format of the hint must be agreed upon out of band by the + signer and the verifiers, and so is not subject to this + specification. + Example use-case is to specify the public key to use, from a + trusted key-ring. + Implementors are RECOMMENDED to derive the value from the public + key as described in RFC 6962. + See: + """ + + +@dataclass(eq=False, repr=False) +class ObjectIdentifier(betterproto.Message): + """An ASN.1 OBJECT IDENTIFIER""" + + id: list[int] = betterproto.int32_field(1) + + +@dataclass(eq=False, repr=False) +class ObjectIdentifierValuePair(betterproto.Message): + """An OID and the corresponding (byte) value.""" + + oid: "ObjectIdentifier" = betterproto.message_field(1) + value: bytes = betterproto.bytes_field(2) + + +@dataclass(eq=False, repr=False) +class DistinguishedName(betterproto.Message): + organization: str = betterproto.string_field(1) + common_name: str = betterproto.string_field(2) + + +@dataclass(eq=False, repr=False) +class X509Certificate(betterproto.Message): + raw_bytes: bytes = betterproto.bytes_field(1) + """DER-encoded X.509 certificate.""" + + +@dataclass(eq=False, repr=False) +class SubjectAlternativeName(betterproto.Message): + type: "SubjectAlternativeNameType" = betterproto.enum_field(1) + regexp: Optional[str] = betterproto.string_field(2, optional=True, group="identity") + """ + A regular expression describing the expected value for + the SAN. + """ + + value: Optional[str] = betterproto.string_field(3, optional=True, group="identity") + """The exact value to match against.""" + + @model_validator(mode="after") + def check_oneof(cls, values): + return cls._validate_field_groups(values) + + +@dataclass(eq=False, repr=False) +class X509CertificateChain(betterproto.Message): + """ + A collection of X.509 certificates. + This "chain" can be used in multiple contexts, such as providing a root CA + certificate within a TUF root of trust or multiple untrusted certificates for + the purpose of chain building. + """ + + certificates: list["X509Certificate"] = betterproto.message_field(1) + """ + One or more DER-encoded certificates. + + In some contexts (such as `VerificationMaterial.x509_certificate_chain`), this sequence + has an imposed order. Unless explicitly specified, there is otherwise no + guaranteed order. + """ + + +@dataclass(eq=False, repr=False) +class TimeRange(betterproto.Message): + """ + The time range is closed and includes both the start and end times, + (i.e., [start, end]). + End is optional to be able to capture a period that has started but + has no known end. + """ + + start: datetime = betterproto.message_field(1) + end: Optional[datetime] = betterproto.message_field(2, optional=True) + + +rebuild_dataclass(HashOutput) # type: ignore +rebuild_dataclass(MessageSignature) # type: ignore +rebuild_dataclass(PublicKey) # type: ignore +rebuild_dataclass(ObjectIdentifierValuePair) # type: ignore +rebuild_dataclass(SubjectAlternativeName) # type: ignore +rebuild_dataclass(X509CertificateChain) # type: ignore +rebuild_dataclass(TimeRange) # type: ignore diff --git a/sigstore/_internal/rekor/v2_types/dev/sigstore/rekor/__init__.py b/sigstore/_internal/rekor/v2_types/dev/sigstore/rekor/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/sigstore/_internal/rekor/v2_types/dev/sigstore/rekor/v2/__init__.py b/sigstore/_internal/rekor/v2_types/dev/sigstore/rekor/v2/__init__.py new file mode 100644 index 000000000..9d267496c --- /dev/null +++ b/sigstore/_internal/rekor/v2_types/dev/sigstore/rekor/v2/__init__.py @@ -0,0 +1,199 @@ +# Generated by the protocol buffer compiler. DO NOT EDIT! +# sources: dsse.proto, entry.proto, hashedrekord.proto, rekor_service.proto, verifier.proto +# plugin: python-betterproto +# This file has been @generated + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from dataclasses import dataclass +else: + from pydantic.dataclasses import dataclass + +from typing import ( + List, + Optional, +) + +import betterproto +from pydantic import model_validator +from pydantic.dataclasses import rebuild_dataclass + +from .....io import intoto as ____io_intoto__ +from ...common import v1 as __common_v1__ + + +@dataclass(eq=False, repr=False) +class PublicKey(betterproto.Message): + """PublicKey contains an encoded public key""" + + raw_bytes: bytes = betterproto.bytes_field(1) + """DER-encoded public key""" + + +@dataclass(eq=False, repr=False) +class Verifier(betterproto.Message): + """ + Either a public key or a X.509 cerificiate with an embedded public key + """ + + public_key: Optional["PublicKey"] = betterproto.message_field( + 1, optional=True, group="verifier" + ) + """ + DER-encoded public key. Encoding method is specified by the key_details attribute + """ + + x509_certificate: Optional["__common_v1__.X509Certificate"] = ( + betterproto.message_field(2, optional=True, group="verifier") + ) + """DER-encoded certificate""" + + key_details: "__common_v1__.PublicKeyDetails" = betterproto.enum_field(3) + """Key encoding and signature algorithm to use for this key""" + + @model_validator(mode="after") + def check_oneof(cls, values): + return cls._validate_field_groups(values) + + +@dataclass(eq=False, repr=False) +class Signature(betterproto.Message): + """A signature and an associated verifier""" + + content: bytes = betterproto.bytes_field(1) + verifier: "Verifier" = betterproto.message_field(2) + + +@dataclass(eq=False, repr=False) +class HashedRekordRequestV002(betterproto.Message): + """A request to add a hashedrekord to the log""" + + digest: bytes = betterproto.bytes_field(1) + """The hashed data""" + + signature: "Signature" = betterproto.message_field(2) + """ + A single signature over the hashed data with the verifier needed to validate it + """ + + +@dataclass(eq=False, repr=False) +class HashedRekordLogEntryV002(betterproto.Message): + data: "__common_v1__.HashOutput" = betterproto.message_field(1) + """The hashed data""" + + signature: "Signature" = betterproto.message_field(2) + """ + A single signature over the hashed data with the verifier needed to validate it + """ + + +@dataclass(eq=False, repr=False) +class DsseRequestV002(betterproto.Message): + """A request to add a DSSE entry to the log""" + + envelope: "____io_intoto__.Envelope" = betterproto.message_field(1) + """A DSSE envelope""" + + verifiers: list["Verifier"] = betterproto.message_field(2) + """ + All necessary verification material to verify all signatures embedded in the envelope + """ + + +@dataclass(eq=False, repr=False) +class DsseLogEntryV002(betterproto.Message): + payload_hash: "__common_v1__.HashOutput" = betterproto.message_field(1) + """The hash of the DSSE payload""" + + signatures: list["Signature"] = betterproto.message_field(2) + """ + Signatures and their associated verification material used to verify the payload + """ + + +@dataclass(eq=False, repr=False) +class Entry(betterproto.Message): + """ + Entry is the message that is canonicalized and uploaded to the log. + This format is meant to be compliant with Rekor v1 entries in that + the `apiVersion` and `kind` can be parsed before parsing the spec. + Clients are expected to understand and handle the differences in the + contents of `spec` between Rekor v1 (a polymorphic OpenAPI defintion) + and Rekor v2 (a typed proto defintion). + """ + + kind: str = betterproto.string_field(1) + api_version: str = betterproto.string_field(2) + spec: "Spec" = betterproto.message_field(3) + + +@dataclass(eq=False, repr=False) +class Spec(betterproto.Message): + """Spec contains one of the Rekor entry types.""" + + hashed_rekord_v0_0_2: Optional["HashedRekordLogEntryV002"] = ( + betterproto.message_field(1, optional=True, group="spec") + ) + dsse_v0_0_2: Optional["DsseLogEntryV002"] = betterproto.message_field( + 2, optional=True, group="spec" + ) + + @model_validator(mode="after") + def check_oneof(cls, values): + return cls._validate_field_groups(values) + + +@dataclass(eq=False, repr=False) +class CreateEntryRequest(betterproto.Message): + """Create a new HashedRekord or DSSE""" + + hashed_rekord_request_v0_0_2: Optional["HashedRekordRequestV002"] = ( + betterproto.message_field(1, optional=True, group="spec") + ) + dsse_request_v0_0_2: Optional["DsseRequestV002"] = betterproto.message_field( + 2, optional=True, group="spec" + ) + + @model_validator(mode="after") + def check_oneof(cls, values): + return cls._validate_field_groups(values) + + +@dataclass(eq=False, repr=False) +class TileRequest(betterproto.Message): + """ + Request for a full or partial tile (see https://github.com/C2SP/C2SP/blob/main/tlog-tiles.md#merkle-tree) + """ + + l: int = betterproto.uint32_field(1) + n: str = betterproto.string_field(2) + """ + N must be either an index encoded as zero-padded 3-digit path elements, e.g. "x123/x456/789", + and may end with ".p/", where "" is a uint8 + """ + + +@dataclass(eq=False, repr=False) +class EntryBundleRequest(betterproto.Message): + """ + Request for a full or partial entry bundle (see https://github.com/C2SP/C2SP/blob/main/tlog-tiles.md#log-entries) + """ + + n: str = betterproto.string_field(1) + """ + N must be either an index encoded as zero-padded 3-digit path elements, e.g. "x123/x456/789", + and may end with ".p/", where "" is a uint8 + """ + + +rebuild_dataclass(Verifier) # type: ignore +rebuild_dataclass(Signature) # type: ignore +rebuild_dataclass(HashedRekordRequestV002) # type: ignore +rebuild_dataclass(HashedRekordLogEntryV002) # type: ignore +rebuild_dataclass(DsseRequestV002) # type: ignore +rebuild_dataclass(DsseLogEntryV002) # type: ignore +rebuild_dataclass(Entry) # type: ignore +rebuild_dataclass(Spec) # type: ignore +rebuild_dataclass(CreateEntryRequest) # type: ignore diff --git a/sigstore/_internal/rekor/v2_types/io/__init__.py b/sigstore/_internal/rekor/v2_types/io/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/sigstore/_internal/rekor/v2_types/io/intoto/__init__.py b/sigstore/_internal/rekor/v2_types/io/intoto/__init__.py new file mode 100644 index 000000000..da1edc772 --- /dev/null +++ b/sigstore/_internal/rekor/v2_types/io/intoto/__init__.py @@ -0,0 +1,64 @@ +# Generated by the protocol buffer compiler. DO NOT EDIT! +# sources: envelope.proto +# plugin: python-betterproto +# This file has been @generated + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from dataclasses import dataclass +else: + from pydantic.dataclasses import dataclass + +from typing import List + +import betterproto +from pydantic.dataclasses import rebuild_dataclass + + +@dataclass(eq=False, repr=False) +class Envelope(betterproto.Message): + """An authenticated message of arbitrary type.""" + + payload: bytes = betterproto.bytes_field(1) + """ + Message to be signed. (In JSON, this is encoded as base64.) + REQUIRED. + """ + + payload_type: str = betterproto.string_field(2) + """ + String unambiguously identifying how to interpret payload. + REQUIRED. + """ + + signatures: list["Signature"] = betterproto.message_field(3) + """ + Signature over: + PAE(type, payload) + Where PAE is defined as: + PAE(type, payload) = "DSSEv1" + SP + LEN(type) + SP + type + SP + LEN(payload) + SP + payload + + = concatenation + SP = ASCII space [0x20] + "DSSEv1" = ASCII [0x44, 0x53, 0x53, 0x45, 0x76, 0x31] + LEN(s) = ASCII decimal encoding of the byte length of s, with no leading zeros + REQUIRED (length >= 1). + """ + + +@dataclass(eq=False, repr=False) +class Signature(betterproto.Message): + sig: bytes = betterproto.bytes_field(1) + """ + Signature itself. (In JSON, this is encoded as base64.) + REQUIRED. + """ + + keyid: str = betterproto.string_field(2) + """ + *Unauthenticated* hint identifying which public key was used. + OPTIONAL. + """ + + +rebuild_dataclass(Envelope) # type: ignore From 2983b4ab641d8a8096d1c93ed60734732d0e9251 Mon Sep 17 00:00:00 2001 From: Ramon Petgrave Date: Tue, 20 May 2025 20:52:38 +0000 Subject: [PATCH 02/19] add rekorv2 client + tests Signed-off-by: Ramon Petgrave --- sigstore/_internal/rekor/client_v2.py | 65 ++++-- sigstore/_internal/rekor/v2_types/README.md | 4 +- .../dev/sigstore/common/v1/__init__.py | 1 - .../dev/sigstore/rekor/v2/__init__.py | 3 +- .../rekor/v2_types/io/intoto/__init__.py | 2 - test/unit/internal/rekor/test_client_v2.py | 199 ++++++++++++++++++ 6 files changed, 251 insertions(+), 23 deletions(-) create mode 100644 test/unit/internal/rekor/test_client_v2.py diff --git a/sigstore/_internal/rekor/client_v2.py b/sigstore/_internal/rekor/client_v2.py index 1e3906f81..d9375cd32 100644 --- a/sigstore/_internal/rekor/client_v2.py +++ b/sigstore/_internal/rekor/client_v2.py @@ -20,19 +20,19 @@ import json import logging + +import rekor_types import requests from cryptography.hazmat.primitives import serialization from cryptography.x509 import Certificate -import rekor_types - - from sigstore._internal import USER_AGENT -from sigstore.hashes import Hashed -from sigstore.models import LogEntry - from sigstore._internal.rekor.v2_types.dev.sigstore.common.v1 import PublicKeyDetails from sigstore._internal.rekor.v2_types.dev.sigstore.rekor import v2 +from sigstore._internal.rekor.v2_types.io import intoto as v2_intoto +from sigstore.dsse import Envelope +from sigstore.hashes import Hashed +from sigstore.models import LogEntry _logger = logging.getLogger(__name__) @@ -71,7 +71,8 @@ def create_entry(self, request: v2.CreateEntryRequest) -> LogEntry: """ Submit a new entry for inclusion in the Rekor log. """ - # There may be a bug in betterproto, where the V_0_0_2 is changed to V002. + # TODO: There may be a bug in betterproto, where the V_0_0_2 is changed to V002, + # Or it is an issue with the proto `json_value`. # See https://github.com/sigstore/rekor-tiles/blob/bd5893730de581629a5f475923c663f776793496/api/proto/rekor_service.proto#L66. payload = request.to_dict() if "hashedRekordRequestV002" in payload: @@ -93,20 +94,20 @@ def create_entry(self, request: v2.CreateEntryRequest) -> LogEntry: return LogEntry._from_dict_rekor(integrated_entry) @classmethod - def _build_create_entry_request( + def _build_hashed_rekord_create_entry_request( cls, - hashed_input: Hashed, - signature: bytes, - certificate: Certificate, + artifact_hashed_input: Hashed, + artifact_signature: bytes, + signining_certificate: Certificate, ) -> v2.CreateEntryRequest: return v2.CreateEntryRequest( hashed_rekord_request_v0_0_2=v2.HashedRekordRequestV002( - digest=hashed_input.digest, + digest=artifact_hashed_input.digest, signature=v2.Signature( - content=signature, + content=artifact_signature, verifier=v2.Verifier( public_key=v2.PublicKey( - raw_bytes=certificate.public_key().public_bytes( + raw_bytes=signining_certificate.public_key().public_bytes( encoding=serialization.Encoding.DER, format=serialization.PublicFormat.SubjectPublicKeyInfo, ) @@ -117,6 +118,37 @@ def _build_create_entry_request( ) ) + @classmethod + def _build_dsse_create_entry_request( + cls, envelope: Envelope, signing_certificate: Certificate + ): + return v2.CreateEntryRequest( + dsse_request_v0_0_2=v2.DsseRequestV002( + envelope=v2_intoto.Envelope( + payload=envelope._inner.payload, + payload_type=envelope._inner.payload_type, + signatures=[ + v2_intoto.Signature( + keyid=signature.keyid, + sig=signature.sig, + ) + for signature in envelope._inner.signatures + ], + ), + verifiers=[ + v2.Verifier( + public_key=v2.PublicKey( + raw_bytes=signing_certificate.public_key().public_bytes( + encoding=serialization.Encoding.DER, + format=serialization.PublicFormat.SubjectPublicKeyInfo, + ) + ), + key_details=DEFAULT_KEY_DETAILS, + ) + ], + ) + ) + @classmethod def production(cls) -> RekorV2Client: """ @@ -145,12 +177,11 @@ def __init__(self, http_error: requests.HTTPError): """ if http_error.response is not None: try: - error = rekor_types.Error.model_validate_json( - http_error.response.text) + 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}") \ No newline at end of file + super().__init__(f"Unexpected Rekor error: {http_error}") diff --git a/sigstore/_internal/rekor/v2_types/README.md b/sigstore/_internal/rekor/v2_types/README.md index 3d2ba2e5c..4df971007 100644 --- a/sigstore/_internal/rekor/v2_types/README.md +++ b/sigstore/_internal/rekor/v2_types/README.md @@ -1,7 +1,9 @@ # V2 Types +TODO: Eventually move these types to sigstore/protobuf-specs. + These are types meant to be used with RekorV2. -Generated from running `make python` in sigstore/rekor-tiles to generate (although not checked into git) and copied into here. +Generated from running `make python` in sigstore/rekor-tiles to generate (although not checked into git) and copied into here, **plus** formatting and lint fixes. Eventually, we will move these types into sigstore/protobuf-specs. diff --git a/sigstore/_internal/rekor/v2_types/dev/sigstore/common/v1/__init__.py b/sigstore/_internal/rekor/v2_types/dev/sigstore/common/v1/__init__.py index 4a4d9fbf7..dac084caf 100644 --- a/sigstore/_internal/rekor/v2_types/dev/sigstore/common/v1/__init__.py +++ b/sigstore/_internal/rekor/v2_types/dev/sigstore/common/v1/__init__.py @@ -12,7 +12,6 @@ from datetime import datetime from typing import ( - List, Optional, ) diff --git a/sigstore/_internal/rekor/v2_types/dev/sigstore/rekor/v2/__init__.py b/sigstore/_internal/rekor/v2_types/dev/sigstore/rekor/v2/__init__.py index 9d267496c..1059d072c 100644 --- a/sigstore/_internal/rekor/v2_types/dev/sigstore/rekor/v2/__init__.py +++ b/sigstore/_internal/rekor/v2_types/dev/sigstore/rekor/v2/__init__.py @@ -11,7 +11,6 @@ from pydantic.dataclasses import dataclass from typing import ( - List, Optional, ) @@ -167,7 +166,7 @@ class TileRequest(betterproto.Message): Request for a full or partial tile (see https://github.com/C2SP/C2SP/blob/main/tlog-tiles.md#merkle-tree) """ - l: int = betterproto.uint32_field(1) + l: int = betterproto.uint32_field(1) # noqa: E741 n: str = betterproto.string_field(2) """ N must be either an index encoded as zero-padded 3-digit path elements, e.g. "x123/x456/789", diff --git a/sigstore/_internal/rekor/v2_types/io/intoto/__init__.py b/sigstore/_internal/rekor/v2_types/io/intoto/__init__.py index da1edc772..6edbdd15b 100644 --- a/sigstore/_internal/rekor/v2_types/io/intoto/__init__.py +++ b/sigstore/_internal/rekor/v2_types/io/intoto/__init__.py @@ -10,8 +10,6 @@ else: from pydantic.dataclasses import dataclass -from typing import List - import betterproto from pydantic.dataclasses import rebuild_dataclass diff --git a/test/unit/internal/rekor/test_client_v2.py b/test/unit/internal/rekor/test_client_v2.py new file mode 100644 index 000000000..1d2dae36a --- /dev/null +++ b/test/unit/internal/rekor/test_client_v2.py @@ -0,0 +1,199 @@ +import hashlib +import secrets + +import pytest + +from sigstore import dsse +from sigstore._internal.rekor.client_v2 import ( + DEFAULT_KEY_DETAILS, + Certificate, + Hashed, + LogEntry, + RekorV2Client, + serialization, + v2, + v2_intoto, +) + +# from sigstore.models import rekor_v1 +from sigstore._utils import sha256_digest +from sigstore.sign import ec + +ALPHA_REKOR_V2_URL = "https://log2025-alpha1.rekor.sigstage.dev" + + +# TODO: add staging and production URLs when available. +@pytest.fixture(params=[ALPHA_REKOR_V2_URL]) +def client(request) -> RekorV2Client: + """ + Returns a RekorV2Client. This fixture is paramaterized to return clients with various URLs. + Test fuctions that consume this fixture will run once for each URL. + """ + return RekorV2Client(base_url=request.param) + + +@pytest.fixture() +def sample_hashed_rekord_request_materials( + staging, +) -> tuple[Hashed, bytes, Certificate]: + """ + Creates materials needed for `RekorV2Client._build_hashed_rekord_create_entry_request`. + """ + sign_ctx_cls, _, id_token = staging + with sign_ctx_cls().signer(id_token) as signer: + cert = signer._signing_cert() + + hashed_input = sha256_digest(secrets.token_bytes(32)) + signature = signer._private_key.sign( + hashed_input.digest, ec.ECDSA(hashed_input._as_prehashed()) + ) + return hashed_input, signature, cert + + +@pytest.fixture() +def sample_hashed_rekord_create_entry_request( + sample_hashed_rekord_request_materials, +) -> v2.CreateEntryRequest: + """ + Returns a sample `CreateEntryRequest` for for hashedrekor. + """ + hashed_input, signature, cert = sample_hashed_rekord_request_materials + return RekorV2Client._build_hashed_rekord_create_entry_request( + artifact_hashed_input=hashed_input, + artifact_signature=signature, + signining_certificate=cert, + ) + + +@pytest.fixture() +def sample_dsse_create_entry_request( + sample_dsse_request_materials, +) -> v2.CreateEntryRequest: + """ + Returns a sample `CreateEntryRequest` for for dsse. + """ + envelope, cert = sample_dsse_request_materials + return RekorV2Client._build_dsse_create_entry_request( + envelope=envelope, signing_certificate=cert + ) + + +@pytest.fixture( + params=[ + sample_hashed_rekord_create_entry_request, + sample_dsse_create_entry_request, + ] +) +def sample_create_entry_request(request) -> v2.CreateEntryRequest: + """ + Returns a sample `CreateEntryRequest`, for each of the the params in the supplied fixture. + """ + return request.getfixturevalue(request.param.__name__) + + +@pytest.fixture() +def sample_dsse_request_materials(staging) -> tuple[dsse.Envelope, Certificate]: + """ + Creates materials needed for `RekorV2Client._build_dsse_create_entry_request`. + """ + sign_ctx_cls, _, id_token = staging + with sign_ctx_cls().signer(id_token) as signer: + cert = signer._signing_cert() + stmt = ( + dsse.StatementBuilder() + .subjects( + [ + dsse.Subject( + name="null", digest={"sha256": hashlib.sha256(b"").hexdigest()} + ) + ] + ) + .predicate_type("https://cosign.sigstore.dev/attestation/v1") + .predicate( + { + "Data": "", + "Timestamp": "2023-12-07T00:37:58Z", + } + ) + ).build() + envelope = dsse._sign(key=signer._private_key, stmt=stmt) + return envelope, cert + + +def test_build_hashed_rekord_create_entry_request( + sample_hashed_rekord_request_materials, +): + """ + Ensures that we produce the request `CreateEntryRequest` correctly for hashedrekords. + """ + hashed_input, signature, cert = sample_hashed_rekord_request_materials + expected_request = v2.CreateEntryRequest( + hashed_rekord_request_v0_0_2=v2.HashedRekordRequestV002( + digest=hashed_input.digest, + signature=v2.Signature( + content=signature, + verifier=v2.Verifier( + public_key=v2.PublicKey( + raw_bytes=cert.public_key().public_bytes( + encoding=serialization.Encoding.DER, + format=serialization.PublicFormat.SubjectPublicKeyInfo, + ) + ), + key_details=DEFAULT_KEY_DETAILS, + ), + ), + ) + ) + actual_request = RekorV2Client._build_hashed_rekord_create_entry_request( + artifact_hashed_input=hashed_input, + artifact_signature=signature, + signining_certificate=cert, + ) + assert expected_request == actual_request + + +def test_build_dsse_create_entry_request(sample_dsse_request_materials): + """ + Ensures that we produce the request `CreateEntryRequest` correctly for dsses. + """ + envelope, cert = sample_dsse_request_materials + expected_request = v2.CreateEntryRequest( + dsse_request_v0_0_2=v2.DsseRequestV002( + envelope=v2_intoto.Envelope( + payload=envelope._inner.payload, + payload_type=envelope._inner.payload_type, + signatures=[ + v2_intoto.Signature( + keyid=signature.keyid, + sig=signature.sig, + ) + for signature in envelope._inner.signatures + ], + ), + verifiers=[ + v2.Verifier( + public_key=v2.PublicKey( + raw_bytes=cert.public_key().public_bytes( + encoding=serialization.Encoding.DER, + format=serialization.PublicFormat.SubjectPublicKeyInfo, + ) + ), + key_details=DEFAULT_KEY_DETAILS, + ) + ], + ) + ) + actual_request = RekorV2Client._build_dsse_create_entry_request( + envelope=envelope, signing_certificate=cert + ) + assert expected_request == actual_request + + +def test_create_entry(sample_create_entry_request, client): + """ + Sends a request to RekorV2 and ensure's the response is parseable to a `LogEntry` and a `TransparencyLogEntry`. + """ + log_entry = client.create_entry(sample_create_entry_request) + assert isinstance(log_entry, LogEntry) + # TODO: Pending https://github.com/sigstore/sigstore-python/pull/1370 + # assert isinstance(log_entry._to_rekor(), rekor_v1.TransparencyLogEntry) From 3f94e7a958c68e3d60f54080d758cc5c8c07fdab Mon Sep 17 00:00:00 2001 From: Ramon Petgrave Date: Tue, 20 May 2025 21:48:15 +0000 Subject: [PATCH 03/19] add lots of docstrings Signed-off-by: Ramon Petgrave --- sigstore/_internal/rekor/client_v2.py | 6 ++-- sigstore/_internal/rekor/v2_types/README.md | 4 ++- sigstore/_internal/rekor/v2_types/__init__.py | 3 ++ .../_internal/rekor/v2_types/dev/__init__.py | 3 ++ .../rekor/v2_types/dev/sigstore/__init__.py | 3 ++ .../v2_types/dev/sigstore/common/__init__.py | 3 ++ .../dev/sigstore/common/v1/__init__.py | 28 +++++++++-------- .../v2_types/dev/sigstore/rekor/__init__.py | 3 ++ .../dev/sigstore/rekor/v2/__init__.py | 30 +++++++++++-------- .../_internal/rekor/v2_types/io/__init__.py | 3 ++ .../rekor/v2_types/io/intoto/__init__.py | 6 +++- 11 files changed, 62 insertions(+), 30 deletions(-) diff --git a/sigstore/_internal/rekor/client_v2.py b/sigstore/_internal/rekor/client_v2.py index d9375cd32..bd4727d24 100644 --- a/sigstore/_internal/rekor/client_v2.py +++ b/sigstore/_internal/rekor/client_v2.py @@ -112,7 +112,7 @@ def _build_hashed_rekord_create_entry_request( format=serialization.PublicFormat.SubjectPublicKeyInfo, ) ), - key_details=DEFAULT_KEY_DETAILS, + key_details=DEFAULT_KEY_DETAILS, # type: ignore[arg-type] ), ), ) @@ -121,7 +121,7 @@ def _build_hashed_rekord_create_entry_request( @classmethod def _build_dsse_create_entry_request( cls, envelope: Envelope, signing_certificate: Certificate - ): + ) -> v2.CreateEntryRequest: return v2.CreateEntryRequest( dsse_request_v0_0_2=v2.DsseRequestV002( envelope=v2_intoto.Envelope( @@ -143,7 +143,7 @@ def _build_dsse_create_entry_request( format=serialization.PublicFormat.SubjectPublicKeyInfo, ) ), - key_details=DEFAULT_KEY_DETAILS, + key_details=DEFAULT_KEY_DETAILS, # type: ignore[arg-type] ) ], ) diff --git a/sigstore/_internal/rekor/v2_types/README.md b/sigstore/_internal/rekor/v2_types/README.md index 4df971007..3e53910ce 100644 --- a/sigstore/_internal/rekor/v2_types/README.md +++ b/sigstore/_internal/rekor/v2_types/README.md @@ -4,6 +4,8 @@ TODO: Eventually move these types to sigstore/protobuf-specs. These are types meant to be used with RekorV2. -Generated from running `make python` in sigstore/rekor-tiles to generate (although not checked into git) and copied into here, **plus** formatting and lint fixes. +Generated from running `make python` in sigstore/rekor-tiles to generate (although not checked into git) and copied into here, **plus** formatting and lint fixes (lots of `noqa` comments). + +Linting is still not expected to pass yet, since `interrogate` docstrings for **all** modules and classes. Eventually, we will move these types into sigstore/protobuf-specs. diff --git a/sigstore/_internal/rekor/v2_types/__init__.py b/sigstore/_internal/rekor/v2_types/__init__.py index e69de29bb..8706278bd 100644 --- a/sigstore/_internal/rekor/v2_types/__init__.py +++ b/sigstore/_internal/rekor/v2_types/__init__.py @@ -0,0 +1,3 @@ +""" +Types for RekorV2 +""" diff --git a/sigstore/_internal/rekor/v2_types/dev/__init__.py b/sigstore/_internal/rekor/v2_types/dev/__init__.py index e69de29bb..dd8a6f2ae 100644 --- a/sigstore/_internal/rekor/v2_types/dev/__init__.py +++ b/sigstore/_internal/rekor/v2_types/dev/__init__.py @@ -0,0 +1,3 @@ +""" +Types used for RekorV2 +""" diff --git a/sigstore/_internal/rekor/v2_types/dev/sigstore/__init__.py b/sigstore/_internal/rekor/v2_types/dev/sigstore/__init__.py index e69de29bb..8706278bd 100644 --- a/sigstore/_internal/rekor/v2_types/dev/sigstore/__init__.py +++ b/sigstore/_internal/rekor/v2_types/dev/sigstore/__init__.py @@ -0,0 +1,3 @@ +""" +Types for RekorV2 +""" diff --git a/sigstore/_internal/rekor/v2_types/dev/sigstore/common/__init__.py b/sigstore/_internal/rekor/v2_types/dev/sigstore/common/__init__.py index e69de29bb..87bbea4ee 100644 --- a/sigstore/_internal/rekor/v2_types/dev/sigstore/common/__init__.py +++ b/sigstore/_internal/rekor/v2_types/dev/sigstore/common/__init__.py @@ -0,0 +1,3 @@ +""" +Common types used by Sigstore services +""" diff --git a/sigstore/_internal/rekor/v2_types/dev/sigstore/common/v1/__init__.py b/sigstore/_internal/rekor/v2_types/dev/sigstore/common/v1/__init__.py index dac084caf..52c51fc84 100644 --- a/sigstore/_internal/rekor/v2_types/dev/sigstore/common/v1/__init__.py +++ b/sigstore/_internal/rekor/v2_types/dev/sigstore/common/v1/__init__.py @@ -3,7 +3,11 @@ # plugin: python-betterproto # This file has been @generated -from typing import TYPE_CHECKING +""" +V1 of the common types +""" + +from typing import TYPE_CHECKING, Any if TYPE_CHECKING: from dataclasses import dataclass @@ -39,7 +43,7 @@ class HashAlgorithm(betterproto.Enum): SHA3_384 = 5 @classmethod - def __get_pydantic_core_schema__(cls, _source_type, _handler): + def __get_pydantic_core_schema__(cls, _source_type: Any, _handler: Any) -> Any: from pydantic_core import core_schema return core_schema.int_schema(ge=0) @@ -115,7 +119,7 @@ class PublicKeyDetails(betterproto.Enum): LMOTS_SHA256 = 15 @classmethod - def __get_pydantic_core_schema__(cls, _source_type, _handler): + def __get_pydantic_core_schema__(cls, _source_type: Any, _handler: Any) -> Any: from pydantic_core import core_schema return core_schema.int_schema(ge=0) @@ -133,7 +137,7 @@ class SubjectAlternativeNameType(betterproto.Enum): """ @classmethod - def __get_pydantic_core_schema__(cls, _source_type, _handler): + def __get_pydantic_core_schema__(cls, _source_type: Any, _handler: Any) -> Any: from pydantic_core import core_schema return core_schema.int_schema(ge=0) @@ -273,7 +277,7 @@ class SubjectAlternativeName(betterproto.Message): """The exact value to match against.""" @model_validator(mode="after") - def check_oneof(cls, values): + def check_oneof(cls: Any, values: Any) -> Any: return cls._validate_field_groups(values) @@ -309,10 +313,10 @@ class TimeRange(betterproto.Message): end: Optional[datetime] = betterproto.message_field(2, optional=True) -rebuild_dataclass(HashOutput) # type: ignore -rebuild_dataclass(MessageSignature) # type: ignore -rebuild_dataclass(PublicKey) # type: ignore -rebuild_dataclass(ObjectIdentifierValuePair) # type: ignore -rebuild_dataclass(SubjectAlternativeName) # type: ignore -rebuild_dataclass(X509CertificateChain) # type: ignore -rebuild_dataclass(TimeRange) # type: ignore +rebuild_dataclass(HashOutput) # type: ignore[arg-type] +rebuild_dataclass(MessageSignature) # type: ignore[arg-type] +rebuild_dataclass(PublicKey) # type: ignore[arg-type] +rebuild_dataclass(ObjectIdentifierValuePair) # type: ignore[arg-type] +rebuild_dataclass(SubjectAlternativeName) # type: ignore[arg-type] +rebuild_dataclass(X509CertificateChain) # type: ignore[arg-type] +rebuild_dataclass(TimeRange) # type: ignore[arg-type] diff --git a/sigstore/_internal/rekor/v2_types/dev/sigstore/rekor/__init__.py b/sigstore/_internal/rekor/v2_types/dev/sigstore/rekor/__init__.py index e69de29bb..c8b7267e1 100644 --- a/sigstore/_internal/rekor/v2_types/dev/sigstore/rekor/__init__.py +++ b/sigstore/_internal/rekor/v2_types/dev/sigstore/rekor/__init__.py @@ -0,0 +1,3 @@ +""" +Types used by Rekor +""" diff --git a/sigstore/_internal/rekor/v2_types/dev/sigstore/rekor/v2/__init__.py b/sigstore/_internal/rekor/v2_types/dev/sigstore/rekor/v2/__init__.py index 1059d072c..f3ed48afe 100644 --- a/sigstore/_internal/rekor/v2_types/dev/sigstore/rekor/v2/__init__.py +++ b/sigstore/_internal/rekor/v2_types/dev/sigstore/rekor/v2/__init__.py @@ -3,7 +3,11 @@ # plugin: python-betterproto # This file has been @generated -from typing import TYPE_CHECKING +""" +Types used by RekorV2 +""" + +from typing import TYPE_CHECKING, Any if TYPE_CHECKING: from dataclasses import dataclass @@ -52,7 +56,7 @@ class Verifier(betterproto.Message): """Key encoding and signature algorithm to use for this key""" @model_validator(mode="after") - def check_oneof(cls, values): + def check_oneof(cls: Any, values: Any) -> Any: return cls._validate_field_groups(values) @@ -140,7 +144,7 @@ class Spec(betterproto.Message): ) @model_validator(mode="after") - def check_oneof(cls, values): + def check_oneof(cls: Any, values: Any) -> Any: return cls._validate_field_groups(values) @@ -156,7 +160,7 @@ class CreateEntryRequest(betterproto.Message): ) @model_validator(mode="after") - def check_oneof(cls, values): + def check_oneof(cls: Any, values: Any) -> Any: return cls._validate_field_groups(values) @@ -187,12 +191,12 @@ class EntryBundleRequest(betterproto.Message): """ -rebuild_dataclass(Verifier) # type: ignore -rebuild_dataclass(Signature) # type: ignore -rebuild_dataclass(HashedRekordRequestV002) # type: ignore -rebuild_dataclass(HashedRekordLogEntryV002) # type: ignore -rebuild_dataclass(DsseRequestV002) # type: ignore -rebuild_dataclass(DsseLogEntryV002) # type: ignore -rebuild_dataclass(Entry) # type: ignore -rebuild_dataclass(Spec) # type: ignore -rebuild_dataclass(CreateEntryRequest) # type: ignore +rebuild_dataclass(Verifier) # type: ignore[arg-type] +rebuild_dataclass(Signature) # type: ignore[arg-type] +rebuild_dataclass(HashedRekordRequestV002) # type: ignore[arg-type] +rebuild_dataclass(HashedRekordLogEntryV002) # type: ignore[arg-type] +rebuild_dataclass(DsseRequestV002) # type: ignore[arg-type] +rebuild_dataclass(DsseLogEntryV002) # type: ignore[arg-type] +rebuild_dataclass(Entry) # type: ignore[arg-type] +rebuild_dataclass(Spec) # type: ignore[arg-type] +rebuild_dataclass(CreateEntryRequest) # type: ignore[arg-type] diff --git a/sigstore/_internal/rekor/v2_types/io/__init__.py b/sigstore/_internal/rekor/v2_types/io/__init__.py index e69de29bb..75a02cef7 100644 --- a/sigstore/_internal/rekor/v2_types/io/__init__.py +++ b/sigstore/_internal/rekor/v2_types/io/__init__.py @@ -0,0 +1,3 @@ +""" +Types used in log entries +""" diff --git a/sigstore/_internal/rekor/v2_types/io/intoto/__init__.py b/sigstore/_internal/rekor/v2_types/io/intoto/__init__.py index 6edbdd15b..cba742504 100644 --- a/sigstore/_internal/rekor/v2_types/io/intoto/__init__.py +++ b/sigstore/_internal/rekor/v2_types/io/intoto/__init__.py @@ -3,6 +3,10 @@ # plugin: python-betterproto # This file has been @generated +""" +Types related to intoto +""" + from typing import TYPE_CHECKING if TYPE_CHECKING: @@ -59,4 +63,4 @@ class Signature(betterproto.Message): """ -rebuild_dataclass(Envelope) # type: ignore +rebuild_dataclass(Envelope) # type: ignore[arg-type] From d04325611af7a9e1424008d1bf88933c31ebd257 Mon Sep 17 00:00:00 2001 From: Ramon Petgrave Date: Tue, 20 May 2025 22:00:31 +0000 Subject: [PATCH 04/19] xfail on local and staging Signed-off-by: Ramon Petgrave --- test/unit/internal/rekor/test_client_v2.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/test/unit/internal/rekor/test_client_v2.py b/test/unit/internal/rekor/test_client_v2.py index 1d2dae36a..86048a9b3 100644 --- a/test/unit/internal/rekor/test_client_v2.py +++ b/test/unit/internal/rekor/test_client_v2.py @@ -6,6 +6,8 @@ from sigstore import dsse from sigstore._internal.rekor.client_v2 import ( DEFAULT_KEY_DETAILS, + DEFAULT_REKOR_URL, + STAGING_REKOR_URL, Certificate, Hashed, LogEntry, @@ -20,10 +22,19 @@ from sigstore.sign import ec ALPHA_REKOR_V2_URL = "https://log2025-alpha1.rekor.sigstage.dev" +LOCAL_REKOR_V2_URL = "http://localhost:3000" -# TODO: add staging and production URLs when available. -@pytest.fixture(params=[ALPHA_REKOR_V2_URL]) +# TODO: add staging and production URLs when available, +# and local after using scaffolding/setup-sigstore-env action +@pytest.fixture( + params=[ + ALPHA_REKOR_V2_URL, + pytest.param(STAGING_REKOR_URL, marks=pytest.mark.xfail), + pytest.param(DEFAULT_REKOR_URL, marks=pytest.mark.xfail), + pytest.param(LOCAL_REKOR_V2_URL, marks=pytest.mark.xfail), + ] +) def client(request) -> RekorV2Client: """ Returns a RekorV2Client. This fixture is paramaterized to return clients with various URLs. From 1a55117a5e9ee6bd9a4afbca89bf63e1a2f9e814 Mon Sep 17 00:00:00 2001 From: Ramon Petgrave Date: Tue, 20 May 2025 22:08:03 +0000 Subject: [PATCH 05/19] add cahngelog Signed-off-by: Ramon Petgrave --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d392b8cf..3904f56c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ All versions prior to 0.9.0 are untracked. * Added support for ed25519 keys. [#1377](https://github.com/sigstore/sigstore-python/pull/1377) +* Added a `RekorV2Client` for posting new entries to a Rekor V2 instance. + ### Fixed * Avoid instantiation issues with `TransparencyLogEntry` when `InclusionPromise` is not present. From 37303645559c75629f7b4a2055805d5356a5c905 Mon Sep 17 00:00:00 2001 From: Ramon Petgrave Date: Wed, 21 May 2025 15:11:31 +0000 Subject: [PATCH 06/19] remove staging and production methods Signed-off-by: Ramon Petgrave --- sigstore/_internal/rekor/client_v2.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/sigstore/_internal/rekor/client_v2.py b/sigstore/_internal/rekor/client_v2.py index bd4727d24..a7aaf5689 100644 --- a/sigstore/_internal/rekor/client_v2.py +++ b/sigstore/_internal/rekor/client_v2.py @@ -149,22 +149,6 @@ def _build_dsse_create_entry_request( ) ) - @classmethod - def production(cls) -> RekorV2Client: - """ - Returns a `RekorV2Client` populated with the default Rekor production instance. - """ - return cls( - DEFAULT_REKOR_URL, - ) - - @classmethod - def staging(cls) -> RekorV2Client: - """ - Returns a `RekorV2Client` populated with the default Rekor staging instance. - """ - return cls(STAGING_REKOR_URL) - class RekorClientError(Exception): """ From 1e78607aae3743a2eba102fed0d792dad88586a4 Mon Sep 17 00:00:00 2001 From: Ramon Petgrave Date: Wed, 21 May 2025 15:17:08 +0000 Subject: [PATCH 07/19] add @pytest.mark.ambient_oidc Signed-off-by: Ramon Petgrave --- test/unit/internal/rekor/test_client_v2.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/unit/internal/rekor/test_client_v2.py b/test/unit/internal/rekor/test_client_v2.py index 86048a9b3..20781b36e 100644 --- a/test/unit/internal/rekor/test_client_v2.py +++ b/test/unit/internal/rekor/test_client_v2.py @@ -131,6 +131,7 @@ def sample_dsse_request_materials(staging) -> tuple[dsse.Envelope, Certificate]: return envelope, cert +@pytest.mark.ambient_oidc def test_build_hashed_rekord_create_entry_request( sample_hashed_rekord_request_materials, ): @@ -163,6 +164,7 @@ def test_build_hashed_rekord_create_entry_request( assert expected_request == actual_request +@pytest.mark.ambient_oidc def test_build_dsse_create_entry_request(sample_dsse_request_materials): """ Ensures that we produce the request `CreateEntryRequest` correctly for dsses. @@ -200,6 +202,7 @@ def test_build_dsse_create_entry_request(sample_dsse_request_materials): assert expected_request == actual_request +@pytest.mark.ambient_oidc def test_create_entry(sample_create_entry_request, client): """ Sends a request to RekorV2 and ensure's the response is parseable to a `LogEntry` and a `TransparencyLogEntry`. From 53c91131a4179d972202de01d70f3305fa20697b Mon Sep 17 00:00:00 2001 From: Ramon Petgrave Date: Wed, 21 May 2025 15:30:42 +0000 Subject: [PATCH 08/19] send the cert, not only the public key Signed-off-by: Ramon Petgrave --- sigstore/_internal/rekor/client_v2.py | 20 +++++++++----------- test/unit/internal/rekor/test_client_v2.py | 19 +++++++------------ 2 files changed, 16 insertions(+), 23 deletions(-) diff --git a/sigstore/_internal/rekor/client_v2.py b/sigstore/_internal/rekor/client_v2.py index a7aaf5689..c7a89b3a5 100644 --- a/sigstore/_internal/rekor/client_v2.py +++ b/sigstore/_internal/rekor/client_v2.py @@ -27,7 +27,7 @@ from cryptography.x509 import Certificate from sigstore._internal import USER_AGENT -from sigstore._internal.rekor.v2_types.dev.sigstore.common.v1 import PublicKeyDetails +from sigstore._internal.rekor.v2_types.dev.sigstore.common import v1 as common_v1 from sigstore._internal.rekor.v2_types.dev.sigstore.rekor import v2 from sigstore._internal.rekor.v2_types.io import intoto as v2_intoto from sigstore.dsse import Envelope @@ -39,7 +39,7 @@ DEFAULT_REKOR_URL = "https://rekor.sigstore.dev" STAGING_REKOR_URL = "https://rekor.sigstage.dev" -DEFAULT_KEY_DETAILS = PublicKeyDetails.PKIX_ECDSA_P384_SHA_256 +DEFAULT_KEY_DETAILS = common_v1.PublicKeyDetails.PKIX_ECDSA_P384_SHA_256 class RekorV2Client: @@ -98,7 +98,7 @@ def _build_hashed_rekord_create_entry_request( cls, artifact_hashed_input: Hashed, artifact_signature: bytes, - signining_certificate: Certificate, + signing_certificate: Certificate, ) -> v2.CreateEntryRequest: return v2.CreateEntryRequest( hashed_rekord_request_v0_0_2=v2.HashedRekordRequestV002( @@ -106,10 +106,9 @@ def _build_hashed_rekord_create_entry_request( signature=v2.Signature( content=artifact_signature, verifier=v2.Verifier( - public_key=v2.PublicKey( - raw_bytes=signining_certificate.public_key().public_bytes( - encoding=serialization.Encoding.DER, - format=serialization.PublicFormat.SubjectPublicKeyInfo, + x509_certificate=common_v1.X509Certificate( + raw_bytes=signing_certificate.public_bytes( + encoding=serialization.Encoding.DER ) ), key_details=DEFAULT_KEY_DETAILS, # type: ignore[arg-type] @@ -137,10 +136,9 @@ def _build_dsse_create_entry_request( ), verifiers=[ v2.Verifier( - public_key=v2.PublicKey( - raw_bytes=signing_certificate.public_key().public_bytes( - encoding=serialization.Encoding.DER, - format=serialization.PublicFormat.SubjectPublicKeyInfo, + x509_certificate=common_v1.X509Certificate( + raw_bytes=signing_certificate.public_bytes( + encoding=serialization.Encoding.DER ) ), key_details=DEFAULT_KEY_DETAILS, # type: ignore[arg-type] diff --git a/test/unit/internal/rekor/test_client_v2.py b/test/unit/internal/rekor/test_client_v2.py index 20781b36e..90119bc39 100644 --- a/test/unit/internal/rekor/test_client_v2.py +++ b/test/unit/internal/rekor/test_client_v2.py @@ -12,6 +12,7 @@ Hashed, LogEntry, RekorV2Client, + common_v1, serialization, v2, v2_intoto, @@ -72,7 +73,7 @@ def sample_hashed_rekord_create_entry_request( return RekorV2Client._build_hashed_rekord_create_entry_request( artifact_hashed_input=hashed_input, artifact_signature=signature, - signining_certificate=cert, + signing_certificate=cert, ) @@ -145,11 +146,8 @@ def test_build_hashed_rekord_create_entry_request( signature=v2.Signature( content=signature, verifier=v2.Verifier( - public_key=v2.PublicKey( - raw_bytes=cert.public_key().public_bytes( - encoding=serialization.Encoding.DER, - format=serialization.PublicFormat.SubjectPublicKeyInfo, - ) + x509_certificate=common_v1.X509Certificate( + raw_bytes=cert.public_bytes(encoding=serialization.Encoding.DER) ), key_details=DEFAULT_KEY_DETAILS, ), @@ -159,7 +157,7 @@ def test_build_hashed_rekord_create_entry_request( actual_request = RekorV2Client._build_hashed_rekord_create_entry_request( artifact_hashed_input=hashed_input, artifact_signature=signature, - signining_certificate=cert, + signing_certificate=cert, ) assert expected_request == actual_request @@ -185,11 +183,8 @@ def test_build_dsse_create_entry_request(sample_dsse_request_materials): ), verifiers=[ v2.Verifier( - public_key=v2.PublicKey( - raw_bytes=cert.public_key().public_bytes( - encoding=serialization.Encoding.DER, - format=serialization.PublicFormat.SubjectPublicKeyInfo, - ) + x509_certificate=common_v1.X509Certificate( + raw_bytes=cert.public_bytes(encoding=serialization.Encoding.DER) ), key_details=DEFAULT_KEY_DETAILS, ) From 79f967ca6671be8b00c87c66c36c4caf2b76fbbe Mon Sep 17 00:00:00 2001 From: Ramon Petgrave Date: Wed, 21 May 2025 15:42:16 +0000 Subject: [PATCH 09/19] abstract the signer fixture Signed-off-by: Ramon Petgrave --- test/unit/internal/rekor/test_client_v2.py | 27 +++++++++++++--------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/test/unit/internal/rekor/test_client_v2.py b/test/unit/internal/rekor/test_client_v2.py index 90119bc39..16fd90361 100644 --- a/test/unit/internal/rekor/test_client_v2.py +++ b/test/unit/internal/rekor/test_client_v2.py @@ -45,18 +45,25 @@ def client(request) -> RekorV2Client: @pytest.fixture() -def sample_hashed_rekord_request_materials( - staging, -) -> tuple[Hashed, bytes, Certificate]: +def sample_signer(staging): """ - Creates materials needed for `RekorV2Client._build_hashed_rekord_create_entry_request`. + Returns a `Signer`. """ sign_ctx_cls, _, id_token = staging with sign_ctx_cls().signer(id_token) as signer: - cert = signer._signing_cert() + return signer + +@pytest.fixture() +def sample_hashed_rekord_request_materials( + sample_signer, +) -> tuple[Hashed, bytes, Certificate]: + """ + Creates materials needed for `RekorV2Client._build_hashed_rekord_create_entry_request`. + """ + cert = sample_signer._signing_cert() hashed_input = sha256_digest(secrets.token_bytes(32)) - signature = signer._private_key.sign( + signature = sample_signer._private_key.sign( hashed_input.digest, ec.ECDSA(hashed_input._as_prehashed()) ) return hashed_input, signature, cert @@ -104,13 +111,11 @@ def sample_create_entry_request(request) -> v2.CreateEntryRequest: @pytest.fixture() -def sample_dsse_request_materials(staging) -> tuple[dsse.Envelope, Certificate]: +def sample_dsse_request_materials(sample_signer) -> tuple[dsse.Envelope, Certificate]: """ Creates materials needed for `RekorV2Client._build_dsse_create_entry_request`. """ - sign_ctx_cls, _, id_token = staging - with sign_ctx_cls().signer(id_token) as signer: - cert = signer._signing_cert() + cert = sample_signer._signing_cert() stmt = ( dsse.StatementBuilder() .subjects( @@ -128,7 +133,7 @@ def sample_dsse_request_materials(staging) -> tuple[dsse.Envelope, Certificate]: } ) ).build() - envelope = dsse._sign(key=signer._private_key, stmt=stmt) + envelope = dsse._sign(key=sample_signer._private_key, stmt=stmt) return envelope, cert From 832f87de751d2ecbd4691de6c090b5d72eda3e66 Mon Sep 17 00:00:00 2001 From: Ramon Petgrave Date: Wed, 21 May 2025 15:43:34 +0000 Subject: [PATCH 10/19] reorganize fixtures Signed-off-by: Ramon Petgrave --- test/unit/internal/rekor/test_client_v2.py | 54 +++++++++++----------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/test/unit/internal/rekor/test_client_v2.py b/test/unit/internal/rekor/test_client_v2.py index 16fd90361..fe88fc0b0 100644 --- a/test/unit/internal/rekor/test_client_v2.py +++ b/test/unit/internal/rekor/test_client_v2.py @@ -69,6 +69,33 @@ def sample_hashed_rekord_request_materials( return hashed_input, signature, cert +@pytest.fixture() +def sample_dsse_request_materials(sample_signer) -> tuple[dsse.Envelope, Certificate]: + """ + Creates materials needed for `RekorV2Client._build_dsse_create_entry_request`. + """ + cert = sample_signer._signing_cert() + stmt = ( + dsse.StatementBuilder() + .subjects( + [ + dsse.Subject( + name="null", digest={"sha256": hashlib.sha256(b"").hexdigest()} + ) + ] + ) + .predicate_type("https://cosign.sigstore.dev/attestation/v1") + .predicate( + { + "Data": "", + "Timestamp": "2023-12-07T00:37:58Z", + } + ) + ).build() + envelope = dsse._sign(key=sample_signer._private_key, stmt=stmt) + return envelope, cert + + @pytest.fixture() def sample_hashed_rekord_create_entry_request( sample_hashed_rekord_request_materials, @@ -110,33 +137,6 @@ def sample_create_entry_request(request) -> v2.CreateEntryRequest: return request.getfixturevalue(request.param.__name__) -@pytest.fixture() -def sample_dsse_request_materials(sample_signer) -> tuple[dsse.Envelope, Certificate]: - """ - Creates materials needed for `RekorV2Client._build_dsse_create_entry_request`. - """ - cert = sample_signer._signing_cert() - stmt = ( - dsse.StatementBuilder() - .subjects( - [ - dsse.Subject( - name="null", digest={"sha256": hashlib.sha256(b"").hexdigest()} - ) - ] - ) - .predicate_type("https://cosign.sigstore.dev/attestation/v1") - .predicate( - { - "Data": "", - "Timestamp": "2023-12-07T00:37:58Z", - } - ) - ).build() - envelope = dsse._sign(key=sample_signer._private_key, stmt=stmt) - return envelope, cert - - @pytest.mark.ambient_oidc def test_build_hashed_rekord_create_entry_request( sample_hashed_rekord_request_materials, From e6a6fe3b9c171ef30c39fad16189530228c4465c Mon Sep 17 00:00:00 2001 From: Ramon Petgrave Date: Wed, 21 May 2025 15:58:15 +0000 Subject: [PATCH 11/19] add tiemout Signed-off-by: Ramon Petgrave --- sigstore/_internal/rekor/client_v2.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/sigstore/_internal/rekor/client_v2.py b/sigstore/_internal/rekor/client_v2.py index c7a89b3a5..7e985b058 100644 --- a/sigstore/_internal/rekor/client_v2.py +++ b/sigstore/_internal/rekor/client_v2.py @@ -39,6 +39,10 @@ DEFAULT_REKOR_URL = "https://rekor.sigstore.dev" STAGING_REKOR_URL = "https://rekor.sigstage.dev" +# TODO: Link to merged documenation. +# See https://github.com/sigstore/rekor-tiles/pull/255/files#diff-eb568acf84d583e4d3734b07773e96912277776bad39c560392aa33ea2cf2210R196 +CREATE_ENTRIES_TIMEOUT_SECONDS = 10 + DEFAULT_KEY_DETAILS = common_v1.PublicKeyDetails.PKIX_ECDSA_P384_SHA_256 @@ -82,7 +86,11 @@ def create_entry(self, request: v2.CreateEntryRequest) -> LogEntry: if "dsseRequestV002" in payload: payload["dsseRequestV0_0_2"] = payload.pop("dsseRequestV002") _logger.debug(f"request: {json.dumps(payload)}") - resp = self.session.post(f"{self.url}/log/entries", json=payload) + resp = self.session.post( + f"{self.url}/log/entries", + json=payload, + timeout=CREATE_ENTRIES_TIMEOUT_SECONDS, + ) try: resp.raise_for_status() From 5baeb8f902a76f2fe7a87ab7ebcba8b708b4cbf9 Mon Sep 17 00:00:00 2001 From: Ramon Petgrave Date: Fri, 23 May 2025 17:52:51 +0000 Subject: [PATCH 12/19] no V002 workaround Signed-off-by: Ramon Petgrave --- sigstore/_internal/rekor/client_v2.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/sigstore/_internal/rekor/client_v2.py b/sigstore/_internal/rekor/client_v2.py index 7e985b058..5839b49f3 100644 --- a/sigstore/_internal/rekor/client_v2.py +++ b/sigstore/_internal/rekor/client_v2.py @@ -79,13 +79,7 @@ def create_entry(self, request: v2.CreateEntryRequest) -> LogEntry: # Or it is an issue with the proto `json_value`. # See https://github.com/sigstore/rekor-tiles/blob/bd5893730de581629a5f475923c663f776793496/api/proto/rekor_service.proto#L66. payload = request.to_dict() - if "hashedRekordRequestV002" in payload: - payload["hashedRekordRequestV0_0_2"] = payload.pop( - "hashedRekordRequestV002" - ) - if "dsseRequestV002" in payload: - payload["dsseRequestV0_0_2"] = payload.pop("dsseRequestV002") - _logger.debug(f"request: {json.dumps(payload)}") + _logger.debug(f"proposed: {json.dumps(payload)}") resp = self.session.post( f"{self.url}/log/entries", json=payload, From 4b2a03a7f14a2bb07c839f841eb258de95ca6b71 Mon Sep 17 00:00:00 2001 From: Ramon Petgrave Date: Fri, 23 May 2025 20:40:34 +0000 Subject: [PATCH 13/19] merge updates Signed-off-by: Ramon Petgrave --- sigstore/_internal/rekor/__init__.py | 36 +++++++ sigstore/_internal/rekor/client_v2.py | 42 +++++--- test/unit/internal/rekor/test_client_v2.py | 119 +++++++++++---------- 3 files changed, 129 insertions(+), 68 deletions(-) diff --git a/sigstore/_internal/rekor/__init__.py b/sigstore/_internal/rekor/__init__.py index 7c1d3e364..04033fbfc 100644 --- a/sigstore/_internal/rekor/__init__.py +++ b/sigstore/_internal/rekor/__init__.py @@ -17,18 +17,54 @@ """ import base64 +from abc import ABC, abstractmethod import rekor_types from cryptography.x509 import Certificate +from sigstore._internal.rekor.v2_types.dev.sigstore.rekor import v2 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", ] +class RekorLogSubmitter(ABC): + @abstractmethod + def create_entry( + self, + request: rekor_types.Hashedrekord | rekor_types.Dsse | v2.CreateEntryRequest, + ) -> LogEntry: + """ + Submit the request to Rekor. + """ + pass + + @classmethod + @abstractmethod + def _build_hashed_rekord_request( + self, hashed_input: Hashed, signature: bytes, certificate: Certificate + ) -> rekor_types.Hashedrekord | v2.CreateEntryRequest: + """ + Construct a hashed rekord request to submit to Rekor. + """ + pass + + @classmethod + @abstractmethod + def _build_dsse_request( + self, envelope: Envelope, certificate: Certificate + ) -> rekor_types.Dsse | v2.CreateEntryRequest: + """ + Construct a dsse request to submit to Rekor. + """ + pass + + # TODO: This should probably live somewhere better. def _hashedrekord_from_parts( cert: Certificate, sig: bytes, hashed: Hashed diff --git a/sigstore/_internal/rekor/client_v2.py b/sigstore/_internal/rekor/client_v2.py index 5839b49f3..c2d10ccac 100644 --- a/sigstore/_internal/rekor/client_v2.py +++ b/sigstore/_internal/rekor/client_v2.py @@ -27,6 +27,7 @@ from cryptography.x509 import Certificate from sigstore._internal import USER_AGENT +from sigstore._internal.rekor import RekorLogSubmitter from sigstore._internal.rekor.v2_types.dev.sigstore.common import v1 as common_v1 from sigstore._internal.rekor.v2_types.dev.sigstore.rekor import v2 from sigstore._internal.rekor.v2_types.io import intoto as v2_intoto @@ -41,12 +42,12 @@ # TODO: Link to merged documenation. # See https://github.com/sigstore/rekor-tiles/pull/255/files#diff-eb568acf84d583e4d3734b07773e96912277776bad39c560392aa33ea2cf2210R196 -CREATE_ENTRIES_TIMEOUT_SECONDS = 10 +CREATE_ENTRIES_TIMEOUT_SECONDS = 20 DEFAULT_KEY_DETAILS = common_v1.PublicKeyDetails.PKIX_ECDSA_P384_SHA_256 -class RekorV2Client: +class RekorV2Client(RekorLogSubmitter): """The internal Rekor client for the v2 API""" # TODO: implement get_tile, get_entry_bundle, get_checkpoint. @@ -71,7 +72,8 @@ def __del__(self) -> None: """ self.session.close() - def create_entry(self, request: v2.CreateEntryRequest) -> LogEntry: + # TODO: when we remove the original Rekor client, remove the type ignore here + def create_entry(self, request: v2.CreateEntryRequest) -> LogEntry: # type: ignore[override] """ Submit a new entry for inclusion in the Rekor log. """ @@ -79,7 +81,13 @@ def create_entry(self, request: v2.CreateEntryRequest) -> LogEntry: # Or it is an issue with the proto `json_value`. # See https://github.com/sigstore/rekor-tiles/blob/bd5893730de581629a5f475923c663f776793496/api/proto/rekor_service.proto#L66. payload = request.to_dict() - _logger.debug(f"proposed: {json.dumps(payload)}") + if "hashedRekordRequestV002" in payload: + payload["hashedRekordRequestV0_0_2"] = payload.pop( + "hashedRekordRequestV002" + ) + if "dsseRequestV002" in payload: + payload["dsseRequestV0_0_2"] = payload.pop("dsseRequestV002") + _logger.debug(f"request: {json.dumps(payload)}") resp = self.session.post( f"{self.url}/log/entries", json=payload, @@ -96,20 +104,23 @@ def create_entry(self, request: v2.CreateEntryRequest) -> LogEntry: return LogEntry._from_dict_rekor(integrated_entry) @classmethod - def _build_hashed_rekord_create_entry_request( + def _build_hashed_rekord_request( cls, - artifact_hashed_input: Hashed, - artifact_signature: bytes, - signing_certificate: Certificate, + hashed_input: Hashed, + signature: bytes, + certificate: Certificate, ) -> v2.CreateEntryRequest: + """ + Construct a hashed rekord request to submit to Rekor. + """ return v2.CreateEntryRequest( hashed_rekord_request_v0_0_2=v2.HashedRekordRequestV002( - digest=artifact_hashed_input.digest, + digest=hashed_input.digest, signature=v2.Signature( - content=artifact_signature, + content=signature, verifier=v2.Verifier( x509_certificate=common_v1.X509Certificate( - raw_bytes=signing_certificate.public_bytes( + raw_bytes=certificate.public_bytes( encoding=serialization.Encoding.DER ) ), @@ -120,9 +131,12 @@ def _build_hashed_rekord_create_entry_request( ) @classmethod - def _build_dsse_create_entry_request( - cls, envelope: Envelope, signing_certificate: Certificate + def _build_dsse_request( + cls, envelope: Envelope, certificate: Certificate ) -> v2.CreateEntryRequest: + """ + Construct a dsse request to submit to Rekor. + """ return v2.CreateEntryRequest( dsse_request_v0_0_2=v2.DsseRequestV002( envelope=v2_intoto.Envelope( @@ -139,7 +153,7 @@ def _build_dsse_create_entry_request( verifiers=[ v2.Verifier( x509_certificate=common_v1.X509Certificate( - raw_bytes=signing_certificate.public_bytes( + raw_bytes=certificate.public_bytes( encoding=serialization.Encoding.DER ) ), diff --git a/test/unit/internal/rekor/test_client_v2.py b/test/unit/internal/rekor/test_client_v2.py index fe88fc0b0..59f31570f 100644 --- a/test/unit/internal/rekor/test_client_v2.py +++ b/test/unit/internal/rekor/test_client_v2.py @@ -1,13 +1,16 @@ import hashlib +import os import secrets import pytest +from id import ( + detect_credential, +) from sigstore import dsse +from sigstore._internal.rekor.client import STAGING_REKOR_URL from sigstore._internal.rekor.client_v2 import ( DEFAULT_KEY_DETAILS, - DEFAULT_REKOR_URL, - STAGING_REKOR_URL, Certificate, Hashed, LogEntry, @@ -17,10 +20,11 @@ v2, v2_intoto, ) - -# from sigstore.models import rekor_v1 +from sigstore._internal.trust import ClientTrustConfig from sigstore._utils import sha256_digest -from sigstore.sign import ec +from sigstore.models import rekor_v1 +from sigstore.oidc import _DEFAULT_AUDIENCE, IdentityToken +from sigstore.sign import SigningContext, ec ALPHA_REKOR_V2_URL = "https://log2025-alpha1.rekor.sigstage.dev" LOCAL_REKOR_V2_URL = "http://localhost:3000" @@ -29,12 +33,13 @@ # TODO: add staging and production URLs when available, # and local after using scaffolding/setup-sigstore-env action @pytest.fixture( + scope="session", params=[ ALPHA_REKOR_V2_URL, pytest.param(STAGING_REKOR_URL, marks=pytest.mark.xfail), - pytest.param(DEFAULT_REKOR_URL, marks=pytest.mark.xfail), - pytest.param(LOCAL_REKOR_V2_URL, marks=pytest.mark.xfail), - ] + # pytest.param(DEFAULT_REKOR_URL, marks=pytest.mark.xfail), + # pytest.param(LOCAL_REKOR_V2_URL, marks=pytest.mark.xfail), + ], ) def client(request) -> RekorV2Client: """ @@ -44,37 +49,46 @@ def client(request) -> RekorV2Client: return RekorV2Client(base_url=request.param) -@pytest.fixture() -def sample_signer(staging): +@pytest.fixture(scope="session") +def sample_cert_and_private_key() -> tuple[Certificate, ec.EllipticCurvePrivateKey]: """ - Returns a `Signer`. + Returns a sample Certificate and ec.EllipticCurvePrivateKey. """ - sign_ctx_cls, _, id_token = staging - with sign_ctx_cls().signer(id_token) as signer: - return signer + # Detect env variable for local interactive tests. + token = os.getenv("SIGSTORE_IDENTITY_TOKEN_staging") + if not token: + # If the variable is not defined, try getting an ambient token. + token = detect_credential(_DEFAULT_AUDIENCE) + with SigningContext.from_trust_config(ClientTrustConfig.staging()).signer( + IdentityToken(token) + ) as signer: + return signer._signing_cert(), signer._private_key -@pytest.fixture() + +@pytest.fixture(scope="session") def sample_hashed_rekord_request_materials( - sample_signer, + sample_cert_and_private_key, ) -> tuple[Hashed, bytes, Certificate]: """ Creates materials needed for `RekorV2Client._build_hashed_rekord_create_entry_request`. """ - cert = sample_signer._signing_cert() + cert, private_key = sample_cert_and_private_key hashed_input = sha256_digest(secrets.token_bytes(32)) - signature = sample_signer._private_key.sign( + signature = private_key.sign( hashed_input.digest, ec.ECDSA(hashed_input._as_prehashed()) ) return hashed_input, signature, cert -@pytest.fixture() -def sample_dsse_request_materials(sample_signer) -> tuple[dsse.Envelope, Certificate]: +@pytest.fixture(scope="session") +def sample_dsse_request_materials( + sample_cert_and_private_key, +) -> tuple[dsse.Envelope, Certificate]: """ Creates materials needed for `RekorV2Client._build_dsse_create_entry_request`. """ - cert = sample_signer._signing_cert() + cert, private_key = sample_cert_and_private_key stmt = ( dsse.StatementBuilder() .subjects( @@ -92,11 +106,11 @@ def sample_dsse_request_materials(sample_signer) -> tuple[dsse.Envelope, Certifi } ) ).build() - envelope = dsse._sign(key=sample_signer._private_key, stmt=stmt) + envelope = dsse._sign(key=private_key, stmt=stmt) return envelope, cert -@pytest.fixture() +@pytest.fixture(scope="session") def sample_hashed_rekord_create_entry_request( sample_hashed_rekord_request_materials, ) -> v2.CreateEntryRequest: @@ -104,14 +118,14 @@ def sample_hashed_rekord_create_entry_request( Returns a sample `CreateEntryRequest` for for hashedrekor. """ hashed_input, signature, cert = sample_hashed_rekord_request_materials - return RekorV2Client._build_hashed_rekord_create_entry_request( - artifact_hashed_input=hashed_input, - artifact_signature=signature, - signing_certificate=cert, + return RekorV2Client._build_hashed_rekord_request( + hashed_input=hashed_input, + signature=signature, + certificate=cert, ) -@pytest.fixture() +@pytest.fixture(scope="session") def sample_dsse_create_entry_request( sample_dsse_request_materials, ) -> v2.CreateEntryRequest: @@ -119,22 +133,7 @@ def sample_dsse_create_entry_request( Returns a sample `CreateEntryRequest` for for dsse. """ envelope, cert = sample_dsse_request_materials - return RekorV2Client._build_dsse_create_entry_request( - envelope=envelope, signing_certificate=cert - ) - - -@pytest.fixture( - params=[ - sample_hashed_rekord_create_entry_request, - sample_dsse_create_entry_request, - ] -) -def sample_create_entry_request(request) -> v2.CreateEntryRequest: - """ - Returns a sample `CreateEntryRequest`, for each of the the params in the supplied fixture. - """ - return request.getfixturevalue(request.param.__name__) + return RekorV2Client._build_dsse_request(envelope=envelope, certificate=cert) @pytest.mark.ambient_oidc @@ -159,10 +158,10 @@ def test_build_hashed_rekord_create_entry_request( ), ) ) - actual_request = RekorV2Client._build_hashed_rekord_create_entry_request( - artifact_hashed_input=hashed_input, - artifact_signature=signature, - signing_certificate=cert, + actual_request = RekorV2Client._build_hashed_rekord_request( + hashed_input=hashed_input, + signature=signature, + certificate=cert, ) assert expected_request == actual_request @@ -196,18 +195,30 @@ def test_build_dsse_create_entry_request(sample_dsse_request_materials): ], ) ) - actual_request = RekorV2Client._build_dsse_create_entry_request( - envelope=envelope, signing_certificate=cert + actual_request = RekorV2Client._build_dsse_request( + envelope=envelope, certificate=cert ) assert expected_request == actual_request +@pytest.mark.parametrize( + "sample_create_entry_request", + [ + sample_hashed_rekord_create_entry_request.__name__, + sample_dsse_create_entry_request.__name__, + ], +) @pytest.mark.ambient_oidc -def test_create_entry(sample_create_entry_request, client): +def test_create_entry( + request: pytest.FixtureRequest, + sample_create_entry_request: str, + client: RekorV2Client, +): """ Sends a request to RekorV2 and ensure's the response is parseable to a `LogEntry` and a `TransparencyLogEntry`. """ - log_entry = client.create_entry(sample_create_entry_request) + log_entry = client.create_entry( + request.getfixturevalue(sample_create_entry_request) + ) assert isinstance(log_entry, LogEntry) - # TODO: Pending https://github.com/sigstore/sigstore-python/pull/1370 - # assert isinstance(log_entry._to_rekor(), rekor_v1.TransparencyLogEntry) + assert isinstance(log_entry._to_rekor(), rekor_v1.TransparencyLogEntry) From 73eaf0e4764c4c3d520170d2992fa9825ac209a3 Mon Sep 17 00:00:00 2001 From: Ramon Petgrave Date: Fri, 23 May 2025 17:52:51 +0000 Subject: [PATCH 14/19] no V002 workaround Signed-off-by: Ramon Petgrave --- sigstore/_internal/rekor/client_v2.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/sigstore/_internal/rekor/client_v2.py b/sigstore/_internal/rekor/client_v2.py index c2d10ccac..91d271beb 100644 --- a/sigstore/_internal/rekor/client_v2.py +++ b/sigstore/_internal/rekor/client_v2.py @@ -81,13 +81,7 @@ def create_entry(self, request: v2.CreateEntryRequest) -> LogEntry: # type: ign # Or it is an issue with the proto `json_value`. # See https://github.com/sigstore/rekor-tiles/blob/bd5893730de581629a5f475923c663f776793496/api/proto/rekor_service.proto#L66. payload = request.to_dict() - if "hashedRekordRequestV002" in payload: - payload["hashedRekordRequestV0_0_2"] = payload.pop( - "hashedRekordRequestV002" - ) - if "dsseRequestV002" in payload: - payload["dsseRequestV0_0_2"] = payload.pop("dsseRequestV002") - _logger.debug(f"request: {json.dumps(payload)}") + _logger.debug(f"proposed: {json.dumps(payload)}") resp = self.session.post( f"{self.url}/log/entries", json=payload, From 34043a22cd899aa3db0efb64089a60c9b6df6250 Mon Sep 17 00:00:00 2001 From: Ramon Petgrave Date: Fri, 23 May 2025 20:49:06 +0000 Subject: [PATCH 15/19] regorganize tests Signed-off-by: Ramon Petgrave --- test/unit/conftest.py | 2 +- test/unit/internal/rekor/test_client_v2.py | 29 +++++++--------------- 2 files changed, 10 insertions(+), 21 deletions(-) diff --git a/test/unit/conftest.py b/test/unit/conftest.py index 768625cb9..b9ef29891 100644 --- a/test/unit/conftest.py +++ b/test/unit/conftest.py @@ -214,7 +214,7 @@ def ctx_cls(): return ctx_cls, IdentityToken(token) -@pytest.fixture +@pytest.fixture(scope="session") def staging() -> tuple[type[SigningContext], type[Verifier], IdentityToken]: """ Returns a SigningContext, Verifier, and IdentityToken for the staging environment. diff --git a/test/unit/internal/rekor/test_client_v2.py b/test/unit/internal/rekor/test_client_v2.py index 59f31570f..76c531626 100644 --- a/test/unit/internal/rekor/test_client_v2.py +++ b/test/unit/internal/rekor/test_client_v2.py @@ -1,14 +1,10 @@ import hashlib -import os import secrets import pytest -from id import ( - detect_credential, -) from sigstore import dsse -from sigstore._internal.rekor.client import STAGING_REKOR_URL +from sigstore._internal.rekor.client import DEFAULT_REKOR_URL, STAGING_REKOR_URL from sigstore._internal.rekor.client_v2 import ( DEFAULT_KEY_DETAILS, Certificate, @@ -20,11 +16,9 @@ v2, v2_intoto, ) -from sigstore._internal.trust import ClientTrustConfig from sigstore._utils import sha256_digest from sigstore.models import rekor_v1 -from sigstore.oidc import _DEFAULT_AUDIENCE, IdentityToken -from sigstore.sign import SigningContext, ec +from sigstore.sign import ec ALPHA_REKOR_V2_URL = "https://log2025-alpha1.rekor.sigstage.dev" LOCAL_REKOR_V2_URL = "http://localhost:3000" @@ -37,8 +31,8 @@ params=[ ALPHA_REKOR_V2_URL, pytest.param(STAGING_REKOR_URL, marks=pytest.mark.xfail), - # pytest.param(DEFAULT_REKOR_URL, marks=pytest.mark.xfail), - # pytest.param(LOCAL_REKOR_V2_URL, marks=pytest.mark.xfail), + pytest.param(DEFAULT_REKOR_URL, marks=pytest.mark.skip), + pytest.param(LOCAL_REKOR_V2_URL, marks=pytest.mark.skip), ], ) def client(request) -> RekorV2Client: @@ -50,19 +44,14 @@ def client(request) -> RekorV2Client: @pytest.fixture(scope="session") -def sample_cert_and_private_key() -> tuple[Certificate, ec.EllipticCurvePrivateKey]: +def sample_cert_and_private_key( + staging, +) -> tuple[Certificate, ec.EllipticCurvePrivateKey]: """ Returns a sample Certificate and ec.EllipticCurvePrivateKey. """ - # Detect env variable for local interactive tests. - token = os.getenv("SIGSTORE_IDENTITY_TOKEN_staging") - if not token: - # If the variable is not defined, try getting an ambient token. - token = detect_credential(_DEFAULT_AUDIENCE) - - with SigningContext.from_trust_config(ClientTrustConfig.staging()).signer( - IdentityToken(token) - ) as signer: + sign_ctx_cls, _, identity = staging + with sign_ctx_cls().signer(identity) as signer: return signer._signing_cert(), signer._private_key From a733d5a10ba81206e27643d78c739f455dfc497f Mon Sep 17 00:00:00 2001 From: Ramon Petgrave Date: Fri, 23 May 2025 21:09:35 +0000 Subject: [PATCH 16/19] use new methods for building requests Signed-off-by: Ramon Petgrave --- sigstore/_internal/rekor/client.py | 70 +++++++++++++++++++++++++++++- sigstore/sign.py | 45 +++---------------- 2 files changed, 75 insertions(+), 40 deletions(-) diff --git a/sigstore/_internal/rekor/client.py b/sigstore/_internal/rekor/client.py index 80801579d..8d011b223 100644 --- a/sigstore/_internal/rekor/client.py +++ b/sigstore/_internal/rekor/client.py @@ -18,6 +18,7 @@ from __future__ import annotations +import base64 import json import logging from abc import ABC @@ -26,8 +27,15 @@ 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 ( + RekorLogSubmitter, +) +from sigstore.dsse import Envelope +from sigstore.hashes import Hashed from sigstore.models import LogEntry _logger = logging.getLogger(__name__) @@ -216,7 +224,7 @@ def post( return oldest_entry -class RekorClient: +class RekorClient(RekorLogSubmitter): """The internal Rekor client""" def __init__(self, url: str) -> None: @@ -261,3 +269,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( # type: ignore[override] + self, request: rekor_types.Hashedrekord | rekor_types.Dsse + ) -> 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 + ) -> rekor_types.Hashedrekord: + """ + Construct a hashed rekord request to submit to Rekor. + """ + return 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(), + ) + ), + ), + ) + + def _build_dsse_request( # type: ignore[override] + self, envelope: Envelope, certificate: Certificate + ) -> rekor_types.Dsse: + """ + Construct a dsse request to submit to Rekor. + """ + return 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: + # See: + 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() + ], + ), + ), + ) diff --git a/sigstore/sign.py b/sigstore/sign.py index 643cc7960..d806070bb 100644 --- a/sigstore/sign.py +++ b/sigstore/sign.py @@ -38,7 +38,6 @@ from __future__ import annotations -import base64 import logging from collections.abc import Iterator from contextlib import contextmanager @@ -47,7 +46,7 @@ import cryptography.x509 as x509 import rekor_types -from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import ec from cryptography.x509.oid import NameOID from sigstore_protobuf_specs.dev.sigstore.common.v1 import ( @@ -182,7 +181,7 @@ def _finalize_sign( Perform the common "finalizing" steps in a Sigstore signing flow. """ # Submit the proposed entry to the transparency log - entry = self._signing_ctx._rekor.log.entries.post(proposed_entry) + entry = self._signing_ctx._rekor.create_entry(proposed_entry) _logger.debug(f"Transparency log entry created with index: {entry.log_index}") @@ -211,26 +210,12 @@ def sign_dsse( """ cert = self._signing_cert() - # Prepare inputs - b64_cert = base64.b64encode( - cert.public_bytes(encoding=serialization.Encoding.PEM) - ) - # Sign the statement, producing a DSSE envelope content = dsse._sign(self._private_key, input_) # Create the proposed DSSE log entry - proposed_entry = 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: - # See: - proposed_content=rekor_types.dsse.ProposedContent( # type: ignore[call-arg] - envelope=content.to_json(), - verifiers=[b64_cert.decode()], - ), - ), + proposed_entry = self._signing_ctx._rekor._build_dsse_request( + envelope=content, certificate=cert ) return self._finalize_sign(cert, content, proposed_entry) @@ -255,11 +240,6 @@ def sign_artifact( cert = self._signing_cert() - # Prepare inputs - b64_cert = base64.b64encode( - cert.public_bytes(encoding=serialization.Encoding.PEM) - ) - # Sign artifact hashed_input = sha256_digest(input_) @@ -276,21 +256,8 @@ def sign_artifact( ) # Create the proposed hashedrekord entry - proposed_entry = rekor_types.Hashedrekord( - spec=rekor_types.hashedrekord.HashedrekordV001Schema( - signature=rekor_types.hashedrekord.Signature( - content=base64.b64encode(artifact_signature).decode(), - public_key=rekor_types.hashedrekord.PublicKey( - content=b64_cert.decode() - ), - ), - data=rekor_types.hashedrekord.Data( - hash=rekor_types.hashedrekord.Hash( - algorithm=hashed_input._as_hashedrekord_algorithm(), - value=hashed_input.digest.hex(), - ) - ), - ), + proposed_entry = self._signing_ctx._rekor._build_hashed_rekord_request( + hashed_input=hashed_input, signature=artifact_signature, certificate=cert ) return self._finalize_sign(cert, content, proposed_entry) From 90d82440ece451f457e4c52485a78659abb9d0fe Mon Sep 17 00:00:00 2001 From: Ramon Petgrave Date: Fri, 23 May 2025 21:11:42 +0000 Subject: [PATCH 17/19] changelog Signed-off-by: Ramon Petgrave --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd279d047..61ada3b14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ All versions prior to 0.9.0 are untracked. [#1377](https://github.com/sigstore/sigstore-python/pull/1377) * Added a `RekorV2Client` for posting new entries to a Rekor V2 instance. + [#1400](https://github.com/sigstore/sigstore-python/pull/1400) ### Fixed From b49d8a79a1098e8188d08373e75310de1b4cf30a Mon Sep 17 00:00:00 2001 From: Ramon Petgrave Date: Fri, 23 May 2025 21:13:20 +0000 Subject: [PATCH 18/19] cleanup comment Signed-off-by: Ramon Petgrave --- sigstore/_internal/rekor/client_v2.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/sigstore/_internal/rekor/client_v2.py b/sigstore/_internal/rekor/client_v2.py index 91d271beb..1709ed098 100644 --- a/sigstore/_internal/rekor/client_v2.py +++ b/sigstore/_internal/rekor/client_v2.py @@ -77,9 +77,6 @@ def create_entry(self, request: v2.CreateEntryRequest) -> LogEntry: # type: ign """ Submit a new entry for inclusion in the Rekor log. """ - # TODO: There may be a bug in betterproto, where the V_0_0_2 is changed to V002, - # Or it is an issue with the proto `json_value`. - # See https://github.com/sigstore/rekor-tiles/blob/bd5893730de581629a5f475923c663f776793496/api/proto/rekor_service.proto#L66. payload = request.to_dict() _logger.debug(f"proposed: {json.dumps(payload)}") resp = self.session.post( From 80422f2157ddc2629a8eb9bb75aab788d48a92c5 Mon Sep 17 00:00:00 2001 From: Ramon Petgrave Date: Fri, 23 May 2025 21:18:26 +0000 Subject: [PATCH 19/19] future import Signed-off-by: Ramon Petgrave --- sigstore/_internal/rekor/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sigstore/_internal/rekor/__init__.py b/sigstore/_internal/rekor/__init__.py index 04033fbfc..3cc68fde7 100644 --- a/sigstore/_internal/rekor/__init__.py +++ b/sigstore/_internal/rekor/__init__.py @@ -16,6 +16,8 @@ APIs for interacting with Rekor. """ +from __future__ import annotations + import base64 from abc import ABC, abstractmethod