some modifications - VaultPass GPG-encrypted creds are almost working.

This commit is contained in:
brent s. 2020-03-29 19:45:25 -04:00
parent feb032b84f
commit 2545138ae1
Signed by: bts
GPG Key ID: 8C004C2F93481F6B
7 changed files with 111 additions and 42 deletions

View File

@ -175,7 +175,8 @@ To determine the behaviour of how this behaves, please refer to the below table.
<!-- SNIP --> <!-- SNIP -->
<auth> <auth>
<!-- "Automagic" (#1). <!-- "Automagic" (#1).
First $VAULT_TOKEN environment variable is checked, then ~/.vault-token is checked. --> First $VAULT_TOKEN environment variable is checked,
then ~/.vault-token is checked. -->
<token/> <token/>


<!-- Source is considered the only place to fetch token from (#2). --> <!-- Source is considered the only place to fetch token from (#2). -->
@ -232,8 +233,8 @@ To get around needing to store plaintext credentials on-disk in any form, VaultP
elements. These elements are of the same composition (described <<gpg_elements, below>>) and allow you to use GPG to elements. These elements are of the same composition (described <<gpg_elements, below>>) and allow you to use GPG to
encrypt that sensitive information. encrypt that sensitive information.


Note that while this does increase security, it breaks compatibility with other XML parsers - they won't be able to While this does increase security, it breaks compatibility with other XML parsers - they won't be able to decrypt and
decrypt and parse the encrypted snippet unless explicitly coded to do so. parse the encrypted snippet unless explicitly coded to do so.


==== `*Gpg` elements ==== `*Gpg` elements
`*Gpg` elements (`authGpg`, `unsealGpg`) have the same structure: `*Gpg` elements (`authGpg`, `unsealGpg`) have the same structure:
@ -241,23 +242,16 @@ decrypt and parse the encrypted snippet unless explicitly coded to do so.
. `unsealGpg`/`authGpg`, the container element. . `unsealGpg`/`authGpg`, the container element.
.. The path to the encrypted file as the contained text. .. The path to the encrypted file as the contained text.


It has some optional attributes as well: It has one optional attribute, `gpgHome` footnote:optelem[] -- the GPG home directory to use. If not specified,

VaultPass will first check the **`GNUPGHOME`** environment variable. If that isn't defined, we'll default to
.`*Gpg` element attributes `~/.gnupg/` (or whatever the compiled-in default is).
|===
|Attribute |Content

|`keyFPR` footnote:optelem[] | The GPG key to use to decrypt the file. It accepts multiple key ID formats, but it's *highly* recommended to
use the full 40-character (without spaces) key fingerprint. If not specified, VaultPass will use the first private key
it finds in the keyring.
|`gpgHome` footnote:optelem[] | The GPG home directory. If not specified, VaultPass will first check the **`GNUPGHOME`** environment
variable. If that's empty, we'll default to `~/.gnupg/`.
|===


The contents of the encrypted file should match the **unencrypted** XML content it's replacing. The contents of the encrypted file should match the **unencrypted** XML content it's replacing.


CAUTION: Note that if you use namespaces in your `vaultpass.xml` config file, you **MUST** use matching declarations in CAUTION: Note that if you use namespaces in your `vaultpass.xml` config file, you **MUST** use matching declarations in
your encrypted file. your encrypted file. You **MAY** exclude the `xsi:schemaLocation` specification, however, if it's the same as your
`vaultpass.xml`. It is **highly** recommended that you use the same xsi:shemaLocation, however (or leave it out
entirely).


Let's look at an example of GPG-encrypted elements. Let's look at an example of GPG-encrypted elements.


@ -273,11 +267,9 @@ Let's look at an example of GPG-encrypted elements.


<server> <server>
<uri>http://localhost:8000/</uri> <uri>http://localhost:8000/</uri>
<unsealGpg keyFPR="D34DB33FD34DB33FD34DB33FD34DB33FD34DB33F" <unsealGpg gpgHome="~/.gnupg">~/.private/vaultpass/unseal.asc</unsealGpg>
gpgHome="~/.gnupg">~/.private/vaultpass/unseal.asc</unsealGpg>
</server> </server>
<authGpg keyFPR="D34DB33FD34DB33FD34DB33FD34DB33FD34DB33F" <authGpg gpgHome="~/.gnupg">~/.private/vaultpass/auth.gpg</unsealGpg>
gpgHome="~/.gnupg">~/.private/vaultpass/auth.gpg</unsealGpg>
</vaultpass> </vaultpass>
---- ----



7
testxml.py Executable file
View File

@ -0,0 +1,7 @@
#!/usr/bin/env python3

import vaultpass

cfg = vaultpass.config.LocalFile('/tmp/vaultpass.xml')
cfg.main()

View File

@ -16,7 +16,6 @@ class PassMan(object):


def __init__(self, cfg = '~/.config/vaultpass.xml'): def __init__(self, cfg = '~/.config/vaultpass.xml'):
self.cfg = config.getConfig(cfg) self.cfg = config.getConfig(cfg)
self.cfg.main()
self._getURI() self._getURI()
self.getClient() self.getClient()


@ -29,6 +28,8 @@ class PassMan(object):
def getClient(self): def getClient(self):
# This may need to be re-tooled in the future. # This may need to be re-tooled in the future.
auth_xml = self.cfg.xml.find('auth') auth_xml = self.cfg.xml.find('auth')
if auth_xml is None:
raise RuntimeError('Could not find authentication')
authmethod_xml = auth_xml.getchildren()[0] authmethod_xml = auth_xml.getchildren()[0]
for a in dir(auth): for a in dir(auth):
if a.startswith('_'): if a.startswith('_'):

View File

@ -128,6 +128,7 @@ class Token(_AuthBase):
self.token = self._getEnv(e) self.token = self._getEnv(e)
else: else:
self.token = self._getFile(a) self.token = self._getFile(a)
self.client = hvac.Client(url = self.uri)
self.client.token = self.token self.client.token = self.token
self.authCheck() self.authCheck()
return(None) return(None)

View File

@ -3,6 +3,7 @@ import os
import logging import logging
import re import re
## ##
from . import gpg_handler
import requests import requests
from lxml import etree from lxml import etree


@ -12,6 +13,8 @@ _logger = logging.getLogger()




class Config(object): class Config(object):
gpg = None
gpg_elems = ('authGpg', 'unsealGpg')
xsd_path = None xsd_path = None
tree = None tree = None
namespaced_tree = None namespaced_tree = None
@ -31,6 +34,31 @@ class Config(object):
self.populateDefaults() self.populateDefaults()
if validate: if validate:
self.validate() self.validate()
g = self.parseGpg()
if g:
# And do it again.
if populate_defaults:
self.populateDefaults()
if validate:
self.validate()
return(None)

def decryptGpg(self, gpg_xml):
home = gpg_xml.attrib.get('gpgHome')
tag = gpg_xml.tag
ns_xml = self.xml.find(tag)
xml = self.stripNS(obj = ns_xml).tag
fpath = gpg_xml.text
if not self.gpg:
self.gpg = gpg_handler.GPG(home = home)
else:
self.gpg.gpg.home = home
self.gpg.initHome()
ns_dcrpt_xml = etree.fromstring(self.gpg.decrypt(fpath))
dcrpt_xml = self.stripNS(obj = ns_dcrpt_xml)
ns_xml.getparent().replace(ns_xml, ns_dcrpt_xml)
xml.getparent().replace(xml, dcrpt_xml)
self.parse()
return(None) return(None)


def fetch(self): # Just a fail-safe; this is overridden by specific subclasses. def fetch(self): # Just a fail-safe; this is overridden by specific subclasses.
@ -87,11 +115,8 @@ class Config(object):
_logger.info('Rendered XSD.') _logger.info('Rendered XSD.')
return(None) return(None)


def parseRaw(self, parser = None): def parse(self):
self.xml = etree.fromstring(self.raw, parser = parser) # This can used to "re-parse" the self.xml and self.namespaced_xml.
_logger.debug('Generated xml.')
self.namespaced_xml = etree.fromstring(self.raw, parser = parser)
_logger.debug('Generated namespaced xml.')
self.tree = self.xml.getroottree() self.tree = self.xml.getroottree()
_logger.debug('Generated tree.') _logger.debug('Generated tree.')
self.namespaced_tree = self.namespaced_xml.getroottree() self.namespaced_tree = self.namespaced_xml.getroottree()
@ -103,6 +128,26 @@ class Config(object):
self.stripNS() self.stripNS()
return(None) return(None)


def parseGpg(self):
gpg_elem_found = False # Change to True if we find any GPG-encrypted elems
search = []
for x in self.gpg_elems:
search.append("local-name()='{0}'".format(x))
search = '[{0}]'.format(' or '.join(search))
print(search)
gpg_elems = self.namespaced_xml.findall('|'.join(search))
for e in gpg_elems:
print(e)
return(gpg_elem_found)

def parseRaw(self, parser = None):
self.xml = etree.fromstring(self.raw, parser = parser)
_logger.debug('Generated xml.')
self.namespaced_xml = etree.fromstring(self.raw, parser = parser)
_logger.debug('Generated namespaced xml.')
self.parse()
return(None)

def populateDefaults(self): def populateDefaults(self):
_logger.info('Populating missing values with defaults from XSD.') _logger.info('Populating missing values with defaults from XSD.')
if not self.xsd: if not self.xsd:

39
vaultpass/gpg_handler.py Normal file
View File

@ -0,0 +1,39 @@
import io
import logging
import os
##
import gpg # https://pypi.org/project/gpg/


_logger = logging.getLogger()


class GPG(object):
home = None
gpg = None

def __init__(self, home = None):
if home:
self.home = home
self.initHome()

def decrypt(self, fpath):
fpath = os.path.abspath(os.path.expanduser(fpath))
with open(fpath, 'rb') as fh:
iobuf = io.BytesIO(fh.read())
iobuf.seek(0, 0)
rslt = self.gpg.decrypt(iobuf)
decrypted = rslt[0]
return(decrypted)

def initHome(self):
if not self.home:
h = os.environ.get('GNUPGHOME')
if h:
self.home = h
if self.home:
self.home = os.path.abspath(os.path.expanduser(self.home))
if not os.path.isdir(self.home):
raise ValueError('GPG home does not exist')
_logger.debug('Set GPG home to explicitly specified value {0}'.format(self.home))
return(None)

View File

@ -1,16 +0,0 @@
import os
import logging
##
import gpg


# Special shoutout to Jthan for ruining my life.


_logger = logging.getLogger()


class GPGAuth(object):
def __init__(self, gpgauth_xml):
pass