//
// CertMgr.cs: Certificate Manager clone tool (CLI version)
//
// Author:
//	Sebastien Pouliot  <sebastien@ximian.com>
//
// Copyright (C) 2004-2005 Novell, Inc (http://www.novell.com)
//

using System;
using System.Collections;
using System.Globalization;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Reflection;
using System.Security.Cryptography;
using SSCX = System.Security.Cryptography.X509Certificates;
using System.Text;

using Mono.Security.Authenticode;
using Mono.Security.Cryptography;
using Mono.Security.X509;
using Mono.Security.Protocol.Tls;

[assembly: AssemblyTitle ("Mono Certificate Manager")]
[assembly: AssemblyDescription ("Manage X.509 certificates and CRL from stores.")]

namespace Mono.Tools {

	class CertificateManager {

		static private void Header () 
		{
			Console.WriteLine (new AssemblyInfo ().ToString ());
		}

		static private void Help () 
		{
			Console.WriteLine ("Usage: certmgr [action] [object-type] [options] store [filename]");
			Console.WriteLine ("   or: certmgr -list object-type [options] store");
			Console.WriteLine ("   or: certmgr -del object-type [options] store certhash");
			Console.WriteLine ("   or: certmgr -ssl [options] url");
			Console.WriteLine ("   or: certmgr -put object-type [options] store certfile");
			Console.WriteLine ("   or: certmgr -importKey [options] store pkcs12file");
			Console.WriteLine ();
			Console.WriteLine ("actions");
			Console.WriteLine ("\t-add\t\tAdd a certificate, CRL or CTL to specified store");
			Console.WriteLine ("\t-del\t\tRemove a certificate, CRL or CTL to specified store");
			Console.WriteLine ("\t-put\t\tCopy a certificate, CRL or CTL from a store to a file");
			Console.WriteLine ("\t-list\t\tList certificates, CRL or CTL in the specified store.");
			Console.WriteLine ("\t-ssl\t\tDownload and add certificates from an SSL session");
			Console.WriteLine ("\t-importKey\tImport PKCS12 privateKey to keypair store.");
			Console.WriteLine ("object types");
			Console.WriteLine ("\t-c\t\tadd/del/put certificates");
			Console.WriteLine ("\t-crl\t\tadd/del/put certificate revocation lists");
			Console.WriteLine ("\t-ctl\t\tadd/del/put certificate trust lists [unsupported]");
			Console.WriteLine ("other options");
			Console.WriteLine ("\t-m\t\tuse the machine certificate store (default to user)");
			Console.WriteLine ("\t-v\t\tverbose mode (display status for every steps)");
			Console.WriteLine ("\t-p [password]\tPassword used to decrypt PKCS12");
			Console.WriteLine ("\t-pem\t\tPut certificate in Base-64 encoded format (default DER encoded)");
			Console.WriteLine ("\t-?\t\th[elp]\tDisplay this help message");
			Console.WriteLine ();
		}

		static string GetCommand (string arg) 
		{
			if ((arg == null) || (arg.Length < 1))
				return null;

			switch (arg [0]) {
				case '/':
					return arg.Substring (1).ToUpper ();
				case '-':
					if (arg.Length < 2)
						return null;
					int n = ((arg [1] == '-') ? 2 : 1);
					return arg.Substring (n).ToUpper ();
				default:
					return arg;
			}
		}

		enum Action {
			None,
			Add,
			Delete,
			Put,
			List,
			Ssl,
			ImportKey
		}

		static Action GetAction (string arg) 
		{
			Action action = Action.None;
			switch (GetCommand (arg)) {
				case "ADD":
					action = Action.Add;
					break;
				case "DEL":
				case "DELETE":
					action = Action.Delete;
					break;
				case "PUT":
					action = Action.Put;
					break;
				case "LST":
				case "LIST":
					action = Action.List;
					break;
				case "SSL":
				case "TLS":
					action = Action.Ssl;
					break;
				case "IMPORTKEY":
					action = Action.ImportKey;
					break;
			}
			return action;
		}

		enum ObjectType {
			None,
			Certificate,
			CRL,
			CTL
		}

