Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Certificate passphrase check #238

Open
wants to merge 52 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 49 commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
d4e2a3f
Added skeleton of new object
danopt Aug 21, 2023
d274cd5
Revert commits
danopt Aug 28, 2023
a470fde
Revert commits
danopt Aug 28, 2023
c06d900
Corrected parameter
danopt Aug 28, 2023
0a2b827
Added parameter passphrase_check to module
danopt Aug 28, 2023
6dc347e
Catched wrong passphrases with an exception
danopt Aug 28, 2023
bb117f1
Added task in molecule tests for passphrase_check
danopt Aug 28, 2023
0bc0a59
Added condition to load certificate info only if passphrase_check is …
danopt Aug 28, 2023
7d3c4cb
Added condition to map certificate only if loaded
danopt Aug 28, 2023
6cd2807
Added passphrase_check parameter to molecule task
danopt Aug 28, 2023
d88212f
Added a condition to fail module if passphrase_check is false
danopt Aug 28, 2023
f39d88f
Set no_log for new parameter
danopt Aug 28, 2023
932b330
Privatized passphrase_check variable
danopt Aug 28, 2023
048704e
Privatized pkcs12_data variable
danopt Aug 28, 2023
da01eac
Added passphrase_check key value to result dict seeded for Ansible mo…
danopt Aug 28, 2023
0e81e58
Corrected key value for passphrase_check in results dict
danopt Aug 28, 2023
6267d87
Added task to molecule scenario to check correct passphrase in passph…
danopt Aug 28, 2023
ea70b6d
Set passphrase_check result if mode is disabled
danopt Aug 28, 2023
5b543cb
Only map certificate if passphrase_check mode is disabled
danopt Aug 28, 2023
630fb4a
Optimized code by 2 lines
danopt Aug 28, 2023
b3535e1
Added unit tests
danopt Aug 29, 2023
47b1f4d
Revert last commit
danopt Aug 30, 2023
0c50189
Redesigned checks in unit tests
danopt Aug 30, 2023
9b7ebbf
Typo
danopt Aug 30, 2023
8ce9d6a
Debug
danopt Aug 30, 2023
a78a42b
Debug
danopt Aug 30, 2023
64eb7c9
Debug
danopt Aug 30, 2023
a4e27b8
Debug
danopt Aug 30, 2023
521be99
Debug
danopt Aug 30, 2023
ceacd1d
Fixed argument check
danopt Aug 31, 2023
e25fe8f
Removed print statement
danopt Aug 31, 2023
70d8dce
Removed redundant exceptions in cert_info module
danopt Aug 31, 2023
fbe5871
Test functionallity of unit test again
danopt Aug 31, 2023
e6b11f3
Removed debug test
danopt Aug 31, 2023
a8a8258
Merge branch 'main' into feature/crt_passphrase_check
danopt Aug 31, 2023
e85a6b8
Added documentation for new parameter
danopt Aug 31, 2023
562801b
Merge branch 'feature/crt_passphrase_check' of github.com:NETWAYS/ans…
danopt Aug 31, 2023
3d61904
Updated plugins overview
danopt Aug 31, 2023
002f6d2
Fixed typo
danopt Aug 31, 2023
8773c07
Fixed typo
danopt Aug 31, 2023
1ed7144
Fixed typo
danopt Aug 31, 2023
75a64bb
Fixed typo
danopt Aug 31, 2023
6ef199c
Fixed typo
danopt Aug 31, 2023
1005029
Typo - Update README.md
danopt Sep 11, 2023
e2fab52
Added comment to unit tests
danopt Sep 11, 2023
c01a203
Merge branch 'main' into feature/crt_passphrase_check
danopt Sep 25, 2023
7f7ce15
Added merge_group to GitHub Action
danopt Sep 28, 2023
ebc2249
Merge branch 'feature/crt_passphrase_check' of github.com:NETWAYS/ans…
danopt Sep 28, 2023
0a22093
Merge branch 'main' into feature/crt_passphrase_check
danopt Oct 10, 2023
d70cb58
Specified branch in merge_group
danopt Oct 10, 2023
69ac901
Removed merge_group from test_plugins.yml
danopt Oct 10, 2023
3b538d2
Merge branch 'main' into feature/crt_passphrase_check
danopt Dec 4, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/test_plugins.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ on:
- 'molecule/plugins/**'
- '.config/pep8.yml'
- 'tests/**'
merge_group:

