Dne 3.6.2015 v 14:17 Jan Cholasta napsal(a):
Dne 2.6.2015 v 02:00 Endi Sukma Dewata napsal(a):
Please take a look at the updated patch.

On 5/27/2015 12:39 AM, Jan Cholasta wrote:
21) vault_archive is not a retrieve operation, it should be
based on
LDAPUpdate instead of LDAPRetrieve. Or Command actually, since it
does
not do anything with LDAP. The same applies to vault_retrieve.

The vault_archive does not actually modify the LDAP entry
because it
stores the data in KRA. It is actually an LDAPRetrieve operation
because
it needs to get the vault info before it can perform the archival
operation. Same thing with vault_retrieve.

It is not a LDAPRetrieve operation, because it has different
semantics.
Please use Command as base class and either use ldap2 for direct
LDAP or
call vault_show instead of hacking around LDAPRetrieve.

It's been changed to inherit from LDAPQuery instead.

NACK, it's not a LDAPQuery operation, because it has different
semantics. There is more to a command than executing code, so you
should
use a correct base class.

Changed to inherit from Command as requested. Now these commands no
longer have a direct access to the vault object (self.obj) although
they
are accessing vault objects like other vault commands. Also now the
vault name argument has to be added explicitly on each command.

You can inherit from crud.Retrieve and crud.Update to get self.obj and
the argument back.

I tried this:

   class vault_retrieve(Command, crud.Retrieve):

and it gave me an error:

   TypeError: Error when calling the metaclass bases
       Cannot create a consistent method resolution
   order (MRO) for bases Retrieve, Command

I'm sticking with the original code since it works fine although not
ideal. I'm not a Python expert, so if you know how to fix this properly
please feel free to post a patch on top of this.

The class hierarchy is as follows:

    frontend.Command
        frontend.Method
            crud.PKQuery
                crud.Retrieve
                cdur.Update

So removing Command from the list of base classes should fix it.


If KRA is not installed, vault-archive and vault-retrieve fail with
internal error.

Added a code to check KRA installation in all vault commands. If you
know a way not to load the vault plugin if the KRA is not installed
please let me know, that's probably even better. Not sure how that will
work on the client side though.

I see this has been already resolved in the other thread.


The commands still behave differently based on whether they were called
from API which was initialized with in_server set to True or False.

That is unfortunately a restriction imposed by the framework. In order
to guarantee the security, the vault is designed to have separate client
and server code. The client code encrypts the secret, the server code
forwards the encrypted secret to KRA. To archive a secret into a vault
properly, you are supposed to call the client code. If you're calling
the server code directly, you are responsible to do your own encryption
(i.e. generating session key, nonce, and vault data).

I understand why the code has to be separated, what I don't understand
is why it is in fact *not* separated and crammed into a single command,
making weird and undefined behavior possible.


If another plugin wants to use vault, it should implement a client code
which calls the vault client code to perform the archival from the
client side.

What is the use case for calling the vault API from the server side
anyway? Wouldn't that defeat the purpose of having a vault? If a secret
exists on the server side in an unencrypted form doesn't it mean the
secret may already have been compromised?

Server API is used not only by the server itself, but also by installers
for example. Anyway the point is that there *can't* be a broken API like
this, you should at least raise an error if the command is called from
server API, although actually separating it into client and server parts
would be preferable.


There is no point in exposing the session_key, nonce and vault_data
options in CLI when their value is always overwritten in forward().

I agree there is no need to expose them in CLI, but in this framework
the API also defines the CLI. If there's a way to keep them in the
server API but not expose them in the CLI please let me know. Or, if
there's a way to define completely separate server API (without a
matching client CLI) and client CLI (without a matching server API) that
will work too.

As I suggested above, you can split the commands into separate client
and server commands. The client command should inherit from
frontend.Local so that it is always executed locally and the server
command should have a "NO_CLI = True" attribute so that it is not
available in the CLI.


Will this always succeed?

+        # deactivate vault record in KRA
+        response = kra_client.keys.list_keys(
+            client_key_id, pki.key.KeyClient.KEY_STATUS_ACTIVE)

Yes. If there's no active keys it will return an empty collection.

