/*  $Id: SignEnvelopedSignature.cs $ 
 *   Last updated:
 *   $Date: 2022-01-29 15:43:00 $
 *   $Version: 0.9.1 $
 */

/******************************* LICENSE ***********************************
 * Copyright (C) 2022 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>
****************************************************************************
*/

using System;
using System.Text;
using System.Text.RegularExpressions;
using System.IO;
using System.Diagnostics;

/*
 * Requires DI Management CryptoSys Libraries available from <https://cryptosys.net/>.
 * 1. CryptoSys PKI Pro: https://cryptosys.net/pki/
 * 2. SC14N, a straightforward XML canonicalization utility: https://cryptosys.net/sc14n/
 * EITHER add references to
 * `diCrSysPKINet.dll` (installed by default in `C:\Program Files (x86)\CryptoSysPKI\DotNet`)
 * `diSc14nNet.dll` (installed by default in `C:\Program Files (x86)\Sc14n\DotNet`)
 * OR add the C# source code files `CryptoSysPKI.cs` and `diSc14nNet.cs` 
 * directly to your project.
 */
using Pki = CryptoSysPKI;
using Sc14n;

namespace DIManagement.SignEnvelopedSignature
{
    class SignEnvelopedSignature
    {
        static void Main(string[] args)
        {
            string signedDoc;
            
            // Setup to display debugging info in console in Debug mode
            // (Should be ignored in final Release mode)
            TextWriterTraceListener[] listeners = new TextWriterTraceListener[] {
                new TextWriterTraceListener(Console.Out)
            };
            Debug.Listeners.AddRange(listeners);

            // Debug to check libraries are available and where
            Debug.WriteLine("PKI Version=" + Pki.General.Version() + " " + Pki.General.ModuleName() + " [" + Pki.General.CompileTime() + "]");
            Debug.WriteLine("SC14N Version=" + Sc14n.Gen.Version() + " " + Sc14n.Gen.ModuleName() + " [" + Sc14n.Gen.CompileTime() + "]");

            // Require minimum PKI
            if (Pki.General.Version() < 200000) {
                Console.WriteLine("PKI version must be 20.0.0 or above");
                return;
            }

            // Show current working directory
            Debug.WriteLine("CWD=" + System.IO.Directory.GetCurrentDirectory());

            // Do the business...
            signedDoc = SignDoc("cancelacion-2022-signed.xml", "cancelacion-2022-base.xml", "emisor2021.cer", "emisor.key", password:"12345678a", noFlatten:false);
            // Returns either the name of the file just created or an error message beginning with "**ERROR"
            if (signedDoc.IndexOf("**ERROR") >= 0) {
                Console.WriteLine(signedDoc);
            } else {
                Console.WriteLine("Created file '{0}'", signedDoc);
            }
        }

