diff --git a/changelogs/fragments/407-x509_certificate-signature.yml b/changelogs/fragments/407-x509_certificate-signature.yml new file mode 100644 index 00000000..bcdf0a9b --- /dev/null +++ b/changelogs/fragments/407-x509_certificate-signature.yml @@ -0,0 +1,4 @@ +bugfixes: + - "x509_certificate - for the ``ownca`` provider, check whether the CA private key actually belongs to the CA certificate (https://github.com/ansible-collections/community.crypto/pull/407)." + - "x509_certificate - regenerate certificate when the CA's public key changes for ``provider=ownca`` (https://github.com/ansible-collections/community.crypto/pull/407)." + - "x509_certificate - regenerate certificate when the private key changes for ``provider=selfsigned`` (https://github.com/ansible-collections/community.crypto/pull/407)." diff --git a/plugins/module_utils/crypto/cryptography_support.py b/plugins/module_utils/crypto/cryptography_support.py index 98d0fb02..776a7be1 100644 --- a/plugins/module_utils/crypto/cryptography_support.py +++ b/plugins/module_utils/crypto/cryptography_support.py @@ -32,13 +32,36 @@ from ansible_collections.community.crypto.plugins.module_utils.version import Lo try: import cryptography from cryptography import x509 + from cryptography.exceptions import InvalidSignature from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import serialization + from cryptography.hazmat.primitives.asymmetric import padding import ipaddress except ImportError: # Error handled in the calling module. pass +try: + import cryptography.hazmat.primitives.asymmetric.rsa +except ImportError: + pass +try: + import cryptography.hazmat.primitives.asymmetric.ec +except ImportError: + pass +try: + import cryptography.hazmat.primitives.asymmetric.dsa +except ImportError: + pass +try: + import cryptography.hazmat.primitives.asymmetric.ed25519 +except ImportError: + pass +try: + import cryptography.hazmat.primitives.asymmetric.ed448 +except ImportError: + pass + try: # This is a separate try/except since this is only present in cryptography 2.5 or newer from cryptography.hazmat.primitives.serialization.pkcs12 import ( @@ -58,8 +81,13 @@ except ImportError: _load_pkcs12 = None from .basic import ( + CRYPTOGRAPHY_HAS_DSA_SIGN, + CRYPTOGRAPHY_HAS_EC_SIGN, CRYPTOGRAPHY_HAS_ED25519, + CRYPTOGRAPHY_HAS_ED25519_SIGN, CRYPTOGRAPHY_HAS_ED448, + CRYPTOGRAPHY_HAS_ED448_SIGN, + CRYPTOGRAPHY_HAS_RSA_SIGN, CRYPTOGRAPHY_HAS_X25519, CRYPTOGRAPHY_HAS_X25519_FULL, CRYPTOGRAPHY_HAS_X448, @@ -664,3 +692,40 @@ def _parse_pkcs12_legacy(pkcs12_bytes, passphrase=None): if maybe_name != backend._ffi.NULL: friendly_name = backend._ffi.string(maybe_name) return private_key, certificate, additional_certificates, friendly_name + + +def cryptography_verify_signature(signature, data, hash_algorithm, signer_public_key): + ''' + Check whether the given signature of the given data was signed by the given public key object. + ''' + try: + if CRYPTOGRAPHY_HAS_RSA_SIGN and isinstance(signer_public_key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey): + signer_public_key.verify(signature, data, padding.PKCS1v15(), hash_algorithm) + return True + if CRYPTOGRAPHY_HAS_EC_SIGN and isinstance(signer_public_key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey): + signer_public_key.verify(signature, data, cryptography.hazmat.primitives.asymmetric.ec.ECDSA(hash_algorithm)) + return True + if CRYPTOGRAPHY_HAS_DSA_SIGN and isinstance(signer_public_key, cryptography.hazmat.primitives.asymmetric.dsa.DSAPublicKey): + signer_public_key.verify(signature, data, hash_algorithm) + return True + if CRYPTOGRAPHY_HAS_ED25519_SIGN and isinstance(signer_public_key, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PublicKey): + signer_public_key.verify(signature, data) + return True + if CRYPTOGRAPHY_HAS_ED448_SIGN and isinstance(signer_public_key, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PublicKey): + signer_public_key.verify(signature, data) + return True + raise OpenSSLObjectError(u'Unsupported public key type {0}'.format(type(signer_public_key))) + except InvalidSignature: + return False + + +def cryptography_verify_certificate_signature(certificate, signer_public_key): + ''' + Check whether the given X509 certificate object was signed by the given public key object. + ''' + return cryptography_verify_signature( + certificate.signature, + certificate.tbs_certificate_bytes, + certificate.signature_hash_algorithm, + signer_public_key + ) diff --git a/plugins/module_utils/crypto/module_backends/certificate_ownca.py b/plugins/module_utils/crypto/module_backends/certificate_ownca.py index e28d7dc3..f84c2d37 100644 --- a/plugins/module_utils/crypto/module_backends/certificate_ownca.py +++ b/plugins/module_utils/crypto/module_backends/certificate_ownca.py @@ -28,8 +28,10 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.support im ) from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import ( + cryptography_compare_public_keys, cryptography_key_needs_digest_for_signing, cryptography_serial_number_of_cert, + cryptography_verify_certificate_signature, ) from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate import ( @@ -107,6 +109,9 @@ class OwnCACertificateBackendCryptography(CertificateBackend): except OpenSSLBadPassphraseError as exc: module.fail_json(msg=str(exc)) + if not cryptography_compare_public_keys(self.ca_cert.public_key(), self.ca_private_key.public_key()): + raise CertificateError('The CA private key does not belong to the CA certificate') + if cryptography_key_needs_digest_for_signing(self.ca_private_key): if self.digest is None: raise CertificateError( @@ -175,6 +180,10 @@ class OwnCACertificateBackendCryptography(CertificateBackend): self._ensure_existing_certificate_loaded() + # Check whether certificate is signed by CA certificate + if not cryptography_verify_certificate_signature(self.existing_certificate, self.ca_cert.public_key()): + return True + # Check subject if self.ca_cert.subject != self.existing_certificate.issuer: return True diff --git a/plugins/module_utils/crypto/module_backends/certificate_selfsigned.py b/plugins/module_utils/crypto/module_backends/certificate_selfsigned.py index d92fbb68..7b5484ab 100644 --- a/plugins/module_utils/crypto/module_backends/certificate_selfsigned.py +++ b/plugins/module_utils/crypto/module_backends/certificate_selfsigned.py @@ -22,6 +22,7 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.support im from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import ( cryptography_key_needs_digest_for_signing, cryptography_serial_number_of_cert, + cryptography_verify_certificate_signature, ) from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate import ( @@ -135,8 +136,16 @@ class SelfSignedCertificateBackendCryptography(CertificateBackend): return self.cert.public_bytes(Encoding.PEM) def needs_regeneration(self): - return super(SelfSignedCertificateBackendCryptography, self).needs_regeneration( - not_before=self.notBefore, not_after=self.notAfter) + if super(SelfSignedCertificateBackendCryptography, self).needs_regeneration(not_before=self.notBefore, not_after=self.notAfter): + return True + + self._ensure_existing_certificate_loaded() + + # Check whether certificate is signed by private key + if not cryptography_verify_certificate_signature(self.existing_certificate, self.privatekey.public_key()): + return True + + return False def dump(self, include_certificate): result = super(SelfSignedCertificateBackendCryptography, self).dump(include_certificate) diff --git a/tests/integration/targets/x509_certificate/tasks/ownca.yml b/tests/integration/targets/x509_certificate/tasks/ownca.yml index 4ba05435..d78bf3fc 100644 --- a/tests/integration/targets/x509_certificate/tasks/ownca.yml +++ b/tests/integration/targets/x509_certificate/tasks/ownca.yml @@ -344,7 +344,7 @@ path: '{{ remote_tmp_dir }}/ownca_cert_backup.pem' csr_path: '{{ remote_tmp_dir }}/csr_ecc.csr' ownca_path: '{{ remote_tmp_dir }}/ca_cert.pem' - ownca_privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + ownca_privatekey_path: '{{ remote_tmp_dir }}/ca_privatekey.pem' provider: ownca ownca_digest: sha256 backup: yes @@ -355,7 +355,7 @@ path: '{{ remote_tmp_dir }}/ownca_cert_backup.pem' csr_path: '{{ remote_tmp_dir }}/csr_ecc.csr' ownca_path: '{{ remote_tmp_dir }}/ca_cert.pem' - ownca_privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + ownca_privatekey_path: '{{ remote_tmp_dir }}/ca_privatekey.pem' provider: ownca ownca_digest: sha256 backup: yes @@ -366,7 +366,7 @@ path: '{{ remote_tmp_dir }}/ownca_cert_backup.pem' csr_path: '{{ remote_tmp_dir }}/csr.csr' ownca_path: '{{ remote_tmp_dir }}/ca_cert.pem' - ownca_privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + ownca_privatekey_path: '{{ remote_tmp_dir }}/ca_privatekey.pem' provider: ownca ownca_digest: sha256 backup: yes @@ -394,7 +394,7 @@ path: '{{ remote_tmp_dir }}/ownca_cert_ski.pem' csr_path: '{{ remote_tmp_dir }}/csr_ecc.csr' ownca_path: '{{ remote_tmp_dir }}/ca_cert.pem' - ownca_privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + ownca_privatekey_path: '{{ remote_tmp_dir }}/ca_privatekey.pem' provider: ownca ownca_digest: sha256 ownca_create_subject_key_identifier: always_create @@ -406,7 +406,7 @@ path: '{{ remote_tmp_dir }}/ownca_cert_ski.pem' csr_path: '{{ remote_tmp_dir }}/csr_ecc.csr' ownca_path: '{{ remote_tmp_dir }}/ca_cert.pem' - ownca_privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + ownca_privatekey_path: '{{ remote_tmp_dir }}/ca_privatekey.pem' provider: ownca ownca_digest: sha256 ownca_create_subject_key_identifier: always_create @@ -418,7 +418,7 @@ path: '{{ remote_tmp_dir }}/ownca_cert_ski.pem' csr_path: '{{ remote_tmp_dir }}/csr_ecc.csr' ownca_path: '{{ remote_tmp_dir }}/ca_cert.pem' - ownca_privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + ownca_privatekey_path: '{{ remote_tmp_dir }}/ca_privatekey.pem' provider: ownca ownca_digest: sha256 ownca_create_subject_key_identifier: never_create @@ -430,7 +430,7 @@ path: '{{ remote_tmp_dir }}/ownca_cert_ski.pem' csr_path: '{{ remote_tmp_dir }}/csr_ecc.csr' ownca_path: '{{ remote_tmp_dir }}/ca_cert.pem' - ownca_privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + ownca_privatekey_path: '{{ remote_tmp_dir }}/ca_privatekey.pem' provider: ownca ownca_digest: sha256 ownca_create_subject_key_identifier: never_create @@ -442,7 +442,7 @@ path: '{{ remote_tmp_dir }}/ownca_cert_ski.pem' csr_path: '{{ remote_tmp_dir }}/csr_ecc.csr' ownca_path: '{{ remote_tmp_dir }}/ca_cert.pem' - ownca_privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + ownca_privatekey_path: '{{ remote_tmp_dir }}/ca_privatekey.pem' provider: ownca ownca_digest: sha256 ownca_create_subject_key_identifier: always_create @@ -454,7 +454,7 @@ path: '{{ remote_tmp_dir }}/ownca_cert_aki.pem' csr_path: '{{ remote_tmp_dir }}/csr_ecc.csr' ownca_path: '{{ remote_tmp_dir }}/ca_cert.pem' - ownca_privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + ownca_privatekey_path: '{{ remote_tmp_dir }}/ca_privatekey.pem' provider: ownca ownca_digest: sha256 ownca_create_authority_key_identifier: yes @@ -466,7 +466,7 @@ path: '{{ remote_tmp_dir }}/ownca_cert_aki.pem' csr_path: '{{ remote_tmp_dir }}/csr_ecc.csr' ownca_path: '{{ remote_tmp_dir }}/ca_cert.pem' - ownca_privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + ownca_privatekey_path: '{{ remote_tmp_dir }}/ca_privatekey.pem' provider: ownca ownca_digest: sha256 ownca_create_authority_key_identifier: yes @@ -478,7 +478,7 @@ path: '{{ remote_tmp_dir }}/ownca_cert_aki.pem' csr_path: '{{ remote_tmp_dir }}/csr_ecc.csr' ownca_path: '{{ remote_tmp_dir }}/ca_cert.pem' - ownca_privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + ownca_privatekey_path: '{{ remote_tmp_dir }}/ca_privatekey.pem' provider: ownca ownca_digest: sha256 ownca_create_authority_key_identifier: no @@ -490,7 +490,7 @@ path: '{{ remote_tmp_dir }}/ownca_cert_aki.pem' csr_path: '{{ remote_tmp_dir }}/csr_ecc.csr' ownca_path: '{{ remote_tmp_dir }}/ca_cert.pem' - ownca_privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + ownca_privatekey_path: '{{ remote_tmp_dir }}/ca_privatekey.pem' provider: ownca ownca_digest: sha256 ownca_create_authority_key_identifier: no @@ -502,7 +502,7 @@ path: '{{ remote_tmp_dir }}/ownca_cert_aki.pem' csr_path: '{{ remote_tmp_dir }}/csr_ecc.csr' ownca_path: '{{ remote_tmp_dir }}/ca_cert.pem' - ownca_privatekey_path: '{{ remote_tmp_dir }}/privatekey.pem' + ownca_privatekey_path: '{{ remote_tmp_dir }}/ca_privatekey.pem' provider: ownca ownca_digest: sha256 ownca_create_authority_key_identifier: yes