From 895e99b6843c2fa2274acab824607c33c1a560a4 Mon Sep 17 00:00:00 2001 From: Christian Heimes Date: Mon, 7 Oct 2019 14:13:03 +0200 Subject: [PATCH] Support AES for KRA archival wrapping The vault plugin has used TripleDES (des-ede3-cbc) as default wrapping algorithm since the plugin was introduced. Allow use of AES-128-CBC as alternative wrapping algorithm for transport of secrets. Fixes: https://pagure.io/freeipa/issue/6524 Signed-off-by: Christian Heimes Reviewed-By: Christian Heimes Reviewed-By: Alexander Bokovoy --- API.txt | 7 +- VERSION.m4 | 5 +- ipaclient/plugins/vault.py | 155 +++++++++++++++++++++++++------------ ipalib/capabilities.py | 4 + ipalib/constants.py | 12 +++ ipaserver/plugins/vault.py | 61 ++++++++++++--- 6 files changed, 180 insertions(+), 64 deletions(-) diff --git a/API.txt b/API.txt index 576fa7c51e31886b257ccf176aaf232c0f2ea5ee..f95f2c8457e39f2268386a8a2336952d3285e008 100644 --- a/API.txt +++ b/API.txt @@ -6548,7 +6548,7 @@ output: Output('completed', type=[]) output: Output('failed', type=[]) output: Entry('result') command: vault_archive_internal/1 -args: 1,9,3 +args: 1,10,3 arg: Str('cn', cli_name='name') option: Flag('all', autofill=True, cli_name='all', default=False) option: Bytes('nonce') @@ -6559,6 +6559,7 @@ option: Flag('shared?', autofill=True, default=False) option: Str('username?', cli_name='user') option: Bytes('vault_data') option: Str('version?') +option: StrEnum('wrapping_algo?', autofill=True, default=u'des-ede3-cbc', values=[u'des-ede3-cbc', u'aes-128-cbc']) output: Entry('result') output: Output('summary', type=[, ]) output: PrimaryKey('value') @@ -6649,7 +6650,7 @@ output: Output('completed', type=[]) output: Output('failed', type=[]) output: Entry('result') command: vault_retrieve_internal/1 -args: 1,7,3 +args: 1,8,3 arg: Str('cn', cli_name='name') option: Flag('all', autofill=True, cli_name='all', default=False) option: Flag('raw', autofill=True, cli_name='raw', default=False) @@ -6658,6 +6659,7 @@ option: Bytes('session_key') option: Flag('shared?', autofill=True, default=False) option: Str('username?', cli_name='user') option: Str('version?') +option: StrEnum('wrapping_algo?', autofill=True, default=u'des-ede3-cbc', values=[u'des-ede3-cbc', u'aes-128-cbc']) output: Entry('result') output: Output('summary', type=[, ]) output: PrimaryKey('value') @@ -7327,6 +7329,7 @@ default: vaultcontainer_del/1 default: vaultcontainer_remove_owner/1 default: vaultcontainer_show/1 default: whoami/1 +capability: vault_aes_keywrap 2.246 capability: messages 2.52 capability: optional_uid_params 2.54 capability: permissions2 2.69 diff --git a/VERSION.m4 b/VERSION.m4 index 70aaff4c9b9514a5937eae60074376e1a592464e..997ac35e74fa6f2a96da027ed3ce93cf809b62a7 100644 --- a/VERSION.m4 +++ b/VERSION.m4 @@ -86,9 +86,8 @@ define(IPA_DATA_VERSION, 20100614120000) # # ######################################################## define(IPA_API_VERSION_MAJOR, 2) -# Last change: add enable_sid to config -define(IPA_API_VERSION_MINOR, 245) - +# Last change: Add wrapping algorithm to vault archive/retrieve +define(IPA_API_VERSION_MINOR, 246) ######################################################## # Following values are auto-generated from values above diff --git a/ipaclient/plugins/vault.py b/ipaclient/plugins/vault.py index d3a1d370efaccc7e5b0088bd3df341d76884d509..115171c7768d44251c17d0bcdac9c37b3a25db99 100644 --- a/ipaclient/plugins/vault.py +++ b/ipaclient/plugins/vault.py @@ -25,11 +25,12 @@ import io import json import logging import os +import ssl import tempfile from cryptography.fernet import Fernet, InvalidToken from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC from cryptography.hazmat.primitives.asymmetric import padding from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes @@ -39,7 +40,7 @@ from cryptography.hazmat.primitives.serialization import ( from ipaclient.frontend import MethodOverride from ipalib import x509 -from ipalib.constants import USER_CACHE_PATH +from ipalib import constants from ipalib.frontend import Local, Method, Object from ipalib.util import classproperty from ipalib import api, errors @@ -546,42 +547,49 @@ class vault_mod(Local): return response -class _TransportCertCache: +class _KraConfigCache: + """The KRA config cache stores vaultconfig-show result. + """ def __init__(self): self._dirname = os.path.join( - USER_CACHE_PATH, 'ipa', 'kra-transport-certs' + constants.USER_CACHE_PATH, 'ipa', 'kra-config' ) def _get_filename(self, domain): - basename = DNSName(domain).ToASCII() + '.pem' + basename = DNSName(domain).ToASCII() + '.json' return os.path.join(self._dirname, basename) - def load_cert(self, domain): - """Load cert from cache + def load(self, domain): + """Load config from cache :param domain: IPA domain - :return: cryptography.x509.Certificate or None + :return: dict or None """ filename = self._get_filename(domain) try: try: - return x509.load_certificate_from_file(filename) - except EnvironmentError as e: + with open(filename) as f: + return json.load(f) + except OSError as e: if e.errno != errno.ENOENT: raise except Exception: logger.warning("Failed to load %s", filename, exc_info=True) return None - def store_cert(self, domain, transport_cert): - """Store a new cert or override existing cert + def store(self, domain, response): + """Store config in cache :param domain: IPA domain - :param transport_cert: cryptography.x509.Certificate - :return: True if cert was stored successfully + :param config: ipa vaultconfig-show response + :return: True if config was stored successfully """ + config = response['result'].copy() + # store certificate as PEM-encoded ASCII + config['transport_cert'] = ssl.DER_cert_to_PEM_cert( + config['transport_cert'] + ) filename = self._get_filename(domain) - pem = transport_cert.public_bytes(serialization.Encoding.PEM) try: try: os.makedirs(self._dirname) @@ -589,9 +597,9 @@ class _TransportCertCache: if e.errno != errno.EEXIST: raise with tempfile.NamedTemporaryFile(dir=self._dirname, delete=False, - mode='wb') as f: + mode='w') as f: try: - f.write(pem) + json.dump(config, f) ipautil.flush_sync(f) f.close() os.rename(f.name, filename) @@ -604,8 +612,8 @@ class _TransportCertCache: else: return True - def remove_cert(self, domain): - """Remove a cert from cache, ignores errors + def remove(self, domain): + """Remove a config from cache, ignores errors :param domain: IPA domain :return: True if cert was found and removed @@ -621,7 +629,7 @@ class _TransportCertCache: return True -_transport_cert_cache = _TransportCertCache() +_kra_config_cache = _KraConfigCache() @register(override=True, no_fail=True) @@ -636,13 +644,8 @@ class vaultconfig_show(MethodOverride): response = super(vaultconfig_show, self).forward(*args, **options) - # cache transport certificate - transport_cert = x509.load_der_x509_certificate( - response['result']['transport_cert']) - - _transport_cert_cache.store_cert( - self.api.env.domain, transport_cert - ) + # cache config + _kra_config_cache.store(self.api.env.domain, response) if file: with open(file, 'wb') as f: @@ -652,10 +655,54 @@ class vaultconfig_show(MethodOverride): class ModVaultData(Local): - def _generate_session_key(self): - key_length = max(algorithms.TripleDES.key_sizes) - algo = algorithms.TripleDES(os.urandom(key_length // 8)) - return algo + def _generate_session_key(self, name): + if name not in constants.VAULT_WRAPPING_SUPPORTED_ALGOS: + msg = _("{algo} is not a supported vault wrapping algorithm") + raise errors.ValidationError(msg.format(algo=repr(name))) + if name == constants.VAULT_WRAPPING_AES128_CBC: + return algorithms.AES(os.urandom(128 // 8)) + elif name == constants.VAULT_WRAPPING_3DES: + return algorithms.TripleDES(os.urandom(196 // 8)) + else: + # unreachable + raise ValueError(name) + + def _get_vaultconfig(self, force_refresh=False): + config = None + if not force_refresh: + config = _kra_config_cache.load(self.api.env.domain) + if config is None: + # vaultconfig_show also caches data + response = self.api.Command.vaultconfig_show() + config = response['result'] + transport_cert = x509.load_der_x509_certificate( + config['transport_cert'] + ) + else: + # cached JSON uses PEM-encoded ASCII string + transport_cert = x509.load_pem_x509_certificate( + config['transport_cert'].encode('ascii') + ) + + default_algo = config.get('wrapping_default_algorithm') + if default_algo is None: + # old server + wrapping_algo = constants.VAULT_WRAPPING_AES128_CBC + elif default_algo in constants.VAULT_WRAPPING_SUPPORTED_ALGOS: + # try to use server default + wrapping_algo = default_algo + else: + # prefer server's sorting order + for algo in config['wrapping_supported_algorithms']: + if algo in constants.VAULT_WRAPPING_SUPPORTED_ALGOS: + wrapping_algo = algo + break + else: + raise errors.ValidationError( + "No overlapping wrapping algorithm between server and " + "client." + ) + return transport_cert, wrapping_algo def _do_internal(self, algo, transport_cert, raise_unexpected, *args, **options): @@ -675,29 +722,23 @@ class ModVaultData(Local): except (errors.InternalError, errors.ExecutionError, errors.GenericError): - _transport_cert_cache.remove_cert(self.api.env.domain) + _kra_config_cache.remove(self.api.env.domain) if raise_unexpected: raise return None - def internal(self, algo, *args, **options): + def internal(self, algo, transport_cert, *args, **options): """ Calls the internal counterpart of the command. """ - domain = self.api.env.domain - # try call with cached transport certificate - transport_cert = _transport_cert_cache.load_cert(domain) - if transport_cert is not None: - result = self._do_internal(algo, transport_cert, False, + result = self._do_internal(algo, transport_cert, False, *args, **options) - if result is not None: - return result + if result is not None: + return result # retrieve transport certificate (cached by vaultconfig_show) - response = self.api.Command.vaultconfig_show() - transport_cert = x509.load_der_x509_certificate( - response['result']['transport_cert']) + transport_cert = self._get_vaultconfig(force_refresh=True)[0] # call with the retrieved transport certificate return self._do_internal(algo, transport_cert, True, *args, **options) @@ -777,7 +818,7 @@ class vault_archive(ModVaultData): def _wrap_data(self, algo, json_vault_data): """Encrypt data with wrapped session key and transport cert - :param bytes algo: wrapping algorithm instance + :param algo: wrapping algorithm instance :param bytes json_vault_data: dumped vault data :return: """ @@ -929,15 +970,24 @@ class vault_archive(ModVaultData): json_vault_data = json.dumps(vault_data).encode('utf-8') + # get config + transport_cert, wrapping_algo = self._get_vaultconfig() + # let options override wrapping algo + # For backwards compatibility do not send old legacy wrapping algo + # to server. Only send the option when non-3DES is used. + wrapping_algo = options.pop('wrapping_algo', wrapping_algo) + if wrapping_algo != constants.VAULT_WRAPPING_3DES: + options['wrapping_algo'] = wrapping_algo + # generate session key - algo = self._generate_session_key() + algo = self._generate_session_key(wrapping_algo) # wrap vault data nonce, wrapped_vault_data = self._wrap_data(algo, json_vault_data) options.update( nonce=nonce, vault_data=wrapped_vault_data ) - return self.internal(algo, *args, **options) + return self.internal(algo, transport_cert, *args, **options) @register(no_fail=True) @@ -1061,10 +1111,19 @@ class vault_retrieve(ModVaultData): vault = self.api.Command.vault_show(*args, **options)['result'] vault_type = vault['ipavaulttype'][0] + # get config + transport_cert, wrapping_algo = self._get_vaultconfig() + # let options override wrapping algo + # For backwards compatibility do not send old legacy wrapping algo + # to server. Only send the option when non-3DES is used. + wrapping_algo = options.pop('wrapping_algo', wrapping_algo) + if wrapping_algo != constants.VAULT_WRAPPING_3DES: + options['wrapping_algo'] = wrapping_algo + # generate session key - algo = self._generate_session_key() + algo = self._generate_session_key(wrapping_algo) # send retrieval request to server - response = self.internal(algo, *args, **options) + response = self.internal(algo, transport_cert, *args, **options) # unwrap data with session key vault_data = self._unwrap_response( algo, diff --git a/ipalib/capabilities.py b/ipalib/capabilities.py index 55b84aa6bc73d583e7bd5d03d2f4f1cc5c8e7c0b..4d8ae408bf67c280d27ce494baa9db9aaff0cd69 100644 --- a/ipalib/capabilities.py +++ b/ipalib/capabilities.py @@ -54,6 +54,10 @@ capabilities = dict( # dns_name_values: dnsnames as objects dns_name_values=u'2.88', + + # vault supports aes key wrapping + vault_aes_keywrap='2.246' + ) diff --git a/ipalib/constants.py b/ipalib/constants.py index 9f19b0f9941ba5068f1e6c218092e3b76fdc7599..11171b2e8aeb6f7306299b2bd7db3a3f39d29d4a 100644 --- a/ipalib/constants.py +++ b/ipalib/constants.py @@ -374,3 +374,15 @@ KRA_TRACKING_REQS = { } ALLOWED_NETBIOS_CHARS = string.ascii_uppercase + string.digits + '-' + +# vault data wrapping algorithms +VAULT_WRAPPING_3DES = 'des-ede3-cbc' +VAULT_WRAPPING_AES128_CBC = 'aes-128-cbc' +VAULT_WRAPPING_SUPPORTED_ALGOS = ( + # old default was 3DES + VAULT_WRAPPING_3DES, + # supported since pki-kra >= 10.4 + VAULT_WRAPPING_AES128_CBC, +) +# 3DES for backwards compatibility +VAULT_WRAPPING_DEFAULT_ALGO = VAULT_WRAPPING_3DES diff --git a/ipaserver/plugins/vault.py b/ipaserver/plugins/vault.py index aebac7dff7bb9d183c6012cc685577d476e18c4e..4d40f66c6a793a831e91c5fe25c8b5277cbd1972 100644 --- a/ipaserver/plugins/vault.py +++ b/ipaserver/plugins/vault.py @@ -23,6 +23,10 @@ from ipalib.frontend import Command, Object from ipalib import api, errors from ipalib import Bytes, Flag, Str, StrEnum from ipalib import output +from ipalib.constants import ( + VAULT_WRAPPING_SUPPORTED_ALGOS, VAULT_WRAPPING_DEFAULT_ALGO, + VAULT_WRAPPING_3DES, VAULT_WRAPPING_AES128_CBC, +) from ipalib.crud import PKQuery, Retrieve from ipalib.parameters import Principal from ipalib.plugable import Registry @@ -39,14 +43,8 @@ from ipaserver.masters import is_service_enabled if api.env.in_server: import pki.account import pki.key - # pylint: disable=no-member - try: - # pki >= 10.4.0 - from pki.crypto import DES_EDE3_CBC_OID - except ImportError: - DES_EDE3_CBC_OID = pki.key.KeyClient.DES_EDE3_CBC_OID - # pylint: enable=no-member - + from pki.crypto import DES_EDE3_CBC_OID + from pki.crypto import AES_128_CBC_OID if six.PY3: unicode = str @@ -652,6 +652,20 @@ class vault(LDAPObject): ), ) + def _translate_algorithm(self, name): + if name is None: + name = VAULT_WRAPPING_DEFAULT_ALGO + if name not in VAULT_WRAPPING_SUPPORTED_ALGOS: + msg = _("{algo} is not a supported vault wrapping algorithm") + raise errors.ValidationError(msg.format(algo=name)) + if name == VAULT_WRAPPING_3DES: + return DES_EDE3_CBC_OID + elif name == VAULT_WRAPPING_AES128_CBC: + return AES_128_CBC_OID + else: + # unreachable + raise ValueError(name) + def get_dn(self, *keys, **options): """ Generates vault DN from parameters. @@ -992,14 +1006,18 @@ class vaultconfig_show(Retrieve): ) def execute(self, *args, **options): - if not self.api.Command.kra_is_enabled()['result']: raise errors.InvocationError( format=_('KRA service is not enabled')) + config = dict( + wrapping_supported_algorithms=VAULT_WRAPPING_SUPPORTED_ALGOS, + wrapping_default_algorithm=VAULT_WRAPPING_DEFAULT_ALGO, + ) + with self.api.Backend.kra.get_client() as kra_client: transport_cert = kra_client.system_certs.get_transport_cert() - config = {'transport_cert': transport_cert.binary} + config['transport_cert'] = transport_cert.binary self.api.Object.config.show_servroles_attributes( config, "KRA server", **options) @@ -1029,6 +1047,13 @@ class vault_archive_internal(PKQuery): 'nonce', doc=_('Nonce'), ), + StrEnum( + 'wrapping_algo?', + doc=_('Key wrapping algorithm'), + values=VAULT_WRAPPING_SUPPORTED_ALGOS, + default=VAULT_WRAPPING_DEFAULT_ALGO, + autofill=True, + ), ) has_output = output.standard_entry @@ -1045,6 +1070,9 @@ class vault_archive_internal(PKQuery): nonce = options.pop('nonce') wrapped_session_key = options.pop('session_key') + wrapping_algo = options.pop('wrapping_algo', None) + algorithm_oid = self.obj._translate_algorithm(wrapping_algo) + # retrieve vault info vault = self.api.Command.vault_show(*args, **options)['result'] @@ -1071,7 +1099,7 @@ class vault_archive_internal(PKQuery): pki.key.KeyClient.PASS_PHRASE_TYPE, wrapped_vault_data, wrapped_session_key, - algorithm_oid=DES_EDE3_CBC_OID, + algorithm_oid=algorithm_oid, nonce_iv=nonce, ) @@ -1098,6 +1126,13 @@ class vault_retrieve_internal(PKQuery): 'session_key', doc=_('Session key wrapped with transport certificate'), ), + StrEnum( + 'wrapping_algo?', + doc=_('Key wrapping algorithm'), + values=VAULT_WRAPPING_SUPPORTED_ALGOS, + default=VAULT_WRAPPING_DEFAULT_ALGO, + autofill=True, + ), ) has_output = output.standard_entry @@ -1112,6 +1147,9 @@ class vault_retrieve_internal(PKQuery): wrapped_session_key = options.pop('session_key') + wrapping_algo = options.pop('wrapping_algo', None) + algorithm_oid = self.obj._translate_algorithm(wrapping_algo) + # retrieve vault info vault = self.api.Command.vault_show(*args, **options)['result'] @@ -1132,6 +1170,9 @@ class vault_retrieve_internal(PKQuery): key_info = response.key_infos[0] + # XXX hack + kra_client.keys.encrypt_alg_oid = algorithm_oid + # retrieve encrypted data from KRA key = kra_client.keys.retrieve_key( key_info.get_key_id(), -- 2.34.1