package com.mps.deepviolet.api;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.URL;
import java.net.UnknownHostException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.Security;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.PKIXParameters;
import java.security.cert.TrustAnchor;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Formatter;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;

import org.bouncycastle.asn1.ASN1Boolean;
import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1Integer;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.ASN1Primitive;
import org.bouncycastle.asn1.DERApplicationSpecific;
import org.bouncycastle.asn1.DERBitString;
import org.bouncycastle.asn1.DERIA5String;
import org.bouncycastle.asn1.DEROctetString;
import org.bouncycastle.asn1.DERSequence;
import org.bouncycastle.asn1.DERSet;
import org.bouncycastle.asn1.DERTaggedObject;
import org.bouncycastle.asn1.DERUTF8String;
import org.bouncycastle.asn1.DERVisibleString;
import org.bouncycastle.asn1.DLSequence;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.jayway.jsonpath.Configuration;
import com.jayway.jsonpath.JsonPath;
import com.mps.deepviolet.api.IDVSession.CIPHER_NAME_CONVENTION;
import com.mps.deepviolet.util.FileUtils;

//import sun.security.provider.certpath.OCSP;
//import sun.security.provider.certpath.OCSP.RevocationStatus;

/**
 * Utility class to handle cryptographic functions.  Significant contributions around
 * ciphersuite handling adapted from code examples by <a href="mailto:pornin@bolet.org">Thomas Pornin</a>.
 * For more information see, <a href="http://tools.ietf.org/html/rfc5246">The Transport Layer Security (TLS) Protocol Version 1.2</a>,
 * <a href="http://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml">Transport Layer Security (TLS) Parameters</a>,
 * <a href="http://www.bolet.org/TestSSLServer/">TestSSLServer</a>
 * @author Milton Smith
 */
class CipherSuiteUtil {

// Handshake protocol version legend
//	SSL v1
//	SSL v2
//	SSL v3
//	SSL v3.1 = TLS v1.0
//	SSL v3.2 = TLS v1.1
//	SSL v3.3 = TLS v1.2
	
	private static final Logger logger = LoggerFactory.getLogger("com.mps.deepviolet.suite.CipherSuiteUtil");
	
	// Common OIDs to Extension Mappings
	private static final HashMap<String,String> OIDMAP = new HashMap<String,String>();
	
	private static final SSLSocketFactory dsc = HttpsURLConnection.getDefaultSSLSocketFactory();
	
	private static final HostnameVerifier dhv = HttpsURLConnection.getDefaultHostnameVerifier();
	
	static final int MAX_RECORD_LEN = 16384;
	static final int CHANGE_CIPHER_SPEC = 20;
	static final int ALERT              = 21;
	static final int HANDSHAKE          = 22;
	static final int APPLICATION        = 23;
	private static final SecureRandom RNG = new SecureRandom();

	private static final byte[] SSL2_CLIENT_HELLO = {
		(byte)0x80, (byte)0x2E,  // header (record length)
		(byte)0x01,              // message type (CLIENT HELLO)
		(byte)0x00, (byte)0x02,  // version (0x0002)
		(byte)0x00, (byte)0x15,  // cipher specs list length
		(byte)0x00, (byte)0x00,  // session ID length
		(byte)0x00, (byte)0x10,  // challenge length
		0x01, 0x00, (byte)0x80,  // SSL_CK_RC4_128_WITH_MD5
		0x02, 0x00, (byte)0x80,  // SSL_CK_RC4_128_EXPORT40_WITH_MD5
		0x03, 0x00, (byte)0x80,  // SSL_CK_RC2_128_CBC_WITH_MD5
		0x04, 0x00, (byte)0x80,  // SSL_CK_RC2_128_CBC_EXPORT40_WITH_MD5
		0x05, 0x00, (byte)0x80,  // SSL_CK_IDEA_128_CBC_WITH_MD5
		0x06, 0x00, (byte)0x40,  // SSL_CK_DES_64_CBC_WITH_MD5
		0x07, 0x00, (byte)0xC0,  // SSL_CK_DES_192_EDE3_CBC_WITH_MD5
		0x54, 0x54, 0x54, 0x54,  // challenge data (16 bytes)
		0x54, 0x54, 0x54, 0x54,
		0x54, 0x54, 0x54, 0x54,
		0x54, 0x54, 0x54, 0x54
	};
	
	private static final byte[] HEARTBEAT = {
			(byte)0x018,             // message type (HEARTBEAT)
			(byte)0x03, (byte)0x01,  // tls version
			(byte)0x00, (byte)0x03,  // length
			(byte)0x01,              // type (request)
			(byte)0x40, (byte)0x00   // payload length
	};
	
	static final int UNASSIGNED = -1; // no evaluation in json mapping file
	static final int CLEAR  = 0; // no encryption
	static final int WEAK   = 1; // weak encryption: 40-bit key
	static final int MEDIUM = 2; // medium encryption: 56-bit key
	static final int STRONG = 3; // strong encryption
	public static final String NO_CIPHERS = "No Ciphers";	
	static Map<Integer, CipherSuite> CIPHER_SUITES =
			new TreeMap<Integer, CipherSuite>();
	
	
	static boolean bCiphersInitialized = false;
	static {

		// Generate cipher map dynamically based upon Mozilla json data.
		//initCipherMap();
        
	}
	
	

	static {
		
//        Security.addProvider(new com.sun.net.ssl.internal.ssl.Provider());
		
		Security.addProvider( new BouncyCastleProvider() );
        
		OIDMAP.put( "2.5.29.14","SubjectKeyIdentifier");
		OIDMAP.put( "2.5.29.15","KeyUsage");
		OIDMAP.put( "2.5.29.16","PrivateKeyUsage");
		OIDMAP.put( "2.5.29.17","SubjectAlternativeName");
		OIDMAP.put( "2.5.29.18","IssuerAlternativeName");
		OIDMAP.put( "2.5.29.19","BasicConstraints");
		OIDMAP.put( "2.5.29.30","NameConstraints");
		OIDMAP.put( "2.5.29.33","PolicyMappings");
		OIDMAP.put( "2.5.29.35","AuthorityKeyIdentifier");
		OIDMAP.put( "2.5.29.36","PolicyConstraints");
		
		OIDMAP.put( "1.3.6.1.5.5.7.48.1","ocsp");
		OIDMAP.put( "1.3.6.1.5.5.7.48.2","caIssuers");
		OIDMAP.put( "1.2.840.113549.1.1.1","SubjectPublicKeyInfo");
		OIDMAP.put( "1.3.6.1.5.5.7.48.1.1","BasicOCSPResponse");
		OIDMAP.put( "1.2.840.113549.1.1.5","SignatureAlgorithm");
		OIDMAP.put( "1.3.6.1.5.5.7.1.1","AuthorityInfoAccess");
		OIDMAP.put("1.3.6.1.4.1.11129.2.4.2", "SignedCertificateTimestampList");
		OIDMAP.put("1.3.6.1.5.5.7.2.2", "CPSUserNotice");
		OIDMAP.put( "2.5.29.31","CRLDistributionPoints");
		OIDMAP.put( "2.5.29.32","CertificatePolicies");
		OIDMAP.put( "1.3.6.1.4.1.6449.1.2.1.5.1","CertificatePolicyId");
		OIDMAP.put( "2.5.29.32","CertificatePolicies");
		OIDMAP.put( "1.3.6.1.5.5.7.2.1","qualifierID");
		OIDMAP.put( "2.5.29.37","ExtendedKeyUsages");
		OIDMAP.put( "2.5.29.15","KeyUsage");
		OIDMAP.put( "2.5.29.14","SubjectKeyIdentifier");
		
	}
		
