From ec1061bd1347291e0fe49cd2449cd5fd88b15595 Mon Sep 17 00:00:00 2001 From: Cody Soyland Date: Thu, 14 May 2026 17:41:14 -0400 Subject: [PATCH 1/7] verify, rekor: encode DSSE as hashedrekord for Rekor v2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements rekor-v2-spec §6.1.4: DSSE envelope entries are submitted and verified as hashedrekord/0.0.2 with digest = Hash(PAE(payloadType, payload)) and signature.content = envelope.signatures[0].sig. This is vibecoded — a maintainer should take a closer look at correctness, especially the hash-algorithm dispatch in _hash_for_key_details and the entry-body reconstruction in _validate_hashedrekord_v002_dsse_entry_body. Passes the draft conformance tests in sigstore/sigstore-conformance#371. Related spec change: sigstore/architecture-docs#63 Signed-off-by: Cody Soyland --- sigstore/_internal/rekor/client_v2.py | 29 +++++-- sigstore/verify/verifier.py | 110 ++++++++++++++++++++++++-- 2 files changed, 124 insertions(+), 15 deletions(-) diff --git a/sigstore/_internal/rekor/client_v2.py b/sigstore/_internal/rekor/client_v2.py index 106b1dcd9..225a21c04 100644 --- a/sigstore/_internal/rekor/client_v2.py +++ b/sigstore/_internal/rekor/client_v2.py @@ -19,6 +19,7 @@ from __future__ import annotations import base64 +import hashlib import json import logging import threading @@ -37,7 +38,7 @@ RekorClientError, RekorLogSubmitter, ) -from sigstore.dsse import Envelope +from sigstore.dsse import Envelope, _pae from sigstore.hashes import Hashed from sigstore.models import TransparencyLogEntry @@ -137,13 +138,25 @@ def _build_dsse_request( cls, envelope: Envelope, certificate: Certificate ) -> EntryRequestBody: """ - Construct a dsse request to submit to Rekor. + Construct a hashedrekord request for a DSSE envelope. + + Rekor v2 only supports the hashedrekord entry type; DSSE envelopes are + uploaded as a hashedrekord whose digest is `Hash(PAE(payloadType, + payload))` and whose `signature.content` equals + `envelope.signatures[0].sig`. See rekor-v2-spec §6.1.4. + + sigstore-python only signs with ECDSA P-256, so the hash function is + always SHA-256 here. A general implementation would select the hash + function from the signing algorithm per the algorithm registry. """ + pae = _pae(envelope._inner.payload_type, envelope._inner.payload) + digest = hashlib.sha256(pae).digest() req = rekor_v2.entry.CreateEntryRequest( - dsse_request_v002=rekor_v2.dsse.DSSERequestV002( - envelope=envelope._inner, - verifiers=[ - rekor_v2.verifier.Verifier( + hashed_rekord_request_v002=rekor_v2.hashedrekord.HashedRekordRequestV002( + digest=base64.b64encode(digest), + signature=rekor_v2.verifier.Signature( + content=base64.b64encode(envelope.signature), + verifier=rekor_v2.verifier.Verifier( x509_certificate=common_v1.X509Certificate( raw_bytes=base64.b64encode( certificate.public_bytes( @@ -152,8 +165,8 @@ def _build_dsse_request( ) ), key_details=_get_key_details(certificate), - ) - ], + ), + ), ) ) return EntryRequestBody(req.to_dict()) diff --git a/sigstore/verify/verifier.py b/sigstore/verify/verifier.py index 7b81e9c2b..f5d5fc41a 100644 --- a/sigstore/verify/verifier.py +++ b/sigstore/verify/verifier.py @@ -19,7 +19,9 @@ from __future__ import annotations import base64 +import hashlib import logging +from collections.abc import Callable from datetime import datetime, timezone from typing import cast @@ -43,6 +45,7 @@ from sigstore_models.rekor import v2 from sigstore import dsse +from sigstore.dsse import _pae from sigstore._internal.rekor import _hashedrekord_from_parts from sigstore._internal.rekor.client import RekorClient from sigstore._internal.sct import ( @@ -431,18 +434,23 @@ def verify_dsse( # *cannot* verify, since the envelope is uncanonicalized JSON. # Instead, we manually pick apart the entry body below and verify # the parts we can (namely the payload hash and signature list). + # + # Rekor v2 (entry kind=hashedrekord/0.0.2) records DSSE envelopes as + # hashedrekord entries whose digest covers PAE(payloadType, payload) + # and whose signature.content equals envelope.signatures[0].sig. + # See rekor-v2-spec §6.1.4. entry = bundle.log_entry - if entry._inner.kind_version.kind != "dsse": - raise VerificationError( - f"Expected entry type dsse, got {entry._inner.kind_version.kind}" - ) - if entry._inner.kind_version.version == "0.0.2": + kind = entry._inner.kind_version.kind + version = entry._inner.kind_version.version + if kind == "hashedrekord" and version == "0.0.2": + _validate_hashedrekord_v002_dsse_entry_body(bundle) + elif kind == "dsse" and version == "0.0.2": _validate_dsse_v002_entry_body(bundle) - elif entry._inner.kind_version.version == "0.0.1": + elif kind == "dsse" and version == "0.0.1": _validate_dsse_v001_entry_body(bundle) else: raise VerificationError( - f"Unsupported dsse version {entry._inner.kind_version.version}" + f"Unsupported DSSE log entry type: {kind}/{version}" ) return (envelope._inner.payload_type, envelope._inner.payload) @@ -611,6 +619,56 @@ def _validate_hashedrekord_v001_entry_body( ) +def _validate_hashedrekord_v002_dsse_entry_body(bundle: Bundle) -> None: + """ + Validate Entry body for a Rekor v2 DSSE envelope encoded as a + hashedrekord/0.0.2 entry (rekor-v2-spec §6.1.4). + + The expected entry body has: + - data.digest = Hash(PAE(payloadType, payload)), where Hash is the + externalized hash function of the entry's signing algorithm. + - data.algorithm = the matching HashAlgorithm. + - signature.content = envelope.signatures[0].sig. + - signature.verifier = the bundle's signing certificate. + """ + entry = bundle.log_entry + envelope = bundle._dsse_envelope + if envelope is None: + raise VerificationError( + "cannot perform DSSE verification on a bundle without a DSSE envelope" + ) + if len(envelope._inner.signatures) != 1: + raise VerificationError( + "DSSE envelope must have exactly one signature for hashedrekord encoding" + ) + + expected_verifier = _v2_verifier_from_certificate(bundle.signing_certificate) + algorithm, hash_func = _hash_for_key_details(expected_verifier.key_details) + pae_digest = hash_func(_pae(envelope._inner.payload_type, envelope._inner.payload)).digest() + + expected_body = v2.entry.Entry( + kind=entry._inner.kind_version.kind, + api_version=entry._inner.kind_version.version, + spec=v2.entry.Spec( + hashed_rekord_v002=v2.hashedrekord.HashedRekordLogEntryV002( + data=v1.HashOutput( + algorithm=algorithm, + digest=base64.b64encode(pae_digest), + ), + signature=v2.verifier.Signature( + content=base64.b64encode(envelope.signature), + verifier=expected_verifier, + ), + ) + ), + ) + actual_body = v2.entry.Entry.from_json(entry._inner.canonicalized_body) + if expected_body != actual_body: + raise VerificationError( + "transparency log entry is inconsistent with other materials" + ) + + def _validate_hashedrekord_v002_entry_body( bundle: Bundle, hashed_input: Hashed ) -> None: @@ -677,3 +735,41 @@ def _v2_verifier_from_certificate(certificate: Certificate) -> v2.verifier.Verif ), key_details=key_details, ) + + +def _hash_for_key_details( + key_details: v1.PublicKeyDetails, +) -> tuple[v1.HashAlgorithm, Callable[[bytes], "hashlib._Hash"]]: + """ + Map a `PublicKeyDetails` to the externalized hash function and matching + `HashAlgorithm` per the algorithm registry. Only signing algorithms with + an externalized prehash are eligible to sign a hashedrekord entry, so + ed25519 (pure) is rejected. + """ + sha256_algos = { + v1.PublicKeyDetails.PKIX_ECDSA_P256_SHA_256, + v1.PublicKeyDetails.PKIX_ECDSA_P256_HMAC_SHA_256, + v1.PublicKeyDetails.PKIX_RSA_PKCS1V15_2048_SHA256, + v1.PublicKeyDetails.PKIX_RSA_PKCS1V15_3072_SHA256, + v1.PublicKeyDetails.PKIX_RSA_PKCS1V15_4096_SHA256, + v1.PublicKeyDetails.PKIX_RSA_PSS_2048_SHA256, + v1.PublicKeyDetails.PKIX_RSA_PSS_3072_SHA256, + v1.PublicKeyDetails.PKIX_RSA_PSS_4096_SHA256, + v1.PublicKeyDetails.PKIX_ECDSA_P384_SHA_256, + v1.PublicKeyDetails.PKIX_ECDSA_P521_SHA_256, + } + sha384_algos = {v1.PublicKeyDetails.PKIX_ECDSA_P384_SHA_384} + sha512_algos = { + v1.PublicKeyDetails.PKIX_ECDSA_P521_SHA_512, + v1.PublicKeyDetails.PKIX_ED25519_PH, + } + if key_details in sha256_algos: + return v1.HashAlgorithm.SHA2_256, hashlib.sha256 + if key_details in sha384_algos: + return v1.HashAlgorithm.SHA2_384, hashlib.sha384 + if key_details in sha512_algos: + return v1.HashAlgorithm.SHA2_512, hashlib.sha512 + raise VerificationError( + f"signing algorithm {key_details} has no externalized prehash; " + "cannot be used for a hashedrekord entry (rekor-v2-spec §6.1.4)" + ) From 7c2ac608bb4093c8f106cf63ef24c3fd0cb51c36 Mon Sep 17 00:00:00 2001 From: Cody Soyland Date: Thu, 14 May 2026 17:43:36 -0400 Subject: [PATCH 2/7] verify: fix lint (import order, format, quoted annotation) Signed-off-by: Cody Soyland --- sigstore/verify/verifier.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/sigstore/verify/verifier.py b/sigstore/verify/verifier.py index f5d5fc41a..6d573ab58 100644 --- a/sigstore/verify/verifier.py +++ b/sigstore/verify/verifier.py @@ -45,7 +45,6 @@ from sigstore_models.rekor import v2 from sigstore import dsse -from sigstore.dsse import _pae from sigstore._internal.rekor import _hashedrekord_from_parts from sigstore._internal.rekor.client import RekorClient from sigstore._internal.sct import ( @@ -54,6 +53,7 @@ from sigstore._internal.timestamp import TimestampSource, TimestampVerificationResult from sigstore._internal.trust import KeyringPurpose from sigstore._utils import base64_encode_pem_cert, sha256_digest +from sigstore.dsse import _pae from sigstore.errors import CertValidationError, VerificationError from sigstore.hashes import Hashed from sigstore.models import Bundle, ClientTrustConfig, TrustedRoot @@ -644,7 +644,9 @@ def _validate_hashedrekord_v002_dsse_entry_body(bundle: Bundle) -> None: expected_verifier = _v2_verifier_from_certificate(bundle.signing_certificate) algorithm, hash_func = _hash_for_key_details(expected_verifier.key_details) - pae_digest = hash_func(_pae(envelope._inner.payload_type, envelope._inner.payload)).digest() + pae_digest = hash_func( + _pae(envelope._inner.payload_type, envelope._inner.payload) + ).digest() expected_body = v2.entry.Entry( kind=entry._inner.kind_version.kind, @@ -739,7 +741,7 @@ def _v2_verifier_from_certificate(certificate: Certificate) -> v2.verifier.Verif def _hash_for_key_details( key_details: v1.PublicKeyDetails, -) -> tuple[v1.HashAlgorithm, Callable[[bytes], "hashlib._Hash"]]: +) -> tuple[v1.HashAlgorithm, Callable[[bytes], hashlib._Hash]]: """ Map a `PublicKeyDetails` to the externalized hash function and matching `HashAlgorithm` per the algorithm registry. Only signing algorithms with From 3e367d039032b4c15c5c5752d3f844220760ab1f Mon Sep 17 00:00:00 2001 From: Cody Soyland Date: Mon, 18 May 2026 14:44:37 -0400 Subject: [PATCH 3/7] Add Envelope.pae() helper Replace cross-module `_pae` imports in the Rekor v2 producer and DSSE hashedrekord verifier with a public `Envelope.pae()` method. Addresses review feedback on #1776: drops the cross-module use of a private symbol and lets the hashedrekord-for-DSSE docstring read as `Hash(envelope.pae())`. No behavior change. Signed-off-by: Cody Soyland --- sigstore/_internal/rekor/client_v2.py | 11 +++++------ sigstore/dsse/__init__.py | 6 ++++++ sigstore/verify/verifier.py | 5 +---- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/sigstore/_internal/rekor/client_v2.py b/sigstore/_internal/rekor/client_v2.py index 225a21c04..9ea7e49c6 100644 --- a/sigstore/_internal/rekor/client_v2.py +++ b/sigstore/_internal/rekor/client_v2.py @@ -38,7 +38,7 @@ RekorClientError, RekorLogSubmitter, ) -from sigstore.dsse import Envelope, _pae +from sigstore.dsse import Envelope from sigstore.hashes import Hashed from sigstore.models import TransparencyLogEntry @@ -141,16 +141,15 @@ def _build_dsse_request( Construct a hashedrekord request for a DSSE envelope. Rekor v2 only supports the hashedrekord entry type; DSSE envelopes are - uploaded as a hashedrekord whose digest is `Hash(PAE(payloadType, - payload))` and whose `signature.content` equals - `envelope.signatures[0].sig`. See rekor-v2-spec §6.1.4. + uploaded as a hashedrekord whose digest is `Hash(envelope.pae())` and + whose `signature.content` equals `envelope.signatures[0].sig`. See + rekor-v2-spec §6.1.4. sigstore-python only signs with ECDSA P-256, so the hash function is always SHA-256 here. A general implementation would select the hash function from the signing algorithm per the algorithm registry. """ - pae = _pae(envelope._inner.payload_type, envelope._inner.payload) - digest = hashlib.sha256(pae).digest() + digest = hashlib.sha256(envelope.pae()).digest() req = rekor_v2.entry.CreateEntryRequest( hashed_rekord_request_v002=rekor_v2.hashedrekord.HashedRekordRequestV002( digest=base64.b64encode(digest), diff --git a/sigstore/dsse/__init__.py b/sigstore/dsse/__init__.py index 38caf5843..f08fd1a9f 100644 --- a/sigstore/dsse/__init__.py +++ b/sigstore/dsse/__init__.py @@ -246,6 +246,12 @@ def signature(self) -> bytes: """Return the decoded bytes of the Envelope signature.""" return self._signature_bytes + def pae(self) -> bytes: + """ + Return the PAE encoding of this envelope's `payloadType` and `payload`. + """ + return _pae(self._inner.payload_type, self._inner.payload) + def _pae(type_: str, body: bytes) -> bytes: """ diff --git a/sigstore/verify/verifier.py b/sigstore/verify/verifier.py index 6d573ab58..addb5751c 100644 --- a/sigstore/verify/verifier.py +++ b/sigstore/verify/verifier.py @@ -53,7 +53,6 @@ from sigstore._internal.timestamp import TimestampSource, TimestampVerificationResult from sigstore._internal.trust import KeyringPurpose from sigstore._utils import base64_encode_pem_cert, sha256_digest -from sigstore.dsse import _pae from sigstore.errors import CertValidationError, VerificationError from sigstore.hashes import Hashed from sigstore.models import Bundle, ClientTrustConfig, TrustedRoot @@ -644,9 +643,7 @@ def _validate_hashedrekord_v002_dsse_entry_body(bundle: Bundle) -> None: expected_verifier = _v2_verifier_from_certificate(bundle.signing_certificate) algorithm, hash_func = _hash_for_key_details(expected_verifier.key_details) - pae_digest = hash_func( - _pae(envelope._inner.payload_type, envelope._inner.payload) - ).digest() + pae_digest = hash_func(envelope.pae()).digest() expected_body = v2.entry.Entry( kind=entry._inner.kind_version.kind, From bbc93fd12ccb2ae0493bad92218512255e9743b2 Mon Sep 17 00:00:00 2001 From: Cody Soyland Date: Mon, 18 May 2026 14:48:16 -0400 Subject: [PATCH 4/7] Drop dsse/0.0.2 verifier path; replace staging test fixture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rekor v2 will not support the dsse entry type — DSSE envelopes are encoded as hashedrekord/0.0.2 (rekor-v2-spec §6.1.4). Remove the now-orphaned dsse/0.0.2 dispatch branch and `_validate_dsse_v002_entry_body` function. The dsse/0.0.1 path stays for Rekor v1 legacy bundles. Replaces `test/assets/a.dsse.staging-rekor-v2.txt.sigstore.json` with the `rekor2-dsse-happy-path` fixture from the sigstore-conformance `dsse-hashedrekord-test-bundles` branch, so the staging DSSE verification test now exercises hashedrekord/0.0.2 end-to-end. Addresses review feedback on #1776. Signed-off-by: Cody Soyland --- sigstore/verify/verifier.py | 57 +++-------------- .../a.dsse.staging-rekor-v2.txt.sigstore.json | 61 ++++++++++++++++++- 2 files changed, 68 insertions(+), 50 deletions(-) diff --git a/sigstore/verify/verifier.py b/sigstore/verify/verifier.py index addb5751c..bb635f512 100644 --- a/sigstore/verify/verifier.py +++ b/sigstore/verify/verifier.py @@ -427,24 +427,19 @@ def verify_dsse( # (8): verify the consistency of the log entry's body against # the other bundle materials. - # NOTE: This is very slightly weaker than the consistency check - # for hashedrekord entries, due to how inclusion is recorded for DSSE: - # the included entry for DSSE includes an envelope hash that we - # *cannot* verify, since the envelope is uncanonicalized JSON. - # Instead, we manually pick apart the entry body below and verify - # the parts we can (namely the payload hash and signature list). - # - # Rekor v2 (entry kind=hashedrekord/0.0.2) records DSSE envelopes as - # hashedrekord entries whose digest covers PAE(payloadType, payload) - # and whose signature.content equals envelope.signatures[0].sig. - # See rekor-v2-spec §6.1.4. + # Rekor v2 records DSSE envelopes as hashedrekord/0.0.2 entries whose + # digest covers PAE(payloadType, payload) and whose signature.content + # equals envelope.signatures[0].sig (rekor-v2-spec §6.1.4). Rekor v1 + # used a dsse/0.0.1 entry, which is slightly weaker than the + # hashedrekord consistency check: dsse entries record an envelope + # hash that we *cannot* verify (the envelope is uncanonicalized JSON), + # so we manually pick apart the entry body and verify the parts we + # can (payload hash and signature list). entry = bundle.log_entry kind = entry._inner.kind_version.kind version = entry._inner.kind_version.version if kind == "hashedrekord" and version == "0.0.2": _validate_hashedrekord_v002_dsse_entry_body(bundle) - elif kind == "dsse" and version == "0.0.2": - _validate_dsse_v002_entry_body(bundle) elif kind == "dsse" and version == "0.0.1": _validate_dsse_v001_entry_body(bundle) else: @@ -561,42 +556,6 @@ def _validate_dsse_v001_entry_body(bundle: Bundle) -> None: raise VerificationError("log entry signatures do not match bundle") -def _validate_dsse_v002_entry_body(bundle: Bundle) -> None: - """ - Validate Entry body for dsse v002. - """ - entry = bundle.log_entry - envelope = bundle._dsse_envelope - if envelope is None: - raise VerificationError( - "cannot perform DSSE verification on a bundle without a DSSE envelope" - ) - try: - v2_body = v2.entry.Entry.from_json(entry._inner.canonicalized_body) - except ValidationError as exc: - raise VerificationError(f"invalid DSSE log entry: {exc}") - - if v2_body.spec.dsse_v002 is None: - raise VerificationError("invalid DSSE log entry: missing dsse_v002 field") - - if v2_body.spec.dsse_v002.payload_hash.algorithm != v1.HashAlgorithm.SHA2_256: - raise VerificationError("expected SHA256 hash in DSSE entry") - - digest = sha256_digest(envelope._inner.payload).digest - if v2_body.spec.dsse_v002.payload_hash.digest != digest: - raise VerificationError("DSSE entry payload hash does not match bundle") - - v2_signatures = [ - v2.verifier.Signature( - content=base64.b64encode(signature.sig), - verifier=_v2_verifier_from_certificate(bundle.signing_certificate), - ) - for signature in envelope._inner.signatures - ] - if v2_signatures != v2_body.spec.dsse_v002.signatures: - raise VerificationError("log entry signatures do not match bundle") - - def _validate_hashedrekord_v001_entry_body( bundle: Bundle, hashed_input: Hashed ) -> None: diff --git a/test/assets/a.dsse.staging-rekor-v2.txt.sigstore.json b/test/assets/a.dsse.staging-rekor-v2.txt.sigstore.json index af2fe26f5..3a816e48d 100644 --- a/test/assets/a.dsse.staging-rekor-v2.txt.sigstore.json +++ b/test/assets/a.dsse.staging-rekor-v2.txt.sigstore.json @@ -1 +1,60 @@ -{"mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json", "verificationMaterial": {"certificate": {"rawBytes": "MIIDBDCCAoqgAwIBAgIUYlZafqye+P/bWSMSdvxrr7y+NUEwCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjUwNjA5MjEwNjI1WhcNMjUwNjA5MjExNjI1WjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEwDj9XB2rrkUTaCgPE3OGPJ+176EZM3u2SK2XLKoMUQn79zywhocahVPybzn/6nMkWkew8SFaDhkL4PCAENNzcqOCAakwggGlMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUQ/OiAAk5AAqjN5apYfVwt/M4S5UwHwYDVR0jBBgwFoAUcYYwphR8Ym/599b0BRp/X//rb6wwWQYDVR0RAQH/BE8wTYFLaW5zZWN1cmUtY2xvdWR0b3Atc2hhcmVkLXVzZXJAY2xvdWR0b3AtcHJvZC11cy1lYXN0LmlhbS5nc2VydmljZWFjY291bnQuY29tMCkGCisGAQQBg78wAQEEG2h0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbTArBgorBgEEAYO/MAEIBB0MG2h0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbTCBigYKKwYBBAHWeQIEAgR8BHoAeAB2ACswvNxoiMni4dgmKV50H0g5MZYC8pwzy15DQP6yrIZ6AAABl1aEEo4AAAQDAEcwRQIhAJzFA8xqE8owuQqk9ao7NLQy/YoTsy23A+ZU3cdL+MM1AiAZyN3FSWf13Fl3oL+P5jAvv0xRyqGrWEyZJw4KO7XhnDAKBggqhkjOPQQDAwNoADBlAjA9OgkRsqwLbt59TB0Jb15NBBQiaNBRRqUdo2FuSrvEWWDnnynmqo0GygnbCmz2CJwCMQDFCWJExAUGX7v5UQUzDz1pc1b0WvX1wAP2fhbgir2yZZRcsr4OdWz31arOo6USvVI="}, "tlogEntries": [{"logIndex": "689", "logId": {"keyId": "8w1amZ2S5mJIQkQmPxdMuOrL/oJkvFg9MnQXmeOCXck="}, "kindVersion": {"kind": "dsse", "version": "0.0.2"}, "inclusionProof": {"logIndex": "689", "rootHash": "VLopDAB81ENEy7SM2Oe4gxf026TulneLw22pUPlt0qE=", "treeSize": "690", "hashes": ["7G2mWiDIVCMp4cUCF9+qqADG/ICLRt3I2I9nqIWaKnA=", "/Fm4+swicRuu0gv27PWsZ2C1hw3IbCcatPnSV6oTbOw=", "9AF3UpKoSTEa5MS8BHGJxKHH9zVkJgn29s03k14ZtdI=", "QMesRTEZdIgthOEinYE/9J7wGv+VmArDZTICj9POmhY=", "UNUMG62rMwoqCqFKknh4R5Ubkf5Z6dj+Pk0m/1xu8uo="], "checkpoint": {"envelope": "log2025-alpha1.rekor.sigstage.dev\n690\nVLopDAB81ENEy7SM2Oe4gxf026TulneLw22pUPlt0qE=\n\n\u2014 log2025-alpha1.rekor.sigstage.dev 8w1amfdsl47Li2mk9esQ1K+vF9tg8WCLlNKBcoVTzrHr4howD6z2171ij8XW6d48AUEoV4PK1DDz5jHUlCQ98okwLQw=\n"}}, "canonicalizedBody": "eyJhcGlWZXJzaW9uIjoiMC4wLjIiLCJraW5kIjoiZHNzZSIsInNwZWMiOnsiZHNzZVYwMDIiOnsicGF5bG9hZEhhc2giOnsiYWxnb3JpdGhtIjoiU0hBMl8yNTYiLCJkaWdlc3QiOiI0a2QxR3VyKzFmZE1wMHVBZFJyQnBQYTZONXB3OWx0b25pZXdlekg4MmhvPSJ9LCJzaWduYXR1cmVzIjpbeyJjb250ZW50IjoiTUVZQ0lRQ3F6dEJCTXpiYmU3alN6NXFQOE93U3hKWDBFb0VTSGg5d21uRXljUzd3S3dJaEFMd1BIaWt0b2dRY3greFZMWEhsSU56dTI1clRTNW5YRkJ3OEtxcXp5OGZkIiwidmVyaWZpZXIiOnsia2V5RGV0YWlscyI6IlBLSVhfRUNEU0FfUDI1Nl9TSEFfMjU2IiwieDUwOUNlcnRpZmljYXRlIjp7InJhd0J5dGVzIjoiTUlJREJEQ0NBb3FnQXdJQkFnSVVZbFphZnF5ZStQL2JXU01TZHZ4cnI3eStOVUV3Q2dZSUtvWkl6ajBFQXdNd056RVZNQk1HQTFVRUNoTU1jMmxuYzNSdmNtVXVaR1YyTVI0d0hBWURWUVFERXhWemFXZHpkRzl5WlMxcGJuUmxjbTFsWkdsaGRHVXdIaGNOTWpVd05qQTVNakV3TmpJMVdoY05NalV3TmpBNU1qRXhOakkxV2pBQU1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRXdEajlYQjJycmtVVGFDZ1BFM09HUEorMTc2RVpNM3UyU0syWExLb01VUW43OXp5d2hvY2FoVlB5YnpuLzZuTWtXa2V3OFNGYURoa0w0UENBRU5OemNxT0NBYWt3Z2dHbE1BNEdBMVVkRHdFQi93UUVBd0lIZ0RBVEJnTlZIU1VFRERBS0JnZ3JCZ0VGQlFjREF6QWRCZ05WSFE0RUZnUVVRL09pQUFrNUFBcWpONWFwWWZWd3QvTTRTNVV3SHdZRFZSMGpCQmd3Rm9BVWNZWXdwaFI4WW0vNTk5YjBCUnAvWC8vcmI2d3dXUVlEVlIwUkFRSC9CRTh3VFlGTGFXNXpaV04xY21VdFkyeHZkV1IwYjNBdGMyaGhjbVZrTFhWelpYSkFZMnh2ZFdSMGIzQXRjSEp2WkMxMWN5MWxZWE4wTG1saGJTNW5jMlZ5ZG1salpXRmpZMjkxYm5RdVkyOXRNQ2tHQ2lzR0FRUUJnNzh3QVFFRUcyaDBkSEJ6T2k4dllXTmpiM1Z1ZEhNdVoyOXZaMnhsTG1OdmJUQXJCZ29yQmdFRUFZTy9NQUVJQkIwTUcyaDBkSEJ6T2k4dllXTmpiM1Z1ZEhNdVoyOXZaMnhsTG1OdmJUQ0JpZ1lLS3dZQkJBSFdlUUlFQWdSOEJIb0FlQUIyQUNzd3ZOeG9pTW5pNGRnbUtWNTBIMGc1TVpZQzhwd3p5MTVEUVA2eXJJWjZBQUFCbDFhRUVvNEFBQVFEQUVjd1JRSWhBSnpGQTh4cUU4b3d1UXFrOWFvN05MUXkvWW9Uc3kyM0ErWlUzY2RMK01NMUFpQVp5TjNGU1dmMTNGbDNvTCtQNWpBdnYweFJ5cUdyV0V5Wkp3NEtPN1hobkRBS0JnZ3Foa2pPUFFRREF3Tm9BREJsQWpBOU9na1JzcXdMYnQ1OVRCMEpiMTVOQkJRaWFOQlJScVVkbzJGdVNydkVXV0RubnlubXFvMEd5Z25iQ216MkNKd0NNUURGQ1dKRXhBVUdYN3Y1VVFVekR6MXBjMWIwV3ZYMXdBUDJmaGJnaXIyeVpaUmNzcjRPZFd6MzFhck9vNlVTdlZJPSJ9fX1dfX19"}], "timestampVerificationData": {"rfc3161Timestamps": [{"signedTimestamp": "MIIE5zADAgEAMIIE3gYJKoZIhvcNAQcCoIIEzzCCBMsCAQMxDTALBglghkgBZQMEAgEwgcEGCyqGSIb3DQEJEAEEoIGxBIGuMIGrAgEBBgkrBgEEAYO/MAIwMTANBglghkgBZQMEAgEFAAQg7mKrZuedCow8ht74HmPFNT7ZP18+JAF/WDRwwOFuzn8CFBKaF0PyLXni4RkH6K+ZuzF9x2JcGA8yMDI1MDYwOTIxMDYyOFowAwIBAQIIWJ9Fv2Y6K7CgMqQwMC4xFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEVMBMGA1UEAxMMc2lnc3RvcmUtdHNhoIICEzCCAg8wggGWoAMCAQICFAo1oQZh1eJBc8aJlqfyffJ+A3ynMAoGCCqGSM49BAMDMDkxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEgMB4GA1UEAxMXc2lnc3RvcmUtdHNhLXNlbGZzaWduZWQwHhcNMjUwMzI4MDkxNDA2WhcNMzUwMzI2MDgxNDA2WjAuMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxFTATBgNVBAMTDHNpZ3N0b3JlLXRzYTB2MBAGByqGSM49AgEGBSuBBAAiA2IABMdb+Rdx6Q/XoB7pJ6QRZUc+0AUQybuGnlc7fcyS0WNJb5sdZRe1gTNnPQDfGRj0LJg6h5STdkf+/kcS5L5S85HNfSDsd/Le5hhhHAe2oFA3Qhfyst0Uy0itF6P9AIB0HaNqMGgwDgYDVR0PAQH/BAQDAgeAMB0GA1UdDgQWBBSo/GT2KN4u5jtzT1SMUsThnN1TpTAfBgNVHSMEGDAWgBQ7IEZZXrUyTUcwzm5j7nN0R/IEfTAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAKBggqhkjOPQQDAwNnADBkAjBEr1UuhhrRd9/idfU38BDViV40b+ItPx0BcC1EpF+k31e4NJxvFZ6jRyS7xKQLTo0CMFA97ssE16K0D9Q4G1dPaxfWHp/ghKrP4hKYniVj7LdvNEkjmeTWvncj1ZPf/EhZOjGCAdowggHWAgEBMFEwOTEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSAwHgYDVQQDExdzaWdzdG9yZS10c2Etc2VsZnNpZ25lZAIUCjWhBmHV4kFzxomWp/J98n4DfKcwCwYJYIZIAWUDBAIBoIH8MBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAcBgkqhkiG9w0BCQUxDxcNMjUwNjA5MjEwNjI4WjAvBgkqhkiG9w0BCQQxIgQgm3w3T24hj0XJHfurAzfPAUM+UpN9mOfHY9jwsQe6eYkwgY4GCyqGSIb3DQEJEAIvMX8wfTB7MHkEIAb0/+BH/rNZmbczsNejI1Ac/BjkwDNmqEXXdTbnSydEMFUwPaQ7MDkxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEgMB4GA1UEAxMXc2lnc3RvcmUtdHNhLXNlbGZzaWduZWQCFAo1oQZh1eJBc8aJlqfyffJ+A3ynMAoGCCqGSM49BAMCBGYwZAIwJQ/ArYnYtKS38pLXrZ1A/CT1VGgDRUoSkslIGKlHU98qwoWUjjgmmdbeYakSqfENAjABbYaUoMwznhyQd8CKMo7f092Z3Plwa/enOQqgmyu1dAPpmD8rYr2VEjVEGKcvVoY="}]}}, "dsseEnvelope": {"payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoiYS50eHQiLCJkaWdlc3QiOnsic2hhMjU2IjoiZTI0OGE1ZGI0OTMzZGJhNjU3ODIwMDIzOGM5MWE1N2Y1ZTY1YjkyNWI3MzA1MGFlNzg2OTMzNDY4YjdhYzEwMSJ9fV0sInByZWRpY2F0ZVR5cGUiOiJodHRwczovL3Nsc2EuZGV2L3Byb3ZlbmFuY2UvdjEiLCJwcmVkaWNhdGUiOnsiYnVpbGREZWZpbml0aW9uIjp7ImJ1aWxkVHlwZSI6Imh0dHBzOi8vYWN0aW9ucy5naXRodWIuaW8vYnVpbGR0eXBlcy93b3JrZmxvdy92MSIsImV4dGVybmFsUGFyYW1ldGVycyI6eyJ3b3JrZmxvdyI6eyJyZWYiOiJyZWZzL3RhZ3MvMS4yMS4wIiwicmVwb3NpdG9yeSI6Imh0dHBzOi8vZ2l0aHViLmNvbS9vY3RvLW9yZy9vY3RvLXJlcG8iLCJwYXRoIjoiLmdpdGh1Yi93b3JrZmxvd3MvY2kueWFtbCJ9fSwiaW50ZXJuYWxQYXJhbWV0ZXJzIjp7ImdpdGh1YiI6eyJldmVudF9uYW1lIjoicHVzaCIsInJlcG9zaXRvcnlfaWQiOiIwMDAwMDAwMDAiLCJyZXBvc2l0b3J5X293bmVyX2lkIjoiMDAwMDAwMCIsInJ1bm5lcl9lbnZpcm9ubWVudCI6ImdpdGh1Yi1ob3N0ZWQifX0sInJlc29sdmVkRGVwZW5kZW5jaWVzIjpbeyJ1cmkiOiJnaXQraHR0cHM6Ly9naXRodWIuY29tL29jdG8tb3JnL29jdG8tcmVwb0ByZWZzL3RhZ3MvMS4yMS4wIiwiZGlnZXN0Ijp7ImdpdENvbW1pdCI6IjFhYzkzY2UyMWVlNTI2YjM2ZmQxNTRiOTA1OGQ5N2RmYWE0MjRjNTAifX1dfSwicnVuRGV0YWlscyI6eyJidWlsZGVyIjp7ImlkIjoiaHR0cHM6Ly9naXRodWIuY29tL29jdG8tb3JnL29jdG8tcmVwby8uZ2l0aHViL3dvcmtmbG93cy9kb2NrZXIueWFtbEByZWZzL2hlYWRzL2RldmVsb3BtZW50In0sIm1ldGFkYXRhIjp7Imludm9jYXRpb25JZCI6Imh0dHBzOi8vZ2l0aHViLmNvbS9vY3RvLW9yZy9vY3RvLXJlcG8vYWN0aW9ucy9ydW5zLzEwMzEzOTgzMjE4L2F0dGVtcHRzLzIifX19fQ==", "payloadType": "application/vnd.in-toto+json", "signatures": [{"sig": "MEYCIQCqztBBMzbbe7jSz5qP8OwSxJX0EoESHh9wmnEycS7wKwIhALwPHiktogQcx+xVLXHlINzu25rTS5nXFBw8Kqqzy8fd"}]}} +{ + "mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json", + "verificationMaterial": { + "certificate": { + "rawBytes": "MIIIPjCCB8OgAwIBAgIUeM3NlSziGqyL9eZI60oZxLb9zagwCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjYwNTEzMTkyMzMyWhcNMjYwNTEzMTkzMzMyWjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE1QyGuXvW6qw/A8ZGhPMkTgNtuI5KD78Melrj2MiRes4Q/35YI+tC14XKYjQDQDgIOVODYXt948sJw9zG7gXMkKOCBuIwggbeMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUMz1orfizj2bmeTSSIKuK9zh5iPcwHwYDVR0jBBgwFoAUcYYwphR8Ym/599b0BRp/X//rb6wwgaUGA1UdEQEB/wSBmjCBl4aBlGh0dHBzOi8vZ2l0aHViLmNvbS9zaWdzdG9yZS1jb25mb3JtYW5jZS9leHRyZW1lbHktZGFuZ2Vyb3VzLXB1YmxpYy1vaWRjLWJlYWNvbi8uZ2l0aHViL3dvcmtmbG93cy9leHRyZW1lbHktZGFuZ2Vyb3VzLW9pZGMtYmVhY29uLnltbEByZWZzL2hlYWRzL21haW4wOQYKKwYBBAGDvzABAQQraHR0cHM6Ly90b2tlbi5hY3Rpb25zLmdpdGh1YnVzZXJjb250ZW50LmNvbTAfBgorBgEEAYO/MAECBBF3b3JrZmxvd19kaXNwYXRjaDA2BgorBgEEAYO/MAEDBCg5NzdlOGQwODU1NGNhOWJkYTI3OWM5NmU5MWJlMjExOWZiNjdjNzMwMC0GCisGAQQBg78wAQQEH0V4dHJlbWVseSBkYW5nZXJvdXMgT0lEQyBiZWFjb24wSQYKKwYBBAGDvzABBQQ7c2lnc3RvcmUtY29uZm9ybWFuY2UvZXh0cmVtZWx5LWRhbmdlcm91cy1wdWJsaWMtb2lkYy1iZWFjb24wHQYKKwYBBAGDvzABBgQPcmVmcy9oZWFkcy9tYWluMDsGCisGAQQBg78wAQgELQwraHR0cHM6Ly90b2tlbi5hY3Rpb25zLmdpdGh1YnVzZXJjb250ZW50LmNvbTCBpgYKKwYBBAGDvzABCQSBlwyBlGh0dHBzOi8vZ2l0aHViLmNvbS9zaWdzdG9yZS1jb25mb3JtYW5jZS9leHRyZW1lbHktZGFuZ2Vyb3VzLXB1YmxpYy1vaWRjLWJlYWNvbi8uZ2l0aHViL3dvcmtmbG93cy9leHRyZW1lbHktZGFuZ2Vyb3VzLW9pZGMtYmVhY29uLnltbEByZWZzL2hlYWRzL21haW4wOAYKKwYBBAGDvzABCgQqDCg5NzdlOGQwODU1NGNhOWJkYTI3OWM5NmU5MWJlMjExOWZiNjdjNzMwMB0GCisGAQQBg78wAQsEDwwNZ2l0aHViLWhvc3RlZDBeBgorBgEEAYO/MAEMBFAMTmh0dHBzOi8vZ2l0aHViLmNvbS9zaWdzdG9yZS1jb25mb3JtYW5jZS9leHRyZW1lbHktZGFuZ2Vyb3VzLXB1YmxpYy1vaWRjLWJlYWNvbjA4BgorBgEEAYO/MAENBCoMKDk3N2U4ZDA4NTU0Y2E5YmRhMjc5Yzk2ZTkxYmUyMTE5ZmI2N2M3MzAwHwYKKwYBBAGDvzABDgQRDA9yZWZzL2hlYWRzL21haW4wGQYKKwYBBAGDvzABDwQLDAk2MzI1OTY4OTcwNwYKKwYBBAGDvzABEAQpDCdodHRwczovL2dpdGh1Yi5jb20vc2lnc3RvcmUtY29uZm9ybWFuY2UwGQYKKwYBBAGDvzABEQQLDAkxMzE4MDQ1NjMwgaYGCisGAQQBg78wARIEgZcMgZRodHRwczovL2dpdGh1Yi5jb20vc2lnc3RvcmUtY29uZm9ybWFuY2UvZXh0cmVtZWx5LWRhbmdlcm91cy1wdWJsaWMtb2lkYy1iZWFjb24vLmdpdGh1Yi93b3JrZmxvd3MvZXh0cmVtZWx5LWRhbmdlcm91cy1vaWRjLWJlYWNvbi55bWxAcmVmcy9oZWFkcy9tYWluMDgGCisGAQQBg78wARMEKgwoOTc3ZThkMDg1NTRjYTliZGEyNzljOTZlOTFiZTIxMTlmYjY3YzczMDAhBgorBgEEAYO/MAEUBBMMEXdvcmtmbG93X2Rpc3BhdGNoMIGCBgorBgEEAYO/MAEVBHQMcmh0dHBzOi8vZ2l0aHViLmNvbS9zaWdzdG9yZS1jb25mb3JtYW5jZS9leHRyZW1lbHktZGFuZ2Vyb3VzLXB1YmxpYy1vaWRjLWJlYWNvbi9hY3Rpb25zL3J1bnMvMjU4MjEyODIwNTkvYXR0ZW1wdHMvMTAWBgorBgEEAYO/MAEWBAgMBnB1YmxpYzCBlQYKKwYBBAHWeQIEAgSBhgSBgwCBAH8APmBxU3RuGJLgkLI2sUl2Jy9ntE1vks5vdxFKtyKgprYAAAGeIstZkAAIAAAFAAAN3hQEAwBIMEYCIQC21zfS/kOLTPP8uvSrAJ286X4dw5beF3pgCtHp3FsvbAIhAMR7wwTWf9++dtLPpMlRmvzOC2gwgS4j9f7eRLw2txXMMAoGCCqGSM49BAMDA2kAMGYCMQDoTVmOTK23t5eE/1BMr7GPxiyvF3+D8XJ/Okx7TRHuizJjlNF5dNRWLXRkp9okFrcCMQDQppyt8F1tbFc8mQqirwA90eyEJfBVjG4D0loN6t7JwKrO+K/QuoE8B1mozTZixIc=" + }, + "tlogEntries": [ + { + "logIndex": "4026478", + "logId": { + "keyId": "09OnDKEw7/hpZiYVPoTRzRbglHk0sylsUovegnRUlJY=" + }, + "kindVersion": { + "kind": "hashedrekord", + "version": "0.0.2" + }, + "inclusionProof": { + "logIndex": "4026478", + "rootHash": "5iaX4xK6PMnp4ZFTP4iywMcwLgaTjzgRTkrTOUFIrfY=", + "treeSize": "4026479", + "hashes": [ + "y9eH/Cl/glEuLMKtwV0bgZ+a1P/AjoPyvu/iUeanaIM=", + "nrNQ2vb5JM2No6zELZt05onTYzqQOwDUcs24jAa6lzk=", + "HfZO2uZZbokwORUD/0dMRpBEouo33Ab2nQ2N6r4v688=", + "grMdYn8tE73JvJU8Ixk4hl8/fKu06hUdgpPNsuAfb7o=", + "i1gQ61xe3f9MP09y2e9Z4n5ZnDiFuqQmTK/u7Gji4Ws=", + "TkPA4Mv4plM3c6oejf5r+PqVxfMPk0x+QAjumzaswsU=", + "wGq46RfkA9tCYZ2CCOOMGQttyhHIqMksBAl+soDBgRw=", + "WPWftR+8Qjv3qiIBp48n3PiweejcpuHjokhaSIJaUOo=", + "kgGccNZjCDNKiQBUSPorZcTXoy9T0OIygUNX2V9tF7s=", + "7vMeapYqpjDZc8itt3uF6dGxL5qkbFsQ18jBiblH/5g=", + "ma8nVjhpJ0JmgSzaxhxP7bvHXtiZC6nJXIBmDV8kNAI=", + "CZJWJtYZFYWwC+DsEiCMnWled/ED66viI0Wz/L+9vxk=", + "Y8Q9QaTpqRAlSoWnNwyYGDerVLL1f6b8osmbDYEF6og=" + ], + "checkpoint": { + "envelope": "log2025-alpha3.rekor.sigstage.dev\n4026479\n5iaX4xK6PMnp4ZFTP4iywMcwLgaTjzgRTkrTOUFIrfY=\n\n— log2025-alpha3.rekor.sigstage.dev 09OnDHwVrKeXjBYQ4NJ5EoENdtpBwZcV3X8n+cAZFIk+8LBPKEpZcKqoN2CfjGw0zlQwt+U2pLt5UyYEFv+STPHPmQg=\n— witness.stagemole.eu Z/euoAAAAABqBM+21mV7JbHwWmLP20ZSpNWneHMkITOPopt+Vj1MoJSILqOIPpx93pbRfA0U+Eq5IiJjEUhG1JwGhPCJEUT5yDcFDg==\n— staging.witness.transparency.goog/ring-any-bells LhqNyQAAAABqBM+2Ob9EtpF9h6IUMXc/LTRkkXiNAgbbohBxHekby1oASdDtPLkgZLmBHH73DF25f2L8xGgp/qVwoZK5thKXc6XnCw==\n— witness.navigli.sunlight.geomys.org o+AP4gAAAABqBM+2NoYlK4g3lkC5QltDB6f2F9U8yF6+8kXKjLKHAYvSDJ3u5dZufSU3bz+AtJMdOoMixoFJBkQ397WY/mE0i9tlCg==\n" + } + }, + "canonicalizedBody": "eyJhcGlWZXJzaW9uIjoiMC4wLjIiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJoYXNoZWRSZWtvcmRWMDAyIjp7ImRhdGEiOnsiYWxnb3JpdGhtIjoiU0hBMl8yNTYiLCJkaWdlc3QiOiJWL0lUWEZsTWY1Tmc1NXBVSXNLUitJZU5TdVhGT2J3RTNLUmRwajE4TGhFPSJ9LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FVUNJUUNRUTExbnNvVGFPNTIvTUM0OGcwN21qQVRkeEQ4Nnl3ZHUzTnhKVWpIN3lBSWdXR2MrTVIzZUlBb1d1OEpFMVFNMllEc2xwb0ZBek1WV3BOMTE1ZkNLdXFnPSIsInZlcmlmaWVyIjp7ImtleURldGFpbHMiOiJQS0lYX0VDRFNBX1AyNTZfU0hBXzI1NiIsIng1MDlDZXJ0aWZpY2F0ZSI6eyJyYXdCeXRlcyI6Ik1JSUlQakNDQjhPZ0F3SUJBZ0lVZU0zTmxTemlHcXlMOWVaSTYwb1p4TGI5emFnd0NnWUlLb1pJemowRUF3TXdOekVWTUJNR0ExVUVDaE1NYzJsbmMzUnZjbVV1WkdWMk1SNHdIQVlEVlFRREV4VnphV2R6ZEc5eVpTMXBiblJsY20xbFpHbGhkR1V3SGhjTk1qWXdOVEV6TVRreU16TXlXaGNOTWpZd05URXpNVGt6TXpNeVdqQUFNRmt3RXdZSEtvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUUxUXlHdVh2VzZxdy9BOFpHaFBNa1RnTnR1STVLRDc4TWVscmoyTWlSZXM0US8zNVlJK3RDMTRYS1lqUURRRGdJT1ZPRFlYdDk0OHNKdzl6RzdnWE1rS09DQnVJd2dnYmVNQTRHQTFVZER3RUIvd1FFQXdJSGdEQVRCZ05WSFNVRUREQUtCZ2dyQmdFRkJRY0RBekFkQmdOVkhRNEVGZ1FVTXoxb3JmaXpqMmJtZVRTU0lLdUs5emg1aVBjd0h3WURWUjBqQkJnd0ZvQVVjWVl3cGhSOFltLzU5OWIwQlJwL1gvL3JiNnd3Z2FVR0ExVWRFUUVCL3dTQm1qQ0JsNGFCbEdoMGRIQnpPaTh2WjJsMGFIVmlMbU52YlM5emFXZHpkRzl5WlMxamIyNW1iM0p0WVc1alpTOWxlSFJ5WlcxbGJIa3RaR0Z1WjJWeWIzVnpMWEIxWW14cFl5MXZhV1JqTFdKbFlXTnZiaTh1WjJsMGFIVmlMM2R2Y210bWJHOTNjeTlsZUhSeVpXMWxiSGt0WkdGdVoyVnliM1Z6TFc5cFpHTXRZbVZoWTI5dUxubHRiRUJ5WldaekwyaGxZV1J6TDIxaGFXNHdPUVlLS3dZQkJBR0R2ekFCQVFRcmFIUjBjSE02THk5MGIydGxiaTVoWTNScGIyNXpMbWRwZEdoMVluVnpaWEpqYjI1MFpXNTBMbU52YlRBZkJnb3JCZ0VFQVlPL01BRUNCQkYzYjNKclpteHZkMTlrYVhOd1lYUmphREEyQmdvckJnRUVBWU8vTUFFREJDZzVOemRsT0dRd09EVTFOR05oT1dKa1lUSTNPV001Tm1VNU1XSmxNakV4T1daaU5qZGpOek13TUMwR0Npc0dBUVFCZzc4d0FRUUVIMFY0ZEhKbGJXVnNlU0JrWVc1blpYSnZkWE1nVDBsRVF5QmlaV0ZqYjI0d1NRWUtLd1lCQkFHRHZ6QUJCUVE3YzJsbmMzUnZjbVV0WTI5dVptOXliV0Z1WTJVdlpYaDBjbVZ0Wld4NUxXUmhibWRsY205MWN5MXdkV0pzYVdNdGIybGtZeTFpWldGamIyNHdIUVlLS3dZQkJBR0R2ekFCQmdRUGNtVm1jeTlvWldGa2N5OXRZV2x1TURzR0Npc0dBUVFCZzc4d0FRZ0VMUXdyYUhSMGNITTZMeTkwYjJ0bGJpNWhZM1JwYjI1ekxtZHBkR2gxWW5WelpYSmpiMjUwWlc1MExtTnZiVENCcGdZS0t3WUJCQUdEdnpBQkNRU0Jsd3lCbEdoMGRIQnpPaTh2WjJsMGFIVmlMbU52YlM5emFXZHpkRzl5WlMxamIyNW1iM0p0WVc1alpTOWxlSFJ5WlcxbGJIa3RaR0Z1WjJWeWIzVnpMWEIxWW14cFl5MXZhV1JqTFdKbFlXTnZiaTh1WjJsMGFIVmlMM2R2Y210bWJHOTNjeTlsZUhSeVpXMWxiSGt0WkdGdVoyVnliM1Z6TFc5cFpHTXRZbVZoWTI5dUxubHRiRUJ5WldaekwyaGxZV1J6TDIxaGFXNHdPQVlLS3dZQkJBR0R2ekFCQ2dRcURDZzVOemRsT0dRd09EVTFOR05oT1dKa1lUSTNPV001Tm1VNU1XSmxNakV4T1daaU5qZGpOek13TUIwR0Npc0dBUVFCZzc4d0FRc0VEd3dOWjJsMGFIVmlMV2h2YzNSbFpEQmVCZ29yQmdFRUFZTy9NQUVNQkZBTVRtaDBkSEJ6T2k4dloybDBhSFZpTG1OdmJTOXphV2R6ZEc5eVpTMWpiMjVtYjNKdFlXNWpaUzlsZUhSeVpXMWxiSGt0WkdGdVoyVnliM1Z6TFhCMVlteHBZeTF2YVdSakxXSmxZV052YmpBNEJnb3JCZ0VFQVlPL01BRU5CQ29NS0RrM04yVTRaREE0TlRVMFkyRTVZbVJoTWpjNVl6azJaVGt4WW1VeU1URTVabUkyTjJNM016QXdId1lLS3dZQkJBR0R2ekFCRGdRUkRBOXlaV1p6TDJobFlXUnpMMjFoYVc0d0dRWUtLd1lCQkFHRHZ6QUJEd1FMREFrMk16STFPVFk0T1Rjd053WUtLd1lCQkFHRHZ6QUJFQVFwRENkb2RIUndjem92TDJkcGRHaDFZaTVqYjIwdmMybG5jM1J2Y21VdFkyOXVabTl5YldGdVkyVXdHUVlLS3dZQkJBR0R2ekFCRVFRTERBa3hNekU0TURRMU5qTXdnYVlHQ2lzR0FRUUJnNzh3QVJJRWdaY01nWlJvZEhSd2N6b3ZMMmRwZEdoMVlpNWpiMjB2YzJsbmMzUnZjbVV0WTI5dVptOXliV0Z1WTJVdlpYaDBjbVZ0Wld4NUxXUmhibWRsY205MWN5MXdkV0pzYVdNdGIybGtZeTFpWldGamIyNHZMbWRwZEdoMVlpOTNiM0pyWm14dmQzTXZaWGgwY21WdFpXeDVMV1JoYm1kbGNtOTFjeTF2YVdSakxXSmxZV052Ymk1NWJXeEFjbVZtY3k5b1pXRmtjeTl0WVdsdU1EZ0dDaXNHQVFRQmc3OHdBUk1FS2d3b09UYzNaVGhrTURnMU5UUmpZVGxpWkdFeU56bGpPVFpsT1RGaVpUSXhNVGxtWWpZM1l6Y3pNREFoQmdvckJnRUVBWU8vTUFFVUJCTU1FWGR2Y210bWJHOTNYMlJwYzNCaGRHTm9NSUdDQmdvckJnRUVBWU8vTUFFVkJIUU1jbWgwZEhCek9pOHZaMmwwYUhWaUxtTnZiUzl6YVdkemRHOXlaUzFqYjI1bWIzSnRZVzVqWlM5bGVIUnlaVzFsYkhrdFpHRnVaMlZ5YjNWekxYQjFZbXhwWXkxdmFXUmpMV0psWVdOdmJpOWhZM1JwYjI1ekwzSjFibk12TWpVNE1qRXlPREl3TlRrdllYUjBaVzF3ZEhNdk1UQVdCZ29yQmdFRUFZTy9NQUVXQkFnTUJuQjFZbXhwWXpDQmxRWUtLd1lCQkFIV2VRSUVBZ1NCaGdTQmd3Q0JBSDhBUG1CeFUzUnVHSkxna0xJMnNVbDJKeTludEUxdmtzNXZkeEZLdHlLZ3ByWUFBQUdlSXN0WmtBQUlBQUFGQUFBTjNoUUVBd0JJTUVZQ0lRQzIxemZTL2tPTFRQUDh1dlNyQUoyODZYNGR3NWJlRjNwZ0N0SHAzRnN2YkFJaEFNUjd3d1RXZjkrK2R0TFBwTWxSbXZ6T0MyZ3dnUzRqOWY3ZVJMdzJ0eFhNTUFvR0NDcUdTTTQ5QkFNREEya0FNR1lDTVFEb1RWbU9USzIzdDVlRS8xQk1yN0dQeGl5dkYzK0Q4WEovT2t4N1RSSHVpekpqbE5GNWROUldMWFJrcDlva0ZyY0NNUURRcHB5dDhGMXRiRmM4bVFxaXJ3QTkwZXlFSmZCVmpHNEQwbG9ONnQ3SndLck8rSy9RdW9FOEIxbW96VFppeEljPSJ9fX19fX0=" + } + ], + "timestampVerificationData": { + "rfc3161Timestamps": [ + { + "signedTimestamp": "MIICyTADAgEAMIICwAYJKoZIhvcNAQcCoIICsTCCAq0CAQMxDTALBglghkgBZQMEAgEwgbcGCyqGSIb3DQEJEAEEoIGnBIGkMIGhAgEBBgkrBgEEAYO/MAIwMTANBglghkgBZQMEAgEFAAQgjX5Xio4t+IEaOE5biG+aDNk1v4/BAvVFXgEIPk5v3fcCFGHXGiOwSbTlNRxtTBWVti9MwBAfGA8yMDI2MDUxMzE5MjMzM1owAwIBAaAypDAwLjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MRUwEwYDVQQDEwxzaWdzdG9yZS10c2GgADGCAdswggHXAgEBMFEwOTEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSAwHgYDVQQDExdzaWdzdG9yZS10c2Etc2VsZnNpZ25lZAIUCjWhBmHV4kFzxomWp/J98n4DfKcwCwYJYIZIAWUDBAIBoIH8MBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAcBgkqhkiG9w0BCQUxDxcNMjYwNTEzMTkyMzMzWjAvBgkqhkiG9w0BCQQxIgQgoeWTIE4L3Wzl4cRLipv4OUK3JoVH7Pe8PB0IJ98DzPwwgY4GCyqGSIb3DQEJEAIvMX8wfTB7MHkEIAb0/+BH/rNZmbczsNejI1Ac/BjkwDNmqEXXdTbnSydEMFUwPaQ7MDkxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEgMB4GA1UEAxMXc2lnc3RvcmUtdHNhLXNlbGZzaWduZWQCFAo1oQZh1eJBc8aJlqfyffJ+A3ynMAoGCCqGSM49BAMCBGcwZQIxAOpdA84e4XAADTKL9f+91oIdrnzrtwqVEX1Dv94Ze2Jh2urTg9a1S5iisq6FWDcoswIwKGNF0KYpD0v1AK0M5RM9K4Akf6t2O+gXuUynG7nBFFTj2U/E9IbF7iuChw1zkyEQ" + } + ] + } + }, + "dsseEnvelope": { + "payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoiYS50eHQiLCJkaWdlc3QiOnsic2hhMjU2IjoiYTBjZmM3MTI3MWQ2ZTI3OGU1N2NkMzMyZmY5NTdjM2Y3MDQzZmRkYTM1NGM0Y2JiMTkwYTMwZDU2ZWZhMDFiZiJ9fV0sInByZWRpY2F0ZVR5cGUiOiJodHRwczovL3Nsc2EuZGV2L3Byb3ZlbmFuY2UvdjEiLCJwcmVkaWNhdGUiOnsiYnVpbGREZWZpbml0aW9uIjp7ImJ1aWxkVHlwZSI6Imh0dHBzOi8vZ2l0aHViLmNvbS9zaWdzdG9yZS1jb25mb3JtYW5jZS9yZWtvcjItZHNzZS1oYXNoZWRyZWtvcmQtcmVnZW4ifSwicnVuRGV0YWlscyI6eyJidWlsZGVyIjp7ImlkIjoiaHR0cHM6Ly9naXRodWIuY29tL3NpZ3N0b3JlLWNvbmZvcm1hbmNlL3Jla29yMi1kc3NlLWhhc2hlZHJla29yZC1yZWdlbiJ9fX19", + "payloadType": "application/vnd.in-toto+json", + "signatures": [ + { + "sig": "MEUCIQCQQ11nsoTaO52/MC48g07mjATdxD86ywdu3NxJUjH7yAIgWGc+MR3eIAoWu8JE1QM2YDslpoFAzMVWpN115fCKuqg=" + } + ] + } +} \ No newline at end of file From a717f15f8267ba19e3972c9e1588c456962d1d21 Mon Sep 17 00:00:00 2001 From: Cody Soyland Date: Fri, 22 May 2026 14:20:31 -0400 Subject: [PATCH 5/7] key_details: centralize algorithm registry; use for DSSE prehash Replace the inline hash-for-key-details mappings in verifier.py and the hardcoded SHA-256 in client_v2.py with a single algorithm registry table in key_details.py, matching the spec at architecture-docs/algorithm-registry.md. Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Cody Soyland --- sigstore/_internal/key_details.py | 100 +++++++++++++++++++++++++- sigstore/_internal/rekor/client_v2.py | 13 ++-- sigstore/verify/verifier.py | 66 ++--------------- 3 files changed, 108 insertions(+), 71 deletions(-) diff --git a/sigstore/_internal/key_details.py b/sigstore/_internal/key_details.py index d3717650a..4eabf412a 100644 --- a/sigstore/_internal/key_details.py +++ b/sigstore/_internal/key_details.py @@ -13,12 +13,108 @@ # limitations under the License. """ -Utilities for getting PublicKeyDetails. +Utilities for PublicKeyDetails and the algorithm registry. """ +from __future__ import annotations + +import hashlib +from collections.abc import Callable +from dataclasses import dataclass + from cryptography.hazmat.primitives.asymmetric import ec, ed25519, padding, rsa from cryptography.x509 import Certificate -from sigstore_models.common.v1 import PublicKeyDetails +from sigstore_models.common.v1 import HashAlgorithm, PublicKeyDetails + + +@dataclass(frozen=True) +class AlgorithmDetails: + """Details for a single entry in the algorithm registry.""" + + key_details: PublicKeyDetails + hash_algorithm: HashAlgorithm + hash_func: Callable[[bytes], hashlib._Hash] | None + + +# Algorithm registry table. +# See https://github.com/sigstore/architecture-docs/blob/main/algorithm-registry.md +_ALGORITHM_REGISTRY: list[AlgorithmDetails] = [ + # RSA PKCS1v15 + AlgorithmDetails( + PublicKeyDetails.PKIX_RSA_PKCS1V15_2048_SHA256, + HashAlgorithm.SHA2_256, + hashlib.sha256, + ), + AlgorithmDetails( + PublicKeyDetails.PKIX_RSA_PKCS1V15_3072_SHA256, + HashAlgorithm.SHA2_256, + hashlib.sha256, + ), + AlgorithmDetails( + PublicKeyDetails.PKIX_RSA_PKCS1V15_4096_SHA256, + HashAlgorithm.SHA2_256, + hashlib.sha256, + ), + # ECDSA + AlgorithmDetails( + PublicKeyDetails.PKIX_ECDSA_P256_SHA_256, + HashAlgorithm.SHA2_256, + hashlib.sha256, + ), + AlgorithmDetails( + PublicKeyDetails.PKIX_ECDSA_P384_SHA_384, + HashAlgorithm.SHA2_384, + hashlib.sha384, + ), + AlgorithmDetails( + PublicKeyDetails.PKIX_ECDSA_P521_SHA_512, + HashAlgorithm.SHA2_512, + hashlib.sha512, + ), + # Ed25519 + AlgorithmDetails( + PublicKeyDetails.PKIX_ED25519, + HashAlgorithm.HASH_ALGORITHM_UNSPECIFIED, + None, + ), + AlgorithmDetails( + PublicKeyDetails.PKIX_ED25519_PH, + HashAlgorithm.SHA2_512, + hashlib.sha512, + ), +] + +_DETAILS_BY_KEY: dict[PublicKeyDetails, AlgorithmDetails] = { + entry.key_details: entry for entry in _ALGORITHM_REGISTRY +} + + +def _get_algorithm_details(key_details: PublicKeyDetails) -> AlgorithmDetails: + """ + Look up algorithm details by ``PublicKeyDetails`` enum value. + """ + details = _DETAILS_BY_KEY.get(key_details) + if details is None: + raise ValueError(f"unknown signature algorithm: {key_details}") + return details + + +def _get_prehash( + key_details: PublicKeyDetails, +) -> tuple[HashAlgorithm, Callable[[bytes], hashlib._Hash]]: + """ + Return the externalized hash function for a signing algorithm. + + Only algorithms with an externalized prehash can be used in hashedrekord + entries. Pure ed25519 (no prehash) raises ``ValueError``. + """ + details = _get_algorithm_details(key_details) + if details.hash_func is None: + raise ValueError( + f"signing algorithm {key_details} has no externalized prehash; " + "cannot be used for a hashedrekord entry (rekor-v2-spec §6.1.4)" + ) + return details.hash_algorithm, details.hash_func def _get_key_details(certificate: Certificate) -> PublicKeyDetails: diff --git a/sigstore/_internal/rekor/client_v2.py b/sigstore/_internal/rekor/client_v2.py index 9ea7e49c6..4fc6b1c6a 100644 --- a/sigstore/_internal/rekor/client_v2.py +++ b/sigstore/_internal/rekor/client_v2.py @@ -19,7 +19,6 @@ from __future__ import annotations import base64 -import hashlib import json import logging import threading @@ -32,7 +31,7 @@ from sigstore_models.rekor.v1 import TransparencyLogEntry as _TransparencyLogEntry from sigstore._internal import USER_AGENT -from sigstore._internal.key_details import _get_key_details +from sigstore._internal.key_details import _get_key_details, _get_prehash from sigstore._internal.rekor import ( EntryRequestBody, RekorClientError, @@ -144,12 +143,10 @@ def _build_dsse_request( uploaded as a hashedrekord whose digest is `Hash(envelope.pae())` and whose `signature.content` equals `envelope.signatures[0].sig`. See rekor-v2-spec §6.1.4. - - sigstore-python only signs with ECDSA P-256, so the hash function is - always SHA-256 here. A general implementation would select the hash - function from the signing algorithm per the algorithm registry. """ - digest = hashlib.sha256(envelope.pae()).digest() + key_details = _get_key_details(certificate) + _, hash_func = _get_prehash(key_details) + digest = hash_func(envelope.pae()).digest() req = rekor_v2.entry.CreateEntryRequest( hashed_rekord_request_v002=rekor_v2.hashedrekord.HashedRekordRequestV002( digest=base64.b64encode(digest), @@ -163,7 +160,7 @@ def _build_dsse_request( ) ) ), - key_details=_get_key_details(certificate), + key_details=key_details, ), ), ) diff --git a/sigstore/verify/verifier.py b/sigstore/verify/verifier.py index bb635f512..13918366e 100644 --- a/sigstore/verify/verifier.py +++ b/sigstore/verify/verifier.py @@ -19,9 +19,7 @@ from __future__ import annotations import base64 -import hashlib import logging -from collections.abc import Callable from datetime import datetime, timezone from typing import cast @@ -45,6 +43,7 @@ from sigstore_models.rekor import v2 from sigstore import dsse +from sigstore._internal.key_details import _get_key_details, _get_prehash from sigstore._internal.rekor import _hashedrekord_from_parts from sigstore._internal.rekor.client import RekorClient from sigstore._internal.sct import ( @@ -601,7 +600,7 @@ def _validate_hashedrekord_v002_dsse_entry_body(bundle: Bundle) -> None: ) expected_verifier = _v2_verifier_from_certificate(bundle.signing_certificate) - algorithm, hash_func = _hash_for_key_details(expected_verifier.key_details) + algorithm, hash_func = _get_prehash(expected_verifier.key_details) pae_digest = hash_func(envelope.pae()).digest() expected_body = v2.entry.Entry( @@ -665,69 +664,14 @@ def _v2_verifier_from_certificate(certificate: Certificate) -> v2.verifier.Verif """ Return a Rekor v2 Verifier for the signing certificate. - This method decides which signature algorithms are supported for verification - (in a rekor v2 entry), see - https://github.com/sigstore/architecture-docs/blob/main/algorithm-registry.md. - Note that actual signature verification happens in verify_artifact() and - verify_dsse(): New keytypes need to be added here and in those methods. + Key-to-algorithm mapping is handled by the algorithm registry via + `_get_key_details`. """ - public_key = certificate.public_key() - - if isinstance(public_key, ec.EllipticCurvePublicKey): - if isinstance(public_key.curve, ec.SECP256R1): - key_details = v1.PublicKeyDetails.PKIX_ECDSA_P256_SHA_256 - elif isinstance(public_key.curve, ec.SECP384R1): - key_details = v1.PublicKeyDetails.PKIX_ECDSA_P384_SHA_384 - elif isinstance(public_key.curve, ec.SECP521R1): - key_details = v1.PublicKeyDetails.PKIX_ECDSA_P521_SHA_512 - else: - raise ValueError(f"Unsupported EC curve: {public_key.curve.name}") - else: - raise ValueError(f"Unsupported public key type: {type(public_key)}") - return v2.verifier.Verifier( x509_certificate=v1.X509Certificate( raw_bytes=base64.b64encode( certificate.public_bytes(encoding=serialization.Encoding.DER) ) ), - key_details=key_details, - ) - - -def _hash_for_key_details( - key_details: v1.PublicKeyDetails, -) -> tuple[v1.HashAlgorithm, Callable[[bytes], hashlib._Hash]]: - """ - Map a `PublicKeyDetails` to the externalized hash function and matching - `HashAlgorithm` per the algorithm registry. Only signing algorithms with - an externalized prehash are eligible to sign a hashedrekord entry, so - ed25519 (pure) is rejected. - """ - sha256_algos = { - v1.PublicKeyDetails.PKIX_ECDSA_P256_SHA_256, - v1.PublicKeyDetails.PKIX_ECDSA_P256_HMAC_SHA_256, - v1.PublicKeyDetails.PKIX_RSA_PKCS1V15_2048_SHA256, - v1.PublicKeyDetails.PKIX_RSA_PKCS1V15_3072_SHA256, - v1.PublicKeyDetails.PKIX_RSA_PKCS1V15_4096_SHA256, - v1.PublicKeyDetails.PKIX_RSA_PSS_2048_SHA256, - v1.PublicKeyDetails.PKIX_RSA_PSS_3072_SHA256, - v1.PublicKeyDetails.PKIX_RSA_PSS_4096_SHA256, - v1.PublicKeyDetails.PKIX_ECDSA_P384_SHA_256, - v1.PublicKeyDetails.PKIX_ECDSA_P521_SHA_256, - } - sha384_algos = {v1.PublicKeyDetails.PKIX_ECDSA_P384_SHA_384} - sha512_algos = { - v1.PublicKeyDetails.PKIX_ECDSA_P521_SHA_512, - v1.PublicKeyDetails.PKIX_ED25519_PH, - } - if key_details in sha256_algos: - return v1.HashAlgorithm.SHA2_256, hashlib.sha256 - if key_details in sha384_algos: - return v1.HashAlgorithm.SHA2_384, hashlib.sha384 - if key_details in sha512_algos: - return v1.HashAlgorithm.SHA2_512, hashlib.sha512 - raise VerificationError( - f"signing algorithm {key_details} has no externalized prehash; " - "cannot be used for a hashedrekord entry (rekor-v2-spec §6.1.4)" + key_details=_get_key_details(certificate), ) From d3e91c756d51a2b778db1844daed15988eaacf1e Mon Sep 17 00:00:00 2001 From: Cody Soyland Date: Fri, 22 May 2026 14:23:10 -0400 Subject: [PATCH 6/7] key_details: fix HashAlgorithm for pure Ed25519 HASH_ALGORITHM_UNSPECIFIED doesn't exist in sigstore_models; use None instead, since _get_prehash already rejects algorithms with no prehash. Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Cody Soyland --- sigstore/_internal/key_details.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sigstore/_internal/key_details.py b/sigstore/_internal/key_details.py index 4eabf412a..bed3ccfd0 100644 --- a/sigstore/_internal/key_details.py +++ b/sigstore/_internal/key_details.py @@ -32,7 +32,7 @@ class AlgorithmDetails: """Details for a single entry in the algorithm registry.""" key_details: PublicKeyDetails - hash_algorithm: HashAlgorithm + hash_algorithm: HashAlgorithm | None hash_func: Callable[[bytes], hashlib._Hash] | None @@ -74,7 +74,7 @@ class AlgorithmDetails: # Ed25519 AlgorithmDetails( PublicKeyDetails.PKIX_ED25519, - HashAlgorithm.HASH_ALGORITHM_UNSPECIFIED, + None, None, ), AlgorithmDetails( From c55ffe072a53d45eb397e11ada3334e179f4501f Mon Sep 17 00:00:00 2001 From: Cody Soyland Date: Fri, 22 May 2026 17:14:45 -0400 Subject: [PATCH 7/7] key_details: narrow None check so mypy sees non-None hash_algorithm Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Cody Soyland --- sigstore/_internal/key_details.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sigstore/_internal/key_details.py b/sigstore/_internal/key_details.py index bed3ccfd0..effc0a99e 100644 --- a/sigstore/_internal/key_details.py +++ b/sigstore/_internal/key_details.py @@ -109,7 +109,7 @@ def _get_prehash( entries. Pure ed25519 (no prehash) raises ``ValueError``. """ details = _get_algorithm_details(key_details) - if details.hash_func is None: + if details.hash_algorithm is None or details.hash_func is None: raise ValueError( f"signing algorithm {key_details} has no externalized prehash; " "cannot be used for a hashedrekord entry (rekor-v2-spec §6.1.4)"