		static ObjectType GetObjectType (string arg) 
		{
			ObjectType type = ObjectType.None;
			switch (GetCommand (arg)) {
				case "C":
				case "CERT":
				case "CERTIFICATE":
					type = ObjectType.Certificate;
					break;
				case "CRL":
					type = ObjectType.CRL;
					break;
				case "CTL":
					type = ObjectType.CTL;
					break;
			}
			return type;
		}

		static X509Store GetStoreFromName (string storeName, bool machine) 
		{
			X509Stores stores = ((machine) ? X509StoreManager.LocalMachine : X509StoreManager.CurrentUser);
			X509Store store = null;
			switch (storeName) {
				case X509Stores.Names.Personal:
					return stores.Personal;
				case X509Stores.Names.OtherPeople:
					return stores.OtherPeople;
				case X509Stores.Names.IntermediateCA:
					return stores.IntermediateCA;
				case "Root": // special case (same as trusted root)
				case X509Stores.Names.TrustedRoot:
					return stores.TrustedRoot;
				case X509Stores.Names.Untrusted:
					return stores.Untrusted;
			}
			return store;
		}

		static byte[] PEM (string type, byte[] data) 
		{
			string pem = Encoding.ASCII.GetString (data);
			string header = String.Format ("-----BEGIN {0}-----", type);
			string footer = String.Format ("-----END {0}-----", type);
			int start = pem.IndexOf (header) + header.Length;
			int end = pem.IndexOf (footer, start);
			string base64 = pem.Substring (start, (end - start));
			return Convert.FromBase64String (base64);
		}
		
		static byte[] ToPEM (string type, byte[] data)
		{
			string header = String.Format ("-----BEGIN {0}-----", type);
			string footer = String.Format ("-----END {0}-----", type);
			
			string encodedString = Convert.ToBase64String (data);

			StringBuilder sb = new StringBuilder ();
			int remaining = encodedString.Length;
			sb.AppendLine (header);
			for (int i = 0; i <= encodedString.Length; i += 64) {
				if (remaining >= 64) {
					sb.AppendLine (encodedString.Substring (i, 64));
				} else {
					sb.AppendLine (encodedString.Substring (i, remaining));
				}
				remaining -= 64;
			}
			sb.AppendLine (footer);
			return Encoding.ASCII.GetBytes (sb.ToString ());
		}

		static X509CertificateCollection LoadCertificates (string filename, string password, bool verbose) 
		{
			X509Certificate x509 = null;
			X509CertificateCollection coll = new X509CertificateCollection ();
			switch (Path.GetExtension (filename).ToUpper ()) {
				case ".P7B":
				case ".SPC":
					SoftwarePublisherCertificate spc = SoftwarePublisherCertificate.CreateFromFile (filename);
					coll.AddRange (spc.Certificates);
					spc = null;
					break;
				case ".CER":
				case ".CRT":
					using (FileStream fs = File.OpenRead (filename)) {
						byte[] data = new byte [fs.Length];
						fs.Read (data, 0, data.Length);
						if (data [0] != 0x30) {
							// maybe it's ASCII PEM base64 encoded ?
							data = PEM ("CERTIFICATE", data);
						}
						if (data != null)
							x509 = new X509Certificate (data);
					}
					if (x509 != null)
						coll.Add (x509);
					break;
				case ".P12":
				case ".PFX":
					PKCS12 p12 = password == null ? PKCS12.LoadFromFile (filename)
						: PKCS12.LoadFromFile (filename, password);
					X509CertificateCollection tmp = new X509CertificateCollection (p12.Certificates);

					for (int i = 0; i != p12.Keys.Count; i++) {
						X509Certificate cert = p12.Certificates[i];
						RSACryptoServiceProvider pk = p12.Keys[i] as RSACryptoServiceProvider;

						if (pk == null || pk.PublicOnly)
							continue;

						if (verbose)
							Console.WriteLine ("Found key for certificate: {0}", cert.SubjectName);

						tmp[0].RSA = pk;
					}
					coll.AddRange(tmp);
					p12 = null;
					break;
				default:
					Console.WriteLine ("Unknown file extension: {0}", 
						Path.GetExtension (filename));
					break;
			}
			return coll;
		}

