Add document signature verification app

This commit is contained in:
Roberto Rosario
2011-12-04 05:52:09 -04:00
parent 071fed8996
commit 071139c5bf
20 changed files with 884 additions and 0 deletions

View File

@@ -0,0 +1,19 @@
from django.utils.translation import ugettext_lazy as _
from documents.models import Document
from navigation.api import register_links, register_top_menu, \
register_model_list_columns, register_multi_item_links, \
register_sidebar_template
from main.api import register_diagnostic, register_maintenance_links
from permissions.api import register_permission, set_namespace_title
from project_setup.api import register_setup
PERMISSION_DOCUMENT_VERIFY = {'namespace': 'django_gpg', 'name': 'document_verify', 'label': _(u'Verify document signatures')}
# Permission setup
set_namespace_title('django_gpg', _(u'Signatures'))
register_permission(PERMISSION_DOCUMENT_VERIFY)
document_verify = {'text': _(u'Signatures'), 'view': 'document_verify', 'args': 'object.pk', 'famfam': 'text_signature', 'permissions': [PERMISSION_DOCUMENT_VERIFY]}
register_links(Document, [document_verify], menu_name='form_header')

267
apps/django_gpg/api.py Normal file
View File

@@ -0,0 +1,267 @@
import types
from StringIO import StringIO
from pickle import dumps
import gnupg
from django.core.files.base import File
from django.utils.translation import ugettext_lazy as _
from django_gpg.exceptions import GPGVerificationError, GPGSigningError, \
GPGDecryptionError, KeyDeleteError, KeyGenerationError, \
KeyFetchingError, KeyDoesNotExist
KEY_TYPES = {
'pub': _(u'Public'),
'sec': _(u'Secret'),
}
KEY_CLASS_RSA = 'RSA'
KEY_CLASS_DSA = 'DSA'
KEY_CLASS_ELG = 'ELG-E'
KEY_PRIMARY_CLASSES = (
((KEY_CLASS_RSA), _(u'RSA')),
((KEY_CLASS_DSA), _(u'DSA')),
)
KEY_SECONDARY_CLASSES = (
((KEY_CLASS_RSA), _(u'RSA')),
((KEY_CLASS_ELG), _(u'Elgamal')),
)
class Key(object):
@staticmethod
def get_key_id(fingerprint):
return fingerprint[-16:]
@classmethod
def get_all(cls, gpg, secret=False, exclude=None):
result = []
keys = gpg.gpg.list_keys(secret=secret)
if exclude:
excluded_id = exclude.key_id
else:
excluded_id = u''
for key in keys:
if not key['keyid'] in excluded_id:
key_instance = Key(
fingerprint=key['fingerprint'],
uids=key['uids'],
type=key['type'],
data=gpg.gpg.export_keys([key['keyid']], secret=secret)
)
result.append(key_instance)
return result
@classmethod
def get(cls, gpg, key_id, secret=False, search_keyservers=False):
if len(key_id) > 16:
# key_id is a fingerprint
key_id = Key.get_key_id(key_id)
keys = gpg.gpg.list_keys(secret=secret)
key = next((key for key in keys if key['keyid'] == key_id), None)
if not key:
if search_keyservers and secret==False:
try:
gpg.receive_key(key_id)
return Key(gpg, key_id)
except KeyFetchingError:
raise KeyDoesNotExist
else:
raise KeyDoesNotExist
key_instance = Key(
fingerprint=key['fingerprint'],
uids=key['uids'],
type=key['type'],
data=gpg.gpg.export_keys([key['keyid']], secret=secret)
)
return key_instance
def __init__(self, fingerprint, uids, type, data):
self.fingerprint = fingerprint
self.uids = uids
self.type = type
self.data = data
@property
def key_id(self):
return Key.get_key_id(self.fingerprint)
@property
def user_ids(self):
return u', '.join(self.uids)
def __str__(self):
return '%s "%s" (%s)' % (self.key_id, self.user_ids, KEY_TYPES.get(self.type, _(u'unknown')))
def __unicode__(self):
return unicode(self.__str__())
def __repr__(self):
return self.__unicode__()
class GPG(object):
def __init__(self, binary_path=None, home=None, keyring=None, keyservers=None):
kwargs = {}
if binary_path:
kwargs['gpgbinary'] = binary_path
if home:
kwargs['gnupghome'] = home
if keyring:
kwargs['keyring'] = keyring
self.keyservers = keyservers
self.gpg = gnupg.GPG(**kwargs)
def verify_w_retry(self, file_input):
if isinstance(file_input, types.StringTypes):
input_descriptor = open(file_input, 'rb')
elif isinstance(file_input, types.FileType) or isinstance(file_input, File):
input_descriptor = file_input
elif issubclass(file_input.__class__, StringIO):
input_descriptor = file_input
else:
raise ValueError('Invalid file_input argument type')
try:
verify = self.verify_file(input_descriptor)
if verify.status == 'no public key':
# Try to fetch the public key from the keyservers
try:
self.receive_key(verify.key_id)
return self.verify_w_retry(file_input)
except KeyFetchingError:
return verify
else:
return verify
except IOError:
return False
def verify_file(self, file_input):
"""
Verify the signature of a file.
"""
if isinstance(file_input, types.StringTypes):
descriptor = open(file_input, 'rb')
elif isinstance(file_input, types.FileType) or isinstance(file_input, File) or isinstance(file_input, StringIO):
descriptor = file_input
else:
raise ValueError('Invalid file_input argument type')
verify = self.gpg.verify_file(descriptor)
descriptor.close()
if verify:
return verify
elif getattr(verify, 'status', None) == 'no public key':
# Exception to the rule, to be able to query the keyservers
return verify
else:
raise GPGVerificationError()
def verify(self, data):
# TODO: try to merge with verify_file
verify = self.gpg.verify(data)
if verify:
return verify
else:
raise GPGVerificationError(verify.status)
def sign_file(self, file_input, key=None, destination=None, key_id=None, passphrase=None, clearsign=False):
"""
Signs a filename, storing the signature and the original file
in the destination filename provided (the destination file is
overrided if it already exists), if no destination file name is
provided the signature is returned.
"""
kwargs = {}
kwargs['clearsign'] = clearsign
if key_id:
kwargs['keyid'] = key_id
if key:
kwargs['keyid'] = key.key_id
if passphrase:
kwargs['passphrase'] = passphrase
if isinstance(file_input, types.StringTypes):
input_descriptor = open(file_input, 'rb')
elif isinstance(file_input, types.FileType) or isinstance(file_input, File):
input_descriptor = file_input
elif issubclass(file_input.__class__, StringIO):
input_descriptor = file_input
else:
raise ValueError('Invalid file_input argument type')
if destination:
output_descriptor = open(destination, 'wb')
signed_data = self.gpg.sign_file(input_descriptor, **kwargs)
if not signed_data.fingerprint:
raise GPGSigningError('Unable to sign file')
if destination:
output_descriptor.write(signed_data.data)
input_descriptor.close()
if destination:
output_descriptor.close()
if not destination:
return signed_data
def decrypt_file(self, file_input):
if isinstance(file_input, types.StringTypes):
input_descriptor = open(file_input, 'rb')
elif isinstance(file_input, types.FileType) or isinstance(file_input, File) or isinstance(file_input, StringIO):
input_descriptor = file_input
else:
raise ValueError('Invalid file_input argument type')
result = self.gpg.decrypt_file(input_descriptor)
input_descriptor.close()
if not result.status:
raise GPGDecryptionError('Unable to decrypt file')
return result
def create_key(self, *args, **kwargs):
if kwargs.get('passphrase') == u'':
kwargs.pop('passphrase')
input_data = self.gpg.gen_key_input(**kwargs)
key = self.gpg.gen_key(input_data)
if not key:
raise KeyGenerationError('Unable to generate key')
return Key.get(self, key.fingerprint)
def delete_key(self, key):
status = self.gpg.delete_keys(key.fingerprint, key.type == 'sec').status
if status == 'Must delete secret key first':
self.delete_key(Key.get(self, key.fingerprint, secret=True))
self.delete_key(key)
elif status != 'ok':
raise KeyDeleteError('Unable to delete key')
def receive_key(self, key_id):
for keyserver in self.keyservers:
import_result = self.gpg.recv_keys(keyserver, key_id)
if import_result:
return Key.get(self, import_result.fingerprints[0], secret=False)
raise KeyFetchingError

