From 221da3e8b18edbd81fb31053867ab01fbec2332a Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Mon, 8 Apr 2019 10:30:05 +0200 Subject: [PATCH] Implement Ed25519, Ed448, X25519 and X448 support (cryptography backend). (#54947) --- .../modules/crypto/openssl_privatekey.py | 41 +++++++++++++------ .../targets/openssl_privatekey/tasks/impl.yml | 39 ++++++++++++++++-- .../openssl_privatekey/tests/validate.yml | 19 +++++++++ 3 files changed, 82 insertions(+), 17 deletions(-) diff --git a/lib/ansible/modules/crypto/openssl_privatekey.py b/lib/ansible/modules/crypto/openssl_privatekey.py index ed6155b0d66..86953e1ed58 100644 --- a/lib/ansible/modules/crypto/openssl_privatekey.py +++ b/lib/ansible/modules/crypto/openssl_privatekey.py @@ -19,9 +19,9 @@ short_description: Generate OpenSSL private keys description: - This module allows one to (re)generate OpenSSL private keys. - One can generate L(RSA,https://en.wikipedia.org/wiki/RSA_(cryptosystem)), - L(DSA,https://en.wikipedia.org/wiki/Digital_Signature_Algorithm) or - L(ECC,https://en.wikipedia.org/wiki/Elliptic-curve_cryptography) - private keys. + L(DSA,https://en.wikipedia.org/wiki/Digital_Signature_Algorithm), + L(ECC,https://en.wikipedia.org/wiki/Elliptic-curve_cryptography) or + L(EdDSA,https://en.wikipedia.org/wiki/EdDSA) private keys. - Keys are generated in PEM format. - "Please note that the module regenerates private keys if they don't match the module's options. In particular, if you provide another passphrase @@ -52,12 +52,13 @@ options: type: description: - The algorithm used to generate the TLS/SSL private key. - - Note that C(ECC) requires the C(cryptography) backend. - - Depending on the curve, you need a newer version of the cryptography backend. + - Note that C(ECC), C(X25519), C(X448), C(Ed25519) and C(Ed448) require the C(cryptography) backend. + C(X25519) needs cryptography 2.5 or newer, while C(X448), C(Ed25519) and C(Ed448) require + cryptography 2.6 or newer. For C(ECC), the minimal cryptography version required depends on the + I(curve) option. type: str default: RSA - #choices: [ DSA, ECC, RSA, X448, X25519, Ed448, Ed25519 ] - choices: [ DSA, ECC, RSA ] + choices: [ DSA, ECC, Ed25519, Ed448, RSA, X25519, X448 ] curve: description: - Note that not all curves are supported by all versions of C(cryptography). @@ -239,8 +240,14 @@ else: try: import cryptography.hazmat.primitives.asymmetric.x25519 CRYPTOGRAPHY_HAS_X25519 = True + try: + cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey.private_bytes + CRYPTOGRAPHY_HAS_X25519_FULL = True + except AttributeError: + CRYPTOGRAPHY_HAS_X25519_FULL = False except ImportError: CRYPTOGRAPHY_HAS_X25519 = False + CRYPTOGRAPHY_HAS_X25519_FULL = False try: import cryptography.hazmat.primitives.asymmetric.x448 CRYPTOGRAPHY_HAS_X448 = True @@ -467,6 +474,8 @@ class PrivateKeyCryptography(PrivateKeyBase): self.curve = module.params['curve'] if not CRYPTOGRAPHY_HAS_X25519 and self.type == 'X25519': self.module.fail_json(msg='Your cryptography version does not support X25519') + if not CRYPTOGRAPHY_HAS_X25519_FULL and self.type == 'X25519': + self.module.fail_json(msg='Your cryptography version does not support X25519 serialization') if not CRYPTOGRAPHY_HAS_X448 and self.type == 'X448': self.module.fail_json(msg='Your cryptography version does not support X448') if not CRYPTOGRAPHY_HAS_ED25519 and self.type == 'Ed25519': @@ -475,6 +484,7 @@ class PrivateKeyCryptography(PrivateKeyBase): self.module.fail_json(msg='Your cryptography version does not support Ed448') def _generate_private_key_data(self): + format = cryptography.hazmat.primitives.serialization.PrivateFormat.TraditionalOpenSSL try: if self.type == 'RSA': self.privatekey = cryptography.hazmat.primitives.asymmetric.rsa.generate_private_key( @@ -487,14 +497,18 @@ class PrivateKeyCryptography(PrivateKeyBase): key_size=self.size, backend=self.cryptography_backend ) - if CRYPTOGRAPHY_HAS_X25519 and self.type == 'X25519': + if CRYPTOGRAPHY_HAS_X25519_FULL and self.type == 'X25519': self.privatekey = cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey.generate() + format = cryptography.hazmat.primitives.serialization.PrivateFormat.PKCS8 if CRYPTOGRAPHY_HAS_X448 and self.type == 'X448': self.privatekey = cryptography.hazmat.primitives.asymmetric.x448.X448PrivateKey.generate() + format = cryptography.hazmat.primitives.serialization.PrivateFormat.PKCS8 if CRYPTOGRAPHY_HAS_ED25519 and self.type == 'Ed25519': self.privatekey = cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey.generate() + format = cryptography.hazmat.primitives.serialization.PrivateFormat.PKCS8 if CRYPTOGRAPHY_HAS_ED448 and self.type == 'Ed448': self.privatekey = cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey.generate() + format = cryptography.hazmat.primitives.serialization.PrivateFormat.PKCS8 if self.type == 'ECC' and self.curve in self.curves: if self.curves[self.curve]['deprecated']: self.module.warn('Elliptic curves of type {0} should not be used for new keys!'.format(self.curve)) @@ -516,7 +530,7 @@ class PrivateKeyCryptography(PrivateKeyBase): # Serialize key return self.privatekey.private_bytes( encoding=cryptography.hazmat.primitives.serialization.Encoding.PEM, - format=cryptography.hazmat.primitives.serialization.PrivateFormat.TraditionalOpenSSL, + format=format, encryption_algorithm=encryption_algorithm ) @@ -565,6 +579,10 @@ class PrivateKeyCryptography(PrivateKeyBase): return self.type == 'X25519' if CRYPTOGRAPHY_HAS_X448 and isinstance(privatekey, cryptography.hazmat.primitives.asymmetric.x448.X448PrivateKey): return self.type == 'X448' + if CRYPTOGRAPHY_HAS_ED25519 and isinstance(privatekey, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey): + return self.type == 'Ed25519' + if CRYPTOGRAPHY_HAS_ED448 and isinstance(privatekey, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey): + return self.type == 'Ed448' if isinstance(privatekey, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey): if self.type != 'ECC': return False @@ -590,10 +608,7 @@ def main(): state=dict(type='str', default='present', choices=['present', 'absent']), size=dict(type='int', default=4096), type=dict(type='str', default='RSA', choices=[ - 'RSA', 'DSA', 'ECC', - # FIXME: NO LONGER TRUE: x25519 is missing serialization functions: https://github.com/pyca/cryptography/issues/4386 - # FIXME: NO LONGER TRUE: x448 is also missing it: https://github.com/pyca/cryptography/pull/4580#issuecomment-437913340 - # 'X448', 'X25519', 'Ed448', 'Ed25519' + 'DSA', 'ECC', 'Ed25519', 'Ed448', 'RSA', 'X25519', 'X448' ]), curve=dict(type='str', choices=[ 'secp384r1', 'secp521r1', 'secp224r1', 'secp192r1', 'secp256k1', diff --git a/test/integration/targets/openssl_privatekey/tasks/impl.yml b/test/integration/targets/openssl_privatekey/tasks/impl.yml index a5128451f7c..86f444615a1 100644 --- a/test/integration/targets/openssl_privatekey/tasks/impl.yml +++ b/test/integration/targets/openssl_privatekey/tasks/impl.yml @@ -55,10 +55,6 @@ when: select_crypto_backend == 'pyopenssl' - set_fact: ecc_types: - # - curve: X448 - # min_cryptography_version: "2.5" - # - curve: X25519 - # min_cryptography_version: "2.0" - curve: secp384r1 openssl_name: secp384r1 min_cryptography_version: "0.5" @@ -143,6 +139,41 @@ label: "{{ item.curve }}" register: privatekey_ecc_idempotency +- block: + - name: Test other type generation + openssl_privatekey: + path: '{{ output_dir }}/privatekey-{{ item.type }}.pem' + type: "{{ item.type }}" + select_crypto_backend: '{{ select_crypto_backend }}' + when: cryptography_version.stdout is version(item.min_version, '>=') + loop: "{{ types }}" + loop_control: + label: "{{ item.type }}" + register: privatekey_t1_generate + + - name: Test other type generation (idempotency) + openssl_privatekey: + path: '{{ output_dir }}/privatekey-{{ item.type }}.pem' + type: "{{ item.type }}" + select_crypto_backend: '{{ select_crypto_backend }}' + when: cryptography_version.stdout is version(item.min_version, '>=') + loop: "{{ types }}" + loop_control: + label: "{{ item.type }}" + register: privatekey_t1_idempotency + + when: select_crypto_backend == 'cryptography' + vars: + types: + - type: X25519 + min_version: '2.5' + - type: Ed25519 + min_version: '2.6' + - type: Ed448 + min_version: '2.6' + - type: X448 + min_version: '2.6' + - name: Generate privatekey with passphrase openssl_privatekey: path: '{{ output_dir }}/privatekeypw.pem' diff --git a/test/integration/targets/openssl_privatekey/tests/validate.yml b/test/integration/targets/openssl_privatekey/tests/validate.yml index 7bb1fe454e6..c7762173282 100644 --- a/test/integration/targets/openssl_privatekey/tests/validate.yml +++ b/test/integration/targets/openssl_privatekey/tests/validate.yml @@ -105,6 +105,25 @@ loop_control: label: "{{ item.item.curve }}" +- name: Validate other type generation (just check changed) + assert: + that: + - item is changed + loop: "{{ privatekey_t1_generate.results }}" + when: "'skip_reason' not in item" + loop_control: + label: "{{ item.item.type }}" + + +- name: Validate other type generation idempotency + assert: + that: + - item is not changed + loop: "{{ privatekey_t1_idempotency.results }}" + when: "'skip_reason' not in item" + loop_control: + label: "{{ item.item.type }}" + - name: Validate passphrase changing assert: that: