Merge branch 'feature/detached_signatures' into development

This commit is contained in:
Roberto Rosario
2011-12-06 01:51:20 -04:00
7 changed files with 298 additions and 14 deletions

View File

@@ -16,6 +16,8 @@ PERMISSION_KEY_VIEW = {'namespace': 'django_gpg', 'name': 'key_view', 'label': _
PERMISSION_KEY_DELETE = {'namespace': 'django_gpg', 'name': 'key_delete', 'label': _(u'Delete keys')}
PERMISSION_KEYSERVER_QUERY = {'namespace': 'django_gpg', 'name': 'keyserver_query', 'label': _(u'Query keyservers')}
PERMISSION_KEY_RECEIVE = {'namespace': 'django_gpg', 'name': 'key_receive', 'label': _(u'Import key from keyservers')}
PERMISSION_SIGNATURE_UPLOAD = {'namespace': 'django_gpg', 'name': 'signature_upload', 'label': _(u'Upload detached signatures')}
PERMISSION_SIGNATURE_DOWNLOAD = {'namespace': 'django_gpg', 'name': 'key_receive', 'label': _(u'Download detached signatures')}
# Permission setup
set_namespace_title('django_gpg', _(u'Signatures'))
@@ -24,6 +26,11 @@ register_permission(PERMISSION_KEY_VIEW)
register_permission(PERMISSION_KEY_DELETE)
register_permission(PERMISSION_KEYSERVER_QUERY)
register_permission(PERMISSION_KEY_RECEIVE)
register_permission(PERMISSION_SIGNATURE_UPLOAD)
register_permission(PERMISSION_SIGNATURE_DOWNLOAD)
def has_embedded_signature(context):
return context['object'].signature_state
# Setup views
private_keys = {'text': _(u'private keys'), 'view': 'key_private_list', 'args': 'object.pk', 'famfam': 'key', 'icon': 'key.png', 'permissions': [PERMISSION_KEY_VIEW]}
@@ -31,12 +38,16 @@ public_keys = {'text': _(u'public keys'), 'view': 'key_public_list', 'args': 'ob
key_delete = {'text': _(u'delete'), 'view': 'key_delete', 'args': ['object.fingerprint', 'object.type'], 'famfam': 'key_delete', 'permissions': [PERMISSION_KEY_DELETE]}
key_query = {'text': _(u'Query keyservers'), 'view': 'key_query', 'famfam': 'zoom', 'permissions': [PERMISSION_KEYSERVER_QUERY]}
key_receive = {'text': _(u'Import'), 'view': 'key_receive', 'args': 'object.keyid', 'famfam': 'key_add', 'keep_query': True, 'permissions': [PERMISSION_KEY_RECEIVE]}
document_signature_upload = {'text': _(u'Upload signature'), 'view': 'document_signature_upload', 'args': 'object.pk', 'famfam': 'pencil_add', 'permissions': [PERMISSION_SIGNATURE_UPLOAD], 'conditional_disable': has_embedded_signature}
document_signature_download = {'text': _(u'Download signature'), 'view': 'document_signature_download', 'args': 'object.pk', 'famfam': 'disk', 'permissions': [PERMISSION_SIGNATURE_DOWNLOAD], 'conditional_disable': has_embedded_signature}
# Document views
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')
register_links(['document_verify', 'document_signature_upload', 'document_signature_download'], [document_signature_upload, document_signature_download], menu_name='sidebar')
register_links(['key_delete', 'key_private_list', 'key_public_list', 'key_query'], [private_keys, public_keys, key_query], menu_name='sidebar')
register_links(Key, [key_delete])
@@ -44,3 +55,6 @@ register_links(KeyServerKey, [key_receive])
register_setup(private_keys)
register_setup(public_keys)

View File

@@ -2,6 +2,8 @@ import types
from StringIO import StringIO
from pickle import dumps
import logging
import tempfile
import os
from django.core.files.base import File
from django.utils.translation import ugettext_lazy as _
@@ -164,7 +166,7 @@ class GPG(object):
self.gpg = gnupg.GPG(**kwargs)
def verify_w_retry(self, file_input):
def verify_w_retry(self, file_input, detached_signature=None):
if isinstance(file_input, types.StringTypes):
input_descriptor = open(file_input, 'rb')
elif isinstance(file_input, types.FileType) or isinstance(file_input, File):
@@ -175,12 +177,12 @@ class GPG(object):
raise ValueError('Invalid file_input argument type')
try:
verify = self.verify_file(input_descriptor)
verify = self.verify_file(input_descriptor, detached_signature)
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)
return self.verify_w_retry(file_input, detached_signature)
except KeyFetchingError:
return verify
else:
@@ -188,7 +190,7 @@ class GPG(object):
except IOError:
return False
def verify_file(self, file_input):
def verify_file(self, file_input, detached_signature=None):
"""
Verify the signature of a file.
"""
@@ -199,7 +201,17 @@ class GPG(object):
else:
raise ValueError('Invalid file_input argument type')
verify = self.gpg.verify_file(descriptor)
if detached_signature:
# Save the original data and invert the argument order
# Signature first, file second
file_descriptor, filename = tempfile.mkstemp(prefix='django_gpg')
file_data = file_input.read()
file_input.close()
os.write(file_descriptor, file_data)
os.close(file_descriptor)
verify = self.gpg.verify_file(detached_signature, data_filename=filename)
else:
verify = self.gpg.verify_file(descriptor)
descriptor.close()
if verify:

View File

@@ -11,3 +11,9 @@ class KeySearchForm(forms.Form):
label=_(u'Term'),
help_text=_(u'Name, e-mail, key ID or key fingerprint to look for.')
)
class DetachedSignatureForm(forms.Form):
file = forms.FileField(
label=_(u'Signature file'),
)

