Question RFC: .NET 4.5 licensing using RSA4096 SNK, SHA256 signed XML, assembly sig enforcing

Herman

Well-known member
Joined
Oct 18, 2011
Messages
882
Location
Montreal, QC, CA
Programming Experience
10+
Hello folks,

For the past week I have been looking at taking advantage of the .NET 4.5 improvements to code signing and XML signing to produce a licensing subsystem I can use to license my own products. I now have the thing working pretty well, and I am looking for input as to how it could be improved, or what security issues I might have missed. Please feel free to propose enhancements and point out flaws if you see them.

Abstract

What I wanted was to be able to take a solution with assemblies signed with a strong name key, and use that same key pair to sign licenses on my server and validate them in the client application. Historically the strong name assembly signature in .NET was always pretty weak. Until .NET 4.5 it only supported RSA keylengths of 1024 bits and used the now obsolete SHA1 hashing algorithm to produce signatures. The assembly signature is also not enforced by default, and only used to identify assemblies in the GAC. NET 4.5 has now added the support for any key length, and for the SHA256 signatures. The same support was also added to the SignedXml class. I wanted to use the strong name key pair to A) avoid having to distribute a separate public key and B) avoid having to dish out for a suitable CA certificate. I had an idea to use reflection to extract the public key from the signed assembly and use it to first validate the assembly integrity by enforcing signature validation, and then validate the license when an assertion was made.

The code

First, I created a strong name key file with a 4096-bits RSA key:

    Public Shared Function SaveKeyPairToSnk(rsa As RSACryptoServiceProvider, filename As String) As Boolean
        Try
            Using fs As New FileStream(filename, FileMode.Create, FileAccess.Write)
                Dim bytes = rsa.ExportCspBlob(True)
                fs.Write(bytes, 0, bytes.Length)
            End Using
            Return True
        Catch ex As Exception
            Return False
        End Try
    End Function


And signed some test assemblies with it in a VS solution.

Then, I needed a way to enforce the assembly signatures before asserting the license, to ensure they were not tampered with. I designed a base class that P/Invokes to StrongNameSignatureVerificationEx (mscoree.dll) for this, and throws is the validation fails. My protected assemblies would inherit from this class and call the base constructor on activation, which would validate the calling assembly signature and throw if validation failed, or if the calling assembly had a different public key than the base assembly, to marry them together.

Public Class LicensedClassBase
    Shared Sub New()
        AssertLicense(Assembly.GetCallingAssembly())
    End Sub

    Public Sub New()
        AssertLicense(Assembly.GetCallingAssembly())
    End Sub

    Private Shared Sub VerifyAssemblySignature(assmbly As Assembly)
        Dim wasVerified As Boolean
        If Not (NativeMethods.StrongNameSignatureVerificationEx(assmbly.Location, True, wasVerified) _
                AndAlso wasVerified _
                AndAlso assmbly.GetName().GetPublicKey().SequenceEqual(Assembly.GetExecutingAssembly().GetName.GetPublicKey())) Then
            Throw New LicensingException("Signature verification failed: Assembly signature did not match.")
        End If
    End Sub

End Class


To enforce signature validation, I simply inherit from this class and call the base constructors from the derived constructors. If the base constructor throws, the derived class cannot be instantiated. This part works nicely and prevents any tampering in the signed assemblies.

Then I started working on the licenses. This is a sample of a signed license file. This is actually one of my debugging licenses:

