├── .editorconfig
├── .gitattributes
├── .github
└── workflows
│ ├── ci.yml
│ ├── release-nuseal-generator.yml
│ ├── release-nuseal.yml
│ └── release.yml
├── .gitignore
├── CODE_OF_CONDUCT.md
├── Directory.Packages.props
├── LICENSE.txt
├── NuSeal.sln
├── README.md
├── ci.slnf
├── clean.sh
├── coverage.sh
├── exclusion.dic
├── icon.png
├── logo.png
├── readme-nuget.md
├── samples
├── AuthorPackage
│ ├── AuthorPackage.csproj
│ ├── Foo.cs
│ └── public_key.pem
├── Directory.Packages.props
├── SampleNuGetFeed
│ └── AuthorPackage.0.4.0.nupkg
└── UserApp
│ ├── Program.cs
│ ├── UserApp.csproj
│ └── YourProductName.lic
├── src
├── NuSeal.Generator
│ ├── License.cs
│ ├── LicenseParameters.cs
│ ├── NuSeal.Generator.csproj
│ └── RsaKeyGenerator.cs
├── NuSeal
│ ├── Assets.cs
│ ├── ConsumerParameters.cs
│ ├── FileUtils.cs
│ ├── LicenseValidationResult.cs
│ ├── LicenseValidator.cs
│ ├── NuSeal.csproj
│ ├── NuSealOptions.cs
│ ├── NuSealValidationMode.cs
│ ├── NuSealValidationScope.cs
│ ├── PemData.cs
│ ├── Tasks
│ │ ├── GenerateConsumerAssetsTask.cs
│ │ └── ValidateLicenseTask_0_4_0.cs
│ └── build
│ │ ├── NuSeal.props
│ │ └── NuSeal.targets
├── ProjectMetadata.props
└── ProjectMetadata.targets
└── tests
└── NuSeal.Tests
├── AssetsTests.cs
├── ConsumerParametersTests.cs
├── FileUtils_TryGetLicenseTests.cs
├── GenerateConsumerAssetsTaskTests.cs
├── GlobalUsings.cs
├── LicenseValidatorTests.cs
├── NuSeal.Tests.csproj
├── TestLogger.cs
└── ValidateLicenseTaskTests_0_4_0.cs
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Remove the line below if you want to inherit .editorconfig settings from higher directories
2 | root = true
3 |
4 | #### Core EditorConfig Options ####
5 |
6 | # All files
7 | [*]
8 | charset = utf-8
9 | indent_style = space
10 | indent_size = 2
11 | tab_width = 2
12 | end_of_line = crlf
13 | trim_trailing_whitespace = true
14 | insert_final_newline = true
15 | spelling_exclusion_path = exclusion.dic
16 |
17 | # bash scripts
18 | [*.{sh,bash}]
19 | end_of_line = lf
20 |
21 | # C# and VB files
22 | [*.{cs,vb}]
23 | indent_size = 4
24 | tab_width = 4
25 | charset = utf-8-bom
26 |
27 | #### .NET Code Actions ####
28 |
29 | # Type members
30 | dotnet_hide_advanced_members = false
31 | dotnet_member_insertion_location = with_other_members_of_the_same_kind
32 | dotnet_property_generation_behavior = prefer_throwing_properties
33 |
34 | # Symbol search
35 | dotnet_search_reference_assemblies = true
36 |
37 | #### .NET Coding Conventions ####
38 |
39 | # Organize usings
40 | dotnet_separate_import_directive_groups = false
41 | dotnet_sort_system_directives_first = false
42 | file_header_template = unset
43 |
44 | # this. and Me. preferences
45 | dotnet_style_qualification_for_field = false:silent
46 | dotnet_style_qualification_for_property = false:silent
47 | dotnet_style_qualification_for_method = false:silent
48 | dotnet_style_qualification_for_event = false:silent
49 |
50 | # Language keywords vs BCL types preferences
51 | dotnet_style_predefined_type_for_locals_parameters_members = true:silent
52 | dotnet_style_predefined_type_for_member_access = true:silent
53 |
54 | # Parentheses preferences
55 | dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent
56 | dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent
57 | dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent
58 | dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
59 |
60 | # Modifier preferences
61 | dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
62 |
63 | # Expression-level preferences
64 | dotnet_prefer_system_hash_code = true
65 | dotnet_style_coalesce_expression = true:suggestion
66 | dotnet_style_collection_initializer = true:silent
67 | dotnet_style_explicit_tuple_names = true:suggestion
68 | dotnet_style_namespace_match_folder = true:silent
69 | dotnet_style_null_propagation = true:suggestion
70 | dotnet_style_object_initializer = true:silent
71 | dotnet_style_operator_placement_when_wrapping = beginning_of_line
72 | dotnet_style_prefer_auto_properties = true:silent
73 | dotnet_style_prefer_collection_expression = when_types_loosely_match:silent
74 | dotnet_style_prefer_compound_assignment = true:suggestion
75 | dotnet_style_prefer_conditional_expression_over_assignment = true:silent
76 | dotnet_style_prefer_conditional_expression_over_return = true:silent
77 | dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed
78 | dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
79 | dotnet_style_prefer_inferred_tuple_names = true:suggestion
80 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
81 | dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
82 | dotnet_style_prefer_simplified_interpolation = true:suggestion
83 |
84 | # Field preferences
85 | dotnet_style_readonly_field = true:suggestion
86 |
87 | # Parameter preferences
88 | dotnet_code_quality_unused_parameters = all:suggestion
89 |
90 | # Suppression preferences
91 | dotnet_remove_unnecessary_suppression_exclusions = none
92 |
93 | # New line preferences
94 | dotnet_style_allow_multiple_blank_lines_experimental = true:silent
95 | dotnet_style_allow_statement_immediately_after_block_experimental = true:silent
96 |
97 | #### Naming styles ####
98 |
99 | # Naming rules
100 |
101 | dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
102 | dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
103 | dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
104 |
105 | dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
106 | dotnet_naming_rule.types_should_be_pascal_case.symbols = types
107 | dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
108 |
109 | dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
110 | dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
111 | dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
112 |
113 | dotnet_naming_rule.private_or_internal_field_should_be__fieldname.severity = suggestion
114 | dotnet_naming_rule.private_or_internal_field_should_be__fieldname.symbols = private_or_internal_field
115 | dotnet_naming_rule.private_or_internal_field_should_be__fieldname.style = _fieldname
116 |
117 | dotnet_naming_rule.public_field_should_be_pascal_case.severity = suggestion
118 | dotnet_naming_rule.public_field_should_be_pascal_case.symbols = public_field
119 | dotnet_naming_rule.public_field_should_be_pascal_case.style = pascal_case
120 |
121 | # Symbol specifications
122 |
123 | dotnet_naming_symbols.interface.applicable_kinds = interface
124 | dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
125 | dotnet_naming_symbols.interface.required_modifiers =
126 |
127 | dotnet_naming_symbols.private_or_internal_field.applicable_kinds = field
128 | dotnet_naming_symbols.private_or_internal_field.applicable_accessibilities = internal, private, private_protected
129 | dotnet_naming_symbols.private_or_internal_field.required_modifiers =
130 |
131 | dotnet_naming_symbols.public_field.applicable_kinds = field
132 | dotnet_naming_symbols.public_field.applicable_accessibilities = public
133 | dotnet_naming_symbols.public_field.required_modifiers =
134 |
135 | dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
136 | dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
137 | dotnet_naming_symbols.types.required_modifiers =
138 |
139 | dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
140 | dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
141 | dotnet_naming_symbols.non_field_members.required_modifiers =
142 |
143 | # Naming styles
144 |
145 | dotnet_naming_style.pascal_case.required_prefix =
146 | dotnet_naming_style.pascal_case.required_suffix =
147 | dotnet_naming_style.pascal_case.word_separator =
148 | dotnet_naming_style.pascal_case.capitalization = pascal_case
149 |
150 | dotnet_naming_style.begins_with_i.required_prefix = I
151 | dotnet_naming_style.begins_with_i.required_suffix =
152 | dotnet_naming_style.begins_with_i.word_separator =
153 | dotnet_naming_style.begins_with_i.capitalization = pascal_case
154 |
155 | dotnet_naming_style._fieldname.required_prefix = _
156 | dotnet_naming_style._fieldname.required_suffix =
157 | dotnet_naming_style._fieldname.word_separator =
158 | dotnet_naming_style._fieldname.capitalization = camel_case
159 |
160 | # Analyzers
161 |
162 | dotnet_diagnostic.CA2211.severity = silent
163 |
164 | # C# files
165 | [*.cs]
166 |
167 | #### C# Coding Conventions ####
168 |
169 | # var preferences
170 | csharp_style_var_elsewhere = true:silent
171 | csharp_style_var_for_built_in_types = true:silent
172 | csharp_style_var_when_type_is_apparent = true:silent
173 |
174 | # Expression-bodied members
175 | csharp_style_expression_bodied_accessors = true:silent
176 | csharp_style_expression_bodied_constructors = false:silent
177 | csharp_style_expression_bodied_indexers = true:silent
178 | csharp_style_expression_bodied_lambdas = true:silent
179 | csharp_style_expression_bodied_local_functions = false:silent
180 | csharp_style_expression_bodied_methods = false:silent
181 | csharp_style_expression_bodied_operators = false:silent
182 | csharp_style_expression_bodied_properties = true:silent
183 |
184 | # Pattern matching preferences
185 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
186 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
187 | csharp_style_prefer_extended_property_pattern = true:suggestion
188 | csharp_style_prefer_not_pattern = true:suggestion
189 | csharp_style_prefer_pattern_matching = true:silent
190 | csharp_style_prefer_switch_expression = true:suggestion
191 |
192 | # Null-checking preferences
193 | csharp_style_conditional_delegate_call = true:suggestion
194 |
195 | # Modifier preferences
196 | csharp_prefer_static_anonymous_function = true:suggestion
197 | csharp_prefer_static_local_function = true:suggestion
198 | csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async
199 | csharp_style_prefer_readonly_struct = true:suggestion
200 | csharp_style_prefer_readonly_struct_member = true:suggestion
201 |
202 | # Code-block preferences
203 | csharp_prefer_braces = true:silent
204 | csharp_prefer_simple_using_statement = true:silent
205 | csharp_prefer_system_threading_lock = true:suggestion
206 | csharp_style_namespace_declarations = file_scoped:silent
207 | csharp_style_prefer_method_group_conversion = true:silent
208 | csharp_style_prefer_primary_constructors = true:silent
209 | csharp_style_prefer_top_level_statements = true:silent
210 |
211 | # Expression-level preferences
212 | csharp_prefer_simple_default_expression = true:suggestion
213 | csharp_style_deconstructed_variable_declaration = true:suggestion
214 | csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion
215 | csharp_style_inlined_variable_declaration = true:suggestion
216 | csharp_style_prefer_index_operator = true:suggestion
217 | csharp_style_prefer_local_over_anonymous_function = true:suggestion
218 | csharp_style_prefer_null_check_over_type_check = true:suggestion
219 | csharp_style_prefer_range_operator = true:suggestion
220 | csharp_style_prefer_tuple_swap = true:suggestion
221 | csharp_style_prefer_utf8_string_literals = true:suggestion
222 | csharp_style_throw_expression = true:suggestion
223 | csharp_style_unused_value_assignment_preference = discard_variable:suggestion
224 | csharp_style_unused_value_expression_statement_preference = discard_variable:silent
225 |
226 | # 'using' directive preferences
227 | csharp_using_directive_placement = outside_namespace:silent
228 |
229 | # New line preferences
230 | csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent
231 | csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true:silent
232 | csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true:silent
233 | csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true:silent
234 | csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent
235 |
236 | #### C# Formatting Rules ####
237 |
238 | # New line preferences
239 | csharp_new_line_before_catch = true
240 | csharp_new_line_before_else = true
241 | csharp_new_line_before_finally = true
242 | csharp_new_line_before_members_in_anonymous_types = true
243 | csharp_new_line_before_members_in_object_initializers = true
244 | csharp_new_line_before_open_brace = all
245 | csharp_new_line_between_query_expression_clauses = true
246 |
247 | # Indentation preferences
248 | csharp_indent_block_contents = true
249 | csharp_indent_braces = false
250 | csharp_indent_case_contents = true
251 | csharp_indent_case_contents_when_block = true
252 | csharp_indent_labels = one_less_than_current
253 | csharp_indent_switch_labels = true
254 |
255 | # Space preferences
256 | csharp_space_after_cast = false
257 | csharp_space_after_colon_in_inheritance_clause = true
258 | csharp_space_after_comma = true
259 | csharp_space_after_dot = false
260 | csharp_space_after_keywords_in_control_flow_statements = true
261 | csharp_space_after_semicolon_in_for_statement = true
262 | csharp_space_around_binary_operators = before_and_after
263 | csharp_space_around_declaration_statements = false
264 | csharp_space_before_colon_in_inheritance_clause = true
265 | csharp_space_before_comma = false
266 | csharp_space_before_dot = false
267 | csharp_space_before_open_square_brackets = false
268 | csharp_space_before_semicolon_in_for_statement = false
269 | csharp_space_between_empty_square_brackets = false
270 | csharp_space_between_method_call_empty_parameter_list_parentheses = false
271 | csharp_space_between_method_call_name_and_opening_parenthesis = false
272 | csharp_space_between_method_call_parameter_list_parentheses = false
273 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
274 | csharp_space_between_method_declaration_name_and_open_parenthesis = false
275 | csharp_space_between_method_declaration_parameter_list_parentheses = false
276 | csharp_space_between_parentheses = false
277 | csharp_space_between_square_brackets = false
278 |
279 | # Wrapping preferences
280 | csharp_preserve_single_line_blocks = true
281 | csharp_preserve_single_line_statements = true
282 |
283 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | ###############################################################################
2 | # Set default behavior to automatically normalize line endings.
3 | ###############################################################################
4 | * text=auto
5 |
6 | # Declare files that will always have LF line endings on checkout.
7 | *.sh text eol=lf
8 |
9 | ###############################################################################
10 | # Set default behavior for command prompt diff.
11 | #
12 | # This is need for earlier builds of msysgit that does not have it on by
13 | # default for csharp files.
14 | # Note: This is only used by command line
15 | ###############################################################################
16 | #*.cs diff=csharp
17 |
18 | ###############################################################################
19 | # Set the merge driver for project and solution files
20 | #
21 | # Merging from the command prompt will add diff markers to the files if there
22 | # are conflicts (Merging from VS is not affected by the settings below, in VS
23 | # the diff markers are never inserted). Diff markers may cause the following
24 | # file extensions to fail to load in VS. An alternative would be to treat
25 | # these files as binary and thus will always conflict and require user
26 | # intervention with every merge. To do so, just uncomment the entries below
27 | ###############################################################################
28 | #*.sln merge=binary
29 | #*.csproj merge=binary
30 | #*.vbproj merge=binary
31 | #*.vcxproj merge=binary
32 | #*.vcproj merge=binary
33 | #*.dbproj merge=binary
34 | #*.fsproj merge=binary
35 | #*.lsproj merge=binary
36 | #*.wixproj merge=binary
37 | #*.modelproj merge=binary
38 | #*.sqlproj merge=binary
39 | #*.wwaproj merge=binary
40 |
41 | ###############################################################################
42 | # behavior for image files
43 | #
44 | # image files are treated as binary by default.
45 | ###############################################################################
46 | *.jpg binary
47 | *.png binary
48 | *.gif binary
49 | *.ico binary
50 |
51 | ###############################################################################
52 | # diff behavior for common document formats
53 | #
54 | # Convert binary document formats to text before diffing them. This feature
55 | # is only available from the command line. Turn it on by uncommenting the
56 | # entries below.
57 | ###############################################################################
58 | #*.doc diff=astextplain
59 | #*.DOC diff=astextplain
60 | #*.docx diff=astextplain
61 | #*.DOCX diff=astextplain
62 | #*.dot diff=astextplain
63 | #*.DOT diff=astextplain
64 | #*.pdf diff=astextplain
65 | #*.PDF diff=astextplain
66 | #*.rtf diff=astextplain
67 | #*.RTF diff=astextplain
68 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: Build and Test
2 |
3 | on:
4 | workflow_dispatch:
5 | pull_request:
6 | branches:
7 | - main
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - name: Checkout
15 | uses: actions/checkout@v4
16 | - name: Setup dotnet
17 | uses: actions/setup-dotnet@v4
18 | with:
19 | dotnet-version: 9.x
20 | - name: Build
21 | run: dotnet build ci.slnf --configuration Release
22 | - name: Test
23 | run: dotnet test ci.slnf --configuration Release --no-build --no-restore
24 |
--------------------------------------------------------------------------------
/.github/workflows/release-nuseal-generator.yml:
--------------------------------------------------------------------------------
1 | name: Release NuSeal.Generator
2 |
3 | on:
4 | workflow_dispatch:
5 | release:
6 | types: [published]
7 |
8 | jobs:
9 | build:
10 | runs-on: ubuntu-latest
11 | if: contains(github.event.release.tag_name, 'nuseal-generator-v')
12 |
13 | steps:
14 | - name: Checkout
15 | uses: actions/checkout@v4
16 |
17 | - name: Setup dotnet
18 | uses: actions/setup-dotnet@v4
19 | with:
20 | dotnet-version: 9.x
21 |
22 | - name: Build
23 | run: dotnet build ci.slnf --configuration Release
24 |
25 | - name: Test
26 | run: dotnet test ci.slnf --configuration Release --no-build --no-restore
27 |
28 | - name: Pack
29 | run: dotnet pack ci.slnf --configuration Release --no-build --no-restore --output .
30 |
31 | - name: Push to NuGet
32 | run: dotnet nuget push "NuSeal.*.nupkg" --api-key ${{secrets.NUGET_API_KEY}} --source https://api.nuget.org/v3/index.json
33 |
--------------------------------------------------------------------------------
/.github/workflows/release-nuseal.yml:
--------------------------------------------------------------------------------
1 | name: Release NuSeal
2 |
3 | on:
4 | workflow_dispatch:
5 | release:
6 | types: [published]
7 |
8 | jobs:
9 | build:
10 | runs-on: ubuntu-latest
11 | if: contains(github.event.release.tag_name, 'nuseal-v')
12 |
13 | steps:
14 | - name: Checkout
15 | uses: actions/checkout@v4
16 |
17 | - name: Setup dotnet
18 | uses: actions/setup-dotnet@v4
19 | with:
20 | dotnet-version: 9.x
21 |
22 | - name: Build
23 | run: dotnet build ci.slnf --configuration Release
24 |
25 | - name: Test
26 | run: dotnet test ci.slnf --configuration Release --no-build --no-restore
27 |
28 | - name: Pack
29 | run: dotnet pack ci.slnf --configuration Release --no-build --no-restore --output .
30 |
31 | - name: Push to NuGet
32 | run: dotnet nuget push "NuSeal.*.nupkg" --api-key ${{secrets.NUGET_API_KEY}} --source https://api.nuget.org/v3/index.json
33 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release All Packages
2 |
3 | on:
4 | workflow_dispatch:
5 | release:
6 | types: [published]
7 |
8 | jobs:
9 | build:
10 | runs-on: ubuntu-latest
11 | if: "!contains(github.event.release.tag_name, 'nuseal-v') && !contains(github.event.release.tag_name, 'nuseal-generator-v')"
12 |
13 | steps:
14 | - name: Checkout
15 | uses: actions/checkout@v4
16 |
17 | - name: Setup dotnet
18 | uses: actions/setup-dotnet@v4
19 | with:
20 | dotnet-version: 9.x
21 |
22 | - name: Build
23 | run: dotnet build ci.slnf --configuration Release
24 |
25 | - name: Test
26 | run: dotnet test ci.slnf --configuration Release --no-build --no-restore
27 |
28 | - name: Pack
29 | run: dotnet pack ci.slnf --configuration Release --no-build --no-restore --output .
30 |
31 | - name: Push to NuGet
32 | run: dotnet nuget push "*.nupkg" --api-key ${{secrets.NUGET_API_KEY}} --source https://api.nuget.org/v3/index.json
33 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.rsuser
8 | *.suo
9 | *.user
10 | *.userosscache
11 | *.sln.docstates
12 |
13 | # User-specific files (MonoDevelop/Xamarin Studio)
14 | *.userprefs
15 |
16 | # Mono auto generated files
17 | mono_crash.*
18 |
19 | # Build results
20 | [Dd]ebug/
21 | [Dd]ebugPublic/
22 | [Rr]elease/
23 | [Rr]eleases/
24 | x64/
25 | x86/
26 | [Ww][Ii][Nn]32/
27 | [Aa][Rr][Mm]/
28 | [Aa][Rr][Mm]64/
29 | bld/
30 | [Bb]in/
31 | [Oo]bj/
32 | [Oo]ut/
33 | [Ll]og/
34 | [Ll]ogs/
35 |
36 | # Visual Studio 2015/2017 cache/options directory
37 | .vs/
38 | # Uncomment if you have tasks that create the project's static files in wwwroot
39 | #wwwroot/
40 |
41 | # Visual Studio 2017 auto generated files
42 | Generated\ Files/
43 |
44 | # MSTest test Results
45 | [Tt]est[Rr]esult*/
46 | [Bb]uild[Ll]og.*
47 |
48 | # NUnit
49 | *.VisualState.xml
50 | TestResult.xml
51 | nunit-*.xml
52 |
53 | # Build Results of an ATL Project
54 | [Dd]ebugPS/
55 | [Rr]eleasePS/
56 | dlldata.c
57 |
58 | # Benchmark Results
59 | BenchmarkDotNet.Artifacts/
60 |
61 | # .NET Core
62 | project.lock.json
63 | project.fragment.lock.json
64 | artifacts/
65 |
66 | # ASP.NET Scaffolding
67 | ScaffoldingReadMe.txt
68 |
69 | # StyleCop
70 | StyleCopReport.xml
71 |
72 | # Files built by Visual Studio
73 | *_i.c
74 | *_p.c
75 | *_h.h
76 | *.ilk
77 | *.meta
78 | *.obj
79 | *.iobj
80 | *.pch
81 | *.pdb
82 | *.ipdb
83 | *.pgc
84 | *.pgd
85 | *.rsp
86 | *.sbr
87 | *.tlb
88 | *.tli
89 | *.tlh
90 | *.tmp
91 | *.tmp_proj
92 | *_wpftmp.csproj
93 | *.log
94 | *.vspscc
95 | *.vssscc
96 | .builds
97 | *.pidb
98 | *.svclog
99 | *.scc
100 |
101 | # Chutzpah Test files
102 | _Chutzpah*
103 |
104 | # Visual C++ cache files
105 | ipch/
106 | *.aps
107 | *.ncb
108 | *.opendb
109 | *.opensdf
110 | *.sdf
111 | *.cachefile
112 | *.VC.db
113 | *.VC.VC.opendb
114 |
115 | # Visual Studio profiler
116 | *.psess
117 | *.vsp
118 | *.vspx
119 | *.sap
120 |
121 | # Visual Studio Trace Files
122 | *.e2e
123 |
124 | # TFS 2012 Local Workspace
125 | $tf/
126 |
127 | # Guidance Automation Toolkit
128 | *.gpState
129 |
130 | # ReSharper is a .NET coding add-in
131 | _ReSharper*/
132 | *.[Rr]e[Ss]harper
133 | *.DotSettings.user
134 |
135 | # TeamCity is a build add-in
136 | _TeamCity*
137 |
138 | # DotCover is a Code Coverage Tool
139 | *.dotCover
140 |
141 | # AxoCover is a Code Coverage Tool
142 | .axoCover/*
143 | !.axoCover/settings.json
144 |
145 | # Coverlet is a free, cross platform Code Coverage Tool
146 | coverage*.json
147 | coverage*.xml
148 | coverage*.info
149 |
150 | # Visual Studio code coverage results
151 | *.coverage
152 | *.coveragexml
153 |
154 | # NCrunch
155 | _NCrunch_*
156 | .*crunch*.local.xml
157 | nCrunchTemp_*
158 |
159 | # MightyMoose
160 | *.mm.*
161 | AutoTest.Net/
162 |
163 | # Web workbench (sass)
164 | .sass-cache/
165 |
166 | # Installshield output folder
167 | [Ee]xpress/
168 |
169 | # DocProject is a documentation generator add-in
170 | DocProject/buildhelp/
171 | DocProject/Help/*.HxT
172 | DocProject/Help/*.HxC
173 | DocProject/Help/*.hhc
174 | DocProject/Help/*.hhk
175 | DocProject/Help/*.hhp
176 | DocProject/Help/Html2
177 | DocProject/Help/html
178 |
179 | # Click-Once directory
180 | publish/
181 |
182 | # Publish Web Output
183 | *.[Pp]ublish.xml
184 | *.azurePubxml
185 | # Note: Comment the next line if you want to checkin your web deploy settings,
186 | # but database connection strings (with potential passwords) will be unencrypted
187 | *.pubxml
188 | *.publishproj
189 |
190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
191 | # checkin your Azure Web App publish settings, but sensitive information contained
192 | # in these scripts will be unencrypted
193 | PublishScripts/
194 |
195 | # NuGet Packages
196 | *.nupkg
197 | # NuGet Symbol Packages
198 | *.snupkg
199 | # The packages folder can be ignored because of Package Restore
200 | **/[Pp]ackages/*
201 | # except build/, which is used as an MSBuild target.
202 | !**/[Pp]ackages/build/
203 | # Uncomment if necessary however generally it will be regenerated when needed
204 | #!**/[Pp]ackages/repositories.config
205 | # NuGet v3's project.json files produces more ignorable files
206 | *.nuget.props
207 | *.nuget.targets
208 |
209 | # Microsoft Azure Build Output
210 | csx/
211 | *.build.csdef
212 |
213 | # Microsoft Azure Emulator
214 | ecf/
215 | rcf/
216 |
217 | # Windows Store app package directories and files
218 | AppPackages/
219 | BundleArtifacts/
220 | Package.StoreAssociation.xml
221 | _pkginfo.txt
222 | *.appx
223 | *.appxbundle
224 | *.appxupload
225 |
226 | # Visual Studio cache files
227 | # files ending in .cache can be ignored
228 | *.[Cc]ache
229 | # but keep track of directories ending in .cache
230 | !?*.[Cc]ache/
231 |
232 | # Others
233 | ClientBin/
234 | ~$*
235 | *~
236 | *.dbmdl
237 | *.dbproj.schemaview
238 | *.jfm
239 | *.pfx
240 | *.publishsettings
241 | orleans.codegen.cs
242 |
243 | # Including strong name files can present a security risk
244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
245 | #*.snk
246 |
247 | # Since there are multiple workflows, uncomment next line to ignore bower_components
248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
249 | #bower_components/
250 |
251 | # RIA/Silverlight projects
252 | Generated_Code/
253 |
254 | # Backup & report files from converting an old project file
255 | # to a newer Visual Studio version. Backup files are not needed,
256 | # because we have git ;-)
257 | _UpgradeReport_Files/
258 | Backup*/
259 | UpgradeLog*.XML
260 | UpgradeLog*.htm
261 | ServiceFabricBackup/
262 | *.rptproj.bak
263 |
264 | # SQL Server files
265 | *.mdf
266 | *.ldf
267 | *.ndf
268 |
269 | # Business Intelligence projects
270 | *.rdl.data
271 | *.bim.layout
272 | *.bim_*.settings
273 | *.rptproj.rsuser
274 | *- [Bb]ackup.rdl
275 | *- [Bb]ackup ([0-9]).rdl
276 | *- [Bb]ackup ([0-9][0-9]).rdl
277 |
278 | # Microsoft Fakes
279 | FakesAssemblies/
280 |
281 | # GhostDoc plugin setting file
282 | *.GhostDoc.xml
283 |
284 | # Node.js Tools for Visual Studio
285 | .ntvs_analysis.dat
286 | node_modules/
287 |
288 | # Visual Studio 6 build log
289 | *.plg
290 |
291 | # Visual Studio 6 workspace options file
292 | *.opt
293 |
294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
295 | *.vbw
296 |
297 | # Visual Studio LightSwitch build output
298 | **/*.HTMLClient/GeneratedArtifacts
299 | **/*.DesktopClient/GeneratedArtifacts
300 | **/*.DesktopClient/ModelManifest.xml
301 | **/*.Server/GeneratedArtifacts
302 | **/*.Server/ModelManifest.xml
303 | _Pvt_Extensions
304 |
305 | # Paket dependency manager
306 | .paket/paket.exe
307 | paket-files/
308 |
309 | # FAKE - F# Make
310 | .fake/
311 |
312 | # CodeRush personal settings
313 | .cr/personal
314 |
315 | # Python Tools for Visual Studio (PTVS)
316 | __pycache__/
317 | *.pyc
318 |
319 | # Cake - Uncomment if you are using it
320 | # tools/**
321 | # !tools/packages.config
322 |
323 | # Tabs Studio
324 | *.tss
325 |
326 | # Telerik's JustMock configuration file
327 | *.jmconfig
328 |
329 | # BizTalk build output
330 | *.btp.cs
331 | *.btm.cs
332 | *.odx.cs
333 | *.xsd.cs
334 |
335 | # OpenCover UI analysis results
336 | OpenCover/
337 |
338 | # Azure Stream Analytics local run output
339 | ASALocalRun/
340 |
341 | # MSBuild Binary and Structured Log
342 | *.binlog
343 |
344 | # NVidia Nsight GPU debugger configuration file
345 | *.nvuser
346 |
347 | # MFractors (Xamarin productivity tool) working folder
348 | .mfractor/
349 |
350 | # Local History for Visual Studio
351 | .localhistory/
352 |
353 | # BeatPulse healthcheck temp database
354 | healthchecksdb
355 |
356 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
357 | MigrationBackup/
358 |
359 | # Ionide (cross platform F# VS Code tools) working folder
360 | .ionide/
361 |
362 | # Fody - auto-generated XML schema
363 | FodyWeavers.xsd
364 |
365 | # Common IntelliJ Platform excludes
366 | # User specific
367 | **/.idea/**/workspace.xml
368 | **/.idea/**/tasks.xml
369 | **/.idea/shelf/*
370 | **/.idea/dictionaries
371 | **/.idea/httpRequests/
372 |
373 | # Sensitive or high-churn files
374 | **/.idea/**/dataSources/
375 | **/.idea/**/dataSources.ids
376 | **/.idea/**/dataSources.xml
377 | **/.idea/**/dataSources.local.xml
378 | **/.idea/**/sqlDataSources.xml
379 | **/.idea/**/dynamic.xml
380 |
381 | # Rider
382 | # Rider auto-generates .iml files, and contentModel.xml
383 | **/.idea/**/*.iml
384 | **/.idea/**/contentModel.xml
385 | **/.idea/**/modules.xml
386 |
387 | # Developer config files
388 | **/appsettings.Development*.json
389 | !/samples/SampleNuGetFeed/*.nupkg
390 | /.idea
391 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Code of Conduct
2 |
3 | This project has adopted the code of conduct defined by the Contributor Covenant
4 | to clarify expected behavior in our community.
5 | For more information, see the [.NET Foundation Code of Conduct](https://dotnetfoundation.org/code-of-conduct).
6 |
--------------------------------------------------------------------------------
/Directory.Packages.props:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | true
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Fati Iseni
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.
22 |
--------------------------------------------------------------------------------
/NuSeal.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.14.36414.22
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NuSeal", "src\NuSeal\NuSeal.csproj", "{E6DBA299-2C55-4F52-829A-CADA5F298F85}"
7 | EndProject
8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_meta", "_meta", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
9 | ProjectSection(SolutionItems) = preProject
10 | .editorconfig = .editorconfig
11 | .gitattributes = .gitattributes
12 | .gitignore = .gitignore
13 | .github\workflows\ci.yml = .github\workflows\ci.yml
14 | Directory.Packages.props = Directory.Packages.props
15 | exclusion.dic = exclusion.dic
16 | src\ProjectMetadata.props = src\ProjectMetadata.props
17 | src\ProjectMetadata.targets = src\ProjectMetadata.targets
18 | readme-nuget.md = readme-nuget.md
19 | README.md = README.md
20 | .github\workflows\release-nuseal-generator.yml = .github\workflows\release-nuseal-generator.yml
21 | .github\workflows\release-nuseal.yml = .github\workflows\release-nuseal.yml
22 | .github\workflows\release.yml = .github\workflows\release.yml
23 | EndProjectSection
24 | EndProject
25 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NuSeal.Generator", "src\NuSeal.Generator\NuSeal.Generator.csproj", "{C28C3A06-FC92-4B0E-90F2-77F8FB47EC99}"
26 | EndProject
27 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{3966E974-E9A7-4C0B-A5A6-444CE97BDBC3}"
28 | EndProject
29 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UserApp", "samples\UserApp\UserApp.csproj", "{A74FC223-4864-49A2-B983-A31C7884E290}"
30 | EndProject
31 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AuthorPackage", "samples\AuthorPackage\AuthorPackage.csproj", "{A8A700B8-64D9-4FAE-8645-7D120F918885}"
32 | EndProject
33 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NuSeal.Tests", "tests\NuSeal.Tests\NuSeal.Tests.csproj", "{BD4624C4-62AF-438F-ABE3-7EFC8C5E141B}"
34 | EndProject
35 | Global
36 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
37 | Debug|Any CPU = Debug|Any CPU
38 | Release|Any CPU = Release|Any CPU
39 | EndGlobalSection
40 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
41 | {E6DBA299-2C55-4F52-829A-CADA5F298F85}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
42 | {E6DBA299-2C55-4F52-829A-CADA5F298F85}.Debug|Any CPU.Build.0 = Debug|Any CPU
43 | {E6DBA299-2C55-4F52-829A-CADA5F298F85}.Release|Any CPU.ActiveCfg = Release|Any CPU
44 | {E6DBA299-2C55-4F52-829A-CADA5F298F85}.Release|Any CPU.Build.0 = Release|Any CPU
45 | {C28C3A06-FC92-4B0E-90F2-77F8FB47EC99}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
46 | {C28C3A06-FC92-4B0E-90F2-77F8FB47EC99}.Debug|Any CPU.Build.0 = Debug|Any CPU
47 | {C28C3A06-FC92-4B0E-90F2-77F8FB47EC99}.Release|Any CPU.ActiveCfg = Release|Any CPU
48 | {C28C3A06-FC92-4B0E-90F2-77F8FB47EC99}.Release|Any CPU.Build.0 = Release|Any CPU
49 | {A74FC223-4864-49A2-B983-A31C7884E290}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
50 | {A74FC223-4864-49A2-B983-A31C7884E290}.Debug|Any CPU.Build.0 = Debug|Any CPU
51 | {A74FC223-4864-49A2-B983-A31C7884E290}.Release|Any CPU.ActiveCfg = Release|Any CPU
52 | {A74FC223-4864-49A2-B983-A31C7884E290}.Release|Any CPU.Build.0 = Release|Any CPU
53 | {A8A700B8-64D9-4FAE-8645-7D120F918885}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
54 | {A8A700B8-64D9-4FAE-8645-7D120F918885}.Debug|Any CPU.Build.0 = Debug|Any CPU
55 | {A8A700B8-64D9-4FAE-8645-7D120F918885}.Release|Any CPU.ActiveCfg = Release|Any CPU
56 | {A8A700B8-64D9-4FAE-8645-7D120F918885}.Release|Any CPU.Build.0 = Release|Any CPU
57 | {BD4624C4-62AF-438F-ABE3-7EFC8C5E141B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
58 | {BD4624C4-62AF-438F-ABE3-7EFC8C5E141B}.Debug|Any CPU.Build.0 = Debug|Any CPU
59 | {BD4624C4-62AF-438F-ABE3-7EFC8C5E141B}.Release|Any CPU.ActiveCfg = Release|Any CPU
60 | {BD4624C4-62AF-438F-ABE3-7EFC8C5E141B}.Release|Any CPU.Build.0 = Release|Any CPU
61 | EndGlobalSection
62 | GlobalSection(SolutionProperties) = preSolution
63 | HideSolutionNode = FALSE
64 | EndGlobalSection
65 | GlobalSection(NestedProjects) = preSolution
66 | {A74FC223-4864-49A2-B983-A31C7884E290} = {3966E974-E9A7-4C0B-A5A6-444CE97BDBC3}
67 | {A8A700B8-64D9-4FAE-8645-7D120F918885} = {3966E974-E9A7-4C0B-A5A6-444CE97BDBC3}
68 | EndGlobalSection
69 | GlobalSection(ExtensibilityGlobals) = postSolution
70 | SolutionGuid = {1E0303A6-17E3-4F2F-9F41-C99524B8D485}
71 | EndGlobalSection
72 | EndGlobal
73 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | [](https://www.nuget.org/packages/NuSeal)
4 |
5 | [](https://github.com/fiseni/NuSeal/actions/workflows/ci.yml)
6 |
7 |
8 |
9 | ---
10 | NuSeal provides the infrastructure for creating and validating NuGet package licenses. The validation occurs during build time (offline), preventing unauthorized use of your packages. It's designed to be generic while allowing each author to set their own public key and license policies.
11 |
12 | Packages:
13 |
14 | 1. [NuSeal](https://www.nuget.org/packages/NuSeal) - Core package that validates licenses during build time (`netstandard2.0` library)
15 | 2. [NuSeal.Generator](https://www.nuget.org/packages/NuSeal.Generator) - Helper package for generating RSA key pairs and licenses (`net8.0` library)
16 |
17 | ## Table of Contents
18 | - [TL;DR](#tldr)
19 | - [For Package Authors](#for-package-authors)
20 | - [1. Create RSA Key Pairs](#1-create-rsa-key-pairs)
21 | - [2. Create Licenses for Users](#2-create-licenses-for-users)
22 | - [3. Protect Your NuGet Package](#3-protect-your-nuget-package)
23 | - [For End Users](#for-end-users)
24 | - [NuSeal Default Behavior](#nuseal-default-behavior)
25 | - [NuSeal Customization Options](#nuseal-customization-options)
26 | - [1. Validation Mode](#1-validation-mode)
27 | - [2. Validation Scope](#2-validation-scope)
28 | - [3. Validation Condition](#3-validation-condition)
29 | - [4. Output Path](#4-output-path)
30 | - [5. Merging custom assets](#5-merging-custom-assets)
31 | - [6. Disable packing assets](#6-disable-packing-assets)
32 |
33 | ## TL;DR
34 |
35 | - Authors create RSA key pairs in PEM format. They may create them using `NuSeal.Generator` package.
36 | - Authors create licenses for their users using `NuSeal.Generator` package. License files are named `YourProductName.lic`.
37 | - Authors install the `NuSeal` package in their NuGet package to protect it.
38 | - Authors add `NuSealPem` item providing the path to the public key PEM and the name of the product.
39 | - End users obtain a license file and place it anywhere in their project directory tree.
40 |
41 | ## For Package Authors
42 |
43 | ### 1. Create RSA Key Pairs
44 |
45 | Package authors first need to create public/private key pairs. You can use the `NuSeal.Generator` package for this.
46 |
47 | ```xml
48 |
49 |
50 |
51 | ```
52 |
53 | Then generate the keys.
54 |
55 | ```csharp
56 | var keys = NuSeal.RsaKeyGenerator.GeneratePem();
57 | File.WriteAllText("private_key.pem", keys.PrivateKey);
58 | File.WriteAllText("public_key.pem", keys.PublicKey);
59 | ```
60 |
61 | Keep the private key secure and confidential, as it will be used to sign licenses.
62 |
63 | ### 2. Create Licenses for Users
64 |
65 | Once you have the key pair, you can create licenses for your product:
66 |
67 | ```csharp
68 | var license = NuSeal.License.Create(new()
69 | {
70 | PrivateKeyPem = keys.PrivateKey,
71 | ProductName = "YourProductName",
72 | SubscriptionId = "00000000-0000-0000-0000-000000000000",
73 | ClientId = "00000000-0000-0000-0000-000000000000",
74 | Edition = "Free",
75 | Issuer = "YourCompany",
76 | Audience = "NuSeal",
77 | StartDate = DateTimeOffset.UtcNow,
78 | ExpirationDate = DateTimeOffset.UtcNow.AddYears(1),
79 | GracePeriodInDays = 30,
80 | AdditionalClaims = []
81 | });
82 |
83 | // Save the license to a file
84 | File.WriteAllText("YourProductName.lic", license);
85 | ```
86 |
87 | Parameters explained:
88 | - privateKeyPem - Your private RSA key in PEM format
89 | - productName - Unique identifier of your product associated with this license. It might be the package name if this license is intended only for this package; or it might be a bundle name if the license is associated with group of packages. Important: this name is used while defining the `NuSealPem` item and as a license filename.
90 | - subscriptionId - Unique identifier for the customer subscription
91 | - clientId - Unique identifier for the customer or user
92 | - edition - Edition of your product (e.g., "Free", "Professional", "Enterprise")
93 | - issuer - Your company or organization name
94 | - audience - Intended audience for the license (e.g., "NuSeal")
95 | - startDate - When the license becomes valid
96 | - expirationDate - When the license expires
97 | - gracePeriodInDays - Number of days after expiration during which the license is still considered valid. It will emit a warning instead of an error during validation.
98 | - additionalClaims - Any additional claims you want to include in the license
99 |
100 | ### 3. Protect Your NuGet Package
101 |
102 | To protect your NuGet package, add the `NuSeal` package as a dependency:
103 |
104 | ```xml
105 |
106 |
107 |
108 | ```
109 |
110 | Then, add a `NuSealPem` item providing the path to the public key PEM and the name of the product:
111 |
112 | ```xml
113 |
114 |
115 |
116 | ```
117 |
118 | It's a common practice that authors provide licenses for a single package or a bundle of packages. In this case, you may add multiple items. You may use the same or a different PEM file per product.
119 |
120 | ```xml
121 |
122 |
123 |
124 |
125 | ```
126 |
127 | NuSeal will try to find and validate the license against all specified products. At least one valid license is required to pass the validation.
128 |
129 | ## For End Users
130 |
131 | End users of your protected NuGet package need to:
132 |
133 | 1. Obtain a license file from you (the package author)
134 | 2. Place the license file in one of these locations:
135 | - In the solution/repository root directory.
136 | - Anywhere in the directory tree.
137 |
138 | The license file should be named `YourProductName.lic`. Important: Avoid checking the license file into source control to prevent leaks.
139 |
140 | ## NuSeal Default Behavior
141 |
142 | The default behavior of NuSeal is as follows.
143 |
144 | - The `YourPackageId.props` and `YourPackageId.targets` assets are generated in the build output path.
145 | - The generated assets are packed into the NuGet package under the `build` folder.
146 | - License validation is executed for direct consumers of the protected package.
147 | - If no license is found, the build fails with an error.
148 | - The license is validated against the following criteria:
149 | - The license has valid lifetime
150 | - The license is signed with the private key corresponding to the specified public key in `NuSealPem`
151 | - The `product` claim in the license matches the product name specified in `NuSealPem`
152 |
153 | ## NuSeal Customization Options
154 |
155 | The authors can customize the default behavior and adjust the policies to fit their needs.
156 |
157 | ### 1. Validation Mode
158 | It alters the behavior when no valid license is found.
159 | - `Error` (default): The build fails with an error if no valid license is found.
160 | - `Warning`: The build emits a warning if no valid license is found, but continues.
161 |
162 | ```xml
163 |
164 | Warning
165 |
166 | ```
167 |
168 | ### 2. Validation Scope
169 | Depending on the nature of the library and the business model, authors may want a different strategy where even transitive consumers are required to have a license.
170 | - `Direct` (default): The assets are packed only to `build` directory. Only projects that directly consume the protected package will be validated for licenses.
171 | - `Transitive`: The assets are packed to `buildTransitive` and `build` directories. The `build` is necessary to support projects using `packages.config`. The assets will flow to all consumers, direct and transitive. For this scope, to avoid cluttering the build for large solutions, we're constraining the validation to only executable assemblies.
172 |
173 | ```xml
174 |
175 | Transitive
176 |
177 | ```
178 |
179 | ### 3. Validation Condition
180 | The generated target, depending on the validation scope, may or may not include a condition.
181 | - `Direct` (default): No condition is applied. All projects that directly consume the protected package will be validated for licenses.
182 | - `Transitive`: The target includes a condition to only validate executable assemblies.
183 | ```xml
184 | Condition="'$(OutputType)' == 'Exe' Or '$(OutputType)' == 'WinExe'"
185 | ```
186 | The authors may alter this behavior and specify their custom condition as follows. If defined, it will be applied regardless of the scope.
187 |
188 | ```xml
189 |
190 | "'#(OutputType)' == 'Exe' Or '#(OutputType)' == 'WinExe'"
191 |
192 | ```
193 |
194 | Note that `#()` is used instead of `$()`. Since we need to preserve the original condition as literal, and avoid variable evaluation; the `$` and `@` characters should not be used. Use the `#` character instead, and we'll do the replacement during asset generation.
195 | - Use `##` for `@`
196 | - Use `#` for `$`
197 |
198 | ### 4. Output Path
199 |
200 | By default, the assets `YourPackageId.props` and `YourPackageId.targets` are generated in the build output path; the value of `$(OutputPath)`.
201 | If the authors want to generate the assets in a different location, they can specify an output path as follows.
202 |
203 | ```xml
204 |
205 | YOUR_DESIRED_OUTPUT_PATH
206 |
207 | ```
208 |
209 | ### 5. Merging custom assets
210 |
211 | By default, once the `YourPackageId.props` and `YourPackageId.targets` assets are generated, we add items to pack them in the NuGet package. We're packing them in `build` directory, and in case of `Transitive` scope in `buildTransitive` directory as well.
212 |
213 | ```xml
214 |
215 |
216 | ```
217 |
218 | If the author is already packing assets to their NuGet package, they can provide them to NuSeal. We'll do the merging and pack the merged assets.
219 |
220 | ```xml
221 |
222 | build\YourPackageId.props
223 | build\YourPackageId.targets
224 |
225 | ```
226 |
227 | ### 6. Disable packing assets
228 |
229 | Authors may have a different strategy for generating NuGet packages (e.g. they use nuspec files), have complex workflows or simply want to manually pack the assets. In that case they may disable packing assets altogether. We'll just generate the assets in the output path, and it's up to the authors to pack or further process them.
230 |
231 | ```xml
232 |
233 | disable
234 |
235 | ```
236 |
237 | ## Give a Star! :star:
238 | If you like or are using this project please give it a star. Thanks!
239 |
--------------------------------------------------------------------------------
/ci.slnf:
--------------------------------------------------------------------------------
1 | {
2 | "solution": {
3 | "path": "NuSeal.sln",
4 | "projects": [
5 | "src\\NuSeal\\NuSeal.csproj",
6 | "src\\NuSeal.Generator\\NuSeal.Generator.csproj",
7 | "tests\\NuSeal.Tests\\NuSeal.Tests.csproj"
8 | ]
9 | }
10 | }
--------------------------------------------------------------------------------
/clean.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # Fati Iseni
3 |
4 | WorkingDir="$(pwd)"
5 |
6 | ########## Make sure you're not on a root path :)
7 | safetyCheck()
8 | {
9 | declare -a arr=("" "/" "/c" "/d" "c:\\" "d:\\" "C:\\" "D:\\")
10 | for i in "${arr[@]}"
11 | do
12 | if [ "$WorkingDir" = "$i" ]; then
13 | echo "";
14 | echo "You are on a root path. Please run the script from a given directory.";
15 | exit 1;
16 | fi
17 | done
18 | }
19 |
20 | deleteBinObj()
21 | {
22 | echo "Deleting bin and obj directories...";
23 | find "$WorkingDir/" -type d -name "bin" -exec rm -rf {} \; > /dev/null 2>&1;
24 | find "$WorkingDir/" -type d -name "obj" -exec rm -rf {} \; > /dev/null 2>&1;
25 | }
26 |
27 | deleteVSDir()
28 | {
29 | echo "Deleting .vs directories...";
30 | find "$WorkingDir/" -type d -name ".vs" -exec rm -rf {} \; > /dev/null 2>&1;
31 | }
32 |
33 | deleteLogs()
34 | {
35 | echo "Deleting Logs directories...";
36 | find "$WorkingDir/" -type d -name "Logs" -exec rm -rf {} \; > /dev/null 2>&1;
37 | }
38 |
39 | deleteUserCsprojFiles()
40 | {
41 | echo "Deleting *.csproj.user files...";
42 | find "$WorkingDir/" -type f -name "*.csproj.user" -exec rm -rf {} \; > /dev/null 2>&1;
43 | }
44 |
45 | deleteTestResults()
46 | {
47 | echo "Deleting test and coverage artifacts...";
48 | find "$WorkingDir/" -type d -name "TestResults" -exec rm -rf {} \; > /dev/null 2>&1;
49 | }
50 |
51 | deleteLocalGitBranches()
52 | {
53 | echo "Deleting local unused git branches (e.g. no corresponding remote branch)...";
54 | git fetch -p && git branch -vv | awk '/: gone\]/{print $1}' | xargs -I {} git branch -D {}
55 | }
56 |
57 | showhelp()
58 | {
59 | echo "Usage:";
60 | echo "";
61 | echo -e "clean.sh [obj | vs | logs | user | coverages | branches | all]";
62 | echo "";
63 | echo -e "obj (Default)\t-\tDeletes bin and obj directories.";
64 | echo -e "vs\t\t-\tDeletes .vs directories.";
65 | echo -e "logs\t\t-\tDeletes Logs directories.";
66 | echo -e "user\t\t-\tDeletes *.csproj.user files.";
67 | echo -e "coverages\t-\tDeletes test and coverage artifacts.";
68 | echo -e "branches\t-\tDeletes local unused git branches (e.g. no corresponding remote branch).";
69 | echo -e "all\t\t-\tApply all options";
70 | }
71 |
72 | safetyCheck;
73 | echo "";
74 |
75 | if [ "$1" = "help" ]; then
76 | showhelp;
77 | elif [ "$1" = "obj" ]; then
78 | deleteBinObj;
79 | elif [ "$1" = "vs" ]; then
80 | deleteVSDir;
81 | elif [ "$1" = "logs" ]; then
82 | deleteLogs;
83 | elif [ "$1" = "user" ]; then
84 | deleteUserCsprojFiles;
85 | elif [ "$1" = "coverages" ]; then
86 | deleteTestResults;
87 | elif [ "$1" = "branches" ]; then
88 | deleteLocalGitBranches;
89 | elif [ "$1" = "all" ]; then
90 | deleteBinObj;
91 | deleteVSDir;
92 | deleteLogs;
93 | deleteUserCsprojFiles;
94 | deleteTestResults;
95 | deleteLocalGitBranches;
96 | else
97 | deleteBinObj;
98 | fi
99 |
--------------------------------------------------------------------------------
/coverage.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | dotnet tool list -g dotnet-reportgenerator-globaltool > /dev/null 2>&1
4 | exists=$(echo $?)
5 | if [ $exists -ne 0 ]; then
6 | echo "Installing ReportGenerator..."
7 | dotnet tool install -g dotnet-reportgenerator-globaltool
8 | echo "ReportGenerator installed"
9 | fi
10 |
11 | find . -type d -name TestResults -exec rm -rf {} \; > /dev/null 2>&1
12 |
13 | testtarget="$1"
14 |
15 | if [ "$testtarget" = "" ]; then
16 | testtarget="ci.slnf"
17 | fi
18 |
19 | dotnet build $testtarget --configuration Release
20 | dotnet test $testtarget --configuration Release --no-build --no-restore --collect:"XPlat Code Coverage;Format=opencover"
21 |
22 | reportgenerator \
23 | -reports:tests/**/coverage.opencover.xml \
24 | -targetdir:TestResults \
25 | -reporttypes:"Html;Badges;MarkdownSummaryGithub" \
26 | -assemblyfilters:-*Tests*
27 |
--------------------------------------------------------------------------------
/exclusion.dic:
--------------------------------------------------------------------------------
1 | Pozitron
2 | pozitron
3 | microsoft
4 | Defs
5 | nuseal
6 | NuSeal
7 | netstandard
8 | pems
9 | Nuget
10 | msbuild
11 | Dlls
12 | Pems
13 | Dlls
14 |
--------------------------------------------------------------------------------
/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fiseni/NuSeal/00858c1475e20e38df993ee45571c67313717ec6/icon.png
--------------------------------------------------------------------------------
/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fiseni/NuSeal/00858c1475e20e38df993ee45571c67313717ec6/logo.png
--------------------------------------------------------------------------------
/readme-nuget.md:
--------------------------------------------------------------------------------
1 | NuSeal provides the infrastructure for creating and validating NuGet package licenses. The validation occurs during build time (offline), preventing unauthorized use of your packages. It's designed to be generic while allowing each author to set their own public key and license policies.
2 |
3 | Packages:
4 |
5 | 1. [NuSeal](https://www.nuget.org/packages/NuSeal) - Core package that validates licenses during build time (`netstandard2.0` library)
6 | 2. [NuSeal.Generator](https://www.nuget.org/packages/NuSeal.Generator) - Helper package for generating RSA key pairs and licenses (`net8.0` library)
7 |
8 | ## Table of Contents
9 | - [TL;DR](#tldr)
10 | - [For Package Authors](#for-package-authors)
11 | - [1. Create RSA Key Pairs](#1-create-rsa-key-pairs)
12 | - [2. Create Licenses for Users](#2-create-licenses-for-users)
13 | - [3. Protect Your NuGet Package](#3-protect-your-nuget-package)
14 | - [For End Users](#for-end-users)
15 | - [NuSeal Default Behavior](#nuseal-default-behavior)
16 | - [NuSeal Customization Options](#nuseal-customization-options)
17 | - [1. Validation Mode](#1-validation-mode)
18 | - [2. Validation Scope](#2-validation-scope)
19 | - [3. Validation Condition](#3-validation-condition)
20 | - [4. Output Path](#4-output-path)
21 | - [5. Merging custom assets](#5-merging-custom-assets)
22 | - [6. Disable packing assets](#6-disable-packing-assets)
23 |
24 | ## TL;DR
25 |
26 | - Authors create RSA key pairs in PEM format. They may create them using `NuSeal.Generator` package.
27 | - Authors create licenses for their users using `NuSeal.Generator` package. License files are named `YourProductName.lic`.
28 | - Authors install the `NuSeal` package in their NuGet package to protect it.
29 | - Authors add `NuSealPem` item providing the path to the public key PEM and the name of the product.
30 | - End users obtain a license file and place it anywhere in their project directory tree.
31 |
32 | ## For Package Authors
33 |
34 | ### 1. Create RSA Key Pairs
35 |
36 | Package authors first need to create public/private key pairs. You can use the `NuSeal.Generator` package for this.
37 |
38 | ```xml
39 |
40 |
41 |
42 | ```
43 |
44 | Then generate the keys.
45 |
46 | ```csharp
47 | var keys = NuSeal.RsaKeyGenerator.GeneratePem();
48 | File.WriteAllText("private_key.pem", keys.PrivateKey);
49 | File.WriteAllText("public_key.pem", keys.PublicKey);
50 | ```
51 |
52 | Keep the private key secure and confidential, as it will be used to sign licenses.
53 |
54 | ### 2. Create Licenses for Users
55 |
56 | Once you have the key pair, you can create licenses for your product:
57 |
58 | ```csharp
59 | var license = NuSeal.License.Create(new()
60 | {
61 | PrivateKeyPem = keys.PrivateKey,
62 | ProductName = "YourProductName",
63 | SubscriptionId = "00000000-0000-0000-0000-000000000000",
64 | ClientId = "00000000-0000-0000-0000-000000000000",
65 | Edition = "Free",
66 | Issuer = "YourCompany",
67 | Audience = "NuSeal",
68 | StartDate = DateTimeOffset.UtcNow,
69 | ExpirationDate = DateTimeOffset.UtcNow.AddYears(1),
70 | GracePeriodInDays = 30,
71 | AdditionalClaims = []
72 | });
73 |
74 | // Save the license to a file
75 | File.WriteAllText("YourProductName.lic", license);
76 | ```
77 |
78 | Parameters explained:
79 | - privateKeyPem - Your private RSA key in PEM format
80 | - productName - Unique identifier of your product associated with this license. It might be the package name if this license is intended only for this package; or it might be a bundle name if the license is associated with group of packages. Important: this name is used while defining the `NuSealPem` item and as a license filename.
81 | - subscriptionId - Unique identifier for the customer subscription
82 | - clientId - Unique identifier for the customer or user
83 | - edition - Edition of your product (e.g., "Free", "Professional", "Enterprise")
84 | - issuer - Your company or organization name
85 | - audience - Intended audience for the license (e.g., "NuSeal")
86 | - startDate - When the license becomes valid
87 | - expirationDate - When the license expires
88 | - gracePeriodInDays - Number of days after expiration during which the license is still considered valid. It will emit a warning instead of an error during validation.
89 | - additionalClaims - Any additional claims you want to include in the license
90 |
91 | ### 3. Protect Your NuGet Package
92 |
93 | To protect your NuGet package, add the `NuSeal` package as a dependency:
94 |
95 | ```xml
96 |
97 |
98 |
99 | ```
100 |
101 | Then, add a `NuSealPem` item providing the path to the public key PEM and the name of the product:
102 |
103 | ```xml
104 |
105 |
106 |
107 | ```
108 |
109 | It's a common practice that authors provide licenses for a single package or a bundle of packages. In this case, you may add multiple items. You may use the same or a different PEM file per product.
110 |
111 | ```xml
112 |
113 |
114 |
115 |
116 | ```
117 |
118 | NuSeal will try to find and validate the license against all specified products. At least one valid license is required to pass the validation.
119 |
120 | ## For End Users
121 |
122 | End users of your protected NuGet package need to:
123 |
124 | 1. Obtain a license file from you (the package author)
125 | 2. Place the license file in one of these locations:
126 | - In the solution/repository root directory.
127 | - Anywhere in the directory tree.
128 |
129 | The license file should be named `YourProductName.lic`. Important: Avoid checking the license file into source control to prevent leaks.
130 |
131 | ## NuSeal Default Behavior
132 |
133 | The default behavior of NuSeal is as follows.
134 |
135 | - The `YourPackageId.props` and `YourPackageId.targets` assets are generated in the build output path.
136 | - The generated assets are packed into the NuGet package under the `build` folder.
137 | - License validation is executed for direct consumers of the protected package.
138 | - If no license is found, the build fails with an error.
139 | - The license is validated against the following criteria:
140 | - The license has valid lifetime
141 | - The license is signed with the private key corresponding to the specified public key in `NuSealPem`
142 | - The `product` claim in the license matches the product name specified in `NuSealPem`
143 |
144 | ## NuSeal Customization Options
145 |
146 | The authors can customize the default behavior and adjust the policies to fit their needs.
147 |
148 | ### 1. Validation Mode
149 | It alters the behavior when no valid license is found.
150 | - `Error` (default): The build fails with an error if no valid license is found.
151 | - `Warning`: The build emits a warning if no valid license is found, but continues.
152 |
153 | ```xml
154 |
155 | Warning
156 |
157 | ```
158 |
159 | ### 2. Validation Scope
160 | Depending on the nature of the library and the business model, authors may want a different strategy where even transitive consumers are required to have a license.
161 | - `Direct` (default): The assets are packed only to `build` directory. Only projects that directly consume the protected package will be validated for licenses.
162 | - `Transitive`: The assets are packed to `buildTransitive` and `build` directories. The `build` is necessary to support projects using `packages.config`. The assets will flow to all consumers, direct and transitive. For this scope, to avoid cluttering the build for large solutions, we're constraining the validation to only executable assemblies.
163 |
164 | ```xml
165 |
166 | Transitive
167 |
168 | ```
169 |
170 | ### 3. Validation Condition
171 | The generated target, depending on the validation scope, may or may not include a condition.
172 | - `Direct` (default): No condition is applied. All projects that directly consume the protected package will be validated for licenses.
173 | - `Transitive`: The target includes a condition to only validate executable assemblies.
174 | ```xml
175 | Condition="'$(OutputType)' == 'Exe' Or '$(OutputType)' == 'WinExe'"
176 | ```
177 | The authors may alter this behavior and specify their custom condition as follows. If defined, it will be applied regardless of the scope.
178 |
179 | ```xml
180 |
181 | "'#(OutputType)' == 'Exe' Or '#(OutputType)' == 'WinExe'"
182 |
183 | ```
184 |
185 | Note that `#()` is used instead of `$()`. Since we need to preserve the original condition as literal, and avoid variable evaluation; the `$` and `@` characters should not be used. Use the `#` character instead, and we'll do the replacement during asset generation.
186 | - Use `##` for `@`
187 | - Use `#` for `$`
188 |
189 | ### 4. Output Path
190 |
191 | By default, the assets `YourPackageId.props` and `YourPackageId.targets` are generated in the build output path; the value of `$(OutputPath)`.
192 | If the authors want to generate the assets in a different location, they can specify an output path as follows.
193 |
194 | ```xml
195 |
196 | YOUR_DESIRED_OUTPUT_PATH
197 |
198 | ```
199 |
200 | ### 5. Merging custom assets
201 |
202 | By default, once the `YourPackageId.props` and `YourPackageId.targets` assets are generated, we add items to pack them in the NuGet package. We're packing them in `build` directory, and in case of `Transitive` scope in `buildTransitive` directory as well.
203 |
204 | ```xml
205 |
206 |
207 | ```
208 |
209 | If the author is already packing assets to their NuGet package, they can provide them to NuSeal. We'll do the merging and pack the merged assets.
210 |
211 | ```xml
212 |
213 | build\YourPackageId.props
214 | build\YourPackageId.targets
215 |
216 | ```
217 |
218 | ### 6. Disable packing assets
219 |
220 | Authors may have a different strategy for generating NuGet packages (e.g. they use nuspec files), have complex workflows or simply want to manually pack the assets. In that case they may disable packing assets altogether. We'll just generate the assets in the output path, and it's up to the authors to pack or further process them.
221 |
222 | ```xml
223 |
224 | disable
225 |
226 | ```
227 |
228 | ## Give a Star! :star:
229 | If you like or are using this project please give it a star. Thanks!
230 |
--------------------------------------------------------------------------------
/samples/AuthorPackage/AuthorPackage.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0
5 | AuthorPackage
6 | 0.4.0
7 | true
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/samples/AuthorPackage/Foo.cs:
--------------------------------------------------------------------------------
1 | namespace AuthorPackage
2 | {
3 | public class Foo
4 | {
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/samples/AuthorPackage/public_key.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN PUBLIC KEY-----
2 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAygsSRxp1MInUqDz2nPk+
3 | +BPP8ojPdydEg8inQbx7SonV+HBuUfRnbhp/0w298bQP0X1fz+RwnjUDdakV9vsa
4 | zrK3RH/Ulq0tLrQXKBRZVP2rot4SWWYcdncnvYIiXSpAK2kisxYX1BL56wAEigKX
5 | CoCmQl8YleATGf2EEZ80tOmL6eEtJZ3rFxcaIbdx6z10XwIkvMM4CgbEPIpGZqva
6 | lceYsQ/KioeoxbyjBiNOu3DnkjpzhgbDg/dMKMVvZ1DiJBWvaKkToVDpfGFFpwUs
7 | OEvTfMysHGQ/YqQU+AoGjQJr3/n4X9+THSsXF+Ga7mxMc9x9SwOMebM9q6LDUoG7
8 | cQIDAQAB
9 | -----END PUBLIC KEY-----
--------------------------------------------------------------------------------
/samples/Directory.Packages.props:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/samples/SampleNuGetFeed/AuthorPackage.0.4.0.nupkg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fiseni/NuSeal/00858c1475e20e38df993ee45571c67313717ec6/samples/SampleNuGetFeed/AuthorPackage.0.4.0.nupkg
--------------------------------------------------------------------------------
/samples/UserApp/Program.cs:
--------------------------------------------------------------------------------
1 |
2 | _ = new AuthorPackage.Foo();
3 |
4 | Console.WriteLine("Hello, World!");
5 |
--------------------------------------------------------------------------------
/samples/UserApp/UserApp.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net9.0
6 | enable
7 | enable
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | $(RestoreSources);$(MSBuildThisFileDirectory)..\SampleNuGetFeed
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/samples/UserApp/YourProductName.lic:
--------------------------------------------------------------------------------
1 | eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJOdVNlYWwiLCJpc3MiOiJZb3VyQ29tcGFueSIsImV4cCI6MTc4ODQ1MjkyNywibmJmIjoxNzU2OTE2OTI3LCJzdWIiOiIwMDAwMDAwMC0wMDAwLTAwMDAtMDAwMC0wMDAwMDAwMDAwMDAiLCJwcm9kdWN0IjoiWW91clByb2R1Y3ROYW1lIiwiZWRpdGlvbiI6IkZyZWUiLCJpYXQiOjE3NTY5MTY5Mzd9.nzTHfuh-Ae1ktqUvU8hc31_iXs_dbMdPoxJrx6jBFqAVC4VCBISelaOgvdhTliN_k9HBfKNrXvXoZMqeGvm4DQnjAYmhc3vvpa5oSSDPS_xq2s-oK8_CCK05Vbk3gCr7RkoGl2y2PQjf8EsXeAkNFi0xj4hdtcsIuqoV8O4j8Q7qCQahkPA1SFJC-eLJmSQbGuQVSGGl6Rq3zHx4T6wjga7LazudGMWwTMO_I4ThcxVEVgcSzfu9epH--d1pw20ms5B9vl1SIxC7ImjDgYc0eahATVjhwZM-vttSe9WSTr9eRdps_EMXjBkyotSG8UqPqv1bvlokUGj-mresp1VeYg
--------------------------------------------------------------------------------
/src/NuSeal.Generator/License.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.IdentityModel.JsonWebTokens;
2 | using Microsoft.IdentityModel.Tokens;
3 | using System;
4 | using System.Collections.Concurrent;
5 | using System.Collections.Generic;
6 | using System.Security.Claims;
7 | using System.Security.Cryptography;
8 |
9 | namespace NuSeal;
10 |
11 | public class License
12 | {
13 | private static readonly ConcurrentDictionary _credentialCache = new();
14 |
15 | public static string Create(LicenseParameters parameters)
16 | {
17 | ArgumentNullException.ThrowIfNull(parameters);
18 | ArgumentException.ThrowIfNullOrWhiteSpace(parameters.PrivateKeyPem);
19 | ArgumentException.ThrowIfNullOrWhiteSpace(parameters.ProductName);
20 |
21 | var credentials = _credentialCache.GetOrAdd(parameters.PrivateKeyPem, static key =>
22 | {
23 | var rsa = RSA.Create();
24 | rsa.ImportFromPem(key.AsSpan());
25 | var rsaSecurityKey = new RsaSecurityKey(rsa);
26 | var credentials = new SigningCredentials(
27 | rsaSecurityKey,
28 | SecurityAlgorithms.RsaSha256);
29 | return credentials;
30 | });
31 |
32 | var claims = new List()
33 | {
34 | new(JwtRegisteredClaimNames.Sub, parameters.SubscriptionId),
35 | new("product", parameters.ProductName),
36 | };
37 |
38 | if (parameters.AdditionalClaims.Length > 0)
39 | {
40 | claims.AddRange(parameters.AdditionalClaims);
41 | }
42 |
43 | if (!string.IsNullOrWhiteSpace(parameters.Edition))
44 | {
45 | claims.Add(new Claim("edition", parameters.Edition));
46 | }
47 |
48 | if (!string.IsNullOrWhiteSpace(parameters.ClientId))
49 | {
50 | claims.Add(new Claim("client", parameters.ClientId));
51 | }
52 |
53 | if (parameters.GracePeriodInDays.HasValue)
54 | {
55 | claims.Add(new Claim("grace_period_days", parameters.GracePeriodInDays.Value.ToString(), ClaimValueTypes.Integer32));
56 | }
57 |
58 | var tokenDescriptor = new SecurityTokenDescriptor
59 | {
60 | Subject = new ClaimsIdentity(claims),
61 | NotBefore = parameters.StartDate.UtcDateTime,
62 | Expires = parameters.ExpirationDate.UtcDateTime,
63 | Issuer = parameters.Issuer,
64 | Audience = parameters.Audience,
65 | SigningCredentials = credentials
66 | };
67 |
68 | var handler = new JsonWebTokenHandler();
69 | var token = handler.CreateToken(tokenDescriptor);
70 |
71 | return token;
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/NuSeal.Generator/LicenseParameters.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Security.Claims;
3 |
4 | namespace NuSeal;
5 |
6 | public record LicenseParameters
7 | {
8 | public required string PrivateKeyPem { get; init; }
9 | public required string ProductName { get; init; }
10 | public string Issuer { get; init; } = "NuSeal";
11 | public string Audience { get; init; } = "NuSeal";
12 | public string SubscriptionId { get; init; } = Guid.Empty.ToString();
13 | public string? ClientId { get; init; }
14 | public string? Edition { get; init; }
15 | public DateTimeOffset StartDate { get; init; } = DateTimeOffset.UtcNow;
16 | public DateTimeOffset ExpirationDate { get; init; } = DateTimeOffset.UtcNow.AddYears(1);
17 | public int? GracePeriodInDays { get; init; }
18 | public Claim[] AdditionalClaims { get; init; } = Array.Empty();
19 | }
20 |
--------------------------------------------------------------------------------
/src/NuSeal.Generator/NuSeal.Generator.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | NuSeal.Generator
8 | NuSeal
9 | net8.0
10 | disable
11 | enable
12 | latest
13 |
14 |
15 |
16 | 0.4.0
17 | NuSeal.Generator
18 | NuSeal.Generator
19 | A helper library for generating licenses for NuSeal.
20 | A helper library for generating licenses for NuSeal.
21 | fiseni pozitron nuseal nuget license
22 |
23 | Refer to Releases page for details.
24 | https://github.com/fiseni/NuSeal/releases
25 |
26 |
27 |
28 |
29 | embedded
30 | true
31 | true
32 | false
33 |
34 | true
35 | false
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/src/NuSeal.Generator/RsaKeyGenerator.cs:
--------------------------------------------------------------------------------
1 | using System.Security.Cryptography;
2 |
3 | namespace NuSeal;
4 |
5 | public class RsaKeyGenerator
6 | {
7 | public static RsaPemPair GeneratePem()
8 | {
9 | using var rsa = RSA.Create(2048);
10 | var privateKey = rsa.ExportPkcs8PrivateKeyPem();
11 | var publicKey = rsa.ExportSubjectPublicKeyInfoPem();
12 | return new RsaPemPair(publicKey, privateKey);
13 | }
14 | }
15 |
16 | public class RsaPemPair(string publicKey, string privateKey)
17 | {
18 | public string PublicKey { get; } = publicKey;
19 | public string PrivateKey { get; } = privateKey;
20 | }
21 |
--------------------------------------------------------------------------------
/src/NuSeal/Assets.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Linq;
4 |
5 | namespace NuSeal;
6 |
7 | internal class Assets
8 | {
9 | private const string TASK_NAME = nameof(ValidateLicenseTask_0_4_0);
10 |
11 | public static string GenerateProps(ConsumerParameters parameters)
12 | {
13 | var pemItems = string.Join(Environment.NewLine, parameters.Pems.Select(x =>
14 | {
15 | var item = $"""
16 |
19 | """;
20 | return item;
21 | }));
22 |
23 | var output = $"""
24 |
25 |
26 |
27 | $([MSBuild]::NormalizePath('$(NugetPackageRoot)', 'nuseal', '{parameters.NuSealVersion}', 'tasks', 'netstandard2.0', 'NuSeal_{parameters.NuSealVersionSuffix}.dll'))
28 |
29 |
30 |
31 | {pemItems}
32 |
33 |
34 |
38 |
39 |
40 | """;
41 | return output;
42 | }
43 |
44 | public static string GenerateTargets(ConsumerParameters parameters)
45 | {
46 | var output = $"""
47 |
48 |
49 |
52 |
53 |
62 |
63 |
64 |
65 |
66 | """;
67 | return output;
68 | }
69 |
70 | public static string AppendConsumerAsset(string nuSealAssetContent, string? consumerAssetFile)
71 | {
72 | if (consumerAssetFile is not null && File.Exists(consumerAssetFile))
73 | {
74 | var content = File.ReadAllText(consumerAssetFile);
75 | content = RemoveProjectTags(content, consumerAssetFile);
76 | if (!string.IsNullOrWhiteSpace(content))
77 | {
78 | var linedEnding = DetectLineEnding(content);
79 | nuSealAssetContent = nuSealAssetContent.Replace("", $"{content}{linedEnding}");
80 | }
81 | }
82 | return nuSealAssetContent;
83 | }
84 |
85 | // Very rudimentary, but it's not worth parsing the XML properly for this
86 | public static string RemoveProjectTags(string content, string fileName)
87 | {
88 | var startIndex = content.IndexOf(" tag. File {fileName}");
91 |
92 | var endIndex = content.IndexOf(">", startIndex);
93 | if (endIndex == -1)
94 | throw new ArgumentException($"The provided content has invalid xml content. File {fileName}");
95 |
96 | var closingTagIndex = content.IndexOf("", endIndex);
97 | if (closingTagIndex == -1)
98 | throw new ArgumentException($"The provided content does not contain a closing tag. File {fileName}");
99 |
100 | string projectTag = content.Substring(startIndex, endIndex - startIndex + 1);
101 | return content.Replace(projectTag, "").Replace("", "");
102 | }
103 |
104 | public static string DetectLineEnding(string content)
105 | {
106 | var index = content.IndexOf('\n');
107 | if (index > 0 && content[index - 1] == '\r')
108 | return "\r\n";
109 | return "\n";
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/src/NuSeal/ConsumerParameters.cs:
--------------------------------------------------------------------------------
1 | namespace NuSeal;
2 |
3 | internal class ConsumerParameters
4 | {
5 | public ConsumerParameters(
6 | string nuSealAssetsPath,
7 | string nuSealVersion,
8 | string outputPath,
9 | string packageId,
10 | string assemblyName,
11 | PemData[] pems,
12 | string? condition,
13 | string? propsFile,
14 | string? targetsFile,
15 | string? validationMode,
16 | string? validationScope)
17 | {
18 | NuSealAssetsPath = nuSealAssetsPath;
19 | NuSealVersion = nuSealVersion;
20 | OutputPath = outputPath;
21 | PackageId = packageId;
22 | AssemblyName = assemblyName;
23 | Pems = pems;
24 | PropsFile = string.IsNullOrWhiteSpace(propsFile) ? null : propsFile;
25 | TargetsFile = string.IsNullOrWhiteSpace(targetsFile) ? null : targetsFile;
26 |
27 | Options = new NuSealOptions(validationMode, validationScope);
28 | ValidationMode = Options.ValidationMode.ToString();
29 | ValidationScope = Options.ValidationScope.ToString();
30 | PackageSuffix = packageId.Replace(".", "");
31 | NuSealVersionSuffix = nuSealVersion.Replace(".", "_").Replace("-", "_");
32 |
33 | if (string.IsNullOrWhiteSpace(condition))
34 | {
35 | TargetCondition = Options.ValidationScope == NuSealValidationScope.Transitive
36 | ? $"Condition=\"'$(OutputType)' == 'Exe' Or '$(OutputType)' == 'WinExe'\""
37 | : "";
38 | }
39 | else
40 | {
41 | var parsedCondition = condition!.Replace("##", "$").Replace("#", "$");
42 | if (parsedCondition[0] != '"')
43 | {
44 | parsedCondition = $"\"{parsedCondition}";
45 | }
46 | if (parsedCondition[parsedCondition.Length - 1] != '"')
47 | {
48 | parsedCondition = $"{parsedCondition}\"";
49 | }
50 | TargetCondition = $"Condition={parsedCondition}";
51 | }
52 | }
53 |
54 | public string NuSealAssetsPath { get; }
55 | public string NuSealVersion { get; }
56 | public string OutputPath { get; }
57 | public string PackageId { get; }
58 | public string AssemblyName { get; }
59 | public PemData[] Pems { get; }
60 | public string TargetCondition { get; }
61 | public string? PropsFile { get; }
62 | public string? TargetsFile { get; }
63 | public string ValidationMode { get; }
64 | public string ValidationScope { get; }
65 | public string PackageSuffix { get; }
66 | public string NuSealVersionSuffix { get; }
67 | public NuSealOptions Options { get; }
68 | }
69 |
--------------------------------------------------------------------------------
/src/NuSeal/FileUtils.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 |
3 | namespace NuSeal;
4 |
5 | internal class FileUtils
6 | {
7 | internal static bool TryGetLicense(string targetAssemblyPath, string productName, out string licenseContent)
8 | {
9 | if (string.IsNullOrWhiteSpace(targetAssemblyPath) || string.IsNullOrWhiteSpace(productName))
10 | {
11 | licenseContent = string.Empty;
12 | return false;
13 | }
14 |
15 | var licenseFileName = $"{productName}.lic";
16 |
17 | try
18 | {
19 | var dir = new DirectoryInfo(Path.GetDirectoryName(targetAssemblyPath)!);
20 | while (dir is not null)
21 | {
22 | var file = Path.Combine(dir.FullName, licenseFileName);
23 | if (File.Exists(file))
24 | {
25 | licenseContent = File.ReadAllText(file).Trim();
26 | return true;
27 | }
28 | dir = dir.Parent;
29 | }
30 | }
31 | catch
32 | {
33 | }
34 |
35 | licenseContent = string.Empty;
36 | return false;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/NuSeal/LicenseValidationResult.cs:
--------------------------------------------------------------------------------
1 | namespace NuSeal;
2 |
3 | ///
4 | /// Represents the result of a license validation.
5 | ///
6 | internal enum LicenseValidationResult
7 | {
8 | ///
9 | /// The license is valid.
10 | ///
11 | Valid = 1,
12 |
13 | ///
14 | /// The license has expired but is still within the grace period.
15 | ///
16 | ExpiredWithinGracePeriod = 10,
17 |
18 | ///
19 | /// The license has expired and the grace period has also expired.
20 | ///
21 | ExpiredOutsideGracePeriod = 20,
22 |
23 | ///
24 | /// The license is invalid (wrong product, wrong format, etc.).
25 | ///
26 | Invalid = 100
27 | }
28 |
--------------------------------------------------------------------------------
/src/NuSeal/LicenseValidator.cs:
--------------------------------------------------------------------------------
1 | using Org.BouncyCastle.Crypto.Parameters;
2 | using Org.BouncyCastle.OpenSsl;
3 | using Org.BouncyCastle.Security;
4 | using System;
5 | using System.IO;
6 | using System.Text;
7 | using System.Text.Json;
8 |
9 | namespace NuSeal;
10 |
11 | // I was using Microsoft.IdentityModel.JsonWebTokens to parse tokens.
12 | // But, that package has ungodly amount of dependencies.
13 | // We have to pack all dlls in the tasks folder, so having that dependency is not acceptable.
14 | // We'll parse and validate the token manually.
15 | internal class LicenseValidator
16 | {
17 | internal static LicenseValidationResult Validate(PemData pem, string license)
18 | {
19 | if (string.IsNullOrWhiteSpace(pem.PublicKeyPem)
20 | || string.IsNullOrWhiteSpace(pem.ProductName)
21 | || string.IsNullOrWhiteSpace(license))
22 | {
23 | return LicenseValidationResult.Invalid;
24 | }
25 |
26 | try
27 | {
28 | var parts = license.Split('.');
29 | if (parts.Length != 3)
30 | return LicenseValidationResult.Invalid;
31 |
32 | if (VerifyHeader(parts) is false)
33 | return LicenseValidationResult.Invalid;
34 |
35 | if (VerifySignature(pem, parts) is false)
36 | return LicenseValidationResult.Invalid;
37 |
38 | var payloadBytes = Base64UrlDecode(parts[1]);
39 | var payload = JsonDocument.Parse(payloadBytes).RootElement;
40 |
41 | if (VerifyProductName(payload, pem.ProductName) is false)
42 | return LicenseValidationResult.Invalid;
43 |
44 | return VerifyLifetime(payload);
45 | }
46 | catch
47 | {
48 | return LicenseValidationResult.Invalid;
49 | }
50 | }
51 |
52 | private static bool VerifyHeader(string[] parts)
53 | {
54 | var headerBytes = Base64UrlDecode(parts[0]);
55 | var header = JsonDocument.Parse(headerBytes).RootElement;
56 |
57 | if (!header.TryGetProperty("alg", out var alg))
58 | return false;
59 |
60 | return string.Equals(alg.GetString(), "RS256", StringComparison.OrdinalIgnoreCase);
61 | }
62 |
63 | // Note: RSA ImportFromPem is available in .NET 5.0 and later
64 | // We'll use BouncyCastle for netstandard2.0
65 | private static bool VerifySignature(PemData pem, string[] parts)
66 | {
67 | var signatureBytes = Base64UrlDecode(parts[2]);
68 | var data = Encoding.UTF8.GetBytes($"{parts[0]}.{parts[1]}");
69 |
70 | var publicKey = GetRsaPublicKeyParameters(pem.PublicKeyPem);
71 | var verifier = SignerUtilities.GetSigner("SHA256withRSA");
72 | verifier.Init(false, publicKey);
73 | verifier.BlockUpdate(data, 0, data.Length);
74 |
75 | return verifier.VerifySignature(signatureBytes);
76 |
77 | static RsaKeyParameters GetRsaPublicKeyParameters(string pemKey)
78 | {
79 | using var reader = new StringReader(pemKey);
80 | var pemReader = new PemReader(reader);
81 | var obj = pemReader.ReadObject();
82 |
83 | if (obj is RsaKeyParameters rsaKeyParams && !rsaKeyParams.IsPrivate)
84 | {
85 | return rsaKeyParams;
86 | }
87 | throw new ArgumentException("PEM string does not contain a valid RSA public key.", nameof(pemKey));
88 | }
89 | }
90 |
91 | private static bool VerifyProductName(JsonElement payload, string productName)
92 | {
93 | if (!payload.TryGetProperty("product", out var productClaim))
94 | return false;
95 |
96 | return string.Equals(productClaim.GetString(), productName, StringComparison.OrdinalIgnoreCase);
97 | }
98 |
99 | private static LicenseValidationResult VerifyLifetime(JsonElement payload)
100 | {
101 | var clockSkewInMinutes = 5;
102 | var now = DateTimeOffset.UtcNow;
103 |
104 | if (!payload.TryGetProperty("nbf", out var nbf)
105 | || !payload.TryGetProperty("exp", out var exp))
106 | {
107 | return LicenseValidationResult.Invalid;
108 | }
109 |
110 | var nbfUtc = DateTimeOffset.FromUnixTimeSeconds(nbf.GetInt64()).UtcDateTime.AddMinutes(-1 * clockSkewInMinutes);
111 | if (now < nbfUtc)
112 | {
113 | return LicenseValidationResult.Invalid;
114 | }
115 |
116 | var expUtc = DateTimeOffset.FromUnixTimeSeconds(exp.GetInt64()).UtcDateTime.AddMinutes(clockSkewInMinutes);
117 | if (now > expUtc)
118 | {
119 | if (payload.TryGetProperty("grace_period_days", out var gracePeriodDaysElement))
120 | {
121 | var gracePeriodDays = gracePeriodDaysElement.GetInt32();
122 | if (gracePeriodDays > 0 && now <= expUtc.AddDays(gracePeriodDays))
123 | return LicenseValidationResult.ExpiredWithinGracePeriod;
124 | }
125 |
126 | return LicenseValidationResult.ExpiredOutsideGracePeriod;
127 | }
128 |
129 | return LicenseValidationResult.Valid;
130 | }
131 |
132 | private static byte[] Base64UrlDecode(string input)
133 | {
134 | string padded = input.Replace('-', '+').Replace('_', '/');
135 | switch (padded.Length % 4)
136 | {
137 | case 2: padded += "=="; break;
138 | case 3: padded += "="; break;
139 | }
140 | return Convert.FromBase64String(padded);
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/src/NuSeal/NuSeal.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | NuSeal_0_4_0
8 | NuSeal
9 | netstandard2.0
10 | disable
11 | enable
12 | latest
13 |
14 |
15 |
16 | 0.4.0
17 | NuSeal
18 | NuSeal
19 | A library that validates licenses during build time.
20 | A library that validates licenses during build time.
21 | fiseni pozitron nuseal nuget license msbuild
22 |
23 | Refer to Releases page for details.
24 | https://github.com/fiseni/NuSeal/releases
25 |
26 |
27 |
28 |
29 | embedded
30 | true
31 | true
32 | true
33 | false
34 | true
35 |
36 | true
37 | false
38 | $(TargetsForTfmSpecificBuildOutput);CopyProjectReferencesToPackage;CopyAssemblyToPackage
39 | tasks
40 |
41 |
42 | true
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
62 |
63 |
64 |
67 |
68 |
69 |
70 |
73 |
74 |
75 |
78 |
79 |
80 |
81 |
85 |
86 |
87 |
91 |
92 |
93 |
94 |
95 |
--------------------------------------------------------------------------------
/src/NuSeal/NuSealOptions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace NuSeal;
4 |
5 | internal class NuSealOptions
6 | {
7 | public NuSealValidationMode ValidationMode { get; }
8 | public NuSealValidationScope ValidationScope { get; }
9 |
10 | public NuSealOptions(
11 | string? validationMode,
12 | string? validationScope)
13 | {
14 | ValidationMode = string.Equals(validationMode, "Warning", StringComparison.OrdinalIgnoreCase)
15 | ? NuSealValidationMode.Warning
16 | : NuSealValidationMode.Error;
17 |
18 | ValidationScope = string.Equals(validationScope, "Transitive", StringComparison.OrdinalIgnoreCase)
19 | ? NuSealValidationScope.Transitive
20 | : NuSealValidationScope.Direct;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/NuSeal/NuSealValidationMode.cs:
--------------------------------------------------------------------------------
1 | namespace NuSeal;
2 |
3 | internal enum NuSealValidationMode
4 | {
5 | Error = 1,
6 | Warning = 2
7 | }
8 |
--------------------------------------------------------------------------------
/src/NuSeal/NuSealValidationScope.cs:
--------------------------------------------------------------------------------
1 | namespace NuSeal;
2 |
3 | internal enum NuSealValidationScope
4 | {
5 | Direct = 1,
6 | Transitive = 2,
7 | }
8 |
--------------------------------------------------------------------------------
/src/NuSeal/PemData.cs:
--------------------------------------------------------------------------------
1 | namespace NuSeal;
2 |
3 | internal class PemData
4 | {
5 | public PemData(string productName, string publicKeyPem)
6 | {
7 | ProductName = productName;
8 | PublicKeyPem = publicKeyPem;
9 | }
10 |
11 | public string ProductName { get; }
12 | public string PublicKeyPem { get; }
13 | }
14 |
--------------------------------------------------------------------------------
/src/NuSeal/Tasks/GenerateConsumerAssetsTask.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Build.Framework;
2 | using Microsoft.Build.Utilities;
3 | using System;
4 | using System.IO;
5 | using System.Linq;
6 |
7 | namespace NuSeal;
8 |
9 | public partial class GenerateConsumerAssetsTask : Task
10 | {
11 | [Required]
12 | public string NuSealAssetsPath { get; set; } = "";
13 |
14 | [Required]
15 | public string NuSealVersion { get; set; } = "";
16 |
17 | [Required]
18 | public string ConsumerOutputPath { get; set; } = "";
19 |
20 | [Required]
21 | public string ConsumerPackageId { get; set; } = "";
22 |
23 | [Required]
24 | public string ConsumerAssemblyName { get; set; } = "";
25 |
26 | [Required]
27 | public ITaskItem[] ConsumerPems { get; set; } = Array.Empty();
28 |
29 | public string? ConsumerCondition { get; set; }
30 |
31 | public string? ConsumerPropsFile { get; set; }
32 |
33 | public string? ConsumerTargetsFile { get; set; }
34 |
35 | public string? ValidationMode { get; set; }
36 |
37 | public string? ValidationScope { get; set; }
38 |
39 | public override bool Execute()
40 | {
41 | if (string.IsNullOrEmpty(NuSealVersion))
42 | {
43 | Log.LogError("NuSeal: The version of NuSeal package can not be determined!");
44 | return false;
45 | }
46 |
47 | if (string.IsNullOrEmpty(ConsumerOutputPath))
48 | {
49 | Log.LogError("NuSeal: The value of $(OutputPath) is empty!");
50 | return false;
51 | }
52 |
53 | if (string.IsNullOrEmpty(ConsumerPackageId))
54 | {
55 | Log.LogError("NuSeal: The PackageId property must be defined!");
56 | return false;
57 | }
58 |
59 | if (string.IsNullOrEmpty(ConsumerAssemblyName))
60 | {
61 | Log.LogError("NuSeal: The value of $(AssemblyName) is empty!");
62 | return false;
63 | }
64 |
65 | if (ConsumerPems.Length == 0)
66 | {
67 | Log.LogError("NuSeal: No public PEM files are defined.");
68 | return false;
69 | }
70 |
71 | var pems = ConsumerPems.Select(x =>
72 | {
73 | var pemFileName = x.ItemSpec;
74 | var publicKeyPem = File.ReadAllText(pemFileName);
75 | var productName = x.GetMetadata("ProductName");
76 | return new PemData(productName, publicKeyPem);
77 | }).ToArray();
78 |
79 | if (pems.Any(x => string.IsNullOrEmpty(x.ProductName)))
80 | {
81 | Log.LogError("NuSeal: The `NuSealPem` items must contain `ProductName` metadata.");
82 | return false;
83 | }
84 |
85 | try
86 | {
87 | var parameters = new ConsumerParameters(
88 | nuSealAssetsPath: NuSealAssetsPath,
89 | nuSealVersion: NuSealVersion,
90 | outputPath: ConsumerOutputPath,
91 | packageId: ConsumerPackageId,
92 | assemblyName: ConsumerAssemblyName,
93 | pems: pems,
94 | condition: ConsumerCondition,
95 | propsFile: ConsumerPropsFile,
96 | targetsFile: ConsumerTargetsFile,
97 | validationMode: ValidationMode,
98 | validationScope: ValidationScope);
99 |
100 | var props = Assets.GenerateProps(parameters);
101 | var targets = Assets.GenerateTargets(parameters);
102 |
103 | props = Assets.AppendConsumerAsset(props, parameters.PropsFile);
104 | targets = Assets.AppendConsumerAsset(targets, parameters.TargetsFile);
105 |
106 | var propsOutputFile = Path.Combine(parameters.OutputPath, $"{parameters.PackageId}.props");
107 | var targetsOutputFile = Path.Combine(parameters.OutputPath, $"{parameters.PackageId}.targets");
108 |
109 | File.WriteAllText(propsOutputFile, props);
110 | File.WriteAllText(targetsOutputFile, targets);
111 | return true;
112 | }
113 | catch (Exception ex)
114 | {
115 | Log.LogErrorFromException(ex, true);
116 | return false;
117 | }
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/src/NuSeal/Tasks/ValidateLicenseTask_0_4_0.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Build.Framework;
2 | using Microsoft.Build.Utilities;
3 | using System;
4 | using System.Linq;
5 |
6 | namespace NuSeal;
7 |
8 | public partial class ValidateLicenseTask_0_4_0 : Task
9 | {
10 | public string TargetAssemblyPath { get; set; } = "";
11 | public string NuSealVersion { get; set; } = "";
12 | public string ProtectedPackageId { get; set; } = "";
13 | public string ProtectedAssemblyName { get; set; } = "";
14 | public ITaskItem[] Pems { get; set; } = Array.Empty();
15 | public string ValidationMode { get; set; } = "";
16 | public string ValidationScope { get; set; } = "";
17 |
18 | public override bool Execute()
19 | {
20 | if (string.IsNullOrWhiteSpace(TargetAssemblyPath)
21 | || string.IsNullOrWhiteSpace(NuSealVersion)
22 | || string.IsNullOrWhiteSpace(ProtectedPackageId)
23 | || string.IsNullOrWhiteSpace(ProtectedAssemblyName)
24 | || Pems is null
25 | || Pems.Length == 0
26 | || string.IsNullOrWhiteSpace(ValidationMode)
27 | || string.IsNullOrWhiteSpace(ValidationScope))
28 | {
29 | // This should never happen as we always pass all arguments while preparing assets for authors.
30 | // If that's the case, something went really wrong, and we won't break end users' builds.
31 | Log.LogMessage(MessageImportance.High, "NuSeal: Invalid arguments for {0}", nameof(ValidateLicenseTask_0_4_0));
32 | return true;
33 | }
34 |
35 | var options = new NuSealOptions(ValidationMode, ValidationScope);
36 |
37 | try
38 | {
39 | var pems = Pems.Select(x =>
40 | {
41 | var publicKeyPem = x.ItemSpec.Trim();
42 | var productName = x.GetMetadata("ProductName");
43 | return new PemData(productName, publicKeyPem);
44 | });
45 |
46 | var bestValidationResult = LicenseValidationResult.Invalid;
47 |
48 | foreach (var pem in pems)
49 | {
50 | if (FileUtils.TryGetLicense(TargetAssemblyPath, pem.ProductName, out var license))
51 | {
52 | var validationResult = LicenseValidator.Validate(pem, license);
53 | if (validationResult == LicenseValidationResult.Valid)
54 | {
55 | return true;
56 | }
57 |
58 | if (validationResult < bestValidationResult)
59 | {
60 | bestValidationResult = validationResult;
61 | }
62 | }
63 | }
64 |
65 | var errorMessage = bestValidationResult switch
66 | {
67 | LicenseValidationResult.ExpiredWithinGracePeriod
68 | => "NuSeal: License for {0} has expired but is within the grace period. Please renew your license soon.",
69 | LicenseValidationResult.ExpiredOutsideGracePeriod
70 | => "NuSeal: License for {0} has expired. Please renew your license.",
71 | _ => "NuSeal: No valid license found for NuGet Package: {0}."
72 | };
73 |
74 | if (bestValidationResult == LicenseValidationResult.ExpiredWithinGracePeriod)
75 | {
76 | Log.LogWarning(errorMessage, ProtectedPackageId);
77 | return true;
78 | }
79 |
80 | if (options.ValidationMode == NuSealValidationMode.Warning)
81 | {
82 | Log.LogWarning(errorMessage, ProtectedPackageId);
83 | return true;
84 | }
85 | else
86 | {
87 | Log.LogError(errorMessage, ProtectedPackageId);
88 | return false;
89 | }
90 | }
91 | catch (Exception ex)
92 | {
93 | Log.LogMessage(MessageImportance.High, "NuSeal: Failed to process license validation for {0}. Error: {1}", ProtectedPackageId, ex.Message);
94 | }
95 |
96 | return true;
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/src/NuSeal/build/NuSeal.props:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | $([MSBuild]::NormalizePath('$(MSBuildThisFileDirectory)', '..', 'tasks', 'netstandard2.0', 'NuSeal_0_4_0.dll'))
5 |
6 |
7 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/NuSeal/build/NuSeal.targets:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 | $(OutputPath)
8 |
9 |
10 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/src/ProjectMetadata.props:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Fati Iseni
5 | Pozitron Group
6 | Copyright © 2025 Pozitron Group
7 | NuSeal License Validation
8 |
9 | https://github.com/fiseni/NuSeal
10 | https://github.com/fiseni/NuSeal
11 | true
12 | git
13 | MIT
14 | readme-nuget.md
15 | icon.png
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/src/ProjectMetadata.targets:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/tests/NuSeal.Tests/AssetsTests.cs:
--------------------------------------------------------------------------------
1 | namespace Tests;
2 |
3 | public class AssetsTests : IDisposable
4 | {
5 | private readonly string _testDirectory;
6 |
7 | public AssetsTests()
8 | {
9 | _testDirectory = Path.Combine(Path.GetTempPath(), $"NuSealTests_{Guid.NewGuid()}");
10 | Directory.CreateDirectory(_testDirectory);
11 | }
12 |
13 | [Fact]
14 | public void GenerateProps_ReturnUniqueAsset()
15 | {
16 | var parameters = new ConsumerParameters(
17 | nuSealAssetsPath: "path/to/nuseal/assets",
18 | nuSealVersion: "1.2.3",
19 | outputPath: _testDirectory,
20 | packageId: "Prefix.PackageId1",
21 | assemblyName: "Assembly1",
22 | pems: GeneratePemData("ProductA", "ProductB"),
23 | condition: null,
24 | propsFile: null,
25 | targetsFile: null,
26 | validationMode: null,
27 | validationScope: null);
28 |
29 | var expectedContent = $"""
30 |
31 |
32 |
33 | $([MSBuild]::NormalizePath('$(NugetPackageRoot)', 'nuseal', '1.2.3', 'tasks', 'netstandard2.0', 'NuSeal_1_2_3.dll'))
34 |
35 |
36 |
37 |
48 |
59 |
60 |
61 |
65 |
66 |
67 | """;
68 |
69 | var result = Assets.GenerateProps(parameters);
70 |
71 | result.Should().Be(expectedContent);
72 | }
73 |
74 | [Fact]
75 | public void GenerateTargets_ReturnUniqueAsset_GivenDirectScope()
76 | {
77 | var parameters1 = new ConsumerParameters(
78 | nuSealAssetsPath: "path/to/nuseal/assets",
79 | nuSealVersion: "1.2.3",
80 | outputPath: _testDirectory,
81 | packageId: "Prefix.PackageId1",
82 | assemblyName: "Assembly1",
83 | pems: GeneratePemData("ProductA"),
84 | condition: null,
85 | propsFile: null,
86 | targetsFile: null,
87 | validationMode: "Warning",
88 | validationScope: "Direct");
89 |
90 | var expectedContent = $"""
91 |
92 |
93 |
96 |
97 |
106 |
107 |
108 |
109 |
110 | """;
111 |
112 | var result = Assets.GenerateTargets(parameters1);
113 |
114 | result.Should().Be(expectedContent);
115 | }
116 |
117 | [Fact]
118 | public void GenerateTargets_ReturnUniqueAsset_GivenTransitiveScope()
119 | {
120 | var parameters1 = new ConsumerParameters(
121 | nuSealAssetsPath: "path/to/nuseal/assets",
122 | nuSealVersion: "1.2.3",
123 | outputPath: _testDirectory,
124 | packageId: "Prefix.PackageId1",
125 | assemblyName: "Assembly1",
126 | pems: GeneratePemData("ProductA"),
127 | condition: null,
128 | propsFile: null,
129 | targetsFile: null,
130 | validationMode: "Warning",
131 | validationScope: "Transitive");
132 |
133 | var expectedContent = $"""
134 |
135 |
136 |
139 |
140 |
149 |
150 |
151 |
152 |
153 | """;
154 |
155 | var result = Assets.GenerateTargets(parameters1);
156 |
157 | result.Should().Be(expectedContent);
158 | }
159 |
160 | [Fact]
161 | public void GenerateTargets_ReturnUniqueAsset_GivenDirectScopeAndCondition()
162 | {
163 | var parameters1 = new ConsumerParameters(
164 | nuSealAssetsPath: "path/to/nuseal/assets",
165 | nuSealVersion: "1.2.3",
166 | outputPath: _testDirectory,
167 | packageId: "Prefix.PackageId1",
168 | assemblyName: "Assembly1",
169 | pems: GeneratePemData("ProductA"),
170 | condition: "'#(OutputType)' == 'Exe' Or '#(OutputType)' == 'WinExe'",
171 | propsFile: null,
172 | targetsFile: null,
173 | validationMode: "Warning",
174 | validationScope: "Direct");
175 |
176 | var expectedContent = $"""
177 |
178 |
179 |
182 |
183 |
192 |
193 |
194 |
195 |
196 | """;
197 |
198 | var result = Assets.GenerateTargets(parameters1);
199 |
200 | result.Should().Be(expectedContent);
201 | }
202 |
203 | [Fact]
204 | public void GenerateTargets_ReturnUniqueAsset_GivenTransitiveScopeAndCondition()
205 | {
206 | var parameters1 = new ConsumerParameters(
207 | nuSealAssetsPath: "path/to/nuseal/assets",
208 | nuSealVersion: "1.2.3",
209 | outputPath: _testDirectory,
210 | packageId: "Prefix.PackageId1",
211 | assemblyName: "Assembly1",
212 | pems: GeneratePemData("ProductA"),
213 | condition: "'#(OutputType)' == 'Exe'",
214 | propsFile: null,
215 | targetsFile: null,
216 | validationMode: "Warning",
217 | validationScope: "Transitive");
218 |
219 | var expectedContent = $"""
220 |
221 |
222 |
225 |
226 |
235 |
236 |
237 |
238 |
239 | """;
240 |
241 | var result = Assets.GenerateTargets(parameters1);
242 |
243 | result.Should().Be(expectedContent);
244 | }
245 |
246 | [Fact]
247 | public void AppendConsumerAsset_ReturnsMergedContent()
248 | {
249 | var input = """
250 |
251 |
252 | 1.0.0
253 |
254 |
255 | """;
256 |
257 | var consumerAssetFile = Path.Combine(_testDirectory, "consumer.props");
258 | var consumerAsset = """
259 |
260 |
261 | TestValue
262 |
263 |
264 | """;
265 | File.WriteAllText(consumerAssetFile, consumerAsset);
266 |
267 | var expectedContent = """
268 |
269 |
270 | 1.0.0
271 |
272 |
273 |
274 | TestValue
275 |
276 |
277 |
278 | """;
279 |
280 | var result = Assets.AppendConsumerAsset(input, consumerAssetFile);
281 |
282 | result.Should().Be(expectedContent);
283 | }
284 |
285 | [Fact]
286 | public void AppendConsumerAsset_ReturnInput_GivenEmptyAsset()
287 | {
288 | var input = """
289 |
290 |
291 | 1.0.0
292 |
293 |
294 | """;
295 |
296 | var consumerAssetFile = Path.Combine(_testDirectory, "consumer.props");
297 | var consumerAsset = """
298 |
299 |
300 | """;
301 | File.WriteAllText(consumerAssetFile, consumerAsset);
302 |
303 | var result = Assets.AppendConsumerAsset(input, consumerAssetFile);
304 |
305 | result.Should().Be(input);
306 | }
307 |
308 | [Fact]
309 | public void AppendConsumerAsset_ReturnInput_GivenAssetFileDoesntExist()
310 | {
311 | var input = "test content";
312 | var file = Path.Combine(_testDirectory, "nonexistent.props");
313 |
314 | var result = Assets.AppendConsumerAsset(input, file);
315 |
316 | result.Should().Be(input);
317 | }
318 |
319 | [Fact]
320 | public void AppendConsumerAsset_ReturnInput_GivenNullAssetFile()
321 | {
322 | var input = "test content";
323 |
324 | var result = Assets.AppendConsumerAsset(input, null);
325 |
326 | result.Should().Be(input);
327 | }
328 |
329 | [Fact]
330 | public void RemoveProjectTags_GivenValidContent()
331 | {
332 | var input = """
333 |
334 |
335 | TestValue
336 |
337 |
338 | """;
339 |
340 | var expected = """
341 |
342 |
343 | TestValue
344 |
345 |
346 | """;
347 |
348 | var result = Assets.RemoveProjectTags(input, "filename");
349 |
350 | result.Should().Be(expected);
351 | }
352 |
353 | [Fact]
354 | public void RemoveProjectTags_ThrowsArgumentException_GivenNoProjectStartTag()
355 | {
356 | var input = """
357 |
358 | TestValue
359 |
360 |
361 | """;
362 |
363 | var action = () =>
364 | {
365 | Assets.RemoveProjectTags(input, "filename");
366 | };
367 |
368 | action.Should().Throw();
369 | }
370 |
371 | [Fact]
372 | public void RemoveProjectTags_ThrowsArgumentException_GivenNoEndTag()
373 | {
374 | var input = """
375 |
376 |
377 | TestValue
378 |
379 | """;
380 |
381 | var action = () =>
382 | {
383 | Assets.RemoveProjectTags(input, "filename");
384 | };
385 |
386 | action.Should().Throw();
387 | }
388 |
389 | [Fact]
390 | public void RemoveProjectTags_ThrowsArgumentException_GivenNoClosingTags()
391 | {
392 | var input = """
393 |
398 | {
399 | Assets.RemoveProjectTags(input, "filename");
400 | };
401 |
402 | action.Should().Throw();
403 | }
404 |
405 | [Fact]
406 | public void RemoveProjectTags_ThrowsArgumentException_GivenOpenAngleBracketButNoProjectTag()
407 | {
408 | var input = """
409 |
411 | TestValue
412 |
413 | """;
414 |
415 | var action = () =>
416 | {
417 | Assets.RemoveProjectTags(input, "filename");
418 | };
419 |
420 | action.Should().Throw();
421 | }
422 |
423 | [Fact]
424 | public void DetectLineEnding_ReturnCRLF_GivenCRLF()
425 | {
426 | var input = "some\r\ntext";
427 |
428 | var result = Assets.DetectLineEnding(input);
429 |
430 | result.Should().Be("\r\n");
431 | }
432 |
433 | [Fact]
434 | public void DetectLineEnding_ReturnLF_GivenLF()
435 | {
436 | var input = "some\ntext";
437 |
438 | var result = Assets.DetectLineEnding(input);
439 |
440 | result.Should().Be("\n");
441 | }
442 |
443 | [Fact]
444 | public void DetectLineEnding_ReturnLF_GivenNoNewLine()
445 | {
446 | var input = "some text";
447 |
448 | var result = Assets.DetectLineEnding(input);
449 |
450 | result.Should().Be("\n");
451 | }
452 |
453 | private static PemData[] GeneratePemData(params string[] productNames)
454 | {
455 | return productNames.Select(productName =>
456 | {
457 | var publicKeyPem = """
458 | -----BEGIN PUBLIC KEY-----
459 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAygsSRxp1MInUqDz2nPk+
460 | +BPP8ojPdydEg8inQbx7SonV+HBuUfRnbhp/0w298bQP0X1fz+RwnjUDdakV9vsa
461 | zrK3RH/Ulq0tLrQXKBRZVP2rot4SWWYcdncnvYIiXSpAK2kisxYX1BL56wAEigKX
462 | CoCmQl8YleATGf2EEZ80tOmL6eEtJZ3rFxcaIbdx6z10XwIkvMM4CgbEPIpGZqva
463 | lceYsQ/KioeoxbyjBiNOu3DnkjpzhgbDg/dMKMVvZ1DiJBWvaKkToVDpfGFFpwUs
464 | OEvTfMysHGQ/YqQU+AoGjQJr3/n4X9+THSsXF+Ga7mxMc9x9SwOMebM9q6LDUoG7
465 | cQIDAQAB
466 | -----END PUBLIC KEY-----
467 | """;
468 |
469 | return new PemData(productName, publicKeyPem);
470 | }).ToArray();
471 | }
472 |
473 | public void Dispose()
474 | {
475 | try
476 | {
477 | Directory.Delete(_testDirectory, true);
478 | }
479 | catch
480 | {
481 | }
482 | }
483 | }
484 |
--------------------------------------------------------------------------------
/tests/NuSeal.Tests/ConsumerParametersTests.cs:
--------------------------------------------------------------------------------
1 | namespace Tests;
2 |
3 | public class ConsumerParametersTests
4 | {
5 | [Fact]
6 | public void Constructor_SetsDefaultOptions_GivenNull()
7 | {
8 | var parameters = new ConsumerParameters(
9 | nuSealAssetsPath: "path/to/assets",
10 | nuSealVersion: "1.0.0",
11 | outputPath: "path/to/output",
12 | packageId: "Package.Id",
13 | assemblyName: "Assembly",
14 | pems: Array.Empty(),
15 | condition: null,
16 | propsFile: null,
17 | targetsFile: null,
18 | validationMode: null,
19 | validationScope: null);
20 |
21 | parameters.ValidationMode.Should().Be("Error");
22 | parameters.ValidationScope.Should().Be("Direct");
23 | parameters.Options.Should().NotBeNull();
24 | }
25 |
26 | [Fact]
27 | public void Constructor_HandlesNullPropsAndTargetsFiles()
28 | {
29 | var parameters = new ConsumerParameters(
30 | nuSealAssetsPath: "path/to/assets",
31 | nuSealVersion: "1.0.0",
32 | outputPath: "path/to/output",
33 | packageId: "Package.Id",
34 | assemblyName: "Assembly",
35 | pems: Array.Empty(),
36 | condition: null,
37 | propsFile: null,
38 | targetsFile: null,
39 | validationMode: "Error",
40 | validationScope: "Transitive");
41 |
42 | parameters.PropsFile.Should().BeNull();
43 | parameters.TargetsFile.Should().BeNull();
44 | }
45 |
46 | [Fact]
47 | public void Constructor_SetsPropsAndTargetsToNull_GivenEmptyStrings()
48 | {
49 | var parameters = new ConsumerParameters(
50 | nuSealAssetsPath: "path/to/assets",
51 | nuSealVersion: "1.0.0",
52 | outputPath: "path/to/output",
53 | packageId: "Package.Id",
54 | assemblyName: "Assembly",
55 | pems: Array.Empty(),
56 | condition: null,
57 | propsFile: "",
58 | targetsFile: " ",
59 | validationMode: "Error",
60 | validationScope: "Transitive");
61 |
62 | parameters.PropsFile.Should().BeNull();
63 | parameters.TargetsFile.Should().BeNull();
64 | }
65 |
66 | [Fact]
67 | public void Suffix_RemovesDots_FromPackageId()
68 | {
69 | var parameters = new ConsumerParameters(
70 | nuSealAssetsPath: "path/to/assets",
71 | nuSealVersion: "1.0.0",
72 | outputPath: "path/to/output",
73 | packageId: "My.Complex.Package.Id",
74 | assemblyName: "Assembly",
75 | pems: Array.Empty(),
76 | condition: null,
77 | propsFile: null,
78 | targetsFile: null,
79 | validationMode: "Error",
80 | validationScope: "Transitive");
81 |
82 | parameters.PackageSuffix.Should().Be("MyComplexPackageId");
83 | }
84 |
85 | [Theory]
86 | [InlineData("Warning", "Direct", "Warning", "Direct")]
87 | [InlineData("warning", "direct", "Warning", "Direct")]
88 | [InlineData("ERROR", "TRANSITIVE", "Error", "Transitive")]
89 | [InlineData("Error", "Transitive", "Error", "Transitive")]
90 | [InlineData("Unknown", "Unknown", "Error", "Direct")]
91 | public void Constructor_HandlesValidationModeAndScopeCorrectly(
92 | string inputValidationMode,
93 | string inputValidationScope,
94 | string expectedMode,
95 | string expectedScope)
96 | {
97 | var parameters = new ConsumerParameters(
98 | nuSealAssetsPath: "path/to/assets",
99 | nuSealVersion: "1.0.0",
100 | outputPath: "path/to/output",
101 | packageId: "Package.Id",
102 | assemblyName: "Assembly",
103 | pems: Array.Empty(),
104 | condition: null,
105 | propsFile: null,
106 | targetsFile: null,
107 | validationMode: inputValidationMode,
108 | validationScope: inputValidationScope);
109 |
110 | parameters.ValidationMode.Should().Be(expectedMode);
111 | parameters.ValidationScope.Should().Be(expectedScope);
112 | }
113 |
114 | [Fact]
115 | public void Constructor_SetsPropertiesCorrectly()
116 | {
117 | var nuSealAssetsPath = "path/to/assets";
118 | var nuSealVersion = "1.2.3";
119 | var outputPath = "path/to/output";
120 | var packageId = "Prefix.PackageId";
121 | var assemblyName = "AssemblyName";
122 | var pems = new PemData[1];
123 | var condition = "'#(OutputType)' == 'Exe' Or '#(OutputType)' == 'WinExe'";
124 | var propsFile = "props.props";
125 | var targetsFile = "targets.targets";
126 | var validationMode = "Warning";
127 | var validationScope = "Direct";
128 |
129 | var parameters = new ConsumerParameters(
130 | nuSealAssetsPath: nuSealAssetsPath,
131 | nuSealVersion: nuSealVersion,
132 | outputPath: outputPath,
133 | packageId: packageId,
134 | assemblyName: assemblyName,
135 | pems: pems,
136 | condition: condition,
137 | propsFile: propsFile,
138 | targetsFile: targetsFile,
139 | validationMode: validationMode,
140 | validationScope: validationScope);
141 |
142 | parameters.NuSealAssetsPath.Should().Be(nuSealAssetsPath);
143 | parameters.NuSealVersion.Should().Be(nuSealVersion);
144 | parameters.OutputPath.Should().Be(outputPath);
145 | parameters.PackageId.Should().Be(packageId);
146 | parameters.AssemblyName.Should().Be(assemblyName);
147 | parameters.Pems.Should().BeSameAs(pems);
148 | parameters.TargetCondition.Should().Be("Condition=\"'$(OutputType)' == 'Exe' Or '$(OutputType)' == 'WinExe'\"");
149 | parameters.PropsFile.Should().Be(propsFile);
150 | parameters.TargetsFile.Should().Be(targetsFile);
151 | parameters.ValidationMode.Should().Be("Warning");
152 | parameters.ValidationScope.Should().Be("Direct");
153 | parameters.PackageSuffix.Should().Be("PrefixPackageId");
154 | parameters.Options.Should().NotBeNull();
155 | }
156 | }
157 |
--------------------------------------------------------------------------------
/tests/NuSeal.Tests/FileUtils_TryGetLicenseTests.cs:
--------------------------------------------------------------------------------
1 | namespace Tests;
2 |
3 | public class FileUtils_TryGetLicenseTests : IDisposable
4 | {
5 | private readonly string _testRootDirectory;
6 | private readonly string _subDirectory;
7 | private readonly string _subSubDirectory;
8 | private readonly string _targetAssemblyPath;
9 | private readonly string _productName;
10 |
11 | public FileUtils_TryGetLicenseTests()
12 | {
13 | // Create a unique test directory structure for each test run
14 | _testRootDirectory = Path.Combine(Path.GetTempPath(), $"NuSealTests_{Guid.NewGuid()}");
15 | _subDirectory = Path.Combine(_testRootDirectory, "SubDir");
16 | _subSubDirectory = Path.Combine(_subDirectory, "SubSubDir");
17 |
18 | Directory.CreateDirectory(_testRootDirectory);
19 | Directory.CreateDirectory(_subDirectory);
20 | Directory.CreateDirectory(_subSubDirectory);
21 |
22 | _targetAssemblyPath = Path.Combine(_subSubDirectory, "TargetAssembly.dll");
23 | File.WriteAllText(_targetAssemblyPath, "dummy content");
24 |
25 | _productName = "TestProduct";
26 | }
27 |
28 | [Fact]
29 | public void ReturnsFalse_GivenNoLicenseFile()
30 | {
31 | var result = FileUtils.TryGetLicense(_targetAssemblyPath, _productName, out string licenseContent);
32 |
33 | result.Should().BeFalse();
34 | licenseContent.Should().BeEmpty();
35 | }
36 |
37 | [Fact]
38 | public void ReturnsTrue_GivenLicenseFileInSameDirectory()
39 | {
40 | var licenseContent = "license-content";
41 | var licenseFilePath = Path.Combine(_subSubDirectory, $"{_productName}.lic");
42 | File.WriteAllText(licenseFilePath, licenseContent);
43 |
44 | var result = FileUtils.TryGetLicense(_targetAssemblyPath, _productName, out string retrievedContent);
45 |
46 | result.Should().BeTrue();
47 | retrievedContent.Should().Be(licenseContent);
48 | }
49 |
50 | [Fact]
51 | public void ReturnsTrue_GivenLicenseFileInParentDirectory()
52 | {
53 | var licenseContent = "license-content";
54 | var licenseFilePath = Path.Combine(_subDirectory, $"{_productName}.lic");
55 | File.WriteAllText(licenseFilePath, licenseContent);
56 |
57 | var result = FileUtils.TryGetLicense(_targetAssemblyPath, _productName, out string retrievedContent);
58 |
59 | result.Should().BeTrue();
60 | retrievedContent.Should().Be(licenseContent);
61 | }
62 |
63 | [Fact]
64 | public void ReturnsTrue_GivenLicenseFileInRootDirectory()
65 | {
66 | var licenseContent = "license-content";
67 | var licenseFilePath = Path.Combine(_testRootDirectory, $"{_productName}.lic");
68 | File.WriteAllText(licenseFilePath, licenseContent);
69 |
70 | var result = FileUtils.TryGetLicense(_targetAssemblyPath, _productName, out string retrievedContent);
71 |
72 | result.Should().BeTrue();
73 | retrievedContent.Should().Be(licenseContent);
74 | }
75 |
76 | [Fact]
77 | public void ReturnsFirstLicenseFound_GivenMultipleLicenseFiles()
78 | {
79 | var licenseContentRoot = "license-content-root";
80 | var licenseContentParent = "license-content-parent";
81 | var licenseContentSame = "license-content-same";
82 |
83 | File.WriteAllText(Path.Combine(_testRootDirectory, $"{_productName}.lic"), licenseContentRoot);
84 | File.WriteAllText(Path.Combine(_subDirectory, $"{_productName}.lic"), licenseContentParent);
85 | File.WriteAllText(Path.Combine(_subSubDirectory, $"{_productName}.lic"), licenseContentSame);
86 |
87 | var result = FileUtils.TryGetLicense(_targetAssemblyPath, _productName, out string retrievedContent);
88 |
89 | result.Should().BeTrue();
90 | // Should find the one in the same directory first
91 | retrievedContent.Should().Be(licenseContentSame);
92 | }
93 |
94 | [Fact]
95 | public void TrimsLicenseContent_GivenLicenseWithWhitespace()
96 | {
97 | var licenseContent = " license-content-with-whitespace \r\n ";
98 | var licenseFilePath = Path.Combine(_subSubDirectory, $"{_productName}.lic");
99 | File.WriteAllText(licenseFilePath, licenseContent);
100 |
101 | var result = FileUtils.TryGetLicense(_targetAssemblyPath, _productName, out string retrievedContent);
102 |
103 | result.Should().BeTrue();
104 | retrievedContent.Should().Be("license-content-with-whitespace");
105 | }
106 |
107 | [Fact]
108 | public void ReturnsFalse_GivenInvalidTargetAssemblyPath()
109 | {
110 | var invalidPath = Path.Combine(_subSubDirectory, "NonExistent.dll");
111 |
112 | var result = FileUtils.TryGetLicense(invalidPath, _productName, out string licenseContent);
113 |
114 | result.Should().BeFalse();
115 | licenseContent.Should().BeEmpty();
116 | }
117 |
118 | [Fact]
119 | public void ReturnsFalse_GivenNullTargetAssemblyPath()
120 | {
121 | var result = FileUtils.TryGetLicense(null!, _productName, out string licenseContent);
122 |
123 | result.Should().BeFalse();
124 | licenseContent.Should().BeEmpty();
125 | }
126 |
127 | [Fact]
128 | public void ReturnsFalse_GivenEmptyTargetAssemblyPath()
129 | {
130 | var result = FileUtils.TryGetLicense("", _productName, out string licenseContent);
131 |
132 | result.Should().BeFalse();
133 | licenseContent.Should().BeEmpty();
134 | }
135 |
136 | [Fact]
137 | public void UsesProductNameForLicenseFile_GivenDifferentProductNames()
138 | {
139 | var licenseContent = "license-content";
140 | var otherProductName = "OtherProduct";
141 |
142 | // Create license file for a different product
143 | File.WriteAllText(Path.Combine(_subSubDirectory, $"{otherProductName}.lic"), licenseContent);
144 |
145 | var result = FileUtils.TryGetLicense(_targetAssemblyPath, _productName, out string retrievedContent);
146 |
147 | result.Should().BeFalse();
148 | retrievedContent.Should().BeEmpty();
149 | }
150 |
151 | [Fact]
152 | public void ReturnsFalse_GivenEmptyProductName()
153 | {
154 | var licenseContent = "license-content";
155 | // Create license file with empty name
156 | File.WriteAllText(Path.Combine(_subSubDirectory, ".lic"), licenseContent);
157 |
158 | var result = FileUtils.TryGetLicense(_targetAssemblyPath, "", out string retrievedContent);
159 |
160 | result.Should().BeFalse();
161 | retrievedContent.Should().BeEmpty();
162 | }
163 |
164 | public void Dispose()
165 | {
166 | try
167 | {
168 | Directory.Delete(_testRootDirectory, true);
169 | }
170 | catch
171 | {
172 | }
173 | }
174 | }
175 |
--------------------------------------------------------------------------------
/tests/NuSeal.Tests/GenerateConsumerAssetsTaskTests.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Build.Framework;
2 | using Microsoft.Build.Utilities;
3 |
4 | namespace Tests;
5 |
6 | public class GenerateConsumerAssetsTaskTests : IDisposable
7 | {
8 | private readonly TestBuildEngine _buildEngine;
9 | private readonly string _testDirectory;
10 |
11 | public GenerateConsumerAssetsTaskTests()
12 | {
13 | _buildEngine = new TestBuildEngine();
14 | _testDirectory = Path.Combine(Path.GetTempPath(), $"NuSealTests_{Guid.NewGuid()}");
15 | Directory.CreateDirectory(_testDirectory);
16 | }
17 |
18 | [Fact]
19 | public void ReturnsFalse_LogsError_GivenExceptionInProcessing()
20 | {
21 | var nonExistentPath = Path.Combine(_testDirectory, "nonexistent");
22 |
23 | var task = new GenerateConsumerAssetsTask()
24 | {
25 | BuildEngine = _buildEngine,
26 | NuSealAssetsPath = "path/to/nuseal/assets",
27 | NuSealVersion = "1.2.3",
28 | ConsumerOutputPath = nonExistentPath,
29 | ConsumerPackageId = "Prefix.PackageId1",
30 | ConsumerAssemblyName = "Assembly1",
31 | ConsumerPems = GeneratePemTaskItems("ProductA"),
32 | ConsumerPropsFile = null,
33 | ConsumerTargetsFile = null,
34 | ValidationMode = null,
35 | ValidationScope = null,
36 | };
37 | var result = task.Execute();
38 |
39 | result.Should().BeFalse();
40 | _buildEngine.Messages.Should().BeEmpty();
41 | _buildEngine.Warnings.Should().BeEmpty();
42 | _buildEngine.Errors.Should().NotBeEmpty();
43 | }
44 |
45 | [Fact]
46 | public void ReturnsFalse_LogsError_GivenEmptyNuSealVersion()
47 | {
48 | var task = new GenerateConsumerAssetsTask()
49 | {
50 | BuildEngine = _buildEngine,
51 | NuSealAssetsPath = "path/to/nuseal/assets",
52 | NuSealVersion = "",
53 | ConsumerOutputPath = _testDirectory,
54 | ConsumerPackageId = "Prefix.PackageId1",
55 | ConsumerAssemblyName = "Assembly1",
56 | ConsumerPems = GeneratePemTaskItems("ProductA"),
57 | ConsumerPropsFile = null,
58 | ConsumerTargetsFile = null,
59 | ValidationMode = null,
60 | ValidationScope = null,
61 | };
62 | var result = task.Execute();
63 |
64 | result.Should().BeFalse();
65 | _buildEngine.Messages.Should().BeEmpty();
66 | _buildEngine.Warnings.Should().BeEmpty();
67 | _buildEngine.Errors.Should().NotBeEmpty();
68 | }
69 |
70 | [Fact]
71 | public void ReturnsFalse_LogsError_GivenEmptyConsumerOutputPath()
72 | {
73 | var task = new GenerateConsumerAssetsTask()
74 | {
75 | BuildEngine = _buildEngine,
76 | NuSealAssetsPath = "path/to/nuseal/assets",
77 | NuSealVersion = "1.2.3",
78 | ConsumerOutputPath = "",
79 | ConsumerPackageId = "Prefix.PackageId1",
80 | ConsumerAssemblyName = "Assembly1",
81 | ConsumerPems = GeneratePemTaskItems("ProductA"),
82 | ConsumerPropsFile = null,
83 | ConsumerTargetsFile = null,
84 | ValidationMode = null,
85 | ValidationScope = null,
86 | };
87 | var result = task.Execute();
88 |
89 | result.Should().BeFalse();
90 | _buildEngine.Messages.Should().BeEmpty();
91 | _buildEngine.Warnings.Should().BeEmpty();
92 | _buildEngine.Errors.Should().NotBeEmpty();
93 | }
94 |
95 | [Fact]
96 | public void ReturnsFalse_LogsError_GivenEmptyConsumerPackageId()
97 | {
98 | var task = new GenerateConsumerAssetsTask()
99 | {
100 | BuildEngine = _buildEngine,
101 | NuSealAssetsPath = "path/to/nuseal/assets",
102 | NuSealVersion = "1.2.3",
103 | ConsumerOutputPath = _testDirectory,
104 | ConsumerPackageId = "",
105 | ConsumerAssemblyName = "Assembly1",
106 | ConsumerPems = GeneratePemTaskItems("ProductA"),
107 | ConsumerPropsFile = null,
108 | ConsumerTargetsFile = null,
109 | ValidationMode = null,
110 | ValidationScope = null,
111 | };
112 | var result = task.Execute();
113 |
114 | result.Should().BeFalse();
115 | _buildEngine.Messages.Should().BeEmpty();
116 | _buildEngine.Warnings.Should().BeEmpty();
117 | _buildEngine.Errors.Should().NotBeEmpty();
118 | }
119 |
120 | [Fact]
121 | public void ReturnsFalse_LogsError_GivenEmptyConsumerAssemblyName()
122 | {
123 | var task = new GenerateConsumerAssetsTask()
124 | {
125 | BuildEngine = _buildEngine,
126 | NuSealAssetsPath = "path/to/nuseal/assets",
127 | NuSealVersion = "1.2.3",
128 | ConsumerOutputPath = _testDirectory,
129 | ConsumerPackageId = "Prefix.PackageId1",
130 | ConsumerAssemblyName = "",
131 | ConsumerPems = GeneratePemTaskItems("ProductA"),
132 | ConsumerPropsFile = null,
133 | ConsumerTargetsFile = null,
134 | ValidationMode = null,
135 | ValidationScope = null,
136 | };
137 | var result = task.Execute();
138 |
139 | result.Should().BeFalse();
140 | _buildEngine.Messages.Should().BeEmpty();
141 | _buildEngine.Warnings.Should().BeEmpty();
142 | _buildEngine.Errors.Should().NotBeEmpty();
143 | }
144 |
145 | [Fact]
146 | public void ReturnsFalse_LogsError_GivenEmptyPems()
147 | {
148 | var task = new GenerateConsumerAssetsTask()
149 | {
150 | BuildEngine = _buildEngine,
151 | NuSealAssetsPath = "path/to/nuseal/assets",
152 | NuSealVersion = "1.2.3",
153 | ConsumerOutputPath = _testDirectory,
154 | ConsumerPackageId = "Prefix.PackageId1",
155 | ConsumerAssemblyName = "Assembly1",
156 | ConsumerPems = Array.Empty(),
157 | ConsumerPropsFile = null,
158 | ConsumerTargetsFile = null,
159 | ValidationMode = null,
160 | ValidationScope = null,
161 | };
162 | var result = task.Execute();
163 |
164 | result.Should().BeFalse();
165 | _buildEngine.Messages.Should().BeEmpty();
166 | _buildEngine.Warnings.Should().BeEmpty();
167 | _buildEngine.Errors.Should().NotBeEmpty();
168 | }
169 |
170 | [Fact]
171 | public void ReturnsFalse_LogsError_GivenPemWithMissingProductName()
172 | {
173 | var task = new GenerateConsumerAssetsTask()
174 | {
175 | BuildEngine = _buildEngine,
176 | NuSealAssetsPath = "path/to/nuseal/assets",
177 | NuSealVersion = "1.2.3",
178 | ConsumerOutputPath = _testDirectory,
179 | ConsumerPackageId = "Prefix.PackageId1",
180 | ConsumerAssemblyName = "Assembly1",
181 | ConsumerPems = GeneratePemTaskItems("Product1", ""),
182 | ConsumerPropsFile = null,
183 | ConsumerTargetsFile = null,
184 | ValidationMode = null,
185 | ValidationScope = null,
186 | };
187 | var result = task.Execute();
188 |
189 | result.Should().BeFalse();
190 | _buildEngine.Messages.Should().BeEmpty();
191 | _buildEngine.Warnings.Should().BeEmpty();
192 | _buildEngine.Errors.Should().NotBeEmpty();
193 | }
194 |
195 | [Fact]
196 | public void ReturnTrueAndGeneratesAssets()
197 | {
198 | var consumerPropsFile = Path.Combine(_testDirectory, "consumer.props");
199 | var consumerProps = """
200 |
201 |
202 | PropsTestValue
203 |
204 |
205 | """;
206 | File.WriteAllText(consumerPropsFile, consumerProps);
207 |
208 | var consumerTargetsFile = Path.Combine(_testDirectory, "consumer.targets");
209 | var consumerTargets = """
210 |
211 |
212 | TargetsTestValue
213 |
214 |
215 | """;
216 | File.WriteAllText(consumerTargetsFile, consumerTargets);
217 |
218 | var expectedProps = $"""
219 |
220 |
221 |
222 | $([MSBuild]::NormalizePath('$(NugetPackageRoot)', 'nuseal', '1.2.3', 'tasks', 'netstandard2.0', 'NuSeal_1_2_3.dll'))
223 |
224 |
225 |
226 |
237 |
248 |
249 |
250 |
254 |
255 |
256 |
257 | PropsTestValue
258 |
259 |
260 |
261 | """;
262 |
263 | var expectedTargets = $"""
264 |
265 |
266 |
269 |
270 |
279 |
280 |
281 |
282 |
283 |
284 | TargetsTestValue
285 |
286 |
287 |
288 | """;
289 |
290 | var task = new GenerateConsumerAssetsTask()
291 | {
292 | BuildEngine = _buildEngine,
293 | NuSealAssetsPath = "path/to/nuseal/assets",
294 | NuSealVersion = "1.2.3",
295 | ConsumerOutputPath = _testDirectory,
296 | ConsumerPackageId = "Prefix.PackageId1",
297 | ConsumerAssemblyName = "Assembly1",
298 | ConsumerPems = GeneratePemTaskItems("ProductA", "ProductB"),
299 | ConsumerPropsFile = consumerPropsFile,
300 | ConsumerTargetsFile = consumerTargetsFile,
301 | ValidationMode = "Error",
302 | ValidationScope = "Transitive",
303 | };
304 | var result = task.Execute();
305 |
306 | var generatedProps = File.ReadAllText(Path.Combine(_testDirectory, "Prefix.PackageId1.props"));
307 | var generatedTargets = File.ReadAllText(Path.Combine(_testDirectory, "Prefix.PackageId1.targets"));
308 |
309 | result.Should().BeTrue();
310 | generatedProps.Should().Be(expectedProps);
311 | generatedTargets.Should().Be(expectedTargets);
312 | _buildEngine.Messages.Should().BeEmpty();
313 | _buildEngine.Warnings.Should().BeEmpty();
314 | _buildEngine.Errors.Should().BeEmpty();
315 | }
316 |
317 | private ITaskItem[] GeneratePemTaskItems(params string[] productNames)
318 | {
319 | return productNames.Select(productName =>
320 | {
321 | var publicKeyPem = """
322 | -----BEGIN PUBLIC KEY-----
323 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAygsSRxp1MInUqDz2nPk+
324 | +BPP8ojPdydEg8inQbx7SonV+HBuUfRnbhp/0w298bQP0X1fz+RwnjUDdakV9vsa
325 | zrK3RH/Ulq0tLrQXKBRZVP2rot4SWWYcdncnvYIiXSpAK2kisxYX1BL56wAEigKX
326 | CoCmQl8YleATGf2EEZ80tOmL6eEtJZ3rFxcaIbdx6z10XwIkvMM4CgbEPIpGZqva
327 | lceYsQ/KioeoxbyjBiNOu3DnkjpzhgbDg/dMKMVvZ1DiJBWvaKkToVDpfGFFpwUs
328 | OEvTfMysHGQ/YqQU+AoGjQJr3/n4X9+THSsXF+Ga7mxMc9x9SwOMebM9q6LDUoG7
329 | cQIDAQAB
330 | -----END PUBLIC KEY-----
331 | """;
332 |
333 | var path = Path.Combine(_testDirectory, $"{Guid.NewGuid().ToString()}.pem");
334 | File.WriteAllText(path, publicKeyPem);
335 | var taskItem = new TaskItem(path);
336 | if (!string.IsNullOrWhiteSpace(productName))
337 | {
338 | taskItem.SetMetadata("ProductName", productName);
339 | }
340 | return taskItem;
341 | }).ToArray();
342 | }
343 |
344 | public void Dispose()
345 | {
346 | try
347 | {
348 | Directory.Delete(_testDirectory, true);
349 | }
350 | catch
351 | {
352 | }
353 | }
354 | }
355 |
--------------------------------------------------------------------------------
/tests/NuSeal.Tests/GlobalUsings.cs:
--------------------------------------------------------------------------------
1 | global using FluentAssertions;
2 | global using NuSeal;
3 | global using Xunit;
4 |
--------------------------------------------------------------------------------
/tests/NuSeal.Tests/LicenseValidatorTests.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.IdentityModel.JsonWebTokens;
2 | using Microsoft.IdentityModel.Tokens;
3 | using System.Security.Claims;
4 | using System.Security.Cryptography;
5 | using System.Text;
6 | using System.Text.Json;
7 |
8 | namespace Tests;
9 |
10 | public class LicenseValidatorTests
11 | {
12 | private const string _productName = "TestProduct";
13 | private readonly RsaKeyPair _keyPair;
14 |
15 | public LicenseValidatorTests()
16 | {
17 | _keyPair = GenerateRsaKeyPair();
18 | }
19 |
20 | [Fact]
21 | public void ReturnsValid_GivenValidLicense()
22 | {
23 | var pemData = new PemData(_productName, _keyPair.PublicKeyPem);
24 | var validLicense = GenerateValidLicense();
25 |
26 | var result = LicenseValidator.Validate(pemData, validLicense);
27 |
28 | result.Should().Be(LicenseValidationResult.Valid);
29 | }
30 |
31 | [Fact]
32 | public void ReturnsValid_GivenValidLicenseWithDifferentProductCase()
33 | {
34 | var pemData = new PemData(_productName.ToUpper(), _keyPair.PublicKeyPem);
35 | var validLicense = GenerateLicense(productName: _productName.ToLower());
36 |
37 | var result = LicenseValidator.Validate(pemData, validLicense);
38 |
39 | result.Should().Be(LicenseValidationResult.Valid);
40 | }
41 |
42 | [Fact]
43 | public void ReturnsValid_GivenExpiredLicenseWithinClockSkew()
44 | {
45 | var pemData = new PemData(_productName, _keyPair.PublicKeyPem);
46 | var expiredLicense = GenerateLicense(
47 | startDate: DateTimeOffset.UtcNow.AddDays(-10),
48 | expirationDate: DateTimeOffset.UtcNow.AddMinutes(-1));
49 |
50 | var result = LicenseValidator.Validate(pemData, expiredLicense);
51 |
52 | result.Should().Be(LicenseValidationResult.Valid);
53 | }
54 |
55 | [Fact]
56 | public void ReturnsValid_GivenFutureLicenseWithinClockSkew()
57 | {
58 | var pemData = new PemData(_productName, _keyPair.PublicKeyPem);
59 | var futureLicense = GenerateLicense(
60 | startDate: DateTimeOffset.UtcNow.AddMinutes(1),
61 | expirationDate: DateTimeOffset.UtcNow.AddDays(20));
62 |
63 | var result = LicenseValidator.Validate(pemData, futureLicense);
64 |
65 | result.Should().Be(LicenseValidationResult.Valid);
66 | }
67 |
68 | [Fact]
69 | public void ReturnsInvalid_GivenFutureLicense()
70 | {
71 | var pemData = new PemData(_productName, _keyPair.PublicKeyPem);
72 | var futureLicense = GenerateLicense(
73 | startDate: DateTimeOffset.UtcNow.AddDays(10),
74 | expirationDate: DateTimeOffset.UtcNow.AddDays(20));
75 |
76 | var result = LicenseValidator.Validate(pemData, futureLicense);
77 |
78 | result.Should().Be(LicenseValidationResult.Invalid);
79 | }
80 |
81 | [Fact]
82 | public void ReturnsExpiredOutsideGracePeriod_GivenExpiredLicense()
83 | {
84 | var pemData = new PemData(_productName, _keyPair.PublicKeyPem);
85 | var expiredLicense = GenerateLicense(
86 | startDate: DateTimeOffset.UtcNow.AddDays(-10),
87 | expirationDate: DateTimeOffset.UtcNow.AddDays(-1));
88 |
89 | var result = LicenseValidator.Validate(pemData, expiredLicense);
90 |
91 | result.Should().Be(LicenseValidationResult.ExpiredOutsideGracePeriod);
92 | }
93 |
94 | [Fact]
95 | public void ReturnsExpiredOutsideGracePeriod_GivenExpiredLicenseOutsideGracePeriod()
96 | {
97 | var pemData = new PemData(_productName, _keyPair.PublicKeyPem);
98 | var expiredLicense = GenerateLicense(
99 | startDate: DateTimeOffset.UtcNow.AddDays(-20),
100 | expirationDate: DateTimeOffset.UtcNow.AddDays(-10),
101 | gracePeriodDays: 7);
102 |
103 | var result = LicenseValidator.Validate(pemData, expiredLicense);
104 |
105 | result.Should().Be(LicenseValidationResult.ExpiredOutsideGracePeriod);
106 | }
107 |
108 | [Fact]
109 | public void ReturnsExpiredWithinGracePeriod_GivenExpiredLicenseWithinGracePeriod()
110 | {
111 | var pemData = new PemData(_productName, _keyPair.PublicKeyPem);
112 | var expiredLicense = GenerateLicense(
113 | startDate: DateTimeOffset.UtcNow.AddDays(-10),
114 | expirationDate: DateTimeOffset.UtcNow.AddDays(-2),
115 | gracePeriodDays: 7);
116 |
117 | var result = LicenseValidator.Validate(pemData, expiredLicense);
118 |
119 | result.Should().Be(LicenseValidationResult.ExpiredWithinGracePeriod);
120 | }
121 |
122 | [Fact]
123 | public void ReturnsInvalid_GivenNoStartDate()
124 | {
125 | var pemData = new PemData(_productName, _keyPair.PublicKeyPem);
126 | var license = GenerateLicense(
127 | includeStartDate: false);
128 |
129 | var result = LicenseValidator.Validate(pemData, license);
130 |
131 | result.Should().Be(LicenseValidationResult.Invalid);
132 | }
133 |
134 | [Fact]
135 | public void ReturnsInvalid_GivenNoEndDate()
136 | {
137 | var pemData = new PemData(_productName, _keyPair.PublicKeyPem);
138 | var license = GenerateLicense(
139 | includeEndDate: false);
140 |
141 | var result = LicenseValidator.Validate(pemData, license);
142 |
143 | result.Should().Be(LicenseValidationResult.Invalid);
144 | }
145 |
146 | [Fact]
147 | public void ReturnsInvalid_GivenLicenseWithDifferentProductName()
148 | {
149 | var pemData = new PemData(_productName, _keyPair.PublicKeyPem);
150 | var licenseWithDifferentProduct = GenerateLicense(productName: "DifferentProduct");
151 |
152 | var result = LicenseValidator.Validate(pemData, licenseWithDifferentProduct);
153 |
154 | result.Should().Be(LicenseValidationResult.Invalid);
155 | }
156 |
157 | [Fact]
158 | public void ReturnsInvalid_GivenLicenseWithMissingProductClaim()
159 | {
160 | var pemData = new PemData(_productName, _keyPair.PublicKeyPem);
161 | var licenseWithoutProductClaim = GenerateLicense(includeProductName: false);
162 |
163 | var result = LicenseValidator.Validate(pemData, licenseWithoutProductClaim);
164 |
165 | result.Should().Be(LicenseValidationResult.Invalid);
166 | }
167 |
168 | [Fact]
169 | public void ReturnsInvalid_GivenInvalidPem()
170 | {
171 | var invalidPublicKeyPem = "invalid-public-key-pem";
172 | var pemData = new PemData(_productName, invalidPublicKeyPem);
173 | var license = GenerateValidLicense();
174 |
175 | var result = LicenseValidator.Validate(pemData, license);
176 |
177 | result.Should().Be(LicenseValidationResult.Invalid);
178 | }
179 |
180 | [Fact]
181 | public void ReturnsInvalid_GivenMissingAlgHeader()
182 | {
183 | var pemData = new PemData(_productName, _keyPair.PublicKeyPem);
184 | var license = GenerateJwtWithoutAlg();
185 |
186 | var result = LicenseValidator.Validate(pemData, license);
187 |
188 | result.Should().Be(LicenseValidationResult.Invalid);
189 | }
190 |
191 | [Fact]
192 | public void ReturnsInvalid_GivenInvalidLicenseFormat()
193 | {
194 | var pemData = new PemData(_productName, _keyPair.PublicKeyPem);
195 | var invalidLicense = "not-a-valid-jwt-token";
196 |
197 | var result = LicenseValidator.Validate(pemData, invalidLicense);
198 |
199 | result.Should().Be(LicenseValidationResult.Invalid);
200 | }
201 |
202 | [Fact]
203 | public void ReturnsInvalid_GivenLicenseSignedWithDifferentKey()
204 | {
205 | var pemData = new PemData(_productName, _keyPair.PublicKeyPem);
206 | var differentKeyPair = GenerateRsaKeyPair();
207 | var licenseSignedWithDifferentKey = GenerateLicense(differentKeyPair.PrivateKeyPem);
208 |
209 | var result = LicenseValidator.Validate(pemData, licenseSignedWithDifferentKey);
210 |
211 | result.Should().Be(LicenseValidationResult.Invalid);
212 | }
213 |
214 | [Fact]
215 | public void ReturnsInvalid_GivenNullPublicKey()
216 | {
217 | var pemData = new PemData(_productName, null!);
218 | var license = GenerateValidLicense();
219 |
220 | var result = LicenseValidator.Validate(pemData, license);
221 |
222 | result.Should().Be(LicenseValidationResult.Invalid);
223 | }
224 |
225 | [Fact]
226 | public void ReturnsInvalid_GivenEmptyPublicKey()
227 | {
228 | var pemData = new PemData(_productName, "");
229 | var license = GenerateValidLicense();
230 |
231 | var result = LicenseValidator.Validate(pemData, license);
232 |
233 | result.Should().Be(LicenseValidationResult.Invalid);
234 | }
235 |
236 | [Fact]
237 | public void ReturnsInvalid_GivenWhitespacePublicKey()
238 | {
239 | var pemData = new PemData(_productName, " ");
240 | var license = GenerateValidLicense();
241 |
242 | var result = LicenseValidator.Validate(pemData, license);
243 |
244 | result.Should().Be(LicenseValidationResult.Invalid);
245 | }
246 |
247 | [Fact]
248 | public void ReturnsInvalid_GivenNullProductName()
249 | {
250 | var pemData = new PemData(null!, _keyPair.PublicKeyPem);
251 | var license = GenerateValidLicense();
252 |
253 | var result = LicenseValidator.Validate(pemData, license);
254 |
255 | result.Should().Be(LicenseValidationResult.Invalid);
256 | }
257 |
258 | [Fact]
259 | public void ReturnsInvalid_GivenEmptyProductName()
260 | {
261 | var pemData = new PemData("", _keyPair.PublicKeyPem);
262 | var license = GenerateValidLicense();
263 |
264 | var result = LicenseValidator.Validate(pemData, license);
265 |
266 | result.Should().Be(LicenseValidationResult.Invalid);
267 | }
268 |
269 | [Fact]
270 | public void ReturnsInvalid_GivenWhitespaceProductName()
271 | {
272 | var pemData = new PemData(" ", _keyPair.PublicKeyPem);
273 | var license = GenerateValidLicense();
274 |
275 | var result = LicenseValidator.Validate(pemData, license);
276 |
277 | result.Should().Be(LicenseValidationResult.Invalid);
278 | }
279 |
280 | [Fact]
281 | public void ReturnsInvalid_GivenNullLicense()
282 | {
283 | var pemData = new PemData(_productName, _keyPair.PublicKeyPem);
284 |
285 | var result = LicenseValidator.Validate(pemData, null!);
286 |
287 | result.Should().Be(LicenseValidationResult.Invalid);
288 | }
289 |
290 | [Fact]
291 | public void ReturnsInvalid_GivenEmptyLicense()
292 | {
293 | var pemData = new PemData(_productName, _keyPair.PublicKeyPem);
294 |
295 | var result = LicenseValidator.Validate(pemData, "");
296 |
297 | result.Should().Be(LicenseValidationResult.Invalid);
298 | }
299 |
300 | [Fact]
301 | public void ReturnsInvalid_GivenWhitespaceLicense()
302 | {
303 | var pemData = new PemData(_productName, _keyPair.PublicKeyPem);
304 |
305 | var result = LicenseValidator.Validate(pemData, " ");
306 |
307 | result.Should().Be(LicenseValidationResult.Invalid);
308 | }
309 |
310 | private string GenerateValidLicense() => GenerateLicense();
311 |
312 | private string GenerateLicense(
313 | string? privateKeyPem = null,
314 | string? productName = null,
315 | DateTimeOffset? startDate = null,
316 | DateTimeOffset? expirationDate = null,
317 | int? gracePeriodDays = null,
318 | bool includeProductName = true,
319 | bool includeStartDate = true,
320 | bool includeEndDate = true)
321 | {
322 | using var rsa = RSA.Create();
323 |
324 | privateKeyPem ??= _keyPair.PrivateKeyPem;
325 | rsa.ImportFromPem(privateKeyPem.AsSpan());
326 |
327 | var handler = new JsonWebTokenHandler();
328 |
329 | var credentials = new SigningCredentials(
330 | new RsaSecurityKey(rsa),
331 | SecurityAlgorithms.RsaSha256);
332 |
333 | var claims = new List
334 | {
335 | new(JwtRegisteredClaimNames.Sub, Guid.NewGuid().ToString()),
336 | };
337 |
338 | if (includeProductName is true)
339 | {
340 | claims.Add(new("product", productName ?? _productName));
341 | }
342 |
343 | if (gracePeriodDays.HasValue)
344 | {
345 | claims.Add(new("grace_period_days", gracePeriodDays.Value.ToString(), ClaimValueTypes.Integer32));
346 | }
347 |
348 | var tokenDescriptor = new SecurityTokenDescriptor
349 | {
350 | Subject = new ClaimsIdentity(claims),
351 | SigningCredentials = credentials,
352 | };
353 |
354 | if (includeStartDate)
355 | {
356 | tokenDescriptor.NotBefore = startDate?.UtcDateTime ?? DateTimeOffset.UtcNow.UtcDateTime;
357 | }
358 | else
359 | {
360 | handler.SetDefaultTimesOnTokenCreation = false;
361 | }
362 |
363 | if (includeEndDate)
364 | {
365 | tokenDescriptor.Expires = expirationDate?.UtcDateTime ?? DateTimeOffset.UtcNow.AddYears(1).UtcDateTime;
366 | }
367 | else
368 | {
369 | handler.SetDefaultTimesOnTokenCreation = false;
370 | }
371 |
372 | var token = handler.CreateToken(tokenDescriptor);
373 |
374 | return token;
375 | }
376 |
377 | public static string GenerateJwtWithoutAlg()
378 | {
379 | var header = new Dictionary { { "typ", "JWT" } }; // no "alg"
380 | var payload = new Dictionary
381 | {
382 | { "sub", "test-sub" },
383 | { "iat", DateTimeOffset.UtcNow.ToUnixTimeSeconds() },
384 | { "exp", DateTimeOffset.UtcNow.AddMinutes(5).ToUnixTimeSeconds() }
385 | };
386 |
387 | string headerJson = JsonSerializer.Serialize(header);
388 | string payloadJson = JsonSerializer.Serialize(payload);
389 |
390 | string headerPart = Base64UrlEncode(Encoding.UTF8.GetBytes(headerJson));
391 | string payloadPart = Base64UrlEncode(Encoding.UTF8.GetBytes(payloadJson));
392 | var signaturePart = Base64UrlEncode(RandomNumberGenerator.GetBytes(32));
393 |
394 | return $"{headerPart}.{payloadPart}.{signaturePart}";
395 |
396 | static string Base64UrlEncode(byte[] bytes)
397 | {
398 | string s = Convert.ToBase64String(bytes);
399 | s = s.TrimEnd('=');
400 | s = s.Replace('+', '-').Replace('/', '_');
401 | return s;
402 | }
403 | }
404 |
405 | private static RsaKeyPair GenerateRsaKeyPair()
406 | {
407 | using var rsa = RSA.Create(2048);
408 | var privateKey = rsa.ExportPkcs8PrivateKeyPem();
409 | var publicKey = rsa.ExportSubjectPublicKeyInfoPem();
410 | return new RsaKeyPair(privateKey, publicKey);
411 | }
412 |
413 | private record RsaKeyPair(string PrivateKeyPem, string PublicKeyPem);
414 | }
415 |
--------------------------------------------------------------------------------
/tests/NuSeal.Tests/NuSeal.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Tests
5 | net9.0
6 | enable
7 | enable
8 |
9 | false
10 | true
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | all
21 | runtime; build; native; contentfiles; analyzers; buildtransitive
22 |
23 |
24 | all
25 | runtime; build; native; contentfiles; analyzers; buildtransitive
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/tests/NuSeal.Tests/TestLogger.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Build.Framework;
2 | using System.Collections;
3 |
4 | namespace Tests;
5 |
6 | public class TestBuildEngine : IBuildEngine
7 | {
8 | public List Messages { get; } = [];
9 | public List Warnings { get; } = [];
10 | public List Errors { get; } = [];
11 | public List CustomEvents { get; } = [];
12 |
13 | public bool BuildProjectFile(
14 | string projectFileName,
15 | string[] targetNames,
16 | IDictionary globalProperties,
17 | IDictionary targetOutputs) => true;
18 | public int ColumnNumberOfTaskNode => 0;
19 | public bool ContinueOnError => false;
20 | public int LineNumberOfTaskNode => 0;
21 | public string ProjectFileOfTaskNode => "test.proj";
22 | public void LogCustomEvent(CustomBuildEventArgs e) => CustomEvents.Add(e);
23 | public void LogErrorEvent(BuildErrorEventArgs e) => Errors.Add(e);
24 | public void LogMessageEvent(BuildMessageEventArgs e) => Messages.Add(e);
25 | public void LogWarningEvent(BuildWarningEventArgs e) => Warnings.Add(e);
26 | }
27 |
--------------------------------------------------------------------------------
/tests/NuSeal.Tests/ValidateLicenseTaskTests_0_4_0.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Build.Framework;
2 | using Microsoft.Build.Utilities;
3 |
4 | namespace Tests;
5 |
6 | public class ValidateLicenseTaskTests_0_4_0 : IDisposable
7 | {
8 | private readonly RsaPemPair _rsaPemPair;
9 | private readonly TestBuildEngine _buildEngine;
10 | private readonly string _tempDir;
11 | private readonly string _targetAssemblyPath;
12 |
13 | public ValidateLicenseTaskTests_0_4_0()
14 | {
15 | _rsaPemPair = RsaKeyGenerator.GeneratePem();
16 | _buildEngine = new TestBuildEngine();
17 | _tempDir = Path.Combine(Path.GetTempPath(), $"NuSealTests_{Guid.NewGuid()}");
18 | Directory.CreateDirectory(_tempDir);
19 | _targetAssemblyPath = Path.Combine(_tempDir, "MainApp.dll");
20 | File.WriteAllText(_targetAssemblyPath, "Dummy content");
21 | }
22 |
23 | [Fact]
24 | public void ReturnsTrue_GivenValidLicenseAndDirectScope()
25 | {
26 | var productName = "TestProduct";
27 | CreateValidLicense(productName);
28 |
29 | var task = new ValidateLicenseTask_0_4_0
30 | {
31 | BuildEngine = _buildEngine,
32 | TargetAssemblyPath = _targetAssemblyPath,
33 | NuSealVersion = "1.2.3",
34 | ProtectedPackageId = "TestPackageId",
35 | ProtectedAssemblyName = "TestAssembly",
36 | Pems = GeneratePemTaskItems(productName),
37 | ValidationMode = "Error",
38 | ValidationScope = "Direct",
39 | };
40 |
41 | var result = task.Execute();
42 |
43 | result.Should().BeTrue();
44 | _buildEngine.Messages.Should().BeEmpty();
45 | _buildEngine.Warnings.Should().BeEmpty();
46 | _buildEngine.Errors.Should().BeEmpty();
47 | }
48 |
49 | [Fact]
50 | public void ReturnsTrue_GivenValidLicenseAndTransitiveScope()
51 | {
52 | var productName = "TestProduct";
53 | CreateValidLicense(productName);
54 |
55 | var task = new ValidateLicenseTask_0_4_0
56 | {
57 | BuildEngine = _buildEngine,
58 | TargetAssemblyPath = _targetAssemblyPath,
59 | NuSealVersion = "1.2.3",
60 | ProtectedPackageId = "TestPackageId",
61 | ProtectedAssemblyName = "TestAssembly",
62 | Pems = GeneratePemTaskItems(productName),
63 | ValidationMode = "Error",
64 | ValidationScope = "Transitive",
65 | };
66 |
67 | var result = task.Execute();
68 |
69 | result.Should().BeTrue();
70 | _buildEngine.Messages.Should().BeEmpty();
71 | _buildEngine.Warnings.Should().BeEmpty();
72 | _buildEngine.Errors.Should().BeEmpty();
73 | }
74 |
75 | [Fact]
76 | public void ReturnsTrue_LogsWarning_GivenNoLicenseAndWarningMode()
77 | {
78 | var productName = "TestProduct";
79 |
80 | var task = new ValidateLicenseTask_0_4_0
81 | {
82 | BuildEngine = _buildEngine,
83 | TargetAssemblyPath = _targetAssemblyPath,
84 | NuSealVersion = "1.2.3",
85 | ProtectedPackageId = "TestPackageId",
86 | ProtectedAssemblyName = "TestAssembly",
87 | Pems = GeneratePemTaskItems(productName),
88 | ValidationMode = "Warning",
89 | ValidationScope = "Direct",
90 | };
91 |
92 | var result = task.Execute();
93 |
94 | result.Should().BeTrue();
95 | _buildEngine.Messages.Should().BeEmpty();
96 | _buildEngine.Warnings.Should().NotBeEmpty();
97 | _buildEngine.Errors.Should().BeEmpty();
98 | }
99 |
100 | [Fact]
101 | public void ReturnsFalse_LogsError_GivenNoLicenseAndErrorMode()
102 | {
103 | var productName = "TestProduct";
104 |
105 | var task = new ValidateLicenseTask_0_4_0
106 | {
107 | BuildEngine = _buildEngine,
108 | TargetAssemblyPath = _targetAssemblyPath,
109 | NuSealVersion = "1.2.3",
110 | ProtectedPackageId = "TestPackageId",
111 | ProtectedAssemblyName = "TestAssembly",
112 | Pems = GeneratePemTaskItems(productName),
113 | ValidationMode = "Error",
114 | ValidationScope = "Direct",
115 | };
116 |
117 | var result = task.Execute();
118 |
119 | result.Should().BeFalse();
120 | _buildEngine.Messages.Should().BeEmpty();
121 | _buildEngine.Warnings.Should().BeEmpty();
122 | _buildEngine.Errors.Should().NotBeEmpty();
123 | }
124 |
125 | [Fact]
126 | public void ReturnsTrue_LogsInfo_GivenNoPems()
127 | {
128 | var productName = "TestProduct";
129 | CreateValidLicense(productName);
130 |
131 | var task = new ValidateLicenseTask_0_4_0
132 | {
133 | BuildEngine = _buildEngine,
134 | TargetAssemblyPath = _targetAssemblyPath,
135 | NuSealVersion = "1.2.3",
136 | ProtectedPackageId = "TestPackageId",
137 | ProtectedAssemblyName = "TestAssembly",
138 | Pems = Array.Empty(),
139 | ValidationMode = "Error",
140 | ValidationScope = "Direct",
141 | };
142 |
143 | var result = task.Execute();
144 |
145 | result.Should().BeTrue();
146 | _buildEngine.Messages.Should().NotBeEmpty();
147 | _buildEngine.Warnings.Should().BeEmpty();
148 | _buildEngine.Errors.Should().BeEmpty();
149 | }
150 |
151 | [Fact]
152 | public void ReturnsTrue_LogsWarning_GivenWarningMode_ExpiredWithinGracePeriodLicense()
153 | {
154 | var productName = "TestProduct";
155 |
156 | var licenseParameters = new LicenseParameters
157 | {
158 | ProductName = productName,
159 | PrivateKeyPem = _rsaPemPair.PrivateKey,
160 | ExpirationDate = DateTime.UtcNow.AddDays(-1),
161 | GracePeriodInDays = 7
162 | };
163 |
164 | CreateLicense(licenseParameters);
165 |
166 | var task = new ValidateLicenseTask_0_4_0
167 | {
168 | BuildEngine = _buildEngine,
169 | TargetAssemblyPath = _targetAssemblyPath,
170 | NuSealVersion = "1.2.3",
171 | ProtectedPackageId = "TestPackageId",
172 | ProtectedAssemblyName = "TestAssembly",
173 | Pems = GeneratePemTaskItems(productName),
174 | ValidationMode = "Warning",
175 | ValidationScope = "Direct",
176 | };
177 |
178 | var result = task.Execute();
179 |
180 | result.Should().BeTrue();
181 | _buildEngine.Messages.Should().BeEmpty();
182 | _buildEngine.Warnings.Should().NotBeEmpty();
183 | _buildEngine.Errors.Should().BeEmpty();
184 | }
185 |
186 | [Fact]
187 | public void ReturnsTrue_LogsWarning_GivenErrorMode_ExpiredWithinGracePeriodLicense()
188 | {
189 | var productName = "TestProduct";
190 |
191 | var licenseParameters = new LicenseParameters
192 | {
193 | ProductName = productName,
194 | PrivateKeyPem = _rsaPemPair.PrivateKey,
195 | ExpirationDate = DateTime.UtcNow.AddDays(-1),
196 | GracePeriodInDays = 7
197 | };
198 |
199 | CreateLicense(licenseParameters);
200 |
201 | var task = new ValidateLicenseTask_0_4_0
202 | {
203 | BuildEngine = _buildEngine,
204 | TargetAssemblyPath = _targetAssemblyPath,
205 | NuSealVersion = "1.2.3",
206 | ProtectedPackageId = "TestPackageId",
207 | ProtectedAssemblyName = "TestAssembly",
208 | Pems = GeneratePemTaskItems(productName),
209 | ValidationMode = "Error",
210 | ValidationScope = "Direct",
211 | };
212 |
213 | var result = task.Execute();
214 |
215 | result.Should().BeTrue();
216 | _buildEngine.Messages.Should().BeEmpty();
217 | _buildEngine.Warnings.Should().NotBeEmpty();
218 | _buildEngine.Errors.Should().BeEmpty();
219 | }
220 |
221 | [Fact]
222 | public void ReturnsTrue_LogsWarning_GivenWarningMode_ExpiredOutsideGracePeriodLicense()
223 | {
224 | var productName = "TestProduct";
225 |
226 | var licenseParameters = new LicenseParameters
227 | {
228 | ProductName = productName,
229 | PrivateKeyPem = _rsaPemPair.PrivateKey,
230 | ExpirationDate = DateTime.UtcNow.AddDays(-10),
231 | GracePeriodInDays = 1
232 | };
233 |
234 | CreateLicense(licenseParameters);
235 |
236 | var task = new ValidateLicenseTask_0_4_0
237 | {
238 | BuildEngine = _buildEngine,
239 | TargetAssemblyPath = _targetAssemblyPath,
240 | NuSealVersion = "1.2.3",
241 | ProtectedPackageId = "TestPackageId",
242 | ProtectedAssemblyName = "TestAssembly",
243 | Pems = GeneratePemTaskItems(productName),
244 | ValidationMode = "Warning",
245 | ValidationScope = "Direct",
246 | };
247 |
248 | var result = task.Execute();
249 |
250 | result.Should().BeTrue();
251 | _buildEngine.Messages.Should().BeEmpty();
252 | _buildEngine.Warnings.Should().NotBeEmpty();
253 | _buildEngine.Errors.Should().BeEmpty();
254 | }
255 |
256 | [Fact]
257 | public void ReturnsFalse_LogsError_GivenErrorMode_ExpiredOutsideGracePeriodLicense()
258 | {
259 | var productName = "TestProduct";
260 |
261 | var licenseParameters = new LicenseParameters
262 | {
263 | ProductName = productName,
264 | PrivateKeyPem = _rsaPemPair.PrivateKey,
265 | ExpirationDate = DateTime.UtcNow.AddDays(-10),
266 | GracePeriodInDays = 1
267 | };
268 |
269 | CreateLicense(licenseParameters);
270 |
271 | var task = new ValidateLicenseTask_0_4_0
272 | {
273 | BuildEngine = _buildEngine,
274 | TargetAssemblyPath = _targetAssemblyPath,
275 | NuSealVersion = "1.2.3",
276 | ProtectedPackageId = "TestPackageId",
277 | ProtectedAssemblyName = "TestAssembly",
278 | Pems = GeneratePemTaskItems(productName),
279 | ValidationMode = "Error",
280 | ValidationScope = "Direct",
281 | };
282 |
283 | var result = task.Execute();
284 |
285 | result.Should().BeFalse();
286 | _buildEngine.Messages.Should().BeEmpty();
287 | _buildEngine.Warnings.Should().BeEmpty();
288 | _buildEngine.Errors.Should().NotBeEmpty();
289 | }
290 |
291 | [Fact]
292 | public void ReturnsTrue_LogsInfo_GivenNullTargetAssemblyPath()
293 | {
294 | var task = new ValidateLicenseTask_0_4_0
295 | {
296 | BuildEngine = _buildEngine,
297 | TargetAssemblyPath = null!,
298 | NuSealVersion = "1.2.3",
299 | ProtectedPackageId = "x",
300 | ProtectedAssemblyName = "x",
301 | Pems = GeneratePemTaskItems("x"),
302 | ValidationMode = "x",
303 | ValidationScope = "x",
304 | };
305 |
306 | var result = task.Execute();
307 |
308 | result.Should().BeTrue();
309 | _buildEngine.Messages.Should().NotBeEmpty();
310 | _buildEngine.Warnings.Should().BeEmpty();
311 | _buildEngine.Errors.Should().BeEmpty();
312 | }
313 |
314 | [Fact]
315 | public void ReturnsTrue_LogsInfo_GivenEmptyTargetAssemblyPath()
316 | {
317 | var task = new ValidateLicenseTask_0_4_0
318 | {
319 | BuildEngine = _buildEngine,
320 | TargetAssemblyPath = "",
321 | NuSealVersion = "1.2.3",
322 | ProtectedPackageId = "x",
323 | ProtectedAssemblyName = "x",
324 | Pems = GeneratePemTaskItems("x"),
325 | ValidationMode = "x",
326 | ValidationScope = "x",
327 | };
328 |
329 | var result = task.Execute();
330 |
331 | result.Should().BeTrue();
332 | _buildEngine.Messages.Should().NotBeEmpty();
333 | _buildEngine.Warnings.Should().BeEmpty();
334 | _buildEngine.Errors.Should().BeEmpty();
335 | }
336 |
337 | [Fact]
338 | public void ReturnsTrue_LogsInfo_GivenWhitespaceTargetAssemblyPath()
339 | {
340 | var task = new ValidateLicenseTask_0_4_0
341 | {
342 | BuildEngine = _buildEngine,
343 | TargetAssemblyPath = " ",
344 | NuSealVersion = "1.2.3",
345 | ProtectedPackageId = "x",
346 | ProtectedAssemblyName = "x",
347 | Pems = GeneratePemTaskItems("x"),
348 | ValidationMode = "x",
349 | ValidationScope = "x",
350 | };
351 |
352 | var result = task.Execute();
353 |
354 | result.Should().BeTrue();
355 | _buildEngine.Messages.Should().NotBeEmpty();
356 | _buildEngine.Warnings.Should().BeEmpty();
357 | _buildEngine.Errors.Should().BeEmpty();
358 | }
359 |
360 | [Fact]
361 | public void ReturnsTrue_LogsInfo_GivenNullNuSealVersion()
362 | {
363 | var task = new ValidateLicenseTask_0_4_0
364 | {
365 | BuildEngine = _buildEngine,
366 | TargetAssemblyPath = "x",
367 | NuSealVersion = null!,
368 | ProtectedPackageId = "x",
369 | ProtectedAssemblyName = "x",
370 | Pems = GeneratePemTaskItems("x"),
371 | ValidationMode = "x",
372 | ValidationScope = "x",
373 | };
374 |
375 | var result = task.Execute();
376 |
377 | result.Should().BeTrue();
378 | _buildEngine.Messages.Should().NotBeEmpty();
379 | _buildEngine.Warnings.Should().BeEmpty();
380 | _buildEngine.Errors.Should().BeEmpty();
381 | }
382 |
383 | [Fact]
384 | public void ReturnsTrue_LogsInfo_GivenEmptyNuSealVersion()
385 | {
386 | var task = new ValidateLicenseTask_0_4_0
387 | {
388 | BuildEngine = _buildEngine,
389 | TargetAssemblyPath = "x",
390 | NuSealVersion = "",
391 | ProtectedPackageId = "x",
392 | ProtectedAssemblyName = "x",
393 | Pems = GeneratePemTaskItems("x"),
394 | ValidationMode = "x",
395 | ValidationScope = "x",
396 | };
397 |
398 | var result = task.Execute();
399 |
400 | result.Should().BeTrue();
401 | _buildEngine.Messages.Should().NotBeEmpty();
402 | _buildEngine.Warnings.Should().BeEmpty();
403 | _buildEngine.Errors.Should().BeEmpty();
404 | }
405 |
406 | [Fact]
407 | public void ReturnsTrue_LogsInfo_GivenWhitespaceNuSealVersion()
408 | {
409 | var task = new ValidateLicenseTask_0_4_0
410 | {
411 | BuildEngine = _buildEngine,
412 | TargetAssemblyPath = "x",
413 | NuSealVersion = " ",
414 | ProtectedPackageId = "x",
415 | ProtectedAssemblyName = "x",
416 | Pems = GeneratePemTaskItems("x"),
417 | ValidationMode = "x",
418 | ValidationScope = "x",
419 | };
420 |
421 | var result = task.Execute();
422 |
423 | result.Should().BeTrue();
424 | _buildEngine.Messages.Should().NotBeEmpty();
425 | _buildEngine.Warnings.Should().BeEmpty();
426 | _buildEngine.Errors.Should().BeEmpty();
427 | }
428 |
429 | [Fact]
430 | public void ReturnsTrue_LogsInfo_GivenNullProtectedPackageId()
431 | {
432 | var task = new ValidateLicenseTask_0_4_0
433 | {
434 | BuildEngine = _buildEngine,
435 | TargetAssemblyPath = "x",
436 | NuSealVersion = "1.2.3",
437 | ProtectedPackageId = null!,
438 | ProtectedAssemblyName = "x",
439 | Pems = GeneratePemTaskItems("x"),
440 | ValidationMode = "x",
441 | ValidationScope = "x",
442 | };
443 |
444 | var result = task.Execute();
445 |
446 | result.Should().BeTrue();
447 | _buildEngine.Messages.Should().NotBeEmpty();
448 | _buildEngine.Warnings.Should().BeEmpty();
449 | _buildEngine.Errors.Should().BeEmpty();
450 | }
451 |
452 | [Fact]
453 | public void ReturnsTrue_LogsInfo_GivenEmptyProtectedPackageId()
454 | {
455 | var task = new ValidateLicenseTask_0_4_0
456 | {
457 | BuildEngine = _buildEngine,
458 | TargetAssemblyPath = "x",
459 | NuSealVersion = "1.2.3",
460 | ProtectedPackageId = "",
461 | ProtectedAssemblyName = "x",
462 | Pems = GeneratePemTaskItems("x"),
463 | ValidationMode = "x",
464 | ValidationScope = "x",
465 | };
466 |
467 | var result = task.Execute();
468 |
469 | result.Should().BeTrue();
470 | _buildEngine.Messages.Should().NotBeEmpty();
471 | _buildEngine.Warnings.Should().BeEmpty();
472 | _buildEngine.Errors.Should().BeEmpty();
473 | }
474 |
475 | [Fact]
476 | public void ReturnsTrue_LogsInfo_GivenWhitespaceProtectedPackageId()
477 | {
478 | var task = new ValidateLicenseTask_0_4_0
479 | {
480 | BuildEngine = _buildEngine,
481 | TargetAssemblyPath = "x",
482 | NuSealVersion = "1.2.3",
483 | ProtectedPackageId = " ",
484 | ProtectedAssemblyName = "x",
485 | Pems = GeneratePemTaskItems("x"),
486 | ValidationMode = "x",
487 | ValidationScope = "x",
488 | };
489 |
490 | var result = task.Execute();
491 |
492 | result.Should().BeTrue();
493 | _buildEngine.Messages.Should().NotBeEmpty();
494 | _buildEngine.Warnings.Should().BeEmpty();
495 | _buildEngine.Errors.Should().BeEmpty();
496 | }
497 |
498 | [Fact]
499 | public void ReturnsTrue_LogsInfo_GivenNullProtectedAssemblyName()
500 | {
501 | var task = new ValidateLicenseTask_0_4_0
502 | {
503 | BuildEngine = _buildEngine,
504 | TargetAssemblyPath = "x",
505 | NuSealVersion = "1.2.3",
506 | ProtectedPackageId = "x",
507 | ProtectedAssemblyName = null!,
508 | Pems = GeneratePemTaskItems("x"),
509 | ValidationMode = "x",
510 | ValidationScope = "x",
511 | };
512 |
513 | var result = task.Execute();
514 |
515 | result.Should().BeTrue();
516 | _buildEngine.Messages.Should().NotBeEmpty();
517 | _buildEngine.Warnings.Should().BeEmpty();
518 | _buildEngine.Errors.Should().BeEmpty();
519 | }
520 |
521 | [Fact]
522 | public void ReturnsTrue_LogsInfo_GivenEmptyProtectedAssemblyName()
523 | {
524 | var task = new ValidateLicenseTask_0_4_0
525 | {
526 | BuildEngine = _buildEngine,
527 | TargetAssemblyPath = "x",
528 | NuSealVersion = "1.2.3",
529 | ProtectedPackageId = "x",
530 | ProtectedAssemblyName = "",
531 | Pems = GeneratePemTaskItems("x"),
532 | ValidationMode = "x",
533 | ValidationScope = "x",
534 | };
535 |
536 | var result = task.Execute();
537 |
538 | result.Should().BeTrue();
539 | _buildEngine.Messages.Should().NotBeEmpty();
540 | _buildEngine.Warnings.Should().BeEmpty();
541 | _buildEngine.Errors.Should().BeEmpty();
542 | }
543 |
544 | [Fact]
545 | public void ReturnsTrue_LogsInfo_GivenWhitespaceProtectedAssemblyName()
546 | {
547 | var task = new ValidateLicenseTask_0_4_0
548 | {
549 | BuildEngine = _buildEngine,
550 | TargetAssemblyPath = "x",
551 | NuSealVersion = "1.2.3",
552 | ProtectedPackageId = "x",
553 | ProtectedAssemblyName = " ",
554 | Pems = GeneratePemTaskItems("x"),
555 | ValidationMode = "x",
556 | ValidationScope = "x",
557 | };
558 |
559 | var result = task.Execute();
560 |
561 | result.Should().BeTrue();
562 | _buildEngine.Messages.Should().NotBeEmpty();
563 | _buildEngine.Warnings.Should().BeEmpty();
564 | _buildEngine.Errors.Should().BeEmpty();
565 | }
566 |
567 | [Fact]
568 | public void ReturnsTrue_LogsInfo_GivenNullPems()
569 | {
570 | var task = new ValidateLicenseTask_0_4_0
571 | {
572 | BuildEngine = _buildEngine,
573 | TargetAssemblyPath = "x",
574 | NuSealVersion = "1.2.3",
575 | ProtectedPackageId = "x",
576 | ProtectedAssemblyName = "x",
577 | Pems = null!,
578 | ValidationMode = "x",
579 | ValidationScope = "x",
580 | };
581 |
582 | var result = task.Execute();
583 |
584 | result.Should().BeTrue();
585 | _buildEngine.Messages.Should().NotBeEmpty();
586 | _buildEngine.Warnings.Should().BeEmpty();
587 | _buildEngine.Errors.Should().BeEmpty();
588 | }
589 |
590 | [Fact]
591 | public void ReturnsTrue_LogsInfo_GivenEmptyPems()
592 | {
593 | var task = new ValidateLicenseTask_0_4_0
594 | {
595 | BuildEngine = _buildEngine,
596 | TargetAssemblyPath = "x",
597 | NuSealVersion = "1.2.3",
598 | ProtectedPackageId = "x",
599 | ProtectedAssemblyName = "x",
600 | Pems = Array.Empty(),
601 | ValidationMode = "x",
602 | ValidationScope = "x",
603 | };
604 |
605 | var result = task.Execute();
606 |
607 | result.Should().BeTrue();
608 | _buildEngine.Messages.Should().NotBeEmpty();
609 | _buildEngine.Warnings.Should().BeEmpty();
610 | _buildEngine.Errors.Should().BeEmpty();
611 | }
612 |
613 | [Fact]
614 | public void ReturnsTrue_LogsInfo_GivenNullValidationMode()
615 | {
616 | var task = new ValidateLicenseTask_0_4_0
617 | {
618 | BuildEngine = _buildEngine,
619 | TargetAssemblyPath = "x",
620 | NuSealVersion = "1.2.3",
621 | ProtectedPackageId = "x",
622 | ProtectedAssemblyName = "x",
623 | Pems = GeneratePemTaskItems("x"),
624 | ValidationMode = null!,
625 | ValidationScope = "x",
626 | };
627 |
628 | var result = task.Execute();
629 |
630 | result.Should().BeTrue();
631 | _buildEngine.Messages.Should().NotBeEmpty();
632 | _buildEngine.Warnings.Should().BeEmpty();
633 | _buildEngine.Errors.Should().BeEmpty();
634 | }
635 |
636 | [Fact]
637 | public void ReturnsTrue_LogsInfo_GivenEmptyValidationMode()
638 | {
639 | var task = new ValidateLicenseTask_0_4_0
640 | {
641 | BuildEngine = _buildEngine,
642 | TargetAssemblyPath = "x",
643 | NuSealVersion = "1.2.3",
644 | ProtectedPackageId = "x",
645 | ProtectedAssemblyName = "x",
646 | Pems = GeneratePemTaskItems("x"),
647 | ValidationMode = "",
648 | ValidationScope = "x",
649 | };
650 |
651 | var result = task.Execute();
652 |
653 | result.Should().BeTrue();
654 | _buildEngine.Messages.Should().NotBeEmpty();
655 | _buildEngine.Warnings.Should().BeEmpty();
656 | _buildEngine.Errors.Should().BeEmpty();
657 | }
658 |
659 | [Fact]
660 | public void ReturnsTrue_LogsInfo_GivenWhitespaceValidationMode()
661 | {
662 | var task = new ValidateLicenseTask_0_4_0
663 | {
664 | BuildEngine = _buildEngine,
665 | TargetAssemblyPath = "x",
666 | NuSealVersion = "1.2.3",
667 | ProtectedPackageId = "x",
668 | ProtectedAssemblyName = "x",
669 | Pems = GeneratePemTaskItems("x"),
670 | ValidationMode = " ",
671 | ValidationScope = "x",
672 | };
673 |
674 | var result = task.Execute();
675 |
676 | result.Should().BeTrue();
677 | _buildEngine.Messages.Should().NotBeEmpty();
678 | _buildEngine.Warnings.Should().BeEmpty();
679 | _buildEngine.Errors.Should().BeEmpty();
680 | }
681 |
682 | [Fact]
683 | public void ReturnsTrue_LogsInfo_GivenNullValidationScope()
684 | {
685 | var task = new ValidateLicenseTask_0_4_0
686 | {
687 | BuildEngine = _buildEngine,
688 | TargetAssemblyPath = "x",
689 | NuSealVersion = "1.2.3",
690 | ProtectedPackageId = "x",
691 | ProtectedAssemblyName = "x",
692 | Pems = GeneratePemTaskItems("x"),
693 | ValidationMode = "x",
694 | ValidationScope = null!,
695 | };
696 |
697 | var result = task.Execute();
698 |
699 | result.Should().BeTrue();
700 | _buildEngine.Messages.Should().NotBeEmpty();
701 | _buildEngine.Warnings.Should().BeEmpty();
702 | _buildEngine.Errors.Should().BeEmpty();
703 | }
704 |
705 | [Fact]
706 | public void ReturnsTrue_LogsInfo_GivenEmptyValidationScope()
707 | {
708 | var task = new ValidateLicenseTask_0_4_0
709 | {
710 | BuildEngine = _buildEngine,
711 | TargetAssemblyPath = "x",
712 | NuSealVersion = "1.2.3",
713 | ProtectedPackageId = "x",
714 | ProtectedAssemblyName = "x",
715 | Pems = GeneratePemTaskItems("x"),
716 | ValidationMode = "x",
717 | ValidationScope = "",
718 | };
719 |
720 | var result = task.Execute();
721 |
722 | result.Should().BeTrue();
723 | _buildEngine.Messages.Should().NotBeEmpty();
724 | _buildEngine.Warnings.Should().BeEmpty();
725 | _buildEngine.Errors.Should().BeEmpty();
726 | }
727 |
728 | [Fact]
729 | public void ReturnsTrue_LogsInfo_GivenWhitespaceValidationScope()
730 | {
731 | var task = new ValidateLicenseTask_0_4_0
732 | {
733 | BuildEngine = _buildEngine,
734 | TargetAssemblyPath = "x",
735 | NuSealVersion = "1.2.3",
736 | ProtectedPackageId = "x",
737 | ProtectedAssemblyName = "x",
738 | Pems = GeneratePemTaskItems("x"),
739 | ValidationMode = "x",
740 | ValidationScope = " ",
741 | };
742 |
743 | var result = task.Execute();
744 |
745 | result.Should().BeTrue();
746 | _buildEngine.Messages.Should().NotBeEmpty();
747 | _buildEngine.Warnings.Should().BeEmpty();
748 | _buildEngine.Errors.Should().BeEmpty();
749 | }
750 |
751 |
752 | private void CreateValidLicense(string productName)
753 | {
754 | var licenseParams = new LicenseParameters
755 | {
756 | ProductName = productName,
757 | PrivateKeyPem = _rsaPemPair.PrivateKey,
758 | };
759 | var license = License.Create(licenseParams);
760 | var licenseFilePath = Path.Combine(_tempDir, $"{productName}.lic");
761 | File.WriteAllText(licenseFilePath, license);
762 | }
763 |
764 | private void CreateLicense(LicenseParameters licenseParameters)
765 | {
766 | var license = License.Create(licenseParameters);
767 | var licenseFilePath = Path.Combine(_tempDir, $"{licenseParameters.ProductName}.lic");
768 | File.WriteAllText(licenseFilePath, license);
769 | }
770 |
771 | private ITaskItem[] GeneratePemTaskItems(params string[] productNames)
772 | {
773 | return productNames.Select(productName =>
774 | {
775 | var taskItem = new TaskItem(_rsaPemPair.PublicKey);
776 | taskItem.SetMetadata("ProductName", productName);
777 | return taskItem;
778 | }).ToArray();
779 | }
780 |
781 | public void Dispose()
782 | {
783 | try
784 | {
785 | Directory.Delete(_tempDir, true);
786 | }
787 | catch
788 | {
789 | }
790 | }
791 | }
792 |
--------------------------------------------------------------------------------