- PEP8
- 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:
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
34
pgpy/pgp.py
34
pgpy/pgp.py
@@ -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))
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
29
setup.py
29
setup.py
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
12
tox.ini
@@ -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
11
tox.sh
@@ -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 $*
|
||||
Reference in New Issue
Block a user