[Freeipa-devel] [PATCH] Password vault

Endi Sukma Dewata edewata at redhat.com
Wed May 27 00:38:33 UTC 2015


Please take a look at the attached patch to add vault-archive/retrieve 
commands.

On 4/20/2015 1:12 AM, Jan Cholasta wrote:
>>>>> 16) You do way too much stuff in vault_add.forward(). Only code that
>>>>> must be done on the client needs to be there, i.e. handling of the
>>>>> "data", "text" and "in" options.
>>>>>
>>>>> The vault_archive call must be in vault_add.execute(), otherwise a) we
>>>>> will be making 2 RPC calls from the client and b) it won't be
>>>>> called at
>>>>> all when api.env.in_server is True.
>>>>
>>>> This is done by design. The vault_add.forward() generates the salt and
>>>> the keys. The vault_archive.forward() will encrypt the data. These
>>>> operations have to be done on the client side to secure the
>>>> transport of
>>>> the data from the client through the server and finally to KRA. This
>>>> mechanism prevents the server from looking at the unencrypted data.
>>>
>>> OK, but that does not justify that it's broken in server-side API. It
>>> can and should be done so that it works the same way on both client and
>>> server.
>>>
>>> I think the best solution would be to split the command into two
>>> commands, server-side vault_archive_raw to archive already encrypted
>>> data, and client-side vault_archive to encrypt data and archive them
>>> with vault_archive_raw in its .execute(). Same thing for vault_retrieve.
>>
>> Actually I think it's better to just merge the add and archive, reducing
>> the number of RPC calls. The vault_archive now will support two types of
>> operations:
>>
>> (a) Archive data into a new vault (it will create the vault just before
>> archiving the data):
>>
>>    $ ipa vault-archive testvault --create --in data ...
>>
>> (b) Archive data into an existing vault:
>>
>>    $ ipa vault-archive testvault --in data ...
>>
>> The vault_add is now just a wrapper for the vault_archive(a).
>
> If that's just an implementation detail, OK.
>
> If it's possible to modify existing vault objects using vault-add or
> create new objects using vault-archive, then NACK.

The vault-archive is now completely separate from vault-add. You can no 
longer archive data while creating a vault:

   $ ipa vault-add test
   $ ipa vault-archive test --in secret.bin

>>> BTW, I also think it would be better if there were 2 separate sets of
>>> commands for binary and textual data
>>> (vault_{archive,retrieve}_{data,text}) rather than trying to handle
>>> everything in vault_{archive,retrieve}.
>>
>> I don't think we want to provide a separate command of every possible
>> data type & operation combination. Users would get confused. The archive
>> & retrieve commands should be able to handle all current & future data
>> types with options.
>
> A command with two sets of mutually exclusive options is really two
> commands in disguise, which is a good sign it should be divided into two
> actual commands.
>
> Who are you to say users would get confused? I say they would be more
> confused by a command with a plethora of mutually exclusive "options".
>
> What other possible data types are there?

The patch has been simplified to support only binary data. The data can 
be specified using either of these options:

   $ ipa vault-archive test --data <base-64 encoded data>
   $ ipa vault-archive test --in <input file>

I don't think we want to provide two separate commands for these options 
although they are mutually exclusive, do we?

>>>> The add & archive combination was added for convenience, not for
>>>> optimization. This way you would be able to archive data into a new
>>>> vault using a single command. Without this, you'd have to execute two
>>>> separate commands: add & archive, which will result in 2 RPC calls
>>>> anyway.
>>>
>>> I think I would prefer if it was separate, as that would be consistent
>>> with other plugins (e.g. for objects with members, we don't allow adding
>>> members directly in -add, you have to use -add-member after -add).
>>
>> The vault data is similar to group description, not group members. When
>> creating a group we can supply the description. If not specified it will
>> be blank. Archiving vault data is similar to updating the group
>> description.
>
> It's similar to group members because there are separate commands to
> manipulate them.

Just because there are separate commands doesn't mean vault data 
(single-valued) is similar to group members (multi-valued). It uses 
separate commands because of the encryption steps involved while 
archiving/retrieving data.