+        for key_info in response.key_infos:
+            kra_client.keys.modify_key_status(
+                key_info.get_key_id(),
+                pki.key.KeyClient.KEY_STATUS_INACTIVE)

This loop will do nothing given an empty collection.

If not, we might get into an inconsistent state, where the vault is
deleted in LDAP but still active in KRA. (I'm not sure if this is
actually a problem or not.)

That can only happen if the server crashes after deleting the vault but
before deactivating the key. Regardless, it will not be a problem
because the key is identified by vault ID/path so it will not conflict
with other vaults, and it will get overwritten if the same vault is
recreated again.

OK.


Attached is a patch including the requested changes.

I have also changed vault_config to vaultconfig_show, for consistency with {,dns}config_show (it also makes the transport certificate retrieval code in vault_{archive,retrieve} simpler).

I have noticed that triple-length DES is used for the session key. Wouldn't AES be better?

        # generate session key
        mechanism = nss.CKM_DES3_CBC_PAD

BTW, ipa-kra-install is broken with pki-core-10.2.4-1, but it works with pki-core-10.2.1-3.

--
Jan Cholasta
>From a2b988c458402aa198f08e3d7fadb1e54840f5ef Mon Sep 17 00:00:00 2001
From: "Endi S. Dewata" <edew...@redhat.com>
Date: Fri, 5 Jun 2015 08:49:39 +0000
Subject: [PATCH] Added vault-archive and vault-retrieve commands.

New commands have been added to archive and retrieve
data into and from a vault, also to retrieve the
transport certificate.

https://fedorahosted.org/freeipa/ticket/3872
---
 API.txt                                   |  65 ++++
 VERSION                                   |   4 +-
 ipalib/plugins/vault.py                   | 488 +++++++++++++++++++++++++++++-
 ipatests/test_xmlrpc/test_vault_plugin.py |  72 ++++-
 make-lint                                 |   1 +
 5 files changed, 626 insertions(+), 4 deletions(-)

diff --git a/API.txt b/API.txt
index 6520f2c..e2841ef 100644
--- a/API.txt
+++ b/API.txt
@@ -4921,6 +4921,36 @@ option: Str('version?', exclude='webui')
 output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
 output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
 output: PrimaryKey('value', None, None)
+command: vault_archive
+args: 1,8,3
+arg: Str('cn', attribute=True, cli_name='name', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.-]+$', primary_key=True, query=True, required=True)
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Bytes('data?')
+option: Str('in?')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Str('service?')
+option: Flag('shared?', autofill=True, default=False)
+option: Str('user?')
+option: Str('version?', exclude='webui')
+output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
+output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
+output: PrimaryKey('value', None, None)
+command: vault_archive_encrypted
+args: 1,10,3
+arg: Str('cn', attribute=True, cli_name='name', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.-]+$', primary_key=True, query=True, required=True)
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Str('description', attribute=True, autofill=False, cli_name='desc', multivalue=False, required=False)
+option: Bytes('nonce')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Str('service?')
+option: Bytes('session_key')
+option: Flag('shared?', autofill=True, default=False)
+option: Str('user?')
+option: Bytes('vault_data')
+option: Str('version?', exclude='webui')
+output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
+output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
+output: PrimaryKey('value', None, None)
 command: vault_del
 args: 1,5,3
 arg: Str('cn', attribute=True, cli_name='name', maxlength=255, multivalue=True, pattern='^[a-zA-Z0-9_.-]+$', primary_key=True, query=True, required=True)
@@ -4967,6 +4997,32 @@ option: Str('version?', exclude='webui')
 output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
 output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
 output: PrimaryKey('value', None, None)
+command: vault_retrieve
+args: 1,7,3
+arg: Str('cn', attribute=True, cli_name='name', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.-]+$', primary_key=True, query=True, required=True)
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Str('out?')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Str('service?')
+option: Flag('shared?', autofill=True, default=False)
+option: Str('user?')
+option: Str('version?', exclude='webui')
+output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
+output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
+output: PrimaryKey('value', None, None)
+command: vault_retrieve_encrypted
+args: 1,7,3
+arg: Str('cn', attribute=True, cli_name='name', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.-]+$', primary_key=True, query=True, required=True)
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Str('service?')
+option: Bytes('session_key')
+option: Flag('shared?', autofill=True, default=False)
+option: Str('user?')
+option: Str('version?', exclude='webui')
+output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
+output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
+output: PrimaryKey('value', None, None)
 command: vault_show
 args: 1,7,3
 arg: Str('cn', attribute=True, cli_name='name', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.-]+$', primary_key=True, query=True, required=True)
