Skip to content

Conversation

@ramonpetgrave64
Copy link

@ramonpetgrave64 ramonpetgrave64 commented May 13, 2025

Client support for Rekor V2: sigstore-python #289

Fixes #167

Pending #166

Adds codegen for rekorv2

Testing

Steps in the codegen workflow

make dev
source env/bin/activate
./codegen/codegen.sh

generated file

# generated by datamodel-codegen:
#   filename:  rekor_service.swagger.json
#   version:   0.30.1

from __future__ import annotations

from enum import Enum
from typing import Any, List, Optional

from pydantic import BaseModel, ConfigDict, Field, RootModel, StrictInt, StrictStr


class Model(RootModel[Any]):
    model_config = ConfigDict(
        populate_by_name=True,
    )
    root: Any


class IointotoSignature(BaseModel):
    model_config = ConfigDict(
        populate_by_name=True,
    )
    sig: Optional[str] = Field(
        default=None,
        description="Signature itself. (In JSON, this is encoded as base64.)\nREQUIRED.",
    )
    keyid: Optional[StrictStr] = Field(
        default=None,
        description="*Unauthenticated* hint identifying which public key was used.\nOPTIONAL.",
    )


class ProtobufAny(BaseModel):
    """`Any` contains an arbitrary serialized protocol buffer message along with a
    URL that describes the type of the serialized message.

    Protobuf library provides support to pack/unpack Any values in the form
    of utility functions or additional generated methods of the Any type.

    Example 1: Pack and unpack a message in C++.

        Foo foo = ...;
        Any any;
        any.PackFrom(foo);
        ...
        if (any.UnpackTo(&foo)) {
          ...
        }

    Example 2: Pack and unpack a message in Java.

        Foo foo = ...;
        Any any = Any.pack(foo);
        ...
        if (any.is(Foo.class)) {
          foo = any.unpack(Foo.class);
        }
        // or ...
        if (any.isSameTypeAs(Foo.getDefaultInstance())) {
          foo = any.unpack(Foo.getDefaultInstance());
        }

     Example 3: Pack and unpack a message in Python.

        foo = Foo(...)
        any = Any()
        any.Pack(foo)
        ...
        if any.Is(Foo.DESCRIPTOR):
          any.Unpack(foo)
          ...

     Example 4: Pack and unpack a message in Go

         foo := &pb.Foo{...}
         any, err := anypb.New(foo)
         if err != nil {
           ...
         }
         ...
         foo := &pb.Foo{}
         if err := any.UnmarshalTo(foo); err != nil {
           ...
         }

    The pack methods provided by protobuf library will by default use
    'type.googleapis.com/full.type.name' as the type URL and the unpack
    methods only use the fully qualified type name after the last '/'
    in the type URL, for example "foo.bar.com/x/y.z" will yield type
    name "y.z".

    JSON
    ====
    The JSON representation of an `Any` value uses the regular
    representation of the deserialized, embedded message, with an
    additional field `@type` which contains the type URL. Example:

        package google.profile;
        message Person {
          string first_name = 1;
          string last_name = 2;
        }

        {
          "@type": "type.googleapis.com/google.profile.Person",
          "firstName": <string>,
          "lastName": <string>
        }

    If the embedded message type is well-known and has a custom JSON
    representation, that representation will be embedded adding a field
    `value` which holds the custom JSON in addition to the `@type`
    field. Example (for message [google.protobuf.Duration][]):

        {
          "@type": "type.googleapis.com/google.protobuf.Duration",
          "value": "1.212s"
        }
    """

    model_config = ConfigDict(
        populate_by_name=True,
    )
    field_type: Optional[StrictStr] = Field(
        default=None,
        alias="@type",
        description='A URL/resource name that uniquely identifies the type of the serialized\nprotocol buffer message. This string must contain at least\none "/" character. The last segment of the URL\'s path must represent\nthe fully qualified name of the type (as in\n`path/google.protobuf.Duration`). The name should be in a canonical form\n(e.g., leading "." is not accepted).\n\nIn practice, teams usually precompile into the binary all types that they\nexpect it to use in the context of Any. However, for URLs which use the\nscheme `http`, `https`, or no scheme, one can optionally set up a type\nserver that maps type URLs to message definitions as follows:\n\n* If no scheme is provided, `https` is assumed.\n* An HTTP GET on the URL must yield a [google.protobuf.Type][]\n  value in binary format, or produce an error.\n* Applications are allowed to cache lookup results based on the\n  URL, or have them precompiled into a binary to avoid any\n  lookup. Therefore, binary compatibility needs to be preserved\n  on changes to types. (Use versioned type names to manage\n  breaking changes.)\n\nNote: this functionality is not currently available in the official\nprotobuf release, and it is not used for type URLs beginning with\ntype.googleapis.com. As of May 2023, there are no widely used type server\nimplementations and no plans to implement one.\n\nSchemes other than `http`, `https` (or the empty scheme) might be\nused with implementation specific semantics.',
    )


