From 0b4d8a3b96121d0a89b42109177c45c64fb23309 Mon Sep 17 00:00:00 2001 From: Ramon Petgrave Date: Fri, 20 Jun 2025 17:33:10 +0000 Subject: [PATCH 1/6] add _get_key_details Signed-off-by: Ramon Petgrave --- sigstore/_internal/key_details.py | 38 ++++++++++++++++++++++++ test/unit/internal/test_key_details.py | 40 ++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 sigstore/_internal/key_details.py create mode 100644 test/unit/internal/test_key_details.py diff --git a/sigstore/_internal/key_details.py b/sigstore/_internal/key_details.py new file mode 100644 index 000000000..b931cd7af --- /dev/null +++ b/sigstore/_internal/key_details.py @@ -0,0 +1,38 @@ +# 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 cryptography.hazmat.primitives.asymmetric import ec +from sigstore_protobuf_specs.dev.sigstore.common import v1 + + +def _get_key_details(public_key: v1.PublicKey) -> v1.PublicKeyDetails: + """ + Determine PublicKeyDetails from the public key. + See https://github.com/sigstore/architecture-docs/blob/6a8d78108ef4bb403046817fbcead211a9dca71d/algorithm-registry.md. + """ + if isinstance(public_key, ec.EllipticCurvePublicKey): + if isinstance(public_key.curve, ec.SECP256R1): + return v1.PublicKeyDetails.PKIX_ECDSA_P256_SHA_256 + elif isinstance(public_key.curve, ec.SECP384R1): + return v1.PublicKeyDetails.PKIX_ECDSA_P384_SHA_384 + elif isinstance(public_key.curve, ec.SECP521R1): + return v1.PublicKeyDetails.PKIX_ECDSA_P521_SHA_512 + else: + raise ValueError(f"Unsupported EC curve: {public_key.curve.name}") + else: + raise ValueError(f"Unsupported public key type: {type(public_key)}") diff --git a/test/unit/internal/test_key_details.py b/test/unit/internal/test_key_details.py new file mode 100644 index 000000000..1a91a1b63 --- /dev/null +++ b/test/unit/internal/test_key_details.py @@ -0,0 +1,40 @@ +# 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. + +import pytest +from cryptography.hazmat.primitives.asymmetric import ec +from sigstore_protobuf_specs.dev.sigstore.common import v1 + +from sigstore._internal.key_details import _get_key_details + + +@pytest.mark.parametrize( + "public_key", + [ + ec.generate_private_key(ec.SECP256R1()).public_key(), + ec.generate_private_key(ec.SECP384R1()).public_key(), + ec.generate_private_key(ec.SECP521R1()).public_key(), + pytest.param( + ec.generate_private_key(ec.SECP192R1()).public_key(), + marks=[pytest.mark.xfail(strict=True)], + ), + ], +) +def test_get_key_details(public_key): + """ + Ensures that we return a PublicKeyDetails for supported key types. + """ + key_details = _get_key_details(public_key) + print(key_details) + assert isinstance(key_details, v1.PublicKeyDetails) From 13464b0f33c24c83bfab833037a7c4b6eddc313b Mon Sep 17 00:00:00 2001 From: Ramon Petgrave Date: Fri, 20 Jun 2025 17:45:43 +0000 Subject: [PATCH 2/6] use in v2 client, fix types Signed-off-by: Ramon Petgrave --- sigstore/_internal/key_details.py | 12 ++++++++---- sigstore/_internal/rekor/client_v2.py | 24 +++--------------------- 2 files changed, 11 insertions(+), 25 deletions(-) diff --git a/sigstore/_internal/key_details.py b/sigstore/_internal/key_details.py index b931cd7af..4b0ce1aeb 100644 --- a/sigstore/_internal/key_details.py +++ b/sigstore/_internal/key_details.py @@ -16,23 +16,27 @@ Utilities for getting the sigstore_protobuf_specs.dev.sigstore.common.v1.PublicKeyDetails. """ +from typing import cast + from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives.asymmetric.types import PublicKeyTypes from sigstore_protobuf_specs.dev.sigstore.common import v1 -def _get_key_details(public_key: v1.PublicKey) -> v1.PublicKeyDetails: +def _get_key_details(public_key: PublicKeyTypes) -> v1.PublicKeyDetails: """ Determine PublicKeyDetails from the public key. See https://github.com/sigstore/architecture-docs/blob/6a8d78108ef4bb403046817fbcead211a9dca71d/algorithm-registry.md. """ if isinstance(public_key, ec.EllipticCurvePublicKey): if isinstance(public_key.curve, ec.SECP256R1): - return v1.PublicKeyDetails.PKIX_ECDSA_P256_SHA_256 + key_details = v1.PublicKeyDetails.PKIX_ECDSA_P256_SHA_256 elif isinstance(public_key.curve, ec.SECP384R1): - return v1.PublicKeyDetails.PKIX_ECDSA_P384_SHA_384 + key_details = v1.PublicKeyDetails.PKIX_ECDSA_P384_SHA_384 elif isinstance(public_key.curve, ec.SECP521R1): - return v1.PublicKeyDetails.PKIX_ECDSA_P521_SHA_512 + key_details = v1.PublicKeyDetails.PKIX_ECDSA_P521_SHA_512 else: raise ValueError(f"Unsupported EC curve: {public_key.curve.name}") else: raise ValueError(f"Unsupported public key type: {type(public_key)}") + return cast(v1.PublicKeyDetails, key_details) diff --git a/sigstore/_internal/rekor/client_v2.py b/sigstore/_internal/rekor/client_v2.py index a7d4e9327..ecef5c822 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.public_key()), ), ), ) @@ -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.public_key()), ) ], ) From 4b1576cc1d758eeeec3394bbb1eb33943e7d9dec Mon Sep 17 00:00:00 2001 From: Ramon Petgrave Date: Fri, 20 Jun 2025 20:21:13 +0000 Subject: [PATCH 3/6] details from certificate, add rsa and ed25519 Signed-off-by: Ramon Petgrave --- sigstore/_internal/key_details.py | 41 +++++++-- sigstore/_internal/rekor/client_v2.py | 4 +- test/unit/internal/test_key_details.py | 111 ++++++++++++++++++++++--- 3 files changed, 139 insertions(+), 17 deletions(-) diff --git a/sigstore/_internal/key_details.py b/sigstore/_internal/key_details.py index 4b0ce1aeb..f32c7dcd6 100644 --- a/sigstore/_internal/key_details.py +++ b/sigstore/_internal/key_details.py @@ -18,16 +18,20 @@ from typing import cast -from cryptography.hazmat.primitives.asymmetric import ec -from cryptography.hazmat.primitives.asymmetric.types import PublicKeyTypes +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(public_key: PublicKeyTypes) -> v1.PublicKeyDetails: +def _get_key_details(certificate: Certificate) -> v1.PublicKeyDetails: """ - Determine PublicKeyDetails from the public key. - See https://github.com/sigstore/architecture-docs/blob/6a8d78108ef4bb403046817fbcead211a9dca71d/algorithm-registry.md. + Determine PublicKeyDetails from the Certificate. + 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 @@ -37,6 +41,33 @@ def _get_key_details(public_key: PublicKeyTypes) -> v1.PublicKeyDetails: 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 == 2048: + raise ValueError("Unsupported RSA key size: 2048") + elif 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 ecef5c822..d2c758a50 100644 --- a/sigstore/_internal/rekor/client_v2.py +++ b/sigstore/_internal/rekor/client_v2.py @@ -113,7 +113,7 @@ def _build_hashed_rekord_request( encoding=serialization.Encoding.DER ) ), - key_details=_get_key_details(certificate.public_key()), + key_details=_get_key_details(certificate), ), ), ) @@ -147,7 +147,7 @@ def _build_dsse_request( encoding=serialization.Encoding.DER ) ), - key_details=_get_key_details(certificate.public_key()), + key_details=_get_key_details(certificate), ) ], ) diff --git a/test/unit/internal/test_key_details.py b/test/unit/internal/test_key_details.py index 1a91a1b63..43302fcba 100644 --- a/test/unit/internal/test_key_details.py +++ b/test/unit/internal/test_key_details.py @@ -12,29 +12,120 @@ # 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 ec +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( - "public_key", + "mock_certificate", [ - ec.generate_private_key(ec.SECP256R1()).public_key(), - ec.generate_private_key(ec.SECP384R1()).public_key(), - ec.generate_private_key(ec.SECP521R1()).public_key(), + # 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( - ec.generate_private_key(ec.SECP192R1()).public_key(), + 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(public_key): +def test_get_key_details(mock_certificate): """ - Ensures that we return a PublicKeyDetails for supported key types. + Ensures that we return a PublicKeyDetails for supported key types and schemes. """ - key_details = _get_key_details(public_key) - print(key_details) + key_details = _get_key_details(mock_certificate) assert isinstance(key_details, v1.PublicKeyDetails) From 60b5025f1bcbe04dc76bc0704e3dddc8ad3e3990 Mon Sep 17 00:00:00 2001 From: Ramon Petgrave Date: Fri, 20 Jun 2025 20:25:34 +0000 Subject: [PATCH 4/6] changelog Signed-off-by: Ramon Petgrave --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b10608fc0..fac5f4469 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. From 4bd34d7e145a60b110925106cc8c1556aa01417f Mon Sep 17 00:00:00 2001 From: Ramon Petgrave Date: Fri, 20 Jun 2025 20:44:53 +0000 Subject: [PATCH 5/6] note about unrecommended types Signed-off-by: Ramon Petgrave --- sigstore/_internal/key_details.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sigstore/_internal/key_details.py b/sigstore/_internal/key_details.py index f32c7dcd6..9ef95f516 100644 --- a/sigstore/_internal/key_details.py +++ b/sigstore/_internal/key_details.py @@ -26,6 +26,7 @@ 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 From 3482068f9bdbc42d6e36eba91ec7e72a91f1be32 Mon Sep 17 00:00:00 2001 From: Ramon Petgrave Date: Mon, 23 Jun 2025 14:46:03 +0000 Subject: [PATCH 6/6] cleanup if-else Signed-off-by: Ramon Petgrave --- sigstore/_internal/key_details.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/sigstore/_internal/key_details.py b/sigstore/_internal/key_details.py index 9ef95f516..7c65ec8ba 100644 --- a/sigstore/_internal/key_details.py +++ b/sigstore/_internal/key_details.py @@ -43,9 +43,7 @@ def _get_key_details(certificate: Certificate) -> v1.PublicKeyDetails: else: raise ValueError(f"Unsupported EC curve: {public_key.curve.name}") elif isinstance(public_key, rsa.RSAPublicKey): - if public_key.key_size == 2048: - raise ValueError("Unsupported RSA key size: 2048") - elif public_key.key_size == 3072: + 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):