	// TODO This needs to go bye bye, abad idea.  I'm thinking a better way to do this is eventually
	// get to a MVC type architecture.  This would better address the ways deepviolet can be used
	static ServerMetadata getServerMetadataInstance( URL url, IDVSession.CIPHER_NAME_CONVENTION cipher_name_convention, MutableDVSession session, DVBackgroundTask dvtask ) throws Exception {
		
		HostData hostdata = new HostData(url);;
		Boolean compress = false;
		
		// Generate cipher map dynamically based upon Mozilla json data.
		dvtask.setStatusBarMessage("Initializing DV ciphersuite maps.");
		if( !bCiphersInitialized) initCipherMap();
		
		dvtask.setStatusBarMessage("Reviewing server protocols.");
		
		String name = url.getHost();
		int port = ( url.getPort() > 0 ) ? url.getPort() : 443;
		
		InetSocketAddress isa = new InetSocketAddress(name, port);

		Set<Integer> sv = new TreeSet<Integer>();
		for (int v = 0x0300; v <= 0x0303; v ++) {
			CipherSuiteUtilServerHello sh = connect(isa,
				v, CIPHER_SUITES.keySet());
			if (sh == null) {
				continue;
			}
			sv.add(sh.protoVersion);
			dvtask.setStatusBarMessage("Analysing TLS version "+sh.protoVersion);
			if (sh.compression == 1) {
				compress = true;
				session.setSessionPropertyValue(IDVSession.SESSION_PROPERTIES.DEFLATE_COMPRESSION, "true");
				logger.warn("Server vulnerable to CRIME attack.  Compression enabled.");
			}else{
				session.setSessionPropertyValue(IDVSession.SESSION_PROPERTIES.DEFLATE_COMPRESSION, "false");
			}
		}
		
		dvtask.setStatusBarMessage("Sending TLS server Hello SSLv2");
		ServerHelloSSLv2 sh2 = connectV2(isa);

		if (sh2 != null) {
			sv.add(0x0200);
		}

		if (sv.size() == 0) {
			dvtask.setStatusBarMessage("Server may not be SSL/TLS enabled. host=" + isa);
			logger.error("Server may not be SSL/TLS enabled. host=" + isa);
			return null;
		}

		dvtask.setStatusBarMessage("Normalizing ciphersuite names to "+cipher_name_convention+" specification");
		
		Set<Integer> lastSuppCS = null;
		Map<Integer, Set<Integer>> suppCS = new TreeMap<Integer, Set<Integer>>();
		Set<String> certID = new TreeSet<String>();
		boolean vulnFREAK = false;
		
		if (sh2 != null) {

			ArrayList<String> listv2 = new ArrayList<String>();
			String[] tmp = new String[0];
			
			Set<Integer> vc2 = new TreeSet<Integer>();
			for (int c : sh2.cipherSuites) {
				vc2.add(c);
			}
			for (int c : vc2) {
				
				String suitename = cipherSuiteStringV2(c, cipher_name_convention);
				
				if( !vulnFREAK ) {
					if( suitename == null || suitename.length()==0 ) {
						logger.info("Freak vulnerability analysis skipped due to null ciphersuite name. id=0x" + c );
					} else {
						vulnFREAK = cipherSuiteStringV2(c, cipher_name_convention).indexOf("RSA_EXPORT") > -1; 
						if (vulnFREAK ) {
							logger.warn("Following ciphersuite vulnerable to FREAK attack, "+suitename);
						}
					}
				}
				listv2.add( suitename+"(0x"+Integer.toHexString(c)+")" );

			}
			
			suppCS.put(0x0200, vc2);
			if (sh2.serverCertName != null) {
				hostdata.setScalarValue("getServerMetadataInstance",sh2.serverCertHash, sh2.serverCertName);				
			}
			
			hostdata.setVectorValue( "getServerMetadataInstance",versionString(0x0200), listv2.toArray(tmp));
			
		}

		dvtask.setStatusBarMessage("Starting ciphersuite analysis.");
		
		int agMaxStrength = STRONG;
		int agMinStrength = STRONG;
		boolean vulnBEAST = false;
		//boolean vulnROBOT = false;
		
		for (int v : sv) {
			
			if (v == 0x0200) {
				continue;
			}
			Set<Integer> vsc = supportedSuites(isa, v, certID);
			suppCS.put(v, vsc);
			
			ArrayList<String> listv = new ArrayList<String>();
			String[] tmp = new String[0];
						
			for (int c : vsc) {
				
				String suitename = cipherSuiteString(c, cipher_name_convention);
				if( suitename == null || suitename.length()==0 ) {
					logger.info("Freak vulnerability analysis skipped due to null ciphersuite name. id=0x" + c );
				} else {
					if( !vulnFREAK ) {
						vulnFREAK = cipherSuiteString(c, cipher_name_convention).indexOf("RSA_EXPORT") > -1; 
						if( vulnFREAK ) {
							logger.warn("Following ciphersuite vulnerable to FREAK attack, "+suitename);
						}
					}
				}
				
				//Set<Integer> vsc = suppCS.get(c);
				agMaxStrength = Math.min(
					maxStrength(vsc), agMaxStrength);
				agMinStrength = Math.min(
					minStrength(vsc), agMinStrength);
				if (!vulnBEAST) {
					vulnBEAST = testBEAST(isa, c, vsc);
					if( vulnBEAST ) {
						logger.warn("Following ciphersuite vulnerable to BEAST attack, "+suitename);
					}
				}
//				if (!vulnROBOT) {
//					vulnROBOT = suitename.startsWith("TLS_RSA");
//					if( vulnROBOT ) {
//						logger.warn("Following ciphersuite vulnerable to ROBOT attack, "+suitename);
//					}
//				}
				
				listv.add( suitename+"(0x"+Integer.toHexString(c)+")" );
			}			

			hostdata.setVectorValue( "getServerMetadataInstance",versionString(v), listv.toArray(tmp));
			
		}
		
		
//		// Iterate over supported ciphersuites.
//		int agMaxStrength = STRONG;
//		int agMinStrength = STRONG;
//		boolean vulnBEAST = false;
//		boolean vulnROBOT = false;
//		for (int v : sv) {
//			Set<Integer> vsc = suppCS.get(v);
//			agMaxStrength = Math.min(
//				maxStrength(vsc), agMaxStrength);
//			agMinStrength = Math.min(
//				minStrength(vsc), agMinStrength);
//			if (!vulnBEAST) {
//				vulnBEAST = testBEAST(isa, v, vsc);
//			}
//			if (!vulnROBOT) {
//				vulnROBOT = cipherSuiteString(v, cipher_name_convention).startsWith("TLS_RSA");
//			}
//		}
		
		//TODO: NEEDS TO BE CHECKED AND TESTED.
		session.setVulnerabilityAssessmentValue(IDVSession.VULNERABILITY_ASSESSMENTS.MINIMAL_ENCRYPTION_STRENGTH, strengthString(agMinStrength));
		session.setVulnerabilityAssessmentValue(IDVSession.VULNERABILITY_ASSESSMENTS.ACHIEVABLE_ENCRYPTION_STRENGTH, strengthString(agMaxStrength));
		session.setVulnerabilityAssessmentValue(IDVSession.VULNERABILITY_ASSESSMENTS.BEAST_VULNERABLE, Boolean.toString(vulnBEAST)); // Notes: Check TLS version <=1.0 and CBC enabled.  https://blog.qualys.com/ssllabs/2013/09/10/is-beast-still-a-threat
		session.setVulnerabilityAssessmentValue(IDVSession.VULNERABILITY_ASSESSMENTS.CRIME_VULNERABLE, Boolean.toString(compress));  // Notes: Check TLS compression enabled, https://en.wikipedia.org/wiki/CRIME 
		session.setVulnerabilityAssessmentValue(IDVSession.VULNERABILITY_ASSESSMENTS.FREAK_VULNERABLE, Boolean.toString(vulnFREAK)); // Notes: Check ciphers containing, RSA_EXPORT https://censys.io/blog/freak
		//session.setVulnerabilityAssessmentValue(IDVSession.VULNERABILITY_ASSESSMENTS.ROBOT_VULNERABLE, Boolean.toString(vulnROBOT)); // Notes: Check ciphers starting, TLS_RSA https://robotattack.org/
	
		dvtask.setStatusBarMessage("Ciphersuite analysis complete.");
		
		return hostdata;
		
	}
	
