x509_certificate: check existing certificate's signature for selfsigned and ownca provider (#407)

* Verify whether signature matches.

* Add changelog fragment.

* Forgot imports.

* Fix wrong name.

* Check whether the CA private key fits to the CA certificate. Use correct key in tests.

* Refactor code.
pull/408/head
Felix Fontein 2022-02-16 07:38:11 +01:00 committed by GitHub
parent 3ebc132c03
commit 28729657ac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 102 additions and 15 deletions

View File

@ -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)."

View File

@ -32,13 +32,36 @@ from ansible_collections.community.crypto.plugins.module_utils.version import Lo
try: try:
import cryptography import cryptography
from cryptography import x509 from cryptography import x509
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.backends import default_backend from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import padding
import ipaddress import ipaddress
except ImportError: except ImportError:
# Error handled in the calling module. # Error handled in the calling module.
pass 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: try:
# This is a separate try/except since this is only present in cryptography 2.5 or newer # This is a separate try/except since this is only present in cryptography 2.5 or newer
from cryptography.hazmat.primitives.serialization.pkcs12 import ( from cryptography.hazmat.primitives.serialization.pkcs12 import (
@ -58,8 +81,13 @@ except ImportError:
_load_pkcs12 = None _load_pkcs12 = None
from .basic import ( from .basic import (
CRYPTOGRAPHY_HAS_DSA_SIGN,
CRYPTOGRAPHY_HAS_EC_SIGN,
CRYPTOGRAPHY_HAS_ED25519, CRYPTOGRAPHY_HAS_ED25519,
CRYPTOGRAPHY_HAS_ED25519_SIGN,
CRYPTOGRAPHY_HAS_ED448, CRYPTOGRAPHY_HAS_ED448,
CRYPTOGRAPHY_HAS_ED448_SIGN,
CRYPTOGRAPHY_HAS_RSA_SIGN,
CRYPTOGRAPHY_HAS_X25519, CRYPTOGRAPHY_HAS_X25519,
CRYPTOGRAPHY_HAS_X25519_FULL, CRYPTOGRAPHY_HAS_X25519_FULL,
CRYPTOGRAPHY_HAS_X448, CRYPTOGRAPHY_HAS_X448,
@ -664,3 +692,40 @@ def _parse_pkcs12_legacy(pkcs12_bytes, passphrase=None):
if maybe_name != backend._ffi.NULL: if maybe_name != backend._ffi.NULL:
friendly_name = backend._ffi.string(maybe_name) friendly_name = backend._ffi.string(maybe_name)
return private_key, certificate, additional_certificates, friendly_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
)

View File

@ -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 ( from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
cryptography_compare_public_keys,
cryptography_key_needs_digest_for_signing, cryptography_key_needs_digest_for_signing,
cryptography_serial_number_of_cert, cryptography_serial_number_of_cert,
cryptography_verify_certificate_signature,
) )
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate import ( from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate import (
@ -107,6 +109,9 @@ class OwnCACertificateBackendCryptography(CertificateBackend):
except OpenSSLBadPassphraseError as exc: except OpenSSLBadPassphraseError as exc:
module.fail_json(msg=str(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 cryptography_key_needs_digest_for_signing(self.ca_private_key):
if self.digest is None: if self.digest is None:
raise CertificateError( raise CertificateError(
@ -175,6 +180,10 @@ class OwnCACertificateBackendCryptography(CertificateBackend):
self._ensure_existing_certificate_loaded() 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 # Check subject
if self.ca_cert.subject != self.existing_certificate.issuer: if self.ca_cert.subject != self.existing_certificate.issuer:
return True return True

View File

@ -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 ( from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
cryptography_key_needs_digest_for_signing, cryptography_key_needs_digest_for_signing,
cryptography_serial_number_of_cert, cryptography_serial_number_of_cert,
cryptography_verify_certificate_signature,
) )
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate import ( 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) return self.cert.public_bytes(Encoding.PEM)
def needs_regeneration(self): def needs_regeneration(self):
return super(SelfSignedCertificateBackendCryptography, self).needs_regeneration( if super(SelfSignedCertificateBackendCryptography, self).needs_regeneration(not_before=self.notBefore, not_after=self.notAfter):
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): def dump(self, include_certificate):
result = super(SelfSignedCertificateBackendCryptography, self).dump(include_certificate) result = super(SelfSignedCertificateBackendCryptography, self).dump(include_certificate)

View File

@ -344,7 +344,7 @@
path: '{{ remote_tmp_dir }}/ownca_cert_backup.pem' path: '{{ remote_tmp_dir }}/ownca_cert_backup.pem'
csr_path: '{{ remote_tmp_dir }}/csr_ecc.csr' csr_path: '{{ remote_tmp_dir }}/csr_ecc.csr'
ownca_path: '{{ remote_tmp_dir }}/ca_cert.pem' 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 provider: ownca
ownca_digest: sha256 ownca_digest: sha256
backup: yes backup: yes
@ -355,7 +355,7 @@
path: '{{ remote_tmp_dir }}/ownca_cert_backup.pem' path: '{{ remote_tmp_dir }}/ownca_cert_backup.pem'
csr_path: '{{ remote_tmp_dir }}/csr_ecc.csr' csr_path: '{{ remote_tmp_dir }}/csr_ecc.csr'
ownca_path: '{{ remote_tmp_dir }}/ca_cert.pem' 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 provider: ownca
ownca_digest: sha256 ownca_digest: sha256
backup: yes backup: yes
@ -366,7 +366,7 @@
path: '{{ remote_tmp_dir }}/ownca_cert_backup.pem' path: '{{ remote_tmp_dir }}/ownca_cert_backup.pem'
csr_path: '{{ remote_tmp_dir }}/csr.csr' csr_path: '{{ remote_tmp_dir }}/csr.csr'
ownca_path: '{{ remote_tmp_dir }}/ca_cert.pem' 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 provider: ownca
ownca_digest: sha256 ownca_digest: sha256
backup: yes backup: yes
@ -394,7 +394,7 @@
path: '{{ remote_tmp_dir }}/ownca_cert_ski.pem' path: '{{ remote_tmp_dir }}/ownca_cert_ski.pem'
csr_path: '{{ remote_tmp_dir }}/csr_ecc.csr' csr_path: '{{ remote_tmp_dir }}/csr_ecc.csr'
ownca_path: '{{ remote_tmp_dir }}/ca_cert.pem' 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 provider: ownca
ownca_digest: sha256 ownca_digest: sha256
ownca_create_subject_key_identifier: always_create ownca_create_subject_key_identifier: always_create
@ -406,7 +406,7 @@
path: '{{ remote_tmp_dir }}/ownca_cert_ski.pem' path: '{{ remote_tmp_dir }}/ownca_cert_ski.pem'
csr_path: '{{ remote_tmp_dir }}/csr_ecc.csr' csr_path: '{{ remote_tmp_dir }}/csr_ecc.csr'
ownca_path: '{{ remote_tmp_dir }}/ca_cert.pem' 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 provider: ownca
ownca_digest: sha256 ownca_digest: sha256
ownca_create_subject_key_identifier: always_create ownca_create_subject_key_identifier: always_create
@ -418,7 +418,7 @@
path: '{{ remote_tmp_dir }}/ownca_cert_ski.pem' path: '{{ remote_tmp_dir }}/ownca_cert_ski.pem'
csr_path: '{{ remote_tmp_dir }}/csr_ecc.csr' csr_path: '{{ remote_tmp_dir }}/csr_ecc.csr'
ownca_path: '{{ remote_tmp_dir }}/ca_cert.pem' 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 provider: ownca
ownca_digest: sha256 ownca_digest: sha256
ownca_create_subject_key_identifier: never_create ownca_create_subject_key_identifier: never_create
@ -430,7 +430,7 @@
path: '{{ remote_tmp_dir }}/ownca_cert_ski.pem' path: '{{ remote_tmp_dir }}/ownca_cert_ski.pem'
csr_path: '{{ remote_tmp_dir }}/csr_ecc.csr' csr_path: '{{ remote_tmp_dir }}/csr_ecc.csr'
ownca_path: '{{ remote_tmp_dir }}/ca_cert.pem' 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 provider: ownca
ownca_digest: sha256 ownca_digest: sha256
ownca_create_subject_key_identifier: never_create ownca_create_subject_key_identifier: never_create
@ -442,7 +442,7 @@
path: '{{ remote_tmp_dir }}/ownca_cert_ski.pem' path: '{{ remote_tmp_dir }}/ownca_cert_ski.pem'
csr_path: '{{ remote_tmp_dir }}/csr_ecc.csr' csr_path: '{{ remote_tmp_dir }}/csr_ecc.csr'
ownca_path: '{{ remote_tmp_dir }}/ca_cert.pem' 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 provider: ownca
ownca_digest: sha256 ownca_digest: sha256
ownca_create_subject_key_identifier: always_create ownca_create_subject_key_identifier: always_create
@ -454,7 +454,7 @@
path: '{{ remote_tmp_dir }}/ownca_cert_aki.pem' path: '{{ remote_tmp_dir }}/ownca_cert_aki.pem'
csr_path: '{{ remote_tmp_dir }}/csr_ecc.csr' csr_path: '{{ remote_tmp_dir }}/csr_ecc.csr'
ownca_path: '{{ remote_tmp_dir }}/ca_cert.pem' 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 provider: ownca
ownca_digest: sha256 ownca_digest: sha256
ownca_create_authority_key_identifier: yes ownca_create_authority_key_identifier: yes
@ -466,7 +466,7 @@
path: '{{ remote_tmp_dir }}/ownca_cert_aki.pem' path: '{{ remote_tmp_dir }}/ownca_cert_aki.pem'
csr_path: '{{ remote_tmp_dir }}/csr_ecc.csr' csr_path: '{{ remote_tmp_dir }}/csr_ecc.csr'
ownca_path: '{{ remote_tmp_dir }}/ca_cert.pem' 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 provider: ownca
ownca_digest: sha256 ownca_digest: sha256
ownca_create_authority_key_identifier: yes ownca_create_authority_key_identifier: yes
@ -478,7 +478,7 @@
path: '{{ remote_tmp_dir }}/ownca_cert_aki.pem' path: '{{ remote_tmp_dir }}/ownca_cert_aki.pem'
csr_path: '{{ remote_tmp_dir }}/csr_ecc.csr' csr_path: '{{ remote_tmp_dir }}/csr_ecc.csr'
ownca_path: '{{ remote_tmp_dir }}/ca_cert.pem' 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 provider: ownca
ownca_digest: sha256 ownca_digest: sha256
ownca_create_authority_key_identifier: no ownca_create_authority_key_identifier: no
@ -490,7 +490,7 @@
path: '{{ remote_tmp_dir }}/ownca_cert_aki.pem' path: '{{ remote_tmp_dir }}/ownca_cert_aki.pem'
csr_path: '{{ remote_tmp_dir }}/csr_ecc.csr' csr_path: '{{ remote_tmp_dir }}/csr_ecc.csr'
ownca_path: '{{ remote_tmp_dir }}/ca_cert.pem' 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 provider: ownca
ownca_digest: sha256 ownca_digest: sha256
ownca_create_authority_key_identifier: no ownca_create_authority_key_identifier: no
@ -502,7 +502,7 @@
path: '{{ remote_tmp_dir }}/ownca_cert_aki.pem' path: '{{ remote_tmp_dir }}/ownca_cert_aki.pem'
csr_path: '{{ remote_tmp_dir }}/csr_ecc.csr' csr_path: '{{ remote_tmp_dir }}/csr_ecc.csr'
ownca_path: '{{ remote_tmp_dir }}/ca_cert.pem' 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 provider: ownca
ownca_digest: sha256 ownca_digest: sha256
ownca_create_authority_key_identifier: yes ownca_create_authority_key_identifier: yes