How to sign XML using RSA in .NET

Scott Brady
Scott Brady
C#

Signing XML (XMLDSig) is not a particularly fun thing to deal with, and, from a security perspective, getting it wrong has been the source of many headaches. Whether it is peculiarities from the core XML specification or just the placement of a signature element, XML and XML signing have been the source of many vulnerabilities, including most of SAML’s.

In this article, you’re going to see how to create and validate signed XML in .NET using RSA while avoiding some of the common security issues with XML. This will follow a similar structure to the guidance in “Security in .NET” and the Microsoft documentation, but with a focus on .NET Core/.NET and some of the attacks you will need to defend against when dealing with signed XML.

To learn how to use algorithms not yet supported out of the box by Microsoft, check out my other article, “ECDSA and Custom XML Signatures in .NET”.

Safely loading XML in .NET

To start with, you’ll need to safely load in some XML without falling victim to the various forms of XML eXternal Entity (XXE) attacks.

The .NET libraries that you use to handle XML signature all require the use of XmlElement, rather than the newer XElement. This means that you’ll need to create an XmlDocument.

const string xml = "<message><content>Just remember ALL CAPS when you spell the man name</content></message>";
var xmlDoc = new XmlDocument {PreserveWhitespace = true};

using var stringReader = new StringReader(text);
using var xmlReader = XmlReader.Create(stringReader);
xml.Load(xmlReader);

In this example, you’re loading in the XML from a string using an XmlReader and XmlDocument’s Load method. If you are loading in the XML differently, make sure that you are safe from XXE attacks. You can check your library against OWASP’s XXE prevention cheat sheet, but I’m not sure if this is up-to-date past .NET Framework 4.x.

Based on my testing, the above usage of XmlDocument and XmlReader is safe in .NET 5, but it might not be in earlier versions of .NET. If you’re using this in a library, I recommend writing some unit tests checking for the core attacks, such as the billion laughs attack, which run on the different versions of .NET that you support.

Preserving whitespace is often important to maintain interoperability.

Signing keys

In this example, you’ll sign XML using “http://www.w3.org/2001/04/xmldsig-more#rsa-sha256”, which means RSASSA-PKCS1-v1_5 using SHA-256. .NET supports this algorithm out of the box, and it’s typically the most common signing algorithm you’ll see across identity providers such as Azure AD and Auth0. It is equivalent to JOSE’s RS256. Better algorithms are available.

To create signatures, you’ll need an RSA key. You could use the RSA class directly, but it is very common with XML digital signatures to see an X.509 certificate used to share keys.

For now, let’s use an in-memory key and a self-signed certificate. In production, you’ll want to use something long-lived and shareable. For instance, you could generate your keys using OpenSSL and distribute them as PEM files.

// in-memory key and certificate - not suitable for production
var rsa = RSA.Create(3072);
X509Certificate2 cert = new CertificateRequest("CN=test", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1)
    .CreateSelfSigned(DateTimeOffset.UtcNow.AddDays(-2), DateTimeOffset.UtcNow.AddDays(-2));

Signing XML in .NET

Now that you have your XML and keys let’s sign some XML. To do so, you’ll need to install Microsoft’s XML cryptography library from nuget:

dotnet add package System.Security.Cryptography.Xml

Using your private key, you can then use SignedXml to create an XML digital signature for an XmlElement object.

// set key, signing algorithm, and canonicalization method
var signedXml = new SignedXml(xml.DocumentElement) {SigningKey = cert.GetRSAPrivateKey()};
signedXml.SignedInfo.SignatureMethod = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256";
signedXml.SignedInfo.CanonicalizationMethod = "http://www.w3.org/2001/10/xml-exc-c14n#";

// sign whole document using "SAML style" transforms
var reference = new Reference {Uri = string.Empty}; 
reference.AddTransform(new XmlDsigEnvelopedSignatureTransform());
reference.AddTransform(new XmlDsigExcC14NTransform());
signedXml.AddReference(reference);

// create signature
signedXml.ComputeSignature();

// get signature XML element and add it as a child of the root element
signedXml.GetXml();
xml.DocumentElement?.AppendChild(signedXml.GetXml());

This example follows a similar structure to the Security in .NET guidance, but with SAML-style canonicalization & transforms and an X509 certificate.