        /// <summary>
        /// Sign an enveloped-signature XML document
        /// </summary>
        /// <param name="outputxmlFile">Name of signed output file to be created</param>
        /// <param name="baseXmlFile">Path to base XML document.</param>
        /// <param name="certFile">Filename of X.509 certificate file, or certificate-as-a-string</param>
        /// <param name="keyFile">Filename of matching private key (or PEM string)</param>
        /// <param name="password">Password for encrypted private key ("" if not encrypted)</param>
        /// <param name="noFlatten">Set as <c>true</c> to leave XML in original format (default=flatten to a single line)</param>
        /// <returns>String containing the name of the output document or an error message beginning "***ERROR:"</returns>
        /// <remarks>
        /// The base XML document is expected to be a well-formed XML document with placeholders of the form <c>@!...!@</c>.
        /// This code assumes specific hard-coded values are set in the Signature template.
        /// The Reference must have <c>URI=""</c>, the Transform algorithm must be "http://www.w3.org/2000/09/xmldsig#enveloped-signature".
        /// The SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1",
        /// the DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1", and
        /// the CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315".
        /// The placeholders <c>@!DIGVAL!@</c> and <c>@!SIGVAL!@</c> are mandatory. 
        /// All other placeholders are optional and will be ignored if not present.
        /// </remarks>
        static string SignDoc(string outputxmlFile, string baseXmlFile, string certFile, string keyFile, string password, bool noFlatten = false)
        {
            // DEFAULT ALGORITHMS
            // These match the algorithms hard-coded into the Signature template
            // (change the template, remember to change these)
            const Pki.SigAlgorithm sigAlg = Pki.SigAlgorithm.Rsa_Sha1;
            const Sc14n.DigAlg digAlg = Sc14n.DigAlg.Sha1;
            const Sc14n.TranMethod tranMethod = Sc14n.TranMethod.Inclusive;

            string xmlStr;
            string strMsg;
            byte[] b;
            StringBuilder sbPriKey;
            string pubKey, certStr;
            string s, sidigval;

            // Read in base XML document and flatten it, if necessary
            xmlStr = File.ReadAllText(baseXmlFile);
            if (!noFlatten)
                xmlStr = FlattenXml(xmlStr);

            // Read in private key, cert as a string, and public key
            sbPriKey = Pki.Rsa.ReadPrivateKey(keyFile, password);
            if (sbPriKey.Length == 0) {
                strMsg = String.Format("**ERROR: Failed to read private key file '{0}'", keyFile);
                return strMsg;
            }
            certStr = Pki.X509.ReadStringFromFile(certFile);
            if (certStr.Length == 0) {
                strMsg = String.Format("**ERROR: Failed to read X.509 certificate in '{0}'", certFile);
                return strMsg;
            }
            // We will use the cert string from now on
            pubKey = Pki.Rsa.ReadPublicKey(certStr).ToString();
            if (pubKey.Length == 0) {
                strMsg = String.Format("**ERROR: Could not extract public keyFile from certificate");
                return strMsg;
            }

            // Check private and public keys match
            if (Pki.Rsa.KeyMatch(sbPriKey.ToString(), pubKey) != 0) {
                return ("***ERROR: private key does not match public key in certificate");
            }

            // Set Timestamp
            // NOTE: if @!TIMESTAMP!@" placeholder does not exist, this has no effect, 
            // so you can hardcode an fixed timestamp in the baseXML if you wish
            s = IsoTime_Local();
            Debug.WriteLine("SigningTime=" + s);
            xmlStr = xmlStr.Replace("@!TIMESTAMP!@", s);

            // Fill in the KeyInfo values, if they exist. 
            s = certStr;
            xmlStr = xmlStr.Replace("@!CERTIFICATE!@", s);
            s = Pki.X509.QueryCert(certStr, "serialNumber", Pki.X509.OutputOpts.Decimal);
            xmlStr = xmlStr.Replace("@!SERIALNUMBER!@", s);
            s = Pki.X509.QueryCert(certStr, "issuerName", Pki.X509.OutputOpts.Ldap);
            xmlStr = xmlStr.Replace("@!ISSUERNAME!@", s);
            s = Pki.Rsa.ToXMLString(pubKey, 0);
            xmlStr = xmlStr.Replace("@!RSAKEYVALUE!@", s);

            // Get XML as a byte array
            // Assumes base file is UTF-8 encoded
            b = System.Text.Encoding.UTF8.GetBytes(xmlStr);

            // Compute the digest value over the canonicalized data excluding the Signature element
            s = Sc14n.C14n.ToDigest(b, "Signature", Sc14n.Tran.OmitByTag, digAlg);
            Debug.WriteLine("DigestValue=" + s);
            // Set the DigestValue in the original string
            xmlStr = xmlStr.Replace("@!DIGVAL!@", s);

            // Now back to a temp byte array to perform C14N on SignedInfo
            b = System.Text.Encoding.UTF8.GetBytes(xmlStr);
            // Compute digest of SignedInfo
            sidigval = Sc14n.C14n.ToDigest(b, "SignedInfo", Tran.SubsetByTag, digAlg, tranMethod);
            Debug.WriteLine("SignedInfo Digest=" + sidigval);
            if (sidigval.Length == 0) {
                strMsg = String.Format("**ERROR: Failed to compute C14N digest value: '{0}'", Sc14n.Err.LastError());
                return strMsg;
            }

            // Compute signature value and insert in Signature string
            s = Pki.Sig.SignDigest(Pki.Cnv.FromBase64(sidigval), sbPriKey.ToString(), "", sigAlg);

            // Clean up private key
            Pki.Wipe.String(sbPriKey);

            Debug.WriteLine(String.Format("SignatureValue=" + s));
            if (s.Length == 0) {
                strMsg = String.Format("**ERROR: Failed to compute signature: '{0}'", Pki.General.LastError());
                return strMsg;
            }
            xmlStr = xmlStr.Replace("@!SIGVAL!@", s);

            // Final check for uncompleted '@!..!@'
            int nret = xmlStr.IndexOf("@!");
            if (nret >= 0) {
                Debug.WriteLine("WARNING: uncompleted '@!..!@' items");
            }

            // Output XML should now be complete
            // Final check (Note convert string to UTF-8 bytes before passing)
            s = Sc14n.C14n.ToDigest(Encoding.UTF8.GetBytes(xmlStr), "", 0, 0, 0);
            if (s.Length == 0) {
                strMsg = String.Format("**ERROR: XML internal problem: '{0}'", Sc14n.Err.LastError());
                return strMsg;
            }

            Debug.WriteLine("FINAL XML:");
            Debug.WriteLine(xmlStr);
            // Write final document string to output file
            File.WriteAllText(outputxmlFile, xmlStr);

            // Return full path name of output file created
            FileInfo fi = new FileInfo(outputxmlFile);
            return fi.FullName;
        }

        static string IsoTime_Local()
        {
            // Compute current signing time in <xs:datetime> form
            return DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss");
        }

        /// <summary>
        /// Flatten XML data in a string
        /// </summary>
        /// <param name="s">String containing XML data</param>
        /// <returns>XML data with no whitespace between elements</returns>
        static string FlattenXml(string s)
        {
            s = Regex.Replace(s, @"^\s+", "", RegexOptions.Multiline);
            s = Regex.Replace(s, @"\s+$", "", RegexOptions.Multiline);
            s = s.Replace("\r\n", " ").Replace("\n", " ");
            s = Regex.Replace(s, @">\s+<", @"><");
            return s;
        }
    }

}