├── .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 |   [![NuGet](https://img.shields.io/nuget/v/NuSeal.svg)](https://www.nuget.org/packages/NuSeal) 4 | 5 |   [![Actions Status](https://github.com/fiseni/NuSeal/actions/workflows/ci.yml/badge.svg)](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 | --------------------------------------------------------------------------------