Added symmetric and asymmetric vaults.

The vault plugin has been modified to support symmetric and asymmetric
vaults to provide additional security over the standard vault by
encrypting the data before it's sent to the server. The encryption
functionality is implemented using the python-cryptography library.

https://fedorahosted.org/freeipa/ticket/3872

Reviewed-By: Jan Cholasta <jcholast@redhat.com>
This commit is contained in:
Endi S. Dewata 2014-10-24 19:53:16 -04:00 committed by Jan Cholasta
parent 8ee975b276
commit fc5c614950
6 changed files with 797 additions and 75 deletions

58
API.txt
View File

@ -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,14 @@ 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)
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: Bytes('data?')
option: Str('in?')
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)
@ -5436,11 +5443,26 @@ 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
command: vault_archive
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('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)
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_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: 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')

View File

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

View File

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

View File

@ -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' )

View File

@ -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:
@ -75,6 +114,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>
@ -126,6 +171,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()

View File

@ -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,
},
},
},