From 7f3d9d4d4969e35f1edb0eb635e3c37b03e99c01 Mon Sep 17 00:00:00 2001 From: Jussi Kukkonen Date: Mon, 9 Jun 2025 12:10:49 +0300 Subject: [PATCH 01/14] trust: Start returning RekorV2Client from signingconfig If signingconfig contains rekor v2, let's start preferring it Make sure we test the status quo (no rekor v2 in signing config) and the case where there is a rekor v2 in signing config. Signed-off-by: Jussi Kukkonen --- sigstore/_internal/trust.py | 16 ++++-- .../signingconfig-only-v1-rekor.v2.json | 53 +++++++++++++++++++ test/unit/internal/test_trust.py | 16 +++++- 3 files changed, 80 insertions(+), 5 deletions(-) create mode 100644 test/assets/signing_config/signingconfig-only-v1-rekor.v2.json diff --git a/sigstore/_internal/trust.py b/sigstore/_internal/trust.py index 8179e9770..6e8298d64 100644 --- a/sigstore/_internal/trust.py +++ b/sigstore/_internal/trust.py @@ -60,7 +60,9 @@ ) from sigstore._internal.fulcio.client import FulcioClient +from sigstore._internal.rekor import RekorLogSubmitter from sigstore._internal.rekor.client import RekorClient +from sigstore._internal.rekor.client_v2 import RekorV2Client from sigstore._internal.timestamp import TimestampAuthorityClient from sigstore._internal.tuf import DEFAULT_TUF_URL, STAGING_TUF_URL, TrustUpdater from sigstore._utils import ( @@ -73,7 +75,7 @@ from sigstore.errors import Error, MetadataError, TUFError, VerificationError # Versions supported by this client -REKOR_VERSIONS = [1] +REKOR_VERSIONS = [1, 2] TSA_VERSIONS = [1] FULCIO_VERSIONS = [1] OIDC_VERSIONS = [1] @@ -420,11 +422,19 @@ def _get_valid_services( return result[:count] - def get_tlogs(self) -> list[RekorClient]: + def get_tlogs(self) -> list[RekorLogSubmitter]: """ Returns the rekor transparency log clients to sign with. """ - return [RekorClient(tlog.url) for tlog in self._tlogs] + result: list[RekorLogSubmitter] = [] + for tlog in self._tlogs: + if tlog.major_api_version == 1: + result.append(RekorClient(tlog.url)) + elif tlog.major_api_version == 2: + result.append(RekorV2Client(tlog.url)) + else: + raise AssertionError(f"Unexpected Rekor v{tlog.major_api_version}") + return result def get_fulcio(self) -> FulcioClient: """ diff --git a/test/assets/signing_config/signingconfig-only-v1-rekor.v2.json b/test/assets/signing_config/signingconfig-only-v1-rekor.v2.json new file mode 100644 index 000000000..1a4305259 --- /dev/null +++ b/test/assets/signing_config/signingconfig-only-v1-rekor.v2.json @@ -0,0 +1,53 @@ +{ + "mediaType": "application/vnd.dev.sigstore.signingconfig.v0.2+json", + "caUrls": [ + { + "url": "https://fulcio.example.com", + "majorApiVersion": 1, + "validFor": { + "start": "2023-04-14T21:38:40Z" + } + }, + { + "url": "https://fulcio-old.example.com", + "majorApiVersion": 1, + "validFor": { + "start": "2022-04-14T21:38:40Z", + "end": "2023-04-14T21:38:40Z" + } + } + ], + "oidcUrls": [ + { + "url": "https://oauth2.example.com/auth", + "majorApiVersion": 1, + "validFor": { + "start": "2025-04-16T00:00:00Z" + } + } + ], + "rekorTlogUrls": [ + { + "url": "https://rekor.example.com", + "majorApiVersion": 1, + "validFor": { + "start": "2021-01-12T11:53:27Z" + } + } + ], + "tsaUrls": [ + { + "url": "https://timestamp.example.com/api/v1/timestamp", + "majorApiVersion": 1, + "validFor": { + "start": "2025-04-09T00:00:00Z" + } + } + ], + "rekorTlogConfig": { + "selector": "ANY" + }, + "tsaConfig": { + "selector": "ANY" + } +} diff --git a/test/unit/internal/test_trust.py b/test/unit/internal/test_trust.py index 14b00624f..4eef7f68c 100644 --- a/test/unit/internal/test_trust.py +++ b/test/unit/internal/test_trust.py @@ -28,6 +28,7 @@ from sigstore._internal.fulcio.client import FulcioClient from sigstore._internal.rekor.client import RekorClient +from sigstore._internal.rekor.client_v2 import RekorV2Client from sigstore._internal.timestamp import TimestampAuthorityClient from sigstore._internal.trust import ( CertificateAuthority, @@ -83,16 +84,27 @@ def test_good(self, asset): assert fulcio.url == "https://fulcio.example.com" assert signing_config.get_oidc_url() == "https://oauth2.example.com/auth" + # signing config contains v1 and v2, we pick v2 tlogs = signing_config.get_tlogs() assert len(tlogs) == 1 - assert isinstance(tlogs[0], RekorClient) - assert tlogs[0].url == "https://rekor.example.com/api/v1" + assert isinstance(tlogs[0], RekorV2Client) + assert tlogs[0].url == "https://rekor-v2.example.com/api/v2" tsas = signing_config.get_tsas() assert len(tsas) == 1 assert isinstance(tsas[0], TimestampAuthorityClient) assert tsas[0].url == "https://timestamp.example.com/api/v1/timestamp" + def test_good_only_v1_rekor(self, asset): + """Test case where a rekor 2 instance is not available""" + path = asset("signing_config/signingconfig-only-v1-rekor.v2.json") + signing_config = SigningConfig.from_file(path) + + tlogs = signing_config.get_tlogs() + assert len(tlogs) == 1 + assert isinstance(tlogs[0], RekorClient) + assert tlogs[0].url == "https://rekor.example.com/api/v1" + @pytest.mark.parametrize( "services, versions, config, expected_result", [ From 00e0349f7d4b56b58ac3a13d51458af7acdf2548 Mon Sep 17 00:00:00 2001 From: Jussi Kukkonen Date: Mon, 9 Jun 2025 12:23:00 +0300 Subject: [PATCH 02/14] test: Add trust config for signing with staging rekor v2 This is current staging trust root and signing config, with just the rekor v2 instance added to signing config $ TRUSTCONFIG=test/assets/trust_config/staging-but-sign-with-rekor-v2.json $ sigstore --trust-config $TRUSTCONFIG sign README.md Signed-off-by: Jussi Kukkonen --- .../staging-but-sign-with-rekor-v2.json | 183 ++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 test/assets/trust_config/staging-but-sign-with-rekor-v2.json diff --git a/test/assets/trust_config/staging-but-sign-with-rekor-v2.json b/test/assets/trust_config/staging-but-sign-with-rekor-v2.json new file mode 100644 index 000000000..3c1dd3fa0 --- /dev/null +++ b/test/assets/trust_config/staging-but-sign-with-rekor-v2.json @@ -0,0 +1,183 @@ +{ + "mediaType": "application/vnd.dev.sigstore.clienttrustconfig.v0.1+json", + "trustedRoot": { + "mediaType": "application/vnd.dev.sigstore.trustedroot+json;version=0.1", + "tlogs": [ + { + "baseUrl": "https://rekor.sigstage.dev", + "hashAlgorithm": "SHA2_256", + "publicKey": { + "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEDODRU688UYGuy54mNUlaEBiQdTE9nYLr0lg6RXowI/QV/RE1azBn4Eg5/2uTOMbhB1/gfcHzijzFi9Tk+g1Prg==", + "keyDetails": "PKIX_ECDSA_P256_SHA_256", + "validFor": { + "start": "2021-01-12T11:53:27Z" + } + }, + "logId": { + "keyId": "0y8wo8MtY5wrdiIFohx7sHeI5oKDpK5vQhGHI6G+pJY=" + } + }, + { + "baseUrl": "https://log2025-alpha1.rekor.sigstage.dev", + "hashAlgorithm": "SHA2_256", + "publicKey": { + "rawBytes": "MCowBQYDK2VwAyEAPn+AREHoBaZ7wgS1zBqpxmLSGnyhxXj4lFxSdWVB8o8=", + "keyDetails": "PKIX_ED25519", + "validFor": { + "start": "2025-04-16T00:00:00Z" + } + }, + "logId": { + "keyId": "RycrnT/11WQ15JtgBXeYVLlFYMtbAka7+JnxUQaOX5E=" + } + } + ], + "certificateAuthorities": [ + { + "subject": { + "organization": "sigstore.dev", + "commonName": "sigstore" + }, + "uri": "https://fulcio.sigstage.dev", + "certChain": { + "certificates": [ + { + "rawBytes": "MIICGTCCAaCgAwIBAgITJta/okfgHvjabGm1BOzuhrwA1TAKBggqhkjOPQQDAzAqMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIyMDQxNDIxMzg0MFoXDTMyMDMyMjE2NTA0NVowNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASosAySWJQ/tK5r8T5aHqavk0oI+BKQbnLLdmOMRXHQF/4Hx9KtNfpcdjH9hNKQSBxSlLFFN3tvFCco0qFBzWYwZtsYsBe1l91qYn/9VHFTaEVwYQWIJEEvrs0fvPuAqjajezB5MA4GA1UdDwEB/wQEAwIBBjATBgNVHSUEDDAKBggrBgEFBQcDAzASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBRxhjCmFHxib/n31vQFGn9f/+tvrDAfBgNVHSMEGDAWgBT/QjK6aH2rOnCv3AzUGuI+h49mZTAKBggqhkjOPQQDAwNnADBkAjAM1lbKkcqQlE/UspMTbWNo1y2TaJ44tx3l/FJFceTSdDZ+0W1OHHeU4twie/lq8XgCMHQxgEv26xNNiAGyPXbkYgrDPvbOqp0UeWX4mJnLSrBr3aN/KX1SBrKQu220FmVL0Q==" + }, + { + "rawBytes": "MIIB9jCCAXugAwIBAgITDdEJvluliE0AzYaIE4jTMdnFTzAKBggqhkjOPQQDAzAqMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIyMDMyNTE2NTA0NloXDTMyMDMyMjE2NTA0NVowKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABMo9BUNk9QIYisYysC24+2OytoV72YiLonYcqR3yeVnYziPt7Xv++CYE8yoCTiwedUECCWKOcvQKRCJZb9ht4Hzy+VvBx36hK+C6sECCSR0x6pPSiz+cTk1f788ZjBlUZaNjMGEwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP9CMrpofas6cK/cDNQa4j6Hj2ZlMB8GA1UdIwQYMBaAFP9CMrpofas6cK/cDNQa4j6Hj2ZlMAoGCCqGSM49BAMDA2kAMGYCMQD+kojuzMwztNay9Ibzjuk//ZL5m6T2OCsm45l1lY004pcb984L926BowodoirFMcMCMQDIJtFHhP/1D3a+M3dAGomOb6O4CmTry3TTPbPsAFnv22YA0Y+P21NVoxKDjdu0tkw=" + } + ] + }, + "validFor": { + "start": "2022-04-14T21:38:40Z" + } + } + ], + "ctlogs": [ + { + "baseUrl": "https://ctfe.sigstage.dev/test", + "hashAlgorithm": "SHA2_256", + "publicKey": { + "rawBytes": "MIICCgKCAgEA27A2MPQXm0I0v7/Ly5BIauDjRZF5Jor9vU+QheoE2UIIsZHcyYq3slHzSSHy2lLj1ZD2d91CtJ492ZXqnBmsr4TwZ9jQ05tW2mGIRI8u2DqN8LpuNYZGz/f9SZrjhQQmUttqWmtu3UoLfKz6NbNXUnoo+NhZFcFRLXJ8VporVhuiAmL7zqT53cXR3yQfFPCUDeGnRksnlhVIAJc3AHZZSHQJ8DEXMhh35TVv2nYhTI3rID7GwjXXw4ocz7RGDD37ky6p39Tl5NB71gT1eSqhZhGHEYHIPXraEBd5+3w9qIuLWlp5Ej/K6Mu4ELioXKCUimCbwy+Cs8UhHFlqcyg4AysOHJwIadXIa8LsY51jnVSGrGOEBZevopmQPNPtyfFY3dmXSS+6Z3RD2Gd6oDnNGJzpSyEk410Ag5uvNDfYzJLCWX9tU8lIxNwdFYmIwpd89HijyRyoGnoJ3entd63cvKfuuix5r+GHyKp1Xm1L5j5AWM6P+z0xigwkiXnt+adexAl1J9wdDxv/pUFEESRF4DG8DFGVtbdH6aR1A5/vD4krO4tC1QYUSeyL5Mvsw8WRqIFHcXtgybtxylljvNcGMV1KXQC8UFDmpGZVDSHx6v3e/BHMrZ7gjoCCfVMZ/cFcQi0W2AIHPYEMH/C95J2r4XbHMRdYXpovpOoT5Ca78gsCAwEAAQ==", + "keyDetails": "PKCS1_RSA_PKCS1V5", + "validFor": { + "start": "2021-03-14T00:00:00Z", + "end": "2022-07-31T00:00:00Z" + } + }, + "logId": { + "keyId": "G3wUKk6ZK6ffHh/FdCRUE2wVekyzHEEIpSG4savnv0w=" + } + }, + { + "baseUrl": "https://ctfe.sigstage.dev/2022", + "hashAlgorithm": "SHA2_256", + "publicKey": { + "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEh99xuRi6slBFd8VUJoK/rLigy4bYeSYWO/fE6Br7r0D8NpMI94+A63LR/WvLxpUUGBpY8IJA3iU2telag5CRpA==", + "keyDetails": "PKIX_ECDSA_P256_SHA_256", + "validFor": { + "start": "2022-07-01T00:00:00Z", + "end": "2022-07-31T00:00:00Z" + } + }, + "logId": { + "keyId": "++JKOMQt7SJ3ynUHnCfnDhcKP8/58J4TueMqXuk3HmA=" + } + }, + { + "baseUrl": "https://ctfe.sigstage.dev/2022-2", + "hashAlgorithm": "SHA2_256", + "publicKey": { + "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE8gEDKNme8AnXuPBgHjrtXdS6miHqc24CRblNEOFpiJRngeq8Ko73Y+K18yRYVf1DXD4AVLwvKyzdNdl5n0jUSQ==", + "keyDetails": "PKIX_ECDSA_P256_SHA_256", + "validFor": { + "start": "2022-07-01T00:00:00Z" + } + }, + "logId": { + "keyId": "KzC83GiIyeLh2CYpXnQfSDkxlgLynDPLXkNA/rKshno=" + } + } + ], + "timestampAuthorities": [ + { + "subject": { + "organization": "sigstore.dev", + "commonName": "sigstore-tsa-selfsigned" + }, + "uri": "https://timestamp.sigstage.dev/api/v1/timestamp", + "certChain": { + "certificates": [ + { + "rawBytes": "MIICDzCCAZagAwIBAgIUCjWhBmHV4kFzxomWp/J98n4DfKcwCgYIKoZIzj0EAwMwOTEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSAwHgYDVQQDExdzaWdzdG9yZS10c2Etc2VsZnNpZ25lZDAeFw0yNTAzMjgwOTE0MDZaFw0zNTAzMjYwODE0MDZaMC4xFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEVMBMGA1UEAxMMc2lnc3RvcmUtdHNhMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEx1v5F3HpD9egHuknpBFlRz7QBRDJu4aeVzt9zJLRY0lvmx1lF7WBM2c9AN8ZGPQsmDqHlJN2R/7+RxLkvlLzkc19IOx38t7mGGEcB7agUDdCF/Ky3RTLSK0Xo/0AgHQdo2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFKj8ZPYo3i7mO3NPVIxSxOGc3VOlMB8GA1UdIwQYMBaAFDsgRlletTJNRzDObmPuc3RH8gR9MBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMDA2cAMGQCMESvVS6GGtF33+J19TfwENWJXjRv4i0/HQFwLUSkX6TfV7g0nG8VnqNHJLvEpAtOjQIwUD3uywTXorQP1DgbV09rF9Yen+CEqs/iEpieJWPst280SSOZ5Na+dyPVk9/8SFk6" + }, + { + "rawBytes": "MIIB9zCCAXygAwIBAgIUCPExEFKiQh0dP4sp5ltmSYSSkFUwCgYIKoZIzj0EAwMwOTEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSAwHgYDVQQDExdzaWdzdG9yZS10c2Etc2VsZnNpZ25lZDAeFw0yNTAzMjgwOTE0MDZaFw0zNTAzMjYwODE0MDZaMDkxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEgMB4GA1UEAxMXc2lnc3RvcmUtdHNhLXNlbGZzaWduZWQwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATt0tIDWyo4ARfL9BaSo0W5bJQEbKJTU/u7llvdjSI5aTkOAJa8tixn2+LEfPG4dMFdsMPtsIuU1qn2OqFiuMk6vHv/c+az25RQVY1oo50iMb0jIL3N4FgwhPFpZnCbQPOjRTBDMA4GA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBQ7IEZZXrUyTUcwzm5j7nN0R/IEfTAKBggqhkjOPQQDAwNpADBmAjEA2MI1VXgbf3dUOSc95hSRypBKOab18eh2xzQtxUsHvWeY+1iFgyMluUuNR6taoSmFAjEA31m2czguZhKYX+4JSKu5pRYhBTXAd8KKQ3xdPRX/qCaLvT2qJAEQ1YQM3EJRrtI7" + } + ] + }, + "validFor": { + "start": "2025-04-09T00:00:00Z" + } + } + ] + }, + "signing_config": { + "mediaType": "application/vnd.dev.sigstore.signingconfig.v0.2+json", + "caUrls": [ + { + "url": "https://fulcio.sigstage.dev", + "majorApiVersion": 1, + "validFor": { + "start": "2022-04-14T21:38:40Z" + }, + "operator": "sigstore.dev" + } + ], + "oidcUrls": [ + { + "url": "https://oauth2.sigstage.dev/auth", + "majorApiVersion": 1, + "validFor": { + "start": "2025-04-16T00:00:00Z" + }, + "operator": "sigstore.dev" + } + ], + "rekorTlogUrls": [ + { + "url": "https://log2025-alpha1.rekor.sigstage.dev", + "majorApiVersion": 2, + "validFor": { + "start": "2025-06-09T00:00:00Z" + }, + "operator": "sigstore.dev" + }, + { + "url": "https://rekor.sigstage.dev", + "majorApiVersion": 1, + "validFor": { + "start": "2021-01-12T11:53:27Z" + }, + "operator": "sigstore.dev" + } + ], + "tsaUrls": [ + { + "url": "https://timestamp.sigstage.dev/api/v1/timestamp", + "majorApiVersion": 1, + "validFor": { + "start": "2025-04-09T00:00:00Z" + }, + "operator": "sigstore.dev" + } + ], + "rekorTlogConfig": { + "selector": "ANY" + }, + "tsaConfig": { + "selector": "ANY" + } + } +} \ No newline at end of file From 932a6e8bd7d7495e7e21c1a72f26bdb89e0ecda1 Mon Sep 17 00:00:00 2001 From: Jussi Kukkonen Date: Mon, 9 Jun 2025 13:23:43 +0300 Subject: [PATCH 03/14] verify: Initial support for rekor v2 verify This code is originally from Ramon, updated by Jussi $ TRUSTCONFIG=test/assets/trust_config/staging-but-sign-with-rekor-v2.json $ sigstore --trust-config $TRUSTCONFIG sign README.md $ sigstore --staging verify identity \ --cert-identity jku@goto.fi \ --cert-oidc-issuer https://github.com/login/oauth README.md OK: README.md Co-authored-by: Ramon Petgrave Signed-off-by: Jussi Kukkonen --- sigstore/verify/verifier.py | 161 ++++++++++++++++++++++++++++-------- 1 file changed, 126 insertions(+), 35 deletions(-) diff --git a/sigstore/verify/verifier.py b/sigstore/verify/verifier.py index 56d132de2..25500a265 100644 --- a/sigstore/verify/verifier.py +++ b/sigstore/verify/verifier.py @@ -25,8 +25,10 @@ import rekor_types from cryptography.exceptions import InvalidSignature +from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import ec -from cryptography.x509 import ExtendedKeyUsage, KeyUsage +from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePublicKey +from cryptography.x509 import Certificate, ExtendedKeyUsage, KeyUsage from cryptography.x509.oid import ExtendedKeyUsageOID from OpenSSL.crypto import ( X509, @@ -38,6 +40,8 @@ from pydantic import ValidationError from rfc3161_client import TimeStampResponse, VerifierBuilder from rfc3161_client import VerificationError as Rfc3161VerificationError +from sigstore_protobuf_specs.dev.sigstore.common import v1 +from sigstore_protobuf_specs.dev.sigstore.rekor import v2 from sigstore import dsse from sigstore._internal.rekor import _hashedrekord_from_parts @@ -371,6 +375,20 @@ def _verify_common_signing_cert( f"invalid signing cert: expired at time of signing, time via {vts}" ) + @staticmethod + def _get_key_details(certificate: Certificate) -> v1.PublicKeyDetails: + """Determine PublicKeyDetails from a certificate""" + public_key = certificate.public_key() + if isinstance(public_key, EllipticCurvePublicKey): + if public_key.curve.name == "secp256r1": + return cast( + v1.PublicKeyDetails, + v1.PublicKeyDetails.PKIX_ECDSA_P256_SHA_256, + ) + # TODO support other keys + raise ValueError(f"Unsupported EC curve: {public_key.curve.name}") + raise ValueError(f"Unsupported public key type: {type(public_key)}") + def verify_dsse( self, bundle: Bundle, policy: VerificationPolicy ) -> tuple[str, bytes]: @@ -418,34 +436,74 @@ def verify_dsse( # Instead, we manually pick apart the entry body below and verify # the parts we can (namely the payload hash and signature list). entry = bundle.log_entry - try: - entry_body = rekor_types.Dsse.model_validate_json( - base64.b64decode(entry.body) - ) - except ValidationError as exc: - raise VerificationError(f"invalid DSSE log entry: {exc}") - - payload_hash = sha256_digest(envelope._inner.payload).digest.hex() if ( - entry_body.spec.root.payload_hash.algorithm # type: ignore[union-attr] - != rekor_types.dsse.Algorithm.SHA256 + entry._kind_version.kind == "dsse" + and entry._kind_version.version == "0.0.2" ): - raise VerificationError("expected SHA256 payload hash in DSSE log entry") - if payload_hash != entry_body.spec.root.payload_hash.value: # type: ignore[union-attr] - raise VerificationError("log entry payload hash does not match bundle") - - # NOTE: Like `dsse._verify`: multiple signatures would be frivolous here, - # but we handle them just in case the signer has somehow produced multiple - # signatures for their envelope with the same signing key. - signatures = [ - rekor_types.dsse.Signature( - signature=base64.b64encode(signature.sig).decode(), - verifier=base64_encode_pem_cert(bundle.signing_certificate), - ) - for signature in envelope._inner.signatures - ] - if signatures != entry_body.spec.root.signatures: - raise VerificationError("log entry signatures do not match bundle") + try: + entry_body = v2.Entry().from_json(base64.b64decode(entry.body)) + except ValidationError as exc: + raise VerificationError(f"invalid DSSE log entry: {exc}") + + if ( + entry_body.spec.dsse_v002.payload_hash.algorithm + != v1.HashAlgorithm.SHA2_256 + ): + raise VerificationError("expected SHA256 hash in DSSE entry") + + payload_hash = sha256_digest(envelope._inner.payload).digest + if entry_body.spec.dsse_v002.payload_hash.digest != payload_hash: + raise VerificationError("DSSE entry payload hash does not match bundle") + + signatures = [ + v2.Signature( + content=signature.sig, + verifier=v2.Verifier( + x509_certificate=v1.X509Certificate( + bundle.signing_certificate.public_bytes( + encoding=serialization.Encoding.DER + ) + ), + key_details=self._get_key_details(bundle.signing_certificate), + ), + ) + for signature in envelope._inner.signatures + ] + if signatures != entry_body.spec.dsse_v002.signatures: + raise VerificationError("log entry signatures do not match bundle") + else: + try: + entry_body = rekor_types.Dsse.model_validate_json( + base64.b64decode(entry.body) + ) + except ValidationError as exc: + raise VerificationError(f"invalid DSSE log entry: {exc}") + + payload_hash = sha256_digest(envelope._inner.payload).digest.hex() + if ( + # type: ignore[union-attr] + entry_body.spec.root.payload_hash.algorithm + != rekor_types.dsse.Algorithm.SHA256 + ): + raise VerificationError( + "expected SHA256 payload hash in DSSE log entry" + ) + # type: ignore[union-attr] + if payload_hash != entry_body.spec.root.payload_hash.value: + raise VerificationError("log entry payload hash does not match bundle") + + # NOTE: Like `dsse._verify`: multiple signatures would be frivolous here, + # but we handle them just in case the signer has somehow produced multiple + # signatures for their envelope with the same signing key. + signatures = [ + rekor_types.dsse.Signature( + signature=base64.b64encode(signature.sig).decode(), + verifier=base64_encode_pem_cert(bundle.signing_certificate), + ) + for signature in envelope._inner.signatures + ] + if signatures != entry_body.spec.root.signatures: + raise VerificationError("log entry signatures do not match bundle") return (envelope._inner.payload_type, envelope._inner.payload) @@ -491,14 +549,47 @@ def verify_artifact( # the other bundle materials (and input being verified). entry = bundle.log_entry - expected_body = _hashedrekord_from_parts( - bundle.signing_certificate, - bundle._inner.message_signature.signature, # type: ignore[union-attr] - hashed_input, - ) - actual_body = rekor_types.Hashedrekord.model_validate_json( - base64.b64decode(entry.body) - ) + if ( + entry._kind_version.kind == "hashedrekord" + and entry._kind_version.version == "0.0.2" + ): + expected_body = v2.Entry( + kind=entry._kind_version.kind, + api_version=entry._kind_version.version, + spec=v2.Spec( + hashed_rekord_v002=v2.HashedRekordLogEntryV002( + data=v1.HashOutput( + algorithm=bundle._inner.message_signature.message_digest.algorithm, + digest=bundle._inner.message_signature.message_digest.digest, + ), + signature=v2.Signature( + content=bundle._inner.message_signature.signature, + verifier=v2.Verifier( + x509_certificate=v1.X509Certificate( + bundle.signing_certificate.public_bytes( + encoding=serialization.Encoding.DER + ) + ), + key_details=self._get_key_details( + bundle.signing_certificate + ), + ), + ), + ) + ), + ) + actual_body = v2.Entry().from_json(base64.b64decode(entry.body)) + else: + expected_body = _hashedrekord_from_parts( + bundle.signing_certificate, + # type: ignore[union-attr] + bundle._inner.message_signature.signature, + hashed_input, + ) + actual_body = rekor_types.Hashedrekord.model_validate_json( + base64.b64decode(entry.body) + ) + if expected_body != actual_body: raise VerificationError( "transparency log entry is inconsistent with other materials" From 57ea5451876b0b6cfdf93a26a35fe02e4d9aaf3d Mon Sep 17 00:00:00 2001 From: Jussi Kukkonen Date: Mon, 9 Jun 2025 13:44:25 +0300 Subject: [PATCH 04/14] verifier: Fix lint issues This makes the code quite a bit uglier: we will likely want to refactor... Signed-off-by: Jussi Kukkonen --- sigstore/verify/verifier.py | 48 +++++++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/sigstore/verify/verifier.py b/sigstore/verify/verifier.py index 25500a265..d885fde8a 100644 --- a/sigstore/verify/verifier.py +++ b/sigstore/verify/verifier.py @@ -441,21 +441,26 @@ def verify_dsse( and entry._kind_version.version == "0.0.2" ): try: - entry_body = v2.Entry().from_json(base64.b64decode(entry.body)) + v2_body = v2.Entry().from_json(base64.b64decode(entry.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 ( - entry_body.spec.dsse_v002.payload_hash.algorithm + v2_body.spec.dsse_v002.payload_hash.algorithm != v1.HashAlgorithm.SHA2_256 ): raise VerificationError("expected SHA256 hash in DSSE entry") - payload_hash = sha256_digest(envelope._inner.payload).digest - if entry_body.spec.dsse_v002.payload_hash.digest != payload_hash: + 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") - signatures = [ + v2_signatures = [ v2.Signature( content=signature.sig, verifier=v2.Verifier( @@ -469,7 +474,7 @@ def verify_dsse( ) for signature in envelope._inner.signatures ] - if signatures != entry_body.spec.dsse_v002.signatures: + if v2_signatures != v2_body.spec.dsse_v002.signatures: raise VerificationError("log entry signatures do not match bundle") else: try: @@ -481,15 +486,13 @@ def verify_dsse( payload_hash = sha256_digest(envelope._inner.payload).digest.hex() if ( - # type: ignore[union-attr] - entry_body.spec.root.payload_hash.algorithm + entry_body.spec.root.payload_hash.algorithm # type: ignore[union-attr] != rekor_types.dsse.Algorithm.SHA256 ): raise VerificationError( "expected SHA256 payload hash in DSSE log entry" ) - # type: ignore[union-attr] - if payload_hash != entry_body.spec.root.payload_hash.value: + if payload_hash != entry_body.spec.root.payload_hash.value: # type: ignore[union-attr] raise VerificationError("log entry payload hash does not match bundle") # NOTE: Like `dsse._verify`: multiple signatures would be frivolous here, @@ -553,7 +556,12 @@ def verify_artifact( entry._kind_version.kind == "hashedrekord" and entry._kind_version.version == "0.0.2" ): - expected_body = v2.Entry( + if bundle._inner.message_signature is None: + raise VerificationError( + "invalid hashedrekord log entry: missing message signature" + ) + + v2_expected_body = v2.Entry( kind=entry._kind_version.kind, api_version=entry._kind_version.version, spec=v2.Spec( @@ -578,19 +586,23 @@ def verify_artifact( ) ), ) - actual_body = v2.Entry().from_json(base64.b64decode(entry.body)) + v2_actual_body = v2.Entry().from_json(base64.b64decode(entry.body)) + if v2_expected_body != v2_actual_body: + raise VerificationError( + "transparency log entry is inconsistent with other materials" + ) + else: expected_body = _hashedrekord_from_parts( bundle.signing_certificate, - # type: ignore[union-attr] - bundle._inner.message_signature.signature, + bundle._inner.message_signature.signature, # type: ignore[union-attr] hashed_input, ) actual_body = rekor_types.Hashedrekord.model_validate_json( base64.b64decode(entry.body) ) - if expected_body != actual_body: - raise VerificationError( - "transparency log entry is inconsistent with other materials" - ) + if expected_body != actual_body: + raise VerificationError( + "transparency log entry is inconsistent with other materials" + ) From 9eec8b704270a0b430a2e291973253787f07e97f Mon Sep 17 00:00:00 2001 From: Jussi Kukkonen Date: Mon, 9 Jun 2025 15:14:50 +0300 Subject: [PATCH 05/14] tests: Add tests for signing and verifying rekorv2 bundles These are fairly basic for now. Signed-off-by: Jussi Kukkonen --- test/assets/staging-rekor-v2.txt | 5 ++++ .../assets/staging-rekor-v2.txt.sigstore.json | 1 + test/unit/conftest.py | 29 +++++++++++++++++++ test/unit/test_sign.py | 15 ++++++++++ test/unit/verify/test_verifier.py | 3 +- 5 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 test/assets/staging-rekor-v2.txt create mode 100644 test/assets/staging-rekor-v2.txt.sigstore.json diff --git a/test/assets/staging-rekor-v2.txt b/test/assets/staging-rekor-v2.txt new file mode 100644 index 000000000..1895b2228 --- /dev/null +++ b/test/assets/staging-rekor-v2.txt @@ -0,0 +1,5 @@ +DO NOT MODIFY ME! + +this is "staging-rekor-v2.txt", a sample input for sigstore-python's unit tests. + +DO NOT MODIFY ME! diff --git a/test/assets/staging-rekor-v2.txt.sigstore.json b/test/assets/staging-rekor-v2.txt.sigstore.json new file mode 100644 index 000000000..80c325534 --- /dev/null +++ b/test/assets/staging-rekor-v2.txt.sigstore.json @@ -0,0 +1 @@ +{"mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json", "verificationMaterial": {"certificate": {"rawBytes": "MIICyzCCAlCgAwIBAgIUJc/6ox+xb+Cmb5UVrFhdu5jiMzIwCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjUwNjA5MTE1NzM1WhcNMjUwNjA5MTIwNzM1WjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEvoYb1h6sjlOR276rCjnPc/PgZtTahLzmf32f08PZ/2eWr4q979itVw1PG8IhcK3E2ZiihegXEgh4mPkkMn78BKOCAW8wggFrMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUsoZlvpIKgR6WlgezvkD6xzHypcMwHwYDVR0jBBgwFoAUcYYwphR8Ym/599b0BRp/X//rb6wwGQYDVR0RAQH/BA8wDYELamt1QGdvdG8uZmkwLAYKKwYBBAGDvzABAQQeaHR0cHM6Ly9naXRodWIuY29tL2xvZ2luL29hdXRoMC4GCisGAQQBg78wAQgEIAweaHR0cHM6Ly9naXRodWIuY29tL2xvZ2luL29hdXRoMIGKBgorBgEEAdZ5AgQCBHwEegB4AHYAKzC83GiIyeLh2CYpXnQfSDkxlgLynDPLXkNA/rKshnoAAAGXVI2aFgAABAMARzBFAiBDHRpKGTpiU3Nx28XgewlvzbMt/ug6ipN8Xj9tryWbwQIhAP/3Cngo4St1nAggkflowySL0fPYg/QDcJKE6XceON3WMAoGCCqGSM49BAMDA2kAMGYCMQCfyQmcNbg2g5PD9Jrb9yOS+vEwwThoY2YDoptDzhJvOxNYLek6DRwCAjZ4SqeTwmQCMQDD3lXotLGsn/CJxGlEiVaF2+z3SKb+bLGGKQATHPkZ/XHvLI2cAdVhcTYeEn36shE="}, "tlogEntries": [{"logIndex": "645", "logId": {"keyId": "8w1amZ2S5mJIQkQmPxdMuOrL/oJkvFg9MnQXmeOCXck="}, "kindVersion": {"kind": "hashedrekord", "version": "0.0.2"}, "inclusionProof": {"logIndex": "645", "rootHash": "kNum4JmdViJPfZLMRB3xPi6flATj2JzJSiF+1pQDzNQ=", "treeSize": "646", "hashes": ["eTqr8nE8VGEREKQ2MDQeD+zKHTJERE6iNw0tG1G+WbQ=", "wzbEsO0X3AWHadlgJZx7yhJdRVEZ2dEY21okXQ6UIi4=", "QMesRTEZdIgthOEinYE/9J7wGv+VmArDZTICj9POmhY=", "UNUMG62rMwoqCqFKknh4R5Ubkf5Z6dj+Pk0m/1xu8uo="], "checkpoint": {"envelope": "log2025-alpha1.rekor.sigstage.dev\n646\nkNum4JmdViJPfZLMRB3xPi6flATj2JzJSiF+1pQDzNQ=\n\n\u2014 log2025-alpha1.rekor.sigstage.dev 8w1amQA0XB55lIjvC/rvbpawQn9lp2R5TSkvqoNJuxcH9Ii05Ddi66xN8z5ZE6GsK2MkvgNZuqnZ5RtHbq2kpt/B8AE=\n"}}, "canonicalizedBody": "eyJhcGlWZXJzaW9uIjoiMC4wLjIiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJoYXNoZWRSZWtvcmRWMDAyIjp7ImRhdGEiOnsiYWxnb3JpdGhtIjoiU0hBMl8yNTYiLCJkaWdlc3QiOiJGZlp5UmhGWklidDhIZURuNmVrblhJQVczQ1ZLREFDWWlKUkxmdE5rU3FvPSJ9LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FVUNJQVo2VDhBVVpTQ0JaYUtKa3NMbFNpbE5xRUVPdDRaeUdNR2VwVXBLcDdWR0FpRUFzL1gwa01KVG5FT3V6L0RMV3hUTDR3QlZOa2lXSVVERjM2RUVENzAzOTZBPSIsInZlcmlmaWVyIjp7ImtleURldGFpbHMiOiJQS0lYX0VDRFNBX1AyNTZfU0hBXzI1NiIsIng1MDlDZXJ0aWZpY2F0ZSI6eyJyYXdCeXRlcyI6Ik1JSUN5ekNDQWxDZ0F3SUJBZ0lVSmMvNm94K3hiK0NtYjVVVnJGaGR1NWppTXpJd0NnWUlLb1pJemowRUF3TXdOekVWTUJNR0ExVUVDaE1NYzJsbmMzUnZjbVV1WkdWMk1SNHdIQVlEVlFRREV4VnphV2R6ZEc5eVpTMXBiblJsY20xbFpHbGhkR1V3SGhjTk1qVXdOakE1TVRFMU56TTFXaGNOTWpVd05qQTVNVEl3TnpNMVdqQUFNRmt3RXdZSEtvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUV2b1liMWg2c2psT1IyNzZyQ2puUGMvUGdadFRhaEx6bWYzMmYwOFBaLzJlV3I0cTk3OWl0VncxUEc4SWhjSzNFMlppaWhlZ1hFZ2g0bVBra01uNzhCS09DQVc4d2dnRnJNQTRHQTFVZER3RUIvd1FFQXdJSGdEQVRCZ05WSFNVRUREQUtCZ2dyQmdFRkJRY0RBekFkQmdOVkhRNEVGZ1FVc29abHZwSUtnUjZXbGdlenZrRDZ4ekh5cGNNd0h3WURWUjBqQkJnd0ZvQVVjWVl3cGhSOFltLzU5OWIwQlJwL1gvL3JiNnd3R1FZRFZSMFJBUUgvQkE4d0RZRUxhbXQxUUdkdmRHOHVabWt3TEFZS0t3WUJCQUdEdnpBQkFRUWVhSFIwY0hNNkx5OW5hWFJvZFdJdVkyOXRMMnh2WjJsdUwyOWhkWFJvTUM0R0Npc0dBUVFCZzc4d0FRZ0VJQXdlYUhSMGNITTZMeTluYVhSb2RXSXVZMjl0TDJ4dloybHVMMjloZFhSb01JR0tCZ29yQmdFRUFkWjVBZ1FDQkh3RWVnQjRBSFlBS3pDODNHaUl5ZUxoMkNZcFhuUWZTRGt4bGdMeW5EUExYa05BL3JLc2hub0FBQUdYVkkyYUZnQUFCQU1BUnpCRkFpQkRIUnBLR1RwaVUzTngyOFhnZXdsdnpiTXQvdWc2aXBOOFhqOXRyeVdid1FJaEFQLzNDbmdvNFN0MW5BZ2drZmxvd3lTTDBmUFlnL1FEY0pLRTZYY2VPTjNXTUFvR0NDcUdTTTQ5QkFNREEya0FNR1lDTVFDZnlRbWNOYmcyZzVQRDlKcmI5eU9TK3ZFd3dUaG9ZMllEb3B0RHpoSnZPeE5ZTGVrNkRSd0NBalo0U3FlVHdtUUNNUUREM2xYb3RMR3NuL0NKeEdsRWlWYUYyK3ozU0tiK2JMR0dLUUFUSFBrWi9YSHZMSTJjQWRWaGNUWWVFbjM2c2hFPSJ9fX19fX0="}], "timestampVerificationData": {"rfc3161Timestamps": [{"signedTimestamp": "MIIE6zADAgEAMIIE4gYJKoZIhvcNAQcCoIIE0zCCBM8CAQMxDTALBglghkgBZQMEAgEwgcMGCyqGSIb3DQEJEAEEoIGzBIGwMIGtAgEBBgkrBgEEAYO/MAIwMTANBglghkgBZQMEAgEFAAQgOjYPmS6Qixa9OQqdXWQMPN66194GUnV3liEVd7cbW8oCFQDuYcF6Hx3Wi2sgxpmG+IG2KlvUKRgPMjAyNTA2MDkxMTU3MzhaMAMCAQECCQCbf5cNt4JRDqAypDAwLjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MRUwEwYDVQQDEwxzaWdzdG9yZS10c2GgggITMIICDzCCAZagAwIBAgIUCjWhBmHV4kFzxomWp/J98n4DfKcwCgYIKoZIzj0EAwMwOTEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSAwHgYDVQQDExdzaWdzdG9yZS10c2Etc2VsZnNpZ25lZDAeFw0yNTAzMjgwOTE0MDZaFw0zNTAzMjYwODE0MDZaMC4xFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEVMBMGA1UEAxMMc2lnc3RvcmUtdHNhMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEx1v5F3HpD9egHuknpBFlRz7QBRDJu4aeVzt9zJLRY0lvmx1lF7WBM2c9AN8ZGPQsmDqHlJN2R/7+RxLkvlLzkc19IOx38t7mGGEcB7agUDdCF/Ky3RTLSK0Xo/0AgHQdo2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFKj8ZPYo3i7mO3NPVIxSxOGc3VOlMB8GA1UdIwQYMBaAFDsgRlletTJNRzDObmPuc3RH8gR9MBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMDA2cAMGQCMESvVS6GGtF33+J19TfwENWJXjRv4i0/HQFwLUSkX6TfV7g0nG8VnqNHJLvEpAtOjQIwUD3uywTXorQP1DgbV09rF9Yen+CEqs/iEpieJWPst280SSOZ5Na+dyPVk9/8SFk6MYIB3DCCAdgCAQEwUTA5MRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxIDAeBgNVBAMTF3NpZ3N0b3JlLXRzYS1zZWxmc2lnbmVkAhQKNaEGYdXiQXPGiZan8n3yfgN8pzALBglghkgBZQMEAgGggfwwGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNTA2MDkxMTU3MzhaMC8GCSqGSIb3DQEJBDEiBCA6qJ7IlNaN4uuHegN2O+NsWY5kB6sw8E/Q3H3arU8jmDCBjgYLKoZIhvcNAQkQAi8xfzB9MHsweQQgBvT/4Ef+s1mZtzOw16MjUBz8GOTAM2aoRdd1NudLJ0QwVTA9pDswOTEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSAwHgYDVQQDExdzaWdzdG9yZS10c2Etc2VsZnNpZ25lZAIUCjWhBmHV4kFzxomWp/J98n4DfKcwCgYIKoZIzj0EAwIEaDBmAjEA9vHFXY/Ia5L2g8F7ipZpiJOgDoAau7L+UkE5c1cCM2FYDZN1QQzWjXGj1CwQMOcuAjEAtBIxQiiecOzOkFo1Bj0n9xkIjyErSBT+P3P6OWgwdivDosxQCTMF7iNeI7wgFQxw"}]}}, "messageSignature": {"messageDigest": {"algorithm": "SHA2_256", "digest": "FfZyRhFZIbt8HeDn6eknXIAW3CVKDACYiJRLftNkSqo="}, "signature": "MEUCIAZ6T8AUZSCBZaKJksLlSilNqEEOt4ZyGMGepUpKp7VGAiEAs/X0kMJTnEOuz/DLWxTL4wBVNkiWIUDF36EED70396A="}} diff --git a/test/unit/conftest.py b/test/unit/conftest.py index d40d8d7fc..da3ca1573 100644 --- a/test/unit/conftest.py +++ b/test/unit/conftest.py @@ -31,6 +31,7 @@ from id import ( detect_credential, ) +from sigstore_protobuf_specs.dev.sigstore.trustroot.v1 import Service from tuf.api.exceptions import DownloadHTTPError from tuf.ngclient import FetcherInterface, updater @@ -237,6 +238,34 @@ def signer(): return signer, verifier, IdentityToken(token) +@pytest.fixture +def staging_with_rekorv2() -> tuple[ + type[SigningContext], type[Verifier], IdentityToken +]: + """ + Returns a SigningContext, Verifier, and IdentityToken for the staging environment. + The signingContext will use the Rekor V2 instance even if it is not yet enabled in + staging signing config. + """ + + def signer(): + trust_config = ClientTrustConfig.staging() + trust_config.signing_config._tlogs.append( + Service("https://log2025-alpha1.rekor.sigstage.dev", 2) + ) + return SigningContext.from_trust_config(trust_config) + + verifier = Verifier.staging + + # Detect env variable for local interactive tests. + token = os.getenv("SIGSTORE_IDENTITY_TOKEN_staging") + if not token: + # If the variable is not defined, try getting an ambient token. + token = detect_credential(TEST_CLIENT_ID) + + return signer, verifier, IdentityToken(token) + + @pytest.fixture def dummy_jwt(): def _dummy_jwt(claims: dict): diff --git a/test/unit/test_sign.py b/test/unit/test_sign.py index 244cfc8e7..46a26fb99 100644 --- a/test/unit/test_sign.py +++ b/test/unit/test_sign.py @@ -51,6 +51,21 @@ def test_sign_rekor_entry_consistent(sign_ctx_and_ident_for_env): assert expected_entry.log_index == actual_entry.log_index +@pytest.mark.staging +def test_sign_with_staging_rekor_v2(staging_with_rekorv2, null_policy): + ctx_cls, verifier_cls, identity = staging_with_rekorv2 + + ctx: SigningContext = ctx_cls() + verifier = verifier_cls() + assert identity is not None + + payload = secrets.token_bytes(32) + with ctx.signer(identity) as signer: + bundle = signer.sign_artifact(payload) + + verifier.verify_artifact(payload, bundle, null_policy) + + @pytest.mark.parametrize("env", ["staging", "production"]) @pytest.mark.ambient_oidc def test_sct_verify_keyring_lookup_error(sign_ctx_and_ident_for_env, monkeypatch): diff --git a/test/unit/verify/test_verifier.py b/test/unit/verify/test_verifier.py index 057b35e50..dcbcb6181 100644 --- a/test/unit/verify/test_verifier.py +++ b/test/unit/verify/test_verifier.py @@ -76,7 +76,8 @@ def test_verifier_multiple_verifications(signing_materials, null_policy): @pytest.mark.online @pytest.mark.parametrize( - "filename", ("bundle.txt", "bundle_v3.txt", "bundle_v3_alt.txt") + "filename", + ("bundle.txt", "bundle_v3.txt", "bundle_v3_alt.txt", "staging-rekor-v2.txt"), ) def test_verifier_bundle(signing_bundle, null_policy, filename): (file, bundle) = signing_bundle(filename) From b04e3c07b375181731488695f48b0f3e102042e7 Mon Sep 17 00:00:00 2001 From: Ramon Petgrave Date: Mon, 9 Jun 2025 19:25:30 +0000 Subject: [PATCH 06/14] refactor entry validation Signed-off-by: Ramon Petgrave --- sigstore/verify/verifier.py | 253 ++++++++++++++++++++---------------- 1 file changed, 141 insertions(+), 112 deletions(-) diff --git a/sigstore/verify/verifier.py b/sigstore/verify/verifier.py index d885fde8a..536387a1b 100644 --- a/sigstore/verify/verifier.py +++ b/sigstore/verify/verifier.py @@ -440,73 +440,9 @@ def verify_dsse( entry._kind_version.kind == "dsse" and entry._kind_version.version == "0.0.2" ): - try: - v2_body = v2.Entry().from_json(base64.b64decode(entry.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.Signature( - content=signature.sig, - verifier=v2.Verifier( - x509_certificate=v1.X509Certificate( - bundle.signing_certificate.public_bytes( - encoding=serialization.Encoding.DER - ) - ), - key_details=self._get_key_details(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") + validate_dsse_v002_entry_body(bundle) else: - try: - entry_body = rekor_types.Dsse.model_validate_json( - base64.b64decode(entry.body) - ) - except ValidationError as exc: - raise VerificationError(f"invalid DSSE log entry: {exc}") - - payload_hash = sha256_digest(envelope._inner.payload).digest.hex() - if ( - entry_body.spec.root.payload_hash.algorithm # type: ignore[union-attr] - != rekor_types.dsse.Algorithm.SHA256 - ): - raise VerificationError( - "expected SHA256 payload hash in DSSE log entry" - ) - if payload_hash != entry_body.spec.root.payload_hash.value: # type: ignore[union-attr] - raise VerificationError("log entry payload hash does not match bundle") - - # NOTE: Like `dsse._verify`: multiple signatures would be frivolous here, - # but we handle them just in case the signer has somehow produced multiple - # signatures for their envelope with the same signing key. - signatures = [ - rekor_types.dsse.Signature( - signature=base64.b64encode(signature.sig).decode(), - verifier=base64_encode_pem_cert(bundle.signing_certificate), - ) - for signature in envelope._inner.signatures - ] - if signatures != entry_body.spec.root.signatures: - raise VerificationError("log entry signatures do not match bundle") + validate_dsse_v001_entry_body(bundle) return (envelope._inner.payload_type, envelope._inner.payload) @@ -551,58 +487,151 @@ def verify_artifact( # (8): verify the consistency of the log entry's body against # the other bundle materials (and input being verified). entry = bundle.log_entry - if ( entry._kind_version.kind == "hashedrekord" and entry._kind_version.version == "0.0.2" ): - if bundle._inner.message_signature is None: - raise VerificationError( - "invalid hashedrekord log entry: missing message signature" - ) + validate_hashedrekord_v002_entry_body(bundle) + else: + validate_hashedrekord_v001_entry_body(bundle, hashed_input) - v2_expected_body = v2.Entry( - kind=entry._kind_version.kind, - api_version=entry._kind_version.version, - spec=v2.Spec( - hashed_rekord_v002=v2.HashedRekordLogEntryV002( - data=v1.HashOutput( - algorithm=bundle._inner.message_signature.message_digest.algorithm, - digest=bundle._inner.message_signature.message_digest.digest, - ), - signature=v2.Signature( - content=bundle._inner.message_signature.signature, - verifier=v2.Verifier( - x509_certificate=v1.X509Certificate( - bundle.signing_certificate.public_bytes( - encoding=serialization.Encoding.DER - ) - ), - key_details=self._get_key_details( - bundle.signing_certificate - ), - ), - ), + +def validate_dsse_v001_entry_body(bundle: Bundle) -> None: + """ + Valideate the Entry body for dsse v001. + """ + 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: + entry_body = rekor_types.Dsse.model_validate_json(base64.b64decode(entry.body)) + except ValidationError as exc: + raise VerificationError(f"invalid DSSE log entry: {exc}") + + payload_hash = sha256_digest(envelope._inner.payload).digest.hex() + if ( + entry_body.spec.root.payload_hash.algorithm # type: ignore[union-attr] + != rekor_types.dsse.Algorithm.SHA256 + ): + raise VerificationError("expected SHA256 payload hash in DSSE log entry") + if payload_hash != entry_body.spec.root.payload_hash.value: # type: ignore[union-attr] + raise VerificationError("log entry payload hash does not match bundle") + + # NOTE: Like `dsse._verify`: multiple signatures would be frivolous here, + # but we handle them just in case the signer has somehow produced multiple + # signatures for their envelope with the same signing key. + signatures = [ + rekor_types.dsse.Signature( + signature=base64.b64encode(signature.sig).decode(), + verifier=base64_encode_pem_cert(bundle.signing_certificate), + ) + for signature in envelope._inner.signatures + ] + if signatures != entry_body.spec.root.signatures: + raise VerificationError("log entry signatures do not match bundle") + + +def validate_dsse_v002_entry_body(bundle: Bundle) -> None: + """ + Valideate the 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().from_json(base64.b64decode(entry.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.Signature( + content=signature.sig, + verifier=v2.Verifier( + x509_certificate=v1.X509Certificate( + bundle.signing_certificate.public_bytes( + encoding=serialization.Encoding.DER ) ), - ) - v2_actual_body = v2.Entry().from_json(base64.b64decode(entry.body)) - if v2_expected_body != v2_actual_body: - raise VerificationError( - "transparency log entry is inconsistent with other materials" - ) + key_details=Verifier._get_key_details(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") - else: - expected_body = _hashedrekord_from_parts( - bundle.signing_certificate, - bundle._inner.message_signature.signature, # type: ignore[union-attr] - hashed_input, - ) - actual_body = rekor_types.Hashedrekord.model_validate_json( - base64.b64decode(entry.body) - ) - if expected_body != actual_body: - raise VerificationError( - "transparency log entry is inconsistent with other materials" - ) +def validate_hashedrekord_v001_entry_body(bundle: Bundle, hashed_input: Hashed) -> None: + """ + Valideate the Entry body for hashedrekord v001. + """ + entry = bundle.log_entry + expected_body = _hashedrekord_from_parts( + bundle.signing_certificate, + bundle._inner.message_signature.signature, # type: ignore[union-attr] + hashed_input, + ) + actual_body = rekor_types.Hashedrekord.model_validate_json( + base64.b64decode(entry.body) + ) + if expected_body != actual_body: + raise VerificationError( + "transparency log entry is inconsistent with other materials" + ) + + +def validate_hashedrekord_v002_entry_body(bundle: Bundle) -> None: + """ + Valideate the Entry body for hashedrekord v002. + """ + entry = bundle.log_entry + if bundle._inner.message_signature is None: + raise VerificationError( + "invalid hashedrekord log entry: missing message signature" + ) + v2_expected_body = v2.Entry( + kind=entry._kind_version.kind, + api_version=entry._kind_version.version, + spec=v2.Spec( + hashed_rekord_v002=v2.HashedRekordLogEntryV002( + data=v1.HashOutput( + algorithm=bundle._inner.message_signature.message_digest.algorithm, + digest=bundle._inner.message_signature.message_digest.digest, + ), + signature=v2.Signature( + content=bundle._inner.message_signature.signature, + verifier=v2.Verifier( + x509_certificate=v1.X509Certificate( + bundle.signing_certificate.public_bytes( + encoding=serialization.Encoding.DER + ) + ), + key_details=Verifier._get_key_details( + bundle.signing_certificate + ), + ), + ), + ) + ), + ) + v2_actual_body = v2.Entry().from_json(base64.b64decode(entry.body)) + if v2_expected_body != v2_actual_body: + raise VerificationError( + "transparency log entry is inconsistent with other materials" + ) From e173a3b3e7653e463c3ab9ad01d5e8feef5ffc8b Mon Sep 17 00:00:00 2001 From: Ramon Petgrave Date: Mon, 9 Jun 2025 19:32:22 +0000 Subject: [PATCH 07/14] use staging_with_rekorv2 fixture Signed-off-by: Ramon Petgrave --- test/unit/internal/rekor/test_client_v2.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/test/unit/internal/rekor/test_client_v2.py b/test/unit/internal/rekor/test_client_v2.py index e8058223a..41c0e52a2 100644 --- a/test/unit/internal/rekor/test_client_v2.py +++ b/test/unit/internal/rekor/test_client_v2.py @@ -19,23 +19,19 @@ from sigstore import dsse from sigstore._internal.rekor.client_v2 import ( LogEntry, - RekorV2Client, ) from sigstore.models import rekor_v1 -ALPHA_REKOR_V2_URL = "https://log2025-alpha1.rekor.sigstage.dev" - @pytest.mark.staging @pytest.mark.ambient_oidc -def test_rekor_v2_create_entry_dsse(staging): +def test_rekor_v2_create_entry_dsse(staging_with_rekorv2): # This is not a real unit test: it requires not only staging rekor but also TUF # fulcio and oidc -- maybe useful only until we have real integration tests in place - sign_ctx_cls, _, identity = staging + sign_ctx_cls, _, identity = staging_with_rekorv2 # Hack to run Signer.sign() with staging rekor v2 sign_ctx = sign_ctx_cls() - sign_ctx._rekor = RekorV2Client(ALPHA_REKOR_V2_URL) stmt = ( dsse.StatementBuilder() @@ -64,14 +60,13 @@ def test_rekor_v2_create_entry_dsse(staging): @pytest.mark.staging @pytest.mark.ambient_oidc -def test_rekor_v2_create_entry_hashed_rekord(staging): +def test_rekor_v2_create_entry_hashed_rekord(staging_with_rekorv2): # This is not a real unit test: it requires not only staging rekor but also TUF # fulcio and oidc -- maybe useful only until we have real integration tests in place - sign_ctx_cls, _, identity = staging + sign_ctx_cls, _, identity = staging_with_rekorv2 # Hack to run Signer.sign() with staging rekor v2 sign_ctx = sign_ctx_cls() - sign_ctx._rekor = RekorV2Client(ALPHA_REKOR_V2_URL) with sign_ctx.signer(identity) as signer: bundle = signer.sign_artifact(b"") From c868f55e2bc44fed533e69ce85967513c09a66b2 Mon Sep 17 00:00:00 2001 From: Ramon Petgrave Date: Mon, 9 Jun 2025 19:58:03 +0000 Subject: [PATCH 08/14] staging_with_rekorv2 uses embedded trust config Signed-off-by: Ramon Petgrave --- test/unit/conftest.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/unit/conftest.py b/test/unit/conftest.py index da3ca1573..d53be369e 100644 --- a/test/unit/conftest.py +++ b/test/unit/conftest.py @@ -31,7 +31,6 @@ from id import ( detect_credential, ) -from sigstore_protobuf_specs.dev.sigstore.trustroot.v1 import Service from tuf.api.exceptions import DownloadHTTPError from tuf.ngclient import FetcherInterface, updater @@ -239,9 +238,9 @@ def signer(): @pytest.fixture -def staging_with_rekorv2() -> tuple[ - type[SigningContext], type[Verifier], IdentityToken -]: +def staging_with_rekorv2( + asset, +) -> tuple[type[SigningContext], type[Verifier], IdentityToken]: """ Returns a SigningContext, Verifier, and IdentityToken for the staging environment. The signingContext will use the Rekor V2 instance even if it is not yet enabled in @@ -249,9 +248,10 @@ def staging_with_rekorv2() -> tuple[ """ def signer(): - trust_config = ClientTrustConfig.staging() - trust_config.signing_config._tlogs.append( - Service("https://log2025-alpha1.rekor.sigstage.dev", 2) + trust_config = ClientTrustConfig.from_json( + asset( + os.path.join("trust_config", "staging-but-sign-with-rekor-v2.json") + ).read_text() ) return SigningContext.from_trust_config(trust_config) From 732cdbffd9620c323409c6875a65df24d7d30cfa Mon Sep 17 00:00:00 2001 From: Ramon Petgrave Date: Mon, 9 Jun 2025 21:34:40 +0000 Subject: [PATCH 09/14] verify rekorv2 dssse bundle Signed-off-by: Ramon Petgrave --- test/assets/a.dsse.staging-rekor-v2.txt | 5 +++++ .../a.dsse.staging-rekor-v2.txt.sigstore.json | 1 + test/unit/verify/test_verifier.py | 14 +++++++++++++- 3 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 test/assets/a.dsse.staging-rekor-v2.txt create mode 100644 test/assets/a.dsse.staging-rekor-v2.txt.sigstore.json diff --git a/test/assets/a.dsse.staging-rekor-v2.txt b/test/assets/a.dsse.staging-rekor-v2.txt new file mode 100644 index 000000000..8d0585ac7 --- /dev/null +++ b/test/assets/a.dsse.staging-rekor-v2.txt @@ -0,0 +1,5 @@ +DO NOT MODIFY ME! + +this is "a.txt", a sample input for sigstore-python's unit tests. + +DO NOT MODIFY ME! diff --git a/test/assets/a.dsse.staging-rekor-v2.txt.sigstore.json b/test/assets/a.dsse.staging-rekor-v2.txt.sigstore.json new file mode 100644 index 000000000..af2fe26f5 --- /dev/null +++ b/test/assets/a.dsse.staging-rekor-v2.txt.sigstore.json @@ -0,0 +1 @@ +{"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"}]}} diff --git a/test/unit/verify/test_verifier.py b/test/unit/verify/test_verifier.py index dcbcb6181..1f48a703b 100644 --- a/test/unit/verify/test_verifier.py +++ b/test/unit/verify/test_verifier.py @@ -79,13 +79,25 @@ def test_verifier_multiple_verifications(signing_materials, null_policy): "filename", ("bundle.txt", "bundle_v3.txt", "bundle_v3_alt.txt", "staging-rekor-v2.txt"), ) -def test_verifier_bundle(signing_bundle, null_policy, filename): +def test_verifier_bundle_artifact(signing_bundle, null_policy, filename): (file, bundle) = signing_bundle(filename) verifier = Verifier.staging() verifier.verify_artifact(file.read_bytes(), bundle, null_policy) +@pytest.mark.online +@pytest.mark.parametrize( + "filename", + ("a.dsse.staging-rekor-v2.txt",), +) +def test_verifier_bundle_dsse(signing_bundle, null_policy, filename): + (file, bundle) = signing_bundle(filename) + + verifier = Verifier.staging() + verifier.verify_dsse(bundle, null_policy) + + @pytest.mark.parametrize( "filename", ("bundle.txt", "bundle_v3.txt", "bundle_v3_alt.txt") ) From 46d9873ac1ef76d970ac55e330fca76ba1506fec Mon Sep 17 00:00:00 2001 From: Ramon Petgrave Date: Mon, 9 Jun 2025 21:34:49 +0000 Subject: [PATCH 10/14] Revert "staging_with_rekorv2 uses embedded trust config" This reverts commit d7ddd509d7dff60e732a17b15cb8c8e561e54f8d. Signed-off-by: Ramon Petgrave --- test/unit/conftest.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/unit/conftest.py b/test/unit/conftest.py index d53be369e..da3ca1573 100644 --- a/test/unit/conftest.py +++ b/test/unit/conftest.py @@ -31,6 +31,7 @@ from id import ( detect_credential, ) +from sigstore_protobuf_specs.dev.sigstore.trustroot.v1 import Service from tuf.api.exceptions import DownloadHTTPError from tuf.ngclient import FetcherInterface, updater @@ -238,9 +239,9 @@ def signer(): @pytest.fixture -def staging_with_rekorv2( - asset, -) -> tuple[type[SigningContext], type[Verifier], IdentityToken]: +def staging_with_rekorv2() -> tuple[ + type[SigningContext], type[Verifier], IdentityToken +]: """ Returns a SigningContext, Verifier, and IdentityToken for the staging environment. The signingContext will use the Rekor V2 instance even if it is not yet enabled in @@ -248,10 +249,9 @@ def staging_with_rekorv2( """ def signer(): - trust_config = ClientTrustConfig.from_json( - asset( - os.path.join("trust_config", "staging-but-sign-with-rekor-v2.json") - ).read_text() + trust_config = ClientTrustConfig.staging() + trust_config.signing_config._tlogs.append( + Service("https://log2025-alpha1.rekor.sigstage.dev", 2) ) return SigningContext.from_trust_config(trust_config) From 5261d23e3b9ebdfc70c8b71043fc348917b80503 Mon Sep 17 00:00:00 2001 From: Ramon Petgrave Date: Mon, 9 Jun 2025 22:12:17 +0000 Subject: [PATCH 11/14] use ambient oidc Signed-off-by: Ramon Petgrave --- test/unit/test_sign.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/unit/test_sign.py b/test/unit/test_sign.py index 46a26fb99..b7f056379 100644 --- a/test/unit/test_sign.py +++ b/test/unit/test_sign.py @@ -52,6 +52,7 @@ def test_sign_rekor_entry_consistent(sign_ctx_and_ident_for_env): @pytest.mark.staging +@pytest.mark.ambient_oidc def test_sign_with_staging_rekor_v2(staging_with_rekorv2, null_policy): ctx_cls, verifier_cls, identity = staging_with_rekorv2 From c0733ac7bb731b7927e72acaef509f09cabbe89a Mon Sep 17 00:00:00 2001 From: Jussi Kukkonen Date: Tue, 10 Jun 2025 16:52:34 +0300 Subject: [PATCH 12/14] verifier: One more refactor We can handle not just the key extraction but getting the whole v2.Verifier for the certificate: both v002 types need it. Also make private methods private and improve docstrings Signed-off-by: Jussi Kukkonen --- sigstore/verify/verifier.py | 86 ++++++++++++++++++------------------- 1 file changed, 42 insertions(+), 44 deletions(-) diff --git a/sigstore/verify/verifier.py b/sigstore/verify/verifier.py index 536387a1b..faa68a690 100644 --- a/sigstore/verify/verifier.py +++ b/sigstore/verify/verifier.py @@ -375,20 +375,6 @@ def _verify_common_signing_cert( f"invalid signing cert: expired at time of signing, time via {vts}" ) - @staticmethod - def _get_key_details(certificate: Certificate) -> v1.PublicKeyDetails: - """Determine PublicKeyDetails from a certificate""" - public_key = certificate.public_key() - if isinstance(public_key, EllipticCurvePublicKey): - if public_key.curve.name == "secp256r1": - return cast( - v1.PublicKeyDetails, - v1.PublicKeyDetails.PKIX_ECDSA_P256_SHA_256, - ) - # TODO support other keys - raise ValueError(f"Unsupported EC curve: {public_key.curve.name}") - raise ValueError(f"Unsupported public key type: {type(public_key)}") - def verify_dsse( self, bundle: Bundle, policy: VerificationPolicy ) -> tuple[str, bytes]: @@ -440,9 +426,9 @@ def verify_dsse( entry._kind_version.kind == "dsse" and entry._kind_version.version == "0.0.2" ): - validate_dsse_v002_entry_body(bundle) + _validate_dsse_v002_entry_body(bundle) else: - validate_dsse_v001_entry_body(bundle) + _validate_dsse_v001_entry_body(bundle) return (envelope._inner.payload_type, envelope._inner.payload) @@ -491,14 +477,14 @@ def verify_artifact( entry._kind_version.kind == "hashedrekord" and entry._kind_version.version == "0.0.2" ): - validate_hashedrekord_v002_entry_body(bundle) + _validate_hashedrekord_v002_entry_body(bundle) else: - validate_hashedrekord_v001_entry_body(bundle, hashed_input) + _validate_hashedrekord_v001_entry_body(bundle, hashed_input) -def validate_dsse_v001_entry_body(bundle: Bundle) -> None: +def _validate_dsse_v001_entry_body(bundle: Bundle) -> None: """ - Valideate the Entry body for dsse v001. + Validate the Entry body for dsse v001. """ entry = bundle.log_entry envelope = bundle._dsse_envelope @@ -534,9 +520,9 @@ 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: +def _validate_dsse_v002_entry_body(bundle: Bundle) -> None: """ - Valideate the Entry body for dsse v002. + Validate Entry body for dsse v002. """ entry = bundle.log_entry envelope = bundle._dsse_envelope @@ -562,14 +548,7 @@ def validate_dsse_v002_entry_body(bundle: Bundle) -> None: v2_signatures = [ v2.Signature( content=signature.sig, - verifier=v2.Verifier( - x509_certificate=v1.X509Certificate( - bundle.signing_certificate.public_bytes( - encoding=serialization.Encoding.DER - ) - ), - key_details=Verifier._get_key_details(bundle.signing_certificate), - ), + verifier=_v2_verifier_from_certificate(bundle.signing_certificate), ) for signature in envelope._inner.signatures ] @@ -577,9 +556,11 @@ def validate_dsse_v002_entry_body(bundle: Bundle) -> None: raise VerificationError("log entry signatures do not match bundle") -def validate_hashedrekord_v001_entry_body(bundle: Bundle, hashed_input: Hashed) -> None: +def _validate_hashedrekord_v001_entry_body( + bundle: Bundle, hashed_input: Hashed +) -> None: """ - Valideate the Entry body for hashedrekord v001. + Validate the Entry body for hashedrekord v001. """ entry = bundle.log_entry expected_body = _hashedrekord_from_parts( @@ -596,9 +577,9 @@ def validate_hashedrekord_v001_entry_body(bundle: Bundle, hashed_input: Hashed) ) -def validate_hashedrekord_v002_entry_body(bundle: Bundle) -> None: +def _validate_hashedrekord_v002_entry_body(bundle: Bundle) -> None: """ - Valideate the Entry body for hashedrekord v002. + Validate Entry body for hashedrekord v002. """ entry = bundle.log_entry if bundle._inner.message_signature is None: @@ -616,16 +597,7 @@ def validate_hashedrekord_v002_entry_body(bundle: Bundle) -> None: ), signature=v2.Signature( content=bundle._inner.message_signature.signature, - verifier=v2.Verifier( - x509_certificate=v1.X509Certificate( - bundle.signing_certificate.public_bytes( - encoding=serialization.Encoding.DER - ) - ), - key_details=Verifier._get_key_details( - bundle.signing_certificate - ), - ), + verifier=_v2_verifier_from_certificate(bundle.signing_certificate), ), ) ), @@ -635,3 +607,29 @@ def validate_hashedrekord_v002_entry_body(bundle: Bundle) -> None: raise VerificationError( "transparency log entry is inconsistent with other materials" ) + + +def _v2_verifier_from_certificate(certificate: Certificate) -> v2.Verifier: + public_key = certificate.public_key() + key_details = None + + if isinstance(public_key, EllipticCurvePublicKey): + if public_key.curve.name == "secp256r1": + key_details = cast( + v1.PublicKeyDetails, + v1.PublicKeyDetails.PKIX_ECDSA_P256_SHA_256, + ) + else: + raise ValueError(f"Unsupported EC curve: {public_key.curve.name}") + + # TODO support other keys + + if key_details is None: + raise ValueError(f"Unsupported public key type: {type(public_key)}") + + return v2.Verifier( + x509_certificate=v1.X509Certificate( + certificate.public_bytes(encoding=serialization.Encoding.DER) + ), + key_details=key_details, + ) From 600eb7e92e0aa6874304b9aa2bb4ecc45af0aefc Mon Sep 17 00:00:00 2001 From: Jussi Kukkonen Date: Tue, 10 Jun 2025 17:05:45 +0300 Subject: [PATCH 13/14] verifier: Improve error messages with unknown types Signed-off-by: Jussi Kukkonen --- sigstore/verify/verifier.py | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/sigstore/verify/verifier.py b/sigstore/verify/verifier.py index faa68a690..d2efbd32f 100644 --- a/sigstore/verify/verifier.py +++ b/sigstore/verify/verifier.py @@ -422,13 +422,18 @@ def verify_dsse( # Instead, we manually pick apart the entry body below and verify # the parts we can (namely the payload hash and signature list). entry = bundle.log_entry - if ( - entry._kind_version.kind == "dsse" - and entry._kind_version.version == "0.0.2" - ): + if entry._kind_version.kind != "dsse": + raise VerificationError( + f"Expected entry type dsse, got {entry._kind_version.kind}" + ) + if entry._kind_version.version == "0.0.2": _validate_dsse_v002_entry_body(bundle) - else: + elif entry._kind_version.version == "0.0.1": _validate_dsse_v001_entry_body(bundle) + else: + raise VerificationError( + f"Unsupported dsse version {entry._kind_version.version}" + ) return (envelope._inner.payload_type, envelope._inner.payload) @@ -473,13 +478,19 @@ def verify_artifact( # (8): verify the consistency of the log entry's body against # the other bundle materials (and input being verified). entry = bundle.log_entry - if ( - entry._kind_version.kind == "hashedrekord" - and entry._kind_version.version == "0.0.2" - ): + if entry._kind_version.kind != "hashedrekord": + raise VerificationError( + f"Expected entry type hashedrekord, got {entry._kind_version.kind}" + ) + + if entry._kind_version.version == "0.0.2": _validate_hashedrekord_v002_entry_body(bundle) - else: + elif entry._kind_version.version == "0.0.1": _validate_hashedrekord_v001_entry_body(bundle, hashed_input) + else: + raise VerificationError( + f"Unsupported hashedrekord version {entry._kind_version.version}" + ) def _validate_dsse_v001_entry_body(bundle: Bundle) -> None: From 47b7664fd9d96f9aa306088ae464230c389dc091 Mon Sep 17 00:00:00 2001 From: Jussi Kukkonen Date: Wed, 11 Jun 2025 11:56:22 +0300 Subject: [PATCH 14/14] verify: Handle more ECDSA keys in signing cert This change affects the signing certificate verification in rekor v2 entries: * Support all ECDSA keys listed in https://github.com/sigstore/architecture-docs/blob/main/algorithm-registry.md * Don't support other algorithms yet since the actual signature verification does not support them currently Signed-off-by: Jussi Kukkonen --- sigstore/verify/verifier.py | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/sigstore/verify/verifier.py b/sigstore/verify/verifier.py index d2efbd32f..3d0ac5f2c 100644 --- a/sigstore/verify/verifier.py +++ b/sigstore/verify/verifier.py @@ -27,7 +27,6 @@ from cryptography.exceptions import InvalidSignature from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import ec -from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePublicKey from cryptography.x509 import Certificate, ExtendedKeyUsage, KeyUsage from cryptography.x509.oid import ExtendedKeyUsageOID from OpenSSL.crypto import ( @@ -621,26 +620,32 @@ def _validate_hashedrekord_v002_entry_body(bundle: Bundle) -> None: def _v2_verifier_from_certificate(certificate: Certificate) -> v2.Verifier: + """ + Return a Rekor v2 protobuf 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. + """ public_key = certificate.public_key() - key_details = None - if isinstance(public_key, EllipticCurvePublicKey): - if public_key.curve.name == "secp256r1": - key_details = cast( - v1.PublicKeyDetails, - v1.PublicKeyDetails.PKIX_ECDSA_P256_SHA_256, - ) + 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}") - - # TODO support other keys - - if key_details is None: + else: raise ValueError(f"Unsupported public key type: {type(public_key)}") return v2.Verifier( x509_certificate=v1.X509Certificate( certificate.public_bytes(encoding=serialization.Encoding.DER) ), - key_details=key_details, + key_details=cast(v1.PublicKeyDetails, key_details), )