#!/usr/bin/python
#
# dane is a tool to generate and verify HASTLS and TLSA records
# By Paul Wouters <paul@xelerance.com> 
# Copyright 2011 by Xelerance http://www.xelerance.com/
# License: GNU GENERAL PUBLIC LICENSE Version 2 or later
#
# https://datatracker.ietf.org/wg/dane/charter/
# https://datatracker.ietf.org/doc/draft-ietf-dane-protocol/

import sys
import getopt
import binascii
import ssl, socket
import hashlib

try:
	import argparse
except ImportError:
	print "dane requires the python-argparse"
	print "Fedora/CentOS: yum install python-argparse"
	print "Debian/Ubuntu: apt-get install python-argparse"
	sys.exit()

# We want ldns for AD flag support - maybe we will add libunbound later to not need to depend on
# DNSSEC capable resolver in /etc/resolv.conf
try:
	import ldns
except ImportError:
	print "dane requires the ldns-python sub-package from http://www.nlnetlabs.nl/projects/ldns/"
	print "Fedora/CentOS: yum install ldns-python"
	print "Debian/Ubuntu: apt-get install python-ldns"
	sys.exit()


global fmt
global secure
global transport
secure = True
transport = "both"
global quiet
quiet = False

def create_txt(hostname, pubkey):
	""" Kaminsky / Gilmore type TLS pubkey in TXT RRtype """

def create_hastls(hostname, fallback_default, services):
	""" Creates a HASTLS RRtype """

def ldns_lookup(hostname):
	global secure
	global quiet
	# do ldns work, complain if no AD bit
	resolver = ldns.ldns_resolver.new_frm_file("/etc/resolv.conf")
	resolver.set_dnssec(True)
	pkt = resolver.query(hostname, ldns.LDNS_RR_TYPE_ANY, ldns.LDNS_RR_CLASS_IN)
	if pkt.get_rcode() is ldns.LDNS_RCODE_SERVFAIL:
		print "%s lookup failed (server error or dnssec validation failed)"%name
		print "use --insecure to bypass dnssec validation - NOT RECOMMENDED!!"
		sys.exit(1)
	if pkt.get_rcode() is ldns.LDNS_RCODE_NXDOMAIN:
		if pkt.ad():
			confidence = "(non-existence proven by DNSSEC)"
		else:
			confidence = ""
		sys.exit("%s lookup failed %s"%(hostname,confidence))
	if pkt.get_rcode() is ldns.LDNS_RCODE_NOERROR:
		if not pkt.ad():
			if secure:
				sys.exit("Aborted: dnssec required but DNS lookup was insecure (use --insecure to override)\n")
			else:
				if quiet:
					print("Warning: dnssec requested but not available")
	else:
		sys.exit("unknown ldns error, aborted")

	return pkt

def create_tlsa(hostname, certtype, reftype):
	global fmt
	global secure
	global transport

	if not (fmt in ( "rfc","draft", "both")):
		fmt = "draft"

	"""Creates a TLSA RRtype"""
	""" get all A/AAAA records for hostname """
	pkt = ldns_lookup(hostname)
	# we now have a valid answer, CNAME's got expanded, so we have one or more A/AAAA records
	ipv4 = pkt.rr_list_by_type(ldns.LDNS_RR_TYPE_A, ldns.LDNS_SECTION_ANSWER)
	ipv6 = pkt.rr_list_by_type(ldns.LDNS_RR_TYPE_AAAA, ldns.LDNS_SECTION_ANSWER)
	drafts = []
	rfcs = []

	if ipv4 and transport is not "ipv6":
		for arecord in ipv4.rrs():
			try:
				a = str(arecord).split()[-1].strip()
				v4TLSA = draft_genTLSA(hostname, a, certtype, reftype)
				if v4TLSA:
					rfcv4TLSA = rfc_genTLSA(hostname, a, certtype, reftype)
					draft =  v4TLSA.strip()
					rfc  =  rfcv4TLSA.strip()
					if draft and not (draft in drafts):
						drafts.append(draft)
						rfcs.append(rfc)
			except:
				pass
	if ipv6 and transport is not "ipv4":
		for aaaarecord in ipv6.rrs():
			try:
				aaaa = str(aaaarecord).split()[-1].strip()
				v6TLSA = draft_genTLSA(hostname, aaaa, certtype, reftype)
				if v6TLSA:
					rfcv6TLSA = rfc_genTLSA(hostname, aaaa, certtype, reftype)
					draft =  v6TLSA.strip()
					rfc  =  rfcv6TLSA.strip()
					if draft and not (draft in drafts):
						drafts.append(draft)
						rfcs.append(rfc)
			except:
				pass

	if rfcs:
		if not fmt == "rfc":
			print "%s"%("\n".join(drafts))
	if drafts:
		if not fmt == "draft":
			print "%s"%("\n".join(rfcs))

