Repository: ansible-collections/community.general

Issue:
java_keystore module is not working when private key has a passphrase
<!--- Verify first that your issue is not already reported on GitHub -->
<!--- Also test if the latest release and devel branch are affected too -->
<!--- Complete *all* sections as described, this form is processed automatically -->

##### SUMMARY
The java_keystore module does not work when my private certificate has a passphrase (without a passphrase, it is working as expected). It hangs waiting for the passphrase to be entered via stdin.

##### ISSUE TYPE
- Bug Report

##### COMPONENT NAME
java_keystore

##### ANSIBLE VERSION
<!--- Paste verbatim output from "ansible --version" between quotes -->
```
ansible 2.9.7
  config file = /home/fgiorget/.ansible.cfg
  configured module search path = ['/home/fgiorget/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /home/fgiorget/venv/lib/python3.7/site-packages/ansible
  executable location = /home/fgiorget/venv/bin/ansible
  python version = 3.7.6 (default, Jan 30 2020, 09:44:41) [GCC 9.2.1 20190827 (Red Hat 9.2.1-1)]
```

##### CONFIGURATION
<!--- Paste verbatim output from "ansible-config dump --only-changed" between quotes -->
```ANSIBLE_NOCOWS(/home/fgiorget/.ansible.cfg) = True
ANSIBLE_PIPELINING(/home/fgiorget/.ansible.cfg) = True
ANSIBLE_SSH_ARGS(/home/fgiorget/.ansible.cfg) = -C -o ForwardAgent=yes -o StrictHostKeyChecking=no -o ControlMaster=auto -o ControlPersist=600s -o PreferredAuthentications=publickey
ANSIBLE_SSH_CONTROL_PATH(/home/fgiorget/.ansible.cfg) = /tmp/ansible-ssh-%%h-%%p-%%r
DEFAULT_LOAD_CALLBACK_PLUGINS(/home/fgiorget/.ansible.cfg) = True
DEFAULT_LOG_PATH(/home/fgiorget/.ansible.cfg) = /tmp/ansible_logs
DEFAULT_PRIVATE_KEY_FILE(/home/fgiorget/.ansible.cfg) = /home/fgiorget/.ssh/cci.pem
DEFAULT_REMOTE_USER(/home/fgiorget/.ansible.cfg) = root
DEFAULT_ROLES_PATH(/home/fgiorget/.ansible.cfg) = ['/home/fgiorget/git/ansible_messagingqe/roles']
DEFAULT_SCP_IF_SSH(/home/fgiorget/.ansible.cfg) = smart
DEFAULT_SSH_TRANSFER_METHOD(/home/fgiorget/.ansible.cfg) = smart
DEFAULT_STDOUT_CALLBACK(/home/fgiorget/.ansible.cfg) = yaml
DEFAULT_TIMEOUT(/home/fgiorget/.ansible.cfg) = 100
HOST_KEY_CHECKING(/home/fgiorget/.ansible.cfg) = False
```

##### OS / ENVIRONMENT
<!--- Provide all relevant information below, e.g. target OS versions, network device firmware, etc. --
>
OS: Fedora 31

##### STEPS TO REPRODUCE
<!--- Describe exactly how to reproduce the problem, using a minimal test-case -->

<!--- Paste example playbooks or commands between quotes below -->
```- hosts: localhost
  tasks:
    - java_keystore:
        certificate: "{{lookup('file', '/tmp/cert.pem') }}"
        private_key: "{{lookup('file', '/tmp/cert.key') }}"
        password: mypassword
        dest: /tmp/myuser.keystore
        name: myalias 
        force: yes
```

<!--- HINT: You can paste gist.github.com links for larger files -->

##### EXPECTED RESULTS
<!--- Describe what you expected to happen when running the steps above -->
Keystore created and key/cert imported.

##### ACTUAL RESULTS
<!--- Describe what actually happened. If possible run with extra verbosity (-vvvv) -->
Ansible hangs as it waits for existing private key passphrase from stdin.

<!--- Paste verbatim command output between quotes -->
```TASK [java_keystore] **********************************************************************************************************************************************************************************************
Enter pass phrase for /tmp/cert.key:
```



Discussion:

Resolving patch:
diff --git a/plugins/modules/system/java_keystore.py b/plugins/modules/system/java_keystore.py
--- a/plugins/modules/system/java_keystore.py
+++ b/plugins/modules/system/java_keystore.py
@@ -32,6 +32,12 @@
         description:
           - Private key that should be used to create the key store.
         required: true
