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

acme_* modules: support private key passprases #207

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions changelogs/fragments/207-acme-account-key-passphrase.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
minor_changes:
- "acme_* modules - support account key passphrases for ``cryptography`` backend (https://github.com/ansible-collections/community.crypto/issues/197, https://github.com/ansible-collections/community.crypto/pull/207)."
- "acme_certificate_revoke - support revoking by private keys that are passphrase protected for ``cryptography`` backend (https://github.com/ansible-collections/community.crypto/pull/207)."
- "acme_challenge_cert_helper - add ``private_key_passphrase`` parameter (https://github.com/ansible-collections/community.crypto/pull/207)."
6 changes: 6 additions & 0 deletions plugins/doc_fragments/acme.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ class ModuleDocFragment(object):
Ansible in the process of moving the module with its argument to
the node where it is executed."
type: str
account_key_passphrase:
description:
- Phassphrase to use to decode the account key.
- "B(Note:) this is not supported by the C(openssl) backend, only by the C(cryptography) backend."
type: str
version_added: 1.6.0
account_uri:
description:
- "If specified, assumes that the account URI is as given. If the
Expand Down
11 changes: 8 additions & 3 deletions plugins/module_utils/acme/acme.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ def __init__(self, module, backend):
# account_key path and content are mutually exclusive
self.account_key_file = module.params['account_key_src']
self.account_key_content = module.params['account_key_content']
self.account_key_passphrase = module.params['account_key_passphrase']

# Grab account URI from module parameters.
# Make sure empty string is treated as None.
Expand All @@ -122,7 +123,10 @@ def __init__(self, module, backend):
self.account_jws_header = None
if self.account_key_file is not None or self.account_key_content is not None:
try:
self.account_key_data = self.parse_key(key_file=self.account_key_file, key_content=self.account_key_content)
self.account_key_data = self.parse_key(
key_file=self.account_key_file,
key_content=self.account_key_content,
passphrase=self.account_key_passphrase)
except KeyParsingError as e:
raise ModuleFailException("Error while parsing account key: {msg}".format(msg=e.msg))
self.account_jwk = self.account_key_data['jwk']
Expand All @@ -146,14 +150,14 @@ def set_account_uri(self, uri):
self.account_jws_header.pop('jwk')
self.account_jws_header['kid'] = self.account_uri

