- modified tox.ini so that py32 can continue to be tested even though cryptography no longer supports it
 - Key Generation - #147 :
   - implemented new API method
   - added unit tests for generating keys, adding user ids, and adding new subkeys
   - added unit tests to test basic expected exception raising when trying to use incomplete keys
   - added a very basic key-completeness test to the @KeyAction decorator
 - added __contains__ to SignatureVerification
This commit is contained in:
Michael Greene
2015-06-01 17:45:41 -07:00
parent f05e9e9a37
commit 84567e085f
13 changed files with 184 additions and 77 deletions

View File

@@ -33,7 +33,7 @@ before_script:
- if [[ -z "$TOXENV" ]]; then export TOXENV=py${TRAVIS_PYTHON_VERSION//.}; fi
- if [[ "$TRAVIS_PYTHON_VERSION" == 'pypy' ]]; then export TOXENV=pypy; fi
- if [[ "$TRAVIS_PYTHON_VERSION" == 'pypy3' ]]; then export TOXENV=pypy3; fi
# use setup.py to invoke testing via coveralls
# run tox
script:
- tox
# and report coverage to coveralls, but only if this was a pytest run

View File

@@ -145,12 +145,23 @@ class PubKeyAlgorithm(IntEnum):
# DiffieHellman = 0x15 # X9.42
@property
def can_sign(self):
return self in [PubKeyAlgorithm.RSAEncryptOrSign, PubKeyAlgorithm.DSA]
def can_gen(self):
return self in {PubKeyAlgorithm.RSAEncryptOrSign,
PubKeyAlgorithm.DSA}
@property
def can_encrypt(self): # pragma: no cover
return self in [PubKeyAlgorithm.RSAEncryptOrSign, PubKeyAlgorithm.ElGamal]
return self in {PubKeyAlgorithm.RSAEncryptOrSign, PubKeyAlgorithm.ElGamal}
@property
def can_sign(self):
return self in {PubKeyAlgorithm.RSAEncryptOrSign, PubKeyAlgorithm.DSA}
@property
def deprecated(self):
return self in {PubKeyAlgorithm.RSAEncrypt,
PubKeyAlgorithm.RSASign,
PubKeyAlgorithm.FormerlyElGamalEncryptOrSign}
class CompressionAlgorithm(IntEnum):

View File

@@ -2,6 +2,7 @@
"""
import contextlib
import functools
import six
import warnings
from singledispatch import singledispatch
@@ -101,8 +102,16 @@ class KeyAction(object):
"".format(attr=attr, eval=str(expected), got=str(getattr(key, attr))))
def __call__(self, action):
@functools.wraps(action)
# @functools.wraps(action)
@six.wraps(action)
def _action(key, *args, **kwargs):
if key._key is None:
raise PGPError("No key!")
# if a key is in the process of being created, it needs to be allowed to certify its own user id
if len(key._uids) == 0 and key.is_primary and action is not key.certify.__wrapped__:
raise PGPError("Key is not complete - please add a User ID!")
with self.usage(key, kwargs.get('user', None)) as _key:
self.check_attributes(key)

View File

@@ -25,6 +25,7 @@ from .types import MPI
from .types import MPIs
from ..constants import HashAlgorithm
from ..constants import PubKeyAlgorithm
from ..constants import String2KeyType
from ..constants import SymmetricKeyAlgorithm
@@ -635,6 +636,10 @@ class PrivKey(PubKey):
def _generate(self, key_size):
"""Generate a new PrivKey"""
def _compute_chksum(self):
chs = sum(sum(bytearray(c.to_mpibytes())) for c in self) % 65536
self.chksum = bytearray(self.int_to_bytes(chs, 2))
def publen(self):
return sum(len(i) for i in super(self.__class__, self).__iter__())
@@ -724,8 +729,7 @@ class RSAPriv(PrivKey, RSAPub):
del pkn
del pk
chs = sum(sum(c.to_mpibytes()) for c in self) % 65536
self.chksum = bytearray(self.int_to_bytes(chs, 2))
self._compute_chksum()
def parse(self, packet):
super(RSAPriv, self).parse(packet)
@@ -802,8 +806,7 @@ class DSAPriv(PrivKey, DSAPub):
del pkn
del pk
chs = sum(sum(c.to_mpibytes()) for c in self) % 65536
self.chksum = bytearray(self.int_to_bytes(chs, 2))
self._compute_chksum()
def parse(self, packet):
super(DSAPriv, self).parse(packet)
@@ -848,7 +851,7 @@ class ElGPriv(PrivKey, ElGPub):
raise NotImplementedError()
def _generate(self, key_size):
return NotImplemented
raise NotImplementedError(PubKeyAlgorithm.ElGamal)
def parse(self, packet):
super(ElGPriv, self).parse(packet)

View File

@@ -765,6 +765,8 @@ class PrivKeyV4(PrivKey, PubKeyV4):
# build a key packet
pk = PrivKeyV4()
pk.pkalg = key_algorithm
if pk.keymaterial is None:
raise NotImplementedError(key_algorithm)
pk.keymaterial._generate(key_size)
pk.update_hlen()
return pk

View File

@@ -71,9 +71,9 @@ class SubPacket(Dispatchable):
self.header = Header()
# if self.__typeid__ not in [-1, None]:
if (self.header.typeid == 0x00
and (not hasattr(self.__typeid__, '__abstractmethod__'))
and (self.__typeid__ not in [-1, None])):
if (self.header.typeid == 0x00 and
(not hasattr(self.__typeid__, '__abstractmethod__')) and
(self.__typeid__ not in [-1, None])):
self.header.typeid = self.__typeid__
def __bytearray__(self):

View File

@@ -17,7 +17,6 @@ import six
from datetime import datetime
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
@@ -45,6 +44,7 @@ from .packet import Packet
from .packet import Primary
from .packet import Private
from .packet import PrivKeyV4
from .packet import PrivSubKeyV4
from .packet import Public
from .packet import Sub
from .packet import UserID
@@ -1221,6 +1221,10 @@ class PGPKey(PGPObject, Armorable):
# new private key shell first
key = PGPKey()
if key_algorithm in {PubKeyAlgorithm.RSAEncrypt, PubKeyAlgorithm.RSASign}: # pragma: no cover
warnings.warn('{:s} is deprecated - generating key using RSAEncryptOrSign')
key_algorithm = PubKeyAlgorithm.RSAEncryptOrSign
# generate some key data to match key_algorithm and key_size
key._key = PrivKeyV4.new(key_algorithm, key_size)
@@ -1395,6 +1399,32 @@ class PGPKey(PGPObject, Armorable):
u._parent = None
self._uids.remove(u)
def add_subkey(self, key, **prefs):
if self.is_public:
raise PGPError("Cannot add a subkey to a public key. Add the subkey to the private component first!")
if key.is_public:
raise PGPError("Cannot add a public key as a subkey to this key")
if key.is_primary:
if len(key._children) > 0:
raise PGPError("Cannot add a key that already has subkeys as a subkey!")
# convert key into a subkey
npk = PrivSubKeyV4()
npk.pkalg = key._key.pkalg
npk.created = key._key.created
npk.keymaterial = key._key.keymaterial
key._key = npk
key._key.update_hlen()
self._children[key.fingerprint.keyid] = key
key._parent = self
##TODO: skip this step if the key already has a subkey binding signature
bsig = self.bind(key, **prefs)
key |= bsig
def _get_key_flags(self, user=None):
if self.is_primary:
if user is not None:
@@ -1986,8 +2016,8 @@ class PGPKey(PGPObject, Armorable):
orphaned = []
# last holds the last non-signature thing processed
getpkt = lambda d: Packet(d) if len(d) > 0 else None
##TODO: see issue #141 and fix this better
getpkt = lambda d: Packet(d) if len(d) > 0 else None # flake8: noqa
# getpkt = iter(functools.partial(getpkt, data), None)
getpkt = filter(lambda p: p.header.tag != PacketTag.Trust, iter(functools.partial(getpkt, data), None))

View File

@@ -10,7 +10,6 @@ import collections
import operator
import os
import re
import sys
import warnings
from enum import EnumMeta
@@ -543,6 +542,9 @@ class SignatureVerification(object):
super(SignatureVerification, self).__init__()
self._subjects = []
def __contains__(self, item):
return item in {ii for i in self._subjects for ii in [i.signature, i.subject]}
def __len__(self):
return len(self._subjects)

View File

@@ -1,16 +1,32 @@
#!/usr/bin/python
# from distutils.core import setup
import importlib.machinery
import sys
from setuptools import setup
# this is dirty
import sys
sys.path.append('pgpy')
import _author
_loader = importlib.machinery.SourceFileLoader('_author', 'pgpy/_author.py')
_author = _loader.load_module()
# long_description is the contents of README.rst
with open('README.rst') as readme:
long_desc = readme.read()
_requires = [
'cryptography>=0.8',
'enum34',
'pyasn1',
'six',
'singledispatch',
]
if sys.version_info[:2] == (3, 2):
# cryptography dropped support for Python 3.2 in 0.9
# I still need to support Python 3.2 for the time being, and it's still feasible to do so currently,
# so just ensure we install 0.8.x on 3.2
_requires[0] = 'cryptography>=0.8,<0.9'
setup(
# metadata
name = 'PGPy',
@@ -49,10 +65,7 @@ setup(
"signature", ],
# dependencies
install_requires = ['cryptography==0.6',
'enum34',
'six',
'singledispatch'],
install_requires = _requires,
# urls
url = "https://github.com/SecurityInnovation/PGPy",

View File

@@ -27,10 +27,12 @@ from pgpy.constants import SignatureType
from pgpy.constants import SymmetricKeyAlgorithm
from pgpy.constants import TrustLevel
from pgpy.errors import PGPDecryptionError
from pgpy.errors import PGPError
from pgpy.packet.packets import PrivKeyV4
from pgpy.packet.packets import PrivSubKeyV4
def _read(f, mode='r'):
with open(f, mode) as ff:
@@ -196,11 +198,12 @@ class TestPGPKey(object):
'sigkey': [ PGPKey.from_file(f)[0] for f in sorted(glob.glob('tests/testdata/signatures/*.key.asc')) ],
'sigsig': [ PGPSignature.from_file(f) for f in sorted(glob.glob('tests/testdata/signatures/*.sig.asc')) ],
'sigsubj': sorted(glob.glob('tests/testdata/signatures/*.subj')),
'key_alg': [ PubKeyAlgorithm.RSAEncryptOrSign, PubKeyAlgorithm.DSA ]
'key_alg': [ PubKeyAlgorithm.RSAEncryptOrSign, PubKeyAlgorithm.DSA ],
}
string_sigs = dict()
timestamp_sigs = dict()
standalone_sigs = dict()
gen_keys = dict()
encmessage = []
@contextmanager
@@ -218,9 +221,6 @@ class TestPGPKey(object):
e.args += (warning.message,)
raise
def test_new(self):
pytest.skip("not implemented yet")
def test_protect(self):
pytest.skip("not implemented yet")
@@ -496,15 +496,42 @@ class TestPGPKey(object):
assert sv
def test_new_key(self, key_alg):
pytest.skip("not implemented yet")
# create a key and a user id and add the UID to the key
uid = PGPUID.new('Hugo Gernsback', 'Science Fiction Plus', 'hugo.gernsback@space.local')
key = PGPKey.new(PubKeyAlgorithm.RSAEncryptOrSign, 1024)
key.add_uid(uid, hashes=[HashAlgorithm.SHA224])
def test_new_subkey(self):
pytest.skip("not implemented yet")
# self-verify the key
assert key.verify(key)
def test_add_subkey(self):
# when this is implemented, it will replace the temporary test_bind_subkey below
# and test_revoke_subkey will be rewritten
pytest.skip("not implemented yet")
self.gen_keys[key_alg] = key
def test_new_subkey(self, key_alg):
key = self.gen_keys[key_alg]
subkey = PGPKey.new(PubKeyAlgorithm.RSAEncryptOrSign, 1024)
assert subkey._key
assert not isinstance(subkey._key, PrivSubKeyV4)
# now add the subkey to key and then verify it
key.add_subkey(subkey, usage={KeyFlags.EncryptCommunications})
# subkey should be a PrivSubKeyV4 now, not a PrivKeyV4
assert isinstance(subkey._key, PrivSubKeyV4)
# self-verify
sv = self.gen_keys[key_alg].verify(self.gen_keys[key_alg])
assert sv
assert subkey in sv
def test_gpg_verify_new_key(self, key_alg, write_clean, gpg_import, gpg_check_sigs):
# with GnuPG
key = self.gen_keys[key_alg]
with write_clean('tests/testdata/genkey.asc', 'w', str(key)), \
gpg_import('./genkey.asc') as kio:
assert 'invalid self-signature' not in kio
assert gpg_check_sigs(key.fingerprint.keyid, *[skid for skid in key._children.keys()])
def test_gpg_verify_key(self, targette_sec, write_clean, gpg_import, gpg_check_sigs):
# with GnuPG
@@ -513,41 +540,6 @@ class TestPGPKey(object):
assert 'invalid self-signature' not in kio
assert gpg_check_sigs(targette_sec.fingerprint.keyid)
def test_bind_subkey(self, sec, pub, write_clean, gpg_import, gpg_check_sigs):
# this is temporary, until subkey generation works
# replace the first subkey's binding signature with a new one
subkey = next(iter(pub.subkeys.values()))
old_usage = next(sig for sig in subkey._signatures if sig.type == SignatureType.Subkey_Binding).key_flags
subkey._signatures.clear()
with self.assert_warnings():
bsig = sec.bind(subkey, usage=old_usage)
assert bsig.type == SignatureType.Subkey_Binding
assert 'EmbeddedSignature' in bsig._signature.subpackets
subkey |= bsig
assert len([sig for sig in subkey._signatures if sig.type == SignatureType.Subkey_Binding]) == \
len([sig for sig in subkey._signatures if sig.type == SignatureType.PrimaryKey_Binding])
assert {SignatureType.Subkey_Binding, SignatureType.PrimaryKey_Binding} <= {sig.type for sig in subkey._signatures}
assert all(sig.embedded for sig in subkey._signatures if sig.type == SignatureType.PrimaryKey_Binding)
# verify with PGPy
sv = pub.verify(subkey)
assert bsig in iter(s.signature for s in sv._subjects)
assert sv
sv = pub.verify(pub)
assert bsig in iter(s.signature for s in sv._subjects)
assert sv
# verify with GPG
kfp = '{:s}.asc'.format(pub.fingerprint.shortid)
with write_clean(os.path.join('tests', 'testdata', kfp), 'w', str(pub)), \
gpg_import(os.path.join('.', kfp)) as kio:
assert 'invalid self-signature' not in kio
assert gpg_check_sigs(pub.fingerprint.keyid, subkey.fingerprint.keyid)
def test_revoke_key(self, sec, pub, write_clean, gpg_import, gpg_check_sigs):
with self.assert_warnings():
rsig = sec.revoke(pub, sigtype=SignatureType.KeyRevocation, reason=RevocationReason.Retired,
@@ -594,3 +586,7 @@ class TestPGPKey(object):
# and remove it, for good measure
subkey._signatures.remove(rsig)
assert rsig not in subkey
# tests where ordering is important

View File

@@ -14,6 +14,7 @@ from pgpy.types import Fingerprint
from pgpy.types import SignatureVerification
from pgpy.constants import HashAlgorithm
from pgpy.constants import PubKeyAlgorithm
from pgpy.constants import SymmetricKeyAlgorithm
from pgpy.errors import PGPDecryptionError
@@ -48,6 +49,15 @@ def targette_sec():
class TestPGPKey(object):
params = {
'key_alg': [ pka for pka in PubKeyAlgorithm if pka.can_gen and not pka.deprecated ],
'key_alg_unim': [ pka for pka in PubKeyAlgorithm if not pka.can_gen and not pka.deprecated ],
}
key_badsize = {
PubKeyAlgorithm.RSAEncryptOrSign: 256,
PubKeyAlgorithm.DSA: 512,
}
def test_unlock_pubkey(self, rsa_pub, recwarn):
with rsa_pub.unlock("QwertyUiop") as _unlocked:
assert _unlocked is rsa_pub
@@ -157,6 +167,26 @@ class TestPGPKey(object):
assert str(w.message) == "Incorrect crc24"
assert w.filename == __file__
def test_empty_key_action(self):
key = PGPKey()
with pytest.raises(PGPError):
key.sign('asdf')
def test_new_key_no_uid_action(self):
key = PGPKey.new(PubKeyAlgorithm.RSAEncryptOrSign, 1024)
with pytest.raises(PGPError):
key.sign('asdf')
def test_new_key_invalid_size(self, key_alg):
with pytest.raises(ValueError):
PGPKey.new(key_alg, self.key_badsize[key_alg])
def test_new_key_unimplemented_alg(self, key_alg_unim):
with pytest.raises(NotImplementedError):
PGPKey.new(key_alg_unim, 512)
class TestPGPKeyring(object):
kr = PGPKeyring(_read('tests/testdata/pubtest.asc'))

12
tox.ini
View File

@@ -12,8 +12,18 @@ ignore = E201,E202,E221,E251,E265,F821,N805
max-line-length = 160
[testenv]
deps = -rrequirements-test.txt
passenv = HOME ARCHFLAGS LDFLAGS CFLAGS INCLUDE LIB LD_LIBRARY_PATH
deps =
py{py,py3,27,33,34}: cryptography>=0.8
py32: cryptography>=0.8,<0.9
enum34
pyasn1
six>=1.7.2
singledispatch
pytest
pytest-cov
commands =
; ./.darwin-fix.sh
py.test --cov pgpy --cov-report term-missing tests/
[testenv:setup]

11
tox.sh
View File

@@ -2,9 +2,10 @@
# homebrew is installed and so is a brewed openssl
if [[ $(uname) == "Darwin" ]] && command -v brew &>/dev/null && brew list openssl &>/dev/null; then
env ARCHFLAGS="-arch x86_64" LDFLAGS="-L/usr/local/opt/openssl/lib" CFLAGS="-I/usr/local/opt/openssl/include" tox $*
# env ARCHFLAGS="-arch x86_64" LDFLAGS="-L/usr/local/opt/openssl/lib" CFLAGS="-I/usr/local/opt/openssl/include" tox $*
export ARCHFLAGS="-arch x86_64"
export LDFLAGS="-L/usr/local/opt/openssl/lib"
export CFLAGS="-I/usr/local/opt/openssl/include"
fi
else
tox $*
fi
tox $*