using System;
using System.Text;
using System.Diagnostics;
using System.IO;
using Sc14n;
using Pki = CryptoSysPKI;

/*  
 * $Id: TestSc14nPki.cs $
 * Last updated:
 *   $Date: 2019-12-13 17:51 $
 *   $Version: 2.1.0 $
 */
/* Some tests using the SC14N .NET interface with CryptoSys PKI.
 * 
 * Requires `Sc14n` and `CryptoSys PKI` to be installed on your system,
 * Available from <http://cryptosys.net/sc14n/> and <http://cryptosys.net/pki>,
 * repectively. 
 * Add references to .NET libraries `diSc14nNet.dll` and `diCrSysPKINet.dll`.
 * Note we've used "Pki" as an alias for "CryptoSysPKI" to save typing.
 * 
 * Test files, e.g. `olamundo.xml`, are in `sc14n-testfiles.zip`. These must be in the CWD.
 * 
 * This is a Console Application written for target .NET Framework 2.0 and above 
 * Please report any bugs to <http://cryptosys.net/contact/>
 */
/******************************* LICENSE ***********************************
 * Copyright (C) 2017-19 David Ireland, DI Management Services Pty Limited.
 * All rights reserved. <https://di-mgt.com.au> <https://cryptosys.net>
 * The code in this module is licensed under the terms of the MIT license.  
 * For a copy, see <http://opensource.org/licenses/MIT>
****************************************************************************
*/

namespace TestSc14nPKI
{
    class TestSc14nPKI
    {
        static void Main(string[] args)
        {
            // If either of these fail, the package is not installed properly...
            Console.WriteLine("Sc14n Version={0}", Sc14n.Gen.Version());
            Console.WriteLine("CrPKI Version={0}", Pki.General.Version());

            string fname, oname;
            int n;
            bool isLatin1;

            // With .NET we need to "know" the encoding of the input data.

            // Input XML is ISO-8859-1 encoded (aka Latin-1)
            fname = "olamundo-base.xml";
            oname = "olamundo-new-signed.xml";
            isLatin1 = true;
            Console.WriteLine("FILE: {0}", fname);
            n = MakeSignedXml(oname, fname, myPriKey, myPassword, isLatin1);
            Console.WriteLine("MakeSignedXml->'{0}' returns {1} (expecting 0)", oname, n);
            Debug.Assert(0 == n);
            
            // Input XML contains Chinese characters UTF-8-encoded
            fname = "daiwei-base.xml";
            oname = "daiwei-new-signed.xml";
            isLatin1 = false;
            Console.WriteLine("FILE: {0}", fname);
            n = MakeSignedXml(oname, fname, myPriKey, myPassword, isLatin1);
            Console.WriteLine("MakeSignedXml->'{0}' returns {1} (expecting 0)", oname, n);
            Debug.Assert(0 == n);

            // Input XML contains Chinese characters as character entities
            // Note that digest value and signature value should be identical to previous one
            fname = "daiwei-ents-base.xml";
            oname = "daiwei-ents-new-signed.xml";
            isLatin1 = false;
            Console.WriteLine("FILE: {0}", fname);
            n = MakeSignedXml(oname, fname, myPriKey, myPassword, isLatin1);
            Console.WriteLine("MakeSignedXml->'{0}' returns {1} (expecting 0)", oname, n);
            Debug.Assert(0 == n);

            Console.WriteLine("\nALL DONE.");
        }

