/*  $Id: VerifyDoc.cs $ 
 *   Last updated:
 *   $Date: 2021-02-06 11:29:00 $
 *   $Version: 1.2.0 $
 */

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

/* Changelog:
 * v1.2.0: 
 * + Fixed problem with <DD> element and &apos;/&quot; entities
 * + Changed checks using .IndexOf from >0 to >=0.
 * v1.1.0: Accept filename in command line.
 */

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.IO;
using System.Diagnostics;
/*
 * DI Management CryptoSys Libraries available from <https://cryptosys.net/>.
 * 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`)
 * `diXmlsqNet.dll` (installed by default in `C:\Program Files (x86)\xmlsq\DotNet`)
 * OR add the C# source code files `CryptoSysPKI.cs`, `diSc14nNet.cs` and `diXmlsqNet.cs` 
 * directly to your project.
 */
using Pki = CryptoSysPKI;
using Sc14n;
using Xmlsq;

namespace DIManagement.VerifyDoc
{
    class Program
    {
        static void Main(string[] args)
        {
            string s;

            // USAGE: if command line contains "--test", then do hard-coded tests
            // Otherwise expecting a list of filenames in command line.

            if (args.Length == 0) {
                Console.WriteLine("Usage: VerifyDoc FILE [FILES...]");
                Console.WriteLine("       VerifyDoc --test");
                return;
            } 
            else if (args.Length > 0 && !args[0].Equals("--test", StringComparison.OrdinalIgnoreCase)) {
                // Process file(s) specified in command line
                foreach (string arg in args) {
                    s = VerifyDoc.VerifySignedEnvio(arg);
                    Console.WriteLine(s);
                    Debug.Assert("OK" == s, "VerifyDoc failed");
                }
            } 
            else {    // Do hard-coded tests
                Console.WriteLine("DOING HARD-CODED TESTS...");
                // SII reference document from 2003
                s = VerifyDoc.VerifySignedEnvio("F60T33-ejemplo.xml");
                Console.WriteLine(s);
                Debug.Assert("OK" == s, "VerifyDoc failed");
                // From user who claims it verified (2017)
                s = VerifyDoc.VerifySignedEnvio("EnvioDTE_OK.xml");
                Console.WriteLine(s);
                Debug.Assert("OK" == s, "VerifyDoc failed");

                // Output from our MakeEnvio program
                s = VerifyDoc.VerifySignedEnvio("output-boleta.xml");
                Console.WriteLine(s);
                Debug.Assert("OK" == s, "VerifyDoc failed");
                s = VerifyDoc.VerifySignedEnvio("output-enviodte.xml");
                Console.WriteLine(s);
                Debug.Assert("OK" == s, "VerifyDoc failed");
            }


            Console.WriteLine("ALL DONE.");
        }
    }
    class VerifyDoc
    {
        // GLOBAL DEFAULT ALGORITHMS
        // These match the algorithms hard-coded into the Signature template
        // (change one, remember to change the other)
        const Pki.HashAlgorithm hashAlg = Pki.HashAlgorithm.Sha1;
        const Pki.SigAlgorithm sigAlg = Pki.SigAlgorithm.Rsa_Sha1;
        const Sc14n.DigAlg digAlg = Sc14n.DigAlg.Sha1;
        const Sc14n.TranMethod tranMethod = Sc14n.TranMethod.Inclusive;