	/*
	 * Original Mozilla JSON file
	 * server-side-tls-conf-4.0.json = https://statics.tls.security.mozilla.org/server-side-tls-conf-4.0.json
	 * ciphermap.json = https://github.com/april/tls-table/blob/master/tls-table.py
	 */
	private static void initCipherMap() {
		
		    String ciphermap = FileUtils.getJsonResourceAsString("ciphermap.json");
		    Object document = Configuration.defaultConfiguration().jsonProvider().parse(ciphermap);
		    List<?> ciphermetalist = JsonPath.read(document, "$[?(@.*)]");
		  
		    // Note (milton):  A point to consider about the Mozilla json evaluation data and a source
		    // of confusion for me is that all ciphers listed in modern are also listed in
		    // intermediate.  All ciphers in intermediate are also listed in old.  To separate
		    // the evals out properly for DV purposes we add ciphers to a map in the following
		    // order, modern, intermediate, and then old.  Further once we set the cipher
		    // evaluation we don't allow the evaluation to be reassigned to a lower level.
		    HashMap<String, Integer> strengtheval = new HashMap<String, Integer>();
		    String cipherevaluation = FileUtils.getJsonResourceAsString("server-side-tls-conf-4.0.json");
		    Object d2 = Configuration.defaultConfiguration().jsonProvider().parse(cipherevaluation);
		    List<String> mc1 = JsonPath.read(d2, "$.configurations.modern.ciphersuites.*");
		    for( String ciph: mc1) {
		    	strengtheval.put(ciph, new Integer(STRONG));
		    }
		    //TODO note this does not return just intermediate.ciphersuites.* need to fix
		    Object d3 = Configuration.defaultConfiguration().jsonProvider().parse(cipherevaluation);
		    List<String> mc2 = JsonPath.read(d3, "$.configurations.intermediate.ciphersuites.*");
		    for( String ciph: mc2) {
		    	if( !strengtheval.containsKey(ciph) ){ // don't remove, see note.
		    		strengtheval.put(ciph,new Integer(MEDIUM));
		    	}
		    }
		    //TODO note this does not return just intermediate.ciphersuites.* need to fix
		    Object d4 = Configuration.defaultConfiguration().jsonProvider().parse(cipherevaluation);
		    List<String> mc3 = JsonPath.read(d4, "$.configurations.old.ciphersuites.*");
		    for( String ciph: mc3) {
		    	if( !strengtheval.containsKey(ciph) ){ // don't remove, see note.
		    		strengtheval.put(ciph,new Integer(WEAK));
		    	}
		    }
		    
		    Iterator<?> i = ciphermetalist.iterator();
		    while( i.hasNext() ) {
		    	Map<?, ?> ci = (Map<?, ?>)i.next();
		    	//System.out.println("ci="+ci.getClass().getName());
		    	Iterator<?> keys = ci.keySet().iterator();
		    	while ( keys.hasNext() ) {
		    		Object obj = keys.next();
			    	//System.out.println("obj="+obj.getClass().getName()+" val="+obj.toString());
			    	Map<?, ?> ch1 = (Map<?, ?>)ci.get(obj.toString());
			    	//System.out.println("ch1="+ch1.getClass().getName()+" val="+ch1.toString());
			    	
			    	List<String> ciphercode = Arrays.asList(obj.toString().split(","));
		        	String ho = ciphercode.get(0);
		        	List<String> lo = Arrays.asList(ciphercode.get(1).split("x"));
		        	String sho = ho;
		        	String slo = lo.get(1).replaceFirst("^0+(?!$)", "");
		        	sho = ( sho.equals("0") ) ? "" : sho;
		        	Integer cc1 = Integer.decode(sho+slo);
		        	
		        	Iterator<?> cns = ch1.values().iterator();
		        	int istrengtheval = -1; //unknown strength
		        	while( cns.hasNext() ) {
		        		String key = (String)cns.next();
		        		if( strengtheval.containsKey(key) ) {
		        			istrengtheval = ((Integer)strengtheval.get(key)).intValue();
		        			break;
		        		}
		        	}
		        	logger.debug("Cached Mozilla ciphers, "+obj.toString()+" "+ch1.toString()+" strengtheval="+istrengtheval);
			    	makeCS(cc1.intValue(),ch1, istrengtheval);
			    	
		    	}
		    }

		
	}


