openssh_keypair: Adding passphrase parameter (#225)

* Integrating openssh module utils with openssh_keypair

* Added explicit PEM formatting for OpenSSH < 7.8

* Adding changelog fragment

* Adding OpenSSL/cryptography dependency for integration tests

* Adding private_key_format option and removing forced cryptography update for CI

* Fixed version check for bcrypt and key_format option name

* Setting no_log=False for private_key_format

* Docs correction and simplification of control flow for private_key_format
pull/229/head
Ajpantuso 2021-05-10 08:47:01 -04:00 committed by GitHub
parent 37c1540ff4
commit 6100d9b4df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 395 additions and 69 deletions

View File

@ -0,0 +1,2 @@
minor_changes:
- openssh_keypair - Added ``passphrase`` paramter to openssh_keypair for encrypting/decrtypting OpenSSH private keys (https://github.com/ansible-collections/community.crypto/pull/225).

View File

@ -19,24 +19,23 @@ from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
from base64 import b64encode, b64decode from base64 import b64encode, b64decode
from distutils.version import LooseVersion
from getpass import getuser from getpass import getuser
from socket import gethostname from socket import gethostname
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
HAS_CRYPTOGRAPHY,
CRYPTOGRAPHY_HAS_ED25519,
)
try: try:
import cryptography as c
from cryptography.exceptions import InvalidSignature, UnsupportedAlgorithm from cryptography.exceptions import InvalidSignature, UnsupportedAlgorithm
from cryptography.hazmat.backends.openssl import backend from cryptography.hazmat.backends.openssl import backend
from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import dsa, ec, rsa, padding from cryptography.hazmat.primitives.asymmetric import dsa, ec, rsa, padding
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey
except ImportError:
pass
if HAS_CRYPTOGRAPHY and CRYPTOGRAPHY_HAS_ED25519: if LooseVersion(c.__version__) >= LooseVersion("3.0"):
HAS_OPENSSH_PRIVATE_FORMAT = True
else:
HAS_OPENSSH_PRIVATE_FORMAT = False
HAS_OPENSSH_SUPPORT = True HAS_OPENSSH_SUPPORT = True
_ALGORITHM_PARAMETERS = { _ALGORITHM_PARAMETERS = {
@ -76,7 +75,8 @@ if HAS_CRYPTOGRAPHY and CRYPTOGRAPHY_HAS_ED25519:
} }
} }
} }
else: except ImportError:
HAS_OPENSSH_PRIVATE_FORMAT = False
HAS_OPENSSH_SUPPORT = False HAS_OPENSSH_SUPPORT = False
_ALGORITHM_PARAMETERS = {} _ALGORITHM_PARAMETERS = {}
@ -192,12 +192,14 @@ class Asymmetric_Keypair(object):
) )
@classmethod @classmethod
def load(cls, path, passphrase=None, key_format='PEM'): def load(cls, path, passphrase=None, private_key_format='PEM', public_key_format='PEM', no_public_key=False):
"""Returns an Asymmetric_Keypair object loaded from the supplied file path """Returns an Asymmetric_Keypair object loaded from the supplied file path
:path: A path to an existing private key to be loaded :path: A path to an existing private key to be loaded
:passphrase: Secret of type bytes used to decrypt the private key being loaded :passphrase: Secret of type bytes used to decrypt the private key being loaded
:key_format: Format of key files to be loaded :private_key_format: Format of private key to be loaded
:public_key_format: Format of public key to be loaded
:no_public_key: Set 'True' to only load a private key and automatically populate the matching public key
""" """
if passphrase: if passphrase:
@ -205,8 +207,11 @@ class Asymmetric_Keypair(object):
else: else:
encryption_algorithm = serialization.NoEncryption() encryption_algorithm = serialization.NoEncryption()
privatekey = load_privatekey(path, passphrase, key_format) privatekey = load_privatekey(path, passphrase, private_key_format)
publickey = load_publickey(path + '.pub', key_format) if no_public_key:
publickey = privatekey.public_key()
else:
publickey = load_publickey(path + '.pub', public_key_format)
# Ed25519 keys are always of size 256 and do not have a key_size attribute # Ed25519 keys are always of size 256 and do not have a key_size attribute
if isinstance(privatekey, Ed25519PrivateKey): if isinstance(privatekey, Ed25519PrivateKey):
@ -352,11 +357,11 @@ class OpenSSH_Keypair(object):
:comment: Comment for a newly generated OpenSSH public key :comment: Comment for a newly generated OpenSSH public key
""" """
if not comment: if comment is None:
comment = "%s@%s" % (getuser(), gethostname()) comment = "%s@%s" % (getuser(), gethostname())
asym_keypair = Asymmetric_Keypair.generate(keytype, size, passphrase) asym_keypair = Asymmetric_Keypair.generate(keytype, size, passphrase)
openssh_privatekey = cls.encode_openssh_privatekey(asym_keypair) openssh_privatekey = cls.encode_openssh_privatekey(asym_keypair, 'SSH')
openssh_publickey = cls.encode_openssh_publickey(asym_keypair, comment) openssh_publickey = cls.encode_openssh_publickey(asym_keypair, comment)
fingerprint = calculate_fingerprint(openssh_publickey) fingerprint = calculate_fingerprint(openssh_publickey)
@ -365,20 +370,21 @@ class OpenSSH_Keypair(object):
openssh_privatekey=openssh_privatekey, openssh_privatekey=openssh_privatekey,
openssh_publickey=openssh_publickey, openssh_publickey=openssh_publickey,
fingerprint=fingerprint, fingerprint=fingerprint,
comment=comment comment=comment,
) )
@classmethod @classmethod
def load(cls, path, passphrase=None): def load(cls, path, passphrase=None, no_public_key=False):
"""Returns an Openssh_Keypair object loaded from the supplied file path """Returns an Openssh_Keypair object loaded from the supplied file path
:path: A path to an existing private key to be loaded :path: A path to an existing private key to be loaded
:passphrase: Secret used to decrypt the private key being loaded :passphrase: Secret used to decrypt the private key being loaded
:no_public_key: Set 'True' to only load a private key and automatically populate the matching public key
""" """
comment = extract_comment(path + '.pub') comment = extract_comment(path + '.pub')
asym_keypair = Asymmetric_Keypair.load(path, passphrase, 'SSH') asym_keypair = Asymmetric_Keypair.load(path, passphrase, 'SSH', 'SSH', no_public_key)
openssh_privatekey = cls.encode_openssh_privatekey(asym_keypair) openssh_privatekey = cls.encode_openssh_privatekey(asym_keypair, 'SSH')
openssh_publickey = cls.encode_openssh_publickey(asym_keypair, comment) openssh_publickey = cls.encode_openssh_publickey(asym_keypair, comment)
fingerprint = calculate_fingerprint(openssh_publickey) fingerprint = calculate_fingerprint(openssh_publickey)
@ -387,21 +393,31 @@ class OpenSSH_Keypair(object):
openssh_privatekey=openssh_privatekey, openssh_privatekey=openssh_privatekey,
openssh_publickey=openssh_publickey, openssh_publickey=openssh_publickey,
fingerprint=fingerprint, fingerprint=fingerprint,
comment=comment comment=comment,
) )
@staticmethod @staticmethod
def encode_openssh_privatekey(asym_keypair): def encode_openssh_privatekey(asym_keypair, key_format):
"""Returns an OpenSSH encoded private key for a given keypair """Returns an OpenSSH encoded private key for a given keypair
:asym_keypair: Asymmetric_Keypair from the private key is extracted :asym_keypair: Asymmetric_Keypair from the private key is extracted
:key_format: Format of the encoded private key.
""" """
# OpenSSH formatted private keys are not available in Cryptography <3.0 if key_format == 'SSH':
try: # Default to PEM format if SSH not available
privatekey_format = serialization.PrivateFormat.OpenSSH if not HAS_OPENSSH_PRIVATE_FORMAT:
except AttributeError:
privatekey_format = serialization.PrivateFormat.PKCS8 privatekey_format = serialization.PrivateFormat.PKCS8
else:
privatekey_format = serialization.PrivateFormat.OpenSSH
elif key_format == 'PKCS8':
privatekey_format = serialization.PrivateFormat.PKCS8
elif key_format == 'PKCS1':
if asym_keypair.key_type == 'ed25519':
raise InvalidKeyFormatError("ed25519 keys cannot be represented in PKCS1 format")
privatekey_format = serialization.PrivateFormat.TraditionalOpenSSL
else:
raise InvalidKeyFormatError("The accepted private key formats are SSH, PKCS8, and PKCS1")
encoded_privatekey = asym_keypair.private_key.private_bytes( encoded_privatekey = asym_keypair.private_key.private_bytes(
encoding=serialization.Encoding.PEM, encoding=serialization.Encoding.PEM,
@ -425,7 +441,7 @@ class OpenSSH_Keypair(object):
validate_comment(comment) validate_comment(comment)
encoded_publickey += (" %s" % comment).encode(encoding=_TEXT_ENCODING) encoded_publickey += (" %s" % comment).encode(encoding=_TEXT_ENCODING) if comment else b''
return encoded_publickey return encoded_publickey
@ -502,7 +518,7 @@ class OpenSSH_Keypair(object):
validate_comment(comment) validate_comment(comment)
self.__comment = comment self.__comment = comment
encoded_comment = (" %s" % self.__comment).encode(encoding=_TEXT_ENCODING) encoded_comment = (" %s" % self.__comment).encode(encoding=_TEXT_ENCODING) if self.__comment else b''
self.__openssh_publickey = b' '.join(self.__openssh_publickey.split(b' ', 2)[:2]) + encoded_comment self.__openssh_publickey = b' '.join(self.__openssh_publickey.split(b' ', 2)[:2]) + encoded_comment
return self.__openssh_publickey return self.__openssh_publickey
@ -513,7 +529,7 @@ class OpenSSH_Keypair(object):
""" """
self.__asym_keypair.update_passphrase(passphrase) self.__asym_keypair.update_passphrase(passphrase)
self.__openssh_privatekey = OpenSSH_Keypair.encode_openssh_privatekey(self.__asym_keypair) self.__openssh_privatekey = OpenSSH_Keypair.encode_openssh_privatekey(self.__asym_keypair, 'SSH')
def load_privatekey(path, passphrase, key_format): def load_privatekey(path, passphrase, key_format):
@ -549,6 +565,21 @@ def load_privatekey(path, passphrase, key_format):
) )
except ValueError as e: except ValueError as e:
# Revert to PEM if key could not be loaded in SSH format
if key_format == 'SSH':
try:
privatekey = privatekey_loaders['PEM'](
data=content,
password=passphrase,
backend=backend,
)
except ValueError as e:
raise InvalidPrivateKeyFileError(e)
except TypeError as e:
raise InvalidPassphraseError(e)
except UnsupportedAlgorithm as e:
raise InvalidAlgorithmError(e)
else:
raise InvalidPrivateKeyFileError(e) raise InvalidPrivateKeyFileError(e)
except TypeError as e: except TypeError as e:
raise InvalidPassphraseError(e) raise InvalidPassphraseError(e)
@ -645,4 +676,4 @@ def calculate_fingerprint(openssh_publickey):
decoded_pubkey = b64decode(openssh_publickey.split(b' ')[1]) decoded_pubkey = b64decode(openssh_publickey.split(b' ')[1])
digest.update(decoded_pubkey) digest.update(decoded_pubkey)
return b64encode(digest.finalize()).decode(encoding=_TEXT_ENCODING).rstrip('=') return 'SHA256:%s' % b64encode(digest.finalize()).decode(encoding=_TEXT_ENCODING).rstrip('=')