        public static string VerifySignedEnvio(string xmlFile)
        {
            string xmlStr;
            byte[] xmlData;
            int nsigs, ndtes;
            List<string> dtelist = new List<string>();
            string strMsg;

            /* Expected form:
                <EnvioDTE> or <EnvioBOLETA>
                  <SetDTE ID="...">
				    <Caratula>
					(<DTE>)+
                  <Signature>
                </EnvioDTE>
				
				<DTE>
				  <Documento ID="...">
				  <Signature>
				</DTE>
				
				<Documento>
				  ...
				  <TED version="1.0">
    				  <DD>
    				  <FRMT>
  				  </TED>
  				  ...
				</Documento>
             */

            // Read in entire file to byte array - we will use this to compute C14N values (exact byte sequences are important)
            xmlData = File.ReadAllBytes(xmlFile);
            Console.WriteLine("\nFILE: '{0}' {1} bytes", xmlFile, xmlData.Length);

            // Convert this to a Unicode string - we will use this to query/extract string values and do regex operations.
            // (NB explicitly expecting Latin-1-encoded data)
            xmlStr = System.Text.Encoding.GetEncoding("iso-8859-1").GetString(xmlData);

            // Capture each instance of a DTE document, save in a list.
            // NB We have to use a regex for this: Xmlsq does not preserve whitespace needed for C14N.
            string pattern = @"(<DTE.*?</DTE>)";
            //Console.WriteLine(pattern);
            foreach (Match match in Regex.Matches(xmlStr, pattern, RegexOptions.Singleline)) {
                //Console.WriteLine("\n==\n{0}\n==\n", match.Groups[1].Value);
                dtelist.Add(match.Groups[1].Value);
            }
            ndtes = dtelist.Count;
            Console.WriteLine("#DTE docs = {0}", ndtes);

            // If there are N x DTE docs, we expect N+1 Signature elements
            nsigs = Xmlsq.Query.Count(xmlStr, "//Signature");
            Console.WriteLine("#Signatures={0}", nsigs);

            if (nsigs != ndtes + 1 ) {
                strMsg = String.Format("**ERROR: Found {0} DTE documents and {1} Signature elements. If there are N*DTE docs, we expect N+1 Signatures", ndtes, nsigs);
                return strMsg;
            }

            // Verify outer document
            strMsg = VerifySignedDoc(xmlStr, xmlData);
            Console.WriteLine(strMsg);
            if (strMsg.IndexOf("**ERROR") >= 0) {
                return strMsg;
            }

            // Verify Signature in each DTE document
            foreach (string dtedoc in dtelist) {
                // NB no XML encoding declaration for DTE, so encode in UTF-8
                byte[] b = System.Text.Encoding.UTF8.GetBytes(dtedoc);
                strMsg = VerifySignedDoc(dtedoc, b);
                Console.WriteLine(strMsg);
                if (strMsg.IndexOf("**ERROR") >= 0) {
                    return strMsg;
                }
                strMsg = VerifyFRMA(dtedoc);
                Console.WriteLine(strMsg);
                if (strMsg.IndexOf("**ERROR") >= 0) {
                    return strMsg;
                }
            }

            Console.WriteLine("Verified file '{0}'", xmlFile);
            return "OK";
        }

