#!/usr/bin/python
#
# openpgpkey: create OPENPGPKEY DNS record from a key in your keychain.
#
# Copyright 2012 - 2015 Paul Wouters <paul@cypherpunks.ca>
# Copyright 2015 Dirk Stoecker <github@dstoecker.de>
# Copyright 2015 Jan Vcelak <jv@fcelda.cz>
# Copyright 2015 Carsten Strotmann <cs@sys4.de>
# Copyright 2015 Ondrej Sury <ondrej@sury.org>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

VERSION="2.7"
OPENPGPKEY=61

import sys
import os
import gnupg
import unbound
import hashlib
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 createOPENPGPKEY(email, gpgdisk, keyid, output, debug):
	ekey64 = "".join(gpgdisk.export_keys(keyid,minimal=True, secret=False, armor=True).split("\n")[2:-3])
	user,domain = email.lower().split("@")
	euser = sha256trunc(user)

	if output == "rfc" or output == "both":
		print "; keyid: %s" % keyid
		print "%s._openpgpkey.%s. IN OPENPGPKEY %s" % (euser, domain, ekey64)
	if output ==  "generic" or output == "both":
		if debug:
			print "Length for generic record is %s" % len(ekey)
		print "; keyid: %s" % keyid
		print "%s._openpgpkey.%s. IN TYPE61 \# %s %s" % (euser, domain, len(ekey), asctohex(ekey))

def sha256trunc(data):
	"""Compute SHA2-256 hash truncated to 28 octets."""
	return hashlib.sha256(data).hexdigest()[:56]

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.lower().split("@")
	except:
		sys.exit("Invalid email syntax")

	keyname = "%s._openpgpkey.%s"%(sha256trunc(username), 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='', 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('--keyid', action='store', default='', help='specify key by keyid')

	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='rfc', 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 = None
		if args.rootanchor:
			if os.path.isfile(args.rootanchor):
				rootanchor = args.rootanchor
			else:
				print >> sys.stdout, 'openpgpkey: %s is not a file. Unable to use it as rootanchor' % args.rootanchor
				sys.exit(1)
		else:
			cauldron = ( "/var/lib/unbound/root.anchor", "/var/lib/unbound/root.key", "/etc/unbound/root.key" )
			for root in cauldron:
				if os.path.isfile(root):
					rootanchor=root
					break
		if rootanchor:
			try:
				ctx.add_ta_file(rootanchor)
			except:
				unbound.ub_ctx_trustedkeys(ctx,rootanchor)

		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)

		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.keyid:
					pubkey = gpgnet.export_keys(args.keyid, minimal=True)
					if not pubkey:
						print >> sys.stderr, 'openpgpkey: Requested keyid not present in received OpenPGP data'
						sys.exit(1)
				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)
				if not args.uid and not args.keyid:
					pubkey = gpgnet.export_keys(args.email, minimal=True)
					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'
		found = False
		# if we have the keyid, use that
		if args.keyid:
			ekey = gpgdisk.export_keys(args.keyid,minimal=True, secret=False, armor=False)
			if ekey:
				found = True
				createOPENPGPKEY(args.email, gpgdisk, args.keyid, args.output, args.debug)
		# else find key
		if not found:
			disk_keys = gpgdisk.list_keys()
			for pgpkey in disk_keys:
				for uid in pgpkey["uids"]:
					if "<%s>"%args.email in uid:
						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)
						createOPENPGPKEY(args.email, gpgdisk, pgpkey["keyid"], args.output, args.debug)
						found = True
		# give up
		if not found:
			errt = "No key found for "
			if args.email:
				errt += "email address %s "%args.email
			if args.uid:
				errt += "or uid %s "%args.uid
			if args.keyid:
				errt += "or keyid %s "%args.keyid
			sys.exit(errt)
