From 21c8650180f3ad5fd248a24a116a672805ce4dce Mon Sep 17 00:00:00 2001 From: Jakob Ackermann Date: Mon, 1 Apr 2019 13:18:33 +0200 Subject: [PATCH] openssh_cert: add serial_number param (#54653) * [openssh_cert] cleanup the returned certificate info - Drop the certificate path - it is already present in rc.filename. - Drop the leading whitespace for all lines. Signed-off-by: Jakob Ackermann * [openssh_cert] add support for a certificate serial number Signed-off-by: Jakob Ackermann * [openssh_cert] fix lint error Signed-off-by: Jakob Ackermann * [openssh_cert] drop explicit default value Signed-off-by: Jakob Ackermann * [openssh_cert] enforce the specified or missing serial number Signed-off-by: Jakob Ackermann * [openssh_cert] passing no explicit serial number ignores any present one Signed-off-by: Jakob Ackermann --- lib/ansible/modules/crypto/openssh_cert.py | 32 ++++- .../targets/openssh_cert/tasks/main.yml | 111 ++++++++++++++++++ 2 files changed, 138 insertions(+), 5 deletions(-) diff --git a/lib/ansible/modules/crypto/openssh_cert.py b/lib/ansible/modules/crypto/openssh_cert.py index 00e6d61f75..d2aa2a68a7 100644 --- a/lib/ansible/modules/crypto/openssh_cert.py +++ b/lib/ansible/modules/crypto/openssh_cert.py @@ -108,6 +108,14 @@ options: description: - Specify the key identity when signing a public key. The identifier that is logged by the server when the certificate is used for authentication. type: str + serial_number: + description: + - "Specify the certificate serial number. + The serial number is logged by the server when the certificate is used for authentication. + The certificate serial number may be used in a KeyRevocationList. + The serial number may be omitted for checks, but must be specified again for a new certificate. + Note: The default value set by ssh-keygen is 0." + type: int extends_documentation_fragment: files ''' @@ -216,6 +224,7 @@ class Certificate(object): self.public_key = module.params['public_key'] self.path = module.params['path'] self.identifier = module.params['identifier'] + self.serial_number = module.params['serial_number'] self.valid_from = module.params['valid_from'] self.valid_to = module.params['valid_to'] self.valid_at = module.params['valid_at'] @@ -290,6 +299,9 @@ class Certificate(object): else: args.extend(['-I', ""]) + if self.serial_number is not None: + args.extend(['-z', str(self.serial_number)]) + if self.principals: args.extend(['-n', ','.join(self.principals)]) @@ -377,6 +389,7 @@ class Certificate(object): if principals == ["(none)"]: principals = None cert_type = re.findall("( user | host )", proc[1])[0].strip() + serial_number = re.search(r"Serial: (\d+)", proc[1]).group(1) validity = re.findall("(from (\\d{4}-\\d{2}-\\d{2}T\\d{2}(:\\d{2}){2}) to (\\d{4}-\\d{2}-\\d{2}T\\d{2}(:\\d{2}){2}))", proc[1]) if validity: if validity[0][1]: @@ -402,6 +415,11 @@ class Certificate(object): file_args = module.load_file_common_arguments(module.params) return not module.set_fs_attributes_if_different(file_args, False) + def _check_serial_number(): + if self.serial_number is None: + return True + return self.serial_number == int(serial_number) + def _check_type(): return self.type == cert_type @@ -441,10 +459,10 @@ class Certificate(object): return False - if not perms_required: - return _check_type() and _check_principals() and _check_validity(module) + if perms_required and not _check_perms(module): + return False - return _check_perms(module) and _check_type() and _check_principals() and _check_validity(module) + return _check_type() and _check_principals() and _check_validity(module) and _check_serial_number() def dump(self): @@ -456,9 +474,12 @@ class Certificate(object): for word in arr: if word in keywords: concated.append(string) - string = "" - string += " " + word + string = word + else: + string += " " + word concated.append(string) + # drop the certificate path + concated.pop(0) return concated def format_cert_info(): @@ -512,6 +533,7 @@ def main(): public_key=dict(type='path'), path=dict(type='path', required=True), identifier=dict(type='str'), + serial_number=dict(type='int'), valid_from=dict(type='str'), valid_to=dict(type='str'), valid_at=dict(type='str'), diff --git a/test/integration/targets/openssh_cert/tasks/main.yml b/test/integration/targets/openssh_cert/tasks/main.yml index feae638c47..5e4606b9c6 100644 --- a/test/integration/targets/openssh_cert/tasks/main.yml +++ b/test/integration/targets/openssh_cert/tasks/main.yml @@ -239,6 +239,117 @@ - "clear" valid_from: "2001-01-21" valid_to: "2019-01-21" + - name: Generate cert without serial + openssh_cert: + type: user + signing_key: '{{ output_dir }}/id_key' + public_key: '{{ output_dir }}/id_key.pub' + path: '{{ output_dir }}/id_cert_no_serial' + valid_from: always + valid_to: forever + register: rc_no_serial_number + - name: check default serial + assert: + that: + - "'Serial: 0' in rc_no_serial_number.info" + msg: OpenSSH user certificate contains the default serial number. + - name: Generate cert without serial (idempotent) + openssh_cert: + type: user + signing_key: '{{ output_dir }}/id_key' + public_key: '{{ output_dir }}/id_key.pub' + path: '{{ output_dir }}/id_cert_no_serial' + valid_from: always + valid_to: forever + register: rc_no_serial_number_idempotent + - name: check idempotent + assert: + that: + - rc_no_serial_number_idempotent is not changed + msg: OpenSSH certificate generation without serial number is idempotent. + - name: Generate cert with serial 42 + openssh_cert: + type: user + signing_key: '{{ output_dir }}/id_key' + public_key: '{{ output_dir }}/id_key.pub' + path: '{{ output_dir }}/id_cert_serial_42' + valid_from: always + valid_to: forever + serial_number: 42 + register: rc_serial_number + - name: check serial 42 + assert: + that: + - "'Serial: 42' in rc_serial_number.info" + msg: OpenSSH user certificate contains the serial number from the params. + - name: Generate cert with serial 42 (idempotent) + openssh_cert: + type: user + signing_key: '{{ output_dir }}/id_key' + public_key: '{{ output_dir }}/id_key.pub' + path: '{{ output_dir }}/id_cert_serial_42' + valid_from: always + valid_to: forever + serial_number: 42 + register: rc_serial_number_idempotent + - name: check idempotent + assert: + that: + - rc_serial_number_idempotent is not changed + msg: OpenSSH certificate generation with serial number is idempotent. + - name: Generate cert with changed serial number + openssh_cert: + type: user + signing_key: '{{ output_dir }}/id_key' + public_key: '{{ output_dir }}/id_key.pub' + path: '{{ output_dir }}/id_cert_serial_42' + valid_from: always + valid_to: forever + serial_number: 1337 + register: rc_serial_number_changed + - name: check changed + assert: + that: + - rc_serial_number_changed is changed + msg: OpenSSH certificate regenerated upon serial number change. + - name: Generate cert with removed serial number + openssh_cert: + type: user + signing_key: '{{ output_dir }}/id_key' + public_key: '{{ output_dir }}/id_key.pub' + path: '{{ output_dir }}/id_cert_serial_42' + valid_from: always + valid_to: forever + serial_number: 0 + register: rc_serial_number_removed + - name: check changed + assert: + that: + - rc_serial_number_removed is changed + msg: OpenSSH certificate regenerated upon serial number removal. + - name: Generate a new cert with serial number + openssh_cert: + type: user + signing_key: '{{ output_dir }}/id_key' + public_key: '{{ output_dir }}/id_key.pub' + path: '{{ output_dir }}/id_cert_serial_ignore' + valid_from: always + valid_to: forever + serial_number: 42 + - name: Generate cert again, omitting the parameter serial_number (idempotent) + openssh_cert: + type: user + signing_key: '{{ output_dir }}/id_key' + public_key: '{{ output_dir }}/id_key.pub' + path: '{{ output_dir }}/id_cert_serial_ignore' + valid_from: always + valid_to: forever + register: rc_serial_number_ignored + - name: check idempotent + assert: + that: + - rc_serial_number_ignored is not changed + msg: OpenSSH certificate generation with omitted serial number is idempotent. - name: Remove certificate (check mode) openssh_cert: state: absent