VB.NET:
<License xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
    <Id>243</Id>
    <CustId>4365</CustId>
    <CustName>Joe Blow</CustName>
    <IssueDate>2016-05-07T15:49:46.1482476-04:00</IssueDate>
    <ExpiryDate>2017-05-07T15:49:46.1482476-04:00</ExpiryDate>
    <ProductId>1</ProductId>
    <ProductName>Abacus</ProductName>
    <ProductVersion>1</ProductVersion>
    <ProductEdition>DEV</ProductEdition>
    <ProductCount>1</ProductCount>
    <HardwareIds>
        <string>l/tYpAUEn9yhRQg9bijp/g==</string>
    </HardwareIds>
    <Features>
        <string>2EF5D742-F06F-42E0-9199-06D94B31B97E</string>
        <string>F4A23FDF-39CC-422E-A2AC-D279A27B64FF</string>
    </Features>
    <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/2001/04/xmldsig-more#rsa-sha256" />
            <Reference URI="">
                <Transforms>
                    <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
                    <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
                </Transforms>
                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256" />
                <DigestValue>gNyvSh639wV7wHa4UYGPG524pjQ8JZBgaHhEiAm541k=</DigestValue>
            </Reference>
        </SignedInfo>
        <SignatureValue>ntQaT+PMZIS6eke81Vu0uRy8JJDhDfPic5e9Er34tDm00oprQ4qAFVJ1reuXSt+GIf/8XZAV0vR9RLqbB6R5K26lfQc5FCUotLYYjAYexFxwFzJqFV2hrYjhNxYHnXZRs37wY9iVbZlrG7fmEvqg7uN5cb1/K5a3VTFPoZvcUYkswfbzgxmdMdFDdOJCLLLA5oQEI3E60G32FABTJi11Sn9vCSnyePEJdi8yhJCUU9897bD7t2vkoyfbl7Ud5UyEPXUuKDBuX1uIUlU1WatlvH4qghaeV/LfQk8RSP7wHrtrB6T281ko+1+CdebnjTg5FTjo8vwknBXgDK8CRSQVm6DxNf0zeE+IGOhGXFRMCfFOsS9/jnKLT0wMIIqxPMKBX5cXDTX/4udHw6hLEc9H9X/vQLCyTl76ew8gdpgtZZKt8T/Tms8GUrAcIqZYIsUO399LS17lPtOJ2rXlzhDZSjRdVzHnQmGOWxDMtRF9Jb6b13Gr9JuXtPOmrJTl9kCsr+Dv81/h1aCa6xuwIkJtKS2n233+E6zsuSXj/eQJH56lsOJq9ijyXPtRV8LPXkY1Dta5vBwV2EeBA2LAzVOqU6SmM0B99XMCV90PcRLw71OnpdmMs/iUBQNyzn3Awk68hcJy5H3StZD5kl41RObYHQLvVU8/U6bFuwUiY1MAizM=</SignatureValue>
    </Signature>
</License>

This was signed using this function:

    Public Shared Function SignXml(xmlDoc As XmlDocument, rsaKey As RSACryptoServiceProvider) As XmlDocument
        Try
            CryptoConfig.AddAlgorithm(GetType(RSAPKCS1SHA256SignatureDescription), "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256")

            Dim signedXml As New SignedXml(xmlDoc)
            signedXml.SigningKey = rsaKey
            signedXml.SignedInfo.SignatureMethod = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"

            Dim reference As New Reference()
            reference.Uri = ""
            reference.AddTransform(New XmlDsigEnvelopedSignatureTransform())
            reference.AddTransform(New XmlDsigExcC14NTransform())
            reference.DigestMethod = "http://www.w3.org/2001/04/xmlenc#sha256"

            signedXml.AddReference(reference)
            signedXml.ComputeSignature()

            Dim xmlDigitalSignature As XmlElement = signedXml.GetXml()
            xmlDoc.DocumentElement.AppendChild(xmlDoc.ImportNode(xmlDigitalSignature, True))
            If xmlDoc.FirstChild.GetType() = GetType(XmlDeclaration) Then xmlDoc.RemoveChild(xmlDoc.FirstChild)

            Return xmlDoc
        Catch ex As Exception
            xmlDoc = Nothing
        End Try
        Return xmlDoc
    End Function


And is validated using this one:

    Public Shared Sub VerifySignedXml(xmlDoc As XmlDocument, rsaKey As RSACryptoServiceProvider)
        Dim signedXml As New SignedXml(xmlDoc)
        Dim nodeList As XmlNodeList = xmlDoc.GetElementsByTagName("Signature")

        If nodeList.Count > 0 Then
            signedXml.LoadXml(CType(nodeList(0), XmlElement))
        Else
            Throw New LicensingException("Signed XML verification failed: No Signature was found in the document.")
        End If

        If Not signedXml.CheckSignature(rsaKey) Then
            Throw New LicensingException("Signed XML verification failed: Document signature did not match.")
        End If
    End Sub


