Add model test. Finish file signing method. Add key signing exceptions.

This commit is contained in:
Roberto Rosario
2016-03-22 18:02:40 -04:00
parent dc5d25fd00
commit 189cda437f
10 changed files with 316 additions and 65 deletions

View File

@@ -7,21 +7,9 @@ from .models import Key
@admin.register(Key) @admin.register(Key)
class KeyAdmin(admin.ModelAdmin): class KeyAdmin(admin.ModelAdmin):
#date_hierarchy = 'datetime' list_display = (
list_display = ('key_id', 'user_id', 'key_type') 'key_id', 'user_id', 'creation_date', 'expiration_date', 'key_type'
#readonly_fields = list_display
"""
key_id = models.CharField(
max_length=16, unique=True, verbose_name=_('Key ID')
) )
creation_date = models.DateField(verbose_name=_('Creation date')) list_filter = ('key_type',)
expiration_date = models.DateField(verbose_name=_('Expiration date')) readonly_fields = list_display + ('fingerprint', 'length', 'algorithm')
fingerprint = models.CharField( search_fields = ('key_id', 'user_id',)
max_length=40, verbose_name=_('Fingerprint')
)
length = models.PositiveIntegerField(verbose_name=_('Length'))
algorithm = models.PositiveIntegerField(verbose_name=_('Algorithm'))
user_id = models.TextField(verbose_name=_('User ID'))
key_type = models.CharField(max_length=3, verbose_name=_('Type'))
"""

View File

@@ -39,3 +39,15 @@ class KeyDoesNotExist(GPGException):
class KeyImportError(GPGException): class KeyImportError(GPGException):
pass pass
class NeedPassphrase(GPGException):
"""
Passphrase is needed but none was provided
"""
class PassphraseError(GPGException):
"""
Passphrase provided is incorrect
"""

View File

@@ -7,6 +7,14 @@ KEY_TYPES = {
'sec': _('Secret'), 'sec': _('Secret'),
} }
KEY_TYPE_PUBLIC = 'pub'
KEY_TYPE_SECRET = 'sec'
KEY_TYPE_CHOICES = (
(KEY_TYPE_PUBLIC, _('Public')),
(KEY_TYPE_SECRET, _('Secret')),
)
KEY_CLASS_RSA = 'RSA' KEY_CLASS_RSA = 'RSA'
KEY_CLASS_DSA = 'DSA' KEY_CLASS_DSA = 'DSA'
KEY_CLASS_ELG = 'ELG-E' KEY_CLASS_ELG = 'ELG-E'

View File

@@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('django_gpg', '0001_initial'),
]
operations = [
migrations.RemoveField(
model_name='key',
name='data',
),
migrations.AddField(
model_name='key',
name='key_data',
field=models.TextField(default='', verbose_name='Key data'),
preserve_default=False,
),
]

View File

@@ -0,0 +1,59 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('django_gpg', '0002_auto_20160322_1756'),
]
operations = [
migrations.AlterField(
model_name='key',
name='algorithm',
field=models.PositiveIntegerField(verbose_name='Algorithm', editable=False),
),
migrations.AlterField(
model_name='key',
name='creation_date',
field=models.DateField(verbose_name='Creation date', editable=False),
),
migrations.AlterField(
model_name='key',
name='expiration_date',
field=models.DateField(verbose_name='Expiration date', null=True, editable=False, blank=True),
),
migrations.AlterField(
model_name='key',
name='fingerprint',
field=models.CharField(verbose_name='Fingerprint', unique=True, max_length=40, editable=False),
),
migrations.AlterField(
model_name='key',
name='key_data',
field=models.TextField(verbose_name='Key data', editable=False),
),
migrations.AlterField(
model_name='key',
name='key_id',
field=models.CharField(verbose_name='Key ID', unique=True, max_length=16, editable=False),
),
migrations.AlterField(
model_name='key',
name='key_type',
field=models.CharField(verbose_name='Type', max_length=3, editable=False),
),
migrations.AlterField(
model_name='key',
name='length',
field=models.PositiveIntegerField(verbose_name='Length', editable=False),
),
migrations.AlterField(
model_name='key',
name='user_id',
field=models.TextField(verbose_name='User ID', editable=False),
),
]

