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

91 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 self._trust_chain = x509.load_pem_x509_certificates( 

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

77 ) 

78 

79 @override 

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

81 def _to_protobuf_certificate(certificate): 

82 return common_pb.X509Certificate( 

83 raw_bytes=base64.b64encode( 

84 certificate.public_bytes( 

85 encoding=serialization.Encoding.DER 

86 ) 

87 ) 

88 ) 

89 

90 chain = [_to_protobuf_certificate(self._signing_certificate)] 

91 chain.extend( 

92 [ 

93 _to_protobuf_certificate(certificate) 

94 for certificate in self._trust_chain 

95 ] 

96 ) 

97 

98 return bundle_pb.VerificationMaterial( 

99 x509_certificate_chain=common_pb.X509CertificateChain( 

100 certificates=chain 

101 ), 

102 tlog_entries=[], 

103 ) 

104 

105 

106def _log_certificate_fingerprint( 

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

108) -> None: 

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

110 

111 Args: 

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

113 certificate: Certificate to compute fingerprint of and log. 

114 hash_algorithm: The algorithm used to compute the fingerprint. 

115 """ 

116 fp = certificate.fingerprint(hash_algorithm) 

117 logger.info( 

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

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

120 ) 

121 

122 

123class Verifier(sigstore_pb.Verifier): 

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

125 

126 def __init__( 

127 self, 

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

129 log_fingerprints: bool = False, 

130 ): 

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

132 

133 Args: 

134 certificate_chain_paths: Paths to certificates used to verify 

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

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

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

138 log_fingerprints: Log the fingerprints of certificates 

139 """ 

140 self._log_fingerprints = log_fingerprints 

141 

142 if not certificate_chain_paths: 

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

144 

145 certificates = x509.load_pem_x509_certificates( 

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

147 ) 

148 

149 self._store = crypto.X509Store() 

150 for certificate in certificates: 

151 if self._log_fingerprints: 

152 _log_certificate_fingerprint( 

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

154 ) 

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

156 

157 @override 

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

159 public_key = self._verify_certificates(bundle.verification_material) 

160 envelope = bundle.dsse_envelope 

161 try: 

162 public_key.verify( 

163 envelope.signatures[0].sig, 

164 sigstore_pb.pae(envelope.payload), 

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

166 ) 

167 except exceptions.InvalidSignature: 

168 # Compatibility layer with pre 1.0 release 

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

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

171 # the material that got signed over. 

172 public_key.verify( 

173 envelope.signatures[0].sig, 

174 sigstore_pb.pae_compat(envelope.payload), 

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

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

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

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

179 # For the hardcode path see: 

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

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

182 ) 

183 

184 return envelope.payload_type, envelope.payload 

185 

186 def _verify_certificates( 

187 self, 

188 verification_material: bundle_pb.VerificationMaterial, 

189 log_fingerprints: bool = False, 

190 ) -> ec.EllipticCurvePublicKey: 

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

192 

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

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

195 from the key used during signing. 

196 """ 

197 

198 def _to_openssl_certificate(certificate_bytes, log_fingerprints): 

199 cert = x509.load_der_x509_certificate(certificate_bytes) 

200 if log_fingerprints: 

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

202 return crypto.X509.from_cryptography(cert) 

203 

204 signing_chain = verification_material.x509_certificate_chain 

205 signing_certificate = x509.load_der_x509_certificate( 

206 signing_chain.certificates[0].raw_bytes 

207 ) 

208 

209 max_signing_time = signing_certificate.not_valid_before_utc 

210 self._store.set_time(max_signing_time) 

211 

212 trust_chain_ssl = [ 

213 _to_openssl_certificate( 

214 certificate.raw_bytes, self._log_fingerprints 

215 ) 

216 for certificate in signing_chain.certificates[1:] 

217 ] 

218 signing_certificate_ssl = _to_openssl_certificate( 

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

220 ) 

221 

222 store_context = crypto.X509StoreContext( 

223 self._store, signing_certificate_ssl, trust_chain_ssl 

224 ) 

225 store_context.verify_certificate() 

226 

227 extensions = signing_certificate.extensions 

228 can_use_for_signing = False 

229 try: 

230 usage = extensions.get_extension_for_class(x509.KeyUsage) 

231 if usage.value.digital_signature: 

232 can_use_for_signing = True 

233 except x509.ExtensionNotFound: 

234 logger.warn("Certificate does not specify 'KeyUsage'.") 

235 

236 if not can_use_for_signing: 

237 try: 

238 usage = extensions.get_extension_for_class( 

239 x509.ExtendedKeyUsage 

240 ) 

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

242 can_use_for_signing = True 

243 except x509.ExtensionNotFound: 

244 logger.warn("Certificate does not specify 'ExtendedKeyUsage'.") 

245 

246 if not can_use_for_signing: 

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

248 

249 return signing_certificate.public_key()