openssl_privatekey_info: disable private key consistency checks by default (#309)

* Disable private key consistency checks by default.

* Improve formulations, mention side-channel attacks.
pull/313/head
Felix Fontein 2021-10-20 18:28:22 +02:00 committed by GitHub
parent a581f1ebcd
commit c5df302faa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 51 additions and 30 deletions

View File

@ -0,0 +1,4 @@
minor_changes:
- openssl_privatekey_info - add ``check_consistency`` option to request private key consistency checks to be done (https://github.com/ansible-collections/community.crypto/pull/309).
breaking_changes:
- openssl_privatekey_info - by default consistency checks are not run; they need to be explicitly requested by passing ``check_consistency=true`` (https://github.com/ansible-collections/community.crypto/pull/309).

View File

@ -55,20 +55,21 @@ else:
SIGNATURE_TEST_DATA = b'1234' SIGNATURE_TEST_DATA = b'1234'
def _get_cryptography_private_key_info(key): def _get_cryptography_private_key_info(key, need_private_key_data=False):
key_type, key_public_data = _get_cryptography_public_key_info(key.public_key()) key_type, key_public_data = _get_cryptography_public_key_info(key.public_key())
key_private_data = dict() key_private_data = dict()
if isinstance(key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey): if need_private_key_data:
private_numbers = key.private_numbers() if isinstance(key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey):
key_private_data['p'] = private_numbers.p private_numbers = key.private_numbers()
key_private_data['q'] = private_numbers.q key_private_data['p'] = private_numbers.p
key_private_data['exponent'] = private_numbers.d key_private_data['q'] = private_numbers.q
elif isinstance(key, cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKey): key_private_data['exponent'] = private_numbers.d
private_numbers = key.private_numbers() elif isinstance(key, cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKey):
key_private_data['x'] = private_numbers.x private_numbers = key.private_numbers()
elif isinstance(key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey): key_private_data['x'] = private_numbers.x
private_numbers = key.private_numbers() elif isinstance(key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey):
key_private_data['multiplier'] = private_numbers.private_value private_numbers = key.private_numbers()
key_private_data['multiplier'] = private_numbers.private_value
return key_type, key_public_data, key_private_data return key_type, key_public_data, key_private_data
@ -174,20 +175,21 @@ class PrivateKeyParseError(OpenSSLObjectError):
@six.add_metaclass(abc.ABCMeta) @six.add_metaclass(abc.ABCMeta)
class PrivateKeyInfoRetrieval(object): class PrivateKeyInfoRetrieval(object):
def __init__(self, module, backend, content, passphrase=None, return_private_key_data=False): def __init__(self, module, backend, content, passphrase=None, return_private_key_data=False, check_consistency=False):
# content must be a bytes string # content must be a bytes string
self.module = module self.module = module
self.backend = backend self.backend = backend
self.content = content self.content = content
self.passphrase = passphrase self.passphrase = passphrase
self.return_private_key_data = return_private_key_data self.return_private_key_data = return_private_key_data
self.check_consistency = check_consistency
@abc.abstractmethod @abc.abstractmethod
def _get_public_key(self, binary): def _get_public_key(self, binary):
pass pass
@abc.abstractmethod @abc.abstractmethod
def _get_key_info(self): def _get_key_info(self, need_private_key_data=False):
pass pass
@abc.abstractmethod @abc.abstractmethod
@ -216,20 +218,22 @@ class PrivateKeyInfoRetrieval(object):
result['public_key_fingerprints'] = get_fingerprint_of_bytes( result['public_key_fingerprints'] = get_fingerprint_of_bytes(
pk, prefer_one=prefer_one_fingerprint) if pk is not None else dict() pk, prefer_one=prefer_one_fingerprint) if pk is not None else dict()
key_type, key_public_data, key_private_data = self._get_key_info() key_type, key_public_data, key_private_data = self._get_key_info(
need_private_key_data=self.return_private_key_data or self.check_consistency)
result['type'] = key_type result['type'] = key_type
result['public_data'] = key_public_data result['public_data'] = key_public_data
if self.return_private_key_data: if self.return_private_key_data:
result['private_data'] = key_private_data result['private_data'] = key_private_data
result['key_is_consistent'] = self._is_key_consistent(key_public_data, key_private_data) if self.check_consistency:
if result['key_is_consistent'] is False: result['key_is_consistent'] = self._is_key_consistent(key_public_data, key_private_data)
# Only fail when it is False, to avoid to fail on None (which means "we don't know") if result['key_is_consistent'] is False:
msg = ( # Only fail when it is False, to avoid to fail on None (which means "we don't know")
"Private key is not consistent! (See " msg = (
"https://blog.hboeck.de/archives/888-How-I-tricked-Symantec-with-a-Fake-Private-Key.html)" "Private key is not consistent! (See "
) "https://blog.hboeck.de/archives/888-How-I-tricked-Symantec-with-a-Fake-Private-Key.html)"
raise PrivateKeyConsistencyError(msg, result) )
raise PrivateKeyConsistencyError(msg, result)
return result return result
@ -244,8 +248,8 @@ class PrivateKeyInfoRetrievalCryptography(PrivateKeyInfoRetrieval):
serialization.PublicFormat.SubjectPublicKeyInfo serialization.PublicFormat.SubjectPublicKeyInfo
) )
def _get_key_info(self): def _get_key_info(self, need_private_key_data=False):
return _get_cryptography_private_key_info(self.key) return _get_cryptography_private_key_info(self.key, need_private_key_data=need_private_key_data)
def _is_key_consistent(self, key_public_data, key_private_data): def _is_key_consistent(self, key_public_data, key_private_data):
return _is_cryptography_key_consistent(self.key, key_public_data, key_private_data) return _is_cryptography_key_consistent(self.key, key_public_data, key_private_data)
@ -258,7 +262,7 @@ def get_privatekey_info(module, backend, content, passphrase=None, return_privat
return info.get_info(prefer_one_fingerprint=prefer_one_fingerprint) return info.get_info(prefer_one_fingerprint=prefer_one_fingerprint)
def select_backend(module, backend, content, passphrase=None, return_private_key_data=False): def select_backend(module, backend, content, passphrase=None, return_private_key_data=False, check_consistency=False):
if backend == 'auto': if backend == 'auto':
# Detection what is possible # Detection what is possible
can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION) can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
@ -277,6 +281,6 @@ def select_backend(module, backend, content, passphrase=None, return_private_key
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)), module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
exception=CRYPTOGRAPHY_IMP_ERR) exception=CRYPTOGRAPHY_IMP_ERR)
return backend, PrivateKeyInfoRetrievalCryptography( return backend, PrivateKeyInfoRetrievalCryptography(
module, content, passphrase=passphrase, return_private_key_data=return_private_key_data) module, content, passphrase=passphrase, return_private_key_data=return_private_key_data, check_consistency=check_consistency)
else: else:
raise ValueError('Unsupported value for backend: {0}'.format(backend)) raise ValueError('Unsupported value for backend: {0}'.format(backend))

