ACME: add support for IP identifiers (#53660)

* Adding support for IP identifiers according to https://tools.ietf.org/html/draft-ietf-acme-ip-05.

* Add changelog.

* Make sure that the authorizations return value is unchanged for CSRs with DNS-only SANs.

* Remove unneeded import.

* type -> identifier_type

* Python 2.6 compatibility.

* Fix unit tests.

* Add IP address normalization.

* Extend tests.

* Move data into fixtures.

* Adjust BOTMETA.
This commit is contained in:
Felix Fontein 2019-03-13 10:16:56 +01:00 committed by René Moser
parent 028facdfed
commit c2cb82ec14
12 changed files with 449 additions and 148 deletions

2
.github/BOTMETA.yml vendored
View file

@ -1345,7 +1345,7 @@ files:
<<: *docker <<: *docker
support: community support: community
test/units/module_utils/facts/network/test_generic_bsd.py: *bsd test/units/module_utils/facts/network/test_generic_bsd.py: *bsd
test/units/module_utils/test_acme.py: *crypto test/units/module_utils/acme: *crypto
test/units/module_utils/xenserver/: bvitnik test/units/module_utils/xenserver/: bvitnik
test/units/modules/cloud/docker: test/units/modules/cloud/docker:
<<: *docker <<: *docker

View file

@ -0,0 +1,2 @@
minor_changes:
- "acme_certificate - add experimental support for IP address identifiers."

View file

@ -822,21 +822,97 @@ class ACMEAccount(object):
return True, account_data return True, account_data
def cryptography_get_csr_domains(module, csr_filename): def _normalize_ip(ip):
if ':' not in ip:
# For IPv4 addresses: remove trailing zeros per nibble
ip = '.'.join([nibble.lstrip('0') or '0' for nibble in ip.split('.')])
return ip
# For IPv6 addresses:
# 1. Make them lowercase and split
ip = ip.lower()
i = ip.find('::')
if i >= 0:
front = ip[:i].split(':') or []
back = ip[i + 2:].split(':') or []
ip = front + ['0'] * (8 - len(front) - len(back)) + back
else:
ip = ip.split(':')
# 2. Remove trailing zeros per nibble
ip = [nibble.lstrip('0') or '0' for nibble in ip]
# 3. Find longest consecutive sequence of zeros
zeros_start = -1
zeros_length = -1
current_start = -1
for i, nibble in enumerate(ip):
if nibble == '0':
if current_start < 0:
current_start = i
elif current_start >= 0:
if i - current_start > zeros_length:
zeros_start = current_start
zeros_length = i - current_start
current_start = -1
if current_start >= 0:
if 8 - current_start > zeros_length:
zeros_start = current_start
zeros_length = 8 - current_start
# 4. If the sequence has at least two elements, contract
if zeros_length >= 2:
return ':'.join(ip[:zeros_start]) + '::' + ':'.join(ip[zeros_start + zeros_length:])
# 5. If not, return full IP
return ':'.join(ip)
def openssl_get_csr_identifiers(openssl_binary, module, csr_filename):
''' '''
Return a set of requested domains (CN and SANs) for the CSR. 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'.
''' '''
domains = set([]) openssl_csr_cmd = [openssl_binary, "req", "-in", csr_filename, "-noout", "-text"]
dummy, out, dummy = module.run_command(openssl_csr_cmd, check_rc=True)
identifiers = set([])
common_name = re.search(r"Subject:.* CN\s?=\s?([^\s,;/]+)", to_text(out, errors='surrogate_or_strict'))
if common_name is not None:
identifiers.add(('dns', common_name.group(1)))
subject_alt_names = re.search(
r"X509v3 Subject Alternative Name: (?:critical)?\n +([^\n]+)\n",
to_text(out, errors='surrogate_or_strict'), re.MULTILINE | re.DOTALL)
if subject_alt_names is not None:
for san in subject_alt_names.group(1).split(", "):
if san.lower().startswith("dns:"):
identifiers.add(('dns', san[4:]))
elif san.lower().startswith("ip:"):
identifiers.add(('ip', _normalize_ip(san[3:])))
elif san.lower().startswith("ip address:"):
identifiers.add(('ip', _normalize_ip(san[11:])))
else:
raise ModuleFailException('Found unsupported SAN identifier "{0}"'.format(san))
return identifiers
def cryptography_get_csr_identifiers(module, csr_filename):
'''
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) csr = cryptography.x509.load_pem_x509_csr(read_file(csr_filename), _cryptography_backend)
for sub in csr.subject: for sub in csr.subject:
if sub.oid == cryptography.x509.oid.NameOID.COMMON_NAME: if sub.oid == cryptography.x509.oid.NameOID.COMMON_NAME:
domains.add(sub.value) identifiers.add(('dns', sub.value))
for extension in csr.extensions: for extension in csr.extensions:
if extension.oid == cryptography.x509.oid.ExtensionOID.SUBJECT_ALTERNATIVE_NAME: if extension.oid == cryptography.x509.oid.ExtensionOID.SUBJECT_ALTERNATIVE_NAME:
for name in extension.value: for name in extension.value:
if isinstance(name, cryptography.x509.DNSName): if isinstance(name, cryptography.x509.DNSName):
domains.add(name.value) identifiers.add(('dns', name.value))
return domains elif isinstance(name, cryptography.x509.IPAddress):
identifiers.add(('ip', _normalize_ip(str(name.value))))
else:
raise ModuleFailException('Found unsupported SAN identifier {0}'.format(name))
return identifiers
def cryptography_get_cert_days(module, cert_file, now=None): def cryptography_get_cert_days(module, cert_file, now=None):

View file

@ -39,6 +39,8 @@ description:
L(the main ACME specification,https://tools.ietf.org/html/draft-ietf-acme-acme-18#section-8) L(the main ACME specification,https://tools.ietf.org/html/draft-ietf-acme-acme-18#section-8)
and the L(TLS-ALPN-01 specification,https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-05#section-3). and the L(TLS-ALPN-01 specification,https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-05#section-3).
Also, consider the examples provided for this module." Also, consider the examples provided for this module."
- "The module includes experimental support for IP identifiers according to
the L(current ACME IP draft,https://tools.ietf.org/html/draft-ietf-acme-ip-05)."
notes: notes:
- "At least one of C(dest) and C(fullchain_dest) must be specified." - "At least one of C(dest) and C(fullchain_dest) must be specified."
- "This module includes basic account management functionality. - "This module includes basic account management functionality.
@ -298,19 +300,27 @@ EXAMPLES = r'''
RETURN = ''' RETURN = '''
cert_days: cert_days:
description: the number of days the certificate remains valid. description: The number of days the certificate remains valid.
returned: success returned: success
type: int type: int
challenge_data: challenge_data:
description: per domain / challenge type challenge data description: Per identifier / challenge type challenge data.
returned: changed returned: changed
type: complex type: complex
contains: contains:
resource: resource:
description: the challenge resource that must be created for validation description: The challenge resource that must be created for validation.
returned: changed returned: changed
type: str type: str
sample: .well-known/acme-challenge/evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA sample: .well-known/acme-challenge/evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA
resource_original:
description:
- The original challenge resource including type identifier for C(tls-alpn-01)
challenges.
returned: changed and challenge is C(tls-alpn-01)
type: str
sample: DNS:example.com
version_added: "2.8"
resource_value: resource_value:
description: description:
- The value the resource has to produce for the validation. - The value the resource has to produce for the validation.
@ -325,13 +335,13 @@ challenge_data:
type: str type: str
sample: IlirfxKKXA...17Dt3juxGJ-PCt92wr-oA sample: IlirfxKKXA...17Dt3juxGJ-PCt92wr-oA
record: record:
description: the full DNS record's name for the challenge description: The full DNS record's name for the challenge.
returned: changed and challenge is C(dns-01) returned: changed and challenge is C(dns-01)
type: str type: str
sample: _acme-challenge.example.com sample: _acme-challenge.example.com
version_added: "2.5" version_added: "2.5"
challenge_data_dns: challenge_data_dns:
description: list of TXT values per DNS record, in case challenge is C(dns-01) description: List of TXT values per DNS record, in case challenge is C(dns-01).
returned: changed returned: changed
type: dict type: dict
version_added: "2.5" version_added: "2.5"
@ -362,8 +372,13 @@ account_uri:
''' '''
from ansible.module_utils.acme import ( from ansible.module_utils.acme import (
ModuleFailException, write_file, nopad_b64, pem_to_der, ACMEAccount, ModuleFailException,
HAS_CURRENT_CRYPTOGRAPHY, cryptography_get_csr_domains, cryptography_get_cert_days, write_file, nopad_b64, pem_to_der,
ACMEAccount,
HAS_CURRENT_CRYPTOGRAPHY,
cryptography_get_csr_identifiers,
openssl_get_csr_identifiers,
cryptography_get_cert_days,
set_crypto_backend, set_crypto_backend,
) )
@ -377,7 +392,7 @@ import time
from datetime import datetime from datetime import datetime
from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils._text import to_text, to_bytes from ansible.module_utils._text import to_bytes
def get_cert_days(module, cert_file): def get_cert_days(module, cert_file):
@ -454,49 +469,37 @@ class ACMEClient(object):
# signed ACME request. # signed ACME request.
pass pass
# Extract list of domains from CSR
if not os.path.exists(self.csr): if not os.path.exists(self.csr):
raise ModuleFailException("CSR %s not found" % (self.csr)) raise ModuleFailException("CSR %s not found" % (self.csr))
self._openssl_bin = module.get_bin_path('openssl', True) self._openssl_bin = module.get_bin_path('openssl', True)
self.domains = self._get_csr_domains()
def _get_csr_domains(self): # Extract list of identifiers from CSR
self.identifiers = self._get_csr_identifiers()
def _get_csr_identifiers(self):
''' '''
Parse the CSR and return the list of requested domains Parse the CSR and return the list of requested identifiers
''' '''
if HAS_CURRENT_CRYPTOGRAPHY: if HAS_CURRENT_CRYPTOGRAPHY:
return cryptography_get_csr_domains(self.module, self.csr) return cryptography_get_csr_identifiers(self.module, self.csr)
openssl_csr_cmd = [self._openssl_bin, "req", "-in", self.csr, "-noout", "-text"] else:
dummy, out, dummy = self.module.run_command(openssl_csr_cmd, check_rc=True) return openssl_get_csr_identifiers(self._openssl_bin, self.module, self.csr)
domains = set([]) def _add_or_update_auth(self, identifier_type, identifier, auth):
common_name = re.search(r"Subject:.*? CN\s?=\s?([^\s,;/]+)", to_text(out, errors='surrogate_or_strict'))
if common_name is not None:
domains.add(common_name.group(1))
subject_alt_names = re.search(
r"X509v3 Subject Alternative Name: (?:critical)?\n +([^\n]+)\n",
to_text(out, errors='surrogate_or_strict'), re.MULTILINE | re.DOTALL)
if subject_alt_names is not None:
for san in subject_alt_names.group(1).split(", "):
if san.startswith("DNS:"):
domains.add(san[4:])
return domains
def _add_or_update_auth(self, domain, auth):
''' '''
Add or update the given authroization in the global authorizations list. Add or update the given authroization in the global authorizations list.
Return True if the auth was updated/added and False if no change was Return True if the auth was updated/added and False if no change was
necessary. necessary.
''' '''
if self.authorizations.get(domain) == auth: if self.authorizations.get(identifier_type + ':' + identifier) == auth:
return False return False
self.authorizations[domain] = auth self.authorizations[identifier_type + ':' + identifier] = auth
return True return True
def _new_authz_v1(self, domain): def _new_authz_v1(self, identifier_type, identifier):
''' '''
Create a new authorization for the given domain. Create a new authorization for the given identifier.
Return the authorization object of the new authorization Return the authorization object of the new authorization
https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-6.4 https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-6.4
''' '''
@ -505,7 +508,7 @@ class ACMEClient(object):
new_authz = { new_authz = {
"resource": "new-authz", "resource": "new-authz",
"identifier": {"type": "dns", "value": domain}, "identifier": {"type": identifier_type, "value": identifier},
} }
result, info = self.account.send_signed_request(self.directory['new-authz'], new_authz) result, info = self.account.send_signed_request(self.directory['new-authz'], new_authz)
@ -515,7 +518,7 @@ class ACMEClient(object):
result['uri'] = info['location'] result['uri'] = info['location']
return result return result
def _get_challenge_data(self, auth, domain): def _get_challenge_data(self, auth, identifier_type, identifier):
''' '''
Returns a dict with the data for all proposed (and supported) challenges Returns a dict with the data for all proposed (and supported) challenges
of the given authorization. of the given authorization.
@ -526,31 +529,55 @@ class ACMEClient(object):
# is not responsible for fulfilling the challenges. Calculate # is not responsible for fulfilling the challenges. Calculate
# and return the required information for each challenge. # and return the required information for each challenge.
for challenge in auth['challenges']: for challenge in auth['challenges']:
type = challenge['type'] challenge_type = challenge['type']
token = re.sub(r"[^A-Za-z0-9_\-]", "_", challenge['token']) token = re.sub(r"[^A-Za-z0-9_\-]", "_", challenge['token'])
keyauthorization = self.account.get_keyauthorization(token) keyauthorization = self.account.get_keyauthorization(token)
if type == 'http-01': if challenge_type == 'http-01':
# https://tools.ietf.org/html/draft-ietf-acme-acme-18#section-8.3 # https://tools.ietf.org/html/draft-ietf-acme-acme-18#section-8.3
resource = '.well-known/acme-challenge/' + token resource = '.well-known/acme-challenge/' + token
data[type] = {'resource': resource, 'resource_value': keyauthorization} data[challenge_type] = {'resource': resource, 'resource_value': keyauthorization}
elif type == 'dns-01': elif challenge_type == 'dns-01':
if identifier_type != 'dns':
continue
# https://tools.ietf.org/html/draft-ietf-acme-acme-18#section-8.4 # https://tools.ietf.org/html/draft-ietf-acme-acme-18#section-8.4
resource = '_acme-challenge' resource = '_acme-challenge'
value = nopad_b64(hashlib.sha256(to_bytes(keyauthorization)).digest()) value = nopad_b64(hashlib.sha256(to_bytes(keyauthorization)).digest())
record = (resource + domain[1:]) if domain.startswith('*.') else (resource + '.' + domain) record = (resource + identifier[1:]) if identifier.startswith('*.') else (resource + '.' + identifier)
data[type] = {'resource': resource, 'resource_value': value, 'record': record} data[challenge_type] = {'resource': resource, 'resource_value': value, 'record': record}
elif type == 'tls-alpn-01': elif challenge_type == 'tls-alpn-01':
# https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-05#section-3 # https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-05#section-3
resource = domain if identifier_type == 'ip':
if ':' in identifier:
# IPv6 address: use reverse IP6.ARPA mapping (RFC3596)
i = identifier.find('::')
if i >= 0:
nibbles = [nibble for nibble in identifier[:i].split(':') if nibble]
suffix = [nibble for nibble in identifier[i + 1:].split(':') if nibble]
if len(nibbles) + len(suffix) < 8:
nibbles.extend(['0'] * (8 - len(nibbles) - len(suffix)))
nibbles.extend(suffix)
else:
nibbles = identifier.split(':')
resource = []
for nibble in reversed(nibbles):
nibble = '0' * (4 - len(nibble)) + nibble.lower()
for octet in reversed(nibble):
resource.append(octet)
resource = '.'.join(resource) + '.ip6.arpa.'
else:
# IPv4 address: use reverse IN-ADDR.ARPA mapping (RFC1034)
resource = '.'.join(reversed(identifier.split('.'))) + '.in-addr.arpa.'
else:
resource = identifier
value = base64.b64encode(hashlib.sha256(to_bytes(keyauthorization)).digest()) value = base64.b64encode(hashlib.sha256(to_bytes(keyauthorization)).digest())
data[type] = {'resource': resource, 'resource_value': value} data[challenge_type] = {'resource': resource, 'resource_original': identifier_type + ':' + identifier, 'resource_value': value}
else: else:
continue continue
return data return data
def _fail_challenge(self, domain, auth, error): def _fail_challenge(self, identifier_type, identifier, auth, error):
''' '''
Aborts with a specific error for a challenge. Aborts with a specific error for a challenge.
''' '''
@ -564,9 +591,9 @@ class ACMEClient(object):
error_details += ' DETAILS: {0};'.format(challenge['error']['detail']) error_details += ' DETAILS: {0};'.format(challenge['error']['detail'])
else: else:
error_details += ';' error_details += ';'
raise ModuleFailException("{0}: {1}".format(error.format(domain), error_details)) raise ModuleFailException("{0}: {1}".format(error.format(identifier_type + ':' + identifier), error_details))
def _validate_challenges(self, domain, auth): def _validate_challenges(self, identifier_type, identifier, auth):
''' '''
Validate the authorization provided in the auth dict. Returns True Validate the authorization provided in the auth dict. Returns True
when the validation was successful and False when it was not. when the validation was successful and False when it was not.
@ -592,7 +619,7 @@ class ACMEClient(object):
while status not in ['valid', 'invalid', 'revoked']: while status not in ['valid', 'invalid', 'revoked']:
result, dummy = self.account.get_request(auth['uri']) result, dummy = self.account.get_request(auth['uri'])
result['uri'] = auth['uri'] result['uri'] = auth['uri']
if self._add_or_update_auth(domain, result): if self._add_or_update_auth(identifier_type, identifier, result):
self.changed = True self.changed = True
# https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-6.1.2 # https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-6.1.2
# "status (required, string): ... # "status (required, string): ...
@ -604,7 +631,7 @@ class ACMEClient(object):
time.sleep(2) time.sleep(2)
if status == 'invalid': if status == 'invalid':
self._fail_challenge(domain, result, 'Authorization for {0} returned invalid') self._fail_challenge(identifier_type, identifier, result, 'Authorization for {0} returned invalid')
return status == 'valid' return status == 'valid'
@ -717,10 +744,10 @@ class ACMEClient(object):
https://tools.ietf.org/html/draft-ietf-acme-acme-18#section-7.4 https://tools.ietf.org/html/draft-ietf-acme-acme-18#section-7.4
''' '''
identifiers = [] identifiers = []
for domain in self.domains: for identifier_type, identifier in self.identifiers:
identifiers.append({ identifiers.append({
'type': 'dns', 'type': identifier_type,
'value': domain, 'value': identifier,
}) })
new_order = { new_order = {
"identifiers": identifiers "identifiers": identifiers
@ -733,10 +760,11 @@ class ACMEClient(object):
for auth_uri in result['authorizations']: for auth_uri in result['authorizations']:
auth_data, dummy = self.account.get_request(auth_uri) auth_data, dummy = self.account.get_request(auth_uri)
auth_data['uri'] = auth_uri auth_data['uri'] = auth_uri
domain = auth_data['identifier']['value'] identifier_type = auth_data['identifier']['type']
identifier = auth_data['identifier']['value']
if auth_data.get('wildcard', False): if auth_data.get('wildcard', False):
domain = '*.{0}'.format(domain) identifier = '*.{0}'.format(identifier)
self.authorizations[domain] = auth_data self.authorizations[identifier_type + ':' + identifier] = auth_data
self.order_uri = info['location'] self.order_uri = info['location']
self.finalize_uri = result['finalize'] self.finalize_uri = result['finalize']
@ -758,14 +786,17 @@ class ACMEClient(object):
def start_challenges(self): def start_challenges(self):
''' '''
Create new authorizations for all domains of the CSR, Create new authorizations for all identifiers of the CSR,
respectively start a new order for ACME v2. respectively start a new order for ACME v2.
''' '''
self.authorizations = {} self.authorizations = {}
if self.version == 1: if self.version == 1:
for domain in self.domains: for identifier_type, identifier in self.identifiers:
new_auth = self._new_authz_v1(domain) if identifier_type != 'dns':
self._add_or_update_auth(domain, new_auth) raise ModuleFailException('ACME v1 only supports DNS identifiers!')
for identifier_type, identifier in self.identifiers:
new_auth = self._new_authz_v1(identifier_type, identifier)
self._add_or_update_auth(identifier_type, identifier, new_auth)
else: else:
self._new_order_v2() self._new_order_v2()
self.changed = True self.changed = True
@ -777,12 +808,14 @@ class ACMEClient(object):
''' '''
# Get general challenge data # Get general challenge data
data = {} data = {}
for domain, auth in self.authorizations.items(): for type_identifier, auth in self.authorizations.items():
data[domain] = self._get_challenge_data(self.authorizations[domain], domain) identifier_type, identifier = type_identifier.split(':', 1)
# We drop the type from the key to preserve backwards compatibility
data[identifier] = self._get_challenge_data(self.authorizations[type_identifier], identifier_type, identifier)
# Get DNS challenge data # Get DNS challenge data
data_dns = {} data_dns = {}
if self.challenge == 'dns-01': if self.challenge == 'dns-01':
for domain, challenges in data.items(): for identifier, challenges in data.items():
if self.challenge in challenges: if self.challenge in challenges:
values = data_dns.get(challenges[self.challenge]['record']) values = data_dns.get(challenges[self.challenge]['record'])
if values is None: if values is None:
@ -793,7 +826,7 @@ class ACMEClient(object):
def finish_challenges(self): def finish_challenges(self):
''' '''
Verify challenges for all domains of the CSR. Verify challenges for all identifiers of the CSR.
''' '''
self.authorizations = {} self.authorizations = {}
@ -801,9 +834,9 @@ class ACMEClient(object):
if self.version == 1: if self.version == 1:
# For ACME v1, we attempt to create new authzs. Existing ones # For ACME v1, we attempt to create new authzs. Existing ones
# will be returned instead. # will be returned instead.
for domain in self.domains: for identifier_type, identifier in self.identifiers:
new_auth = self._new_authz_v1(domain) new_auth = self._new_authz_v1(identifier_type, identifier)
self._add_or_update_auth(domain, new_auth) self._add_or_update_auth(identifier_type, identifier, new_auth)
else: else:
# For ACME v2, we obtain the order object by fetching the # For ACME v2, we obtain the order object by fetching the
# order URI, and extract the information from there. # order URI, and extract the information from there.
@ -818,17 +851,19 @@ class ACMEClient(object):
for auth_uri in result['authorizations']: for auth_uri in result['authorizations']:
auth_data, dummy = self.account.get_request(auth_uri) auth_data, dummy = self.account.get_request(auth_uri)
auth_data['uri'] = auth_uri auth_data['uri'] = auth_uri
domain = auth_data['identifier']['value'] identifier_type = auth_data['identifier']['type']
identifier = auth_data['identifier']['value']
if auth_data.get('wildcard', False): if auth_data.get('wildcard', False):
domain = '*.{0}'.format(domain) identifier = '*.{0}'.format(identifier)
self.authorizations[domain] = auth_data self.authorizations[identifier_type + ':' + identifier] = auth_data
self.finalize_uri = result['finalize'] self.finalize_uri = result['finalize']
# Step 2: validate challenges # Step 2: validate challenges
for domain, auth in self.authorizations.items(): for type_identifier, auth in self.authorizations.items():
if auth['status'] == 'pending': if auth['status'] == 'pending':
self._validate_challenges(domain, auth) identifier_type, identifier = type_identifier.split(':', 1)
self._validate_challenges(identifier_type, identifier, auth)
def get_certificate(self): def get_certificate(self):
''' '''
@ -836,14 +871,14 @@ class ACMEClient(object):
First verifies whether all authorizations are valid; if not, aborts First verifies whether all authorizations are valid; if not, aborts
with an error. with an error.
''' '''
for domain in self.domains: for identifier_type, identifier in self.identifiers:
auth = self.authorizations.get(domain) auth = self.authorizations.get(identifier_type + ':' + identifier)
if auth is None: if auth is None:
raise ModuleFailException('Found no authorization information for "{0}"!'.format(domain)) raise ModuleFailException('Found no authorization information for "{0}"!'.format(identifier_type + ':' + identifier))
if 'status' not in auth: if 'status' not in auth:
self._fail_challenge(domain, auth, 'Authorization for {0} returned no status') self._fail_challenge(identifier_type, identifier, auth, 'Authorization for {0} returned no status')
if auth['status'] != 'valid': if auth['status'] != 'valid':
self._fail_challenge(domain, auth, 'Authorization for {0} returned status ' + str(auth['status'])) self._fail_challenge(identifier_type, identifier, auth, 'Authorization for {0} returned status ' + str(auth['status']))
if self.version == 1: if self.version == 1:
cert = self._new_cert_v1() cert = self._new_cert_v1()
@ -879,8 +914,8 @@ class ACMEClient(object):
if self.version == 1: if self.version == 1:
authz_deactivate['resource'] = 'authz' authz_deactivate['resource'] = 'authz'
if self.authorizations: if self.authorizations:
for domain in self.domains: for identifier_type, identifier in self.identifiers:
auth = self.authorizations.get(domain) auth = self.authorizations.get(identifier_type + ':' + identifier)
if auth is None or auth.get('status') != 'valid': if auth is None or auth.get('status') != 'valid':
continue continue
try: try:
@ -968,9 +1003,13 @@ def main():
if module.params['deactivate_authzs']: if module.params['deactivate_authzs']:
client.deactivate_authzs() client.deactivate_authzs()
data, data_dns = client.get_challenges_data() data, data_dns = client.get_challenges_data()
auths = dict()
for k, v in client.authorizations.items():
# Remove "type:" from key
auths[k.split(':', 1)[1]] = v
module.exit_json( module.exit_json(
changed=client.changed, changed=client.changed,
authorizations=client.authorizations, authorizations=auths,
finalize_uri=client.finalize_uri, finalize_uri=client.finalize_uri,
order_uri=client.order_uri, order_uri=client.order_uri,
account_uri=client.account.uri, account_uri=client.account.uri,

View file

@ -0,0 +1,11 @@
-----BEGIN CERTIFICATE-----
MIIBljCCATugAwIBAgIBATAKBggqhkjOPQQDAjAWMRQwEgYDVQQDEwthbnNpYmxl
LmNvbTAeFw0xODExMjUxNTI4MjNaFw0xODExMjYxNTI4MjRaMBYxFDASBgNVBAMT
C2Fuc2libGUuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEAJz0yAAXAwEm
OhTRkjXxwgedbWO6gobYM3lWszrS68G8QSzhXR6AmQ3IzZDimnTTXO7XhVylDT8S
LzE44/Epm6N6MHgwIwYDVR0RBBwwGoILZXhhbXBsZS5jb22CC2V4YW1wbGUub3Jn
MAwGA1UdEwEB/wQCMAAwDwYDVR0PAQH/BAUDAweAADATBgNVHSUEDDAKBggrBgEF
BQcDATAdBgNVHQ4EFgQUmNL9PMzNaUX74owwLFRiGDS3B3MwCgYIKoZIzj0EAwID
SQAwRgIhALz7Ur96ky0OfM5D9MwFmCg2jccqm/UglGI9+4KeOEIyAiEAwFX4tdll
QSrd1HY/jMsHwdK5wH3JkK/9+fGwyRP11VI=
-----END CERTIFICATE-----

View file

@ -0,0 +1,9 @@
-----BEGIN NEW CERTIFICATE REQUEST-----
MIIBJTCBzQIBADAWMRQwEgYDVQQDEwthbnNpYmxlLmNvbTBZMBMGByqGSM49AgEG
CCqGSM49AwEHA0IABACc9MgAFwMBJjoU0ZI18cIHnW1juoKG2DN5VrM60uvBvEEs
4V0egJkNyM2Q4pp001zu14VcpQ0/Ei8xOOPxKZugVTBTBgkqhkiG9w0BCQ4xRjBE
MCMGA1UdEQQcMBqCC2V4YW1wbGUuY29tggtleGFtcGxlLm9yZzAMBgNVHRMBAf8E
AjAAMA8GA1UdDwEB/wQFAwMHgAAwCgYIKoZIzj0EAwIDRwAwRAIgcDyoRmwFVBDl
FvbFZtiSd5wmJU1ltM6JtcfnLWnjY54CICruOByrropFUkOKKb4xXOYsgaDT93Wr
URnCJfTLr2T3
-----END NEW CERTIFICATE REQUEST-----

View file

@ -0,0 +1,28 @@
Certificate Request:
Data:
Version: 1 (0x0)
Subject: CN = ansible.com
Subject Public Key Info:
Public Key Algorithm: id-ecPublicKey
Public-Key: (256 bit)
pub:
04:00:9c:f4:c8:00:17:03:01:26:3a:14:d1:92:35:
f1:c2:07:9d:6d:63:ba:82:86:d8:33:79:56:b3:3a:
d2:eb:c1:bc:41:2c:e1:5d:1e:80:99:0d:c8:cd:90:
e2:9a:74:d3:5c:ee:d7:85:5c:a5:0d:3f:12:2f:31:
38:e3:f1:29:9b
ASN1 OID: prime256v1
NIST CURVE: P-256
Attributes:
Requested Extensions:
X509v3 Subject Alternative Name:
DNS:example.com, DNS:example.org
X509v3 Basic Constraints: critical
CA:FALSE
X509v3 Key Usage: critical
Digital Signature
Signature Algorithm: ecdsa-with-SHA256
30:44:02:20:70:3c:a8:46:6c:05:54:10:e5:16:f6:c5:66:d8:
92:77:9c:26:25:4d:65:b4:ce:89:b5:c7:e7:2d:69:e3:63:9e:
02:20:2a:ee:38:1c:ab:ae:8a:45:52:43:8a:29:be:31:5c:e6:
2c:81:a0:d3:f7:75:ab:51:19:c2:25:f4:cb:af:64:f7

View file

@ -0,0 +1,27 @@
-----BEGIN CERTIFICATE REQUEST-----
MIIEqjCCApICAQAwADCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANv1
V7gDsh76O//d9wclBcW6kNpWeR6eAggzThwbMZjcO7GFHQsBZCZGGVdyS37uhejc
RrIBdtDDWXhoh3Dz+GQxD+6GuwAEFyL1F3MfT0v1HHoO8fE74G5mD6+ZA2HRDeU9
jf8BPyVWHBtNbCmJGSlSNOFejWCmwvsLARQxqFBuTyRjgos4BkLyWMqZRukrzO1P
z7IBhuFrB608t+AG4vGnPXZNM7xefhzO8bPOiepT0YS2ERPkFmOy97SnwTGdKykw
ZYM9oKukYhE4Z+yOaTFpJMBNXwDCI5TMnhtc6eJrf5sOFH92n2E9+YWMoahUOiTw
G6XV5HfSpySpwORUaTITQRsPAM+bmK9f1jB6ctfFVwpa8uW/h8pSgbHgZvkeD6s6
rFLh9TQ24t0vrRmhnY7/AMFgbgJoBTBq0l0lEXS4FCGKDGqQOqSws+eHR/pHA4uY
v8d498SQl9fYsT/c7Uj3/JnMSRVN942yQUFCzwLf0/WzWCi2HTqPM8CPh5ryiJ30
GAN2eb026/noyTOXm479Tg9o86Tw9qczE0j0CdcRnr6J337RGHQg58PZ7j+hnUmK
wgyclyvjE10ZFBgToMGSnzYp5UeRcOFZ3bnK6LOsGC75mIvz2OQgSQeO5VQASEnO
9uhygNyo91sK4BtVroloit8ZCa82LlsHSCj/mMzPAgMBAAGgZTBjBgkqhkiG9w0B
CQ4xVjBUMFIGA1UdEQRLMEmCC2Fuc2libGUuY29thwR/AAABhxAAAAAAAAAAAAAA
AAAAAAABhxAgAQ2IrBD+AQAAAAAAAAAAhxAgARI0VnirzZh2VDIQ/ty6MA0GCSqG
SIb3DQEBCwUAA4ICAQBFRuANzVRcze+iur0YevjtYIXDa03GoWWkgnLuE8u8epTM
2248duG3TmvVvxWPN4iFrvFcZIvNsevBo+Z7kXJ24m3YldtXvwfAYmCZ062apSoh
yzgo3Q0KfDehwLcoJPe5bh+jbbgJVGGvJug/QFyHSVl+iGyFUXE7pwafl9LuNDi3
yfOYZLIQ34mBH4Rsvymj9xSTYliWDEEU/o7RrrZeEqkOxNeLh64LbnifdrYUputz
yBURg2xs9hpAsytZJX90iJW8aYPM1aQ7eetqTViIRoqUAmIQobnKlNnpOliBHl+p
RY+AtTnsfAetKUP7OsAZkHRTGAXx0JHJQ1ITY8w5Dcw/v1bDCbAfkDubBP3X+us9
RQk2h6m74hWFFNu9xOfkNejPf7h4gywfDjo/wGZFSWKyi6avB9V53znZgRUwc009
p5MM9e37MH8pyBqfnbSwOj4hUoyecRCIAFdywjMb9akP2u15XP3MOtJOEvecyCxN
TZBxupTg65zB47GeSAufnc8FaTZkE8xPuCtbvqOVOkWYqzlqNdCfK8f3AZdlpwLh
38wdUm5G7LIu6aQNiY66aQs9qVpoGvqdmxHRkuSwqwZxGgzcY1yJaWGXQ6R4jgC3
VKlMTUVs1WYV6jrYLHcVt6Rn/2FVTOns3Jn6cTPOdKViYoqF+yW8yCEAqAskZw==
-----END CERTIFICATE REQUEST-----

View file

@ -0,0 +1,78 @@
Certificate Request:
Data:
Version: 1 (0x0)
Subject:
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
RSA Public-Key: (4096 bit)
Modulus:
00:db:f5:57:b8:03:b2:1e:fa:3b:ff:dd:f7:07:25:
05:c5:ba:90:da:56:79:1e:9e:02:08:33:4e:1c:1b:
31:98:dc:3b:b1:85:1d:0b:01:64:26:46:19:57:72:
4b:7e:ee:85:e8:dc:46:b2:01:76:d0:c3:59:78:68:
87:70:f3:f8:64:31:0f:ee:86:bb:00:04:17:22:f5:
17:73:1f:4f:4b:f5:1c:7a:0e:f1:f1:3b:e0:6e:66:
0f:af:99:03:61:d1:0d:e5:3d:8d:ff:01:3f:25:56:
1c:1b:4d:6c:29:89:19:29:52:34:e1:5e:8d:60:a6:
c2:fb:0b:01:14:31:a8:50:6e:4f:24:63:82:8b:38:
06:42:f2:58:ca:99:46:e9:2b:cc:ed:4f:cf:b2:01:
86:e1:6b:07:ad:3c:b7:e0:06:e2:f1:a7:3d:76:4d:
33:bc:5e:7e:1c:ce:f1:b3:ce:89:ea:53:d1:84:b6:
11:13:e4:16:63:b2:f7:b4:a7:c1:31:9d:2b:29:30:
65:83:3d:a0:ab:a4:62:11:38:67:ec:8e:69:31:69:
24:c0:4d:5f:00:c2:23:94:cc:9e:1b:5c:e9:e2:6b:
7f:9b:0e:14:7f:76:9f:61:3d:f9:85:8c:a1:a8:54:
3a:24:f0:1b:a5:d5:e4:77:d2:a7:24:a9:c0:e4:54:
69:32:13:41:1b:0f:00:cf:9b:98:af:5f:d6:30:7a:
72:d7:c5:57:0a:5a:f2:e5:bf:87:ca:52:81:b1:e0:
66:f9:1e:0f:ab:3a:ac:52:e1:f5:34:36:e2:dd:2f:
ad:19:a1:9d:8e:ff:00:c1:60:6e:02:68:05:30:6a:
d2:5d:25:11:74:b8:14:21:8a:0c:6a:90:3a:a4:b0:
b3:e7:87:47:fa:47:03:8b:98:bf:c7:78:f7:c4:90:
97:d7:d8:b1:3f:dc:ed:48:f7:fc:99:cc:49:15:4d:
f7:8d:b2:41:41:42:cf:02:df:d3:f5:b3:58:28:b6:
1d:3a:8f:33:c0:8f:87:9a:f2:88:9d:f4:18:03:76:
79:bd:36:eb:f9:e8:c9:33:97:9b:8e:fd:4e:0f:68:
f3:a4:f0:f6:a7:33:13:48:f4:09:d7:11:9e:be:89:
df:7e:d1:18:74:20:e7:c3:d9:ee:3f:a1:9d:49:8a:
c2:0c:9c:97:2b:e3:13:5d:19:14:18:13:a0:c1:92:
9f:36:29:e5:47:91:70:e1:59:dd:b9:ca:e8:b3:ac:
18:2e:f9:98:8b:f3:d8:e4:20:49:07:8e:e5:54:00:
48:49:ce:f6:e8:72:80:dc:a8:f7:5b:0a:e0:1b:55:
ae:89:68:8a:df:19:09:af:36:2e:5b:07:48:28:ff:
98:cc:cf
Exponent: 65537 (0x10001)
Attributes:
Requested Extensions:
X509v3 Subject Alternative Name:
DNS:ansible.com, IP Address:127.0.0.1, IP Address:0:0:0:0:0:0:0:1, IP Address:2001:D88:AC10:FE01:0:0:0:0, IP Address:2001:1234:5678:ABCD:9876:5432:10FE:DCBA
Signature Algorithm: sha256WithRSAEncryption
45:46:e0:0d:cd:54:5c:cd:ef:a2:ba:bd:18:7a:f8:ed:60:85:
c3:6b:4d:c6:a1:65:a4:82:72:ee:13:cb:bc:7a:94:cc:db:6e:
3c:76:e1:b7:4e:6b:d5:bf:15:8f:37:88:85:ae:f1:5c:64:8b:
cd:b1:eb:c1:a3:e6:7b:91:72:76:e2:6d:d8:95:db:57:bf:07:
c0:62:60:99:d3:ad:9a:a5:2a:21:cb:38:28:dd:0d:0a:7c:37:
a1:c0:b7:28:24:f7:b9:6e:1f:a3:6d:b8:09:54:61:af:26:e8:
3f:40:5c:87:49:59:7e:88:6c:85:51:71:3b:a7:06:9f:97:d2:
ee:34:38:b7:c9:f3:98:64:b2:10:df:89:81:1f:84:6c:bf:29:
a3:f7:14:93:62:58:96:0c:41:14:fe:8e:d1:ae:b6:5e:12:a9:
0e:c4:d7:8b:87:ae:0b:6e:78:9f:76:b6:14:a6:eb:73:c8:15:
11:83:6c:6c:f6:1a:40:b3:2b:59:25:7f:74:88:95:bc:69:83:
cc:d5:a4:3b:79:eb:6a:4d:58:88:46:8a:94:02:62:10:a1:b9:
ca:94:d9:e9:3a:58:81:1e:5f:a9:45:8f:80:b5:39:ec:7c:07:
ad:29:43:fb:3a:c0:19:90:74:53:18:05:f1:d0:91:c9:43:52:
13:63:cc:39:0d:cc:3f:bf:56:c3:09:b0:1f:90:3b:9b:04:fd:
d7:fa:eb:3d:45:09:36:87:a9:bb:e2:15:85:14:db:bd:c4:e7:
e4:35:e8:cf:7f:b8:78:83:2c:1f:0e:3a:3f:c0:66:45:49:62:
b2:8b:a6:af:07:d5:79:df:39:d9:81:15:30:73:4d:3d:a7:93:
0c:f5:ed:fb:30:7f:29:c8:1a:9f:9d:b4:b0:3a:3e:21:52:8c:
9e:71:10:88:00:57:72:c2:33:1b:f5:a9:0f:da:ed:79:5c:fd:
cc:3a:d2:4e:12:f7:9c:c8:2c:4d:4d:90:71:ba:94:e0:eb:9c:
c1:e3:b1:9e:48:0b:9f:9d:cf:05:69:36:64:13:cc:4f:b8:2b:
5b:be:a3:95:3a:45:98:ab:39:6a:35:d0:9f:2b:c7:f7:01:97:
65:a7:02:e1:df:cc:1d:52:6e:46:ec:b2:2e:e9:a4:0d:89:8e:
ba:69:0b:3d:a9:5a:68:1a:fa:9d:9b:11:d1:92:e4:b0:ab:06:
71:1a:0c:dc:63:5c:89:69:61:97:43:a4:78:8e:00:b7:54:a9:
4c:4d:45:6c:d5:66:15:ea:3a:d8:2c:77:15:b7:a4:67:ff:61:
55:4c:e9:ec:dc:99:fa:71:33:ce:74:a5:62:62:8a:85:fb:25:
bc:c8:21:00:a8:0b:24:67

View file

@ -0,0 +1,5 @@
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIDWajU0PyhYKeulfy/luNtkAve7DkwQ01bXJ97zbxB66oAoGCCqGSM49
AwEHoUQDQgAEAJz0yAAXAwEmOhTRkjXxwgedbWO6gobYM3lWszrS68G8QSzhXR6A
mQ3IzZDimnTTXO7XhVylDT8SLzE44/Epmw==
-----END EC PRIVATE KEY-----

View file

@ -0,0 +1,14 @@
read EC key
Private-Key: (256 bit)
priv:
35:9a:8d:4d:0f:ca:16:0a:7a:e9:5f:cb:f9:6e:36:
d9:00:bd:ee:c3:93:04:34:d5:b5:c9:f7:bc:db:c4:
1e:ba
pub:
04:00:9c:f4:c8:00:17:03:01:26:3a:14:d1:92:35:
f1:c2:07:9d:6d:63:ba:82:86:d8:33:79:56:b3:3a:
d2:eb:c1:bc:41:2c:e1:5d:1e:80:99:0d:c8:cd:90:
e2:9a:74:d3:5c:ee:d7:85:5c:a5:0d:3f:12:2f:31:
38:e3:f1:29:9b
ASN1 OID: prime256v1
NIST CURVE: P-256

View file

@ -1,5 +1,6 @@
import base64 import base64
import datetime import datetime
import os.path
import pytest import pytest
from mock import MagicMock from mock import MagicMock
@ -15,10 +16,18 @@ from ansible.module_utils.acme import (
# _sign_request_openssl, # _sign_request_openssl,
_parse_key_cryptography, _parse_key_cryptography,
# _sign_request_cryptography, # _sign_request_cryptography,
cryptography_get_csr_domains, _normalize_ip,
openssl_get_csr_identifiers,
cryptography_get_csr_identifiers,
cryptography_get_cert_days, cryptography_get_cert_days,
) )
def load_fixture(name):
with open(os.path.join(os.path.dirname(__file__), 'fixtures', name)) as f:
return f.read()
################################################ ################################################
NOPAD_B64 = [ NOPAD_B64 = [
@ -58,13 +67,7 @@ def test_write_file(tmpdir):
TEST_PEM_DERS = [ TEST_PEM_DERS = [
( (
r""" load_fixture('privatekey_1.pem'),
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIDWajU0PyhYKeulfy/luNtkAve7DkwQ01bXJ97zbxB66oAoGCCqGSM49
AwEHoUQDQgAEAJz0yAAXAwEmOhTRkjXxwgedbWO6gobYM3lWszrS68G8QSzhXR6A
mQ3IzZDimnTTXO7XhVylDT8SLzE44/Epmw==
-----END EC PRIVATE KEY-----
""",
base64.b64decode('MHcCAQEEIDWajU0PyhYKeulfy/luNtkAve7DkwQ01bXJ97zbxB66oAo' base64.b64decode('MHcCAQEEIDWajU0PyhYKeulfy/luNtkAve7DkwQ01bXJ97zbxB66oAo'
'GCCqGSM49AwEHoUQDQgAEAJz0yAAXAwEmOhTRkjXxwgedbWO6gobYM3' 'GCCqGSM49AwEHoUQDQgAEAJz0yAAXAwEmOhTRkjXxwgedbWO6gobYM3'
'lWszrS68G8QSzhXR6AmQ3IzZDimnTTXO7XhVylDT8SLzE44/Epmw==') 'lWszrS68G8QSzhXR6AmQ3IzZDimnTTXO7XhVylDT8SLzE44/Epmw==')
@ -83,13 +86,7 @@ def test_pem_to_der(pem, der, tmpdir):
TEST_KEYS = [ TEST_KEYS = [
( (
r""" load_fixture('privatekey_1.pem'),
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIDWajU0PyhYKeulfy/luNtkAve7DkwQ01bXJ97zbxB66oAoGCCqGSM49
AwEHoUQDQgAEAJz0yAAXAwEmOhTRkjXxwgedbWO6gobYM3lWszrS68G8QSzhXR6A
mQ3IzZDimnTTXO7XhVylDT8SLzE44/Epmw==
-----END EC PRIVATE KEY-----
""",
{ {
'alg': 'ES256', 'alg': 'ES256',
'hash': 'sha256', 'hash': 'sha256',
@ -102,22 +99,7 @@ mQ3IzZDimnTTXO7XhVylDT8SLzE44/Epmw==
'point_size': 32, 'point_size': 32,
'type': 'ec', 'type': 'ec',
}, },
r""" load_fixture('privatekey_1.txt'),
read EC key
Private-Key: (256 bit)
priv:
35:9a:8d:4d:0f:ca:16:0a:7a:e9:5f:cb:f9:6e:36:
d9:00:bd:ee:c3:93:04:34:d5:b5:c9:f7:bc:db:c4:
1e:ba
pub:
04:00:9c:f4:c8:00:17:03:01:26:3a:14:d1:92:35:
f1:c2:07:9d:6d:63:ba:82:86:d8:33:79:56:b3:3a:
d2:eb:c1:bc:41:2c:e1:5d:1e:80:99:0d:c8:cd:90:
e2:9a:74:d3:5c:ee:d7:85:5c:a5:0d:3f:12:2f:31:
38:e3:f1:29:9b
ASN1 OID: prime256v1
NIST CURVE: P-256
"""
) )
] ]
@ -146,43 +128,73 @@ if HAS_CURRENT_CRYPTOGRAPHY:
################################################ ################################################
TEST_CSR = r""" TEST_IPS = [
-----BEGIN NEW CERTIFICATE REQUEST----- ("0:0:0:0:0:0:0:1", "::1"),
MIIBJTCBzQIBADAWMRQwEgYDVQQDEwthbnNpYmxlLmNvbTBZMBMGByqGSM49AgEG ("1::0:2", "1::2"),
CCqGSM49AwEHA0IABACc9MgAFwMBJjoU0ZI18cIHnW1juoKG2DN5VrM60uvBvEEs ("0000:0001:0000:0000:0000:0000:0000:0001", "0:1::1"),
4V0egJkNyM2Q4pp001zu14VcpQ0/Ei8xOOPxKZugVTBTBgkqhkiG9w0BCQ4xRjBE ("0000:0001:0000:0000:0001:0000:0000:0001", "0:1::1:0:0:1"),
MCMGA1UdEQQcMBqCC2V4YW1wbGUuY29tggtleGFtcGxlLm9yZzAMBgNVHRMBAf8E ("0000:0001:0000:0001:0000:0001:0000:0001", "0:1:0:1:0:1:0:1"),
AjAAMA8GA1UdDwEB/wQFAwMHgAAwCgYIKoZIzj0EAwIDRwAwRAIgcDyoRmwFVBDl ("0.0.0.0", "0.0.0.0"),
FvbFZtiSd5wmJU1ltM6JtcfnLWnjY54CICruOByrropFUkOKKb4xXOYsgaDT93Wr ("000.001.000.000", "0.1.0.0"),
URnCJfTLr2T3 ("2001:d88:ac10:fe01:0:0:0:0", "2001:d88:ac10:fe01::"),
-----END NEW CERTIFICATE REQUEST----- ("0000:0000:0000:0000:0000:0000:0000:0000", "::"),
""" ]
if HAS_CURRENT_CRYPTOGRAPHY: @pytest.mark.parametrize("ip, result", TEST_IPS)
def test_csrdomains_cryptography(tmpdir): def test_normalize_ip(ip, result):
fn = tmpdir / 'test.csr' assert _normalize_ip(ip) == result
fn.write(TEST_CSR)
module = MagicMock()
domains = cryptography_get_csr_domains(module, str(fn))
assert domains == set(['ansible.com', 'example.com', 'example.org'])
################################################ ################################################
TEST_CERT = r""" TEST_CSRS = [
-----BEGIN CERTIFICATE----- (
MIIBljCCATugAwIBAgIBATAKBggqhkjOPQQDAjAWMRQwEgYDVQQDEwthbnNpYmxl load_fixture('csr_1.pem'),
LmNvbTAeFw0xODExMjUxNTI4MjNaFw0xODExMjYxNTI4MjRaMBYxFDASBgNVBAMT set([
C2Fuc2libGUuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEAJz0yAAXAwEm ('dns', 'ansible.com'),
OhTRkjXxwgedbWO6gobYM3lWszrS68G8QSzhXR6AmQ3IzZDimnTTXO7XhVylDT8S ('dns', 'example.com'),
LzE44/Epm6N6MHgwIwYDVR0RBBwwGoILZXhhbXBsZS5jb22CC2V4YW1wbGUub3Jn ('dns', 'example.org')
MAwGA1UdEwEB/wQCMAAwDwYDVR0PAQH/BAUDAweAADATBgNVHSUEDDAKBggrBgEF ]),
BQcDATAdBgNVHQ4EFgQUmNL9PMzNaUX74owwLFRiGDS3B3MwCgYIKoZIzj0EAwID load_fixture('csr_1.txt'),
SQAwRgIhALz7Ur96ky0OfM5D9MwFmCg2jccqm/UglGI9+4KeOEIyAiEAwFX4tdll ),
QSrd1HY/jMsHwdK5wH3JkK/9+fGwyRP11VI= (
-----END CERTIFICATE----- load_fixture('csr_2.pem'),
""" set([
('dns', 'ansible.com'),
('ip', '127.0.0.1'),
('ip', '::1'),
('ip', '2001:d88:ac10:fe01::'),
('ip', '2001:1234:5678:abcd:9876:5432:10fe:dcba')
]),
load_fixture('csr_2.txt'),
),
]
@pytest.mark.parametrize("csr, result, openssl_output", TEST_CSRS)
def test_csridentifiers_openssl(csr, result, openssl_output, tmpdir):
fn = tmpdir / 'test.csr'
fn.write(csr)
module = MagicMock()
module.run_command = MagicMock(return_value=(0, openssl_output, 0))
identifiers = openssl_get_csr_identifiers('openssl', module, str(fn))
assert identifiers == result
if HAS_CURRENT_CRYPTOGRAPHY:
@pytest.mark.parametrize("csr, result, openssl_output", TEST_CSRS)
def test_csridentifiers_cryptography(csr, result, openssl_output, tmpdir):
fn = tmpdir / 'test.csr'
fn.write(csr)
module = MagicMock()
identifiers = cryptography_get_csr_identifiers(module, str(fn))
assert identifiers == result
################################################
TEST_CERT = load_fixture("cert_1.pem")
TEST_CERT_DAYS = [ TEST_CERT_DAYS = [
(datetime.datetime(2018, 11, 15, 1, 2, 3), 11), (datetime.datetime(2018, 11, 15, 1, 2, 3), 11),