View File

View File

@@ -0,0 +1,15 @@
'''
Configuration options for the django_gpg app
'''
from django.utils.translation import ugettext_lazy as _
from smart_settings.api import register_settings
register_settings(
namespace=u'django_gpg',
module=u'django_gpg.conf.settings',
settings=[
{'name': u'KEYSERVERS', 'global_name': u'SIGNATURES_KEYSERVERS', 'default': ['keyserver.ubuntu.com'], 'description': _(u'List of keyservers to be queried for unknown keys.')},
]
)

View File

@@ -0,0 +1,31 @@
class GPGException(Exception):
pass
class GPGVerificationError(GPGException):
pass
class GPGSigningError(GPGException):
pass
class GPGDecryptionError(GPGException):
pass
class KeyDeleteError(GPGException):
pass
class KeyGenerationError(GPGException):
pass
class KeyFetchingError(GPGException):
pass
class KeyDoesNotExist(GPGException):
pass

View File

@@ -0,0 +1,203 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2011-11-28 05:18-0400\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: api.py:18
msgid "Public"
msgstr ""
#: api.py:19
msgid "Secret"
msgstr ""
#: api.py:27 api.py:32
msgid "RSA"
msgstr ""
#: api.py:28
msgid "DSA"
msgstr ""
#: api.py:33
msgid "Elgamal"
msgstr ""
#: api.py:89
msgid "unknown"
msgstr ""
#: forms.py:12
msgid "Real name"
msgstr ""
#: forms.py:13
msgid "Your real name."
msgstr ""
#: forms.py:17
msgid "Comment"
msgstr ""
#: forms.py:19
msgid "A comment or a note to help identify this key."
msgstr ""
#: forms.py:23
msgid "Email"
msgstr ""
#: forms.py:28
msgid "Primary key class"
msgstr ""
#: forms.py:29
msgid "The key that will be used to sign uploaded content."
msgstr ""
#: forms.py:33
msgid "Primary key size (in bytes)"
msgstr ""
#: forms.py:41
msgid "Secondary key class"
msgstr ""
#: forms.py:42
msgid "The key that will be used to encrypt uploaded content."
msgstr ""
#: forms.py:46
msgid "Secondary key size (in bytes)"
msgstr ""
#: forms.py:53
msgid "Expiration"
msgstr ""
#: forms.py:54
msgid ""
"You can use 0 for a non expiring key, an ISO date in the form: <year>-"
"<month>-<day> or a date difference from the current date in the forms: "
"<days>d, <months>m, <weeks>w or <years>y."
msgstr ""
#: forms.py:59
msgid "Passphrase"
msgstr ""
#: forms.py:65
msgid "Passphrase (verification)"
msgstr ""
#: forms.py:72
msgid "Both passphrase fields entries must match."
msgstr ""
#: forms.py:80
msgid "Key"
msgstr ""
#: forms.py:81
msgid "Key to be published, only the public part of the key will be sent."
msgstr ""
#: tasks.py:27
#, python-format
msgid "Key pair: %s, created successfully."
msgstr ""
#: tasks.py:34
#, python-format
msgid "Key creation error; %s"
msgstr ""
#: views.py:27
msgid "Private key list"
msgstr ""
#: views.py:30
msgid "Public key list"
msgstr ""
#: views.py:54
msgid "Key pair queued for creation, refresh this page to check results."
msgstr ""
#: views.py:64
msgid "Create a new key"
msgstr ""
#: views.py:65
msgid ""
"The key creation process can take quite some time to complete, please be "
"patient."
msgstr ""
#: views.py:75
#, python-format
msgid "Key: %s, deleted successfully."
msgstr ""
#: views.py:82
msgid "Delete key"
msgstr ""
#: views.py:83
#, python-format
msgid ""
"Are you sure you wish to delete key:%s? If you try to delete a public key "
"that is part of a public/private pair the private key will be deleted as "
"well."
msgstr ""
#: views.py:95
#, python-format
msgid "Key publish request for key: %s, has been sent"
msgstr ""
#: views.py:98
msgid "Unable to send key publish call"
msgstr ""
#: views.py:105
msgid "Publish a key to the OpenRelay network"
msgstr ""
#: templates/key_list.html:10
msgid "ID"
msgstr ""
#: templates/key_list.html:11
msgid "User IDs"
msgstr ""
#: templates/key_list.html:12
msgid "Fingerprint"
msgstr ""
#: templates/key_list.html:13
msgid "Links"
msgstr ""
#: templates/key_list.html:22
msgid "Delete"
msgstr ""
#: templates/key_list.html:26
msgid "There are no keys available."
msgstr ""

