From 3f5b07a0d6cf13e78628d9a62103266ba08f3422 Mon Sep 17 00:00:00 2001 From: Sander Kondratjev Date: Wed, 22 Apr 2026 15:48:24 +0300 Subject: [PATCH] NFC-154 Deprecations, warning fixes and using formatter. Fix UnverifiedSigningCertificates fields testing. Signed-off-by: Sander KondratjevSander.Kondratjev@Nortal.com --- .github/workflows/sonarcloud-analysis.yml | 5 +- README.md | 10 + example/README.md | 10 + example/src/.editorconfig | 453 ++++++++ .../Certificates/CertificateLoader.cs | 28 +- .../ClaimsIdentityExtensions.cs | 7 +- .../Controllers/Api/AuthController.cs | 40 +- .../Controllers/Api/BaseController.cs | 25 +- .../Controllers/Api/ChallengeController.cs | 13 +- .../Api/MobileAuthInitController.cs | 8 +- .../Controllers/Api/SignController.cs | 30 +- .../Controllers/WelcomeController.cs | 8 +- .../Dto/AuthenticateRequestDto.cs | 4 +- .../Dto/CertificateDto.cs | 6 +- .../Dto/ChallengeDto.cs | 4 +- .../Dto/DigestDto.cs | 8 +- .../WebEid.AspNetCore.Example/Dto/FileDto.cs | 13 +- .../Dto/SignatureAlgorithmDto.cs | 10 +- .../Dto/SignatureDto.cs | 8 +- .../LoggedInAuthorizationHandler.cs | 7 +- .../Options/WebEidMobileOptions.cs | 4 +- .../Pages/Index.cshtml | 1014 +++++++++-------- .../Pages/WebEidCallback.cshtml | 153 +-- .../Pages/WebEidCallback.cshtml.cs | 6 +- .../Pages/WebEidLogin.cshtml | 69 +- .../Pages/WebEidLogin.cshtml.cs | 2 +- .../Pages/Welcome.cshtml | 289 ++--- .../Pages/Welcome.cshtml.cs | 23 +- .../src/WebEid.AspNetCore.Example/Program.cs | 12 +- .../Services/MobileRequestUriBuilder.cs | 14 +- .../SessionBackedChallengeNonceStore.cs | 40 +- .../Signing/DigiDocConfiguration.cs | 15 +- .../Signing/MobileSigningService.cs | 12 +- .../Signing/SigningService.cs | 33 +- .../src/WebEid.AspNetCore.Example/Startup.cs | 61 +- .../WebEid.AspNetCore.Example.csproj | 6 +- src/.editorconfig | 3 +- .../Certificate/CertificateDataTest.cs | 5 +- src/WebEid.Security.Tests/Logger.cs | 8 +- .../Nonce/ChallengeNonceGeneratorTests.cs | 22 +- .../Nonce/InMemoryChallengeNonceStore.cs | 4 +- .../TestUtils/AbstractTestWithValidator.cs | 9 +- .../TestUtils/ByteArrayExtensions.cs | 2 +- .../TestUtils/Certificates.cs | 12 +- .../TestUtils/DateTimeExtensions.cs | 5 +- .../TestUtils/OcspServiceMaker.cs | 10 +- .../Util/DateTimeProviderTests.cs | 2 +- .../Util/X509CertificateExtensionsTests.cs | 24 +- .../Validator/AuthTokenAlgorithmTest.cs | 19 +- .../AuthTokenCertificateBelgianIdCardTest.cs | 7 +- .../AuthTokenCertificateFinnishIdCardTest.cs | 7 +- .../Validator/AuthTokenCertificateTest.cs | 76 +- .../AuthTokenSignatureValidatorTests.cs | 4 +- .../Validator/AuthTokenStructureTest.cs | 14 +- .../AuthTokenValidationConfigurationTests.cs | 38 +- .../AuthTokenValidatorBuilderTest.cs | 317 +++++- .../Validator/Ocsp/OcspClientTests.cs | 164 +++ .../Ocsp/OcspResponseValidatorTests.cs | 6 +- .../Ocsp/OcspServiceProviderTests.cs | 2 +- .../Validator/Ocsp/OcspUrlsTests.cs | 6 +- ...jectCertificateNotRevokedValidatorTests.cs | 78 +- .../AuthTokenV11CertificateTest.cs | 20 +- .../AuthTokenVersion11ValidatorTest.cs | 85 +- .../AuthTokenVersion1ValidatorTest.cs | 37 +- .../AuthTokenVersionValidatorFactoryTest.cs | 17 +- .../AuthToken/UnverifiedSigningCertificate.cs | 1 - .../Challenge/ChallengeNonce.cs | 24 +- .../Challenge/ChallengeNonceGenerator.cs | 28 +- .../Challenge/IChallengeNonceGenerator.cs | 4 +- .../Challenge/IChallengeNonceStore.cs | 8 +- .../Exceptions/AuthTokenException.cs | 2 + src/WebEid.Security/Util/CertificateLoader.cs | 22 +- src/WebEid.Security/Util/ResourceReader.cs | 6 +- src/WebEid.Security/Util/StreamExtensions.cs | 2 +- .../Util/X509Certificate2Extensions.cs | 2 +- .../Util/X509CertificateExtensions.cs | 4 +- .../Validator/AuthTokenSignatureValidator.cs | 50 +- .../AuthTokenValidationConfiguration.cs | 92 +- .../Validator/AuthTokenValidator.cs | 18 +- .../Validator/AuthTokenValidatorBuilder.cs | 81 +- .../ISubjectCertificateValidator.cs | 4 +- .../SubjectCertificateNotRevokedValidator.cs | 52 +- .../SubjectCertificatePolicyValidator.cs | 19 +- .../SubjectCertificatePurposeValidator.cs | 26 +- .../SubjectCertificateTrustedValidator.cs | 17 +- .../SubjectCertificateValidatorBatch.cs | 4 +- .../Validator/IAuthTokenValidator.cs | 6 +- .../Validator/Ocsp/IOcspClient.cs | 2 +- .../Validator/Ocsp/OcspClient.cs | 18 +- .../Validator/Ocsp/OcspRequestBuilder.cs | 24 +- .../Validator/Ocsp/OcspResponseValidator.cs | 4 +- .../Validator/Ocsp/OcspServiceProvider.cs | 28 +- .../Validator/Ocsp/Service/AiaOcspService.cs | 19 +- .../Service/AiaOcspServiceConfiguration.cs | 35 +- .../Ocsp/Service/DesignatedOcspService.cs | 8 +- .../DesignatedOcspServiceConfiguration.cs | 15 +- .../Validator/Ocsp/Service/IOcspService.cs | 6 +- .../AuthTokenVersion11Validator.cs | 22 +- .../AuthTokenVersion1Validator.cs | 10 +- .../AuthTokenVersionValidatorFactory.cs | 36 +- .../IAuthTokenVersionValidator.cs | 6 +- src/WebEid.Security/WebEid.Security.csproj | 1 - 102 files changed, 2506 insertions(+), 1653 deletions(-) create mode 100644 example/src/.editorconfig create mode 100644 src/WebEid.Security.Tests/Validator/Ocsp/OcspClientTests.cs diff --git a/.github/workflows/sonarcloud-analysis.yml b/.github/workflows/sonarcloud-analysis.yml index c124d77..fe76e86 100644 --- a/.github/workflows/sonarcloud-analysis.yml +++ b/.github/workflows/sonarcloud-analysis.yml @@ -67,7 +67,8 @@ jobs: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} shell: powershell run: | - .\.sonar\scanner\dotnet-sonarscanner begin /k:"web-eid_web-eid-authtoken-validation-dotnet" /o:"web-eid" /d:sonar.cs.opencover.reportsPaths="**/TestResults/**/coverage.opencover.xml" -d:sonar.cs.vstest.reportsPaths="**/TestResults/*.trx" /d:sonar.verbose=true /d:sonar.token="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" + .\.sonar\scanner\dotnet-sonarscanner begin /k:"web-eid_web-eid-authtoken-validation-dotnet" /o:"web-eid" /d:sonar.cs.opencover.reportsPaths="**/TestResults/**/coverage.opencover.xml" /d:sonar.cs.vstest.reportsPaths="**/TestResults/*.trx" /d:sonar.verbose=true /d:sonar.token="$env:SONAR_TOKEN" /d:sonar.host.url="https://sonarcloud.io" dotnet build --configuration Release --no-restore src/WebEid.Security.sln + dotnet format src/WebEid.Security.sln --verify-no-changes --no-restore dotnet test src/WebEid.Security.sln --logger trx --collect:"XPlat Code Coverage" -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=opencover --results-directory "TestResults" - .\.sonar\scanner\dotnet-sonarscanner end /d:sonar.login="${{ secrets.SONAR_TOKEN }}" + .\.sonar\scanner\dotnet-sonarscanner end /d:sonar.token="$env:SONAR_TOKEN" diff --git a/README.md b/README.md index 6ed984f..e62fcb5 100644 --- a/README.md +++ b/README.md @@ -544,6 +544,16 @@ ChallengeNonce challengeNonce = nonceGenerator.GenerateAndStoreNonce(timeToLive) The `GenerateAndStoreNonce(TimeSpan ttl)` method both generates the nonce and stores it in the store. The `ttl` parameter defines nonce time-to-live duration. When the time-to-live passes, the nonce is considered to be expired. +# Code formatting + +The project uses `.editorconfig` for .NET code formatting rules. + +To format the library code, run: + +```bash +dotnet format src/WebEid.Security.sln --no-restore +``` + ## Feedback For technical support or to report issues, please submit a [support ticket](https://github.com/web-eid/web-eid-authtoken-validation-dotnet/issues) or contact our support team at [help@ria.ee](mailto:help@ria.ee). diff --git a/example/README.md b/example/README.md index 9741ba6..861ab7e 100644 --- a/example/README.md +++ b/example/README.md @@ -266,3 +266,13 @@ app.UseForwardedHeaders(new ForwardedHeadersOptions By default, this middleware is already enabled in the application. A Docker Compose configuration file `docker-compose.yml` is available in the `src` directory for running the Docker image `web-eid-asp-dotnet-example` on port 8480 behind a reverse proxy. + +# Code formatting + +The project uses `.editorconfig` for .NET code formatting rules. + +To format the library code, run: + +```bash +dotnet format example/src/WebEid.AspNetCore.Example.sln --no-restore +``` diff --git a/example/src/.editorconfig b/example/src/.editorconfig new file mode 100644 index 0000000..eadfeba --- /dev/null +++ b/example/src/.editorconfig @@ -0,0 +1,453 @@ +# Version: 4.1.1 (Using https://semver.org/) +# Updated: 2022-05-23 +# See https://github.com/RehanSaeed/EditorConfig/releases for release notes. +# See https://github.com/RehanSaeed/EditorConfig for updates to this file. +# See http://EditorConfig.org for more information about .editorconfig files. +# +# Modified by Erkki Arus (erkki@raulwalter.com) on 2023-07-17 - disabled IDE0009 warning. + +########################################## +# Common Settings +########################################## + +# This file is the top-most EditorConfig file +root = true + +# All Files +[*] +charset = utf-8 +indent_style = space +indent_size = 4 +insert_final_newline = true +trim_trailing_whitespace = true + +########################################## +# File Extension Settings +########################################## + +# Visual Studio Solution Files +[*.sln] +indent_style = tab + +# Visual Studio XML Project Files +[*.{csproj,vbproj,vcxproj.filters,proj,projitems,shproj}] +indent_size = 2 + +# XML Configuration Files +[*.{xml,config,props,targets,nuspec,resx,ruleset,vsixmanifest,vsct}] +indent_size = 2 + +# JSON Files +[*.{json,json5,webmanifest}] +indent_size = 2 + +# YAML Files +[*.{yml,yaml}] +indent_size = 2 + +# Markdown Files +[*.{md,mdx}] +trim_trailing_whitespace = false + +# Web Files +[*.{htm,html,js,jsm,ts,tsx,cjs,cts,ctsx,mjs,mts,mtsx,css,sass,scss,less,pcss,svg,vue}] +indent_size = 2 + +# Batch Files +[*.{cmd,bat}] +end_of_line = crlf + +# Bash Files +[*.sh] +end_of_line = lf + +# Makefiles +[Makefile] +indent_style = tab + +########################################## +# Default .NET Code Style Severities +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/configuration-options#scope +########################################## + +[*.{cs,csx,cake,vb,vbx}] +# Default Severity for all .NET Code Style rules below +dotnet_analyzer_diagnostic.severity = warning +dotnet_diagnostic.CA1848.severity = suggestion + +########################################## +# Language Rules +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/language-rules +########################################## + +# .NET Style Rules +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/language-rules#net-style-rules +[*.{cs,csx,cake,vb,vbx}] +# "this." and "Me." qualifiers +dotnet_style_qualification_for_field = false:warning +dotnet_style_qualification_for_property = false:warning +dotnet_style_qualification_for_method = false:warning +dotnet_style_qualification_for_event = false:warning +# Language keywords instead of framework type names for type references +dotnet_style_predefined_type_for_locals_parameters_members = true:warning +dotnet_style_predefined_type_for_member_access = true:warning +# Modifier preferences +dotnet_style_require_accessibility_modifiers = always:warning +csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:warning +visual_basic_preferred_modifier_order = Partial,Default,Private,Protected,Public,Friend,NotOverridable,Overridable,MustOverride,Overloads,Overrides,MustInherit,NotInheritable,Static,Shared,Shadows,ReadOnly,WriteOnly,Dim,Const,WithEvents,Widening,Narrowing,Custom,Async:warning +dotnet_style_readonly_field = true:warning +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:warning +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:warning +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:warning +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:warning +# Expression-level preferences +dotnet_style_object_initializer = true:warning +dotnet_style_collection_initializer = true:warning +dotnet_style_explicit_tuple_names = true:warning +dotnet_style_prefer_inferred_tuple_names = true:warning +dotnet_style_prefer_inferred_anonymous_type_member_names = true:warning +dotnet_style_prefer_auto_properties = true:warning +dotnet_style_prefer_conditional_expression_over_assignment = false:suggestion +dotnet_diagnostic.IDE0045.severity = suggestion +dotnet_style_prefer_conditional_expression_over_return = false:suggestion +dotnet_diagnostic.IDE0046.severity = suggestion +dotnet_style_prefer_compound_assignment = true:warning +dotnet_style_prefer_simplified_interpolation = true:warning +dotnet_style_prefer_simplified_boolean_expressions = true:warning +# Null-checking preferences +dotnet_style_coalesce_expression = true:warning +dotnet_style_null_propagation = true:warning +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:warning +# File header preferences +# file_header_template = \n© PROJECT-AUTHOR\n +# If you use StyleCop, you'll need to disable SA1636: File header copyright text should match. +# dotnet_diagnostic.SA1636.severity = none +# Undocumented +dotnet_style_operator_placement_when_wrapping = end_of_line:warning +csharp_style_prefer_null_check_over_type_check = true:warning + +# C# Style Rules +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/language-rules#c-style-rules +[*.{cs,csx,cake}] +# 'var' preferences +csharp_style_var_for_built_in_types = true:warning +csharp_style_var_when_type_is_apparent = true:warning +csharp_style_var_elsewhere = true:warning +# Expression-bodied members +csharp_style_expression_bodied_methods = true:warning +csharp_style_expression_bodied_constructors = true:warning +csharp_style_expression_bodied_operators = true:warning +csharp_style_expression_bodied_properties = true:warning +csharp_style_expression_bodied_indexers = true:warning +csharp_style_expression_bodied_accessors = true:warning +csharp_style_expression_bodied_lambdas = true:warning +csharp_style_expression_bodied_local_functions = true:warning +# Pattern matching preferences +csharp_style_pattern_matching_over_is_with_cast_check = true:warning +csharp_style_pattern_matching_over_as_with_null_check = true:warning +csharp_style_prefer_switch_expression = true:warning +csharp_style_prefer_pattern_matching = true:warning +csharp_style_prefer_not_pattern = true:warning +# Expression-level preferences +csharp_style_inlined_variable_declaration = true:warning +csharp_prefer_simple_default_expression = true:warning +csharp_style_pattern_local_over_anonymous_function = true:warning +csharp_style_deconstructed_variable_declaration = true:warning +csharp_style_prefer_index_operator = true:warning +csharp_style_prefer_range_operator = true:warning +csharp_style_implicit_object_creation_when_type_is_apparent = true:warning +# "Null" checking preferences +csharp_style_throw_expression = true:warning +csharp_style_conditional_delegate_call = true:warning +# Code block preferences +csharp_prefer_braces = true:warning +csharp_prefer_simple_using_statement = true:suggestion +dotnet_diagnostic.IDE0063.severity = suggestion +# 'using' directive preferences +csharp_using_directive_placement = inside_namespace:warning +# Modifier preferences +csharp_prefer_static_local_function = true:warning + +########################################## +# Unnecessary Code Rules +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/unnecessary-code-rules +########################################## + +# .NET Unnecessary code rules +[*.{cs,csx,cake,vb,vbx}] +dotnet_code_quality_unused_parameters = all:warning +dotnet_remove_unnecessary_suppression_exclusions = none:warning + +# C# Unnecessary code rules +[*.{cs,csx,cake}] +csharp_style_unused_value_expression_statement_preference = discard_variable:suggestion +dotnet_diagnostic.IDE0058.severity = suggestion +csharp_style_unused_value_assignment_preference = discard_variable:suggestion +dotnet_diagnostic.IDE0059.severity = suggestion + +########################################## +# Formatting Rules +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/formatting-rules +########################################## + +# .NET formatting rules +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/formatting-rules#net-formatting-rules +[*.{cs,csx,cake,vb,vbx}] +# Organize using directives +dotnet_sort_system_directives_first = true +dotnet_separate_import_directive_groups = false +# Dotnet namespace options +dotnet_style_namespace_match_folder = true:suggestion +dotnet_diagnostic.IDE0130.severity = suggestion + +# C# formatting rules +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/formatting-rules#c-formatting-rules +[*.{cs,csx,cake}] +# Newline options +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/formatting-rules#new-line-options +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_between_query_expression_clauses = true +# Indentation options +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/formatting-rules#indentation-options +csharp_indent_case_contents = true +csharp_indent_switch_labels = true +csharp_indent_labels = no_change +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents_when_block = false +# Spacing options +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/formatting-rules#spacing-options +csharp_space_after_cast = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_between_parentheses = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_around_binary_operators = before_and_after +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_after_comma = true +csharp_space_before_comma = false +csharp_space_after_dot = false +csharp_space_before_dot = false +csharp_space_after_semicolon_in_for_statement = true +csharp_space_before_semicolon_in_for_statement = false +csharp_space_around_declaration_statements = false +csharp_space_before_open_square_brackets = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_square_brackets = false +# Wrap options +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/formatting-rules#wrap-options +csharp_preserve_single_line_statements = false +csharp_preserve_single_line_blocks = true +# Namespace options +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/formatting-rules#namespace-options +csharp_style_namespace_declarations = block_scoped:warning + +########################################## +# .NET Naming Rules +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/naming-rules +########################################## + +[*.{cs,csx,cake,vb,vbx}] + +########################################## +# Styles +########################################## + +# camel_case_style - Define the camelCase style +dotnet_naming_style.camel_case_style.capitalization = camel_case +# pascal_case_style - Define the PascalCase style +dotnet_naming_style.pascal_case_style.capitalization = pascal_case +# first_upper_style - The first character must start with an upper-case character +dotnet_naming_style.first_upper_style.capitalization = first_word_upper +# prefix_interface_with_i_style - Interfaces must be PascalCase and the first character of an interface must be an 'I' +dotnet_naming_style.prefix_interface_with_i_style.capitalization = pascal_case +dotnet_naming_style.prefix_interface_with_i_style.required_prefix = I +# prefix_type_parameters_with_t_style - Generic Type Parameters must be PascalCase and the first character must be a 'T' +dotnet_naming_style.prefix_type_parameters_with_t_style.capitalization = pascal_case +dotnet_naming_style.prefix_type_parameters_with_t_style.required_prefix = T +# disallowed_style - Anything that has this style applied is marked as disallowed +dotnet_naming_style.disallowed_style.capitalization = pascal_case +dotnet_naming_style.disallowed_style.required_prefix = ____RULE_VIOLATION____ +dotnet_naming_style.disallowed_style.required_suffix = ____RULE_VIOLATION____ +# internal_error_style - This style should never occur... if it does, it indicates a bug in file or in the parser using the file +dotnet_naming_style.internal_error_style.capitalization = pascal_case +dotnet_naming_style.internal_error_style.required_prefix = ____INTERNAL_ERROR____ +dotnet_naming_style.internal_error_style.required_suffix = ____INTERNAL_ERROR____ + +########################################## +# .NET Design Guideline Field Naming Rules +# Naming rules for fields follow the .NET Framework design guidelines +# https://docs.microsoft.com/dotnet/standard/design-guidelines/index +########################################## + +# All public/protected/protected_internal constant fields must be PascalCase +# https://docs.microsoft.com/dotnet/standard/design-guidelines/field +dotnet_naming_symbols.public_protected_constant_fields_group.applicable_accessibilities = public, protected, protected_internal +dotnet_naming_symbols.public_protected_constant_fields_group.required_modifiers = const +dotnet_naming_symbols.public_protected_constant_fields_group.applicable_kinds = field +dotnet_naming_rule.public_protected_constant_fields_must_be_pascal_case_rule.symbols = public_protected_constant_fields_group +dotnet_naming_rule.public_protected_constant_fields_must_be_pascal_case_rule.style = pascal_case_style +dotnet_naming_rule.public_protected_constant_fields_must_be_pascal_case_rule.severity = warning + +# All public/protected/protected_internal static readonly fields must be PascalCase +# https://docs.microsoft.com/dotnet/standard/design-guidelines/field +dotnet_naming_symbols.public_protected_static_readonly_fields_group.applicable_accessibilities = public, protected, protected_internal +dotnet_naming_symbols.public_protected_static_readonly_fields_group.required_modifiers = static, readonly +dotnet_naming_symbols.public_protected_static_readonly_fields_group.applicable_kinds = field +dotnet_naming_rule.public_protected_static_readonly_fields_must_be_pascal_case_rule.symbols = public_protected_static_readonly_fields_group +dotnet_naming_rule.public_protected_static_readonly_fields_must_be_pascal_case_rule.style = pascal_case_style +dotnet_naming_rule.public_protected_static_readonly_fields_must_be_pascal_case_rule.severity = warning + +# No other public/protected/protected_internal fields are allowed +# https://docs.microsoft.com/dotnet/standard/design-guidelines/field +dotnet_naming_symbols.other_public_protected_fields_group.applicable_accessibilities = public, protected, protected_internal +dotnet_naming_symbols.other_public_protected_fields_group.applicable_kinds = field +dotnet_naming_rule.other_public_protected_fields_disallowed_rule.symbols = other_public_protected_fields_group +dotnet_naming_rule.other_public_protected_fields_disallowed_rule.style = disallowed_style +dotnet_naming_rule.other_public_protected_fields_disallowed_rule.severity = error + +########################################## +# StyleCop Field Naming Rules +# Naming rules for fields follow the StyleCop analyzers +# This does not override any rules using disallowed_style above +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers +########################################## + +# All constant fields must be PascalCase +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1303.md +dotnet_naming_symbols.stylecop_constant_fields_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected, private +dotnet_naming_symbols.stylecop_constant_fields_group.required_modifiers = const +dotnet_naming_symbols.stylecop_constant_fields_group.applicable_kinds = field +dotnet_naming_rule.stylecop_constant_fields_must_be_pascal_case_rule.symbols = stylecop_constant_fields_group +dotnet_naming_rule.stylecop_constant_fields_must_be_pascal_case_rule.style = pascal_case_style +dotnet_naming_rule.stylecop_constant_fields_must_be_pascal_case_rule.severity = warning + +# All static readonly fields must be PascalCase +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1311.md +dotnet_naming_symbols.stylecop_static_readonly_fields_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected, private +dotnet_naming_symbols.stylecop_static_readonly_fields_group.required_modifiers = static, readonly +dotnet_naming_symbols.stylecop_static_readonly_fields_group.applicable_kinds = field +dotnet_naming_rule.stylecop_static_readonly_fields_must_be_pascal_case_rule.symbols = stylecop_static_readonly_fields_group +dotnet_naming_rule.stylecop_static_readonly_fields_must_be_pascal_case_rule.style = pascal_case_style +dotnet_naming_rule.stylecop_static_readonly_fields_must_be_pascal_case_rule.severity = warning + +# No non-private instance fields are allowed +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1401.md +dotnet_naming_symbols.stylecop_fields_must_be_private_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected +dotnet_naming_symbols.stylecop_fields_must_be_private_group.applicable_kinds = field +dotnet_naming_rule.stylecop_instance_fields_must_be_private_rule.symbols = stylecop_fields_must_be_private_group +dotnet_naming_rule.stylecop_instance_fields_must_be_private_rule.style = disallowed_style +dotnet_naming_rule.stylecop_instance_fields_must_be_private_rule.severity = error + +# Private fields must be camelCase +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1306.md +dotnet_naming_symbols.stylecop_private_fields_group.applicable_accessibilities = private +dotnet_naming_symbols.stylecop_private_fields_group.applicable_kinds = field +dotnet_naming_rule.stylecop_private_fields_must_be_camel_case_rule.symbols = stylecop_private_fields_group +dotnet_naming_rule.stylecop_private_fields_must_be_camel_case_rule.style = camel_case_style +dotnet_naming_rule.stylecop_private_fields_must_be_camel_case_rule.severity = warning + +# Local variables must be camelCase +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1312.md +dotnet_naming_symbols.stylecop_local_fields_group.applicable_accessibilities = local +dotnet_naming_symbols.stylecop_local_fields_group.applicable_kinds = local +dotnet_naming_rule.stylecop_local_fields_must_be_camel_case_rule.symbols = stylecop_local_fields_group +dotnet_naming_rule.stylecop_local_fields_must_be_camel_case_rule.style = camel_case_style +dotnet_naming_rule.stylecop_local_fields_must_be_camel_case_rule.severity = silent + +# This rule should never fire. However, it's included for at least two purposes: +# First, it helps to understand, reason about, and root-case certain types of issues, such as bugs in .editorconfig parsers. +# Second, it helps to raise immediate awareness if a new field type is added (as occurred recently in C#). +dotnet_naming_symbols.sanity_check_uncovered_field_case_group.applicable_accessibilities = * +dotnet_naming_symbols.sanity_check_uncovered_field_case_group.applicable_kinds = field +dotnet_naming_rule.sanity_check_uncovered_field_case_rule.symbols = sanity_check_uncovered_field_case_group +dotnet_naming_rule.sanity_check_uncovered_field_case_rule.style = internal_error_style +dotnet_naming_rule.sanity_check_uncovered_field_case_rule.severity = error + + +########################################## +# Other Naming Rules +########################################## + +# All of the following must be PascalCase: +# - Namespaces +# https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-namespaces +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1300.md +# - Classes and Enumerations +# https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-classes-structs-and-interfaces +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1300.md +# - Delegates +# https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-classes-structs-and-interfaces#names-of-common-types +# - Constructors, Properties, Events, Methods +# https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-type-members +dotnet_naming_symbols.element_group.applicable_kinds = namespace, class, enum, struct, delegate, event, method, property +dotnet_naming_rule.element_rule.symbols = element_group +dotnet_naming_rule.element_rule.style = pascal_case_style +dotnet_naming_rule.element_rule.severity = warning + +# Interfaces use PascalCase and are prefixed with uppercase 'I' +# https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-classes-structs-and-interfaces +dotnet_naming_symbols.interface_group.applicable_kinds = interface +dotnet_naming_rule.interface_rule.symbols = interface_group +dotnet_naming_rule.interface_rule.style = prefix_interface_with_i_style +dotnet_naming_rule.interface_rule.severity = warning + +# Generics Type Parameters use PascalCase and are prefixed with uppercase 'T' +# https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-classes-structs-and-interfaces +dotnet_naming_symbols.type_parameter_group.applicable_kinds = type_parameter +dotnet_naming_rule.type_parameter_rule.symbols = type_parameter_group +dotnet_naming_rule.type_parameter_rule.style = prefix_type_parameters_with_t_style +dotnet_naming_rule.type_parameter_rule.severity = warning + +# Function parameters use camelCase +# https://docs.microsoft.com/dotnet/standard/design-guidelines/naming-parameters +dotnet_naming_symbols.parameters_group.applicable_kinds = parameter +dotnet_naming_rule.parameters_rule.symbols = parameters_group +dotnet_naming_rule.parameters_rule.style = camel_case_style +dotnet_naming_rule.parameters_rule.severity = warning + +########################################## +# License +########################################## +# The following applies as to the .editorconfig file ONLY, and is +# included below for reference, per the requirements of the license +# corresponding to this .editorconfig file. +# See: https://github.com/RehanSaeed/EditorConfig +# +# MIT License +# +# Copyright (c) 2017-2019 Muhammad Rehan Saeed +# Copyright (c) 2019 Henry Gabryjelski +# +# Permission is hereby granted, free of charge, to any +# person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the +# Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, +# sublicense, and/or sell copies of the Software, and to permit +# persons to whom the Software is furnished to do so, subject +# to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +########################################## diff --git a/example/src/WebEid.AspNetCore.Example/Certificates/CertificateLoader.cs b/example/src/WebEid.AspNetCore.Example/Certificates/CertificateLoader.cs index 186694c..bcc8231 100644 --- a/example/src/WebEid.AspNetCore.Example/Certificates/CertificateLoader.cs +++ b/example/src/WebEid.AspNetCore.Example/Certificates/CertificateLoader.cs @@ -17,7 +17,7 @@ // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -namespace WebEid.AspNetCore.Example.Certificates +namespace WebEid.AspNetCore.Example.Certificates { using System.Collections.Generic; using System.IO; @@ -26,33 +26,19 @@ internal static class CertificateLoader { - public static X509Certificate2[] LoadTrustedCaCertificatesFromDisk(bool isTest = false) - { - return new FileReader(GetCertPath(isTest), "*.cer").ReadFiles() - .Select(file => new X509Certificate2(file)) - .ToArray(); - } + public static X509Certificate2[] LoadTrustedCaCertificatesFromDisk(bool isTest = false) => [.. new FileReader(GetCertPath(isTest), "*.cer").ReadFiles().Select(X509CertificateLoader.LoadCertificate)]; - private static string GetCertPath(bool isTest) - { - return isTest ? "Certificates/Dev" : "Certificates/Prod"; - } + private static string GetCertPath(bool isTest) => isTest ? "Certificates/Dev" : "Certificates/Prod"; } - internal class FileReader + internal sealed class FileReader(string path, string? searchPattern = null) { - private readonly string path; - private readonly string searchPattern; - - public FileReader(string path, string searchPattern = null) - { - this.path = path; - this.searchPattern = searchPattern; - } + private readonly string path = path; + private readonly string? searchPattern = searchPattern; public IEnumerable ReadFiles() { - foreach (var file in Directory.EnumerateFiles(this.path, this.searchPattern)) + foreach (var file in Directory.EnumerateFiles(path, searchPattern ?? "*")) { yield return File.ReadAllBytes(file); } diff --git a/example/src/WebEid.AspNetCore.Example/ClaimsIdentityExtensions.cs b/example/src/WebEid.AspNetCore.Example/ClaimsIdentityExtensions.cs index ddc8983..6029248 100644 --- a/example/src/WebEid.AspNetCore.Example/ClaimsIdentityExtensions.cs +++ b/example/src/WebEid.AspNetCore.Example/ClaimsIdentityExtensions.cs @@ -17,16 +17,13 @@ // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -namespace WebEid.AspNetCore.Example +namespace WebEid.AspNetCore.Example { using System.Linq; using System.Security.Claims; public static class ClaimsIdentityExtensions { - public static string GetIdCode(this ClaimsIdentity identity) - { - return identity.Claims.SingleOrDefault(claim => claim.Type == ClaimTypes.NameIdentifier)?.Value; - } + public static string? GetIdCode(this ClaimsIdentity identity) => identity.Claims.SingleOrDefault(claim => claim.Type == ClaimTypes.NameIdentifier)?.Value; } } diff --git a/example/src/WebEid.AspNetCore.Example/Controllers/Api/AuthController.cs b/example/src/WebEid.AspNetCore.Example/Controllers/Api/AuthController.cs index 3b2e95d..063aae1 100644 --- a/example/src/WebEid.AspNetCore.Example/Controllers/Api/AuthController.cs +++ b/example/src/WebEid.AspNetCore.Example/Controllers/Api/AuthController.cs @@ -19,46 +19,38 @@ namespace WebEid.AspNetCore.Example.Controllers.Api { - using Microsoft.AspNetCore.Authentication; - using Microsoft.AspNetCore.Authentication.Cookies; - using Microsoft.AspNetCore.Mvc; - using Microsoft.AspNetCore.Http; - using Security.Util; - using Security.Validator; + using System; using System.Collections.Generic; using System.Security.Claims; using System.Text.Json; using System.Threading.Tasks; + using Microsoft.AspNetCore.Authentication; + using Microsoft.AspNetCore.Authentication.Cookies; + using Microsoft.AspNetCore.Http; + using Microsoft.AspNetCore.Mvc; using Security.Challenge; + using Security.Util; + using Security.Validator; using WebEid.AspNetCore.Example.Dto; using WebEid.Security.AuthToken; - using System; [Route("[controller]")] [ApiController] - public class AuthController : BaseController + public class AuthController(IAuthTokenValidator authTokenValidator, IChallengeNonceStore challengeNonceStore) : BaseController { - private readonly IAuthTokenValidator authTokenValidator; - private readonly IChallengeNonceStore challengeNonceStore; - - public AuthController(IAuthTokenValidator authTokenValidator, IChallengeNonceStore challengeNonceStore) - { - this.authTokenValidator = authTokenValidator; - this.challengeNonceStore = challengeNonceStore; - } + private readonly IAuthTokenValidator authTokenValidator = authTokenValidator; + private readonly IChallengeNonceStore challengeNonceStore = challengeNonceStore; [HttpPost("login")] public async Task Login([FromBody] AuthenticateRequestDto dto) { - try - { - await SignInUser(dto?.AuthToken); - return Ok(); - } - catch (ArgumentNullException) + if (dto?.AuthToken is null) { return BadRequest(new { error = "Missing auth_token" }); } + + await SignInUser(dto.AuthToken); + return Ok(); } [HttpPost("logout")] @@ -108,13 +100,13 @@ await HttpContext.SignInAsync( CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(identity), new AuthenticationProperties { AllowRefresh = true }); - + // Assign a unique ID within the session to enable the use of a unique temporary container name across successive requests. // A unique temporary container name is required to facilitate simultaneous signing from multiple browsers. SetUniqueIdInSession(); } - private static void AddNewClaimIfCertificateHasData(List claims, string claimType, Func dataGetter) + private static void AddNewClaimIfCertificateHasData(List claims, string claimType, Func dataGetter) { var claimData = dataGetter(); if (!string.IsNullOrEmpty(claimData)) diff --git a/example/src/WebEid.AspNetCore.Example/Controllers/Api/BaseController.cs b/example/src/WebEid.AspNetCore.Example/Controllers/Api/BaseController.cs index 79831e3..de19069 100644 --- a/example/src/WebEid.AspNetCore.Example/Controllers/Api/BaseController.cs +++ b/example/src/WebEid.AspNetCore.Example/Controllers/Api/BaseController.cs @@ -17,7 +17,7 @@ // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -namespace WebEid.AspNetCore.Example.Controllers.Api +namespace WebEid.AspNetCore.Example.Controllers.Api { using System; using Microsoft.AspNetCore.Http; @@ -25,26 +25,15 @@ public abstract class BaseController : ControllerBase { - const string uniqueIdKey = "UniqueId"; + private const string UniqueIdKey = "UniqueId"; - protected void RemoveUserContainerFile() - { - System.IO.File.Delete(GetUserContainerName()); - } + protected void RemoveUserContainerFile() => System.IO.File.Delete(GetUserContainerName()); - protected void SetUniqueIdInSession() - { - HttpContext.Session.SetString(uniqueIdKey, Guid.NewGuid().ToString()); - } + protected void SetUniqueIdInSession() => HttpContext.Session.SetString(UniqueIdKey, Guid.NewGuid().ToString()); - private string GetUniqueIdFromSession() - { - return HttpContext.Session.GetString(uniqueIdKey); - } + private string GetUniqueIdFromSession() => HttpContext.Session.GetString(UniqueIdKey) + ?? throw new InvalidOperationException("Unique ID not found in session."); - protected string GetUserContainerName() - { - return $"container_{GetUniqueIdFromSession()}"; - } + protected string GetUserContainerName() => $"container_{GetUniqueIdFromSession()}"; } } diff --git a/example/src/WebEid.AspNetCore.Example/Controllers/Api/ChallengeController.cs b/example/src/WebEid.AspNetCore.Example/Controllers/Api/ChallengeController.cs index 764a1bf..80d22aa 100644 --- a/example/src/WebEid.AspNetCore.Example/Controllers/Api/ChallengeController.cs +++ b/example/src/WebEid.AspNetCore.Example/Controllers/Api/ChallengeController.cs @@ -17,23 +17,18 @@ // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -namespace WebEid.AspNetCore.Example.Controllers.Api +namespace WebEid.AspNetCore.Example.Controllers.Api { + using System; using Microsoft.AspNetCore.Mvc; using Security.Challenge; - using System; using WebEid.AspNetCore.Example.Dto; [Route("auth")] [ApiController] - public class ChallengeController : BaseController + public class ChallengeController(IChallengeNonceGenerator challengeNonceGenerator) : BaseController { - private readonly IChallengeNonceGenerator challengeNonceGenerator; - - public ChallengeController(IChallengeNonceGenerator challengeNonceGenerator) - { - this.challengeNonceGenerator = challengeNonceGenerator; - } + private readonly IChallengeNonceGenerator challengeNonceGenerator = challengeNonceGenerator; [HttpGet] [Route("challenge")] diff --git a/example/src/WebEid.AspNetCore.Example/Controllers/Api/MobileAuthInitController.cs b/example/src/WebEid.AspNetCore.Example/Controllers/Api/MobileAuthInitController.cs index 0cc54ce..f7fb71e 100644 --- a/example/src/WebEid.AspNetCore.Example/Controllers/Api/MobileAuthInitController.cs +++ b/example/src/WebEid.AspNetCore.Example/Controllers/Api/MobileAuthInitController.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2025-2025 Estonian Information System Authority +// Copyright (c) 2025-2025 Estonian Information System Authority // // Permission is hereby granted, free of charge, to any person obtaining a copy of // this software and associated documentation files (the "Software"), to deal in @@ -21,13 +21,13 @@ namespace WebEid.AspNetCore.Example.Controllers.Api { using System; using System.Text; - using Microsoft.AspNetCore.Mvc; - using Microsoft.Extensions.Options; using System.Text.Json; using System.Text.Json.Serialization; + using Microsoft.AspNetCore.Mvc; + using Microsoft.Extensions.Options; using Options; - using Services; using Security.Challenge; + using Services; [ApiController] [Route("auth/mobile")] diff --git a/example/src/WebEid.AspNetCore.Example/Controllers/Api/SignController.cs b/example/src/WebEid.AspNetCore.Example/Controllers/Api/SignController.cs index 1c61fd6..4c30b1c 100644 --- a/example/src/WebEid.AspNetCore.Example/Controllers/Api/SignController.cs +++ b/example/src/WebEid.AspNetCore.Example/Controllers/Api/SignController.cs @@ -22,34 +22,30 @@ namespace WebEid.AspNetCore.Example.Controllers.Api using System; using System.Security.Claims; using System.Threading.Tasks; + using Dto; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; - using Dto; using Services; using Signing; [Route("[controller]")] [ApiController] [Authorize(Policy = "LoggedInOnly")] - public class SignController : BaseController + public class SignController(SigningService signingService, MobileSigningService mobileSigningService, ILogger logger) : BaseController { private const string SignedFile = "example-for-signing.asice"; - private readonly SigningService signingService; - private readonly MobileSigningService mobileSigningService; - private readonly ILogger logger; - - public SignController(SigningService signingService, MobileSigningService mobileSigningService, ILogger logger) - { - this.signingService = signingService; - this.mobileSigningService = mobileSigningService; - this.logger = logger; - } + private readonly SigningService signingService = signingService; + private readonly MobileSigningService mobileSigningService = mobileSigningService; + private readonly ILogger logger = logger; [HttpPost("prepare")] public DigestDto Prepare([FromBody] CertificateDto data) { - return signingService.PrepareContainer(data, (ClaimsIdentity)HttpContext.User.Identity, GetUserContainerName()); + var identity = HttpContext.User.Identity as ClaimsIdentity + ?? throw new InvalidOperationException("User identity is missing or invalid."); + + return signingService.PrepareContainer(data, identity, GetUserContainerName()); } [HttpPost("sign")] @@ -62,7 +58,9 @@ public FileDto Sign([FromBody] SignatureDto data) [HttpPost("mobile/init")] public MobileSigningService.MobileInitRequest MobileInit() { - var identity = (ClaimsIdentity)HttpContext.User.Identity; + var identity = HttpContext.User.Identity as ClaimsIdentity + ?? throw new InvalidOperationException("User identity is missing or invalid."); + var container = GetUserContainerName(); return mobileSigningService.InitCertificateOrSigningRequest(identity, container); } @@ -70,7 +68,9 @@ public MobileSigningService.MobileInitRequest MobileInit() [HttpPost("mobile/certificate")] public MobileSigningService.MobileInitRequest CertificatePost([FromBody] CertificateDto certificateDto) { - var identity = (ClaimsIdentity)HttpContext.User.Identity; + var identity = HttpContext.User.Identity as ClaimsIdentity + ?? throw new InvalidOperationException("User identity is missing or invalid."); + var containerName = GetUserContainerName(); return mobileSigningService.InitSigningRequest( diff --git a/example/src/WebEid.AspNetCore.Example/Controllers/WelcomeController.cs b/example/src/WebEid.AspNetCore.Example/Controllers/WelcomeController.cs index c9743e4..7790730 100644 --- a/example/src/WebEid.AspNetCore.Example/Controllers/WelcomeController.cs +++ b/example/src/WebEid.AspNetCore.Example/Controllers/WelcomeController.cs @@ -17,16 +17,12 @@ // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -namespace WebEid.AspNetCore.Example.Controllers +namespace WebEid.AspNetCore.Example.Controllers { - using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; public class WelcomeController : Controller { - public IActionResult Index() - { - return View(); - } + public IActionResult Index() => View(); } } diff --git a/example/src/WebEid.AspNetCore.Example/Dto/AuthenticateRequestDto.cs b/example/src/WebEid.AspNetCore.Example/Dto/AuthenticateRequestDto.cs index a5a015b..743bfac 100644 --- a/example/src/WebEid.AspNetCore.Example/Dto/AuthenticateRequestDto.cs +++ b/example/src/WebEid.AspNetCore.Example/Dto/AuthenticateRequestDto.cs @@ -17,7 +17,7 @@ // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -namespace WebEid.AspNetCore.Example.Dto +namespace WebEid.AspNetCore.Example.Dto { using System.Text.Json.Serialization; using Security.AuthToken; @@ -25,6 +25,6 @@ public class AuthenticateRequestDto { [JsonPropertyName("auth_token")] - public WebEidAuthToken AuthToken { get; set; } + public required WebEidAuthToken AuthToken { get; set; } } } diff --git a/example/src/WebEid.AspNetCore.Example/Dto/CertificateDto.cs b/example/src/WebEid.AspNetCore.Example/Dto/CertificateDto.cs index 5fabd62..404bcad 100644 --- a/example/src/WebEid.AspNetCore.Example/Dto/CertificateDto.cs +++ b/example/src/WebEid.AspNetCore.Example/Dto/CertificateDto.cs @@ -17,7 +17,7 @@ // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -namespace WebEid.AspNetCore.Example.Dto +namespace WebEid.AspNetCore.Example.Dto { using System.Collections.Generic; using System.Text.Json.Serialization; @@ -25,7 +25,7 @@ public class CertificateDto { [JsonPropertyName("certificate")] - public string CertificateBase64String { get; set; } - public IList SupportedSignatureAlgorithms { get; set; } + public required string CertificateBase64String { get; set; } + public required IList SupportedSignatureAlgorithms { get; set; } } } diff --git a/example/src/WebEid.AspNetCore.Example/Dto/ChallengeDto.cs b/example/src/WebEid.AspNetCore.Example/Dto/ChallengeDto.cs index 63aa24c..2d0f826 100644 --- a/example/src/WebEid.AspNetCore.Example/Dto/ChallengeDto.cs +++ b/example/src/WebEid.AspNetCore.Example/Dto/ChallengeDto.cs @@ -17,10 +17,10 @@ // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -namespace WebEid.AspNetCore.Example.Dto +namespace WebEid.AspNetCore.Example.Dto { public class ChallengeDto { - public string Nonce { get; set; } + public required string Nonce { get; set; } } } diff --git a/example/src/WebEid.AspNetCore.Example/Dto/DigestDto.cs b/example/src/WebEid.AspNetCore.Example/Dto/DigestDto.cs index 08b1d87..3e94180 100644 --- a/example/src/WebEid.AspNetCore.Example/Dto/DigestDto.cs +++ b/example/src/WebEid.AspNetCore.Example/Dto/DigestDto.cs @@ -17,11 +17,11 @@ // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -namespace WebEid.AspNetCore.Example.Dto +namespace WebEid.AspNetCore.Example.Dto { public class DigestDto { - public string Hash { get; set; } - public string HashFunction { get; set; } + public required string Hash { get; set; } + public required string HashFunction { get; set; } } -} \ No newline at end of file +} diff --git a/example/src/WebEid.AspNetCore.Example/Dto/FileDto.cs b/example/src/WebEid.AspNetCore.Example/Dto/FileDto.cs index b29cd14..2f5b665 100644 --- a/example/src/WebEid.AspNetCore.Example/Dto/FileDto.cs +++ b/example/src/WebEid.AspNetCore.Example/Dto/FileDto.cs @@ -17,15 +17,10 @@ // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -namespace WebEid.AspNetCore.Example.Dto +namespace WebEid.AspNetCore.Example.Dto { - public class FileDto + public class FileDto(string name) { - public FileDto(string name) - { - this.Name = name; - } - - public string Name { get; } + public string Name { get; } = name; } -} \ No newline at end of file +} diff --git a/example/src/WebEid.AspNetCore.Example/Dto/SignatureAlgorithmDto.cs b/example/src/WebEid.AspNetCore.Example/Dto/SignatureAlgorithmDto.cs index 3ac7527..3deb05b 100644 --- a/example/src/WebEid.AspNetCore.Example/Dto/SignatureAlgorithmDto.cs +++ b/example/src/WebEid.AspNetCore.Example/Dto/SignatureAlgorithmDto.cs @@ -17,12 +17,12 @@ // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -namespace WebEid.AspNetCore.Example.Dto +namespace WebEid.AspNetCore.Example.Dto { public class SignatureAlgorithmDto { - public string CryptoAlgorithm { get; set; } - public string HashFunction { get; set; } - public string PaddingScheme { get; set; } + public required string CryptoAlgorithm { get; set; } + public required string HashFunction { get; set; } + public required string PaddingScheme { get; set; } } -} \ No newline at end of file +} diff --git a/example/src/WebEid.AspNetCore.Example/Dto/SignatureDto.cs b/example/src/WebEid.AspNetCore.Example/Dto/SignatureDto.cs index ff5e9d8..3714b97 100644 --- a/example/src/WebEid.AspNetCore.Example/Dto/SignatureDto.cs +++ b/example/src/WebEid.AspNetCore.Example/Dto/SignatureDto.cs @@ -17,15 +17,15 @@ // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -namespace WebEid.AspNetCore.Example.Dto +namespace WebEid.AspNetCore.Example.Dto { public class SignatureDto { - public SignatureAlgorithmDto SignatureAlgorithm { get; set; } + public SignatureAlgorithmDto? SignatureAlgorithm { get; set; } /// /// Base64 signature /// - public string Signature { get; set; } + public required string Signature { get; set; } } -} \ No newline at end of file +} diff --git a/example/src/WebEid.AspNetCore.Example/LoggedInAuthorizationHandler.cs b/example/src/WebEid.AspNetCore.Example/LoggedInAuthorizationHandler.cs index dc01f3e..47cb013 100644 --- a/example/src/WebEid.AspNetCore.Example/LoggedInAuthorizationHandler.cs +++ b/example/src/WebEid.AspNetCore.Example/LoggedInAuthorizationHandler.cs @@ -17,17 +17,17 @@ // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -namespace WebEid.AspNetCore.Example +namespace WebEid.AspNetCore.Example { - using Microsoft.AspNetCore.Authorization; using System.Threading.Tasks; + using Microsoft.AspNetCore.Authorization; public class LoggedInAuthorizationHandler : AuthorizationHandler { protected override Task HandleRequirementAsync( AuthorizationHandlerContext context, LoggedInRequirement requirement) { - if (context.User.Identity.IsAuthenticated) + if (context.User.Identity?.IsAuthenticated == true) { context.Succeed(requirement); } @@ -36,5 +36,4 @@ protected override Task HandleRequirementAsync( } public class LoggedInRequirement : IAuthorizationRequirement { } - } diff --git a/example/src/WebEid.AspNetCore.Example/Options/WebEidMobileOptions.cs b/example/src/WebEid.AspNetCore.Example/Options/WebEidMobileOptions.cs index a7f2310..f2f75cb 100644 --- a/example/src/WebEid.AspNetCore.Example/Options/WebEidMobileOptions.cs +++ b/example/src/WebEid.AspNetCore.Example/Options/WebEidMobileOptions.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2025-2025 Estonian Information System Authority +// Copyright (c) 2025-2025 Estonian Information System Authority // // Permission is hereby granted, free of charge, to any person obtaining a copy of // this software and associated documentation files (the "Software"), to deal in @@ -29,4 +29,4 @@ public class WebEidMobileOptions public bool RequestSigningCert { get; set; } } -} \ No newline at end of file +} diff --git a/example/src/WebEid.AspNetCore.Example/Pages/Index.cshtml b/example/src/WebEid.AspNetCore.Example/Pages/Index.cshtml index 62c809e..5823f46 100644 --- a/example/src/WebEid.AspNetCore.Example/Pages/Index.cshtml +++ b/example/src/WebEid.AspNetCore.Example/Pages/Index.cshtml @@ -2,19 +2,20 @@ @inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Xsrf + - - + + @{ - var tokens = Xsrf.GetAndStoreTokens(HttpContext); + var tokens = Xsrf.GetAndStoreTokens(HttpContext); } - - + + Web eID: electronic ID smart cards on the Web - - + + @@ -22,492 +23,523 @@ + -
-
-
-

