Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New cryptography backend for openssl_certificate #53924

Merged
merged 26 commits into from
Mar 22, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
203f8e7
New cryptography backend for openssl_certificate
Shaps Mar 11, 2019
a618ec0
Run test with various backends.
felixfontein Mar 18, 2019
832d2ed
Prefixing tests.
felixfontein Mar 18, 2019
bd8ce84
Make sure we have the correct backend available.
felixfontein Mar 18, 2019
bf45b70
Linting (flake8).
felixfontein Mar 18, 2019
d119322
Make sure certificate is actually valid at some time in the past.
felixfontein Mar 18, 2019
316c0ea
Improve error handling.
felixfontein Mar 18, 2019
48f624a
Trying to fix validation for cryptography backend.
felixfontein Mar 18, 2019
518a6fb
Moved cryptography import to separate try/except
Shaps Mar 18, 2019
bc50878
Fixed issue with keyUsage test in assertonly
Shaps Mar 19, 2019
e30befd
Fixed CI/Lint issues
Shaps Mar 19, 2019
60fa534
Fix private key problem for OwnCA.
felixfontein Mar 19, 2019
ce7255a
Cryptography backend doesn't support v2 certs.
felixfontein Mar 19, 2019
691a39b
issue an expired cert with command when using cryptography backend
Shaps Mar 19, 2019
fcb4877
Added warning when backend is auto and v2 cert is requested
Shaps Mar 19, 2019
ce86719
Bumped min cryptography version to 1.6
Shaps Mar 19, 2019
c4ad8a1
Correctly check for failure when backend is cryptography and cert is v2
Shaps Mar 19, 2019
b0fbdbf
Use self.backend where possible
Shaps Mar 19, 2019
4995db3
Use secp521r1 EC when testing on CentOS6
Shaps Mar 20, 2019
b7096bf
Fixed pylint issue
Shaps Mar 20, 2019
d4a6978
AcmeCertificate support for both backends
Shaps Mar 20, 2019
757c85d
Review fixes
Shaps Mar 21, 2019
175121c
Fixed missing '(' when raising error
Shaps Mar 21, 2019
99c255d
Fixed date_fmt loop
Shaps Mar 21, 2019
e6f8b8a
Updated docs and requirements with cryptography
Shaps Mar 21, 2019
9a6d077
Add openssl_certificate to changelog.
felixfontein Mar 21, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions changelogs/fragments/openssl-cryptography.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
minor_changes:
- "openssl_certificate - now works with both PyOpenSSL and cryptography Python libraries. Autodetection can be overridden with ``select_crypto_backend`` option."
- "openssl_csr - now works with both PyOpenSSL and cryptography Python libraries. Autodetection can be overridden with ``select_crypto_backend`` option."
- "openssl_privatekey - now works with both PyOpenSSL and cryptography Python libraries. Autodetection can be overridden with ``select_crypto_backend`` option."
116 changes: 79 additions & 37 deletions lib/ansible/module_utils/crypto.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,16 @@
# user know that OpenSSL couldn't be found.
pass

try:
from cryptography import x509
Shaps marked this conversation as resolved.
Show resolved Hide resolved
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
except ImportError:
# Error handled in the calling module.
pass


import abc
import datetime
import errno
Expand Down Expand Up @@ -82,70 +92,87 @@ def get_fingerprint(path, passphrase=None):
return None


def load_privatekey(path, passphrase=None, check_passphrase=True):
def load_privatekey(path, passphrase=None, check_passphrase=True, backend='pyopenssl'):
"""Load the specified OpenSSL private key."""

try:
with open(path, 'rb') as b_priv_key_fh:
priv_key_detail = b_priv_key_fh.read()

# 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 and e.args[0][0][2] == 'bad decrypt':
# 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
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)
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:
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:
if passphrase is None and len(e.args) > 0 and len(e.args[0]) > 0 and e.args[0][0][2] == 'bad decrypt':
# 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!')
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 and e.args[0][0][2] == 'bad decrypt':
# 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
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:
if passphrase is None and len(e.args) > 0 and len(e.args[0]) > 0 and e.args[0][0][2] == 'bad decrypt':
# 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,
passphrase,
cryptography_backend())
except TypeError as e:
raise OpenSSLBadPassphraseError('Wrong or empty passphrase provided for private key')
except ValueError as e:
raise OpenSSLBadPassphraseError('Wrong passphrase provided for private key')

return result
except (IOError, OSError) as exc:
raise OpenSSLObjectError(exc)


def load_certificate(path):
def load_certificate(path, backend='pyopenssl'):
"""Load the specified certificate."""

try:
with open(path, 'rb') as cert_fh:
cert_content = cert_fh.read()
return crypto.load_certificate(crypto.FILETYPE_PEM, cert_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):
def load_certificate_request(path, backend='pyopenssl'):
"""Load the specified certificate signing request."""

try:
with open(path, 'rb') as csr_fh:
csr_content = csr_fh.read()
return crypto.load_certificate_request(crypto.FILETYPE_PEM, csr_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):
Expand Down Expand Up @@ -192,6 +219,21 @@ def convert_relative_to_datetime(relative_time_string):
return datetime.datetime.utcnow() - offset


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):

Expand Down