DeployLX Software Protection System

Creating a Custom Limit

DeployLX includes 35 limits that can accommodate most licensing needs. However some scenarios require very unique or special handling which you can create a custom limit for.

This topic will cover all the steps needed to create a fictitious sample limit that checks for the existence of a specific environment variable and if it matches the value configured by setting properties on the limit.

Take a moment to read the The License Validation Process topic to learn how limits are involved in the licensing process.

In This Topic

Create the limit class

Create the basic class structure by deriving from the Limit class.

Create a property

Create a property on the limit that is exposed to the DeployLX Manager and through code to configure the behavior of the custom limit.

Persist the limit

Read and Write the limit state to the license file.

Perform validation

Override the Validate, Granted and Peek methods to perform the actual limit enforcing logic.

Register the limit

Register the limit with the DeployLX Manager so that it can be added to a license in the Advanced License Editor.

Distribute the limit

Distribute the assembly containing the limit with the protected software.

Complete sample class View a complete sample custom limit class.

Creating the Class

Create a new class that derives from the Limit class.

Imports System
Imports DeployLX.Licensing.v5

Public Class EnvironmentCheckLimit
    Inherits Limit
    Public Sub New()
    End Sub
End Class
using System;
using DeployLX.Licensing.v5;

public class EnvironmentCheckLimit : Limit 
{
    public EnvironmentCheckLimit()
    {
    }
}

Creating a Property

Limit expose properties that modify the behavior of the limit. Take care to prevent a license from being modified in memory by protecting the properties. Call AssertNotReadOnly before making any changes to a property and you call OnChanged if the value assigned to the property is different than the current value.

See Persisting the Limit State for information on saving the properties to the license.

Public Property VariableName() As String
    Get
        Return _variableName
    End Get
    Set
        AssertNotReadOnly()
        If _variableName <> Value Then
            _variableName = Value
            OnChanged("VariableName")
        End If
    End Set
End Property
Private _variableName As String
public string VariableName
{
    get { return _variableName; }
    set {
        AssertNotReadOnly();
        if( _variableName != value )
        {
            _variableName = value;
            OnChanged( "VariableName" );
        }
    }
}
private string _variableName;

Persisting the Limit State

Save Types

When there is a request to save the license to disk or to memory for a signature check the limit must write itself to an XML stream. The three save scenarios are described by the LicenseSaveType enumeration and described here.

Normal

The license is being persisted to disk and all data should be saved to the license.

Signing

The license is being signed by the DeployLX Manager, license server or other license creation tool. Only data and protected properties required for validation should be written.

Some properties should be excluded when signing for example the NextReminder property of the Registration limit does not effect the security of the license but needs to be tracked. This property is written only when license is saved to disk.

SignatureCheck The license signature is being checked at runtime. The output should be exactly the same as what was written when the license was signed.

Persisting a Limit with Simple Properties

Protected properties may not be modified on the client machine. The value must be the same each time the license is saved so that the license signature can be verified.

Typical properties use primitive types such as string values, enums, numbers, etc. Persisting such theses properties is straight forward requiring no special handling.

When saving simple properties the values are written as attributes of the limit in XML. Use the WriteAttributeString and GetAttribute methods when persisting the properties.

Protected Overrides Function WriteToXml(_
        ByVal writer As System.Xml.XmlWriter, _
        ByVal signing As LicenseSaveType) As Boolean

    writer.WriteAttributeString("variableName", _variableName)
    writer.WriteAttributeString("variableValue", _variableValue)

    Return True
End Function

Protected Overrides Function ReadFromXml(_
        ByVal reader As System.Xml.XmlReader) As Boolean

    _variableName = reader.GetAttribute("variableName")
    _variableValue = reader.GetAttribute("variableValue")

    Return ReadChildLimits(reader)
End Function
protected override bool WriteToXml( 
        System.Xml.XmlWriter writer, 
        LicenseSaveType signing )
{
    writer.WriteAttributeString( "variableName", _variableName );
    writer.WriteAttributeString( "variableValue", _variableValue );

    return true;
}

protected override bool ReadFromXml( 
        System.Xml.XmlReader reader )
{
    _variableName = reader.GetAttribute( "variableName" );
    _variableValue = reader.GetAttribute( "variableValue" );

    return ReadChildLimits( reader );
}

Persisting a Limit with Complex Properties and Child Objects

Complex child objects cannot be used when deriving from built in limits. Use only simple properties.

Limits that expose collections or complex properties that cannot be reduced to a single value must create child elements in the XML stream.

