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

63 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 hashlib 

18import pathlib 

19from typing import Optional 

20 

21from cryptography import exceptions 

22from cryptography.hazmat.primitives import hashes 

23from cryptography.hazmat.primitives import serialization 

24from cryptography.hazmat.primitives.asymmetric import ec 

25from google.protobuf import json_format 

26from sigstore_protobuf_specs.dev.sigstore.bundle import v1 as bundle_pb 

27from sigstore_protobuf_specs.dev.sigstore.common import v1 as common_pb 

28from sigstore_protobuf_specs.io import intoto as intoto_pb 

29from typing_extensions import override 

30 

31from model_signing._signing import sign_sigstore_pb as sigstore_pb 

32from model_signing._signing import signing 

33 

34 

35def _check_supported_ec_key(public_key: ec.EllipticCurvePublicKey): 

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

37 

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

39 Sigstore's protobuf specs. 

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

41 

42 Args: 

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

44 

45 Raises: 

46 ValueError: The key is not supported. 

47 """ 

48 curve = public_key.curve.name 

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

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

51 

52 

53def get_ec_key_hash( 

54 public_key: ec.EllipticCurvePublicKey, 

55) -> hashes.HashAlgorithm: 

56 """Returns the public key hashing algorithm. 

57 

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

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

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

61 

62 Args: 

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

64 

65 Raises: 

66 ValueError: The key is not supported. 

67 """ 

68 key_size = public_key.curve.key_size 

69 

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

71 if key_size == 256: 

72 return hashes.SHA256() 

73 if key_size == 384: 

74 return hashes.SHA384() 

75 if key_size == 521: 

76 return hashes.SHA512() 

77 

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

79 

80 

81class Signer(sigstore_pb.Signer): 

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

83 

84 def __init__( 

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

86 ): 

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

88 

89 Args: 

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

91 password: Optional password for the private key. 

92 """ 

93 self._private_key = serialization.load_pem_private_key( 

94 private_key_path.read_bytes(), password 

95 ) 

96 _check_supported_ec_key(self._private_key.public_key()) 

97 

98 @override 

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

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

101 "utf-8" 

102 ) 

103 

104 raw_signature = intoto_pb.Signature( 

105 sig=self._private_key.sign( 

106 sigstore_pb.pae(raw_payload), 

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

108 ), 

109 keyid="", 

110 ) 

111 

112 envelope = intoto_pb.Envelope( 

113 payload=raw_payload, 

114 payload_type=signing._IN_TOTO_JSON_PAYLOAD_TYPE, 

115 signatures=[raw_signature], 

116 ) 

117 

118 return sigstore_pb.Signature( 

119 bundle_pb.Bundle( 

120 media_type=sigstore_pb._BUNDLE_MEDIA_TYPE, 

121 verification_material=self._get_verification_material(), 

122 dsse_envelope=envelope, 

123 ) 

124 ) 

125 

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

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

128 public_key = self._private_key.public_key() 

129 

130 raw_bytes = public_key.public_bytes( 

131 encoding=serialization.Encoding.PEM, 

132 format=serialization.PublicFormat.SubjectPublicKeyInfo, 

133 ) 

134 

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

136 

137 return bundle_pb.VerificationMaterial( 

138 public_key=common_pb.PublicKeyIdentifier(hint=hash_bytes) 

139 ) 

140 

141 

142class Verifier(sigstore_pb.Verifier): 

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

144 

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

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

147 

148 Args: 

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

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

151 """ 

152 self._public_key = serialization.load_pem_public_key( 

153 public_key_path.read_bytes() 

154 ) 

155 _check_supported_ec_key(self._public_key) 

156 

157 @override 

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

159 raw_bytes = self._public_key.public_bytes( 

160 encoding=serialization.Encoding.PEM, 

161 format=serialization.PublicFormat.SubjectPublicKeyInfo, 

162 ) 

163 

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

165 

166 if bundle.verification_material.public_key.hint: 

167 key_hint = bundle.verification_material.public_key.hint 

168 if key_hint != hash_bytes: 

169 raise ValueError( 

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

171 "verification material does not match the provided " 

172 "public key. " 

173 ) 

174 else: 

175 print( 

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

177 "verification material format. Please re-sign " 

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

179 "identifier hash. " 

180 ) 

181 

182 envelope = bundle.dsse_envelope 

183 try: 

184 self._public_key.verify( 

185 envelope.signatures[0].sig, 

186 sigstore_pb.pae(envelope.payload), 

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

188 ) 

189 except exceptions.InvalidSignature: 

190 # Compatibility layer with pre 1.0 release 

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

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

193 # the material that got signed over. 

194 self._public_key.verify( 

195 envelope.signatures[0].sig, 

196 sigstore_pb.pae_compat(envelope.payload), 

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

198 ) 

199 

200 return envelope.payload_type, envelope.payload