#!/usr/bin/python

# openpgpkey: create OPENPGPKEY DNS record from a key in your keychain.

# Copyright 2012 - 2014 Paul Wouters <paul@cypherpunks.ca>
#
# License: GNU GENERAL PUBLIC LICENSE Version 2 or later

VERSION="2.6"
OPENPGPKEY=61

import sys
import os
import gnupg
import unbound
from hashlib import sha224
import tempfile
import shutil

global ctx

def asctohex(s):
    empty = '' # I use this construct because I find ''.join() too dense
    return empty.join(['%02x' % ord(c) for c in s]) # the %02 pads when needed

def getOPENPGPKEY(email,insecure_ok):
	"""This function queries for an OPENPGPKEY DNS Resource Record and compares it with the local gnupg keyring"""

	global ctx

	try:
		username, domainname = email.split("@")
	except:
		sys.exit("Invalid email syntax")

	keyname = "%s._openpgpkey.%s"%(sha224(username).hexdigest() ,domainname)

	status, result = ctx.resolve(keyname, OPENPGPKEY)
	if status == 0 and result.havedata:
		if not result.secure:
			if not insecure_ok:
				# The data is insecure and a secure lookup was requested
				sys.exit("Error: query data is not secured by DNSSEC - use --insecure to override")
			else:
				print >> sys.stderr, 'Warning: query data was not secured by DNSSEC.'
		# If we are here the data was either secure or insecure data is accepted
		return result.data.raw
	else:
		sys.exit('Unsuccesful lookup or no data returned for OPENPGPKEY (rrtype 61)')