	static String versionString(int version) {
		if (version == 0x0200) {
			return "SSLv2";
		} else if (version == 0x0300) {
			return "SSLv3";
		} else if ((version >>> 8) == 0x03) {
			return "TLSv1." + ((version & 0xFF) - 1);
		} else {
			return String.format("UNKNOWN_VERSION:0x%04X", version);
		}
	}
	
	/*
	 * Enumerate server cipher suites. This is accomplished by
	 * repeatedly contacting the server, each time removing from our
	 * list of supported suites returned by the server.  The cipher
	 * suites remaining at the end of this operation are unsupported
	 * by the server.
	 */
	static Set<Integer> supportedSuites(InetSocketAddress isa, int version,
		Set<String> serverCertID)
	{
	
		// Notes: the problem with using the past approach, CIPHER_SUITES.keySet(),
		// is that some servers use ciphers outside those included with the
		// Mozilla cipher mapings.  As a result DV was missing some ciphers.
		// The new approach is more comprensive but takes longer.
		//Set<Integer> cs = new TreeSet<Integer>(CIPHER_SUITES.keySet());
		
		Set<Integer> rs = new TreeSet<Integer>();
	
		int BLK_SIZE = 6000;
		int CIPHERMAPSZ = 0xFFFF;
		int i2=0; int i3=1;
		Set<Integer> scanblk = null;
		for ( int i=1; i<CIPHERMAPSZ; i+=BLK_SIZE ) {

			scanblk = new TreeSet<Integer>(); 
			while( i2< BLK_SIZE*i3 ) {
				scanblk.add(i2);
				i2++;
			}
			i3++;
			
//			// Take it easy on server, delay between a little between block checks
//			try {
//				Thread.currentThread().sleep((int)(Math.random()*750));
//			} catch (InterruptedException e) {}
			
			for (;;) {
				//TODO could make this multi-threaded to speed up scanning. 
				//  although need to be kind to servers.  Don't want too
				// many connections and create performance problems.
				CipherSuiteUtilServerHello sh = connect(isa, version, scanblk);
				if (sh == null) {
					break;
				}
				if (!scanblk.contains(sh.cipherSuite)) {
					//TODO need a better way to communicate this in the future
					String ciphersuite = Integer.toHexString(sh.cipherSuite);
					logger.warn("Server wants to use"
						+ " cipher suite "+ciphersuite+" which client"
						+ " did not announce.");
					break;
				}
				scanblk.remove(sh.cipherSuite);
				rs.add(sh.cipherSuite);
				if (sh.serverCertName != null) {
					serverCertID.add(sh.serverCertHash
						+ ": " + sh.serverCertName);
				}
			}
		
		}
		
		return rs;
	}

	
	static int minStrength(Set<Integer> supp)
	{
		int m = STRONG;
		for (int suite : supp) {
			CipherSuite cs = CIPHER_SUITES.get(suite);
			if (cs == null) {
				continue;
			}
			if (cs.strength < m) {
				m = cs.strength;
			}
		}
		return m;
	}

