Generating secure signed licenses using C# and Standard.Licensing

When it comes to licensing software that is installed on an end user’s device, one of the most secure approaches that we can take as software developers, is to generate and subsequently validate a digitally signed license.

With this approach, a cryptographically secure public/private key pair is created that is specific to the software being developed. This key pair, along with a passphrase, is then used to sign licenses that are issued to clients with a unique signature. The licenses are typically generated automatically on the server-side via a remote web service, or via an admin application that only the software vendor has access to. The software that is being licensed will usually check the license file/key on start-up (and often periodically while the software is running) to ensure that the license signature is valid according to the public key.

One of the primary benefits of signed licenses is that once they are issued they cannot be altered by the client without invalidating them. As a result, we can trust that the data contained within the license has not been tampered with.

When developing .NET applications, we have several options to choose from that help make it easier to generate and validate signed licenses for our software. The one I am covering in this article is the Standard.Licensing library.

Standard.Licensing

Standard.Licensing is a project written in C# that has been forked from a once very popular project named Portable.Licensing.

The main reason for Standard.Licensing being developed was to add support for newer framework targets such as .NET Standard, .NET Core, and .NET 5+, as opposed to the original .NET Framework. Having said that, Standard.Licensing still maintains support for .NET Framework v4.0 or above.

Standard.Licensing logo
Source: Standard.Licensing GitHub page

Standard.Licensing uses the Elliptic Curve Digital Signature Algorithm (ECDSA) to cryptographically sign licenses. Signing a license guarantees that any attempts to tamper with the license after it has been issued will render its signature invalid.

The concept of signing is fundamental to the security of the licensing solution that Standard.Licensing provides. It allows us to embed information within a license, such as purchased software modules, that we can trust implicitly due to the digital signature.

In the sub-sections that follow I will guide you through how to use Standard.Licensing in your .NET application.

NuGet package

Before you can start using Standard.Licensing, you’ll need to install the Standard.Licensing NuGet package into your project, either via the command line or via an IDE such as Visual Studio or Visual Studio Code.

Since the NuGet package targets .NET Standard it will work in .NET Standard class libraries, .NET Core libraries, and the latest .NET at the time of writing which is .NET 6.0.

License key generation

Before you can start issuing new licenses and validating the same, you’ll first need to generate a public/private key pair, as demonstrated in the following code sample.

using Standard.Licensing.Security.Cryptography;
 
KeyGenerator keyGenerator = KeyGenerator.Create();
KeyPair keyPair = keyGenerator.GenerateKeyPair();
string
passPhrase = "6mML7vhZGiOv2$cGriVB^YBwEWG6aSmOzyko41Zet*GVqL%zqN"; string privateKey = keyPair.ToEncryptedPrivateKeyString(passPhrase); string publicKey = keyPair.ToPublicKeyString(); Console.WriteLine("Private key: {0}", privateKey); Console.WriteLine("Public key : {0}", publicKey);

Note that using statements have been added to the top of the code samples in this article to help clarify the required namespaces. If you’re following along, you’ll need to move each new using statement to the top of your current file in order for the code to compile.

The above code uses the static KeyGenerator.Create method to create a KeyGenerator object instance. The GenerateKeyPair method is then used to generate a KeyPair object.

A private key string is created by passing a secret passphrase into the ToEncryptedPrivateKeyString method and the public key string is then set by calling the ToPublicKeyString method.

Please be sure to update the passphrase to your own unique value.

Note that in a production application I recommend storing the passphrase and private key as configuration secrets on the server-side of the licensing solution.

It is very important that the private key and the passphrase that is used to generate it are never distributed to the user and should never be included in the source code of the client-side application.

With that cleared up, let’s move on to creating a license using the keys that we have generated.

Creating a license

Standard.Licensing allows us to create licenses in a fluent manner by chaining together methods that return the ILicenseBuilder interface, as shown below.

using Standard.Licensing;