jobs:
pep8:
Expand Down
18 changes: 18 additions & 0 deletions molecule/plugins/converge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,21 @@
- name: Test no parameters
cert_info:
ignore_errors: true
- name: Test wrong passphrase with passphrase_check parameter
cert_info:
path: files/es-ca/elastic-stack-ca.p12
passphrase: PleaseChangeMe-wrong
passphrase_check: true
register: pass_check
- name: Debug
debug:
msg: "{{ pass_check }}"
- name: Test correct passphrase with passphrase_check parameter
cert_info:
path: files/es-ca/elastic-stack-ca.p12
passphrase: PleaseChangeMe
passphrase_check: true
register: pass_check
- name: Debug
debug:
msg: "{{ pass_check }}"
39 changes: 8 additions & 31 deletions plugins/README.md
Original file line number Diff line number Diff line change
@@ -1,31 +1,8 @@
# Collections Plugins Directory

This directory can be used to ship various plugins inside an Ansible collection. Each plugin is placed in a folder that
is named after the type of plugin it is in. It can also include the `module_utils` and `modules` directory that
would contain module utils and modules respectively.

Here is an example directory of the majority of plugins currently supported by Ansible:

```
└── plugins
├── action
├── become
├── cache
├── callback
├── cliconf
├── connection
├── filter
├── httpapi
├── inventory
├── lookup
├── module_utils
├── modules
├── netconf
├── shell
├── strategy
├── terminal
├── test
└── vars
```