Web eID: electronic ID smart cards on the Web

-

- The Web eID project enables usage of European Union electronic identity (eID) smart cards for - secure authentication and digital signing of documents on the web using public-key cryptography. -

-

- Estonian, Finnish, Latvian, Lithuanian, Belgian and Croatian eID cards are supported in the first phase, - but only Estonian eID card support is currently enabled in the test application below. -

-

- Please get in touch by email at help@ria.ee in case you need support with adding Web eID to your project - or want to add support for a new eID card to Web eID. -

-

The privacy policy of the test service is available here. -

- -
-
Web eID plugin status
-
- - Web eID plugin installed -
-
- - Accessed with a mobile device -
- -
-

Table of contents

- - -
-

Usage with Web eID plugin

-

The recommended way of installing Web eID is by installing - the latest Open-EID ID-software package. - In case you do not need or want to install the Open-EID package, install the latest Web eID packages in - Firefox, Chrome, Edge or Safari according to the following instructions: -

-
    -
  1. - Download and run the Web eID native app and browser extension installer: -
      -
    • on Ubuntu Linux, for Firefox and Chrome, download and execute the
      - install-web-eid.sh - script from the console with
      - wget -O - https:///scripts/install-web-eid.sh - | bash
      - Note: as of the 2.5 version, Web eID supports Firefox installed via Snap. -
    • -
    • on macOS 13 or later, for Firefox and Chrome from - here, -
    • -
    • on macOS 13 or later, for Safari, install the extension from - App Store, -
    • -
    • on Windows 10, Windows 11, Windows Server 2019, Windows Server 2022, Windows Server - 2025 for Firefox, Chrome and Edge from - here. -
    • -
    -
  2. -
  3. - The installer will install the browser extension for all supported browsers automatically. - The extension must be manually enabled from either the extension installation pop-up that appears in - the browser or from the browser extensions management page and may need browser restart under - certain circumstances. -
  4. -
-

Testing:

-
    -
  1. - Attach a smart card reader to the computer and insert the eID card into the reader. -
  2. -
  3. Click Authenticate below.
  4. -
- - -

- -

- -
-
-
-

- - -

-
-
-

The uninstaller will remove the browser extension from all supported browsers - automatically.

- -
Ubuntu Linux
-

Uninstall the Web eID software either using the Ubuntu Software Center or from the - console with
- sudo apt purge web-eid

- -
macOS
-

To uninstall the Web eID software, do the following:

-
    -
  1. download the Web eID native app and browser extension installer as instructed - above, -
  2. -
  3. open the downloaded file,
  4. -
  5. open Terminal,
  6. -
  7. drag and drop uninstall.sh from the downloaded file to the - Terminal window, -
  8. -
  9. press Enter and Y,
  10. -
  11. enter your password.
  12. -
- -
Windows
-

Uninstall the Web eID software using Add or remove programs.

-
-
-
-
-

- - -

-
-
-
    -
  • - To debug the extension, open the extension page and select - Inspect to open browser developer tools in extension mode. You can examine - extension - logs in the Console tab, put breakpoints in extension code in the - Debugger tab - and inspect extension network communication in the Network tab. -
  • -
  • - To enable logging in the extension companion native app, -
      -
    • in Linux, run the following command in the console:
      - echo 'logging=true' > ~/.config/RIA/web-eid.conf -
    • -
    • in macOS, run the following command in the console:
      - defaults write \
      -   "$HOME/Library/Containers/eu.web-eid.web-eid/Data/Library/Preferences/eu.web-eid.web-eid.plist" - \
      -   logging true
      - defaults write - "$HOME/Library/Containers/eu.web-eid.web-eid-safari/Data/Library/Preferences/eu.web-eid.web-eid-safari.plist" - \
      -   logging true
      -
    • -
    • in Windows, add the following registry key:
      - [HKEY_CURRENT_USER\SOFTWARE\RIA\web-eid]
      - "logging"="true" -
    • -
    -
  • -
  • - The native app logs are stored in -
      -
    • ~/.local/share/RIA/web-eid/web-eid.log in Linux
    • -
    • ~/Library/Containers/eu.web-eid.web-eid/Data/Library/Application\ - Support/RIA/web-eid/web-eid.log in macOS -
    • -
    • ~/Library/Containers/eu.web-eid.web-eid-safari/Data/Library/Application\ - Support/RIA/web-eid-safari/web-eid-safari.log of Safari in macOS -
    • -
    • - C:/Users/<USER>/AppData/Local/RIA/web-eid/web-eid.log in - Windows. -
    • -
    -
  • -
  • - You can verify if debugging works by executing the native application - web-eid - manually, - there will be an informative message in the logs. -
  • -
-
-
-
-
-

- -

-
-
-

- Technical overview of the solution is available in the project - system architecture - document. - Overview of authentication token validation implementation in the back end is available - in the - web-eid-authtoken-validation-java Java library - README. -

-

- Security analysis of the solution is available - in - this - document. -

-
-
+
+
+
+

Web eID: electronic ID smart cards on the Web

+

+ The Web eID project enables usage of European Union electronic identity (eID) smart cards for + secure authentication and digital signing of documents on the web using public-key cryptography. +

+

+ Estonian, Finnish, Latvian, Lithuanian, Belgian and Croatian eID cards are supported in the first + phase, + but only Estonian eID card support is currently enabled in the test application below. +

+

+ Please get in touch by email at help@ria.ee in case you need support with adding Web eID to your + project + or want to add support for a new eID card to Web eID. +

+

The privacy policy of the test service is available here. +

+ +
+
Web eID plugin status
+
+ + Web eID plugin installed
-
-

- -

-
-
-

- Currently the Web eID back-end libraries are available for Java, .NET and PHP web - applications. -

-

- To implement authentication and digital signing with Web eID in a Java, .NET or PHP web - application, - you need to -

-
    -
  • use the web-eid.js JavaScript library in the front end of the web application - according to the instructions - here, -
  • -
  • for authentication -
      -
    • in Java use the web-eid-authtoken-validation-java library in - the back end of the web application according to the instructions - here, -
    • -
    • in .NET/C# use the web-eid-authtoken-validation-dotnet library - according to the - instructions - here -
    • -
    • in PHP use the web-eid-authtoken-validation-php library according to - the - instructions - here -
    • -
    -
  • -
  • for digital signing -
      -
    • in Java use the digidoc4j library in the back end of the web - application according to the instructions - here, -
    • -
    • in .NET/C# use the libdigidocpp library in the back end of the web - application according to the instructions - here. -
    • -
    -
  • -
-

- The full source code of an example Spring Boot web application that uses Web eID for - authentication - and digital signing is available - here. - The .NET/C# version of the example is available - here. - The PHP version of the example is available - here. -

-
-
+
+ + Accessed with a mobile device
-
- -
-

Usage without Web eID plugin

-

The Web eID solution can also be used without installing the Web eID native app and browser extension. - This includes devices like mobile phones, tablets, and some Chromebooks, where the Web eID plugin cannot - currently be installed. -

-

- -

- -
-
-
-

- -

-
-
-

- Technical overview of the solution is available in the project - system - architecture document. - Overview of authentication token validation implementation in the back end is available - in the - web-eid-authtoken-validation-java Java library - README. -

+
+

Table of contents

+ + +
+

Usage with Web eID plugin

+

The recommended way of installing Web eID is by installing + the latest Open-EID ID-software + package. + In case you do not need or want to install the Open-EID package, install the latest Web eID packages + in + Firefox, Chrome, Edge or Safari according to the following instructions: +

+
    +
  1. + Download and run the Web eID native app and browser extension installer: +
      +
    • on Ubuntu Linux, for Firefox and Chrome, download and execute the
      + install-web-eid.sh + script from the console with
      + wget -O - https:///scripts/install-web-eid.sh + | bash
      + Note: as of the 2.5 version, Web eID supports Firefox installed via Snap. +
    • +
    • on macOS 13 or later, for Firefox and Chrome from + here, +
    • +
    • on macOS 13 or later, for Safari, install the extension from + App Store, +
    • +
    • on Windows 10, Windows 11, Windows Server 2019, Windows Server 2022, Windows + Server + 2025 for Firefox, Chrome and Edge from + here. +
    • +
    +
  2. +
  3. + The installer will install the browser extension for all supported browsers automatically. + The extension must be manually enabled from either the extension installation pop-up that + appears in + the browser or from the browser extensions management page and may need browser restart under + certain circumstances. +
  4. +
+

Testing:

+
    +
  1. + Attach a smart card reader to the computer and insert the eID card into the reader. +
  2. +
  3. Click Authenticate below.
  4. +
+ + -
-
-
-
-
-
- -
- EU fund flags -
- - - - - - +

+ +

+ +
+
+
+

+ + +

+
+
+

The uninstaller will remove the browser extension from all supported browsers + automatically.

+ +
Ubuntu Linux
+

Uninstall the Web eID software either using the Ubuntu Software Center or from the + console with
+ sudo apt purge web-eid +

+ +
macOS
+

To uninstall the Web eID software, do the following:

+
    +
  1. download the Web eID native app and browser extension installer as instructed + above, +
  2. +
  3. open the downloaded file,
  4. +
  5. open Terminal,
  6. +
  7. drag and drop uninstall.sh from the downloaded file to the + Terminal window, +
  8. +
  9. press Enter and Y,
  10. +
  11. enter your password.
  12. +
+ +
Windows
+

Uninstall the Web eID software using Add or remove programs.

+
+
+
+
+

+ + +

+
+
+
    +
  • + To debug the extension, open the extension page and select + Inspect to open browser developer tools in extension mode. You can + examine + extension + logs in the Console tab, put breakpoints in extension code in the + Debugger tab + and inspect extension network communication in the Network tab. +
  • +
  • + To enable logging in the extension companion native app, +
      +
    • in Linux, run the following command in the console:
      + echo 'logging=true' > ~/.config/RIA/web-eid.conf +
    • +
    • in macOS, run the following command in the console:
      + defaults write \
      +   "$HOME/Library/Containers/eu.web-eid.web-eid/Data/Library/Preferences/eu.web-eid.web-eid.plist" + \
      +   logging true
      + defaults write + "$HOME/Library/Containers/eu.web-eid.web-eid-safari/Data/Library/Preferences/eu.web-eid.web-eid-safari.plist" + \
      +   logging true
      +
    • +
    • in Windows, add the following registry key:
      + [HKEY_CURRENT_USER\SOFTWARE\RIA\web-eid]
      + "logging"="true" +
    • +
    +
  • +
  • + The native app logs are stored in +
      +
    • ~/.local/share/RIA/web-eid/web-eid.log in Linux
    • +
    • ~/Library/Containers/eu.web-eid.web-eid/Data/Library/Application\ + Support/RIA/web-eid/web-eid.log in macOS +
    • +
    • ~/Library/Containers/eu.web-eid.web-eid-safari/Data/Library/Application\ + Support/RIA/web-eid-safari/web-eid-safari.log + of Safari in macOS +
    • +
    • + C:/Users/<USER>/AppData/Local/RIA/web-eid/web-eid.log + in + Windows. +
    • +
    +
  • +
  • + You can verify if debugging works by executing the native application + web-eid + manually, + there will be an informative message in the logs. +
  • +
+
+
+
+
+

+ +

+
+
+

+ Technical overview of the solution is available in the project + system + architecture + document. + Overview of authentication token validation implementation in the back end is + available + in the + web-eid-authtoken-validation-java Java library + README. +

+

+ Security analysis of the solution is available + in + this + document. +

+
+
+
+
+

+ +

+
+
+

+ Currently the Web eID back-end libraries are available for Java, .NET and PHP web + applications. +

+

+ To implement authentication and digital signing with Web eID in a Java, .NET or PHP + web + application, + you need to +

+
    +
  • use the web-eid.js JavaScript library in the front end of the web + application + according to the instructions + here, +
  • +
  • for authentication +
      +
    • in Java use the web-eid-authtoken-validation-java library in + the back end of the web application according to the instructions + here, +
    • +
    • in .NET/C# use the web-eid-authtoken-validation-dotnet library + according to the + instructions + here +
    • +
    • in PHP use the web-eid-authtoken-validation-php library according + to + the + instructions + here +
    • +
    +
  • +
  • for digital signing +
      +
    • in Java use the digidoc4j library in the back end of the web + application according to the instructions + here, +
    • +
    • in .NET/C# use the libdigidocpp library in the back end of the + web + application according to the instructions + here. +
    • +
    +
  • +
+

+ The full source code of an example Spring Boot web application that uses Web eID for + authentication + and digital signing is available + here. + The .NET/C# version of the example is available + here. + The PHP version of the example is available + here. +

+
+
+
+
+ +
+

Usage without Web eID plugin

+

The Web eID solution can also be used without installing the Web eID native app and browser + extension. + This includes devices like mobile phones, tablets, and some Chromebooks, where the Web eID plugin + cannot + currently be installed. +

+

+ +

+ +
+
+
+

+ +

+
+
+

+ Technical overview of the solution is available in the project + system + architecture document. + Overview of authentication token validation implementation in the back end is + available + in the + web-eid-authtoken-validation-java Java library + README. +

+ +
+
+
+
+
+
+
+ +
+ EU fund flags +
+ + + + + + + diff --git a/example/src/WebEid.AspNetCore.Example/Pages/WebEidCallback.cshtml b/example/src/WebEid.AspNetCore.Example/Pages/WebEidCallback.cshtml index 8d58893..0326388 100644 --- a/example/src/WebEid.AspNetCore.Example/Pages/WebEidCallback.cshtml +++ b/example/src/WebEid.AspNetCore.Example/Pages/WebEidCallback.cshtml @@ -8,94 +8,97 @@ + - - - + + + Completing signing… - - + + -
-

Completing signing…

-
-
- +
+

Completing signing…

+
+
+ - + - + window.location.replace( + "/welcome?signed=" + encodeURIComponent(result.name) + ); + })().catch((error) => { + console.error(error); + showErrorMessage(error, "Signing failed"); + const spinner = document.querySelector("#spinner-message"); + spinner.style.display = "none"; + const actions = document.getElementById("error-actions"); + const goBackButton = document.getElementById("back-button"); + actions.style.display = "block"; + goBackButton.addEventListener("click", () => { + window.location.replace("/welcome?error=webeid-callback"); + }); + }); + + diff --git a/example/src/WebEid.AspNetCore.Example/Pages/WebEidCallback.cshtml.cs b/example/src/WebEid.AspNetCore.Example/Pages/WebEidCallback.cshtml.cs index 0959eac..9f21a9c 100644 --- a/example/src/WebEid.AspNetCore.Example/Pages/WebEidCallback.cshtml.cs +++ b/example/src/WebEid.AspNetCore.Example/Pages/WebEidCallback.cshtml.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2025-2025 Estonian Information System Authority +// Copyright (c) 2025-2025 Estonian Information System Authority // // Permission is hereby granted, free of charge, to any person obtaining a copy of // this software and associated documentation files (the "Software"), to deal in @@ -20,7 +20,7 @@ namespace WebEid.AspNetCore.Example.Pages { using Microsoft.AspNetCore.Mvc.RazorPages; - + public class WebEidCallbackModel : PageModel { public void OnGet() @@ -28,4 +28,4 @@ public void OnGet() /* Intentionally left empty. This Razor Page only renders HTML and JavaScript for WebEID callback. */ } } -} \ No newline at end of file +} diff --git a/example/src/WebEid.AspNetCore.Example/Pages/WebEidLogin.cshtml b/example/src/WebEid.AspNetCore.Example/Pages/WebEidLogin.cshtml index de9a73c..56265fe 100644 --- a/example/src/WebEid.AspNetCore.Example/Pages/WebEidLogin.cshtml +++ b/example/src/WebEid.AspNetCore.Example/Pages/WebEidLogin.cshtml @@ -3,11 +3,12 @@ @inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Xsrf @{ -var tokens = Xsrf.GetAndStoreTokens(HttpContext); + var tokens = Xsrf.GetAndStoreTokens(HttpContext); } + @@ -19,6 +20,7 @@ var tokens = Xsrf.GetAndStoreTokens(HttpContext); +

