diff --git a/CHANGELOG.md b/CHANGELOG.md index 66df7198c..955f538bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,10 +19,12 @@ All versions prior to 0.9.0 are untracked. * API: `IdentityToken` now supports `client_id` for audience claim validation. [#1402](https://github.com/sigstore/sigstore-python/pull/1402) - * Added a `RekorV2Client` for posting new entries to a Rekor V2 instance. [#1400](https://github.com/sigstore/sigstore-python/pull/1422) +* Added a function for determining the `key_details` of a certificate`. + [#1456](https://github.com/sigstore/sigstore-python/pull/1456) + ### Fixed * Avoid instantiation issues with `TransparencyLogEntry` when `InclusionPromise` is not present. diff --git a/sigstore/_internal/key_details.py b/sigstore/_internal/key_details.py new file mode 100644 index 000000000..7c65ec8ba --- /dev/null +++ b/sigstore/_internal/key_details.py @@ -0,0 +1,72 @@ +# Copyright 2025 The Sigstore Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Utilities for getting the sigstore_protobuf_specs.dev.sigstore.common.v1.PublicKeyDetails. +""" + +from typing import cast + +from cryptography.hazmat.primitives.asymmetric import ec, ed25519, padding, rsa +from cryptography.x509 import Certificate +from sigstore_protobuf_specs.dev.sigstore.common import v1 + + +def _get_key_details(certificate: Certificate) -> v1.PublicKeyDetails: + """ + Determine PublicKeyDetails from the Certificate. + We disclude the unrecommended types. + See + - https://github.com/sigstore/architecture-docs/blob/6a8d78108ef4bb403046817fbcead211a9dca71d/algorithm-registry.md. + - https://github.com/sigstore/protobuf-specs/blob/3aaae418f76fb4b34df4def4cd093c464f20fed3/protos/sigstore_common.proto + """ + public_key = certificate.public_key() + params = certificate.signature_algorithm_parameters + 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}") + elif isinstance(public_key, rsa.RSAPublicKey): + if public_key.key_size == 3072: + if isinstance(params, padding.PKCS1v15): + key_details = v1.PublicKeyDetails.PKIX_RSA_PKCS1V15_3072_SHA256 + elif isinstance(params, padding.PSS): + key_details = v1.PublicKeyDetails.PKIX_RSA_PSS_3072_SHA256 + else: + raise ValueError( + f"Unsupported public key type, size, and padding: {type(public_key)}, {public_key.key_size}, {params}" + ) + elif public_key.key_size == 4096: + if isinstance(params, padding.PKCS1v15): + key_details = v1.PublicKeyDetails.PKIX_RSA_PKCS1V15_3072_SHA256 + elif isinstance(params, padding.PSS): + key_details = v1.PublicKeyDetails.PKIX_RSA_PSS_3072_SHA256 + else: + raise ValueError( + f"Unsupported public key type, size, and padding: {type(public_key)}, {public_key.key_size}, {params}" + ) + else: + raise ValueError(f"Unsupported RSA key size: {public_key.key_size}") + elif isinstance(public_key, ed25519.Ed25519PublicKey): + key_details = v1.PublicKeyDetails.PKIX_ED25519 + # There is likely no need to explicitly detect PKIX_ED25519_PH, especially since the cryptography + # library does not yet support Ed25519ph. + else: + raise ValueError(f"Unsupported public key type: {type(public_key)}") + return cast(v1.PublicKeyDetails, key_details) diff --git a/sigstore/_internal/rekor/client_v2.py b/sigstore/_internal/rekor/client_v2.py index a7d4e9327..d2c758a50 100644 --- a/sigstore/_internal/rekor/client_v2.py +++ b/sigstore/_internal/rekor/client_v2.py @@ -20,17 +20,16 @@ import json import logging -from typing import cast import requests from cryptography.hazmat.primitives import serialization -from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePublicKey from cryptography.x509 import Certificate from sigstore_protobuf_specs.dev.sigstore.common import v1 as common_v1 from sigstore_protobuf_specs.dev.sigstore.rekor import v2 from sigstore_protobuf_specs.io import intoto from sigstore._internal import USER_AGENT +from sigstore._internal.key_details import _get_key_details from sigstore._internal.rekor import ( EntryRequestBody, RekorClientError, @@ -93,23 +92,6 @@ def create_entry(self, payload: EntryRequestBody) -> LogEntry: _logger.debug(f"integrated: {integrated_entry}") return LogEntry._from_dict_rekor(integrated_entry) - @staticmethod - def _get_key_details(certificate: Certificate) -> common_v1.PublicKeyDetails: - """ - Determine PublicKeyDetails from a certificate - - We know that sign.Signer only uses secp256r1, so do not support anything else. - """ - public_key = certificate.public_key() - if isinstance(public_key, EllipticCurvePublicKey): - if public_key.curve.name == "secp256r1": - return cast( - common_v1.PublicKeyDetails, - common_v1.PublicKeyDetails.PKIX_ECDSA_P256_SHA_256, - ) - raise ValueError(f"Unsupported EC curve: {public_key.curve.name}") - raise ValueError(f"Unsupported public key type: {type(public_key)}") - @classmethod def _build_hashed_rekord_request( cls, @@ -131,7 +113,7 @@ def _build_hashed_rekord_request( encoding=serialization.Encoding.DER ) ), - key_details=cls._get_key_details(certificate), + key_details=_get_key_details(certificate), ), ), ) @@ -165,7 +147,7 @@ def _build_dsse_request( encoding=serialization.Encoding.DER ) ), - key_details=cls._get_key_details(certificate), + key_details=_get_key_details(certificate), ) ], ) diff --git a/test/unit/internal/test_key_details.py b/test/unit/internal/test_key_details.py new file mode 100644 index 000000000..43302fcba --- /dev/null +++ b/test/unit/internal/test_key_details.py @@ -0,0 +1,131 @@ +# Copyright 2025 The Sigstore Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from unittest.mock import Mock + +import pytest +from cryptography.hazmat.primitives.asymmetric import dsa, ec, ed25519, padding, rsa +from sigstore_protobuf_specs.dev.sigstore.common import v1 + +from sigstore._internal.key_details import _get_key_details + + +@pytest.mark.parametrize( + "mock_certificate", + [ + # ec + pytest.param( + Mock( + public_key=Mock( + return_value=ec.generate_private_key(ec.SECP192R1()).public_key() + ) + ), + marks=[pytest.mark.xfail(strict=True)], + ), + Mock( + public_key=Mock( + return_value=ec.generate_private_key(ec.SECP256R1()).public_key() + ) + ), + Mock( + public_key=Mock( + return_value=ec.generate_private_key(ec.SECP384R1()).public_key() + ) + ), + Mock( + public_key=Mock( + return_value=ec.generate_private_key(ec.SECP521R1()).public_key() + ) + ), + # rsa pkcs1 + pytest.param( + Mock( + public_key=Mock( + return_value=rsa.generate_private_key( + public_exponent=65537, key_size=2048 + ).public_key() + ), + signature_algorithm_parameters=padding.PKCS1v15(), + ), + marks=[pytest.mark.xfail(strict=True)], + ), + Mock( + public_key=Mock( + return_value=rsa.generate_private_key( + public_exponent=65537, key_size=3072 + ).public_key() + ), + signature_algorithm_parameters=padding.PKCS1v15(), + ), + Mock( + public_key=Mock( + return_value=rsa.generate_private_key( + public_exponent=65537, key_size=4096 + ).public_key() + ), + signature_algorithm_parameters=padding.PKCS1v15(), + ), + # rsa pss + pytest.param( + Mock( + public_key=Mock( + return_value=rsa.generate_private_key( + public_exponent=65537, key_size=2048 + ).public_key() + ), + signature_algorithm_parameters=padding.PSS(None, 0), + ), + marks=[pytest.mark.xfail(strict=True)], + ), + Mock( + public_key=Mock( + return_value=rsa.generate_private_key( + public_exponent=65537, key_size=3072 + ).public_key() + ), + signature_algorithm_parameters=padding.PSS(None, 0), + ), + Mock( + public_key=Mock( + return_value=rsa.generate_private_key( + public_exponent=65537, key_size=4096 + ).public_key() + ), + signature_algorithm_parameters=padding.PSS(None, 0), + ), + # ed25519 + Mock( + public_key=Mock( + return_value=ed25519.Ed25519PrivateKey.generate().public_key(), + signature_algorithm_parameters=None, + ) + ), + # unsupported + pytest.param( + Mock( + public_key=Mock( + return_value=dsa.generate_private_key(key_size=1024).public_key() + ), + signature_algorithm_parameters=None, + ), + marks=[pytest.mark.xfail(strict=True)], + ), + ], +) +def test_get_key_details(mock_certificate): + """ + Ensures that we return a PublicKeyDetails for supported key types and schemes. + """ + key_details = _get_key_details(mock_certificate) + assert isinstance(key_details, v1.PublicKeyDetails)