13
\$\begingroup\$

Abstract

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.

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.

What I am looking for here is constructive criticism, possible vulnerabilities in the approach (other than the private key becoming known), or how I could make this better/more secure.

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. If the base class constructor throws, the protected derived class cannot be used.

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

    Public Sub New()
        VerifyAssemblySignature(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

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

<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()  
                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

And 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

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.

\$\endgroup\$
2
+25
\$\begingroup\$

I Think you solution is a good compromise between efford and security.

However, verifing the signature of the assembly from inside the assembly is not a big approvment of security. If anybody is able to tamper your assembly, she can just remove the check. One further improvment could be to move the verification to a native asembly. To ensure that the method of the native assembly is called, you could encrypt one or more of the application's base assemblies and load (and decrypt) them with the method of the native assembly that verify the entry assembly.

Another improvment could be the way you handle the hardware ID. You generate a single harware ID from 3 device ids (CPU, HD and Motherboard). If one of the device ids change, your hardware ID does not match anymore. A more tolerant solution is to check that at least 2 of the 3 device ids matches. That ensures that your license is still valid if the customer change its processor for instance.

\$\endgroup\$
  • \$\begingroup\$ Thanks. Actually the assembly signature verification is done from inside a separate assembly. Think of it as a Licensing.Core assembly, that would be obfuscated before deployment. I think that's more or less what you were proposing in paragraph 2. \$\endgroup\$ – Drunken Code Monkey May 16 '16 at 20:58

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.