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

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

64 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 

18import hashlib 

19import pathlib 

20from typing import Optional 

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 google.protobuf import json_format 

27from sigstore_models import intoto as intoto_pb 

28from sigstore_models.bundle import v1 as bundle_pb 

29from sigstore_models.common import v1 as common_pb 

30from typing_extensions import override 

31 

32from model_signing._signing import sign_sigstore_pb as sigstore_pb 

33from model_signing._signing import signing 

34 

35 

36def _check_supported_ec_key(public_key: ec.EllipticCurvePublicKey): 

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

38 

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

40 Sigstore's protobuf specs. 

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

42 

43 Args: 

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

45 

46 Raises: 

47 ValueError: The key is not supported. 

48 """ 

49 curve = public_key.curve.name 

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

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

52 

53 

54def get_ec_key_hash( 

55 public_key: ec.EllipticCurvePublicKey, 

56) -> hashes.HashAlgorithm: 

57 """Returns the public key hashing algorithm. 

58 

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

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

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

62 

63 Args: 

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

65 

66 Raises: 

67 ValueError: The key is not supported. 

68 """ 

69 key_size = public_key.curve.key_size 

70 

71 # TODO: Once Python 3.9 support is deprecated revert to using `match` 

72 if key_size == 256: 

73 return hashes.SHA256() 

74 if key_size == 384: 

75 return hashes.SHA384() 

76 if key_size == 521: 

77 return hashes.SHA512() 

78 

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

80 

81 

82class Signer(sigstore_pb.Signer): 

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

84 

85 def __init__( 

86 self, private_key_path: pathlib.Path, password: Optional[str] = None 

87 ): 

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

89 

90 Args: 

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

92 password: Optional password for the private key. 

93 """ 

94 self._private_key = serialization.load_pem_private_key( 

95 private_key_path.read_bytes(), password 

96 ) 

97 _check_supported_ec_key(self._private_key.public_key()) 

98 

99 @override 

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

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

102 "utf-8" 

103 ) 

104 

105 raw_signature = intoto_pb.Signature( 

106 sig=base64.b64encode( 

107 self._private_key.sign( 

108 sigstore_pb.pae(raw_payload), 

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

110 ) 

111 ), 

112 keyid="", 

113 ) 

114 

115 envelope = intoto_pb.Envelope( 

116 payload=base64.b64encode(raw_payload), 

117 payload_type=signing._IN_TOTO_JSON_PAYLOAD_TYPE, 

118 signatures=[raw_signature], 

119 ) 

120 

121 return sigstore_pb.Signature( 

122 bundle_pb.Bundle( 

123 media_type=sigstore_pb._BUNDLE_MEDIA_TYPE, 

124 verification_material=self._get_verification_material(), 

125 dsse_envelope=envelope, 

126 ) 

127 ) 

128 

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

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

131 public_key = self._private_key.public_key() 

132 

133 raw_bytes = public_key.public_bytes( 

134 encoding=serialization.Encoding.PEM, 

135 format=serialization.PublicFormat.SubjectPublicKeyInfo, 

136 ) 

137 

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

139 

140 return bundle_pb.VerificationMaterial( 

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

142 tlog_entries=[], 

143 ) 

144 

145 

146class Verifier(sigstore_pb.Verifier): 

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

148 

149 def __init__(self, public_key_path: pathlib.Path): 

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

151 

152 Args: 

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

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

155 """ 

156 self._public_key = serialization.load_pem_public_key( 

157 public_key_path.read_bytes() 

158 ) 

159 _check_supported_ec_key(self._public_key) 

160 

161 @override 

162 def _verify_bundle(self, bundle: bundle_pb.Bundle) -> tuple[str, bytes]: 

163 raw_bytes = self._public_key.public_bytes( 

164 encoding=serialization.Encoding.PEM, 

165 format=serialization.PublicFormat.SubjectPublicKeyInfo, 

166 ) 

167 

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

169 

170 if bundle.verification_material.public_key.hint: 

171 key_hint = bundle.verification_material.public_key.hint 

172 if key_hint != hash_bytes: 

173 raise ValueError( 

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

175 "verification material does not match the provided " 

176 "public key. " 

177 ) 

178 else: 

179 print( 

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

181 "verification material format. Please re-sign " 

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

183 "identifier hash. " 

184 ) 

185 

186 envelope = bundle.dsse_envelope 

187 try: 

188 self._public_key.verify( 

189 envelope.signatures[0].sig, 

190 sigstore_pb.pae(envelope.payload), 

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

192 ) 

193 except exceptions.InvalidSignature: 

194 # Compatibility layer with pre 1.0 release 

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

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

197 # the material that got signed over. 

198 self._public_key.verify( 

199 envelope.signatures[0].sig, 

200 sigstore_pb.pae_compat(envelope.payload), 

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

202 ) 

203 

204 return envelope.payload_type, envelope.payload