        /// <summary>
        /// Create a XML-DSIG signed file given proforma XML document
        /// </summary>
        /// <param name="outFile">Name of outfile to create</param>
        /// <param name="baseFile">Name of input XML document</param>
        /// <param name="priKey">PKCS8 encrypted private key file or PEM-string</param>
        /// <param name="password">Password for private key</param>
        /// <param name="isLatin1">Set true if file is known to be Latin-1 encoded (ISO-8859-1) or false if UTF-8 or US-ASCII</param>
        /// <returns>Zero (0) on success otherwise nonzero error code (an integer cast of <see cref="MSXerror"/> enum)</returns>
        /// <remarks>Input XML document is expected to be enveloped-signature with single reference URI="",
        /// C14N method REC-xml-c14n-20010315, signature method xmldsig#rsa-sha1, and digest method xmldsig#sha1.
        /// KeyValue is expected to be in RSAKeyValue form.
        /// Items to be replaced should be marked "%digval%", "%sigval%" and "%keyval%".
        /// </remarks>
        public static int MakeSignedXml(string outFile, string baseFile, string priKey, string password, bool isLatin1)
        {
            byte[] b, dataIn, dataOut;
            string s, xmlStr, newStr;
            string digval, digval_si, sigval, keyval;
            int status;

            // Compute digest value of body excluding <Signature> element
            // (this assumes Reference URI="" and DigestMethod is SHA-1)
            digval = C14n.ToDigest(baseFile, "Signature", Tran.OmitByTag, DigAlg.Sha1);
            Debug.WriteLine("DIGVAL={0}", digval);
            if (digval.Length == 0) {
                return (int)MSXerror.TransformExclSignatureFailed;
            }

            // Extract the SignedInfo element into memory
            // Note %digval% parameter to be completed
            b = C14n.ToBytes(baseFile, "SignedInfo", Tran.SubsetByTag);
            if (b.Length == 0) {
                return (int)MSXerror.TransformSignedInfoFailed;
            }
            Debug.WriteLine("SIGNEDINFO (BASE):");
            Debug.WriteLine(System.Text.Encoding.UTF8.GetString(b));

            // Insert the required DigestValue we prepared earlier
            // Note the SignedInfo element is *always* US-ASCII encoded,
            // so we can safely use the more convenient String.Replace function
            s = System.Text.Encoding.UTF8.GetString(b).Replace("%digval%", digval);
            Debug.WriteLine("SIGNEDINFO (COMPLETED):");
            Debug.WriteLine(s);
            // Now compute the digest value of this string
            digval_si = C14n.ToDigest(System.Text.Encoding.UTF8.GetBytes(s), DigAlg.Sha1);
            Debug.WriteLine("SHA1(signedinfo)= {0}", digval_si);

            // Compute signature value from this digest value
            sigval = SigValFromDigVal(digval_si, priKey, myPassword);
            Debug.WriteLine("SIG= {0}", sigval);

            // Get the RSA Key Value in required XML form
            keyval = KeyValFromCert(priKey);

            // Now compose the output file by substituting the correct values
            // (Note we make no other checks of the input XML - that's up to you)

            // Read in base XML file as a byte array
            dataIn = ReadABinaryFile(baseFile);
            if (dataIn.Length == 0) return (int)MSXerror.ReadFileFailed;
            // Convert to a string so we can use String.Replace
            // We need to know the encoding
            if (isLatin1)
                xmlStr = System.Text.Encoding.GetEncoding("ISO-8859-1").GetString(dataIn);
            else
                xmlStr = System.Text.Encoding.UTF8.GetString(dataIn);
            Debug.WriteLine(xmlStr);

            newStr = xmlStr.Replace("%digval%", digval).Replace("%sigval%", sigval).Replace("keyval", keyval);

            // Convert back to bytes then write out file
            if (isLatin1)
                dataOut = System.Text.Encoding.GetEncoding("ISO-8859-1").GetBytes(newStr);
            else
                dataOut = System.Text.Encoding.UTF8.GetBytes(newStr);

            status = (WriteABinaryFile(outFile, dataOut) ? 0 : (int)MSXerror.WriteFileFailed);
            return status;
        }

        /// <summary>
        /// Error codes for MakeSignedXml
        /// </summary>
        public enum MSXerror
        {
            OkSuccess = 0,
            WriteFileFailed,
            ReadFileFailed,
            TransformExclSignatureFailed,
            TransformSignedInfoFailed,
        }

        //**********************
        // PKI HELPER FUNCTIONS
        //**********************

