Verified Commit bf9fa332 by NavaL Committed by meskio

[feat] decryption interoperability, when the current key pair

        is renewed

- there is only one private inactive key that is the key
 expiring last among all inactive keys
- if there is an inactive key, decryption with it,  is tried
if it fails with the current active key.
parent 95f8a39c
......@@ -205,6 +205,71 @@ RZXoH+FTg9UAW87eqU610npOkT6cRaBxaMK/mDtGNdc=
-----END PGP PRIVATE KEY BLOCK-----
"""
# different private key to the same address
# A4337956D27DC89C Leap Test Key <leap@leap.se>
# no expiry date
DIFFERENT_KEY_FPR = "EFA5842560D6462AD9CCE7A2A4337956D27DC89C"
DIFFERENT_PRIVATE_KEY = """
-----BEGIN PGP PRIVATE KEY BLOCK-----
Version: GnuPG v1
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
=3mjv
-----END PGP PRIVATE KEY BLOCK-----
"""
# key 7FEE575A: public key "anotheruser <anotheruser@leap.se>"
KEY_FINGERPRINT_2 = "F6E2B572ADB84EA58BD2E9A57F9DFA687FEE575A"
PUBLIC_KEY_2 = """
......
......@@ -207,7 +207,47 @@ class KeyManager(object):
yield self.put_raw_key(
raw_key, address=address, validation=validation_level)
def get_key(self, address, private=False, fetch_remote=True):
@defer.inlineCallbacks
def get_key(self, address, private=False, active=True, fetch_remote=True):
"""
Return a key bound to address.
For an active key: first, search for the key in local storage.
If it is not available, then try to fetch from nickserver.
The inactive key is fetched locally, for the case of multiple keys
for the same address. This can be used to attempt decryption
from multiple keys.
:param address: The address bound to the key.
:type address: str
:param private: Look for a private key instead of a public one?
:type private: bool
:param active: Look for the current active key
:type private: bool
:param fetch_remote: If key not found in local storage try to fetch
from nickserver
:type fetch_remote: bool
:return: A Deferred which fires with an EncryptionKey bound to address,
or which fails with KeyNotFound if no key was found neither
locally or in keyserver or fail with KeyVersionError if the
key has a format not supported by this version of KeyManager
:rtype: Deferred
:raise UnsupportedKeyTypeError: if invalid key type
"""
if active:
key = yield self._get_key(address, private, fetch_remote)
defer.returnValue(key)
all_keys = yield self.get_all_keys(private)
inactive_keys = filter(lambda _key: not _key.is_active(), all_keys)
if inactive_keys:
inactive_keys = sorted(inactive_keys,
key=lambda _key: _key.expiry_date)
defer.returnValue(inactive_keys[-1])
def _get_key(self, address, private=False, fetch_remote=True):
"""
Return a key bound to address.
......@@ -462,7 +502,8 @@ class KeyManager(object):
fetch_remote=True):
"""
Decrypt data using private key from address and verify with public key
bound to verify address.
bound to verify address. If the decryption using the active private
key fails, then decription using the inactive key, if any, is tried.
:param data: The data to be decrypted.
:type data: str
......@@ -489,7 +530,7 @@ class KeyManager(object):
"""
@defer.inlineCallbacks
def decrypt(keys):
def _decrypt(keys):
pubkey, privkey = keys
decrypted, signed = yield self._openpgp.decrypt(
data, privkey, passphrase=passphrase, verify=pubkey)
......@@ -507,6 +548,24 @@ class KeyManager(object):
(pubkey.fingerprint,))
defer.returnValue((decrypted, signature))
@defer.inlineCallbacks
def decrypt_with_inactive_key(keys, original_decrypt_error):
verify_key, active_key = keys
inactive_key = yield self.get_key(address, private=True,
active=False)
if inactive_key:
result = yield _decrypt([verify_key, inactive_key])
defer.returnValue(result)
raise original_decrypt_error
@defer.inlineCallbacks
def decrypt(keys):
try:
result = yield _decrypt(keys)
except keymanager_errors.DecryptError as e:
result = yield decrypt_with_inactive_key(keys, e)
defer.returnValue(result)
dpriv = self.get_key(address, private=True)
dpub = defer.succeed(None)
if verify is not None:
......
......@@ -289,7 +289,7 @@ class OpenPGPKey(object):
return key, value
def has_expired(self):
return self.expiry_date < datetime.now()
return self.expiry_date and self.expiry_date < datetime.now()
def __iter__(self):
return self
......
......@@ -195,6 +195,70 @@ RZXoH+FTg9UAW87eqU610npOkT6cRaBxaMK/mDtGNdc=
-----END PGP PRIVATE KEY BLOCK-----
"""
# different private key to the same address
# A4337956D27DC89C Leap Test Key <leap@leap.se>
# no expiry date
DIFFERENT_KEY_FPR = "EFA5842560D6462AD9CCE7A2A4337956D27DC89C"
DIFFERENT_PRIVATE_KEY = """
-----BEGIN PGP PRIVATE KEY BLOCK-----
Version: GnuPG v1
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
=3mjv
-----END PGP PRIVATE KEY BLOCK-----
"""
# key 7FEE575A: public key "anotheruser <anotheruser@leap.se>"
KEY_FINGERPRINT_2 = "F6E2B572ADB84EA58BD2E9A57F9DFA687FEE575A"
PUBLIC_KEY_2 = """
......
......@@ -52,8 +52,8 @@ from common import (
KEY_EXPIRING_CREATION_DATE,
PRIVATE_EXPIRING_KEY,
NEW_PUB_KEY,
OLD_AND_NEW_KEY_ADDRESS
)
OLD_AND_NEW_KEY_ADDRESS,
DIFFERENT_PRIVATE_KEY, DIFFERENT_KEY_FPR)
NICKSERVER_URI = "http://leap.se/"
REMOTE_KEY_URL = "http://site.domain/key"
......@@ -195,6 +195,25 @@ class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase):
self.assertTrue(key.private)
@defer.inlineCallbacks
def test_create_and_get_two_private_keys_sets_first_key_inactive(self):
km = self._key_manager()
yield km._openpgp.put_raw_key(PRIVATE_KEY, ADDRESS)
yield km._openpgp.put_raw_key(DIFFERENT_PRIVATE_KEY, ADDRESS)
# get the key
inactive_key = yield km.get_key(ADDRESS, private=True,
active=False, fetch_remote=False)
active_key = yield km.get_key(ADDRESS, private=True,
active=True, fetch_remote=False)
self.assertEqual(
inactive_key.fingerprint.lower(), KEY_FINGERPRINT.lower())
self.assertEqual(
active_key.fingerprint.lower(), DIFFERENT_KEY_FPR.lower())
self.assertTrue(inactive_key.private)
self.assertTrue(active_key.private)
self.assertFalse(inactive_key.is_active())
self.assertTrue(active_key.is_active())
@defer.inlineCallbacks
def test_send_key(self):
"""
Test that request is well formed when sending keys to server.
......@@ -577,6 +596,25 @@ class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase):
self.assertIn(old_key.fingerprint[-16:], renewed_public_key.signatures)
@defer.inlineCallbacks
def test_key_regenerate_deactivate_the_old_private_key(self):
km = self._key_manager(user=ADDRESS_EXPIRING)
yield km._openpgp.put_raw_key(PRIVATE_EXPIRING_KEY, ADDRESS_EXPIRING)
old_key = yield km.get_key(ADDRESS_EXPIRING)
new_key = yield km.regenerate_key()
retrieved_old_key = yield km.get_key(ADDRESS_EXPIRING,
private=True, active=False)
renewed_public_key = yield km.get_key(ADDRESS_EXPIRING, private=False)
self.assertEqual(old_key.fingerprint,
retrieved_old_key.fingerprint)
self.assertNotEqual(old_key.fingerprint,
new_key.fingerprint)
self.assertEqual(new_key.fingerprint, renewed_public_key.fingerprint)
self.assertIn(old_key.fingerprint[-16:], renewed_public_key.signatures)
@defer.inlineCallbacks
def test_key_regenerate_resets_all_public_key_sign_used(self):
km = self._key_manager(user=ADDRESS_EXPIRING)
......@@ -630,6 +668,27 @@ class KeyManagerCryptoTestCase(KeyManagerWithSoledadTestCase):
self.assertEqual(signingkey.fingerprint, key.fingerprint)
@defer.inlineCallbacks
def test_keymanager_openpgp_decryption_tries_inactive_valid_key(self):
km = self._key_manager()
# put raw private key
yield km._openpgp.put_raw_key(PRIVATE_KEY, ADDRESS)
yield km._openpgp.put_raw_key(PRIVATE_KEY_2, ADDRESS_2)
# encrypt
encdata = yield km.encrypt(self.RAW_DATA, ADDRESS, sign=ADDRESS_2,
fetch_remote=False)
self.assertNotEqual(self.RAW_DATA, encdata)
# renew key
new_key = yield km.regenerate_key()
# decrypt
rawdata, signingkey = yield km.decrypt(
encdata, ADDRESS, verify=ADDRESS_2, fetch_remote=False)
self.assertEqual(self.RAW_DATA, rawdata)
key = yield km.get_key(ADDRESS_2, private=False, fetch_remote=False)
self.assertEqual(signingkey.fingerprint, key.fingerprint)
@defer.inlineCallbacks
def test_keymanager_openpgp_encrypt_decrypt_wrong_sign(self):
km = self._key_manager()
# put raw keys
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment