/*  $Id: SignSigId.cs $ 
 *   Last updated:
 *   $Date: 2022-02-13 07:57:00 $
 *   $Version: 1.0.0 $
 */

/******************************* 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.SignSigId
{
    class SignSigId
    {
        static void Main(string[] args)
        {
            // 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);

            // Check required libraries
            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;
            }

            string signedDoc = SignSigIdDoc("sig-id2-signed.xml", "sig-id2-base.xml", "AlicePrivRSASign.p8e", "password");
            // 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 a XML-DSIG document with an ID reference.
        /// </summary>
        /// <param name="outputXmlFile">Name of signed output file to be created</param>
        /// <param name="baseXmlFile">Path to base XML document</param>
        /// <param name="keyFile">Filename of private key to be used for signing (or PEM string)</param>
        /// <param name="password">Password for encrypted private key ("" if not encrypted)</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 for algorithms in the Signature template.
        /// </remarks>
        static string SignSigIdDoc(string outputXmlFile, string baseXmlFile, string keyFile, string password)
        {
            // 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_Sha256;  // http://www.w3.org/2001/04/xmldsig-more#rsa-sha256
            const Sc14n.DigAlg sigDigAlg = Sc14n.DigAlg.Sha256;   // To match digest alg in signature alg
            const Sc14n.DigAlg digAlg = Sc14n.DigAlg.Sha256;  // http://www.w3.org/2001/04/xmlenc#sha256
            const Sc14n.TranMethod tranMethod = Sc14n.TranMethod.Inclusive; // http://www.w3.org/TR/2001/REC-xml-c14n-20010315

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

            // Read in base XML document
            xmlStr = File.ReadAllText(baseXmlFile);

            // Read in private 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;
            }

            // 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 the element with ID attribute value ID=MyInvoice
            s = Sc14n.C14n.ToDigest(b, "ID=MyInvoice", Sc14n.Tran.SubsetById, digAlg, tranMethod);
            Debug.WriteLine("DigestValue=" + s);
            // Set the DigestValue in the original string
            xmlStr = xmlStr.Replace("@!DIGVAL!@", s);

            // Compute digest of (completed) SignedInfo
            sidigval = Sc14n.C14n.ToDigest(Encoding.UTF8.GetBytes(xmlStr), "ds:SignedInfo", Tran.SubsetByTag, sigDigAlg, tranMethod);
            Debug.WriteLine("ds: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;
            }

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

}