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

/*  
 *   $Id: TestSc14nPKI.cs $
 *   Last updated:
 *   $Date: 2017-07-09 05:11 $
 *   $Version: 0.9.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://di-mgt.com.au/sc14n/> and <http://www.cryptosys.net/pki>,
 * repectively. 
 * Add a references to `diSc14nNet.dll` and `diCrSysPKINet`.
 * Note we've used "Pki" as an alias for "CryptoSysPKI" to save typing.
 * 
 * This is a Console Application written for target .NET Framework 2.0 and above 
 * Please report any bugs to <http://www.di-mgt.com.au/contact>
 */
/******************************* LICENSE ***********************************
 * Copyright (C) 2017 David Ireland, DI Management Services Pty Limited.
 * All rights reserved. <www.di-mgt.com.au> <www.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;

            // 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, myCert, isLatin1);
            Console.WriteLine("MakeSignedXml->'{0}' returns {1} (expecting 0)", oname, 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, myCert, isLatin1);
            Console.WriteLine("MakeSignedXml->'{0}' returns {1} (expecting 0)", oname, 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.xml";
            oname = "daiwei-ents-new-signed.xml";
            isLatin1 = false;
            Console.WriteLine("FILE: {0}", fname);
            n = MakeSignedXml(oname, fname, myPriKey, myPassword, myCert, isLatin1);
            Console.WriteLine("MakeSignedXml->'{0}' returns {1} (expecting 0)", oname, 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="cert">X.509 certificate file or PEM-string</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</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, string cert, bool isLatin1)
        {
            byte[] b, dataIn, dataOut;
            string s, xmlStr, newStr;
            string digval, digval_si, sigval, keyval;
            int status;

            // First, make sure the private key and certificate actually match
            if (!IsKeyAndCertMatch(priKey, password, cert)) {
                return -100;
            }

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

            // Extract the SignedInfo element into memory
            // Note %digval% parameter to be completed
            b = C14n.ToBytes(baseFile, "SignedInfo", Tran.SubsetByTag);
            Debug.Assert(b.Length > 0, "C14n.ToBytes failed");
            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, myPriKey, myPassword);
            Debug.WriteLine("SIG= {0}", sigval);

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

            // 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 -101;
            // 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 : -102);
            return status;
        }

        /// <summary>
        /// Computes the signature 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;
        }

        public static string KeyValFromCert(string cert)
        {
            string keyval = Pki.Rsa.ToXMLString(Pki.Rsa.ReadPublicKey(cert).ToString(), 0);
            return keyval;
        }

        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;
        }

    }
}