+    private_key_passphrase:
+        description:
+          - Pass phrase for reading the private key, if required.
+        type: str
+        required: false
+        version_added: "2.10"
     password:
         description:
           - Password that should be used to secure the key store.
@@ -115,7 +121,7 @@
 
 
 def read_certificate_fingerprint(module, openssl_bin, certificate_path):
-    current_certificate_fingerprint_cmd = "%s x509 -noout -in %s -fingerprint -sha256" % (openssl_bin, certificate_path)
+    current_certificate_fingerprint_cmd = [openssl_bin, "x509", "-noout", "-in", certificate_path, "-fingerprint", "-sha256"]
     (rc, current_certificate_fingerprint_out, current_certificate_fingerprint_err) = run_commands(module, current_certificate_fingerprint_cmd)
     if rc != 0:
         return module.fail_json(msg=current_certificate_fingerprint_out,
@@ -135,7 +141,7 @@ def read_certificate_fingerprint(module, openssl_bin, certificate_path):
 
 
 def read_stored_certificate_fingerprint(module, keytool_bin, alias, keystore_path, keystore_password):
-    stored_certificate_fingerprint_cmd = "%s -list -alias '%s' -keystore '%s' -storepass '%s' -v" % (keytool_bin, alias, keystore_path, keystore_password)
+    stored_certificate_fingerprint_cmd = [keytool_bin, "-list", "-alias", alias, "-keystore", keystore_path, "-storepass", keystore_password, "-v"]
     (rc, stored_certificate_fingerprint_out, stored_certificate_fingerprint_err) = run_commands(module, stored_certificate_fingerprint_cmd)
     if rc != 0:
         if "keytool error: java.lang.Exception: Alias <%s> does not exist" % alias not in stored_certificate_fingerprint_out:
@@ -157,8 +163,8 @@ def read_stored_certificate_fingerprint(module, keytool_bin, alias, keystore_pat
         return stored_certificate_match.group(1)
 
 
-def run_commands(module, cmd, check_rc=True):
-    return module.run_command(cmd, check_rc)
+def run_commands(module, cmd, data=None, check_rc=True):
+    return module.run_command(cmd, check_rc=check_rc, data=data)
 
 
 def create_file(path, content):
@@ -185,7 +191,7 @@ def cert_changed(module, openssl_bin, keytool_bin, keystore_path, keystore_pass,
         os.remove(certificate_path)
 
 
-def create_jks(module, name, openssl_bin, keytool_bin, keystore_path, password):
+def create_jks(module, name, openssl_bin, keytool_bin, keystore_path, password, keypass):
     if module.check_mode:
         module.exit_json(changed=True)
     else:
@@ -199,23 +205,33 @@ def create_jks(module, name, openssl_bin, keytool_bin, keystore_path, password):
             if os.path.exists(keystore_p12_path):
                 os.remove(keystore_p12_path)
 
-            export_p12_cmd = "%s pkcs12 -export -name '%s' -in '%s' -inkey '%s' -out '%s' -passout 'pass:%s'" % (
-                openssl_bin, name, certificate_path, private_key_path, keystore_p12_path, password)
-            (rc, export_p12_out, export_p12_err) = run_commands(module, export_p12_cmd)
+            export_p12_cmd = [openssl_bin, "pkcs12", "-export", "-name", name, "-in", certificate_path,
+                              "-inkey", private_key_path, "-out",
+                              keystore_p12_path, "-passout", "stdin"]
+
+            # when keypass is provided, add -passin
+            cmd_stdin = ""
+            if keypass:
+                export_p12_cmd.append("-passin")
+                export_p12_cmd.append("stdin")
+                cmd_stdin = "%s\n" % keypass
+
+            cmd_stdin += "%s\n%s" % (password, password)
+            (rc, export_p12_out, export_p12_err) = run_commands(module, export_p12_cmd, data=cmd_stdin)
             if rc != 0:
                 return module.fail_json(msg=export_p12_out,
                                         rc=rc,
                                         cmd=export_p12_cmd)
 
-            import_keystore_cmd = "%s -importkeystore " \
-                                  "-destkeystore '%s' " \
-                                  "-srckeystore '%s' " \
-                                  "-srcstoretype pkcs12 " \
-                                  "-alias '%s' " \
-                                  "-deststorepass '%s' " \
-                                  "-srcstorepass '%s' " \
-                                  "-noprompt" % (keytool_bin, keystore_path, keystore_p12_path, name, password, password)
-            (rc, import_keystore_out, import_keystore_err) = run_commands(module, import_keystore_cmd)
+            import_keystore_cmd = [keytool_bin, "-importkeystore",
+                                   "-destkeystore", keystore_path,
+                                   "-srckeystore", keystore_p12_path,
+                                   "-srcstoretype", "pkcs12",
+                                   "-alias", name,
+                                   "-deststorepass", password,
+                                   "-srcstorepass", password,
+                                   "-noprompt"]
+            (rc, import_keystore_out, import_keystore_err) = run_commands(module, import_keystore_cmd, data=None)
             if rc == 0:
                 update_jks_perm(module, keystore_path)
                 return module.exit_json(changed=True,
@@ -246,6 +262,7 @@ def update_jks_perm(module, keystore_path):
 def process_jks(module):
     name = module.params['name']
     password = module.params['password']
+    keypass = module.params['private_key_passphrase']
     keystore_path = module.params['dest']
     force = module.params['force']
     openssl_bin = module.get_bin_path('openssl', True)
@@ -253,16 +270,16 @@ def process_jks(module):
 
     if os.path.exists(keystore_path):
         if force:
-            create_jks(module, name, openssl_bin, keytool_bin, keystore_path, password)
+            create_jks(module, name, openssl_bin, keytool_bin, keystore_path, password, keypass)
         else:
             if cert_changed(module, openssl_bin, keytool_bin, keystore_path, password, name):
-                create_jks(module, name, openssl_bin, keytool_bin, keystore_path, password)
+                create_jks(module, name, openssl_bin, keytool_bin, keystore_path, password, keypass)
             else:
                 if not module.check_mode:
                     update_jks_perm(module, keystore_path)
                 return module.exit_json(changed=False)
     else:
-        create_jks(module, name, openssl_bin, keytool_bin, keystore_path, password)
+        create_jks(module, name, openssl_bin, keytool_bin, keystore_path, password, keypass)
 
 
 class ArgumentSpec(object):
@@ -275,7 +292,8 @@ def __init__(self):
             private_key=dict(required=True, no_log=True),
             password=dict(required=True, no_log=True),
             dest=dict(required=True),
-            force=dict(required=False, default=False, type='bool')
+            force=dict(required=False, default=False, type='bool'),
+            private_key_passphrase=dict(required=False, no_log=True, type='str')
         )
         self.argument_spec = argument_spec
 


Test patch:
diff --git a/tests/unit/plugins/modules/system/test_java_keystore.py b/tests/unit/plugins/modules/system/test_java_keystore.py
--- a/tests/unit/plugins/modules/system/test_java_keystore.py
+++ b/tests/unit/plugins/modules/system/test_java_keystore.py
@@ -64,19 +64,49 @@ def test_create_jks_success(self):
         module.exit_json = Mock()
 
         with patch('os.remove', return_value=True):
-            self.run_commands.side_effect = lambda args, kwargs: (0, '', '')
-            create_jks(module, "test", "openssl", "keytool", "/path/to/keystore.jks", "changeit")
+            self.run_commands.side_effect = lambda module, cmd, data: (0, '', '')
+            create_jks(module, "test", "openssl", "keytool", "/path/to/keystore.jks", "changeit", "")
             module.exit_json.assert_called_once_with(
                 changed=True,
-                cmd="keytool -importkeystore "
-                    "-destkeystore '/path/to/keystore.jks' "
-                    "-srckeystore '/tmp/keystore.p12' -srcstoretype pkcs12 -alias 'test' "
-                    "-deststorepass 'changeit' -srcstorepass 'changeit' -noprompt",
+                cmd=["keytool", "-importkeystore",
+                     "-destkeystore", "/path/to/keystore.jks",
+                     "-srckeystore", "/tmp/keystore.p12", "-srcstoretype", "pkcs12", "-alias", "test",
+                     "-deststorepass", "changeit", "-srcstorepass", "changeit", "-noprompt"],
                 msg='',
                 rc=0,
                 stdout_lines=''
             )
 
+    def test_create_jks_keypass_fail_export_pkcs12(self):
+        set_module_args(dict(
+            certificate='cert-foo',
+            private_key='private-foo',
+            private_key_passphrase='passphrase-foo',
+            dest='/path/to/keystore.jks',
+            name='foo',
+            password='changeit'
+        ))
+
+        module = AnsibleModule(
+            argument_spec=self.spec.argument_spec,
+            supports_check_mode=self.spec.supports_check_mode
+        )
+
+        module.fail_json = Mock()
+
+        with patch('os.remove', return_value=True):
+            self.run_commands.side_effect = [(1, '', ''), (0, '', '')]
+            create_jks(module, "test", "openssl", "keytool", "/path/to/keystore.jks", "changeit", "passphrase-foo")
+            module.fail_json.assert_called_once_with(
+                cmd=["openssl", "pkcs12", "-export", "-name", "test",
+                     "-in", "/tmp/foo.crt", "-inkey", "/tmp/foo.key",
+                     "-out", "/tmp/keystore.p12",
+                     "-passout", "stdin",
+                     "-passin", "stdin"],
+                msg='',
+                rc=1
+            )
+
     def test_create_jks_fail_export_pkcs12(self):
         set_module_args(dict(
             certificate='cert-foo',
@@ -95,12 +125,12 @@ def test_create_jks_fail_export_pkcs12(self):
 
         with patch('os.remove', return_value=True):
             self.run_commands.side_effect = [(1, '', ''), (0, '', '')]
-            create_jks(module, "test", "openssl", "keytool", "/path/to/keystore.jks", "changeit")
+            create_jks(module, "test", "openssl", "keytool", "/path/to/keystore.jks", "changeit", "")
             module.fail_json.assert_called_once_with(
-                cmd="openssl pkcs12 -export -name 'test' "
-                    "-in '/tmp/foo.crt' -inkey '/tmp/foo.key' "
-                    "-out '/tmp/keystore.p12' "
-                    "-passout 'pass:changeit'",
+                cmd=["openssl", "pkcs12", "-export", "-name", "test",
+                     "-in", "/tmp/foo.crt", "-inkey", "/tmp/foo.key",
+                     "-out", "/tmp/keystore.p12",
+                     "-passout", "stdin"],
                 msg='',
                 rc=1
             )
@@ -123,12 +153,12 @@ def test_create_jks_fail_import_key(self):
 
         with patch('os.remove', return_value=True):
             self.run_commands.side_effect = [(0, '', ''), (1, '', '')]
-            create_jks(module, "test", "openssl", "keytool", "/path/to/keystore.jks", "changeit")
+            create_jks(module, "test", "openssl", "keytool", "/path/to/keystore.jks", "changeit", "")
             module.fail_json.assert_called_once_with(
-                cmd="keytool -importkeystore "
-                    "-destkeystore '/path/to/keystore.jks' "
-                    "-srckeystore '/tmp/keystore.p12' -srcstoretype pkcs12 -alias 'test' "
-                    "-deststorepass 'changeit' -srcstorepass 'changeit' -noprompt",
+                cmd=["keytool", "-importkeystore",
+                     "-destkeystore", "/path/to/keystore.jks",
+                     "-srckeystore", "/tmp/keystore.p12", "-srcstoretype", "pkcs12", "-alias", "test",
+                     "-deststorepass", "changeit", "-srcstorepass", "changeit", "-noprompt"],
                 msg='',
                 rc=1
             )
@@ -231,7 +261,7 @@ def test_cert_changed_fail_read_cert(self):
             self.run_commands.side_effect = [(1, '', 'Oops'), (0, 'SHA256: wxyz:9876:stuv', '')]
             cert_changed(module, "openssl", "keytool", "/path/to/keystore.jks", "changeit", 'foo')
             module.fail_json.assert_called_once_with(
-                cmd="openssl x509 -noout -in /tmp/foo.crt -fingerprint -sha256",
+                cmd=["openssl", "x509", "-noout", "-in", "/tmp/foo.crt", "-fingerprint", "-sha256"],
                 msg='',
                 err='Oops',
                 rc=1
@@ -257,7 +287,7 @@ def test_cert_changed_fail_read_keystore(self):
             self.run_commands.side_effect = [(0, 'foo: wxyz:9876:stuv', ''), (1, '', 'Oops')]
             cert_changed(module, "openssl", "keytool", "/path/to/keystore.jks", "changeit", 'foo')
             module.fail_json.assert_called_with(
-                cmd="keytool -list -alias 'foo' -keystore '/path/to/keystore.jks' -storepass 'changeit' -v",
+                cmd=["keytool", "-list", "-alias", "foo", "-keystore", "/path/to/keystore.jks", "-storepass", "changeit", "-v"],
                 msg='',
                 err='Oops',
                 rc=1