Skip to content

Commit a18d758

Browse files
ramonpetgrave64jku
andauthored
Add function for determining key_details (#1456)
* add _get_key_details Signed-off-by: Ramon Petgrave <[email protected]> * use in v2 client, fix types Signed-off-by: Ramon Petgrave <[email protected]> * details from certificate, add rsa and ed25519 Signed-off-by: Ramon Petgrave <[email protected]> * changelog Signed-off-by: Ramon Petgrave <[email protected]> * note about unrecommended types Signed-off-by: Ramon Petgrave <[email protected]> * cleanup if-else Signed-off-by: Ramon Petgrave <[email protected]> --------- Signed-off-by: Ramon Petgrave <[email protected]> Co-authored-by: Jussi Kukkonen <[email protected]>
1 parent e3fe11c commit a18d758

File tree

4 files changed

+209
-22
lines changed

4 files changed

+209
-22
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,12 @@ All versions prior to 0.9.0 are untracked.
1919
* API: `IdentityToken` now supports `client_id` for audience claim validation.
2020
[#1402](https://github.com/sigstore/sigstore-python/pull/1402)
2121

22-
2322
* Added a `RekorV2Client` for posting new entries to a Rekor V2 instance.
2423
[#1400](https://github.com/sigstore/sigstore-python/pull/1422)
2524

25+
* Added a function for determining the `key_details` of a certificate`.
26+
[#1456](https://github.com/sigstore/sigstore-python/pull/1456)
27+
2628
### Fixed
2729

2830
* Avoid instantiation issues with `TransparencyLogEntry` when `InclusionPromise` is not present.

sigstore/_internal/key_details.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# Copyright 2025 The Sigstore Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""
16+
Utilities for getting the sigstore_protobuf_specs.dev.sigstore.common.v1.PublicKeyDetails.
17+
"""
18+
19+
from typing import cast
20+
21+
from cryptography.hazmat.primitives.asymmetric import ec, ed25519, padding, rsa
22+
from cryptography.x509 import Certificate
23+
from sigstore_protobuf_specs.dev.sigstore.common import v1
24+
25+
26+
def _get_key_details(certificate: Certificate) -> v1.PublicKeyDetails:
27+
"""
28+
Determine PublicKeyDetails from the Certificate.
29+
We disclude the unrecommended types.
30+
See
31+
- https://github.com/sigstore/architecture-docs/blob/6a8d78108ef4bb403046817fbcead211a9dca71d/algorithm-registry.md.
32+
- https://github.com/sigstore/protobuf-specs/blob/3aaae418f76fb4b34df4def4cd093c464f20fed3/protos/sigstore_common.proto
33+
"""
34+
public_key = certificate.public_key()
35+
params = certificate.signature_algorithm_parameters
36+
if isinstance(public_key, ec.EllipticCurvePublicKey):
37+
if isinstance(public_key.curve, ec.SECP256R1):
38+
key_details = v1.PublicKeyDetails.PKIX_ECDSA_P256_SHA_256
39+
elif isinstance(public_key.curve, ec.SECP384R1):
40+
key_details = v1.PublicKeyDetails.PKIX_ECDSA_P384_SHA_384
41+
elif isinstance(public_key.curve, ec.SECP521R1):
42+
key_details = v1.PublicKeyDetails.PKIX_ECDSA_P521_SHA_512
43+
else:
44+
raise ValueError(f"Unsupported EC curve: {public_key.curve.name}")
45+
elif isinstance(public_key, rsa.RSAPublicKey):
46+
if public_key.key_size == 3072:
47+
if isinstance(params, padding.PKCS1v15):
48+
key_details = v1.PublicKeyDetails.PKIX_RSA_PKCS1V15_3072_SHA256
49+
elif isinstance(params, padding.PSS):
50+
key_details = v1.PublicKeyDetails.PKIX_RSA_PSS_3072_SHA256
51+
else:
52+
raise ValueError(
53+
f"Unsupported public key type, size, and padding: {type(public_key)}, {public_key.key_size}, {params}"
54+
)
55+
elif public_key.key_size == 4096:
56+
if isinstance(params, padding.PKCS1v15):
57+
key_details = v1.PublicKeyDetails.PKIX_RSA_PKCS1V15_3072_SHA256
58+
elif isinstance(params, padding.PSS):
59+
key_details = v1.PublicKeyDetails.PKIX_RSA_PSS_3072_SHA256
60+
else:
61+
raise ValueError(
62+
f"Unsupported public key type, size, and padding: {type(public_key)}, {public_key.key_size}, {params}"
63+
)
64+
else:
65+
raise ValueError(f"Unsupported RSA key size: {public_key.key_size}")
66+
elif isinstance(public_key, ed25519.Ed25519PublicKey):
67+
key_details = v1.PublicKeyDetails.PKIX_ED25519
68+
# There is likely no need to explicitly detect PKIX_ED25519_PH, especially since the cryptography
69+
# library does not yet support Ed25519ph.
70+
else:
71+
raise ValueError(f"Unsupported public key type: {type(public_key)}")
72+
return cast(v1.PublicKeyDetails, key_details)

sigstore/_internal/rekor/client_v2.py

Lines changed: 3 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,16 @@
2020

2121
import json
2222
import logging
23-
from typing import cast
2423

2524
import requests
2625
from cryptography.hazmat.primitives import serialization
27-
from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePublicKey
2826
from cryptography.x509 import Certificate
2927
from sigstore_protobuf_specs.dev.sigstore.common import v1 as common_v1
3028
from sigstore_protobuf_specs.dev.sigstore.rekor import v2
3129
from sigstore_protobuf_specs.io import intoto
3230

3331
from sigstore._internal import USER_AGENT
32+
from sigstore._internal.key_details import _get_key_details
3433
from sigstore._internal.rekor import (
3534
EntryRequestBody,
3635
RekorClientError,
@@ -93,23 +92,6 @@ def create_entry(self, payload: EntryRequestBody) -> LogEntry:
9392
_logger.debug(f"integrated: {integrated_entry}")
9493
return LogEntry._from_dict_rekor(integrated_entry)
9594

96-
@staticmethod
97-
def _get_key_details(certificate: Certificate) -> common_v1.PublicKeyDetails:
98-
"""
99-
Determine PublicKeyDetails from a certificate
100-
101-
We know that sign.Signer only uses secp256r1, so do not support anything else.
102-
"""
103-
public_key = certificate.public_key()
104-
if isinstance(public_key, EllipticCurvePublicKey):
105-
if public_key.curve.name == "secp256r1":
106-
return cast(
107-
common_v1.PublicKeyDetails,
108-
common_v1.PublicKeyDetails.PKIX_ECDSA_P256_SHA_256,
109-
)
110-
raise ValueError(f"Unsupported EC curve: {public_key.curve.name}")
111-
raise ValueError(f"Unsupported public key type: {type(public_key)}")
112-
11395
@classmethod
11496
def _build_hashed_rekord_request(
11597
cls,
@@ -131,7 +113,7 @@ def _build_hashed_rekord_request(
131113
encoding=serialization.Encoding.DER
132114
)
133115
),
134-
key_details=cls._get_key_details(certificate),
116+
key_details=_get_key_details(certificate),
135117
),
136118
),
137119
)
@@ -165,7 +147,7 @@ def _build_dsse_request(
165147
encoding=serialization.Encoding.DER
166148
)
167149
),
168-
key_details=cls._get_key_details(certificate),
150+
key_details=_get_key_details(certificate),
169151
)
170152
],
171153
)
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
# Copyright 2025 The Sigstore Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from unittest.mock import Mock
16+
17+
import pytest
18+
from cryptography.hazmat.primitives.asymmetric import dsa, ec, ed25519, padding, rsa
19+
from sigstore_protobuf_specs.dev.sigstore.common import v1
20+
21+
from sigstore._internal.key_details import _get_key_details
22+
23+
24+
@pytest.mark.parametrize(
25+
"mock_certificate",
26+
[
27+
# ec
28+
pytest.param(
29+
Mock(
30+
public_key=Mock(
31+
return_value=ec.generate_private_key(ec.SECP192R1()).public_key()
32+
)
33+
),
34+
marks=[pytest.mark.xfail(strict=True)],
35+
),
36+
Mock(
37+
public_key=Mock(
38+
return_value=ec.generate_private_key(ec.SECP256R1()).public_key()
39+
)
40+
),
41+
Mock(
42+
public_key=Mock(
43+
return_value=ec.generate_private_key(ec.SECP384R1()).public_key()
44+
)
45+
),
46+
Mock(
47+
public_key=Mock(
48+
return_value=ec.generate_private_key(ec.SECP521R1()).public_key()
49+
)
50+
),
51+
# rsa pkcs1
52+
pytest.param(
53+
Mock(
54+
public_key=Mock(
55+
return_value=rsa.generate_private_key(
56+
public_exponent=65537, key_size=2048
57+
).public_key()
58+
),
59+
signature_algorithm_parameters=padding.PKCS1v15(),
60+
),
61+
marks=[pytest.mark.xfail(strict=True)],
62+
),
63+
Mock(
64+
public_key=Mock(
65+
return_value=rsa.generate_private_key(
66+
public_exponent=65537, key_size=3072
67+
).public_key()
68+
),
69+
signature_algorithm_parameters=padding.PKCS1v15(),
70+
),
71+
Mock(
72+
public_key=Mock(
73+
return_value=rsa.generate_private_key(
74+
public_exponent=65537, key_size=4096
75+
).public_key()
76+
),
77+
signature_algorithm_parameters=padding.PKCS1v15(),
78+
),
79+
# rsa pss
80+
pytest.param(
81+
Mock(
82+
public_key=Mock(
83+
return_value=rsa.generate_private_key(
84+
public_exponent=65537, key_size=2048
85+
).public_key()
86+
),
87+
signature_algorithm_parameters=padding.PSS(None, 0),
88+
),
89+
marks=[pytest.mark.xfail(strict=True)],
90+
),
91+
Mock(
92+
public_key=Mock(
93+
return_value=rsa.generate_private_key(
94+
public_exponent=65537, key_size=3072
95+
).public_key()
96+
),
97+
signature_algorithm_parameters=padding.PSS(None, 0),
98+
),
99+
Mock(
100+
public_key=Mock(
101+
return_value=rsa.generate_private_key(
102+
public_exponent=65537, key_size=4096
103+
).public_key()
104+
),
105+
signature_algorithm_parameters=padding.PSS(None, 0),
106+
),
107+
# ed25519
108+
Mock(
109+
public_key=Mock(
110+
return_value=ed25519.Ed25519PrivateKey.generate().public_key(),
111+
signature_algorithm_parameters=None,
112+
)
113+
),
114+
# unsupported
115+
pytest.param(
116+
Mock(
117+
public_key=Mock(
118+
return_value=dsa.generate_private_key(key_size=1024).public_key()
119+
),
120+
signature_algorithm_parameters=None,
121+
),
122+
marks=[pytest.mark.xfail(strict=True)],
123+
),
124+
],
125+
)
126+
def test_get_key_details(mock_certificate):
127+
"""
128+
Ensures that we return a PublicKeyDetails for supported key types and schemes.
129+
"""
130+
key_details = _get_key_details(mock_certificate)
131+
assert isinstance(key_details, v1.PublicKeyDetails)

0 commit comments

Comments
 (0)