Skip to content

Commit

Permalink
Merge pull request #171 from olcf/ngin_behalf
Browse files Browse the repository at this point in the history
Ngin behalf
  • Loading branch information
Noah Ginsburg authored May 5, 2020
2 parents 6282c8e + 597f181 commit 94a50fd
Show file tree
Hide file tree
Showing 9 changed files with 214 additions and 106 deletions.
19 changes: 11 additions & 8 deletions docs/source/Commands.rst
Original file line number Diff line number Diff line change
Expand Up @@ -671,22 +671,25 @@ This unlocks a password and displays it on stdout
.. code-block:: bash
usage: pkpass.py show [-h] [-a] [--cabundle CABUNDLE] [-c CARD_SLOT]
[--certpath CERTPATH] [--color COLOR] [-i IDENTITY] [-I]
[--keypath KEYPATH] [--no-cache] [--nopassphrase]
[--noverify] [--pwstore PWSTORE] [-q] [-r] [--stdin]
[--theme-map THEME_MAP] [-v]
usage: pkpass.py show [-h] [-a] [-b BEHALF] [--cabundle CABUNDLE]
[-c CARD_SLOT] [--certpath CERTPATH] [--color COLOR]
[-i IDENTITY] [-I] [--keypath KEYPATH] [--no-cache]
[--nopassphrase] [--noverify] [--pwstore PWSTORE] [-q]
[-r] [--stdin] [--theme-map THEME_MAP] [-v]
[pwname]
positional arguments:
pwname Name of the password. Ex:
passwords/team/infrastructure/root
passwords/team/infrastructure/root
optional arguments:
-h, --help show this help message and exit
-a, --all Show all available password to the given user, if a
pwname is supplied filtering will be done case-
insensitivey based on the filename
-b BEHALF, --behalf BEHALF
Show passwords for a user using a password as its
private key
--cabundle CABUNDLE Path to CA certificate bundle file
-c CARD_SLOT, --card_slot CARD_SLOT
The slot number of the card that should be used
Expand Down
11 changes: 11 additions & 0 deletions docs/source/Configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,14 @@ This will create an unsigned keypair. We really want it to create a certificate
openssl req -newkey rsa:4096 -keyout local.key -x509 -out local.cert
As long as the private and public keys are in directories that pkpass can find, distribution to those identities works exactly the same. Keys must be named 'username.key'. For user foo, the private key must be named 'foo.key' and reside in the keypath directory.

Behalf of functionality
-----------------------
To utilize the functionality for showing a password on behalf of another user you need to create a password that is the private key of this user. Then when you issue a show command you specify the username with the `-b` flag

Example:

.. code-block:: bash
pkpass show password_i_dont_have_direct_access_to -b rsa_user
the argument `rsa_user` needs to be both the username and the password name for the password that store's this user's rsa key
8 changes: 8 additions & 0 deletions libpkpass/commands/arguments.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@
}
},

'behalf': {
'args': ['-b', '--behalf'],
'kwargs': {
'help': 'Show passwords for a user using a password as its private key',
'type': str,
}
},