Binary file not shown.

View File

@@ -0,0 +1,217 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
#
# Translators:
# Roberto Rosario <roberto.rosario.gonzalez@gmail.com>, 2011.
msgid ""
msgstr ""
"Project-Id-Version: OpenRelay\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2011-11-28 05:18-0400\n"
"PO-Revision-Date: 2011-11-28 09:25+0000\n"
"Last-Translator: rosarior <roberto.rosario.gonzalez@gmail.com>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: es\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
#: api.py:18
msgid "Public"
msgstr "Pública"
#: api.py:19
msgid "Secret"
msgstr "Secreta"
#: api.py:27 api.py:32
msgid "RSA"
msgstr "RSA"
#: api.py:28
msgid "DSA"
msgstr "DSA"
#: api.py:33
msgid "Elgamal"
msgstr "Elgamal"
#: api.py:89
msgid "unknown"
msgstr "desconocido"
#: forms.py:12
msgid "Real name"
msgstr "Nombre real"
#: forms.py:13
msgid "Your real name."
msgstr "Su nombre real."
#: forms.py:17
msgid "Comment"
msgstr "Comentario"
#: forms.py:19
msgid "A comment or a note to help identify this key."
msgstr "Un comentario o una nota para ayudar a identificar esta llave."
#: forms.py:23
msgid "Email"
msgstr "E-mail"
#: forms.py:28
msgid "Primary key class"
msgstr "Clase de llave primaria"
#: forms.py:29
msgid "The key that will be used to sign uploaded content."
msgstr "La llave que se utilizara para firmar el contenido subido."
#: forms.py:33
msgid "Primary key size (in bytes)"
msgstr "Tamaño de la llave principal (en bytes)"
#: forms.py:41
msgid "Secondary key class"
msgstr "Clase de llave secundaria"
#: forms.py:42
msgid "The key that will be used to encrypt uploaded content."
msgstr "La llave que se utilizara para cifrar el contenido subido."
#: forms.py:46
msgid "Secondary key size (in bytes)"
msgstr "Tamaño de la llave secundaria (en bytes)"
#: forms.py:53
msgid "Expiration"
msgstr "Expiración"
#: forms.py:54
msgid ""
"You can use 0 for a non expiring key, an ISO date in the form: "
"<year>-<month>-<day> or a date difference from the current date in the "
"forms: <days>d, <months>m, <weeks>w or <years>y."
msgstr ""
"Usted puede utilizar 0 para llaves que no expiran, una fecha ISO en la "
"forma: <año>-<mes>-<dia> o una diferencia de fecha con respecto a la fecha "
"actual en las formas: <dias>d, <meses>m, <semanas>w o <años>y."
#: forms.py:59
msgid "Passphrase"
msgstr "Frase de contraseña"
#: forms.py:65
msgid "Passphrase (verification)"
msgstr "Contraseña (verificación)"
#: forms.py:72
msgid "Both passphrase fields entries must match."
msgstr "Las entradas de los campos de contraseña deben coincidir."
#: forms.py:80
msgid "Key"
msgstr "Llave"
#: forms.py:81
msgid "Key to be published, only the public part of the key will be sent."
msgstr ""
"La llave para ser publicada, sólo la parte pública de la llave será enviada."
#: tasks.py:27
#, python-format
msgid "Key pair: %s, created successfully."
msgstr "Par de llaves: %s, creado correctamente."
#: tasks.py:34
#, python-format
msgid "Key creation error; %s"
msgstr "Error de creación de llave; %s"
#: views.py:27
msgid "Private key list"
msgstr "Lista de llaves privadas"
#: views.py:30
msgid "Public key list"
msgstr "Lista de llaves públicas"
#: views.py:54
msgid "Key pair queued for creation, refresh this page to check results."
msgstr ""
"Par de llaves en lista de creación, vuelva a cargar esta página "
"periodicamente para comprobar los resultados."
#: views.py:64
msgid "Create a new key"
msgstr "Crear una llave nueva"
#: views.py:65
msgid ""
"The key creation process can take quite some time to complete, please be "
"patient."
msgstr ""
"El proceso de creación de la llaves puede tomar bastante tiempo en "
"completar, por favor, sea paciente."
#: views.py:75
#, python-format
msgid "Key: %s, deleted successfully."
msgstr "Llave: %s, eliminada exitosamente."
#: views.py:82
msgid "Delete key"
msgstr "Eliminar la llave"
#: views.py:83
#, python-format
msgid ""
"Are you sure you wish to delete key:%s? If you try to delete a public key "
"that is part of a public/private pair the private key will be deleted as "
"well."
msgstr ""
"¿Está seguro que desea eliminar la llave: %s? Si intenta eliminar una llave"
" pública que forma parte de una pareja pública / privada de la llave privada"
" se eliminarán también."
#: views.py:95
#, python-format
msgid "Key publish request for key: %s, has been sent"
msgstr "Solicitud publicación de llave: %s, ha sido enviada"
#: views.py:98
msgid "Unable to send key publish call"
msgstr "No se puede enviar llave publica"
#: views.py:105
msgid "Publish a key to the OpenRelay network"
msgstr "Publicar una llave a la red OpenRelay"
#: templates/key_list.html:10
msgid "ID"
msgstr "Identificador"
#: templates/key_list.html:11
msgid "User IDs"
msgstr "ID de usuarios"
#: templates/key_list.html:12
msgid "Fingerprint"
msgstr "Huella digital"
#: templates/key_list.html:13
msgid "Links"
msgstr "Enlaces"
#: templates/key_list.html:22
msgid "Delete"
msgstr "Eliminar"
#: templates/key_list.html:26
msgid "There are no keys available."
msgstr "No hay llaves disponibles."

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,4 @@
from django_gpg.api import GPG
from django_gpg.conf.settings import KEYSERVERS
gpg = GPG(keyservers=KEYSERVERS)

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