	static int maxStrength(Set<Integer> supp)
	{
		int m = CLEAR;
		for (int suite : supp) {
			CipherSuite cs = CIPHER_SUITES.get(suite);
			if (cs == null) {
				continue;
			}
			if (cs.strength > m) {
				m = cs.strength;
			}
		}
		return m;
	}
	
	/**
	 * Utility method to convert an <code>javax.security.cert.X509Certificate</code> to
	 * <code>java.security.cert.X509Certificate</code>.  No doubt, a bit of legacy.
	 * @param cert X509 certificate to convert.
	 * @return java.security.cert.X509Certificate Converted object
	 * @see http://exampledepot.8waytrips.com/egs/javax.security.cert/ConvertCert.html
	 */
//	public static java.security.cert.X509Certificate convert(javax.security.cert.X509Certificate cert) {
//	    try {
//	        byte[] encoded = cert.getEncoded();
//	        ByteArrayInputStream bis = new ByteArrayInputStream(encoded);
//	        java.security.cert.CertificateFactory cf
//	            = java.security.cert.CertificateFactory.getInstance("X.509");
//	        return (java.security.cert.X509Certificate)cf.generateCertificate(bis);
//	    } catch (javax.security.cert.CertificateEncodingException e) {
//			logger.error(e.getMessage(),e);
//	    } catch (java.security.cert.CertificateException e) {
//			logger.error(e.getMessage(),e);
//	    }
//	    return null;
//	}
	
	/**
	 * Analysis to determine ciphersuite strength.
	 * @param protocol Ciphersuite protocol to test.
	 * @return String indicating strength, CLEAR(no encryption), WEAK, MEDIUM, STRONG. 
	 */
	static final String getStrength(String protocol) {
		
		String clear = "CLEAR{no encryption}";
		String weak = "WEAK";
		String medium = "MEDIUM";
		String strong = "STRONG";
		String unknown = "UNKNOWN";
		
		String result = unknown;
		
		if (protocol.contains("_NULL_") ) {
			
			result = clear;
		
		} else {
			
			Collection suites = CIPHER_SUITES.values();
			CipherSuite c = null;
			Iterator i = suites.iterator();
			while( i.hasNext() ) {
				c = (CipherSuite)i.next();
				if ( c.names.containsValue(protocol) ) {
					switch ( c.strength ) {
					case STRONG:
						result = strong;
						break;
					case MEDIUM:
						result = medium;
						break;
					case WEAK:
						result = weak;
						break;
					}
				}

			}
			
		}
	    
		return result;
		
	}
	
	/**
	 * Retrieve a server certificate based upon URL.
	 * @param url Target URL
	 * @return X509Certificate Server certificate.
	 * @throws Exception Thrown on problems.
	 */
	static final X509Certificate getServerCertificate(URL url) throws Exception {
		
		X509Certificate[] certs = getServerCertificateChain(url);
		
		return certs[0];
		
	}
	