DateTime expiryDate = DateTime.UtcNow.Date.AddDays(30); string deviceIdentifier = Environment.MachineName; string customerName = "Jonathan Crozier"; License newLicense = License.New()     .WithUniqueIdentifier(Guid.NewGuid())     .ExpiresAt(expiryDate)     .WithAdditionalAttributes(new Dictionary<stringstring>     {         { "DeviceIdentifier", deviceIdentifier }     })     .LicensedTo((c=> c.Name = customerName)     .CreateAndSignWithPrivateKey(privateKey, passPhrase);

The above code creates a license with a unique GUID identifier by passing the result of the Guid.NewGuid method into the WithUniqueIdentifier method.

An expiry date is configured for the license using the ExpiresAt method. In this case, the license will expire after 30 days have passed. This is entirely optional and you are free to create licenses that never expire.

Note that you can also create a trial license by adding a method call to .As(LicenseType.Trial).

The WithAdditionalAttributes method accepts a Dictionary with string keys and string values. As the name suggests, this method can be used to add additional attributes that will be stored as part of the created license. These attributes can then be used to perform specific checks when validating the license later.

The LicensedTo method is used to store information regarding the user to whom the license has been issued. It is possible to set the user’s name, company name, and email address, if required.

Lastly, the CreateAndSignWithPrivateKey method is used to finalise the creation of the license and sign it using the specified private key.

License formats

When serialised, licenses created by Standard.Licensing are represented as XML.

Following on from the code in the previous sub-section, we can use the ToString method on a License object to obtain its XML representation as shown below.

string licenseXml = newLicense.ToString();
 
Console.WriteLine(licenseXml);

The output of the above code is as follows.

<License>
<Id>ffac2078-cb01-4f94-94e8-6fbb6c5f1bf8</Id>
<Expiration>Thu, 20 Oct 2022 00:00:00 GMT</Expiration>
<LicenseAttributes>
<Attribute name="DeviceIdentifier">SPECTRE-007</Attribute>
</LicenseAttributes>
<Customer>
<Name>Jonathan Crozier</Name>
</Customer>
<Signature>MEUCIFDnFrg/lBnbXoM0hTX3TJB7j20+LUSc/7CuKqd2tjt/AiEAs6s2K0mJOQLmT6TZ2oT8kdCtWiHl+bb/Sb559roZmv0=</Signature>
</License>

As you would expect, since the license format is XML, it is also straightforward to save the license to an XML file, as per the following code sample.

using System.Text;
using System.Xml; using (var xmlWriter = new XmlTextWriter("license.xml", Encoding.UTF8)) {     newLicense.Save(xmlWriter); }

Another option for storing the license is to encode its XML representation as a Base64 or Base64 URL encoded string.

By using this approach, you can treat the license more like a license key as opposed to a license file. This can work well for lots of scenarios, for example, you could store the license in a text-based column in a database and/or pass the license key to and from a licensing server via HTTP.

For Base64 URL encoding, you could use the Base64UrlEncoder class that was documented in my earlier Base64 URL encoding using C# post, as shown below.

string licenseKey = Base64UrlEncoder.Encode(newLicense.ToString());

Console.WriteLine(licenseKey);

Note that you’ll need to add an appropriate using statement for the namespace that wraps the Base64UrlEncoder class.

The output of the above code is as follows.

PExpY2Vuc2U-DQogIDxJZD5mZmFjMjA3OC1jYjAxLTRmOTQtOTRlOC02ZmJiNmM1ZjFiZjg8L0lkPg0KICA8RXhwaXJhdGlvbj5UaHUsIDIwIE9jdCAyMDIyIDAwOjAwOjAwIEdNVDwvRXhwaXJhdGlvbj4NCiAgPExpY2Vuc2VBdHRyaWJ1dGVzPg0KICAgIDxBdHRyaWJ1dGUgbmFtZT0iRGV2aWNlSWRlbnRpZmllciI-U1BFQ1RSRS0wMDc8L0F0dHJpYnV0ZT4NCiAgPC9MaWNlbnNlQXR0cmlidXRlcz4NCiAgPEN1c3RvbWVyPg0KICAgIDxOYW1lPkpvbmF0aGFuIENyb3ppZXI8L05hbWU-DQogIDwvQ3VzdG9tZXI-DQogIDxTaWduYXR1cmU-TUVVQ0lGRG5GcmcvbEJuYlhvTTBoVFgzVEpCN2oyMCtMVVNjLzdDdUtxZDJ0anQvQWlFQXM2czJLMG1KT1FMbVQ2VFoyb1Q4a2RDdFdpSGwrYmIvU2I1NTlyb1ptdjA9PC9TaWduYXR1cmU-DQo8L0xpY2Vuc2U-

The above string is 568 characters in length, which while long, is more than manageable for both storage and transport across the wire where required.

Loading a license

Depending on how you have chosen to store the license you can either load the license from an XML file using a stream or you can load the license based on its XML string representation directly.

To load from a file you can pass a Stream or XmlTextReader object instance to one of the static License.Load method overloads, as shown in the code sample below.

License license;
 
using (var xmlReader = new XmlTextReader("license.xml"))
{
    license = License.Load(xmlReader);
}

To load from an XML string that has been Base64 URL encoded, use the code below.

License license = License.Load(Base64UrlEncoder.Decode(licenseKey));

Note that the code above assumes that there is an existing variable in scope named licenseKey which contains the license encoded as a Base64 URL encoded string.

Validating a license

Once the license has been loaded by the client-side application, you will need to validate it before allowing users to access your software package.

Below is an example of how to validate the license that was previously created in the Creating a license sub-section.

using Standard.Licensing.Validation;

string currentDeviceIdentifier = Environment.MachineName; var validationFailures =    license.Validate()            .ExpirationDate()            .And()            .Signature(publicKey)            .And()            .AssertThat(lic => // Check Device Identifier matches.                lic.AdditionalAttributes.Get("DeviceIdentifier"== currentDeviceIdentifier,                new GeneralValidationFailure()                {                    Message      = "Invalid Device.",                    HowToResolve = "Contact the supplier to obtain a new license key."                })            .AssertValidLicense()
           .ToList(); if (validationFailures.Any()) {     throw new UnauthorizedAccessException(validationFailures.First().Message); }

The above code uses the Validate method to start the validation chain and demonstrates how to validate a license that has a built-in expiration date by adding a call to the ExpirationDate method.

Other checks are added by calling the And method, followed by the method related to the check to perform.

The signature of the license is checked by calling the Signature method and passing in the relevant public key that is associated with the license.

Note that it is ok to distribute the public key for the license along with the client-side application. The public key is non-sensitive data and needs to be accessible to validate the license properly.

The AssertThat method is used to perform custom validation logic on attributes of the license such as its Type, Quantity, and ProductFeatures properties. In the above code, the value of an additional attribute named ‘DeviceIdentifier’ is checked to ensure that the current device identifier matches the device identifier stored in the license file.

Note that the device identifier assertion in the above code is very rudimentary and is purely intended to demonstrate how additional attributes stored within a license can be checked. For a more meaningful device identifier check, consider calculating the device identifier using a library such as DeviceId, as documented in my What makes a good device identifier? post.

Lastly, the AssertValidLicense method is called to complete the license check and returns a collection of validation failures which can be inspected. If any validation failures occur, the application should inform the user that their license is invalid and either prompt for a license file/key or exit.

Additional considerations

Standard.Licensing is a great little library for creating secure licenses that cannot be altered without corrupting the signature of the license, allowing the licenses to be safely stored on the client side.

However, client-side security is a hard problem and it is not possible to fully protect applications that require licensing based on client-side checks alone.

If you want to increase the security of your application further in regards to licensing, you may want to consider combining the usage of signed licenses created by Standard.Licensing with remote license checks.

At a very high level, this would involve creating a licensing server API that implements endpoints to check and renew licenses. Such a server could connect to a database holding the details of customers and/or devices that are currently licensed, providing you with a way of validating that only users who have been issued a license from your server can use your software.

By using the Base64 URL encoding described previously, licenses with a built-in expiry date could be transferred to the client easily and could be used to validate that the client has a valid license in cases where the client is offline for a period. Stale licenses could then be renewed once the client is back online again to renew the offline grace period.

Summary

In this article, I have documented how to use the Standard.Licensing library to create, save, load, and validate signed licenses.

I started by directing you to the relevant NuGet package and then looked at how to generate a public/private key pair which can be used to create secure licenses that cannot be tampered with.

After creating the licenses, I explained how they can be saved to different formats and loaded again when needed.

Finally, I demonstrated how to validate licenses and discussed some additional considerations that could help to make the licensing of your software more robust, depending on your particular security requirements.


I hope you enjoyed this post! Comments are always welcome and I respond to all questions.

If you like my content and it helped you out, please check out the button below 🙂

Comments

Anonymous

This was very helpful!

April 15, 2023

Jonathan Crozier

Hey, I’m glad you found it helpful 🙂

April 15, 2023

Mike Williams

Very helpful, just had to do a little tweak to enable displaying of the validation results to the user as our end user app is in .Net Framework (min version 4.6.2). It kept on crashing, presumably due to validationFailures being enumerated twice

var validationResult = new Dictionary();

foreach (var failure in validationFailures)
{
validationResult.Add(failure.Message, failure.HowToResolve);
}

if (validationResult.Count > 0)
{
foreach (var kvp in validationResult)
{
Console.WriteLine(kvp.Key);
Console.WriteLine(” ” + kvp.Value);
}
Console.WriteLine(“Validation failed”);
}
else
{
Console.WriteLine(“Validated”);
}

Console.WriteLine(“Done”);
Console.ReadLine();

April 17, 2023

Jonathan Crozier

Thanks for the comment Mike and for sharing your solution!

April 18, 2023

Karl

If I modify the content of the public key, the license can still be verified.
Even if I modify the signature string in the license file, the license can still be verified. Is my usage incorrect?

Below is the test code I wrote based on your blog. Can you help me find any issues with the code?

KeyGenerator keyGenerator = KeyGenerator.Create();
KeyPair keyPair = keyGenerator.GenerateKeyPair();
string passPhrase = “6mML7vhZGiOv2$cGriVB^YBwEWG6aSmOzyko41Zet*GVqL%zqN”;
string privateKey = keyPair.ToEncryptedPrivateKeyString(passPhrase);
string publicKey = keyPair.ToPublicKeyString();

File.WriteAllText(“C:\\privateKey.txt”, privateKey.ToString(), Encoding.UTF8);
File.WriteAllText(“C:\\publicKey.txt”, publicKey.ToString(), Encoding.UTF8);

DateTime expiryDate = DateTime.UtcNow.Date.AddDays(30);
string deviceIdentifier = Environment.MachineName;
string customerName = “Karl Zhang”;
string customerEmailAddress = “”;

License newLicense = License.New()
.WithUniqueIdentifier(Guid.NewGuid())
.ExpiresAt(expiryDate)
.WithAdditionalAttributes(new Dictionary
{
{ “DeviceIdentifier”, deviceIdentifier }
})
.LicensedTo(customerName, customerEmailAddress)
.CreateAndSignWithPrivateKey(privateKey, passPhrase);

File.WriteAllText(“C:\\License.lic”, newLicense.ToString(), Encoding.UTF8);

string xmlString = File.ReadAllText(“C:\\License.lic”);
License license = License.Load(xmlString);

string currentDeviceIdentifier = Environment.MachineName;

string publicKey = File.ReadAllText(“C:\\publicKey.txt”);
var validationFailures =
license.Validate()
.ExpirationDate()
.And()
.Signature(publicKey)
.And()
.AssertThat(lic => // Check Device Identifier matches.
lic.AdditionalAttributes.Get(“DeviceIdentifier”) == currentDeviceIdentifier,
new GeneralValidationFailure()
{
Message = “Invalid Device.”,
HowToResolve = “Contact the supplier to obtain a new license key.”
})
.AssertValidLicense();

if (validationFailures.Any())
{
throw new UnauthorizedAccessException(validationFailures.First().Message);
}

May 23, 2023

Jonathan Crozier

Hi Karl, the code seems to be fine. I’ve tried your version of the code out and when I change the public key or something in the license file the license cannot be verified. Are you able to explain in more detail how you are testing this?

May 23, 2023

Eddy

1. Generate license key
2. License creation

The identification device generated in the code number 2 in the above order must be the unique identifier of the client. What should I do?

Finally, the code understood that license authentication is done on the client.

I am thinking about Step 2 in MS .NET 6.0 WinForms.

July 24, 2023

Jonathan Crozier

Hi Eddy,

The answer to your question will depend on how you are planning to issue licenses to clients.

As an example, the client application could first calculate its unique device identifier (see What makes a good device identifier?) and pass it up to a web service which then generates the license on the server and returns it as an encoded license key to the client.

July 25, 2023

Eddy

I understand it. Thank you ^^

August 2, 2023

Wally

I am a little confused.
If I want the verification to be entirely on the client side do I need to supply the user with the license file and the public key?

July 28, 2023

Jonathan Crozier

The license must be generated on the server so that it can be signed by the private key. The client must have access to the license and will need the public key to validate it. The public key can be shipped with the client software, but the private key is a secret and therefore must only be accessible on the server.

August 2, 2023

Wally

Thank you. I get it.
I have another question.
There is an option to create a dictionary of Product Features. Adding Product Features to the license simply adds the dictionary items to the xml file. How does this keep the end user from simply editing the license file to have a Yes instead of a No for a license option?

August 11, 2023

Jonathan Crozier

Since the license is cryptographically signed with the private key on the server, any changes made to the license by the user would invalidate its signature. This is the fundamental principle behind the Standard.Licensing library. I hope that makes sense, let me know if you have any other questions!

August 14, 2023

Lieuwe

If in your example in validating a license the deviceIdentifier is something else, so the validation fails, you get an exception “Sequence contains no elements”.
To avoid this you should end the validationFailures with .ToList().
Everything else: great article!

March 11, 2024

Jonathan Crozier

Hey Lieuwe, thank you for the comment! I will check this out and get back to you.

March 11, 2024

Jonathan Crozier

Lieuwe, you are quite correct, this is the same issue that was previously pointed out by Mike Williams in an earlier comment.

I have updated the code sample to include a call to the ToList() method to fix the exception.

Thanks you for bringing this to my attention again 🙂

March 21, 2024