View File

@ -45,9 +45,20 @@ options:
- Whether to return private key data. - Whether to return private key data.
- Only set this to C(yes) when you want private information about this key to - Only set this to C(yes) when you want private information about this key to
leave the remote machine. leave the remote machine.
- "WARNING: you have to make sure that private key data isn't accidentally logged!" - "B(WARNING:) you have to make sure that private key data isn't accidentally logged!"
type: bool type: bool
default: no default: no
check_consistency:
description:
- Whether to check consistency of the private key.
- In community.crypto < 2.0.0, consistency was always checked.
- Since community.crypto 2.0.0, the consistency check has been disabled by default to
avoid private key material to be transported around and computed with, and only do
so when requested explicitly. This can potentially prevent
L(side-channel attacks,https://en.wikipedia.org/wiki/Side-channel_attack).
type: bool
default: false
version_added: 2.0.0
select_crypto_backend: select_crypto_backend:
description: description:
@ -95,7 +106,7 @@ key_is_consistent:
- Whether the key is consistent. Can also return C(none) next to C(yes) and - Whether the key is consistent. Can also return C(none) next to C(yes) and
C(no), to indicate that consistency could not be checked. C(no), to indicate that consistency could not be checked.
- In case the check returns C(no), the module will fail. - In case the check returns C(no), the module will fail.
returned: always returned: when I(check_consistency=true)
type: bool type: bool
public_key: public_key:
description: Private key's public key in PEM format. description: Private key's public key in PEM format.
@ -208,6 +219,7 @@ def main():
content=dict(type='str', no_log=True), content=dict(type='str', no_log=True),
passphrase=dict(type='str', no_log=True), passphrase=dict(type='str', no_log=True),
return_private_key_data=dict(type='bool', default=False), return_private_key_data=dict(type='bool', default=False),
check_consistency=dict(type='bool', default=False),
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=(
@ -241,7 +253,8 @@ def main():
module.params['select_crypto_backend'], module.params['select_crypto_backend'],
data, data,
passphrase=module.params['passphrase'], passphrase=module.params['passphrase'],
return_private_key_data=module.params['return_private_key_data']) return_private_key_data=module.params['return_private_key_data'],
check_consistency=module.params['check_consistency'])
try: try:
result.update(module_backend.get_info()) result.update(module_backend.get_info())