Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/model_signing/_signing/sign_ec_key.py: 88%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

75 statements  

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"""Signers and verifiers using elliptic curve keys.""" 

16 

17import base64 

18from datetime import datetime 

19import hashlib 

20import pathlib 

21 

22from cryptography import exceptions 

23from cryptography.hazmat.primitives import hashes 

24from cryptography.hazmat.primitives import serialization 

25from cryptography.hazmat.primitives.asymmetric import ec 

26from cryptography.hazmat.primitives.asymmetric import types as crypto_types 

27from google.protobuf import json_format 

28from sigstore_models import intoto as intoto_pb 

29from sigstore_models.bundle import v1 as bundle_pb 

30from sigstore_models.common import v1 as common_pb 

31from typing_extensions import override 

32 

33from model_signing._signing import sign_sigstore_pb as sigstore_pb 

34from model_signing._signing import signing 

35 

36 

37def _check_supported_ec_key(public_key: crypto_types.PublicKeyTypes): 

38 """Checks if the elliptic curve key is supported by our package. 

39 

40 We only support a family of curves, trying to match those specified by 

41 Sigstore's protobuf specs. 

42 See https://github.com/sigstore/model-transparency/issues/385. 

43 

44 Args: 

45 public_key: The public key to check. Can be obtained from a private key. 

46 

47 Raises: 

48 ValueError: The key is not supported, or is not an elliptic curve one. 

49 """ 

50 if not isinstance(public_key, ec.EllipticCurvePublicKey): 

51 raise ValueError("Only elliptic curve keys are supported") 

52 

53 curve = public_key.curve.name 

54 if curve not in ["secp256r1", "secp384r1", "secp521r1"]: 

55 raise ValueError(f"Unsupported key for curve '{curve}'") 

56 

57 

58def get_ec_key_hash( 

59 public_key: ec.EllipticCurvePublicKey, 

60) -> hashes.HashAlgorithm: 

61 """Returns the public key hashing algorithm. 

62 

63 We need to record this in the sigstore bundle when signing and retrieve when 

64 performing verification. This is according to sigstore protobuf specs and is 

65 used both when signing with keys and when signing with certificates. 

66 

67 Args: 

68 public_key: The public key to get the hash algorithm from. 

69 

70 Raises: 

71 ValueError: The key is not supported. 

72 """ 

73 key_size = public_key.curve.key_size 

74 

75 match key_size: 

76 case 256: 

77 return hashes.SHA256() 

78 case 384: 

79 return hashes.SHA384() 

80 case 521: 

81 return hashes.SHA512() 

82 case _: 

83 raise ValueError(f"Unexpected key size {key_size}") 

84 

85 

86class Signer(sigstore_pb.Signer): 

87 """Signer using an elliptic curve private key.""" 

88 

89 def __init__( 

90 self, 

91 private_key_path: pathlib.Path, 

92 password: str | None = None, 

93 tsa_url: str | None = None, 

94 ): 

95 """Initializes the signer with the private key and optional password. 

96 

97 Args: 

98 private_key_path: The path to the PEM encoded private key. 

99 password: Optional password for the private key. 

100 tsa_url: Optional URL of an RFC 3161 Timestamp Authority. 

101 """ 

102 self._private_key = serialization.load_pem_private_key( 

103 private_key_path.read_bytes(), password 

104 ) 

105 _check_supported_ec_key(self._private_key.public_key()) 

106 self._tsa_url = tsa_url 

107 

108 @override 

109 def sign(self, payload: signing.Payload) -> signing.Signature: 

110 raw_payload = json_format.MessageToJson(payload.statement.pb).encode( 

111 "utf-8" 

112 ) 

113 

114 signature_bytes = self._private_key.sign( 

115 sigstore_pb.pae(raw_payload), 

116 ec.ECDSA(get_ec_key_hash(self._private_key.public_key())), 

117 ) 

118 

119 raw_signature = intoto_pb.Signature( 

120 sig=base64.b64encode(signature_bytes), keyid="" 

121 ) 

122 

