//
// CertificateFormatter.cs: Certificate Formatter (not GUI specific)
//
// Author:
//	Sebastien Pouliot  <sebastien@ximian.com>
//
// (C) 2003 Motus Technologies Inc. (http://www.motus.com)
// (C) 2004 Novell (http://www.novell.com)
//

using System;
using System.Collections;
using System.Configuration;
using System.IO;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;

using Mono.Security.X509;
using Mono.Security.X509.Extensions;

namespace Mono.Tools.CertView {

	public class CertificateFormatter {

		public class FieldNames {
			public FieldNames () {}

			public const string Version = "Version";
			public const string SerialNumber = "Serial number";
			public const string SignatureAlgorithm = "Signature algorithm";
			public const string Issuer = "Issuer";
			public const string ValidFrom = "Valid from";
			public const string ValidUntil = "Valid until";
			public const string Subject = "Subject";
			public const string PublicKey = "Public key";
		}

		public class PropertyNames {
			public PropertyNames () {}

			public const string ThumbprintAlgorithm = "Thumbprint algorithm";
			public const string Thumbprint = "Thumbprint";
		}

		public class Help {
			public Help () {}

			public const string IssuedBy = "This is the distinguished name (DN) of the certificate authority (CA) that issued this certificate.";
			public const string IssuedTo = "This is the distinguished name (DN) of the entity (individual, device or organization) to whom the certificate was issued.";
			public const string ValidFrom = "This certificate isn't valid before the specified date.";
			public const string ValidUntil = "This certificate isn't valid after the specified date. This also means that the certificate authority (CA) won't publish the status of the certificate after this date.";
		}

		private const string untrustedRoot = "This root certificate isn't part of your trusted root store. Please read your documentation carefully before adding a new root certificate in your trusted store.";
		private const string unknownCriticalExtension = "This certificate contains unknown critical extensions and shouldn't be used by applications that can't process those extensions.";
		private const string noSignatureCheck = "The signature of the certificate can;t be verified without the issuer certificate.";
		private const string noValidation = "No CRL, nor an OCSP responder, has been found to validate the status of the certificate.";
		private const string unsupportedHash = "The {0} algorithm is unsupported by the .NET Framework. The certificate signature cannot be verified.";

		private string thumbprintAlgorithm;
		private X509Certificate x509;
		private string status;
		private string[] subjectAltName;

		private static string defaultThumbprintAlgo;
		private static Hashtable extensions;

		static CertificateFormatter () 
		{
			IDictionary tb = (IDictionary) ConfigurationSettings.GetConfig ("Thumbprint");
			defaultThumbprintAlgo = ((tb != null) ? (string) tb ["Algorithm"] : "SHA1");

			extensions = new Hashtable ();
			IDictionary exts = (IDictionary) ConfigurationSettings.GetConfig ("X509.Extensions");
			if (exts != null) {
				foreach (DictionaryEntry ext in exts)
					extensions.Add (ext.Key, ext.Value);
			}
		}

		private X509Extension CreateExtensionFromOid (string oid, object[] args) 
		{
			try {
				Type algoClass = null;
				string algo = (string) extensions [oid];
				// do we have an entry
				if (algo == null)
					return (X509Extension) args [0];
				algoClass = Type.GetType (algo);
				// call the constructor for the type
				return (X509Extension) Activator.CreateInstance (algoClass, args);
			}
			catch {
				// method doesn't throw any exception
				return (X509Extension) args [0];
			}
		}

		public CertificateFormatter (string filename)
		{
			byte[] data = null;
			using (FileStream fs = File.Open (filename, FileMode.Open, FileAccess.Read, FileShare.Read)) {
        			data = new byte [fs.Length];
        			fs.Read (data, 0, data.Length);
        			fs.Close ();
       			}
       			
       			if ((data != null) && (data.Length > 0)) {
        			X509Certificate x509 = null;
        			if (data [0] != 0x30) {
        				// it may be PEM encoded
        				data = FromPEM (data);
        			}
        			
        			if (data [0] == 0x30) {
        				x509 = new X509Certificate (data);
                			if (x509 != null) {
                				Initialize (x509);
                			}
        			}
			}
		}
		
		private byte[] FromPEM (byte[] data) 
		{
			string pem = Encoding.ASCII.GetString (data);
			int start = pem.IndexOf ("-----BEGIN CERTIFICATE-----");
			if (start < 0)
				return null;

			start += 27; // 27 being the -----BEGIN CERTIFICATE----- length
			int end = pem.IndexOf ("-----END CERTIFICATE-----", start);
			if (end < start)
				return null;
				
			string base64 = pem.Substring (start, (end - start));
			return Convert.FromBase64String (base64);
		}

		public CertificateFormatter (X509Certificate cert)
		{
			Initialize (cert);
		}

		internal void Initialize (X509Certificate cert) 
		{
			x509 = cert;
			thumbprintAlgorithm = defaultThumbprintAlgo;
			try {
				// preprocess some informations
				foreach (X509Extension xe in x509.Extensions) {
					if ((!extensions.ContainsKey (xe.OID)) && (xe.Critical))
						status = unknownCriticalExtension;
					if (xe.OID == "2.5.29.17") {
						SubjectAltNameExtension san = new SubjectAltNameExtension (xe);
						subjectAltName = san.RFC822;
					}
				}
				
				if (x509.IsSelfSigned) {
					status = untrustedRoot;
				}
			}
			catch (Exception e) {
				status = e.ToString ();
			}
		}

		public X509Certificate Certificate {
			get { return x509; }
		}

		public string Status {
			get { return status; }
		}

		public X509Extension GetExtension (int i) 
		{
			X509Extension xe = x509.Extensions [i];
			object[] extn = new object [1] { xe };
			return CreateExtensionFromOid (xe.OID, extn); 
		}

