Repository: ansible-collections/community.general

Issue:
java_cert module checks only alias
##### SUMMARY
please read https://github.com/ansible/ansible/pull/56778

##### ISSUE TYPE
- Bug Report

##### COMPONENT NAME
java_cert
It seems the code mentioned in the PR above never reached this repository even if it's a main feature

##### ANSIBLE VERSION
```
ansible 2.9.5
  config file = None
  configured module search path = ['/home/myusername/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/local/lib/python3.7/site-packages/ansible
  executable location = /usr/local/bin/ansible
  python version = 3.7.8 (heads/master-dirty:daa285d, Jul 28 2020, 20:00:50) [GCC 9.3.1 20200408 (Red Hat 9.3.1-2)]
```
but should not be limited to.


Discussion:
Files identified in the description:
* [`plugins/modules/system/java_cert.py`](https://github.com/ansible-collections/community.general/blob/master/plugins/modules/system/java_cert.py)

If these files are inaccurate, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
@genialmaniac: Greetings! Thanks for taking the time to open this issue. In order for the community to handle your issue effectively, we need a bit more information.

Here are the items we could not find in your description:
- ansible version

Please set the description of this issue with this template:
https://raw.githubusercontent.com/ansible/ansible/devel/.github/ISSUE_TEMPLATE/bug_report.md

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: issue_missing_data --->
cc @MorrisA @bcoca @d-little @flynn1973 @gforster @haad @kairoaraujo @marvin-sinister @mator @molekuul @ramooncamacho @wtcross
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
needs_contributor

Resolving patch:
diff --git a/plugins/modules/system/java_cert.py b/plugins/modules/system/java_cert.py
--- a/plugins/modules/system/java_cert.py
+++ b/plugins/modules/system/java_cert.py
@@ -10,6 +10,7 @@
 DOCUMENTATION = r'''
 ---
 module: java_cert
+
 short_description: Uses keytool to import/remove key from java keystore (cacerts)
 description:
   - This is a wrapper module around keytool, which can be used to import/remove
@@ -81,9 +82,12 @@
   state:
     description:
       - Defines action which can be either certificate import or removal.
+      - When state is present, the certificate will always idempotently be inserted
+        into the keystore, even if there already exists a cert alias that is different.
     type: str
     choices: [ absent, present ]
     default: present
+requirements: [openssl, keytool]
 author:
 - Adam Hamsik (@haad)
 '''
@@ -166,41 +170,143 @@
 '''
 
 import os
+import tempfile
+import random
+import string
 import re
 
+
 # import module snippets
 from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils.six.moves.urllib.parse import urlparse
+from ansible.module_utils.six.moves.urllib.request import getproxies
 
 
-def get_keystore_type(keystore_type):
+def _get_keystore_type_keytool_parameters(keystore_type):
     ''' Check that custom keystore is presented in parameters '''
     if keystore_type:
-        return " -storetype '%s'" % keystore_type
-    return ''
+        return ["-storetype", keystore_type]
+    return []
 
 
-def check_cert_present(module, executable, keystore_path, keystore_pass, alias, keystore_type):
+def _check_cert_present(module, executable, keystore_path, keystore_pass, alias, keystore_type):
     ''' Check if certificate with alias is present in keystore
         located at keystore_path '''
-    test_cmd = ("%s -noprompt -list -keystore '%s' -storepass '%s' "
-                "-alias '%s' %s") % (executable, keystore_path, keystore_pass, alias, get_keystore_type(keystore_type))
-
-    check_rc, dummy, dummy = module.run_command(test_cmd)
+    test_cmd = [
+        executable,
+        "-list",
+        "-keystore",
+        keystore_path,
+        "-alias",
+        alias,
+        "-rfc"
+    ]
+    test_cmd += _get_keystore_type_keytool_parameters(keystore_type)
+
+    (check_rc, stdout, dummy) = module.run_command(test_cmd, data=keystore_pass, check_rc=False)
     if check_rc == 0:
-        return True
-    return False
-
+        return (True, stdout)
+    return (False, '')
+
+
+def _get_certificate_from_url(module, executable, url, port, pem_certificate_output):
+    remote_cert_pem_chain = _download_cert_url(module, executable, url, port)
+    with open(pem_certificate_output, 'w') as f:
+        f.write(remote_cert_pem_chain)
+
+
+def _get_first_certificate_from_x509_file(module, pem_certificate_file, pem_certificate_output, openssl_bin):
+    """ Read a X509 certificate chain file and output the first certificate in the list """
+    extract_cmd = [
+        openssl_bin,
+        "x509",
+        "-in",
+        pem_certificate_file,
+        "-out",
+        pem_certificate_output
+    ]
+    (extract_rc, dummy, extract_stderr) = module.run_command(extract_cmd, check_rc=False)
+
+    if extract_rc != 0:
+        # trying der encoded file
+        extract_cmd += ["-inform", "der"]
+        (extract_rc, dummy, extract_stderr) = module.run_command(extract_cmd, check_rc=False)
+
+        if extract_rc != 0:
+            # this time it's a real failure
+            module.fail_json(msg="Internal module failure, cannot extract certificate, error: %s" % extract_stderr,
+                             rc=extract_rc, cmd=extract_cmd)
+
+    return extract_rc
+
+
+def _get_digest_from_x509_file(module, pem_certificate_file, openssl_bin):
+    """ Read a X509 certificate file and output sha256 digest using openssl """
+    # cleanup file before to compare
+    (dummy, tmp_certificate) = tempfile.mkstemp()
+    module.add_cleanup_file(tmp_certificate)
+    _get_first_certificate_from_x509_file(module, pem_certificate_file, tmp_certificate, openssl_bin)
+    dgst_cmd = [
+        openssl_bin,
+        "dgst",
+        "-r",
+        "-sha256",
+        tmp_certificate
+    ]
+    (dgst_rc, dgst_stdout, dgst_stderr) = module.run_command(dgst_cmd, check_rc=False)
+
+    if dgst_rc != 0:
+        module.fail_json(msg="Internal module failure, cannot compute digest for certificate, error: %s" % dgst_stderr,
+                         rc=dgst_rc, cmd=dgst_cmd)
+
+    return dgst_stdout.split(' ')[0]
+
+
+def _export_public_cert_from_pkcs12(module, executable, pkcs_file, alias, password, dest):
+    """ Runs keytools to extract the public cert from a PKCS12 archive and write it to a file. """
+    export_cmd = [
+        executable,
+        "-list",
+        "-keystore",
+        pkcs_file,
+        "-alias",
+        alias,
+        "-storetype",
+        "pkcs12",
+        "-rfc"
+    ]
+    (export_rc, export_stdout, export_err) = module.run_command(export_cmd, data=password, check_rc=False)
+
+    if export_rc != 0:
+        module.fail_json(msg="Internal module failure, cannot extract public certificate from pkcs12, error: %s" % export_err,
+                         rc=export_rc)
+
+    with open(dest, 'w') as f:
+        f.write(export_stdout)
+
+
+def get_proxy_settings(scheme='https'):
+    """ Returns a tuple containing (proxy_host, proxy_port). (False, False) if no proxy is found """
+    proxy_url = getproxies().get(scheme, '')
+    if not proxy_url:
+        return (False, False)
+    else:
+        parsed_url = urlparse(proxy_url)
+        if parsed_url.scheme:
+            (proxy_host, proxy_port) = parsed_url.netloc.split(':')
+        else:
+            (proxy_host, proxy_port) = parsed_url.path.split(':')
+        return (proxy_host, proxy_port)
 
-def import_cert_url(module, executable, url, port, keystore_path, keystore_pass, alias, keystore_type, trust_cacert):
-    ''' Import certificate from URL into keystore located at keystore_path '''
 
-    https_proxy = os.getenv("https_proxy")
+def build_proxy_options():
+    """ Returns list of valid proxy options for keytool """
+    (proxy_host, proxy_port) = get_proxy_settings()
     no_proxy = os.getenv("no_proxy")
 
-    proxy_opts = ''
-    if https_proxy is not None:
-        (proxy_host, proxy_port) = https_proxy.split(':')
-        proxy_opts = "-J-Dhttps.proxyHost=%s -J-Dhttps.proxyPort=%s" % (proxy_host, proxy_port)
+    proxy_opts = []
+    if proxy_host:
+        proxy_opts.extend(["-J-Dhttps.proxyHost=%s" % proxy_host, "-J-Dhttps.proxyPort=%s" % proxy_port])
 
         if no_proxy is not None:
             # For Java's nonProxyHosts property, items are separated by '|',
@@ -210,68 +316,48 @@ def import_cert_url(module, executable, url, port, keystore_path, keystore_pass,
 
             # The property name is http.nonProxyHosts, there is no
             # separate setting for HTTPS.
-            proxy_opts += " -J-Dhttp.nonProxyHosts='%s'" % non_proxy_hosts
+            proxy_opts.extend(["-J-Dhttp.nonProxyHosts=%s" % non_proxy_hosts])
+    return proxy_opts
 
-    fetch_cmd = "%s -printcert -rfc -sslserver %s %s:%d" % (executable, proxy_opts, url, port)
-    import_cmd = ("%s -importcert -noprompt -keystore '%s' "
-                  "-storepass '%s' -alias '%s' %s") % (executable, keystore_path,
-                                                       keystore_pass, alias,
-                                                       get_keystore_type(keystore_type))
-    if trust_cacert:
-        import_cmd = import_cmd + " -trustcacerts"
+
+def _download_cert_url(module, executable, url, port):
+    """ Fetches the certificate from the remote URL using `keytool -printcert...`
+          The PEM formatted string is returned """
+    proxy_opts = build_proxy_options()
+    fetch_cmd = [executable, "-printcert", "-rfc", "-sslserver"] + proxy_opts + ["%s:%d" % (url, port)]
 
     # Fetch SSL certificate from remote host.
-    dummy, fetch_out, dummy = module.run_command(fetch_cmd, check_rc=True)
+    (fetch_rc, fetch_out, fetch_err) = module.run_command(fetch_cmd, check_rc=False)
 
-    # Use remote certificate from remote host and import it to a java keystore
-    (import_rc, import_out, import_err) = module.run_command(import_cmd,
-                                                             data=fetch_out,
-                                                             check_rc=False)
-    diff = {'before': '\n', 'after': '%s\n' % alias}
-    if import_rc == 0:
-        module.exit_json(changed=True, msg=import_out,
-                         rc=import_rc, cmd=import_cmd, stdout=import_out,
-                         diff=diff)
-    else:
-        module.fail_json(msg=import_out, rc=import_rc, cmd=import_cmd,
-                         error=import_err)
+    if fetch_rc != 0:
+        module.fail_json(msg="Internal module failure, cannot download certificate, error: %s" % fetch_err,
+                         rc=fetch_rc, cmd=fetch_cmd)
+
+    return fetch_out
 
 
 def import_cert_path(module, executable, path, keystore_path, keystore_pass, alias, keystore_type, trust_cacert):
     ''' Import certificate from path into keystore located on
         keystore_path as alias '''
-    import_cmd = ("%s -importcert -noprompt -keystore '%s' "
-                  "-storepass '%s' -file '%s' -alias '%s' %s") % (executable, keystore_path,
-                                                                  keystore_pass, path, alias,
-                                                                  get_keystore_type(keystore_type))
+    import_cmd = [
+        executable,
+        "-importcert",
+        "-noprompt",
+        "-keystore",
+        keystore_path,
+        "-file",
+        path,
+        "-alias",
+        alias
+    ]
+    import_cmd += _get_keystore_type_keytool_parameters(keystore_type)
 
     if trust_cacert:
-        import_cmd = import_cmd + " -trustcacerts"
-
-    # Use local certificate from local path and import it to a java keystore
-    (import_rc, import_out, import_err) = module.run_command(import_cmd,
-                                                             check_rc=False)
-
-    diff = {'before': '\n', 'after': '%s\n' % alias}
-    if import_rc == 0:
-        module.exit_json(changed=True, msg=import_out,
-                         rc=import_rc, cmd=import_cmd, stdout=import_out,
-                         error=import_err, diff=diff)
-    else:
-        module.fail_json(msg=import_out, rc=import_rc, cmd=import_cmd)
-
-
-def import_pkcs12_path(module, executable, path, keystore_path, keystore_pass, pkcs12_pass, pkcs12_alias, alias, keystore_type):
-    ''' Import pkcs12 from path into keystore located on
-        keystore_path as alias '''
-    import_cmd = ("%s -importkeystore -noprompt -destkeystore '%s' -srcstoretype PKCS12 "
-                  "-deststorepass '%s' -destkeypass '%s' -srckeystore '%s' -srcstorepass '%s' "
-                  "-srcalias '%s' -destalias '%s' %s") % (executable, keystore_path, keystore_pass,
-                                                          keystore_pass, path, pkcs12_pass, pkcs12_alias,
-                                                          alias, get_keystore_type(keystore_type))
+        import_cmd.extend(["-trustcacerts"])
 
     # Use local certificate from local path and import it to a java keystore
     (import_rc, import_out, import_err) = module.run_command(import_cmd,
+                                                             data="%s\n%s" % (keystore_pass, keystore_pass),
                                                              check_rc=False)
 
     diff = {'before': '\n', 'after': '%s\n' % alias}
@@ -283,19 +369,29 @@ def import_pkcs12_path(module, executable, path, keystore_path, keystore_pass, p
         module.fail_json(msg=import_out, rc=import_rc, cmd=import_cmd)
 
 
-def delete_cert(module, executable, keystore_path, keystore_pass, alias, keystore_type):
+def delete_cert(module, executable, keystore_path, keystore_pass, alias, keystore_type, exit_after=True):
     ''' Delete certificate identified with alias from keystore on keystore_path '''
-    del_cmd = ("%s -delete -keystore '%s' -storepass '%s' "
-               "-alias '%s' %s") % (executable, keystore_path, keystore_pass, alias, get_keystore_type(keystore_type))
+    del_cmd = [
+        executable,
+        "-delete",
+        "-noprompt",
+        "-keystore",
+        keystore_path,
+        "-alias",
+        alias
+    ]
+
+    del_cmd += _get_keystore_type_keytool_parameters(keystore_type)
 
     # Delete SSL certificate from keystore
-    (del_rc, del_out, del_err) = module.run_command(del_cmd, check_rc=True)
+    (del_rc, del_out, del_err) = module.run_command(del_cmd, data=keystore_pass, check_rc=True)
 
-    diff = {'before': '%s\n' % alias, 'after': None}
+    if exit_after:
+        diff = {'before': '%s\n' % alias, 'after': None}
 
-    module.exit_json(changed=True, msg=del_out,
-                     rc=del_rc, cmd=del_cmd, stdout=del_out,
-                     error=del_err, diff=diff)
+        module.exit_json(changed=True, msg=del_out,
+                         rc=del_rc, cmd=del_cmd, stdout=del_out,
+                         error=del_err, diff=diff)
 
 
 def test_keytool(module, executable):
@@ -333,7 +429,8 @@ def main():
 
     module = AnsibleModule(
         argument_spec=argument_spec,
-        required_one_of=[['cert_path', 'cert_url', 'pkcs12_path']],
+        required_if=[['state', 'present', ('cert_path', 'cert_url', 'pkcs12_path'), True],
+                     ['state', 'absent', ('cert_url', 'cert_alias'), True]],
         required_together=[['keystore_path', 'keystore_pass']],
         mutually_exclusive=[
             ['cert_url', 'cert_path', 'pkcs12_path']
@@ -359,6 +456,9 @@ def main():
     executable = module.params.get('executable')
     state = module.params.get('state')
 
+    # openssl dependency resolution
+    openssl_bin = module.get_bin_path('openssl', True)
+
     if path and not cert_alias:
         module.fail_json(changed=False,
                          msg="Using local path import from %s requires alias argument."
@@ -369,30 +469,61 @@ def main():
     if not keystore_create:
         test_keystore(module, keystore_path)
 
-    cert_present = check_cert_present(module, executable, keystore_path,
-                                      keystore_pass, cert_alias, keystore_type)
+    alias_exists, alias_exists_output = _check_cert_present(
+        module, executable, keystore_path, keystore_pass, cert_alias, keystore_type)
+
+    (dummy, new_certificate) = tempfile.mkstemp()
+    (dummy, old_certificate) = tempfile.mkstemp()
+    module.add_cleanup_file(new_certificate)
+    module.add_cleanup_file(old_certificate)
 
-    if state == 'absent' and cert_present:
+    if state == 'absent' and alias_exists:
         if module.check_mode:
             module.exit_json(changed=True)
 
+        # delete and exit
         delete_cert(module, executable, keystore_path, keystore_pass, cert_alias, keystore_type)
 
-    elif state == 'present' and not cert_present:
-        if module.check_mode:
-            module.exit_json(changed=True)
+    # dump certificate to enroll in the keystore on disk and compute digest
+    if state == 'present':
+        # The alias exists in the keystore so we must now compare the SHA256 hash of the
+        # public certificate already in the keystore, and the certificate we  are wanting to add
+        if alias_exists:
+            with open(old_certificate, "w") as f:
+                f.write(alias_exists_output)
+            keystore_cert_digest = _get_digest_from_x509_file(module, old_certificate, openssl_bin)
+
+        else:
+            keystore_cert_digest = ''
 
         if pkcs12_path:
-            import_pkcs12_path(module, executable, pkcs12_path, keystore_path,
-                               keystore_pass, pkcs12_pass, pkcs12_alias, cert_alias, keystore_type)
+            # Extracting certificate with openssl
+            _export_public_cert_from_pkcs12(module, executable, pkcs12_path, cert_alias, pkcs12_pass, new_certificate)
 
-        if path:
-            import_cert_path(module, executable, path, keystore_path,
-                             keystore_pass, cert_alias, keystore_type, trust_cacert)
+        elif path:
+            # Extracting the X509 digest is a bit easier. Keytool will print the PEM
+            # certificate to stdout so we don't need to do any transformations.
+            new_certificate = path
+
+        elif url:
+            # Getting the X509 digest from a URL is the same as from a path, we just have
+            # to download the cert first
+            _get_certificate_from_url(module, executable, url, port, new_certificate)
 
-        if url:
-            import_cert_url(module, executable, url, port, keystore_path,
-                            keystore_pass, cert_alias, keystore_type, trust_cacert)
+        new_cert_digest = _get_digest_from_x509_file(module, new_certificate, openssl_bin)
+
+        if keystore_cert_digest != new_cert_digest:
+
+            if module.check_mode:
+                module.exit_json(changed=True)
+
+            if alias_exists:
+                # The certificate in the keystore does not match with the one we want to be present
+                # The existing certificate must first be deleted before we insert the correct one
+                delete_cert(module, executable, keystore_path, keystore_pass, cert_alias, keystore_type, exit_after=False)
+
+            import_cert_path(module, executable, new_certificate, keystore_path,
+                             keystore_pass, cert_alias, keystore_type, trust_cacert)
 
     module.exit_json(changed=False)
 


Test patch:
diff --git a/tests/integration/targets/java_cert/defaults/main.yml b/tests/integration/targets/java_cert/defaults/main.yml
--- a/tests/integration/targets/java_cert/defaults/main.yml
+++ b/tests/integration/targets/java_cert/defaults/main.yml
@@ -1,3 +1,13 @@
 ---
 test_pkcs12_path: testpkcs.p12
-test_keystore_path: keystore.jks
\ No newline at end of file
+test_keystore_path: keystore.jks
+test_keystore2_path: "{{ output_dir }}/keystore2.jks"
+test_keystore2_password: changeit
+test_cert_path: "{{ output_dir }}/cert.pem"
+test_key_path: "{{ output_dir }}/key.pem"
+test_cert2_path: "{{ output_dir }}/cert2.pem"
+test_key2_path: "{{ output_dir }}/key2.pem"
+test_pkcs_path: "{{ output_dir }}/cert.p12"
+test_pkcs2_path: "{{ output_dir }}/cert2.p12"
+test_ssl: setupSSLServer.py
+test_ssl_port: 21500
\ No newline at end of file
diff --git a/tests/integration/targets/java_cert/files/setupSSLServer.py b/tests/integration/targets/java_cert/files/setupSSLServer.py
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/java_cert/files/setupSSLServer.py
@@ -0,0 +1,20 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+import ssl
+import os
+import sys
+
+root_dir = sys.argv[1]
+port = int(sys.argv[2])
+
+try:
+    from BaseHTTPServer import HTTPServer
+    from SimpleHTTPServer import SimpleHTTPRequestHandler
+except ModuleNotFoundError:
+    from http.server import HTTPServer, SimpleHTTPRequestHandler
+
+httpd = HTTPServer(('localhost', port), SimpleHTTPRequestHandler)
+httpd.socket = ssl.wrap_socket(httpd.socket, server_side=True,
+                               certfile=os.path.join(root_dir, 'cert.pem'),
+                               keyfile=os.path.join(root_dir, 'key.pem'))
+httpd.handle_request()
diff --git a/tests/integration/targets/java_cert/meta/main.yml b/tests/integration/targets/java_cert/meta/main.yml
--- a/tests/integration/targets/java_cert/meta/main.yml
+++ b/tests/integration/targets/java_cert/meta/main.yml
@@ -1,2 +1,3 @@
 dependencies:
   - setup_java_keytool
+  - setup_openssl
diff --git a/tests/integration/targets/java_cert/tasks/main.yml b/tests/integration/targets/java_cert/tasks/main.yml
--- a/tests/integration/targets/java_cert/tasks/main.yml
+++ b/tests/integration/targets/java_cert/tasks/main.yml
@@ -11,15 +11,16 @@
 
     - name: import pkcs12
       java_cert:
-         pkcs12_path: "{{output_dir}}/{{ test_pkcs12_path }}"
-         pkcs12_password: changeit
-         pkcs12_alias: default
-         cert_alias: default
-         keystore_path: "{{output_dir}}/{{ test_keystore_path }}"
-         keystore_pass: changeme_keystore
-         keystore_create: yes
-         state: present
+        pkcs12_path: "{{output_dir}}/{{ test_pkcs12_path }}"
+        pkcs12_password: changeit
+        pkcs12_alias: default
+        cert_alias: default
+        keystore_path: "{{output_dir}}/{{ test_keystore_path }}"
+        keystore_pass: changeme_keystore
+        keystore_create: yes
+        state: present
       register: result_success
+
     - name: verify success
       assert:
         that:
@@ -27,14 +28,14 @@
 
     - name: import pkcs12 with wrong password
       java_cert:
-         pkcs12_path: "{{output_dir}}/{{ test_pkcs12_path }}"
-         pkcs12_password: wrong_pass
-         pkcs12_alias: default
-         cert_alias: default_new
-         keystore_path: "{{output_dir}}/{{ test_keystore_path }}"
-         keystore_pass: changeme_keystore
-         keystore_create: yes
-         state: present
+        pkcs12_path: "{{output_dir}}/{{ test_pkcs12_path }}"
+        pkcs12_password: wrong_pass
+        pkcs12_alias: default
+        cert_alias: default_new
+        keystore_path: "{{output_dir}}/{{ test_keystore_path }}"
+        keystore_pass: changeme_keystore
+        keystore_create: yes
+        state: present
       ignore_errors: true
       register: result_wrong_pass
 
@@ -45,16 +46,62 @@
 
     - name: test fail on mutually exclusive params
       java_cert:
-         cert_path: ca.crt
-         pkcs12_path: "{{output_dir}}/{{ test_pkcs12_path }}"
-         cert_alias: default
-         keystore_path: "{{output_dir}}/{{ test_keystore_path }}"
-         keystore_pass: changeme_keystore
-         keystore_create: yes
-         state: present
+        cert_path: ca.crt
+        pkcs12_path: "{{output_dir}}/{{ test_pkcs12_path }}"
+        cert_alias: default
+        keystore_path: "{{output_dir}}/{{ test_keystore_path }}"
+        keystore_pass: changeme_keystore
+        keystore_create: yes
+        state: present
       ignore_errors: true
       register: result_excl_params
+
     - name: verify failed exclusive params
       assert:
         that:
         - result_excl_params is failed
+
+    - name: test fail on missing required params
+      java_cert:
+        keystore_path: "{{output_dir}}/{{ test_keystore_path }}"
+        keystore_pass: changeme_keystore
+        state: absent
+      ignore_errors: true
+      register: result_missing_required_param
+
+    - name: verify failed missing required params
+      assert:
+        that:
+        - result_missing_required_param is failed
+
+    - name: delete object based on cert_alias parameter
+      java_cert:
+        keystore_path: "{{output_dir}}/{{ test_keystore_path }}"
+        keystore_pass: changeme_keystore
+        cert_alias: default
+        state: absent
+      ignore_errors: true
+      register: result_alias_deleted
+
+    - name: verify object successfully deleted
+      assert:
+        that:
+        - result_alias_deleted is successful
+
+    - name: include extended test suite 
+      import_tasks: state_change.yml
+
+    - name: cleanup environment
+      file:
+        path: "{{ item }}"
+        state: absent
+      loop:
+        - "{{ output_dir }}/{{ test_pkcs12_path }}"
+        - "{{ output_dir }}/{{ test_keystore_path }}"
+        - "{{ test_keystore2_path }}"
+        - "{{ test_cert_path }}"
+        - "{{ test_key_path }}"
+        - "{{ test_cert2_path }}"
+        - "{{ test_key2_path }}"
+        - "{{ test_pkcs_path }}"
+        - "{{ test_pkcs2_path }}"
\ No newline at end of file
diff --git a/tests/integration/targets/java_cert/tasks/state_change.yml b/tests/integration/targets/java_cert/tasks/state_change.yml
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/java_cert/tasks/state_change.yml
@@ -0,0 +1,169 @@
+---
+- name: Generate the self signed cert used as a place holder to create the java keystore
+  command: openssl req -x509 -newkey rsa:4096 -keyout {{ test_key_path }} -out {{ test_cert_path }} -days 365 -nodes -subj '/CN=localhost'
+  args:
+    creates: "{{ test_key_path }}"
+
+- name: Create the test keystore
+  java_keystore:
+    name: placeholder
+    dest: "{{ test_keystore2_path }}"
+    password: "{{ test_keystore2_password }}"
+    private_key: "{{ lookup('file', '{{ test_key_path }}') }}"
+    certificate: "{{ lookup('file', '{{ test_cert_path }}') }}"
+
+- name: Generate the self signed cert we will use for testing
+  command: openssl req -x509 -newkey rsa:4096 -keyout '{{ test_key2_path }}' -out '{{ test_cert2_path }}' -days 365 -nodes -subj '/CN=localhost'
+  args:
+    creates: "{{ test_key2_path }}"
+
+- name: |
+    Import the newly created certificate. This is our main test.
+    If the java_cert has been updated properly, then this task will report changed each time
+    since the module will be comparing the hash of the certificate instead of validating that the alias
+    simply exists
+  java_cert:
+    cert_alias: test_cert
+    cert_path: "{{ test_cert2_path }}"
+    keystore_path: "{{ test_keystore2_path }}"
+    keystore_pass: "{{ test_keystore2_password }}"
+    state: present
+  register: result_x509_changed
+
+- name: Verify the x509 status has changed
+  assert:
+    that:
+      - result_x509_changed is changed
+
+- name: |
+    We also want to make sure that the status doesnt change if we import the same cert
+  java_cert:
+    cert_alias: test_cert
+    cert_path: "{{ test_cert2_path }}"
+    keystore_path: "{{ test_keystore2_path }}"
+    keystore_pass: "{{ test_keystore2_password }}"
+    state: present
+  register: result_x509_succeeded
+
+- name: Verify the x509 status is ok
+  assert:
+    that:
+      - result_x509_succeeded is succeeded
+
+- name: Create the pkcs12 archive from the test x509 cert
+  command: >
+    openssl pkcs12
+    -in {{ test_cert_path }}
+    -inkey {{ test_key_path }}
+    -export
+    -name test_pkcs12_cert
+    -out {{ test_pkcs_path }}
+    -passout pass:"{{ test_keystore2_password }}"
+
+- name: Create the pkcs12 archive from the certificate we will be trying to add to the keystore
+  command: >
+    openssl pkcs12
+    -in {{ test_cert2_path }}
+    -inkey {{ test_key2_path }}
+    -export
+    -name test_pkcs12_cert
+    -out {{ test_pkcs2_path }}
+    -passout pass:"{{ test_keystore2_password }}"
+
+- name: >
+    Ensure the original pkcs12 cert is in the keystore
+  java_cert:
+    cert_alias: test_pkcs12_cert
+    pkcs12_alias: test_pkcs12_cert
+    pkcs12_path: "{{ test_pkcs_path }}"
+    pkcs12_password: "{{ test_keystore2_password }}"
+    keystore_path: "{{ test_keystore2_path }}"
+    keystore_pass: "{{ test_keystore2_password }}"
+    state: present
+
+- name: |
+    Perform the same test, but we will now be testing the pkcs12 functionality
+    If we add a different pkcs12 cert with the same alias, we should have a chnaged result, NOT the same
+  java_cert:
+    cert_alias: test_pkcs12_cert
+    pkcs12_alias: test_pkcs12_cert
+    pkcs12_path: "{{ test_pkcs2_path }}"
+    pkcs12_password: "{{ test_keystore2_password }}"
+    keystore_path: "{{ test_keystore2_path }}"
+    keystore_pass: "{{ test_keystore2_password }}"
+    state: present
+  register: result_pkcs12_changed
+
+- name: Verify the pkcs12 status has changed
+  assert:
+    that:
+      - result_pkcs12_changed is changed
+
+- name: |
+    We are requesting the same cert now, so the status should show OK
+  java_cert:
+    cert_alias: test_pkcs12_cert
+    pkcs12_alias: test_pkcs12_cert
+    pkcs12_path: "{{ test_pkcs2_path }}"
+    pkcs12_password: "{{ test_keystore2_password }}"
+    keystore_path: "{{ test_keystore2_path }}"
+    keystore_pass: "{{ test_keystore2_password }}"
+  register: result_pkcs12_succeeded
+
+- name: Verify the pkcs12 status is ok
+  assert:
+    that:
+      - result_pkcs12_succeeded is succeeded
+
+- name: Copy the ssl server script
+  copy:
+    src: "setupSSLServer.py"
+    dest: "{{ output_dir }}"
+
+- name: Create an SSL server that we will use for testing URL imports
+  command: python {{ output_dir }}/setupSSLServer.py {{ output_dir }} {{ test_ssl_port }}
+  async: 10
+  poll: 0
+
+- name: |
+    Download the original cert.pem from our temporary server. The current cert should contain
+     cert2.pem. Importing this cert should return a status of changed
+  java_cert:
+    cert_alias: test_cert_localhost
+    cert_url: localhost
+    cert_port: "{{ test_ssl_port }}"
+    keystore_path: "{{ test_keystore2_path }}"
+    keystore_pass: "{{ test_keystore2_password }}"
+    state: present
+  register: result_url_changed
+
+- name: Verify that the url status is changed
+  assert:
+    that:
+      - result_url_changed is changed
+
+- name: Ensure we can remove the x509 cert
+  java_cert:
+    cert_alias: test_cert
+    keystore_path: "{{ test_keystore2_path }}"
+    keystore_pass: "{{ test_keystore2_password }}"
+    state: absent
+  register: result_x509_absent
+
+- name: Verify the x509 cert is absent
+  assert:
+    that:
+      - result_x509_absent is changed
+
+- name: Ensure we can remove the pkcs12 archive
+  java_cert:
+    cert_alias: test_pkcs12_cert
+    keystore_path: "{{ test_keystore2_path }}"
+    keystore_pass: "{{ test_keystore2_password }}"
+    state: absent
+  register: result_pkcs12_absent
+
+- name: Verify the pkcs12 archive is absent
+  assert:
+    that:
+      - result_pkcs12_absent is changed