Import Upstream version 4.12.4
This commit is contained in:
@@ -34,3 +34,11 @@ class automember_add_condition(MethodOverride):
|
||||
flags=['suppress_empty'],
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@register(override=True, no_fail=True)
|
||||
class automember_rebuild(MethodOverride):
|
||||
def interactive_prompt_callback(self, kw):
|
||||
msg = _('IMPORTANT: In case of a high number of users, hosts or '
|
||||
'groups, the operation may require high CPU usage.')
|
||||
self.Backend.textui.print_plain(msg)
|
||||
|
||||
@@ -104,13 +104,15 @@ class automountlocation_tofiles(MethodOverride):
|
||||
textui.print_plain('/etc/%s:' % m['automountmapname'])
|
||||
for k in orphankeys:
|
||||
if len(k) == 0: continue
|
||||
dn = DN(k[0]['dn'])
|
||||
if dn['automountmapname'] == m['automountmapname'][0]:
|
||||
textui.print_plain(
|
||||
'%s\t%s' % (
|
||||
k[0]['automountkey'][0], k[0]['automountinformation'][0]
|
||||
for key in k:
|
||||
dn = DN(key['dn'])
|
||||
if dn['automountmapname'] == m['automountmapname'][0]:
|
||||
textui.print_plain(
|
||||
'%s\t%s' % (
|
||||
key['automountkey'][0],
|
||||
key['automountinformation'][0]
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@register()
|
||||
@@ -216,8 +218,8 @@ class automountlocation_import(Command):
|
||||
# Now iterate over the map files and add the keys. To handle
|
||||
# continuation lines I'll make a pass through it to skip comments
|
||||
# etc and also to combine lines.
|
||||
for m in maps:
|
||||
map = self.__read_mapfile(maps[m])
|
||||
for m, filename in maps.items():
|
||||
map = self.__read_mapfile(filename)
|
||||
lines = []
|
||||
cont = ''
|
||||
for x in map:
|
||||
@@ -225,7 +227,7 @@ class automountlocation_import(Command):
|
||||
continue
|
||||
x = x.rstrip()
|
||||
if x.startswith('+'):
|
||||
result['skipped'].append([m, maps[m]])
|
||||
result['skipped'].append([m, filename])
|
||||
continue
|
||||
if len(x) == 0:
|
||||
continue
|
||||
|
||||
107
ipaclient/plugins/baseuser.py
Normal file
107
ipaclient/plugins/baseuser.py
Normal file
@@ -0,0 +1,107 @@
|
||||
#
|
||||
# Copyright (C) 2022 FreeIPA Contributors see COPYING for license
|
||||
#
|
||||
|
||||
import os
|
||||
import logging
|
||||
import subprocess
|
||||
from ipaclient.frontend import MethodOverride
|
||||
from ipalib import errors
|
||||
from ipalib import Bool, Flag, StrEnum
|
||||
from ipalib.text import _
|
||||
from ipaplatform.paths import paths
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class baseuser_add_passkey(MethodOverride):
|
||||
takes_options = (
|
||||
Flag(
|
||||
'register',
|
||||
cli_name='register',
|
||||
doc=_('Register the passkey'),
|
||||
),
|
||||
Bool(
|
||||
'require_user_verification?',
|
||||
cli_name='require_user_verification',
|
||||
doc=_('Require user verification during authentication with '
|
||||
'the passkey')
|
||||
),
|
||||
StrEnum(
|
||||
'cosetype?',
|
||||
cli_name='cose_type',
|
||||
doc=_('COSE type to use for registration'),
|
||||
values=('es256', 'rs256', 'eddsa'),
|
||||
),
|
||||
StrEnum(
|
||||
'credtype?',
|
||||
cli_name="cred_type",
|
||||
doc=_('Credential type'),
|
||||
values=('server-side', 'discoverable'),
|
||||
),
|
||||
)
|
||||
|
||||
def get_args(self):
|
||||
# ipapasskey is not mandatory as it can be built
|
||||
# from the registration step
|
||||
for arg in super(baseuser_add_passkey, self).get_args():
|
||||
if arg.name == 'ipapasskey':
|
||||
yield arg.clone(required=False, alwaysask=False)
|
||||
else:
|
||||
yield arg.clone()
|
||||
|
||||
def forward(self, *args, **options):
|
||||
if self.api.env.context == 'cli':
|
||||
# 2 formats are possible for ipa user-add-passkey:
|
||||
# --register [--require-user-verification] [--cose-type ...]
|
||||
# or
|
||||
# passkey:<key id>,<pub key>
|
||||
for option in super(baseuser_add_passkey, self).get_options():
|
||||
if args and option in options:
|
||||
raise errors.MutuallyExclusiveError(
|
||||
reason=_("cannot specify both %s and "
|
||||
"passkey mapping").format(option))
|
||||
# if the first format is used, need to register the key first
|
||||
# and obtained the data
|
||||
if 'register' in options:
|
||||
# Ensure the executable exists
|
||||
if not os.path.exists(paths.PASSKEY_CHILD):
|
||||
raise errors.ValidationError(name="register", error=_(
|
||||
"Missing executable %s, use the command with "
|
||||
"LOGIN PASSKEY instead of LOGIN --register")
|
||||
% paths.PASSKEY_CHILD)
|
||||
|
||||
options.pop('register')
|
||||
cosetype = options.pop('cosetype', None)
|
||||
require_verif = options.pop('require_user_verification', None)
|
||||
credtype = options.pop('credtype', None)
|
||||
cmd = [paths.PASSKEY_CHILD, "--register",
|
||||
"--domain", self.api.env.domain,
|
||||
"--username", args[0]]
|
||||
if cosetype:
|
||||
cmd.append("--type")
|
||||
cmd.append(cosetype)
|
||||
if require_verif is not None:
|
||||
cmd.append("--user-verification")
|
||||
cmd.append(str(require_verif).lower())
|
||||
if credtype:
|
||||
cmd.append("--cred-type")
|
||||
cmd.append(credtype)
|
||||
|
||||
logger.debug("Executing command: %s", cmd)
|
||||
passkey = None
|
||||
with subprocess.Popen(cmd, stdout=subprocess.PIPE,
|
||||
bufsize=1,
|
||||
universal_newlines=True) as subp:
|
||||
for line in subp.stdout:
|
||||
if line.startswith("passkey:"):
|
||||
passkey = line.strip()
|
||||
else:
|
||||
print(line.strip())
|
||||
|
||||
if subp.returncode != 0:
|
||||
raise errors.NotFound(reason="Failed to generate passkey")
|
||||
|
||||
args = (args[0], [passkey])
|
||||
|
||||
return super(baseuser_add_passkey, self).forward(*args, **options)
|
||||
@@ -26,15 +26,16 @@ class WithCertOutArgs(MethodOverride):
|
||||
filename = None
|
||||
if 'certificate_out' in options:
|
||||
filename = options.pop('certificate_out')
|
||||
|
||||
result = super(WithCertOutArgs, self).forward(*keys, **options)
|
||||
|
||||
if filename:
|
||||
try:
|
||||
util.check_writable_file(filename)
|
||||
except errors.FileError as e:
|
||||
raise errors.ValidationError(name='certificate-out',
|
||||
error=str(e))
|
||||
|
||||
result = super(WithCertOutArgs, self).forward(*keys, **options)
|
||||
|
||||
if filename:
|
||||
# if result certificate / certificate_chain not present in result,
|
||||
# it means Dogtag did not provide it (probably due to LWCA key
|
||||
# replication lag or failure. The server transmits a warning
|
||||
|
||||
@@ -21,8 +21,6 @@
|
||||
|
||||
import base64
|
||||
|
||||
import six
|
||||
|
||||
from ipaclient.frontend import MethodOverride
|
||||
from ipalib import errors
|
||||
from ipalib import x509
|
||||
@@ -31,9 +29,6 @@ from ipalib.parameters import BinaryFile, File, Flag, Str
|
||||
from ipalib.plugable import Registry
|
||||
from ipalib.text import _
|
||||
|
||||
if six.PY3:
|
||||
unicode = str
|
||||
|
||||
register = Registry()
|
||||
|
||||
|
||||
@@ -48,112 +43,36 @@ class CertRetrieveOverride(MethodOverride):
|
||||
)
|
||||
|
||||
def forward(self, *args, **options):
|
||||
filename = None
|
||||
if 'certificate_out' in options:
|
||||
certificate_out = options.pop('certificate_out')
|
||||
try:
|
||||
util.check_writable_file(certificate_out)
|
||||
except errors.FileError as e:
|
||||
raise errors.ValidationError(name='certificate-out',
|
||||
error=str(e))
|
||||
else:
|
||||
certificate_out = None
|
||||
filename = options.pop('certificate_out')
|
||||
|
||||
result = super(CertRetrieveOverride, self).forward(*args, **options)
|
||||
|
||||
if certificate_out is not None:
|
||||
if filename is not None:
|
||||
try:
|
||||
util.check_writable_file(filename)
|
||||
except errors.FileError as e:
|
||||
raise errors.ValidationError(name='certificate-out',
|
||||
error=str(e))
|
||||
if options.get('chain', False):
|
||||
certs = result['result']['certificate_chain']
|
||||
else:
|
||||
certs = [base64.b64decode(result['result']['certificate'])]
|
||||
certs = (x509.load_der_x509_certificate(cert) for cert in certs)
|
||||
x509.write_certificate_list(certs, certificate_out)
|
||||
x509.write_certificate_list(certs, filename)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@register(override=True, no_fail=True)
|
||||
class cert_request(CertRetrieveOverride):
|
||||
takes_options = CertRetrieveOverride.takes_options + (
|
||||
Str(
|
||||
'database?',
|
||||
label=_('Path to NSS database'),
|
||||
doc=_('Path to NSS database to use for private key'),
|
||||
),
|
||||
Str(
|
||||
'private_key?',
|
||||
label=_('Path to private key file'),
|
||||
doc=_('Path to PEM file containing a private key'),
|
||||
),
|
||||
Str(
|
||||
'password_file?',
|
||||
label=_(
|
||||
'File containing a password for the private key or database'),
|
||||
),
|
||||
Str(
|
||||
'csr_profile_id?',
|
||||
label=_('Name of CSR generation profile (if not the same as'
|
||||
' profile_id)'),
|
||||
),
|
||||
)
|
||||
|
||||
def get_args(self):
|
||||
for arg in super(cert_request, self).get_args():
|
||||
if arg.name == 'csr':
|
||||
arg = arg.clone_retype(arg.name, File, required=False)
|
||||
yield arg
|
||||
|
||||
def forward(self, csr=None, **options):
|
||||
database = options.pop('database', None)
|
||||
private_key = options.pop('private_key', None)
|
||||
csr_profile_id = options.pop('csr_profile_id', None)
|
||||
password_file = options.pop('password_file', None)
|
||||
|
||||
if csr is None:
|
||||
# Deferred import, ipaclient.csrgen is expensive to load.
|
||||
# see https://pagure.io/freeipa/issue/7484
|
||||
from ipaclient import csrgen
|
||||
|
||||
if database:
|
||||
adaptor = csrgen.NSSAdaptor(database, password_file)
|
||||
elif private_key:
|
||||
adaptor = csrgen.OpenSSLAdaptor(
|
||||
key_filename=private_key, password_filename=password_file)
|
||||
else:
|
||||
raise errors.InvocationError(
|
||||
message=u"One of 'database' or 'private_key' is required")
|
||||
|
||||
pubkey_info = adaptor.get_subject_public_key_info()
|
||||
pubkey_info_b64 = base64.b64encode(pubkey_info)
|
||||
|
||||
# If csr_profile_id is passed, that takes precedence.
|
||||
# Otherwise, use profile_id. If neither are passed, the default
|
||||
# in cert_get_requestdata will be used.
|
||||
profile_id = csr_profile_id
|
||||
if profile_id is None:
|
||||
profile_id = options.get('profile_id')
|
||||
|
||||
response = self.api.Command.cert_get_requestdata(
|
||||
profile_id=profile_id,
|
||||
principal=options.get('principal'),
|
||||
public_key_info=pubkey_info_b64)
|
||||
|
||||
req_info_b64 = response['result']['request_info']
|
||||
req_info = base64.b64decode(req_info_b64)
|
||||
|
||||
csr = adaptor.sign_csr(req_info)
|
||||
|
||||
if not csr:
|
||||
raise errors.CertificateOperationError(
|
||||
error=(_('Generated CSR was empty')))
|
||||
|
||||
else:
|
||||
if database is not None or private_key is not None:
|
||||
raise errors.MutuallyExclusiveError(reason=_(
|
||||
"Options 'database' and 'private_key' are not compatible"
|
||||
" with 'csr'"))
|
||||
|
||||
return super(cert_request, self).forward(csr, **options)
|
||||
|
||||
|
||||
@register(override=True, no_fail=True)
|
||||
class cert_show(CertRetrieveOverride):
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
#
|
||||
# Copyright (C) 2016 FreeIPA Contributors see COPYING for license
|
||||
#
|
||||
|
||||
import base64
|
||||
|
||||
import six
|
||||
|
||||
from ipalib import api
|
||||
from ipalib import errors
|
||||
from ipalib import output
|
||||
from ipalib import util
|
||||
from ipalib.frontend import Local, Str
|
||||
from ipalib.parameters import Bytes, Principal
|
||||
from ipalib.plugable import Registry
|
||||
from ipalib.text import _
|
||||
from ipapython import dogtag
|
||||
|
||||
|
||||
if six.PY3:
|
||||
unicode = str
|
||||
|
||||
register = Registry()
|
||||
|
||||
__doc__ = _("""
|
||||
Commands to build certificate requests automatically
|
||||
""")
|
||||
|
||||
|
||||
@register()
|
||||
class cert_get_requestdata(Local):
|
||||
__doc__ = _('Gather data for a certificate signing request.')
|
||||
|
||||
NO_CLI = True
|
||||
|
||||
takes_options = (
|
||||
Principal(
|
||||
'principal',
|
||||
label=_('Principal'),
|
||||
doc=_('Principal for this certificate (e.g.'
|
||||
' HTTP/test.example.com)'),
|
||||
),
|
||||
Str(
|
||||
'profile_id?',
|
||||
label=_('Profile ID'),
|
||||
doc=_('CSR Generation Profile to use'),
|
||||
),
|
||||
Bytes(
|
||||
'public_key_info',
|
||||
label=_('Subject Public Key Info'),
|
||||
doc=_('DER-encoded SubjectPublicKeyInfo structure'),
|
||||
),
|
||||
Str(
|
||||
'out?',
|
||||
doc=_('Write CertificationRequestInfo to file'),
|
||||
),
|
||||
)
|
||||
|
||||
has_output = (
|
||||
output.Output(
|
||||
'result',
|
||||
type=dict,
|
||||
doc=_('Dictionary mapping variable name to value'),
|
||||
),
|
||||
)
|
||||
|
||||
has_output_params = (
|
||||
Str(
|
||||
'request_info',
|
||||
label=_('CertificationRequestInfo structure'),
|
||||
)
|
||||
)
|
||||
|
||||
def execute(self, *args, **options):
|
||||
# Deferred import, ipaclient.csrgen is expensive to load.
|
||||
# see https://pagure.io/freeipa/issue/7484
|
||||
from ipaclient import csrgen
|
||||
from ipaclient import csrgen_ffi
|
||||
|
||||
if 'out' in options:
|
||||
util.check_writable_file(options['out'])
|
||||
|
||||
principal = options.get('principal')
|
||||
profile_id = options.get('profile_id')
|
||||
if profile_id is None:
|
||||
profile_id = dogtag.DEFAULT_PROFILE
|
||||
public_key_info = options.get('public_key_info')
|
||||
public_key_info = base64.b64decode(public_key_info)
|
||||
|
||||
if self.api.env.in_server:
|
||||
backend = self.api.Backend.ldap2
|
||||
else:
|
||||
backend = self.api.Backend.rpcclient
|
||||
if not backend.isconnected():
|
||||
backend.connect()
|
||||
|
||||
try:
|
||||
if principal.is_host:
|
||||
principal_obj = api.Command.host_show(
|
||||
principal.hostname, all=True)
|
||||
elif principal.is_service:
|
||||
principal_obj = api.Command.service_show(
|
||||
unicode(principal), all=True)
|
||||
elif principal.is_user:
|
||||
principal_obj = api.Command.user_show(
|
||||
principal.username, all=True)
|
||||
except errors.NotFound:
|
||||
raise errors.NotFound(
|
||||
reason=_("The principal for this request doesn't exist."))
|
||||
principal_obj = principal_obj['result']
|
||||
config = api.Command.config_show()['result']
|
||||
|
||||
generator = csrgen.CSRGenerator(csrgen.FileRuleProvider())
|
||||
|
||||
csr_config = generator.csr_config(principal_obj, config, profile_id)
|
||||
request_info = base64.b64encode(csrgen_ffi.build_requestinfo(
|
||||
csr_config.encode('utf8'), public_key_info))
|
||||
|
||||
result = {}
|
||||
if 'out' in options:
|
||||
with open(options['out'], 'wb') as f:
|
||||
f.write(request_info)
|
||||
else:
|
||||
result = dict(request_info=request_info)
|
||||
|
||||
return dict(
|
||||
result=result
|
||||
)
|
||||
@@ -38,6 +38,8 @@ class hbactest(CommandOverride):
|
||||
# Note that we don't actually use --detail below to see if details need
|
||||
# to be printed as our execute() method will return None for corresponding
|
||||
# entries and None entries will be skipped.
|
||||
self.log_messages(output)
|
||||
|
||||
for o in self.output:
|
||||
if o == 'value':
|
||||
continue
|
||||
|
||||
@@ -22,6 +22,7 @@ import sys
|
||||
|
||||
from ipaclient.frontend import MethodOverride
|
||||
from ipalib import api, Str, Password, _
|
||||
from ipalib import errors
|
||||
from ipalib.messages import add_message, ResultFormattingError
|
||||
from ipalib.plugable import Registry
|
||||
from ipalib.frontend import Local
|
||||
@@ -129,7 +130,6 @@ class HTTPSHandler(urllib.request.HTTPSHandler):
|
||||
return create_https_connection(host, **tmp)
|
||||
|
||||
def https_open(self, req):
|
||||
# pylint: disable=no-member
|
||||
return self.do_open(self.__inner, req)
|
||||
|
||||
@register()
|
||||
@@ -156,8 +156,6 @@ class otptoken_sync(Local):
|
||||
segments = list(urllib.parse.urlparse(self.api.env.xmlrpc_uri))
|
||||
assert segments[0] == 'https' # Ensure encryption.
|
||||
segments[2] = segments[2].replace('/xml', '/session/sync_token')
|
||||
# urlunparse *can* take one argument
|
||||
# pylint: disable=too-many-function-args
|
||||
sync_uri = urllib.parse.urlunparse(segments)
|
||||
|
||||
# Prepare the query.
|
||||
@@ -170,7 +168,6 @@ class otptoken_sync(Local):
|
||||
query = query.encode('utf-8')
|
||||
|
||||
# Sync the token.
|
||||
# pylint: disable=E1101
|
||||
handler = HTTPSHandler(
|
||||
cafile=api.env.tls_ca_cert,
|
||||
tls_version_min=api.env.tls_version_min,
|
||||
@@ -180,11 +177,13 @@ class otptoken_sync(Local):
|
||||
status['result'][self.header] = rsp.info().get(self.header, 'unknown')
|
||||
rsp.close()
|
||||
|
||||
if status['result'][self.header] != "ok":
|
||||
msg = {'error': 'Error contacting server!',
|
||||
'invalid-credentials': 'Invalid Credentials!',
|
||||
}.get(status['result'][self.header], 'Unknown Error!')
|
||||
raise errors.ExecutionError(
|
||||
message=_("Unable to synchronize token: %s") % msg)
|
||||
return status
|
||||
|
||||
def output_for_cli(self, textui, result, *keys, **options):
|
||||
textui.print_plain({
|
||||
'ok': 'Token synchronized.',
|
||||
'error': 'Error contacting server!',
|
||||
'invalid-credentials': 'Invalid Credentials!',
|
||||
}.get(result['result'][self.header], 'Unknown Error!'))
|
||||
textui.print_plain('Token synchronized.')
|
||||
|
||||
14
ipaclient/plugins/stageuser.py
Normal file
14
ipaclient/plugins/stageuser.py
Normal file
@@ -0,0 +1,14 @@
|
||||
#
|
||||
# Copyright (C) 2022 FreeIPA Contributors see COPYING for license
|
||||
#
|
||||
from ipaclient.plugins.baseuser import baseuser_add_passkey
|
||||
from ipalib.plugable import Registry
|
||||
from ipalib import _
|
||||
|
||||
|
||||
register = Registry()
|
||||
|
||||
|
||||
@register(override=True, no_fail=True)
|
||||
class stageuser_add_passkey(baseuser_add_passkey):
|
||||
__doc__ = _("Add one or more passkey mappings to the user entry.")
|
||||
@@ -39,9 +39,11 @@ class sudorule_disable(MethodOverride):
|
||||
@register(override=True, no_fail=True)
|
||||
class sudorule_add_option(MethodOverride):
|
||||
def output_for_cli(self, textui, result, cn, **options):
|
||||
opts = self.normalize(**options)
|
||||
textui.print_dashed(
|
||||
_('Added option "%(option)s" to Sudo Rule "%(rule)s"')
|
||||
% dict(option=options['ipasudoopt'], rule=cn))
|
||||
% dict(option=','.join(opts['ipasudoopt']), rule=cn)
|
||||
)
|
||||
|
||||
super(sudorule_add_option, self).output_for_cli(textui, result, cn,
|
||||
**options)
|
||||
@@ -50,8 +52,10 @@ class sudorule_add_option(MethodOverride):
|
||||
@register(override=True, no_fail=True)
|
||||
class sudorule_remove_option(MethodOverride):
|
||||
def output_for_cli(self, textui, result, cn, **options):
|
||||
opts = self.normalize(**options)
|
||||
textui.print_dashed(
|
||||
_('Removed option "%(option)s" from Sudo Rule "%(rule)s"')
|
||||
% dict(option=options['ipasudoopt'], rule=cn))
|
||||
% dict(option=','.join(opts['ipasudoopt']), rule=cn)
|
||||
)
|
||||
super(sudorule_remove_option, self).output_for_cli(textui, result, cn,
|
||||
**options)
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from ipaclient.frontend import MethodOverride
|
||||
from ipaclient.plugins.baseuser import baseuser_add_passkey
|
||||
from ipalib import errors
|
||||
from ipalib import Flag
|
||||
from ipalib import util
|
||||
@@ -79,3 +80,8 @@ class user_show(MethodOverride):
|
||||
raise errors.NoCertificateError(entry=keys[-1])
|
||||
else:
|
||||
return super(user_show, self).forward(*keys, **options)
|
||||
|
||||
|
||||
@register(override=True, no_fail=True)
|
||||
class user_add_passkey(baseuser_add_passkey):
|
||||
__doc__ = _("Add one or more passkey mappings to the user entry.")
|
||||
|
||||
@@ -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
|
||||
@@ -118,8 +119,8 @@ def encrypt(data, symmetric_key=None, public_key=None):
|
||||
return public_key_obj.encrypt(
|
||||
data,
|
||||
padding.OAEP(
|
||||
mgf=padding.MGF1(algorithm=hashes.SHA1()),
|
||||
algorithm=hashes.SHA1(),
|
||||
mgf=padding.MGF1(algorithm=hashes.SHA256()),
|
||||
algorithm=hashes.SHA256(),
|
||||
label=None
|
||||
)
|
||||
)
|
||||
@@ -153,8 +154,8 @@ def decrypt(data, symmetric_key=None, private_key=None):
|
||||
return private_key_obj.decrypt(
|
||||
data,
|
||||
padding.OAEP(
|
||||
mgf=padding.MGF1(algorithm=hashes.SHA1()),
|
||||
algorithm=hashes.SHA1(),
|
||||
mgf=padding.MGF1(algorithm=hashes.SHA256()),
|
||||
algorithm=hashes.SHA256(),
|
||||
label=None
|
||||
)
|
||||
)
|
||||
@@ -546,41 +547,47 @@ class vault_mod(Local):
|
||||
return response
|
||||
|
||||
|
||||
class _TransportCertCache:
|
||||
def __init__(self):
|
||||
self._dirname = os.path.join(
|
||||
USER_CACHE_PATH, 'ipa', 'kra-transport-certs'
|
||||
)
|
||||
class _KraConfigCache:
|
||||
"""The KRA config cache stores vaultconfig-show result.
|
||||
"""
|
||||
def __init__(self, api):
|
||||
self._dirname = os.path.join(api.env.cache_dir, '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)
|
||||
@@ -588,9 +595,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)
|
||||
@@ -603,8 +610,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
|
||||
@@ -620,7 +627,7 @@ class _TransportCertCache:
|
||||
return True
|
||||
|
||||
|
||||
_transport_cert_cache = _TransportCertCache()
|
||||
_kra_config_cache = _KraConfigCache(api)
|
||||
|
||||
|
||||
@register(override=True, no_fail=True)
|
||||
@@ -635,13 +642,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:
|
||||
@@ -651,20 +653,89 @@ 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_3DES
|
||||
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):
|
||||
use_oaep=False, *args, **options):
|
||||
public_key = transport_cert.public_key()
|
||||
|
||||
# wrap session key with transport certificate
|
||||
wrapped_session_key = public_key.encrypt(
|
||||
algo.key,
|
||||
padding.PKCS1v15()
|
||||
)
|
||||
# KRA may be configured using either the default PKCS1v15 or RSA-OAEP.
|
||||
# there is no way to query this info using the REST interface.
|
||||
if not use_oaep:
|
||||
# PKCS1v15() causes an OpenSSL exception when FIPS is enabled
|
||||
# if so, we fallback to RSA-OAEP
|
||||
try:
|
||||
wrapped_session_key = public_key.encrypt(
|
||||
algo.key,
|
||||
padding.PKCS1v15()
|
||||
)
|
||||
except ValueError:
|
||||
wrapped_session_key = public_key.encrypt(
|
||||
algo.key,
|
||||
padding.OAEP(
|
||||
mgf=padding.MGF1(algorithm=hashes.SHA256()),
|
||||
algorithm=hashes.SHA256(),
|
||||
label=None
|
||||
)
|
||||
)
|
||||
else:
|
||||
wrapped_session_key = public_key.encrypt(
|
||||
algo.key,
|
||||
padding.OAEP(
|
||||
mgf=padding.MGF1(algorithm=hashes.SHA256()),
|
||||
algorithm=hashes.SHA256(),
|
||||
label=None
|
||||
)
|
||||
)
|
||||
|
||||
options['session_key'] = wrapped_session_key
|
||||
|
||||
name = self.name + '_internal'
|
||||
@@ -674,31 +745,38 @@ class ModVaultData(Local):
|
||||
except (errors.InternalError,
|
||||
errors.ExecutionError,
|
||||
errors.GenericError):
|
||||
_transport_cert_cache.remove_cert(self.api.env.domain)
|
||||
if raise_unexpected:
|
||||
_kra_config_cache.remove(self.api.env.domain)
|
||||
if raise_unexpected and use_oaep:
|
||||
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:
|
||||
try:
|
||||
result = self._do_internal(algo, transport_cert, False,
|
||||
*args, **options)
|
||||
if result is not None:
|
||||
return result
|
||||
False, *args, **options)
|
||||
except errors.EncodingError:
|
||||
result = self._do_internal(algo, transport_cert, False,
|
||||
True, *args, **options)
|
||||
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
|
||||
result = self._do_internal(algo, transport_cert, True,
|
||||
False, *args, **options)
|
||||
|
||||
if result is not None:
|
||||
return result
|
||||
|
||||
# call and use_oaep this time, last attempt
|
||||
return self._do_internal(algo, transport_cert, True,
|
||||
*args, **options)
|
||||
True, *args, **options)
|
||||
|
||||
|
||||
@register(no_fail=True)
|
||||
@@ -758,7 +836,8 @@ class vault_archive(ModVaultData):
|
||||
if option.name not in ('nonce',
|
||||
'session_key',
|
||||
'vault_data',
|
||||
'version'):
|
||||
'version',
|
||||
'wrapping_algo'):
|
||||
yield option
|
||||
for option in super(vault_archive, self).get_options():
|
||||
yield option
|
||||
@@ -775,7 +854,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:
|
||||
"""
|
||||
@@ -927,15 +1006,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)
|
||||
@@ -1001,7 +1089,7 @@ class vault_retrieve(ModVaultData):
|
||||
|
||||
def get_options(self):
|
||||
for option in self.api.Command.vault_retrieve_internal.options():
|
||||
if option.name not in ('session_key', 'version'):
|
||||
if option.name not in ('session_key', 'version', 'wrapping_algo'):
|
||||
yield option
|
||||
for option in super(vault_retrieve, self).get_options():
|
||||
yield option
|
||||
@@ -1059,10 +1147,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,
|
||||
|
||||
Reference in New Issue
Block a user