View File

@ -7,7 +7,6 @@
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
DOCUMENTATION = ''' DOCUMENTATION = '''
--- ---
module: openssh_keypair module: openssh_keypair
@ -19,6 +18,8 @@ description:
or C(ecdsa) private keys." or C(ecdsa) private keys."
requirements: requirements:
- "ssh-keygen" - "ssh-keygen"
- cryptography >= 2.6 (if using I(passphrase) and OpenSSH < 7.8 is installed)
- cryptography >= 3.0 (if using I(passphrase) and OpenSSH >= 7.8 is installed)
options: options:
state: state:
description: description:
@ -55,6 +56,23 @@ options:
description: description:
- Provides a new comment to the public key. - Provides a new comment to the public key.
type: str type: str
passphrase:
description:
- Passphrase used to decrypt an existing private key or encrypt a newly generated private key.
- Passphrases are not supported for I(type=rsa1).
type: str
version_added: 1.7.0
private_key_format:
description:
- Used when a value for I(passphrase) is provided 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.
- 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.
type: str
default: auto
choices:
- auto
version_added: 1.7.0
regenerate: regenerate:
description: description:
- Allows to configure in which situations the module is allowed to regenerate private keys. - Allows to configure in which situations the module is allowed to regenerate private keys.
@ -101,6 +119,11 @@ EXAMPLES = '''
community.crypto.openssh_keypair: community.crypto.openssh_keypair:
path: /tmp/id_ssh_rsa path: /tmp/id_ssh_rsa
- name: Generate an OpenSSH keypair with the default values (4096 bits, rsa) and encrypted private key
community.crypto.openssh_keypair:
path: /tmp/id_ssh_rsa
passphrase: super_secret_password
- name: Generate an OpenSSH rsa keypair with a different size (2048 bits) - name: Generate an OpenSSH rsa keypair with a different size (2048 bits)
community.crypto.openssh_keypair: community.crypto.openssh_keypair:
path: /tmp/id_ssh_rsa path: /tmp/id_ssh_rsa
@ -153,9 +176,19 @@ comment:
import errno import errno
import os import os
import stat import stat
from distutils.version import LooseVersion
from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import AnsibleModule, missing_required_lib
from ansible.module_utils._text import to_native from ansible.module_utils._text import to_native, to_text, to_bytes
from ansible_collections.community.crypto.plugins.module_utils.crypto.openssh import parse_openssh_version
from ansible_collections.community.crypto.plugins.module_utils.openssh.cryptography_openssh import (
HAS_OPENSSH_SUPPORT,
HAS_OPENSSH_PRIVATE_FORMAT,
InvalidPassphraseError,
InvalidPrivateKeyFileError,
OpenSSH_Keypair,
)
class KeypairError(Exception): class KeypairError(Exception):
@ -171,6 +204,7 @@ class Keypair(object):
self.size = module.params['size'] self.size = module.params['size']
self.type = module.params['type'] self.type = module.params['type']
self.comment = module.params['comment'] self.comment = module.params['comment']
self.passphrase = module.params['passphrase']
self.changed = False self.changed = False
self.check_mode = module.check_mode self.check_mode = module.check_mode
self.privatekey = None self.privatekey = None
@ -180,6 +214,44 @@ class Keypair(object):
if self.regenerate == 'always': if self.regenerate == 'always':
self.force = True self.force = True
# The empty string is intentionally ignored so that dependency checks do not cause unnecessary failure
if self.passphrase:
if not HAS_OPENSSH_SUPPORT:
module.fail_json(
msg=missing_required_lib(
'cryptography >= 2.6',
reason="to encrypt/decrypt private keys with passphrases"
)
)
if module.params['private_key_format'] == 'auto':
ssh = module.get_bin_path('ssh', True)
proc = module.run_command([ssh, '-Vq'])
ssh_version = parse_openssh_version(proc[2].strip())
self.private_key_format = 'SSH'
if LooseVersion(ssh_version) < LooseVersion("7.8") and self.type != 'ed25519':
# OpenSSH made SSH formatted private keys available in version 6.5,
# but still defaulted to PKCS1 format with the exception of ed25519 keys
self.private_key_format = 'PKCS1'
if self.private_key_format == 'SSH' and not HAS_OPENSSH_PRIVATE_FORMAT:
module.fail_json(
msg=missing_required_lib(
'cryptography >= 3.0',
reason="to load/dump private keys in the default OpenSSH format for OpenSSH >= 7.8 " +
"or for ed25519 keys"
)
)
if self.type == 'rsa1':
module.fail_json(msg="Passphrases are not supported for RSA1 keys.")
self.passphrase = to_bytes(self.passphrase)
else:
self.private_key_format = None
if self.type in ('rsa', 'rsa1'): if self.type in ('rsa', 'rsa1'):
self.size = 4096 if self.size is None else self.size self.size = 4096 if self.size is None else self.size
if self.size < 1024: if self.size < 1024:
@ -204,6 +276,12 @@ class Keypair(object):
def generate(self, module): def generate(self, module):
# generate a keypair # generate a keypair
if self.force or not self.isPrivateKeyValid(module, perms_required=False): if self.force or not self.isPrivateKeyValid(module, perms_required=False):
try:
if os.path.exists(self.path) and not os.access(self.path, os.W_OK):
os.chmod(self.path, stat.S_IWUSR + stat.S_IRUSR)
self.changed = True
if not self.passphrase:
args = [ args = [
module.get_bin_path('ssh-keygen', True), module.get_bin_path('ssh-keygen', True),
'-q', '-q',
@ -218,10 +296,6 @@ class Keypair(object):
else: else:
args.extend(['-C', ""]) args.extend(['-C', ""])
try:
if os.path.exists(self.path) and not os.access(self.path, os.W_OK):
os.chmod(self.path, stat.S_IWUSR + stat.S_IRUSR)
self.changed = True
stdin_data = None stdin_data = None
if os.path.exists(self.path): if os.path.exists(self.path):
stdin_data = 'y' stdin_data = 'y'
@ -230,13 +304,43 @@ class Keypair(object):
self.fingerprint = proc[1].split() self.fingerprint = proc[1].split()
pubkey = module.run_command([module.get_bin_path('ssh-keygen', True), '-yf', self.path]) pubkey = module.run_command([module.get_bin_path('ssh-keygen', True), '-yf', self.path])
self.public_key = pubkey[1].strip('\n') self.public_key = pubkey[1].strip('\n')
else:
keypair = OpenSSH_Keypair.generate(
keytype=self.type,
size=self.size,
passphrase=self.passphrase,
comment=self.comment if self.comment else "",
)
with open(self.path, 'w+b') as f:
f.write(
OpenSSH_Keypair.encode_openssh_privatekey(
keypair.asymmetric_keypair,
self.private_key_format
)
)
os.chmod(self.path, stat.S_IWUSR + stat.S_IRUSR)
with open(self.path + '.pub', 'w+b') as f:
f.write(keypair.public_key)
os.chmod(self.path + ".pub", stat.S_IWUSR + stat.S_IRUSR + stat.S_IRGRP + stat.S_IROTH)
self.fingerprint = [
str(keypair.size), keypair.fingerprint, keypair.comment, "(%s)" % keypair.key_type.upper()
]
self.public_key = to_text(b' '.join(keypair.public_key.split(b' ', 2)[:2]))
except Exception as e: except Exception as e:
self.remove() self.remove()
module.fail_json(msg="%s" % to_native(e)) module.fail_json(msg="%s" % to_native(e))
elif not self.isPublicKeyValid(module, perms_required=False): elif not self.isPublicKeyValid(module, perms_required=False):
if not self.passphrase:
pubkey = module.run_command([module.get_bin_path('ssh-keygen', True), '-yf', self.path]) pubkey = module.run_command([module.get_bin_path('ssh-keygen', True), '-yf', self.path])
pubkey = pubkey[1].strip('\n') pubkey = pubkey[1].strip('\n')
else:
keypair = OpenSSH_Keypair.load(
path=self.path,
passphrase=self.passphrase,
no_public_key=True,
)
pubkey = to_text(keypair.public_key)
try: try:
self.changed = True self.changed = True
with open(self.path + ".pub", "w") as pubkey_f: with open(self.path + ".pub", "w") as pubkey_f:
@ -252,9 +356,14 @@ class Keypair(object):
try: try:
if os.path.exists(self.path) and not os.access(self.path, os.W_OK): if os.path.exists(self.path) and not os.access(self.path, os.W_OK):
os.chmod(self.path, stat.S_IWUSR + stat.S_IRUSR) os.chmod(self.path, stat.S_IWUSR + stat.S_IRUSR)
if not self.passphrase:
args = [module.get_bin_path('ssh-keygen', True), args = [module.get_bin_path('ssh-keygen', True),
'-q', '-o', '-c', '-C', self.comment, '-f', self.path] '-q', '-o', '-c', '-C', self.comment, '-f', self.path]
module.run_command(args) module.run_command(args)
else:
keypair.comment = self.comment
with open(self.path + ".pub", "w+b") as pubkey_f:
pubkey_f.write(keypair.public_key + b'\n')
except IOError: except IOError:
module.fail_json( module.fail_json(
msg='Unable to update the comment for the public key.') msg='Unable to update the comment for the public key.')
@ -269,11 +378,26 @@ class Keypair(object):
def _check_pass_protected_or_broken_key(self, module): def _check_pass_protected_or_broken_key(self, module):
key_state = module.run_command([module.get_bin_path('ssh-keygen', True), key_state = module.run_command([module.get_bin_path('ssh-keygen', True),
'-P', '', '-yf', self.path], check_rc=False) '-P', '', '-yf', self.path], check_rc=False)
if not self.passphrase:
if key_state[0] == 255 or 'is not a public key file' in key_state[2]: if key_state[0] == 255 or 'is not a public key file' in key_state[2]:
return True return True
if 'incorrect passphrase' in key_state[2] or 'load failed' in key_state[2]: if 'incorrect passphrase' in key_state[2] or 'load failed' in key_state[2]:
return True return True
return False return False
else:
try:
OpenSSH_Keypair.load(
path=self.path,
passphrase=self.passphrase,
no_public_key=True,
)
except (InvalidPrivateKeyFileError, InvalidPassphraseError) as e:
return True
# Cryptography >= 3.0 uses a SSH key loader which does not raise an exception when a passphrase is provided
# when loading an unencrypted key so 'ssh-keygen' is used for this check
if key_state[0] == 0:
return True
return False
def isPrivateKeyValid(self, module, perms_required=True): def isPrivateKeyValid(self, module, perms_required=True):
@ -365,8 +489,16 @@ class Keypair(object):
pubkey_parts = _parse_pubkey(_get_pubkey_content()) pubkey_parts = _parse_pubkey(_get_pubkey_content())
if not self.passphrase:
pubkey = module.run_command([module.get_bin_path('ssh-keygen', True), '-yf', self.path]) pubkey = module.run_command([module.get_bin_path('ssh-keygen', True), '-yf', self.path])
pubkey = pubkey[1].strip('\n') pubkey = pubkey[1].strip('\n')
else:
keypair = OpenSSH_Keypair.load(
path=self.path,
passphrase=self.passphrase,
no_public_key=True,
)
pubkey = to_text(keypair.public_key)
if _pubkey_valid(pubkey): if _pubkey_valid(pubkey):
self.public_key = pubkey self.public_key = pubkey
else: else:
@ -438,6 +570,8 @@ def main():
default='partial_idempotence', default='partial_idempotence',
choices=['never', 'fail', 'partial_idempotence', 'full_idempotence', 'always'] 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'])
), ),
supports_check_mode=True, supports_check_mode=True,
add_file_common_args=True, add_file_common_args=True,

View File

@ -1,2 +1,3 @@
dependencies: dependencies:
- setup_ssh_keygen - setup_ssh_keygen
- setup_openssl

View File

@ -4,6 +4,9 @@
# and should not be used as examples of how to write Ansible roles # # and should not be used as examples of how to write Ansible roles #
#################################################################### ####################################################################
# Bumps up cryptography and bcrypt versions to be compatible with OpenSSH >= 7.8
- import_tasks: ./setup_bcrypt.yml
- name: Generate privatekey1 - standard (check mode) - name: Generate privatekey1 - standard (check mode)
openssh_keypair: openssh_keypair:
path: '{{ output_dir }}/privatekey1' path: '{{ output_dir }}/privatekey1'
@ -124,7 +127,7 @@
register: privatekey7_modified_result register: privatekey7_modified_result
- name: Generate password protected key - name: Generate password protected key
command: 'ssh-keygen -f {{ output_dir }}/privatekey8 -N password' command: 'ssh-keygen -f {{ output_dir }}/privatekey8 -N {{ passphrase }}'
- name: Try to modify the password protected key - should fail - name: Try to modify the password protected key - should fail
openssh_keypair: openssh_keypair:
@ -140,6 +143,58 @@
size: 2048 size: 2048
register: privatekey8_result_force register: privatekey8_result_force
- name: Generate another password protected key
command: 'ssh-keygen -f {{ output_dir }}/privatekey9 -N {{ passphrase }}'
- name: Try to modify the password protected key with passphrase
openssh_keypair:
path: '{{ output_dir }}/privatekey9'
size: 1024
passphrase: "{{ passphrase }}"
register: privatekey9_modified_result
when: cryptography_version.stdout is version('3.0', '>=') and bcrypt_version.stdout is version('3.1.5', '>=')
- name: Generate another unprotected key
openssh_keypair:
path: '{{ output_dir }}/privatekey10'
size: 2048
- name: Try to Modify unprotected key with passphrase
openssh_keypair:
path: '{{ output_dir }}/privatekey10'
size: 2048
passphrase: "{{ passphrase }}"
ignore_errors: true
register: privatekey10_result
when: cryptography_version.stdout is version('3.0', '>=') and bcrypt_version.stdout is version('3.1.5', '>=')
- name: Try to force modify the password protected key with force=true
openssh_keypair:
path: '{{ output_dir }}/privatekey10'
size: 2048
passphrase: "{{ passphrase }}"
force: true
register: privatekey10_result_force
when: cryptography_version.stdout is version('3.0', '>=') and bcrypt_version.stdout is version('3.1.5', '>=')
- name: Ensure that ssh-keygen can read keys generated with passphrase
command: 'ssh-keygen -yf {{ output_dir }}/privatekey10 -P {{ passphrase }}'
register: privatekey10_result_sshkeygen
when: cryptography_version.stdout is version('3.0', '>=') and bcrypt_version.stdout is version('3.1.5', '>=')
- name: Generate PEM encoded key with passphrase
command: 'ssh-keygen -f {{ output_dir }}/privatekey11 -N {{ passphrase }} -m PEM'
when: cryptography_version.stdout is version('3.0', '>=') and bcrypt_version.stdout is version('3.1.5', '>=')
- name: Try to verify a PEM encoded key
openssh_keypair:
path: '{{ output_dir }}/privatekey11'
size: 2048
passphrase: "{{ passphrase }}"
register: privatekey11_result
when: cryptography_version.stdout is version('3.0', '>=') and bcrypt_version.stdout is version('3.1.5', '>=')
- import_tasks: ../tests/validate.yml - import_tasks: ../tests/validate.yml
@ -152,8 +207,9 @@
size: 1024 size: 1024
loop: "{{ regenerate_values }}" loop: "{{ regenerate_values }}"
- name: Regenerate - setup password protected keys - name: Regenerate - setup password protected keys
command: 'ssh-keygen -f {{ output_dir }}/regenerate-b-{{ item }} -N password' command: 'ssh-keygen -f {{ output_dir }}/regenerate-b-{{ item }} -N {{ passphrase }}'
loop: "{{ regenerate_values }}" loop: "{{ regenerate_values }}"
- name: Regenerate - setup broken keys - name: Regenerate - setup broken keys
copy: copy:
dest: '{{ output_dir }}/regenerate-c-{{ item.0 }}{{ item.1 }}' dest: '{{ output_dir }}/regenerate-c-{{ item.0 }}{{ item.1 }}'
@ -162,6 +218,10 @@
with_nested: with_nested:
- "{{ regenerate_values }}" - "{{ regenerate_values }}"
- [ '', '.pub' ] - [ '', '.pub' ]
-
- name: Regenerate - setup password protected keys for passphrse test
command: 'ssh-keygen -f {{ output_dir }}/regenerate-d-{{ item }} -N {{ passphrase }}'
loop: "{{ regenerate_values }}"
- name: Regenerate - modify broken keys (check mode) - name: Regenerate - modify broken keys (check mode)
openssh_keypair: openssh_keypair:
@ -225,6 +285,29 @@
- result.results[3] is changed - result.results[3] is changed
- result.results[4] is changed - result.results[4] is changed
- name: Regenerate - modify password protected keys with passphrase (check mode)
openssh_keypair:
path: '{{ output_dir }}/regenerate-b-{{ item }}'
type: rsa
size: 1024
passphrase: "{{ passphrase }}"
regenerate: '{{ item }}'
check_mode: yes
loop: "{{ regenerate_values }}"
ignore_errors: yes
register: result
when: cryptography_version.stdout is version('3.0', '>=') and bcrypt_version.stdout is version('3.1.5', '>=')
- assert:
that:
- result.results[0] is success
- result.results[1] is failed
- "'Key has wrong type and/or size. Will not proceed.' in result.results[1].msg"
- result.results[2] is changed
- result.results[3] is changed
- result.results[4] is changed
when: cryptography_version.stdout is version('3.0', '>=') and bcrypt_version.stdout is version('3.1.5', '>=')
- name: Regenerate - modify password protected keys - name: Regenerate - modify password protected keys
openssh_keypair: openssh_keypair:
path: '{{ output_dir }}/regenerate-b-{{ item }}' path: '{{ output_dir }}/regenerate-b-{{ item }}'
@ -245,6 +328,28 @@
- result.results[3] is changed - result.results[3] is changed
- result.results[4] is changed - result.results[4] is changed
- name: Regenerate - modify password protected keys with passphrase
openssh_keypair:
path: '{{ output_dir }}/regenerate-d-{{ item }}'
type: rsa
size: 1024
passphrase: "{{ passphrase }}"
regenerate: '{{ item }}'
loop: "{{ regenerate_values }}"
ignore_errors: yes
register: result
when: cryptography_version.stdout is version('3.0', '>=') and bcrypt_version.stdout is version('3.1.5', '>=')
- assert:
that:
- result.results[0] is success
- result.results[1] is failed
- "'Key has wrong type and/or size. Will not proceed.' in result.results[1].msg"
- result.results[2] is changed
- result.results[3] is changed
- result.results[4] is changed
when: cryptography_version.stdout is version('3.0', '>=') and bcrypt_version.stdout is version('3.1.5', '>=')
- name: Regenerate - not modify regular keys (check mode) - name: Regenerate - not modify regular keys (check mode)
openssh_keypair: openssh_keypair:
path: '{{ output_dir }}/regenerate-a-{{ item }}' path: '{{ output_dir }}/regenerate-a-{{ item }}'

View File

@ -0,0 +1,24 @@
---
####################################################################
# WARNING: These are designed specifically for Ansible tests #
# and should not be used as examples of how to write Ansible roles #
####################################################################
- name: Attempt to install dependencies for OpenSSH > 7.8
block:
- name: Ensure bcrypt 3.1.5 available
become: true
pip:
name: bcrypt==3.1.5
extra_args: "-c {{ remote_constraints }}"
- name: Register bcrypt version
command: "{{ ansible_python.executable }} -c 'import bcrypt; print(bcrypt.__version__)'"
register: bcrypt_version
ignore_errors: true
- name: Ensure bcrypt_version is defined
set_fact:
bcrypt_version:
stdout: 0.0
when: bcrypt_version is not defined

View File

@ -138,3 +138,31 @@
assert: assert:
that: that:
- privatekey8_result_force is changed - privatekey8_result_force is changed
- block:
- name: Check that password protected key with passphrase was regenerated
assert:
that:
- privatekey9_modified_result is changed
- name: Check that modifying unprotected key with passphrase fails
assert:
that:
- privatekey10_result is failed
- "'Unable to read the key. The key is protected with a passphrase or broken.' in privatekey8_result.msg"
- name: Check that unprotected key was regenerated with force=yes and passphrase supplied
assert:
that:
- privatekey10_result_force is changed
- name: Check that ssh-keygen output from passphrase protected key matches openssh_keypair
assert:
that:
- privatekey10_result_force.public_key == privatekey10_result_sshkeygen.stdout
- name: Check that PEM encoded private keys are loaded successfully
assert:
that:
- privatekey11_result is success
when: cryptography_version.stdout is version('3.0', '>=') and bcrypt_version.stdout is version('3.1.5', '>=')

View File

@ -1,4 +1,5 @@
--- ---
passphrase: password
regenerate_values: regenerate_values:
- never - never
- fail - fail