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.