diff --git a/changelogs/fragments/129-x509_certificate-no-csr-selfsigned.yml b/changelogs/fragments/129-x509_certificate-no-csr-selfsigned.yml new file mode 100644 index 00000000..b95421f4 --- /dev/null +++ b/changelogs/fragments/129-x509_certificate-no-csr-selfsigned.yml @@ -0,0 +1,2 @@ +minor_changes: +- "x509_certificate - for the ``selfsigned`` provider, a CSR is not required anymore. If no CSR is provided, the module behaves as if a minimal CSR which only contains the public key has been provided (https://github.com/ansible-collections/community.crypto/issues/32, https://github.com/ansible-collections/community.crypto/pull/129)." \ No newline at end of file diff --git a/plugins/modules/x509_certificate.py b/plugins/modules/x509_certificate.py index 6d82bcac..add55bda 100644 --- a/plugins/modules/x509_certificate.py +++ b/plugins/modules/x509_certificate.py @@ -87,13 +87,13 @@ options: csr_path: description: - Path to the Certificate Signing Request (CSR) used to generate this certificate. - - This is not required in C(assertonly) mode. + - This is not required in C(assertonly) or C(selfsigned) mode. - This is mutually exclusive with I(csr_content). type: path csr_content: description: - Content of the Certificate Signing Request (CSR) used to generate this certificate. - - This is not required in C(assertonly) mode. + - This is not required in C(assertonly) or C(selfsigned) mode. - This is mutually exclusive with I(csr_path). type: str version_added: '1.0.0' @@ -1144,7 +1144,7 @@ class SelfSignedCertificateCryptography(Certificate): self.version = module.params['selfsigned_version'] self.serial_number = x509.random_serial_number() - if self.csr_content is None and not os.path.exists(self.csr_path): + 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) ) @@ -1153,11 +1153,6 @@ class SelfSignedCertificateCryptography(Certificate): 'The private key file {0} does not exist'.format(self.privatekey_path) ) - self.csr = load_certificate_request( - path=self.csr_path, - content=self.csr_content, - backend=self.backend - ) self._module = module try: @@ -1170,6 +1165,28 @@ class SelfSignedCertificateCryptography(Certificate): except OpenSSLBadPassphraseError as exc: module.fail_json(msg=to_native(exc)) + if self.csr_path is not None or self.csr_content is not None: + self.csr = load_certificate_request( + path=self.csr_path, + content=self.csr_content, + backend=self.backend + ) + else: + # 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( @@ -1179,14 +1196,6 @@ class SelfSignedCertificateCryptography(Certificate): self.digest = None def generate(self, module): - if self.privatekey_content is None and not os.path.exists(self.privatekey_path): - raise CertificateError( - 'The private key %s does not exist' % self.privatekey_path - ) - if self.csr_content is None and not os.path.exists(self.csr_path): - raise CertificateError( - 'The certificate signing request file %s does not exist' % self.csr_path - ) if not self.check(module, perms_required=False) or self.force: try: cert_builder = x509.CertificateBuilder() @@ -1285,7 +1294,7 @@ class SelfSignedCertificate(Certificate): self.version = module.params['selfsigned_version'] self.serial_number = generate_serial_number() - if self.csr_content is None and not os.path.exists(self.csr_path): + 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) ) @@ -1294,10 +1303,6 @@ class SelfSignedCertificate(Certificate): 'The private key file {0} does not exist'.format(self.privatekey_path) ) - self.csr = load_certificate_request( - path=self.csr_path, - content=self.csr_content, - ) try: self.privatekey = load_privatekey( path=self.privatekey_path, @@ -1307,18 +1312,18 @@ class SelfSignedCertificate(Certificate): except OpenSSLBadPassphraseError as exc: module.fail_json(msg=str(exc)) + if self.csr_path is not None or self.csr_content is not None: + self.csr = load_certificate_request( + path=self.csr_path, + content=self.csr_content, + ) + else: + # 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(self, module): - - if self.privatekey_content is None and not os.path.exists(self.privatekey_path): - raise CertificateError( - 'The private key %s does not exist' % self.privatekey_path - ) - - if self.csr_content is None and not os.path.exists(self.csr_path): - raise CertificateError( - 'The certificate signing request file %s does not exist' % self.csr_path - ) - if not self.check(module, perms_required=False) or self.force: cert = crypto.X509() cert.set_serial_number(self.serial_number) @@ -2666,8 +2671,8 @@ def main(): certificate = CertificateAbsent(module) else: - if module.params['provider'] != 'assertonly' and module.params['csr_path'] is None and module.params['csr_content'] is None: - module.fail_json(msg='csr_path or csr_content is required when provider is not assertonly') + if module.params['provider'] not in ('assertonly', 'selfsigned') and module.params['csr_path'] is None and module.params['csr_content'] is None: + module.fail_json(msg='csr_path or csr_content is required when provider is not assertonly or selfsigned') base_dir = os.path.dirname(module.params['path']) or '.' if not os.path.isdir(base_dir): diff --git a/tests/integration/targets/x509_certificate/tasks/ownca.yml b/tests/integration/targets/x509_certificate/tasks/ownca.yml index ba2cc0ce..45ca894b 100644 --- a/tests/integration/targets/x509_certificate/tasks/ownca.yml +++ b/tests/integration/targets/x509_certificate/tasks/ownca.yml @@ -245,11 +245,11 @@ ignore_errors: yes register: passphrase_error_3 -- name: Create broken certificate +- name: (OwnCA, {{select_crypto_backend}}) Create broken certificate copy: dest: "{{ output_dir }}/ownca_broken.pem" content: "broken" -- name: Regenerate broken cert +- name: (OwnCA, {{select_crypto_backend}}) Regenerate broken cert x509_certificate: path: '{{ output_dir }}/ownca_broken.pem' csr_path: '{{ output_dir }}/csr_ecc.csr' diff --git a/tests/integration/targets/x509_certificate/tasks/selfsigned.yml b/tests/integration/targets/x509_certificate/tasks/selfsigned.yml index 9c5039e8..f61c078b 100644 --- a/tests/integration/targets/x509_certificate/tasks/selfsigned.yml +++ b/tests/integration/targets/x509_certificate/tasks/selfsigned.yml @@ -10,6 +10,36 @@ cipher: auto select_crypto_backend: cryptography +- name: (Selfsigned, {{select_crypto_backend}}) Generate selfsigned certificate without CSR + x509_certificate: + path: '{{ output_dir }}/cert_no_csr.pem' + privatekey_path: '{{ output_dir }}/privatekey.pem' + provider: selfsigned + selfsigned_digest: sha256 + select_crypto_backend: '{{ select_crypto_backend }}' + return_content: yes + register: selfsigned_certificate_no_csr + +- name: (Selfsigned, {{select_crypto_backend}}) Generate selfsigned certificate without CSR - idempotency + x509_certificate: + path: '{{ output_dir }}/cert_no_csr.pem' + privatekey_path: '{{ output_dir }}/privatekey.pem' + provider: selfsigned + selfsigned_digest: sha256 + select_crypto_backend: '{{ select_crypto_backend }}' + return_content: yes + register: selfsigned_certificate_no_csr_idempotence + +- name: (Selfsigned, {{select_crypto_backend}}) Generate selfsigned certificate without CSR (check mode) + x509_certificate: + path: '{{ output_dir }}/cert_no_csr.pem' + privatekey_path: '{{ output_dir }}/privatekey.pem' + provider: selfsigned + selfsigned_digest: sha256 + select_crypto_backend: '{{ select_crypto_backend }}' + check_mode: yes + register: selfsigned_certificate_no_csr_idempotence_check + - name: (Selfsigned, {{select_crypto_backend}}) Generate CSR openssl_csr: path: '{{ output_dir }}/csr.csr' @@ -250,11 +280,11 @@ ignore_errors: yes register: passphrase_error_3 -- name: Create broken certificate +- name: (Selfsigned, {{select_crypto_backend}}) Create broken certificate copy: dest: "{{ output_dir }}/cert_broken.pem" content: "broken" -- name: Regenerate broken cert +- name: (Selfsigned, {{select_crypto_backend}}) Regenerate broken cert x509_certificate: path: '{{ output_dir }}/cert_broken.pem' csr_path: '{{ output_dir }}/csr_ecc.csr' diff --git a/tests/integration/targets/x509_certificate/tests/validate_selfsigned.yml b/tests/integration/targets/x509_certificate/tests/validate_selfsigned.yml index 85df20c5..9561cae4 100644 --- a/tests/integration/targets/x509_certificate/tests/validate_selfsigned.yml +++ b/tests/integration/targets/x509_certificate/tests/validate_selfsigned.yml @@ -3,6 +3,40 @@ shell: 'openssl rsa -noout -modulus -in {{ output_dir }}/privatekey.pem' register: privatekey_modulus +- name: (Selfsigned validation, {{select_crypto_backend}}) Validate behavior for no CSR + assert: + that: + - selfsigned_certificate_no_csr is changed + - selfsigned_certificate_no_csr_idempotence is not changed + - selfsigned_certificate_no_csr_idempotence_check is not changed + +- name: (Selfsigned validation, {{select_crypto_backend}}) Validate certificate with no CSR (test - certificate modulus) + shell: 'openssl x509 -noout -modulus -in {{ output_dir }}/cert_no_csr.pem' + register: cert_modulus + +- name: (Selfsigned validation, {{select_crypto_backend}}) Validate certificate with no CSR (test - certficate version == default == 3) + shell: 'openssl x509 -noout -in {{ output_dir}}/cert_no_csr.pem -text | grep "Version" | sed "s/.*: \(.*\) .*/\1/g"' + register: cert_version + +- name: (Selfsigned validation, {{select_crypto_backend}}) Validate certificate with no CSR (assert) + assert: + that: + - cert_modulus.stdout == privatekey_modulus.stdout + - cert_version.stdout == '3' + +- name: (Selfsigned validation, {{select_crypto_backend}}) Validate certificate with no CSR idempotence + assert: + that: + - selfsigned_certificate_no_csr.serial_number == selfsigned_certificate_no_csr_idempotence.serial_number + - selfsigned_certificate_no_csr.notBefore == selfsigned_certificate_no_csr_idempotence.notBefore + - selfsigned_certificate_no_csr.notAfter == selfsigned_certificate_no_csr_idempotence.notAfter + +- name: (Selfsigned validation, {{select_crypto_backend}}) Validate data retrieval with no CSR + assert: + that: + - selfsigned_certificate_no_csr.certificate == lookup('file', output_dir ~ '/cert_no_csr.pem', rstrip=False) + - selfsigned_certificate_no_csr.certificate == selfsigned_certificate_no_csr_idempotence.certificate + - name: (Selfsigned validation, {{select_crypto_backend}}) Validate certificate (test - certificate modulus) shell: 'openssl x509 -noout -modulus -in {{ output_dir }}/cert.pem' register: cert_modulus