diff --git a/CHANGELOG.md b/CHANGELOG.md index e8d1fa56c..ad3a890bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,9 @@ All versions prior to 0.9.0 are untracked. ### Changed +* `sigstore verify` now performs additional verification of Rekor's inclusion proofs by cross-checking them against signed checkpoints + ([#634](https://github.com/sigstore/sigstore-python/pull/634)) + * A cached copy of the trust bundle is now included with the distribution ([#611](https://github.com/sigstore/sigstore-python/pull/611)) diff --git a/sigstore/_internal/rekor/__init__.py b/sigstore/_internal/rekor/__init__.py index 087e8a6da..8fa8a2c2e 100644 --- a/sigstore/_internal/rekor/__init__.py +++ b/sigstore/_internal/rekor/__init__.py @@ -16,6 +16,7 @@ APIs for interacting with Rekor. """ +from .checkpoint import SignedCheckpoint from .client import RekorClient -__all__ = ["RekorClient"] +__all__ = ["RekorClient", "SignedCheckpoint"] diff --git a/sigstore/_internal/rekor/checkpoint.py b/sigstore/_internal/rekor/checkpoint.py new file mode 100644 index 000000000..177b9e2cf --- /dev/null +++ b/sigstore/_internal/rekor/checkpoint.py @@ -0,0 +1,232 @@ +# Copyright 2023 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. + +""" +Rekor Checkpoint machinery. +""" + +from __future__ import annotations + +import base64 +import re +import struct +from dataclasses import dataclass +from typing import List + +from pydantic import BaseModel, Field, StrictStr + +from sigstore._internal.keyring import KeyringSignatureError +from sigstore._internal.rekor.client import RekorClient +from sigstore._utils import KeyID +from sigstore.transparency import LogEntry + + +@dataclass(frozen=True) +class RekorSignature: + """ + Represents a `RekorSignature` containing: + + - the name of the signature, e.g. "rekor.sigstage.dev" + - the signature hash + - the base64 signature + """ + + name: str + sig_hash: bytes + signature: bytes + + +class CheckpointError(Exception): + """Raised during LogCheckpoint parsing or verification.""" + + +class LogCheckpoint(BaseModel): + """ + Represents a Rekor `LogCheckpoint` containing: + + - an origin, e.g. "rekor.sigstage.dev - 8050909264565447525" + - the size of the log, + - the hash of the log, + - and any ancillary contants, e.g. "Timestamp: 1679349379012118479" + + See: + """ + + origin: StrictStr + log_size: int + log_hash: StrictStr + other_content: List[str] + + @classmethod + def from_text(cls, text: str) -> LogCheckpoint: + """ + Serialize from the text header ("note") of a SignedNote. + """ + + lines = text.strip().split("\n") + if len(lines) < 4: + raise CheckpointError("Malformed LogCheckpoint: too few items in header!") + + origin = lines[0] + if len(origin) == 0: + raise CheckpointError("Malformed LogCheckpoint: empty origin!") + + log_size = int(lines[1]) + root_hash = base64.b64decode(lines[2]).hex() + + return LogCheckpoint( + origin=origin, + log_size=log_size, + log_hash=root_hash, + other_content=lines[3:], + ) + + @classmethod + def to_text(self) -> str: + """ + Serialize a `LogCheckpoint` into text format. + See class definition for a prose description of the format. + """ + return "\n".join( + [ + self.origin, + str(self.log_size), + self.log_hash, + ] + + self.other_content + ) + + +@dataclass(frozen=True) +class SignedNote: + """ + Represents a "signed note" containing a note and its corresponding list of signatures. + """ + + note: StrictStr = Field(..., alias="note") + signatures: list[RekorSignature] = Field(..., alias="signatures") + + @classmethod + def from_text(cls, text: str) -> SignedNote: + """ + Deserialize from a bundled text 'note'. + + A note contains: + - a name, a string associated with the signer, + - a separator blank line, + - and signature(s), each signature takes the form + `\u2014 NAME SIGNATURE\n` + (where \u2014 == em dash). + + This is derived from Rekor's `UnmarshalText`: + + """ + + separator: str = "\n\n" + if text.count(separator) != 1: + raise CheckpointError( + "Note must contain one blank line, deliniating the text from the signature block" + ) + split = text.index(separator) + + header: str = text[: split + 1] + data: str = text[split + len(separator) :] + + if len(data) == 0: + raise CheckpointError( + "Malformed Note: must contain at least one signature!" + ) + if data[-1] != "\n": + raise CheckpointError("Malformed Note: data section must end with newline!") + + sig_parser = re.compile(r"\u2014 (\S+) (\S+)\n") + signatures: list[RekorSignature] = [] + for name, signature in re.findall(sig_parser, data): + signature_bytes: bytes = base64.b64decode(signature) + if len(signature_bytes) < 5: + raise CheckpointError( + "Malformed Note: signature contains too few bytes" + ) + + signature = RekorSignature( + name=name, + sig_hash=struct.unpack(">4s", signature_bytes[0:4])[0], + signature=base64.b64encode(signature_bytes[4:]), + ) + signatures.append(signature) + + return cls(note=header, signatures=signatures) + + def verify(self, client: RekorClient, key_id: KeyID) -> None: + """ + Verify the `SignedNote` with using the given RekorClient by verifying each contained signature. + """ + + note = str.encode(self.note) + + for sig in self.signatures: + if sig.sig_hash != key_id[:4]: + raise CheckpointError("sig_hash hint does not match expected key_id") + + try: + client._rekor_keyring.verify( + key_id=key_id, signature=base64.b64decode(sig.signature), data=note + ) + except KeyringSignatureError as sig_err: + raise CheckpointError("invalid signature") from sig_err + + +@dataclass(frozen=True) +class SignedCheckpoint: + """ + Represents a *signed* `Checkpoint`: a `LogCheckpoint` and its corresponding `SignedNote`. + """ + + signed_note: SignedNote + checkpoint: LogCheckpoint + + @classmethod + def from_text(cls, text: str) -> SignedCheckpoint: + """ + Create a new `SignedCheckpoint` from the text representation. + """ + + signed_note = SignedNote.from_text(text) + checkpoint = LogCheckpoint.from_text(signed_note.note) + return cls(signed_note=signed_note, checkpoint=checkpoint) + + +def verify_checkpoint(client: RekorClient, entry: LogEntry) -> None: + """ + Verify the inclusion proof's checkpoint. + """ + + inclusion_proof = entry.inclusion_proof + if inclusion_proof is None: + raise CheckpointError("Rekor entry has no inclusion proof") + + # verification occurs in two stages: + # 1) verify the signature on the checkpoint + # 2) verify the root hash in the checkpoint matches the root hash from the inclusion proof. + signed_checkpoint = SignedCheckpoint.from_text(inclusion_proof.checkpoint) + signed_checkpoint.signed_note.verify(client, KeyID(bytes.fromhex(entry.log_id))) + + checkpoint_hash = signed_checkpoint.checkpoint.log_hash + root_hash = inclusion_proof.root_hash + + if checkpoint_hash != root_hash: + raise CheckpointError( + "Inclusion proof contains invalid root hash signature: ", + f"expected {str(checkpoint_hash)} got {str(root_hash)}", + ) diff --git a/sigstore/_internal/set.py b/sigstore/_internal/set.py index ddad6e9aa..208059416 100644 --- a/sigstore/_internal/set.py +++ b/sigstore/_internal/set.py @@ -38,7 +38,7 @@ def verify_set(client: RekorClient, entry: LogEntry) -> None: Verify the Signed Entry Timestamp for a given Rekor `entry` using the given `client`. """ - signed_entry_ts = base64.b64decode(entry.signed_entry_timestamp) + signed_entry_ts = base64.b64decode(entry.inclusion_promise) try: client._rekor_keyring.verify( diff --git a/sigstore/sign.py b/sigstore/sign.py index 97e1e9f4d..4c37febac 100644 --- a/sigstore/sign.py +++ b/sigstore/sign.py @@ -60,6 +60,7 @@ X509CertificateChain, ) from sigstore_protobuf_specs.dev.sigstore.rekor.v1 import ( + Checkpoint, InclusionPromise, InclusionProof, KindVersion, @@ -238,6 +239,9 @@ def _to_bundle(self) -> Bundle: hashes=[ bytes.fromhex(h) for h in self.log_entry.inclusion_proof.hashes ], + checkpoint=Checkpoint( + envelope=self.log_entry.inclusion_proof.checkpoint + ), ) tlog_entry = TransparencyLogEntry( @@ -247,7 +251,7 @@ def _to_bundle(self) -> Bundle: integrated_time=self.log_entry.integrated_time, inclusion_promise=InclusionPromise( signed_entry_timestamp=base64.b64decode( - self.log_entry.signed_entry_timestamp + self.log_entry.inclusion_promise ) ), inclusion_proof=inclusion_proof, diff --git a/sigstore/transparency.py b/sigstore/transparency.py index 3854e1fd1..d80b0ada6 100644 --- a/sigstore/transparency.py +++ b/sigstore/transparency.py @@ -66,16 +66,17 @@ class LogEntry: The index of this entry within the log. """ - inclusion_proof: Optional["LogInclusionProof"] + inclusion_proof: Optional[LogInclusionProof] """ An optional inclusion proof for this log entry. - - Only present for entries retrieved from online logs. """ - signed_entry_timestamp: B64Str + inclusion_promise: B64Str """ - The base64-encoded Signed Entry Timestamp (SET) for this log entry. + An inclusion promise for this log entry. + + Internally, this is a base64-encoded Signed Entry Timestamp (SET) for this + log entry. """ @classmethod @@ -90,7 +91,6 @@ def _from_response(cls, dict_: dict[str, Any]) -> LogEntry: raise ValueError("Received multiple entries in response") uuid, entry = entries[0] - return LogEntry( uuid=uuid, body=entry["body"], @@ -100,7 +100,7 @@ def _from_response(cls, dict_: dict[str, Any]) -> LogEntry: inclusion_proof=LogInclusionProof.parse_obj( entry["verification"]["inclusionProof"] ), - signed_entry_timestamp=entry["verification"]["signedEntryTimestamp"], + inclusion_promise=entry["verification"]["signedEntryTimestamp"], ) def encode_canonical(self) -> bytes: @@ -125,10 +125,11 @@ class LogInclusionProof(BaseModel): Represents an inclusion proof for a transparency log entry. """ + checkpoint: StrictStr = Field(..., alias="checkpoint") + hashes: List[StrictStr] = Field(..., alias="hashes") log_index: StrictInt = Field(..., alias="logIndex") root_hash: StrictStr = Field(..., alias="rootHash") tree_size: StrictInt = Field(..., alias="treeSize") - hashes: List[StrictStr] = Field(..., alias="hashes") class Config: allow_population_by_field_name = True diff --git a/sigstore/verify/models.py b/sigstore/verify/models.py index 59033fce0..884b81365 100644 --- a/sigstore/verify/models.py +++ b/sigstore/verify/models.py @@ -22,6 +22,7 @@ import json import logging from dataclasses import dataclass +from textwrap import dedent from typing import IO from cryptography.hazmat.primitives.serialization import Encoding @@ -40,6 +41,7 @@ base64_encode_pem_cert, sha256_streaming, ) +from sigstore.errors import Error from sigstore.transparency import LogEntry, LogInclusionProof logger = logging.getLogger(__name__) @@ -95,11 +97,27 @@ class VerificationFailure(VerificationResult): """ -class InvalidMaterials(Exception): +class InvalidMaterials(Error): """ - The associated `VerificationMaterials` are invalid in some way. + Raised when the associated `VerificationMaterials` are invalid in some way. """ + def diagnostics(self) -> str: + """Returns diagnostics for the error.""" + + return dedent( + f"""\ + An issue occurred while parsing the verification materials. + + The provided verification materials are malformed and may have been + modified maliciously. + + Additional context: + + {self} + """ + ) + class RekorEntryMissing(Exception): """ @@ -223,6 +241,7 @@ def from_bundle( Effect: `input_` is consumed as part of construction. """ certs = bundle.verification_material.x509_certificate_chain.certificates + if len(certs) == 0: raise InvalidMaterials("expected non-empty certificate chain in bundle") cert_pem = PEMCert( @@ -240,20 +259,36 @@ def from_bundle( ) tlog_entry = tlog_entries[0] - inclusion_proof = LogInclusionProof( - log_index=tlog_entry.inclusion_proof.log_index, - root_hash=tlog_entry.inclusion_proof.root_hash.hex(), - tree_size=tlog_entry.inclusion_proof.tree_size, - hashes=[h.hex() for h in tlog_entry.inclusion_proof.hashes], - ) + # NOTE: Bundles are not required to include inclusion proofs, + # since offline (or non-gossiped) verification of an inclusion proof is + # only as strong as verification of the inclusion promise, which + # is always provided. + inclusion_proof = tlog_entry.inclusion_proof + parsed_inclusion_proof: LogInclusionProof | None = None + if inclusion_proof: + checkpoint = inclusion_proof.checkpoint + + # If the inclusion proof is provided, it must include its + # checkpoint. + if not checkpoint.envelope: + raise InvalidMaterials("expected checkpoint in inclusion proof") + + parsed_inclusion_proof = LogInclusionProof( + checkpoint=checkpoint.envelope, + hashes=[h.hex() for h in inclusion_proof.hashes], + log_index=inclusion_proof.log_index, + root_hash=inclusion_proof.root_hash.hex(), + tree_size=inclusion_proof.tree_size, + ) + entry = LogEntry( uuid=None, body=B64Str(base64.b64encode(tlog_entry.canonicalized_body).decode()), integrated_time=tlog_entry.integrated_time, log_id=tlog_entry.log_id.key_id.hex(), log_index=tlog_entry.log_index, - inclusion_proof=inclusion_proof, - signed_entry_timestamp=B64Str( + inclusion_proof=parsed_inclusion_proof, + inclusion_promise=B64Str( base64.b64encode( tlog_entry.inclusion_promise.signed_entry_timestamp ).decode() @@ -282,8 +317,22 @@ def rekor_entry(self, client: RekorClient) -> LogEntry: """ Returns a `RekorEntry` for the current signing materials. """ + + # The Rekor entry we use depends on a few different states: + # 1. If the user has requested offline verification and we've + # been given an offline Rekor entry to use, we use it. + # 2. If the user has not requested offline verification, + # we *opportunistically* use the offline Rekor entry, + # so long as it contains an inclusion proof. If it doesn't + # contain an inclusion proof, then we do an online entry lookup. + offline = self._offline + has_rekor_entry = self.has_rekor_entry + has_inclusion_proof = ( + self.has_rekor_entry and self._rekor_entry.inclusion_proof is not None # type: ignore + ) + entry: LogEntry | None - if self._offline and self.has_rekor_entry: + if (offline and has_rekor_entry) or (not offline and has_inclusion_proof): logger.debug("using offline rekor entry") entry = self._rekor_entry else: @@ -294,6 +343,7 @@ def rekor_entry(self, client: RekorClient) -> LogEntry: self.certificate, ) + # No matter what we do above, we must end up with a Rekor entry. if entry is None: raise RekorEntryMissing diff --git a/sigstore/verify/verifier.py b/sigstore/verify/verifier.py index 606f940db..1c94fc107 100644 --- a/sigstore/verify/verifier.py +++ b/sigstore/verify/verifier.py @@ -40,6 +40,10 @@ InvalidInclusionProofError, verify_merkle_inclusion, ) +from sigstore._internal.rekor.checkpoint import ( + CheckpointError, + verify_checkpoint, +) from sigstore._internal.rekor.client import RekorClient from sigstore._internal.set import InvalidSETError, verify_set from sigstore._internal.tuf import TrustUpdater @@ -246,17 +250,30 @@ def verify( # 5) Verify the inclusion proof supplied by Rekor for this artifact. # - # We skip the inclusion proof only if explicitly requested. - if not materials._offline: + # The inclusion proof should always be present in the online case. In + # the offline case, if it is present, we verify it. + if entry.inclusion_proof: try: verify_merkle_inclusion(entry) except InvalidInclusionProofError as exc: return VerificationFailure( reason=f"invalid Rekor inclusion proof: {exc}" ) + + try: + verify_checkpoint(self._rekor, entry) + except CheckpointError as exc: + return VerificationFailure(reason=f"invalid Rekor root hash: {exc}") + + logger.debug("Successfully verified inclusion proof...") + elif not materials._offline: + # Paranoia: if we weren't given an inclusion proof, then + # this *must* have been offline verification. If it was online + # then we've somehow entered an invalid state, so fail. + return VerificationFailure(reason="missing Rekor inclusion proof") else: - logger.debug( - "offline verification requested: skipping Merkle inclusion proof" + logger.warning( + "inclusion proof not present in bundle: skipping due to offline verification" ) # 6) Verify the Signed Entry Timestamp (SET) supplied by Rekor for this artifact diff --git a/test/unit/assets/bundle.txt.crt b/test/unit/assets/bundle.txt.crt new file mode 100644 index 000000000..5a97fd42c --- /dev/null +++ b/test/unit/assets/bundle.txt.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIC5zCCAmygAwIBAgIUJ3vpewdf6e91rgjqCqagstF4qn8wCgYIKoZIzj0EAwMw +NzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRl +cm1lZGlhdGUwHhcNMjMwNDI2MDAyMTA4WhcNMjMwNDI2MDAzMTA4WjAAMHYwEAYH +KoZIzj0CAQYFK4EEACIDYgAE2sd6+lOBcn5MXtnbwca7zcwpprl7GUZiKTO9IWpA +UfVTtx+BXGHQCRwsFy/d7dLlf4hurIqhzMD5yaC2kcU9/8c9G55JyBXF8Dx5SQm9 +y2rPWFIdm29Ql9A3I3yyEFyPo4IBbjCCAWowDgYDVR0PAQH/BAQDAgeAMBMGA1Ud +JQQMMAoGCCsGAQUFBwMDMB0GA1UdDgQWBBTlaUfjpiXGhBP3hOCW0JJZDSPxgzAf +BgNVHSMEGDAWgBRxhjCmFHxib/n31vQFGn9f/+tvrDAYBgNVHREBAf8EDjAMgQph +QHRueS50b3duMCwGCisGAQQBg78wAQEEHmh0dHBzOi8vZ2l0aHViLmNvbS9sb2dp +bi9vYXV0aDAuBgorBgEEAYO/MAEIBCAMHmh0dHBzOi8vZ2l0aHViLmNvbS9sb2dp +bi9vYXV0aDCBigYKKwYBBAHWeQIEAgR8BHoAeAB2ACswvNxoiMni4dgmKV50H0g5 +MZYC8pwzy15DQP6yrIZ6AAABh7rveBsAAAQDAEcwRQIhAKOZPMN9Q9qO1HXigHBP +t+Ic16yy2Zgv2KQ23i5WLj16AiAzrFpuayGXdoK+hYePl9dEeXjG/vB2jK/E3sEs +IrXtETAKBggqhkjOPQQDAwNpADBmAjEAgmhg80mI/Scr0isBnD5FYXZ8WxA8tnBB +Pmdf4aNGForGazGXaFQVPXgBVPv+YGI/AjEA0QzPC5dHD/WWXW2GbEC4dpwFk8OG +RkiExMOy/+CqabbVg+/lx1N9VGBTlUTft45d +-----END CERTIFICATE----- + diff --git a/test/unit/assets/bundle.txt.sig b/test/unit/assets/bundle.txt.sig new file mode 100644 index 000000000..1b6569e76 --- /dev/null +++ b/test/unit/assets/bundle.txt.sig @@ -0,0 +1 @@ +MGUCMQCOOJqTY6XWgB64izK2WVP07b0SG9M5WPCwKhfTPwMvtsgUi8KeRGwQkvvLYbKHdqUCMEbOXFG0NMqEQxWVb6rmGnexdADuGf6Jl8qAC8tn67p3QfVoXzMvFA61PzxwVwvb8g== diff --git a/test/unit/assets/bundle.txt.sigstore b/test/unit/assets/bundle.txt.sigstore index f73397b5f..fac453e00 100644 --- a/test/unit/assets/bundle.txt.sigstore +++ b/test/unit/assets/bundle.txt.sigstore @@ -1 +1 @@ -{"mediaType": "application/vnd.dev.sigstore.bundle+json;version=0.1", "verificationMaterial": {"x509CertificateChain": {"certificates": [{"rawBytes": "MIICwjCCAkegAwIBAgIUNRulROGJTUrEWvs9h68bMocfMbcwCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjMwMTMwMjI0MjA4WhcNMjMwMTMwMjI1MjA4WjAAMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEC4pHa0GudExSiDdn1RwUrytQUraA6CkGiiuVWnP661vvPfETx/3xr5/Q/8sy00tg7LjR5yFggFKSmM8E7Q03YAWZvORioljrokKVSLbJ7tEVtiJsraGaQYfcLcfk+Ei+o4IBSTCCAUUwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMB0GA1UdDgQWBBSgjmExD0FvLB3+YdpMkbc8D/aTpjAfBgNVHSMEGDAWgBRxhjCmFHxib/n31vQFGn9f/+tvrDAjBgNVHREBAf8EGTAXgRV3aWxsaWFtQHlvc3Nhcmlhbi5uZXQwLAYKKwYBBAGDvzABAQQeaHR0cHM6Ly9naXRodWIuY29tL2xvZ2luL29hdXRoMIGKBgorBgEEAdZ5AgQCBHwEegB4AHYAKzC83GiIyeLh2CYpXnQfSDkxlgLynDPLXkNA/rKshnoAAAGGBNhGsgAABAMARzBFAiEAyCATYmUVra04RNbRWA1B9IvOQb1Oo6dWbVcmD7lpDA4CIHuU5JUEd6+mud17S2sA0I+lZdknTw3fxK3wwMhWo4BrMAoGCCqGSM49BAMDA2kAMGYCMQCvIjyVjvhvgoLWD9D2S/GKsvCXfAZXR4V+JJvBKrqNJBclJKrEWJoVEryC09nyi+cCMQDsg29gfCZGmtQo2I/1JV3eypmnnrqAX/ot3RE5O2iTVwpgVD+G+ZPBX0xb0nQBVqI="}]}, "tlogEntries": [{"logIndex": "2798447", "logId": {"keyId": "0y8wo8MtY5wrdiIFohx7sHeI5oKDpK5vQhGHI6G+pJY="}, "kindVersion": {"kind": "hashedrekord", "version": "0.0.1"}, "integratedTime": "1675118528", "inclusionPromise": {"signedEntryTimestamp": "MEUCIQCvADnaopfUq3ZHmMH5axUTAnVsFm+lRwZTzojS/S/j6gIgTBFqilaERr4ynGts13KQnhW+N+f3SZHuKEPa56TsGjk="}, "inclusionProof": {"logIndex": "2783628", "rootHash": "yI+q1pOVBmLshdZ/AMZyobBGoZSnlP7DEJKa1oih/EM=", "treeSize": "2783629", "hashes": ["M2NdF1n5XRkCCOSIfaQjxtlgrZAtEmt0gPiPc4RERIQ=", "xdOVB9j9HhIpNr3XuX1x3h3YeQbiG3C2ORYLa53P9xk=", "nijvvfATxTieswSd7U9UXoT4CGrSShbXN6vwgF0hz3o=", "i045tKzGMiRsPd+6s0019t2W/w/mPWYAMFQazJ9Z9SI=", "Te4YkwkpHbNU40NJrsh0R/dYUd7IzsjfgscYw6qulqs=", "jiYMh5IprbGRK0sVt0QT4jK3+/wJvwhwO9zm+oJ+vyI=", "oDOc4/cWh/p+nUSrwVD3sGbbXaOdfmqx8ed9TBf/6GE=", "Li4l4euEirqV/WiWSGmyrvIQoYF80WAFTcGY2SXG5tY=", "GkJkTsUxj1BshWxCshtF5bL+BVbG7ZPSzJe157aFBd4=", "P7oQEMYLmrkMhQLUuYWXJ2mL524qm2+ib1buwM/lvic=", "VwBj5hN1tw74kRJeHAQaqdSWrXWk7Zb4c1PJfrpiKNw="]}, "canonicalizedBody": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI4MDJkZDYwZmY4ODMzMzgwMmYyNTg1ZTczMDQzYmQyMWMzNDEyODVlMTk5MmZlNWIzMTc1NWUxY2FkZWFlMzBlIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1HWUNNUUN3UCtVM25QcE9RaWpScDJPK2c2UDhSQzRnZlVMK1BDTkRIcGJmekhqbHVlVWdIanNOZE5SMng2dTRkL0ZpL1ZrQ01RRFExM24vS1hmbEhRekltbG9xRGxPdkxBT2JlR3BZUzdkWUIrWEpIdGw1dnNGUW51R0FHZ1Byei92NWxrQjY2ems9IiwicHVibGljS2V5Ijp7ImNvbnRlbnQiOiJMUzB0TFMxQ1JVZEpUaUJEUlZKVVNVWkpRMEZVUlMwdExTMHRDazFKU1VOM2FrTkRRV3RsWjBGM1NVSkJaMGxWVGxKMWJGSlBSMHBVVlhKRlYzWnpPV2cyT0dKTmIyTm1UV0pqZDBObldVbExiMXBKZW1vd1JVRjNUWGNLVG5wRlZrMUNUVWRCTVZWRlEyaE5UV015Ykc1ak0xSjJZMjFWZFZwSFZqSk5ValIzU0VGWlJGWlJVVVJGZUZaNllWZGtlbVJIT1hsYVV6RndZbTVTYkFwamJURnNXa2RzYUdSSFZYZElhR05PVFdwTmQwMVVUWGROYWtrd1RXcEJORmRvWTA1TmFrMTNUVlJOZDAxcVNURk5ha0UwVjJwQlFVMUlXWGRGUVZsSUNrdHZXa2w2YWpCRFFWRlpSa3MwUlVWQlEwbEVXV2RCUlVNMGNFaGhNRWQxWkVWNFUybEVaRzR4VW5kVmNubDBVVlZ5WVVFMlEydEhhV2wxVmxkdVVEWUtOakYyZGxCbVJWUjRMek40Y2pVdlVTODRjM2t3TUhSbk4weHFValY1Um1kblJrdFRiVTA0UlRkUk1ETlpRVmRhZGs5U2FXOXNhbkp2YTB0V1UweGlTZ28zZEVWV2RHbEtjM0poUjJGUldXWmpUR05tYXl0RmFTdHZORWxDVTFSRFEwRlZWWGRFWjFsRVZsSXdVRUZSU0M5Q1FWRkVRV2RsUVUxQ1RVZEJNVlZrQ2twUlVVMU5RVzlIUTBOelIwRlJWVVpDZDAxRVRVSXdSMEV4VldSRVoxRlhRa0pUWjJwdFJYaEVNRVoyVEVJeksxbGtjRTFyWW1NNFJDOWhWSEJxUVdZS1FtZE9Wa2hUVFVWSFJFRlhaMEpTZUdocVEyMUdTSGhwWWk5dU16RjJVVVpIYmpsbUx5dDBkbkpFUVdwQ1owNVdTRkpGUWtGbU9FVkhWRUZZWjFKV013cGhWM2h6WVZkR2RGRkliSFpqTTA1b1kyMXNhR0pwTlhWYVdGRjNURUZaUzB0M1dVSkNRVWRFZG5wQlFrRlJVV1ZoU0ZJd1kwaE5Oa3g1T1c1aFdGSnZDbVJYU1hWWk1qbDBUREo0ZGxveWJIVk1NamxvWkZoU2IwMUpSMHRDWjI5eVFtZEZSVUZrV2pWQloxRkRRa2gzUldWblFqUkJTRmxCUzNwRE9ETkhhVWtLZVdWTWFESkRXWEJZYmxGbVUwUnJlR3huVEhsdVJGQk1XR3RPUVM5eVMzTm9ibTlCUVVGSFIwSk9hRWR6WjBGQlFrRk5RVko2UWtaQmFVVkJlVU5CVkFwWmJWVldjbUV3TkZKT1lsSlhRVEZDT1VsMlQxRmlNVTl2Tm1SWFlsWmpiVVEzYkhCRVFUUkRTVWgxVlRWS1ZVVmtOaXR0ZFdReE4xTXljMEV3U1N0c0NscGthMjVVZHpObWVFc3pkM2ROYUZkdk5FSnlUVUZ2UjBORGNVZFRUVFE1UWtGTlJFRXlhMEZOUjFsRFRWRkRka2xxZVZacWRtaDJaMjlNVjBRNVJESUtVeTlIUzNOMlExaG1RVnBZVWpSV0swcEtka0pMY25GT1NrSmpiRXBMY2tWWFNtOVdSWEo1UXpBNWJubHBLMk5EVFZGRWMyY3lPV2RtUTFwSGJYUlJid295U1M4eFNsWXpaWGx3Ylc1dWNuRkJXQzl2ZEROU1JUVlBNbWxVVm5kd1oxWkVLMGNyV2xCQ1dEQjRZakJ1VVVKV2NVazlDaTB0TFMwdFJVNUVJRU5GVWxSSlJrbERRVlJGTFMwdExTMEsifX19fQ=="}]}, "messageSignature": {"messageDigest": {"algorithm": "SHA2_256", "digest": "gC3WD/iDM4AvJYXnMEO9IcNBKF4Zkv5bMXVeHK3q4w4="}, "signature": "MGYCMQCwP+U3nPpOQijRp2O+g6P8RC4gfUL+PCNDHpbfzHjlueUgHjsNdNR2x6u4d/Fi/VkCMQDQ13n/KXflHQzImloqDlOvLAObeGpYS7dYB+XJHtl5vsFQnuGAGgPrz/v5lkB66zk="}} +{"mediaType": "application/vnd.dev.sigstore.bundle+json;version=0.1", "verificationMaterial": {"x509CertificateChain": {"certificates": [{"rawBytes": "MIIC5zCCAmygAwIBAgIUJ3vpewdf6e91rgjqCqagstF4qn8wCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjMwNDI2MDAyMTA4WhcNMjMwNDI2MDAzMTA4WjAAMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE2sd6+lOBcn5MXtnbwca7zcwpprl7GUZiKTO9IWpAUfVTtx+BXGHQCRwsFy/d7dLlf4hurIqhzMD5yaC2kcU9/8c9G55JyBXF8Dx5SQm9y2rPWFIdm29Ql9A3I3yyEFyPo4IBbjCCAWowDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMB0GA1UdDgQWBBTlaUfjpiXGhBP3hOCW0JJZDSPxgzAfBgNVHSMEGDAWgBRxhjCmFHxib/n31vQFGn9f/+tvrDAYBgNVHREBAf8EDjAMgQphQHRueS50b3duMCwGCisGAQQBg78wAQEEHmh0dHBzOi8vZ2l0aHViLmNvbS9sb2dpbi9vYXV0aDAuBgorBgEEAYO/MAEIBCAMHmh0dHBzOi8vZ2l0aHViLmNvbS9sb2dpbi9vYXV0aDCBigYKKwYBBAHWeQIEAgR8BHoAeAB2ACswvNxoiMni4dgmKV50H0g5MZYC8pwzy15DQP6yrIZ6AAABh7rveBsAAAQDAEcwRQIhAKOZPMN9Q9qO1HXigHBPt+Ic16yy2Zgv2KQ23i5WLj16AiAzrFpuayGXdoK+hYePl9dEeXjG/vB2jK/E3sEsIrXtETAKBggqhkjOPQQDAwNpADBmAjEAgmhg80mI/Scr0isBnD5FYXZ8WxA8tnBBPmdf4aNGForGazGXaFQVPXgBVPv+YGI/AjEA0QzPC5dHD/WWXW2GbEC4dpwFk8OGRkiExMOy/+CqabbVg+/lx1N9VGBTlUTft45d"}]}, "tlogEntries": [{"logIndex": "7390977", "logId": {"keyId": "0y8wo8MtY5wrdiIFohx7sHeI5oKDpK5vQhGHI6G+pJY="}, "kindVersion": {"kind": "hashedrekord", "version": "0.0.1"}, "integratedTime": "1682468469", "inclusionPromise": {"signedEntryTimestamp": "MEUCICSJs5PgN4W3Lku3ybrwfNLAKMWaOvffg2tnqm19VrWEAiEA16MVPsWDoaAljsxGefpQazpvYfs1pv8lzdgZQ0I4rH0="}, "inclusionProof": {"logIndex": "7376158", "rootHash": "LE67t2Zlc0g35az81xMg0cgM2DULj8fNsGGHTcRthcs=", "treeSize": "7376159", "hashes": ["zgesNHwk09VvW4IDaPrJMtX59glNyyLPzeJO1Gw1hCI=", "lJiFr9ZP5FO8BjqLAUQ16A/0/LoOOQ0gfeNhdxaxO2w=", "sMImd51DBHQnH1tz4sGk8gXB+FjWyusVXbP0GmpFnB4=", "cDU1nEpl0WCRlxLi/gNVzykDzobU4qG/7BQZxn0qDgU=", "4CRqWzG3qpxKvlHuZg5O6QjQiwOzerbjwsAh30EVlA8=", "Ru0p3GE/zB2zub2/xR5rY/aM4J+5VJmiIuIl2enF/ws=", "2W+NG5yGR68lrLGcw4gn9CSCfeQF98d3LMfdo8tPyok=", "bEs1eYxy9R6hR2veGEwYW4PEdrZKrdqZ7uDlmmNtlas=", "sgQMnwcK7VxxAi+fygxq8iJ+zWqShjXm07/AWobWcXU=", "y4BESazXFcefRzxpN1PfJHoqRaKnPJPM5H/jotx0QY8=", "xiNEdLOpmGQERCR+DCEFVRK+Ns6G0BLV9M6sQQkRhik="], "checkpoint": {"envelope": "rekor.sigstage.dev - 8050909264565447525\n7376159\nLE67t2Zlc0g35az81xMg0cgM2DULj8fNsGGHTcRthcs=\nTimestamp: 1682468469199678948\n\n\u2014 rekor.sigstage.dev 0y8wozBEAiBbAodz3dBqJjGMhnZEkbaTDVxc8+tBEPKbaWUZoqxFvwIgGtYzFgFaM3UXBRHmzgmcrCxA145dpQ2YD0yFqiPHO7U=\n"}}, "canonicalizedBody": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI4MDJkZDYwZmY4ODMzMzgwMmYyNTg1ZTczMDQzYmQyMWMzNDEyODVlMTk5MmZlNWIzMTc1NWUxY2FkZWFlMzBlIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1HVUNNUUNPT0pxVFk2WFdnQjY0aXpLMldWUDA3YjBTRzlNNVdQQ3dLaGZUUHdNdnRzZ1VpOEtlUkd3UWt2dkxZYktIZHFVQ01FYk9YRkcwTk1xRVF4V1ZiNnJtR25leGRBRHVHZjZKbDhxQUM4dG42N3AzUWZWb1h6TXZGQTYxUHp4d1Z3dmI4Zz09IiwicHVibGljS2V5Ijp7ImNvbnRlbnQiOiJMUzB0TFMxQ1JVZEpUaUJEUlZKVVNVWkpRMEZVUlMwdExTMHRDazFKU1VNMWVrTkRRVzE1WjBGM1NVSkJaMGxWU2pOMmNHVjNaR1kyWlRreGNtZHFjVU54WVdkemRFWTBjVzQ0ZDBObldVbExiMXBKZW1vd1JVRjNUWGNLVG5wRlZrMUNUVWRCTVZWRlEyaE5UV015Ykc1ak0xSjJZMjFWZFZwSFZqSk5ValIzU0VGWlJGWlJVVVJGZUZaNllWZGtlbVJIT1hsYVV6RndZbTVTYkFwamJURnNXa2RzYUdSSFZYZElhR05PVFdwTmQwNUVTVEpOUkVGNVRWUkJORmRvWTA1TmFrMTNUa1JKTWsxRVFYcE5WRUUwVjJwQlFVMUlXWGRGUVZsSUNrdHZXa2w2YWpCRFFWRlpSa3MwUlVWQlEwbEVXV2RCUlRKelpEWXJiRTlDWTI0MVRWaDBibUozWTJFM2VtTjNjSEJ5YkRkSFZWcHBTMVJQT1VsWGNFRUtWV1pXVkhSNEswSllSMGhSUTFKM2MwWjVMMlEzWkV4c1pqUm9kWEpKY1doNlRVUTFlV0ZETW10alZUa3ZPR001UnpVMVNubENXRVk0UkhnMVUxRnRPUXA1TW5KUVYwWkpaRzB5T1ZGc09VRXpTVE41ZVVWR2VWQnZORWxDWW1wRFEwRlhiM2RFWjFsRVZsSXdVRUZSU0M5Q1FWRkVRV2RsUVUxQ1RVZEJNVlZrQ2twUlVVMU5RVzlIUTBOelIwRlJWVVpDZDAxRVRVSXdSMEV4VldSRVoxRlhRa0pVYkdGVlptcHdhVmhIYUVKUU0yaFBRMWN3U2twYVJGTlFlR2Q2UVdZS1FtZE9Wa2hUVFVWSFJFRlhaMEpTZUdocVEyMUdTSGhwWWk5dU16RjJVVVpIYmpsbUx5dDBkbkpFUVZsQ1owNVdTRkpGUWtGbU9FVkVha0ZOWjFGd2FBcFJTRkoxWlZNMU1HSXpaSFZOUTNkSFEybHpSMEZSVVVKbk56aDNRVkZGUlVodGFEQmtTRUo2VDJrNGRsb3liREJoU0ZacFRHMU9kbUpUT1hOaU1tUndDbUpwT1haWldGWXdZVVJCZFVKbmIzSkNaMFZGUVZsUEwwMUJSVWxDUTBGTlNHMW9NR1JJUW5wUGFUaDJXakpzTUdGSVZtbE1iVTUyWWxNNWMySXlaSEFLWW1rNWRsbFlWakJoUkVOQ2FXZFpTMHQzV1VKQ1FVaFhaVkZKUlVGblVqaENTRzlCWlVGQ01rRkRjM2QyVG5odmFVMXVhVFJrWjIxTFZqVXdTREJuTlFwTldsbERPSEIzZW5reE5VUlJVRFo1Y2tsYU5rRkJRVUpvTjNKMlpVSnpRVUZCVVVSQlJXTjNVbEZKYUVGTFQxcFFUVTQ1VVRseFR6RklXR2xuU0VKUUNuUXJTV014Tm5sNU1scG5kakpMVVRJemFUVlhUR294TmtGcFFYcHlSbkIxWVhsSFdHUnZTeXRvV1dWUWJEbGtSV1ZZYWtjdmRrSXlha3N2UlROelJYTUtTWEpZZEVWVVFVdENaMmR4YUd0cVQxQlJVVVJCZDA1d1FVUkNiVUZxUlVGbmJXaG5PREJ0U1M5VFkzSXdhWE5DYmtRMVJsbFlXamhYZUVFNGRHNUNRZ3BRYldSbU5HRk9SMFp2Y2tkaGVrZFlZVVpSVmxCWVowSldVSFlyV1VkSkwwRnFSVUV3VVhwUVF6VmtTRVF2VjFkWVZ6SkhZa1ZETkdSd2QwWnJPRTlIQ2xKcmFVVjRUVTk1THl0RGNXRmlZbFpuS3k5c2VERk9PVlpIUWxSc1ZWUm1kRFExWkFvdExTMHRMVVZPUkNCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2c9PSJ9fX19"}]}, "messageSignature": {"messageDigest": {"algorithm": "SHA2_256", "digest": "gC3WD/iDM4AvJYXnMEO9IcNBKF4Zkv5bMXVeHK3q4w4="}, "signature": "MGUCMQCOOJqTY6XWgB64izK2WVP07b0SG9M5WPCwKhfTPwMvtsgUi8KeRGwQkvvLYbKHdqUCMEbOXFG0NMqEQxWVb6rmGnexdADuGf6Jl8qAC8tn67p3QfVoXzMvFA61PzxwVwvb8g=="}} diff --git a/test/unit/assets/bundle_no_checkpoint.txt b/test/unit/assets/bundle_no_checkpoint.txt new file mode 100644 index 000000000..42f25dbd1 --- /dev/null +++ b/test/unit/assets/bundle_no_checkpoint.txt @@ -0,0 +1,5 @@ +DO NOT MODIFY ME! + +this is "bundle.txt", a sample input for sigstore-python's unit tests. + +DO NOT MODIFY ME! diff --git a/test/unit/assets/bundle_no_checkpoint.txt.bundle b/test/unit/assets/bundle_no_checkpoint.txt.bundle new file mode 100644 index 000000000..e69de29bb diff --git a/test/unit/assets/bundle_no_checkpoint.txt.crt b/test/unit/assets/bundle_no_checkpoint.txt.crt new file mode 100644 index 000000000..d0e0a6af8 --- /dev/null +++ b/test/unit/assets/bundle_no_checkpoint.txt.crt @@ -0,0 +1 @@ +MGUCMArXoJGZeHwbgH1sCqhkv2f2J9XntOwIP1MrcXoqBsU3AAyeyB/1ggizV6ScbQFPtQIxAIoH4b4PCIbqufTc6UG4eTchZgYh5hW8m4BOkhbCEiCzKsaZ0Trg8+Hm1N8egtVgYw== diff --git a/test/unit/assets/bundle_no_checkpoint.txt.sigstore b/test/unit/assets/bundle_no_checkpoint.txt.sigstore new file mode 100644 index 000000000..f73397b5f --- /dev/null +++ b/test/unit/assets/bundle_no_checkpoint.txt.sigstore @@ -0,0 +1 @@ +{"mediaType": "application/vnd.dev.sigstore.bundle+json;version=0.1", "verificationMaterial": {"x509CertificateChain": {"certificates": [{"rawBytes": "MIICwjCCAkegAwIBAgIUNRulROGJTUrEWvs9h68bMocfMbcwCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjMwMTMwMjI0MjA4WhcNMjMwMTMwMjI1MjA4WjAAMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEC4pHa0GudExSiDdn1RwUrytQUraA6CkGiiuVWnP661vvPfETx/3xr5/Q/8sy00tg7LjR5yFggFKSmM8E7Q03YAWZvORioljrokKVSLbJ7tEVtiJsraGaQYfcLcfk+Ei+o4IBSTCCAUUwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMB0GA1UdDgQWBBSgjmExD0FvLB3+YdpMkbc8D/aTpjAfBgNVHSMEGDAWgBRxhjCmFHxib/n31vQFGn9f/+tvrDAjBgNVHREBAf8EGTAXgRV3aWxsaWFtQHlvc3Nhcmlhbi5uZXQwLAYKKwYBBAGDvzABAQQeaHR0cHM6Ly9naXRodWIuY29tL2xvZ2luL29hdXRoMIGKBgorBgEEAdZ5AgQCBHwEegB4AHYAKzC83GiIyeLh2CYpXnQfSDkxlgLynDPLXkNA/rKshnoAAAGGBNhGsgAABAMARzBFAiEAyCATYmUVra04RNbRWA1B9IvOQb1Oo6dWbVcmD7lpDA4CIHuU5JUEd6+mud17S2sA0I+lZdknTw3fxK3wwMhWo4BrMAoGCCqGSM49BAMDA2kAMGYCMQCvIjyVjvhvgoLWD9D2S/GKsvCXfAZXR4V+JJvBKrqNJBclJKrEWJoVEryC09nyi+cCMQDsg29gfCZGmtQo2I/1JV3eypmnnrqAX/ot3RE5O2iTVwpgVD+G+ZPBX0xb0nQBVqI="}]}, "tlogEntries": [{"logIndex": "2798447", "logId": {"keyId": "0y8wo8MtY5wrdiIFohx7sHeI5oKDpK5vQhGHI6G+pJY="}, "kindVersion": {"kind": "hashedrekord", "version": "0.0.1"}, "integratedTime": "1675118528", "inclusionPromise": {"signedEntryTimestamp": "MEUCIQCvADnaopfUq3ZHmMH5axUTAnVsFm+lRwZTzojS/S/j6gIgTBFqilaERr4ynGts13KQnhW+N+f3SZHuKEPa56TsGjk="}, "inclusionProof": {"logIndex": "2783628", "rootHash": "yI+q1pOVBmLshdZ/AMZyobBGoZSnlP7DEJKa1oih/EM=", "treeSize": "2783629", "hashes": ["M2NdF1n5XRkCCOSIfaQjxtlgrZAtEmt0gPiPc4RERIQ=", "xdOVB9j9HhIpNr3XuX1x3h3YeQbiG3C2ORYLa53P9xk=", "nijvvfATxTieswSd7U9UXoT4CGrSShbXN6vwgF0hz3o=", "i045tKzGMiRsPd+6s0019t2W/w/mPWYAMFQazJ9Z9SI=", "Te4YkwkpHbNU40NJrsh0R/dYUd7IzsjfgscYw6qulqs=", "jiYMh5IprbGRK0sVt0QT4jK3+/wJvwhwO9zm+oJ+vyI=", "oDOc4/cWh/p+nUSrwVD3sGbbXaOdfmqx8ed9TBf/6GE=", "Li4l4euEirqV/WiWSGmyrvIQoYF80WAFTcGY2SXG5tY=", "GkJkTsUxj1BshWxCshtF5bL+BVbG7ZPSzJe157aFBd4=", "P7oQEMYLmrkMhQLUuYWXJ2mL524qm2+ib1buwM/lvic=", "VwBj5hN1tw74kRJeHAQaqdSWrXWk7Zb4c1PJfrpiKNw="]}, "canonicalizedBody": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI4MDJkZDYwZmY4ODMzMzgwMmYyNTg1ZTczMDQzYmQyMWMzNDEyODVlMTk5MmZlNWIzMTc1NWUxY2FkZWFlMzBlIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1HWUNNUUN3UCtVM25QcE9RaWpScDJPK2c2UDhSQzRnZlVMK1BDTkRIcGJmekhqbHVlVWdIanNOZE5SMng2dTRkL0ZpL1ZrQ01RRFExM24vS1hmbEhRekltbG9xRGxPdkxBT2JlR3BZUzdkWUIrWEpIdGw1dnNGUW51R0FHZ1Byei92NWxrQjY2ems9IiwicHVibGljS2V5Ijp7ImNvbnRlbnQiOiJMUzB0TFMxQ1JVZEpUaUJEUlZKVVNVWkpRMEZVUlMwdExTMHRDazFKU1VOM2FrTkRRV3RsWjBGM1NVSkJaMGxWVGxKMWJGSlBSMHBVVlhKRlYzWnpPV2cyT0dKTmIyTm1UV0pqZDBObldVbExiMXBKZW1vd1JVRjNUWGNLVG5wRlZrMUNUVWRCTVZWRlEyaE5UV015Ykc1ak0xSjJZMjFWZFZwSFZqSk5ValIzU0VGWlJGWlJVVVJGZUZaNllWZGtlbVJIT1hsYVV6RndZbTVTYkFwamJURnNXa2RzYUdSSFZYZElhR05PVFdwTmQwMVVUWGROYWtrd1RXcEJORmRvWTA1TmFrMTNUVlJOZDAxcVNURk5ha0UwVjJwQlFVMUlXWGRGUVZsSUNrdHZXa2w2YWpCRFFWRlpSa3MwUlVWQlEwbEVXV2RCUlVNMGNFaGhNRWQxWkVWNFUybEVaRzR4VW5kVmNubDBVVlZ5WVVFMlEydEhhV2wxVmxkdVVEWUtOakYyZGxCbVJWUjRMek40Y2pVdlVTODRjM2t3TUhSbk4weHFValY1Um1kblJrdFRiVTA0UlRkUk1ETlpRVmRhZGs5U2FXOXNhbkp2YTB0V1UweGlTZ28zZEVWV2RHbEtjM0poUjJGUldXWmpUR05tYXl0RmFTdHZORWxDVTFSRFEwRlZWWGRFWjFsRVZsSXdVRUZSU0M5Q1FWRkVRV2RsUVUxQ1RVZEJNVlZrQ2twUlVVMU5RVzlIUTBOelIwRlJWVVpDZDAxRVRVSXdSMEV4VldSRVoxRlhRa0pUWjJwdFJYaEVNRVoyVEVJeksxbGtjRTFyWW1NNFJDOWhWSEJxUVdZS1FtZE9Wa2hUVFVWSFJFRlhaMEpTZUdocVEyMUdTSGhwWWk5dU16RjJVVVpIYmpsbUx5dDBkbkpFUVdwQ1owNVdTRkpGUWtGbU9FVkhWRUZZWjFKV013cGhWM2h6WVZkR2RGRkliSFpqTTA1b1kyMXNhR0pwTlhWYVdGRjNURUZaUzB0M1dVSkNRVWRFZG5wQlFrRlJVV1ZoU0ZJd1kwaE5Oa3g1T1c1aFdGSnZDbVJYU1hWWk1qbDBUREo0ZGxveWJIVk1NamxvWkZoU2IwMUpSMHRDWjI5eVFtZEZSVUZrV2pWQloxRkRRa2gzUldWblFqUkJTRmxCUzNwRE9ETkhhVWtLZVdWTWFESkRXWEJZYmxGbVUwUnJlR3huVEhsdVJGQk1XR3RPUVM5eVMzTm9ibTlCUVVGSFIwSk9hRWR6WjBGQlFrRk5RVko2UWtaQmFVVkJlVU5CVkFwWmJWVldjbUV3TkZKT1lsSlhRVEZDT1VsMlQxRmlNVTl2Tm1SWFlsWmpiVVEzYkhCRVFUUkRTVWgxVlRWS1ZVVmtOaXR0ZFdReE4xTXljMEV3U1N0c0NscGthMjVVZHpObWVFc3pkM2ROYUZkdk5FSnlUVUZ2UjBORGNVZFRUVFE1UWtGTlJFRXlhMEZOUjFsRFRWRkRka2xxZVZacWRtaDJaMjlNVjBRNVJESUtVeTlIUzNOMlExaG1RVnBZVWpSV0swcEtka0pMY25GT1NrSmpiRXBMY2tWWFNtOVdSWEo1UXpBNWJubHBLMk5EVFZGRWMyY3lPV2RtUTFwSGJYUlJid295U1M4eFNsWXpaWGx3Ylc1dWNuRkJXQzl2ZEROU1JUVlBNbWxVVm5kd1oxWkVLMGNyV2xCQ1dEQjRZakJ1VVVKV2NVazlDaTB0TFMwdFJVNUVJRU5GVWxSSlJrbERRVlJGTFMwdExTMEsifX19fQ=="}]}, "messageSignature": {"messageDigest": {"algorithm": "SHA2_256", "digest": "gC3WD/iDM4AvJYXnMEO9IcNBKF4Zkv5bMXVeHK3q4w4="}, "signature": "MGYCMQCwP+U3nPpOQijRp2O+g6P8RC4gfUL+PCNDHpbfzHjlueUgHjsNdNR2x6u4d/Fi/VkCMQDQ13n/KXflHQzImloqDlOvLAObeGpYS7dYB+XJHtl5vsFQnuGAGgPrz/v5lkB66zk="}} diff --git a/test/unit/internal/rekor/test_client.py b/test/unit/internal/rekor/test_client.py index 119276daa..46a0b4f53 100644 --- a/test/unit/internal/rekor/test_client.py +++ b/test/unit/internal/rekor/test_client.py @@ -21,24 +21,32 @@ class TestRekorInclusionProof: def test_valid(self): - proof = LogInclusionProof(log_index=1, root_hash="abcd", tree_size=2, hashes=[]) + proof = LogInclusionProof( + log_index=1, root_hash="abcd", tree_size=2, hashes=[], checkpoint="" + ) assert proof is not None def test_negative_log_index(self): with pytest.raises( ValidationError, match="Inclusion proof has invalid log index" ): - LogInclusionProof(log_index=-1, root_hash="abcd", tree_size=2, hashes=[]) + LogInclusionProof( + log_index=-1, root_hash="abcd", tree_size=2, hashes=[], checkpoint="" + ) def test_negative_tree_size(self): with pytest.raises( ValidationError, match="Inclusion proof has invalid tree size" ): - LogInclusionProof(log_index=1, root_hash="abcd", tree_size=-1, hashes=[]) + LogInclusionProof( + log_index=1, root_hash="abcd", tree_size=-1, hashes=[], checkpoint="" + ) def test_log_index_outside_tree_size(self): with pytest.raises( ValidationError, match="Inclusion proof has log index greater than or equal to tree size", ): - LogInclusionProof(log_index=2, root_hash="abcd", tree_size=1, hashes=[]) + LogInclusionProof( + log_index=2, root_hash="abcd", tree_size=1, hashes=[], checkpoint="" + ) diff --git a/test/unit/verify/test_models.py b/test/unit/verify/test_models.py index d8d023a27..fbbf5fdb2 100644 --- a/test/unit/verify/test_models.py +++ b/test/unit/verify/test_models.py @@ -80,3 +80,9 @@ def test_verification_materials_bundle_no_log_entry(self, signing_bundle): InvalidMaterials, match="expected exactly one log entry, got 0" ): signing_bundle("bundle_no_log_entry.txt") + + def test_verification_materials_offline_no_checkpoint(self, signing_bundle): + with pytest.raises( + InvalidMaterials, match="expected checkpoint in inclusion proof" + ): + signing_bundle("bundle_no_checkpoint.txt", offline=True) diff --git a/test/unit/verify/test_verifier.py b/test/unit/verify/test_verifier.py index 70406fff8..e7a61114e 100644 --- a/test/unit/verify/test_verifier.py +++ b/test/unit/verify/test_verifier.py @@ -15,8 +15,13 @@ import pretend import pytest +from sigstore.transparency import LogEntry from sigstore.verify import policy -from sigstore.verify.models import VerificationFailure, VerificationSuccess +from sigstore.verify.models import ( + VerificationFailure, + VerificationMaterials, + VerificationSuccess, +) from sigstore.verify.verifier import CertificateVerificationFailure, Verifier @@ -110,13 +115,35 @@ def test_verifier_policy_check(signing_materials): @pytest.mark.online -def test_verifier_invalid_signature(signing_materials, null_policy, monkeypatch): +def test_verifier_invalid_signature(signing_materials, null_policy): materials = signing_materials("bad.txt") verifier = Verifier.staging() assert not verifier.verify(materials, null_policy) +@pytest.mark.online +def test_verifier_invalid_online_missing_inclusion_proof( + signing_materials, null_policy, monkeypatch +): + verifier = Verifier.staging() + + materials: VerificationMaterials = signing_materials("a.txt") + # Retrieve the entry, strip its inclusion proof, stuff it back + # into the materials, and then patch out the check that insures the + # inclusion proof's presence. + # This effectively emulates a "misbehaving" Rekor instance that returns + # log entries without corresponding inclusion proofs. + entry: LogEntry = materials.rekor_entry(verifier._rekor) + entry.__dict__["inclusion_proof"] = None + materials._rekor_entry = entry + monkeypatch.setattr(materials, "rekor_entry", lambda *a: entry) + + result = verifier.verify(materials, null_policy) + assert not result + assert result == VerificationFailure(reason="missing Rekor inclusion proof") + + @pytest.mark.online @pytest.mark.xfail def test_verifier_fail_expiry(signing_materials, null_policy, monkeypatch):