Herman
Well-known member
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:
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.
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:
This was signed using this function:
And is validated using this one:
Both are called from the base class constructor, to which I added this code to assert the license:
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).
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).