Generate Digital Signature but with a Specific Namespace Prefix ("ds:")
Asked Answered
S

3

17

I digitally sign XML files, but need the signature tags contain the namespace prefix "ds". I researched quite the google and found many of the same questions, but no satisfactory answer.

I tried to put the "ds" manually in the file, but the signature becomes invalid. The tag "SignatureValue" signs the tag "SignedInfo" so the signature becomes invalid.

Could somebody show me how I generate the value of the tag "SignatureValue" so I can replace the signature after adding the prefix "ds"?

Sorghum answered 1/6, 2015 at 17:48 Comment(8)
That should kick it back into view :)Turbofan
Note that the signature is placed on the canonicalized version of the plaintext (the XML element where the signature is placed upon). You need the private key to generate the signature, so you cannot replace the signature value itself. The trick would be to insert the "ds" namespace without altering the canonical representation, so that the signature stays the same.Turbofan
I think I personally solved this by screaming to the users of a MS BizTalk server to use software that generates standardized signatures :)Turbofan
Any luck with it in the mean time, Renato? If you find an answer don't forget to post it! I presume here that you want to leave the signature value intact?Turbofan
"You can use the classes in the System.Security.Cryptography.Xml namespace to sign an XML document or part of an XML document with a digital signature. XML digital signatures (XMLDSIG) allow you to verify that data was not altered after it was signed." MSDN what are you actually trying to accomplish here; for, it seems like your intent is to modify a signed xml file yet pass integrity validation as being unmodified..which goes against the purpose..Plossl
having said that, what qualifies as part of an XMLDocument vs an XMLDocument??.. can you load an xml file into an XMLDocument and call it part of an XML Document?.. is that the premise of your question?.. I would like to make a reference to another SO post in a similiar-premised Q&A: HEREPlossl
I have edited your title. Please see, "Should questions include “tags” in their titles?", where the consensus is "no, they should not".Tobar
What makes you think that you need the namespace prefix to be ds. That's not how XML works. You may have received this requirement from people who do not understand XML. It's possible that the requirement is about software that doesn't understand XML, but it seems unlikely for such software to also be smart enough to deal with XML signatures.Tobar
B
18

Apparently a lot of people ran into the same problem. After investigating source code of the class Signature, I came to conclusion that Microsoft aimed to help us. There is hardcoded prefix "ds" in the method LoadXml(). So, it is possible to generate signature, then add namespace prefix "ds" to it, load modified signature back and recompute "SignatureValue". Unfortunatelly bug in the library makes things a bit harder than they need to be. The code with workaround and comments is below.

public static void SignXml(XmlDocument xmlDoc, X509Certificate2 cert)
{
        // transformation cert -> key omitted
        RSACryptoServiceProvider key;

        // Create a SignedXml object. 
        SignedXml signedXml = new SignedXml(xmlDoc);

        // Add the key to the SignedXml document. 
        signedXml.SigningKey = key;
        signedXml.SignedInfo.SignatureMethod = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256";
        signedXml.SignedInfo.CanonicalizationMethod = SignedXml.XmlDsigExcC14NTransformUrl;

        // Create a reference to be signed. 
        Reference reference = new Reference();
        reference.Uri = "#foo";
        reference.DigestMethod = "http://www.w3.org/2001/04/xmlenc#sha256";
        // Add an enveloped transformation to the reference. 
        reference.AddTransform(new XmlDsigEnvelopedSignatureTransform());
        reference.AddTransform(new XmlDsigExcC14NTransform());
        signedXml.AddReference(reference);

        KeyInfo keyInfo = new KeyInfo();
        KeyInfoX509Data keyInfoData = new KeyInfoX509Data();
        keyInfoData.AddIssuerSerial(cert.IssuerName.Format(false), cert.SerialNumber);
        keyInfo.AddClause(keyInfoData);
        signedXml.KeyInfo = keyInfo;

        // Compute the signature. 
        signedXml.ComputeSignature();

        // Add prefix "ds:" to signature
        XmlElement signature = signedXml.GetXml();
        SetPrefix("ds", signature);

        // Load modified signature back
        signedXml.LoadXml(signature);

        // this is workaround for overcoming a bug in the library
        signedXml.SignedInfo.References.Clear();

        // Recompute the signature
        signedXml.ComputeSignature();
        string recomputedSignature = Convert.ToBase64String(signedXml.SignatureValue);

        // Replace value of the signature with recomputed one
        ReplaceSignature(signature, recomputedSignature);

        // Append the signature to the XML document. 
        xmlDoc.DocumentElement.InsertAfter(xmlDoc.ImportNode(signature, true), xmlDoc.DocumentElement.FirstChild);
    }

    private static void SetPrefix(string prefix, XmlNode node)
    {
        node.Prefix = prefix;
        foreach (XmlNode n in node.ChildNodes)
        {
            SetPrefix(prefix, n);
        }
    }

    private static void ReplaceSignature(XmlElement signature, string newValue)
    {
        if (signature == null) throw new ArgumentNullException(nameof(signature));
        if (signature.OwnerDocument == null) throw new ArgumentException("No owner document", nameof(signature));

        XmlNamespaceManager nsm = new XmlNamespaceManager(signature.OwnerDocument.NameTable);
        nsm.AddNamespace("ds", SignedXml.XmlDsigNamespaceUrl);

        XmlNode signatureValue = signature.SelectSingleNode("ds:SignatureValue", nsm);
        if (signatureValue == null)
            throw new Exception("Signature does not contain 'ds:SignatureValue'");

        signatureValue.InnerXml = newValue;
    }
