Using XML signing for licensing

Sample implementation of licensing infrastructure using .NET XML signing capabilities

Author: Sergey Sorokin
Last updated: February 8, 2013

Introduction

Let's assume you sell some .NET-based software and would like to restrict software capabilities according to the amount of payments you receive from your customers for the license you sell them. Let's also assume you would like to distribute a free trial license for your software that will allow customers to try some basic functionality of your software. This article suggests using .NET XML signing capabilities for building licensing infrastructure. We will discuss the development of:

This article doesn't discuss in detail .NET XML signing API details, this is covered pretty well in other sources. Source code for both applications and all sample keys can be downloaded here (~30kb).

License information

You have to decide what to put into your license, here is my suggestion:

public class LicenseInfo
{
  public string ProductName;
  public Guid LicenseId;
  public string LicenseeName;
  public string LicenseeContact;
  public string IssueDate;
  public string ExpiryDate;
  public int MaxCores;
  public bool IsTrial;
}

All fields are self-explanatory. MaxCores is the maximum allowed number of processor cores for the machine our licensed application can run.

License Manager class

This is the piece of code that is used by both the LicenceTool and sample LicensedApplication and it performs license creation, signing and verification. License Manager does not contain any sensitive information (like private keys), but it's worth keeping its code well-protected.

License creation

License creation is just a matter of producing a XML file with trial settings:

<?xml version="1.0" encoding="utf-8"?>
<LicenseInfo xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <ProductName>Acme Product #1</ProductName>
  <LicenseId>00000000-0000-0000-0000-000000000000</LicenseId>
  <LicenseeName>For trial and educational use only</LicenseeName>
  <LicenseeContact>None</LicenseeContact>
  <IssueDate>0001-01-01 00:00:00</IssueDate>
  <ExpiryDate>2100-01-01 00:00:00</ExpiryDate>
  <MaxCores>2</MaxCores>
  <IsTrial>true</IsTrial>
</LicenseInfo>

License signing

License Manager knows how to sign XML:

public static void SignLicense(string licenseFilePath, string pfxFilePath, string pfxFilePassword)
{
// Update IssueDate and Id
LicenseInfo li;
XmlSerializer xmlSerializer = new XmlSerializer(typeof(LicenseInfo));
using (XmlReader xmlReader = XmlReader.Create(licenseFilePath))
{
  li = (LicenseInfo)xmlSerializer.Deserialize(xmlReader);
}

li.LicenseId = Guid.NewGuid();
li.IssueDate = DateTime.UtcNow.ToString(DateFormatString);

// Save it back to the file
SaveLicense(li, licenseFilePath);

// Get certificate
X509Certificate2 certificate = new X509Certificate2(pfxFilePath, pfxFilePassword);
RSACryptoServiceProvider rsaCsp = (RSACryptoServiceProvider)certificate.PrivateKey;

// Load license xml again
XmlDocument xmlDoc = new XmlDocument();
xmlDoc.Load(licenseFilePath);

// Create signed xml object
SignedXml signedXml = new SignedXml(xmlDoc);
signedXml.SigningKey = rsaCsp;

// Create a reference to be signed.
Reference reference = new Reference();
reference.Uri = "";

XmlDsigEnvelopedSignatureTransform env = new XmlDsigEnvelopedSignatureTransform();
reference.AddTransform(env);
signedXml.AddReference(reference);

// Compute signature
signedXml.ComputeSignature();

// Embed signature into the doc
XmlElement xmlDigitalSignature = signedXml.GetXml();
xmlDoc.DocumentElement.AppendChild(xmlDoc.ImportNode(xmlDigitalSignature, true));

XmlWriterSettings xmlWriterSettings = new XmlWriterSettings()
{
  Encoding = new UTF8Encoding(false),
  ConformanceLevel = ConformanceLevel.Document,
  Indent = true
};
using (XmlWriter w = XmlWriter.Create(licenseFilePath, xmlWriterSettings))
{
  xmlDoc.WriteTo(w);
}

// Export public key so we can hardcode it in LicenseManagerBase.PublicKey
string publicKey = certificate.PublicKey.Key.ToXmlString(false);
using (StreamWriter w = new StreamWriter(PublicKeyFileName))
{
  w.Write(publicKey);
}
}

I will not discuss the above code in detail, please find information about XML signing in other sources. This code adds a Signature element to your XML license:

