Skip to content

Commit

Permalink
Merge pull request #5 from rmlibre/develop
Browse files Browse the repository at this point in the history
v0.5.7 update - instance-isolated identities

This update adds automatic enforcement of instance identities, preventing one instance from using the gpg-agent cache to access secret keys which aren't associated to that instance's key. This check is enforced by the ``passphrase`` attribute. If a user creates instance identities with the same passphrase, then this check can be side-stepped.
  • Loading branch information
rmlibre authored Dec 25, 2019
2 parents a0ff926 + 34b7b90 commit a65a95a
Show file tree
Hide file tree
Showing 7 changed files with 165 additions and 13 deletions.
26 changes: 26 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,32 @@
=============


Changes for version 0.5.7
=========================

Minor Changes
-------------

- Tests added to include checks for instance-isolated identities.


Major Changes
-------------

- ``reset_daemon()`` calls added to ``decrypt()``, ``verify()``, ``sign()``
& ``encrypt()``. This call kills the gpg-agent process & restarts it,
which in turn wipes the caching of secret keys available on the system
without a passphrase. This is crucial for users of applications with
multiple GnuPG objects that handle separate key identities. That's
because these methods will now throw ``PermissionError`` or ``LookupError``
if a private key operation is needed from an instance which is already
assigned to another private key in the keyring. This gives some important
anonymity protections to users.
- More improvements to error reporting.




Changes for version 0.5.6
=========================

Expand Down
26 changes: 26 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,32 @@ After a user no longer considers a key useful, or wants to dissociate from the k
=============


Changes for version 0.5.7
=========================

Minor Changes
-------------

- Tests added to include checks for instance-isolated identities.


Major Changes
-------------

- ``reset_daemon()`` calls added to ``decrypt()``, ``verify()``, ``sign()``
& ``encrypt()``. This call kills the gpg-agent process & restarts it,
which in turn wipes the caching of secret keys available on the system
without a passphrase. This is crucial for users of applications with
multiple GnuPG objects that handle separate key identities. That's
because these methods will now throw ``PermissionError`` or ``LookupError``
if a private key operation is needed from an instance which is already
assigned to another private key in the keyring. This gives some important
anonymity protections to users.
- More improvements to error reporting.