class Rekorv2PublicKey(BaseModel):
    model_config = ConfigDict(
        populate_by_name=True,
    )
    raw_bytes: str = Field(..., alias="rawBytes", title="DER-encoded public key")


class RpcStatus(BaseModel):
    model_config = ConfigDict(
        populate_by_name=True,
    )
    code: Optional[StrictInt] = None
    message: Optional[StrictStr] = None
    details: Optional[List[ProtobufAny]] = None


class V1Checkpoint(BaseModel):
    model_config = ConfigDict(
        populate_by_name=True,
    )
    envelope: StrictStr


class V1InclusionPromise(BaseModel):
    """The inclusion promise is calculated by Rekor. It's calculated as a
    signature over a canonical JSON serialization of the persisted entry, the
    log ID, log index and the integration timestamp.
    See https://github.com/sigstore/rekor/blob/a6e58f72b6b18cc06cefe61808efd562b9726330/pkg/api/entries.go#L54
    The format of the signature depends on the transparency log's public key.
    If the signature algorithm requires a hash function and/or a signature
    scheme (e.g. RSA) those has to be retrieved out-of-band from the log's
    operators, together with the public key.
    This is used to verify the integration timestamp's value and that the log
    has promised to include the entry.
    """

    model_config = ConfigDict(
        populate_by_name=True,
    )
    signed_entry_timestamp: str = Field(..., alias="signedEntryTimestamp")


class V1InclusionProof(BaseModel):
    """InclusionProof is the proof returned from the transparency log. Can
    be used for offline or online verification against the log.
    """

    model_config = ConfigDict(
        populate_by_name=True,
    )
    log_index: StrictStr = Field(
        ...,
        alias="logIndex",
        description="The index of the entry in the tree it was written to.",
    )
    root_hash: str = Field(
        ...,
        alias="rootHash",
        description="The hash digest stored at the root of the merkle tree at the time\nthe proof was generated.",
    )
    tree_size: StrictStr = Field(
        ...,
        alias="treeSize",
        description="The size of the merkle tree at the time the proof was generated.",
    )
    hashes: List[str] = Field(
        ...,
        description="A list of hashes required to compute the inclusion proof, sorted\nin order from leaf to root.\nNote that leaf and root hashes are not included.\nThe root hash is available separately in this message, and the\nleaf hash should be calculated by the client.",
    )
    checkpoint: V1Checkpoint = Field(
        ...,
        description="Signature of the tree head, as of the time of this proof was\ngenerated. See above info on 'Checkpoint' for more details.",
    )


class V1KindVersion(BaseModel):
    """KindVersion contains the entry's kind and api version."""

    model_config = ConfigDict(
        populate_by_name=True,
    )
    kind: StrictStr = Field(
        ...,
        title="Kind is the type of entry being stored in the log.\nSee here for a list: https://github.com/sigstore/rekor/tree/main/pkg/types",
    )
    version: StrictStr = Field(..., description="The specific api version of the type.")