'cabundle': {
'args': ['--cabundle'],
'kwargs': {
Expand Down
45 changes: 42 additions & 3 deletions libpkpass/commands/show.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""This module is used to process the decryption of keys"""
import os
import fnmatch
from tempfile import gettempdir
from libpkpass.commands.command import Command
from libpkpass.password import PasswordEntry
from libpkpass.errors import PasswordIOError, CliArgumentError, NotARecipientError, DecryptionError
Expand All @@ -12,16 +13,25 @@ class Show(Command):
####################################################################
name = 'show'
description = 'Display a password'
selected_args = Command.selected_args + ['pwname', 'pwstore', 'stdin', 'keypath', 'nopassphrase',
'noverify', 'card_slot', 'all', 'recovery', 'ignore_decrypt']

selected_args = Command.selected_args + [
'all', 'behalf', 'card_slot', 'ignore_decrypt', 'keypath', 'nopassphrase',
'noverify', 'pwname', 'pwstore', 'recovery', 'stdin'
]
####################################################################
def _run_command_execution(self):
""" Run function for class. """
####################################################################
password = PasswordEntry()
myidentity = self.identities.iddb[self.args['identity']]
if 'behalf' in self.args and self.args['behalf']:
self._behalf_prep(password, myidentity)
else:
self._show_wrapper(password, myidentity)

####################################################################
def _show_wrapper(self, password, myidentity):
"""Wrapper for show to allow for on behalf of"""
####################################################################
if self.args['all']:
try:
self._walk_dir(self.args['pwstore'], password, myidentity, self.args['ignore_decrypt'])
Expand All @@ -33,6 +43,35 @@ def _run_command_execution(self):
self._decrypt_wrapper(
self.args['pwstore'], password, myidentity, self.args['pwname'])

####################################################################
def _behalf_prep(self, password, myidentity):
"""Create necessary temporary file for rsa key"""
####################################################################
password.read_password_data(os.path.join(self.args['pwstore'], self.args['behalf']))
# allows the password to be stored outside the root of the password directory
self.args['behalf'] = self.args['behalf'].split('/')[-1]
temp_key = os.path.join(gettempdir(), '%s.key' % self.args['behalf'])
plaintext_pw = password.decrypt_entry(
identity=myidentity, passphrase=self.passphrase, card_slot=self.args['card_slot'])
with open(temp_key, 'w') as fname:
fname.write(
'%s\n%s\n%s' % (
'-----BEGIN RSA PRIVATE KEY-----',
plaintext_pw.replace(
'-----BEGIN RSA PRIVATE KEY-----', ''
).replace(
' -----END RSA PRIVATE KEY----', ''
).replace(' ', '\n'),
'-----END RSA PRIVATE KEY-----'
)
)
self.args['identity'] = self.args['behalf']
myidentity = self.identities.iddb[self.args['identity']]
self.args['key_path'] = temp_key
myidentity['key_path'] = temp_key
self._show_wrapper(password, myidentity)
os.unlink(temp_key)

####################################################################
def _walk_dir(self, directory, password, myidentity, ignore_decrypt=False):
"""Walk our directory searching for passwords"""
Expand Down
48 changes: 48 additions & 0 deletions libpkpass/crypto.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,54 @@ def get_cert_element(cert, element):
except IndexError:
raise X509CertificateError(stdout)

##############################################################################
def get_card_element(element):
"""Return an arbitrary element of a pcks15 capable device"""
##############################################################################
command = ['pkcs15-tool', '--read-certificate', '1']
proc = Popen(command, stdout=PIPE, stdin=PIPE, stderr=STDOUT)
stdout, _ = proc.communicate()
return get_cert_element(stdout, element)

##############################################################################
def get_card_fingerprint():
""" Return the modulus of the x509 certificate of the identity """
##############################################################################
# SHA1 Fingerprint=F9:9D:71:54:55:BE:99:24:6A:5E:E0:BB:48:F9:63:AE:A2:05:54:98
return get_card_element('fingerprint').split('=')[1]

##############################################################################
def get_card_subject():
""" Return the subject DN of the x509 certificate of the identity """
##############################################################################
# subject= /C=US/O=Entrust/OU=Certification Authorities/OU=Entrust Managed Services SSP CA
return ' '.join(get_card_element('subject').split(' ')[1:])

##############################################################################
def get_card_issuer():
""" Return the issuer DN of the x509 certificate of the identity """
##############################################################################
# issuer= /C=US/O=Entrust/OU=Certification Authorities/OU=Entrust Managed Services SSP CA
return ' '.join(get_card_element('issuer').split(' ')[1:])

##############################################################################
def get_card_enddate():
""" Return the issuer DN of the x509 certificate of the identity """
##############################################################################
return get_card_element('enddate').split('=')[1]

##############################################################################
def get_card_issuerhash():
""" Return the issuer DN of the x509 certificate of the identity """
##############################################################################
return get_card_element('issuer_hash')

##############################################################################
def get_card_subjecthash():
""" Return the issuer DN of the x509 certificate of the identity """
##############################################################################
return get_card_element('subject_hash')

##############################################################################
def sk_encrypt_string(plaintext_string, key):
""" Symmetrically Encrypt and return a base 64 encoded string using the provided secret"""
Expand Down
9 changes: 9 additions & 0 deletions libpkpass/identities.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,28 @@ class IdentityDB():
things pertinent to recipients and their groups and keys. """
##########################################################################

#######################################################################
def __init__(self):
#######################################################################
self.extensions = {'certificate': ['.cert', '.crt'],
'key': '.key'}
self.cabundle = ""
self.iddb = {}

#######################################################################
def __repr__(self):
#######################################################################
return "%s(%r)" % (self.__class__, self.__dict__)

#######################################################################
def __str__(self):
#######################################################################
return "%r" % self.__dict__

#######################################################################
def _load_certs_from_external(self, connection_map, nocache):
"""Load certificates from external via use of plugin"""
#######################################################################
if 'base_directory' in connection_map and connection_map['base_directory']:
temp_dir = connection_map['base_directory']
del connection_map['base_directory']
Expand Down
72 changes: 31 additions & 41 deletions libpkpass/password.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
import uuid
import time
import os
from subprocess import Popen, PIPE, STDOUT
import yaml
from libpkpass.escrow import pk_split_secret
from libpkpass.errors import NotARecipientError, DecryptionError, PasswordIOError, YamlFormatError
from libpkpass.errors import NotARecipientError, DecryptionError, PasswordIOError, YamlFormatError,\
X509CertificateError
import libpkpass.crypto as crypto

#######################################################################
Expand Down Expand Up @@ -118,25 +118,29 @@ def add_recipients(
self.recipients.update(new_recipients)
if escrow_users:
#escrow_users may now be none after the set operations
if (len(escrow_users) > 3) and (len(list((set(escrow_users) - set(recipients)))) < 3):
print("warning: min_escrow requirement not met after removing password recipients from escrow user list")
return
escrow_map, split_secret = self.add_escrow(
secret=secret,
escrow_users=escrow_users,
minimum=minimum,
pwstore=pwstore)
self.process_escrow_map(
escrow_map,
split_secret=split_secret,
distributor=distributor,
recipients=recipients,
identitydb=identitydb,
encryption_algorithm=encryption_algorithm,
passphrase=passphrase,
card_slot=card_slot,
escrow_users=escrow_users,
minimum=minimum)
try:
if (len(escrow_users) > 3) and (len(list((set(escrow_users) - set(recipients)))) < 3):
print("warning: min_escrow requirement not met after removing password recipients from escrow user list")
return
escrow_map, split_secret = self.add_escrow(
secret=secret,
escrow_users=escrow_users,
minimum=minimum,
pwstore=pwstore)
self.process_escrow_map(
escrow_map,
split_secret=split_secret,
distributor=distributor,
recipients=recipients,
identitydb=identitydb,
encryption_algorithm=encryption_algorithm,
passphrase=passphrase,
card_slot=card_slot,
escrow_users=escrow_users,
minimum=minimum)
except ValueError as err:
print("Warning cannot create escrow shares, reason: %s" % err)
print("Your password has been created without escrow capabilities")


#######################################################################
Expand Down Expand Up @@ -167,14 +171,18 @@ def _add_recipient(
'derived_key': encrypted_derived_key,
'recipient_hash': cert['subjecthash'],
}
try:
distributor_hash = crypto.get_card_subjecthash()
except X509CertificateError:
distributor_hash = identitydb.iddb[distributor]['certs'][0]['subjecthash']
recipient_entry = {
'encrypted_secrets': encrypted_secrets,
# 'distributor_fingerprint': crypto.get_cert_fingerprint( identitydb.iddb[distributor] ),
# 'recipient_fingerprint': crypto.get_cert_fingerprint( identitydb.iddb[recipient] ),
'encryption_algorithm': encryption_algorithm,
'timestamp': time.time(),
'distributor': distributor,
'distributor_hash': get_distributor_hash(),
'distributor_hash': distributor_hash,
}
message = self._create_signable_string(recipient_entry)
recipient_entry['signature'] = crypto.pk_sign_string(
Expand Down Expand Up @@ -221,13 +229,7 @@ def decrypt_entry(self, identity=None, passphrase=None, card_slot=None):
passphrase
)
else:
command = ['pkcs15-tool', '--read-certificate', '1']
proc = Popen(command, stdout=PIPE, stdin=PIPE, stderr=STDOUT)
stdout, _ = proc.communicate()
command = ['openssl', 'x509', '-noout', "-fingerprint"]
proc = Popen(command, stdout=PIPE, stdin=PIPE, stderr=STDOUT)
stdout, _ = proc.communicate(input=stdout)
cert_key = stdout.decode("ASCII").rstrip().split('=')[1]
cert_key = crypto.get_card_fingerprint()
return crypto.pk_decrypt_string(
recipient_entry['encrypted_secrets'][cert_key]['encrypted_secret'],
recipient_entry['encrypted_secrets'][cert_key]['derived_key'],
Expand Down Expand Up @@ -346,15 +348,3 @@ def write_password_data(self, filename, overwrite=False,
fname.write(yaml.safe_dump(passdata, default_flow_style=False))
except (OSError, IOError):
raise PasswordIOError("Error creating '%s'" % filename)

#######################################################################
def get_distributor_hash():
"""Return hash of cert distributor is using"""
#######################################################################
command = ['pkcs15-tool', '--read-certificate', '1']
proc = Popen(command, stdout=PIPE, stdin=PIPE, stderr=STDOUT)
stdout, _ = proc.communicate()
command = ['openssl', 'x509', '-noout', "-subject_hash"]
proc = Popen(command, stdout=PIPE, stdin=PIPE, stderr=STDOUT)
stdout, _ = proc.communicate(input=stdout)
return stdout.decode("ASCII").rstrip()
Loading

0 comments on commit 94a50fd

Please sign in to comment.