Improve handling of IDNA/Unicode domains (#436)
* Prepare IDNA/Unicode conversion code. Use to normalize input. * Use IDNA library first (IDNA2008) and Python's IDNA2003 implementation as a fallback. * Make sure idna is installed. * Add changelog fragment. * 'punycode' → 'idna'. * Add name_encoding options and tests. * Avoid invalid character for IDNA2008. * Linting. * Forgot to upate value. * Work around cryptography bug. Fix port handling for URIs. * Forgot other place sensitive to cryptography bug. * Forgot one. (Will likely still fail.) * Decode IDNA in _compress_entry() to avoid comparison screw-ups. * Work around Python 3.5 problem in Ansible 2.9's default test container. * Update changelog fragment. * Fix error, add tests. * Python 2 compatibility. * Update requirements.pull/453/head
parent
90efcc1ca7
commit
4cf951596f
|
@ -0,0 +1,12 @@
|
||||||
|
minor_changes:
|
||||||
|
- "Support automatic conversion for Internalionalized Domain Names (IDNs).
|
||||||
|
When passing general names, for example Subject Altenative Names to ``community.crypto.openssl_csr``, these will automatically be converted to IDNA.
|
||||||
|
Conversion will be done per label to IDNA2008 if possible, and IDNA2003 if IDNA2008 conversion fails for that label.
|
||||||
|
Note that IDNA conversion requires `the Python idna library <https://pypi.org/project/idna/>`_ to be installed.
|
||||||
|
Please note that depending on which versions of the cryptography library are used, it could try to process the converted IDNA
|
||||||
|
another time with the Python ``idna`` library and reject IDNA2003 encoded values. Using a new enough ``cryptography`` version avoids this
|
||||||
|
(https://github.com/ansible-collections/community.crypto/issues/426, https://github.com/ansible-collections/community.crypto/pull/436)."
|
||||||
|
- "openssl_csr_info - add ``name_encoding`` option to control the encoding (IDNA, Unicode) used to return domain names in general names (https://github.com/ansible-collections/community.crypto/pull/436)."
|
||||||
|
- "x509_certificate_info - add ``name_encoding`` option to control the encoding (IDNA, Unicode) used to return domain names in general names (https://github.com/ansible-collections/community.crypto/pull/436)."
|
||||||
|
- "x509_crl - add ``name_encoding`` option to control the encoding (IDNA, Unicode) used to return domain names in general names (https://github.com/ansible-collections/community.crypto/pull/436)."
|
||||||
|
- "x509_crl_info - add ``name_encoding`` option to control the encoding (IDNA, Unicode) used to return domain names in general names (https://github.com/ansible-collections/community.crypto/pull/436)."
|
|
@ -0,0 +1,30 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright: (c) 2022, Felix Fontein <felix@fontein.de>
|
||||||
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
|
||||||
|
from __future__ import absolute_import, division, print_function
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleDocFragment(object):
|
||||||
|
DOCUMENTATION = r'''
|
||||||
|
options:
|
||||||
|
name_encoding:
|
||||||
|
description:
|
||||||
|
- How to encode names (DNS names, URIs, email addresses) in return values.
|
||||||
|
- C(ignore) will use the encoding returned by the backend.
|
||||||
|
- C(idna) will convert all labels of domain names to IDNA encoding.
|
||||||
|
IDNA2008 will be preferred, and IDNA2003 will be used if IDNA2008 encoding fails.
|
||||||
|
- C(unicode) will convert all labels of domain names to Unicode.
|
||||||
|
IDNA2008 will be preferred, and IDNA2003 will be used if IDNA2008 decoding fails.
|
||||||
|
- B(Note) that C(idna) and C(unicode) require the L(idna Python library,https://pypi.org/project/idna/) to be installed.
|
||||||
|
type: str
|
||||||
|
default: ignore
|
||||||
|
choices:
|
||||||
|
- ignore
|
||||||
|
- idna
|
||||||
|
- unicode
|
||||||
|
requirements:
|
||||||
|
- If I(name_encoding) is set to another value than C(ignore), the L(idna Python library,https://pypi.org/project/idna/) needs to be installed.
|
||||||
|
'''
|
|
@ -95,12 +95,12 @@ def cryptography_decode_revoked_certificate(cert):
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def cryptography_dump_revoked(entry):
|
def cryptography_dump_revoked(entry, idn_rewrite='ignore'):
|
||||||
return {
|
return {
|
||||||
'serial_number': entry['serial_number'],
|
'serial_number': entry['serial_number'],
|
||||||
'revocation_date': entry['revocation_date'].strftime(TIMESTAMP_FORMAT),
|
'revocation_date': entry['revocation_date'].strftime(TIMESTAMP_FORMAT),
|
||||||
'issuer':
|
'issuer':
|
||||||
[cryptography_decode_name(issuer) for issuer in entry['issuer']]
|
[cryptography_decode_name(issuer, idn_rewrite=idn_rewrite) for issuer in entry['issuer']]
|
||||||
if entry['issuer'] is not None else None,
|
if entry['issuer'] is not None else None,
|
||||||
'issuer_critical': entry['issuer_critical'],
|
'issuer_critical': entry['issuer_critical'],
|
||||||
'reason': REVOCATION_REASON_MAP_INVERSE.get(entry['reason']) if entry['reason'] is not None else None,
|
'reason': REVOCATION_REASON_MAP_INVERSE.get(entry['reason']) if entry['reason'] is not None else None,
|
||||||
|
|
|
@ -23,8 +23,11 @@ import base64
|
||||||
import binascii
|
import binascii
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
import traceback
|
||||||
|
|
||||||
from ansible.module_utils.common.text.converters import to_text, to_bytes
|
from ansible.module_utils.common.text.converters import to_text, to_bytes
|
||||||
|
from ansible.module_utils.six.moves.urllib.parse import urlparse, urlunparse, ParseResult
|
||||||
|
|
||||||
from ._asn1 import serialize_asn1_string_as_der
|
from ._asn1 import serialize_asn1_string_as_der
|
||||||
|
|
||||||
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
|
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
|
||||||
|
@ -80,6 +83,16 @@ except ImportError:
|
||||||
# Error handled in the calling module.
|
# Error handled in the calling module.
|
||||||
_load_pkcs12 = None
|
_load_pkcs12 = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
import idna
|
||||||
|
|
||||||
|
HAS_IDNA = True
|
||||||
|
except ImportError:
|
||||||
|
HAS_IDNA = False
|
||||||
|
IDNA_IMP_ERROR = traceback.format_exc()
|
||||||
|
|
||||||
|
from ansible.module_utils.basic import missing_required_lib
|
||||||
|
|
||||||
from .basic import (
|
from .basic import (
|
||||||
CRYPTOGRAPHY_HAS_DSA_SIGN,
|
CRYPTOGRAPHY_HAS_DSA_SIGN,
|
||||||
CRYPTOGRAPHY_HAS_EC_SIGN,
|
CRYPTOGRAPHY_HAS_EC_SIGN,
|
||||||
|
@ -359,6 +372,80 @@ def cryptography_parse_relative_distinguished_name(rdn):
|
||||||
return cryptography.x509.RelativeDistinguishedName(names)
|
return cryptography.x509.RelativeDistinguishedName(names)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_ascii(value):
|
||||||
|
'''Check whether the Unicode string `value` contains only ASCII characters.'''
|
||||||
|
try:
|
||||||
|
value.encode("ascii")
|
||||||
|
return True
|
||||||
|
except UnicodeEncodeError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _adjust_idn(value, idn_rewrite):
|
||||||
|
if idn_rewrite == 'ignore' or not value:
|
||||||
|
return value
|
||||||
|
if idn_rewrite == 'idna' and _is_ascii(value):
|
||||||
|
return value
|
||||||
|
if idn_rewrite not in ('idna', 'unicode'):
|
||||||
|
raise ValueError('Invalid value for idn_rewrite: "{0}"'.format(idn_rewrite))
|
||||||
|
if not HAS_IDNA:
|
||||||
|
raise OpenSSLObjectError(
|
||||||
|
missing_required_lib('idna', reason='to transform {what} DNS name "{name}" to {dest}'.format(
|
||||||
|
name=value,
|
||||||
|
what='IDNA' if idn_rewrite == 'unicode' else 'Unicode',
|
||||||
|
dest='Unicode' if idn_rewrite == 'unicode' else 'IDNA',
|
||||||
|
)))
|
||||||
|
# Since IDNA does not like '*' or empty labels (except one empty label at the end),
|
||||||
|
# we split and let IDNA only handle labels that are neither empty or '*'.
|
||||||
|
parts = value.split(u'.')
|
||||||
|
for index, part in enumerate(parts):
|
||||||
|
if part in (u'', u'*'):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
if idn_rewrite == 'idna':
|
||||||
|
parts[index] = idna.encode(part).decode('ascii')
|
||||||
|
elif idn_rewrite == 'unicode' and part.startswith(u'xn--'):
|
||||||
|
parts[index] = idna.decode(part)
|
||||||
|
except idna.IDNAError as exc2008:
|
||||||
|
try:
|
||||||
|
if idn_rewrite == 'idna':
|
||||||
|
parts[index] = part.encode('idna').decode('ascii')
|
||||||
|
elif idn_rewrite == 'unicode' and part.startswith(u'xn--'):
|
||||||
|
parts[index] = part.encode('ascii').decode('idna')
|
||||||
|
except Exception as exc2003:
|
||||||
|
raise OpenSSLObjectError(
|
||||||
|
u'Error while transforming part "{part}" of {what} DNS name "{name}" to {dest}.'
|
||||||
|
u' IDNA2008 transformation resulted in "{exc2008}", IDNA2003 transformation resulted in "{exc2003}".'.format(
|
||||||
|
part=part,
|
||||||
|
name=value,
|
||||||
|
what='IDNA' if idn_rewrite == 'unicode' else 'Unicode',
|
||||||
|
dest='Unicode' if idn_rewrite == 'unicode' else 'IDNA',
|
||||||
|
exc2003=exc2003,
|
||||||
|
exc2008=exc2008,
|
||||||
|
))
|
||||||
|
return u'.'.join(parts)
|
||||||
|
|
||||||
|
|
||||||
|
def _adjust_idn_email(value, idn_rewrite):
|
||||||
|
idx = value.find(u'@')
|
||||||
|
if idx < 0:
|
||||||
|
return value
|
||||||
|
return u'{0}@{1}'.format(value[:idx], _adjust_idn(value[idx + 1:], idn_rewrite))
|
||||||
|
|
||||||
|
|
||||||
|
def _adjust_idn_url(value, idn_rewrite):
|
||||||
|
url = urlparse(value)
|
||||||
|
host = _adjust_idn(url.hostname, idn_rewrite)
|
||||||
|
if url.username is not None and url.password is not None:
|
||||||
|
host = u'{0}:{1}@{2}'.format(url.username, url.password, host)
|
||||||
|
elif url.username is not None:
|
||||||
|
host = u'{0}@{1}'.format(url.username, host)
|
||||||
|
if url.port is not None:
|
||||||
|
host = u'{0}:{1}'.format(host, url.port)
|
||||||
|
return urlunparse(
|
||||||
|
ParseResult(scheme=url.scheme, netloc=host, path=url.path, params=url.params, query=url.query, fragment=url.fragment))
|
||||||
|
|
||||||
|
|
||||||
def cryptography_get_name(name, what='Subject Alternative Name'):
|
def cryptography_get_name(name, what='Subject Alternative Name'):
|
||||||
'''
|
'''
|
||||||
Given a name string, returns a cryptography x509.GeneralName object.
|
Given a name string, returns a cryptography x509.GeneralName object.
|
||||||
|
@ -366,16 +453,16 @@ def cryptography_get_name(name, what='Subject Alternative Name'):
|
||||||
'''
|
'''
|
||||||
try:
|
try:
|
||||||
if name.startswith('DNS:'):
|
if name.startswith('DNS:'):
|
||||||
return x509.DNSName(to_text(name[4:]))
|
return x509.DNSName(_adjust_idn(to_text(name[4:]), 'idna'))
|
||||||
if name.startswith('IP:'):
|
if name.startswith('IP:'):
|
||||||
address = to_text(name[3:])
|
address = to_text(name[3:])
|
||||||
if '/' in address:
|
if '/' in address:
|
||||||
return x509.IPAddress(ipaddress.ip_network(address))
|
return x509.IPAddress(ipaddress.ip_network(address))
|
||||||
return x509.IPAddress(ipaddress.ip_address(address))
|
return x509.IPAddress(ipaddress.ip_address(address))
|
||||||
if name.startswith('email:'):
|
if name.startswith('email:'):
|
||||||
return x509.RFC822Name(to_text(name[6:]))
|
return x509.RFC822Name(_adjust_idn_email(to_text(name[6:]), 'idna'))
|
||||||
if name.startswith('URI:'):
|
if name.startswith('URI:'):
|
||||||
return x509.UniformResourceIdentifier(to_text(name[4:]))
|
return x509.UniformResourceIdentifier(_adjust_idn_url(to_text(name[4:]), 'idna'))
|
||||||
if name.startswith('RID:'):
|
if name.startswith('RID:'):
|
||||||
m = re.match(r'^([0-9]+(?:\.[0-9]+)*)$', to_text(name[4:]))
|
m = re.match(r'^([0-9]+(?:\.[0-9]+)*)$', to_text(name[4:]))
|
||||||
if not m:
|
if not m:
|
||||||
|
@ -422,21 +509,23 @@ def _dn_escape_value(value):
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
def cryptography_decode_name(name):
|
def cryptography_decode_name(name, idn_rewrite='ignore'):
|
||||||
'''
|
'''
|
||||||
Given a cryptography x509.GeneralName object, returns a string.
|
Given a cryptography x509.GeneralName object, returns a string.
|
||||||
Raises an OpenSSLObjectError if the name is not supported.
|
Raises an OpenSSLObjectError if the name is not supported.
|
||||||
'''
|
'''
|
||||||
|
if idn_rewrite not in ('ignore', 'idna', 'unicode'):
|
||||||
|
raise AssertionError('idn_rewrite must be one of "ignore", "idna", or "unicode"')
|
||||||
if isinstance(name, x509.DNSName):
|
if isinstance(name, x509.DNSName):
|
||||||
return u'DNS:{0}'.format(name.value)
|
return u'DNS:{0}'.format(_adjust_idn(name.value, idn_rewrite))
|
||||||
if isinstance(name, x509.IPAddress):
|
if isinstance(name, x509.IPAddress):
|
||||||
if isinstance(name.value, (ipaddress.IPv4Network, ipaddress.IPv6Network)):
|
if isinstance(name.value, (ipaddress.IPv4Network, ipaddress.IPv6Network)):
|
||||||
return u'IP:{0}/{1}'.format(name.value.network_address.compressed, name.value.prefixlen)
|
return u'IP:{0}/{1}'.format(name.value.network_address.compressed, name.value.prefixlen)
|
||||||
return u'IP:{0}'.format(name.value.compressed)
|
return u'IP:{0}'.format(name.value.compressed)
|
||||||
if isinstance(name, x509.RFC822Name):
|
if isinstance(name, x509.RFC822Name):
|
||||||
return u'email:{0}'.format(name.value)
|
return u'email:{0}'.format(_adjust_idn_email(name.value, idn_rewrite))
|
||||||
if isinstance(name, x509.UniformResourceIdentifier):
|
if isinstance(name, x509.UniformResourceIdentifier):
|
||||||
return u'URI:{0}'.format(name.value)
|
return u'URI:{0}'.format(_adjust_idn_url(name.value, idn_rewrite))
|
||||||
if isinstance(name, x509.DirectoryName):
|
if isinstance(name, x509.DirectoryName):
|
||||||
# According to https://datatracker.ietf.org/doc/html/rfc4514.html#section-2.1 the
|
# According to https://datatracker.ietf.org/doc/html/rfc4514.html#section-2.1 the
|
||||||
# list needs to be reversed, and joined by commas
|
# list needs to be reversed, and joined by commas
|
||||||
|
|
|
@ -207,6 +207,7 @@ class CertificateInfoRetrievalCryptography(CertificateInfoRetrieval):
|
||||||
"""Validate the supplied cert, using the cryptography backend"""
|
"""Validate the supplied cert, using the cryptography backend"""
|
||||||
def __init__(self, module, content):
|
def __init__(self, module, content):
|
||||||
super(CertificateInfoRetrievalCryptography, self).__init__(module, 'cryptography', content)
|
super(CertificateInfoRetrievalCryptography, self).__init__(module, 'cryptography', content)
|
||||||
|
self.name_encoding = module.params.get('name_encoding', 'ignore')
|
||||||
|
|
||||||
def _get_der_bytes(self):
|
def _get_der_bytes(self):
|
||||||
return self.cert.public_bytes(serialization.Encoding.DER)
|
return self.cert.public_bytes(serialization.Encoding.DER)
|
||||||
|
@ -309,7 +310,7 @@ class CertificateInfoRetrievalCryptography(CertificateInfoRetrieval):
|
||||||
def _get_subject_alt_name(self):
|
def _get_subject_alt_name(self):
|
||||||
try:
|
try:
|
||||||
san_ext = self.cert.extensions.get_extension_for_class(x509.SubjectAlternativeName)
|
san_ext = self.cert.extensions.get_extension_for_class(x509.SubjectAlternativeName)
|
||||||
result = [cryptography_decode_name(san) for san in san_ext.value]
|
result = [cryptography_decode_name(san, idn_rewrite=self.name_encoding) for san in san_ext.value]
|
||||||
return result, san_ext.critical
|
return result, san_ext.critical
|
||||||
except cryptography.x509.ExtensionNotFound:
|
except cryptography.x509.ExtensionNotFound:
|
||||||
return None, False
|
return None, False
|
||||||
|
@ -341,7 +342,7 @@ class CertificateInfoRetrievalCryptography(CertificateInfoRetrieval):
|
||||||
ext = self.cert.extensions.get_extension_for_class(x509.AuthorityKeyIdentifier)
|
ext = self.cert.extensions.get_extension_for_class(x509.AuthorityKeyIdentifier)
|
||||||
issuer = None
|
issuer = None
|
||||||
if ext.value.authority_cert_issuer is not None:
|
if ext.value.authority_cert_issuer is not None:
|
||||||
issuer = [cryptography_decode_name(san) for san in ext.value.authority_cert_issuer]
|
issuer = [cryptography_decode_name(san, idn_rewrite=self.name_encoding) for san in ext.value.authority_cert_issuer]
|
||||||
return ext.value.key_identifier, issuer, ext.value.authority_cert_serial_number
|
return ext.value.key_identifier, issuer, ext.value.authority_cert_serial_number
|
||||||
except cryptography.x509.ExtensionNotFound:
|
except cryptography.x509.ExtensionNotFound:
|
||||||
return None, None, None
|
return None, None, None
|
||||||
|
|
|
@ -51,6 +51,7 @@ class CRLInfoRetrieval(object):
|
||||||
self.module = module
|
self.module = module
|
||||||
self.content = content
|
self.content = content
|
||||||
self.list_revoked_certificates = list_revoked_certificates
|
self.list_revoked_certificates = list_revoked_certificates
|
||||||
|
self.name_encoding = module.params.get('name_encoding', 'ignore')
|
||||||
|
|
||||||
def get_info(self):
|
def get_info(self):
|
||||||
self.crl_pem = identify_pem_format(self.content)
|
self.crl_pem = identify_pem_format(self.content)
|
||||||
|
@ -86,7 +87,7 @@ class CRLInfoRetrieval(object):
|
||||||
result['revoked_certificates'] = []
|
result['revoked_certificates'] = []
|
||||||
for cert in self.crl:
|
for cert in self.crl:
|
||||||
entry = cryptography_decode_revoked_certificate(cert)
|
entry = cryptography_decode_revoked_certificate(cert)
|
||||||
result['revoked_certificates'].append(cryptography_dump_revoked(entry))
|
result['revoked_certificates'].append(cryptography_dump_revoked(entry, idn_rewrite=self.name_encoding))
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
|
@ -174,6 +174,7 @@ class CSRInfoRetrievalCryptography(CSRInfoRetrieval):
|
||||||
"""Validate the supplied CSR, using the cryptography backend"""
|
"""Validate the supplied CSR, using the cryptography backend"""
|
||||||
def __init__(self, module, content, validate_signature):
|
def __init__(self, module, content, validate_signature):
|
||||||
super(CSRInfoRetrievalCryptography, self).__init__(module, 'cryptography', content, validate_signature)
|
super(CSRInfoRetrievalCryptography, self).__init__(module, 'cryptography', content, validate_signature)
|
||||||
|
self.name_encoding = module.params.get('name_encoding', 'ignore')
|
||||||
|
|
||||||
def _get_subject_ordered(self):
|
def _get_subject_ordered(self):
|
||||||
result = []
|
result = []
|
||||||
|
@ -256,7 +257,7 @@ class CSRInfoRetrievalCryptography(CSRInfoRetrieval):
|
||||||
def _get_subject_alt_name(self):
|
def _get_subject_alt_name(self):
|
||||||
try:
|
try:
|
||||||
san_ext = self.csr.extensions.get_extension_for_class(x509.SubjectAlternativeName)
|
san_ext = self.csr.extensions.get_extension_for_class(x509.SubjectAlternativeName)
|
||||||
result = [cryptography_decode_name(san) for san in san_ext.value]
|
result = [cryptography_decode_name(san, idn_rewrite=self.name_encoding) for san in san_ext.value]
|
||||||
return result, san_ext.critical
|
return result, san_ext.critical
|
||||||
except cryptography.x509.ExtensionNotFound:
|
except cryptography.x509.ExtensionNotFound:
|
||||||
return None, False
|
return None, False
|
||||||
|
@ -264,8 +265,8 @@ class CSRInfoRetrievalCryptography(CSRInfoRetrieval):
|
||||||
def _get_name_constraints(self):
|
def _get_name_constraints(self):
|
||||||
try:
|
try:
|
||||||
nc_ext = self.csr.extensions.get_extension_for_class(x509.NameConstraints)
|
nc_ext = self.csr.extensions.get_extension_for_class(x509.NameConstraints)
|
||||||
permitted = [cryptography_decode_name(san) for san in nc_ext.value.permitted_subtrees or []]
|
permitted = [cryptography_decode_name(san, idn_rewrite=self.name_encoding) for san in nc_ext.value.permitted_subtrees or []]
|
||||||
excluded = [cryptography_decode_name(san) for san in nc_ext.value.excluded_subtrees or []]
|
excluded = [cryptography_decode_name(san, idn_rewrite=self.name_encoding) for san in nc_ext.value.excluded_subtrees or []]
|
||||||
return permitted, excluded, nc_ext.critical
|
return permitted, excluded, nc_ext.critical
|
||||||
except cryptography.x509.ExtensionNotFound:
|
except cryptography.x509.ExtensionNotFound:
|
||||||
return None, None, False
|
return None, None, False
|
||||||
|
@ -291,7 +292,7 @@ class CSRInfoRetrievalCryptography(CSRInfoRetrieval):
|
||||||
ext = self.csr.extensions.get_extension_for_class(x509.AuthorityKeyIdentifier)
|
ext = self.csr.extensions.get_extension_for_class(x509.AuthorityKeyIdentifier)
|
||||||
issuer = None
|
issuer = None
|
||||||
if ext.value.authority_cert_issuer is not None:
|
if ext.value.authority_cert_issuer is not None:
|
||||||
issuer = [cryptography_decode_name(san) for san in ext.value.authority_cert_issuer]
|
issuer = [cryptography_decode_name(san, idn_rewrite=self.name_encoding) for san in ext.value.authority_cert_issuer]
|
||||||
return ext.value.key_identifier, issuer, ext.value.authority_cert_serial_number
|
return ext.value.key_identifier, issuer, ext.value.authority_cert_serial_number
|
||||||
except cryptography.x509.ExtensionNotFound:
|
except cryptography.x509.ExtensionNotFound:
|
||||||
return None, None, None
|
return None, None, None
|
||||||
|
|
|
@ -44,6 +44,9 @@ options:
|
||||||
default: auto
|
default: auto
|
||||||
choices: [ auto, cryptography ]
|
choices: [ auto, cryptography ]
|
||||||
|
|
||||||
|
extends_documentation_fragment:
|
||||||
|
- community.crypto.name_encoding
|
||||||
|
|
||||||
seealso:
|
seealso:
|
||||||
- module: community.crypto.openssl_csr
|
- module: community.crypto.openssl_csr
|
||||||
- module: community.crypto.openssl_csr_pipe
|
- module: community.crypto.openssl_csr_pipe
|
||||||
|
@ -124,7 +127,9 @@ key_usage_critical:
|
||||||
returned: success
|
returned: success
|
||||||
type: bool
|
type: bool
|
||||||
subject_alt_name:
|
subject_alt_name:
|
||||||
description: Entries in the C(subject_alt_name) extension, or C(none) if extension is not present.
|
description:
|
||||||
|
- Entries in the C(subject_alt_name) extension, or C(none) if extension is not present.
|
||||||
|
- See I(name_encoding) for how IDNs are handled.
|
||||||
returned: success
|
returned: success
|
||||||
type: list
|
type: list
|
||||||
elements: str
|
elements: str
|
||||||
|
@ -152,6 +157,7 @@ name_constraints_excluded:
|
||||||
description:
|
description:
|
||||||
- List of excluded subtrees the CA cannot sign certificates for.
|
- List of excluded subtrees the CA cannot sign certificates for.
|
||||||
- Is C(none) if extension is not present.
|
- Is C(none) if extension is not present.
|
||||||
|
- See I(name_encoding) for how IDNs are handled.
|
||||||
returned: success
|
returned: success
|
||||||
type: list
|
type: list
|
||||||
elements: str
|
elements: str
|
||||||
|
@ -281,6 +287,7 @@ authority_cert_issuer:
|
||||||
description:
|
description:
|
||||||
- The CSR's authority cert issuer as a list of general names.
|
- The CSR's authority cert issuer as a list of general names.
|
||||||
- Is C(none) if the C(AuthorityKeyIdentifier) extension is not present.
|
- Is C(none) if the C(AuthorityKeyIdentifier) extension is not present.
|
||||||
|
- See I(name_encoding) for how IDNs are handled.
|
||||||
returned: success
|
returned: success
|
||||||
type: list
|
type: list
|
||||||
elements: str
|
elements: str
|
||||||
|
@ -312,6 +319,7 @@ def main():
|
||||||
argument_spec=dict(
|
argument_spec=dict(
|
||||||
path=dict(type='path'),
|
path=dict(type='path'),
|
||||||
content=dict(type='str'),
|
content=dict(type='str'),
|
||||||
|
name_encoding=dict(type='str', default='ignore', choices=['ignore', 'idna', 'unicode']),
|
||||||
select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography']),
|
select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography']),
|
||||||
),
|
),
|
||||||
required_one_of=(
|
required_one_of=(
|
||||||
|
|
|
@ -62,6 +62,9 @@ options:
|
||||||
default: auto
|
default: auto
|
||||||
choices: [ auto, cryptography ]
|
choices: [ auto, cryptography ]
|
||||||
|
|
||||||
|
extends_documentation_fragment:
|
||||||
|
- community.crypto.name_encoding
|
||||||
|
|
||||||
notes:
|
notes:
|
||||||
- All timestamp values are provided in ASN.1 TIME format, in other words, following the C(YYYYMMDDHHMMSSZ) pattern.
|
- All timestamp values are provided in ASN.1 TIME format, in other words, following the C(YYYYMMDDHHMMSSZ) pattern.
|
||||||
They are all in UTC.
|
They are all in UTC.
|
||||||
|
@ -168,7 +171,9 @@ key_usage_critical:
|
||||||
returned: success
|
returned: success
|
||||||
type: bool
|
type: bool
|
||||||
subject_alt_name:
|
subject_alt_name:
|
||||||
description: Entries in the C(subject_alt_name) extension, or C(none) if extension is not present.
|
description:
|
||||||
|
- Entries in the C(subject_alt_name) extension, or C(none) if extension is not present.
|
||||||
|
- See I(name_encoding) for how IDNs are handled.
|
||||||
returned: success
|
returned: success
|
||||||
type: list
|
type: list
|
||||||
elements: str
|
elements: str
|
||||||
|
@ -355,6 +360,7 @@ authority_cert_issuer:
|
||||||
description:
|
description:
|
||||||
- The certificate's authority cert issuer as a list of general names.
|
- The certificate's authority cert issuer as a list of general names.
|
||||||
- Is C(none) if the C(AuthorityKeyIdentifier) extension is not present.
|
- Is C(none) if the C(AuthorityKeyIdentifier) extension is not present.
|
||||||
|
- See I(name_encoding) for how IDNs are handled.
|
||||||
returned: success
|
returned: success
|
||||||
type: list
|
type: list
|
||||||
elements: str
|
elements: str
|
||||||
|
@ -397,6 +403,7 @@ def main():
|
||||||
path=dict(type='path'),
|
path=dict(type='path'),
|
||||||
content=dict(type='str'),
|
content=dict(type='str'),
|
||||||
valid_at=dict(type='dict'),
|
valid_at=dict(type='dict'),
|
||||||
|
name_encoding=dict(type='str', default='ignore', choices=['ignore', 'idna', 'unicode']),
|
||||||
select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography']),
|
select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography']),
|
||||||
),
|
),
|
||||||
required_one_of=(
|
required_one_of=(
|
||||||
|
|
|
@ -242,6 +242,7 @@ options:
|
||||||
|
|
||||||
extends_documentation_fragment:
|
extends_documentation_fragment:
|
||||||
- files
|
- files
|
||||||
|
- community.crypto.name_encoding
|
||||||
|
|
||||||
notes:
|
notes:
|
||||||
- All ASN.1 TIME values should be specified following the YYYYMMDDHHMMSSZ pattern.
|
- All ASN.1 TIME values should be specified following the YYYYMMDDHHMMSSZ pattern.
|
||||||
|
@ -297,6 +298,7 @@ issuer:
|
||||||
description:
|
description:
|
||||||
- The CRL's issuer.
|
- The CRL's issuer.
|
||||||
- Note that for repeated values, only the last one will be returned.
|
- Note that for repeated values, only the last one will be returned.
|
||||||
|
- See I(name_encoding) for how IDNs are handled.
|
||||||
returned: success
|
returned: success
|
||||||
type: dict
|
type: dict
|
||||||
sample: '{"organizationName": "Ansible", "commonName": "ca.example.com"}'
|
sample: '{"organizationName": "Ansible", "commonName": "ca.example.com"}'
|
||||||
|
@ -336,7 +338,9 @@ revoked_certificates:
|
||||||
type: str
|
type: str
|
||||||
sample: 20190413202428Z
|
sample: 20190413202428Z
|
||||||
issuer:
|
issuer:
|
||||||
description: The certificate's issuer.
|
description:
|
||||||
|
- The certificate's issuer.
|
||||||
|
- See I(name_encoding) for how IDNs are handled.
|
||||||
type: list
|
type: list
|
||||||
elements: str
|
elements: str
|
||||||
sample: '["DNS:ca.example.org"]'
|
sample: '["DNS:ca.example.org"]'
|
||||||
|
@ -405,6 +409,7 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.support im
|
||||||
)
|
)
|
||||||
|
|
||||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
|
||||||
|
cryptography_decode_name,
|
||||||
cryptography_get_name,
|
cryptography_get_name,
|
||||||
cryptography_name_to_oid,
|
cryptography_name_to_oid,
|
||||||
cryptography_oid_to_name,
|
cryptography_oid_to_name,
|
||||||
|
@ -468,6 +473,7 @@ class CRL(OpenSSLObject):
|
||||||
self.update = module.params['mode'] == 'update'
|
self.update = module.params['mode'] == 'update'
|
||||||
self.ignore_timestamps = module.params['ignore_timestamps']
|
self.ignore_timestamps = module.params['ignore_timestamps']
|
||||||
self.return_content = module.params['return_content']
|
self.return_content = module.params['return_content']
|
||||||
|
self.name_encoding = module.params['name_encoding']
|
||||||
self.crl_content = None
|
self.crl_content = None
|
||||||
|
|
||||||
self.privatekey_path = module.params['privatekey_path']
|
self.privatekey_path = module.params['privatekey_path']
|
||||||
|
@ -595,11 +601,20 @@ class CRL(OpenSSLObject):
|
||||||
super(CRL, self).remove(self.module)
|
super(CRL, self).remove(self.module)
|
||||||
|
|
||||||
def _compress_entry(self, entry):
|
def _compress_entry(self, entry):
|
||||||
|
issuer = None
|
||||||
|
if entry['issuer'] is not None:
|
||||||
|
# Normalize to IDNA. If this is used-provided, it was already converted to
|
||||||
|
# IDNA (by cryptography_get_name) and thus the `idna` library is present.
|
||||||
|
# If this is coming from cryptography and isn't already in IDNA (i.e. ascii),
|
||||||
|
# cryptography < 2.1 must be in use, which depends on `idna`. So this should
|
||||||
|
# not require `idna` except if it was already used by code earlier during
|
||||||
|
# this invocation.
|
||||||
|
issuer = tuple(cryptography_decode_name(issuer, idn_rewrite='idna') for issuer in entry['issuer'])
|
||||||
if self.ignore_timestamps:
|
if self.ignore_timestamps:
|
||||||
# Throw out revocation_date
|
# Throw out revocation_date
|
||||||
return (
|
return (
|
||||||
entry['serial_number'],
|
entry['serial_number'],
|
||||||
tuple(entry['issuer']) if entry['issuer'] is not None else None,
|
issuer,
|
||||||
entry['issuer_critical'],
|
entry['issuer_critical'],
|
||||||
entry['reason'],
|
entry['reason'],
|
||||||
entry['reason_critical'],
|
entry['reason_critical'],
|
||||||
|
@ -610,7 +625,7 @@ class CRL(OpenSSLObject):
|
||||||
return (
|
return (
|
||||||
entry['serial_number'],
|
entry['serial_number'],
|
||||||
entry['revocation_date'],
|
entry['revocation_date'],
|
||||||
tuple(entry['issuer']) if entry['issuer'] is not None else None,
|
issuer,
|
||||||
entry['issuer_critical'],
|
entry['issuer_critical'],
|
||||||
entry['reason'],
|
entry['reason'],
|
||||||
entry['reason_critical'],
|
entry['reason_critical'],
|
||||||
|
@ -765,7 +780,7 @@ class CRL(OpenSSLObject):
|
||||||
result['issuer'][k] = v
|
result['issuer'][k] = v
|
||||||
result['revoked_certificates'] = []
|
result['revoked_certificates'] = []
|
||||||
for entry in self.revoked_certificates:
|
for entry in self.revoked_certificates:
|
||||||
result['revoked_certificates'].append(cryptography_dump_revoked(entry))
|
result['revoked_certificates'].append(cryptography_dump_revoked(entry, idn_rewrite=self.name_encoding))
|
||||||
elif self.crl:
|
elif self.crl:
|
||||||
result['last_update'] = self.crl.last_update.strftime(TIMESTAMP_FORMAT)
|
result['last_update'] = self.crl.last_update.strftime(TIMESTAMP_FORMAT)
|
||||||
result['next_update'] = self.crl.next_update.strftime(TIMESTAMP_FORMAT)
|
result['next_update'] = self.crl.next_update.strftime(TIMESTAMP_FORMAT)
|
||||||
|
@ -780,7 +795,7 @@ class CRL(OpenSSLObject):
|
||||||
result['revoked_certificates'] = []
|
result['revoked_certificates'] = []
|
||||||
for cert in self.crl:
|
for cert in self.crl:
|
||||||
entry = cryptography_decode_revoked_certificate(cert)
|
entry = cryptography_decode_revoked_certificate(cert)
|
||||||
result['revoked_certificates'].append(cryptography_dump_revoked(entry))
|
result['revoked_certificates'].append(cryptography_dump_revoked(entry, idn_rewrite=self.name_encoding))
|
||||||
|
|
||||||
if self.return_content:
|
if self.return_content:
|
||||||
result['crl'] = self.crl_content
|
result['crl'] = self.crl_content
|
||||||
|
@ -836,6 +851,7 @@ def main():
|
||||||
required_one_of=[['path', 'content', 'serial_number']],
|
required_one_of=[['path', 'content', 'serial_number']],
|
||||||
mutually_exclusive=[['path', 'content', 'serial_number']],
|
mutually_exclusive=[['path', 'content', 'serial_number']],
|
||||||
),
|
),
|
||||||
|
name_encoding=dict(type='str', default='ignore', choices=['ignore', 'idna', 'unicode']),
|
||||||
),
|
),
|
||||||
required_if=[
|
required_if=[
|
||||||
('state', 'present', ['privatekey_path', 'privatekey_content'], True),
|
('state', 'present', ['privatekey_path', 'privatekey_content'], True),
|
||||||
|
|
|
@ -40,6 +40,9 @@ options:
|
||||||
default: true
|
default: true
|
||||||
version_added: 1.7.0
|
version_added: 1.7.0
|
||||||
|
|
||||||
|
extends_documentation_fragment:
|
||||||
|
- community.crypto.name_encoding
|
||||||
|
|
||||||
notes:
|
notes:
|
||||||
- All timestamp values are provided in ASN.1 TIME format, in other words, following the C(YYYYMMDDHHMMSSZ) pattern.
|
- All timestamp values are provided in ASN.1 TIME format, in other words, following the C(YYYYMMDDHHMMSSZ) pattern.
|
||||||
They are all in UTC.
|
They are all in UTC.
|
||||||
|
@ -76,6 +79,7 @@ issuer:
|
||||||
description:
|
description:
|
||||||
- The CRL's issuer.
|
- The CRL's issuer.
|
||||||
- Note that for repeated values, only the last one will be returned.
|
- Note that for repeated values, only the last one will be returned.
|
||||||
|
- See I(name_encoding) for how IDNs are handled.
|
||||||
returned: success
|
returned: success
|
||||||
type: dict
|
type: dict
|
||||||
sample: '{"organizationName": "Ansible", "commonName": "ca.example.com"}'
|
sample: '{"organizationName": "Ansible", "commonName": "ca.example.com"}'
|
||||||
|
@ -115,7 +119,9 @@ revoked_certificates:
|
||||||
type: str
|
type: str
|
||||||
sample: 20190413202428Z
|
sample: 20190413202428Z
|
||||||
issuer:
|
issuer:
|
||||||
description: The certificate's issuer.
|
description:
|
||||||
|
- The certificate's issuer.
|
||||||
|
- See I(name_encoding) for how IDNs are handled.
|
||||||
type: list
|
type: list
|
||||||
elements: str
|
elements: str
|
||||||
sample: '["DNS:ca.example.org"]'
|
sample: '["DNS:ca.example.org"]'
|
||||||
|
@ -173,6 +179,7 @@ def main():
|
||||||
path=dict(type='path'),
|
path=dict(type='path'),
|
||||||
content=dict(type='str'),
|
content=dict(type='str'),
|
||||||
list_revoked_certificates=dict(type='bool', default=True),
|
list_revoked_certificates=dict(type='bool', default=True),
|
||||||
|
name_encoding=dict(type='str', default='ignore', choices=['ignore', 'idna', 'unicode']),
|
||||||
),
|
),
|
||||||
required_one_of=(
|
required_one_of=(
|
||||||
['path', 'content'],
|
['path', 'content'],
|
||||||
|
|
|
@ -8,6 +8,20 @@
|
||||||
select_crypto_backend: '{{ select_crypto_backend }}'
|
select_crypto_backend: '{{ select_crypto_backend }}'
|
||||||
register: result
|
register: result
|
||||||
|
|
||||||
|
- name: "({{ select_crypto_backend }}) Get CSR info (IDNA encoding)"
|
||||||
|
openssl_csr_info:
|
||||||
|
path: '{{ remote_tmp_dir }}/csr_1.csr'
|
||||||
|
name_encoding: idna
|
||||||
|
select_crypto_backend: '{{ select_crypto_backend }}'
|
||||||
|
register: result_idna
|
||||||
|
|
||||||
|
- name: "({{ select_crypto_backend }}) Get CSR info (Unicode encoding)"
|
||||||
|
openssl_csr_info:
|
||||||
|
path: '{{ remote_tmp_dir }}/csr_1.csr'
|
||||||
|
name_encoding: unicode
|
||||||
|
select_crypto_backend: '{{ select_crypto_backend }}'
|
||||||
|
register: result_unicode
|
||||||
|
|
||||||
- name: "({{ select_crypto_backend }}) Check whether subject and extensions behaves as expected"
|
- name: "({{ select_crypto_backend }}) Check whether subject and extensions behaves as expected"
|
||||||
assert:
|
assert:
|
||||||
that:
|
that:
|
||||||
|
@ -23,8 +37,11 @@
|
||||||
- result.extensions_by_oid['2.5.29.15'].critical == true
|
- result.extensions_by_oid['2.5.29.15'].critical == true
|
||||||
- result.extensions_by_oid['2.5.29.15'].value in ['AwMA/4A=', 'AwMH/4A=']
|
- result.extensions_by_oid['2.5.29.15'].value in ['AwMA/4A=', 'AwMH/4A=']
|
||||||
# Subject Alternative Names
|
# Subject Alternative Names
|
||||||
|
- result.subject_alt_name[1] == ("DNS:âņsïbłè.com" if cryptography_version.stdout is version('2.1', '<') else "DNS:xn--sb-oia0a7a53bya.com")
|
||||||
|
- result_unicode.subject_alt_name[1] == "DNS:âņsïbłè.com"
|
||||||
|
- result_idna.subject_alt_name[1] == "DNS:xn--sb-oia0a7a53bya.com"
|
||||||
- result.extensions_by_oid['2.5.29.17'].critical == false
|
- result.extensions_by_oid['2.5.29.17'].critical == false
|
||||||
- result.extensions_by_oid['2.5.29.17'].value == 'MGCCD3d3dy5hbnNpYmxlLmNvbYcEAQIDBIcQAAAAAAAAAAAAAAAAAAAAAYEQdGVzdEBleGFtcGxlLm9yZ4YjaHR0cHM6Ly9leGFtcGxlLm9yZy90ZXN0L2luZGV4Lmh0bWw='
|
- result.extensions_by_oid['2.5.29.17'].value == 'MHmCD3d3dy5hbnNpYmxlLmNvbYIXeG4tLXNiLW9pYTBhN2E1M2J5YS5jb22HBAECAwSHEAAAAAAAAAAAAAAAAAAAAAGBEHRlc3RAZXhhbXBsZS5vcmeGI2h0dHBzOi8vZXhhbXBsZS5vcmcvdGVzdC9pbmRleC5odG1s'
|
||||||
# Basic Constraints
|
# Basic Constraints
|
||||||
- result.extensions_by_oid['2.5.29.19'].critical == true
|
- result.extensions_by_oid['2.5.29.19'].critical == true
|
||||||
- result.extensions_by_oid['2.5.29.19'].value == 'MAYBAf8CARc='
|
- result.extensions_by_oid['2.5.29.19'].value == 'MAYBAf8CARc='
|
||||||
|
|
|
@ -4,6 +4,11 @@
|
||||||
# and should not be used as examples of how to write Ansible roles #
|
# and should not be used as examples of how to write Ansible roles #
|
||||||
####################################################################
|
####################################################################
|
||||||
|
|
||||||
|
- name: Make sure the Python idna library is installed
|
||||||
|
pip:
|
||||||
|
name: idna
|
||||||
|
state: present
|
||||||
|
|
||||||
- name: Generate privatekey
|
- name: Generate privatekey
|
||||||
openssl_privatekey:
|
openssl_privatekey:
|
||||||
path: '{{ remote_tmp_dir }}/privatekey.pem'
|
path: '{{ remote_tmp_dir }}/privatekey.pem'
|
||||||
|
@ -67,6 +72,7 @@
|
||||||
- biometricInfo
|
- biometricInfo
|
||||||
subject_alt_name:
|
subject_alt_name:
|
||||||
- "DNS:www.ansible.com"
|
- "DNS:www.ansible.com"
|
||||||
|
- "DNS:âņsïbłè.com"
|
||||||
- "IP:1.2.3.4"
|
- "IP:1.2.3.4"
|
||||||
- "IP:::1"
|
- "IP:::1"
|
||||||
- "email:test@example.org"
|
- "email:test@example.org"
|
||||||
|
|
|
@ -8,6 +8,20 @@
|
||||||
select_crypto_backend: '{{ select_crypto_backend }}'
|
select_crypto_backend: '{{ select_crypto_backend }}'
|
||||||
register: result
|
register: result
|
||||||
|
|
||||||
|
- name: ({{select_crypto_backend}}) Get certificate info (IDNA encoding)
|
||||||
|
x509_certificate_info:
|
||||||
|
path: '{{ remote_tmp_dir }}/cert_1.pem'
|
||||||
|
name_encoding: idna
|
||||||
|
select_crypto_backend: '{{ select_crypto_backend }}'
|
||||||
|
register: result_idna
|
||||||
|
|
||||||
|
- name: ({{select_crypto_backend}}) Get certificate info (Unicode encoding)
|
||||||
|
x509_certificate_info:
|
||||||
|
path: '{{ remote_tmp_dir }}/cert_1.pem'
|
||||||
|
name_encoding: unicode
|
||||||
|
select_crypto_backend: '{{ select_crypto_backend }}'
|
||||||
|
register: result_unicode
|
||||||
|
|
||||||
- name: Check whether issuer and subject and extensions behave as expected
|
- name: Check whether issuer and subject and extensions behave as expected
|
||||||
assert:
|
assert:
|
||||||
that:
|
that:
|
||||||
|
@ -21,7 +35,26 @@
|
||||||
- result.public_key_data.size == (default_rsa_key_size_certifiates | int)
|
- result.public_key_data.size == (default_rsa_key_size_certifiates | int)
|
||||||
- "result.subject_alt_name == [
|
- "result.subject_alt_name == [
|
||||||
'DNS:www.ansible.com',
|
'DNS:www.ansible.com',
|
||||||
'DNS:' ~ ('öç' if cryptography_version.stdout is version('2.1', '<') else 'xn--74h') ~ '.com',
|
'DNS:' ~ ('öç' if cryptography_version.stdout is version('2.1', '<') else 'xn--7ca3a') ~ '.com',
|
||||||
|
'DNS:' ~ ('www.öç' if cryptography_version.stdout is version('2.1', '<') else 'xn--74h') ~ '.com',
|
||||||
|
'IP:1.2.3.4',
|
||||||
|
'IP:::1',
|
||||||
|
'email:test@example.org',
|
||||||
|
'URI:https://example.org/test/index.html'
|
||||||
|
]"
|
||||||
|
- "result_idna.subject_alt_name == [
|
||||||
|
'DNS:www.ansible.com',
|
||||||
|
'DNS:xn--7ca3a.com',
|
||||||
|
'DNS:' ~ ('www.xn--7ca3a' if cryptography_version.stdout is version('2.1', '<') else 'xn--74h') ~ '.com',
|
||||||
|
'IP:1.2.3.4',
|
||||||
|
'IP:::1',
|
||||||
|
'email:test@example.org',
|
||||||
|
'URI:https://example.org/test/index.html'
|
||||||
|
]"
|
||||||
|
- "result_unicode.subject_alt_name == [
|
||||||
|
'DNS:www.ansible.com',
|
||||||
|
'DNS:öç.com',
|
||||||
|
'DNS:' ~ ('www.öç' if cryptography_version.stdout is version('2.1', '<') else '☺') ~ '.com',
|
||||||
'IP:1.2.3.4',
|
'IP:1.2.3.4',
|
||||||
'IP:::1',
|
'IP:::1',
|
||||||
'email:test@example.org',
|
'email:test@example.org',
|
||||||
|
@ -37,9 +70,9 @@
|
||||||
- result.extensions_by_oid['2.5.29.17'].critical == false
|
- result.extensions_by_oid['2.5.29.17'].critical == false
|
||||||
- >
|
- >
|
||||||
result.extensions_by_oid['2.5.29.17'].value == (
|
result.extensions_by_oid['2.5.29.17'].value == (
|
||||||
'MG+CD3d3dy5hbnNpYmxlLmNvbYINeG4tLTdjYTNhLmNvbYcEAQIDBIcQAAAAAAAAAAAAAAAAAAAAAYEQdGVzdEBleGFtcGxlLm9yZ4YjaHR0cHM6Ly9leGFtcGxlLm9yZy90ZXN0L2luZGV4Lmh0bWw='
|
'MIGCgg93d3cuYW5zaWJsZS5jb22CDXhuLS03Y2EzYS5jb22CEXd3dy54bi0tN2NhM2EuY29thwQBAgMEhxAAAAAAAAAAAAAAAAAAAAABgRB0ZXN0QGV4YW1wbGUub3JnhiNodHRwczovL2V4YW1wbGUub3JnL3Rlc3QvaW5kZXguaHRtbA=='
|
||||||
if cryptography_version.stdout is version('2.1', '<') else
|
if cryptography_version.stdout is version('2.1', '<') else
|
||||||
'MG2CD3d3dy5hbnNpYmxlLmNvbYILeG4tLTc0aC5jb22HBAECAwSHEAAAAAAAAAAAAAAAAAAAAAGBEHRlc3RAZXhhbXBsZS5vcmeGI2h0dHBzOi8vZXhhbXBsZS5vcmcvdGVzdC9pbmRleC5odG1s'
|
'MHyCD3d3dy5hbnNpYmxlLmNvbYINeG4tLTdjYTNhLmNvbYILeG4tLTc0aC5jb22HBAECAwSHEAAAAAAAAAAAAAAAAAAAAAGBEHRlc3RAZXhhbXBsZS5vcmeGI2h0dHBzOi8vZXhhbXBsZS5vcmcvdGVzdC9pbmRleC5odG1s'
|
||||||
)
|
)
|
||||||
# Basic Constraints
|
# Basic Constraints
|
||||||
- result.extensions_by_oid['2.5.29.19'].critical == true
|
- result.extensions_by_oid['2.5.29.19'].critical == true
|
||||||
|
|
|
@ -4,6 +4,11 @@
|
||||||
# and should not be used as examples of how to write Ansible roles #
|
# and should not be used as examples of how to write Ansible roles #
|
||||||
####################################################################
|
####################################################################
|
||||||
|
|
||||||
|
- name: Make sure the Python idna library is installed
|
||||||
|
pip:
|
||||||
|
name: idna
|
||||||
|
state: present
|
||||||
|
|
||||||
- name: Generate privatekey
|
- name: Generate privatekey
|
||||||
openssl_privatekey:
|
openssl_privatekey:
|
||||||
path: '{{ remote_tmp_dir }}/privatekey.pem'
|
path: '{{ remote_tmp_dir }}/privatekey.pem'
|
||||||
|
@ -67,7 +72,9 @@
|
||||||
- biometricInfo
|
- biometricInfo
|
||||||
subject_alt_name:
|
subject_alt_name:
|
||||||
- "DNS:www.ansible.com"
|
- "DNS:www.ansible.com"
|
||||||
- "DNS:{{ 'öç' if cryptography_version.stdout is version('2.1', '<') else 'xn--74h' }}.com"
|
- "DNS:öç.com"
|
||||||
|
# cryptography < 2.1 cannot handle certain Unicode characters
|
||||||
|
- "DNS:{{ 'www.öç' if cryptography_version.stdout is version('2.1', '<') else '☺' }}.com"
|
||||||
- "IP:1.2.3.4"
|
- "IP:1.2.3.4"
|
||||||
- "IP:::1"
|
- "IP:::1"
|
||||||
- "email:test@example.org"
|
- "email:test@example.org"
|
||||||
|
|
|
@ -536,13 +536,91 @@
|
||||||
revoked_certificates:
|
revoked_certificates:
|
||||||
- serial_number: 1234
|
- serial_number: 1234
|
||||||
revocation_date: 20191001000000Z
|
revocation_date: 20191001000000Z
|
||||||
|
# * cryptography < 2.1 strips username and password from URIs. To avoid problems, we do
|
||||||
|
# not pass usernames and passwords for URIs when the cryptography version is < 2.1.
|
||||||
|
# * Python 3.5 before 3.5.8 rc 1 has a bug in urllib.parse.urlparse() that results in an
|
||||||
|
# error if a Unicode netloc has a username or password included.
|
||||||
|
# (https://github.com/ansible-collections/community.crypto/pull/436#issuecomment-1101737134)
|
||||||
|
# This affects the Python 3.5 included in Ansible 2.9's default test container; to avoid
|
||||||
|
# this, we also do not pass usernames and passwords for Python 3.5.
|
||||||
issuer:
|
issuer:
|
||||||
- "DNS:ca.example.org"
|
- "DNS:ca.example.org"
|
||||||
|
- "DNS:ffóò.ḃâŗ.çøṁ"
|
||||||
|
- "email:foo@ḃâŗ.çøṁ"
|
||||||
|
- "URI:https://{{ '' if cryptography_version.stdout is version('2.1', '<') or ansible_facts.python.version.minor == 5 else 'admin:hunter2@' }}ffóò.ḃâŗ.çøṁ/baz?foo=bar"
|
||||||
|
- "URI:https://{{ '' if cryptography_version.stdout is version('2.1', '<') or ansible_facts.python.version.minor == 5 else 'goo@' }}www.straße.de"
|
||||||
|
- "URI:https://straße.de:8080"
|
||||||
|
- "URI:http://gefäß.org"
|
||||||
|
- "URI:http://{{ '' if cryptography_version.stdout is version('2.1', '<') or ansible_facts.python.version.minor == 5 else 'a:b@' }}ä:1"
|
||||||
issuer_critical: true
|
issuer_critical: true
|
||||||
register: crl_3
|
register: crl_3
|
||||||
|
|
||||||
|
- name: Create CRL 3 (IDNA encoding)
|
||||||
|
x509_crl:
|
||||||
|
path: '{{ remote_tmp_dir }}/ca-crl3.crl'
|
||||||
|
privatekey_path: '{{ remote_tmp_dir }}/ca.key'
|
||||||
|
issuer:
|
||||||
|
CN: Ansible
|
||||||
|
last_update: +0d
|
||||||
|
next_update: +0d
|
||||||
|
revoked_certificates:
|
||||||
|
- serial_number: 1234
|
||||||
|
revocation_date: 20191001000000Z
|
||||||
|
issuer:
|
||||||
|
- "DNS:ca.example.org"
|
||||||
|
- "DNS:xn--ff-3jad.xn--2ca8uh37e.xn--7ca8a981n"
|
||||||
|
- "email:foo@xn--2ca8uh37e.xn--7ca8a981n"
|
||||||
|
- "URI:https://{{ '' if cryptography_version.stdout is version('2.1', '<') or ansible_facts.python.version.minor == 5 else 'admin:hunter2@' }}xn--ff-3jad.xn--2ca8uh37e.xn--7ca8a981n/baz?foo=bar"
|
||||||
|
- "URI:https://{{ '' if cryptography_version.stdout is version('2.1', '<') or ansible_facts.python.version.minor == 5 else 'goo@' }}www.xn--strae-oqa.de"
|
||||||
|
- "URI:https://xn--strae-oqa.de:8080"
|
||||||
|
- "URI:http://xn--gef-7kay.org"
|
||||||
|
- "URI:http://{{ '' if cryptography_version.stdout is version('2.1', '<') or ansible_facts.python.version.minor == 5 else 'a:b@' }}xn--4ca:1"
|
||||||
|
issuer_critical: true
|
||||||
|
ignore_timestamps: true
|
||||||
|
name_encoding: idna
|
||||||
|
register: crl_3_idna
|
||||||
|
|
||||||
|
- name: Create CRL 3 (Unicode encoding)
|
||||||
|
x509_crl:
|
||||||
|
path: '{{ remote_tmp_dir }}/ca-crl3.crl'
|
||||||
|
privatekey_path: '{{ remote_tmp_dir }}/ca.key'
|
||||||
|
issuer:
|
||||||
|
CN: Ansible
|
||||||
|
last_update: +0d
|
||||||
|
next_update: +0d
|
||||||
|
revoked_certificates:
|
||||||
|
- serial_number: 1234
|
||||||
|
revocation_date: 20191001000000Z
|
||||||
|
issuer:
|
||||||
|
- "DNS:ca.example.org"
|
||||||
|
- "DNS:ffóò.ḃâŗ.çøṁ"
|
||||||
|
- "email:foo@ḃâŗ.çøṁ"
|
||||||
|
- "URI:https://{{ '' if cryptography_version.stdout is version('2.1', '<') or ansible_facts.python.version.minor == 5 else 'admin:hunter2@' }}ffóò.ḃâŗ.çøṁ/baz?foo=bar"
|
||||||
|
- "URI:https://{{ '' if cryptography_version.stdout is version('2.1', '<') or ansible_facts.python.version.minor == 5 else 'goo@' }}www.straße.de"
|
||||||
|
- "URI:https://straße.de:8080"
|
||||||
|
- "URI:http://gefäß.org"
|
||||||
|
- "URI:http://{{ '' if cryptography_version.stdout is version('2.1', '<') or ansible_facts.python.version.minor == 5 else 'a:b@' }}ä:1"
|
||||||
|
issuer_critical: true
|
||||||
|
ignore_timestamps: true
|
||||||
|
name_encoding: unicode
|
||||||
|
register: crl_3_unicode
|
||||||
|
|
||||||
- name: Retrieve CRL 3 infos
|
- name: Retrieve CRL 3 infos
|
||||||
x509_crl_info:
|
x509_crl_info:
|
||||||
path: '{{ remote_tmp_dir }}/ca-crl3.crl'
|
path: '{{ remote_tmp_dir }}/ca-crl3.crl'
|
||||||
list_revoked_certificates: true
|
list_revoked_certificates: true
|
||||||
register: crl_3_info
|
register: crl_3_info
|
||||||
|
|
||||||
|
- name: Retrieve CRL 3 infos (IDNA encoding)
|
||||||
|
x509_crl_info:
|
||||||
|
path: '{{ remote_tmp_dir }}/ca-crl3.crl'
|
||||||
|
name_encoding: idna
|
||||||
|
list_revoked_certificates: true
|
||||||
|
register: crl_3_info_idna
|
||||||
|
|
||||||
|
- name: Retrieve CRL 3 infos (Unicode encoding)
|
||||||
|
x509_crl_info:
|
||||||
|
path: '{{ remote_tmp_dir }}/ca-crl3.crl'
|
||||||
|
name_encoding: unicode
|
||||||
|
list_revoked_certificates: true
|
||||||
|
register: crl_3_info_unicode
|
||||||
|
|
|
@ -4,6 +4,11 @@
|
||||||
# and should not be used as examples of how to write Ansible roles #
|
# and should not be used as examples of how to write Ansible roles #
|
||||||
####################################################################
|
####################################################################
|
||||||
|
|
||||||
|
- name: Make sure the Python idna library is installed
|
||||||
|
pip:
|
||||||
|
name: idna
|
||||||
|
state: present
|
||||||
|
|
||||||
- set_fact:
|
- set_fact:
|
||||||
certificates:
|
certificates:
|
||||||
- name: ca
|
- name: ca
|
||||||
|
|
|
@ -107,6 +107,73 @@
|
||||||
assert:
|
assert:
|
||||||
that:
|
that:
|
||||||
- crl_3.revoked_certificates == crl_3_info.revoked_certificates
|
- crl_3.revoked_certificates == crl_3_info.revoked_certificates
|
||||||
- crl_3.revoked_certificates[0].issuer == [
|
- crl_3.revoked_certificates[0].issuer == ([
|
||||||
"DNS:ca.example.org",
|
"DNS:ca.example.org",
|
||||||
]
|
"DNS:ffóò.ḃâŗ.çøṁ",
|
||||||
|
"email:foo@ḃâŗ.çøṁ",
|
||||||
|
"URI:https://ffóò.ḃâŗ.çøṁ/baz?foo=bar",
|
||||||
|
"URI:https://www.straße.de",
|
||||||
|
"URI:https://straße.de:8080",
|
||||||
|
"URI:http://gefäß.org",
|
||||||
|
"URI:http://ä:1",
|
||||||
|
] if cryptography_version.stdout is version('2.1', '<') else [
|
||||||
|
"DNS:ca.example.org",
|
||||||
|
"DNS:xn--ff-3jad.xn--2ca8uh37e.xn--7ca8a981n",
|
||||||
|
"email:foo@xn--2ca8uh37e.xn--7ca8a981n",
|
||||||
|
"URI:https://xn--ff-3jad.xn--2ca8uh37e.xn--7ca8a981n/baz?foo=bar",
|
||||||
|
"URI:https://www.xn--strae-oqa.de",
|
||||||
|
"URI:https://xn--strae-oqa.de:8080",
|
||||||
|
"URI:http://xn--gef-7kay.org",
|
||||||
|
"URI:http://xn--4ca:1",
|
||||||
|
] if ansible_facts.python.version.minor == 5 else [
|
||||||
|
"DNS:ca.example.org",
|
||||||
|
"DNS:xn--ff-3jad.xn--2ca8uh37e.xn--7ca8a981n",
|
||||||
|
"email:foo@xn--2ca8uh37e.xn--7ca8a981n",
|
||||||
|
"URI:https://admin:hunter2@xn--ff-3jad.xn--2ca8uh37e.xn--7ca8a981n/baz?foo=bar",
|
||||||
|
"URI:https://goo@www.xn--strae-oqa.de",
|
||||||
|
"URI:https://xn--strae-oqa.de:8080",
|
||||||
|
"URI:http://xn--gef-7kay.org",
|
||||||
|
"URI:http://a:b@xn--4ca:1",
|
||||||
|
])
|
||||||
|
- crl_3_idna is not changed
|
||||||
|
- crl_3_idna.revoked_certificates == crl_3_info_idna.revoked_certificates
|
||||||
|
- crl_3_idna.revoked_certificates[0].issuer == ([
|
||||||
|
"DNS:ca.example.org",
|
||||||
|
"DNS:xn--ff-3jad.xn--2ca8uh37e.xn--7ca8a981n",
|
||||||
|
"email:foo@xn--2ca8uh37e.xn--7ca8a981n",
|
||||||
|
"URI:https://xn--ff-3jad.xn--2ca8uh37e.xn--7ca8a981n/baz?foo=bar",
|
||||||
|
"URI:https://www.xn--strae-oqa.de",
|
||||||
|
"URI:https://xn--strae-oqa.de:8080",
|
||||||
|
"URI:http://xn--gef-7kay.org",
|
||||||
|
"URI:http://xn--4ca:1",
|
||||||
|
] if cryptography_version.stdout is version('2.1', '<') or ansible_facts.python.version.minor == 5 else [
|
||||||
|
"DNS:ca.example.org",
|
||||||
|
"DNS:xn--ff-3jad.xn--2ca8uh37e.xn--7ca8a981n",
|
||||||
|
"email:foo@xn--2ca8uh37e.xn--7ca8a981n",
|
||||||
|
"URI:https://admin:hunter2@xn--ff-3jad.xn--2ca8uh37e.xn--7ca8a981n/baz?foo=bar",
|
||||||
|
"URI:https://goo@www.xn--strae-oqa.de",
|
||||||
|
"URI:https://xn--strae-oqa.de:8080",
|
||||||
|
"URI:http://xn--gef-7kay.org",
|
||||||
|
"URI:http://a:b@xn--4ca:1",
|
||||||
|
])
|
||||||
|
- crl_3_unicode is not changed
|
||||||
|
- crl_3_unicode.revoked_certificates == crl_3_info_unicode.revoked_certificates
|
||||||
|
- crl_3_unicode.revoked_certificates[0].issuer == ([
|
||||||
|
"DNS:ca.example.org",
|
||||||
|
"DNS:ffóò.ḃâŗ.çøṁ",
|
||||||
|
"email:foo@ḃâŗ.çøṁ",
|
||||||
|
"URI:https://ffóò.ḃâŗ.çøṁ/baz?foo=bar",
|
||||||
|
"URI:https://www.straße.de",
|
||||||
|
"URI:https://straße.de:8080",
|
||||||
|
"URI:http://gefäß.org",
|
||||||
|
"URI:http://ä:1",
|
||||||
|
] if cryptography_version.stdout is version('2.1', '<') or ansible_facts.python.version.minor == 5 else [
|
||||||
|
"DNS:ca.example.org",
|
||||||
|
"DNS:ffóò.ḃâŗ.çøṁ",
|
||||||
|
"email:foo@ḃâŗ.çøṁ",
|
||||||
|
"URI:https://admin:hunter2@ffóò.ḃâŗ.çøṁ/baz?foo=bar",
|
||||||
|
"URI:https://goo@www.straße.de",
|
||||||
|
"URI:https://straße.de:8080",
|
||||||
|
"URI:http://gefäß.org",
|
||||||
|
"URI:http://a:b@ä:1",
|
||||||
|
])
|
||||||
|
|
|
@ -20,6 +20,7 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.basic impo
|
||||||
|
|
||||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
|
||||||
cryptography_get_name,
|
cryptography_get_name,
|
||||||
|
_adjust_idn,
|
||||||
_parse_dn_component,
|
_parse_dn_component,
|
||||||
_parse_dn,
|
_parse_dn,
|
||||||
)
|
)
|
||||||
|
@ -27,6 +28,67 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptograp
|
||||||
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
|
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('unicode, idna, cycled_unicode', [
|
||||||
|
(u'..', u'..', None),
|
||||||
|
(u'foo.com', u'foo.com', None),
|
||||||
|
(u'.foo.com.', u'.foo.com.', None),
|
||||||
|
(u'*.foo.com', u'*.foo.com', None),
|
||||||
|
(u'straße', u'xn--strae-oqa', None),
|
||||||
|
(u'ffóò.ḃâŗ.çøṁ', u'xn--ff-3jad.xn--2ca8uh37e.xn--7ca8a981n', u'ffóò.ḃâŗ.çøṁ'),
|
||||||
|
(u'*.☺.', u'*.xn--74h.', None),
|
||||||
|
])
|
||||||
|
def test_adjust_idn(unicode, idna, cycled_unicode):
|
||||||
|
if cycled_unicode is None:
|
||||||
|
cycled_unicode = unicode
|
||||||
|
|
||||||
|
result = _adjust_idn(unicode, 'ignore')
|
||||||
|
print(result, unicode)
|
||||||
|
assert result == unicode
|
||||||
|
|
||||||
|
result = _adjust_idn(idna, 'ignore')
|
||||||
|
print(result, idna)
|
||||||
|
assert result == idna
|
||||||
|
|
||||||
|
result = _adjust_idn(unicode, 'unicode')
|
||||||
|
print(result, unicode)
|
||||||
|
assert result == unicode
|
||||||
|
|
||||||
|
result = _adjust_idn(idna, 'unicode')
|
||||||
|
print(result, cycled_unicode)
|
||||||
|
assert result == cycled_unicode
|
||||||
|
|
||||||
|
result = _adjust_idn(unicode, 'idna')
|
||||||
|
print(result, idna)
|
||||||
|
assert result == idna
|
||||||
|
|
||||||
|
result = _adjust_idn(idna, 'idna')
|
||||||
|
print(result, idna)
|
||||||
|
assert result == idna
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('value, idn_rewrite, message', [
|
||||||
|
(u'bar', 'foo', re.escape(u'Invalid value for idn_rewrite: "foo"')),
|
||||||
|
])
|
||||||
|
def test_adjust_idn_fail_valueerror(value, idn_rewrite, message):
|
||||||
|
with pytest.raises(ValueError, match=message):
|
||||||
|
result = _adjust_idn(value, idn_rewrite)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('value, idn_rewrite, message', [
|
||||||
|
(
|
||||||
|
u'xn--a',
|
||||||
|
'unicode',
|
||||||
|
u'''^Error while transforming part u?"xn\\-\\-a" of IDNA DNS name u?"xn\\-\\-a" to Unicode\\.'''
|
||||||
|
u''' IDNA2008 transformation resulted in "Codepoint U\\+0080 at position 1 of u?'\\\\x80' not allowed",'''
|
||||||
|
u''' IDNA2003 transformation resulted in "(decoding with 'idna' codec failed'''
|
||||||
|
u''' \\(UnicodeError: )?Invalid character u?'\\\\x80'\\)?"\\.$'''
|
||||||
|
),
|
||||||
|
])
|
||||||
|
def test_adjust_idn_fail_user_error(value, idn_rewrite, message):
|
||||||
|
with pytest.raises(OpenSSLObjectError, match=message):
|
||||||
|
result = _adjust_idn(value, idn_rewrite)
|
||||||
|
|
||||||
|
|
||||||
def test_cryptography_get_name_invalid_prefix():
|
def test_cryptography_get_name_invalid_prefix():
|
||||||
with pytest.raises(OpenSSLObjectError, match="^Cannot parse Subject Alternative Name"):
|
with pytest.raises(OpenSSLObjectError, match="^Cannot parse Subject Alternative Name"):
|
||||||
cryptography_get_name('fake:value')
|
cryptography_get_name('fake:value')
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
bcrypt
|
bcrypt
|
||||||
cryptography
|
cryptography
|
||||||
|
idna
|
||||||
ipaddress ; python_version < '3.0'
|
ipaddress ; python_version < '3.0'
|
||||||
|
|
||||||
unittest2 ; python_version < '2.7'
|
unittest2 ; python_version < '2.7'
|
||||||
|
|
Loading…
Reference in New Issue