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

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

92 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 certificates.""" 

16 

17import base64 

18from collections.abc import Iterable 

19import logging 

20import pathlib 

21 

22import certifi 

23from cryptography import exceptions 

24from cryptography import x509 

25from cryptography.hazmat.primitives import hashes 

26from cryptography.hazmat.primitives import serialization 

27from cryptography.hazmat.primitives.asymmetric import ec 

28from cryptography.x509 import oid 

29from OpenSSL import crypto 

30from sigstore_models.bundle import v1 as bundle_pb 

31from sigstore_models.common import v1 as common_pb 

32from typing_extensions import override 

33 

34from model_signing._signing import sign_ec_key as ec_key 

35from model_signing._signing import sign_sigstore_pb as sigstore_pb 

36 

37 

38logger = logging.getLogger(__name__) 

39 

40 

41class Signer(ec_key.Signer): 

42 """Signer using certificates.""" 

43 

44 def __init__( 

45 self, 

46 private_key_path: pathlib.Path, 

47 signing_certificate_path: pathlib.Path, 

48 certificate_chain_paths: Iterable[pathlib.Path], 

49 ): 

50 """Initializes the signer with the key, certificate and trust chain. 

51 

52 Args: 

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

54 signing_certificate_path: The path to the signing certificate. 

55 certificate_chain_paths: Paths to other certificates used to 

56 establish chain of trust. 

57 

58 Raises: 

59 ValueError: Signing certificate's public key does not match the 

60 private key's public pair. 

61 """ 

62 super().__init__(private_key_path) 

63 self._signing_certificate = x509.load_pem_x509_certificate( 

64 signing_certificate_path.read_bytes() 

65 ) 

66 

67 public_key_from_key = self._private_key.public_key() 

68 public_key_from_certificate = self._signing_certificate.public_key() 

69 if public_key_from_key != public_key_from_certificate: 

70 raise ValueError( 

71 "The public key from the certificate does not match " 

72 "the public key paired with the private key" 

73 ) 

74 

75 chain_bytes = b"".join( 

76 [path.read_bytes() for path in certificate_chain_paths] 

77 ) 

78 self._trust_chain = ( 

79 x509.load_pem_x509_certificates(chain_bytes) if chain_bytes else [] 

80 ) 

81 

82 @override 

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

84 def _to_protobuf_certificate(certificate): 

85 return common_pb.X509Certificate( 

86 raw_bytes=base64.b64encode( 

87 certificate.public_bytes( 

88 encoding=serialization.Encoding.DER 

89 ) 

90 ) 

91 ) 

92 

93 chain = [_to_protobuf_certificate(self._signing_certificate)] 

94 chain.extend( 

95 [ 

96 _to_protobuf_certificate(certificate) 

97 for certificate in self._trust_chain 

98 ] 

99 ) 

100 

101 return bundle_pb.VerificationMaterial( 

102 x509_certificate_chain=common_pb.X509CertificateChain( 

103 certificates=chain 

104 ), 

105 tlog_entries=[], 

106 ) 

107 

108 

109def _log_certificate_fingerprint( 

110 where: str, certificate: x509.Certificate, hash_algorithm: hashes.Hash 

111) -> None: 

112 """Log the fingerprint of a certificate, for debugging. 

113 

114 Args: 

115 where: Location of where this gets called from, useful for debugging. 

116 certificate: Certificate to compute fingerprint of and log. 

117 hash_algorithm: The algorithm used to compute the fingerprint. 

118 """ 

119 fp = certificate.fingerprint(hash_algorithm) 

120 logger.info( 

121 f"[{where:^8}] {hash_algorithm.name} " 

122 f"Fingerprint: {':'.join(f'{b:02X}' for b in fp)}" 

123 ) 

124 

125 

126class Verifier(sigstore_pb.Verifier): 

127 """Verifier for signatures generated via signing with certificates.""" 

128 

129 def __init__( 

130 self, 

131 certificate_chain_paths: Iterable[pathlib.Path] = frozenset(), 

132 log_fingerprints: bool = False, 

133 ): 

134 """Initializes the verifier with the list of certificates to use. 

135 

136 Args: 

137 certificate_chain_paths: Paths to certificates used to verify 

138 signature and establish chain of trust. By default this is empty, 

139 in which case we would use the root certificates from the 

140 operating system, as per `certifi.where()`. 

141 log_fingerprints: Log the fingerprints of certificates 