		static ArrayList LoadCRLs (string filename) 
		{
			X509Crl crl = null;
			ArrayList list = new ArrayList ();
			switch (Path.GetExtension (filename).ToUpper ()) {
				case ".P7B":
				case ".SPC":
					SoftwarePublisherCertificate spc = SoftwarePublisherCertificate.CreateFromFile (filename);
					list.AddRange (spc.Crls);
					spc = null;
					break;
				case ".CRL":
					using (FileStream fs = File.OpenRead (filename)) {
						byte[] data = new byte [fs.Length];
						fs.Read (data, 0, data.Length);
						crl = new X509Crl (data);
					}
					list.Add (crl);
					break;
				default:
					Console.WriteLine ("Unknown file extension: {0}", 
						Path.GetExtension (filename));
					break;
			}
			return list;
		}

		static void Add (ObjectType type, X509Store store, string file, string password, bool verbose) 
		{
			switch (type) {
				case ObjectType.Certificate:
					X509CertificateCollection coll = LoadCertificates (file, password, verbose);
					foreach (X509Certificate x509 in coll) {
						store.Import (x509);
					}
					Console.WriteLine ("{0} certificate(s) added to store {1}.", 
						coll.Count, store.Name);
					break;
				case ObjectType.CRL:
					ArrayList list = LoadCRLs (file);
					foreach (X509Crl crl in list) {
						store.Import (crl);
					}
					Console.WriteLine ("{0} CRL(s) added to store {1}.", 
						list.Count, store.Name);
					break;
				default:
					throw new NotSupportedException (type.ToString ());
			}
		}

		static void Delete (ObjectType type, X509Store store, string hash, bool verbose) 
		{
			switch (type) {
				case ObjectType.Certificate:
					foreach (X509Certificate x509 in store.Certificates) {
						if (hash == CryptoConvert.ToHex (x509.Hash)) {
							store.Remove (x509);
							Console.WriteLine ("Certificate removed from store.");
							return;
						}
					}
					break;
				case ObjectType.CRL:
					foreach (X509Crl crl in store.Crls) {
						if (hash == CryptoConvert.ToHex (crl.Hash)) {
							store.Remove (crl);
							Console.WriteLine ("CRL removed from store.");
							return;
						}
					}
					break;
				default:
					throw new NotSupportedException (type.ToString ());
			}
		}

		static void Put (ObjectType type, X509Store store, string file, bool machine, bool pem, bool verbose) 
		{
			if (String.IsNullOrEmpty (file)) {
				Console.Error.WriteLine("error: no filename provided to put the certificate.");
				Help();
				return;
			}

			switch (type) {
			case ObjectType.Certificate:
				for(int i = 0; i < store.Certificates.Count; i++) {
					Console.WriteLine ("==============Certificate # {0} ==========", i + 1);
					DisplayCertificate (store.Certificates[i], machine, verbose);
				}
				int selection;
				Console.Write("Enter cert # from the above list to put-->");
				if (!int.TryParse(Console.ReadLine(), out selection) || selection > store.Certificates.Count) {
					Console.Error.WriteLine ("error: invalid selection.");
					return;
				}
			
				SSCX.X509Certificate2 cert = new SSCX.X509Certificate2 (store.Certificates[selection-1].RawData);
				byte[] data = null;
				if(pem) {
					data = ToPEM ("CERTIFICATE", cert.Export (SSCX.X509ContentType.Cert));
				} else {
					data = cert.Export (SSCX.X509ContentType.Cert);
				}
				
				using (FileStream fs = File.Create (file)) {
					fs.Write(data, 0, data.Length);
				}
				
				Console.WriteLine ("Certificate put to {0}.", file);
				break;
			default:
				throw new NotSupportedException ("Put " + type + " not supported yet");
			}
		}