Canonicalization tells the verifier of the XML how to turn the Signature element from XML into bytes so that it can validate the signature. For example, here you are using “http://www.w3.org/2001/10/xml-exc-c14n#”, a very common canonicalization method, seen in heavy use in the SAML world.

Transforms, on the other hand, describe how to turn the XML that you signed into bytes. If the verifier doesn’t know how you did this, they will likely not reproduce the same bytes, and it will appear as an invalid signature. For example, here you are using “http://www.w3.org/2000/09/xmldsig#enveloped-signature”, which tells the validator that it should remove the signature element from the document before it validates the signature.

Also of note is that you are signing all of the XML, which is why you are using an empty string as the reference URI. If you were to have multiple signatures or sign particular parts of the XML, you would need to set this URI to point to the ID of that element. More on this later.

Example signed XML

Running the above code gives you XML like the following (formatted for readability). The Signature element is a child of the root element (it’s an enveloped signature, and it signed the root element) and contains the signature value itself, along with information about how to validate the signature.

<message>
  <content>Just remember ALL CAPS when you spell the man name</content>
  <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
    <SignedInfo>
      <CanonicalizationMethod
        Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
      <SignatureMethod
        Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" />
      <Reference
        URI="">
        <Transforms>
          <Transform
            Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
          <Transform
            Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
        </Transforms>
        <DigestMethod
          Algorithm="http://www.w3.org/2001/04/xmlenc#sha256" />
        <DigestValue>9A4u9FDDvQS0fcqS76EbS5Ir95wh3JOu2QldyyfWrHs=</DigestValue>
      </Reference>
    </SignedInfo>
    <SignatureValue>vJnhvsE5nhs29ezcaJhSoYeGhyybPjnO3TCwhne7sqmgnvOCHbqqBU9qYCk2mivqM3yVCKkpBoYBEk3GsS1xc9HAUHLzPpXdr/N6Z0j2tfJ1v5S3OMiRPxgqrhIJaBOesfeEK9ohiE7/R7MXaat6AEVEgHevSIefr4ZWPWZqnXG1rIjN2q9eAZ52PCPolqSeKDzDLqBIdtjHFoS1sd+mz9g6mQDC6wnngC8IkbKQa4OlnzJ4SsK1SPUS/FOGkOOxZzjzAQleFEehZNDKzfjonmLLRX7GtHKwa9tnl24n7gfcj43fV8/uHAhYHkdAbhqzFuLbX6dv8bgQ0GAFAKOvbZsFyPxPCigu1+Fzs99LUOfRknkrr9+YMWDDlpHpR8m1UUqSt0mbZyvHHjR0dwDtYAASjHCTr9MRTPROCOLp34RzCI4wruw3jTVuoNfdFKQSStF6IUdk6eNPqg7369hiukiRq50vmRru/+jNq2uepOlMVI7i02QeSyaJD7wMjVnr</SignatureValue>
  </Signature>
</message>

Embedding keys in the Signature elements

You could also embed the public key into the Signature element. Of course, the verifier would not usually trust this key (they would already know the correct public key and use that instead), but this can be useful for debugging and testing the signature in online validation tools.

// ...create signed XML

// OPTIONAL: embed the public key in the XML.
// This MUST NOT be trusted during validation (used for debugging only)
var keyInfo = new KeyInfo();
keyInfo.AddClause(new KeyInfoX509Data(cert));
signedXml.KeyInfo = keyInfo;

// compute signature...

A better use for the KeyInfo element is to include a KeyName, which you can use to reference the public key that the verifier should use to validate the signature. Specifying a key like this can help when the signature’s issuer has multiple keys (e.g., during key rollover).

Example KeyInfo element

A KeyInfo element containing an X.509 certificate and a key name would look something like the following:

