Allow to pass CSR to acme_certificate as csr_content (#115)

* Allow to pass CSR to acme_certificate as csr_content.

* Make sure contents are bytes.

* No need to write CSR to disk.

* Forgot version_added.

* Fix documentation.
pull/121/head
Felix Fontein 2020-10-09 14:01:34 +02:00 committed by GitHub
parent 8e10e1e590
commit 42dd19c387
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 80 additions and 31 deletions

View File

@ -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).

View File

@ -140,17 +140,23 @@ 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 = []
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 f:
for line in lines:
if line.startswith('-----'):
header_line_count += 1
if header_line_count == 2:
@ -159,8 +165,6 @@ def pem_to_der(pem_filename):
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())
return base64.b64decode(''.join(certificate_lines))
@ -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'))
@ -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))

View File

@ -125,9 +125,23 @@ options:
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:
- "Content of 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
@ -279,7 +293,7 @@ EXAMPLES = r'''
- 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
@ -576,6 +590,7 @@ class ACMEClient(object):
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')
@ -613,7 +628,7 @@ class ACMEClient(object):
# 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)
@ -626,9 +641,9 @@ class ACMEClient(object):
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):
'''
@ -767,7 +782,7 @@ class ACMEClient(object):
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),
}
@ -844,7 +859,7 @@ class ACMEClient(object):
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),
@ -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']),
@ -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,
)

View File

@ -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 }}"
@ -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 }}"
@ -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 }}"
@ -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 }}"
@ -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 }}"
@ -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 }}"
@ -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 }}"
@ -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 }}"
@ -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 }}"
@ -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 }}"
@ -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 }}"

View File

@ -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:
@ -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"
@ -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"