View File

@@ -5,7 +5,8 @@ urlpatterns = patterns('django_gpg.views',
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'),
url(r'^upload/signature/(?P<document_pk>\d+)/$', 'document_signature_upload', (), 'document_signature_upload'),
url(r'^download/signature/(?P<document_pk>\d+)/$', 'document_signature_download', (), 'document_signature_download'),
url(r'^query/$', 'key_query', (), 'key_query'),
url(r'^receive/(?P<key_id>.+)/$', 'key_receive', (), 'key_receive'),
)

View File

@@ -15,14 +15,16 @@ from documents.models import Document, RecentDocument
from permissions.api import check_permissions
from common.utils import pretty_size, parse_range, urlquote, \
return_diff, encapsulate
from filetransfers.api import serve_file
from django_gpg.api import Key, SIGNATURE_STATES
from django_gpg.runtime import gpg
from django_gpg.exceptions import GPGVerificationError, KeyFetchingError
from django_gpg import (PERMISSION_DOCUMENT_VERIFY, PERMISSION_KEY_VIEW,
PERMISSION_KEY_DELETE, PERMISSION_KEYSERVER_QUERY,
PERMISSION_KEY_RECEIVE)
from django_gpg.forms import KeySearchForm
PERMISSION_KEY_RECEIVE, PERMISSION_SIGNATURE_UPLOAD,
PERMISSION_SIGNATURE_DOWNLOAD)
from django_gpg.forms import KeySearchForm, DetachedSignatureForm
logger = logging.getLogger(__name__)
@@ -193,10 +195,8 @@ def document_verify(request, document_pk):
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(raw=True))
except GPGVerificationError:
signature = None
signature = document.verify_signature()
signature_state = SIGNATURE_STATES.get(getattr(signature, 'status', None))
@@ -230,3 +230,56 @@ def document_verify(request, document_pk):
'document': document,
'paragraphs': paragraphs,
}, context_instance=RequestContext(request))
def document_signature_upload(request, document_pk):
check_permissions(request.user, [PERMISSION_SIGNATURE_UPLOAD])
document = get_object_or_404(Document, pk=document_pk)
RecentDocument.objects.add_document_for_user(request.user, document)
post_action_redirect = None
previous = request.POST.get('previous', request.GET.get('previous', request.META.get('HTTP_REFERER', '/')))
next = request.POST.get('next', request.GET.get('next', post_action_redirect if post_action_redirect else request.META.get('HTTP_REFERER', '/')))
if request.method == 'POST':
form = DetachedSignatureForm(request.POST, request.FILES)
if form.is_valid():
try:
document.add_detached_signature(request.FILES['file'])
messages.success(request, _(u'Detached signature uploaded successfully.'))
return HttpResponseRedirect(next)
except Exception, msg:
messages.error(request, msg)
return HttpResponseRedirect(previous)
else:
form = DetachedSignatureForm()
return render_to_response('generic_form.html', {
'title': _(u'Upload detached signature for: %s') % document,
'form_icon': 'key_delete.png',
'next': next,
'form': form,
'previous': previous,
'object': document,
}, context_instance=RequestContext(request))
def document_signature_download(request, document_pk):
check_permissions(request.user, [PERMISSION_SIGNATURE_DOWNLOAD])
document = get_object_or_404(Document, pk=document_pk)
try:
if document.has_detached_signature():
signature = document.detached_signature()
return serve_file(
request,
signature,
save_as=u'"%s.sig"' % document.filename,
content_type=u'application/octet-stream'
)
except Exception, e:
messages.error(request, e)
return HttpResponseRedirect(request.META['HTTP_REFERER'])
return HttpResponseRedirect(request.META['HTTP_REFERER'])

View File