		static void DisplayCertificate (X509Certificate x509, bool machine, bool verbose)
		{
			Console.WriteLine ("{0}X.509 v{1} Certificate", (x509.IsSelfSigned ? "Self-signed " : String.Empty), x509.Version);
			Console.WriteLine ("  Serial Number: {0}", CryptoConvert.ToHex (x509.SerialNumber));
			Console.WriteLine ("  Issuer Name:   {0}", x509.IssuerName);
			Console.WriteLine ("  Subject Name:  {0}", x509.SubjectName);
			Console.WriteLine ("  Valid From:    {0}", x509.ValidFrom);
			Console.WriteLine ("  Valid Until:   {0}", x509.ValidUntil);
			Console.WriteLine ("  Unique Hash:   {0}", CryptoConvert.ToHex (x509.Hash));
			if (verbose) {
				Console.WriteLine ("  Key Algorithm:        {0}", x509.KeyAlgorithm);
				Console.WriteLine ("  Algorithm Parameters: {0}", (x509.KeyAlgorithmParameters == null) ? "None" :
					CryptoConvert.ToHex (x509.KeyAlgorithmParameters));
				Console.WriteLine ("  Public Key:           {0}", CryptoConvert.ToHex (x509.PublicKey));
				Console.WriteLine ("  Signature Algorithm:  {0}", x509.SignatureAlgorithm);
				Console.WriteLine ("  Algorithm Parameters: {0}", (x509.SignatureAlgorithmParameters == null) ? "None" :
					CryptoConvert.ToHex (x509.SignatureAlgorithmParameters));
				Console.WriteLine ("  Signature:            {0}", CryptoConvert.ToHex (x509.Signature));
				RSACryptoServiceProvider rsaCsp = x509.RSA as RSACryptoServiceProvider;
				RSAManaged rsaManaged = x509.RSA as RSAManaged;
				Console.WriteLine ("  Private Key:			{0}", ((rsaCsp != null && !rsaCsp.PublicOnly) 
					|| (rsaManaged != null && !rsaManaged.PublicOnly)));
				CspParameters cspParams = new CspParameters ();
				cspParams.KeyContainerName = CryptoConvert.ToHex (x509.Hash);
				cspParams.Flags = machine ? CspProviderFlags.UseMachineKeyStore : 0;
				KeyPairPersistence kpp = new KeyPairPersistence (cspParams);
				Console.WriteLine ("  KeyPair Key:			{0}", kpp.Load ());
			}
			Console.WriteLine ();
		}

		static void DisplayCrl (X509Crl crl, bool machine, bool verbose)
		{
			Console.WriteLine ("X.509 v{0} CRL", crl.Version);
			Console.WriteLine ("  Issuer Name:   {0}", crl.IssuerName);
			Console.WriteLine ("  This Update:   {0}", crl.ThisUpdate);
			Console.WriteLine ("  Next Update:   {0} {1}", crl.NextUpdate, crl.IsCurrent ? String.Empty : "update overdue!");
			Console.WriteLine ("  Unique Hash:   {0}", CryptoConvert.ToHex (crl.Hash));
			if (verbose) {
				Console.WriteLine ("  Signature Algorithm:  {0}", crl.SignatureAlgorithm);
				Console.WriteLine ("  Signature:            {0}", CryptoConvert.ToHex (crl.Signature));
				int n = 0;
				foreach (X509Crl.X509CrlEntry entry in crl.Entries) {
					Console.WriteLine ("    #{0}: Serial: {1} revoked on {2}",
						++n, CryptoConvert.ToHex (entry.SerialNumber), entry.RevocationDate);
				}
			}
		}

		static void List (ObjectType type, X509Store store, bool machine, string file, bool verbose) 
		{
			switch (type) {
				case ObjectType.Certificate:
					foreach (X509Certificate x509 in store.Certificates) {
						DisplayCertificate (x509, machine, verbose);
					}
					break;
				case ObjectType.CRL:
					foreach (X509Crl crl in store.Crls) {
						DisplayCrl (crl, machine, verbose);
					}
					break;
				default:
					throw new NotSupportedException (type.ToString ());
			}
		}