        /// <summary>
        /// Verify a signed XMLDSIG document.
        /// </summary>
        /// <param name="xmlStr">String containing XML data.</param>
        /// <param name="xmlData">Byte array containing same XML data encoded as per declaration encoding.</param>
        /// <returns>"OK" if signature validates or an error message beginning with "**ERROR" if not.</returns>
        /// <remarks>If XML declaration is <c>&lt;?xml version="1.0" encoding="ISO-8859-1"?&gt;</c> 
        /// then <c>xmlData</c> must be encoded in Latin-1, otherwise
        /// if declaration includes <c>encoding="UTF-8"</c> or encoding attribute is absent, 
        /// then <c>xmlData</c> must be encoded in UTF-8.</remarks>
        static string VerifySignedDoc(string xmlStr, byte[] xmlData) {
            int n, nsigs;
            string sig;
            string rsakeyvalue;
            string pubkey;
            string refuri, siref;
            string dig, digval;
            string sigval;
            string strMsg;

            /* Expected form of xmlStr
                <Outer>
                  <Inner ID="#Foo">
                  <Signature>
				</Outer>
				
				<Signature>
				  <SignedInfo>
				    <!--includes exactly one Reference element-->
					<Reference URI="#Foo">
				  </SignedInfo
				</Signature>
            */

            Console.WriteLine("\nVerifying XMLDSIG Signature...");

            /* [XMLDSIG] 3.2.1 Reference Validation. For each Reference in SignedInfo:
              1. Obtain the data object to be digested. 
              2. Digest the resulting data object using the DigestMethod specified in its Reference specification.
              3. Compare the generated digest value against DigestValue in the SignedInfo Reference; if there is any mismatch, validation fails.
            */

            // Extract last Signature element: WARNING reformatted, so no good for C14N'ing
            // but we can extract reference and digest values from it
            sig = Xmlsq.Query.FullQuery(xmlStr, "(//Signature)[last()]");
            //Console.WriteLine(sig);
            if (sig.Length == 0) {
                strMsg = String.Format("**ERROR: Cannot find a Signature element");
                return strMsg;
            }

            // Get the Reference, expected in the form URI="#Foo"
            refuri = Xmlsq.Query.FullQuery(sig, "(//SignedInfo/Reference)[1]/@URI", Query.Opts.Raw);
            Console.WriteLine("[{0}]", refuri);
            if (refuri.Length == 0) {
                strMsg = String.Format("**ERROR: Cannot find expected Reference element of the form URI='#...'");
                return strMsg;
            }
            // URI="#Foo" => ID=Foo
            refuri = refuri.Replace(@"""", "").Replace("#", "").Replace("URI", "ID");
            Console.WriteLine("[{0}]", refuri);

            // Compute the digest of the data object (NB using byte array as input here)
            dig = Sc14n.C14n.ToDigest(xmlData, refuri, Tran.SubsetById, digAlg, tranMethod);
            Console.WriteLine("Computed digest value=" + dig);
            if (dig.Length == 0) {
                strMsg = String.Format("**ERROR: Cannot compute digest value: {0}", Sc14n.Err.LastError());
                return strMsg;
            }

            // Extract the DigestValue from the SignedInfo Reference
            digval = Xmlsq.Query.GetText(sig, "(//SignedInfo/Reference)[1]/DigestValue");
            Console.WriteLine("Reference DigestValue=" + digval);

            // Compare the generated digest value against the DigestValue in the SignedInfo Reference
            if (!string.Equals(dig, digval, StringComparison.CurrentCultureIgnoreCase)) {
                strMsg = String.Format("**ERROR: Signature validation failed");
                return strMsg;
            }

            /* [XMLDSIG] 3.2.2 Signature Validation
                1. Obtain the keying information from KeyInfo.
                2. Confirm the SignatureValue over the SignedInfo element.
            */

            // Extract the RSAKeyValue element from this signature
            rsakeyvalue = Xmlsq.Query.FullQuery(sig, "//RSAKeyValue", Query.Opts.Trim | Query.Opts.Raw);
            //Console.WriteLine(rsakeyvalue);
            if (rsakeyvalue.Length == 0) {
                strMsg = String.Format("**ERROR: Cannot extract an RSAKeyValue element");
                return strMsg;
            }

            // Convert to an internal public key string
            pubkey = Pki.Rsa.FromXMLString(rsakeyvalue).ToString();
            if (pubkey.Length == 0) {
                strMsg = String.Format("**ERROR: Cannot extract a valid RSA public key");
                return strMsg;
            }
            Console.WriteLine("Signing key bits = {0}, KeyHashCode={1:X8}", Pki.Rsa.KeyBits(pubkey), Pki.Rsa.KeyHashCode(pubkey));

            // Extract the SignatureValue
            sigval = Xmlsq.Query.GetText(sig, "//SignatureValue", Query.Opts.Trim);
            Console.WriteLine("SignatureValue=" + sigval);

            // Compute the digest of the last SignedInfo object
            nsigs = Xmlsq.Query.Count(xmlStr, "//Signature");
            siref = String.Format("SignedInfo[{0}]", nsigs);
            Console.WriteLine(siref);
            //  (NB use byte array)
            dig = Sc14n.C14n.ToDigest(xmlData, siref, Tran.SubsetByTag, digAlg, tranMethod);
            Console.WriteLine("SignedInfo digest value=" + dig);
            byte[] b = Sc14n.C14n.ToBytes(xmlData, siref, Tran.SubsetByTag, tranMethod);
            //Console.WriteLine(System.Text.Encoding.Default.GetString(b));

            // Verify the signature using the digest value
            n = Pki.Sig.VerifyDigest(sigval, Pki.Cnv.FromBase64(dig), pubkey, sigAlg);
            Console.WriteLine("Pki.Sig.VerifyDigest returns {0} (expecting 0)", n);
            if (n != 0) {
                strMsg = String.Format("**ERROR: Signature validation failed");
                return strMsg;
            }

            return "OK";

        }

        static string VerifyFRMA(string xmlStr)
        {
            string strMsg;
            string ddelem;
            string rsakeyvalue;
            string pubkey;
            byte[] b;
            string sigval;
            int n;

            Console.WriteLine("\nVerifying FRMA...");

            // [2021-02-05] FIX
            // We can use Xmlsq to extract the flattened DD element - NO!
            // PROBLEM: Xpath converts XML entities &apos; and &qout; to literal characters, e.g. " and '
            //ddelem = Xmlsq.Query.FullQuery(xmlStr, "//DD", Query.Opts.Raw | Query.Opts.Trim);
            //if (ddelem.Length == 0) {
            //    strMsg = String.Format("**ERROR: Cannot find DD element in DTE document");
            //    return strMsg;
            //}
            // SOLUTION: Use regex instead to extract the <DD> element.
            Match match = Regex.Match(xmlStr, @"(<DD.*?</DD>)", RegexOptions.Singleline);
            if (!match.Success) {
                strMsg = String.Format("**ERROR: Cannot find DD element in DTE document");
                return strMsg;
            }
            ddelem = match.Value;
            // Now flatten it - this is the input for the FRMT signature
            ddelem = Regex.Replace(ddelem, @">\s+<", @"><");
            //Console.WriteLine("\n--\n" + ddelem + "\n--\n");

            // Extract the RSAPK element
            rsakeyvalue = Xmlsq.Query.FullQuery(ddelem, "//RSAPK", Query.Opts.Raw | Query.Opts.Trim);
            //Console.WriteLine(rsakeyvalue);
            // Change element names to match XKMS2 format we expect for XML key string
            rsakeyvalue = rsakeyvalue.Replace("RSAPK>", "RSAKeyValue>").Replace("M>", "Modulus>").Replace("E>", "Exponent>");
            //Console.WriteLine(rsakeyvalue);
            // Read in public key from RSAKeyValue string
            pubkey = Pki.Rsa.ReadPublicKey(rsakeyvalue).ToString();
            Console.WriteLine("FRMA Public key has {0} bits, KeyHashCode={1:X8}", Pki.Rsa.KeyBits(pubkey), Pki.Rsa.KeyHashCode(pubkey));

            // Extract the FRMT signature value from the main doc
            sigval = Xmlsq.Query.GetText(xmlStr, "//TED/FRMT", Query.Opts.Trim);
            Console.WriteLine("FRMT signature value={0}", sigval);

            Console.WriteLine("DD element={0}", ddelem);

            // Verify the signature over the bytes of flattened DD element in Latin-1 encoding
            b = System.Text.Encoding.GetEncoding("iso-8859-1").GetBytes(ddelem);
            n = Pki.Sig.VerifyData(sigval, b, pubkey, sigAlg);
            Console.WriteLine("Pki.Sig.VerifyData returns {0} (expecting 0)", n);

            if (n != 0) {
                strMsg = String.Format("**ERROR: FRMT signature is invalid");
                return strMsg;
            }

            return "OK";
        }

    }
}