Dne 5.6.2015 v 21:50 Endi Sukma Dewata napsal(a):
On 6/5/2015 7:13 AM, Jan Cholasta wrote:
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 other thread was talking about removing the pki-base dependency on
the client side, but the vault plugin is still loaded on both client and
server regardless of KRA installation. Ideally the vault plugin should
not even be loaded so you cannot even execute the commands.

I don't agree - ideally the vault plugin should do the check at runtime, because it should work without httpd restart when KRA is installed on other replica. The KRA installer needs to be fixed in order to support this. I will provide a patch.


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.

I see the vault_archive and vault_retrieve now inherit from PKQuery, and
there is a hack to execute the forward() even on the server side. A few
things below:

1. Why didn't you use frontend.Local as you initially suggested? If
there's a problem with frontend.Local please attach the ticket number in
the code.

2. The forward() can be merged into run(). There is no need to keep the
code in forward(). It would make more sense to have a run() method that
runs both on client and server, rather than a forward() that is supposed
to run on the client only but now forced to run on server too,
semantically speaking.

I have fixed the commands to inherit from Local.


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

3. The parameter description for nonce should be just 'Nonce' instead of
'Nonce encrypted'.

Fixed.


4. There's a PEP8 error.

Fixed.


5. The VERSION needs to be updated.

Fixed.


Assuming the above issues are addressed, ACK.

OK, pushed to master: df1bd39a43f30138cf55e0e7720fa3dec1d912e0


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

That's the default used by the KRA's client library, and that's what the
KRA has been tested with. We probably can change it to AES later. It
shouldn't be blocking this patch.

OK, no problem.


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

There's a bug in IPA: https://bugzilla.redhat.com/show_bug.cgi?id=1228671

The patch needs a rebase and version bumb ("VERSION" line at the top of ipa-pki-proxy.conf).

--
Jan Cholasta
>From 6bbc08ab3abaeffb2e5b9b218174a52d09c7832d 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                   | 496 +++++++++++++++++++++++++++++-
 ipatests/test_xmlrpc/test_vault_plugin.py |  72 ++++-
 make-lint                                 |   1 +
 5 files changed, 634 insertions(+), 4 deletions(-)

diff --git a/API.txt b/API.txt
index eca4e30..9e3f223 100644
--- a/API.txt
+++ b/API.txt
@@ -5146,6 +5146,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)
@@ -5192,6 +5222,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)
@@ -5205,6 +5261,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 fe746a7..535b3e2 100644
--- a/VERSION
+++ b/VERSION
@@ -90,5 +90,5 @@ IPA_DATA_VERSION=20100614120000
 #                                                      #
 ########################################################
 IPA_API_VERSION_MAJOR=2
-IPA_API_VERSION_MINOR=125
-# Last change: derny - migration now accepts scope as argument 
+IPA_API_VERSION_MINOR=126
+# Last change: edewata - added vault-archive and vault-retrieve
diff --git a/ipalib/plugins/vault.py b/ipalib/plugins/vault.py
index ebb9f9f..e1e64aa 100644
--- a/ipalib/plugins/vault.py
+++ b/ipalib/plugins/vault.py
@@ -17,16 +17,33 @@
 # 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 krbV
+
+from ipalib.frontend import Command, Object, Local
 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 +111,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 +287,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 +320,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 +341,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 +390,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 +417,399 @@ 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, Local):
+    __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"')
+
+    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 = ''
+
+        if self.api.env.in_server:
+            backend = self.api.Backend.ldap2
+        else:
+            backend = self.api.Backend.rpcclient
+        if not backend.isconnected():
+            backend.connect(ccache=krbV.default_context().default_ccache())
+
+        # 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'),
+        ),
+    )
+
+    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, Local):
+    __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"')
+
+    def forward(self, *args, **options):
+
+        output_file = options.get('out')
+
+        # don't send these parameters to server
+        if 'out' in options:
+            del options['out']
+
+        if self.api.env.in_server:
+            backend = self.api.Backend.ldap2
+        else:
+            backend = self.api.Backend.rpcclient
+        if not backend.isconnected():
+            backend.connect(ccache=krbV.default_context().default_ccache())
+
+        # 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