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"""
16Utilities for PublicKeyDetails and the algorithm registry.
17"""
18
19from __future__ import annotations
20
21import hashlib
22from collections.abc import Callable
23from dataclasses import dataclass
24
25from cryptography.hazmat.primitives.asymmetric import ec, ed25519, padding, rsa
26from cryptography.x509 import Certificate
27from sigstore_models.common.v1 import HashAlgorithm, PublicKeyDetails
28
29
30@dataclass(frozen=True)
31class AlgorithmDetails:
32 """Details for a single entry in the algorithm registry."""
33
34 key_details: PublicKeyDetails
35 hash_algorithm: HashAlgorithm | None
36 hash_func: Callable[[bytes], hashlib._Hash] | None
37
38
39# Algorithm registry table.
40# See https://github.com/sigstore/architecture-docs/blob/main/algorithm-registry.md
41_ALGORITHM_REGISTRY: list[AlgorithmDetails] = [
42 # RSA PKCS1v15
43 AlgorithmDetails(
44 PublicKeyDetails.PKIX_RSA_PKCS1V15_2048_SHA256,
45 HashAlgorithm.SHA2_256,
46 hashlib.sha256,
47 ),
48 AlgorithmDetails(
49 PublicKeyDetails.PKIX_RSA_PKCS1V15_3072_SHA256,
50 HashAlgorithm.SHA2_256,
51 hashlib.sha256,
52 ),
53 AlgorithmDetails(
54 PublicKeyDetails.PKIX_RSA_PKCS1V15_4096_SHA256,
55 HashAlgorithm.SHA2_256,
56 hashlib.sha256,
57 ),
58 # ECDSA
59 AlgorithmDetails(
60 PublicKeyDetails.PKIX_ECDSA_P256_SHA_256,
61 HashAlgorithm.SHA2_256,
62 hashlib.sha256,
63 ),
64 AlgorithmDetails(
65 PublicKeyDetails.PKIX_ECDSA_P384_SHA_384,
66 HashAlgorithm.SHA2_384,
67 hashlib.sha384,
68 ),
69 AlgorithmDetails(
70 PublicKeyDetails.PKIX_ECDSA_P521_SHA_512,
71 HashAlgorithm.SHA2_512,
72 hashlib.sha512,
73 ),
74 # Ed25519
75 AlgorithmDetails(
76 PublicKeyDetails.PKIX_ED25519,
77 None,
78 None,
79 ),
80 AlgorithmDetails(
81 PublicKeyDetails.PKIX_ED25519_PH,
82 HashAlgorithm.SHA2_512,
83 hashlib.sha512,
84 ),
85]
86
87_DETAILS_BY_KEY: dict[PublicKeyDetails, AlgorithmDetails] = {
88 entry.key_details: entry for entry in _ALGORITHM_REGISTRY
89}
90
91
92def _get_algorithm_details(key_details: PublicKeyDetails) -> AlgorithmDetails:
93 """
94 Look up algorithm details by ``PublicKeyDetails`` enum value.
95 """
96 details = _DETAILS_BY_KEY.get(key_details)
97 if details is None:
98 raise ValueError(f"unknown signature algorithm: {key_details}")
99 return details
100
101
102def _get_prehash(
103 key_details: PublicKeyDetails,
104) -> tuple[HashAlgorithm, Callable[[bytes], hashlib._Hash]]:
105 """
106 Return the externalized hash function for a signing algorithm.
107
108 Only algorithms with an externalized prehash can be used in hashedrekord
109 entries. Pure ed25519 (no prehash) raises ``ValueError``.
110 """
111 details = _get_algorithm_details(key_details)
112 if details.hash_algorithm is None or details.hash_func is None:
113 raise ValueError(
114 f"signing algorithm {key_details} has no externalized prehash; "
115 "cannot be used for a hashedrekord entry (rekor-v2-spec §6.1.4)"
116 )
117 return details.hash_algorithm, details.hash_func
118
119
120def _get_key_details(certificate: Certificate) -> PublicKeyDetails:
121 """
122 Determine PublicKeyDetails from the Certificate.
123 We disclude the unrecommended types.
124 See
125 - https://github.com/sigstore/architecture-docs/blob/6a8d78108ef4bb403046817fbcead211a9dca71d/algorithm-registry.md.
126 - https://github.com/sigstore/protobuf-specs/blob/3aaae418f76fb4b34df4def4cd093c464f20fed3/protos/sigstore_common.proto
127 """
128 public_key = certificate.public_key()
129 params = certificate.signature_algorithm_parameters
130 if isinstance(public_key, ec.EllipticCurvePublicKey):
131 if isinstance(public_key.curve, ec.SECP256R1):
132 key_details = PublicKeyDetails.PKIX_ECDSA_P256_SHA_256
133 elif isinstance(public_key.curve, ec.SECP384R1):
134 key_details = PublicKeyDetails.PKIX_ECDSA_P384_SHA_384
135 elif isinstance(public_key.curve, ec.SECP521R1):
136 key_details = PublicKeyDetails.PKIX_ECDSA_P521_SHA_512
137 else:
138 raise ValueError(f"Unsupported EC curve: {public_key.curve.name}")
139 elif isinstance(public_key, rsa.RSAPublicKey):
140 if public_key.key_size == 2048:
141 if isinstance(params, padding.PKCS1v15):
142 key_details = PublicKeyDetails.PKIX_RSA_PKCS1V15_2048_SHA256
143 else:
144 raise ValueError(
145 f"Unsupported public key type, size, and padding: {type(public_key)}, {public_key.key_size}, {params}"
146 )
147 elif public_key.key_size == 3072:
148 if isinstance(params, padding.PKCS1v15):
149 key_details = PublicKeyDetails.PKIX_RSA_PKCS1V15_3072_SHA256
150 else:
151 raise ValueError(
152 f"Unsupported public key type, size, and padding: {type(public_key)}, {public_key.key_size}, {params}"
153 )
154 elif public_key.key_size == 4096:
155 if isinstance(params, padding.PKCS1v15):
156 key_details = PublicKeyDetails.PKIX_RSA_PKCS1V15_4096_SHA256
157 else:
158 raise ValueError(
159 f"Unsupported public key type, size, and padding: {type(public_key)}, {public_key.key_size}, {params}"
160 )
161 else:
162 raise ValueError(f"Unsupported RSA key size: {public_key.key_size}")
163 elif isinstance(public_key, ed25519.Ed25519PublicKey):
164 key_details = PublicKeyDetails.PKIX_ED25519
165 # There is likely no need to explicitly detect PKIX_ED25519_PH, especially since the cryptography
166 # library does not yet support Ed25519ph.
167 else:
168 raise ValueError(f"Unsupported public key type: {type(public_key)}")
169 return key_details