[pius-commit] pius pius-keyring-mgr, NONE, 1.1 pius-party-worksheet, NONE, 1.1
Brought to you by:
jaymzh
|
From: Phil D. <ja...@us...> - 2011-03-13 02:23:01
|
Update of /cvsroot/pgpius/pius
In directory vz-cvs-2.sog:/tmp/cvs-serv15957
Added Files:
pius-keyring-mgr pius-party-worksheet
Log Message:
Add two new utilities!
pius-keyring-mgr
A utility for building and managing party keyrings.
pius-party-worksheet
A utility to generate party worksheets.
Signed-off-by: Phil Dibowitz <ph...@ip...>
--- NEW FILE: pius-keyring-mgr ---
#!/usr/bin/python
'''A utility to create and manage party keyrings.'''
# vim:tw=80:ai:tabstop=2:expandtab:shiftwidth=2
#
# Copyright (c) 2011 - present Phil Dibowitz (ph...@ip...)
#
# 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, version 2.
#
import email
import mailbox
import optparse
import os
import re
import shutil
import smtplib
import subprocess
import sys
DEBUG_ON = False
BADKEYS_RE = re.compile('00000000|12345678|no pgp key')
DEFAULT_KEYSERVERS = ['pool.sks-keyservers.net', 'pgp.mit.edu', 'keys.gnupg.net']
VERSION = '2.0.9'
HOME = os.environ.get('HOME')
GNUPGHOME = os.environ.get('GNUPGHOME', os.path.join(HOME, '.gnupg'))
DEFAULT_GPG_PATH = '/usr/bin/gpg'
DEFAULT_CSV_DELIMITER=','
DEFAULT_CSV_NAME_FIELD=2
DEFAULT_CSV_EMAIL_FIELD=3
DEFAULT_CSV_FP_FIELD=4
DEFAULT_TMP_DIR='/tmp/pius_keyring_mgr_tmp'
DEFAULT_EMAIL_TEXT = '''Dear %(name)s,
You signed up for the %(party)sPGP Keysigning Party with the following key:
%(fp)s
However, I have not been able to find your key. I have tried the following
keyservers:
%(keyservers)s
Please upload your key to one of the the aforemention keyservers. You can do
this simply with:
gpg --keyserver %(keyserver)s --send-key KEYID
Where 'KEYID' is the keyid of your PGP key. For more information see this site:
http://www.phildev.net/pgp/
Your key will be searched for again in 24-48 hours and if your key is not there,
you will receive another email.
You do not need to contact me if you upload your key. If you have questions you
may email %(from)s.
Generated by PIUS Keyring Manager (http://www.phildev.net/pius/).
'''
def debug(line):
'''Print a debug message if debugging is on.'''
if DEBUG_ON:
print 'DEBUG:', line
def print_default_email():
'''Print the default email that is sent out.'''
interpolation_dict = {'name': '<name>', 'email': '<email>',
'from': '<your_email>',
'party': '<party_name> ',
'keyserver': '<example_keyserver>',
'keyservers': '<keyserver_list>',
'fp': '<fingerprint>'}
print 'DEFAULT EMAIL TEXT:\n'
print DEFAULT_EMAIL_TEXT % interpolation_dict
def keyid_from_fp(fp):
'''Given a fingerprint without whitespace, returns keyid.'''
return fp[32:40]
def parse_csv(filename, sep, name_field, email_field, fp_field):
'''Parse a CSV for name, email, fingerprint, and generate keyid.'''
fp_field = fp_field - 1
name_field = name_field - 1
email_field = email_field - 1
report = open(filename, 'r')
keys = []
for line in report:
parts = line.split(sep)
if BADKEYS_RE.search(parts[fp_field]):
continue
fp = parts[fp_field].replace(' ', '')
keyid = keyid_from_fp(fp)
keys.append({'name': parts[name_field],
'email': parts[email_field],
'keyid': keyid,
'fingerprint': fp})
return keys
def parse_mbox(filename):
'''Parse an mbox for name, email, fingerprints and keys.
Note that in the even of a fingerprint, keyid is generated, otherwise
just the ASCII-armored key is stored.'''
box = mailbox.mbox(filename)
key_re = re.compile(r'(-----BEGIN PGP PUBLIC KEY BLOCK-----\n.*-----END PGP'
' PUBLIC KEY BLOCK-----)', re.DOTALL);
fp_re = re.compile(r'((?:[\dA-Fa-f]{4}[ \t\n]*){10})')
uid_re = re.compile(r'(.*) <(.*)>$')
fixname1_re = re.compile(r'^[\'"]')
fixname2_re = re.compile(r'[\'"]$')
# make sure the re here is the same used for space in fp_re above
fixrp_re = re.compile(r'[ \t\n]+')
keys = []
num_fps = num_keys = 0
for msg in box:
m = email.parser.Parser()
p = m.parsestr(msg.as_string())
uid = p.get('From')
name = None
mail = None
match = uid_re.search(uid)
if match:
name = match.group(1)
mail = match.group(2)
name = fixname1_re.sub('', name)
name = fixname2_re.sub('', name)
for part in msg.walk():
# if decoded returns None, we're in multipart messages, or other
# non-convertable-to-text data, so we can move on. If it's multipart
# then we'll get to the sub-parts on further iterations of walk()
decoded = part.get_payload(None, True)
if not decoded:
debug('Skipping non-decodable part')
continue
data = {'name': name, 'email': mail}
matches = key_re.findall(decoded)
if matches:
for match in matches:
num_keys = num_keys + 1
tmp = data.copy()
tmp['key'] = match
keys.append(tmp)
continue
matches = fp_re.findall(decoded)
if matches:
for match in matches:
num_fps = num_fps + 1
fp = fixrp_re.sub('', match)
keyid = keyid_from_fp(fp)
tmp = data.copy()
tmp.update({'fingerprint': fp, 'keyid': keyid})
keys.append(tmp)
fp = 'wonk'
print ("Found %s keys in mbox: %s fingerprints and %s full keys" %
(len(keys), num_fps, num_keys))
return keys
class KeyringBuilder(object):
'''A class for building and managing keyrings.'''
QUIET_OPTS = '-q --no-tty --no-auto-check-trustdb --batch'
AUTO_OPTS = '--command-fd 0 --status-fd 1 --no-options --with-colons'
def __init__(self, gpg_path, keyring, keyservers, tmp_dir):
self.gpg = gpg_path
self.keyring = keyring
self.found = []
self.notfound = []
self.keyservers = keyservers
self.tmp_dir = tmp_dir
self.basecmd = '%s --no-default-keyring --keyring %s' % (self.gpg,
self.keyring)
#
# BEGIN INTERNAL FUNCTIONS
#
def _tmpfile_path(self, tfile):
'''Internal function to take a filename and put it in self.tmp_dir.'''
return '%s/%s' % (self.tmp_dir, tfile)
def _remove_file(self, tfile):
if os.path.exists(tfile):
os.unlink(tfile)
def _printable_fingerprint(self, fp):
'''Given a whitespace-collapsed FP, print it in friendly format.'''
return ('%s %s %s %s %s %s %s %s %s %s' %
(fp[0:4], fp[4:8], fp[8:12], fp[12:16], fp[16:20], fp[20:24],
fp[24:28], fp[28:32], fp[32:36], fp[36:40]))
def _import_key_file(self, kfile):
'''Given a keyfile, import it into our keyring.'''
extra_opts = '%s %s' % (self.QUIET_OPTS, self.AUTO_OPTS)
cmd = '%s %s --import %s' % (self.basecmd, extra_opts, kfile)
debug(cmd)
gpg = subprocess.Popen(cmd, shell=True, stdin=None, close_fds=True,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
for line in gpg.stdout:
parts = line.strip().split(' ')
if parts[1] == 'IMPORTED':
print 'Importing %s (%s)' % (parts[2][8:16], ' '.join(parts[3:]))
takekey = False
gpg.wait()
retval = gpg.returncode
if retval != 0:
return False
return True
def _write_and_import_key(self, key):
'''Write out key to a file and call _import_key_file() on it.'''
# FIXME
filename = self._tmpfile_path('pius_keyring_mgr.tmp.txt')
fh = open(filename, 'w')
fh.write(key['key'])
fh.close()
self._import_key_file(filename)
self._remove_file(filename)
def _print_list(self, keylist):
'''Helper function for print_report.'''
for key in keylist:
print (' %s <%s>\n %s' %
(key['name'], key['email'],
self._printable_fingerprint(key['fingerprint'])))
def _get_email_body(self, keyinfo):
kstext = ' ' + '\n '.join(self.keyservers)
interp = {'name': keyinfo['name'],
'email': keyinfo['email'],
'from': self.fromaddr,
'fp': self._printable_fingerprint(keyinfo['fingerprint']),
'party': self.party,
'keyservers': kstext,
'keyserver': self.keyservers[0]}
hdrs = '''To: %(name)s <%(email)s>
From: %(from)s
Subject: %(party)sPGP Keysignign Party: Can't find your key!''' % interp
body = ''
if self.mail_text:
body = open(self.mail_text, 'r').read() % interp
else:
body = DEFAULT_EMAIL_TEXT % interp
return hdrs + '\n\n' + body
def _send_email(self, override_email, keyinfo):
body = self._get_email_body(keyinfo)
efrom = self.fromaddr
eto = [keyinfo['email'], self.fromaddr]
if override_email:
eto = override_email
print "Sending mail to %s" % eto
smtp = smtplib.SMTP('localhost', '587')
smtp.ehlo()
smtp.sendmail(efrom, eto, body)
smtp.quit
#
# BEGIN PUBLIC FUNCTIONS
#
def get_key(self, key):
'''Try to get key from any keyservers we know about.'''
basecmd = '%s %s --no-default-keyring --keyring %s' % (self.gpg,
self.QUIET_OPTS,
self.keyring)
found = False
for ks in self.keyservers:
cmd = '%s --keyserver %s --recv-key %s' % (basecmd, ks, key)
debug(cmd)
gpg = subprocess.Popen(cmd, shell=True, stdin=None, close_fds=True,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
gpg.wait()
retval = gpg.returncode
if retval == 0:
found = True
print 'Found %s (%s)' % (key, ks)
break
if not found:
print 'NOT Found %s' % key
return found
def get_all_keys(self, keys):
'''Wrapper to call get_key() on all keys.'''
attempted_keyids = []
for key in keys:
if 'keyid' in key:
if key['keyid'] in attempted_keyids:
debug('Skipping %s, already processed')
continue
attempted_keyids.append(key['keyid'])
if self.get_key(key['keyid']):
self.found.append(key)
else:
self.notfound.append(key)
elif 'key' in key:
self._write_and_import_key(key)
def print_report(self):
'''Print small report about what was and was not found.'''
print 'KEYS FOUND:'
self._print_list(self.found)
print 'KEYS NOT FOUND:'
self._print_list(self.notfound)
def send_emails(self, fromaddr, override_email, party, mail_text):
self.mail_text = mail_text
self.fromaddr = fromaddr
self.party = ''
if party:
self.party = '%s ' % party
for k in self.notfound:
self._send_email(override_email, k)
def _backup_keyring(self):
return shutil.copy(self.keyring, '%s-pius-backup' % self.keyring)
def prune(self):
self._backup_keyring()
extra_opts = '%s %s' % (self.QUIET_OPTS, self.AUTO_OPTS)
cmd = '%s %s --fingerprint' % (self.basecmd, extra_opts)
debug(cmd)
gpg = subprocess.Popen(cmd, shell=True, stdin=None, close_fds=True,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
fps = []
for line in gpg.stdout:
if line.startswith('fpr'):
parts = line.split(':')
fps.append(parts[9])
gpg.wait()
basecmd = '%s --fingerprint' % self.basecmd
basedelcmd = '%s %s --delete-key' % (self.basecmd, extra_opts)
for fp in fps:
cmd = '%s %s' % (basecmd, fp)
gpg = os.popen(cmd, 'r')
for line in gpg:
print line.strip()
gpg.close()
ans = raw_input('Delete this key? (\'yes\' to delete, \'q\' to quit'
' anything to skip) ')
if ans in ('q', 'Q'):
print 'Dying at user request'
sys.exit(1)
if ans == 'yes':
print "Deleting key ..."
cmd = '%s %s' % (basedelcmd, fp)
debug(cmd)
gpg = os.popen(cmd, 'r')
gpg.close()
print
backup = '%s-pius-backup' % self.keyring
print 'A backup file is in %s' % backup
# END class KeyringBuilder
def check_options(parser, mode, options):
'''Check options for user error.'''
global DEBUG_ON
if mode == 'help':
parser.print_help()
sys.exit(0)
if not mode or mode not in ('build', 'prune'):
parser.error('Invalid or missing mode.')
if options.debug:
DEBUG_ON = True
if not options.keyring:
parser.error('Must specify a keyring')
if mode == 'build':
if not options.csv_file and not options.mbox_file:
parser.error('Build mode needs one of --csv-file or --mbox-file')
if os.path.exists(options.tmp_dir) and not os.path.isdir(options.tmp_dir):
parser.error('%s exists but isn\'t a directory. It must not exist or be\n'
'a directory.' % options.tmp_dir)
if not os.path.exists(options.tmp_dir):
try:
os.mkdir(options.tmp_dir, 0700)
except OSError, msg:
parser.error('%s was doesn\'t exist, and was unable to be created: %s'
% (options.tmp_dir, msg))
def main():
usage = '%prog <mode> [options]'
intro = '''
%prog has several modes to help you manage keyrings. It is primarily designed
to help manage keysigning party rings, but can be used to manage any PGP
keyring. A mode must be the first argument. The options below are grouped by
their mode, and an explanation of modes is at the bottom.
'''
outro = '''
Example: %s build --csv-file /tmp/report --mbox-file /tmp/mbox --mail
yo...@co...
''' % sys.argv[0]
parser = optparse.OptionParser(usage=usage, description=intro, epilog=outro,
version='%%prog %s' % VERSION)
parser.set_defaults(delimiter=DEFAULT_CSV_DELIMITER,
name_field=DEFAULT_CSV_NAME_FIELD,
email_field=DEFAULT_CSV_EMAIL_FIELD,
fp_field=DEFAULT_CSV_FP_FIELD,
gpg_path=DEFAULT_GPG_PATH,
party='',
tmp_dir=DEFAULT_TMP_DIR)
common = optparse.OptionGroup(parser, 'Options common to all modes')
common.add_option('-d', '--debug', dest='debug', action='store_true',
help='Debug output')
common.add_option('-g', '--gpg-path', dest='gpg_path', metavar='PATH',
help='Path to gpg binary. [default; %default]')
common.add_option('-r', '--keyring', dest='keyring', metavar='KEYRING', nargs=1,
help='Keyring file.')
common.add_option('-v', '--verbose', dest='verbose', action='store_true',
help='Print summaries')
parser.add_option_group(common)
build_intro = '''
The "build" mode is the most common mode. It's primary functionality is parsing
a CSV file, attempting to find all the keys, and then emailing anyone whose key
could not be found. For completness sake, it can also import keys from a file.
Options for 'build' mode'''
build = optparse.OptionGroup(parser, build_intro)
build.add_option('-b', '--mbox-file', dest='mbox_file', metavar='FILE',
help='Parse mbox FILE, examining each message for'
' fingerprints or ascii-armored keys. Tries to be as'
' intelligent as possible here, including decoding'
' messages as necessary.')
build.add_option('-c', '--csv-file', dest='csv_file', metavar='FILE',
help='Parse FILE as a CSV and import keys. You will almost'
' want -D, -E, -F, and -N to control CSV parsing.')
build.add_option('-D', '--delimiter', dest='delimiter', metavar='DELIMITER',
help='Only meaningful with -c. Field delimiter to use when'
' parsing the CSV. [default: %default]')
build.add_option('-E', '--email-field', dest='email_field', metavar='FIELD',
type='int',
help='Only meaningful with -c. The field number of the CSV'
' where the email is. [default: %default]')
build.add_option('-F', '--fp-field', dest='fp_field', metavar='FIELD',
type='int',
help='Only meaningful with -c. The field number of the CSV'
' where the fingerprint is. [default: %default]')
build.add_option('-m', '--mail', dest='mail', metavar='EMAIL',
help='Email people whos keys were not found, using EMAIL')
build.add_option('-M', '--mail-text', dest='mail_text', metavar='FILE',
help='Use the text in FILE as the body of email when'
' sending out emails instead of the default text.'
' To see the default text use'
' --print-default-email. Requires -m.')
build.add_option('-N', '--name-field', dest='name_field', metavar='FIELD',
type='int',
help='Only meaningful with -c. The field number of the CSV'
' where the name is. [default: %default]')
build.add_option('-n', '--override-email', dest='mail_override',
metavar='EMAIL', nargs=1,
help='Rather than send to the user, send to this address.'
' Mostly useful for debugging.')
build.add_option('-p', '--party', dest='party', metavar='NAME', nargs=1,
help='The name of the party. This will be printed in the'
' emails sent out. Only useful with -m.')
build.add_option('-s', '--keyservers', dest='keyservers', metavar='KEYRING',
action='append', help='Keyservers to try. Specify this option'
' once for each server (-s foo -s bar). [default: %s]' %
', '.join(DEFAULT_KEYSERVERS))
build.add_option('-t', '--tmp-dir', dest='tmp_dir',
help='Directory to put temporary stuff in. [default:'
' %default]')
build.add_option('-T', '--print-default-email', dest='print_default_email',
action='store_true', help='Print the default email.')
parser.add_option_group(build)
prune_intro = '''
The "prune" mode asks about each key on a keyring and removes one you specify.
This is useful for trimming a keyring of people who didn't show after a party
before distributing they keyring.
There are no options'''
prune = optparse.OptionGroup(parser, prune_intro)
parser.add_option_group(prune)
(options, args) = parser.parse_args()
mode = None
if args:
mode = args.pop()
if options.print_default_email:
print_default_email()
sys.exit(0)
check_options(parser, mode, options)
# We can't set this as a default above because 'append' will not allow
# users to completely override the list
if not options.keyservers:
options.keyservers = DEFAULT_KEYSERVERS
kb = KeyringBuilder(options.gpg_path, options.keyring, options.keyservers,
options.tmp_dir)
if mode == 'prune':
kb.prune()
sys.exit(0)
# all that's left is mode == 'build'
if not mode == 'build':
print 'Unrecognized mode %s' % mode
sys.exit(1)
keys = []
if options.mbox_file:
keys = parse_mbox(options.mbox_file)
if options.csv_file:
keys.extend(parse_csv(options.csv_file, options.delimiter,
options.name_field, options.email_field, options.fp_field))
if not keys:
print "No keys IDs extract from CSV"
sys.exit(0)
if keys:
kb.get_all_keys(keys)
if options.verbose:
kb.print_report()
if options.mail:
kb.send_emails(options.mail, options.mail_override, options.party,
options.mail_text)
if __name__ == '__main__':
main()
--- NEW FILE: pius-party-worksheet ---
#!/usr/bin/perl
# vim:expandtab:ai:tabstop=2:textwidth=78:softtabstop=2:shiftwidth=2
use strict;
use warnings;
use Getopt::Long;
#
# Copyright (c) 2008 - present Phil Dibowitz (ph...@ip...)
#
# 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, version 2.
#
# Given a PGP Keyring, generate a worksheet for a keysigning party.
#
# This is largely based on party-table.pl by
# V. Alex Brennen <va...@cr...> and Gerfried Fuchs <al...@is...>
# You can find the original at http://www.cryptnet.net/people/vab/
#
use constant VERSION => '2.0.9';
sub print_html_header
{
my $extra_fields = shift;
print "<html>\n<head><title>PGP Keysigning Party Keys</title></head>\n";
print "<body>\n<table border=1>\n";
print "<tr>\n"
. " <th>Key ID</th>\n <th>Owner</th>\n <th>Fingerprint</th>\n"
. " <th>Size</th>\n <th>Type</th>\n <th>Key Info Matches?</th>\n"
. " <th>Owner ID Matches?</th>\n";
foreach my $field (@$extra_fields) {
print " <th>$field</th>\n";
}
print "</tr>\n";
}
sub get_fingerprints
{
my $keyring = shift;
my $cmd = 'gpg --fingerprint --no-default-keyring --no-options'
. " --with-colons --keyring $keyring | egrep "
. '\'^(pub|fpr):\'';
my @fps = `$cmd`;
return \@fps;
}
sub parse_fingerprints
{
my $fps = shift;
my $key_metadata = {};
while (my $line = shift(@{$fps})) {
if ($line =~ /^pub/) {
my ($pub, $comptrust, $size, $type, $longid, $date, undef,
undef, $settrust, $owner, undef, undef, $flags, undef)
= split(/:/, $line);
my $id = substr($longid, 8);
my ($fpr, undef, undef, undef, undef, undef, undef, undef, undef,
$fingerprint) = split(/:/, shift(@{$fps}));
if ($type eq '17') {
$type = 'DSA';
} elsif ($type eq '20') {
$type = 'El Gamal';
} elsif ($type eq '1') {
$type = 'RSA';
}
if (length($fingerprint) == 40) {
for my $i qw(36 32 28 24 20 16 12 8 4) {
if ($i != 20) {
substr($fingerprint, $i, 0, ' ');
}
if ($i == 20) {
substr($fingerprint, $i, 0, "\n");
}
}
} elsif (length($fingerprint) == 32) {
for my $i qw(30 28 26 24 22 20 18 16 14 12 10 8 6 4 2) {
if ($i != 16) {
substr($fingerprint, $i, 0, ' ');
}
if ($i == 16) {
substr($fingerprint, $i, 0, "\n");
}
}
}
$owner =~ s/&/&/;
$owner =~ s/</<\;/;
$owner =~ s/>/>\;/;
push (@{$key_metadata->{$owner}},
{'id' => $id,
'owner' => $owner,
'fingerprint' => $fingerprint,
'size' => $size,
'type' => $type});
}
}
return $key_metadata;
}
sub print_table_body
{
my ($metadata, $num_fields) = @_;
# Loop to create extra-fields HTML only once
my $extra_fields_html = '';
for (my $i = 0; $i < $num_fields; $i++) {
$extra_fields_html .= " <td> </td>\n";
}
foreach my $user (sort(keys(%{$metadata}))) {
foreach my $key (@{$metadata->{$user}}) {
print "<tr>\n"
. " <td><pre>$key->{'id'}</pre></td>\n"
. " <td>$key->{'owner'}</td>\n"
. " <td><pre>$key->{'fingerprint'}</pre></td>\n"
. " <td>$key->{'size'}</td>\n"
. " <td>$key->{'type'}</td>\n"
. " <td> </td>\n"
. " <td> </td>\n"
. $extra_fields_html
. "</tr>\n";
}
}
}
sub print_html_footer
{
print "</table>\n</body>\n</html>";
}
sub help
{
my $err = shift || 0;
# If we're printing this due to incorrect usage, the user is probably
# re-directing output to a file, so we need to print to stderr.
my $fh = *STDOUT;
if ($err) {
$fh = *STDERR;
}
print $fh 'PIUS PGP Keysigning Party Worksheet Generator ' . VERSION . "\n\n";
print $fh <<EOF;
Usage: $0 <options> <keyring> > out-file.html
<keyring> should be the gpg keyring file with the public keys for all party
participants.
Options:
-e, --extra-fields <fields>
A comma-separated list of extra colums to have. This is useful if a subset
of the participants want to do something extra such as S/MIME for CA Cert
verification.
-h, --help
Print this help message and exit.
-v, --version
Print the version and exit.
EOF
}
my $opts = {};
GetOptions($opts,
'extra-fields|e=s',
'help|h',
'version|v',
) || die('Bad options');
if (exists($opts->{'help'})) {
help();
exit(0);
}
if (exists($opts->{'version'})) {
print "$0 " . VERSION . "\n";
exit(0);
}
my $extra_fields = [];
if (exists($opts->{'extra-fields'})) {
@$extra_fields = split(',', $opts->{'extra-fields'});
}
my $keyring = shift;
unless($keyring) {
help(1);
exit(1);
}
my $fps = get_fingerprints($keyring);
my $metadata = parse_fingerprints($fps);
print_html_header($extra_fields);
print_table_body($metadata, scalar(@$extra_fields));
print_html_footer();
|