ECDSA and Custom XML Signatures in .NET

Scott Brady
Scott Brady
C#

XML digital signatures are a tricky beast to handle but, while you’d rather be using something else, that doesn’t mean you get to neglect it by using outdated cryptography.

In this article, I’m going to show you how to sign XML in .NET Core 2 onwards using ECDSA (an Elliptic Curve Digital Signing Algorithm) rather than the in-built RSA.

While these examples use ECDSA, you can use the same approach to support any signing algorithm not supported out of the box by .NET. Check out my previous article to learn more about XML signing in .NET.

ECDSA XML signing

Let’s start with a basic sample of XML signing in .NET using SAML-style canonicalization and transforms. However, instead of using RSA, let’s use an Elliptic Curve (EC) key and our new signing algorithm: “http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256”. This namespace represents ECDSA using the P-256 curve and SHA-256. It’s the same as JOSE’s ES256.

To sign XML, you’ll need to install Microsoft’s XML cryptography library from nuget:

dotnet add package System.Security.Cryptography.Xml

Then, your code for signing XML with ECDSA:

const string text = "<message><content>Just remember ALL CAPS when you spell the man name</content></message>";
var xml = new XmlDocument {PreserveWhitespace = true, XmlResolver = null};
xml.LoadXml(text);

// in-memory key and certificate - not suitable for production
var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
X509Certificate2 cert = new CertificateRequest("CN=test", ecdsa, HashAlgorithmName.SHA256)
    .CreateSelfSigned(DateTimeOffset.UtcNow.AddDays(-2), DateTimeOffset.UtcNow.AddDays(-2));

// set your signing key, signing algorithm, and canonicalization method
var signedXml = new SignedXml(xml.DocumentElement) {SigningKey = cert.GetECDsaPrivateKey()};
signedXml.SignedInfo.SignatureMethod = "http://www.w3.org/2001/04/xmldsig-more#ecdsa-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 code uses an in-memory ECDsa key and X509Certificate2. In your code, you would load in your own ECDsa key.

However, if you try running this code out of the box, you’ll get the following error:

CryptographicException: SignatureDescription could not be created for the signature algorithm supplied.

This is because .NET does not support this ECDSA for XML signing out of the box, so you’ll need to add support yourself. Let’s take a look at how to support custom XML signing algorithms in .NET and then add support for ECDSA.

Custom XML signing algorithms in .NET

To sign XML, SignedXml uses an implementation of SignatureDescription, which it creates using System.Security.Cryptography.Xml.CryptoHelpers.

CryptoHelpers creates objects based on a key. So, for instance, “MD5” will return a new MD5 object, and “http://www.w3.org/2001/04/xmldsig-more#rsa-sha256”, the XML digital signature algorithm used in my previous article, will return a new RSAPKCS1SHA256SignatureDescription object.

If CryptoHelpers does not know the key, it will call into System.Security.Cryptography.CryptoConfig, which has its own lookup table for various cryptographic objects. However, with CryptoConfig, you can register new implementations, such as a signature description for “http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256”, which you’ll see shortly.

SignedXml is expecting an implementation of System.Security.Cryptography.SignatureDescription, which is responsible for creating objects that can generate the digest (hash) and the signature. For signatures, it uses a formatter (AsymmetricSignatureFormatter) and deformatter (AsymmetricSignatureDeformatter).

Here’s some pseudo-code for how SignedXml validated an XML digital signature:

SignedXml.ComputeSignature():
    // calls CryptoHelpers.CreateFromKnownName and then CryptoConfig.CreateFromName
    signatureDescription = CryptoHelpers.CreateFromName(signingAlgorithm)

    formatter = SignatureDescription.CreateFormatter(signingKey)
    signature = formatter.CreateSignature(hashingAlgorithm)

Now that you know the building blocks, let’s add support for our new signing algorithm.

Creating a custom SignatureDescription

Let’s start with the SignatureDescription and the associated signature formatters.

The formatters will be adaptors for .NET’s ECDsa object, which already has all the functionality you need to create a signature from a hash and validate a hash against a signature.

public class EcdsaSignatureFormatter : AsymmetricSignatureFormatter
{
    private ECDsa key;

