From 1dcc135da564dfceab7eb8091caa9933a99d4ad3 Mon Sep 17 00:00:00 2001 From: Andrew Pantuso Date: Sun, 18 Sep 2022 20:10:29 -0400 Subject: [PATCH] feat: add private_key_format choices for openssh_keypair (#511) * feat: add private_key_format choices for openssh_keypair * chore: add changelog fragment --- ...ssh_keypair-private_key_format_options.yml | 4 ++ .../module_utils/openssh/backends/common.py | 22 +++++- .../openssh/backends/keypair_backend.py | 20 ++++++ plugins/modules/openssh_keypair.py | 16 ++++- .../tests/cryptography_backend.yml | 72 +++++++++++++++++++ 5 files changed, 130 insertions(+), 4 deletions(-) create mode 100644 changelogs/fragments/511-openssh_keypair-private_key_format_options.yml diff --git a/changelogs/fragments/511-openssh_keypair-private_key_format_options.yml b/changelogs/fragments/511-openssh_keypair-private_key_format_options.yml new file mode 100644 index 00000000..ce94d1c7 --- /dev/null +++ b/changelogs/fragments/511-openssh_keypair-private_key_format_options.yml @@ -0,0 +1,4 @@ +minor_changes: + - "openssh_keypair - added ``pkcs1``, ``pkcs8``, and ``ssh`` to the available choices + for the ``private_key_format`` option. + (https://github.com/ansible-collections/community.crypto/pull/511)." diff --git a/plugins/module_utils/openssh/backends/common.py b/plugins/module_utils/openssh/backends/common.py index e9ef469a..6e274a6d 100644 --- a/plugins/module_utils/openssh/backends/common.py +++ b/plugins/module_utils/openssh/backends/common.py @@ -219,10 +219,11 @@ class KeygenCommand(object): class PrivateKey(object): - def __init__(self, size, key_type, fingerprint): + def __init__(self, size, key_type, fingerprint, format=''): self._size = size self._type = key_type self._fingerprint = fingerprint + self._format = format @property def size(self): @@ -236,6 +237,10 @@ class PrivateKey(object): def fingerprint(self): return self._fingerprint + @property + def format(self): + return self._format + @classmethod def from_string(cls, string): properties = string.split() @@ -251,6 +256,7 @@ class PrivateKey(object): 'size': self._size, 'type': self._type, 'fingerprint': self._fingerprint, + 'format': self._format, } @@ -324,3 +330,17 @@ class PublicKey(object): 'comment': self._comment, 'public_key': self._data, } + + +def parse_private_key_format(path): + with open(path, 'r') as file: + header = file.readline().strip() + + if header == '-----BEGIN OPENSSH PRIVATE KEY-----': + return 'SSH' + elif header == '-----BEGIN PRIVATE KEY-----': + return 'PKCS8' + elif header == '-----BEGIN RSA PRIVATE KEY-----': + return 'PKCS1' + + return '' diff --git a/plugins/module_utils/openssh/backends/keypair_backend.py b/plugins/module_utils/openssh/backends/keypair_backend.py index f20910f3..95da8597 100644 --- a/plugins/module_utils/openssh/backends/keypair_backend.py +++ b/plugins/module_utils/openssh/backends/keypair_backend.py @@ -31,6 +31,7 @@ from ansible_collections.community.crypto.plugins.module_utils.openssh.backends. OpensshModule, PrivateKey, PublicKey, + parse_private_key_format, ) from ansible_collections.community.crypto.plugins.module_utils.openssh.utils import ( any_in, @@ -182,8 +183,13 @@ class KeypairBackend(OpensshModule): return all([ self.size == self.original_private_key.size, self.type == self.original_private_key.type, + self._private_key_valid_backend(), ]) + @abc.abstractmethod + def _private_key_valid_backend(self): + pass + @OpensshModule.trigger_change @OpensshModule.skip_if_check_mode def _generate(self): @@ -329,6 +335,9 @@ class KeypairBackendOpensshBin(KeypairBackend): except (IOError, OSError) as e: self.module.fail_json(msg=to_native(e)) + def _private_key_valid_backend(self): + return True + class KeypairBackendCryptography(KeypairBackend): def __init__(self, module): @@ -360,6 +369,8 @@ class KeypairBackendCryptography(KeypairBackend): "or for ed25519 keys" ) ) + else: + result = key_format.upper() return result @@ -386,6 +397,7 @@ class KeypairBackendCryptography(KeypairBackend): size=keypair.size, key_type=keypair.key_type, fingerprint=keypair.fingerprint, + format=parse_private_key_format(self.private_key_path) ) def _get_public_key(self): @@ -428,6 +440,14 @@ class KeypairBackendCryptography(KeypairBackend): except (IOError, OSError) as e: self.module.fail_json(msg=to_native(e)) + def _private_key_valid_backend(self): + # avoids breaking behavior and prevents + # automatic conversions with OpenSSH upgrades + if self.module.params['private_key_format'] == 'auto': + return True + + return self.private_key_format == self.original_private_key.format + def select_backend(module, backend): can_use_cryptography = HAS_OPENSSH_SUPPORT diff --git a/plugins/modules/openssh_keypair.py b/plugins/modules/openssh_keypair.py index 10ed6dbe..b7cbb823 100644 --- a/plugins/modules/openssh_keypair.py +++ b/plugins/modules/openssh_keypair.py @@ -66,14 +66,20 @@ options: version_added: 1.7.0 private_key_format: description: - - Used when a I(backend=cryptography) to select a format for the private key at the provided I(path). - - The only valid option currently is C(auto) which will match the key format of the installed OpenSSH version. + - Used when I(backend=cryptography) to select a format for the private key at the provided I(path). + - When set to C(auto) this module will match the key format of the installed OpenSSH version. - For OpenSSH < 7.8 private keys will be in PKCS1 format except ed25519 keys which will be in OpenSSH format. - For OpenSSH >= 7.8 all private key types will be in the OpenSSH format. + - Using this option when I(regenerate=partial_idempotence) or I(regenerate=full_idempotence) will cause + a new keypair to be generated if the private key's format does not match the value of I(private_key_format). + This module will not however convert existing private keys between formats. type: str default: auto choices: - auto + - pkcs1 + - pkcs8 + - ssh version_added: 1.7.0 backend: description: @@ -210,7 +216,11 @@ def main(): choices=['never', 'fail', 'partial_idempotence', 'full_idempotence', 'always'] ), passphrase=dict(type='str', no_log=True), - private_key_format=dict(type='str', default='auto', no_log=False, choices=['auto']), + private_key_format=dict( + type='str', + default='auto', + no_log=False, + choices=['auto', 'pkcs1', 'pkcs8', 'ssh']), backend=dict(type='str', default='auto', choices=['auto', 'cryptography', 'opensshbin']) ), supports_check_mode=True, diff --git a/tests/integration/targets/openssh_keypair/tests/cryptography_backend.yml b/tests/integration/targets/openssh_keypair/tests/cryptography_backend.yml index 62eb776d..3339ffe7 100644 --- a/tests/integration/targets/openssh_keypair/tests/cryptography_backend.yml +++ b/tests/integration/targets/openssh_keypair/tests/cryptography_backend.yml @@ -94,3 +94,75 @@ path: '{{ remote_tmp_dir }}/pem_encoded' backend: cryptography state: absent + +- name: Generate a private key with specified format + openssh_keypair: + path: '{{ remote_tmp_dir }}/private_key_format' + private_key_format: pkcs1 + backend: cryptography + +- name: Generate a private key with specified format (Idempotent) + openssh_keypair: + path: '{{ remote_tmp_dir }}/private_key_format' + private_key_format: pkcs1 + backend: cryptography + register: private_key_format_idempotent + +- name: Check that private key with specified format is idempotent + assert: + that: + - private_key_format_idempotent is not changed + +- name: Change to PKCS8 format + openssh_keypair: + path: '{{ remote_tmp_dir }}/private_key_format' + private_key_format: pkcs8 + backend: cryptography + register: private_key_format_pkcs8 + +- name: Check that format change causes regeneration + assert: + that: + - private_key_format_pkcs8 is changed + +- name: Change to PKCS8 format (Idempotent) + openssh_keypair: + path: '{{ remote_tmp_dir }}/private_key_format' + private_key_format: pkcs8 + backend: cryptography + register: private_key_format_pkcs8_idempotent + +- name: Check that private key with PKCS8 format is idempotent + assert: + that: + - private_key_format_pkcs8_idempotent is not changed + +- name: Change to SSH format + openssh_keypair: + path: '{{ remote_tmp_dir }}/private_key_format' + private_key_format: ssh + backend: cryptography + register: private_key_format_ssh + +- name: Check that format change causes regeneration + assert: + that: + - private_key_format_ssh is changed + +- name: Change to SSH format (Idempotent) + openssh_keypair: + path: '{{ remote_tmp_dir }}/private_key_format' + private_key_format: ssh + backend: cryptography + register: private_key_format_ssh_idempotent + +- name: Check that private key with SSH format is idempotent + assert: + that: + - private_key_format_ssh_idempotent is not changed + +- name: Remove private key with specified format + openssh_keypair: + path: '{{ remote_tmp_dir }}/private_key_format' + backend: cryptography + state: absent