def parse_key(self, key_file=None, key_content=None):
def parse_key(self, key_file=None, key_content=None, passphrase=None):
'''
Parses an RSA or Elliptic Curve key file in PEM format and returns key_data.
In case of an error, raises KeyParsingError.
'''
if key_file is None and key_content is None:
raise AssertionError('One of key_file and key_content must be specified!')
error, key_data = self.backend.parse_key(key_file, key_content)
error, key_data = self.backend.parse_key(key_file, key_content, passphrase=passphrase)
if error:
raise KeyParsingError(error)
return key_data
Expand Down Expand Up @@ -311,6 +315,7 @@ def get_default_argspec():
return dict(
account_key_src=dict(type='path', aliases=['account_key']),
account_key_content=dict(type='str', no_log=True),
account_key_passphrase=dict(type='str', no_log=True),
account_uri=dict(type='str'),
acme_directory=dict(type='str'),
acme_version=dict(type='int', choices=[1, 2]),
Expand Down
7 changes: 5 additions & 2 deletions plugins/module_utils/acme/backend_cryptography.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ class CryptographyBackend(CryptoBackend):
def __init__(self, module):
super(CryptographyBackend, self).__init__(module)

def parse_key(self, key_file=None, key_content=None):
def parse_key(self, key_file=None, key_content=None, passphrase=None):
'''
Parses an RSA or Elliptic Curve key file in PEM format and returns a pair
(error, key_data).
Expand All @@ -191,7 +191,10 @@ def parse_key(self, key_file=None, key_content=None):
key_content = to_bytes(key_content)
# Parse key
try:
key = cryptography.hazmat.primitives.serialization.load_pem_private_key(key_content, password=None, backend=_cryptography_backend)
key = cryptography.hazmat.primitives.serialization.load_pem_private_key(
key_content,
password=to_bytes(passphrase) if passphrase is not None else None,
backend=_cryptography_backend)
except Exception as e:
return 'error while loading key: {0}'.format(e), None
if isinstance(key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey):
Expand Down
4 changes: 3 additions & 1 deletion plugins/module_utils/acme/backend_openssl_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,13 @@ def __init__(self, module, openssl_binary=None):
openssl_binary = module.get_bin_path('openssl', True)
self.openssl_binary = openssl_binary

def parse_key(self, key_file=None, key_content=None):
def parse_key(self, key_file=None, key_content=None, passphrase=None):
'''
Parses an RSA or Elliptic Curve key file in PEM format and returns a pair
(error, key_data).
'''
if passphrase is not None:
return 'openssl backend does not support key passphrases', {}
# If key_file isn't given, but key_content, write that to a temporary file
if key_file is None:
fd, tmpsrc = tempfile.mkstemp()
Expand Down
2 changes: 1 addition & 1 deletion plugins/module_utils/acme/backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def __init__(self, module):
self.module = module

@abc.abstractmethod
def parse_key(self, key_file=None, key_content=None):
def parse_key(self, key_file=None, key_content=None, passphrase=None):
'''
Parses an RSA or Elliptic Curve key file in PEM format and returns a pair
(error, key_data).
Expand Down
10 changes: 9 additions & 1 deletion plugins/modules/acme_account.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,12 @@
- "Mutually exclusive with C(new_account_key_src)."
- "Required if C(new_account_key_src) is not used and state is C(changed_key)."
type: str
new_account_key_passphrase:
description:
- Phassphrase to use to decode the new account key.
- "B(Note:) this is not supported by the C(openssl) backend, only by the C(cryptography) backend."
type: str
version_added: 1.6.0
external_account_binding:
description:
- Allows to provide external account binding data during account creation.
Expand Down Expand Up @@ -183,6 +189,7 @@ def main():
contact=dict(type='list', elements='str', default=[]),
new_account_key_src=dict(type='path'),
new_account_key_content=dict(type='str', no_log=True),
new_account_key_passphrase=dict(type='str', no_log=True),
external_account_binding=dict(type='dict', options=dict(
kid=dict(type='str', required=True),
alg=dict(type='str', required=True, choices=['HS256', 'HS384', 'HS512']),
Expand Down Expand Up @@ -272,7 +279,8 @@ def main():
try:
new_key_data = client.parse_key(
module.params.get('new_account_key_src'),
module.params.get('new_account_key_content')
module.params.get('new_account_key_content'),
passphrase=module.params.get('new_account_key_passphrase'),
)
except KeyParsingError as e:
raise ModuleFailException("Error while parsing new account key: {msg}".format(msg=e.msg))
Expand Down
12 changes: 9 additions & 3 deletions plugins/modules/acme_certificate_revoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@
private keys in PEM format can be used as well."
- "Mutually exclusive with C(account_key_content)."
- "Required if C(account_key_content) is not used."
type: path
account_key_content:
description:
- "Content of the ACME account RSA or Elliptic Curve key."
Expand All @@ -69,7 +68,6 @@
temporary file. It can still happen that it is written to disk by
Ansible in the process of moving the module with its argument to
the node where it is executed."
type: str
private_key_src:
description:
- "Path to the certificate's private key."
Expand All @@ -91,6 +89,12 @@
Ansible in the process of moving the module with its argument to
the node where it is executed."
type: str
private_key_passphrase:
description:
- Phassphrase to use to decode the certificate's private key.
- "B(Note:) this is not supported by the C(openssl) backend, only by the C(cryptography) backend."
type: str
version_added: 1.6.0
revoke_reason:
description:
- "One of the revocation reasonCodes defined in
Expand Down Expand Up @@ -146,6 +150,7 @@ def main():
argument_spec.update(dict(
private_key_src=dict(type='path'),
private_key_content=dict(type='str', no_log=True),
private_key_passphrase=dict(type='str', no_log=True),
certificate=dict(type='path', required=True),
revoke_reason=dict(type='int'),
))
Expand Down Expand Up @@ -184,9 +189,10 @@ def main():
private_key_content = module.params.get('private_key_content')
# Revoke certificate
if private_key or private_key_content:
passphrase = module.params['private_key_passphrase']
# Step 1: load and parse private key
try:
private_key_data = client.parse_key(private_key, private_key_content)
private_key_data = client.parse_key(private_key, private_key_content, passphrase=passphrase)
except KeyParsingError as e:
raise ModuleFailException("Error while parsing private key: {msg}".format(msg=e.msg))
# Step 2: sign revokation request with private key
Expand Down
12 changes: 11 additions & 1 deletion plugins/modules/acme_challenge_cert_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@
- "Content of the private key to use for this challenge certificate."
- "Mutually exclusive with C(private_key_src)."
type: str
private_key_passphrase:
description:
- Phassphrase to use to decode the private key.
type: str
version_added: 1.6.0
notes:
- Does not support C(check_mode).
'''
Expand Down Expand Up @@ -187,6 +192,7 @@ def main():
challenge_data=dict(type='dict', required=True),
private_key_src=dict(type='path'),
private_key_content=dict(type='str', no_log=True),
private_key_passphrase=dict(type='str', no_log=True),
),
required_one_of=(
['private_key_src', 'private_key_content'],
Expand All @@ -205,12 +211,16 @@ def main():

# Get hold of private key
private_key_content = module.params.get('private_key_content')
private_key_passphrase = module.params.get('private_key_passphrase')
if private_key_content is None:
private_key_content = read_file(module.params['private_key_src'])
else:
private_key_content = to_bytes(private_key_content)
try:
private_key = cryptography.hazmat.primitives.serialization.load_pem_private_key(private_key_content, password=None, backend=_cryptography_backend)
private_key = cryptography.hazmat.primitives.serialization.load_pem_private_key(
private_key_content,
password=to_bytes(private_key_passphrase) if private_key_passphrase is not None else None,
backend=_cryptography_backend)
except Exception as e:
raise ModuleFailException('Error while loading private key: {0}'.format(e))

Expand Down
47 changes: 31 additions & 16 deletions tests/integration/targets/acme_account/tasks/impl.yml
Original file line number Diff line number Diff line change
@@ -1,20 +1,29 @@
- name: Generate account keys
command: "{{ openssl_binary }} ecparam -name prime256v1 -genkey -out {{ output_dir }}/{{ item }}.pem"
loop:
- accountkey
- accountkey2
- accountkey3
- accountkey4
- accountkey5
- block:
- name: Generate account keys
openssl_privatekey:
path: "{{ output_dir }}/{{ item.name }}.pem"
passphrase: "{{ item.pass | default(omit, true) }}"
cipher: "{{ 'auto' if item.pass | default() else omit }}"
type: ECC
curve: secp256r1
force: true
loop: "{{ account_keys }}"

- name: Parse account keys (to ease debugging some test failures)
command: "{{ openssl_binary }} ec -in {{ output_dir }}/{{ item }}.pem -noout -text"
loop:
- accountkey
- accountkey2
- accountkey3
- accountkey4
- accountkey5
- name: Parse account keys (to ease debugging some test failures)
openssl_privatekey_info:
path: "{{ output_dir }}/{{ item.name }}.pem"
passphrase: "{{ item.pass | default(omit, true) }}"
return_private_key_data: true
loop: "{{ account_keys }}"

vars:
account_keys:
- name: accountkey
- name: accountkey2
pass: "{{ 'hunter2' if select_crypto_backend != 'openssl' else '' }}"
- name: accountkey3
- name: accountkey4
- name: accountkey5

- name: Do not try to create account
acme_account:
Expand Down Expand Up @@ -173,6 +182,7 @@
acme_directory: https://{{ acme_host }}:14000/dir
validate_certs: no
new_account_key_src: "{{ output_dir }}/accountkey2.pem"
new_account_key_passphrase: "{{ 'hunter2' if select_crypto_backend != 'openssl' else omit }}"
state: changed_key
contact:
- mailto:example@example.com
Expand All @@ -188,6 +198,7 @@
acme_directory: https://{{ acme_host }}:14000/dir
validate_certs: no
new_account_key_src: "{{ output_dir }}/accountkey2.pem"
new_account_key_passphrase: "{{ 'hunter2' if select_crypto_backend != 'openssl' else omit }}"
state: changed_key
contact:
- mailto:example@example.com
Expand All @@ -197,6 +208,7 @@
acme_account:
select_crypto_backend: "{{ select_crypto_backend }}"
account_key_src: "{{ output_dir }}/accountkey2.pem"
account_key_passphrase: "{{ 'hunter2' if select_crypto_backend != 'openssl' else omit }}"
acme_version: 2
acme_directory: https://{{ acme_host }}:14000/dir
validate_certs: no
Expand All @@ -209,6 +221,7 @@
acme_account:
select_crypto_backend: "{{ select_crypto_backend }}"
account_key_src: "{{ output_dir }}/accountkey2.pem"
account_key_passphrase: "{{ 'hunter2' if select_crypto_backend != 'openssl' else omit }}"
acme_version: 2
acme_directory: https://{{ acme_host }}:14000/dir
validate_certs: no
Expand All @@ -219,6 +232,7 @@
acme_account:
select_crypto_backend: "{{ select_crypto_backend }}"
account_key_src: "{{ output_dir }}/accountkey2.pem"
account_key_passphrase: "{{ 'hunter2' if select_crypto_backend != 'openssl' else omit }}"
acme_version: 2
acme_directory: https://{{ acme_host }}:14000/dir
validate_certs: no
Expand All @@ -229,6 +243,7 @@
acme_account:
select_crypto_backend: "{{ select_crypto_backend }}"
account_key_src: "{{ output_dir }}/accountkey2.pem"
account_key_passphrase: "{{ 'hunter2' if select_crypto_backend != 'openssl' else omit }}"
acme_version: 2
acme_directory: https://{{ acme_host }}:14000/dir
validate_certs: no
Expand Down
23 changes: 17 additions & 6 deletions tests/integration/targets/acme_account_info/tasks/impl.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
---
- name: Generate account key
command: "{{ openssl_binary }} ecparam -name prime256v1 -genkey -out {{ output_dir }}/accountkey.pem"
- block:
- name: Generate account keys
openssl_privatekey:
path: "{{ output_dir }}/{{ item }}.pem"
type: ECC
curve: secp256r1
force: true
loop: "{{ account_keys }}"

- name: Generate second account key
command: "{{ openssl_binary }} ecparam -name prime256v1 -genkey -out {{ output_dir }}/accountkey2.pem"
- name: Parse account keys (to ease debugging some test failures)
openssl_privatekey_info:
path: "{{ output_dir }}/{{ item }}.pem"
return_private_key_data: true
loop: "{{ account_keys }}"

- name: Parse account key (to ease debugging some test failures)
command: "{{ openssl_binary }} ec -in {{ output_dir }}/accountkey.pem -noout -text"
vars:
account_keys:
- accountkey
- accountkey2

- name: Check that account does not exist
acme_account_info:
Expand Down
28 changes: 22 additions & 6 deletions tests/integration/targets/acme_certificate/tasks/impl.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,26 @@
---
## SET UP ACCOUNT KEYS ########################################################################
- name: Create ECC256 account key
command: "{{ openssl_binary }} ecparam -name prime256v1 -genkey -out {{ output_dir }}/account-ec256.pem"
- name: Create ECC384 account key
command: "{{ openssl_binary }} ecparam -name secp384r1 -genkey -out {{ output_dir }}/account-ec384.pem"
- name: Create RSA account key
command: "{{ openssl_binary }} genrsa -out {{ output_dir }}/account-rsa.pem {{ default_rsa_key_size }}"
- block:
- name: Generate account keys
openssl_privatekey:
path: "{{ output_dir }}/{{ item.name }}.pem"
type: "{{ item.type }}"
size: "{{ item.size | default(omit) }}"
curve: "{{ item.curve | default(omit) }}"
force: true
loop: "{{ account_keys }}"

vars:
account_keys:
- name: account-ec256
type: ECC
curve: secp256r1
- name: account-ec384
type: ECC
curve: secp384r1
- name: account-rsa
type: RSA
size: "{{ default_rsa_key_size }}"
## SET UP ACCOUNTS ############################################################################
- name: Make sure ECC256 account hasn't been created yet
acme_account:
Expand Down Expand Up @@ -72,6 +87,7 @@
vars:
certgen_title: Certificate 2
certificate_name: cert-2
certificate_passphrase: "{{ 'hunter2' if select_crypto_backend != 'openssl' else '' }}"
key_type: ec256
subject_alt_name: "DNS:*.example.com,DNS:example.com"
subject_alt_name_critical: yes
Expand Down
Loading