Signing you in…

@@ -34,41 +36,42 @@ var tokens = Xsrf.GetAndStoreTokens(HttpContext);

- + document.getElementById("back-button").addEventListener("click", () => { + window.location.replace("/"); + }); + }); + + diff --git a/example/src/WebEid.AspNetCore.Example/Pages/WebEidLogin.cshtml.cs b/example/src/WebEid.AspNetCore.Example/Pages/WebEidLogin.cshtml.cs index af4ff24..361ceb7 100644 --- a/example/src/WebEid.AspNetCore.Example/Pages/WebEidLogin.cshtml.cs +++ b/example/src/WebEid.AspNetCore.Example/Pages/WebEidLogin.cshtml.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2025-2025 Estonian Information System Authority +// Copyright (c) 2025-2025 Estonian Information System Authority // // Permission is hereby granted, free of charge, to any person obtaining a copy of // this software and associated documentation files (the "Software"), to deal in diff --git a/example/src/WebEid.AspNetCore.Example/Pages/Welcome.cshtml b/example/src/WebEid.AspNetCore.Example/Pages/Welcome.cshtml index 65e1fae..2dbfba1 100644 --- a/example/src/WebEid.AspNetCore.Example/Pages/Welcome.cshtml +++ b/example/src/WebEid.AspNetCore.Example/Pages/Welcome.cshtml @@ -3,172 +3,175 @@ @inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Xsrf @{ -var tokens = Xsrf.GetAndStoreTokens(HttpContext); + var tokens = Xsrf.GetAndStoreTokens(HttpContext); } + - - + + - - + + Welcome! - - + + -
-
-
- -

- -

- -
-

Digital signing

-

Welcome, @Model.PrincipalName!

-
- -
+
+
+
-
-

- To test digital signing, click Sign document: +

+

- -
-
- -

- - -