    public EcdsaSignatureFormatter(ECDsa key) => this.key = key;

    public override void SetKey(AsymmetricAlgorithm key) => this.key = key as ECDsa;
    
    public override void SetHashAlgorithm(string strName) { }

    public override byte[] CreateSignature(byte[] rgbHash) => key.SignHash(rgbHash);
}

public class EcdsaSignatureDeformatter : AsymmetricSignatureDeformatter
{
    private ECDsa key;

    public EcdsaSignatureDeformatter(ECDsa key) => this.key = key;

    public override void SetKey(AsymmetricAlgorithm key) => this.key = key as ECDsa;
    
    public override void SetHashAlgorithm(string strName) { }

    public override bool VerifySignature(byte[] rgbHash, byte[] rgbSignature) 
        => key.VerifyHash(rgbHash, rgbSignature);
}

You only really need to care about CreateSignature and VerifySignature methods, which are just calling into ECDsa. .NET is handling all of the canonicalization and transformations for you. All that’s left for you to do is sign the hash.

Using these formatters, you can then create a really simple SignatureDescription that returns these formatters and handles some basic validation (e.g., an EC key is being used and looks like it’s for the right curve).

public class Ecdsa256SignatureDescription : SignatureDescription
{
    public Ecdsa256SignatureDescription()
    {
        KeyAlgorithm = typeof(ECDsa).AssemblyQualifiedName;
    }
    
    public override HashAlgorithm CreateDigest() => SHA256.Create();

    public override AsymmetricSignatureFormatter CreateFormatter(AsymmetricAlgorithm key)
    {
        if (!(key is ECDsa ecdsa) || ecdsa.KeySize != 256) throw new InvalidOperationException("Requires EC key using P-256");
        return new EcdsaSignatureFormatter(ecdsa);
    }

    public override AsymmetricSignatureDeformatter CreateDeformatter(AsymmetricAlgorithm key)
    {
        if (!(key is ECDsa ecdsa) || ecdsa.KeySize != 256) throw new InvalidOperationException("Requires EC key using P-256");
        return new EcdsaSignatureDeformatter(ecdsa);
    }
}

Registering a custom algorithm

You can register your custom SignatureDescription for a signing algorithm by using CryptoConfig’s AddAlgorithm method. It will need to be registered globally in your app.

CryptoConfig.AddAlgorithm(
    typeof(Ecdsa256SignatureDescription), 
    "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256");

With your custom algorithm registered, you can now run the previous code and sign XML using ECDSA.

XML signed using ECDSA

This results in the following XML (formatted for readability):

<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#ecdsa-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>A1Vz+93PgSq3auxwqW087exDtOYgSazYTSgYlXZgWVZI6tKXwrZZ9O4SdQiHHI4Y2Ro8Ho5zgf+HjpN/ushvPw==</SignatureValue>
      <KeyInfo>
         <X509Data>
            <X509Certificate>MIIBEDCBuKADAgECAggVM8YfT8EdfTAKBggqhkjOPQQDAjAPMQ0wCwYDVQQDEwR0ZXN0MB4XDTIxMDczMDA3NTU1NVoXDTIxMDczMDA3NTU1NVowDzENMAsGA1UEAxMEdGVzdDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABHpb/uly4GB+9G2BKgToi57/XzqapEdo2Pys48RMtj8tc6WE2BO0TJoR1KJZ1Bu05OQ0aOwyFGo1QY65V6sgONIwCgYIKoZIzj0EAwIDRwAwRAIgV50ULGELC8aTSxOmTPptqHjOgKlbKLlQ+CuErOUBCucCIBvn/IWSLPVqwoQNzP7VnRgk9mZvUuTW0MaIf/4lhOc7</X509Certificate>
         </X509Data>
      </KeyInfo>
   </Signature>
</message>

With the optional key info element like this, you can copy & paste your output into online signature validators for testing. Just be aware of how you handle whitespace and formatting. For example, the line breaks in the above example will invalidate the signature.

Source code and next steps

As usual, you can find executable examples in my samples repository. However, to use this in anger, you’ll need to create your own EC key and load it into your app. Luckily, I have articles for that too:

For SAML, ECDSA XML digital signatures, alongside RSA-PSS, are a requirement for eIDAS.