From d4969ede51e6098e962ff660daf13e8c61d4ac28 Mon Sep 17 00:00:00 2001 From: "Endi S. Dewata" Date: Fri, 31 Jul 2015 07:53:15 +0200 Subject: Added support for changing vault encryption. The vault-mod command has been modified to support changing vault encryption attributes (i.e. type, password, public/private keys) in addition to normal attributes (i.e. description). Changing the encryption requires retrieving the stored secret with the old attributes and rearchiving it with the new attributes. https://fedorahosted.org/freeipa/ticket/5176 Reviewed-By: Martin Basti --- API.txt | 27 +++- VERSION | 4 +- ipalib/plugins/vault.py | 233 ++++++++++++++++++++++++++-- ipatests/test_xmlrpc/test_vault_plugin.py | 249 ++++++++++++++++++++++++++++++ 4 files changed, 498 insertions(+), 15 deletions(-) diff --git a/API.txt b/API.txt index b0f456e72..8105cfb5b 100644 --- a/API.txt +++ b/API.txt @@ -5474,11 +5474,12 @@ output: Output('completed', , None) output: Output('failed', , None) output: Entry('result', , Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None)) command: vault_archive -args: 1,10,3 +args: 1,11,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('override_password?', autofill=True, default=False) option: Str('password?', cli_name='password') option: Str('password_file?', cli_name='password_file') option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui') @@ -5538,6 +5539,30 @@ output: ListOfEntries('result', (, ), Gettext('A list output: Output('summary', (, ), None) output: Output('truncated', , None) command: vault_mod +args: 1,18,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('change_password?', autofill=True, default=False) +option: Str('description?', cli_name='desc') +option: Bytes('ipavaultpublickey?', cli_name='public_key') +option: Bytes('ipavaultsalt?', cli_name='salt') +option: Str('ipavaulttype?', cli_name='type') +option: Str('new_password?', cli_name='new_password') +option: Str('new_password_file?', cli_name='new_password_file') +option: Str('old_password?', cli_name='old_password') +option: Str('old_password_file?', cli_name='old_password_file') +option: Bytes('private_key?', cli_name='private_key') +option: Str('private_key_file?', cli_name='private_key_file') +option: Str('public_key_file?', cli_name='public_key_file') +option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui') +option: Str('service?') +option: Flag('shared?', autofill=True, default=False) +option: Str('username?', cli_name='user') +option: Str('version?', exclude='webui') +output: Entry('result', , Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None)) +output: Output('summary', (, ), None) +output: PrimaryKey('value', None, None) +command: vault_mod_internal args: 1,15,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: Str('addattr*', cli_name='addattr', exclude='webui') diff --git a/VERSION b/VERSION index 9fe2f4d4f..3fdd2db88 100644 --- a/VERSION +++ b/VERSION @@ -90,5 +90,5 @@ IPA_DATA_VERSION=20100614120000 # # ######################################################## IPA_API_VERSION_MAJOR=2 -IPA_API_VERSION_MINOR=152 -# Last change: mbasti - add 'user-stage' command +IPA_API_VERSION_MINOR=153 +# Last change: edewata - Added support for changing vault encryption. diff --git a/ipalib/plugins/vault.py b/ipalib/plugins/vault.py index 4b2c8a518..6a07a76b5 100644 --- a/ipalib/plugins/vault.py +++ b/ipalib/plugins/vault.py @@ -116,10 +116,36 @@ EXAMPLES: ipa vault-show [--user |--service |--shared] """) + _(""" - Modify a vault: + Modify vault description: ipa vault-mod [--user |--service |--shared] --desc +""") + _(""" + Modify vault type: + ipa vault-mod + [--user |--service |--shared] + --type + [old password/private key] + [new password/public key] +""") + _(""" + Modify symmetric vault password: + ipa vault-mod + [--user |--service |--shared] + --change-password + ipa vault-mod + [--user |--service |--shared] + --old-password + --new-password + ipa vault-mod + [--user |--service |--shared] + --old-password-file + --new-password-file +""") + _(""" + Modify asymmetric vault keys: + ipa vault-mod + [--user |--service |--shared] + --private-key-file + --public-key-file """) + _(""" Delete a vault: ipa vault-del @@ -457,7 +483,7 @@ class vault(LDAPObject): print ' ** Passwords do not match! **' - def get_existing_password(self, new=False): + def get_existing_password(self): """ Gets existing password from user. """ @@ -871,9 +897,182 @@ class vault_find(LDAPSearch): @register() -class vault_mod(LDAPUpdate): +class vault_mod(PKQuery, Local): __doc__ = _('Modify a vault.') + takes_options = vault_options + ( + Str( + 'description?', + cli_name='desc', + doc=_('Vault description'), + ), + Str( + 'ipavaulttype?', + cli_name='type', + doc=_('Vault type'), + ), + Bytes( + 'ipavaultsalt?', + cli_name='salt', + doc=_('Vault salt'), + ), + Flag( + 'change_password?', + doc=_('Change password'), + ), + Str( + 'old_password?', + cli_name='old_password', + doc=_('Old vault password'), + ), + Str( # TODO: use File parameter + 'old_password_file?', + cli_name='old_password_file', + doc=_('File containing the old vault password'), + ), + Str( + 'new_password?', + cli_name='new_password', + doc=_('New vault password'), + ), + Str( # TODO: use File parameter + 'new_password_file?', + cli_name='new_password_file', + doc=_('File containing the new vault password'), + ), + Bytes( + 'private_key?', + cli_name='private_key', + doc=_('Old vault private key'), + ), + Str( # TODO: use File parameter + 'private_key_file?', + cli_name='private_key_file', + doc=_('File containing the old vault private key'), + ), + Bytes( + 'ipavaultpublickey?', + cli_name='public_key', + doc=_('New vault public key'), + ), + Str( # TODO: use File parameter + 'public_key_file?', + cli_name='public_key_file', + doc=_('File containing the new vault public key'), + ), + ) + + has_output = output.standard_entry + + def forward(self, *args, **options): + + vault_type = options.pop('ipavaulttype', False) + salt = options.pop('ipavaultsalt', False) + change_password = options.pop('change_password', False) + + old_password = options.pop('old_password', None) + old_password_file = options.pop('old_password_file', None) + new_password = options.pop('new_password', None) + new_password_file = options.pop('new_password_file', None) + + old_private_key = options.pop('private_key', None) + old_private_key_file = options.pop('private_key_file', None) + new_public_key = options.pop('ipavaultpublickey', None) + new_public_key_file = options.pop('public_key_file', None) + + 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()) + + # determine the vault type based on parameters specified + if vault_type: + pass + + elif change_password or new_password or new_password_file or salt: + vault_type = u'symmetric' + + elif new_public_key or new_public_key_file: + vault_type = u'asymmetric' + + # if vault type is specified, retrieve existing secret + if vault_type: + opts = options.copy() + opts.pop('description', None) + + opts['password'] = old_password + opts['password_file'] = old_password_file + opts['private_key'] = old_private_key + opts['private_key_file'] = old_private_key_file + + response = self.api.Command.vault_retrieve(*args, **opts) + data = response['result']['data'] + + opts = options.copy() + + # if vault type is specified, update crypto attributes + if vault_type: + opts['ipavaulttype'] = vault_type + + if vault_type == u'standard': + opts['ipavaultsalt'] = None + opts['ipavaultpublickey'] = None + + elif vault_type == u'symmetric': + if salt: + opts['ipavaultsalt'] = salt + else: + opts['ipavaultsalt'] = os.urandom(16) + + opts['ipavaultpublickey'] = None + + elif vault_type == u'asymmetric': + + # get new vault public key + if new_public_key and new_public_key_file: + raise errors.MutuallyExclusiveError( + reason=_('New public key specified multiple times')) + + elif new_public_key: + pass + + elif new_public_key_file: + new_public_key = validated_read('public_key_file', + new_public_key_file, + mode='rb') + + else: + raise errors.ValidationError( + name='ipavaultpublickey', + error=_('Missing new vault public key')) + + opts['ipavaultsalt'] = None + opts['ipavaultpublickey'] = new_public_key + + response = self.api.Command.vault_mod_internal(*args, **opts) + + # if vault type is specified, rearchive existing secret + if vault_type: + opts = options.copy() + opts.pop('description', None) + + opts['data'] = data + opts['password'] = new_password + opts['password_file'] = new_password_file + opts['override_password'] = True + + self.api.Command.vault_archive(*args, **opts) + + return response + + +@register() +class vault_mod_internal(LDAPUpdate): + + NO_CLI = True + takes_options = LDAPUpdate.takes_options + vault_options msg_summary = _('Modified vault "%(value)s"') @@ -994,6 +1193,10 @@ class vault_archive(PKQuery, Local): cli_name='password_file', doc=_('File containing the vault password'), ), + Flag( + 'override_password?', + doc=_('Override existing password'), + ), ) has_output = output.standard_entry @@ -1008,6 +1211,8 @@ class vault_archive(PKQuery, Local): password = options.get('password') password_file = options.get('password_file') + override_password = options.pop('override_password', False) + # don't send these parameters to server if 'data' in options: del options['data'] @@ -1062,15 +1267,19 @@ class vault_archive(PKQuery, Local): password = password.rstrip('\n') else: - password = self.obj.get_existing_password() - - # verify password by retrieving existing data - opts = options.copy() - opts['password'] = password - try: - self.api.Command.vault_retrieve(*args, **opts) - except errors.NotFound: - pass + if override_password: + password = self.obj.get_new_password() + else: + password = self.obj.get_existing_password() + + if not override_password: + # verify password by retrieving existing data + opts = options.copy() + opts['password'] = password + try: + self.api.Command.vault_retrieve(*args, **opts) + except errors.NotFound: + pass salt = vault['ipavaultsalt'][0] diff --git a/ipatests/test_xmlrpc/test_vault_plugin.py b/ipatests/test_xmlrpc/test_vault_plugin.py index 77c515413..483a47410 100644 --- a/ipatests/test_xmlrpc/test_vault_plugin.py +++ b/ipatests/test_xmlrpc/test_vault_plugin.py @@ -37,6 +37,7 @@ asymmetric_vault_name = u'asymmetric_test_vault' secret = ''.join(map(chr, xrange(0, 256))) password = u'password' +other_password = u'other_password' public_key = """ -----BEGIN PUBLIC KEY----- @@ -80,6 +81,48 @@ kUlCMj24a8XsShzYTWBIyW2ngvGe3pQ9PfjkUdm0LGZjYITCBvgOKw== -----END RSA PRIVATE KEY----- """ +other_public_key = """ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv7E/QLVyKjrgDctZ50U7 +rmtL7Ks1QLoccp9WvZJ6WI1rYd0fX5FySS4dI6QTNZc6qww8NeNuZtkoxT9m1wkk +Rl/3wK7fWNLenH/+VHOaTQc20exg7ztfsO7JIsmKmigtticdR5C4jLfjcOp+WjLH +w3zrmrO5SIZ8njxMoDcQJa2vu/t281U/I7ti8ue09FSitIECU05vgmPS+MnXR8HK +PxXqrNkjl29mXNbPiByWwlse3Prwved9I7fwgpiHJqUBFudD/0tZ4DWyLG7t9wM1 +O8gRaRg1r+ENVpmMSvXo4+8+bR3rEYddD5zU7nKXafeuthXlXplae/8uZmCiSI63 +TwIDAQAB +-----END PUBLIC KEY----- +""" + +other_private_key = """ +-----BEGIN RSA PRIVATE KEY----- +MIIEpgIBAAKCAQEAv7E/QLVyKjrgDctZ50U7rmtL7Ks1QLoccp9WvZJ6WI1rYd0f +X5FySS4dI6QTNZc6qww8NeNuZtkoxT9m1wkkRl/3wK7fWNLenH/+VHOaTQc20exg +7ztfsO7JIsmKmigtticdR5C4jLfjcOp+WjLHw3zrmrO5SIZ8njxMoDcQJa2vu/t2 +81U/I7ti8ue09FSitIECU05vgmPS+MnXR8HKPxXqrNkjl29mXNbPiByWwlse3Prw +ved9I7fwgpiHJqUBFudD/0tZ4DWyLG7t9wM1O8gRaRg1r+ENVpmMSvXo4+8+bR3r +EYddD5zU7nKXafeuthXlXplae/8uZmCiSI63TwIDAQABAoIBAQCA+0GFR9F+isjx +Xy+qBpKmxLl8kKKvX8r+cSpLOkEqTlW/rqqKgnI0vVuL/L2UJKKsLvpghBxoBZyC +RCvtatBGrhIlS0UrHg/9m73Ek1hylfUUAQokTn4PrkwWJSgmm/xOATmZSs5ymNTn +yFCmXl69sdNR77YvD5bQXeBtOT+bKXy7yQ1TmYPwwSjL+WSlMV6ZfE3HNVmxPTpk +CTFS638cJblWk9MUIy8HIlhu6If2P4RnHr7ZGGivhREayvs0zXcAfqhIyFHruxSE +yYnmqH9paWjv5mP3YyLoKr+NUvvxnBr/9wCTt0TKgG8G6rpkHuPDLQni9wUGnew8 +QdMgFEohAoGBAPH4vaVB5gDVfvIqwJBsBLHpPq72GvxjrM/exD0jIIpXZxz9gCql +CmC5b1RS1uy8PMoc/RO4CE7UTLaTesciP6LjTD1RhH3rLLJO8/iVC1RXgMrCLHLm +ZQnDhIQGGNQxpvBjQy5ZOWat2dFxYhHN630IFPOtrWsOmJ5HsL1JrjzxAoGBAMrO +R1zNwQ42VbJS6AFshZVjmUV2h3REGh4zG/9IqL0Hz493hyCTGoDPLLXIbtkqNqzQ +XibSZ9RMVPKKTiNQTx91DTgh4Anz8xUr84tA2iAf3ayNWKi3Y3GhmP2EWp1qYeom +kV8Uq0lt4dHZuEo3LuqvbtbzlF9qUXqKS5qy6Tg/AoGBAKCp02o2HjzxhS/QeTmr +r1ZeE7PiTzrECAuh01TwzPtuW1XhcEdgfEqK9cPcmT5pIkflBZkhOcr1pdYYiI5O +TEigeY/BX6KoE251hALLG9GtpCN82DyWhAH+oy9ySOwj5793eTT+I2HtD1LE4SQH +QVQsmJTP/fS2pVl7KnwUvy9RAoGBAKzo2qchNewsHzx+uxgbsnkABfnXaP2T4sDE +yqYJCPTB6BFl02vOf9Y6zN/gF8JH333P2bY3xhaXTgXMLXqmSg+D+NVW7HEP8Lyo +UGj1zgN9p74qdODEGqETKiFb6vYzcW/1mhP6x18/tDz658k+611kXZge7O288+MK +bhNjXrx5AoGBAMox25PcxVgOjCd9+LdUcIOG6LQ971eCH1NKL9YAekICnwMrStbK +veCYju6ok4ZWnMiH8MR1jgC39RWtjJZwynCuPXUP2/vZkoVf1tCZyz7dSm8TdS/2 +5NdOHVy7+NQcEPSm7/FmXdpcR9ZSGAuxMBfnEUibdyz5LdJGnFUN/+HS +-----END RSA PRIVATE KEY----- +""" + class test_vault_plugin(Declarative): @@ -590,6 +633,48 @@ class test_vault_plugin(Declarative): }, }, + { + 'desc': 'Change standard vault to symmetric vault', + 'command': ( + 'vault_mod', + [standard_vault_name], + { + 'ipavaulttype': u'symmetric', + 'new_password': password, + }, + ), + 'expected': { + 'value': standard_vault_name, + 'summary': u'Modified vault "%s"' % standard_vault_name, + 'result': { + 'cn': [standard_vault_name], + 'ipavaulttype': [u'symmetric'], + 'ipavaultsalt': [fuzzy_string], + 'owner_user': [u'admin'], + }, + }, + }, + + { + 'desc': 'Retrieve secret from standard vault converted to ' + 'symmetric vault', + 'command': ( + 'vault_retrieve', + [standard_vault_name], + { + 'password': password, + }, + ), + 'expected': { + 'value': standard_vault_name, + 'summary': 'Retrieved data from vault "%s"' + % standard_vault_name, + 'result': { + 'data': secret, + }, + }, + }, + { 'desc': 'Create symmetric vault', 'command': ( @@ -652,6 +737,90 @@ class test_vault_plugin(Declarative): }, }, + { + 'desc': 'Change symmetric vault password', + 'command': ( + 'vault_mod', + [symmetric_vault_name], + { + 'old_password': password, + 'new_password': other_password, + }, + ), + 'expected': { + 'value': symmetric_vault_name, + 'summary': u'Modified vault "%s"' % symmetric_vault_name, + 'result': { + 'cn': [symmetric_vault_name], + 'ipavaulttype': [u'symmetric'], + 'ipavaultsalt': [fuzzy_string], + 'owner_user': [u'admin'], + }, + }, + }, + + { + 'desc': 'Retrieve secret from symmetric vault with new password', + 'command': ( + 'vault_retrieve', + [symmetric_vault_name], + { + 'password': other_password, + }, + ), + 'expected': { + 'value': symmetric_vault_name, + 'summary': 'Retrieved data from vault "%s"' + % symmetric_vault_name, + 'result': { + 'data': secret, + }, + }, + }, + + { + 'desc': 'Change symmetric vault to asymmetric vault', + 'command': ( + 'vault_mod', + [symmetric_vault_name], + { + 'ipavaulttype': u'asymmetric', + 'old_password': other_password, + 'ipavaultpublickey': public_key, + }, + ), + 'expected': { + 'value': symmetric_vault_name, + 'summary': u'Modified vault "%s"' % symmetric_vault_name, + 'result': { + 'cn': [symmetric_vault_name], + 'ipavaulttype': [u'asymmetric'], + 'ipavaultpublickey': [public_key], + 'owner_user': [u'admin'], + }, + }, + }, + + { + 'desc': 'Retrieve secret from symmetric vault converted to ' + 'asymmetric vault', + 'command': ( + 'vault_retrieve', + [symmetric_vault_name], + { + 'private_key': private_key, + }, + ), + 'expected': { + 'value': symmetric_vault_name, + 'summary': 'Retrieved data from vault "%s"' + % symmetric_vault_name, + 'result': { + 'data': secret, + }, + }, + }, + { 'desc': 'Create asymmetric vault', 'command': ( @@ -713,4 +882,84 @@ class test_vault_plugin(Declarative): }, }, + { + 'desc': 'Change asymmetric vault keys', + 'command': ( + 'vault_mod', + [asymmetric_vault_name], + { + 'private_key': private_key, + 'ipavaultpublickey': other_public_key, + }, + ), + 'expected': { + 'value': asymmetric_vault_name, + 'summary': u'Modified vault "%s"' % asymmetric_vault_name, + 'result': { + 'cn': [asymmetric_vault_name], + 'ipavaulttype': [u'asymmetric'], + 'ipavaultpublickey': [other_public_key], + 'owner_user': [u'admin'], + }, + }, + }, + + { + 'desc': 'Retrieve secret from asymmetric vault with new keys', + 'command': ( + 'vault_retrieve', + [asymmetric_vault_name], + { + 'private_key': other_private_key, + }, + ), + 'expected': { + 'value': asymmetric_vault_name, + 'summary': 'Retrieved data from vault "%s"' + % asymmetric_vault_name, + 'result': { + 'data': secret, + }, + }, + }, + + { + 'desc': 'Change asymmetric vault to standard vault', + 'command': ( + 'vault_mod', + [asymmetric_vault_name], + { + 'ipavaulttype': u'standard', + 'private_key': other_private_key, + }, + ), + 'expected': { + 'value': asymmetric_vault_name, + 'summary': u'Modified vault "%s"' % asymmetric_vault_name, + 'result': { + 'cn': [asymmetric_vault_name], + 'ipavaulttype': [u'standard'], + 'owner_user': [u'admin'], + }, + }, + }, + + { + 'desc': 'Retrieve secret from asymmetric vault converted to ' + 'standard vault', + 'command': ( + 'vault_retrieve', + [asymmetric_vault_name], + {}, + ), + 'expected': { + 'value': asymmetric_vault_name, + 'summary': 'Retrieved data from vault "%s"' + % asymmetric_vault_name, + 'result': { + 'data': secret, + }, + }, + }, + ] -- cgit