Skip to content
Howard van Rooijen By Howard van Rooijen Co-Founder
Endjin.Licensing - Part 3: How to create and validate a license

We've open sourced a lightweight licensing framework we've been using internally over the last couple of years. In a 5 part series I'm covering the motivation behind building it, how you can use it, how you can extend it for your own scenarios and how you could use it in a real world situation:

In the last part I covered off how we used Specflow to create a series of executable specifications to described the desired behaviour of the licensing framework – but we didn't touch upon and of the implementation details. Essentially there are 5 main parts to the licensing framework:

  • A Private Key.
  • A Public Key, generated from the Private Key:
<RSAKeyValue>
  <Modulus>lZHF2f9PuYZIfsKCXbUmfHM/JwT4UstHRzZ+EpQixbyaIajEBvA7v2ZdUykF72bTzOMA2CxA+C9yxqpjkG/G4DHcvovN2suvcsIOgnydkVSBzYg38bOohS1ycSMUUGX0ilyeQB2v+s52LJknaMKKnKA6sHDz9Y5H+vrJ2G+fe6qhl87wW+oKq8UvfEAevFbwOvljajoduhZQHJYNHZrKfQK29s1WoUMLafXMtofExj1PTIDHGt1nXre6UY9FSETy78D7S6wVc7j0jjM57PBgoalqfacrb/VB701wNhhUPUrzt+R7VtealVsxZB3cJo213oRJiz2/byfAueEKBWnMRR1UUm9Uuxa8kbvoRutKI2Fm/6UdcHlbxvJ4GM1sCz+ijz8/zwWFx9UQpgpqzyJ2YH9CENbIW7IPqoUqt029Us9JGmHNbyE8V/oIoccUfvFmmh2n+zD2GxkViBsM8fbGh+fl1i9y0cmL2EaM0yWV+AHADxANS58BfQk7IVj7XHxP9s2AlEgIKslm8V14ar+saocoAL2mBM9sUbucJ5v8KgTviUYsPFFKv+YpWiBSGG3EY6tGO3QUvv+dK+mibFMby92VbRm27FHlxCX9OXzDYkrTTKTF0vlhtsZiQYCNQSwzmxEZpjFV/lhfq/+zJBcuoruXEGleRl/rpesVJrwGHAc=</Modulus>
  <Exponent>AQAB</Exponent>
</RSAKeyValue>

The License, cryptographically signed using SHA256, to ensure it's tamper proof:

<License>
  <Id>dcc9b07f-88ba-4fdb-be7f-8ed73f3fd81f</Id>
  <ExpirationDate>2015-02-28T23:59:59</ExpirationDate>
  <IssueDate>2015-02-23T16:38:24.8555688+00:00</IssueDate>
  <Type>Subscription</Type>
  <LicensedCores>2</LicensedCores>
  <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
    <SignedInfo>
      <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315" />
      <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1" />
      <Reference URI="">
        <Transforms>
          <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
        </Transforms>
        <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
        <DigestValue>Nfakr5p0hQxec++oQ9CcmMeRR0M=</DigestValue>
      </Reference>
    </SignedInfo>
    <SignatureValue>SAXwFSU7g/NzUIabRdki/5RirwMQSzUQJOpkrTyvj5NL6xKlkYaZJUeGxO6eOwJQobQORuEhkaJfxNnskKVor2wDbcBluFIQVIBfAS+y+NqzBufvFPOYI6TO83Xq16/oz5vC5BI7xbtbtmCd+Q0Kz5ymcPIXUzyueA52z0IqfHswhXztBBamRmOSSumCBrTxccggt/iMZu690puG9wBvKAy3UOImWDcx6lHyq/VUBG4zeNaR75tRDJ9xMVy2l4ukl5Y4qnJe2iZsmTAQKoZMj0jjBYjWYX3T8SEx6MhuJpnvOwD5eIVm3s1bw/AelwRwO+mTDtPQYaYKDLndvENJgLVQztHPXFn913AbFdqHpg7UUueaG3hItSjm1xs7WgcDBGc5SY1RGtWllXAOr7DLFFzeMQZJIE6ejJVO3Y8gTSmWP8NxFFTpIjRdK9RX6V3bMXsG/uhwbHD/nVRT6YOWZLhC2G9zuzSqDFtIN+G3OTbMX5WjKicr5FXHpFBsch5uCj9Zlu7CsaIPv0rA9VoDrEl37he0c3qmoC7PXDG08gvfByAVnQQI4MSolD3/+srxl1aW+NHpGJIs92ON5bmO33kzHxcAjOQoLJNr1sPM+5ZLeRwj8RVpWTMjIrE2iqZuQVgLy7dHtkdwRg9Hg6iJrGXvR7jqtgxvaHUKVcols2U=</SignatureValue>
  </Signature>