	/**
	 * Return server responses
	 * @param url Target URL
	 * @return Map HTTPS response headers
	 * @throws Exception Thrown on problems.
	 */
	static final Map<String, List<String>> getHttpResponseHeaders(URL url) throws Exception {
		
		HttpsURLConnection conn = null;
		
		Map<String, List<String>> result = new HashMap<String, List<String>>();
		
		try {
			
			enableTLSChainTesting(false);
		
	        conn = (HttpsURLConnection)url.openConnection();
	        
	        conn.connect();
	        
	        result = conn.getHeaderFields();
			
		} finally {
			
			enableTLSChainTesting(true);
		}
		
		
        return result;
        
	}
	
//	public static final synchronized ServerMetadata getHttpResponseHeaders(URL url) throws Exception {
//		
//		HostData hostdata = null;
//		
//		// return cached instance of server TLS data if available and not expired.
//		if ( hostcache.containsKey( url ) ) {
//			hostdata = hostcache.get(url);
//			if( !hostdata.isExpired() )
//				return hostdata;
//		}
//		
//		// No cached instance of TLS data so create some
//		hostdata = new HostData(url);
//		hostcache.put(url, hostdata);	
//		
//        HttpsURLConnection conn = (HttpsURLConnection)url.openConnection();
//        conn.connect();
//        Map<String, List<String>> headers = conn.getHeaderFields();
//        
//        Set<String> k = headers.keySet();
//        Iterator<String> keys = k.iterator();
//        
//        while( keys.hasNext() ) {
//        	
//        	String key = keys.next();
//        	hostdata.setVectorValue(key,(String[])headers.get(key).toArray() );
//        	
//        }
//		
//        return hostdata;
//        
//	}
	
	/**
	 * Enable default testing for TLS certificate trust chains.
	 * @param value true, chain will be tested.  false, chain will not be tested.
	 * @throws Exception Thrown on error
	 */
	static final void enableTLSChainTesting( boolean value ) throws Exception {

		if( value ) {
			
			HttpsURLConnection.setDefaultSSLSocketFactory(dsc);
			HttpsURLConnection.setDefaultHostnameVerifier(dhv);
			
		} else {
			
			SSLContext sc = SSLContext.getInstance("TLS");
			sc.init(null, new TrustManager[] { new TrustAllX509TrustManager() }, new java.security.SecureRandom());
			HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
			HttpsURLConnection.setDefaultHostnameVerifier( new HostnameVerifier(){
			    public boolean verify(String string,SSLSession ssls) {
			        return true;
			    }
			});
			
		}
		
	}
	
	/**
	 * Retrieve a certificate chain based upon URL.  Note this API will return
	 * certificates with unvalidated and possibly bad trust chains.
	 * @param url Target URL
	 * @return X509Certificate Certificate chain
	 * @throws Exception Thrown on problems.
	 * @see <a href="http://stackoverflow.com/questions/19723415/java-overriding-function-to-disable-ssl-certificate-check">java-overriding-function-to-disable-ssl-certificate-check</a>
	 */
	static final X509Certificate[] getServerCertificateChain(URL url) throws Exception {

        ArrayList<X509Certificate> list = new ArrayList<X509Certificate>();
		
		try {
			
			enableTLSChainTesting(false);
			
	        HttpsURLConnection conn = (HttpsURLConnection)url.openConnection();
	        conn.connect();
	        Certificate[] certs = conn.getServerCertificates();
	        
	        for (Certificate cert : certs) {
	        	
	            if(cert instanceof X509Certificate) {            	
	            	list.add( (X509Certificate)cert );           
	            } else {
	            	logger.info("Unsupported certificate type.  type="+cert.getClass().getName());
	            }
	        }
	        
		} finally {
			
			enableTLSChainTesting(true);
			
		}
	
        return list.toArray(new X509Certificate[0]);
	}
	
	/**
	 * Get a list of the Java root certificates.  
	 * For more information, <a href="http://stackoverflow.com/questions/3508050/how-can-i-get-a-list-of-trusted-root-certificates-in-java">How can I get a list of trusted root certificates in Java?</a>
	 * @return An array of X509Certificates root certificates from the Java trust store
	 * @throws Exception Thrown on problems.
	 */
	static final X509Certificate[] getJavaRootCertificates() throws Exception {
		
		//TODO: Maybe be good to consider caching this at some point (at least for a few seconds)
		
		// Load the JDK's cacerts keystore file
		String filename = System.getProperty("java.home") + "/lib/security/cacerts".replace('/', File.separatorChar);
    	logger.debug("CACERTS file, "+filename);
		FileInputStream is = new FileInputStream(filename);
		KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
		String password = "changeit"; //default password
		keystore.load(is, password.toCharArray());
		
		// This class retrieves the most-trusted CAs from the keystore
		PKIXParameters params = new PKIXParameters(keystore);
		
		// Get the set of trust anchors, which contain the most-trusted CA certificates,
		// and return the java root certs.
		Iterator<TrustAnchor> it = params.getTrustAnchors().iterator();
		ArrayList<X509Certificate> result = new ArrayList<X509Certificate>();
		while( it.hasNext() ) {
			TrustAnchor ta = it.next();
			result.add(ta.getTrustedCert());
		}
		
		return result.toArray(new X509Certificate[0]);

	}

//	/**
//	 * Test to see if a particular SHA1 hash is a root in the Java system keystore.
//	 * @param sha1hash
//	 * @return true, SHA1 hash belongs to a Java root.  false, no Java root found.
//	 */
//	public static final boolean isJavaRootCertificateSHA1(String sha1hash) throws Exception {
//		
//		boolean result = false;
//		
//		for( X509Certificate cert : getJavaRootCertificates() ) {
//			
//			String fingerprint = sha1Fingerprint(cert.getEncoded());
//		
//			if( fingerprint.equals(sha1hash) ) {
//				
//				result = true; break;
//			}
//		}
//		
//		return result;
//	}
	