8
apps/django_gpg/urls.py Normal file
View File

@@ -0,0 +1,8 @@
from django.conf.urls.defaults import patterns, url
urlpatterns = patterns('django_gpg.views',
url(r'^delete/(?P<fingerprint>.+)/(?P<key_type>\w+)/$', 'key_delete', (), 'key_delete'),
url(r'^list/private/$', 'key_list', {'secret': True}, 'key_private_list'),
url(r'^list/public/$', 'key_list', {'secret': False}, 'key_public_list'),
url(r'^verify/(?P<document_pk>\d+)/$', 'document_verify', (), 'document_verify'),
)

113
apps/django_gpg/views.py Normal file
View File

@@ -0,0 +1,113 @@
from datetime import datetime
from django.utils.translation import ugettext_lazy as _
from django.http import HttpResponseRedirect
from django.shortcuts import render_to_response, get_object_or_404
from django.template import RequestContext
from django.contrib import messages
from django.core.urlresolvers import reverse
from django.utils.safestring import mark_safe
from django.conf import settings
from django.template.defaultfilters import force_escape
from documents.models import Document, RecentDocument
from permissions.api import check_permissions
from django_gpg.api import Key
from django_gpg.runtime import gpg
from django_gpg.exceptions import GPGVerificationError
from django_gpg import PERMISSION_DOCUMENT_VERIFY
def key_list(request, secret=True):
if secret:
object_list = Key.get_all(gpg, secret=True)
title = _(u'Private key list')
else:
object_list = Key.get_all(gpg)
title = _(u'Public key list')
return render_to_response('key_list.html', {
'object_list': object_list,
'title': title,
}, context_instance=RequestContext(request))
def key_delete(request, fingerprint, key_type):
if request.method == 'POST':
try:
secret = key_type == 'sec'
key = Key.get(gpg, fingerprint, secret=secret)
gpg.delete_key(key)
messages.success(request, _(u'Key: %s, deleted successfully.') % fingerprint)
return HttpResponseRedirect(reverse('home_view'))
except Exception, msg:
messages.error(request, msg)
return HttpResponseRedirect(reverse('home_view'))
return render_to_response('generic_confirm.html', {
'title': _(u'Delete key'),
'message': _(u'Are you sure you wish to delete key:%s? If you try to delete a public key that is part of a public/private pair the private key will be deleted as well.') % Key.get(gpg, fingerprint)
}, context_instance=RequestContext(request))
def document_verify(request, document_pk):
check_permissions(request.user, [PERMISSION_DOCUMENT_VERIFY])
document = get_object_or_404(Document, pk=document_pk)
RecentDocument.objects.add_document_for_user(request.user, document)
try:
signature = gpg.verify_w_retry(document.open())
except GPGVerificationError:
signature = None
signature_states = {
'signature bad': {
'text': _(u'Bad signature.'),
'icon': 'cross.png'
},
None: {
'text': _(u'Document not signed or invalid signature.'),
'icon': 'cross.png'
},
'signature error': {
'text': _(u'Signature error.'),
'icon': 'cross.png'
},
'no public key': {
'text': _(u'Document is signed but no public key is available for verification.'),
'icon': 'user_silhouette.png'
},
'signature good': {
'text': _(u'Document is signed, and signature is good.'),
'icon': 'document_signature.png'
},
'signature valid': {
'text': _(u'Document is signed with a valid signature.'),
'icon': 'document_signature.png'
},
}
signature_state = signature_states.get(getattr(signature, 'status', None))
widget = (u'<img style="vertical-align: middle;" src="%simages/icons/%s" />' % (settings.STATIC_URL, signature_state['icon']))
paragraphs = [
_(u'Signature status: %s %s') % (mark_safe(widget), signature_state['text']),
]
if signature:
paragraphs.extend(
[
_(u'Signature ID: %s') % signature.signature_id,
_(u'Key ID: %s') % signature.key_id,
_(u'Timestamp: %s') % datetime.fromtimestamp(int(signature.sig_timestamp)),
_(u'Signee: %s') % force_escape(getattr(signature, 'username', u'')),
]
)
return render_to_response('generic_template.html', {
'title': _(u'signature properties for: %s') % document,
'object': document,
'document': document,
'paragraphs': paragraphs,
}, context_instance=RequestContext(request))

