mirror of
https://github.com/ansible-collections/community.crypto.git
synced 2026-05-06 21:33:00 +00:00
Refactor module_utils/crypto.py (#27)
* Refactor module_utils/crypto.py: split up into multiple smaller modules * Remove superfluous files. * Fix sanity errors. * Move CRL entry dumping code to module_utils. * Move obj2txt usage from CRL modules to module_utils/crpyto/cryptography_crl. * Move generic I/O functions to plugins/module_utils/io.py. * Add helper method for retrieving serial number of certificate. * Add compatibility code into __init__.py. * Fix syntax error, and add ignore.txt entries for non-empty __init__.
This commit is contained in:
99
plugins/module_utils/crypto/__init__.py
Normal file
99
plugins/module_utils/crypto/__init__.py
Normal file
@@ -0,0 +1,99 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# (c) 2016, Yanis Guenane <yanis+ansible@guenane.org>
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
# THIS FILE IS FOR COMPATIBILITY ONLY! YOU SHALL NOT IMPORT IT!
|
||||
#
|
||||
# This fill will be removed eventually, so if you're using it,
|
||||
# please stop doing so.
|
||||
|
||||
from .basic import (
|
||||
HAS_PYOPENSSL,
|
||||
CRYPTOGRAPHY_HAS_X25519,
|
||||
CRYPTOGRAPHY_HAS_X25519_FULL,
|
||||
CRYPTOGRAPHY_HAS_X448,
|
||||
CRYPTOGRAPHY_HAS_ED25519,
|
||||
CRYPTOGRAPHY_HAS_ED448,
|
||||
HAS_CRYPTOGRAPHY,
|
||||
OpenSSLObjectError,
|
||||
OpenSSLBadPassphraseError,
|
||||
)
|
||||
|
||||
from .cryptography_crl import (
|
||||
REVOCATION_REASON_MAP,
|
||||
REVOCATION_REASON_MAP_INVERSE,
|
||||
cryptography_decode_revoked_certificate,
|
||||
)
|
||||
|
||||
from .cryptography_support import (
|
||||
cryptography_get_extensions_from_cert,
|
||||
cryptography_get_extensions_from_csr,
|
||||
cryptography_name_to_oid,
|
||||
cryptography_oid_to_name,
|
||||
cryptography_get_name,
|
||||
cryptography_decode_name,
|
||||
cryptography_parse_key_usage_params,
|
||||
cryptography_get_basic_constraints,
|
||||
cryptography_key_needs_digest_for_signing,
|
||||
cryptography_compare_public_keys,
|
||||
)
|
||||
|
||||
from .identify import (
|
||||
identify_private_key_format,
|
||||
)
|
||||
|
||||
from .math import (
|
||||
binary_exp_mod,
|
||||
simple_gcd,
|
||||
quick_is_not_prime,
|
||||
count_bits,
|
||||
)
|
||||
|
||||
from ._obj2txt import obj2txt as _obj2txt
|
||||
|
||||
from ._objects_data import OID_MAP as _OID_MAP
|
||||
|
||||
from ._objects import OID_LOOKUP as _OID_LOOKUP
|
||||
from ._objects import NORMALIZE_NAMES as _NORMALIZE_NAMES
|
||||
from ._objects import NORMALIZE_NAMES_SHORT as _NORMALIZE_NAMES_SHORT
|
||||
|
||||
from .pyopenssl_support import (
|
||||
pyopenssl_normalize_name,
|
||||
pyopenssl_get_extensions_from_cert,
|
||||
pyopenssl_get_extensions_from_csr,
|
||||
)
|
||||
|
||||
from .support import (
|
||||
get_fingerprint_of_bytes,
|
||||
get_fingerprint,
|
||||
load_privatekey,
|
||||
load_certificate,
|
||||
load_certificate_request,
|
||||
parse_name_field,
|
||||
convert_relative_to_datetime,
|
||||
get_relative_time_option,
|
||||
select_message_digest,
|
||||
OpenSSLObject,
|
||||
)
|
||||
|
||||
from ..io import (
|
||||
load_file_if_exists,
|
||||
write_file,
|
||||
)
|
||||
43
plugins/module_utils/crypto/_obj2txt.py
Normal file
43
plugins/module_utils/crypto/_obj2txt.py
Normal file
@@ -0,0 +1,43 @@
|
||||
# This excerpt is dual licensed under the terms of the Apache License, Version
|
||||
# 2.0, and the BSD License. See the LICENSE file at
|
||||
# https://github.com/pyca/cryptography/blob/master/LICENSE for complete details.
|
||||
#
|
||||
# Adapted from cryptography's hazmat/backends/openssl/decode_asn1.py
|
||||
#
|
||||
# Copyright (c) 2015, 2016 Paul Kehrer (@reaperhulk)
|
||||
# Copyright (c) 2017 Fraser Tweedale (@frasertweedale)
|
||||
|
||||
# Relevant commits from cryptography project (https://github.com/pyca/cryptography):
|
||||
# pyca/cryptography@719d536dd691e84e208534798f2eb4f82aaa2e07
|
||||
# pyca/cryptography@5ab6d6a5c05572bd1c75f05baf264a2d0001894a
|
||||
# pyca/cryptography@2e776e20eb60378e0af9b7439000d0e80da7c7e3
|
||||
# pyca/cryptography@fb309ed24647d1be9e319b61b1f2aa8ebb87b90b
|
||||
# pyca/cryptography@2917e460993c475c72d7146c50dc3bbc2414280d
|
||||
# pyca/cryptography@3057f91ea9a05fb593825006d87a391286a4d828
|
||||
# pyca/cryptography@d607dd7e5bc5c08854ec0c9baff70ba4a35be36f
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
def obj2txt(openssl_lib, openssl_ffi, obj):
|
||||
# Set to 80 on the recommendation of
|
||||
# https://www.openssl.org/docs/crypto/OBJ_nid2ln.html#return_values
|
||||
#
|
||||
# But OIDs longer than this occur in real life (e.g. Active
|
||||
# Directory makes some very long OIDs). So we need to detect
|
||||
# and properly handle the case where the default buffer is not
|
||||
# big enough.
|
||||
#
|
||||
buf_len = 80
|
||||
buf = openssl_ffi.new("char[]", buf_len)
|
||||
|
||||
# 'res' is the number of bytes that *would* be written if the
|
||||
# buffer is large enough. If 'res' > buf_len - 1, we need to
|
||||
# alloc a big-enough buffer and go again.
|
||||
res = openssl_lib.OBJ_obj2txt(buf, buf_len, obj, 1)
|
||||
if res > buf_len - 1: # account for terminating null byte
|
||||
buf_len = res + 1
|
||||
buf = openssl_ffi.new("char[]", buf_len)
|
||||
res = openssl_lib.OBJ_obj2txt(buf, buf_len, obj, 1)
|
||||
return openssl_ffi.buffer(buf, res)[:].decode()
|
||||
46
plugins/module_utils/crypto/_objects.py
Normal file
46
plugins/module_utils/crypto/_objects.py
Normal file
@@ -0,0 +1,46 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# (c) 2019, Felix Fontein <felix@fontein.de>
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
from ._objects_data import OID_MAP
|
||||
|
||||
OID_LOOKUP = dict()
|
||||
NORMALIZE_NAMES = dict()
|
||||
NORMALIZE_NAMES_SHORT = dict()
|
||||
|
||||
for dotted, names in OID_MAP.items():
|
||||
for name in names:
|
||||
if name in NORMALIZE_NAMES and OID_LOOKUP[name] != dotted:
|
||||
raise AssertionError(
|
||||
'Name collision during setup: "{0}" for OIDs {1} and {2}'
|
||||
.format(name, dotted, OID_LOOKUP[name])
|
||||
)
|
||||
NORMALIZE_NAMES[name] = names[0]
|
||||
NORMALIZE_NAMES_SHORT[name] = names[-1]
|
||||
OID_LOOKUP[name] = dotted
|
||||
for alias, original in [('userID', 'userId')]:
|
||||
if alias in NORMALIZE_NAMES:
|
||||
raise AssertionError(
|
||||
'Name collision during adding aliases: "{0}" (alias for "{1}") is already mapped to OID {2}'
|
||||
.format(alias, original, OID_LOOKUP[alias])
|
||||
)
|
||||
NORMALIZE_NAMES[alias] = original
|
||||
NORMALIZE_NAMES_SHORT[alias] = NORMALIZE_NAMES_SHORT[original]
|
||||
OID_LOOKUP[alias] = OID_LOOKUP[original]
|
||||
File diff suppressed because it is too large
Load Diff
112
plugins/module_utils/crypto/basic.py
Normal file
112
plugins/module_utils/crypto/basic.py
Normal file
@@ -0,0 +1,112 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# (c) 2016, Yanis Guenane <yanis+ansible@guenane.org>
|
||||
# (c) 2020, Felix Fontein <felix@fontein.de>
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
from distutils.version import LooseVersion
|
||||
|
||||
try:
|
||||
import OpenSSL # noqa
|
||||
from OpenSSL import crypto # noqa
|
||||
HAS_PYOPENSSL = True
|
||||
except ImportError:
|
||||
# Error handled in the calling module.
|
||||
HAS_PYOPENSSL = False
|
||||
|
||||
try:
|
||||
import cryptography
|
||||
from cryptography import x509
|
||||
|
||||
# Older versions of cryptography (< 2.1) do not have __hash__ functions for
|
||||
# general name objects (DNSName, IPAddress, ...), while providing overloaded
|
||||
# equality and string representation operations. This makes it impossible to
|
||||
# use them in hash-based data structures such as set or dict. Since we are
|
||||
# actually doing that in x509_certificate, and potentially in other code,
|
||||
# we need to monkey-patch __hash__ for these classes to make sure our code
|
||||
# works fine.
|
||||
if LooseVersion(cryptography.__version__) < LooseVersion('2.1'):
|
||||
# A very simply hash function which relies on the representation
|
||||
# of an object to be implemented. This is the case since at least
|
||||
# cryptography 1.0, see
|
||||
# https://github.com/pyca/cryptography/commit/7a9abce4bff36c05d26d8d2680303a6f64a0e84f
|
||||
def simple_hash(self):
|
||||
return hash(repr(self))
|
||||
|
||||
# The hash functions for the following types were added for cryptography 2.1:
|
||||
# https://github.com/pyca/cryptography/commit/fbfc36da2a4769045f2373b004ddf0aff906cf38
|
||||
x509.DNSName.__hash__ = simple_hash
|
||||
x509.DirectoryName.__hash__ = simple_hash
|
||||
x509.GeneralName.__hash__ = simple_hash
|
||||
x509.IPAddress.__hash__ = simple_hash
|
||||
x509.OtherName.__hash__ = simple_hash
|
||||
x509.RegisteredID.__hash__ = simple_hash
|
||||
|
||||
if LooseVersion(cryptography.__version__) < LooseVersion('1.2'):
|
||||
# The hash functions for the following types were added for cryptography 1.2:
|
||||
# https://github.com/pyca/cryptography/commit/b642deed88a8696e5f01ce6855ccf89985fc35d0
|
||||
# https://github.com/pyca/cryptography/commit/d1b5681f6db2bde7a14625538bd7907b08dfb486
|
||||
x509.RFC822Name.__hash__ = simple_hash
|
||||
x509.UniformResourceIdentifier.__hash__ = simple_hash
|
||||
|
||||
# Test whether we have support for X25519, X448, Ed25519 and/or Ed448
|
||||
try:
|
||||
import cryptography.hazmat.primitives.asymmetric.x25519
|
||||
CRYPTOGRAPHY_HAS_X25519 = True
|
||||
try:
|
||||
cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey.private_bytes
|
||||
CRYPTOGRAPHY_HAS_X25519_FULL = True
|
||||
except AttributeError:
|
||||
CRYPTOGRAPHY_HAS_X25519_FULL = False
|
||||
except ImportError:
|
||||
CRYPTOGRAPHY_HAS_X25519 = False
|
||||
CRYPTOGRAPHY_HAS_X25519_FULL = False
|
||||
try:
|
||||
import cryptography.hazmat.primitives.asymmetric.x448
|
||||
CRYPTOGRAPHY_HAS_X448 = True
|
||||
except ImportError:
|
||||
CRYPTOGRAPHY_HAS_X448 = False
|
||||
try:
|
||||
import cryptography.hazmat.primitives.asymmetric.ed25519
|
||||
CRYPTOGRAPHY_HAS_ED25519 = True
|
||||
except ImportError:
|
||||
CRYPTOGRAPHY_HAS_ED25519 = False
|
||||
try:
|
||||
import cryptography.hazmat.primitives.asymmetric.ed448
|
||||
CRYPTOGRAPHY_HAS_ED448 = True
|
||||
except ImportError:
|
||||
CRYPTOGRAPHY_HAS_ED448 = False
|
||||
|
||||
HAS_CRYPTOGRAPHY = True
|
||||
except ImportError:
|
||||
# Error handled in the calling module.
|
||||
CRYPTOGRAPHY_HAS_X25519 = False
|
||||
CRYPTOGRAPHY_HAS_X25519_FULL = False
|
||||
CRYPTOGRAPHY_HAS_X448 = False
|
||||
CRYPTOGRAPHY_HAS_ED25519 = False
|
||||
CRYPTOGRAPHY_HAS_ED448 = False
|
||||
HAS_CRYPTOGRAPHY = False
|
||||
|
||||
|
||||
class OpenSSLObjectError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class OpenSSLBadPassphraseError(OpenSSLObjectError):
|
||||
pass
|
||||
125
plugins/module_utils/crypto/cryptography_crl.py
Normal file
125
plugins/module_utils/crypto/cryptography_crl.py
Normal file
@@ -0,0 +1,125 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# (c) 2019, Felix Fontein <felix@fontein.de>
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
try:
|
||||
from cryptography import x509
|
||||
except ImportError:
|
||||
# Error handled in the calling module.
|
||||
pass
|
||||
|
||||
from .basic import (
|
||||
HAS_CRYPTOGRAPHY,
|
||||
)
|
||||
|
||||
from .cryptography_support import (
|
||||
cryptography_decode_name,
|
||||
)
|
||||
|
||||
from ._obj2txt import (
|
||||
obj2txt,
|
||||
)
|
||||
|
||||
|
||||
TIMESTAMP_FORMAT = "%Y%m%d%H%M%SZ"
|
||||
|
||||
|
||||
if HAS_CRYPTOGRAPHY:
|
||||
REVOCATION_REASON_MAP = {
|
||||
'unspecified': x509.ReasonFlags.unspecified,
|
||||
'key_compromise': x509.ReasonFlags.key_compromise,
|
||||
'ca_compromise': x509.ReasonFlags.ca_compromise,
|
||||
'affiliation_changed': x509.ReasonFlags.affiliation_changed,
|
||||
'superseded': x509.ReasonFlags.superseded,
|
||||
'cessation_of_operation': x509.ReasonFlags.cessation_of_operation,
|
||||
'certificate_hold': x509.ReasonFlags.certificate_hold,
|
||||
'privilege_withdrawn': x509.ReasonFlags.privilege_withdrawn,
|
||||
'aa_compromise': x509.ReasonFlags.aa_compromise,
|
||||
'remove_from_crl': x509.ReasonFlags.remove_from_crl,
|
||||
}
|
||||
REVOCATION_REASON_MAP_INVERSE = dict()
|
||||
for k, v in REVOCATION_REASON_MAP.items():
|
||||
REVOCATION_REASON_MAP_INVERSE[v] = k
|
||||
|
||||
else:
|
||||
REVOCATION_REASON_MAP = dict()
|
||||
REVOCATION_REASON_MAP_INVERSE = dict()
|
||||
|
||||
|
||||
def cryptography_decode_revoked_certificate(cert):
|
||||
result = {
|
||||
'serial_number': cert.serial_number,
|
||||
'revocation_date': cert.revocation_date,
|
||||
'issuer': None,
|
||||
'issuer_critical': False,
|
||||
'reason': None,
|
||||
'reason_critical': False,
|
||||
'invalidity_date': None,
|
||||
'invalidity_date_critical': False,
|
||||
}
|
||||
try:
|
||||
ext = cert.extensions.get_extension_for_class(x509.CertificateIssuer)
|
||||
result['issuer'] = list(ext.value)
|
||||
result['issuer_critical'] = ext.critical
|
||||
except x509.ExtensionNotFound:
|
||||
pass
|
||||
try:
|
||||
ext = cert.extensions.get_extension_for_class(x509.CRLReason)
|
||||
result['reason'] = ext.value.reason
|
||||
result['reason_critical'] = ext.critical
|
||||
except x509.ExtensionNotFound:
|
||||
pass
|
||||
try:
|
||||
ext = cert.extensions.get_extension_for_class(x509.InvalidityDate)
|
||||
result['invalidity_date'] = ext.value.invalidity_date
|
||||
result['invalidity_date_critical'] = ext.critical
|
||||
except x509.ExtensionNotFound:
|
||||
pass
|
||||
return result
|
||||
|
||||
|
||||
def cryptography_dump_revoked(entry):
|
||||
return {
|
||||
'serial_number': entry['serial_number'],
|
||||
'revocation_date': entry['revocation_date'].strftime(TIMESTAMP_FORMAT),
|
||||
'issuer':
|
||||
[cryptography_decode_name(issuer) for issuer in entry['issuer']]
|
||||
if entry['issuer'] is not None else None,
|
||||
'issuer_critical': entry['issuer_critical'],
|
||||
'reason': REVOCATION_REASON_MAP_INVERSE.get(entry['reason']) if entry['reason'] is not None else None,
|
||||
'reason_critical': entry['reason_critical'],
|
||||
'invalidity_date':
|
||||
entry['invalidity_date'].strftime(TIMESTAMP_FORMAT)
|
||||
if entry['invalidity_date'] is not None else None,
|
||||
'invalidity_date_critical': entry['invalidity_date_critical'],
|
||||
}
|
||||
|
||||
|
||||
def cryptography_get_signature_algorithm_oid_from_crl(crl):
|
||||
try:
|
||||
return crl.signature_algorithm_oid
|
||||
except AttributeError:
|
||||
# Older cryptography versions don't have signature_algorithm_oid yet
|
||||
dotted = obj2txt(
|
||||
crl._backend._lib,
|
||||
crl._backend._ffi,
|
||||
crl._x509_crl.sig_alg.algorithm
|
||||
)
|
||||
return x509.oid.ObjectIdentifier(dotted)
|
||||
302
plugins/module_utils/crypto/cryptography_support.py
Normal file
302
plugins/module_utils/crypto/cryptography_support.py
Normal file
@@ -0,0 +1,302 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# (c) 2019, Felix Fontein <felix@fontein.de>
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
import base64
|
||||
import binascii
|
||||
|
||||
from ansible.module_utils._text import to_text
|
||||
|
||||
try:
|
||||
import cryptography
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
import ipaddress
|
||||
except ImportError:
|
||||
# Error handled in the calling module.
|
||||
pass
|
||||
|
||||
from .basic import (
|
||||
CRYPTOGRAPHY_HAS_ED25519,
|
||||
CRYPTOGRAPHY_HAS_ED448,
|
||||
OpenSSLObjectError,
|
||||
)
|
||||
|
||||
from ._objects import (
|
||||
OID_LOOKUP,
|
||||
OID_MAP,
|
||||
NORMALIZE_NAMES_SHORT,
|
||||
NORMALIZE_NAMES,
|
||||
)
|
||||
|
||||
from ._obj2txt import obj2txt
|
||||
|
||||
|
||||
def cryptography_get_extensions_from_cert(cert):
|
||||
# Since cryptography won't give us the DER value for an extension
|
||||
# (that is only stored for unrecognized extensions), we have to re-do
|
||||
# the extension parsing outselves.
|
||||
result = dict()
|
||||
backend = cert._backend
|
||||
x509_obj = cert._x509
|
||||
|
||||
for i in range(backend._lib.X509_get_ext_count(x509_obj)):
|
||||
ext = backend._lib.X509_get_ext(x509_obj, i)
|
||||
if ext == backend._ffi.NULL:
|
||||
continue
|
||||
crit = backend._lib.X509_EXTENSION_get_critical(ext)
|
||||
data = backend._lib.X509_EXTENSION_get_data(ext)
|
||||
backend.openssl_assert(data != backend._ffi.NULL)
|
||||
der = backend._ffi.buffer(data.data, data.length)[:]
|
||||
entry = dict(
|
||||
critical=(crit == 1),
|
||||
value=base64.b64encode(der),
|
||||
)
|
||||
oid = obj2txt(backend._lib, backend._ffi, backend._lib.X509_EXTENSION_get_object(ext))
|
||||
result[oid] = entry
|
||||
return result
|
||||
|
||||
|
||||
def cryptography_get_extensions_from_csr(csr):
|
||||
# Since cryptography won't give us the DER value for an extension
|
||||
# (that is only stored for unrecognized extensions), we have to re-do
|
||||
# the extension parsing outselves.
|
||||
result = dict()
|
||||
backend = csr._backend
|
||||
|
||||
extensions = backend._lib.X509_REQ_get_extensions(csr._x509_req)
|
||||
extensions = backend._ffi.gc(
|
||||
extensions,
|
||||
lambda ext: backend._lib.sk_X509_EXTENSION_pop_free(
|
||||
ext,
|
||||
backend._ffi.addressof(backend._lib._original_lib, "X509_EXTENSION_free")
|
||||
)
|
||||
)
|
||||
|
||||
for i in range(backend._lib.sk_X509_EXTENSION_num(extensions)):
|
||||
ext = backend._lib.sk_X509_EXTENSION_value(extensions, i)
|
||||
if ext == backend._ffi.NULL:
|
||||
continue
|
||||
crit = backend._lib.X509_EXTENSION_get_critical(ext)
|
||||
data = backend._lib.X509_EXTENSION_get_data(ext)
|
||||
backend.openssl_assert(data != backend._ffi.NULL)
|
||||
der = backend._ffi.buffer(data.data, data.length)[:]
|
||||
entry = dict(
|
||||
critical=(crit == 1),
|
||||
value=base64.b64encode(der),
|
||||
)
|
||||
oid = obj2txt(backend._lib, backend._ffi, backend._lib.X509_EXTENSION_get_object(ext))
|
||||
result[oid] = entry
|
||||
return result
|
||||
|
||||
|
||||
def cryptography_name_to_oid(name):
|
||||
dotted = OID_LOOKUP.get(name)
|
||||
if dotted is None:
|
||||
raise OpenSSLObjectError('Cannot find OID for "{0}"'.format(name))
|
||||
return x509.oid.ObjectIdentifier(dotted)
|
||||
|
||||
|
||||
def cryptography_oid_to_name(oid, short=False):
|
||||
dotted_string = oid.dotted_string
|
||||
names = OID_MAP.get(dotted_string)
|
||||
name = names[0] if names else oid._name
|
||||
if short:
|
||||
return NORMALIZE_NAMES_SHORT.get(name, name)
|
||||
else:
|
||||
return NORMALIZE_NAMES.get(name, name)
|
||||
|
||||
|
||||
def cryptography_get_name(name):
|
||||
'''
|
||||
Given a name string, returns a cryptography x509.Name object.
|
||||
Raises an OpenSSLObjectError if the name is unknown or cannot be parsed.
|
||||
'''
|
||||
try:
|
||||
if name.startswith('DNS:'):
|
||||
return x509.DNSName(to_text(name[4:]))
|
||||
if name.startswith('IP:'):
|
||||
return x509.IPAddress(ipaddress.ip_address(to_text(name[3:])))
|
||||
if name.startswith('email:'):
|
||||
return x509.RFC822Name(to_text(name[6:]))
|
||||
if name.startswith('URI:'):
|
||||
return x509.UniformResourceIdentifier(to_text(name[4:]))
|
||||
except Exception as e:
|
||||
raise OpenSSLObjectError('Cannot parse Subject Alternative Name "{0}": {1}'.format(name, e))
|
||||
if ':' not in name:
|
||||
raise OpenSSLObjectError('Cannot parse Subject Alternative Name "{0}" (forgot "DNS:" prefix?)'.format(name))
|
||||
raise OpenSSLObjectError('Cannot parse Subject Alternative Name "{0}" (potentially unsupported by cryptography backend)'.format(name))
|
||||
|
||||
|
||||
def _get_hex(bytesstr):
|
||||
if bytesstr is None:
|
||||
return bytesstr
|
||||
data = binascii.hexlify(bytesstr)
|
||||
data = to_text(b':'.join(data[i:i + 2] for i in range(0, len(data), 2)))
|
||||
return data
|
||||
|
||||
|
||||
def cryptography_decode_name(name):
|
||||
'''
|
||||
Given a cryptography x509.Name object, returns a string.
|
||||
Raises an OpenSSLObjectError if the name is not supported.
|
||||
'''
|
||||
if isinstance(name, x509.DNSName):
|
||||
return 'DNS:{0}'.format(name.value)
|
||||
if isinstance(name, x509.IPAddress):
|
||||
return 'IP:{0}'.format(name.value.compressed)
|
||||
if isinstance(name, x509.RFC822Name):
|
||||
return 'email:{0}'.format(name.value)
|
||||
if isinstance(name, x509.UniformResourceIdentifier):
|
||||
return 'URI:{0}'.format(name.value)
|
||||
if isinstance(name, x509.DirectoryName):
|
||||
# FIXME: test
|
||||
return 'DirName:' + ''.join(['/{0}:{1}'.format(attribute.oid._name, attribute.value) for attribute in name.value])
|
||||
if isinstance(name, x509.RegisteredID):
|
||||
# FIXME: test
|
||||
return 'RegisteredID:{0}'.format(name.value)
|
||||
if isinstance(name, x509.OtherName):
|
||||
# FIXME: test
|
||||
return '{0}:{1}'.format(name.type_id.dotted_string, _get_hex(name.value))
|
||||
raise OpenSSLObjectError('Cannot decode name "{0}"'.format(name))
|
||||
|
||||
|
||||
def _cryptography_get_keyusage(usage):
|
||||
'''
|
||||
Given a key usage identifier string, returns the parameter name used by cryptography's x509.KeyUsage().
|
||||
Raises an OpenSSLObjectError if the identifier is unknown.
|
||||
'''
|
||||
if usage in ('Digital Signature', 'digitalSignature'):
|
||||
return 'digital_signature'
|
||||
if usage in ('Non Repudiation', 'nonRepudiation'):
|
||||
return 'content_commitment'
|
||||
if usage in ('Key Encipherment', 'keyEncipherment'):
|
||||
return 'key_encipherment'
|
||||
if usage in ('Data Encipherment', 'dataEncipherment'):
|
||||
return 'data_encipherment'
|
||||
if usage in ('Key Agreement', 'keyAgreement'):
|
||||
return 'key_agreement'
|
||||
if usage in ('Certificate Sign', 'keyCertSign'):
|
||||
return 'key_cert_sign'
|
||||
if usage in ('CRL Sign', 'cRLSign'):
|
||||
return 'crl_sign'
|
||||
if usage in ('Encipher Only', 'encipherOnly'):
|
||||
return 'encipher_only'
|
||||
if usage in ('Decipher Only', 'decipherOnly'):
|
||||
return 'decipher_only'
|
||||
raise OpenSSLObjectError('Unknown key usage "{0}"'.format(usage))
|
||||
|
||||
|
||||
def cryptography_parse_key_usage_params(usages):
|
||||
'''
|
||||
Given a list of key usage identifier strings, returns the parameters for cryptography's x509.KeyUsage().
|
||||
Raises an OpenSSLObjectError if an identifier is unknown.
|
||||
'''
|
||||
params = dict(
|
||||
digital_signature=False,
|
||||
content_commitment=False,
|
||||
key_encipherment=False,
|
||||
data_encipherment=False,
|
||||
key_agreement=False,
|
||||
key_cert_sign=False,
|
||||
crl_sign=False,
|
||||
encipher_only=False,
|
||||
decipher_only=False,
|
||||
)
|
||||
for usage in usages:
|
||||
params[_cryptography_get_keyusage(usage)] = True
|
||||
return params
|
||||
|
||||
|
||||
def cryptography_get_basic_constraints(constraints):
|
||||
'''
|
||||
Given a list of constraints, returns a tuple (ca, path_length).
|
||||
Raises an OpenSSLObjectError if a constraint is unknown or cannot be parsed.
|
||||
'''
|
||||
ca = False
|
||||
path_length = None
|
||||
if constraints:
|
||||
for constraint in constraints:
|
||||
if constraint.startswith('CA:'):
|
||||
if constraint == 'CA:TRUE':
|
||||
ca = True
|
||||
elif constraint == 'CA:FALSE':
|
||||
ca = False
|
||||
else:
|
||||
raise OpenSSLObjectError('Unknown basic constraint value "{0}" for CA'.format(constraint[3:]))
|
||||
elif constraint.startswith('pathlen:'):
|
||||
v = constraint[len('pathlen:'):]
|
||||
try:
|
||||
path_length = int(v)
|
||||
except Exception as e:
|
||||
raise OpenSSLObjectError('Cannot parse path length constraint "{0}" ({1})'.format(v, e))
|
||||
else:
|
||||
raise OpenSSLObjectError('Unknown basic constraint "{0}"'.format(constraint))
|
||||
return ca, path_length
|
||||
|
||||
|
||||
def cryptography_key_needs_digest_for_signing(key):
|
||||
'''Tests whether the given private key requires a digest algorithm for signing.
|
||||
|
||||
Ed25519 and Ed448 keys do not; they need None to be passed as the digest algorithm.
|
||||
'''
|
||||
if CRYPTOGRAPHY_HAS_ED25519 and isinstance(key, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey):
|
||||
return False
|
||||
if CRYPTOGRAPHY_HAS_ED448 and isinstance(key, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def cryptography_compare_public_keys(key1, key2):
|
||||
'''Tests whether two public keys are the same.
|
||||
|
||||
Needs special logic for Ed25519 and Ed448 keys, since they do not have public_numbers().
|
||||
'''
|
||||
if CRYPTOGRAPHY_HAS_ED25519:
|
||||
a = isinstance(key1, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PublicKey)
|
||||
b = isinstance(key2, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PublicKey)
|
||||
if a or b:
|
||||
if not a or not b:
|
||||
return False
|
||||
a = key1.public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw)
|
||||
b = key2.public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw)
|
||||
return a == b
|
||||
if CRYPTOGRAPHY_HAS_ED448:
|
||||
a = isinstance(key1, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PublicKey)
|
||||
b = isinstance(key2, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PublicKey)
|
||||
if a or b:
|
||||
if not a or not b:
|
||||
return False
|
||||
a = key1.public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw)
|
||||
b = key2.public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw)
|
||||
return a == b
|
||||
return key1.public_numbers() == key2.public_numbers()
|
||||
|
||||
|
||||
def cryptography_serial_number_of_cert(cert):
|
||||
'''Returns cert.serial_number.
|
||||
|
||||
Also works for old versions of cryptography.
|
||||
'''
|
||||
try:
|
||||
return cert.serial_number
|
||||
except AttributeError:
|
||||
# The property was called "serial" before cryptography 1.4
|
||||
return cert.serial
|
||||
45
plugins/module_utils/crypto/identify.py
Normal file
45
plugins/module_utils/crypto/identify.py
Normal file
@@ -0,0 +1,45 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# (c) 2019, Felix Fontein <felix@fontein.de>
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
PEM_START = '-----BEGIN '
|
||||
PEM_END = '-----'
|
||||
PKCS8_PRIVATEKEY_NAMES = ('PRIVATE KEY', 'ENCRYPTED PRIVATE KEY')
|
||||
PKCS1_PRIVATEKEY_SUFFIX = ' PRIVATE KEY'
|
||||
|
||||
|
||||
def identify_private_key_format(content):
|
||||
'''Given the contents of a private key file, identifies its format.'''
|
||||
# See https://github.com/openssl/openssl/blob/master/crypto/pem/pem_pkey.c#L40-L85
|
||||
# (PEM_read_bio_PrivateKey)
|
||||
# and https://github.com/openssl/openssl/blob/master/include/openssl/pem.h#L46-L47
|
||||
# (PEM_STRING_PKCS8, PEM_STRING_PKCS8INF)
|
||||
try:
|
||||
lines = content.decode('utf-8').splitlines(False)
|
||||
if lines[0].startswith(PEM_START) and lines[0].endswith(PEM_END) and len(lines[0]) > len(PEM_START) + len(PEM_END):
|
||||
name = lines[0][len(PEM_START):-len(PEM_END)]
|
||||
if name in PKCS8_PRIVATEKEY_NAMES:
|
||||
return 'pkcs8'
|
||||
if len(name) > len(PKCS1_PRIVATEKEY_SUFFIX) and name.endswith(PKCS1_PRIVATEKEY_SUFFIX):
|
||||
return 'pkcs1'
|
||||
return 'unknown-pem'
|
||||
except UnicodeDecodeError:
|
||||
pass
|
||||
return 'raw'
|
||||
81
plugins/module_utils/crypto/math.py
Normal file
81
plugins/module_utils/crypto/math.py
Normal file
@@ -0,0 +1,81 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# (c) 2019, Felix Fontein <felix@fontein.de>
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
import sys
|
||||
|
||||
|
||||
def binary_exp_mod(f, e, m):
|
||||
'''Computes f^e mod m in O(log e) multiplications modulo m.'''
|
||||
# Compute len_e = floor(log_2(e))
|
||||
len_e = -1
|
||||
x = e
|
||||
while x > 0:
|
||||
x >>= 1
|
||||
len_e += 1
|
||||
# Compute f**e mod m
|
||||
result = 1
|
||||
for k in range(len_e, -1, -1):
|
||||
result = (result * result) % m
|
||||
if ((e >> k) & 1) != 0:
|
||||
result = (result * f) % m
|
||||
return result
|
||||
|
||||
|
||||
def simple_gcd(a, b):
|
||||
'''Compute GCD of its two inputs.'''
|
||||
while b != 0:
|
||||
a, b = b, a % b
|
||||
return a
|
||||
|
||||
|
||||
def quick_is_not_prime(n):
|
||||
'''Does some quick checks to see if we can poke a hole into the primality of n.
|
||||
|
||||
A result of `False` does **not** mean that the number is prime; it just means
|
||||
that we couldn't detect quickly whether it is not prime.
|
||||
'''
|
||||
if n <= 2:
|
||||
return True
|
||||
# The constant in the next line is the product of all primes < 200
|
||||
if simple_gcd(n, 7799922041683461553249199106329813876687996789903550945093032474868511536164700810) > 1:
|
||||
return True
|
||||
# TODO: maybe do some iterations of Miller-Rabin to increase confidence
|
||||
# (https://en.wikipedia.org/wiki/Miller%E2%80%93Rabin_primality_test)
|
||||
return False
|
||||
|
||||
|
||||
python_version = (sys.version_info[0], sys.version_info[1])
|
||||
if python_version >= (2, 7) or python_version >= (3, 1):
|
||||
# Ansible still supports Python 2.6 on remote nodes
|
||||
def count_bits(no):
|
||||
no = abs(no)
|
||||
if no == 0:
|
||||
return 0
|
||||
return no.bit_length()
|
||||
else:
|
||||
# Slow, but works
|
||||
def count_bits(no):
|
||||
no = abs(no)
|
||||
count = 0
|
||||
while no > 0:
|
||||
no >>= 1
|
||||
count += 1
|
||||
return count
|
||||
102
plugins/module_utils/crypto/pyopenssl_support.py
Normal file
102
plugins/module_utils/crypto/pyopenssl_support.py
Normal file
@@ -0,0 +1,102 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# (c) 2019, Felix Fontein <felix@fontein.de>
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
import base64
|
||||
|
||||
from ansible.module_utils._text import to_bytes, to_text
|
||||
|
||||
try:
|
||||
import OpenSSL
|
||||
except ImportError:
|
||||
# Error handled in the calling module.
|
||||
pass
|
||||
|
||||
from ._objects import (
|
||||
NORMALIZE_NAMES_SHORT,
|
||||
NORMALIZE_NAMES,
|
||||
)
|
||||
|
||||
from ._obj2txt import obj2txt
|
||||
|
||||
|
||||
def pyopenssl_normalize_name(name, short=False):
|
||||
nid = OpenSSL._util.lib.OBJ_txt2nid(to_bytes(name))
|
||||
if nid != 0:
|
||||
b_name = OpenSSL._util.lib.OBJ_nid2ln(nid)
|
||||
name = to_text(OpenSSL._util.ffi.string(b_name))
|
||||
if short:
|
||||
return NORMALIZE_NAMES_SHORT.get(name, name)
|
||||
else:
|
||||
return NORMALIZE_NAMES.get(name, name)
|
||||
|
||||
|
||||
def pyopenssl_get_extensions_from_cert(cert):
|
||||
# While pyOpenSSL allows us to get an extension's DER value, it won't
|
||||
# give us the dotted string for an OID. So we have to do some magic to
|
||||
# get hold of it.
|
||||
result = dict()
|
||||
ext_count = cert.get_extension_count()
|
||||
for i in range(0, ext_count):
|
||||
ext = cert.get_extension(i)
|
||||
entry = dict(
|
||||
critical=bool(ext.get_critical()),
|
||||
value=base64.b64encode(ext.get_data()),
|
||||
)
|
||||
oid = obj2txt(
|
||||
OpenSSL._util.lib,
|
||||
OpenSSL._util.ffi,
|
||||
OpenSSL._util.lib.X509_EXTENSION_get_object(ext._extension)
|
||||
)
|
||||
# This could also be done a bit simpler:
|
||||
#
|
||||
# oid = obj2txt(OpenSSL._util.lib, OpenSSL._util.ffi, OpenSSL._util.lib.OBJ_nid2obj(ext._nid))
|
||||
#
|
||||
# Unfortunately this gives the wrong result in case the linked OpenSSL
|
||||
# doesn't know the OID. That's why we have to get the OID dotted string
|
||||
# similarly to how cryptography does it.
|
||||
result[oid] = entry
|
||||
return result
|
||||
|
||||
|
||||
def pyopenssl_get_extensions_from_csr(csr):
|
||||
# While pyOpenSSL allows us to get an extension's DER value, it won't
|
||||
# give us the dotted string for an OID. So we have to do some magic to
|
||||
# get hold of it.
|
||||
result = dict()
|
||||
for ext in csr.get_extensions():
|
||||
entry = dict(
|
||||
critical=bool(ext.get_critical()),
|
||||
value=base64.b64encode(ext.get_data()),
|
||||
)
|
||||
oid = obj2txt(
|
||||
OpenSSL._util.lib,
|
||||
OpenSSL._util.ffi,
|
||||
OpenSSL._util.lib.X509_EXTENSION_get_object(ext._extension)
|
||||
)
|
||||
# This could also be done a bit simpler:
|
||||
#
|
||||
# oid = obj2txt(OpenSSL._util.lib, OpenSSL._util.ffi, OpenSSL._util.lib.OBJ_nid2obj(ext._nid))
|
||||
#
|
||||
# Unfortunately this gives the wrong result in case the linked OpenSSL
|
||||
# doesn't know the OID. That's why we have to get the OID dotted string
|
||||
# similarly to how cryptography does it.
|
||||
result[oid] = entry
|
||||
return result
|
||||
354
plugins/module_utils/crypto/support.py
Normal file
354
plugins/module_utils/crypto/support.py
Normal file
@@ -0,0 +1,354 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# (c) 2016, Yanis Guenane <yanis+ansible@guenane.org>
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
import abc
|
||||
import datetime
|
||||
import errno
|
||||
import hashlib
|
||||
import os
|
||||
import re
|
||||
|
||||
from ansible.module_utils import six
|
||||
from ansible.module_utils._text import to_native, to_bytes
|
||||
|
||||
try:
|
||||
from OpenSSL import crypto
|
||||
HAS_PYOPENSSL = True
|
||||
except ImportError:
|
||||
# Error handled in the calling module.
|
||||
HAS_PYOPENSSL = False
|
||||
|
||||
try:
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.backends import default_backend as cryptography_backend
|
||||
from cryptography.hazmat.primitives.serialization import load_pem_private_key
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
except ImportError:
|
||||
# Error handled in the calling module.
|
||||
pass
|
||||
|
||||
from .basic import (
|
||||
OpenSSLObjectError,
|
||||
OpenSSLBadPassphraseError,
|
||||
)
|
||||
|
||||
|
||||
def get_fingerprint_of_bytes(source):
|
||||
"""Generate the fingerprint of the given bytes."""
|
||||
|
||||
fingerprint = {}
|
||||
|
||||
try:
|
||||
algorithms = hashlib.algorithms
|
||||
except AttributeError:
|
||||
try:
|
||||
algorithms = hashlib.algorithms_guaranteed
|
||||
except AttributeError:
|
||||
return None
|
||||
|
||||
for algo in algorithms:
|
||||
f = getattr(hashlib, algo)
|
||||
try:
|
||||
h = f(source)
|
||||
except ValueError:
|
||||
# This can happen for hash algorithms not supported in FIPS mode
|
||||
# (https://github.com/ansible/ansible/issues/67213)
|
||||
continue
|
||||
try:
|
||||
# Certain hash functions have a hexdigest() which expects a length parameter
|
||||
pubkey_digest = h.hexdigest()
|
||||
except TypeError:
|
||||
pubkey_digest = h.hexdigest(32)
|
||||
fingerprint[algo] = ':'.join(pubkey_digest[i:i + 2] for i in range(0, len(pubkey_digest), 2))
|
||||
|
||||
return fingerprint
|
||||
|
||||
|
||||
def get_fingerprint(path, passphrase=None, content=None, backend='pyopenssl'):
|
||||
"""Generate the fingerprint of the public key. """
|
||||
|
||||
privatekey = load_privatekey(path, passphrase=passphrase, content=content, check_passphrase=False, backend=backend)
|
||||
|
||||
if backend == 'pyopenssl':
|
||||
try:
|
||||
publickey = crypto.dump_publickey(crypto.FILETYPE_ASN1, privatekey)
|
||||
except AttributeError:
|
||||
# If PyOpenSSL < 16.0 crypto.dump_publickey() will fail.
|
||||
try:
|
||||
bio = crypto._new_mem_buf()
|
||||
rc = crypto._lib.i2d_PUBKEY_bio(bio, privatekey._pkey)
|
||||
if rc != 1:
|
||||
crypto._raise_current_error()
|
||||
publickey = crypto._bio_to_string(bio)
|
||||
except AttributeError:
|
||||
# By doing this we prevent the code from raising an error
|
||||
# yet we return no value in the fingerprint hash.
|
||||
return None
|
||||
elif backend == 'cryptography':
|
||||
publickey = privatekey.public_key().public_bytes(
|
||||
serialization.Encoding.DER,
|
||||
serialization.PublicFormat.SubjectPublicKeyInfo
|
||||
)
|
||||
|
||||
return get_fingerprint_of_bytes(publickey)
|
||||
|
||||
|
||||
def load_privatekey(path, passphrase=None, check_passphrase=True, content=None, backend='pyopenssl'):
|
||||
"""Load the specified OpenSSL private key.
|
||||
|
||||
The content can also be specified via content; in that case,
|
||||
this function will not load the key from disk.
|
||||
"""
|
||||
|
||||
try:
|
||||
if content is None:
|
||||
with open(path, 'rb') as b_priv_key_fh:
|
||||
priv_key_detail = b_priv_key_fh.read()
|
||||
else:
|
||||
priv_key_detail = content
|
||||
|
||||
if backend == 'pyopenssl':
|
||||
|
||||
# First try: try to load with real passphrase (resp. empty string)
|
||||
# Will work if this is the correct passphrase, or the key is not
|
||||
# password-protected.
|
||||
try:
|
||||
result = crypto.load_privatekey(crypto.FILETYPE_PEM,
|
||||
priv_key_detail,
|
||||
to_bytes(passphrase or ''))
|
||||
except crypto.Error as e:
|
||||
if len(e.args) > 0 and len(e.args[0]) > 0:
|
||||
if e.args[0][0][2] in ('bad decrypt', 'bad password read'):
|
||||
# This happens in case we have the wrong passphrase.
|
||||
if passphrase is not None:
|
||||
raise OpenSSLBadPassphraseError('Wrong passphrase provided for private key!')
|
||||
else:
|
||||
raise OpenSSLBadPassphraseError('No passphrase provided, but private key is password-protected!')
|
||||
raise OpenSSLObjectError('Error while deserializing key: {0}'.format(e))
|
||||
if check_passphrase:
|
||||
# Next we want to make sure that the key is actually protected by
|
||||
# a passphrase (in case we did try the empty string before, make
|
||||
# sure that the key is not protected by the empty string)
|
||||
try:
|
||||
crypto.load_privatekey(crypto.FILETYPE_PEM,
|
||||
priv_key_detail,
|
||||
to_bytes('y' if passphrase == 'x' else 'x'))
|
||||
if passphrase is not None:
|
||||
# Since we can load the key without an exception, the
|
||||
# key isn't password-protected
|
||||
raise OpenSSLBadPassphraseError('Passphrase provided, but private key is not password-protected!')
|
||||
except crypto.Error as e:
|
||||
if passphrase is None and len(e.args) > 0 and len(e.args[0]) > 0:
|
||||
if e.args[0][0][2] in ('bad decrypt', 'bad password read'):
|
||||
# The key is obviously protected by the empty string.
|
||||
# Don't do this at home (if it's possible at all)...
|
||||
raise OpenSSLBadPassphraseError('No passphrase provided, but private key is password-protected!')
|
||||
elif backend == 'cryptography':
|
||||
try:
|
||||
result = load_pem_private_key(priv_key_detail,
|
||||
None if passphrase is None else to_bytes(passphrase),
|
||||
cryptography_backend())
|
||||
except TypeError:
|
||||
raise OpenSSLBadPassphraseError('Wrong or empty passphrase provided for private key')
|
||||
except ValueError:
|
||||
raise OpenSSLBadPassphraseError('Wrong passphrase provided for private key')
|
||||
|
||||
return result
|
||||
except (IOError, OSError) as exc:
|
||||
raise OpenSSLObjectError(exc)
|
||||
|
||||
|
||||
def load_certificate(path, content=None, backend='pyopenssl'):
|
||||
"""Load the specified certificate."""
|
||||
|
||||
try:
|
||||
if content is None:
|
||||
with open(path, 'rb') as cert_fh:
|
||||
cert_content = cert_fh.read()
|
||||
else:
|
||||
cert_content = content
|
||||
if backend == 'pyopenssl':
|
||||
return crypto.load_certificate(crypto.FILETYPE_PEM, cert_content)
|
||||
elif backend == 'cryptography':
|
||||
return x509.load_pem_x509_certificate(cert_content, cryptography_backend())
|
||||
except (IOError, OSError) as exc:
|
||||
raise OpenSSLObjectError(exc)
|
||||
|
||||
|
||||
def load_certificate_request(path, content=None, backend='pyopenssl'):
|
||||
"""Load the specified certificate signing request."""
|
||||
try:
|
||||
if content is None:
|
||||
with open(path, 'rb') as csr_fh:
|
||||
csr_content = csr_fh.read()
|
||||
else:
|
||||
csr_content = content
|
||||
except (IOError, OSError) as exc:
|
||||
raise OpenSSLObjectError(exc)
|
||||
if backend == 'pyopenssl':
|
||||
return crypto.load_certificate_request(crypto.FILETYPE_PEM, csr_content)
|
||||
elif backend == 'cryptography':
|
||||
return x509.load_pem_x509_csr(csr_content, cryptography_backend())
|
||||
|
||||
|
||||
def parse_name_field(input_dict):
|
||||
"""Take a dict with key: value or key: list_of_values mappings and return a list of tuples"""
|
||||
|
||||
result = []
|
||||
for key in input_dict:
|
||||
if isinstance(input_dict[key], list):
|
||||
for entry in input_dict[key]:
|
||||
result.append((key, entry))
|
||||
else:
|
||||
result.append((key, input_dict[key]))
|
||||
return result
|
||||
|
||||
|
||||
def convert_relative_to_datetime(relative_time_string):
|
||||
"""Get a datetime.datetime or None from a string in the time format described in sshd_config(5)"""
|
||||
|
||||
parsed_result = re.match(
|
||||
r"^(?P<prefix>[+-])((?P<weeks>\d+)[wW])?((?P<days>\d+)[dD])?((?P<hours>\d+)[hH])?((?P<minutes>\d+)[mM])?((?P<seconds>\d+)[sS]?)?$",
|
||||
relative_time_string)
|
||||
|
||||
if parsed_result is None or len(relative_time_string) == 1:
|
||||
# not matched or only a single "+" or "-"
|
||||
return None
|
||||
|
||||
offset = datetime.timedelta(0)
|
||||
if parsed_result.group("weeks") is not None:
|
||||
offset += datetime.timedelta(weeks=int(parsed_result.group("weeks")))
|
||||
if parsed_result.group("days") is not None:
|
||||
offset += datetime.timedelta(days=int(parsed_result.group("days")))
|
||||
if parsed_result.group("hours") is not None:
|
||||
offset += datetime.timedelta(hours=int(parsed_result.group("hours")))
|
||||
if parsed_result.group("minutes") is not None:
|
||||
offset += datetime.timedelta(
|
||||
minutes=int(parsed_result.group("minutes")))
|
||||
if parsed_result.group("seconds") is not None:
|
||||
offset += datetime.timedelta(
|
||||
seconds=int(parsed_result.group("seconds")))
|
||||
|
||||
if parsed_result.group("prefix") == "+":
|
||||
return datetime.datetime.utcnow() + offset
|
||||
else:
|
||||
return datetime.datetime.utcnow() - offset
|
||||
|
||||
|
||||
def get_relative_time_option(input_string, input_name, backend='cryptography'):
|
||||
"""Return an absolute timespec if a relative timespec or an ASN1 formatted
|
||||
string is provided.
|
||||
|
||||
The return value will be a datetime object for the cryptography backend,
|
||||
and a ASN1 formatted string for the pyopenssl backend."""
|
||||
result = to_native(input_string)
|
||||
if result is None:
|
||||
raise OpenSSLObjectError(
|
||||
'The timespec "%s" for %s is not valid' %
|
||||
input_string, input_name)
|
||||
# Relative time
|
||||
if result.startswith("+") or result.startswith("-"):
|
||||
result_datetime = convert_relative_to_datetime(result)
|
||||
if backend == 'pyopenssl':
|
||||
return result_datetime.strftime("%Y%m%d%H%M%SZ")
|
||||
elif backend == 'cryptography':
|
||||
return result_datetime
|
||||
# Absolute time
|
||||
if backend == 'pyopenssl':
|
||||
return input_string
|
||||
elif backend == 'cryptography':
|
||||
for date_fmt in ['%Y%m%d%H%M%SZ', '%Y%m%d%H%MZ', '%Y%m%d%H%M%S%z', '%Y%m%d%H%M%z']:
|
||||
try:
|
||||
return datetime.datetime.strptime(result, date_fmt)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
raise OpenSSLObjectError(
|
||||
'The time spec "%s" for %s is invalid' %
|
||||
(input_string, input_name)
|
||||
)
|
||||
|
||||
|
||||
def select_message_digest(digest_string):
|
||||
digest = None
|
||||
if digest_string == 'sha256':
|
||||
digest = hashes.SHA256()
|
||||
elif digest_string == 'sha384':
|
||||
digest = hashes.SHA384()
|
||||
elif digest_string == 'sha512':
|
||||
digest = hashes.SHA512()
|
||||
elif digest_string == 'sha1':
|
||||
digest = hashes.SHA1()
|
||||
elif digest_string == 'md5':
|
||||
digest = hashes.MD5()
|
||||
return digest
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class OpenSSLObject(object):
|
||||
|
||||
def __init__(self, path, state, force, check_mode):
|
||||
self.path = path
|
||||
self.state = state
|
||||
self.force = force
|
||||
self.name = os.path.basename(path)
|
||||
self.changed = False
|
||||
self.check_mode = check_mode
|
||||
|
||||
def check(self, module, perms_required=True):
|
||||
"""Ensure the resource is in its desired state."""
|
||||
|
||||
def _check_state():
|
||||
return os.path.exists(self.path)
|
||||
|
||||
def _check_perms(module):
|
||||
file_args = module.load_file_common_arguments(module.params)
|
||||
return not module.set_fs_attributes_if_different(file_args, False)
|
||||
|
||||
if not perms_required:
|
||||
return _check_state()
|
||||
|
||||
return _check_state() and _check_perms(module)
|
||||
|
||||
@abc.abstractmethod
|
||||
def dump(self):
|
||||
"""Serialize the object into a dictionary."""
|
||||
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def generate(self):
|
||||
"""Generate the resource."""
|
||||
|
||||
pass
|
||||
|
||||
def remove(self, module):
|
||||
"""Remove the resource from the filesystem."""
|
||||
|
||||
try:
|
||||
os.remove(self.path)
|
||||
self.changed = True
|
||||
except OSError as exc:
|
||||
if exc.errno != errno.ENOENT:
|
||||
raise OpenSSLObjectError(exc)
|
||||
else:
|
||||
pass
|
||||
101
plugins/module_utils/io.py
Normal file
101
plugins/module_utils/io.py
Normal file
@@ -0,0 +1,101 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# (c) 2016, Yanis Guenane <yanis+ansible@guenane.org>
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
import errno
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
|
||||
def load_file_if_exists(path, module=None, ignore_errors=False):
|
||||
'''
|
||||
Load the file as a bytes string. If the file does not exist, ``None`` is returned.
|
||||
|
||||
If ``ignore_errors`` is ``True``, will ignore errors. Otherwise, errors are
|
||||
raised as exceptions if ``module`` is not specified, and result in ``module.fail_json``
|
||||
being called when ``module`` is specified.
|
||||
'''
|
||||
try:
|
||||
with open(path, 'rb') as f:
|
||||
return f.read()
|
||||
except EnvironmentError as exc:
|
||||
if exc.errno == errno.ENOENT:
|
||||
return None
|
||||
if ignore_errors:
|
||||
return None
|
||||
if module is None:
|
||||
raise
|
||||
module.fail_json('Error while loading {0} - {1}'.format(path, str(exc)))
|
||||
except Exception as exc:
|
||||
if ignore_errors:
|
||||
return None
|
||||
if module is None:
|
||||
raise
|
||||
module.fail_json('Error while loading {0} - {1}'.format(path, str(exc)))
|
||||
|
||||
|
||||
def write_file(module, content, default_mode=None, path=None):
|
||||
'''
|
||||
Writes content into destination file as securely as possible.
|
||||
Uses file arguments from module.
|
||||
'''
|
||||
# Find out parameters for file
|
||||
try:
|
||||
file_args = module.load_file_common_arguments(module.params, path=path)
|
||||
except TypeError:
|
||||
# The path argument is only supported in Ansible 2.10+. Fall back to
|
||||
# pre-2.10 behavior of module_utils/crypto.py for older Ansible versions.
|
||||
file_args = module.load_file_common_arguments(module.params)
|
||||
if path is not None:
|
||||
file_args['path'] = path
|
||||
if file_args['mode'] is None:
|
||||
file_args['mode'] = default_mode
|
||||
# Create tempfile name
|
||||
tmp_fd, tmp_name = tempfile.mkstemp(prefix=b'.ansible_tmp')
|
||||
try:
|
||||
os.close(tmp_fd)
|
||||
except Exception:
|
||||
pass
|
||||
module.add_cleanup_file(tmp_name) # if we fail, let Ansible try to remove the file
|
||||
try:
|
||||
try:
|
||||
# Create tempfile
|
||||
file = os.open(tmp_name, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
||||
os.write(file, content)
|
||||
os.close(file)
|
||||
except Exception as e:
|
||||
try:
|
||||
os.remove(tmp_name)
|
||||
except Exception:
|
||||
pass
|
||||
module.fail_json(msg='Error while writing result into temporary file: {0}'.format(e))
|
||||
# Update destination to wanted permissions
|
||||
if os.path.exists(file_args['path']):
|
||||
module.set_fs_attributes_if_different(file_args, False)
|
||||
# Move tempfile to final destination
|
||||
module.atomic_move(tmp_name, file_args['path'])
|
||||
# Try to update permissions again
|
||||
module.set_fs_attributes_if_different(file_args, False)
|
||||
except Exception as e:
|
||||
try:
|
||||
os.remove(tmp_name)
|
||||
except Exception:
|
||||
pass
|
||||
module.fail_json(msg='Error while writing result: {0}'.format(e))
|
||||
Reference in New Issue
Block a user