<Signature ...>
  <SignedInfo>
    <CanonicalizationMethod Algorithm="..." />
    <SignatureMethod Algorithm="." />
    <Reference URI="">
      <Transforms>
        <Transform Algorithm="..." />
      </Transforms>
      <DigestMethod Algorithm="..." />
      <DigestValue>...</DigestValue>
    </Reference>
  </SignedInfo>
  <SignatureValue>...</SignatureValue>
</Signature>

License verification

Of course, License Manager should know how to check if the license is properly signed:

public static LicenseInfo GetLicenseInfo(string licenseFilePath, string publicKey)
{
// Get public key
RSACryptoServiceProvider csp = new RSACryptoServiceProvider();
csp.FromXmlString(publicKey);

// Get license file
if (!File.Exists(licenseFilePath))
{
  throw new CryptographicException("License file not found.");
}

XmlDocument xmlDoc = new XmlDocument();
try
{
  xmlDoc.Load(licenseFilePath);
}
catch (XmlException ex)
{
  throw new CryptographicException("Cannot load license file, bad XML. " + ex.Message);
}

// Find the "Signature" node and create a new XmlNodeList object.
XmlNodeList nodeList = xmlDoc.GetElementsByTagName("Signature");

if (nodeList.Count <= 0)
{
  throw new CryptographicException("No Signature element was found in the license document.");
}

if (nodeList.Count >= 2)
{
  throw new CryptographicException("More that one signature was found for the license document.");
}

SignedXml signedXml = new SignedXml(xmlDoc);

// Load the first node.
signedXml.LoadXml((XmlElement)nodeList[0]);

// Check the signature and return the result.
if (!signedXml.CheckSignature(csp))
{
  throw new CryptographicException("License document has invalid signature.");
}

// Derserialize LicenseInfo
XmlSerializer xmlSerializer = new XmlSerializer(typeof(LicenseInfo));
LicenseInfo li = (LicenseInfo)xmlSerializer.Deserialize(new XmlTextReader(new StringReader(xmlDoc.OuterXml)));

// Check if the license has valid data
...
return li;
}

Again, this code is just a sample that uses .NET XML signing API and is pretty straightforward.

See licensing in action

After building licensing tool and sample licensed application, let's go to the test folder and simulate licensing workflow.

Trial licensing process

Let's prepare a license that will be distributed with the trial version of your product.

1. Create a new license file Acme.License.xml:

Acme.LicenseTool.exe create

2. Do not modify license info.

3. Sign license file (the tool will assume its name is Acme.License.xml):

Acme.LicenseTool.exe sign /cert:..\keys\Acme.Certificate.pfx

You may forget to change trial settings before signing the license, so the tool will display a reminder if some settings look like trial.

4. Run Acme.LicenseTool.exe without parameters to see signed license info.

Running licensed application in trial mode

Now let's assume we are on the customer's end and using licensed application that has a restriction on the number of processor cores.

1. Run Acme.LicensedApplication.exe. If this is atrial license with number of cores is 2, and the actual number of cores on your machine is more than that (which is very likely these days), the licensed application will display the license violation warning.

2. Modify Acme.License.xml - set MaxCores to, say 16. Run Acme.LicensedApplication.exe again. The application will complain about invalid license file signature and will consider this deployment a trial, which automatically sets maximum allowed number of cores to 2.

3. Remove Acme.License.xml and run Acme.LicensedApplication.exe again. The application will complain about missing license file and will default to trial mode.

Commercial licensing process

Let's pretend we have a paying customer FooBar Inc, and we need to prepare a commercial license for them. Repeat the steps 1 to 4 of the trial licensing process, but on step 2, replace the maximum number of cores to, say 16, and update customer information:

<LicenseeName>FooBar Inc.</LicenseeName>
<LicenseeContact>john.doe@foobarinc.com</LicenseeContact>
<MaxCores>16</MaxCores>
<IsTrial>false</IsTrial>

Please note that this time, Acme.LicenseTool.exe did not complain about trial settings when signing the license, and displayed values in green on step 4. This means this is a valid non-trial license.

Running licensed application with commercial license

Run Acme.LicensedApplication.exe. If the number of cores on your machine is 16 or less, it will report that license verification passed successfully.

What's next (read: what's missing)

This article does not cover:

When you cover all of the above, you should be good to go. Good luck!