</License>

License Validation Rules which are used to examine the license and determine whether it's valid for any given set of conditions:

public interface ILicenseValidationRule
{
    void Validate(LicenseCriteria licenseCriteria);
}

And finally, the License Validator, which takes in a client license, the public key and the list of validation rules and then performs the validation, if the license is invalid, exceptions are thrown detailing the ways in which the license is invalid:

public interface ILicenseValidator
{
    void Validate(IClientLicense clientLicense, ICryptoKey publicKey, IEnumerable<ILicenseValidationRule> validationRules);

    LicenseCriteria LicenseCriteria { get; set; }
}

To generate a license 3 steps are required:

  1. You need to create your LicenseCriteria – this is the domain object that contains the details of the license; the id, issue date, expiration date, license type and any custom metadata you wish to add.
  2. A Private Key is required. This is generated by the PrivateKeyProvider, which can also be used to recreate the private key from a string – a useful scenario as you'll want to generate the private key once per major version of your application, store it in string form in a repository of some kind, and rehydrate it when you need to generate a new license.
  3. Finally, you need to pass the LicenseCriteria and the Private Key to the ServerLicenseGenerate, which will use these two inputs to create a ServerLicense, which is an object that contains the Public Key, Private Key, License Criteria and License – it's an object you would most likely want to store against the user for a full audit trail of the license created for them.

The code snippet below shows how how to generate a Public Key and License and write them out to the file system:

public static void Main(string[] args)
{
    var dataDirectory = @"..\..\..\..\LicenseData".ResolveBaseDirectory();
    var publicKeyPath = @"..\..\..\..\LicenseData\PublicKey.xml".ResolveBaseDirectory();
    var licensePath = @"..\..\..\..\LicenseData\License.xml".ResolveBaseDirectory();

    if (!Directory.Exists(dataDirectory))
    {
        Directory.CreateDirectory(dataDirectory);
    }

    var licenseCriteria = new LicenseCriteria
    {
        ExpirationDate = DateTimeOffset.UtcNow.LastDayOfMonth().EndOfDay(),
        IssueDate = DateTimeOffset.UtcNow,
        Id = Guid.NewGuid(),
        MetaData = new Dictionary<string, string> { { "LicensedCores", "2" } },
        Type = "Subscription"
    };

    var privateKey = new RsaPrivateKeyProvider().Create();
    var serverLicense = new ServerLicenseGenerator().Generate(privateKey, licenseCriteria);
    var clientLicense = serverLicense.ToClientLicense();

    // In a real implementation, you would embed the public key into the assembly, via a resource file
    File.WriteAllText(publicKeyPath, privateKey.ExtractPublicKey().Contents);

    // In a real implementation you would implement ILicenseRepository
    File.WriteAllText(licensePath, clientLicense.Content.InnerXml);

    Console.WriteLine(Messsages.LicenseGenerated, dataDirectory);
    Console.WriteLine(Messsages.PressAnyKey);
            
    Console.ReadKey();
}