@@ -4980,6 +5036,15 @@ option: Str('version?', exclude='webui')
 output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
 output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
 output: PrimaryKey('value', None, None)
+command: vaultconfig_show
+args: 0,4,3
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Str('transport_out?')
+option: Str('version?', exclude='webui')
+output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
+output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
+output: PrimaryKey('value', None, None)
 capability: messages 2.52
 capability: optional_uid_params 2.54
 capability: permissions2 2.69
diff --git a/VERSION b/VERSION
index 2ad3827..60e04f6 100644
--- a/VERSION
+++ b/VERSION
@@ -90,5 +90,5 @@ IPA_DATA_VERSION=20100614120000
 #                                                      #
 ########################################################
 IPA_API_VERSION_MAJOR=2
-IPA_API_VERSION_MINOR=123
-# Last change: rcritten - added service constraint delegation plugin
+IPA_API_VERSION_MINOR=124
+# Last change: edewata - added vault-archive and vault-retrieve
diff --git a/ipalib/plugins/vault.py b/ipalib/plugins/vault.py
index ebb9f9f..fc37e0d 100644
--- a/ipalib/plugins/vault.py
+++ b/ipalib/plugins/vault.py
@@ -17,16 +17,32 @@
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
+import base64
+import json
+import os
+import sys
+import tempfile
+
+import nss.nss as nss
+
+from ipalib.frontend import Command, Object
 from ipalib import api, errors
-from ipalib import Str, Flag
+from ipalib import Bytes, Str, Flag
 from ipalib import output
+from ipalib.crud import PKQuery, Retrieve, Update
 from ipalib.plugable import Registry
 from ipalib.plugins.baseldap import LDAPObject, LDAPCreate, LDAPDelete,\
     LDAPSearch, LDAPUpdate, LDAPRetrieve
 from ipalib.request import context
 from ipalib.plugins.user import split_principal
 from ipalib import _, ngettext
+from ipaplatform.paths import paths
 from ipapython.dn import DN
+from ipapython.nsslib import current_dbdir
+
+if api.env.in_server:
+    import pki.account
+    import pki.key
 
 __doc__ = _("""
 Vaults
@@ -94,6 +110,33 @@ EXAMPLES:
 """) + _("""
  Delete a user vault:
    ipa vault-del <name> --user <username>
+""") + _("""
+ Display vault configuration:
+   ipa vault-config
+""") + _("""
+ Archive data into private vault:
+   ipa vault-archive <name> --in <input file>
+""") + _("""
+ Archive data into service vault:
+   ipa vault-archive <name> --service <service name> --in <input file>
+""") + _("""
+ Archive data into shared vault:
+   ipa vault-archive <name> --shared --in <input file>
+""") + _("""
+ Archive data into user vault:
+   ipa vault-archive <name> --user <username> --in <input file>
+""") + _("""
+ Retrieve data from private vault:
+   ipa vault-retrieve <name> --out <output file>
+""") + _("""
+ Retrieve data from service vault:
+   ipa vault-retrieve <name> --service <service name> --out <output file>
+""") + _("""
+ Retrieve data from shared vault:
+   ipa vault-retrieve <name> --shared --out <output file>
+""") + _("""
+ Retrieve data from user vault:
+   ipa vault-retrieve <name> --user <user name> --out <output file>
 """)
 
 register = Registry()
@@ -243,6 +286,26 @@ class vault(LDAPObject):
         for entry in entries:
             self.backend.add_entry(entry)
 
+    def get_key_id(self, dn):
+        """
+        Generates a client key ID to archive/retrieve data in KRA.
+        """
+
+        # TODO: create container_dn after object initialization then reuse it
+        container_dn = DN(self.container_dn, self.api.env.basedn)
+
+        # make sure the DN is a vault DN
+        if not dn.endswith(container_dn, 1):
+            raise ValueError('Invalid vault DN: %s' % dn)
+
+        # construct the vault ID from the bottom up
+        id = u''
+        for rdn in dn[:-len(container_dn)]:
+            name = rdn['cn']
+            id = u'/' + name + id
+
+        return 'ipa:' + id
+
 
 @register()
 class vault_add(LDAPCreate):