-
-
- - + + + diff --git a/example/src/WebEid.AspNetCore.Example/Pages/Welcome.cshtml.cs b/example/src/WebEid.AspNetCore.Example/Pages/Welcome.cshtml.cs index 7e19ee1..9045905 100644 --- a/example/src/WebEid.AspNetCore.Example/Pages/Welcome.cshtml.cs +++ b/example/src/WebEid.AspNetCore.Example/Pages/Welcome.cshtml.cs @@ -25,10 +25,15 @@ namespace WebEid.AspNetCore.Example.Pages public class WelcomeModel : PageModel { - public string PrincipalName => GetPrincipalName((ClaimsIdentity)this.User.Identity); + public string PrincipalName => GetPrincipalName(User.Identity as ClaimsIdentity); - private static string GetPrincipalName(ClaimsIdentity identity) + private static string GetPrincipalName(ClaimsIdentity? identity) { + if (identity is null) + { + return string.Empty; + } + var givenName = identity.Claims.Where(claim => claim.Type == ClaimTypes.GivenName) .Select(claim => claim.Value) .SingleOrDefault(); @@ -37,15 +42,13 @@ private static string GetPrincipalName(ClaimsIdentity identity) .SingleOrDefault(); if (!string.IsNullOrEmpty(givenName) && !string.IsNullOrEmpty(surname)) - { + { return $"{givenName} {surname}"; } - else - { - return identity.Claims.Where(claim => claim.Type == ClaimTypes.Name) - .Select(claim => claim.Value) - .SingleOrDefault(); - } + + return identity.Claims.Where(claim => claim.Type == ClaimTypes.Name) + .Select(claim => claim.Value) + .SingleOrDefault() ?? string.Empty; } } -} \ No newline at end of file +} diff --git a/example/src/WebEid.AspNetCore.Example/Program.cs b/example/src/WebEid.AspNetCore.Example/Program.cs index af5995f..5f3556a 100644 --- a/example/src/WebEid.AspNetCore.Example/Program.cs +++ b/example/src/WebEid.AspNetCore.Example/Program.cs @@ -24,16 +24,10 @@ namespace WebEid.AspNetCore.Example public static class Program { - public static void Main(string[] args) - { - CreateHostBuilder(args).Build().Run(); - } + public static void Main(string[] args) => CreateHostBuilder(args).Build().Run(); public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - }); + .ConfigureWebHostDefaults(webBuilder => webBuilder.UseStartup()); } -} \ No newline at end of file +} diff --git a/example/src/WebEid.AspNetCore.Example/Services/MobileRequestUriBuilder.cs b/example/src/WebEid.AspNetCore.Example/Services/MobileRequestUriBuilder.cs index 42b4d55..662b112 100644 --- a/example/src/WebEid.AspNetCore.Example/Services/MobileRequestUriBuilder.cs +++ b/example/src/WebEid.AspNetCore.Example/Services/MobileRequestUriBuilder.cs @@ -23,20 +23,12 @@ namespace WebEid.AspNetCore.Example.Services using Microsoft.Extensions.Options; using Options; - public class MobileRequestUriBuilder + public class MobileRequestUriBuilder(IOptions options) { - private readonly string baseUri; + private readonly string baseUri = options.Value.BaseRequestUri; - public MobileRequestUriBuilder(IOptions options) - { - baseUri = options.Value.BaseRequestUri; - } - - public string Build(string path, string encodedPayload) - { - return baseUri.StartsWith("http", StringComparison.OrdinalIgnoreCase) + public string Build(string path, string encodedPayload) => baseUri.StartsWith("http", StringComparison.OrdinalIgnoreCase) ? $"{baseUri.TrimEnd('/')}/{path}#{encodedPayload}" : $"{baseUri}{path}#{encodedPayload}"; - } } } diff --git a/example/src/WebEid.AspNetCore.Example/SessionBackedChallengeNonceStore.cs b/example/src/WebEid.AspNetCore.Example/SessionBackedChallengeNonceStore.cs index e269f36..51542d8 100644 --- a/example/src/WebEid.AspNetCore.Example/SessionBackedChallengeNonceStore.cs +++ b/example/src/WebEid.AspNetCore.Example/SessionBackedChallengeNonceStore.cs @@ -17,37 +17,41 @@ // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -using Microsoft.AspNetCore.Http; -using System.Text.Json; -using WebEid.Security.Challenge; - namespace WebEid.AspNetCore.Example { - public class SessionBackedChallengeNonceStore : IChallengeNonceStore + using System; + using System.Text.Json; + using Microsoft.AspNetCore.Http; + using WebEid.Security.Challenge; + + public class SessionBackedChallengeNonceStore(IHttpContextAccessor httpContextAccessor) : IChallengeNonceStore { private const string ChallengeNonceKey = "challenge-nonce"; - private readonly IHttpContextAccessor httpContextAccessor; - - public SessionBackedChallengeNonceStore(IHttpContextAccessor httpContextAccessor) - { - this.httpContextAccessor = httpContextAccessor; - } + private readonly IHttpContextAccessor httpContextAccessor = httpContextAccessor; public void Put(ChallengeNonce challengeNonce) { - this.httpContextAccessor.HttpContext.Session.SetString(ChallengeNonceKey, JsonSerializer.Serialize(challengeNonce)); + var httpContext = httpContextAccessor.HttpContext + ?? throw new InvalidOperationException("HttpContext is not available."); + + httpContext.Session.SetString(ChallengeNonceKey, JsonSerializer.Serialize(challengeNonce)); } - public ChallengeNonce GetAndRemoveImpl() + public ChallengeNonce? GetAndRemoveImpl() { - var httpContext = this.httpContextAccessor.HttpContext; - var challenceNonceJson = httpContext.Session.GetString(ChallengeNonceKey); - if (!string.IsNullOrWhiteSpace(challenceNonceJson)) + var httpContext = httpContextAccessor.HttpContext; + if (httpContext is null) + { + return null; + } + + var challengeNonceJson = httpContext.Session.GetString(ChallengeNonceKey); + if (!string.IsNullOrWhiteSpace(challengeNonceJson)) { httpContext.Session.Remove(ChallengeNonceKey); - return JsonSerializer.Deserialize(challenceNonceJson); + return JsonSerializer.Deserialize(challengeNonceJson); } return null; } } -} \ No newline at end of file +} diff --git a/example/src/WebEid.AspNetCore.Example/Signing/DigiDocConfiguration.cs b/example/src/WebEid.AspNetCore.Example/Signing/DigiDocConfiguration.cs index 6e4854c..21e153c 100644 --- a/example/src/WebEid.AspNetCore.Example/Signing/DigiDocConfiguration.cs +++ b/example/src/WebEid.AspNetCore.Example/Signing/DigiDocConfiguration.cs @@ -17,14 +17,14 @@ // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -namespace WebEid.AspNetCore.Example.Services +namespace WebEid.AspNetCore.Example.Services { + using System; using digidoc; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Hosting; - using System; - public class DigiDocConfiguration + public class DigiDocConfiguration(IWebHostEnvironment env) { /// /// Base64 cert from https://open-eid.github.io/test-TL/trusted-test-tsl.crt @@ -58,12 +58,7 @@ public class DigiDocConfiguration private const string TestTslUrl = "https://open-eid.github.io/test-TL/tl-mp-test-EE.xml"; private const string TestTsUrl = "http://demo.sk.ee/tsa/"; - private readonly IWebHostEnvironment env; - - public DigiDocConfiguration(IWebHostEnvironment env) - { - this.env = env; - } + private readonly IWebHostEnvironment env = env; public void Initialize() { @@ -74,7 +69,7 @@ public void Initialize() conf.setTSLCert(Convert.FromBase64String(TestTslCert)); conf.setTSUrl(TestTsUrl); } - DigiDocConf.init(conf); + Conf.init(conf); } } } diff --git a/example/src/WebEid.AspNetCore.Example/Signing/MobileSigningService.cs b/example/src/WebEid.AspNetCore.Example/Signing/MobileSigningService.cs index aa8c244..586a32b 100644 --- a/example/src/WebEid.AspNetCore.Example/Signing/MobileSigningService.cs +++ b/example/src/WebEid.AspNetCore.Example/Signing/MobileSigningService.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2025-2025 Estonian Information System Authority +// Copyright (c) 2025-2025 Estonian Information System Authority // // Permission is hereby granted, free of charge, to any person obtaining a copy of // this software and associated documentation files (the "Software"), to deal in @@ -19,22 +19,18 @@ namespace WebEid.AspNetCore.Example.Signing { - using System; using System.Collections.Generic; using System.Security.Claims; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; - using Microsoft.Extensions.Options; + using Dto; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.WebUtilities; - using Dto; - using Options; using Services; public class MobileSigningService( SigningService signingService, - IOptions mobileOptions, IHttpContextAccessor httpContextAccessor, MobileRequestUriBuilder uriBuilder ) @@ -72,7 +68,7 @@ private MobileInitRequest InitCertificateRequest() var request = httpContextAccessor.HttpContext!.Request; var baseUrl = $"{request.Scheme}://{request.Host}"; var responseUri = $"{baseUrl}{CertificateResponsePath}"; - + var requestObj = new RequestObject { ResponseUri = responseUri @@ -140,4 +136,4 @@ internal sealed record RequestObject public string? HashFunction { get; init; } } } -} \ No newline at end of file +} diff --git a/example/src/WebEid.AspNetCore.Example/Signing/SigningService.cs b/example/src/WebEid.AspNetCore.Example/Signing/SigningService.cs index edb37d5..422a4f3 100644 --- a/example/src/WebEid.AspNetCore.Example/Signing/SigningService.cs +++ b/example/src/WebEid.AspNetCore.Example/Signing/SigningService.cs @@ -17,12 +17,12 @@ // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -namespace WebEid.AspNetCore.Example.Services +namespace WebEid.AspNetCore.Example.Services { using System; using System.Collections.Generic; - using System.Linq; using System.IO; + using System.Linq; using System.Security.Claims; using System.Security.Cryptography.X509Certificates; using digidoc; @@ -33,13 +33,11 @@ public class SigningService : IDisposable { private static readonly string FileToSign = Path.Combine("wwwroot", "files", "example-for-signing.txt"); - private readonly DigiDocConfiguration configuration; private readonly ILogger logger; - private bool _disposedValue; + private bool disposedValue; public SigningService(DigiDocConfiguration configuration, ILogger logger) { - this.configuration = configuration; this.logger = logger; // The current implementation of the static DigiDoc library assumes that SigningService is used as a singleton. @@ -51,17 +49,26 @@ public SigningService(DigiDocConfiguration configuration, ILogger logger) public DigestDto PrepareContainer(CertificateDto data, ClaimsIdentity identity, string tempContainerName) { - var certificate = new X509Certificate(Convert.FromBase64String(data.CertificateBase64String)); + var certificateBytes = Convert.FromBase64String(data.CertificateBase64String); + var certificate = X509CertificateLoader.LoadCertificate(certificateBytes); + if (identity.GetIdCode() != certificate.GetSubjectIdCode()) { throw new ArgumentException( "Authenticated subject ID code differs from signing certificate subject ID code"); } - this.logger?.LogDebug("Creating container file: '{0}'", tempContainerName); - Container container = Container.create(tempContainerName); +#pragma warning disable CA1873 + logger?.LogDebug("Creating container file: '{ContainerName}'", tempContainerName); +#pragma warning restore CA1873 + + var container = Container.create(tempContainerName); container.addDataFile(FileToSign, "application/octet-stream"); - logger?.LogInformation("Preparing container for signing for file '{0}'", tempContainerName); + +#pragma warning disable CA1873 + logger?.LogInformation("Preparing container for signing for file '{ContainerName}'", tempContainerName); +#pragma warning restore CA1873 + var signature = container.prepareWebSignature(certificate.Export(X509ContentType.Cert), "time-stamp"); var hashFunction = GetSupportedHashAlgorithm(data.SupportedSignatureAlgorithms, signature.signatureMethod()); @@ -85,8 +92,8 @@ public void SignContainer(SignatureDto signatureDto, string tempContainerName) private static string GetSupportedHashAlgorithm(IList supportedSignatureAlgorithms, string signatureMethod) { - var framgment = new Uri(signatureMethod).Fragment; - if (supportedSignatureAlgorithms.FirstOrDefault(algo => FragmentEquals(framgment, algo), null) is SignatureAlgorithmDto algo) + var fragment = new Uri(signatureMethod).Fragment; + if (supportedSignatureAlgorithms.FirstOrDefault(algo => FragmentEquals(fragment, algo)) is SignatureAlgorithmDto algo) { return algo.HashFunction; } @@ -121,7 +128,7 @@ public void Dispose() private void Dispose(bool disposing) { - if (!_disposedValue) + if (!disposedValue) { if (disposing) { @@ -129,7 +136,7 @@ private void Dispose(bool disposing) } digidoc.terminate(); - _disposedValue = true; + disposedValue = true; } } } diff --git a/example/src/WebEid.AspNetCore.Example/Startup.cs b/example/src/WebEid.AspNetCore.Example/Startup.cs index 5fe6dd2..daa5cf7 100644 --- a/example/src/WebEid.AspNetCore.Example/Startup.cs +++ b/example/src/WebEid.AspNetCore.Example/Startup.cs @@ -19,71 +19,55 @@ namespace WebEid.AspNetCore.Example { + using System; + using System.Configuration; + using System.Security.Cryptography; + using System.Threading.Tasks; using Certificates; using Microsoft.AspNetCore.Authentication.Cookies; + using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; + using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.HttpOverrides; + using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; - using Services; - using System; - using System.Security.Cryptography; + using Options; using Security.Challenge; using Security.Validator; - using System.Configuration; - using Microsoft.AspNetCore.Authorization; - using System.Threading.Tasks; - using Microsoft.AspNetCore.Http; - using Microsoft.AspNetCore.Mvc; - using Options; + using Services; using Signing; - public class Startup + public class Startup(IConfiguration configuration, IWebHostEnvironment environment) { - public Startup(IConfiguration configuration, IWebHostEnvironment environment) - { - Configuration = configuration; - CurrentEnvironment = environment; - } - - private IConfiguration Configuration { get; } - private IWebHostEnvironment CurrentEnvironment { get; } + private IConfiguration Configuration { get; } = configuration; + private IWebHostEnvironment CurrentEnvironment { get; } = environment; // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { - var loggerFactory = LoggerFactory.Create(builder => - { - builder.AddConsole(); - }); + var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole()); var logger = loggerFactory.CreateLogger("Web-eId ASP.NET Core Example"); services.AddSingleton(logger); - services.AddRazorPages(options => - { - options.Conventions.AuthorizePage("/welcome", "LoggedInOnly"); - }); + services.AddRazorPages(options => options.Conventions.AuthorizePage("/welcome", "LoggedInOnly")); - services.AddAuthorization(options => - { - options.AddPolicy("LoggedInOnly", policy => + services + .AddAuthorizationBuilder() + .AddPolicy("LoggedInOnly", policy => { policy.AuthenticationSchemes.Add(CookieAuthenticationDefaults.AuthenticationScheme); policy.RequireAuthenticatedUser(); policy.Requirements.Add(new LoggedInRequirement()); }); - }); services.AddSingleton(); services.AddControllers(); - services.AddMvc(options => - { - options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute()); - }); + services.AddMvc(options => options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute())); services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options => @@ -115,7 +99,7 @@ public void ConfigureServices(IServiceCollection services) options.IdleTimeout = TimeSpan.FromSeconds(60); options.Cookie.IsEssential = true; }); - + services.AddOptions() .Bind(Configuration.GetSection("WebEidMobile")) .ValidateDataAnnotations() @@ -138,17 +122,14 @@ public void ConfigureServices(IServiceCollection services) services.AddSingleton(); services.AddSingleton(); - services.AddAntiforgery(options => - { - options.Cookie.SecurePolicy = CookieSecurePolicy.Always; - }); + services.AddAntiforgery(options => options.Cookie.SecurePolicy = CookieSecurePolicy.Always); // Add support for running behind a TLS terminating proxy. services.Configure(options => { options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; // Only use this if you're behind a known proxy: - options.KnownNetworks.Clear(); + options.KnownIPNetworks.Clear(); options.KnownProxies.Clear(); }); } diff --git a/example/src/WebEid.AspNetCore.Example/WebEid.AspNetCore.Example.csproj b/example/src/WebEid.AspNetCore.Example/WebEid.AspNetCore.Example.csproj index d4abaff..3a2fc3f 100644 --- a/example/src/WebEid.AspNetCore.Example/WebEid.AspNetCore.Example.csproj +++ b/example/src/WebEid.AspNetCore.Example/WebEid.AspNetCore.Example.csproj @@ -2,6 +2,7 @@ net10.0 + enable aspnet-web_eid_asp_dotnet_example-3FF31439-FDC0-4164-B154-03E005FE96CD WebEid.AspNetCore.Example false @@ -12,11 +13,6 @@ - - - - - diff --git a/src/.editorconfig b/src/.editorconfig index 5402a9f..eadfeba 100644 --- a/src/.editorconfig +++ b/src/.editorconfig @@ -73,6 +73,7 @@ indent_style = tab [*.{cs,csx,cake,vb,vbx}] # Default Severity for all .NET Code Style rules below dotnet_analyzer_diagnostic.severity = warning +dotnet_diagnostic.CA1848.severity = suggestion ########################################## # Language Rules @@ -250,7 +251,7 @@ csharp_preserve_single_line_statements = false csharp_preserve_single_line_blocks = true # Namespace options # https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/formatting-rules#namespace-options -csharp_style_namespace_declarations = file_scoped:warning +csharp_style_namespace_declarations = block_scoped:warning ########################################## # .NET Naming Rules diff --git a/src/WebEid.Security.Tests/Certificate/CertificateDataTest.cs b/src/WebEid.Security.Tests/Certificate/CertificateDataTest.cs index 3444a49..6c2c515 100644 --- a/src/WebEid.Security.Tests/Certificate/CertificateDataTest.cs +++ b/src/WebEid.Security.Tests/Certificate/CertificateDataTest.cs @@ -1,6 +1,5 @@ namespace WebEid.Security.Tests.Certificate { - using System.Security.Cryptography.X509Certificates; using NUnit.Framework; using WebEid.Security.Tests.TestUtils; using WebEid.Security.Util; @@ -10,7 +9,7 @@ public class CertificateTests [Test] public void WhenOrganizationCertificateThenSubjectCNAndIdCodeAndCountryCodeExtractionSucceeds() { - X509Certificate organizationCert = Certificates.GetOrganizationCert(); + var organizationCert = Certificates.GetOrganizationCert(); Assert.That(organizationCert.GetSubjectCn(), Is.EqualTo("Testijad.ee isikutuvastus")); Assert.That(organizationCert.GetSubjectIdCode(), Is.EqualTo("12276279")); Assert.That(organizationCert.GetSubjectCountryCode(), Is.EqualTo("EE")); @@ -19,7 +18,7 @@ public void WhenOrganizationCertificateThenSubjectCNAndIdCodeAndCountryCodeExtra [Test] public void WhenOrganizationCertificateThenSubjectGivenNameAndSurnameAreNull() { - X509Certificate organizationCert = Certificates.GetOrganizationCert(); + var organizationCert = Certificates.GetOrganizationCert(); Assert.That(organizationCert.GetSubjectGivenName(), Is.Null); Assert.That(organizationCert.GetSubjectSurname(), Is.Null); } diff --git a/src/WebEid.Security.Tests/Logger.cs b/src/WebEid.Security.Tests/Logger.cs index b2b22b4..d9ecd2d 100644 --- a/src/WebEid.Security.Tests/Logger.cs +++ b/src/WebEid.Security.Tests/Logger.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright © 2020-2024 Estonian Information System Authority * * Permission is hereby granted, free of charge, to any person obtaining a copy @@ -27,16 +27,16 @@ namespace WebEid.Security.Tests public class Logger : ILogger { - public Logger() => this.Logs = new List(); + public Logger() => Logs = []; public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) { - if (!this.IsEnabled(logLevel)) + if (!IsEnabled(logLevel)) { return; } - this.Logs.Add(formatter(state, exception)); + Logs.Add(formatter(state, exception)); } public bool IsEnabled(LogLevel logLevel) => true; diff --git a/src/WebEid.Security.Tests/Nonce/ChallengeNonceGeneratorTests.cs b/src/WebEid.Security.Tests/Nonce/ChallengeNonceGeneratorTests.cs index b3f2a81..61b24c6 100644 --- a/src/WebEid.Security.Tests/Nonce/ChallengeNonceGeneratorTests.cs +++ b/src/WebEid.Security.Tests/Nonce/ChallengeNonceGeneratorTests.cs @@ -23,8 +23,8 @@ namespace WebEid.Security.Tests.Nonce { using System; using System.Security.Cryptography; - using NUnit.Framework; using Challenge; + using NUnit.Framework; [TestFixture] public class ChallengeNonceGeneratorTests @@ -37,20 +37,20 @@ public class ChallengeNonceGeneratorTests [SetUp] protected void SetUp() { - this.store = new InMemoryChallengeNonceStore(); - this.ttl = TimeSpan.FromMinutes(5); + store = new InMemoryChallengeNonceStore(); + ttl = TimeSpan.FromMinutes(5); } [Test] public void NonceIsGeneratedAndStored() { using var rndGenerator = RandomNumberGenerator.Create(); - var nonceGenerator = new ChallengeNonceGenerator(rndGenerator, this.store); + var nonceGenerator = new ChallengeNonceGenerator(rndGenerator, store); - var nonce1 = nonceGenerator.GenerateAndStoreNonce(this.ttl); - var nonce2 = nonceGenerator.GenerateAndStoreNonce(this.ttl); + var nonce1 = nonceGenerator.GenerateAndStoreNonce(ttl); + var nonce2 = nonceGenerator.GenerateAndStoreNonce(ttl); - var nonce2FromStore = this.store.GetAndRemove(); + var nonce2FromStore = store.GetAndRemove(); // Validate that generated nonce was put into the store. Assert.That(nonce2, Is.EqualTo(nonce2FromStore)); @@ -78,7 +78,7 @@ public void BuildNonceGeneratorWithoutStoreThrowsArgumentNullException() public void BuildNonceGeneratorWithNegativeTtlThrowsArgumentOutOfRangeException() { using var rndGenerator = RandomNumberGenerator.Create(); - var generator = new ChallengeNonceGenerator(rndGenerator, this.store); + var generator = new ChallengeNonceGenerator(rndGenerator, store); Assert.Throws(() => generator.GenerateAndStoreNonce(TimeSpan.FromMinutes(1).Negate())); } @@ -87,7 +87,7 @@ public void BuildNonceGeneratorWithNegativeTtlThrowsArgumentOutOfRangeException( public void BuildNonceGeneratorWithZeroTtlThrowsArgumentOutOfRangeException() { using var rndGenerator = RandomNumberGenerator.Create(); - var generator = new ChallengeNonceGenerator(rndGenerator, this.store); + var generator = new ChallengeNonceGenerator(rndGenerator, store); Assert.Throws(() => generator.GenerateAndStoreNonce(TimeSpan.Zero)); } @@ -96,14 +96,14 @@ public void BuildNonceGeneratorWithZeroTtlThrowsArgumentOutOfRangeException() public void BuildNonceGeneratorWithRandomNumberGeneratorAndCacheDoesNotThrowException() { using var rndGenerator = RandomNumberGenerator.Create(); - Assert.DoesNotThrow(() => new ChallengeNonceGenerator(rndGenerator, this.store)); + Assert.DoesNotThrow(() => new ChallengeNonceGenerator(rndGenerator, store)); } [Test] public void BuildNonceGeneratorWithRandomNumberGeneratorAndCacheAndPositiveTtlDoesNotThrowException() { using var rndGenerator = RandomNumberGenerator.Create(); - var generator = new ChallengeNonceGenerator(rndGenerator, this.store); + var generator = new ChallengeNonceGenerator(rndGenerator, store); Assert.DoesNotThrow(() => generator.GenerateAndStoreNonce(TimeSpan.FromMinutes(1))); } } diff --git a/src/WebEid.Security.Tests/Nonce/InMemoryChallengeNonceStore.cs b/src/WebEid.Security.Tests/Nonce/InMemoryChallengeNonceStore.cs index 57fac5f..ddbf2da 100644 --- a/src/WebEid.Security.Tests/Nonce/InMemoryChallengeNonceStore.cs +++ b/src/WebEid.Security.Tests/Nonce/InMemoryChallengeNonceStore.cs @@ -29,8 +29,8 @@ internal sealed class InMemoryChallengeNonceStore : IChallengeNonceStore public ChallengeNonce GetAndRemoveImpl() { - var result = this.challengeNonce; - this.challengeNonce = null; + var result = challengeNonce; + challengeNonce = null; return result; } diff --git a/src/WebEid.Security.Tests/TestUtils/AbstractTestWithValidator.cs b/src/WebEid.Security.Tests/TestUtils/AbstractTestWithValidator.cs index 906bb62..b1ca25d 100644 --- a/src/WebEid.Security.Tests/TestUtils/AbstractTestWithValidator.cs +++ b/src/WebEid.Security.Tests/TestUtils/AbstractTestWithValidator.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright © 2020-2025 Estonian Information System Authority * * Permission is hereby granted, free of charge, to any person obtaining a copy @@ -65,12 +65,5 @@ protected void SetUp() protected WebEidAuthToken ReplaceTokenField(string token, string field, string value) => Validator.Parse(token.Replace(field, value)); - - protected string RemoveJsonField(string json, string fieldName) - { - var node = Newtonsoft.Json.Linq.JObject.Parse(json); - node.Remove(fieldName); - return node.ToString(Newtonsoft.Json.Formatting.None); - } } } diff --git a/src/WebEid.Security.Tests/TestUtils/ByteArrayExtensions.cs b/src/WebEid.Security.Tests/TestUtils/ByteArrayExtensions.cs index 6711485..90dbb94 100644 --- a/src/WebEid.Security.Tests/TestUtils/ByteArrayExtensions.cs +++ b/src/WebEid.Security.Tests/TestUtils/ByteArrayExtensions.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright © 2020-2024 Estonian Information System Authority * * Permission is hereby granted, free of charge, to any person obtaining a copy diff --git a/src/WebEid.Security.Tests/TestUtils/Certificates.cs b/src/WebEid.Security.Tests/TestUtils/Certificates.cs index 3c10b71..38e0ab1 100644 --- a/src/WebEid.Security.Tests/TestUtils/Certificates.cs +++ b/src/WebEid.Security.Tests/TestUtils/Certificates.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright © 2020-2024 Estonian Information System Authority * * Permission is hereby granted, free of charge, to any person obtaining a copy @@ -82,19 +82,13 @@ public static X509Certificate GetTestSkOcspResponder2020() public static X509Certificate2 GetJaakKristjanEsteid2018Cert() { - if (jaakKristjanEsteid2018Cert == null) - { - jaakKristjanEsteid2018Cert = CertificateLoader.LoadCertificateFromBase64String(JaakKristjanEsteid2018Cert); - } + jaakKristjanEsteid2018Cert ??= CertificateLoader.LoadCertificateFromBase64String(JaakKristjanEsteid2018Cert); return jaakKristjanEsteid2018Cert; } public static X509Certificate GetMariliisEsteid2015Cert() { - if (mariliisEsteid2015Cert == null) - { - mariliisEsteid2015Cert = CertificateLoader.LoadCertificateFromBase64String(MariliisEsteid2015Cert); - } + mariliisEsteid2015Cert ??= CertificateLoader.LoadCertificateFromBase64String(MariliisEsteid2015Cert); return mariliisEsteid2015Cert; } diff --git a/src/WebEid.Security.Tests/TestUtils/DateTimeExtensions.cs b/src/WebEid.Security.Tests/TestUtils/DateTimeExtensions.cs index 9ae998a..3713f9b 100644 --- a/src/WebEid.Security.Tests/TestUtils/DateTimeExtensions.cs +++ b/src/WebEid.Security.Tests/TestUtils/DateTimeExtensions.cs @@ -26,10 +26,7 @@ namespace WebEid.Security.Tests.TestUtils internal static class DateTimeExtensions { - internal static DateTime TrimMilliseconds(this DateTime dt) - { - return dt.AddTicks(-dt.Ticks % TimeSpan.TicksPerSecond); - } + internal static DateTime TrimMilliseconds(this DateTime dt) => dt.AddTicks(-dt.Ticks % TimeSpan.TicksPerSecond); internal static DerGeneralizedTime ToDerGenTime(this DateTime dateTime) => new(dateTime); } diff --git a/src/WebEid.Security.Tests/TestUtils/OcspServiceMaker.cs b/src/WebEid.Security.Tests/TestUtils/OcspServiceMaker.cs index 983ca3b..f13cb3e 100644 --- a/src/WebEid.Security.Tests/TestUtils/OcspServiceMaker.cs +++ b/src/WebEid.Security.Tests/TestUtils/OcspServiceMaker.cs @@ -35,11 +35,11 @@ public class OcspServiceMaker private static readonly List TrustedCaCertificates; private static readonly Uri TestEsteid2015 = new("http://aia.demo.sk.ee/esteid2015"); - static OcspServiceMaker() => TrustedCaCertificates = new List - { + static OcspServiceMaker() => TrustedCaCertificates = + [ new(Certificates.GetTestEsteid2015Ca()), new(Certificates.GetTestEsteid2018Ca()) - }; + ]; public static OcspServiceProvider GetAiaOcspServiceProvider() => new(null, GetAiaOcspServiceConfiguration()); @@ -49,7 +49,7 @@ public class OcspServiceMaker public static OcspServiceProvider GetDesignatedOcspServiceProvider(string ocspServiceAccessLocation) => new(GetDesignatedOcspServiceConfiguration(true, ocspServiceAccessLocation), GetAiaOcspServiceConfiguration()); - private static AiaOcspServiceConfiguration GetAiaOcspServiceConfiguration() => new(new List { TestEsteid2015 }, TrustedCaCertificates); + private static AiaOcspServiceConfiguration GetAiaOcspServiceConfiguration() => new([TestEsteid2015], TrustedCaCertificates); public static DesignatedOcspServiceConfiguration GetDesignatedOcspServiceConfiguration() => GetDesignatedOcspServiceConfiguration(true, TestOcspAccessLocation); @@ -58,7 +58,7 @@ public class OcspServiceMaker private static DesignatedOcspServiceConfiguration GetDesignatedOcspServiceConfiguration(bool doesSupportNonce, string ocspServiceAccessLocation) => new( new Uri(ocspServiceAccessLocation), DotNetUtilities.FromX509Certificate(Certificates.GetTestSkOcspResponder2020()), - TrustedCaCertificates.Select(DotNetUtilities.FromX509Certificate).ToList(), + [.. TrustedCaCertificates.Select(DotNetUtilities.FromX509Certificate)], doesSupportNonce); } } diff --git a/src/WebEid.Security.Tests/Util/DateTimeProviderTests.cs b/src/WebEid.Security.Tests/Util/DateTimeProviderTests.cs index d978305..e406acb 100644 --- a/src/WebEid.Security.Tests/Util/DateTimeProviderTests.cs +++ b/src/WebEid.Security.Tests/Util/DateTimeProviderTests.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright © 2020-2024 Estonian Information System Authority * * Permission is hereby granted, free of charge, to any person obtaining a copy diff --git a/src/WebEid.Security.Tests/Util/X509CertificateExtensionsTests.cs b/src/WebEid.Security.Tests/Util/X509CertificateExtensionsTests.cs index 1784e84..5c1c82b 100644 --- a/src/WebEid.Security.Tests/Util/X509CertificateExtensionsTests.cs +++ b/src/WebEid.Security.Tests/Util/X509CertificateExtensionsTests.cs @@ -36,58 +36,58 @@ public class X509CertificateExtensionsTests [OneTimeSetUp] public void SetUp() => - this.certificate = Certificates.CertificateLoader.LoadCertificateFromResource("Karl-Kristjan-Joeorg.cer"); + certificate = Certificates.CertificateLoader.LoadCertificateFromResource("Karl-Kristjan-Joeorg.cer"); [Test] public void GetSubjectIdCodeReturnsCorrectValue() => - Assert.That("PNOEE-38001085718", Is.EqualTo(this.certificate.GetSubjectIdCode())); + Assert.That("PNOEE-38001085718", Is.EqualTo(certificate.GetSubjectIdCode())); [Test] public void GetSubjectCnReturnsCorrectValue() => - Assert.That("JÕEORG,JAAK-KRISTJAN,38001085718", Is.EqualTo(this.certificate.GetSubjectCn())); + Assert.That("JÕEORG,JAAK-KRISTJAN,38001085718", Is.EqualTo(certificate.GetSubjectCn())); [Test] public void GetSubjectGivenNameReturnsCorrectValue() => - Assert.That("JAAK-KRISTJAN", Is.EqualTo(this.certificate.GetSubjectGivenName())); + Assert.That("JAAK-KRISTJAN", Is.EqualTo(certificate.GetSubjectGivenName())); [Test] public void GetSubjectSurnameReturnsCorrectValue() => - Assert.That("JÕEORG", Is.EqualTo(this.certificate.GetSubjectSurname())); + Assert.That("JÕEORG", Is.EqualTo(certificate.GetSubjectSurname())); [Test] public void GetSubjectCountryCodeReturnsCorrectValue() => - Assert.That("EE", Is.EqualTo(this.certificate.GetSubjectCountryCode())); + Assert.That("EE", Is.EqualTo(certificate.GetSubjectCountryCode())); [Test] public void ValidateBcNotYetValidCertificateExpiryThrowsException() => Assert.Throws(() => - DotNetUtilities.FromX509Certificate(this.certificate) + DotNetUtilities.FromX509Certificate(certificate) .ValidateCertificateExpiry(new DateTime(2000, 1, 1), "Test")); [Test] public void ValidateBcExpiredCertificateExpiryThrowsException() => Assert.Throws(() => - DotNetUtilities.FromX509Certificate(this.certificate) + DotNetUtilities.FromX509Certificate(certificate) .ValidateCertificateExpiry(new DateTime(2030, 1, 1), "Test")); [Test] public void ValidateBcValidCertificateExpiryDoesNotThrowException() => Assert.DoesNotThrow(() => - DotNetUtilities.FromX509Certificate(this.certificate) + DotNetUtilities.FromX509Certificate(certificate) .ValidateCertificateExpiry(new DateTime(2021, 08, 1), "Test")); [Test] public void ValidateNotYetValidCertificateExpiryThrowsException() => Assert.Throws(() => - new X509Certificate2(this.certificate) + new X509Certificate2(certificate) .ValidateCertificateExpiry(new DateTime(2000, 1, 1), "Test")); [Test] public void ValidateExpiredCertificateExpiryThrowsException() => Assert.Throws(() => - new X509Certificate2(this.certificate) + new X509Certificate2(certificate) .ValidateCertificateExpiry(new DateTime(2030, 1, 1), "Test")); [Test] public void ValidateValidCertificateExpiryDoesNotThrowException() => Assert.DoesNotThrow(() => - new X509Certificate2(this.certificate) + new X509Certificate2(certificate) .ValidateCertificateExpiry(new DateTime(2021, 08, 1), "Test")); } } diff --git a/src/WebEid.Security.Tests/Validator/AuthTokenAlgorithmTest.cs b/src/WebEid.Security.Tests/Validator/AuthTokenAlgorithmTest.cs index 72ed452..048ba68 100644 --- a/src/WebEid.Security.Tests/Validator/AuthTokenAlgorithmTest.cs +++ b/src/WebEid.Security.Tests/Validator/AuthTokenAlgorithmTest.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright © 2020-2025 Estonian Information System Authority * * Permission is hereby granted, free of charge, to any person obtaining a copy @@ -21,8 +21,8 @@ */ namespace WebEid.Security.Tests.Validator { - using NUnit.Framework; using Exceptions; + using NUnit.Framework; using TestUtils; public class AuthTokenAlgorithmTest : AbstractTestWithValidator @@ -44,11 +44,12 @@ public void WhenAlgorithmEmptyThenParsingFailsAsync() } [Test] - public void WhenAlgorithmInvalidThenParsingFailsAsync() + public void WhenAlgorithmInvalidThenParsingFails() { - var authToken = ReplaceTokenField(ValidAuthTokenStr, "ES384", "\u0000\t\ninvalid"); - Assert.ThrowsAsync(() => Validator.Validate(authToken, ValidChallengeNonce)) - .WithMessage("Unsupported signature algorithm"); + var authToken = ValidAuthTokenStr.Replace("\"ES384\"", "\"\u0000\t\ninvalid\""); + + Assert.Throws(() => Validator.Parse(authToken)) + .WithMessage("Error parsing Web eID authentication token"); } [Test] @@ -74,7 +75,7 @@ public void WhenV11TokenHasInvalidCryptoAlgorithmThenValidationFailsAsync() [Test] public void WhenV11TokenHasInvalidHashFunctionThenValidationFailsAsync() { - var token = ReplaceTokenField( ValidV11AuthTokenStr, "\"hashFunction\":\"SHA-256\"", "\"hashFunction\":\"NOT_A_HASH\""); + var token = ReplaceTokenField(ValidV11AuthTokenStr, "\"hashFunction\":\"SHA-256\"", "\"hashFunction\":\"NOT_A_HASH\""); Assert.ThrowsAsync(() => Validator.Validate(token, ValidChallengeNonce)) .WithMessage("Unsupported signature algorithm"); } @@ -82,7 +83,7 @@ public void WhenV11TokenHasInvalidHashFunctionThenValidationFailsAsync() [Test] public void WhenV11TokenHasInvalidPaddingSchemeThenValidationFailsAsync() { - var token = ReplaceTokenField( ValidV11AuthTokenStr, "\"paddingScheme\":\"PKCS1.5\"", "\"paddingScheme\":\"BAD_PADDING\""); + var token = ReplaceTokenField(ValidV11AuthTokenStr, "\"paddingScheme\":\"PKCS1.5\"", "\"paddingScheme\":\"BAD_PADDING\""); Assert.ThrowsAsync(() => Validator.Validate(token, ValidChallengeNonce)) .WithMessage("Unsupported signature algorithm"); } @@ -90,7 +91,7 @@ public void WhenV11TokenHasInvalidPaddingSchemeThenValidationFailsAsync() [Test] public void WhenV11TokenHasEmptySupportedAlgorithmsThenValidationFailsAsync() { - var token = ReplaceTokenField( ValidV11AuthTokenStr, "\"supportedSignatureAlgorithms\":[{\"cryptoAlgorithm\":\"RSA\",\"hashFunction\":\"SHA-256\",\"paddingScheme\":\"PKCS1.5\"}]", "\"supportedSignatureAlgorithms\":[]"); + var token = ReplaceTokenField(ValidV11AuthTokenStr, "\"supportedSignatureAlgorithms\":[{\"cryptoAlgorithm\":\"RSA\",\"hashFunction\":\"SHA-256\",\"paddingScheme\":\"PKCS1.5\"}]", "\"supportedSignatureAlgorithms\":[]"); Assert.ThrowsAsync(() => Validator.Validate(token, ValidChallengeNonce)) .WithMessage("'supportedSignatureAlgorithms' field is missing"); } diff --git a/src/WebEid.Security.Tests/Validator/AuthTokenCertificateBelgianIdCardTest.cs b/src/WebEid.Security.Tests/Validator/AuthTokenCertificateBelgianIdCardTest.cs index a35a757..d29af2b 100644 --- a/src/WebEid.Security.Tests/Validator/AuthTokenCertificateBelgianIdCardTest.cs +++ b/src/WebEid.Security.Tests/Validator/AuthTokenCertificateBelgianIdCardTest.cs @@ -23,13 +23,12 @@ namespace WebEid.Security.Tests.Validator { using System; using NUnit.Framework; - using WebEid.Security.Exceptions; using WebEid.Security.Tests.TestUtils; using WebEid.Security.Util; public class AuthTokenCertificateBelgianIdCardTest : AbstractTestWithValidator { - private const string BelgianTestIdCardAuthTokenEcc = + private const string BelgianTestIdCardAuthTokenEcc = "{" + " \"action\": \"web-eid:authenticate-success\"," + " \"algorithm\": \"ES384\"," + @@ -39,7 +38,7 @@ public class AuthTokenCertificateBelgianIdCardTest : AbstractTestWithValidator " \"unverifiedCertificate\": \"MIIDQDCCAsegAwIBAgIQEAAAAAAA8evx/gAAAAGKYTAKBggqhkjOPQQDAzAuMQswCQYDVQQGEwJCRTEfMB0GA1UEAwwWZUlEIFRFU1QgRUMgQ2l0aXplbiBDQTAeFw0yMDEwMjIyMjAwMDBaFw0zMDEwMjIyMjAwMDBaMHYxCzAJBgNVBAYTAkJFMScwJQYDVQQDDB5Ob3JhIFNwZWNpbWVuIChBdXRoZW50aWNhdGlvbikxETAPBgNVBAQMCFNwZWNpbWVuMRUwEwYDVQQqDAxOb3JhIEFuZ8OobGUxFDASBgNVBAUTCzAxMDUwMzk5ODY0MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEPybypdwlvczyuKQtJ87s/RmB+hRFaP4BdtR/Sc8jfQTmKUYVn0KDYZPBllh928yMPxU7F+Za3FtFrAPCnDH75IquYsn0oc5olVO7Uas5gn61Y2EA5askyCljNVLA0Gquo4IBYDCCAVwwHwYDVR0jBBgwFoAU3bN/45oZjlnJEVYCLfX6nW/ehEswDgYDVR0PAQH/BAQDAgeAMEkGA1UdIARCMEAwPgYHYDgMAQECAjAzMDEGCCsGAQUFBwIBFiVodHRwOi8vZWlkZGV2Y2FyZHMuemV0ZXNjYXJkcy5iZS9jZXJ0MBMGA1UdJQQMMAoGCCsGAQUFBwMCMEUGA1UdHwQ+MDwwOqA4oDaGNGh0dHA6Ly9laWRkZXZjYXJkcy56ZXRlc2NhcmRzLmJlL2NybC9jaXRpemVuY2FFQy5jcmwwgYEGCCsGAQUFBwEBBHUwczA+BggrBgEFBQcwAoYyaHR0cDovL2VpZGRldmNhcmRzLnpldGVzY2FyZHMuYmUvY2VydC9yb290Y2FFQy5jcnQwMQYIKwYBBQUHMAGGJWh0dHA6Ly9laWRkZXZjYXJkcy56ZXRlc2NhcmRzLmJlOjg4ODgwCgYIKoZIzj0EAwMDZwAwZAIwE7uLOjrhXbid+tRKe/5wgE/R3rFVsE6HkpHJg+9+mqlBToLrLWvckmiPRmUot85BAjBNyxy48pVF+azJEnt0Z/hipToVhgJLlMkPFwZiL2+4B3w2WtNeSphEl3gjClos+Wg=\"" + "}"; - private const string BelgianTestIdCardAuthTokenRsa = + private const string BelgianTestIdCardAuthTokenRsa = "{" + " \"action\": \"web-eid:authenticate-success\"," + " \"algorithm\": \"RS256\"," + @@ -68,6 +67,6 @@ public void WhenIdCardWithRSASignatureCertificateIsValidatedThenValidationSuccee var token = validator.Parse(BelgianTestIdCardAuthTokenRsa); Assert.DoesNotThrowAsync(() => validator.Validate(token, "YPVgYc7Qds0qmK/RilPLffnsIg7IIovM4BAWqGZWwiY=")); } - + } } diff --git a/src/WebEid.Security.Tests/Validator/AuthTokenCertificateFinnishIdCardTest.cs b/src/WebEid.Security.Tests/Validator/AuthTokenCertificateFinnishIdCardTest.cs index 31937ea..611f250 100644 --- a/src/WebEid.Security.Tests/Validator/AuthTokenCertificateFinnishIdCardTest.cs +++ b/src/WebEid.Security.Tests/Validator/AuthTokenCertificateFinnishIdCardTest.cs @@ -23,13 +23,12 @@ namespace WebEid.Security.Tests.Validator { using System; using NUnit.Framework; - using WebEid.Security.Exceptions; using WebEid.Security.Tests.TestUtils; using WebEid.Security.Util; public class AuthTokenCertificateFinnishIdCardTest : AbstractTestWithValidator { - private const string FinnishTestIdCardBackmanJuhaniAuthToken = + private const string FinnishTestIdCardBackmanJuhaniAuthToken = "{" + " \"action\": \"web-eid:authenticate-success\"," + " \"algorithm\": \"ES384\"," + @@ -39,7 +38,7 @@ public class AuthTokenCertificateFinnishIdCardTest : AbstractTestWithValidator " \"unverifiedCertificate\": \"MIIEOjCCA7+gAwIBAgIEBhwJHTAMBggqhkjOPQQDAwUAMHgxCzAJBgNVBAYTAkZJMSkwJwYDVQQKDCBEaWdpLSBqYSB2YWVzdG90aWV0b3ZpcmFzdG8gVEVTVDEYMBYGA1UECwwPVGVzdGl2YXJtZW50ZWV0MSQwIgYDVQQDDBtEVlYgVEVTVCBDZXJ0aWZpY2F0ZXMgLSBHNUUwHhcNMjMwMTI1MjIwMDAwWhcNMjgwMTIzMjE1OTU5WjB5MQswCQYDVQQGEwJGSTESMBAGA1UEBRMJOTk5MDIwMDE2MQ8wDQYDVQQqDAZKVUhBTkkxGTAXBgNVBAQMEFNQRUNJTUVOLUJBQ0tNQU4xKjAoBgNVBAMMIVNQRUNJTUVOLUJBQ0tNQU4gSlVIQU5JIDk5OTAyMDAxNjB2MBAGByqGSM49AgEGBSuBBAAiA2IABKq3yVI9NYmZwV2Matvk6yXFLLYn087ldhvl1AfCRoV8mTGhmL+y/R4DzaTeTrS9epEUcR9x2697h6DLBUkiOlAcI3nN92RJgNlBOCdvBdNcYgx57njSJHde4Rsm5gmLLqOCAhUwggIRMB8GA1UdIwQYMBaAFBKet+Iox/OUaou9Tcb0wjaXUkIIMB0GA1UdDgQWBBS8olmlfP/C700H4k/wLPrKX513QzAOBgNVHQ8BAf8EBAMCA4gwgc0GA1UdIASBxTCBwjCBvwYKKoF2hAVjCoJgATCBsDAnBggrBgEFBQcCARYbaHR0cDovL3d3dy5maW5laWQuZmkvY3BzOTkvMIGEBggrBgEFBQcCAjB4GnZWYXJtZW5uZXBvbGl0aWlra2Egb24gc2FhdGF2aWxsYSAtIENlcnRpZmlrYXRwb2xpY3kgZmlubnMgLSBDZXJ0aWZpY2F0ZSBwb2xpY3kgaXMgYXZhaWxhYmxlIGh0dHA6Ly93d3cuZmluZWlkLmZpL2Nwczk5MDAGA1UdEQQpMCeBJVMxSnVoYW5pMDQ5LlNQRUNJTUVOLUJhY2ttYW5AdGVzdGkuZmkwDwYDVR0TAQH/BAUwAwEBADA4BgNVHR8EMTAvMC2gK6AphidodHRwOi8vcHJveHkuZmluZWlkLmZpL2NybC9kdnZ0cDVlYy5jcmwwcgYIKwYBBQUHAQEEZjBkMDIGCCsGAQUFBzAChiZodHRwOi8vcHJveHkuZmluZWlkLmZpL2NhL2R2dnRwNWVjLmNydDAuBggrBgEFBQcwAYYiaHR0cDovL29jc3B0ZXN0LmZpbmVpZC5maS9kdnZ0cDVlYzAMBggqhkjOPQQDAwUAA2cAMGQCMClSh2MQZVYZyKfgmntQxuVUtQvIIqs8aOdsKpla4wt/IU6hMbGEAfIv4AzLXLsS5QIwUcjlY8BCj4+x84ihAqqHNIle6kyKek/Tj994SjQBmUadtyUSDvg8O5MppKvgJCNV\"" + "}"; - private const string FinnishTestIdCardBabafoVeliAuthToken = + private const string FinnishTestIdCardBabafoVeliAuthToken = "{" + " \"action\": \"web-eid:authenticate-success\"," + " \"algorithm\": \"PS256\"," + @@ -68,6 +67,6 @@ public void WhenIdCardSignatureCertificateWithG4RootCertificateIsValidatedThenVa var token = validator.Parse(FinnishTestIdCardBabafoVeliAuthToken); Assert.DoesNotThrowAsync(() => validator.Validate(token, "ZqlDATkQRqh7LkqEbspBc2qDjot29oiNLlITdLgiVIo=")); } - + } } diff --git a/src/WebEid.Security.Tests/Validator/AuthTokenCertificateTest.cs b/src/WebEid.Security.Tests/Validator/AuthTokenCertificateTest.cs index 3e58fc6..0f7a261 100644 --- a/src/WebEid.Security.Tests/Validator/AuthTokenCertificateTest.cs +++ b/src/WebEid.Security.Tests/Validator/AuthTokenCertificateTest.cs @@ -54,103 +54,103 @@ public class AuthTokenCertificateTest : AbstractTestWithValidator [Test] public void WhenCertificateFieldIsMissingThenParsingFailsAsync() { - var authToken = this.ReplaceTokenField(AuthToken, "\"unverifiedCertificate\":\"X5C\",", ""); - Assert.ThrowsAsync(() => this.Validator.Validate(authToken, ValidChallengeNonce)) + var authToken = ReplaceTokenField(AuthToken, "\"unverifiedCertificate\":\"X5C\",", ""); + Assert.ThrowsAsync(() => Validator.Validate(authToken, ValidChallengeNonce)) .WithMessage("'unverifiedCertificate' field is missing, null or empty"); } [Test] public void WhenCertificateFieldIsEmptyThenParsingFailsAsync() { - var authToken = this.ReplaceTokenField(AuthToken, "X5C", ""); - Assert.ThrowsAsync(() => this.Validator.Validate(authToken, ValidChallengeNonce)) + var authToken = ReplaceTokenField(AuthToken, "X5C", ""); + Assert.ThrowsAsync(() => Validator.Validate(authToken, ValidChallengeNonce)) .WithMessage("'unverifiedCertificate' field is missing, null or empty"); } [Test] public void WhenCertificateFieldIsArrayThenParsingFails() => Assert.Throws(() => - this.ReplaceTokenField(AuthToken, "\"X5C\"", "[1,2,3,4]")) + ReplaceTokenField(AuthToken, "\"X5C\"", "[1,2,3,4]")) .WithMessage("Error parsing Web eID authentication token"); [Test] - public void WhenCertificateFieldIsNumberThenParsingFailsAsync() + public void WhenCertificateFieldIsNumberThenParsingFails() { - var authToken = this.ReplaceTokenField(AuthToken, "\"X5C\"", "1234"); - Assert.ThrowsAsync(() => this.Validator.Validate(authToken, ValidChallengeNonce)) - .WithMessage("'unverifiedCertificate' field must contain a valid certificate"); + var authToken = AuthToken.Replace("\"X5C\"", "1234"); + Assert.Throws(() => Validator.Parse(authToken)) + .WithMessage("Error parsing Web eID authentication token"); } [Test] public void WhenCertificateFieldIsNotBase64ThenParsingFailsAsync() { - var authToken = this.ReplaceTokenField(AuthToken, "X5C", "This is not a certificate"); - Assert.ThrowsAsync(() => this.Validator.Validate(authToken, ValidChallengeNonce)) + var authToken = ReplaceTokenField(AuthToken, "X5C", "This is not a certificate"); + Assert.ThrowsAsync(() => Validator.Validate(authToken, ValidChallengeNonce)) .WithMessage("'unverifiedCertificate' field must contain a valid certificate"); } [Test] public void WhenCertificateFieldIsNotCertificateThenParsingFailsAsync() { - var authToken = this.ReplaceTokenField(AuthToken, "X5C", "VGhpcyBpcyBub3QgYSBjZXJ0aWZpY2F0ZQ"); - Assert.ThrowsAsync(() => this.Validator.Validate(authToken, ValidChallengeNonce)) + var authToken = ReplaceTokenField(AuthToken, "X5C", "VGhpcyBpcyBub3QgYSBjZXJ0aWZpY2F0ZQ"); + Assert.ThrowsAsync(() => Validator.Validate(authToken, ValidChallengeNonce)) .WithMessage("'unverifiedCertificate' field must contain a valid certificate"); } [Test] public void WhenCertificateKeyUsageIsMissingThenValidationFailsAsync() { - var authToken = this.ReplaceTokenField(AuthToken, "X5C", MissingKeyUsageCert); - Assert.ThrowsAsync(() => this.Validator.Validate(authToken, ValidChallengeNonce)); + var authToken = ReplaceTokenField(AuthToken, "X5C", MissingKeyUsageCert); + Assert.ThrowsAsync(() => Validator.Validate(authToken, ValidChallengeNonce)); } [Test] public void WhenCertificatePurposeIsWrongThenValidationFailsAsync() { - var authToken = this.ReplaceTokenField(AuthToken, "X5C", WrongPurposeCert); - Assert.ThrowsAsync(() => this.Validator.Validate(authToken, ValidChallengeNonce)); + var authToken = ReplaceTokenField(AuthToken, "X5C", WrongPurposeCert); + Assert.ThrowsAsync(() => Validator.Validate(authToken, ValidChallengeNonce)); } [Test] public void WhenCertificatePolicyIsWrongThenValidationFailsAsync() { - var authToken = this.ReplaceTokenField(AuthToken, "X5C", WrongPolicyCert); - Assert.ThrowsAsync(() => this.Validator.Validate(authToken, ValidChallengeNonce)); + var authToken = ReplaceTokenField(AuthToken, "X5C", WrongPolicyCert); + Assert.ThrowsAsync(() => Validator.Validate(authToken, ValidChallengeNonce)); } [Test] public void WhenCertificatePolicyIsDisallowedThenValidationFailsAsync() { var authTokenValidator = AuthTokenValidators.GetAuthTokenValidatorWithDisallowedEsteidPolicy(); - Assert.ThrowsAsync(() => authTokenValidator.Validate(this.ValidAuthToken, ValidChallengeNonce)); + Assert.ThrowsAsync(() => authTokenValidator.Validate(ValidAuthToken, ValidChallengeNonce)); } [Test] public void WhenUsingOldMobileIdCertificateThenValidationFailsAsync() { - var authToken = this.ReplaceTokenField(AuthToken, "X5C", OldMobileIdCert); - Assert.ThrowsAsync(() => this.Validator.Validate(authToken, ValidChallengeNonce)); + var authToken = ReplaceTokenField(AuthToken, "X5C", OldMobileIdCert); + Assert.ThrowsAsync(() => Validator.Validate(authToken, ValidChallengeNonce)); } [Test] public void WhenUsingNewMobileIdCertificateThenValidationFailsAsync() { - var authToken = this.ReplaceTokenField(AuthToken, "X5C", NewMobileIdCert); - Assert.ThrowsAsync(() => this.Validator.Validate(authToken, ValidChallengeNonce)); + var authToken = ReplaceTokenField(AuthToken, "X5C", NewMobileIdCert); + Assert.ThrowsAsync(() => Validator.Validate(authToken, ValidChallengeNonce)); } [Test] public void WhenCertificateIsExpiredRsaThenValidationFailsAsync() { - var authToken = this.ReplaceTokenField(AuthToken, "X5C", ExpiredRsaCert); - Assert.ThrowsAsync(() => this.Validator.Validate(authToken, ValidChallengeNonce)); + var authToken = ReplaceTokenField(AuthToken, "X5C", ExpiredRsaCert); + Assert.ThrowsAsync(() => Validator.Validate(authToken, ValidChallengeNonce)); } [Test] public void WhenCertificateIsExpiredEcdsaThenValidationFailsAsync() { - var authToken = this.ReplaceTokenField(AuthToken, "X5C", ExpiredEcdsaCert); - Assert.ThrowsAsync(() => this.Validator.Validate(authToken, ValidChallengeNonce)); + var authToken = ReplaceTokenField(AuthToken, "X5C", ExpiredEcdsaCert); + Assert.ThrowsAsync(() => Validator.Validate(authToken, ValidChallengeNonce)); } // Subject certificate validity: @@ -164,7 +164,7 @@ public void WhenCertificateIsExpiredEcdsaThenValidationFailsAsync() public void WhenUserCertificateIsNotYetValidThenValidationFailsAsync() { using var _ = DateTimeProvider.OverrideUtcNow(new DateTime(2018, 10, 17)); - Assert.ThrowsAsync(() => this.Validator.Validate(this.ValidAuthToken, ValidChallengeNonce)) + Assert.ThrowsAsync(() => Validator.Validate(ValidAuthToken, ValidChallengeNonce)) .WithMessage("User certificate is not yet valid"); } @@ -172,8 +172,8 @@ public void WhenUserCertificateIsNotYetValidThenValidationFailsAsync() public void WhenTrustedCACertificateIsNotYetValidThenUserCertValidationFailsAsync() { using var _ = DateTimeProvider.OverrideUtcNow(new DateTime(2018, 8, 17)); - Assert.ThrowsAsync(() => this.Validator - .Validate(this.ValidAuthToken, ValidChallengeNonce)) + Assert.ThrowsAsync(() => Validator + .Validate(ValidAuthToken, ValidChallengeNonce)) .WithMessage("User certificate is not yet valid"); } @@ -181,7 +181,7 @@ public void WhenTrustedCACertificateIsNotYetValidThenUserCertValidationFailsAsyn public void WhenUserCertificateIsNoLongerValidThenValidationFailsAsync() { using var _ = DateTimeProvider.OverrideUtcNow(new DateTime(2026, 10, 19)); - Assert.ThrowsAsync(() => this.Validator.Validate(this.ValidAuthToken, ValidChallengeNonce)) + Assert.ThrowsAsync(() => Validator.Validate(ValidAuthToken, ValidChallengeNonce)) .WithMessage("User certificate has expired"); } @@ -190,8 +190,8 @@ public void WhenUserCertificateIsNoLongerValidThenValidationFailsAsync() public void WhenTrustedCACertificateIsNoLongerValidThenUserCertValidationFailsAsync() { using var _ = DateTimeProvider.OverrideUtcNow(new DateTime(2033, 10, 19)); - Assert.ThrowsAsync(() => this.Validator - .Validate(this.ValidAuthToken, ValidChallengeNonce)) + Assert.ThrowsAsync(() => Validator + .Validate(ValidAuthToken, ValidChallengeNonce)) .WithMessage("User certificate has expired"); } @@ -201,7 +201,7 @@ public void WhenTrustedCACertificateIsNoLongerValidThenUserCertValidationFailsAs public void WhenUnrelatedCACertificateIsExpiredThenValidationSucceeds() { using var _ = DateTimeProvider.OverrideUtcNow(new DateTime(2024, 7, 1)); - var authToken = this.Validator.Parse(ValidUntil2026TokenStr); + var authToken = Validator.Parse(ValidUntil2026TokenStr); var validatorWithExpiredUnrelatedTrustedCA = AuthTokenValidators.GetAuthTokenValidatorWthJuly2024ExpiredUnrelatedTrustedCA(); Assert.DoesNotThrowAsync(() => validatorWithExpiredUnrelatedTrustedCA.Validate(authToken, ValidChallengeNonce)); @@ -212,7 +212,7 @@ public void WhenUnrelatedCACertificateIsExpiredThenValidationSucceeds() public void WhenCertificateIsRevokedThenOcspCheckFailsAsync() { var authTokenValidator = AuthTokenValidators.GetAuthTokenValidatorWithOcspCheck(); - var authToken = this.ReplaceTokenField(AuthToken, "X5C", RevokedCert); + var authToken = ReplaceTokenField(AuthToken, "X5C", RevokedCert); using var _ = DateTimeProvider.OverrideUtcNow(new DateTime(2019, 1, 1)); Assert.ThrowsAsync(() => authTokenValidator.Validate(authToken, ValidChallengeNonce)) .InnerException.IsInstanceOf(); @@ -223,7 +223,7 @@ public void WhenCertificateIsRevokedThenOcspCheckFailsAsync() public void WhenCertificateIsRevokedThenOcspCheckWithDesignatedServiceFailsAsync() { var authTokenValidator = AuthTokenValidators.GetAuthTokenValidatorWithDesignatedOcspCheck(); - var authToken = this.ReplaceTokenField(AuthToken, "X5C", RevokedCert); + var authToken = ReplaceTokenField(AuthToken, "X5C", RevokedCert); using var _ = DateTimeProvider.OverrideUtcNow(new DateTime(2019, 1, 1)); Assert.ThrowsAsync(() => authTokenValidator.Validate(authToken, ValidChallengeNonce)) .InnerException.IsInstanceOf(); @@ -233,7 +233,7 @@ public void WhenCertificateIsRevokedThenOcspCheckWithDesignatedServiceFailsAsync public void WhenCertificateCaIsNotPartOfTrustChainThenValidationFailsAsync() { var authTokenValidator = AuthTokenValidators.GetAuthTokenValidatorWithWrongTrustedCa(); - Assert.ThrowsAsync(() => authTokenValidator.Validate(this.ValidAuthToken, ValidChallengeNonce)); + Assert.ThrowsAsync(() => authTokenValidator.Validate(ValidAuthToken, ValidChallengeNonce)); } } } diff --git a/src/WebEid.Security.Tests/Validator/AuthTokenSignatureValidatorTests.cs b/src/WebEid.Security.Tests/Validator/AuthTokenSignatureValidatorTests.cs index fca7e48..d64865b 100644 --- a/src/WebEid.Security.Tests/Validator/AuthTokenSignatureValidatorTests.cs +++ b/src/WebEid.Security.Tests/Validator/AuthTokenSignatureValidatorTests.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright © 2020-2025 Estonian Information System Authority * * Permission is hereby granted, free of charge, to any person obtaining a copy @@ -175,7 +175,7 @@ public void WhenValidRS256V11SignatureThenValidationSucceeds() => var signatureValidator = new AuthTokenSignatureValidator(new Uri("https://ria.ee")); - signatureValidator.Validate( "RS256", publicKey, authToken.Signature, ValidChallengeNonce); + signatureValidator.Validate("RS256", publicKey, authToken.Signature, ValidChallengeNonce); }); diff --git a/src/WebEid.Security.Tests/Validator/AuthTokenStructureTest.cs b/src/WebEid.Security.Tests/Validator/AuthTokenStructureTest.cs index 2127906..26da385 100644 --- a/src/WebEid.Security.Tests/Validator/AuthTokenStructureTest.cs +++ b/src/WebEid.Security.Tests/Validator/AuthTokenStructureTest.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright © 2020-2025 Estonian Information System Authority * * Permission is hereby granted, free of charge, to any person obtaining a copy @@ -29,29 +29,29 @@ public class AuthTokenStructureTest : AbstractTestWithValidator { [Test] public void WhenNullTokenThenParsingFails() => - Assert.Throws(() => this.Validator.Parse(null)) + Assert.Throws(() => Validator.Parse(null)) .WithMessage("Auth token is null or too short"); [Test] public void WhenNullStrTokenThenParsingFails() => - Assert.Throws(() => this.Validator.Parse("null")) + Assert.Throws(() => Validator.Parse("null")) .WithMessage("Auth token is null or too short"); [Test] public void WhenTokenTooShortThenParsingFails() => - Assert.Throws(() => this.Validator.Parse(new string(new char[99]))) + Assert.Throws(() => Validator.Parse(new string(new char[99]))) .WithMessage("Auth token is null or too short"); [Test] public void WhenTokenTooLongThenParsingFails() => - Assert.Throws(() => this.Validator.Parse(new string(new char[10001]))) + Assert.Throws(() => Validator.Parse(new string(new char[10001]))) .WithMessage("Auth token is too long"); [Test] public void WhenUnknownTokenVersionThenParsingFailsAsync() { - var authToken = this.ReplaceTokenField(ValidAuthTokenStr, "web-eid:1", "invalid"); - Assert.ThrowsAsync(() => this.Validator.Validate(authToken, "")) + var authToken = ReplaceTokenField(ValidAuthTokenStr, "web-eid:1", "invalid"); + Assert.ThrowsAsync(() => Validator.Validate(authToken, "")) .WithMessage("Token format version 'invalid' is currently not supported"); } } diff --git a/src/WebEid.Security.Tests/Validator/AuthTokenValidationConfigurationTests.cs b/src/WebEid.Security.Tests/Validator/AuthTokenValidationConfigurationTests.cs index e44842b..8822046 100644 --- a/src/WebEid.Security.Tests/Validator/AuthTokenValidationConfigurationTests.cs +++ b/src/WebEid.Security.Tests/Validator/AuthTokenValidationConfigurationTests.cs @@ -22,14 +22,13 @@ namespace WebEid.Security.Tests.Validator { using System; - using System.Collections.Generic; + using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using NUnit.Framework; using Org.BouncyCastle.Security; using Security.Validator; using Security.Validator.Ocsp.Service; using TestUtils; - using X509Certificate = Org.BouncyCastle.X509.X509Certificate; [TestFixture] public class AuthTokenValidationConfigurationTests @@ -38,7 +37,7 @@ public class AuthTokenValidationConfigurationTests public void AuthTokenValidationConfigurationWithoutSiteOriginThrowsArgumentException() { var configuration = new AuthTokenValidationConfiguration(); - Assert.Throws(() => configuration.Validate()) + Assert.Throws(configuration.Validate) .WithMessage("Value cannot be null. (Parameter 'siteOrigin')"); } @@ -47,7 +46,7 @@ public void AuthTokenValidationConfigurationWithInvalidSiteOriginThrowsArgumentE { var configuration = new AuthTokenValidationConfiguration { SiteOrigin = new Uri("invalid", UriKind.RelativeOrAbsolute) }; - Assert.Throws(() => configuration.Validate()) + Assert.Throws(configuration.Validate) .WithMessage("Provided URI is not a valid URL"); } @@ -58,7 +57,7 @@ public void AuthTokenValidationConfigurationWithoutTrustedCertificatesThrowsArgu { SiteOrigin = new Uri("https://valid", UriKind.RelativeOrAbsolute), }; - Assert.Throws(() => configuration.Validate()) + Assert.Throws(configuration.Validate) .WithMessage("At least one trusted certificate authority must be provided"); } @@ -69,9 +68,9 @@ public void AuthTokenValidationConfigurationWithZeroOcspRequestTimeoutThrowsArgu { SiteOrigin = new Uri("https://valid", UriKind.RelativeOrAbsolute), }; - configuration.TrustedCaCertificates.Add(new X509Certificate2(Array.Empty())); + configuration.TrustedCaCertificates.Add(CreateTestCertificate()); configuration.OcspRequestTimeout = TimeSpan.Zero; - Assert.Throws(() => configuration.Validate()) + Assert.Throws(configuration.Validate) .WithMessage("OCSP request timeout must be greater than zero (Parameter 'timeSpan')"); } @@ -109,8 +108,8 @@ public void AuthTokenValidationConfigurationWithCorrectDataDoesNotThrowException { SiteOrigin = new Uri("https://valid", UriKind.RelativeOrAbsolute), }; - configuration.TrustedCaCertificates.Add(new X509Certificate2(Array.Empty())); - Assert.DoesNotThrow(() => configuration.Validate()); + configuration.TrustedCaCertificates.Add(CreateTestCertificate()); + Assert.DoesNotThrow(configuration.Validate); } [Test] @@ -119,21 +118,34 @@ public void AuthTokenValidationConfigurationCopyCopiesAllData() var configuration = new AuthTokenValidationConfiguration { SiteOrigin = new Uri("https://valid", UriKind.RelativeOrAbsolute), + OcspRequestTimeout = TimeSpan.FromMinutes(1) }; - configuration.OcspRequestTimeout = TimeSpan.FromMinutes(1); configuration.TrustedCaCertificates.Add(new X509Certificate2(Certificates.GetTestEsteid2015Ca())); configuration.TrustedCaCertificates.Add(new X509Certificate2(Certificates.GetTestEsteid2018Ca())); configuration.DesignatedOcspServiceConfiguration = new DesignatedOcspServiceConfiguration( new Uri("http://ocsp.ee"), DotNetUtilities.FromX509Certificate(Certificates.GetTestEsteid2018Ca()), - new List - { + [ DotNetUtilities.FromX509Certificate(Certificates.GetTestEsteid2015Ca()), DotNetUtilities.FromX509Certificate(Certificates.GetTestEsteid2018Ca()) - }); + ]); var copyOfConfiguration = configuration.Copy(); Assert.That(configuration, Is.EqualTo(copyOfConfiguration)); } + + private static X509Certificate2 CreateTestCertificate() + { + using var rsa = RSA.Create(2048); + var request = new CertificateRequest( + "CN=Test Certificate", + rsa, + HashAlgorithmName.SHA256, + RSASignaturePadding.Pkcs1); + + return request.CreateSelfSigned( + DateTimeOffset.UtcNow.AddDays(-1), + DateTimeOffset.UtcNow.AddDays(1)); + } } } diff --git a/src/WebEid.Security.Tests/Validator/AuthTokenValidatorBuilderTest.cs b/src/WebEid.Security.Tests/Validator/AuthTokenValidatorBuilderTest.cs index 73c3478..30ed539 100644 --- a/src/WebEid.Security.Tests/Validator/AuthTokenValidatorBuilderTest.cs +++ b/src/WebEid.Security.Tests/Validator/AuthTokenValidatorBuilderTest.cs @@ -22,26 +22,42 @@ namespace WebEid.Security.Tests.Validator { using System; + using System.Security.Cryptography; + using System.Security.Cryptography.X509Certificates; + using Microsoft.Extensions.Logging; + using Moq; using NUnit.Framework; using WebEid.Security.Tests.TestUtils; using WebEid.Security.Validator; + using WebEid.Security.Validator.Ocsp; + using WebEid.Security.Validator.Ocsp.Service; public class AuthTokenValidatorBuilderTest { private AuthTokenValidatorBuilder builder; + private Mock loggerMock; + private AuthTokenValidatorBuilder builderWithLogger; [SetUp] - public void SetUp() => this.builder = new AuthTokenValidatorBuilder(); + public void SetUp() + { + builder = new AuthTokenValidatorBuilder(); + + loggerMock = new Mock(); + loggerMock.Setup(x => x.IsEnabled(LogLevel.Debug)).Returns(true); + + builderWithLogger = new AuthTokenValidatorBuilder(loggerMock.Object); + } [Test] public void WhenOriginMissingThenBuildingFails() => - Assert.Throws(() => this.builder.Build()) + Assert.Throws(() => builder.Build()) .WithMessage("Value cannot be null. (Parameter 'siteOrigin')"); [Test] public void WhenRootCertificateAuthorityMissingThenBuildingFails() { - var authTokenValidatorBuilder = this.builder.WithSiteOrigin(new Uri("https://ria.ee")); + var authTokenValidatorBuilder = builder.WithSiteOrigin(new Uri("https://ria.ee")); Assert.Throws(() => authTokenValidatorBuilder.Build()) .WithMessage("At least one trusted certificate authority must be provided"); } @@ -49,7 +65,7 @@ public void WhenRootCertificateAuthorityMissingThenBuildingFails() [Test] public void WhenOriginNotUrlThenBuildingFails() { - var authTokenValidatorBuilder = this.builder.WithSiteOrigin(new Uri("c:/test")); + var authTokenValidatorBuilder = builder.WithSiteOrigin(new Uri("c:/test")); Assert.Throws(() => authTokenValidatorBuilder.Build()) .WithMessage("Invalid URI: The hostname could not be parsed."); } @@ -57,7 +73,7 @@ public void WhenOriginNotUrlThenBuildingFails() [Test] public void WhenOriginExcessiveElementsThenBuildingFails() { - var authTokenValidatorBuilder = this.builder.WithSiteOrigin(new Uri("https://ria.ee/excessive-element")); + var authTokenValidatorBuilder = builder.WithSiteOrigin(new Uri("https://ria.ee/excessive-element")); Assert.Throws(() => authTokenValidatorBuilder.Build()) .WithMessage("Origin URI must only contain the HTTPS scheme, host and optional port component"); } @@ -65,7 +81,7 @@ public void WhenOriginExcessiveElementsThenBuildingFails() [Test] public void WhenOriginProtocolHttpThenBuildingFails() { - var authTokenValidatorBuilder = this.builder.WithSiteOrigin(new Uri("http://ria.ee")); + var authTokenValidatorBuilder = builder.WithSiteOrigin(new Uri("http://ria.ee")); Assert.Throws(() => authTokenValidatorBuilder.Build()) .WithMessage("Origin URI must only contain the HTTPS scheme, host and optional port component"); } @@ -73,9 +89,296 @@ public void WhenOriginProtocolHttpThenBuildingFails() [Test] public void WhenOriginProtocolInvalidThenBuildingFails() { - var authTokenValidatorBuilder = this.builder.WithSiteOrigin(new Uri("ria://ria.ee")); + var authTokenValidatorBuilder = builder.WithSiteOrigin(new Uri("ria://ria.ee")); Assert.Throws(() => authTokenValidatorBuilder.Build()) .WithMessage("Invalid URI: Invalid port specified."); // Bug in Uri.CreateThis() } + + [Test] + public void WhenValidMandatoryConfigurationThenBuildingSucceeds() + { + var validator = builder + .WithSiteOrigin(new Uri("https://ria.ee")) + .WithTrustedCertificateAuthorities(CreateTestCertificate()) + .Build(); + + Assert.That(validator, Is.Not.Null); + } + + [Test] + public void WhenWithSiteOriginCalledThenSameBuilderIsReturned() + { + var result = builder.WithSiteOrigin(new Uri("https://ria.ee")); + + Assert.That(result, Is.SameAs(builder)); + } + + [Test] + public void WhenWithTrustedCertificateAuthoritiesCalledThenSameBuilderIsReturned() + { + var result = builder.WithTrustedCertificateAuthorities(CreateTestCertificate()); + + Assert.That(result, Is.SameAs(builder)); + } + + [Test] + public void WhenWithDisallowedCertificatePoliciesCalledThenSameBuilderIsReturned() + { + var result = builder.WithDisallowedCertificatePolicies("1.2.3.4.5"); + + Assert.That(result, Is.SameAs(builder)); + } + + [Test] + public void WhenWithoutUserCertificateRevocationCheckWithOcspCalledThenSameBuilderIsReturned() + { + var result = builder.WithoutUserCertificateRevocationCheckWithOcsp(); + + Assert.That(result, Is.SameAs(builder)); + } + + [Test] + public void WhenWithOcspRequestTimeoutCalledThenSameBuilderIsReturned() + { + var result = builder.WithOcspRequestTimeout(TimeSpan.FromSeconds(10)); + + Assert.That(result, Is.SameAs(builder)); + } + + [Test] + public void WhenWithAllowedOcspResponseTimeSkewCalledThenSameBuilderIsReturned() + { + var result = builder.WithAllowedOcspResponseTimeSkew(TimeSpan.FromMinutes(3)); + + Assert.That(result, Is.SameAs(builder)); + } + + [Test] + public void WhenWithMaxOcspResponseThisUpdateAgeCalledThenSameBuilderIsReturned() + { + var result = builder.WithMaxOcspResponseThisUpdateAge(TimeSpan.FromMinutes(1)); + + Assert.That(result, Is.SameAs(builder)); + } + + [Test] + public void WhenWithNonceDisabledOcspUrlsCalledThenSameBuilderIsReturned() + { + var result = builder.WithNonceDisabledOcspUrls(new Uri("https://ocsp.ria.ee")); + + Assert.That(result, Is.SameAs(builder)); + } + + [Test] + public void WhenWithDesignatedOcspServiceConfigurationCalledThenSameBuilderIsReturned() + { + var serviceConfiguration = new DesignatedOcspServiceConfiguration( + new Uri("https://ocsp.ria.ee"), + CreateOcspResponderCertificate(), + []); + + var result = builder.WithDesignatedOcspServiceConfiguration(serviceConfiguration); + + Assert.That(result, Is.SameAs(builder)); + } + + [Test] + public void WhenWithOcspClientCalledThenSameBuilderIsReturned() + { + var ocspClient = new Mock().Object; + + var result = builder.WithOcspClient(ocspClient); + + Assert.That(result, Is.SameAs(builder)); + } + + [Test] + public void WhenCustomOcspClientConfiguredThenBuildingSucceeds() + { + var ocspClient = new Mock().Object; + + var validator = builder + .WithSiteOrigin(new Uri("https://ria.ee")) + .WithTrustedCertificateAuthorities(CreateTestCertificate()) + .WithOcspClient(ocspClient) + .Build(); + + Assert.That(validator, Is.Not.Null); + } + + [Test] + public void WhenOcspRevocationCheckDisabledThenBuildingSucceedsWithoutCustomOcspClient() + { + var validator = builder + .WithSiteOrigin(new Uri("https://ria.ee")) + .WithTrustedCertificateAuthorities(CreateTestCertificate()) + .WithoutUserCertificateRevocationCheckWithOcsp() + .Build(); + + Assert.That(validator, Is.Not.Null); + } + + [Test] + public void WhenAllOptionalConfigurationMethodsUsedThenBuildingSucceeds() + { + var serviceConfiguration = new DesignatedOcspServiceConfiguration( + new Uri("https://ocsp.ria.ee"), + CreateOcspResponderCertificate(), + []); + + var validator = builder + .WithSiteOrigin(new Uri("https://ria.ee")) + .WithTrustedCertificateAuthorities(CreateTestCertificate()) + .WithDisallowedCertificatePolicies("1.2.3.4.5", "1.3.6.1.4.1") + .WithOcspRequestTimeout(TimeSpan.FromSeconds(7)) + .WithAllowedOcspResponseTimeSkew(TimeSpan.FromMinutes(2)) + .WithMaxOcspResponseThisUpdateAge(TimeSpan.FromMinutes(1)) + .WithNonceDisabledOcspUrls( + new Uri("https://ocsp.ria.ee"), + new Uri("https://aia.sk.ee/ocsp")) + .WithDesignatedOcspServiceConfiguration(serviceConfiguration) + .WithoutUserCertificateRevocationCheckWithOcsp() + .Build(); + + Assert.That(validator, Is.Not.Null); + } + + [Test] + public void WhenWithSiteOriginCalledWithDebugLoggerThenDebugLogIsWritten() + { + builderWithLogger.WithSiteOrigin(new Uri("https://ria.ee")); + + VerifyDebugLogWasWritten(Times.Once()); + } + + [Test] + public void WhenWithTrustedCertificateAuthoritiesCalledWithDebugLoggerThenDebugLogIsWritten() + { + builderWithLogger.WithTrustedCertificateAuthorities(CreateTestCertificate()); + + VerifyDebugLogWasWritten(Times.Once()); + } + + [Test] + public void WhenWithDisallowedCertificatePoliciesCalledWithDebugLoggerThenDebugLogIsWritten() + { + builderWithLogger.WithDisallowedCertificatePolicies("1.2.3.4.5"); + + VerifyDebugLogWasWritten(Times.Once()); + } + + [Test] + public void WhenWithoutUserCertificateRevocationCheckWithOcspCalledWithLoggerThenDebugLogIsWritten() + { + builderWithLogger.WithoutUserCertificateRevocationCheckWithOcsp(); + + VerifyDebugLogWasWritten(Times.Once()); + } + + [Test] + public void WhenWithOcspRequestTimeoutCalledWithDebugLoggerThenDebugLogIsWritten() + { + builderWithLogger.WithOcspRequestTimeout(TimeSpan.FromSeconds(10)); + + VerifyDebugLogWasWritten(Times.Once()); + } + + [Test] + public void WhenWithAllowedOcspResponseTimeSkewCalledWithDebugLoggerThenDebugLogIsWritten() + { + builderWithLogger.WithAllowedOcspResponseTimeSkew(TimeSpan.FromMinutes(3)); + + VerifyDebugLogWasWritten(Times.Once()); + } + + [Test] + public void WhenWithMaxOcspResponseThisUpdateAgeCalledWithDebugLoggerThenDebugLogIsWritten() + { + builderWithLogger.WithMaxOcspResponseThisUpdateAge(TimeSpan.FromMinutes(1)); + + VerifyDebugLogWasWritten(Times.Once()); + } + + [Test] + public void WhenWithNonceDisabledOcspUrlsCalledWithDebugLoggerThenDebugLogIsWritten() + { + builderWithLogger.WithNonceDisabledOcspUrls(new Uri("https://ocsp.ria.ee")); + + VerifyDebugLogWasWritten(Times.Once()); + } + + [Test] + public void WhenWithDesignatedOcspServiceConfigurationCalledWithLoggerThenDebugLogIsWritten() + { + var serviceConfiguration = new DesignatedOcspServiceConfiguration( + new Uri("https://ocsp.ria.ee"), + CreateOcspResponderCertificate(), + []); + + builderWithLogger.WithDesignatedOcspServiceConfiguration(serviceConfiguration); + + VerifyDebugLogWasWritten(Times.Once()); + } + + [Test] + public void WhenWithOcspClientCalledWithLoggerThenDebugLogIsWritten() + { + var ocspClient = new Mock().Object; + + builderWithLogger.WithOcspClient(ocspClient); + + VerifyDebugLogWasWritten(Times.Once()); + } + + private static X509Certificate2 CreateTestCertificate() + { + using var rsa = RSA.Create(2048); + var request = new CertificateRequest( + "CN=Test Certificate", + rsa, + HashAlgorithmName.SHA256, + RSASignaturePadding.Pkcs1); + + return request.CreateSelfSigned( + DateTimeOffset.UtcNow.AddDays(-1), + DateTimeOffset.UtcNow.AddDays(1)); + } + + private static Org.BouncyCastle.X509.X509Certificate CreateOcspResponderCertificate() + { + using var rsa = RSA.Create(2048); + var request = new CertificateRequest( + "CN=Test OCSP Responder", + rsa, + HashAlgorithmName.SHA256, + RSASignaturePadding.Pkcs1); + + var enhancedKeyUsages = new OidCollection + { + new Oid("1.3.6.1.5.5.7.3.9"), + }; + request.CertificateExtensions.Add( + new X509EnhancedKeyUsageExtension(enhancedKeyUsages, true)); + + request.CertificateExtensions.Add( + new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature, true)); + + using var certificate = request.CreateSelfSigned( + DateTimeOffset.UtcNow.AddDays(-1), + DateTimeOffset.UtcNow.AddDays(1)); + + return Org.BouncyCastle.Security.DotNetUtilities.FromX509Certificate(certificate); + } + +#pragma warning disable CA1873 + private void VerifyDebugLogWasWritten(Times times) => loggerMock.Verify( + x => x.Log( + It.Is(l => l == LogLevel.Debug), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny>()), + times); +#pragma warning restore CA1873 } } diff --git a/src/WebEid.Security.Tests/Validator/Ocsp/OcspClientTests.cs b/src/WebEid.Security.Tests/Validator/Ocsp/OcspClientTests.cs new file mode 100644 index 0000000..f681d75 --- /dev/null +++ b/src/WebEid.Security.Tests/Validator/Ocsp/OcspClientTests.cs @@ -0,0 +1,164 @@ +/* + * Copyright © 2025-2025 Estonian Information System Authority + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +namespace WebEid.Security.Tests.Validator.Ocsp +{ + using System; + using System.IO; + using System.Net; + using System.Net.Http; + using System.Net.Sockets; + using System.Text; + using System.Threading.Tasks; + using NUnit.Framework; + using Org.BouncyCastle.Ocsp; + using Security.Validator.Ocsp; + + [TestFixture] + public class OcspClientTests + { + private const string OcspResponseType = "application/ocsp-response"; + + [Test] + public void WhenHttpResponseIsNotSuccessfulThenThrowsHttpRequestException() + { + using var listener = CreateListener(out var uri); + var serverTask = RespondOnce(listener, HttpStatusCode.BadRequest, OcspResponseType, Encoding.UTF8.GetBytes("bad")); + + using var ocspClient = new OcspClient(TimeSpan.FromSeconds(5)); + var request = CreateOcspRequest(); + + Assert.ThrowsAsync(() => ocspClient.Request(uri, request)); + Assert.DoesNotThrowAsync(async () => await serverTask); + } + + [Test] + public void WhenResponseContentTypeIsInvalidThenThrowsIOException() + { + using var listener = CreateListener(out var uri); + var serverTask = RespondOnce(listener, HttpStatusCode.OK, "text/plain", Encoding.UTF8.GetBytes("not-ocsp")); + + using var ocspClient = new OcspClient(TimeSpan.FromSeconds(5)); + var request = CreateOcspRequest(); + + Assert.ThrowsAsync(() => ocspClient.Request(uri, request)); + Assert.DoesNotThrowAsync(async () => await serverTask); + } + + [Test] + public void WhenResponseContentTypeIsOcspThenAttemptsToParseResponse() + { + using var listener = CreateListener(out var uri); + var serverTask = RespondOnce(listener, HttpStatusCode.OK, OcspResponseType, Encoding.UTF8.GetBytes("not-a-valid-ocsp-response")); + + using var ocspClient = new OcspClient(TimeSpan.FromSeconds(5)); + var request = CreateOcspRequest(); + + Assert.ThrowsAsync(async () => _ = await ocspClient.Request(uri, request)); + + Assert.DoesNotThrowAsync(async () => await serverTask); + } + + [Test] + public void WhenResponseContentTypeIsNullThenAttemptsToParseResponse() + { + using var listener = CreateListener(out var uri); + var serverTask = RespondOnceWithoutContentType(listener, HttpStatusCode.OK, Encoding.UTF8.GetBytes("not-a-valid-ocsp-response")); + + using var ocspClient = new OcspClient(TimeSpan.FromSeconds(5)); + var request = CreateOcspRequest(); + + Assert.ThrowsAsync(async () => _ = await ocspClient.Request(uri, request)); + Assert.DoesNotThrowAsync(async () => await serverTask); + } + + [Test] + public void WhenDisposedThenDoesNotThrow() + { + var ocspClient = new OcspClient(TimeSpan.FromSeconds(5)); + + Assert.DoesNotThrow(ocspClient.Dispose); + } + + private static OcspReq CreateOcspRequest() + { + var generator = new OcspReqGenerator(); + return generator.Generate(); + } + + private static HttpListener CreateListener(out Uri uri) + { + var port = GetFreePort(); + var prefix = $"http://127.0.0.1:{port}/"; + var listener = new HttpListener(); + listener.Prefixes.Add(prefix); + listener.Start(); + uri = new Uri(prefix); + return listener; + } + + private static async Task RespondOnce(HttpListener listener, HttpStatusCode statusCode, string contentType, byte[] body) + { + try + { + var context = await listener.GetContextAsync(); + context.Response.StatusCode = (int)statusCode; + context.Response.ContentType = contentType; + context.Response.ContentLength64 = body.Length; + + await context.Response.OutputStream.WriteAsync(body); + context.Response.OutputStream.Close(); + context.Response.Close(); + } + finally + { + listener.Stop(); + } + } + + private static int GetFreePort() + { + var tcpListener = new TcpListener(IPAddress.Loopback, 0); + tcpListener.Start(); + var port = ((IPEndPoint)tcpListener.LocalEndpoint).Port; + tcpListener.Stop(); + return port; + } + + private static async Task RespondOnceWithoutContentType(HttpListener listener, HttpStatusCode statusCode, byte[] body) + { + try + { + var context = await listener.GetContextAsync(); + context.Response.StatusCode = (int)statusCode; + context.Response.ContentLength64 = body.Length; + + await context.Response.OutputStream.WriteAsync(body); + context.Response.OutputStream.Close(); + context.Response.Close(); + } + finally + { + listener.Stop(); + } + } + } +} diff --git a/src/WebEid.Security.Tests/Validator/Ocsp/OcspResponseValidatorTests.cs b/src/WebEid.Security.Tests/Validator/Ocsp/OcspResponseValidatorTests.cs index cdd73cc..7ed1a76 100644 --- a/src/WebEid.Security.Tests/Validator/Ocsp/OcspResponseValidatorTests.cs +++ b/src/WebEid.Security.Tests/Validator/Ocsp/OcspResponseValidatorTests.cs @@ -22,15 +22,15 @@ namespace WebEid.Security.Tests.Validator.Ocsp { using System; + using System.Globalization; using NUnit.Framework; + using Org.BouncyCastle.Asn1.Ocsp; using Org.BouncyCastle.Ocsp; using Security.Validator.Ocsp; - using WebEid.Security.Validator; - using Org.BouncyCastle.Asn1.Ocsp; - using System.Globalization; using WebEid.Security.Exceptions; using WebEid.Security.Tests.TestUtils; using WebEid.Security.Util; + using WebEid.Security.Validator; [TestFixture] public class OcspResponseValidatorTests diff --git a/src/WebEid.Security.Tests/Validator/Ocsp/OcspServiceProviderTests.cs b/src/WebEid.Security.Tests/Validator/Ocsp/OcspServiceProviderTests.cs index 7200eb0..67ea18f 100644 --- a/src/WebEid.Security.Tests/Validator/Ocsp/OcspServiceProviderTests.cs +++ b/src/WebEid.Security.Tests/Validator/Ocsp/OcspServiceProviderTests.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright © 2020-2024 Estonian Information System Authority * * Permission is hereby granted, free of charge, to any person obtaining a copy diff --git a/src/WebEid.Security.Tests/Validator/Ocsp/OcspUrlsTests.cs b/src/WebEid.Security.Tests/Validator/Ocsp/OcspUrlsTests.cs index 58b9d20..2841a98 100644 --- a/src/WebEid.Security.Tests/Validator/Ocsp/OcspUrlsTests.cs +++ b/src/WebEid.Security.Tests/Validator/Ocsp/OcspUrlsTests.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright © 2020-2024 Estonian Information System Authority * * Permission is hereby granted, free of charge, to any person obtaining a copy @@ -44,12 +44,12 @@ public void WhenExtensionValueIsInvalidThenReturnsNull() { var mockCertificate = new Mock(); _ = mockCertificate.Setup(x => x.GetExtensionValue(It.IsAny())) - .Returns(() => new DerOctetString(ToByteArray(new sbyte[] { 1, 2, 3 }))); + .Returns(() => new DerOctetString(ToByteArray([1, 2, 3]))); Assert.That(OcspUrls.GetOcspUri(mockCertificate.Object), Is.Null); } private static byte ToByte(sbyte sb) => (byte)(sb & 0xFF); - private static byte[] ToByteArray(sbyte[] sBytes) => sBytes.Select(ToByte).ToArray(); + private static byte[] ToByteArray(sbyte[] sBytes) => [.. sBytes.Select(ToByte)]; } } diff --git a/src/WebEid.Security.Tests/Validator/Validators/SubjectCertificateNotRevokedValidatorTests.cs b/src/WebEid.Security.Tests/Validator/Validators/SubjectCertificateNotRevokedValidatorTests.cs index 7d7c47a..b9e50ef 100644 --- a/src/WebEid.Security.Tests/Validator/Validators/SubjectCertificateNotRevokedValidatorTests.cs +++ b/src/WebEid.Security.Tests/Validator/Validators/SubjectCertificateNotRevokedValidatorTests.cs @@ -47,23 +47,23 @@ public sealed class SubjectCertificateNotRevokedValidatorTests : IDisposable private AuthTokenValidationConfiguration configuration; - public SubjectCertificateNotRevokedValidatorTests() => this.ocspClient = new OcspClient(TimeSpan.FromSeconds(5)); + public SubjectCertificateNotRevokedValidatorTests() => ocspClient = new OcspClient(TimeSpan.FromSeconds(5)); [SetUp] public void SetUp() { - this.trustedValidator = new SubjectCertificateTrustedValidator(null, null); - SetSubjectCertificateIssuerCertificate(this.trustedValidator); - this.esteid2018Cert = Certificates.GetJaakKristjanEsteid2018Cert(); - this.configuration = new AuthTokenValidationConfiguration(); + trustedValidator = new SubjectCertificateTrustedValidator(null, null); + SetSubjectCertificateIssuerCertificate(trustedValidator); + esteid2018Cert = Certificates.GetJaakKristjanEsteid2018Cert(); + configuration = new AuthTokenValidationConfiguration(); } [Test] public void WhenValidAiaOcspResponderConfigurationThenSucceeds() { - var validator = this.GetSubjectCertificateNotRevokedValidatorWithAiaOcsp(this.ocspClient); + var validator = GetSubjectCertificateNotRevokedValidatorWithAiaOcsp(ocspClient); Assert.DoesNotThrowAsync(() => - validator.Validate(this.esteid2018Cert)); + validator.Validate(esteid2018Cert)); } [Test] @@ -72,7 +72,7 @@ public void WhenValidDesignatedOcspResponderConfigurationThenSucceeds() { var ocspServiceProvider = OcspServiceMaker.GetDesignatedOcspServiceProvider(); var validator = GetSubjectCertificateNotRevokedValidatior(ocspServiceProvider); - Assert.DoesNotThrowAsync(() => validator.Validate(this.esteid2018Cert)); + Assert.DoesNotThrowAsync(() => validator.Validate(esteid2018Cert)); } [Test] @@ -81,7 +81,7 @@ public void WhenValidOcspNonceDisabledConfigurationThenSucceeds() { var ocspServiceProvider = OcspServiceMaker.GetDesignatedOcspServiceProvider(false); var validator = GetSubjectCertificateNotRevokedValidatior(ocspServiceProvider); - Assert.DoesNotThrowAsync(() => validator.Validate(this.esteid2018Cert)); + Assert.DoesNotThrowAsync(() => validator.Validate(esteid2018Cert)); } [Test] @@ -90,7 +90,7 @@ public void WhenOcspUrlIsInvalidThenThrows() var ocspServiceProvider = OcspServiceMaker.GetDesignatedOcspServiceProvider("http://invalid.invalid"); var validator = GetSubjectCertificateNotRevokedValidatior(ocspServiceProvider); Assert.ThrowsAsync(() => - validator.Validate(this.esteid2018Cert)) + validator.Validate(esteid2018Cert)) .InnerException.IsInstanceOf(); } @@ -101,7 +101,7 @@ public void WhenOcspRequestFailsThenThrows() OcspServiceMaker.GetDesignatedOcspServiceProvider("http://demo.sk.ee/ocsps"); var validator = GetSubjectCertificateNotRevokedValidatior(ocspServiceProvider); Assert.ThrowsAsync(() => - validator.Validate(this.esteid2018Cert)) + validator.Validate(esteid2018Cert)) .InnerException .HasMessageStartingWith("OCSP request was not successful, response: StatusCode: 404"); } @@ -109,9 +109,9 @@ public void WhenOcspRequestFailsThenThrows() [Test] public void WhenOcspRequestHasInvalidBodyThenThrows() { - var validator = this.GetSubjectCertificateNotRevokedValidatorWithAiaOcsp(new OcspClientMock("invalid")); + var validator = GetSubjectCertificateNotRevokedValidatorWithAiaOcsp(new OcspClientMock("invalid")); Assert.ThrowsAsync(() => - validator.Validate(this.esteid2018Cert)) + validator.Validate(esteid2018Cert)) .InnerException .HasMessage("corrupted stream - out of bounds length found: 110 >= 7"); } @@ -119,20 +119,20 @@ public void WhenOcspRequestHasInvalidBodyThenThrows() [Test] public void WhenOcspResponseIsNotSuccessfulThenThrows() { - var validator = this.GetSubjectCertificateNotRevokedValidatorWithAiaOcsp( + var validator = GetSubjectCertificateNotRevokedValidatorWithAiaOcsp( new OcspClientMock(BuildOcspResponseBodyWithInternalErrorStatus())); Assert.ThrowsAsync(() => - validator.Validate(this.esteid2018Cert)) + validator.Validate(esteid2018Cert)) .WithMessage("User certificate revocation check has failed: Response status: internal error"); } [Test] public void WhenOcspResponseHasInvalidCertificateIdThenThrows() { - var validator = this.GetSubjectCertificateNotRevokedValidatorWithAiaOcsp( + var validator = GetSubjectCertificateNotRevokedValidatorWithAiaOcsp( new OcspClientMock(BuildOcspResponseBodyWithInvalidCertificateId())); Assert.ThrowsAsync(() => - validator.Validate(this.esteid2018Cert)) + validator.Validate(esteid2018Cert)) .WithMessage( "User certificate revocation check has failed: OCSP responded with certificate ID that differs from the requested ID"); } @@ -140,20 +140,20 @@ public void WhenOcspResponseHasInvalidCertificateIdThenThrows() [Test] public void WhenOcspResponseHasInvalidSignatureThenThrows() { - var validator = this.GetSubjectCertificateNotRevokedValidatorWithAiaOcsp( + var validator = GetSubjectCertificateNotRevokedValidatorWithAiaOcsp( new OcspClientMock(BuildOcspResponseBodyWithInvalidSignature())); Assert.ThrowsAsync(() => - validator.Validate(this.esteid2018Cert)) + validator.Validate(esteid2018Cert)) .WithMessage("User certificate revocation check has failed: OCSP response signature is invalid"); } [Test] public void WhenOcspResponseHasInvalidResponderCertThenThrows() { - var validator = this.GetSubjectCertificateNotRevokedValidatorWithAiaOcsp( + var validator = GetSubjectCertificateNotRevokedValidatorWithAiaOcsp( new OcspClientMock(BuildOcspResponseBodyWithInvalidResponderCert())); Assert.ThrowsAsync(() => - validator.Validate(this.esteid2018Cert)) + validator.Validate(esteid2018Cert)) .InnerException .IsInstanceOf() .HasMessageStartingWith("corrupted stream - out of bounds length found: 322 >= 266"); @@ -162,10 +162,10 @@ public void WhenOcspResponseHasInvalidResponderCertThenThrows() [Test] public void WhenOcspResponseHasInvalidTagThenThrows() { - var validator = this.GetSubjectCertificateNotRevokedValidatorWithAiaOcsp( + var validator = GetSubjectCertificateNotRevokedValidatorWithAiaOcsp( new OcspClientMock(BuildOcspResponseBodyWithInvalidTag())); Assert.ThrowsAsync(() => - validator.Validate(this.esteid2018Cert)) + validator.Validate(esteid2018Cert)) .InnerException .IsInstanceOf() .HasMessage("problem decoding object: System.IO.IOException: unknown tag 23 encountered"); @@ -174,10 +174,10 @@ public void WhenOcspResponseHasInvalidTagThenThrows() [Test] public void WhenOcspResponseHas2CertResponsesThenThrows() { - var validator = this.GetSubjectCertificateNotRevokedValidatorWithAiaOcsp( + var validator = GetSubjectCertificateNotRevokedValidatorWithAiaOcsp( new OcspClientMock(Certificates.ResourceReader.ReadFromResource("ocsp_response_with_2_responses.der"))); Assert.ThrowsAsync(() => - validator.Validate(this.esteid2018Cert)) + validator.Validate(esteid2018Cert)) .WithMessage( "User certificate revocation check has failed: OCSP response must contain one response, received 2 responses instead"); } @@ -186,10 +186,10 @@ public void WhenOcspResponseHas2CertResponsesThenThrows() [Ignore("It is difficult to make Python and C# CertId equal, needs more work")] public void WhenOcspResponseHas2ResponderCertsThenThrows() { - var validator = this.GetSubjectCertificateNotRevokedValidatorWithAiaOcsp( + var validator = GetSubjectCertificateNotRevokedValidatorWithAiaOcsp( new OcspClientMock(Certificates.ResourceReader.ReadFromResource("ocsp_response_with_2_responder_certs.der"))); Assert.ThrowsAsync(() => - validator.Validate(this.esteid2018Cert)) + validator.Validate(esteid2018Cert)) .WithMessage( "User certificate revocation check has failed: OCSP response must contain one responder certificate, received 2 certificates instead"); } @@ -198,10 +198,10 @@ public void WhenOcspResponseHas2ResponderCertsThenThrows() public void WhenOcspResponseRevokedThenThrows() { using var _ = DateTimeProvider.OverrideUtcNow(new DateTime(2021, 9, 18)); - var validator = this.GetSubjectCertificateNotRevokedValidatorWithAiaOcsp( + var validator = GetSubjectCertificateNotRevokedValidatorWithAiaOcsp( new OcspClientMock(Certificates.ResourceReader.ReadFromResource("ocsp_response_revoked.der"))); Assert.ThrowsAsync(() => - validator.Validate(this.esteid2018Cert)) + validator.Validate(esteid2018Cert)) .InnerException.IsInstanceOf() .WithMessage("User certificate has been revoked: Revocation reason: 0"); } @@ -213,13 +213,13 @@ public void WhenOcspResponseUnknownThenThrows() var ocspServiceProvider = OcspServiceMaker.GetDesignatedOcspServiceProvider("https://web-eid-test.free.beeceptor.com"); var ocspClientMock = new OcspClientMock(Certificates.ResourceReader.ReadFromResource("ocsp_response_unknown.der")); var validator = new SubjectCertificateNotRevokedValidator( - this.trustedValidator, + trustedValidator, ocspClientMock, ocspServiceProvider, configuration.AllowedOcspResponseTimeSkew, configuration.MaxOcspResponseThisUpdateAge); Assert.ThrowsAsync(() => - validator.Validate(this.esteid2018Cert)) + validator.Validate(esteid2018Cert)) .InnerException.IsInstanceOf() .WithMessage("User certificate has been revoked: Unknown status"); } @@ -228,10 +228,10 @@ public void WhenOcspResponseUnknownThenThrows() public void WhenOcspResponseCaCertNotTrustedThenThrows() { using var _ = DateTimeProvider.OverrideUtcNow(new DateTime(2021, 3, 1)); - var validator = this.GetSubjectCertificateNotRevokedValidatorWithAiaOcsp( + var validator = GetSubjectCertificateNotRevokedValidatorWithAiaOcsp( new OcspClientMock(Certificates.ResourceReader.ReadFromResource("ocsp_response_unknown.der"))); var ex = Assert.ThrowsAsync(() => - validator.Validate(this.esteid2018Cert)); + validator.Validate(esteid2018Cert)); Assert.That(ex.InnerException, Is.TypeOf()); Assert.That(ex.InnerException.Message, Does.StartWith( @@ -242,10 +242,10 @@ public void WhenOcspResponseCaCertNotTrustedThenThrows() [Test] public void WhenOcspResponseCACertExpiredThenThrows() { - var validator = this.GetSubjectCertificateNotRevokedValidatorWithAiaOcsp( + var validator = GetSubjectCertificateNotRevokedValidatorWithAiaOcsp( new OcspClientMock(Certificates.ResourceReader.ReadFromResource("ocsp_response_unknown.der"))); var ex = Assert.ThrowsAsync(() => - validator.Validate(this.esteid2018Cert)); + validator.Validate(esteid2018Cert)); Assert.That(ex.InnerException, Is.TypeOf()); Assert.That(ex.InnerException.Message, Does.StartWith("AIA OCSP responder certificate has expired")); @@ -297,7 +297,7 @@ private static byte[] GetOcspResponseBytesFromResource() => Certificates.ResourceReader.ReadFromResource("ocsp_response.der"); private SubjectCertificateNotRevokedValidator GetSubjectCertificateNotRevokedValidatorWithAiaOcsp(IOcspClient client) => - new(this.trustedValidator, client, OcspServiceMaker.GetAiaOcspServiceProvider(), configuration.AllowedOcspResponseTimeSkew, configuration.MaxOcspResponseThisUpdateAge); + new(trustedValidator, client, OcspServiceMaker.GetAiaOcspServiceProvider(), configuration.AllowedOcspResponseTimeSkew, configuration.MaxOcspResponseThisUpdateAge); private static void SetSubjectCertificateIssuerCertificate(SubjectCertificateTrustedValidator trustedValidator) => SetPrivatePropertyValue(trustedValidator, "SubjectCertificateIssuerCertificate", new X509Certificate2(Certificates.GetTestEsteid2018Ca())); @@ -320,7 +320,7 @@ private static void SetPrivatePropertyValue(object obj, string propName, T va } private SubjectCertificateNotRevokedValidator GetSubjectCertificateNotRevokedValidatior(OcspServiceProvider ocspServiceProvider) - => new(this.trustedValidator, this.ocspClient, ocspServiceProvider, configuration.AllowedOcspResponseTimeSkew, configuration.MaxOcspResponseThisUpdateAge); + => new(trustedValidator, ocspClient, ocspServiceProvider, configuration.AllowedOcspResponseTimeSkew, configuration.MaxOcspResponseThisUpdateAge); private sealed class OcspClientMock : IOcspClient { @@ -334,11 +334,11 @@ public void Dispose() { } public async Task Request(Uri uri, OcspReq ocspReq) { - var contentBytes = await this.content.ReadAsByteArrayAsync(); + var contentBytes = await content.ReadAsByteArrayAsync(); return new OcspResp(contentBytes); } } - public void Dispose() => this.ocspClient?.Dispose(); + public void Dispose() => ocspClient?.Dispose(); } } diff --git a/src/WebEid.Security.Tests/Validator/VersionValidators/AuthTokenV11CertificateTest.cs b/src/WebEid.Security.Tests/Validator/VersionValidators/AuthTokenV11CertificateTest.cs index a336e49..bd7a82a 100644 --- a/src/WebEid.Security.Tests/Validator/VersionValidators/AuthTokenV11CertificateTest.cs +++ b/src/WebEid.Security.Tests/Validator/VersionValidators/AuthTokenV11CertificateTest.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright © 2025-2025 Estonian Information System Authority * * Permission is hereby granted, free of charge, to any person obtaining a copy @@ -22,13 +22,13 @@ namespace WebEid.Security.Tests.Validator.VersionValidators { using System; - using NUnit.Framework; - using Exceptions; - using WebEid.Security.Validator; using System.Text.Json; using System.Text.Json.Nodes; using AuthToken; + using Exceptions; + using NUnit.Framework; using TestUtils; + using WebEid.Security.Validator; public class AuthTokenV11CertificateTest : AbstractTestWithValidator { @@ -140,12 +140,12 @@ public void WhenV11SigningCertificateKeyUsageInvalidThenValidationFails() Validator.Validate(token, ValidChallengeNonce)); } + private static readonly JsonSerializerOptions DefaultJsonSerializerOptions = new() + { + PropertyNameCaseInsensitive = true + }; + private static WebEidAuthToken DeserializeToken(string json) => - JsonSerializer.Deserialize( - json, - new JsonSerializerOptions - { - PropertyNameCaseInsensitive = true - })!; + JsonSerializer.Deserialize(json, DefaultJsonSerializerOptions)!; } } diff --git a/src/WebEid.Security.Tests/Validator/VersionValidators/AuthTokenVersion11ValidatorTest.cs b/src/WebEid.Security.Tests/Validator/VersionValidators/AuthTokenVersion11ValidatorTest.cs index 9cdffd9..d59ab4c 100644 --- a/src/WebEid.Security.Tests/Validator/VersionValidators/AuthTokenVersion11ValidatorTest.cs +++ b/src/WebEid.Security.Tests/Validator/VersionValidators/AuthTokenVersion11ValidatorTest.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright © 2025-2025 Estonian Information System Authority * * Permission is hereby granted, free of charge, to any person obtaining a copy @@ -22,40 +22,41 @@ namespace WebEid.Security.Tests.Validator.VersionValidators { using System; - using System.Collections.Generic; using System.Security.Cryptography.X509Certificates; - using AuthToken; - using NUnit.Framework; using Exceptions; - using WebEid.Security.Validator; - using WebEid.Security.Validator.VersionValidators; - using WebEid.Security.Validator.Ocsp; using Microsoft.Extensions.Logging; using Moq; + using NUnit.Framework; using Security.Validator.CertValidators; using Security.Validator.Ocsp.Service; + using WebEid.Security.Tests.TestUtils; + using WebEid.Security.Validator; + using WebEid.Security.Validator.Ocsp; + using WebEid.Security.Validator.VersionValidators; - public class AuthTokenVersion11ValidatorTest + public class AuthTokenVersion11ValidatorTest : AbstractTestWithValidator { private SubjectCertificateValidatorBatch subjectValidators; private AuthTokenSignatureValidator signatureValidator; private AuthTokenValidationConfiguration configuration; private IOcspClient ocspClient; private OcspServiceProvider ocspServiceProvider; - private AuthTokenVersion11Validator validator; + private AuthTokenVersion11Validator v11Validator; [SetUp] - public void SetUp() + public new void SetUp() { + base.SetUp(); + subjectValidators = SubjectCertificateValidatorBatch.CreateFrom(); configuration = new AuthTokenValidationConfiguration { SiteOrigin = new Uri("https://example.com") }; - #pragma warning disable SYSLIB0026 +#pragma warning disable SYSLIB0026 configuration.TrustedCaCertificates.Add(new X509Certificate2()); - #pragma warning disable SYSLIB0026 +#pragma warning disable SYSLIB0026 signatureValidator = new Mock( new Uri("https://ria.ee") @@ -64,8 +65,8 @@ public void SetUp() ocspClient = new Mock().Object; var aiaConfig = new AiaOcspServiceConfiguration( - new List(), - new List() + [], + [] ); ocspServiceProvider = new OcspServiceProvider( @@ -73,23 +74,20 @@ public void SetUp() aiaOcspServiceConfiguration: aiaConfig ); - validator = new AuthTokenVersion11Validator( - subjectValidators, - signatureValidator, - configuration, - ocspClient, - ocspServiceProvider, - Mock.Of() - ); + v11Validator = new AuthTokenVersion11Validator( + subjectValidators, + signatureValidator, + configuration, + ocspClient, + ocspServiceProvider, + Mock.Of() + ); } [TestCase("web-eid:1.1")] [TestCase("web-eid:1.1.0")] [TestCase("web-eid:1.10")] - public void WhenFormatIsV11OrPrefixedVariantThenSupportsReturnsTrue(string format) - { - Assert.That(validator.Supports(format), Is.True); - } + public void WhenFormatIsV11OrPrefixedVariantThenSupportsReturnsTrue(string format) => Assert.That(v11Validator.Supports(format), Is.True); [TestCase(null)] [TestCase("")] @@ -97,42 +95,27 @@ public void WhenFormatIsV11OrPrefixedVariantThenSupportsReturnsTrue(string forma [TestCase("web-eid:1.0")] [TestCase("web-eid:2")] [TestCase("webauthn:1.1")] - public void WhenFormatIsNullEmptyOrNotV11ThenSupportsReturnsFalse(string format) - { - Assert.That(validator.Supports(format), Is.False); - } + public void WhenFormatIsNullEmptyOrNotV11ThenSupportsReturnsFalse(string format) => Assert.That(v11Validator.Supports(format), Is.False); [Test] public void WhenUnverifiedSigningCertificatesMissingThenValidationFails() { - var token = new WebEidAuthToken - { - Format = "web-eid:1.1", - UnverifiedSigningCertificates = null - }; + var token = Validator.Parse(ValidV11AuthTokenStr); + token.UnverifiedSigningCertificates = null; - Assert.ThrowsAsync(() => - validator.Validate(token, "nonce")); + var ex = Assert.ThrowsAsync(() => Validator.Validate(token, ValidChallengeNonce)); + Assert.That(ex!.Message, Is.EqualTo( + "'unverifiedSigningCertificates' field is missing, null or empty for format 'web-eid:1.1'")); } [Test] public void WhenSupportedSignatureAlgorithmsMissingThenValidationFails() { - var token = new WebEidAuthToken - { - Format = "web-eid:1.1", - UnverifiedSigningCertificates = new List - { - new UnverifiedSigningCertificate - { - Certificate = "ABC123", - SupportedSignatureAlgorithms = null - } - } - }; + var token = Validator.Parse(ValidV11AuthTokenStr); + token.UnverifiedSigningCertificates[0].SupportedSignatureAlgorithms = null; - Assert.ThrowsAsync(() => - validator.Validate(token, "nonce")); + var ex = Assert.ThrowsAsync(() => Validator.Validate(token, ValidChallengeNonce)); + Assert.That(ex!.Message, Is.EqualTo("'supportedSignatureAlgorithms' field is missing")); } } } diff --git a/src/WebEid.Security.Tests/Validator/VersionValidators/AuthTokenVersion1ValidatorTest.cs b/src/WebEid.Security.Tests/Validator/VersionValidators/AuthTokenVersion1ValidatorTest.cs index 1281012..02fabb4 100644 --- a/src/WebEid.Security.Tests/Validator/VersionValidators/AuthTokenVersion1ValidatorTest.cs +++ b/src/WebEid.Security.Tests/Validator/VersionValidators/AuthTokenVersion1ValidatorTest.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright © 2025-2025 Estonian Information System Authority * * Permission is hereby granted, free of charge, to any person obtaining a copy @@ -22,18 +22,17 @@ namespace WebEid.Security.Tests.Validator.VersionValidators { using System; - using System.Collections.Generic; using System.Security.Cryptography.X509Certificates; using AuthToken; - using NUnit.Framework; using Exceptions; + using Microsoft.Extensions.Logging; + using Moq; + using NUnit.Framework; using WebEid.Security.Validator; - using WebEid.Security.Validator.VersionValidators; using WebEid.Security.Validator.CertValidators; using WebEid.Security.Validator.Ocsp; using WebEid.Security.Validator.Ocsp.Service; - using Microsoft.Extensions.Logging; - using Moq; + using WebEid.Security.Validator.VersionValidators; public class AuthTokenVersion1ValidatorTest { @@ -53,9 +52,9 @@ public void SetUp() { SiteOrigin = new Uri("https://example.com") }; - #pragma warning disable SYSLIB0026 +#pragma warning disable SYSLIB0026 configuration.TrustedCaCertificates.Add(new X509Certificate2()); - #pragma warning disable SYSLIB0026 +#pragma warning disable SYSLIB0026 signatureValidator = new Mock( new Uri("https://ria.ee") @@ -64,8 +63,8 @@ public void SetUp() ocspClient = new Mock().Object; var aiaConfig = new AiaOcspServiceConfiguration( - new List(), - new List() + [], + [] ); ocspServiceProvider = new OcspServiceProvider( @@ -87,10 +86,7 @@ public void SetUp() [TestCase("web-eid:1.0")] [TestCase("web-eid:1.1")] [TestCase("web-eid:1.10")] - public void WhenFormatIsAnyMajorV1VariantThenSupportsReturnsTrue(string format) - { - Assert.That(validator.Supports(format), Is.True); - } + public void WhenFormatIsAnyMajorV1VariantThenSupportsReturnsTrue(string format) => Assert.That(validator.Supports(format), Is.True); [TestCase(null)] [TestCase("")] @@ -98,10 +94,7 @@ public void WhenFormatIsAnyMajorV1VariantThenSupportsReturnsTrue(string format) [TestCase("web-eid:0.9")] [TestCase("web-eid:2")] [TestCase("webauthn:1")] - public void WhenFormatIsNullEmptyOrNotV1ThenSupportsReturnsFalse(string format) - { - Assert.That(validator.Supports(format), Is.False); - } + public void WhenFormatIsNullEmptyOrNotV1ThenSupportsReturnsFalse(string format) => Assert.That(validator.Supports(format), Is.False); [Test] public void WhenUnverifiedCertificateMissingThenValidationFails() @@ -125,10 +118,10 @@ public void WhenUnverifiedSigningCertificatesPresentForV1ThenValidationFails() var token = new WebEidAuthToken { Format = "web-eid:1", - UnverifiedSigningCertificates = new List - { - new UnverifiedSigningCertificate() - } + UnverifiedSigningCertificates = + [ + new() + ] }; var ex = Assert.ThrowsAsync(() => diff --git a/src/WebEid.Security.Tests/Validator/VersionValidators/AuthTokenVersionValidatorFactoryTest.cs b/src/WebEid.Security.Tests/Validator/VersionValidators/AuthTokenVersionValidatorFactoryTest.cs index 398fbd4..c83cb4c 100644 --- a/src/WebEid.Security.Tests/Validator/VersionValidators/AuthTokenVersionValidatorFactoryTest.cs +++ b/src/WebEid.Security.Tests/Validator/VersionValidators/AuthTokenVersionValidatorFactoryTest.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright © 2025-2025 Estonian Information System Authority * * Permission is hereby granted, free of charge, to any person obtaining a copy @@ -21,10 +21,9 @@ */ namespace WebEid.Security.Tests.Validator.VersionValidators { - using System.Collections.Generic; - using NUnit.Framework; - using Moq; using Exceptions; + using Moq; + using NUnit.Framework; using WebEid.Security.Validator.VersionValidators; public class AuthTokenVersionValidatorFactoryTest @@ -36,7 +35,7 @@ public void WhenValidatorSupportsFormatThenSupportsReturnsTrue() v11.Setup(v => v.Supports("web-eid:1.1")).Returns(true); var factory = new AuthTokenVersionValidatorFactory( - new List { v11.Object } + [v11.Object] ); Assert.That(factory.Supports("web-eid:1.1"), Is.True); @@ -49,7 +48,7 @@ public void WhenValidatorDoesNotSupportFormatThenSupportsReturnsFalse() v11.Setup(v => v.Supports("web-eid:1.1")).Returns(false); var factory = new AuthTokenVersionValidatorFactory( - new List { v11.Object } + [v11.Object] ); Assert.That(factory.Supports("web-eid:2"), Is.False); @@ -62,7 +61,7 @@ public void WhenValidatorDoesNotSupportFormatThenSupportsReturnsFalse() [TestCase("web-eid")] public void WhenUnsupportedFormatThenGetValidatorForThrows(string format) { - var factory = new AuthTokenVersionValidatorFactory(new List()); + var factory = new AuthTokenVersionValidatorFactory([]); Assert.Throws(() => factory.GetValidatorFor(format) @@ -79,7 +78,7 @@ public void WhenMultipleValidatorsAndFirstIsV11ThenGetValidatorForReturnsV11() v1.Setup(v => v.Supports("web-eid:1")).Returns(true); var factory = new AuthTokenVersionValidatorFactory( - new List { v11.Object, v1.Object } + [v11.Object, v1.Object] ); var chosen = factory.GetValidatorFor("web-eid:1.1"); @@ -97,7 +96,7 @@ public void WhenFormatIsBaseV1ThenGetValidatorForReturnsV1() v1.Setup(v => v.Supports("web-eid:1")).Returns(true); var factory = new AuthTokenVersionValidatorFactory( - new List { v11.Object, v1.Object } + [v11.Object, v1.Object] ); var chosen = factory.GetValidatorFor("web-eid:1"); diff --git a/src/WebEid.Security/AuthToken/UnverifiedSigningCertificate.cs b/src/WebEid.Security/AuthToken/UnverifiedSigningCertificate.cs index a69c9e8..63d2c8f 100644 --- a/src/WebEid.Security/AuthToken/UnverifiedSigningCertificate.cs +++ b/src/WebEid.Security/AuthToken/UnverifiedSigningCertificate.cs @@ -22,7 +22,6 @@ namespace WebEid.Security.AuthToken { using System.Collections.Generic; - using System.Text.Json.Serialization; /// /// Unverified signing certificate and its supported signature algorithms. diff --git a/src/WebEid.Security/Challenge/ChallengeNonce.cs b/src/WebEid.Security/Challenge/ChallengeNonce.cs index 7848b40..9608825 100644 --- a/src/WebEid.Security/Challenge/ChallengeNonce.cs +++ b/src/WebEid.Security/Challenge/ChallengeNonce.cs @@ -26,28 +26,22 @@ namespace WebEid.Security.Challenge /// /// Represents a challenge nonce used in the Web eID system. /// - public class ChallengeNonce + /// + /// Initializes a new instance of the class. + /// + /// The base64-encoded value of the nonce. + /// The expiration time for the nonce. + /// Thrown when the provided base64EncodedNonce is null. + public class ChallengeNonce(string base64EncodedNonce, DateTime expirationTime) { /// /// The base64-encoded value of the nonce. /// - public string Base64EncodedNonce { get; private set; } + public string Base64EncodedNonce { get; private set; } = base64EncodedNonce ?? throw new ArgumentNullException(nameof(base64EncodedNonce)); /// /// The expiration time for the nonce. /// - public DateTime ExpirationTime { get; private set; } - - /// - /// Initializes a new instance of the class. - /// - /// The base64-encoded value of the nonce. - /// The expiration time for the nonce. - /// Thrown when the provided base64EncodedNonce is null. - public ChallengeNonce(string base64EncodedNonce, DateTime expirationTime) - { - this.Base64EncodedNonce = base64EncodedNonce ?? throw new ArgumentNullException(nameof(base64EncodedNonce)); - this.ExpirationTime = expirationTime; - } + public DateTime ExpirationTime { get; private set; } = expirationTime; } } diff --git a/src/WebEid.Security/Challenge/ChallengeNonceGenerator.cs b/src/WebEid.Security/Challenge/ChallengeNonceGenerator.cs index 1af948e..3a94183 100644 --- a/src/WebEid.Security/Challenge/ChallengeNonceGenerator.cs +++ b/src/WebEid.Security/Challenge/ChallengeNonceGenerator.cs @@ -28,22 +28,16 @@ namespace WebEid.Security.Challenge /// /// Generates and stores cryptographic nonces for the Web eID system. /// - public sealed class ChallengeNonceGenerator : IChallengeNonceGenerator + /// + /// Initializes a new instance of the class. + /// + /// The source of random bytes for generating nonces. + /// The store where generated nonce values will be stored. + /// Thrown when either randomNumberGenerator or store is null. + public sealed class ChallengeNonceGenerator(RandomNumberGenerator randomNumberGenerator, IChallengeNonceStore store) : IChallengeNonceGenerator { - private readonly IChallengeNonceStore store; - private readonly RandomNumberGenerator randomNumberGenerator; - - /// - /// Initializes a new instance of the class. - /// - /// The source of random bytes for generating nonces. - /// The store where generated nonce values will be stored. - /// Thrown when either randomNumberGenerator or store is null. - public ChallengeNonceGenerator(RandomNumberGenerator randomNumberGenerator, IChallengeNonceStore store) - { - this.randomNumberGenerator = randomNumberGenerator ?? throw new ArgumentNullException(nameof(randomNumberGenerator), "Secure random generator must not be null"); - this.store = store ?? throw new ArgumentNullException(nameof(store), "Challenge nonce store must not be null"); - } + private readonly IChallengeNonceStore store = store ?? throw new ArgumentNullException(nameof(store), "Challenge nonce store must not be null"); + private readonly RandomNumberGenerator randomNumberGenerator = randomNumberGenerator ?? throw new ArgumentNullException(nameof(randomNumberGenerator), "Secure random generator must not be null"); /// /// Generates a cryptographic nonce, a large random number that can be used only once, @@ -60,11 +54,11 @@ public ChallengeNonce GenerateAndStoreNonce(TimeSpan ttl) } var nonceBytes = new byte[IChallengeNonceGenerator.NonceLength]; - this.randomNumberGenerator.GetBytes(nonceBytes); + randomNumberGenerator.GetBytes(nonceBytes); var base64StringNonce = Convert.ToBase64String(nonceBytes); var expirationTime = DateTimeProvider.UtcNow.Add(ttl); var challengeNonce = new ChallengeNonce(base64StringNonce, expirationTime); - this.store.Put(challengeNonce); + store.Put(challengeNonce); return challengeNonce; } } diff --git a/src/WebEid.Security/Challenge/IChallengeNonceGenerator.cs b/src/WebEid.Security/Challenge/IChallengeNonceGenerator.cs index d3096bb..26bed82 100644 --- a/src/WebEid.Security/Challenge/IChallengeNonceGenerator.cs +++ b/src/WebEid.Security/Challenge/IChallengeNonceGenerator.cs @@ -31,7 +31,7 @@ public interface IChallengeNonceGenerator /// /// Challenge Nonce length in bytes. /// - const int NonceLength = 32; + public const int NonceLength = 32; /// /// Generates a cryptographic nonce, a large random number that can be used only once, @@ -39,6 +39,6 @@ public interface IChallengeNonceGenerator /// /// Challenge nonce time-to-live duration. When the time-to-live passes, the nonce is considered to be expired. /// a ChallengeNonce that contains the Base64-encoded nonce and its expiry time - ChallengeNonce GenerateAndStoreNonce(TimeSpan ttl); + public ChallengeNonce GenerateAndStoreNonce(TimeSpan ttl); } } diff --git a/src/WebEid.Security/Challenge/IChallengeNonceStore.cs b/src/WebEid.Security/Challenge/IChallengeNonceStore.cs index 246c6ec..e938453 100644 --- a/src/WebEid.Security/Challenge/IChallengeNonceStore.cs +++ b/src/WebEid.Security/Challenge/IChallengeNonceStore.cs @@ -33,13 +33,15 @@ public interface IChallengeNonceStore /// Inserts given object into the store. /// /// The nonce object to be stored. - void Put(ChallengeNonce challengeNonce); + public void Put(ChallengeNonce challengeNonce); +#pragma warning disable CA1711 /// /// Internal implementation of method. /// /// Stored object. - ChallengeNonce GetAndRemoveImpl(); + public ChallengeNonce GetAndRemoveImpl(); +#pragma warning restore CA1711 /// /// Removes and returns the object being stored. @@ -53,7 +55,7 @@ public interface IChallengeNonceStore /// If there is no Challenge Nonce stored, either no method was not called or it was removed previously /// due expiration then ChallengeNonceNotFoundException exception is thrown. /// - ChallengeNonce GetAndRemove() + public ChallengeNonce GetAndRemove() { var challengeNonce = GetAndRemoveImpl() ?? throw new ChallengeNonceNotFoundException(); diff --git a/src/WebEid.Security/Exceptions/AuthTokenException.cs b/src/WebEid.Security/Exceptions/AuthTokenException.cs index 7fb766e..cc22cfe 100644 --- a/src/WebEid.Security/Exceptions/AuthTokenException.cs +++ b/src/WebEid.Security/Exceptions/AuthTokenException.cs @@ -53,7 +53,9 @@ protected AuthTokenException(string msg, Exception innerException) : base(msg, i /// /// The that holds the serialized object data. /// The that contains contextual information about the source or destination. +#pragma warning disable SYSLIB0051 [ExcludeFromCodeCoverage] protected AuthTokenException(SerializationInfo info, StreamingContext context) : base(info, context) { } +#pragma warning restore SYSLIB0051 } } diff --git a/src/WebEid.Security/Util/CertificateLoader.cs b/src/WebEid.Security/Util/CertificateLoader.cs index 3dfe2e7..51d1050 100644 --- a/src/WebEid.Security/Util/CertificateLoader.cs +++ b/src/WebEid.Security/Util/CertificateLoader.cs @@ -28,15 +28,13 @@ namespace WebEid.Security.Util /// /// Provides functionality for loading X.509 certificates from resources or base64-encoded strings. /// - public class CertificateLoader + /// + /// Initializes a new instance of the class. + /// + /// The resource reader used to load certificates from resources. + public class CertificateLoader(ResourceReader resourceReader) { - private readonly ResourceReader resourceReader; - - /// - /// Initializes a new instance of the class. - /// - /// The resource reader used to load certificates from resources. - public CertificateLoader(ResourceReader resourceReader) => this.resourceReader = resourceReader; + private readonly ResourceReader resourceReader = resourceReader; /// /// Loads X.509 certificates from resources. @@ -44,8 +42,8 @@ public class CertificateLoader /// The names of the resources containing certificates. /// An array of loaded X.509 certificates. public X509Certificate2[] LoadCertificatesFromResources(params string[] resourceNames) => - resourceNames?.Select(resourceName => this.LoadCertificateFromResource(resourceName)).ToArray() ?? - Array.Empty(); + resourceNames?.Select(LoadCertificateFromResource).ToArray() ?? + []; /// /// Loads an X.509 certificate from a resource. @@ -53,7 +51,7 @@ public X509Certificate2[] LoadCertificatesFromResources(params string[] resource /// The name of the resource containing the certificate. /// The loaded X.509 certificate. public X509Certificate2 LoadCertificateFromResource(string resourceName) => - new X509Certificate2(this.resourceReader.ReadFromResource(resourceName)); + X509CertificateLoader.LoadCertificate(resourceReader.ReadFromResource(resourceName)); /// /// Loads an X.509 certificate from a base64-encoded string. @@ -61,6 +59,6 @@ public X509Certificate2 LoadCertificateFromResource(string resourceName) => /// The base64-encoded certificate string. /// The loaded X.509 certificate. public static X509Certificate2 LoadCertificateFromBase64String(string certificate) => - new X509Certificate2(Convert.FromBase64String(certificate)); + X509CertificateLoader.LoadCertificate(Convert.FromBase64String(certificate)); } } diff --git a/src/WebEid.Security/Util/ResourceReader.cs b/src/WebEid.Security/Util/ResourceReader.cs index 1af1ae4..1862182 100644 --- a/src/WebEid.Security/Util/ResourceReader.cs +++ b/src/WebEid.Security/Util/ResourceReader.cs @@ -42,7 +42,7 @@ public ResourceReader(Assembly assembly, string assemblyNamespace) { this.assembly = assembly; this.assemblyNamespace = assemblyNamespace; - if (!this.assemblyNamespace.EndsWith(".", StringComparison.InvariantCulture)) + if (!this.assemblyNamespace.EndsWith('.')) { this.assemblyNamespace += "."; } @@ -56,11 +56,11 @@ public ResourceReader(Assembly assembly, string assemblyNamespace) /// Thrown when the resource is not found. public byte[] ReadFromResource(string filename) { - using var stream = this.GetStream(filename) ?? throw new ArgumentException($"Unable to find resource: {this.assemblyNamespace + filename}"); + using var stream = GetStream(filename) ?? throw new ArgumentException($"Unable to find resource: {assemblyNamespace + filename}"); return stream.ToByteArray(); } private Stream GetStream(string filename) => - this.assembly.GetManifestResourceStream(this.assemblyNamespace + filename); + assembly.GetManifestResourceStream(assemblyNamespace + filename); } } diff --git a/src/WebEid.Security/Util/StreamExtensions.cs b/src/WebEid.Security/Util/StreamExtensions.cs index 686d69f..cb449eb 100644 --- a/src/WebEid.Security/Util/StreamExtensions.cs +++ b/src/WebEid.Security/Util/StreamExtensions.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright © 2020-2024 Estonian Information System Authority * * Permission is hereby granted, free of charge, to any person obtaining a copy diff --git a/src/WebEid.Security/Util/X509Certificate2Extensions.cs b/src/WebEid.Security/Util/X509Certificate2Extensions.cs index 5f19a4f..203d6e5 100644 --- a/src/WebEid.Security/Util/X509Certificate2Extensions.cs +++ b/src/WebEid.Security/Util/X509Certificate2Extensions.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright © 2025-2025 Estonian Information System Authority * * Permission is hereby granted, free of charge, to any person obtaining a copy diff --git a/src/WebEid.Security/Util/X509CertificateExtensions.cs b/src/WebEid.Security/Util/X509CertificateExtensions.cs index b5fe1f9..6c59b4c 100644 --- a/src/WebEid.Security/Util/X509CertificateExtensions.cs +++ b/src/WebEid.Security/Util/X509CertificateExtensions.cs @@ -129,7 +129,7 @@ public static X509Certificate2 ValidateIsValidAndSignedByTrustedCa(this X509Cert return chainElement.Certificate; } - catch (Exception ex) when (!(ex is CertificateNotTrustedException)) + catch (Exception ex) when (ex is not CertificateNotTrustedException) { throw new CertificateNotTrustedException(certificate, ex); } @@ -147,7 +147,7 @@ public static X509Certificate2 ParseCertificate(string certificateInBase64, stri try { var certificateBytes = Convert.FromBase64String(certificateInBase64); - return new X509Certificate2(certificateBytes); + return X509CertificateLoader.LoadCertificate(certificateBytes); } catch (Exception ex) { diff --git a/src/WebEid.Security/Validator/AuthTokenSignatureValidator.cs b/src/WebEid.Security/Validator/AuthTokenSignatureValidator.cs index 737e4eb..411da72 100644 --- a/src/WebEid.Security/Validator/AuthTokenSignatureValidator.cs +++ b/src/WebEid.Security/Validator/AuthTokenSignatureValidator.cs @@ -23,7 +23,6 @@ namespace WebEid.Security.Validator { using System; using System.Collections.Generic; - using System.Collections.ObjectModel; using System.Linq; using System.Security.Cryptography; using System.Text; @@ -33,10 +32,14 @@ namespace WebEid.Security.Validator /// /// Provides functionality for validating the signature of an authentication token (JWT) using a specified algorithm and a public key. /// - public class AuthTokenSignatureValidator + /// + /// Initializes a new instance of the class. + /// + /// The site origin (as a Uri). + public class AuthTokenSignatureValidator(Uri siteOrigin) { - private static readonly ICollection SupportedSigningAlgorithms = new Collection - { + private static readonly ICollection SupportedSigningAlgorithms = + [ SecurityAlgorithms.EcdsaSha256, SecurityAlgorithms.EcdsaSha384, SecurityAlgorithms.EcdsaSha512, @@ -46,16 +49,9 @@ public class AuthTokenSignatureValidator SecurityAlgorithms.RsaSsaPssSha256, SecurityAlgorithms.RsaSsaPssSha384, SecurityAlgorithms.RsaSsaPssSha512 - }; - - private readonly byte[] originBytes; + ]; - /// - /// Initializes a new instance of the class. - /// - /// The site origin (as a Uri). - public AuthTokenSignatureValidator(Uri siteOrigin) => - this.originBytes = Encoding.UTF8.GetBytes(siteOrigin.OriginalString); + private readonly byte[] originBytes = Encoding.UTF8.GetBytes(siteOrigin.OriginalString); /// /// Validates the signature of an authentication token. @@ -68,8 +64,7 @@ public void Validate(string algorithm, SecurityKey publicKey, string tokenSignat { if (string.IsNullOrEmpty(currentNonce)) { throw new ArgumentNullException(nameof(currentNonce)); } - if (publicKey == null) - { throw new ArgumentNullException(nameof(publicKey)); } + ArgumentNullException.ThrowIfNull(publicKey); RequireNotEmpty(algorithm, nameof(algorithm)); RequireNotEmpty(tokenSignature, nameof(tokenSignature)); @@ -80,7 +75,7 @@ public void Validate(string algorithm, SecurityKey publicKey, string tokenSignat var decodedSignature = Base64UrlEncoder.DecodeBytes(tokenSignature); - var originHash = hashAlgorithm.ComputeHash(this.originBytes); + var originHash = hashAlgorithm.ComputeHash(originBytes); var nonceHash = hashAlgorithm.ComputeHash(Encoding.UTF8.GetBytes(currentNonce)); var concatSignedFields = originHash.Concat(nonceHash).ToArray(); @@ -107,7 +102,7 @@ private static void RequireNotEmpty(string algorithm, string fieldName) private static void ValidateIfAlgorithmIsSupported(string algorithmName, CryptoProviderFactory cryptoProviderFactory, SecurityKey publicKey) { if (string.IsNullOrWhiteSpace(algorithmName) || - !SupportedSigningAlgorithms.Any(alg => alg.Equals(algorithmName, StringComparison.InvariantCultureIgnoreCase)) || + !SupportedSigningAlgorithms.Any(alg => alg.Equals(algorithmName, StringComparison.OrdinalIgnoreCase)) || !cryptoProviderFactory.IsSupportedAlgorithm(algorithmName, publicKey)) { throw new AuthTokenParseException("Unsupported signature algorithm"); @@ -116,11 +111,22 @@ private static void ValidateIfAlgorithmIsSupported(string algorithmName, CryptoP private static HashAlgorithm HashAlgorithmForName(string algorithmName) { - var hashAlgorithmName = "SHA" + algorithmName[^3..]; - var hashAlgorithm = HashAlgorithm.Create(hashAlgorithmName); - if (hashAlgorithm == null) // Should not happen as ValidateIfAlgorithmIsSupported() should assure correct algorithm name. - { throw new NotSupportedException($"Unsupported hash algorithm '{hashAlgorithmName}'"); } - return hashAlgorithm; + if (algorithmName.EndsWith("256", StringComparison.Ordinal)) + { + return SHA256.Create(); + } + + if (algorithmName.EndsWith("384", StringComparison.Ordinal)) + { + return SHA384.Create(); + } + + if (algorithmName.EndsWith("512", StringComparison.Ordinal)) + { + return SHA512.Create(); + } + + throw new NotSupportedException($"Unsupported hash algorithm '{algorithmName}'"); } } } diff --git a/src/WebEid.Security/Validator/AuthTokenValidationConfiguration.cs b/src/WebEid.Security/Validator/AuthTokenValidationConfiguration.cs index 8decf17..0045c66 100644 --- a/src/WebEid.Security/Validator/AuthTokenValidationConfiguration.cs +++ b/src/WebEid.Security/Validator/AuthTokenValidationConfiguration.cs @@ -38,15 +38,15 @@ internal AuthTokenValidationConfiguration() { } private AuthTokenValidationConfiguration(AuthTokenValidationConfiguration other) { - this.SiteOrigin = other.SiteOrigin; - this.TrustedCaCertificates = new List(other.TrustedCaCertificates); - this.IsUserCertificateRevocationCheckWithOcspEnabled = other.IsUserCertificateRevocationCheckWithOcspEnabled; - this.OcspRequestTimeout = other.OcspRequestTimeout; - this.AllowedOcspResponseTimeSkew = other.AllowedOcspResponseTimeSkew; - this.MaxOcspResponseThisUpdateAge = other.MaxOcspResponseThisUpdateAge; - this.DesignatedOcspServiceConfiguration = other.DesignatedOcspServiceConfiguration; - this.DisallowedSubjectCertificatePolicies = new ReadOnlyCollection(other.DisallowedSubjectCertificatePolicies); - this.NonceDisabledOcspUrls = new List(other.NonceDisabledOcspUrls); + SiteOrigin = other.SiteOrigin; + TrustedCaCertificates = [.. other.TrustedCaCertificates]; + IsUserCertificateRevocationCheckWithOcspEnabled = other.IsUserCertificateRevocationCheckWithOcspEnabled; + OcspRequestTimeout = other.OcspRequestTimeout; + AllowedOcspResponseTimeSkew = other.AllowedOcspResponseTimeSkew; + MaxOcspResponseThisUpdateAge = other.MaxOcspResponseThisUpdateAge; + DesignatedOcspServiceConfiguration = other.DesignatedOcspServiceConfiguration; + DisallowedSubjectCertificatePolicies = new ReadOnlyCollection(other.DisallowedSubjectCertificatePolicies); + NonceDisabledOcspUrls = [.. other.NonceDisabledOcspUrls]; } /// @@ -57,7 +57,7 @@ private AuthTokenValidationConfiguration(AuthTokenValidationConfiguration other) /// /// The list of trusted CA certificates. /// - public List TrustedCaCertificates { get; } = new List(); + public List TrustedCaCertificates { get; } = []; /// /// A value indicating whether user certificate revocation check with OCSP is enabled. @@ -90,19 +90,19 @@ private AuthTokenValidationConfiguration(AuthTokenValidationConfiguration other) /// The list of disallowed subject certificate policies. /// public IList DisallowedSubjectCertificatePolicies { get; } = - new List(new[] - { + [ SubjectCertificatePolicies.EsteidSk2015MobileIdPolicyV1, SubjectCertificatePolicies.EsteidSk2015MobileIdPolicyV2, SubjectCertificatePolicies.EsteidSk2015MobileIdPolicyV3, SubjectCertificatePolicies.EsteidSk2015MobileIdPolicy - }); +, + ]; /// /// The list of OCSP URLs where the nonce extension is disabled. /// Disable OCSP nonce extension for EstEID 2015 cards by default. /// - public List NonceDisabledOcspUrls { get; } = new List { }; + public List NonceDisabledOcspUrls { get; } = []; private static void RequirePositiveTimeSpan(TimeSpan timeSpan, string fieldName) @@ -120,17 +120,17 @@ private static void RequirePositiveTimeSpan(TimeSpan timeSpan, string fieldName) /// When required parameters are null public void Validate() { - ValidateSiteOriginURL(this.SiteOrigin); + ValidateSiteOriginURL(SiteOrigin); - if (!this.TrustedCaCertificates.Any()) + if (TrustedCaCertificates.Count == 0) { throw new ArgumentException("At least one trusted certificate authority must be provided"); } - RequirePositiveTimeSpan(this.OcspRequestTimeout, "OCSP request timeout"); - RequirePositiveTimeSpan(this.AllowedOcspResponseTimeSkew, "Allowed OCSP response time-skew"); - RequirePositiveTimeSpan(this.MaxOcspResponseThisUpdateAge, "Max OCSP response thisUpdate age"); + RequirePositiveTimeSpan(OcspRequestTimeout, "OCSP request timeout"); + RequirePositiveTimeSpan(AllowedOcspResponseTimeSkew, "Allowed OCSP response time-skew"); + RequirePositiveTimeSpan(MaxOcspResponseThisUpdateAge, "Max OCSP response thisUpdate age"); } - + /// /// Validates that the given URI is an origin URL as defined in MDN, /// in the form of "://" [ ":" ]]]>. @@ -140,10 +140,7 @@ public void Validate() /// When the URI is not in the form of origin URL private static void ValidateSiteOriginURL(Uri siteOrigin) { - if (siteOrigin == null) - { - throw new ArgumentNullException(nameof(siteOrigin)); - } + ArgumentNullException.ThrowIfNull(siteOrigin); try { @@ -165,18 +162,43 @@ private static void ValidateSiteOriginURL(Uri siteOrigin) /// /// A new instance with the same configuration as the original. public AuthTokenValidationConfiguration Copy() => - new AuthTokenValidationConfiguration(this); + new(this); + + /// + public bool Equals(AuthTokenValidationConfiguration other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return Equals(SiteOrigin, other.SiteOrigin) && + Enumerable.SequenceEqual(TrustedCaCertificates, other.TrustedCaCertificates) && + IsUserCertificateRevocationCheckWithOcspEnabled == other.IsUserCertificateRevocationCheckWithOcspEnabled && + OcspRequestTimeout.Equals(other.OcspRequestTimeout) && + AllowedOcspResponseTimeSkew.Equals(other.AllowedOcspResponseTimeSkew) && + MaxOcspResponseThisUpdateAge.Equals(other.MaxOcspResponseThisUpdateAge) && + Equals(DesignatedOcspServiceConfiguration, other.DesignatedOcspServiceConfiguration) && + Enumerable.SequenceEqual(DisallowedSubjectCertificatePolicies, other.DisallowedSubjectCertificatePolicies) && + Enumerable.SequenceEqual(NonceDisabledOcspUrls, other.NonceDisabledOcspUrls); + } + + /// + public override bool Equals(object obj) => + obj is AuthTokenValidationConfiguration other && Equals(other); /// - public bool Equals(AuthTokenValidationConfiguration other) => - this.SiteOrigin.Equals(other.SiteOrigin) && - Enumerable.SequenceEqual(this.TrustedCaCertificates, other.TrustedCaCertificates) && - this.IsUserCertificateRevocationCheckWithOcspEnabled.Equals(other - .IsUserCertificateRevocationCheckWithOcspEnabled) && - this.OcspRequestTimeout.Equals(other.OcspRequestTimeout) && - Enumerable.SequenceEqual(this.DisallowedSubjectCertificatePolicies, - other.DisallowedSubjectCertificatePolicies) && - Equals(this.DesignatedOcspServiceConfiguration, other.DesignatedOcspServiceConfiguration) && - Enumerable.SequenceEqual(this.NonceDisabledOcspUrls, other.NonceDisabledOcspUrls); + public override int GetHashCode() => HashCode.Combine( + SiteOrigin, + IsUserCertificateRevocationCheckWithOcspEnabled, + OcspRequestTimeout, + AllowedOcspResponseTimeSkew, + MaxOcspResponseThisUpdateAge, + DesignatedOcspServiceConfiguration); } } diff --git a/src/WebEid.Security/Validator/AuthTokenValidator.cs b/src/WebEid.Security/Validator/AuthTokenValidator.cs index 42d0dc3..4e91d16 100644 --- a/src/WebEid.Security/Validator/AuthTokenValidator.cs +++ b/src/WebEid.Security/Validator/AuthTokenValidator.cs @@ -23,11 +23,11 @@ namespace WebEid.Security.Validator { using System; using System.Security.Cryptography.X509Certificates; + using System.Text.Json; using System.Threading.Tasks; + using AuthToken; using Exceptions; using Microsoft.Extensions.Logging; - using Newtonsoft.Json; - using AuthToken; using Ocsp; using VersionValidators; @@ -88,11 +88,7 @@ public WebEidAuthToken Parse(string authToken) /// A task representing the validation result with the user certificate. public Task Validate(WebEidAuthToken authToken, string currentChallengeNonce) { - if (authToken == null) - { - throw new ArgumentNullException(nameof(authToken), "authToken must not be null"); - } - + ArgumentNullException.ThrowIfNull(authToken, nameof(authToken)); logger?.LogInformation("Starting token validation"); try @@ -119,11 +115,17 @@ private static void ValidateTokenLength(string authToken) } } + private static readonly JsonSerializerOptions SerializerOptions = new() + { + PropertyNameCaseInsensitive = true, + AllowTrailingCommas = true, + }; + private static WebEidAuthToken ParseToken(string authToken) { try { - return JsonConvert.DeserializeObject(authToken) + return JsonSerializer.Deserialize(authToken, SerializerOptions) ?? throw new AuthTokenParseException("Web eID authentication token is null"); } catch (JsonException ex) diff --git a/src/WebEid.Security/Validator/AuthTokenValidatorBuilder.cs b/src/WebEid.Security/Validator/AuthTokenValidatorBuilder.cs index d10945d..8d7f5a1 100644 --- a/src/WebEid.Security/Validator/AuthTokenValidatorBuilder.cs +++ b/src/WebEid.Security/Validator/AuthTokenValidatorBuilder.cs @@ -22,6 +22,7 @@ namespace WebEid.Security.Validator { using System; + using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Security.Cryptography.X509Certificates; using Microsoft.Extensions.Logging; @@ -32,17 +33,16 @@ namespace WebEid.Security.Validator /// /// Represents a builder for configuring and creating an . /// - public class AuthTokenValidatorBuilder + /// + /// Initializes a new instance of the class. + /// + /// The logger for capturing log messages (optional). + [SuppressMessage("Design", "CA1001:Types that own disposable fields should be disposable", Justification = "The builder passes the OCSP client to the created validator and does not own its lifetime after Build().")] + public class AuthTokenValidatorBuilder(ILogger logger = null) { - private readonly ILogger logger; + private readonly ILogger logger = logger; private IOcspClient ocspClient; - private readonly AuthTokenValidationConfiguration configuration = new AuthTokenValidationConfiguration(); - - /// - /// Initializes a new instance of the class. - /// - /// The logger for capturing log messages (optional). - public AuthTokenValidatorBuilder(ILogger logger = null) => this.logger = logger; + private readonly AuthTokenValidationConfiguration configuration = new(); /// /// Sets the expected site origin, i.e. the domain that the application is running on. @@ -53,8 +53,11 @@ public class AuthTokenValidatorBuilder /// the builder instance for method chaining> public AuthTokenValidatorBuilder WithSiteOrigin(Uri origin) { - this.configuration.SiteOrigin = origin; - this.logger?.LogDebug("Origin set to {0}", this.configuration.SiteOrigin); + configuration.SiteOrigin = origin; + if (logger?.IsEnabled(LogLevel.Debug) == true) + { + logger.LogDebug("Origin set to {Origin}", configuration.SiteOrigin); + } return this; } @@ -68,9 +71,12 @@ public AuthTokenValidatorBuilder WithSiteOrigin(Uri origin) /// the builder instance for method chaining public AuthTokenValidatorBuilder WithTrustedCertificateAuthorities(params X509Certificate2[] certificates) { - this.configuration.TrustedCaCertificates.AddRange(certificates); - this.logger?.LogDebug("Trusted intermediate certificate authorities set to {0}", - this.configuration.TrustedCaCertificates.Select(c => c.GetSubjectCn())); + configuration.TrustedCaCertificates.AddRange(certificates); + if (logger?.IsEnabled(LogLevel.Debug) == true) + { + logger.LogDebug("Trusted intermediate certificate authorities set to {TrustedCaCertificates}", + configuration.TrustedCaCertificates.Select(c => c.GetSubjectCn())); + } return this; } @@ -85,10 +91,13 @@ public AuthTokenValidatorBuilder WithDisallowedCertificatePolicies(params string { foreach (var policy in policies) { - this.configuration.DisallowedSubjectCertificatePolicies.Add(policy); + configuration.DisallowedSubjectCertificatePolicies.Add(policy); + } + if (logger?.IsEnabled(LogLevel.Debug) == true) + { + logger.LogDebug("Disallowed subject certificate policies set to {Policies}", + string.Join(", ", configuration.DisallowedSubjectCertificatePolicies)); } - this.logger?.LogDebug("Disallowed subject certificate policies set to {0}", - string.Join(", ", this.configuration.DisallowedSubjectCertificatePolicies)); return this; } @@ -100,8 +109,8 @@ public AuthTokenValidatorBuilder WithDisallowedCertificatePolicies(params string /// the builder instance for method chaining public AuthTokenValidatorBuilder WithoutUserCertificateRevocationCheckWithOcsp() { - this.configuration.IsUserCertificateRevocationCheckWithOcspEnabled = false; - this.logger?.LogDebug( + configuration.IsUserCertificateRevocationCheckWithOcspEnabled = false; + logger?.LogDebug( "User certificate revocation check with OCSP is disabled, you should turn off the revocation check only in exceptional circumstances"); return this; } @@ -114,8 +123,11 @@ public AuthTokenValidatorBuilder WithoutUserCertificateRevocationCheckWithOcsp() /// the builder instance for method chaining public AuthTokenValidatorBuilder WithOcspRequestTimeout(TimeSpan ocspRequestTimeout) { - this.configuration.OcspRequestTimeout = ocspRequestTimeout; - this.logger?.LogDebug("OCSP request timeout set to {0}.", ocspRequestTimeout); + configuration.OcspRequestTimeout = ocspRequestTimeout; + if (logger?.IsEnabled(LogLevel.Debug) == true) + { + logger.LogDebug("OCSP request timeout set to {OcspRequestTimeout}.", ocspRequestTimeout); + } return this; } @@ -131,8 +143,11 @@ public AuthTokenValidatorBuilder WithOcspRequestTimeout(TimeSpan ocspRequestTime /// the builder instance for method chaining public AuthTokenValidatorBuilder WithAllowedOcspResponseTimeSkew(TimeSpan allowedTimeSkew) { - this.configuration.AllowedOcspResponseTimeSkew = allowedTimeSkew; - this.logger?.LogDebug("Allowed OCSP response time skew set to {0}.", allowedTimeSkew); + configuration.AllowedOcspResponseTimeSkew = allowedTimeSkew; + if (logger?.IsEnabled(LogLevel.Debug) == true) + { + logger.LogDebug("Allowed OCSP response time skew set to {AllowedTimeSkew}.", allowedTimeSkew); + } return this; } @@ -144,8 +159,11 @@ public AuthTokenValidatorBuilder WithAllowedOcspResponseTimeSkew(TimeSpan allowe /// the builder instance for method chaining public AuthTokenValidatorBuilder WithMaxOcspResponseThisUpdateAge(TimeSpan maxThisUpdateAge) { - this.configuration.MaxOcspResponseThisUpdateAge = maxThisUpdateAge; - this.logger?.LogDebug("Maximum OCSP response thisUpdate age set to {0}.", maxThisUpdateAge); + configuration.MaxOcspResponseThisUpdateAge = maxThisUpdateAge; + if (logger?.IsEnabled(LogLevel.Debug) == true) + { + logger.LogDebug("Maximum OCSP response thisUpdate age set to {MaxThisUpdateAge}.", maxThisUpdateAge); + } return this; } @@ -157,8 +175,13 @@ public AuthTokenValidatorBuilder WithMaxOcspResponseThisUpdateAge(TimeSpan maxTh /// the builder instance for method chaining public AuthTokenValidatorBuilder WithNonceDisabledOcspUrls(params Uri[] urls) { - this.configuration.NonceDisabledOcspUrls.AddRange(urls); - this.logger?.LogDebug("OCSP URLs for which the nonce protocol extension is disabled set to {0}", this.configuration.NonceDisabledOcspUrls); + configuration.NonceDisabledOcspUrls.AddRange(urls); + if (logger?.IsEnabled(LogLevel.Debug) == true) + { + logger.LogDebug( + "OCSP URLs for which the nonce protocol extension is disabled set to {NonceDisabledOcspUrls}", + configuration.NonceDisabledOcspUrls); + } return this; } @@ -172,8 +195,8 @@ public AuthTokenValidatorBuilder WithNonceDisabledOcspUrls(params Uri[] urls) /// the builder instance for method chaining public AuthTokenValidatorBuilder WithDesignatedOcspServiceConfiguration(DesignatedOcspServiceConfiguration serviceConfiguration) { - this.configuration.DesignatedOcspServiceConfiguration = serviceConfiguration; - this.logger?.LogDebug("Using designated OCSP service configuration"); + configuration.DesignatedOcspServiceConfiguration = serviceConfiguration; + logger?.LogDebug("Using designated OCSP service configuration"); return this; } diff --git a/src/WebEid.Security/Validator/CertValidators/ISubjectCertificateValidator.cs b/src/WebEid.Security/Validator/CertValidators/ISubjectCertificateValidator.cs index b1431d6..4089eb0 100644 --- a/src/WebEid.Security/Validator/CertValidators/ISubjectCertificateValidator.cs +++ b/src/WebEid.Security/Validator/CertValidators/ISubjectCertificateValidator.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright © 2020-2025 Estonian Information System Authority * * Permission is hereby granted, free of charge, to any person obtaining a copy @@ -30,6 +30,6 @@ namespace WebEid.Security.Validator.CertValidators /// internal interface ISubjectCertificateValidator { - Task Validate(X509Certificate2 subjectCertificate); + public Task Validate(X509Certificate2 subjectCertificate); } } diff --git a/src/WebEid.Security/Validator/CertValidators/SubjectCertificateNotRevokedValidator.cs b/src/WebEid.Security/Validator/CertValidators/SubjectCertificateNotRevokedValidator.cs index 447a9fc..3b44216 100644 --- a/src/WebEid.Security/Validator/CertValidators/SubjectCertificateNotRevokedValidator.cs +++ b/src/WebEid.Security/Validator/CertValidators/SubjectCertificateNotRevokedValidator.cs @@ -35,30 +35,20 @@ namespace WebEid.Security.Validator.CertValidators using Org.BouncyCastle.Security; using WebEid.Security.Util; - internal sealed class SubjectCertificateNotRevokedValidator : ISubjectCertificateValidator + internal sealed class SubjectCertificateNotRevokedValidator(SubjectCertificateTrustedValidator trustValidator, + IOcspClient ocspClient, + OcspServiceProvider ocspServiceProvider, + TimeSpan allowedOcspTimeSkew, + TimeSpan maxOcspResponseThisUpdateAge, + ILogger logger = null) : ISubjectCertificateValidator { - private readonly SubjectCertificateTrustedValidator trustValidator; - private readonly IOcspClient ocspClient; - private readonly OcspServiceProvider ocspServiceProvider; - private readonly ILogger logger; - - private readonly TimeSpan allowedOcspTimeSkew; - private readonly TimeSpan maxOcspResponseThisUpdateAge; - - public SubjectCertificateNotRevokedValidator(SubjectCertificateTrustedValidator trustValidator, - IOcspClient ocspClient, - OcspServiceProvider ocspServiceProvider, - TimeSpan allowedOcspTimeSkew, - TimeSpan maxOcspResponseThisUpdateAge, - ILogger logger = null) - { - this.trustValidator = trustValidator; - this.ocspClient = ocspClient; - this.ocspServiceProvider = ocspServiceProvider; - this.logger = logger; - this.allowedOcspTimeSkew = allowedOcspTimeSkew; - this.maxOcspResponseThisUpdateAge = maxOcspResponseThisUpdateAge; - } + private readonly SubjectCertificateTrustedValidator trustValidator = trustValidator; + private readonly IOcspClient ocspClient = ocspClient; + private readonly OcspServiceProvider ocspServiceProvider = ocspServiceProvider; + private readonly ILogger logger = logger; + + private readonly TimeSpan allowedOcspTimeSkew = allowedOcspTimeSkew; + private readonly TimeSpan maxOcspResponseThisUpdateAge = maxOcspResponseThisUpdateAge; /// /// Validates that the user certificate from the authentication token is not revoked with OCSP. @@ -70,15 +60,15 @@ public async Task Validate(X509Certificate2 subjectCertificate) try { var certificate = DotNetUtilities.FromX509Certificate(subjectCertificate); - var ocspService = this.ocspServiceProvider.GetService(certificate); + var ocspService = ocspServiceProvider.GetService(certificate); if (!ocspService.DoesSupportNonce) { - this.logger?.LogDebug("Disabling OCSP nonce extension"); + logger?.LogDebug("Disabling OCSP nonce extension"); } var certificateId = new CertificateID(CertificateID.HashSha1, - DotNetUtilities.FromX509Certificate(this.trustValidator.SubjectCertificateIssuerCertificate), + DotNetUtilities.FromX509Certificate(trustValidator.SubjectCertificateIssuerCertificate), certificate.SerialNumber); var request = new OcspRequestBuilder() @@ -86,8 +76,8 @@ public async Task Validate(X509Certificate2 subjectCertificate) .WithCertificateId(certificateId) .Build(); - this.logger?.LogDebug("Sending OCSP request"); - var response = await this.ocspClient.Request(ocspService.AccessLocation, request); + logger?.LogDebug("Sending OCSP request"); + var response = await ocspClient.Request(ocspService.AccessLocation, request); if (response.Status != OcspResponseStatus.Successful) { throw new UserCertificateOcspCheckFailedException( @@ -95,13 +85,13 @@ public async Task Validate(X509Certificate2 subjectCertificate) } var basicOcspResponse = (BasicOcspResp)response.GetResponseObject(); - this.VerifyOcspResponse(basicOcspResponse, ocspService, certificateId); + VerifyOcspResponse(basicOcspResponse, ocspService, certificateId); if (ocspService.DoesSupportNonce) { VerifyOcspResponseNonce(request, basicOcspResponse); } } - catch (Exception ex) when (!(ex is UserCertificateOcspCheckFailedException)) + catch (Exception ex) when (ex is not UserCertificateOcspCheckFailedException) { throw new UserCertificateOcspCheckFailedException(ex); } @@ -169,7 +159,7 @@ private void VerifyOcspResponse(BasicOcspResp basicResponse, // Now we can accept the signed response as valid and validate the certificate status. OcspResponseValidator.ValidateOcspResponseStatus(certStatusResponse); - this.logger?.LogDebug("OCSP check result is GOOD"); + logger?.LogDebug("OCSP check result is GOOD"); } private static void VerifyOcspResponseNonce(OcspReq request, BasicOcspResp response) diff --git a/src/WebEid.Security/Validator/CertValidators/SubjectCertificatePolicyValidator.cs b/src/WebEid.Security/Validator/CertValidators/SubjectCertificatePolicyValidator.cs index d97434a..4d73bac 100644 --- a/src/WebEid.Security/Validator/CertValidators/SubjectCertificatePolicyValidator.cs +++ b/src/WebEid.Security/Validator/CertValidators/SubjectCertificatePolicyValidator.cs @@ -32,17 +32,11 @@ namespace WebEid.Security.Validator.CertValidators using Org.BouncyCastle.Asn1.X509; using Org.BouncyCastle.Security; - internal sealed class SubjectCertificatePolicyValidator : ISubjectCertificateValidator + internal sealed class SubjectCertificatePolicyValidator(ICollection disallowedSubjectCertificatePolicies, ILogger logger) : ISubjectCertificateValidator { - private readonly ICollection disallowedSubjectCertificatePolicies; - private readonly ILogger logger; - - public SubjectCertificatePolicyValidator(ICollection disallowedSubjectCertificatePolicies, ILogger logger) - { - this.disallowedSubjectCertificatePolicies = disallowedSubjectCertificatePolicies + private readonly ICollection disallowedSubjectCertificatePolicies = disallowedSubjectCertificatePolicies ?? throw new ArgumentNullException(nameof(disallowedSubjectCertificatePolicies)); - this.logger = logger; - } + private readonly ILogger logger = logger; /// /// Validates that the user certificate policies match the configured policies. @@ -59,18 +53,19 @@ public Task Validate(X509Certificate2 subjectCertificate) var certificatePolicies = CertificatePolicies.GetInstance(extensionValue?.GetOctets()); if (certificatePolicies?.GetPolicyInformation() .Any(policy => - this.disallowedSubjectCertificatePolicies + disallowedSubjectCertificatePolicies .Any(disallowedPolicy => disallowedPolicy.Value == policy.PolicyIdentifier.Id)) ?? false) { throw new UserCertificateDisallowedPolicyException(); } } - catch (Exception ex) when (!(ex is UserCertificateDisallowedPolicyException)) + catch (Exception ex) when (ex is not UserCertificateDisallowedPolicyException) { throw new UserCertificateParseException(ex); } - this.logger?.LogDebug("User certificate does not contain disallowed policies."); + + logger?.LogDebug("User certificate does not contain disallowed policies."); return Task.CompletedTask; } diff --git a/src/WebEid.Security/Validator/CertValidators/SubjectCertificatePurposeValidator.cs b/src/WebEid.Security/Validator/CertValidators/SubjectCertificatePurposeValidator.cs index 00ae211..44cc55b 100644 --- a/src/WebEid.Security/Validator/CertValidators/SubjectCertificatePurposeValidator.cs +++ b/src/WebEid.Security/Validator/CertValidators/SubjectCertificatePurposeValidator.cs @@ -32,17 +32,15 @@ namespace WebEid.Security.Validator.CertValidators /// /// Validates the purpose of the user certificate from the authentication token. /// - internal sealed class SubjectCertificatePurposeValidator : ISubjectCertificateValidator + /// + /// Creates an instance of SubjectCertificatePurposeValidator. + /// + /// Logger instance to log validation results. + internal sealed class SubjectCertificatePurposeValidator(ILogger logger = null) : ISubjectCertificateValidator { - private readonly ILogger logger; + private readonly ILogger logger = logger; private const string ExtendedKeyUsageClientAuthentication = "1.3.6.1.5.5.7.3.2"; - /// - /// Creates an instance of SubjectCertificatePurposeValidator. - /// - /// Logger instance to log validation results. - public SubjectCertificatePurposeValidator(ILogger logger = null) => this.logger = logger; - /// /// Validates that the purpose of the user certificate from the authentication token contains client authentication. /// @@ -53,21 +51,17 @@ public Task Validate(X509Certificate2 subjectCertificate) { try { - var keyUsage = subjectCertificate.Extensions.OfType().FirstOrDefault(); - if (keyUsage == null) - { - throw new UserCertificateMissingPurposeException(); - } + var keyUsage = subjectCertificate.Extensions.OfType().FirstOrDefault() ?? throw new UserCertificateMissingPurposeException(); if ((keyUsage.KeyUsages & X509KeyUsageFlags.DigitalSignature) != X509KeyUsageFlags.DigitalSignature) { throw new UserCertificateWrongPurposeException(); } var usages = subjectCertificate.Extensions.OfType().ToArray(); - if (!usages.Any()) + if (usages.Length == 0) { // Digital Signature extension present, but Extended Key Usage extension not present, // assume it is an authentication certificate (e.g. Luxembourg eID). - this.logger?.LogDebug("User certificate has Digital Signature key usage and no Extended Key Usage extension, this means that it can be used for client authentication."); + logger?.LogDebug("User certificate has Digital Signature key usage and no Extended Key Usage extension, this means that it can be used for client authentication."); return Task.CompletedTask; } @@ -77,7 +71,7 @@ public Task Validate(X509Certificate2 subjectCertificate) throw new UserCertificateWrongPurposeException(); } - this.logger?.LogDebug("User certificate can be used for client authentication."); + logger?.LogDebug("User certificate can be used for client authentication."); } catch (CryptographicException ex) { diff --git a/src/WebEid.Security/Validator/CertValidators/SubjectCertificateTrustedValidator.cs b/src/WebEid.Security/Validator/CertValidators/SubjectCertificateTrustedValidator.cs index 16c7cc5..67363e4 100644 --- a/src/WebEid.Security/Validator/CertValidators/SubjectCertificateTrustedValidator.cs +++ b/src/WebEid.Security/Validator/CertValidators/SubjectCertificateTrustedValidator.cs @@ -28,16 +28,10 @@ namespace WebEid.Security.Validator.CertValidators using Util; using WebEid.Security.Exceptions; - internal sealed class SubjectCertificateTrustedValidator : ISubjectCertificateValidator + internal sealed class SubjectCertificateTrustedValidator(ICollection trustedCaCertificates, ILogger logger) : ISubjectCertificateValidator { - private readonly ICollection trustedCaCertificates; - private readonly ILogger logger; - - public SubjectCertificateTrustedValidator(ICollection trustedCaCertificates, ILogger logger) - { - this.trustedCaCertificates = trustedCaCertificates; - this.logger = logger; - } + private readonly ICollection trustedCaCertificates = trustedCaCertificates; + private readonly ILogger logger = logger; /// /// Checks that the user certificate from the authentication token is valid and signed by @@ -50,8 +44,9 @@ public SubjectCertificateTrustedValidator(ICollection trustedC /// when a CA certificate in the chain or the user certificate is expired public Task Validate(X509Certificate2 subjectCertificate) { - this.SubjectCertificateIssuerCertificate = subjectCertificate.ValidateIsValidAndSignedByTrustedCa(this.trustedCaCertificates); - this.logger?.LogDebug("Subject certificate is valid and signed by a trusted CA"); + SubjectCertificateIssuerCertificate = subjectCertificate.ValidateIsValidAndSignedByTrustedCa(trustedCaCertificates); + + logger?.LogDebug("Subject certificate is valid and signed by a trusted CA"); return Task.CompletedTask; } diff --git a/src/WebEid.Security/Validator/CertValidators/SubjectCertificateValidatorBatch.cs b/src/WebEid.Security/Validator/CertValidators/SubjectCertificateValidatorBatch.cs index 830568e..710f26d 100644 --- a/src/WebEid.Security/Validator/CertValidators/SubjectCertificateValidatorBatch.cs +++ b/src/WebEid.Security/Validator/CertValidators/SubjectCertificateValidatorBatch.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright © 2020-2025 Estonian Information System Authority * * Permission is hereby granted, free of charge, to any person obtaining a copy @@ -41,7 +41,7 @@ internal sealed class SubjectCertificateValidatorBatch /// Creates a new from the provided validators. /// internal static SubjectCertificateValidatorBatch CreateFrom(params ISubjectCertificateValidator[] validatorList) => - new SubjectCertificateValidatorBatch(new List(validatorList)); + new([.. validatorList]); /// /// Executes all validators in this batch for the specified subject certificate. diff --git a/src/WebEid.Security/Validator/IAuthTokenValidator.cs b/src/WebEid.Security/Validator/IAuthTokenValidator.cs index 9a7bd51..bb7baa5 100644 --- a/src/WebEid.Security/Validator/IAuthTokenValidator.cs +++ b/src/WebEid.Security/Validator/IAuthTokenValidator.cs @@ -23,9 +23,9 @@ namespace WebEid.Security.Validator { using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; + using AuthToken; using Exceptions; using Util; - using AuthToken; /// /// Interface for validating Web eID authentication tokens. @@ -38,7 +38,7 @@ public interface IAuthTokenValidator /// the Web eID authentication token string, in Web eID JSON format /// the Web eID authentication token /// When parsing fails - WebEidAuthToken Parse(string authToken); + public WebEidAuthToken Parse(string authToken); /// /// Validates the Web eID authentication token signed by the subject and returns @@ -50,6 +50,6 @@ public interface IAuthTokenValidator /// /// validated subject certificate /// When validation fails - Task Validate(WebEidAuthToken authToken, string currentChallengeNonce); + public Task Validate(WebEidAuthToken authToken, string currentChallengeNonce); } } diff --git a/src/WebEid.Security/Validator/Ocsp/IOcspClient.cs b/src/WebEid.Security/Validator/Ocsp/IOcspClient.cs index d6e6fa7..33b30d4 100644 --- a/src/WebEid.Security/Validator/Ocsp/IOcspClient.cs +++ b/src/WebEid.Security/Validator/Ocsp/IOcspClient.cs @@ -36,6 +36,6 @@ public interface IOcspClient : IDisposable /// The URI of the OCSP responder. /// The OCSP request to be sent. /// The OCSP response. - Task Request(Uri uri, OcspReq ocspReq); + public Task Request(Uri uri, OcspReq ocspReq); } } diff --git a/src/WebEid.Security/Validator/Ocsp/OcspClient.cs b/src/WebEid.Security/Validator/Ocsp/OcspClient.cs index 5287ba3..9a6c6d6 100644 --- a/src/WebEid.Security/Validator/Ocsp/OcspClient.cs +++ b/src/WebEid.Security/Validator/Ocsp/OcspClient.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright © 2020-2024 Estonian Information System Authority * * Permission is hereby granted, free of charge, to any person obtaining a copy @@ -42,8 +42,8 @@ internal sealed class OcspClient : IOcspClient public OcspClient(TimeSpan ocspRequestTimeout, ILogger logger = null) { - this.httpClientHandler = new HttpClientHandler { AutomaticDecompression = DecompressionMethods.GZip }; - this.httpClient = new HttpClient(this.httpClientHandler) { Timeout = ocspRequestTimeout }; + httpClientHandler = new HttpClientHandler { AutomaticDecompression = DecompressionMethods.GZip }; + httpClient = new HttpClient(httpClientHandler) { Timeout = ocspRequestTimeout }; this.logger = logger; } @@ -60,12 +60,16 @@ public async Task Request(Uri uri, OcspReq ocspReq) var content = new ByteArrayContent(data); content.Headers.ContentType = new MediaTypeHeaderValue(OcspRequestType); content.Headers.ContentLength = data.Length; - var response = await this.httpClient.PostAsync(uri, content); + var response = await httpClient.PostAsync(uri, content); if (!response.IsSuccessStatusCode) { throw new HttpRequestException($"OCSP request was not successful, response: {response}"); } - this.logger?.LogDebug("OCSP response: {0}", response); + + if (logger?.IsEnabled(LogLevel.Debug) == true) + { + logger.LogDebug("OCSP response: {Response}", response); + } if (response.Content.Headers.ContentType != null && response.Content.Headers.ContentType.MediaType != OcspResponseType) @@ -79,8 +83,8 @@ public async Task Request(Uri uri, OcspReq ocspReq) public void Dispose() { - this.httpClient?.Dispose(); - this.httpClientHandler?.Dispose(); + httpClient?.Dispose(); + httpClientHandler?.Dispose(); } } } diff --git a/src/WebEid.Security/Validator/Ocsp/OcspRequestBuilder.cs b/src/WebEid.Security/Validator/Ocsp/OcspRequestBuilder.cs index 1edc69d..a5ebe42 100644 --- a/src/WebEid.Security/Validator/Ocsp/OcspRequestBuilder.cs +++ b/src/WebEid.Security/Validator/Ocsp/OcspRequestBuilder.cs @@ -60,7 +60,7 @@ public OcspRequestBuilder WithCertificateId(CertificateID certificateId) /// The builder instance. public OcspRequestBuilder EnableOcspNonce(bool enable) { - this.ocspNonceEnabled = enable; + ocspNonceEnabled = enable; return this; } @@ -71,40 +71,34 @@ public OcspRequestBuilder EnableOcspNonce(bool enable) /// An instance of object public OcspReq Build() { - ValidateParameters(this.certificateId); + ValidateParameters(certificateId); var generator = new OcspReqGenerator(); - generator.AddRequest(this.certificateId); + generator.AddRequest(certificateId); - if (this.ocspNonceEnabled) + if (ocspNonceEnabled) { - this.AddNonce(generator); + AddNonce(generator); } return generator.Generate(); } - private static void ValidateParameters(CertificateID certificateId) - { - if (certificateId == null) - { - throw new ArgumentNullException(nameof(certificateId)); - } - } + private static void ValidateParameters(CertificateID certificateId) => ArgumentNullException.ThrowIfNull(certificateId); private void AddNonce(OcspReqGenerator builder) { - this.Nonce = new byte[32]; + Nonce = new byte[32]; using (var rndGenerator = RandomNumberGenerator.Create()) { - rndGenerator.GetBytes(this.Nonce); + rndGenerator.GetBytes(Nonce); } IDictionary extensions = new Hashtable { { OcspObjectIdentifiers.PkixOcspNonce, - new X509Extension(false, new DerOctetString(new DerOctetString(this.Nonce))) } + new X509Extension(false, new DerOctetString(new DerOctetString(Nonce))) } }; builder.SetRequestExtensions(new X509Extensions(extensions)); } diff --git a/src/WebEid.Security/Validator/Ocsp/OcspResponseValidator.cs b/src/WebEid.Security/Validator/Ocsp/OcspResponseValidator.cs index bfa78de..90f8bbf 100644 --- a/src/WebEid.Security/Validator/Ocsp/OcspResponseValidator.cs +++ b/src/WebEid.Security/Validator/Ocsp/OcspResponseValidator.cs @@ -145,7 +145,7 @@ public static void ValidateOcspResponseStatus(SingleResp certStatusResponse) internal interface ISingleResp { - DateTime ThisUpdate { get; } - DateTimeObject NextUpdate { get; } + public DateTime ThisUpdate { get; } + public DateTimeObject NextUpdate { get; } } } diff --git a/src/WebEid.Security/Validator/Ocsp/OcspServiceProvider.cs b/src/WebEid.Security/Validator/Ocsp/OcspServiceProvider.cs index 344eb88..a59e683 100644 --- a/src/WebEid.Security/Validator/Ocsp/OcspServiceProvider.cs +++ b/src/WebEid.Security/Validator/Ocsp/OcspServiceProvider.cs @@ -27,22 +27,16 @@ namespace WebEid.Security.Validator.Ocsp /// /// Provides an OCSP (Online Certificate Status Protocol) service provider based on configuration. /// - public class OcspServiceProvider + /// + /// Initializes a new instance of the class. + /// + /// The configuration for the designated OCSP service. + /// The configuration for the AIA (Authority Information Access) OCSP service. + public class OcspServiceProvider(DesignatedOcspServiceConfiguration designatedOcspServiceConfiguration, AiaOcspServiceConfiguration aiaOcspServiceConfiguration) { - private readonly DesignatedOcspService designatedOcspService; - private readonly AiaOcspServiceConfiguration aiaOcspServiceConfiguration; - - /// - /// Initializes a new instance of the class. - /// - /// The configuration for the designated OCSP service. - /// The configuration for the AIA (Authority Information Access) OCSP service. - public OcspServiceProvider(DesignatedOcspServiceConfiguration designatedOcspServiceConfiguration, AiaOcspServiceConfiguration aiaOcspServiceConfiguration) - { - this.designatedOcspService = designatedOcspServiceConfiguration != null ? new DesignatedOcspService(designatedOcspServiceConfiguration) : null; - this.aiaOcspServiceConfiguration = aiaOcspServiceConfiguration ?? + private readonly DesignatedOcspService designatedOcspService = designatedOcspServiceConfiguration != null ? new DesignatedOcspService(designatedOcspServiceConfiguration) : null; + private readonly AiaOcspServiceConfiguration aiaOcspServiceConfiguration = aiaOcspServiceConfiguration ?? throw new ArgumentNullException(nameof(aiaOcspServiceConfiguration)); - } /// /// Gets the appropriate OCSP service based on the certificate issuer. @@ -51,11 +45,11 @@ public OcspServiceProvider(DesignatedOcspServiceConfiguration designatedOcspServ /// An instance of . public IOcspService GetService(Org.BouncyCastle.X509.X509Certificate certificate = null) { - if (this.designatedOcspService != null && this.designatedOcspService.SupportsIssuerOf(certificate)) + if (designatedOcspService != null && designatedOcspService.SupportsIssuerOf(certificate)) { - return this.designatedOcspService; + return designatedOcspService; } - return new AiaOcspService(this.aiaOcspServiceConfiguration, certificate); + return new AiaOcspService(aiaOcspServiceConfiguration, certificate); } } } diff --git a/src/WebEid.Security/Validator/Ocsp/Service/AiaOcspService.cs b/src/WebEid.Security/Validator/Ocsp/Service/AiaOcspService.cs index bff9f2d..030e776 100644 --- a/src/WebEid.Security/Validator/Ocsp/Service/AiaOcspService.cs +++ b/src/WebEid.Security/Validator/Ocsp/Service/AiaOcspService.cs @@ -38,13 +38,11 @@ internal class AiaOcspService : IOcspService public AiaOcspService(AiaOcspServiceConfiguration configuration, Org.BouncyCastle.X509.X509Certificate certificate) { - if (configuration == null) - { - throw new ArgumentNullException(nameof(configuration)); - } - this.AccessLocation = GetOcspAiaUrlFromCertificate(certificate); - this.trustedCaCertificates = configuration.TrustedCaCertificates; - this.DoesSupportNonce = !configuration.NonceDisabledOcspUrls.Contains(this.AccessLocation); + ArgumentNullException.ThrowIfNull(configuration); + + AccessLocation = GetOcspAiaUrlFromCertificate(certificate); + trustedCaCertificates = configuration.TrustedCaCertificates; + DoesSupportNonce = !configuration.NonceDisabledOcspUrls.Contains(AccessLocation); } public bool DoesSupportNonce { get; } @@ -52,8 +50,7 @@ public AiaOcspService(AiaOcspServiceConfiguration configuration, private static Uri GetOcspAiaUrlFromCertificate(Org.BouncyCastle.X509.X509Certificate certificate) { - if (certificate == null) - { throw new ArgumentNullException(nameof(certificate)); } + ArgumentNullException.ThrowIfNull(certificate); return certificate.GetOcspUri() ?? throw new UserCertificateOcspCheckFailedException("Getting the AIA OCSP responder field " + @@ -68,9 +65,9 @@ public void ValidateResponderCertificate(Org.BouncyCastle.X509.X509Certificate r // Trusted certificates validity has been already verified in ValidateCertificateExpiry(). OcspResponseValidator.ValidateHasSigningExtension(responderCertificate); _ = new X509Certificate2(DotNetUtilities.ToX509Certificate(responderCertificate)) - .ValidateIsValidAndSignedByTrustedCa(this.trustedCaCertificates); + .ValidateIsValidAndSignedByTrustedCa(trustedCaCertificates); } - catch (Exception ex) when (!(ex is CertificateNotTrustedException) && !(ex is CertificateExpiredException)) + catch (Exception ex) when (ex is not CertificateNotTrustedException and not CertificateExpiredException) { throw new OcspCertificateException("Invalid certificate", ex); } diff --git a/src/WebEid.Security/Validator/Ocsp/Service/AiaOcspServiceConfiguration.cs b/src/WebEid.Security/Validator/Ocsp/Service/AiaOcspServiceConfiguration.cs index 9de36b5..0c218f8 100644 --- a/src/WebEid.Security/Validator/Ocsp/Service/AiaOcspServiceConfiguration.cs +++ b/src/WebEid.Security/Validator/Ocsp/Service/AiaOcspServiceConfiguration.cs @@ -29,30 +29,25 @@ namespace WebEid.Security.Validator.Ocsp.Service /// /// Represents the configuration for an AIA (Authority Information Access) OCSP service. /// - public sealed class AiaOcspServiceConfiguration : IEquatable + /// + /// Initializes a new instance of the class. + /// + /// List of OCSP responder URLs where nonce is disabled. + /// List of trusted CA certificates. + public sealed class AiaOcspServiceConfiguration(List nonceDisabledOcspUrls, List trustedCaCertificates) : IEquatable { - /// - /// Initializes a new instance of the class. - /// - /// List of OCSP responder URLs where nonce is disabled. - /// List of trusted CA certificates. - public AiaOcspServiceConfiguration(List nonceDisabledOcspUrls, List trustedCaCertificates) - { - this.NonceDisabledOcspUrls = - nonceDisabledOcspUrls ?? throw new ArgumentNullException(nameof(nonceDisabledOcspUrls)); - this.TrustedCaCertificates = - trustedCaCertificates ?? throw new ArgumentNullException(nameof(trustedCaCertificates)); - } /// /// Gets the list of OCSP responder URLs where nonce is disabled. /// - public List NonceDisabledOcspUrls { get; } + public List NonceDisabledOcspUrls { get; } = + nonceDisabledOcspUrls ?? throw new ArgumentNullException(nameof(nonceDisabledOcspUrls)); /// /// Gets the list of trusted CA certificates. /// - public List TrustedCaCertificates { get; } + public List TrustedCaCertificates { get; } = + trustedCaCertificates ?? throw new ArgumentNullException(nameof(trustedCaCertificates)); /// /// Determines whether the current instance is equal to another . @@ -71,20 +66,20 @@ public bool Equals(AiaOcspServiceConfiguration other) return true; } - return Enumerable.SequenceEqual(this.NonceDisabledOcspUrls, other.NonceDisabledOcspUrls) && - Enumerable.SequenceEqual(this.TrustedCaCertificates, other.TrustedCaCertificates); + return Enumerable.SequenceEqual(NonceDisabledOcspUrls, other.NonceDisabledOcspUrls) && + Enumerable.SequenceEqual(TrustedCaCertificates, other.TrustedCaCertificates); } /// - public override bool Equals(object obj) => ReferenceEquals(this, obj) || obj is AiaOcspServiceConfiguration other && Equals(other); + public override bool Equals(object obj) => ReferenceEquals(this, obj) || (obj is AiaOcspServiceConfiguration other && Equals(other)); /// public override int GetHashCode() { unchecked { - return ((this.NonceDisabledOcspUrls != null ? this.NonceDisabledOcspUrls.GetHashCode() : 0) * 397) ^ - (this.TrustedCaCertificates != null ? this.TrustedCaCertificates.GetHashCode() : 0); + return ((NonceDisabledOcspUrls != null ? NonceDisabledOcspUrls.GetHashCode() : 0) * 397) ^ + (TrustedCaCertificates != null ? TrustedCaCertificates.GetHashCode() : 0); } } diff --git a/src/WebEid.Security/Validator/Ocsp/Service/DesignatedOcspService.cs b/src/WebEid.Security/Validator/Ocsp/Service/DesignatedOcspService.cs index f0813ac..cea61b2 100644 --- a/src/WebEid.Security/Validator/Ocsp/Service/DesignatedOcspService.cs +++ b/src/WebEid.Security/Validator/Ocsp/Service/DesignatedOcspService.cs @@ -36,19 +36,19 @@ internal class DesignatedOcspService : IOcspService public DesignatedOcspService(DesignatedOcspServiceConfiguration configuration) { this.configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); - this.DoesSupportNonce = this.configuration.DoesSupportNonce; + DoesSupportNonce = this.configuration.DoesSupportNonce; } public bool DoesSupportNonce { get; } - public Uri AccessLocation => this.configuration.OcspServiceAccessLocation; + public Uri AccessLocation => configuration.OcspServiceAccessLocation; - public bool SupportsIssuerOf(X509Certificate certificate) => this.configuration.SupportsIssuerOf(certificate); + public bool SupportsIssuerOf(X509Certificate certificate) => configuration.SupportsIssuerOf(certificate); public void ValidateResponderCertificate(X509Certificate responderCertificate, DateTime now) { // Certificate pinning is implemented simply by comparing the certificates or their public keys, // see https://owasp.org/www-community/controls/Certificate_and_Public_Key_Pinning. - if (!this.configuration.ResponderCertificate.Equals(responderCertificate)) + if (!configuration.ResponderCertificate.Equals(responderCertificate)) { throw new OcspCertificateException( "Responder certificate from the OCSP response is not equal to the configured designated OCSP responder certificate"); diff --git a/src/WebEid.Security/Validator/Ocsp/Service/DesignatedOcspServiceConfiguration.cs b/src/WebEid.Security/Validator/Ocsp/Service/DesignatedOcspServiceConfiguration.cs index 9996ba4..77e6679 100644 --- a/src/WebEid.Security/Validator/Ocsp/Service/DesignatedOcspServiceConfiguration.cs +++ b/src/WebEid.Security/Validator/Ocsp/Service/DesignatedOcspServiceConfiguration.cs @@ -46,12 +46,12 @@ public DesignatedOcspServiceConfiguration(Uri ocspServiceAccessLocation, List supportedCertificateIssuers, bool doesSupportNonce = true) { - this.OcspServiceAccessLocation = ocspServiceAccessLocation ?? + OcspServiceAccessLocation = ocspServiceAccessLocation ?? throw new ArgumentNullException(nameof(ocspServiceAccessLocation)); - this.ResponderCertificate = + ResponderCertificate = responderCertificate ?? throw new ArgumentNullException(nameof(responderCertificate)); - this.SupportedIssuers = supportedCertificateIssuers.Select(i => i.SubjectDN).ToList(); - this.DoesSupportNonce = doesSupportNonce; + SupportedIssuers = [.. supportedCertificateIssuers.Select(i => i.SubjectDN)]; + DoesSupportNonce = doesSupportNonce; OcspResponseValidator.ValidateHasSigningExtension(responderCertificate); } @@ -83,11 +83,8 @@ public DesignatedOcspServiceConfiguration(Uri ocspServiceAccessLocation, /// Thrown when the certificate is null. public bool SupportsIssuerOf(X509Certificate certificate) { - if (certificate == null) - { - throw new ArgumentNullException(nameof(certificate)); - } - return this.SupportedIssuers.Contains(certificate.IssuerDN); + ArgumentNullException.ThrowIfNull(certificate); + return SupportedIssuers.Contains(certificate.IssuerDN); } } } diff --git a/src/WebEid.Security/Validator/Ocsp/Service/IOcspService.cs b/src/WebEid.Security/Validator/Ocsp/Service/IOcspService.cs index f3e69e0..0b6328c 100644 --- a/src/WebEid.Security/Validator/Ocsp/Service/IOcspService.cs +++ b/src/WebEid.Security/Validator/Ocsp/Service/IOcspService.cs @@ -31,18 +31,18 @@ public interface IOcspService /// /// Gets a value indicating whether the service supports including a nonce in requests. /// - bool DoesSupportNonce { get; } + public bool DoesSupportNonce { get; } /// /// Gets the access location (URI) of the OCSP service. /// - Uri AccessLocation { get; } + public Uri AccessLocation { get; } /// /// Validates the responder certificate based on current system time. /// /// The responder's X.509 certificate. /// Current system time. - void ValidateResponderCertificate(Org.BouncyCastle.X509.X509Certificate responderCertificate, DateTime now); + public void ValidateResponderCertificate(Org.BouncyCastle.X509.X509Certificate responderCertificate, DateTime now); } } diff --git a/src/WebEid.Security/Validator/VersionValidators/AuthTokenVersion11Validator.cs b/src/WebEid.Security/Validator/VersionValidators/AuthTokenVersion11Validator.cs index 70514de..2b0c436 100644 --- a/src/WebEid.Security/Validator/VersionValidators/AuthTokenVersion11Validator.cs +++ b/src/WebEid.Security/Validator/VersionValidators/AuthTokenVersion11Validator.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright © 2025-2025 Estonian Information System Authority * * Permission is hereby granted, free of charge, to any person obtaining a copy @@ -26,11 +26,11 @@ namespace WebEid.Security.Validator.VersionValidators using System.Linq; using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; + using AuthToken; using CertValidators; + using Exceptions; using Microsoft.Extensions.Logging; using Ocsp; - using AuthToken; - using Exceptions; using Org.BouncyCastle.Asn1; using Org.BouncyCastle.Asn1.X509; using Util; @@ -44,14 +44,14 @@ public sealed class AuthTokenVersion11Validator : AuthTokenVersion1Validator private const string V11_SUPPORTED_TOKEN_FORMAT_PREFIX = "web-eid:1.1"; private static readonly HashSet SupportedSigningCryptoAlgorithms = - new HashSet(StringComparer.OrdinalIgnoreCase) + new(StringComparer.OrdinalIgnoreCase) { "ECC", "RSA" }; private static readonly HashSet SupportedSigningPaddingSchemes = - new HashSet(StringComparer.OrdinalIgnoreCase) + new(StringComparer.OrdinalIgnoreCase) { "NONE", "PKCS1.5", @@ -59,7 +59,7 @@ public sealed class AuthTokenVersion11Validator : AuthTokenVersion1Validator }; private static readonly HashSet SupportedSigningHashFunctions = - new HashSet(StringComparer.OrdinalIgnoreCase) + new(StringComparer.OrdinalIgnoreCase) { "SHA-224", "SHA-256", @@ -121,7 +121,7 @@ private static void ValidateSupportedSignatureAlgorithms(UnverifiedSigningCertif throw new AuthTokenParseException("'supportedSignatureAlgorithms' field is missing"); } - bool hasInvalid = + var hasInvalid = algorithms.Any(algorithm => algorithm == null || algorithm.CryptoAlgorithm == null || @@ -225,8 +225,8 @@ private static void ValidateSigningCertificateKeyUsage(X509Certificate2 cert) private static bool SubjectsMatch(X509Certificate2 subjectCert, X509Certificate2 signingCert) { - byte[] authRaw = subjectCert.SubjectName.RawData; - byte[] signRaw = signingCert.SubjectName.RawData; + var authRaw = subjectCert.SubjectName.RawData; + var signRaw = signingCert.SubjectName.RawData; var authAsn1 = Asn1Object.FromByteArray(authRaw); var signAsn1 = Asn1Object.FromByteArray(signRaw); @@ -244,13 +244,13 @@ private static byte[] GetAuthorityKeyIdentifier(X509Certificate2 cert) var akiExt = cert.Extensions["2.5.29.35"]; if (akiExt == null) { - return Array.Empty(); + return []; } var akiObj = Asn1Object.FromByteArray(akiExt.RawData); var aki = AuthorityKeyIdentifier.GetInstance(akiObj); - return aki.GetKeyIdentifier() ?? Array.Empty(); + return aki.GetKeyIdentifier() ?? []; } catch (Exception ex) { diff --git a/src/WebEid.Security/Validator/VersionValidators/AuthTokenVersion1Validator.cs b/src/WebEid.Security/Validator/VersionValidators/AuthTokenVersion1Validator.cs index 96bbeaa..c6fe7b0 100644 --- a/src/WebEid.Security/Validator/VersionValidators/AuthTokenVersion1Validator.cs +++ b/src/WebEid.Security/Validator/VersionValidators/AuthTokenVersion1Validator.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright © 2025-2025 Estonian Information System Authority * * Permission is hereby granted, free of charge, to any person obtaining a copy @@ -24,12 +24,12 @@ namespace WebEid.Security.Validator.VersionValidators using System; using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; - using CertValidators; using AuthToken; + using CertValidators; using Exceptions; - using Util; - using Ocsp; using Microsoft.Extensions.Logging; + using Ocsp; + using Util; /// /// Validator for token format web-eid:1.0. @@ -124,5 +124,5 @@ public virtual async Task Validate(WebEidAuthToken authToken, private static bool IsExactV10Format(string format) => V1_SUPPORTED_TOKEN_FORMAT_PREFIX.Equals(format, StringComparison.OrdinalIgnoreCase) || "web-eid:1.0".Equals(format, StringComparison.OrdinalIgnoreCase); - } + } } diff --git a/src/WebEid.Security/Validator/VersionValidators/AuthTokenVersionValidatorFactory.cs b/src/WebEid.Security/Validator/VersionValidators/AuthTokenVersionValidatorFactory.cs index 92b9151..d71f66c 100644 --- a/src/WebEid.Security/Validator/VersionValidators/AuthTokenVersionValidatorFactory.cs +++ b/src/WebEid.Security/Validator/VersionValidators/AuthTokenVersionValidatorFactory.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright © 2025-2025 Estonian Information System Authority * * Permission is hereby granted, free of charge, to any person obtaining a copy @@ -25,7 +25,6 @@ namespace WebEid.Security.Validator.VersionValidators using System.Collections.Generic; using System.Linq; using System.Security.Cryptography; - using System.Security.Cryptography.X509Certificates; using CertValidators; using Exceptions; using Microsoft.Extensions.Logging; @@ -36,15 +35,12 @@ namespace WebEid.Security.Validator.VersionValidators /// Provides a factory for selecting the correct authentication token validator /// based on the token format version. /// - public sealed class AuthTokenVersionValidatorFactory + /// + /// Creates a new instance of the class. + /// + public sealed class AuthTokenVersionValidatorFactory(IList validators) { - private readonly IReadOnlyList validators; - - /// - /// Creates a new instance of the class. - /// - public AuthTokenVersionValidatorFactory(IList validators) => - this.validators = validators?.ToList() + private readonly IReadOnlyList validators = validators?.ToList() ?? throw new ArgumentNullException(nameof(validators)); /// @@ -58,13 +54,8 @@ public bool Supports(string format) => /// public IAuthTokenVersionValidator GetValidatorFor(string format) { - var validator = validators.FirstOrDefault(v => v.Supports(format)); - - if (validator == null) - { - throw new AuthTokenParseException( + var validator = validators.FirstOrDefault(v => v.Supports(format)) ?? throw new AuthTokenParseException( $"Token format version '{format}' is currently not supported"); - } return validator; } @@ -78,10 +69,7 @@ public static AuthTokenVersionValidatorFactory Create( IOcspClient ocspClient, ILogger logger = null) { - if (configuration == null) - { - throw new ArgumentNullException(nameof(configuration)); - } + ArgumentNullException.ThrowIfNull(configuration); var validationConfig = configuration.Copy(); var trustedCaCertificates = validationConfig.TrustedCaCertificates; @@ -89,7 +77,7 @@ public static AuthTokenVersionValidatorFactory Create( var simpleSubjectValidators = SubjectCertificateValidatorBatch.CreateFrom( new SubjectCertificatePurposeValidator(logger), new SubjectCertificatePolicyValidator( - validationConfig.DisallowedSubjectCertificatePolicies.Select(x => new Oid(x)).ToArray(), + [.. validationConfig.DisallowedSubjectCertificatePolicies.Select(x => new Oid(x))], logger) ); @@ -125,11 +113,11 @@ public static AuthTokenVersionValidatorFactory Create( logger ); - return new AuthTokenVersionValidatorFactory(new List - { + return new AuthTokenVersionValidatorFactory( + [ validatorV11, validatorV10 - }); + ]); } } } diff --git a/src/WebEid.Security/Validator/VersionValidators/IAuthTokenVersionValidator.cs b/src/WebEid.Security/Validator/VersionValidators/IAuthTokenVersionValidator.cs index 508728d..4815399 100644 --- a/src/WebEid.Security/Validator/VersionValidators/IAuthTokenVersionValidator.cs +++ b/src/WebEid.Security/Validator/VersionValidators/IAuthTokenVersionValidator.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright © 2025-2025 Estonian Information System Authority * * Permission is hereby granted, free of charge, to any person obtaining a copy @@ -35,7 +35,7 @@ public interface IAuthTokenVersionValidator /// Whether this validator supports the specified token format, /// e.g. "web-eid:1.0" or "web-eid:1.1". /// - bool Supports(string format); + public bool Supports(string format); /// /// Validates the Web eID authentication token according to the @@ -45,6 +45,6 @@ public interface IAuthTokenVersionValidator /// Server-issued challenge nonce. /// Validated authentication certificate. /// If validation fails. - Task Validate(WebEidAuthToken authToken, string currentChallengeNonce); + public Task Validate(WebEidAuthToken authToken, string currentChallengeNonce); } } diff --git a/src/WebEid.Security/WebEid.Security.csproj b/src/WebEid.Security/WebEid.Security.csproj index bb91fc4..50e4624 100644 --- a/src/WebEid.Security/WebEid.Security.csproj +++ b/src/WebEid.Security/WebEid.Security.csproj @@ -16,7 +16,6 @@ -