        /// <summary>
        /// Compute the signature value from digest value.
        /// </summary>
        /// <param name="digval">Base64-encoded digest value of data to be signed</param>
        /// <param name="priKey">PKCS8 encrypted private key file or PEM-string</param>
        /// <param name="password">Password for private key</param>
        /// <returns>Base64-encoded signature value or empty string on error</returns>
        public static string SigValFromDigVal(string digval, string priKey, string password)
        {
            string sigval = Pki.Sig.SignDigest(Pki.Cnv.FromBase64(digval), priKey, password, Pki.SigAlgorithm.Rsa_Sha1);
            return sigval;
        }

        /// <summary>
        /// Extract XML-style RSAKeyValue from X.509 certificate.
        /// </summary>
        /// <param name="cert">X.509 certificate file or PEM string</param>
        /// <returns>RSAKeyValue as a string or empty string on error</returns>
        public static string KeyValFromCert(string cert)
        {
            string keyval = Pki.Rsa.ToXMLString(Pki.Rsa.ReadPublicKey(cert).ToString(), 0);
            return keyval;
        }

        /// <summary>
        /// Extract XML-style RSAKeyValue from RSA private key.
        /// </summary>
        /// <param name="priKey">PKCS8 encrypted private key file or PEM-string</param>
        /// <param name="password">Password for private key</param>
        /// <returns>RSAKeyValue as a string or empty string on error</returns>
        public static string KeyValFromPriKey(string priKey, string password)
        {
            // CAUTION: make sure you exclude the private key parameters here
            string keyval = Pki.Rsa.ToXMLString(Pki.Rsa.ReadPrivateKey(priKey, password).ToString(), Pki.Rsa.XmlOptions.ExcludePrivateParams);
            return keyval;
        }
        
        /// <summary>
        /// Return true if private key and certificate are matched.
        /// </summary>
        /// <param name="priKey">PKCS8 encrypted private key file or PEM-string</param>
        /// <param name="password">Password for private key</param>
        /// <param name="cert">X.509 certificate file or PEM string</param>
        /// <returns>true if private key and certificate are matched or false if not</returns>
        public static bool IsKeyAndCertMatch(string priKey, string password, string cert)
        {
            int n = Pki.Rsa.KeyMatch(Pki.Rsa.ReadPrivateKey(priKey, password), Pki.Rsa.ReadPublicKey(cert));
            return (0 == n);
        }