Changes for version 0.5.6
=========================

Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
setup(
name="tiny_gnupg",
license="GPLv3",
version="0.5.6",
version="0.5.7",
description=description,
long_description=long_description,
url="https://github.com/rmlibre/tiny_gnupg",
Expand Down
51 changes: 51 additions & 0 deletions tests/test_tiny_gnupg.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,57 @@ def test_export_import(gpg):
"""Successfully blocked non-boolean"""


def test_isolated_identities(gpg):
anon = GnuPG("anon_user", "[email protected]", "rubbish_pw")
anon.gen_key()
anon_uid = anon.fingerprint
### decrypting
enc_msg = gpg.encrypt("hi!", anon_uid)
msg = anon.decrypt(enc_msg)
try:
failed = False
gpg.decrypt(enc_msg)
except LookupError:
failed = True
"""
Same user succefully prevented from decrypting message sent to a
different identity.
"""
finally:
assert failed
### signatures
sig_0 = anon.sign("message", local_user=anon_uid)
sig_1 = anon.sign("message")
try:
failed = False
gpg.sign("message", local_user=anon_uid)
except PermissionError:
failed = True
"""
Same user successfully prevented from signing message with the
secret key associated with another identity.
"""
finally:
assert failed
### encrypting
try:
failed = False
gpg.encrypt("greetings", anon_uid, local_user=anon_uid)
except PermissionError:
failed = True
"""
Same user successfully prevented from signing an encrypted message
with the secret key associated with another identity.
"""
finally:
assert failed
while True:
try:
anon.delete(anon.email)
except:
break


def test_cipher(gpg):
test_email = "[email protected]"
run(gpg.network_import(test_email))
Expand Down
26 changes: 26 additions & 0 deletions tiny_gnupg.egg-info/PKG-INFO
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,32 @@ Description: tiny_gnupg - A small-as-possible solution for handling GnuPG ed2551
=============


Changes for version 0.5.7
=========================

Minor Changes
-------------

- Tests added to include checks for instance-isolated identities.


Major Changes
-------------

- ``reset_daemon()`` calls added to ``decrypt()``, ``verify()``, ``sign()``
& ``encrypt()``. This call kills the gpg-agent process & restarts it,
which in turn wipes the caching of secret keys available on the system
without a passphrase. This is crucial for users of applications with
multiple GnuPG objects that handle separate key identities. That's
because these methods will now throw ``PermissionError`` or ``LookupError``
if a private key operation is needed from an instance which is already
assigned to another private key in the keyring. This gives some important
anonymity protections to users.
- More improvements to error reporting.




Changes for version 0.5.6
=========================

Expand Down
2 changes: 1 addition & 1 deletion tiny_gnupg/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@
# All rights reserved.
#

__version__ = "0.5.6"
__version__ = "0.5.7"

from .tiny_gnupg import GnuPG, run, __all__
45 changes: 34 additions & 11 deletions tiny_gnupg/tiny_gnupg.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,9 +186,28 @@ def encode_inputs(self, *inputs):

def read_output(self, command=(), inputs=b"", **kw):
"""Quotes terminal escape characters & runs user commands"""
return check_output(
[quote(part) for part in command], input=inputs, **kw
).decode()
try:
return check_output(
[quote(part) for part in command], input=inputs, **kw
).decode()
except Exception as source:
error = source
try:
kw.pop("stderr") if "stderr" in kw else 0
check_output(
[quote(part) for part in command],
input=inputs,
stderr=STDOUT,
**kw,
).decode()
except CalledProcessError as permissions_check:
notice = "Passphrase wrong, inexistent key, or invalid rights "
notice += "to access secret key."
warning = PermissionError(notice)
warning.inputs = inputs
warning.command = command
warning.output = permissions_check.output.decode()
raise warning if "Bad passphrase" in warning.output else error

def gen_key(self):
"""
Expand Down Expand Up @@ -369,6 +388,7 @@ def encrypt(self, message="", uid="", sign=True, local_user=""):
matching ``local_user`` or defaults to instance key. Optionally,
if ``sign`` == False, ``message`` won't be signed.
"""
self.reset_daemon() if sign else 0
uid = self.key_fingerprint(uid) # avoid wkd lookups
command = self.command(
"--command-fd",
Expand Down Expand Up @@ -401,6 +421,7 @@ async def auto_encrypt(

def decrypt(self, message=""):
"""Decrypts ``message`` autodetecting correct key from keyring"""
self.reset_daemon()
fingerprint = self.packet_fingerprint(message)
fingerprint = self.key_fingerprint(fingerprint)
try:
Expand All @@ -412,13 +433,13 @@ def decrypt(self, message=""):
try:
self.read_output(command, inputs, stderr=STDOUT)
except CalledProcessError as error:
output = error.output
error_lines = output.decode().strip().split("\n")
error_lines = error.output.decode().strip().split("\n")
sentinel = "gpg: using"
uid = [line[-40:] for line in error_lines if sentinel in line]
notice = f"UID '{uid}' not in package keyring"
uid = uid[-1] if uid else ""
notice = f"UID '{uid}' not in the instance's keyring."
warning = LookupError(notice)
warning.value = uid[-1] if uid else fingerprint
warning.value = uid if uid else fingerprint
raise warning

async def auto_decrypt(self, message=""):
Expand All @@ -438,6 +459,7 @@ def sign(self, target="", local_user="", *, key=False):
uid or the instance default. Optionally signs ``target`` message
if ``key`` == False.
"""
self.reset_daemon()
if key == True: # avoid truthiness
command = self.command(
"--local-user",
Expand All @@ -464,6 +486,7 @@ def verify(self, message=""):
Verifies signed ``message`` if the corresponding public key is
in the local keyring.
"""
self.reset_daemon()
fingerprint = self.packet_fingerprint(message)
fingerprint = self.key_fingerprint(fingerprint)
try:
Expand All @@ -489,15 +512,15 @@ async def auto_verify(self, message=""):

def raw_list_keys(self, uid="", secret=False):
"""Returns the terminal output of the --list-keys ``uid`` option"""
secret = "-secret" if secret else ""
secret = "secret-" if secret else ""
if uid:
command = self.command(f"--list{secret}-keys", uid)
command = self.command(f"--list-{secret}keys", uid)
else:
command = self.command(f"--list{secret}-keys")
command = self.command(f"--list-{secret}keys")
try:
return self.read_output(command)
except CalledProcessError:
notice = f"UID '{uid}' not in package keyring"
notice = f"UID '{uid}' not in package {secret}keyring"
warning = LookupError(notice)
warning.value = uid
raise warning
Expand Down

0 comments on commit a65a95a

Please sign in to comment.