> You have to choose one of:
>
>    a) vault data is settable using vault-add and vault-mod and gettable
> using vault-mod, vault-show and vault-find
>
>    b) vault data is settable using vault-archive and gettable using
> vault-retrieve
>
> Anything in between is not permitted.

As mentioned above, the add and archive commands are now separate.

>>>>> 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.

There are existing commands that inherit from LDAPQuery while doing 
other things too, so the 'semantic' seems to be kind of arbitrarily 
defined and subjective.

>>>>> 22) vault_archive will break with binary data that is not UTF-8
>>>>> encoded
>>>>> text.
>>>>>
>>>>> This is where it occurs:
>>>>>
>>>>> +        vault_data[u'data'] = unicode(data)
>>>>>
>>>>> Generally, don't use unicode() on str values and str() on unicode
>>>>> values
>>>>> directly, always use .decode() and .encode().
>>
>> The unicode(s, encoding) is actually equivalent to s.decode(encoding),
>> so the following code will not solve the problem:
>>
>>    vault_data[u'data'] = data.decode()
>>
>> As you said, decode() will only work if the data being decoded actually
>> follows the encoding rules (i.e. already UTF-8 encoded).
>>
>>>> It needs to be a Unicode because json.dumps() doesn't work with binary
>>>> data. Fixed by adding base-64 encoding.
>>
>> The base-64 encoding is necessary to convert random binaries into ASCII
>> so it can be decoded into Unicode. Here is the current code:
>>
>>    vault_data[u'data'] = unicode(base64.b64encode(data))
>>
>> which is equivalent to:
>>
>>    vault_data[u'data'] = base64.b64encode(data).decode()
>
> If you read a little bit further, you would get to the point, which is
> certainly not calling .decode() without arguments, but *always
> explicitly specify the encoding*.

Added the explicit encoding name although it's not necessary since the 
data being encoded/decoded is base-64 encoded (i.e. ASCII).

>>> If something str needs to be unicode, you should use .decode() to
>>> explicitly specify the encoding, instead of relying on unicode() to pick
>>> the correct one.
>>
>> Since we know this is ASCII data we can now specify UTF-8 encoding.
>>
>>    vault_data[u'data'] = base64.b64encode(data).decode('utf-8')
>>
>> But for anything that comes from user input (e.g. filenames, passwords),
>> it's better to use the default encoding because that can be configured
>> by the user.
>
> You are confusing user's configured encoding with Python's default
> encoding. Default encoding in Python isn't derived from user's
> localization settings.
>
> Anyway, anything that comes from user input is already decoded using
> user's configured encoding when it enters the framework so I don't know
> why are you even bringing it up here.

It's irrelevant now that the command only supports binary data.

>>> Anyway, I think a better solution than base64 would be to use the
>>> "raw_unicode_escape" encoding:
>>
>> As explained above, base-64 encoding is necessary because random
>> binaries don't follow any encoding rules. I'd rather not use
>> raw_unicode_escape because it's not really a text data.
>
> The result of decoding binary data using raw_unicode_escape is perfectly
> valid unicode data which doesn't eat 33% more space like base64 encoded
> binary does, hence my suggestion.
>
> Anyway, it turns out when encoded in JSON, raw_unicode_escape string
> generally takes more space than base64 encoded string because of JSON
> escaping, so base64 is indeed better.

Unchanged. It still uses base-64 encoding.

>> Here's how it's
>> now implemented:
>>
>>>      if data:
>>>          data = data.decode('raw_unicode_escape')
>>
>> Input data is already in binaries, no conversion needed.
>>
>>>      elif text:
>>>          data = text
>>
>> Input text will be converted to binaries with default encoding:
>>
>>    data = text.encode()
>
> See what the default encoding actually is and why you shouldn't rely on
> it above.

Irrelevant now.

>>>      elif input_file:
>>>          with open(input_file, 'rb') as f:
>>>              data = f.read()
>>>          data = data.decode('raw_unicode_escape')
>>
>> Input contains binary data, no conversion needed.
>>
>>>      else:
>>>          data = u''
>>
>> If not specified, the data will be empty string:
>>
>>    data = ''
>>
>> The data needs to be converted into binaries so it can be encrypted
>> before transport (depending on the vault type):
>>
>>    data = self.obj.encrypt(data, ...)
>>
>>>      vault_data[u'data'] = data
>>
>> Then for transport the data is base-64 encoded first, then converted
>> into Unicode:
>>
>>    vault_data[u'data'] = base64.b64encode(data).decode('utf-8')

-- 
Endi S. Dewata
-------------- next part --------------
>From bdd5c6900509e223ba54aa105a77f8404579e2b7 Mon Sep 17 00:00:00 2001
From: "Endi S. Dewata" <edewata at redhat.com>
Date: Tue, 21 Oct 2014 10:57:08 -0400
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                                   |  28 ++
 VERSION                                   |   4 +-
 ipalib/plugins/vault.py                   | 452 +++++++++++++++++++++++++++++-
 ipatests/test_xmlrpc/test_vault_plugin.py |  71 ++++-
 4 files changed, 551 insertions(+), 4 deletions(-)

diff --git a/API.txt b/API.txt
index da69f32de5c12c0d85a7d61d9027385aa3c0ee05..3741e6f16689e43838c2d31a44872d1ea47589c7 100644
--- a/API.txt
+++ b/API.txt
@@ -4768,6 +4768,24 @@ 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,9,1
+arg: Str('cn', cli_name='name', maxlength=255, pattern='^[a-zA-Z0-9_.-]+$')
+option: Bytes('data?')
+option: Str('in?')
+option: Str('nonce?')
+option: Str('service?')
+option: Str('session_key?')
+option: Flag('shared?', autofill=True, default=False)
+option: Str('user?')
+option: Str('vault_data?')
+option: Str('version?', exclude='webui')
+output: Output('result', None, None)
+command: vault_config
+args: 0,2,1
+option: Str('transport_out?')
+option: Str('version?', exclude='webui')
+output: Output('result', 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)
@@ -4814,6 +4832,16 @@ 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,6,1
+arg: Str('cn', cli_name='name', maxlength=255, pattern='^[a-zA-Z0-9_.-]+$')
+option: Str('out?')
+option: Str('service?')
+option: Str('session_key?')
+option: Flag('shared?', autofill=True, default=False)
+option: Str('user?')
+option: Str('version?', exclude='webui')
+output: Output('result', 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)
diff --git a/VERSION b/VERSION
index 07c00d000064a7687497b09524aa821dbcecc88a..2bfb2fe46b3760f30e1aa378841544a51f014728 100644
--- a/VERSION
+++ b/VERSION
@@ -90,5 +90,5 @@ IPA_DATA_VERSION=20100614120000
 #                                                      #
 ########################################################
 IPA_API_VERSION_MAJOR=2
-IPA_API_VERSION_MINOR=121
-# Last change: pvoborni - added server-find and server-show
+IPA_API_VERSION_MINOR=122
+# Last change: edewata - added vault-archive and vault-retrieve
diff --git a/ipalib/plugins/vault.py b/ipalib/plugins/vault.py
index ebb9f9fd3cf3b5a7d6b44ac9e63e122e8f71aa1a..c6fd41063058672a90a95979a907876161f3256a 100644
--- a/ipalib/plugins/vault.py
+++ b/ipalib/plugins/vault.py
@@ -17,8 +17,21 @@
 # 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
+
+import pki.account
+import pki.crypto
+import pki.key
+
+from ipalib.frontend import Command
 from ipalib import api, errors
-from ipalib import Str, Flag
+from ipalib import Bytes, Str, Flag
 from ipalib import output
 from ipalib.plugable import Registry
 from ipalib.plugins.baseldap import LDAPObject, LDAPCreate, LDAPDelete,\