Protected Overrides Function WriteToXml(_
        ByVal writer As XmlWriter,_
        ByVal signing As LicenseSaveType) As Boolean

    writer.WriteAttributeString("variableName", _variableName)
    writer.WriteAttributeString("variableValue", _variableValue)

    For Each address As String In _servers
        writer.WriteStartElement("Server")
        writer.WriteAttributeString("address", 
        	Toolbox.XmlEncode(address))
        writer.WriteEndElement()
    Next address

    Return True
End Function

Protected Overrides Function ReadFromXml(ByVal reader As XmlReader) As Boolean

    _variableName = reader.GetAttribute("variableName")
    _variableValue = reader.GetAttribute("variableValue")
    _servers.Clear()

    If (Not MyBase.ReadFromXml(reader)) Then
        Return False
    End If

    If (Not reader.IsEmptyElement) Then
        reader.Read()
        Do While Not reader.EOF
            If reader.IsStartElement() Then
                Select Case reader.Name
                    Case "Server"
                        attr = reader.GetAttribute("address")
                        If attr Is Nothing Then
                            Return False
                        End If
                        _servers.Add(Toolbox.XmlDecode(attr))
                        reader.Read()
                    Case "Limit"
                        If (Not ReadChildLimit(reader)) Then
                            Return False
                        End If
                    Case Else
                        reader.Skip()
                End Select
            Else
                reader.Read()
                Exit Do
            End If
        Loop
    Else
        reader.Read()
    End If

    Return True
End Function
protected override bool WriteToXml( XmlWriter writer, LicenseSaveType signing )
{

    writer.WriteAttributeString( "variableName", _variableName );
    writer.WriteAttributeString( "variableValue", _variableValue );
	
    foreach( string address in _servers )
    {
        writer.WriteStartElement( "Server" );
        writer.WriteAttributeString( "address", 
        	Toolbox.XmlEncode( address ) );
        writer.WriteEndElement();
    }

    return true;
}

protected override bool ReadFromXml( XmlReader reader )
{
    _variableName = reader.GetAttribute( "variableName" );
    _variableValue = reader.GetAttribute( "variableValue" );

    _servers.Clear();

    if( !base.ReadFromXml( reader ) )
        return false;

    if( !reader.IsEmptyElement )
    {
        reader.Read();
        while( !reader.EOF )
        {
            if( reader.IsStartElement() )
            {
                switch( reader.Name )
                {
                    case "Server":
                        attr = reader.GetAttribute( "address" );
                        if( attr == null )
                            return false;
                        _servers.Add( Toolbox.XmlDecode( attr ) );
                        reader.Read();
                        break;
                    case "Limit":
                        if( !ReadChildLimit( reader ) )
                            return false;
                        break;
                    default:
                        reader.Skip();
                        break;
                }
            }
            else
            {
                reader.Read();
                break;
            }
        }
    }
    else
    {
        reader.Read();
    }

    return true;
}

To save volatile information use one of the following methods.

MetaValues collection

The MetaValues collection can be used to store any arbitrary data that is persisted with the license. This collection is encrypted on disk but is not protected from in-memory modification.

SetPersistentData

SetPersistentData stores information in a secure hidden location on the user's machine. Use GetPersistentData to retrieve a copy of the value at a later time. The values stored using this method will remain on the user's machine even if they uninstall the application and later reinstall it. The only way to reset the data is to change the limit's LimitId property or the license's LicenseId property.

RuntimeState

The RuntimeState collection is used to store variables that should be used by all instances of the limit in memory even if the license is reloaded from disk.

For instance if the limit keeps a count of the executions and the license cache is reset the current instance will be removed from memory and recreated when the license is validated again. Using the RuntimeState o keep the execution count in memory. The state is lost however when the application is restarted.

Performing Validation

Validation is performed at runtime and is managed by the SecureLicenseManager. When searching for a valid license each limit will have an opportunity to examine the current licensing context to determine if it meets the requirements of the limit. See The License Validation Process topic for more details.

Validate & Granted Methods

The Validate method of the limit is called the first time the license is validated and each time the license cache is reset. Most limits only need to check the license context once when the license is first validated since the relevant portion of the licensing context does not change over time.

Limits that depend on the current dynamically changing state of the application will need to perform validation each time a license is requested even if the license was previously cached. Use the Granted method to perform checks every time a license is validated. Code in the Granted method should be fast since it is executed each time the license is validated.

Public Overrides Function Validate(ByVal context As SecureLicenseContext) As ValidationResult
    If (Not IsVariableSet(context, True)) Then
        Return ValidationResult.Invalid
    End If

    Return MyBase.Validate(context)
End Function


Public Overrides Function Granted(ByVal context As SecureLicenseContext) As ValidationResult
    ' Don't need to check it again since the variable was found before.
    Return MyBase.Granted(context)
End Function