Both are called from the base class constructor, to which I added this code to assert the license:

    Private Shared Sub AssertLicense(assmbly As Assembly)
        VerifyAssemblySignature(assmbly)

        If assmbly IsNot Assembly.GetExecutingAssembly() Then
            Dim _config = New Configuration.ConfigManager()

            Dim serializer As New XmlSerializer(GetType(License))

            Dim featureId = ""
            Dim attrib = assmbly.GetCustomAttributes(True).OfType(Of FeatureIdAttribute)().FirstOrDefault
            If attrib IsNot Nothing Then
                featureId = attrib.FeatureId
            End If

            Utils.VerifySignedXml(_config.License, Utils.GetAssemblyPublicKey(assmbly))

            Using reader As XmlReader = New XmlNodeReader(_config.License)

                If serializer.CanDeserialize(reader) Then
                    Dim lic As License = serializer.Deserialize(reader)
                     Dim now = Utils.GetCurrentDateTime() ' This retrieves date and time  from HTTP response headers, or fallsback to DateTime.Now

                    If lic Is Nothing Then
                        Throw New LicensingException("Your license is corrupted.")
                    End If

                    If lic.IssueDate > now Then
                        Throw New LicensingException("Your license has not been activated yet.")
                    End If

                    If lic.ExpiryDate < now Then
                        Throw New LicensingException("Your license is expired.")
                    End If

                    If Not lic.HardwareIds.Contains(Utils.GetHardwareId()) Then
                        Throw New LicensingException("Your license is not valid for this hardware platform.")
                    End If

                    If Not My.Application.Info.ProductName.StartsWith(lic.ProductName, True, CultureInfo.InvariantCulture) Then
                        Throw New LicensingException("Your license is not valid for this product.")
                    End If

                     If Not  My.Application.Info.Version.ToString().StartsWith(lic.ProductVersion,  True, CultureInfo.InvariantCulture) Then
                        Throw New LicensingException("Your license is not valid for this version of the product.")
                    End If
                    
                    If Not (attrib IsNot Nothing _
                         AndAlso lic.Features.FirstOrDefault(Function(f) f.ToUpperInvariant() =  featureId.ToUpperInvariant) IsNot Nothing) Then
                        Throw New LicensingException("You current license does not include access to the feature invoked.")
                    End If
                End If
            End Using
        End If
    End Sub


For the features support, I created a custom assembly attribute with a GUID as the feature ID, and I stamp the feature's assembly with it.

I realize this was a long post, and I welcome your input and suggestions/improvements, as well as possible vulnerabilities (other than the private key becoming known, that is a given).
 

Herman

Well-known member
Joined
Oct 18, 2011
Messages
882
Location
Montreal, QC, CA
Programming Experience
10+
Some planned additions are a licensing server using a WCF service backed with a database that can be linked to a CRM for automated license generation, activation and management, and support for floating licenses, as well as client component for online license activation and updates.

Also, I forgot to add above, this is how the hardware ID is generated:

    Public Shared Function GetHardwareId() As String
        Try
            Dim rawId = ""

            Using mbs As New ManagementObjectSearcher("Select * From Win32_processor")
                rawId += mbs.Get().Cast(Of ManagementObject)().First()("ProcessorID").ToString
            End Using

            Using dsk As New ManagementObject("win32_logicaldisk.deviceid=""c:""")
                dsk.Get()
                rawId += dsk("VolumeSerialNumber").ToString()
            End Using

            Using mos As New ManagementObjectSearcher("Select * From Win32_ComputerSystemProduct")
                rawId += DirectCast(mos.Get().Cast(Of ManagementObject)().First()("UUID"), String)
            End Using

            Using md5 As New MD5CryptoServiceProvider
                Return Convert.ToBase64String(md5.ComputeHash(Encoding.UTF8.GetBytes(rawId)))
            End Using
        Catch ex As Exception
            Return Nothing
        End Try
    End Function
 
Top Bottom