Skip to content

Commit

Permalink
bugfixen
Browse files Browse the repository at this point in the history
- added missed call to _compute.chksum in ECDSAPriv
- replaced all gpg wrapper fixtures in the unit test suite with gpg package
- moved test suite gnupghome location to tests/gnupghome
  • Loading branch information
Commod0re committed Aug 16, 2017
1 parent eb287e0 commit ca7ca6b
Show file tree
Hide file tree
Showing 12 changed files with 142 additions and 369 deletions.
10 changes: 8 additions & 2 deletions docs/source/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,16 @@
Changelog
*********

v0.5.0
v0.4.3
======

Released: not yet
Released: August 15, 2017

Bugs Fixed
----------

* Private key checksum calculations were not getting stored for ECDSA keys; this has been fixed.
* The test suite gpg wrappers have been replaced with use of the `gpg <https://pypi.python.org/pypi/gpg/1.8.0>`_ package. (#171)

v0.4.2
======
Expand Down
2 changes: 1 addition & 1 deletion pgpy/_author.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@
__author__ = "Michael Greene"
__copyright__ = "Copyright (c) 2014 Michael Greene"
__license__ = "BSD"
__version__ = str(LooseVersion("0.5.0"))
__version__ = str(LooseVersion("0.4.3"))
1 change: 1 addition & 0 deletions pgpy/packet/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -1303,6 +1303,7 @@ def _generate(self, oid):
self.x = MPI(pubn.x)
self.y = MPI(pubn.y)
self.s = MPI(pk.private_numbers().private_value)
self._compute_chksum()

def parse(self, packet):
super(ECDSAPriv, self).parse(packet)
Expand Down
3 changes: 2 additions & 1 deletion requirements-test.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ pytest
pytest-cov
pytest-ordering
flake8
pep8-naming
pep8-naming
gpg==1.8.0
241 changes: 17 additions & 224 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,19 @@
"""PGPy conftest"""
import pytest

import contextlib
import glob
import gpg
import os
import re
import select
import subprocess
import sys
import time

from distutils.version import LooseVersion

from cryptography.hazmat.backends import openssl

openssl_ver = LooseVersion(openssl.backend.openssl_version_text().split(' ')[1])
gpg_ver = LooseVersion('0')
pgpdump_ver = LooseVersion('0')
python_gpg_ver = LooseVersion(gpg.version.versionstr)
gnupghome = os.path.join(os.path.dirname(__file__), 'gnupghome')


# ensure external commands we need to run exist
Expand All @@ -33,237 +30,33 @@
sys.path.insert(1, os.path.join(os.getcwd(), 'tests'))


def _which(cmd):
for d in iter(p for p in os.getenv('PATH').split(':') if os.path.isdir(p)):
if cmd in os.listdir(d) and os.access(os.path.realpath(os.path.join(d, cmd)), os.X_OK):
return os.path.join(d, cmd)


# run a subprocess command, wait for it to complete, and then return decoded output
def _run(bin, *binargs, **pkw):
_default_pkw = {'stdout': subprocess.PIPE,
'stderr': subprocess.PIPE}

popen_kwargs = _default_pkw.copy()
popen_kwargs.update(pkw)

cmd = subprocess.Popen([bin] + list(binargs), **popen_kwargs)
cmd.wait()
cmdo, cmde = cmd.communicate()

cmdo = cmdo.decode('latin-1') if cmdo is not None else ""
cmde = cmde.decode('latin-1') if cmde is not None else ""

return cmdo, cmde


_gpg_bin = _which('gpg2')
_gpg_args = ('--options', './pgpy.gpg.conf', '--expert', '--status-fd')
_gpg_env = {}
_gpg_env['GNUPGHOME'] = os.path.abspath(os.path.abspath('tests/testdata'))
_gpg_kwargs = dict()
_gpg_kwargs['cwd'] = 'tests/testdata'
_gpg_kwargs['env'] = _gpg_env
_gpg_kwargs['stdout'] = subprocess.PIPE
_gpg_kwargs['stderr'] = subprocess.STDOUT
_gpg_kwargs['close_fds'] = False


# GPG boilerplate function
def _gpg(*gpg_args, **popen_kwargs):
# gpgfd is our "read" end of the pipe
# _gpgfd is gpg's "write" end
gpgfd, _gpgfd = os.pipe()

# on python >= 3.4, we need to set _gpgfd as inheritable
# older versions do not have this function
if sys.version_info >= (3, 4):
os.set_inheritable(_gpgfd, True)

args = (_gpg_bin,) + _gpg_args + (str(_gpgfd),) + gpg_args
kwargs = _gpg_kwargs.copy()
kwargs.update(popen_kwargs)

try:
# use this as the buffer for collecting status-fd output
c = bytearray()

cmd = subprocess.Popen(args, **kwargs)
while cmd.poll() is None:
while gpgfd in select.select([gpgfd,], [], [], 0)[0]:
c += os.read(gpgfd, 1)

else:
# sleep for a bit
time.sleep(0.010)

# finish reading if needed
while gpgfd in select.select([gpgfd,], [], [], 0)[0]:
c += os.read(gpgfd, 1)

# collect stdout and stderr
o, e = cmd.communicate()

finally:
# close the pipes we used for this
os.close(gpgfd)
os.close(_gpgfd)

return c.decode('latin-1'), (o or b'').decode('latin-1'), (e or b'').decode('latin-1')


# fixtures
@pytest.fixture()
def gpg_import():
@contextlib.contextmanager
def _gpg_import(*keypaths):
# if GPG version is 2.1 or newer, we need to add a setup/teardown step in creating the keybox folder
if gpg_ver >= '2.1':
if not os.path.exists('tests/testdata/private-keys-v1.d'):
os.mkdir('tests/testdata/private-keys-v1.d')
time.sleep(0.5)

gpgc, gpgo, gpge = _gpg('--import', *list(keypaths))

try:
yield gpgo

finally:
[os.remove(f) for f in glob.glob('tests/testdata/testkeys.*')]
if gpg_ver >= '2.1':
[os.remove(f) for f in glob.glob('tests/testdata/private-keys-v1.d/*')]

time.sleep(0.5)

return _gpg_import


@pytest.fixture()
def gpg_check_sigs():
def _gpg_check_sigs(*keyids):
gpgc, gpgo, gpge = _gpg('--check-sigs', *keyids)
return 'sig-' not in gpgo

return _gpg_check_sigs


@pytest.fixture()
def gpg_verify():
sfd_verify = re.compile(r'^\[GNUPG:\] (?:GOOD|EXP)SIG (?P<keyid>[0-9A-F]+) .*'
r'^\[GNUPG:\] VALIDSIG (?:[0-9A-F]{,24})\1', flags=re.MULTILINE | re.DOTALL)

def _gpg_verify(gpg_subjpath, gpg_sigpath=None, keyid=None):
rargs = [gpg_sigpath, gpg_subjpath] if gpg_sigpath is not None else [gpg_subjpath,]

gpgc, gpgo, gpge = _gpg('--verify', *rargs)

sigs = [ sv.group('keyid') for sv in sfd_verify.finditer(gpgc) ]

if keyid is not None:
return keyid in sigs

return sigs

return _gpg_verify


@pytest.fixture
def gpg_decrypt():
sfd_decrypt = re.compile(r'^\[GNUPG:\] BEGIN_DECRYPTION\n'
r'^\[GNUPG:\] DECRYPTION_INFO \d+ \d+\n'
r'^\[GNUPG:\] PLAINTEXT (?:62|74|75) (?P<tstamp>\d+) (?P<fname>.*)\n'
r'^\[GNUPG:\] PLAINTEXT_LENGTH \d+\n'
r'\[GNUPG:\] DECRYPTION_OKAY\n'
r'(?:^\[GNUPG:\] GOODMDC\n)?'
r'^\[GNUPG:\] END_DECRYPTION', flags=re.MULTILINE)

def _gpg_decrypt(encmsgpath, passphrase=None, keyid=None):
a = []

if passphrase is not None:
# create a pipe to send the passphrase to GnuPG through
pfdr, pfdw = os.pipe()

# write the passphrase to the pipe buffer right away
os.write(pfdw, passphrase.encode())
os.write(pfdw, b'\n')

# on python >= 3.4, we need to set pfdr as inheritable
# older versions do not have this function
if sys.version_info >= (3, 4):
os.set_inheritable(pfdr, True)

a.extend(['--batch', '--passphrase-fd', str(pfdr)])

elif keyid is not None:
a.extend(['--recipient', keyid])

a.extend(['--decrypt', encmsgpath])

gpgc, gpgo, gpge = _gpg(*a, stderr=subprocess.PIPE)

status = sfd_decrypt.match(gpgc)
return gpgo

return _gpg_decrypt


@pytest.fixture
def gpg_print():
sfd_text = re.compile(r'^\[GNUPG:\] PLAINTEXT (?:62|74|75) (?P<tstamp>\d+) (?P<fname>.*)\n'
r'^\[GNUPG:\] PLAINTEXT_LENGTH (?P<len>\d+)\n', re.MULTILINE)

gpg_text = re.compile(r'(?:- gpg control packet\n)?(?P<text>.*)', re.MULTILINE | re.DOTALL)

def _gpg_print(infile):
gpgc, gpgo, gpge = _gpg('-o-', infile, stderr=subprocess.PIPE)
status = sfd_text.match(gpgc)
tlen = len(gpgo) if status is None else int(status.group('len'))

return gpg_text.match(gpgo).group('text')[:tlen]

return _gpg_print


@pytest.fixture
def gpg_keyid_file():
def _gpg_keyid_file(infile):
c, o, e = _gpg('--list-packets', infile)
return re.findall(r'^\s+keyid: ([0-9A-F]+)', o, flags=re.MULTILINE)
return _gpg_keyid_file


@pytest.fixture()
def pgpdump():
def _pgpdump(infile):
return _run(_which('pgpdump'), '-agimplu', infile)[0]

return _pgpdump


# pytest hooks

# pytest_configure
# called after command line options have been parsed and all plugins and initial conftest files been loaded.
def pytest_configure(config):
print("== PGPy Test Suite ==")

# ensure commands we need exist
for cmd in ['gpg2', 'pgpdump']:
if _which(cmd) is None:
print("Error: Missing Command: " + cmd)
exit(-1)
# clear out gnupghome
clear_globs = [os.path.join(gnupghome, 'private-keys-v1.d', '*.key'),
os.path.join(gnupghome, '*.kbx*'),
os.path.join(gnupghome, '*.gpg*'),
os.path.join(gnupghome, '.*'),
os.path.join(gnupghome, 'random_seed')]
for fpath in iter(f for cg in clear_globs for f in glob.glob(cg)):
os.unlink(fpath)

# get the GnuPG version
gpg_ver.parse(_run(_which('gpg2'), '--version')[0].splitlines()[0].split(' ')[-1])
gpg_ver.parse(gpg.core.check_version())

# get the pgpdump version
v, _ = _run(_which('pgpdump'), '-v', stderr=subprocess.STDOUT)
pgpdump_ver.parse(v.split(' ')[2].strip(','))
# check that there are no keys loaded, now
with gpg.Context(offline=True) as c:
c.set_engine_info(gpg.constants.PROTOCOL_OpenPGP, home_dir=gnupghome)
assert len(list(c.keylist())) == 0
assert len(list(c.keylist(secret=True))) == 0

# display the working directory and the OpenSSL/GPG/pgpdump versions
print("Working Directory: " + os.getcwd())
print("Using OpenSSL " + str(openssl_ver))
print("Using GnuPG " + str(gpg_ver))
print("Using pgpdump " + str(pgpdump_ver))
print("")
1 change: 1 addition & 0 deletions tests/gnupghome/gpg-agent.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
allow-loopback-pinentry
5 changes: 0 additions & 5 deletions tests/testdata/pgpy.gpg.conf → tests/gnupghome/gpg.conf
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,6 @@
# always expert
expert
trust-model always
# keyring stuff
no-default-keyring
keyring testkeys.pub.gpg
secret-keyring testkeys.sec.gpg
trustdb-name testkeys.trust.gpg

# don't try to auto-locate keys except in the local keyring(s)
no-auto-key-locate
Expand Down
Loading

0 comments on commit ca7ca6b

Please sign in to comment.