├── .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 | ```
--------------------------------------------------------------------------------