mirror of
https://salsa.debian.org/freeipa-team/freeipa.git
synced 2025-01-26 16:16:31 -06:00
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 <cheimes@redhat.com> Reviewed-By: Alexander Bokovoy <abokovoy@redhat.com> Reviewed-By: Christian Heimes <cheimes@redhat.com>
This commit is contained in:
parent
517ae594e1
commit
40c362e1ee
7
API.txt
7
API.txt
@ -6548,7 +6548,7 @@ output: Output('completed', type=[<type 'int'>])
|
||||
output: Output('failed', type=[<type 'dict'>])
|
||||
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=[<type 'unicode'>, <type 'NoneType'>])
|
||||
output: PrimaryKey('value')
|
||||
@ -6649,7 +6650,7 @@ output: Output('completed', type=[<type 'int'>])
|
||||
output: Output('failed', type=[<type 'dict'>])
|
||||
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=[<type 'unicode'>, <type 'NoneType'>])
|
||||
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
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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'
|
||||
|
||||
)
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -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,12 +43,8 @@ from ipaserver.masters import is_service_enabled
|
||||
if api.env.in_server:
|
||||
import pki.account
|
||||
import pki.key
|
||||
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
|
||||
|
||||
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(),
|
||||
|
Loading…
Reference in New Issue
Block a user