monkeysign-1.1/0000755000000000000000000000000012222377511010426 5ustar monkeysign-1.1/test.py0000755000000000000000000000166112222377511011766 0ustar #!/usr/bin/python # -*- coding: utf-8 -*- # # Copyright (C) 2012-2013 Antoine Beaupré # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import unittest import sys import os import time sys.path.append(os.path.dirname(__file__)) suite = unittest.TestLoader().discover('tests') unittest.TextTestRunner().run(suite) monkeysign-1.1/scripts/0000755000000000000000000000000012222377511012115 5ustar monkeysign-1.1/scripts/monkeysign0000777000000000000000000000000012222377511016312 2monkeyscanustar monkeysign-1.1/scripts/monkeyscan0000755000000000000000000000301612222377511014212 0ustar #!/usr/bin/env python # -*- coding: utf-8 -*- # # Copyright (C) 2012-2013 Antoine Beaupré # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import sys import os directory, basename = os.path.split(sys.argv[0]) path, directory = os.path.split(directory) if directory == 'scripts': sys.path.insert(0, os.path.dirname(__file__) + '/..') if basename == 'monkeysign': from monkeysign.cli import MonkeysignCli as ui else: try: from monkeysign.gtkui import MonkeysignScanUi as ui except ImportError as e: print "some modules missing for scanning functionality: %s" % e sys.exit(1) from monkeysign.gpg import GpgRuntimeError with ui() as u: try: u.main() except GpgRuntimeError as e: if u.options.debug: raise # throw full backtrace else: sys.exit(e.strerror) # only show error except KeyboardInterrupt: sys.exit() monkeysign-1.1/README0000644000000000000000000000206412222377511011310 0ustar Monkeysign: OpenPGP Key Exchange for Humans =========================================== monkeysign is a tool to overhaul the OpenPGP keysigning experience and bring it closer to something that most primates can understand. The project makes use of cheap digital cameras and the type of bar code known as a QRcode to provide a human-friendly yet still-secure keysigning experience. No more reciting tedious strings of hexadecimal characters. And, you can build a little rogue's gallery of the people that you have met and exchanged keys with! Monkeysign was written by Jerome Charaoui and Antoine Beaupré and is licensed under GPLv3. Requirements ------------ The following Python packages are required for the GUI to work. * python-qrencode * python-gtk2 * python-zbar * python-zbarpygtk If they are not available, the commandline signing tool should still work but doesn't recognize QR codes. Of course, all this depends on the GnuPG program. Installing ---------- To install monkeysign, run: setup.py install There is also a Debian package available. monkeysign-1.1/monkeysign/0000755000000000000000000000000012222377676012625 5ustar monkeysign-1.1/monkeysign/ui.py0000644000000000000000000005266312222377511013614 0ustar # -*- coding: utf-8 -*- # # Copyright (C) 2012-2013 Antoine Beaupré # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # gpg interface from monkeysign.gpg import Keyring, TempKeyring, GpgRuntimeError import monkeysign.translation # mail functions from email.mime.multipart import MIMEMultipart from email.mime.message import MIMEMessage from email.mime.application import MIMEApplication from email.mime.base import MIMEBase from email.mime.text import MIMEText from email.header import Header from email.utils import parseaddr, formataddr from email import Charset import smtplib import subprocess # system libraries import optparse import sys import re import os import shutil class MonkeysignUi(object): """User interface abstraction for monkeysign. This aims to factor out a common pattern to sign keys that is used regardless of the UI used. This is mostly geared at console/text-based and X11 interfaces, but could also be ported to other interfaces (touch-screen/phone interfaces would be interesting). The actual process is in main(), which outlines what the subclasses of this should be doing. You should have a docstring in derived classes, as it will be added to the 'usage' output. You should also set the usage and epilog parameters, see parse_args(). """ # what gets presented to the user in the usage (first and last lines) # default is to use the OptionParser's defaults # the 'docstring' above is the long description usage=None epilog=None @classmethod def parser(self): """parse the commandline arguments""" parser = optparse.OptionParser(description=self.__doc__, usage=self.usage, epilog=self.epilog, formatter=NowrapHelpFormatter()) parser.add_option('-d', '--debug', dest='debug', default=False, action='store_true', help=_('request debugging information from GPG engine (lots of garbage)')) parser.add_option('-v', '--verbose', dest='verbose', default=False, action='store_true', help=_('explain what we do along the way')) parser.add_option('-n', '--dry-run', dest='dryrun', default=False, action='store_true', help=_('do not actually do anything')) parser.add_option('-u', '--user', dest='user', help=_('user id to sign the key with')) parser.add_option('--cert-level', dest='certlevel', help=_('certification level to sign the key with')) parser.add_option('-l', '--local', dest='local', default=False, action='store_true', help=_('import in normal keyring a local certification')) parser.add_option('-k', '--keyserver', dest='keyserver', help=_('keyserver to fetch keys from')) parser.add_option('-s', '--smtp', dest='smtpserver', help=_('SMTP server to use, use a colon to specify the port number if non-standard')) parser.add_option('--smtpuser', dest='smtpuser', help=_('username for the SMTP server (default: no user)')) parser.add_option('--smtppass', dest='smtppass', help=_('password for the SMTP server (default: prompted, if --smtpuser is specified)')) parser.add_option('--no-mail', dest='nomail', default=False, action='store_true', help=_('Do not send email at all. (Default is to use sendmail.)')) parser.add_option('-t', '--to', dest='to', help=_('Override destination email for testing (default is to use the first uid on the key or send email to each uid chosen)')) return parser def parse_args(self, args): parser = self.parser() (self.options, self.pattern) = parser.parse_args(args=args) # XXX: a bit clunky because the cli expects this to be the # output of parse_args() while the GTK ui expects this to be # populated as a string, later if len(self.pattern) < 1: self.pattern = None else: # accept space-separated fingerprints self.pattern = "".join(self.pattern) # make sure parser can be accessed outside of this function return parser def __init__(self, args = None): # the options that determine how we operate, from the parse_args() self.options = {} # the key we are signing, can be a keyid or a uid pattern self.pattern = None # the regular keyring we suck secrets and maybe the key to be signed from self.keyring = Keyring() # the temporary keyring we operate in, actually initialized in prepare() # this is because we want the constructor to just initialise # data structures and not write any data self.tmpkeyring = None # the fingerprints that we actually signed self.signed_keys = {} # temporary, to keep track of the OpenPGPkey we are signing self.signing_key = None self.parse_args(args) # set a default logging mechanism self.logfile = sys.stderr self.log(_('Initializing UI')) # create the temporary keyring # XXX: i would prefer this to be done outside the constructor self.prepare() def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): # this is implicit in the garbage collection, but tell the user anyways self.log(_('deleting the temporary keyring %s') % self.tmpkeyring.homedir) if exc_type is NotImplementedError: self.abort(str(exc_value)) def prepare(self): # initialize the temporary keyring directory self.tmpkeyring = TempKeyring() if self.options.debug: self.tmpkeyring.context.debug = self.logfile self.keyring.context.debug = self.logfile if self.options.keyserver is not None: self.tmpkeyring.context.set_option('keyserver', self.options.keyserver) if self.options.user is not None: self.tmpkeyring.context.set_option('local-user', self.options.user) if self.options.certlevel is not None: self.tmpkeyring.context.set_option('default-cert-level', self.options.certlevel) self.tmpkeyring.context.set_option('secret-keyring', self.keyring.homedir + '/secring.gpg') # copy the gpg.conf from the real keyring try: shutil.copy(self.keyring.homedir + '/gpg.conf', self.tmpkeyring.homedir) self.log(_('copied your gpg.conf in temporary keyring')) except IOError as e: # no such file or directory is alright: it means the use # has no gpg.conf (because we are certain the temp homedir # exists at this point) if e.errno != 2: pass def main(self): """ General process =============== 1. fetch the key into a temporary keyring 1.a) if allowed (@todo), from the keyservers 1.b) from the local keyring (@todo try that first?) 2. copy the signing key secrets into the keyring 3. for every user id (or all, if -a is specified) 3.1. sign the uid, using gpg-agent 3.2. export and encrypt the signature 3.3. mail the key to the user 3.4. optionnally (-l), create a local signature and import in local keyring 4. trash the temporary keyring """ pass # we don't do anything because we allow for interactive process def abort(self, message): """show a message to the user and abort program""" sys.exit(message) def warn(self, message): """display an warning message this should not interrupt the flow of the program, but must be visible to the user""" print message.encode('utf-8') def log(self, message): """log an informational message if verbose""" if self.options.verbose: print >>self.logfile, message def yes_no(self, prompt, default = True): """default UI is not interactive, so we assume yes all the time""" return True def choose_uid(self, prompt, uids): raise NotImplementedError('choosing not implemented in base class') def prompt_line(self, prompt): raise NotImplementedError('prompting for a line not implemented in base class') def prompt_pass(self, prompt): raise NotImplementedError('prompting for a password not implemented in base class') def find_key(self): """find the key to be signed somewhere""" # 1.b) from the local keyring self.log(_('looking for key %s in your keyring') % self.pattern) if not self.tmpkeyring.import_data(self.keyring.export_data(self.pattern)): self.log(_('key not in local keyring')) # 1.a) if allowed, from the keyservers self.log(_('fetching key %s from keyservers') % self.pattern) if not re.search('^[0-9A-F]*$', self.pattern, re.IGNORECASE): # this is not a keyid # the problem here is that we need to implement --search-keys, and it's a pain raise NotImplementedError(_('please provide a keyid or fingerprint, uids are not supported yet')) if not self.tmpkeyring.fetch_keys(self.pattern): self.abort(_('could not find key %s in your keyring or keyservers') % self.pattern) def copy_secrets(self): """import secret keys (but only the public part) from your keyring we use --secret-keyring instead of copying the secret key material, but we still need the public part in the temporary keyring for this to work. """ self.log(_('copying your private key to temporary keyring in %s') % self.tmpkeyring.homedir) # detect the proper uid if self.options.user is None: keys = self.keyring.get_keys(None, True, False) else: keys = self.keyring.get_keys(self.options.user, True, False) for fpr, key in keys.iteritems(): self.log(_('found secret key: %s') % key) if not key.invalid and not key.disabled and not key.expired and not key.revoked: self.signing_key = key break if self.signing_key is None: self.abort(_('no default secret key found, abort!')) self.log(_('signing key chosen: %s') % self.signing_key.fpr) # export public key material associated with detected private if not self.tmpkeyring.import_data(self.keyring.export_data(self.signing_key.fpr)): self.abort(_('could not find public key material, do you have a GPG key?')) def sign_key(self): """sign the key uids, as specified""" keys = self.tmpkeyring.get_keys(self.pattern) self.log(_('found %d keys matching your request') % len(keys)) for key in keys: alluids = self.yes_no(_("""\ Signing the following key %s Sign all identities? [y/N] \ """) % keys[key], False) self.chosen_uid = None if alluids: pattern = keys[key].fpr else: pattern = self.choose_uid(_('Choose the identity to sign'), keys[key]) if not pattern: self.log(_('no identity chosen')) return False if not self.options.to: self.options.to = pattern self.chosen_uid = pattern if not self.options.dryrun: if not self.yes_no(_('Really sign key? [y/N] '), False): continue if not self.tmpkeyring.sign_key(pattern, alluids): self.warn(_('key signing failed')) else: self.signed_keys[key] = keys[key] if self.options.local: self.log(_('making a non-exportable signature')) self.tmpkeyring.context.set_option('export-options', 'export-minimal') # this is inefficient - we could save a copy if we would fetch the key directly if not self.keyring.import_data(self.tmpkeyring.export_data(self.pattern)): self.abort(_('could not import public key back into public keyring, something is wrong')) if not self.keyring.sign_key(pattern, alluids, True): self.warn(_('local key signing failed')) def export_key(self): if self.options.user is not None and '@' in self.options.user: from_user = self.options.user else: from_user = self.signing_key.uidslist[0].uid if len(self.signed_keys) < 1: self.warn(_('no key signed, nothing to export')) for fpr, key in self.signed_keys.items(): if self.chosen_uid is None: for uid in key.uids.values(): try: msg = EmailFactory(self.tmpkeyring.export_data(fpr), fpr, uid.uid, from_user, self.options.to) except GpgRuntimeError as e: self.warn(_('failed to create email: %s') % e) break self.sendmail(msg) else: try: msg = EmailFactory(self.tmpkeyring.export_data(fpr), fpr, self.chosen_uid, from_user, self.options.to) except GpgRuntimeError as e: self.warn(_('failed to create email: %s') % e) break self.sendmail(msg) def sendmail(self, msg): """actually send the email expects an EmailFactory email, but will not mail if nomail is set""" if self.options.smtpserver is not None and not self.options.nomail: if self.options.dryrun: return True server = smtplib.SMTP(self.options.smtpserver) server.set_debuglevel(self.options.debug) try: server.starttls() except SMTPException: self.warn(_('SMTP server does not support STARTTLS')) if self.options.smtpuser: self.warn(_('authentication credentials will be sent in clear text')) if self.options.smtpuser: if not self.options.smtppass: self.options.smtppass = self.prompt_pass(_('enter SMTP password for server %s: ') % self.options.smtpserver) server.login(self.options.smtpuser, self.options.smtppass) server.sendmail(msg.mailfrom.encode('utf-8'), msg.mailto.encode('utf-8'), msg.as_string().encode('utf-8')) server.quit() self.warn(_('sent message through SMTP server %s to %s') % (self.options.smtpserver, msg.mailto)) return True elif not self.options.nomail: if self.options.dryrun: return True p = subprocess.Popen(['/usr/sbin/sendmail', '-t'], stdin=subprocess.PIPE) p.communicate(msg.as_string().encode('utf-8')) self.warn(_('sent message through sendmail to %s') % msg.mailto) else: # okay, no mail, just dump the exported key then self.warn(_("""\ not sending email to %s, as requested, here's the email message: %s""") % (msg.mailto, msg.create_mail_from_block(msg.tmpkeyring.export_data(msg.keyfpr)))) class EmailFactory: """email generator this is a factory, ie. a class generating an object that represents the email and when turned into a string, is the actual mail. """ # the email subject subject = _("Your signed OpenPGP key") # the email body body = _(""" Please find attached your signed PGP key. You can import the signed key by running each through `gpg --import`. If you have multiple user ids, each signature was sent in a separate email to each user id. Note that your key was not uploaded to any keyservers. If you want this new signature to be available to others, please upload it yourself. With GnuPG this can be done using: gpg --keyserver pool.sks-keyservers.net --send-key Regards, """) def __init__(self, keydata, keyfpr, recipient, mailfrom, mailto): """email constructor we expect to find the following arguments: keydata: the signed public key material keyfpr: the fingerprint of that public key recipient: the recipient to encrypt the mail to mailfrom: who the mail originates from mailto: who to send the mail to (usually similar to recipient, but can be used to specify specific keyids""" (self.keyfpr, self.recipient, self.mailfrom, self.mailto) = (keyfpr, recipient, mailfrom.decode('utf-8'), mailto or recipient) self.mailto = self.mailto.decode('utf-8') # operate over our own keyring, this allows us to remove UIDs freely self.tmpkeyring = TempKeyring() # copy data over from the UI keyring self.tmpkeyring.import_data(keydata) # prepare for email transport self.tmpkeyring.context.set_option('armor') # XXX: why is this necessary? self.tmpkeyring.context.set_option('always-trust') # remove UIDs we don't want to send self.cleanup_uids() # cleanup email addresses self.cleanup_emails() def cleanup_emails(self): # wrap real name in quotes self.mailfrom = re.sub(r'^(.*) <', r'"\1" <', # trim comment from uid re.sub(r' \([^)]*\)', r'', self.mailfrom)) # same with mailto self.mailto = re.sub(r'^(.*) <', r'"\1" <', re.sub(r' \([^)]*\)', r'', self.mailto)) def cleanup_uids(self): """this will remove any UID not matching the 'recipient' set in the class""" for fpr, key in self.tmpkeyring.get_keys().iteritems(): todelete = [] for uid in key.uids.values(): if self.recipient != uid.uid: todelete.append(uid.uid) for uid in todelete: self.tmpkeyring.del_uid(fpr, uid) def get_message(self): # first layer, seen from within: # an encrypted MIME message, made of two parts: the # introduction and the signed key material message = self.create_mail_from_block(self.tmpkeyring.export_data(self.keyfpr)) encrypted = self.tmpkeyring.encrypt_data(message.as_string(), self.keyfpr) # the second layer up, made of two parts: a version number # and the first layer, encrypted return self.wrap_crypted_mail(encrypted) def __str__(self): return self.get_message().as_string().decode('utf-8') def as_string(self): return self.__str__() def create_mail_from_block(self, data): """ a multipart/mixed message containing a plain-text message explaining what this is, and a second part containing PGP data """ # Override python's weird assumption that utf-8 text should be encoded with # base64, and instead use quoted-printable (for both subject and body). I # can't figure out a way to specify QP (quoted-printable) instead of base64 in # a way that doesn't modify global state. :-( # (taken from http://radix.twistedmatrix.com/2010/07/how-to-send-good-unicode-email-with.html) Charset.add_charset('utf-8', Charset.QP, Charset.QP, 'utf-8') text = MIMEText(self.body, 'plain', 'utf-8') filename = "yourkey.asc" # should be 0xkeyid.uididx.signed-by-0xkeyid.asc keypart = MIMEBase('application', 'pgp-keys', name=filename) keypart.add_header('Content-Disposition', 'attachment', filename=filename) keypart.add_header('Content-Transfer-Encoding', '7bit') keypart.add_header('Content-Description', _('PGP Key , uid (')) keypart.set_payload(data) return MIMEMultipart('mixed', None, [text, keypart]) def wrap_crypted_mail(self, encrypted): p1 = MIMEBase('application', 'pgp-encrypted', filename='signedkey.msg') p1.add_header('Content-Disposition','attachment', filename='signedkey.msg') p1.set_payload('Version: 1') p2 = MIMEBase('application', 'octet-stream', filename='msg.asc') p2.add_header('Content-Disposition', 'inline', filename='msg.asc') p2.add_header('Content-Transfer-Encoding', '7bit') p2.set_payload(encrypted) msg = MIMEMultipart('encrypted', None, [p1, p2], protocol="application/pgp-encrypted") msg.preamble = _('This is a multi-part message in PGP/MIME format...') msg['Subject'] = Header(self.subject.encode('utf-8'), 'UTF-8').encode() name, address = parseaddr(self.mailfrom) msg['From'] = formataddr((Header(name.encode('utf-8'), 'UTF-8').encode(), address)) name, address = parseaddr(self.mailto) msg['To'] = formataddr((Header(name.encode('utf-8'), 'UTF-8').encode(), address)) return msg class NowrapHelpFormatter(optparse.IndentedHelpFormatter): """A non-wrapping formatter for OptionParse.""" def _format_text(self, text): return text monkeysign-1.1/monkeysign/gpg.py0000644000000000000000000007326412222377511013754 0ustar # -*- coding: utf-8 -*- # # Copyright (C) 2012-2013 Antoine Beaupré # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """ Native Python / GPG API This API was written to replace the GPGME bindings because the GPGME API has a few problems: 1. it is arcane and difficult to grasp 2. it is very closely bound to the internal GPG data and commandline structures, which are quite confusing 3. GPGME doesn't actually talk to a GPG library, but interacts with GPG through the commandline 4. GPGME developers are not willing to extend GPGME to cover private key material management and consider this is outside the scope of the project. The latter two points are especially problematic for this project, and I have therefore started working on a replacement. Operations are performed mostly through the Keyring or KeyringTmp class (if you do not want to access your regular keyring but an empty temporary one). This is how you can access keys, which are represented by the OpenPGPkey datastructure, but which will not look in your keyring or on the keyservers itself without the Keyring class. It seems that I have missed a similar project that's been around for quite a while (2008-2012): https://code.google.com/p/python-gnupg/ The above project has a lot of similarities with this implementation, but is better because: 1. it actually parses most status outputs from GPG, in a clean way 2. uses threads so it doesn't block 3. supports streams 4. supports verification, key generation and deletion 5. has a cleaner and more complete test suite However, the implementation here has: 1. key signing support 2. a cleaner API Error handling is somewhat inconsistent here. Some functions rely on exceptions, other on boolean return values. We prefer exceptions as it allows us to propagate error messages to the UI, but make sure to generate a RuntimeError, and not a ProtocolError, which are unreadable to the user. """ import os, tempfile, shutil, subprocess, re from StringIO import StringIO import monkeysign.translation class Context(): """Python wrapper for GnuPG This wrapper allows for a simpler interface than GPGME or PyME to GPG, and bypasses completely GPGME to interoperate directly with GPG as a process. It uses the gpg-agent to prompt for passphrases and communicates with GPG over the stdin for commnads (--command-fd) and stdout for status (--status-fd). """ # the gpg binary to call gpg_binary = 'gpg' # a list of key => value commandline options # # to pass a flag without options, use None as the value options = { 'status-fd': 2, 'command-fd': 0, 'no-tty': None, 'quiet': None, 'batch': None, 'use-agent': None, 'with-colons': None, 'with-fingerprint': None, 'fixed-list-mode': None, 'list-options': 'show-sig-subpackets,show-uid-validity,show-unusable-uids,show-unusable-subkeys,show-keyring,show-sig-expire', } # whether to paste output here and there # if not false, needs to be a file descriptor debug = False def __init__(self): self.options = dict(Context.options) # copy def set_option(self, option, value = None): """set an option to pass to gpg this adds the given 'option' commandline argument with the value 'value'. to pass a flag without an argument, use 'None' for value """ self.options[option] = value def unset_option(self, option): """remove an option from the gpg commandline""" if option in self.options: del self.options[option] else: return false def build_command(self, command): """internal helper to build a proper gpg commandline this will add relevant arguments around the gpg binary. like the options arguments, the command is expected to be a regular gpg command with the -- stripped. the -- are added before being called. this is to make the code more readable, and eventually support other backends that actually make more sense. this uses build_command to create a commandline out of the 'options' dictionary, and appends the provided command at the end. this is because order of certain options matter in gpg, where some options (like --recv-keys) are expected to be at the end. it is here that the options dictionary is converted into a list. the command argument is expected to be a list of arguments that can be converted to strings. if it is not a list, it is cast into a list.""" options = [] for left, right in self.options.iteritems(): options += ['--' + left] if right is not None: options += [str(right)] if type(command) is str: command = [command] if len(command) > 0 and command[0][0:2] != '--': command[0] = '--' + command[0] return [self.gpg_binary] + options + command def call_command(self, command, stdin=None): """internal wrapper to call a GPG commandline this will call the command generated by build_command() and setup a regular pipe to the subcommand. this assumes that we have the status-fd on stdout and command-fd on stdin, but could really be used in any other way. we pass the stdin argument in the standard input of gpg and we keep the output in the stdout and stderr array. the exit code is in the returncode variable. we can optionnally watch for a confirmation pattern on the statusfd. """ proc = subprocess.Popen(self.build_command(command), stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) (self.stdout, self.stderr) = proc.communicate(stdin) self.returncode = proc.returncode if self.debug: print >>self.debug, 'command:', self.build_command(command) print >>self.debug, 'ret:', self.returncode, 'stdout:', self.stdout, 'stderr:', self.stderr return proc.returncode == 0 def seek_pattern(self, fd, pattern): """iterate over file descriptor until certain pattern is found fd is a file descriptor pattern a string describing a regular expression to match this will skip lines not matching pattern until the pattern is found. it will raise an IOError if the pattern is not found and EOF is reached. this may hang for streams that do not send EOF or are waiting for input. """ line = fd.readline() match = re.search(pattern, line) while line and not match: if self.debug: print >>self.debug, "skipped:", line, line = fd.readline() match = re.search(pattern, line) if match: if self.debug: print >>self.debug, "FOUND:", line, return match else: raise GpgProtocolError(self.returncode, _("could not find pattern '%s' in input") % pattern) def seek(self, fd, pattern): """look for a specific GNUPG status line in the output this is a stub for seek_pattern() """ return self.seek_pattern(fd, '^\[GNUPG:\] ' + pattern) def expect_pattern(self, fd, pattern): """make sure the next line matches the provided pattern in contrast with seek_pattern(), this will *not* skip non-matching lines and instead raise an exception if such a line is found. this therefore looks only at the next line, but may also hang like seek_pattern() """ line = fd.readline() match = re.search(pattern, line) if self.debug: if match: print >>self.debug, "FOUND:", line, else: print >>self.debug, "SKIPPED:", line, if not match: raise GpgProtocolError(self.returncode, 'expected "%s", found "%s"' % (pattern, line)) return match def expect(self, fd, pattern): """look for a specific GNUPG status on the next line of output this is a stub for expect() """ return self.expect_pattern(fd, '^\[GNUPG:\] ' + pattern) def version(self): """return the version of the GPG binary""" self.call_command(['version']) m = re.search('gpg \(GnuPG\) (\d+.\d+(?:.\d+)*)', self.stdout) return m.group(1) class Keyring(): """Keyring functionalities. This allows various operations (e.g. listing, signing, exporting data) on a keyring. Concretely, we talk about a "keyring", but we really mean a set of public and private keyrings and their trust databases. In practice, this is the equivalent of the GNUPGHOME or --homedir in GPG, and in fact this is implemented by setting a specific homedir to tell GPG to operate on a specific keyring. We actually use the --homedir parameter to gpg to set the keyring we operate upon. """ # the context this keyring is associated with context = None def __init__(self, homedir=None): """constructor for the gpg context this mostly sets options, and allows passing in a different homedir, that will be added to the option right here and there. by default, we do not create or destroy the keyring, although later function calls on the object may modify the keyring (or other keyrings, if the homedir option is modified. """ self.context = Context() if homedir is not None: self.context.set_option('homedir', homedir) else: homedir = os.environ['HOME'] + '/.gnupg' if 'GNUPGHOME' in os.environ: homedir = os.environ['GNUPGHOME'] self.homedir = homedir def import_data(self, data): """Import OpenPGP data blocks into the keyring. This takes actual OpenPGP data, ascii-armored or not, gpg will gladly take it. This can be signatures, public, private keys, etc. You may need to set import-flags to import non-exportable signatures, however. """ self.context.call_command(['import'], data) fd = StringIO(self.context.stderr) try: self.context.seek(fd, 'IMPORT_OK') self.context.seek(fd, 'IMPORT_RES') except GpgProtocolError: return False return True def export_data(self, fpr = None, secret = False): """Export OpenPGP data blocks from the keyring. This exports actual OpenPGP data, by default in binary format, but can also be exported asci-armored by setting the 'armor' option.""" self.context.set_option('armor') if secret: command = ['export-secret-keys'] else: command = ['export'] if fpr: command += [fpr] self.context.call_command(command) return self.context.stdout def fetch_keys(self, fpr, keyserver = None): """Download keys from a keyserver into the local keyring This expects a fingerprint (or a at least a key id). Returns true if the command succeeded. """ if keyserver is not None: self.context.set_option('keyserver', keyserver) self.context.call_command(['recv-keys', fpr]) return self.context.returncode == 0 def get_keys(self, pattern = None, secret = False, public = True): """load keys matching a specific patterns this uses the (rather poor) list-keys API to load keys information """ keys = {} if public: command = ['list-keys'] if pattern: command += [pattern] self.context.call_command(command) if self.context.returncode == 0: # discard trustdb data, first line of output self.context.stdout = "\n".join(self.context.stdout.split("\n")[1:]) for keydata in self.context.stdout.split("pub:"): if not keydata: continue keydata = "pub:" + keydata key = OpenPGPkey(keydata) keys[key.fpr] = key elif self.context.returncode == 2: return None else: raise GpgProtocolError(self.context.returncode, _('unexpected GPG exit code in list-keys: %d') % self.context.returncode) if secret: command = ['list-secret-keys'] if pattern: command += [pattern] self.context.call_command(command) if self.context.returncode == 0: for keydata in self.context.stdout.split("sec::"): if not keydata: continue keydata = "sec::" + keydata key = OpenPGPkey(keydata) # check if we already have that key, in which case we # add to it instead of adding a new key if key.fpr in keys: keys[key.fpr].parse_gpg_list(self.context.stdout) del key else: keys[key.fpr] = key elif self.context.returncode == 2: return None else: raise GpgProtocolError(self.context.returncode, _('unexpected GPG exit code in list-keys: %d') % self.context.returncode) return keys def encrypt_data(self, data, recipient): """encrypt data using asymetric encryption returns the encrypted data or raise a GpgRuntimeError if it fails """ self.context.call_command(['recipient', recipient, '--encrypt'], data) if self.context.returncode == 0: return self.context.stdout else: raise GpgRuntimeError(self.context.returncode, _('encryption to %s failed: %s.') % (recipient, self.context.stderr.split("\n")[-2])) def decrypt_data(self, data): """decrypt data using asymetric encryption returns the plaintext data or raise a GpgRuntimeError if it failed. """ self.context.call_command(['--decrypt'], data) if self.context.returncode == 0: return self.context.stdout else: raise GpgRuntimeError(self.context.returncode, _('decryption failed: %s') % self.context.stderr.split("\n")[-2]) def del_uid(self, fingerprint, pattern): if self.context.debug: print >>self.context.debug, 'command:', self.context.build_command(['edit-key', fingerprint]) proc = subprocess.Popen(self.context.build_command(['edit-key', fingerprint]), stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) # start copy-paste from sign_key() self.context.expect(proc.stderr, 'GET_LINE keyedit.prompt') while True: m = self.context.seek_pattern(proc.stdout, '^uid:.::::::::([^:]*):::[^:]*:(\d+),[^:]*:') if m and m.group(1) == pattern: # XXX: we don't have the +1 that sign_key has, why? index = int(m.group(2)) break print >>proc.stdin, str(index) self.context.expect(proc.stderr, 'GOT_IT') self.context.expect(proc.stderr, 'GET_LINE keyedit.prompt') # end of copy-paste from sign_key() print >>proc.stdin, 'deluid' self.context.expect(proc.stderr, 'GOT_IT') self.context.expect(proc.stderr, 'GET_BOOL keyedit.remove.uid.okay') print >>proc.stdin, 'y' self.context.expect(proc.stderr, 'GOT_IT') self.context.expect(proc.stderr, 'GET_LINE keyedit.prompt') print >>proc.stdin, 'save' self.context.expect(proc.stderr, 'GOT_IT') return proc.wait() == 0 def sign_key(self, pattern, signall = False, local = False): """sign a OpenPGP public key By default it looks up and signs a specific uid, but it can also sign all uids in one shot thanks to GPG's optimization on that. The pattern here should be a full user id if we sign a specific key (default) or any pattern (fingerprint, keyid, partial user id) that GPG will accept if we sign all uids. @todo that this currently block if the pattern specifies an incomplete UID and we do not sign all keys. """ # we iterate over the keys matching the provided # keyid, but we should really load those uids from the # output of --sign-key if self.context.debug: print >>self.context.debug, 'command:', self.context.build_command([['sign-key', 'lsign-key'][local], pattern]) proc = subprocess.Popen(self.context.build_command([['sign-key', 'lsign-key'][local], pattern]), stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) # if there are multiple uids to sign, we'll get this point, and a whole other interface try: multiuid = self.context.expect(proc.stderr, 'GET_BOOL keyedit.sign_all.okay') except GpgProtocolError: multiuid = False if multiuid: if signall: # special case, sign all keys print >>proc.stdin, "y" self.context.expect(proc.stderr, 'GOT_IT') # confirm signature try: self.context.expect(proc.stderr, 'GET_BOOL sign_uid.okay') except GpgProtocolError as e: if 'sign_uid.dupe_okay' in str(e): raise GpgRuntimeError(self.context.returncode, _('you already signed that key')) else: raise GpgRuntimeError(self.context.returncode, _('unable to open key for editing: %s') % self.context.stderr.decode('utf-8')) print >>proc.stdin, 'y' self.context.expect(proc.stderr, 'GOT_IT') # expect the passphrase confirmation # we seek because i have seen a USERID_HINT in some cases try: self.context.seek(proc.stderr, 'GOOD_PASSPHRASE') except GpgProtocolError: raise GpgRuntimeError(self.context.returncode, _('unable to prompt for passphrase, is gpg-agent running?')) return proc.wait() == 0 # don't sign all uids print >>proc.stdin, "n" self.context.expect(proc.stderr, 'GOT_IT') # select the uid self.context.expect(proc.stderr, 'GET_LINE keyedit.prompt') while True: # XXX: this will hang if the pattern requested is not found, we need a better way! m = self.context.seek_pattern(proc.stdout, '^uid:.::::::::([^:]*):::[^:]*:(\d+),[^:]*:') if m and m.group(1) == pattern: index = int(m.group(2)) + 1 break print >>proc.stdin, str(index) self.context.expect(proc.stderr, 'GOT_IT') # sign the selected uid self.context.seek(proc.stderr, 'GET_LINE keyedit.prompt') print >>proc.stdin, "sign" self.context.expect(proc.stderr, 'GOT_IT') # confirm signature try: self.context.expect(proc.stderr, 'GET_BOOL sign_uid.okay') except GpgProtocolError: raise GpgRuntimeError(self.context.returncode, _('unable to open key for editing: %s') % self.context.stderr.decode('utf-8')) # we fallthrough here if there's only one key to sign try: print >>proc.stdin, 'y' except IOError as e: if e.errno == 32: # broken pipe, probably that key is missing raise GpgRuntimeError(self.context.returncode, _('unable to open key for editing: %s') % self.context.stderr.decode('utf-8')) else: pass try: self.context.expect(proc.stderr, 'GOT_IT') except GpgProtocolError as e: # deal with expired keys # XXX: weird that this happens here and not earlier if 'EXPIRED' in str(e): raise GpgRuntimeError(self.context.returncode, _('key is expired, cannot sign')) else: raise # expect the passphrase confirmation try: self.context.seek(proc.stderr, 'GOOD_PASSPHRASE') except GpgProtocolError: raise GpgRuntimeError(self.context.returncode, _('password confirmation failed')) if multiuid: # we save the resulting key in uid selection mode self.context.expect(proc.stderr, 'GET_LINE keyedit.prompt') print >>proc.stdin, "save" self.context.expect(proc.stderr, 'GOT_IT') return proc.wait() == 0 class TempKeyring(Keyring): def __init__(self): """Override the parent class to generate a temporary GPG home that gets destroyed at the end of operations.""" Keyring.__init__(self, tempfile.mkdtemp(prefix="pygpg-")) def __del__(self): shutil.rmtree(self.homedir) class OpenPGPkey(): """An OpenPGP key. Some of this datastructure is taken verbatim from GPGME. """ # the key has a revocation certificate # @todo - not implemented revoked = False # the expiry date is set and it is passed # @todo - not implemented expired = False # the key has been disabled # @todo - not implemented disabled = False # ? invalid = False # the various flags on this key purpose = {} # This is true if the subkey can be used for qualified # signatures according to local government regulations. # @todo - not implemented qualified = False # this key has also secret key material secret = False # This is the public key algorithm supported by this subkey. algo = -1 # This is the length of the subkey (in bits). length = None # The key fingerprint (a string representation) fpr = None # The key id (a string representation), only if the fingerprint is unavailable # use keyid() instead of this field to find the keyid _keyid = None # This is the creation timestamp of the subkey. This is -1 if # the timestamp is invalid, and 0 if it is not available. creation = 0 # This is the expiration timestamp of the subkey, or 0 if the # subkey does not expire. expiry = 0 # single-character trust status, see trust_map below for parsing trust = None # the list of OpenPGPuids associated with this key uids = {} # the list of subkeys associated with this key subkeys = {} trust_map = {'o': 'new', # this key is new to the system 'i': 'invalid', # The key is invalid (e.g. due to a # missing self-signature) 'd': 'disabled', # The key has been disabled # (deprecated - use the 'D' in field # 12 instead) 'r': 'revoked', # The key has been revoked 'e': 'expired', # The key has expired '-': 'unknown', # Unknown trust (i.e. no value # assigned) 'q': 'undefined', # Undefined trust, '-' and 'q' may # safely be treated as the same # value for most purposes 'n': 'none', # Don't trust this key at all 'm': 'marginal', # There is marginal trust in this key 'f': 'full', # The key is fully trusted 'u': 'ultimate', # The key is ultimately trusted. # This often means that the secret # key is available, but any key may # be marked as ultimately trusted. } def __init__(self, data=None): self.purpose = { 'encrypt': True, # if the public key part can be used to encrypt data 'sign': True, # if the private key part can be used to sign data 'certify': True, # if the private key part can be used to sign other keys 'authenticate': True, # if this key can be used for authentication purposes } self.uids = {} self.subkeys = {} if data is not None: self.parse_gpg_list(data) def keyid(self, l=8): if self.fpr is None: assert(self._keyid is not None) return self._keyid[-l:] return self.fpr[-l:] def get_trust(self): return OpenPGPkey.trust_map[self.trust] def parse_gpg_list(self, text): uidslist = [] for block in text.split("\n"): record = block.split(":") #for block in record: # print >>sys.stderr, block, "|\t", #print >>sys.stderr, "\n" rectype = record[0] if rectype == 'tru': (rectype, trust, selflen, algo, keyid, creation, expiry, serial) = record elif rectype == 'fpr': self.fpr = record[9] elif rectype == 'pub': (null, self.trust, self.length, self.algo, keyid, self.creation, self.expiry, serial, trust, uid, sigclass, purpose, smime) = record for p in self.purpose: self.purpose[p] = p[0].lower() in purpose.lower() if self.trust == '': self.trust = '-' elif rectype == 'uid': (rectype, trust, null , null, null, creation, expiry, uidhash, null, uid, null) = record uid = OpenPGPuid(uid, trust, creation, expiry, uidhash) self.uids[uidhash] = uid uidslist.append(uid) elif rectype == 'sub': subkey = OpenPGPkey() (rectype, trust, subkey.length, subkey.algo, subkey._keyid, subkey.creation, subkey.expiry, serial, trust, uid, sigclass, purpose, smime) = record for p in subkey.purpose: subkey.purpose[p] = p[0].lower() in purpose.lower() self.subkeys[subkey._keyid] = subkey elif rectype == 'sec': (null, self.trust, self.length, self.algo, keyid, self.creation, self.expiry, serial, trust, uid, sigclass, purpose, smime, wtf, wtf, wtf) = record self.secret = True if self.trust == '': self.trust = '-' elif rectype == 'ssb': subkey = OpenPGPkey() (rectype, trust, subkey.length, subkey.algo, subkey._keyid, subkey.creation, subkey.expiry, serial, trust, uid, sigclass, purpose, smime, wtf, wtf, wtf) = record if subkey._keyid in self.subkeys: # XXX: nothing else to add here? self.subkeys[subkey._keyid].secret = True else: self.subkeys[subkey._keyid] = subkey elif rectype == 'uat': pass # user attributes, ignore for now elif rectype == 'rvk': pass # revocation key, ignored for now elif rectype == '': pass else: raise NotImplementedError(_("record type '%s' not implemented") % rectype) if uidslist: self.uidslist = uidslist def __str__(self): ret = u'pub [%s] %sR/' % (self.get_trust(), self.length) ret += self.keyid(8) + u" " + self.creation if self.expiry: ret += u' [expiry: ' + self.expiry + ']' ret += u"\n" ret += u' Fingerprint = ' + self.format_fpr() + "\n" i = 1 for uid in self.uidslist: ret += u"uid %d [%s] %s\n" % (i, uid.get_trust(), uid.uid.decode('utf-8')) i += 1 for subkey in self.subkeys.values(): ret += u"sub " + subkey.length + u"R/" + subkey.keyid(8) + u" " + subkey.creation if subkey.expiry: ret += u' [expiry: ' + subkey.expiry + "]" ret += u"\n" return ret def format_fpr(self): """display a clean version of the fingerprint this is the display we usually see """ l = list(self.fpr) # explode s = '' for i in range(10): # output 4 chars s += ''.join(l[4*i:4*i+4]) # add a space, except at the end if i < 9: s += ' ' # add an extra space in the middle if i == 4: s += ' ' return s class OpenPGPuid(): def __init__(self, uid, trust, creation = 0, expire = None, uidhash = ''): self.uid = uid self.trust = trust if self.trust == '': self.trust = '-' self.creation = creation self.expire = expire self.uidhash = uidhash def get_trust(self): return OpenPGPkey.trust_map[self.trust] class GpgProtocolError(IOError): """simple exception raised when we have trouble talking with GPG we try to pass the subprocess.popen.returncode as an errorno and a significant description string this error shouldn't be propagated to the user, because it will contain mostly "expect" jargon from the DETAILS.txt file. the gpg module should instead raise a GpgRutimeError with a user-readable error message (e.g. "key not found"). """ pass class GpgRuntimeError(IOError): pass monkeysign-1.1/monkeysign/msgfmt.py0000755000000000000000000001451312222377511014467 0ustar #!/usr/bin/env python # -*- coding: iso-8859-1 -*- # Written by Martin v. Lwis # Plural forms support added by alexander smishlajev """ Generate binary message catalog from textual translation description. This program converts a textual Uniforum-style message catalog (.po file) into a binary GNU catalog (.mo file). This is essentially the same function as the GNU msgfmt program, however, it is a simpler implementation. Usage: msgfmt.py [OPTIONS] filename.po Options: -o file --output-file=file Specify the output file to write to. If omitted, output will go to a file named filename.mo (based off the input file name). -h --help Print this message and exit. -V --version Display version information and exit. """ import sys import os import getopt import struct import array __version__ = "1.1" MESSAGES = {} def usage (ecode, msg=''): """ Print usage and msg and exit with given code. """ print >> sys.stderr, __doc__ if msg: print >> sys.stderr, msg sys.exit(ecode) def add (msgid, transtr, fuzzy): """ Add a non-fuzzy translation to the dictionary. """ global MESSAGES if not fuzzy and transtr and not transtr.startswith('\0'): MESSAGES[msgid] = transtr def generate (): """ Return the generated output. """ global MESSAGES keys = MESSAGES.keys() # the keys are sorted in the .mo file keys.sort() offsets = [] ids = strs = '' for _id in keys: # For each string, we need size and file offset. Each string is NUL # terminated; the NUL does not count into the size. offsets.append((len(ids), len(_id), len(strs), len(MESSAGES[_id]))) ids += _id + '\0' strs += MESSAGES[_id] + '\0' output = '' # The header is 7 32-bit unsigned integers. We don't use hash tables, so # the keys start right after the index tables. # translated string. keystart = 7*4+16*len(keys) # and the values start after the keys valuestart = keystart + len(ids) koffsets = [] voffsets = [] # The string table first has the list of keys, then the list of values. # Each entry has first the size of the string, then the file offset. for o1, l1, o2, l2 in offsets: koffsets += [l1, o1+keystart] voffsets += [l2, o2+valuestart] offsets = koffsets + voffsets output = struct.pack("Iiiiiii", 0x950412deL, # Magic 0, # Version len(keys), # # of entries 7*4, # start of key index 7*4+len(keys)*8, # start of value index 0, 0) # size and offset of hash table output += array.array("i", offsets).tostring() output += ids output += strs return output def make (filename, outfile): ID = 1 STR = 2 global MESSAGES MESSAGES = {} # Compute .mo name from .po name and arguments if filename.endswith('.po'): infile = filename else: infile = filename + '.po' if outfile is None: outfile = os.path.splitext(infile)[0] + '.mo' try: lines = open(infile).readlines() except IOError, msg: print >> sys.stderr, msg sys.exit(1) section = None fuzzy = 0 # Parse the catalog msgid = msgstr = '' lno = 0 for l in lines: lno += 1 # If we get a comment line after a msgstr, this is a new entry if l[0] == '#' and section == STR: add(msgid, msgstr, fuzzy) section = None fuzzy = 0 # Record a fuzzy mark if l[:2] == '#,' and (l.find('fuzzy') >= 0): fuzzy = 1 # Skip comments if l[0] == '#': continue # Start of msgid_plural section, separate from singular form with \0 if l.startswith('msgid_plural'): msgid += '\0' l = l[12:] # Now we are in a msgid section, output previous section elif l.startswith('msgid'): if section == STR: add(msgid, msgstr, fuzzy) section = ID l = l[5:] msgid = msgstr = '' # Now we are in a msgstr section elif l.startswith('msgstr'): section = STR l = l[6:] # Check for plural forms if l.startswith('['): # Separate plural forms with \0 if not l.startswith('[0]'): msgstr += '\0' # Ignore the index - must come in sequence l = l[l.index(']') + 1:] # Skip empty lines l = l.strip() if not l: continue # XXX: Does this always follow Python escape semantics? l = eval(l) if section == ID: msgid += l elif section == STR: msgstr += l else: print >> sys.stderr, 'Syntax error on %s:%d' % (infile, lno), \ 'before:' print >> sys.stderr, l sys.exit(1) # Add last entry if section == STR: add(msgid, msgstr, fuzzy) # Compute output output = generate() try: open(outfile,"wb").write(output) except IOError,msg: print >> sys.stderr, msg def main (): try: opts, args = getopt.getopt(sys.argv[1:], 'hVo:', ['help', 'version', 'output-file=']) except getopt.error, msg: usage(1, msg) outfile = None # parse options for opt, arg in opts: if opt in ('-h', '--help'): usage(0) elif opt in ('-V', '--version'): print >> sys.stderr, "msgfmt.py", __version__ sys.exit(0) elif opt in ('-o', '--output-file'): outfile = arg # do it if not args: print >> sys.stderr, 'No input file given' print >> sys.stderr, "Try `msgfmt --help' for more information." return for filename in args: make(filename, outfile) if __name__ == '__main__': main() monkeysign-1.1/monkeysign/cli.py0000644000000000000000000000743712222377511013745 0ustar # -*- coding: utf-8 -*- # # Copyright (C) 2012-2013 Antoine Beaupré # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import sys import os import getpass from monkeysign.ui import MonkeysignUi import monkeysign.translation class MonkeysignCli(MonkeysignUi): """sign a key in a safe fashion. This command signs a key based on the fingerprint or user id specified on the commandline, encrypt the result and mail it to the user. This leave the choice of publishing the certification to that person and makes sure that person owns the identity signed. This program assumes you have gpg-agent configured to prompt for passwords.""" # override default options to allow passing a keyid usage = _('%prog [options] ') epilog = _(': a GPG fingerprint or key id') def parse_args(self, args): """override main parsing: we absolutely need an argument""" parser = MonkeysignUi.parse_args(self, args) if self.pattern is None: parser.print_usage() sys.exit(_('wrong number of arguments, use -h for full help')) def main(self): """main code execution loop we expect to have the commandline parsed for us """ MonkeysignUi.main(self) if not 'GPG_TTY' in os.environ: os.environ['GPG_TTY'] = os.popen('tty').read() self.log(_('reset GPG_TTY to %s') % os.environ['GPG_TTY']) # 1. fetch the key into a temporary keyring self.find_key() # 2. copy the signing key secrets into the keyring self.copy_secrets() self.warn(_('Preparing to sign with this key\n\n%s') % self.signing_key) # 3. for every user id (or all, if -a is specified) # 3.1. sign the uid, using gpg-agent self.sign_key() # 3.2. export and encrypt the signature # 3.3. mail the key to the user # 3.4. optionnally (-l), create a local signature and import in #local keyring self.export_key() # 4. trash the temporary keyring # implicit def yes_no(self, prompt, default = None): ans = raw_input(prompt.encode('utf-8')) while default is None and ans.lower() not in ["y", "n"]: ans = raw_input(prompt) if default: return default else: return ans.lower() == 'y' def prompt_line(self, prompt): return raw_input(prompt.encode('utf-8')) def prompt_pass(self, prompt): return getpass.getpass(prompt) def choose_uid(self, prompt, key): """present the user with a list of UIDs and let him choose one""" try: allowed_uids = [] for uid in key.uidslist: allowed_uids.append(uid.uid) prompt += _(' (1-%d or full UID, control-c to abort): ') % len(allowed_uids) pattern = raw_input(prompt) while not (pattern in allowed_uids or (pattern.isdigit() and int(pattern)-1 in range(0,len(allowed_uids)))): print _('invalid uid') pattern = raw_input(prompt) if pattern.isdigit(): pattern = allowed_uids[int(pattern)-1] return pattern except KeyboardInterrupt: return False monkeysign-1.1/monkeysign/__init__.py0000644000000000000000000000212212222377511014717 0ustar # -*- coding: utf-8 -*- __version_info__ = ('1', '1') __version__ = '.'.join(__version_info__) __copyright__ = """Copyright (C) 2010-2013 Antoine Beaupré, Jerome Charaoui This program comes with ABSOLUTELY NO WARRANTY. This is free software, and you are welcome to redistribute it under certain conditions. For details see the COPYRIGHT file distributed along this program.""" __license__ = """ This package is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or any later version. This package is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this package; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA """ monkeysign-1.1/monkeysign/gtkui.py0000644000000000000000000004743212222377511014320 0ustar # -*- coding: utf-8 -*- # # Copyright (C) 2010 Jerome Charaoui # Copyright (C) 2012-2013 Antoine Beaupré # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import sys, os, stat, subprocess import re import StringIO import gtk import gobject import pygtk; pygtk.require('2.0') import pango import zbar, zbarpygtk from qrencode import encode as _qrencode from qrencode import encode_scaled as _qrencode_scaled from monkeysign.gpg import Keyring from monkeysign.ui import MonkeysignUi import monkeysign.translation class MonkeysignScanUi(MonkeysignUi): """sign a key in a safe fashion using a webcam to scan for qr-codes This command will fire up a graphical interface and turn on the webcam (if available) on this computer. It will also display a qr-code of your main OpenPGP key. The webcam is used to capture an OpenPGP fingerprint represented as a qrcode (or whatever the zbar library can parse) and then go through a signing process. The signature is then encrypted and mailed to the user. This leave the choice of publishing the certification to that person and makes sure that person owns the identity signed. This program assumes you have gpg-agent configure to prompt for passwords. """ def main(self): # threads *must* be properly initialized to use zbarpygtk gtk.gdk.threads_init() gtk.gdk.threads_enter() self.window = MonkeysignScan() self.window.msui = self # XXX: this probably belongs lower in the stack, # because we don't want to create a temporary keyring # just when we start the graphical UI, but instead # really when we sign MonkeysignUi.main(self) gtk.main() gtk.gdk.threads_leave() def yes_no(self, prompt, default = None): """we ignore default! gotta fix that""" md = gtk.MessageDialog(self.window, gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_QUESTION, gtk.BUTTONS_YES_NO, prompt) gtk.gdk.threads_enter() response = md.run() gtk.gdk.threads_leave() md.destroy() return response == gtk.RESPONSE_YES def abort(self, prompt): """we don't actually abort, just exit threads and resume capture""" md = gtk.MessageDialog(self.window, gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, prompt) gtk.gdk.threads_enter() md.run() gtk.gdk.threads_leave() md.destroy() self.window.resume_capture() def warn(self, prompt): """display the message but let things go""" md = gtk.MessageDialog(self.window, gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_WARNING, gtk.BUTTONS_OK, prompt) gtk.gdk.threads_enter() md.run() gtk.gdk.threads_leave() md.destroy() def choose_uid(self, prompt, key): md = gtk.Dialog(prompt, self.window, gtk.DIALOG_DESTROY_WITH_PARENT, (gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT, gtk.STOCK_OK, gtk.RESPONSE_ACCEPT)) # simple explanation l = gtk.Label(prompt) md.vbox.pack_start(l) l.show() # list of uids self.uid_radios = None for uid in key.uidslist: r = gtk.RadioButton(self.uid_radios, uid.uid) r.show() md.vbox.pack_start(r) if self.uid_radios is None: self.uid_radios = r self.uid_radios.set_active(True) gtk.gdk.threads_enter() response = md.run() gtk.gdk.threads_leave() label = None if response == gtk.RESPONSE_ACCEPT: self.log(_('okay, signing')) label = [ r for r in self.uid_radios.get_group() if r.get_active()][0].get_label() else: self.log(_('user denied signature')) md.destroy() return label class MonkeysignScan(gtk.Window): ui = ''' ''' def __init__(self): super(MonkeysignScan, self).__init__() self.md = [] # modal dialogs to destroy # Set up main window self.set_title(_('Monkeysign (scan)')) self.set_position(gtk.WIN_POS_CENTER) self.connect("destroy", self.destroy) # Menu uimanager = gtk.UIManager() accelgroup = uimanager.get_accel_group() self.add_accel_group(accelgroup) actiongroup = gtk.ActionGroup('MonkeysignGen_Menu') actiongroup.add_actions([ ('File', None, _('_File')), ('Save as...', gtk.STOCK_SAVE, _('_Save as...'), None, None, self.save_qrcode), ('Print', gtk.STOCK_PRINT, _('_Print'), None, None, self.print_op), ('Edit', None, '_Edit'), ('Copy', gtk.STOCK_COPY, _('_Copy'), None, _('Copy image to clipboard'), self.clip_qrcode), ('Quit', gtk.STOCK_QUIT, _('_Quit'), None, None, self.destroy), ]) uimanager.insert_action_group(actiongroup, 0) uimanager.add_ui_from_string(self.ui) # Video device list combo box video_found = False cell = gtk.CellRendererText() cell.props.ellipsize = pango.ELLIPSIZE_END self.video_ls = gtk.ListStore(str) self.video_cb = gtk.ComboBox(self.video_ls) self.video_cb.pack_start(cell, True) self.video_cb.add_attribute(cell, 'text', 0) for (root, dirs, files) in os.walk("/dev"): for dev in files: path = os.path.join(root, dev) if not os.access(path, os.F_OK): continue info = os.stat(path) if stat.S_ISCHR(info.st_mode) and os.major(info.st_rdev) == 81: video_found = True self.video_ls.append([path]) self.video_cb.connect("changed", self.video_changed) # Webcam preview display if video_found == True: self.zbar = zbarpygtk.Gtk() self.zbar.connect("decoded-text", self.decoded) self.zbarframe = gtk.Frame() self.zbarframe.add(self.zbar) self.video_cb.set_active(0) else: camframe = gtk.Frame() self.zbarframe = camframe self.zbar = zbarpygtk.Gtk() error_icon = gtk.Image() error_icon.set_from_stock(gtk.STOCK_DIALOG_ERROR, gtk.ICON_SIZE_DIALOG) vbox = gtk.VBox() error_icon_bottom = gtk.Alignment(0, 1, 1, 0) error_icon_bottom.add(error_icon) error_label_top = gtk.Alignment(0, 0, 1, 0) error_label_top.add(gtk.Label(_('No video device detected.'))) vbox.pack_start(error_icon_bottom) vbox.pack_start(error_label_top) vbox.set_size_request(320, 320) camframe.add(vbox) # Ultimate keys list self.ultimate_keys = Keyring().get_keys(None, True, False).values() # Keep ultimately trusted keys in memory self.mykey = gtk.combo_box_new_text() cell = gtk.CellRendererText() cell.props.ellipsize = pango.ELLIPSIZE_END i = 0 actions = [] for key in self.ultimate_keys: self.mykey.append_text(key.uidslist[0].uid) i += 1 if (i > 0): self.mykey.set_active(0) # QR code display self.pixbuf = None # Hold QR code in pixbuf self.last_allocation = gtk.gdk.Rectangle() # Remember last allocation when resizing self.printsettings = None # Initialise print settings self.connect("expose-event", self.expose_event) # hook up to resize events self.qrcode = gtk.Image() # QR Code widget save = gtk.Button(stock=gtk.STOCK_SAVE) # Save button save.connect("clicked", self.save_qrcode); printbtn = gtk.Button(stock=gtk.STOCK_PRINT) # Print button printbtn.connect("clicked", self.print_op); self.clip = gtk.Clipboard() # Clipboard self.last_allocation = self.get_allocation() # Setup window layout mainvbox = gtk.VBox() mainhbox = gtk.HBox() lvbox = gtk.VBox() lvbox.pack_start(self.video_cb, False, False) lvbox.pack_start(self.zbarframe, False, False, 5) mainhbox.pack_start(lvbox, False, False, 10) mainvbox.pack_start(uimanager.get_widget('/MenuBar'), False, False) mainvbox.pack_start(self.mykey, False, False) mainvbox.pack_start(mainhbox, False, False, 10) self.add(mainvbox) # Setup window layout hbox = gtk.HBox(False, 2) vbox = gtk.VBox(False, 2) self.swin = gtk.ScrolledWindow() self.swin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) self.swin.add_with_viewport(self.qrcode) vbox.pack_start(self.swin, True, True, 0) hbox_btns = gtk.HBox(False, 2) hbox_btns.pack_start(save, False, False, 3) hbox_btns.pack_start(printbtn, False, False, 3) halign = gtk.Alignment(0.5, 0, 0, 0) halign.add(hbox_btns) vbox.pack_start(halign, False, False, 3) hbox.pack_start(vbox, True, True, 10) mainhbox.pack_start(hbox, True, True, 10) self.mykey.connect("changed", self.key_changed) # Start the show self.show_all() def expose_event(self, widget, event): """When window is resized, regenerate the QR code""" if self.get_allocation() != self.last_allocation: self.last_allocation = self.get_allocation() self.key_changed() def key_changed(self, action=None, current=None, user_data=None): """When another key is chosen, generate new QR code""" x = self.mykey.get_active(); fpr = self.ultimate_keys[x].fpr self.pixbuf = self.image_to_pixbuf(self.make_qrcode(fpr)) self.qrcode.set_from_pixbuf(self.pixbuf) def video_changed(self, widget=None): """callback invoked when a new video device is selected from the drop-down list. sets the new device for the zbar widget, which will eventually cause it to be opened and enabled """ i = self.video_cb.get_active_iter() if i: dev = self.video_cb.get_model().get_value(i, 0) self.zbar.set_video_device(dev) else: self.zbar.set_video_enabled(False) def make_qrcode(self, fingerprint): """Given a fingerprint, generate a QR code with appropriate prefix""" rect = self.swin.get_allocation() if rect.width < rect.height: size = rect.width - 15 else: size = rect.height - 15 version, width, image = _qrencode_scaled('OPENPGP4FPR:'+fingerprint,size,0,1,2,True) return image def save_qrcode(self, widget=None): """Use a file chooser dialog to enable user to save the current QR code as a PNG image file""" key = self.ultimate_keys[self.mykey.get_active()] image = self.make_qrcode(key.fpr) dialog = gtk.FileChooserDialog(_('Save QR code'), None, gtk.FILE_CHOOSER_ACTION_SAVE, (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_SAVE, gtk.RESPONSE_OK)) dialog.set_default_response(gtk.RESPONSE_OK) dialog.set_current_name(key.keyid() + '.png') dialog.show() response = dialog.run() if response == gtk.RESPONSE_OK: name = dialog.get_filename() image.save(name, 'PNG') elif response == gtk.RESPONSE_CANCEL: pass dialog.destroy() return def clip_qrcode(self, widget=None): self.clip.set_image(self.pixbuf) def print_op(self, widget=None): keyid = self.ultimate_keys[self.mykey.get_active()].subkeys[0].keyid() print_op = gtk.PrintOperation() print_op.set_job_name('Monkeysign-'+keyid) print_op.set_n_pages(1) print_op.connect("draw_page", self.print_qrcode) res = print_op.run(gtk.PRINT_OPERATION_ACTION_PRINT_DIALOG, self) def print_qrcode(self, operation=None, context=None, page_nr=None): ctx = context.get_cairo_context() ctx.set_source_pixbuf(self.pixbuf, 0, 0) ctx.paint() ctx.restore() return def image_to_pixbuf(self, image): """Utility function to convert a PIL image instance to Pixbuf""" fd = StringIO.StringIO() image.save(fd, "ppm") contents = fd.getvalue() fd.close() loader = gtk.gdk.PixbufLoader("pnm") loader.write(contents, len(contents)) pixbuf = loader.get_pixbuf() loader.close() return pixbuf def decoded(self, zbar, data): """callback invoked when a barcode is decoded by the zbar widget. checks for an openpgp fingerprint """ def update_progress_callback(*args): """callback invoked for pulsating progressbar """ if self.keep_pulsing: self.progressbar.pulse() return True else: return False def watch_out_callback(pid, condition): """callback invoked when gpg key download is finished """ self.keep_pulsing=False self.dialog.destroy() self.msui.log(_('fetching finished')) if condition == 0: # 2. copy the signing key secrets into the keyring self.msui.copy_secrets() # 3. for every user id (or all, if -a is specified) # 3.1. sign the uid, using gpg-agent self.msui.sign_key() # 3.2. export and encrypt the signature # 3.3. mail the key to the user self.msui.export_key() # 3.4. optionnally (-l), create a local signature and import in #local keyring # 4. trash the temporary keyring self.resume_capture() for md in self.md: md.destroy() else: # 1.b) from the local keyring (@todo try that first?) self.msui.find_key() return # Look for prefix and hexadecimal 40-ascii-character fingerprint m = re.search("((?:[0-9A-F]{4}\s*){10})", data, re.IGNORECASE) if m != None: # Found fingerprint, get it and strip spaces for GPG self.msui.pattern = m.group(1).replace(' ', '') # Capture and display the video frame containing QR code self.zbarframe.set_shadow_type(gtk.SHADOW_NONE) alloc = self.zbarframe.allocation pixbuf = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, True, 8, alloc.width, alloc.height) pixbuf.get_from_drawable(self.zbarframe.window, self.zbarframe.window.get_colormap(), alloc.x, alloc.y, 0, 0, alloc.width, alloc.height) self.capture = gtk.Image() self.capture.set_from_pixbuf(pixbuf) self.capture.show() self.zbarframe.remove(self.zbar) self.zbarframe.add(self.capture) self.zbarframe.set_shadow_type(gtk.SHADOW_ETCHED_IN) # Disable video capture self.zbar.set_video_enabled(False) # 1. fetch the key into a temporary keyring - we override the find_key() because we want to be interactive # 1.a) if allowed (@todo), from the keyservers if self.msui.options.keyserver is not None: self.msui.tmpkeyring.context.set_option('keyserver', self.msui.options.keyserver) command = self.msui.tmpkeyring.context.build_command(['recv-keys', self.msui.pattern]) self.msui.log('cmd: ' + str(command)) self.dialog = gtk.Dialog(title=_('Please wait'), parent=None, flags=gtk.DIALOG_MODAL, buttons=None) self.dialog.add_button('gtk-cancel', gtk.RESPONSE_CANCEL) message = gtk.Label(_('Retrieving public key from server...')) message.show() self.progressbar = gtk.ProgressBar() self.progressbar.show() self.dialog.vbox.pack_start(message, True, True, 5) self.dialog.vbox.pack_start(self.progressbar, False, False, 5) self.dialog.set_size_request(250, 100) self.keep_pulsing = True proc = subprocess.Popen(command, 0, None, subprocess.PIPE, subprocess.PIPE, subprocess.PIPE) gobject.child_watch_add(proc.pid, watch_out_callback) gobject.timeout_add(100, update_progress_callback) if self.dialog.run() == gtk.RESPONSE_CANCEL: proc.kill() return else: print _('ignoring found data: %s') % data def resume_capture(self): self.zbarframe.remove(self.capture) self.zbarframe.add(self.zbar) self.zbar.set_video_enabled(True) self.capture = None def destroy(self, widget, data=None): self.zbar.set_video_enabled(False) del self.msui gtk.main_quit() monkeysign-1.1/monkeysign/translation.py0000644000000000000000000000663512222377511015533 0ustar # -*- coding: utf-8 -*- # # Copyright (C) 2012-2013 Antoine Beaupré # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import os import sys import msgfmt from distutils.command.build import build from distutils.core import Command import locale import gettext import pkg_resources # Initialize gettext, taken from deluge 1.3.3 (GPL3) try: locale.setlocale(locale.LC_ALL, '') if hasattr(locale, "bindtextdomain"): locale.bindtextdomain('monkeysign', pkg_resources.resource_filename('monkeysign', "po")) if hasattr(locale, "textdomain"): locale.textdomain('monkeysign') gettext.install('monkeysign', pkg_resources.resource_filename('monkeysign', "po"), unicode=True, names='ngettext') except Exception, e: print "Unable to initialize translations: %s" % e import __builtin__ __builtin__.__dict__["_"] = lambda x: x # stolen from deluge-1.3.3 (GPL3) class build_trans(Command): description = 'Compile .po files into .mo files' user_options = [ ('build-lib', None, "lib build folder"), ('po-dir', 'po/', 'directory where .po files are stored, relative to the current directory'), ] def initialize_options(self): self.build_lib = None self.po_dir = 'po/' def finalize_options(self): self.set_undefined_options('build', ('build_lib', 'build_lib')) def run(self): po_dir = self.po_dir appname = self.distribution.get_name() self.announce('compiling po files from %s' % po_dir, 2) uptoDate = False for path, names, filenames in os.walk(po_dir): for f in filenames: uptoDate = False if f.endswith('.po'): lang = f[:len(f) - 3] src = os.path.join(path, f) dest_path = os.path.join(self.build_lib, appname, 'po', lang, \ 'LC_MESSAGES') dest = os.path.join(dest_path, appname + '.mo') if not os.path.exists(dest_path): os.makedirs(dest_path) if not os.path.exists(dest): sys.stdout.write('%s, ' % lang) sys.stdout.flush() msgfmt.make(src, dest) else: src_mtime = os.stat(src)[8] dest_mtime = os.stat(dest)[8] if src_mtime > dest_mtime: sys.stdout.write('%s, ' % lang) sys.stdout.flush() msgfmt.make(src, dest) else: uptoDate = True if uptoDate: self.announce('po files already upto date.', 2) build.sub_commands.append(('build_trans', None)) monkeysign-1.1/monkeysign/documentation.py0000644000000000000000000001347012222377511016041 0ustar # -*- coding: utf-8 -*- """build_manpage command -- Generate man page from setup()""" import os import datetime from distutils.command.build import build from distutils.core import Command from distutils.errors import DistutilsOptionError import optparse class build_manpage(Command): description = 'Generate man page from setup().' user_options = [ ('output=', 'O', 'output directory'), ('parsers=', None, 'module path to optparser (e.g. command:mymod:func)'), ] def initialize_options(self): self.output = None self.parsers = None def finalize_options(self): if self.output is None: raise DistutilsOptionError('\'output\' option is required') if self.parsers is None: raise DistutilsOptionError('\'parser\' option is required') self._today = datetime.date.today() self._parsers = [] for parser in self.parsers.split(): scriptname, mod_name, func_name = parser.split(':') fromlist = mod_name.split('.') try: class_name, func_name = func_name.split('.') except ValueError: class_name = None mod = __import__(mod_name, fromlist=fromlist) if class_name is not None: cls = getattr(mod, class_name) parser = getattr(cls, func_name)() else: parser = getattr(mod, func_name)() parser.formatter = ManPageFormatter() parser.formatter.set_parser(parser) parser.prog = scriptname self._parsers.append(parser) def _markup(self, txt): return txt.replace('-', '\\-') def _write_header(self, parser): appname = parser.prog ret = [] ret.append('.TH %s 1 %s\n' % (self._markup(appname), self._today.strftime('%Y\\-%m\\-%d'))) description = parser.get_description() if description: name = self._markup('%s - %s' % (self._markup(appname), description.splitlines()[0])) else: name = self._markup(appname) ret.append('.SH NAME\n%s\n' % name) # override argv, we need to format it later prog_bak = parser.prog parser.prog = '' synopsis = parser.get_usage().lstrip(' ') parser.prog = prog_bak if synopsis: ret.append('.SH SYNOPSIS\n.B %s\n%s\n' % (self._markup(appname), synopsis)) long_desc = parser.get_description() if long_desc: ret.append('.SH DESCRIPTION\n%s\n' % self._markup("\n".join(long_desc.splitlines()[1:]))) return ''.join(ret) def _write_options(self, parser): ret = ['.SH OPTIONS\n'] ret.append(parser.format_option_help()) return ''.join(ret) def _write_footer(self, parser): ret = [] appname = self.distribution.get_name() author = '%s <%s>' % (self.distribution.get_author(), self.distribution.get_author_email()) ret.append(('.SH AUTHORS\n.B %s\nwas written by %s.\n' % (self._markup(appname), self._markup(author)))) homepage = self.distribution.get_url() ret.append(('.SH DISTRIBUTION\nThe latest version of %s may ' 'be downloaded from\n' '.UR %s\n.UE\n' % (self._markup(appname), self._markup(homepage),))) return ''.join(ret) def run(self): for parser in self._parsers: manpage = [] manpage.append(self._write_header(parser)) manpage.append(self._write_options(parser)) manpage.append(self._write_footer(parser)) try: os.mkdir(self.output) except OSError: # ignore already existing directory pass path = os.path.join(self.output, parser.prog + '.1') self.announce('writing man page to %s' % path, 2) stream = open(path, 'w') stream.write(''.join(manpage)) stream.close() class ManPageFormatter(optparse.HelpFormatter): def __init__(self, indent_increment=2, max_help_position=24, width=None, short_first=1): optparse.HelpFormatter.__init__(self, indent_increment, max_help_position, width, short_first) def _markup(self, txt): return txt.replace('-', '\\-') def format_usage(self, usage): return self._markup(usage) def format_heading(self, heading): if self.level == 0: return '' return '.TP\n%s\n' % self._markup(heading.upper()) def format_option(self, option): result = [] opts = self.option_strings[option] result.append('.TP\n.B %s\n' % self._markup(opts)) if option.help: help_text = '%s\n' % self._markup(self.expand_default(option)) result.append(help_text) return ''.join(result) class build_slides(Command): description = 'Generate the HTML presentation with rst2s5.' user_options = [ ('file=', 'f', 'rst file'), ] def initialize_options(self): self.file = None def finalize_options(self): if self.file is None: raise DistutilsOptionError('\'file\' option is required') def run(self): html = os.path.dirname(self.file) + os.path.splitext(os.path.basename(self.file))[0] + '.html' self.announce('processing slides from %s to %s' % (self.file, html), 2) os.system('rst2s5 --theme default "%s" "%s"' % (self.file, html)) build.sub_commands.append(('build_manpage', None)) build.sub_commands.append(('build_slides', None)) monkeysign-1.1/po/0000755000000000000000000000000012222377511011044 5ustar monkeysign-1.1/po/Makefile0000644000000000000000000000032312222377511012502 0ustar *.po: messages.pot msgmerge -U $@ $< messages.pot: ../monkeysign/cli.py ../monkeysign/gtkui.py ../monkeysign/ui.py ../monkeysign/gpg.py pygettext -k_ -kN_ -D -o $@ $^ xgettext -k_ -kN_ -L Python -j -o $@ $^ monkeysign-1.1/po/messages.pot0000644000000000000000000002027612222377511013406 0ustar # 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 , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2013-08-14 23:21-0400\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=CHARSET\n" "Content-Transfer-Encoding: 8bit\n" #: ../monkeysign/cli.py:24 msgid "" "sign a key in a safe fashion.\n" "\n" "This command signs a key based on the fingerprint or user id\n" "specified on the commandline, encrypt the result and mail it to the\n" "user. This leave the choice of publishing the certification to that\n" "person and makes sure that person owns the identity signed.\n" "\n" "This program assumes you have gpg-agent configured to prompt for\n" "passwords." msgstr "" #: ../monkeysign/cli.py:35 msgid "%prog [options] " msgstr "" #: ../monkeysign/cli.py:36 msgid ": a GPG fingerprint or key id" msgstr "" #: ../monkeysign/cli.py:43 ../monkeysign/ui.py:99 msgid "wrong number of arguments, use -h for full help" msgstr "" #: ../monkeysign/cli.py:59 #, python-format msgid "" "Preparing to sign with this key\n" "\n" "%s" msgstr "" #: ../monkeysign/cli.py:88 #, python-format msgid " (1-%d or full UID, control-c to abort): " msgstr "" #: ../monkeysign/cli.py:92 msgid "invalid uid" msgstr "" #: ../monkeysign/gpg.py:209 #, python-format msgid "could not find pattern '%s' in input" msgstr "" #: ../monkeysign/gpg.py:357 ../monkeysign/gpg.py:377 #, python-format msgid "unexpected GPG exit code in list-keys: %d" msgstr "" #: ../monkeysign/gpg.py:389 #, python-format msgid "encryption to %s failed: %s." msgstr "" #: ../monkeysign/gpg.py:400 #, python-format msgid "decryption failed: %s" msgstr "" #: ../monkeysign/gpg.py:452 ../monkeysign/gpg.py:461 ../monkeysign/gpg.py:490 #, python-format msgid "unable to open key for editing: %s" msgstr "" #: ../monkeysign/gpg.py:506 msgid "password confirmation failed" msgstr "" #: ../monkeysign/gpg.py:676 #, python-format msgid "record type '%s' not implemented" msgstr "" #: ../monkeysign/gtkui.py:38 msgid "" "sign a key in a safe fashion using a webcam to scan for qr-codes\n" "\n" "This command will fire up a graphical interface and turn on the webcam\n" "(if available) on this computer. It will also display a qr-code of\n" "your main OpenPGP key.\n" "\n" "The webcam is used to capture an OpenPGP fingerprint represented as a\n" "qrcode (or whatever the zbar library can parse) and then go through a\n" "signing process.\n" "\n" "The signature is then encrypted and mailed to the user. This leave the\n" "choice of publishing the certification to that person and makes sure\n" "that person owns the identity signed.\n" "\n" "This program assumes you have gpg-agent configure to prompt for\n" "passwords.\n" msgstr "" #: ../monkeysign/gtkui.py:121 msgid "okay, signing" msgstr "" #: ../monkeysign/gtkui.py:124 msgid "user denied signature" msgstr "" #: ../monkeysign/gtkui.py:151 msgid "Monkeysign (scan)" msgstr "" #: ../monkeysign/gtkui.py:161 msgid "_File" msgstr "" #: ../monkeysign/gtkui.py:162 msgid "_Save as..." msgstr "" #: ../monkeysign/gtkui.py:163 msgid "_Print" msgstr "" #: ../monkeysign/gtkui.py:165 msgid "Copy image to clipboard" msgstr "" #: ../monkeysign/gtkui.py:165 msgid "_Copy" msgstr "" #: ../monkeysign/gtkui.py:166 msgid "_Quit" msgstr "" #: ../monkeysign/gtkui.py:207 msgid "No video device detected." msgstr "" #: ../monkeysign/gtkui.py:313 msgid "Save QR code" msgstr "" #: ../monkeysign/gtkui.py:375 msgid "fetching finished" msgstr "" #: ../monkeysign/gtkui.py:425 msgid "Please wait" msgstr "" #: ../monkeysign/gtkui.py:427 msgid "Retrieving public key from server..." msgstr "" #: ../monkeysign/gtkui.py:442 #, python-format msgid "ignoring found data: %s" msgstr "" #: ../monkeysign/ui.py:66 msgid "parse the commandline arguments" msgstr "" #: ../monkeysign/ui.py:69 msgid "request debugging information from GPG engine (lots of garbage)" msgstr "" #: ../monkeysign/ui.py:71 msgid "explain what we do along the way" msgstr "" #: ../monkeysign/ui.py:73 msgid "do not actually do anything" msgstr "" #: ../monkeysign/ui.py:74 msgid "user id to sign the key with" msgstr "" #: ../monkeysign/ui.py:76 msgid "import in normal keyring a local certification" msgstr "" #: ../monkeysign/ui.py:78 msgid "keyserver to fetch keys from" msgstr "" #: ../monkeysign/ui.py:79 msgid "SMTP server to use" msgstr "" #: ../monkeysign/ui.py:81 msgid "Do not send email at all. (Default is to use sendmail.)" msgstr "" #: ../monkeysign/ui.py:83 msgid "" "Override destination email for testing (default is to use the first uid on " "the key or send email to each uid chosen)" msgstr "" #: ../monkeysign/ui.py:128 msgid "Initializing UI" msgstr "" #: ../monkeysign/ui.py:139 #, python-format msgid "deleting the temporary keyring %s" msgstr "" #: ../monkeysign/ui.py:160 msgid "copied your gpg.conf in temporary keyring" msgstr "" #: ../monkeysign/ui.py:209 msgid "find the key to be signed somewhere" msgstr "" #: ../monkeysign/ui.py:213 #, python-format msgid "looking for key %s in your keyring" msgstr "" #: ../monkeysign/ui.py:215 msgid "key not in local keyring" msgstr "" #: ../monkeysign/ui.py:218 #, python-format msgid "fetching key %s from keyservers" msgstr "" #: ../monkeysign/ui.py:222 msgid "please provide a keyid or fingerprint, uids are not supported yet" msgstr "" #: ../monkeysign/ui.py:225 #, python-format msgid "could not find key %s in your keyring or keyservers" msgstr "" #: ../monkeysign/ui.py:234 #, python-format msgid "copying your private key to temporary keyring in %s" msgstr "" #: ../monkeysign/ui.py:242 #, python-format msgid "found secret key: %s" msgstr "" #: ../monkeysign/ui.py:248 msgid "no default secret key found, abort!" msgstr "" #: ../monkeysign/ui.py:249 #, python-format msgid "signing key chosen: %s" msgstr "" #: ../monkeysign/ui.py:253 msgid "could not find public key material, do you have a GPG key?" msgstr "" #: ../monkeysign/ui.py:256 msgid "sign the key uids, as specified" msgstr "" #: ../monkeysign/ui.py:260 #, python-format msgid "found %d keys matching your request" msgstr "" #: ../monkeysign/ui.py:263 #, python-format msgid "" "Signing the following key\n" "\n" "%s\n" "\n" "Sign all identities? [y/N] " msgstr "" #: ../monkeysign/ui.py:275 msgid "Choose the identity to sign" msgstr "" #: ../monkeysign/ui.py:277 msgid "no identity chosen" msgstr "" #: ../monkeysign/ui.py:284 msgid "Really sign key? [y/N] " msgstr "" #: ../monkeysign/ui.py:287 msgid "key signing failed" msgstr "" #: ../monkeysign/ui.py:291 msgid "making a non-exportable signature" msgstr "" #: ../monkeysign/ui.py:296 msgid "" "could not import public key back into public keyring, something is wrong" msgstr "" #: ../monkeysign/ui.py:298 msgid "local key signing failed" msgstr "" #: ../monkeysign/ui.py:306 msgid "no key signed, nothing to export" msgstr "" #: ../monkeysign/ui.py:314 ../monkeysign/ui.py:321 #, python-format msgid "failed to create email: %s" msgstr "" #: ../monkeysign/ui.py:335 #, python-format msgid "sent message through SMTP server %s to %s" msgstr "" #: ../monkeysign/ui.py:341 #, python-format msgid "sent message through sendmail to %s" msgstr "" #: ../monkeysign/ui.py:344 #, python-format msgid "" "not sending email to %s, as requested, here's the email message:\n" "\n" "%s" msgstr "" #: ../monkeysign/ui.py:359 msgid "Your signed OpenPGP key" msgstr "" #: ../monkeysign/ui.py:362 msgid "" "\n" "Please find attached your signed PGP key. You can import the signed\n" "key by running each through `gpg --import`.\n" "\n" "Note that your key was not uploaded to any keyservers. If you want\n" "this new signature to be available to others, please upload it\n" "yourself. With GnuPG this can be done using:\n" "\n" " gpg --keyserver pool.sks-keyservers.net --send-key \n" "\n" "Regards,\n" msgstr "" #: ../monkeysign/ui.py:431 msgid "PGP Key , uid (" msgstr "" #: ../monkeysign/ui.py:446 msgid "This is a multi-part message in PGP/MIME format..." msgstr "" monkeysign-1.1/presentation.rst0000644000000000000000000000215312222377511013674 0ustar Monkeysign: OpenPGP key exchange for humans =========================================== what is PGP ----------- * signs * encrypt * certifies what is the monkeysphere ------------------------ * get out of the CA cartel * supports SSH and HTTPS for now, more to come * useable right now! why u no PGP??? --------------- * key problem with PGP: the web of trust key monkeysign features ----------------------- * display and scan qrcode-encoded fingerprints - no more typing! * caff replacement: * signs in a separate keyring * mails the signatures to each email so that email is verified... * ... and the signed person gets to decide if certification is public and more! --------- * SMTP support (TLS, user auth) * local signatures * unit tests * GUI and commandline interface * modular python architecture instead of single perl script * packaged in Debian, install from git on other OS, packages welcome help needed! ------------ * wrote this from scratch * python-gnupg reimplementation, help needed for merge * translations * testing / bug reports / patches welcome! monkeysign-1.1/setup.py0000755000000000000000000000637612222377511012157 0ustar #!/usr/bin/python # -*- coding: utf-8 -*- # # Copyright (C) 2012-2013 Antoine Beaupré # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . from distutils.core import setup from glob import glob from monkeysign import __version__ as version import monkeysign.documentation import monkeysign.translation setup(name = 'monkeysign', description='OpenPGP key exchange for humans', long_description=""" monkeysign is a tool to overhaul the OpenPGP keysigning experience and bring it closer to something that most primates can understand. The project makes use of cheap digital cameras and the type of bar code known as a QRcode to provide a human-friendly yet still-secure keysigning experience. No more reciting tedious strings of hexadecimal characters. And, you can build a little rogue's gallery of the people that you have met and exchanged keys with! """, version=version, author='Antoine Beaupré', author_email='anarcat@debian.org', url='http://web.monkeysphere.info/', packages=['monkeysign'], scripts=['scripts/monkeysign', 'scripts/monkeyscan'], cmdclass={'build_manpage': monkeysign.documentation.build_manpage, 'build_trans': monkeysign.translation.build_trans, 'build_slides': monkeysign.documentation.build_slides, }, data_files=[('share/man/man1', glob('man/*.1')), ('share/doc/monkeysign', glob('presentation.*')), ('share/doc/monkeysign/ui/default', glob('ui/default/*')), ], classifiers=[ 'Development Status :: 4 - Beta', 'Intended Audience :: Developers', 'Intended Audience :: System Administrators', 'Intended Audience :: End Users/Desktop', 'Intended Audience :: Information Technology', 'Intended Audience :: Legal Industry', 'Intended Audience :: Telecommunications Industry', 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)', 'Programming Language :: Python', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 2.4', 'Programming Language :: Python :: 2.5', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Operating System :: OS Independent', 'Environment :: X11 Applications :: GTK', 'Environment :: Console', 'Natural Language :: English', 'Topic :: Communications :: Email', 'Topic :: Multimedia :: Video :: Capture', 'Topic :: Security :: Cryptography', 'Topic :: Software Development :: Libraries :: Python Modules' ], ) monkeysign-1.1/TODO0000644000000000000000000000724512222377511011126 0ustar Release process =============== * make sure tests pass (`./test.py`) * update version in `monkeysign/__init__.py` and run `dch -i -D unstable` * signed and annotated tag (`git tag -s -u keyid x.y`) * build Debian package (`git-buildpackage`) * install and test Debian package (`dpkg -i ../build-area/monkeysign_*.deb`) * upload Debian package * push commits and tags to the git repository * add announcement on website and mailing list 2.0 blockers ============ The following needs to be fixed before 2.0 is released. * properly handle exceptions in GTK UI * usability issues: * don't popup - because we can't control location - besides popups are evil * merge the two monkeyscan classes, and cleanup the GTK UI code * move video dropdown in preferences * add explanations on what will happen, maybe pic of two laptops * move key selection an identity menu * label the two frames to explain what the camera and qrcode does * drop the save/print buttons * there shouldn't be a question "sign all user IDs?" -- there should just be a list of user IDs presented with checkboxes next to them all pre-checked by default and the user can un-check the ones they want * gpg-agent should be started if it's not already * merge with python-gnupg (see below) * make all options accessible from the GUI (preferences? see configparser and json modules) * make sure the GUI and CLI have feature parity, for example right now the GUI can't do local signatures without a commandline flag * maybe then: merge in a single "monkeysign" binary * if not merging: harmonize error handling in gpg.py, see the file docstring for more info Other wishlist items ==================== * reuse tactical tech's security in a box PGP training material for documentation * having a windows port would be important for wider adoption * encode a "can you keep my picture" in the qrcode - the code would be advisory (as the remote implementations could simply not respect the setting) - but if it is set, we should avoid keeping a picture of the person associated with the qrcode, maybe by blurring out the image outside of the qrcode * make a batch mode: pictures are recorded in a gallery and then can be processed one at a time * recognise my own fingerprint "yep, that's me!" * complete keyring management? * key generation The merge question ================== This software has a primitive GPG Python API that duplicates the work of at least two other libraries. The `pythong-gnupg` library is particularly similar and communication was started to consider the possibility of merging. This section details how we should deal with this. * decide to merge or split from pythong-gnupg (done: we merge) - for merge arguments: - gnupg has more history and authors - has more features (see below) - avoid project proliferation (already 4 python APIs to gpg) - may be less work - against merge arguments: - needs to rewrite monkeysign again (fairly easy) - i just spend about 20 hours in two days on this project - python-gnupg doesn't seem to have a VCS - hosted on code.google.com, BSD license (minor) * apply those improvements to python-gnupg - add key signing support - split the "context" and "keyring" classes - port monkeysign to python-gnupg - make sure python-gnupg is secure (ie. that it doesn't use popen([...], shell=True), see below It seems that we have a new upstream for python-gnupg that resolves most problems documented here: https://github.com/isislovecruft/python-gnupg We are in contact with upstream and they are open to merging in changes, so we will go in that direction. monkeysign-1.1/setup.cfg0000644000000000000000000000026512222377511012252 0ustar [build_manpage] output=man/ parsers=monkeysign:monkeysign.cli:MonkeysignCli.parser monkeyscan:monkeysign.gtkui:MonkeysignScanUi.parser [build_slides] file=presentation.rst monkeysign-1.1/tests/0000755000000000000000000000000012222377511011570 5ustar monkeysign-1.1/tests/323F39BD-secret.asc0000644000000000000000000000362012222377511014543 0ustar -----BEGIN PGP PRIVATE KEY BLOCK----- Version: GnuPG v1.4.12 (GNU/Linux) lQHYBFAReAcBBAC0bT/SR1MdTANnaVS1zOCnfWcRH8Sk8S1mSME1fs9YMvM6rjiN xo2yKrQ51TJdB/+3NZI1x77LFbxpFCcdhn+3Ah9B8ORXaWfYy14gcLraWFwQo+j5 wY/7Un75Chm+/yQJmCrrLpvh+GeEshnXgVLSpGOTDX0O/LdvpuVvDSTpKQARAQAB AAP9EAUfRJF2rMRCDR2KGvZNADIfQ6L5d9e+OzW8if5vdJpZhF1RwizeCfLGu9fV N/Ns3hyQldvdcfTFHONgMbvufPAqvAW5oxw7Pn3w6cyWX/ShkgP47Oodb7bLazgc 9JacYW1UZ4RyPv6lC9V/8ZKmnU1gFFyIKigQOuGZkdiDdC0CAMzZ30FeYD0DMPeX KqrTLXIbK5CEn227oeURNOCRUDDFBeBvM9Hrqh7LZJedYQ8sTJwstb3NQMsWL+1B je6hoZMCAOF6K3QDwEZgwJZuloEijyjLAcejIxHHgro8ctaYz8mfBhsqrOXqW8w7 487TnHlx+ZqyaSQFkCcN6oRBfVZqb9MCANOLPpgdX9+jkNxcjs0eaSEAnDTUvwNK c/bM0aH4MZnqi9josBPBMhxwXr+z7EOO6tISRQldkH4dh6ErLRApp4ehI7QuTW9u a2V5c3BoZXJlIHNlY29uZCB0ZXN0IGtleSA8YmFyQGV4YW1wbGUuY29tPoi3BBMB CAAhBQJQEXgHAhsDBQsJCAcDBRUKCQgLBRYCAwEAAh4BAheAAAoJEKMedeQyPzm9 esMEAIAHat18hKJpiVm3MrbZZD3h6+Ij/om3UBjlsIpFWNI1+oN5M3jw2DIj6jIT Xi+YwCMWLBmNhBKeNPdCWnfJvvimtu0r5efnOYCDoXp2rsf1eKC0/C+ZblB+MpGv LIACCBy8lfbP5xYoD4dLFGKE2kDrJN2HTm/h5HqwJuYuvcW4nQHYBFAReAcBBADO zLogkXksQ9eoHWWiBNMCqFlFXeNm1q02fZBZOnof4/p+cGYbBd4ErfqH6bxjuL1B fGaWii7nWU+Z0cMdPmZx9yZoFOJy4LUuQuzYBblhF/KK+jrYOyn1PaamLg/1w2RL 8bT+gwe9OYs+qNNLJyvJ9REU71IY5XHTVoGqmoYEnwARAQABAAP/Wqh4jk+H8gZZ 0/rzM6huwvL/k7/ZsZs6OzGnpt5SbImapnRq5rzXPHDy7ENlEciKu1sodCVm84f9 M/83zKmu6cxttH8Dsjg8wsgd6YEZYjF3USHq0zPQQUME8FMN9y0Pey4/TnW/O4lH vrnEmd+gY27u70xe2TLIei15AC3m3QUCAN9IBqBUKF7NLPHo0p2n+9uaS7cZB1V6 OBIrtB4Ba6RkDtuJZFnREdAiJ4cl2UNbI+Ujac9yl7djuyCGwbe5aPUCAO0abI0d mEO6z7xVzvtTl7Sc1X+NyDsKHKayIQZmWXGCf1sIziXv8Nmu8qFI5zlv5iyQKAKE N4+iuYZofih7isMCAL2RgFUm8fDz2u3K3581s9Uo+nkPI+K2ECDALqZZgCxOC0OB N7pFuzQ9qsvRp3Dtn7Nwvxc8ewTaQ/ZlTNuk8COkHYifBBgBCAAJBQJQEXgHAhsM AAoJEKMedeQyPzm9tyAD/iZ0xyN8L6iQaH/2x1bL3wybsj+ABEBy/1L6pwhPGY+P 6EMjJmz5ZP9tQ2eEWLcfFTyrIIHb3qjBNHGheFQYNM1DCNyGSJkp06/vZqYRSZqU xPnhOYivlrfdWNheQpv9DkJms0kBAfnrU2vMtqbI6eHMZFFPAXOCJp8Kol40/Y4S =fhBy -----END PGP PRIVATE KEY BLOCK----- monkeysign-1.1/tests/96F47C6A.asc0000644000000000000000000000245512222377511013277 0ustar -----BEGIN PGP PUBLIC KEY BLOCK----- Version: GnuPG v1.4.12 (GNU/Linux) mI0EUAlt9AEEAMVygQA7sGE7xS5lGk8bPK7vtBuNAb9ETjXmC8jPLZ35KTviRq1c NfFl0J66ObgcLEQjl84GMQ6Du6qLDAAgwi/2TqzmoGljXEiOd+lePBOhEV7WgkTx sBhniZINe/q0Sv4OJzQxfaOsW3eyfHJEXq6oCGqX+f0Nat1ygPNt/ji7ABEBAAG0 GlRlc3QgS2V5IDxmb29AZXhhbXBsZS5jb20+iLgEEwECACIFAlAJbfQCGwMGCwkI BwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEIbk5wqW9HxqzpQD/0UZ7vg8ck/AlB6X N43ecVoX8KuLQEV1QGc4dJ9zDJ7yfojs1b1Nlcb397VEufTeRDOyCueop5cvqv6f ViFrA/V1xsn2RIjZhYSjk9m7UPRItpwLGHv6WSmYeSoCYXbTBtf+bf76c0TN97a3 BQTyRpa85GsWcT3S2Vpmv+PmcW2GtC1TZWNvbmQgVGVzdCBLZXkgPHVuaXR0ZXN0 c0Btb25rZXlzcGhlcmUuaW5mbz6ItwQTAQgAIQUCUggJxQIbAwULCQgHAwUVCgkI CwUWAgMBAAIeAQIXgAAKCRCG5OcKlvR8amc6A/0SUtaGHWVxChOPcdnjHN2/UAWL bMSLes2kJsjxRQ5aV5X3ZE7rsiQn2cIKs1f6hA159yicsim+dIq9GD6+yaIsR4Vv q9p7FvrbjR6H0CdWRb1ZWQCD0wiEB5H+nT8Hppijt+6s8X0LQf9U+Pp9EnsRG2G+ aLVdmhVnEzMsLN+mhriNBFAJbfQBBADdI8x+RWaUfPXPxIOa2xwQ/PSmUvX59l6X wJy5/Ai77fuNPU7NzEcXj5CS5JLjAOCrzUn8hHWMaxSMiqHc3dkUptZsrIBihTo6 52ikQ6/B6czPpPG31Gz+0Ptwo8hrLov0AIHb/S3p64poY3wrxUuQSItJRIhkiWeJ WebjkSp2iQARAQABiJ8EGAECAAkFAlAJbfQCGwwACgkQhuTnCpb0fGpJ4QP/WW93 MKAxXLpyNrxcKRap2dQdR6/NVL7EwK+1d/aGliC8FnAuSgEsO5i6xc3yJtsUQHjF 1+gyjrSfFi98Vod3wkcts35tNb6I6/Q0aOzCQjz+zdg3RHUGaOPp0DyL8ydZ4nxQ jftIrEQdl7vyLWFuQkTo3UOg4P8LOlUJJhFeI74= =WOMM -----END PGP PUBLIC KEY BLOCK----- monkeysign-1.1/tests/96F47C6A-revoke.asc0000644000000000000000000000063212222377511014563 0ustar -----BEGIN PGP PUBLIC KEY BLOCK----- Version: GnuPG v1.4.12 (GNU/Linux) Comment: A revocation certificate should follow iLMEIAECAB0FAlH/mXUWHQN0ZXN0IGtleSwgZG8gbm90IHVzZQAKCRCG5OcKlvR8 at/WBAC3/8CH+pDMqC1HEOueOTF6SrT6jRkmbWvcUeYfxc+eOENnZZNwUoCHM4xa Y9RMHAWKB5dDZVFhafAO5x5wqSMfiwbPu+NVQhUd9o7a1e03FTnZ3gI8kVDX6Enk YOjLCUXY8Yu/BnFt2kKYFkYl5+OuN97xhX04fC1Bv1WsuDQHww== =wTco -----END PGP PUBLIC KEY BLOCK----- monkeysign-1.1/tests/7B75921E.asc0000644000000000000000000002315412222377511013252 0ustar -----BEGIN PGP PUBLIC KEY BLOCK----- Version: GnuPG v1.4.12 (GNU/Linux) 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 8nyFHgK/Q8jPXyef3Reb20jm/vcw+tTb+68= =VZ8W -----END PGP PUBLIC KEY BLOCK----- monkeysign-1.1/tests/test_network.py0000755000000000000000000000457512222377511014710 0ustar #!/usr/bin/python # -*- coding: utf-8 -*- # # Copyright (C) 2012-2013 Antoine Beaupré # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """Tests that hit the network. Those tests are in a separate file to allow the base set of tests to be ran without internet access. """ import unittest import sys, os import signal sys.path.append(os.path.dirname(__file__) + '/..') from monkeysign.gpg import TempKeyring from test_lib import TestTimeLimit, AlarmException class TestGpgNetwork(TestTimeLimit): """Separate test cases for functions that hit the network each test needs to run under a specific timeout so we don't wait on the network forever""" def setUp(self): self.gpg = TempKeyring() self.gpg.context.set_option('keyserver', 'pool.sks-keyservers.net') TestTimeLimit.setUp(self) def test_fetch_keys(self): """test key fetching from keyservers""" try: self.assertTrue(self.gpg.fetch_keys('4023702F')) except AlarmException: raise unittest.case._ExpectedFailure(sys.exc_info()) def test_special_key(self): """test a key that sign_key had trouble with""" self.assertTrue(self.gpg.import_data(open(os.path.dirname(__file__) + '/96F47C6A.asc').read())) self.assertTrue(self.gpg.import_data(open(os.path.dirname(__file__) + '/96F47C6A-secret.asc').read())) try: self.assertTrue(self.gpg.fetch_keys('3CCDBB7355D1758F549354D20B123309D3366755')) except AlarmException: raise unittest.case._ExpectedFailure(sys.exc_info()) self.assertTrue(self.gpg.sign_key('3CCDBB7355D1758F549354D20B123309D3366755', True)) def tearDown(self): TestTimeLimit.tearDown(self) del self.gpg if __name__ == '__main__': unittest.main() monkeysign-1.1/tests/323F39BD.asc0000644000000000000000000000201512222377511013255 0ustar -----BEGIN PGP PUBLIC KEY BLOCK----- Version: GnuPG v1.4.12 (GNU/Linux) mI0EUBF4BwEEALRtP9JHUx1MA2dpVLXM4Kd9ZxEfxKTxLWZIwTV+z1gy8zquOI3G jbIqtDnVMl0H/7c1kjXHvssVvGkUJx2Gf7cCH0Hw5FdpZ9jLXiBwutpYXBCj6PnB j/tSfvkKGb7/JAmYKusum+H4Z4SyGdeBUtKkY5MNfQ78t2+m5W8NJOkpABEBAAG0 Lk1vbmtleXNwaGVyZSBzZWNvbmQgdGVzdCBrZXkgPGJhckBleGFtcGxlLmNvbT6I twQTAQgAIQUCUBF4BwIbAwULCQgHAwUVCgkICwUWAgMBAAIeAQIXgAAKCRCjHnXk Mj85vXrDBACAB2rdfISiaYlZtzK22WQ94eviI/6Jt1AY5bCKRVjSNfqDeTN48Ngy I+oyE14vmMAjFiwZjYQSnjT3Qlp3yb74prbtK+Xn5zmAg6F6dq7H9XigtPwvmW5Q fjKRryyAAggcvJX2z+cWKA+HSxRihNpA6yTdh05v4eR6sCbmLr3FuLiNBFAReAcB BADOzLogkXksQ9eoHWWiBNMCqFlFXeNm1q02fZBZOnof4/p+cGYbBd4ErfqH6bxj uL1BfGaWii7nWU+Z0cMdPmZx9yZoFOJy4LUuQuzYBblhF/KK+jrYOyn1PaamLg/1 w2RL8bT+gwe9OYs+qNNLJyvJ9REU71IY5XHTVoGqmoYEnwARAQABiJ8EGAEIAAkF AlAReAcCGwwACgkQox515DI/Ob23IAP+JnTHI3wvqJBof/bHVsvfDJuyP4AEQHL/ UvqnCE8Zj4/oQyMmbPlk/21DZ4RYtx8VPKsggdveqME0caF4VBg0zUMI3IZImSnT r+9mphFJmpTE+eE5iK+Wt91Y2F5Cm/0OQmazSQEB+etTa8y2psjp4cxkUU8Bc4Im nwqiXjT9jhI= =dZeU -----END PGP PUBLIC KEY BLOCK----- monkeysign-1.1/tests/__init__.py0000644000000000000000000000000012222377511013667 0ustar monkeysign-1.1/tests/test_ui.py0000755000000000000000000002634412222377511013632 0ustar #!/usr/bin/python # -*- coding: utf-8 -*- # # Copyright (C) 2012-2013 Antoine Beaupré # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """ Test suite for the basic user interface class. """ import unittest import os import sys import re sys.path.append(os.path.dirname(__file__) + '/..') from monkeysign.ui import MonkeysignUi, EmailFactory from monkeysign.gpg import TempKeyring from test_lib import TestTimeLimit class CliBaseTest(unittest.TestCase): def setUp(self): self.argv = sys.argv sys.argv = [ 'monkeysign', '--no-mail' ] def write_to_callback(self, stdin, callback): r, w = os.pipe() pid = os.fork() if pid: # parent os.close(w) os.dup2(r, 0) # make stdin read from the child oldstdout = sys.stdout sys.stdout = open('/dev/null', 'w') # silence output callback(self) sys.stdout = oldstdout else: # child os.close(r) w = os.fdopen(w, 'w') w.write(stdin) # say whatever is needed to msign-cli w.flush() os._exit(0) def tearDown(self): sys.argv = self.argv class CliTestCase(CliBaseTest): def test_call_usage(self): with self.assertRaises(SystemExit): execfile(os.path.dirname(__file__) + '/../scripts/monkeysign') class CliTestDialog(CliBaseTest): def setUp(self): CliBaseTest.setUp(self) self.gpg = TempKeyring() os.environ['GNUPGHOME'] = self.gpg.homedir self.assertTrue(self.gpg.import_data(open(os.path.dirname(__file__) + '/7B75921E.asc').read())) self.assertTrue(self.gpg.import_data(open(os.path.dirname(__file__) + '/96F47C6A.asc').read())) self.assertTrue(self.gpg.import_data(open(os.path.dirname(__file__) + '/96F47C6A-secret.asc').read())) sys.argv += [ '-u', '96F47C6A', '7B75921E' ] def test_sign_fake_keyring(self): """test if we can sign a key on a fake keyring""" def callback(self): execfile(os.path.dirname(__file__) + '/../scripts/monkeysign') self.write_to_callback("y\ny\n", callback) # just say yes def test_sign_one_uid(self): """test if we can sign only one keyid""" def callback(self): execfile(os.path.dirname(__file__) + '/../scripts/monkeysign') self.write_to_callback("n\n1\ny\n", callback) # just say yes def test_two_empty_responses(self): """test what happens when we answer nothing twice this tests for bug #716675""" def callback(self): with self.assertRaises(EOFError): execfile(os.path.dirname(__file__) + '/../scripts/monkeysign') self.write_to_callback("\n\n", callback) # say 'default' twice class CliTestSpacedFingerprint(CliTestDialog): def setUp(self): CliTestDialog.setUp(self) sys.argv.pop() # remove the uid from parent class sys.argv += '8DC9 01CE 6414 6C04 8AD5 0FBB 7921 5252 7B75 921E'.split() class BaseTestCase(unittest.TestCase): pattern = None args = [] def setUp(self): self.args = [ '--no-mail' ] + self.args + [ x for x in sys.argv[1:] if x.startswith('-') ] if self.pattern is not None: self.args += [ self.pattern ] self.ui = MonkeysignUi(self.args) self.ui.keyring = TempKeyring() self.ui.prepare() # needed because we changed the base keyring class BasicTests(BaseTestCase): pattern = '7B75921E' def setUp(self): BaseTestCase.setUp(self) self.homedir = self.ui.tmpkeyring.homedir def test_cleanup(self): del self.ui self.assertFalse(os.path.exists(self.homedir)) class SigningTests(BaseTestCase): pattern = '7B75921E' def setUp(self): """setup a basic keyring capable of signing a local key""" BaseTestCase.setUp(self) self.assertTrue(self.ui.keyring.import_data(open(os.path.dirname(__file__) + '/7B75921E.asc').read())) self.assertTrue(self.ui.tmpkeyring.import_data(open(os.path.dirname(__file__) + '/96F47C6A.asc').read())) self.assertTrue(self.ui.keyring.import_data(open(os.path.dirname(__file__) + '/96F47C6A.asc').read())) self.assertTrue(self.ui.keyring.import_data(open(os.path.dirname(__file__) + '/96F47C6A-secret.asc').read())) def test_find_key(self): """test if we can extract the key locally this duplicates tests from the gpg code, but is necessary to test later functions""" self.ui.find_key() def test_copy_secrets(self): """test if we can copy secrets between the two keyrings this duplicates tests from the gpg code, but is necessary to test later functions""" self.test_find_key() self.ui.copy_secrets() self.assertTrue(self.ui.keyring.get_keys(None, True, False)) self.assertGreaterEqual(len(self.ui.keyring.get_keys(None, True, False)), 1) self.assertGreaterEqual(len(self.ui.keyring.get_keys(None, True, True)), 1) def test_sign_key(self): """test if we can sign the keys non-interactively""" self.test_copy_secrets() self.ui.sign_key() self.assertGreaterEqual(len(self.ui.signed_keys), 1) def test_create_mail_multiple(self): """test if exported keys contain the right uid""" self.test_sign_key() for fpr, key in self.ui.signed_keys.items(): oldmsg = None for uid in key.uids.values(): msg = EmailFactory(self.ui.tmpkeyring.export_data(fpr), fpr, uid.uid, 'unittests@localhost', 'devnull@localhost') if oldmsg is not None: self.assertNotEqual(oldmsg.as_string(), msg.as_string()) self.assertNotEqual(oldmsg.create_mail_from_block(oldmsg.tmpkeyring.export_data(fpr)).as_string(), msg.create_mail_from_block(oldmsg.tmpkeyring.export_data(fpr)).as_string()) self.assertNotEqual(oldmsg.tmpkeyring.export_data(fpr), msg.tmpkeyring.export_data(fpr)) oldmsg = msg self.assertIsNot(oldmsg, None) class EmailFactoryTest(BaseTestCase): pattern = '7B75921E' def setUp(self): """setup a basic keyring capable of signing a local key""" BaseTestCase.setUp(self) self.assertTrue(self.ui.tmpkeyring.import_data(open(os.path.dirname(__file__) + '/7B75921E.asc').read())) self.assertTrue(self.ui.tmpkeyring.import_data(open(os.path.dirname(__file__) + '/96F47C6A.asc').read())) self.assertTrue(self.ui.tmpkeyring.import_data(open(os.path.dirname(__file__) + '/96F47C6A-secret.asc').read())) self.email = EmailFactory(self.ui.tmpkeyring.export_data(self.pattern), self.pattern, 'Antoine Beaupré ', 'nobody@example.com', 'nobody@example.com') def test_cleanup_uids(self): """test if we can properly remove irrelevant UIDs""" for fpr, key in self.email.tmpkeyring.get_keys('7B75921E').iteritems(): for u, uid in key.uids.iteritems(): self.assertEqual(self.email.recipient, uid.uid) def test_mail_key(self): """test if we can generate a mail with a key inside""" data = self.email.tmpkeyring.export_data(self.pattern) self.assertNotEqual(data, '') message = self.email.create_mail_from_block(data) match = re.compile("""Content-Type: multipart/mixed; boundary="===============[0-9]*==" MIME-Version: 1.0 --===============[0-9]*== Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable %s --===============[0-9]*== Content-Type: application/pgp-keys; name="yourkey.asc" MIME-Version: 1.0 Content-Disposition: attachment; filename="yourkey.asc" Content-Transfer-Encoding: 7bit Content-Description: PGP Key , uid \( -----BEGIN PGP PUBLIC KEY BLOCK----- .* -----END PGP PUBLIC KEY BLOCK----- --===============[0-9]*==--""" % (self.email.body), re.DOTALL) self.assertRegexpMatches(message.as_string(), match) return message def test_wrap_crypted_mail(self): match = re.compile("""Content-Type: multipart/encrypted; protocol="application/pgp-encrypted"; boundary="===============%s==" MIME-Version: 1.0 Subject: .* From: nobody@example.com To: nobody@example.com This is a multi-part message in PGP/MIME format... --===============%s== Content-Type: application/pgp-encrypted; filename="signedkey.msg" MIME-Version: 1.0 Content-Disposition: attachment; filename="signedkey.msg" Version: 1 --===============%s== Content-Type: application/octet-stream; filename="msg.asc" MIME-Version: 1.0 Content-Disposition: inline; filename="msg.asc" Content-Transfer-Encoding: 7bit -----BEGIN PGP MESSAGE----- .* -----END PGP MESSAGE----- --===============%s==--""" % tuple(['[0-9]*'] * 4), re.DOTALL) self.assertRegexpMatches(self.email.as_string(), match) def test_weird_from(self): """make sure we don't end up with spaces in our email address""" self.email = EmailFactory(self.ui.tmpkeyring.export_data(self.pattern), self.pattern, 'Antoine Beaupré ', 'Antoine Beaupré (home address) ', 'nobody@example.com') match = re.compile("""From: (([^ ]* )|("[^"]*" ))?<[^> ]*>$""", re.DOTALL | re.MULTILINE) self.assertRegexpMatches(self.email.as_string(), match) class KeyserverTests(BaseTestCase): args = [ '--keyserver', 'pool.sks-keyservers.net' ] pattern = '7B75921E' def test_find_key(self): """this should find the key on the keyservers""" self.ui.find_key() class FakeKeyringTests(BaseTestCase): args = [] pattern = '96F47C6A' def setUp(self): """we setup a fake keyring with the public key to sign and add our private keys""" BaseTestCase.setUp(self) self.ui.keyring.import_data(open(os.path.dirname(__file__) + '/96F47C6A.asc').read()) def test_find_key(self): """test if we can find a key on the local keyring""" self.ui.find_key() class NonExistentKeyTests(BaseTestCase, TestTimeLimit): """test behavior with a key that can't be found""" args = [] # odds that a key with all zeros as fpr are low, unless something happens between PGP and bitcoins... pattern = '0000000000000000000000000000000000000000' def test_find_key(self): """find_key() should exit if the key can't be found on keyservers or local keyring""" try: with self.assertRaises(SystemExit): self.ui.find_key() except AlarmException: raise unittest.case._ExpectedFailure(sys.exc_info()) if __name__ == '__main__': unittest.main() monkeysign-1.1/tests/test_gpg.py0000755000000000000000000003417312222377511013771 0ustar #!/usr/bin/python # -*- coding: utf-8 -*- # # Copyright (C) 2012-2013 Antoine Beaupré # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """General test suite for the GPG API. Tests that require network access should go in test_network.py. """ import sys, os, shutil from StringIO import StringIO import unittest import tempfile sys.path.append(os.path.dirname(__file__) + '/..') from monkeysign.gpg import * class TestContext(unittest.TestCase): """Tests for the Context class. Those should be limited to talking to the GPG binary, not operating on actual keyrings or GPG data.""" # those need to match the options in the Gpg class options = Context.options # ... and this is the rendered version of the above rendered_options = ['gpg', '--command-fd', '0', '--with-fingerprint', '--list-options', 'show-sig-subpackets,show-uid-validity,show-unusable-uids,show-unusable-subkeys,show-keyring,show-sig-expire', '--batch', '--fixed-list-mode', '--no-tty', '--with-colons', '--use-agent', '--status-fd', '2', '--quiet' ] def setUp(self): self.gpg = Context() def test_plain(self): """make sure other instances do not poison us""" d = Context() d.set_option('homedir', '/var/nonexistent') self.assertNotIn('homedir', self.gpg.options) def test_set_option(self): """make sure setting options works""" self.gpg.set_option('armor') self.assertIn('armor', self.gpg.options) self.gpg.set_option('keyserver', 'foo.example.com') self.assertDictContainsSubset({'keyserver': 'foo.example.com'}, self.gpg.options) def test_command(self): """test various command creation if this fails, it's probably because you added default options to the tested class without adding them in the test class """ c = self.rendered_options + ['--version'] c2 = self.gpg.build_command(['version']) self.assertItemsEqual(c, c2) c = self.rendered_options + ['--export', 'foo'] c2 = self.gpg.build_command(['export', 'foo']) self.assertItemsEqual(c, c2) def test_version(self): """make sure version() returns something""" self.assertTrue(self.gpg.version()) def test_seek_debug(self): """test if seek actually respects debug""" self.gpg.debug = True # should yield an attribute error, that's fine with self.assertRaises(AttributeError): self.gpg.seek(StringIO('test'), 'test') # now within a keyring? k = TempKeyring() k.context.debug = True with self.assertRaises(AttributeError): k.import_data(open(os.path.dirname(__file__) + '/96F47C6A.asc').read()) class TestTempKeyring(unittest.TestCase): """Test the TempKeyring class.""" def setUp(self): self.gpg = TempKeyring() self.assertIn('homedir', self.gpg.context.options) def tearDown(self): del self.gpg class TestKeyringBase(unittest.TestCase): """Base class for Keyring class tests. This shouldn't implement any tests that we don't want to see implemented every time. """ def setUp(self): """setup the test environment we test using a temporary keyring because it's too dangerous otherwise. we are not using the TempKeyring class however, because we may want to keep that data for examination later. see the tearDown() function for that. """ self.tmp = tempfile.mkdtemp(prefix="pygpg-") self.gpg = Keyring(self.tmp) self.assertEqual(self.gpg.context.options['homedir'], self.tmp) # to avoid rebuilding the trust base after uid changes and so on self.gpg.context.set_option('always-trust') def tearDown(self): """trash the temporary directory we created""" shutil.rmtree(self.tmp) class TestKeyringBasics(TestKeyringBase): """Test the Keyring class base functionality.""" def test_home(self): """test if the homedir is properly set and populated""" self.gpg.export_data('') # dummy call to make gpg populate his directory self.assertTrue(open(self.tmp + '/pubring.gpg')) def test_import(self): """make sure import_data returns true on known good data it should throw an exception if there's something wrong with the backend too """ self.assertTrue(self.gpg.import_data(open(os.path.dirname(__file__) + '/96F47C6A.asc').read())) def test_import_fail(self): """test that import_data() throws an error on wrong data""" self.assertFalse(self.gpg.import_data('')) def test_export(self): """test that we can export data similar to what we import @todo this will probably fail if tests are ran on a different GPG version """ self.assertTrue(self.gpg.import_data(open(os.path.dirname(__file__) + '/96F47C6A.asc').read())) k1 = open(os.path.dirname(__file__) + '/96F47C6A.asc').read() self.gpg.context.set_option('armor') self.gpg.context.set_option('export-options', 'export-minimal') k2 = self.gpg.export_data('96F47C6A') self.assertEqual(k1,k2) def test_get_missing_secret_keys(self): """make sure we fail to get secret keys when they are missing""" self.assertTrue(self.gpg.import_data(open(os.path.dirname(__file__) + '/7B75921E.asc').read())) # this shouldn't show anything, as this is just a public key blob self.assertFalse(self.gpg.get_keys('8DC901CE64146C048AD50FBB792152527B75921E', True, False)) def test_export_secret(self): """make sure we can import and export secret data""" self.assertTrue(self.gpg.import_data(open(os.path.dirname(__file__) + '/96F47C6A-secret.asc').read())) self.secret = self.gpg.export_data('96F47C6A', True) self.assertTrue(self.secret) def test_list_imported_secrets(self): """make sure we can print imported secrets""" self.assertTrue(self.gpg.import_data(open(os.path.dirname(__file__) + '/96F47C6A-secret.asc').read())) self.assertTrue(self.gpg.get_keys(None, True, False)) def test_empty_keyring(self): """a test should work on an empty keyring this is also a test of exporting an empty keyring""" self.assertEqual(self.gpg.export_data(), '') def test_sign_key_missing_key(self): """try to sign a missing key this should fail because we don't have the public key material for the requested key however, gpg returns the wrong exit code here, so we end up at looking if there is really no output """ self.assertTrue(self.gpg.import_data(open(os.path.dirname(__file__) + '/96F47C6A-secret.asc').read())) with self.assertRaises(GpgRuntimeError): self.gpg.sign_key('7B75921E') def test_failed_revoke(self): self.gpg.import_data(open(os.path.dirname(__file__) + '/96F47C6A-revoke.asc').read()) with self.assertRaises(GpgRuntimeError): self.gpg.sign_key('7B75921E', True) class TestKeyringWithKeys(TestKeyringBase): def setUp(self): TestKeyringBase.setUp(self) self.assertTrue(self.gpg.import_data(open(os.path.dirname(__file__) + '/7B75921E.asc').read())) self.assertTrue(self.gpg.import_data(open(os.path.dirname(__file__) + '/96F47C6A.asc').read())) self.assertTrue(self.gpg.import_data(open(os.path.dirname(__file__) + '/96F47C6A-secret.asc').read())) def test_get_keys(self): """test that we can list the keys after importing them @todo we should check the data structure """ # just a cute display for now for fpr, key in self.gpg.get_keys('96F47C6A').iteritems(): print key def test_sign_key_wrong_user(self): """make sure sign_key with a erroneous local-user fails that is, even if all other conditions are ok""" self.gpg.context.set_option('local-user', '0000000F') with self.assertRaises(GpgRuntimeError): self.gpg.sign_key('7B75921E', True) def test_sign_key_all_uids(self): """test signature of all uids of a key""" self.assertTrue(self.gpg.sign_key('7B75921E', True)) self.gpg.context.call_command(['list-sigs', '7B75921E']) self.assertRegexpMatches(self.gpg.context.stdout, 'sig:::1:86E4E70A96F47C6A:[^:]*::::Second Test Key :10x:') def test_sign_key_single_uid(self): """test signing a key with a single uid""" self.assertTrue(self.gpg.import_data(open(os.path.dirname(__file__) + '/323F39BD.asc').read())) self.assertTrue(self.gpg.sign_key('323F39BD', True)) self.gpg.context.call_command(['list-sigs', '323F39BD']) self.assertRegexpMatches(self.gpg.context.stdout, 'sig:::1:A31E75E4323F39BD:[^:]*::::Monkeysphere second test key :[0-9]*x:') def test_sign_key_one_uid(self): """test signature of a single uid""" self.assertTrue(self.gpg.sign_key('Antoine Beaupré ')) self.gpg.context.call_command(['list-sigs', '7B75921E']) self.assertRegexpMatches(self.gpg.context.stdout, 'sig:::1:86E4E70A96F47C6A:[^:]*::::Second Test Key :10x:') def test_sign_key_as_user(self): """normal signature with a signing user specified""" self.gpg.context.set_option('local-user', '96F47C6A') self.assertTrue(self.gpg.sign_key('7B75921E', True)) @unittest.expectedFailure def test_sign_already_signed(self): """test if signing a already signed key fails with a meaningful message""" raise NotImplementedError('not detecting already signed keys properly yet') def test_encrypt_decrypt_data_armored_untrusted(self): """test if we can encrypt data to our private key (and decrypt it)""" plaintext = 'i come in peace' self.gpg.context.set_option('always-trust') # evil? self.gpg.context.set_option('armor') cyphertext = self.gpg.encrypt_data(plaintext, '96F47C6A') self.assertTrue(cyphertext) p = self.gpg.decrypt_data(cyphertext) self.assertTrue(p) self.assertEqual(p, plaintext) def test_gen_key(self): """test key generation not implemented""" #self.fpr = self.gpg.gen_key() #self.assertTrue(self.fpr) pass def test_multi_secrets(self): """test if we get confused with multiple secret keys""" self.assertTrue(self.gpg.import_data(open(os.path.dirname(__file__) + '/323F39BD.asc').read())) self.assertTrue(self.gpg.import_data(open(os.path.dirname(__file__) + '/323F39BD-secret.asc').read())) keys = self.gpg.get_keys(None, True, False) self.assertEqual(len(keys.keys()), 2) #for fpr, key in keys.iteritems(): # print >>sys.stderr, "key:", key def test_del_uid(self): """test uid deletion, gpg.del_uid()""" userid = 'Antoine Beaupré ' self.assertTrue(self.gpg.import_data(open(os.path.dirname(__file__) + '/7B75921E.asc').read())) found = False keys = self.gpg.get_keys('7B75921E') for fpr, key in keys.iteritems(): for u, uid in key.uids.iteritems(): self.assertIsInstance(uid, OpenPGPuid) if userid == uid.uid: found = True break self.assertTrue(found, "that we can find the userid before removing it") self.assertTrue(self.gpg.del_uid(fpr, userid)) for fpr, key in self.gpg.get_keys('7B75921E').iteritems(): for u, uid in key.uids.iteritems(): self.assertNotEqual(userid, uid.uid) def test_del_uid_except(self): """see if we can easily delete all uids except a certain one""" self.assertTrue(self.gpg.import_data(open(os.path.dirname(__file__) + '/7B75921E.asc').read())) userid = 'Antoine Beaupré ' keys = self.gpg.get_keys('7B75921E') todelete = [] for fpr, key in keys.iteritems(): for u, uid in key.uids.iteritems(): if userid != uid.uid: todelete.append(uid.uid) for uid in todelete: self.gpg.del_uid(fpr, uid) for fpr, key in self.gpg.get_keys('7B75921E').iteritems(): for u, uid in key.uids.iteritems(): self.assertEqual(userid, uid.uid) class TestOpenPGPkey(unittest.TestCase): def setUp(self): self.key = OpenPGPkey("""tru::1:1343350431:0:3:1:5 pub:-:1024:1:86E4E70A96F47C6A:1342795252:::-:::scESC: fpr:::::::::3F94240C918E63590B04152E86E4E70A96F47C6A: uid:-::::1342795252::214CB0EDA28F3CA8754A4D43B7CDB7B114171B3C::Test Key : sub:-:1024:1:894EE34814B46386:1342795252::::::e:""") def test_no_dupe_uids(self): key = OpenPGPkey() self.assertEqual(key.uids, {}) def test_format_fpr(self): expected = '3F94 240C 918E 6359 0B04 152E 86E4 E70A 96F4 7C6A' actual = self.key.format_fpr() self.assertEqual(expected, actual) def test_get_trust(self): self.assertEqual('unknown', self.key.get_trust()) class TestSecretOpenPGPkey(unittest.TestCase): def setUp(self): self.key = OpenPGPkey("""sec::1024:17:586073B34023702F:1110320887:1268438180::::::::: fpr:::::::::C9E1F1230DBE47D57BAB3C60586073B34023702F: uid:::::::2451063FCBB4D262938687C2D8F6B949B0A3AF01::The Anarcat : ssb::2048:16:C016FF12EB8D47BB:1110320966::::::::::""") def test_print(self): print self.key if __name__ == '__main__': unittest.main() monkeysign-1.1/tests/test_lib.py0000755000000000000000000000300712222377511013752 0ustar #!/usr/bin/python # -*- coding: utf-8 -*- # # Copyright (C) 2012-2013 Antoine Beaupré # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """ Library of test tools that can be reused across different test suites. """ import signal import time import unittest class AlarmException(IOError): pass def handle_alarm(signum, frame): raise AlarmException('timeout in %s' % frame.f_code.co_name) class TestTimeLimit(unittest.TestCase): """a test with a timeout""" # in seconds timeout = 3 def setUp(self): signal.signal(signal.SIGALRM, handle_alarm) signal.setitimer(signal.ITIMER_REAL, self.timeout) def tearDown(self): signal.alarm(0) class TestTimeLimitSelfTest(TestTimeLimit): # 10ms timeout = 0.01 def test_signal(self): with self.assertRaises(AlarmException): time.sleep(1) if __name__ == '__main__': unittest.main() monkeysign-1.1/tests/96F47C6A-secret.asc0000644000000000000000000000426112222377511014557 0ustar -----BEGIN PGP PRIVATE KEY BLOCK----- Version: GnuPG v1.4.12 (GNU/Linux) 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 8i1hbkJE6N1DoOD/CzpVCSYRXiO+ =QUsR -----END PGP PRIVATE KEY BLOCK----- monkeysign-1.1/debian/0000755000000000000000000000000012222377511011650 5ustar monkeysign-1.1/debian/menu0000644000000000000000000000017412222377511012541 0ustar ?package(monkeysign):needs="X11" section="Applications/System/Security"\ title="monkeysign" command="/usr/bin/monkeyscan" monkeysign-1.1/debian/compat0000644000000000000000000000000212222377511013046 0ustar 9 monkeysign-1.1/debian/control0000644000000000000000000000260712222377511013260 0ustar Source: monkeysign Section: utils Priority: optional Maintainer: Antoine Beaupré Build-Depends: debhelper (>= 9), python (>= 2.6), python-setuptools, python-qrencode, python-gtk2, python-zbar, python-zbarpygtk, python-docutils Standards-Version: 3.9.4 Homepage: http://web.monkeysphere.info/monkeysign Vcs-Git: git://git.monkeysphere.info/monkeysign Package: monkeysign Architecture: all Depends: ${shlibs:Depends}, ${misc:Depends}, ${python:Depends}, python-pkg-resources, gnupg Recommends: python-qrencode, python-gtk2, python-zbar, python-zbarpygtk Description: OpenPGP key signing and exchange for humans monkeysign is a tool to overhaul the OpenPGP keysigning experience and bring it closer to something that most primates can understand. . The project makes use of cheap digital cameras and the type of bar code known as a QRcode to provide a human-friendly yet still-secure keysigning experience. . No more reciting tedious strings of hexadecimal characters. And, you can build a little rogue's gallery of the people that you have met and exchanged keys with! . Monkeysign is the commandline signing software, a caff replacement. Monkeyscan is the graphical user interface that scans qrcodes. monkeysign-1.1/debian/changelog0000644000000000000000000001311212222377511013520 0ustar monkeysign (1.1) unstable; urgency=low [Antoine Beaupré] * improved SMTP support: * SMTP username and passwords can be passed as commandline arguments * SMTP password is prompted if not specified * use STARTTLS if available * enable SMTP debugging only debugging is enabled * show the unencrypted email with --no-mail (Closes: #720049) * warn when gpg-agent is not running or failing (Closes: #723052) * set GPG_TTY if it is missing (Closes: #719908) * bail out on already signed keys (Closes: #720055) * mention monkeyscan in the package description so it can be found more easily * fix python-pkg-resources dependency * don't show backtrace on control-c * add missing files to .gitignore (Closes: #724007) * ship with a neat little slideshow to make presentations [Philip Jägenstedt] * fix some typos (Closes: #722964) * add --cert-level option (Closes: #722740) -- Antoine Beaupré Tue, 01 Oct 2013 00:22:30 +0200 monkeysign (1.0) unstable; urgency=low * stop copying secrets to the temporary keyring * make sure we use the right signing key when specified * signatures on multiple UIDs now get properly sent separately (Closes: #719241) * this includes "deluid" support on the gpg library * significantly refactor email creation * improve unit tests on commandline scripts, invalid (revoked) keys and timeout handling * provide manpages (Closes: #716674) * avoid showing binary garbage on export when debugging * properly fail if password confirmation fails * user interfaces now translatable * accept space-separated key fingerprints * fix single UID key signing * proper formatting of UIDs with comments (removed) and spaces (wrapped) for emails -- Antoine Beaupré Wed, 14 Aug 2013 20:51:44 -0400 monkeysign (0.9) unstable; urgency=low * refactor unit tests again to optimise UI tests and test mail generation * fix error handling in encryption/decryption (Closes: #717622) * rename msign-cli to monkeysign and msign to monkeyscan (Closes: #717623) * handle interruptions cleanly when choosing user IDs (see: #716675) -- Antoine Beaupré Tue, 23 Jul 2013 10:56:50 -0400 monkeysign (0.8) unstable; urgency=low * refactor unit test suite to allow testing the commandline tool interactively * don't fail on empty input when choosing uid (Closes: #716675) * we also explain how to refuse signing a key better * optimise network tests so they timeout (so fail) faster -- Antoine Beaupré Wed, 17 Jul 2013 22:52:02 -0400 monkeysign (0.7.1) unstable; urgency=low * fix binary package dependency on python * update to debhelper 9 * update to standards 3.9.4, no change -- Antoine Beaupré Sun, 07 Jul 2013 09:58:56 -0400 monkeysign (0.7) unstable; urgency=low * fix crash when key not found on keyservers * use a proper message in outgoing emails * unit tests extended to cover user interface * import keys from the local keyring before looking at the keyserver * fix print/save exports (thanks Simon!) * don't depend on a graphical interface * update copyright dates and notices * mark as priority: optional instead of extra -- Antoine Beaupré Sat, 06 Jul 2013 01:07:28 -0400 monkeysign (0.6) unstable; urgency=low * fix warnings in the graphical interface * make qr-code detection be case-insensitive * fix syntax error * follow executable renames properly -- Antoine Beaupré Sat, 06 Oct 2012 16:08:48 +0200 monkeysign (0.5) unstable; urgency=low * non-exportable signatures (--local) support * simplify the monkeysign-scan UI * rename monkeysign-scan to msign and monkeysign-cli to msign-cli to avoid tab-completion conflict with monkeysphere executables, at the request of Monkeysphere developers * usability: make sure arguments are case-insensitive * fix email format so it's actually readable -- Antoine Beaupré Fri, 05 Oct 2012 11:14:37 +0200 monkeysign (0.4) unstable; urgency=low * merge display and scanning of qrcodes * really remove remaining pyme dependency * list key indexes to allow choosing more clearly * copy the gpg.conf in temporary keyring * fix keyserver operation in GUI * implement UID choosing in GUI -- Antoine Beaupré Wed, 01 Aug 2012 02:33:29 -0400 monkeysign (0.3) unstable; urgency=low * allow keyserver to be enabled while not specified * do not set an empty keyserver, fixing weird keyserver errors on -scan * fix window reference in UI, spotted by dkg * mark this as architecture-independent, spotted by dkg * make setup executable * reference new homepage * API change: functions return false instead of raising exceptions * fix multiple keys listing support -- Antoine Beaupré Thu, 26 Jul 2012 12:41:54 -0400 monkeysign (0.2) unstable; urgency=low * only load information from private keys when doing key detection * add debugging in key choosing algorithm * import private keyring even in dry-run * properly import re, fixing a crash * add usage for monkeysign-scan * fixup modules list so that the package actually works * make this not crash completely if there's no video * improve short description so that it matches 'key signing' * fix dependencies * fix typo, noticed by micah -- Antoine Beaupré Sun, 22 Jul 2012 13:38:00 -0400 monkeysign (0.1) unstable; urgency=low * Initial Release. -- Antoine Beaupré Sat, 21 Jul 2012 12:05:59 -0400 monkeysign-1.1/debian/docs0000644000000000000000000000001412222377511012516 0ustar README TODO monkeysign-1.1/debian/rules0000755000000000000000000000031512222377511012727 0ustar #!/usr/bin/make -f # -*- makefile -*- # Uncomment this to turn on verbose mode. #export DH_VERBOSE=1 # This has to be exported to make some magic below work. export DH_OPTIONS %: dh $@ --with=python2 monkeysign-1.1/debian/doc-base0000644000000000000000000000055412222377511013254 0ustar Document: monkeysign Title: Debian monkeysign presentation Author: Antoine Beaupré Abstract: This presentation is a quick set of slides to present monkeysign Section: System/Security Format: text Files: /usr/share/doc/monkeysign/presentation.rst Format: HTML Index: /usr/share/doc/monkeysign/presentation.html Files: /usr/share/doc/monkeysign/presentation.html monkeysign-1.1/debian/copyright0000644000000000000000000000207212222377511013604 0ustar Format: http://dep.debian.net/deps/dep5 Upstream-Name: monkeysign Source: git://git.monkeysphere.info/monkeysign Files: * Copyright: 2010 Jerome Charaoui 2010-2013 Antoine Beaupré License: GPL-3.0+ Files: debian/* Copyright: 2013 Antoine Beaupré License: GPL-3.0+ License: GPL-3.0+ This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. . This package is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. . You should have received a copy of the GNU General Public License along with this program. If not, see . . On Debian systems, the complete text of the GNU General Public License version 3 can be found in "/usr/share/common-licenses/GPL-3". monkeysign-1.1/debian/source/0000755000000000000000000000000012222377511013150 5ustar monkeysign-1.1/debian/source/format0000644000000000000000000000001512222377511014357 0ustar 3.0 (native)