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

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

90 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 

17from collections.abc import Iterable 

18import logging 

19import pathlib 

20 

21import certifi 

22from cryptography import exceptions 

23from cryptography import x509 

24from cryptography.hazmat.primitives import hashes 

25from cryptography.hazmat.primitives import serialization 

26from cryptography.hazmat.primitives.asymmetric import ec 

27from cryptography.x509 import oid 

28from OpenSSL import crypto 

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

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

31from typing_extensions import override 

32 

33from model_signing._signing import sign_ec_key as ec_key 

34from model_signing._signing import sign_sigstore_pb as sigstore_pb 

35 

36 

37logger = logging.getLogger(__name__) 

38 

39 

40class Signer(ec_key.Signer): 

41 """Signer using certificates.""" 

42 

43 def __init__( 

44 self, 

45 private_key_path: pathlib.Path, 

46 signing_certificate_path: pathlib.Path, 

47 certificate_chain_paths: Iterable[pathlib.Path], 

48 ): 

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

50 

51 Args: 

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

53 signing_certificate_path: The path to the signing certificate. 

54 certificate_chain_paths: Paths to other certificates used to 

55 establish chain of trust. 

56 

57 Raises: 

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

59 private key's public pair. 

60 """ 

61 super().__init__(private_key_path) 

62 self._signing_certificate = x509.load_pem_x509_certificate( 

63 signing_certificate_path.read_bytes() 

64 ) 

65 

66 public_key_from_key = self._private_key.public_key() 

67 public_key_from_certificate = self._signing_certificate.public_key() 

68 if public_key_from_key != public_key_from_certificate: 

69 raise ValueError( 

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

71 "the public key paired with the private key" 

72 ) 

73 

74 self._trust_chain = x509.load_pem_x509_certificates( 

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

76 ) 

77 

78 @override 

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

80 def _to_protobuf_certificate(certificate): 

81 return common_pb.X509Certificate( 

82 raw_bytes=certificate.public_bytes( 

83 encoding=serialization.Encoding.DER 

84 ) 

85 ) 

86 

87 chain = [_to_protobuf_certificate(self._signing_certificate)] 

88 chain.extend( 

89 [ 

90 _to_protobuf_certificate(certificate) 

91 for certificate in self._trust_chain 

92 ] 

93 ) 

94 

95 return bundle_pb.VerificationMaterial( 

96 x509_certificate_chain=common_pb.X509CertificateChain( 

97 certificates=chain 

98 ) 

99 ) 

100 

101 

102def _log_certificate_fingerprint( 

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

104) -> None: 

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

106 

107 Args: 

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

109 certificate: Certificate to compute fingerprint of and log. 

110 hash_algorithm: The algorithm used to compute the fingerprint. 

111 """ 

112 fp = certificate.fingerprint(hash_algorithm) 

113 logger.info( 

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

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

116 ) 

117 

118 

119class Verifier(sigstore_pb.Verifier): 

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

121 

122 def __init__( 

123 self, 

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

125 log_fingerprints: bool = False, 

126 ): 

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

128 

129 Args: 

130 certificate_chain_paths: Paths to certificates used to verify 

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

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

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

134 log_fingerprints: Log the fingerprints of certificates 

135 """ 

136 self._log_fingerprints = log_fingerprints 

137 

138 if not certificate_chain_paths: 

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

140 

141 certificates = x509.load_pem_x509_certificates( 

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

143 ) 

144 

145 self._store = crypto.X509Store() 

146 for certificate in certificates: 

147 if self._log_fingerprints: 

148 _log_certificate_fingerprint( 

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

150 ) 

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

152 

153 @override 

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

155 public_key = self._verify_certificates(bundle.verification_material) 

156 envelope = bundle.dsse_envelope 

157 try: 

158 public_key.verify( 

159 envelope.signatures[0].sig, 

160 sigstore_pb.pae(envelope.payload), 

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

162 ) 

163 except exceptions.InvalidSignature: 

164 # Compatibility layer with pre 1.0 release 

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

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

167 # the material that got signed over. 

168 public_key.verify( 

169 envelope.signatures[0].sig, 

170 sigstore_pb.pae_compat(envelope.payload), 

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

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

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

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

175 # For the hardcode path see: 

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

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

178 ) 

179 

180 return envelope.payload_type, envelope.payload 

181 

182 def _verify_certificates( 

183 self, 

184 verification_material: bundle_pb.VerificationMaterial, 

185 log_fingerprints: bool = False, 

186 ) -> ec.EllipticCurvePublicKey: 

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

188 

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

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

191 from the key used during signing. 

192 """ 

193 

194 def _to_openssl_certificate(certificate_bytes, log_fingerprints): 

195 cert = x509.load_der_x509_certificate(certificate_bytes) 

196 if log_fingerprints: 

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

198 return crypto.X509.from_cryptography(cert) 

199 

200 signing_chain = verification_material.x509_certificate_chain 

201 signing_certificate = x509.load_der_x509_certificate( 

202 signing_chain.certificates[0].raw_bytes 

203 ) 

204 

205 max_signing_time = signing_certificate.not_valid_before_utc 

206 self._store.set_time(max_signing_time) 

207 

208 trust_chain_ssl = [ 

209 _to_openssl_certificate( 

210 certificate.raw_bytes, self._log_fingerprints 

211 ) 

212 for certificate in signing_chain.certificates[1:] 

213 ] 

214 signing_certificate_ssl = _to_openssl_certificate( 

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

216 ) 

217 

218 store_context = crypto.X509StoreContext( 

219 self._store, signing_certificate_ssl, trust_chain_ssl 

220 ) 

221 store_context.verify_certificate() 

222 

223 extensions = signing_certificate.extensions 

224 can_use_for_signing = False 

225 try: 

226 usage = extensions.get_extension_for_class(x509.KeyUsage) 

227 if usage.value.digital_signature: 

228 can_use_for_signing = True 

229 except x509.ExtensionNotFound: 

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

231 

232 if not can_use_for_signing: 

233 try: 

234 usage = extensions.get_extension_for_class( 

235 x509.ExtendedKeyUsage 

236 ) 

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

238 can_use_for_signing = True 

239 except x509.ExtensionNotFound: 

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

241 

242 if not can_use_for_signing: 

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

244 

245 return signing_certificate.public_key()