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

Allow to pass CSR to acme_certificate as csr_content #115

Merged
Show file tree
Hide file tree
Changes from 4 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
2 changes: 2 additions & 0 deletions changelogs/fragments/115-acme_certificate-csr_content.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
minor_changes:
- acme_certificate - allow to pass CSR file as content with new option ``csr_content`` (https://github.com/ansible-collections/community.crypto/pull/115).
56 changes: 35 additions & 21 deletions plugins/module_utils/acme.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,27 +140,31 @@ def write_file(module, dest, content):
return changed


def pem_to_der(pem_filename):
def pem_to_der(pem_filename, pem_content=None):
'''
Load PEM file, and convert to DER.
Load PEM file, or use PEM file's content, and convert to DER.

If PEM contains multiple entities, the first entity will be used.
'''
certificate_lines = []
try:
with open(pem_filename, "rt") as f:
header_line_count = 0
for line in f:
if line.startswith('-----'):
header_line_count += 1
if header_line_count == 2:
# If certificate file contains other certs appended
# (like intermediate certificates), ignore these.
break
continue
certificate_lines.append(line.strip())
except Exception as err:
raise ModuleFailException("cannot load PEM file {0}: {1}".format(pem_filename, to_native(err)), exception=traceback.format_exc())
if pem_content is not None:
lines = pem_content.splitlines()
else:
try:
with open(pem_filename, "rt") as f:
lines = list(f)
except Exception as err:
raise ModuleFailException("cannot load PEM file {0}: {1}".format(pem_filename, to_native(err)), exception=traceback.format_exc())
header_line_count = 0
for line in lines:
if line.startswith('-----'):
header_line_count += 1
if header_line_count == 2:
# If certificate file contains other certs appended
# (like intermediate certificates), ignore these.
break
continue
certificate_lines.append(line.strip())
return base64.b64decode(''.join(certificate_lines))


Expand Down Expand Up @@ -989,14 +993,20 @@ def _normalize_ip(ip):
return ip


def openssl_get_csr_identifiers(openssl_binary, module, csr_filename):
def openssl_get_csr_identifiers(openssl_binary, module, csr_filename, csr_content=None):
'''
Return a set of requested identifiers (CN and SANs) for the CSR.
Each identifier is a pair (type, identifier), where type is either
'dns' or 'ip'.
'''
openssl_csr_cmd = [openssl_binary, "req", "-in", csr_filename, "-noout", "-text"]
dummy, out, dummy = module.run_command(openssl_csr_cmd, check_rc=True)
filename = csr_filename
data = None
if csr_content is not None:
filename = '-'
data = csr_content.encode('utf-8')

openssl_csr_cmd = [openssl_binary, "req", "-in", filename, "-noout", "-text"]
dummy, out, dummy = module.run_command(openssl_csr_cmd, data=data, check_rc=True)

identifiers = set([])
common_name = re.search(r"Subject:.* CN\s?=\s?([^\s,;/]+)", to_text(out, errors='surrogate_or_strict'))
Expand All @@ -1018,14 +1028,18 @@ def openssl_get_csr_identifiers(openssl_binary, module, csr_filename):
return identifiers


def cryptography_get_csr_identifiers(module, csr_filename):
def cryptography_get_csr_identifiers(module, csr_filename, csr_content=None):
'''
Return a set of requested identifiers (CN and SANs) for the CSR.
Each identifier is a pair (type, identifier), where type is either
'dns' or 'ip'.
'''
identifiers = set([])
csr = cryptography.x509.load_pem_x509_csr(read_file(csr_filename), _cryptography_backend)
if csr_content is None:
csr_content = read_file(csr_filename)
else:
csr_content = to_bytes(csr_content)
csr = cryptography.x509.load_pem_x509_csr(csr_content, _cryptography_backend)
for sub in csr.subject:
if sub.oid == cryptography.x509.oid.NameOID.COMMON_NAME:
identifiers.add(('dns', sub.value))
Expand Down
34 changes: 26 additions & 8 deletions plugins/modules/acme_certificate.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,23 @@
account key. This is a bad idea from a security point of view, and
the CA should not accept the CSR. The ACME server should return an
error in this case."
- Precisely one of I(csr) or I(csr_content) must be specified.
type: path
required: true
aliases: ['src']
csr_content:
description:
- "File containing the CSR for the new certificate."
- "Can be created with C(openssl req ...)."
- "The CSR may contain multiple Subject Alternate Names, but each one
will lead to an individual challenge that must be fulfilled for the
CSR to be signed."
- "I(Note): the private key used to create the CSR I(must not) be the
account key. This is a bad idea from a security point of view, and
the CA should not accept the CSR. The ACME server should return an
error in this case."
- Precisely one of I(csr) or I(csr_content) must be specified.
type: str
version_added: 1.2.0
data:
description:
- "The data to validate ongoing challenges. This must be specified for
Expand Down Expand Up @@ -279,7 +293,7 @@
- name: Create a challenge for sample.com using a account key file.
community.crypto.acme_certificate:
account_key_src: /etc/pki/cert/private/account.key
csr: /etc/pki/cert/csr/sample.com.csr
csr_content: "{{ lookup('file', '/etc/pki/cert/csr/sample.com.csr') }}"
dest: /etc/httpd/ssl/sample.com.crt
fullchain_dest: /etc/httpd/ssl/sample.com-fullchain.crt
register: sample_com_challenge
Expand Down Expand Up @@ -576,6 +590,7 @@ def __init__(self, module):
self.version = module.params['acme_version']
self.challenge = module.params['challenge']
self.csr = module.params['csr']
self.csr_content = module.params['csr_content']
self.dest = module.params.get('dest')
self.fullchain_dest = module.params.get('fullchain_dest')
self.chain_dest = module.params.get('chain_dest')
Expand Down Expand Up @@ -613,7 +628,7 @@ def __init__(self, module):
# signed ACME request.
pass

if not os.path.exists(self.csr):
if self.csr is not None and not os.path.exists(self.csr):
raise ModuleFailException("CSR %s not found" % (self.csr))

self._openssl_bin = module.get_bin_path('openssl', True)
Expand All @@ -626,9 +641,9 @@ def _get_csr_identifiers(self):
Parse the CSR and return the list of requested identifiers
'''
if HAS_CURRENT_CRYPTOGRAPHY:
return cryptography_get_csr_identifiers(self.module, self.csr)
return cryptography_get_csr_identifiers(self.module, self.csr, self.csr_content)
else:
return openssl_get_csr_identifiers(self._openssl_bin, self.module, self.csr)
return openssl_get_csr_identifiers(self._openssl_bin, self.module, self.csr, self.csr_content)

def _add_or_update_auth(self, identifier_type, identifier, auth):
'''
Expand Down Expand Up @@ -767,7 +782,7 @@ def _finalize_cert(self):
Return the certificate object as dict
https://tools.ietf.org/html/rfc8555#section-7.4
'''
csr = pem_to_der(self.csr)
csr = pem_to_der(self.csr, self.csr_content)
new_cert = {
"csr": nopad_b64(csr),
}
Expand Down Expand Up @@ -844,7 +859,7 @@ def _new_cert_v1(self):
Return the certificate object as dict
https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-6.5
'''
csr = pem_to_der(self.csr)
csr = pem_to_der(self.csr, self.csr_content)
new_cert = {
"resource": "new-cert",
"csr": nopad_b64(csr),
Expand Down Expand Up @@ -1177,7 +1192,8 @@ def main():
agreement=dict(type='str'),
terms_agreed=dict(type='bool', default=False),
challenge=dict(type='str', default='http-01', choices=['http-01', 'dns-01', 'tls-alpn-01']),
csr=dict(type='path', required=True, aliases=['src']),
csr=dict(type='path', aliases=['src']),
csr_content=dict(type='str'),
data=dict(type='dict'),
dest=dict(type='path', aliases=['cert']),
fullchain_dest=dict(type='path', aliases=['fullchain']),
Expand All @@ -1199,9 +1215,11 @@ def main():
required_one_of=(
['account_key_src', 'account_key_content'],
['dest', 'fullchain_dest'],
['csr', 'csr_content'],
),
mutually_exclusive=(
['account_key_src', 'account_key_content'],
['csr', 'csr_content'],
),
supports_check_mode=True,
)
Expand Down
11 changes: 11 additions & 0 deletions tests/integration/targets/acme_certificate/tasks/impl.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
select_chain:
- test_certificates: last
issuer: "{{ acme_roots[1].subject }}"
use_csr_content: true
- name: Store obtain results for cert 1
set_fact:
cert_1_obtain_results: "{{ certificate_obtain_result }}"
Expand Down Expand Up @@ -93,6 +94,7 @@
subject: "{{ acme_intermediates[0].subject }}"
- test_certificates: all
issuer: "{{ acme_roots[2].subject }}"
use_csr_content: false
- name: Store obtain results for cert 2
set_fact:
cert_2_obtain_results: "{{ certificate_obtain_result }}"
Expand All @@ -118,6 +120,7 @@
select_chain:
- test_certificates: last
subject: "{{ acme_roots[1].subject }}"
use_csr_content: true
- name: Store obtain results for cert 3
set_fact:
cert_3_obtain_results: "{{ certificate_obtain_result }}"
Expand Down Expand Up @@ -145,6 +148,7 @@
issuer: "{{ acme_roots[2].subject }}"
- test_certificates: last
issuer: "{{ acme_roots[1].subject }}"
use_csr_content: false
- name: Store obtain results for cert 4
set_fact:
cert_4_obtain_results: "{{ certificate_obtain_result }}"
Expand All @@ -165,6 +169,7 @@
remaining_days: 10
terms_agreed: no
account_email: ""
use_csr_content: true
- name: Store obtain results for cert 5a
set_fact:
cert_5a_obtain_results: "{{ certificate_obtain_result }}"
Expand All @@ -185,6 +190,7 @@
remaining_days: 10
terms_agreed: no
account_email: ""
use_csr_content: false
- name: Store obtain results for cert 5b
set_fact:
cert_5_recreate_1: "{{ challenge_data is changed }}"
Expand All @@ -204,6 +210,7 @@
remaining_days: 1000
terms_agreed: no
account_email: ""
use_csr_content: true
- name: Store obtain results for cert 5c
set_fact:
cert_5_recreate_2: "{{ challenge_data is changed }}"
Expand All @@ -224,6 +231,7 @@
remaining_days: 10
terms_agreed: no
account_email: ""
use_csr_content: false
- name: Store obtain results for cert 5d
set_fact:
cert_5_recreate_3: "{{ challenge_data is changed }}"
Expand Down Expand Up @@ -255,6 +263,7 @@
subject_key_identifier: "{{ acme_intermediates[0].subject_key_identifier }}"
- test_certificates: last
issuer: "{{ acme_roots[1].subject }}"
use_csr_content: true
- name: Store obtain results for cert 6
set_fact:
cert_6_obtain_results: "{{ certificate_obtain_result }}"
Expand Down Expand Up @@ -282,6 +291,7 @@
select_chain:
- test_certificates: last
authority_key_identifier: "{{ acme_roots[2].subject_key_identifier }}"
use_csr_content: false
- name: Store obtain results for cert 7
set_fact:
cert_7_obtain_results: "{{ certificate_obtain_result }}"
Expand All @@ -307,6 +317,7 @@
remaining_days: 10
terms_agreed: yes
account_email: "example@example.org"
use_csr_content: true
- name: Store obtain results for cert 8
set_fact:
cert_8_obtain_results: "{{ certificate_obtain_result }}"
Expand Down
8 changes: 6 additions & 2 deletions tests/integration/targets/setup_acme/tasks/obtain-cert.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
privatekey_path: "{{ output_dir }}/{{ certificate_name }}.key"
subject_alt_name: "{{ subject_alt_name }}"
subject_alt_name_critical: "{{ subject_alt_name_critical }}"
return_content: true
register: csr_result
## ACME STEP 1 ################################################################################
- name: ({{ certgen_title }}) Obtain cert, step 1
acme_certificate:
Expand All @@ -29,7 +31,8 @@
account_key: "{{ (output_dir ~ '/' ~ account_key ~ '.pem') if account_key_content is not defined else omit }}"
account_key_content: "{{ account_key_content | default(omit) }}"
modify_account: "{{ modify_account }}"
csr: "{{ output_dir }}/{{ certificate_name }}.csr"
csr: "{{ omit if use_csr_content | default(false) else output_dir ~ '/' ~ certificate_name ~ '.csr' }}"
csr_content: "{{ csr_result.csr if use_csr_content | default(false) else omit }}"
dest: "{{ output_dir }}/{{ certificate_name }}.pem"
fullchain_dest: "{{ output_dir }}/{{ certificate_name }}-fullchain.pem"
chain_dest: "{{ output_dir }}/{{ certificate_name }}-chain.pem"
Expand Down Expand Up @@ -100,7 +103,8 @@
account_key_content: "{{ account_key_content | default(omit) }}"
account_uri: "{{ challenge_data.account_uri }}"
modify_account: "{{ modify_account }}"
csr: "{{ output_dir }}/{{ certificate_name }}.csr"
csr: "{{ omit if use_csr_content | default(false) else output_dir ~ '/' ~ certificate_name ~ '.csr' }}"
csr_content: "{{ csr_result.csr if use_csr_content | default(false) else omit }}"
dest: "{{ output_dir }}/{{ certificate_name }}.pem"
fullchain_dest: "{{ output_dir }}/{{ certificate_name }}-fullchain.pem"
chain_dest: "{{ output_dir }}/{{ certificate_name }}-chain.pem"
Expand Down