	/**
	 * Test to see if a particular IssuerDN is a root in the Java system keystore.
	 * @param IssuerDN Issuing authority
	 * @return true, IssuerDN matches a Java root.  false, no matching IssuerDN found.
	 */
	static final boolean isJavaRootCertificateDN(String IssuerDN) throws Exception {
		
		boolean result = false;
		
		for( X509Certificate cert : getJavaRootCertificates() ) {
			
			if ( cert.getIssuerDN().getName().equals(IssuerDN) ) {
				
				result = true; break;
			}
		}
		
		return result;
	}
	
	/**
	 * Check trust status of each certificate in the chain.
	 * @param certs Chain of X509Certificates to test.
	 * @param url Server URL to test against.
	 * @throws KeyStoreException
	 * @throws NoSuchAlgorithmException
	 * @throws UnknownHostException
	 * @throws IOException
	 * @return True, the certificate chain is trusted.  False, the chain is not trusted.
	 */
	static final boolean checkTrustedCertificate( X509Certificate[] certs, URL url) throws KeyStoreException,
			NoSuchAlgorithmException, UnknownHostException, IOException {
		
		boolean valid = false;
		
        SSLSocketFactory factory = (SSLSocketFactory) SSLSocketFactory.getDefault();
        SSLSocket socket = (SSLSocket)factory.createSocket(url.getHost(), url.getDefaultPort());  
        SSLSession session = socket.getSession();
        String keyexchalgo = getKeyExchangeAlgorithm(session);
		try { socket.close(); } catch( IOException e ) {}
		
		TrustManagerFactory trustManagerFactory =
			    TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
			trustManagerFactory.init((KeyStore)null);
			// you could use a non-default KeyStore as your truststore too, instead of null.

			for (TrustManager trustManager: trustManagerFactory.getTrustManagers()) {  
			    if (trustManager instanceof X509TrustManager) {  
			        X509TrustManager x509TrustManager = (X509TrustManager)trustManager;  
			        try { 
			        	x509TrustManager.checkServerTrusted(certs,keyexchalgo);
			        	valid = true;
			        } catch( CertificateException e ) {
			        	// Eat the stacktrace
			        	logger.error( "url="+url.toString() );
			        }
			    }
			        
			}
		return valid;
	}
	
	// check OCSP
//	public static final String checkOCSPStatus(X509Certificate cert, X509Certificate issuer) {
//		
//		// init PKIX parameters
//        PKIXParameters params = null;
//	    params = new PKIXParameters(trustedCertsSet);
//	    params.addCertStore(store);
//	
//	    // enable OCSP
//	    Security.setProperty("ocsp.enable", "true");
//	    if (ocspServer != null) {
//			Security.setProperty("ocsp.responderURL", args[1]);
//			Security.setProperty("ocsp.responderCertSubjectName",
//			    ocspCert.getSubjectX500Principal().getName());
//	    }
//	
//	    // perform validation
//	    CertPathValidator cpv = CertPathValidator.getInstance("PKIX");
//	    PKIXCertPathValidatorResult cpv_result  =
//		(PKIXCertPathValidatorResult) cpv.validate(cp, params);
//	    X509Certificate trustedCert = (X509Certificate)
//		cpv_result.getTrustAnchor().getTrustedCert();
//	
//	    if (trustedCert == null) {
//	    	System.out.println("Trsuted Cert = NULL");
//	    } else {
//	    	System.out.println("Trusted CA DN = " +
//	    			trustedCert.getSubjectDN());
//	    }
//		
//		return buff.toString();	
//		
//    }
	
	/**
	 * Parse out the Cipher's Key Exchange Algorithm
	 * @param session Target SSLSession
	 * @return String TLS key exchange algorithm.
	 */
	private static final String getKeyExchangeAlgorithm( SSLSession session ) {
		
		String cipher = session.getCipherSuite().toString();
		
		int i1 = cipher.indexOf('_')+1;
		
