Infrastructure for secure license management in .NET applications
This small project will help you to manage licensing in your .NET applications. To check for license may be as simple as:
if (!LicenseManager.IsLicenseValid)
Environment.Exit(1);
Please note that it's still a work in progress (inspired from Stack Overflow Where can I store (and manage) Application license information?) then many things need to be completed (see issues) and code must be reviewed and simplified. All documentation is still a TO DO, please refer to both source code and cited SO post. Any help on this (also for documentation) is more than welcome!
Client: the machine where application you want to protect is deployed.
Server: the machine where you create licenses. It may be a public accessible web server or simply your development machine but your customers must not have direct access to its content.
Contact: it's a text string generated at client side which contains some unique identifiers used to uniquely tie one license to a specific machine.
License: it's a text string generated at server side and used client side which contains information about the license (to whom you licensed your application, its expiration and which features are enabled). License is generated from a contact and it's indissolubly tied to a specific machine.
This project cannot be used as-is, to be effective you must include full source code in your repository and change it as described here, read carefully these notes. Project is separated in groups:
Radev.Licensing.dll
. This assembly also
contains code used by server components.Radev.Licensing.Server.dll
. Your customers must never have an accessible copy of this assembly because
it contains your private key.LicenseManager.cs
but
it must be customized for your specific use.To start using this project you have to carefully follow these steps:
Radev.Licensing\Keys.cs
and
both public and private keys in Radev.Licensing.Server\Keys.cs
.LicenseManager.cs
in each assembly where you need to check for the license together with a reference
to Radev.Licensing.dll
.Feature
enum to include all features you want to protect with license. You may simply omit this file (and relative
code in LicenseManager
) if you just need to know if there is a license but you don't protect single features.ContactWriter.ToFile("path_for_contact", ContactFactory.Create<Contact>());
If you need plain text (for example to send an e-mail) you can use ContactWriter.ToString()
.
var contact = ContactReader.FromFile("path_for_contact");
var license = LicenseFactory.Create<License>(contact);
// Fill license with any information you need
// ...
LicenseWriter.ToFile("path_for_license", license);
LicenseManager
methods:// Mere license presence enables a basic set of features...
if (LicenseManager.IsLicenseValid == false)
QuitApplicationBecauseInvalidLicense();
// Here we check if an advanced feature is available too...
if (LicenseManager.IsFeatureAvailable(Feature.AdvancedExpensiveFeature)) {
ExecuteAnAdvancedExpensiveFeature();
}
If in doubt about where you license file should be you may read Where to Save License File.
Please note you can assign a validity interval for your license (I strongly suggest to always make it valid from the day it has been created) and also specify which version (again a range) of your software is enabled by that license. For example:
// This license is valid from now to the far future...
license.Validity = new Interval(DateTime.Now, null);
// For any software versione in the 2.xx family
license.MinimumVersion = new Version(2, 0);
license.MaximumVersion = new Version(2, 65535);
To have an adequate protection few assumptions must be satisfied:
First of all we need to uniquely identify a specific machine. There are various IDs and settings we may use but we must combine more than one of them because hardware configuration may change and we do not want to invalidate our license for each small change.
I decided to use WMI queries to read configuration values, because they're easy to use and to change,
and by default (but it can be changed in Licensing\Token.cs
) I read only IDs, because they're less
subject to change compared to - for example - memory size. Note that this choice is not mandatory
and you may want to change it implementing your own Licensing\Client\HardwareAnalyzer.cs
class
and changing Licensing\Token.cs
accordingly.
Application will compare hardware configuration stored in its license with current live configuration
and if they differ more than a specified amount then it will consider license as invalid. For details
check License.IsAssociatedWithThisMachine()
and License.MaximumNumberOfHardwareChanges
. Currently
license will check these values:
Note that I didn't use any disk serial number (hard to reliably read) nor MAC addresses (NICs may be turned on and off at run-time).
Note that you may use motherboard and computer manufacturer name to detect virtual machines (Microsoft, Oracle, VMWare and similar). It's not a reliable method but it has the advantage to be extremly simple. For a more reliable check you have to individually check for virtual machines you know, for more details check Wiki page about Virtual Machines.
After we collected all required information into a contact then we encrypt that data with our public key and then encode it as base64 (to make it easy to transfer, for example, via e-mail). Note that here encryption isn't strictly necessary (because everything a cracker needs is available and visible in application code).
Server code will then decode and decrypt contact information, fill license as required and then sign result with our private key. Everything is then encoded again as base64. From this moment our application can detect any change to license content because signature (verified with public key) won't match.
Of course you also need to protect application assemblies otherwise they may be tampered. This topic is
vast (especially because a cracker has a big surface attack) but you should at least start signing
all your assemblies and then - eventually - embed Radev.Licensing.dll
into your application assembly as
a resource (to make things slightly more complicate for casual crackers), you will load it inside
AppDomain.CurrentDomain.AssemblyResolve
event handler.
There are more things to consider:
NOP
instead of CALL
).More about this later...