123 envelope = intoto_pb.Envelope( 

124 payload=base64.b64encode(raw_payload), 

125 payload_type=signing._IN_TOTO_JSON_PAYLOAD_TYPE, 

126 signatures=[raw_signature], 

127 ) 

128 

129 verification_material = self._get_verification_material() 

130 

131 if self._tsa_url: 

132 verification_material.timestamp_verification_data = ( 

133 sigstore_pb.request_timestamp(signature_bytes, self._tsa_url) 

134 ) 

135 

136 return sigstore_pb.Signature( 

137 bundle_pb.Bundle( 

138 media_type=sigstore_pb._BUNDLE_MEDIA_TYPE, 

139 verification_material=verification_material, 

140 dsse_envelope=envelope, 

141 ) 

142 ) 

143 

144 def _get_verification_material(self) -> bundle_pb.VerificationMaterial: 

145 """Returns the verification material to include in the bundle.""" 

146 public_key = self._private_key.public_key() 

147 

148 raw_bytes = public_key.public_bytes( 

149 encoding=serialization.Encoding.PEM, 

150 format=serialization.PublicFormat.SubjectPublicKeyInfo, 

151 ) 

152 

153 hash_bytes = hashlib.sha256(raw_bytes).digest().hex() 

154 

155 return bundle_pb.VerificationMaterial( 

156 public_key=common_pb.PublicKeyIdentifier(hint=hash_bytes), 

157 tlog_entries=[], 

158 ) 

159 

160 

161class Verifier(sigstore_pb.Verifier): 

162 """Verifier for signatures generated with an elliptic curve private key.""" 

163 

164 def __init__( 

165 self, 

166 public_key_path: pathlib.Path, 

167 trust_config: pathlib.Path | None = None, 

168 require_timestamp: bool = False, 

169 ): 

170 """Initializes the verifier with the public key to use. 

171 

172 Args: 

173 public_key_path: The path to the public key to use. This must be 

174 paired with the private key used to generate the signature. 

175 trust_config: A Sigstore client trust configuration or trusted root. 

176 require_timestamp: Reject bundles without an RFC 3161 timestamp. 

177 """ 

178 super().__init__(trust_config, require_timestamp=require_timestamp) 

179 self._public_key = serialization.load_pem_public_key( 

180 public_key_path.read_bytes() 

181 ) 

182 _check_supported_ec_key(self._public_key) 

183 

184 @override 

185 def _verify_bundle( 

186 self, bundle: bundle_pb.Bundle, verification_time: datetime | None 

187 ) -> tuple[str, bytes]: 

188 raw_bytes = self._public_key.public_bytes( 

189 encoding=serialization.Encoding.PEM, 

190 format=serialization.PublicFormat.SubjectPublicKeyInfo, 

191 ) 

192 

193 hash_bytes = hashlib.sha256(raw_bytes).digest().hex() 

194 

195 if bundle.verification_material.public_key.hint: 

196 key_hint = bundle.verification_material.public_key.hint 

197 if key_hint != hash_bytes: 

198 raise ValueError( 

199 "Key mismatch: The public key hash in the signature's " 

200 "verification material does not match the provided " 

201 "public key. " 

202 ) 

203 else: 

204 print( 

205 "WARNING: This model's signature uses an older " 

206 "verification material format. Please re-sign " 

207 "with an updated signer to use a public key " 

208 "identifier hash. " 

209 ) 

210 

211 envelope = bundle.dsse_envelope 

212 try: 

213 self._public_key.verify( 

214 envelope.signatures[0].sig, 

215 sigstore_pb.pae(envelope.payload), 

216 ec.ECDSA(get_ec_key_hash(self._public_key)), 

217 ) 

218 except exceptions.InvalidSignature: 

219 # Compatibility layer with pre 1.0 release 

220 # Here, we patch over a bug in `pae` which mixed unicode `str` and 

221 # `bytes`. As a result, additional escape characters were added to 

222 # the material that got signed over. 

223 self._public_key.verify( 

224 envelope.signatures[0].sig, 

225 sigstore_pb.pae_compat(envelope.payload), 

226 ec.ECDSA(get_ec_key_hash(self._public_key)), 

227 ) 

228 

229 return envelope.payload_type, envelope.payload