View File

@@ -1155,6 +1155,8 @@ def document_version_list(request, document_pk):
check_permissions(request.user, [PERMISSION_DOCUMENT_VIEW])
document = get_object_or_404(Document, pk=document_pk)
RecentDocument.objects.add_document_for_user(request.user, document)
context = {
'object_list': document.versions.order_by('-timestamp'),
'title': _(u'versions for document: %s') % document,

View File

@@ -20,3 +20,4 @@ django-compressor==1.1
-e git://github.com/rosarior/django-sendfile.git#egg=django-sendfile
djangorestframework==0.2.3
South==0.7.3
python-gnupg==0.2.8

View File

@@ -17,3 +17,4 @@ django-compressor==1.1
-e git://github.com/rosarior/django-sendfile.git#egg=django-sendfile
djangorestframework==0.2.3
South==0.7.3
python-gnupg==0.2.8

View File

@@ -133,6 +133,7 @@ INSTALLED_APPS = (
'lock_manager',
'web_theme',
'common',
'django_gpg',
'pagination',
'dynamic_search',
'filetransfers',

View File

@@ -28,6 +28,7 @@ urlpatterns = patterns('',
(r'^project_setup/', include('project_setup.urls')),
(r'^project_tools/', include('project_tools.urls')),
(r'^api/', include('rest_api.urls')),
(r'^signatures/', include('django_gpg.urls')),
)