@@ -0,0 +1,157 @@
# encoding: utf-8
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding field 'DocumentVersion.signature_file'
db.add_column('documents_documentversion', 'signature_file', self.gf('django.db.models.fields.files.FileField')(max_length=100, null=True, blank=True), keep_default=False)
def backwards(self, orm):
# Deleting field 'DocumentVersion.signature_file'
db.delete_column('documents_documentversion', 'signature_file')
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
'comments.comment': {
'Meta': {'ordering': "('submit_date',)", 'object_name': 'Comment', 'db_table': "'django_comments'"},
'comment': ('django.db.models.fields.TextField', [], {'max_length': '3000'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'content_type_set_for_comment'", 'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'ip_address': ('django.db.models.fields.IPAddressField', [], {'max_length': '15', 'null': 'True', 'blank': 'True'}),
'is_public': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_removed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'object_pk': ('django.db.models.fields.TextField', [], {}),
'site': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['sites.Site']"}),
'submit_date': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'comment_comments'", 'null': 'True', 'to': "orm['auth.User']"}),
'user_email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'user_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}),
'user_url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
'documents.document': {
'Meta': {'ordering': "['-date_added']", 'object_name': 'Document'},
'date_added': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
'document_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.DocumentType']", 'null': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'uuid': ('django.db.models.fields.CharField', [], {'max_length': '48', 'blank': 'True'})
},
'documents.documentpage': {
'Meta': {'ordering': "['page_number']", 'object_name': 'DocumentPage'},
'content': ('django.db.models.fields.TextField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
'document_version': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.DocumentVersion']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'page_label': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'}),
'page_number': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1', 'db_index': 'True'})
},
'documents.documentpagetransformation': {
'Meta': {'ordering': "('order',)", 'object_name': 'DocumentPageTransformation'},
'arguments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'document_page': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.DocumentPage']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'order': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'null': 'True', 'db_index': 'True', 'blank': 'True'}),
'transformation': ('django.db.models.fields.CharField', [], {'max_length': '128'})
},
'documents.documenttype': {
'Meta': {'ordering': "['name']", 'object_name': 'DocumentType'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '32'})
},
'documents.documenttypefilename': {
'Meta': {'ordering': "['filename']", 'object_name': 'DocumentTypeFilename'},
'document_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.DocumentType']"}),
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'filename': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
},
'documents.documentversion': {
'Meta': {'unique_together': "(('document', 'major', 'minor', 'micro', 'release_level', 'serial'),)", 'object_name': 'DocumentVersion'},
'checksum': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'document': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.Document']"}),
'encoding': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '64'}),
'file': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}),
'filename': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'major': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}),
'micro': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
'mimetype': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '64'}),
'minor': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
'release_level': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}),
'serial': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
'signature_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}),
'signature_state': ('django.db.models.fields.CharField', [], {'max_length': '16', 'null': 'True', 'blank': 'True'}),
'timestamp': ('django.db.models.fields.DateTimeField', [], {})
},
'documents.recentdocument': {
'Meta': {'ordering': "('-datetime_accessed',)", 'object_name': 'RecentDocument'},
'datetime_accessed': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}),
'document': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.Document']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'sites.site': {
'Meta': {'ordering': "('domain',)", 'object_name': 'Site', 'db_table': "'django_site'"},
'domain': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'taggit.tag': {
'Meta': {'object_name': 'Tag'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '100', 'db_index': 'True'})
},
'taggit.taggeditem': {
'Meta': {'object_name': 'TaggedItem'},
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_tagged_items'", 'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'object_id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}),
'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_items'", 'to': "orm['taggit.Tag']"})
}
}
complete_apps = ['documents']

View File

@@ -290,8 +290,20 @@ class Document(models.Model):
return version.save()
filename = property(_get_filename, _set_filename)
def add_detached_signature(self, *args, **kwargs):
return self.latest_version.add_detached_signature(*args, **kwargs)
def has_detached_signature(self):
return self.latest_version.has_detached_signature()
def detached_signature(self):
return self.latest_version.detached_signature()
def verify_signature(self):
return self.latest_version.verify_signature()
class DocumentVersion(models.Model):
'''
Model that describes a document version and its properties
@@ -320,6 +332,7 @@ class DocumentVersion(models.Model):
filename = models.CharField(max_length=255, default=u'', editable=False, db_index=True)
checksum = models.TextField(blank=True, null=True, verbose_name=_(u'checksum'), editable=False)
signature_state = models.CharField(blank=True, null=True, max_length=16, verbose_name=_(u'signature state'), editable=False)
signature_file = models.FileField(blank=True, null=True, upload_to=get_filename_from_uuid, storage=STORAGE_BACKEND(), verbose_name=_(u'signature file'), editable=False)
class Meta:
unique_together = ('document', 'major', 'minor', 'micro', 'release_level', 'serial')
@@ -534,7 +547,35 @@ class DocumentVersion(models.Model):
return self.file.storage.size(self.file.path)
else:
return None
def add_detached_signature(self, detached_signature):
if not self.signature_state:
self.signature_file = detached_signature
self.save()
else:
raise Exception('document already has an embedded signature')
def has_detached_signature(self):
if self.signature_file:
return self.signature_file.storage.exists(self.signature_file.path)
else:
return False
def detached_signature(self):
return self.signature_file.storage.open(self.signature_file.path)
def verify_signature(self):
try:
if self.has_detached_signature():
logger.debug('has detached signature')
signature = gpg.verify_w_retry(self.open(), self.detached_signature())
else:
signature = gpg.verify_w_retry(self.open(raw=True))
except GPGVerificationError:
signature = None
return signature
class DocumentTypeFilename(models.Model):
'''