		static X509CertificateCollection GetCertificatesFromSslSession (string url) 
		{
			Uri uri = new Uri (url);
			IPHostEntry host = Dns.Resolve (uri.Host);
			IPAddress ip = host.AddressList [0];
			Socket socket = new Socket (ip.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
			socket.Connect (new IPEndPoint (ip, uri.Port));
			NetworkStream ns = new NetworkStream (socket, false);
			SslClientStream ssl = new SslClientStream (ns, uri.Host, false, Mono.Security.Protocol.Tls.SecurityProtocolType.Default, null);
			ssl.ServerCertValidationDelegate += new CertificateValidationCallback (CertificateValidation);

			try {
				// we don't really want to write to the server (as we don't know
				// the protocol it using) but we must send something to be sure the
				// SSL handshake is done (so we receive the X.509 certificates).
				StreamWriter sw = new StreamWriter (ssl);
				sw.WriteLine (Environment.NewLine);
				sw.Flush ();
				socket.Poll (30000, SelectMode.SelectRead);
			}
			finally {
				socket.Close ();
			}

			// we need a little reflection magic to get this information
			PropertyInfo pi = typeof (SslStreamBase).GetProperty ("ServerCertificates", BindingFlags.Instance | BindingFlags.NonPublic);
			if (pi == null) {
				Console.WriteLine ("Sorry but you need a newer version of Mono.Security.dll to use this feature.");
				return null;
			}
			return (X509CertificateCollection) pi.GetValue (ssl, null);
		}

		static bool CertificateValidation (SSCX.X509Certificate certificate, int[] certificateErrors)
		{
			// the main reason to download it is that it's not trusted
			return true;
			// OTOH we ask user confirmation before adding certificates into the stores
		}

		static void Ssl (string host, bool machine, bool verbose) 
		{
			if (verbose) {
				Console.WriteLine ("Importing certificates from '{0}' into the {1} stores.",
					host, machine ? "machine" : "user");
			}
			int n=0;

			X509CertificateCollection coll = GetCertificatesFromSslSession (host);
			if (coll != null) {
				X509Store store = null;
				// start by the end (root) so we can stop adding them anytime afterward
				for (int i = coll.Count - 1; i >= 0; i--) {
					X509Certificate x509 = coll [i];
					bool selfsign = false;
					bool failed = false;
					try {
						selfsign = x509.IsSelfSigned;
					}
					catch {
						// sadly it's hard to interpret old certificates with MD2
						// without manually changing the machine.config file
						failed = true;
					}

					if (selfsign) {
						// this is a root
						store = GetStoreFromName (X509Stores.Names.TrustedRoot, machine);
					} else if (i == 0) {
						// server certificate isn't (generally) an intermediate CA
						store = GetStoreFromName (X509Stores.Names.OtherPeople, machine);
					} else {
						// all other certificates should be intermediate CA
						store = GetStoreFromName (X509Stores.Names.IntermediateCA, machine);
					}

					Console.WriteLine ("{0}{1}X.509 Certificate v{2}", 	
						Environment.NewLine,
						selfsign ? "Self-signed " : String.Empty,
						x509.Version);
					Console.WriteLine ("   Issued from: {0}", x509.IssuerName);
					Console.WriteLine ("   Issued to:   {0}", x509.SubjectName);
					Console.WriteLine ("   Valid from:  {0}", x509.ValidFrom);
					Console.WriteLine ("   Valid until: {0}", x509.ValidUntil);

					if (!x509.IsCurrent)
						Console.WriteLine ("   *** WARNING: Certificate isn't current ***");
					if ((i > 0) && !selfsign) {
						X509Certificate signer = coll [i-1];
						bool signed = false;
						try {
							if (signer.RSA != null) {
								signed = x509.VerifySignature (signer.RSA);
							} else if (signer.DSA != null) {
								signed = x509.VerifySignature (signer.DSA);
							} else {
								Console.WriteLine ("   *** WARNING: Couldn't not find who signed this certificate ***");
								signed = true; // skip next warning
							}

							if (!signed)
								Console.WriteLine ("   *** WARNING: Certificate signature is INVALID ***");
						}
						catch {
							failed = true;
						}
					}
					if (failed) {
						Console.WriteLine ("   *** ERROR: Couldn't decode certificate properly ***");
						Console.WriteLine ("   *** try 'man certmgr' for additional help or report to bugzilla.novell.com ***");
						break;
					}

					if (store.Certificates.Contains (x509)) {
						Console.WriteLine ("This certificate is already in the {0} store.", store.Name);
					} else {
						Console.Write ("Import this certificate into the {0} store ?", store.Name);
						string answer = Console.ReadLine ().ToUpper ();
						if ((answer == "YES") || (answer == "Y")) {
							store.Import (x509);
							n++;
						} else {
							if (verbose) {
								Console.WriteLine ("Certificate not imported into store {0}.", 
									store.Name);
							}
							break;
						}
					}
				}
			}

			Console.WriteLine ();
			if (n == 0) {
				Console.WriteLine ("No certificate were added to the stores.");
			} else {
				Console.WriteLine ("{0} certificate{1} added to the stores.", 
					n, (n == 1) ? String.Empty : "s");
			}
		}

		static void ImportKey (ObjectType type, bool machine, string file, string password, bool verbose)
		{
			switch (type) {
				case ObjectType.Certificate:
					X509CertificateCollection coll = LoadCertificates (file, password, verbose);
					int count = 0;

					foreach (X509Certificate x509 in coll) {
						RSACryptoServiceProvider pk = x509.RSA as RSACryptoServiceProvider;

						if (pk == null || pk.PublicOnly)
							continue;

						CspParameters csp = new CspParameters ();
						csp.KeyContainerName = CryptoConvert.ToHex (x509.Hash);
						csp.Flags = machine ? CspProviderFlags.UseMachineKeyStore : 0;
						RSACryptoServiceProvider rsa = new RSACryptoServiceProvider (csp);
						rsa.ImportParameters (pk.ExportParameters (true));
						rsa.PersistKeyInCsp = true;
						count++;
					}
					Console.WriteLine ("{0} keys(s) imported to KeyPair {1} persister.", 
						count, machine ? "LocalMachine" : "CurrentUser");
					break;
				default:
					throw new NotSupportedException (type.ToString ());
			}
		}

		[STAThread]
		static void Main (string[] args)
		{
			string password = null;
			bool verbose = false;
			bool pem = false;
			bool machine = false;

			Header ();
			if (args.Length < 2) {
				Help ();
				return;
			}

			Action action = GetAction (args [0]);
			ObjectType type = ObjectType.None;

			int n = 1;
			if (action != Action.Ssl) {
				type = GetObjectType (args [n]);
				if (type != ObjectType.None)
					n++;
			}
			
			for (int i = n; i < args.Length; i++) {
				switch (GetCommand (args[i])) {
				case "V":
					verbose = true;
					n++;
					break;
				case "M":
					machine = true;
					n++;
					break;
				case "P":
					password = args[++n];
					n++;
					break;
				case "PEM":
					pem = true;
					n++;
					break;
				}
			}
			
			X509Store store = null;
			string storeName = null;
			if (action != Action.Ssl) {
				if ((action == Action.None) || (type == ObjectType.None)) {
					Help ();
					return;
				}
				if (type == ObjectType.CTL) {
					Console.WriteLine ("CTL are not supported");
					return;
				}

				storeName = args [n++];
				store = GetStoreFromName (storeName, machine);
				if (store == null) {
					Console.WriteLine ("Invalid Store: {0}", storeName);
					Console.WriteLine ("Valid stores are: {0}, {1}, {2}, {3} and {4}",
						X509Stores.Names.Personal,
						X509Stores.Names.OtherPeople, 
						X509Stores.Names.IntermediateCA, 
						X509Stores.Names.TrustedRoot, 
						X509Stores.Names.Untrusted);
					return;
				}
			}

			string file = (n < args.Length) ? args [n] : null;

			// now action!
			try {
				switch (action) {
				case Action.Add:
					Add (type, store, file, password, verbose);
					break;
				case Action.Delete:
					Delete (type, store, file, verbose);
					break;
				case Action.Put:
					Put (type, store, file, machine, pem, verbose);
					break;
				case Action.List:
					List (type, store, machine, file, verbose);
					break;
				case Action.Ssl:
					Ssl (file, machine, verbose);
					break;
				case Action.ImportKey:
					ImportKey (type, machine, file, password, verbose);
					break;
				default:
					throw new NotSupportedException (action.ToString ());
				}
			}
			catch (UnauthorizedAccessException uae) {
				Console.WriteLine ("Access to the {0} '{1}' certificate store has been denied.", 
					(machine ? "machine" : "user"), storeName);
				if (verbose) {
					Console.WriteLine (uae);
				}
			}
		}
	}
}