Validating a license is also a straight forward process. The first step requires us to retrieve the Public Key and License we generated in the previous step, can pass that to a helper method which performs the validation:

public static void Main(string[] args)
{
    // In a real implementation the public key would be embedded in the assembly 
    // possibly via an embedded resource
    var publicKeyPath = @"..\..\..\..\LicenseData\PublicKey.xml".ResolveBaseDirectory();
    
    // You could also either load the license from the file system or deliver it
    // on demand from a web endpoint
    var licensePath = @"..\..\..\..\LicenseData\License.xml".ResolveBaseDirectory();

    if (!File.Exists(publicKeyPath) || !File.Exists(licensePath))
    {
        Console.WriteLine(Messages.RunServerAppFirst);
        Console.WriteLine(Messages.PressAnyKey);
        Console.ReadKey();

        Environment.Exit(-1);
    }

    ValidateLicense(publicKeyPath, licensePath);

    Console.WriteLine(Messages.NoLicenseViolations);
    Console.WriteLine(Messages.PressAnyKey);

    Console.ReadKey();
}

Next we need to rehydrate the Public Key and Client License back into domain objects, then we need to specify the collection of ILicenseValidationRule we are going to use to validate the license. The final step is to pass in the ClientLicense, Public Key and  license validation rules into the LicenseValidator to perform the validation. If the license validates the Validate() method will return, if there are any problems either a InvalidLicenseException or AggregateException containing a collection of LicenseViolationException will be thrown. The majority of the code in the snippet below performs the error handling and conversion of exceptions into messages that can be shown to the user:

private static void ValidateLicense(string publicKeyPath, string licensePath)
{
    var publicKey = new PublicCryptoKey { Contents = File.ReadAllText(publicKeyPath) };
    var clientLicense = ClientLicense.Create(File.ReadAllText(licensePath));

    var violations = new List<string>();

    try
    {
        var licenseValidationRules = new List<ILicenseValidationRule>
        {
            new LicenseHasNotExpiredRule(), 
            new ValidNumberOfCoresLicenseRule()
        };

        new LicenseValidator().Validate(clientLicense, publicKey, licenseValidationRules);
    }
    catch (InvalidLicenseException exception)
    {
        violations.Add(exception.Message);
    }
    catch (AggregateException ex)
    {
        var innerExceptions = ex.InnerExceptions;

        foreach (var exception in innerExceptions)
        {
            if (exception is LicenseViolationException)
            {
                violations.Add(exception.Message);
            }
        }
        
        // If you've got to this point and there are no violations,
        // something very undesirable is happening, so bubble it up.
        if (!violations.Any())
        {
            throw;
        }
    }
    catch (Exception)
    {
        violations.Add(Messages.UnknownLicenseError);
    }

    if (violations.Any())
    {
        Console.WriteLine(Messages.LicenseViolationsEncountered);
        
        foreach (var violation in violations)
        {
            Console.WriteLine(" - " + violation);
        }

        Console.WriteLine(Messages.PressAnyKey);
        Console.ReadKey();

        Environment.Exit(-1);
    }
}

In the next part of the series, I'll take you through a step by step guide for implementing custom validation logic.

The Introduction to Rx.NET 2nd Edition (2024) Book, by Ian Griffiths & Lee Campbell, is now available to download for FREE.

@HowardvRooijen

Programming C# 10 Book, by Ian Griffiths, published by O'Reilly Media, is now available to buy.

Sign up to Azure Weekly to receive Azure related news and articles direct to your inbox every Monday, or follow @azureweekly on Twitter.

Howard van Rooijen

Co-Founder

Howard van Rooijen

Howard spent 10 years as a technology consultant helping some of the UK's best known organisations work smarter, before founding endjin in 2010. He's a Microsoft ScaleUp Mentor, and a Microsoft MVP for Azure and Developer Technologies, and helps small teams achieve big things using data, AI and Microsoft Azure.