if __name__ == '__main__':
	import argparse
	# create the parser
	parser = argparse.ArgumentParser(description='Create and verify OPENPGPKEY records.', epilog='For bugs. see paul@nohats.ca')

	parser.add_argument('--verify','-v', action='store_true', help='Verify an OPENPGPKEY record, exit 0 when all records are matched, exit 1 on error.')
	parser.add_argument('--fetch','-f', action='store_true', help='Fetch an OPENPGPKEY record, and show it in ascii armor on stdout')

	parser.add_argument('--create','-c', action='store_true', help='Create an OPENPGKEY record')
	parser.add_argument('--version', action='version', version='openpgpkey version: %s'%VERSION, help='show version and exit')

	parser.add_argument('--insecure', action='store_true', default=False, help='Allow use of non-dnssec secured answers')
	parser.add_argument('--resolvconf', action='store', default='', help='Use a recursive resolver listed in a resolv.conf file (default: /etc/resolv.conf)')
	parser.add_argument('--rootanchor', action='store', default='/var/lib/unbound/root.anchor', help='Location of the unbound compatible DNSSEC root.anchor (default: /var/lib/unbound/root.anchor)')
	parser.add_argument('email', metavar="email")

	parser.add_argument('--uid', action='store', default='', help='override the uid (email address) within the key')

	parser.add_argument('--debug', '-d', action='store_true', help='Print details plus the result of the validation')
	parser.add_argument('--quiet', '-q', action='store_true', help='Ignored for backwards compatibility')

	# default for now to generic - when more tools support OPENPGPKEY, switch the default to rfc
	parser.add_argument('--output', '-o', action='store', default='generic', choices=['generic','rfc','both'], help='The type of output. Generic (RFC 3597, TYPE61), RFC (OPENPGPKEY) or both (default: %(default)s).')

	args = parser.parse_args()

	if args.verify and args.create:
		sys.exit("openpgpkey: must specify --create or --verify")

	if args.verify or args.fetch:
		# unbound setup, only for verify
		global ctx
		ctx = unbound.ub_ctx()
		resolvconf = "/etc/resolv.conf"
		rootanchor = "/var/lib/unbound/root.anchor"
		dlvkey = "/etc/unbound/dlv.isc.org.key"

		if args.resolvconf:
			if os.path.isfile(args.resolvconf):
				resolvconf = args.resolvconf
			else:
				print >> sys.stdout, 'openpgpkey: %s is not a file. Unable to use it as resolv.conf' % args.resolvconf
				sys.exit(1)
		ctx.resolvconf(resolvconf)

		if os.path.isfile(dlvkey):
			ctx.set_option("dlv-anchor-file:", dlvkey)

		if args.rootanchor:
			if os.path.isfile(args.rootanchor):
				resolvconf = args.rootanchor
			else:
				print >> sys.stdout, 'openpgpkey: %s is not a file. Unable to use it as rootanchor' % args.rootanchor
				sys.exit(1)
		else:
			if not os.path.isfile(rootanchor):
				rootanchor = None
		if rootanchor:
			ctx.add_ta_file(rootanchor)
		else:
			# fallback to a root key if we can find it
			if os.path.isfile("/etc/unbound/root.key"):
				rootkey = "/etc/unbound/root.key"
			elif os.path.isfile("/var/lib/unbound/root.key"):
				rootkey = "/var/lib/unbound/root.key"
			else:
				rootkey = None
			if rootkey:
				unbound.ub_ctx_trustedkeys(ctx,rootkey)
		# unbound setup completed

		openpgpkeys = getOPENPGPKEY(args.email, args.insecure)
		if len(openpgpkeys) == 0:
			print >> sys.stderr, 'openpgpkey: Received nothing?'
			sys.exit(1)
		fdir = tempfile.mkdtemp(".gpg","openpgpkey-","/tmp/")
		gpgnet = gnupg.GPG(gnupghome=fdir)
		gpgnet.decode_errors = 'ignore'

		for openpgpkey in openpgpkeys:
			import_result = gpgnet.import_keys(openpgpkey)
			if args.fetch:
				if args.uid:
					pubkey = gpgnet.export_keys(args.uid, minimal=True)
					if not pubkey:
						print >> sys.stderr, 'openpgpkey: Requested uid not present in received OpenPGP data'
						for id in  gpgnet.list_keys()[0]['uids']:
							print >> sys.stderr, "# %s"%id
						sys.exit(1)
				else:
					pubkey = gpgnet.export_keys(args.email, minimal=True)
				if not pubkey:
					print >> sys.stderr, 'openpgpkey: Received OpenPGP data does not contain a key with keyid %s'%args.email
					print >> sys.stderr, '(add --uid <uid> to override with any of the below received uids)'
					for id in  gpgnet.list_keys()[0]['uids']:
						print >> sys.stderr, "# %s"%id
					sys.exit(1)

				pubkey = pubkey.replace("Version:","Comment: %s key obtained from DNS\nVersion:"%args.email)
				if args.insecure:
					pubkey = pubkey.replace("Version:","Comment: NOT VALIDATED BY DNSSEC\nVersion:")
				else:
					pubkey = pubkey.replace("Version:","Comment: key transfer was protected by DNSSEC\nVersion:")
				print pubkey

		if args.fetch:
			sys.exit(0)

		received_keys = gpgnet.list_keys() 
		gpgdisk = gnupg.GPG()
		gpgdisk.decode_errors = 'ignore'
		disk_keys = gpgdisk.list_keys()
		for pkey in received_keys:
			if args.debug:
				print >> sys.stderr, "Received from DNS: Key-ID:%s Fingerprint:%s"%(pkey["keyid"], pkey["fingerprint"])
			found = False
			for dkey in disk_keys:
				if args.debug:
					print >> sys.stderr, "Local disk: Key-ID:%s Fingerprint:%s"%(dkey["keyid"], dkey["fingerprint"])
				if pkey["keyid"] == dkey["keyid"] and pkey["fingerprint"] == dkey["fingerprint"]:
					found = True
			if found == False:
				shutil.rmtree(fdir)
				sys.exit("Received key with keyid %s was not found"%pkey["keyid"])
			else:
				if args.debug:
					print >> sys.stderr, "Received key with keyid %s was found"%pkey["keyid"]
		print "All OPENPGPKEY records matched with content from the local keyring"
		shutil.rmtree(fdir)
		sys.exit(0)

	else: # we want to create
		gpgdisk = gnupg.GPG()
		gpgdisk.decode_errors = 'ignore'
		disk_keys = gpgdisk.list_keys()
		found = False
		for pgpkey in disk_keys:
			for uid in pgpkey["uids"]:
				if "<%s>"%args.email in uid:
					found = True
					if args.debug:
						print >> sys.stderr,  "Found matching KeyID: %s (%s) for %s"%(pgpkey["keyid"], pgpkey["fingerprint"], uid)
					ekey = gpgdisk.export_keys(pgpkey["keyid"],minimal=True, secret=False, armor=False)
					ekey64 = "".join(gpgdisk.export_keys(pgpkey["keyid"],minimal=True, secret=False, armor=True).split("\n")[2:-3])
					user,domain = args.email.split("@")
					euser = sha224(user).hexdigest()

					if args.output == "rfc":
						print "%s._openpgpkey.%s. IN OPENPGPKEY %s"%(euser,domain,ekey64)
					if args.output ==  "generic":
						if args.debug:
							print "Length for generic record is %s"%len(ekey)
						print "%s._openpgpkey.%s. IN TYPE61 \# %s %s"%(euser,domain,len(ekey),asctohex(ekey))
		if not found:
			sys.exit("No key found containing the email address '%s' in the keyid(s)"%args.email)
		