		public string Extension (int i, bool detailed) 
		{
			X509Extension xe = x509.Extensions [i];
			if (!detailed)
				return Array2Word (xe.Value.Value);
			return Extension2String (x509.Extensions[i].Value.Value);
		}

		private string DN (string dname, bool detailed) 
		{
			string[] a = dname.Split (',');
			StringBuilder sb = new StringBuilder ();

			if (detailed) {
				foreach (string s in a) {
					string s2 = s.Trim () + Environment.NewLine;
					sb.Insert (0, s2.Replace ("=", " = "));
				}
			}
			else {
				foreach (string s in a) {
					string s2 = s.Trim ();
					sb.Insert (0, s2.Substring (s2.IndexOf ("=") + 1) + ", ");
				}
				// must remove last ", "
				sb.Remove (sb.Length - 2, 2);
			}

			return sb.ToString();
		}

		public string Issuer (bool detailed) 
		{
			return DN (x509.IssuerName, detailed);
		}

		public string PublicKey (bool detailed) 
		{
			if (detailed)
				return Array2Word (x509.PublicKey);

			if (x509.RSA != null)
				return "RSA (" + x509.RSA.KeySize + " Bits)";
			else if (x509.DSA != null)
				return "DSA (" + x509.DSA.KeySize + " Bits)";
			return "Unknown key type (unknown key size)";
		}

		public string SerialNumber (bool detailed) 
		{
			byte[] sn = (byte[]) x509.SerialNumber.Clone ();
			Array.Reverse (sn);
			return CertificateFormatter.Array2Word (sn);
		}

		public string Subject (bool detailed) 
		{
			return DN (x509.SubjectName, detailed);
		}

		public string SubjectAltName (bool detailed) 
		{
			if ((subjectAltName == null) || (subjectAltName.Length < 1))
				return String.Empty;
			if (!detailed)
				return "mailto:" + subjectAltName [0];

			StringBuilder sb = new StringBuilder ();
			foreach (string s in subjectAltName) {
				sb.Append (s);
				sb.Append (Environment.NewLine);
			}
			return sb.ToString ();
		}

		public string SignatureAlgorithm (bool detailed) 
		{
			string result = null;

			switch (x509.SignatureAlgorithm) {
				case "1.2.840.10040.4.3":
					result = "sha1DSA";
					break;
				case "1.2.840.113549.1.1.2":
					result = "md2RSA";
					status = String.Format (unsupportedHash, "MD2");
					break;
				case "1.2.840.113549.1.1.3":
					result = "md4RSA";
					status = String.Format (unsupportedHash, "MD4");
					break;
				case "1.2.840.113549.1.1.4":
					result = "md5RSA";
					break;
				case "1.2.840.113549.1.1.5":
					result = "sha1RSA";
					break;
				case "1.3.14.3.2.29":
					result = "sha1WithRSASignature";
					break;
				default:
					result = x509.SignatureAlgorithm;
					if (detailed)
						return "unknown (" + result + ")";
					return result;
			}
			if (detailed)
				result += " (" + x509.SignatureAlgorithm + ")";
			return result;
		}

		public string ThumbprintAlgorithm {
			get { return thumbprintAlgorithm.ToLower (); }
			set { thumbprintAlgorithm = value; }
		}

		public byte[] Thumbprint {
			get {
				HashAlgorithm ha = HashAlgorithm.Create (thumbprintAlgorithm);
				return ha.ComputeHash (x509.RawData);
			}
		}

		public string ValidFrom (bool detailed) 
		{
			return x509.ValidFrom.ToString ();
		}

		public string ValidUntil (bool detailed) 
		{
			return x509.ValidUntil.ToString ();
		}

		public string Version (bool detailed)
		{
			return "V" + x509.Version;
		}

		static public string OneLine (string input) 
		{
			// remove tabulation
			string oneline = input.Replace ("\t", "");
			// remove new lines after :
			oneline = oneline.Replace (":" + Environment.NewLine, ":");
			// remove ending new line (if present)
			if (oneline.EndsWith (Environment.NewLine))
				oneline = oneline.Substring (0, oneline.Length - Environment.NewLine.Length);
			// replace remaining new lines by comma + space
			return oneline.Replace (Environment.NewLine, ", ");
		}

		static public string Array2Word (byte[] array) 
		{
			StringBuilder sb = new StringBuilder ();
			int x = 0;
			while (x < array.Length) {
				sb.Append (array [x].ToString ("X2"));
				if (x % 2 == 1)
					sb.Append (" ");
				x++;
			}
			return sb.ToString ();
		}

		static private void WriteLine (StringBuilder sb, byte[] extnValue, int n, int pos) 
		{
			int p = pos;
			StringBuilder preview = new StringBuilder ();
			for (int j=0; j < 8; j++) {
				if (j < n) {
					sb.Append (extnValue [p++].ToString ("X2"));
					sb.Append (" ");
				}
				else
					sb.Append ("   ");
			}
			sb.Append ("  ");
			p = pos;
			for (int j=0; j < n; j++) {
				byte b = extnValue [p++];
				if (b < 0x20)
					sb.Append (".");
				else
					sb.Append (Convert.ToChar (b));
			}
			sb.Append (Environment.NewLine);
		}

		static public string Extension2String (byte[] extnValue) 
		{
			StringBuilder sb = new StringBuilder ();
			int div = (extnValue.Length >> 3);
			int rem = (extnValue.Length - (div << 3));
			int x = 0;
			for (int i=0; i < div; i++) {
				WriteLine (sb, extnValue, 8, x);
				x += 8;
			}
			WriteLine (sb, extnValue, rem, x);
			return sb.ToString ();
		}
	}
}