Protected Function IsVariableSet(_
        ByVal context As SecureLicenseContext,_
        ByVal reportError As Boolean) As Boolean
    Try
        Dim value As String = _
          Environment.GetEnvironmentVariable(_variableName)
        If value Is Nothing OrElse value <> _variableValue Then
            If reportError Then
                context.ReportError(_
                    "Environment variable ({0}) missing or not set.",_
                    Me,_
                    Nothing,_
                    ErrorSeverity.Normal,_
                    _variableName)
            End If
            Return False
        End If
        Return True
    Catch ex As Exception
        If reportError Then
            context.ReportError("Could not check environment.",_
                                Me,_
                                ex,_
                                ErrorSeverity.High)
        End If
        Return False
    End Try
End Function
public override ValidationResult Validate( SecureLicenseContext context )
{
    if( !IsVariableSet( context, true ) )
        return ValidationResult.Invalid;

    return base.Validate( context );
}


public override ValidationResult Granted( SecureLicenseContext context )
{
    // Don't need to check it again since the variable was found before.
    return base.Granted( context );
}

protected bool IsVariableSet( SecureLicenseContext context, 
                              bool reportError )
{
    try
    {
        var value = Environment.GetEnvironmentVariable( _variableName );
        if( value == null || value != _variableValue )
        {
            if( reportError )
                context.ReportError( 
                    "Environment variable ({0}) missing or not set.", 
                    this, 
                    null, 
                    ErrorSeverity.Normal, 
                    _variableName );
            return false;
        }
        return true;
    }
    catch( Exception ex )
    {
        if( reportError )
            context.ReportError( "Could not check environment.", 
                                 this, 
                                 ex, 
                                 ErrorSeverity.High );
        return false;
    }
}

Peek Method

DeployLX attempts to validate licenses as quickly as possible with the smallest amount of impact on the user. The Peek method quickly determines if the limit will require full validation or interaction with the user. Implementing a fast Peek method will allow DeployLX to use short-circuit logic when choosing a license.

The code in the Peek method may be invoked several times during the validation process and should be very fast.

The code in the Peek method should never show a form to the user or wait on any external resources. If a form needs to be displayed or long processing needs to take place then return PeekResult.NeedsUser.

Public Overrides Function Peek(ByVal context As SecureLicenseContext) As PeekResult
    If (Not IsVariableSet(context, False)) Then
        Return PeekResult.Invalid
    End If

    Return MyBase.Peek(context)
End Function
public override PeekResult Peek( SecureLicenseContext context )
{
    if( !IsVariableSet( context, false ) )
        return PeekResult.Invalid;

    return base.Peek( context );
}

Registering a Custom Limit with the DeployLX Manager

To create a license that uses the custom limit it must be registered with DeployLX. Once registered the custom limit can be added to any license just like the built in limits by selecting it from the limit gallery on the License tab of the Advanced License Editor.

See the Creating an Editor for a Custom Limit topic to learn how to create an editor for the limit.

To register a custom limit

  1. Open the File Menu and select DeployLX Options.
  2. Select the Licensing | Custom Limits node.
  3. Select Add.
  4. The Select Custom Limit form is displayed. Select Browse (...) to open the assembly containing the custom limit.
  5. Select the limit and select OK to close the form.

Distributing the Custom Limit

When DeployLX sees a custom limit in a license it will look for the assembly that hosts the limit and will attempt to load it. If the assembly has not been loaded into memory yet then it mst be installed in either the GAC or in the same folder as the host application.

The best way to ensure that the limit is available for validation is to include it in the protected assembly itself. This will ensure that DeployLX will always be able to find it.

Caution If you obfuscate the assembly that contains the custom limit you must not rename the custom limit since DeployLX finds the limit by name.

Complete Sample Class

Imports System
Imports DeployLX.Licensing.v5

