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:
- Part 1: Why build another licensing system?
- Part 2: Defining the desired behaviour
- Part 3: How to create and validate a license
- Part 4: How to implement custom validation logic
- Part 5: Real world usage patterns
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.0000000+00:00</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:
- 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. - 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. - Finally, you need to pass the
LicenseCriteria
and the Private Key to theServerLicenseGenerate
, which will use these two inputs to create aServerLicense
, 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.
Sign up to Azure Weekly to receive Azure related news and articles direct to your inbox every Monday, or follow @azureweekly on Twitter.