View File

@@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('django_gpg', '0003_auto_20160322_1810'),
]
operations = [
migrations.AlterField(
model_name='key',
name='key_data',
field=models.TextField(verbose_name='Key data'),
),
migrations.AlterField(
model_name='key',
name='key_type',
field=models.CharField(verbose_name='Type', max_length=3, editable=False, choices=[('pub', 'Public'), ('sec', 'Secret')]),
),
]

View File

@@ -1,10 +1,5 @@
from __future__ import absolute_import, unicode_literals from __future__ import absolute_import, unicode_literals
try:
from cStringIO import StringIO
except ImportError:
from StringIO import StringIO
from datetime import date from datetime import date
import logging import logging
import os import os
@@ -13,20 +8,37 @@ import tempfile
import gnupg import gnupg
from django.conf import settings from django.core.exceptions import ValidationError
from django.core.exceptions import PermissionDenied from django.db import models
from django.core.files import File
from django.core.urlresolvers import reverse
from django.db import models, transaction
from django.utils.encoding import python_2_unicode_compatible from django.utils.encoding import python_2_unicode_compatible
from django.utils.timezone import now from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext, ugettext_lazy as _
from .settings import setting_gpg_path, setting_keyservers from .literals import KEY_TYPE_CHOICES, KEY_TYPE_SECRET
from .exceptions import NeedPassphrase, PassphraseError
from .settings import setting_gpg_path, setting_keyserver
ERROR_MSG_NEED_PASSPHRASE = 'NEED_PASSPHRASE'
ERROR_MSG_BAD_PASSPHRASE = 'BAD_PASSPHRASE'
ERROR_MSG_GOOD_PASSPHRASE = 'GOOD_PASSPHRASE'
OUTPUT_MESSAGE_CONTAINS_PRIVATE_KEY = 'Contains private key'
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def gpg_command(function):
temporary_directory = tempfile.mkdtemp()
os.chmod(temporary_directory, 0x1C0)
gpg = gnupg.GPG(
gnupghome=temporary_directory, gpgbinary=setting_gpg_path.value
)
result = function(gpg=gpg)
shutil.rmtree(temporary_directory)
return result
class KeyManager(models.Manager): class KeyManager(models.Manager):
def receive_key(self, key_id): def receive_key(self, key_id):
temporary_directory = tempfile.mkdtemp() temporary_directory = tempfile.mkdtemp()
@@ -37,7 +49,7 @@ class KeyManager(models.Manager):
gnupghome=temporary_directory, gpgbinary=setting_gpg_path.value gnupghome=temporary_directory, gpgbinary=setting_gpg_path.value
) )
import_results = gpg.recv_keys(setting_keyservers.value[0], key_id) import_results = gpg.recv_keys(setting_keyserver.value, key_id)
key_data = gpg.export_keys(import_results.fingerprints[0]) key_data = gpg.export_keys(import_results.fingerprints[0])
@@ -52,29 +64,48 @@ class KeyManager(models.Manager):
gnupghome=temporary_directory, gpgbinary=setting_gpg_path.value gnupghome=temporary_directory, gpgbinary=setting_gpg_path.value
) )
result = gpg.search_keys(query=query, keyserver=setting_keyservers.value[0]) result = gpg.search_keys(
query=query, keyserver=setting_keyserver.value
)
shutil.rmtree(temporary_directory) shutil.rmtree(temporary_directory)
return result return result
def public_keys(self):
return self.filter(key_type='pub')
def private_keys(self):
return self.filter(key_type='')
@python_2_unicode_compatible @python_2_unicode_compatible
class Key(models.Model): class Key(models.Model):
data = models.TextField(verbose_name=_('Data')) key_data = models.TextField(verbose_name=_('Key data'))
key_id = models.CharField( key_id = models.CharField(
max_length=16, unique=True, verbose_name=_('Key ID') editable=False, max_length=16, unique=True, verbose_name=_('Key ID')
)
creation_date = models.DateField(
editable=False, verbose_name=_('Creation date')
) )
creation_date = models.DateField(verbose_name=_('Creation date'))
expiration_date = models.DateField( expiration_date = models.DateField(
blank=True, null=True, verbose_name=_('Expiration date') blank=True, editable=False, null=True,
verbose_name=_('Expiration date')
) )
fingerprint = models.CharField( fingerprint = models.CharField(
max_length=40, unique=True, verbose_name=_('Fingerprint') editable=False, max_length=40, unique=True,
verbose_name=_('Fingerprint')
)
length = models.PositiveIntegerField(
editable=False, verbose_name=_('Length')
)
algorithm = models.PositiveIntegerField(
editable=False, verbose_name=_('Algorithm')
)
user_id = models.TextField(editable=False, verbose_name=_('User ID'))
key_type = models.CharField(
choices=KEY_TYPE_CHOICES, editable=False, max_length=3,
verbose_name=_('Type')
) )
length = models.PositiveIntegerField(verbose_name=_('Length'))
algorithm = models.PositiveIntegerField(verbose_name=_('Algorithm'))
user_id = models.TextField(verbose_name=_('User ID'))
key_type = models.CharField(max_length=3, verbose_name=_('Type'))
objects = KeyManager() objects = KeyManager()
@@ -82,44 +113,49 @@ class Key(models.Model):
verbose_name = _('Key') verbose_name = _('Key')
verbose_name_plural = _('Keys') verbose_name_plural = _('Keys')
def clean(self):
def import_key(gpg):
return gpg.import_keys(key_data=self.key_data)
import_results = gpg_command(function=import_key)
if not import_results.count:
raise ValidationError('Invalid key data')
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
temporary_directory = tempfile.mkdtemp() temporary_directory = tempfile.mkdtemp()
logger.debug('temporary_directory: %s', temporary_directory)
gpg = gnupg.GPG( gpg = gnupg.GPG(
gnupghome=temporary_directory, gpgbinary=setting_gpg_path.value gnupghome=temporary_directory, gpgbinary=setting_gpg_path.value
) )
import_results = gpg.import_keys(key_data=self.data) import_results = gpg.import_keys(key_data=self.key_data)
logger.debug('import_results.results: %s', import_results.results) key_info = gpg.list_keys(keys=import_results.fingerprints[0])[0]
logger.debug('import_results.fingerprints: %s', import_results.fingerprints)
key_data = gpg.list_keys(keys=import_results.fingerprints[0])[0] logger.debug('key_info: %s', key_info)
logger.debug('key_data: %s', key_data)
shutil.rmtree(temporary_directory) shutil.rmtree(temporary_directory)
self.key_id = key_data['keyid'] self.key_id = key_info['keyid']
self.algorithm = key_data['algo'] self.algorithm = key_info['algo']
self.creation_date = date.fromtimestamp(int(key_data['date'])) self.creation_date = date.fromtimestamp(int(key_info['date']))
if key_data['expires']: if key_info['expires']:
self.expiration_date = date.fromtimestamp(int(key_data['expires'])) self.expiration_date = date.fromtimestamp(int(key_info['expires']))
self.fingerprint = key_data['fingerprint'] self.fingerprint = key_info['fingerprint']
self.length = int(key_data['length']) self.length = int(key_info['length'])
self.user_id = key_data['uids'][0] self.user_id = key_info['uids'][0]
self.key_type = key_data['type'] if OUTPUT_MESSAGE_CONTAINS_PRIVATE_KEY in import_results.results[0]['text']:
self.key_type = KEY_TYPE_SECRET
else:
self.key_type = key_info['type']
super(Key, self).save(*args, **kwargs) super(Key, self).save(*args, **kwargs)
def __str__(self): def __str__(self):
return self.key_id return self.key_id
def sign_file(self, file_object, passphrase=None, clearsign=True, detach=False, binary=False): def sign_file(self, file_object, passphrase=None, clearsign=True, detached=False, binary=False, output=None):
output = StringIO()
temporary_directory = tempfile.mkdtemp() temporary_directory = tempfile.mkdtemp()
gpg = gnupg.GPG( gpg = gnupg.GPG(
@@ -128,3 +164,20 @@ class Key(models.Model):
import_results = gpg.import_keys(key_data=self.data) import_results = gpg.import_keys(key_data=self.data)
file_sign_results = gpg.sign_file(
file=file_object, keyid=import_results.fingerprints[0],
passphrase=passphrase, clearsign=clearsign, detach=detached,
binary=binary, output=output
)
shutil.rmtree(temporary_directory)
logger.debug('file_sign_results.stderr: %s', file_sign_results.stderr)
if ERROR_MSG_NEED_PASSPHRASE in file_sign_results.stderr:
if ERROR_MSG_BAD_PASSPHRASE in file_sign_results.stderr:
raise PassphraseError
elif ERROR_MSG_GOOD_PASSPHRASE not in file_sign_results.stderr:
raise NeedPassphrase
return file_sign_results

View File

@@ -8,10 +8,6 @@ from django.utils.translation import ugettext_lazy as _
from smart_settings import Namespace from smart_settings import Namespace
namespace = Namespace(name='django_gpg', label=_('Signatures')) namespace = Namespace(name='django_gpg', label=_('Signatures'))
setting_keyservers = namespace.add_setting(
global_name='SIGNATURES_KEYSERVERS', default=['pool.sks-keyservers.net'],
help_text=_('List of keyservers to be queried for unknown keys.')
)
setting_gpg_home = namespace.add_setting( setting_gpg_home = namespace.add_setting(
global_name='SIGNATURES_GPG_HOME', global_name='SIGNATURES_GPG_HOME',
default=os.path.join(settings.MEDIA_ROOT, 'gpg_home'), default=os.path.join(settings.MEDIA_ROOT, 'gpg_home'),
@@ -24,3 +20,11 @@ setting_gpg_path = namespace.add_setting(
global_name='SIGNATURES_GPG_PATH', default='/usr/bin/gpg', global_name='SIGNATURES_GPG_PATH', default='/usr/bin/gpg',
help_text=_('Path to the GPG binary.'), is_path=True help_text=_('Path to the GPG binary.'), is_path=True
) )
setting_keyserver = namespace.add_setting(
global_name='SIGNATURES_KEYSERVER', default='pool.sks-keyservers.net',
help_text=_('Keyserver used to query for keys.')
)
setting_keyservers = namespace.add_setting(
global_name='SIGNATURES_KEYSERVERS', default=['pool.sks-keyservers.net'],
help_text=_('List of keyservers to be queried for unknown keys.')
)

View File

@@ -4,3 +4,66 @@ TEST_GPG_HOME = '/tmp/test_gpg_home'
TEST_KEY_ID = '607138F1AECC5A5CA31CB7715F3F7F75D210724D' TEST_KEY_ID = '607138F1AECC5A5CA31CB7715F3F7F75D210724D'
TEST_KEYSERVERS = ['pool.sks-keyservers.net'] TEST_KEYSERVERS = ['pool.sks-keyservers.net']
TEST_UIDS = 'Roberto Rosario' TEST_UIDS = 'Roberto Rosario'
TEST_KEY_DATA = '''-----BEGIN PGP PRIVATE KEY BLOCK-----
Version: GnuPG v1
lQO+BFbxfC8BCACnUZoD96W4+CSIaU9G8I08kXu2zJLzy2XgUtwLx8VQ8dOHr0E/
UembHkjMeH6Gjxu2Yrrbl9/Anzd+lkP0L9BV7WqjXpHmPxuaRlsrNXuMyX0YWtjo
zvCo/mVBKEt1aEejuE6YbeZdBQraym32ew8hTXQhPwqbKPC9LTUa2tDjkJHs0DLU
5Hvg2/16IYd94ZHAH+wOa4WrR/6wU1VBfFCGBl+xbSvburLYDwhNZC9+sIu61BO8
fZh48IIQ89Hin7cS/ovHTBF2Sr3n5yRzatV2eXXmT5AQdpTEpD3HPF82HXNRrSUK
I+BIoIGXnPg3wotOyahFGrC8RluY7QhU/KBdABEBAAH+AwMCyBnD0YX+KwtgKrBg
Nxz+lWc6bWQ4CvdxW4rlLTujXBbTYQ0YUpZ44qLXhq9Yso7760LF/ZZK4I12AZ+J
PCxubmYCBKg7HIHG1/tT6ACJyoWhCaO2rNXx7zh3SnYFNjvEoCUXoEoupoZ/Hk6J
NGCdJPUZe4mTY9lVHTSnwPusyGeSu9i51J4kREb0E1sN9UgMHNoJawu5BJw0Yl97
wD0U1cP93BB9FA+3KHUZDcj0v5exSkvWO1HQKzkZAaWOPfHoGCVRRBe4fYhjgumv
cbu7p1ve4ysooOO28DD/bIgbLA9swQjJT9CgwTnudmrn+3PEY9ghPFm4pLjUMWBK
nkBsSGQ1y7rCeGNGg5lAAKQfzL7gseiS0f+lmfSXsl1VTFWI89cCwnP7rTYHjsyS
Fs1V5/HhwCUL3SVJL+p6VMtZ4VWVlZ+Hm27hD0VYnmvd/cO8h14NRF3R/If7Ut+8
nqDwwtxTUPcDLzs2gbjGt9XhpVXCvoUExxZuf/q91wTUJGQ96wjKOopyH67i22m/
Orr29VGdzaE9iLe+cicf4ZwwKLzLczTVSjk2KUpSFx5KaFMcekHaBo+h1ABYfYQd
DE+3zKnuVMgF3Z2VXdKj4meibByc0BvrILLhcZ08eqWAd+Duyo2eSZyWV+1FKbKw
qtzudRxKMtEh5h4y1vn4eRd1zEQPBG9m9CTLUeO0l60Q1/gy/VwmAsiJZkcI8KSS
9HVw672+Q3gAcblLyYJrIvKT2EyLD2rSijxgx61//s9UR9k0a9iFXB11FtQ6N3Ct
+msBMO3wFGviZ2iqWiMYiGDoIXMil6G1KtJLkDc5uDXFMc5see12vlsFmEDFScvj
Nnslh9ajbC+mfgRPZFtprtoaGFUd4VRDM7/rr7kuuCZFQ1QebEVjJjmQnfgpowa7
C7QhRXhhbXBsZSBhZG1pbiA8YWRtaW5AZXhhbXBsZS5jb20+iQE4BBMBAgAiAhsD
BgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAUCVvGFyAAKCRBBJenFcfN4rIwSB/4l
PbS0F8tGtetzPIgPYerI2OwZDbVyrTVGbrY0ZJJfWXR0vyTJ38s3dZNC22ct3+g1
t1RVxFGssSZYW0StlPyb2u+5VUI4LDWmbaDL3QbN5KTkyXtGLaWUwJ/TC4EkAKEa
8HKqRpZOfeUj4gTIm2uwpYwihyVY2M6EW6we5DUOScX1kIO/VTB+QWChFcLEjZb3
LdSSOEKI56QHV/Sxn38jp0tD7E/yBBVw+HhFamhqwrYTnxy2/W/xHBvWQk34ZnTf
o/mZyBlWz5h6JaKhHyw0akRQbBSfo3huW6+RKI3QHj82f85zv01Uzvjxvaz/N5SP
/MuHTPgG8g69Bg4Ik6jjnQO+BFbxfC8BCADQYEUpx79976Ut5ZtMj3CNpndUWHB1
l2wa/vd+Gb6Yzm+/hu3t5GG8uxzFk9TC33G7/Ugyob2V0eVXS8rqIbiqbRW7Nmb6
RF4xeEZkUlLTmzXu9vJLRCW0f0ui+YJz6Rdgn4BCRJ+/OkLIoB9axDxDL+961ftw
LqBTK3IpQc+VwjBLPTofApJGjM/pExJDskAi4IJpd8sz5Djc7MkF/tANSWVdvNOA
lTIWZkfSiY2cThmC1WgL1KfSSYcFH0Z6/8M2qzF/9+D//j7WSq2GPahqVueVIE4r
CIi3ffayXdPsiEzgkZqJxeZyt8ht74qTgZhAhmIxnobrLg1nbwOVGx0tABEBAAH+
AwMCyBnD0YX+Kwtgqas29fXB07iu+YJbSEXDsg56zrdDBToOFODrpRsqQtVofRyO
1GVDt1qE8jJF+zxnxSWawFLwR3mUs8/RKmdOm9cLnsadjCSWXWXPgb0w5mzcaVBa
tn9CtnF2G30D77LtBrkhnKtmjpW2Etudd7wkBYtSL4mqADX+8SgbFlR5jYtlFcUl
6HziXFzFSDEJ3YOE4LMm39pk+p7Kn/1GvxLleXu46uQZU3yEUxmnrHFSmolehWJk
1OR6CZ4SDmsKyFF9aNJPo+0ytU/VyOOuruaEQwp6r+zuM9sanrZJVGwlN5PRhfmr
+TrUwStsh2sdKrQ10xDxBBp7xThR3wz3+REO2c6uIEIkXhSAOARK1EQGXpAeK35x
uAUief4yMMiBKweKADT9ic36xxmc52Ov7Nrkwgj8PXma3gWiktTPhGWLZQ/YdXTW
fV+IwDShJEmTPOAAtxqPljj9isC1qPS2ylJXrHyws3jz0xIMYe8GbgK1UmURC7DI
CAXC4K6x5/3Uuz+kirbQRXVt1c8O8azy/Zc9a97qodWd7NBHTAr8xk2JlcesjHmk
rGSKsm53sGV0PTweoi4n1YiEE6yBpCEoobcAABWfojCYIe5W54PTf7nkc+Ayzd9t
7ipTELF8RKHHBU42penurBAX+U3aSe6rUfhlTuVs8KykzT/4pQeUzndNYQos6KLH
C50CHXQbeLchdvDAzO0j80j8YGciRv0U+juaZMct+NCi/SNU46RD7qs85M9rB77/
GzOyrpsfVA0lfS5Z/g25+TqxEBTypiGMSh5Exza1Nwc2tIRExoYThW22SAM2PWqg
zw+aeNyC4uJWc9Qzf9sVMC1vaUUkf7cRMl8Lh7fNkX/sBUB4X8E3IG2UpeHKiWxp
UjRRioHbL6k8qEviaSyJLIkBJQQYAQIADwUCVvF8LwIbDAUJAA0vAAAKCRBBJenF
cfN4rAkxB/9Xyvsny6iBY1aFrIr2roOyXg1rX+NjEfo+HZqUIjpESQcviIatQcGB
1MVnvABVKCQWzQyoIkOyAmTUHKb0aLDynDblIctMVOy80wEtWRHcMQo4PzGUPJn3
hZOukiotQTeawLvyeoBY1M4FJaCvPYvUNl+PEUVLi2h2VFkANrtzJMjZpmI5iR62
h4oCbUV5JHhOyB+89Y1w8haFU9LrgOER2kXff1xU6wMfLdcO5ApV/sRJcNdYL7Cg
7nJLpOu33rvGW97adFMStZxXz4k+VXLErvtkT72XZX9TjS8hmIRxHKZgpb12ZkUe
8aeg3z/W+YctdRt81bi5isgM+oML9LAQ
=JZ5G
-----END PGP PRIVATE KEY BLOCK-----'''
TEST_KEY_ID = '4125E9C571F378AC'
TEST_KEY_FINGERPRINT = '6A24574E0A35004CDDFD22704125E9C571F378AC'

View File

@@ -0,0 +1,16 @@
from __future__ import unicode_literals
from django.test import TestCase
from ..models import Key
from .literals import TEST_KEY_DATA, TEST_KEY_FINGERPRINT, TEST_KEY_ID
class KeyTestCase(TestCase):
def test_key_instance_creation(self):
# Creating a Key instance is analogous to importing a key
key = Key.objects.create(key_data=TEST_KEY_DATA)
self.assertEqual(key.key_id, TEST_KEY_ID)
self.assertEqual(key.fingerprint, TEST_KEY_FINGERPRINT)