From eea7bfc6bf55c95441245aa6d00ac1272453d304 Mon Sep 17 00:00:00 2001 From: Ajpantuso Date: Wed, 15 Sep 2021 02:53:53 -0400 Subject: [PATCH] openssh_cert - adding signature_algorithm option (#277) * Initial Commit * Update supported OpenSSH versions for RSA SHA-2 signed certs * Updating 'regenerate' documentation --- .../module_utils/openssh/backends/common.py | 5 +- plugins/module_utils/openssh/certificate.py | 5 ++ plugins/module_utils/openssh/utils.py | 3 +- plugins/modules/openssh_cert.py | 29 ++++++- .../openssh_cert/tests/key_idempotency.yml | 75 +++++++++++++++++++ 5 files changed, 113 insertions(+), 4 deletions(-) diff --git a/plugins/module_utils/openssh/backends/common.py b/plugins/module_utils/openssh/backends/common.py index bd28c534..fce51e79 100644 --- a/plugins/module_utils/openssh/backends/common.py +++ b/plugins/module_utils/openssh/backends/common.py @@ -160,7 +160,8 @@ class KeygenCommand(object): self._run_command = module.run_command def generate_certificate(self, certificate_path, identifier, options, pkcs11_provider, principals, - serial_number, signing_key_path, type, time_parameters, use_agent, **kwargs): + serial_number, signature_algorithm, signing_key_path, type, + time_parameters, use_agent, **kwargs): args = [self._bin_path, '-s', signing_key_path, '-P', '', '-I', identifier] if options: @@ -178,6 +179,8 @@ class KeygenCommand(object): args.extend(['-U']) if time_parameters.validity_string: args.extend(['-V', time_parameters.validity_string]) + if signature_algorithm: + args.extend(['-t', signature_algorithm]) args.append(certificate_path) return self._run_command(args, **kwargs) diff --git a/plugins/module_utils/openssh/certificate.py b/plugins/module_utils/openssh/certificate.py index ad3a4f57..06525f22 100644 --- a/plugins/module_utils/openssh/certificate.py +++ b/plugins/module_utils/openssh/certificate.py @@ -555,6 +555,11 @@ class OpensshCertificate(object): def signing_key(self): return to_text(self._cert_info.signing_key_fingerprint()) + @property + def signature_type(self): + signature_data = OpensshParser.signature_data(self.signature) + return to_text(signature_data['signature_type']) + @staticmethod def _parse_cert_info(pub_key_type, parser): cert_info = get_cert_info_object(pub_key_type) diff --git a/plugins/module_utils/openssh/utils.py b/plugins/module_utils/openssh/utils.py index 22aa7cf7..3728a144 100644 --- a/plugins/module_utils/openssh/utils.py +++ b/plugins/module_utils/openssh/utils.py @@ -201,8 +201,9 @@ class OpensshParser(object): signature_blob = parser.string() blob_parser = cls(signature_blob) - if signature_type == b'ssh-rsa': + if signature_type in (b'ssh-rsa', b'rsa-sha2-256', b'rsa-sha2-512'): # https://datatracker.ietf.org/doc/html/rfc4253#section-6.6 + # https://datatracker.ietf.org/doc/html/rfc8332#section-3 signature_data['s'] = cls._big_int(signature_blob, "big") elif signature_type == b'ssh-dss': # https://datatracker.ietf.org/doc/html/rfc4253#section-6.6 diff --git a/plugins/modules/openssh_cert.py b/plugins/modules/openssh_cert.py index 5f8820a6..c1bed019 100644 --- a/plugins/modules/openssh_cert.py +++ b/plugins/modules/openssh_cert.py @@ -49,7 +49,7 @@ options: - When C(fail) the task will fail if a certificate already exists at I(path) and does not match the module's options. - When C(partial_idempotence) an existing certificate will be regenerated based on - I(serial), I(type), I(valid_from), I(valid_to), I(valid_at), and I(principals). + I(serial), I(signature_algorithm), I(type), I(valid_from), I(valid_to), I(valid_at), and I(principals). - When C(full_idempotence) I(identifier), I(options), I(public_key), and I(signing_key) are also considered when compared against an existing certificate. - C(always) is equivalent to I(force=true). @@ -62,6 +62,26 @@ options: - always default: partial_idempotence version_added: 1.8.0 + signature_algorithm: + description: + - As of OpenSSH 8.2 the SHA-1 signature algorithm for RSA keys has been disabled and C(ssh) will refuse + host certificates signed with the SHA-1 algorithm. OpenSSH 8.1 made C(rsa-sha2-512) the default algorithm + when acting as a CA and signing certificates with a RSA key. However, for OpenSSH versions less than 8.1 + the SHA-2 signature algorithms, C(rsa-sha2-256) or C(rsa-sha2-512), must be specified using this option + if compatibility with newer C(ssh) clients is required. Conversely if hosts using OpenSSH version 8.2 + or greater must remain compatible with C(ssh) clients using OpenSSH less than 7.2, then C(ssh-rsa) + can be used when generating host certificates (a corresponding change to the sshd_config to add C(ssh-rsa) + to the C(CASignatureAlgorithms) keyword is also required). + - Using any value for this option with a non-RSA I(signing_key) will cause this module to fail. + - "Note: OpenSSH versions prior to 7.2 do not support SHA-2 signature algorithms for RSA keys and OpenSSH + versions prior to 7.3 do not support SHA-2 signature algorithms for certificates." + - See U(https://www.openssh.com/txt/release-8.2) for more information. + type: str + choices: + - ssh-rsa + - rsa-sha2-256 + - rsa-sha2-512 + version_added: 1.10.0 signing_key: description: - The path to the private openssh key that is used for signing the public key in order to generate the certificate. @@ -269,6 +289,7 @@ class Certificate(OpensshModule): self.public_key = self.module.params['public_key'] self.regenerate = self.module.params['regenerate'] if not self.module.params['force'] else 'always' self.serial_number = self.module.params['serial_number'] + self.signature_algorithm = self.module.params['signature_algorithm'] self.signing_key = self.module.params['signing_key'] self.state = self.module.params['state'] self.type = self.module.params['type'] @@ -366,6 +387,7 @@ class Certificate(OpensshModule): def _is_partially_valid(self): return all([ set(self.original_data.principals) == set(self.principals), + self.original_data.signature_type == self.signature_algorithm if self.signature_algorithm else True, self.original_data.serial == self.serial_number if self.serial_number is not None else True, self.original_data.type == self.type, self._compare_time_parameters(), @@ -425,7 +447,7 @@ class Certificate(OpensshModule): self.ssh_keygen.generate_certificate( key_copy, self.identifier, self.options, self.pkcs11_provider, self.principals, self.serial_number, - self.signing_key, self.type, self.time_parameters, self.use_agent, + self.signature_algorithm, self.signing_key, self.type, self.time_parameters, self.use_agent, environ_update=dict(TZ="UTC"), check_rc=True ) @@ -485,6 +507,8 @@ def get_cert_dict(data): result = data.to_dict() result.pop('nonce') + result['signature_algorithm'] = data.signature_type + return result @@ -503,6 +527,7 @@ def main(): default='partial_idempotence', choices=['never', 'fail', 'partial_idempotence', 'full_idempotence', 'always'] ), + signature_algorithm=dict(type='str', choices=['ssh-rsa', 'rsa-sha2-256', 'rsa-sha2-512']), signing_key=dict(type='path'), serial_number=dict(type='int'), state=dict(type='str', default='present', choices=['absent', 'present']), diff --git a/tests/integration/targets/openssh_cert/tests/key_idempotency.yml b/tests/integration/targets/openssh_cert/tests/key_idempotency.yml index 814dd203..8d3157d2 100644 --- a/tests/integration/targets/openssh_cert/tests/key_idempotency.yml +++ b/tests/integration/targets/openssh_cert/tests/key_idempotency.yml @@ -20,6 +20,81 @@ valid_from: always valid_to: forever +- block: + - name: Generate cert with updated signature algorithm + openssh_cert: + type: user + path: "{{ certificate_path }}" + public_key: "{{ public_key }}" + signing_key: "{{ signing_key }}" + signature_algorithm: rsa-sha2-256 + valid_from: always + valid_to: forever + register: updated_signature_algorithm + + - name: Assert signature algorithm update causes change + assert: + that: + - updated_signature_algorithm is changed + + - name: Generate cert with updated signature algorithm (idempotent) + openssh_cert: + type: user + path: "{{ certificate_path }}" + public_key: "{{ public_key }}" + signing_key: "{{ signing_key }}" + signature_algorithm: rsa-sha2-256 + valid_from: always + valid_to: forever + register: updated_signature_algorithm_idempotent + + - name: Assert signature algorithm update is idempotent + assert: + that: + - updated_signature_algorithm_idempotent is not changed + + - name: Generate cert with original signature algorithm + openssh_cert: + type: user + path: "{{ certificate_path }}" + public_key: "{{ public_key }}" + signing_key: "{{ signing_key }}" + signature_algorithm: ssh-rsa + valid_from: always + valid_to: forever + register: second_signature_algorithm + + - name: Assert second signature algorithm update causes change + assert: + that: + - second_signature_algorithm is changed + + - name: Omit signature algorithm + openssh_cert: + type: user + path: "{{ certificate_path }}" + public_key: "{{ public_key }}" + signing_key: "{{ signing_key }}" + valid_from: always + valid_to: forever + register: omitted_signature_algorithm + + - name: Assert omitted_signature_algorithm does not cause change + assert: + that: + - omitted_signature_algorithm is not changed + + - name: Revert to original certificate + openssh_cert: + type: user + path: "{{ certificate_path }}" + public_key: "{{ public_key }}" + signing_key: "{{ signing_key }}" + valid_from: always + valid_to: forever + regenerate: always + when: openssh_version is version("7.3", ">=") + - name: Generate cert with new signing key openssh_cert: type: user