//
// SignedXml.cs - SignedXml implementation for XML Signature
//
// Author:
//	Sebastien Pouliot (spouliot@motus.com)
//
// (C) 2002, 2003 Motus Technologies Inc. (http://www.motus.com)
//

using System;
using System.Collections;
using System.IO;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Xml;

using SSCX = System.Security.Cryptography.Xml;

#if WSE2
using Microsoft.Web.Services.Xml;
#endif
#if (WSE1 || WSE2)
using Microsoft.Web.Services.Security;

namespace Microsoft.Web.Services.Security {

	public class SignedXml : IXmlElement {
#else
using System.Security.Cryptography.Xml;

namespace System.Security.Cryptography.Xml {

	public class SignedXml {
#endif

#if (WSE1 || WSE2)
		private SignedXmlSignature signature;

		public SignedXml () 
		{
			signature = new SignedXmlSignature ();
			signature.SignedInfo = new SignedInfo ();
		}
#else
		private Signature signature;

		public SignedXml () 
		{
			signature = new Signature ();
			signature.SignedInfo = new SignedInfo ();
		}
#endif
		private AsymmetricAlgorithm key;
		private string keyName;
		private XmlDocument envdoc;

		public SignedXml (XmlDocument document) : this ()
		{
			envdoc = document;
		}

		public SignedXml (XmlElement elem) : this ()
		{
			if (elem == null)
				throw new ArgumentNullException ("elem");
		}

		public const string XmlDsigCanonicalizationUrl = "http://www.w3.org/TR/2001/REC-xml-c14n-20010315";
		public const string XmlDsigCanonicalizationWithCommentsUrl = XmlDsigCanonicalizationUrl + "#WithComments";
		public const string XmlDsigNamespaceUrl = "http://www.w3.org/2000/09/xmldsig#";
		public const string XmlDsigDSAUrl = XmlDsigNamespaceUrl + "dsa-sha1";
		public const string XmlDsigHMACSHA1Url = XmlDsigNamespaceUrl + "hmac-sha1";
		public const string XmlDsigMinimalCanonicalizationUrl = XmlDsigNamespaceUrl + "minimal";
		public const string XmlDsigRSASHA1Url = XmlDsigNamespaceUrl + "rsa-sha1";
		public const string XmlDsigSHA1Url = XmlDsigNamespaceUrl + "sha1";

		public SSCX.KeyInfo KeyInfo {
			get { return signature.KeyInfo; }
			set { signature.KeyInfo = value; }
		}

#if (WSE1 || WSE2)
		public SignedXmlSignature Signature {
			get { return signature; }
		}
#else
		public Signature Signature {
			get { return signature; }
		}
#endif
		public string SignatureLength {
			get { return signature.SignedInfo.SignatureLength; }
		}

		public string SignatureMethod {
			get { return signature.SignedInfo.SignatureMethod; }
		}

		public byte[] SignatureValue {
			get { return signature.SignatureValue; }
		}

		public SignedInfo SignedInfo {
			get { return signature.SignedInfo; }
		}

		public AsymmetricAlgorithm SigningKey {
			get { return key; }
			set { key = value; }
		}

		public string SigningKeyName {
			get { return keyName; }
			set { keyName = value; }
		}

		public void AddObject (SSCX.DataObject dataObject) 
		{
			signature.AddObject (dataObject);
		}

		public void AddReference (Reference reference) 
		{
			signature.SignedInfo.AddReference (reference);
		}

		private Stream ApplyTransform (SSCX.Transform t, XmlDocument doc) 
		{
			t.LoadInput (doc);
			if (t is SSCX.XmlDsigEnvelopedSignatureTransform) {
				XmlDocument d = (XmlDocument) t.GetOutput ();
				MemoryStream ms = new MemoryStream ();
				d.Save (ms);
				return ms;
			}
			else
				return (Stream) t.GetOutput ();
		}

		private Stream ApplyTransform (SSCX.Transform t, Stream s) 
		{
			try {
				t.LoadInput (s);
				s = (Stream) t.GetOutput ();
			}
			catch (Exception e) {
				string temp = e.ToString (); // stop debugger
			}
			return s;
		}

		[MonoTODO("incomplete")]
		private byte[] GetReferenceHash (Reference r) 
		{
			XmlDocument doc = new XmlDocument ();
			doc.PreserveWhitespace = true;
			if (r.Uri == "")
				doc = envdoc;
			else {
				foreach (SSCX.DataObject obj in signature.ObjectList) {
					if ("#" + obj.Id == r.Uri) {
						doc.LoadXml (obj.GetXml ().OuterXml);
						break;
					}
				}
			}

			Stream s = null;
			if (r.TransformChain.Count > 0) {		
				foreach (SSCX.Transform t in r.TransformChain) {
					if (s == null)
						s = ApplyTransform (t, doc);
					else
						s = ApplyTransform (t, s);
				}
			}
			else
				s = ApplyTransform (new SSCX.XmlDsigC14NTransform (), doc);

			// TODO: We should reuse the same hash object (when possible)
			HashAlgorithm hash = (HashAlgorithm) CryptoConfig.CreateFromName (r.DigestMethod);
			return hash.ComputeHash (s);
		}

		private void DigestReferences () 
		{
			// we must tell each reference which hash algorithm to use 
			// before asking for the SignedInfo XML !
			foreach (Reference r in signature.SignedInfo.References) {
				// assume SHA-1 if nothing is specified
				if (r.DigestMethod == null)
					r.DigestMethod = XmlDsigSHA1Url;
				r.DigestValue = GetReferenceHash (r);
			}
		}
		