Baggett answered 20/8, 2016 at 17:34 Comment(0)
M
4

Edit: You can see an algorithm which this post kind of alluded to in my other answer.

Assuming that it isn't possible without writing your own algorithm to canonicalize and sign the document, a possible workaround could be to "inject" the namespace prefix on the signature elements after signing, and then removing it from them before verifying.

For example:

void SignXml(XmlDocument xmlDoc, RSA Key)
{
    SignedXml signedXml = new SignedXml(xmlDoc);
    signedXml.SigningKey = Key;

    Reference reference = new Reference("");
    reference.AddTransform(new XmlDsigEnvelopedSignatureTransform());

    signedXml.AddReference(reference);

    signedXml.ComputeSignature();

    XmlElement xmlSignature = signedXml.GetXml();

    //Here we set the namespace prefix on the signature element and all child elements to "ds", invalidating the signature.
    AssignNameSpacePrefixToElementTree(xmlSignature, "ds");

    xmlDoc.DocumentElement.AppendChild(xmlDoc.ImportNode(xmlSignature, true));
}

bool VerifyXmlSignature(XmlDocument xmlDoc, RSA Key, string prefix)
{
    SignedXml signedXml = new SignedXml(xmlDoc);

    //Get the <ds:Signature /> element
    XmlElement xmlSignature = (XmlElement)xmlDoc.GetElementsByTagName(prefix + ":Signature")[0];

    //Undo what we did after signing
    AssignNameSpacePrefixToElementTree(xmlSignature, "");

    //Now it will pass verification.
    signedXml.LoadXml(xmlSignature);
    return signedXml.CheckSignature(Key);
}

void AssignNameSpacePrefixToElementTree(XmlElement element, string prefix)
{
    element.Prefix = prefix;

    foreach (var child in element.ChildNodes)
    {
        if (child is XmlElement)
            AssignNameSpacePrefixToElementTree(child as XmlElement, prefix);
    }
}
Manx answered 11/6, 2015 at 0:51 Comment(1)
I've submitted a new answer outlining an algorithm to recompute the signature after prefixing the Signature elements and how you might handle validation in C# thereafter.Manx
M
3

Since you are the one signing the documents, this should be easy enough to do, but the classes in System.Security.Cryptography.Xml don't really support this.

