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/sigstore/verify/verifier.py b/sigstore/verify/verifier.py index 56d132de2..d885fde8a 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,77 @@ 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: + 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") + 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") return (envelope._inner.payload_type, envelope._inner.payload) @@ -491,15 +552,57 @@ 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 expected_body != actual_body: - raise VerificationError( - "transparency log entry is inconsistent with other materials" + 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" + ) + + 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 + ), + ), + ), + ) + ), ) + 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, + 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" + ) 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/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/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 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/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", [ 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)