<Signature>
  // SignedInfo element
  <KeyInfo>
    <X509Data>
	  <X509Certificate>MIIDnzCCAgegAwIBAgIJAMpH03rPnY5UMA0GCSqGSIb3DQEBCwUAMA8xDTALBgNVBAMTBHRlc3QwHhcNMjEwOTA0MTkzMjA3WhcNMjEwOTA0MTkzMjA3WjAPMQ0wCwYDVQQDEwR0ZXN0MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEA6ZJlF7zIr8Leir3/upBlZHcqfMwPEAArfEVPIUtA4ar9g7DGBwqrPNzyon+GsVQpBOS3+aJKJ0eSNrgPqcxKuqhiKOSh/2rmV1OntcU+iMsd/ze33fwL5rQN9yZcAQCLj91bcGBxEapi4KoN8/N3W5TEx0kasqBzYNimLwE2vh35fMl3/nxyK61n/yF5LPva0j/DK5o8FIjawnCAUvXrRshpnliTjG3ijh104TDIozVGNOeIqXklVn9PYvc6cmYTXyRKd2/F0cnWf2dqF31t1jyF9wRlinZt7Tj7Uc+e0dzC9lw/tVAhLEdQdW0UaAzH12wbGiZdJWbzj5awjwaAuSsfgJRIcYCi/pRTELmuz+fohFY6nb0xZExwl/hhT+52mUdKy+zNuV3pVZm1XDMD3KNj5hTBUhiMyKZtv6poEIueBBFDx2/q06vRwhZV628a8O0tzj4xWf4mBPSVNXt8HN7+Lq3WPVqHSlZeJySAh00wh6RGoBSsBCtMypdbPLstAgMBAAEwDQYJKoZIhvcNAQELBQADggGBAGrWDNTch8hfDBB51N7gNpWKZfy/B8DXtXaywOcR8q2OJ5Aed4o0XUwaX0DK0d+dTiC1zeLv/TSggYEgVxxcQEkIVXutEEC5ptkovbWRlkvyxcuaIng62Cv0YFiRAJo7owZQZchIMslJVgR2qHJN0B3m/4tuNRiW9MgbsD9u3ubdUMg7nlFCK1Q4SgViaSojwiYjAlyosGu8a5ymJpLu+Yyo19U8MEDc46WtY6shROjyyjyIVZpFLVpr8JxKnF+edh5WOBDK2oPO/73pvAS/kOvckSq/cTvChA2uRfiKy3rRGcKNR3C5EhR3MH5dpB5gKnNVSRxjTL748/nQ95TKhFDuZz+q6PBAO2ZeBoo/vb2ho+Ydkyo59cAlOytvO84Em3PsCQ/iC8RxAeC3w4jCUvjHq/HzyGY+6HmXisVWVEHTCYk/xQPv8bnNLB7+gsWr2KYWGXywvuPXdURT7yTZIQhXcYYHok8/yWKxe2o+iNOr1iSQ/hbKw19XwxgLXBhJRQ==</X509Certificate>
	</X509Data>
    <KeyName>BECB8B98E5A0E6FD2ADA01B1EB00AEA45C4462E2</KeyName>
  </KeyInfo>
</Signature>

Validating XML signatures in .NET

To validate the XML digital signature, you’ll need to use the correct public key and use it to validate the Signature element against the element you expect to be signed.

var signedXml = new SignedXml(xml);

// double-check the schema
// usually we would validate using XPath
var signatureElement = xml.GetElementsByTagName("Signature");
if (signatureElement.Count != 1)
    throw new InvalidOperationException("Too many signatures!");

signedXml.LoadXml((XmlElement) signatureElement[0]);

// validate references here!
if ((signedXml.SignedInfo.References[0] as Reference)?.Uri != "")
    throw new InvalidOperationException("Check your references!");

bool isValid = signedXml.CheckSignature(cert.GetRSAPublicKey());

Here you pass SignedXml the full XML document (which includes the Signature element), extract the signature element, and then validate the signature using your public key. That public key should be something you already had possession of instead of any keys embedded in the Signature element. If you had multiple keys, you might check to see if the signature included a KeyName so that you don’t have to try them all.

Defending against XML signature wrapping attacks

You’re also validating the number of signature elements and confirming that the chosen signature element references the correct element. This is a defense against XML signature wrapping, an attack that originates against SAML, but it works against any XML signature and schema.

To defeat signature wrapping attacks, you need to ensure that the signature element you extract is the correct one according to your XML schema (usually using XPath) and that it references the element you expect it to have signed. You can read more about XML signature wrapping and how to defend against them in OWASP’s SAML cheat sheet.

Source Code

You can find an executable sample of the above code in my samples repository. This also includes sample code for using ECDSA, rather than RSA, which I cover in my next article, “ECDSA XML Signatures in .NET”.

If you want to learn more about XML signatures and the Signature element, I recommend reading the remarks in SignedXml’s documentation.