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

68 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 

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 cryptography.hazmat.primitives.asymmetric import types as crypto_types 

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: crypto_types.PublicKeyTypes): 

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, or is not an elliptic curve one. 

48 """ 

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

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

51 

52 curve = public_key.curve.name 

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

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

55 

56 

57def get_ec_key_hash( 

58 public_key: ec.EllipticCurvePublicKey, 

59) -> hashes.HashAlgorithm: 

60 """Returns the public key hashing algorithm. 

61 

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

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

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

65 

66 Args: 

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

68 

69 Raises: 

70 ValueError: The key is not supported. 

71 """ 

72 key_size = public_key.curve.key_size 

73 

74 match key_size: 

75 case 256: 

76 return hashes.SHA256() 

77 case 384: 

78 return hashes.SHA384() 

79 case 521: 

80 return hashes.SHA512() 

81 case _: 

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

83 

84 

85class Signer(sigstore_pb.Signer): 

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

87 

88 def __init__( 

89 self, private_key_path: pathlib.Path, password: str | None = None 

90 ): 

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

92 

93 Args: 

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

95 password: Optional password for the private key. 

96 """ 

97 self._private_key = serialization.load_pem_private_key( 

98 private_key_path.read_bytes(), password 

99 ) 

100 _check_supported_ec_key(self._private_key.public_key()) 

101 

102 @override 

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

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

105 "utf-8" 

106 ) 

107 

108 raw_signature = intoto_pb.Signature( 

109 sig=base64.b64encode( 

110 self._private_key.sign( 

111 sigstore_pb.pae(raw_payload), 

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

113 ) 

114 ), 

115 keyid="", 

116 ) 

117 

118 envelope = intoto_pb.Envelope( 

119 payload=base64.b64encode(raw_payload), 

120 payload_type=signing._IN_TOTO_JSON_PAYLOAD_TYPE, 

121 signatures=[raw_signature], 

122 ) 

123 

124 return sigstore_pb.Signature( 

125 bundle_pb.Bundle( 

126 media_type=sigstore_pb._BUNDLE_MEDIA_TYPE, 

127 verification_material=self._get_verification_material(), 

128 dsse_envelope=envelope, 

129 ) 

130 ) 

131 

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

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

134 public_key = self._private_key.public_key() 

135 

136 raw_bytes = public_key.public_bytes( 

137 encoding=serialization.Encoding.PEM, 

138 format=serialization.PublicFormat.SubjectPublicKeyInfo, 

139 ) 

140 

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

142 

143 return bundle_pb.VerificationMaterial( 

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

145 tlog_entries=[], 

146 ) 

147 

148 

149class Verifier(sigstore_pb.Verifier): 

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

151 

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

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

154 

155 Args: 

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

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

158 """ 

159 self._public_key = serialization.load_pem_public_key( 

160 public_key_path.read_bytes() 

161 ) 

162 _check_supported_ec_key(self._public_key) 

163 

164 @override 

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

166 raw_bytes = self._public_key.public_bytes( 

167 encoding=serialization.Encoding.PEM, 

168 format=serialization.PublicFormat.SubjectPublicKeyInfo, 

169 ) 

170 

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

172 

173 if bundle.verification_material.public_key.hint: 

174 key_hint = bundle.verification_material.public_key.hint 

175 if key_hint != hash_bytes: 

176 raise ValueError( 

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

178 "verification material does not match the provided " 

179 "public key. " 

180 ) 

181 else: 

182 print( 

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

184 "verification material format. Please re-sign " 

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

186 "identifier hash. " 

187 ) 

188 

189 envelope = bundle.dsse_envelope 

190 try: 

191 self._public_key.verify( 

192 envelope.signatures[0].sig, 

193 sigstore_pb.pae(envelope.payload), 

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

195 ) 

196 except exceptions.InvalidSignature: 

197 # Compatibility layer with pre 1.0 release 

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

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

200 # the material that got signed over. 

201 self._public_key.verify( 

202 envelope.signatures[0].sig, 

203 sigstore_pb.pae_compat(envelope.payload), 

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

205 ) 

206 

207 return envelope.payload_type, envelope.payload