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月07日T15:49:46.1482476-04:00</IssueDate>
<ExpiryDate>2017年05月07日T15: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.
1 Answer 1
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.
-
\$\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– Drunken Code Monkey2016年05月16日 20:58:39 +00:00Commented May 16, 2016 at 20:58