Compare commits
32 Commits
with_open_
...
kant_0.9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ab99f0f22 | ||
|
|
4dedd79942 | ||
|
|
b2ba35504d | ||
|
|
4da7afdeaf | ||
|
|
23a0dfedb1 | ||
|
|
fb7d964516 | ||
|
|
72c1532284 | ||
|
|
130074788a | ||
|
|
30f508f40c | ||
|
|
8ff59fdaf0 | ||
|
|
28e46f6f51 | ||
|
|
e0a625853d | ||
|
|
31ecf0b262 | ||
|
|
fe48317d07 | ||
|
|
9be695aea6 | ||
|
|
b1aaca28d7 | ||
|
|
05c3fcc825 | ||
|
|
4cf5a6393a | ||
|
|
3909b0c783 | ||
|
|
5ace114ef8 | ||
|
|
5ad4f0bda8 | ||
|
|
3869b30198 | ||
|
|
f652aa7c35 | ||
|
|
6dbc713dc9 | ||
|
|
20388431aa | ||
|
|
eea9cf778e | ||
|
|
b93ac7368d | ||
|
|
7df13e51e3 | ||
|
|
eddf7750c7 | ||
|
|
efa84759da | ||
|
|
86eba8b6ab | ||
|
|
a1925e1053 |
89
arch/reference
Normal file
89
arch/reference
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
some random snippets to incorporate...
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
######################
|
||||||
|
this was to assist with https://www.archlinux.org/news/perl-library-path-change/
|
||||||
|
the following was used to gen the /tmp/perlfix.pkgs.lst:
|
||||||
|
pacman -Qqo '/usr/lib/perl5/vendor_perl' >> /tmp/perlfix.pkgs.lst ; pacman -Qqo '/usr/lib/perl5/site_perl' >> /tmp/perlfix.pkgs.lst
|
||||||
|
######################
|
||||||
|
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import re
|
||||||
|
import os
|
||||||
|
import pprint
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
pkgs = []
|
||||||
|
|
||||||
|
pkglstfile = '/tmp/perlfix.pkgs.lst'
|
||||||
|
|
||||||
|
if os.path.isfile(pkglstfile):
|
||||||
|
with open(pkglstfile, 'r') as f:
|
||||||
|
pkgs = f.read().splitlines()
|
||||||
|
|
||||||
|
pkgd = {'rdeps': [],
|
||||||
|
'deps': [],
|
||||||
|
'remove': []}
|
||||||
|
|
||||||
|
for p in pkgs:
|
||||||
|
pkgchkcmd = ['apacman', '-Q', p]
|
||||||
|
with open(os.devnull, 'w') as devnull:
|
||||||
|
pkgchk = subprocess.run(pkgchkcmd, stdout = devnull, stderr = devnull).returncode
|
||||||
|
if pkgchk != 0: # not installed anymore
|
||||||
|
break
|
||||||
|
cmd = ['apacman',
|
||||||
|
'-Qi',
|
||||||
|
p]
|
||||||
|
stdout = subprocess.run(cmd, stdout = subprocess.PIPE).stdout.decode('utf-8').strip().splitlines()
|
||||||
|
#pprint.pprint(stdout)
|
||||||
|
d = {re.sub('\s', '_', k.strip().lower()):v.strip() for k, v in (dict(k.split(':', 1) for k in stdout).items())}
|
||||||
|
|
||||||
|
# some pythonizations..
|
||||||
|
# list of things(keys) that should be lists
|
||||||
|
ll = ['architecture', 'conflicts_with', 'depends_on', 'groups', 'licenses', 'make_depends',
|
||||||
|
'optional_deps', 'provides', 'replaces', 'required_by']
|
||||||
|
# and now actually listify
|
||||||
|
for k in ll:
|
||||||
|
if k in d.keys():
|
||||||
|
if d[k].lower() in ('none', ''):
|
||||||
|
d[k] = None
|
||||||
|
else:
|
||||||
|
d[k] = d[k].split()
|
||||||
|
# Not necessary... blah blah inconsistent whitespace blah blah.
|
||||||
|
#for k in ('build_date', 'install_date'):
|
||||||
|
# if k in d.keys():
|
||||||
|
# try:
|
||||||
|
# d[k] = datetime.datetime.strptime(d[k], '%a %d %b %Y %H:%M:%S %p %Z')
|
||||||
|
# except:
|
||||||
|
# d[k] = datetime.datetime.strptime(d[k], '%a %d %b %Y %H:%M:%S %p')
|
||||||
|
|
||||||
|
#pprint.pprint(d)
|
||||||
|
if d['required_by']:
|
||||||
|
pkgd['rdeps'].extend(d['required_by'])
|
||||||
|
else:
|
||||||
|
if d['install_reason'] != 'Explicitly installed':
|
||||||
|
pkgd['remove'].append(p)
|
||||||
|
if d['depends_on']:
|
||||||
|
pkgd['deps'].extend(d['depends_on'])
|
||||||
|
#break
|
||||||
|
|
||||||
|
for x in ('rdeps', 'deps'):
|
||||||
|
pkgd[x].sort()
|
||||||
|
|
||||||
|
#for p in pkgd['rdeps']:
|
||||||
|
# if p in pkgd['deps']:
|
||||||
|
# pkgd['
|
||||||
|
|
||||||
|
#print('DEPENDENCIES:')
|
||||||
|
#print('\n'.join(pkgd['deps']))
|
||||||
|
#print('\nREQUIRED BY:')
|
||||||
|
#print('\n'.join(pkgd['rdeps']))
|
||||||
|
#print('\nCAN REMOVE:')
|
||||||
|
print('\n'.join(pkgd['remove']))
|
||||||
|
|
||||||
|
#cmd = ['apacman', '-R']
|
||||||
|
#cmd.extend(pkgd['remove'])
|
||||||
|
#subprocess.run(cmd)
|
||||||
151
arch/repoclone.py
Executable file
151
arch/repoclone.py
Executable file
@@ -0,0 +1,151 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import configparser
|
||||||
|
import datetime
|
||||||
|
import os
|
||||||
|
import pprint
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
cfgfile = os.path.join(os.environ['HOME'], '.arch.repoclone.ini')
|
||||||
|
|
||||||
|
# Rsync options
|
||||||
|
opts = [
|
||||||
|
'--recursive', # recurse into directories
|
||||||
|
'--times', # preserve modification times
|
||||||
|
'--links', # copy symlinks as symlinks
|
||||||
|
'--hard-links', # preserve hard links
|
||||||
|
'--quiet', # suppress non-error messages
|
||||||
|
'--delete-after', # receiver deletes after transfer, not during
|
||||||
|
'--delay-updates', # put all updated files into place at end
|
||||||
|
'--copy-links', # transform symlink into referent file/dir
|
||||||
|
'--safe-links', # ignore symlinks that point outside the tree
|
||||||
|
#'--max-delete', # don't delete more than NUM files
|
||||||
|
'--delete-excluded', # also delete excluded files from dest dirs
|
||||||
|
'--exclude=.*' # exclude files matching PATTERN
|
||||||
|
]
|
||||||
|
|
||||||
|
def sync(args):
|
||||||
|
with open(os.devnull, 'w') as devnull:
|
||||||
|
mntchk = subprocess.run(['findmnt', args['mount']], stdout = devnull, stderr = devnull)
|
||||||
|
if mntchk.returncode != 0:
|
||||||
|
exit('!! BAILING OUT; {0} isn\'t mounted !!'.format(args['mount']))
|
||||||
|
if args['bwlimit'] >= 1:
|
||||||
|
opts.insert(10, '--bwlimit=' + str(args['bwlimit'])) # limit socket I/O bandwidth
|
||||||
|
for k in ('destination', 'logfile', 'lockfile'):
|
||||||
|
os.makedirs(os.path.dirname(args[k]), exist_ok = True)
|
||||||
|
paths = os.environ['PATH'].split(':')
|
||||||
|
rsync = '/usr/bin/rsync' # set the default
|
||||||
|
for p in paths:
|
||||||
|
testpath = os.path.join(p, 'rsync')
|
||||||
|
if os.path.isfile(testpath):
|
||||||
|
rsync = testpath # in case rsync isn't in /usr/bin/rsync
|
||||||
|
break
|
||||||
|
cmd = [rsync] # the path to the binary
|
||||||
|
cmd.extend(opts) # the arguments
|
||||||
|
# TODO: implement repos here?
|
||||||
|
cmd.append(os.path.join(args['mirror'], '.')) # the path on the remote mirror
|
||||||
|
cmd.append(os.path.join(args['destination'], '.')) # the local destination
|
||||||
|
if os.path.isfile(args['lockfile']):
|
||||||
|
with open(args['lockfile'], 'r') as f:
|
||||||
|
existingpid = f.read().strip()
|
||||||
|
if os.isatty(sys.stdin.fileno()):
|
||||||
|
# Running from shell
|
||||||
|
exit('!! A repo synchronization seems to already be running (PID: {0}). Quitting. !!'.format(existingpid))
|
||||||
|
else:
|
||||||
|
exit() # we're running in cron, shut the hell up.
|
||||||
|
else:
|
||||||
|
with open(args['lockfile'], 'w') as f:
|
||||||
|
f.write(str(os.getpid()))
|
||||||
|
with open(args['logfile'], 'a') as log:
|
||||||
|
c = subprocess.run(cmd, stdout = log, stderr = subprocess.PIPE)
|
||||||
|
now = int(datetime.datetime.timestamp(datetime.datetime.utcnow()))
|
||||||
|
with open(os.path.join(args['destination'], 'lastsync'), 'w') as f:
|
||||||
|
f.write(str(now) + '\n')
|
||||||
|
os.remove(args['lockfile'])
|
||||||
|
# Only report errors at the end of the run if we aren't running in cron. Otherwise, log them.
|
||||||
|
errors = c.stderr.decode('utf-8').splitlines()
|
||||||
|
if os.isatty(sys.stdin.fileno()):
|
||||||
|
print('We encountered some errors:')
|
||||||
|
for e in errors:
|
||||||
|
if e.startswith('symlink has no referent: '):
|
||||||
|
print('Broken upstream symlink: {0}'.format(e.split()[1].replace('"', '')))
|
||||||
|
else:
|
||||||
|
print(e)
|
||||||
|
else:
|
||||||
|
with open(args['logfile'], 'a') as f:
|
||||||
|
for e in errors:
|
||||||
|
f.write('{0}\n'.format(e))
|
||||||
|
return()
|
||||||
|
|
||||||
|
def getDefaults():
|
||||||
|
# Hardcoded defaults
|
||||||
|
dflt = {'mirror': 'rsync://mirror.square-r00t.net/arch/',
|
||||||
|
'repos': 'core,extra,community,multilib,iso/latest',
|
||||||
|
'destination': '/srv/repos/arch',
|
||||||
|
'mount': '/',
|
||||||
|
'bwlimit': 0,
|
||||||
|
'lockfile': '/var/run/repo-sync.lck',
|
||||||
|
'logfile': '/var/log/repo/arch.log'}
|
||||||
|
realcfg = configparser.ConfigParser(defaults = dflt)
|
||||||
|
if not os.path.isfile(cfgfile):
|
||||||
|
with open(cfgfile, 'w') as f:
|
||||||
|
realcfg.write(f)
|
||||||
|
realcfg.read(cfgfile)
|
||||||
|
return(realcfg)
|
||||||
|
|
||||||
|
def parseArgs():
|
||||||
|
cfg = getDefaults()
|
||||||
|
liveopts = cfg['DEFAULT']
|
||||||
|
args = argparse.ArgumentParser(description = 'Synchronization for a remote Arch repository to a local one.',
|
||||||
|
epilog = ('This program will write a default configuration file to {0} ' +
|
||||||
|
'if one is not found.'.format(cfgfile)))
|
||||||
|
args.add_argument('-m',
|
||||||
|
'--mirror',
|
||||||
|
dest = 'mirror',
|
||||||
|
default = liveopts['mirror'],
|
||||||
|
help = ('The upstream mirror to sync from, must be an rsync URI '+
|
||||||
|
'(Default: {0}').format(liveopts['mirror']))
|
||||||
|
# TODO: can we do this?
|
||||||
|
# args.add_argument('-r',
|
||||||
|
# '--repos',
|
||||||
|
# dest = 'repos',
|
||||||
|
# default = liveopts['repos'],
|
||||||
|
# help = ('The repositories to sync; must be a comma-separated list. ' +
|
||||||
|
# '(Currently not used.) Default: {0}').format(','.join(liveopts['repos'])))
|
||||||
|
args.add_argument('-d',
|
||||||
|
'--destination',
|
||||||
|
dest = 'destination',
|
||||||
|
default = liveopts['destination'],
|
||||||
|
help = 'The destination directory to sync to. Default: {0}'.format(liveopts['destination']))
|
||||||
|
args.add_argument('-b',
|
||||||
|
'--bwlimit',
|
||||||
|
dest = 'bwlimit',
|
||||||
|
default = liveopts['bwlimit'],
|
||||||
|
type = int,
|
||||||
|
help = 'The amount, in Kilobytes per second, to throttle the sync to. Default is to not throttle (0).')
|
||||||
|
args.add_argument('-l',
|
||||||
|
'--log',
|
||||||
|
dest = 'logfile',
|
||||||
|
default = liveopts['logfile'],
|
||||||
|
help = 'The path to the logfile. Default: {0}'.format(liveopts['logfile']))
|
||||||
|
args.add_argument('-L',
|
||||||
|
'--lock',
|
||||||
|
dest = 'lockfile',
|
||||||
|
default = liveopts['lockfile'],
|
||||||
|
help = 'The path to the lockfile. Default: {0}'.format(liveopts['lockfile']))
|
||||||
|
args.add_argument('-M',
|
||||||
|
'--mount',
|
||||||
|
dest = 'mount',
|
||||||
|
default = liveopts['mount'],
|
||||||
|
help = 'The mountpoint for your --destination. The script will exit if this point is not mounted. ' +
|
||||||
|
'If you don\'t need mount checking, just use /. Default: {0}'.format(liveopts['mount']))
|
||||||
|
return(args)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
args = vars(parseArgs().parse_args())
|
||||||
|
sync(args)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
353
gpg/kant.py
353
gpg/kant.py
@@ -1,353 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import datetime
|
|
||||||
import email
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
from io import BytesIO
|
|
||||||
from socket import *
|
|
||||||
import urllib.parse
|
|
||||||
import gpgme # non-stdlib; Arch package is "python-pygpgme"
|
|
||||||
|
|
||||||
# TODO:
|
|
||||||
# -attach pubkey when sending below email
|
|
||||||
# mail to first email address in key with signed message:
|
|
||||||
#Subj: Your GPG key has been signed
|
|
||||||
#
|
|
||||||
#Hello! Thank you for participating in a keysigning party and exchanging keys.
|
|
||||||
#
|
|
||||||
#I have signed your key (KEYID) with trust level "TRUSTLEVEL" because:
|
|
||||||
#
|
|
||||||
#* You have presented sufficient proof of identity
|
|
||||||
#
|
|
||||||
#The signatures have been pushed to KEYSERVERS.
|
|
||||||
#
|
|
||||||
#I have taken the liberty of attaching my public key in the event you've not signed it yet and were unable to find it. Please feel free to push to pgp.mit.edu or hkps.pool.sks-keyservers.net.
|
|
||||||
#
|
|
||||||
#As a reminder, my key ID, Keybase.io username, and verification/proof of identity can all be found at:
|
|
||||||
#
|
|
||||||
#https://devblog.square-r00t.net/about/my-gpg-public-key-verification-of-identity
|
|
||||||
#
|
|
||||||
#Thanks again!
|
|
||||||
|
|
||||||
def getKeys(args):
|
|
||||||
# Get our concept
|
|
||||||
os.environ['GNUPGHOME'] = args['gpgdir']
|
|
||||||
gpg = gpgme.Context()
|
|
||||||
keys = {}
|
|
||||||
allkeys = []
|
|
||||||
# Do we have the key already? If not, fetch.
|
|
||||||
for k in args['rcpts'].keys():
|
|
||||||
if args['rcpts'][k]['type'] == 'fpr':
|
|
||||||
allkeys.append(k)
|
|
||||||
if args['rcpts'][k]['type'] == 'email':
|
|
||||||
# We need to actually do a lookup on the email address.
|
|
||||||
with open(os.devnull, 'w') as f:
|
|
||||||
# TODO: replace with gpg.keylist_mode(gpgme.KEYLIST_MODE_EXTERN) and internal mechanisms?
|
|
||||||
keyout = subprocess.run(['gpg2',
|
|
||||||
'--search-keys',
|
|
||||||
'--with-colons',
|
|
||||||
'--batch',
|
|
||||||
k],
|
|
||||||
stdout = subprocess.PIPE,
|
|
||||||
stderr = f)
|
|
||||||
keyout = keyout.stdout.decode('utf-8').splitlines()
|
|
||||||
for line in keyout:
|
|
||||||
if line.startswith('pub:'):
|
|
||||||
key = line.split(':')[1]
|
|
||||||
keys[key] = {}
|
|
||||||
keys[key]['uids'] = {}
|
|
||||||
keys[key]['time'] = int(line.split(':')[4])
|
|
||||||
elif line.startswith('uid:'):
|
|
||||||
uid = re.split('<(.*)>', urllib.parse.unquote(line.split(':')[1].strip()))
|
|
||||||
uid.remove('')
|
|
||||||
uid = [u.strip() for u in uid]
|
|
||||||
keys[key]['uids'][uid[1]] = {}
|
|
||||||
keys[key]['uids'][uid[1]]['comment'] = uid[0]
|
|
||||||
keys[key]['uids'][uid[1]]['time'] = int(line.split(':')[2])
|
|
||||||
if len(keys) > 1: # Print the keys and prompt for a selection.
|
|
||||||
print('\nWe found the following keys for <{0}>...\n\nKEY ID:'.format(k))
|
|
||||||
for k in keys:
|
|
||||||
print('{0}\n{1:6}(Generated at {2}) UIDs:'.format(k, '', datetime.datetime.utcfromtimestamp(keys[k]['time'])))
|
|
||||||
for email in keys[k]['uids']:
|
|
||||||
print('{0:42}(Generated {3}) <{2}> {1}'.format('',
|
|
||||||
keys[k]['uids'][email]['comment'],
|
|
||||||
email,
|
|
||||||
datetime.datetime.utcfromtimestamp(
|
|
||||||
keys[k]['uids'][email]['time'])))
|
|
||||||
print()
|
|
||||||
while True:
|
|
||||||
key = input('Please enter the (full) appropriate key: ')
|
|
||||||
if key not in keys.keys():
|
|
||||||
print('Please enter a full key ID from the list above or hit ctrl-d to exit.')
|
|
||||||
else:
|
|
||||||
allkeys.append(key)
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
if not len(keys.keys()) >= 1:
|
|
||||||
print('Could not find {0}!'.format(k))
|
|
||||||
continue
|
|
||||||
key = list(keys.keys())[0]
|
|
||||||
print('\nFound key {0} for <{1}> (Generated at {2}):'.format(key, k, datetime.datetime.utcfromtimestamp(keys[key]['time'])))
|
|
||||||
for email in keys[key]['uids']:
|
|
||||||
print('\t(Generated {2}) {0} <{1}>'.format(keys[key]['uids'][email]['comment'],
|
|
||||||
email,
|
|
||||||
datetime.datetime.utcfromtimestamp(keys[key]['uids'][email]['time'])))
|
|
||||||
allkeys.append(key)
|
|
||||||
print()
|
|
||||||
## And now we can (FINALLY) fetch the key(s).
|
|
||||||
# TODO: replace with gpg.keylist_mode(gpgme.KEYLIST_MODE_EXTERN) and internal mechanisms?
|
|
||||||
recvcmd = ['gpg2', '--recv-keys', '--batch', '--yes'] # We'll add the keys onto the end of this next.
|
|
||||||
recvcmd.extend(allkeys)
|
|
||||||
with open(os.devnull, 'w') as f:
|
|
||||||
subprocess.run(recvcmd, stdout = f, stderr = f) # We hide stderr because gpg, for some unknown reason, spits non-errors to stderr.
|
|
||||||
return(allkeys)
|
|
||||||
|
|
||||||
def sigKeys(keyids):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def modifyDirmngr(op, args):
|
|
||||||
if not args['keyservers']:
|
|
||||||
return()
|
|
||||||
pid = str(os.getpid())
|
|
||||||
activecfg = os.path.join(args['gpgdir'], 'dirmngr.conf')
|
|
||||||
bakcfg = '{0}.{1}'.format(activecfg, pid)
|
|
||||||
if op in ('new', 'start'):
|
|
||||||
if os.path.lexists(activecfg):
|
|
||||||
shutil.copy2(activecfg, bakcfg)
|
|
||||||
with open(bakcfg, 'r') as read, open(activecfg, 'w') as write:
|
|
||||||
for line in read:
|
|
||||||
if not line.startswith('keyserver '):
|
|
||||||
write.write(line)
|
|
||||||
with open(activecfg, 'a') as f:
|
|
||||||
for s in args['keyservers']:
|
|
||||||
uri = '{0}://{1}:{2}'.format(s['proto'], s['server'], s['port'][0])
|
|
||||||
f.write('keyserver {0}\n'.format(uri))
|
|
||||||
if op in ('old', 'stop'):
|
|
||||||
if os.path.lexists(bakcfg):
|
|
||||||
with open(bakcfg, 'r') as read, open(activecfg, 'w') as write:
|
|
||||||
for line in read:
|
|
||||||
write.write(line)
|
|
||||||
os.remove(bakcfg)
|
|
||||||
else:
|
|
||||||
os.remove(activecfg)
|
|
||||||
subprocess.run(['gpgconf',
|
|
||||||
'--reload',
|
|
||||||
'dirmngr'])
|
|
||||||
return()
|
|
||||||
|
|
||||||
def serverParser(uri):
|
|
||||||
# https://en.wikipedia.org/wiki/Key_server_(cryptographic)#Keyserver_examples
|
|
||||||
# We need to make a mapping of the default ports.
|
|
||||||
server = {}
|
|
||||||
protos = {'hkp': [11371, ['tcp', 'udp']],
|
|
||||||
'hkps': [443, ['tcp']], # Yes, same as https
|
|
||||||
'http': [80, ['tcp']],
|
|
||||||
'https': [443, ['tcp']], # SSL/TLS
|
|
||||||
'ldap': [389, ['tcp', 'udp']], # includes TLS negotiation since it runs on the same port
|
|
||||||
'ldaps': [636, ['tcp', 'udp']]} # SSL
|
|
||||||
urlobj = urllib.parse.urlparse(uri)
|
|
||||||
server['proto'] = urlobj.scheme
|
|
||||||
lazy = False
|
|
||||||
if not server['proto']:
|
|
||||||
server['proto'] = 'hkp' # Default
|
|
||||||
server['server'] = urlobj.hostname
|
|
||||||
if not server['server']:
|
|
||||||
server['server'] = re.sub('^([A-Za-z]://)?(.+[^:][^0-9])(:[0-9]+)?$', '\g<2>', uri)
|
|
||||||
lazy = True
|
|
||||||
server['port'] = urlobj.port
|
|
||||||
if not server['port']:
|
|
||||||
if lazy:
|
|
||||||
p = re.sub('.*:([0-9]+)$', '\g<1>', uri)
|
|
||||||
server['port'] = protos[server['proto']] # Default
|
|
||||||
return(server)
|
|
||||||
|
|
||||||
def parseArgs():
|
|
||||||
def getDefGPGDir():
|
|
||||||
try:
|
|
||||||
gpgdir = os.environ['GNUPGHOME']
|
|
||||||
except KeyError:
|
|
||||||
try:
|
|
||||||
homedir = os.environ['HOME']
|
|
||||||
gpgdchk = os.path.join(homedir, '.gnupg')
|
|
||||||
except KeyError:
|
|
||||||
# There is no reason that this should ever get this far, but... edge cases be crazy.
|
|
||||||
gpgdchk = os.path.join(os.path.expanduser('~'), '.gnupg')
|
|
||||||
if os.path.isdir(gpgdchk):
|
|
||||||
gpgdir = gpgdchk
|
|
||||||
else:
|
|
||||||
gpgdir = None
|
|
||||||
return(gpgdir)
|
|
||||||
def getDefKey(defgpgdir):
|
|
||||||
os.environ['GNUPGHOME'] = defgpgdir
|
|
||||||
if not defgpgdir:
|
|
||||||
return(None)
|
|
||||||
defkey = None
|
|
||||||
gpg = gpgme.Context()
|
|
||||||
for k in gpg.keylist(None, True): # params are query and secret keyring, respectively
|
|
||||||
if k.can_sign and True not in (k.revoked, k.expired, k.disabled):
|
|
||||||
defkey = k.subkeys[0].fpr
|
|
||||||
break # We'll just use the first primary key we find that's valid as the default.
|
|
||||||
return(defkey)
|
|
||||||
def getDefKeyservers(defgpgdir):
|
|
||||||
srvlst = [None]
|
|
||||||
# We don't need these since we use the gpg agent. Requires GPG 2.1 and above, probably.
|
|
||||||
#if os.path.isfile(os.path.join(defgpgdir, 'dirmngr.conf')):
|
|
||||||
# pass
|
|
||||||
dirmgr_out = subprocess.run(['gpg-connect-agent', '--dirmngr', 'keyserver', '/bye'], stdout = subprocess.PIPE)
|
|
||||||
for l in dirmgr_out.stdout.decode('utf-8').splitlines():
|
|
||||||
#if len(l) == 3 and l.lower().startswith('s keyserver'): # It's a keyserver line
|
|
||||||
if l.lower().startswith('s keyserver'): # It's a keyserver line
|
|
||||||
s = l.split()[2]
|
|
||||||
if len(srvlst) == 1 and srvlst[0] == None:
|
|
||||||
srvlst = [s]
|
|
||||||
else:
|
|
||||||
srvlst.append(s)
|
|
||||||
return(','.join(srvlst))
|
|
||||||
defgpgdir = getDefGPGDir()
|
|
||||||
defkey = getDefKey(defgpgdir)
|
|
||||||
defkeyservers = getDefKeyservers(defgpgdir)
|
|
||||||
args = argparse.ArgumentParser(description = 'Keysigning Assistance and Notifying Tool (KANT)',
|
|
||||||
epilog = 'brent s. || 2017 || https://square-r00t.net',
|
|
||||||
formatter_class = argparse.RawTextHelpFormatter)
|
|
||||||
args.add_argument('-k',
|
|
||||||
'--keys',
|
|
||||||
dest = 'keys',
|
|
||||||
required = True,
|
|
||||||
help = 'A single or comma-separated list of keys to sign,\ntrust, and notify. Can also be an email address.')
|
|
||||||
args.add_argument('-K',
|
|
||||||
'--sigkey',
|
|
||||||
dest = 'sigkey',
|
|
||||||
default = defkey,
|
|
||||||
help = 'The key to use when signing other keys.\nDefault is \033[1m{0}\033[0m.'.format(defkey))
|
|
||||||
args.add_argument('-b',
|
|
||||||
'--batch',
|
|
||||||
dest = 'batchfile',
|
|
||||||
default = None,
|
|
||||||
metavar = '/path/to/batchfile',
|
|
||||||
help = 'If specified, a CSV file to use as a batch run\nin the format of (one per line):\n' +
|
|
||||||
'\n\033[1mKEY_FINGERPRINT_OR_EMAIL_ADDRESS,TRUSTLEVEL,PUSH_TO_KEYSERVER\033[0m\n' +
|
|
||||||
'\n\033[1mTRUSTLEVEL\033[0m can be numeric or string:' +
|
|
||||||
'\n\n\t\033[1m0 = Unknown\n\t1 = Untrusted\n\t2 = Marginal\n\t3 = Full\n\t4 = Ultimate\033[0m\n' +
|
|
||||||
'\n\033[1mPUSH_TO_KEYSERVER\033[0m can be \033[1m1/True\033[0m or \033[1m0/False\033[0m. If marked as False,\n' +
|
|
||||||
'the signature will be made local/non-exportable.')
|
|
||||||
|
|
||||||
args.add_argument('-d',
|
|
||||||
'--gpgdir',
|
|
||||||
dest = 'gpgdir',
|
|
||||||
default = defgpgdir,
|
|
||||||
help = 'The GnuPG configuration directory to use (containing\n' +
|
|
||||||
'your keys, etc.); default is \033[1m{0}\033[0m.'.format(defgpgdir))
|
|
||||||
args.add_argument('-s',
|
|
||||||
'--keyservers',
|
|
||||||
dest = 'keyservers',
|
|
||||||
default = defkeyservers,
|
|
||||||
help = 'The comma-separated keyserver(s) to push to. If "None", don\'t\n' +
|
|
||||||
'push signatures (local/non-exportable signatures will be made).\n'
|
|
||||||
'Default keyserver list is: \n\n\033[1m{0}\033[0m\n\n'.format(re.sub(',', '\n', defkeyservers)))
|
|
||||||
args.add_argument('-n',
|
|
||||||
'--netproto',
|
|
||||||
dest = 'netproto',
|
|
||||||
action = 'store',
|
|
||||||
choices = ['4', '6'],
|
|
||||||
default = '4',
|
|
||||||
help = 'Whether to use (IPv)4 or (IPv)6. Default is to use IPv4.')
|
|
||||||
args.add_argument('-t',
|
|
||||||
'--testkeyservers',
|
|
||||||
dest = 'testkeyservers',
|
|
||||||
action = 'store_true',
|
|
||||||
help = 'If specified, initiate a test connection with each\n'
|
|
||||||
'\nkeyserver before anything else. Disabled by default.')
|
|
||||||
return(args)
|
|
||||||
|
|
||||||
def verifyArgs(args):
|
|
||||||
## Some pythonization...
|
|
||||||
# We don't want to only strip the values, we want to remove ALL whitespace.
|
|
||||||
#args['keys'] = [k.strip() for k in args['keys'].split(',')]
|
|
||||||
#args['keyservers'] = [s.strip() for s in args['keyservers'].split(',')]
|
|
||||||
args['keys'] = [re.sub('\s', '', k) for k in args['keys'].split(',')]
|
|
||||||
args['keyservers'] = [re.sub('\s', '', s) for s in args['keyservers'].split(',')]
|
|
||||||
args['keyservers'] = [serverParser(s) for s in args['keyservers']]
|
|
||||||
## Key(s) to sign
|
|
||||||
args['rcpts'] = {}
|
|
||||||
for k in args['keys']:
|
|
||||||
args['rcpts'][k] = {}
|
|
||||||
try:
|
|
||||||
int(k, 16)
|
|
||||||
ktype = 'fpr'
|
|
||||||
except: # If it isn't a valid key ID...
|
|
||||||
if not re.match('^[\w\.\+\-]+\@[\w-]+\.[a-z]{2,3}$', k): # is it an email address?
|
|
||||||
raise ValueError('{0} is not a valid email address'.format(k))
|
|
||||||
else:
|
|
||||||
ktype = 'email'
|
|
||||||
args['rcpts'][k]['type'] = ktype
|
|
||||||
if ktype == 'fpr' and not len(k) == 40: # Security is important. We don't want users getting collisions, so we don't allow shortened key IDs.
|
|
||||||
raise ValueError('{0} is not a full 40-char key ID or key fingerprint'.format(k))
|
|
||||||
del args['keys']
|
|
||||||
## Batch file
|
|
||||||
if args['batchfile']:
|
|
||||||
batchfilepath = os.path.abspath(os.path.expanduser(args['batchfile']))
|
|
||||||
if not os.path.isfile(batchfilepath):
|
|
||||||
raise ValueError('{0} does not exist or is not a regular file.'.format(batchfilepath))
|
|
||||||
else:
|
|
||||||
args['batchfile'] = batchfilepath
|
|
||||||
## Signing key
|
|
||||||
if not args['sigkey']:
|
|
||||||
raise ValueError('A key for signing is required') # We need a key we can sign with.
|
|
||||||
else:
|
|
||||||
if not os.path.lexists(args['gpgdir']):
|
|
||||||
raise FileNotFoundError('{0} does not exist'.format(args['gpgdir']))
|
|
||||||
elif os.path.isfile(args['gpgdir']):
|
|
||||||
raise NotADirectoryError('{0} is not a directory'.format(args['gpgdir']))
|
|
||||||
try:
|
|
||||||
os.environ['GNUPGHOME'] = args['gpgdir']
|
|
||||||
gpg = gpgme.Context()
|
|
||||||
except:
|
|
||||||
raise RuntimeError('Could not use {0} as a GnuPG home'.format(args['gpgdir']))
|
|
||||||
# Now we need to verify that the private key exists...
|
|
||||||
try:
|
|
||||||
sigkey = gpg.get_key(args['sigkey'], True)
|
|
||||||
except GpgmeError:
|
|
||||||
raise ValueError('Cannot use key {0}'.format(args['sigkey']))
|
|
||||||
# And that it is an eligible candidate to use to sign.
|
|
||||||
if not sigkey.can_sign or True in (sigkey.revoked, sigkey.expired, sigkey.disabled):
|
|
||||||
raise ValueError('{0} is not a valid candidate for signing'.format(args['sigkey']))
|
|
||||||
## Keyservers
|
|
||||||
if args['testkeyservers']:
|
|
||||||
for s in args['keyservers']:
|
|
||||||
# Test to make sure the keyserver is accessible.
|
|
||||||
# First we need to construct a way to use python's socket connector
|
|
||||||
# Great. Now we need to just quickly check to make sure it's accessible - if specified.
|
|
||||||
if args['netproto'] == '4':
|
|
||||||
nettype = AF_INET
|
|
||||||
elif args['netproto'] == '6':
|
|
||||||
nettype = AF_INET6
|
|
||||||
for proto in s['port'][1]:
|
|
||||||
if proto == 'udp':
|
|
||||||
netproto = SOCK_DGRAM
|
|
||||||
elif proto == 'tcp':
|
|
||||||
netproto = SOCK_STREAM
|
|
||||||
sock = socket(nettype, netproto)
|
|
||||||
sock.settimeout(10)
|
|
||||||
tests = sock.connect_ex((s['server'], int(s['port'][0])))
|
|
||||||
uristr = '{0}://{1}:{2} ({3})'.format(s['proto'], s['server'], s['port'][0], proto.upper())
|
|
||||||
if not tests == 0:
|
|
||||||
raise RuntimeError('Keyserver {0} is not available'.format(uristr))
|
|
||||||
else:
|
|
||||||
print('Keyserver {0} is accepting connections.'.format(uristr))
|
|
||||||
sock.close()
|
|
||||||
return(args)
|
|
||||||
|
|
||||||
def main():
|
|
||||||
rawargs = parseArgs()
|
|
||||||
args = verifyArgs(vars(rawargs.parse_args()))
|
|
||||||
modifyDirmngr('new', args)
|
|
||||||
fprs = getKeys(args)
|
|
||||||
sigKeys(fprs)
|
|
||||||
modifyDirmngr('old', args)
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
2
gpg/kant/.gitignore
vendored
Normal file
2
gpg/kant/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/gpgme.pdf
|
||||||
|
/tests
|
||||||
18
gpg/kant/commented.testbatch.kant.csv
Normal file
18
gpg/kant/commented.testbatch.kant.csv
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# NOTE: The python csv module does NOT skip
|
||||||
|
# commented lines!
|
||||||
|
# This is my personal key. Ultimate trust,
|
||||||
|
# push key, careful checking, notify
|
||||||
|
748231EBCBD808A14F5E85D28C004C2F93481F6B,4,1,3,1
|
||||||
|
# This is a testing junk key generated on a completely separate box,
|
||||||
|
# and does not exist on ANY keyservers nor the local keyring.
|
||||||
|
# Never trust, local sig, unknown checking, don't notify
|
||||||
|
A03CACFD7123AF443A3A185298A8A46921C8DDEF,-1,0,0,0
|
||||||
|
# This is jthan's key.
|
||||||
|
# assign full trust, push to keyserver, casual checking, notify
|
||||||
|
EFD9413B17293AFDFE6EA6F1402A088DEDF104CB,full,true,casual,yes
|
||||||
|
# This is paden's key.
|
||||||
|
# assign Marginal trust, push to keyserver, casual checking, notify
|
||||||
|
6FA8AE12AEC90B035EEE444FE70457341A63E830,2,True,Casual,True
|
||||||
|
# This is the email for the Sysadministrivia serverkey.
|
||||||
|
# Assign full trust, push to keyserver, careful checking, don't notify
|
||||||
|
<admin@sysadministrivia.com>, full, yes, careful, false
|
||||||
|
15
gpg/kant/docs/README
Normal file
15
gpg/kant/docs/README
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
GENERATING THE MAN PAGE:
|
||||||
|
If you have asciidoctor installed, you can generate the manpage one of two ways.
|
||||||
|
|
||||||
|
The first way:
|
||||||
|
|
||||||
|
asciidoctor -b manpage kant.1.adoc -o- | groff -Tascii -man | gz -c > kant.1.gz
|
||||||
|
|
||||||
|
This will generate a fixed-width man page.
|
||||||
|
|
||||||
|
|
||||||
|
The second way (recommended):
|
||||||
|
|
||||||
|
asciidoctor -b manpage kant.1.adoc -o- | gz -c > kant.1.gz
|
||||||
|
|
||||||
|
This will generate a dynamic-width man page. Most modern versions of man want this version.
|
||||||
46
gpg/kant/docs/REF.args.struct.txt
Normal file
46
gpg/kant/docs/REF.args.struct.txt
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
The __init__() function of kant.SigSession() takes a single argument: args.
|
||||||
|
|
||||||
|
it should be a dict, structured like this:
|
||||||
|
|
||||||
|
{'batch': False,
|
||||||
|
'checklevel': None,
|
||||||
|
'gpgdir': '/home/bts/.gnupg',
|
||||||
|
'keys': 'EFD9413B17293AFDFE6EA6F1402A088DEDF104CB,admin@sysadministrivia.com',
|
||||||
|
'keyservers': 'hkp://sks.mirror.square-r00t.net:11371,hkps://hkps.pool.sks-keyservers.net:443,http://pgp.mit.edu:80',
|
||||||
|
'local': 'false',
|
||||||
|
'msmtp_profile': None,
|
||||||
|
'notify': True,
|
||||||
|
'sigkey': '748231EBCBD808A14F5E85D28C004C2F93481F6B',
|
||||||
|
'testkeyservers': False,
|
||||||
|
'trustlevel': None}
|
||||||
|
|
||||||
|
The gpgdir, sigkey, and keyservers are set from system defaults in kant.parseArgs() if it's run interactively.
|
||||||
|
This *may* be reworked in the future to provide a mechanism for external calls to kant.SigSession() but for now,
|
||||||
|
it's up to you to provide all the data in the dict in the above format.
|
||||||
|
|
||||||
|
It will then internally verify these items and do various conversions, so that self.args becomes this:
|
||||||
|
(Note that some keys, such as "local", are validated and converted to appropriate values later on
|
||||||
|
e.g. 'false' => False)
|
||||||
|
|
||||||
|
{'batch': False,
|
||||||
|
'checklevel': None,
|
||||||
|
'gpgdir': '/home/bts/.gnupg',
|
||||||
|
'keys': ['EFD9413B17293AFDFE6EA6F1402A088DEDF104CB',
|
||||||
|
'admin@sysadministrivia.com'],
|
||||||
|
'keyservers': [{'port': [11371, ['tcp', 'udp']],
|
||||||
|
'proto': 'hkp',
|
||||||
|
'server': 'sks.mirror.square-r00t.net'},
|
||||||
|
{'port': [443, ['tcp']],
|
||||||
|
'proto': 'hkps',
|
||||||
|
'server': 'hkps.pool.sks-keyservers.net'},
|
||||||
|
{'port': [80, ['tcp']],
|
||||||
|
'proto': 'http',
|
||||||
|
'server': 'pgp.mit.edu'}],
|
||||||
|
'local': 'false',
|
||||||
|
'msmtp_profile': None,
|
||||||
|
'notify': True,
|
||||||
|
'rcpts': {'EFD9413B17293AFDFE6EA6F1402A088DEDF104CB': {'type': 'fpr'},
|
||||||
|
'admin@sysadministrivia.com': {'type': 'email'}},
|
||||||
|
'sigkey': '748231EBCBD808A14F5E85D28C004C2F93481F6B',
|
||||||
|
'testkeyservers': False,
|
||||||
|
'trustlevel': None}
|
||||||
33
gpg/kant/docs/REF.funcs.struct.txt
Normal file
33
gpg/kant/docs/REF.funcs.struct.txt
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
The following functions are available within the SigSession() class:
|
||||||
|
|
||||||
|
getTpls()
|
||||||
|
Get the user-specified templates if they exist, otherwise set up stock ones.
|
||||||
|
|
||||||
|
modifyDirmngr(op)
|
||||||
|
*op* can be either:
|
||||||
|
new/start/replace - modify dirmngr to use the runtime-specified keyserver(s)
|
||||||
|
old/stop/restore - modify dirmngr back to the keyservers that were defined before modification
|
||||||
|
|
||||||
|
buildKeys()
|
||||||
|
build out the keys dict (see REF.keys.struct.txt).
|
||||||
|
|
||||||
|
getKeys()
|
||||||
|
fetch keys in the keys dict (see REF.keys.struct.txt) from a keyserver if they aren't found in the local keyring.
|
||||||
|
|
||||||
|
trustKeys()
|
||||||
|
set up trusts for the keys in the keys dict (see REF.keys.struct.txt). prompts for each trust not found/specified at runtime.
|
||||||
|
|
||||||
|
sigKeys()
|
||||||
|
sign keys in the keys dict (see REF.keys.struct.txt), either exportable or local depending on runtime specification.
|
||||||
|
|
||||||
|
pushKeys()
|
||||||
|
push keys in the keys dict (see REF.keys.struct.txt) to the keyservers specified at runtime (as long as they weren't specified to be local/non-exportable signatures; then we don't bother).
|
||||||
|
|
||||||
|
sendMails()
|
||||||
|
send emails to each of the recipients specified in the keys dict (see REF.keys.struct.txt).
|
||||||
|
|
||||||
|
serverParser(uri)
|
||||||
|
returns a dict of a keyserver URI broken up into separate components easier for parsing.
|
||||||
|
|
||||||
|
verifyArgs(locargs)
|
||||||
|
does some verifications, classifies certain data, calls serverParser(), etc.
|
||||||
127
gpg/kant/docs/REF.keys.struct.txt
Normal file
127
gpg/kant/docs/REF.keys.struct.txt
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
TYPES:
|
||||||
|
d = dict
|
||||||
|
l = list
|
||||||
|
s = string
|
||||||
|
i = int
|
||||||
|
b = binary (True/False)
|
||||||
|
o = object
|
||||||
|
|
||||||
|
- pkey's dict key is the 40-char key ID of the primary key
|
||||||
|
- "==>" indicates the next item is a dict and the current item may contain one or more elements of the same format,
|
||||||
|
"++>" is a list,
|
||||||
|
"-->" is a "flat" item (string, object, int, etc.)
|
||||||
|
-"status" is one of "an UPGRADE", "a DOWNGRADE", or "a NEW TRUST".
|
||||||
|
|
||||||
|
keys(d) ==> (40-char key ID)(s) ==> pkey(d) --> email(s)
|
||||||
|
--> name(s)
|
||||||
|
--> creation (o, datetime)
|
||||||
|
--> key(o, gpg)
|
||||||
|
--> trust(i)
|
||||||
|
--> check(i)
|
||||||
|
--> local(b)
|
||||||
|
--> notify(b)
|
||||||
|
==> subkeys(d) ==> (40-char key ID)(s) --> creation
|
||||||
|
--> change(b)
|
||||||
|
--> sign(b)
|
||||||
|
--> status(s)
|
||||||
|
==> uids(d) ==> email(s) --> name(s)
|
||||||
|
--> comment(s)
|
||||||
|
--> email(s)
|
||||||
|
--> updated(o, datetime)*
|
||||||
|
|
||||||
|
* For many keys, this is unset. In-code, this is represented by having a timestamp of 0, or a
|
||||||
|
datetime object matching UNIX epoch. This is converted to a string, "Never/unknown".
|
||||||
|
|
||||||
|
for email templates, they are looped over for each key dict as "key".
|
||||||
|
so for example, instead of specifying "keys['748231EBCBD808A14F5E85D28C004C2F93481F6B']['pkey']['name']",
|
||||||
|
you instead should specify "key['pkey']['name']". To get the name of e.g. the second uid,
|
||||||
|
you'd use "key['uids'][(uid email)]['name'].
|
||||||
|
|
||||||
|
e.g. in the code, it's this:
|
||||||
|
{'748231EBCBD808A14F5E85D28C004C2F93481F6B': {'change': None,
|
||||||
|
'check': 0,
|
||||||
|
'local': False,
|
||||||
|
'notify': True,
|
||||||
|
'pkey': {'creation': '2013-12-10 '
|
||||||
|
'08:35:52',
|
||||||
|
'email': 'brent.saner@gmail.com',
|
||||||
|
'key': '<GPGME object>',
|
||||||
|
'name': 'Brent Timothy '
|
||||||
|
'Saner'},
|
||||||
|
'sign': True,
|
||||||
|
'status': None,
|
||||||
|
'subkeys': {'748231EBCBD808A14F5E85D28C004C2F93481F6B': '2013-12-10 '
|
||||||
|
'08:35:52'},
|
||||||
|
'trust': 2,
|
||||||
|
'uids': {'brent.saner@gmail.com': {'comment': '',
|
||||||
|
'name': 'Brent '
|
||||||
|
'Timothy '
|
||||||
|
'Saner',
|
||||||
|
'updated': 'Never/unknown'},
|
||||||
|
'bts@square-r00t.net': {'comment': 'http://www.square-r00t.net',
|
||||||
|
'name': 'Brent '
|
||||||
|
'S.',
|
||||||
|
'updated': 'Never/unknown'},
|
||||||
|
'r00t@sysadministrivia.com': {'comment': 'https://sysadministrivia.com',
|
||||||
|
'name': 'r00t^2',
|
||||||
|
'updated': 'Never/unknown'},
|
||||||
|
'squarer00t@keybase.io': {'comment': '',
|
||||||
|
'name': 'keybase.io/squarer00t',
|
||||||
|
'updated': 'Never/unknown'}}}}
|
||||||
|
but this is passed to the email template as:
|
||||||
|
{'change': None,
|
||||||
|
'check': 0,
|
||||||
|
'local': False,
|
||||||
|
'notify': True,
|
||||||
|
'pkey': {'creation': '2013-12-10 08:35:52',
|
||||||
|
'email': 'brent.saner@gmail.com',
|
||||||
|
'key': '<GPGME object>',
|
||||||
|
'name': 'Brent Timothy Saner'},
|
||||||
|
'sign': True,
|
||||||
|
'status': None,
|
||||||
|
'subkeys': {'748231EBCBD808A14F5E85D28C004C2F93481F6B': '2013-12-10 08:35:52'},
|
||||||
|
'trust': 2,
|
||||||
|
'uids': {'brent.saner@gmail.com': {'comment': '',
|
||||||
|
'name': 'Brent Timothy Saner',
|
||||||
|
'updated': '1970-01-01 00:00:00'},
|
||||||
|
'bts@square-r00t.net': {'comment': 'http://www.square-r00t.net',
|
||||||
|
'name': 'Brent S.',
|
||||||
|
'updated': 'Never/unknown'},
|
||||||
|
'r00t@sysadministrivia.com': {'comment': 'https://sysadministrivia.com',
|
||||||
|
'name': 'r00t^2',
|
||||||
|
'updated': 'Never/unknown'},
|
||||||
|
'squarer00t@keybase.io': {'comment': '',
|
||||||
|
'name': 'keybase.io/squarer00t',
|
||||||
|
'updated': 'Never/unknown'}}}
|
||||||
|
|
||||||
|
(because the emails are iterated through the keys).
|
||||||
|
|
||||||
|
|
||||||
|
the same structure is available via the "mykey" dictionary (e.g. to get the key ID of *your* key,
|
||||||
|
you can use "mykey['subkeys'][0][0]"):
|
||||||
|
|
||||||
|
{'change': False,
|
||||||
|
'check': None,
|
||||||
|
'local': False,
|
||||||
|
'notify': False,
|
||||||
|
'pkey': {'creation': '2017-09-07 20:54:31',
|
||||||
|
'email': 'test@test.com',
|
||||||
|
'key': '<GPGME object>',
|
||||||
|
'name': 'test user'},
|
||||||
|
'sign': False,
|
||||||
|
'status': None,
|
||||||
|
'subkeys': {'1CD9200637EC587D1F8EB94198748C2879CCE88D': '2017-09-07 20:54:31',
|
||||||
|
'2805EC3D90E2229795AFB73FF85BC40E6E17F339': '2017-09-07 20:54:31'},
|
||||||
|
'trust': 'ultimate',
|
||||||
|
'uids': {'test@test.com': {'comment': 'this is a testing junk key. DO NOT '
|
||||||
|
'IMPORT/SIGN/TRUST.',
|
||||||
|
'name': 'test user',
|
||||||
|
'updated': 'Never/unknown'}}}
|
||||||
|
|
||||||
|
|
||||||
|
you also have the following variables/lists/etc. available for templates (via the Jinja2 templating syntax[0]):
|
||||||
|
- "keyservers", a list of keyservers set.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[0] http://jinja.pocoo.org/docs/2.9/templates/
|
||||||
257
gpg/kant/docs/kant.1
Normal file
257
gpg/kant/docs/kant.1
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
'\" t
|
||||||
|
.\" Title: kant
|
||||||
|
.\" Author: Brent Saner
|
||||||
|
.\" Generator: Asciidoctor 1.5.6.1
|
||||||
|
.\" Date: 2017-09-21
|
||||||
|
.\" Manual: KANT - Keysigning and Notification Tool
|
||||||
|
.\" Source: KANT
|
||||||
|
.\" Language: English
|
||||||
|
.\"
|
||||||
|
.TH "KANT" "1" "2017-09-21" "KANT" "KANT \- Keysigning and Notification Tool"
|
||||||
|
.ie \n(.g .ds Aq \(aq
|
||||||
|
.el .ds Aq '
|
||||||
|
.ss \n[.ss] 0
|
||||||
|
.nh
|
||||||
|
.ad l
|
||||||
|
.de URL
|
||||||
|
\\$2 \(laURL: \\$1 \(ra\\$3
|
||||||
|
..
|
||||||
|
.if \n[.g] .mso www.tmac
|
||||||
|
.LINKSTYLE blue R < >
|
||||||
|
.SH "NAME"
|
||||||
|
kant \- Sign GnuPG/OpenPGP/PGP keys and notify the key owner(s)
|
||||||
|
.SH "SYNOPSIS"
|
||||||
|
.sp
|
||||||
|
\fBkant\fP [\fIOPTION\fP] \-k/\-\-key \fI<KEY_IDS|BATCHFILE>\fP
|
||||||
|
.SH "OPTIONS"
|
||||||
|
.sp
|
||||||
|
Keysigning (and keysigning parties) can be a lot of fun, and can offer someone with new keys a way into the WoT (Web\-of\-Trust).
|
||||||
|
Unfortunately, they can be intimidating to those new to the experience.
|
||||||
|
This tool offers a simple and easy\-to\-use interface to sign public keys (normal, local\-only, and/or non\-exportable),
|
||||||
|
set owner trust, specify level of checking done, and push the signatures to a keyserver. It even supports batch operation via a CSV file.
|
||||||
|
On successful completion, information about the keys that were signed and the key used to sign are saved to ~/.kant/cache/YYYY.MM.DD_HH.MM.SS.
|
||||||
|
.sp
|
||||||
|
\fB\-h\fP, \fB\-\-help\fP
|
||||||
|
.RS 4
|
||||||
|
Display brief help/usage and exit.
|
||||||
|
.RE
|
||||||
|
.sp
|
||||||
|
\fB\-k\fP \fIKEY_IDS|BATCHFILE\fP, \fB\-\-key\fP \fIKEY_IDS|BATCHFILE\fP
|
||||||
|
.RS 4
|
||||||
|
A single or comma\-separated list of key IDs (see \fBKEY ID FORMAT\fP) to sign, trust, and notify. Can also be an email address.
|
||||||
|
If \fB\-b\fP/\fB\-\-batch\fP is specified, this should instead be a path to the batch file (see \fBBATCHFILE/Format\fP).
|
||||||
|
.RE
|
||||||
|
.sp
|
||||||
|
\fB\-K\fP \fIKEY_ID\fP, \fB\-\-sigkey\fP \fIKEY_ID\fP
|
||||||
|
.RS 4
|
||||||
|
The key to use when signing other keys (see \fBKEY ID FORMAT\fP). The default key is automatically determined at runtime
|
||||||
|
(it will be displayed in \fB\-h\fP/\fB\-\-help\fP output).
|
||||||
|
.RE
|
||||||
|
.sp
|
||||||
|
\fB\-t\fP \fITRUSTLEVEL\fP, \fB\-\-trust\fP \fITRUSTLEVEL\fP
|
||||||
|
.RS 4
|
||||||
|
The trust level to automatically apply to all keys (if not specified, KANT will prompt for each key).
|
||||||
|
See \fBBATCHFILE/TRUSTLEVEL\fP for trust level notations.
|
||||||
|
.RE
|
||||||
|
.sp
|
||||||
|
\fB\-c\fP \fICHECKLEVEL\fP, \fB\-\-check\fP \fICHECKLEVEL\fP
|
||||||
|
.RS 4
|
||||||
|
The level of checking that was done to confirm the validity of ownership for all keys being signed. If not specified,
|
||||||
|
the default is for KANT to prompt for each key we sign. See \fBBATCHFILE/CHECKLEVEL\fP for check level notations.
|
||||||
|
.RE
|
||||||
|
.sp
|
||||||
|
\fB\-l\fP \fILOCAL\fP, \fB\-\-local\fP \fILOCAL\fP
|
||||||
|
.RS 4
|
||||||
|
If specified, make the signature(s) local\-only (i.e. non\-exportable, don\(cqt push to a keyserver).
|
||||||
|
See \fBBATCHFILE/LOCAL\fP for more information on local signatures.
|
||||||
|
.RE
|
||||||
|
.sp
|
||||||
|
\fB\-n\fP, \fB\-\-no\-notify\fP
|
||||||
|
.RS 4
|
||||||
|
This requires some explanation. If you have MSMTP[1] installed and configured for the currently active user,
|
||||||
|
then we will send out emails to recipients letting them know we have signed their key. However, if MSMTP is installed and configured
|
||||||
|
but this flag is given, then we will NOT attempt to send emails. See \fBMAIL\fP for more information.
|
||||||
|
.RE
|
||||||
|
.sp
|
||||||
|
\fB\-s\fP \fIKEYSERVER(S)\fP, \fB\-\-keyservers\fP \fIKEYSERVER(S)\fP
|
||||||
|
.RS 4
|
||||||
|
The comma\-separated keyserver(s) to push to. The default keyserver list is automatically generated at runtime.
|
||||||
|
.RE
|
||||||
|
.sp
|
||||||
|
\fB\-m\fP \fIPROFILE\fP, \fB\-\-msmtp\-profile\fP \fIPROFILE\fP
|
||||||
|
.RS 4
|
||||||
|
If specified, use the msmtp profile named \fIPROFILE\fP. If this is not specified, KANT first looks for an msmtp configuration named KANT (case\-sensitive). If it doesn\(cqt find one, it will use the profile specified as the default profile in your msmtp configuration. See \fBMAIL\fP for more information.
|
||||||
|
.RE
|
||||||
|
.sp
|
||||||
|
\fB\-b\fP, \fB\-\-batch\fP
|
||||||
|
.RS 4
|
||||||
|
If specified, operate in batch mode. See \fBBATCHFILE\fP for more information.
|
||||||
|
.RE
|
||||||
|
.sp
|
||||||
|
\fB\-D\fP \fIGPGDIR\fP, \fB\-\-gpgdir\fP \fIGPGDIR\fP
|
||||||
|
.RS 4
|
||||||
|
The GnuPG configuration directory to use (containing your keys, etc.). The default is automatically generated at runtime,
|
||||||
|
but will probably be \fB/home/<yourusername>/.gnupg\fP or similar.
|
||||||
|
.RE
|
||||||
|
.sp
|
||||||
|
\fB\-T\fP, \fB\-\-testkeyservers\fP
|
||||||
|
.RS 4
|
||||||
|
If specified, initiate a basic test connection with each set keyserver before anything else. Disabled by default.
|
||||||
|
.RE
|
||||||
|
.SH "KEY ID FORMAT"
|
||||||
|
.sp
|
||||||
|
Key IDs can be specified in one of two ways. The first (and preferred) way is to use the full 160\-bit (40\-character, hexadecimal) key ID.
|
||||||
|
A little known fact is the fingerprint of a key:
|
||||||
|
.sp
|
||||||
|
\fBDEAD BEEF DEAD BEEF DEAD BEEF DEAD BEEF DEAD BEEF\fP
|
||||||
|
.sp
|
||||||
|
is actually the full key ID of the primary key; i.e.:
|
||||||
|
.sp
|
||||||
|
\fBDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF\fP
|
||||||
|
.sp
|
||||||
|
The second way to specify a key, as far as KANT is concerned, is to use an email address.
|
||||||
|
Do note that if more than one key is found that matches the email address given (and they usually are), you will be prompted to select the specific
|
||||||
|
correct key ID anyways so it\(cqs usually a better idea to have the owner present their full key ID/fingerprint right from the get\-go.
|
||||||
|
.SH "BATCHFILE"
|
||||||
|
.SS "Format"
|
||||||
|
.sp
|
||||||
|
The batch file is a CSV\-formatted (comma\-delimited) file containing keys to sign and other information about them. It keeps the following format:
|
||||||
|
.sp
|
||||||
|
\fBKEY_ID,TRUSTLEVEL,LOCAL,CHECKLEVEL,NOTIFY\fP
|
||||||
|
.sp
|
||||||
|
For more information on each column, reference the appropriate sub\-section below.
|
||||||
|
.SS "KEY_ID"
|
||||||
|
.sp
|
||||||
|
See \fBKEY ID FORMAT\fP.
|
||||||
|
.SS "TRUSTLEVEL"
|
||||||
|
.sp
|
||||||
|
The \fITRUSTLEVEL\fP is specified by the following levels (you can use either the numeric or string representation):
|
||||||
|
.sp
|
||||||
|
.if n \{\
|
||||||
|
.RS 4
|
||||||
|
.\}
|
||||||
|
.nf
|
||||||
|
\fB\-1 = Never
|
||||||
|
0 = Unknown
|
||||||
|
1 = Untrusted
|
||||||
|
2 = Marginal
|
||||||
|
3 = Full
|
||||||
|
4 = Ultimate\fP
|
||||||
|
.fi
|
||||||
|
.if n \{\
|
||||||
|
.RE
|
||||||
|
.\}
|
||||||
|
.sp
|
||||||
|
It is how much trust to assign to a key, and the signatures that key makes on other keys.[2]
|
||||||
|
.SS "LOCAL"
|
||||||
|
.sp
|
||||||
|
Whether or not to push to a keyserver. It can be either the numeric or string representation of the following:
|
||||||
|
.sp
|
||||||
|
.if n \{\
|
||||||
|
.RS 4
|
||||||
|
.\}
|
||||||
|
.nf
|
||||||
|
\fB0 = False
|
||||||
|
1 = True\fP
|
||||||
|
.fi
|
||||||
|
.if n \{\
|
||||||
|
.RE
|
||||||
|
.\}
|
||||||
|
.sp
|
||||||
|
If \fB1/True\fP, KANT will sign the key with a local signature (and the signature will not be pushed to a keyserver or be exportable).[3]
|
||||||
|
.SS "CHECKLEVEL"
|
||||||
|
.sp
|
||||||
|
The amount of checking that has been done to confirm that the owner of the key is who they say they are and that the key matches their provided information.
|
||||||
|
It can be either the numeric or string representation of the following:
|
||||||
|
.sp
|
||||||
|
.if n \{\
|
||||||
|
.RS 4
|
||||||
|
.\}
|
||||||
|
.nf
|
||||||
|
\fB0 = Unknown
|
||||||
|
1 = None
|
||||||
|
2 = Casual
|
||||||
|
3 = Careful\fP
|
||||||
|
.fi
|
||||||
|
.if n \{\
|
||||||
|
.RE
|
||||||
|
.\}
|
||||||
|
.sp
|
||||||
|
It is up to you to determine the classification of the amount of checking you have done, but the following is recommended (it is the policy
|
||||||
|
the author follows):
|
||||||
|
.sp
|
||||||
|
.if n \{\
|
||||||
|
.RS 4
|
||||||
|
.\}
|
||||||
|
.nf
|
||||||
|
\fBUnknown:\fP The key is unknown and has not been reviewed
|
||||||
|
|
||||||
|
\fBNone:\fP The key has been signed, but no confirmation of the
|
||||||
|
ownership of the key has been performed (typically
|
||||||
|
a local signature)
|
||||||
|
|
||||||
|
\fBCasual:\fP The key has been presented and the owner is either
|
||||||
|
known to the signer or they have provided some form
|
||||||
|
of non\-government\-issued identification or other
|
||||||
|
proof (website, Keybase.io, etc.)
|
||||||
|
|
||||||
|
\fBCareful:\fP The same as \fBCasual\fP requirements but they have
|
||||||
|
provided a government\-issued ID and all information
|
||||||
|
matches
|
||||||
|
.fi
|
||||||
|
.if n \{\
|
||||||
|
.RE
|
||||||
|
.\}
|
||||||
|
.sp
|
||||||
|
It\(cqs important to check each key you sign carefully. Failure to do so may hurt others\(aq trust in your key.[4]
|
||||||
|
.SH "MAIL"
|
||||||
|
.sp
|
||||||
|
The mailing feature of KANT is very handy; it will let you send notifications to the owners of the keys you sign. This is encouraged because: 1.) it\(cqs courteous to let them know where they can fetch the signature you just made on their key, 2.) it\(cqs courteous to let them know if you did/did not push to a keyserver (some people don\(cqt want their keys pushed, and it\(cqs a good idea to respect that wish), and 3.) the mailer also attaches the pubkey for the key you used to sign with, in case your key isn\(cqt on a keyserver, etc.
|
||||||
|
.sp
|
||||||
|
However, in order to do this since many ISPs block outgoing mail, one would typically use something like msmtp (http://msmtp.sourceforge.net/). Note that you don\(cqt even need msmtp to be installed, you just need to have msmtp configuration files set up via either /etc/msmtprc or ~/.msmtprc. KANT will parse these configuration files and use a purely pythonic implementation for sending the emails (see \fBSENDING\fP).
|
||||||
|
.sp
|
||||||
|
It supports templated mail messages as well (see \fBTEMPLATES\fP). It sends a MIME multipart email, in both plaintext and HTML formatting, for mail clients that may only support one or the other. It will also sign the email message using your signing key (see \fB\-K\fP, \fB\-\-sigkey\fP) and attach a binary (.gpg) and ASCII\-armored (.asc) export of your pubkey.
|
||||||
|
.SS "SENDING"
|
||||||
|
.sp
|
||||||
|
KANT first looks for ~/.msmtprc and, if not found, will look for /etc/msmtprc. If neither are found, mail notifications will not be sent and it will be up to you to contact the key owner(s) and let them know you have signed their key(s). If it does find either, it will use the first configuration file it finds and first look for a profile called "KANT" (without quotation marks). If this is not found, it will use whatever profile is specified for as the default profile (e.g. \fBaccount default: someprofilename\fP in the msmtprc).
|
||||||
|
.SS "TEMPLATES"
|
||||||
|
.sp
|
||||||
|
KANT, on first run (even with a \fB\-h\fP/\fB\-\-help\fP execution), will create the default email templates (which can be found as ~/.kant/email.html.j2 and ~/.kant/email.plain.j2). These support templating via Jinja2 (http://jinja.pocoo.org/docs/2.9/templates/), and the following variables/dictionaries/lists are exported for your use:
|
||||||
|
.sp
|
||||||
|
.if n \{\
|
||||||
|
.RS 4
|
||||||
|
.\}
|
||||||
|
.nf
|
||||||
|
* \fBkey\fP \- a dictionary of information about the recipient\(aqs key (see docs/REF.keys.struct.txt)
|
||||||
|
* \fBmykey\fP \- a dictionary of information about your key (see docs/REF.keys.struct.txt)
|
||||||
|
* \fBkeyservers\fP \- a list of keyservers that the key has been pushed to (if an exportable/non\-local signature was made)
|
||||||
|
.fi
|
||||||
|
.if n \{\
|
||||||
|
.RE
|
||||||
|
.\}
|
||||||
|
.sp
|
||||||
|
And of course you can set your own variables inside the template as well (http://jinja.pocoo.org/docs/2.9/templates/#assignments).
|
||||||
|
.SH "SEE ALSO"
|
||||||
|
.sp
|
||||||
|
gpg(1), gpgconf(1), msmtp(1)
|
||||||
|
.SH "RESOURCES"
|
||||||
|
.sp
|
||||||
|
\fBAuthor\(cqs web site:\fP https://square\-r00t.net/
|
||||||
|
.sp
|
||||||
|
\fBAuthor\(cqs GPG information:\fP https://square\-r00t.net/gpg\-info
|
||||||
|
.SH "COPYING"
|
||||||
|
.sp
|
||||||
|
Copyright (C) 2017 Brent Saner.
|
||||||
|
.sp
|
||||||
|
Free use of this software is granted under the terms of the GPLv3 License.
|
||||||
|
.SH "NOTES"
|
||||||
|
1. http://msmtp.sourceforge.net/
|
||||||
|
2. For more information on trust levels and the Web of Trust, see: https://www.gnupg.org/gph/en/manual/x334.html and https://www.gnupg.org/gph/en/manual/x547.html
|
||||||
|
3. For more information on pushing to keyservers and local signatures, see: https://www.gnupg.org/gph/en/manual/r899.html#LSIGN and https://lists.gnupg.org/pipermail/gnupg-users/2007-January/030242.html
|
||||||
|
4. GnuPG documentation refers to this as "validity"; see https://www.gnupg.org/gph/en/manual/x334.html
|
||||||
|
.SH "AUTHOR(S)"
|
||||||
|
.sp
|
||||||
|
\fBBrent Saner\fP
|
||||||
|
.RS 4
|
||||||
|
Author(s).
|
||||||
|
.RE
|
||||||
195
gpg/kant/docs/kant.1.adoc
Normal file
195
gpg/kant/docs/kant.1.adoc
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
= kant(1)
|
||||||
|
Brent Saner
|
||||||
|
v1.0.0
|
||||||
|
:doctype: manpage
|
||||||
|
:manmanual: KANT - Keysigning and Notification Tool
|
||||||
|
:mansource: KANT
|
||||||
|
:man-linkstyle: pass:[blue R < >]
|
||||||
|
|
||||||
|
== NAME
|
||||||
|
|
||||||
|
KANT - Sign GnuPG/OpenPGP/PGP keys and notify the key owner(s)
|
||||||
|
|
||||||
|
== SYNOPSIS
|
||||||
|
|
||||||
|
*kant* [_OPTION_] -k/--key _<KEY_IDS|BATCHFILE>_
|
||||||
|
|
||||||
|
== OPTIONS
|
||||||
|
|
||||||
|
Keysigning (and keysigning parties) can be a lot of fun, and can offer someone with new keys a way into the WoT (Web-of-Trust).
|
||||||
|
Unfortunately, they can be intimidating to those new to the experience.
|
||||||
|
This tool offers a simple and easy-to-use interface to sign public keys (normal, local-only, and/or non-exportable),
|
||||||
|
set owner trust, specify level of checking done, and push the signatures to a keyserver. It even supports batch operation via a CSV file.
|
||||||
|
On successful completion, information about the keys that were signed and the key used to sign are saved to ~/.kant/cache/YYYY.MM.DD_HH.MM.SS.
|
||||||
|
|
||||||
|
*-h*, *--help*::
|
||||||
|
Display brief help/usage and exit.
|
||||||
|
|
||||||
|
*-k* _KEY_IDS|BATCHFILE_, *--key* _KEY_IDS|BATCHFILE_::
|
||||||
|
A single or comma-separated list of key IDs (see *KEY ID FORMAT*) to sign, trust, and notify. Can also be an email address.
|
||||||
|
If *-b*/*--batch* is specified, this should instead be a path to the batch file (see *BATCHFILE/Format*).
|
||||||
|
|
||||||
|
*-K* _KEY_ID_, *--sigkey* _KEY_ID_::
|
||||||
|
The key to use when signing other keys (see *KEY ID FORMAT*). The default key is automatically determined at runtime
|
||||||
|
(it will be displayed in *-h*/*--help* output).
|
||||||
|
|
||||||
|
*-t* _TRUSTLEVEL_, *--trust* _TRUSTLEVEL_::
|
||||||
|
The trust level to automatically apply to all keys (if not specified, KANT will prompt for each key).
|
||||||
|
See *BATCHFILE/TRUSTLEVEL* for trust level notations.
|
||||||
|
|
||||||
|
*-c* _CHECKLEVEL_, *--check* _CHECKLEVEL_::
|
||||||
|
The level of checking that was done to confirm the validity of ownership for all keys being signed. If not specified,
|
||||||
|
the default is for KANT to prompt for each key we sign. See *BATCHFILE/CHECKLEVEL* for check level notations.
|
||||||
|
|
||||||
|
*-l* _LOCAL_, *--local* _LOCAL_::
|
||||||
|
If specified, make the signature(s) local-only (i.e. non-exportable, don't push to a keyserver).
|
||||||
|
See *BATCHFILE/LOCAL* for more information on local signatures.
|
||||||
|
|
||||||
|
*-n*, *--no-notify*::
|
||||||
|
This requires some explanation. If you have MSMTPfootnote:[\http://msmtp.sourceforge.net/] installed and configured for the currently active user,
|
||||||
|
then we will send out emails to recipients letting them know we have signed their key. However, if MSMTP is installed and configured
|
||||||
|
but this flag is given, then we will NOT attempt to send emails. See *MAIL* for more information.
|
||||||
|
|
||||||
|
*-s* _KEYSERVER(S)_, *--keyservers* _KEYSERVER(S)_::
|
||||||
|
The comma-separated keyserver(s) to push to. The default keyserver list is automatically generated at runtime.
|
||||||
|
|
||||||
|
*-m* _PROFILE_, *--msmtp-profile* _PROFILE_::
|
||||||
|
If specified, use the msmtp profile named _PROFILE_. If this is not specified, KANT first looks for an msmtp configuration named KANT (case-sensitive). If it doesn't find one, it will use the profile specified as the default profile in your msmtp configuration. See *MAIL* for more information.
|
||||||
|
|
||||||
|
*-b*, *--batch*::
|
||||||
|
If specified, operate in batch mode. See *BATCHFILE* for more information.
|
||||||
|
|
||||||
|
*-D* _GPGDIR_, *--gpgdir* _GPGDIR_::
|
||||||
|
The GnuPG configuration directory to use (containing your keys, etc.). The default is automatically generated at runtime,
|
||||||
|
but will probably be */home/<yourusername>/.gnupg* or similar.
|
||||||
|
|
||||||
|
*-T*, *--testkeyservers*::
|
||||||
|
If specified, initiate a basic test connection with each set keyserver before anything else. Disabled by default.
|
||||||
|
|
||||||
|
== KEY ID FORMAT
|
||||||
|
Key IDs can be specified in one of two ways. The first (and preferred) way is to use the full 160-bit (40-character, hexadecimal) key ID.
|
||||||
|
A little known fact is the fingerprint of a key:
|
||||||
|
|
||||||
|
*DEAD BEEF DEAD BEEF DEAD BEEF DEAD BEEF DEAD BEEF*
|
||||||
|
|
||||||
|
is actually the full key ID of the primary key; i.e.:
|
||||||
|
|
||||||
|
*DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF*
|
||||||
|
|
||||||
|
The second way to specify a key, as far as KANT is concerned, is to use an email address.
|
||||||
|
Do note that if more than one key is found that matches the email address given (and they usually are), you will be prompted to select the specific
|
||||||
|
correct key ID anyways so it's usually a better idea to have the owner present their full key ID/fingerprint right from the get-go.
|
||||||
|
|
||||||
|
== BATCHFILE
|
||||||
|
|
||||||
|
=== Format
|
||||||
|
The batch file is a CSV-formatted (comma-delimited) file containing keys to sign and other information about them. It keeps the following format:
|
||||||
|
|
||||||
|
*KEY_ID,TRUSTLEVEL,LOCAL,CHECKLEVEL,NOTIFY*
|
||||||
|
|
||||||
|
For more information on each column, reference the appropriate sub-section below.
|
||||||
|
|
||||||
|
=== KEY_ID
|
||||||
|
See *KEY ID FORMAT*.
|
||||||
|
|
||||||
|
=== TRUSTLEVEL
|
||||||
|
The _TRUSTLEVEL_ is specified by the following levels (you can use either the numeric or string representation):
|
||||||
|
|
||||||
|
[subs=+quotes]
|
||||||
|
....
|
||||||
|
*-1 = Never
|
||||||
|
0 = Unknown
|
||||||
|
1 = Untrusted
|
||||||
|
2 = Marginal
|
||||||
|
3 = Full
|
||||||
|
4 = Ultimate*
|
||||||
|
....
|
||||||
|
|
||||||
|
It is how much trust to assign to a key, and the signatures that key makes on other keys.footnote:[For more information
|
||||||
|
on trust levels and the Web of Trust, see: \https://www.gnupg.org/gph/en/manual/x334.html and \https://www.gnupg.org/gph/en/manual/x547.html]
|
||||||
|
|
||||||
|
=== LOCAL
|
||||||
|
Whether or not to push to a keyserver. It can be either the numeric or string representation of the following:
|
||||||
|
|
||||||
|
[subs=+quotes]
|
||||||
|
....
|
||||||
|
*0 = False
|
||||||
|
1 = True*
|
||||||
|
....
|
||||||
|
|
||||||
|
If *1/True*, KANT will sign the key with a local signature (and the signature will not be pushed to a keyserver or be exportable).footnote:[For
|
||||||
|
more information on pushing to keyservers and local signatures, see: \https://www.gnupg.org/gph/en/manual/r899.html#LSIGN and
|
||||||
|
\https://lists.gnupg.org/pipermail/gnupg-users/2007-January/030242.html]
|
||||||
|
|
||||||
|
=== CHECKLEVEL
|
||||||
|
The amount of checking that has been done to confirm that the owner of the key is who they say they are and that the key matches their provided information.
|
||||||
|
It can be either the numeric or string representation of the following:
|
||||||
|
|
||||||
|
[subs=+quotes]
|
||||||
|
....
|
||||||
|
*0 = Unknown
|
||||||
|
1 = None
|
||||||
|
2 = Casual
|
||||||
|
3 = Careful*
|
||||||
|
....
|
||||||
|
|
||||||
|
It is up to you to determine the classification of the amount of checking you have done, but the following is recommended (it is the policy
|
||||||
|
the author follows):
|
||||||
|
|
||||||
|
[subs=+quotes]
|
||||||
|
....
|
||||||
|
*Unknown:* The key is unknown and has not been reviewed
|
||||||
|
|
||||||
|
*None:* The key has been signed, but no confirmation of the
|
||||||
|
ownership of the key has been performed (typically
|
||||||
|
a local signature)
|
||||||
|
|
||||||
|
*Casual:* The key has been presented and the owner is either
|
||||||
|
known to the signer or they have provided some form
|
||||||
|
of non-government-issued identification or other
|
||||||
|
proof (website, Keybase.io, etc.)
|
||||||
|
|
||||||
|
*Careful:* The same as *Casual* requirements but they have
|
||||||
|
provided a government-issued ID and all information
|
||||||
|
matches
|
||||||
|
....
|
||||||
|
|
||||||
|
It's important to check each key you sign carefully. Failure to do so may hurt others' trust in your key.footnote:[GnuPG documentation refers
|
||||||
|
to this as "validity"; see \https://www.gnupg.org/gph/en/manual/x334.html]
|
||||||
|
|
||||||
|
== MAIL
|
||||||
|
The mailing feature of KANT is very handy; it will let you send notifications to the owners of the keys you sign. This is encouraged because: 1.) it's courteous to let them know where they can fetch the signature you just made on their key, 2.) it's courteous to let them know if you did/did not push to a keyserver (some people don't want their keys pushed, and it's a good idea to respect that wish), and 3.) the mailer also attaches the pubkey for the key you used to sign with, in case your key isn't on a keyserver, etc.
|
||||||
|
|
||||||
|
However, in order to do this since many ISPs block outgoing mail, one would typically use something like msmtp (\http://msmtp.sourceforge.net/). Note that you don't even need msmtp to be installed, you just need to have msmtp configuration files set up via either /etc/msmtprc or ~/.msmtprc. KANT will parse these configuration files and use a purely pythonic implementation for sending the emails (see *SENDING*).
|
||||||
|
|
||||||
|
It supports templated mail messages as well (see *TEMPLATES*). It sends a MIME multipart email, in both plaintext and HTML formatting, for mail clients that may only support one or the other. It will also sign the email message using your signing key (see *-K*, *--sigkey*) and attach a binary (.gpg) and ASCII-armored (.asc) export of your pubkey.
|
||||||
|
|
||||||
|
=== SENDING
|
||||||
|
KANT first looks for ~/.msmtprc and, if not found, will look for /etc/msmtprc. If neither are found, mail notifications will not be sent and it will be up to you to contact the key owner(s) and let them know you have signed their key(s). If it does find either, it will use the first configuration file it finds and first look for a profile called "KANT" (without quotation marks). If this is not found, it will use whatever profile is specified for as the default profile (e.g. *account default: someprofilename* in the msmtprc).
|
||||||
|
|
||||||
|
=== TEMPLATES
|
||||||
|
KANT, on first run (even with a *-h*/*--help* execution), will create the default email templates (which can be found as ~/.kant/email.html.j2 and ~/.kant/email.plain.j2). These support templating via Jinja2 (\http://jinja.pocoo.org/docs/2.9/templates/), and the following variables/dictionaries/lists are exported for your use:
|
||||||
|
|
||||||
|
[subs=+quotes]
|
||||||
|
....
|
||||||
|
* *key* - a dictionary of information about the recipient's key (see docs/REF.keys.struct.txt)
|
||||||
|
* *mykey* - a dictionary of information about your key (see docs/REF.keys.struct.txt)
|
||||||
|
* *keyservers* - a list of keyservers that the key has been pushed to (if an exportable/non-local signature was made)
|
||||||
|
....
|
||||||
|
|
||||||
|
And of course you can set your own variables inside the template as well (\http://jinja.pocoo.org/docs/2.9/templates/#assignments).
|
||||||
|
|
||||||
|
== SEE ALSO
|
||||||
|
gpg(1), gpgconf(1), msmtp(1)
|
||||||
|
|
||||||
|
== RESOURCES
|
||||||
|
|
||||||
|
*Author's web site:* \https://square-r00t.net/
|
||||||
|
|
||||||
|
*Author's GPG information:* \https://square-r00t.net/gpg-info
|
||||||
|
|
||||||
|
== COPYING
|
||||||
|
|
||||||
|
Copyright \(C) 2017 {author}.
|
||||||
|
|
||||||
|
Free use of this software is granted under the terms of the GPLv3 License.
|
||||||
961
gpg/kant/kant.py
Executable file
961
gpg/kant/kant.py
Executable file
@@ -0,0 +1,961 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import base64
|
||||||
|
import csv
|
||||||
|
import datetime
|
||||||
|
import json
|
||||||
|
import lzma
|
||||||
|
import operator
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import smtplib
|
||||||
|
import subprocess
|
||||||
|
from email.message import Message
|
||||||
|
from email.mime.application import MIMEApplication
|
||||||
|
from email.mime.multipart import MIMEMultipart
|
||||||
|
from email.mime.text import MIMEText
|
||||||
|
from functools import reduce
|
||||||
|
from io import BytesIO
|
||||||
|
from socket import *
|
||||||
|
import urllib.parse
|
||||||
|
import jinja2 # non-stdlib; Arch package is python-jinja2
|
||||||
|
import gpg # non-stdlib; Arch package is "python-gpgme" - see:
|
||||||
|
import gpg.constants # https://git.archlinux.org/svntogit/packages.git/tree/trunk/PKGBUILD?h=packages/gpgme and
|
||||||
|
import gpg.errors # https://gnupg.org/ftp/gcrypt/gpgme/ (incl. python bindings in build)
|
||||||
|
import pprint # development debug
|
||||||
|
|
||||||
|
|
||||||
|
class SigSession(object): # see docs/REFS.funcs.struct.txt
|
||||||
|
def __init__(self, args):
|
||||||
|
# These are the "stock" templates for emails. It's a PITA, but to save some space since we store them
|
||||||
|
# inline in here, they're XZ'd and base64'd.
|
||||||
|
self.email_tpl = {}
|
||||||
|
self.email_tpl['plain'] = ('/Td6WFoAAATm1rRGAgAhARwAAAAQz1jM4ATxAnZdACQZSZhvFgKNdKNXbSf05z0ZPvTvmdQ0mJQg' +
|
||||||
|
'atgzhPVeLKxz22bhxedC813X5I8Gn2g9q9Do2jPPgXOzysImWXoraY4mhz0BAo2Zx1u6AiQQLdN9' +
|
||||||
|
'/jwrDrUEtb8M/QzmRd+8JrYN8s8vhViJZARMNHYnPeQK5GYEoGZEQ8l2ULmpTjAn9edSnrMmNSb2' +
|
||||||
|
'EC86CuyhaWDPsQeIamWW1t+MWmgsggE3xKYADKXHMQyXvhv/TAn987dEbzmrkpg8PCjxWt1wKRAr' +
|
||||||
|
'siDpCGvXLiBwnDtN1D7ocwbZVKty2GELbYt0f0CT7n5Pyu9n0P7QMnErM38kLR1nReopQp41+CsG' +
|
||||||
|
'orb8EpGGVdFa7sSWSANQtGTjx/1JHecpkTN8xX4kAjMWKYujWlZi/HzN7y/W5GDJM3ycVEUTsDRV' +
|
||||||
|
'6AusncRBFbo4/+K6cn5WCrhqd5jY2vDJR6KcO0O3usHUMzvOF0S0CZhUbA3Mil5DmPwFrdFrESby' +
|
||||||
|
'O1xH3uvgHpA5X91qkpEajokOOkY3FZm0oeANh9AMoMfDFTuqi41Nq9Myk4VKNEfzioChn9IfFxX0' +
|
||||||
|
'Luw6OyXtWJdpe3BvO7pWazLhvdIY4poh9brvJ25cG1kDMOlmC3NEb+POeqQ5aUr4XaRqFstk3grb' +
|
||||||
|
'8EjiGBzg18uHsbhjyReXnZprJjwzWUdwpV6j+2JFI13UEp16oTyTwyhHdpAmAg+lQJQxtcMpnUeX' +
|
||||||
|
'/xBkQGs+rqe0e/i8ZQ80XsLAoScxUL+45v9vANYV+lCWRnm/2GZOtCFs1Cb4t9hOeV0P1cwxw7fG' +
|
||||||
|
'b1A921JUkHbASFiv2EFsgf0lkvnMgz2slNXKcLuwB6X0CAAAALypR4JWDUR6AAGSBfIJAABGCaV4' +
|
||||||
|
'scRn+wIAAAAABFla')
|
||||||
|
self.email_tpl['html'] = ('/Td6WFoAAATm1rRGAgAhARwAAAAQz1jM4AXfAtVdAB4aCobvStaHNqdVBn1LjZcL+G+98rmZ7eGZ' +
|
||||||
|
'Wqx+3LjENIv37L1aGPICZDRBrsBugzVSasRBHkdHMrWW7SsPfRzw6btQfASTHLr48auPJlJgXTnb' +
|
||||||
|
'vgDd2ELrs6p5m5Wip3qD4NeNuwj4QMcxszWF1vLa1oZiNAmCSunIF8bNTw+lmI50h2M6bXfx80Og' +
|
||||||
|
'T2HGcuTp07Mp+XLyZQJ5lbQyu5BRhwyKpu14sq9qrVkxmYt8AAxgUyhvRkooHSuug4O8ArMFXqqX' +
|
||||||
|
'usX9P3zERAsi/TqWIFaG0xoBdrWf/zpGtsVQ+5TtCGOfUHGfIBaNy9Q+FOvfLJFYEzxac992Fkd0' +
|
||||||
|
'as4RsN31FaySbBmZ8eB3zGbpjS7QH7CA70QYkRcYXcjWE9xHD3Wzxa3DFE0ihKAyVwakxvjgYa2B' +
|
||||||
|
'7G6uYO606c+a6vHfPhgvY7Eph+I7ip0btfBbcKZ+XBSd0DtCd7ZvI7vlGJdW2/OBXHfNmCndMP1W' +
|
||||||
|
'Ujd0ASQAQBbJr4rIxYygckSPWti4nBe9JpKTVWqdWRXWjeYGci1dKIjKs7JfS1PGJR50iuyANBun' +
|
||||||
|
'yQ9oIRafb3nreBqtpXZ4LKM5hC697BaeOIcocXyMALf0a06AUmIaRQfO3AZrPxyOPH3EYOKIMrjM' +
|
||||||
|
'EosihPVVyYuKUVOg3wWq5aeIC9zM7Htw4FNh2NB5QDYY6HxIqIVUfHCGz+4GaPBVaf0eie8kHaQR' +
|
||||||
|
'xj+DkAiWQDmN/JRZeTlsy4d3P8XcArOLmxzql/iDzFqtzpD5d91o8I3HU9BJlDJFPs8bC2eCjYs8' +
|
||||||
|
'o3WJET/UIch6YXQOemXa72aWdBVSytfKBMtL7uekd4ARGbFZYyW2x1agkAZGiWt7gwY8RVEoKyZH' +
|
||||||
|
'bbvIvOhQ/j1BDuJFJO3BEgekeLhBPpG7cEewseXjGjoWZWtGr+qFTI//w+oDtdqGtJaGtELL3WYU' +
|
||||||
|
'/tMiQU9AfXkTsODAjvduAAAAAIixVQ23iBDFAAHxBeALAADIP1EPscRn+wIAAAAABFla')
|
||||||
|
# Set up a dict of some constants and mappings
|
||||||
|
self.maps = {}
|
||||||
|
# Keylist modes
|
||||||
|
self.maps['keylist'] = {'local': gpg.constants.KEYLIST_MODE_LOCAL, # local keyring
|
||||||
|
'remote': gpg.constants.KEYLIST_MODE_EXTERN, # keyserver
|
||||||
|
# both - this is SUPPOSED to work, but doesn't seem to... it's unreliable at best?
|
||||||
|
'both': gpg.constants.KEYLIST_MODE_LOCAL|gpg.constants.KEYLIST_MODE_EXTERN}
|
||||||
|
# Validity/trust levels
|
||||||
|
self.maps['trust'] = {-1: ['never', gpg.constants.VALIDITY_NEVER], # this is... probably? not ideal, but. Never trust the key.
|
||||||
|
0: ['unknown', gpg.constants.VALIDITY_UNKNOWN], # The key's trust is unknown - typically because it hasn't been set yet.
|
||||||
|
1: ['untrusted', gpg.constants.VALIDITY_UNDEFINED], # The key is explicitly set to a blank trust
|
||||||
|
2: ['marginal', gpg.constants.VALIDITY_MARGINAL], # Trust a little.
|
||||||
|
3: ['full', gpg.constants.VALIDITY_FULL], # This is going to be the default for verified key ownership.
|
||||||
|
4: ['ultimate', gpg.constants.VALIDITY_ULTIMATE]} # This should probably only be reserved for keys you directly control.
|
||||||
|
# Validity/trust reverse mappings - see self.maps['trust'] for the meanings of these
|
||||||
|
# Used for fetching display/feedback
|
||||||
|
self.maps['rtrust'] = {gpg.constants.VALIDITY_NEVER: 'Never',
|
||||||
|
gpg.constants.VALIDITY_UNKNOWN: 'Unknown',
|
||||||
|
gpg.constants.VALIDITY_UNDEFINED: 'Untrusted',
|
||||||
|
gpg.constants.VALIDITY_MARGINAL: 'Marginal',
|
||||||
|
gpg.constants.VALIDITY_FULL: 'Full',
|
||||||
|
gpg.constants.VALIDITY_ULTIMATE: 'Ultimate'}
|
||||||
|
# Local signature and other binary (True/False) mappings
|
||||||
|
self.maps['binmap'] = {0: ['no', False],
|
||||||
|
1: ['yes', True]}
|
||||||
|
# Level of care taken when checking key ownership/valid identity
|
||||||
|
self.maps['check'] = {0: ['unknown', 0],
|
||||||
|
1: ['none', 1],
|
||||||
|
2: ['casual', 2],
|
||||||
|
3: ['careful', 3]}
|
||||||
|
# Default protocol/port mappings for keyservers
|
||||||
|
self.maps['proto'] = {'hkp': [11371, ['tcp', 'udp']], # Standard HKP protocol
|
||||||
|
'hkps': [443, ['tcp']], # Yes, same as https
|
||||||
|
'http': [80, ['tcp']], # HTTP (plaintext)
|
||||||
|
'https': [443, ['tcp']], # SSL/TLS
|
||||||
|
'ldap': [389, ['tcp', 'udp']], # Includes TLS negotiation since it runs on the same port
|
||||||
|
'ldaps': [636, ['tcp', 'udp']]} # SSL
|
||||||
|
self.maps['hashalgos'] = {gpg.constants.MD_MD5: 'md5',
|
||||||
|
gpg.constants.MD_SHA1: 'sha1',
|
||||||
|
gpg.constants.MD_RMD160: 'ripemd160',
|
||||||
|
gpg.constants.MD_MD2: 'md2',
|
||||||
|
gpg.constants.MD_TIGER: 'tiger192',
|
||||||
|
gpg.constants.MD_HAVAL: 'haval',
|
||||||
|
gpg.constants.MD_SHA256: 'sha256',
|
||||||
|
gpg.constants.MD_SHA384: 'sha384',
|
||||||
|
gpg.constants.MD_SHA512: 'sha512',
|
||||||
|
gpg.constants.MD_SHA224: 'sha224',
|
||||||
|
gpg.constants.MD_MD4: 'md4',
|
||||||
|
gpg.constants.MD_CRC32: 'crc32',
|
||||||
|
gpg.constants.MD_CRC32_RFC1510: 'crc32rfc1510',
|
||||||
|
gpg.constants.MD_CRC24_RFC2440: 'crc24rfc2440'}
|
||||||
|
# Now that all the static data's set up, we can continue.
|
||||||
|
self.args = self.verifyArgs(args) # Make the args accessible to all functions in the class - see docs/REF.args.struct.txt
|
||||||
|
# Get the GPGME context
|
||||||
|
try:
|
||||||
|
os.environ['GNUPGHOME'] = self.args['gpgdir']
|
||||||
|
self.ctx = gpg.Context()
|
||||||
|
except:
|
||||||
|
raise RuntimeError('Could not use {0} as a GnuPG home'.format(self.args['gpgdir']))
|
||||||
|
self.cfgdir = os.path.join(os.environ['HOME'], '.kant')
|
||||||
|
if not os.path.isdir(self.cfgdir):
|
||||||
|
print('No KANT configuration directory found; creating one at {0}...'.format(self.cfgdir))
|
||||||
|
os.makedirs(self.cfgdir, exist_ok = True)
|
||||||
|
self.keys = {} # See docs/REF.keys.struct.txt
|
||||||
|
self.mykey = {} # ""
|
||||||
|
self.tpls = {} # Email templates will go here
|
||||||
|
self.getTpls() # Build out self.tpls
|
||||||
|
return(None)
|
||||||
|
|
||||||
|
def getEditPrompt(self, key, cmd): # "key" should be the FPR of the primary key
|
||||||
|
# This mapping defines the default "answers" to the gpgme key editing.
|
||||||
|
# https://www.apt-browse.org/browse/debian/wheezy/main/amd64/python-pyme/1:0.8.1-2/file/usr/share/doc/python-pyme/examples/t-edit.py
|
||||||
|
# https://searchcode.com/codesearch/view/20535820/
|
||||||
|
# https://git.gnupg.org/cgi-bin/gitweb.cgi?p=gnupg.git;a=blob;f=doc/DETAILS
|
||||||
|
# You can get the prompt identifiers and status indicators without grokking the source
|
||||||
|
# by first interactively performing the type of edit(s) you want to do with this command:
|
||||||
|
# gpg --status-fd 2 --command-fd 2 --edit-key <KEY_ID>
|
||||||
|
if key['trust'] >= gpg.constants.VALIDITY_FULL: # For tsigning, it only prompts for two trust levels:
|
||||||
|
_loctrust = 2 # "I trust fully"
|
||||||
|
else:
|
||||||
|
_loctrust = 1 # "I trust marginally"
|
||||||
|
# TODO: make the trust depth configurable. 1 is probably the safest, but we try to guess here.
|
||||||
|
# "Full" trust is a pretty big thing.
|
||||||
|
if key['trust'] >= gpg.constants.VALIDITY_FULL:
|
||||||
|
_locdepth = 2 # Allow +1 level of trust extension
|
||||||
|
else:
|
||||||
|
_locdepth = 1 # Only trust this key
|
||||||
|
_map = {'cmds': ['trust', 'fpr', 'sign', 'tsign', 'lsign', 'nrsign', 'grip', 'list', # Valid commands
|
||||||
|
'uid', 'key', 'check', 'deluid', 'delkey', 'delsig', 'pref', 'showpref',
|
||||||
|
'revsig', 'enable', 'disable', 'showphoto', 'clean', 'minimize', 'save',
|
||||||
|
'quit'],
|
||||||
|
'prompts': {'edit_ownertrust': {'value': str(key['trust']), # Pulled at time of call
|
||||||
|
'set_ultimate': {'okay': 'yes'}}, # If confirming ultimate trust, we auto-answer yes
|
||||||
|
'untrusted_key': {'override': 'yes'}, # We don't care if it's untrusted
|
||||||
|
'pklist': {'user_id': {'enter': key['pkey']['email']}}, # Prompt for a user ID - can we change this to key ID?
|
||||||
|
'sign_uid': {'class': str(key['check']), # The certification/"check" level
|
||||||
|
'okay': 'yes'}, # Are you sure that you want to sign this key with your key..."
|
||||||
|
'trustsig_prompt': {'trust_value': str(_loctrust), # This requires some processing; see above
|
||||||
|
'trust_depth': str(_locdepth), # The "depth" of the trust signature.
|
||||||
|
'trust_regexp': None}, # We can "Restrict" trust to certain domains, but this isn't really necessary.
|
||||||
|
'keyedit': {'prompt': cmd, # Initiate trust editing
|
||||||
|
'save': {'okay': 'yes'}}}} # Save if prompted
|
||||||
|
return(_map)
|
||||||
|
|
||||||
|
def getTpls(self):
|
||||||
|
for t in ('plain', 'html'):
|
||||||
|
_tpl_file = os.path.join(self.cfgdir, 'email.{0}.j2'.format(t))
|
||||||
|
if os.path.isfile(_tpl_file):
|
||||||
|
with open(_tpl_file, 'r') as f:
|
||||||
|
self.tpls[t] = f.read()
|
||||||
|
else:
|
||||||
|
self.tpls[t] = lzma.decompress(base64.b64decode(email_tpl[t]),
|
||||||
|
format = lzma.FORMAT_XZ,
|
||||||
|
memlimit = None,
|
||||||
|
filters = None).decode('utf-8')
|
||||||
|
with open(_tpl_file, 'w') as f:
|
||||||
|
f.write('{0}'.format(self.tpls[t]))
|
||||||
|
print('Created: {0}'.format(tpl_file))
|
||||||
|
return(self.tpls)
|
||||||
|
|
||||||
|
def modifyDirmngr(self, op):
|
||||||
|
if not self.args['keyservers']:
|
||||||
|
return()
|
||||||
|
_pid = str(os.getpid())
|
||||||
|
_activecfg = os.path.join(self.args['gpgdir'], 'dirmngr.conf')
|
||||||
|
_activegpgconf = os.path.join(self.args['gpgdir'], 'gpg.conf')
|
||||||
|
_bakcfg = '{0}.{1}'.format(_activecfg, _pid)
|
||||||
|
_bakgpgconf = '{0}.{1}'.format(_activegpgconf, _pid)
|
||||||
|
## Modify files
|
||||||
|
if op in ('new', 'start', 'replace'):
|
||||||
|
# Replace the keyservers
|
||||||
|
if os.path.lexists(_activecfg):
|
||||||
|
shutil.copy2(_activecfg, _bakcfg)
|
||||||
|
with open(_bakcfg, 'r') as read, open(_activecfg, 'w') as write:
|
||||||
|
for line in read:
|
||||||
|
if not line.startswith('keyserver '):
|
||||||
|
write.write(line)
|
||||||
|
with open(_activecfg, 'a') as f:
|
||||||
|
for s in self.args['keyservers']:
|
||||||
|
_uri = '{0}://{1}:{2}'.format(s['proto'],
|
||||||
|
s['server'],
|
||||||
|
s['port'][0])
|
||||||
|
f.write('keyserver {0}\n'.format(_uri))
|
||||||
|
# Use stronger ciphers, etc. and prompt for check/certification levels
|
||||||
|
if os.path.lexists(_activegpgconf):
|
||||||
|
shutil.copy2(_activegpgconf, _bakgpgconf)
|
||||||
|
with open(_activegpgconf, 'w') as f:
|
||||||
|
f.write('cipher-algo AES256\ndigest-algo SHA512\ncert-digest-algo SHA512\ncompress-algo BZIP2\nask-cert-level\n')
|
||||||
|
## Restore files
|
||||||
|
if op in ('old', 'stop', 'restore'):
|
||||||
|
# Restore the keyservers
|
||||||
|
if os.path.lexists(_bakcfg):
|
||||||
|
with open(_bakcfg, 'r') as read, open(_activecfg, 'w') as write:
|
||||||
|
for line in read:
|
||||||
|
write.write(line)
|
||||||
|
os.remove(_bakcfg)
|
||||||
|
else:
|
||||||
|
os.remove(_activecfg)
|
||||||
|
# Restore GPG settings
|
||||||
|
if os.path.lexists(_bakgpgconf):
|
||||||
|
with open(_bakgpgconf, 'r') as read, open(_activegpgconf, 'w') as write:
|
||||||
|
for line in read:
|
||||||
|
write.write(line)
|
||||||
|
os.remove(_bakgpgconf)
|
||||||
|
else:
|
||||||
|
os.remove(_activegpgconf)
|
||||||
|
subprocess.run(['gpgconf', '--reload', 'dirmngr']) # I *really* wish we could do this via GPGME.
|
||||||
|
return()
|
||||||
|
|
||||||
|
def getKeys(self):
|
||||||
|
_keyids = []
|
||||||
|
_keys = {}
|
||||||
|
# Do we have the key already? If not, fetch.
|
||||||
|
for r in list(self.args['rcpts'].keys()):
|
||||||
|
if self.args['rcpts'][r]['type'] == 'fpr':
|
||||||
|
_keyids.append(r)
|
||||||
|
self.ctx.set_keylist_mode(self.maps['keylist']['remote'])
|
||||||
|
try:
|
||||||
|
_k = self.ctx.get_key(r)
|
||||||
|
except:
|
||||||
|
print('{0}: We could not find this key on the keyserver.'.format(r)) # Key not on server
|
||||||
|
del(self.args['rcpts'][r])
|
||||||
|
_keyids.remove(r)
|
||||||
|
continue
|
||||||
|
self.ctx.set_keylist_mode(self.maps['keylist']['local'])
|
||||||
|
_keys[r] = {'fpr': r,
|
||||||
|
'obj': _k,
|
||||||
|
'created': _k.subkeys[0].timestamp}
|
||||||
|
if 'T' in str(_keys[r]['created']):
|
||||||
|
_keys[r]['created'] = int(datetime.datetime.strptime(_keys[r]['created'],
|
||||||
|
'%Y%m%dT%H%M%S').timestamp())
|
||||||
|
if self.args['rcpts'][r]['type'] == 'email':
|
||||||
|
# We need to actually do a lookup on the email address.
|
||||||
|
_keytmp = []
|
||||||
|
for k in self.ctx.keylist(r, mode = self.maps['keylist']['remote']):
|
||||||
|
_keytmp.append(k)
|
||||||
|
for k in _keytmp:
|
||||||
|
_keys[k.fpr] = {'fpr': k.fpr,
|
||||||
|
'obj': k,
|
||||||
|
'created': k.subkeys[0].timestamp,
|
||||||
|
'uids': {}}
|
||||||
|
# Per the docs (<gpg>/docs/DETAILS, "*** Field 6 - Creation date"),
|
||||||
|
# they may change this to ISO 8601...
|
||||||
|
if 'T' in str(_keys[k.fpr]['created']):
|
||||||
|
_keys[k.fpr]['created'] = int(datetime.datetime.strptime(_keys[k.fpr]['created'],
|
||||||
|
'%Y%m%dT%H%M%S').timestamp())
|
||||||
|
for s in k.uids:
|
||||||
|
_keys[k.fpr]['uids'][s.email] = {'comment': s.comment,
|
||||||
|
'updated': s.last_update}
|
||||||
|
if len(_keytmp) > 1: # Print the keys and prompt for a selection.
|
||||||
|
|
||||||
|
print('\nWe found the following keys for {0}...\n\nKEY ID:'.format(r))
|
||||||
|
for s in _keytmp:
|
||||||
|
print('{0}\n{1:6}(Generated at {2}) UIDs:'.format(s.fpr,
|
||||||
|
'',
|
||||||
|
datetime.datetime.utcfromtimestamp(s.subkeys[0].timestamp)))
|
||||||
|
for u in s.uids:
|
||||||
|
if u.last_update == 0:
|
||||||
|
_updated = 'Never/Unknown'
|
||||||
|
else:
|
||||||
|
_updated = datetime.datetime.utcfromtimestamp(u.last_update)
|
||||||
|
print('{0:42}(Updated {3}) <{2}> {1}'.format('',
|
||||||
|
u.comment,
|
||||||
|
u.email,
|
||||||
|
_updated))
|
||||||
|
print()
|
||||||
|
while True:
|
||||||
|
key = input('Please enter the (full) appropriate key: ')
|
||||||
|
if key not in _keys.keys():
|
||||||
|
print('Please enter a full key ID from the list above or hit ctrl-d to exit.')
|
||||||
|
else:
|
||||||
|
_keyids.append(key)
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
if len(_keytmp) == 0:
|
||||||
|
print('Could not find {0}!'.format(r))
|
||||||
|
del(self.args['rcpts'][r])
|
||||||
|
continue
|
||||||
|
_keyids.append(k.fpr)
|
||||||
|
print('\nFound key {0} for {1} (Generated at {2}):'.format(_keys[k.fpr]['fpr'],
|
||||||
|
r,
|
||||||
|
datetime.datetime.utcfromtimestamp(_keys[k.fpr]['created'])))
|
||||||
|
for email in _keys[k.fpr]['uids']:
|
||||||
|
if _keys[k.fpr]['uids'][email]['updated'] == 0:
|
||||||
|
_updated = 'Never/Unknown'
|
||||||
|
else:
|
||||||
|
_updated = datetime.datetime.utcfromtimestamp(_keys[k.fpr]['uids'][email]['updated'])
|
||||||
|
print('\t(Generated {2}) {0} <{1}>'.format(_keys[k.fpr]['uids'][email]['comment'],
|
||||||
|
email,
|
||||||
|
_updated))
|
||||||
|
print()
|
||||||
|
## And now we can (FINALLY) fetch the key(s).
|
||||||
|
print(_keyids)
|
||||||
|
for g in _keyids:
|
||||||
|
try:
|
||||||
|
self.ctx.op_import_keys([_keys[g]['obj']])
|
||||||
|
except gpg.errors.GPGMEError:
|
||||||
|
print('Key {0} could not be found on the keyserver'.format(g)) # The key isn't on the keyserver
|
||||||
|
self.ctx.set_keylist_mode(self.maps['keylist']['local'])
|
||||||
|
for k in _keys:
|
||||||
|
if k not in _keyids:
|
||||||
|
continue
|
||||||
|
_key = _keys[k]['obj']
|
||||||
|
self.keys[k] = {'pkey': {'email': _key.uids[0].email,
|
||||||
|
'name': _key.uids[0].name,
|
||||||
|
'creation': datetime.datetime.utcfromtimestamp(_keys[k]['created']),
|
||||||
|
'key': _key},
|
||||||
|
'trust': self.args['trustlevel'], # Not set yet; we'll modify this later in buildKeys().
|
||||||
|
'local': self.args['local'], # Not set yet; we'll modify this later in buildKeys().
|
||||||
|
'notify': self.args['notify'], # Same...
|
||||||
|
'sign': True, # We don't need to prompt for this since we detect if we need to sign or not
|
||||||
|
'change': None, # ""
|
||||||
|
'status': None} # Same.
|
||||||
|
# And we add the subkeys in yet another loop.
|
||||||
|
self.keys[k]['subkeys'] = {}
|
||||||
|
self.keys[k]['uids'] = {}
|
||||||
|
for s in _key.subkeys:
|
||||||
|
self.keys[k]['subkeys'][s.fpr] = datetime.datetime.utcfromtimestamp(s.timestamp)
|
||||||
|
for u in _key.uids:
|
||||||
|
self.keys[k]['uids'][u.email] = {'name': u.name,
|
||||||
|
'comment': u.comment,
|
||||||
|
'updated': datetime.datetime.utcfromtimestamp(u.last_update)}
|
||||||
|
del(_keys)
|
||||||
|
return()
|
||||||
|
|
||||||
|
def buildKeys(self):
|
||||||
|
self.getKeys()
|
||||||
|
# Before anything else, let's set up our own key info.
|
||||||
|
_key = self.ctx.get_key(self.args['sigkey'], secret = True)
|
||||||
|
self.mykey = {'pkey': {'email': _key.uids[0].email,
|
||||||
|
'name': _key.uids[0].name,
|
||||||
|
'creation': datetime.datetime.utcfromtimestamp(_key.subkeys[0].timestamp),
|
||||||
|
'key': _key},
|
||||||
|
'trust': 'ultimate', # No duh. This is our own key.
|
||||||
|
'local': False, # We keep our own key array separate, so we don't push it anyways.
|
||||||
|
'notify': False, # ""
|
||||||
|
'check': None, # ""
|
||||||
|
'change': False, # ""
|
||||||
|
'status': None, # ""
|
||||||
|
'sign': False} # ""
|
||||||
|
self.mykey['subkeys'] = {}
|
||||||
|
self.mykey['uids'] = {}
|
||||||
|
for s in _key.subkeys:
|
||||||
|
self.mykey['subkeys'][s.fpr] = datetime.datetime.utcfromtimestamp(s.timestamp)
|
||||||
|
for u in _key.uids:
|
||||||
|
self.mykey['uids'][u.email] = {'name': u.name,
|
||||||
|
'comment': u.comment,
|
||||||
|
'updated': datetime.datetime.utcfromtimestamp(u.last_update)}
|
||||||
|
# Now let's set up our trusts.
|
||||||
|
if self.args['batch']:
|
||||||
|
self.batchParse()
|
||||||
|
else:
|
||||||
|
for k in list(self.keys.keys()):
|
||||||
|
self.promptTrust(k)
|
||||||
|
self.promptCheck(k)
|
||||||
|
self.promptLocal(k)
|
||||||
|
self.promptNotify(k)
|
||||||
|
# In case we removed any keys, we have to run this outside of the loops
|
||||||
|
for k in list(self.keys.keys()):
|
||||||
|
for t in ('trust', 'local', 'check', 'notify'):
|
||||||
|
self.keysCleanup(k, t)
|
||||||
|
# TODO: populate self.keys[key]['change']; we use this for trust (but not sigs)
|
||||||
|
return()
|
||||||
|
|
||||||
|
def batchParse(self):
|
||||||
|
# First we grab the info from CSV
|
||||||
|
csvlines = csv.reader(self.csvraw, delimiter = ',', quotechar = '"')
|
||||||
|
for row in csvlines:
|
||||||
|
row[0] = row[0].replace('<', '').replace('>', '')
|
||||||
|
try:
|
||||||
|
if self.args['rcpts'][row[0]]['type'] == 'fpr':
|
||||||
|
k = row[0]
|
||||||
|
else: # It's an email.
|
||||||
|
key_set = False
|
||||||
|
while not key_set:
|
||||||
|
for i in list(self.keys.keys()):
|
||||||
|
if row[0] in list(self.keys[i]['uids'].keys()):
|
||||||
|
k = i
|
||||||
|
key_set = True
|
||||||
|
self.keys[k]['trust'] = row[1].lower().strip()
|
||||||
|
self.keys[k]['local'] = row[2].lower().strip()
|
||||||
|
self.keys[k]['check'] = row[3].lower().strip()
|
||||||
|
self.keys[k]['notify'] = row[4].lower().strip()
|
||||||
|
except KeyError:
|
||||||
|
continue # It was deemed to be an invalid key earlier
|
||||||
|
return()
|
||||||
|
|
||||||
|
def promptTrust(self, k):
|
||||||
|
if 'trust' not in self.keys[k].keys() or not self.keys[k]['trust']:
|
||||||
|
trust_in = input(('\nWhat trust level should we assign to {0}? (The default is '+
|
||||||
|
'Marginal.)\n\t\t\t\t ({1} <{2}>)' +
|
||||||
|
'\n\n\t\033[1m-1 = Never\n\t 0 = Unknown\n\t 1 = Untrusted\n\t 2 = Marginal\n\t 3 = Full' +
|
||||||
|
'\n\t 4 = Ultimate\033[0m\nTrust: ').format(k,
|
||||||
|
self.keys[k]['pkey']['name'],
|
||||||
|
self.keys[k]['pkey']['email']))
|
||||||
|
if trust_in == '':
|
||||||
|
trust_in = 'marginal' # Has to be a str, so we can "pretend" it was entered
|
||||||
|
self.keys[k]['trust'] = trust_in
|
||||||
|
return()
|
||||||
|
|
||||||
|
def promptCheck(self, k):
|
||||||
|
if 'check' not in self.keys[k].keys() or self.keys[k]['check'] == None:
|
||||||
|
check_in = input(('\nHow carefully have you checked {0}\'s validity of identity/ownership of the key? ' +
|
||||||
|
'(Default is Unknown.)\n' +
|
||||||
|
'\n\t\033[1m0 = Unknown\n\t1 = None\n\t2 = Casual\n\t3 = Careful\033[0m\nCheck level: ').format(k))
|
||||||
|
if check_in == '':
|
||||||
|
check_in = 'unknown'
|
||||||
|
self.keys[k]['check'] = check_in
|
||||||
|
return()
|
||||||
|
|
||||||
|
def promptLocal(self, k):
|
||||||
|
if 'local' not in self.keys[k].keys() or self.keys[k]['local'] == None:
|
||||||
|
if self.args['keyservers']:
|
||||||
|
local_in = input(('\nShould we locally sign {0} '+
|
||||||
|
'(if yes, the signature will be non-exportable; if no, we will be able to push to a keyserver) ' +
|
||||||
|
'(Yes/\033[1mNO\033[0m)? ').format(k))
|
||||||
|
if local_in == '':
|
||||||
|
local_in = False
|
||||||
|
self.keys[k]['local'] = local_in
|
||||||
|
return()
|
||||||
|
|
||||||
|
def promptNotify(self, k):
|
||||||
|
if 'notify' not in self.keys[k].keys() or self.keys[k]['notify'] == None:
|
||||||
|
notify_in = input(('\nShould we notify {0} (via <{1}>) (\033[1mYES\033[0m/No)? ').format(k,
|
||||||
|
self.keys[k]['pkey']['email']))
|
||||||
|
if notify_in == '':
|
||||||
|
notify_in = True
|
||||||
|
self.keys[k]['local'] = local_in
|
||||||
|
return()
|
||||||
|
|
||||||
|
def keysCleanup(self, k, t): # At some point, this WHOLE thing would probably be cleaner with bitwise flags...
|
||||||
|
s = t
|
||||||
|
_errs = {'trust': 'trust level',
|
||||||
|
'local': 'local signature option',
|
||||||
|
'check': 'check level',
|
||||||
|
'notify': 'notify flag'}
|
||||||
|
if k not in self.keys.keys():
|
||||||
|
return() # It was deleted already.
|
||||||
|
if t in ('local', 'notify'): # these use a binary mapping
|
||||||
|
t = 'binmap'
|
||||||
|
# We can do some basic stuff right here.
|
||||||
|
if str(self.keys[k][s]).lower() in ('n', 'no', 'false'):
|
||||||
|
self.keys[k][s] = False
|
||||||
|
return()
|
||||||
|
elif str(self.keys[k][s]).lower() in ('y', 'yes', 'true'):
|
||||||
|
self.keys[k][s] = True
|
||||||
|
return()
|
||||||
|
# Make sure we have a known value. These will ALWAYS be str's, either from the CLI or CSV.
|
||||||
|
value_in = str(self.keys[k][s]).lower().strip()
|
||||||
|
for dictk, dictv in self.maps[t].items():
|
||||||
|
if value_in == dictv[0]:
|
||||||
|
self.keys[k][s] = int(dictk)
|
||||||
|
elif value_in == str(dictk):
|
||||||
|
self.keys[k][s] = int(dictk)
|
||||||
|
if not isinstance(self.keys[k][s], int): # It didn't get set
|
||||||
|
print('{0}: "{1}" is not a valid {2}; skipping. Run kant again to fix.'.format(k, self.keys[k][s], _errs[s]))
|
||||||
|
del(self.keys[k])
|
||||||
|
return()
|
||||||
|
# Determine if we need to change the trust.
|
||||||
|
if t == 'trust':
|
||||||
|
cur_trust = self.keys[k]['pkey']['key'].owner_trust
|
||||||
|
if cur_trust == self.keys[k]['trust']:
|
||||||
|
self.keys[k]['change'] = False
|
||||||
|
else:
|
||||||
|
self.keys[k]['change'] = True
|
||||||
|
return()
|
||||||
|
|
||||||
|
def sigKeys(self): # The More Business-End(TM)
|
||||||
|
# NOTE: If the trust level is anything but 2 (the default), we should use op_interact() instead and do a tsign.
|
||||||
|
self.ctx.keylist_mode = gpg.constants.KEYLIST_MODE_SIGS
|
||||||
|
_mkey = self.mykey['pkey']['key']
|
||||||
|
self.ctx.signers = [_mkey]
|
||||||
|
for k in list(self.keys.keys()):
|
||||||
|
key = self.keys[k]['pkey']['key']
|
||||||
|
for uid in key.uids:
|
||||||
|
for s in uid.signatures:
|
||||||
|
try:
|
||||||
|
signerkey = ctx.get_key(s.keyid).subkeys[0].fpr
|
||||||
|
if signerkey == mkey.subkeys[0].fpr:
|
||||||
|
self.trusts[k]['sign'] = False # We already signed this key
|
||||||
|
except gpgme.GpgError:
|
||||||
|
pass # usually if we get this it means we don't have a signer's key in our keyring
|
||||||
|
# And again, we loop. ALLLLL that buildup for one line.
|
||||||
|
for k in list(self.keys.keys()):
|
||||||
|
# TODO: configure to allow for user-entered expiration?
|
||||||
|
if self.keys[k]['sign']:
|
||||||
|
self.ctx.key_sign(self.keys[k]['pkey']['key'], local = self.keys[k]['local'])
|
||||||
|
return()
|
||||||
|
|
||||||
|
class KeyEditor(object):
|
||||||
|
def __init__(self, optmap):
|
||||||
|
self.replied_once = False # This is used to handle the first prompt vs. the last
|
||||||
|
self.optmap = optmap
|
||||||
|
return(None)
|
||||||
|
|
||||||
|
def editKey(self, status, args, out):
|
||||||
|
_result = None
|
||||||
|
out.seek(0, 0)
|
||||||
|
def mapDict(m, d):
|
||||||
|
return(reduce(operator.getitem, m, d))
|
||||||
|
if args == 'keyedit.prompt' and self.replied_once:
|
||||||
|
_result = 'quit'
|
||||||
|
elif status == 'KEY_CONSIDERED':
|
||||||
|
_result = None
|
||||||
|
self.replied_once = False
|
||||||
|
elif status == 'GET_LINE':
|
||||||
|
self.replied_once = True
|
||||||
|
_ilist = args.split('.')
|
||||||
|
_result = mapDict(_ilist, self.optmap['prompts'])
|
||||||
|
if not _result:
|
||||||
|
_result = None
|
||||||
|
return(_result)
|
||||||
|
|
||||||
|
def trustKeys(self): # The Son of Business-End(TM)
|
||||||
|
# TODO: add check for change
|
||||||
|
for k in list(self.keys.keys()):
|
||||||
|
_key = self.keys[k]
|
||||||
|
if _key['change']:
|
||||||
|
_map = self.getEditPrompt(_key, 'trust')
|
||||||
|
out = gpg.Data()
|
||||||
|
self.ctx.interact(_key['pkey']['key'], self.KeyEditor(_map).editKey, sink = out, fnc_value = out)
|
||||||
|
out.seek(0, 0)
|
||||||
|
return()
|
||||||
|
|
||||||
|
def pushKeys(self): # The Last Business-End(TM)
|
||||||
|
for k in list(self.keys.keys()):
|
||||||
|
if not self.keys[k]['local'] and self.keys[k]['sign']:
|
||||||
|
self.ctx.op_export(k, gpg.constants.EXPORT_MODE_EXTERN, None)
|
||||||
|
return()
|
||||||
|
|
||||||
|
class Mailer(object): # I lied; The Return of the Business-End(TM)
|
||||||
|
def __init__(self):
|
||||||
|
_homeconf = os.path.join(os.environ['HOME'], '.msmtprc')
|
||||||
|
_sysconf = '/etc/msmtprc'
|
||||||
|
self.msmtp = {'conf': None}
|
||||||
|
if not os.path.isfile(_homeconf):
|
||||||
|
if not os.path.isfile(_sysconf):
|
||||||
|
self.msmtp['conf'] = False
|
||||||
|
else:
|
||||||
|
self.msmtp['conf'] = _sysconf
|
||||||
|
else:
|
||||||
|
self.msmtp['conf'] = _homeconf
|
||||||
|
if self.msmtp['conf']:
|
||||||
|
# Okay. So we have a config file, which we're assuming to be set up correctly.
|
||||||
|
# Now we need to parse the config.
|
||||||
|
self.msmtp['cfg'] = self.getCfg()
|
||||||
|
return(None)
|
||||||
|
|
||||||
|
def getCfg(self):
|
||||||
|
cfg = {'default': None, 'defaults': {}}
|
||||||
|
_defaults = False
|
||||||
|
_acct = None
|
||||||
|
with open(self.msmtp['conf'], 'r') as f:
|
||||||
|
_cfg_raw = f.read()
|
||||||
|
for l in _cfg_raw.splitlines():
|
||||||
|
if re.match('^\s?(#.*|)$', l):
|
||||||
|
continue # Skip over blank and commented lines
|
||||||
|
_line = [i.strip() for i in re.split('\s+', l.strip(), maxsplit = 1)]
|
||||||
|
if _line[0] == 'account':
|
||||||
|
if re.match('^default\s?:\s?', _line[1]): # it's the default account specifier
|
||||||
|
cfg['default'] = _line[1].split(':', maxsplit = 1)[1].strip()
|
||||||
|
else:
|
||||||
|
if _line[1] not in cfg.keys(): # it's a new account definition
|
||||||
|
cfg[_line[1]] = {}
|
||||||
|
_acct = _line[1]
|
||||||
|
_defaults = False
|
||||||
|
elif _line[0] == 'defaults': # it's the defaults
|
||||||
|
_acct = 'defaults'
|
||||||
|
else: # it's a config directive
|
||||||
|
cfg[_acct][_line[0]] = _line[1]
|
||||||
|
for a in list(cfg):
|
||||||
|
if a != 'default':
|
||||||
|
for k, v in cfg['defaults'].items():
|
||||||
|
if k not in cfg[a].keys():
|
||||||
|
cfg[a][k] = v
|
||||||
|
del(cfg['defaults'])
|
||||||
|
return(cfg)
|
||||||
|
|
||||||
|
def sendEmail(self, msg, key, profile): # This needs way more parsing to support things like plain ol' port 25 plaintext (ugh), etc.
|
||||||
|
if 'tls-starttls' in self.msmtp['cfg'][profile].keys() and self.msmtp['cfg'][profile]['tls-starttls'] == 'on':
|
||||||
|
smtpserver = smtplib.SMTP(self.msmtp['cfg'][profile]['host'], int(self.msmtp['cfg'][profile]['port']))
|
||||||
|
smtpserver.ehlo()
|
||||||
|
smtpserver.starttls()
|
||||||
|
# we need to EHLO twice with a STARTTLS because email is weird.
|
||||||
|
elif self.msmtp['cfg'][profile]['tls'] == 'on':
|
||||||
|
smtpserver = smtplib.SMTP_SSL(self.msmtp['cfg'][profile]['host'], int(self.msmtp['cfg'][profile]['port']))
|
||||||
|
smtpserver.ehlo()
|
||||||
|
smtpserver.login(self.msmtp['cfg'][profile]['user'], self.msmtp['cfg'][profile]['password'])
|
||||||
|
smtpserver.sendmail(self.msmtp['cfg'][profile]['user'], key['pkey']['email'], msg.as_string())
|
||||||
|
smtpserver.close()
|
||||||
|
return()
|
||||||
|
|
||||||
|
def postalWorker(self):
|
||||||
|
m = self.Mailer()
|
||||||
|
if 'KANT' in m.msmtp['cfg'].keys():
|
||||||
|
_profile = 'KANT'
|
||||||
|
else:
|
||||||
|
_profile = m.msmtp['cfg']['default'] # TODO: let this be specified on the CLI args?
|
||||||
|
if 'user' not in m.msmtp['cfg'][_profile].keys() or not m.msmtp['cfg'][_profile]['user']:
|
||||||
|
return() # We don't have MSMTP configured.
|
||||||
|
# Reconstruct the keyserver list.
|
||||||
|
_keyservers = []
|
||||||
|
for k in self.args['keyservers']:
|
||||||
|
_keyservers.append('{0}://{1}:{2}'.format(k['proto'], k['server'], k['port'][0]))
|
||||||
|
# Export our key so we can attach it.
|
||||||
|
_pubkeys = {}
|
||||||
|
for e in ('asc', 'gpg'):
|
||||||
|
if e == 'asc':
|
||||||
|
self.ctx.armor = True
|
||||||
|
else:
|
||||||
|
self.ctx.armor = False
|
||||||
|
_pubkeys[e] = gpg.Data() # This is a data buffer to store your ASCII-armored pubkeys
|
||||||
|
self.ctx.op_export_keys([self.mykey['pkey']['key']], 0, _pubkeys[e])
|
||||||
|
_pubkeys[e].seek(0, 0) # Read with e.g. _sigs['asc'].read()
|
||||||
|
for k in list(self.keys.keys()):
|
||||||
|
if self.keys[k]['notify']:
|
||||||
|
_body = {}
|
||||||
|
for t in list(self.tpls.keys()):
|
||||||
|
# There's gotta be a more efficient way of doing this...
|
||||||
|
#_tplenv = jinja2.Environment(loader = jinja2.BaseLoader()).from_string(self.tpls[t])
|
||||||
|
_tplenv = jinja2.Environment().from_string(self.tpls[t])
|
||||||
|
_body[t] = _tplenv.render(key = self.keys[k],
|
||||||
|
mykey = self.mykey,
|
||||||
|
keyservers = _keyservers)
|
||||||
|
b = MIMEMultipart('alternative') # Set up a body
|
||||||
|
for c in _body.keys():
|
||||||
|
b.attach(MIMEText(_body[c], c))
|
||||||
|
bmsg = MIMEMultipart()
|
||||||
|
bmsg.attach(b)
|
||||||
|
for s in _pubkeys.keys():
|
||||||
|
_attchmnt = MIMEApplication(_pubkeys[s].read(), '{0}.{1}'.format(self.mykey['pkey']['key'].fpr, s))
|
||||||
|
_attchmnt['Content-Disposition'] = 'attachment; filename="{0}.{1}"'.format(self.mykey['pkey']['key'].fpr, s)
|
||||||
|
bmsg.attach(_attchmnt)
|
||||||
|
# Now we sign the body. This incomprehensible bit monkey-formats bmsg to be a multi-RFC-compatible
|
||||||
|
# string, which is then passed to our gpgme instance's signing mechanishm, and the output of that is
|
||||||
|
# returned as plaintext. Whew.
|
||||||
|
self.ctx.armor = True
|
||||||
|
|
||||||
|
_sig = self.ctx.sign((bmsg.as_string().replace('\n', '\r\n')).encode('utf-8'),
|
||||||
|
mode = gpg.constants.SIG_MODE_DETACH)
|
||||||
|
imsg = Message() # Build yet another intermediate message...
|
||||||
|
imsg['Content-Type'] = 'application/pgp-signature; name="signature.asc"'
|
||||||
|
imsg['Content-Description'] = 'OpenPGP digital signature'
|
||||||
|
imsg.set_payload(_sig[0].decode('utf-8'))
|
||||||
|
msg = MIMEMultipart(_subtype = 'signed',
|
||||||
|
micalg = "pgp-{0}".format(self.maps['hashalgos'][_sig[1].signatures[0].hash_algo]),
|
||||||
|
protocol = 'application/pgp-signature')
|
||||||
|
msg.attach(bmsg) # Attach the body (plaintext, html, pubkey attachmants)
|
||||||
|
msg.attach(imsg) # Attach the isignature
|
||||||
|
msg['To'] = self.keys[k]['pkey']['email']
|
||||||
|
if 'from' in m.msmtp['cfg'][_profile].keys():
|
||||||
|
msg['From'] = m.msmtp['cfg'][_profile]['from']
|
||||||
|
else:
|
||||||
|
msg['From'] = self.mykey['pkey']['email']
|
||||||
|
msg['Subject'] = 'Your GnuPG/PGP key has been signed'
|
||||||
|
msg['Openpgp'] = 'id={0}'.format(self.mykey['pkey']['key'].fpr)
|
||||||
|
msg['Date'] = datetime.datetime.now().strftime('%a, %d %b %Y %H:%M:%S %z')
|
||||||
|
msg['User-Agent'] = 'KANT (part of the r00t^2 OpTools suite: https://git.square-r00t.net/OpTools)'
|
||||||
|
m.sendEmail(msg, self.keys[k], _profile) # Send the email
|
||||||
|
for d in (msg, imsg, bmsg, b, _body, _tplenv): # Not necessary, but it pays to be paranoid; we do NOT want leaks.
|
||||||
|
del(d)
|
||||||
|
del(m)
|
||||||
|
return()
|
||||||
|
|
||||||
|
def saveResults(self):
|
||||||
|
_cachedir = os.path.join(self.cfgdir, 'cache', datetime.datetime.utcnow().strftime('%Y.%m.%d_%H.%M.%S'))
|
||||||
|
os.makedirs(_cachedir, exist_ok = True)
|
||||||
|
for k in self.keys.keys():
|
||||||
|
_keyout = self.keys[k]
|
||||||
|
# We need to normalize the datetime objects and gpg objects to strings
|
||||||
|
_keyout['pkey']['creation'] = str(self.keys[k]['pkey']['creation'])
|
||||||
|
_keyout['pkey']['key'] = '<GPGME object>'
|
||||||
|
for u in list(_keyout['uids'].keys()):
|
||||||
|
_keyout['uids'][u]['updated'] = str(self.keys[k]['uids'][u]['updated'])
|
||||||
|
for s in list(_keyout['subkeys'].keys()):
|
||||||
|
_keyout['subkeys'][s] = str(self.keys[k]['subkeys'][s])
|
||||||
|
_fname = os.path.join(_cachedir, '{0}.json'.format(k))
|
||||||
|
with open(_fname, 'a') as f:
|
||||||
|
f.write('{0}\n'.format(json.dumps(_keyout, sort_keys = True, indent = 4)))
|
||||||
|
del(_keyout)
|
||||||
|
# And let's grab a copy of our key in the state that it exists in currently
|
||||||
|
_mykey = self.mykey
|
||||||
|
# We need to normalize the datetime objects and gpg objects to strings again
|
||||||
|
_mykey['pkey']['creation'] = str(_mykey['pkey']['creation'])
|
||||||
|
_mykey['pkey']['key'] = '<GPGME object>'
|
||||||
|
for u in list(_mykey['uids'].keys()):
|
||||||
|
_mykey['uids'][u]['updated'] = str(self.mykey['uids'][u]['updated'])
|
||||||
|
for s in list(_mykey['subkeys'].keys()):
|
||||||
|
_mykey['subkeys'][s] = str(self.mykey['subkeys'][s])
|
||||||
|
with open(os.path.join(_cachedir, '_SIGKEY.json'), 'w') as f:
|
||||||
|
f.write('{0}\n'.format(json.dumps(_mykey, sort_keys = True, indent = 4)))
|
||||||
|
return()
|
||||||
|
|
||||||
|
def serverParser(self, uri):
|
||||||
|
# https://en.wikipedia.org/wiki/Key_server_(cryptographic)#Keyserver_examples
|
||||||
|
_server = {}
|
||||||
|
_urlobj = urllib.parse.urlparse(uri)
|
||||||
|
_server['proto'] = _urlobj.scheme
|
||||||
|
_lazy = False
|
||||||
|
if not _server['proto']:
|
||||||
|
_server['proto'] = 'hkp' # Default
|
||||||
|
_server['server'] = _urlobj.hostname
|
||||||
|
if not _server['server']:
|
||||||
|
_server['server'] = re.sub('^([A-Za-z]://)?(.+[^:][^0-9])(:[0-9]+)?$', '\g<2>', uri, re.MULTILINE)
|
||||||
|
_lazy = True
|
||||||
|
_server['port'] = _urlobj.port
|
||||||
|
if not _server['port']:
|
||||||
|
if _lazy:
|
||||||
|
_p = re.sub('.*:([0-9]+)$', '\g<1>', uri, re.MULTILINE)
|
||||||
|
_server['port'] = self.maps['proto'][_server['proto']] # Default
|
||||||
|
return(_server)
|
||||||
|
|
||||||
|
def verifyArgs(self, locargs):
|
||||||
|
## Some pythonization...
|
||||||
|
if not locargs['batch']:
|
||||||
|
locargs['keys'] = [re.sub('\s', '', k) for k in locargs['keys'].split(',')]
|
||||||
|
else:
|
||||||
|
## Batch file
|
||||||
|
_batchfilepath = os.path.abspath(os.path.expanduser(locargs['keys']))
|
||||||
|
if not os.path.isfile(_batchfilepath):
|
||||||
|
raise ValueError('{0} does not exist or is not a regular file.'.format(_batchfilepath))
|
||||||
|
else:
|
||||||
|
with open(_batchfilepath, 'r') as f:
|
||||||
|
self.csvraw = f.readlines()
|
||||||
|
locargs['keys'] = _batchfilepath
|
||||||
|
locargs['keyservers'] = [re.sub('\s', '', s) for s in locargs['keyservers'].split(',')]
|
||||||
|
locargs['keyservers'] = [self.serverParser(s) for s in locargs['keyservers']]
|
||||||
|
## Key(s) to sign
|
||||||
|
locargs['rcpts'] = {}
|
||||||
|
if not locargs['batch']:
|
||||||
|
_keyiter = locargs['keys']
|
||||||
|
else:
|
||||||
|
_keyiter = []
|
||||||
|
for row in csv.reader(self.csvraw, delimiter = ',', quotechar = '"'):
|
||||||
|
_keyiter.append(row[0])
|
||||||
|
for k in _keyiter:
|
||||||
|
locargs['rcpts'][k] = {}
|
||||||
|
try:
|
||||||
|
int(k, 16)
|
||||||
|
_ktype = 'fpr'
|
||||||
|
except: # If it isn't a valid key ID...
|
||||||
|
if not re.match('^<?[\w\.\+\-]+\@[\w-]+\.[a-z]{2,3}>?$', k): # is it an email address?
|
||||||
|
raise ValueError('{0} is not a valid email address'.format(k))
|
||||||
|
else:
|
||||||
|
r = k.replace('<', '').replace('>', '')
|
||||||
|
locargs['rcpts'][r] = locargs['rcpts'][k]
|
||||||
|
if k != r:
|
||||||
|
del(locargs['rcpts'][k])
|
||||||
|
k = r
|
||||||
|
_ktype = 'email'
|
||||||
|
locargs['rcpts'][k]['type'] = _ktype
|
||||||
|
# Security is important. We don't want users getting collisions, so we don't allow shortened key IDs.
|
||||||
|
if _ktype == 'fpr' and not len(k) == 40:
|
||||||
|
raise ValueError('{0} is not a full 40-char key ID or key fingerprint'.format(k))
|
||||||
|
## Signing key
|
||||||
|
if not locargs['sigkey']:
|
||||||
|
raise ValueError('A key for signing is required') # We need a key we can sign with.
|
||||||
|
else:
|
||||||
|
if not os.path.lexists(locargs['gpgdir']):
|
||||||
|
raise FileNotFoundError('{0} does not exist'.format(locargs['gpgdir']))
|
||||||
|
elif os.path.isfile(locargs['gpgdir']):
|
||||||
|
raise NotADirectoryError('{0} is not a directory'.format(locargs['gpgdir']))
|
||||||
|
# Now we need to verify that the private key exists...
|
||||||
|
try:
|
||||||
|
_ctx = gpg.Context()
|
||||||
|
_sigkey = _ctx.get_key(locargs['sigkey'], True)
|
||||||
|
except gpg.errors.GPGMEError or gpg.errors.KeyNotFound:
|
||||||
|
raise ValueError('Cannot use key {0}'.format(locargs['sigkey']))
|
||||||
|
# And that it is an eligible candidate to use to sign.
|
||||||
|
if not _sigkey.can_sign or True in (_sigkey.revoked, _sigkey.expired, _sigkey.disabled):
|
||||||
|
raise ValueError('{0} is not a valid candidate for signing'.format(locargs['sigkey']))
|
||||||
|
## Keyservers
|
||||||
|
if locargs['testkeyservers']:
|
||||||
|
for s in locargs['keyservers']:
|
||||||
|
# Test to make sure the keyserver is accessible.
|
||||||
|
_v6test = socket(AF_INET6, SOCK_DGRAM)
|
||||||
|
try:
|
||||||
|
_v6test.connect(('ipv6.square-r00t.net', 0))
|
||||||
|
_nettype = AF_INET6 # We have IPv6 intarwebz
|
||||||
|
except:
|
||||||
|
_nettype = AF_INET # No IPv6, default to IPv4
|
||||||
|
for _proto in locargs['keyservers'][s]['port'][1]:
|
||||||
|
if _proto == 'udp':
|
||||||
|
_netproto = SOCK_DGRAM
|
||||||
|
elif _proto == 'tcp':
|
||||||
|
_netproto = SOCK_STREAM
|
||||||
|
_sock = socket(nettype, netproto)
|
||||||
|
_sock.settimeout(10)
|
||||||
|
_tests = _sock.connect_ex((locargs['keyservers'][s]['server'],
|
||||||
|
int(locargs['keyservers'][s]['port'][0])))
|
||||||
|
_uristr = '{0}://{1}:{2} ({3})'.format(locargs['keyservers'][s]['proto'],
|
||||||
|
locargs['keyservers'][s]['server'],
|
||||||
|
locargs['keyservers'][s]['port'][0],
|
||||||
|
_proto.upper())
|
||||||
|
if not tests == 0:
|
||||||
|
raise OSError('Keyserver {0} is not available'.format(_uristr))
|
||||||
|
else:
|
||||||
|
print('Keyserver {0} is accepting connections.'.format(_uristr))
|
||||||
|
sock.close()
|
||||||
|
return(locargs)
|
||||||
|
|
||||||
|
def parseArgs():
|
||||||
|
def getDefGPGDir():
|
||||||
|
try:
|
||||||
|
gpgdir = os.environ['GNUPGHOME']
|
||||||
|
except KeyError:
|
||||||
|
try:
|
||||||
|
homedir = os.environ['HOME']
|
||||||
|
gpgdchk = os.path.join(homedir, '.gnupg')
|
||||||
|
except KeyError:
|
||||||
|
# There is no reason that this should ever get this far, but... edge cases be crazy.
|
||||||
|
gpgdchk = os.path.join(os.path.expanduser('~'), '.gnupg')
|
||||||
|
if os.path.isdir(gpgdchk):
|
||||||
|
gpgdir = gpgdchk
|
||||||
|
else:
|
||||||
|
gpgdir = None
|
||||||
|
return(gpgdir)
|
||||||
|
def getDefKey(defgpgdir):
|
||||||
|
os.environ['GNUPGHOME'] = defgpgdir
|
||||||
|
if not defgpgdir:
|
||||||
|
return(None)
|
||||||
|
defkey = None
|
||||||
|
ctx = gpg.Context()
|
||||||
|
for k in ctx.keylist(None, secret = True): # "None" is query string; this grabs all keys in the private keyring
|
||||||
|
if k.can_sign and True not in (k.revoked, k.expired, k.disabled):
|
||||||
|
defkey = k.subkeys[0].fpr
|
||||||
|
break # We'll just use the first primary key we find that's valid as the default.
|
||||||
|
return(defkey)
|
||||||
|
def getDefKeyservers(defgpgdir):
|
||||||
|
srvlst = [None]
|
||||||
|
# We don't need these since we use the gpg agent. Requires GPG 2.1 and above, probably.
|
||||||
|
#if os.path.isfile(os.path.join(defgpgdir, 'dirmngr.conf')):
|
||||||
|
# pass
|
||||||
|
dirmgr_out = subprocess.run(['gpg-connect-agent', '--dirmngr', 'keyserver', '/bye'], stdout = subprocess.PIPE)
|
||||||
|
for l in dirmgr_out.stdout.decode('utf-8').splitlines():
|
||||||
|
#if len(l) == 3 and l.lower().startswith('s keyserver'): # It's a keyserver line
|
||||||
|
if l.lower().startswith('s keyserver'): # It's a keyserver line
|
||||||
|
s = l.split()[2]
|
||||||
|
if len(srvlst) == 1 and srvlst[0] == None:
|
||||||
|
srvlst = [s]
|
||||||
|
else:
|
||||||
|
srvlst.append(s)
|
||||||
|
return(','.join(srvlst))
|
||||||
|
defgpgdir = getDefGPGDir()
|
||||||
|
defkey = getDefKey(defgpgdir)
|
||||||
|
defkeyservers = getDefKeyservers(defgpgdir)
|
||||||
|
args = argparse.ArgumentParser(description = 'Keysigning Assistance and Notifying Tool (KANT)',
|
||||||
|
epilog = 'brent s. || 2017 || https://square-r00t.net')
|
||||||
|
args.add_argument('-k',
|
||||||
|
'--keys',
|
||||||
|
dest = 'keys',
|
||||||
|
metavar = 'KEYS | /path/to/batchfile',
|
||||||
|
required = True,
|
||||||
|
help = 'A single/comma-separated list of keys to sign, ' +
|
||||||
|
'trust, & notify. Can also be an email address. ' +
|
||||||
|
'If -b/--batch is specified, this should instead be ' +
|
||||||
|
'a path to the batch file. See the man page for more info.')
|
||||||
|
args.add_argument('-K',
|
||||||
|
'--sigkey',
|
||||||
|
dest = 'sigkey',
|
||||||
|
default = defkey,
|
||||||
|
help = 'The key to use when signing other keys. Default is \033[1m{0}\033[0m.'.format(defkey))
|
||||||
|
args.add_argument('-t',
|
||||||
|
'--trust',
|
||||||
|
dest = 'trustlevel',
|
||||||
|
default = None,
|
||||||
|
help = 'The trust level to automatically apply to all keys ' +
|
||||||
|
'(if not specified, kant will prompt for each key). ' +
|
||||||
|
'See BATCHFILE/TRUSTLEVEL in the man page for trust ' +
|
||||||
|
'level notations.')
|
||||||
|
args.add_argument('-c',
|
||||||
|
'--check',
|
||||||
|
dest = 'checklevel',
|
||||||
|
default = None,
|
||||||
|
help = 'The level of checking done (if not specified, kant will ' +
|
||||||
|
'prompt for each key). See -b/--batch for check level notations.')
|
||||||
|
args.add_argument('-l',
|
||||||
|
'--local',
|
||||||
|
dest = 'local',
|
||||||
|
default = None,
|
||||||
|
help = 'Make the signature(s) local-only (i.e. don\'t push to a keyserver).')
|
||||||
|
args.add_argument('-n',
|
||||||
|
'--no-notify',
|
||||||
|
dest = 'notify',
|
||||||
|
action = 'store_false',
|
||||||
|
help = 'If specified, do NOT notify any key recipients that you\'ve signed ' +
|
||||||
|
'their key, even if KANT is able to.')
|
||||||
|
args.add_argument('-s',
|
||||||
|
'--keyservers',
|
||||||
|
dest = 'keyservers',
|
||||||
|
default = defkeyservers,
|
||||||
|
help = 'The comma-separated keyserver(s) to push to.\n' +
|
||||||
|
'Default keyserver list is: \n\n\t\033[1m{0}\033[0m\n\n'.format(re.sub(',',
|
||||||
|
'\n\t',
|
||||||
|
defkeyservers)))
|
||||||
|
args.add_argument('-m',
|
||||||
|
'--msmtp',
|
||||||
|
dest = 'msmtp_profile',
|
||||||
|
default = None,
|
||||||
|
help = 'The msmtp profile to use to send the notification emails. See the man page for more information.')
|
||||||
|
args.add_argument('-b',
|
||||||
|
'--batch',
|
||||||
|
dest = 'batch',
|
||||||
|
action = 'store_true',
|
||||||
|
help = 'If specified, -k/--keys is a CSV file to use as a ' +
|
||||||
|
'batch run. See the BATCHFILE section in the man page for more info.')
|
||||||
|
args.add_argument('-D',
|
||||||
|
'--gpgdir',
|
||||||
|
dest = 'gpgdir',
|
||||||
|
default = defgpgdir,
|
||||||
|
help = 'The GnuPG configuration directory to use (containing\n' +
|
||||||
|
'your keys, etc.); default is \033[1m{0}\033[0m.'.format(defgpgdir))
|
||||||
|
args.add_argument('-T',
|
||||||
|
'--testkeyservers',
|
||||||
|
dest = 'testkeyservers',
|
||||||
|
action = 'store_true',
|
||||||
|
help = 'If specified, initiate a test connection with each\n'
|
||||||
|
'set keyserver before anything else. Disabled by default.')
|
||||||
|
return(args)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# This could be cleaner-looking, but we do it this way so the class can be used externally
|
||||||
|
# with a dict instead of an argparser result.
|
||||||
|
args = vars(parseArgs().parse_args())
|
||||||
|
sess = SigSession(args)
|
||||||
|
sess.modifyDirmngr('new')
|
||||||
|
sess.buildKeys()
|
||||||
|
sess.sigKeys()
|
||||||
|
sess.trustKeys()
|
||||||
|
sess.pushKeys()
|
||||||
|
sess.postalWorker()
|
||||||
|
sess.saveResults()
|
||||||
|
sess.modifyDirmngr('old')
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
282
gpg/kant/test.py
Executable file
282
gpg/kant/test.py
Executable file
@@ -0,0 +1,282 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# This is less of a test suite and more of an active documentation on some python-gpgme (https://pypi.python.org/pypi/gpg) examples.
|
||||||
|
# Because their only documentation for the python bindings is in pydoc, and the C API manual is kind of useless.
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import gpg
|
||||||
|
import gpg.constants
|
||||||
|
import inspect
|
||||||
|
import jinja2
|
||||||
|
import os
|
||||||
|
import pprint
|
||||||
|
import re
|
||||||
|
import smtplib
|
||||||
|
from email.mime.application import MIMEApplication
|
||||||
|
from email.mime.multipart import MIMEMultipart
|
||||||
|
from email.mime.text import MIMEText
|
||||||
|
import subprocess
|
||||||
|
import operator
|
||||||
|
from functools import reduce
|
||||||
|
|
||||||
|
os.environ['GNUPGHOME'] = '/home/bts/tmpgpg'
|
||||||
|
# JUST in case we need to...
|
||||||
|
#subprocess.run(['gpgconf', '--reload', 'dirmngr'])
|
||||||
|
|
||||||
|
# my key ID
|
||||||
|
#mykey = '748231EBCBD808A14F5E85D28C004C2F93481F6B'
|
||||||
|
mykey = '2805EC3D90E2229795AFB73FF85BC40E6E17F339'
|
||||||
|
# a key to test with
|
||||||
|
theirkey = 'CA7D304ABA7A3E24C9414D32FFA0F1361AD82A06'
|
||||||
|
testfetch = [theirkey, '748231EBCBD808A14F5E85D28C004C2F93481F6B']
|
||||||
|
|
||||||
|
# Create a context
|
||||||
|
# Params:
|
||||||
|
#armor -- enable ASCII armoring (default False)
|
||||||
|
#textmode -- enable canonical text mode (default False)
|
||||||
|
#offline -- do not contact external key sources (default False)
|
||||||
|
#signers -- list of keys used for signing (default [])
|
||||||
|
#pinentry_mode -- pinentry mode (default PINENTRY_MODE_DEFAULT)
|
||||||
|
#protocol -- protocol to use (default PROTOCOL_OpenPGP)
|
||||||
|
#home_dir -- state directory (default is the engine default)
|
||||||
|
ctx = gpg.Context()
|
||||||
|
|
||||||
|
# Fetch a key from the keyring
|
||||||
|
#secret -- to request a secret key
|
||||||
|
mkey = ctx.get_key(mykey)
|
||||||
|
tkey = ctx.get_key(theirkey)
|
||||||
|
|
||||||
|
## Print the attributes of our key and other info
|
||||||
|
##https://stackoverflow.com/a/41737776
|
||||||
|
##for k in (mkey, tkey):
|
||||||
|
#for k in [mkey]:
|
||||||
|
# for i in inspect.getmembers(k):
|
||||||
|
# if not i[0].startswith('_'):
|
||||||
|
# pprint.pprint(i)
|
||||||
|
#pprint.pprint(ctx.get_engine_info())
|
||||||
|
|
||||||
|
# Print the constants
|
||||||
|
#pprint.pprint(inspect.getmembers(gpg.constants))
|
||||||
|
|
||||||
|
# Get remote key. Use an OR to search both keyserver and local.
|
||||||
|
#ctx.set_keylist_mode(gpg.constants.KEYLIST_MODE_EXTERN|gpg.constants.KEYLIST_MODE_LOCAL)
|
||||||
|
klmodes = {'local': gpg.constants.KEYLIST_MODE_LOCAL,
|
||||||
|
'remote': gpg.constants.KEYLIST_MODE_EXTERN,
|
||||||
|
'both': gpg.constants.KEYLIST_MODE_LOCAL|gpg.constants.KEYLIST_MODE_EXTERN}
|
||||||
|
|
||||||
|
# List keys
|
||||||
|
#pattern -- return keys matching pattern (default: all keys)
|
||||||
|
#secret -- return only secret keys (default: False)
|
||||||
|
#mode -- keylist mode (default: list local keys)
|
||||||
|
#source -- read keys from source instead from the keyring
|
||||||
|
# (all other options are ignored in this case)
|
||||||
|
tkey2 = None
|
||||||
|
|
||||||
|
# jrdemasi@gmail.com = 0xEFD9413B17293AFDFE6EA6F1402A088DEDF104CB
|
||||||
|
for k in ctx.keylist(pattern = 'jrdemasi', secret = False, mode = klmodes['remote'], source = None):
|
||||||
|
#pprint.pprint(inspect.getmembers(k))
|
||||||
|
tkey2 = k
|
||||||
|
#print(tkey2.fpr)
|
||||||
|
|
||||||
|
# Test fetching from a keyserver - we'll grab the last key from the above iteration
|
||||||
|
try:
|
||||||
|
ctx.op_import_keys([tkey2])
|
||||||
|
except gpg.errors.GPGMEError:
|
||||||
|
pass # key isn't on the keyserver, or it isn't accessible, etc.
|
||||||
|
|
||||||
|
# Test signing
|
||||||
|
ctx.key_tofu_policy(tkey2, gpg.constants.TOFU_POLICY_ASK)
|
||||||
|
ctx.signers = [mkey]
|
||||||
|
days_valid = 4
|
||||||
|
exptime = 4 * 24 * 60 * 60
|
||||||
|
ctx.key_sign(tkey2, expires_in = exptime, local = True)
|
||||||
|
|
||||||
|
# https://www.apt-browse.org/browse/debian/wheezy/main/amd64/python-pyme/1:0.8.1-2/file/usr/share/doc/python-pyme/examples/t-edit.py
|
||||||
|
# https://searchcode.com/codesearch/view/20535820/
|
||||||
|
# https://git.gnupg.org/cgi-bin/gitweb.cgi?p=gnupg.git;a=blob;f=doc/DETAILS;h=0be55f4d64178a5636cbe9f12f63c6f9853f3aa2;hb=refs/heads/master
|
||||||
|
class KeyEditor(object):
|
||||||
|
def __init__(self):
|
||||||
|
self.replied_once = False
|
||||||
|
trust = '3' # this is the level of trust... in this case, marginal.
|
||||||
|
rcptemail = 'test@test.com'
|
||||||
|
# we exclude 'help'
|
||||||
|
self.kprmpt = ['trust', 'fpr', 'sign', 'tsign', 'lsign', 'nrsign', 'grip', 'list',
|
||||||
|
'uid', 'key', 'check', 'deluid', 'delkey', 'delsig', 'pref', 'showpref',
|
||||||
|
'revsig', 'enable', 'disable', 'showphoto', 'clean', 'minimize', 'save',
|
||||||
|
'quit']
|
||||||
|
self.prmpt = {'edit_ownertrust': {'value': trust,
|
||||||
|
'set_ultimate': {'okay': 'yes'}},
|
||||||
|
'untrusted_key': {'override': 'yes'},
|
||||||
|
'pklist': {'user_id': {'enter': rcptemail}},
|
||||||
|
'keyedit': {'prompt': 'trust', # the mode we initiate.
|
||||||
|
'save': {'okay': 'yes'}}}
|
||||||
|
|
||||||
|
def edit_fnc(self, status, args, out):
|
||||||
|
result = None
|
||||||
|
out.seek(0, 0)
|
||||||
|
#print(status, args)
|
||||||
|
#print(out.read().decode('utf-8'))
|
||||||
|
#print('{0} ({1})'.format(status, args))
|
||||||
|
def mapDict(m, d):
|
||||||
|
return(reduce(operator.getitem, m, d))
|
||||||
|
if args == 'keyedit.prompt' and self.replied_once:
|
||||||
|
result = 'quit'
|
||||||
|
elif status == 'KEY_CONSIDERED':
|
||||||
|
result = None
|
||||||
|
self.replied_once = False
|
||||||
|
elif status == 'GET_LINE':
|
||||||
|
#print('DEBUG: looking up mapping...')
|
||||||
|
self.replied_once = True
|
||||||
|
_ilist = args.split('.')
|
||||||
|
result = mapDict(_ilist, self.prmpt)
|
||||||
|
if not result:
|
||||||
|
result = None
|
||||||
|
return(result)
|
||||||
|
|
||||||
|
# Test setting trust
|
||||||
|
out = gpg.Data()
|
||||||
|
ctx.interact(tkey2, KeyEditor().edit_fnc, sink = out, fnc_value = out)
|
||||||
|
out.seek(0, 0)
|
||||||
|
#print(out.read(), end = '\n\n')
|
||||||
|
|
||||||
|
#Test sending to a keyserver
|
||||||
|
buf = gpg.Data()
|
||||||
|
ctx.op_export(tkey2.fpr, gpg.constants.EXPORT_MODE_EXTERN, None)
|
||||||
|
|
||||||
|
# Test writing the pubkey out to a file
|
||||||
|
buf = gpg.Data()
|
||||||
|
ctx.op_export_keys([tkey2], 0, buf) # do i NEED to specify a mode?
|
||||||
|
buf.seek(0, 0)
|
||||||
|
with open('/tmp/pubkeytest.gpg', 'wb') as f:
|
||||||
|
f.write(buf.read())
|
||||||
|
#del(buf)
|
||||||
|
# Let's also test writing out the ascii-armored..
|
||||||
|
ctx.armor = True
|
||||||
|
#buf = gpg.Data()
|
||||||
|
buf.seek(0, 0)
|
||||||
|
ctx.op_export_keys([tkey2], 0, buf) # do i NEED to specify a mode?
|
||||||
|
buf.seek(0, 0)
|
||||||
|
#print(buf.read())
|
||||||
|
#buf.seek(0, 0)
|
||||||
|
with open('/tmp/pubkeytest.asc', 'wb') as f:
|
||||||
|
f.write(buf.read())
|
||||||
|
del(buf)
|
||||||
|
|
||||||
|
# And lastly, let's test msmtprc
|
||||||
|
def getCfg(fname):
|
||||||
|
cfg = {'default': None, 'defaults': {}}
|
||||||
|
_defaults = False
|
||||||
|
_acct = None
|
||||||
|
with open(fname, 'r') as f:
|
||||||
|
cfg_raw = f.read()
|
||||||
|
for l in cfg_raw.splitlines():
|
||||||
|
if re.match('^\s?(#.*|)$', l):
|
||||||
|
continue # skip over blank and commented lines
|
||||||
|
line = [i.strip() for i in re.split('\s+', l.strip(), maxsplit = 1)]
|
||||||
|
if line[0] == 'account':
|
||||||
|
if re.match('^default\s?:\s?', line[1]): # it's the default account specifier
|
||||||
|
cfg['default'] = line[1].split(':', maxsplit = 1)[1].strip()
|
||||||
|
else:
|
||||||
|
if line[1] not in cfg.keys(): # it's a new account definition
|
||||||
|
cfg[line[1]] = {}
|
||||||
|
_acct = line[1]
|
||||||
|
_defaults = False
|
||||||
|
elif line[0] == 'defaults': # it's the defaults
|
||||||
|
_acct = 'defaults'
|
||||||
|
else: # it's a config directive
|
||||||
|
cfg[_acct][line[0]] = line[1]
|
||||||
|
for a in list(cfg):
|
||||||
|
if a != 'default':
|
||||||
|
for k, v in cfg['defaults'].items():
|
||||||
|
if k not in cfg[a].keys():
|
||||||
|
cfg[a][k] = v
|
||||||
|
del(cfg['defaults'])
|
||||||
|
return(cfg)
|
||||||
|
homeconf = os.path.join(os.environ['HOME'], '.msmtprc')
|
||||||
|
sysconf = '/etc/msmtprc'
|
||||||
|
msmtp = {'path': None}
|
||||||
|
if not os.path.isfile(homeconf):
|
||||||
|
if not os.path.isfile(sysconf):
|
||||||
|
msmtp['conf'] = False
|
||||||
|
else:
|
||||||
|
msmtp['conf'] = sysconf
|
||||||
|
else:
|
||||||
|
msmtp['conf'] = homeconf
|
||||||
|
if os.path.isfile(msmtp['conf']):
|
||||||
|
path = os.environ['PATH']
|
||||||
|
for p in path.split(':'):
|
||||||
|
fullpath = os.path.join(p, 'msmtp')
|
||||||
|
if os.path.isfile(fullpath):
|
||||||
|
msmtp['path'] = fullpath
|
||||||
|
break # break out the first instance of it we find since the shell parses PATH first to last and so do we
|
||||||
|
if msmtp['path']:
|
||||||
|
# Okay. So we have a config file, which we're assuming to be set up correctly, and a path to a binary.
|
||||||
|
# Now we need to parse the config.
|
||||||
|
msmtp['cfg'] = getCfg(msmtp['conf'])
|
||||||
|
pprint.pprint(msmtp)
|
||||||
|
if msmtp['path']:
|
||||||
|
# Get the appropriate MSMTP profile
|
||||||
|
profile = msmtp['cfg']['default']
|
||||||
|
# Buuuut i use a different profile when i test, because i use msmtp for production-type stuff.
|
||||||
|
#if os.environ['USER'] == 'bts':
|
||||||
|
# profile = 'gmailtesting'
|
||||||
|
# Now we can try to send an email... yikes.
|
||||||
|
## First we set up the message templates.
|
||||||
|
body_in = {'plain': None, 'html': None}
|
||||||
|
body_in['plain'] = """Hello, person!
|
||||||
|
|
||||||
|
This is a test message.
|
||||||
|
|
||||||
|
Thanks."""
|
||||||
|
body_in['html'] = """\
|
||||||
|
<html>
|
||||||
|
<head></head>
|
||||||
|
<body>
|
||||||
|
<p><b>Hi there, person!</b> This is a test email.</p>
|
||||||
|
<p>It supports fun things like HTML.</p>
|
||||||
|
<p>--<br><a href='https://games.square-r00t.net/'>https://games.square-r00t.net</a><br>
|
||||||
|
Admin: <a href='mailto:bts@square-r00t.net'>r00t^2</a>
|
||||||
|
</p>
|
||||||
|
</body>
|
||||||
|
</html>"""
|
||||||
|
# Now, some attachments.
|
||||||
|
part = {}
|
||||||
|
ctx.armor = False
|
||||||
|
buf = gpg.Data()
|
||||||
|
ctx.op_export_keys([tkey2], 0, buf)
|
||||||
|
buf.seek(0, 0)
|
||||||
|
part['gpg'] = MIMEApplication(buf.read(), '{0}.gpg'.format(tkey2.fpr))
|
||||||
|
part['gpg']['Content-Disposition'] = 'attachment; filename="{0}.gpg"'.format(tkey2.fpr)
|
||||||
|
ctx.armor = True
|
||||||
|
buf.seek(0, 0)
|
||||||
|
ctx.op_export_keys([tkey2], 0, buf)
|
||||||
|
buf.seek(0, 0)
|
||||||
|
part['asc'] = MIMEApplication(buf.read(), '{0}.asc'.format(tkey2.fpr))
|
||||||
|
part['asc']['Content-Disposition'] = 'attachment; filename="{0}.asc"'.format(tkey2.fpr)
|
||||||
|
#msg = MIMEMultipart('alternative')
|
||||||
|
msg = MIMEMultipart()
|
||||||
|
msg['preamble'] = 'This is a multi-part message in MIME format.\n'
|
||||||
|
msg['From'] = msmtp['cfg'][profile]['from']
|
||||||
|
msg['To'] = msmtp['cfg'][profile]['from'] # to send to more than one: ', '.join(somelist)
|
||||||
|
msg['Date'] = datetime.datetime.now().strftime('%a, %d %b %Y %H:%M:%S %z')
|
||||||
|
msg['Subject'] = 'TEST EMAIL VIA TEST.PY'
|
||||||
|
msg['epilogue'] = ''
|
||||||
|
body = MIMEMultipart('alternative')
|
||||||
|
body.attach(MIMEText(body_in['plain'], 'plain'))
|
||||||
|
body.attach(MIMEText(body_in['html'], 'html'))
|
||||||
|
msg.attach(body)
|
||||||
|
for f in part.keys():
|
||||||
|
msg.attach(part[f])
|
||||||
|
|
||||||
|
# This needs way more parsing to support things like plain ol' port 25 plaintext (ugh), etc.
|
||||||
|
if 'tls-starttls' in msmtp['cfg'][profile].keys() and msmtp['cfg'][profile]['tls-starttls'] == 'on':
|
||||||
|
smtpserver = smtplib.SMTP(msmtp['cfg'][profile]['host'], int(msmtp['cfg'][profile]['port']))
|
||||||
|
smtpserver.ehlo()
|
||||||
|
smtpserver.starttls()
|
||||||
|
# we need to EHLO again after a STARTTLS because email is weird.
|
||||||
|
elif msmtp['cfg'][profile]['tls'] == 'on':
|
||||||
|
smtpserver = smtplib.SMTP_SSL(msmtp['cfg'][profile]['host'], int(msmtp['cfg'][profile]['port']))
|
||||||
|
smtpserver.ehlo()
|
||||||
|
smtpserver.login(msmtp['cfg'][profile]['user'], msmtp['cfg'][profile]['password'])
|
||||||
|
smtpserver.sendmail(msmtp['cfg'][profile]['user'], msg['To'], msg.as_string())
|
||||||
|
smtpserver.close()
|
||||||
5
gpg/kant/testbatch.kant.csv
Normal file
5
gpg/kant/testbatch.kant.csv
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
748231EBCBD808A14F5E85D28C004C2F93481F6B,4,1,3,1
|
||||||
|
A03CACFD7123AF443A3A185298A8A46921C8DDEF,-1,0,0,0
|
||||||
|
EFD9413B17293AFDFE6EA6F1402A088DEDF104CB,full,true,casual,yes
|
||||||
|
6FA8AE12AEC90B035EEE444FE70457341A63E830,2,True,Casual,True
|
||||||
|
<admin@sysadministrivia.com>, full, yes, careful, false
|
||||||
|
297
gpg/sksdump.py
297
gpg/sksdump.py
@@ -1,21 +1,9 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
# NOTE: This was written for systemd systems only.
|
|
||||||
# Tweaking would be needed for non-systemd systems
|
|
||||||
# (since every non-systemd uses their own init system callables...)
|
|
||||||
#
|
|
||||||
# Thanks to Matt Rude and https://gist.github.com/mattrude/b0ac735d07b0031bb002 so I can know what the hell I'm doing.
|
# Thanks to Matt Rude and https://gist.github.com/mattrude/b0ac735d07b0031bb002 so I can know what the hell I'm doing.
|
||||||
#
|
|
||||||
# IMPORTANT: This script uses certaion permissions functions that require some forethought. You can either run as root,
|
|
||||||
# which is the "easy" way, OR you can run as the sks user. Has to be one or the other; you'll SERIOUSLY mess things up
|
|
||||||
# otherwise. If you run as the sks user, MAKE SURE the following is set in your sudoers (where SKSUSER is the username sks runs as:
|
|
||||||
# Cmnd_Alias SKSCMDS = /usr/bin/systemctl start sks-db,\
|
|
||||||
# /usr/bin/systemctl stop sks-db,\
|
|
||||||
# /usr/bin/systemctl start sks-recon,\
|
|
||||||
# /usr/bin/systemctl stop sks-recon
|
|
||||||
# SKSUSER ALL = NOPASSWD: SKSCMDS
|
|
||||||
|
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import configparser
|
||||||
import datetime
|
import datetime
|
||||||
import getpass
|
import getpass
|
||||||
import os
|
import os
|
||||||
@@ -26,138 +14,289 @@ from grp import getgrnam
|
|||||||
NOW = datetime.datetime.utcnow()
|
NOW = datetime.datetime.utcnow()
|
||||||
NOWstr = NOW.strftime('%Y-%m-%d')
|
NOWstr = NOW.strftime('%Y-%m-%d')
|
||||||
|
|
||||||
sks = {
|
# TODO:
|
||||||
# chowning - MAKE SURE THIS IS THE USER SKS RUNS AS.
|
# - cleanup/rotation should be optional
|
||||||
'user': 'sks',
|
|
||||||
# chowning
|
cfgfile = os.path.join(os.environ['HOME'], '.sksdump.ini')
|
||||||
|
|
||||||
|
def getDefaults():
|
||||||
|
# Hardcoded defaults
|
||||||
|
dflt = {'system': {'user': 'sks',
|
||||||
'group': 'sks',
|
'group': 'sks',
|
||||||
# Where your SKS DB is
|
'compress': 'xz',
|
||||||
'basedir': '/var/lib/sks',
|
|
||||||
# Where the dumps should go. This dir is scrubbed based on mtime, so ONLY use this dir for dumps.
|
|
||||||
'destdir': '/srv/http/sks/dumps',
|
|
||||||
# If None, don't compress dumps. If one of: 'xz', 'gz', 'bz2', or 'lrz' (for lrzip) then use that compression algo.
|
|
||||||
'compress': 'lrz',
|
|
||||||
# The service name(s) to stop for the dump and to start again afterwards.
|
|
||||||
'svcs': ['sks-db', 'sks-recon'],
|
'svcs': ['sks-db', 'sks-recon'],
|
||||||
# I would hope this is self-explanatory. If not, this is where we log the outout of the sks dump process. (and any rsync errors, too)
|
|
||||||
'logfile': '/var/log/sksdump.log',
|
'logfile': '/var/log/sksdump.log',
|
||||||
# If not None value, where we should push the dumps when done. Can be a local path too, obviously.
|
|
||||||
'rsync': 'root@sks.mirror.square-r00t.net:/srv/http/sks/dumps/.',
|
|
||||||
# How many previous days of dumps should we keep?
|
|
||||||
'days': 1,
|
'days': 1,
|
||||||
# How many keys to include per dump file
|
'dumpkeys': 15000},
|
||||||
'dumpkeys': 15000
|
'paths': {'basedir': '/var/lib/sks',
|
||||||
}
|
'destdir': '/srv/http/sks/dumps',
|
||||||
|
'rsync': 'root@mirror.square-r00t.net:/srv/http/sks/dumps'},
|
||||||
|
'runtime': {'nodump': None, 'nocompress': None, 'nosync': None}}
|
||||||
|
## Build out the default .ini.
|
||||||
|
dflt_str = ('# IMPORTANT: This script uses certain permissions functions that require some forethought.\n' +
|
||||||
|
'# You can either run as root, which is the "easy" way, OR you can run as the sks user.\n' +
|
||||||
|
'# Has to be one or the other; you\'ll SERIOUSLY mess things up otherwise.\n' +
|
||||||
|
'# If you run as the sks user, MAKE SURE the following is set in your sudoers\n' +
|
||||||
|
'# (where SKSUSER is the username sks runs as):\n#\tCmnd_Alias SKSCMDS = ' +
|
||||||
|
'/usr/bin/systemctl start sks-db,\\\n#\t\t/usr/bin/systemctl stop sks-db,\\\n#\t\t' +
|
||||||
|
'/usr/bin/systemctl start sks-recon,\\\n#\t\t/usr/bin/systemctl stop sks-recon\n#\t' +
|
||||||
|
'SKSUSER ALL = NOPASSWD: SKSCMDS\n\n')
|
||||||
|
dflt_str += ('# This was written for systemd systems only. Tweaking would be needed for non-systemd systems\n' +
|
||||||
|
'# (since every non-systemd uses their own init system callables...)\n\n')
|
||||||
|
# [system]
|
||||||
|
d = dflt['system']
|
||||||
|
dflt_str += ('## SKSDUMP CONFIG FILE ##\n\n# This section controls various system configuration.\n' +
|
||||||
|
'[system]\n# This should be the user SKS runs as.\nuser = {0}\n# This is the group that' +
|
||||||
|
'SKS runs as.\ngroup = {1}\n# If None, don\'t compress dumps.\n# If one of: ' +
|
||||||
|
'xz, gz, bz2, or lrz (for lrzip) then use that compression algo.\ncompress = {2}\n' +
|
||||||
|
'# These services will be started/stopped, in order, before/after dumps. ' +
|
||||||
|
'Comma-separated.\nsvcs = {3}\n# The path to the logfile.\nlogfile = {4}\n# The number ' +
|
||||||
|
'of days of rotated key dumps. If None, don\'t rotate.\ndays = {5}\n# How many keys to include in each ' +
|
||||||
|
'dump file.\ndumpkeys = {6}\n\n').format(d['user'],
|
||||||
|
d['group'],
|
||||||
|
d['compress'],
|
||||||
|
','.join(d['svcs']),
|
||||||
|
d['logfile'],
|
||||||
|
d['days'],
|
||||||
|
d['dumpkeys'])
|
||||||
|
# [paths]
|
||||||
|
d = dflt['paths']
|
||||||
|
dflt_str += ('# This section controls where stuff goes and where we should find it.\n[paths]\n# ' +
|
||||||
|
'Where your SKS DB is.\nbasedir = {0}\n# This is the base directory where the dumps should go.\n' +
|
||||||
|
'# There will be a sub-directory created for each date.\ndestdir = {1}\n# The ' +
|
||||||
|
'path for rsyncing the dumps. If None, don\'t rsync.\nrsync = {2}\n\n').format(d['basedir'],
|
||||||
|
d['destdir'],
|
||||||
|
d['rsync'])
|
||||||
|
# [runtime]
|
||||||
|
d = dflt['runtime']
|
||||||
|
dflt_str += ('# This section controls runtime options. These can be overridden at the commandline.\n' +
|
||||||
|
'# They take no values; they\'re merely options.\n[runtime]\n# Don\'t dump any keys.\n' +
|
||||||
|
'# Useful for dedicated in-transit/prep boxes.\n;nodump\n# Don\'t compress the dumps, even if ' +
|
||||||
|
'we have a compression scheme specified in [system:compress].\n;nocompress\n# Don\'t sync to' +
|
||||||
|
'another server/path, even if one is specified in [paths:rsync].\n;nosync\n')
|
||||||
|
realcfg = configparser.ConfigParser(defaults = dflt, allow_no_value = True)
|
||||||
|
if not os.path.isfile(cfgfile):
|
||||||
|
with open(cfgfile, 'w') as f:
|
||||||
|
f.write(dflt_str)
|
||||||
|
realcfg.read(cfgfile)
|
||||||
|
return(realcfg)
|
||||||
|
|
||||||
|
def svcMgmt(op, args):
|
||||||
# symlinks? relative path? HOME reference? WE HANDLE IT ALL.
|
|
||||||
sks['destdir'] = os.path.realpath(os.path.abspath(os.path.expanduser(sks['destdir'])))
|
|
||||||
|
|
||||||
def svcMgmt(op):
|
|
||||||
if op not in ('start', 'stop'):
|
if op not in ('start', 'stop'):
|
||||||
raise ValueError('Operation must be start or stop')
|
raise ValueError('Operation must be start or stop')
|
||||||
for svc in sks['svcs']:
|
for svc in args['svcs'].split(','):
|
||||||
cmd = ['/usr/bin/systemctl', op, svc]
|
cmd = ['/usr/bin/systemctl', op, svc.strip()]
|
||||||
if getpass.getuser() != 'root':
|
if getpass.getuser() != 'root':
|
||||||
cmd.insert(0, 'sudo')
|
cmd.insert(0, 'sudo')
|
||||||
subprocess.run(cmd)
|
subprocess.run(cmd)
|
||||||
return()
|
return()
|
||||||
|
|
||||||
def destPrep():
|
def destPrep(args):
|
||||||
nowdir = os.path.join(sks['destdir'], NOWstr)
|
nowdir = os.path.join(args['destdir'], NOWstr)
|
||||||
curdir = os.path.join(sks['destdir'], 'current')
|
curdir = os.path.join(args['destdir'], 'current')
|
||||||
PAST = NOW - datetime.timedelta(days = sks['days'])
|
PAST = NOW - datetime.timedelta(days = args['days'])
|
||||||
for thisdir, dirs, files in os.walk(sks['destdir']):
|
for thisdir, dirs, files in os.walk(args['destdir'], topdown = False):
|
||||||
for f in files:
|
for f in files:
|
||||||
|
try: # we use a try here because if the link's broken, the script bails out.
|
||||||
fstat = os.stat(os.path.join(thisdir, f))
|
fstat = os.stat(os.path.join(thisdir, f))
|
||||||
mtime = fstat.st_mtime
|
mtime = fstat.st_mtime
|
||||||
if int(mtime) < PAST.timestamp():
|
if int(mtime) < PAST.timestamp():
|
||||||
os.remove(os.path.join(thisdir, f))
|
os.remove(os.path.join(thisdir, f))
|
||||||
|
except FileNotFoundError: # broken symlink
|
||||||
try:
|
try:
|
||||||
os.removedirs(sks['destdir']) # Remove empty dirs
|
os.remove(os.path.join(thisdir, f))
|
||||||
except:
|
except:
|
||||||
pass # thisisfine.jpg
|
pass # just... ignore it. it's fine, whatever.
|
||||||
|
# Delete if empty dir
|
||||||
|
if os.path.isdir(thisdir):
|
||||||
|
if len(os.listdir(thisdir)) == 0:
|
||||||
|
os.rmdir(thisdir)
|
||||||
|
for d in dirs:
|
||||||
|
_dir = os.path.join(thisdir, d)
|
||||||
|
if os.path.isdir(_dir):
|
||||||
|
if len(os.listdir(_dir)) == 0:
|
||||||
|
os.rmdir(os.path.join(thisdir, d))
|
||||||
|
#try:
|
||||||
|
# os.removedirs(sks['destdir']) # Remove empty dirs
|
||||||
|
#except:
|
||||||
|
# pass # thisisfine.jpg
|
||||||
os.makedirs(nowdir, exist_ok = True)
|
os.makedirs(nowdir, exist_ok = True)
|
||||||
if getpass.getuser() == 'root':
|
if getpass.getuser() == 'root':
|
||||||
uid = getpwnam(sks['user']).pw_uid
|
uid = getpwnam(args['user']).pw_uid
|
||||||
gid = getgrnam(sks['group']).gr_gid
|
gid = getgrnam(args['group']).gr_gid
|
||||||
for d in (sks['destdir'], nowdir):
|
for d in (args['destdir'], nowdir): # we COULD set it as part of the os.makedirs, but iirc it doesn't set it for existing dirs
|
||||||
os.chown(d, uid, gid)
|
os.chown(d, uid, gid)
|
||||||
if os.path.isdir(curdir):
|
if os.path.isdir(curdir):
|
||||||
os.remove(curdir)
|
os.remove(curdir)
|
||||||
os.symlink(NOWstr, curdir, target_is_directory = True)
|
os.symlink(NOWstr, curdir, target_is_directory = True)
|
||||||
return()
|
return()
|
||||||
|
|
||||||
def dumpDB():
|
def dumpDB(args):
|
||||||
destPrep()
|
destPrep(args)
|
||||||
os.chdir(sks['basedir'])
|
os.chdir(args['basedir'])
|
||||||
svcMgmt('stop')
|
svcMgmt('stop', args)
|
||||||
cmd = ['sks',
|
cmd = ['sks',
|
||||||
'dump',
|
'dump',
|
||||||
str(sks['dumpkeys']), # How many keys per dump?
|
str(args['dumpkeys']), # How many keys per dump?
|
||||||
os.path.join(sks['destdir'], NOWstr), # Where should it go?
|
os.path.join(args['destdir'], NOWstr), # Where should it go?
|
||||||
'keydump.{0}'.format(NOWstr)] # What the filename prefix should be
|
'keydump.{0}'.format(NOWstr)] # What the filename prefix should be
|
||||||
if getpass.getuser() == 'root':
|
if getpass.getuser() == 'root':
|
||||||
cmd2 = ['sudo', '-u', sks['user']]
|
cmd2 = ['sudo', '-u', args['user']]
|
||||||
cmd2.extend(cmd)
|
cmd2.extend(cmd)
|
||||||
cmd = cmd2
|
cmd = cmd2
|
||||||
with open(sks['logfile'], 'a') as f:
|
with open(args['logfile'], 'a') as f:
|
||||||
f.write('===== {0} =====\n'.format(str(datetime.datetime.utcnow())))
|
f.write('===== {0} =====\n'.format(str(datetime.datetime.utcnow())))
|
||||||
subprocess.run(cmd, stdout = f, stderr = f)
|
subprocess.run(cmd, stdout = f, stderr = f)
|
||||||
svcMgmt('start')
|
svcMgmt('start', args)
|
||||||
return()
|
return()
|
||||||
|
|
||||||
def compressDB():
|
def compressDB(args):
|
||||||
if not sks['compress']:
|
if not args['compress']:
|
||||||
return()
|
return()
|
||||||
curdir = os.path.join(sks['destdir'], NOWstr)
|
curdir = os.path.join(args['destdir'], NOWstr)
|
||||||
for thisdir, dirs, files in os.walk(curdir): # I use os.walk here because we might handle this differently in the future...
|
for thisdir, dirs, files in os.walk(curdir): # I use os.walk here because we might handle this differently in the future...
|
||||||
|
files.sort()
|
||||||
for f in files:
|
for f in files:
|
||||||
fullpath = os.path.join(thisdir, f)
|
fullpath = os.path.join(thisdir, f)
|
||||||
newfile = '{0}.{1}'.format(fullpath, sks['compress'])
|
newfile = '{0}.{1}'.format(fullpath, args['compress'])
|
||||||
with open(sks['logfile'], 'a') as f:
|
# TODO: add compressed tarball support.
|
||||||
|
# However, I can't do this on memory-constrained systems for lrzip.
|
||||||
|
# See: https://github.com/kata198/python-lrzip/issues/1
|
||||||
|
with open(args['logfile'], 'a') as f:
|
||||||
f.write('===== {0} Now compressing {1} =====\n'.format(str(datetime.datetime.utcnow()), fullpath))
|
f.write('===== {0} Now compressing {1} =====\n'.format(str(datetime.datetime.utcnow()), fullpath))
|
||||||
if sks['compress'].lower() == 'gz':
|
if args['compress'].lower() == 'gz':
|
||||||
import gzip
|
import gzip
|
||||||
with open(fullpath, 'rb') as fh_in, gzip.open(newfile, 'wb') as fh_out:
|
with open(fullpath, 'rb') as fh_in, gzip.open(newfile, 'wb') as fh_out:
|
||||||
fh_out.writelines(fh_in)
|
fh_out.writelines(fh_in)
|
||||||
elif sks['compress'].lower() == 'xz':
|
elif args['compress'].lower() == 'xz':
|
||||||
import lzma
|
import lzma
|
||||||
with open(fullpath, 'rb') as fh_in, lzma.open(newfile, 'wb', preset = 9|lzma.PRESET_EXTREME) as fh_out:
|
with open(fullpath, 'rb') as fh_in, lzma.open(newfile, 'wb', preset = 9|lzma.PRESET_EXTREME) as fh_out:
|
||||||
fh_out.writelines(fh_in)
|
fh_out.writelines(fh_in)
|
||||||
elif sks['compress'].lower() == 'bz2':
|
elif args['compress'].lower() == 'bz2':
|
||||||
import bz2
|
import bz2
|
||||||
with open(fullpath, 'rb') as fh_in, bz2.open(newfile, 'wb') as fh_out:
|
with open(fullpath, 'rb') as fh_in, bz2.open(newfile, 'wb') as fh_out:
|
||||||
fh_out.writelines(fh_in)
|
fh_out.writelines(fh_in)
|
||||||
elif sks['compress'].lower() == 'lrz':
|
elif args['compress'].lower() == 'lrz':
|
||||||
import lrzip
|
import lrzip
|
||||||
with open(fullpath, 'rb') as fh_in, open(newfile, 'wb') as fh_out:
|
with open(fullpath, 'rb') as fh_in, open(newfile, 'wb') as fh_out:
|
||||||
fh_out.write(lrzip.compress(fh_in.read()))
|
fh_out.write(lrzip.compress(fh_in.read()))
|
||||||
os.remove(fullpath)
|
os.remove(fullpath)
|
||||||
if getpass.getuser() == 'root':
|
if getpass.getuser() == 'root':
|
||||||
uid = getpwnam(sks['user']).pw_uid
|
uid = getpwnam(args['user']).pw_uid
|
||||||
gid = getgrnam(sks['group']).gr_gid
|
gid = getgrnam(args['group']).gr_gid
|
||||||
os.chown(newfile, uid, gid)
|
os.chown(newfile, uid, gid)
|
||||||
return()
|
return()
|
||||||
|
|
||||||
def syncDB():
|
def syncDB(args):
|
||||||
if not sks['rsync']:
|
if not args['rsync']:
|
||||||
return()
|
return()
|
||||||
cmd = ['rsync',
|
cmd = ['rsync',
|
||||||
'-a',
|
'-a',
|
||||||
'--delete',
|
'--delete',
|
||||||
os.path.join(sks['destdir'], '.'),
|
os.path.join(args['destdir'], '.'),
|
||||||
sks['rsync']]
|
args['rsync']]
|
||||||
with open(sks['logfile'], 'a') as f:
|
with open(args['logfile'], 'a') as f:
|
||||||
|
f.write('===== {0} Rsyncing to mirror =====\n'.format(str(datetime.datetime.utcnow())))
|
||||||
|
with open(args['logfile'], 'a') as f:
|
||||||
subprocess.run(cmd, stdout = f, stderr = f)
|
subprocess.run(cmd, stdout = f, stderr = f)
|
||||||
return()
|
return()
|
||||||
|
|
||||||
|
def parseArgs():
|
||||||
|
cfg = getDefaults()
|
||||||
|
system = cfg['system']
|
||||||
|
paths = cfg['paths']
|
||||||
|
runtime = cfg['runtime']
|
||||||
|
args = argparse.ArgumentParser(description = 'sksdump - a tool for dumping the SKS Database',
|
||||||
|
epilog = 'brent s. || 2017 || https://square-r00t.net')
|
||||||
|
args.add_argument('-u',
|
||||||
|
'--user',
|
||||||
|
default = system['user'],
|
||||||
|
dest = 'user',
|
||||||
|
help = 'The user that you run SKS services as.')
|
||||||
|
args.add_argument('-g',
|
||||||
|
'--group',
|
||||||
|
default = system['group'],
|
||||||
|
dest = 'group',
|
||||||
|
help = 'The group that SKS services run as.')
|
||||||
|
args.add_argument('-c',
|
||||||
|
'--compress',
|
||||||
|
default = system['compress'],
|
||||||
|
dest = 'compress',
|
||||||
|
choices = ['xz', 'gz', 'bz2', 'lrz', None],
|
||||||
|
help = 'The compression scheme to apply to the dumps.')
|
||||||
|
args.add_argument('-s',
|
||||||
|
'--services',
|
||||||
|
default = system['svcs'],
|
||||||
|
dest = 'svcs',
|
||||||
|
help = 'A comma-separated list of services that will be stopped/started for the dump (in the provided order).')
|
||||||
|
args.add_argument('-l',
|
||||||
|
'--log',
|
||||||
|
default = system['logfile'],
|
||||||
|
dest = 'logfile',
|
||||||
|
help = 'The path to the logfile.')
|
||||||
|
args.add_argument('-a',
|
||||||
|
'--days',
|
||||||
|
default = system['days'],
|
||||||
|
dest = 'days',
|
||||||
|
type = int,
|
||||||
|
help = 'How many days to keep rotation for.')
|
||||||
|
args.add_argument('-d',
|
||||||
|
'--dumpkeys',
|
||||||
|
default = system['dumpkeys'],
|
||||||
|
dest = 'dumpkeys',
|
||||||
|
type = int,
|
||||||
|
help = 'How many keys to put in each dump.')
|
||||||
|
args.add_argument('-b',
|
||||||
|
'--basedir',
|
||||||
|
default = paths['basedir'],
|
||||||
|
dest = 'basedir',
|
||||||
|
help = 'The directory which holds your SKS DB.')
|
||||||
|
args.add_argument('-e',
|
||||||
|
'--destdir',
|
||||||
|
default = paths['destdir'],
|
||||||
|
dest = 'destdir',
|
||||||
|
help = 'The directory where the dumps should be saved (a sub-directory with the date will be created).')
|
||||||
|
args.add_argument('-r',
|
||||||
|
'--rsync',
|
||||||
|
default = paths['rsync'],
|
||||||
|
dest = 'rsync',
|
||||||
|
help = 'The remote (user@host:/path/) or local (/path/) path to use to sync the dumps to.')
|
||||||
|
args.add_argument('-D',
|
||||||
|
'--no-dump',
|
||||||
|
dest = 'nodump',
|
||||||
|
action = 'store_true',
|
||||||
|
default = ('nodump' in runtime),
|
||||||
|
help = 'Don\'t dump the SKS DB (default is to dump)')
|
||||||
|
args.add_argument('-C',
|
||||||
|
'--no-compress',
|
||||||
|
dest = 'nocompress',
|
||||||
|
action = 'store_true',
|
||||||
|
default = ('nocompress' in runtime),
|
||||||
|
help = 'Don\'t compress the DB dumps (default is to compress)')
|
||||||
|
args.add_argument('-S',
|
||||||
|
'--no-sync',
|
||||||
|
dest = 'nosync',
|
||||||
|
action = 'store_true',
|
||||||
|
default = ('nosync' in runtime),
|
||||||
|
help = 'Don\'t sync the dumps to the remote server.')
|
||||||
|
varargs = vars(args.parse_args())
|
||||||
|
return(varargs)
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
if getpass.getuser() not in ('root', sks['user']):
|
args = parseArgs()
|
||||||
exit('ERROR: You must be root or {0}!'.format(sks['user']))
|
if getpass.getuser() not in ('root', args['user']):
|
||||||
dumpDB()
|
exit('ERROR: You must be root or {0}!'.format(args['user']))
|
||||||
compressDB()
|
with open(args['logfile'], 'a') as f:
|
||||||
syncDB()
|
f.write('===== {0} STARTING =====\n'.format(str(datetime.datetime.utcnow())))
|
||||||
|
if not args['nodump']:
|
||||||
|
dumpDB(args)
|
||||||
|
if not args['nocompress']:
|
||||||
|
compressDB(args)
|
||||||
|
if not args['nosync']:
|
||||||
|
syncDB(args)
|
||||||
|
with open(args['logfile'], 'a') as f:
|
||||||
|
f.write('===== {0} DONE =====\n'.format(str(datetime.datetime.utcnow())))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
|||||||
Reference in New Issue
Block a user