diff options
-rw-r--r-- | API.txt | 52 | ||||
-rw-r--r-- | VERSION | 4 | ||||
-rw-r--r-- | freeipa.spec.in | 2 | ||||
-rw-r--r-- | install/share/60basev3.ldif | 4 | ||||
-rw-r--r-- | ipalib/plugins/vault.py | 583 | ||||
-rw-r--r-- | ipatests/test_xmlrpc/test_vault_plugin.py | 221 |
6 files changed, 794 insertions, 72 deletions
@@ -5408,11 +5408,16 @@ output: Output('result', <type 'bool'>, None) output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None) output: PrimaryKey('value', None, None) command: vault_add -args: 1,9,3 -arg: Str('cn', attribute=True, cli_name='name', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.-]+$', primary_key=True, required=True) +args: 1,14,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') option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui') -option: Str('description', attribute=True, cli_name='desc', multivalue=False, required=False) +option: Str('description?', cli_name='desc') +option: Bytes('ipapublickey?', cli_name='public_key') +option: Str('ipavaulttype?', cli_name='type') +option: Str('password?', cli_name='password') +option: Str('password_file?', cli_name='password_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: Str('setattr*', cli_name='setattr', exclude='webui') @@ -5422,12 +5427,30 @@ 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_add_internal +args: 1,10,3 +arg: Str('cn', attribute=True, cli_name='name', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.-]+$', primary_key=True, required=True) +option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui') +option: Str('description', attribute=True, cli_name='desc', multivalue=False, required=False) +option: Bytes('ipapublickey', attribute=True, cli_name='public_key', multivalue=False, required=False) +option: Bytes('ipavaultsalt', attribute=True, cli_name='salt', multivalue=False, required=False) +option: Str('ipavaulttype', attribute=True, autofill=True, cli_name='type', default=u'standard', multivalue=False, required=False) +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 -args: 1,8,3 +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: Bytes('data?') option: Str('in?') +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') option: Str('service?') option: Flag('shared?', autofill=True, default=False) @@ -5436,11 +5459,10 @@ 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 +command: vault_archive_internal +args: 1,9,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?') @@ -5464,11 +5486,12 @@ output: Output('result', <type 'dict'>, None) output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None) output: ListOfPrimaryKeys('value', None, None) command: vault_find -args: 1,11,4 +args: 1,12,4 arg: Str('criteria?', noextrawhitespace=False) option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui') option: Str('cn', attribute=True, autofill=False, cli_name='name', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.-]+$', primary_key=True, query=True, required=False) option: Str('description', attribute=True, autofill=False, cli_name='desc', multivalue=False, query=True, required=False) +option: Str('ipavaulttype', attribute=True, autofill=False, cli_name='type', default=u'standard', multivalue=False, query=True, required=False) option: Flag('pkey_only?', autofill=True, default=False) option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui') option: Str('service?') @@ -5482,12 +5505,15 @@ output: ListOfEntries('result', (<type 'list'>, <type 'tuple'>), Gettext('A list output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None) output: Output('truncated', <type 'bool'>, None) command: vault_mod -args: 1,11,3 +args: 1,14,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') option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui') option: Str('delattr*', cli_name='delattr', exclude='webui') option: Str('description', attribute=True, autofill=False, cli_name='desc', multivalue=False, required=False) +option: Bytes('ipapublickey', attribute=True, autofill=False, cli_name='public_key', multivalue=False, required=False) +option: Bytes('ipavaultsalt', attribute=True, autofill=False, cli_name='salt', multivalue=False, required=False) +option: Str('ipavaulttype', attribute=True, autofill=False, cli_name='type', default=u'standard', multivalue=False, required=False) option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui') option: Flag('rights', autofill=True, default=False) option: Str('service?') @@ -5499,10 +5525,14 @@ output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDA output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None) output: PrimaryKey('value', None, None) command: vault_retrieve -args: 1,7,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: Str('out?') +option: Str('password?', cli_name='password') +option: Str('password_file?', cli_name='password_file') +option: Bytes('private_key?', cli_name='private_key') +option: Str('private_key_file?', cli_name='private_key_file') option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui') option: Str('service?') option: Flag('shared?', autofill=True, default=False) @@ -5511,7 +5541,7 @@ 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 +command: vault_retrieve_internal 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') @@ -90,5 +90,5 @@ IPA_DATA_VERSION=20100614120000 # # ######################################################## IPA_API_VERSION_MAJOR=2 -IPA_API_VERSION_MINOR=137 -# Last change: mbabinsk: Commands to manage user/host/service certificates +IPA_API_VERSION_MINOR=138 +# Last change: edewata - added symmetric and asymmetric vaults diff --git a/freeipa.spec.in b/freeipa.spec.in index 52af50dd0..8fee33bd9 100644 --- a/freeipa.spec.in +++ b/freeipa.spec.in @@ -68,6 +68,7 @@ BuildRequires: python-ldap BuildRequires: python-setuptools BuildRequires: python-krbV BuildRequires: python-nss +BuildRequires: python-cryptography BuildRequires: python-netaddr BuildRequires: python-kerberos >= 1.1-14 BuildRequires: python-rhsm @@ -293,6 +294,7 @@ Requires: iproute Requires: keyutils Requires: pyOpenSSL Requires: python-nss >= 0.16 +Requires: python-cryptography Requires: python-lxml Requires: python-netaddr Requires: libipa_hbac-python diff --git a/install/share/60basev3.ldif b/install/share/60basev3.ldif index 33f4804e3..cb159db05 100644 --- a/install/share/60basev3.ldif +++ b/install/share/60basev3.ldif @@ -56,6 +56,8 @@ attributeTypes: (2.16.840.1.113730.3.8.11.64 NAME 'ipaSecretKeyRef' DESC 'DN of attributeTypes: (2.16.840.1.113730.3.8.11.65 NAME 'ipaWrappingMech' DESC 'PKCS#11 wrapping mechanism equivalent to CK_MECHANISM_TYPE' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE X-ORIGIN 'IPA v4.1') attributeTypes: (2.16.840.1.113730.3.8.11.70 NAME 'ipaPermTargetTo' DESC 'Destination location to move an entry IPA permission ACI' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE X-ORIGIN 'IPA v4.0' ) attributeTypes: (2.16.840.1.113730.3.8.11.71 NAME 'ipaPermTargetFrom' DESC 'Source location from where moving an entry IPA permission ACI' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE X-ORIGIN 'IPA v4.0' ) +attributeTypes: (2.16.840.1.113730.3.8.18.2.1 NAME 'ipaVaultType' DESC 'IPA vault type' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'IPA v4.2') +attributeTypes: (2.16.840.1.113730.3.8.18.2.2 NAME 'ipaVaultSalt' DESC 'IPA vault salt' EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 X-ORIGIN 'IPA v4.2' ) objectClasses: (2.16.840.1.113730.3.8.12.1 NAME 'ipaExternalGroup' SUP top STRUCTURAL MUST ( cn ) MAY ( ipaExternalMember $ memberOf $ description $ owner) X-ORIGIN 'IPA v3' ) objectClasses: (2.16.840.1.113730.3.8.12.2 NAME 'ipaNTUserAttrs' SUP top AUXILIARY MUST ( ipaNTSecurityIdentifier ) MAY ( ipaNTHash $ ipaNTLogonScript $ ipaNTProfilePath $ ipaNTHomeDirectory $ ipaNTHomeDirectoryDrive ) X-ORIGIN 'IPA v3' ) objectClasses: (2.16.840.1.113730.3.8.12.3 NAME 'ipaNTGroupAttrs' SUP top AUXILIARY MUST ( ipaNTSecurityIdentifier ) X-ORIGIN 'IPA v3' ) @@ -79,4 +81,4 @@ objectClasses: (2.16.840.1.113730.3.8.12.24 NAME 'ipaPublicKeyObject' DESC 'Wrap objectClasses: (2.16.840.1.113730.3.8.12.25 NAME 'ipaPrivateKeyObject' DESC 'Wrapped private keys' SUP top AUXILIARY MUST ( ipaPrivateKey $ ipaWrappingKey $ ipaWrappingMech ) X-ORIGIN 'IPA v4.1' ) objectClasses: (2.16.840.1.113730.3.8.12.26 NAME 'ipaSecretKeyObject' DESC 'Wrapped secret keys' SUP top AUXILIARY MUST ( ipaSecretKey $ ipaWrappingKey $ ipaWrappingMech ) X-ORIGIN 'IPA v4.1' ) objectClasses: (2.16.840.1.113730.3.8.12.34 NAME 'ipaSecretKeyRefObject' DESC 'Indirect storage for encoded key material' SUP top AUXILIARY MUST ( ipaSecretKeyRef ) X-ORIGIN 'IPA v4.1' ) -objectClasses: (2.16.840.1.113730.3.8.18.1.1 NAME 'ipaVault' DESC 'IPA vault' SUP top STRUCTURAL MUST ( cn ) MAY ( description ) X-ORIGIN 'IPA v4.2' ) +objectClasses: (2.16.840.1.113730.3.8.18.1.1 NAME 'ipaVault' DESC 'IPA vault' SUP top STRUCTURAL MUST ( cn ) MAY ( description $ ipaVaultType $ ipaVaultSalt $ ipaPublicKey ) X-ORIGIN 'IPA v4.2' ) diff --git a/ipalib/plugins/vault.py b/ipalib/plugins/vault.py index f80ecfdfa..193fa5cbb 100644 --- a/ipalib/plugins/vault.py +++ b/ipalib/plugins/vault.py @@ -18,11 +18,20 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import base64 +import getpass import json import os import sys import tempfile +from cryptography.fernet import Fernet, InvalidToken +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC +from cryptography.hazmat.primitives.asymmetric import padding +from cryptography.hazmat.primitives.serialization import load_pem_public_key,\ + load_pem_private_key + import nss.nss as nss import krbV @@ -50,6 +59,36 @@ Vaults """) + _(""" Manage vaults. """) + _(""" +Vault is a secure place to store a secret. +""") + _(""" +Based on the ownership there are three vault categories: +* user/private vault +* service vault +* shared vault +""") + _(""" +User vaults are vaults owned used by a particular user. Private +vaults are vaults owned the current user. Service vaults are +vaults owned by a service. Shared vaults are owned by the admin +but they can be used by other users or services. +""") + _(""" +Based on the security mechanism there are three types of +vaults: +* standard vault +* symmetric vault +* asymmetric vault +""") + _(""" +Standard vault uses a secure mechanism to transport and +store the secret. The secret can only be retrieved by users +that have access to the vault. +""") + _(""" +Symmetric vault is similar to the standard vault, but it +pre-encrypts the secret using a password before transport. +The secret can only be retrieved using the same password. +""") + _(""" +Asymmetric vault is similar to the standard vault, but it +pre-encrypts the secret using a public key before transport. +The secret can only be retrieved using the private key. +""") + _(""" EXAMPLES: """) + _(""" List private vaults: @@ -76,6 +115,12 @@ EXAMPLES: Add a user vault: ipa vault-add <name> --user <username> """) + _(""" + Add a symmetric vault: + ipa vault-add <name> --type symmetric --password-file password.txt +""") + _(""" + Add an asymmetric vault: + ipa vault-add <name> --type asymmetric --public-key-file public.pem +""") + _(""" Show a private vault: ipa vault-show <name> """) + _(""" @@ -113,7 +158,7 @@ EXAMPLES: ipa vault-del <name> --user <username> """) + _(""" Display vault configuration: - ipa vault-config + ipa vaultconfig-show """) + _(""" Archive data into private vault: ipa vault-archive <name> --in <input file> @@ -127,6 +172,12 @@ EXAMPLES: Archive data into user vault: ipa vault-archive <name> --user <username> --in <input file> """) + _(""" + Archive data into symmetric vault: + ipa vault-archive <name> --in <input file> +""") + _(""" + Archive data into asymmetric vault: + ipa vault-archive <name> --in <input file> +""") + _(""" Retrieve data from private vault: ipa vault-retrieve <name> --out <output file> """) + _(""" @@ -137,7 +188,13 @@ EXAMPLES: ipa vault-retrieve <name> --shared --out <output file> """) + _(""" Retrieve data from user vault: - ipa vault-retrieve <name> --user <user name> --out <output file> + ipa vault-retrieve <name> --user <username> --out <output file> +""") + _(""" + Retrieve data from symmetric vault: + ipa vault-retrieve <name> --out data.bin +""") + _(""" + Retrieve data from asymmetric vault: + ipa vault-retrieve <name> --out data.bin --private-key-file private.pem """) register = Registry() @@ -146,7 +203,7 @@ register = Registry() vault_options = ( Str( 'service?', - doc=_('Service name'), + doc=_('Service name of the service vault'), ), Flag( 'shared?', @@ -154,7 +211,7 @@ vault_options = ( ), Str( 'user?', - doc=_('Username'), + doc=_('Username of the user vault'), ), ) @@ -174,6 +231,14 @@ class vault(LDAPObject): default_attributes = [ 'cn', 'description', + 'ipavaulttype', + 'ipavaultsalt', + 'ipapublickey', + ] + search_display_attributes = [ + 'cn', + 'description', + 'ipavaulttype', ] label = _('Vaults') @@ -195,6 +260,28 @@ class vault(LDAPObject): label=_('Description'), doc=_('Vault description'), ), + Str( + 'ipavaulttype?', + cli_name='type', + label=_('Type'), + doc=_('Vault type'), + default=u'standard', + autofill=True, + ), + Bytes( + 'ipavaultsalt?', + cli_name='salt', + label=_('Salt'), + doc=_('Vault salt'), + flags=['no_search'], + ), + Bytes( + 'ipapublickey?', + cli_name='public_key', + label=_('Public key'), + doc=_('Vault public key'), + flags=['no_search'], + ), ) def get_dn(self, *keys, **options): @@ -307,12 +394,232 @@ class vault(LDAPObject): return 'ipa:' + id + def get_new_password(self): + """ + Gets new password from user and verify it. + """ + while True: + password = getpass.getpass('New password: ').decode( + sys.stdin.encoding) + password2 = getpass.getpass('Verify password: ').decode( + sys.stdin.encoding) + + if password == password2: + return password + + print ' ** Passwords do not match! **' + + def get_existing_password(self, new=False): + """ + Gets existing password from user. + """ + return getpass.getpass('Password: ').decode(sys.stdin.encoding) + + def generate_symmetric_key(self, password, salt): + """ + Generates symmetric key from password and salt. + """ + kdf = PBKDF2HMAC( + algorithm=hashes.SHA256(), + length=32, + salt=salt, + iterations=100000, + backend=default_backend() + ) + + return base64.b64encode(kdf.derive(password.encode('utf-8'))) + + def encrypt(self, data, symmetric_key=None, public_key=None): + """ + Encrypts data with symmetric key or public key. + """ + if symmetric_key: + fernet = Fernet(symmetric_key) + return fernet.encrypt(data) + + elif public_key: + rsa_public_key = load_pem_public_key( + data=public_key, + backend=default_backend() + ) + return rsa_public_key.encrypt( + data, + padding.OAEP( + mgf=padding.MGF1(algorithm=hashes.SHA1()), + algorithm=hashes.SHA1(), + label=None + ) + ) + + def decrypt(self, data, symmetric_key=None, private_key=None): + """ + Decrypts data with symmetric key or public key. + """ + if symmetric_key: + try: + fernet = Fernet(symmetric_key) + return fernet.decrypt(data) + except InvalidToken: + raise errors.AuthenticationError( + message=_('Invalid credentials')) + + elif private_key: + try: + rsa_private_key = load_pem_private_key( + data=private_key, + password=None, + backend=default_backend() + ) + return rsa_private_key.decrypt( + data, + padding.OAEP( + mgf=padding.MGF1(algorithm=hashes.SHA1()), + algorithm=hashes.SHA1(), + label=None + ) + ) + except AssertionError: + raise errors.AuthenticationError( + message=_('Invalid credentials')) + @register() -class vault_add(LDAPCreate): +class vault_add(PKQuery, Local): __doc__ = _('Create a new vault.') - takes_options = LDAPCreate.takes_options + vault_options + takes_options = LDAPCreate.takes_options + vault_options + ( + Str( + 'description?', + cli_name='desc', + doc=_('Vault description'), + ), + Str( + 'ipavaulttype?', + cli_name='type', + doc=_('Vault type'), + ), + Str( + 'password?', + cli_name='password', + doc=_('Vault password'), + ), + Str( # TODO: use File parameter + 'password_file?', + cli_name='password_file', + doc=_('File containing the vault password'), + ), + Bytes( + 'ipapublickey?', + cli_name='public_key', + doc=_('Vault public key'), + ), + Str( # TODO: use File parameter + 'public_key_file?', + cli_name='public_key_file', + doc=_('File containing the vault public key'), + ), + ) + + has_output = output.standard_entry + + def forward(self, *args, **options): + + vault_type = options.get('ipavaulttype', u'standard') + password = options.get('password') + password_file = options.get('password_file') + public_key = options.get('ipapublickey') + public_key_file = options.get('public_key_file') + + # don't send these parameters to server + if 'password' in options: + del options['password'] + if 'password_file' in options: + del options['password_file'] + if 'public_key_file' in options: + del options['public_key_file'] + + 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()) + + if vault_type == u'standard': + + pass + + elif vault_type == u'symmetric': + + # get password + if password and password_file: + raise errors.MutuallyExclusiveError( + reason=_('Password specified multiple times')) + + elif password: + pass + + elif password_file: + with open(password_file, 'rb') as f: + password = f.read().rstrip('\n').decode('utf-8') + + else: + password = self.obj.get_new_password() + + # generate vault salt + options['ipavaultsalt'] = os.urandom(16) + + elif vault_type == u'asymmetric': + + # get new vault public key + if public_key and public_key_file: + raise errors.MutuallyExclusiveError( + reason=_('Public key specified multiple times')) + + elif public_key: + pass + + elif public_key_file: + with open(public_key_file, 'rb') as f: + public_key = f.read() + + # store vault public key + options['ipapublickey'] = public_key + + else: + raise errors.ValidationError( + name='ipapublickey', + error=_('Missing vault public key')) + + # create vault + response = self.api.Command.vault_add_internal(*args, **options) + + # prepare parameters for archival + opts = options.copy() + if 'description' in opts: + del opts['description'] + if 'ipavaulttype' in opts: + del opts['ipavaulttype'] + + if vault_type == u'symmetric': + opts['password'] = password + del opts['ipavaultsalt'] + + elif vault_type == u'asymmetric': + del opts['ipapublickey'] + + # archive blank data + self.api.Command.vault_archive(*args, **opts) + + return response + + +@register() +class vault_add_internal(LDAPCreate): + + NO_CLI = True + + takes_options = vault_options msg_summary = _('Added vault "%(value)s"') @@ -513,29 +820,46 @@ class vault_archive(PKQuery, Local): 'in?', doc=_('File containing data to archive'), ), + Str( + 'password?', + cli_name='password', + doc=_('Vault password'), + ), + Str( # TODO: use File parameter + 'password_file?', + cli_name='password_file', + doc=_('File containing the vault password'), + ), ) has_output = output.standard_entry - msg_summary = _('Archived data into vault "%(value)s"') - def forward(self, *args, **options): + name = args[-1] + data = options.get('data') input_file = options.get('in') + password = options.get('password') + password_file = options.get('password_file') + # don't send these parameters to server if 'data' in options: del options['data'] if 'in' in options: del options['in'] + if 'password' in options: + del options['password'] + if 'password_file' in options: + del options['password_file'] # get data if data and input_file: raise errors.MutuallyExclusiveError( reason=_('Input data specified multiple times')) - if input_file: + elif input_file: with open(input_file, 'rb') as f: data = f.read() @@ -549,13 +873,77 @@ class vault_archive(PKQuery, Local): if not backend.isconnected(): backend.connect(ccache=krbV.default_context().default_ccache()) + # retrieve vault info + vault = self.api.Command.vault_show(*args, **options)['result'] + + vault_type = vault['ipavaulttype'][0] + + if vault_type == u'standard': + + encrypted_key = None + + elif vault_type == u'symmetric': + + # get password + if password and password_file: + raise errors.MutuallyExclusiveError( + reason=_('Password specified multiple times')) + + elif password: + pass + + elif password_file: + with open(password_file) as f: + password = f.read().rstrip('\n').decode('utf-8') + + 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 + + salt = vault['ipavaultsalt'][0] + + # generate encryption key from vault password + encryption_key = self.obj.generate_symmetric_key( + password, salt) + + # encrypt data with encryption key + data = self.obj.encrypt(data, symmetric_key=encryption_key) + + encrypted_key = None + + elif vault_type == u'asymmetric': + + public_key = vault['ipapublickey'][0].encode('utf-8') + + # generate encryption key + encryption_key = base64.b64encode(os.urandom(32)) + + # encrypt data with encryption key + data = self.obj.encrypt(data, symmetric_key=encryption_key) + + # encrypt encryption key with public key + encrypted_key = self.obj.encrypt( + encryption_key, public_key=public_key) + + else: + raise errors.ValidationError( + name='vault_type', + error=_('Invalid vault type')) + # 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'] + config = self.api.Command.vaultconfig_show()['result'] + transport_cert_der = config['transport_cert'] nss_transport_cert = nss.Certificate(transport_cert_der) # generate session key @@ -579,6 +967,10 @@ class vault_archive(PKQuery, Local): vault_data = {} vault_data[u'data'] = base64.b64encode(data).decode('utf-8') + if encrypted_key: + vault_data[u'encrypted_key'] = base64.b64encode(encrypted_key)\ + .decode('utf-8') + json_vault_data = json.dumps(vault_data) # wrap vault_data with session key @@ -595,16 +987,12 @@ class vault_archive(PKQuery, Local): options['vault_data'] = wrapped_vault_data - response = self.api.Command.vault_archive_encrypted(*args, **options) - - response['result'] = {} - del response['summary'] - - return response + return self.api.Command.vault_archive_internal(*args, **options) @register() -class vault_archive_encrypted(Update): +class vault_archive_internal(PKQuery): + NO_CLI = True takes_options = vault_options + ( @@ -622,6 +1010,10 @@ class vault_archive_encrypted(Update): ), ) + has_output = output.standard_entry + + msg_summary = _('Archived data into vault "%(value)s"') + def execute(self, *args, **options): if not self.api.Command.kra_is_enabled()['result']: @@ -633,8 +1025,7 @@ class vault_archive_encrypted(Update): wrapped_session_key = options.pop('session_key') # retrieve vault info - result = self.api.Command.vault_show(*args, **options) - vault = result['result'] + vault = self.api.Command.vault_show(*args, **options)['result'] # connect to KRA kra_client = self.api.Backend.kra.get_client() @@ -666,7 +1057,14 @@ class vault_archive_encrypted(Update): kra_account.logout() - return result + response = { + 'value': args[-1], + 'result': {}, + } + + response['summary'] = self.msg_summary % response + + return response @register() @@ -678,6 +1076,26 @@ class vault_retrieve(PKQuery, Local): 'out?', doc=_('File to store retrieved data'), ), + Str( + 'password?', + cli_name='password', + doc=_('Vault password'), + ), + Str( # TODO: use File parameter + 'password_file?', + cli_name='password_file', + doc=_('File containing the vault password'), + ), + Bytes( + 'private_key?', + cli_name='private_key', + doc=_('Vault private key'), + ), + Str( # TODO: use File parameter + 'private_key_file?', + cli_name='private_key_file', + doc=_('File containing the vault private key'), + ), ) has_output = output.standard_entry @@ -688,15 +1106,28 @@ class vault_retrieve(PKQuery, Local): ), ) - msg_summary = _('Retrieved data from vault "%(value)s"') - def forward(self, *args, **options): + name = args[-1] + output_file = options.get('out') + password = options.get('password') + password_file = options.get('password_file') + private_key = options.get('private_key') + private_key_file = options.get('private_key_file') + # don't send these parameters to server if 'out' in options: del options['out'] + if 'password' in options: + del options['password'] + if 'password_file' in options: + del options['password_file'] + if 'private_key' in options: + del options['private_key'] + if 'private_key_file' in options: + del options['private_key_file'] if self.api.env.in_server: backend = self.api.Backend.ldap2 @@ -705,13 +1136,18 @@ class vault_retrieve(PKQuery, Local): if not backend.isconnected(): backend.connect(ccache=krbV.default_context().default_ccache()) + # retrieve vault info + vault = self.api.Command.vault_show(*args, **options)['result'] + + vault_type = vault['ipavaulttype'][0] + # 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'] + config = self.api.Command.vaultconfig_show()['result'] + transport_cert_der = config['transport_cert'] nss_transport_cert = nss.Certificate(transport_cert_der) # generate session key @@ -729,7 +1165,7 @@ class vault_retrieve(PKQuery, Local): # send retrieval request to server options['session_key'] = wrapped_session_key.data - response = self.api.Command.vault_retrieve_encrypted(*args, **options) + response = self.api.Command.vault_retrieve_internal(*args, **options) result = response['result'] nonce = result['nonce'] @@ -751,18 +1187,85 @@ class vault_retrieve(PKQuery, Local): vault_data = json.loads(json_vault_data) data = base64.b64decode(vault_data[u'data'].encode('utf-8')) + encrypted_key = None + + if 'encrypted_key' in vault_data: + encrypted_key = base64.b64decode(vault_data[u'encrypted_key'] + .encode('utf-8')) + + if vault_type == u'standard': + + pass + + elif vault_type == u'symmetric': + + salt = vault['ipavaultsalt'][0] + + # get encryption key from vault password + if password and password_file: + raise errors.MutuallyExclusiveError( + reason=_('Password specified multiple times')) + + elif password: + pass + + elif password_file: + with open(password_file) as f: + password = f.read().rstrip('\n').decode('utf-8') + + else: + password = self.obj.get_existing_password() + + # generate encryption key from password + encryption_key = self.obj.generate_symmetric_key(password, salt) + + # decrypt data with encryption key + data = self.obj.decrypt(data, symmetric_key=encryption_key) + + elif vault_type == u'asymmetric': + + # get encryption key with vault private key + if private_key and private_key_file: + raise errors.MutuallyExclusiveError( + reason=_('Private key specified multiple times')) + + elif private_key: + pass + + elif private_key_file: + with open(private_key_file, 'rb') as f: + private_key = f.read() + + else: + raise errors.ValidationError( + name='private_key', + error=_('Missing vault private key')) + + # decrypt encryption key with private key + encryption_key = self.obj.decrypt( + encrypted_key, private_key=private_key) + + # decrypt data with encryption key + data = self.obj.decrypt(data, symmetric_key=encryption_key) + + else: + raise errors.ValidationError( + name='vault_type', + error=_('Invalid vault type')) + if output_file: with open(output_file, 'w') as f: f.write(data) - response['result'] = {'data': data} - del response['summary'] + else: + response['result'] = {'data': data} return response @register() -class vault_retrieve_encrypted(Retrieve): +class vault_retrieve_internal(PKQuery): + NO_CLI = True takes_options = vault_options + ( @@ -772,6 +1275,10 @@ class vault_retrieve_encrypted(Retrieve): ), ) + has_output = output.standard_entry + + msg_summary = _('Retrieved data from vault "%(value)s"') + def execute(self, *args, **options): if not self.api.Command.kra_is_enabled()['result']: @@ -781,8 +1288,7 @@ class vault_retrieve_encrypted(Retrieve): wrapped_session_key = options.pop('session_key') # retrieve vault info - result = self.api.Command.vault_show(*args, **options) - vault = result['result'] + vault = self.api.Command.vault_show(*args, **options)['result'] # connect to KRA kra_client = self.api.Backend.kra.get_client() @@ -807,12 +1313,19 @@ class vault_retrieve_encrypted(Retrieve): key_info.get_key_id(), wrapped_session_key) - vault['vault_data'] = key.encrypted_data - vault['nonce'] = key.nonce_data - kra_account.logout() - return result + response = { + 'value': args[-1], + 'result': { + 'vault_data': key.encrypted_data, + 'nonce': key.nonce_data, + }, + } + + response['summary'] = self.msg_summary % response + + return response @register() diff --git a/ipatests/test_xmlrpc/test_vault_plugin.py b/ipatests/test_xmlrpc/test_vault_plugin.py index 9a40547b1..f8b57855a 100644 --- a/ipatests/test_xmlrpc/test_vault_plugin.py +++ b/ipatests/test_xmlrpc/test_vault_plugin.py @@ -22,15 +22,63 @@ Test the `ipalib/plugins/vault.py` module. """ from ipalib import api, errors -from xmlrpc_test import Declarative +from xmlrpc_test import Declarative, fuzzy_string vault_name = u'test_vault' service_name = u'HTTP/server.example.com' user_name = u'testuser' +standard_vault_name = u'standard_test_vault' +symmetric_vault_name = u'symmetric_test_vault' +asymmetric_vault_name = u'asymmetric_test_vault' + # binary data from \x00 to \xff secret = ''.join(map(chr, xrange(0, 256))) +password = u'password' + +public_key = """ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnT61EFxUOQgCJdM0tmw/ +pRRPDPGchTClnU1eBtiQD3ItKYf1+weMGwGOSJXPtkto7NlE7Qs8WHAr0UjyeBDe +k/zeB6nSVdk47OdaW1AHrJL+44r238Jbm/+7VO5lTu6Z4N5p0VqoWNLi0Uh/CkqB +tsxXaaAgjMp0AGq2U/aO/akeEYWQOYIdqUKVgAEKX5MmIA8tmbmoYIQ+B4Q3vX7N +otG4eR6c2o9Fyjd+M4Gai5Ce0fSrigRvxAYi8xpRkQ5yQn5gf4WVrn+UKTfOIjLO +pVThop+Xivcre3SpI0kt6oZPhBw9i8gbMnqifVmGFpVdhq+QVBqp+MVJvTbhRPG6 +3wIDAQAB +-----END PUBLIC KEY----- +""" + +private_key = """ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAnT61EFxUOQgCJdM0tmw/pRRPDPGchTClnU1eBtiQD3ItKYf1 ++weMGwGOSJXPtkto7NlE7Qs8WHAr0UjyeBDek/zeB6nSVdk47OdaW1AHrJL+44r2 +38Jbm/+7VO5lTu6Z4N5p0VqoWNLi0Uh/CkqBtsxXaaAgjMp0AGq2U/aO/akeEYWQ +OYIdqUKVgAEKX5MmIA8tmbmoYIQ+B4Q3vX7NotG4eR6c2o9Fyjd+M4Gai5Ce0fSr +igRvxAYi8xpRkQ5yQn5gf4WVrn+UKTfOIjLOpVThop+Xivcre3SpI0kt6oZPhBw9 +i8gbMnqifVmGFpVdhq+QVBqp+MVJvTbhRPG63wIDAQABAoIBAQCD2bXnfxPcMnvi +jaPwpvoDCPF0EBBHmk/0g5ApO2Qon3uBDJFUqbJwXrCY6o2d9MOJfnGONlKmcYA8 +X+d4h+SqwGjIkjxdYeSauS+Jy6Rzr1ptH/P8EjPQrfG9uJxYQDflV3nxYwwwVrx7 +8kccMPdteRB+8Bb7FzOHufMimmayCNFETnVT5CKH2PrYoPB+fr0itCipWOenDp33 +e73OV+K9U3rclmtHaoRxGohqByKfQRUkipjw4m+T3qfZZc5eN77RGW8J+oL1GVom +fwtiH7N1HVte0Dmd13nhiASg355kjqRPcIMPsRHvXkOpgg5HRUTKG5elqAyvvm27 +Fzj1YdeRAoGBAMnE61+FYh8qCyEGe8r6RGjO8iuoyk1t+0gBWbmILLBiRnj4K8Tc +k7HBG/pg3XCNbCuRwiLg8tk3VAAXzn6o+IJr3QnKbNCGa1lKfYU4mt11sBEyuL5V +NpZcZ8IiPhMlGyDA9cFbTMKOE08RqbOIdxOmTizFt0R5sYZAwOjEvBIZAoGBAMeC +N/P0bdrScFZGeS51wEdiWme/CO0IyGoqU6saI8L0dbmMJquiaAeIEjIKLqxH1RON +axhsyk97e0PCcc5QK62Utf50UUAbL/v7CpIG+qdSRYDO4bVHSCkwF32N3pYh/iVU +EsEBEkZiJi0dWa/0asDbsACutxcHda3RI5pi7oO3AoGAcbGNs/CUHt1xEfX2UaT+ +YVSjb2iYPlNH8gYYygvqqqVl8opdF3v3mYUoP8jPXrnCBzcF/uNk1HNx2O+RQxvx +lIQ1NGwlLsdfvBvWaPhBg6LqSHadVVrs/IMrUGA9PEp/Y9B3arIIqeSnCrn4Nxsh +higDCwWKRIKSPwVD7qXVGBkCgYEAu5/CASIRIeYgEXMLSd8hKcDcJo8o1MoauIT/ +1Hyrvw9pm0qrn2QHk3WrLvYWeJzBTTcEzZ6aEG+fN9UodA8/VGnzUc6QDsrCsKWh +hj0cArlDdeSZrYLQ4TNCFCiUePqU6QQM8weP6TMqlejxTKF+t8qi1bF5rCWuzP1P +D0UU7DcCgYAUvmEGckugS+FTatop8S/rmkcQ4Bf5M/YCZfsySavucDiHcBt0QtXt +Swh0XdDsYS3W1yj2XqqsQ7R58KNaffCHjjulWFzb5IiuSvvdxzWtiXHisOpO36MJ +kUlCMj24a8XsShzYTWBIyW2ngvGe3pQ9PfjkUdm0LGZjYITCBvgOKw== +-----END RSA PRIVATE KEY----- +""" + class test_vault_plugin(Declarative): @@ -42,6 +90,9 @@ class test_vault_plugin(Declarative): }), ('vault_del', [vault_name], {'shared': True, 'continue': True}), ('vault_del', [vault_name], {'user': user_name, 'continue': True}), + ('vault_del', [standard_vault_name], {'continue': True}), + ('vault_del', [symmetric_vault_name], {'continue': True}), + ('vault_del', [asymmetric_vault_name], {'continue': True}), ] tests = [ @@ -61,6 +112,7 @@ class test_vault_plugin(Declarative): % (vault_name, api.env.basedn), 'objectclass': [u'top', u'ipaVault'], 'cn': [vault_name], + 'ipavaulttype': [u'standard'], }, }, }, @@ -81,6 +133,7 @@ class test_vault_plugin(Declarative): 'dn': u'cn=%s,cn=admin,cn=users,cn=vaults,cn=kra,%s' % (vault_name, api.env.basedn), 'cn': [vault_name], + 'ipavaulttype': [u'standard'], }, ], }, @@ -100,6 +153,7 @@ class test_vault_plugin(Declarative): 'dn': u'cn=%s,cn=admin,cn=users,cn=vaults,cn=kra,%s' % (vault_name, api.env.basedn), 'cn': [vault_name], + 'ipavaulttype': [u'standard'], }, }, }, @@ -119,6 +173,7 @@ class test_vault_plugin(Declarative): 'result': { 'cn': [vault_name], 'description': [u'Test vault'], + 'ipavaulttype': [u'standard'], }, }, }, @@ -156,6 +211,7 @@ class test_vault_plugin(Declarative): % (vault_name, service_name, api.env.basedn), 'objectclass': [u'top', u'ipaVault'], 'cn': [vault_name], + 'ipavaulttype': [u'standard'], }, }, }, @@ -178,6 +234,7 @@ class test_vault_plugin(Declarative): 'dn': u'cn=%s,cn=%s,cn=services,cn=vaults,cn=kra,%s' % (vault_name, service_name, api.env.basedn), 'cn': [vault_name], + 'ipavaulttype': [u'standard'], }, ], }, @@ -199,6 +256,7 @@ class test_vault_plugin(Declarative): 'dn': u'cn=%s,cn=%s,cn=services,cn=vaults,cn=kra,%s' % (vault_name, service_name, api.env.basedn), 'cn': [vault_name], + 'ipavaulttype': [u'standard'], }, }, }, @@ -219,6 +277,7 @@ class test_vault_plugin(Declarative): 'result': { 'cn': [vault_name], 'description': [u'Test vault'], + 'ipavaulttype': [u'standard'], }, }, }, @@ -258,6 +317,7 @@ class test_vault_plugin(Declarative): % (vault_name, api.env.basedn), 'objectclass': [u'top', u'ipaVault'], 'cn': [vault_name], + 'ipavaulttype': [u'standard'], }, }, }, @@ -280,6 +340,7 @@ class test_vault_plugin(Declarative): 'dn': u'cn=%s,cn=shared,cn=vaults,cn=kra,%s' % (vault_name, api.env.basedn), 'cn': [vault_name], + 'ipavaulttype': [u'standard'], }, ], }, @@ -301,6 +362,7 @@ class test_vault_plugin(Declarative): 'dn': u'cn=%s,cn=shared,cn=vaults,cn=kra,%s' % (vault_name, api.env.basedn), 'cn': [vault_name], + 'ipavaulttype': [u'standard'], }, }, }, @@ -321,6 +383,7 @@ class test_vault_plugin(Declarative): 'result': { 'cn': [vault_name], 'description': [u'Test vault'], + 'ipavaulttype': [u'standard'], }, }, }, @@ -360,6 +423,7 @@ class test_vault_plugin(Declarative): % (vault_name, user_name, api.env.basedn), 'objectclass': [u'top', u'ipaVault'], 'cn': [vault_name], + 'ipavaulttype': [u'standard'], }, }, }, @@ -382,6 +446,7 @@ class test_vault_plugin(Declarative): 'dn': u'cn=%s,cn=%s,cn=users,cn=vaults,cn=kra,%s' % (vault_name, user_name, api.env.basedn), 'cn': [vault_name], + 'ipavaulttype': [u'standard'], }, ], }, @@ -403,6 +468,7 @@ class test_vault_plugin(Declarative): 'dn': u'cn=%s,cn=%s,cn=users,cn=vaults,cn=kra,%s' % (vault_name, user_name, api.env.basedn), 'cn': [vault_name], + 'ipavaulttype': [u'standard'], }, }, }, @@ -423,6 +489,7 @@ class test_vault_plugin(Declarative): 'result': { 'cn': [vault_name], 'description': [u'Test vault'], + 'ipavaulttype': [u'standard'], }, }, }, @@ -446,50 +513,53 @@ class test_vault_plugin(Declarative): }, { - 'desc': 'Create vault for archival', + 'desc': 'Create standard vault', 'command': ( 'vault_add', - [vault_name], + [standard_vault_name], {}, ), 'expected': { - 'value': vault_name, - 'summary': 'Added vault "%s"' % vault_name, + 'value': standard_vault_name, + 'summary': 'Added vault "%s"' % standard_vault_name, 'result': { - 'dn': u'cn=%s,cn=admin,cn=users,cn=vaults,%s' - % (vault_name, api.env.basedn), + 'dn': u'cn=%s,cn=admin,cn=users,cn=vaults,cn=kra,%s' + % (standard_vault_name, api.env.basedn), 'objectclass': [u'top', u'ipaVault'], - 'cn': [vault_name], + 'cn': [standard_vault_name], + 'ipavaulttype': [u'standard'], }, }, }, { - 'desc': 'Archive secret', + 'desc': 'Archive secret into standard vault', 'command': ( 'vault_archive', - [vault_name], + [standard_vault_name], { 'data': secret, }, ), 'expected': { - 'value': vault_name, - 'summary': 'Archived data into vault "%s"' % vault_name, + 'value': standard_vault_name, + 'summary': 'Archived data into vault "%s"' + % standard_vault_name, 'result': {}, }, }, { - 'desc': 'Retrieve secret', + 'desc': 'Retrieve secret from standard vault', 'command': ( 'vault_retrieve', - [vault_name], + [standard_vault_name], {}, ), 'expected': { - 'value': vault_name, - 'summary': 'Retrieved data from vault "%s"' % vault_name, + 'value': standard_vault_name, + 'summary': 'Retrieved data from vault "%s"' + % standard_vault_name, 'result': { 'data': secret, }, @@ -497,17 +567,122 @@ class test_vault_plugin(Declarative): }, { - 'desc': 'Delete vault for archival', + 'desc': 'Create symmetric vault', 'command': ( - 'vault_del', - [vault_name], - {}, + 'vault_add', + [symmetric_vault_name], + { + 'ipavaulttype': u'symmetric', + 'password': password, + }, ), 'expected': { - 'value': [vault_name], - 'summary': u'Deleted vault "%s"' % vault_name, + 'value': symmetric_vault_name, + 'summary': 'Added vault "%s"' % symmetric_vault_name, 'result': { - 'failed': (), + 'dn': u'cn=%s,cn=admin,cn=users,cn=vaults,cn=kra,%s' + % (symmetric_vault_name, api.env.basedn), + 'objectclass': [u'top', u'ipaVault'], + 'cn': [symmetric_vault_name], + 'ipavaulttype': [u'symmetric'], + 'ipavaultsalt': [fuzzy_string], + }, + }, + }, + + { + 'desc': 'Archive secret into symmetric vault', + 'command': ( + 'vault_archive', + [symmetric_vault_name], + { + 'password': password, + 'data': secret, + }, + ), + 'expected': { + 'value': symmetric_vault_name, + 'summary': 'Archived data into vault "%s"' + % symmetric_vault_name, + 'result': {}, + }, + }, + + { + 'desc': 'Retrieve secret from symmetric vault', + 'command': ( + 'vault_retrieve', + [symmetric_vault_name], + { + 'password': password, + }, + ), + 'expected': { + 'value': symmetric_vault_name, + 'summary': 'Retrieved data from vault "%s"' + % symmetric_vault_name, + 'result': { + 'data': secret, + }, + }, + }, + + { + 'desc': 'Create asymmetric vault', + 'command': ( + 'vault_add', + [asymmetric_vault_name], + { + 'ipavaulttype': u'asymmetric', + 'ipapublickey': public_key, + }, + ), + 'expected': { + 'value': asymmetric_vault_name, + 'summary': 'Added vault "%s"' % asymmetric_vault_name, + 'result': { + 'dn': u'cn=%s,cn=admin,cn=users,cn=vaults,cn=kra,%s' + % (asymmetric_vault_name, api.env.basedn), + 'objectclass': [u'top', u'ipaVault'], + 'cn': [asymmetric_vault_name], + 'ipavaulttype': [u'asymmetric'], + 'ipapublickey': [public_key], + }, + }, + }, + + { + 'desc': 'Archive secret into asymmetric vault', + 'command': ( + 'vault_archive', + [asymmetric_vault_name], + { + 'data': secret, + }, + ), + 'expected': { + 'value': asymmetric_vault_name, + 'summary': 'Archived data into vault "%s"' + % asymmetric_vault_name, + 'result': {}, + }, + }, + + { + 'desc': 'Retrieve secret from asymmetric vault', + 'command': ( + 'vault_retrieve', + [asymmetric_vault_name], + { + 'private_key': private_key, + }, + ), + 'expected': { + 'value': asymmetric_vault_name, + 'summary': 'Retrieved data from vault "%s"' + % asymmetric_vault_name, + 'result': { + 'data': secret, }, }, }, |