# return the record
def draft_genTLSA(hostname, address, certtype, reftype):
	global quiet

	# We don't use ssl.get_server_certificate because it does not support IPv6, and it converts DER to PEM, which
	# we would just have to convert back to DER using ssl.PEM_cert_to_DER_cert()
	try:
		conn = socket.socket()
		conn.connect((address, 443))
	except socket.error, msg:
		conn  = None
		if not quiet:
			print "%s (%s): %s"%(hostname, address,msg)
		return
	try:
		sock = ssl.wrap_socket(conn)
	except ssl.SSLError, msg:
		if not quiet:
			print "%s (%s): %s"%(hostname, address,msg)
		return
	dercert = sock.getpeercert(True)
	sock.close()
	conn.close()

	if certtype != 1:
		print "Only EE-cert supported right now"
	# octet length is half of the string length
	if reftype == 0:
		certhex = binascii.b2a_hex(dercert).upper()
		return "_443._tcp.%s IN TYPE65468 \# %s 0%s0%s%s"%(hostname,len(certhex)/2+2,certtype,reftype, certhex)
	if reftype == 1:
		# sha256
		data_length = 32
	if reftype == 2:
		# sha512
		data_length = 64
	# certtype and reftype are part of the length
	data_length += 2  
	return "_443._tcp.%s IN TYPE65468 \# %s 0%s0%s%s"%(hostname,data_length,certtype,reftype, hashCert(reftype,dercert))

def rfc_genTLSA(hostname, address, certtype, reftype):
	try:
		conn = socket.socket()
		conn.connect((address, 443))
	except socket.error, msg:
		conn = None
		print "%s (%s): %s"%(hostname, address,msg)
		return
	sock = ssl.wrap_socket(conn)
	dercert = sock.getpeercert(True)
	sock.close()
	conn.close()
	if certtype != 1:
		print "Only EE-cert supported right now"
	return "_443._tcp.%s IN TLSA %s %s %s"%(hostname, certtype, reftype, hashCert(reftype,dercert))

# take PEM encoded EE-cert and DER encode it, then sha256 it
def hashCert(reftype,certblob):
	if reftype == 0:
		return binascii.b2a_hex(certblob).upper()
	elif reftype == 1:
		hashobj = hashlib.sha256()
		hashobj.update(certblob)
	elif reftype == 2:
		hashobj = hashlib.sha512()
		hashobj.update(certblob)
	else:
		return 0
	return hashobj.hexdigest().upper()


def checkExistingTLSA(address):
	""" check if a TLSA record already exists, to compare and notify if update is needed """

def checkExistingHASTLS(address):
	""" check if a HASTLS record already exists, to compare and notify if update is needed """