142 """ 

143 self._log_fingerprints = log_fingerprints 

144 

145 if not certificate_chain_paths: 

146 certificate_chain_paths = [pathlib.Path(certifi.where())] 

147 

148 certificates = x509.load_pem_x509_certificates( 

149 b"".join([path.read_bytes() for path in certificate_chain_paths]) 

150 ) 

151 

152 self._store = crypto.X509Store() 

153 for certificate in certificates: 

154 if self._log_fingerprints: 

155 _log_certificate_fingerprint( 

156 "init", certificate, hashes.SHA256() 

157 ) 

158 self._store.add_cert(crypto.X509.from_cryptography(certificate)) 

159 

160 @override 

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

162 public_key = self._verify_certificates(bundle.verification_material) 

163 envelope = bundle.dsse_envelope 

164 try: 

165 public_key.verify( 

166 envelope.signatures[0].sig, 

167 sigstore_pb.pae(envelope.payload), 

168 ec.ECDSA(ec_key.get_ec_key_hash(public_key)), 

169 ) 

170 except exceptions.InvalidSignature: 

171 # Compatibility layer with pre 1.0 release 

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

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

174 # the material that got signed over. 

175 public_key.verify( 

176 envelope.signatures[0].sig, 

177 sigstore_pb.pae_compat(envelope.payload), 

178 # Note another bug here: the v0.2 signatures were generated with 

179 # hardcoded SHA256 hash, instead of the one that matches the 

180 # key type. To verify those signatures, we have to hardcode this 

181 # here too (instead of `ec_key.get_ec_key_hash(public_key)`). 

182 # For the hardcode path see: 

183 # https://github.com/sigstore/model-transparency/blob/9737f0e28349bf43897857ada7beaa22ec18e9a6/src/model_signing/signature/key.py#L103 

184 ec.ECDSA(hashes.SHA256()), 

185 ) 

186 

187 return envelope.payload_type, envelope.payload 

188 

189 def _verify_certificates( 

190 self, 

191 verification_material: bundle_pb.VerificationMaterial, 

192 log_fingerprints: bool = False, 

193 ) -> ec.EllipticCurvePublicKey: 

194 """Verifies the certificate chain and returns the public key. 

195 

196 The public key is extracted from the signing certificate from the chain 

197 of trust, after the chain is validated. It must match the public key 

198 from the key used during signing. 

199 """ 

200 

201 def _to_openssl_certificate(certificate_bytes, log_fingerprints): 

202 cert = x509.load_der_x509_certificate(certificate_bytes) 

203 if log_fingerprints: 

204 _log_certificate_fingerprint("verify", cert, hashes.SHA256()) 

205 return crypto.X509.from_cryptography(cert) 

206 

207 signing_chain = verification_material.x509_certificate_chain 

208 signing_certificate = x509.load_der_x509_certificate( 

209 signing_chain.certificates[0].raw_bytes 

210 ) 

211 

212 max_signing_time = signing_certificate.not_valid_before_utc 

213 self._store.set_time(max_signing_time) 

214 

215 trust_chain_ssl = [ 

216 _to_openssl_certificate( 

217 certificate.raw_bytes, self._log_fingerprints 

218 ) 

219 for certificate in signing_chain.certificates[1:] 

220 ] 

221 signing_certificate_ssl = _to_openssl_certificate( 

222 signing_chain.certificates[0].raw_bytes, self._log_fingerprints 

223 ) 

224 

225 store_context = crypto.X509StoreContext( 

226 self._store, signing_certificate_ssl, trust_chain_ssl 

227 ) 

228 store_context.verify_certificate() 

229 

230 extensions = signing_certificate.extensions 

231 can_use_for_signing = False 

232 try: 

233 usage = extensions.get_extension_for_class(x509.KeyUsage) 

234 if usage.value.digital_signature: 

235 can_use_for_signing = True 

236 except x509.ExtensionNotFound: 

237 logger.warning("Certificate does not specify 'KeyUsage'.") 

238 

239 if not can_use_for_signing: 

240 try: 

241 usage = extensions.get_extension_for_class( 

242 x509.ExtendedKeyUsage 

243 ) 

244 if oid.ExtendedKeyUsageOID.CODE_SIGNING in usage.value: 

245 can_use_for_signing = True 

246 except x509.ExtensionNotFound: 

247 logger.warning( 

248 "Certificate does not specify 'ExtendedKeyUsage'." 

249 ) 

250 

251 if not can_use_for_signing: 

252 raise ValueError("Signing certificate cannot be used for signing") 

253 

254 return signing_certificate.public_key()