Public Class EnvironmentCheckLimit
    Inherits Limit
    Public Sub New()
    End Sub

    Public Overrides ReadOnly Property Description() As String
        Get
            Return "Checks that an environment variable has been set to a specific value."
        End Get
    End Property

    Public Overrides ReadOnly Property Name() As String
        Get
            Return "Environment Check"
        End Get
    End Property

    Public Property VariableName() As String
        Get
            Return _variableName
        End Get
        Set
            AssertNotReadOnly()
            If _variableName <> Value Then
                _variableName = Value
                OnChanged("VariableName")
            End If
        End Set
    End Property
    Private _variableName As String

    Public Property VariableValue() As String
        Get
            Return _variableValue
        End Get
        Set
            AssertNotReadOnly()
            If _variableValue <> Value Then
                _variableValue = Value
                OnChanged("VariableValue")
            End If
        End Set
    End Property
    Private _variableValue As String

    Public Overrides Function Validate(ByVal context As SecureLicenseContext) As ValidationResult
        If (Not IsVariableSet(context, True)) Then
            Return ValidationResult.Invalid
        End If

        Return MyBase.Validate(context)
    End Function


    Public Overrides Function Granted(ByVal context As SecureLicenseContext) As ValidationResult
        If (Not IsVariableSet(context, True)) Then
            Return ValidationResult.Invalid
        End If
        Return MyBase.Granted(context)
    End Function

    Public Overrides Function Peek(ByVal context As SecureLicenseContext) As PeekResult
        If (Not IsVariableSet(context, False)) Then
            Return PeekResult.Invalid
        End If

        Return MyBase.Peek(context)
    End Function

    Protected Overrides Function WriteToXml(ByVal writer As System.Xml.XmlWriter,_
                                            ByVal signing As LicenseSaveType) As Boolean
        If _variableName Is Nothing Then
            Return False
        End If

        If _variableValue Is Nothing Then
            Return False
        End If

        writer.WriteAttributeString("variableName", _variableName)
        writer.WriteAttributeString("variableValue", _variableValue)

        ' Any child limits will automatically be written to the XML.
        Return True
    End Function

    Protected Overrides Function ReadFromXml(ByVal reader As System.Xml.XmlReader) As Boolean
        _variableName = reader.GetAttribute("variableName")
        _variableValue = reader.GetAttribute("variableValue")

        Return ReadChildLimits(reader)
    End Function

    Protected Function IsVariableSet(ByVal context As SecureLicenseContext, _
                                     ByVal reportError As Boolean) As Boolean
        Try
            Dim value As String = Environment.GetEnvironmentVariable(_variableName)
            If value Is Nothing OrElse value <> _variableValue Then
                If reportError Then
                    context.ReportError( _
                        "Environment variable ({0}) missing or not set.", _
                        Me, _
                        Nothing, _
                        ErrorSeverity.Normal, _
                        _variableName)
                End If
                Return False
            End If
            Return True
        Catch ex As Exception
            If reportError Then
                context.ReportError("Could not check environment.", Me, ex, ErrorSeverity.High)
            End If
            Return False
        End Try
    End Function
End Class
using System;
using DeployLX.Licensing.v5;

public class EnvironmentCheckLimit : Limit 
{
    public EnvironmentCheckLimit()
    {
    }

    public override string Description
    {
        get { return "Checks that an environment variable has been set to a specific value."; }
    }

    public override string Name
    {
        get { return "Environment Check"; }
    }

    public string VariableName
    {
        get { return _variableName; }
        set {
            AssertNotReadOnly();
            if( _variableName != value )
            {
                _variableName = value;
                OnChanged( "VariableName" );
            }
        }
    }
    private string _variableName;

    public string VariableValue
    {
        get { return _variableValue; }
        set {
            AssertNotReadOnly();
            if( _variableValue != value )
            {
                _variableValue = value;
                OnChanged( "VariableValue" );
            }
        }
    }
    private string _variableValue;
            
    public override ValidationResult Validate( SecureLicenseContext context )
    {
        if( !IsVariableSet( context, true ) )
            return ValidationResult.Invalid;

        return base.Validate( context );
    }


    public override ValidationResult Granted( SecureLicenseContext context )
    {
        if( !IsVariableSet( context, true ) )
            return ValidationResult.Invalid;
        return base.Granted( context );
    }

    public override PeekResult Peek( SecureLicenseContext context )
    {
        if( !IsVariableSet( context, false ) )
            return PeekResult.Invalid;

        return base.Peek( context );
    }

    protected override bool WriteToXml( System.Xml.XmlWriter writer, LicenseSaveType signing )
    {
        if( _variableName == null )
            return false;

        if( _variableValue == null )
            return false;

        writer.WriteAttributeString( "variableName", _variableName );
        writer.WriteAttributeString( "variableValue", _variableValue );

        // Any child limits will automatically be written to the XML.
        return true;
    }
	
    protected override bool ReadFromXml( System.Xml.XmlReader reader )
    {
        _variableName = reader.GetAttribute( "variableName" );
        _variableValue = reader.GetAttribute( "variableValue" );

        return ReadChildLimits( reader );
    }
        
    protected bool IsVariableSet( SecureLicenseContext context, bool reportError )
    {
        try
        {
            string value = Environment.GetEnvironmentVariable( _variableName );
            if( value == null || value != _variableValue )
            {
                if( reportError )
                    context.ReportError( 
                        "Environment variable ({0}) missing or not set.", 
                        this, 
                        null, 
                        ErrorSeverity.Normal, 
                        _variableName );
                return false;
            }
            return true;
        }
        catch( Exception ex )
        {
            if( reportError )
                context.ReportError( "Could not check environment.", 
                                     this, 
                                     ex, 
                                     ErrorSeverity.High );
            return false;
        }
    }

}

See Also