		private Stream SignedInfoTransformed () 
		{
			SSCX.Transform t = (SSCX.Transform) CryptoConfig.CreateFromName (signature.SignedInfo.CanonicalizationMethod);
			if (t == null)
				return null;

			XmlDocument doc = new XmlDocument ();
			doc.LoadXml (signature.SignedInfo.GetXml ().OuterXml);
			return ApplyTransform (t, doc); 
		}

		private byte[] Hash (string hashAlgorithm) 
		{
			HashAlgorithm hash = HashAlgorithm.Create (hashAlgorithm);
			// get the hash of the C14N SignedInfo element
			return hash.ComputeHash (SignedInfoTransformed ());
		}

		public virtual bool CheckSignature () 
		{
			// CryptographicException
			if (key == null)
				key = GetPublicKey ();
			return CheckSignature (key);
		}

		private bool CheckReferenceIntegrity () 
		{
			// check digest (hash) for every reference
			foreach (Reference r in signature.SignedInfo.References) {
				// stop at first broken reference
				if (! Compare (r.DigestValue, GetReferenceHash (r)))
					return false;
			}
			return true;
		}

		public bool CheckSignature (AsymmetricAlgorithm key) 
		{
			if (key == null)
				throw new ArgumentNullException ("key");

			// Part 1: Are all references digest valid ?
			bool result = CheckReferenceIntegrity ();
			if (result) {
				// Part 2: Is the signature (over SignedInfo) valid ?
				SignatureDescription sd = (SignatureDescription) CryptoConfig.CreateFromName (signature.SignedInfo.SignatureMethod);

				byte[] hash = Hash (sd.DigestAlgorithm);
				AsymmetricSignatureDeformatter verifier = (AsymmetricSignatureDeformatter) CryptoConfig.CreateFromName (sd.DeformatterAlgorithm);

				if (verifier != null) {
					verifier.SetHashAlgorithm (sd.DigestAlgorithm);
					result = verifier.VerifySignature (hash, signature.SignatureValue); 
				}
				else
					result = false;
			}

			return result;
		}

		private bool Compare (byte[] expected, byte[] actual) 
		{
			bool result = ((expected != null) && (actual != null));
			if (result) {
				int l = expected.Length;
				result = (l == actual.Length);
				if (result) {
					for (int i=0; i < l; i++) {
						if (expected[i] != actual[i])
							return false;
					}
				}
			}
			return result;
		}

		public bool CheckSignature (KeyedHashAlgorithm macAlg) 
		{
			if (macAlg == null)
				throw new ArgumentNullException ("macAlg");

			// Part 1: Are all references digest valid ?
			bool result = CheckReferenceIntegrity ();
			if (result) {
				// Part 2: Is the signature (over SignedInfo) valid ?
				byte[] actual = macAlg.ComputeHash (SignedInfoTransformed ());
				result = Compare (signature.SignatureValue, actual);
			}
			return result;
		}

		public bool CheckSignatureReturningKey (out AsymmetricAlgorithm signingKey) 
		{
			// here's the key used for verifying the signature
			if (key == null)
				key = GetPublicKey ();
			signingKey = key;
			// we'll find the key if we haven't already
			return CheckSignature (key);
		}

		public void ComputeSignature () 
		{
			if (key != null) {
				// required before hashing
				signature.SignedInfo.SignatureMethod = key.SignatureAlgorithm;
				DigestReferences ();

				SignatureDescription sd = (SignatureDescription) CryptoConfig.CreateFromName (signature.SignedInfo.SignatureMethod);

				// the hard part - C14Ning the KeyInfo
				byte[] hash = Hash (sd.DigestAlgorithm);
				AsymmetricSignatureFormatter signer = null;

				// in need for a CryptoConfig factory
				if (key is DSA)
					signer = new DSASignatureFormatter (key);
				else if (key is RSA) 
					signer = new RSAPKCS1SignatureFormatter (key);

				if (signer != null) {
					signer.SetHashAlgorithm ("SHA1");
					signature.SignatureValue = signer.CreateSignature (hash);
				}
			}
		}

		public void ComputeSignature (KeyedHashAlgorithm macAlg) 
		{
			if (macAlg == null)
				throw new ArgumentNullException ("macAlg");

			if (macAlg is HMACSHA1) {
				DigestReferences ();

				signature.SignedInfo.SignatureMethod = XmlDsigHMACSHA1Url;
				signature.SignatureValue = macAlg.ComputeHash (SignedInfoTransformed ());
			}
			else 
				throw new CryptographicException ("unsupported algorithm");
		}

		// is that all ?
		public virtual XmlElement GetIdElement (XmlDocument document, string idValue) 
		{
			return document.GetElementById (idValue);
		}

		protected virtual AsymmetricAlgorithm GetPublicKey () 
		{
			AsymmetricAlgorithm key = null;
			if (signature.KeyInfo != null) {
				foreach (SSCX.KeyInfoClause kic in signature.KeyInfo) {
					if (kic is SSCX.DSAKeyValue)
						key = DSA.Create ();
					else if (kic is SSCX.RSAKeyValue) 
						key = RSA.Create ();

					if (key != null) {
						key.FromXmlString (kic.GetXml ().InnerXml);
						break;
					}
				}
			}
			return key;
		}

		public XmlElement GetXml () 
		{
			return signature.GetXml ();
		}

		public void LoadXml (XmlElement value) 
		{
			signature.LoadXml (value);
		}

#if (WSE1 || WSE2)
		[MonoTODO]
		public virtual XmlElement GetXml (XmlDocument document) 
		{
			return null;
		}
#endif

#if ! NET_1_0
		private XmlResolver xmlResolver;

		[MonoTODO("property not (yet) used in class")]
		[ComVisible(false)]
		XmlResolver Resolver {
			set { xmlResolver = value; }
		}
#endif
	}
}
