//
// crlupdate.cs: CRL downloader / updater
//
// Author:
//	Sebastien Pouliot  <sebastien@ximian.com>
//
// Copyright (C) 2011 Novell, Inc (http://www.novell.com)
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
// 
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//

using System;
using System.Net;
using System.Reflection;

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

[assembly: AssemblyTitle ("Mono CRL Updater")]
[assembly: AssemblyDescription ("Download and update X.509 certificate revocation lists from your stores.")]

namespace Mono.Tools {

	class CrlUpdater {

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

		static private void Help ()
		{
			Console.WriteLine ("Usage: crlupdate [-m] [-v] [-f] [-?]");
			Console.WriteLine ();
			Console.WriteLine ("\t-m\tuse the machine certificate store (default to user)");
			Console.WriteLine ("\t-v\tverbose mode (display status for every steps)");
			Console.WriteLine ("\t-f\tforce download (and replace existing CRL)");
			Console.WriteLine ("\t-?\tDisplay this help message");
			Console.WriteLine ();
		}

		static X509Certificate FindCrlIssuer (string name, byte[] aki, X509CertificateCollection col)
		{
			foreach (X509Certificate cert in col) {
				if (name != cert.SubjectName)
					continue;
				if ((aki == null) || Compare (aki, GetSubjectKeyIdentifier (cert.Extensions ["2.5.29.14"])))
					return cert;
			}
			return null;
		}

		static X509Certificate FindCrlIssuer (X509Crl crl)
		{
			string name = crl.IssuerName;
			byte [] aki = GetAuthorityKeyIdentifier (crl.Extensions ["2.5.29.35"]);
			X509Certificate cert = FindCrlIssuer (name, aki, X509StoreManager.IntermediateCACertificates);
			if (cert != null)
				return cert;
			return FindCrlIssuer (name, aki, X509StoreManager.TrustedRootCertificates);
		}

		static X509Chain chain = new X509Chain ();

		static bool VerifyCrl (X509Crl crl)
		{
			X509Certificate issuer = FindCrlIssuer (crl);
			if (issuer == null)
				return false;

			if (!crl.VerifySignature (issuer))
				return false;

			chain.Reset ();
			return chain.Build (issuer);
		}

		static void Download (string url, X509Store store)
		{
			if (verbose)
				Console.WriteLine ("Downloading: {0}", url);

			WebClient wc = new WebClient ();
			string error = "download";
			try {
				byte [] data = wc.DownloadData (url);
				error = "decode";
				X509Crl crl = new X509Crl (data);
				error = "import";
				// warn if CRL is not current - but still allow it to be imported
				if (!crl.IsCurrent && verbose)
					Console.WriteLine ("WARNING: CRL is not current: {0}", url);

				// only import the CRL if its signature is valid and coming from a trusted root
				if (VerifyCrl (crl))
					store.Import (crl);
				else
					Console.WriteLine ("ERROR: could not validate CRL: {0}", url);
			}
			catch (Exception e) {
				Console.WriteLine ("ERROR: could not {0}: {1}", error, url);
				if (verbose) {
					Console.WriteLine (e);
					Console.WriteLine ();
				}
			}
		}

		static byte [] GetAuthorityKeyIdentifier (X509Extension ext)
		{
			if (ext == null)
				return null;

			AuthorityKeyIdentifierExtension aki = new AuthorityKeyIdentifierExtension (ext);
			return aki.Identifier;
		}

		static byte [] GetSubjectKeyIdentifier (X509Extension ext)
		{
			if (ext == null)
				return null;

			SubjectKeyIdentifierExtension ski = new SubjectKeyIdentifierExtension (ext);
			return ski.Identifier;
		}

		static bool Compare (byte [] a, byte [] b)
		{
			if (a == null)
				return (b == null);
			else if (b == null)
				return false;

			if (a.Length != b.Length)
				return false;

			for (int i = 0; i < a.Length; i++) {
				if (a [i] != b [i])
					return false;
			}
			return true;
		}

		static X509Crl FindCrl (X509Certificate cert, X509Store store)
		{
			string name = cert.SubjectName;
			byte [] ski = GetSubjectKeyIdentifier (cert.Extensions ["2.5.29.14"]);
			foreach (X509Crl crl in store.Crls) {
				if (crl.IssuerName != name)
					continue;
				if ((ski == null) || Compare (ski, GetAuthorityKeyIdentifier (crl.Extensions ["2.5.29.35"])))
					return crl;
			}
			return null;
		}

		static void UpdateStore (X509Store store)
		{
			// for each certificate
			foreach (X509Certificate cert in store.Certificates) {

				// do we already have a matching CRL ? (or are we forced to download?)
				X509Crl crl = force ? null : FindCrl (cert, store);
				// without a CRL (or with a CRL in need of updating)
				if ((crl == null) || !crl.IsCurrent) {
					X509Extension ext = cert.Extensions ["2.5.29.31"];
					if (ext == null) {
						if (verbose)
							Console.WriteLine ("WARNING: No cRL distribution point found for '{0}'", cert.SubjectName);
						continue;
					}

					CRLDistributionPointsExtension crlDP = new CRLDistributionPointsExtension (ext);
					foreach (var dp in crlDP.DistributionPoints) {
						string name = dp.Name.Trim ();
						if (name.StartsWith ("URL="))
							Download (name.Substring (4), store);
						else if (verbose)
							Console.WriteLine ("WARNING: Unsupported distribution point: '{0}'", name);
					}
				}
			}
		}

		static bool verbose = false;
		static bool force = false;

		static int Main (string [] args)
		{
			bool machine = false;

			for (int i = 0; i < args.Length; i++) {
				switch (args [i]) {
				case "-m":
				case "--m":
					machine = true;
					break;
				case "-v":
				case "--v":
					verbose = true;
					break;
				case "-f":
				case "--f":
					force = true;
					break;
				case "-help":
				case "--help":
				case "-?":
				case "--?":
					Help ();
					return 0;
				}
			}

			try {
				X509Stores stores = ((machine) ? X509StoreManager.LocalMachine : X509StoreManager.CurrentUser);
				// for all store (expect Untrusted)
				UpdateStore (stores.TrustedRoot);
				UpdateStore (stores.IntermediateCA);
				UpdateStore (stores.Personal);
				UpdateStore (stores.OtherPeople);
				return 0;
			}
			catch (Exception e) {
				Console.WriteLine ("ERROR: Unexpected exception: {0}", e);
				return 1;
			}
		}
	}
}