From af9571495ba0938387f2256c53864462254dbc3f Mon Sep 17 00:00:00 2001 From: Austin Lucas Lake <53884490+austinlucaslake@users.noreply.github.com> Date: Thu, 2 May 2024 03:35:05 -0700 Subject: [PATCH] updated formating+documentation and added ability to specify multiple subkeys --- plugins/modules/gpg_keypair.py | 366 +++++++++++++++++---------------- 1 file changed, 186 insertions(+), 180 deletions(-) diff --git a/plugins/modules/gpg_keypair.py b/plugins/modules/gpg_keypair.py index 2aa95be9..f71bf034 100644 --- a/plugins/modules/gpg_keypair.py +++ b/plugins/modules/gpg_keypair.py @@ -15,7 +15,7 @@ author: "Austin Lucas Lake (@austinlucaslake)" short_description: Generate or delete GPG private and public keys version_added: 2.20.0 description: - - "This module allows one to generate or delete OpenSSH private and public keys using GnuPG (gpg)." + - "This module allows one to generate or delete GPG private and public keys using GnuPG (gpg)." requirements: - gpg >= 2.1 extends_documentation_fragment: @@ -36,59 +36,56 @@ options: choices: [ present, absent ] key_type: description: - - "Specifies the type of key to create. - Supported key types are V(RSA), V(DSA), V(ECDSA), V(EDDSA), and V(ECDH)." + - Specifies the type of key to create. type: str - choices: ['RSA', 'DSA', 'ECDSA', 'EDDSA', 'ECDH'] + choices: ['RSA', 'DSA', 'ECDSA', 'EDDSA'] key_length: description: - For non-ECC keys, this specifies the number of bits in the key to create. - For RSA keys, the minimum is V(1024), the maximum is V(4096), and the default is V(3072). - For DSA keys, the minimum is V(768), the maximum is V(3072), and the default is V(2048). - - Invalid values will automatically be saturated in the afforemented ranges for each respective key. + - As per gpg's behavior, values below the allowed ranges will be set to the respective defaults, and those above the allowed ranges will saturate at the maximum. - For ECC keys, this parameter will be ignored. type: int key_curve: description: - For ECC keys, this specifies the curve used to generate the keys. - - Supported key curves are V(cv25519), V(nistp256), V(nistp384), V(nistp521), V(brainpoolP256r1), V(brainpoolP384r1), V(brainpoolP512r1), and V(secp256k1). - - EDDSA keys can only be used with V(cv25519). - - Only EDDSA and ECDH keys support V(cv25519), and for both, V(cv25519) is the default. - - For ECDSA and ECDH, the default is V(brainpoolP512r1). + - EDDSA keys only support the V(ed25519) curve and they can only be generate using said curve. + - For ECDSA keys, the default is V(brainpoolP512r1). - For non-ECC keys, this parameter with be ignored. type: str - choices: ['cv25519', 'nistp256', 'nistp384', 'nistp521', 'brainpoolP256r1', 'brainpoolP384r1', 'brainpoolP512r1', 'secp256k1'] + choices: ['nistp256', 'nistp384', 'nistp521', 'brainpoolP256r1', 'brainpoolP384r1', 'brainpoolP512r1', 'secp256k1', 'ed25519'] key_usage: description: - Specifies usage(s) for key. - - Support usages are V(encrypt), V(sign), V(auth), V(cert). - V(cert) is given to all primary keys regardess, however can be used to only give V(vert) usage to a key. - If not usage is specified, the valid usages for the given key type with be assigned. - If O(state) is V(absent), this parameter is ignored. type: list elements: str choices: ['encrypt', 'sign', 'auth', 'cert'] - subkey_type: + subkeys: description: - - Similar to O(key_type), but also supports V(ELG). - type: str - default: EDDSA - choices: ['RSA', 'DSA', 'ECDSA', 'EDDSA', 'ECDH', 'ELG'] - subkey_length: - description: - - Similar to O(key_length). - - For ELG keys, the minimum is V(1024), the maximum is V(4096), and the default is V(3072)." - type: int - subkey_curve: - description: - - "Similar to O(key_curve)" - type: str - choices: ['cv25519', 'nistp256', 'nistp384', 'nistp521', 'brainpoolP256r1', 'brainpoolP384r1', 'brainpoolP512r1', 'secp256k1'] - key_usage: - description: - - Similar to O(key_usage), but does not support V(cert). - type: list[str] - choices: ['encrypt', 'sign', 'auth', 'cert'] + - List of subkeys with their own respective key types, lengths, curves, and usages. + - Similar to O(key_type), O(key_length), O(key_curve), and (key_usage). + - Supports ECDH and ELG keys. + - For both ECDH and ELG keys, the only supported usage is V(encrypt). + - For ECDH keys, the default curve is V(brainpoolP512r1). + - ECDH keys also support the V(cv25519) curve. + - For ELG keys, the minimum length is V(1024) bits, the maximum length is V(4096) bits, and the default length is V(3072) bits. + + type: list + elements: dict + options: + subkey_type: + type: str + subkey_length: + type: int + subkey_curve: + type: str + subkey_usage: + type: list + elements: str name: description: - Specifies a name for the key. @@ -104,38 +101,34 @@ options: passphrase: description: - Passphrase used to decrypt an existing private key or encrypt a newly generated private key. - - If O(state) is V(absent), this parameter is ignored. + - If O(state=absent), this parameter is ignored. type: str fingerprints: description: - Specifies keys to match against. - - Provided fingerprints will take priority over user-id "V(name) (V(comment)) ". - - If O(state) is V(absent), keys with the provided fingerprints will be deleted if found. - type: list[str] + - Provided fingerprints will take priority over user-id "O(name) (O(comment)) ". + - If O(state=absent), keys with the provided fingerprints will be deleted if found. + type: list + elements: str keyserver: description: - Specifies keyserver to upload key to. - - If O(state) is V(absent), this parameter will be ignored. + - If O(state=absent), this parameter will be ignored. type: str transient_key: description: - Allows key generation to use a faster, but less secure random number generator. type: bool default: False - return_fingerprints: - description: - - Allows for the return of fingerprint(s) for newly created or deleted keys(s) - type: bool - default: False ''' EXAMPLES = ''' -- name: Generate the default GPG keypair (Ed25519) +- name: Generate the default GPG keypair community.crypto.gpg_keypair: - name: Generate the default GPG keypair with a passphrase community.crypto.gpg_keypair: - passphrase: super_secret_password + passphrase: {{ passphrase }} - name: Generate a RSA GPG keypair with the default RSA size (2048 bits) community.crypto.gpg_keypair: @@ -149,49 +142,42 @@ EXAMPLES = ''' - name: Generate an ECC GPG keypair community.crypto.gpg_keypair: key_type: EDDSA - key_curve: cv25519 + key_curve: ed25519 - name: Generate a GPG keypair and with a subkey: community.crypto.gpg_keypair: - subkey_type: ECDH - subkey_curve: cv25519 + subkeys: + - { subkey_type: ECDH, subkey_curve: cv25519 } - name: Generate a GPG keypair with custom user-id: community.crypto.gpg_keypair: - name: Your Name - comment: Interesting comment. - email: example@email.com - -- name: Generate a GPG keypair and return fingerprint of new key - community.crypto.gpg_keypair: - return_fingerprints: true - register: gpg_keys + name: name + comment: comment + email: name@email.com - name: Delete GPG keypair(s) matching a specified user-id: community.crypto.gpg_keypair: - state: abscent - name: Your Name - comment: Interesting comment. - email: example@email.com + state: absent + name: name + comment: comment + email: name@email.com -- name: Delete GPG keypair(s) matching a specified fingerprint: +- name: Delete a GPG keypair matching a specified fingerprint: community.crypto.gpg_keypair: state: abscent fingerprints: - ABC123... - ''' RETURN = ''' -size: - description: Size (in bits) of the SSH private key. - returned: changed or success - type: int - sample: 4096 +changed: + description: Indicates if changes were made to GPG keyring. + type: bool + sample: True fingerprints: - description: Fingerprint(s) of newly created or deleted key(s) - return: changed and O(return_fingerprints=true) - type: list[str] + description: Fingerprint(s) of newly created or matched key(s). + type: list + elements: str sample: [ ABC123... ] ''' @@ -204,144 +190,154 @@ from ansible.module_utils.basic import AnsibleModule from ansible_collections.community.crypto.plugins.module_utils.gnupg.cli import GPGError from ansible_collections.community.crypto.plugins.plugin_utils.gnupg import GPGError -def validate_params(params): - if params['override'] and params['present'] and not (params['fingerprint'] or params['name'] or params['comment'] or params['email']): - raise GPGError, 'To override existing keys, please provide any combination of the `fingerprint`, `name`, `comment`, and `email` parameters.' - keys = ['key'] - if params['subkey_type']: - keys.append('subkey') - for key in keys: - if params[f'{key}_type'] == 'EDDSA': - if not params[f'{key}_usage']: params[f'{key}_usage'] = ['sign', 'auth'] - elif params[f'{key}_usage'] not in list(itertools.combinations(['sign', 'auth'])): - raise GPGError, f'Invalid {key}_usage for {params[f"{key}_type"]} {key}.' - if not params[f'{key}_curve'] or params[f'{key}_curve'] == 'cv25519': - params[f'{key}_curve'] = 'ed25519' - elif params[f'{key}_curve'] != 'cv25519': - raise GPGError, f'Invalid {key}_curve for {params[f"{key}_type"]} {key}.' - elif params[f'{key}_type'] == 'ECDH': - if not params[f'{key}_usage']: params[f'{key}_usage'] = ['encrypt'] - elif params[f'{key}_usage'] != ['encrypt']: - raise GPGError, f'Invalid {key}_usage for {params[f"{key}_type"]} {key}.' - if not params[f'{key}_curve']: params[f'{key}_curve'] = 'cv25519' - elif params[f'{key}_curve'] != 'cv25519': - raise GPGError, f'Invalid {key}_curve for {params[f"{key}_type"]} {key}.' - elif params[f'{key}_type'] == 'ECDSA': - if not params[f'{key}_usage']: params[f'{key}_usage'] = ['sign', 'auth'] - elif params[f'{key}_usage'] not in list(itertools.combinations(['sign', 'auth'])): - raise GPGError, f'Invalid {key}_usage for {params[f"{key}_type"]} {key}.' - if not params[f'{key}_curve']: params[f'{key}_curve'] = 'brainpoolp521r1' - elif params[f'{key}_curve'] not in ['nistp256', 'nistp384', 'nistp521', 'brainpoolP256r1', 'brainpoolP384r1', 'brainpoolP512r1', 'secp256k1']: - raise GPGError, f'Invalid {key}_curve for {params[f"{key}_type"]} {key}.' - elif params[f'{key}_type'] == 'RSA': - if not params[f'{key}_usage']: params = ['ecrypt', 'sign', 'auth'] - elif not params[f'{key}_usage'] not in list(itertools.combinatios(['ecrypt', 'sign', 'auth'])): - raise GPGError, f'Invalid {key}_usage for {params[f"{key}_type"]} {key}.' - if not params[f'{key}_length']: params[f'{key}_length'] = 3072 - elif not 1024 <= params[f'{key}_length'] < 4096: - params[f'{key}_length'] = min(max(params[f'{key}_length'], 1024), 4096) - elif params[f'{key}_type'] == 'DSA': - if not params[f'{key}_usage']: params[f'{key}_usage'] = ['sign', 'auth'] - elif params[f'{key}_usage'] not in list(itertools.combinations(['sign', 'auth'])): - raise GPGError, f'Invalid {key}_usage for {params[f"{key}_type"]} {key}.' - if not params[f'{key}_length']: params[f'{key}_length'] = 2048 - elif not 768 <= params[f'{key}_length'] < 3072: - params[f'{key}_length'] = min(max(params[f'{key}_length'], 768), 3072) - elif params[f'{key}_type'] == 'ELG': - if params[f'{key}_type'] == params['key_type']: - raise GPGError, f'Invalid algorithm for {key}_type parameter.' - if not params[f'{key}_usage']: params[f'{key}_usage'] = ['encrypt'] - elif params[f'{key}_usage'] != ['encrypt']: - raise GPGError, f'Invalid {key}_iusage for {params[f"{key}_type"]} {key}.' - if not params[f'{key}_length']: params[f'{key}_length'] = 3072 - elif not 1024 <= params[f'{key}_length'] < 4096: - params[f'{key}_length'] = min(max(params[f'{key}_length'], 1024), 4096) +def validate_key(key_type, key_length, key_curve, key_usage, key_name = 'primary key'): + if key_type == 'EDDSA': + if key_curve and key_curve != 'ed25519': + raise GPGError('Invalid curve for {} {}.'.format(key_type, key_name)) + elif: + raise GPGError('No curve provided for {} {}.'.format(key_type, key_name)) + elif key_usage and key_usage not in list(itertools.combinations(['sign', 'auth'])): + raise GPGError('Invalid usage for {} {}.'.format(key_type, key_name)) + elif key_type == 'ECDH': + if key_name = 'primary key': + raise GPGError('Invalid type for {}.'.format(key_name)) + elif key_usage and key_usage != ['encrypt']: + raise GPGError('Invalid usage for {} {}.'.format(key_type, key_name)) + elif not key_curve: + raise GPGError('No curve provided for {} {}.'.format(key_type, key_name)) + elif key_type == 'ECDSA': + if key_curve and key_curve not in ['nistp256', 'nistp384', 'nistp521', 'brainpoolP256r1', 'brainpoolP384r1', 'brainpoolP512r1', 'secp256k1']: + raise GPGError('Invalid curve for {} {}.'.format(key_type, key_name)) + elif not key_curve: + raise GPGError('No curve provided for {} {}.'.format(key_type, key_name)) + elif key_usage and key_usage not in list(itertools.combinations(['sign', 'auth'])): + raise GPGError('Invalid usage for {} {}.'.format(key_type, key_name)) + elif key_type == 'RSA': + if key_usage and key_usage not in list(itertools.combinatios(['ecrypt', 'sign', 'auth'])): + raise GPGError('Invalid usage for {} {}.'.format(key_type, key_name)) + elif key_type == 'DSA': + if key_usage and key_usage not in list(itertools.combinations(['sign', 'auth'])): + raise GPGError('Invalid usage for {} {}.'.format(key_type, key_name)) + elif key_type == 'ELG': + if key_name == 'primary key': + raise GPGError('Invalid type for {}.'.format(key_name)) + elif key_usage != ['encrypt']: + raise GPGError('Invalid usage for {} {}.'.format(key_type, key_name)) + + +def validate_params(params): + validate_key(params['key_type'], params['key_length'], params['key_curve'], params['key_usage']) + for index, subkey in enumerate(params['subkeys']): + validate_key(subkey['subkey_type'], subkey['subkey_length'], subkey['subkey_curve'], subkey['subkey_usage'], ('subkey #{}').format(index+1)) + def list_matching_keys(name, comment, email, fingerprint): - user_id = "" + user_id = '' if params['name']: - user_id += f'{params["name"]} ' + user_id += '{} '.format(params["name"]) if params['comment']: - user_id += f'({params["comment"]}) ' + user_id += '({}) '.format(params["comment"]) if params['email']: - user_id += f'<{params["email"]}>' + user_id += '<{}>'.format(params["email"]) if user_id: - user_id = f'"{user_id.strip()}"' + user_id = '"{}"'.format(user_id.strip()) if user_id or fingerprints: - _, stdout, _ = gpg_runner.run_command(['gpg', '--batch', '--list-secret-keys', f'{*fingerprints if fingerprints else user_id}']) + _, stdout, _ = gpg_runner.run_command(['gpg', '--batch', '--list-secret-keys', '{}'.format(*fingerprints if fingerprints else user_id)]) lines = stdout.split('\n') matching_keys = [line.strip() for line in lines if line.strip().isalnum()] + for key in matching_keys: + # TODO: match based on key_type, key_usage, key_curve, and subkeys + pass return matching_keys return [] + def delete_keypair(gpg_runner, matching_keys, check_mode): if matching_keys: gpg_runner.run_command([ - f'{"dry-run" if check_mode else ""}', + '--dry-run' if check_mode else '', '--batch', '--yes', '--delete-secret-and-public-key', *matching_keys ], check_rc=True) - if params['return_fingerprints']: - return dict(changed=True, fingerprints=matching_keys) - return dict(changed=True, fingerprints=[]) + return dict(changed=True, fingerprints=matching_keys) return dict(changed=False, fingerprints=[]) + +def add_subkey(gpg_runner, fingerprint, subkey_index, subkey_type, subkey_length, subkey_curve, subkey_usage, subkey_index): + if subkey_type in ['RSA', 'DSA'. 'ELG']: + algo = '{}'.format(subkey_type.lower()) + if subkey_length: + algo += str(subkey_length) + elif subkey_curve: + algo = subkey_curve + else: + algo = None + gpg_runner.run_command([ + '--batch', '--quick-add-key', fingerprint, algo if algo else 'default', *usage, expire_date if expire_date else 0 + ]) + else: + raise GPGError('No algorithm applied for subkey #{}'.format(subkey_index+1)) + + def generate_keypair(gpg_runner, params, matching_keys, check_mode): if matching_keys: - if params['return_fingerprints']: - return dict(changed=False, fingerprints=matching_keys) - return dict(change=False, fingerprints=[]) + return dict(changed=False, fingerprints=matching_keys) - parameters = f"""<