If you only need the Signature elements to be prefixed, then, provided the Signature is not referenced in itself (or if it is part of a referenced element then so long as it's removed with a transform as in "http://www.w3.org/2000/09/xmldsig#enveloped-signature") then all you need to do is recalculate the SignatureValue based on your altered SignedInfo element. See the SignEnveloped method below for an example.

However, your signature won't pass the validation outlined in MSDN's How to: Verify the Digital Signatures of XML Documents, because rather than calculating the SignatureValue to check against by actually reading the document's SignedInfo, the SignedXml class seems to generate a new one without prefixed elements. The class below works around SignedXmls seemingly errant implementations, but there could also be validation issues in other frameworks not expecting prefixed elements.

public static class XmlSigning
{
    private static Type tSignedXml = typeof(SignedXml);
    private static ResourceManager SecurityResources = new ResourceManager("system.security", tSignedXml.Assembly);

    //these methods from the SignedXml class still work with prefixed Signature elements, but they are private
    private static ParameterExpression thisSignedXmlParam = Expression.Parameter(tSignedXml);
    private static Func<SignedXml, bool> CheckSignatureFormat
        = Expression.Lambda<Func<SignedXml, bool>>(
            Expression.Call(thisSignedXmlParam, tSignedXml.GetMethod("CheckSignatureFormat", BindingFlags.NonPublic | BindingFlags.Instance)),
            thisSignedXmlParam).Compile();
    private static Func<SignedXml, bool> CheckDigestedReferences
        = Expression.Lambda<Func<SignedXml, bool>>(
            Expression.Call(thisSignedXmlParam, tSignedXml.GetMethod("CheckDigestedReferences", BindingFlags.NonPublic | BindingFlags.Instance)),
            thisSignedXmlParam).Compile();

    public static void SignEnveloped(XmlDocument xmlDoc, RSACryptoServiceProvider key, string signatureNamespacePrefix)
    {
        SignedXml signedXml = new SignedXml(xmlDoc);
        signedXml.SigningKey = key;

        Reference reference = new Reference("");
        reference.AddTransform(new XmlDsigEnvelopedSignatureTransform());

        signedXml.AddReference(reference);

        signedXml.ComputeSignature();

        XmlElement xmlSignature = signedXml.GetXml();

        if (!string.IsNullOrEmpty(signatureNamespacePrefix))
        {
            //Here we set the namespace prefix on the signature element and all child elements to "ds", invalidating the signature.
            AssignNameSpacePrefixToElementTree(xmlSignature, "ds");

            //So let's recompute the SignatureValue based on our new SignatureInfo...

            //For XPath
            XmlNamespaceManager namespaceManager = new XmlNamespaceManager(xmlDoc.NameTable);
            namespaceManager.AddNamespace("ds", "http://www.w3.org/2000/09/xmldsig#"); //this prefix is arbitrary and used only for XPath

            XmlElement xmlSignedInfo = xmlSignature.SelectSingleNode("ds:SignedInfo", namespaceManager) as XmlElement;

            //Canonicalize the SignedInfo element
            XmlDsigC14NTransform transform = new XmlDsigC14NTransform();
            XmlDocument signedInfoDoc = new XmlDocument();
            signedInfoDoc.LoadXml(xmlSignedInfo.OuterXml);
            transform.LoadInput(signedInfoDoc);

            //Compute the new SignatureValue
            string signatureValue = Convert.ToBase64String(key.SignData(transform.GetOutput() as MemoryStream, new SHA1CryptoServiceProvider()));
            //Set it in the xml
            XmlElement xmlSignatureValue = xmlSignature.SelectSingleNode("ds:SignatureValue", namespaceManager) as XmlElement;
            xmlSignatureValue.InnerText = signatureValue;
        }

        xmlDoc.DocumentElement.AppendChild(xmlDoc.ImportNode(xmlSignature, true));
    }

    public static bool CheckSignature(XmlDocument xmlDoc, RSACryptoServiceProvider key)
    {
        if (key == null)
            throw new ArgumentNullException("key");

        SignedXml signedXml = new SignedXml(xmlDoc);

        //For XPath
        XmlNamespaceManager namespaceManager = new XmlNamespaceManager(xmlDoc.NameTable);
        namespaceManager.AddNamespace("ds", "http://www.w3.org/2000/09/xmldsig#"); //this prefix is arbitrary and used only for XPath

        XmlElement xmlSignature = xmlDoc.SelectSingleNode("//ds:Signature", namespaceManager) as XmlElement;

        signedXml.LoadXml(xmlSignature);

        //These are the three methods called in SignedXml's CheckSignature method, but the built-in CheckSignedInfo will not validate prefixed Signature elements
        return CheckSignatureFormat(signedXml) && CheckDigestedReferences(signedXml) && CheckSignedInfo(signedXml, key);
    }

    private static bool CheckSignedInfo(SignedXml signedXml, AsymmetricAlgorithm key)
    {
        //Copied from reflected System.Security.Cryptography.Xml.SignedXml
        SignatureDescription signatureDescription = CryptoConfig.CreateFromName(signedXml.SignatureMethod) as SignatureDescription;
        if (signatureDescription == null)
            throw new CryptographicException(SecurityResources.GetString("Cryptography_Xml_SignatureDescriptionNotCreated"));

        Type type = Type.GetType(signatureDescription.KeyAlgorithm);
        Type type2 = key.GetType();
        if (type != type2 && !type.IsSubclassOf(type2) && !type2.IsSubclassOf(type))
            return false;

        HashAlgorithm hashAlgorithm = signatureDescription.CreateDigest();
        if (hashAlgorithm == null)
            throw new CryptographicException(SecurityResources.GetString("Cryptography_Xml_CreateHashAlgorithmFailed"));

        //Except this. The SignedXml class creates and cananicalizes a Signature element without any prefix, rather than using the element from the document provided
        byte[] c14NDigest = GetC14NDigest(signedXml, hashAlgorithm);

        AsymmetricSignatureDeformatter asymmetricSignatureDeformatter = signatureDescription.CreateDeformatter(key);
        return asymmetricSignatureDeformatter.VerifySignature(c14NDigest, signedXml.Signature.SignatureValue);
    }

    private static byte[] GetC14NDigest(SignedXml signedXml, HashAlgorithm hashAlgorithm)
    {
        Transform canonicalizeTransform = signedXml.SignedInfo.CanonicalizationMethodObject;
        XmlDocument xmlDoc = new XmlDocument();
        xmlDoc.LoadXml(signedXml.SignedInfo.GetXml().OuterXml);
        canonicalizeTransform.LoadInput(xmlDoc);
        return canonicalizeTransform.GetDigestedOutput(hashAlgorithm);
    }

    private static void AssignNameSpacePrefixToElementTree(XmlElement element, string prefix)
    {
        element.Prefix = prefix;

        foreach (var child in element.ChildNodes)
        {
            if (child is XmlElement)
                AssignNameSpacePrefixToElementTree(child as XmlElement, prefix);
        }
    }
}
Manx answered 12/6, 2015 at 22:39 Comment(2)
Renato, could you please check if this works correctly for your situation? Although I am interested in the outcome, I don't have anything to test against.Turbofan
I had to make a few adjustments to get it to work specifically with my case, however, after making those adjustments, I was able to compute the signature, update with the prefix, and verifiy the signature successfully.Neoclassic

© 2022 - 2024 — McMap. All rights reserved.