From fa70690e5ca447f9bc52e24dea75100857dc13e5 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Fri, 23 Aug 2019 14:01:42 +0200 Subject: [PATCH] openssl_certificate/csr(_info): add support for SubjectKeyIdentifier and AuthorityKeyIdentifier (#60741) * Add support for SubjectKeyIdentifier and AuthorityKeyIdentifier to _info modules. * Adding SubjectKeyIdentifier and AuthorityKeyIdentifier support to openssl_certificate and openssl_csr. * Fix type of authority_cert_issuer. * Add basic tests. * Add changelog. * Added proper tests for _info modules. * Fix docs bug. * Make sure new features are only used when cryptography backend for openssl_csr is available. * Work around jinja2 being too old on some CI hosts. * Add tests for openssl_csr. * Add openssl_certificate tests. * Fix idempotence test. * Move one level up. * Add ownca_create_authority_key_identifier option. * Add ownca_create_authority_key_identifier option. * Add idempotency check. * Apparently the function call expected different args for cryptography < 2.7. * Fix copy'n'paste errors and typos. * string -> general name. * Add disclaimer. * Implement always_create / create_if_not_provided / never_create for openssl_certificate. * Update changelog and porting guide. * Add comments for defaults. --- .../60741-openssl-cert-csr-key-identifier.yml | 6 + .../rst/porting_guides/porting_guide_2.9.rst | 2 + .../modules/crypto/openssl_certificate.py | 166 +++++++++++++- .../crypto/openssl_certificate_info.py | 83 +++++++ lib/ansible/modules/crypto/openssl_csr.py | 142 +++++++++++- .../modules/crypto/openssl_csr_info.py | 83 +++++++ .../openssl_certificate/tasks/ownca.yml | 130 +++++++++++ .../openssl_certificate/tasks/selfsigned.yml | 60 ++++++ .../tests/validate_ownca.yml | 20 ++ .../tests/validate_selfsigned.yml | 10 + .../openssl_certificate_info/tasks/impl.yml | 33 +++ .../openssl_certificate_info/tasks/main.yml | 24 ++- .../test_plugins/jinja_compatibility.py | 15 ++ .../targets/openssl_csr/tasks/impl.yml | 203 +++++++++++++++++- .../targets/openssl_csr/tasks/main.yml | 4 + .../targets/openssl_csr/tests/validate.yml | 30 +++ .../targets/openssl_csr_info/tasks/impl.yml | 33 +++ .../targets/openssl_csr_info/tasks/main.yml | 24 ++- .../test_plugins/jinja_compatibility.py | 15 ++ 19 files changed, 1074 insertions(+), 9 deletions(-) create mode 100644 changelogs/fragments/60741-openssl-cert-csr-key-identifier.yml create mode 100644 test/integration/targets/openssl_certificate_info/test_plugins/jinja_compatibility.py create mode 100644 test/integration/targets/openssl_csr_info/test_plugins/jinja_compatibility.py diff --git a/changelogs/fragments/60741-openssl-cert-csr-key-identifier.yml b/changelogs/fragments/60741-openssl-cert-csr-key-identifier.yml new file mode 100644 index 00000000000..8d20b711a02 --- /dev/null +++ b/changelogs/fragments/60741-openssl-cert-csr-key-identifier.yml @@ -0,0 +1,6 @@ +minor_changes: +- "openssl_certificate - add support for subject key identifier and authority key identifier extensions. Subject key identifiers are created by default when not explicitly disabled." +- "openssl_certificate - the ``ownca`` provider creates authority key identifiers if not explicitly disabled with ``ownca_create_authority_key_identifier: no``." +- "openssl_certificate_info - add support for subject key identifier and authority key identifier extensions." +- "openssl_csr - add support for subject key identifier and authority key identifier extensions." +- "openssl_csr_info - add support for subject key identifier and authority key identifier extensions." diff --git a/docs/docsite/rst/porting_guides/porting_guide_2.9.rst b/docs/docsite/rst/porting_guides/porting_guide_2.9.rst index cf9760e41eb..0f9450125b0 100644 --- a/docs/docsite/rst/porting_guides/porting_guide_2.9.rst +++ b/docs/docsite/rst/porting_guides/porting_guide_2.9.rst @@ -306,6 +306,8 @@ Noteworthy module changes * `mysql_db ` returns new `db_list` parameter in addition to `db` parameter. This `db_list` parameter refers to list of database names. `db` parameter will be deprecated in version `2.13`. * `snow_record ` and `snow_record_find ` now takes environment variables for `instance`, `username` and `password` parameters. This change marks these parameters as optional. * The deprecated ``force`` option in ``win_firewall_rule`` has been removed. +* :ref:`openssl_certificate `'s ``ownca`` provider creates authority key identifiers if not explicitly disabled with ``ownca_create_authority_key_identifier: no``. This is only the case for the ``cryptography`` backend, which is selected by default if the ``cryptography`` library is available. +* :ref:`openssl_certificate `'s ``ownca`` and ``selfsigned`` providers create subject key identifiers if not explicitly disabled with ``ownca_create_subject_key_identifier: never_create`` resp. ``selfsigned_create_subject_key_identifier: never_create``. If a subject key identifier is provided by the CSR, it is taken; if not, it is created from the public key. This is only the case for the ``cryptography`` backend, which is selected by default if the ``cryptography`` library is available. Plugins diff --git a/lib/ansible/modules/crypto/openssl_certificate.py b/lib/ansible/modules/crypto/openssl_certificate.py index b2499aadd7e..3b12cdbe5b7 100644 --- a/lib/ansible/modules/crypto/openssl_certificate.py +++ b/lib/ansible/modules/crypto/openssl_certificate.py @@ -138,6 +138,21 @@ options: default: +3650d aliases: [ selfsigned_notAfter ] + selfsigned_create_subject_key_identifier: + description: + - Whether to create the Subject Key Identifier (SKI) from the public key. + - A value of C(create_if_not_provided) (default) only creates a SKI when the CSR does not + provide one. + - A value of C(always_create) always creates a SKI. If the CSR provides one, that one is + ignored. + - A value of C(never_create) never creates a SKI. If the CSR provides one, that one is used. + - This is only used by the C(selfsigned) provider. + - Note that this is only supported if the C(cryptography) backend is used! + type: str + choices: [create_if_not_provided, always_create, never_create] + default: create_if_not_provided + version_added: "2.9" + ownca_path: description: - Remote absolute path of the CA (Certificate Authority) certificate. @@ -204,6 +219,33 @@ options: default: +3650d version_added: "2.7" + ownca_create_subject_key_identifier: + description: + - Whether to create the Subject Key Identifier (SKI) from the public key. + - A value of C(create_if_not_provided) (default) only creates a SKI when the CSR does not + provide one. + - A value of C(always_create) always creates a SKI. If the CSR provides one, that one is + ignored. + - A value of C(never_create) never creates a SKI. If the CSR provides one, that one is used. + - This is only used by the C(ownca) provider. + - Note that this is only supported if the C(cryptography) backend is used! + type: str + choices: [create_if_not_provided, always_create, never_create] + default: create_if_not_provided + version_added: "2.9" + + ownca_create_authority_key_identifier: + description: + - Create a Authority Key Identifier from the CA's certificate. If the CSR provided + a authority key identifier, it is ignored. + - The Authority Key Identifier is generated from the CA certificate's Subject Key Identifier, + if available. If it is not available, the CA certificate's public key will be used. + - This is only used by the C(ownca) provider. + - Note that this is only supported if the C(cryptography) backend is used! + type: bool + default: yes + version_added: "2.9" + acme_accountkey_path: description: - The path to the accountkey for the C(acme) provider. @@ -843,6 +885,11 @@ class Certificate(crypto_utils.OpenSSLObject): self.backend = backend self.module = module + # The following are default values which make sure check() works as + # before if providers do not explicitly change these properties. + self.create_subject_key_identifier = 'never_create' + self.create_authority_key_identifier = False + self.backup = module.params['backup'] self.backup_file = None @@ -918,13 +965,21 @@ class Certificate(crypto_utils.OpenSSLObject): if self.csr.subject != self.cert.subject: return False # Check extensions - cert_exts = self.cert.extensions - csr_exts = self.csr.extensions + cert_exts = list(self.cert.extensions) + csr_exts = list(self.csr.extensions) + if self.create_subject_key_identifier != 'never_create': + # Filter out SubjectKeyIdentifier extension before comparison + cert_exts = list(filter(lambda x: not isinstance(x.value, x509.SubjectKeyIdentifier), cert_exts)) + csr_exts = list(filter(lambda x: not isinstance(x.value, x509.SubjectKeyIdentifier), csr_exts)) + if self.create_authority_key_identifier: + # Filter out AuthorityKeyIdentifier extension before comparison + cert_exts = list(filter(lambda x: not isinstance(x.value, x509.AuthorityKeyIdentifier), cert_exts)) + csr_exts = list(filter(lambda x: not isinstance(x.value, x509.AuthorityKeyIdentifier), csr_exts)) if len(cert_exts) != len(csr_exts): return False for cert_ext in cert_exts: try: - csr_ext = csr_exts.get_extension_for_oid(cert_ext.oid) + csr_ext = self.csr.extensions.get_extension_for_oid(cert_ext.oid) if cert_ext != csr_ext: return False except cryptography.x509.ExtensionNotFound as dummy: @@ -966,6 +1021,29 @@ class Certificate(crypto_utils.OpenSSLObject): if not self._validate_csr(): return False + # Check SubjectKeyIdentifier + if self.backend == 'cryptography' and self.create_subject_key_identifier != 'never_create': + # Get hold of certificate's SKI + try: + ext = self.cert.extensions.get_extension_for_class(x509.SubjectKeyIdentifier) + except cryptography.x509.ExtensionNotFound as dummy: + return False + # Get hold of CSR's SKI for 'create_if_not_provided' + csr_ext = None + if self.create_subject_key_identifier == 'create_if_not_provided': + try: + csr_ext = self.csr.extensions.get_extension_for_class(x509.SubjectKeyIdentifier) + except cryptography.x509.ExtensionNotFound as dummy: + pass + if csr_ext is None: + # If CSR had no SKI, or we chose to ignore it ('always_create'), compare with created SKI + if ext.value.digest != x509.SubjectKeyIdentifier.from_public_key(self.cert.public_key()).digest: + return False + else: + # If CSR had SKI and we didn't ignore it ('create_if_not_provided'), compare SKIs + if ext.value.digest != csr_ext.value.digest: + return False + return True @@ -995,6 +1073,7 @@ class SelfSignedCertificateCryptography(Certificate): """Generate the self-signed certificate, using the cryptography backend""" def __init__(self, module): super(SelfSignedCertificateCryptography, self).__init__(module, 'cryptography') + self.create_subject_key_identifier = module.params['selfsigned_create_subject_key_identifier'] self.notBefore = self.get_relative_time_option(module.params['selfsigned_not_before'], 'selfsigned_not_before') self.notAfter = self.get_relative_time_option(module.params['selfsigned_not_after'], 'selfsigned_not_after') self.digest = crypto_utils.select_message_digest(module.params['selfsigned_digest']) @@ -1043,8 +1122,18 @@ class SelfSignedCertificateCryptography(Certificate): 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)) @@ -1098,6 +1187,8 @@ class SelfSignedCertificate(Certificate): def __init__(self, module): super(SelfSignedCertificate, 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 = self.get_relative_time_option(module.params['selfsigned_not_before'], 'selfsigned_not_before') self.notAfter = self.get_relative_time_option(module.params['selfsigned_not_after'], 'selfsigned_not_after') self.digest = module.params['selfsigned_digest'] @@ -1187,6 +1278,8 @@ class OwnCACertificateCryptography(Certificate): """Generate the own CA certificate. Using the cryptography backend""" def __init__(self, module): super(OwnCACertificateCryptography, self).__init__(module, 'cryptography') + self.create_subject_key_identifier = module.params['ownca_create_subject_key_identifier'] + self.create_authority_key_identifier = module.params['ownca_create_authority_key_identifier'] self.notBefore = self.get_relative_time_option(module.params['ownca_not_before'], 'ownca_not_before') self.notAfter = self.get_relative_time_option(module.params['ownca_not_after'], 'ownca_not_after') self.digest = crypto_utils.select_message_digest(module.params['ownca_digest']) @@ -1243,8 +1336,34 @@ class OwnCACertificateCryptography(Certificate): 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.csr.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 + if self.create_authority_key_identifier and isinstance(extension.value, x509.AuthorityKeyIdentifier): + continue 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.csr.public_key()), + critical=False + ) + if self.create_authority_key_identifier: + try: + ext = self.ca_cert.extensions.get_extension_for_class(x509.SubjectKeyIdentifier) + cert_builder = cert_builder.add_extension( + x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier(ext.value) + if CRYPTOGRAPHY_VERSION >= LooseVersion('2.7') else + x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier(ext), + critical=False + ) + except cryptography.x509.ExtensionNotFound: + cert_builder = cert_builder.add_extension( + x509.AuthorityKeyIdentifier.from_issuer_public_key(self.ca_cert.public_key()), + critical=False + ) certificate = cert_builder.sign( private_key=self.ca_private_key, algorithm=self.digest, @@ -1264,6 +1383,32 @@ class OwnCACertificateCryptography(Certificate): if module.set_fs_attributes_if_different(file_args, False): self.changed = True + def check(self, module, perms_required=True): + """Ensure the resource is in its desired state.""" + + if not super(OwnCACertificateCryptography, self).check(module, perms_required): + return False + + # Check AuthorityKeyIdentifier + if self.create_authority_key_identifier: + try: + ext = self.ca_cert.extensions.get_extension_for_class(x509.SubjectKeyIdentifier) + expected_ext = ( + x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier(ext.value) + if CRYPTOGRAPHY_VERSION >= LooseVersion('2.7') else + x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier(ext) + ) + except cryptography.x509.ExtensionNotFound: + expected_ext = x509.AuthorityKeyIdentifier.from_issuer_public_key(self.ca_cert.public_key()) + try: + ext = self.cert.extensions.get_extension_for_class(x509.AuthorityKeyIdentifier) + if ext.value != expected_ext: + return False + except cryptography.x509.ExtensionNotFound as dummy: + return False + + return True + def dump(self, check_mode=False): result = { @@ -1303,6 +1448,10 @@ class OwnCACertificate(Certificate): self.digest = module.params['ownca_digest'] self.version = module.params['ownca_version'] self.serial_number = randint(1000, 99999) + if module.params['ownca_create_subject_key_identifier'] != 'create_if_not_provided': + module.fail_json(msg='ownca_create_subject_key_identifier cannot be used with the pyOpenSSL backend!') + if module.params['ownca_create_authority_key_identifier']: + module.warn('ownca_create_authority_key_identifier is ignored by the pyOpenSSL backend!') self.ca_cert_path = module.params['ownca_path'] self.ca_privatekey_path = module.params['ownca_privatekey_path'] self.ca_privatekey_passphrase = module.params['ownca_privatekey_passphrase'] @@ -2268,6 +2417,11 @@ def main(): 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'] + ), # provider: ownca ownca_path=dict(type='path'), @@ -2277,6 +2431,12 @@ def main(): ownca_version=dict(type='int', default=3), ownca_not_before=dict(type='str', default='+0s'), ownca_not_after=dict(type='str', default='+3650d'), + ownca_create_subject_key_identifier=dict( + type='str', + default='create_if_not_provided', + choices=['create_if_not_provided', 'always_create', 'never_create'] + ), + ownca_create_authority_key_identifier=dict(type='bool', default=True), # provider: acme acme_accountkey_path=dict(type='path'), diff --git a/lib/ansible/modules/crypto/openssl_certificate_info.py b/lib/ansible/modules/crypto/openssl_certificate_info.py index 9a1a77e6d17..a4e48fee5de 100644 --- a/lib/ansible/modules/crypto/openssl_certificate_info.py +++ b/lib/ansible/modules/crypto/openssl_certificate_info.py @@ -238,10 +238,45 @@ valid_at: or not. returned: success type: dict +subject_key_identifier: + description: + - The certificate's subject key identifier. + - The identifier is returned in hexadecimal, with C(:) used to separate bytes. + - Is C(none) if the C(SubjectKeyIdentifier) extension is not present. + returned: success and if the pyOpenSSL backend is I(not) used + type: str + sample: '00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33' + version_added: "2.9" +authority_key_identifier: + description: + - The certificate's authority key identifier. + - The identifier is returned in hexadecimal, with C(:) used to separate bytes. + - Is C(none) if the C(AuthorityKeyIdentifier) extension is not present. + returned: success and if the pyOpenSSL backend is I(not) used + type: str + sample: '00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33' + version_added: "2.9" +authority_cert_issuer: + description: + - The certificate's authority cert issuer as a list of general names. + - Is C(none) if the C(AuthorityKeyIdentifier) extension is not present. + returned: success and if the pyOpenSSL backend is I(not) used + type: list + sample: "[DNS:www.ansible.com, IP:1.2.3.4]" + version_added: "2.9" +authority_cert_serial_number: + description: + - The certificate's authority cert serial number. + - Is C(none) if the C(AuthorityKeyIdentifier) extension is not present. + returned: success and if the pyOpenSSL backend is I(not) used + type: int + sample: '12345' + version_added: "2.9" ''' import abc +import binascii import datetime import os import traceback @@ -392,6 +427,14 @@ class CertificateInfo(crypto_utils.OpenSSLObject): def _get_public_key(self, binary): pass + @abc.abstractmethod + def _get_subject_key_identifier(self): + pass + + @abc.abstractmethod + def _get_authority_key_identifier(self): + pass + @abc.abstractmethod def _get_serial_number(self): pass @@ -437,6 +480,21 @@ class CertificateInfo(crypto_utils.OpenSSLObject): pk = self._get_public_key(binary=True) result['public_key_fingerprints'] = crypto_utils.get_fingerprint_of_bytes(pk) if pk is not None else dict() + if self.backend != 'pyopenssl': + ski = self._get_subject_key_identifier() + if ski is not None: + ski = to_native(binascii.hexlify(ski)) + ski = ':'.join([ski[i:i + 2] for i in range(0, len(ski), 2)]) + result['subject_key_identifier'] = ski + + aki, aci, acsn = self._get_authority_key_identifier() + if aki is not None: + aki = to_native(binascii.hexlify(aki)) + aki = ':'.join([aki[i:i + 2] for i in range(0, len(aki), 2)]) + result['authority_key_identifier'] = aki + result['authority_cert_issuer'] = aci + result['authority_cert_serial_number'] = acsn + result['serial_number'] = self._get_serial_number() result['extensions_by_oid'] = self._get_all_extensions() @@ -563,6 +621,23 @@ class CertificateInfoCryptography(CertificateInfo): serialization.PublicFormat.SubjectPublicKeyInfo ) + def _get_subject_key_identifier(self): + try: + ext = self.cert.extensions.get_extension_for_class(x509.SubjectKeyIdentifier) + return ext.value.digest + except cryptography.x509.ExtensionNotFound: + return None + + def _get_authority_key_identifier(self): + try: + ext = self.cert.extensions.get_extension_for_class(x509.AuthorityKeyIdentifier) + issuer = None + if ext.value.authority_cert_issuer is not None: + issuer = [crypto_utils.cryptography_decode_name(san) for san in ext.value.authority_cert_issuer] + return ext.value.key_identifier, issuer, ext.value.authority_cert_serial_number + except cryptography.x509.ExtensionNotFound: + return None, None, None + def _get_serial_number(self): return self.cert.serial_number @@ -675,6 +750,14 @@ class CertificateInfoPyOpenSSL(CertificateInfo): self.module.warn('Your pyOpenSSL version does not support dumping public keys. ' 'Please upgrade to version 16.0 or newer, or use the cryptography backend.') + def _get_subject_key_identifier(self): + # Won't be implemented + return None + + def _get_authority_key_identifier(self): + # Won't be implemented + return None, None, None + def _get_serial_number(self): return self.cert.get_serial_number() diff --git a/lib/ansible/modules/crypto/openssl_csr.py b/lib/ansible/modules/crypto/openssl_csr.py index dc2fa9ca64a..b12c3134fd4 100644 --- a/lib/ansible/modules/crypto/openssl_csr.py +++ b/lib/ansible/modules/crypto/openssl_csr.py @@ -200,6 +200,66 @@ options: type: bool default: no version_added: "2.8" + create_subject_key_identifier: + description: + - Create the Subject Key Identifier from the public key. + - "Please note that commercial CAs can ignore the value, respectively use a value of + their own choice instead. Specifying this option is mostly useful for self-signed + certificates or for own CAs." + - Note that this is only supported if the C(cryptography) backend is used! + type: bool + default: no + version_added: "2.9" + subject_key_identifier: + description: + - The subject key identifier as a hex string, where two bytes are separated by colons. + - "Example: C(00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33)" + - "Please note that commercial CAs ignore this value, respectively use a value of their + own choice. Specifying this option is mostly useful for self-signed certificates + or for own CAs." + - Note that this option can only be used if I(create_subject_key_identifier) is C(no). + - Note that this is only supported if the C(cryptography) backend is used! + type: str + version_added: "2.9" + authority_key_identifier: + description: + - The authority key identifier as a hex string, where two bytes are separated by colons. + - "Example: C(00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33)" + - If specified, I(authority_cert_issuer) must also be specified. + - "Please note that commercial CAs ignore this value, respectively use a value of their + own choice. Specifying this option is mostly useful for self-signed certificates + or for own CAs." + - Note that this is only supported if the C(cryptography) backend is used! + - The C(AuthorityKeyIdentifier) will only be added if at least one of I(authority_key_identifier), + I(authority_cert_issuer) and I(authority_cert_serial_number) is specified. + type: str + version_added: "2.9" + authority_cert_issuer: + description: + - Names that will be present in the authority cert issuer field of the certificate signing request. + - Values must be prefixed by their options. (i.e., C(email), C(URI), C(DNS), C(RID), C(IP), C(dirName), + C(otherName) and the ones specific to your CA) + - "Example: C(DNS:ca.example.org)" + - If specified, I(authority_key_identifier) must also be specified. + - "Please note that commercial CAs ignore this value, respectively use a value of their + own choice. Specifying this option is mostly useful for self-signed certificates + or for own CAs." + - Note that this is only supported if the C(cryptography) backend is used! + - The C(AuthorityKeyIdentifier) will only be added if at least one of I(authority_key_identifier), + I(authority_cert_issuer) and I(authority_cert_serial_number) is specified. + type: list + version_added: "2.9" + authority_cert_serial_number: + description: + - The authority cert serial number. + - Note that this is only supported if the C(cryptography) backend is used! + - "Please note that commercial CAs ignore this value, respectively use a value of their + own choice. Specifying this option is mostly useful for self-signed certificates + or for own CAs." + - The C(AuthorityKeyIdentifier) will only be added if at least one of I(authority_key_identifier), + I(authority_cert_issuer) and I(authority_cert_serial_number) is specified. + type: int + version_added: "2.9" extends_documentation_fragment: - files notes: @@ -329,6 +389,7 @@ backup_file: ''' import abc +import binascii import os import traceback from distutils.version import LooseVersion @@ -406,9 +467,17 @@ class CertificateSigningRequestBase(crypto_utils.OpenSSLObject): self.basicConstraints_critical = module.params['basic_constraints_critical'] self.ocspMustStaple = module.params['ocsp_must_staple'] self.ocspMustStaple_critical = module.params['ocsp_must_staple_critical'] + self.create_subject_key_identifier = module.params['create_subject_key_identifier'] + self.subject_key_identifier = module.params['subject_key_identifier'] + self.authority_key_identifier = module.params['authority_key_identifier'] + self.authority_cert_issuer = module.params['authority_cert_issuer'] + self.authority_cert_serial_number = module.params['authority_cert_serial_number'] self.request = None self.privatekey = None + if self.create_subject_key_identifier and self.subject_key_identifier is not None: + module.fail_json(msg='subject_key_identifier cannot be specified if create_subject_key_identifier is true') + self.backup = module.params['backup'] self.backup_file = None @@ -432,6 +501,18 @@ class CertificateSigningRequestBase(crypto_utils.OpenSSLObject): self.subjectAltName = ['DNS:%s' % sub[1]] break + if self.subject_key_identifier is not None: + try: + self.subject_key_identifier = binascii.unhexlify(self.subject_key_identifier.replace(':', '')) + except Exception as e: + raise CertificateSigningRequestError('Cannot parse subject_key_identifier: {0}'.format(e)) + + if self.authority_key_identifier is not None: + try: + self.authority_key_identifier = binascii.unhexlify(self.authority_key_identifier.replace(':', '')) + except Exception as e: + raise CertificateSigningRequestError('Cannot parse authority_key_identifier: {0}'.format(e)) + @abc.abstractmethod def _generate_csr(self): pass @@ -496,6 +577,11 @@ class CertificateSigningRequestBase(crypto_utils.OpenSSLObject): class CertificateSigningRequestPyOpenSSL(CertificateSigningRequestBase): def __init__(self, module): + if module.params['create_subject_key_identifier']: + module.fail_json(msg='You cannot use create_subject_key_identifier with the pyOpenSSL backend!') + for o in ('subject_key_identifier', 'authority_key_identifier', 'authority_cert_issuer', 'authority_cert_serial_number'): + if module.params[o] is not None: + module.fail_json(msg='You cannot use {0} with the pyOpenSSL backend!'.format(o)) super(CertificateSigningRequestPyOpenSSL, self).__init__(module) def _generate_csr(self): @@ -691,6 +777,23 @@ class CertificateSigningRequestCryptography(CertificateSigningRequestBase): critical=self.ocspMustStaple_critical ) + if self.create_subject_key_identifier: + csr = csr.add_extension( + cryptography.x509.SubjectKeyIdentifier.from_public_key(self.privatekey.public_key()), + critical=False + ) + elif self.subject_key_identifier is not None: + csr = csr.add_extension(cryptography.x509.SubjectKeyIdentifier(self.subject_key_identifier), critical=False) + + if self.authority_key_identifier is not None or self.authority_cert_issuer is not None or self.authority_cert_serial_number is not None: + issuers = None + if self.authority_cert_issuer is not None: + issuers = [crypto_utils.cryptography_get_name(n) for n in self.authority_cert_issuer] + csr = csr.add_extension( + cryptography.x509.AuthorityKeyIdentifier(self.authority_key_identifier, issuers, self.authority_cert_serial_number), + critical=False + ) + digest = None if self.digest == 'sha256': digest = cryptography.hazmat.primitives.hashes.SHA256() @@ -806,11 +909,42 @@ class CertificateSigningRequestCryptography(CertificateSigningRequestBase): else: return tlsfeature_ext is None + def _check_subject_key_identifier(extensions): + ext = _find_extension(extensions, cryptography.x509.SubjectKeyIdentifier) + if self.create_subject_key_identifier or self.subject_key_identifier is not None: + if not ext or ext.critical: + return False + if self.create_subject_key_identifier: + digest = cryptography.x509.SubjectKeyIdentifier.from_public_key(self.privatekey.public_key()).digest + return ext.value.digest == digest + else: + return ext.value.digest == self.subject_key_identifier + else: + return ext is None + + def _check_authority_key_identifier(extensions): + ext = _find_extension(extensions, cryptography.x509.AuthorityKeyIdentifier) + if self.authority_key_identifier is not None or self.authority_cert_issuer is not None or self.authority_cert_serial_number is not None: + if not ext or ext.critical: + return False + aci = None + csr_aci = None + if self.authority_cert_issuer is not None: + aci = [str(crypto_utils.cryptography_get_name(n)) for n in self.authority_cert_issuer] + if ext.value.authority_cert_issuer is not None: + csr_aci = [str(n) for n in ext.value.authority_cert_issuer] + return (ext.value.key_identifier == self.authority_key_identifier + and csr_aci == aci + and ext.value.authority_cert_serial_number == self.authority_cert_serial_number) + else: + return ext is None + def _check_extensions(csr): extensions = csr.extensions return (_check_subjectAltName(extensions) and _check_keyUsage(extensions) and _check_extenededKeyUsage(extensions) and _check_basicConstraints(extensions) and - _check_ocspMustStaple(extensions)) + _check_ocspMustStaple(extensions) and _check_subject_key_identifier(extensions) and + _check_authority_key_identifier(extensions)) def _check_signature(csr): if not csr.is_signature_valid: @@ -865,8 +999,14 @@ def main(): ocsp_must_staple=dict(type='bool', default=False, aliases=['ocspMustStaple']), ocsp_must_staple_critical=dict(type='bool', default=False, aliases=['ocspMustStaple_critical']), backup=dict(type='bool', default=False), + create_subject_key_identifier=dict(type='bool', default=False), + subject_key_identifier=dict(type='str'), + authority_key_identifier=dict(type='str'), + authority_cert_issuer=dict(type='list', elements='str'), + authority_cert_serial_number=dict(type='int'), select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography', 'pyopenssl']), ), + required_together=[('authority_cert_issuer', 'authority_cert_serial_number')], add_file_common_args=True, supports_check_mode=True, ) diff --git a/lib/ansible/modules/crypto/openssl_csr_info.py b/lib/ansible/modules/crypto/openssl_csr_info.py index 47e44601a96..7294f48ed04 100644 --- a/lib/ansible/modules/crypto/openssl_csr_info.py +++ b/lib/ansible/modules/crypto/openssl_csr_info.py @@ -160,10 +160,45 @@ public_key_fingerprints: type: dict sample: "{'sha256': 'd4:b3:aa:6d:c8:04:ce:4e:ba:f6:29:4d:92:a3:94:b0:c2:ff:bd:bf:33:63:11:43:34:0f:51:b0:95:09:2f:63', 'sha512': 'f7:07:4a:f0:b0:f0:e6:8b:95:5f:f9:e6:61:0a:32:68:f1..." +subject_key_identifier: + description: + - The CSR's subject key identifier. + - The identifier is returned in hexadecimal, with C(:) used to separate bytes. + - Is C(none) if the C(SubjectKeyIdentifier) extension is not present. + returned: success and if the pyOpenSSL backend is I(not) used + type: str + sample: '00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33' + version_added: "2.9" +authority_key_identifier: + description: + - The CSR's authority key identifier. + - The identifier is returned in hexadecimal, with C(:) used to separate bytes. + - Is C(none) if the C(AuthorityKeyIdentifier) extension is not present. + returned: success and if the pyOpenSSL backend is I(not) used + type: str + sample: '00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33' + version_added: "2.9" +authority_cert_issuer: + description: + - The CSR's authority cert issuer as a list of general names. + - Is C(none) if the C(AuthorityKeyIdentifier) extension is not present. + returned: success and if the pyOpenSSL backend is I(not) used + type: list + sample: "[DNS:www.ansible.com, IP:1.2.3.4]" + version_added: "2.9" +authority_cert_serial_number: + description: + - The CSR's authority cert serial number. + - Is C(none) if the C(AuthorityKeyIdentifier) extension is not present. + returned: success and if the pyOpenSSL backend is I(not) used + type: int + sample: '12345' + version_added: "2.9" ''' import abc +import binascii import os import traceback from distutils.version import LooseVersion @@ -258,6 +293,14 @@ class CertificateSigningRequestInfo(crypto_utils.OpenSSLObject): def _get_public_key(self, binary): pass + @abc.abstractmethod + def _get_subject_key_identifier(self): + pass + + @abc.abstractmethod + def _get_authority_key_identifier(self): + pass + @abc.abstractmethod def _get_all_extensions(self): pass @@ -285,6 +328,21 @@ class CertificateSigningRequestInfo(crypto_utils.OpenSSLObject): pk = self._get_public_key(binary=True) result['public_key_fingerprints'] = crypto_utils.get_fingerprint_of_bytes(pk) if pk is not None else dict() + if self.backend != 'pyopenssl': + ski = self._get_subject_key_identifier() + if ski is not None: + ski = to_native(binascii.hexlify(ski)) + ski = ':'.join([ski[i:i + 2] for i in range(0, len(ski), 2)]) + result['subject_key_identifier'] = ski + + aki, aci, acsn = self._get_authority_key_identifier() + if aki is not None: + aki = to_native(binascii.hexlify(aki)) + aki = ':'.join([aki[i:i + 2] for i in range(0, len(aki), 2)]) + result['authority_key_identifier'] = aki + result['authority_cert_issuer'] = aci + result['authority_cert_serial_number'] = acsn + result['extensions_by_oid'] = self._get_all_extensions() result['signature_valid'] = self._is_signature_valid() @@ -394,6 +452,23 @@ class CertificateSigningRequestInfoCryptography(CertificateSigningRequestInfo): serialization.PublicFormat.SubjectPublicKeyInfo ) + def _get_subject_key_identifier(self): + try: + ext = self.csr.extensions.get_extension_for_class(x509.SubjectKeyIdentifier) + return ext.value.digest + except cryptography.x509.ExtensionNotFound: + return None + + def _get_authority_key_identifier(self): + try: + ext = self.csr.extensions.get_extension_for_class(x509.AuthorityKeyIdentifier) + issuer = None + if ext.value.authority_cert_issuer is not None: + issuer = [crypto_utils.cryptography_decode_name(san) for san in ext.value.authority_cert_issuer] + return ext.value.key_identifier, issuer, ext.value.authority_cert_serial_number + except cryptography.x509.ExtensionNotFound: + return None, None, None + def _get_all_extensions(self): return crypto_utils.cryptography_get_extensions_from_csr(self.csr) @@ -486,6 +561,14 @@ class CertificateSigningRequestInfoPyOpenSSL(CertificateSigningRequestInfo): self.module.warn('Your pyOpenSSL version does not support dumping public keys. ' 'Please upgrade to version 16.0 or newer, or use the cryptography backend.') + def _get_subject_key_identifier(self): + # Won't be implemented + return None + + def _get_authority_key_identifier(self): + # Won't be implemented + return None, None, None + def _get_all_extensions(self): return crypto_utils.pyopenssl_get_extensions_from_csr(self.csr) diff --git a/test/integration/targets/openssl_certificate/tasks/ownca.yml b/test/integration/targets/openssl_certificate/tasks/ownca.yml index 2a5569a022a..8bd9829cdb1 100644 --- a/test/integration/targets/openssl_certificate/tasks/ownca.yml +++ b/test/integration/targets/openssl_certificate/tasks/ownca.yml @@ -308,4 +308,134 @@ select_crypto_backend: '{{ select_crypto_backend }}' register: ownca_backup_5 +- name: (OwnCA, {{select_crypto_backend}}) Create subject key identifier + openssl_certificate: + path: '{{ output_dir }}/ownca_cert_ski.pem' + csr_path: '{{ output_dir }}/csr_ecc.csr' + ownca_path: '{{ output_dir }}/ca_cert.pem' + ownca_privatekey_path: '{{ output_dir }}/privatekey.pem' + provider: ownca + ownca_digest: sha256 + ownca_create_subject_key_identifier: always_create + select_crypto_backend: '{{ select_crypto_backend }}' + when: select_crypto_backend != 'pyopenssl' + register: ownca_subject_key_identifier_1 + +- name: (OwnCA, {{select_crypto_backend}}) Create subject key identifier (idempotency) + openssl_certificate: + path: '{{ output_dir }}/ownca_cert_ski.pem' + csr_path: '{{ output_dir }}/csr_ecc.csr' + ownca_path: '{{ output_dir }}/ca_cert.pem' + ownca_privatekey_path: '{{ output_dir }}/privatekey.pem' + provider: ownca + ownca_digest: sha256 + ownca_create_subject_key_identifier: always_create + select_crypto_backend: '{{ select_crypto_backend }}' + when: select_crypto_backend != 'pyopenssl' + register: ownca_subject_key_identifier_2 + +- name: (OwnCA, {{select_crypto_backend}}) Create subject key identifier (remove) + openssl_certificate: + path: '{{ output_dir }}/ownca_cert_ski.pem' + csr_path: '{{ output_dir }}/csr_ecc.csr' + ownca_path: '{{ output_dir }}/ca_cert.pem' + ownca_privatekey_path: '{{ output_dir }}/privatekey.pem' + provider: ownca + ownca_digest: sha256 + ownca_create_subject_key_identifier: never_create + select_crypto_backend: '{{ select_crypto_backend }}' + when: select_crypto_backend != 'pyopenssl' + register: ownca_subject_key_identifier_3 + +- name: (OwnCA, {{select_crypto_backend}}) Create subject key identifier (remove idempotency) + openssl_certificate: + path: '{{ output_dir }}/ownca_cert_ski.pem' + csr_path: '{{ output_dir }}/csr_ecc.csr' + ownca_path: '{{ output_dir }}/ca_cert.pem' + ownca_privatekey_path: '{{ output_dir }}/privatekey.pem' + provider: ownca + ownca_digest: sha256 + ownca_create_subject_key_identifier: never_create + select_crypto_backend: '{{ select_crypto_backend }}' + when: select_crypto_backend != 'pyopenssl' + register: ownca_subject_key_identifier_4 + +- name: (OwnCA, {{select_crypto_backend}}) Create subject key identifier (re-enable) + openssl_certificate: + path: '{{ output_dir }}/ownca_cert_ski.pem' + csr_path: '{{ output_dir }}/csr_ecc.csr' + ownca_path: '{{ output_dir }}/ca_cert.pem' + ownca_privatekey_path: '{{ output_dir }}/privatekey.pem' + provider: ownca + ownca_digest: sha256 + ownca_create_subject_key_identifier: always_create + select_crypto_backend: '{{ select_crypto_backend }}' + when: select_crypto_backend != 'pyopenssl' + register: ownca_subject_key_identifier_5 + +- name: (OwnCA, {{select_crypto_backend}}) Create authority key identifier + openssl_certificate: + path: '{{ output_dir }}/ownca_cert_aki.pem' + csr_path: '{{ output_dir }}/csr_ecc.csr' + ownca_path: '{{ output_dir }}/ca_cert.pem' + ownca_privatekey_path: '{{ output_dir }}/privatekey.pem' + provider: ownca + ownca_digest: sha256 + ownca_create_authority_key_identifier: yes + select_crypto_backend: '{{ select_crypto_backend }}' + when: select_crypto_backend != 'pyopenssl' + register: ownca_authority_key_identifier_1 + +- name: (OwnCA, {{select_crypto_backend}}) Create authority key identifier (idempotency) + openssl_certificate: + path: '{{ output_dir }}/ownca_cert_aki.pem' + csr_path: '{{ output_dir }}/csr_ecc.csr' + ownca_path: '{{ output_dir }}/ca_cert.pem' + ownca_privatekey_path: '{{ output_dir }}/privatekey.pem' + provider: ownca + ownca_digest: sha256 + ownca_create_authority_key_identifier: yes + select_crypto_backend: '{{ select_crypto_backend }}' + when: select_crypto_backend != 'pyopenssl' + register: ownca_authority_key_identifier_2 + +- name: (OwnCA, {{select_crypto_backend}}) Create authority key identifier (remove) + openssl_certificate: + path: '{{ output_dir }}/ownca_cert_aki.pem' + csr_path: '{{ output_dir }}/csr_ecc.csr' + ownca_path: '{{ output_dir }}/ca_cert.pem' + ownca_privatekey_path: '{{ output_dir }}/privatekey.pem' + provider: ownca + ownca_digest: sha256 + ownca_create_authority_key_identifier: no + select_crypto_backend: '{{ select_crypto_backend }}' + when: select_crypto_backend != 'pyopenssl' + register: ownca_authority_key_identifier_3 + +- name: (OwnCA, {{select_crypto_backend}}) Create authority key identifier (remove idempotency) + openssl_certificate: + path: '{{ output_dir }}/ownca_cert_aki.pem' + csr_path: '{{ output_dir }}/csr_ecc.csr' + ownca_path: '{{ output_dir }}/ca_cert.pem' + ownca_privatekey_path: '{{ output_dir }}/privatekey.pem' + provider: ownca + ownca_digest: sha256 + ownca_create_authority_key_identifier: no + select_crypto_backend: '{{ select_crypto_backend }}' + when: select_crypto_backend != 'pyopenssl' + register: ownca_authority_key_identifier_4 + +- name: (OwnCA, {{select_crypto_backend}}) Create authority key identifier (re-add) + openssl_certificate: + path: '{{ output_dir }}/ownca_cert_aki.pem' + csr_path: '{{ output_dir }}/csr_ecc.csr' + ownca_path: '{{ output_dir }}/ca_cert.pem' + ownca_privatekey_path: '{{ output_dir }}/privatekey.pem' + provider: ownca + ownca_digest: sha256 + ownca_create_authority_key_identifier: yes + select_crypto_backend: '{{ select_crypto_backend }}' + when: select_crypto_backend != 'pyopenssl' + register: ownca_authority_key_identifier_5 + - import_tasks: ../tests/validate_ownca.yml diff --git a/test/integration/targets/openssl_certificate/tasks/selfsigned.yml b/test/integration/targets/openssl_certificate/tasks/selfsigned.yml index 8e145197c7f..74ba851511f 100644 --- a/test/integration/targets/openssl_certificate/tasks/selfsigned.yml +++ b/test/integration/targets/openssl_certificate/tasks/selfsigned.yml @@ -308,4 +308,64 @@ select_crypto_backend: '{{ select_crypto_backend }}' register: selfsigned_backup_5 +- name: (Selfsigned, {{select_crypto_backend}}) Create subject key identifier test + openssl_certificate: + path: '{{ output_dir }}/selfsigned_cert_ski.pem' + csr_path: '{{ output_dir }}/csr_ecc.csr' + privatekey_path: '{{ output_dir }}/privatekey_ecc.pem' + provider: selfsigned + selfsigned_digest: sha256 + selfsigned_create_subject_key_identifier: always_create + select_crypto_backend: '{{ select_crypto_backend }}' + when: select_crypto_backend != 'pyopenssl' + register: selfsigned_subject_key_identifier_1 + +- name: (Selfsigned, {{select_crypto_backend}}) Create subject key identifier test (idempotency) + openssl_certificate: + path: '{{ output_dir }}/selfsigned_cert_ski.pem' + csr_path: '{{ output_dir }}/csr_ecc.csr' + privatekey_path: '{{ output_dir }}/privatekey_ecc.pem' + provider: selfsigned + selfsigned_digest: sha256 + selfsigned_create_subject_key_identifier: always_create + select_crypto_backend: '{{ select_crypto_backend }}' + when: select_crypto_backend != 'pyopenssl' + register: selfsigned_subject_key_identifier_2 + +- name: (Selfsigned, {{select_crypto_backend}}) Create subject key identifier test (remove) + openssl_certificate: + path: '{{ output_dir }}/selfsigned_cert_ski.pem' + csr_path: '{{ output_dir }}/csr_ecc.csr' + privatekey_path: '{{ output_dir }}/privatekey_ecc.pem' + provider: selfsigned + selfsigned_digest: sha256 + selfsigned_create_subject_key_identifier: never_create + select_crypto_backend: '{{ select_crypto_backend }}' + when: select_crypto_backend != 'pyopenssl' + register: selfsigned_subject_key_identifier_3 + +- name: (Selfsigned, {{select_crypto_backend}}) Create subject key identifier test (remove idempotency) + openssl_certificate: + path: '{{ output_dir }}/selfsigned_cert_ski.pem' + csr_path: '{{ output_dir }}/csr_ecc.csr' + privatekey_path: '{{ output_dir }}/privatekey_ecc.pem' + provider: selfsigned + selfsigned_digest: sha256 + selfsigned_create_subject_key_identifier: never_create + select_crypto_backend: '{{ select_crypto_backend }}' + when: select_crypto_backend != 'pyopenssl' + register: selfsigned_subject_key_identifier_4 + +- name: (Selfsigned, {{select_crypto_backend}}) Create subject key identifier test (re-enable) + openssl_certificate: + path: '{{ output_dir }}/selfsigned_cert_ski.pem' + csr_path: '{{ output_dir }}/csr_ecc.csr' + privatekey_path: '{{ output_dir }}/privatekey_ecc.pem' + provider: selfsigned + selfsigned_digest: sha256 + selfsigned_create_subject_key_identifier: always_create + select_crypto_backend: '{{ select_crypto_backend }}' + when: select_crypto_backend != 'pyopenssl' + register: selfsigned_subject_key_identifier_5 + - import_tasks: ../tests/validate_selfsigned.yml diff --git a/test/integration/targets/openssl_certificate/tests/validate_ownca.yml b/test/integration/targets/openssl_certificate/tests/validate_ownca.yml index 3d90d75fcca..a8c6114034b 100644 --- a/test/integration/targets/openssl_certificate/tests/validate_ownca.yml +++ b/test/integration/targets/openssl_certificate/tests/validate_ownca.yml @@ -120,3 +120,23 @@ - ownca_backup_4.backup_file is string - ownca_backup_5 is not changed - ownca_backup_5.backup_file is undefined + +- name: Check create subject key identifier + assert: + that: + - ownca_subject_key_identifier_1 is changed + - ownca_subject_key_identifier_2 is not changed + - ownca_subject_key_identifier_3 is changed + - ownca_subject_key_identifier_4 is not changed + - ownca_subject_key_identifier_5 is changed + when: select_crypto_backend != 'pyopenssl' + +- name: Check create authority key identifier + assert: + that: + - ownca_authority_key_identifier_1 is changed + - ownca_authority_key_identifier_2 is not changed + - ownca_authority_key_identifier_3 is changed + - ownca_authority_key_identifier_4 is not changed + - ownca_authority_key_identifier_5 is changed + when: select_crypto_backend != 'pyopenssl' diff --git a/test/integration/targets/openssl_certificate/tests/validate_selfsigned.yml b/test/integration/targets/openssl_certificate/tests/validate_selfsigned.yml index 1c24effa11d..432a2d8d089 100644 --- a/test/integration/targets/openssl_certificate/tests/validate_selfsigned.yml +++ b/test/integration/targets/openssl_certificate/tests/validate_selfsigned.yml @@ -126,3 +126,13 @@ - selfsigned_backup_4.backup_file is string - selfsigned_backup_5 is not changed - selfsigned_backup_5.backup_file is undefined + +- name: Check create subject key identifier + assert: + that: + - selfsigned_subject_key_identifier_1 is changed + - selfsigned_subject_key_identifier_2 is not changed + - selfsigned_subject_key_identifier_3 is changed + - selfsigned_subject_key_identifier_4 is not changed + - selfsigned_subject_key_identifier_5 is changed + when: select_crypto_backend != 'pyopenssl' diff --git a/test/integration/targets/openssl_certificate_info/tasks/impl.yml b/test/integration/targets/openssl_certificate_info/tasks/impl.yml index 125b9e85a7f..19191a27bb9 100644 --- a/test/integration/targets/openssl_certificate_info/tasks/impl.yml +++ b/test/integration/targets/openssl_certificate_info/tasks/impl.yml @@ -18,6 +18,19 @@ - "['organizationalUnitName', 'Crypto Department'] in result.subject_ordered" - "['organizationalUnitName', 'ACME Department'] in result.subject_ordered" +- name: Check SubjectKeyIdentifier and AuthorityKeyIdentifier + assert: + that: + - result.subject_key_identifier == "00:11:22:33" + - result.authority_key_identifier == "44:55:66:77" + - result.authority_cert_issuer == expected_authority_cert_issuer + - result.authority_cert_serial_number == 12345 + vars: + expected_authority_cert_issuer: + - "DNS:ca.example.org" + - "IP:1.2.3.4" + when: select_crypto_backend != 'pyopenssl' and cryptography_version.stdout is version('1.3', '>=') + - name: Update result list set_fact: info_results: "{{ info_results + [result] }}" @@ -47,6 +60,18 @@ select_crypto_backend: '{{ select_crypto_backend }}' register: result +- name: Check AuthorityKeyIdentifier + assert: + that: + - result.authority_key_identifier is none + - result.authority_cert_issuer == expected_authority_cert_issuer + - result.authority_cert_serial_number == 12345 + vars: + expected_authority_cert_issuer: + - "DNS:ca.example.org" + - "IP:1.2.3.4" + when: select_crypto_backend != 'pyopenssl' and cryptography_version.stdout is version('1.3', '>=') + - name: Update result list set_fact: info_results: "{{ info_results + [result] }}" @@ -57,6 +82,14 @@ select_crypto_backend: '{{ select_crypto_backend }}' register: result +- name: Check AuthorityKeyIdentifier + assert: + that: + - result.authority_key_identifier == "44:55:66:77" + - result.authority_cert_issuer is none + - result.authority_cert_serial_number is none + when: select_crypto_backend != 'pyopenssl' and cryptography_version.stdout is version('1.3', '>=') + - name: Update result list set_fact: info_results: "{{ info_results + [result] }}" diff --git a/test/integration/targets/openssl_certificate_info/tasks/main.yml b/test/integration/targets/openssl_certificate_info/tasks/main.yml index 36a3681f36c..56033b69b21 100644 --- a/test/integration/targets/openssl_certificate_info/tasks/main.yml +++ b/test/integration/targets/openssl_certificate_info/tasks/main.yml @@ -69,6 +69,14 @@ - "pathlen:23" basic_constraints_critical: yes ocsp_must_staple: yes + subject_key_identifier: '{{ "00:11:22:33" if cryptography_version.stdout is version("1.3", ">=") else omit }}' + authority_key_identifier: '{{ "44:55:66:77" if cryptography_version.stdout is version("1.3", ">=") else omit }}' + authority_cert_issuer: '{{ value_for_authority_cert_issuer if cryptography_version.stdout is version("1.3", ">=") else omit }}' + authority_cert_serial_number: '{{ 12345 if cryptography_version.stdout is version("1.3", ">=") else omit }}' + vars: + value_for_authority_cert_issuer: + - "DNS:ca.example.org" + - "IP:1.2.3.4" - name: Generate CSR 2 openssl_csr: @@ -90,12 +98,19 @@ - "IP:DEAD:BEEF::1" basic_constraints: - "CA:FALSE" + authority_cert_issuer: '{{ value_for_authority_cert_issuer if cryptography_version.stdout is version("1.3", ">=") else omit }}' + authority_cert_serial_number: '{{ 12345 if cryptography_version.stdout is version("1.3", ">=") else omit }}' + vars: + value_for_authority_cert_issuer: + - "DNS:ca.example.org" + - "IP:1.2.3.4" - name: Generate CSR 4 openssl_csr: path: '{{ output_dir }}/csr_4.csr' privatekey_path: '{{ output_dir }}/privatekey.pem' useCommonNameForSAN: no + authority_key_identifier: '{{ "44:55:66:77" if cryptography_version.stdout is version("1.3", ">=") else omit }}' - name: Generate selfsigned certificates openssl_certificate: @@ -147,7 +162,14 @@ - name: Compare results assert: that: - - item.0 == item.1 + - ' (item.0 | dict2items | rejectattr("key", "in", keys_to_ignore) | list | items2dict) + == (item.1 | dict2items | rejectattr("key", "in", keys_to_ignore) | list | items2dict)' quiet: yes loop: "{{ pyopenssl_info_results | zip(cryptography_info_results) | list }}" when: pyopenssl_version.stdout is version('0.15', '>=') and cryptography_version.stdout is version('1.6', '>=') + vars: + keys_to_ignore: + - subject_key_identifier + - authority_key_identifier + - authority_cert_issuer + - authority_cert_serial_number diff --git a/test/integration/targets/openssl_certificate_info/test_plugins/jinja_compatibility.py b/test/integration/targets/openssl_certificate_info/test_plugins/jinja_compatibility.py new file mode 100644 index 00000000000..fc2b5f0fcb8 --- /dev/null +++ b/test/integration/targets/openssl_certificate_info/test_plugins/jinja_compatibility.py @@ -0,0 +1,15 @@ +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +def compatibility_in_test(a, b): + return a in b + + +class TestModule: + ''' Ansible math jinja2 tests ''' + + def tests(self): + return { + 'in': compatibility_in_test, + } diff --git a/test/integration/targets/openssl_csr/tasks/impl.yml b/test/integration/targets/openssl_csr/tasks/impl.yml index 8978c4c2342..fe6f4db94e7 100644 --- a/test/integration/targets/openssl_csr/tasks/impl.yml +++ b/test/integration/targets/openssl_csr/tasks/impl.yml @@ -339,6 +339,179 @@ select_crypto_backend: '{{ select_crypto_backend }}' register: csr_backup_5 +- name: Generate CSR with subject key identifier + openssl_csr: + path: '{{ output_dir }}/csr_ski.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + subject_key_identifier: "00:11:22:33" + select_crypto_backend: '{{ select_crypto_backend }}' + when: select_crypto_backend != 'pyopenssl' + register: subject_key_identifier_1 + +- name: Generate CSR with subject key identifier (idempotency) + openssl_csr: + path: '{{ output_dir }}/csr_ski.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + subject_key_identifier: "00:11:22:33" + select_crypto_backend: '{{ select_crypto_backend }}' + when: select_crypto_backend != 'pyopenssl' + register: subject_key_identifier_2 + +- name: Generate CSR with subject key identifier (change) + openssl_csr: + path: '{{ output_dir }}/csr_ski.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + subject_key_identifier: "44:55:66:77:88" + select_crypto_backend: '{{ select_crypto_backend }}' + when: select_crypto_backend != 'pyopenssl' + register: subject_key_identifier_3 + +- name: Generate CSR with subject key identifier (auto-create) + openssl_csr: + path: '{{ output_dir }}/csr_ski.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + create_subject_key_identifier: yes + select_crypto_backend: '{{ select_crypto_backend }}' + when: select_crypto_backend != 'pyopenssl' + register: subject_key_identifier_4 + +- name: Generate CSR with subject key identifier (auto-create idempotency) + openssl_csr: + path: '{{ output_dir }}/csr_ski.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + create_subject_key_identifier: yes + select_crypto_backend: '{{ select_crypto_backend }}' + when: select_crypto_backend != 'pyopenssl' + register: subject_key_identifier_5 + +- name: Generate CSR with subject key identifier (remove) + openssl_csr: + path: '{{ output_dir }}/csr_ski.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + select_crypto_backend: '{{ select_crypto_backend }}' + when: select_crypto_backend != 'pyopenssl' + register: subject_key_identifier_6 + +- name: Generate CSR with authority key identifier + openssl_csr: + path: '{{ output_dir }}/csr_aki.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + authority_key_identifier: "00:11:22:33" + select_crypto_backend: '{{ select_crypto_backend }}' + when: select_crypto_backend != 'pyopenssl' + register: authority_key_identifier_1 + +- name: Generate CSR with authority key identifier (idempotency) + openssl_csr: + path: '{{ output_dir }}/csr_aki.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + authority_key_identifier: "00:11:22:33" + select_crypto_backend: '{{ select_crypto_backend }}' + when: select_crypto_backend != 'pyopenssl' + register: authority_key_identifier_2 + +- name: Generate CSR with authority key identifier (change) + openssl_csr: + path: '{{ output_dir }}/csr_aki.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + authority_key_identifier: "44:55:66:77:88" + select_crypto_backend: '{{ select_crypto_backend }}' + when: select_crypto_backend != 'pyopenssl' + register: authority_key_identifier_3 + +- name: Generate CSR with authority key identifier (remove) + openssl_csr: + path: '{{ output_dir }}/csr_aki.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + select_crypto_backend: '{{ select_crypto_backend }}' + when: select_crypto_backend != 'pyopenssl' + register: authority_key_identifier_4 + +- name: Generate CSR with authority cert issuer / serial number + openssl_csr: + path: '{{ output_dir }}/csr_acisn.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + authority_cert_issuer: + - "DNS:ca.example.org" + - "IP:1.2.3.4" + authority_cert_serial_number: 12345 + select_crypto_backend: '{{ select_crypto_backend }}' + when: select_crypto_backend != 'pyopenssl' + register: authority_cert_issuer_sn_1 + +- name: Generate CSR with authority cert issuer / serial number (idempotency) + openssl_csr: + path: '{{ output_dir }}/csr_acisn.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + authority_cert_issuer: + - "DNS:ca.example.org" + - "IP:1.2.3.4" + authority_cert_serial_number: 12345 + select_crypto_backend: '{{ select_crypto_backend }}' + when: select_crypto_backend != 'pyopenssl' + register: authority_cert_issuer_sn_2 + +- name: Generate CSR with authority cert issuer / serial number (change issuer) + openssl_csr: + path: '{{ output_dir }}/csr_acisn.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + authority_cert_issuer: + - "IP:1.2.3.4" + - "DNS:ca.example.org" + authority_cert_serial_number: 12345 + select_crypto_backend: '{{ select_crypto_backend }}' + when: select_crypto_backend != 'pyopenssl' + register: authority_cert_issuer_sn_3 + +- name: Generate CSR with authority cert issuer / serial number (change serial number) + openssl_csr: + path: '{{ output_dir }}/csr_acisn.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + authority_cert_issuer: + - "IP:1.2.3.4" + - "DNS:ca.example.org" + authority_cert_serial_number: 54321 + select_crypto_backend: '{{ select_crypto_backend }}' + when: select_crypto_backend != 'pyopenssl' + register: authority_cert_issuer_sn_4 + +- name: Generate CSR with authority cert issuer / serial number (remove) + openssl_csr: + path: '{{ output_dir }}/csr_acisn.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + subject: + commonName: www.ansible.com + when: select_crypto_backend != 'pyopenssl' + register: authority_cert_issuer_sn_5 + - name: Generate CSR with everything openssl_csr: path: '{{ output_dir }}/csr_everything.csr' @@ -396,7 +569,15 @@ - "pathlen:23" basic_constraints_critical: yes ocsp_must_staple: yes - select_crypto_backend: '{{ select_crypto_backend }}' + subject_key_identifier: '{{ "00:11:22:33" if select_crypto_backend != "pyopenssl" else omit }}' + authority_key_identifier: '{{ "44:55:66:77" if select_crypto_backend != "pyopenssl" else omit }}' + authority_cert_issuer: '{{ value_for_authority_cert_issuer if select_crypto_backend != "pyopenssl" else omit }}' + authority_cert_serial_number: '{{ 12345 if select_crypto_backend != "pyopenssl" else omit }}' + select_crypto_backend: '{{ select_crypto_backend }}' + vars: + value_for_authority_cert_issuer: + - "DNS:ca.example.org" + - "IP:1.2.3.4" register: everything_1 - name: Generate CSR with everything (idempotent, check mode) @@ -456,7 +637,15 @@ - "pathlen:23" basic_constraints_critical: yes ocsp_must_staple: yes - select_crypto_backend: '{{ select_crypto_backend }}' + subject_key_identifier: '{{ "00:11:22:33" if select_crypto_backend != "pyopenssl" else omit }}' + authority_key_identifier: '{{ "44:55:66:77" if select_crypto_backend != "pyopenssl" else omit }}' + authority_cert_issuer: '{{ value_for_authority_cert_issuer if select_crypto_backend != "pyopenssl" else omit }}' + authority_cert_serial_number: '{{ 12345 if select_crypto_backend != "pyopenssl" else omit }}' + select_crypto_backend: '{{ select_crypto_backend }}' + vars: + value_for_authority_cert_issuer: + - "DNS:ca.example.org" + - "IP:1.2.3.4" check_mode: yes register: everything_2 @@ -517,5 +706,13 @@ - "pathlen:23" basic_constraints_critical: yes ocsp_must_staple: yes - select_crypto_backend: '{{ select_crypto_backend }}' + subject_key_identifier: '{{ "00:11:22:33" if select_crypto_backend != "pyopenssl" else omit }}' + authority_key_identifier: '{{ "44:55:66:77" if select_crypto_backend != "pyopenssl" else omit }}' + authority_cert_issuer: '{{ value_for_authority_cert_issuer if select_crypto_backend != "pyopenssl" else omit }}' + authority_cert_serial_number: '{{ 12345 if select_crypto_backend != "pyopenssl" else omit }}' + select_crypto_backend: '{{ select_crypto_backend }}' + vars: + value_for_authority_cert_issuer: + - "DNS:ca.example.org" + - "IP:1.2.3.4" register: everything_3 diff --git a/test/integration/targets/openssl_csr/tasks/main.yml b/test/integration/targets/openssl_csr/tasks/main.yml index 5b0eba3912b..d0d5d908788 100644 --- a/test/integration/targets/openssl_csr/tasks/main.yml +++ b/test/integration/targets/openssl_csr/tasks/main.yml @@ -16,6 +16,8 @@ select_crypto_backend: pyopenssl - import_tasks: ../tests/validate.yml + vars: + select_crypto_backend: pyopenssl when: pyopenssl_version.stdout is version('0.15', '>=') @@ -36,5 +38,7 @@ select_crypto_backend: cryptography - import_tasks: ../tests/validate.yml + vars: + select_crypto_backend: pyopenssl when: cryptography_version.stdout is version('1.3', '>=') diff --git a/test/integration/targets/openssl_csr/tests/validate.yml b/test/integration/targets/openssl_csr/tests/validate.yml index 43e3b3336b1..d592a4d2362 100644 --- a/test/integration/targets/openssl_csr/tests/validate.yml +++ b/test/integration/targets/openssl_csr/tests/validate.yml @@ -125,6 +125,36 @@ that: - output_broken is changed +- name: Verify that subject key identifier handling works + assert: + that: + - subject_key_identifier_1 is changed + - subject_key_identifier_2 is not changed + - subject_key_identifier_3 is changed + - subject_key_identifier_4 is changed + - subject_key_identifier_5 is not changed + - subject_key_identifier_6 is not changed + when: select_crypto_backend != 'pyopenssl' + +- name: Verify that authority key identifier handling works + assert: + that: + - authority_key_identifier_1 is changed + - authority_key_identifier_2 is not changed + - authority_key_identifier_3 is changed + - authority_key_identifier_4 is changed + when: select_crypto_backend != 'pyopenssl' + +- name: Verify that authority cert issuer / serial number handling works + assert: + that: + - authority_cert_issuer_sn_1 is changed + - authority_cert_issuer_sn_2 is not changed + - authority_cert_issuer_sn_3 is changed + - authority_cert_issuer_sn_4 is changed + - authority_cert_issuer_sn_5 is changed + when: select_crypto_backend != 'pyopenssl' + - name: Check backup assert: that: diff --git a/test/integration/targets/openssl_csr_info/tasks/impl.yml b/test/integration/targets/openssl_csr_info/tasks/impl.yml index f783ce9d411..9516583ef5d 100644 --- a/test/integration/targets/openssl_csr_info/tasks/impl.yml +++ b/test/integration/targets/openssl_csr_info/tasks/impl.yml @@ -15,6 +15,19 @@ - "['organizationalUnitName', 'Crypto Department'] in result.subject_ordered" - "['organizationalUnitName', 'ACME Department'] in result.subject_ordered" +- name: Check SubjectKeyIdentifier and AuthorityKeyIdentifier + assert: + that: + - result.subject_key_identifier == "00:11:22:33" + - result.authority_key_identifier == "44:55:66:77" + - result.authority_cert_issuer == expected_authority_cert_issuer + - result.authority_cert_serial_number == 12345 + vars: + expected_authority_cert_issuer: + - "DNS:ca.example.org" + - "IP:1.2.3.4" + when: select_crypto_backend != 'pyopenssl' and cryptography_version.stdout is version('1.3', '>=') + - name: Update result list set_fact: info_results: "{{ info_results + [result] }}" @@ -35,6 +48,18 @@ select_crypto_backend: '{{ select_crypto_backend }}' register: result +- name: Check AuthorityKeyIdentifier + assert: + that: + - result.authority_key_identifier is none + - result.authority_cert_issuer == expected_authority_cert_issuer + - result.authority_cert_serial_number == 12345 + vars: + expected_authority_cert_issuer: + - "DNS:ca.example.org" + - "IP:1.2.3.4" + when: select_crypto_backend != 'pyopenssl' and cryptography_version.stdout is version('1.3', '>=') + - name: Update result list set_fact: info_results: "{{ info_results + [result] }}" @@ -45,6 +70,14 @@ select_crypto_backend: '{{ select_crypto_backend }}' register: result +- name: Check AuthorityKeyIdentifier + assert: + that: + - result.authority_key_identifier == "44:55:66:77" + - result.authority_cert_issuer is none + - result.authority_cert_serial_number is none + when: select_crypto_backend != 'pyopenssl' and cryptography_version.stdout is version('1.3', '>=') + - name: Update result list set_fact: info_results: "{{ info_results + [result] }}" diff --git a/test/integration/targets/openssl_csr_info/tasks/main.yml b/test/integration/targets/openssl_csr_info/tasks/main.yml index 3621945c56c..9686250109f 100644 --- a/test/integration/targets/openssl_csr_info/tasks/main.yml +++ b/test/integration/targets/openssl_csr_info/tasks/main.yml @@ -69,6 +69,14 @@ - "pathlen:23" basic_constraints_critical: yes ocsp_must_staple: yes + subject_key_identifier: '{{ "00:11:22:33" if cryptography_version.stdout is version("1.3", ">=") else omit }}' + authority_key_identifier: '{{ "44:55:66:77" if cryptography_version.stdout is version("1.3", ">=") else omit }}' + authority_cert_issuer: '{{ value_for_authority_cert_issuer if cryptography_version.stdout is version("1.3", ">=") else omit }}' + authority_cert_serial_number: '{{ 12345 if cryptography_version.stdout is version("1.3", ">=") else omit }}' + vars: + value_for_authority_cert_issuer: + - "DNS:ca.example.org" + - "IP:1.2.3.4" - name: Generate CSR 2 openssl_csr: @@ -90,12 +98,19 @@ - "IP:DEAD:BEEF::1" basic_constraints: - "CA:FALSE" + authority_cert_issuer: '{{ value_for_authority_cert_issuer if cryptography_version.stdout is version("1.3", ">=") else omit }}' + authority_cert_serial_number: '{{ 12345 if cryptography_version.stdout is version("1.3", ">=") else omit }}' + vars: + value_for_authority_cert_issuer: + - "DNS:ca.example.org" + - "IP:1.2.3.4" - name: Generate CSR 4 openssl_csr: path: '{{ output_dir }}/csr_4.csr' privatekey_path: '{{ output_dir }}/privatekey.pem' useCommonNameForSAN: no + authority_key_identifier: '{{ "44:55:66:77" if cryptography_version.stdout is version("1.3", ">=") else omit }}' - name: Prepare result list set_fact: @@ -132,7 +147,14 @@ - name: Compare results assert: that: - - item.0 == item.1 + - ' (item.0 | dict2items | rejectattr("key", "in", keys_to_ignore) | list | items2dict) + == (item.1 | dict2items | rejectattr("key", "in", keys_to_ignore) | list | items2dict)' quiet: yes loop: "{{ pyopenssl_info_results | zip(cryptography_info_results) | list }}" when: pyopenssl_version.stdout is version('0.15', '>=') and cryptography_version.stdout is version('1.3', '>=') + vars: + keys_to_ignore: + - subject_key_identifier + - authority_key_identifier + - authority_cert_issuer + - authority_cert_serial_number diff --git a/test/integration/targets/openssl_csr_info/test_plugins/jinja_compatibility.py b/test/integration/targets/openssl_csr_info/test_plugins/jinja_compatibility.py new file mode 100644 index 00000000000..fc2b5f0fcb8 --- /dev/null +++ b/test/integration/targets/openssl_csr_info/test_plugins/jinja_compatibility.py @@ -0,0 +1,15 @@ +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +def compatibility_in_test(a, b): + return a in b + + +class TestModule: + ''' Ansible math jinja2 tests ''' + + def tests(self): + return { + 'in': compatibility_in_test, + }