@@ -26,7 +39,9 @@ from ipalib.plugins.baseldap import LDAPObject, LDAPCreate, LDAPDelete,\
 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
 
 __doc__ = _("""
 Vaults
@@ -94,6 +109,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 +285,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):
@@ -273,6 +335,29 @@ class vault_del(LDAPDelete):
 
     msg_summary = _('Deleted vault "%(value)s"')
 
+    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):
@@ -319,3 +404,368 @@ class vault_show(LDAPRetrieve):
     __doc__ = _('Display information about a vault.')
 
     takes_options = LDAPRetrieve.takes_options + vault_options
+
+
+ at register()
+class vault_config(Command):
+    __doc__ = _('Show vault configuration.')
+
+    takes_options = (
+        Str(
+            'transport_out?',
+            doc=_('Output file to store the transport certificate'),
+        ),
+    )
+
+    has_output_params = (
+        Str(
+            'transport_cert',
+            label=_('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(vault_config, 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):
+
+        kra_client = self.api.Backend.kra.get_client()
+        transport_cert = kra_client.system_certs.get_transport_cert()
+        return {
+            'result': {
+                'transport_cert': transport_cert.encoded
+            }
+        }
+
+
+ at register()
+class vault_archive(Command):
+    __doc__ = _('Archive data into a vault.')
+
+    takes_args = (
+        Str(
+            'cn',
+            cli_name='name',
+            label=_('Vault name'),
+            pattern='^[a-zA-Z0-9_.-]+$',
+            pattern_errmsg='may only include letters, numbers, _, ., and -',
+            maxlength=255,
+        ),
+    )
+
+    takes_options = vault_options + (
+        Bytes(
+            'data?',
+            doc=_('Binary data to archive'),
+        ),
+        Str(  # TODO: use File parameter
+            'in?',
+            doc=_('File containing data to archive'),
+        ),
+        Str(
+            'session_key?',
+            doc=_(
+                'Session key wrapped with transport certificate'
+                ' and encoded in base-64'),
+        ),
+        Str(
+            'vault_data?',
+            doc=_(
+                'Vault data encrypted with session key'
+                ' and encoded in base-64'),
+        ),
+        Str(
+            'nonce?',
+            doc=_('Nonce encrypted encoded in base-64'),
+        ),
+    )
+
+    msg_summary = _('Archived data into vault "%(value)s"')
+
+    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
+        crypto = pki.crypto.NSSCryptoProvider(paths.IPA_NSSDB_DIR)
+        crypto.initialize()
+        current_dbdir = paths.IPA_NSSDB_DIR
+
+        # retrieve transport certificate
+        (file, filename) = tempfile.mkstemp()
+        os.close(file)
+        try:
+            self.api.Command.vault_config(transport_out=unicode(filename))
+            transport_cert_der = nss.read_der_from_file(filename, True)
+            nss_transport_cert = nss.Certificate(transport_cert_der)
+
+        finally:
+            os.remove(filename)
+
+        # generate session key
+        session_key = crypto.generate_session_key()
+
+        # wrap session key with transport certificate
+        wrapped_session_key = crypto.asymmetric_wrap(
+            session_key,
+            nss_transport_cert
+        )
+
+        options['session_key'] = base64.b64encode(wrapped_session_key)\
+            .decode('utf-8')
+
+        nonce = crypto.generate_nonce_iv()
+        options['nonce'] = base64.b64encode(nonce).decode('utf-8')
+
+        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
+        wrapped_vault_data = crypto.symmetric_wrap(
+            json_vault_data,
+            session_key,
+            nonce_iv=nonce
+        )
+
+        options['vault_data'] = base64.b64encode(wrapped_vault_data)\
+            .decode('utf-8')
+
+        return super(vault_archive, self).forward(*args, **options)
+
+    def execute(self, *args, **options):
+
+        vault_name = args[0]
+
+        # retrieve vault info
+        vault = self.api.Command.vault_show(
+            vault_name,
+            service=options.get('service'),
+            shared=options.get('shared'),
+            user=options.get('user'),
+        )['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.api.Object.vault.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)
+
+        wrapped_session_key = base64.b64decode(options['session_key'])
+        nonce = base64.b64decode(options['nonce'])
+
+        # forward wrapped data to KRA
+        wrapped_vault_data = base64.b64decode(options['vault_data'])
+
+        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()
+
+        response = {}
+        response['result'] = {}
+
+        return response
+
+
+ at register()
+class vault_retrieve(Command):
+    __doc__ = _('Retrieve a data from a vault.')
+
+    takes_args = (
+        Str(
+            'cn',
+            cli_name='name',
+            label=_('Vault name'),
+            pattern='^[a-zA-Z0-9_.-]+$',
+            pattern_errmsg='may only include letters, numbers, _, ., and -',
+            maxlength=255,
+        ),
+    )
+
+    takes_options = vault_options + (
+        Str(
+            'out?',
+            doc=_('File to store retrieved data'),
+        ),
+        Str(
+            'session_key?',
+            doc=_(
+                'Session key wrapped with transport certificate'
+                ' and encoded in base-64'),
+        ),
+    )
+
+    has_output_params = (
+        Bytes(
+            'data',
+            label=_('Data'),
+        ),
+    )
+
+    msg_summary = _('Retrieved data from vault "%(value)s"')
+
+    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
+        crypto = pki.crypto.NSSCryptoProvider(paths.IPA_NSSDB_DIR)
+        crypto.initialize()
+        current_dbdir = paths.IPA_NSSDB_DIR
+
+        # retrieve transport certificate
+        (file, filename) = tempfile.mkstemp()
+        os.close(file)
+        try:
+            self.api.Command.vault_config(transport_out=unicode(filename))
+            transport_cert_der = nss.read_der_from_file(filename, True)
+            nss_transport_cert = nss.Certificate(transport_cert_der)
+
+        finally:
+            os.remove(filename)
+
+        # generate session key
+        session_key = crypto.generate_session_key()
+
+        # wrap session key with transport certificate
+        wrapped_session_key = crypto.asymmetric_wrap(
+            session_key,
+            nss_transport_cert
+        )
+
+        # send retrieval request to server
+        options['session_key'] = base64.b64encode(wrapped_session_key)\
+            .decode('utf-8')
+
+        response = super(vault_retrieve, self).forward(*args, **options)
+
+        result = response['result']
+        nonce = base64.b64decode(result['nonce'])
+
+        # unwrap data with session key
+        wrapped_vault_data = base64.b64decode(result['vault_data'])
+
+        json_vault_data = crypto.symmetric_unwrap(
+            wrapped_vault_data,
+            session_key,
+            nonce_iv=nonce)
+
+        vault_data = json.loads(json_vault_data)
+        data = base64.b64decode(vault_data[u'data'].encode('utf-8'))
+
+        if output_file:
+            response = {}
+            response['result'] = {}
+            with open(output_file, 'w') as f:
+                f.write(data)
+
+        else:
+            response['result']['data'] = data
+            del response['result']['nonce']
+            del response['result']['vault_data']
+
+        return response
+
+    def execute(self, *args, **options):
+
+        vault_name = args[0]
+
+        # retrieve vault info
+        vault = self.api.Command.vault_show(
+            vault_name,
+            service=options.get('service'),
+            shared=options.get('shared'),
+            user=options.get('user'),
+        )['result']
+
+        wrapped_session_key = base64.b64decode(options['session_key'])
+
+        # 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.api.Object.vault.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'] = base64.b64encode(
+            key.encrypted_data).decode('utf-8')
+        vault['nonce'] = base64.b64encode(key.nonce_data).decode('utf-8')
+
+        kra_account.logout()
+
+        response = {}
+        response['result'] = vault
+
+        return response
diff --git a/ipatests/test_xmlrpc/test_vault_plugin.py b/ipatests/test_xmlrpc/test_vault_plugin.py
index 44d397c583928d98ec252899398ae6c3a83c207c..0664addd646806f1b8a5083ef5da16c4dfc015dc 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,70 @@ 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': {
+                'result': {},
+            },
+        },
+
+        {
+            'desc': 'Retrieve secret',
+            'command': (
+                'vault_retrieve',
+                [vault_name],
+                {},
+            ),
+            'expected': {
+                'result': {
+                    'dn': u'cn=%s,cn=admin,cn=users,cn=vaults,%s'
+                          % (vault_name, api.env.basedn),
+                    'cn': [vault_name],
+                    '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': (),
+                },
+            },
+        },
+
     ]
-- 
1.9.3



More information about the Freeipa-devel mailing list