6

I am trying to sign an XML file in C# using Signature Class library by Microsoft.

What I have done is like this-

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Security.Cryptography.Xml;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Xml;
using XMLSigner.Model;
using DataObject = System.Security.Cryptography.Xml.DataObject;

internal static XmlDocument GetSignedXMLDocument(XmlDocument xmlDocument, X509Certificate2 certificate, long procedureSerial = -1, string reason = "")
{
    //Check if local time is OK
    if(!Ntp.CheckIfLocalTimeIsOk()) {
        MessageBox.Show("PC Time is need to be updated before sign !");
        return null;    //Last Sign Not Verified
    }
    //Then sign the xml
    try
    {
        //MessageBox.Show(certificate.Subject);
        SignedXml signedXml = new SignedXml(xmlDocument);
        signedXml.SigningKey = certificate.PrivateKey;

        // Create a reference to be signed
        Reference reference = new Reference();
        /////////////////////
        reference.Uri = "";//"#" + procedureSerial;
        //reference.Type = reason;
        //reference.Id = DateTime.UtcNow.Ticks.ToString();
        reference.Id = Base64EncodedCurrentTime();
        //reference.TransformChain = ;
        /////////////////////
        // Add an enveloped transformation to the reference.            
        XmlDsigEnvelopedSignatureTransform env = new XmlDsigEnvelopedSignatureTransform(true);
        reference.AddTransform(env);

        // Add the reference to the SignedXml object.
        signedXml.AddReference(reference);

        //canonicalize
        XmlDsigC14NTransform c14t = new XmlDsigC14NTransform();
        reference.AddTransform(c14t);

        KeyInfo keyInfo = new KeyInfo();
        KeyInfoX509Data keyInfoData = new KeyInfoX509Data(certificate);
        KeyInfoName kin = new KeyInfoName();
        //kin.Value = "Public key of certificate";
        kin.Value = certificate.FriendlyName;

        RSA rsa = (RSA)certificate.PublicKey.Key;
        RSAKeyValue rkv = new RSAKeyValue(rsa);
        keyInfo.AddClause(rkv);

        keyInfo.AddClause(kin);
        keyInfo.AddClause(keyInfoData);
        signedXml.KeyInfo = keyInfo;

        //////////////////////////////////////////Add Other Data as we need////
        // Add the data object to the signature.
        //CreateMetaDataObject("Name", GetNetworkTime());
        signedXml.AddObject(CreateMetaDataObject(procedureSerial, reason));
        ///////////////////////////////////////////////////////////////////////
        // Compute the signature.
        signedXml.ComputeSignature();

        // Get the XML representation of the signature and save 
        // it to an XmlElement object.
        XmlElement xmlDigitalSignature = signedXml.GetXml();

        xmlDocument.DocumentElement.AppendChild(
                xmlDocument.ImportNode(xmlDigitalSignature, true)
            );
        /////////////////////
    } catch (Exception exception) {
        MessageBox.Show("Internal System Error during sign");
        throw exception;
    }
    return xmlDocument;
}

And it is working completely fine. But I have an issue with this code. I have to use TSA Server for the stored time in the XML Signature, but the time is set from local PC, to avoid this issue, I have checked time manually from Ntp.CheckIfLocalTimeIsOk() function defined in here. But I like to have the time come from a TSA link like-

Is it possible to configure TSA in XmlSignature in C#?

