mirror of
https://github.com/ansible-collections/community.crypto.git
synced 2026-05-06 13:22:58 +00:00
Refactor x509_certificate module, add x509_certificate_pipe module (#135)
* Move documentation to doc fragment. * Prepare module backends. * Linting. * Fix comments. * First shot at actually moving code. * Forgot SKI check. * Remove unused imports. * Improve check mode. * Fix 'returned'. * Move csr_* checks. * Explicitly specify parameter. * Add x509_certificate_pipe module. * Update other seealsos. * Forgot to remove doc fragment. * Adjust to work with macOS 10.15. * Update plugins/module_utils/crypto/module_backends/certificate_entrust.py Co-authored-by: Chris Trufan <31186388+ctrufan@users.noreply.github.com> * Add changelog fragments for entrust bugfix and module refactorings. * Restore old behavior of Entrust backend when existing certificate cannot be parsed. * Update plugins/modules/x509_certificate_pipe.py Co-authored-by: Chris Trufan <31186388+ctrufan@users.noreply.github.com> * Remove Entrust provider from x509_certificate_pipe for now. * Add own CA tests. * One more fix for Entrust provider, when csr_content is used. * Update plugins/modules/x509_certificate_pipe.py Co-authored-by: Chris Trufan <31186388+ctrufan@users.noreply.github.com> * Fix another broken example. * Revert "Remove Entrust provider from x509_certificate_pipe for now." This reverts commit 6ee5d7d4f99f0fe2218276a2d3f1f38b676c29b9. * ci_complete * Apply suggestions from code review Co-authored-by: MarkusTeufelberger <mteufelberger@mgit.at> * Improve example. * Improve readability of example, add another one. * Extend descriptions of csr_* for selfsigned. * Improve documentation. * Move deprecation message up. * Explain empty choices. Co-authored-by: Chris Trufan <31186388+ctrufan@users.noreply.github.com> Co-authored-by: MarkusTeufelberger <mteufelberger@mgit.at>
This commit is contained in:
@@ -0,0 +1,263 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org>
|
||||
# Copyright: (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
import os
|
||||
|
||||
from random import randrange
|
||||
|
||||
from ansible.module_utils._text import to_bytes
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
|
||||
get_relative_time_option,
|
||||
select_message_digest,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
|
||||
cryptography_key_needs_digest_for_signing,
|
||||
cryptography_serial_number_of_cert,
|
||||
)
|
||||
|
||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate import (
|
||||
CertificateError,
|
||||
CertificateBackend,
|
||||
CertificateProvider,
|
||||
)
|
||||
|
||||
try:
|
||||
from OpenSSL import crypto
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
try:
|
||||
import cryptography
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives.serialization import Encoding
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
class SelfSignedCertificateBackendCryptography(CertificateBackend):
|
||||
def __init__(self, module):
|
||||
super(SelfSignedCertificateBackendCryptography, self).__init__(module, 'cryptography')
|
||||
|
||||
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', backend=self.backend)
|
||||
self.notAfter = get_relative_time_option(module.params['selfsigned_not_after'], 'selfsigned_not_after', backend=self.backend)
|
||||
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(
|
||||
'The certificate signing request file {0} does not exist'.format(self.csr_path)
|
||||
)
|
||||
if self.privatekey_content is None and not os.path.exists(self.privatekey_path):
|
||||
raise CertificateError(
|
||||
'The private key file {0} does not exist'.format(self.privatekey_path)
|
||||
)
|
||||
|
||||
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='Unsupported digest "{0}"'.format(module.params['selfsigned_digest']))
|
||||
try:
|
||||
self.csr = csr.sign(self.privatekey, digest, default_backend())
|
||||
except TypeError as e:
|
||||
if str(e) == 'Algorithm must be a registered hash algorithm.' and digest is None:
|
||||
self.module.fail_json(msg='Signing with Ed25519 and Ed448 keys requires cryptography 2.8 or newer.')
|
||||
raise
|
||||
|
||||
if cryptography_key_needs_digest_for_signing(self.privatekey):
|
||||
if self.digest is None:
|
||||
raise CertificateError(
|
||||
'The digest %s is not supported with the cryptography backend' % module.params['selfsigned_digest']
|
||||
)
|
||||
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 = cert_builder.not_valid_before(self.notBefore)
|
||||
cert_builder = cert_builder.not_valid_after(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))
|
||||
|
||||
try:
|
||||
certificate = cert_builder.sign(
|
||||
private_key=self.privatekey, algorithm=self.digest,
|
||||
backend=default_backend()
|
||||
)
|
||||
except TypeError as e:
|
||||
if str(e) == 'Algorithm must be a registered hash algorithm.' and self.digest is None:
|
||||
self.module.fail_json(msg='Signing with Ed25519 and Ed448 keys requires cryptography 2.8 or newer.')
|
||||
raise
|
||||
|
||||
self.cert = certificate
|
||||
|
||||
def get_certificate_data(self):
|
||||
"""Return bytes for self.cert."""
|
||||
return self.cert.public_bytes(Encoding.PEM)
|
||||
|
||||
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': self.cert.not_valid_before.strftime("%Y%m%d%H%M%SZ"),
|
||||
'notAfter': self.cert.not_valid_after.strftime("%Y%m%d%H%M%SZ"),
|
||||
'serial_number': cryptography_serial_number_of_cert(self.cert),
|
||||
})
|
||||
|
||||
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 SelfSignedCertificateBackendPyOpenSSL(CertificateBackend):
|
||||
def __init__(self, module):
|
||||
super(SelfSignedCertificateBackendPyOpenSSL, self).__init__(module, 'pyopenssl')
|
||||
|
||||
if module.params['selfsigned_create_subject_key_identifier'] != 'create_if_not_provided':
|
||||
module.fail_json(msg='selfsigned_create_subject_key_identifier cannot be used with the pyOpenSSL backend!')
|
||||
self.notBefore = get_relative_time_option(module.params['selfsigned_not_before'], 'selfsigned_not_before', backend=self.backend)
|
||||
self.notAfter = get_relative_time_option(module.params['selfsigned_not_after'], 'selfsigned_not_after', backend=self.backend)
|
||||
self.digest = module.params['selfsigned_digest']
|
||||
self.version = module.params['selfsigned_version']
|
||||
self.serial_number = generate_serial_number()
|
||||
|
||||
if self.csr_path is not None and not os.path.exists(self.csr_path):
|
||||
raise CertificateError(
|
||||
'The certificate signing request file {0} does not exist'.format(self.csr_path)
|
||||
)
|
||||
if self.privatekey_content is None and not os.path.exists(self.privatekey_path):
|
||||
raise CertificateError(
|
||||
'The private key file {0} does not exist'.format(self.privatekey_path)
|
||||
)
|
||||
|
||||
self._ensure_private_key_loaded()
|
||||
|
||||
self._ensure_csr_loaded()
|
||||
if self.csr is None:
|
||||
# Create empty CSR on the fly
|
||||
self.csr = crypto.X509Req()
|
||||
self.csr.set_pubkey(self.privatekey)
|
||||
self.csr.sign(self.privatekey, self.digest)
|
||||
|
||||
def generate_certificate(self):
|
||||
"""(Re-)Generate certificate."""
|
||||
cert = crypto.X509()
|
||||
cert.set_serial_number(self.serial_number)
|
||||
cert.set_notBefore(to_bytes(self.notBefore))
|
||||
cert.set_notAfter(to_bytes(self.notAfter))
|
||||
cert.set_subject(self.csr.get_subject())
|
||||
cert.set_issuer(self.csr.get_subject())
|
||||
cert.set_version(self.version - 1)
|
||||
cert.set_pubkey(self.csr.get_pubkey())
|
||||
cert.add_extensions(self.csr.get_extensions())
|
||||
|
||||
cert.sign(self.privatekey, self.digest)
|
||||
self.cert = cert
|
||||
|
||||
def get_certificate_data(self):
|
||||
"""Return bytes for self.cert."""
|
||||
return crypto.dump_certificate(crypto.FILETYPE_PEM, self.cert)
|
||||
|
||||
def dump(self, include_certificate):
|
||||
result = super(SelfSignedCertificateBackendPyOpenSSL, self).dump(include_certificate)
|
||||
|
||||
if self.module.check_mode:
|
||||
result.update({
|
||||
'notBefore': self.notBefore,
|
||||
'notAfter': self.notAfter,
|
||||
'serial_number': self.serial_number,
|
||||
})
|
||||
else:
|
||||
if self.cert is None:
|
||||
self.cert = self.existing_certificate
|
||||
result.update({
|
||||
'notBefore': self.cert.get_notBefore(),
|
||||
'notAfter': self.cert.get_notAfter(),
|
||||
'serial_number': self.cert.get_serial_number(),
|
||||
})
|
||||
|
||||
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, backend):
|
||||
if backend == 'cryptography':
|
||||
return SelfSignedCertificateBackendCryptography(module)
|
||||
if backend == 'pyopenssl':
|
||||
return SelfSignedCertificateBackendPyOpenSSL(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']
|
||||
),
|
||||
))
|
||||
Reference in New Issue
Block a user