        // HARD-CODED PRIVATE KEY AND CERTIFICATE (FOR OUR CONVENIENCE IN TESTING)
        // Alice's PKCS8 encrypted key and X.509 certificate
        // from RFC 4134 "Examples of S/MIME Messages"
        // Private key password is "password"
        private const string myPassword = "password";    // High security practice here!!
        private const string myPriKey = @"-----BEGIN ENCRYPTED PRIVATE KEY-----
            MIICojAcBgoqhkiG9w0BDAEDMA4ECFleZ90vhGrRAgIEAASCAoA9rti16XVH
            K4AJVe1CNf61NIpIogu/Xs4Yn4hXflvewiOwe6/9FkxBXLbhKdbQWn1Z4p3C
            njVns2VYEO/qpJR3LciHMwp5dsqedUVVia//CqFHtEV9WfvCKWgmlkkT1YEm
            1aChZnPP5i6IhwVT9qvFluTZhvVmjW0YyF86OrOp0uxxVic7phPbnPrOMelf
            ZPc3A3EGpzDPkxN+o0obw87tUgCL+s0KtUOr3c6Si4KQ3IQjrjZxQF4Se3t/
            4PEpqUl5EpYiCx9q5uqb0Lr1kWiiQ5/inZm5ETc+qO+ENcp0KjnX523CATYd
            U5iOjl/X9XZeJrMpOCXogEuhmLPRauYP1HEWnAY/hLW93v10QJXY6ALlbkL0
            sd5WU8Ces7T04b/p4/12yxqYqV68QePyfHpegdraDq3vRfopSwrUxtL9cisP
            jsQcJ5FL/SfloFbmld4CKIjMsromsEWqo6rfo3JqNizgTVIIWExy3jDT9VvK
            d9ADH0g3JCbuFzaWVOZMmZ0wlo28PKkLQ8FkW8CG/Lq/Q/bHLPM+sPdLN+ke
            gpA6fvL4wpku4ST7hmeN1vWbRLlCfuFijux77hdM7knO9/MawICsA4XdzR78
            p0C2hJlc6p46IWZaINQXGstTbJMh+mJ7i1lrbG2kvZ2Twf9R+RaLp2mPHjb1
            +P+3f2L3tOoC31oJ18u/L1MXEWxLEZHB0+ANg+N/0/icwImcI0D+wVN2puU4
            m58j81sGZUEAB3aFEbPxoX3y+qYlOnt1OfdY7WnNdyr9ZzI09fkrTvujF4LU
            nycqE+MXerf0PxkNu1qv9bQvCoH8x3J2EVdMxPBtH1Fb7SbE66cNyh//qzZo
            B9Je
            -----END ENCRYPTED PRIVATE KEY-----";
        private const string myCert = @"-----BEGIN CERTIFICATE-----
            MIICLDCCAZWgAwIBAgIQRjRrx4AAVrwR024uxBCzsDANBgkqhkiG9w0BAQUFADAS
            MRAwDgYDVQQDEwdDYXJsUlNBMB4XDTk5MDkxOTAxMDg0N1oXDTM5MTIzMTIzNTk1
            OVowEzERMA8GA1UEAxMIQWxpY2VSU0EwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJ
            AoGBAOCJczmN2PX16Id2OX9OsAW7U4PeD7er3H3HdSkNBS5tEt+mhibU0m+qWCn8
            l+z6glEPMIC+sVCeRkTxLLvYMs/GaG8H2bBgrL7uNAlqE/X3BQWT3166NVbZYf8Z
            f8mB5vhs6odAcO+sbSx0ny36VTq5mXcCpkhSjE7zVzhXdFdfAgMBAAGjgYEwfzAM
            BgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIGwDAfBgNVHSMEGDAWgBTp4JAnrHgg
            eprTTPJCN04irp44uzAdBgNVHQ4EFgQUd9K00bdMioqjzkWdzuw8oDrj/1AwHwYD
            VR0RBBgwFoEUQWxpY2VSU0FAZXhhbXBsZS5jb20wDQYJKoZIhvcNAQEFBQADgYEA
            PnBHqEjME1iPylFxa042GF0EfoCxjU3MyqOPzH1WyLzPbrMcWakgqgWBqE4lradw
            FHUv9ceb0Q7pY9Jkt8ZmbnMhVN/0uiVdfUnTlGsiNnRzuErsL2Tt0z3Sp0LF6DeK
            tNufZ+S9n/n+dO/q+e5jatg/SyUJtdgadq7rm9tJsCI=
            -----END CERTIFICATE-----";

        //*****************
        // FILE UTILITIES *
        //*****************    	
        static byte[] ReadABinaryFile(string fileName)
		{
			byte[] b = new byte[0];
			FileInfo finfo = new FileInfo(fileName);
			if (finfo.Exists)
			{
				FileStream fsi = finfo.OpenRead();
				BinaryReader br = new BinaryReader(fsi);
				int count = (int)fsi.Length;
				b = br.ReadBytes(count);
				br.Close();
				fsi.Close();
			}
			Debug.Assert(finfo.Exists, "File '" + fileName + "' does not exist.");
			return b;
		}

        static bool WriteABinaryFile(string fileName, byte[] data)
        {
            FileStream fs;
            BinaryWriter bw;
            fs = new FileStream(fileName, FileMode.Create, FileAccess.Write);
            bw = new BinaryWriter(fs);
            bw.Write(data);
            bw.Close();
            fs.Close();
            return true;
        }

    }
}