Abrar Jahin
  • 13,970
  • 24
  • 112
  • 161
  • 1
    Is your machine clock already connected to a Network Time Server? If so, going to a different time server is not going to give you better time. You could always purchase a GPS Time Server so your machine would have accurate time. If you already have a Network you could register for the port number to get the time directly. See : http://www.pinvoke.net/default.aspx/ntdll.NtQuerySystemTime – jdweng Sep 21 '20 at 09:51
  • 2
    This is a desktop client, so PC time may not be correct always. If it is a server application, then there should be no issue. But I can't depend on user PC time. And according to compliance, the time should come from a TSA server. – Abrar Jahin Sep 21 '20 at 10:14
  • 1
    What happens if PC is not on internet? Here is c# code to get time from NIST (US government time standard) : http://csharphelper.com/blog/2014/11/get-the-current-time-from-the-nist-server-in-c/ – jdweng Sep 21 '20 at 10:43
  • 1
    According to compliance, the time should come from a TSA server., not an NTP server, that is the issue :(. Anyway, thank for trying help – Abrar Jahin Sep 21 '20 at 11:56
  • 1
    You will not find a better TSA server then the US Government Official Time Standard. – jdweng Sep 21 '20 at 12:10
  • 1
    @jdweng , true, but I like to have the link customizable. I like to have customisable TSA so that it can be used in any restricted network where say NIST server can't be accessed – Abrar Jahin Sep 21 '20 at 12:17
  • 1
    You then get into the same issue that you are trying to solve. Suppose a client changes the date of the time server so it is not accurate. How do you know the time server is accurate? And what happens when the client doesn't have a time server? Why are you trying to generate a certificate in a restricted network? I would generate the certificate in a non restricted network and then copy into restricted network. The only problem is the private key. The best solution on restricted networks is to purchase a GPS Time Server and cannot be spoofed. – jdweng Sep 21 '20 at 12:40
  • 1
    Please have a little look at what is TSA dear friend @jdweng . – Abrar Jahin Sep 21 '20 at 15:21
  • 1
    https://www.ietf.org/rfc/rfc3161.txt – Abrar Jahin Sep 21 '20 at 15:22
  • 1
    What u are asking is impossible because if that is possible, then that is not a TSA server because RFC 3161 compliment will be gone. I think u need to learn a little more to discuss about this issue, friend @jdweng – Abrar Jahin Sep 21 '20 at 15:23
  • 1
    A Time Standard is a Time Standard. So you want a accurate time. Does it matter that is is a TSA (which is a registered port on a network) or from an HTTP connection? Even you said : "the time SHOULD come from a TSA server", MUST. There is a difference between a requirement and an implementation. How sure should the TSA be? A TSA is just a device which puts time on a specific port number. What you are trying to do is to make sure the Certificate is generated with the correct date. How do you prevent your users from purposely using a wrong date by changing the TSA date? – jdweng Sep 21 '20 at 16:24
  • 1
    Thanks for your important information friend @jdweng . I am completely agree with you personally. Actually we are trying to implement a local XML signer which will exist in user local PC. Our XML signer should comply rfc 3161 according to requirement :(. So, out time should come from a TSA server. Also, our XML Signer desktop client can work on different networks where all TSA servers may not be accessed, so need configurable TSA URL, that's why I am trying to use TSA from a variable. – Abrar Jahin Sep 22 '20 at 05:17
  • You have to be a lawyer to read a RFC specification. The NIST website meet RFC 3161 according to para 1.0 and 3.4. See : https://tools.ietf.org/html/rfc3161. Para says "Any Trusted Time Source" and the US government is very trusted and para say it can be an http. – jdweng Sep 22 '20 at 09:36
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/221922/discussion-between-abrar-jahin-and-jdweng). – Abrar Jahin Sep 23 '20 at 07:20
  • Take a look at bouncy castle project referenced in this SO answer: https://stackoverflow.com/questions/52224410/bouncycastle-timestampprotocol-how-do-i-get-the-original-hash-from-a-timestamp – MikeJ Sep 29 '20 at 20:59

1 Answers1

1

I have solved the problem myself.

What I have done is to create a hash from the XMLDocument like this-

private static byte[] GetXmlHashByteStream(XmlDocument xmlDoc)
{
    byte[] hash;
    XmlDsigC14NTransform transformer = new XmlDsigC14NTransform();
    transformer.LoadInput(xmlDoc);
    using (Stream stream = (Stream)transformer.GetOutput(typeof(Stream)))
    {
        SHA1 sha1 = SHA1.Create();
        hash = sha1.ComputeHash(stream);
        stream.Close();
    }
    return hash;
}

Then get the Timestamp Hash like this-

string stampURI = "http://timestamp.globalsign.com/scripts/timstamp.dll"
private TimeStampResponse GetSignedHashFromTsa(byte[] hash)
{
    TimeStampRequestGenerator reqGen = new TimeStampRequestGenerator();

    TimeStampRequest request = reqGen.Generate(
                TspAlgorithms.Sha1,
                hash,
                BigInteger.ValueOf(100)
            );
    byte[] reqData = request.GetEncoded();

    HttpWebRequest httpReq = (HttpWebRequest)WebRequest.Create(stampURI);
    httpReq.Method = "POST";
    httpReq.ContentType = "application/timestamp-query";
    httpReq.ContentLength = reqData.Length;

    //Configure Timeout
    //httpReq.Timeout = 5000;
    //httpReq.ReadWriteTimeout = 32000;

    // Write the request content
    Stream reqStream = httpReq.GetRequestStream();
    reqStream.Write(reqData, 0, reqData.Length);
    reqStream.Close();

    HttpWebResponse httpResp = (HttpWebResponse)httpReq.GetResponse();

    // Read the response
    Stream respStream = new BufferedStream(httpResp.GetResponseStream());
    TimeStampResponse response = new TimeStampResponse(respStream);
    respStream.Close();

    return response;
}

Re-

If you like to get signed Timestamp string from the response, you can do like this-

internal string GetSignedHashFromTsa(XmlDocument xmlDxocument)
{
    byte[] hash = GetXmlHashByteStream(xmlDxocument);
    TimeStampResponse timeStampResponse = GetSignedHashFromTsa(hash);
    byte[] signedEncodedByteStream = timeStampResponse.GetEncoded();
    return Convert.ToBase64String(signedEncodedByteStream);
}

If you like to get the time from the hash string, then you have to do something like this-

internal static DateTime? GetTsaTimeFromSignedHash(string tsaSignedHashString)
{
    try {
        byte[] bytes = Convert.FromBase64String(tsaSignedHashString);
        TimeStampResponse timeStampResponse = new TimeStampResponse(bytes);
        return timeStampResponse.TimeStampToken.TimeStampInfo.GenTime;
    }
    catch(Exception ex)
    {
        Console.WriteLine(ex.Message);
        //throw ex;
        return null;
    }
}
Abrar Jahin
  • 13,970
  • 24
  • 112
  • 161