├── .gitignore ├── LICENCE ├── LicencingNET.Example ├── LicencingNET.Example.csproj └── Program.cs ├── LicencingNET.sln ├── LicencingNET ├── ComparisonUtils.cs ├── Constants.cs ├── Licence.cs ├── LicencingNET.csproj ├── NTP.cs └── ValidationResult.cs └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # globs 2 | Makefile.in 3 | *.userprefs 4 | *.usertasks 5 | config.make 6 | config.status 7 | aclocal.m4 8 | install-sh 9 | autom4te.cache/ 10 | *.tar.gz 11 | tarballs/ 12 | test-results/ 13 | 14 | # Mac bundle stuff 15 | *.dmg 16 | *.app 17 | 18 | # content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore 19 | # General 20 | .DS_Store 21 | .AppleDouble 22 | .LSOverride 23 | 24 | # Icon must end with two \r 25 | Icon 26 | 27 | 28 | # Thumbnails 29 | ._* 30 | 31 | # Files that might appear in the root of a volume 32 | .DocumentRevisions-V100 33 | .fseventsd 34 | .Spotlight-V100 35 | .TemporaryItems 36 | .Trashes 37 | .VolumeIcon.icns 38 | .com.apple.timemachine.donotpresent 39 | 40 | # Directories potentially created on remote AFP share 41 | .AppleDB 42 | .AppleDesktop 43 | Network Trash Folder 44 | Temporary Items 45 | .apdisk 46 | 47 | # content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore 48 | # Windows thumbnail cache files 49 | Thumbs.db 50 | ehthumbs.db 51 | ehthumbs_vista.db 52 | 53 | # Dump file 54 | *.stackdump 55 | 56 | # Folder config file 57 | [Dd]esktop.ini 58 | 59 | # Recycle Bin used on file shares 60 | $RECYCLE.BIN/ 61 | 62 | # Windows Installer files 63 | *.cab 64 | *.msi 65 | *.msix 66 | *.msm 67 | *.msp 68 | 69 | # Windows shortcuts 70 | *.lnk 71 | 72 | # content below from: https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 73 | ## Ignore Visual Studio temporary files, build results, and 74 | ## files generated by popular Visual Studio add-ons. 75 | ## 76 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 77 | 78 | # User-specific files 79 | *.suo 80 | *.user 81 | *.userosscache 82 | *.sln.docstates 83 | 84 | # User-specific files (MonoDevelop/Xamarin Studio) 85 | *.userprefs 86 | 87 | # Build results 88 | [Dd]ebug/ 89 | [Dd]ebugPublic/ 90 | [Rr]elease/ 91 | [Rr]eleases/ 92 | x64/ 93 | x86/ 94 | bld/ 95 | [Bb]in/ 96 | [Oo]bj/ 97 | [Ll]og/ 98 | 99 | # Visual Studio 2015/2017 cache/options directory 100 | .vs/ 101 | # Uncomment if you have tasks that create the project's static files in wwwroot 102 | #wwwroot/ 103 | 104 | # Visual Studio 2017 auto generated files 105 | Generated\ Files/ 106 | 107 | # MSTest test Results 108 | [Tt]est[Rr]esult*/ 109 | [Bb]uild[Ll]og.* 110 | 111 | # NUNIT 112 | *.VisualState.xml 113 | TestResult.xml 114 | 115 | # Build Results of an ATL Project 116 | [Dd]ebugPS/ 117 | [Rr]eleasePS/ 118 | dlldata.c 119 | 120 | # Benchmark Results 121 | BenchmarkDotNet.Artifacts/ 122 | 123 | # .NET Core 124 | project.lock.json 125 | project.fragment.lock.json 126 | artifacts/ 127 | 128 | # StyleCop 129 | StyleCopReport.xml 130 | 131 | # Files built by Visual Studio 132 | *_i.c 133 | *_p.c 134 | *_h.h 135 | *.ilk 136 | *.meta 137 | *.obj 138 | *.iobj 139 | *.pch 140 | *.pdb 141 | *.ipdb 142 | *.pgc 143 | *.pgd 144 | *.rsp 145 | *.sbr 146 | *.tlb 147 | *.tli 148 | *.tlh 149 | *.tmp 150 | *.tmp_proj 151 | *_wpftmp.csproj 152 | *.log 153 | *.vspscc 154 | *.vssscc 155 | .builds 156 | *.pidb 157 | *.svclog 158 | *.scc 159 | 160 | # Chutzpah Test files 161 | _Chutzpah* 162 | 163 | # Visual C++ cache files 164 | ipch/ 165 | *.aps 166 | *.ncb 167 | *.opendb 168 | *.opensdf 169 | *.sdf 170 | *.cachefile 171 | *.VC.db 172 | *.VC.VC.opendb 173 | 174 | # Visual Studio profiler 175 | *.psess 176 | *.vsp 177 | *.vspx 178 | *.sap 179 | 180 | # Visual Studio Trace Files 181 | *.e2e 182 | 183 | # TFS 2012 Local Workspace 184 | $tf/ 185 | 186 | # Guidance Automation Toolkit 187 | *.gpState 188 | 189 | # ReSharper is a .NET coding add-in 190 | _ReSharper*/ 191 | *.[Rr]e[Ss]harper 192 | *.DotSettings.user 193 | 194 | # JustCode is a .NET coding add-in 195 | .JustCode 196 | 197 | # TeamCity is a build add-in 198 | _TeamCity* 199 | 200 | # DotCover is a Code Coverage Tool 201 | *.dotCover 202 | 203 | # AxoCover is a Code Coverage Tool 204 | .axoCover/* 205 | !.axoCover/settings.json 206 | 207 | # Visual Studio code coverage results 208 | *.coverage 209 | *.coveragexml 210 | 211 | # NCrunch 212 | _NCrunch_* 213 | .*crunch*.local.xml 214 | nCrunchTemp_* 215 | 216 | # MightyMoose 217 | *.mm.* 218 | AutoTest.Net/ 219 | 220 | # Web workbench (sass) 221 | .sass-cache/ 222 | 223 | # Installshield output folder 224 | [Ee]xpress/ 225 | 226 | # DocProject is a documentation generator add-in 227 | DocProject/buildhelp/ 228 | DocProject/Help/*.HxT 229 | DocProject/Help/*.HxC 230 | DocProject/Help/*.hhc 231 | DocProject/Help/*.hhk 232 | DocProject/Help/*.hhp 233 | DocProject/Help/Html2 234 | DocProject/Help/html 235 | 236 | # Click-Once directory 237 | publish/ 238 | 239 | # Publish Web Output 240 | *.[Pp]ublish.xml 241 | *.azurePubxml 242 | # Note: Comment the next line if you want to checkin your web deploy settings, 243 | # but database connection strings (with potential passwords) will be unencrypted 244 | *.pubxml 245 | *.publishproj 246 | 247 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 248 | # checkin your Azure Web App publish settings, but sensitive information contained 249 | # in these scripts will be unencrypted 250 | PublishScripts/ 251 | 252 | # NuGet Packages 253 | *.nupkg 254 | # The packages folder can be ignored because of Package Restore 255 | **/[Pp]ackages/* 256 | # except build/, which is used as an MSBuild target. 257 | !**/[Pp]ackages/build/ 258 | # Uncomment if necessary however generally it will be regenerated when needed 259 | #!**/[Pp]ackages/repositories.config 260 | # NuGet v3's project.json files produces more ignorable files 261 | *.nuget.props 262 | *.nuget.targets 263 | 264 | # Microsoft Azure Build Output 265 | csx/ 266 | *.build.csdef 267 | 268 | # Microsoft Azure Emulator 269 | ecf/ 270 | rcf/ 271 | 272 | # Windows Store app package directories and files 273 | AppPackages/ 274 | BundleArtifacts/ 275 | Package.StoreAssociation.xml 276 | _pkginfo.txt 277 | *.appx 278 | 279 | # Visual Studio cache files 280 | # files ending in .cache can be ignored 281 | *.[Cc]ache 282 | # but keep track of directories ending in .cache 283 | !*.[Cc]ache/ 284 | 285 | # Others 286 | ClientBin/ 287 | ~$* 288 | *~ 289 | *.dbmdl 290 | *.dbproj.schemaview 291 | *.jfm 292 | *.pfx 293 | *.publishsettings 294 | orleans.codegen.cs 295 | 296 | # Including strong name files can present a security risk 297 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 298 | #*.snk 299 | 300 | # Since there are multiple workflows, uncomment next line to ignore bower_components 301 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 302 | #bower_components/ 303 | 304 | # RIA/Silverlight projects 305 | Generated_Code/ 306 | 307 | # Backup & report files from converting an old project file 308 | # to a newer Visual Studio version. Backup files are not needed, 309 | # because we have git ;-) 310 | _UpgradeReport_Files/ 311 | Backup*/ 312 | UpgradeLog*.XML 313 | UpgradeLog*.htm 314 | ServiceFabricBackup/ 315 | *.rptproj.bak 316 | 317 | # SQL Server files 318 | *.mdf 319 | *.ldf 320 | *.ndf 321 | 322 | # Business Intelligence projects 323 | *.rdl.data 324 | *.bim.layout 325 | *.bim_*.settings 326 | *.rptproj.rsuser 327 | 328 | # Microsoft Fakes 329 | FakesAssemblies/ 330 | 331 | # GhostDoc plugin setting file 332 | *.GhostDoc.xml 333 | 334 | # Node.js Tools for Visual Studio 335 | .ntvs_analysis.dat 336 | node_modules/ 337 | 338 | # Visual Studio 6 build log 339 | *.plg 340 | 341 | # Visual Studio 6 workspace options file 342 | *.opt 343 | 344 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 345 | *.vbw 346 | 347 | # Visual Studio LightSwitch build output 348 | **/*.HTMLClient/GeneratedArtifacts 349 | **/*.DesktopClient/GeneratedArtifacts 350 | **/*.DesktopClient/ModelManifest.xml 351 | **/*.Server/GeneratedArtifacts 352 | **/*.Server/ModelManifest.xml 353 | _Pvt_Extensions 354 | 355 | # Paket dependency manager 356 | .paket/paket.exe 357 | paket-files/ 358 | 359 | # FAKE - F# Make 360 | .fake/ 361 | 362 | # JetBrains Rider 363 | .idea/ 364 | *.sln.iml 365 | 366 | # CodeRush personal settings 367 | .cr/personal 368 | 369 | # Python Tools for Visual Studio (PTVS) 370 | __pycache__/ 371 | *.pyc 372 | 373 | # Cake - Uncomment if you are using it 374 | # tools/** 375 | # !tools/packages.config 376 | 377 | # Tabs Studio 378 | *.tss 379 | 380 | # Telerik's JustMock configuration file 381 | *.jmconfig 382 | 383 | # BizTalk build output 384 | *.btp.cs 385 | *.btm.cs 386 | *.odx.cs 387 | *.xsd.cs 388 | 389 | # OpenCover UI analysis results 390 | OpenCover/ 391 | 392 | # Azure Stream Analytics local run output 393 | ASALocalRun/ 394 | 395 | # MSBuild Binary and Structured Log 396 | *.binlog 397 | 398 | # NVidia Nsight GPU debugger configuration file 399 | *.nvuser 400 | 401 | # MFractors (Xamarin productivity tool) working folder 402 | .mfractor/ 403 | 404 | # Local History for Visual Studio 405 | .localhistory/ -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Albin Corén 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /LicencingNET.Example/LicencingNET.Example.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exe 4 | net35 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /LicencingNET.Example/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Security.Cryptography; 4 | 5 | namespace LicencingNET.Example 6 | { 7 | class Program 8 | { 9 | static void Main(string[] args) 10 | { 11 | /* GENERATE KEYS */ 12 | /* PRODUCTION WOULD USE CERTIFICATES INSTEAD */ 13 | RSAParameters privateKey; 14 | RSAParameters publicKey; 15 | using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider(2048)) 16 | { 17 | privateKey = rsa.ExportParameters(true); 18 | publicKey = rsa.ExportParameters(false); 19 | } 20 | 21 | // Create unsigned licence 22 | Licence licence = Licence.Create(null, DateTime.Now, DateTime.Now.AddDays(30), new Dictionary() 23 | { 24 | { "LicenceType", "Trial" }, 25 | { "CustomerName", "John Doe" }, 26 | { "CustomerEmail", "john.doe@contoso.com" }, 27 | { "CustomerCompany", "Contoso Ltd." } 28 | }); 29 | 30 | using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider()) 31 | { 32 | rsa.ImportParameters(privateKey); 33 | 34 | // Sign the licence with the private key. 35 | if (licence.Sign(rsa)) 36 | { 37 | Console.WriteLine("Signed"); 38 | } 39 | else 40 | { 41 | Console.WriteLine("Failed to sign"); 42 | } 43 | } 44 | 45 | byte[] binaryLicence = licence.ToBinary(); 46 | 47 | // GIVE LICENCE TO CLIENT IN BINARY FORM (XML IS ALSO SUPPORTED) 48 | 49 | using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider()) 50 | { 51 | rsa.ImportParameters(publicKey); 52 | 53 | // Parse the binary licence 54 | Licence clientLicence = Licence.FromBinary(binaryLicence); 55 | 56 | // Validate that the licence is still valid. 57 | ValidationResult result = clientLicence.Validate(rsa); 58 | 59 | if (result == ValidationResult.Valid) 60 | { 61 | Console.WriteLine("Valid!"); 62 | } 63 | else 64 | { 65 | Console.WriteLine("Invalid + " + result); 66 | } 67 | } 68 | 69 | Console.Read(); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /LicencingNET.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LicencingNET.Example", "LicencingNET.Example\LicencingNET.Example.csproj", "{3A43F04D-C0D9-484D-92B6-FC202F5466A2}" 5 | EndProject 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LicencingNET", "LicencingNET\LicencingNET.csproj", "{9B1A4400-5BC3-434A-81B5-54D19A924F53}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {3A43F04D-C0D9-484D-92B6-FC202F5466A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {3A43F04D-C0D9-484D-92B6-FC202F5466A2}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {3A43F04D-C0D9-484D-92B6-FC202F5466A2}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {3A43F04D-C0D9-484D-92B6-FC202F5466A2}.Release|Any CPU.Build.0 = Release|Any CPU 18 | {9B1A4400-5BC3-434A-81B5-54D19A924F53}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {9B1A4400-5BC3-434A-81B5-54D19A924F53}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {9B1A4400-5BC3-434A-81B5-54D19A924F53}.Release|Any CPU.ActiveCfg = Release|Any CPU 21 | {9B1A4400-5BC3-434A-81B5-54D19A924F53}.Release|Any CPU.Build.0 = Release|Any CPU 22 | EndGlobalSection 23 | EndGlobal 24 | -------------------------------------------------------------------------------- /LicencingNET/ComparisonUtils.cs: -------------------------------------------------------------------------------- 1 | namespace LicencingNET 2 | { 3 | internal static class ComparisonUtils 4 | { 5 | internal static bool ConstTimeArrayEqual(byte[] a, byte[] b) 6 | { 7 | if (a.Length != b.Length) 8 | return false; 9 | 10 | int i = a.Length; 11 | int cmp = 0; 12 | 13 | while (i != 0) 14 | { 15 | --i; 16 | cmp |= (a[i] ^ b[i]); 17 | } 18 | 19 | return cmp == 0; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /LicencingNET/Constants.cs: -------------------------------------------------------------------------------- 1 | namespace LicencingNET 2 | { 3 | internal static class Constants 4 | { 5 | internal static ushort CURRENT_VERSION = 0; 6 | internal static ushort[] SUPPORTED_VERSIONS = new ushort[] 7 | { 8 | 0 9 | }; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /LicencingNET/Licence.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Security.Cryptography; 6 | using System.Security.Cryptography.X509Certificates; 7 | using System.Xml; 8 | 9 | namespace LicencingNET 10 | { 11 | /// 12 | /// Represents a software licence. 13 | /// 14 | public class Licence 15 | { 16 | /// 17 | /// Serial number for the licence. Should be unique. 18 | /// 19 | /// The serial number of the licence. 20 | public Guid Serial { get; internal set; } 21 | /// 22 | /// NotBefore date specifies the start date of the licence validity period. 23 | /// If null, it has no start. 24 | /// 25 | /// The not before date. 26 | public DateTime? NotBefore { get; internal set; } 27 | /// 28 | /// NotAfter date specifies the end date of the licence validity period. 29 | /// If null, it has no end. 30 | /// 31 | /// The not after date. 32 | public DateTime? NotAfter { get; internal set; } 33 | /// 34 | /// Custom attributes of the licence. 35 | /// This could contain information about who its valid for, or what features/products the licence unlocks. 36 | /// 37 | /// The licence attributes. 38 | public Dictionary Attributes { get; internal set; } 39 | /// 40 | /// Gets the signature for the licence. 41 | /// 42 | /// The signature of the licence. 43 | public byte[] Signature { get; internal set; } 44 | /// 45 | /// Gets whether or not the licence is signed. 46 | /// 47 | /// true if is signed; otherwise, false. 48 | public bool HasSignature => Signature != null; 49 | 50 | private Licence() 51 | { 52 | 53 | } 54 | 55 | /// 56 | /// Gets the licence in binary format. 57 | /// 58 | /// The binary encoded licence. 59 | public byte[] ToBinary() => ToBinary(true); 60 | /// 61 | /// Constructs a licence object from a binary encoded licence. 62 | /// 63 | /// The decoded licence. 64 | /// The binary encoded licence. 65 | public static Licence FromBinary(byte[] binary) => FromBinary(binary, true); 66 | 67 | /// 68 | /// Gets the licence in xml format. 69 | /// 70 | /// The xml encoded licence. 71 | public string ToXML() => ToXML(true); 72 | /// 73 | /// Constructs a licence object from a xml encoded licence. 74 | /// 75 | /// The decoded licence. 76 | /// The xml encoded licence. 77 | public static Licence FromXML(string xml) => FromXML(xml, true); 78 | 79 | /// 80 | /// Creates a licence with a optional Serial, NotBefore date, NotAfter date and licence attributes. 81 | /// 82 | /// The created licence. 83 | /// The serial for the licence. If null, it defaults to Guid.NewGuid(). 84 | /// The start date for the licence. If null, it will have no start date. 85 | /// The end date for the licence. If null, it will have no end date 86 | /// The custom licence attributes. 87 | public static Licence Create(Guid? serial, DateTime? notBefore, DateTime? notAfter, Dictionary attributes) 88 | { 89 | return new Licence() 90 | { 91 | Serial = serial != null ? serial.Value : Guid.NewGuid(), 92 | NotBefore = notBefore, 93 | NotAfter = notAfter, 94 | Attributes = attributes != null ? attributes : new Dictionary(), 95 | Signature = null 96 | }; 97 | } 98 | 99 | /// 100 | /// Modifies all properties the and clears the signature. 101 | /// 102 | /// The new Serial. 103 | /// The new NotBefore date. 104 | /// The new NotAfter date. 105 | /// The new Attributes. 106 | public void ModifyAndClearSignature(Guid? serial, DateTime? notBefore, DateTime? notAfter, Dictionary attributes) 107 | { 108 | Signature = null; 109 | Serial = serial != null ? serial.Value : Guid.NewGuid(); 110 | NotBefore = notBefore; 111 | NotAfter = notAfter; 112 | Attributes = attributes != null ? attributes : new Dictionary(); 113 | } 114 | 115 | /// 116 | /// Validate the licence using the specified certificate. 117 | /// 118 | /// The validation status. 119 | /// The certificate used to validate the licence. 120 | public ValidationResult Validate(X509Certificate2 certificate, bool useNtp = true) => Validate(certificate.PublicKey.Key, useNtp); 121 | 122 | /// 123 | /// Validate the licence using the specified RSA or DSA public key. 124 | /// 125 | /// The validation status. 126 | /// The public RSA or DSA key used to validate the licence. 127 | public ValidationResult Validate(AsymmetricAlgorithm publicKey, bool useNtp = true) 128 | { 129 | if (publicKey == null) 130 | { 131 | throw new ArgumentNullException(nameof(publicKey), "Public key cannot be null"); 132 | } 133 | 134 | if (!HasSignature) 135 | { 136 | return ValidationResult.NoSignature; 137 | } 138 | 139 | DateTime currentTime = DateTime.UtcNow; 140 | try 141 | { 142 | if (useNtp) 143 | { 144 | currentTime = NTP.GetNetworkTime(); 145 | } 146 | } 147 | catch 148 | { 149 | } 150 | 151 | if (NotAfter != null && currentTime > NotAfter.Value.ToUniversalTime()) 152 | { 153 | return ValidationResult.Exipired; 154 | } 155 | 156 | if (NotBefore != null && currentTime < NotBefore.Value.ToUniversalTime()) 157 | { 158 | return ValidationResult.NotStarted; 159 | } 160 | 161 | using (SHA512 sha = SHA512.Create()) 162 | { 163 | byte[] licenceBinary = ToBinary(false); 164 | 165 | if (publicKey is RSACryptoServiceProvider rsa) 166 | { 167 | if (rsa.VerifyData(licenceBinary, sha, Signature)) 168 | { 169 | return ValidationResult.Valid; 170 | } 171 | else 172 | { 173 | return ValidationResult.InvalidSignature; 174 | } 175 | } 176 | 177 | if (publicKey is DSACryptoServiceProvider dsa) 178 | { 179 | if (dsa.VerifySignature(sha.ComputeHash(licenceBinary), Signature)) 180 | { 181 | return ValidationResult.Valid; 182 | } 183 | else 184 | { 185 | return ValidationResult.InvalidSignature; 186 | } 187 | } 188 | 189 | throw new NotSupportedException("Only RSA and DSA signatures are supported"); 190 | } 191 | } 192 | 193 | /// 194 | /// Sign the licence with the specified certificates private RSA or DSA key. 195 | /// 196 | /// Whether the signature was successfully created. 197 | /// The certificate containing a private RSA or DSA key. 198 | public bool Sign(X509Certificate2 certificate) => Sign(certificate.PrivateKey); 199 | 200 | /// 201 | /// Sign the licence with the specified private RSA or DSA key. 202 | /// 203 | /// Whether the signature was successfully created. 204 | /// The private RSA or DSA key to use. 205 | public bool Sign(AsymmetricAlgorithm privateKey) 206 | { 207 | if (privateKey == null) 208 | { 209 | throw new ArgumentNullException(nameof(privateKey), "Private key cannot be null"); 210 | } 211 | 212 | using (SHA512 sha = SHA512.Create()) 213 | { 214 | byte[] licenceBinary = ToBinary(false); 215 | 216 | if (privateKey is RSACryptoServiceProvider rsa && !rsa.PublicOnly) 217 | { 218 | byte[] signature = rsa.SignData(licenceBinary, sha); 219 | 220 | Signature = signature; 221 | 222 | return true; 223 | } 224 | 225 | if (privateKey is DSACryptoServiceProvider dsa && !dsa.PublicOnly) 226 | { 227 | byte[] licenceHash = sha.ComputeHash(licenceBinary); 228 | byte[] signature = dsa.CreateSignature(licenceHash); 229 | 230 | Signature = signature; 231 | 232 | return true; 233 | } 234 | } 235 | 236 | return false; 237 | } 238 | 239 | internal static Licence FromBinary(byte[] binary, bool importSignature) 240 | { 241 | if (binary == null) 242 | { 243 | throw new ArgumentNullException(nameof(binary), "Binary input cannot be null"); 244 | } 245 | 246 | using (MemoryStream stream = new MemoryStream(binary)) 247 | { 248 | using (BinaryReader reader = new BinaryReader(stream)) 249 | { 250 | Licence licence = new Licence(); 251 | 252 | ushort version = reader.ReadUInt16(); 253 | if (!Constants.SUPPORTED_VERSIONS.Contains(version)) 254 | { 255 | throw new FormatException("Version " + version + " is not supported"); 256 | } 257 | 258 | byte serialLength = reader.ReadByte(); 259 | licence.Serial = new Guid(reader.ReadBytes(serialLength)); 260 | 261 | if (reader.ReadBoolean()) 262 | { 263 | licence.NotBefore = DateTime.FromBinary(reader.ReadInt64()); 264 | } 265 | else 266 | { 267 | licence.NotBefore = null; 268 | } 269 | 270 | if (reader.ReadBoolean()) 271 | { 272 | licence.NotAfter = DateTime.FromBinary(reader.ReadInt64()); 273 | } 274 | else 275 | { 276 | licence.NotAfter = null; 277 | } 278 | 279 | ushort attributeCount = reader.ReadUInt16(); 280 | licence.Attributes = new Dictionary(); 281 | for (int i = 0; i < attributeCount; i++) 282 | { 283 | licence.Attributes.Add(reader.ReadString(), reader.ReadString()); 284 | } 285 | 286 | if (importSignature) 287 | { 288 | ushort signatureLength = reader.ReadUInt16(); 289 | byte[] signature = signatureLength == 0 ? null : reader.ReadBytes(signatureLength); 290 | 291 | licence.Signature = signature; 292 | } 293 | 294 | return licence; 295 | } 296 | } 297 | } 298 | 299 | internal static Licence FromXML(string xml, bool importSignature) 300 | { 301 | if (string.IsNullOrEmpty(xml) || string.IsNullOrEmpty(xml.Trim())) 302 | { 303 | throw new ArgumentNullException(nameof(xml), "Xml input cannot be null or empty"); 304 | } 305 | 306 | using (StringReader stringReader = new StringReader(xml)) 307 | { 308 | using (XmlReader reader = XmlReader.Create(stringReader)) 309 | { 310 | Licence licence = new Licence(); 311 | 312 | if (!reader.ReadToFollowing("Licence")) 313 | { 314 | throw new FormatException("Licence tag not found"); 315 | } 316 | 317 | string versionString = reader.GetAttribute("Version"); 318 | if (ushort.TryParse(versionString, out ushort version)) 319 | { 320 | if (!Constants.SUPPORTED_VERSIONS.Contains(version)) 321 | { 322 | throw new FormatException("Version " + version + " is not supported"); 323 | } 324 | } 325 | else 326 | { 327 | throw new FormatException("Invalid Version format"); 328 | } 329 | 330 | reader.ReadStartElement("Licence"); 331 | 332 | string serialString = reader.ReadElementContentAsString("Serial", ""); 333 | try 334 | { 335 | Guid serial = new Guid(serialString); 336 | 337 | licence.Serial = serial; 338 | } 339 | catch (Exception e) 340 | { 341 | throw new FormatException("Invalid Serial format", e); 342 | } 343 | 344 | string notBeforeString = reader.ReadElementContentAsString("NotBefore", ""); 345 | if (notBeforeString.ToLower() == "n/a") 346 | { 347 | licence.NotBefore = null; 348 | } 349 | else 350 | { 351 | if (long.TryParse(notBeforeString, out long notBeforeLong)) 352 | { 353 | licence.NotBefore = DateTime.FromBinary(notBeforeLong); 354 | } 355 | else 356 | { 357 | throw new FormatException("Invalid NotBefore format"); 358 | } 359 | } 360 | 361 | 362 | string notAfterString = reader.ReadElementContentAsString("NotAfter", ""); 363 | if (notAfterString.ToLower() == "n/a") 364 | { 365 | licence.NotAfter = null; 366 | } 367 | else 368 | { 369 | if (long.TryParse(notAfterString, out long notAfterLong)) 370 | { 371 | licence.NotAfter = DateTime.FromBinary(notAfterLong); 372 | } 373 | else 374 | { 375 | throw new FormatException("Invalid NotAfter format"); 376 | } 377 | } 378 | 379 | Dictionary attributes = new Dictionary(); 380 | 381 | reader.ReadStartElement("Attributes"); 382 | while (reader.NodeType == XmlNodeType.Element && reader.Name == "Attribute" && reader.Read()) 383 | { 384 | string key = reader.ReadElementContentAsString("Key", ""); 385 | string value = reader.ReadElementContentAsString("Value", ""); 386 | 387 | attributes.Add(key, value); 388 | 389 | while (true) 390 | { 391 | bool fail = reader.NodeType != XmlNodeType.EndElement || reader.Name != "Attribute"; 392 | 393 | if (!reader.Read()) 394 | { 395 | throw new FormatException("Could not find Attribute end element"); 396 | } 397 | 398 | if (!fail) 399 | { 400 | break; 401 | } 402 | } 403 | } 404 | 405 | while (true) 406 | { 407 | bool fail = reader.NodeType != XmlNodeType.EndElement || reader.Name != "Attributes"; 408 | 409 | if (!reader.Read()) 410 | { 411 | throw new FormatException("Could not find Attributes end element"); 412 | } 413 | 414 | if (!fail) 415 | { 416 | break; 417 | } 418 | } 419 | 420 | licence.Attributes = attributes; 421 | 422 | if (importSignature) 423 | { 424 | string signatureString = reader.ReadElementContentAsString("Signature", ""); 425 | try 426 | { 427 | byte[] signatureBytes = Convert.FromBase64String(signatureString); 428 | licence.Signature = signatureBytes; 429 | } 430 | catch (Exception e) 431 | { 432 | licence.Signature = null; 433 | throw new FormatException("Invalid signature", e); 434 | } 435 | } 436 | else 437 | { 438 | licence.Signature = null; 439 | } 440 | 441 | if (!(reader.NodeType == XmlNodeType.EndElement && reader.Name == "Licence")) 442 | { 443 | throw new FormatException("No licence end tag found"); 444 | } 445 | 446 | return licence; 447 | } 448 | } 449 | } 450 | 451 | internal string ToXML(bool exportSignature) 452 | { 453 | using (StringWriter stringWriter = new StringWriter()) 454 | { 455 | using (XmlWriter writer = XmlWriter.Create(stringWriter)) 456 | { 457 | writer.WriteStartElement("Licence"); 458 | writer.WriteAttributeString("Version", Constants.CURRENT_VERSION.ToString()); 459 | writer.WriteElementString("Serial", Serial.ToString()); 460 | writer.WriteElementString("NotBefore", (NotBefore == null) ? "N/A" : NotBefore.Value.ToBinary().ToString()); 461 | writer.WriteElementString("NotAfter", (NotAfter == null) ? "N/A" : NotAfter.Value.ToBinary().ToString()); 462 | 463 | writer.WriteStartElement("Attributes"); 464 | List> sortedAttributes = Attributes.Select(x => x).OrderBy(x => x.Key).ToList(); 465 | 466 | foreach (KeyValuePair attribute in sortedAttributes) 467 | { 468 | writer.WriteStartElement("Attribute"); 469 | writer.WriteElementString("Key", attribute.Key); 470 | writer.WriteElementString("Value", attribute.Value); 471 | writer.WriteEndElement(); 472 | } 473 | writer.WriteEndElement(); 474 | 475 | if (exportSignature) 476 | { 477 | writer.WriteElementString("Signature", Signature == null ? "" : Convert.ToBase64String(Signature)); 478 | } 479 | 480 | writer.WriteEndElement(); 481 | writer.Flush(); 482 | } 483 | 484 | return stringWriter.ToString(); 485 | } 486 | } 487 | 488 | internal byte[] ToBinary(bool exportSignature) 489 | { 490 | using (MemoryStream stream = new MemoryStream()) 491 | { 492 | using (BinaryWriter writer = new BinaryWriter(stream)) 493 | { 494 | byte[] serialBinary = Serial.ToByteArray(); 495 | 496 | if (serialBinary.Length > byte.MaxValue) 497 | { 498 | throw new InvalidOperationException("Invalid serial"); 499 | } 500 | 501 | writer.Write(Constants.CURRENT_VERSION); 502 | 503 | writer.Write((byte)serialBinary.Length); 504 | writer.Write(serialBinary); 505 | 506 | writer.Write(NotBefore != null); 507 | if (NotBefore != null) 508 | { 509 | writer.Write(NotBefore.Value.ToBinary()); 510 | } 511 | 512 | writer.Write(NotAfter != null); 513 | if (NotAfter != null) 514 | { 515 | writer.Write(NotAfter.Value.ToBinary()); 516 | } 517 | 518 | if (Attributes.Count > ushort.MaxValue) 519 | { 520 | throw new InvalidOperationException("Too many attributes"); 521 | } 522 | 523 | List> sortedAttributes = Attributes.Select(x => x).OrderBy(x => x.Key).ToList(); 524 | 525 | writer.Write((ushort)sortedAttributes.Count); 526 | foreach (KeyValuePair attribute in sortedAttributes) 527 | { 528 | writer.Write(attribute.Key); 529 | writer.Write(attribute.Value); 530 | } 531 | 532 | if (exportSignature) 533 | { 534 | if (Signature == null) 535 | { 536 | writer.Write((ushort)0); 537 | } 538 | else 539 | { 540 | writer.Write((ushort)Signature.Length); 541 | writer.Write(Signature); 542 | } 543 | } 544 | } 545 | 546 | return stream.ToArray(); 547 | } 548 | } 549 | } 550 | } 551 | -------------------------------------------------------------------------------- /LicencingNET/LicencingNET.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7.1 4 | net35;net45;net471;netcoreapp3.1;netstandard2.0 5 | 6 | 7 | -------------------------------------------------------------------------------- /LicencingNET/NTP.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Net.Sockets; 4 | 5 | namespace LicencingNET 6 | { 7 | internal static class NTP 8 | { 9 | // Address to NTP server 10 | private const string ntpServer = "pool.ntp.org"; 11 | // Offset to get to the "Transmit Timestamp" field (time at which the reply departed the server for the client, in 64-bit timestamp format.) 12 | private const byte serverTimeOffset = 40; 13 | 14 | internal static DateTime GetNetworkTime() 15 | { 16 | // NTP message size - 16 bytes of the digest (RFC 2030) 17 | byte[] ntpData = new byte[48]; 18 | 19 | // Setting the Leap Indicator, Version Number and Mode values 20 | ntpData[0] = 0x1B; //LI = 0 (no warning), VN = 3 (IPv4 only), Mode = 3 (Client Mode) 21 | 22 | // Resolve NTP address 23 | IPAddress[] addresses = Dns.GetHostEntry(ntpServer).AddressList; 24 | 25 | // The UDP port number assigned to NTP is 123 26 | IPEndPoint endPoint = new IPEndPoint(addresses[0], 123); 27 | 28 | // Create UDP socket 29 | using (Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp)) 30 | { 31 | socket.Connect(endPoint); 32 | 33 | // Stops code hang if NTP is blocked 34 | socket.ReceiveTimeout = 3000; 35 | socket.SendTimeout = 3000; 36 | 37 | // Sends NTP request 38 | socket.SendTo(ntpData, endPoint); 39 | 40 | // Waits for NTP response 41 | socket.Receive(ntpData); 42 | } 43 | 44 | // Seconds part (fixes endianess) 45 | ulong intPart = (ulong)ntpData[serverTimeOffset] << 24 | (ulong)ntpData[serverTimeOffset + 1] << 16 | (ulong)ntpData[serverTimeOffset + 2] << 8 | (ulong)ntpData[serverTimeOffset + 3]; 46 | // Seconds fraction (fixes endianess) 47 | ulong fractPart = (ulong)ntpData[serverTimeOffset + 4] << 24 | (ulong)ntpData[serverTimeOffset + 5] << 16 | (ulong)ntpData[serverTimeOffset + 6] << 8 | (ulong)ntpData[serverTimeOffset + 7]; 48 | 49 | // Milliseconds offset 50 | ulong milliseconds = (intPart * 1000) + ((fractPart * 1000) / 0x100000000L); 51 | 52 | // **UTC** time 53 | DateTime networkDateTime = (new DateTime(1900, 1, 1, 0, 0, 0, DateTimeKind.Utc)).AddMilliseconds((long)milliseconds); 54 | 55 | return networkDateTime.ToLocalTime(); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /LicencingNET/ValidationResult.cs: -------------------------------------------------------------------------------- 1 | namespace LicencingNET 2 | { 3 | /// 4 | /// Validation result. 5 | /// 6 | public enum ValidationResult 7 | { 8 | /// 9 | /// No failure. The licence is valid. 10 | /// 11 | Valid, 12 | /// 13 | /// The licence has expired. 14 | /// 15 | Exipired, 16 | /// 17 | /// The licence validity period has not yet started. 18 | /// 19 | NotStarted, 20 | /// 21 | /// The signature did not match. 22 | /// The licence might have been altered or is forged. 23 | /// 24 | InvalidSignature, 25 | /// 26 | /// The licence has no signature. 27 | /// 28 | NoSignature 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LicencingNET 2 | LicencingNET is a lightweight, cross platform and dependency free library for .NET that allows you to easily implement software licencing and product registration into your applications. It contains methods for generating, signing and validating licences. LicencingNET works by creating a licence with certain grants and then signing it cryptographically to prevent tamper. 3 | 4 | ## Features 5 | * Allows you to add ANY custom claims to a licence (for example, trial licences or licences that just unlock SOME features) 6 | * Supports Start and Expiration dates 7 | * Supports .NET 2.0 and above 8 | * Supports .NET Standard 2.0 9 | * Dependency free 10 | * Tamper proof licences 11 | * Backwards compatible licence format (Both binary and XML) 12 | * RSA and DSA support 13 | * Supports RSA/DSA keys or RSA/DSA certificate / PFXs 14 | * Tiny implementation to reduce attack surface (less than 100 lines for signing and verifying) 15 | * Uses NTP by default to counter clock change attacks (optional) 16 | 17 | 18 | ## Usage 19 | To create and validate licences. You need a RSA or DSA key pair. Either you can get this by generating a pair below, or by using certificates. This only has to be done once. 20 | ### Generate Keys 21 | 22 | ```csharp 23 | using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider(2048)) 24 | { 25 | RSAParameters privateKey = rsa.ExportParameters(true); 26 | RSAParameters publicKey = rsa.ExportParameters(false); 27 | 28 | XmlSerializer serializer = new XmlSerializer(typeof(RSAParameters)); 29 | 30 | using (StringWriter publicKeyWriter = new StringWriter()) 31 | using (StringWriter privateKeyWriter = new StringWriter()) 32 | { 33 | serializer.Serialize(privateKeyWriter, privateKey); 34 | serializer.Serialize(publicKeyWriter, publicKey); 35 | 36 | // Store this public key in all programs that has to validate a licence. 37 | // This is safe to distribute to clients. 38 | string publicKeyXml = publicKeyWriter.ToString(); 39 | // Store this private key on your server where you want to create licences. 40 | // This is secret! If anyone gets hold of it, they can create as many licences as they like. 41 | string privateKeyXml = privateKeyWriter.ToString(); 42 | } 43 | } 44 | ``` 45 | 46 | Now that you have the keys saved as XML. Before you use them with LicencingNET, they have to be turned back into RSAParameters. This can be done like this: 47 | 48 | ```csharp 49 | using (StringReader keyReader = new StringReader(xmlString)) 50 | { 51 | XmlSerializer serializer = new XmlSerializer(typeof(RSAParameters)); 52 | RSAParameters key = (RSAParameters)serializer.Deserialize(keyReader); 53 | } 54 | ``` 55 | 56 | ### Creating a licence 57 | First, you need to create a licence. Below is an example of how that can be done. This can ONLY be done on your server where you can keep the private key secret. 58 | 59 | ```csharp 60 | // Creates a trial licence that is valid from now, and for 30 days with information about the receiver of the licence. 61 | Licence licence = Licence.Create(null, DateTime.Now, DateTime.Now.AddDays(30), new Dictionary() 62 | { 63 | { "LicenceType", "Trial" }, 64 | { "CustomerName", "John Doe" }, 65 | { "CustomerEmail", "john.doe@contoso.com" }, 66 | { "CustomerCompany", "Contoso Ltd." } 67 | }); 68 | ``` 69 | 70 | ### Signing the licence 71 | After it has been created, it has to be signed to make it tamper proof. It can be done like this: 72 | 73 | ```csharp 74 | // Make sure you use the private key here. 75 | RSAParameters privateKey; 76 | using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider()) 77 | { 78 | // Import the private key. 79 | rsa.ImportParameters(privateKey); 80 | // Sign the licence. 81 | bool success = licence.Sign(rsa); 82 | } 83 | ``` 84 | 85 | ### Exporting licences 86 | Next, you can send it to your clients. Either in binary or in XML. It can be done like this: 87 | 88 | ```csharp 89 | byte[] binaryLicence = licence.ToBinary(); 90 | ``` 91 | 92 | ```csharp 93 | string xmlLicence = licence.ToXML(); 94 | ``` 95 | 96 | ### Importing licences 97 | Next, in your application. Simply validate the licence and enable features based on the licence grants. Start by deserializing it: 98 | 99 | ```csharp 100 | Licence licence = Licence.FromXML(xmlLicence); 101 | ``` 102 | 103 | ```csharp 104 | Licence licence = Licence.FromBinary(binaryLicence); 105 | ``` 106 | 107 | ### Validating licences 108 | Next, make sure the licence is valid. This requires the public key. This can be done like this: 109 | 110 | ```csharp 111 | // Make sure you use the public key here. 112 | RSAParameters publicKey; 113 | using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider()) 114 | { 115 | // Import the public key. 116 | rsa.ImportParameters(publicKey); 117 | 118 | // Validate the licence 119 | ValidationResult result = licence.Validate(rsa); 120 | 121 | if (result == ValidationResult.Valid) 122 | { 123 | // Licence is valid! 124 | // You can now safely use the attributes that you created. 125 | // Example: 126 | if (licence.Attributes.TryGetValue("LicenceType", out string licenceType)) 127 | { 128 | if (licenceType == "Trial") 129 | { 130 | // Activate trial features. 131 | } 132 | else if if (licenceType == "Full") 133 | { 134 | // Activate all features. 135 | } 136 | } 137 | } 138 | else 139 | { 140 | // Licence is not valid. 141 | Console.WriteLine("Invalid + " + failure); 142 | } 143 | } 144 | ``` --------------------------------------------------------------------------------