@@ -256,6 +319,10 @@ class vault_add(LDAPCreate):
                      **options):
         assert isinstance(dn, DN)
 
+        if not self.api.env.enable_kra:
+            raise errors.InvocationError(
+                format=_('KRA service is not enabled'))
+
         try:
             parent_dn = DN(*dn[1:])
             self.obj.create_container(parent_dn)
@@ -273,6 +340,38 @@ class vault_del(LDAPDelete):
 
     msg_summary = _('Deleted vault "%(value)s"')
 
+    def pre_callback(self, ldap, dn, *keys, **options):
+        assert isinstance(dn, DN)
+
+        if not self.api.env.enable_kra:
+            raise errors.InvocationError(
+                format=_('KRA service is not enabled'))
+
+        return dn
+
+    def post_callback(self, ldap, dn, *args, **options):
+        assert isinstance(dn, DN)
+
+        kra_client = self.api.Backend.kra.get_client()
+
+        kra_account = pki.account.AccountClient(kra_client.connection)
+        kra_account.login()
+
+        client_key_id = self.obj.get_key_id(dn)
+
+        # deactivate vault record in KRA
+        response = kra_client.keys.list_keys(
+            client_key_id, pki.key.KeyClient.KEY_STATUS_ACTIVE)
+
+        for key_info in response.key_infos:
+            kra_client.keys.modify_key_status(
+                key_info.get_key_id(),
+                pki.key.KeyClient.KEY_STATUS_INACTIVE)
+
+        kra_account.logout()
+
+        return True
+
 
 @register()
 class vault_find(LDAPSearch):
@@ -290,6 +389,10 @@ class vault_find(LDAPSearch):
                      **options):
         assert isinstance(base_dn, DN)
 
+        if not self.api.env.enable_kra:
+            raise errors.InvocationError(
+                format=_('KRA service is not enabled'))
+
         base_dn = self.obj.get_dn(*args, **options)
 
         return (filter, base_dn, scope)
@@ -313,9 +416,392 @@ class vault_mod(LDAPUpdate):
 
     msg_summary = _('Modified vault "%(value)s"')
 
+    def pre_callback(self, ldap, dn, entry_attrs, attrs_list,
+                     *keys, **options):
+
+        assert isinstance(dn, DN)
+
+        if not self.api.env.enable_kra:
+            raise errors.InvocationError(
+                format=_('KRA service is not enabled'))
+
+        return dn
+
 
 @register()
 class vault_show(LDAPRetrieve):
     __doc__ = _('Display information about a vault.')
 
     takes_options = LDAPRetrieve.takes_options + vault_options