class V1LogId(BaseModel):
    """LogId captures the identity of a transparency log."""

    model_config = ConfigDict(
        populate_by_name=True,
    )
    key_id: str = Field(
        ...,
        alias="keyId",
        description="The unique identity of the log, represented by its public key.",
    )


class V1PublicKeyDetails(str, 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).

     - PKCS1_RSA_PKCS1V5: RSA

    See RFC8017
     - PKCS1_RSA_PSS: See RFC8017
     - PKIX_RSA_PKCS1V15_2048_SHA256: RSA public key in PKIX format, PKCS#1v1.5 signature
     - PKIX_RSA_PSS_2048_SHA256: RSA public key in PKIX format, RSASSA-PSS signature

    See RFC4055
     - PKIX_ECDSA_P256_HMAC_SHA_256: ECDSA

    See RFC6979
     - PKIX_ECDSA_P256_SHA_256: See NIST FIPS 186-4
     - PKIX_ED25519: Ed 25519

    See RFC8032
     - LMS_SHA256: 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.
    """

    public_key_details_unspecified = "PUBLIC_KEY_DETAILS_UNSPECIFIED"
    pkcs1_rsa_pkcs1_v5 = "PKCS1_RSA_PKCS1V5"
    pkcs1_rsa_pss = "PKCS1_RSA_PSS"
    pkix_rsa_pkcs1_v5 = "PKIX_RSA_PKCS1V5"
    pkix_rsa_pss = "PKIX_RSA_PSS"
    pkix_rsa_pkcs1_v15_2048_sha256 = "PKIX_RSA_PKCS1V15_2048_SHA256"
    pkix_rsa_pkcs1_v15_3072_sha256 = "PKIX_RSA_PKCS1V15_3072_SHA256"
    pkix_rsa_pkcs1_v15_4096_sha256 = "PKIX_RSA_PKCS1V15_4096_SHA256"
    pkix_rsa_pss_2048_sha256 = "PKIX_RSA_PSS_2048_SHA256"
    pkix_rsa_pss_3072_sha256 = "PKIX_RSA_PSS_3072_SHA256"
    pkix_rsa_pss_4096_sha256 = "PKIX_RSA_PSS_4096_SHA256"
    pkix_ecdsa_p256_hmac_sha_256 = "PKIX_ECDSA_P256_HMAC_SHA_256"
    pkix_ecdsa_p256_sha_256 = "PKIX_ECDSA_P256_SHA_256"
    pkix_ecdsa_p384_sha_384 = "PKIX_ECDSA_P384_SHA_384"
    pkix_ecdsa_p521_sha_512 = "PKIX_ECDSA_P521_SHA_512"
    pkix_ed25519 = "PKIX_ED25519"
    pkix_ed25519_ph = "PKIX_ED25519_PH"
    lms_sha256 = "LMS_SHA256"
    lmots_sha256 = "LMOTS_SHA256"


class V1TransparencyLogEntry(BaseModel):
    """TransparencyLogEntry captures all the details required from Rekor to
    reconstruct an entry, given that the payload is provided via other means.
    This type can easily be created from the existing response from Rekor.
    Future iterations could rely on Rekor returning the minimal set of
    attributes (excluding the payload) that are required for verifying the
    inclusion promise. The inclusion promise (called SignedEntryTimestamp in
    the response from Rekor) is similar to a Signed Certificate Timestamp
    as described here https://www.rfc-editor.org/rfc/rfc6962.html#section-3.2.
    """

    model_config = ConfigDict(
        populate_by_name=True,
    )
    log_index: StrictStr = Field(
        ...,
        alias="logIndex",
        description="The global index of the entry, used when querying the log by index.",
    )
    log_id: V1LogId = Field(..., alias="logId", description="The unique identifier of the log.")
    kind_version: V1KindVersion = Field(
        ...,
        alias="kindVersion",
        description="The kind (type) and version of the object associated with this\nentry. These values are required to construct the entry during\nverification.",
    )
    integrated_time: StrictStr = Field(
        ...,
        alias="integratedTime",
        description="The UNIX timestamp from the log when the entry was persisted.\nThe integration time MUST NOT be trusted if inclusion_promise\nis omitted.",
    )
    inclusion_promise: Optional[V1InclusionPromise] = Field(
        default=None,
        alias="inclusionPromise",
        description="The inclusion promise/signed entry timestamp from the log.\nRequired for v0.1 bundles, and MUST be verified.\nOptional for >= v0.2 bundles if another suitable source of\ntime is present (such as another source of signed time,\nor the current system time for long-lived certificates).\nMUST be verified if no other suitable source of time is present,\nand SHOULD be verified otherwise.",
    )
    inclusion_proof: V1InclusionProof = Field(
        ...,
        alias="inclusionProof",
        description="The inclusion proof can be used for offline or online verification\nthat the entry was appended to the log, and that the log has not been\naltered.",
    )
    canonicalized_body: Optional[str] = Field(
        default=None,
        alias="canonicalizedBody",
        description='Optional. The canonicalized transparency log entry, used to\nreconstruct the Signed Entry Timestamp (SET) during verification.\nThe contents of this field are the same as the `body` field in\na Rekor response, meaning that it does **not** include the "full"\ncanonicalized form (of log index, ID, etc.) which are\nexposed as separate fields. The verifier is responsible for\ncombining the `canonicalized_body`, `log_index`, `log_id`,\nand `integrated_time` into the payload that the SET\'s signature\nis generated over.\nThis field is intended to be used in cases where the SET cannot be\nproduced determinisitically (e.g. inconsistent JSON field ordering,\ndiffering whitespace, etc).\n\nIf set, clients MUST verify that the signature referenced in the\n`canonicalized_body` matches the signature provided in the\n`Bundle.content`.\nIf not set, clients are responsible for constructing an equivalent\npayload from other sources to verify the signature.',
    )


class V1X509Certificate(BaseModel):
    model_config = ConfigDict(
        populate_by_name=True,
    )
    raw_bytes: str = Field(..., alias="rawBytes", description="DER-encoded X.509 certificate.")


class V2Verifier(BaseModel):
    model_config = ConfigDict(
        populate_by_name=True,
    )
    public_key: Rekorv2PublicKey = Field(
        ...,
        alias="publicKey",
        title="DER-encoded public key. Encoding method is specified by the key_details attribute",
    )
    x509_certificate: V1X509Certificate = Field(
        ...,
        alias="x509Certificate",
        title="DER-encoded certificate",
    )
    key_details: V1PublicKeyDetails = Field(
        ...,
        alias="keyDetails",
        title="Key encoding and signature algorithm to use for this key",
    )


class ApiHttpBody(BaseModel):
    """Message that represents an arbitrary HTTP body. It should only be used for
    payload formats that can't be represented as JSON, such as raw binary or
    an HTML page.


    This message can be used both in streaming and non-streaming API methods in
    the request as well as the response.

    It can be used as a top-level request field, which is convenient if one
    wants to extract parameters from either the URL or HTTP template into the
    request fields and also want access to the raw HTTP body.

    Example:
        message GetResourceRequest {
          // A unique request id.
          string request_id = 1;

          // The raw HTTP body is bound to this field.
          google.api.HttpBody http_body = 2;

        }

        service ResourceService {
          rpc GetResource(GetResourceRequest)
            returns (google.api.HttpBody);
          rpc UpdateResource(google.api.HttpBody)
            returns (google.protobuf.Empty);

        }

    Example with streaming methods:

        service CaldavService {
          rpc GetCalendar(stream google.api.HttpBody)
            returns (stream google.api.HttpBody);
          rpc UpdateCalendar(stream google.api.HttpBody)
            returns (stream google.api.HttpBody);

        }

    Use of this type only changes how the request and response bodies are
    handled, all other features will continue to work unchanged.

    """

    model_config = ConfigDict(
        populate_by_name=True,
    )
    content_type: Optional[StrictStr] = Field(
        default=None,
        alias="contentType",
        description="The HTTP Content-Type header value specifying the content type of the body.",
    )
    data: Optional[str] = Field(
        default=None,
        description="The HTTP request/response body as raw binary.",
    )
    extensions: Optional[List[ProtobufAny]] = Field(
        default=None,
        description="Application specific response metadata. Must be set in the first response\nfor streaming APIs.",
    )


class IntotoEnvelope(BaseModel):
    """An authenticated message of arbitrary type."""

    model_config = ConfigDict(
        populate_by_name=True,
    )
    payload: Optional[str] = Field(
        default=None,
        description="Message to be signed. (In JSON, this is encoded as base64.)\nREQUIRED.",
    )
    payload_type: Optional[StrictStr] = Field(
        default=None,
        alias="payloadType",
        description="String unambiguously identifying how to interpret payload.\nREQUIRED.",
    )
    signatures: Optional[List[IointotoSignature]] = Field(
        default=None,
        description='Signature over:\n    PAE(type, payload)\nWhere PAE is defined as:\nPAE(type, payload) = "DSSEv1" + SP + LEN(type) + SP + type + SP + LEN(payload) + SP + payload\n+               = concatenation\nSP              = ASCII space [0x20]\n"DSSEv1"        = ASCII [0x44, 0x53, 0x53, 0x45, 0x76, 0x31]\nLEN(s)          = ASCII decimal encoding of the byte length of s, with no leading zeros\nREQUIRED (length >= 1).',
    )


class Rekorv2Signature(BaseModel):
    model_config = ConfigDict(
        populate_by_name=True,
    )
    content: str
    verifier: V2Verifier


class V2DSSERequestV002(BaseModel):
    model_config = ConfigDict(
        populate_by_name=True,
    )
    envelope: IntotoEnvelope = Field(..., title="A DSSE envelope")
    verifiers: List[V2Verifier] = Field(
        ...,
        title="All necessary verification material to verify all signatures embedded in the envelope",
    )


class V2HashedRekordRequestV002(BaseModel):
    model_config = ConfigDict(
        populate_by_name=True,
    )
    digest: str = Field(..., title="The hashed data")
    signature: Rekorv2Signature = Field(
        ...,
        title="A single signature over the hashed data with the verifier needed to validate it",
    )


class V2CreateEntryRequest(BaseModel):
    model_config = ConfigDict(
        populate_by_name=True,
    )
    hashed_rekord_request_v0_0_2: V2HashedRekordRequestV002 = Field(
        ...,
        alias="hashedRekordRequestV0_0_2",
    )
    dsse_request_v0_0_2: V2DSSERequestV002 = Field(..., alias="dsseRequestV0_0_2")

Some types, like apiHttpBody and ProtobufAny might not be useful, but perhaps in rekor-tiles we can avoid generating them.

Signed-off-by: Ramon Petgrave <[email protected]>
Signed-off-by: Ramon Petgrave <[email protected]>
Signed-off-by: Ramon Petgrave <[email protected]>
Signed-off-by: Ramon Petgrave <[email protected]>
Signed-off-by: Ramon Petgrave <[email protected]>
--branch="${rekorv2_ref}" \
https://github.com/sigstore/rekor-tiles \
"${rekorv2_dir}"
rekorv2_types=(rekor_service)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

only the rekor_service has types we are interested in

"E501", # handled by black, and catches some docstrings we can't autofix
"ERA001", # false positives
"D400", # overly opinionated docstrings
"D205", # 1 blank line required between summary line and description
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would be the first time that the genrated docstrings need to be multiline, but so far the formatters can't autofix.

"TCH001", # False positive: imports are re-exports, not just for type hints.
]
"src/rekor_types/_internal/*.py" = [
"src/rekor_types/_internal/**/*.py" = [
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

include the new /rekorv2/ subdirectory.

done

# RekorV2
rekorv2_ref="main" # not yet a non-prelease release.
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using #main, while there is not an a "latest" release.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support Rekorv2 types

1 participant