def main(argv=None):
	global fmt
	global secure
	global transport
	global quiet

	if argv is None:
		argv = sys.argv
		
	# create the parser
	parser = argparse.ArgumentParser(description='Create TLS related DNS records for hosts or an entire zone. version 1.2.1')

	# AXFR
	parser.add_argument('-n', '--nameserver', metavar="nameserver", action='append', help='nameserver to query')
	parser.add_argument('--axfr', action='store_true', help='use AXFR (all A/AAAA records will be scanned)')
	# IETF status related, currently --draft is the default
	parser.add_argument('--draft', action='store_true',help='output in draft private rrtype (65468/65469) format (default)')
	parser.add_argument('--rfc', action='store_true',help='output in rfc (TLSA/HASTLS) rrtype format')
	
	# HASTLS related
	# parser.add_argument('--hastls', action='store_true',help='generate HASTLS record')
	# parser.add_argument('--service', nargs='+', metavar="<service|port>", action='append',
	# 	help='add service without fallback to insecure')
	# parser.add_argument('--fallback', nargs='+', metavar="<service|port>", action='append',
	#	help='add service with fallback to insecure')

	# TLSA related	
	parser.add_argument('--tlsa', action='store_true',help='generate TLSA record (default:yes)')
	parser.add_argument('--eecert', action='store_true',help='use EEcert for TLSA record (default)')
	parser.add_argument('--cacert', action='store_true',help='use CAcert for TLSA record')
	parser.add_argument('--pubkey', action='store_true',help='use pubkey for TLSA record (not supported yet)')
	parser.add_argument('--txt', action='store_true',help='generate Kaminsky style TXT record (not supported yet)')
	parser.add_argument('--sha256', action='store_true',help='use SHA256 for the TLSA cert type')
	parser.add_argument('--sha512', action='store_true',help='use SHA512 for the TLSA cert type')
	parser.add_argument('--full', action='store_true',help='use full certificate for the TLSA cert type')

	# allow non-dnssec answers
	parser.add_argument('--insecure', action='store_true',help='allow use of non-dnssec answers to find SSL hosts')

	# limit networking to ipv4 or ipv6 only
	parser.add_argument('-4', dest='ipv4', action='store_true',help='use ipv4 networking only')
	parser.add_argument('-6', dest='ipv6', action='store_true',help='use ipv6 networking only')

	parser.add_argument('-q', '--quiet', action='store_true',help='suppress warnings and errors')
	parser.add_argument('-v', '--version', action='store_true',help='show version and exit')

	# finally, the host list
	parser.add_argument('hosts', metavar="hostname", nargs='+')

	args = parser.parse_args(argv[1:])

	if args.version:
		sys.exit("dane: version 1.2.1")
	if not args.rfc:
		args.draft = True

	if args.cacert:
		sys.exit("TLSA CAcert type record not yet supported")
	if args.pubkey:
		sys.exit("TLSA Pubkey type record not yet supported")
	if args.sha512:
		reftype=2
	elif args.full:
		reftype=0
	else:
		reftype=1

	if args.quiet:
		quiet = True

	if args.tlsa:
		args.eecert = True

	if args.insecure:
		secure = False

	if args.ipv4 and not args.ipv6:
		transport = "ipv4"
	if args.ipv6 and not args.ipv4:
		transport = "ipv6"

	# filter the non-options arguments for an "@argument" and convert it for the axfr option.
	filterHost= []
	if not args.nameserver:
		args.nameserver = []
	for host in args.hosts:
		if host[0] == "@":
			args.nameserver.append(host[1:])
			args.hosts.remove(host)
			args.axfr=True
	

	if args.rfc and args.draft:
		fmt = "both"
	elif args.rfc:
		fmt = "rfc"
	else:
		fmt = "draft"

	if not args.hosts:
		main("--help")

	if not args.axfr:
		for host in args.hosts:
			if host[-1] != ".":
				host += "."
			if not args.axfr:
				create_tlsa(host,1,reftype)
	if args.axfr:
		# Try and AXFR it
		# BUG: only works when nameserver is a name, not an ip :(
		dname = ldns.ldns_dname(args.nameserver[0])
		resolver = ldns.ldns_resolver.new_frm_file("/etc/resolv.conf")
		resolver.set_dnssec(True)
		addr = resolver.get_addr_by_name(dname, ldns.LDNS_RR_CLASS_IN, ldns.LDNS_RD);
		if (not addr):
			sys.exit("nameserver '%s' failed to resolve"%args.nameserver[0])
		#remove all nameservers
		while resolver.pop_nameserver(): 
			pass
		#insert server addr
		for rr in addr.rrs():
			resolver.push_nameserver_rr(rr)

		for host in args.hosts:
			dname = ldns.ldns_dname(host)
			status = resolver.axfr_start(dname, ldns.LDNS_RR_CLASS_IN)
			if status != ldns.LDNS_STATUS_OK:
				raise Exception("Can't start AXFR. Error: %s" % ldns.ldns_get_errorstr_by_id(status))
			#Print results
			ipv4 = []
			ipv6 = []
			while True:
				rr = resolver.axfr_next()
				if not rr:
					break

				rdf = rr.owner()
				if rr.get_type_str() == "A":
					if not (rr.get_type_str() in ipv4):
						ipv4.append(str(rdf))
				if rr.get_type_str() == "AAAA":
					if not (rr.get_type_str() in ipv6):
						ipv6.append(str(rdf))

			if transport != "ipv6":
				for host in ipv4:
					create_tlsa(host,1,reftype)
			if transport != "ipv4":
				for host in ipv6:
					create_tlsa(host,1,reftype)
	
if __name__ == "__main__":
	sys.exit(main(sys.argv))