A full list of plugin types can be found at [Working With Plugins](https://docs.ansible.com/ansible-core/2.13/plugins/plugins.html).
# `netways.elasticstack` Plugins Directory

## Overview
- [module_utils](https://github.com/NETWAYS/ansible-collection-elasticstack/tree/main/plugins/module_utils)
- [`certs` module util](https://github.com/NETWAYS/ansible-collection-elasticstack/tree/main/plugins/module_utils)
- [modules](https://github.com/NETWAYS/ansible-collection-elasticstack/tree/main/plugins/modules)
- [`cert_info` module](https://github.com/NETWAYS/ansible-collection-elasticstack/tree/main/plugins/modules)

7 changes: 2 additions & 5 deletions plugins/module_utils/README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
# Documentation: netways.elasticstack module_utils

## Overview
- [`certs` module_util](#cert_info-module)

## `netways.elasticstack.certs` function
## `netways.elasticstack.certs` functions

### `bytes_to_hex()` function

Since binascii.hexlify doesn't support a second parameter, which would define a seperator (e.g. ":") for hex strings in older Python versions like 2.6 and 2.7, we implemeted a small function to get similar results.

**Parameter:** A __bytes__ object that represent a hexadecimal value (e.g. b'\\x82S \\x11\\xc7s\\xa7^*w\\xc1\\xdf\"\\xe4#\\xb4\\xc4P\\xba\\xcf')
**Parameter:** A __bytes__ string that represent a hexadecimal value (e.g. b'\\x82S \\x11\\xc7s\\xa7^*w\\xc1\\xdf\"\\xe4#\\xb4\\xc4P\\xba\\xcf')

**Return:** A hexadecimal __string__ seperated by colons (e.g. "82:53:20:11:C7:73:A7:5E:2A:77:C1:DF:22:E4:23:B4:C4:50:BA:CF")

Expand Down
53 changes: 35 additions & 18 deletions plugins/module_utils/certs.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,13 +76,15 @@ class AnalyzeCertificate():
def __init__(self, module, result):
self.module = module
self.result = result
self.__passphrase_check = self.module.params['passphrase_check']
self.__passphrase = self.module.params['passphrase']
self.__path = self.module.params['path']
self.__cert = None
self.__private_key = None
self.__additional_certs = None
self.load_certificate()
self.load_info()
if not self.__passphrase_check:
self.load_info()

def load_certificate(self):
# track if module can load pkcs12
Expand All @@ -95,7 +97,7 @@ def load_certificate(self):
# read the pkcs12 file
try:
with open(self.__path, 'rb') as f:
pkcs12_data = f.read()
__pkcs12_data = f.read()
except IOError as e:
self.module.fail_json(
msg='IOError: %s' % (to_native(e))
Expand All @@ -104,32 +106,47 @@ def load_certificate(self):
# for cryptography >= 3.1.x
try:
__pkcs12_tuple = pkcs12.load_key_and_certificates(
pkcs12_data,
__pkcs12_data,
to_bytes(self.__passphrase),
)
loaded = True
except ValueError as e:
self.result["passphrase_check"] = False
if self.__passphrase_check:
self.module.exit_json(**self.result)
else:
self.module.fail_json(msg='ValueError: %s' % to_native(e))
except Exception:
self.module.log(
msg="Couldn't load certificate without backend. Trying with backend."
)
# try to load with 3 parameters for
# cryptography >= 2.5.x and <= 3.0.x
if not loaded:
# create backend object
backend = default_backend()
# call load_key_and_certificates with 3 paramters
__pkcs12_tuple = pkcs12.load_key_and_certificates(
pkcs12_data,
to_bytes(self.__passphrase),
backend
)
self.module.log(
msg="Loaded certificate with backend."
)
# map loaded certificate to object
self.__private_key = __pkcs12_tuple[0]
self.__cert = __pkcs12_tuple[1]
self.__additional_certs = __pkcs12_tuple[2]
try:
# create backend object
backend = default_backend()
# call load_key_and_certificates with 3 paramters
__pkcs12_tuple = pkcs12.load_key_and_certificates(
__pkcs12_data,
to_bytes(self.__passphrase),
backend
)
self.module.log(
msg="Loaded certificate with backend."
)
loaded = True
except ValueError as e:
self.result["passphrase_check"] = False
if self.__passphrase_check:
self.module.exit_json(**self.result)
else:
self.module.fail_json(msg='ValueError: %s' % to_native(e))
if loaded and not self.__passphrase_check:
# map loaded certificate to object
self.__private_key = __pkcs12_tuple[0]
self.__cert = __pkcs12_tuple[1]
self.__additional_certs = __pkcs12_tuple[2]

def load_info(self):
self.general_info()
Expand Down
49 changes: 45 additions & 4 deletions plugins/modules/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

## `cert_info` module

The netways.elasticstack.cert_info module gathers information about pkcs12 certificates generated by the Elastic stack cert util.
The netways.elasticstack.cert_info module gathers information about pkcs12 certificates generated by the Elastic Stack cert util.

### Dependencies
- python-cryptography >= 2.5.0 on the remote node
Expand Down Expand Up @@ -71,9 +71,11 @@ Currently, the information of the following extensions and values will be return
`path`:
Absolute path to certificate. (**Default:** undefined, required)

`password`:
The password of the pkcs12 certificate. (**Default:** No default, optional)
`passphrase`:
The passphrase of the pkcs12 certificate. (**Default:** No default, optional)

`passphrase_check`:
This will only check the passphrase and returns a bool in the results. If enabled it won't return any certificate information, only the passphrase_check result. (**Default:** False, optional)

### Returns
All keys and values that will be returned with the results variable of the module:
Expand Down Expand Up @@ -101,12 +103,15 @@ The serial number of the certificate as **str** which represents an integer.
- `critical`: The value of critical as **str** which represents a bool.
- `values`: The keys and their values of the extension as **str**. (See: Supported extensions)

`passphrase_check`:
A **bool** that will be `True` if the passphrase check was positive and `False`, if not. It's also possible that it returns `False` if the certificate is corrupted, since Python can't differentiate it and handles exceptions like this as a "VauleError".

### Example
```
- name: Test
cert_info:
path: /opt/es-ca/elasticsearch-ca.pkcs12
password: PleaseChangeMe
passphrase: PleaseChangeMe
register: test

- name: Debug
Expand Down Expand Up @@ -156,3 +161,39 @@ ok: [localhost] => {
}
}
```

### Example of passphrase_check
```
- name: Test correct passphrase wit passphrase_check parameter
cert_info:
path: /opt/es-ca/elasticsearch-ca.pkcs12
passphrase: PleaseChangeMe
passphrase_check: True
register: test

- name: Debug
debug:
msg: "{{ test }}"
```

**Output**:
```
TASK [Test correct passphrase wit passphrase_check parameter] ******************
ok: [localhost]

TASK [Debug] *******************************************************************
ok: [localhost] => {
"msg": {
"changed": false,
"extensions": {},
"failed": false,
"issuer": "",
"not_valid_after": "",
"not_valid_before": "",
"passphrase_check": true,
"serial_number": "",
"subject": "",
"version": ""
}
}
```
15 changes: 6 additions & 9 deletions plugins/modules/cert_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
def run_module():
module_args = dict(
path=dict(type='str', no_log=True, required=True),
passphrase=dict(type='str', no_log=True, required=False, default=None)
passphrase=dict(type='str', no_log=True, required=False, default=None),
passphrase_check=dict(type='bool', no_log=True, required=False, default=False)
)

# seed the result dict
Expand All @@ -34,7 +35,8 @@ def run_module():
not_valid_before='',
serial_number='',
subject='',
version=''
version='',
passphrase_check=True
)

# the AnsibleModule object
Expand All @@ -47,13 +49,8 @@ def run_module():
if module.check_mode:
module.exit_json(**result)

try:
cert_info = AnalyzeCertificate(module, result)
result = cert_info.return_result()
except ValueError as e:
module.fail_json(msg='ValueError: %s' % to_native(e))
except Exception as e:
module.fail_json(msg='Exception: %s: %s' % (to_native(type(e)), to_native(e)))
cert_info = AnalyzeCertificate(module, result)
result = cert_info.return_result()

module.exit_json(**result)

Expand Down
41 changes: 36 additions & 5 deletions tests/unit/plugins/modules/test_cert_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from unittest.mock import patch
from ansible.module_utils import basic
from ansible.module_utils.common.text.converters import to_bytes

sys.path.append('/home/runner/.ansible/collections/')
from ansible_collections.netways.elasticstack.plugins.modules import cert_info

Expand Down Expand Up @@ -55,6 +56,7 @@ class AnsibleExitJson(Exception):
"""Exception class to be raised by module.exit_json and caught by the test case"""
pass


class AnsibleFailJson(Exception):
"""Exception class to be raised by module.fail_json and caught by the test case"""
pass
Expand All @@ -70,12 +72,22 @@ def exit_json(*args, **kwargs):

checks_passed = True

# check every item in certificate if it matches with the result
# and if that fails, don't catch the Exception, so the test will fail
for item in certificate:
if certificate[item] != kwargs[item]:
# only if passphrase_check mode is disabled
if args[0].params['passphrase_check'] is not True:
# check every item in certificate if it matches with the result
# and if that fails, don't catch the Exception, so the test will fail
for item in certificate:
if certificate[item] != kwargs[item]:
checks_passed = False
# if passphrase_check mode is enabled
else:
# fail checks, if passphrase is wrong and passphrase_check kwarg is not False
if args[0].params['passphrase'] == 'PleaseChangeMe-Wrong' and kwargs['passphrase_check'] is True:
checks_passed = False

# fail checks, if passphrase is correct and passphrase_check kwarg is not True
if args[0].params['passphrase'] == 'PleaseChangeMe' and kwargs['passphrase_check'] is False:
checks_passed = False

if checks_passed:
raise AnsibleExitJson(kwargs)

Expand Down Expand Up @@ -131,6 +143,25 @@ def test_module_exit_when_path_and_password_correct(self):
})
cert_info.main()

# Tests with passphrase_check mode set to True (default is False)
def test_module_exit_when_password_wrong_with_passphrase_check(self):
with self.assertRaises(AnsibleExitJson):
set_module_args({
'path': 'molecule/plugins/files/es-ca/elastic-stack-ca.p12',
'passphrase': 'PleaseChangeMe-Wrong',
'passphrase_check': True
martialblog marked this conversation as resolved.
Show resolved Hide resolved
})
cert_info.main()

def test_module_exit_when_password_correct_with_passphrase_check(self):
with self.assertRaises(AnsibleExitJson):
set_module_args({
'path': 'molecule/plugins/files/es-ca/elastic-stack-ca.p12',
'passphrase': 'PleaseChangeMe',
'passphrase_check': True
})
cert_info.main()


if __name__ == '__main__':
unittest.main()
Loading