+
+    def pre_callback(self, ldap, dn, attrs_list, *keys, **options):
+        assert isinstance(dn, DN)
+
+        if not self.api.env.enable_kra:
+            raise errors.InvocationError(
+                format=_('KRA service is not enabled'))
+
+        return dn
+
+
+@register()
+class vaultconfig(Object):
+    __doc__ = _('Vault configuration')
+
+    takes_params = (
+        Bytes(
+            'transport_cert',
+            label=_('Transport Certificate'),
+        ),
+    )
+
+
+@register()
+class vaultconfig_show(Retrieve):
+    __doc__ = _('Show vault configuration.')
+
+    takes_options = (
+        Str(
+            'transport_out?',
+            doc=_('Output file to store the transport certificate'),
+        ),
+    )
+
+    def forward(self, *args, **options):
+
+        file = options.get('transport_out')
+
+        # don't send these parameters to server
+        if 'transport_out' in options:
+            del options['transport_out']
+
+        response = super(vaultconfig_show, self).forward(*args, **options)
+
+        if file:
+            with open(file, 'w') as f:
+                f.write(response['result']['transport_cert'])
+
+        return response
+
+    def execute(self, *args, **options):
+
+        if not self.api.env.enable_kra:
+            raise errors.InvocationError(
+                format=_('KRA service is not enabled'))
+
+        kra_client = self.api.Backend.kra.get_client()
+        transport_cert = kra_client.system_certs.get_transport_cert()
+        return {
+            'result': {
+                'transport_cert': transport_cert.binary
+            },
+            'value': None,
+        }
+
+
+@register()
+class vault_archive(PKQuery):
+    __doc__ = _('Archive data into a vault.')
+
+    takes_options = vault_options + (
+        Bytes(
+            'data?',
+            doc=_('Binary data to archive'),
+        ),
+        Str(  # TODO: use File parameter
+            'in?',
+            doc=_('File containing data to archive'),
+        ),
+    )
+
+    has_output = output.standard_entry
+
+    msg_summary = _('Archived data into vault "%(value)s"')
+
+    # FIXME: Should inherit from frontend.Local instead
+    def run(self, *args, **options):
+        return self.forward(*args, **options)
+
+    def forward(self, *args, **options):
+
+        data = options.get('data')
+        input_file = options.get('in')
+
+        # don't send these parameters to server
+        if 'data' in options:
+            del options['data']
+        if 'in' in options:
+            del options['in']
+
+        # get data
+        if data and input_file:
+            raise errors.MutuallyExclusiveError(
+                reason=_('Input data specified multiple times'))
+
+        if input_file:
+            with open(input_file, 'rb') as f:
+                data = f.read()
+
+        elif not data:
+            data = ''
+
+        # initialize NSS database
+        current_dbdir = paths.IPA_NSSDB_DIR
+        nss.nss_init(current_dbdir)
+
+        # retrieve transport certificate
+        config = self.api.Command.vaultconfig_show()
+        transport_cert_der = config['result']['transport_cert']
+        nss_transport_cert = nss.Certificate(transport_cert_der)
+
+        # generate session key
+        mechanism = nss.CKM_DES3_CBC_PAD
+        slot = nss.get_best_slot(mechanism)
+        key_length = slot.get_best_key_length(mechanism)
+        session_key = slot.key_gen(mechanism, None, key_length)
+
+        # wrap session key with transport certificate
+        public_key = nss_transport_cert.subject_public_key_info.public_key
+        wrapped_session_key = nss.pub_wrap_sym_key(mechanism,
+                                                   public_key,
+                                                   session_key)
+
+        options['session_key'] = wrapped_session_key.data
+
+        nonce_length = nss.get_iv_length(mechanism)
+        nonce = nss.generate_random(nonce_length)
+        options['nonce'] = nonce
+
+        vault_data = {}
+        vault_data[u'data'] = base64.b64encode(data).decode('utf-8')
+
+        json_vault_data = json.dumps(vault_data)
+
+        # wrap vault_data with session key
+        iv_si = nss.SecItem(nonce)
+        iv_param = nss.param_from_iv(mechanism, iv_si)
+
+        encoding_ctx = nss.create_context_by_sym_key(mechanism,
+                                                     nss.CKA_ENCRYPT,
+                                                     session_key,
+                                                     iv_param)
+
+        wrapped_vault_data = encoding_ctx.cipher_op(json_vault_data)\
+            + encoding_ctx.digest_final()
+
+        options['vault_data'] = wrapped_vault_data
+
+        response = self.api.Command.vault_archive_encrypted(*args, **options)
+
+        response['result'] = {}
+        del response['summary']
+
+        return response
+
+
+@register()
+class vault_archive_encrypted(Update):
+    NO_CLI = True
+
+    takes_options = vault_options + (
+        Bytes(
+            'session_key',
+            doc=_('Session key wrapped with transport certificate'),
+        ),
+        Bytes(
+            'vault_data',
+            doc=_('Vault data encrypted with session key'),
+        ),
+        Bytes(
+            'nonce',
+            doc=_('Nonce encrypted'),
+        ),
+    )
+
+    def execute(self, *args, **options):
+
+        if not self.api.env.enable_kra:
+            raise errors.InvocationError(
+                format=_('KRA service is not enabled'))
+
+        wrapped_vault_data = options.pop('vault_data')
+        nonce = options.pop('nonce')
+        wrapped_session_key = options.pop('session_key')
+
+        # retrieve vault info
+        result = self.api.Command.vault_show(*args, **options)
+        vault = result['result']
+
+        # connect to KRA
+        kra_client = self.api.Backend.kra.get_client()
+
+        kra_account = pki.account.AccountClient(kra_client.connection)
+        kra_account.login()
+
+        client_key_id = self.obj.get_key_id(vault['dn'])
+
+        # deactivate existing vault record in KRA
+        response = kra_client.keys.list_keys(
+            client_key_id,
+            pki.key.KeyClient.KEY_STATUS_ACTIVE)
+
+        for key_info in response.key_infos:
+            kra_client.keys.modify_key_status(
+                key_info.get_key_id(),
+                pki.key.KeyClient.KEY_STATUS_INACTIVE)
+
+        # forward wrapped data to KRA
+        kra_client.keys.archive_encrypted_data(
+            client_key_id,
+            pki.key.KeyClient.PASS_PHRASE_TYPE,
+            wrapped_vault_data,
+            wrapped_session_key,
+            None,
+            nonce,
+        )
+
+        kra_account.logout()
+
+        return result
+
+
+@register()
+class vault_retrieve(PKQuery):
+    __doc__ = _('Retrieve a data from a vault.')
+
+    takes_options = vault_options + (
+        Str(
+            'out?',
+            doc=_('File to store retrieved data'),
+        ),
+    )
+
+    has_output = output.standard_entry
+    has_output_params = (
+        Bytes(
+            'data',
+            label=_('Data'),
+        ),
+    )
+
+    msg_summary = _('Retrieved data from vault "%(value)s"')
+
+    # FIXME: Should inherit from frontend.Local instead
+    def run(self, *args, **options):
+        return self.forward(*args, **options)
+
+    def forward(self, *args, **options):
+
+        output_file = options.get('out')
+
+        # don't send these parameters to server
+        if 'out' in options:
+            del options['out']
+
+        # initialize NSS database
+        current_dbdir = paths.IPA_NSSDB_DIR
+        nss.nss_init(current_dbdir)
+
+        # retrieve transport certificate
+        config = self.api.Command.vaultconfig_show()
+        transport_cert_der = config['result']['transport_cert']
+        nss_transport_cert = nss.Certificate(transport_cert_der)
+
+        # generate session key
+        mechanism = nss.CKM_DES3_CBC_PAD
+        slot = nss.get_best_slot(mechanism)
+        key_length = slot.get_best_key_length(mechanism)
+        session_key = slot.key_gen(mechanism, None, key_length)
+
+        # wrap session key with transport certificate
+        public_key = nss_transport_cert.subject_public_key_info.public_key
+        wrapped_session_key = nss.pub_wrap_sym_key(mechanism,
+                                                   public_key,
+                                                   session_key)
+
+        # send retrieval request to server
+        options['session_key'] = wrapped_session_key.data
+
+        response = self.api.Command.vault_retrieve_encrypted(*args, **options)
+
+        result = response['result']
+        nonce = result['nonce']
+
+        # unwrap data with session key
+        wrapped_vault_data = result['vault_data']
+
+        iv_si = nss.SecItem(nonce)
+        iv_param = nss.param_from_iv(mechanism, iv_si)
+
+        decoding_ctx = nss.create_context_by_sym_key(mechanism,
+                                                     nss.CKA_DECRYPT,
+                                                     session_key,
+                                                     iv_param)
+
+        json_vault_data = decoding_ctx.cipher_op(wrapped_vault_data)\
+            + decoding_ctx.digest_final()
+
+        vault_data = json.loads(json_vault_data)
+        data = base64.b64decode(vault_data[u'data'].encode('utf-8'))
+
+        if output_file:
+            with open(output_file, 'w') as f:
+                f.write(data)
+
+        response['result'] = {'data': data}
+        del response['summary']
+
+        return response
+
+@register()
+class vault_retrieve_encrypted(Retrieve):
+    NO_CLI = True
+
+    takes_options = vault_options + (
+        Bytes(
+            'session_key',
+            doc=_('Session key wrapped with transport certificate'),
+        ),
+    )
+
+    def execute(self, *args, **options):
+
+        if not self.api.env.enable_kra:
+            raise errors.InvocationError(
+                format=_('KRA service is not enabled'))
+
+        wrapped_session_key = options.pop('session_key')
+
+        # retrieve vault info
+        result = self.api.Command.vault_show(*args, **options)
+        vault = result['result']
+
+        # connect to KRA
+        kra_client = self.api.Backend.kra.get_client()
+
+        kra_account = pki.account.AccountClient(kra_client.connection)
+        kra_account.login()
+
+        client_key_id = self.obj.get_key_id(vault['dn'])
+
+        # find vault record in KRA
+        response = kra_client.keys.list_keys(
+            client_key_id,
+            pki.key.KeyClient.KEY_STATUS_ACTIVE)
+
+        if not len(response.key_infos):
+            raise errors.NotFound(reason=_('No archived data.'))
+
+        key_info = response.key_infos[0]
+
+        # retrieve encrypted data from KRA
+        key = kra_client.keys.retrieve_key(
+            key_info.get_key_id(),
+            wrapped_session_key)
+
+        vault['vault_data'] = key.encrypted_data
+        vault['nonce'] = key.nonce_data
+
+        kra_account.logout()
+
+        return result
diff --git a/ipatests/test_xmlrpc/test_vault_plugin.py b/ipatests/test_xmlrpc/test_vault_plugin.py
index 44d397c..4b18672 100644
--- a/ipatests/test_xmlrpc/test_vault_plugin.py
+++ b/ipatests/test_xmlrpc/test_vault_plugin.py
@@ -22,12 +22,15 @@ Test the `ipalib/plugins/vault.py` module.
 """
 
 from ipalib import api, errors
-from xmlrpc_test import Declarative, fuzzy_string
+from xmlrpc_test import Declarative
 
 vault_name = u'test_vault'
 service_name = u'HTTP/server.example.com'
 user_name = u'testuser'
 
+# binary data from \x00 to \xff
+secret = ''.join(map(chr, xrange(0, 256)))
+
 
 class test_vault_plugin(Declarative):
 
@@ -442,4 +445,71 @@ class test_vault_plugin(Declarative):
             },
         },
 
+        {
+            'desc': 'Create vault for archival',
+            'command': (
+                'vault_add',
+                [vault_name],
+                {},
+            ),
+            'expected': {
+                'value': vault_name,
+                'summary': 'Added vault "%s"' % vault_name,
+                'result': {
+                    'dn': u'cn=%s,cn=admin,cn=users,cn=vaults,%s'
+                          % (vault_name, api.env.basedn),
+                    'objectclass': [u'top', u'ipaVault'],
+                    'cn': [vault_name],
+                },
+            },
+        },
+
+        {
+            'desc': 'Archive secret',
+            'command': (
+                'vault_archive',
+                [vault_name],
+                {
+                    'data': secret,
+                },
+            ),
+            'expected': {
+                'value': vault_name,
+                'summary': 'Archived data into vault "%s"' % vault_name,
+                'result': {},
+            },
+        },
+
+        {
+            'desc': 'Retrieve secret',
+            'command': (
+                'vault_retrieve',
+                [vault_name],
+                {},
+            ),
+            'expected': {
+                'value': vault_name,
+                'summary': 'Retrieved data from vault "%s"' % vault_name,
+                'result': {
+                    'data': secret,
+                },
+            },
+        },
+
+        {
+            'desc': 'Delete vault for archival',
+            'command': (
+                'vault_del',
+                [vault_name],
+                {},
+            ),
+            'expected': {
+                'value': [vault_name],
+                'summary': u'Deleted vault "%s"' % vault_name,
+                'result': {
+                    'failed': (),
+                },
+            },
+        },
+
     ]
diff --git a/make-lint b/make-lint
index 40dceff..0447985 100755
--- a/make-lint
+++ b/make-lint
@@ -62,6 +62,7 @@ class IPATypeChecker(TypeChecker):
         'unittest.case': ['assertEqual', 'assertRaises'],
         'nose.tools': ['assert_equal', 'assert_raises'],
         'datetime.tzinfo': ['houroffset', 'minoffset', 'utcoffset', 'dst'],
+        'nss.nss.subject_public_key_info': ['public_key'],
 
         # IPA classes
         'ipalib.base.NameSpace': ['add', 'mod', 'del', 'show', 'find'],
-- 
2.1.0

-- 
Manage your subscription for the Freeipa-devel mailing list:
https://www.redhat.com/mailman/listinfo/freeipa-devel
Contribute to FreeIPA: http://www.freeipa.org/page/Contribute/Code

Reply via email to