# Copyright (c) 2016-2017, Yanis Guenane # Copyright (c) 2017, Markus Teufelberger # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations import os from random import randrange from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import ( CRYPTOGRAPHY_TIMEZONE, cryptography_key_needs_digest_for_signing, cryptography_verify_certificate_signature, get_not_valid_after, get_not_valid_before, set_not_valid_after, set_not_valid_before, ) from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate import ( CertificateBackend, CertificateError, CertificateProvider, ) from ansible_collections.community.crypto.plugins.module_utils.crypto.support import ( select_message_digest, ) from ansible_collections.community.crypto.plugins.module_utils.time import ( get_relative_time_option, ) try: import cryptography from cryptography import x509 from cryptography.hazmat.primitives.serialization import Encoding except ImportError: pass class SelfSignedCertificateBackendCryptography(CertificateBackend): def __init__(self, module): super(SelfSignedCertificateBackendCryptography, self).__init__(module) self.create_subject_key_identifier = module.params[ "selfsigned_create_subject_key_identifier" ] self.notBefore = get_relative_time_option( module.params["selfsigned_not_before"], "selfsigned_not_before", with_timezone=CRYPTOGRAPHY_TIMEZONE, ) self.notAfter = get_relative_time_option( module.params["selfsigned_not_after"], "selfsigned_not_after", with_timezone=CRYPTOGRAPHY_TIMEZONE, ) self.digest = select_message_digest(module.params["selfsigned_digest"]) self.version = module.params["selfsigned_version"] self.serial_number = x509.random_serial_number() if self.csr_path is not None and not os.path.exists(self.csr_path): raise CertificateError( f"The certificate signing request file {self.csr_path} does not exist" ) if self.privatekey_content is None and not os.path.exists(self.privatekey_path): raise CertificateError( f"The private key file {self.privatekey_path} does not exist" ) self._module = module self._ensure_private_key_loaded() self._ensure_csr_loaded() if self.csr is None: # Create empty CSR on the fly csr = cryptography.x509.CertificateSigningRequestBuilder() csr = csr.subject_name(cryptography.x509.Name([])) digest = None if cryptography_key_needs_digest_for_signing(self.privatekey): digest = self.digest if digest is None: self.module.fail_json( msg=f'Unsupported digest "{module.params["selfsigned_digest"]}"' ) self.csr = csr.sign(self.privatekey, digest) if cryptography_key_needs_digest_for_signing(self.privatekey): if self.digest is None: raise CertificateError( f"The digest {module.params['selfsigned_digest']} is not supported with the cryptography backend" ) else: self.digest = None def generate_certificate(self): """(Re-)Generate certificate.""" try: cert_builder = x509.CertificateBuilder() cert_builder = cert_builder.subject_name(self.csr.subject) cert_builder = cert_builder.issuer_name(self.csr.subject) cert_builder = cert_builder.serial_number(self.serial_number) cert_builder = set_not_valid_before(cert_builder, self.notBefore) cert_builder = set_not_valid_after(cert_builder, self.notAfter) cert_builder = cert_builder.public_key(self.privatekey.public_key()) has_ski = False for extension in self.csr.extensions: if isinstance(extension.value, x509.SubjectKeyIdentifier): if self.create_subject_key_identifier == "always_create": continue has_ski = True cert_builder = cert_builder.add_extension( extension.value, critical=extension.critical ) if not has_ski and self.create_subject_key_identifier != "never_create": cert_builder = cert_builder.add_extension( x509.SubjectKeyIdentifier.from_public_key( self.privatekey.public_key() ), critical=False, ) except ValueError as e: raise CertificateError(str(e)) certificate = cert_builder.sign( private_key=self.privatekey, algorithm=self.digest, ) self.cert = certificate def get_certificate_data(self): """Return bytes for self.cert.""" return self.cert.public_bytes(Encoding.PEM) def needs_regeneration(self): if super(SelfSignedCertificateBackendCryptography, self).needs_regeneration( not_before=self.notBefore, not_after=self.notAfter ): return True self._ensure_existing_certificate_loaded() # Check whether certificate is signed by private key if not cryptography_verify_certificate_signature( self.existing_certificate, self.privatekey.public_key() ): return True return False def dump(self, include_certificate): result = super(SelfSignedCertificateBackendCryptography, self).dump( include_certificate ) if self.module.check_mode: result.update( { "notBefore": self.notBefore.strftime("%Y%m%d%H%M%SZ"), "notAfter": self.notAfter.strftime("%Y%m%d%H%M%SZ"), "serial_number": self.serial_number, } ) else: if self.cert is None: self.cert = self.existing_certificate result.update( { "notBefore": get_not_valid_before(self.cert).strftime( "%Y%m%d%H%M%SZ" ), "notAfter": get_not_valid_after(self.cert).strftime( "%Y%m%d%H%M%SZ" ), "serial_number": self.cert.serial_number, } ) return result def generate_serial_number(): """Generate a serial number for a certificate""" while True: result = randrange(0, 1 << 160) if result >= 1000: return result class SelfSignedCertificateProvider(CertificateProvider): def validate_module_args(self, module): if ( module.params["privatekey_path"] is None and module.params["privatekey_content"] is None ): module.fail_json( msg="One of privatekey_path and privatekey_content must be specified for the selfsigned provider." ) def needs_version_two_certs(self, module): return module.params["selfsigned_version"] == 2 def create_backend(self, module): return SelfSignedCertificateBackendCryptography(module) def add_selfsigned_provider_to_argument_spec(argument_spec): argument_spec.argument_spec["provider"]["choices"].append("selfsigned") argument_spec.argument_spec.update( dict( selfsigned_version=dict(type="int", default=3), selfsigned_digest=dict(type="str", default="sha256"), selfsigned_not_before=dict( type="str", default="+0s", aliases=["selfsigned_notBefore"] ), selfsigned_not_after=dict( type="str", default="+3650d", aliases=["selfsigned_notAfter"] ), selfsigned_create_subject_key_identifier=dict( type="str", default="create_if_not_provided", choices=["create_if_not_provided", "always_create", "never_create"], ), ) )