From 80947a234e7dce7faeea5c00b1bd08fb8b7bd068 Mon Sep 17 00:00:00 2001 From: "Sander.Kondratjev" Date: Fri, 21 Nov 2025 16:08:09 +0200 Subject: [PATCH 1/3] Add web-eid-1.1 token support NFC-99 Signed-off-by: Sander Kondratjev Co-authored-by: Mart Aarma --- .github/workflows/coverity-analysis.yml | 2 +- .github/workflows/dotnet-build-example.yml | 2 +- .github/workflows/dotnet-build-linux.yml | 2 +- .github/workflows/dotnet-build-windows.yml | 4 +- .github/workflows/sonarcloud-analysis.yml | 3 +- README.md | 122 ++- example/README.md | 29 +- example/src/.dockerignore | 2 +- example/src/.editorconfig | 453 ++++++++++ example/src/Dockerfile | 2 +- .../Certificates/CertificateLoader.cs | 28 +- .../ClaimsIdentityExtensions.cs | 7 +- .../Controllers/Api/AuthController.cs | 104 ++- .../Controllers/Api/BaseController.cs | 36 +- .../Controllers/Api/ChallengeController.cs | 13 +- .../Api/MobileAuthInitController.cs | 93 +++ .../Controllers/Api/SignController.cs | 126 ++- .../Controllers/WelcomeController.cs | 8 +- .../Dto/AuthenticateRequestDto.cs | 8 +- .../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 | 32 + .../Pages/Index.cshtml | 604 ++++++++++++-- .../Pages/WebEidCallback.cshtml | 104 +++ .../Pages/WebEidCallback.cshtml.cs | 31 + .../Pages/WebEidLogin.cshtml | 77 ++ .../Pages/WebEidLogin.cshtml.cs | 31 + .../Pages/Welcome.cshtml | 240 +++--- .../Pages/Welcome.cshtml.cs | 23 +- .../src/WebEid.AspNetCore.Example/Program.cs | 12 +- .../Services/ContainerCleanupService.cs | 71 ++ .../Services/MobileRequestUriBuilder.cs | 34 + .../SessionBackedChallengeNonceStore.cs | 40 +- .../Signing/DigiDocConfiguration.cs | 15 +- .../Signing/MobileSigningService.cs | 136 +++ .../Signing/SigningService.cs | 33 +- .../src/WebEid.AspNetCore.Example/Startup.cs | 88 +- .../WebEid.AspNetCore.Example.csproj | 20 +- .../appsettings.Development.json | 4 + .../appsettings.json | 4 + .../wwwroot/css/bootstrap.min.css | 13 +- .../wwwroot/css/main.css | 73 +- .../wwwroot/favicon.ico | Bin 5430 -> 0 bytes .../wwwroot/img/android-chrome-192x192.png | Bin 0 -> 11514 bytes .../wwwroot/img/android-chrome-512x512.png | Bin 0 -> 46458 bytes .../wwwroot/img/apple-touch-icon.png | Bin 0 -> 10297 bytes .../wwwroot/img/eu-fund-flags.jpg | Bin 0 -> 75474 bytes .../wwwroot/img/eu-fund-flags.png | Bin 0 -> 25226 bytes .../wwwroot/img/eu-fund-flags.svg | 787 ------------------ .../wwwroot/img/favicon-16x16.png | Bin 0 -> 651 bytes .../wwwroot/img/favicon-32x32.png | Bin 0 -> 1289 bytes .../wwwroot/img/favicon.ico | Bin 0 -> 15406 bytes .../wwwroot/js/bootstrap.bundle.min.js | 7 + .../wwwroot/js/device-utils.js | 7 + .../wwwroot/js/payload.js | 45 + 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 | 25 +- .../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 | 67 +- .../AuthTokenCertificateBelgianIdCardTest.cs | 7 +- .../AuthTokenCertificateFinnishIdCardTest.cs | 7 +- .../Validator/AuthTokenCertificateTest.cs | 76 +- .../AuthTokenSignatureValidatorTests.cs | 103 ++- .../Validator/AuthTokenStructureTest.cs | 18 +- .../AuthTokenValidationConfigurationTests.cs | 38 +- .../Validator/AuthTokenValidatonOcspTest.cs | 3 +- .../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 | 151 ++++ .../AuthTokenVersion11ValidatorTest.cs | 138 +++ .../AuthTokenVersion1ValidatorTest.cs | 145 ++++ .../VersionValidators/AuthTokenVersionTest.cs | 78 ++ .../AuthTokenVersionValidatorFactoryTest.cs | 107 +++ .../WebEid.Security.Tests.csproj | 2 +- .../AuthToken/SupportedSignatureAlgorithm.cs | 43 + .../AuthToken/UnverifiedSigningCertificate.cs | 40 + .../AuthToken/WebEidAuthToken.cs | 8 +- .../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 | 41 + .../Util/X509CertificateExtensions.cs | 7 +- .../Validator/AuthTokenSignatureValidator.cs | 50 +- .../AuthTokenValidationConfiguration.cs | 92 +- .../Validator/AuthTokenValidator.cs | 132 +-- .../Validator/AuthTokenValidatorBuilder.cs | 106 ++- .../ISubjectCertificateValidator.cs | 6 +- .../SubjectCertificateNotRevokedValidator.cs | 54 +- .../SubjectCertificatePolicyValidator.cs | 21 +- .../SubjectCertificatePurposeValidator.cs | 26 +- .../SubjectCertificateTrustedValidator.cs | 19 +- .../SubjectCertificateValidatorBatch.cs | 71 +- .../Validator/IAuthTokenValidator.cs | 13 +- .../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 +- .../VersionValidators/AuthTokenVersion.cs | 87 ++ .../AuthTokenVersion11Validator.cs | 295 +++++++ .../AuthTokenVersion1Validator.cs | 117 +++ .../AuthTokenVersionValidatorFactory.cs | 123 +++ .../IAuthTokenVersionValidator.cs | 50 ++ src/WebEid.Security/WebEid.Security.csproj | 10 +- 132 files changed, 5025 insertions(+), 1907 deletions(-) create mode 100644 example/src/.editorconfig create mode 100644 example/src/WebEid.AspNetCore.Example/Controllers/Api/MobileAuthInitController.cs create mode 100644 example/src/WebEid.AspNetCore.Example/Options/WebEidMobileOptions.cs create mode 100644 example/src/WebEid.AspNetCore.Example/Pages/WebEidCallback.cshtml create mode 100644 example/src/WebEid.AspNetCore.Example/Pages/WebEidCallback.cshtml.cs create mode 100644 example/src/WebEid.AspNetCore.Example/Pages/WebEidLogin.cshtml create mode 100644 example/src/WebEid.AspNetCore.Example/Pages/WebEidLogin.cshtml.cs create mode 100644 example/src/WebEid.AspNetCore.Example/Services/ContainerCleanupService.cs create mode 100644 example/src/WebEid.AspNetCore.Example/Services/MobileRequestUriBuilder.cs create mode 100644 example/src/WebEid.AspNetCore.Example/Signing/MobileSigningService.cs delete mode 100644 example/src/WebEid.AspNetCore.Example/wwwroot/favicon.ico create mode 100644 example/src/WebEid.AspNetCore.Example/wwwroot/img/android-chrome-192x192.png create mode 100644 example/src/WebEid.AspNetCore.Example/wwwroot/img/android-chrome-512x512.png create mode 100644 example/src/WebEid.AspNetCore.Example/wwwroot/img/apple-touch-icon.png create mode 100644 example/src/WebEid.AspNetCore.Example/wwwroot/img/eu-fund-flags.jpg create mode 100644 example/src/WebEid.AspNetCore.Example/wwwroot/img/eu-fund-flags.png delete mode 100644 example/src/WebEid.AspNetCore.Example/wwwroot/img/eu-fund-flags.svg create mode 100644 example/src/WebEid.AspNetCore.Example/wwwroot/img/favicon-16x16.png create mode 100644 example/src/WebEid.AspNetCore.Example/wwwroot/img/favicon-32x32.png create mode 100644 example/src/WebEid.AspNetCore.Example/wwwroot/img/favicon.ico create mode 100644 example/src/WebEid.AspNetCore.Example/wwwroot/js/bootstrap.bundle.min.js create mode 100644 example/src/WebEid.AspNetCore.Example/wwwroot/js/device-utils.js create mode 100644 example/src/WebEid.AspNetCore.Example/wwwroot/js/payload.js create mode 100644 src/WebEid.Security.Tests/Validator/Ocsp/OcspClientTests.cs create mode 100644 src/WebEid.Security.Tests/Validator/VersionValidators/AuthTokenV11CertificateTest.cs create mode 100644 src/WebEid.Security.Tests/Validator/VersionValidators/AuthTokenVersion11ValidatorTest.cs create mode 100644 src/WebEid.Security.Tests/Validator/VersionValidators/AuthTokenVersion1ValidatorTest.cs create mode 100644 src/WebEid.Security.Tests/Validator/VersionValidators/AuthTokenVersionTest.cs create mode 100644 src/WebEid.Security.Tests/Validator/VersionValidators/AuthTokenVersionValidatorFactoryTest.cs create mode 100644 src/WebEid.Security/AuthToken/SupportedSignatureAlgorithm.cs create mode 100644 src/WebEid.Security/AuthToken/UnverifiedSigningCertificate.cs create mode 100644 src/WebEid.Security/Util/X509Certificate2Extensions.cs create mode 100644 src/WebEid.Security/Validator/VersionValidators/AuthTokenVersion.cs create mode 100644 src/WebEid.Security/Validator/VersionValidators/AuthTokenVersion11Validator.cs create mode 100644 src/WebEid.Security/Validator/VersionValidators/AuthTokenVersion1Validator.cs create mode 100644 src/WebEid.Security/Validator/VersionValidators/AuthTokenVersionValidatorFactory.cs create mode 100644 src/WebEid.Security/Validator/VersionValidators/IAuthTokenVersionValidator.cs diff --git a/.github/workflows/coverity-analysis.yml b/.github/workflows/coverity-analysis.yml index eeb42132..b27ff90c 100644 --- a/.github/workflows/coverity-analysis.yml +++ b/.github/workflows/coverity-analysis.yml @@ -23,7 +23,7 @@ jobs: - name: Setup dotnet uses: actions/setup-dotnet@v4 with: - dotnet-version: 6.0.x # SDK Version to use. + dotnet-version: 10.0.x # SDK Version to use. - name: Cache Nuget packages uses: actions/cache@v4 diff --git a/.github/workflows/dotnet-build-example.yml b/.github/workflows/dotnet-build-example.yml index 254bac59..82aaf523 100644 --- a/.github/workflows/dotnet-build-example.yml +++ b/.github/workflows/dotnet-build-example.yml @@ -24,7 +24,7 @@ jobs: - name: Setup dotnet uses: actions/setup-dotnet@v4 with: - dotnet-version: 8.0.x # SDK Version to use. + dotnet-version: 10.0.x # SDK Version to use. - name: Cache Nuget packages uses: actions/cache@v4 diff --git a/.github/workflows/dotnet-build-linux.yml b/.github/workflows/dotnet-build-linux.yml index b31c92b6..bd3a7c82 100644 --- a/.github/workflows/dotnet-build-linux.yml +++ b/.github/workflows/dotnet-build-linux.yml @@ -20,7 +20,7 @@ jobs: - name: Setup dotnet uses: actions/setup-dotnet@v4 with: - dotnet-version: 8.0.x # SDK Version to use. + dotnet-version: 10.0.x # SDK Version to use. - name: Cache Nuget packages uses: actions/cache@v4 diff --git a/.github/workflows/dotnet-build-windows.yml b/.github/workflows/dotnet-build-windows.yml index f487a55f..3c7cad69 100644 --- a/.github/workflows/dotnet-build-windows.yml +++ b/.github/workflows/dotnet-build-windows.yml @@ -20,7 +20,7 @@ jobs: - name: Setup dotnet uses: actions/setup-dotnet@v4 with: - dotnet-version: 8.0.x # SDK Version to use. + dotnet-version: 10.0.x # SDK Version to use. - name: Setup MSBuild uses: microsoft/setup-msbuild@v1.1 @@ -45,4 +45,4 @@ jobs: run: msbuild src/WebEid.Security.sln /t:Build /p:Configuration=Release - name: Test - run: vstest.console.exe src/WebEid.Security.Tests/bin/Release/net8.0/WebEID.Security.Tests.dll + run: vstest.console.exe src/WebEid.Security.Tests/bin/Release/net10.0/WebEID.Security.Tests.dll diff --git a/.github/workflows/sonarcloud-analysis.yml b/.github/workflows/sonarcloud-analysis.yml index da52f375..c84a6649 100644 --- a/.github/workflows/sonarcloud-analysis.yml +++ b/.github/workflows/sonarcloud-analysis.yml @@ -22,7 +22,7 @@ jobs: - name: Setup dotnet uses: actions/setup-dotnet@v4 with: - dotnet-version: 6.0.x # SDK Version to use. + dotnet-version: 10.0.x # SDK Version to use. - name: Set up JDK 21 uses: actions/setup-java@v4 @@ -71,5 +71,6 @@ jobs: 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="$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.token="$env:SONAR_TOKEN" diff --git a/README.md b/README.md index 70658580..a58e265e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # web-eid-authtoken-validation-dotnet -![European Regional Development Fund](https://raw.githubusercontent.com/open-eid/DigiDoc4-Client/master/client/images/EL_Regionaalarengu_Fond.png) +European Regional Development Fund Web eID 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. @@ -310,6 +310,116 @@ When using standard [ASP.NET cookie authentication](https://docs.microsoft.com/e } ``` +- Similarly, the `MobileAuthInitController` generates a challenge nonce and returns the mobile deep-link for starting the Web eID Mobile authentication flow, and the `MobileAuthLoginController` handles the mobile login request by validating the returned authentication token and creating the authentication cookie. + ```cs + using System; + using System.Text; + using Microsoft.AspNetCore.Mvc; + using Microsoft.Extensions.Options; + using System.Text.Json; + using System.Text.Json.Serialization; + using Options; + using Security.Challenge; + + [ApiController] + [Route("auth/mobile")] + public class MobileAuthInitController( + IChallengeNonceGenerator nonceGenerator, + IOptions mobileOptions + ) : ControllerBase + { + private const string WebEidMobileAuthPath = "auth"; + private const string MobileLoginPath = "/auth/mobile/login"; + + [HttpPost("init")] + public IActionResult Init() + { + var challenge = nonceGenerator.GenerateAndStoreNonce(TimeSpan.FromMinutes(5)); + var challengeBase64 = challenge.Base64EncodedNonce; + + var loginUri = $"{Request.Scheme}://{Request.Host}{MobileLoginPath}"; + + var payload = new AuthPayload + { + Challenge = challengeBase64, + LoginUri = loginUri, + GetSigningCertificate = mobileOptions.Value.RequestSigningCert ? true : null + }; + + var json = JsonSerializer.Serialize(payload); + var encodedPayload = Convert.ToBase64String(Encoding.UTF8.GetBytes(json)); + + var authUri = BuildAuthUri(encodedPayload); + + return Ok(new AuthUri + { + AuthUriValue = authUri + }); + } + ``` + + ```cs + using Microsoft.AspNetCore.Mvc; + using System.Text.Json; + using Dto; + using Security.Challenge; + using Security.Validator; + using System.Security.Claims; + using System.Threading.Tasks; + using Microsoft.AspNetCore.Authentication; + using Microsoft.AspNetCore.Authentication.Cookies; + using Security.Util; + + [ApiController] + [Route("auth/mobile")] + public class MobileAuthLoginController( + IAuthTokenValidator authTokenValidator, + IChallengeNonceStore challengeNonceStore + ) : ControllerBase + { + [HttpPost("login")] + public async Task MobileLogin([FromBody] AuthenticateRequestDto dto) + { + if (dto?.AuthToken == null) + { + return BadRequest(new { error = "Missing auth_token" }); + } + + var parsedToken = dto.AuthToken; + var certificate = await authTokenValidator.Validate( + parsedToken, + challengeNonceStore.GetAndRemove().Base64EncodedNonce); + + var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme); + + identity.AddClaim(new Claim(ClaimTypes.GivenName, certificate.GetSubjectGivenName())); + identity.AddClaim(new Claim(ClaimTypes.Surname, certificate.GetSubjectSurname())); + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, certificate.GetSubjectIdCode())); + identity.AddClaim(new Claim(ClaimTypes.Name, certificate.GetSubjectCn())); + + if (!string.IsNullOrEmpty(parsedToken.UnverifiedSigningCertificate)) + { + identity.AddClaim(new Claim("signingCertificate", parsedToken.UnverifiedSigningCertificate)); + } + + if (parsedToken.SupportedSignatureAlgorithms != null) + { + identity.AddClaim(new Claim( + "supportedSignatureAlgorithms", + JsonSerializer.Serialize(parsedToken.SupportedSignatureAlgorithms))); + } + + await HttpContext.SignInAsync( + CookieAuthenticationDefaults.AuthenticationScheme, + new ClaimsPrincipal(identity), + new AuthenticationProperties { IsPersistent = false }); + + return Ok(new { redirect = "/welcome" }); + } + } + ``` + + # Table of contents * [Introduction](#introduction) @@ -442,6 +552,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 4fc5cd31..8b8a3f6f 100644 --- a/example/README.md +++ b/example/README.md @@ -1,6 +1,6 @@ # Web eID ASP.NET example -![European Regional Development Fund](https://github.com/open-eid/DigiDoc4-Client/blob/master/client/images/EL_Regionaalarengu_Fond.png) +European Regional Development Fund This project is an example ASP.NET web application that shows how to implement strong authentication and digital signing with electronic ID smart cards using Web eID. @@ -91,8 +91,8 @@ Set up the `libdigidocpp` library as follows: 1. Install the _libdigidocpp-4.0.0.8301.x64.msi_ package or higher. The installation packages are available from [https://github.com/open-eid/libdigidocpp/releases](https://github.com/open-eid/libdigidocpp/releases). 2. Copy the C# source files from the `libdigidocpp` installation folder `include\digidocpp_csharp` to the `src\WebEid.AspNetCore.Example\DigiDoc` folder. -3. Copy all files from the `libdigidocpp` installation folder to the example application build output folder `bin\Debug\net8.0` (after building, see next step). - * Windows: Also copy folder `schema` from `libdigidocpp` installation folder to the example application build output folder `bin\Debug\net8.0` +3. Copy all files from the `libdigidocpp` installation folder to the example application build output folder `bin\Debug\net10.0` (after building, see next step). + * Windows: Also copy folder `schema` from `libdigidocpp` installation folder to the example application build output folder `bin\Debug\net10.0` 4. When running in the `Development` profile, create an empty file named `EE_T.xml` for TSL cache as described in the [_Using test TSL lists_](https://github.com/open-eid/libdigidocpp/wiki/Using-test-TSL-lists#preconditions) section of the `libdigidocpp` wiki. #### For Ubuntu Linux @@ -123,7 +123,7 @@ Set up the `libdigidocpp` library as follows: 1. Install the *libdigidocpp_4.0.0.1460.pkg* package or higher. The installation packages are available from [https://github.com/open-eid/libdigidocpp/releases](https://github.com/open-eid/libdigidocpp/releases). 2. Copy the C# source files from `/Library/libdigidocpp/include/digidocpp_csharp` directory to `src/WebEid.AspNetCore.Example/DigiDoc` directory. -3. Go to `bin/Debug/net8.0` directory and create symbolic link to `/Library/libdigidocpp/lib/libdigidoc_csharp.dylib` library: +3. Go to `bin/Debug/net10.0` directory and create symbolic link to `/Library/libdigidocpp/lib/libdigidoc_csharp.dylib` library: ```cmd ln -s /Library/libdigidocpp/lib/libdigidoc_csharp.dylib ``` @@ -132,7 +132,7 @@ Further information is available in the [libdigidocpp example C# application sou ### 5. Build the application -You need to have the [.NET 8.0 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/8.0) installed for building the application package. +You need to have the [.NET 10.0 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/10.0) installed for building the application package. Build the application by running the following command in a terminal window under the `src` directory: ```cmd @@ -171,7 +171,8 @@ The `src\WebEid.AspNetCore.Example` directory contains the ASP.NET application s - digital signing, - `DigiDoc`: contains the C# binding files of the `libdigidocpp` library; these files must be copied from the `libdigidocpp` installation directory `\include\digidocpp_csharp`, - `Pages`: Razor pages, -- `Signing`: Web eID signing service implementation that uses `libdigidocpp`. +- `Services`: Web eID signing service implementation that uses `libdigidocpp`. +- `Options`: strongly-typed configuration classes for mobile Web eID settings such as `BaseRequestUri` and `RequestSigningCert` (when set to false, initiates a separate signing-certificate flow to demo requesting the certificate without prior authentication, as the signing certificate normally comes from the authentication flow). ## More information @@ -192,7 +193,7 @@ then please follow these steps in this chapter to build a Docker image in Ubuntu Before you begin, ensure you have the following installed on your system: -- .NET SDK 8.0 +- .NET SDK 10.0 - libdigidocpp-csharp You can install them using the following commands: @@ -205,7 +206,7 @@ sudo apt update ``` then install the packages ```sh -sudo apt install dotnet-sdk-8.0 libdigidocpp-csharp +sudo apt install dotnet-sdk-10.0 libdigidocpp-csharp ``` Add a NuGet package source for web-eid-authtoken-validation-dotnet library: @@ -238,7 +239,7 @@ To build the application, follow these steps: 4. Update the `OriginUrl` in the `appsettings.json` to match your production environment. Please replace https://localhost:8443 with your actual domain name where you intend to run the application: ```sh - sed -i 's#"OriginUrl": "https://localhost:44391"#"OriginUrl": "https://localhost:8443"#' WebEid.AspNetCore.Example/bin/Release/net8.0/publish/appsettings.json + sed -i 's#"OriginUrl": "https://localhost:44391"#"OriginUrl": "https://example.com"#' WebEid.AspNetCore.Example/bin/Release/net10.0/publish/appsettings.json ``` ### Building the Docker image @@ -299,3 +300,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/.dockerignore b/example/src/.dockerignore index 50e2413e..cc331793 100644 --- a/example/src/.dockerignore +++ b/example/src/.dockerignore @@ -24,4 +24,4 @@ LICENSE README.md -!WebEid.AspNetCore.Example/bin/Release/net8.0/publish/ +!WebEid.AspNetCore.Example/bin/Release/net10.0/publish/ diff --git a/example/src/.editorconfig b/example/src/.editorconfig new file mode 100644 index 00000000..eadfeba8 --- /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/Dockerfile b/example/src/Dockerfile index c02568b4..005bbb69 100644 --- a/example/src/Dockerfile +++ b/example/src/Dockerfile @@ -13,7 +13,7 @@ RUN echo "deb [signed-by=/usr/share/keyrings/ria-repository.gpg] https://install apt-get clean && \ rm -rf /var/lib/apt/lists -COPY ./WebEid.AspNetCore.Example/bin/Release/net8.0/publish/ . +COPY ./WebEid.AspNetCore.Example/bin/Release/net10.0/publish/ . COPY ./WebEid.AspNetCore.Example/Certificates/Dev/self-signed-server-certificate.pfx /https/self-signed-server-certificate.pfx diff --git a/example/src/WebEid.AspNetCore.Example/Certificates/CertificateLoader.cs b/example/src/WebEid.AspNetCore.Example/Certificates/CertificateLoader.cs index 186694c1..bcc8231c 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 ddc89830..6029248b 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 b8873fda..bd1430d9 100644 --- a/example/src/WebEid.AspNetCore.Example/Controllers/Api/AuthController.cs +++ b/example/src/WebEid.AspNetCore.Example/Controllers/Api/AuthController.cs @@ -19,70 +19,110 @@ namespace WebEid.AspNetCore.Example.Controllers.Api { + 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 System.Collections.Generic; - using System.Security.Claims; - using System.Threading.Tasks; - using Security.Challenge; using WebEid.AspNetCore.Example.Dto; - using System; + using WebEid.Security.AuthToken; + using WebEid.Security.Exceptions; [Route("[controller]")] [ApiController] - public class AuthController : BaseController + public class AuthController(IAuthTokenValidator authTokenValidator, IChallengeNonceStore challengeNonceStore) : BaseController { - private readonly IAuthTokenValidator authTokenValidator; - private readonly IChallengeNonceStore challengeNonceStore; + private readonly IAuthTokenValidator authTokenValidator = authTokenValidator; + private readonly IChallengeNonceStore challengeNonceStore = challengeNonceStore; + + [HttpPost("login")] + public async Task Login([FromBody] AuthenticateRequestDto dto) + { + if (dto?.AuthToken is null) + { + return BadRequest(new { error = "Missing auth_token" }); + } + + try + { + await SignInUser(dto.AuthToken); + return Ok(); + } + catch (Exception ex) when (ex is ChallengeNonceNotFoundException or ChallengeNonceExpiredException) + { + return Unauthorized(new { error = "challenge_nonce_not_found_or_expired" }); + } + catch (AuthTokenException) + { + return Unauthorized(new { error = "authentication_failed" }); + } + } - public AuthController(IAuthTokenValidator authTokenValidator, IChallengeNonceStore challengeNonceStore) + [HttpPost("logout")] + public async Task Logout() { - this.authTokenValidator = authTokenValidator; - this.challengeNonceStore = challengeNonceStore; + if (HasActiveSession()) + { + RemoveUserContainerFile(); + } + + await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); } - [HttpPost] - [Route("login")] - public async Task Login([FromBody] AuthenticateRequestDto authToken) + private async Task SignInUser(WebEidAuthToken authToken) { - var certificate = await this.authTokenValidator.Validate(authToken.AuthToken, this.challengeNonceStore.GetAndRemove().Base64EncodedNonce); + if (authToken == null) + { + throw new ArgumentNullException(nameof(authToken), "authToken must not be null"); + } - List claims = new(); + var certificate = await authTokenValidator.Validate(authToken, challengeNonceStore.GetAndRemove().Base64EncodedNonce); + var claims = new List(); AddNewClaimIfCertificateHasData(claims, ClaimTypes.GivenName, certificate.GetSubjectGivenName); AddNewClaimIfCertificateHasData(claims, ClaimTypes.Surname, certificate.GetSubjectSurname); AddNewClaimIfCertificateHasData(claims, ClaimTypes.NameIdentifier, certificate.GetSubjectIdCode); AddNewClaimIfCertificateHasData(claims, ClaimTypes.Name, certificate.GetSubjectCn); - var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); + var signingCertificate = authToken.UnverifiedSigningCertificates != null && + authToken.UnverifiedSigningCertificates.Count > 0 + ? authToken.UnverifiedSigningCertificates[0] + : null; + + if (signingCertificate != null && !string.IsNullOrEmpty(signingCertificate.Certificate)) + { + claims.Add(new Claim( + "signingCertificate", + signingCertificate.Certificate)); + } - var authProperties = new AuthenticationProperties + if (signingCertificate?.SupportedSignatureAlgorithms != null) { - AllowRefresh = true - }; + claims.Add(new Claim( + "supportedSignatureAlgorithms", + JsonSerializer.Serialize(signingCertificate.SupportedSignatureAlgorithms))); + } + + var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); await HttpContext.SignInAsync( CookieAuthenticationDefaults.AuthenticationScheme, - new ClaimsPrincipal(claimsIdentity), - authProperties); - + 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(); } - [HttpGet] - [Route("logout")] - public async Task Logout() - { - RemoveUserContainerFile(); - await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); - } - - 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 79831e35..b62e18d7 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,18 @@ public abstract class BaseController : ControllerBase { - const string uniqueIdKey = "UniqueId"; - - protected void RemoveUserContainerFile() - { - System.IO.File.Delete(GetUserContainerName()); - } - - protected void SetUniqueIdInSession() - { - HttpContext.Session.SetString(uniqueIdKey, Guid.NewGuid().ToString()); - } - - private string GetUniqueIdFromSession() - { - return HttpContext.Session.GetString(uniqueIdKey); - } - - protected string GetUserContainerName() - { - return $"container_{GetUniqueIdFromSession()}"; - } + private const string UniqueIdKey = "UniqueId"; + + protected void RemoveUserContainerFile() => System.IO.File.Delete(GetUserContainerName()); + + protected void SetUniqueIdInSession() => HttpContext.Session.SetString(UniqueIdKey, Guid.NewGuid().ToString()); + + protected bool HasActiveSession() => GetUniqueIdFromSession() is not null; + + protected UnauthorizedObjectResult SessionExpired() => Unauthorized(new { error = "session_expired" }); + + private string? GetUniqueIdFromSession() => HttpContext.Session.GetString(UniqueIdKey); + + 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 764a1bfb..80d22aaf 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 new file mode 100644 index 00000000..fa98d9a8 --- /dev/null +++ b/example/src/WebEid.AspNetCore.Example/Controllers/Api/MobileAuthInitController.cs @@ -0,0 +1,93 @@ +// 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 +// 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.AspNetCore.Example.Controllers.Api +{ + using System; + using System.Text; + using System.Text.Json; + using System.Text.Json.Serialization; + using Microsoft.AspNetCore.Mvc; + using Microsoft.Extensions.Configuration; + using Microsoft.Extensions.Options; + using Options; + using Security.Challenge; + using Services; + + [ApiController] + [Route("auth/mobile")] + public class MobileAuthInitController( + IChallengeNonceGenerator nonceGenerator, + IOptions mobileOptions, + MobileRequestUriBuilder uriBuilder, + IConfiguration configuration + ) : ControllerBase + { + private const string WebEidMobileAuthPath = "auth"; + private const string MobileLoginPath = "/auth/mobile/login"; + + [HttpPost("init")] + public IActionResult Init() + { + var challenge = nonceGenerator.GenerateAndStoreNonce(TimeSpan.FromMinutes(5)); + var challengeBase64 = challenge.Base64EncodedNonce; + + var loginUri = $"{configuration["OriginUrl"]}{MobileLoginPath}"; + + var payload = new AuthPayload + { + Challenge = challengeBase64, + LoginUri = loginUri, + GetSigningCertificate = mobileOptions.Value.RequestSigningCert ? true : null + }; + + var json = JsonSerializer.Serialize(payload); + var encodedPayload = Convert.ToBase64String(Encoding.UTF8.GetBytes(json)); + + var authUri = uriBuilder.Build(WebEidMobileAuthPath, encodedPayload); + + return Ok(new AuthUri + { + AuthUriValue = authUri + }); + } + + private sealed record AuthPayload + { + [JsonInclude] + [JsonPropertyName("challenge")] + public required string Challenge { get; init; } + + [JsonInclude] + [JsonPropertyName("loginUri")] + public required string LoginUri { get; init; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("getSigningCertificate")] + public bool? GetSigningCertificate { get; init; } + } + + private sealed record AuthUri + { + [JsonInclude] + [JsonPropertyName("authUri")] + public required string AuthUriValue { get; init; } + } + } +} diff --git a/example/src/WebEid.AspNetCore.Example/Controllers/Api/SignController.cs b/example/src/WebEid.AspNetCore.Example/Controllers/Api/SignController.cs index d4f68065..7a1670f6 100644 --- a/example/src/WebEid.AspNetCore.Example/Controllers/Api/SignController.cs +++ b/example/src/WebEid.AspNetCore.Example/Controllers/Api/SignController.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2021-2024 Estonian Information System Authority +// Copyright (c) 2021-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 @@ -17,58 +17,138 @@ // 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 System.IO; using System.Security.Claims; using System.Threading.Tasks; + using Dto; + using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; - using Microsoft.Extensions.Logging; using Services; - using WebEid.AspNetCore.Example.Dto; + using Signing; [Route("[controller]")] [ApiController] - public class SignController : BaseController + [Authorize(Policy = "LoggedInOnly")] + public class SignController(SigningService signingService, MobileSigningService mobileSigningService) : BaseController { private const string SignedFile = "example-for-signing.asice"; - private readonly SigningService signingService; - private readonly ILogger logger; + private readonly SigningService signingService = signingService; + private readonly MobileSigningService mobileSigningService = mobileSigningService; - public SignController(SigningService signingService, ILogger logger) + [HttpPost("prepare")] + public ActionResult Prepare([FromBody] CertificateDto data) { - this.signingService = signingService; - this.logger = logger; + if (!HasActiveSession()) + { + return SessionExpired(); + } + + var identity = HttpContext.User.Identity as ClaimsIdentity + ?? throw new InvalidOperationException("User identity is missing or invalid."); + + try + { + return signingService.PrepareContainer(data, identity, GetUserContainerName()); + } + catch (ArgumentException) + { + return BadRequest(new { error = "signing_certificate_mismatch" }); + } } - [Route("prepare")] - [HttpPost] - public DigestDto Prepare([FromBody] CertificateDto data) + [HttpPost("sign")] + public ActionResult Sign([FromBody] SignatureDto data) { - return signingService.PrepareContainer(data, (ClaimsIdentity)HttpContext.User.Identity, GetUserContainerName()); + if (!HasActiveSession()) + { + return SessionExpired(); + } + + signingService.SignContainer(data, GetUserContainerName()); + return new FileDto(SignedFile); } - [Route("sign")] - [HttpPost] - public FileDto Sign([FromBody] SignatureDto data) + [HttpPost("mobile/init")] + public ActionResult MobileInit() { - signingService.SignContainer(data, GetUserContainerName()); + if (!HasActiveSession()) + { + return SessionExpired(); + } + + var identity = HttpContext.User.Identity as ClaimsIdentity + ?? throw new InvalidOperationException("User identity is missing or invalid."); + + var container = GetUserContainerName(); + + try + { + return mobileSigningService.InitCertificateOrSigningRequest(identity, container); + } + catch (ArgumentException) + { + return BadRequest(new { error = "signing_certificate_mismatch" }); + } + } + + [HttpPost("mobile/certificate")] + public ActionResult CertificatePost([FromBody] CertificateDto certificateDto) + { + if (!HasActiveSession()) + { + return SessionExpired(); + } + + var identity = HttpContext.User.Identity as ClaimsIdentity + ?? throw new InvalidOperationException("User identity is missing or invalid."); + + var containerName = GetUserContainerName(); + + try + { + return mobileSigningService.InitSigningRequest(identity, certificateDto, containerName); + } + catch (ArgumentException) + { + return BadRequest(new { error = "signing_certificate_mismatch" }); + } + } + + [HttpPost("mobile/signature")] + public ActionResult SignaturePost([FromBody] SignatureDto signatureDto) + { + if (!HasActiveSession()) + { + return SessionExpired(); + } + + signingService.SignContainer(signatureDto, GetUserContainerName()); return new FileDto(SignedFile); } - [Route("download")] - [HttpGet] + [HttpGet("download")] public async Task Download() { + if (!HasActiveSession()) + { + return SessionExpired(); + } + try { var content = await System.IO.File.ReadAllBytesAsync(GetUserContainerName()); return File(content, "application/vnd.etsi.asic-e+zip", SignedFile); } - catch (Exception ex) + catch (InvalidOperationException) + { + return SessionExpired(); + } + catch (FileNotFoundException) { - logger?.LogError(ex, "Error occurred while downloading user container file"); - return BadRequest(); + return NotFound(); } } } diff --git a/example/src/WebEid.AspNetCore.Example/Controllers/WelcomeController.cs b/example/src/WebEid.AspNetCore.Example/Controllers/WelcomeController.cs index c9743e4d..77907305 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 c36726f2..743bfacf 100644 --- a/example/src/WebEid.AspNetCore.Example/Dto/AuthenticateRequestDto.cs +++ b/example/src/WebEid.AspNetCore.Example/Dto/AuthenticateRequestDto.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2021-2024 Estonian Information System Authority +// Copyright (c) 2021-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 @@ -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.Dto +namespace WebEid.AspNetCore.Example.Dto { using System.Text.Json.Serialization; using Security.AuthToken; public class AuthenticateRequestDto { - [JsonPropertyName("auth-token")] - public WebEidAuthToken AuthToken { get; set; } + [JsonPropertyName("auth_token")] + 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 5fabd62c..404bcadb 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 63aa24c0..2d0f8268 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 08b1d873..3e941808 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 b29cd148..2f5b6655 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 3ac75277..3deb05b7 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 ff5e9d80..3714b974 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 dc01f3e0..47cb0133 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 new file mode 100644 index 00000000..f2f75cba --- /dev/null +++ b/example/src/WebEid.AspNetCore.Example/Options/WebEidMobileOptions.cs @@ -0,0 +1,32 @@ +// 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 +// 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.AspNetCore.Example.Options +{ + using System.ComponentModel.DataAnnotations; + + public class WebEidMobileOptions + { + [Required] + [RegularExpression("^.*(?:[^/]|://)$", ErrorMessage = "Base URI must not have a trailing slash")] + public string BaseRequestUri { get; set; } = null!; + + public bool RequestSigningCert { get; set; } + } +} diff --git a/example/src/WebEid.AspNetCore.Example/Pages/Index.cshtml b/example/src/WebEid.AspNetCore.Example/Pages/Index.cshtml index d25e828d..5823f465 100644 --- a/example/src/WebEid.AspNetCore.Example/Pages/Index.cshtml +++ b/example/src/WebEid.AspNetCore.Example/Pages/Index.cshtml @@ -2,16 +2,28 @@ @inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Xsrf + + @{ + var tokens = Xsrf.GetAndStoreTokens(HttpContext); + } + + + Web eID: electronic ID smart cards on the Web - - + + + + + + + + +
@@ -22,96 +34,512 @@ secure authentication and digital signing of documents on the web using public-key cryptography.

- Estonian, Finnish, Latvian, Lithuanian and Croatian eID cards are supported in the first phase, but only - Estonian eID card support is currently enabled in the test application below. + 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 + 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 +
-

- More information about the Web eID project, including installation and usage instructions - is available on the project [website](https://web-eid.eu/). -

-

Click Authenticate below to test authentication and digital signing.

+
+

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 privacy policy of the test service is available here. -

-
-
- - -
- 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 new file mode 100644 index 00000000..03263888 --- /dev/null +++ b/example/src/WebEid.AspNetCore.Example/Pages/WebEidCallback.cshtml @@ -0,0 +1,104 @@ +@page "/sign/mobile/{mode}" +@model WebEid.AspNetCore.Example.Pages.WebEidCallbackModel +@inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Xsrf + +@{ + var tokens = Xsrf.GetAndStoreTokens(HttpContext); +} + + + + + + + + + + + Completing signing… + + + + + +
+

Completing signing…

+
+
+ + + + + + + + + + diff --git a/example/src/WebEid.AspNetCore.Example/Pages/WebEidCallback.cshtml.cs b/example/src/WebEid.AspNetCore.Example/Pages/WebEidCallback.cshtml.cs new file mode 100644 index 00000000..9f21a9ca --- /dev/null +++ b/example/src/WebEid.AspNetCore.Example/Pages/WebEidCallback.cshtml.cs @@ -0,0 +1,31 @@ +// 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 +// 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.AspNetCore.Example.Pages +{ + using Microsoft.AspNetCore.Mvc.RazorPages; + + public class WebEidCallbackModel : PageModel + { + public void OnGet() + { + /* Intentionally left empty. This Razor Page only renders HTML and JavaScript for WebEID callback. */ + } + } +} diff --git a/example/src/WebEid.AspNetCore.Example/Pages/WebEidLogin.cshtml b/example/src/WebEid.AspNetCore.Example/Pages/WebEidLogin.cshtml new file mode 100644 index 00000000..56265fe1 --- /dev/null +++ b/example/src/WebEid.AspNetCore.Example/Pages/WebEidLogin.cshtml @@ -0,0 +1,77 @@ +@page "/auth/mobile/login" +@model WebEid.AspNetCore.Example.Pages.WebEidLoginModel +@inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Xsrf + +@{ + var tokens = Xsrf.GetAndStoreTokens(HttpContext); +} + + + + + + + + Signing you in… + + + + + + + + + +
+

Signing you in…

+
+
+ + + + + + + + + diff --git a/example/src/WebEid.AspNetCore.Example/Pages/WebEidLogin.cshtml.cs b/example/src/WebEid.AspNetCore.Example/Pages/WebEidLogin.cshtml.cs new file mode 100644 index 00000000..361ceb7a --- /dev/null +++ b/example/src/WebEid.AspNetCore.Example/Pages/WebEidLogin.cshtml.cs @@ -0,0 +1,31 @@ +// 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 +// 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.AspNetCore.Example.Pages +{ + using Microsoft.AspNetCore.Mvc.RazorPages; + + public class WebEidLoginModel : PageModel + { + public void OnGet() + { + /* Intentionally left empty. This Razor Page only renders HTML and JavaScript for WebEID login. */ + } + } +} diff --git a/example/src/WebEid.AspNetCore.Example/Pages/Welcome.cshtml b/example/src/WebEid.AspNetCore.Example/Pages/Welcome.cshtml index 3f7eaad5..2dbfba1f 100644 --- a/example/src/WebEid.AspNetCore.Example/Pages/Welcome.cshtml +++ b/example/src/WebEid.AspNetCore.Example/Pages/Welcome.cshtml @@ -1,23 +1,37 @@ @page @model WebEid.AspNetCore.Example.Pages.WelcomeModel @inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Xsrf + +@{ + var tokens = Xsrf.GetAndStoreTokens(HttpContext); +} + - + + - + + + + Welcome! - - + + + +
-
+ +

+ +

+ +

Digital signing

Welcome, @Model.PrincipalName!

@@ -26,106 +40,138 @@

- To test digital signing, you can sign the following document by clicking - Sign document below: + To test digital signing, click Sign document:

-
-
-
- - - + + document.addEventListener("DOMContentLoaded", () => { + const urlParams = new URLSearchParams(window.location.search); + const signedFile = urlParams.get("signed"); + + if (signedFile) { + document.querySelector("#webeid-sign-button").style.display = "none"; + document.querySelector("#example-document").style.display = "none"; + document.querySelector("#webeid-download-button").style.display = "inline-block"; + document.querySelector("#file-name").textContent = "Signature added: " + signedFile; + } + }); + + + diff --git a/example/src/WebEid.AspNetCore.Example/Pages/Welcome.cshtml.cs b/example/src/WebEid.AspNetCore.Example/Pages/Welcome.cshtml.cs index 7e19ee1a..90459059 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 af5995fb..5f3556ad 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/ContainerCleanupService.cs b/example/src/WebEid.AspNetCore.Example/Services/ContainerCleanupService.cs new file mode 100644 index 00000000..d16f1262 --- /dev/null +++ b/example/src/WebEid.AspNetCore.Example/Services/ContainerCleanupService.cs @@ -0,0 +1,71 @@ +// Copyright (c) 2026-2026 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.AspNetCore.Example.Services +{ + using System; + using System.IO; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.Extensions.Hosting; + using Microsoft.Extensions.Logging; + + public class ContainerCleanupService(ILogger logger) : BackgroundService + { + private static readonly TimeSpan CleanupInterval = TimeSpan.FromMinutes(30); + private static readonly TimeSpan MaxFileAge = TimeSpan.FromHours(1); + private readonly ILogger logger = logger; + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + DeleteOldContainerFiles(); + + await Task.Delay(CleanupInterval, stoppingToken); + } + } + + private void DeleteOldContainerFiles() + { + var directory = Directory.GetCurrentDirectory(); + var threshold = DateTime.UtcNow - MaxFileAge; + + foreach (var file in Directory.EnumerateFiles(directory, "container_*")) + { + try + { + var lastWriteTime = File.GetLastWriteTimeUtc(file); + + if (lastWriteTime < threshold) + { + File.Delete(file); +#pragma warning disable CA1873 + logger.LogInformation("Deleted old container file: {File}", file); +#pragma warning restore CA1873 + } + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) + { + logger.LogWarning(ex, "Failed to delete old container file: {File}", file); + } + } + } + } +} diff --git a/example/src/WebEid.AspNetCore.Example/Services/MobileRequestUriBuilder.cs b/example/src/WebEid.AspNetCore.Example/Services/MobileRequestUriBuilder.cs new file mode 100644 index 00000000..662b1122 --- /dev/null +++ b/example/src/WebEid.AspNetCore.Example/Services/MobileRequestUriBuilder.cs @@ -0,0 +1,34 @@ +// 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 +// 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.AspNetCore.Example.Services +{ + using System; + using Microsoft.Extensions.Options; + using Options; + + public class MobileRequestUriBuilder(IOptions options) + { + private readonly string baseUri = options.Value.BaseRequestUri; + + 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 e269f36d..51542d82 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 6e4854cd..21e153ca 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 new file mode 100644 index 00000000..536b5a8a --- /dev/null +++ b/example/src/WebEid.AspNetCore.Example/Signing/MobileSigningService.cs @@ -0,0 +1,136 @@ +// 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 +// 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.AspNetCore.Example.Signing +{ + using System.Collections.Generic; + using System.Security.Claims; + using System.Text; + using System.Text.Json; + using System.Text.Json.Serialization; + using Dto; + using Microsoft.AspNetCore.Http; + using Microsoft.AspNetCore.WebUtilities; + using Microsoft.Extensions.Configuration; + using Services; + + public class MobileSigningService( + SigningService signingService, + IHttpContextAccessor httpContextAccessor, + MobileRequestUriBuilder uriBuilder, + IConfiguration configuration + ) + { + private const string WebEidMobileSignPath = "sign"; + private const string WebEidMobileCertPath = "cert"; + private const string CertificateResponsePath = "/sign/mobile/certificate"; + private const string SignatureResponsePath = "/sign/mobile/signature"; + + public MobileInitRequest InitCertificateOrSigningRequest(ClaimsIdentity identity, string containerName) + { + var signingCert = identity.FindFirst("signingCertificate")?.Value; + var algosJson = identity.FindFirst("supportedSignatureAlgorithms")?.Value; + + if (string.IsNullOrEmpty(signingCert) || string.IsNullOrEmpty(algosJson)) + { + return InitCertificateRequest(); + } + + var algorithms = + JsonSerializer.Deserialize>(algosJson) + ?? []; + + var certDto = new CertificateDto + { + CertificateBase64String = signingCert, + SupportedSignatureAlgorithms = algorithms + }; + + return InitSigningRequest(identity, certDto, containerName); + } + + private MobileInitRequest InitCertificateRequest() + { + var responseUri = $"{configuration["OriginUrl"]}{CertificateResponsePath}"; + var requestObj = new RequestObject + { + ResponseUri = responseUri + }; + + var encoded = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(requestObj))); + var requestUri = uriBuilder.Build(WebEidMobileCertPath, encoded); + + return new MobileInitRequest + { + RequestUri = requestUri + }; + } + + public MobileInitRequest InitSigningRequest( + ClaimsIdentity identity, + CertificateDto certificateDto, + string containerName) + { + var digest = signingService.PrepareContainer(certificateDto, identity, containerName); + var responseUri = $"{configuration["OriginUrl"]}{SignatureResponsePath}"; + + var requestObj = new RequestObject + { + ResponseUri = responseUri, + SigningCertificate = certificateDto.CertificateBase64String, + Hash = digest.Hash, + HashFunction = digest.HashFunction + }; + + var encoded = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(requestObj))); + var requestUri = uriBuilder.Build(WebEidMobileSignPath, encoded); + + return new MobileInitRequest + { + RequestUri = requestUri + }; + } + + public sealed record MobileInitRequest + { + [JsonInclude] + [JsonPropertyName("requestUri")] + public required string RequestUri { get; init; } + } + + internal sealed record RequestObject + { + [JsonInclude] + [JsonPropertyName("responseUri")] + public required string ResponseUri { get; init; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("signingCertificate")] + public string? SigningCertificate { get; init; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("hash")] + public string? Hash { get; init; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("hashFunction")] + public string? HashFunction { get; init; } + } + } +} diff --git a/example/src/WebEid.AspNetCore.Example/Signing/SigningService.cs b/example/src/WebEid.AspNetCore.Example/Signing/SigningService.cs index edb37d5a..422a4f3a 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 bbabba4d..051ee692 100644 --- a/example/src/WebEid.AspNetCore.Example/Startup.cs +++ b/example/src/WebEid.AspNetCore.Example/Startup.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2021-2024 Estonian Information System Authority +// Copyright (c) 2021-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,76 +19,64 @@ 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 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 => { options.Cookie.Name = "__Host-WebEid.AspNetCore.Example.Auth"; options.Cookie.SecurePolicy = CookieSecurePolicy.Always; - options.Cookie.SameSite = SameSiteMode.Strict; + // Set to "strict" if Web eID for Mobile flow is not used - this would restrict sending back the + // authentication response in the Web eID for Mobile flow. + options.Cookie.SameSite = SameSiteMode.Lax; options.Events.OnRedirectToLogin = context => { context.Response.Redirect("/"); @@ -105,11 +93,20 @@ public void ConfigureServices(IServiceCollection services) { options.Cookie.Name = "__Host-WebEid.AspNetCore.Example.Session"; options.Cookie.SecurePolicy = CookieSecurePolicy.Always; - options.Cookie.SameSite = SameSiteMode.Strict; - options.IdleTimeout = TimeSpan.FromSeconds(60); + // Set to "strict" if Web eID for Mobile flow is not used - this would restrict sending back the + // authentication response in the Web eID for Mobile flow. + options.Cookie.SameSite = SameSiteMode.Lax; + options.IdleTimeout = TimeSpan.FromSeconds(300); options.Cookie.IsEssential = true; }); + services.AddOptions() + .Bind(Configuration.GetSection("WebEidMobile")) + .ValidateDataAnnotations() + .ValidateOnStart(); + + services.AddSingleton(); + var url = GetOriginUrl(Configuration); services.AddSingleton(new AuthTokenValidatorBuilder(logger) @@ -119,22 +116,22 @@ public void ConfigureServices(IServiceCollection services) services.AddSingleton(RandomNumberGenerator.Create()); services.AddSingleton(); + services.AddScoped(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - services.AddAntiforgery(options => - { - options.Cookie.SecurePolicy = CookieSecurePolicy.Always; - }); + services.AddAntiforgery(options => options.Cookie.SecurePolicy = CookieSecurePolicy.Always); + + services.AddHostedService(); // 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(); }); } @@ -153,6 +150,9 @@ private static Uri GetOriginUrl(IConfiguration configuration) // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { + // Add support for running behind a TLS terminating proxy. + app.UseForwardedHeaders(); + if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); @@ -162,16 +162,14 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); - // Add support for running behind a TLS terminating proxy. - app.UseForwardedHeaders(); } app.UseStaticFiles(); app.UseRouting(); - app.UseAuthorization(); app.UseAuthentication(); + app.UseAuthorization(); app.UseSession(); app.UseEndpoints(endpoints => diff --git a/example/src/WebEid.AspNetCore.Example/WebEid.AspNetCore.Example.csproj b/example/src/WebEid.AspNetCore.Example/WebEid.AspNetCore.Example.csproj index c94ba60d..1310bcd6 100644 --- a/example/src/WebEid.AspNetCore.Example/WebEid.AspNetCore.Example.csproj +++ b/example/src/WebEid.AspNetCore.Example/WebEid.AspNetCore.Example.csproj @@ -1,7 +1,8 @@  - net8.0 + net10.0 + enable aspnet-web_eid_asp_dotnet_example-3FF31439-FDC0-4164-B154-03E005FE96CD WebEid.AspNetCore.Example false @@ -12,15 +13,20 @@ - - - - - - + + + + + + + + + + diff --git a/example/src/WebEid.AspNetCore.Example/appsettings.Development.json b/example/src/WebEid.AspNetCore.Example/appsettings.Development.json index a0f43a06..8d2e1d3f 100644 --- a/example/src/WebEid.AspNetCore.Example/appsettings.Development.json +++ b/example/src/WebEid.AspNetCore.Example/appsettings.Development.json @@ -1,6 +1,10 @@ { "OriginUrl": "https://localhost:44391", "DetailedErrors": true, + "WebEidMobile": { + "BaseRequestUri": "web-eid-mobile://", + "RequestSigningCert": false + }, "Logging": { "LogLevel": { "Default": "Information", diff --git a/example/src/WebEid.AspNetCore.Example/appsettings.json b/example/src/WebEid.AspNetCore.Example/appsettings.json index 7d0c71b3..b51d7119 100644 --- a/example/src/WebEid.AspNetCore.Example/appsettings.json +++ b/example/src/WebEid.AspNetCore.Example/appsettings.json @@ -1,5 +1,9 @@ { "OriginUrl": "https://localhost:44391", + "WebEidMobile": { + "BaseRequestUri": "web-eid-mobile://", + "RequestSigningCert": false + }, "Logging": { "LogLevel": { "Default": "Information", diff --git a/example/src/WebEid.AspNetCore.Example/wwwroot/css/bootstrap.min.css b/example/src/WebEid.AspNetCore.Example/wwwroot/css/bootstrap.min.css index 7d2a868f..4dc744c1 100644 --- a/example/src/WebEid.AspNetCore.Example/wwwroot/css/bootstrap.min.css +++ b/example/src/WebEid.AspNetCore.Example/wwwroot/css/bootstrap.min.css @@ -1,7 +1,6 @@ -/*! - * Bootstrap v4.5.0 (https://getbootstrap.com/) - * Copyright 2011-2020 The Bootstrap Authors - * Copyright 2011-2020 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - */:root{--blue:#007bff;--indigo:#6610f2;--purple:#6f42c1;--pink:#e83e8c;--red:#dc3545;--orange:#fd7e14;--yellow:#ffc107;--green:#28a745;--teal:#20c997;--cyan:#17a2b8;--white:#fff;--gray:#6c757d;--gray-dark:#343a40;--primary:#007bff;--secondary:#6c757d;--success:#28a745;--info:#17a2b8;--warning:#ffc107;--danger:#dc3545;--light:#f8f9fa;--dark:#343a40;--breakpoint-xs:0;--breakpoint-sm:576px;--breakpoint-md:768px;--breakpoint-lg:992px;--breakpoint-xl:1200px;--font-family-sans-serif:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-family-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus:not(:focus-visible){outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]){color:inherit;text-decoration:none}a:not([href]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-bottom:.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:2.5rem}.h2,h2{font-size:2rem}.h3,h3{font-size:1.75rem}.h4,h4{font-size:1.5rem}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:6rem;font-weight:300;line-height:1.2}.display-2{font-size:5.5rem;font-weight:300;line-height:1.2}.display-3{font-size:4.5rem;font-weight:300;line-height:1.2}.display-4{font-size:3.5rem;font-weight:300;line-height:1.2}hr{margin-top:1rem;margin-bottom:1rem;border:0;border-top:1px solid rgba(0,0,0,.1)}.small,small{font-size:80%;font-weight:400}.mark,mark{padding:.2em;background-color:#fcf8e3}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:90%;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote-footer{display:block;font-size:80%;color:#6c757d}.blockquote-footer::before{content:"\2014\00A0"}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #dee2e6;border-radius:.25rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:90%;color:#6c757d}code{font-size:87.5%;color:#e83e8c;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:87.5%;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:100%;font-weight:700}pre{display:block;font-size:87.5%;color:#212529}pre code{font-size:inherit;color:inherit;word-break:normal}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:576px){.container{max-width:540px}}@media (min-width:768px){.container{max-width:720px}}@media (min-width:992px){.container{max-width:960px}}@media (min-width:1200px){.container{max-width:1140px}}.container-fluid,.container-lg,.container-md,.container-sm,.container-xl{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}.row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-15px;margin-left:-15px}.no-gutters{margin-right:0;margin-left:0}.no-gutters>.col,.no-gutters>[class*=col-]{padding-right:0;padding-left:0}.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-auto,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-auto,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-auto,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-auto,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xl-auto{position:relative;width:100%;padding-right:15px;padding-left:15px}.col{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;min-width:0;max-width:100%}.row-cols-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-first{-ms-flex-order:-1;order:-1}.order-last{-ms-flex-order:13;order:13}.order-0{-ms-flex-order:0;order:0}.order-1{-ms-flex-order:1;order:1}.order-2{-ms-flex-order:2;order:2}.order-3{-ms-flex-order:3;order:3}.order-4{-ms-flex-order:4;order:4}.order-5{-ms-flex-order:5;order:5}.order-6{-ms-flex-order:6;order:6}.order-7{-ms-flex-order:7;order:7}.order-8{-ms-flex-order:8;order:8}.order-9{-ms-flex-order:9;order:9}.order-10{-ms-flex-order:10;order:10}.order-11{-ms-flex-order:11;order:11}.order-12{-ms-flex-order:12;order:12}.offset-1{margin-left:8.333333%}.offset-2{margin-left:16.666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.333333%}.offset-5{margin-left:41.666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.333333%}.offset-8{margin-left:66.666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.333333%}.offset-11{margin-left:91.666667%}@media (min-width:576px){.col-sm{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;min-width:0;max-width:100%}.row-cols-sm-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-sm-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-sm-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-sm-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-sm-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-sm-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-sm-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-sm-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-sm-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-sm-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-sm-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-sm-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-sm-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-sm-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-sm-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-sm-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-sm-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-sm-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-sm-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-sm-first{-ms-flex-order:-1;order:-1}.order-sm-last{-ms-flex-order:13;order:13}.order-sm-0{-ms-flex-order:0;order:0}.order-sm-1{-ms-flex-order:1;order:1}.order-sm-2{-ms-flex-order:2;order:2}.order-sm-3{-ms-flex-order:3;order:3}.order-sm-4{-ms-flex-order:4;order:4}.order-sm-5{-ms-flex-order:5;order:5}.order-sm-6{-ms-flex-order:6;order:6}.order-sm-7{-ms-flex-order:7;order:7}.order-sm-8{-ms-flex-order:8;order:8}.order-sm-9{-ms-flex-order:9;order:9}.order-sm-10{-ms-flex-order:10;order:10}.order-sm-11{-ms-flex-order:11;order:11}.order-sm-12{-ms-flex-order:12;order:12}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.333333%}.offset-sm-2{margin-left:16.666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.333333%}.offset-sm-5{margin-left:41.666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.333333%}.offset-sm-8{margin-left:66.666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.333333%}.offset-sm-11{margin-left:91.666667%}}@media (min-width:768px){.col-md{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;min-width:0;max-width:100%}.row-cols-md-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-md-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-md-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-md-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-md-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-md-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-md-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-md-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-md-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-md-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-md-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-md-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-md-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-md-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-md-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-md-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-md-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-md-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-md-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-md-first{-ms-flex-order:-1;order:-1}.order-md-last{-ms-flex-order:13;order:13}.order-md-0{-ms-flex-order:0;order:0}.order-md-1{-ms-flex-order:1;order:1}.order-md-2{-ms-flex-order:2;order:2}.order-md-3{-ms-flex-order:3;order:3}.order-md-4{-ms-flex-order:4;order:4}.order-md-5{-ms-flex-order:5;order:5}.order-md-6{-ms-flex-order:6;order:6}.order-md-7{-ms-flex-order:7;order:7}.order-md-8{-ms-flex-order:8;order:8}.order-md-9{-ms-flex-order:9;order:9}.order-md-10{-ms-flex-order:10;order:10}.order-md-11{-ms-flex-order:11;order:11}.order-md-12{-ms-flex-order:12;order:12}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.333333%}.offset-md-2{margin-left:16.666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.333333%}.offset-md-5{margin-left:41.666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.333333%}.offset-md-8{margin-left:66.666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.333333%}.offset-md-11{margin-left:91.666667%}}@media (min-width:992px){.col-lg{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;min-width:0;max-width:100%}.row-cols-lg-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-lg-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-lg-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-lg-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-lg-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-lg-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-lg-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-lg-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-lg-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-lg-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-lg-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-lg-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-lg-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-lg-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-lg-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-lg-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-lg-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-lg-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-lg-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-lg-first{-ms-flex-order:-1;order:-1}.order-lg-last{-ms-flex-order:13;order:13}.order-lg-0{-ms-flex-order:0;order:0}.order-lg-1{-ms-flex-order:1;order:1}.order-lg-2{-ms-flex-order:2;order:2}.order-lg-3{-ms-flex-order:3;order:3}.order-lg-4{-ms-flex-order:4;order:4}.order-lg-5{-ms-flex-order:5;order:5}.order-lg-6{-ms-flex-order:6;order:6}.order-lg-7{-ms-flex-order:7;order:7}.order-lg-8{-ms-flex-order:8;order:8}.order-lg-9{-ms-flex-order:9;order:9}.order-lg-10{-ms-flex-order:10;order:10}.order-lg-11{-ms-flex-order:11;order:11}.order-lg-12{-ms-flex-order:12;order:12}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.333333%}.offset-lg-2{margin-left:16.666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.333333%}.offset-lg-5{margin-left:41.666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.333333%}.offset-lg-8{margin-left:66.666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.333333%}.offset-lg-11{margin-left:91.666667%}}@media (min-width:1200px){.col-xl{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;min-width:0;max-width:100%}.row-cols-xl-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-xl-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-xl-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-xl-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-xl-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-xl-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-xl-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-xl-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-xl-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-xl-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-xl-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-xl-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-xl-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-xl-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-xl-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-xl-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-xl-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-xl-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-xl-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-xl-first{-ms-flex-order:-1;order:-1}.order-xl-last{-ms-flex-order:13;order:13}.order-xl-0{-ms-flex-order:0;order:0}.order-xl-1{-ms-flex-order:1;order:1}.order-xl-2{-ms-flex-order:2;order:2}.order-xl-3{-ms-flex-order:3;order:3}.order-xl-4{-ms-flex-order:4;order:4}.order-xl-5{-ms-flex-order:5;order:5}.order-xl-6{-ms-flex-order:6;order:6}.order-xl-7{-ms-flex-order:7;order:7}.order-xl-8{-ms-flex-order:8;order:8}.order-xl-9{-ms-flex-order:9;order:9}.order-xl-10{-ms-flex-order:10;order:10}.order-xl-11{-ms-flex-order:11;order:11}.order-xl-12{-ms-flex-order:12;order:12}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.333333%}.offset-xl-2{margin-left:16.666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.333333%}.offset-xl-5{margin-left:41.666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.333333%}.offset-xl-8{margin-left:66.666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.333333%}.offset-xl-11{margin-left:91.666667%}}.table{width:100%;margin-bottom:1rem;color:#212529}.table td,.table th{padding:.75rem;vertical-align:top;border-top:1px solid #dee2e6}.table thead th{vertical-align:bottom;border-bottom:2px solid #dee2e6}.table tbody+tbody{border-top:2px solid #dee2e6}.table-sm td,.table-sm th{padding:.3rem}.table-bordered{border:1px solid #dee2e6}.table-bordered td,.table-bordered th{border:1px solid #dee2e6}.table-bordered thead td,.table-bordered thead th{border-bottom-width:2px}.table-borderless tbody+tbody,.table-borderless td,.table-borderless th,.table-borderless thead th{border:0}.table-striped tbody tr:nth-of-type(odd){background-color:rgba(0,0,0,.05)}.table-hover tbody tr:hover{color:#212529;background-color:rgba(0,0,0,.075)}.table-primary,.table-primary>td,.table-primary>th{background-color:#b8daff}.table-primary tbody+tbody,.table-primary td,.table-primary th,.table-primary thead th{border-color:#7abaff}.table-hover .table-primary:hover{background-color:#9fcdff}.table-hover .table-primary:hover>td,.table-hover .table-primary:hover>th{background-color:#9fcdff}.table-secondary,.table-secondary>td,.table-secondary>th{background-color:#d6d8db}.table-secondary tbody+tbody,.table-secondary td,.table-secondary th,.table-secondary thead th{border-color:#b3b7bb}.table-hover .table-secondary:hover{background-color:#c8cbcf}.table-hover .table-secondary:hover>td,.table-hover .table-secondary:hover>th{background-color:#c8cbcf}.table-success,.table-success>td,.table-success>th{background-color:#c3e6cb}.table-success tbody+tbody,.table-success td,.table-success th,.table-success thead th{border-color:#8fd19e}.table-hover .table-success:hover{background-color:#b1dfbb}.table-hover .table-success:hover>td,.table-hover .table-success:hover>th{background-color:#b1dfbb}.table-info,.table-info>td,.table-info>th{background-color:#bee5eb}.table-info tbody+tbody,.table-info td,.table-info th,.table-info thead th{border-color:#86cfda}.table-hover .table-info:hover{background-color:#abdde5}.table-hover .table-info:hover>td,.table-hover .table-info:hover>th{background-color:#abdde5}.table-warning,.table-warning>td,.table-warning>th{background-color:#ffeeba}.table-warning tbody+tbody,.table-warning td,.table-warning th,.table-warning thead th{border-color:#ffdf7e}.table-hover .table-warning:hover{background-color:#ffe8a1}.table-hover .table-warning:hover>td,.table-hover .table-warning:hover>th{background-color:#ffe8a1}.table-danger,.table-danger>td,.table-danger>th{background-color:#f5c6cb}.table-danger tbody+tbody,.table-danger td,.table-danger th,.table-danger thead th{border-color:#ed969e}.table-hover .table-danger:hover{background-color:#f1b0b7}.table-hover .table-danger:hover>td,.table-hover .table-danger:hover>th{background-color:#f1b0b7}.table-light,.table-light>td,.table-light>th{background-color:#fdfdfe}.table-light tbody+tbody,.table-light td,.table-light th,.table-light thead th{border-color:#fbfcfc}.table-hover .table-light:hover{background-color:#ececf6}.table-hover .table-light:hover>td,.table-hover .table-light:hover>th{background-color:#ececf6}.table-dark,.table-dark>td,.table-dark>th{background-color:#c6c8ca}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#95999c}.table-hover .table-dark:hover{background-color:#b9bbbe}.table-hover .table-dark:hover>td,.table-hover .table-dark:hover>th{background-color:#b9bbbe}.table-active,.table-active>td,.table-active>th{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover>td,.table-hover .table-active:hover>th{background-color:rgba(0,0,0,.075)}.table .thead-dark th{color:#fff;background-color:#343a40;border-color:#454d55}.table .thead-light th{color:#495057;background-color:#e9ecef;border-color:#dee2e6}.table-dark{color:#fff;background-color:#343a40}.table-dark td,.table-dark th,.table-dark thead th{border-color:#454d55}.table-dark.table-bordered{border:0}.table-dark.table-striped tbody tr:nth-of-type(odd){background-color:rgba(255,255,255,.05)}.table-dark.table-hover tbody tr:hover{color:#fff;background-color:rgba(255,255,255,.075)}@media (max-width:575.98px){.table-responsive-sm{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-sm>.table-bordered{border:0}}@media (max-width:767.98px){.table-responsive-md{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-md>.table-bordered{border:0}}@media (max-width:991.98px){.table-responsive-lg{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-lg>.table-bordered{border:0}}@media (max-width:1199.98px){.table-responsive-xl{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-xl>.table-bordered{border:0}}.table-responsive{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive>.table-bordered{border:0}.form-control{display:block;width:100%;height:calc(1.5em + .75rem + 2px);padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control::-ms-expand{background-color:transparent;border:0}.form-control:-moz-focusring{color:transparent;text-shadow:0 0 0 #495057}.form-control:focus{color:#495057;background-color:#fff;border-color:#80bdff;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.form-control::-webkit-input-placeholder{color:#6c757d;opacity:1}.form-control::-moz-placeholder{color:#6c757d;opacity:1}.form-control:-ms-input-placeholder{color:#6c757d;opacity:1}.form-control::-ms-input-placeholder{color:#6c757d;opacity:1}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}input[type=date].form-control,input[type=datetime-local].form-control,input[type=month].form-control,input[type=time].form-control{-webkit-appearance:none;-moz-appearance:none;appearance:none}select.form-control:focus::-ms-value{color:#495057;background-color:#fff}.form-control-file,.form-control-range{display:block;width:100%}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem;line-height:1.5}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem;line-height:1.5}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;font-size:1rem;line-height:1.5;color:#212529;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{height:calc(1.5em + .5rem + 2px);padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.form-control-lg{height:calc(1.5em + 1rem + 2px);padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}select.form-control[multiple],select.form-control[size]{height:auto}textarea.form-control{height:auto}.form-group{margin-bottom:1rem}.form-text{display:block;margin-top:.25rem}.form-row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-5px;margin-left:-5px}.form-row>.col,.form-row>[class*=col-]{padding-right:5px;padding-left:5px}.form-check{position:relative;display:block;padding-left:1.25rem}.form-check-input{position:absolute;margin-top:.3rem;margin-left:-1.25rem}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{color:#6c757d}.form-check-label{margin-bottom:0}.form-check-inline{display:-ms-inline-flexbox;display:inline-flex;-ms-flex-align:center;align-items:center;padding-left:0;margin-right:.75rem}.form-check-inline .form-check-input{position:static;margin-top:0;margin-right:.3125rem;margin-left:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#28a745}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(40,167,69,.9);border-radius:.25rem}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:#28a745;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.custom-select.is-valid,.was-validated .custom-select:valid{border-color:#28a745;padding-right:calc(.75em + 2.3125rem);background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px,url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e") #fff no-repeat center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem)}.custom-select.is-valid:focus,.was-validated .custom-select:valid:focus{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#28a745}.form-check-input.is-valid~.valid-feedback,.form-check-input.is-valid~.valid-tooltip,.was-validated .form-check-input:valid~.valid-feedback,.was-validated .form-check-input:valid~.valid-tooltip{display:block}.custom-control-input.is-valid~.custom-control-label,.was-validated .custom-control-input:valid~.custom-control-label{color:#28a745}.custom-control-input.is-valid~.custom-control-label::before,.was-validated .custom-control-input:valid~.custom-control-label::before{border-color:#28a745}.custom-control-input.is-valid:checked~.custom-control-label::before,.was-validated .custom-control-input:valid:checked~.custom-control-label::before{border-color:#34ce57;background-color:#34ce57}.custom-control-input.is-valid:focus~.custom-control-label::before,.was-validated .custom-control-input:valid:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.custom-control-input.is-valid:focus:not(:checked)~.custom-control-label::before,.was-validated .custom-control-input:valid:focus:not(:checked)~.custom-control-label::before{border-color:#28a745}.custom-file-input.is-valid~.custom-file-label,.was-validated .custom-file-input:valid~.custom-file-label{border-color:#28a745}.custom-file-input.is-valid:focus~.custom-file-label,.was-validated .custom-file-input:valid:focus~.custom-file-label{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(220,53,69,.9);border-radius:.25rem}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:#dc3545;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.custom-select.is-invalid,.was-validated .custom-select:invalid{border-color:#dc3545;padding-right:calc(.75em + 2.3125rem);background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px,url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e") #fff no-repeat center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem)}.custom-select.is-invalid:focus,.was-validated .custom-select:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#dc3545}.form-check-input.is-invalid~.invalid-feedback,.form-check-input.is-invalid~.invalid-tooltip,.was-validated .form-check-input:invalid~.invalid-feedback,.was-validated .form-check-input:invalid~.invalid-tooltip{display:block}.custom-control-input.is-invalid~.custom-control-label,.was-validated .custom-control-input:invalid~.custom-control-label{color:#dc3545}.custom-control-input.is-invalid~.custom-control-label::before,.was-validated .custom-control-input:invalid~.custom-control-label::before{border-color:#dc3545}.custom-control-input.is-invalid:checked~.custom-control-label::before,.was-validated .custom-control-input:invalid:checked~.custom-control-label::before{border-color:#e4606d;background-color:#e4606d}.custom-control-input.is-invalid:focus~.custom-control-label::before,.was-validated .custom-control-input:invalid:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.custom-control-input.is-invalid:focus:not(:checked)~.custom-control-label::before,.was-validated .custom-control-input:invalid:focus:not(:checked)~.custom-control-label::before{border-color:#dc3545}.custom-file-input.is-invalid~.custom-file-label,.was-validated .custom-file-input:invalid~.custom-file-label{border-color:#dc3545}.custom-file-input.is-invalid:focus~.custom-file-label,.was-validated .custom-file-input:invalid:focus~.custom-file-label{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.form-inline{display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-align:center;align-items:center}.form-inline .form-check{width:100%}@media (min-width:576px){.form-inline label{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;margin-bottom:0}.form-inline .form-group{display:-ms-flexbox;display:flex;-ms-flex:0 0 auto;flex:0 0 auto;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-align:center;align-items:center;margin-bottom:0}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-plaintext{display:inline-block}.form-inline .custom-select,.form-inline .input-group{width:auto}.form-inline .form-check{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;width:auto;padding-left:0}.form-inline .form-check-input{position:relative;-ms-flex-negative:0;flex-shrink:0;margin-top:0;margin-right:.25rem;margin-left:0}.form-inline .custom-control{-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center}.form-inline .custom-control-label{margin-bottom:0}}.btn{display:inline-block;font-weight:400;color:#212529;text-align:center;vertical-align:middle;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:transparent;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;line-height:1.5;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:#212529;text-decoration:none}.btn.focus,.btn:focus{outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.btn.disabled,.btn:disabled{opacity:.65}.btn:not(:disabled):not(.disabled){cursor:pointer}a.btn.disabled,fieldset:disabled a.btn{pointer-events:none}.btn-primary{color:#fff;background-color:#007bff;border-color:#007bff}.btn-primary:hover{color:#fff;background-color:#0069d9;border-color:#0062cc}.btn-primary.focus,.btn-primary:focus{color:#fff;background-color:#0069d9;border-color:#0062cc;box-shadow:0 0 0 .2rem rgba(38,143,255,.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#007bff;border-color:#007bff}.btn-primary:not(:disabled):not(.disabled).active,.btn-primary:not(:disabled):not(.disabled):active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#0062cc;border-color:#005cbf}.btn-primary:not(:disabled):not(.disabled).active:focus,.btn-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(38,143,255,.5)}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:hover{color:#fff;background-color:#5a6268;border-color:#545b62}.btn-secondary.focus,.btn-secondary:focus{color:#fff;background-color:#5a6268;border-color:#545b62;box-shadow:0 0 0 .2rem rgba(130,138,145,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:not(:disabled):not(.disabled).active,.btn-secondary:not(:disabled):not(.disabled):active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#545b62;border-color:#4e555b}.btn-secondary:not(:disabled):not(.disabled).active:focus,.btn-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(130,138,145,.5)}.btn-success{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:hover{color:#fff;background-color:#218838;border-color:#1e7e34}.btn-success.focus,.btn-success:focus{color:#fff;background-color:#218838;border-color:#1e7e34;box-shadow:0 0 0 .2rem rgba(72,180,97,.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:not(:disabled):not(.disabled).active,.btn-success:not(:disabled):not(.disabled):active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#1e7e34;border-color:#1c7430}.btn-success:not(:disabled):not(.disabled).active:focus,.btn-success:not(:disabled):not(.disabled):active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(72,180,97,.5)}.btn-info{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:hover{color:#fff;background-color:#138496;border-color:#117a8b}.btn-info.focus,.btn-info:focus{color:#fff;background-color:#138496;border-color:#117a8b;box-shadow:0 0 0 .2rem rgba(58,176,195,.5)}.btn-info.disabled,.btn-info:disabled{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:not(:disabled):not(.disabled).active,.btn-info:not(:disabled):not(.disabled):active,.show>.btn-info.dropdown-toggle{color:#fff;background-color:#117a8b;border-color:#10707f}.btn-info:not(:disabled):not(.disabled).active:focus,.btn-info:not(:disabled):not(.disabled):active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(58,176,195,.5)}.btn-warning{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{color:#212529;background-color:#e0a800;border-color:#d39e00}.btn-warning.focus,.btn-warning:focus{color:#212529;background-color:#e0a800;border-color:#d39e00;box-shadow:0 0 0 .2rem rgba(222,170,12,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:not(:disabled):not(.disabled).active,.btn-warning:not(:disabled):not(.disabled):active,.show>.btn-warning.dropdown-toggle{color:#212529;background-color:#d39e00;border-color:#c69500}.btn-warning:not(:disabled):not(.disabled).active:focus,.btn-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(222,170,12,.5)}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:hover{color:#fff;background-color:#c82333;border-color:#bd2130}.btn-danger.focus,.btn-danger:focus{color:#fff;background-color:#c82333;border-color:#bd2130;box-shadow:0 0 0 .2rem rgba(225,83,97,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:not(:disabled):not(.disabled).active,.btn-danger:not(:disabled):not(.disabled):active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#bd2130;border-color:#b21f2d}.btn-danger:not(:disabled):not(.disabled).active:focus,.btn-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(225,83,97,.5)}.btn-light{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{color:#212529;background-color:#e2e6ea;border-color:#dae0e5}.btn-light.focus,.btn-light:focus{color:#212529;background-color:#e2e6ea;border-color:#dae0e5;box-shadow:0 0 0 .2rem rgba(216,217,219,.5)}.btn-light.disabled,.btn-light:disabled{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:not(:disabled):not(.disabled).active,.btn-light:not(:disabled):not(.disabled):active,.show>.btn-light.dropdown-toggle{color:#212529;background-color:#dae0e5;border-color:#d3d9df}.btn-light:not(:disabled):not(.disabled).active:focus,.btn-light:not(:disabled):not(.disabled):active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(216,217,219,.5)}.btn-dark{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:hover{color:#fff;background-color:#23272b;border-color:#1d2124}.btn-dark.focus,.btn-dark:focus{color:#fff;background-color:#23272b;border-color:#1d2124;box-shadow:0 0 0 .2rem rgba(82,88,93,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:not(:disabled):not(.disabled).active,.btn-dark:not(:disabled):not(.disabled):active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#1d2124;border-color:#171a1d}.btn-dark:not(:disabled):not(.disabled).active:focus,.btn-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(82,88,93,.5)}.btn-outline-primary{color:#007bff;border-color:#007bff}.btn-outline-primary:hover{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary.focus,.btn-outline-primary:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#007bff;background-color:transparent}.btn-outline-primary:not(:disabled):not(.disabled).active,.btn-outline-primary:not(:disabled):not(.disabled):active,.show>.btn-outline-primary.dropdown-toggle{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary:not(:disabled):not(.disabled).active:focus,.btn-outline-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-outline-secondary{color:#6c757d;border-color:#6c757d}.btn-outline-secondary:hover{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary.focus,.btn-outline-secondary:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#6c757d;background-color:transparent}.btn-outline-secondary:not(:disabled):not(.disabled).active,.btn-outline-secondary:not(:disabled):not(.disabled):active,.show>.btn-outline-secondary.dropdown-toggle{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary:not(:disabled):not(.disabled).active:focus,.btn-outline-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-outline-success{color:#28a745;border-color:#28a745}.btn-outline-success:hover{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success.focus,.btn-outline-success:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#28a745;background-color:transparent}.btn-outline-success:not(:disabled):not(.disabled).active,.btn-outline-success:not(:disabled):not(.disabled):active,.show>.btn-outline-success.dropdown-toggle{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success:not(:disabled):not(.disabled).active:focus,.btn-outline-success:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-outline-info{color:#17a2b8;border-color:#17a2b8}.btn-outline-info:hover{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info.focus,.btn-outline-info:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#17a2b8;background-color:transparent}.btn-outline-info:not(:disabled):not(.disabled).active,.btn-outline-info:not(:disabled):not(.disabled):active,.show>.btn-outline-info.dropdown-toggle{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info:not(:disabled):not(.disabled).active:focus,.btn-outline-info:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-outline-warning{color:#ffc107;border-color:#ffc107}.btn-outline-warning:hover{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning.focus,.btn-outline-warning:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-warning:not(:disabled):not(.disabled).active,.btn-outline-warning:not(:disabled):not(.disabled):active,.show>.btn-outline-warning.dropdown-toggle{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning:not(:disabled):not(.disabled).active:focus,.btn-outline-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-outline-danger{color:#dc3545;border-color:#dc3545}.btn-outline-danger:hover{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger.focus,.btn-outline-danger:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#dc3545;background-color:transparent}.btn-outline-danger:not(:disabled):not(.disabled).active,.btn-outline-danger:not(:disabled):not(.disabled):active,.show>.btn-outline-danger.dropdown-toggle{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger:not(:disabled):not(.disabled).active:focus,.btn-outline-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-outline-light{color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:hover{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light.focus,.btn-outline-light:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-light:not(:disabled):not(.disabled).active,.btn-outline-light:not(:disabled):not(.disabled):active,.show>.btn-outline-light.dropdown-toggle{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:not(:disabled):not(.disabled).active:focus,.btn-outline-light:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-dark{color:#343a40;border-color:#343a40}.btn-outline-dark:hover{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark.focus,.btn-outline-dark:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#343a40;background-color:transparent}.btn-outline-dark:not(:disabled):not(.disabled).active,.btn-outline-dark:not(:disabled):not(.disabled):active,.show>.btn-outline-dark.dropdown-toggle{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark:not(:disabled):not(.disabled).active:focus,.btn-outline-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-link{font-weight:400;color:#007bff;text-decoration:none}.btn-link:hover{color:#0056b3;text-decoration:underline}.btn-link.focus,.btn-link:focus{text-decoration:underline}.btn-link.disabled,.btn-link:disabled{color:#6c757d;pointer-events:none}.btn-group-lg>.btn,.btn-lg{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:.5rem}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{position:relative;height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.dropdown,.dropleft,.dropright,.dropup{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:10rem;padding:.5rem 0;margin:.125rem 0 0;font-size:1rem;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropdown-menu-left{right:auto;left:0}.dropdown-menu-right{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-left{right:auto;left:0}.dropdown-menu-sm-right{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-left{right:auto;left:0}.dropdown-menu-md-right{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-left{right:auto;left:0}.dropdown-menu-lg-right{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-left{right:auto;left:0}.dropdown-menu-xl-right{right:0;left:auto}}.dropup .dropdown-menu{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-menu{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropright .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropright .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-toggle::after{vertical-align:0}.dropleft .dropdown-menu{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropleft .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropleft .dropdown-toggle::after{display:none}.dropleft .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropleft .dropdown-toggle:empty::after{margin-left:0}.dropleft .dropdown-toggle::before{vertical-align:0}.dropdown-menu[x-placement^=bottom],.dropdown-menu[x-placement^=left],.dropdown-menu[x-placement^=right],.dropdown-menu[x-placement^=top]{right:auto;bottom:auto}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid #e9ecef}.dropdown-item{display:block;width:100%;padding:.25rem 1.5rem;clear:both;font-weight:400;color:#212529;text-align:inherit;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#16181b;text-decoration:none;background-color:#f8f9fa}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#007bff}.dropdown-item.disabled,.dropdown-item:disabled{color:#6c757d;pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1.5rem;margin-bottom:0;font-size:.875rem;color:#6c757d;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1.5rem;color:#212529}.btn-group,.btn-group-vertical{position:relative;display:-ms-inline-flexbox;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;-ms-flex:1 1 auto;flex:1 1 auto}.btn-group-vertical>.btn:hover,.btn-group>.btn:hover{z-index:1}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus{z-index:1}.btn-toolbar{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-pack:start;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn-group:not(:first-child),.btn-group>.btn:not(:first-child){margin-left:-1px}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropright .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropleft .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{-ms-flex-direction:column;flex-direction:column;-ms-flex-align:start;align-items:flex-start;-ms-flex-pack:center;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn:not(:first-child){border-top-left-radius:0;border-top-right-radius:0}.btn-group-toggle>.btn,.btn-group-toggle>.btn-group>.btn{margin-bottom:0}.btn-group-toggle>.btn input[type=checkbox],.btn-group-toggle>.btn input[type=radio],.btn-group-toggle>.btn-group>.btn input[type=checkbox],.btn-group-toggle>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:stretch;align-items:stretch;width:100%}.input-group>.custom-file,.input-group>.custom-select,.input-group>.form-control,.input-group>.form-control-plaintext{position:relative;-ms-flex:1 1 auto;flex:1 1 auto;width:1%;min-width:0;margin-bottom:0}.input-group>.custom-file+.custom-file,.input-group>.custom-file+.custom-select,.input-group>.custom-file+.form-control,.input-group>.custom-select+.custom-file,.input-group>.custom-select+.custom-select,.input-group>.custom-select+.form-control,.input-group>.form-control+.custom-file,.input-group>.form-control+.custom-select,.input-group>.form-control+.form-control,.input-group>.form-control-plaintext+.custom-file,.input-group>.form-control-plaintext+.custom-select,.input-group>.form-control-plaintext+.form-control{margin-left:-1px}.input-group>.custom-file .custom-file-input:focus~.custom-file-label,.input-group>.custom-select:focus,.input-group>.form-control:focus{z-index:3}.input-group>.custom-file .custom-file-input:focus{z-index:4}.input-group>.custom-select:not(:last-child),.input-group>.form-control:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-select:not(:first-child),.input-group>.form-control:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.custom-file{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center}.input-group>.custom-file:not(:last-child) .custom-file-label,.input-group>.custom-file:not(:last-child) .custom-file-label::after{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-file:not(:first-child) .custom-file-label{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-append,.input-group-prepend{display:-ms-flexbox;display:flex}.input-group-append .btn,.input-group-prepend .btn{position:relative;z-index:2}.input-group-append .btn:focus,.input-group-prepend .btn:focus{z-index:3}.input-group-append .btn+.btn,.input-group-append .btn+.input-group-text,.input-group-append .input-group-text+.btn,.input-group-append .input-group-text+.input-group-text,.input-group-prepend .btn+.btn,.input-group-prepend .btn+.input-group-text,.input-group-prepend .input-group-text+.btn,.input-group-prepend .input-group-text+.input-group-text{margin-left:-1px}.input-group-prepend{margin-right:-1px}.input-group-append{margin-left:-1px}.input-group-text{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;padding:.375rem .75rem;margin-bottom:0;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.25rem}.input-group-text input[type=checkbox],.input-group-text input[type=radio]{margin-top:0}.input-group-lg>.custom-select,.input-group-lg>.form-control:not(textarea){height:calc(1.5em + 1rem + 2px)}.input-group-lg>.custom-select,.input-group-lg>.form-control,.input-group-lg>.input-group-append>.btn,.input-group-lg>.input-group-append>.input-group-text,.input-group-lg>.input-group-prepend>.btn,.input-group-lg>.input-group-prepend>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.input-group-sm>.custom-select,.input-group-sm>.form-control:not(textarea){height:calc(1.5em + .5rem + 2px)}.input-group-sm>.custom-select,.input-group-sm>.form-control,.input-group-sm>.input-group-append>.btn,.input-group-sm>.input-group-append>.input-group-text,.input-group-sm>.input-group-prepend>.btn,.input-group-sm>.input-group-prepend>.input-group-text{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.input-group-lg>.custom-select,.input-group-sm>.custom-select{padding-right:1.75rem}.input-group>.input-group-append:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group>.input-group-append:last-child>.input-group-text:not(:last-child),.input-group>.input-group-append:not(:last-child)>.btn,.input-group>.input-group-append:not(:last-child)>.input-group-text,.input-group>.input-group-prepend>.btn,.input-group>.input-group-prepend>.input-group-text{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.input-group-append>.btn,.input-group>.input-group-append>.input-group-text,.input-group>.input-group-prepend:first-child>.btn:not(:first-child),.input-group>.input-group-prepend:first-child>.input-group-text:not(:first-child),.input-group>.input-group-prepend:not(:first-child)>.btn,.input-group>.input-group-prepend:not(:first-child)>.input-group-text{border-top-left-radius:0;border-bottom-left-radius:0}.custom-control{position:relative;display:block;min-height:1.5rem;padding-left:1.5rem}.custom-control-inline{display:-ms-inline-flexbox;display:inline-flex;margin-right:1rem}.custom-control-input{position:absolute;left:0;z-index:-1;width:1rem;height:1.25rem;opacity:0}.custom-control-input:checked~.custom-control-label::before{color:#fff;border-color:#007bff;background-color:#007bff}.custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-control-input:focus:not(:checked)~.custom-control-label::before{border-color:#80bdff}.custom-control-input:not(:disabled):active~.custom-control-label::before{color:#fff;background-color:#b3d7ff;border-color:#b3d7ff}.custom-control-input:disabled~.custom-control-label,.custom-control-input[disabled]~.custom-control-label{color:#6c757d}.custom-control-input:disabled~.custom-control-label::before,.custom-control-input[disabled]~.custom-control-label::before{background-color:#e9ecef}.custom-control-label{position:relative;margin-bottom:0;vertical-align:top}.custom-control-label::before{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;pointer-events:none;content:"";background-color:#fff;border:#adb5bd solid 1px}.custom-control-label::after{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;content:"";background:no-repeat 50%/50% 50%}.custom-checkbox .custom-control-label::before{border-radius:.25rem}.custom-checkbox .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26l2.974 2.99L8 2.193z'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::before{border-color:#007bff;background-color:#007bff}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' viewBox='0 0 4 4'%3e%3cpath stroke='%23fff' d='M0 2h4'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-checkbox .custom-control-input:disabled:indeterminate~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-radio .custom-control-label::before{border-radius:50%}.custom-radio .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.custom-radio .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-switch{padding-left:2.25rem}.custom-switch .custom-control-label::before{left:-2.25rem;width:1.75rem;pointer-events:all;border-radius:.5rem}.custom-switch .custom-control-label::after{top:calc(.25rem + 2px);left:calc(-2.25rem + 2px);width:calc(1rem - 4px);height:calc(1rem - 4px);background-color:#adb5bd;border-radius:.5rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,-webkit-transform .15s ease-in-out;transition:transform .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:transform .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,-webkit-transform .15s ease-in-out}@media (prefers-reduced-motion:reduce){.custom-switch .custom-control-label::after{transition:none}}.custom-switch .custom-control-input:checked~.custom-control-label::after{background-color:#fff;-webkit-transform:translateX(.75rem);transform:translateX(.75rem)}.custom-switch .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-select{display:inline-block;width:100%;height:calc(1.5em + .75rem + 2px);padding:.375rem 1.75rem .375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;vertical-align:middle;background:#fff url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px;border:1px solid #ced4da;border-radius:.25rem;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-select:focus{border-color:#80bdff;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-select:focus::-ms-value{color:#495057;background-color:#fff}.custom-select[multiple],.custom-select[size]:not([size="1"]){height:auto;padding-right:.75rem;background-image:none}.custom-select:disabled{color:#6c757d;background-color:#e9ecef}.custom-select::-ms-expand{display:none}.custom-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #495057}.custom-select-sm{height:calc(1.5em + .5rem + 2px);padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem}.custom-select-lg{height:calc(1.5em + 1rem + 2px);padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem}.custom-file{position:relative;display:inline-block;width:100%;height:calc(1.5em + .75rem + 2px);margin-bottom:0}.custom-file-input{position:relative;z-index:2;width:100%;height:calc(1.5em + .75rem + 2px);margin:0;opacity:0}.custom-file-input:focus~.custom-file-label{border-color:#80bdff;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-file-input:disabled~.custom-file-label,.custom-file-input[disabled]~.custom-file-label{background-color:#e9ecef}.custom-file-input:lang(en)~.custom-file-label::after{content:"Browse"}.custom-file-input~.custom-file-label[data-browse]::after{content:attr(data-browse)}.custom-file-label{position:absolute;top:0;right:0;left:0;z-index:1;height:calc(1.5em + .75rem + 2px);padding:.375rem .75rem;font-weight:400;line-height:1.5;color:#495057;background-color:#fff;border:1px solid #ced4da;border-radius:.25rem}.custom-file-label::after{position:absolute;top:0;right:0;bottom:0;z-index:3;display:block;height:calc(1.5em + .75rem);padding:.375rem .75rem;line-height:1.5;color:#495057;content:"Browse";background-color:#e9ecef;border-left:inherit;border-radius:0 .25rem .25rem 0}.custom-range{width:100%;height:1.4rem;padding:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-range:focus{outline:0}.custom-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range::-moz-focus-outer{border:0}.custom-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#007bff;border:0;border-radius:1rem;-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.custom-range::-webkit-slider-thumb:active{background-color:#b3d7ff}.custom-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#007bff;border:0;border-radius:1rem;-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-moz-range-thumb{-moz-transition:none;transition:none}}.custom-range::-moz-range-thumb:active{background-color:#b3d7ff}.custom-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-ms-thumb{width:1rem;height:1rem;margin-top:0;margin-right:.2rem;margin-left:.2rem;background-color:#007bff;border:0;border-radius:1rem;-ms-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-ms-thumb{-ms-transition:none;transition:none}}.custom-range::-ms-thumb:active{background-color:#b3d7ff}.custom-range::-ms-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:transparent;border-color:transparent;border-width:.5rem}.custom-range::-ms-fill-lower{background-color:#dee2e6;border-radius:1rem}.custom-range::-ms-fill-upper{margin-right:15px;background-color:#dee2e6;border-radius:1rem}.custom-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.custom-range:disabled::-webkit-slider-runnable-track{cursor:default}.custom-range:disabled::-moz-range-thumb{background-color:#adb5bd}.custom-range:disabled::-moz-range-track{cursor:default}.custom-range:disabled::-ms-thumb{background-color:#adb5bd}.custom-control-label::before,.custom-file-label,.custom-select{transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.custom-control-label::before,.custom-file-label,.custom-select{transition:none}}.nav{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem}.nav-link:focus,.nav-link:hover{text-decoration:none}.nav-link.disabled{color:#6c757d;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-item{margin-bottom:-1px}.nav-tabs .nav-link{border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#e9ecef #e9ecef #dee2e6}.nav-tabs .nav-link.disabled{color:#6c757d;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#007bff}.nav-fill .nav-item{-ms-flex:1 1 auto;flex:1 1 auto;text-align:center}.nav-justified .nav-item{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;text-align:center}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between;padding:.5rem 1rem}.navbar .container,.navbar .container-fluid,.navbar .container-lg,.navbar .container-md,.navbar .container-sm,.navbar .container-xl{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between}.navbar-brand{display:inline-block;padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;line-height:inherit;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-nav{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static;float:none}.navbar-text{display:inline-block;padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{-ms-flex-preferred-size:100%;flex-basis:100%;-ms-flex-positive:1;flex-grow:1;-ms-flex-align:center;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem}.navbar-toggler:focus,.navbar-toggler:hover{text-decoration:none}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;content:"";background:no-repeat center center;background-size:100% 100%}@media (max-width:575.98px){.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid,.navbar-expand-sm>.container-lg,.navbar-expand-sm>.container-md,.navbar-expand-sm>.container-sm,.navbar-expand-sm>.container-xl{padding-right:0;padding-left:0}}@media (min-width:576px){.navbar-expand-sm{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-sm .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid,.navbar-expand-sm>.container-lg,.navbar-expand-sm>.container-md,.navbar-expand-sm>.container-sm,.navbar-expand-sm>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-sm .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}}@media (max-width:767.98px){.navbar-expand-md>.container,.navbar-expand-md>.container-fluid,.navbar-expand-md>.container-lg,.navbar-expand-md>.container-md,.navbar-expand-md>.container-sm,.navbar-expand-md>.container-xl{padding-right:0;padding-left:0}}@media (min-width:768px){.navbar-expand-md{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-md .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md>.container,.navbar-expand-md>.container-fluid,.navbar-expand-md>.container-lg,.navbar-expand-md>.container-md,.navbar-expand-md>.container-sm,.navbar-expand-md>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-md .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}}@media (max-width:991.98px){.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid,.navbar-expand-lg>.container-lg,.navbar-expand-lg>.container-md,.navbar-expand-lg>.container-sm,.navbar-expand-lg>.container-xl{padding-right:0;padding-left:0}}@media (min-width:992px){.navbar-expand-lg{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-lg .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid,.navbar-expand-lg>.container-lg,.navbar-expand-lg>.container-md,.navbar-expand-lg>.container-sm,.navbar-expand-lg>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-lg .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}}@media (max-width:1199.98px){.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid,.navbar-expand-xl>.container-lg,.navbar-expand-xl>.container-md,.navbar-expand-xl>.container-sm,.navbar-expand-xl>.container-xl{padding-right:0;padding-left:0}}@media (min-width:1200px){.navbar-expand-xl{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-xl .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid,.navbar-expand-xl>.container-lg,.navbar-expand-xl>.container-md,.navbar-expand-xl>.container-sm,.navbar-expand-xl>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-xl .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}}.navbar-expand{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand>.container,.navbar-expand>.container-fluid,.navbar-expand>.container-lg,.navbar-expand>.container-md,.navbar-expand>.container-sm,.navbar-expand>.container-xl{padding-right:0;padding-left:0}.navbar-expand .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand>.container,.navbar-expand>.container-fluid,.navbar-expand>.container-lg,.navbar-expand>.container-md,.navbar-expand>.container-sm,.navbar-expand>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-light .navbar-brand{color:rgba(0,0,0,.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.5)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .active>.nav-link,.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .nav-link.show,.navbar-light .navbar-nav .show>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{color:rgba(0,0,0,.5);border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280, 0, 0, 0.5%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-light .navbar-text{color:rgba(0,0,0,.5)}.navbar-light .navbar-text a{color:rgba(0,0,0,.9)}.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:rgba(0,0,0,.9)}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,.5)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-dark .navbar-nav .active>.nav-link,.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .nav-link.show,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,.5);border-color:rgba(255,255,255,.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.5%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-dark .navbar-text{color:rgba(255,255,255,.5)}.navbar-dark .navbar-text a{color:#fff}.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,.125);border-radius:.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-body{-ms-flex:1 1 auto;flex:1 1 auto;min-height:1px;padding:1.25rem}.card-title{margin-bottom:.75rem}.card-subtitle{margin-top:-.375rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1.25rem}.card-header{padding:.75rem 1.25rem;margin-bottom:0;background-color:rgba(0,0,0,.03);border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-header+.list-group .list-group-item:first-child{border-top:0}.card-footer{padding:.75rem 1.25rem;background-color:rgba(0,0,0,.03);border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.625rem;margin-bottom:-.75rem;margin-left:-.625rem;border-bottom:0}.card-header-pills{margin-right:-.625rem;margin-left:-.625rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1.25rem}.card-img,.card-img-bottom,.card-img-top{-ms-flex-negative:0;flex-shrink:0;width:100%}.card-img,.card-img-top{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-deck .card{margin-bottom:15px}@media (min-width:576px){.card-deck{display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap;margin-right:-15px;margin-left:-15px}.card-deck .card{-ms-flex:1 0 0%;flex:1 0 0%;margin-right:15px;margin-bottom:0;margin-left:15px}}.card-group>.card{margin-bottom:15px}@media (min-width:576px){.card-group{display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap}.card-group>.card{-ms-flex:1 0 0%;flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.card-columns .card{margin-bottom:.75rem}@media (min-width:576px){.card-columns{-webkit-column-count:3;-moz-column-count:3;column-count:3;-webkit-column-gap:1.25rem;-moz-column-gap:1.25rem;column-gap:1.25rem;orphans:1;widows:1}.card-columns .card{display:inline-block;width:100%}}.accordion>.card{overflow:hidden}.accordion>.card:not(:last-of-type){border-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.accordion>.card:not(:first-of-type){border-top-left-radius:0;border-top-right-radius:0}.accordion>.card>.card-header{border-radius:0;margin-bottom:-1px}.breadcrumb{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding:.75rem 1rem;margin-bottom:1rem;list-style:none;background-color:#e9ecef;border-radius:.25rem}.breadcrumb-item{display:-ms-flexbox;display:flex}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{display:inline-block;padding-right:.5rem;color:#6c757d;content:"/"}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:underline}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:none}.breadcrumb-item.active{color:#6c757d}.pagination{display:-ms-flexbox;display:flex;padding-left:0;list-style:none;border-radius:.25rem}.page-link{position:relative;display:block;padding:.5rem .75rem;margin-left:-1px;line-height:1.25;color:#007bff;background-color:#fff;border:1px solid #dee2e6}.page-link:hover{z-index:2;color:#0056b3;text-decoration:none;background-color:#e9ecef;border-color:#dee2e6}.page-link:focus{z-index:3;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.page-item:first-child .page-link{margin-left:0;border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.page-item.active .page-link{z-index:3;color:#fff;background-color:#007bff;border-color:#007bff}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;cursor:auto;background-color:#fff;border-color:#dee2e6}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem;line-height:1.5}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem;line-height:1.5}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.badge{display:inline-block;padding:.25em .4em;font-size:75%;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.badge{transition:none}}a.badge:focus,a.badge:hover{text-decoration:none}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.badge-pill{padding-right:.6em;padding-left:.6em;border-radius:10rem}.badge-primary{color:#fff;background-color:#007bff}a.badge-primary:focus,a.badge-primary:hover{color:#fff;background-color:#0062cc}a.badge-primary.focus,a.badge-primary:focus{outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.badge-secondary{color:#fff;background-color:#6c757d}a.badge-secondary:focus,a.badge-secondary:hover{color:#fff;background-color:#545b62}a.badge-secondary.focus,a.badge-secondary:focus{outline:0;box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.badge-success{color:#fff;background-color:#28a745}a.badge-success:focus,a.badge-success:hover{color:#fff;background-color:#1e7e34}a.badge-success.focus,a.badge-success:focus{outline:0;box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.badge-info{color:#fff;background-color:#17a2b8}a.badge-info:focus,a.badge-info:hover{color:#fff;background-color:#117a8b}a.badge-info.focus,a.badge-info:focus{outline:0;box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.badge-warning{color:#212529;background-color:#ffc107}a.badge-warning:focus,a.badge-warning:hover{color:#212529;background-color:#d39e00}a.badge-warning.focus,a.badge-warning:focus{outline:0;box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.badge-danger{color:#fff;background-color:#dc3545}a.badge-danger:focus,a.badge-danger:hover{color:#fff;background-color:#bd2130}a.badge-danger.focus,a.badge-danger:focus{outline:0;box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.badge-light{color:#212529;background-color:#f8f9fa}a.badge-light:focus,a.badge-light:hover{color:#212529;background-color:#dae0e5}a.badge-light.focus,a.badge-light:focus{outline:0;box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.badge-dark{color:#fff;background-color:#343a40}a.badge-dark:focus,a.badge-dark:hover{color:#fff;background-color:#1d2124}a.badge-dark.focus,a.badge-dark:focus{outline:0;box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.jumbotron{padding:2rem 1rem;margin-bottom:2rem;background-color:#e9ecef;border-radius:.3rem}@media (min-width:576px){.jumbotron{padding:4rem 2rem}}.jumbotron-fluid{padding-right:0;padding-left:0;border-radius:0}.alert{position:relative;padding:.75rem 1.25rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:4rem}.alert-dismissible .close{position:absolute;top:0;right:0;padding:.75rem 1.25rem;color:inherit}.alert-primary{color:#004085;background-color:#cce5ff;border-color:#b8daff}.alert-primary hr{border-top-color:#9fcdff}.alert-primary .alert-link{color:#002752}.alert-secondary{color:#383d41;background-color:#e2e3e5;border-color:#d6d8db}.alert-secondary hr{border-top-color:#c8cbcf}.alert-secondary .alert-link{color:#202326}.alert-success{color:#155724;background-color:#d4edda;border-color:#c3e6cb}.alert-success hr{border-top-color:#b1dfbb}.alert-success .alert-link{color:#0b2e13}.alert-info{color:#0c5460;background-color:#d1ecf1;border-color:#bee5eb}.alert-info hr{border-top-color:#abdde5}.alert-info .alert-link{color:#062c33}.alert-warning{color:#856404;background-color:#fff3cd;border-color:#ffeeba}.alert-warning hr{border-top-color:#ffe8a1}.alert-warning .alert-link{color:#533f03}.alert-danger{color:#721c24;background-color:#f8d7da;border-color:#f5c6cb}.alert-danger hr{border-top-color:#f1b0b7}.alert-danger .alert-link{color:#491217}.alert-light{color:#818182;background-color:#fefefe;border-color:#fdfdfe}.alert-light hr{border-top-color:#ececf6}.alert-light .alert-link{color:#686868}.alert-dark{color:#1b1e21;background-color:#d6d8d9;border-color:#c6c8ca}.alert-dark hr{border-top-color:#b9bbbe}.alert-dark .alert-link{color:#040505}@-webkit-keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}.progress{display:-ms-flexbox;display:flex;height:1rem;overflow:hidden;line-height:0;font-size:.75rem;background-color:#e9ecef;border-radius:.25rem}.progress-bar{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:center;justify-content:center;overflow:hidden;color:#fff;text-align:center;white-space:nowrap;background-color:#007bff;transition:width .6s ease}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:progress-bar-stripes 1s linear infinite;animation:progress-bar-stripes 1s linear infinite}@media (prefers-reduced-motion:reduce){.progress-bar-animated{-webkit-animation:none;animation:none}}.media{display:-ms-flexbox;display:flex;-ms-flex-align:start;align-items:flex-start}.media-body{-ms-flex:1;flex:1}.list-group{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:.25rem}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:#495057;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#212529;background-color:#e9ecef}.list-group-item{position:relative;display:block;padding:.75rem 1.25rem;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:#6c757d;pointer-events:none;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#007bff;border-color:#007bff}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:-1px;border-top-width:1px}.list-group-horizontal{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}@media (min-width:576px){.list-group-horizontal-sm{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:768px){.list-group-horizontal-md{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:992px){.list-group-horizontal-lg{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1200px){.list-group-horizontal-xl{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 1px}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{color:#004085;background-color:#b8daff}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#004085;background-color:#9fcdff}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#004085;border-color:#004085}.list-group-item-secondary{color:#383d41;background-color:#d6d8db}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#383d41;background-color:#c8cbcf}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#383d41;border-color:#383d41}.list-group-item-success{color:#155724;background-color:#c3e6cb}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#155724;background-color:#b1dfbb}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#155724;border-color:#155724}.list-group-item-info{color:#0c5460;background-color:#bee5eb}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#0c5460;background-color:#abdde5}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#0c5460;border-color:#0c5460}.list-group-item-warning{color:#856404;background-color:#ffeeba}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#856404;background-color:#ffe8a1}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#856404;border-color:#856404}.list-group-item-danger{color:#721c24;background-color:#f5c6cb}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#721c24;background-color:#f1b0b7}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#721c24;border-color:#721c24}.list-group-item-light{color:#818182;background-color:#fdfdfe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#818182;background-color:#ececf6}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#818182;border-color:#818182}.list-group-item-dark{color:#1b1e21;background-color:#c6c8ca}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#1b1e21;background-color:#b9bbbe}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#1b1e21;border-color:#1b1e21}.close{float:right;font-size:1.5rem;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.5}.close:hover{color:#000;text-decoration:none}.close:not(:disabled):not(.disabled):focus,.close:not(:disabled):not(.disabled):hover{opacity:.75}button.close{padding:0;background-color:transparent;border:0}a.close.disabled{pointer-events:none}.toast{max-width:350px;overflow:hidden;font-size:.875rem;background-color:rgba(255,255,255,.85);background-clip:padding-box;border:1px solid rgba(0,0,0,.1);box-shadow:0 .25rem .75rem rgba(0,0,0,.1);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);opacity:0;border-radius:.25rem}.toast:not(:last-child){margin-bottom:.75rem}.toast.showing{opacity:1}.toast.show{display:block;opacity:1}.toast.hide{display:none}.toast-header{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;padding:.25rem .75rem;color:#6c757d;background-color:rgba(255,255,255,.85);background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,.05)}.toast-body{padding:.75rem}.modal-open{overflow:hidden}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal{position:fixed;top:0;left:0;z-index:1050;display:none;width:100%;height:100%;overflow:hidden;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:-webkit-transform .3s ease-out;transition:transform .3s ease-out;transition:transform .3s ease-out,-webkit-transform .3s ease-out;-webkit-transform:translate(0,-50px);transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{-webkit-transform:none;transform:none}.modal.modal-static .modal-dialog{-webkit-transform:scale(1.02);transform:scale(1.02)}.modal-dialog-scrollable{display:-ms-flexbox;display:flex;max-height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 1rem);overflow:hidden}.modal-dialog-scrollable .modal-footer,.modal-dialog-scrollable .modal-header{-ms-flex-negative:0;flex-shrink:0}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;min-height:calc(100% - 1rem)}.modal-dialog-centered::before{display:block;height:calc(100vh - 1rem);height:-webkit-min-content;height:-moz-min-content;height:min-content;content:""}.modal-dialog-centered.modal-dialog-scrollable{-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:center;justify-content:center;height:100%}.modal-dialog-centered.modal-dialog-scrollable .modal-content{max-height:none}.modal-dialog-centered.modal-dialog-scrollable::before{content:none}.modal-content{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:-ms-flexbox;display:flex;-ms-flex-align:start;align-items:flex-start;-ms-flex-pack:justify;justify-content:space-between;padding:1rem 1rem;border-bottom:1px solid #dee2e6;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.modal-header .close{padding:1rem 1rem;margin:-1rem -1rem -1rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;-ms-flex:1 1 auto;flex:1 1 auto;padding:1rem}.modal-footer{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:end;justify-content:flex-end;padding:.75rem;border-top:1px solid #dee2e6;border-bottom-right-radius:calc(.3rem - 1px);border-bottom-left-radius:calc(.3rem - 1px)}.modal-footer>*{margin:.25rem}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{max-height:calc(100% - 3.5rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-dialog-centered::before{height:calc(100vh - 3.5rem);height:-webkit-min-content;height:-moz-min-content;height:min-content}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width:1200px){.modal-xl{max-width:1140px}}.tooltip{position:absolute;z-index:1070;display:block;margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[x-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[x-placement^=top] .arrow,.bs-tooltip-top .arrow{bottom:0}.bs-tooltip-auto[x-placement^=top] .arrow::before,.bs-tooltip-top .arrow::before{top:0;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-auto[x-placement^=right],.bs-tooltip-right{padding:0 .4rem}.bs-tooltip-auto[x-placement^=right] .arrow,.bs-tooltip-right .arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=right] .arrow::before,.bs-tooltip-right .arrow::before{right:0;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-auto[x-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[x-placement^=bottom] .arrow,.bs-tooltip-bottom .arrow{top:0}.bs-tooltip-auto[x-placement^=bottom] .arrow::before,.bs-tooltip-bottom .arrow::before{bottom:0;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-auto[x-placement^=left],.bs-tooltip-left{padding:0 .4rem}.bs-tooltip-auto[x-placement^=left] .arrow,.bs-tooltip-left .arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=left] .arrow::before,.bs-tooltip-left .arrow::before{left:0;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.popover{position:absolute;top:0;left:0;z-index:1060;display:block;max-width:276px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.popover .arrow{position:absolute;display:block;width:1rem;height:.5rem;margin:0 .3rem}.popover .arrow::after,.popover .arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-auto[x-placement^=top],.bs-popover-top{margin-bottom:.5rem}.bs-popover-auto[x-placement^=top]>.arrow,.bs-popover-top>.arrow{bottom:calc(-.5rem - 1px)}.bs-popover-auto[x-placement^=top]>.arrow::before,.bs-popover-top>.arrow::before{bottom:0;border-width:.5rem .5rem 0;border-top-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=top]>.arrow::after,.bs-popover-top>.arrow::after{bottom:1px;border-width:.5rem .5rem 0;border-top-color:#fff}.bs-popover-auto[x-placement^=right],.bs-popover-right{margin-left:.5rem}.bs-popover-auto[x-placement^=right]>.arrow,.bs-popover-right>.arrow{left:calc(-.5rem - 1px);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=right]>.arrow::before,.bs-popover-right>.arrow::before{left:0;border-width:.5rem .5rem .5rem 0;border-right-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=right]>.arrow::after,.bs-popover-right>.arrow::after{left:1px;border-width:.5rem .5rem .5rem 0;border-right-color:#fff}.bs-popover-auto[x-placement^=bottom],.bs-popover-bottom{margin-top:.5rem}.bs-popover-auto[x-placement^=bottom]>.arrow,.bs-popover-bottom>.arrow{top:calc(-.5rem - 1px)}.bs-popover-auto[x-placement^=bottom]>.arrow::before,.bs-popover-bottom>.arrow::before{top:0;border-width:0 .5rem .5rem .5rem;border-bottom-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=bottom]>.arrow::after,.bs-popover-bottom>.arrow::after{top:1px;border-width:0 .5rem .5rem .5rem;border-bottom-color:#fff}.bs-popover-auto[x-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f7f7f7}.bs-popover-auto[x-placement^=left],.bs-popover-left{margin-right:.5rem}.bs-popover-auto[x-placement^=left]>.arrow,.bs-popover-left>.arrow{right:calc(-.5rem - 1px);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=left]>.arrow::before,.bs-popover-left>.arrow::before{right:0;border-width:.5rem 0 .5rem .5rem;border-left-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=left]>.arrow::after,.bs-popover-left>.arrow::after{right:1px;border-width:.5rem 0 .5rem .5rem;border-left-color:#fff}.popover-header{padding:.5rem .75rem;margin-bottom:0;font-size:1rem;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:.5rem .75rem;color:#212529}.carousel{position:relative}.carousel.pointer-event{-ms-touch-action:pan-y;touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:-webkit-transform .6s ease-in-out;transition:transform .6s ease-in-out;transition:transform .6s ease-in-out,-webkit-transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-right,.carousel-item-next:not(.carousel-item-left){-webkit-transform:translateX(100%);transform:translateX(100%)}.active.carousel-item-left,.carousel-item-prev:not(.carousel-item-right){-webkit-transform:translateX(-100%);transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;-webkit-transform:none;transform:none}.carousel-fade .carousel-item-next.carousel-item-left,.carousel-fade .carousel-item-prev.carousel-item-right,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;width:15%;color:#fff;text-align:center;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:20px;height:20px;background:no-repeat 50%/100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M5.25 0l-4 4 4 4 1.5-1.5L4.25 4l2.5-2.5L5.25 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M2.75 0l-1.5 1.5L3.75 4l-2.5 2.5L2.75 8l4-4-4-4z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:15;display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center;padding-left:0;margin-right:15%;margin-left:15%;list-style:none}.carousel-indicators li{box-sizing:content-box;-ms-flex:0 1 auto;flex:0 1 auto;width:30px;height:3px;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators li{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center}@-webkit-keyframes spinner-border{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes spinner-border{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;border:.25em solid currentColor;border-right-color:transparent;border-radius:50%;-webkit-animation:spinner-border .75s linear infinite;animation:spinner-border .75s linear infinite}.spinner-border-sm{width:1rem;height:1rem;border-width:.2em}@-webkit-keyframes spinner-grow{0%{-webkit-transform:scale(0);transform:scale(0)}50%{opacity:1;-webkit-transform:none;transform:none}}@keyframes spinner-grow{0%{-webkit-transform:scale(0);transform:scale(0)}50%{opacity:1;-webkit-transform:none;transform:none}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;background-color:currentColor;border-radius:50%;opacity:0;-webkit-animation:spinner-grow .75s linear infinite;animation:spinner-grow .75s linear infinite}.spinner-grow-sm{width:1rem;height:1rem}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.bg-primary{background-color:#007bff!important}a.bg-primary:focus,a.bg-primary:hover,button.bg-primary:focus,button.bg-primary:hover{background-color:#0062cc!important}.bg-secondary{background-color:#6c757d!important}a.bg-secondary:focus,a.bg-secondary:hover,button.bg-secondary:focus,button.bg-secondary:hover{background-color:#545b62!important}.bg-success{background-color:#28a745!important}a.bg-success:focus,a.bg-success:hover,button.bg-success:focus,button.bg-success:hover{background-color:#1e7e34!important}.bg-info{background-color:#17a2b8!important}a.bg-info:focus,a.bg-info:hover,button.bg-info:focus,button.bg-info:hover{background-color:#117a8b!important}.bg-warning{background-color:#ffc107!important}a.bg-warning:focus,a.bg-warning:hover,button.bg-warning:focus,button.bg-warning:hover{background-color:#d39e00!important}.bg-danger{background-color:#dc3545!important}a.bg-danger:focus,a.bg-danger:hover,button.bg-danger:focus,button.bg-danger:hover{background-color:#bd2130!important}.bg-light{background-color:#f8f9fa!important}a.bg-light:focus,a.bg-light:hover,button.bg-light:focus,button.bg-light:hover{background-color:#dae0e5!important}.bg-dark{background-color:#343a40!important}a.bg-dark:focus,a.bg-dark:hover,button.bg-dark:focus,button.bg-dark:hover{background-color:#1d2124!important}.bg-white{background-color:#fff!important}.bg-transparent{background-color:transparent!important}.border{border:1px solid #dee2e6!important}.border-top{border-top:1px solid #dee2e6!important}.border-right{border-right:1px solid #dee2e6!important}.border-bottom{border-bottom:1px solid #dee2e6!important}.border-left{border-left:1px solid #dee2e6!important}.border-0{border:0!important}.border-top-0{border-top:0!important}.border-right-0{border-right:0!important}.border-bottom-0{border-bottom:0!important}.border-left-0{border-left:0!important}.border-primary{border-color:#007bff!important}.border-secondary{border-color:#6c757d!important}.border-success{border-color:#28a745!important}.border-info{border-color:#17a2b8!important}.border-warning{border-color:#ffc107!important}.border-danger{border-color:#dc3545!important}.border-light{border-color:#f8f9fa!important}.border-dark{border-color:#343a40!important}.border-white{border-color:#fff!important}.rounded-sm{border-radius:.2rem!important}.rounded{border-radius:.25rem!important}.rounded-top{border-top-left-radius:.25rem!important;border-top-right-radius:.25rem!important}.rounded-right{border-top-right-radius:.25rem!important;border-bottom-right-radius:.25rem!important}.rounded-bottom{border-bottom-right-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-left{border-top-left-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-lg{border-radius:.3rem!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:50rem!important}.rounded-0{border-radius:0!important}.clearfix::after{display:block;clear:both;content:""}.d-none{display:none!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:-ms-flexbox!important;display:flex!important}.d-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}@media (min-width:576px){.d-sm-none{display:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:-ms-flexbox!important;display:flex!important}.d-sm-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:768px){.d-md-none{display:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:-ms-flexbox!important;display:flex!important}.d-md-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:992px){.d-lg-none{display:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:-ms-flexbox!important;display:flex!important}.d-lg-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:1200px){.d-xl-none{display:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:-ms-flexbox!important;display:flex!important}.d-xl-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media print{.d-print-none{display:none!important}.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:-ms-flexbox!important;display:flex!important}.d-print-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}.embed-responsive{position:relative;display:block;width:100%;padding:0;overflow:hidden}.embed-responsive::before{display:block;content:""}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-21by9::before{padding-top:42.857143%}.embed-responsive-16by9::before{padding-top:56.25%}.embed-responsive-4by3::before{padding-top:75%}.embed-responsive-1by1::before{padding-top:100%}.flex-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-center{-ms-flex-align:center!important;align-items:center!important}.align-items-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}@media (min-width:576px){.flex-sm-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-sm-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-sm-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-sm-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-sm-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-sm-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-sm-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-sm-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-sm-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-sm-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-sm-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-sm-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-sm-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-sm-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-sm-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-sm-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-sm-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-sm-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-sm-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-sm-center{-ms-flex-align:center!important;align-items:center!important}.align-items-sm-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-sm-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-sm-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-sm-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-sm-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-sm-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-sm-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-sm-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-sm-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-sm-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-sm-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-sm-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-sm-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-sm-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:768px){.flex-md-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-md-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-md-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-md-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-md-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-md-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-md-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-md-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-md-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-md-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-md-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-md-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-md-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-md-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-md-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-md-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-md-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-md-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-md-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-md-center{-ms-flex-align:center!important;align-items:center!important}.align-items-md-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-md-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-md-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-md-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-md-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-md-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-md-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-md-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-md-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-md-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-md-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-md-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-md-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-md-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:992px){.flex-lg-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-lg-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-lg-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-lg-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-lg-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-lg-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-lg-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-lg-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-lg-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-lg-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-lg-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-lg-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-lg-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-lg-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-lg-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-lg-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-lg-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-lg-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-lg-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-lg-center{-ms-flex-align:center!important;align-items:center!important}.align-items-lg-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-lg-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-lg-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-lg-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-lg-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-lg-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-lg-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-lg-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-lg-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-lg-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-lg-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-lg-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-lg-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-lg-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:1200px){.flex-xl-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-xl-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-xl-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-xl-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-xl-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-xl-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-xl-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-xl-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-xl-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-xl-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-xl-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-xl-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-xl-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-xl-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-xl-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-xl-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-xl-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-xl-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-xl-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-xl-center{-ms-flex-align:center!important;align-items:center!important}.align-items-xl-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-xl-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-xl-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-xl-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-xl-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-xl-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-xl-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-xl-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-xl-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-xl-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-xl-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-xl-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-xl-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-xl-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}.float-left{float:left!important}.float-right{float:right!important}.float-none{float:none!important}@media (min-width:576px){.float-sm-left{float:left!important}.float-sm-right{float:right!important}.float-sm-none{float:none!important}}@media (min-width:768px){.float-md-left{float:left!important}.float-md-right{float:right!important}.float-md-none{float:none!important}}@media (min-width:992px){.float-lg-left{float:left!important}.float-lg-right{float:right!important}.float-lg-none{float:none!important}}@media (min-width:1200px){.float-xl-left{float:left!important}.float-xl-right{float:right!important}.float-xl-none{float:none!important}}.user-select-all{-webkit-user-select:all!important;-moz-user-select:all!important;-ms-user-select:all!important;user-select:all!important}.user-select-auto{-webkit-user-select:auto!important;-moz-user-select:auto!important;-ms-user-select:auto!important;user-select:auto!important}.user-select-none{-webkit-user-select:none!important;-moz-user-select:none!important;-ms-user-select:none!important;user-select:none!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}@supports ((position:-webkit-sticky) or (position:sticky)){.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;overflow:visible;clip:auto;white-space:normal}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mw-100{max-width:100%!important}.mh-100{max-height:100%!important}.min-vw-100{min-width:100vw!important}.min-vh-100{min-height:100vh!important}.vw-100{width:100vw!important}.vh-100{height:100vh!important}.m-0{margin:0!important}.mt-0,.my-0{margin-top:0!important}.mr-0,.mx-0{margin-right:0!important}.mb-0,.my-0{margin-bottom:0!important}.ml-0,.mx-0{margin-left:0!important}.m-1{margin:.25rem!important}.mt-1,.my-1{margin-top:.25rem!important}.mr-1,.mx-1{margin-right:.25rem!important}.mb-1,.my-1{margin-bottom:.25rem!important}.ml-1,.mx-1{margin-left:.25rem!important}.m-2{margin:.5rem!important}.mt-2,.my-2{margin-top:.5rem!important}.mr-2,.mx-2{margin-right:.5rem!important}.mb-2,.my-2{margin-bottom:.5rem!important}.ml-2,.mx-2{margin-left:.5rem!important}.m-3{margin:1rem!important}.mt-3,.my-3{margin-top:1rem!important}.mr-3,.mx-3{margin-right:1rem!important}.mb-3,.my-3{margin-bottom:1rem!important}.ml-3,.mx-3{margin-left:1rem!important}.m-4{margin:1.5rem!important}.mt-4,.my-4{margin-top:1.5rem!important}.mr-4,.mx-4{margin-right:1.5rem!important}.mb-4,.my-4{margin-bottom:1.5rem!important}.ml-4,.mx-4{margin-left:1.5rem!important}.m-5{margin:3rem!important}.mt-5,.my-5{margin-top:3rem!important}.mr-5,.mx-5{margin-right:3rem!important}.mb-5,.my-5{margin-bottom:3rem!important}.ml-5,.mx-5{margin-left:3rem!important}.p-0{padding:0!important}.pt-0,.py-0{padding-top:0!important}.pr-0,.px-0{padding-right:0!important}.pb-0,.py-0{padding-bottom:0!important}.pl-0,.px-0{padding-left:0!important}.p-1{padding:.25rem!important}.pt-1,.py-1{padding-top:.25rem!important}.pr-1,.px-1{padding-right:.25rem!important}.pb-1,.py-1{padding-bottom:.25rem!important}.pl-1,.px-1{padding-left:.25rem!important}.p-2{padding:.5rem!important}.pt-2,.py-2{padding-top:.5rem!important}.pr-2,.px-2{padding-right:.5rem!important}.pb-2,.py-2{padding-bottom:.5rem!important}.pl-2,.px-2{padding-left:.5rem!important}.p-3{padding:1rem!important}.pt-3,.py-3{padding-top:1rem!important}.pr-3,.px-3{padding-right:1rem!important}.pb-3,.py-3{padding-bottom:1rem!important}.pl-3,.px-3{padding-left:1rem!important}.p-4{padding:1.5rem!important}.pt-4,.py-4{padding-top:1.5rem!important}.pr-4,.px-4{padding-right:1.5rem!important}.pb-4,.py-4{padding-bottom:1.5rem!important}.pl-4,.px-4{padding-left:1.5rem!important}.p-5{padding:3rem!important}.pt-5,.py-5{padding-top:3rem!important}.pr-5,.px-5{padding-right:3rem!important}.pb-5,.py-5{padding-bottom:3rem!important}.pl-5,.px-5{padding-left:3rem!important}.m-n1{margin:-.25rem!important}.mt-n1,.my-n1{margin-top:-.25rem!important}.mr-n1,.mx-n1{margin-right:-.25rem!important}.mb-n1,.my-n1{margin-bottom:-.25rem!important}.ml-n1,.mx-n1{margin-left:-.25rem!important}.m-n2{margin:-.5rem!important}.mt-n2,.my-n2{margin-top:-.5rem!important}.mr-n2,.mx-n2{margin-right:-.5rem!important}.mb-n2,.my-n2{margin-bottom:-.5rem!important}.ml-n2,.mx-n2{margin-left:-.5rem!important}.m-n3{margin:-1rem!important}.mt-n3,.my-n3{margin-top:-1rem!important}.mr-n3,.mx-n3{margin-right:-1rem!important}.mb-n3,.my-n3{margin-bottom:-1rem!important}.ml-n3,.mx-n3{margin-left:-1rem!important}.m-n4{margin:-1.5rem!important}.mt-n4,.my-n4{margin-top:-1.5rem!important}.mr-n4,.mx-n4{margin-right:-1.5rem!important}.mb-n4,.my-n4{margin-bottom:-1.5rem!important}.ml-n4,.mx-n4{margin-left:-1.5rem!important}.m-n5{margin:-3rem!important}.mt-n5,.my-n5{margin-top:-3rem!important}.mr-n5,.mx-n5{margin-right:-3rem!important}.mb-n5,.my-n5{margin-bottom:-3rem!important}.ml-n5,.mx-n5{margin-left:-3rem!important}.m-auto{margin:auto!important}.mt-auto,.my-auto{margin-top:auto!important}.mr-auto,.mx-auto{margin-right:auto!important}.mb-auto,.my-auto{margin-bottom:auto!important}.ml-auto,.mx-auto{margin-left:auto!important}@media (min-width:576px){.m-sm-0{margin:0!important}.mt-sm-0,.my-sm-0{margin-top:0!important}.mr-sm-0,.mx-sm-0{margin-right:0!important}.mb-sm-0,.my-sm-0{margin-bottom:0!important}.ml-sm-0,.mx-sm-0{margin-left:0!important}.m-sm-1{margin:.25rem!important}.mt-sm-1,.my-sm-1{margin-top:.25rem!important}.mr-sm-1,.mx-sm-1{margin-right:.25rem!important}.mb-sm-1,.my-sm-1{margin-bottom:.25rem!important}.ml-sm-1,.mx-sm-1{margin-left:.25rem!important}.m-sm-2{margin:.5rem!important}.mt-sm-2,.my-sm-2{margin-top:.5rem!important}.mr-sm-2,.mx-sm-2{margin-right:.5rem!important}.mb-sm-2,.my-sm-2{margin-bottom:.5rem!important}.ml-sm-2,.mx-sm-2{margin-left:.5rem!important}.m-sm-3{margin:1rem!important}.mt-sm-3,.my-sm-3{margin-top:1rem!important}.mr-sm-3,.mx-sm-3{margin-right:1rem!important}.mb-sm-3,.my-sm-3{margin-bottom:1rem!important}.ml-sm-3,.mx-sm-3{margin-left:1rem!important}.m-sm-4{margin:1.5rem!important}.mt-sm-4,.my-sm-4{margin-top:1.5rem!important}.mr-sm-4,.mx-sm-4{margin-right:1.5rem!important}.mb-sm-4,.my-sm-4{margin-bottom:1.5rem!important}.ml-sm-4,.mx-sm-4{margin-left:1.5rem!important}.m-sm-5{margin:3rem!important}.mt-sm-5,.my-sm-5{margin-top:3rem!important}.mr-sm-5,.mx-sm-5{margin-right:3rem!important}.mb-sm-5,.my-sm-5{margin-bottom:3rem!important}.ml-sm-5,.mx-sm-5{margin-left:3rem!important}.p-sm-0{padding:0!important}.pt-sm-0,.py-sm-0{padding-top:0!important}.pr-sm-0,.px-sm-0{padding-right:0!important}.pb-sm-0,.py-sm-0{padding-bottom:0!important}.pl-sm-0,.px-sm-0{padding-left:0!important}.p-sm-1{padding:.25rem!important}.pt-sm-1,.py-sm-1{padding-top:.25rem!important}.pr-sm-1,.px-sm-1{padding-right:.25rem!important}.pb-sm-1,.py-sm-1{padding-bottom:.25rem!important}.pl-sm-1,.px-sm-1{padding-left:.25rem!important}.p-sm-2{padding:.5rem!important}.pt-sm-2,.py-sm-2{padding-top:.5rem!important}.pr-sm-2,.px-sm-2{padding-right:.5rem!important}.pb-sm-2,.py-sm-2{padding-bottom:.5rem!important}.pl-sm-2,.px-sm-2{padding-left:.5rem!important}.p-sm-3{padding:1rem!important}.pt-sm-3,.py-sm-3{padding-top:1rem!important}.pr-sm-3,.px-sm-3{padding-right:1rem!important}.pb-sm-3,.py-sm-3{padding-bottom:1rem!important}.pl-sm-3,.px-sm-3{padding-left:1rem!important}.p-sm-4{padding:1.5rem!important}.pt-sm-4,.py-sm-4{padding-top:1.5rem!important}.pr-sm-4,.px-sm-4{padding-right:1.5rem!important}.pb-sm-4,.py-sm-4{padding-bottom:1.5rem!important}.pl-sm-4,.px-sm-4{padding-left:1.5rem!important}.p-sm-5{padding:3rem!important}.pt-sm-5,.py-sm-5{padding-top:3rem!important}.pr-sm-5,.px-sm-5{padding-right:3rem!important}.pb-sm-5,.py-sm-5{padding-bottom:3rem!important}.pl-sm-5,.px-sm-5{padding-left:3rem!important}.m-sm-n1{margin:-.25rem!important}.mt-sm-n1,.my-sm-n1{margin-top:-.25rem!important}.mr-sm-n1,.mx-sm-n1{margin-right:-.25rem!important}.mb-sm-n1,.my-sm-n1{margin-bottom:-.25rem!important}.ml-sm-n1,.mx-sm-n1{margin-left:-.25rem!important}.m-sm-n2{margin:-.5rem!important}.mt-sm-n2,.my-sm-n2{margin-top:-.5rem!important}.mr-sm-n2,.mx-sm-n2{margin-right:-.5rem!important}.mb-sm-n2,.my-sm-n2{margin-bottom:-.5rem!important}.ml-sm-n2,.mx-sm-n2{margin-left:-.5rem!important}.m-sm-n3{margin:-1rem!important}.mt-sm-n3,.my-sm-n3{margin-top:-1rem!important}.mr-sm-n3,.mx-sm-n3{margin-right:-1rem!important}.mb-sm-n3,.my-sm-n3{margin-bottom:-1rem!important}.ml-sm-n3,.mx-sm-n3{margin-left:-1rem!important}.m-sm-n4{margin:-1.5rem!important}.mt-sm-n4,.my-sm-n4{margin-top:-1.5rem!important}.mr-sm-n4,.mx-sm-n4{margin-right:-1.5rem!important}.mb-sm-n4,.my-sm-n4{margin-bottom:-1.5rem!important}.ml-sm-n4,.mx-sm-n4{margin-left:-1.5rem!important}.m-sm-n5{margin:-3rem!important}.mt-sm-n5,.my-sm-n5{margin-top:-3rem!important}.mr-sm-n5,.mx-sm-n5{margin-right:-3rem!important}.mb-sm-n5,.my-sm-n5{margin-bottom:-3rem!important}.ml-sm-n5,.mx-sm-n5{margin-left:-3rem!important}.m-sm-auto{margin:auto!important}.mt-sm-auto,.my-sm-auto{margin-top:auto!important}.mr-sm-auto,.mx-sm-auto{margin-right:auto!important}.mb-sm-auto,.my-sm-auto{margin-bottom:auto!important}.ml-sm-auto,.mx-sm-auto{margin-left:auto!important}}@media (min-width:768px){.m-md-0{margin:0!important}.mt-md-0,.my-md-0{margin-top:0!important}.mr-md-0,.mx-md-0{margin-right:0!important}.mb-md-0,.my-md-0{margin-bottom:0!important}.ml-md-0,.mx-md-0{margin-left:0!important}.m-md-1{margin:.25rem!important}.mt-md-1,.my-md-1{margin-top:.25rem!important}.mr-md-1,.mx-md-1{margin-right:.25rem!important}.mb-md-1,.my-md-1{margin-bottom:.25rem!important}.ml-md-1,.mx-md-1{margin-left:.25rem!important}.m-md-2{margin:.5rem!important}.mt-md-2,.my-md-2{margin-top:.5rem!important}.mr-md-2,.mx-md-2{margin-right:.5rem!important}.mb-md-2,.my-md-2{margin-bottom:.5rem!important}.ml-md-2,.mx-md-2{margin-left:.5rem!important}.m-md-3{margin:1rem!important}.mt-md-3,.my-md-3{margin-top:1rem!important}.mr-md-3,.mx-md-3{margin-right:1rem!important}.mb-md-3,.my-md-3{margin-bottom:1rem!important}.ml-md-3,.mx-md-3{margin-left:1rem!important}.m-md-4{margin:1.5rem!important}.mt-md-4,.my-md-4{margin-top:1.5rem!important}.mr-md-4,.mx-md-4{margin-right:1.5rem!important}.mb-md-4,.my-md-4{margin-bottom:1.5rem!important}.ml-md-4,.mx-md-4{margin-left:1.5rem!important}.m-md-5{margin:3rem!important}.mt-md-5,.my-md-5{margin-top:3rem!important}.mr-md-5,.mx-md-5{margin-right:3rem!important}.mb-md-5,.my-md-5{margin-bottom:3rem!important}.ml-md-5,.mx-md-5{margin-left:3rem!important}.p-md-0{padding:0!important}.pt-md-0,.py-md-0{padding-top:0!important}.pr-md-0,.px-md-0{padding-right:0!important}.pb-md-0,.py-md-0{padding-bottom:0!important}.pl-md-0,.px-md-0{padding-left:0!important}.p-md-1{padding:.25rem!important}.pt-md-1,.py-md-1{padding-top:.25rem!important}.pr-md-1,.px-md-1{padding-right:.25rem!important}.pb-md-1,.py-md-1{padding-bottom:.25rem!important}.pl-md-1,.px-md-1{padding-left:.25rem!important}.p-md-2{padding:.5rem!important}.pt-md-2,.py-md-2{padding-top:.5rem!important}.pr-md-2,.px-md-2{padding-right:.5rem!important}.pb-md-2,.py-md-2{padding-bottom:.5rem!important}.pl-md-2,.px-md-2{padding-left:.5rem!important}.p-md-3{padding:1rem!important}.pt-md-3,.py-md-3{padding-top:1rem!important}.pr-md-3,.px-md-3{padding-right:1rem!important}.pb-md-3,.py-md-3{padding-bottom:1rem!important}.pl-md-3,.px-md-3{padding-left:1rem!important}.p-md-4{padding:1.5rem!important}.pt-md-4,.py-md-4{padding-top:1.5rem!important}.pr-md-4,.px-md-4{padding-right:1.5rem!important}.pb-md-4,.py-md-4{padding-bottom:1.5rem!important}.pl-md-4,.px-md-4{padding-left:1.5rem!important}.p-md-5{padding:3rem!important}.pt-md-5,.py-md-5{padding-top:3rem!important}.pr-md-5,.px-md-5{padding-right:3rem!important}.pb-md-5,.py-md-5{padding-bottom:3rem!important}.pl-md-5,.px-md-5{padding-left:3rem!important}.m-md-n1{margin:-.25rem!important}.mt-md-n1,.my-md-n1{margin-top:-.25rem!important}.mr-md-n1,.mx-md-n1{margin-right:-.25rem!important}.mb-md-n1,.my-md-n1{margin-bottom:-.25rem!important}.ml-md-n1,.mx-md-n1{margin-left:-.25rem!important}.m-md-n2{margin:-.5rem!important}.mt-md-n2,.my-md-n2{margin-top:-.5rem!important}.mr-md-n2,.mx-md-n2{margin-right:-.5rem!important}.mb-md-n2,.my-md-n2{margin-bottom:-.5rem!important}.ml-md-n2,.mx-md-n2{margin-left:-.5rem!important}.m-md-n3{margin:-1rem!important}.mt-md-n3,.my-md-n3{margin-top:-1rem!important}.mr-md-n3,.mx-md-n3{margin-right:-1rem!important}.mb-md-n3,.my-md-n3{margin-bottom:-1rem!important}.ml-md-n3,.mx-md-n3{margin-left:-1rem!important}.m-md-n4{margin:-1.5rem!important}.mt-md-n4,.my-md-n4{margin-top:-1.5rem!important}.mr-md-n4,.mx-md-n4{margin-right:-1.5rem!important}.mb-md-n4,.my-md-n4{margin-bottom:-1.5rem!important}.ml-md-n4,.mx-md-n4{margin-left:-1.5rem!important}.m-md-n5{margin:-3rem!important}.mt-md-n5,.my-md-n5{margin-top:-3rem!important}.mr-md-n5,.mx-md-n5{margin-right:-3rem!important}.mb-md-n5,.my-md-n5{margin-bottom:-3rem!important}.ml-md-n5,.mx-md-n5{margin-left:-3rem!important}.m-md-auto{margin:auto!important}.mt-md-auto,.my-md-auto{margin-top:auto!important}.mr-md-auto,.mx-md-auto{margin-right:auto!important}.mb-md-auto,.my-md-auto{margin-bottom:auto!important}.ml-md-auto,.mx-md-auto{margin-left:auto!important}}@media (min-width:992px){.m-lg-0{margin:0!important}.mt-lg-0,.my-lg-0{margin-top:0!important}.mr-lg-0,.mx-lg-0{margin-right:0!important}.mb-lg-0,.my-lg-0{margin-bottom:0!important}.ml-lg-0,.mx-lg-0{margin-left:0!important}.m-lg-1{margin:.25rem!important}.mt-lg-1,.my-lg-1{margin-top:.25rem!important}.mr-lg-1,.mx-lg-1{margin-right:.25rem!important}.mb-lg-1,.my-lg-1{margin-bottom:.25rem!important}.ml-lg-1,.mx-lg-1{margin-left:.25rem!important}.m-lg-2{margin:.5rem!important}.mt-lg-2,.my-lg-2{margin-top:.5rem!important}.mr-lg-2,.mx-lg-2{margin-right:.5rem!important}.mb-lg-2,.my-lg-2{margin-bottom:.5rem!important}.ml-lg-2,.mx-lg-2{margin-left:.5rem!important}.m-lg-3{margin:1rem!important}.mt-lg-3,.my-lg-3{margin-top:1rem!important}.mr-lg-3,.mx-lg-3{margin-right:1rem!important}.mb-lg-3,.my-lg-3{margin-bottom:1rem!important}.ml-lg-3,.mx-lg-3{margin-left:1rem!important}.m-lg-4{margin:1.5rem!important}.mt-lg-4,.my-lg-4{margin-top:1.5rem!important}.mr-lg-4,.mx-lg-4{margin-right:1.5rem!important}.mb-lg-4,.my-lg-4{margin-bottom:1.5rem!important}.ml-lg-4,.mx-lg-4{margin-left:1.5rem!important}.m-lg-5{margin:3rem!important}.mt-lg-5,.my-lg-5{margin-top:3rem!important}.mr-lg-5,.mx-lg-5{margin-right:3rem!important}.mb-lg-5,.my-lg-5{margin-bottom:3rem!important}.ml-lg-5,.mx-lg-5{margin-left:3rem!important}.p-lg-0{padding:0!important}.pt-lg-0,.py-lg-0{padding-top:0!important}.pr-lg-0,.px-lg-0{padding-right:0!important}.pb-lg-0,.py-lg-0{padding-bottom:0!important}.pl-lg-0,.px-lg-0{padding-left:0!important}.p-lg-1{padding:.25rem!important}.pt-lg-1,.py-lg-1{padding-top:.25rem!important}.pr-lg-1,.px-lg-1{padding-right:.25rem!important}.pb-lg-1,.py-lg-1{padding-bottom:.25rem!important}.pl-lg-1,.px-lg-1{padding-left:.25rem!important}.p-lg-2{padding:.5rem!important}.pt-lg-2,.py-lg-2{padding-top:.5rem!important}.pr-lg-2,.px-lg-2{padding-right:.5rem!important}.pb-lg-2,.py-lg-2{padding-bottom:.5rem!important}.pl-lg-2,.px-lg-2{padding-left:.5rem!important}.p-lg-3{padding:1rem!important}.pt-lg-3,.py-lg-3{padding-top:1rem!important}.pr-lg-3,.px-lg-3{padding-right:1rem!important}.pb-lg-3,.py-lg-3{padding-bottom:1rem!important}.pl-lg-3,.px-lg-3{padding-left:1rem!important}.p-lg-4{padding:1.5rem!important}.pt-lg-4,.py-lg-4{padding-top:1.5rem!important}.pr-lg-4,.px-lg-4{padding-right:1.5rem!important}.pb-lg-4,.py-lg-4{padding-bottom:1.5rem!important}.pl-lg-4,.px-lg-4{padding-left:1.5rem!important}.p-lg-5{padding:3rem!important}.pt-lg-5,.py-lg-5{padding-top:3rem!important}.pr-lg-5,.px-lg-5{padding-right:3rem!important}.pb-lg-5,.py-lg-5{padding-bottom:3rem!important}.pl-lg-5,.px-lg-5{padding-left:3rem!important}.m-lg-n1{margin:-.25rem!important}.mt-lg-n1,.my-lg-n1{margin-top:-.25rem!important}.mr-lg-n1,.mx-lg-n1{margin-right:-.25rem!important}.mb-lg-n1,.my-lg-n1{margin-bottom:-.25rem!important}.ml-lg-n1,.mx-lg-n1{margin-left:-.25rem!important}.m-lg-n2{margin:-.5rem!important}.mt-lg-n2,.my-lg-n2{margin-top:-.5rem!important}.mr-lg-n2,.mx-lg-n2{margin-right:-.5rem!important}.mb-lg-n2,.my-lg-n2{margin-bottom:-.5rem!important}.ml-lg-n2,.mx-lg-n2{margin-left:-.5rem!important}.m-lg-n3{margin:-1rem!important}.mt-lg-n3,.my-lg-n3{margin-top:-1rem!important}.mr-lg-n3,.mx-lg-n3{margin-right:-1rem!important}.mb-lg-n3,.my-lg-n3{margin-bottom:-1rem!important}.ml-lg-n3,.mx-lg-n3{margin-left:-1rem!important}.m-lg-n4{margin:-1.5rem!important}.mt-lg-n4,.my-lg-n4{margin-top:-1.5rem!important}.mr-lg-n4,.mx-lg-n4{margin-right:-1.5rem!important}.mb-lg-n4,.my-lg-n4{margin-bottom:-1.5rem!important}.ml-lg-n4,.mx-lg-n4{margin-left:-1.5rem!important}.m-lg-n5{margin:-3rem!important}.mt-lg-n5,.my-lg-n5{margin-top:-3rem!important}.mr-lg-n5,.mx-lg-n5{margin-right:-3rem!important}.mb-lg-n5,.my-lg-n5{margin-bottom:-3rem!important}.ml-lg-n5,.mx-lg-n5{margin-left:-3rem!important}.m-lg-auto{margin:auto!important}.mt-lg-auto,.my-lg-auto{margin-top:auto!important}.mr-lg-auto,.mx-lg-auto{margin-right:auto!important}.mb-lg-auto,.my-lg-auto{margin-bottom:auto!important}.ml-lg-auto,.mx-lg-auto{margin-left:auto!important}}@media (min-width:1200px){.m-xl-0{margin:0!important}.mt-xl-0,.my-xl-0{margin-top:0!important}.mr-xl-0,.mx-xl-0{margin-right:0!important}.mb-xl-0,.my-xl-0{margin-bottom:0!important}.ml-xl-0,.mx-xl-0{margin-left:0!important}.m-xl-1{margin:.25rem!important}.mt-xl-1,.my-xl-1{margin-top:.25rem!important}.mr-xl-1,.mx-xl-1{margin-right:.25rem!important}.mb-xl-1,.my-xl-1{margin-bottom:.25rem!important}.ml-xl-1,.mx-xl-1{margin-left:.25rem!important}.m-xl-2{margin:.5rem!important}.mt-xl-2,.my-xl-2{margin-top:.5rem!important}.mr-xl-2,.mx-xl-2{margin-right:.5rem!important}.mb-xl-2,.my-xl-2{margin-bottom:.5rem!important}.ml-xl-2,.mx-xl-2{margin-left:.5rem!important}.m-xl-3{margin:1rem!important}.mt-xl-3,.my-xl-3{margin-top:1rem!important}.mr-xl-3,.mx-xl-3{margin-right:1rem!important}.mb-xl-3,.my-xl-3{margin-bottom:1rem!important}.ml-xl-3,.mx-xl-3{margin-left:1rem!important}.m-xl-4{margin:1.5rem!important}.mt-xl-4,.my-xl-4{margin-top:1.5rem!important}.mr-xl-4,.mx-xl-4{margin-right:1.5rem!important}.mb-xl-4,.my-xl-4{margin-bottom:1.5rem!important}.ml-xl-4,.mx-xl-4{margin-left:1.5rem!important}.m-xl-5{margin:3rem!important}.mt-xl-5,.my-xl-5{margin-top:3rem!important}.mr-xl-5,.mx-xl-5{margin-right:3rem!important}.mb-xl-5,.my-xl-5{margin-bottom:3rem!important}.ml-xl-5,.mx-xl-5{margin-left:3rem!important}.p-xl-0{padding:0!important}.pt-xl-0,.py-xl-0{padding-top:0!important}.pr-xl-0,.px-xl-0{padding-right:0!important}.pb-xl-0,.py-xl-0{padding-bottom:0!important}.pl-xl-0,.px-xl-0{padding-left:0!important}.p-xl-1{padding:.25rem!important}.pt-xl-1,.py-xl-1{padding-top:.25rem!important}.pr-xl-1,.px-xl-1{padding-right:.25rem!important}.pb-xl-1,.py-xl-1{padding-bottom:.25rem!important}.pl-xl-1,.px-xl-1{padding-left:.25rem!important}.p-xl-2{padding:.5rem!important}.pt-xl-2,.py-xl-2{padding-top:.5rem!important}.pr-xl-2,.px-xl-2{padding-right:.5rem!important}.pb-xl-2,.py-xl-2{padding-bottom:.5rem!important}.pl-xl-2,.px-xl-2{padding-left:.5rem!important}.p-xl-3{padding:1rem!important}.pt-xl-3,.py-xl-3{padding-top:1rem!important}.pr-xl-3,.px-xl-3{padding-right:1rem!important}.pb-xl-3,.py-xl-3{padding-bottom:1rem!important}.pl-xl-3,.px-xl-3{padding-left:1rem!important}.p-xl-4{padding:1.5rem!important}.pt-xl-4,.py-xl-4{padding-top:1.5rem!important}.pr-xl-4,.px-xl-4{padding-right:1.5rem!important}.pb-xl-4,.py-xl-4{padding-bottom:1.5rem!important}.pl-xl-4,.px-xl-4{padding-left:1.5rem!important}.p-xl-5{padding:3rem!important}.pt-xl-5,.py-xl-5{padding-top:3rem!important}.pr-xl-5,.px-xl-5{padding-right:3rem!important}.pb-xl-5,.py-xl-5{padding-bottom:3rem!important}.pl-xl-5,.px-xl-5{padding-left:3rem!important}.m-xl-n1{margin:-.25rem!important}.mt-xl-n1,.my-xl-n1{margin-top:-.25rem!important}.mr-xl-n1,.mx-xl-n1{margin-right:-.25rem!important}.mb-xl-n1,.my-xl-n1{margin-bottom:-.25rem!important}.ml-xl-n1,.mx-xl-n1{margin-left:-.25rem!important}.m-xl-n2{margin:-.5rem!important}.mt-xl-n2,.my-xl-n2{margin-top:-.5rem!important}.mr-xl-n2,.mx-xl-n2{margin-right:-.5rem!important}.mb-xl-n2,.my-xl-n2{margin-bottom:-.5rem!important}.ml-xl-n2,.mx-xl-n2{margin-left:-.5rem!important}.m-xl-n3{margin:-1rem!important}.mt-xl-n3,.my-xl-n3{margin-top:-1rem!important}.mr-xl-n3,.mx-xl-n3{margin-right:-1rem!important}.mb-xl-n3,.my-xl-n3{margin-bottom:-1rem!important}.ml-xl-n3,.mx-xl-n3{margin-left:-1rem!important}.m-xl-n4{margin:-1.5rem!important}.mt-xl-n4,.my-xl-n4{margin-top:-1.5rem!important}.mr-xl-n4,.mx-xl-n4{margin-right:-1.5rem!important}.mb-xl-n4,.my-xl-n4{margin-bottom:-1.5rem!important}.ml-xl-n4,.mx-xl-n4{margin-left:-1.5rem!important}.m-xl-n5{margin:-3rem!important}.mt-xl-n5,.my-xl-n5{margin-top:-3rem!important}.mr-xl-n5,.mx-xl-n5{margin-right:-3rem!important}.mb-xl-n5,.my-xl-n5{margin-bottom:-3rem!important}.ml-xl-n5,.mx-xl-n5{margin-left:-3rem!important}.m-xl-auto{margin:auto!important}.mt-xl-auto,.my-xl-auto{margin-top:auto!important}.mr-xl-auto,.mx-xl-auto{margin-right:auto!important}.mb-xl-auto,.my-xl-auto{margin-bottom:auto!important}.ml-xl-auto,.mx-xl-auto{margin-left:auto!important}}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;pointer-events:auto;content:"";background-color:rgba(0,0,0,0)}.text-monospace{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace!important}.text-justify{text-align:justify!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-left{text-align:left!important}.text-right{text-align:right!important}.text-center{text-align:center!important}@media (min-width:576px){.text-sm-left{text-align:left!important}.text-sm-right{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.text-md-left{text-align:left!important}.text-md-right{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.text-lg-left{text-align:left!important}.text-lg-right{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.text-xl-left{text-align:left!important}.text-xl-right{text-align:right!important}.text-xl-center{text-align:center!important}}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.font-weight-light{font-weight:300!important}.font-weight-lighter{font-weight:lighter!important}.font-weight-normal{font-weight:400!important}.font-weight-bold{font-weight:700!important}.font-weight-bolder{font-weight:bolder!important}.font-italic{font-style:italic!important}.text-white{color:#fff!important}.text-primary{color:#007bff!important}a.text-primary:focus,a.text-primary:hover{color:#0056b3!important}.text-secondary{color:#6c757d!important}a.text-secondary:focus,a.text-secondary:hover{color:#494f54!important}.text-success{color:#28a745!important}a.text-success:focus,a.text-success:hover{color:#19692c!important}.text-info{color:#17a2b8!important}a.text-info:focus,a.text-info:hover{color:#0f6674!important}.text-warning{color:#ffc107!important}a.text-warning:focus,a.text-warning:hover{color:#ba8b00!important}.text-danger{color:#dc3545!important}a.text-danger:focus,a.text-danger:hover{color:#a71d2a!important}.text-light{color:#f8f9fa!important}a.text-light:focus,a.text-light:hover{color:#cbd3da!important}.text-dark{color:#343a40!important}a.text-dark:focus,a.text-dark:hover{color:#121416!important}.text-body{color:#212529!important}.text-muted{color:#6c757d!important}.text-black-50{color:rgba(0,0,0,.5)!important}.text-white-50{color:rgba(255,255,255,.5)!important}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.text-decoration-none{text-decoration:none!important}.text-break{word-wrap:break-word!important}.text-reset{color:inherit!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media print{*,::after,::before{text-shadow:none!important;box-shadow:none!important}a:not(.btn){text-decoration:underline}abbr[title]::after{content:" (" attr(title) ")"}pre{white-space:pre-wrap!important}blockquote,pre{border:1px solid #adb5bd;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}@page{size:a3}body{min-width:992px!important}.container{min-width:992px!important}.navbar{display:none}.badge{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #dee2e6!important}.table-dark{color:inherit}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#dee2e6}.table .thead-dark th{color:inherit;border-color:#dee2e6}} -/*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file +@charset "UTF-8";/*! + * Bootstrap v5.3.8 (https://getbootstrap.com/) + * Copyright 2011-2025 The Bootstrap Authors + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */:root,[data-bs-theme=light]{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-black:#000;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-gray-100:#f8f9fa;--bs-gray-200:#e9ecef;--bs-gray-300:#dee2e6;--bs-gray-400:#ced4da;--bs-gray-500:#adb5bd;--bs-gray-600:#6c757d;--bs-gray-700:#495057;--bs-gray-800:#343a40;--bs-gray-900:#212529;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-primary-rgb:13,110,253;--bs-secondary-rgb:108,117,125;--bs-success-rgb:25,135,84;--bs-info-rgb:13,202,240;--bs-warning-rgb:255,193,7;--bs-danger-rgb:220,53,69;--bs-light-rgb:248,249,250;--bs-dark-rgb:33,37,41;--bs-primary-text-emphasis:#052c65;--bs-secondary-text-emphasis:#2b2f32;--bs-success-text-emphasis:#0a3622;--bs-info-text-emphasis:#055160;--bs-warning-text-emphasis:#664d03;--bs-danger-text-emphasis:#58151c;--bs-light-text-emphasis:#495057;--bs-dark-text-emphasis:#495057;--bs-primary-bg-subtle:#cfe2ff;--bs-secondary-bg-subtle:#e2e3e5;--bs-success-bg-subtle:#d1e7dd;--bs-info-bg-subtle:#cff4fc;--bs-warning-bg-subtle:#fff3cd;--bs-danger-bg-subtle:#f8d7da;--bs-light-bg-subtle:#fcfcfd;--bs-dark-bg-subtle:#ced4da;--bs-primary-border-subtle:#9ec5fe;--bs-secondary-border-subtle:#c4c8cb;--bs-success-border-subtle:#a3cfbb;--bs-info-border-subtle:#9eeaf9;--bs-warning-border-subtle:#ffe69c;--bs-danger-border-subtle:#f1aeb5;--bs-light-border-subtle:#e9ecef;--bs-dark-border-subtle:#adb5bd;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans","Liberation Sans",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#212529;--bs-body-color-rgb:33,37,41;--bs-body-bg:#fff;--bs-body-bg-rgb:255,255,255;--bs-emphasis-color:#000;--bs-emphasis-color-rgb:0,0,0;--bs-secondary-color:rgba(33, 37, 41, 0.75);--bs-secondary-color-rgb:33,37,41;--bs-secondary-bg:#e9ecef;--bs-secondary-bg-rgb:233,236,239;--bs-tertiary-color:rgba(33, 37, 41, 0.5);--bs-tertiary-color-rgb:33,37,41;--bs-tertiary-bg:#f8f9fa;--bs-tertiary-bg-rgb:248,249,250;--bs-heading-color:inherit;--bs-link-color:#0d6efd;--bs-link-color-rgb:13,110,253;--bs-link-decoration:underline;--bs-link-hover-color:#0a58ca;--bs-link-hover-color-rgb:10,88,202;--bs-code-color:#d63384;--bs-highlight-color:#212529;--bs-highlight-bg:#fff3cd;--bs-border-width:1px;--bs-border-style:solid;--bs-border-color:#dee2e6;--bs-border-color-translucent:rgba(0, 0, 0, 0.175);--bs-border-radius:0.375rem;--bs-border-radius-sm:0.25rem;--bs-border-radius-lg:0.5rem;--bs-border-radius-xl:1rem;--bs-border-radius-xxl:2rem;--bs-border-radius-2xl:var(--bs-border-radius-xxl);--bs-border-radius-pill:50rem;--bs-box-shadow:0 0.5rem 1rem rgba(0, 0, 0, 0.15);--bs-box-shadow-sm:0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);--bs-box-shadow-lg:0 1rem 3rem rgba(0, 0, 0, 0.175);--bs-box-shadow-inset:inset 0 1px 2px rgba(0, 0, 0, 0.075);--bs-focus-ring-width:0.25rem;--bs-focus-ring-opacity:0.25;--bs-focus-ring-color:rgba(13, 110, 253, 0.25);--bs-form-valid-color:#198754;--bs-form-valid-border-color:#198754;--bs-form-invalid-color:#dc3545;--bs-form-invalid-border-color:#dc3545}[data-bs-theme=dark]{color-scheme:dark;--bs-body-color:#dee2e6;--bs-body-color-rgb:222,226,230;--bs-body-bg:#212529;--bs-body-bg-rgb:33,37,41;--bs-emphasis-color:#fff;--bs-emphasis-color-rgb:255,255,255;--bs-secondary-color:rgba(222, 226, 230, 0.75);--bs-secondary-color-rgb:222,226,230;--bs-secondary-bg:#343a40;--bs-secondary-bg-rgb:52,58,64;--bs-tertiary-color:rgba(222, 226, 230, 0.5);--bs-tertiary-color-rgb:222,226,230;--bs-tertiary-bg:#2b3035;--bs-tertiary-bg-rgb:43,48,53;--bs-primary-text-emphasis:#6ea8fe;--bs-secondary-text-emphasis:#a7acb1;--bs-success-text-emphasis:#75b798;--bs-info-text-emphasis:#6edff6;--bs-warning-text-emphasis:#ffda6a;--bs-danger-text-emphasis:#ea868f;--bs-light-text-emphasis:#f8f9fa;--bs-dark-text-emphasis:#dee2e6;--bs-primary-bg-subtle:#031633;--bs-secondary-bg-subtle:#161719;--bs-success-bg-subtle:#051b11;--bs-info-bg-subtle:#032830;--bs-warning-bg-subtle:#332701;--bs-danger-bg-subtle:#2c0b0e;--bs-light-bg-subtle:#343a40;--bs-dark-bg-subtle:#1a1d20;--bs-primary-border-subtle:#084298;--bs-secondary-border-subtle:#41464b;--bs-success-border-subtle:#0f5132;--bs-info-border-subtle:#087990;--bs-warning-border-subtle:#997404;--bs-danger-border-subtle:#842029;--bs-light-border-subtle:#495057;--bs-dark-border-subtle:#343a40;--bs-heading-color:inherit;--bs-link-color:#6ea8fe;--bs-link-hover-color:#8bb9fe;--bs-link-color-rgb:110,168,254;--bs-link-hover-color-rgb:139,185,254;--bs-code-color:#e685b5;--bs-highlight-color:#dee2e6;--bs-highlight-bg:#664d03;--bs-border-color:#495057;--bs-border-color-translucent:rgba(255, 255, 255, 0.15);--bs-form-valid-color:#75b798;--bs-form-valid-border-color:#75b798;--bs-form-invalid-color:#ea868f;--bs-form-invalid-border-color:#ea868f}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;border:0;border-top:var(--bs-border-width) solid;opacity:.25}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2;color:var(--bs-heading-color)}.h1,h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){.h1,h1{font-size:2.5rem}}.h2,h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){.h2,h2{font-size:2rem}}.h3,h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){.h3,h3{font-size:1.75rem}}.h4,h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){.h4,h4{font-size:1.5rem}}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}.small,small{font-size:.875em}.mark,mark{padding:.1875em;color:var(--bs-highlight-color);background-color:var(--bs-highlight-bg)}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:rgba(var(--bs-link-color-rgb),var(--bs-link-opacity,1));text-decoration:underline}a:hover{--bs-link-color-rgb:var(--bs-link-hover-color-rgb)}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:var(--bs-code-color);word-wrap:break-word}a>code{color:inherit}kbd{padding:.1875rem .375rem;font-size:.875em;color:var(--bs-body-bg);background-color:var(--bs-body-color);border-radius:.25rem}kbd kbd{padding:0;font-size:1em}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:var(--bs-secondary-color);text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator{display:none!important}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;line-height:inherit;font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-cancel-button{cursor:pointer;filter:grayscale(1)}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}::file-selector-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}.lead{font-size:1.25rem;font-weight:300}.display-1{font-weight:300;line-height:1.2;font-size:calc(1.625rem + 4.5vw)}@media (min-width:1200px){.display-1{font-size:5rem}}.display-2{font-weight:300;line-height:1.2;font-size:calc(1.575rem + 3.9vw)}@media (min-width:1200px){.display-2{font-size:4.5rem}}.display-3{font-weight:300;line-height:1.2;font-size:calc(1.525rem + 3.3vw)}@media (min-width:1200px){.display-3{font-size:4rem}}.display-4{font-weight:300;line-height:1.2;font-size:calc(1.475rem + 2.7vw)}@media (min-width:1200px){.display-4{font-size:3.5rem}}.display-5{font-weight:300;line-height:1.2;font-size:calc(1.425rem + 2.1vw)}@media (min-width:1200px){.display-5{font-size:3rem}}.display-6{font-weight:300;line-height:1.2;font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){.display-6{font-size:2.5rem}}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:.875em;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{margin-top:-1rem;margin-bottom:1rem;font-size:.875em;color:#6c757d}.blockquote-footer::before{content:"— "}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:var(--bs-body-bg);border:var(--bs-border-width) solid var(--bs-border-color);border-radius:var(--bs-border-radius);max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:.875em;color:var(--bs-secondary-color)}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{--bs-gutter-x:1.5rem;--bs-gutter-y:0;width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}@media (min-width:1400px){.container,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{max-width:1320px}}:root{--bs-breakpoint-xs:0;--bs-breakpoint-sm:576px;--bs-breakpoint-md:768px;--bs-breakpoint-lg:992px;--bs-breakpoint-xl:1200px;--bs-breakpoint-xxl:1400px}.row{--bs-gutter-x:1.5rem;--bs-gutter-y:0;display:flex;flex-wrap:wrap;margin-top:calc(-1 * var(--bs-gutter-y));margin-right:calc(-.5 * var(--bs-gutter-x));margin-left:calc(-.5 * var(--bs-gutter-x))}.row>*{flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-top:var(--bs-gutter-y)}.col{flex:1 0 0}.row-cols-auto>*{flex:0 0 auto;width:auto}.row-cols-1>*{flex:0 0 auto;width:100%}.row-cols-2>*{flex:0 0 auto;width:50%}.row-cols-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-4>*{flex:0 0 auto;width:25%}.row-cols-5>*{flex:0 0 auto;width:20%}.row-cols-6>*{flex:0 0 auto;width:16.66666667%}.col-auto{flex:0 0 auto;width:auto}.col-1{flex:0 0 auto;width:8.33333333%}.col-2{flex:0 0 auto;width:16.66666667%}.col-3{flex:0 0 auto;width:25%}.col-4{flex:0 0 auto;width:33.33333333%}.col-5{flex:0 0 auto;width:41.66666667%}.col-6{flex:0 0 auto;width:50%}.col-7{flex:0 0 auto;width:58.33333333%}.col-8{flex:0 0 auto;width:66.66666667%}.col-9{flex:0 0 auto;width:75%}.col-10{flex:0 0 auto;width:83.33333333%}.col-11{flex:0 0 auto;width:91.66666667%}.col-12{flex:0 0 auto;width:100%}.offset-1{margin-left:8.33333333%}.offset-2{margin-left:16.66666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.33333333%}.offset-5{margin-left:41.66666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.33333333%}.offset-8{margin-left:66.66666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.33333333%}.offset-11{margin-left:91.66666667%}.g-0,.gx-0{--bs-gutter-x:0}.g-0,.gy-0{--bs-gutter-y:0}.g-1,.gx-1{--bs-gutter-x:0.25rem}.g-1,.gy-1{--bs-gutter-y:0.25rem}.g-2,.gx-2{--bs-gutter-x:0.5rem}.g-2,.gy-2{--bs-gutter-y:0.5rem}.g-3,.gx-3{--bs-gutter-x:1rem}.g-3,.gy-3{--bs-gutter-y:1rem}.g-4,.gx-4{--bs-gutter-x:1.5rem}.g-4,.gy-4{--bs-gutter-y:1.5rem}.g-5,.gx-5{--bs-gutter-x:3rem}.g-5,.gy-5{--bs-gutter-y:3rem}@media (min-width:576px){.col-sm{flex:1 0 0}.row-cols-sm-auto>*{flex:0 0 auto;width:auto}.row-cols-sm-1>*{flex:0 0 auto;width:100%}.row-cols-sm-2>*{flex:0 0 auto;width:50%}.row-cols-sm-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-sm-4>*{flex:0 0 auto;width:25%}.row-cols-sm-5>*{flex:0 0 auto;width:20%}.row-cols-sm-6>*{flex:0 0 auto;width:16.66666667%}.col-sm-auto{flex:0 0 auto;width:auto}.col-sm-1{flex:0 0 auto;width:8.33333333%}.col-sm-2{flex:0 0 auto;width:16.66666667%}.col-sm-3{flex:0 0 auto;width:25%}.col-sm-4{flex:0 0 auto;width:33.33333333%}.col-sm-5{flex:0 0 auto;width:41.66666667%}.col-sm-6{flex:0 0 auto;width:50%}.col-sm-7{flex:0 0 auto;width:58.33333333%}.col-sm-8{flex:0 0 auto;width:66.66666667%}.col-sm-9{flex:0 0 auto;width:75%}.col-sm-10{flex:0 0 auto;width:83.33333333%}.col-sm-11{flex:0 0 auto;width:91.66666667%}.col-sm-12{flex:0 0 auto;width:100%}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.33333333%}.offset-sm-2{margin-left:16.66666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.33333333%}.offset-sm-5{margin-left:41.66666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.33333333%}.offset-sm-8{margin-left:66.66666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.33333333%}.offset-sm-11{margin-left:91.66666667%}.g-sm-0,.gx-sm-0{--bs-gutter-x:0}.g-sm-0,.gy-sm-0{--bs-gutter-y:0}.g-sm-1,.gx-sm-1{--bs-gutter-x:0.25rem}.g-sm-1,.gy-sm-1{--bs-gutter-y:0.25rem}.g-sm-2,.gx-sm-2{--bs-gutter-x:0.5rem}.g-sm-2,.gy-sm-2{--bs-gutter-y:0.5rem}.g-sm-3,.gx-sm-3{--bs-gutter-x:1rem}.g-sm-3,.gy-sm-3{--bs-gutter-y:1rem}.g-sm-4,.gx-sm-4{--bs-gutter-x:1.5rem}.g-sm-4,.gy-sm-4{--bs-gutter-y:1.5rem}.g-sm-5,.gx-sm-5{--bs-gutter-x:3rem}.g-sm-5,.gy-sm-5{--bs-gutter-y:3rem}}@media (min-width:768px){.col-md{flex:1 0 0}.row-cols-md-auto>*{flex:0 0 auto;width:auto}.row-cols-md-1>*{flex:0 0 auto;width:100%}.row-cols-md-2>*{flex:0 0 auto;width:50%}.row-cols-md-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-md-4>*{flex:0 0 auto;width:25%}.row-cols-md-5>*{flex:0 0 auto;width:20%}.row-cols-md-6>*{flex:0 0 auto;width:16.66666667%}.col-md-auto{flex:0 0 auto;width:auto}.col-md-1{flex:0 0 auto;width:8.33333333%}.col-md-2{flex:0 0 auto;width:16.66666667%}.col-md-3{flex:0 0 auto;width:25%}.col-md-4{flex:0 0 auto;width:33.33333333%}.col-md-5{flex:0 0 auto;width:41.66666667%}.col-md-6{flex:0 0 auto;width:50%}.col-md-7{flex:0 0 auto;width:58.33333333%}.col-md-8{flex:0 0 auto;width:66.66666667%}.col-md-9{flex:0 0 auto;width:75%}.col-md-10{flex:0 0 auto;width:83.33333333%}.col-md-11{flex:0 0 auto;width:91.66666667%}.col-md-12{flex:0 0 auto;width:100%}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.33333333%}.offset-md-2{margin-left:16.66666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.33333333%}.offset-md-5{margin-left:41.66666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.33333333%}.offset-md-8{margin-left:66.66666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.33333333%}.offset-md-11{margin-left:91.66666667%}.g-md-0,.gx-md-0{--bs-gutter-x:0}.g-md-0,.gy-md-0{--bs-gutter-y:0}.g-md-1,.gx-md-1{--bs-gutter-x:0.25rem}.g-md-1,.gy-md-1{--bs-gutter-y:0.25rem}.g-md-2,.gx-md-2{--bs-gutter-x:0.5rem}.g-md-2,.gy-md-2{--bs-gutter-y:0.5rem}.g-md-3,.gx-md-3{--bs-gutter-x:1rem}.g-md-3,.gy-md-3{--bs-gutter-y:1rem}.g-md-4,.gx-md-4{--bs-gutter-x:1.5rem}.g-md-4,.gy-md-4{--bs-gutter-y:1.5rem}.g-md-5,.gx-md-5{--bs-gutter-x:3rem}.g-md-5,.gy-md-5{--bs-gutter-y:3rem}}@media (min-width:992px){.col-lg{flex:1 0 0}.row-cols-lg-auto>*{flex:0 0 auto;width:auto}.row-cols-lg-1>*{flex:0 0 auto;width:100%}.row-cols-lg-2>*{flex:0 0 auto;width:50%}.row-cols-lg-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-lg-4>*{flex:0 0 auto;width:25%}.row-cols-lg-5>*{flex:0 0 auto;width:20%}.row-cols-lg-6>*{flex:0 0 auto;width:16.66666667%}.col-lg-auto{flex:0 0 auto;width:auto}.col-lg-1{flex:0 0 auto;width:8.33333333%}.col-lg-2{flex:0 0 auto;width:16.66666667%}.col-lg-3{flex:0 0 auto;width:25%}.col-lg-4{flex:0 0 auto;width:33.33333333%}.col-lg-5{flex:0 0 auto;width:41.66666667%}.col-lg-6{flex:0 0 auto;width:50%}.col-lg-7{flex:0 0 auto;width:58.33333333%}.col-lg-8{flex:0 0 auto;width:66.66666667%}.col-lg-9{flex:0 0 auto;width:75%}.col-lg-10{flex:0 0 auto;width:83.33333333%}.col-lg-11{flex:0 0 auto;width:91.66666667%}.col-lg-12{flex:0 0 auto;width:100%}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.33333333%}.offset-lg-2{margin-left:16.66666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.33333333%}.offset-lg-5{margin-left:41.66666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.33333333%}.offset-lg-8{margin-left:66.66666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.33333333%}.offset-lg-11{margin-left:91.66666667%}.g-lg-0,.gx-lg-0{--bs-gutter-x:0}.g-lg-0,.gy-lg-0{--bs-gutter-y:0}.g-lg-1,.gx-lg-1{--bs-gutter-x:0.25rem}.g-lg-1,.gy-lg-1{--bs-gutter-y:0.25rem}.g-lg-2,.gx-lg-2{--bs-gutter-x:0.5rem}.g-lg-2,.gy-lg-2{--bs-gutter-y:0.5rem}.g-lg-3,.gx-lg-3{--bs-gutter-x:1rem}.g-lg-3,.gy-lg-3{--bs-gutter-y:1rem}.g-lg-4,.gx-lg-4{--bs-gutter-x:1.5rem}.g-lg-4,.gy-lg-4{--bs-gutter-y:1.5rem}.g-lg-5,.gx-lg-5{--bs-gutter-x:3rem}.g-lg-5,.gy-lg-5{--bs-gutter-y:3rem}}@media (min-width:1200px){.col-xl{flex:1 0 0}.row-cols-xl-auto>*{flex:0 0 auto;width:auto}.row-cols-xl-1>*{flex:0 0 auto;width:100%}.row-cols-xl-2>*{flex:0 0 auto;width:50%}.row-cols-xl-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-xl-4>*{flex:0 0 auto;width:25%}.row-cols-xl-5>*{flex:0 0 auto;width:20%}.row-cols-xl-6>*{flex:0 0 auto;width:16.66666667%}.col-xl-auto{flex:0 0 auto;width:auto}.col-xl-1{flex:0 0 auto;width:8.33333333%}.col-xl-2{flex:0 0 auto;width:16.66666667%}.col-xl-3{flex:0 0 auto;width:25%}.col-xl-4{flex:0 0 auto;width:33.33333333%}.col-xl-5{flex:0 0 auto;width:41.66666667%}.col-xl-6{flex:0 0 auto;width:50%}.col-xl-7{flex:0 0 auto;width:58.33333333%}.col-xl-8{flex:0 0 auto;width:66.66666667%}.col-xl-9{flex:0 0 auto;width:75%}.col-xl-10{flex:0 0 auto;width:83.33333333%}.col-xl-11{flex:0 0 auto;width:91.66666667%}.col-xl-12{flex:0 0 auto;width:100%}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.33333333%}.offset-xl-2{margin-left:16.66666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.33333333%}.offset-xl-5{margin-left:41.66666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.33333333%}.offset-xl-8{margin-left:66.66666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.33333333%}.offset-xl-11{margin-left:91.66666667%}.g-xl-0,.gx-xl-0{--bs-gutter-x:0}.g-xl-0,.gy-xl-0{--bs-gutter-y:0}.g-xl-1,.gx-xl-1{--bs-gutter-x:0.25rem}.g-xl-1,.gy-xl-1{--bs-gutter-y:0.25rem}.g-xl-2,.gx-xl-2{--bs-gutter-x:0.5rem}.g-xl-2,.gy-xl-2{--bs-gutter-y:0.5rem}.g-xl-3,.gx-xl-3{--bs-gutter-x:1rem}.g-xl-3,.gy-xl-3{--bs-gutter-y:1rem}.g-xl-4,.gx-xl-4{--bs-gutter-x:1.5rem}.g-xl-4,.gy-xl-4{--bs-gutter-y:1.5rem}.g-xl-5,.gx-xl-5{--bs-gutter-x:3rem}.g-xl-5,.gy-xl-5{--bs-gutter-y:3rem}}@media (min-width:1400px){.col-xxl{flex:1 0 0}.row-cols-xxl-auto>*{flex:0 0 auto;width:auto}.row-cols-xxl-1>*{flex:0 0 auto;width:100%}.row-cols-xxl-2>*{flex:0 0 auto;width:50%}.row-cols-xxl-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-xxl-4>*{flex:0 0 auto;width:25%}.row-cols-xxl-5>*{flex:0 0 auto;width:20%}.row-cols-xxl-6>*{flex:0 0 auto;width:16.66666667%}.col-xxl-auto{flex:0 0 auto;width:auto}.col-xxl-1{flex:0 0 auto;width:8.33333333%}.col-xxl-2{flex:0 0 auto;width:16.66666667%}.col-xxl-3{flex:0 0 auto;width:25%}.col-xxl-4{flex:0 0 auto;width:33.33333333%}.col-xxl-5{flex:0 0 auto;width:41.66666667%}.col-xxl-6{flex:0 0 auto;width:50%}.col-xxl-7{flex:0 0 auto;width:58.33333333%}.col-xxl-8{flex:0 0 auto;width:66.66666667%}.col-xxl-9{flex:0 0 auto;width:75%}.col-xxl-10{flex:0 0 auto;width:83.33333333%}.col-xxl-11{flex:0 0 auto;width:91.66666667%}.col-xxl-12{flex:0 0 auto;width:100%}.offset-xxl-0{margin-left:0}.offset-xxl-1{margin-left:8.33333333%}.offset-xxl-2{margin-left:16.66666667%}.offset-xxl-3{margin-left:25%}.offset-xxl-4{margin-left:33.33333333%}.offset-xxl-5{margin-left:41.66666667%}.offset-xxl-6{margin-left:50%}.offset-xxl-7{margin-left:58.33333333%}.offset-xxl-8{margin-left:66.66666667%}.offset-xxl-9{margin-left:75%}.offset-xxl-10{margin-left:83.33333333%}.offset-xxl-11{margin-left:91.66666667%}.g-xxl-0,.gx-xxl-0{--bs-gutter-x:0}.g-xxl-0,.gy-xxl-0{--bs-gutter-y:0}.g-xxl-1,.gx-xxl-1{--bs-gutter-x:0.25rem}.g-xxl-1,.gy-xxl-1{--bs-gutter-y:0.25rem}.g-xxl-2,.gx-xxl-2{--bs-gutter-x:0.5rem}.g-xxl-2,.gy-xxl-2{--bs-gutter-y:0.5rem}.g-xxl-3,.gx-xxl-3{--bs-gutter-x:1rem}.g-xxl-3,.gy-xxl-3{--bs-gutter-y:1rem}.g-xxl-4,.gx-xxl-4{--bs-gutter-x:1.5rem}.g-xxl-4,.gy-xxl-4{--bs-gutter-y:1.5rem}.g-xxl-5,.gx-xxl-5{--bs-gutter-x:3rem}.g-xxl-5,.gy-xxl-5{--bs-gutter-y:3rem}}.table{--bs-table-color-type:initial;--bs-table-bg-type:initial;--bs-table-color-state:initial;--bs-table-bg-state:initial;--bs-table-color:var(--bs-emphasis-color);--bs-table-bg:var(--bs-body-bg);--bs-table-border-color:var(--bs-border-color);--bs-table-accent-bg:transparent;--bs-table-striped-color:var(--bs-emphasis-color);--bs-table-striped-bg:rgba(var(--bs-emphasis-color-rgb), 0.05);--bs-table-active-color:var(--bs-emphasis-color);--bs-table-active-bg:rgba(var(--bs-emphasis-color-rgb), 0.1);--bs-table-hover-color:var(--bs-emphasis-color);--bs-table-hover-bg:rgba(var(--bs-emphasis-color-rgb), 0.075);width:100%;margin-bottom:1rem;vertical-align:top;border-color:var(--bs-table-border-color)}.table>:not(caption)>*>*{padding:.5rem .5rem;color:var(--bs-table-color-state,var(--bs-table-color-type,var(--bs-table-color)));background-color:var(--bs-table-bg);border-bottom-width:var(--bs-border-width);box-shadow:inset 0 0 0 9999px var(--bs-table-bg-state,var(--bs-table-bg-type,var(--bs-table-accent-bg)))}.table>tbody{vertical-align:inherit}.table>thead{vertical-align:bottom}.table-group-divider{border-top:calc(var(--bs-border-width) * 2) solid currentcolor}.caption-top{caption-side:top}.table-sm>:not(caption)>*>*{padding:.25rem .25rem}.table-bordered>:not(caption)>*{border-width:var(--bs-border-width) 0}.table-bordered>:not(caption)>*>*{border-width:0 var(--bs-border-width)}.table-borderless>:not(caption)>*>*{border-bottom-width:0}.table-borderless>:not(:first-child){border-top-width:0}.table-striped>tbody>tr:nth-of-type(odd)>*{--bs-table-color-type:var(--bs-table-striped-color);--bs-table-bg-type:var(--bs-table-striped-bg)}.table-striped-columns>:not(caption)>tr>:nth-child(2n){--bs-table-color-type:var(--bs-table-striped-color);--bs-table-bg-type:var(--bs-table-striped-bg)}.table-active{--bs-table-color-state:var(--bs-table-active-color);--bs-table-bg-state:var(--bs-table-active-bg)}.table-hover>tbody>tr:hover>*{--bs-table-color-state:var(--bs-table-hover-color);--bs-table-bg-state:var(--bs-table-hover-bg)}.table-primary{--bs-table-color:#000;--bs-table-bg:#cfe2ff;--bs-table-border-color:#a6b5cc;--bs-table-striped-bg:#c5d7f2;--bs-table-striped-color:#000;--bs-table-active-bg:#bacbe6;--bs-table-active-color:#000;--bs-table-hover-bg:#bfd1ec;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-secondary{--bs-table-color:#000;--bs-table-bg:#e2e3e5;--bs-table-border-color:#b5b6b7;--bs-table-striped-bg:#d7d8da;--bs-table-striped-color:#000;--bs-table-active-bg:#cbccce;--bs-table-active-color:#000;--bs-table-hover-bg:#d1d2d4;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-success{--bs-table-color:#000;--bs-table-bg:#d1e7dd;--bs-table-border-color:#a7b9b1;--bs-table-striped-bg:#c7dbd2;--bs-table-striped-color:#000;--bs-table-active-bg:#bcd0c7;--bs-table-active-color:#000;--bs-table-hover-bg:#c1d6cc;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-info{--bs-table-color:#000;--bs-table-bg:#cff4fc;--bs-table-border-color:#a6c3ca;--bs-table-striped-bg:#c5e8ef;--bs-table-striped-color:#000;--bs-table-active-bg:#badce3;--bs-table-active-color:#000;--bs-table-hover-bg:#bfe2e9;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-warning{--bs-table-color:#000;--bs-table-bg:#fff3cd;--bs-table-border-color:#ccc2a4;--bs-table-striped-bg:#f2e7c3;--bs-table-striped-color:#000;--bs-table-active-bg:#e6dbb9;--bs-table-active-color:#000;--bs-table-hover-bg:#ece1be;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-danger{--bs-table-color:#000;--bs-table-bg:#f8d7da;--bs-table-border-color:#c6acae;--bs-table-striped-bg:#eccccf;--bs-table-striped-color:#000;--bs-table-active-bg:#dfc2c4;--bs-table-active-color:#000;--bs-table-hover-bg:#e5c7ca;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-light{--bs-table-color:#000;--bs-table-bg:#f8f9fa;--bs-table-border-color:#c6c7c8;--bs-table-striped-bg:#ecedee;--bs-table-striped-color:#000;--bs-table-active-bg:#dfe0e1;--bs-table-active-color:#000;--bs-table-hover-bg:#e5e6e7;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-dark{--bs-table-color:#fff;--bs-table-bg:#212529;--bs-table-border-color:#4d5154;--bs-table-striped-bg:#2c3034;--bs-table-striped-color:#fff;--bs-table-active-bg:#373b3e;--bs-table-active-color:#fff;--bs-table-hover-bg:#323539;--bs-table-hover-color:#fff;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}@media (max-width:575.98px){.table-responsive-sm{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:767.98px){.table-responsive-md{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:991.98px){.table-responsive-lg{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1199.98px){.table-responsive-xl{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1399.98px){.table-responsive-xxl{overflow-x:auto;-webkit-overflow-scrolling:touch}}.form-label{margin-bottom:.5rem}.col-form-label{padding-top:calc(.375rem + var(--bs-border-width));padding-bottom:calc(.375rem + var(--bs-border-width));margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + var(--bs-border-width));padding-bottom:calc(.5rem + var(--bs-border-width));font-size:1.25rem}.col-form-label-sm{padding-top:calc(.25rem + var(--bs-border-width));padding-bottom:calc(.25rem + var(--bs-border-width));font-size:.875rem}.form-text{margin-top:.25rem;font-size:.875em;color:var(--bs-secondary-color)}.form-control{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:var(--bs-body-color);-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--bs-body-bg);background-clip:padding-box;border:var(--bs-border-width) solid var(--bs-border-color);border-radius:var(--bs-border-radius);transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control[type=file]{overflow:hidden}.form-control[type=file]:not(:disabled):not([readonly]){cursor:pointer}.form-control:focus{color:var(--bs-body-color);background-color:var(--bs-body-bg);border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-control::-webkit-date-and-time-value{min-width:85px;height:1.5em;margin:0}.form-control::-webkit-datetime-edit{display:block;padding:0}.form-control::placeholder{color:var(--bs-secondary-color);opacity:1}.form-control:disabled{background-color:var(--bs-secondary-bg);opacity:1}.form-control::-webkit-file-upload-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:var(--bs-body-color);background-color:var(--bs-tertiary-bg);pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:var(--bs-border-width);border-radius:0;-webkit-transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}.form-control::file-selector-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:var(--bs-body-color);background-color:var(--bs-tertiary-bg);pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:var(--bs-border-width);border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::-webkit-file-upload-button{-webkit-transition:none;transition:none}.form-control::file-selector-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:var(--bs-secondary-bg)}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:var(--bs-secondary-bg)}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;line-height:1.5;color:var(--bs-body-color);background-color:transparent;border:solid transparent;border-width:var(--bs-border-width) 0}.form-control-plaintext:focus{outline:0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{min-height:calc(1.5em + .5rem + calc(var(--bs-border-width) * 2));padding:.25rem .5rem;font-size:.875rem;border-radius:var(--bs-border-radius-sm)}.form-control-sm::-webkit-file-upload-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-sm::file-selector-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-lg{min-height:calc(1.5em + 1rem + calc(var(--bs-border-width) * 2));padding:.5rem 1rem;font-size:1.25rem;border-radius:var(--bs-border-radius-lg)}.form-control-lg::-webkit-file-upload-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}.form-control-lg::file-selector-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}textarea.form-control{min-height:calc(1.5em + .75rem + calc(var(--bs-border-width) * 2))}textarea.form-control-sm{min-height:calc(1.5em + .5rem + calc(var(--bs-border-width) * 2))}textarea.form-control-lg{min-height:calc(1.5em + 1rem + calc(var(--bs-border-width) * 2))}.form-control-color{width:3rem;height:calc(1.5em + .75rem + calc(var(--bs-border-width) * 2));padding:.375rem}.form-control-color:not(:disabled):not([readonly]){cursor:pointer}.form-control-color::-moz-color-swatch{border:0!important;border-radius:var(--bs-border-radius)}.form-control-color::-webkit-color-swatch{border:0!important;border-radius:var(--bs-border-radius)}.form-control-color.form-control-sm{height:calc(1.5em + .5rem + calc(var(--bs-border-width) * 2))}.form-control-color.form-control-lg{height:calc(1.5em + 1rem + calc(var(--bs-border-width) * 2))}.form-select{--bs-form-select-bg-img:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e");display:block;width:100%;padding:.375rem 2.25rem .375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:var(--bs-body-color);-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--bs-body-bg);background-image:var(--bs-form-select-bg-img),var(--bs-form-select-bg-icon,none);background-repeat:no-repeat;background-position:right .75rem center;background-size:16px 12px;border:var(--bs-border-width) solid var(--bs-border-color);border-radius:var(--bs-border-radius);transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-select{transition:none}}.form-select:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-select[multiple],.form-select[size]:not([size="1"]){padding-right:.75rem;background-image:none}.form-select:disabled{background-color:var(--bs-secondary-bg)}.form-select:-moz-focusring{color:transparent;text-shadow:0 0 0 var(--bs-body-color)}.form-select-sm{padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem;border-radius:var(--bs-border-radius-sm)}.form-select-lg{padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem;border-radius:var(--bs-border-radius-lg)}[data-bs-theme=dark] .form-select{--bs-form-select-bg-img:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23dee2e6' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e")}.form-check{display:block;min-height:1.5rem;padding-left:1.5em;margin-bottom:.125rem}.form-check .form-check-input{float:left;margin-left:-1.5em}.form-check-reverse{padding-right:1.5em;padding-left:0;text-align:right}.form-check-reverse .form-check-input{float:right;margin-right:-1.5em;margin-left:0}.form-check-input{--bs-form-check-bg:var(--bs-body-bg);flex-shrink:0;width:1em;height:1em;margin-top:.25em;vertical-align:top;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--bs-form-check-bg);background-image:var(--bs-form-check-bg-image);background-repeat:no-repeat;background-position:center;background-size:contain;border:var(--bs-border-width) solid var(--bs-border-color);-webkit-print-color-adjust:exact;color-adjust:exact;print-color-adjust:exact}.form-check-input[type=checkbox]{border-radius:.25em}.form-check-input[type=radio]{border-radius:50%}.form-check-input:active{filter:brightness(90%)}.form-check-input:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-check-input:checked{background-color:#0d6efd;border-color:#0d6efd}.form-check-input:checked[type=checkbox]{--bs-form-check-bg-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/%3e%3c/svg%3e")}.form-check-input:checked[type=radio]{--bs-form-check-bg-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e")}.form-check-input[type=checkbox]:indeterminate{background-color:#0d6efd;border-color:#0d6efd;--bs-form-check-bg-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e")}.form-check-input:disabled{pointer-events:none;filter:none;opacity:.5}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{cursor:default;opacity:.5}.form-switch{padding-left:2.5em}.form-switch .form-check-input{--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e");width:2em;margin-left:-2.5em;background-image:var(--bs-form-switch-bg);background-position:left center;border-radius:2em;transition:background-position .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-switch .form-check-input{transition:none}}.form-switch .form-check-input:focus{--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%2386b7fe'/%3e%3c/svg%3e")}.form-switch .form-check-input:checked{background-position:right center;--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.form-switch.form-check-reverse{padding-right:2.5em;padding-left:0}.form-switch.form-check-reverse .form-check-input{margin-right:-2.5em;margin-left:0}.form-check-inline{display:inline-block;margin-right:1rem}.btn-check{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.btn-check:disabled+.btn,.btn-check[disabled]+.btn{pointer-events:none;filter:none;opacity:.65}[data-bs-theme=dark] .form-switch .form-check-input:not(:checked):not(:focus){--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%28255, 255, 255, 0.25%29'/%3e%3c/svg%3e")}.form-range{width:100%;height:1.5rem;padding:0;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:transparent}.form-range:focus{outline:0}.form-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range::-moz-focus-outer{border:0}.form-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;-webkit-appearance:none;appearance:none;background-color:#0d6efd;border:0;border-radius:1rem;-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.form-range::-webkit-slider-thumb:active{background-color:#b6d4fe}.form-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:var(--bs-secondary-bg);border-color:transparent;border-radius:1rem}.form-range::-moz-range-thumb{width:1rem;height:1rem;-moz-appearance:none;appearance:none;background-color:#0d6efd;border:0;border-radius:1rem;-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-range::-moz-range-thumb{-moz-transition:none;transition:none}}.form-range::-moz-range-thumb:active{background-color:#b6d4fe}.form-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:var(--bs-secondary-bg);border-color:transparent;border-radius:1rem}.form-range:disabled{pointer-events:none}.form-range:disabled::-webkit-slider-thumb{background-color:var(--bs-secondary-color)}.form-range:disabled::-moz-range-thumb{background-color:var(--bs-secondary-color)}.form-floating{position:relative}.form-floating>.form-control,.form-floating>.form-control-plaintext,.form-floating>.form-select{height:calc(3.5rem + calc(var(--bs-border-width) * 2));min-height:calc(3.5rem + calc(var(--bs-border-width) * 2));line-height:1.25}.form-floating>label{position:absolute;top:0;left:0;z-index:2;max-width:100%;height:100%;padding:1rem .75rem;overflow:hidden;color:rgba(var(--bs-body-color-rgb),.65);text-align:start;text-overflow:ellipsis;white-space:nowrap;pointer-events:none;border:var(--bs-border-width) solid transparent;transform-origin:0 0;transition:opacity .1s ease-in-out,transform .1s ease-in-out}@media (prefers-reduced-motion:reduce){.form-floating>label{transition:none}}.form-floating>.form-control,.form-floating>.form-control-plaintext{padding:1rem .75rem}.form-floating>.form-control-plaintext::placeholder,.form-floating>.form-control::placeholder{color:transparent}.form-floating>.form-control-plaintext:focus,.form-floating>.form-control-plaintext:not(:placeholder-shown),.form-floating>.form-control:focus,.form-floating>.form-control:not(:placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control-plaintext:-webkit-autofill,.form-floating>.form-control:-webkit-autofill{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-select{padding-top:1.625rem;padding-bottom:.625rem;padding-left:.75rem}.form-floating>.form-control-plaintext~label,.form-floating>.form-control:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>.form-select~label{transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:-webkit-autofill~label{transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>textarea:focus~label::after,.form-floating>textarea:not(:placeholder-shown)~label::after{position:absolute;inset:1rem 0.375rem;z-index:-1;height:1.5em;content:"";background-color:var(--bs-body-bg);border-radius:var(--bs-border-radius)}.form-floating>textarea:disabled~label::after{background-color:var(--bs-secondary-bg)}.form-floating>.form-control-plaintext~label{border-width:var(--bs-border-width) 0}.form-floating>.form-control:disabled~label,.form-floating>:disabled~label{color:#6c757d}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-floating,.input-group>.form-select{position:relative;flex:1 1 auto;width:1%;min-width:0}.input-group>.form-control:focus,.input-group>.form-floating:focus-within,.input-group>.form-select:focus{z-index:5}.input-group .btn{position:relative;z-index:2}.input-group .btn:focus{z-index:5}.input-group-text{display:flex;align-items:center;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:var(--bs-body-color);text-align:center;white-space:nowrap;background-color:var(--bs-tertiary-bg);border:var(--bs-border-width) solid var(--bs-border-color);border-radius:var(--bs-border-radius)}.input-group-lg>.btn,.input-group-lg>.form-control,.input-group-lg>.form-select,.input-group-lg>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;border-radius:var(--bs-border-radius-lg)}.input-group-sm>.btn,.input-group-sm>.form-control,.input-group-sm>.form-select,.input-group-sm>.input-group-text{padding:.25rem .5rem;font-size:.875rem;border-radius:var(--bs-border-radius-sm)}.input-group-lg>.form-select,.input-group-sm>.form-select{padding-right:3rem}.input-group:not(.has-validation)>.dropdown-toggle:nth-last-child(n+3),.input-group:not(.has-validation)>.form-floating:not(:last-child)>.form-control,.input-group:not(.has-validation)>.form-floating:not(:last-child)>.form-select,.input-group:not(.has-validation)>:not(:last-child):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating){border-top-right-radius:0;border-bottom-right-radius:0}.input-group.has-validation>.dropdown-toggle:nth-last-child(n+4),.input-group.has-validation>.form-floating:nth-last-child(n+3)>.form-control,.input-group.has-validation>.form-floating:nth-last-child(n+3)>.form-select,.input-group.has-validation>:nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback){margin-left:calc(-1 * var(--bs-border-width));border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.form-floating:not(:first-child)>.form-control,.input-group>.form-floating:not(:first-child)>.form-select{border-top-left-radius:0;border-bottom-left-radius:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:var(--bs-form-valid-color)}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:var(--bs-success);border-radius:var(--bs-border-radius)}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:var(--bs-form-valid-border-color);padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:var(--bs-form-valid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-success-rgb),.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-valid,.was-validated .form-select:valid{border-color:var(--bs-form-valid-border-color)}.form-select.is-valid:not([multiple]):not([size]),.form-select.is-valid:not([multiple])[size="1"],.was-validated .form-select:valid:not([multiple]):not([size]),.was-validated .form-select:valid:not([multiple])[size="1"]{--bs-form-select-bg-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1'/%3e%3c/svg%3e");padding-right:4.125rem;background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-valid:focus,.was-validated .form-select:valid:focus{border-color:var(--bs-form-valid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-success-rgb),.25)}.form-control-color.is-valid,.was-validated .form-control-color:valid{width:calc(3rem + calc(1.5em + .75rem))}.form-check-input.is-valid,.was-validated .form-check-input:valid{border-color:var(--bs-form-valid-border-color)}.form-check-input.is-valid:checked,.was-validated .form-check-input:valid:checked{background-color:var(--bs-form-valid-color)}.form-check-input.is-valid:focus,.was-validated .form-check-input:valid:focus{box-shadow:0 0 0 .25rem rgba(var(--bs-success-rgb),.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:var(--bs-form-valid-color)}.form-check-inline .form-check-input~.valid-feedback{margin-left:.5em}.input-group>.form-control:not(:focus).is-valid,.input-group>.form-floating:not(:focus-within).is-valid,.input-group>.form-select:not(:focus).is-valid,.was-validated .input-group>.form-control:not(:focus):valid,.was-validated .input-group>.form-floating:not(:focus-within):valid,.was-validated .input-group>.form-select:not(:focus):valid{z-index:3}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:var(--bs-form-invalid-color)}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:var(--bs-danger);border-radius:var(--bs-border-radius)}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:var(--bs-form-invalid-border-color);padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:var(--bs-form-invalid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-danger-rgb),.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-invalid,.was-validated .form-select:invalid{border-color:var(--bs-form-invalid-border-color)}.form-select.is-invalid:not([multiple]):not([size]),.form-select.is-invalid:not([multiple])[size="1"],.was-validated .form-select:invalid:not([multiple]):not([size]),.was-validated .form-select:invalid:not([multiple])[size="1"]{--bs-form-select-bg-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");padding-right:4.125rem;background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-invalid:focus,.was-validated .form-select:invalid:focus{border-color:var(--bs-form-invalid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-danger-rgb),.25)}.form-control-color.is-invalid,.was-validated .form-control-color:invalid{width:calc(3rem + calc(1.5em + .75rem))}.form-check-input.is-invalid,.was-validated .form-check-input:invalid{border-color:var(--bs-form-invalid-border-color)}.form-check-input.is-invalid:checked,.was-validated .form-check-input:invalid:checked{background-color:var(--bs-form-invalid-color)}.form-check-input.is-invalid:focus,.was-validated .form-check-input:invalid:focus{box-shadow:0 0 0 .25rem rgba(var(--bs-danger-rgb),.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:var(--bs-form-invalid-color)}.form-check-inline .form-check-input~.invalid-feedback{margin-left:.5em}.input-group>.form-control:not(:focus).is-invalid,.input-group>.form-floating:not(:focus-within).is-invalid,.input-group>.form-select:not(:focus).is-invalid,.was-validated .input-group>.form-control:not(:focus):invalid,.was-validated .input-group>.form-floating:not(:focus-within):invalid,.was-validated .input-group>.form-select:not(:focus):invalid{z-index:4}.btn{--bs-btn-padding-x:0.75rem;--bs-btn-padding-y:0.375rem;--bs-btn-font-family: ;--bs-btn-font-size:1rem;--bs-btn-font-weight:400;--bs-btn-line-height:1.5;--bs-btn-color:var(--bs-body-color);--bs-btn-bg:transparent;--bs-btn-border-width:var(--bs-border-width);--bs-btn-border-color:transparent;--bs-btn-border-radius:var(--bs-border-radius);--bs-btn-hover-border-color:transparent;--bs-btn-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.15),0 1px 1px rgba(0, 0, 0, 0.075);--bs-btn-disabled-opacity:0.65;--bs-btn-focus-box-shadow:0 0 0 0.25rem rgba(var(--bs-btn-focus-shadow-rgb), .5);display:inline-block;padding:var(--bs-btn-padding-y) var(--bs-btn-padding-x);font-family:var(--bs-btn-font-family);font-size:var(--bs-btn-font-size);font-weight:var(--bs-btn-font-weight);line-height:var(--bs-btn-line-height);color:var(--bs-btn-color);text-align:center;text-decoration:none;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;border:var(--bs-btn-border-width) solid var(--bs-btn-border-color);border-radius:var(--bs-btn-border-radius);background-color:var(--bs-btn-bg);transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color)}.btn-check+.btn:hover{color:var(--bs-btn-color);background-color:var(--bs-btn-bg);border-color:var(--bs-btn-border-color)}.btn:focus-visible{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:focus-visible+.btn{border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:checked+.btn,.btn.active,.btn.show,.btn:first-child:active,:not(.btn-check)+.btn:active{color:var(--bs-btn-active-color);background-color:var(--bs-btn-active-bg);border-color:var(--bs-btn-active-border-color)}.btn-check:checked+.btn:focus-visible,.btn.active:focus-visible,.btn.show:focus-visible,.btn:first-child:active:focus-visible,:not(.btn-check)+.btn:active:focus-visible{box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:checked:focus-visible+.btn{box-shadow:var(--bs-btn-focus-box-shadow)}.btn.disabled,.btn:disabled,fieldset:disabled .btn{color:var(--bs-btn-disabled-color);pointer-events:none;background-color:var(--bs-btn-disabled-bg);border-color:var(--bs-btn-disabled-border-color);opacity:var(--bs-btn-disabled-opacity)}.btn-primary{--bs-btn-color:#fff;--bs-btn-bg:#0d6efd;--bs-btn-border-color:#0d6efd;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#0b5ed7;--bs-btn-hover-border-color:#0a58ca;--bs-btn-focus-shadow-rgb:49,132,253;--bs-btn-active-color:#fff;--bs-btn-active-bg:#0a58ca;--bs-btn-active-border-color:#0a53be;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#0d6efd;--bs-btn-disabled-border-color:#0d6efd}.btn-secondary{--bs-btn-color:#fff;--bs-btn-bg:#6c757d;--bs-btn-border-color:#6c757d;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#5c636a;--bs-btn-hover-border-color:#565e64;--bs-btn-focus-shadow-rgb:130,138,145;--bs-btn-active-color:#fff;--bs-btn-active-bg:#565e64;--bs-btn-active-border-color:#51585e;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#6c757d;--bs-btn-disabled-border-color:#6c757d}.btn-success{--bs-btn-color:#fff;--bs-btn-bg:#198754;--bs-btn-border-color:#198754;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#157347;--bs-btn-hover-border-color:#146c43;--bs-btn-focus-shadow-rgb:60,153,110;--bs-btn-active-color:#fff;--bs-btn-active-bg:#146c43;--bs-btn-active-border-color:#13653f;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#198754;--bs-btn-disabled-border-color:#198754}.btn-info{--bs-btn-color:#000;--bs-btn-bg:#0dcaf0;--bs-btn-border-color:#0dcaf0;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#31d2f2;--bs-btn-hover-border-color:#25cff2;--bs-btn-focus-shadow-rgb:11,172,204;--bs-btn-active-color:#000;--bs-btn-active-bg:#3dd5f3;--bs-btn-active-border-color:#25cff2;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#000;--bs-btn-disabled-bg:#0dcaf0;--bs-btn-disabled-border-color:#0dcaf0}.btn-warning{--bs-btn-color:#000;--bs-btn-bg:#ffc107;--bs-btn-border-color:#ffc107;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#ffca2c;--bs-btn-hover-border-color:#ffc720;--bs-btn-focus-shadow-rgb:217,164,6;--bs-btn-active-color:#000;--bs-btn-active-bg:#ffcd39;--bs-btn-active-border-color:#ffc720;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#000;--bs-btn-disabled-bg:#ffc107;--bs-btn-disabled-border-color:#ffc107}.btn-danger{--bs-btn-color:#fff;--bs-btn-bg:#dc3545;--bs-btn-border-color:#dc3545;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#bb2d3b;--bs-btn-hover-border-color:#b02a37;--bs-btn-focus-shadow-rgb:225,83,97;--bs-btn-active-color:#fff;--bs-btn-active-bg:#b02a37;--bs-btn-active-border-color:#a52834;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#dc3545;--bs-btn-disabled-border-color:#dc3545}.btn-light{--bs-btn-color:#000;--bs-btn-bg:#f8f9fa;--bs-btn-border-color:#f8f9fa;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#d3d4d5;--bs-btn-hover-border-color:#c6c7c8;--bs-btn-focus-shadow-rgb:211,212,213;--bs-btn-active-color:#000;--bs-btn-active-bg:#c6c7c8;--bs-btn-active-border-color:#babbbc;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#000;--bs-btn-disabled-bg:#f8f9fa;--bs-btn-disabled-border-color:#f8f9fa}.btn-dark{--bs-btn-color:#fff;--bs-btn-bg:#212529;--bs-btn-border-color:#212529;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#424649;--bs-btn-hover-border-color:#373b3e;--bs-btn-focus-shadow-rgb:66,70,73;--bs-btn-active-color:#fff;--bs-btn-active-bg:#4d5154;--bs-btn-active-border-color:#373b3e;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#212529;--bs-btn-disabled-border-color:#212529}.btn-outline-primary{--bs-btn-color:#0d6efd;--bs-btn-border-color:#0d6efd;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#0d6efd;--bs-btn-hover-border-color:#0d6efd;--bs-btn-focus-shadow-rgb:13,110,253;--bs-btn-active-color:#fff;--bs-btn-active-bg:#0d6efd;--bs-btn-active-border-color:#0d6efd;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#0d6efd;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#0d6efd;--bs-gradient:none}.btn-outline-secondary{--bs-btn-color:#6c757d;--bs-btn-border-color:#6c757d;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#6c757d;--bs-btn-hover-border-color:#6c757d;--bs-btn-focus-shadow-rgb:108,117,125;--bs-btn-active-color:#fff;--bs-btn-active-bg:#6c757d;--bs-btn-active-border-color:#6c757d;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#6c757d;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#6c757d;--bs-gradient:none}.btn-outline-success{--bs-btn-color:#198754;--bs-btn-border-color:#198754;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#198754;--bs-btn-hover-border-color:#198754;--bs-btn-focus-shadow-rgb:25,135,84;--bs-btn-active-color:#fff;--bs-btn-active-bg:#198754;--bs-btn-active-border-color:#198754;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#198754;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#198754;--bs-gradient:none}.btn-outline-info{--bs-btn-color:#0dcaf0;--bs-btn-border-color:#0dcaf0;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#0dcaf0;--bs-btn-hover-border-color:#0dcaf0;--bs-btn-focus-shadow-rgb:13,202,240;--bs-btn-active-color:#000;--bs-btn-active-bg:#0dcaf0;--bs-btn-active-border-color:#0dcaf0;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#0dcaf0;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#0dcaf0;--bs-gradient:none}.btn-outline-warning{--bs-btn-color:#ffc107;--bs-btn-border-color:#ffc107;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#ffc107;--bs-btn-hover-border-color:#ffc107;--bs-btn-focus-shadow-rgb:255,193,7;--bs-btn-active-color:#000;--bs-btn-active-bg:#ffc107;--bs-btn-active-border-color:#ffc107;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#ffc107;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#ffc107;--bs-gradient:none}.btn-outline-danger{--bs-btn-color:#dc3545;--bs-btn-border-color:#dc3545;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#dc3545;--bs-btn-hover-border-color:#dc3545;--bs-btn-focus-shadow-rgb:220,53,69;--bs-btn-active-color:#fff;--bs-btn-active-bg:#dc3545;--bs-btn-active-border-color:#dc3545;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#dc3545;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#dc3545;--bs-gradient:none}.btn-outline-light{--bs-btn-color:#f8f9fa;--bs-btn-border-color:#f8f9fa;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#f8f9fa;--bs-btn-hover-border-color:#f8f9fa;--bs-btn-focus-shadow-rgb:248,249,250;--bs-btn-active-color:#000;--bs-btn-active-bg:#f8f9fa;--bs-btn-active-border-color:#f8f9fa;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#f8f9fa;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#f8f9fa;--bs-gradient:none}.btn-outline-dark{--bs-btn-color:#212529;--bs-btn-border-color:#212529;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#212529;--bs-btn-hover-border-color:#212529;--bs-btn-focus-shadow-rgb:33,37,41;--bs-btn-active-color:#fff;--bs-btn-active-bg:#212529;--bs-btn-active-border-color:#212529;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#212529;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#212529;--bs-gradient:none}.btn-link{--bs-btn-font-weight:400;--bs-btn-color:var(--bs-link-color);--bs-btn-bg:transparent;--bs-btn-border-color:transparent;--bs-btn-hover-color:var(--bs-link-hover-color);--bs-btn-hover-border-color:transparent;--bs-btn-active-color:var(--bs-link-hover-color);--bs-btn-active-border-color:transparent;--bs-btn-disabled-color:#6c757d;--bs-btn-disabled-border-color:transparent;--bs-btn-box-shadow:0 0 0 #000;--bs-btn-focus-shadow-rgb:49,132,253;text-decoration:underline}.btn-link:focus-visible{color:var(--bs-btn-color)}.btn-link:hover{color:var(--bs-btn-hover-color)}.btn-group-lg>.btn,.btn-lg{--bs-btn-padding-y:0.5rem;--bs-btn-padding-x:1rem;--bs-btn-font-size:1.25rem;--bs-btn-border-radius:var(--bs-border-radius-lg)}.btn-group-sm>.btn,.btn-sm{--bs-btn-padding-y:0.25rem;--bs-btn-padding-x:0.5rem;--bs-btn-font-size:0.875rem;--bs-btn-border-radius:var(--bs-border-radius-sm)}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.collapsing.collapse-horizontal{width:0;height:auto;transition:width .35s ease}@media (prefers-reduced-motion:reduce){.collapsing.collapse-horizontal{transition:none}}.dropdown,.dropdown-center,.dropend,.dropstart,.dropup,.dropup-center{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{--bs-dropdown-zindex:1000;--bs-dropdown-min-width:10rem;--bs-dropdown-padding-x:0;--bs-dropdown-padding-y:0.5rem;--bs-dropdown-spacer:0.125rem;--bs-dropdown-font-size:1rem;--bs-dropdown-color:var(--bs-body-color);--bs-dropdown-bg:var(--bs-body-bg);--bs-dropdown-border-color:var(--bs-border-color-translucent);--bs-dropdown-border-radius:var(--bs-border-radius);--bs-dropdown-border-width:var(--bs-border-width);--bs-dropdown-inner-border-radius:calc(var(--bs-border-radius) - var(--bs-border-width));--bs-dropdown-divider-bg:var(--bs-border-color-translucent);--bs-dropdown-divider-margin-y:0.5rem;--bs-dropdown-box-shadow:var(--bs-box-shadow);--bs-dropdown-link-color:var(--bs-body-color);--bs-dropdown-link-hover-color:var(--bs-body-color);--bs-dropdown-link-hover-bg:var(--bs-tertiary-bg);--bs-dropdown-link-active-color:#fff;--bs-dropdown-link-active-bg:#0d6efd;--bs-dropdown-link-disabled-color:var(--bs-tertiary-color);--bs-dropdown-item-padding-x:1rem;--bs-dropdown-item-padding-y:0.25rem;--bs-dropdown-header-color:#6c757d;--bs-dropdown-header-padding-x:1rem;--bs-dropdown-header-padding-y:0.5rem;position:absolute;z-index:var(--bs-dropdown-zindex);display:none;min-width:var(--bs-dropdown-min-width);padding:var(--bs-dropdown-padding-y) var(--bs-dropdown-padding-x);margin:0;font-size:var(--bs-dropdown-font-size);color:var(--bs-dropdown-color);text-align:left;list-style:none;background-color:var(--bs-dropdown-bg);background-clip:padding-box;border:var(--bs-dropdown-border-width) solid var(--bs-dropdown-border-color);border-radius:var(--bs-dropdown-border-radius)}.dropdown-menu[data-bs-popper]{top:100%;left:0;margin-top:var(--bs-dropdown-spacer)}.dropdown-menu-start{--bs-position:start}.dropdown-menu-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-end{--bs-position:end}.dropdown-menu-end[data-bs-popper]{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-start{--bs-position:start}.dropdown-menu-sm-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-sm-end{--bs-position:end}.dropdown-menu-sm-end[data-bs-popper]{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-start{--bs-position:start}.dropdown-menu-md-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-md-end{--bs-position:end}.dropdown-menu-md-end[data-bs-popper]{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-start{--bs-position:start}.dropdown-menu-lg-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-lg-end{--bs-position:end}.dropdown-menu-lg-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-start{--bs-position:start}.dropdown-menu-xl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xl-end{--bs-position:end}.dropdown-menu-xl-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1400px){.dropdown-menu-xxl-start{--bs-position:start}.dropdown-menu-xxl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xxl-end{--bs-position:end}.dropdown-menu-xxl-end[data-bs-popper]{right:0;left:auto}}.dropup .dropdown-menu[data-bs-popper]{top:auto;bottom:100%;margin-top:0;margin-bottom:var(--bs-dropdown-spacer)}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-menu[data-bs-popper]{top:0;right:auto;left:100%;margin-top:0;margin-left:var(--bs-dropdown-spacer)}.dropend .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropend .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-toggle::after{vertical-align:0}.dropstart .dropdown-menu[data-bs-popper]{top:0;right:100%;left:auto;margin-top:0;margin-right:var(--bs-dropdown-spacer)}.dropstart .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropstart .dropdown-toggle::after{display:none}.dropstart .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropstart .dropdown-toggle:empty::after{margin-left:0}.dropstart .dropdown-toggle::before{vertical-align:0}.dropdown-divider{height:0;margin:var(--bs-dropdown-divider-margin-y) 0;overflow:hidden;border-top:1px solid var(--bs-dropdown-divider-bg);opacity:1}.dropdown-item{display:block;width:100%;padding:var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);clear:both;font-weight:400;color:var(--bs-dropdown-link-color);text-align:inherit;text-decoration:none;white-space:nowrap;background-color:transparent;border:0;border-radius:var(--bs-dropdown-item-border-radius,0)}.dropdown-item:focus,.dropdown-item:hover{color:var(--bs-dropdown-link-hover-color);background-color:var(--bs-dropdown-link-hover-bg)}.dropdown-item.active,.dropdown-item:active{color:var(--bs-dropdown-link-active-color);text-decoration:none;background-color:var(--bs-dropdown-link-active-bg)}.dropdown-item.disabled,.dropdown-item:disabled{color:var(--bs-dropdown-link-disabled-color);pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:var(--bs-dropdown-header-padding-y) var(--bs-dropdown-header-padding-x);margin-bottom:0;font-size:.875rem;color:var(--bs-dropdown-header-color);white-space:nowrap}.dropdown-item-text{display:block;padding:var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);color:var(--bs-dropdown-link-color)}.dropdown-menu-dark{--bs-dropdown-color:#dee2e6;--bs-dropdown-bg:#343a40;--bs-dropdown-border-color:var(--bs-border-color-translucent);--bs-dropdown-box-shadow: ;--bs-dropdown-link-color:#dee2e6;--bs-dropdown-link-hover-color:#fff;--bs-dropdown-divider-bg:var(--bs-border-color-translucent);--bs-dropdown-link-hover-bg:rgba(255, 255, 255, 0.15);--bs-dropdown-link-active-color:#fff;--bs-dropdown-link-active-bg:#0d6efd;--bs-dropdown-link-disabled-color:#adb5bd;--bs-dropdown-header-color:#adb5bd}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;flex:1 1 auto}.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn-check:checked+.btn,.btn-group>.btn-check:focus+.btn,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:1}.btn-toolbar{display:flex;flex-wrap:wrap;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group{border-radius:var(--bs-border-radius)}.btn-group>.btn-group:not(:first-child),.btn-group>:not(.btn-check:first-child)+.btn{margin-left:calc(-1 * var(--bs-border-width))}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn.dropdown-toggle-split:first-child,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:nth-child(n+3),.btn-group>:not(.btn-check)+.btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropend .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropstart .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{flex-direction:column;align-items:flex-start;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:calc(-1 * var(--bs-border-width))}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn:nth-child(n+3),.btn-group-vertical>:not(.btn-check)+.btn{border-top-left-radius:0;border-top-right-radius:0}.nav{--bs-nav-link-padding-x:1rem;--bs-nav-link-padding-y:0.5rem;--bs-nav-link-font-weight: ;--bs-nav-link-color:var(--bs-link-color);--bs-nav-link-hover-color:var(--bs-link-hover-color);--bs-nav-link-disabled-color:var(--bs-secondary-color);display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:var(--bs-nav-link-padding-y) var(--bs-nav-link-padding-x);font-size:var(--bs-nav-link-font-size);font-weight:var(--bs-nav-link-font-weight);color:var(--bs-nav-link-color);text-decoration:none;background:0 0;border:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out}@media (prefers-reduced-motion:reduce){.nav-link{transition:none}}.nav-link:focus,.nav-link:hover{color:var(--bs-nav-link-hover-color)}.nav-link:focus-visible{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.nav-link.disabled,.nav-link:disabled{color:var(--bs-nav-link-disabled-color);pointer-events:none;cursor:default}.nav-tabs{--bs-nav-tabs-border-width:var(--bs-border-width);--bs-nav-tabs-border-color:var(--bs-border-color);--bs-nav-tabs-border-radius:var(--bs-border-radius);--bs-nav-tabs-link-hover-border-color:var(--bs-secondary-bg) var(--bs-secondary-bg) var(--bs-border-color);--bs-nav-tabs-link-active-color:var(--bs-emphasis-color);--bs-nav-tabs-link-active-bg:var(--bs-body-bg);--bs-nav-tabs-link-active-border-color:var(--bs-border-color) var(--bs-border-color) var(--bs-body-bg);border-bottom:var(--bs-nav-tabs-border-width) solid var(--bs-nav-tabs-border-color)}.nav-tabs .nav-link{margin-bottom:calc(-1 * var(--bs-nav-tabs-border-width));border:var(--bs-nav-tabs-border-width) solid transparent;border-top-left-radius:var(--bs-nav-tabs-border-radius);border-top-right-radius:var(--bs-nav-tabs-border-radius)}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{isolation:isolate;border-color:var(--bs-nav-tabs-link-hover-border-color)}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:var(--bs-nav-tabs-link-active-color);background-color:var(--bs-nav-tabs-link-active-bg);border-color:var(--bs-nav-tabs-link-active-border-color)}.nav-tabs .dropdown-menu{margin-top:calc(-1 * var(--bs-nav-tabs-border-width));border-top-left-radius:0;border-top-right-radius:0}.nav-pills{--bs-nav-pills-border-radius:var(--bs-border-radius);--bs-nav-pills-link-active-color:#fff;--bs-nav-pills-link-active-bg:#0d6efd}.nav-pills .nav-link{border-radius:var(--bs-nav-pills-border-radius)}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:var(--bs-nav-pills-link-active-color);background-color:var(--bs-nav-pills-link-active-bg)}.nav-underline{--bs-nav-underline-gap:1rem;--bs-nav-underline-border-width:0.125rem;--bs-nav-underline-link-active-color:var(--bs-emphasis-color);gap:var(--bs-nav-underline-gap)}.nav-underline .nav-link{padding-right:0;padding-left:0;border-bottom:var(--bs-nav-underline-border-width) solid transparent}.nav-underline .nav-link:focus,.nav-underline .nav-link:hover{border-bottom-color:currentcolor}.nav-underline .nav-link.active,.nav-underline .show>.nav-link{font-weight:700;color:var(--bs-nav-underline-link-active-color);border-bottom-color:currentcolor}.nav-fill .nav-item,.nav-fill>.nav-link{flex:1 1 auto;text-align:center}.nav-justified .nav-item,.nav-justified>.nav-link{flex-grow:1;flex-basis:0;text-align:center}.nav-fill .nav-item .nav-link,.nav-justified .nav-item .nav-link{width:100%}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{--bs-navbar-padding-x:0;--bs-navbar-padding-y:0.5rem;--bs-navbar-color:rgba(var(--bs-emphasis-color-rgb), 0.65);--bs-navbar-hover-color:rgba(var(--bs-emphasis-color-rgb), 0.8);--bs-navbar-disabled-color:rgba(var(--bs-emphasis-color-rgb), 0.3);--bs-navbar-active-color:rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-brand-padding-y:0.3125rem;--bs-navbar-brand-margin-end:1rem;--bs-navbar-brand-font-size:1.25rem;--bs-navbar-brand-color:rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-brand-hover-color:rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-nav-link-padding-x:0.5rem;--bs-navbar-toggler-padding-y:0.25rem;--bs-navbar-toggler-padding-x:0.75rem;--bs-navbar-toggler-font-size:1.25rem;--bs-navbar-toggler-icon-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%2833, 37, 41, 0.75%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");--bs-navbar-toggler-border-color:rgba(var(--bs-emphasis-color-rgb), 0.15);--bs-navbar-toggler-border-radius:var(--bs-border-radius);--bs-navbar-toggler-focus-width:0.25rem;--bs-navbar-toggler-transition:box-shadow 0.15s ease-in-out;position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding:var(--bs-navbar-padding-y) var(--bs-navbar-padding-x)}.navbar>.container,.navbar>.container-fluid,.navbar>.container-lg,.navbar>.container-md,.navbar>.container-sm,.navbar>.container-xl,.navbar>.container-xxl{display:flex;flex-wrap:inherit;align-items:center;justify-content:space-between}.navbar-brand{padding-top:var(--bs-navbar-brand-padding-y);padding-bottom:var(--bs-navbar-brand-padding-y);margin-right:var(--bs-navbar-brand-margin-end);font-size:var(--bs-navbar-brand-font-size);color:var(--bs-navbar-brand-color);text-decoration:none;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{color:var(--bs-navbar-brand-hover-color)}.navbar-nav{--bs-nav-link-padding-x:0;--bs-nav-link-padding-y:0.5rem;--bs-nav-link-font-weight: ;--bs-nav-link-color:var(--bs-navbar-color);--bs-nav-link-hover-color:var(--bs-navbar-hover-color);--bs-nav-link-disabled-color:var(--bs-navbar-disabled-color);display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link.active,.navbar-nav .nav-link.show{color:var(--bs-navbar-active-color)}.navbar-nav .dropdown-menu{position:static}.navbar-text{padding-top:.5rem;padding-bottom:.5rem;color:var(--bs-navbar-color)}.navbar-text a,.navbar-text a:focus,.navbar-text a:hover{color:var(--bs-navbar-active-color)}.navbar-collapse{flex-grow:1;flex-basis:100%;align-items:center}.navbar-toggler{padding:var(--bs-navbar-toggler-padding-y) var(--bs-navbar-toggler-padding-x);font-size:var(--bs-navbar-toggler-font-size);line-height:1;color:var(--bs-navbar-color);background-color:transparent;border:var(--bs-border-width) solid var(--bs-navbar-toggler-border-color);border-radius:var(--bs-navbar-toggler-border-radius);transition:var(--bs-navbar-toggler-transition)}@media (prefers-reduced-motion:reduce){.navbar-toggler{transition:none}}.navbar-toggler:hover{text-decoration:none}.navbar-toggler:focus{text-decoration:none;outline:0;box-shadow:0 0 0 var(--bs-navbar-toggler-focus-width)}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;background-image:var(--bs-navbar-toggler-icon-bg);background-repeat:no-repeat;background-position:center;background-size:100%}.navbar-nav-scroll{max-height:var(--bs-scroll-height,75vh);overflow-y:auto}@media (min-width:576px){.navbar-expand-sm{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}.navbar-expand-sm .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-sm .offcanvas .offcanvas-header{display:none}.navbar-expand-sm .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:768px){.navbar-expand-md{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}.navbar-expand-md .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-md .offcanvas .offcanvas-header{display:none}.navbar-expand-md .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:992px){.navbar-expand-lg{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}.navbar-expand-lg .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-lg .offcanvas .offcanvas-header{display:none}.navbar-expand-lg .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1200px){.navbar-expand-xl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}.navbar-expand-xl .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-xl .offcanvas .offcanvas-header{display:none}.navbar-expand-xl .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1400px){.navbar-expand-xxl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xxl .navbar-nav{flex-direction:row}.navbar-expand-xxl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xxl .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-xxl .navbar-nav-scroll{overflow:visible}.navbar-expand-xxl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xxl .navbar-toggler{display:none}.navbar-expand-xxl .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-xxl .offcanvas .offcanvas-header{display:none}.navbar-expand-xxl .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}.navbar-expand{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand .navbar-nav{flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-expand .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand .offcanvas .offcanvas-header{display:none}.navbar-expand .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}.navbar-dark,.navbar[data-bs-theme=dark]{--bs-navbar-color:rgba(255, 255, 255, 0.55);--bs-navbar-hover-color:rgba(255, 255, 255, 0.75);--bs-navbar-disabled-color:rgba(255, 255, 255, 0.25);--bs-navbar-active-color:#fff;--bs-navbar-brand-color:#fff;--bs-navbar-brand-hover-color:#fff;--bs-navbar-toggler-border-color:rgba(255, 255, 255, 0.1);--bs-navbar-toggler-icon-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}[data-bs-theme=dark] .navbar-toggler-icon{--bs-navbar-toggler-icon-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.card{--bs-card-spacer-y:1rem;--bs-card-spacer-x:1rem;--bs-card-title-spacer-y:0.5rem;--bs-card-title-color: ;--bs-card-subtitle-color: ;--bs-card-border-width:var(--bs-border-width);--bs-card-border-color:var(--bs-border-color-translucent);--bs-card-border-radius:var(--bs-border-radius);--bs-card-box-shadow: ;--bs-card-inner-border-radius:calc(var(--bs-border-radius) - (var(--bs-border-width)));--bs-card-cap-padding-y:0.5rem;--bs-card-cap-padding-x:1rem;--bs-card-cap-bg:rgba(var(--bs-body-color-rgb), 0.03);--bs-card-cap-color: ;--bs-card-height: ;--bs-card-color: ;--bs-card-bg:var(--bs-body-bg);--bs-card-img-overlay-padding:1rem;--bs-card-group-margin:0.75rem;position:relative;display:flex;flex-direction:column;min-width:0;height:var(--bs-card-height);color:var(--bs-body-color);word-wrap:break-word;background-color:var(--bs-card-bg);background-clip:border-box;border:var(--bs-card-border-width) solid var(--bs-card-border-color);border-radius:var(--bs-card-border-radius)}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:var(--bs-card-inner-border-radius);border-top-right-radius:var(--bs-card-inner-border-radius)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:var(--bs-card-inner-border-radius);border-bottom-left-radius:var(--bs-card-inner-border-radius)}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{flex:1 1 auto;padding:var(--bs-card-spacer-y) var(--bs-card-spacer-x);color:var(--bs-card-color)}.card-title{margin-bottom:var(--bs-card-title-spacer-y);color:var(--bs-card-title-color)}.card-subtitle{margin-top:calc(-.5 * var(--bs-card-title-spacer-y));margin-bottom:0;color:var(--bs-card-subtitle-color)}.card-text:last-child{margin-bottom:0}.card-link+.card-link{margin-left:var(--bs-card-spacer-x)}.card-header{padding:var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);margin-bottom:0;color:var(--bs-card-cap-color);background-color:var(--bs-card-cap-bg);border-bottom:var(--bs-card-border-width) solid var(--bs-card-border-color)}.card-header:first-child{border-radius:var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius) 0 0}.card-footer{padding:var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);color:var(--bs-card-cap-color);background-color:var(--bs-card-cap-bg);border-top:var(--bs-card-border-width) solid var(--bs-card-border-color)}.card-footer:last-child{border-radius:0 0 var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius)}.card-header-tabs{margin-right:calc(-.5 * var(--bs-card-cap-padding-x));margin-bottom:calc(-1 * var(--bs-card-cap-padding-y));margin-left:calc(-.5 * var(--bs-card-cap-padding-x));border-bottom:0}.card-header-tabs .nav-link.active{background-color:var(--bs-card-bg);border-bottom-color:var(--bs-card-bg)}.card-header-pills{margin-right:calc(-.5 * var(--bs-card-cap-padding-x));margin-left:calc(-.5 * var(--bs-card-cap-padding-x))}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:var(--bs-card-img-overlay-padding);border-radius:var(--bs-card-inner-border-radius)}.card-img,.card-img-bottom,.card-img-top{width:100%}.card-img,.card-img-top{border-top-left-radius:var(--bs-card-inner-border-radius);border-top-right-radius:var(--bs-card-inner-border-radius)}.card-img,.card-img-bottom{border-bottom-right-radius:var(--bs-card-inner-border-radius);border-bottom-left-radius:var(--bs-card-inner-border-radius)}.card-group>.card{margin-bottom:var(--bs-card-group-margin)}@media (min-width:576px){.card-group{display:flex;flex-flow:row wrap}.card-group>.card{flex:1 0 0;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child)>.card-header,.card-group>.card:not(:last-child)>.card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child)>.card-footer,.card-group>.card:not(:last-child)>.card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child)>.card-header,.card-group>.card:not(:first-child)>.card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child)>.card-footer,.card-group>.card:not(:first-child)>.card-img-bottom{border-bottom-left-radius:0}}.accordion{--bs-accordion-color:var(--bs-body-color);--bs-accordion-bg:var(--bs-body-bg);--bs-accordion-transition:color 0.15s ease-in-out,background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out,border-radius 0.15s ease;--bs-accordion-border-color:var(--bs-border-color);--bs-accordion-border-width:var(--bs-border-width);--bs-accordion-border-radius:var(--bs-border-radius);--bs-accordion-inner-border-radius:calc(var(--bs-border-radius) - (var(--bs-border-width)));--bs-accordion-btn-padding-x:1.25rem;--bs-accordion-btn-padding-y:1rem;--bs-accordion-btn-color:var(--bs-body-color);--bs-accordion-btn-bg:var(--bs-accordion-bg);--bs-accordion-btn-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23212529' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='m2 5 6 6 6-6'/%3e%3c/svg%3e");--bs-accordion-btn-icon-width:1.25rem;--bs-accordion-btn-icon-transform:rotate(-180deg);--bs-accordion-btn-icon-transition:transform 0.2s ease-in-out;--bs-accordion-btn-active-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23052c65' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='m2 5 6 6 6-6'/%3e%3c/svg%3e");--bs-accordion-btn-focus-box-shadow:0 0 0 0.25rem rgba(13, 110, 253, 0.25);--bs-accordion-body-padding-x:1.25rem;--bs-accordion-body-padding-y:1rem;--bs-accordion-active-color:var(--bs-primary-text-emphasis);--bs-accordion-active-bg:var(--bs-primary-bg-subtle)}.accordion-button{position:relative;display:flex;align-items:center;width:100%;padding:var(--bs-accordion-btn-padding-y) var(--bs-accordion-btn-padding-x);font-size:1rem;color:var(--bs-accordion-btn-color);text-align:left;background-color:var(--bs-accordion-btn-bg);border:0;border-radius:0;overflow-anchor:none;transition:var(--bs-accordion-transition)}@media (prefers-reduced-motion:reduce){.accordion-button{transition:none}}.accordion-button:not(.collapsed){color:var(--bs-accordion-active-color);background-color:var(--bs-accordion-active-bg);box-shadow:inset 0 calc(-1 * var(--bs-accordion-border-width)) 0 var(--bs-accordion-border-color)}.accordion-button:not(.collapsed)::after{background-image:var(--bs-accordion-btn-active-icon);transform:var(--bs-accordion-btn-icon-transform)}.accordion-button::after{flex-shrink:0;width:var(--bs-accordion-btn-icon-width);height:var(--bs-accordion-btn-icon-width);margin-left:auto;content:"";background-image:var(--bs-accordion-btn-icon);background-repeat:no-repeat;background-size:var(--bs-accordion-btn-icon-width);transition:var(--bs-accordion-btn-icon-transition)}@media (prefers-reduced-motion:reduce){.accordion-button::after{transition:none}}.accordion-button:hover{z-index:2}.accordion-button:focus{z-index:3;outline:0;box-shadow:var(--bs-accordion-btn-focus-box-shadow)}.accordion-header{margin-bottom:0}.accordion-item{color:var(--bs-accordion-color);background-color:var(--bs-accordion-bg);border:var(--bs-accordion-border-width) solid var(--bs-accordion-border-color)}.accordion-item:first-of-type{border-top-left-radius:var(--bs-accordion-border-radius);border-top-right-radius:var(--bs-accordion-border-radius)}.accordion-item:first-of-type>.accordion-header .accordion-button{border-top-left-radius:var(--bs-accordion-inner-border-radius);border-top-right-radius:var(--bs-accordion-inner-border-radius)}.accordion-item:not(:first-of-type){border-top:0}.accordion-item:last-of-type{border-bottom-right-radius:var(--bs-accordion-border-radius);border-bottom-left-radius:var(--bs-accordion-border-radius)}.accordion-item:last-of-type>.accordion-header .accordion-button.collapsed{border-bottom-right-radius:var(--bs-accordion-inner-border-radius);border-bottom-left-radius:var(--bs-accordion-inner-border-radius)}.accordion-item:last-of-type>.accordion-collapse{border-bottom-right-radius:var(--bs-accordion-border-radius);border-bottom-left-radius:var(--bs-accordion-border-radius)}.accordion-body{padding:var(--bs-accordion-body-padding-y) var(--bs-accordion-body-padding-x)}.accordion-flush>.accordion-item{border-right:0;border-left:0;border-radius:0}.accordion-flush>.accordion-item:first-child{border-top:0}.accordion-flush>.accordion-item:last-child{border-bottom:0}.accordion-flush>.accordion-item>.accordion-collapse,.accordion-flush>.accordion-item>.accordion-header .accordion-button,.accordion-flush>.accordion-item>.accordion-header .accordion-button.collapsed{border-radius:0}[data-bs-theme=dark] .accordion-button::after{--bs-accordion-btn-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%236ea8fe'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708'/%3e%3c/svg%3e");--bs-accordion-btn-active-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%236ea8fe'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708'/%3e%3c/svg%3e")}.breadcrumb{--bs-breadcrumb-padding-x:0;--bs-breadcrumb-padding-y:0;--bs-breadcrumb-margin-bottom:1rem;--bs-breadcrumb-bg: ;--bs-breadcrumb-border-radius: ;--bs-breadcrumb-divider-color:var(--bs-secondary-color);--bs-breadcrumb-item-padding-x:0.5rem;--bs-breadcrumb-item-active-color:var(--bs-secondary-color);display:flex;flex-wrap:wrap;padding:var(--bs-breadcrumb-padding-y) var(--bs-breadcrumb-padding-x);margin-bottom:var(--bs-breadcrumb-margin-bottom);font-size:var(--bs-breadcrumb-font-size);list-style:none;background-color:var(--bs-breadcrumb-bg);border-radius:var(--bs-breadcrumb-border-radius)}.breadcrumb-item+.breadcrumb-item{padding-left:var(--bs-breadcrumb-item-padding-x)}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:var(--bs-breadcrumb-item-padding-x);color:var(--bs-breadcrumb-divider-color);content:var(--bs-breadcrumb-divider, "/")}.breadcrumb-item.active{color:var(--bs-breadcrumb-item-active-color)}.pagination{--bs-pagination-padding-x:0.75rem;--bs-pagination-padding-y:0.375rem;--bs-pagination-font-size:1rem;--bs-pagination-color:var(--bs-link-color);--bs-pagination-bg:var(--bs-body-bg);--bs-pagination-border-width:var(--bs-border-width);--bs-pagination-border-color:var(--bs-border-color);--bs-pagination-border-radius:var(--bs-border-radius);--bs-pagination-hover-color:var(--bs-link-hover-color);--bs-pagination-hover-bg:var(--bs-tertiary-bg);--bs-pagination-hover-border-color:var(--bs-border-color);--bs-pagination-focus-color:var(--bs-link-hover-color);--bs-pagination-focus-bg:var(--bs-secondary-bg);--bs-pagination-focus-box-shadow:0 0 0 0.25rem rgba(13, 110, 253, 0.25);--bs-pagination-active-color:#fff;--bs-pagination-active-bg:#0d6efd;--bs-pagination-active-border-color:#0d6efd;--bs-pagination-disabled-color:var(--bs-secondary-color);--bs-pagination-disabled-bg:var(--bs-secondary-bg);--bs-pagination-disabled-border-color:var(--bs-border-color);display:flex;padding-left:0;list-style:none}.page-link{position:relative;display:block;padding:var(--bs-pagination-padding-y) var(--bs-pagination-padding-x);font-size:var(--bs-pagination-font-size);color:var(--bs-pagination-color);text-decoration:none;background-color:var(--bs-pagination-bg);border:var(--bs-pagination-border-width) solid var(--bs-pagination-border-color);transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.page-link{transition:none}}.page-link:hover{z-index:2;color:var(--bs-pagination-hover-color);background-color:var(--bs-pagination-hover-bg);border-color:var(--bs-pagination-hover-border-color)}.page-link:focus{z-index:3;color:var(--bs-pagination-focus-color);background-color:var(--bs-pagination-focus-bg);outline:0;box-shadow:var(--bs-pagination-focus-box-shadow)}.active>.page-link,.page-link.active{z-index:3;color:var(--bs-pagination-active-color);background-color:var(--bs-pagination-active-bg);border-color:var(--bs-pagination-active-border-color)}.disabled>.page-link,.page-link.disabled{color:var(--bs-pagination-disabled-color);pointer-events:none;background-color:var(--bs-pagination-disabled-bg);border-color:var(--bs-pagination-disabled-border-color)}.page-item:not(:first-child) .page-link{margin-left:calc(-1 * var(--bs-border-width))}.page-item:first-child .page-link{border-top-left-radius:var(--bs-pagination-border-radius);border-bottom-left-radius:var(--bs-pagination-border-radius)}.page-item:last-child .page-link{border-top-right-radius:var(--bs-pagination-border-radius);border-bottom-right-radius:var(--bs-pagination-border-radius)}.pagination-lg{--bs-pagination-padding-x:1.5rem;--bs-pagination-padding-y:0.75rem;--bs-pagination-font-size:1.25rem;--bs-pagination-border-radius:var(--bs-border-radius-lg)}.pagination-sm{--bs-pagination-padding-x:0.5rem;--bs-pagination-padding-y:0.25rem;--bs-pagination-font-size:0.875rem;--bs-pagination-border-radius:var(--bs-border-radius-sm)}.badge{--bs-badge-padding-x:0.65em;--bs-badge-padding-y:0.35em;--bs-badge-font-size:0.75em;--bs-badge-font-weight:700;--bs-badge-color:#fff;--bs-badge-border-radius:var(--bs-border-radius);display:inline-block;padding:var(--bs-badge-padding-y) var(--bs-badge-padding-x);font-size:var(--bs-badge-font-size);font-weight:var(--bs-badge-font-weight);line-height:1;color:var(--bs-badge-color);text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:var(--bs-badge-border-radius)}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.alert{--bs-alert-bg:transparent;--bs-alert-padding-x:1rem;--bs-alert-padding-y:1rem;--bs-alert-margin-bottom:1rem;--bs-alert-color:inherit;--bs-alert-border-color:transparent;--bs-alert-border:var(--bs-border-width) solid var(--bs-alert-border-color);--bs-alert-border-radius:var(--bs-border-radius);--bs-alert-link-color:inherit;position:relative;padding:var(--bs-alert-padding-y) var(--bs-alert-padding-x);margin-bottom:var(--bs-alert-margin-bottom);color:var(--bs-alert-color);background-color:var(--bs-alert-bg);border:var(--bs-alert-border);border-radius:var(--bs-alert-border-radius)}.alert-heading{color:inherit}.alert-link{font-weight:700;color:var(--bs-alert-link-color)}.alert-dismissible{padding-right:3rem}.alert-dismissible .btn-close{position:absolute;top:0;right:0;z-index:2;padding:1.25rem 1rem}.alert-primary{--bs-alert-color:var(--bs-primary-text-emphasis);--bs-alert-bg:var(--bs-primary-bg-subtle);--bs-alert-border-color:var(--bs-primary-border-subtle);--bs-alert-link-color:var(--bs-primary-text-emphasis)}.alert-secondary{--bs-alert-color:var(--bs-secondary-text-emphasis);--bs-alert-bg:var(--bs-secondary-bg-subtle);--bs-alert-border-color:var(--bs-secondary-border-subtle);--bs-alert-link-color:var(--bs-secondary-text-emphasis)}.alert-success{--bs-alert-color:var(--bs-success-text-emphasis);--bs-alert-bg:var(--bs-success-bg-subtle);--bs-alert-border-color:var(--bs-success-border-subtle);--bs-alert-link-color:var(--bs-success-text-emphasis)}.alert-info{--bs-alert-color:var(--bs-info-text-emphasis);--bs-alert-bg:var(--bs-info-bg-subtle);--bs-alert-border-color:var(--bs-info-border-subtle);--bs-alert-link-color:var(--bs-info-text-emphasis)}.alert-warning{--bs-alert-color:var(--bs-warning-text-emphasis);--bs-alert-bg:var(--bs-warning-bg-subtle);--bs-alert-border-color:var(--bs-warning-border-subtle);--bs-alert-link-color:var(--bs-warning-text-emphasis)}.alert-danger{--bs-alert-color:var(--bs-danger-text-emphasis);--bs-alert-bg:var(--bs-danger-bg-subtle);--bs-alert-border-color:var(--bs-danger-border-subtle);--bs-alert-link-color:var(--bs-danger-text-emphasis)}.alert-light{--bs-alert-color:var(--bs-light-text-emphasis);--bs-alert-bg:var(--bs-light-bg-subtle);--bs-alert-border-color:var(--bs-light-border-subtle);--bs-alert-link-color:var(--bs-light-text-emphasis)}.alert-dark{--bs-alert-color:var(--bs-dark-text-emphasis);--bs-alert-bg:var(--bs-dark-bg-subtle);--bs-alert-border-color:var(--bs-dark-border-subtle);--bs-alert-link-color:var(--bs-dark-text-emphasis)}@keyframes progress-bar-stripes{0%{background-position-x:var(--bs-progress-height)}}.progress,.progress-stacked{--bs-progress-height:1rem;--bs-progress-font-size:0.75rem;--bs-progress-bg:var(--bs-secondary-bg);--bs-progress-border-radius:var(--bs-border-radius);--bs-progress-box-shadow:var(--bs-box-shadow-inset);--bs-progress-bar-color:#fff;--bs-progress-bar-bg:#0d6efd;--bs-progress-bar-transition:width 0.6s ease;display:flex;height:var(--bs-progress-height);overflow:hidden;font-size:var(--bs-progress-font-size);background-color:var(--bs-progress-bg);border-radius:var(--bs-progress-border-radius)}.progress-bar{display:flex;flex-direction:column;justify-content:center;overflow:hidden;color:var(--bs-progress-bar-color);text-align:center;white-space:nowrap;background-color:var(--bs-progress-bar-bg);transition:var(--bs-progress-bar-transition)}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:var(--bs-progress-height) var(--bs-progress-height)}.progress-stacked>.progress{overflow:visible}.progress-stacked>.progress>.progress-bar{width:100%}.progress-bar-animated{animation:1s linear infinite progress-bar-stripes}@media (prefers-reduced-motion:reduce){.progress-bar-animated{animation:none}}.list-group{--bs-list-group-color:var(--bs-body-color);--bs-list-group-bg:var(--bs-body-bg);--bs-list-group-border-color:var(--bs-border-color);--bs-list-group-border-width:var(--bs-border-width);--bs-list-group-border-radius:var(--bs-border-radius);--bs-list-group-item-padding-x:1rem;--bs-list-group-item-padding-y:0.5rem;--bs-list-group-action-color:var(--bs-secondary-color);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-tertiary-bg);--bs-list-group-action-active-color:var(--bs-body-color);--bs-list-group-action-active-bg:var(--bs-secondary-bg);--bs-list-group-disabled-color:var(--bs-secondary-color);--bs-list-group-disabled-bg:var(--bs-body-bg);--bs-list-group-active-color:#fff;--bs-list-group-active-bg:#0d6efd;--bs-list-group-active-border-color:#0d6efd;display:flex;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:var(--bs-list-group-border-radius)}.list-group-numbered{list-style-type:none;counter-reset:section}.list-group-numbered>.list-group-item::before{content:counters(section, ".") ". ";counter-increment:section}.list-group-item{position:relative;display:block;padding:var(--bs-list-group-item-padding-y) var(--bs-list-group-item-padding-x);color:var(--bs-list-group-color);text-decoration:none;background-color:var(--bs-list-group-bg);border:var(--bs-list-group-border-width) solid var(--bs-list-group-border-color)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:var(--bs-list-group-disabled-color);pointer-events:none;background-color:var(--bs-list-group-disabled-bg)}.list-group-item.active{z-index:2;color:var(--bs-list-group-active-color);background-color:var(--bs-list-group-active-bg);border-color:var(--bs-list-group-active-border-color)}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:calc(-1 * var(--bs-list-group-border-width));border-top-width:var(--bs-list-group-border-width)}.list-group-item-action{width:100%;color:var(--bs-list-group-action-color);text-align:inherit}.list-group-item-action:not(.active):focus,.list-group-item-action:not(.active):hover{z-index:1;color:var(--bs-list-group-action-hover-color);text-decoration:none;background-color:var(--bs-list-group-action-hover-bg)}.list-group-item-action:not(.active):active{color:var(--bs-list-group-action-active-color);background-color:var(--bs-list-group-action-active-bg)}.list-group-horizontal{flex-direction:row}.list-group-horizontal>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}@media (min-width:576px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:768px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:992px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:1200px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:1400px){.list-group-horizontal-xxl{flex-direction:row}.list-group-horizontal-xxl>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-xxl>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-xxl>.list-group-item.active{margin-top:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 var(--bs-list-group-border-width)}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{--bs-list-group-color:var(--bs-primary-text-emphasis);--bs-list-group-bg:var(--bs-primary-bg-subtle);--bs-list-group-border-color:var(--bs-primary-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-primary-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-primary-border-subtle);--bs-list-group-active-color:var(--bs-primary-bg-subtle);--bs-list-group-active-bg:var(--bs-primary-text-emphasis);--bs-list-group-active-border-color:var(--bs-primary-text-emphasis)}.list-group-item-secondary{--bs-list-group-color:var(--bs-secondary-text-emphasis);--bs-list-group-bg:var(--bs-secondary-bg-subtle);--bs-list-group-border-color:var(--bs-secondary-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-secondary-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-secondary-border-subtle);--bs-list-group-active-color:var(--bs-secondary-bg-subtle);--bs-list-group-active-bg:var(--bs-secondary-text-emphasis);--bs-list-group-active-border-color:var(--bs-secondary-text-emphasis)}.list-group-item-success{--bs-list-group-color:var(--bs-success-text-emphasis);--bs-list-group-bg:var(--bs-success-bg-subtle);--bs-list-group-border-color:var(--bs-success-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-success-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-success-border-subtle);--bs-list-group-active-color:var(--bs-success-bg-subtle);--bs-list-group-active-bg:var(--bs-success-text-emphasis);--bs-list-group-active-border-color:var(--bs-success-text-emphasis)}.list-group-item-info{--bs-list-group-color:var(--bs-info-text-emphasis);--bs-list-group-bg:var(--bs-info-bg-subtle);--bs-list-group-border-color:var(--bs-info-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-info-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-info-border-subtle);--bs-list-group-active-color:var(--bs-info-bg-subtle);--bs-list-group-active-bg:var(--bs-info-text-emphasis);--bs-list-group-active-border-color:var(--bs-info-text-emphasis)}.list-group-item-warning{--bs-list-group-color:var(--bs-warning-text-emphasis);--bs-list-group-bg:var(--bs-warning-bg-subtle);--bs-list-group-border-color:var(--bs-warning-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-warning-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-warning-border-subtle);--bs-list-group-active-color:var(--bs-warning-bg-subtle);--bs-list-group-active-bg:var(--bs-warning-text-emphasis);--bs-list-group-active-border-color:var(--bs-warning-text-emphasis)}.list-group-item-danger{--bs-list-group-color:var(--bs-danger-text-emphasis);--bs-list-group-bg:var(--bs-danger-bg-subtle);--bs-list-group-border-color:var(--bs-danger-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-danger-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-danger-border-subtle);--bs-list-group-active-color:var(--bs-danger-bg-subtle);--bs-list-group-active-bg:var(--bs-danger-text-emphasis);--bs-list-group-active-border-color:var(--bs-danger-text-emphasis)}.list-group-item-light{--bs-list-group-color:var(--bs-light-text-emphasis);--bs-list-group-bg:var(--bs-light-bg-subtle);--bs-list-group-border-color:var(--bs-light-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-light-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-light-border-subtle);--bs-list-group-active-color:var(--bs-light-bg-subtle);--bs-list-group-active-bg:var(--bs-light-text-emphasis);--bs-list-group-active-border-color:var(--bs-light-text-emphasis)}.list-group-item-dark{--bs-list-group-color:var(--bs-dark-text-emphasis);--bs-list-group-bg:var(--bs-dark-bg-subtle);--bs-list-group-border-color:var(--bs-dark-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-dark-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-dark-border-subtle);--bs-list-group-active-color:var(--bs-dark-bg-subtle);--bs-list-group-active-bg:var(--bs-dark-text-emphasis);--bs-list-group-active-border-color:var(--bs-dark-text-emphasis)}.btn-close{--bs-btn-close-color:#000;--bs-btn-close-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 0 1 1.414 0L8 6.586 14.293.293a1 1 0 1 1 1.414 1.414L9.414 8l6.293 6.293a1 1 0 0 1-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L6.586 8 .293 1.707a1 1 0 0 1 0-1.414'/%3e%3c/svg%3e");--bs-btn-close-opacity:0.5;--bs-btn-close-hover-opacity:0.75;--bs-btn-close-focus-shadow:0 0 0 0.25rem rgba(13, 110, 253, 0.25);--bs-btn-close-focus-opacity:1;--bs-btn-close-disabled-opacity:0.25;box-sizing:content-box;width:1em;height:1em;padding:.25em .25em;color:var(--bs-btn-close-color);background:transparent var(--bs-btn-close-bg) center/1em auto no-repeat;filter:var(--bs-btn-close-filter);border:0;border-radius:.375rem;opacity:var(--bs-btn-close-opacity)}.btn-close:hover{color:var(--bs-btn-close-color);text-decoration:none;opacity:var(--bs-btn-close-hover-opacity)}.btn-close:focus{outline:0;box-shadow:var(--bs-btn-close-focus-shadow);opacity:var(--bs-btn-close-focus-opacity)}.btn-close.disabled,.btn-close:disabled{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;opacity:var(--bs-btn-close-disabled-opacity)}.btn-close-white{--bs-btn-close-filter:invert(1) grayscale(100%) brightness(200%)}:root,[data-bs-theme=light]{--bs-btn-close-filter: }[data-bs-theme=dark]{--bs-btn-close-filter:invert(1) grayscale(100%) brightness(200%)}.toast{--bs-toast-zindex:1090;--bs-toast-padding-x:0.75rem;--bs-toast-padding-y:0.5rem;--bs-toast-spacing:1.5rem;--bs-toast-max-width:350px;--bs-toast-font-size:0.875rem;--bs-toast-color: ;--bs-toast-bg:rgba(var(--bs-body-bg-rgb), 0.85);--bs-toast-border-width:var(--bs-border-width);--bs-toast-border-color:var(--bs-border-color-translucent);--bs-toast-border-radius:var(--bs-border-radius);--bs-toast-box-shadow:var(--bs-box-shadow);--bs-toast-header-color:var(--bs-secondary-color);--bs-toast-header-bg:rgba(var(--bs-body-bg-rgb), 0.85);--bs-toast-header-border-color:var(--bs-border-color-translucent);width:var(--bs-toast-max-width);max-width:100%;font-size:var(--bs-toast-font-size);color:var(--bs-toast-color);pointer-events:auto;background-color:var(--bs-toast-bg);background-clip:padding-box;border:var(--bs-toast-border-width) solid var(--bs-toast-border-color);box-shadow:var(--bs-toast-box-shadow);border-radius:var(--bs-toast-border-radius)}.toast.showing{opacity:0}.toast:not(.show){display:none}.toast-container{--bs-toast-zindex:1090;position:absolute;z-index:var(--bs-toast-zindex);width:-webkit-max-content;width:-moz-max-content;width:max-content;max-width:100%;pointer-events:none}.toast-container>:not(:last-child){margin-bottom:var(--bs-toast-spacing)}.toast-header{display:flex;align-items:center;padding:var(--bs-toast-padding-y) var(--bs-toast-padding-x);color:var(--bs-toast-header-color);background-color:var(--bs-toast-header-bg);background-clip:padding-box;border-bottom:var(--bs-toast-border-width) solid var(--bs-toast-header-border-color);border-top-left-radius:calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width));border-top-right-radius:calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width))}.toast-header .btn-close{margin-right:calc(-.5 * var(--bs-toast-padding-x));margin-left:var(--bs-toast-padding-x)}.toast-body{padding:var(--bs-toast-padding-x);word-wrap:break-word}.modal{--bs-modal-zindex:1055;--bs-modal-width:500px;--bs-modal-padding:1rem;--bs-modal-margin:0.5rem;--bs-modal-color:var(--bs-body-color);--bs-modal-bg:var(--bs-body-bg);--bs-modal-border-color:var(--bs-border-color-translucent);--bs-modal-border-width:var(--bs-border-width);--bs-modal-border-radius:var(--bs-border-radius-lg);--bs-modal-box-shadow:var(--bs-box-shadow-sm);--bs-modal-inner-border-radius:calc(var(--bs-border-radius-lg) - (var(--bs-border-width)));--bs-modal-header-padding-x:1rem;--bs-modal-header-padding-y:1rem;--bs-modal-header-padding:1rem 1rem;--bs-modal-header-border-color:var(--bs-border-color);--bs-modal-header-border-width:var(--bs-border-width);--bs-modal-title-line-height:1.5;--bs-modal-footer-gap:0.5rem;--bs-modal-footer-bg: ;--bs-modal-footer-border-color:var(--bs-border-color);--bs-modal-footer-border-width:var(--bs-border-width);position:fixed;top:0;left:0;z-index:var(--bs-modal-zindex);display:none;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;outline:0}.modal-dialog{position:relative;width:auto;margin:var(--bs-modal-margin);pointer-events:none}.modal.fade .modal-dialog{transform:translate(0,-50px);transition:transform .3s ease-out}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{transform:none}.modal.modal-static .modal-dialog{transform:scale(1.02)}.modal-dialog-scrollable{height:calc(100% - var(--bs-modal-margin) * 2)}.modal-dialog-scrollable .modal-content{max-height:100%;overflow:hidden}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;align-items:center;min-height:calc(100% - var(--bs-modal-margin) * 2)}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;color:var(--bs-modal-color);pointer-events:auto;background-color:var(--bs-modal-bg);background-clip:padding-box;border:var(--bs-modal-border-width) solid var(--bs-modal-border-color);border-radius:var(--bs-modal-border-radius);outline:0}.modal-backdrop{--bs-backdrop-zindex:1050;--bs-backdrop-bg:#000;--bs-backdrop-opacity:0.5;position:fixed;top:0;left:0;z-index:var(--bs-backdrop-zindex);width:100vw;height:100vh;background-color:var(--bs-backdrop-bg)}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:var(--bs-backdrop-opacity)}.modal-header{display:flex;flex-shrink:0;align-items:center;padding:var(--bs-modal-header-padding);border-bottom:var(--bs-modal-header-border-width) solid var(--bs-modal-header-border-color);border-top-left-radius:var(--bs-modal-inner-border-radius);border-top-right-radius:var(--bs-modal-inner-border-radius)}.modal-header .btn-close{padding:calc(var(--bs-modal-header-padding-y) * .5) calc(var(--bs-modal-header-padding-x) * .5);margin-top:calc(-.5 * var(--bs-modal-header-padding-y));margin-right:calc(-.5 * var(--bs-modal-header-padding-x));margin-bottom:calc(-.5 * var(--bs-modal-header-padding-y));margin-left:auto}.modal-title{margin-bottom:0;line-height:var(--bs-modal-title-line-height)}.modal-body{position:relative;flex:1 1 auto;padding:var(--bs-modal-padding)}.modal-footer{display:flex;flex-shrink:0;flex-wrap:wrap;align-items:center;justify-content:flex-end;padding:calc(var(--bs-modal-padding) - var(--bs-modal-footer-gap) * .5);background-color:var(--bs-modal-footer-bg);border-top:var(--bs-modal-footer-border-width) solid var(--bs-modal-footer-border-color);border-bottom-right-radius:var(--bs-modal-inner-border-radius);border-bottom-left-radius:var(--bs-modal-inner-border-radius)}.modal-footer>*{margin:calc(var(--bs-modal-footer-gap) * .5)}@media (min-width:576px){.modal{--bs-modal-margin:1.75rem;--bs-modal-box-shadow:var(--bs-box-shadow)}.modal-dialog{max-width:var(--bs-modal-width);margin-right:auto;margin-left:auto}.modal-sm{--bs-modal-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{--bs-modal-width:800px}}@media (min-width:1200px){.modal-xl{--bs-modal-width:1140px}}.modal-fullscreen{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen .modal-footer,.modal-fullscreen .modal-header{border-radius:0}.modal-fullscreen .modal-body{overflow-y:auto}@media (max-width:575.98px){.modal-fullscreen-sm-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-sm-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-sm-down .modal-footer,.modal-fullscreen-sm-down .modal-header{border-radius:0}.modal-fullscreen-sm-down .modal-body{overflow-y:auto}}@media (max-width:767.98px){.modal-fullscreen-md-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-md-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-md-down .modal-footer,.modal-fullscreen-md-down .modal-header{border-radius:0}.modal-fullscreen-md-down .modal-body{overflow-y:auto}}@media (max-width:991.98px){.modal-fullscreen-lg-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-lg-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-lg-down .modal-footer,.modal-fullscreen-lg-down .modal-header{border-radius:0}.modal-fullscreen-lg-down .modal-body{overflow-y:auto}}@media (max-width:1199.98px){.modal-fullscreen-xl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xl-down .modal-footer,.modal-fullscreen-xl-down .modal-header{border-radius:0}.modal-fullscreen-xl-down .modal-body{overflow-y:auto}}@media (max-width:1399.98px){.modal-fullscreen-xxl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xxl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xxl-down .modal-footer,.modal-fullscreen-xxl-down .modal-header{border-radius:0}.modal-fullscreen-xxl-down .modal-body{overflow-y:auto}}.tooltip{--bs-tooltip-zindex:1080;--bs-tooltip-max-width:200px;--bs-tooltip-padding-x:0.5rem;--bs-tooltip-padding-y:0.25rem;--bs-tooltip-margin: ;--bs-tooltip-font-size:0.875rem;--bs-tooltip-color:var(--bs-body-bg);--bs-tooltip-bg:var(--bs-emphasis-color);--bs-tooltip-border-radius:var(--bs-border-radius);--bs-tooltip-opacity:0.9;--bs-tooltip-arrow-width:0.8rem;--bs-tooltip-arrow-height:0.4rem;z-index:var(--bs-tooltip-zindex);display:block;margin:var(--bs-tooltip-margin);font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;white-space:normal;word-spacing:normal;line-break:auto;font-size:var(--bs-tooltip-font-size);word-wrap:break-word;opacity:0}.tooltip.show{opacity:var(--bs-tooltip-opacity)}.tooltip .tooltip-arrow{display:block;width:var(--bs-tooltip-arrow-width);height:var(--bs-tooltip-arrow-height)}.tooltip .tooltip-arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow,.bs-tooltip-top .tooltip-arrow{bottom:calc(-1 * var(--bs-tooltip-arrow-height))}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before,.bs-tooltip-top .tooltip-arrow::before{top:-1px;border-width:var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * .5) 0;border-top-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow,.bs-tooltip-end .tooltip-arrow{left:calc(-1 * var(--bs-tooltip-arrow-height));width:var(--bs-tooltip-arrow-height);height:var(--bs-tooltip-arrow-width)}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before,.bs-tooltip-end .tooltip-arrow::before{right:-1px;border-width:calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * .5) 0;border-right-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow,.bs-tooltip-bottom .tooltip-arrow{top:calc(-1 * var(--bs-tooltip-arrow-height))}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before,.bs-tooltip-bottom .tooltip-arrow::before{bottom:-1px;border-width:0 calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height);border-bottom-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow,.bs-tooltip-start .tooltip-arrow{right:calc(-1 * var(--bs-tooltip-arrow-height));width:var(--bs-tooltip-arrow-height);height:var(--bs-tooltip-arrow-width)}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before,.bs-tooltip-start .tooltip-arrow::before{left:-1px;border-width:calc(var(--bs-tooltip-arrow-width) * .5) 0 calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height);border-left-color:var(--bs-tooltip-bg)}.tooltip-inner{max-width:var(--bs-tooltip-max-width);padding:var(--bs-tooltip-padding-y) var(--bs-tooltip-padding-x);color:var(--bs-tooltip-color);text-align:center;background-color:var(--bs-tooltip-bg);border-radius:var(--bs-tooltip-border-radius)}.popover{--bs-popover-zindex:1070;--bs-popover-max-width:276px;--bs-popover-font-size:0.875rem;--bs-popover-bg:var(--bs-body-bg);--bs-popover-border-width:var(--bs-border-width);--bs-popover-border-color:var(--bs-border-color-translucent);--bs-popover-border-radius:var(--bs-border-radius-lg);--bs-popover-inner-border-radius:calc(var(--bs-border-radius-lg) - var(--bs-border-width));--bs-popover-box-shadow:var(--bs-box-shadow);--bs-popover-header-padding-x:1rem;--bs-popover-header-padding-y:0.5rem;--bs-popover-header-font-size:1rem;--bs-popover-header-color:inherit;--bs-popover-header-bg:var(--bs-secondary-bg);--bs-popover-body-padding-x:1rem;--bs-popover-body-padding-y:1rem;--bs-popover-body-color:var(--bs-body-color);--bs-popover-arrow-width:1rem;--bs-popover-arrow-height:0.5rem;--bs-popover-arrow-border:var(--bs-popover-border-color);z-index:var(--bs-popover-zindex);display:block;max-width:var(--bs-popover-max-width);font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;white-space:normal;word-spacing:normal;line-break:auto;font-size:var(--bs-popover-font-size);word-wrap:break-word;background-color:var(--bs-popover-bg);background-clip:padding-box;border:var(--bs-popover-border-width) solid var(--bs-popover-border-color);border-radius:var(--bs-popover-border-radius)}.popover .popover-arrow{display:block;width:var(--bs-popover-arrow-width);height:var(--bs-popover-arrow-height)}.popover .popover-arrow::after,.popover .popover-arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid;border-width:0}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow,.bs-popover-top>.popover-arrow{bottom:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width))}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::after,.bs-popover-top>.popover-arrow::before{border-width:var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * .5) 0}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::before{bottom:0;border-top-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-top>.popover-arrow::after{bottom:var(--bs-popover-border-width);border-top-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow,.bs-popover-end>.popover-arrow{left:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));width:var(--bs-popover-arrow-height);height:var(--bs-popover-arrow-width)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::after,.bs-popover-end>.popover-arrow::before{border-width:calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * .5) 0}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::before{left:0;border-right-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-end>.popover-arrow::after{left:var(--bs-popover-border-width);border-right-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow,.bs-popover-bottom>.popover-arrow{top:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width))}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::before{border-width:0 calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::before{top:0;border-bottom-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::after{top:var(--bs-popover-border-width);border-bottom-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:var(--bs-popover-arrow-width);margin-left:calc(-.5 * var(--bs-popover-arrow-width));content:"";border-bottom:var(--bs-popover-border-width) solid var(--bs-popover-header-bg)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow,.bs-popover-start>.popover-arrow{right:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));width:var(--bs-popover-arrow-height);height:var(--bs-popover-arrow-width)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::after,.bs-popover-start>.popover-arrow::before{border-width:calc(var(--bs-popover-arrow-width) * .5) 0 calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::before{right:0;border-left-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-start>.popover-arrow::after{right:var(--bs-popover-border-width);border-left-color:var(--bs-popover-bg)}.popover-header{padding:var(--bs-popover-header-padding-y) var(--bs-popover-header-padding-x);margin-bottom:0;font-size:var(--bs-popover-header-font-size);color:var(--bs-popover-header-color);background-color:var(--bs-popover-header-bg);border-bottom:var(--bs-popover-border-width) solid var(--bs-popover-border-color);border-top-left-radius:var(--bs-popover-inner-border-radius);border-top-right-radius:var(--bs-popover-inner-border-radius)}.popover-header:empty{display:none}.popover-body{padding:var(--bs-popover-body-padding-y) var(--bs-popover-body-padding-x);color:var(--bs-popover-body-color)}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-end,.carousel-item-next:not(.carousel-item-start){transform:translateX(100%)}.active.carousel-item-start,.carousel-item-prev:not(.carousel-item-end){transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item-next.carousel-item-start,.carousel-fade .carousel-item-prev.carousel-item-end,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:flex;align-items:center;justify-content:center;width:15%;padding:0;color:#fff;text-align:center;background:0 0;filter:var(--bs-carousel-control-icon-filter);border:0;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:2rem;height:2rem;background-repeat:no-repeat;background-position:50%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:2;display:flex;justify-content:center;padding:0;margin-right:15%;margin-bottom:1rem;margin-left:15%}.carousel-indicators [data-bs-target]{box-sizing:content-box;flex:0 1 auto;width:30px;height:3px;padding:0;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:var(--bs-carousel-indicator-active-bg);background-clip:padding-box;border:0;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators [data-bs-target]{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:1.25rem;left:15%;padding-top:1.25rem;padding-bottom:1.25rem;color:var(--bs-carousel-caption-color);text-align:center}.carousel-dark{--bs-carousel-indicator-active-bg:#000;--bs-carousel-caption-color:#000;--bs-carousel-control-icon-filter:invert(1) grayscale(100)}:root,[data-bs-theme=light]{--bs-carousel-indicator-active-bg:#fff;--bs-carousel-caption-color:#fff;--bs-carousel-control-icon-filter: }[data-bs-theme=dark]{--bs-carousel-indicator-active-bg:#000;--bs-carousel-caption-color:#000;--bs-carousel-control-icon-filter:invert(1) grayscale(100)}.spinner-border,.spinner-grow{display:inline-block;flex-shrink:0;width:var(--bs-spinner-width);height:var(--bs-spinner-height);vertical-align:var(--bs-spinner-vertical-align);border-radius:50%;animation:var(--bs-spinner-animation-speed) linear infinite var(--bs-spinner-animation-name)}@keyframes spinner-border{to{transform:rotate(360deg)}}.spinner-border{--bs-spinner-width:2rem;--bs-spinner-height:2rem;--bs-spinner-vertical-align:-0.125em;--bs-spinner-border-width:0.25em;--bs-spinner-animation-speed:0.75s;--bs-spinner-animation-name:spinner-border;border:var(--bs-spinner-border-width) solid currentcolor;border-right-color:transparent}.spinner-border-sm{--bs-spinner-width:1rem;--bs-spinner-height:1rem;--bs-spinner-border-width:0.2em}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{--bs-spinner-width:2rem;--bs-spinner-height:2rem;--bs-spinner-vertical-align:-0.125em;--bs-spinner-animation-speed:0.75s;--bs-spinner-animation-name:spinner-grow;background-color:currentcolor;opacity:0}.spinner-grow-sm{--bs-spinner-width:1rem;--bs-spinner-height:1rem}@media (prefers-reduced-motion:reduce){.spinner-border,.spinner-grow{--bs-spinner-animation-speed:1.5s}}.offcanvas,.offcanvas-lg,.offcanvas-md,.offcanvas-sm,.offcanvas-xl,.offcanvas-xxl{--bs-offcanvas-zindex:1045;--bs-offcanvas-width:400px;--bs-offcanvas-height:30vh;--bs-offcanvas-padding-x:1rem;--bs-offcanvas-padding-y:1rem;--bs-offcanvas-color:var(--bs-body-color);--bs-offcanvas-bg:var(--bs-body-bg);--bs-offcanvas-border-width:var(--bs-border-width);--bs-offcanvas-border-color:var(--bs-border-color-translucent);--bs-offcanvas-box-shadow:var(--bs-box-shadow-sm);--bs-offcanvas-transition:transform 0.3s ease-in-out;--bs-offcanvas-title-line-height:1.5}@media (max-width:575.98px){.offcanvas-sm{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:575.98px) and (prefers-reduced-motion:reduce){.offcanvas-sm{transition:none}}@media (max-width:575.98px){.offcanvas-sm.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-sm.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-sm.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-sm.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-sm.show:not(.hiding),.offcanvas-sm.showing{transform:none}.offcanvas-sm.hiding,.offcanvas-sm.show,.offcanvas-sm.showing{visibility:visible}}@media (min-width:576px){.offcanvas-sm{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-sm .offcanvas-header{display:none}.offcanvas-sm .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:767.98px){.offcanvas-md{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:767.98px) and (prefers-reduced-motion:reduce){.offcanvas-md{transition:none}}@media (max-width:767.98px){.offcanvas-md.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-md.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-md.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-md.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-md.show:not(.hiding),.offcanvas-md.showing{transform:none}.offcanvas-md.hiding,.offcanvas-md.show,.offcanvas-md.showing{visibility:visible}}@media (min-width:768px){.offcanvas-md{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-md .offcanvas-header{display:none}.offcanvas-md .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:991.98px){.offcanvas-lg{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:991.98px) and (prefers-reduced-motion:reduce){.offcanvas-lg{transition:none}}@media (max-width:991.98px){.offcanvas-lg.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-lg.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-lg.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-lg.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-lg.show:not(.hiding),.offcanvas-lg.showing{transform:none}.offcanvas-lg.hiding,.offcanvas-lg.show,.offcanvas-lg.showing{visibility:visible}}@media (min-width:992px){.offcanvas-lg{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-lg .offcanvas-header{display:none}.offcanvas-lg .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:1199.98px){.offcanvas-xl{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:1199.98px) and (prefers-reduced-motion:reduce){.offcanvas-xl{transition:none}}@media (max-width:1199.98px){.offcanvas-xl.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-xl.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-xl.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-xl.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-xl.show:not(.hiding),.offcanvas-xl.showing{transform:none}.offcanvas-xl.hiding,.offcanvas-xl.show,.offcanvas-xl.showing{visibility:visible}}@media (min-width:1200px){.offcanvas-xl{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-xl .offcanvas-header{display:none}.offcanvas-xl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:1399.98px){.offcanvas-xxl{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:1399.98px) and (prefers-reduced-motion:reduce){.offcanvas-xxl{transition:none}}@media (max-width:1399.98px){.offcanvas-xxl.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-xxl.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-xxl.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-xxl.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-xxl.show:not(.hiding),.offcanvas-xxl.showing{transform:none}.offcanvas-xxl.hiding,.offcanvas-xxl.show,.offcanvas-xxl.showing{visibility:visible}}@media (min-width:1400px){.offcanvas-xxl{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-xxl .offcanvas-header{display:none}.offcanvas-xxl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}.offcanvas{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}@media (prefers-reduced-motion:reduce){.offcanvas{transition:none}}.offcanvas.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas.show:not(.hiding),.offcanvas.showing{transform:none}.offcanvas.hiding,.offcanvas.show,.offcanvas.showing{visibility:visible}.offcanvas-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.offcanvas-backdrop.fade{opacity:0}.offcanvas-backdrop.show{opacity:.5}.offcanvas-header{display:flex;align-items:center;padding:var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x)}.offcanvas-header .btn-close{padding:calc(var(--bs-offcanvas-padding-y) * .5) calc(var(--bs-offcanvas-padding-x) * .5);margin-top:calc(-.5 * var(--bs-offcanvas-padding-y));margin-right:calc(-.5 * var(--bs-offcanvas-padding-x));margin-bottom:calc(-.5 * var(--bs-offcanvas-padding-y));margin-left:auto}.offcanvas-title{margin-bottom:0;line-height:var(--bs-offcanvas-title-line-height)}.offcanvas-body{flex-grow:1;padding:var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x);overflow-y:auto}.placeholder{display:inline-block;min-height:1em;vertical-align:middle;cursor:wait;background-color:currentcolor;opacity:.5}.placeholder.btn::before{display:inline-block;content:""}.placeholder-xs{min-height:.6em}.placeholder-sm{min-height:.8em}.placeholder-lg{min-height:1.2em}.placeholder-glow .placeholder{animation:placeholder-glow 2s ease-in-out infinite}@keyframes placeholder-glow{50%{opacity:.2}}.placeholder-wave{-webkit-mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);-webkit-mask-size:200% 100%;mask-size:200% 100%;animation:placeholder-wave 2s linear infinite}@keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0%;mask-position:-200% 0%}}.clearfix::after{display:block;clear:both;content:""}.text-bg-primary{color:#fff!important;background-color:RGBA(var(--bs-primary-rgb),var(--bs-bg-opacity,1))!important}.text-bg-secondary{color:#fff!important;background-color:RGBA(var(--bs-secondary-rgb),var(--bs-bg-opacity,1))!important}.text-bg-success{color:#fff!important;background-color:RGBA(var(--bs-success-rgb),var(--bs-bg-opacity,1))!important}.text-bg-info{color:#000!important;background-color:RGBA(var(--bs-info-rgb),var(--bs-bg-opacity,1))!important}.text-bg-warning{color:#000!important;background-color:RGBA(var(--bs-warning-rgb),var(--bs-bg-opacity,1))!important}.text-bg-danger{color:#fff!important;background-color:RGBA(var(--bs-danger-rgb),var(--bs-bg-opacity,1))!important}.text-bg-light{color:#000!important;background-color:RGBA(var(--bs-light-rgb),var(--bs-bg-opacity,1))!important}.text-bg-dark{color:#fff!important;background-color:RGBA(var(--bs-dark-rgb),var(--bs-bg-opacity,1))!important}.link-primary{color:RGBA(var(--bs-primary-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-primary-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-primary-rgb),var(--bs-link-underline-opacity,1))!important}.link-primary:focus,.link-primary:hover{color:RGBA(10,88,202,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(10,88,202,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(10,88,202,var(--bs-link-underline-opacity,1))!important}.link-secondary{color:RGBA(var(--bs-secondary-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-secondary-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-secondary-rgb),var(--bs-link-underline-opacity,1))!important}.link-secondary:focus,.link-secondary:hover{color:RGBA(86,94,100,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(86,94,100,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(86,94,100,var(--bs-link-underline-opacity,1))!important}.link-success{color:RGBA(var(--bs-success-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-success-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-success-rgb),var(--bs-link-underline-opacity,1))!important}.link-success:focus,.link-success:hover{color:RGBA(20,108,67,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(20,108,67,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(20,108,67,var(--bs-link-underline-opacity,1))!important}.link-info{color:RGBA(var(--bs-info-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-info-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-info-rgb),var(--bs-link-underline-opacity,1))!important}.link-info:focus,.link-info:hover{color:RGBA(61,213,243,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(61,213,243,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(61,213,243,var(--bs-link-underline-opacity,1))!important}.link-warning{color:RGBA(var(--bs-warning-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-warning-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-warning-rgb),var(--bs-link-underline-opacity,1))!important}.link-warning:focus,.link-warning:hover{color:RGBA(255,205,57,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(255,205,57,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(255,205,57,var(--bs-link-underline-opacity,1))!important}.link-danger{color:RGBA(var(--bs-danger-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-danger-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-danger-rgb),var(--bs-link-underline-opacity,1))!important}.link-danger:focus,.link-danger:hover{color:RGBA(176,42,55,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(176,42,55,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(176,42,55,var(--bs-link-underline-opacity,1))!important}.link-light{color:RGBA(var(--bs-light-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-light-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-light-rgb),var(--bs-link-underline-opacity,1))!important}.link-light:focus,.link-light:hover{color:RGBA(249,250,251,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(249,250,251,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(249,250,251,var(--bs-link-underline-opacity,1))!important}.link-dark{color:RGBA(var(--bs-dark-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-dark-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-dark-rgb),var(--bs-link-underline-opacity,1))!important}.link-dark:focus,.link-dark:hover{color:RGBA(26,30,33,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(26,30,33,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(26,30,33,var(--bs-link-underline-opacity,1))!important}.link-body-emphasis{color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,1))!important}.link-body-emphasis:focus,.link-body-emphasis:hover{color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-opacity,.75))!important;-webkit-text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,0.75))!important;text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,0.75))!important}.focus-ring:focus{outline:0;box-shadow:var(--bs-focus-ring-x,0) var(--bs-focus-ring-y,0) var(--bs-focus-ring-blur,0) var(--bs-focus-ring-width) var(--bs-focus-ring-color)}.icon-link{display:inline-flex;gap:.375rem;align-items:center;-webkit-text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-opacity,0.5));text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-opacity,0.5));text-underline-offset:0.25em;-webkit-backface-visibility:hidden;backface-visibility:hidden}.icon-link>.bi{flex-shrink:0;width:1em;height:1em;fill:currentcolor;transition:.2s ease-in-out transform}@media (prefers-reduced-motion:reduce){.icon-link>.bi{transition:none}}.icon-link-hover:focus-visible>.bi,.icon-link-hover:hover>.bi{transform:var(--bs-icon-link-transform,translate3d(.25em,0,0))}.ratio{position:relative;width:100%}.ratio::before{display:block;padding-top:var(--bs-aspect-ratio);content:""}.ratio>*{position:absolute;top:0;left:0;width:100%;height:100%}.ratio-1x1{--bs-aspect-ratio:100%}.ratio-4x3{--bs-aspect-ratio:75%}.ratio-16x9{--bs-aspect-ratio:56.25%}.ratio-21x9{--bs-aspect-ratio:42.8571428571%}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}@media (min-width:576px){.sticky-sm-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-sm-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:768px){.sticky-md-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-md-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:992px){.sticky-lg-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-lg-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:1200px){.sticky-xl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-xl-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:1400px){.sticky-xxl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-xxl-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}.hstack{display:flex;flex-direction:row;align-items:center;align-self:stretch}.vstack{display:flex;flex:1 1 auto;flex-direction:column;align-self:stretch}.visually-hidden,.visually-hidden-focusable:not(:focus):not(:focus-within){width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.visually-hidden-focusable:not(:focus):not(:focus-within):not(caption),.visually-hidden:not(caption){position:absolute!important}.visually-hidden *,.visually-hidden-focusable:not(:focus):not(:focus-within) *{overflow:hidden!important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.vr{display:inline-block;align-self:stretch;width:var(--bs-border-width);min-height:1em;background-color:currentcolor;opacity:.25}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.float-start{float:left!important}.float-end{float:right!important}.float-none{float:none!important}.object-fit-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-none{-o-object-fit:none!important;object-fit:none!important}.opacity-0{opacity:0!important}.opacity-25{opacity:.25!important}.opacity-50{opacity:.5!important}.opacity-75{opacity:.75!important}.opacity-100{opacity:1!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.overflow-visible{overflow:visible!important}.overflow-scroll{overflow:scroll!important}.overflow-x-auto{overflow-x:auto!important}.overflow-x-hidden{overflow-x:hidden!important}.overflow-x-visible{overflow-x:visible!important}.overflow-x-scroll{overflow-x:scroll!important}.overflow-y-auto{overflow-y:auto!important}.overflow-y-hidden{overflow-y:hidden!important}.overflow-y-visible{overflow-y:visible!important}.overflow-y-scroll{overflow-y:scroll!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-grid{display:grid!important}.d-inline-grid{display:inline-grid!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.d-none{display:none!important}.shadow{box-shadow:var(--bs-box-shadow)!important}.shadow-sm{box-shadow:var(--bs-box-shadow-sm)!important}.shadow-lg{box-shadow:var(--bs-box-shadow-lg)!important}.shadow-none{box-shadow:none!important}.focus-ring-primary{--bs-focus-ring-color:rgba(var(--bs-primary-rgb), var(--bs-focus-ring-opacity))}.focus-ring-secondary{--bs-focus-ring-color:rgba(var(--bs-secondary-rgb), var(--bs-focus-ring-opacity))}.focus-ring-success{--bs-focus-ring-color:rgba(var(--bs-success-rgb), var(--bs-focus-ring-opacity))}.focus-ring-info{--bs-focus-ring-color:rgba(var(--bs-info-rgb), var(--bs-focus-ring-opacity))}.focus-ring-warning{--bs-focus-ring-color:rgba(var(--bs-warning-rgb), var(--bs-focus-ring-opacity))}.focus-ring-danger{--bs-focus-ring-color:rgba(var(--bs-danger-rgb), var(--bs-focus-ring-opacity))}.focus-ring-light{--bs-focus-ring-color:rgba(var(--bs-light-rgb), var(--bs-focus-ring-opacity))}.focus-ring-dark{--bs-focus-ring-color:rgba(var(--bs-dark-rgb), var(--bs-focus-ring-opacity))}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.top-0{top:0!important}.top-50{top:50%!important}.top-100{top:100%!important}.bottom-0{bottom:0!important}.bottom-50{bottom:50%!important}.bottom-100{bottom:100%!important}.start-0{left:0!important}.start-50{left:50%!important}.start-100{left:100%!important}.end-0{right:0!important}.end-50{right:50%!important}.end-100{right:100%!important}.translate-middle{transform:translate(-50%,-50%)!important}.translate-middle-x{transform:translateX(-50%)!important}.translate-middle-y{transform:translateY(-50%)!important}.border{border:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-0{border:0!important}.border-top{border-top:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-top-0{border-top:0!important}.border-end{border-right:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-end-0{border-right:0!important}.border-bottom{border-bottom:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-bottom-0{border-bottom:0!important}.border-start{border-left:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-start-0{border-left:0!important}.border-primary{--bs-border-opacity:1;border-color:rgba(var(--bs-primary-rgb),var(--bs-border-opacity))!important}.border-secondary{--bs-border-opacity:1;border-color:rgba(var(--bs-secondary-rgb),var(--bs-border-opacity))!important}.border-success{--bs-border-opacity:1;border-color:rgba(var(--bs-success-rgb),var(--bs-border-opacity))!important}.border-info{--bs-border-opacity:1;border-color:rgba(var(--bs-info-rgb),var(--bs-border-opacity))!important}.border-warning{--bs-border-opacity:1;border-color:rgba(var(--bs-warning-rgb),var(--bs-border-opacity))!important}.border-danger{--bs-border-opacity:1;border-color:rgba(var(--bs-danger-rgb),var(--bs-border-opacity))!important}.border-light{--bs-border-opacity:1;border-color:rgba(var(--bs-light-rgb),var(--bs-border-opacity))!important}.border-dark{--bs-border-opacity:1;border-color:rgba(var(--bs-dark-rgb),var(--bs-border-opacity))!important}.border-black{--bs-border-opacity:1;border-color:rgba(var(--bs-black-rgb),var(--bs-border-opacity))!important}.border-white{--bs-border-opacity:1;border-color:rgba(var(--bs-white-rgb),var(--bs-border-opacity))!important}.border-primary-subtle{border-color:var(--bs-primary-border-subtle)!important}.border-secondary-subtle{border-color:var(--bs-secondary-border-subtle)!important}.border-success-subtle{border-color:var(--bs-success-border-subtle)!important}.border-info-subtle{border-color:var(--bs-info-border-subtle)!important}.border-warning-subtle{border-color:var(--bs-warning-border-subtle)!important}.border-danger-subtle{border-color:var(--bs-danger-border-subtle)!important}.border-light-subtle{border-color:var(--bs-light-border-subtle)!important}.border-dark-subtle{border-color:var(--bs-dark-border-subtle)!important}.border-1{border-width:1px!important}.border-2{border-width:2px!important}.border-3{border-width:3px!important}.border-4{border-width:4px!important}.border-5{border-width:5px!important}.border-opacity-10{--bs-border-opacity:0.1}.border-opacity-25{--bs-border-opacity:0.25}.border-opacity-50{--bs-border-opacity:0.5}.border-opacity-75{--bs-border-opacity:0.75}.border-opacity-100{--bs-border-opacity:1}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.mw-100{max-width:100%!important}.vw-100{width:100vw!important}.min-vw-100{min-width:100vw!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mh-100{max-height:100%!important}.vh-100{height:100vh!important}.min-vh-100{min-height:100vh!important}.flex-fill{flex:1 1 auto!important}.flex-row{flex-direction:row!important}.flex-column{flex-direction:column!important}.flex-row-reverse{flex-direction:row-reverse!important}.flex-column-reverse{flex-direction:column-reverse!important}.flex-grow-0{flex-grow:0!important}.flex-grow-1{flex-grow:1!important}.flex-shrink-0{flex-shrink:0!important}.flex-shrink-1{flex-shrink:1!important}.flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.flex-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-start{justify-content:flex-start!important}.justify-content-end{justify-content:flex-end!important}.justify-content-center{justify-content:center!important}.justify-content-between{justify-content:space-between!important}.justify-content-around{justify-content:space-around!important}.justify-content-evenly{justify-content:space-evenly!important}.align-items-start{align-items:flex-start!important}.align-items-end{align-items:flex-end!important}.align-items-center{align-items:center!important}.align-items-baseline{align-items:baseline!important}.align-items-stretch{align-items:stretch!important}.align-content-start{align-content:flex-start!important}.align-content-end{align-content:flex-end!important}.align-content-center{align-content:center!important}.align-content-between{align-content:space-between!important}.align-content-around{align-content:space-around!important}.align-content-stretch{align-content:stretch!important}.align-self-auto{align-self:auto!important}.align-self-start{align-self:flex-start!important}.align-self-end{align-self:flex-end!important}.align-self-center{align-self:center!important}.align-self-baseline{align-self:baseline!important}.align-self-stretch{align-self:stretch!important}.order-first{order:-1!important}.order-0{order:0!important}.order-1{order:1!important}.order-2{order:2!important}.order-3{order:3!important}.order-4{order:4!important}.order-5{order:5!important}.order-last{order:6!important}.m-0{margin:0!important}.m-1{margin:.25rem!important}.m-2{margin:.5rem!important}.m-3{margin:1rem!important}.m-4{margin:1.5rem!important}.m-5{margin:3rem!important}.m-auto{margin:auto!important}.mx-0{margin-right:0!important;margin-left:0!important}.mx-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-3{margin-right:1rem!important;margin-left:1rem!important}.mx-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-5{margin-right:3rem!important;margin-left:3rem!important}.mx-auto{margin-right:auto!important;margin-left:auto!important}.my-0{margin-top:0!important;margin-bottom:0!important}.my-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-0{margin-top:0!important}.mt-1{margin-top:.25rem!important}.mt-2{margin-top:.5rem!important}.mt-3{margin-top:1rem!important}.mt-4{margin-top:1.5rem!important}.mt-5{margin-top:3rem!important}.mt-auto{margin-top:auto!important}.me-0{margin-right:0!important}.me-1{margin-right:.25rem!important}.me-2{margin-right:.5rem!important}.me-3{margin-right:1rem!important}.me-4{margin-right:1.5rem!important}.me-5{margin-right:3rem!important}.me-auto{margin-right:auto!important}.mb-0{margin-bottom:0!important}.mb-1{margin-bottom:.25rem!important}.mb-2{margin-bottom:.5rem!important}.mb-3{margin-bottom:1rem!important}.mb-4{margin-bottom:1.5rem!important}.mb-5{margin-bottom:3rem!important}.mb-auto{margin-bottom:auto!important}.ms-0{margin-left:0!important}.ms-1{margin-left:.25rem!important}.ms-2{margin-left:.5rem!important}.ms-3{margin-left:1rem!important}.ms-4{margin-left:1.5rem!important}.ms-5{margin-left:3rem!important}.ms-auto{margin-left:auto!important}.p-0{padding:0!important}.p-1{padding:.25rem!important}.p-2{padding:.5rem!important}.p-3{padding:1rem!important}.p-4{padding:1.5rem!important}.p-5{padding:3rem!important}.px-0{padding-right:0!important;padding-left:0!important}.px-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-3{padding-right:1rem!important;padding-left:1rem!important}.px-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-5{padding-right:3rem!important;padding-left:3rem!important}.py-0{padding-top:0!important;padding-bottom:0!important}.py-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-0{padding-top:0!important}.pt-1{padding-top:.25rem!important}.pt-2{padding-top:.5rem!important}.pt-3{padding-top:1rem!important}.pt-4{padding-top:1.5rem!important}.pt-5{padding-top:3rem!important}.pe-0{padding-right:0!important}.pe-1{padding-right:.25rem!important}.pe-2{padding-right:.5rem!important}.pe-3{padding-right:1rem!important}.pe-4{padding-right:1.5rem!important}.pe-5{padding-right:3rem!important}.pb-0{padding-bottom:0!important}.pb-1{padding-bottom:.25rem!important}.pb-2{padding-bottom:.5rem!important}.pb-3{padding-bottom:1rem!important}.pb-4{padding-bottom:1.5rem!important}.pb-5{padding-bottom:3rem!important}.ps-0{padding-left:0!important}.ps-1{padding-left:.25rem!important}.ps-2{padding-left:.5rem!important}.ps-3{padding-left:1rem!important}.ps-4{padding-left:1.5rem!important}.ps-5{padding-left:3rem!important}.gap-0{gap:0!important}.gap-1{gap:.25rem!important}.gap-2{gap:.5rem!important}.gap-3{gap:1rem!important}.gap-4{gap:1.5rem!important}.gap-5{gap:3rem!important}.row-gap-0{row-gap:0!important}.row-gap-1{row-gap:.25rem!important}.row-gap-2{row-gap:.5rem!important}.row-gap-3{row-gap:1rem!important}.row-gap-4{row-gap:1.5rem!important}.row-gap-5{row-gap:3rem!important}.column-gap-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.font-monospace{font-family:var(--bs-font-monospace)!important}.fs-1{font-size:calc(1.375rem + 1.5vw)!important}.fs-2{font-size:calc(1.325rem + .9vw)!important}.fs-3{font-size:calc(1.3rem + .6vw)!important}.fs-4{font-size:calc(1.275rem + .3vw)!important}.fs-5{font-size:1.25rem!important}.fs-6{font-size:1rem!important}.fst-italic{font-style:italic!important}.fst-normal{font-style:normal!important}.fw-lighter{font-weight:lighter!important}.fw-light{font-weight:300!important}.fw-normal{font-weight:400!important}.fw-medium{font-weight:500!important}.fw-semibold{font-weight:600!important}.fw-bold{font-weight:700!important}.fw-bolder{font-weight:bolder!important}.lh-1{line-height:1!important}.lh-sm{line-height:1.25!important}.lh-base{line-height:1.5!important}.lh-lg{line-height:2!important}.text-start{text-align:left!important}.text-end{text-align:right!important}.text-center{text-align:center!important}.text-decoration-none{text-decoration:none!important}.text-decoration-underline{text-decoration:underline!important}.text-decoration-line-through{text-decoration:line-through!important}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-break{word-wrap:break-word!important;word-break:break-word!important}.text-primary{--bs-text-opacity:1;color:rgba(var(--bs-primary-rgb),var(--bs-text-opacity))!important}.text-secondary{--bs-text-opacity:1;color:rgba(var(--bs-secondary-rgb),var(--bs-text-opacity))!important}.text-success{--bs-text-opacity:1;color:rgba(var(--bs-success-rgb),var(--bs-text-opacity))!important}.text-info{--bs-text-opacity:1;color:rgba(var(--bs-info-rgb),var(--bs-text-opacity))!important}.text-warning{--bs-text-opacity:1;color:rgba(var(--bs-warning-rgb),var(--bs-text-opacity))!important}.text-danger{--bs-text-opacity:1;color:rgba(var(--bs-danger-rgb),var(--bs-text-opacity))!important}.text-light{--bs-text-opacity:1;color:rgba(var(--bs-light-rgb),var(--bs-text-opacity))!important}.text-dark{--bs-text-opacity:1;color:rgba(var(--bs-dark-rgb),var(--bs-text-opacity))!important}.text-black{--bs-text-opacity:1;color:rgba(var(--bs-black-rgb),var(--bs-text-opacity))!important}.text-white{--bs-text-opacity:1;color:rgba(var(--bs-white-rgb),var(--bs-text-opacity))!important}.text-body{--bs-text-opacity:1;color:rgba(var(--bs-body-color-rgb),var(--bs-text-opacity))!important}.text-muted{--bs-text-opacity:1;color:var(--bs-secondary-color)!important}.text-black-50{--bs-text-opacity:1;color:rgba(0,0,0,.5)!important}.text-white-50{--bs-text-opacity:1;color:rgba(255,255,255,.5)!important}.text-body-secondary{--bs-text-opacity:1;color:var(--bs-secondary-color)!important}.text-body-tertiary{--bs-text-opacity:1;color:var(--bs-tertiary-color)!important}.text-body-emphasis{--bs-text-opacity:1;color:var(--bs-emphasis-color)!important}.text-reset{--bs-text-opacity:1;color:inherit!important}.text-opacity-25{--bs-text-opacity:0.25}.text-opacity-50{--bs-text-opacity:0.5}.text-opacity-75{--bs-text-opacity:0.75}.text-opacity-100{--bs-text-opacity:1}.text-primary-emphasis{color:var(--bs-primary-text-emphasis)!important}.text-secondary-emphasis{color:var(--bs-secondary-text-emphasis)!important}.text-success-emphasis{color:var(--bs-success-text-emphasis)!important}.text-info-emphasis{color:var(--bs-info-text-emphasis)!important}.text-warning-emphasis{color:var(--bs-warning-text-emphasis)!important}.text-danger-emphasis{color:var(--bs-danger-text-emphasis)!important}.text-light-emphasis{color:var(--bs-light-text-emphasis)!important}.text-dark-emphasis{color:var(--bs-dark-text-emphasis)!important}.link-opacity-10{--bs-link-opacity:0.1}.link-opacity-10-hover:hover{--bs-link-opacity:0.1}.link-opacity-25{--bs-link-opacity:0.25}.link-opacity-25-hover:hover{--bs-link-opacity:0.25}.link-opacity-50{--bs-link-opacity:0.5}.link-opacity-50-hover:hover{--bs-link-opacity:0.5}.link-opacity-75{--bs-link-opacity:0.75}.link-opacity-75-hover:hover{--bs-link-opacity:0.75}.link-opacity-100{--bs-link-opacity:1}.link-opacity-100-hover:hover{--bs-link-opacity:1}.link-offset-1{text-underline-offset:0.125em!important}.link-offset-1-hover:hover{text-underline-offset:0.125em!important}.link-offset-2{text-underline-offset:0.25em!important}.link-offset-2-hover:hover{text-underline-offset:0.25em!important}.link-offset-3{text-underline-offset:0.375em!important}.link-offset-3-hover:hover{text-underline-offset:0.375em!important}.link-underline-primary{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-primary-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-primary-rgb),var(--bs-link-underline-opacity))!important}.link-underline-secondary{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-secondary-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-secondary-rgb),var(--bs-link-underline-opacity))!important}.link-underline-success{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-success-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-success-rgb),var(--bs-link-underline-opacity))!important}.link-underline-info{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-info-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-info-rgb),var(--bs-link-underline-opacity))!important}.link-underline-warning{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-warning-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-warning-rgb),var(--bs-link-underline-opacity))!important}.link-underline-danger{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-danger-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-danger-rgb),var(--bs-link-underline-opacity))!important}.link-underline-light{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-light-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-light-rgb),var(--bs-link-underline-opacity))!important}.link-underline-dark{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-dark-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-dark-rgb),var(--bs-link-underline-opacity))!important}.link-underline{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-underline-opacity,1))!important}.link-underline-opacity-0{--bs-link-underline-opacity:0}.link-underline-opacity-0-hover:hover{--bs-link-underline-opacity:0}.link-underline-opacity-10{--bs-link-underline-opacity:0.1}.link-underline-opacity-10-hover:hover{--bs-link-underline-opacity:0.1}.link-underline-opacity-25{--bs-link-underline-opacity:0.25}.link-underline-opacity-25-hover:hover{--bs-link-underline-opacity:0.25}.link-underline-opacity-50{--bs-link-underline-opacity:0.5}.link-underline-opacity-50-hover:hover{--bs-link-underline-opacity:0.5}.link-underline-opacity-75{--bs-link-underline-opacity:0.75}.link-underline-opacity-75-hover:hover{--bs-link-underline-opacity:0.75}.link-underline-opacity-100{--bs-link-underline-opacity:1}.link-underline-opacity-100-hover:hover{--bs-link-underline-opacity:1}.bg-primary{--bs-bg-opacity:1;background-color:rgba(var(--bs-primary-rgb),var(--bs-bg-opacity))!important}.bg-secondary{--bs-bg-opacity:1;background-color:rgba(var(--bs-secondary-rgb),var(--bs-bg-opacity))!important}.bg-success{--bs-bg-opacity:1;background-color:rgba(var(--bs-success-rgb),var(--bs-bg-opacity))!important}.bg-info{--bs-bg-opacity:1;background-color:rgba(var(--bs-info-rgb),var(--bs-bg-opacity))!important}.bg-warning{--bs-bg-opacity:1;background-color:rgba(var(--bs-warning-rgb),var(--bs-bg-opacity))!important}.bg-danger{--bs-bg-opacity:1;background-color:rgba(var(--bs-danger-rgb),var(--bs-bg-opacity))!important}.bg-light{--bs-bg-opacity:1;background-color:rgba(var(--bs-light-rgb),var(--bs-bg-opacity))!important}.bg-dark{--bs-bg-opacity:1;background-color:rgba(var(--bs-dark-rgb),var(--bs-bg-opacity))!important}.bg-black{--bs-bg-opacity:1;background-color:rgba(var(--bs-black-rgb),var(--bs-bg-opacity))!important}.bg-white{--bs-bg-opacity:1;background-color:rgba(var(--bs-white-rgb),var(--bs-bg-opacity))!important}.bg-body{--bs-bg-opacity:1;background-color:rgba(var(--bs-body-bg-rgb),var(--bs-bg-opacity))!important}.bg-transparent{--bs-bg-opacity:1;background-color:transparent!important}.bg-body-secondary{--bs-bg-opacity:1;background-color:rgba(var(--bs-secondary-bg-rgb),var(--bs-bg-opacity))!important}.bg-body-tertiary{--bs-bg-opacity:1;background-color:rgba(var(--bs-tertiary-bg-rgb),var(--bs-bg-opacity))!important}.bg-opacity-10{--bs-bg-opacity:0.1}.bg-opacity-25{--bs-bg-opacity:0.25}.bg-opacity-50{--bs-bg-opacity:0.5}.bg-opacity-75{--bs-bg-opacity:0.75}.bg-opacity-100{--bs-bg-opacity:1}.bg-primary-subtle{background-color:var(--bs-primary-bg-subtle)!important}.bg-secondary-subtle{background-color:var(--bs-secondary-bg-subtle)!important}.bg-success-subtle{background-color:var(--bs-success-bg-subtle)!important}.bg-info-subtle{background-color:var(--bs-info-bg-subtle)!important}.bg-warning-subtle{background-color:var(--bs-warning-bg-subtle)!important}.bg-danger-subtle{background-color:var(--bs-danger-bg-subtle)!important}.bg-light-subtle{background-color:var(--bs-light-bg-subtle)!important}.bg-dark-subtle{background-color:var(--bs-dark-bg-subtle)!important}.bg-gradient{background-image:var(--bs-gradient)!important}.user-select-all{-webkit-user-select:all!important;-moz-user-select:all!important;user-select:all!important}.user-select-auto{-webkit-user-select:auto!important;-moz-user-select:auto!important;user-select:auto!important}.user-select-none{-webkit-user-select:none!important;-moz-user-select:none!important;user-select:none!important}.pe-none{pointer-events:none!important}.pe-auto{pointer-events:auto!important}.rounded{border-radius:var(--bs-border-radius)!important}.rounded-0{border-radius:0!important}.rounded-1{border-radius:var(--bs-border-radius-sm)!important}.rounded-2{border-radius:var(--bs-border-radius)!important}.rounded-3{border-radius:var(--bs-border-radius-lg)!important}.rounded-4{border-radius:var(--bs-border-radius-xl)!important}.rounded-5{border-radius:var(--bs-border-radius-xxl)!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:var(--bs-border-radius-pill)!important}.rounded-top{border-top-left-radius:var(--bs-border-radius)!important;border-top-right-radius:var(--bs-border-radius)!important}.rounded-top-0{border-top-left-radius:0!important;border-top-right-radius:0!important}.rounded-top-1{border-top-left-radius:var(--bs-border-radius-sm)!important;border-top-right-radius:var(--bs-border-radius-sm)!important}.rounded-top-2{border-top-left-radius:var(--bs-border-radius)!important;border-top-right-radius:var(--bs-border-radius)!important}.rounded-top-3{border-top-left-radius:var(--bs-border-radius-lg)!important;border-top-right-radius:var(--bs-border-radius-lg)!important}.rounded-top-4{border-top-left-radius:var(--bs-border-radius-xl)!important;border-top-right-radius:var(--bs-border-radius-xl)!important}.rounded-top-5{border-top-left-radius:var(--bs-border-radius-xxl)!important;border-top-right-radius:var(--bs-border-radius-xxl)!important}.rounded-top-circle{border-top-left-radius:50%!important;border-top-right-radius:50%!important}.rounded-top-pill{border-top-left-radius:var(--bs-border-radius-pill)!important;border-top-right-radius:var(--bs-border-radius-pill)!important}.rounded-end{border-top-right-radius:var(--bs-border-radius)!important;border-bottom-right-radius:var(--bs-border-radius)!important}.rounded-end-0{border-top-right-radius:0!important;border-bottom-right-radius:0!important}.rounded-end-1{border-top-right-radius:var(--bs-border-radius-sm)!important;border-bottom-right-radius:var(--bs-border-radius-sm)!important}.rounded-end-2{border-top-right-radius:var(--bs-border-radius)!important;border-bottom-right-radius:var(--bs-border-radius)!important}.rounded-end-3{border-top-right-radius:var(--bs-border-radius-lg)!important;border-bottom-right-radius:var(--bs-border-radius-lg)!important}.rounded-end-4{border-top-right-radius:var(--bs-border-radius-xl)!important;border-bottom-right-radius:var(--bs-border-radius-xl)!important}.rounded-end-5{border-top-right-radius:var(--bs-border-radius-xxl)!important;border-bottom-right-radius:var(--bs-border-radius-xxl)!important}.rounded-end-circle{border-top-right-radius:50%!important;border-bottom-right-radius:50%!important}.rounded-end-pill{border-top-right-radius:var(--bs-border-radius-pill)!important;border-bottom-right-radius:var(--bs-border-radius-pill)!important}.rounded-bottom{border-bottom-right-radius:var(--bs-border-radius)!important;border-bottom-left-radius:var(--bs-border-radius)!important}.rounded-bottom-0{border-bottom-right-radius:0!important;border-bottom-left-radius:0!important}.rounded-bottom-1{border-bottom-right-radius:var(--bs-border-radius-sm)!important;border-bottom-left-radius:var(--bs-border-radius-sm)!important}.rounded-bottom-2{border-bottom-right-radius:var(--bs-border-radius)!important;border-bottom-left-radius:var(--bs-border-radius)!important}.rounded-bottom-3{border-bottom-right-radius:var(--bs-border-radius-lg)!important;border-bottom-left-radius:var(--bs-border-radius-lg)!important}.rounded-bottom-4{border-bottom-right-radius:var(--bs-border-radius-xl)!important;border-bottom-left-radius:var(--bs-border-radius-xl)!important}.rounded-bottom-5{border-bottom-right-radius:var(--bs-border-radius-xxl)!important;border-bottom-left-radius:var(--bs-border-radius-xxl)!important}.rounded-bottom-circle{border-bottom-right-radius:50%!important;border-bottom-left-radius:50%!important}.rounded-bottom-pill{border-bottom-right-radius:var(--bs-border-radius-pill)!important;border-bottom-left-radius:var(--bs-border-radius-pill)!important}.rounded-start{border-bottom-left-radius:var(--bs-border-radius)!important;border-top-left-radius:var(--bs-border-radius)!important}.rounded-start-0{border-bottom-left-radius:0!important;border-top-left-radius:0!important}.rounded-start-1{border-bottom-left-radius:var(--bs-border-radius-sm)!important;border-top-left-radius:var(--bs-border-radius-sm)!important}.rounded-start-2{border-bottom-left-radius:var(--bs-border-radius)!important;border-top-left-radius:var(--bs-border-radius)!important}.rounded-start-3{border-bottom-left-radius:var(--bs-border-radius-lg)!important;border-top-left-radius:var(--bs-border-radius-lg)!important}.rounded-start-4{border-bottom-left-radius:var(--bs-border-radius-xl)!important;border-top-left-radius:var(--bs-border-radius-xl)!important}.rounded-start-5{border-bottom-left-radius:var(--bs-border-radius-xxl)!important;border-top-left-radius:var(--bs-border-radius-xxl)!important}.rounded-start-circle{border-bottom-left-radius:50%!important;border-top-left-radius:50%!important}.rounded-start-pill{border-bottom-left-radius:var(--bs-border-radius-pill)!important;border-top-left-radius:var(--bs-border-radius-pill)!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}.z-n1{z-index:-1!important}.z-0{z-index:0!important}.z-1{z-index:1!important}.z-2{z-index:2!important}.z-3{z-index:3!important}@media (min-width:576px){.float-sm-start{float:left!important}.float-sm-end{float:right!important}.float-sm-none{float:none!important}.object-fit-sm-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-sm-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-sm-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-sm-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-sm-none{-o-object-fit:none!important;object-fit:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-grid{display:grid!important}.d-sm-inline-grid{display:inline-grid!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:flex!important}.d-sm-inline-flex{display:inline-flex!important}.d-sm-none{display:none!important}.flex-sm-fill{flex:1 1 auto!important}.flex-sm-row{flex-direction:row!important}.flex-sm-column{flex-direction:column!important}.flex-sm-row-reverse{flex-direction:row-reverse!important}.flex-sm-column-reverse{flex-direction:column-reverse!important}.flex-sm-grow-0{flex-grow:0!important}.flex-sm-grow-1{flex-grow:1!important}.flex-sm-shrink-0{flex-shrink:0!important}.flex-sm-shrink-1{flex-shrink:1!important}.flex-sm-wrap{flex-wrap:wrap!important}.flex-sm-nowrap{flex-wrap:nowrap!important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-sm-start{justify-content:flex-start!important}.justify-content-sm-end{justify-content:flex-end!important}.justify-content-sm-center{justify-content:center!important}.justify-content-sm-between{justify-content:space-between!important}.justify-content-sm-around{justify-content:space-around!important}.justify-content-sm-evenly{justify-content:space-evenly!important}.align-items-sm-start{align-items:flex-start!important}.align-items-sm-end{align-items:flex-end!important}.align-items-sm-center{align-items:center!important}.align-items-sm-baseline{align-items:baseline!important}.align-items-sm-stretch{align-items:stretch!important}.align-content-sm-start{align-content:flex-start!important}.align-content-sm-end{align-content:flex-end!important}.align-content-sm-center{align-content:center!important}.align-content-sm-between{align-content:space-between!important}.align-content-sm-around{align-content:space-around!important}.align-content-sm-stretch{align-content:stretch!important}.align-self-sm-auto{align-self:auto!important}.align-self-sm-start{align-self:flex-start!important}.align-self-sm-end{align-self:flex-end!important}.align-self-sm-center{align-self:center!important}.align-self-sm-baseline{align-self:baseline!important}.align-self-sm-stretch{align-self:stretch!important}.order-sm-first{order:-1!important}.order-sm-0{order:0!important}.order-sm-1{order:1!important}.order-sm-2{order:2!important}.order-sm-3{order:3!important}.order-sm-4{order:4!important}.order-sm-5{order:5!important}.order-sm-last{order:6!important}.m-sm-0{margin:0!important}.m-sm-1{margin:.25rem!important}.m-sm-2{margin:.5rem!important}.m-sm-3{margin:1rem!important}.m-sm-4{margin:1.5rem!important}.m-sm-5{margin:3rem!important}.m-sm-auto{margin:auto!important}.mx-sm-0{margin-right:0!important;margin-left:0!important}.mx-sm-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-sm-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-sm-3{margin-right:1rem!important;margin-left:1rem!important}.mx-sm-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-sm-5{margin-right:3rem!important;margin-left:3rem!important}.mx-sm-auto{margin-right:auto!important;margin-left:auto!important}.my-sm-0{margin-top:0!important;margin-bottom:0!important}.my-sm-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-sm-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-sm-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-sm-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-sm-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-sm-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-sm-0{margin-top:0!important}.mt-sm-1{margin-top:.25rem!important}.mt-sm-2{margin-top:.5rem!important}.mt-sm-3{margin-top:1rem!important}.mt-sm-4{margin-top:1.5rem!important}.mt-sm-5{margin-top:3rem!important}.mt-sm-auto{margin-top:auto!important}.me-sm-0{margin-right:0!important}.me-sm-1{margin-right:.25rem!important}.me-sm-2{margin-right:.5rem!important}.me-sm-3{margin-right:1rem!important}.me-sm-4{margin-right:1.5rem!important}.me-sm-5{margin-right:3rem!important}.me-sm-auto{margin-right:auto!important}.mb-sm-0{margin-bottom:0!important}.mb-sm-1{margin-bottom:.25rem!important}.mb-sm-2{margin-bottom:.5rem!important}.mb-sm-3{margin-bottom:1rem!important}.mb-sm-4{margin-bottom:1.5rem!important}.mb-sm-5{margin-bottom:3rem!important}.mb-sm-auto{margin-bottom:auto!important}.ms-sm-0{margin-left:0!important}.ms-sm-1{margin-left:.25rem!important}.ms-sm-2{margin-left:.5rem!important}.ms-sm-3{margin-left:1rem!important}.ms-sm-4{margin-left:1.5rem!important}.ms-sm-5{margin-left:3rem!important}.ms-sm-auto{margin-left:auto!important}.p-sm-0{padding:0!important}.p-sm-1{padding:.25rem!important}.p-sm-2{padding:.5rem!important}.p-sm-3{padding:1rem!important}.p-sm-4{padding:1.5rem!important}.p-sm-5{padding:3rem!important}.px-sm-0{padding-right:0!important;padding-left:0!important}.px-sm-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-sm-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-sm-3{padding-right:1rem!important;padding-left:1rem!important}.px-sm-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-sm-5{padding-right:3rem!important;padding-left:3rem!important}.py-sm-0{padding-top:0!important;padding-bottom:0!important}.py-sm-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-sm-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-sm-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-sm-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-sm-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-sm-0{padding-top:0!important}.pt-sm-1{padding-top:.25rem!important}.pt-sm-2{padding-top:.5rem!important}.pt-sm-3{padding-top:1rem!important}.pt-sm-4{padding-top:1.5rem!important}.pt-sm-5{padding-top:3rem!important}.pe-sm-0{padding-right:0!important}.pe-sm-1{padding-right:.25rem!important}.pe-sm-2{padding-right:.5rem!important}.pe-sm-3{padding-right:1rem!important}.pe-sm-4{padding-right:1.5rem!important}.pe-sm-5{padding-right:3rem!important}.pb-sm-0{padding-bottom:0!important}.pb-sm-1{padding-bottom:.25rem!important}.pb-sm-2{padding-bottom:.5rem!important}.pb-sm-3{padding-bottom:1rem!important}.pb-sm-4{padding-bottom:1.5rem!important}.pb-sm-5{padding-bottom:3rem!important}.ps-sm-0{padding-left:0!important}.ps-sm-1{padding-left:.25rem!important}.ps-sm-2{padding-left:.5rem!important}.ps-sm-3{padding-left:1rem!important}.ps-sm-4{padding-left:1.5rem!important}.ps-sm-5{padding-left:3rem!important}.gap-sm-0{gap:0!important}.gap-sm-1{gap:.25rem!important}.gap-sm-2{gap:.5rem!important}.gap-sm-3{gap:1rem!important}.gap-sm-4{gap:1.5rem!important}.gap-sm-5{gap:3rem!important}.row-gap-sm-0{row-gap:0!important}.row-gap-sm-1{row-gap:.25rem!important}.row-gap-sm-2{row-gap:.5rem!important}.row-gap-sm-3{row-gap:1rem!important}.row-gap-sm-4{row-gap:1.5rem!important}.row-gap-sm-5{row-gap:3rem!important}.column-gap-sm-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-sm-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-sm-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-sm-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-sm-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-sm-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-sm-start{text-align:left!important}.text-sm-end{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.float-md-start{float:left!important}.float-md-end{float:right!important}.float-md-none{float:none!important}.object-fit-md-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-md-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-md-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-md-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-md-none{-o-object-fit:none!important;object-fit:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-grid{display:grid!important}.d-md-inline-grid{display:inline-grid!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:flex!important}.d-md-inline-flex{display:inline-flex!important}.d-md-none{display:none!important}.flex-md-fill{flex:1 1 auto!important}.flex-md-row{flex-direction:row!important}.flex-md-column{flex-direction:column!important}.flex-md-row-reverse{flex-direction:row-reverse!important}.flex-md-column-reverse{flex-direction:column-reverse!important}.flex-md-grow-0{flex-grow:0!important}.flex-md-grow-1{flex-grow:1!important}.flex-md-shrink-0{flex-shrink:0!important}.flex-md-shrink-1{flex-shrink:1!important}.flex-md-wrap{flex-wrap:wrap!important}.flex-md-nowrap{flex-wrap:nowrap!important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-md-start{justify-content:flex-start!important}.justify-content-md-end{justify-content:flex-end!important}.justify-content-md-center{justify-content:center!important}.justify-content-md-between{justify-content:space-between!important}.justify-content-md-around{justify-content:space-around!important}.justify-content-md-evenly{justify-content:space-evenly!important}.align-items-md-start{align-items:flex-start!important}.align-items-md-end{align-items:flex-end!important}.align-items-md-center{align-items:center!important}.align-items-md-baseline{align-items:baseline!important}.align-items-md-stretch{align-items:stretch!important}.align-content-md-start{align-content:flex-start!important}.align-content-md-end{align-content:flex-end!important}.align-content-md-center{align-content:center!important}.align-content-md-between{align-content:space-between!important}.align-content-md-around{align-content:space-around!important}.align-content-md-stretch{align-content:stretch!important}.align-self-md-auto{align-self:auto!important}.align-self-md-start{align-self:flex-start!important}.align-self-md-end{align-self:flex-end!important}.align-self-md-center{align-self:center!important}.align-self-md-baseline{align-self:baseline!important}.align-self-md-stretch{align-self:stretch!important}.order-md-first{order:-1!important}.order-md-0{order:0!important}.order-md-1{order:1!important}.order-md-2{order:2!important}.order-md-3{order:3!important}.order-md-4{order:4!important}.order-md-5{order:5!important}.order-md-last{order:6!important}.m-md-0{margin:0!important}.m-md-1{margin:.25rem!important}.m-md-2{margin:.5rem!important}.m-md-3{margin:1rem!important}.m-md-4{margin:1.5rem!important}.m-md-5{margin:3rem!important}.m-md-auto{margin:auto!important}.mx-md-0{margin-right:0!important;margin-left:0!important}.mx-md-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-md-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-md-3{margin-right:1rem!important;margin-left:1rem!important}.mx-md-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-md-5{margin-right:3rem!important;margin-left:3rem!important}.mx-md-auto{margin-right:auto!important;margin-left:auto!important}.my-md-0{margin-top:0!important;margin-bottom:0!important}.my-md-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-md-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-md-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-md-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-md-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-md-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-md-0{margin-top:0!important}.mt-md-1{margin-top:.25rem!important}.mt-md-2{margin-top:.5rem!important}.mt-md-3{margin-top:1rem!important}.mt-md-4{margin-top:1.5rem!important}.mt-md-5{margin-top:3rem!important}.mt-md-auto{margin-top:auto!important}.me-md-0{margin-right:0!important}.me-md-1{margin-right:.25rem!important}.me-md-2{margin-right:.5rem!important}.me-md-3{margin-right:1rem!important}.me-md-4{margin-right:1.5rem!important}.me-md-5{margin-right:3rem!important}.me-md-auto{margin-right:auto!important}.mb-md-0{margin-bottom:0!important}.mb-md-1{margin-bottom:.25rem!important}.mb-md-2{margin-bottom:.5rem!important}.mb-md-3{margin-bottom:1rem!important}.mb-md-4{margin-bottom:1.5rem!important}.mb-md-5{margin-bottom:3rem!important}.mb-md-auto{margin-bottom:auto!important}.ms-md-0{margin-left:0!important}.ms-md-1{margin-left:.25rem!important}.ms-md-2{margin-left:.5rem!important}.ms-md-3{margin-left:1rem!important}.ms-md-4{margin-left:1.5rem!important}.ms-md-5{margin-left:3rem!important}.ms-md-auto{margin-left:auto!important}.p-md-0{padding:0!important}.p-md-1{padding:.25rem!important}.p-md-2{padding:.5rem!important}.p-md-3{padding:1rem!important}.p-md-4{padding:1.5rem!important}.p-md-5{padding:3rem!important}.px-md-0{padding-right:0!important;padding-left:0!important}.px-md-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-md-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-md-3{padding-right:1rem!important;padding-left:1rem!important}.px-md-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-md-5{padding-right:3rem!important;padding-left:3rem!important}.py-md-0{padding-top:0!important;padding-bottom:0!important}.py-md-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-md-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-md-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-md-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-md-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-md-0{padding-top:0!important}.pt-md-1{padding-top:.25rem!important}.pt-md-2{padding-top:.5rem!important}.pt-md-3{padding-top:1rem!important}.pt-md-4{padding-top:1.5rem!important}.pt-md-5{padding-top:3rem!important}.pe-md-0{padding-right:0!important}.pe-md-1{padding-right:.25rem!important}.pe-md-2{padding-right:.5rem!important}.pe-md-3{padding-right:1rem!important}.pe-md-4{padding-right:1.5rem!important}.pe-md-5{padding-right:3rem!important}.pb-md-0{padding-bottom:0!important}.pb-md-1{padding-bottom:.25rem!important}.pb-md-2{padding-bottom:.5rem!important}.pb-md-3{padding-bottom:1rem!important}.pb-md-4{padding-bottom:1.5rem!important}.pb-md-5{padding-bottom:3rem!important}.ps-md-0{padding-left:0!important}.ps-md-1{padding-left:.25rem!important}.ps-md-2{padding-left:.5rem!important}.ps-md-3{padding-left:1rem!important}.ps-md-4{padding-left:1.5rem!important}.ps-md-5{padding-left:3rem!important}.gap-md-0{gap:0!important}.gap-md-1{gap:.25rem!important}.gap-md-2{gap:.5rem!important}.gap-md-3{gap:1rem!important}.gap-md-4{gap:1.5rem!important}.gap-md-5{gap:3rem!important}.row-gap-md-0{row-gap:0!important}.row-gap-md-1{row-gap:.25rem!important}.row-gap-md-2{row-gap:.5rem!important}.row-gap-md-3{row-gap:1rem!important}.row-gap-md-4{row-gap:1.5rem!important}.row-gap-md-5{row-gap:3rem!important}.column-gap-md-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-md-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-md-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-md-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-md-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-md-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-md-start{text-align:left!important}.text-md-end{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.float-lg-start{float:left!important}.float-lg-end{float:right!important}.float-lg-none{float:none!important}.object-fit-lg-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-lg-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-lg-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-lg-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-lg-none{-o-object-fit:none!important;object-fit:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-grid{display:grid!important}.d-lg-inline-grid{display:inline-grid!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:flex!important}.d-lg-inline-flex{display:inline-flex!important}.d-lg-none{display:none!important}.flex-lg-fill{flex:1 1 auto!important}.flex-lg-row{flex-direction:row!important}.flex-lg-column{flex-direction:column!important}.flex-lg-row-reverse{flex-direction:row-reverse!important}.flex-lg-column-reverse{flex-direction:column-reverse!important}.flex-lg-grow-0{flex-grow:0!important}.flex-lg-grow-1{flex-grow:1!important}.flex-lg-shrink-0{flex-shrink:0!important}.flex-lg-shrink-1{flex-shrink:1!important}.flex-lg-wrap{flex-wrap:wrap!important}.flex-lg-nowrap{flex-wrap:nowrap!important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-lg-start{justify-content:flex-start!important}.justify-content-lg-end{justify-content:flex-end!important}.justify-content-lg-center{justify-content:center!important}.justify-content-lg-between{justify-content:space-between!important}.justify-content-lg-around{justify-content:space-around!important}.justify-content-lg-evenly{justify-content:space-evenly!important}.align-items-lg-start{align-items:flex-start!important}.align-items-lg-end{align-items:flex-end!important}.align-items-lg-center{align-items:center!important}.align-items-lg-baseline{align-items:baseline!important}.align-items-lg-stretch{align-items:stretch!important}.align-content-lg-start{align-content:flex-start!important}.align-content-lg-end{align-content:flex-end!important}.align-content-lg-center{align-content:center!important}.align-content-lg-between{align-content:space-between!important}.align-content-lg-around{align-content:space-around!important}.align-content-lg-stretch{align-content:stretch!important}.align-self-lg-auto{align-self:auto!important}.align-self-lg-start{align-self:flex-start!important}.align-self-lg-end{align-self:flex-end!important}.align-self-lg-center{align-self:center!important}.align-self-lg-baseline{align-self:baseline!important}.align-self-lg-stretch{align-self:stretch!important}.order-lg-first{order:-1!important}.order-lg-0{order:0!important}.order-lg-1{order:1!important}.order-lg-2{order:2!important}.order-lg-3{order:3!important}.order-lg-4{order:4!important}.order-lg-5{order:5!important}.order-lg-last{order:6!important}.m-lg-0{margin:0!important}.m-lg-1{margin:.25rem!important}.m-lg-2{margin:.5rem!important}.m-lg-3{margin:1rem!important}.m-lg-4{margin:1.5rem!important}.m-lg-5{margin:3rem!important}.m-lg-auto{margin:auto!important}.mx-lg-0{margin-right:0!important;margin-left:0!important}.mx-lg-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-lg-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-lg-3{margin-right:1rem!important;margin-left:1rem!important}.mx-lg-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-lg-5{margin-right:3rem!important;margin-left:3rem!important}.mx-lg-auto{margin-right:auto!important;margin-left:auto!important}.my-lg-0{margin-top:0!important;margin-bottom:0!important}.my-lg-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-lg-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-lg-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-lg-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-lg-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-lg-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-lg-0{margin-top:0!important}.mt-lg-1{margin-top:.25rem!important}.mt-lg-2{margin-top:.5rem!important}.mt-lg-3{margin-top:1rem!important}.mt-lg-4{margin-top:1.5rem!important}.mt-lg-5{margin-top:3rem!important}.mt-lg-auto{margin-top:auto!important}.me-lg-0{margin-right:0!important}.me-lg-1{margin-right:.25rem!important}.me-lg-2{margin-right:.5rem!important}.me-lg-3{margin-right:1rem!important}.me-lg-4{margin-right:1.5rem!important}.me-lg-5{margin-right:3rem!important}.me-lg-auto{margin-right:auto!important}.mb-lg-0{margin-bottom:0!important}.mb-lg-1{margin-bottom:.25rem!important}.mb-lg-2{margin-bottom:.5rem!important}.mb-lg-3{margin-bottom:1rem!important}.mb-lg-4{margin-bottom:1.5rem!important}.mb-lg-5{margin-bottom:3rem!important}.mb-lg-auto{margin-bottom:auto!important}.ms-lg-0{margin-left:0!important}.ms-lg-1{margin-left:.25rem!important}.ms-lg-2{margin-left:.5rem!important}.ms-lg-3{margin-left:1rem!important}.ms-lg-4{margin-left:1.5rem!important}.ms-lg-5{margin-left:3rem!important}.ms-lg-auto{margin-left:auto!important}.p-lg-0{padding:0!important}.p-lg-1{padding:.25rem!important}.p-lg-2{padding:.5rem!important}.p-lg-3{padding:1rem!important}.p-lg-4{padding:1.5rem!important}.p-lg-5{padding:3rem!important}.px-lg-0{padding-right:0!important;padding-left:0!important}.px-lg-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-lg-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-lg-3{padding-right:1rem!important;padding-left:1rem!important}.px-lg-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-lg-5{padding-right:3rem!important;padding-left:3rem!important}.py-lg-0{padding-top:0!important;padding-bottom:0!important}.py-lg-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-lg-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-lg-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-lg-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-lg-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-lg-0{padding-top:0!important}.pt-lg-1{padding-top:.25rem!important}.pt-lg-2{padding-top:.5rem!important}.pt-lg-3{padding-top:1rem!important}.pt-lg-4{padding-top:1.5rem!important}.pt-lg-5{padding-top:3rem!important}.pe-lg-0{padding-right:0!important}.pe-lg-1{padding-right:.25rem!important}.pe-lg-2{padding-right:.5rem!important}.pe-lg-3{padding-right:1rem!important}.pe-lg-4{padding-right:1.5rem!important}.pe-lg-5{padding-right:3rem!important}.pb-lg-0{padding-bottom:0!important}.pb-lg-1{padding-bottom:.25rem!important}.pb-lg-2{padding-bottom:.5rem!important}.pb-lg-3{padding-bottom:1rem!important}.pb-lg-4{padding-bottom:1.5rem!important}.pb-lg-5{padding-bottom:3rem!important}.ps-lg-0{padding-left:0!important}.ps-lg-1{padding-left:.25rem!important}.ps-lg-2{padding-left:.5rem!important}.ps-lg-3{padding-left:1rem!important}.ps-lg-4{padding-left:1.5rem!important}.ps-lg-5{padding-left:3rem!important}.gap-lg-0{gap:0!important}.gap-lg-1{gap:.25rem!important}.gap-lg-2{gap:.5rem!important}.gap-lg-3{gap:1rem!important}.gap-lg-4{gap:1.5rem!important}.gap-lg-5{gap:3rem!important}.row-gap-lg-0{row-gap:0!important}.row-gap-lg-1{row-gap:.25rem!important}.row-gap-lg-2{row-gap:.5rem!important}.row-gap-lg-3{row-gap:1rem!important}.row-gap-lg-4{row-gap:1.5rem!important}.row-gap-lg-5{row-gap:3rem!important}.column-gap-lg-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-lg-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-lg-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-lg-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-lg-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-lg-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-lg-start{text-align:left!important}.text-lg-end{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.float-xl-start{float:left!important}.float-xl-end{float:right!important}.float-xl-none{float:none!important}.object-fit-xl-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-xl-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-xl-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-xl-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-xl-none{-o-object-fit:none!important;object-fit:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-grid{display:grid!important}.d-xl-inline-grid{display:inline-grid!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:flex!important}.d-xl-inline-flex{display:inline-flex!important}.d-xl-none{display:none!important}.flex-xl-fill{flex:1 1 auto!important}.flex-xl-row{flex-direction:row!important}.flex-xl-column{flex-direction:column!important}.flex-xl-row-reverse{flex-direction:row-reverse!important}.flex-xl-column-reverse{flex-direction:column-reverse!important}.flex-xl-grow-0{flex-grow:0!important}.flex-xl-grow-1{flex-grow:1!important}.flex-xl-shrink-0{flex-shrink:0!important}.flex-xl-shrink-1{flex-shrink:1!important}.flex-xl-wrap{flex-wrap:wrap!important}.flex-xl-nowrap{flex-wrap:nowrap!important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-xl-start{justify-content:flex-start!important}.justify-content-xl-end{justify-content:flex-end!important}.justify-content-xl-center{justify-content:center!important}.justify-content-xl-between{justify-content:space-between!important}.justify-content-xl-around{justify-content:space-around!important}.justify-content-xl-evenly{justify-content:space-evenly!important}.align-items-xl-start{align-items:flex-start!important}.align-items-xl-end{align-items:flex-end!important}.align-items-xl-center{align-items:center!important}.align-items-xl-baseline{align-items:baseline!important}.align-items-xl-stretch{align-items:stretch!important}.align-content-xl-start{align-content:flex-start!important}.align-content-xl-end{align-content:flex-end!important}.align-content-xl-center{align-content:center!important}.align-content-xl-between{align-content:space-between!important}.align-content-xl-around{align-content:space-around!important}.align-content-xl-stretch{align-content:stretch!important}.align-self-xl-auto{align-self:auto!important}.align-self-xl-start{align-self:flex-start!important}.align-self-xl-end{align-self:flex-end!important}.align-self-xl-center{align-self:center!important}.align-self-xl-baseline{align-self:baseline!important}.align-self-xl-stretch{align-self:stretch!important}.order-xl-first{order:-1!important}.order-xl-0{order:0!important}.order-xl-1{order:1!important}.order-xl-2{order:2!important}.order-xl-3{order:3!important}.order-xl-4{order:4!important}.order-xl-5{order:5!important}.order-xl-last{order:6!important}.m-xl-0{margin:0!important}.m-xl-1{margin:.25rem!important}.m-xl-2{margin:.5rem!important}.m-xl-3{margin:1rem!important}.m-xl-4{margin:1.5rem!important}.m-xl-5{margin:3rem!important}.m-xl-auto{margin:auto!important}.mx-xl-0{margin-right:0!important;margin-left:0!important}.mx-xl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xl-auto{margin-right:auto!important;margin-left:auto!important}.my-xl-0{margin-top:0!important;margin-bottom:0!important}.my-xl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xl-0{margin-top:0!important}.mt-xl-1{margin-top:.25rem!important}.mt-xl-2{margin-top:.5rem!important}.mt-xl-3{margin-top:1rem!important}.mt-xl-4{margin-top:1.5rem!important}.mt-xl-5{margin-top:3rem!important}.mt-xl-auto{margin-top:auto!important}.me-xl-0{margin-right:0!important}.me-xl-1{margin-right:.25rem!important}.me-xl-2{margin-right:.5rem!important}.me-xl-3{margin-right:1rem!important}.me-xl-4{margin-right:1.5rem!important}.me-xl-5{margin-right:3rem!important}.me-xl-auto{margin-right:auto!important}.mb-xl-0{margin-bottom:0!important}.mb-xl-1{margin-bottom:.25rem!important}.mb-xl-2{margin-bottom:.5rem!important}.mb-xl-3{margin-bottom:1rem!important}.mb-xl-4{margin-bottom:1.5rem!important}.mb-xl-5{margin-bottom:3rem!important}.mb-xl-auto{margin-bottom:auto!important}.ms-xl-0{margin-left:0!important}.ms-xl-1{margin-left:.25rem!important}.ms-xl-2{margin-left:.5rem!important}.ms-xl-3{margin-left:1rem!important}.ms-xl-4{margin-left:1.5rem!important}.ms-xl-5{margin-left:3rem!important}.ms-xl-auto{margin-left:auto!important}.p-xl-0{padding:0!important}.p-xl-1{padding:.25rem!important}.p-xl-2{padding:.5rem!important}.p-xl-3{padding:1rem!important}.p-xl-4{padding:1.5rem!important}.p-xl-5{padding:3rem!important}.px-xl-0{padding-right:0!important;padding-left:0!important}.px-xl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xl-0{padding-top:0!important;padding-bottom:0!important}.py-xl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xl-0{padding-top:0!important}.pt-xl-1{padding-top:.25rem!important}.pt-xl-2{padding-top:.5rem!important}.pt-xl-3{padding-top:1rem!important}.pt-xl-4{padding-top:1.5rem!important}.pt-xl-5{padding-top:3rem!important}.pe-xl-0{padding-right:0!important}.pe-xl-1{padding-right:.25rem!important}.pe-xl-2{padding-right:.5rem!important}.pe-xl-3{padding-right:1rem!important}.pe-xl-4{padding-right:1.5rem!important}.pe-xl-5{padding-right:3rem!important}.pb-xl-0{padding-bottom:0!important}.pb-xl-1{padding-bottom:.25rem!important}.pb-xl-2{padding-bottom:.5rem!important}.pb-xl-3{padding-bottom:1rem!important}.pb-xl-4{padding-bottom:1.5rem!important}.pb-xl-5{padding-bottom:3rem!important}.ps-xl-0{padding-left:0!important}.ps-xl-1{padding-left:.25rem!important}.ps-xl-2{padding-left:.5rem!important}.ps-xl-3{padding-left:1rem!important}.ps-xl-4{padding-left:1.5rem!important}.ps-xl-5{padding-left:3rem!important}.gap-xl-0{gap:0!important}.gap-xl-1{gap:.25rem!important}.gap-xl-2{gap:.5rem!important}.gap-xl-3{gap:1rem!important}.gap-xl-4{gap:1.5rem!important}.gap-xl-5{gap:3rem!important}.row-gap-xl-0{row-gap:0!important}.row-gap-xl-1{row-gap:.25rem!important}.row-gap-xl-2{row-gap:.5rem!important}.row-gap-xl-3{row-gap:1rem!important}.row-gap-xl-4{row-gap:1.5rem!important}.row-gap-xl-5{row-gap:3rem!important}.column-gap-xl-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-xl-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-xl-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-xl-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-xl-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-xl-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-xl-start{text-align:left!important}.text-xl-end{text-align:right!important}.text-xl-center{text-align:center!important}}@media (min-width:1400px){.float-xxl-start{float:left!important}.float-xxl-end{float:right!important}.float-xxl-none{float:none!important}.object-fit-xxl-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-xxl-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-xxl-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-xxl-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-xxl-none{-o-object-fit:none!important;object-fit:none!important}.d-xxl-inline{display:inline!important}.d-xxl-inline-block{display:inline-block!important}.d-xxl-block{display:block!important}.d-xxl-grid{display:grid!important}.d-xxl-inline-grid{display:inline-grid!important}.d-xxl-table{display:table!important}.d-xxl-table-row{display:table-row!important}.d-xxl-table-cell{display:table-cell!important}.d-xxl-flex{display:flex!important}.d-xxl-inline-flex{display:inline-flex!important}.d-xxl-none{display:none!important}.flex-xxl-fill{flex:1 1 auto!important}.flex-xxl-row{flex-direction:row!important}.flex-xxl-column{flex-direction:column!important}.flex-xxl-row-reverse{flex-direction:row-reverse!important}.flex-xxl-column-reverse{flex-direction:column-reverse!important}.flex-xxl-grow-0{flex-grow:0!important}.flex-xxl-grow-1{flex-grow:1!important}.flex-xxl-shrink-0{flex-shrink:0!important}.flex-xxl-shrink-1{flex-shrink:1!important}.flex-xxl-wrap{flex-wrap:wrap!important}.flex-xxl-nowrap{flex-wrap:nowrap!important}.flex-xxl-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-xxl-start{justify-content:flex-start!important}.justify-content-xxl-end{justify-content:flex-end!important}.justify-content-xxl-center{justify-content:center!important}.justify-content-xxl-between{justify-content:space-between!important}.justify-content-xxl-around{justify-content:space-around!important}.justify-content-xxl-evenly{justify-content:space-evenly!important}.align-items-xxl-start{align-items:flex-start!important}.align-items-xxl-end{align-items:flex-end!important}.align-items-xxl-center{align-items:center!important}.align-items-xxl-baseline{align-items:baseline!important}.align-items-xxl-stretch{align-items:stretch!important}.align-content-xxl-start{align-content:flex-start!important}.align-content-xxl-end{align-content:flex-end!important}.align-content-xxl-center{align-content:center!important}.align-content-xxl-between{align-content:space-between!important}.align-content-xxl-around{align-content:space-around!important}.align-content-xxl-stretch{align-content:stretch!important}.align-self-xxl-auto{align-self:auto!important}.align-self-xxl-start{align-self:flex-start!important}.align-self-xxl-end{align-self:flex-end!important}.align-self-xxl-center{align-self:center!important}.align-self-xxl-baseline{align-self:baseline!important}.align-self-xxl-stretch{align-self:stretch!important}.order-xxl-first{order:-1!important}.order-xxl-0{order:0!important}.order-xxl-1{order:1!important}.order-xxl-2{order:2!important}.order-xxl-3{order:3!important}.order-xxl-4{order:4!important}.order-xxl-5{order:5!important}.order-xxl-last{order:6!important}.m-xxl-0{margin:0!important}.m-xxl-1{margin:.25rem!important}.m-xxl-2{margin:.5rem!important}.m-xxl-3{margin:1rem!important}.m-xxl-4{margin:1.5rem!important}.m-xxl-5{margin:3rem!important}.m-xxl-auto{margin:auto!important}.mx-xxl-0{margin-right:0!important;margin-left:0!important}.mx-xxl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xxl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xxl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xxl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xxl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xxl-auto{margin-right:auto!important;margin-left:auto!important}.my-xxl-0{margin-top:0!important;margin-bottom:0!important}.my-xxl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xxl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xxl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xxl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xxl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xxl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xxl-0{margin-top:0!important}.mt-xxl-1{margin-top:.25rem!important}.mt-xxl-2{margin-top:.5rem!important}.mt-xxl-3{margin-top:1rem!important}.mt-xxl-4{margin-top:1.5rem!important}.mt-xxl-5{margin-top:3rem!important}.mt-xxl-auto{margin-top:auto!important}.me-xxl-0{margin-right:0!important}.me-xxl-1{margin-right:.25rem!important}.me-xxl-2{margin-right:.5rem!important}.me-xxl-3{margin-right:1rem!important}.me-xxl-4{margin-right:1.5rem!important}.me-xxl-5{margin-right:3rem!important}.me-xxl-auto{margin-right:auto!important}.mb-xxl-0{margin-bottom:0!important}.mb-xxl-1{margin-bottom:.25rem!important}.mb-xxl-2{margin-bottom:.5rem!important}.mb-xxl-3{margin-bottom:1rem!important}.mb-xxl-4{margin-bottom:1.5rem!important}.mb-xxl-5{margin-bottom:3rem!important}.mb-xxl-auto{margin-bottom:auto!important}.ms-xxl-0{margin-left:0!important}.ms-xxl-1{margin-left:.25rem!important}.ms-xxl-2{margin-left:.5rem!important}.ms-xxl-3{margin-left:1rem!important}.ms-xxl-4{margin-left:1.5rem!important}.ms-xxl-5{margin-left:3rem!important}.ms-xxl-auto{margin-left:auto!important}.p-xxl-0{padding:0!important}.p-xxl-1{padding:.25rem!important}.p-xxl-2{padding:.5rem!important}.p-xxl-3{padding:1rem!important}.p-xxl-4{padding:1.5rem!important}.p-xxl-5{padding:3rem!important}.px-xxl-0{padding-right:0!important;padding-left:0!important}.px-xxl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xxl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xxl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xxl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xxl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xxl-0{padding-top:0!important;padding-bottom:0!important}.py-xxl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xxl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xxl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xxl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xxl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xxl-0{padding-top:0!important}.pt-xxl-1{padding-top:.25rem!important}.pt-xxl-2{padding-top:.5rem!important}.pt-xxl-3{padding-top:1rem!important}.pt-xxl-4{padding-top:1.5rem!important}.pt-xxl-5{padding-top:3rem!important}.pe-xxl-0{padding-right:0!important}.pe-xxl-1{padding-right:.25rem!important}.pe-xxl-2{padding-right:.5rem!important}.pe-xxl-3{padding-right:1rem!important}.pe-xxl-4{padding-right:1.5rem!important}.pe-xxl-5{padding-right:3rem!important}.pb-xxl-0{padding-bottom:0!important}.pb-xxl-1{padding-bottom:.25rem!important}.pb-xxl-2{padding-bottom:.5rem!important}.pb-xxl-3{padding-bottom:1rem!important}.pb-xxl-4{padding-bottom:1.5rem!important}.pb-xxl-5{padding-bottom:3rem!important}.ps-xxl-0{padding-left:0!important}.ps-xxl-1{padding-left:.25rem!important}.ps-xxl-2{padding-left:.5rem!important}.ps-xxl-3{padding-left:1rem!important}.ps-xxl-4{padding-left:1.5rem!important}.ps-xxl-5{padding-left:3rem!important}.gap-xxl-0{gap:0!important}.gap-xxl-1{gap:.25rem!important}.gap-xxl-2{gap:.5rem!important}.gap-xxl-3{gap:1rem!important}.gap-xxl-4{gap:1.5rem!important}.gap-xxl-5{gap:3rem!important}.row-gap-xxl-0{row-gap:0!important}.row-gap-xxl-1{row-gap:.25rem!important}.row-gap-xxl-2{row-gap:.5rem!important}.row-gap-xxl-3{row-gap:1rem!important}.row-gap-xxl-4{row-gap:1.5rem!important}.row-gap-xxl-5{row-gap:3rem!important}.column-gap-xxl-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-xxl-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-xxl-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-xxl-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-xxl-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-xxl-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-xxl-start{text-align:left!important}.text-xxl-end{text-align:right!important}.text-xxl-center{text-align:center!important}}@media (min-width:1200px){.fs-1{font-size:2.5rem!important}.fs-2{font-size:2rem!important}.fs-3{font-size:1.75rem!important}.fs-4{font-size:1.5rem!important}}@media print{.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-grid{display:grid!important}.d-print-inline-grid{display:inline-grid!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:flex!important}.d-print-inline-flex{display:inline-flex!important}.d-print-none{display:none!important}} +/*# sourceMappingURL=bootstrap.min.css.map */ diff --git a/example/src/WebEid.AspNetCore.Example/wwwroot/css/main.css b/example/src/WebEid.AspNetCore.Example/wwwroot/css/main.css index 3de3421c..255aa9bc 100644 --- a/example/src/WebEid.AspNetCore.Example/wwwroot/css/main.css +++ b/example/src/WebEid.AspNetCore.Example/wwwroot/css/main.css @@ -65,5 +65,76 @@ body { } .eu-logo-fixed img { - height: 86px; + height: 120px; +} + +/* Remove blue focus outline from accordion buttons */ +.accordion-button:focus { + box-shadow: none !important; + border-color: rgba(0,0,0,.125); +} + +/* Remove blue highlight from active/open accordion buttons */ +.accordion-button:not(.collapsed) { + box-shadow: none !important; + border-color: rgba(0,0,0,.125); +} + +/* Remove any focus-visible styles */ +.accordion-button:focus-visible { + box-shadow: none !important; + outline: none !important; +} + +/* Remove outline from accordion items */ +.accordion-button { + outline: none !important; + font-weight: bold !important; +} + +/* Remove background color from opened accordion sections */ +.accordion-button:not(.collapsed) { + background-color: transparent !important; + color: inherit !important; +} + +/* Keep consistent background for all accordion states */ +.accordion-button, +.accordion-button.collapsed, +.accordion-button:hover, +.accordion-button:active { + background-color: transparent !important; +} + +/* Remove any highlighting from accordion body and items */ +.accordion-item { + background-color: transparent !important; + border: none !important; +} + +.accordion-body { + background-color: transparent !important; +} + +/* Mobile callback loading styles */ +.loading-page { + text-align: center; + padding-top: 3rem; + font-family: system-ui, sans-serif; +} + +.spinner { + width: 40px; + height: 40px; + border: 4px solid #ccc; + border-top-color: #007bff; + border-radius: 50%; + animation: spin 1s linear infinite; + margin: 1rem auto; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } } diff --git a/example/src/WebEid.AspNetCore.Example/wwwroot/favicon.ico b/example/src/WebEid.AspNetCore.Example/wwwroot/favicon.ico deleted file mode 100644 index 63e859b476eff5055e0e557aaa151ca8223fbeef..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5430 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

UN7q>cvY7!FW%2Ga`ZFl!Dg zjkQi9ojXwj)&L{I;Nx7C(?fWPDL!HfXbFdJ#QSIroG?K@rou`Aj#5Jd%*;DQRbk;9 zMi-Tg4aE%50yE?JglKrng!!_B>tZ9N%O%w`fT3e94+Ct+3p)(N#H5b(19Yhf5{P_Q zYK!TgfSUc%vu@)wP^AXC>I|4+9gvs|yu=E4jQk-OQ$czEOmaGM8mLhNL^_z9M&zTP z3?r7i#+$0Upx%I%5QJk`M~>C)-F)WiwC&DSrUCk0yhD1rPVya_dSS9&Hw(=8%?N`p z#QT8t{)MShjvAf@c&hVsXUqblSZ58?8PEbV;N?3b3~4g7$>XEWg18$Sv4Jn%^;D$h|IH4sA4fSPd&p7t!4P^Tj61Ze$&2+6UE5lh!H zkb*FE62RT$G_aipFp^9;{iQI*cNuGqt6BqE1#u69CTB{JJ;m&6^s`0{td@ISz6>p9$7Z#Y6l+;-K zPcJAgF0tG#`L;24i*4MTL@ar8w-i`GL6K31pVljh_L<{BA8luW+KX21s4cWI#h(Gl z0Gi1q(UwvDbp-`hynL}0On=Mr-+0#YUwb-sJn1z~+#qo1n+)G!kvjt06 z+stp5*t?(1wP)Y>)c*3Xw{4`r{;TUAwF~cf%I+RJ$zGZKm3{WjBKu**TFaBGBH&c3 zqmWNxNLjmuubf3SBQIJ&vnU_)HDWq|QnGf16|Y(mmF29LBK3$Z{$alPu17T)6_*tI zjX!shWUA)6WIt6@Tx?&>TVfB6f7dP;HO_b%-8kk&d+@pUZ2GK)wn=VD0@V=%$`^s7 zTDG@FKWoUZb~N+zs=h>LO4N5;y>7E@mfNA- zlqNu(CwF7z+KmG70vkKwV;lAGM7dcn*xgS}vbU$tv9%l2Nk=L1)dU#rE)kf`*7Itx>HO&?<^}AZs`GSDI;l+C)$8 z%H-6*n)RD)vVi=+zu&VP?tjt7JpGQ%UAV#keVl59A!SV$KwYidTN9uOYBRQ8Z67-%}lyx z!ZmZAm4~B-tO5M!>9ZEt?;m^39(wM5Td-u670K@oStoc5SZU^ZRBELw1+;|3BIAJf zQQG;=Qi3BzSJinWCjX!SefvWb{E^6zEj5OgH=~HURf@~10L{fr-(%GyGI8zcG~jDM zPj92s_}rw=?4EIN+Kg`&TR~xwe=ejyjEP{S!DfXbs{%A{4zRdhz25JtP?|fCG8)*J zyTv9g#fcxx zwELe?SZ~2{6L!Lq7`KDYC?Fd&b5m~C`Xk6n#mx13x%7?UF>$81}f9Qk83yCGp|p#f4w%%)@`hQdO>CP z*WnnhMi1TBS5Jj<7%sP?3m{w#rihYu>@-lL1_Uab|HF^|e2_2Z{t%Xx#)Dvt1dn2y zjO73=!SG3T*V!bWx1ky&3N;Wpn0h;N{t|ostTkfmX}{pn_KI8EqL8SHxv2L#>%}@2;#BMT;!ij zr9WO;y1}Vbr$noZjG`@HwZSG%o@MhEtqemvX5rE2W8#uu-Zl%ZJ>Cf5BwOvK0A*xa zqp95lqTPCVbTN8DhU$DD3nYx)t%Q-YOY63?MH)9V^Z1QgLjT))mo7GP_#nIeqGM~t zqb?q5cm8IW-E-wh_VCUBXZK%wij6#DkR5mE9@e9CTWiuN%e*-MhVu&w?cM3~)Ja?) zbX+=Jjm%UHhQqf3y1fp9a!x9lfTSpeWjFHu;EV&0ku||VL6)|2t+-$B&Ng7TZg$v!o$bUU_O^3}P{~F!000mGNkl;(J!ji=e!Lw{!NTQ#%n%=k4Y(@MWz@}n`(vPIyw;QrC*y_9_?c^c^v{o%Ka zxyC}rPXm&uQo=WG-pbPY>}6TQ&b3CD|IxCCkF<<^kF>NdJ8hg!JS)@x(OW%KxWy?2ibnzXQHF&tC@6z8Qt@QiLKo~t<}S-V=|W4}c`x&F8k^OlAxabo-ITk)52l^em?9;C_KU#74 z36|Een}FQJJo(RNb!z~C%*sFSe>TrpUt-*Zk8SbtP$tb-BK`9`&$fbkFFWO^{??{N zlTgKDy}Ye!dIWBv+p1zsu+W)G$SKv|kcokitRZJu<_YIphT_H%-#3;xYJ|FoLeF|r zZobX_ewn@W?hLzk+?&RlZKEEVV6RS@X*~Ywl5eu#<9VJshfVDC!TZ_3emhxuI?WzV zt>_poSh`9UI&eVqMtEmxCInfVaH?0n6pt{Rq7+*#artw#*;Ay(Ul#V$uNT@M9)HcQ zxO==^{g>zM&toUqlo{Xpi=)&gzf`=|^E~U;sf`UjsIPCa!>S{`^NQ6Q?Z>qngYFoD z<6QxCD^;$UI6fpr*QH>3&AA#@{dUnx8~^5KcI{tZ5Rjkq-*0<$@+_OPV7VVIDk?59 z{@109RJ$iLBh3cx*~5Bj@FlFM2d>|yye+nH$?BjxhJ?L(1L$sQI68*fBsO7*nZdPs z{U!zM7uy5RzGuI_>lwKfFWI9nOt#6Ne`{;jZw`87DyB*bGqZ?{N2j(e?1aPjvKCEp zLMxg!Td)j32ih^rRE2t#0Xp#pR3u#)`9Ria)AH3D?9Gp6*)0#eY-ip4gx&tg1bh8M zS>uJvZT+S^2jnt^RkLlZVew&y?dki}b?a_dlX(@V7zSUma&6Enm|~&3186>-#ZDFC z#OE-ND&Bfn`r|sAF=w$o_QDh!aqB)aph-V$zGz)Fy zu&B7$@1O^Z=dEqTPqd|yaRW5|F_DU=UY+KDrRmDjklTyze&Ii+^5r`d(U6ReVx)nH z+>X}Gn`i(QqZXq~tI4KFPgB12Sd8VW=DJv%0?;j#5nFA-rIl+o+B2_zVz)i~is4d> zQPhHwy%lRVSbjmF>*NL0913<=Qw@nnWT^uxqC&P|g-!u7F=)0Ggb4b;B874u{?7;Y zlsb8fm#=jI4qD-*`A3B4Z28fi7mkH4EjE-5Kqh_{mzot675VEogz+c}MVm=fNF*&T zO&++00CeDmxB?PS10lGxajCQebX?RP-$HpJGNqOdd3+^_ed!L+vDazB=LnG3Z_W#P zvQYGTh~bqE&@ohIvSdXVbo~YaIz+!CEfTRX`k;|yX#^){>@-kRT%s8N`p~{&l@^Iu zqnu0uI{1z#fG&!is$5RVpn)Pqm-sAOp$0s{kT)$YVoe)mhX&{ks#KtZ?8@I!f@q*f z!_X@lNC3XAN+8T0L%1uQENUXy2MrQ1%O+gN&f@N?j>&`Z^jgoAUx}w zA^*8^+ZO(t=AIX*%R!~q0q8|)NC#*sAytVLqj=lrsn@32l9hCs!mBAOpoZjbfZ>Gz zdcLX`=rZfd-%*@2z(Ow{&-~v0_V4%X%Xv$Tuw9s<m^S`849TfZ^S-v4x--7{{Iy)|vNu`E}$HwAFAu|^1Y>)FB5(*o}hVwC3q zXcDUfsEf-ojvC-a;wL9ewOb$jue~{ScBp=HS+zaSvrh7#Sp_vDK+guyTO_~=v7;o` z0B_5)!YAJ}d2Px}8};xjcF)+i?87hS$89OJhyZ=S9y?j9W{oxM9k_A{-OMWk^s>N< za|Ogy1I(Bsu8bS8Ve=MSvSO`G_~1*s?yoP{^lf}_mL^E^d1 zvh3jgz5IzuQQlUc%YM|&s~w=Fg?LeMiTSH?<`)#oC5WDLx8xh&0Az8>@smEaQ4hUh zXWaC-{pPM`?TMGC+Gn#D*~Z*_!;g+v-RK1Fy-Qcysgnj&f{#B_O^ouEGC(u9BK30I zaabC7^P|~z-l(y5!pOfz&By)rK|AWA`)ugtf3p#{J!v;R@UlJj;uL%T^Le&#*=qlX z!4P*?b*QSG?5qqsYX4pQCC93|CfegsUiX8p%PRvkI}GkSP)jRe(c%?r?Vag!?bRu> zqUJZIer=Oyd~2V4z0hXOUuxekS!MXm1qF;Nge7_P<#?WFUD~y@L-*-r&BObSA_IhZ z^4mQoCM}nUGsTIc6$z8-ge4^|%t9P$F_F(zB`9yodGlW1$z5F-qBmwKL8QU>Teon0VaA$ESWl;EiL8c2&oY~Q}!?ASy0 zupL@At9=G_yMrO`_jD_Pjjf+X2H85WM*O|l0a{8(4oWO3+qG)qe`)JyyLS)EcLu9# zrG*({f7Vr*h1R|S=Vv zw{K}@4%yExJ^4`i&#g6pQW=c?cTWH*j4ctlFFnu8-#-7!B;l2;ZE?NrFLvpMtpTDL zyYAG%2lAz-9%?&wXk`LaJd}d5#pq&u%avaN&=L^DGx9?3kzYc&8{AIA)Bx`W9zI}a zyMDwk?3`f(t#iAUhP959HW)L(-C&kzIT0!Vy7cU9ZMlIdrAj(#h#K%z>5TL=>)58b z58_c5A8SK3tlhc=uT*hX7lcn3S*-IrGQJFZmy@6>KyT7jABdUTp^c-4rh&9b#M-rP zW=sPeeaTS!?deBY|6RLU(-Zu4EA!{1qh~H3SXd zC$n@{?{4kvmh%VOeOH}qryaeob={#=JUV}sshn4>aEUAR9wL?9AyW>BnPd8b&Xa*Q z$xA#ORi~&rF#dJPTh+QrE(JA_!KL)0Qtg059wzY{_-Fj zb!5Y49-@3W+3RQ#~V=Ywu@?#=D;p8`z4KA@Z6{$)}JH*1`2jj}U*g`(8@aHFgYYtb~vdhOWGw?4xT?Pq)z zp*ra!5zhwf*3E_w-ro-B*TdSYQ`9&oGbZxXr!H093qYGU$@Yh!8HR4xs;PBu-@bnyF?Qp5N5_DVv73|^`CfI#AnVt=vu~wTk7|i|y~hgxw_Y$FFZt`J=SiQA3R!o$FGOkotK?pf4clcyY<4scJ^@x*g*YQ zJ17*_I48@W9mhv~ylS-)_mhl`d?_j37vlFU+{T*gX_XdZ<{w8Luf522${&m8_nk0yJ3&)Qrq9dic8bDM%YfP6L5y^Ra7@000NpNklm?!sd!Ngg;zw(@W~vpSl`)+{ z;?l1uYbG#heClB^@~p z)LR4i%FJ~hCyXr+*UCCmdYu5xjbeNo9t*5ogVoj{M@|EE)&Sro=FF3SYF%@w2)>upb29ff6!fvWi0D#9P0(>(t`PAJuN|aOc2&n(vj0Z)EXdCMpWaUx?_$og&(bC z-KbuGrbr3EuZg$B@6bMPin~!nDZ)`y8UW0!1M^p1{kE{gzLIsPdIOrGB?y+8b;g+D zM<uWq86F^gnKu!4Wze+zTCIW)xR?<}G)>0< zxi*PTPQDmw05J2C^*@y7Gs46wabSimP(Z5!tc8J}#Oh1G*FH0;0J)Nm8h{4M+>F1e zt`Cb@iA$T{C~!a{x_*1h@`PhIPIuH@r5sWgCPE^{*;m*my1c%<_&p-=#gqyjaT z8ZiG(`@|q!oLY)VQ6eNECz}1rKF=I(g5wq6woIm^DW{jYFqkCN`_By12k1t#Y@D@ z%wxIUt8|P37&?ogKBiQzqq=DT_mR0f_vm&k^H*w5j%hbO6jwl(Rar{l@x&`QSL+yW zF7UZq2WlxJL5a5El{@_9>m(xviAV<4TD$S1ga9Xx60cKnoxj7Fcc(Sqco4R(c0cGW|9*zY$YAVK?8u6@9gk0^eC0@yM<~ethII% zhY}9ZWvW40;<9V*FI+2@B}$ z1TBki@`%G?GbeG?qYIAZGA|p;UU+ zyf{GI8aD(hlDHa9F3B(w0C}mzM%N>?Wt@OlrE=8ECty zTJa-dzI5v>wiHoMB7V#O33F_2QeI3VdqX;3Do%JHOJ1Ln3eZT5a>cw|&M+&AzeLA* zA(u~o(I~+yUj?QJp)2|%OcKLT+^BjHT_UbLPkQE@4kZsI9iYopD`gjniH0z`c)gBa zEaq)!i5jXq0zi3tgaNA$r4LprB5Hwta4oRNbof%DbDa`^RwHdAftto6 z)Q2kqpfA@+;@DUr;;F>4xkiUmngA#+*=MRHpSEWHIl8z_`+Rxf2k8nU0N`~fH7Ml( zO|z(>n8jD9?Pf7^LRfE)4$lob&tsP82rn7%_{Alm@sqh>k)1m5L&XKdrNAEs#5b$X zxMKH87t_RR#Q>S>bt<(e)__h(?Wh3(IIqvMqyj6XmNhb4z|BW$06v{b00A%(vM$>R z1t5tT6S5?3%)~B+rU|zJV1B%JtM0FezZY}ucDJWyvOxfP`=(RF7K>Nt7%%Ly@Coh+ zp{g@kx=y8yuj^w5SHh zYV+u2>LV*)(qZB;%|!L{b=~RWE5!KWtoDO-|3>=^KG4C!nlt+hmxV{ZPSu-uGVp{T zF3iiN%rxZdy|_n2aTrhnz#~-Ba4}YxnTaeG&Nuz|oCEVL0K85&JZi9juB@pAa=^fM zrKjjT-;KdN;So+K@N6ABRg5LX3IcwB#^W9!>QudmJm}0a@<~XIITXw{K25puV-Es=J@As;+*zIy}1gcJUqXla7YA z27rnh0C1xG0WLNGjB5T6STKMJKnnl>Oew7SYZls%AKO1M0&8gNsRO8}0Dx;+dQeX< z>Yo4rxF^EbNb{Gw7M50bFAV~y0apO@00{uV0pjPaZ>(WLxd%X7{qbEtN~Rz9PddO+ z>Yxx-RO`vzyMOZkuMi`|+ZO=myUQ!tYw><6a)p$C&M zML-I^{R6xH8y5L{o`1tX|G-XgPl^esf5`0Z1b6y@yC^t1z~7mIY4j-gbpX^kkb*}k zn9su>4yE8D3TA>kJNN+rwAX&{5zY`d3Vukz*L+Qk9#gOq0C4%5>tC?rUogTsh$1Hd z@Yu^c*ca;RhPZnl^5Cw7yu939E$2WFX9Pm@i30@Y;Olhvu@~Ii!84fBT7TF1M=8Lq zAGW$>J$4>-ygQStLE(D;O~LB`@^UZFApz&-@AU^4iM+NqW{^6|HlRYBGzBz z5HWIgarSlgq*P@_@iM5VE2X(TouCM)m*-um=Ra%V|6#Ge$nXRHn_p7^u#gO3vKIsV zG{gj;A@&1kfmZ=EHo24(sz2II?}{1VN6$0oUimk_r(jC@->?60p-!ZHrS^lm-u=OT zY-Dm5;_n;q15^6Mj|&}u0dNz*1h@m>0`LOv14IFm09k+{;Aen3KnDN@m;fvRb^r*# z4FCuD0s;YHfMRQ^<9R4=IFsFJBN zsoqnSQPog2QGKN9r5dIhr<$W8Q0-8iP}5Lfqh_M!q!yqSrtWdo-dnax{-=z%*7gE;PP05j3$h zX*BsX)iljC7#b|icbW~FV_JGzAnjdRQCdY>Em~7r2(1ro1nnEzOxjY~2HGxKEbSca zHZ7U%Ivod{Fr5OOHk}2XD_tPnYq~VLVmcIEH{BTB65RnkJv|G(AiXTT7QH3CJAEkq z8~Pl2B>nI7!}JUE`!~vE|*_!zdUkz zVQ^=7#*ofX&Cty-#jtzz>Q$br@>dP7La+YqYWmd=S3h5!yLx=>)-|DPkFVKW^S_pG zt>oJ8*T%2yUcY`_;QG(kt*#@kCtNSP-g$lI`q2&G4Y3>AH=J%n-pIPqaAV}g=FKZN z`ENeDX?HW^X4=iVn?pA@e!B9Lz)z2Va`-9Yr>vive!~B>&j@6cU<5P58Q(CLGxjpB z-nw*4;FkI=r&}*?72N8)wE(06@&X?NA;1^FLSQ#=iHV*`kV%`#ohgo~ifNE(oB0;A zG_x6V5OWrD8}oM-8WsT-Z59ufM3!0>9LotSC+j0tXVw_jYSt0f!`tk)f4=Q}JN7p6 z_So%{J9qD>-+|ppx`Vzm%|^>6#Ae7A$d=32&9=eL#IDE=VUK06WuM@n<`CjA;t1s^ z;OOVr=j7tl2%U($h^I)t2wwD>=p)ep(F)NyF%~gBvFBoFu}yJ4aVzm8 zaf~?mA?TsU!@`G?61OCDB%Vt&OYBGrNkSyEBuAyLOKC_&N;OK6q=luOrE{gffq)== zP!#AB=v+ot##g3Vh9Ju)`&2et7B9yn_eAcE+-G?@`CsHC1>dxy4>G|q4>r?CN=%?sU8Soo;8lb@dur@da zJpDxQiO-W3Lwds}hFONoMiNG0Mi^tDvAuD*@qx)NCW$7KruR+#O+T64G_x@)Gdna_ zGfy`EZXs?FZqaASW(l=yw7O(vW>svpZ>?^fYQ1CwvUzQTvlX-rvhB5Fv-7ZPv%hKY zU|;u??y33H@~3AGV26B%eMfD_Y{zZLW5_$my3@~2Z=DFv%FfBoD=x||$u6s|%C0G{ z1UFT;G`9`tV`wIn*~ zu9x1J!I$xC##yFY=2+G*S;%aT?3nDs9H*R-+@Eu+^EmV3@=kt({`T#?=KF?xf&8=r z`htLhI(k~LZwaR*DAHD#%j^( z_ed6G+y}r1{|{?5Pit_sy0xF`|E&F-Dul0E6YPy0{yXAa;8r-!jecaBPr<&H5Y<|iwsfoGS_($629 zHC4ibTUQx=WWXU%qsS;Rjv&gQzI^X=!LLQIZ)hU%LF4ls~fk_u%4p0OMt9BN{Rd z6(fL}k&1?q>Y@v1>J5w`rDE?=R)L`%m&5tG9FPigd*=q^*!&|aZp_z6Ho zk)EOq-A#J>tJkkk^rR%v(KBAUeCx_RAcF+cBOYc`RznBL5njaG`c%}IOn@Al&o3+u zf%4Ccr2J(S-aGo%VfoC`&=z-+o+m#xmR1|(e{KHm_I3@#?}Z6SpqPBR)#8V%*9j5Le@RY1q@|E}=AKK^Bae_7yP7WkJ1{{ON7 zO<~`ml&8!IbJ>vZRd{kvl_e+tOmkX%>8N{?O~sP~flQFOePyUjYllx$jsyg?at6vc ze*b-D*S2~PG?bRUaAp>JwtJ0XcZdsU!Kh+1-TH`o>}zGzJ(R(oE$FEKxoZ;;{L1*T zR9sW*q@V^?wog}c=$i8BdhfagdS<&HoV=fx>HKD3S{Oa1H1zDW%*DLsOShRq5z$l~ z`>{X9s%(E4VLZ#lx!V*Uu^##H0>Gj$!?z=sqq1vn+FnswyhStlygFaO0 zvY}(wHhQvF8XZzK7pYPSJA|HuO6?Di0d;Ai1*S8SV0Me#E8ED#x{08zxh;i4n4L^;>BoUhB@ zRO`B=Vh%FtGLmF4Wpe#0}81f z&R4T@M@`5k4>Jy$ybUrQxDVwDs04(<+d7*PaKbp!Qf1fK;OE`8yZ>K!P?)Ezuz&7r z>!OJu0{kt}(zUEK6cQ@~0+}msnYiCvTvl2S{*6V6hw#hD@H4ILoPjGJOFs#4@_R|E7#K1)sAK-r)&z z8xRRC|8&a=W}!?pQ!pP$EB4;e=qTN;i!5J)I7`G9~Zo zTqju2UFBDzTT03@<$ycFkX^ZLC{um1$vdOq58LmO;rXC$c>#qj=$Qm(CYhEsa=t`y zYQjY-OrbxV&8&uTB_=(q>jIFG?>A$<6Nzw4&hrTb^0htb#gFOwwAr{64-*QKKB(A> zG8!U=*`oDi!l#x_wedG*IR*QeD#1woC+91QGn?z*hVle-NOQ1>Va$M(2-Q|tq9vcH z-&A?EZ*_TjyHI(#i$!C++~cE&j-M%k-|6%G5KE&F*Jk-ir0%zJyg*t0YT3eussM=K zgW?%3?D;UB&JAh`Z7_&=2=DJLeQ+<(1_5uy_iB&rn`J_|uXcRue#kz!<~xAvV1>tG zmBVA#*Q4>wGh-ugoRgp?N-gu8G~7|c0m0ylRgZh<*TU2 zSPp7uD@^mxPqHofv>%8cjqkpkMQip z^A2qUh^mCcABfiH7G!gwhKIMRr*M-=Ky9(xMF{5A!*G*Aq>ia$SXgyP26U+w{bA86XKq=l^$o z?~;AM7ZQwiptyai%&nYjra{x(@S2#PKh{K>BetxZv;Jc!smdk%r#TN4Qm;#{Lt8O$ z_q=6$%?mAmjumh6GHDE7;BMn;R7nolqD+3~MrS=$2=AuQ<=|GxVFCWqh{5#P1;F!6 zA&ZEamOM?{*FJm`u^00FM0>I$k2o7;9Nu_ZZ0|c$rb0@bNwneq^Zwk3zD?^f!5N9u z;q?~;K~HidI)YRGU|};Oyf38iv_~~W**lz%I6HK()mhd)75;Z=@`5m1n$#P%YE zgEWt1BusPdFTEWNmaH3i!S#NvszoohJ5;)`SW`JZA``T;Ct-fvRq$(|Z2E4-1)#WF z%048>OvRFbt>(4d<;j`bLW(Z7%HDyX$8=83a@jDKSMdfw&jJ5+b0lB*W^#`!kEHFx9 zb>qw16*6eE?x3s|bGt&J%W7uq49mC&lUU51y4!QOla*wX)YMF9myaLf_3d53Xj>aC zzJGRtSf^uBW*QEPTwH*9`6g_61lxuxdVnxvm6~_OKh;j3d<}v|dxYSp4=E3sKb{aAzr1E+{2QIs0W5SraNCbIP1RYl9Ri1rpJW_wxr&rJd9G*0Q=YWQ{!h`i_g# zKv_f0I^CCy<^ogK_hlL;5^cKq@|GhN8>SCbf~Vsz0Qb?Z{;{JwV13tNH=N*bb-RUa zYh0U6N4B|bwMG3tK7W`{Nl*yh8wn}4=KH_7I@#x4Dym;W>6YU5s9M8Q_c>^J26A+LRy_erR|`UT*T@+K>G zXtjh;{qQ~RAAOoeXQ!``oAt=q!`zGRxVqmdaj>wva`j6~_hVBKtZkZOG$w&Gyet34 zmYLBs?22nZOKQLTQU`%_${C1e!1Uo&+`EP{7`Md3j{|w*rDkn%OpxqDz12xv+Vx#v zv)Xd9Rbcm$%ejfkLdzrB;r5A+Y5{eZl>XKs_9UR?rWnkgZ7sHJlNfJo%RGoRc5Fe- zaf>buvrcJZddcDiT(NuL;}$!M;p*uHl6|J1u)dG=cgG!L0$5Z3#6KnmTlXzqP&)=| z^02C^vdK)>r?LMe?&TH&JEWL>%$Kx2x}rR2N}(}RU8DNgE;WrLhA*y`ID6id?LV#d zcXUatGqQaiUv86fyDE3`*RP+yWD1>JDJ&@~$po!ACF$NmZTOjYGndCknR!U}X&23l zTE+_qfMOE)y7rokc6HS&6!cvL1QZ+{{eSRCF>7yE=fxvD9}}7Of{=UL0AZs2(jc%hVcj5XR#;w2x6CtkMAo-e^uEp`8-+LDcg+m3!9ZPeJ!Z z-$;S!zQ@(d_>-doEnMVO^c;uiIg!`$U>{W+G;-4i5GTnO-c)sLWWCM5XbRr@#Y&g#jS@?%~_k;DK$+P380cF2h2;o<3>v1u_OD{ z>Gnep`DGQ{3>lX>Aoyy17|R7m-~?dE%k>Qz9NK^TPbT}{d}U%@!<-)kY#s1$58-K? z&zR4r-w|DR-OCk;r-<;JNQJczV$wg(W&9tBZYo5*!|)KhwP8*QO+vQIg)1ZTF95&g z9$qD^>>2OjZ$DZ8aX91;f=m`QpL*3=fC830yO+~CUrbumj!k*q;^p;<>V`0a{1$zW zB;Kg|z57^N-psXW+vm5nYkq@4f6FsE@yt7VJ!6hjN+eQxxh2B(`S>AgETj@9r7ErF zlaX5*ok5vlNtSQ=1+EU(WxxU!Ue{`cYJ?hbtXu~jjs+S5TI`CK{rMB)g0PT2KEf=6CAA9nOy?^%OKIaG^;LC~rOvvHf6 z;IwHVP#ecQ9@1v#tJ`MqD9LodKB6jhAC!u!Z4ZfAPMQWMTk@OwWiQ$OaB1C67NPk{ zqa;R)cpl!DYQa2As4dz032bG5fB%qPKPe+mgmUpo!2I-wiT(go<8$N14(JR~0q1A7 zLK{tRnzISvobr4tt5WWKw8hf1flz;S6I<4A!H3p1*4ciEqKV35nJ85y!CY+QDmwJC()BDW398mvS^oPhY6iW6y_k??{e(gssbTEI7VCPI{1=E(O~?Q%v)X-CTve)oyy`>6>MM`O_o zoJugvOHCK7l(2H=E(h4#CsPDV*JNxb!Tttn{K&6ifKwf$rqk@NDWX&5>mHkXmADN$ z^hfnI^m+utKy9wtG4TS*5~43BJV~B4JvL@>BNQW7g8Obzru2VUPK%piXOAWVBWkoS z0LIGA4}im+#k=R<))KM|T6r7=98={X{lwGdh=7cSecP~;5dS1m76m?pOpB9Ii@9VP zonO7_Fyl~2v!+{)fh|zKdUMvl!6xzMr%7Y|h}4q!Dha&Qc~YS!ClAOS8Kl_Tx)My5 zSvxO_M?}wAb&=95{Sg{(` zY&~<&J2mhA*CuAR6%-JxaHB}gqf%eVkRv{@TiM^zd+M z++il9)fXc#Xv>~*%0}o{Qxod4f(Ntu&&Ytc^R7!=rc|1;j)d3bjyoM;U{p&g-Te) z;O&&lh+P^wVVOK(D~(eXnX~A|kCyGt2l*~_QpMRWbwICO z7$=vO+WF$%hKHCsM`{aurY$KhC3T0Nf0HeAfHk4~^Z3$f;f8*gLJnVC$?ru;U(S4H zAahNfdPz%_1Auc1pu+0RlX7KDncWrQG|Fj`Q@U z$r_0vL_yEOfRCGDwe@R8;p$sq{@Y-mq$G2plHKNee#U2xV~g!VzMc+6u7aQH_ni`k z&L#%J*(U8JD^|Gf_~qKNP2TLTgkm@j7A)6d39NH?<#-;sb!?$UqxCuwgtglIpsnv$ z68gThq!dz+_TYA#806cYCg^inetyX_hY?axoSA>jYWmO10s@X94kY=GzgYS&P;me~ zRe9*T8yc_(kH_WrW=}ZlWL{0BIpL34%@N9Dkgs;Hu)*`7223@1^2@oD$(=V2uYXvs zv`dQ@5X_GxjVVhgLE^A=GtLvv)gESrBsn-X0Pc$e4no+Gj#Sh~S5J6^w`(~?diEQ+w=U(x=f`FVkpFec5Bdra%SMkXOmDz33@cUns`rkmRM|PEw zv#y>O0LOc0=iV~*AMK~dBXE&2(`p_g0#@< zMR)zbRLcl;>!nH_KRiwV)9-hDY(6#V*=*{;!y7vrdXrIod8qAmsh;y_qIDf-cqS>2 zPqBtrYnRprld_@&|DQlPyq0UY5T$CWWW?VhzB_qkyvZQj2rX)Ms0Zo@9aYB=zs{xM zs$5`f{hF6oabcPJ?q_c?OI zw!@xF5z*FR=_}O&=PKF>Jv9<|AmhVa#g@s2C?(>Y%UqhOvAS;B!d}CdIQY!_5*U*%2q^EE9ObSnU}lqcNdTrq*x6I)cxHr*=5&SEfC^HNh@5vP3&bdCj+l zF-$%Phh^+$((gC@9Uy<$54qQ4t+w$z!AiPuq#N=6@U^hgiFWEhf(wiYNv+5~Q@A~2 zJ1EoIIe$LO?;9qPl5n1ZuL~he6dsGR_0)`2w8%^fhz)tx*^R>3dNRBA^nx7`b7DW5 z{I8D5>O){T*PI1+)Yx}#YP28C?H#nok)MZ7iI?OJgRwRak1L=4H9vs9Y7u-@?&Fu1 zP(onm&G#*~rZG&i#TxZ!3&zsYu7u_cO9fDQ^PX^OY&))uXEA(ZnInIKSZ7tUxrYk} zu;2n&SuR5dK{>S%cL6xKjCr_)d@v3p18VGDQ5N*(oSD{Sk=OS-N3F>;r$&5eU4* ztC+`0qC7#1P#I^iu87N(X9O|)@BMY;&9$1N`iwIv_p|6%(>ak-i`}^&sHLBlewMEY$~{PUVSR=SHsWv_S_*}Lw_@s zgaXZQXF?7uc|M1kgv?eu=P)K)hr5QivMvF+bwwgGRVwndr|_Y7p5V>LJ(7)%_9bt_ z7DA4K+*VALftd5MRb&DO-cIfVS)bf8;6)p3mo2%B$AZgl%)~Xb*m8rvSmbathvy|( zN?RSBaq6XjZ!Sr)M{))ll9wma_N-vL2k9F#^zah9o@ElD5@$2n8QObM{t(o`AUKH-=_y6C#FVb<1LW` zGA`e?ybWaRmFlY-O(zOd)Ci4V``d&%XmYeyK&tO-uW0o3aB1fe?2aw~Y$+$og^=L6 zn+hM1E^m8}8WKk~`W)v&$Lr<46b7Z~dRCX2s%hP;h#Djvg!g_)yZ}7S*dg!xP9G#i z`6OVKZA5ZVDhg-1PpflX>Vs0}5~ohBr{g0>2yMKlxz^@q zDi-7|y--z6$1o|9m|s8u`{{bbn_$7y86CTDa`{9=o_brmyogN!>(_sHB>k5!|L>E0 z4|Z0dyzyI0sIQW%$4*OvvMo@kv|?3avW+r*la#GWRu_39p9o9xS9dQLVlJjc^tme@ zezRVUD~i`FeI{Tb_WsWsswgWgaLZPGIPjCBqk!Pc!rO`^1AZe-5=1GhhUoNkOmIxh z0uabp*tM@GxI%Lpv~yIt@LZ%d^ zn$ZhVMPY_dj*;gYk;b7SD))VrXGNzLraXepQo_=Un}oeu4YJaU)}^d30N4W<8@MiF z6w%dXpVPU$rF`1b)}6;$mgTnxgTqt_B+NuiJd9jirzD3rD7 zojGm1UIz)pe8FJ&e0k)nVM&ID#wJF6-@LZHJ{YS|MkmK!3zU~-oY`19PSjehUr)~` zCMG?7;qs=9d3SKpJj}hntE(pk$U#}1Di(Z+F@6&=bqim+aOhr_#ZtjFGrd;zEo5YD zYL~&?6$Uo-EPT};pYmzXkF!c|xTe}<&DE;Hlxd`6eI)xGt9PC2ERnP{b*2NEv>GNF z^Amy4q5@sb6O*>$<^!*9(e--2Ol(_k1*{;7*(VxLr96^2fmU6VK78e%iV=uq8_X2V z@BE^o>~xB;#cV{1p-s1#o8tIwWAC6c$_J5>G96&i9n9AXK0V*I=**_ls2&x_9CvU7 zM;xol(1!s7`!qx~6j`T07_Y~SREbuqi@U|zx$0PrOmc9pE{&rb#*LK)l|x`a%Bru3tC{`s-#mP3Zpo$B@6@dJ z@PL^6O6pIS&c%(BCmUPqRPRqNOwJFyZfJvY%I7QIDaPf%Lp5})+A8!W9BbGJjMwjz zfLlI;flSkO_%<&IpvrPvIh2!;Gb3h%hc`}#b%$l6MnqL?sFp2EmlkDaSKHj}2Ng}r zgjNWxm`OPKYF<~g<*kRA_T>lh2&GpC4mBuc!NO-wRg~KqpcB1m`xu>d3C4-GVqv!a zE{(b#7v5sK_~Id%bw0;VUAu$TDn}Pr%FrH-bA73I!p_d|9VxM+^!f#t3w{aVg)Ds@ zFSL1ke?B*jE>|tCKNtE`LsDkFB712VXlhiwJUgoC)4*=i60>4`Eh#pw!zHVdwXH=$ zYP<**jH*(AT6O956ByaPXred8nXk zD_Z|bTDa4+2=0L!!ioO;39;1+7H^q;Dr_LwUuWkF)J5cQ?So;NvF-QWSoc)VH9SaL zO*6B3Ti)=9!`whrgEf{#zAt&gwbJS!8aFYYiM0&{)$i-9lOh}CU2hDYa+vpSm)&*| zW@&2y#rq5<0m))xt#uYVcU2%gc3MQ7s*H7`VtDem8Y^dryI5cr^GIwme-eS6ao6Ni zk8IhO=uBj6b<~KM_B`HSesH-(KT&4fQ)9-j)tY0J`ubkB zZG72U$?u)Yq?m$6=EOImMLfmQEVf9(cvL{P%vR0!_%~NV(PxQ*FnBmm2uum) zr5EV|Rnb;I?VBP-DHUgBh8hlsY_}rzz&1YkI=9SqjJ_k&@sPpr>0u|Re6aHV2FBs` zX5r_mukt9F19^~IlM%|MhDSUqbIbzYX$s4JBNN%w&cD&LoL)sviwtmm)p0B2oI@1* z$q@wO4pWk!*?EwixA`6aYF0`7-ka=3-Z?H_vG~Z;X)hg6wT`}N>Hyuzqi=GV)EZY2t*c#K(FGt4eV+xM=K6ltcK8Rs9!*6=)VNT#3WXb?c57W!Zxbq8UsWr;E zZ$ZDE&3K48Ixj86btz57(emjJL;avTp1}4~3|gnF(^uz2$ZeSI3+1dkmPfE|6X@SP zM_wG-Y2WwbvMrh3ok`-f>ZgPhR%F`9j6?IZ-Rlu53|6j4ebHmXqK>P6@C$(Nz))?w z0=yGtWVdQ)l~`Jgi>&}dQbP<$LT3uR>}MTDeT!t7t!*fKC8CeZ;z0BWggpCD)ztAk zi(_IC&tC&h4lgHjiXJ()%Pi!Cg-zWQORHU6r<*2Egf85=JnT=n01UEKzW^VC)v@0X z(lGoa`;@J{s#*hfu<3%6PhVnDFxHb(cmm2vVKxx)WqQc58Cf|Lr>lGqbNc zy{koPVaC5Kw5z{Wg1@s-pk7uP74paZxv zTbCaAFdq>wE)l!$C=wlg4lHBzSF?{&Hvy9ujM*+iyhjr1n$LDqAzLcs;npb) z-H}F7#_bPwX>+nj?wDlQcw4BI5vny5=vK90+3G-|$$-n6FMWf2Ul^&Ij5(+^gOo?M z*!ouzdR*!-+L;xU?G1l>W_$NMvwUe7?1;N^-R<~J^CU2FF-^a;F)Yse%li`a6jIu) zvPm-bl8PqQc$8-y$RC+kKYJZFBn#?3jP)RfVwv}{WqREz z)pv{_x&u6-OmTW6iUAo_C3T=d-vrX;v8^N?9vLO?$OGpF*Rh>Scx`s|cMWETQ#Lq2 zF`e*t)&ZqXb~m>2o4791Y!WZ32|AOi$075~l%yoIv2P2r~3=!Tb1T z%CO66iIV>~N|Z7iGY;_3kIip;n-G}-vZ|YJcZwf~v5Pk-t|@PBF&B~ToV_9M>f4YJ zU{l+GX=sNzSIg(XV!fD&#$2s1?Rx$BI;kS0TV;1BsME#IFke|yo-4g)eY{9INp z6qKK19pA2WL`>v&9Ie;;u_yE+wxopQke6NC>Apq8A3LFx2s9x*wf4YorTBcuv3R}q z^rOMM#+ocY;radMSjt`UZx`)I`}V_m#^V#^Fbo)>Ixw+T>yL0u^I!%Xd8J z_&sMCT1Z6Tl8Rh#y|Mndx0MHuKBP+hg_`k7b!&(3E0Mub@>xV#!^t-$`1$?4E_FCt zsIf$4cO$q?xq5W4tfnzTbm;2j=_bSptw0K{pIrAuq(v8M+1r9drB)I$YK86ISlWy_ zDi$H|woG3mh&%mCed)x8rd^F|mbD{keG-+Bsw!sGGAQ0LNeUVdv6aTLnoCa2!W(+u zajNhWbghJeCGM1+lx7mva;`chKT6(fV0wSq%ywsUbxNtyDr3_>j}2W>(C`&xtZ&U$ zU8_}gVD!0s=|CWUxa^!INbp?cFtGFbyN^qaEtXr{Yb)(3@A&ozQpKr-kpZdEl~AB^ z#X5AD>o9OKd`4utt-HQ3oK2GfKg{X3RKKol!>bklv)$$BzdoV<2Z0FHapvuE9t04@ zO*3q9pv-8sT<%s{+RhLgwV~IwezP|^eI$grZ0beLewN_#mXIS;bxwK{O3*$@1?RUAB3OvQL<@xP(7JpZMA1EXcLTFN+cnU{qYvRDelg3 zx(NF82^X$Qd01^PU~EG6psG`=*jLgc!XW*?y6Kuzl<8~A^BQ!0C)V_rc6gqAlp!CV-^kX|7(sx7t z+jpw?O^r=r&xlpK`4oFS%c@Q6szcJW@c@>n_1>jc_3X;rcDjN8jOe=un8|79D&BI1 zm0^noRlX3*YTaq#mQ3%rNyn@qn|;k<*Oy_nag!}m)rYprRt=8d-b&Ai>Y~Q0()>v# znwkb<7L(gT!eg_a4LRpYXc>Lz2h6v`h%@+(dA^Dr{(bOa=tmoqk<+uBro#uHRICw0 zy(L8)e&L^oWCfPdVVgu_L^a;5%BI?n7vHJ1unhsOgBTegnSMF5lD#BZdC}yVU_4k2 zp%$Fa!zwPkIB8&O&@;>FgCF2j$spvVIgi}#`N!>bs<*_R1JB|S(;_pf(6Zm+>`F#Z zWM`4M>Iytq+(f6t)*6YIYJJx_NjCPx;X!4~gvt|`0xrVhY12?Z0LMhLr;iyqB2F`Z zxDbO>OCKjM1x#Sn-DX-`n{_&vy;uU;wl4r8^@HZh9kv!L?i~YaV&u)BXeOj%9J)eT50Ph%O-#^e0_h`s9|ZW4UF(>%>6A%x}vn z8)hj*upNZ8{Z;^qsCnf8Q7Mn=KMj=eX!L}ojT>P*%oswlm`}=IF~ac*bfNB0{#RT} z$$}W+;CcLC+VB&6W59=Uavg+k3;m!IzbXi6%+ zBlOV3<{h7Pp3VcEsXTDgs}NnE-fG6K6Ki;LLf)4GXXzVj+9lfjIVYc^d-iz{9i~{5 z)~Dnw;rXcGgRFMHOh~GS4^)pSBW|fL^q_WNt1wK-&uYH-ZAP!>K(T0|Rz}Tf#XMb0 zOW)4?gfY1U)VSD?!d$8Gb_1X2J}eb{(3U>;X<{&M@Ij#Z;t-qPNoB%rV0r&`U$_+f)r`_Y7wu$x-=#qK!l~g5#%vgMoKWIYMz)tc4prwr68WJtOcKgAHsqZDOH*BP< zLM0eKWjtWh&PB{}%0l(lRV?b)L1?p@%D;z; z(g7P&4|UsD?6=q*u8pZ+ zg6li`iPl>Zb49)rbyC)gRgMjd@a%IcAJvUNaRo~~;jhesx znfu=nHS3`oQ9Wjt9%I}xsZ7g~p#QbATnO<<``7MULB=@o7uHW==9tzx43op`-f6;u zg6B01bb}^01>g-CdxP@1%@=^EKKry*|2Pvn-XkwNFUx6)FEvFkaPUZW`RnbeFPt{y z0{6TW0^mAI(;;8N<1z`ltp?mLQ`8(XV)x<9-006h#W^pM0!1VcqGW$ zR(hd2`dRxlM(iDP9>=$4=Ysd7c>LM{C;Z2h*PS&d3TUOC|mSN13n+4Cj?)ZJGx zj$9Yz8YPQ{@M0APa}0$Ko7=BLFvWHd&rx!9UFR8&kf#Ra3V!MM*;ns{Xp3&d7h%^% zJ6F3h_X_^7TE#h%-=MSOSTCeEGPgQ*ix2~l2U21JylNQESAH(PSVj-Q-g9p z`=m7VIcXX@@=rJy%vR&Ihtn(K0m5kkUTjH&Nl0Ir~II`}+~4YY|y{;~ZScLC+`T$VFy&D(RzRW4j*rxCkx9>J#= zU9&^@lHOHcv)aVFXrnLVS?b;%ZYzae?&T_X7(?bmg!VxQTaE;G<8#91ev`&Z4NKg7 zeL&5OfPlDN8BPwy3FNYu?iy&DHrO%GhMyW+Vlq?4d$nU?s_J*ZoPx!5J75%$vE}oG z!GAI0Ul_k3%~ZGQ(2+Giks}(}#lIsrH#%UdS*C`wUXukI>&JN0XFmB)|0v)`@^IPD zbUK()WP9fBtB5jHuAS8zP$()ZPyL|Qg`e}AiiQN^WhZxXa^cI%({JP?Ci#h*BdJ+d z5n3_tBJl7OzqH00jQe4F6{Jr}K`pFy{A^`G@hG)dae3e)U+V31#Ro7Vb4obnJ$?i+ zm9shqsg~n|j6#*0*kjRgs{Nj8@TRtT)Bv|uv3fg`V6*y>bw`*-$#HZflQxU(J_~if`pb;Ff zrf#Bc-`rPz0pK-1z8*q;ujDs*1bdOT*bBMGwA~*c z=BvqTO4%TwoKlU=%(J?3D`o|Fk3L0kRW+x zQ5mp@wHHAURBRPct2hMK6927-InyyTG0F;kmAVl-y&X#A1kDWjqd$+ARot(*2Ui1j z@`5xawWbo>OHfR!A)No+0cQq=eF*tfG4Pb}=F0lBS~3H9v+tnp7-hlm!}e)cn2?6SupkP;nYLD_4F;#>ti-Ck};D#9&krZh?SO? zjjXChzuV$cF;709iASMP3s#;y7l0ciP7u*(FViZr1&V9NxJ;+#PWw(J$jD6LWb1-D zl?0$A?(Nzmj+FTYhoCl20I@Mpw7rhfydjbiA@~KJGq7oy^Lkp+wn_z&MsRa3b@J50 znY_+Pmeg`G3H)WRDbX{5vRvkE3 zTUP5}Tv3efGiezVjxMw%Z_b>r*hUEh%ROGVB$``R;w@J3I$d43Ai^h@+X%#BxL&?( z5(YWWcsnU8<%=}GN;lE%l{_ixT(#$e;RgzYTZbK*cJuJ4t$Z$R&JS}QEjyznm!0;` zB@D@mEu}y7l-QK)o^OIBWA6ymXm4mJyXjL@u1&j5VJ7#+&er_B!H$KwFl3 zWLSgEJJ)yLccwC=^j$*D>+v<6<;`XSQ;}^**QmOQ7@Tx(WX4-%$A-9PuB|s?E8Cco+J5bt z4M>pLLpg=E8r#~G^bB#SmL~YmH!Q%jG8Q^xWl$)no?%cmIz7uOG_ducq|Sm6JyO$I zgqrT(}oY;%5mcf7QSi1mptL!aK`Woo>HCR`s*Nmhj zAMI{e!I+Plb~NVetD9laks6qHk$-x5&6ew{^NFR@-j?Z=zSxHPelt-6DJi5&>XDyR zajV>5QV+IJuR_>oWzzl4V6%RHQPVJSUbT3VMcY)@(#K+Q4zKj~iSeg|$Z-TJHXd(K z0v+wJR4hx(GJZFo;1$L?1FhEnsEcLN+c%Q5=U)~co~ZAdG{8G%9zZOcQX8%>Wp=erZgQ;Q0gEAr0R^JHxWV!1eDN2Kza$_ zs3X;c0i*>9W9R{blq4h&P$`ivErEn0odBWtq9^nI-kEW}_dC})Grx1L^T+ve!L^_4 z=mA%%w*K^-%gU#ERYu9Ht*UH!Qdky%TVTr%H@mQ4OXcg5WB?%7uzxtWIk0Wns z1ZD-VvLYPS5U!fM#Su@F3hLL5g(a*Ca>V)dcQn+(9?h!!;r_XC_V#EZW^sMy#)v!Y z^eQ=bO+*x6cdTcCZh(R7l5z7{sj2wz`B_V^`66L-`SZ%cClX#?AYOldiT`5%O`?ck z81o^3xg_u)=F$}X&@sxeVn!&WvJ;4psIqDFbnaEEwko33+w^O=k-=z5aQwEUrS(DL z$13%UYODH9V&x&~;$xGpgPh3;mZ>aisBpq$y0suR->*d->5eP|dj$cJ>=1DBGf4@)copd0r-Z!LS1hgIy-C6KY$XAStI$fs?`Tcdz3@gVU zQvK@mk%R$4m>DS1f+Z&;V=tX*p?y^TisrR!3%kpS-kvhK8gHc}FtSTDV?LOf-h*IK%0qJVe=$)9h+WdrL zZ{gmIX7F8SiV2+%izi|P5soP*YyfK46um+b|=XgIIHT32&ZSRD)At00_@bcLwk zt#7NfknifvUChO%>lPUbZxLE^Ei4D&w2jm0nIXoR0jRkEB$dd*1&092O7c4NGAd0R z0SSGF#WX->=|Wg(hgYc6LqOm(1;K{mW?`wf-Tq|Qj9Fti@gWs(-VRq6PUiU?#n zt8j@fTtFR`?<}FMiJWm*wQcpRzN-m{!w4i^)(7gwzb3}zwi;n)8u&Ltg&Fk!$chd+8m|lQ~*ys^>`F45F2Fv-O zD!>~+Pq9exu*sXU`8(=A{gDGaGtk}B+kEwRT?U(7=m+tStwDDf7VqW?3o%^AR)s;c zz7of30MCu4S~cf;)2VLF%7jq)YR<;cfAqCC#?DXOua09 zqnbXDk&%%Z7l%?(QfiTserDnqBaFp{lgB_-+r}W-G{ehDpf1D^m zEh~_P1CXI+*}^m+H{f`R9>`92bQ!tyN6!2s%!KRB%N-Ye;`u^#og8vfS^Vm3@hKr$ zf(FAm9Tz*W;P$Tg{4A@4)wjCglF%aquyEqTbO!oAnn(%WD!DmG?RiIZ3W8VKZ7cHE z(4pH45PhTiG|J-;OLJjzd|7a!*W3&aTNp2c#XkB?Wb{8@wV!zO8vM37_1jZa;z2~(l)C zSMF%HtV$+_Dt^Dy`gkn{4#8h|F;wI)W9u+eZ)7v@BMOrYaxRHrhEc|ocq7CIZmC}S z@9z4qbAOkkl5&U(O?96}?K#PjpK2ICW-9IH&gUf+?Xc|p3xe>-z^%;|cDAN~id7(Z z0Vf=H=o;FS)7Z7d@n2jnSkPHM>&C?aY0;}$(!20Mye#cJdiuB4Xg4YlqW5C|VR9z) zP{z5wP6A5sK-(1J0a=W0(;&Tr!VA>Sp8U*_sX&=oipBzB46_jPcH>mtu-~3An7bAmVAW^s*|m3ub<4t-FC0MzHau@ zUoz!b<3wktfzSmD0JJb{hZUx}!wkHWXPnRccb%HK}>lnC9p_ zgLvpx0GE=~zUw6S;B6D?{G5-sQH$AV#X!{+}tX5$}V}IJNNs~ z`lnqo&lkD-$q%|OEFFErp-osDPSbSkJTM|xyxAolzJa(@B972UD-a&UB628^M)SkotCKU5(oJ&33XWEwqQ>#LBPH7;Tm9uSaB4Em?PSKAa?d?)+E# zj!ntM?IBjfVC=)24yV&sY_@})2sxd&qGb}NXDjGn0r$G5=lg&AoStnI`!L~lHt7?O zE;)eo!1ab$;Je~qdb=M)Ps8TTvaN;fCQ(>^s{wng!3f9#g8iQ1D~>>pIl_q9+z2!3 zR#quD?8JGX`9>vsiok|JTb15Qx%sh-nwkNGKF67LGYew-$}Wa}eX{!S24SLqd@2D5 zrfkqo>QyPJj)UHS^$P|zFOB!3aU31 zqMziSjXH~L_q3&3@ zmFa}V5^79MeXQ-AJJ*dCQ_jD#nAkbI{fTEHSkrRewJ<-fI2W93Z75eBj8O>4o~UmE z_V6t(2Zo8yKKDOBz`K^E)&WpvXYw_{x^9y#PcXegOrTW%>pr{c1<(GE_sO)HZtslM z7MYHfGB-C_aZ|Xm@^oey0!pdeWti zVEgfB$Xz3Fbc9E~`9&{v9OlFTM1W8pA4!`Lo0+bPNe_@aAO+dcGyTje@HzTdq@S{N zJj+Uy`0PEIWmO|1Gdp2Xlcb!ZYUgKE(5#qtF8rM1U}AorVVDr}q2L;e72v%w95Z1p z>q~TECmWqezoPjonI(mmzfy=%)1G6e9yyKcU($4*7wd zH&J7-Ghn)VRk(>R!lD$GkCnIUBY>IT)kHQkLl6jqG5=veS?J|OST(CPE3Eb+X>0fh+~dX8ox2tQCnxIx z$T_c})V6G+IGp~)W;cr0=1%*z;+veam9HB-1V>a z-<(v;LT_-!6kFwY(ryanL{Y5X_`G~1z}Hb!ocgM%6Y>E~eut6Le6bF1kQtg8^^qTH zd?HrqIBIma3TEEQdwq!oFRy8Ju${CYb(YYoQqI$Fm(>_Edkh~bqp3kUC!y7?YT?hq>_ZE?Z8XXIP8MNeB7FTW6m5YJ2bO?G$es))7rj3 zZB3C^0_xMUTr#V{s~#E6s1zk$W19)E1k}W0sCsc$do{%U-S^{zYwz$6pMvSWH*Xs? z>7SP7B1a~jScH-?U)fF*%xvNydFafzsOhWa;NjLapXGZ3v6D?Q4N8nl9K?DorSc1w$66Z;(Qzt8#pOeMP&4>3i00+Iw3QNrJKC%q=cC_!93% z@sEEkDLLPJyUkT2#2fO7A9kj`pBl!rOQwI1uY4n$mi%a2uaIo)mzi@vpYM{>cM0@{ zvp=efM^vry?LC~uXoSYqj=2+vFVbB-m<;(6&eSvIn}Rj6H;o0G9J z4C0r!XWfTC>L#AK@vOG4N8PPLgHCODsfHd>>zRv94g$q{=EX^@VWvU)b2N33g_70BOfv1U_V)DQ__S*M zdJ03uQl)#p=KE=exM3%_NtbQJ^w%?L0-6G-p1gvj66x41$8G}EA>Yi#f&Am#ww3(V^(v<|(rwjhsKK^;5qT@X&m2if*A zUU6b6*Pd2ak=rl>M)O`$B&XGM+I&przkB}+3M1ZKD?W!_EMhu;-L%j#vSi?SP9S zMJ44ZO!Iry{FuILV;3b0gG?KoWpa`6vBgd)_X8DIO(QPY2gS92{OX8YOmE!igsq+I zR_~C=aKC@cL9LJt00dw=#t--uZ>HWkyKs0llEZ8c2xih}C1i5#)1kuUbW<2FPqU(9 z#>ZXj6TwytZ&P3rb>!>kb~=2vhk>9~S23o(L?{BJa>Qn4Bt?_V*{Ar{1#}mJ^vXXr z(G-N9?{rxgBC}I(p0sf)LMv9>IeoFFE_mupH(`0CROq|rEb%yrsbKq5f-qhW2U||k zFDTmj#4}=!J0|?UD)Nzq+Yo#2vgm1e=Ek;gNM)!rm(^r2g?u-oo#^!ktLbcjD9;o@ zO{vC^5RntNT3-ujfveSK-Lo$Fj@)iQAHHPvGjKoE-W3@LT=lKsVSV^(EBW)?-+Z$D z>G+@47a`(FzWtFK`}+b1j#IE@)J1dHT*I6+_3d8RA^#zugfUn0gRjJA_dmZjzG%B3o?}1%^s(PqPdBr{Yw~eb_<}#Rrw%L8 z-xyjOk>DMU4c&bJT`a&kEB1p66&YW`7IQo>gIXadZj4m)+p_JilP-w0;2UoT)DXBL zhp7x}E06~+wq)B}!2H|G@<;0*59o_g7+(d_cPz12uMG}PLZfoQO+etsTRN|Q%L^Xi z#NZo8-$d>}v#26!KTlsLhu7^dDY}+saLGiWf|YtFuq{s^!W^yRrV*`9BrK^g_KO=W9kFJTsuy{$FG zP}$UlmCNbI%IxsKy?(R)6P#(A0<}6tLCIoDt&d1e%~Kimu38S<*(toIxW67k?sz=E z{1ul1nzQ)mNaZsd>9^NUl$xs$2f)|S$4dq1?fF{OgupckuhO&&OhyG$vEp6)@!x(? z_=}CeHQo?Q0?4tG(K!&rI#s#sQdnpsQ6f(YOuY1T4`Cq&0$IgvGJm!I?fd`j>u)0w zU+iPPVY>xs8;gnE769)|>B7*(94>}+J6evwC3$Xu7kG*1K zFF`R=FicSs$chVFPD6CQ0uGo7wSx7yI*x9ViD{w0aJSG~2cob4OzA%cH+cT9kl806 z%{Z1s>Ex`K%CpFxo2OgL2XW2PPWFO*f#@gsqwcf;tumn!+Wey@U#RR$Z3VPyHZNoJ ziO(-IOP!1z-{5D|Zoc*UW+|xe2g|$FizrlP;^QkJmNvz`CZpLF7UiqJv$|Rm8(mv3 z$Dvo3EAM^cF=XW$_ina^=)NDdxMGeRI9=6_VqG*ofI5W)B3E|ii*Y~i67A)Q_7Q>! z+FDj=I5*W3USJOvzlB~C8+M+{{3-*k0D(O``#=4eKRi5MI-m!#(JwyGZsv-ybQQwpxDTX)ko)a$&!4*rAS5LX>Mo8?%_s((uPy1ysIdXjOdG&X82URW=QPrMVWZQ*&~0#t)rd|trcbPplotmO}*=BDkM#B6Izl|S?cXva>$O}C`; z)|yCZp>Eq=$ zkM8FuqRp12JhpWBCs1Nx5TOpH=SxZrgqOVD6(GzCx{CM9i{ znCx)m^4uQ<`j6op9-iX~U5B>(BEF-_zYK0F^CtDX4v;pz0z)ag{2`?BVP^ko(B1MX zCmfD~`&sYHnMgjHcb@xCmN2U9;i37hweXo-P%S}J)cV*F8P;KHC&h2;9}VK?>+VgY z__9!&_F>RK4CojX*18H#m%<IAfOiY;?^lG4ZMeMfEb6izkkkt^ASW|Hb&bP-)wi zQ#gt5p7OluJC<;N?z`eB$t*c<47%XBjO>S^$5z&cBGslAmTxQsRQx6KB%2h^?3zcC zU3Yy|npYo;J@ZtyzuVIj4pu=k0c=!8W*HWy%$GX#VmY8K1TYNe3{j(OGcXw$a~V08 zH@PcvO(qTu_^7M17M;ah%v8EsF+yv}G1W%|UD4EjYlp)o*|>)hZKEW> z>%bki9!PvFs^b~nvFucWPlj!099o zmlc?d7fxpttGF_Yr5oH>QdEY`r8-|< zx*n;y*y`z5=#XouDs8mQ9HVd9-Ym;J4WVS4EgV>X z=l!46!vOzn$hX7d5)dq{TyQU<+*LZ&$v$xy8rNqTRBO}eTa<*WxalO|ydY8+1oYpO zD*427=Mzsa>DsC?|B|0$@QW_)(yrISP@TT6@?9y3f%$}%^zt*RrNL6oONy~9%(A0W zCsbk79VuM^SjgKncYFcU3I%q1Db#vie`cw}vKO4ZZ>2z*w7YUtY%dud9?CBC2K09n zA*E1PtaE!%K!||lyv)lLbLwznK&yJeZ2D5UP&=VL<#Yit%NmYaH|8$ZNpY)J9$T1Z zrDpSdxneoEhIAh5oat%bWR|4CorgmJ$UuyS$)2mg{>y-+_D1Ww_s;7`n);8L#3wO~ z?5VvdX?u_Hh~8Ul$oPQP+?LF4)Ys_T7r2C8PVL9!hXo>`FC(%Jm)89J4~b^YtG#`z zmG)Kky3i7F^|;9>Wero9ZMH9%#MR8;m8_-QYfK>ITtQgC^eb%XP^VS10 zeg&>RUTu1sjl23lM7kV)Qlj75+PtrSTgD?6=`0DRV}JVx6vh7bimY4e4ptQ#9dsV_ zKL9k68XY^YK>}mfJ32ZGxDgJXSztG=^qjK%4`aSnz5!0EFw;uMpiB(;<2zJr>^N^8 z5D3&r(OCQG_mckSbJQ=Uqz4pTFx_K{Ve>A?iarO4#)aZuZ5`!@F9nOlr-55C?IN3d z^Pmua%A7!{c2}GUDt$l$vGT;Xn6aO<)qhXe zpGV$>8gm%tLWnxcJh#hgqQXI;I_X{eF?;x2-aGHalxE0nM`S<}jw3pkr~h^nXj8$I zdUQ*XbFEhFndX8YC_N%ROMVFXwwpum5c;kF>A5id*gjt@jfff7!}nf$3GbD6ppg?{ zloT3HHKynoj@DEOWu`1*|)D@px|spIV=$bv*b8Qo4M zwfenYOM`AvcJ-^KrJObIa6E2PU*&xw@6CMbZ2aNJ_D(XV@c30GJ>$V-DXn?fI!Ud% zqov`b=nQp|z#WGqEjCNQk`Iiq)zP=faAQ zVmEH`-^9-Q`@jgx(*CgXBzvvuc1>e2RNN`XgP5`Gxh3xrS5_Rh2$ueNve$XEYdMqv zr?V6lXZci{(^8xgc*}~psuFP9L@!B*v$Lj!QV$$zpltbNpH-^~9!6JFkKcKLf>LZu z!BZ<%PLIrA?)XO-=~idZB?2>gp^n9<;=dPjFl*VB)%NHji{QoFJ+x_5(zRJo^=q#u(6e9PVnwrZ;wq83BPSKY!K| zUbMo9LcmcKqEJ@pTyn|7KAP&v6Dp#^KB6Ifs`da(U(a)GP8T3FJ5;=tk``N|HRT1v zJx~LF3*av42JL-TNMZVYqS1l&sEA)Iqrz)cySUu^Ji&9385En@3#S=8_Wd&VkAnE$ z8MGyhfto=ohf5^YtCwNZk6E$J%C=J=sc_Hv-xZuwzWftIc*;%p<8W60r$5s5hjAi)6Lj~X?wW$|axS=~ zGk$60EBRs!(8X`RqdNWc#fgO85?@^#iGtW1hm%2nc>P!7Z%#D0?q5`@l8|<3^NZ0F zzQ{P6Dfzm1hPLeQZ(q|RuftPWnMHh|m7PKF)3y*Z^#K?C zdn|nfx~7N>OB9uEl4uuj%#2clbrcIY2SyqD$obc2&P_W}ew^7LYY7cfr1EXX-Q)Ka z4s0^a=qmfDZB_cPGs5Wv)Fi2J+n28_l23TW0MR&7H;jD{VaS-gUtl|~lxM$RRSpT4 zo{6rJ2ZsJ7r~WtkkICBphUa&u4_8PN3;I1%&Gw04MqAPqvs#~vFp^;jIu5(}mO|Ps z!70A_`s(pqbKn_Yf%M;6PC=}%02^Q+^n zza;TrZ@$dezPA-_xT9G~KC(ysm>ycucB4l?LyJ;!bVh$3jR<-~2|c_cU;3Y(|333K z=?vWd!tTK=v0yAfpKsUFH^e_aK_ee|xo#r;q)OZatd$iM({I!%6;o_kU>zEYRTt)f z(dLsmo6xQZ#W6GaPdp*zDKLa*f*+)NFYE@4cVBDkx+foA(;W75C&H??4<4f22q_O; z(S5ayB|8I%>NJi`iBloRhO2Co28bnnU4i*7c!yTA+%%m2Y&SK~ra0jEh~*`6Pt)98 zTReZUFBIw>kM~VKVqr;pGmpdc%?KgK`tb|(BkRnNM9=tOu0X@Hn@7Y72}4BL1G_BG zJagFPt~v>~3R?_UCs@C6F{1Ff>+~+1wKGybk{ey&vAD8;l;xev=Ftz|oOCKj44xb-{Ls+9#fOf}eP(FB&f@PD7aO zHN!4*`5_iP444ULf>8P}e2dHIc-HY_=FI5VdWedTWPOCjYJ9lu*Sse>N z)vTL3`x>!GV3rvmq0^HYi{a%8+OvE46+H4Bdki!2Rll`3F)llstt&caHM zRF^dRdx>hq+{EP}luo8X;05u7z_~R1*lDdzB`geI>S|?-D@Yg^Uxa)=sQIRDeOjVCn`T>J?@}i1h*FMU%`>aWGilN-DUT$QW#h?i+6mx+xbYU4K$d>`&yG~E z-#DYt+6}@ZMjst|H5{Y-Yo+^k;baZc1Oswi(zvFeGc(||@owHDM`W}=I2$5^2M$<> z7Gr6(j1Ki^TKaT%JF%j6xED}xq4j)}v^d~dianj!=h*F~$z8H!R%jP(`7hq^pUqdE zdJ#Gkn5Bz=|U2FvUJ*vGqJ>&rNVntf0vLVZ^`;nR>(Rx$&q8Jw=qApNBVGlQ=qHvP6Ms* zyZXNL7Rr)uOUdbl-|H-m zB4UQOLId^CT@`trrA-f1srEm8_T>5dn)rV@i%TGgK472iK=B`ZzEZxTTTgm$y~sn# zYd#U1Ti2~?FlEJHeP!~c)}w`l-ojUF-sx^8$5tUG^Y%-T{tfP_6*m`Oq}mACU!B^* zd0siqf7ZOTc;ylF)}J@{-8zxI`N;U(0}=O2HWDVBxn@VYOyONZ4QE%tLYpFigB|>8 z|LyPJ{jbNrjWBajn4lJ8$B8lWH__02WB=kRAF-C@+rC5yyc;2%ms}A1noV%VZd9N^ z;fjw*kO+WmxlqW9a`%xaXw6K`wHl@~Yl^x$Dgg`4ck6*j$9Tbc1uz*zt_(!3+DNJW zi@L_6stmL%(TGk&29+`c88KoK(urA+P({svul-%#~ijzQ> z24ZJ)h}P1nCdppeC$AI4%3~fKY=oG(#1Sv+Uv&c|?|c3gEs#kV65wpg>eCUQlEEaST8Yk-TQ&s@&H&{G2f0+)L% z?))l$XdN12*BjO~Dy6lxS6m$m?{@#h!?_-HcBjyKJDA-uk1(0gRE{5-E*pz*18Y%? zU9JgHjUSM?Xz6oL4FcOqN`oZ*q%IK{s}a(C6cSOIO6^+qAKiy>ex91 zBz~e9+;R%-SsKiVuWHfGNq`4V)a{PNgmFgXL>vOMeEdxPgCjBh%sy?(LeFCJEH-pY zGXKjph0)>eCT~!fjwOj98?62V-~2+!f;Ia5rD)Yn9mC7XRxofZ8i&i#4-47U99Fnr zLnE=)z16QzRa-wV_kqK}f$?++N=L^^?9mpW~DLF1eL-2hO&w_$@FX5qHO{cbV3zphmr6oNg9||7vE)4)+eI*b+ z^VCl~9}UW0b*H2(68FJVGhrOh1UhAA$0Gk)!q2eqsn#&G4Js{fE0ALjgVueC_x7vp zj7Wh{B>-{R*O(aPC@l#XS0im@T&W!Bsj}*L$D8r}is;za6H5UnNrGwdji9@tZtr=Mg~Ic9oM~U8%e|Wr&$?0753KL%_mAbJcVMjrC@wYT>d;pxCkm4 z^@G%H$AgOX*5_rwcK^r#%w9SuEMZozD+$#0ks8j0!s`3>RHg-(3|PEslEIt7@_7CUt zU7J2n6J1n&D_;qI-~ z4G4mpzal5+=sa}3OC)Gip86()+MW16E6*goCE?6>I-Fda5&t~ zqZfZ9^G`!2Yv4O$K|3xhrW7fjZt?_CQS)n(q@gW6*E;Fl3^q3vG$7x@s}<S^bHB@aW6ucFm#ytF8Sfx!&b@K@es3Wwcal$vGfOU(%t7 zDnluZm&h}25Ti|xEKSp=hg(uo;E217f-2sJF`-nLCrTSL08{2w!lO;t%6!SwqO6;8 z-6BY8t-35<-mazvAQM#0<`n%i>^C-Av1*i6{{Zsrb&MHGtE-0QW(qm01)k^}?B_w{^mI4U8-IxVf8}$v# z0&}4><`~nkR<_wW+JAjs!r`&NtSK>OyZqpZa%7_1QC%77q+KPOR8Ck{1F} z+zXYgoy{tvv&32QGdruW!MMqxxLzrV@W5=z7@()HC(YqjR>dv8@~T8-@roO zDC?)R6JerySqYAtc{W#W=$3IFl7$^*g}LM4X6N;lfcglU_`Z-r+)jpK`AOI!(Atfw zaAh#Q;Xaf(x7b?Jkejayo2eo`c|Owz-BZy!%VzpjwaDv( zE27{9JFh2v#8!#B>Kj1{aRdQHh+u)Po+Nr(+DKx)g#=`ol3Ox&9<2t1emgCM(-BMO{Mqdx&7G=GGCf=b1f`12%sml7%gnrCV9(5DdpkHLOfZKVhE36{h}aW(4)21MhFii&X20d?6!(}y1hrw0R0@pqHjg&osl32qwDpEpd* zQtb6o?1A7aK3+ws#)nz1X!0vZ&*YR+@C}oosPp4o-2r93wbgiLWd=;ZK``YbHLq|zGS?YOVcWP>GAr%xyHXv z{lhSxBS9;)^i$(y4C&!g=Z<##>acw0tH~gN`Gg;Q;;~K|c$_sLd@4z+C`*J7Xca*4 zmk6XhTAh7k9Ga5=Z6|5QHMUo{y%?IvS(+xmhMG}TN<(GRq+~|*f)XIlIz^e8#~^1f zeH0^l@2qaEGpX?=hr&%=#A=#J_lYK;=Uce4mI}!}W~DtXO5R!P=bD~*G6zeYOh3{rgmm*#?Jc5 zUsQE0mgTahe{kPDDMjtEcxdP$8LpE)4Ys9r8G1st9Iak8^KFC&Ne;ZjaWqEeR+Jk} zyJHUG)ul|Ee7=XTP3==RG8*309T?yZ<~a1NRwwSW$)GH^s~bV8t#jI8vn~s`6IjGLi0P{qt`pbpNAJ|2cfM&RBp=*QsfByYHyoE7re0U~Weq z($=&ou8)M}xK&}<9~hN4N*s#nwXXvrcrEX|e4uR*eyir9n#g{X1KB2{SB~3_rFM`l z@;a;y&nUYE_6PU(FmYk)I@p&2D*7!uj1RhBvi`eDC~Nae_1)#OIL&*0G9emRE*_t7 zbvSHDn7mk+{#|Klvrq?jZ&qCKW#fq^C&3+j*?YO7|L!vXI`_AG_y5x?{WCEN?&U6u zJbgRnLFx{!H5Ze|r>XT@_4jL6H1k^!9Ji^<+;KJ?i^Jh#e+o@rYVO#>e-L}St^d3f zZK$NVIRSP6tEaxJfNr&@vS7YH;&MgjZS5qP7%?CJeUR0eTB7fqW;{Hs~3{=8`z;m7EA4^FXkjm?3Z~xI;wj z*W3y_cy(*v=_^N%YI=J9iqttqZ$}(h{vL=ORyl*Kr4t&&A&sRP8^IM%?t@?xTr)vEu&$qdeoIQlJN?JEy)e<_A)`O zp76>MxQ{bPvd|G=jDPw1^`D9RtM&JEDgNsF*LVMTA>a#p$6|#KoN-0(g|1;|5N4Tb z|DL_I*EBfVgg$BYD#!`Oi?{M8Jonx9DcGqriXB<`V(Qnk7MK*X0f5%ciKqI2km$kc zy2qo!wl39{1A3*!)#I%aR(F9#%N0QgCX-ayD0~}_g8g#x!M{>`U!<=UrS4b%KK)XFbCUH=?%2bx$#~8%< z+1uXn%l$bg2$y~Px#c16Ln^ynLpQ(N&oU?Nh+l)HSjB_ej0Yy_72{*krdNWrHE;?g zljfT`g|Qy(jH30121!||>U$z_ey&<=TL)Q>oCPl@#m^XQ>Y1ARqAN6@puVO0ulqxW zr0<9UtS7^>$tR|hlS`695RqU)rE0r*POtrNsu9XJgB9p!DJf)X;Sk$z?iPF8{sAXp zUUX#MQ+BPab|kABb~97LB{jq)sgHmT%N8<9quS)}Np9>Fmsg(wxEEfmKl&DLw09U+ zXAV)$mg_SOZ5fjsogU~4$oAvz=(Y;FquDU-w_N^-=XLido*~XQ`&S(yMH8XOlOX=| z6J_zK@AYEwa8>)qRt6M&j-U*paVmuL!Rt{XBvO53L2l!mgV)xvjlIyl@GN7ed&Q~Y zFOX&##U(kCbv86TeB4AmkDm>}J#qvSE}!TTVm-VU?nK5b8;2Lo6*zlND|s)cd~p96 zzPc3}`s>BKPVFBLB2&*H62DVA)-)fiae}NSICf$eZgZ>kyPTIOpbu3(0Hnfbyrp@{ zvR``aq^nn!S3<;^p!HiiS4?897LHz4G^)xMU_X3W6h5DGVh6XgA8~7RAG*M|wD3ej zb%u9bJ?|T=;o3({Z55R7UEJJ)fa^&Mra~-L2J_8_dX1kmhL#=TE`4_cJ;x<8Z~J45<{8X>@bkVwTz&sHu!@frI=%aumsJqif>4qGiVfG*yY1}S z5*J@T;F7`74Z6*p58m@SyeH**v9@-rd0OPxf}zuEO$sDjo^u~lW@%%qq%=FKD=TQk z6E_fKrE4zx2E-c(I&x$&JwbJvX8L;C>fLnd7`3xI#4WRLgW@)XD^Ip7P1+>Af$m-( z`zi^mfWGPERm6AcuTA^2cmLn_5NA6oo4+!{gh*a;mL1*3NrL-LK_Htqyx*P`)A_Y) z#(c?hSc)4#H$G6mN{bM?@>Z2op>2I4D^lz>9Xr?0$hW4vgvdg$nr#N?Wx+49!XIdA zGeXp+o9v&~TGI?0A=O2Nl$q;RN_R244|Lay7Uld8lTC_etTqmxeO(5Ec*rO zc^NTWgoIfLfIxx3FL~Ey`=LzP7@yK=S44?n#%y)$p&zvxIV5j6g$#E(F<_zCT4BKD zW2Fq}6;TQaLbvdd?9F7~nT_e|@I<(Rws}x1P9vFRm$tvTHLEEQ9-iF%ELQ-pYt?7e z%ID-2d550vtxs1Tu83f@SC&=Ud1gtD!B8 zu@Ew?7hl|lPhXfYnyD=oSaDdmQm~_90iBb*_!#_$YcBO=cbt2>2Lhdv3sDZ z!3xdOr$$WKJzdG@%<_PG*&u%|*nzmk;(z|Un!n=C`=)muFQLxS=h4qxyqyCHWw1c* zRIG=WXYb$DcVW%JT(DE#yuwYj&6tpE8qubitp&KFS)3reAj%1_u_jv%ErM>ze&VU6 z1fTnWf8R|JWesua$?i6`^EY#Co2vvWq-=h%0^s$pr*5p!?z}^@0m)&Py)AIq63RgA z%jw(6StURF5_iW)7rjXLt_8}ubecfOD$=Sw#2?}UgXXG?^{jPMHZ zpTvDQo)4%6{ydvqTs-cqFM6l>9Cv^s9LuekynJtXrhodZ!T-hN z)z8s>5t=m|;9t65(lC&DTGiZzD5b5JG4eJ!`S@KT4%}pS!NT|s?jt#SA|7+`3&2wM z!;rd!(-rsPP2oQM+)eI+dL?ntcc}F!&b^rn&?_9T4n0f<4QSHsV4en+-myc#4i>UL z+7qLYmvg!t;Y$+-S&?pvJAClH4h-e+_UB)?`1|M3|M~c{eEXkl`Jd|WKh**G|D$FA zM02)Td?*N^%uY8aF-Q%E_NIV^vq~z#%y61y*<$7|dB*1EW=K=%Unpb74S12~=o_-tNrI|Hmt=YQKD$e0iKjo{N` zw5%R}qZ&N*Yzn`8r`QaPoo@_wHzyF^Ba)py08u#prR8S`Swn*0oOR`am##?ktb zHnRTxiLJMHWy@`(5hL^4T6c9GH+wwg7?q^La?KM&J*M;uipmk_mIL=o;_Wrp-oleO z3oWADK&7L=>K$wCFA<&G&FCU1!>&MZEfU6zMPabO`C!ax)ARj$fU;Yx*Lo=5ke@gM ztS$^TB_%5hMx6bHHdA%Fn#%A-=~{2@ z`y#7Zwp7H=Zd*fpXk=$d|MunU2TnR^*9!AVl9RgnJJwe7XqfPhMD#^FcIQ&_u*Qes ztlDikuY#88q}1#8ZU~#(`*=wvF|Qw2WqIYNs+c1M`0JZiAv~#C&Ku<5fJq0dfpjry z(eN{3c93!Xj6)%1b7#J2KT_r{a~NH?UJQzAsQ#vhPz$IQ5ko4U95T7v9W&k&m>2v4 zJf!QD>u%(c{rw$%eorhy<<|8(e|FGE@`oXK=xJiB{1#yzDn>Yhb^W}L)N;&3x;n9_Xm4M={x!hQH7BK zLr#8n7tI-1*-X=Ey^_h^uU`K7qyC?@R2m-W1bOH9D9B6=myLyEUO3av-G&oxZ4oOs zgNt2(*Io_G;=WwLO{=57&7RGO#4;N4E92_>ihiIFpz*Q_tl5D5Flc}hcfNxLRM~#OdbjFMDGf|s&R!NDQvBe4uzgc{_R8c`x zgFo3dl_F;;p=eC)x%^mfM;5SkY+~Z9GlrohQjexm&ML zghG^Ow1o+cgFPk~pp{j~kC4AM|1W+e9Ve26w#PK{haIggtd~zO4Jprir*T?KjMKEe zZDf|hjsLt;o{7u}@HNR4DG-e28p~noDjjaD|3_9WL zF*p&=4?Ev)?hN4NnfZUV$UnQ9#_7Hh`MM#ontGqD#SwO?h$nYC;;5LOm2f}_H`4H+ z+qs7Cls~b2!z%!a;QoJmRzYiNsvHn`huY-1WfL0=&e&na)??-LqGxum!L1QanbZAK zLjt$KTeq+L2oNgNS4Xm=Rg%r}f017&#PAX=i9W5yRgmh{qhmTi?#_+fB;vG|wVa06 zyG3Z60Z|xJ4L3~#_!p6pWFxf8AoR+=~fI9@EVI>X5YmUTXuRiU3*EnF9%4m z7NBjT7%Tt;8-Y}|*~*yDfl(f;q} zkJ2J3MD_om?Y*PY&fb6DOv#kguQ6(3ozbYV$0#;XXC~Iz#whkOu@OZyioMRH7%Pq% z8!ApLpeQnmfP&~GD%g!8pkj%=g1wvM^8221?>XzPyS``Lv)1{?rLdH}w)^w!{p|Pi zykC`Hv@{Ku`5l(1&A5!B4cv)z1OoDIML$i>)n3j+U~EsvsMnHy*wMV3*9W z6;S-M(rwso0BXo!5YsE9wWqfkVvFKCrA5FnV z1JcHpEI#B6IhyQ%ng8$y`;TtbO|}D_Km^y+^LrbNw(i^r+=kY2QNhEQncx4%d?lzm zt3(z|+sCvynp-}p$~pjm!5YE8c3!b@Znu;^FeBoEl(E>=)bMJBJt3j`VH0DuC-qQ} ztdzPuwZy*M!yEE8Jln7AeUJboq?yB`86xp3KfXNv$t1J~1*E7L(*f_0C_*_=_ zBUZL*7h`Cxo6=nnQC$HU)Kq=3Oa1464CcRmH#ggK`I^d;k{^0EYI!7aKXXSX0dm9; zWU(SA=QFC8G^-|LucrF;9`N`7 z_jm+9Up4Kq6^A9OX}s1heI3#2)Hhj`Ldt_-!6Ux;toO8BhC_U{Z@zkxV;1{v5C9Xj zx|B8wz(0Yx+n`y$5glCW9Y^e4?^Ymk5r|x!2Vei|mo9`}1wz}5m=z@ZU;RO86<_rH ze|+r!`X$imDc-ov{K}^xm<$^_M?hUp+UPxGpZ6)xoPFYWcV(mc-9DgCdFJ9&L!SEK z7Hs+QH+tVBHN3j(D=v4zeymyoDiEHKC3*Beh?X|>0hf|U?EyHjVM;#=N6BS5)o z6|#oL&*abnF(wM7cptiVA0}-NWG*u(ASDf@jcd0N0TA zS9H3t0NpQi}rJMvXKD4AN^@X z$Dhb`GBMXF&W3`w9L=o{D@#*T=~~`r2eYMZXSx9t&PBxud_m$y?0L?J%bXNkM= zVr(MNX(4xno`8C?7Yo6&gfw=oF}FHlcI`_?%I@G?fLX6=cuhL;o4P_1(_9Fkrfxgt z&tSdKf@!1^%*Y}OvVG_FeU2nFFg4rw8;$3?&@66QIIILl{#Lvv^jf&tq?Obm$NPS( zJy<+vbl@ncCS&f+=G1QD%nZ===}gWNa?&U!_9@(u6w8!$b4y^Som7_;tq@uMCu|`y zDZc~4K?)9(Qtyf6bV{6_L9=tHm)g_*0>({T_dSct=u-oRU9IlBu- z%D@b9Rya|zp(RoA)sA~z4THspb=C4#O5+4aHACzB zckD65;nnbc49B5qT*DJkPj81@TEbBjBqe1c56N)a-YQ3)=x40h)6DvO;m0W@m5-AF ztuBp&e0=hQoI%GCfl)gqWW~266w_T@i#Kc3#8*0Bk*IeiZn~*qZ1aRMwI7G09fEZl zX1Ju4#e%kwv5#@ep`1%r3ASrHo-$5{fQaJ+Gmd`=+`y+AZ}dS2?PVOANpM+tx(g;UuHf_kfxqQJ}ASv#=zB zqd;qC<5a`zdulO+Dvm&EVribc`?NXCAcs&iZI}J!TWZO#Zma0*Ld8#stU}bcIM&hx zU5tGA%6qTBTO_xOwH67R!Co?m5FC8`YTy$1W7|!M;9}kc!?pQDfIHnb*j+~7W$*oa z$-eqx(OX8>iMlw5AKID9U8*bLA6{mW=G1W}@Vts#2>zL}^073Iqcjbx^k~5g2cj9k*bo8oeTpYM#IK9oN*#xoTTaj(vs46B2Wfb(x(q$8#!azuo{GR|^B z3C`;%Tm!>sIYLnVn%pUDtTo&+JumpL(4f+>sVB2p?K6fI(-dbm&lU{nU9Ggm3}$<* zHmC+gssNnrdu3%KWih+uR9s48{aT?^t?_(XHcF->aapPhia2PFMJp~R>6V8D>XZjH zNxiEnD9aZuiA;Q#YjEOz(_!qedXg$tkv&kAuSW~=& z*7aLAbbC%1lrVyQP5t=@uykENo-}#?lYPK+;Y*I&&dL&B&7xK}Pt4_rIxQb-iIHR*Otyz$F$7BchxMGa!)EW{2;M1qofVDm z;^ZpN5(p~86%7@wkD4gcTFQ5;ZEKca^5Uh-!3}~2_2eHLqUza#Rz9Z$YisBgAaD^c zc}QB?)mecY&*;Z+>>@h6)e26CCZ^2(x?;qka>6$cV12srMLeI*ct!!S`TYG3U%z1X z>$N<26{C2Qgx%h-OzO=+o@HXE`s>1YiPF^)S7>bt_F}x4IpEG8Lm{=>XiT=w4MT)5t{j|06rDP2fD=WYQmIkJCPBpbw z!-*SS#UtC;fyPUcaUK-XhWRz6FICX3{ot>ZP}sYAnawQcd&MGi|h$i2ForVyf` zn{7MhcE1HaC_sC6<1HJ;-rfAPH9F|o-0E3=w*up6xy%TOa8@wGQe+Ls!Y$G^k8P)C zHHL|0TE0Yu#PxxM?R9SGJ-(brG;U!~(^#tm-}0+lt<2NbVJX1ZPcrW8(HiMv;1Pq; z0~0$D5KB;&p33&haFYcTN{{3%#C}!fpE76TahxOSScy`~Pu+qz7c4P7jf4xVbupu9 z+17d-dDw`i25VN9v2#d2JBT#AEDyFQXnHbdUYAHS@eoM1?)#kaPbFl)Ew9!bqXjO$ylZMXBd^ zvu8RD-$ttt8n*r8Bjf(+nBTK^E4kX&?`r4y@W49#53mG41=YRnc&MbeeFV)59VYGf zy5cHe0+f2vOG>r)mTg?X5}BwbniWV_bUBUOcyV(-Gr7t}0C&^%mC2mCEJhbN=#V+*I)S@eJHRYU7k-TR8R%O)IUmC~HXAxAqYA?NY*Z!Y&BoWttUZgpRS z_D(JL|1s~ZjL+pANut|Pvv&3_jYi3`x?}n^D<{G5X2GxgG`RWyV;ui+sxDmkU!NO$ zUiBg7sDr)Je{!#Bvj^oXACuZUp^fg1>#?l&9FNu78%QY5D+}U*XSoY|Voe6^+tuUC z3>K%yxw{|fi39PjaIXLu=2lvnPvi3L2?SFiE9c(+I1la-LB6z*B`aXJ@FK&C4$Qw> zD=v}Pe*@l8M=zDT2l475fWRE0Vl^6Yu@(aA#ikrq__|~%L^R|vUIZG@+gv^+Pp>Vp>IdgX0-2ST z97`0!fSzM8)W{OIt#jF53(t$u*We=D8~B{D#n=$x9M5}w5?K)&fW$}$*pRuNSI@ zR|BESGxg!EasGa3l_eDw>M6;siAR`Nl>$Oa*r7JZmFMIWxv|OU7mu)dI~;pwm-`49 z7Q!Yh7BOvckp6ecyYAP&jDHp?fAsefr_j~kx&LhKci#z{`yL_F;b#Z_FBPm3SV7qA z`~U^o?PYOdVS09NwaJ@O3|u(kqSHs|}}Y;#zmOrp@@LPME-XRYp%2UGDQHXMpu zb2lR}G9#cIL$@7v+zME0=W<}!wdJ;o4%S|Da;?tUotB@@nwDHEbg=H-@O24s`%X1~ z8+p~c>{c+&)xpFhI1CGACc+1KC)b8@nkH>s(wGEWsUjxkg&TymQoPG4u@-6{49!4l z4$ukEFm}w-wbWHk=A`zhcG`L__08HjUxZ$^-%a38-xekdOBLLtPLMB4wM_1`^71u% z8ha%(?4LTdV!KVD6*;}KDoC7-h^QiAM>l`#@I-S@JVA+LSzW34IeaW#JiOb%?;< z;6gaFhs!6aV>S{jCU&jXe7A_pFnoIySS+9>M@6nN$>Jub2B42hpVq^NH9ESpG`4wo zjOEG9(-=!4=&z-37VJ80HCgq#YCXZf&i+;qSzUol8R3Ml;VOT4Baoz#ZX<-k8Tas8(jGxbhkELLZ>jCLUY(0Y|T z#4Ib-kL1cI%m|uz`;=6R5yy;l8=pz8moYs2y(bha!Wz9_;a+mr={j|kY0i?Bjc7g9 z7XrBHw$skX_fBDl2PMAUJAhst1R!QFJN4 z(}lmb(jIFrPrbM#u~KTe0=X^PzsM@2WUK2vihr(Y(4vgCm>bEmRPaTmj+vNv`@~<3 z#;c<{+s5A7ilCQsq@VD*x)O5U`f(y*3}uqQfV3NyuqULc)!FwKiW@KAhq4J557F5% zI#LJz3DH5;NxMg<2P%Ddej8?Em>5m7`ck#_Qm@@571xR$idp-0N6*47?|T!wAMrEM zV4fM!glzyJS9JyAuf_~lVrrdt2T8CYyXWZN(Ux%TR^;h%ktJrhFCZz7=yfVW4wU00;$+O6#o zVKa>lc?MmAJ712)F1-=sZB0AhqPFTMI59PYxG6NrgghDDipY_tUgZc6zOyPd%-);) z5Q{!(DKec|dNwv)hqesjXI7vwt)oV+eoSW)ilhe~0z0c1lnN?n?2{D3K_D`6FwDfM zSVe5qK&5`bwp=YB$@51LA9bd%5Xjrmva*Rp6=%u%vKmSgkbPuwGBG-4Xi4%Qx1%WP z;iiU6+px2<)ajbDRUBg{LrcUjJ>?G)w)jV|S*1(Q-wEt|WnvOBe0!synBO`ne1~VO zBbM(@RxiBwTo(04=??m@#Pp4y9e&9y&XWfQt+RYtNvgOU5lLj=ri0;B`?*6~Snhv? zYFWU-WPR5Gv?^>b@cj;a$Q(psA*S4kcw|f%^yZ zE2K1yn}z7Vcj?r-ctvrn*+K@k*Fv;!Z;6-ROZ~Ixo&-4mm(OdrDi@04H#_4~cDA zZLGtkTM_AI4t=X-_HIiJRTeia#g_t5KCMXO@kKU$rCo!kqrV_JE5oQafj%Jz=pC37 z35y02~5jk zlX$}h($1}-$s=PQ$vcru%xD3B44c5EDvs4_EaOL-xO+oZ!DNigOMRvKz@T9WQ94_` zc`ZfmHUEC_QKJai^h}4DX>HnEu0V@}j({Ruyz3xGv6C8;D&@=U{OD)i#1P{f9R{AW z_%|6$u7uAQ1gABh5nc|P#y2iT>C2~II#o{5sZcaB zs!A?Y0U(}uW0fNTaY#!2tAfzOvXv!TE4pRZq>CNrEO)&6)C9j>=_;2dk+>IutF9Rv$GSKncJ z{)=NY&r^iXfMNn7H@)Q(!EMPt-CNSjx{~N{bJGRMQ=aQDb{33NliBNgQJTkt|NIRS z6cIzA$D9*vXSCSIf%epsrJ^`Wvi+gP)@I#KeBk`H@fx3Y^P}!oauVvg>E{bScK~mM zAb^b@5xldJLOG5|*W1kyuxQBZ+&6>~HUG<4)W~-=knn{X1t)e zQ_SJQ?HQxb7eMey)sZISYkiHu-%3#LqfFDC?LCgfk4z2BWrJ|x4B6F=FOUB)?sx$t zgboxtpRWkuug||Y|8cq`$*-?t#nAcumMHjzk0`AD9q*Q?9)~R5IPz%nu7pbd*26eG z3A-Em{XDM}g}UuO-ssCuZ;`x(?w=eU{YhnIj_ZDzGXsu%nO_&yYEJ{#WMZp`q>31+ zot%h#oF^y|^+RwHVDEAUsJk077(*-{-x~tGQ_obqvRniJf*v*Y^PPHlSVjBqKB*BTr;+ zcB<|9UCw{cad4azov;(e6f_e5G<;7Hr8D%s5RM~aU6(GsYEAiP5rxy;{r3J$hJW;T zJN+i40b@mcN0W!sWZ!f%27wj7?Uxf+u@{2Kzhf52Qm3r{e9BOa+8JJv>%MolE`ICGGx>&5yh5};YN+ioNs#~B(5KH5^qoG0qI7Db zT5N6r4oj}ilu3zPrHw|aJ^~o{Ck&c|_oQK-i~l5GP0i94_l#eq1ia>Yzldko@07&) zV&y9BtO>Lk{^9M(!zS~fa0XVfcV&aqzv}1}mKCl-Fb;dgbc47Y>B9JH?ht%VHlpAN zQdYNQIOt1sQF*fH^l&T+w+;uG10vdVPDr%XB{zXd36f$R$)4T83FKyjinxDC77@Y+ zhmUuLzh}NU+2>S@7>aLH4M6Ld--^USiJ;a6Y2&`c)*7b|h1hSWlz%I99uBENulvL9 z@O*-r9QqiI5jIj7hsdCchAMc2c}YR22C-Lq8e*s2wQn7Z);pmnrZ0}eCF9$KkOEFp zc8P7#iSc#or}jDyQ)d*#=mtOT@hrV`5+AA=jGc3~%ybDmUE6#*!VP?b#2tuPWwnYn z2|mm=p$7r?sHy2Wx36nFjxX;$Bm7-PU%BZ|lvQ9io1z0EvQ%6TeJ9K0@co8Cor?Oi zaL^8-aPtryuPHo{rMEv2RwkFZzi-_Y@!;jO13}c`A87!$Rju8e2}ucey2iH7{)qCtS>mOp%Sk~-1Ts8h1fsCN=kI)XtL=n1~z12mN13QEc$*FhgxK;2(?IEB+{* zFVa9?#Wvi+v1#%(qz6tXu3raJ-P|4Q9L83?vcehS0s%)q(p*)rFVBZ0A`FtJb&54@ zT1+eStm()zolz)LDm;6U-xWZs3`Dld8u-Lmo1+04VcFZ;=bEqJQQj1>fUJL-r?>KT zag9_jzXGUDzjWKr_E4(YC3=&N33*UWyJuFj%-&IJytXbv{>=7q@7YNDh)cs0ytSYm z+%r*ASSrDn{cefNKk=W3i82CLM}0F*PS66G!^lgs8-C_Iut`RF{vHiX))l?wGsWR8 z&$~1MDzf_o&-8?E1Li1oVl%W%tWX6xo6ga<2MJxe9N(kVdp7v|8^d@RiJKg+qBJ7J zE;lm!mu@axA~e>^in|u2A-tAaj?z8>Ppmy^9`T6JT4d$hfKksww?rcPG7j$>e1xFE z-e}m)o#8Qn7V1yEDm+qI?^l=%~F%Nim)sAbkhuoQ9BAgT3dI_D9x_{veES z^FO%<)om#IkbmuzV|LdkI_?npCSWrxR$$?XFXJwfmbshmE@ePCJt0R(OKWXZ(axZA znR`3|+d}$!4V8lSG=+AdpoBjwZSO;-XMr1AsGn1~WCTI%9ZtJ9szWE8LhAMTtGcGV z7gL=w>$m2XCfF!Z;n+I2r3B>1f^K*sS&pAFSyvxLk&1mB0)+YjtVVK}9k;}EsAbBnMNI=2m3bDvVQf-q;*Tyg0@KG$= z0I5@@;sBOI9{BF(@NKJuR?{kqKP?`SVn0b1`-*kq)Fu!H8=YQdvY=9fqEGN%f$>eN zoJ@!sE^y*1Cf!|tL4<~UT}&leoB_ppEK7`I|Jw@x3P z?Y|MQR}qkprPH4;)U)po?qH6*j7j)qPjS9EFY4U#WF2W#H_YdE8aG4D4+4&VN)Y|H zmbVEz*|!jEM)JwhMo`UJn`_IyRJBR>C~~&OhPBm={@qOE>faTrs5NM3@BwpQb~U}- z1Nvrt2MS4?1#;&wqwNMxhC015*<~yIg83HxB##2l!%3z=#F|LjhnFpd`aohdeyKA` z?1}EB$}boVweI05{lB%aT-8LO0F}5IVx%ak+TRV03uub0TMU1a2^k;1O{Mk%Ya5SG z{-(P&yWn2#6hxDx)-#8Vq)FxqrJh*Jg4mVBu)WMJp|qZ2{-5w#q3Vd9zv|C%j-jC+)} zqH_kU#Nh?4eiv(US$0k*kG`9yPP#jbJXEQGyWz8tvWgBYXE2HfvYrYKbNIAD>_x!d zsR?0%d~G6e%|&aw!^m5b)8pubf-JP#j2v1arYkZ~v)(L@?xbpJN$cw z#_o?XF+^9CO)R30xiaz%zpO%cjMJdD_wYUSJlb|@W}0iB@fdjFPv%y^2g|2NRy+?O zusMW0Ir=p!>SqH3-Qhd=SAvs#J-STdu*wxTk5Uo`JBRib+ZSBBGi;!eKnDYXt+bS! zCC5gwatIk+SGG7l+T8t)!^!@BRL)MrW25s0Rq92Eg7JGu@8^E))Gab9;^1+G@+9U57_n*(i{LYiw2q4LiBgW8BWX)E>=L-V4R5dxM z`$XVth2wS*%Gy6)NF?M(MDE4tw*jQps>p2W2u(mn&F`m=79tM*UyA!!41H5w(;Xi)1 zcBuPRI$C$ho~lG0HtUsheO#5JHnw9W2iSo6=j6(XIGC%*EbKV$9?LwNJ#c1D9Ljkc z6SZU%8u4gJ>WQXqr#QV)bdJLyU%0)gP0rZnmN^R1iR!1US1wr5WQ zxAfITB8d}HS|d$%&KE~#!xKRk9excMOo6G%&glKn1%=5IeK5?WINiT~7zxzLN`wpT z{%trw3v6F7fLfWD7ev;dv+_}Ksf79m*Lk`vhnW>IC1E}2zg<;d=NXEhVi$Xq)TK0K zKju|FL%KHG{gLiyBnsR0Ux|Ua%vm%KoX8P0j{|&+92xDmRU|GuK`fiua>JTWNT{0f zoNqtM$*c({7!Qs!7IeRSx6|M zMyKW1UxN#6E4?ybkmD#hFBE6`4Uv{hm|51ur`_fxB^tf}+U8fvor zd_gTuGQ>I3cpiRdyvui;rQ0N;+ zN4OsXQQ%}Qm*AVf86JNjMJk`F9Q8-^HBEJM`1)QZUrhc;dRo-e)BLr6j;W1bRh#LS z<5;cbML$EZ%Ga3*TQ%ku6JUFqq0x^LXJs(2!P6w_E1s^=nSFpZbCYCc54L&Do38u; zv1MZ?b>%g`fjFpGu08ywh54{f7~)L1T?wDucTBr=;{iLZy(Hh6Zu5KRwsK$d{HNG3 zs&^CcB^fuyBD^2{)zS4$Nwph8w%frT6h@mg99U<%9Q-s#e&`Z)w}yCs1s%Tr0jH0;Qw8mSjS3MKd9H zcLmRK6J*M9$|O=P<2IvCV5RvK-0d7n(dVNs={is-2$x4Df2`x;0Z^hX=Axf(Vr|T2 z?{#bsFa-R;GePamUkGgBm6K3e+cZy-cJU?Zk|;;MJ9%N0;wk#Zfq~hKcyi+v>x2M= zF+x}pmm7iluoIb}M%8zSx7lvAc`SbV?%kdAC9#?yKIV1Cr4)<(*RpvlOL1LLmIH?oj`JH{{9iQ zIyk2a%3ke_}mA7x(2bCF^KYhe~+gQV2Y`~iOsHp#bkqbE7j3V zy%jG+?d)x(D+h3wK3}L|fDB8_gDCq@0Q1F87NlP)I66v4f`@WNdpO*}GkMN0)|gdp zadCcaJcm|!sAMB; zgg8P297aam;TFae_MV`bb6aR7s97PdPRn)Q+C<<$9K-0XyV}ZcKY|u|XzHY*MAK5s z3oEk=vB_WQ&KM0%?@*t^cu|LYK)IB<$eliyz-^BkJkJ0>R-gm;a!|9?g9_sNjW1OZ z@%er^15oL-4~vXV;L-GqRl&XBH8gOT9Fy@>kb_j?c#M=Xf$62Rc_q9@zu=His4L;5 z=M9obkF1hl}_Z!n9fybJOD!=eb_%MWG~!3%^nM z*FoLa%bi~+YJG1f_M~c5^rtR79arg4kG!p%2N*C z1WTmbD8X3ZeB;(^B7KLPQ6M!TS^i*T)(AM6JcYKi^E`|$>yAyHU}m5s6i zz#l9!8nftIcB8^dc>FjYY%jTQW2KJzaaTI5`8d8c{jiz$XeV%CxhTsZPQdIM{BG(9 zUbMM;*hnWxiPkd)wd$X)rOmPG39z%xteHCGxTl?>zC#kaV^_avlpB1kMqdenSqnTB zRVhrF8pL zgVB;aX(Bz&q_QcJ%Nnsg9;pb(wIA2en z5xRn}C0ovg2(e@i<%ApA6hyh|aFI4X%+9QUI62EKg^_w|sQ5|C51!Hf*Y;=>-FZw{ zjGWu~v#c9I%Bq-uwYsfhYW~V!U49!z^g{ap!BCIY(||0=F~_KybXUO6w|~M1n|)`^ zAq~(c#iI#CqVM*Z7Uq%ex$Z!ezRz*XKyn;G=v^FIA#&kefF8Y35@J!fp~rKc?QAVr zov4e&9=&%}%TNP}4ZK@={Op;a+i3hWzl!MF5Pr~;6*~UYSR4@t9}yBU*7>-{=lsz5 zWFoA1z1}S#VwKHV&jxL?BM|DY`Z8L}w|(Fyrvbhe`zEmoBMb|6%r6rTe~9v)^Nb6h zN@QP2oIFtYK7anOxUT%Fz#436m*a#$LV6Yv#zswVzy@VZ39tm-DpJXA-gRcLE&d~e zAR}5(rgsQ+H~(&xyW z%imJajDsqCAJ!RiYg!%*m}fqPZ>(!XQN|ZsY9RTm86w&lW3`wRSfOyA3J`9twubuJ!PvzLPiI=k7>Jx51ds zBCtWqaG^Up9rz-6&(qxX>w)?T?Cxpt@*RM^xh$d9Acqo!Tz90+u81wUPq|~w2$mc> zRlD!AbsDwA4-`Pm#}d=D?SZvPgz#d~f-qX3!3%Y|aNd|oxKAc`7>DBa>wERy_|;_m zu;vcGpJ2<&c68&vhHCkl6d?Ch>_-k78&0rpL>lpART7vJM3`vW zKHDJSAU}}2m@o{pwLZYN_)lM! zSll;bZmV+WP%7o&#A3=T$2bFH*+Q%ycN7=CUNftxyrY#9%mW4kN@J9y2dP?BLv1l8 zHf^We%pnS+uwaV{VyGs#GeQngn)QM(Uz~o@oK_}h1IqF9@R?sx2{z2$I#->p4_Ar4 zBliyOaft;(EWl8gI8#qHDu~LCqkXG#<~kL6rTDlfIA@@V=fm<+cvk*dw+&xnqiZTc{vB`R?%$-=jGgXIG%c4YaEPS^{xM{1z~k=y@gj_ zKD*?@^(*nZ-;D<9AcWi*9_AjM*QagmD3c*-3FFH|q%{-=K{PHFqpAwGj@~txz^SkF z*dt-6VUaB$Il$>Wx1j({NuOc=>M)`H`jJI0$5L42ky{b7TFhH2V&rC83}ogi{_gCo z`tmf=R;k`L4-_zlszKEhKxy%kTpBGmj?iD_uzt{LHO1;^Tyle9Wuc_gyz7%pC@t(VjH>C5f%EH^}4Ou>+bkiMAiWRxykgl7^} z`=ohi&)O`HQ4wS3_2;YMsMZvws*Rj${kGte@2yrnR7#VsHvrY_F58;*zjiS7;oXM5 zM!yOVu$SY=7jWF~xt(R9^hc-Gyoeg+?~No(C0wu*H}vwIY&;s)!exOH{h*)tM@qdC zFkCZ$-6oC_Nh7YZ(4W)5<-985U90cK=B04w< z&7-y*55okerDVp_V4H#gpO0{SBJJ=jzoB*buT}?#z6qbi%PSV^O3=ryMQcJy_X#GB zL2tZ|n6#4ky#!Ea;UdqCm?bb9yj47BuvYIzs9Iautpa7_`Ii;gkk<2Dh(rVcRWs^q zS;STVWW{~_?#Hmrk{JkD=WHxQ_p65MJvAduBlb(Tvgi{J>c;A!L?_}< zK%N=GBpm2yqZ)VoyhneTJGWKY$N@B4ci9z?>XT#w5_+bNLW_dKySL9Ag!bz+W$tth zwF&fnNjXI0^`Y2FMn^eZZJ1y;D4%#0$tFCoOBe{z5sy!7QqK2X7cH1T)Rp3MB^OOt5+Nti&_CyzgO3LeoYb^n1^{9kDt3-x5#HY$i@Db;j8tT5iJSLIduvCA zt-c%$`}XCTXJ@;Dt6ugCgNK_s6Q&O4VfZ@eZTs-r7X4k0V_z0ays1~EIi&9Wv;?-| z=5@oZZ$bjwN&Is)Eny+SmBEONLb>XWOIt?)GmxCAN!Koe^yBS58B{(?y^rXoeK^kP zu?+R_YvN2{2xHbQ^ba47xQV2|K(tRM8wK+;bi5v6=d$EGnKr*@3I{p+-8;_eX)WN` z3|0W`d5;c?7f!T<8JX6x6vbzFa#U=}71W@>wDBFQ3^2!(j6{^_oh)RA6_fjH2dDa$ zx1iIdjUfl!wNo0w+@wT02(``4YuGPzugX(XvzgQ#0~{EcXb<^L%M~HkgIb09_VKO8 z$@g9eluXj$kvkJg=V_ryO99v}37rHG+1qDXF)s0E9^oyV9bVwPN8>qzSaSbWLO zcq&gEi?i*EsqWJ~b7ul@>7k+xWhL$}HiQh=+6$5q>nO=M$Z*m|=;4Ii+^ELHsE-d+ zXo+cMZTJ1$2wiWRWAV4~uVP>6TTNZ|)8R@iI02=G6&k0pd~&+@Mn36}u|~)DItmrN zU@f`0%1_Gs`bl^~SABx!mO_6nU&|eyGzfD^ElMkkDA$H=SWzeXqa_AnK8XMwgbnD8 z;!sS1&+XaAe2J~2Z;tIf2?>7^O#`OHGb>gS5eOtHm)5@GU~2w#sOyueTto2tj?Owq z&GtKcJiP9*$RLRvl!H)d$dmMe843#-hJ0EJ-xPHEZ6ImI#N`OK;;Vp0mH@iJPNf~i zo9t0bB`2$9lJtC<$z|;sw}}%n&i1j*zZxp9Vb_;(P~eE2-U=P32FepkUiQ_qt^Qa$ zjZ$D~p8FxsAPNRjkZX|jt$n^_Ea9mEaW4b`16h;%SvIEgpNqjl2d0C@3jwgS_@ zF3+XxMjMDg?q4JIzDtsV8pc_h#O)V7lCNy~d?6ksv^6$yT?{6IZ+}$Zth33v*x^?D z8@?jf3`?@u2>LC*k>(~a(-)ErD!(MF^tWK=if$!Msz*te`$X(no4+aDO2*7mh7NcyB!8c=>famko@sF0&o8))eqin(-_$K4 zIpjPx)gWA@k*~Nr-V=ygT?oLY^KrMcmu3{hJsSeKbNF{$HbL`A6`jO3P!VcCLbY_V z0uss+C(iI4?Z#;_C5y$YJd**lGPoAT?mGbpN`NAzA&)DmAY0yS*3NLwVa#3l3GxB{(0C;Uy5xtWFyYM;J|rk)M`4VQ){(kGePMGbjXT^k~)8>4X! zD1LAg>@X}_EuswKGH0l@T>R2&uZ~$gfct%Rg})UNqtHX@pPh(h{6?JqKw6CHeh^?; znbqWEW80*2TYEPk#oQvbTuR6dAK5uQETSVZO@6nmb+aAm?lv&Gvum;81ttU3XZ9iRpah|@EdlJjaUMrcYZqUB#<3Q4c-#at}nXDz(e;~Azu zSEy)Yx=!0fkfxjh?_%l_dTM{x#tyyC@9;-cj{jADGoJhRir%&vdoQqx>m4w{6v9}W z=lsCWR-WQ%@MM_#jvB&8Hx)%&9%~kJXq9H$Pw4 zFy#vRxOi_~LJBT9kBx_U9L_o6!$&>Y3OyjhKRw9~LPo`T0rjfUoE*=ISlwulE#I$4xOR5}!9; zzQ%_SNRNV^auE!ARpiARG$3=ISBu75x--6yyfk;q$5e^TO^-|*Zl!lT%EsX(@9tx!}BT_ z0J~3LbX%^BuyLzri821scHZuYw)uPk&vyQN;ZWg!YX=d`qfq*(Ow=|3Sy*X3t z`7!H=j4S#5oPv*98y6F;dItw~PXv_!4z?XgN-Carm{#_Z{VF${ESKvu^ADdMd~!|D0bTf#XYp?_V? zc`IWb;F`5NlY#OaJBmv@nB4$7ipASh_qrPr$(JlaeXbiS>J6^zvVTk(h(|rR66}Pg zSs(vyHME^r9;HY1yif>qS zgMB3trlF}nNrpB;YC1!79l{3kY~ej^?RrYPnUf3FeLC^=*IkQEL#v=mEl{JUtuF|LPlj1wp!?M+r?DwPdH?i_*X?5^$v zk+;#J^@*At%87wPY8KZWzq-G?<&*RfzD4?G@M&H`G|2#W%!Sr%6<=GJUgbNlY4nXl zYk%C6atDh%?D=zyF?Mn%97{~Xe&KUw`n;b6w9DYtA1JvR83vNZXPxM(u(Gk6VWah_ z-OeiZeS;CKh||CPe)>PWK>v6D{G~7Wfiuijj*LE!;;z;#QL<|`u~68GV?#stqi)D zd)SSZdzt)!Zp9p{Oyt5=L;`CUl<~Pz7Qtp@{V7gaZfTyboj*F zdK7&;f%$wvMRoQs^X=q_&lirp&o`2fuAj~7>~eSI)@hd>A2=&-KIFK^t)KV=Jq!#w z9nYZ-C8+&h?R{rdnrXXkrld^jm_$vi6Ki4}3mW^E*n2G4u*_Jah{lRyo5`dowy3dT zOYERvEP#S#5(~i^MMc39yP#kfdk){;Yp=8ST4$ei{_G!Tt?v)-;$;EPd&Bd*_jBJ@ z2}jiP@YIaf%#}V3ID>V$4|BP1hDf+CH!;=Jx`mwhuaWs~+&5Q-s3siQ3Yto-D=$SM z|Ljd%G!^@EC}6ey%KYodnT*)%q}BZNA}WI7VnMPu%hJ-})UfaM1uY0dNdvq64-|-w zZ(`fFD}LnDk{4X!2zj>42G5buzQGUr8e@DqO)UmOA zAFZQa%O#nlC1pdV*)gqC!4J}Qmrx#phr$xOl9t z=Rg=8FNtOC16ElKOq+}UvV+4);V1R1bgWKEl2VngZaUb6xgAkC8?Rs%b|cwUpQQf7 z3oN!ttHwDGv`+mA?cgH2-shjw7lzE{YG_x~c!5?vUalEVtgcSNz^kzQr*Kp4%`uq`U5+j_81#FQyjtRF(p>qWXDsRW8SeMIZPowG`Jn)a z#nNkx^IYY|4>33w+1q4)cwZng30?KBqTH)sWk|Vf49@i%#Uv)C-_)=oqseKy%1Otk!#cUX_Gyol-?0prGYMLw z|JmogJ*9%Qy#ajq$5*XdAs~yrt&#AwI*Y@DaI+zJoZ5f=JN@rx7xLK;Iu7x?5AP9M zGwDx}zEfAlZ?`StoY)!d0RZ&-_px6Nw|Wkymc@s8UD7P=I-iL9RW9VXK3^V!L}^?h z_h-gdMFt%@nDo|UTReL_uFq?J^ZqZ-voktx+mx>MC{-u^Vg2UZxv~voUuIDJBHKgZ z-IQw@BjMF_*r)tpALg8+E6t$~m?e>P8K|?`6{DVPg0nETNG317z8WWQoR>q0llL9)O4EcdCDIM|U}9FOO|B4=FjX zMquIjkJI$RtB3Ad>sV$IDqlH^ouyMd`eGRF+!>a^&jp(PLtZ)=7y&%-rX!dP51sIX zw&(tp`MsRa9NN3E!WAtb5a_wfK0`8b+g4eGO0(2IGQ(W~ztGWPcA#o`GOzZo zxe5*a4lYG>lp?h7THxWrw8`zj1AlClue@{qf=Z9{kn$GPIDcivPP&I{0^?=HBdmES zCNMNAI(;#}@ZyYRTjxP%rrpH4`msEum%(d^nhbFcxQ8*hvltPp+)|yoIgCp5IZoEGPbvPmQ*60|L$;zX}#pIxpB>E zAabq(l6yYF@AJOau3F`EgT-?{)5Qie7qicmX?zwnKl3%Yq-sfy;_ZBv^CKG=_U zTT=GxDjS6{1G3Z}PftIq7HZ=%C1pV)H#Tg!!!LN9>XcZdgQULpFltpz1?;S88Z{B1 z(Hpa$gfho6LVXl@yr}B340*wl*OcKWuCdvMu^~g(tAHu&jYSF6p8h#xMmV4%!Bu}zgq<H@Xm#Jpb6Uodn+c!q1ObKNz262oHHzcVFiLXS%_KrcbL^!;L} zp)Ee;^+nuA?@UePt-iHzT=RUI2GHgX+JOl#x-$QQTWX6zx3sdhdNjzwK(b1Z=*qaS z=27hG-r_@G2ZpzUcHehtUPyw0KqUqxZ1&v_VEfp^5qcpuZ(q|RVB#=`t4ljTIhq63E$x7lqwOffJkh;j$IW3 zR_Plzy&*lj9I}|0l0kZHK-#y6=xK#{VZ_v-(`?ot`^HW{mhZ#4Y}-lPk*hB?CXUOm z8g}2SLDtm3xz3?qOr_ux2n4wFskyc~+gN^isy!?yWT1}T1g14TOH8ZIOT!7a39Uh~ z(~(Nv2x_B{YAtu+QXT`9On0_zXyhnm_zSzxtE@1ulhyT>LF9tp z(4`rjlbH6Fk*Ga}Fpr+B3hM?_d{&4USKI2j((TP(<6!4_Ev>Vl0gjjjJqkx=H0_RZ zE>-8>zVY0YSA6HDT3ec%J0=JbskqFup9m?Z)Y#Of3;wmaa>asSd`h=@gv@DM&o>eInX@W)-}`k z$;hL}6Vr|!_MZz$ZS{x4^%-Raqbp!cYN&kl9G5ov?cBz3a;zz0j>JVeR{m35oKn7~ z9iF)J$$UmPTDwEoGXu@SAa)ImEr#k|#I~8?KWelwZ5P`wDx9%+tsJ2=MovOR9VrBS z>;5uCrI36xP3=k9ijfA!lb{OQ6T<>s;V(%o?2YjKyYUyEl0chtV7j}m2l!Q+cK96W z{iW`KXdEoo%#!6H=b+h&Fc#K)7_rGP(IuZAwdj0Su%2xGv)}nasIziM%=3>qlrIzz zm?6u!zl1iCnZuKKwKGg6TzhIf!L&WAiZI9d?vqEGRKEr;w!d+_C{+tM&UN7R} zbUgu2cM%t%O&mM0+&uwYy!A0fTQ|kndax@nx{{+VN6y3*EG2gtWhWH@DqF==x$dVh zNwa++PDZ$Ed+F4%q_Ec^gwN~RQRYmaNB*-&SNaH$blGJ@l9>-47G$3}Cbvsl|Lrju zNjIGsl(mSPonxrb<~ENt!8B@F<0%XFkol)q`_qA20w-3~OaKeKNU@AyQmj=*g8hUS zme~g(YK=-OEYynM?451FmsfAMT-RL>K#o^49(Y&5YzV~#4sz+)Jf|X>GT^TP2?>!x zME^E;pghQP-QZY_MFKl>dv9Y^*Bpi-@q(?*@>)ePHvWm@ZTS=vE(inTmzL!EIP;hP zCX+r5Gq==(?AUvj$Que*QW)DVkTKbWH3Va^<(*?e4 zdS)~8)8>nZyNLY`^0v0|ckD2c4R=OJ!TA-rCUR58BgN0zX_;d--aCugvOGm5%%&2q zc#lU)mu39z>-47Ob#|pbvu+VLJG%`=M_znPe9d++>r3?>y?th7`uKAz%3s1M|I3*D zKNDe(bMj>bAKKb)tn|-x7QE6$cAAg6vKJolHdFWR*J2@CoXHa0WK4*P{S2pym6ldk z|0uWn%5QRfA+ox~Tks{UuEV3Cc$QHBimt6jSDWNRa4q%a*$OcP8&^~krda03fL+kH za~0RU5{iCJ=-K{5Q}_4nx)QfjB4$Yk6Qy3_07((nes=qh-BQqq%VGUBe!Vofv4j zT{)u$=tOg7WvZ~#;Ch#&z0vu0?*|Y5HZ7hK8X7!(begTM7!U?}KB4h4=zZ-+42P~= z0|~^hR!@d~nDsJKD7lwquMEh~*sXF6E#s+mD7|oz@x#i6IrfUEy827OZ|B0B$2Z)^ zW*rzNQqFUFOK<7C4YVQn@FX$c<2HDWNvDK;9tv* zi91()xoA()M!w{=@r7$X<2Y?kf(tXr&oq{*25WDz!Gx9uBo<%#!!-VsHv)K;*N-C* z9EaR=6|s$$s)LU0#vO8`Lauh-7}xp*2>kxxR$tp-e`MP3R-mxFjGgyXE4kJY`|*QM z2Cz0@iT{2(kZ@?04u?yZmWs*fLQCu6iRrPwH)-+jW0rlRi-?4T$XJqO%SMll?Nq@v zx_QyaoVn>!XQ9b=&C`%CWFemZRHF*OjhqaM(k*nmpZUFOO6{OV!rS&n3%x)_D7ZEz zSd13iAFq|_1;H@sNz(2pgDkR}_d(xQT)XZU#n{WbyAJHi6?Qx=^m1J9XLkPWQK+`* z+w^uSepd8!jT$lmFR z1!Yh7s~?NitB5y&mg)W9Pg-7cldlr;k?;rSZ_4Z6|7!XUENOD@rD$o{QUYQH`@Uy= zu1$M_WjAYvZy^1Gzl?vTCa_H-MpSS#0#?c^^FvmA)C#f*0{POJ8GbK|4ZbKFrizNy z5VBLFZd#9KO_#haxZ6U9(x~{iJ6}R)Bk9JH@%oT3!DsJETU|YTabM^ak;wCwZU&(> z%kH`Qf-cK4vcv1wyu5ayKGA$X@vX4&(haY7N}cjSQb5z&*hre$-ndUv!iVux-P`djKMz&=m4P|^CfVS zv<3UMrfZcDLx8F|`^^YR3j}zqA+l$z=V-duSHXDpv zf+ppZwl@qx%Q9U8U@eQ|&P&f-oZHvLvj@E<;3)LiW2OC~skV;E4!ac%b;pR{+j(bG zUi;TR6K3iho%5|tjZLjfCQ2k4xGK>G2G*O$x4N$LS+}2@?V0X4K$_=$jyR1)wYYIC z7ha!dql?O{Bm`xH1EkHO@43K_5~*y}wz%8FTFA9gLaS76Y`n>$Tl(?C$<6jV)*$^{ zS9X)2NBR;!Jc*4r@74`JsT&DdoL@KKXEtY^9E* z1U3@B%KlmT{Tt``jA4RFhDd4*(9VsQziVsf2ERtmPH@)wt$Fw&x<=CwJ2I@K852fI zzkZWVhw4FIONJWOp?#$ulI+*mEya}`Y&;=;gv-;~*cQ?jJKYjZ3SJM{C|OTfPG|WC zo2s*Sh#gH1LjxFs~yfe?{H`o6YL+s6ZLYVdB z>~3}q<2hZ6?<-U#;%v{7*We&t8b(H+!N5yEO;lR{q_|-~m5XxAQH(0pE#mRyRzh^3 zc3Gn{W!@X@H(Y@}x}O{N!N_va;@i2%Mm^PpfCb{1&!iO(jRXsh;>vo>o8KUZ>Z*pe z+-FC80fFN?4XqDqw*xx9kE;9A;2`dKe#;;Yzsg$M9+&dk?2>k4kOKmh2VnE!iv&xUb&f!$6+{+^-E@GccvW%FH74noY%E?=Q-HYsK*e$Wgf`)nW*m@= zNi#IGml8f0fV`F%+SDl?5Ua1Ux9Ik>HuQC}(tkxvU4BT`>>6yWb0vxB=BMe{?){KA zf2Zf`yrabpzoKydx+A1e-&Z*Cc(hyqbF8vajLN;cTf#UunOZO*S1F%gf7S zB&8|ImZ`}sm5&UgeO2?eyLMZhQ&GDLZNxP_I$TsexETs7-pi z0qrVwhr+S}XV${k+Be#~LOwSRnvD1ffvhEZvUt{P3tycUE#Ix(Z>t`OrF`<$H7&_# z(fLp0pueLuj!WamX93Rfhi8*#PG@6NkrSQ2u3yB@^m*m+s@hKs?J5d48_|M_ll+=# z^Ue07GL0Vxi=#_q3yY~5j&p#WHhM~aOXc#UwDSQzJyV{`5H&C`M)2MErTfnQLr-x_ zf9Dd5o<=>n{{F)mnAWNnpAM4EA~qF31wg;}^bWk~X@aLXxCTX?o?piJ)~F5-))t%c z#-j@K<_5t*8X_~HA^Q+_T{GHP;-0}{%!Y!%!pssn%Anu6PwAH4E1!tCiI~(7@mHXG zDW<&^my<)GTJq%)hFRw@@82W%51;ig^8?CTx}VUV3X0PPV)1Q*Zais>P~0DE#|Y>Y zB_x5cRH$0M)oQV}oH*j;yv9h}>8Y>%cCKUx(oW~~QhP`S8(-$SXvIY&jc5|2{K%ht zf5A~XK^r=~wQ41oSZ$l;YJu4#qV|I-Mj2@tgkApx)=Pd`z^_y)6e> z3Dvl$K-SPmV^1oF)8yk-R@SMYt=(Qga!HtQf27((DVB+#zKCr^>-`A(xesd|x(rRC z?FU`xpmYsLna|W*uInhP;uJZQusxPw?Va>XJ^A^*WFAuDmXLRiq8OBVt)M z8?r{Brx!7`DHRh8sPw7uEVsx!Hk+)y3(=ddS}-1DOeNrh4NitsXNuD|p3_86nP(Ml zmQ7pw9KMB;060V#%k$ClN7%e(hFlWt`V4z4((05jgp|ksDXWFsjI`;1(^{XB;DFrYGOrIF4HpHc`XD*(2P0(Y{iZ!CGCw~vRj=zV+dC5|Mli9 z5c}sl9lmep82Wj$D;A<#t?w>9n0)<8Xm#ne3UmnaFrfn=c%vGnsUfD2mjg4| z$g&+(nw8e6a~K&V=&7K?nHe3I_{^WJgPG^6-WA8xJ-d=6ADFsv2c5y@4i=bU%hK8BKk>WBNkhZ$#^7^A{zai2G z%!`l-Nbv}G`lHj#mGIL-9mfgw`V%gZnGM*Eu$BD_K_yvCZqstUP)TydSDDH8UJ0xm zJpEh|L;zQdw#$rkwECf{)EBWIYr+Kh6_5C6ESNq1%2WR7T47XG3QvdaV`p@f)eJ6TYKKYrofY2rkIpny| zS%-KBr!TR$uN_1R&CJg&FZKy@lLSO)nym^7G(6O0)@_Cfh`iBK;pluM)@)XiIwHHD zCqvO()cbxMo?wWWWN>5T87f;;tchQ6IM3jHn~ zPaLZgY#8@;Y{@L{d-Nj~n|;I3q5DUk+6En)>Y6eac>AUEyf9(C;RLac!Mud|+<$Pl z48QL+o_#H_Ien*c)bi|m>cgz7_9yoA`@aMzU+|1tD z-_)pqqYE0fWUybitGkM*JNWt1;jDB3rzBM8Nz}~DZyJ>SzGtBmn<%*-G{cx;hYF>o z6K_>V42~ka6YH9lO){KVu%febQv9bc&)!=feLLsYao@*d^KUQV4x(%@Uk~(skB0N- z>+Zi7RP>(MmLd=1A4m@DMYnka@5JH~7mAB9@Au>#KvhZPWtC624Xv-;zP{FSBc^y! z`xV>Rji%Mei^`mA$q{Hl-?fT?R=h(ZeSE%`$mQ?cMp+t~>zhn_<(U4K=`4aUNMyfw zqsOm6YTM@wiPRFH->*P?HEA~5fN{3yswJ|~3?4K9| zgD0J**GpDAGCWo+^tR03{oK34tw|o&9wligcDr6w2;xs7O7v}Ovp07UehNCly$_sL zy~>ida42I(bF*b*aavcX1e{RM^Vk^{u94kxif-vf?2E2wm0>k2kF{`TOJci}#(PM( z#$D^%4dTF2Jlt9aX6VM>Zy#)JPg9l8CCreXNoN(B5dtqhYi$^Q1-Z8E00cE$VeL<* z4+Xb;DJ5TnoOc@=cq#Q#h)`OFs0~9bbFIPOKfx!MQzO|o+yYeEM^CajHCKl?tE^Fu zIqojVi3AYpG3AxWj-=K77$7qsF0g3e{&xQtNPJ)04l$WL_-XWhi{iDaI&)*o^Oic65-y#pR}Fz-!eindqhE-B{6|Xte?ES<K5JB8vSW(&9Ct((Baixo*>4kR2{eZ}nxnjw4W7MJ+zBG`vd4CGTw{!X#6NWr#)xSkOWYw6Eok7Re&=Aj{m& zO`eCAltg)j=xGy@_E5B{R+m_TF5z(5EW@U8O-%r0r6?-o@Xl=5>B+L9YM3^-Ahxi_ zBbl}Da}Dwrq`Kht8yj>(*9U7Gw!dwA?C@dkg|@NlR{0WP8Oddu&g<;gaucDo&`C%g zyOd!cq8U28;mmNe2~Tq!2*UgSsa=YUd|ty_Ikxr!{B{sPAK5rc>|m0=Z?^3C<} zW!!&}h|v8{8-V)A|96Rqwt=(B6uXR=dpYZBVWW9Dzx&sbz>BY|2H-MGKfA;Fh=9a{ zvCsQ5$3ltVbh%~z3AWNo)d=ZnQ*0KdIB7LItCj8;0-sYxBnwkCJ)64Yei_I35sexg#!aA8`vla3+?LGmN8tgT3ARAHlSWOKDV8M9R;lf*_ zfgS3FnTXD0cO8eO-TvsZQt7&styD=xcCKXmNLH?#f6V3x%u{bcM>E2}h+y>rt;;+m zwrJ2e`D2C12^X0OHF;{+z^^HTeO3*W37Mr@4h~e4ac6Zd{FYWTu4_VmR)CUBU%|mc2Y2UY)`)5)VSHFfJpEE}bzU*g zEy@^`rzltvrJpDGoe&)hctGGcQ>QCLcbAr8w|mG=^?iW$G>9z%7_dquIBW6) z1H@L5sS{CJAL#5d`39HW+O^wx>A)(CHqEFlKEZdTU8}G;hKI+w_|{XZoAy+cbqQD1 z30=kb$WkKmxzWqm-+hiz-M{KNWC`S(nPh(UDpme0Q0=<3BG{3KFOQjzt)slMA)uc;$Yq?5qkIW!onwsdkF0T~zC zaf2vD)f$ij9)ie|_*}QBElN@!akzP6F2&9ykK(e%};)Tb_& zML~661xyp_I@Iq^3swA{DeHYZN!ea^(pvlok!gO*w)heP;U2gNC@SH9c*1I=ty0%dIl;V-ek~ zuXWRf{JA)o0Q@w!>Vxr%X_UXyp;!}@G96I!KC?n_PEe-6DaGp)zIp_-0akygJKxfC z>0_?H3QtZjJ(0t0xNAm-udnI)Q+tP?vp!k#RNZ2duWVhWr*G6z#N5hQT{D;|KYd;I z8A)(1Z5(oyKaSNnqM(%M&dN^Pmaax3466|403E@_;O$51}%>~7kqDq%EI4|>{ z#N!QRwp24qr^bMECoyqcD=dhoJX$C3>sFxFWEL2Yx$Kqd^}?!+i$8Cyx;q}*>bKb= za=}%ZtqBdI2c%irjydfV1O*#fW*b61AK92Yha$H{<;}DV0>qg&jTIZKx_^2d{lJnV zF(Bxl>=0t@XQiaXgUMiyXs;{EwSJxPD(LF!o|ksNB@~`t6u!#X4G-AU^ha%l6SL0J zxEcR4bECT$rdGnHY?ZBqv-Rls(s;j2E5Xo?kcWC?p|H0g8^fSxzXMXQ$ei(;PCK1; z>$m6#zWmtL)s@QZaCk-UOg^YAgEzv`1{z!v>gr1OkHa`O*&cxbBhoLr%Pc*;Dz)KS zxf^wj1FlVp`It5xA+CDLl2yliD_i{AIdQd>Z6nL0XBfTAsk*)V4w(`g$usrorY`{Llv_auzQm4J|dD+7Rl(c#gNPdznjY+%T)j0MZCk2?qL z=3YXNJJGok6Fm#pwk?Sv9syvb@~%VKTVqSU#~m}S$1GB38~dmABI>0!>7*ew>i&e+mz!{J9Lv) z;<%1&g6>MFo;QABXaH!l6HjTGy1?dS2LXC| zYumiEk`-1wo2ZdiG*HnGp^_aSZ%3@SvsgyrxXy6hUl{|2V^^w()Xu?Zu6|e$f9EEv z64AA>S4sQ zbe92ckx?E0Yd4rCCM92)u{a`ibK-@{-~Z=K>RdoPmvgYzSIo~Yxx@8(Ks%mr_}e_k zkF&kpnXrJGmp{JUG*vc&@1-_qpw8X`8C2_^9~{?9QD85$o^4nueT*BSX@-COt3jqO z&E}06E}#m;SbG;6jKoh>uN6=QtGF16G`J}bubt6a>BU(>iT`zmkSn`WI!c71Htx%t zi>eNG?05O;9Rw)pRb7Uk@()-=p&m@T>AR;ow)~QiN7A7Iimtx7UY1ETX9jNkiH$hs z`7Gbwu&yW!vB#>{RN$sKbY{a^1+RG3dz`MoT7^YJgIQ(cd&zLsb>NOf%(URoKXo$7 zzHZz4K=%Rhff@*BoV8-?p@S zLOIJTo-CYZEft44?Dfk@AKiltt!=r~8d>Ma@p~k|={E71NSd>(%rw6~_%}C6BbmJ@ za9PtepDBEUb%=r@hX=!+oYZX)DCF)|9POg}_75q;w*5@nsc^W{e2cD(8BJzo&r($} zqd^(SOa`txb@DAjB~BfMyVT-q5*^qYAYA2e?uhO{o==kuuFCY$lXjqM$k+@!OBwOg zy-=Ky(WG=(gpoqcJ3h&YohFPHO)(MN1fbqgzP3kR(<^sAGc**;x6}#Fd1E&`d{~_s zgOz(Q(w&ez!;ly*io13V$aG`*w8oL6vf=K{NMFc84jr5TXQkMw;hs<0*afzT`(x}k z$6c!`pGyO_C}RPKpfi0V=90k=xr{27KS8+hp-uGbOa~*ULtwLGZ}Paa{>EGWyVa`@ z=Ft_PI1`;YWGR)h6JS^B$k@0HuU>R`=QUG3Gc(n}R_xMgr=1CUic*A_q&zilpYQ#l zL!bZl{hNKxCJl%9UV=#x$JuEnr^RgngAl706ep@*J(MOFcd4`QuBWMQ&q}O?oP1BU zOg(uIE?Yx2c=H9&4o90iqQIdh5_8%`k8Z zq*KXy+Pj5lVNp$ZvZ;v4@t=I&<&@?)m|APWGU`H~5){*_&qsw^Y3c||8<<|KD|r2} z@VW&DcOlH~Jl`YJ3#Mr{_O?#o3g?1b$YA_s=f&DaD~_D9xW{s54QzO%P2jF|wO^y2 zCNwwij&V49vO&Hxpe-sv>QCxt$%?Jnha*4tad> zAD?zoO2+i2bI*M{x7rbg`_xGqKztVDz5bOgrv#)iE3qxpfM1oe+`uv-#M_yCfaU=da$@*Dkh$Ye6AA?IPh+HiWIVZ1e$=5=4?_%Pu5%u_s z{;K^!wN>_8)CqX?U23&{eX;2@-^b+bTPaX@EVzIO6|i-dS*y%UL+pTkY^lZuN4=2* zmuLn`=n&~+z%XuOMJDcM{+cpT_srT+mkL1yx?A^{``Qr34<`LyKLc0KqjG@TBNC{h z@dBcJRzISwbv(Qq6|hVsJ)yO|(7L^HHnfY59G(pR|ximtbnYe3@dfOsBzkd=zLXQekYOLU(cgu20WGrkfJZ z{6@if_eX;HQTQqJ*^VgysJ*A$Uisj`6p9pHGmJ?U`Z_@`%Yq@aTIT^%*6k3(> zgQm^A8?@N@D{8YOeZJX;oJ;3Hq@+?Ho0qPreN`5Rx%=EgrFlBWtw|kl8IjTMp;iH=*yWpZeZH3tW zgJdzWniJfgyGtE`?E%nt>uX6Yb+xTkHDZhVEKm~CfLjP@oI*npM>4?DID?&qY~JB+ z>+mX=CO2Q2ulB9(PO477v_l&F4ff%oVdMCq!s3bVyD6%tYD&XI!&<3)FUm~QXB$~y z=jcgnxLlqlRVJ_mmUJkUOAEsSuX0`DI($W9y@koaI?FLl^4H4ZWko%n^=t-Qh6j+p za*eIzi#3=IX)f@m50LVMPX6yq%QN^ZhQfqJ(y!$HH+RwhI4uke`suGJREb+i2xfTY&U-Vr7YfvP#Fii2v%3K&+3QxsY{O^bZ%nX&DuxQ*p2(66eqPvcKF6gz@l zn@?v&i6Ygy|N8}9PVOuoV@+J(c#U13zjUwi<=fHz!0;{XU7IHs1Nlz<`?GIfJy5RQQi|V?q!>2Xf+Xq3`1z=SzWqe&d z+yRZa$+qgFDb69$)yw*o6IX`*(l3*zC19{Bo;4L+Sq5nVAy-yrs${4 zP;}5yhaPQt6w8MX_Y2K$e;6e+bO)A~)`U4oo10o*RdkRk%SNrzk}Io|tHw2Ebv6WC zD7yLaj=bO=v3nE3Rzbqw&Iyqk`YdolmCuV;Jd1dwOPe3@8r&39v=v{-7aAM3^SG_0 zvpu?k(eQKi}LCr|M?QSm*cSK{E07shzEAME< z{k5+^J~)kTYQG-8T7tz70Ig^B=z4vkG{Tv}`u<1|SD;PJlS*{Lmfgou<{p;6v0HSY z$-cE|2~;Jtq|~Mvl^(>{TuWF1ogBOSv=nN{n-=1aRFqB`+h)*STohjLDo+$clyqZQtIzGC#BlQ)0P3G2cV@9I_ra>x#g7zYURUsV5iZe&o5%sz)MZE9W`#H zV6bs52P~qwQ0u_u42=ohBV*o|whUWBS|0Z%N9c>5H`HDa%;*zJb!bcX%CH#-mEnS# z;I9Q}U?n9@3k?f{o~$q3zv}7)@{PMQl_B*Xw@$(&Y78O3Po3N!i0NxA(_(AtFXlCk zLe;^Gr-uO3L15m#98PWcKSfZtEn4$5;%CoSPHc@g>ZtH0mVCUvjla~X0Kl|N$=|k{ za<5()wKV@>F|_n4ye_}jnzOWeQB+7c*Bm#3ZyW(TMUqfs}0{g*p4TNP*(k?2s23^m0-e@lwzmE|)~ z(>Ppu8h8d9CFGpX_NLalm0sG5O2)zF^0R`?)uv^VGR(AkYw z!sb&qtQ6piPUQg%^8k)M9m+?KQ&bqTCUuumg+&$u+XpwUsosuhNt$dxXaXH6h8eWg zAd93vmV+8jgQw=?b(Md=x}bA@XXa==2cooNp{cGVrv7@uXH%BN2Z~{vCzQDNp$)#}Lq1f>PcM90rmHW5J4bldZ+y^|x#8D6R zKWG;H@gFPc|J3ndGo|Nlfl I@7v`60*4u+MgRZ+ literal 0 HcmV?d00001 diff --git a/example/src/WebEid.AspNetCore.Example/wwwroot/img/eu-fund-flags.png b/example/src/WebEid.AspNetCore.Example/wwwroot/img/eu-fund-flags.png new file mode 100644 index 0000000000000000000000000000000000000000..2327bdd2bff57bf9e0fcc7bad4090d856196e39e GIT binary patch literal 25226 zcmce-c{r5s|2I4$WUVB-X_F#LS+k9(d@7+uL!`<(3HCm|}5>);;|zZ>R$4?G?H0&RU9AlIIG+BuxMoDXWl|wf=SVWeYVzm%!*t4H#3@$3aIm7D~6!5O17u(kH_H8+L zcgnFKx^xGT$J^_@;Hj%`o`qIU5;8L5oECm>o41t5N0t@Zcm8Kz5peJyU>AP81 zr4M^Z)y?E<5I50QF<~5=qmT|GZg_gPJJ=bHAx$1DV)un9+GR8OYxnc#~%e zU5+Jn>1AhuR^#WBrNF;)P~im|yk;DT2(g+8GI0@3xfr(hFSh!^DKaahzYpJZVry4I zc-3-RUD0YpH|ySCHC+Du4rR>B@OnH3HTmrIu*k6W5y=`r;3tUwfTOyKPyPz<0TT2K!B zwA`6++%a=`Z@uQORs)69`&{7D9kW0i$(l!jKHX=^`Sf$fK8B%SJ00xA%_zD)3%yfv z{zLaWmomM%Cy+$vvk_S4Zl+rh%P}0-N)C9D9;DmtmqRI^Zy@IJmQ!6TTnmoSLJRjA zOTR|47^+L1^2>bi5dz7QJp_2AzyfS$^B=gsPJU)H41v6Wfew_N@ml%eH8mB;y0I7} z?-LtSk=fuO##&nY$9O?0utL1-W}$|xfa-7-hZv+o{19`TE{p@7NE}$>g}{_KAeS&^ zpbj?2A{oL&d(Mf|dqW)6UwFagz7%3ku;(_5OL<~9i?N!`=WoY8c1#>?j=Il0qJGy% zgmRssG)(8+lbDB5Iw!>%1KQZj#*XHR_e;iab3n|Snmy-qzRIgcXumUps9TelZz$b9 zRGiZZg**+`L#(L7v!O0;1Ry#qWCT-7>Mw33$LwpyLn~w@09f9MGQSuKE9ATMme>gY z9RabQ0r_}D&kB$g90ERU0j2DA4ZrQ@mpg_8`cml7)O>K>EdNLtamQFHnYVyN&NP({ z>!raQcVN%oN+KW;BT)?De;lLX&kNrx!VYjz_jLo9GbNoQ@9$uDhN%3!{qx|q!j}7x z2lfZiaC3MFa1B)T=dQ~r!xG6`^7;iVvNA-5v2?O+F4H`+1ep>@DTiv@asC6u-DUxW z+k9USF!xcB_=6N?&5-J)0lGUH8Ml{an2tW8Cla9;^Ln9k+PM~@F`Be+9LY$KIn1=c z$9f1J{vqcSNImu+QAX#-L(IoRvJsbrC-|pj;(N#4mtoB3s`L;#;78~zL7!s*<51pRhvd_y zFG`LY5VSrt#&{Z8k0$<%oaHZ@dL+Sx@0an1)MuZ5s_M>m!IN_=MfH%I$?4^wn$BCA ziK9Cy_xlxi_Bk|KBLi^-oVuf`Y;7~xg^J7C%Cp6`jIxNc1*{UTcaAfh3@J!(#wXy6 z$(cf}pY{1N4u|}E`l;voEkpU2qAXcCpW>-dQ!X|{gldEHbmZLr*kRm#o~%(Z7UDN9 zzmMt$o!_?-X8HR!XMql5loxM5HV)z~A5r+%MB1a>7Ig6%?-^6VlHL1aAxk25+yv9v(5+HYfNu5@TtC+M1nKDWr30 zl^n*}-^bYes2Dg)5Zc^@#nbi3R8(U@6+%7mQaUApK_olv$=V!)2QMH_A{ZR`G(C0F z&jXBFtwvPjF#R}Vem}@9rPBTknf2i@<6mlFl=6Dg!IF@c={^xwPwB%>8#k42+B+p= zo`hdf`_iwgpawCNHVfv3XbZ33$!Ey#7)bXk6=N{5W}2vXDvo$Zvl z-pK}dHttp@hU{75sW2}52V)N*+KRBNe5ij)1NB!G91V+6GqvlonNWuK*)#)bFBPJN znh@WGHxZ21&)Hifr~O|Pl+T}Ce9tJYQQhS!nOEO-D_NgxuoSp9A^w&y-wJyH{#Xfg z*elQ;;F_L}GyXu`DbL6oYT6?ei_`mCi+D7Zaf4FJteS}atsR{`7^l*xC zYn}s7IRQ<|u*C%$wAy&kgE;f$Y_E0d)rOElTbEGgqq{1sU!}F#o+@iEk*}MN;Ac-r z{|Noltq@IsGccx6G_#9F?Az~RFbB6USj&j9%GZXNsxKj6#rph$uXx>uV(4IOlJFc_ zw#OF@C9XLz8ZH+tHC4)r*KT)stpvxx7;89ge1%m*QgJe-!Vw}d0XZrSnM$LnXIisA z%;lYXM;}WA%xq{>uRTxrRrG$*o}Q&Bt50X^cq!~eFJ@H;TLiR=06lL3HdlqNL97cs)~bbAYm?YthcTCPq* zyeznM@0$TIo1c($I2?V@-Qa{eX-yk}@!8NHaoV8ggY=sxY64qdLGWONTz;ZftXa0@ zE8fJIsS7Omv3nt!dXzk}-C4Ypach@Xc>L(c3_Lt_}dS#jSc90mkWGwT2 z-9#zkEiYuK%R&9XJKyEE_d?y#CZE1Lwj>OB&1-b=G;L{Hy@v)(`&eoju=h-pKJ0|3 ztDR)c3Uw>ASi?y0ao zZ)ACTrcOh;2W9$Vrwp+qh-{TEiPQhlCqq0lv=JKWtlaF}elvX$w0%6{rz+@0H1rPM z?SD}B3zvFQwfQ>6k%U=&;YkYr1xe)rD!Zgmv>@NN=vKHW5uoBtiSd%vv^a>^;8o_( zN9ttE_QL#%naeU|q$(lPZ_WT{dL$bb3nbKZ z;)F7ibV`g}+52xhUwEAMB*cF3gZf9{{bWK^g4NDoD_gh;VCrQN6aW;qBFMi?wAju` zat+q&vji-_msT#T+}z@ru|He=aIrQn@7T@OJeF>Cinj878;- z7Nw^*F#3*8oi^>6Jar{^-jIR|T5@l6#;U}h@OegZ z>pZY2h(|EOnn!Zv82H(2uXeJC?Vda6lP-E+`4u+tgPrLjH@t^b_+vB~tT1Bv9_;GgozI8dwwn}C*-u^A z_sWG0Kittd*p83qq-{S8L&5XAD9b$FSDMvP*XMU3Z_g#)I(!{GL&qU0jw?7)oh_zS_2B1okv3wct26^Sg`ikm2Wm7r+A@imH8zO68wUn+$JWA~_mNkE= zLdA>}i1dRSlIcP15Pnn~W>_5v9fsH6oHCwgq@r1#B0n>Z(ITsB|M*My-G}xm*=*4E zOT+J`pma-duBso(z%j=IK3KwT_Yrw5ud)zoaWiU$tfI2CGW&p;GE8GSkO0@g2T zRvbFlHolS&J$H3&KKRf4!PG&=Zg?XpOi`X7s=H zk;*iF8DVhvam!Ji`Ym6rzOc;^EBrT4n=&uU^f<6V$Z(7eEn6l|aMmgCzQt5TJS=!7mOOH$Cu~ ztO{&WXta0nEe~u^??ERZOA1AFH?~l{WK90I!?ZoZpO+>)*Ab+brZTtt(qP=p16w~f z3}UIkYgML6dEe06;z&Bj_ZYFVObh-)2wVR3W(Jrs7)3@VxbfqFdjOx6nNoxyFT|b~ z2?&R6Qt~@vuW?ekSMXvD93SXFzU`mBcvISDKqs|w$|qXynqYmmpU-2P4IzCsE#XY< zEqlRLUnjm_f_(-yRh=5$_DeCpFBK}q$0(W6N1KX7q%C~M3$atvDq?YSM^_qU29#_h z5lbuI7F_O3Ylf4!4X^;I0nn!Pq4>WFpJUoe8Q#9IwLdr4^3R9v=OOC6-!mi;%a_}a zPF!EM2`Wl;NY=W!%t5(3oe2zj(@^x$d~UkQ9}LvmG#z(Fl6VD1gTW(H?hAvdIHdk8 zt@D%sl8H`M-yqR#SQ|+c#o4P7v^@yK>KJ2w<$y8$$so2ETjEJL6`zyP?rRs~gi)=4odtY&Any(Q@=Mhyj19O7J9Z|vsM7NszR2fTU zjSCF`WV5;_@})DdPAFVpM=%I-2pfR{f(9H)IQ zQV$H|D$^h?JQYeA-Zv(^CVn#3iXrT?1jjBg9Qvz2nQUg z_CG*@?Y$UO1U}4dc~C5!QxLE53|$fuD3)QldwBuOBtou%(ct*$QjC>Ec%~;AT@GG0L6yE{Vl${N+Y&V(-jNPian%96aon$N`Uqbwx$x!1GVj-Kd!|fx_ zKY_^E;`wm)R^8peD6>p!#4^Lx#&^ekJ7sehPO|ccl3E9b@e`qNg{Wj>vo-k%>CC$A z5ig?i=`f&uBCz56Pn~1@92ch^AW)p0dct*UNn5{b6ii&R9=IFIdoKg;?(6E`^r@S= zo^6t10s@)kbI3>0u7v4FJR!ra;a0a48}}L~F!1H9eq1rNtxj|E2;%!nBEAOqdqR>1 zM!Tr)fQ<@ch=g449x5tXn312@9TBUm-;p@l_i#re)s!o#)dcbUL4U&F$h$eE%n?NfZ6ebe=Y>LK>GsBP}VBU!Ro;Gs{IfmD{Qm`~jkYmuY0Y>t=K;+Kya z@X?<2o_?>rxH$ACN6juFJSR@CJ$w$^ur8ph_6E%`dVDA@?LLT8my@!RZ=@G(fU7a! z>2Cm^6#un7$UVM^*zd$CVT?hz>Q^4ri11O#JKkTG{X~ArO9UK6&LXureBLAt4m^z^ z>J0qTM_=0CVMPlt=C>JY;2FA|`7Z^PSO=Z|m1%{{){UO*tB2QEy)`>Grhn2ovhK7Z z(u>?Mw{`XBt|J7P$}F)y+6UbJLu~?fkb*`0$`u)&A@liyJj*KfU5l@-=;~K5`qbGn zMPuaGF~C%L>FF?>nSxB_8}wk_KV*EfRf8d_bioSwE{gUm4eow0 z2Gy@;jlBh`ab?!({$)d|IVT}yLR>Z6t}#fS_%q9?7D3PiHM+vM?Z~P5>+durgF?Mu zbpCt|@Q^q{6lr2R{Be_WdvMS)Bf%Ke+S<3RwO;p{KROg@h4Heyxcmt<^UDjEdDIj> zuXD_aoS;NcZaX5`HxoSf?G!IK#cy-P1*oV>HXAs@t)DRmLVeh#`OKqy#TqLfmzNhk zv?cd=NCfxX4a+HMoyI~64mCM}cUqz^2nVnPVlacH%?D)`~_!IJ3FqF<6 z-TM80nXopa-?4qZT8xvvcuj#1$S0jZ?yC)DC9WHQs%R#KcQ{{HuP>E#THrQ#m=N8X zXkh{RwSahV;h_HvJ;4=?y~clD`KK#*2oR><^Zple^;1O89tFfykbeJ;j}Xx-O!0t} zW}IKr4qb-jbGtQgj|uG|S!sqBSd^bVIl^R3mPMUQ0UdB?^}>G?g8BV&3tK6unqVEm z6g29)CB(06&s3AT7ulKI0VKi$oci6kuFv>ap1*J4MKk)#j!6BTd(u#b<(cZo?!_IZ z8bWkOFPz{~>KcZFZ!p{esStN2lxT*t_ge_uhJ1GSa)E&`R8|3k}P3S4b&O^2h4?bE&BY867F2j20(~xe(!l*O**h} z1YklMS+0IHuIN?}+8Ilos*GbUS?=N0^$9P;1fDXGZ%hV22qVMQ@0s`UFY9U!cfiu_ z@R0I{W1Zfp7sK@-x}~xwxI$mvVw@`Et$X5OE<%~ScpLKO4L+Q_XYWtg?NbOUSlvK` z4YT53CRkaL$}0_wxnZBh?l0>EGBaMYOhzF1*xC}zZznE4+KbR2jU`!|Tan692fgjg zJSwYSw>0Ip{NzWLE3eUv^BpH>ks)xO<>XdvwHaY~?b7W|YoO3rNTDhWFgX2Wr)wkD zuy*%#cdL@xjHop=^$)S~s0+aL?Zu7offZ%kJ`s(s*s_rsqV{OHH@h)lB!Cmdhm8ShNRH(_6Sz=2s~u0T9& zI(?)1yBQ&Rd&a}$Q4p7gy5p*!9_a!QH0i7XM%t3a0_DYSE0Kla9uYMf5e z3S6y-9&5)!@9+Yt-D9+DG?)AD@mv%W9LN>o=zLbA-hVChQ_4_z70 zVlFqUa76o9tFtNm-h9yd-BLgy_9k`)aey`T1Q-cMO>(M7+hPty=;Vw-ul!u}31Pne zm{>0?SSLG(k2n=d_>6j2-1?`=#za{(;JyN*+(c^{2L|7Zy%O)MvYAziM_qdSv?gt1HDy&RNli7 z?GlpnWhO37AdkuYov#6 z*4@=6whP(t;Anr;1ic-2!N7(0gY$sdedeIos)emvOLsoGFn(+d5id^|hwxssoAhgGHy`s80 zB9=5TFc6JGB9Yis71s%hc)a-4<9w1@aPmS=kD+=P*=u_V3C#qW_Is52qLwm4d)Zwx z!n|Y*TcGAsVk26Q#DAiy%=UMUi@59R_@kXX8vWt5i1uZOdKbt(xgx^vjrrktKa-Nj zI92vD5XTwh0*=-U{4W>IJ%vMxdAcz>vY6w&SC`V?^(`gm z<~KcPkBL;bI~6ZZ#`7bD;ae}Emy}PI&HnR&mE%)3Vi_!LWSJQvb%Zye`f%}7XeePm zH!*~unX5D4KWY`&y3waYbxX;}kc&U@iuaHWKpCY5H$_H1kylHN0z^XJg7E?J0^o`j zSg{JxvbLmEQ-SXm%pX6v92(Uiu|D-2p6Hd@bi(oMxr<_}OjY%4}a+ z(LV8=fTStIBO$*zPa< zY|G2`r$WeJZtCyj{2Z^GILT?4gS5E|5$b2YJXr9#ie@lDonOXyfSkI~aI5_!mQ2ebiy@vfI)A^PnJE*y$2~jc2V_w z@vJQbTOGqZ^69+X&I4RHX}WzTJRhH9WoLhXMKNN~`B3t`nUl<%Bx_RKW3koI$;tGi z!!dr=S2?%m97Mz*Jo`el$bfn#7iT+sI(oKNDZZB=1W_&qeNo||=}R#T2RDfD4=_*{ z$dg&!cN&rr@>Ab8BlOCig4{_g^)Td4z!y{Q*!-23lAe(MH3c&3h7X7Z@nEVP3qAzm zLl8wml8PKU4`ADE0tL4Wlp$?bArs0+vSyhNn~3*@TX2rP)iM*G;q``H*yQhmdk<^a zTw#e2Xuk@XI~TEi2_mP%o0Sa;uAfh>NPA`oiVW00fS37i-c62v%I z!uDopT2}pNEZ}13YU<4_+jrs`N8#75soTO7r=XCpPEVhna>F%g_x$=5%`2vQ$=^E! zlyKH?uXrlA6MBaX@RIUtp&&s!4_tMJ!&tJMD{t$<%qt!5jx_{&dhlX%t#)KU{W)8 zd3uSy*|4g!8H$wWO51EW{okmP@f?fb@-%+{YqO!Gu2Q=qZxCM##h37n&NRqH=@AaT6fj@n-W5gU*TDU_$WEc z>ZoukQ7&LKqYJK+ZHMjxQD@b;C@W%1XsbI4ou|^Rd9rmFiZCAAd+nW#v6PyW^T5RpjOFwgMa$RPOEa_HLsu_%9c-q8RVf0DE^Mun-WK7vW0h1#i$mVq&87)EAi-u(Io5iR0ZyJeX!~uVHeYhR^n7*A?Uk zJQ-ww*yC@p#X^LJBd_$3(6$?9CyMwRK@>V!0YD(n{PEF3^0f5sMII7p%y}8!%8f)7 zX;zcN zwqWu>muTXle!exEGMPi@1&|ncl8)ET%t3#7Z#(OU+Sw4R37ukqhW=VWtAl=SQ-Qk$ z=Y@z6ysZ9J;Ny%x7$`piqb8u>p8o{P2`Xp%1QBnSJQB3)hmE>UIt3*xJ;x9Vg#-c5 zT7+i0{>%FgsR+`_#| zYVwLbt{X}p&B2(j4_IBA`qV%g*l*vhwxQiYwm!H$=G&_^zo@wNeq%WF!1Y2LXB`v1 zoMHkNB@Gup%q;(<#|Py!-mBkbv_3sq){bLi2sDO;>{11EQI4WgkWvw_>J7;XSnxwWLdLjJwQL@)r?a=9Z{8>~GfQHE?`GOVv3+KUV_Ov~CKv8#d5-u{Iu$zV~ z8yhWha58kw>i+omMORvjfaP@G*!S%P`t2cBN;~Oz2H4aA?!>S9KdJLB48vqU&by>Kbya2-%AP!|sK9F02eLDpScidkcwtf0k z_cYktu*^4_po8Apb+r-b*~FxL(P`H({RV|t*#>>%RwqsQnjQnQ=uH!y9?X|8VNGex zZ#wz8{%l#EFMza}z4NAED@vB;NB&(K&T0qj*pbhx4~pXnMvh}xv z%>k%MKZBsfl|EDYK8erjL=&lX)oWwTNHR|nSq>#n2g*^TXZ&!)t;D+Q%z|;S>A+%l zG&l7V|E|JoEWOY2i=%@0-PLeHHOPTfAPyB3Ng;xcFhE%pHJ*FY@$R4}uI3SM0oZ}S zirBCpQqV5#tw@Mg^cj`w7NX3jvvwa#O2llJgx5r^6hkj&7L;bEW<0_;f|Qxth2WZg z{<2n(-`l#M?9I%iR9r_cobMOl*+)up0!y2<(ot7$7;@z~!SIu+VeVC^TF9?Vn%<63JYl1{zO!g-yCPk@`nZ9h06S46CR zx+CYQD*~03m5V^ey<^~EvpX9Htk?BTB!UIj-=*3>`P@FphCRx2yelqWe*ElN$dAm) zK3*IL**Z@l@0Qqfc$2%Es2GZ zxtFlpbf!*6-#oMjqB--rzkMKEK;T=Q?{v^$)XC(X+2KMn zzCNyn4iX>5k3`=)JjCII_4{Grro{Zf~eU9`)zfb?|0q*Lh;CKp(KZ((MQ zz&8r4ZZBDiZes2WoH=AN^dW>++C*^?WLnH_hSwCW{OHVfews07*X17gA59@7sGE;d zI8O2TdOEyoB8eqOgRIein2U}~k_(z3<^8)g-T?M{FxB0Px_m@M45X^(^H)L-_R3ZS z=JA!dX((f%Mn|yKc~`9ydocjyuCxVdILb&(!6~}iqaz`S*^K@-Eii2l9lm=|@(*G` zq)yBPNHNI7vt5YU7}Xw~kbWPv*sT-PM{_=Gd4bs4x!Cbwt7>pRXBT}?E?H+8D8Wy6 zu#@l=fLaJsFduf0LA)wJw3Gfs?%Rz3I%S{hmd({LGI^PymwgHZGrDH~AP+iy2RnU- zMkQi1qjrPjDR%0xVu}|jH#Q$FR_h+>4Uf!ym5|VVkq_-zJ{Kar23OJ|xy={)@k+e;~o`SUkjA=3obM7fq8;0~nma;}b( zJLz`w`r=k5!RcX-Kf_|@q+ z6}#r!=Kq15o+YFkGNrf6cx>E!>w;t|-aA5}NUgQ)eB2YifgAWV9L;fiSEegQr!?ea zHlw*B_B4DF(Hk&@Xhik%>vr_RYCraSroiYjy!3&$F5uzGfpixlHblxJ=(-4i?3^?9fD9myj!~*t}{Iq0X z$EH9s%FE;k*esUB3|@04j6g$ozBHZ7lC9nOW*B>)P#T??p>8wUJ6Uwy@AmbmsjB9M z>=h%S@Y=okBA2~tuoz@6zf>He0lGr4;lcFtCU77<57sDWj+dhY)DRbXJF z85V?J4|m-Ai<5W3e4quaX(}b4_a7y8wD$D0hQTWfPXG5X;P(S#Udw8?d)ky4egqjC ztqK|biyj5yRCcp)03FBA%F0?`fTOnX!%ZmO7Aj0ed-(!Lud3Hs;Fjm@Jb`NW$rDFg zkL2$R&Hr6(r-SJ!>5oink|~HJ^L*`QwGL6{lUt+T`>G{XN3qof5Y@IkWMYX2`vlR zU-cTgtM+Y*E0j3ygk4{E+g=K?qFfCjI8da%-$xRO%Gcz;gtr7ELI8Fuh-u18Xr69k#4JJC>?5-}#$5WMU7 z$@1U(p5fwWB#{%zAb?VK?6l@?=RHb$Ln@ zLe21B0Skjw*Z4}0p!*mSnh+j48X;_c2fJloOBj))dOQ~&tMSC$!eF*qxv^kQFEO*1 zrzZFIx)na;K! zukP?9W@*6WrQQvFy@S5oju-faE8nNqL=4#5Vj^}rBFL$)oJ4;t?c$pwS$RwU&j0=H z@sbWvUgL&|_d$1m8sZ%97E-o^R{&Ob+BW1pUEOBjf{Dw8SEH=m<+!%) zjT)RJ&>&rB7#s+8d_%Gx7%KZLWoQNl{5}|LIJKM{vVNyUhkY!5C6|zW^TkRCFPk7Q zYc6{vzJKBv>*X!ubA1jYC;yH-t2{9YmDZSS3J*0a^-X`Jz)n}?nTDC4+?scIR7;qO ziS*O!UOCAIsJoo)^||_)7KfFQ`)kM14B@czqQN~VS{KT1I;6w5fPllmw6>aA@e06> zf~EIubX|p)!ffDD6D&1`kt=@4OOcoQKqi)1Tgs2kCv;Cuj^EX%&G%44Nt?W=gC3_O zjWFP8@Q=W$aN{UOxgt&<=eCSXWQWIT|1DPekwpw&BF;}Mw!!%D36({_3X0)eiKdiG zBP@OpR_HqH4EMX6V%y~pVs`%))8x+Ubx8JsbfF5Z-G+9)`s`PhapYU78NWNsJUd9@?-4{t?~DAA0AR z5&*0d#IUq)`xSf}qYYq-FIX!}`x(+%US3`ny!*$4hx($a8J4oSCU!;VIzfl2M%u?> zcDjZsmq>A)`^H8_C1bvEi8~uJXie8Kp}mW^zDnZG+O)ye(lbmz%Q*hcPe|Dr0!H_a zp+nt+m0Ct*WTZp5kGCpgZ0w!)izfAXxRys~X_m1l895Z*Xgxpj-VbwG39O|JZ!ktZ zP1qn=mihzNe*{sQs00jRJbn%4v+&PYrIRt41Tb(&Pxx-y{-#B$KP*Nyar<7AXL9fT z@|@mjVt(AA&<#zdtoQ5`MXk@BkQ=M>b*cr)!U;zDoHrFkTad;!g;Q6j@0RMk*_iY; zM~Ii$6yI}|=cR(pWkoJb58vONS21{NQ6AUA@(%(C?GtNe4H}x z#gNbpi2C;J+fSQM=a+pORer5wyz>~s6tFwurY)*u%rjBKR1g9&@0~sIa`1;;HUf;G zcjYi0UJP}%&@1XQqRaZso__P*e%L@sKh$XnK;x+B9Xu^}o+!Ql^H4Z_rmz#N1+}x=)>Bpw@#zTFj==cx_~7rjNB_ttze!vzwz<8WAg~XrYQqWg zd^z=j^DN5LHsZz%l9$OQ#DVSobraKW%L$PHKVz2vw$dKd5X<=Z^AJ;uio%wmyt_*p z(C5l~W^4>?TgDjO+4S(=;`Q5psPyie{C=?l+B6tY9jjfm6gS=%br|@AmK|b}#0AD185OQlW9HbxR1bEo2Mk zKHN3h`@bLHJH$NSM&h=q5ceJCb+)YNY#4FG^IIc5RrK@|L*>oy#gBwP}^8?+K{k6V2 zu=ll8kiw#|pl$hgk|T5>>mXZlF^*D909v55mWE&b{LITT{rcH3Z^gQ|+%!l=w+W~~ zsYmF(T@J3t$p+Ez2tEWb0$woDWzB}A8aC-0edW>(WZpPw2G#fyFa1H6gZVPYvi#pG zVQ-=saa|6-FVP`Z6l)Kro~eL0L}*9}?B5qbAQC?>gUkJU#|JDe{8)fr`cX_@xE^P7 zS1>bd$&IyJ#JGfgZOY;N;~8r~!Y{hic(p)*K9bO2-!;5-AGgN5d00OXFIofMW-+vh zNmTg#pldiuBk=0|d2TgyC5Dr+&${&VxV*Hf{f+jGGWbP;Kl9QNUDV~gQWPWXcnDwN z$#CMt599&+dtueU0#4?))52~Y9}3+?^nkZ60|upU@XA}B_jv!lN_&rRl7f#Ky`e?g zf9NA(0)G1F!(+PhjoM=r{IeWAaEIV{Ns4{hXYjnhWG^|JaY<^=7&)Px3Vzvahx-Qd zWMU^ZEx|f9^=r(w@?Z|Vo&jF+`jic`g42$Q8UOEnV8wIYs+H*4t?_#tetKH^iaVxaL-onLJXUZrpn9fmUP-I;olfb>+cooz z;EXa%K62}A5}M)IT|Md6v7u8thI1aCFZk{9eVzNMGh%FQT5Q=Q;k)hUZJsLv+9%i5 z&fCHZI;?KWu8uues!&Xp?1#|TDq%IaIkpk@yPWz#KPAh zrRZm$y@NGf&>c!m?DGL)mS4*4>N|FuDy&)Ck4q*y0O}uf2+~=@P z)be}E+k6yKsY_$gbxPVsgcj;R&o&jGH$L0an2`;GJ0r3p`=NtWop(9``Wn`aU8^XNrE2E2A zA>TPj3~rE~2~2vDj#t|xiK;f(sL&e+I~{^~JC>9aRrCB?srN@lNzKP?0wI?W%LxQR z4M*Z)>j#llV`(rwQvRaiYFY6XJsh(7Gc&LfD(oF@k8L&oLZ2GBmP6F-*(+*~R|dUAZtWd=N3I73dkfuZNwaWmhpSA_ zx_bM(9}TG5P`$U|Uw(*iCsf~_H^%aiuG6HzPtol(<>|9k!sVI~vwb@jmxR|{uk0nq zEXxlE#qrm5mF+#66aLas$Rko()5L+LUB;j0t*Uy_6+g!Md9ilow;fA9Z7cFMJL^4_ zHTymf*1l2^{mbhk!gks-9sVh9EyOo*Mu~DIeGlJQ^T%Cs`qDMpem#v8cc}`zZ;f^+ z01Jk)`T@>p9~qV^P!J>i7;}a3nD!XAQN>4RQ#$9pYtAQE&Rw40{y?LRD8DmJEFJi2 zvKHJqvgi9VDcmRp25(9lh;>L9;S}c>TGC2=va{l#9=^G+XHog*r|Tn^uv??yMIH7; zmD)}0U7ebsw{*)PjJE?NP-*m(6>|GzvHl|Q&D!^-o7#QfD(EFemFur#0fMoDuYdJ8 zqoUhwe0RcJ`p3+Z?GSUz{JdYpcjC8dET7;i2S;aHZdhYv&xG@OeDzJv$PGV-Ww1c{ zhxW*cH%5+Lek@XU<*T5K+9M()`R>pC`=6-%zd1t<3TX22M#6>W0X#MW&hzaQp1#Tv z?|DJ=CBA?$hBkJ(>c zKGNNcn_*#EGjPwiGS!LZrhy)fwnf6Pvh{+>k$eH1=_Fj~Q`PIZ!_JBsoWs(CX$8JP zollcO@(|LDjks;Emo8KUPsiQ#tJynl;t#rfZm+5LH>+Ck?Thvpsjq)^1759d|HHf~ zo_)an!f%;yeT3snr$ItKIuPruD^{XX8Nf5#70&PI{Pv&Z%PH*hgWtO5a9>HZJSBF( z;{4#L>Y56cQzKHml9hMFC9P63XkOpWXWN+vip~@ihobQtm2wmOMAMkxR}xiT04AR{ z%v>e%CTjWYo%`LFh=nzMM!B{Pdlnb0SAQ#giQ``rdw%`vsJYSucg-&XbM9+pv#3&U zYNiEJB9~X)`+LGXjpcsblM=tx>A@xUmEX0M?R{81ycc)UV@kXV!evU&*71GkBPx%W zp%cM|Oq12F+4DmzW^h(^5ggsEj?$b_5-Zn3dH0p`NDAyFsZh4z(CW%dR4Bqt3ab(TD8sDHLsw< zH8x59nR4NHi-rAnp7+Q)75~h);*CNQL1p|pKrdYt*T|cHF*=W1ra`05gjgVD)eZ7&&!5-8L)#PsJZt+S}@G>#0K0^GarE2h# zs?6GA=etY?MG`-?ZWSP?L}D61W?)fW2E3`mN-kKesZg z##Um^_B_9GIX&RZq(bi>OikRHra3X>2gj0P@7z;Xl5gyVESrtu9qq!f&1`bG+xNNf zEAUODPFwPDOn*LhS9x2&CNB3vGbPkTqZ01%i4E!v`XgBu9OeOrMi1fI}JrN_XwIUY^$CezJ*USDJU zRBlY>iJCxW#hHaY+PIT+*}S?Z=yeM$^RlDOC$KjC3qkg>Tm;NR>E8^dosrzWpxA?uW%_o*Ds>KJTJky8I;R66oc(A)0Pz|@v@ zw_9S`q6cY}yd_i<&5`iWdI-zW)6>Y(C$05rjr1!;zG2hrKg#+fZ(+249lp)}39`(x zT}xS!deY$%#lU;**jlPdvxGEGY*U@fB#SwLvnolbQgT=UMSKvW^k{or#pJRx6l~Jc zBYvqQm^~nTv{*|#S~+%gRa$T3bWLs+bm_#arFluCmjNTQVzBG060RrpAFOYquV|}< z=qP*B!q%_GFNO2(E}3F-Uf+yxcV_)Z7CXP?Qxuw}4{U)+%*tMQt}j}xk{)9@)?c1$ z)USY+968??tf*a#FRJ-EZ}X4RjOW8|V8*IoiHW*X-1p_DqGV|QnyPg^meRQvPP!v@ z;-}cM)7o9O6o!Wk-ILZ67e}~hcP;H|2kKJZYINSYqBh7Q*B2?T{gQz3w@^Lq?2?@) z8+U~Z1+s2hhTpFCyI;Ot(VxoSDV=F~>0KMOYA0y>wZ>Ye&Fij}RFl$y=kA50*?uJ? zKa|9!N%#TJq{3!lqWhG`_ML%_A}8NvyT|;y-wr0?O3@*u8-9}-eutI~KH=V63HNCK zOiJ{EJtoeGgwi>>Tf-NfhW^nwHanxyozCev&C!A?QId2q)_EklgDTxQtYx#rO%CF2 zy;dCavP+{S3!h2y(`Fy)htTiJ&%Qz-k|f z!SNlWY&8z=7rI_N2A@|Uj^8Av;VmLne(QX({ntPb*g6xbl}M%(_&$mY`-(^z=z#P) z4SuE}Wm*Hz;xzNHuQRlAR@~bqX?uHNi`|G*b?yiXT+7N`PZu=o?k_g2(rZx@>I(>a zR;8@IVXi+bJ)4hte;;fW2x@g)VQa_lyVqn1Od2dj`LvaCnV`&)pL%G@H!<)IHg#ub9B@U&>G$-oWf8(nfQ9_4*f8X&+N_}v)-Z-W= zZ5uP9@wVov1iR~0pxyh8$04pXa?&P6|42l`vounnHf>lEaqtJg|LWz; zgV}n+J&qPd(G3+_b+5e?wM3MbYAxMV?OW9vYKf(m)KXe&TST>p+H0@1sGv$}jeQGZ zCxRf7h(vPJ-<`R0=g!<^=H9vgy=Uf}_q^x4&wJ*)pXd906Y_D>-y~JaEz1nJ$pshW z3Wgh3zL+Nk!an$E@QzL-R%63X)wjPq5MP)Gd+i@8By*u~J58@1^E_b@?<_O%x=f2*U24vxcsQiNx*!|O_1@a zwfnp)ln2m=Y05(jW;w}z3`ubW{#3+Lrq}d%o0$88*9d^kM(w%B_jZ1Oxj&*4%$fGA z0*Gx`vXtULU(P~*0N`5A7iw8XLnbD2nER4N3hISb7zJma)35cc$0~D9;;9R|1H$~S zfdhOaq0Z{P@Y%}2mTHux^1$kgkJ*{FznqGql>&e@Nd& zZ`&x$YJ=~iJqzBoD2=uT-yMc>X7`vT8d}zM&fm<1dR%=Q!*(%keyKpZB*e4K1f|JR z^X@A>1r6V6ces8!E+ji126xvG^9O$6$l`^t^*_fx+h3xaim*r}5#%-Wf;C69I<>;~ z4f9yf=OJ6osM~6;YkuFnCBbLT^2AB#Yo>7wzbZUq3N?DD{Qw3qsf?F%JMMl+TG=N^qw7yq`KQ845< zw}^AOp9wHdX%@e%OB^0CRf*~yD`Gg@+JC8(6vul3+c=^w^Fpqy?|I{-pg-4P%Xe@#_G~=nFx7$ z-_rHbl{Xe$x+B@E%O8c{QUhul&r{R!mTEiJ2X9(5M^G=gUh>it<^gTKFD{`v5TdwQ z9^33`w(F`pP!3~2eaJK30;byD=~EmPw(FRc@NMXpU);QzX?yp|tTf(k zGL4p*AS^C~X@Wk(sdWk;w7*Lzw-nMib1rmx{-r}C-2i9(@XO(jLL{IkwmuC-sZwgd zM>X^Oljtd&vlUKMdb79Wz0;6l60MQOHz|J@CX*3Zka4ry?M4G?UcIL@#88y$y=9(C z1V|?QD-+ZVsOcr2L&q-nro`a=-i9turuieq58WiyfGc5&86g}Q-Bg(IyxBUlEVI|B zC0P5~YY7`Qf z`dm5}BY&1$T4h~qLAA1aD0&s8{cY55ZbfWP~jje^Y42pgZ4UHL6)GIc5Vxovk1J(P5?{w*{k zwGav4f;w7JyY|~EbB*F!s!znT|KsUJtmY7GC;H+L9RNm;RFIM#*jDYlXkY5l6EdnNOT-wQS19M96C@-gme8 zoc`H$W|mtmEIOyW7gtTSV!j0}%SHO-UADjbQQuD=Bm_ef43z^JT&9uq$Nu#F`B>ZK z9+a$Y7V5K~gZ5;9#K(kXuFxlYtJ zt505giPW}P8A#FhghijlUCthu^{akw+jLKVN*rx-=jE=bsA;`(qgQ-Bs`|;e3seJs zfvNo6_(G#yAIa^z;geSwC(U?$GegF)fERV$)_d4vH{b>OS@JH?txG*Wd6?IXGHO^w zYbV{#<{we_a1On>bNu3R_{-9TfMH2=9%LCR&GF$%%xc`K3P=fLs&4C!c?K&J+HQ1EwMD22XI#-Utv;+;C19lPVvE#eN}=96LPy zl{Br9it;x3={43V%PgC$tpo}hd!)*z7mo)|tt}1_O3a$QcAFBgX(b=3s6v>fIIjWa zUoL=Oi@p=jHhDqTdNh_9^hVIwaOc|uY)tpv1ga=kepc#nUO`B|tHd~@VD_Sq6VyHD z14m5FQ|}kg2irJJe#l)BP=(0qVv#0!JM>zsy?7>0QB_2o~k0**lKIdL77>qX`?J! zT)$>qzwd2||DtkkqnyAY)^fLZ1eP`TmAA};d`5({S`Y<5zt4oZN+KoUTH>1QSq9sX&CSyfCod8TGDQYoSRBxCg zru)#Y@#(f~30XUJxxb)AI-$SUjIt4TLGbct-cjCYdV;AA^jmp8{uQN$(K#-WSLE6M#)CLQTx8%>08oaw}LK$UH&k$XECTFUv~uY69Wp#9Q!ZUuV?Of zby7EMJVF7-4^usp5_tR{XOCaJb)Ex$mRkz5Sa7f9J``iX5EXl|9{g+@!F|Ga390W5ad0yg=hwy@-N3HfCNPdJt@U(B$I? zSywc5Y;a_*+2@TCR_5O&e!nsv2@FdXz}#BthG~3|3-FrMdfKqzM(t$!V~x3eZP5$$ zhmArSH)>DVs-~W%cpBjD3Jousn3VX01d-&`GS#lnMc)YnnSZC}J5IuU_=sm=?dM~z z8_G$_qydB%T%zYWlG0n6yNKy!b)mz0;j;Iq^-g~^YFA}-*+2X-O+V|me#7(kENx-+ zcF@&0AWZ;Rl?o496rLrk$_)jc+k?w!F%m}g^nRm<21S#e9Nm+dr`Z*Soz-`p9L6p| zO<*%WWoP(b2u+qd67_uYdc-Rxkh?}6_%6U#XmNl0t^n_uWJuQPZHvfVqH-*8>*c0d}B0nS@^o_iwvbaGB8 zYM)t0zq@olb%As7WwiXe<({m2X~0i5D$(o7w|RemzL|TpwcEhitb!(b)^fv}S>P^B zH59#z!54OKDJ4%kmv1Q9T}_lL5~v)O99xEW1X~3C^h=7Szt>`P==f3mk73`_uy=k@ za*{{$(Sas+e#|}-hYj0>=tSytDqS~}_YO`nrOcYD?iE(VgvUaxJS1dfEBsU<%<(cz z!8`5VEk7@S4+BctBK~;yocGA~0|;H0gQRE2^tw5A;^evW+QN3{TSZvUM+h(vo@qol z>c8AL7BWIo?FrNb3eAMf2NheLsk;&IZFO@Ci)cvy#!fK5{0SBgwFs3tC&j&z-bz!X ztB9X~C8JMzOPB3Ge5io(K;uT%5uL3Odj`_~OfuQAg42 zX~|CQn|(i5>$mvK`1XWW5A;`* z{L9|G>=1Qx4^Et>6JWuaM!$lB606%lFv58qqWw;ZWd2&wlbcCdmvGVa3SAxOi%ia) zqSn&<)%KqgZ63UuqNx4$;S?QrK#^SSFL-I#BLy+Yw96A+#t$t)ZSJ}f_>-d0o}YL~ z6gEe2!57G`g;cQ_8>^1*j*aW3Mr+~xVRcRRz*X;R|5S}=UR8d4EP8bcVOEF#f!&GR zw=UNfZMe-UZn`aTDk2St7+w`~x~KffOnO9qNC{&(g)A7b)r=efErn$A{p34{rsvF` zMfAT}57a{VetCT7w%;)3=^A5N;6OTTj1HGle)Oi*NL~oF4T4(G=CPF7(F9GlpmL6o zu^rLF(m#e|p;Rs;P)ca%TqJVxmtQU2<+>NZo_w_yp=xv~ZLl`$No&}8qwrhDs;kkp zzpq|o6)$XmrI0j+fru(cHTQf1A&wKju|tMVtX~lm;P~W7L$Kb17P_zw9z{Fo#j6?;V0sf`|Zi~ z8#|w;`})ky@^3{g`#J7@NBa2Uu%&&*Gn5M{Exo0MCgHxOo8|EDe~vOD+t}&gq!^!j zSzYQqEOWO+H|^313!d)uydSnfnrQoNlmVe#{%C#Y9JJDYa^_~Mbqx%)u)3^Of&0W) zsYFA5IwlcIjT9RLUBRw8#>3y2uO?d?0^iN?TM`Z77q%Y1Pf{(qan+W5{qY=;3#Hq* z#+$Aiw*Bf6D*T4@t;>Jv`F;rjuXMu)vv_=2#0kJoBGEx0v0TcjJ#Xr~sBFt9cMM#E z`@ywr>#;Fqc}@R1p>(-@a53=C*iN-Sexxbq3P*(@8Tc*7Zpho#_XIiO+o`$s*ZNFP z$#DHSpZ0MgFCsLOp?I3cFHy?UBDS9)x_k$EGmPfmyYbKmr#4V=|(YI_RYBwT-` zyKjBZx7jknnlmzk|Jb3KH^@(%kp(-#Q$8EeymA1#9ca>HS(0-_3SRJCEP27~h`wdT zr@gbS%Yw*4<{zYbOC`MZhPRmGvAvyhsD9)h`nLzeZzSe|()rK!g}zN*@Z@i>+LkZO zm?;<;vhCN07r^gF4nLIuGu6^6er6(PAMI;Rc1Ae%zOCT32Puon1PnAST3-q*ba!8j z&3ZO_p0rx|h-+X(WbQfVxI*yrCr~>x*2bjvUc8Bf-4|QpvBPgpmudy`oR{cVJ)K!t z>?|=IIR4i0GyBDzO%t=n0-RHAn~i1;9a$ROf~wZ8dmYS$mdYvVV9Ym_3Hl+4Bmr7U z9c910hQvT)p%oRY;IGh`MKdrssXOBU_@~f_LoR-jYp9Ot_*1II(|YR(AM=;mJ$l;u z5Bn;DU8np(u@IY?$QG`EeH)h*jzD>Tu8X2S`9VK9eA#%oHR`(cITmznnO)t+G1sy- z6-b9MN(YJZMp~}R{z-NK&CsY(gSp~p#wWaUU8tDhfw5(pj;9r8p|@CupJ)ssat^!< zNQ%vh(;7E79It;H+OSo8?k#sd_B4omj9#f^&EDmBjJ}w7&Z^nf)o0chm@FQyDVCI* z#STIHbhtz%lHkMrqoP6j#;FtKqWSK2~(FU9ssAfm)!OdMQk32ZM-MsVFD=BeO z5YLZWl{Onp?q{I8L(Sl*Urryq=5B4rV5W*LWhBNPCYFC`YFm@peu`FiWiUTH|kSH4lDM9Hq zQw6T~+3#$v%sJFfBia}R@4MbGM)8m5DgJhEwENMp!}PkN2Z$ph@s_Z|F{FJNihDaG zPXQ`Avxuzq95KRF6ApZT?Jzp{*qpc>h-0%(QPkkCDi-%zM%t}Wl}Z(5g(hpWeL!tr z)lIAWFSkS!NPd5#5GNoBMM9{>^A>lWX!bsqF$#O`~Pk8|;#&1jy+nPe6zFfR**-2W@;Whn7 z#_YJOV+4c~kjSPf@Ybkyt9rC5ZPe!2_wwW`udG`p$9qqd5Hrf_l`+J((=|xOeKVDBVbu1lSC}U!zya4u=k_p zhWD=pFE@KkBpRR~vX1a|>TW1!ZWDPX>lG!$D}@2`-9u^h4wdk@{TN-Q-Jq)X~~FZm?n7T$SUR!f@S0D3meumo?Ln=K>I^ z!x12w%_A3!H!!PkvZAK~LhIuuZR}K-tq_m{$%6q{P)h{Zl3^mR$#)olEiA`lR)p+9 zeJlJ%sq^Hc@Eu2pjJ%ZUTkDx!8%o}BEnCm4bFNWEglKJZWvMNHX}Ke2zs4!Q3h~sSvW!=g5BaiYQViPl7T`hwg~QUZG2lyJDfnu~A#y}~PjnO82uygo zvD}I~H-k_3QgAyS^(!Vi7?FD(GxzxEc|&*j*95?sM{#sFOT>8hsD=RWq3}pe^KwwX zSfn3AX|k>K%tm@Sy)p}e$Uun~4PgA5on0?dVcgMAPQ=IcalWPuJZh*kQjH`k)961I zLpdL2lY#`jHZJT|c=54nHf@8z6c33p8%x+pYYh9hQ3@)0YXm(pdnal}3#0!{hMK_> z42)o8I&C^YIW__+6KyIULr>;uI&}-5TU`cOP;!vt6AR|Ka5@6c@9&F z8J+ta1c!Y`Uey`o(3v1y)c!)90DbZGxQQb5y||@IF_#y?Y;gJw{%W6v-R0LJ*p-56 zq&|TEytO``kY?t#1Z&mCWvC6s^?3H6SkR*q(YsK~u|{}Oa4A&tWiUz_tswWA)7jcn zG0?npvEwt|niPazhsblt1q}K&Sc3GMCK%D=eE01@JEuQ=!8x4T!(kbnbQ`{#opEP{ z?1xqx*L`iqesm6#5bHaQHPFSg_4YM!D7FZYCR`-TG&?anc>1KeSC+ZFDU=HIuDL9pCzEV*l?r+kb4nCrHGw1hVy;f#6Cl%yTV%{y-r7&oD2V zOCK8ji}e0N)xX9Z|7r6d@b_2u_+NVJ|9^G!&wc*)qxpaB=AYsISFiYgHk$ugH~)H{ o|8CO$+tK_Vbu))x@F=i2R%o5dwp7w!h7gOv9pl^OI(8BN0^x^}j{pDw literal 0 HcmV?d00001 diff --git a/example/src/WebEid.AspNetCore.Example/wwwroot/img/eu-fund-flags.svg b/example/src/WebEid.AspNetCore.Example/wwwroot/img/eu-fund-flags.svg deleted file mode 100644 index 0e99b0d4..00000000 --- a/example/src/WebEid.AspNetCore.Example/wwwroot/img/eu-fund-flags.svg +++ /dev/null @@ -1,787 +0,0 @@ - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/example/src/WebEid.AspNetCore.Example/wwwroot/img/favicon-16x16.png b/example/src/WebEid.AspNetCore.Example/wwwroot/img/favicon-16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..dafae929ed553e65ef392d2f48bdb6bf314881e1 GIT binary patch literal 651 zcmV;60(AX}P)t+|`k?4y7*SDlVM&aNja*?t7ZHqbf?>hh#4L!sv8zbD=_12Y^Fm9a zqPxH%ioPxumTcnFq7vzS67MGK;qGwuMB+XQxsN|*#^f5s3G?14$9zkS9o|5Z4>m$Gc| zT}FA$O+*GpC|5hpS+~4_+|6#K(?IeckVX5$P19Rh6DJd5MWf<*g#Dk`@$j@7W$*F0yK7OY?I-e}q!UU$_MDVK< zuBsM%8UBvU<1BKZc=I|0j_reccd?d-#(rs}bIAgE75tbmasO#Ak||z=om`ulnN^L{ z3ynMpiYyq_YzY_nG=iqi=Qvv1g5lV2ReASr(?~Zi3*ItW(q8us;>xWjc=aKwn$n#v zoUbU;obHV*=wM>4UsHsz-%C7jZCZ=q$%6>_JxZAIB0pyLwQ5AV>alvI!4b$0ro(Qt z+4_09D+}5h0%&Wf)Q{F!djjpu1*tXHpF-7fH;jVp{U32QV&||}9o>7fT>D62n9To~ zxGBLyIk`vV)fg5v={;oUFlS80`2~`&kRa8}_meI%Mr1&UG4CcGYAT5i^NcwF00960 lq~Ap000006NklLU40zv~4M3HP1V#J!w@e)nuQu)UembNuxHX076=BAjfTJvRNv}|sg z<*YTE;bzdwWZGa3vK%Oat?+FUNFw4@2ot&Y_Wd1pulIQY0WWvI&pFTey`A4Z&w0*s zuMMO8cs!nPQb6{Q-$)m65jW{3r^pv%C3#|$Y4N}Um@J8W&V+5`AX!abBvXjQYwTsh zboQ&r*QAXxUy4DD$o?06ccA*$ zKa6f_tl2coPfbK-S`uDPpN-;zY}D=FfT-}v%I<9j?zvER=Aw7#?Ng+H8Qzh%x5rFJ zS40A~?LUg-r5_;oolkM<+!e3+fqm?ha4gS#!Q|K4)@9TsN&%+zs%dD&%q3f}`LpkF z-W+61VmY2RRra>t^l$%Ny#ffbQKJ!^-0Ho9y90wd-B<_R&OTE}aIoE^lLC57+J{v1 z_Wy^&$4{HG%!rLL>ReL5WutbiV~+>mGmSx>dX0bnjXWMe^62^JvGJ*TX^v^TU6umQ z`a6#GT5eU`!cBa4K)qtO=;vQNs6Cp)VD|hHAd}N$IiYxaB|=Dag)hP z7~?}j94K4=8Z^zc-HuBE^8W8P1_f#y9ySSwb`&D*nK)xg+2xhIHQEhYbmyeiZgn{1 z{Q-nHg6Tf8$oEW$iA2fkIXHXhJuJ#du?qNE)3i2O0Wt1mbB`#=otMnOEjaycsp1U# z;^kS&R#{%8bY7c(FMj7Lw&H5_c3i0X2%l_NhPWpqRGguacl37AsRCF@3kQ3Mk|4Vc zDYKr!+^450QrS=9eH3GK)ZelRhoSLAri=Ul#DD82&3g9iB%lE>?tB+w$0FXbF3{S)g75Ca|R z*k=>-X+q^VMV5U5b5|SVO$CSp9p!YC|9^Wqmgkk-xI}jQH;hbYPTe=BPKjj!Vqt)j zyv^%*q=F2P(TWsZMdu9WSV^w)+ERus!0XN61|pkN-XulTOEL1a$Rr#pak0HXj*vAZ zkuiCsp4TJF{{R30|Nl9hg3=^g000000NkvXXu0mjf3M+E8 literal 0 HcmV?d00001 diff --git a/example/src/WebEid.AspNetCore.Example/wwwroot/img/favicon.ico b/example/src/WebEid.AspNetCore.Example/wwwroot/img/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..de4eae8f5d7ebdb66cc8faeedc82ce8906ad25d7 GIT binary patch literal 15406 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 U?@ix*Ot.has(e)&&t.get(e).get(i)||null,remove(e,i){if(!t.has(e))return;const n=t.get(e);n.delete(i),0===n.size&&t.delete(e)}},i="transitionend",n=t=>(t&&window.CSS&&window.CSS.escape&&(t=t.replace(/#([^\s"#']+)/g,(t,e)=>`#${CSS.escape(e)}`)),t),s=t=>null==t?`${t}`:Object.prototype.toString.call(t).match(/\s([a-z]+)/i)[1].toLowerCase(),o=t=>{t.dispatchEvent(new Event(i))},r=t=>!(!t||"object"!=typeof t)&&(void 0!==t.jquery&&(t=t[0]),void 0!==t.nodeType),a=t=>r(t)?t.jquery?t[0]:t:"string"==typeof t&&t.length>0?document.querySelector(n(t)):null,l=t=>{if(!r(t)||0===t.getClientRects().length)return!1;const e="visible"===getComputedStyle(t).getPropertyValue("visibility"),i=t.closest("details:not([open])");if(!i)return e;if(i!==t){const e=t.closest("summary");if(e&&e.parentNode!==i)return!1;if(null===e)return!1}return e},c=t=>!t||t.nodeType!==Node.ELEMENT_NODE||!!t.classList.contains("disabled")||(void 0!==t.disabled?t.disabled:t.hasAttribute("disabled")&&"false"!==t.getAttribute("disabled")),h=t=>{if(!document.documentElement.attachShadow)return null;if("function"==typeof t.getRootNode){const e=t.getRootNode();return e instanceof ShadowRoot?e:null}return t instanceof ShadowRoot?t:t.parentNode?h(t.parentNode):null},d=()=>{},u=t=>{t.offsetHeight},f=()=>window.jQuery&&!document.body.hasAttribute("data-bs-no-jquery")?window.jQuery:null,p=[],m=()=>"rtl"===document.documentElement.dir,g=t=>{var e;e=()=>{const e=f();if(e){const i=t.NAME,n=e.fn[i];e.fn[i]=t.jQueryInterface,e.fn[i].Constructor=t,e.fn[i].noConflict=()=>(e.fn[i]=n,t.jQueryInterface)}},"loading"===document.readyState?(p.length||document.addEventListener("DOMContentLoaded",()=>{for(const t of p)t()}),p.push(e)):e()},_=(t,e=[],i=t)=>"function"==typeof t?t.call(...e):i,b=(t,e,n=!0)=>{if(!n)return void _(t);const s=(t=>{if(!t)return 0;let{transitionDuration:e,transitionDelay:i}=window.getComputedStyle(t);const n=Number.parseFloat(e),s=Number.parseFloat(i);return n||s?(e=e.split(",")[0],i=i.split(",")[0],1e3*(Number.parseFloat(e)+Number.parseFloat(i))):0})(e)+5;let r=!1;const a=({target:n})=>{n===e&&(r=!0,e.removeEventListener(i,a),_(t))};e.addEventListener(i,a),setTimeout(()=>{r||o(e)},s)},v=(t,e,i,n)=>{const s=t.length;let o=t.indexOf(e);return-1===o?!i&&n?t[s-1]:t[0]:(o+=i?1:-1,n&&(o=(o+s)%s),t[Math.max(0,Math.min(o,s-1))])},y=/[^.]*(?=\..*)\.|.*/,w=/\..*/,A=/::\d+$/,E={};let T=1;const C={mouseenter:"mouseover",mouseleave:"mouseout"},O=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function x(t,e){return e&&`${e}::${T++}`||t.uidEvent||T++}function k(t){const e=x(t);return t.uidEvent=e,E[e]=E[e]||{},E[e]}function L(t,e,i=null){return Object.values(t).find(t=>t.callable===e&&t.delegationSelector===i)}function S(t,e,i){const n="string"==typeof e,s=n?i:e||i;let o=N(t);return O.has(o)||(o=t),[n,s,o]}function D(t,e,i,n,s){if("string"!=typeof e||!t)return;let[o,r,a]=S(e,i,n);if(e in C){const t=t=>function(e){if(!e.relatedTarget||e.relatedTarget!==e.delegateTarget&&!e.delegateTarget.contains(e.relatedTarget))return t.call(this,e)};r=t(r)}const l=k(t),c=l[a]||(l[a]={}),h=L(c,r,o?i:null);if(h)return void(h.oneOff=h.oneOff&&s);const d=x(r,e.replace(y,"")),u=o?function(t,e,i){return function n(s){const o=t.querySelectorAll(e);for(let{target:r}=s;r&&r!==this;r=r.parentNode)for(const a of o)if(a===r)return j(s,{delegateTarget:r}),n.oneOff&&P.off(t,s.type,e,i),i.apply(r,[s])}}(t,i,r):function(t,e){return function i(n){return j(n,{delegateTarget:t}),i.oneOff&&P.off(t,n.type,e),e.apply(t,[n])}}(t,r);u.delegationSelector=o?i:null,u.callable=r,u.oneOff=s,u.uidEvent=d,c[d]=u,t.addEventListener(a,u,o)}function $(t,e,i,n,s){const o=L(e[i],n,s);o&&(t.removeEventListener(i,o,Boolean(s)),delete e[i][o.uidEvent])}function I(t,e,i,n){const s=e[i]||{};for(const[o,r]of Object.entries(s))o.includes(n)&&$(t,e,i,r.callable,r.delegationSelector)}function N(t){return t=t.replace(w,""),C[t]||t}const P={on(t,e,i,n){D(t,e,i,n,!1)},one(t,e,i,n){D(t,e,i,n,!0)},off(t,e,i,n){if("string"!=typeof e||!t)return;const[s,o,r]=S(e,i,n),a=r!==e,l=k(t),c=l[r]||{},h=e.startsWith(".");if(void 0===o){if(h)for(const i of Object.keys(l))I(t,l,i,e.slice(1));for(const[i,n]of Object.entries(c)){const s=i.replace(A,"");a&&!e.includes(s)||$(t,l,r,n.callable,n.delegationSelector)}}else{if(!Object.keys(c).length)return;$(t,l,r,o,s?i:null)}},trigger(t,e,i){if("string"!=typeof e||!t)return null;const n=f();let s=null,o=!0,r=!0,a=!1;e!==N(e)&&n&&(s=n.Event(e,i),n(t).trigger(s),o=!s.isPropagationStopped(),r=!s.isImmediatePropagationStopped(),a=s.isDefaultPrevented());const l=j(new Event(e,{bubbles:o,cancelable:!0}),i);return a&&l.preventDefault(),r&&t.dispatchEvent(l),l.defaultPrevented&&s&&s.preventDefault(),l}};function j(t,e={}){for(const[i,n]of Object.entries(e))try{t[i]=n}catch(e){Object.defineProperty(t,i,{configurable:!0,get:()=>n})}return t}function M(t){if("true"===t)return!0;if("false"===t)return!1;if(t===Number(t).toString())return Number(t);if(""===t||"null"===t)return null;if("string"!=typeof t)return t;try{return JSON.parse(decodeURIComponent(t))}catch(e){return t}}function F(t){return t.replace(/[A-Z]/g,t=>`-${t.toLowerCase()}`)}const H={setDataAttribute(t,e,i){t.setAttribute(`data-bs-${F(e)}`,i)},removeDataAttribute(t,e){t.removeAttribute(`data-bs-${F(e)}`)},getDataAttributes(t){if(!t)return{};const e={},i=Object.keys(t.dataset).filter(t=>t.startsWith("bs")&&!t.startsWith("bsConfig"));for(const n of i){let i=n.replace(/^bs/,"");i=i.charAt(0).toLowerCase()+i.slice(1),e[i]=M(t.dataset[n])}return e},getDataAttribute:(t,e)=>M(t.getAttribute(`data-bs-${F(e)}`))};class W{static get Default(){return{}}static get DefaultType(){return{}}static get NAME(){throw new Error('You have to implement the static method "NAME", for each component!')}_getConfig(t){return t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t}_mergeConfigObj(t,e){const i=r(e)?H.getDataAttribute(e,"config"):{};return{...this.constructor.Default,..."object"==typeof i?i:{},...r(e)?H.getDataAttributes(e):{},..."object"==typeof t?t:{}}}_typeCheckConfig(t,e=this.constructor.DefaultType){for(const[i,n]of Object.entries(e)){const e=t[i],o=r(e)?"element":s(e);if(!new RegExp(n).test(o))throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option "${i}" provided type "${o}" but expected type "${n}".`)}}}class B extends W{constructor(t,i){super(),(t=a(t))&&(this._element=t,this._config=this._getConfig(i),e.set(this._element,this.constructor.DATA_KEY,this))}dispose(){e.remove(this._element,this.constructor.DATA_KEY),P.off(this._element,this.constructor.EVENT_KEY);for(const t of Object.getOwnPropertyNames(this))this[t]=null}_queueCallback(t,e,i=!0){b(t,e,i)}_getConfig(t){return t=this._mergeConfigObj(t,this._element),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}static getInstance(t){return e.get(a(t),this.DATA_KEY)}static getOrCreateInstance(t,e={}){return this.getInstance(t)||new this(t,"object"==typeof e?e:null)}static get VERSION(){return"5.3.8"}static get DATA_KEY(){return`bs.${this.NAME}`}static get EVENT_KEY(){return`.${this.DATA_KEY}`}static eventName(t){return`${t}${this.EVENT_KEY}`}}const z=t=>{let e=t.getAttribute("data-bs-target");if(!e||"#"===e){let i=t.getAttribute("href");if(!i||!i.includes("#")&&!i.startsWith("."))return null;i.includes("#")&&!i.startsWith("#")&&(i=`#${i.split("#")[1]}`),e=i&&"#"!==i?i.trim():null}return e?e.split(",").map(t=>n(t)).join(","):null},R={find:(t,e=document.documentElement)=>[].concat(...Element.prototype.querySelectorAll.call(e,t)),findOne:(t,e=document.documentElement)=>Element.prototype.querySelector.call(e,t),children:(t,e)=>[].concat(...t.children).filter(t=>t.matches(e)),parents(t,e){const i=[];let n=t.parentNode.closest(e);for(;n;)i.push(n),n=n.parentNode.closest(e);return i},prev(t,e){let i=t.previousElementSibling;for(;i;){if(i.matches(e))return[i];i=i.previousElementSibling}return[]},next(t,e){let i=t.nextElementSibling;for(;i;){if(i.matches(e))return[i];i=i.nextElementSibling}return[]},focusableChildren(t){const e=["a","button","input","textarea","select","details","[tabindex]",'[contenteditable="true"]'].map(t=>`${t}:not([tabindex^="-"])`).join(",");return this.find(e,t).filter(t=>!c(t)&&l(t))},getSelectorFromElement(t){const e=z(t);return e&&R.findOne(e)?e:null},getElementFromSelector(t){const e=z(t);return e?R.findOne(e):null},getMultipleElementsFromSelector(t){const e=z(t);return e?R.find(e):[]}},q=(t,e="hide")=>{const i=`click.dismiss${t.EVENT_KEY}`,n=t.NAME;P.on(document,i,`[data-bs-dismiss="${n}"]`,function(i){if(["A","AREA"].includes(this.tagName)&&i.preventDefault(),c(this))return;const s=R.getElementFromSelector(this)||this.closest(`.${n}`);t.getOrCreateInstance(s)[e]()})},V=".bs.alert",K=`close${V}`,Q=`closed${V}`;class X extends B{static get NAME(){return"alert"}close(){if(P.trigger(this._element,K).defaultPrevented)return;this._element.classList.remove("show");const t=this._element.classList.contains("fade");this._queueCallback(()=>this._destroyElement(),this._element,t)}_destroyElement(){this._element.remove(),P.trigger(this._element,Q),this.dispose()}static jQueryInterface(t){return this.each(function(){const e=X.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}})}}q(X,"close"),g(X);const Y='[data-bs-toggle="button"]';class U extends B{static get NAME(){return"button"}toggle(){this._element.setAttribute("aria-pressed",this._element.classList.toggle("active"))}static jQueryInterface(t){return this.each(function(){const e=U.getOrCreateInstance(this);"toggle"===t&&e[t]()})}}P.on(document,"click.bs.button.data-api",Y,t=>{t.preventDefault();const e=t.target.closest(Y);U.getOrCreateInstance(e).toggle()}),g(U);const G=".bs.swipe",J=`touchstart${G}`,Z=`touchmove${G}`,tt=`touchend${G}`,et=`pointerdown${G}`,it=`pointerup${G}`,nt={endCallback:null,leftCallback:null,rightCallback:null},st={endCallback:"(function|null)",leftCallback:"(function|null)",rightCallback:"(function|null)"};class ot extends W{constructor(t,e){super(),this._element=t,t&&ot.isSupported()&&(this._config=this._getConfig(e),this._deltaX=0,this._supportPointerEvents=Boolean(window.PointerEvent),this._initEvents())}static get Default(){return nt}static get DefaultType(){return st}static get NAME(){return"swipe"}dispose(){P.off(this._element,G)}_start(t){this._supportPointerEvents?this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX):this._deltaX=t.touches[0].clientX}_end(t){this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX-this._deltaX),this._handleSwipe(),_(this._config.endCallback)}_move(t){this._deltaX=t.touches&&t.touches.length>1?0:t.touches[0].clientX-this._deltaX}_handleSwipe(){const t=Math.abs(this._deltaX);if(t<=40)return;const e=t/this._deltaX;this._deltaX=0,e&&_(e>0?this._config.rightCallback:this._config.leftCallback)}_initEvents(){this._supportPointerEvents?(P.on(this._element,et,t=>this._start(t)),P.on(this._element,it,t=>this._end(t)),this._element.classList.add("pointer-event")):(P.on(this._element,J,t=>this._start(t)),P.on(this._element,Z,t=>this._move(t)),P.on(this._element,tt,t=>this._end(t)))}_eventIsPointerPenTouch(t){return this._supportPointerEvents&&("pen"===t.pointerType||"touch"===t.pointerType)}static isSupported(){return"ontouchstart"in document.documentElement||navigator.maxTouchPoints>0}}const rt=".bs.carousel",at=".data-api",lt="ArrowLeft",ct="ArrowRight",ht="next",dt="prev",ut="left",ft="right",pt=`slide${rt}`,mt=`slid${rt}`,gt=`keydown${rt}`,_t=`mouseenter${rt}`,bt=`mouseleave${rt}`,vt=`dragstart${rt}`,yt=`load${rt}${at}`,wt=`click${rt}${at}`,At="carousel",Et="active",Tt=".active",Ct=".carousel-item",Ot=Tt+Ct,xt={[lt]:ft,[ct]:ut},kt={interval:5e3,keyboard:!0,pause:"hover",ride:!1,touch:!0,wrap:!0},Lt={interval:"(number|boolean)",keyboard:"boolean",pause:"(string|boolean)",ride:"(boolean|string)",touch:"boolean",wrap:"boolean"};class St extends B{constructor(t,e){super(t,e),this._interval=null,this._activeElement=null,this._isSliding=!1,this.touchTimeout=null,this._swipeHelper=null,this._indicatorsElement=R.findOne(".carousel-indicators",this._element),this._addEventListeners(),this._config.ride===At&&this.cycle()}static get Default(){return kt}static get DefaultType(){return Lt}static get NAME(){return"carousel"}next(){this._slide(ht)}nextWhenVisible(){!document.hidden&&l(this._element)&&this.next()}prev(){this._slide(dt)}pause(){this._isSliding&&o(this._element),this._clearInterval()}cycle(){this._clearInterval(),this._updateInterval(),this._interval=setInterval(()=>this.nextWhenVisible(),this._config.interval)}_maybeEnableCycle(){this._config.ride&&(this._isSliding?P.one(this._element,mt,()=>this.cycle()):this.cycle())}to(t){const e=this._getItems();if(t>e.length-1||t<0)return;if(this._isSliding)return void P.one(this._element,mt,()=>this.to(t));const i=this._getItemIndex(this._getActive());if(i===t)return;const n=t>i?ht:dt;this._slide(n,e[t])}dispose(){this._swipeHelper&&this._swipeHelper.dispose(),super.dispose()}_configAfterMerge(t){return t.defaultInterval=t.interval,t}_addEventListeners(){this._config.keyboard&&P.on(this._element,gt,t=>this._keydown(t)),"hover"===this._config.pause&&(P.on(this._element,_t,()=>this.pause()),P.on(this._element,bt,()=>this._maybeEnableCycle())),this._config.touch&&ot.isSupported()&&this._addTouchEventListeners()}_addTouchEventListeners(){for(const t of R.find(".carousel-item img",this._element))P.on(t,vt,t=>t.preventDefault());const t={leftCallback:()=>this._slide(this._directionToOrder(ut)),rightCallback:()=>this._slide(this._directionToOrder(ft)),endCallback:()=>{"hover"===this._config.pause&&(this.pause(),this.touchTimeout&&clearTimeout(this.touchTimeout),this.touchTimeout=setTimeout(()=>this._maybeEnableCycle(),500+this._config.interval))}};this._swipeHelper=new ot(this._element,t)}_keydown(t){if(/input|textarea/i.test(t.target.tagName))return;const e=xt[t.key];e&&(t.preventDefault(),this._slide(this._directionToOrder(e)))}_getItemIndex(t){return this._getItems().indexOf(t)}_setActiveIndicatorElement(t){if(!this._indicatorsElement)return;const e=R.findOne(Tt,this._indicatorsElement);e.classList.remove(Et),e.removeAttribute("aria-current");const i=R.findOne(`[data-bs-slide-to="${t}"]`,this._indicatorsElement);i&&(i.classList.add(Et),i.setAttribute("aria-current","true"))}_updateInterval(){const t=this._activeElement||this._getActive();if(!t)return;const e=Number.parseInt(t.getAttribute("data-bs-interval"),10);this._config.interval=e||this._config.defaultInterval}_slide(t,e=null){if(this._isSliding)return;const i=this._getActive(),n=t===ht,s=e||v(this._getItems(),i,n,this._config.wrap);if(s===i)return;const o=this._getItemIndex(s),r=e=>P.trigger(this._element,e,{relatedTarget:s,direction:this._orderToDirection(t),from:this._getItemIndex(i),to:o});if(r(pt).defaultPrevented)return;if(!i||!s)return;const a=Boolean(this._interval);this.pause(),this._isSliding=!0,this._setActiveIndicatorElement(o),this._activeElement=s;const l=n?"carousel-item-start":"carousel-item-end",c=n?"carousel-item-next":"carousel-item-prev";s.classList.add(c),u(s),i.classList.add(l),s.classList.add(l),this._queueCallback(()=>{s.classList.remove(l,c),s.classList.add(Et),i.classList.remove(Et,c,l),this._isSliding=!1,r(mt)},i,this._isAnimated()),a&&this.cycle()}_isAnimated(){return this._element.classList.contains("slide")}_getActive(){return R.findOne(Ot,this._element)}_getItems(){return R.find(Ct,this._element)}_clearInterval(){this._interval&&(clearInterval(this._interval),this._interval=null)}_directionToOrder(t){return m()?t===ut?dt:ht:t===ut?ht:dt}_orderToDirection(t){return m()?t===dt?ut:ft:t===dt?ft:ut}static jQueryInterface(t){return this.each(function(){const e=St.getOrCreateInstance(this,t);if("number"!=typeof t){if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}else e.to(t)})}}P.on(document,wt,"[data-bs-slide], [data-bs-slide-to]",function(t){const e=R.getElementFromSelector(this);if(!e||!e.classList.contains(At))return;t.preventDefault();const i=St.getOrCreateInstance(e),n=this.getAttribute("data-bs-slide-to");return n?(i.to(n),void i._maybeEnableCycle()):"next"===H.getDataAttribute(this,"slide")?(i.next(),void i._maybeEnableCycle()):(i.prev(),void i._maybeEnableCycle())}),P.on(window,yt,()=>{const t=R.find('[data-bs-ride="carousel"]');for(const e of t)St.getOrCreateInstance(e)}),g(St);const Dt=".bs.collapse",$t=`show${Dt}`,It=`shown${Dt}`,Nt=`hide${Dt}`,Pt=`hidden${Dt}`,jt=`click${Dt}.data-api`,Mt="show",Ft="collapse",Ht="collapsing",Wt=`:scope .${Ft} .${Ft}`,Bt='[data-bs-toggle="collapse"]',zt={parent:null,toggle:!0},Rt={parent:"(null|element)",toggle:"boolean"};class qt extends B{constructor(t,e){super(t,e),this._isTransitioning=!1,this._triggerArray=[];const i=R.find(Bt);for(const t of i){const e=R.getSelectorFromElement(t),i=R.find(e).filter(t=>t===this._element);null!==e&&i.length&&this._triggerArray.push(t)}this._initializeChildren(),this._config.parent||this._addAriaAndCollapsedClass(this._triggerArray,this._isShown()),this._config.toggle&&this.toggle()}static get Default(){return zt}static get DefaultType(){return Rt}static get NAME(){return"collapse"}toggle(){this._isShown()?this.hide():this.show()}show(){if(this._isTransitioning||this._isShown())return;let t=[];if(this._config.parent&&(t=this._getFirstLevelChildren(".collapse.show, .collapse.collapsing").filter(t=>t!==this._element).map(t=>qt.getOrCreateInstance(t,{toggle:!1}))),t.length&&t[0]._isTransitioning)return;if(P.trigger(this._element,$t).defaultPrevented)return;for(const e of t)e.hide();const e=this._getDimension();this._element.classList.remove(Ft),this._element.classList.add(Ht),this._element.style[e]=0,this._addAriaAndCollapsedClass(this._triggerArray,!0),this._isTransitioning=!0;const i=`scroll${e[0].toUpperCase()+e.slice(1)}`;this._queueCallback(()=>{this._isTransitioning=!1,this._element.classList.remove(Ht),this._element.classList.add(Ft,Mt),this._element.style[e]="",P.trigger(this._element,It)},this._element,!0),this._element.style[e]=`${this._element[i]}px`}hide(){if(this._isTransitioning||!this._isShown())return;if(P.trigger(this._element,Nt).defaultPrevented)return;const t=this._getDimension();this._element.style[t]=`${this._element.getBoundingClientRect()[t]}px`,u(this._element),this._element.classList.add(Ht),this._element.classList.remove(Ft,Mt);for(const t of this._triggerArray){const e=R.getElementFromSelector(t);e&&!this._isShown(e)&&this._addAriaAndCollapsedClass([t],!1)}this._isTransitioning=!0,this._element.style[t]="",this._queueCallback(()=>{this._isTransitioning=!1,this._element.classList.remove(Ht),this._element.classList.add(Ft),P.trigger(this._element,Pt)},this._element,!0)}_isShown(t=this._element){return t.classList.contains(Mt)}_configAfterMerge(t){return t.toggle=Boolean(t.toggle),t.parent=a(t.parent),t}_getDimension(){return this._element.classList.contains("collapse-horizontal")?"width":"height"}_initializeChildren(){if(!this._config.parent)return;const t=this._getFirstLevelChildren(Bt);for(const e of t){const t=R.getElementFromSelector(e);t&&this._addAriaAndCollapsedClass([e],this._isShown(t))}}_getFirstLevelChildren(t){const e=R.find(Wt,this._config.parent);return R.find(t,this._config.parent).filter(t=>!e.includes(t))}_addAriaAndCollapsedClass(t,e){if(t.length)for(const i of t)i.classList.toggle("collapsed",!e),i.setAttribute("aria-expanded",e)}static jQueryInterface(t){const e={};return"string"==typeof t&&/show|hide/.test(t)&&(e.toggle=!1),this.each(function(){const i=qt.getOrCreateInstance(this,e);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t]()}})}}P.on(document,jt,Bt,function(t){("A"===t.target.tagName||t.delegateTarget&&"A"===t.delegateTarget.tagName)&&t.preventDefault();for(const t of R.getMultipleElementsFromSelector(this))qt.getOrCreateInstance(t,{toggle:!1}).toggle()}),g(qt);var Vt="top",Kt="bottom",Qt="right",Xt="left",Yt="auto",Ut=[Vt,Kt,Qt,Xt],Gt="start",Jt="end",Zt="clippingParents",te="viewport",ee="popper",ie="reference",ne=Ut.reduce(function(t,e){return t.concat([e+"-"+Gt,e+"-"+Jt])},[]),se=[].concat(Ut,[Yt]).reduce(function(t,e){return t.concat([e,e+"-"+Gt,e+"-"+Jt])},[]),oe="beforeRead",re="read",ae="afterRead",le="beforeMain",ce="main",he="afterMain",de="beforeWrite",ue="write",fe="afterWrite",pe=[oe,re,ae,le,ce,he,de,ue,fe];function me(t){return t?(t.nodeName||"").toLowerCase():null}function ge(t){if(null==t)return window;if("[object Window]"!==t.toString()){var e=t.ownerDocument;return e&&e.defaultView||window}return t}function _e(t){return t instanceof ge(t).Element||t instanceof Element}function be(t){return t instanceof ge(t).HTMLElement||t instanceof HTMLElement}function ve(t){return"undefined"!=typeof ShadowRoot&&(t instanceof ge(t).ShadowRoot||t instanceof ShadowRoot)}const ye={name:"applyStyles",enabled:!0,phase:"write",fn:function(t){var e=t.state;Object.keys(e.elements).forEach(function(t){var i=e.styles[t]||{},n=e.attributes[t]||{},s=e.elements[t];be(s)&&me(s)&&(Object.assign(s.style,i),Object.keys(n).forEach(function(t){var e=n[t];!1===e?s.removeAttribute(t):s.setAttribute(t,!0===e?"":e)}))})},effect:function(t){var e=t.state,i={popper:{position:e.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(e.elements.popper.style,i.popper),e.styles=i,e.elements.arrow&&Object.assign(e.elements.arrow.style,i.arrow),function(){Object.keys(e.elements).forEach(function(t){var n=e.elements[t],s=e.attributes[t]||{},o=Object.keys(e.styles.hasOwnProperty(t)?e.styles[t]:i[t]).reduce(function(t,e){return t[e]="",t},{});be(n)&&me(n)&&(Object.assign(n.style,o),Object.keys(s).forEach(function(t){n.removeAttribute(t)}))})}},requires:["computeStyles"]};function we(t){return t.split("-")[0]}var Ae=Math.max,Ee=Math.min,Te=Math.round;function Ce(){var t=navigator.userAgentData;return null!=t&&t.brands&&Array.isArray(t.brands)?t.brands.map(function(t){return t.brand+"/"+t.version}).join(" "):navigator.userAgent}function Oe(){return!/^((?!chrome|android).)*safari/i.test(Ce())}function xe(t,e,i){void 0===e&&(e=!1),void 0===i&&(i=!1);var n=t.getBoundingClientRect(),s=1,o=1;e&&be(t)&&(s=t.offsetWidth>0&&Te(n.width)/t.offsetWidth||1,o=t.offsetHeight>0&&Te(n.height)/t.offsetHeight||1);var r=(_e(t)?ge(t):window).visualViewport,a=!Oe()&&i,l=(n.left+(a&&r?r.offsetLeft:0))/s,c=(n.top+(a&&r?r.offsetTop:0))/o,h=n.width/s,d=n.height/o;return{width:h,height:d,top:c,right:l+h,bottom:c+d,left:l,x:l,y:c}}function ke(t){var e=xe(t),i=t.offsetWidth,n=t.offsetHeight;return Math.abs(e.width-i)<=1&&(i=e.width),Math.abs(e.height-n)<=1&&(n=e.height),{x:t.offsetLeft,y:t.offsetTop,width:i,height:n}}function Le(t,e){var i=e.getRootNode&&e.getRootNode();if(t.contains(e))return!0;if(i&&ve(i)){var n=e;do{if(n&&t.isSameNode(n))return!0;n=n.parentNode||n.host}while(n)}return!1}function Se(t){return ge(t).getComputedStyle(t)}function De(t){return["table","td","th"].indexOf(me(t))>=0}function $e(t){return((_e(t)?t.ownerDocument:t.document)||window.document).documentElement}function Ie(t){return"html"===me(t)?t:t.assignedSlot||t.parentNode||(ve(t)?t.host:null)||$e(t)}function Ne(t){return be(t)&&"fixed"!==Se(t).position?t.offsetParent:null}function Pe(t){for(var e=ge(t),i=Ne(t);i&&De(i)&&"static"===Se(i).position;)i=Ne(i);return i&&("html"===me(i)||"body"===me(i)&&"static"===Se(i).position)?e:i||function(t){var e=/firefox/i.test(Ce());if(/Trident/i.test(Ce())&&be(t)&&"fixed"===Se(t).position)return null;var i=Ie(t);for(ve(i)&&(i=i.host);be(i)&&["html","body"].indexOf(me(i))<0;){var n=Se(i);if("none"!==n.transform||"none"!==n.perspective||"paint"===n.contain||-1!==["transform","perspective"].indexOf(n.willChange)||e&&"filter"===n.willChange||e&&n.filter&&"none"!==n.filter)return i;i=i.parentNode}return null}(t)||e}function je(t){return["top","bottom"].indexOf(t)>=0?"x":"y"}function Me(t,e,i){return Ae(t,Ee(e,i))}function Fe(t){return Object.assign({},{top:0,right:0,bottom:0,left:0},t)}function He(t,e){return e.reduce(function(e,i){return e[i]=t,e},{})}const We={name:"arrow",enabled:!0,phase:"main",fn:function(t){var e,i=t.state,n=t.name,s=t.options,o=i.elements.arrow,r=i.modifiersData.popperOffsets,a=we(i.placement),l=je(a),c=[Xt,Qt].indexOf(a)>=0?"height":"width";if(o&&r){var h=function(t,e){return Fe("number"!=typeof(t="function"==typeof t?t(Object.assign({},e.rects,{placement:e.placement})):t)?t:He(t,Ut))}(s.padding,i),d=ke(o),u="y"===l?Vt:Xt,f="y"===l?Kt:Qt,p=i.rects.reference[c]+i.rects.reference[l]-r[l]-i.rects.popper[c],m=r[l]-i.rects.reference[l],g=Pe(o),_=g?"y"===l?g.clientHeight||0:g.clientWidth||0:0,b=p/2-m/2,v=h[u],y=_-d[c]-h[f],w=_/2-d[c]/2+b,A=Me(v,w,y),E=l;i.modifiersData[n]=((e={})[E]=A,e.centerOffset=A-w,e)}},effect:function(t){var e=t.state,i=t.options.element,n=void 0===i?"[data-popper-arrow]":i;null!=n&&("string"!=typeof n||(n=e.elements.popper.querySelector(n)))&&Le(e.elements.popper,n)&&(e.elements.arrow=n)},requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function Be(t){return t.split("-")[1]}var ze={top:"auto",right:"auto",bottom:"auto",left:"auto"};function Re(t){var e,i=t.popper,n=t.popperRect,s=t.placement,o=t.variation,r=t.offsets,a=t.position,l=t.gpuAcceleration,c=t.adaptive,h=t.roundOffsets,d=t.isFixed,u=r.x,f=void 0===u?0:u,p=r.y,m=void 0===p?0:p,g="function"==typeof h?h({x:f,y:m}):{x:f,y:m};f=g.x,m=g.y;var _=r.hasOwnProperty("x"),b=r.hasOwnProperty("y"),v=Xt,y=Vt,w=window;if(c){var A=Pe(i),E="clientHeight",T="clientWidth";A===ge(i)&&"static"!==Se(A=$e(i)).position&&"absolute"===a&&(E="scrollHeight",T="scrollWidth"),(s===Vt||(s===Xt||s===Qt)&&o===Jt)&&(y=Kt,m-=(d&&A===w&&w.visualViewport?w.visualViewport.height:A[E])-n.height,m*=l?1:-1),s!==Xt&&(s!==Vt&&s!==Kt||o!==Jt)||(v=Qt,f-=(d&&A===w&&w.visualViewport?w.visualViewport.width:A[T])-n.width,f*=l?1:-1)}var C,O=Object.assign({position:a},c&&ze),x=!0===h?function(t,e){var i=t.x,n=t.y,s=e.devicePixelRatio||1;return{x:Te(i*s)/s||0,y:Te(n*s)/s||0}}({x:f,y:m},ge(i)):{x:f,y:m};return f=x.x,m=x.y,l?Object.assign({},O,((C={})[y]=b?"0":"",C[v]=_?"0":"",C.transform=(w.devicePixelRatio||1)<=1?"translate("+f+"px, "+m+"px)":"translate3d("+f+"px, "+m+"px, 0)",C)):Object.assign({},O,((e={})[y]=b?m+"px":"",e[v]=_?f+"px":"",e.transform="",e))}const qe={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:function(t){var e=t.state,i=t.options,n=i.gpuAcceleration,s=void 0===n||n,o=i.adaptive,r=void 0===o||o,a=i.roundOffsets,l=void 0===a||a,c={placement:we(e.placement),variation:Be(e.placement),popper:e.elements.popper,popperRect:e.rects.popper,gpuAcceleration:s,isFixed:"fixed"===e.options.strategy};null!=e.modifiersData.popperOffsets&&(e.styles.popper=Object.assign({},e.styles.popper,Re(Object.assign({},c,{offsets:e.modifiersData.popperOffsets,position:e.options.strategy,adaptive:r,roundOffsets:l})))),null!=e.modifiersData.arrow&&(e.styles.arrow=Object.assign({},e.styles.arrow,Re(Object.assign({},c,{offsets:e.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:l})))),e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-placement":e.placement})},data:{}};var Ve={passive:!0};const Ke={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:function(t){var e=t.state,i=t.instance,n=t.options,s=n.scroll,o=void 0===s||s,r=n.resize,a=void 0===r||r,l=ge(e.elements.popper),c=[].concat(e.scrollParents.reference,e.scrollParents.popper);return o&&c.forEach(function(t){t.addEventListener("scroll",i.update,Ve)}),a&&l.addEventListener("resize",i.update,Ve),function(){o&&c.forEach(function(t){t.removeEventListener("scroll",i.update,Ve)}),a&&l.removeEventListener("resize",i.update,Ve)}},data:{}};var Qe={left:"right",right:"left",bottom:"top",top:"bottom"};function Xe(t){return t.replace(/left|right|bottom|top/g,function(t){return Qe[t]})}var Ye={start:"end",end:"start"};function Ue(t){return t.replace(/start|end/g,function(t){return Ye[t]})}function Ge(t){var e=ge(t);return{scrollLeft:e.pageXOffset,scrollTop:e.pageYOffset}}function Je(t){return xe($e(t)).left+Ge(t).scrollLeft}function Ze(t){var e=Se(t),i=e.overflow,n=e.overflowX,s=e.overflowY;return/auto|scroll|overlay|hidden/.test(i+s+n)}function ti(t){return["html","body","#document"].indexOf(me(t))>=0?t.ownerDocument.body:be(t)&&Ze(t)?t:ti(Ie(t))}function ei(t,e){var i;void 0===e&&(e=[]);var n=ti(t),s=n===(null==(i=t.ownerDocument)?void 0:i.body),o=ge(n),r=s?[o].concat(o.visualViewport||[],Ze(n)?n:[]):n,a=e.concat(r);return s?a:a.concat(ei(Ie(r)))}function ii(t){return Object.assign({},t,{left:t.x,top:t.y,right:t.x+t.width,bottom:t.y+t.height})}function ni(t,e,i){return e===te?ii(function(t,e){var i=ge(t),n=$e(t),s=i.visualViewport,o=n.clientWidth,r=n.clientHeight,a=0,l=0;if(s){o=s.width,r=s.height;var c=Oe();(c||!c&&"fixed"===e)&&(a=s.offsetLeft,l=s.offsetTop)}return{width:o,height:r,x:a+Je(t),y:l}}(t,i)):_e(e)?function(t,e){var i=xe(t,!1,"fixed"===e);return i.top=i.top+t.clientTop,i.left=i.left+t.clientLeft,i.bottom=i.top+t.clientHeight,i.right=i.left+t.clientWidth,i.width=t.clientWidth,i.height=t.clientHeight,i.x=i.left,i.y=i.top,i}(e,i):ii(function(t){var e,i=$e(t),n=Ge(t),s=null==(e=t.ownerDocument)?void 0:e.body,o=Ae(i.scrollWidth,i.clientWidth,s?s.scrollWidth:0,s?s.clientWidth:0),r=Ae(i.scrollHeight,i.clientHeight,s?s.scrollHeight:0,s?s.clientHeight:0),a=-n.scrollLeft+Je(t),l=-n.scrollTop;return"rtl"===Se(s||i).direction&&(a+=Ae(i.clientWidth,s?s.clientWidth:0)-o),{width:o,height:r,x:a,y:l}}($e(t)))}function si(t){var e,i=t.reference,n=t.element,s=t.placement,o=s?we(s):null,r=s?Be(s):null,a=i.x+i.width/2-n.width/2,l=i.y+i.height/2-n.height/2;switch(o){case Vt:e={x:a,y:i.y-n.height};break;case Kt:e={x:a,y:i.y+i.height};break;case Qt:e={x:i.x+i.width,y:l};break;case Xt:e={x:i.x-n.width,y:l};break;default:e={x:i.x,y:i.y}}var c=o?je(o):null;if(null!=c){var h="y"===c?"height":"width";switch(r){case Gt:e[c]=e[c]-(i[h]/2-n[h]/2);break;case Jt:e[c]=e[c]+(i[h]/2-n[h]/2)}}return e}function oi(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=void 0===n?t.placement:n,o=i.strategy,r=void 0===o?t.strategy:o,a=i.boundary,l=void 0===a?Zt:a,c=i.rootBoundary,h=void 0===c?te:c,d=i.elementContext,u=void 0===d?ee:d,f=i.altBoundary,p=void 0!==f&&f,m=i.padding,g=void 0===m?0:m,_=Fe("number"!=typeof g?g:He(g,Ut)),b=u===ee?ie:ee,v=t.rects.popper,y=t.elements[p?b:u],w=function(t,e,i,n){var s="clippingParents"===e?function(t){var e=ei(Ie(t)),i=["absolute","fixed"].indexOf(Se(t).position)>=0&&be(t)?Pe(t):t;return _e(i)?e.filter(function(t){return _e(t)&&Le(t,i)&&"body"!==me(t)}):[]}(t):[].concat(e),o=[].concat(s,[i]),r=o[0],a=o.reduce(function(e,i){var s=ni(t,i,n);return e.top=Ae(s.top,e.top),e.right=Ee(s.right,e.right),e.bottom=Ee(s.bottom,e.bottom),e.left=Ae(s.left,e.left),e},ni(t,r,n));return a.width=a.right-a.left,a.height=a.bottom-a.top,a.x=a.left,a.y=a.top,a}(_e(y)?y:y.contextElement||$e(t.elements.popper),l,h,r),A=xe(t.elements.reference),E=si({reference:A,element:v,placement:s}),T=ii(Object.assign({},v,E)),C=u===ee?T:A,O={top:w.top-C.top+_.top,bottom:C.bottom-w.bottom+_.bottom,left:w.left-C.left+_.left,right:C.right-w.right+_.right},x=t.modifiersData.offset;if(u===ee&&x){var k=x[s];Object.keys(O).forEach(function(t){var e=[Qt,Kt].indexOf(t)>=0?1:-1,i=[Vt,Kt].indexOf(t)>=0?"y":"x";O[t]+=k[i]*e})}return O}function ri(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=i.boundary,o=i.rootBoundary,r=i.padding,a=i.flipVariations,l=i.allowedAutoPlacements,c=void 0===l?se:l,h=Be(n),d=h?a?ne:ne.filter(function(t){return Be(t)===h}):Ut,u=d.filter(function(t){return c.indexOf(t)>=0});0===u.length&&(u=d);var f=u.reduce(function(e,i){return e[i]=oi(t,{placement:i,boundary:s,rootBoundary:o,padding:r})[we(i)],e},{});return Object.keys(f).sort(function(t,e){return f[t]-f[e]})}const ai={name:"flip",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,n=t.name;if(!e.modifiersData[n]._skip){for(var s=i.mainAxis,o=void 0===s||s,r=i.altAxis,a=void 0===r||r,l=i.fallbackPlacements,c=i.padding,h=i.boundary,d=i.rootBoundary,u=i.altBoundary,f=i.flipVariations,p=void 0===f||f,m=i.allowedAutoPlacements,g=e.options.placement,_=we(g),b=l||(_!==g&&p?function(t){if(we(t)===Yt)return[];var e=Xe(t);return[Ue(t),e,Ue(e)]}(g):[Xe(g)]),v=[g].concat(b).reduce(function(t,i){return t.concat(we(i)===Yt?ri(e,{placement:i,boundary:h,rootBoundary:d,padding:c,flipVariations:p,allowedAutoPlacements:m}):i)},[]),y=e.rects.reference,w=e.rects.popper,A=new Map,E=!0,T=v[0],C=0;C=0,S=L?"width":"height",D=oi(e,{placement:O,boundary:h,rootBoundary:d,altBoundary:u,padding:c}),$=L?k?Qt:Xt:k?Kt:Vt;y[S]>w[S]&&($=Xe($));var I=Xe($),N=[];if(o&&N.push(D[x]<=0),a&&N.push(D[$]<=0,D[I]<=0),N.every(function(t){return t})){T=O,E=!1;break}A.set(O,N)}if(E)for(var P=function(t){var e=v.find(function(e){var i=A.get(e);if(i)return i.slice(0,t).every(function(t){return t})});if(e)return T=e,"break"},j=p?3:1;j>0&&"break"!==P(j);j--);e.placement!==T&&(e.modifiersData[n]._skip=!0,e.placement=T,e.reset=!0)}},requiresIfExists:["offset"],data:{_skip:!1}};function li(t,e,i){return void 0===i&&(i={x:0,y:0}),{top:t.top-e.height-i.y,right:t.right-e.width+i.x,bottom:t.bottom-e.height+i.y,left:t.left-e.width-i.x}}function ci(t){return[Vt,Qt,Kt,Xt].some(function(e){return t[e]>=0})}const hi={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:function(t){var e=t.state,i=t.name,n=e.rects.reference,s=e.rects.popper,o=e.modifiersData.preventOverflow,r=oi(e,{elementContext:"reference"}),a=oi(e,{altBoundary:!0}),l=li(r,n),c=li(a,s,o),h=ci(l),d=ci(c);e.modifiersData[i]={referenceClippingOffsets:l,popperEscapeOffsets:c,isReferenceHidden:h,hasPopperEscaped:d},e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-reference-hidden":h,"data-popper-escaped":d})}},di={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:function(t){var e=t.state,i=t.options,n=t.name,s=i.offset,o=void 0===s?[0,0]:s,r=se.reduce(function(t,i){return t[i]=function(t,e,i){var n=we(t),s=[Xt,Vt].indexOf(n)>=0?-1:1,o="function"==typeof i?i(Object.assign({},e,{placement:t})):i,r=o[0],a=o[1];return r=r||0,a=(a||0)*s,[Xt,Qt].indexOf(n)>=0?{x:a,y:r}:{x:r,y:a}}(i,e.rects,o),t},{}),a=r[e.placement],l=a.x,c=a.y;null!=e.modifiersData.popperOffsets&&(e.modifiersData.popperOffsets.x+=l,e.modifiersData.popperOffsets.y+=c),e.modifiersData[n]=r}},ui={name:"popperOffsets",enabled:!0,phase:"read",fn:function(t){var e=t.state,i=t.name;e.modifiersData[i]=si({reference:e.rects.reference,element:e.rects.popper,placement:e.placement})},data:{}},fi={name:"preventOverflow",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,n=t.name,s=i.mainAxis,o=void 0===s||s,r=i.altAxis,a=void 0!==r&&r,l=i.boundary,c=i.rootBoundary,h=i.altBoundary,d=i.padding,u=i.tether,f=void 0===u||u,p=i.tetherOffset,m=void 0===p?0:p,g=oi(e,{boundary:l,rootBoundary:c,padding:d,altBoundary:h}),_=we(e.placement),b=Be(e.placement),v=!b,y=je(_),w="x"===y?"y":"x",A=e.modifiersData.popperOffsets,E=e.rects.reference,T=e.rects.popper,C="function"==typeof m?m(Object.assign({},e.rects,{placement:e.placement})):m,O="number"==typeof C?{mainAxis:C,altAxis:C}:Object.assign({mainAxis:0,altAxis:0},C),x=e.modifiersData.offset?e.modifiersData.offset[e.placement]:null,k={x:0,y:0};if(A){if(o){var L,S="y"===y?Vt:Xt,D="y"===y?Kt:Qt,$="y"===y?"height":"width",I=A[y],N=I+g[S],P=I-g[D],j=f?-T[$]/2:0,M=b===Gt?E[$]:T[$],F=b===Gt?-T[$]:-E[$],H=e.elements.arrow,W=f&&H?ke(H):{width:0,height:0},B=e.modifiersData["arrow#persistent"]?e.modifiersData["arrow#persistent"].padding:{top:0,right:0,bottom:0,left:0},z=B[S],R=B[D],q=Me(0,E[$],W[$]),V=v?E[$]/2-j-q-z-O.mainAxis:M-q-z-O.mainAxis,K=v?-E[$]/2+j+q+R+O.mainAxis:F+q+R+O.mainAxis,Q=e.elements.arrow&&Pe(e.elements.arrow),X=Q?"y"===y?Q.clientTop||0:Q.clientLeft||0:0,Y=null!=(L=null==x?void 0:x[y])?L:0,U=I+K-Y,G=Me(f?Ee(N,I+V-Y-X):N,I,f?Ae(P,U):P);A[y]=G,k[y]=G-I}if(a){var J,Z="x"===y?Vt:Xt,tt="x"===y?Kt:Qt,et=A[w],it="y"===w?"height":"width",nt=et+g[Z],st=et-g[tt],ot=-1!==[Vt,Xt].indexOf(_),rt=null!=(J=null==x?void 0:x[w])?J:0,at=ot?nt:et-E[it]-T[it]-rt+O.altAxis,lt=ot?et+E[it]+T[it]-rt-O.altAxis:st,ct=f&&ot?function(t,e,i){var n=Me(t,e,i);return n>i?i:n}(at,et,lt):Me(f?at:nt,et,f?lt:st);A[w]=ct,k[w]=ct-et}e.modifiersData[n]=k}},requiresIfExists:["offset"]};function pi(t,e,i){void 0===i&&(i=!1);var n,s,o=be(e),r=be(e)&&function(t){var e=t.getBoundingClientRect(),i=Te(e.width)/t.offsetWidth||1,n=Te(e.height)/t.offsetHeight||1;return 1!==i||1!==n}(e),a=$e(e),l=xe(t,r,i),c={scrollLeft:0,scrollTop:0},h={x:0,y:0};return(o||!o&&!i)&&(("body"!==me(e)||Ze(a))&&(c=(n=e)!==ge(n)&&be(n)?{scrollLeft:(s=n).scrollLeft,scrollTop:s.scrollTop}:Ge(n)),be(e)?((h=xe(e,!0)).x+=e.clientLeft,h.y+=e.clientTop):a&&(h.x=Je(a))),{x:l.left+c.scrollLeft-h.x,y:l.top+c.scrollTop-h.y,width:l.width,height:l.height}}function mi(t){var e=new Map,i=new Set,n=[];function s(t){i.add(t.name),[].concat(t.requires||[],t.requiresIfExists||[]).forEach(function(t){if(!i.has(t)){var n=e.get(t);n&&s(n)}}),n.push(t)}return t.forEach(function(t){e.set(t.name,t)}),t.forEach(function(t){i.has(t.name)||s(t)}),n}var gi={placement:"bottom",modifiers:[],strategy:"absolute"};function _i(){for(var t=arguments.length,e=new Array(t),i=0;iNumber.parseInt(t,10)):"function"==typeof t?e=>t(e,this._element):t}_getPopperConfig(){const t={placement:this._getPlacement(),modifiers:[{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"offset",options:{offset:this._getOffset()}}]};return(this._inNavbar||"static"===this._config.display)&&(H.setDataAttribute(this._menu,"popper","static"),t.modifiers=[{name:"applyStyles",enabled:!1}]),{...t,..._(this._config.popperConfig,[void 0,t])}}_selectMenuItem({key:t,target:e}){const i=R.find(".dropdown-menu .dropdown-item:not(.disabled):not(:disabled)",this._menu).filter(t=>l(t));i.length&&v(i,e,t===xi,!i.includes(e)).focus()}static jQueryInterface(t){return this.each(function(){const e=Qi.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}})}static clearMenus(t){if(2===t.button||"keyup"===t.type&&"Tab"!==t.key)return;const e=R.find(Mi);for(const i of e){const e=Qi.getInstance(i);if(!e||!1===e._config.autoClose)continue;const n=t.composedPath(),s=n.includes(e._menu);if(n.includes(e._element)||"inside"===e._config.autoClose&&!s||"outside"===e._config.autoClose&&s)continue;if(e._menu.contains(t.target)&&("keyup"===t.type&&"Tab"===t.key||/input|select|option|textarea|form/i.test(t.target.tagName)))continue;const o={relatedTarget:e._element};"click"===t.type&&(o.clickEvent=t),e._completeHide(o)}}static dataApiKeydownHandler(t){const e=/input|textarea/i.test(t.target.tagName),i="Escape"===t.key,n=[Oi,xi].includes(t.key);if(!n&&!i)return;if(e&&!i)return;t.preventDefault();const s=this.matches(ji)?this:R.prev(this,ji)[0]||R.next(this,ji)[0]||R.findOne(ji,t.delegateTarget.parentNode),o=Qi.getOrCreateInstance(s);if(n)return t.stopPropagation(),o.show(),void o._selectMenuItem(t);o._isShown()&&(t.stopPropagation(),o.hide(),s.focus())}}P.on(document,Ii,ji,Qi.dataApiKeydownHandler),P.on(document,Ii,Fi,Qi.dataApiKeydownHandler),P.on(document,$i,Qi.clearMenus),P.on(document,Ni,Qi.clearMenus),P.on(document,$i,ji,function(t){t.preventDefault(),Qi.getOrCreateInstance(this).toggle()}),g(Qi);const Xi="backdrop",Yi="show",Ui=`mousedown.bs.${Xi}`,Gi={className:"modal-backdrop",clickCallback:null,isAnimated:!1,isVisible:!0,rootElement:"body"},Ji={className:"string",clickCallback:"(function|null)",isAnimated:"boolean",isVisible:"boolean",rootElement:"(element|string)"};class Zi extends W{constructor(t){super(),this._config=this._getConfig(t),this._isAppended=!1,this._element=null}static get Default(){return Gi}static get DefaultType(){return Ji}static get NAME(){return Xi}show(t){if(!this._config.isVisible)return void _(t);this._append();const e=this._getElement();this._config.isAnimated&&u(e),e.classList.add(Yi),this._emulateAnimation(()=>{_(t)})}hide(t){this._config.isVisible?(this._getElement().classList.remove(Yi),this._emulateAnimation(()=>{this.dispose(),_(t)})):_(t)}dispose(){this._isAppended&&(P.off(this._element,Ui),this._element.remove(),this._isAppended=!1)}_getElement(){if(!this._element){const t=document.createElement("div");t.className=this._config.className,this._config.isAnimated&&t.classList.add("fade"),this._element=t}return this._element}_configAfterMerge(t){return t.rootElement=a(t.rootElement),t}_append(){if(this._isAppended)return;const t=this._getElement();this._config.rootElement.append(t),P.on(t,Ui,()=>{_(this._config.clickCallback)}),this._isAppended=!0}_emulateAnimation(t){b(t,this._getElement(),this._config.isAnimated)}}const tn=".bs.focustrap",en=`focusin${tn}`,nn=`keydown.tab${tn}`,sn="backward",on={autofocus:!0,trapElement:null},rn={autofocus:"boolean",trapElement:"element"};class an extends W{constructor(t){super(),this._config=this._getConfig(t),this._isActive=!1,this._lastTabNavDirection=null}static get Default(){return on}static get DefaultType(){return rn}static get NAME(){return"focustrap"}activate(){this._isActive||(this._config.autofocus&&this._config.trapElement.focus(),P.off(document,tn),P.on(document,en,t=>this._handleFocusin(t)),P.on(document,nn,t=>this._handleKeydown(t)),this._isActive=!0)}deactivate(){this._isActive&&(this._isActive=!1,P.off(document,tn))}_handleFocusin(t){const{trapElement:e}=this._config;if(t.target===document||t.target===e||e.contains(t.target))return;const i=R.focusableChildren(e);0===i.length?e.focus():this._lastTabNavDirection===sn?i[i.length-1].focus():i[0].focus()}_handleKeydown(t){"Tab"===t.key&&(this._lastTabNavDirection=t.shiftKey?sn:"forward")}}const ln=".fixed-top, .fixed-bottom, .is-fixed, .sticky-top",cn=".sticky-top",hn="padding-right",dn="margin-right";class un{constructor(){this._element=document.body}getWidth(){const t=document.documentElement.clientWidth;return Math.abs(window.innerWidth-t)}hide(){const t=this.getWidth();this._disableOverFlow(),this._setElementAttributes(this._element,hn,e=>e+t),this._setElementAttributes(ln,hn,e=>e+t),this._setElementAttributes(cn,dn,e=>e-t)}reset(){this._resetElementAttributes(this._element,"overflow"),this._resetElementAttributes(this._element,hn),this._resetElementAttributes(ln,hn),this._resetElementAttributes(cn,dn)}isOverflowing(){return this.getWidth()>0}_disableOverFlow(){this._saveInitialAttribute(this._element,"overflow"),this._element.style.overflow="hidden"}_setElementAttributes(t,e,i){const n=this.getWidth();this._applyManipulationCallback(t,t=>{if(t!==this._element&&window.innerWidth>t.clientWidth+n)return;this._saveInitialAttribute(t,e);const s=window.getComputedStyle(t).getPropertyValue(e);t.style.setProperty(e,`${i(Number.parseFloat(s))}px`)})}_saveInitialAttribute(t,e){const i=t.style.getPropertyValue(e);i&&H.setDataAttribute(t,e,i)}_resetElementAttributes(t,e){this._applyManipulationCallback(t,t=>{const i=H.getDataAttribute(t,e);null!==i?(H.removeDataAttribute(t,e),t.style.setProperty(e,i)):t.style.removeProperty(e)})}_applyManipulationCallback(t,e){if(r(t))e(t);else for(const i of R.find(t,this._element))e(i)}}const fn=".bs.modal",pn=`hide${fn}`,mn=`hidePrevented${fn}`,gn=`hidden${fn}`,_n=`show${fn}`,bn=`shown${fn}`,vn=`resize${fn}`,yn=`click.dismiss${fn}`,wn=`mousedown.dismiss${fn}`,An=`keydown.dismiss${fn}`,En=`click${fn}.data-api`,Tn="modal-open",Cn="show",On="modal-static",xn={backdrop:!0,focus:!0,keyboard:!0},kn={backdrop:"(boolean|string)",focus:"boolean",keyboard:"boolean"};class Ln extends B{constructor(t,e){super(t,e),this._dialog=R.findOne(".modal-dialog",this._element),this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._isShown=!1,this._isTransitioning=!1,this._scrollBar=new un,this._addEventListeners()}static get Default(){return xn}static get DefaultType(){return kn}static get NAME(){return"modal"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||this._isTransitioning||P.trigger(this._element,_n,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._isTransitioning=!0,this._scrollBar.hide(),document.body.classList.add(Tn),this._adjustDialog(),this._backdrop.show(()=>this._showElement(t)))}hide(){this._isShown&&!this._isTransitioning&&(P.trigger(this._element,pn).defaultPrevented||(this._isShown=!1,this._isTransitioning=!0,this._focustrap.deactivate(),this._element.classList.remove(Cn),this._queueCallback(()=>this._hideModal(),this._element,this._isAnimated())))}dispose(){P.off(window,fn),P.off(this._dialog,fn),this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}handleUpdate(){this._adjustDialog()}_initializeBackDrop(){return new Zi({isVisible:Boolean(this._config.backdrop),isAnimated:this._isAnimated()})}_initializeFocusTrap(){return new an({trapElement:this._element})}_showElement(t){document.body.contains(this._element)||document.body.append(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.scrollTop=0;const e=R.findOne(".modal-body",this._dialog);e&&(e.scrollTop=0),u(this._element),this._element.classList.add(Cn),this._queueCallback(()=>{this._config.focus&&this._focustrap.activate(),this._isTransitioning=!1,P.trigger(this._element,bn,{relatedTarget:t})},this._dialog,this._isAnimated())}_addEventListeners(){P.on(this._element,An,t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():this._triggerBackdropTransition())}),P.on(window,vn,()=>{this._isShown&&!this._isTransitioning&&this._adjustDialog()}),P.on(this._element,wn,t=>{P.one(this._element,yn,e=>{this._element===t.target&&this._element===e.target&&("static"!==this._config.backdrop?this._config.backdrop&&this.hide():this._triggerBackdropTransition())})})}_hideModal(){this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._isTransitioning=!1,this._backdrop.hide(()=>{document.body.classList.remove(Tn),this._resetAdjustments(),this._scrollBar.reset(),P.trigger(this._element,gn)})}_isAnimated(){return this._element.classList.contains("fade")}_triggerBackdropTransition(){if(P.trigger(this._element,mn).defaultPrevented)return;const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._element.style.overflowY;"hidden"===e||this._element.classList.contains(On)||(t||(this._element.style.overflowY="hidden"),this._element.classList.add(On),this._queueCallback(()=>{this._element.classList.remove(On),this._queueCallback(()=>{this._element.style.overflowY=e},this._dialog)},this._dialog),this._element.focus())}_adjustDialog(){const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._scrollBar.getWidth(),i=e>0;if(i&&!t){const t=m()?"paddingLeft":"paddingRight";this._element.style[t]=`${e}px`}if(!i&&t){const t=m()?"paddingRight":"paddingLeft";this._element.style[t]=`${e}px`}}_resetAdjustments(){this._element.style.paddingLeft="",this._element.style.paddingRight=""}static jQueryInterface(t,e){return this.each(function(){const i=Ln.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t](e)}})}}P.on(document,En,'[data-bs-toggle="modal"]',function(t){const e=R.getElementFromSelector(this);["A","AREA"].includes(this.tagName)&&t.preventDefault(),P.one(e,_n,t=>{t.defaultPrevented||P.one(e,gn,()=>{l(this)&&this.focus()})});const i=R.findOne(".modal.show");i&&Ln.getInstance(i).hide(),Ln.getOrCreateInstance(e).toggle(this)}),q(Ln),g(Ln);const Sn=".bs.offcanvas",Dn=".data-api",$n=`load${Sn}${Dn}`,In="show",Nn="showing",Pn="hiding",jn=".offcanvas.show",Mn=`show${Sn}`,Fn=`shown${Sn}`,Hn=`hide${Sn}`,Wn=`hidePrevented${Sn}`,Bn=`hidden${Sn}`,zn=`resize${Sn}`,Rn=`click${Sn}${Dn}`,qn=`keydown.dismiss${Sn}`,Vn={backdrop:!0,keyboard:!0,scroll:!1},Kn={backdrop:"(boolean|string)",keyboard:"boolean",scroll:"boolean"};class Qn extends B{constructor(t,e){super(t,e),this._isShown=!1,this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._addEventListeners()}static get Default(){return Vn}static get DefaultType(){return Kn}static get NAME(){return"offcanvas"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||P.trigger(this._element,Mn,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._backdrop.show(),this._config.scroll||(new un).hide(),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.classList.add(Nn),this._queueCallback(()=>{this._config.scroll&&!this._config.backdrop||this._focustrap.activate(),this._element.classList.add(In),this._element.classList.remove(Nn),P.trigger(this._element,Fn,{relatedTarget:t})},this._element,!0))}hide(){this._isShown&&(P.trigger(this._element,Hn).defaultPrevented||(this._focustrap.deactivate(),this._element.blur(),this._isShown=!1,this._element.classList.add(Pn),this._backdrop.hide(),this._queueCallback(()=>{this._element.classList.remove(In,Pn),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._config.scroll||(new un).reset(),P.trigger(this._element,Bn)},this._element,!0)))}dispose(){this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}_initializeBackDrop(){const t=Boolean(this._config.backdrop);return new Zi({className:"offcanvas-backdrop",isVisible:t,isAnimated:!0,rootElement:this._element.parentNode,clickCallback:t?()=>{"static"!==this._config.backdrop?this.hide():P.trigger(this._element,Wn)}:null})}_initializeFocusTrap(){return new an({trapElement:this._element})}_addEventListeners(){P.on(this._element,qn,t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():P.trigger(this._element,Wn))})}static jQueryInterface(t){return this.each(function(){const e=Qn.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}})}}P.on(document,Rn,'[data-bs-toggle="offcanvas"]',function(t){const e=R.getElementFromSelector(this);if(["A","AREA"].includes(this.tagName)&&t.preventDefault(),c(this))return;P.one(e,Bn,()=>{l(this)&&this.focus()});const i=R.findOne(jn);i&&i!==e&&Qn.getInstance(i).hide(),Qn.getOrCreateInstance(e).toggle(this)}),P.on(window,$n,()=>{for(const t of R.find(jn))Qn.getOrCreateInstance(t).show()}),P.on(window,zn,()=>{for(const t of R.find("[aria-modal][class*=show][class*=offcanvas-]"))"fixed"!==getComputedStyle(t).position&&Qn.getOrCreateInstance(t).hide()}),q(Qn),g(Qn);const Xn={"*":["class","dir","id","lang","role",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],dd:[],div:[],dl:[],dt:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","srcset","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},Yn=new Set(["background","cite","href","itemtype","longdesc","poster","src","xlink:href"]),Un=/^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i,Gn=(t,e)=>{const i=t.nodeName.toLowerCase();return e.includes(i)?!Yn.has(i)||Boolean(Un.test(t.nodeValue)):e.filter(t=>t instanceof RegExp).some(t=>t.test(i))},Jn={allowList:Xn,content:{},extraClass:"",html:!1,sanitize:!0,sanitizeFn:null,template:"

"},Zn={allowList:"object",content:"object",extraClass:"(string|function)",html:"boolean",sanitize:"boolean",sanitizeFn:"(null|function)",template:"string"},ts={entry:"(string|element|function|null)",selector:"(string|element)"};class es extends W{constructor(t){super(),this._config=this._getConfig(t)}static get Default(){return Jn}static get DefaultType(){return Zn}static get NAME(){return"TemplateFactory"}getContent(){return Object.values(this._config.content).map(t=>this._resolvePossibleFunction(t)).filter(Boolean)}hasContent(){return this.getContent().length>0}changeContent(t){return this._checkContent(t),this._config.content={...this._config.content,...t},this}toHtml(){const t=document.createElement("div");t.innerHTML=this._maybeSanitize(this._config.template);for(const[e,i]of Object.entries(this._config.content))this._setContent(t,i,e);const e=t.children[0],i=this._resolvePossibleFunction(this._config.extraClass);return i&&e.classList.add(...i.split(" ")),e}_typeCheckConfig(t){super._typeCheckConfig(t),this._checkContent(t.content)}_checkContent(t){for(const[e,i]of Object.entries(t))super._typeCheckConfig({selector:e,entry:i},ts)}_setContent(t,e,i){const n=R.findOne(i,t);n&&((e=this._resolvePossibleFunction(e))?r(e)?this._putElementInTemplate(a(e),n):this._config.html?n.innerHTML=this._maybeSanitize(e):n.textContent=e:n.remove())}_maybeSanitize(t){return this._config.sanitize?function(t,e,i){if(!t.length)return t;if(i&&"function"==typeof i)return i(t);const n=(new window.DOMParser).parseFromString(t,"text/html"),s=[].concat(...n.body.querySelectorAll("*"));for(const t of s){const i=t.nodeName.toLowerCase();if(!Object.keys(e).includes(i)){t.remove();continue}const n=[].concat(...t.attributes),s=[].concat(e["*"]||[],e[i]||[]);for(const e of n)Gn(e,s)||t.removeAttribute(e.nodeName)}return n.body.innerHTML}(t,this._config.allowList,this._config.sanitizeFn):t}_resolvePossibleFunction(t){return _(t,[void 0,this])}_putElementInTemplate(t,e){if(this._config.html)return e.innerHTML="",void e.append(t);e.textContent=t.textContent}}const is=new Set(["sanitize","allowList","sanitizeFn"]),ns="fade",ss="show",os=".tooltip-inner",rs=".modal",as="hide.bs.modal",ls="hover",cs="focus",hs="click",ds={AUTO:"auto",TOP:"top",RIGHT:m()?"left":"right",BOTTOM:"bottom",LEFT:m()?"right":"left"},us={allowList:Xn,animation:!0,boundary:"clippingParents",container:!1,customClass:"",delay:0,fallbackPlacements:["top","right","bottom","left"],html:!1,offset:[0,6],placement:"top",popperConfig:null,sanitize:!0,sanitizeFn:null,selector:!1,template:'',title:"",trigger:"hover focus"},fs={allowList:"object",animation:"boolean",boundary:"(string|element)",container:"(string|element|boolean)",customClass:"(string|function)",delay:"(number|object)",fallbackPlacements:"array",html:"boolean",offset:"(array|string|function)",placement:"(string|function)",popperConfig:"(null|object|function)",sanitize:"boolean",sanitizeFn:"(null|function)",selector:"(string|boolean)",template:"string",title:"(string|element|function)",trigger:"string"};class ps extends B{constructor(t,e){if(void 0===Ai)throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org/docs/v2/)");super(t,e),this._isEnabled=!0,this._timeout=0,this._isHovered=null,this._activeTrigger={},this._popper=null,this._templateFactory=null,this._newContent=null,this.tip=null,this._setListeners(),this._config.selector||this._fixTitle()}static get Default(){return us}static get DefaultType(){return fs}static get NAME(){return"tooltip"}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(){this._isEnabled&&(this._isShown()?this._leave():this._enter())}dispose(){clearTimeout(this._timeout),P.off(this._element.closest(rs),as,this._hideModalHandler),this._element.getAttribute("data-bs-original-title")&&this._element.setAttribute("title",this._element.getAttribute("data-bs-original-title")),this._disposePopper(),super.dispose()}show(){if("none"===this._element.style.display)throw new Error("Please use show on visible elements");if(!this._isWithContent()||!this._isEnabled)return;const t=P.trigger(this._element,this.constructor.eventName("show")),e=(h(this._element)||this._element.ownerDocument.documentElement).contains(this._element);if(t.defaultPrevented||!e)return;this._disposePopper();const i=this._getTipElement();this._element.setAttribute("aria-describedby",i.getAttribute("id"));const{container:n}=this._config;if(this._element.ownerDocument.documentElement.contains(this.tip)||(n.append(i),P.trigger(this._element,this.constructor.eventName("inserted"))),this._popper=this._createPopper(i),i.classList.add(ss),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))P.on(t,"mouseover",d);this._queueCallback(()=>{P.trigger(this._element,this.constructor.eventName("shown")),!1===this._isHovered&&this._leave(),this._isHovered=!1},this.tip,this._isAnimated())}hide(){if(this._isShown()&&!P.trigger(this._element,this.constructor.eventName("hide")).defaultPrevented){if(this._getTipElement().classList.remove(ss),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))P.off(t,"mouseover",d);this._activeTrigger[hs]=!1,this._activeTrigger[cs]=!1,this._activeTrigger[ls]=!1,this._isHovered=null,this._queueCallback(()=>{this._isWithActiveTrigger()||(this._isHovered||this._disposePopper(),this._element.removeAttribute("aria-describedby"),P.trigger(this._element,this.constructor.eventName("hidden")))},this.tip,this._isAnimated())}}update(){this._popper&&this._popper.update()}_isWithContent(){return Boolean(this._getTitle())}_getTipElement(){return this.tip||(this.tip=this._createTipElement(this._newContent||this._getContentForTemplate())),this.tip}_createTipElement(t){const e=this._getTemplateFactory(t).toHtml();if(!e)return null;e.classList.remove(ns,ss),e.classList.add(`bs-${this.constructor.NAME}-auto`);const i=(t=>{do{t+=Math.floor(1e6*Math.random())}while(document.getElementById(t));return t})(this.constructor.NAME).toString();return e.setAttribute("id",i),this._isAnimated()&&e.classList.add(ns),e}setContent(t){this._newContent=t,this._isShown()&&(this._disposePopper(),this.show())}_getTemplateFactory(t){return this._templateFactory?this._templateFactory.changeContent(t):this._templateFactory=new es({...this._config,content:t,extraClass:this._resolvePossibleFunction(this._config.customClass)}),this._templateFactory}_getContentForTemplate(){return{[os]:this._getTitle()}}_getTitle(){return this._resolvePossibleFunction(this._config.title)||this._element.getAttribute("data-bs-original-title")}_initializeOnDelegatedTarget(t){return this.constructor.getOrCreateInstance(t.delegateTarget,this._getDelegateConfig())}_isAnimated(){return this._config.animation||this.tip&&this.tip.classList.contains(ns)}_isShown(){return this.tip&&this.tip.classList.contains(ss)}_createPopper(t){const e=_(this._config.placement,[this,t,this._element]),i=ds[e.toUpperCase()];return wi(this._element,t,this._getPopperConfig(i))}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map(t=>Number.parseInt(t,10)):"function"==typeof t?e=>t(e,this._element):t}_resolvePossibleFunction(t){return _(t,[this._element,this._element])}_getPopperConfig(t){const e={placement:t,modifiers:[{name:"flip",options:{fallbackPlacements:this._config.fallbackPlacements}},{name:"offset",options:{offset:this._getOffset()}},{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"arrow",options:{element:`.${this.constructor.NAME}-arrow`}},{name:"preSetPlacement",enabled:!0,phase:"beforeMain",fn:t=>{this._getTipElement().setAttribute("data-popper-placement",t.state.placement)}}]};return{...e,..._(this._config.popperConfig,[void 0,e])}}_setListeners(){const t=this._config.trigger.split(" ");for(const e of t)if("click"===e)P.on(this._element,this.constructor.eventName("click"),this._config.selector,t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger[hs]=!(e._isShown()&&e._activeTrigger[hs]),e.toggle()});else if("manual"!==e){const t=e===ls?this.constructor.eventName("mouseenter"):this.constructor.eventName("focusin"),i=e===ls?this.constructor.eventName("mouseleave"):this.constructor.eventName("focusout");P.on(this._element,t,this._config.selector,t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusin"===t.type?cs:ls]=!0,e._enter()}),P.on(this._element,i,this._config.selector,t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusout"===t.type?cs:ls]=e._element.contains(t.relatedTarget),e._leave()})}this._hideModalHandler=()=>{this._element&&this.hide()},P.on(this._element.closest(rs),as,this._hideModalHandler)}_fixTitle(){const t=this._element.getAttribute("title");t&&(this._element.getAttribute("aria-label")||this._element.textContent.trim()||this._element.setAttribute("aria-label",t),this._element.setAttribute("data-bs-original-title",t),this._element.removeAttribute("title"))}_enter(){this._isShown()||this._isHovered?this._isHovered=!0:(this._isHovered=!0,this._setTimeout(()=>{this._isHovered&&this.show()},this._config.delay.show))}_leave(){this._isWithActiveTrigger()||(this._isHovered=!1,this._setTimeout(()=>{this._isHovered||this.hide()},this._config.delay.hide))}_setTimeout(t,e){clearTimeout(this._timeout),this._timeout=setTimeout(t,e)}_isWithActiveTrigger(){return Object.values(this._activeTrigger).includes(!0)}_getConfig(t){const e=H.getDataAttributes(this._element);for(const t of Object.keys(e))is.has(t)&&delete e[t];return t={...e,..."object"==typeof t&&t?t:{}},t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t.container=!1===t.container?document.body:a(t.container),"number"==typeof t.delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),t}_getDelegateConfig(){const t={};for(const[e,i]of Object.entries(this._config))this.constructor.Default[e]!==i&&(t[e]=i);return t.selector=!1,t.trigger="manual",t}_disposePopper(){this._popper&&(this._popper.destroy(),this._popper=null),this.tip&&(this.tip.remove(),this.tip=null)}static jQueryInterface(t){return this.each(function(){const e=ps.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}})}}g(ps);const ms=".popover-header",gs=".popover-body",_s={...ps.Default,content:"",offset:[0,8],placement:"right",template:'',trigger:"click"},bs={...ps.DefaultType,content:"(null|string|element|function)"};class vs extends ps{static get Default(){return _s}static get DefaultType(){return bs}static get NAME(){return"popover"}_isWithContent(){return this._getTitle()||this._getContent()}_getContentForTemplate(){return{[ms]:this._getTitle(),[gs]:this._getContent()}}_getContent(){return this._resolvePossibleFunction(this._config.content)}static jQueryInterface(t){return this.each(function(){const e=vs.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}})}}g(vs);const ys=".bs.scrollspy",ws=`activate${ys}`,As=`click${ys}`,Es=`load${ys}.data-api`,Ts="active",Cs="[href]",Os=".nav-link",xs=`${Os}, .nav-item > ${Os}, .list-group-item`,ks={offset:null,rootMargin:"0px 0px -25%",smoothScroll:!1,target:null,threshold:[.1,.5,1]},Ls={offset:"(number|null)",rootMargin:"string",smoothScroll:"boolean",target:"element",threshold:"array"};class Ss extends B{constructor(t,e){super(t,e),this._targetLinks=new Map,this._observableSections=new Map,this._rootElement="visible"===getComputedStyle(this._element).overflowY?null:this._element,this._activeTarget=null,this._observer=null,this._previousScrollData={visibleEntryTop:0,parentScrollTop:0},this.refresh()}static get Default(){return ks}static get DefaultType(){return Ls}static get NAME(){return"scrollspy"}refresh(){this._initializeTargetsAndObservables(),this._maybeEnableSmoothScroll(),this._observer?this._observer.disconnect():this._observer=this._getNewObserver();for(const t of this._observableSections.values())this._observer.observe(t)}dispose(){this._observer.disconnect(),super.dispose()}_configAfterMerge(t){return t.target=a(t.target)||document.body,t.rootMargin=t.offset?`${t.offset}px 0px -30%`:t.rootMargin,"string"==typeof t.threshold&&(t.threshold=t.threshold.split(",").map(t=>Number.parseFloat(t))),t}_maybeEnableSmoothScroll(){this._config.smoothScroll&&(P.off(this._config.target,As),P.on(this._config.target,As,Cs,t=>{const e=this._observableSections.get(t.target.hash);if(e){t.preventDefault();const i=this._rootElement||window,n=e.offsetTop-this._element.offsetTop;if(i.scrollTo)return void i.scrollTo({top:n,behavior:"smooth"});i.scrollTop=n}}))}_getNewObserver(){const t={root:this._rootElement,threshold:this._config.threshold,rootMargin:this._config.rootMargin};return new IntersectionObserver(t=>this._observerCallback(t),t)}_observerCallback(t){const e=t=>this._targetLinks.get(`#${t.target.id}`),i=t=>{this._previousScrollData.visibleEntryTop=t.target.offsetTop,this._process(e(t))},n=(this._rootElement||document.documentElement).scrollTop,s=n>=this._previousScrollData.parentScrollTop;this._previousScrollData.parentScrollTop=n;for(const o of t){if(!o.isIntersecting){this._activeTarget=null,this._clearActiveClass(e(o));continue}const t=o.target.offsetTop>=this._previousScrollData.visibleEntryTop;if(s&&t){if(i(o),!n)return}else s||t||i(o)}}_initializeTargetsAndObservables(){this._targetLinks=new Map,this._observableSections=new Map;const t=R.find(Cs,this._config.target);for(const e of t){if(!e.hash||c(e))continue;const t=R.findOne(decodeURI(e.hash),this._element);l(t)&&(this._targetLinks.set(decodeURI(e.hash),e),this._observableSections.set(e.hash,t))}}_process(t){this._activeTarget!==t&&(this._clearActiveClass(this._config.target),this._activeTarget=t,t.classList.add(Ts),this._activateParents(t),P.trigger(this._element,ws,{relatedTarget:t}))}_activateParents(t){if(t.classList.contains("dropdown-item"))R.findOne(".dropdown-toggle",t.closest(".dropdown")).classList.add(Ts);else for(const e of R.parents(t,".nav, .list-group"))for(const t of R.prev(e,xs))t.classList.add(Ts)}_clearActiveClass(t){t.classList.remove(Ts);const e=R.find(`${Cs}.${Ts}`,t);for(const t of e)t.classList.remove(Ts)}static jQueryInterface(t){return this.each(function(){const e=Ss.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}})}}P.on(window,Es,()=>{for(const t of R.find('[data-bs-spy="scroll"]'))Ss.getOrCreateInstance(t)}),g(Ss);const Ds=".bs.tab",$s=`hide${Ds}`,Is=`hidden${Ds}`,Ns=`show${Ds}`,Ps=`shown${Ds}`,js=`click${Ds}`,Ms=`keydown${Ds}`,Fs=`load${Ds}`,Hs="ArrowLeft",Ws="ArrowRight",Bs="ArrowUp",zs="ArrowDown",Rs="Home",qs="End",Vs="active",Ks="fade",Qs="show",Xs=".dropdown-toggle",Ys=`:not(${Xs})`,Us='[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]',Gs=`.nav-link${Ys}, .list-group-item${Ys}, [role="tab"]${Ys}, ${Us}`,Js=`.${Vs}[data-bs-toggle="tab"], .${Vs}[data-bs-toggle="pill"], .${Vs}[data-bs-toggle="list"]`;class Zs extends B{constructor(t){super(t),this._parent=this._element.closest('.list-group, .nav, [role="tablist"]'),this._parent&&(this._setInitialAttributes(this._parent,this._getChildren()),P.on(this._element,Ms,t=>this._keydown(t)))}static get NAME(){return"tab"}show(){const t=this._element;if(this._elemIsActive(t))return;const e=this._getActiveElem(),i=e?P.trigger(e,$s,{relatedTarget:t}):null;P.trigger(t,Ns,{relatedTarget:e}).defaultPrevented||i&&i.defaultPrevented||(this._deactivate(e,t),this._activate(t,e))}_activate(t,e){t&&(t.classList.add(Vs),this._activate(R.getElementFromSelector(t)),this._queueCallback(()=>{"tab"===t.getAttribute("role")?(t.removeAttribute("tabindex"),t.setAttribute("aria-selected",!0),this._toggleDropDown(t,!0),P.trigger(t,Ps,{relatedTarget:e})):t.classList.add(Qs)},t,t.classList.contains(Ks)))}_deactivate(t,e){t&&(t.classList.remove(Vs),t.blur(),this._deactivate(R.getElementFromSelector(t)),this._queueCallback(()=>{"tab"===t.getAttribute("role")?(t.setAttribute("aria-selected",!1),t.setAttribute("tabindex","-1"),this._toggleDropDown(t,!1),P.trigger(t,Is,{relatedTarget:e})):t.classList.remove(Qs)},t,t.classList.contains(Ks)))}_keydown(t){if(![Hs,Ws,Bs,zs,Rs,qs].includes(t.key))return;t.stopPropagation(),t.preventDefault();const e=this._getChildren().filter(t=>!c(t));let i;if([Rs,qs].includes(t.key))i=e[t.key===Rs?0:e.length-1];else{const n=[Ws,zs].includes(t.key);i=v(e,t.target,n,!0)}i&&(i.focus({preventScroll:!0}),Zs.getOrCreateInstance(i).show())}_getChildren(){return R.find(Gs,this._parent)}_getActiveElem(){return this._getChildren().find(t=>this._elemIsActive(t))||null}_setInitialAttributes(t,e){this._setAttributeIfNotExists(t,"role","tablist");for(const t of e)this._setInitialAttributesOnChild(t)}_setInitialAttributesOnChild(t){t=this._getInnerElement(t);const e=this._elemIsActive(t),i=this._getOuterElement(t);t.setAttribute("aria-selected",e),i!==t&&this._setAttributeIfNotExists(i,"role","presentation"),e||t.setAttribute("tabindex","-1"),this._setAttributeIfNotExists(t,"role","tab"),this._setInitialAttributesOnTargetPanel(t)}_setInitialAttributesOnTargetPanel(t){const e=R.getElementFromSelector(t);e&&(this._setAttributeIfNotExists(e,"role","tabpanel"),t.id&&this._setAttributeIfNotExists(e,"aria-labelledby",`${t.id}`))}_toggleDropDown(t,e){const i=this._getOuterElement(t);if(!i.classList.contains("dropdown"))return;const n=(t,n)=>{const s=R.findOne(t,i);s&&s.classList.toggle(n,e)};n(Xs,Vs),n(".dropdown-menu",Qs),i.setAttribute("aria-expanded",e)}_setAttributeIfNotExists(t,e,i){t.hasAttribute(e)||t.setAttribute(e,i)}_elemIsActive(t){return t.classList.contains(Vs)}_getInnerElement(t){return t.matches(Gs)?t:R.findOne(Gs,t)}_getOuterElement(t){return t.closest(".nav-item, .list-group-item")||t}static jQueryInterface(t){return this.each(function(){const e=Zs.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}})}}P.on(document,js,Us,function(t){["A","AREA"].includes(this.tagName)&&t.preventDefault(),c(this)||Zs.getOrCreateInstance(this).show()}),P.on(window,Fs,()=>{for(const t of R.find(Js))Zs.getOrCreateInstance(t)}),g(Zs);const to=".bs.toast",eo=`mouseover${to}`,io=`mouseout${to}`,no=`focusin${to}`,so=`focusout${to}`,oo=`hide${to}`,ro=`hidden${to}`,ao=`show${to}`,lo=`shown${to}`,co="hide",ho="show",uo="showing",fo={animation:"boolean",autohide:"boolean",delay:"number"},po={animation:!0,autohide:!0,delay:5e3};class mo extends B{constructor(t,e){super(t,e),this._timeout=null,this._hasMouseInteraction=!1,this._hasKeyboardInteraction=!1,this._setListeners()}static get Default(){return po}static get DefaultType(){return fo}static get NAME(){return"toast"}show(){P.trigger(this._element,ao).defaultPrevented||(this._clearTimeout(),this._config.animation&&this._element.classList.add("fade"),this._element.classList.remove(co),u(this._element),this._element.classList.add(ho,uo),this._queueCallback(()=>{this._element.classList.remove(uo),P.trigger(this._element,lo),this._maybeScheduleHide()},this._element,this._config.animation))}hide(){this.isShown()&&(P.trigger(this._element,oo).defaultPrevented||(this._element.classList.add(uo),this._queueCallback(()=>{this._element.classList.add(co),this._element.classList.remove(uo,ho),P.trigger(this._element,ro)},this._element,this._config.animation)))}dispose(){this._clearTimeout(),this.isShown()&&this._element.classList.remove(ho),super.dispose()}isShown(){return this._element.classList.contains(ho)}_maybeScheduleHide(){this._config.autohide&&(this._hasMouseInteraction||this._hasKeyboardInteraction||(this._timeout=setTimeout(()=>{this.hide()},this._config.delay)))}_onInteraction(t,e){switch(t.type){case"mouseover":case"mouseout":this._hasMouseInteraction=e;break;case"focusin":case"focusout":this._hasKeyboardInteraction=e}if(e)return void this._clearTimeout();const i=t.relatedTarget;this._element===i||this._element.contains(i)||this._maybeScheduleHide()}_setListeners(){P.on(this._element,eo,t=>this._onInteraction(t,!0)),P.on(this._element,io,t=>this._onInteraction(t,!1)),P.on(this._element,no,t=>this._onInteraction(t,!0)),P.on(this._element,so,t=>this._onInteraction(t,!1))}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}static jQueryInterface(t){return this.each(function(){const e=mo.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t](this)}})}}return q(mo),g(mo),{Alert:X,Button:U,Carousel:St,Collapse:qt,Dropdown:Qi,Modal:Ln,Offcanvas:Qn,Popover:vs,ScrollSpy:Ss,Tab:Zs,Toast:mo,Tooltip:ps}}); +//# sourceMappingURL=bootstrap.bundle.min.js.map diff --git a/example/src/WebEid.AspNetCore.Example/wwwroot/js/device-utils.js b/example/src/WebEid.AspNetCore.Example/wwwroot/js/device-utils.js new file mode 100644 index 00000000..c29318cc --- /dev/null +++ b/example/src/WebEid.AspNetCore.Example/wwwroot/js/device-utils.js @@ -0,0 +1,7 @@ +export function isMobileDevice() { + const hasTouch = 'ontouchstart' in window || navigator.maxTouchPoints > 0; + const isSmallScreen = window.innerWidth <= 768; + const userAgentRegex = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i; + + return (hasTouch && isSmallScreen) || userAgentRegex.test(navigator.userAgent); +} diff --git a/example/src/WebEid.AspNetCore.Example/wwwroot/js/payload.js b/example/src/WebEid.AspNetCore.Example/wwwroot/js/payload.js new file mode 100644 index 00000000..4035e6c2 --- /dev/null +++ b/example/src/WebEid.AspNetCore.Example/wwwroot/js/payload.js @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2020-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. + */ + +export function parsePayload(context) { + const fragment = window.location.hash.slice(1); + + if (!fragment) { + throw new Error(`Missing ${context} response payload`); + } + + let payload; + try { + payload = JSON.parse(atob(fragment)); + } catch (e) { + console.error(e); + throw new Error(`Failed to parse the ${context} response`); + } + + if (payload.error) { + const error = new Error(payload.message ?? `${context} failed`); + error.code = payload.code; + throw error; + } + + return payload; +} diff --git a/src/.editorconfig b/src/.editorconfig index 5402a9fc..eadfeba8 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 3444a495..6c2c515e 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 b2b22b43..d9ecd2de 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 b3f2a812..61b24c64 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 57fac5f2..ddbf2da9 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 38d8c22d..b1ca25d8 100644 --- a/src/WebEid.Security.Tests/TestUtils/AbstractTestWithValidator.cs +++ b/src/WebEid.Security.Tests/TestUtils/AbstractTestWithValidator.cs @@ -1,5 +1,5 @@ -/* - * Copyright © 2020-2024 Estonian Information System Authority +/* + * Copyright © 2020-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 @@ -34,6 +34,17 @@ public abstract class AbstractTestWithValidator "\"appVersion\":\"https://web-eid.eu/web-eid-app/releases/2.0.0+0\"," + "\"signature\":\"tbMTrZD4CKUj6atjNCHZruIeyPFAEJk2htziQ1t08BSTyA5wKKqmNmzsJ7562hWQ6+tJd6nlidHGE5jVVJRKmPtNv3f9gbT2b7RXcD4t5Pjn8eUCBCA4IX99Af32Z5ln\"," + "\"format\":\"web-eid:1\"}"; + + protected const string ValidV11AuthTokenStr = "{\"algorithm\":\"ES384\"," + + "\"unverifiedCertificate\":\"MIIEBDCCA2WgAwIBAgIQY5OGshxoPMFg+Wfc0gFEaTAKBggqhkjOPQQDBDBgMQswCQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRhDA5OVFJFRS0xMDc0NzAxMzEbMBkGA1UEAwwSVEVTVCBvZiBFU1RFSUQyMDE4MB4XDTIxMDcyMjEyNDMwOFoXDTI2MDcwOTIxNTk1OVowfzELMAkGA1UEBhMCRUUxKjAoBgNVBAMMIUrDlUVPUkcsSkFBSy1LUklTVEpBTiwzODAwMTA4NTcxODEQMA4GA1UEBAwHSsOVRU9SRzEWMBQGA1UEKgwNSkFBSy1LUklTVEpBTjEaMBgGA1UEBRMRUE5PRUUtMzgwMDEwODU3MTgwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQmwEKsJTjaMHSaZj19hb9EJaJlwbKc5VFzmlGMFSJVk4dDy+eUxa5KOA7tWXqzcmhh5SYdv+MxcaQKlKWLMa36pfgv20FpEDb03GCtLqjLTRZ7649PugAQ5EmAqIic29CjggHDMIIBvzAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIDiDBHBgNVHSAEQDA+MDIGCysGAQQBg5EhAQIBMCMwIQYIKwYBBQUHAgEWFWh0dHBzOi8vd3d3LnNrLmVlL0NQUzAIBgYEAI96AQIwHwYDVR0RBBgwFoEUMzgwMDEwODU3MThAZWVzdGkuZWUwHQYDVR0OBBYEFPlp/ceABC52itoqppEmbf71TJz6MGEGCCsGAQUFBwEDBFUwUzBRBgYEAI5GAQUwRzBFFj9odHRwczovL3NrLmVlL2VuL3JlcG9zaXRvcnkvY29uZGl0aW9ucy1mb3ItdXNlLW9mLWNlcnRpZmljYXRlcy8TAkVOMCAGA1UdJQEB/wQWMBQGCCsGAQUFBwMCBggrBgEFBQcDBDAfBgNVHSMEGDAWgBTAhJkpxE6fOwI09pnhClYACCk+ezBzBggrBgEFBQcBAQRnMGUwLAYIKwYBBQUHMAGGIGh0dHA6Ly9haWEuZGVtby5zay5lZS9lc3RlaWQyMDE4MDUGCCsGAQUFBzAChilodHRwOi8vYy5zay5lZS9UZXN0X29mX0VTVEVJRDIwMTguZGVyLmNydDAKBggqhkjOPQQDBAOBjAAwgYgCQgDCAgybz0u3W+tGI+AX+PiI5CrE9ptEHO5eezR1Jo4j7iGaO0i39xTGUB+NSC7P6AQbyE/ywqJjA1a62jTLcS9GHAJCARxN4NO4eVdWU3zVohCXm8WN3DWA7XUcn9TZiLGQ29P4xfQZOXJi/z4PNRRsR4plvSNB3dfyBvZn31HhC7my8woi\"," + + "\"unverifiedSigningCertificates\":[{" + + "\"certificate\":\"MIID6zCCA02gAwIBAgIQT7j6zk6pmVRcyspLo5SqejAKBggqhkjOPQQDBDBgMQswCQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRhDA5OVFJFRS0xMDc0NzAxMzEbMBkGA1UEAwwSVEVTVCBvZiBFU1RFSUQyMDE4MB4XDTE5MDUwMjEwNDUzMVoXDTI5MDUwMjEwNDUzMVowfzELMAkGA1UEBhMCRUUxFjAUBgNVBCoMDUpBQUstS1JJU1RKQU4xEDAOBgNVBAQMB0rDlUVPUkcxKjAoBgNVBAMMIUrDlUVPUkcsSkFBSy1LUklTVEpBTiwzODAwMTA4NTcxODEaMBgGA1UEBRMRUE5PRUUtMzgwMDEwODU3MTgwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASkwENR8GmCpEs6OshDWDfIiKvGuyNMOD2rjIQW321AnZD3oIsqD0svBMNEJJj9Dlvq/47TYDObIa12KAU5IuOBfJs2lrFdSXZjaM+a5TWT3O2JTM36YDH2GcMe/eisepejggGrMIIBpzAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIGQDBIBgNVHSAEQTA/MDIGCysGAQQBg5EhAQIBMCMwIQYIKwYBBQUHAgEWFWh0dHBzOi8vd3d3LnNrLmVlL0NQUzAJBgcEAIvsQAECMB0GA1UdDgQWBBTVX3s48Spy/Es2TcXgkRvwUn2YcjCBigYIKwYBBQUHAQMEfjB8MAgGBgQAjkYBATAIBgYEAI5GAQQwEwYGBACORgEGMAkGBwQAjkYBBgEwUQYGBACORgEFMEcwRRY/aHR0cHM6Ly9zay5lZS9lbi9yZXBvc2l0b3J5L2NvbmRpdGlvbnMtZm9yLXVzZS1vZi1jZXJ0aWZpY2F0ZXMvEwJFTjAfBgNVHSMEGDAWgBTAhJkpxE6fOwI09pnhClYACCk+ezBzBggrBgEFBQcBAQRnMGUwLAYIKwYBBQUHMAGGIGh0dHA6Ly9haWEuZGVtby5zay5lZS9lc3RlaWQyMDE4MDUGCCsGAQUFBzAChilodHRwOi8vYy5zay5lZS9UZXN0X29mX0VTVEVJRDIwMTguZGVyLmNydDAKBggqhkjOPQQDBAOBiwAwgYcCQgGBr+Jbo1GeqgWdIwgMo7SA29AP38JxNm2HWq2Qb+kIHpusAK574Co1K5D4+Mk7/ITTuXQaET5WphHoN7tdAciTaQJBAn0zBigYyVPYSTO68HM6hmlwTwi/KlJDdXW/2NsMjSqofFFJXpGvpxk2CTqSRCjcavxLPnkasTbNROYSJcmM8Xc=\"," + + "\"supportedSignatureAlgorithms\":[{\"cryptoAlgorithm\":\"RSA\",\"hashFunction\":\"SHA-256\",\"paddingScheme\":\"PKCS1.5\"}]" + + "}]," + + "\"appVersion\":\"https://web-eid.eu/web-eid-mobile-app/releases/v1.0.0\"," + + "\"signature\":\"0Ov7ME6pTY1K2GXMj8Wxov/o2fGIMEds8OMY5dKdkB0nrqQX7fG1E5mnsbvyHpMDecMUH6Yg+p1HXdgB/lLqOcFZjt/OVXPjAAApC5d1YgRYATDcxsR1zqQwiNcHdmWn\"," + + "\"format\":\"web-eid:1.1\"}"; + public const string ValidChallengeNonce = "12345678123456781234567812345678912356789123"; private DateTimeProvider dateTimeProvider; @@ -44,15 +55,15 @@ public abstract class AbstractTestWithValidator [SetUp] protected void SetUp() { - this.Validator = AuthTokenValidators.GetAuthTokenValidator(); - this.ValidAuthToken = this.Validator.Parse(ValidAuthTokenStr); - this.dateTimeProvider = DateTimeProvider.OverrideUtcNow(new DateTime(2021, 3, 1)); + Validator = AuthTokenValidators.GetAuthTokenValidator(); + ValidAuthToken = Validator.Parse(ValidAuthTokenStr); + dateTimeProvider = DateTimeProvider.OverrideUtcNow(new DateTime(2021, 8, 1)); } [TearDown] - public void TearDown() => this.dateTimeProvider?.Dispose(); + public void TearDown() => dateTimeProvider?.Dispose(); protected WebEidAuthToken ReplaceTokenField(string token, string field, string value) => - this.Validator.Parse(token.Replace(field, value)); + Validator.Parse(token.Replace(field, value)); } } diff --git a/src/WebEid.Security.Tests/TestUtils/ByteArrayExtensions.cs b/src/WebEid.Security.Tests/TestUtils/ByteArrayExtensions.cs index 6711485c..90dbb94a 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 3c10b71a..38e0ab10 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 9ae998a5..3713f9b1 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 983ca3b2..f13cb3ee 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 d9783054..e406acbd 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 1784e844..5c1c82bf 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 caaa10cc..048ba681 100644 --- a/src/WebEid.Security.Tests/Validator/AuthTokenAlgorithmTest.cs +++ b/src/WebEid.Security.Tests/Validator/AuthTokenAlgorithmTest.cs @@ -1,5 +1,5 @@ -/* - * Copyright © 2020-2024 Estonian Information System Authority +/* + * Copyright © 2020-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 @@ -21,34 +21,79 @@ */ namespace WebEid.Security.Tests.Validator { + using Exceptions; using NUnit.Framework; - using WebEid.Security.Exceptions; - using WebEid.Security.Tests.TestUtils; + using TestUtils; public class AuthTokenAlgorithmTest : AbstractTestWithValidator { [Test] public void WhenAlgorithmNoneThenValidationFailsAsync() { - var authToken = this.ReplaceTokenField(ValidAuthTokenStr, "ES384", "NONE"); - Assert.ThrowsAsync(() => this.Validator.Validate(authToken, ValidChallengeNonce)) + var authToken = ReplaceTokenField(ValidAuthTokenStr, "ES384", "NONE"); + Assert.ThrowsAsync(() => Validator.Validate(authToken, ValidChallengeNonce)) .WithMessage("Unsupported signature algorithm"); } [Test] public void WhenAlgorithmEmptyThenParsingFailsAsync() { - var authToken = this.ReplaceTokenField(ValidAuthTokenStr, "ES384", ""); - Assert.ThrowsAsync(() => this.Validator.Validate(authToken, ValidChallengeNonce)) + var authToken = ReplaceTokenField(ValidAuthTokenStr, "ES384", ""); + Assert.ThrowsAsync(() => Validator.Validate(authToken, ValidChallengeNonce)) .WithMessage("'algorithm' is null or empty"); } [Test] - public void WhenAlgorithmInvalidThenParsingFailsAsync() + public void WhenAlgorithmInvalidThenParsingFails() { - var authToken = this.ReplaceTokenField(ValidAuthTokenStr, "ES384", "\u0000\t\ninvalid"); - Assert.ThrowsAsync(() => this.Validator.Validate(authToken, ValidChallengeNonce)) + var authToken = ValidAuthTokenStr.Replace("\"ES384\"", "\"\u0000\t\ninvalid\""); + + Assert.Throws(() => Validator.Parse(authToken)) + .WithMessage("Error parsing Web eID authentication token"); + } + + [Test] + public void WhenV11TokenMissingSupportedAlgorithmsThenValidationFailsAsync() + { + var token = ReplaceTokenField( + ValidV11AuthTokenStr, + "\"supportedSignatureAlgorithms\":[{\"cryptoAlgorithm\":\"RSA\",\"hashFunction\":\"SHA-256\",\"paddingScheme\":\"PKCS1.5\"}]", + ""); + + Assert.ThrowsAsync(() => Validator.Validate(token, ValidChallengeNonce)) + .WithMessage("'supportedSignatureAlgorithms' field is missing"); + } + + [Test] + public void WhenV11TokenHasInvalidCryptoAlgorithmThenValidationFailsAsync() + { + var token = ReplaceTokenField(ValidV11AuthTokenStr, "\"cryptoAlgorithm\":\"RSA\"", "\"cryptoAlgorithm\":\"INVALID\""); + Assert.ThrowsAsync(() => Validator.Validate(token, ValidChallengeNonce)) + .WithMessage("Unsupported signature algorithm"); + } + + [Test] + public void WhenV11TokenHasInvalidHashFunctionThenValidationFailsAsync() + { + var token = ReplaceTokenField(ValidV11AuthTokenStr, "\"hashFunction\":\"SHA-256\"", "\"hashFunction\":\"NOT_A_HASH\""); + Assert.ThrowsAsync(() => Validator.Validate(token, ValidChallengeNonce)) + .WithMessage("Unsupported signature algorithm"); + } + + [Test] + public void WhenV11TokenHasInvalidPaddingSchemeThenValidationFailsAsync() + { + var token = ReplaceTokenField(ValidV11AuthTokenStr, "\"paddingScheme\":\"PKCS1.5\"", "\"paddingScheme\":\"BAD_PADDING\""); + Assert.ThrowsAsync(() => Validator.Validate(token, ValidChallengeNonce)) .WithMessage("Unsupported signature algorithm"); } + + [Test] + public void WhenV11TokenHasEmptySupportedAlgorithmsThenValidationFailsAsync() + { + 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 a35a7574..d29af2b0 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 31937ea8..611f250c 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 3e58fc67..0f7a2613 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 68620677..d64865ba 100644 --- a/src/WebEid.Security.Tests/Validator/AuthTokenSignatureValidatorTests.cs +++ b/src/WebEid.Security.Tests/Validator/AuthTokenSignatureValidatorTests.cs @@ -1,5 +1,5 @@ -/* - * Copyright © 2020-2024 Estonian Information System Authority +/* + * Copyright © 2020-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 @@ -36,11 +36,29 @@ public class AuthTokenSignatureValidatorTests : AbstractTestWithValidator "\"issuerApp\":\"https://web-eid.eu/web-eid-app/releases/2.0.0+0\"," + "\"signature\":\"xsjXsQvVYXWcdV0YPhxLthJxtf0//R8p9WFFlYJGRARrl1ruyoAUwl0xeHgeZOKeJtwiCYCNWJzCG3VM3ydgt92bKhhk1u0JXIPVqvOkmDY72OCN4q73Y8iGSPVTgjk93TgquHlodf7YcqZNhutwNNf3oldHEWJD5zmkdwdpBFXgeOwTAdFwGljDQZbHr3h1Dr+apUDuloS0WuIzUuu8YXN2b8lh8FCTlF0G0DEjhHd/MGx8dbe3UTLHmD7K9DXv4zLJs6EF9i2v/C10SIBQDkPBSVPqMxCDPECjbEPi2+ds94eU7ThOhOQlFFtJ4KjQNTUa2crSixH7cYZF2rNNmA==\"," + "\"format\":\"web-eid:1.0\"}"; + private const string ValidV11Rs256AuthToken = "{\"algorithm\":\"RS256\"," + + "\"unverifiedCertificate\":\"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\"," + + "\"unverifiedSigningCertificates\":[{" + + "\"certificate\":\"MIID6zCCA02gAwIBAgIQT7j6zk6pmVRcyspLo5SqejAKBggqhkjOPQQDBDBgMQswCQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRhDA5OVFJFRS0xMDc0NzAxMzEbMBkGA1UEAwwSVEVTVCBvZiBFU1RFSUQyMDE4MB4XDTE5MDUwMjEwNDUzMVoXDTI5MDUwMjEwNDUzMVowfzELMAkGA1UEBhMCRUUxFjAUBgNVBCoMDUpBQUstS1JJU1RKQU4xEDAOBgNVBAQMB0rDlUVPUkcxKjAoBgNVBAMMIUrDlUVPUkcsSkFBSy1LUklTVEpBTiwzODAwMTA4NTcxODEaMBgGA1UEBRMRUE5PRUUtMzgwMDEwODU3MTgwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASkwENR8GmCpEs6OshDWDfIiKvGuyNMOD2rjIQW321AnZD3oIsqD0svBMNEJJj9Dlvq/47TYDObIa12KAU5IuOBfJs2lrFdSXZjaM+a5TWT3O2JTM36YDH2GcMe/eisepejggGrMIIBpzAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIGQDBIBgNVHSAEQTA/MDIGCysGAQQBg5EhAQIBMCMwIQYIKwYBBQUHAgEWFWh0dHBzOi8vd3d3LnNrLmVlL0NQUzAJBgcEAIvsQAECMB0GA1UdDgQWBBTVX3s48Spy/Es2TcXgkRvwUn2YcjCBigYIKwYBBQUHAQMEfjB8MAgGBgQAjkYBATAIBgYEAI5GAQQwEwYGBACORgEGMAkGBwQAjkYBBgEwUQYGBACORgEFMEcwRRY/aHR0cHM6Ly9zay5lZS9lbi9yZXBvc2l0b3J5L2NvbmRpdGlvbnMtZm9yLXVzZS1vZi1jZXJ0aWZpY2F0ZXMvEwJFTjAfBgNVHSMEGDAWgBTAhJkpxE6fOwI09pnhClYACCk+ezBzBggrBgEFBQcBAQRnMGUwLAYIKwYBBQUHMAGGIGh0dHA6Ly9haWEuZGVtby5zay5lZS9lc3RlaWQyMDE4MDUGCCsGAQUFBzAChilodHRwOi8vYy5zay5lZS9UZXN0X29mX0VTVEVJRDIwMTguZGVyLmNydDAKBggqhkjOPQQDBAOBiwAwgYcCQgGBr+Jbo1GeqgWdIwgMo7SA29AP38JxNm2HWq2Qb+kIHpusAK574Co1K5D4+Mk7/ITTuXQaET5WphHoN7tdAciTaQJBAn0zBigYyVPYSTO68HM6hmlwTwi/KlJDdXW/2NsMjSqofFFJXpGvpxk2CTqSRCjcavxLPnkasTbNROYSJcmM8Xc=\"," + + "\"supportedSignatureAlgorithms\":[{\"cryptoAlgorithm\":\"RSA\",\"hashFunction\":\"SHA-256\",\"paddingScheme\":\"PKCS1.5\"}]" + + "}]," + + "\"appVersion\":\"https://web-eid.eu/web-eid-mobile-app/releases/v1.0.0\"," + + "\"signature\":\"xsjXsQvVYXWcdV0YPhxLthJxtf0//R8p9WFFlYJGRARrl1ruyoAUwl0xeHgeZOKeJtwiCYCNWJzCG3VM3ydgt92bKhhk1u0JXIPVqvOkmDY72OCN4q73Y8iGSPVTgjk93TgquHlodf7YcqZNhutwNNf3oldHEWJD5zmkdwdpBFXgeOwTAdFwGljDQZbHr3h1Dr+apUDuloS0WuIzUuu8YXN2b8lh8FCTlF0G0DEjhHd/MGx8dbe3UTLHmD7K9DXv4zLJs6EF9i2v/C10SIBQDkPBSVPqMxCDPECjbEPi2+ds94eU7ThOhOQlFFtJ4KjQNTUa2crSixH7cYZF2rNNmA==\"," + + "\"format\":\"web-eid:1.1\"}"; private const string AuthTokenWithWrongCert = "{\"algorithm\":\"ES384\"," + "\"unverifiedCertificate\":\"MIIEBDCCA2WgAwIBAgIQH9NeN14jo0ReaircrN2YvDAKBggqhkjOPQQDBDBgMQswCQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRhDA5OVFJFRS0xMDc0NzAxMzEbMBkGA1UEAwwSVEVTVCBvZiBFU1RFSUQyMDE4MB4XDTIwMDMxMjEyMjgxMloXDTI1MDMxMjIxNTk1OVowfzELMAkGA1UEBhMCRUUxKjAoBgNVBAMMIUrDlUVPUkcsSkFBSy1LUklTVEpBTiwzODAwMTA4NTcxODEQMA4GA1UEBAwHSsOVRU9SRzEWMBQGA1UEKgwNSkFBSy1LUklTVEpBTjEaMBgGA1UEBRMRUE5PRUUtMzgwMDEwODU3MTgwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAARVeP+9l3b1mm3fMHPeCFLbD7esXI8lDc+soWCBoMnZGo3d2Rg/mzKCIWJtw+JhcN7RwFFH9cwZ8Gni4C3QFYBIIJ2GdjX2KQfEkDvRsnKw6ZZmJQ+HC4ZFew3r8gauhfejggHDMIIBvzAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIDiDBHBgNVHSAEQDA+MDIGCysGAQQBg5EhAQIBMCMwIQYIKwYBBQUHAgEWFWh0dHBzOi8vd3d3LnNrLmVlL0NQUzAIBgYEAI96AQIwHwYDVR0RBBgwFoEUMzgwMDEwODU3MThAZWVzdGkuZWUwHQYDVR0OBBYEFOfk7lPOq6rb9IbFZF1q97kJ4s2iMGEGCCsGAQUFBwEDBFUwUzBRBgYEAI5GAQUwRzBFFj9odHRwczovL3NrLmVlL2VuL3JlcG9zaXRvcnkvY29uZGl0aW9ucy1mb3ItdXNlLW9mLWNlcnRpZmljYXRlcy8TAkVOMCAGA1UdJQEB/wQWMBQGCCsGAQUFBwMCBggrBgEFBQcDBDAfBgNVHSMEGDAWgBTAhJkpxE6fOwI09pnhClYACCk+ezBzBggrBgEFBQcBAQRnMGUwLAYIKwYBBQUHMAGGIGh0dHA6Ly9haWEuZGVtby5zay5lZS9lc3RlaWQyMDE4MDUGCCsGAQUFBzAChilodHRwOi8vYy5zay5lZS9UZXN0X29mX0VTVEVJRDIwMTguZGVyLmNydDAKBggqhkjOPQQDBAOBjAAwgYgCQgEQRbzFOSHIcmIEKczhN8xuteYgN2zEXZSJdP0q1iH1RR2AzZ8Ddz6SKRn/bZSzjcd4b7h3AyOEQr2hcidYkxT7sAJCAMPtOUryqp2WbTEUoOpbWrKqp8GjaAiVpBGDn/Xdu5M2Z6dvwZHnFGgRrZXtyUbcAgRW7MQJ0s/9GCVro3iqUzNN\"," + "\"appVersion\":\"https://web-eid.eu/web-eid-app/releases/2.0.0+0\"," + "\"signature\":\"arx164xRiwhIQDINe0J+ZxJWZFOQTx0PBtOaWaxAe7gofEIHRIbV1w0sOCYBJnvmvMem9hU4nc2+iJx2x8poYck4Z6eI3GwtiksIec3XQ9ZIk1n/XchXnmPn3GYV+HzJ\"," + "\"format\":\"web-eid:1.0\"}"; + private const string V11AuthTokenWithWrongCert = "{\"algorithm\":\"ES384\"," + + "\"unverifiedCertificate\":\"MIIEDDCCA26gAwIBAgIQM8UTDe8zVKtcysotoMgBlzAKBggqhkjOPQQDBDBgMQswCQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRhDA5OVFJFRS0xMDc0NzAxMzEbMBkGA1UEAwwSVEVTVCBvZiBFU1RFSUQyMDE4MB4XDTE5MDUwMjEwNDUwMVoXDTI5MDUwMjEwNDUwMVowfzELMAkGA1UEBhMCRUUxFjAUBgNVBCoMDUpBQUstS1JJU1RKQU4xEDAOBgNVBAQMB0rDlUVPUkcxKjAoBgNVBAMMIUrDlUVPUkcsSkFBSy1LUklTVEpBTiwzODAwMTA4NTcxODEaMBgGA1UEBRMRUE5PRUUtMzgwMDEwODU3MTgwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAARh/M6SBatkyMHjTmRgIF1MTqZpVIfqHZD6MrQUHdlykVSLNBmloFjoXbQbSe0l+sgKUPSZWb48IGPC7Mrudt5vLvnKy31qZ5a+2Ceg87NrVzdNCWF2oQrwXw63HieIBMmjggHMMIIByDAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIDiDBHBgNVHSAEQDA+MDIGCysGAQQBg5EhAQIBMCMwIQYIKwYBBQUHAgEWFWh0dHBzOi8vd3d3LnNrLmVlL0NQUzAIBgYEAI96AQIwKAYDVR0RBCEwH4EdamFhay1rcmlzdGphbi5qb2VvcmdAZWVzdGkuZWUwHQYDVR0OBBYEFOSW4XJH0oDJAh2nEqFGhrlF9zXQMGEGCCsGAQUFBwEDBFUwUzBRBgYEAI5GAQUwRzBFFj9odHRwczovL3NrLmVlL2VuL3JlcG9zaXRvcnkvY29uZGl0aW9ucy1mb3ItdXNlLW9mLWNlcnRpZmljYXRlcy8TAkVOMCAGA1UdJQEB/wQWMBQGCCsGAQUFBwMCBggrBgEFBQcDBDAfBgNVHSMEGDAWgBTAhJkpxE6fOwI09pnhClYACCk+ezBzBggrBgEFBQcBAQRnMGUwLAYIKwYBBQUHMAGGIGh0dHA6Ly9haWEuZGVtby5zay5lZS9lc3RlaWQyMDE4MDUGCCsGAQUFBzAChilodHRwOi8vYy5zay5lZS9UZXN0X29mX0VTVEVJRDIwMTguZGVyLmNydDAKBggqhkjOPQQDBAOBiwAwgYcCQQjG3AnPzJdtmoaNI59T8vcjsNjVB5XLfUXiBguizma9I6dFqhHiTtfqo2aWpd+dcL8iz/3Dn03C0ruPLnJVt24lAkIB8M6KO+RcVJqXz8KXMUGstjK+1iIE0hd+2JtNmIJcqgNT7sj8f4NZfsix5JuUpY1j4msWG3k0h79U2bWcR8NQZdU=\"," + + "\"unverifiedSigningCertificates\":[{" + + "\"certificate\":\"MIID6zCCA02gAwIBAgIQT7j6zk6pmVRcyspLo5SqejAKBggqhkjOPQQDBDBgMQswCQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRhDA5OVFJFRS0xMDc0NzAxMzEbMBkGA1UEAwwSVEVTVCBvZiBFU1RFSUQyMDE4MB4XDTE5MDUwMjEwNDUzMVoXDTI5MDUwMjEwNDUzMVowfzELMAkGA1UEBhMCRUUxFjAUBgNVBCoMDUpBQUstS1JJU1RKQU4xEDAOBgNVBAQMB0rDlUVPUkcxKjAoBgNVBAMMIUrDlUVPUkcsSkFBSy1LUklTVEpBTiwzODAwMTA4NTcxODEaMBgGA1UEBRMRUE5PRUUtMzgwMDEwODU3MTgwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASkwENR8GmCpEs6OshDWDfIiKvGuyNMOD2rjIQW321AnZD3oIsqD0svBMNEJJj9Dlvq/47TYDObIa12KAU5IuOBfJs2lrFdSXZjaM+a5TWT3O2JTM36YDH2GcMe/eisepejggGrMIIBpzAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIGQDBIBgNVHSAEQTA/MDIGCysGAQQBg5EhAQIBMCMwIQYIKwYBBQUHAgEWFWh0dHBzOi8vd3d3LnNrLmVlL0NQUzAJBgcEAIvsQAECMB0GA1UdDgQWBBTVX3s48Spy/Es2TcXgkRvwUn2YcjCBigYIKwYBBQUHAQMEfjB8MAgGBgQAjkYBATAIBgYEAI5GAQQwEwYGBACORgEGMAkGBwQAjkYBBgEwUQYGBACORgEFMEcwRRY/aHR0cHM6Ly9zay5lZS9lbi9yZXBvc2l0b3J5L2NvbmRpdGlvbnMtZm9yLXVzZS1vZi1jZXJ0aWZpY2F0ZXMvEwJFTjAfBgNVHSMEGDAWgBTAhJkpxE6fOwI09pnhClYACCk+ezBzBggrBgEFBQcBAQRnMGUwLAYIKwYBBQUHMAGGIGh0dHA6Ly9haWEuZGVtby5zay5lZS9lc3RlaWQyMDE4MDUGCCsGAQUFBzAChilodHRwOi8vYy5zay5lZS9UZXN0X29mX0VTVEVJRDIwMTguZGVyLmNydDAKBggqhkjOPQQDBAOBiwAwgYcCQgGBr+Jbo1GeqgWdIwgMo7SA29AP38JxNm2HWq2Qb+kIHpusAK574Co1K5D4+Mk7/ITTuXQaET5WphHoN7tdAciTaQJBAn0zBigYyVPYSTO68HM6hmlwTwi/KlJDdXW/2NsMjSqofFFJXpGvpxk2CTqSRCjcavxLPnkasTbNROYSJcmM8Xc=\"," + + "\"supportedSignatureAlgorithms\":[{\"cryptoAlgorithm\":\"RSA\",\"hashFunction\":\"SHA-256\",\"paddingScheme\":\"PKCS1.5\"}]" + + "}]," + + "\"appVersion\":\"https://web-eid.eu/web-eid-mobile-app/releases/v1.0.0\"," + + "\"signature\":\"arx164xRiwhIQDINe0J+ZxJWZFOQTx0PBtOaWaxAe7gofEIHRIbV1w0sOCYBJnvmvMem9hU4nc2+iJx2x8poYck4Z6eI3GwtiksIec3XQ9ZIk1n/XchXnmPn3GYV+HzJ\"," + + "\"format\":\"web-eid:1.1\"}"; private static readonly TextInfo Ti = new CultureInfo("et-EE", false).TextInfo; @@ -48,7 +66,7 @@ public class AuthTokenSignatureValidatorTests : AbstractTestWithValidator public void WhenValidES384SignatureThenValidationSucceedsAsync() => Assert.DoesNotThrowAsync(async () => { - var certificate = await this.Validator.Validate(this.ValidAuthToken, ValidChallengeNonce); + var certificate = await Validator.Validate(ValidAuthToken, ValidChallengeNonce); Assert.That(certificate, Is.Not.Null); Assert.That(certificate.GetSubjectCn(), Is.EqualTo("JÕEORG,JAAK-KRISTJAN,38001085718")); @@ -62,7 +80,7 @@ public void WhenValidES384SignatureThenValidationSucceedsAsync() => public void WhenValidRS256SignatureThenValidationSucceeds() => Assert.DoesNotThrow(() => { - var authToken = this.Validator.Parse(ValidRs256AuthToken); + var authToken = Validator.Parse(ValidRs256AuthToken); var publicKey = X509CertificateExtensions.ParseCertificate(authToken.UnverifiedCertificate, "unverifiedCertificate") .GetAsymmetricPublicKey() @@ -74,24 +92,93 @@ public void WhenValidRS256SignatureThenValidationSucceeds() => [Test] public void WhenValidTokenAndWrongChallengeNonceThenValidationFailsAsync() => Assert.ThrowsAsync(() => - this.Validator.Validate(this.ValidAuthToken, "12345678123456781234567812345678912356789124")); + Validator.Validate(ValidAuthToken, "12345678123456781234567812345678912356789124")); [Test] public void WhenValidTokenAndWrongOriginThenValidationFailsAsync() { var authTokenValidator = AuthTokenValidators.GetAuthTokenValidator("https://invalid.org"); Assert.ThrowsAsync(() => - authTokenValidator.Validate(this.ValidAuthToken, ValidChallengeNonce)); + authTokenValidator.Validate(ValidAuthToken, ValidChallengeNonce)); } [Test] public void WhenTokenWithWrongCertThenValidationFailsAsync() { - var authTokenWithWrongCert = this.Validator.Parse(AuthTokenWithWrongCert); + var authTokenWithWrongCert = Validator.Parse(AuthTokenWithWrongCert); Assert.ThrowsAsync(() => - this.Validator.Validate(authTokenWithWrongCert, ValidChallengeNonce)); + Validator.Validate(authTokenWithWrongCert, ValidChallengeNonce)); + } + + [Test] + public void WhenValidV11TokenAndNonceThenValidationSucceedsAsync() => + Assert.DoesNotThrowAsync(async () => + { + var token = Validator.Parse(ValidV11AuthTokenStr); + + var certificate = await Validator.Validate(token, ValidChallengeNonce); + + Assert.That(certificate, Is.Not.Null); + Assert.That(certificate.GetSubjectCn(), Is.EqualTo("JÕEORG,JAAK-KRISTJAN,38001085718")); + Assert.That(ToTitleCase(certificate.GetSubjectGivenName()), Is.EqualTo("Jaak-Kristjan")); + Assert.That(ToTitleCase(certificate.GetSubjectSurname()), Is.EqualTo("Jõeorg")); + Assert.That(certificate.GetSubjectIdCode(), Is.EqualTo("PNOEE-38001085718")); + Assert.That(certificate.GetSubjectCountryCode(), Is.EqualTo("EE")); + }); + + [Test] + public void WhenV11TokenWithWrongChallengeNonceThenValidationFailsAsync() + { + var token = Validator.Parse(ValidV11AuthTokenStr); + + var ex = Assert.ThrowsAsync(() => + Validator.Validate(token, "12345678123456781234567812345678912356789124"))!; + + Assert.That(ex.Message, Does.Contain("signature")); + } + + [Test] + public void WhenV11TokenWithWrongOriginThenValidationFailsAsync() + { + var validatorWithWrongOrigin = + AuthTokenValidators.GetAuthTokenValidator("https://wrong-origin.com"); + + var token = validatorWithWrongOrigin.Parse(ValidV11AuthTokenStr); + + var ex = Assert.ThrowsAsync(() => + validatorWithWrongOrigin.Validate(token, ValidChallengeNonce))!; + + Assert.That(ex.Message, Does.Contain("Token signature validation has failed")); + } + + [Test] + public void WhenV11TokenWithWrongCertThenValidationFailsAsync() + { + var token = Validator.Parse(V11AuthTokenWithWrongCert); + + var ex = Assert.ThrowsAsync(() => + Validator.Validate(token, ValidChallengeNonce))!; + + Assert.That(ex.Message, Does.Contain("signature")); } + [Test] + public void WhenValidRS256V11SignatureThenValidationSucceeds() => + Assert.DoesNotThrow(() => + { + var authToken = Validator.Parse(ValidV11Rs256AuthToken); + var publicKey = + X509CertificateExtensions + .ParseCertificate(authToken.UnverifiedCertificate, "unverifiedCertificate") + .GetAsymmetricPublicKey() + .CreateSecurityKeyWithoutCachingSignatureProviders(); + + var signatureValidator = new AuthTokenSignatureValidator(new Uri("https://ria.ee")); + + signatureValidator.Validate("RS256", publicKey, authToken.Signature, ValidChallengeNonce); + }); + + private static string ToTitleCase(string s) => Ti.ToTitleCase(Ti.ToLower(s)); } } diff --git a/src/WebEid.Security.Tests/Validator/AuthTokenStructureTest.cs b/src/WebEid.Security.Tests/Validator/AuthTokenStructureTest.cs index 9887dfb5..26da3857 100644 --- a/src/WebEid.Security.Tests/Validator/AuthTokenStructureTest.cs +++ b/src/WebEid.Security.Tests/Validator/AuthTokenStructureTest.cs @@ -1,5 +1,5 @@ -/* - * Copyright © 2020-2024 Estonian Information System Authority +/* + * Copyright © 2020-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 @@ -29,30 +29,30 @@ 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, "")) - .WithMessage("Only token format version 'web-eid:1' is currently supported"); + 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 e44842b4..88220468 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/AuthTokenValidatonOcspTest.cs b/src/WebEid.Security.Tests/Validator/AuthTokenValidatonOcspTest.cs index e6efa5e6..1ff0780a 100644 --- a/src/WebEid.Security.Tests/Validator/AuthTokenValidatonOcspTest.cs +++ b/src/WebEid.Security.Tests/Validator/AuthTokenValidatonOcspTest.cs @@ -42,13 +42,12 @@ public void WhenOcspRequestTimeoutIsReachedThenValidationFails() using var _ = DateTimeProvider.OverrideUtcNow(new DateTime(2026, 02, 03)); var authTokenValidator = AuthTokenValidators .GetDefaultAuthTokenValidatorBuilder() - .WithOcspRequestTimeout(TimeSpan.FromMilliseconds(1)) + .WithOcspRequestTimeout(TimeSpan.FromMilliseconds(1)) .Build(); var exception = Assert.ThrowsAsync(() => authTokenValidator.Validate(authTokenValidator.Parse(AuthToken), ValidChallengeNonce)); Assert.That(exception.InnerException, Is.TypeOf()); Assert.That(exception.InnerException.Message, Does.Contain("The request was canceled due to the configured HttpClient.Timeout of")); - } [Test] diff --git a/src/WebEid.Security.Tests/Validator/AuthTokenValidatorBuilderTest.cs b/src/WebEid.Security.Tests/Validator/AuthTokenValidatorBuilderTest.cs index 73c3478f..30ed539d 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 00000000..f681d75d --- /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 cdd73cc7..7ed1a763 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 7200eb01..67ea18fc 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 58b9d206..2841a98c 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 7d7c47a3..b9e50efc 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 new file mode 100644 index 00000000..bd7a82ad --- /dev/null +++ b/src/WebEid.Security.Tests/Validator/VersionValidators/AuthTokenV11CertificateTest.cs @@ -0,0 +1,151 @@ +/* + * 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.VersionValidators +{ + using System; + 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 + { + private const string V11AuthTokenStr = "{\"algorithm\":\"ES384\"," + + "\"unverifiedCertificate\":\"MIIEBDCCA2WgAwIBAgIQY5OGshxoPMFg+Wfc0gFEaTAKBggqhkjOPQQDBDBgMQswCQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRhDA5OVFJFRS0xMDc0NzAxMzEbMBkGA1UEAwwSVEVTVCBvZiBFU1RFSUQyMDE4MB4XDTIxMDcyMjEyNDMwOFoXDTI2MDcwOTIxNTk1OVowfzELMAkGA1UEBhMCRUUxKjAoBgNVBAMMIUrDlUVPUkcsSkFBSy1LUklTVEpBTiwzODAwMTA4NTcxODEQMA4GA1UEBAwHSsOVRU9SRzEWMBQGA1UEKgwNSkFBSy1LUklTVEpBTjEaMBgGA1UEBRMRUE5PRUUtMzgwMDEwODU3MTgwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQmwEKsJTjaMHSaZj19hb9EJaJlwbKc5VFzmlGMFSJVk4dDy+eUxa5KOA7tWXqzcmhh5SYdv+MxcaQKlKWLMa36pfgv20FpEDb03GCtLqjLTRZ7649PugAQ5EmAqIic29CjggHDMIIBvzAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIDiDBHBgNVHSAEQDA+MDIGCysGAQQBg5EhAQIBMCMwIQYIKwYBBQUHAgEWFWh0dHBzOi8vd3d3LnNrLmVlL0NQUzAIBgYEAI96AQIwHwYDVR0RBBgwFoEUMzgwMDEwODU3MThAZWVzdGkuZWUwHQYDVR0OBBYEFPlp/ceABC52itoqppEmbf71TJz6MGEGCCsGAQUFBwEDBFUwUzBRBgYEAI5GAQUwRzBFFj9odHRwczovL3NrLmVlL2VuL3JlcG9zaXRvcnkvY29uZGl0aW9ucy1mb3ItdXNlLW9mLWNlcnRpZmljYXRlcy8TAkVOMCAGA1UdJQEB/wQWMBQGCCsGAQUFBwMCBggrBgEFBQcDBDAfBgNVHSMEGDAWgBTAhJkpxE6fOwI09pnhClYACCk+ezBzBggrBgEFBQcBAQRnMGUwLAYIKwYBBQUHMAGGIGh0dHA6Ly9haWEuZGVtby5zay5lZS9lc3RlaWQyMDE4MDUGCCsGAQUFBzAChilodHRwOi8vYy5zay5lZS9UZXN0X29mX0VTVEVJRDIwMTguZGVyLmNydDAKBggqhkjOPQQDBAOBjAAwgYgCQgDCAgybz0u3W+tGI+AX+PiI5CrE9ptEHO5eezR1Jo4j7iGaO0i39xTGUB+NSC7P6AQbyE/ywqJjA1a62jTLcS9GHAJCARxN4NO4eVdWU3zVohCXm8WN3DWA7XUcn9TZiLGQ29P4xfQZOXJi/z4PNRRsR4plvSNB3dfyBvZn31HhC7my8woi\"," + + "\"unverifiedSigningCertificates\":[{" + + "\"certificate\":\"X5C\"," + + "\"supportedSignatureAlgorithms\":[{\"cryptoAlgorithm\":\"RSA\",\"hashFunction\":\"SHA-256\",\"paddingScheme\":\"PKCS1.5\"}]" + + "}]," + + "\"appVersion\":\"https://web-eid.eu/web-eid-mobile-app/releases/v1.0.0\"," + + "\"signature\":\"0Ov7ME6pTY1K2GXMj8Wxov/o2fGIMEds8OMY5dKdkB0nrqQX7fG1E5mnsbvyHpMDecMUH6Yg+p1HXdgB/lLqOcFZjt/OVXPjAAApC5d1YgRYATDcxsR1zqQwiNcHdmWn\"," + + "\"format\":\"web-eid:1.1\"}"; + + private const string DifferentCertBase64 = "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"; + private const string MissingKeyUsageCert = "MIICxjCCAa6gAwIBAgIJANTbd26vS6fmMA0GCSqGSIb3DQEBBQUAMBUxEzARBgNVBAMTCndlYi1laWQuZXUwHhcNMjAwOTI0MTIyNDMzWhcNMzAwOTIyMTIyNDMzWjAVMRMwEQYDVQQDEwp3ZWItZWlkLmV1MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAza5qBFu5fvs47rx3o9yzBVfIxHjMotID8ppkwWVen/uFxlqsRVi+XnWkggW+K8X45inAnBAVi1rIw7GQNdacSHglyvQfwM64AallmD0+K+QgbqxcO9fvRvlAeISENBc2bGgqTIytPEON5ZmazzbOZjqY3M1QcPlPZOeUm6M9ZcZFhsxpiB4gwZUic9tnCz9eujd6k6DzNVfSRaJcpGA5hJ9aKH4vXS3x7anewna+USEXkRb4Il5zSlZR0i1yrVA1YNOxCG/+GgWvXfvXwdQ0z9BpGwNEyc0mRDNx+umaTukz9t+7/qTcB2JLTuiwM9Gqg5sDDnzPlcZSa7GnIU0MLQIDAQABoxkwFzAVBgNVHREEDjAMggp3ZWItZWlkLmV1MA0GCSqGSIb3DQEBBQUAA4IBAQAYGkBhTlet47uw3JYunYo6dj4nGWSGV4x6LYjCp5QlAmGd28HpC1RFB3ba+inwW8SP69kEOcB0sJQAZ/tV90oCATNsy/Whg/TtiHISL2pr1dyBoKDRWbgTp8jjzcp2Bj9nL14aqpj1t4K1lcoYETX41yVmyyJu6VFs80M5T3yikm2giAhszjChnjyoT2kaEKoua9EUK9SS27pVltgbbvtmeTp3ZPHtBfiDOATL6E03RZ5WfMLRefI796a+RcznnudzQHhMSwcjLpMDgIWpUU4OU7RiwrU+S3MrvgzCjkWh2MGu/OGLB+d3JZoW+eCvigoshmAsbJCMLbh4N78BCPqk"; + + private AuthTokenValidationConfiguration config; + + [SetUp] + public void Init() + { + config = new AuthTokenValidationConfiguration + { + SiteOrigin = new Uri("https://example.com"), + IsUserCertificateRevocationCheckWithOcspEnabled = false + }; + config.TrustedCaCertificates.Clear(); + } + + [Test] + public void WhenValidV11TokenThenValidationSucceeds() + { + var authTokenValidator = new AuthTokenValidator(config, null); + var token = authTokenValidator.Parse(ValidV11AuthTokenStr); + Assert.DoesNotThrowAsync(() => Validator.Validate(token, ValidChallengeNonce)); + } + + [Test] + public void WhenV11SigningCertificateMissingThenValidationFails() + { + var json = JsonNode.Parse(V11AuthTokenStr)!.AsObject(); + json.Remove("unverifiedSigningCertificates"); + + var token = DeserializeToken(json.ToJsonString()); + + var ex = Assert.ThrowsAsync(() => + Validator.Validate(token, ValidChallengeNonce)); + + Assert.That(ex.Message, Does.Contain("unverifiedSigningCertificates")); + } + + [Test] + public void WhenV11SigningCertificateIsNotBase64ThenValidationFails() + { + var json = JsonNode.Parse(V11AuthTokenStr)!.AsObject(); + json["unverifiedSigningCertificates"]!.AsArray()[0]!["certificate"] = "This is not a certificate"; + + var token = DeserializeToken(json.ToJsonString()); + + Assert.ThrowsAsync(() => + Validator.Validate(token, ValidChallengeNonce)); + } + + [Test] + public void WhenV11SigningCertificateIsNotACertificateThenValidationFails() + { + var json = JsonNode.Parse(V11AuthTokenStr)!.AsObject(); + json["unverifiedSigningCertificates"]!.AsArray()[0]!["certificate"] = "VGhpcyBpcyBub3QgYSBjZXJ0aWZpY2F0ZQ"; + + var token = DeserializeToken(json.ToJsonString()); + + Assert.ThrowsAsync(() => + Validator.Validate(token, ValidChallengeNonce)); + } + + [Test] + public void WhenV11SigningCertificateSubjectDoesNotMatchThenValidationFails() + { + var json = JsonNode.Parse(V11AuthTokenStr)!.AsObject(); + json["unverifiedSigningCertificates"]!.AsArray()[0]!["certificate"] = DifferentCertBase64; + + var token = DeserializeToken(json.ToJsonString()); + + Assert.ThrowsAsync(() => + Validator.Validate(token, ValidChallengeNonce)); + } + + [Test] + public void WhenV11SigningCertificateNotIssuedBySameAuthorityThenValidationFails() + { + var json = JsonNode.Parse(V11AuthTokenStr)!.AsObject(); + json["unverifiedSigningCertificates"]!.AsArray()[0]!["certificate"] = ValidChallengeNonce; + + var token = DeserializeToken(json.ToJsonString()); + + Assert.ThrowsAsync(() => + Validator.Validate(token, ValidChallengeNonce)); + } + + [Test] + public void WhenV11SigningCertificateKeyUsageInvalidThenValidationFails() + { + var json = JsonNode.Parse(V11AuthTokenStr)!.AsObject(); + json["unverifiedSigningCertificates"]!.AsArray()[0]!["certificate"] = MissingKeyUsageCert; + + var token = DeserializeToken(json.ToJsonString()); + + Assert.ThrowsAsync(() => + Validator.Validate(token, ValidChallengeNonce)); + } + + private static readonly JsonSerializerOptions DefaultJsonSerializerOptions = new() + { + PropertyNameCaseInsensitive = true + }; + + private static WebEidAuthToken DeserializeToken(string json) => + JsonSerializer.Deserialize(json, DefaultJsonSerializerOptions)!; + } +} diff --git a/src/WebEid.Security.Tests/Validator/VersionValidators/AuthTokenVersion11ValidatorTest.cs b/src/WebEid.Security.Tests/Validator/VersionValidators/AuthTokenVersion11ValidatorTest.cs new file mode 100644 index 00000000..bb539af0 --- /dev/null +++ b/src/WebEid.Security.Tests/Validator/VersionValidators/AuthTokenVersion11ValidatorTest.cs @@ -0,0 +1,138 @@ +/* + * 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.VersionValidators +{ + using System; + using System.Security.Cryptography.X509Certificates; + using Exceptions; + 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 : AbstractTestWithValidator + { + private SubjectCertificateValidatorBatch subjectValidators; + private AuthTokenSignatureValidator signatureValidator; + private AuthTokenValidationConfiguration configuration; + private IOcspClient ocspClient; + private OcspServiceProvider ocspServiceProvider; + private AuthTokenVersion11Validator v11Validator; + + [SetUp] + public new void SetUp() + { + base.SetUp(); + + subjectValidators = SubjectCertificateValidatorBatch.CreateFrom(); + + configuration = new AuthTokenValidationConfiguration + { + SiteOrigin = new Uri("https://example.com") + }; +#pragma warning disable SYSLIB0026 + configuration.TrustedCaCertificates.Add(new X509Certificate2()); +#pragma warning disable SYSLIB0026 + + signatureValidator = new Mock( + new Uri("https://ria.ee") + ).Object; + + ocspClient = new Mock().Object; + + var aiaConfig = new AiaOcspServiceConfiguration( + [], + [] + ); + + ocspServiceProvider = new OcspServiceProvider( + designatedOcspServiceConfiguration: null, + aiaOcspServiceConfiguration: aiaConfig + ); + + v11Validator = new AuthTokenVersion11Validator( + subjectValidators, + signatureValidator, + configuration, + ocspClient, + ocspServiceProvider, + Mock.Of() + ); + } + + [TestCase("web-eid:1.1")] + [TestCase("web-eid:1.2")] + [TestCase("web-eid:1.10")] + [TestCase("web-eid:1.999")] + public void WhenFormatIsV11OrHigherMinorVersionThenSupportsReturnsTrue(string format) => + Assert.That(v11Validator.Supports(format), Is.True); + + [TestCase(null)] + [TestCase("")] + [TestCase("web-eid:1")] + [TestCase("web-eid:1.0")] + [TestCase("web-eid:1.")] + [TestCase("web-eid:1.0TEST")] + [TestCase("web-eid:1.00")] + [TestCase("web-eid:1.1.0")] + [TestCase("web-eid:2")] + [TestCase("web-eid:0.9")] + [TestCase("webauthn:1.1")] + public void WhenFormatIsNullEmptyMinorVersion0NonCanonicalOrMalformedThenSupportsReturnsFalse(string format) => + Assert.That(v11Validator.Supports(format), Is.False); + + [Test] + public void WhenUnverifiedSigningCertificatesMissingThenValidationFails() + { + var token = Validator.Parse(ValidV11AuthTokenStr); + token.UnverifiedSigningCertificates = null; + + 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 = Validator.Parse(ValidV11AuthTokenStr); + token.UnverifiedSigningCertificates[0].SupportedSignatureAlgorithms = null; + + var ex = Assert.ThrowsAsync(() => Validator.Validate(token, ValidChallengeNonce)); + Assert.That(ex!.Message, Is.EqualTo("'supportedSignatureAlgorithms' field is missing")); + } + + [Test] + public void WhenSigningCertificateChainValidationSucceedsThenValidationSucceeds() + { + var token = Validator.Parse(ValidV11AuthTokenStr); + + Assert.DoesNotThrowAsync(() => + Validator.Validate(token, ValidChallengeNonce)); + } + } +} diff --git a/src/WebEid.Security.Tests/Validator/VersionValidators/AuthTokenVersion1ValidatorTest.cs b/src/WebEid.Security.Tests/Validator/VersionValidators/AuthTokenVersion1ValidatorTest.cs new file mode 100644 index 00000000..1e4bdf51 --- /dev/null +++ b/src/WebEid.Security.Tests/Validator/VersionValidators/AuthTokenVersion1ValidatorTest.cs @@ -0,0 +1,145 @@ +/* + * 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.VersionValidators +{ + using System; + using System.Security.Cryptography.X509Certificates; + using AuthToken; + using Exceptions; + using Microsoft.Extensions.Logging; + using Moq; + using NUnit.Framework; + using WebEid.Security.Validator; + using WebEid.Security.Validator.CertValidators; + using WebEid.Security.Validator.Ocsp; + using WebEid.Security.Validator.Ocsp.Service; + using WebEid.Security.Validator.VersionValidators; + + public class AuthTokenVersion1ValidatorTest + { + private SubjectCertificateValidatorBatch subjectValidators; + private AuthTokenSignatureValidator signatureValidator; + private AuthTokenValidationConfiguration configuration; + private IOcspClient ocspClient; + private OcspServiceProvider ocspServiceProvider; + private AuthTokenVersion1Validator validator; + + [SetUp] + public void SetUp() + { + subjectValidators = SubjectCertificateValidatorBatch.CreateFrom(); + + configuration = new AuthTokenValidationConfiguration + { + SiteOrigin = new Uri("https://example.com") + }; +#pragma warning disable SYSLIB0026 + configuration.TrustedCaCertificates.Add(new X509Certificate2()); +#pragma warning disable SYSLIB0026 + + signatureValidator = new Mock( + new Uri("https://ria.ee") + ).Object; + + ocspClient = new Mock().Object; + + var aiaConfig = new AiaOcspServiceConfiguration( + [], + [] + ); + + ocspServiceProvider = new OcspServiceProvider( + designatedOcspServiceConfiguration: null, + aiaOcspServiceConfiguration: aiaConfig + ); + + validator = new AuthTokenVersion1Validator( + subjectValidators, + signatureValidator, + configuration, + ocspClient, + ocspServiceProvider, + Mock.Of() + ); + } + + [TestCase("web-eid:1")] + [TestCase("web-eid:1.0")] + [TestCase("web-eid:1.1")] + [TestCase("web-eid:1.2")] + [TestCase("web-eid:1.10")] + [TestCase("web-eid:1.999")] + public void WhenFormatIsValidMajorV1FormatThenSupportsReturnsTrue(string format) => + Assert.That(validator.Supports(format), Is.True); + + [TestCase(null)] + [TestCase("")] + [TestCase("web-eid")] + [TestCase("web-eid:1.")] + [TestCase("web-eid:1.0TEST")] + [TestCase("web-eid:1.00")] + [TestCase("web-eid:1.000")] + [TestCase("web-eid:01")] + [TestCase("web-eid:1.1.0")] + [TestCase("web-eid:0.9")] + [TestCase("web-eid:2")] + [TestCase("webauthn:1")] + public void WhenFormatIsNullEmptyMalformedOrNotV1ThenSupportsReturnsFalse(string format) => + Assert.That(validator.Supports(format), Is.False); + + [Test] + public void WhenUnverifiedCertificateMissingThenValidationFails() + { + var token = new WebEidAuthToken + { + Format = "web-eid:1", + UnverifiedSigningCertificates = null, + UnverifiedCertificate = null + }; + + var ex = Assert.ThrowsAsync(() => + validator.Validate(token, "nonce")); + + Assert.That(ex.Message, Does.Contain("'unverifiedCertificate' field is missing")); + } + + [Test] + public void WhenUnverifiedSigningCertificatesPresentForV1ThenValidationFails() + { + var token = new WebEidAuthToken + { + Format = "web-eid:1", + UnverifiedSigningCertificates = + [ + new() + ] + }; + + var ex = Assert.ThrowsAsync(() => + validator.Validate(token, "nonce")); + + Assert.That( + ex.Message, + Does.Contain("'unverifiedSigningCertificates' field is not allowed for format 'web-eid:1'")); + } + } +} diff --git a/src/WebEid.Security.Tests/Validator/VersionValidators/AuthTokenVersionTest.cs b/src/WebEid.Security.Tests/Validator/VersionValidators/AuthTokenVersionTest.cs new file mode 100644 index 00000000..8669fe9a --- /dev/null +++ b/src/WebEid.Security.Tests/Validator/VersionValidators/AuthTokenVersionTest.cs @@ -0,0 +1,78 @@ +/* + * 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.VersionValidators +{ + using NUnit.Framework; + using WebEid.Security.Validator.VersionValidators; + + public class AuthTokenVersionTest + { + [TestCase("web-eid:1", 1, 0, true)] + [TestCase("web-eid:1.0", 1, 0, true)] + [TestCase("web-eid:1.1", 1, 0, true)] + [TestCase("web-eid:1.1", 1, 1, true)] + [TestCase("web-eid:1.999", 1, 1, true)] + [TestCase("web-eid:2.0", 2, 0, true)] + [TestCase("web-eid:2.3", 2, 1, true)] + [TestCase("web-eid:1.0", 1, 1, false)] + [TestCase("web-eid:1", 1, 1, false)] + [TestCase("web-eid:2", 1, 0, false)] + [TestCase("web-eid:1.5", 2, 0, false)] + [TestCase("web-eid:1.00", 1, 0, false)] + [TestCase("web-eid:1.000", 1, 0, false)] + [TestCase("web-eid:01", 1, 0, false)] + [TestCase("web-eid:1.", 1, 0, false)] + [TestCase("web-eid:1.1.0", 1, 0, false)] + [TestCase("web-eid:0.9", 1, 0, false)] + [TestCase("webauthn:1", 1, 0, false)] + public void WhenFormatMatchesRequiredMajorAndAtLeastRequiredMinorThenSupportsReturnsExpected( + string format, int requiredMajorVersion, int requiredMinorVersion, bool expected) => + Assert.That( + AuthTokenVersion.Supports(format, requiredMajorVersion, requiredMinorVersion), + Is.EqualTo(expected)); + + [Test] + public void WhenFormatIsNullThenSupportsReturnsFalse() => + Assert.That(AuthTokenVersion.Supports(null, 1, 0), Is.False); + + [TestCase("web-eid:1", 1, 0, true)] + [TestCase("web-eid:1.0", 1, 0, true)] + [TestCase("web-eid:1.1", 1, 1, true)] + [TestCase("web-eid:2.0", 2, 0, true)] + [TestCase("web-eid:1.1", 1, 0, false)] + [TestCase("web-eid:1.2", 1, 1, false)] + [TestCase("web-eid:1.0", 1, 1, false)] + [TestCase("web-eid:1", 2, 0, false)] + [TestCase("web-eid:1.00", 1, 0, false)] + [TestCase("web-eid:01", 1, 0, false)] + [TestCase("webauthn:1", 1, 0, false)] + public void WhenFormatMatchesRequiredMajorAndExactMinorThenSupportsExactlyReturnsExpected( + string format, int requiredMajorVersion, int requiredMinorVersion, bool expected) => + Assert.That( + AuthTokenVersion.SupportsExactly(format, requiredMajorVersion, requiredMinorVersion), + Is.EqualTo(expected)); + + [Test] + public void WhenFormatIsNullThenSupportsExactlyReturnsFalse() => + Assert.That(AuthTokenVersion.SupportsExactly(null, 1, 0), Is.False); + } +} diff --git a/src/WebEid.Security.Tests/Validator/VersionValidators/AuthTokenVersionValidatorFactoryTest.cs b/src/WebEid.Security.Tests/Validator/VersionValidators/AuthTokenVersionValidatorFactoryTest.cs new file mode 100644 index 00000000..c83cb4c5 --- /dev/null +++ b/src/WebEid.Security.Tests/Validator/VersionValidators/AuthTokenVersionValidatorFactoryTest.cs @@ -0,0 +1,107 @@ +/* + * 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.VersionValidators +{ + using Exceptions; + using Moq; + using NUnit.Framework; + using WebEid.Security.Validator.VersionValidators; + + public class AuthTokenVersionValidatorFactoryTest + { + [Test] + public void WhenValidatorSupportsFormatThenSupportsReturnsTrue() + { + var v11 = new Mock(); + v11.Setup(v => v.Supports("web-eid:1.1")).Returns(true); + + var factory = new AuthTokenVersionValidatorFactory( + [v11.Object] + ); + + Assert.That(factory.Supports("web-eid:1.1"), Is.True); + } + + [Test] + public void WhenValidatorDoesNotSupportFormatThenSupportsReturnsFalse() + { + var v11 = new Mock(); + v11.Setup(v => v.Supports("web-eid:1.1")).Returns(false); + + var factory = new AuthTokenVersionValidatorFactory( + [v11.Object] + ); + + Assert.That(factory.Supports("web-eid:2"), Is.False); + } + + [TestCase("web-eid:0.9")] + [TestCase("web-eid:2")] + [TestCase("foo")] + [TestCase("1")] + [TestCase("web-eid")] + public void WhenUnsupportedFormatThenGetValidatorForThrows(string format) + { + var factory = new AuthTokenVersionValidatorFactory([]); + + Assert.Throws(() => + factory.GetValidatorFor(format) + ); + } + + [Test] + public void WhenMultipleValidatorsAndFirstIsV11ThenGetValidatorForReturnsV11() + { + var v11 = new Mock(); + v11.Setup(v => v.Supports("web-eid:1.1")).Returns(true); + + var v1 = new Mock(); + v1.Setup(v => v.Supports("web-eid:1")).Returns(true); + + var factory = new AuthTokenVersionValidatorFactory( + [v11.Object, v1.Object] + ); + + var chosen = factory.GetValidatorFor("web-eid:1.1"); + + Assert.That(chosen, Is.SameAs(v11.Object)); + } + + [Test] + public void WhenFormatIsBaseV1ThenGetValidatorForReturnsV1() + { + var v11 = new Mock(); + v11.Setup(v => v.Supports("web-eid:1.1")).Returns(true); + + var v1 = new Mock(); + v1.Setup(v => v.Supports("web-eid:1")).Returns(true); + + var factory = new AuthTokenVersionValidatorFactory( + [v11.Object, v1.Object] + ); + + var chosen = factory.GetValidatorFor("web-eid:1"); + + Assert.That(chosen, Is.SameAs(v1.Object)); + } + } +} diff --git a/src/WebEid.Security.Tests/WebEid.Security.Tests.csproj b/src/WebEid.Security.Tests/WebEid.Security.Tests.csproj index 9e5db479..40d88189 100644 --- a/src/WebEid.Security.Tests/WebEid.Security.Tests.csproj +++ b/src/WebEid.Security.Tests/WebEid.Security.Tests.csproj @@ -1,7 +1,7 @@ - net8.0 + net10.0 false diff --git a/src/WebEid.Security/AuthToken/SupportedSignatureAlgorithm.cs b/src/WebEid.Security/AuthToken/SupportedSignatureAlgorithm.cs new file mode 100644 index 00000000..a176dc5a --- /dev/null +++ b/src/WebEid.Security/AuthToken/SupportedSignatureAlgorithm.cs @@ -0,0 +1,43 @@ +/* + * 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.AuthToken +{ + /// + /// Represents a signature algorithm supported by the authentication token, + /// including its cryptographic algorithm, hash function, and padding scheme. + /// + public class SupportedSignatureAlgorithm + { + /// + /// The cryptographic algorithm, e.g. "RSA" or "ECDSA". + /// + public string CryptoAlgorithm { get; set; } + /// + /// The hash function, e.g. "SHA-256". + /// + public string HashFunction { get; set; } + /// + /// The padding scheme, e.g. "PKCS1" or "PSS". + /// + public string PaddingScheme { get; set; } + } +} diff --git a/src/WebEid.Security/AuthToken/UnverifiedSigningCertificate.cs b/src/WebEid.Security/AuthToken/UnverifiedSigningCertificate.cs new file mode 100644 index 00000000..63d2c8fc --- /dev/null +++ b/src/WebEid.Security/AuthToken/UnverifiedSigningCertificate.cs @@ -0,0 +1,40 @@ +/* + * 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.AuthToken +{ + using System.Collections.Generic; + + /// + /// Unverified signing certificate and its supported signature algorithms. + /// + public class UnverifiedSigningCertificate + { + /// + /// The base64-encoded signing certificate (DER). + /// + public string Certificate { get; set; } + /// + /// List of supported signature algorithms from the card. + /// + public List SupportedSignatureAlgorithms { get; set; } + } +} diff --git a/src/WebEid.Security/AuthToken/WebEidAuthToken.cs b/src/WebEid.Security/AuthToken/WebEidAuthToken.cs index ecae20bf..75fcaf4c 100644 --- a/src/WebEid.Security/AuthToken/WebEidAuthToken.cs +++ b/src/WebEid.Security/AuthToken/WebEidAuthToken.cs @@ -1,5 +1,5 @@ /* - * Copyright © 2020-2024 Estonian Information System Authority + * Copyright © 2020-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 @@ -21,6 +21,8 @@ */ namespace WebEid.Security.AuthToken { + using System.Collections.Generic; + /// /// The Web eID authentication token /// @@ -44,5 +46,9 @@ public class WebEidAuthToken /// The base64-encoded DER-encoded authentication certificate of the eID user. ///
public string UnverifiedCertificate { get; set; } + /// + /// The base64-encoded signing certificates (DER). + /// + public List UnverifiedSigningCertificates { get; set; } } } diff --git a/src/WebEid.Security/Challenge/ChallengeNonce.cs b/src/WebEid.Security/Challenge/ChallengeNonce.cs index 7848b40c..96088250 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 1af948ee..3a941833 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 d3096bb8..26bed82b 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 246c6ec4..e9384539 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 7fb766e5..cc22cfe2 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 3dfe2e7f..51d1050c 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 1af1ae45..18621822 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 686d69f3..cb449eb7 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 new file mode 100644 index 00000000..203d6e5a --- /dev/null +++ b/src/WebEid.Security/Util/X509Certificate2Extensions.cs @@ -0,0 +1,41 @@ +/* + * 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.Util +{ + using System.Security.Cryptography.X509Certificates; + + /// + /// Provides extension methods for working with instances. + /// + public static class X509Certificate2Extensions + { + /// + /// Converts a into an equivalent + /// BouncyCastle instance. + /// + public static Org.BouncyCastle.X509.X509Certificate ToBouncyCastle(this X509Certificate2 certificate) + { + var parser = new Org.BouncyCastle.X509.X509CertificateParser(); + return parser.ReadCertificate(certificate.RawData); + } + } +} diff --git a/src/WebEid.Security/Util/X509CertificateExtensions.cs b/src/WebEid.Security/Util/X509CertificateExtensions.cs index b5fe1f9f..bf49bbd7 100644 --- a/src/WebEid.Security/Util/X509CertificateExtensions.cs +++ b/src/WebEid.Security/Util/X509CertificateExtensions.cs @@ -90,7 +90,8 @@ public static X509Certificate2 ValidateIsValidAndSignedByTrustedCa(this X509Cert RevocationFlag = X509RevocationFlag.ExcludeRoot, VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority, VerificationTime = DateTimeProvider.UtcNow, - UrlRetrievalTimeout = TimeSpan.Zero + UrlRetrievalTimeout = TimeSpan.Zero, + DisableCertificateDownloads = true } }; @@ -129,7 +130,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 +148,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 737e4eb9..411da724 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 8decf17e..0045c662 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 70341d91..7ba20cc5 100644 --- a/src/WebEid.Security/Validator/AuthTokenValidator.cs +++ b/src/WebEid.Security/Validator/AuthTokenValidator.cs @@ -1,5 +1,5 @@ /* - * Copyright © 2020-2024 Estonian Information System Authority + * Copyright © 2020-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 @@ -22,19 +22,14 @@ namespace WebEid.Security.Validator { using System; - using System.Linq; - using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; + using System.Text.Json; using System.Threading.Tasks; + using AuthToken; using Exceptions; using Microsoft.Extensions.Logging; - using Microsoft.IdentityModel.Tokens; - using Newtonsoft.Json; using Ocsp; - using Ocsp.Service; - using AuthToken; - using Util; - using WebEid.Security.Validator.CertValidators; + using VersionValidators; /// /// Represents the configuration for validating authentication tokens (JWTs) in the Web eID system. @@ -42,12 +37,9 @@ namespace WebEid.Security.Validator public sealed class AuthTokenValidator : IAuthTokenValidator { private readonly ILogger logger; - private readonly AuthTokenValidationConfiguration configuration; - private readonly AuthTokenSignatureValidator authTokenSignatureValidator; - private readonly SubjectCertificateValidatorBatch simpleSubjectCertificateValidators; - private readonly OcspClient ocspClient; - private readonly OcspServiceProvider ocspServiceProvider; + private readonly AuthTokenVersionValidatorFactory versionValidatorFactory; + // Use human-readable meaningful names for token length limits. private const int TokenMinLength = 100; private const int TokenMaxLength = 10000; @@ -55,31 +47,15 @@ public sealed class AuthTokenValidator : IAuthTokenValidator /// Initializes a new instance of the class. /// /// The configuration for token validation. + /// The OCSP client used for certificate revocation checking. /// The logger for capturing log messages (optional). - public AuthTokenValidator(AuthTokenValidationConfiguration configuration, ILogger logger = null) + public AuthTokenValidator(AuthTokenValidationConfiguration configuration, IOcspClient ocspClient, ILogger logger = null) { this.logger = logger; - this.configuration = configuration.Copy(); - this.authTokenSignatureValidator = new AuthTokenSignatureValidator(configuration.SiteOrigin); - - this.simpleSubjectCertificateValidators = SubjectCertificateValidatorBatch.CreateFrom( - new SubjectCertificatePurposeValidator(this.logger), - new SubjectCertificatePolicyValidator(configuration.DisallowedSubjectCertificatePolicies - .Select(policy => new Oid(policy)) - .ToArray(), this.logger) - ); - - if (configuration.IsUserCertificateRevocationCheckWithOcspEnabled) - { - this.ocspClient = new OcspClient(configuration.OcspRequestTimeout, this.logger); - this.ocspServiceProvider = - new OcspServiceProvider(configuration.DesignatedOcspServiceConfiguration, - new AiaOcspServiceConfiguration(configuration.NonceDisabledOcspUrls, - configuration.TrustedCaCertificates)); - } + var validationConfig = configuration.Copy(); - // Turn off caching of signature providers - CryptoProviderFactory.DefaultCacheSignatureProviders = false; + versionValidatorFactory = + AuthTokenVersionValidatorFactory.Create(validationConfig, ocspClient, this.logger); } /// @@ -89,7 +65,7 @@ public AuthTokenValidator(AuthTokenValidationConfiguration configuration, ILogge /// A parsed instance. public WebEidAuthToken Parse(string authToken) { - this.logger?.LogInformation("Starting token parsing"); + logger?.LogInformation("Starting token parsing"); try { @@ -99,7 +75,7 @@ public WebEidAuthToken Parse(string authToken) catch (Exception ex) { // Generally "log and rethrow" is an anti-pattern, but it fits with the surrounding logging style. - this.logger?.LogWarning(ex, "Token parsing was interrupted:"); + logger?.LogWarning(ex, "Token parsing was interrupted:"); throw; } } @@ -112,16 +88,18 @@ public WebEidAuthToken Parse(string authToken) /// A task representing the validation result with the user certificate. public Task Validate(WebEidAuthToken authToken, string currentChallengeNonce) { - this.logger?.LogInformation("Starting token validation"); + ArgumentNullException.ThrowIfNull(authToken, nameof(authToken)); + logger?.LogInformation("Starting token validation"); try { - return this.ValidateToken(authToken, currentChallengeNonce); + var validator = versionValidatorFactory.GetValidatorFor(authToken.Format); + return validator.Validate(authToken, currentChallengeNonce); } catch (Exception ex) { // Generally "log and rethrow" is an anti-pattern, but it fits with the surrounding logging style. - this.logger?.LogWarning(ex, "Token validation was interrupted:"); + logger?.LogWarning(ex, "Token validation was interrupted:"); throw; } } @@ -138,81 +116,23 @@ private static void ValidateTokenLength(string authToken) } } + private static readonly JsonSerializerOptions SerializerOptions = new() + { + PropertyNameCaseInsensitive = true, + AllowTrailingCommas = true, + }; + private static WebEidAuthToken ParseToken(string authToken) { try { - var token = JsonConvert.DeserializeObject(authToken); - if (token == null) - { - throw new AuthTokenParseException("Web eID authentication token deserialization failed"); - } - return token; + return JsonSerializer.Deserialize(authToken, SerializerOptions) + ?? throw new AuthTokenParseException("Web eID authentication token is null"); } catch (JsonException ex) { throw new AuthTokenParseException("Error parsing Web eID authentication token", ex); } } - - private async Task ValidateToken(WebEidAuthToken token, string currentChallengeNonce) - { - if (token.Format == null || !token.Format.StartsWith(IAuthTokenValidator.CURRENT_TOKEN_FORMAT_VERSION)) - { - throw new AuthTokenParseException($"Only token format version '{IAuthTokenValidator.CURRENT_TOKEN_FORMAT_VERSION}' is currently supported"); - } - if (string.IsNullOrEmpty(token.UnverifiedCertificate)) - { - throw new AuthTokenParseException("'unverifiedCertificate' field is missing, null or empty"); - } - - var subjectCertificate = X509CertificateExtensions.ParseCertificate(token.UnverifiedCertificate, "unverifiedCertificate"); - - await this.simpleSubjectCertificateValidators.ExecuteFor(subjectCertificate); - await this.GetCertTrustValidators().ExecuteFor(subjectCertificate); - - try - { - var publicKey = subjectCertificate - .GetAsymmetricPublicKey() - .CreateSecurityKeyWithoutCachingSignatureProviders(); - - // It is guaranteed that if the signature verification succeeds, then the origin and challenge - // have been implicitly and correctly verified without the need to implement any additional checks. - this.authTokenSignatureValidator.Validate(token.Algorithm, - publicKey, - token.Signature, - currentChallengeNonce); - - return subjectCertificate; - - } - catch (Exception ex) when (!(ex is AuthTokenException)) - { - throw new AuthTokenSignatureValidationException(ex); - } - } - - /// - /// Creates the certificate trust validator batch. - /// As SubjectCertificateTrustedValidator has mutable state that SubjectCertificateNotRevokedValidator depends on, - /// they cannot be reused/cached in an instance variable in a multi-threaded environment. Hence they are - /// re-created for each validation run for thread safety. - /// - /// certificate trust validator batch - private SubjectCertificateValidatorBatch GetCertTrustValidators() - { - var certTrustedValidator = - new SubjectCertificateTrustedValidator(this.configuration.TrustedCaCertificates, this.logger); - - return SubjectCertificateValidatorBatch.CreateFrom(certTrustedValidator) - .AddOptional(this.configuration.IsUserCertificateRevocationCheckWithOcspEnabled, - new SubjectCertificateNotRevokedValidator(certTrustedValidator, - this.ocspClient, - this.ocspServiceProvider, - this.configuration.AllowedOcspResponseTimeSkew, - this.configuration.MaxOcspResponseThisUpdateAge, - this.logger)); - } } } diff --git a/src/WebEid.Security/Validator/AuthTokenValidatorBuilder.cs b/src/WebEid.Security/Validator/AuthTokenValidatorBuilder.cs index 106b127c..8d7f5a11 100644 --- a/src/WebEid.Security/Validator/AuthTokenValidatorBuilder.cs +++ b/src/WebEid.Security/Validator/AuthTokenValidatorBuilder.cs @@ -22,25 +22,27 @@ namespace WebEid.Security.Validator { using System; + using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Security.Cryptography.X509Certificates; using Microsoft.Extensions.Logging; + using Ocsp; using Ocsp.Service; using Util; /// /// 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 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 ILogger logger = logger; + private IOcspClient ocspClient; + private readonly AuthTokenValidationConfiguration configuration = new(); /// /// Sets the expected site origin, i.e. the domain that the application is running on. @@ -51,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; } @@ -66,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; } @@ -83,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; } @@ -98,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; } @@ -112,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; } @@ -129,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; } @@ -142,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; } @@ -155,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; } @@ -170,8 +195,19 @@ 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; + } + + /// + /// Configures a custom OCSP client for user certificate revocation checking. + /// + /// the builder instance for method chaining + public AuthTokenValidatorBuilder WithOcspClient(IOcspClient ocspClient) + { + this.ocspClient = ocspClient; + logger?.LogDebug("Custom OCSP client configured"); return this; } @@ -182,8 +218,16 @@ public AuthTokenValidatorBuilder WithDesignatedOcspServiceConfiguration(Designat /// public IAuthTokenValidator Build() { - this.configuration.Validate(); - return new AuthTokenValidator(this.configuration, this.logger); + configuration.Validate(); + + if (configuration.IsUserCertificateRevocationCheckWithOcspEnabled && ocspClient == null) + { + ocspClient = new OcspClient( + configuration.OcspRequestTimeout, + logger); + } + + return new AuthTokenValidator(configuration, ocspClient, logger); } } } diff --git a/src/WebEid.Security/Validator/CertValidators/ISubjectCertificateValidator.cs b/src/WebEid.Security/Validator/CertValidators/ISubjectCertificateValidator.cs index cc955bb9..4089eb0d 100644 --- a/src/WebEid.Security/Validator/CertValidators/ISubjectCertificateValidator.cs +++ b/src/WebEid.Security/Validator/CertValidators/ISubjectCertificateValidator.cs @@ -1,5 +1,5 @@ -/* - * Copyright © 2020-2024 Estonian Information System Authority +/* + * Copyright © 2020-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 @@ -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 334f4e84..3b442166 100644 --- a/src/WebEid.Security/Validator/CertValidators/SubjectCertificateNotRevokedValidator.cs +++ b/src/WebEid.Security/Validator/CertValidators/SubjectCertificateNotRevokedValidator.cs @@ -1,5 +1,5 @@ /* - * Copyright © 2020-2024 Estonian Information System Authority + * Copyright © 2020-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 @@ -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 04ad923b..4d73bacf 100644 --- a/src/WebEid.Security/Validator/CertValidators/SubjectCertificatePolicyValidator.cs +++ b/src/WebEid.Security/Validator/CertValidators/SubjectCertificatePolicyValidator.cs @@ -1,5 +1,5 @@ /* - * Copyright © 2020-2024 Estonian Information System Authority + * Copyright © 2020-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 @@ -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 3149042e..44cc55bd 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. /// - public 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 90e20fff..67363e4a 100644 --- a/src/WebEid.Security/Validator/CertValidators/SubjectCertificateTrustedValidator.cs +++ b/src/WebEid.Security/Validator/CertValidators/SubjectCertificateTrustedValidator.cs @@ -1,5 +1,5 @@ /* - * Copyright © 2020-2024 Estonian Information System Authority + * Copyright © 2020-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 @@ -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 0859a8e2..710f26d4 100644 --- a/src/WebEid.Security/Validator/CertValidators/SubjectCertificateValidatorBatch.cs +++ b/src/WebEid.Security/Validator/CertValidators/SubjectCertificateValidatorBatch.cs @@ -1,5 +1,5 @@ -/* - * Copyright © 2020-2024 Estonian Information System Authority +/* + * Copyright © 2020-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 @@ -21,32 +21,85 @@ */ namespace WebEid.Security.Validator.CertValidators { + using System; using System.Collections.Generic; using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; + using Microsoft.Extensions.Logging; + using Ocsp; + using WebEid.Security.Validator; + /// + /// Represents an ordered batch of subject certificate validators that are executed sequentially + /// during authentication token validation. + /// internal sealed class SubjectCertificateValidatorBatch { private readonly List validatorList; - public static SubjectCertificateValidatorBatch CreateFrom(params ISubjectCertificateValidator[] validatorList) => - new SubjectCertificateValidatorBatch(new List(validatorList)); + /// + /// Creates a new from the provided validators. + /// + internal static SubjectCertificateValidatorBatch CreateFrom(params ISubjectCertificateValidator[] validatorList) => + new([.. validatorList]); + /// + /// Executes all validators in this batch for the specified subject certificate. + /// public async Task ExecuteFor(X509Certificate2 subjectCertificate) { - foreach (var validator in this.validatorList) + foreach (var validator in validatorList) { await validator.Validate(subjectCertificate); } } - public SubjectCertificateValidatorBatch AddOptional(bool condition, ISubjectCertificateValidator optionalValidator) + /// + /// Creates the certificate trust validators batch. + /// As SubjectCertificateTrustedValidator has mutable state that SubjectCertificateNotRevokedValidator depends on, + /// they cannot be reused/cached in an instance variable in a multi-threaded environment. Hence, they are + /// re-created for each validation run for thread safety. + /// + /// @return certificate trust validator batch + /// + public static SubjectCertificateValidatorBatch ForTrustValidation( + AuthTokenValidationConfiguration configuration, + ICollection trustedCaCertificates, + IOcspClient ocspClient, + OcspServiceProvider ocspServiceProvider, + ILogger logger) { - if (condition) + if (configuration.IsUserCertificateRevocationCheckWithOcspEnabled) { - this.validatorList.Add(optionalValidator); + if (ocspClient == null) + { + throw new InvalidOperationException("OCSP revocation checking is enabled but IOcspClient is not configured"); + } + + if (ocspServiceProvider == null) + { + throw new InvalidOperationException("OCSP revocation checking is enabled but OcspServiceProvider is not configured"); + } + } + + var certTrustedValidator = new SubjectCertificateTrustedValidator(trustedCaCertificates, logger); + var batch = CreateFrom(certTrustedValidator); + + if (configuration.IsUserCertificateRevocationCheckWithOcspEnabled) + { + batch.validatorList.Add( + new SubjectCertificateNotRevokedValidator( + certTrustedValidator, + ocspClient, + ocspServiceProvider, + configuration.AllowedOcspResponseTimeSkew, + configuration.MaxOcspResponseThisUpdateAge, + logger + ) + ); } - return this; + + return batch; } private SubjectCertificateValidatorBatch(List validatorList) => diff --git a/src/WebEid.Security/Validator/IAuthTokenValidator.cs b/src/WebEid.Security/Validator/IAuthTokenValidator.cs index 29e10091..bb7baa50 100644 --- a/src/WebEid.Security/Validator/IAuthTokenValidator.cs +++ b/src/WebEid.Security/Validator/IAuthTokenValidator.cs @@ -1,5 +1,5 @@ /* - * Copyright © 2020-2024 Estonian Information System Authority + * Copyright © 2020-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 @@ -23,27 +23,22 @@ namespace WebEid.Security.Validator { using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; + using AuthToken; using Exceptions; using Util; - using WebEid.Security.AuthToken; /// /// Interface for validating Web eID authentication tokens. /// public interface IAuthTokenValidator { - /// - /// The current token format version - /// - const string CURRENT_TOKEN_FORMAT_VERSION = "web-eid:1"; - /// /// Parses the Web eID authentication token signed by the subject. /// /// 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 @@ -55,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 d6e6fa7b..33b30d4a 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 5287ba33..9a6c6d62 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 1edc69da..a5ebe420 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 bfa78de1..90f8bbf4 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 344eb88f..a59e6839 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 bff9f2d8..030e776e 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 9de36b5d..0c218f8f 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 f0813ac0..cea61b27 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 9996ba46..77e66796 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 f3e69e08..0b6328c4 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/AuthTokenVersion.cs b/src/WebEid.Security/Validator/VersionValidators/AuthTokenVersion.cs new file mode 100644 index 00000000..8fe9019b --- /dev/null +++ b/src/WebEid.Security/Validator/VersionValidators/AuthTokenVersion.cs @@ -0,0 +1,87 @@ +/* + * 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.Validator.VersionValidators +{ + using System.Globalization; + using System.Text.RegularExpressions; + + /// + /// Utility for matching Web eID authentication token format version strings of the form + /// web-eid:<major>[.<minor>], e.g. web-eid:1 or web-eid:1.1. + /// + internal static partial class AuthTokenVersion + { + // Matches 'web-eid:' with an optional canonical '.', where both numbers have no leading + // zeros ('0' or '[1-9]\d*') and are at most 9 digits so that they always fit in an int. Non-canonical + // spellings such as 'web-eid:1.00' or 'web-eid:01' are rejected so that ambiguous version numbers cannot + // bypass the more specific validators. + [GeneratedRegex(@"^web-eid:(0|[1-9]\d{0,8})(?:\.(0|[1-9]\d{0,8}))?$")] + private static partial Regex TokenFormatRegex(); + + /// + /// Returns whether the given token format has exactly the required major version and a minor version that + /// is greater than or equal to the required minor version. A missing minor version is treated as 0. + /// Backwards-compatible minor version changes are supported within the same major version, while an + /// incompatible major version change is not. + /// + internal static bool Supports(string format, int requiredExactMajorVersion, int requiredMinimalMinorVersion) + { + var version = Parse(format); + return version.HasValue && + version.Value.Major == requiredExactMajorVersion && + version.Value.Minor >= requiredMinimalMinorVersion; + } + + /// + /// Returns whether the given token format has exactly the required major version and exactly the required + /// minor version. A missing minor version is treated as 0. + /// + internal static bool SupportsExactly(string format, int requiredExactMajorVersion, int requiredExactMinorVersion) + { + var version = Parse(format); + return version.HasValue && + version.Value.Major == requiredExactMajorVersion && + version.Value.Minor == requiredExactMinorVersion; + } + + private static (int Major, int Minor)? Parse(string format) + { + if (format == null) + { + return null; + } + + var match = TokenFormatRegex().Match(format); + if (!match.Success) + { + return null; + } + + var major = int.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture); + var minor = match.Groups[2].Success + ? int.Parse(match.Groups[2].Value, CultureInfo.InvariantCulture) + : 0; + + return (major, minor); + } + } +} diff --git a/src/WebEid.Security/Validator/VersionValidators/AuthTokenVersion11Validator.cs b/src/WebEid.Security/Validator/VersionValidators/AuthTokenVersion11Validator.cs new file mode 100644 index 00000000..d540bdb1 --- /dev/null +++ b/src/WebEid.Security/Validator/VersionValidators/AuthTokenVersion11Validator.cs @@ -0,0 +1,295 @@ +/* + * 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.Validator.VersionValidators +{ + using System; + using System.Collections.Generic; + 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 Org.BouncyCastle.Asn1; + using Org.BouncyCastle.Asn1.X509; + using Util; + + /// + /// Validator for token formats with major version 1 and minor version 1 or higher, e.g. web-eid:1.1. + /// Extends V1 validator with additional checks for signing certificate + supported algorithms. + /// + public sealed class AuthTokenVersion11Validator : AuthTokenVersion1Validator + { + private const int SupportedMinimalMinorVersion = 1; + + private static readonly HashSet SupportedSigningCryptoAlgorithms = + new(StringComparer.OrdinalIgnoreCase) + { + "ECC", + "RSA" + }; + + private static readonly HashSet SupportedSigningPaddingSchemes = + new(StringComparer.OrdinalIgnoreCase) + { + "NONE", + "PKCS1.5", + "PSS" + }; + + private static readonly HashSet SupportedSigningHashFunctions = + new(StringComparer.OrdinalIgnoreCase) + { + "SHA-224", + "SHA-256", + "SHA-384", + "SHA-512", + "SHA3-224", + "SHA3-256", + "SHA3-384", + "SHA3-512" + }; + + private readonly AuthTokenValidationConfiguration configuration; + private readonly IOcspClient ocspClient; + private readonly OcspServiceProvider ocspServiceProvider; + private readonly ILogger logger; + + /// + /// Initializes a validator for Web eID authentication tokens with token format major version 1 + /// and minor version 1 or higher. + /// + internal AuthTokenVersion11Validator( + SubjectCertificateValidatorBatch simpleSubjectCertificateValidators, + AuthTokenSignatureValidator signatureValidator, + AuthTokenValidationConfiguration configuration, + IOcspClient ocspClient, + OcspServiceProvider ocspServiceProvider, + ILogger logger = null) + : base(simpleSubjectCertificateValidators, signatureValidator, configuration, + ocspClient, ocspServiceProvider, logger) + { + this.configuration = configuration; + this.ocspClient = ocspClient; + this.ocspServiceProvider = ocspServiceProvider; + this.logger = logger; + } + + /// + /// Determines whether this validator supports the specified token format. + /// + public override bool Supports(string format) => + AuthTokenVersion.Supports(format, SupportedExactMajorVersion, SupportedMinimalMinorVersion); + + /// + /// Validates a Web eID authentication token with token format major version 1 + /// and minor version 1 or higher, and returns the authenticated user's certificate. + /// + public override async Task Validate(WebEidAuthToken authToken, string currentChallengeNonce) + { + var subjectCertificate = await base.Validate(authToken, currentChallengeNonce); + var signingCertificates = ValidateSigningCertificates(authToken); + + foreach (var signingCertificate in signingCertificates) + { + ValidateSameSubject(subjectCertificate, signingCertificate); + ValidateSameIssuer(subjectCertificate, signingCertificate); + ValidateSigningCertificateValidity(signingCertificate); + ValidateSigningCertificateKeyUsage(signingCertificate); + await ValidateSigningCertificateChainAsync(signingCertificate); + } + + return subjectCertificate; + } + + private static void ValidateSupportedSignatureAlgorithms(UnverifiedSigningCertificate cert) + { + var algorithms = cert.SupportedSignatureAlgorithms; + + if (algorithms == null || algorithms.Count == 0) + { + throw new AuthTokenParseException("'supportedSignatureAlgorithms' field is missing"); + } + + var hasInvalid = + algorithms.Any(algorithm => + algorithm == null || + algorithm.CryptoAlgorithm == null || + algorithm.HashFunction == null || + algorithm.PaddingScheme == null || + !SupportedSigningCryptoAlgorithms.Contains(algorithm.CryptoAlgorithm) || + !SupportedSigningHashFunctions.Contains(algorithm.HashFunction) || + !SupportedSigningPaddingSchemes.Contains(algorithm.PaddingScheme)); + + if (hasInvalid) + { + throw new AuthTokenParseException("Unsupported signature algorithm"); + } + } + + private static List ValidateSigningCertificates(WebEidAuthToken token) + { + var signingCertificates = token.UnverifiedSigningCertificates; + + if (signingCertificates == null || signingCertificates.Count == 0) + { + throw new AuthTokenParseException( + $"'unverifiedSigningCertificates' field is missing, null or empty for format '{token.Format}'"); + } + + var result = new List(); + + foreach (var certificate in signingCertificates) + { + if (certificate == null || string.IsNullOrEmpty(certificate.Certificate)) + { + throw new AuthTokenParseException( + $"'unverifiedSigningCertificates' contains a null or empty entry for format '{token.Format}'"); + } + + ValidateSupportedSignatureAlgorithms(certificate); + + try + { + result.Add( + X509CertificateExtensions.ParseCertificate( + certificate.Certificate, + "unverifiedSigningCertificates")); + } + catch (Exception ex) + { + throw new AuthTokenParseException("Failed to decode signing certificate", ex); + } + } + + return result; + } + + private static void ValidateSameSubject(X509Certificate2 subjectCert, X509Certificate2 signingCert) + { + if (!SubjectsMatch(subjectCert, signingCert)) + { + throw new AuthTokenParseException( + "Signing certificate subject does not match authentication certificate subject"); + } + } + + private static void ValidateSameIssuer(X509Certificate2 subjectCert, X509Certificate2 signingCert) + { + var subjectAki = GetAuthorityKeyIdentifier(subjectCert); + var signingAki = GetAuthorityKeyIdentifier(signingCert); + + if (subjectAki.Length == 0 || + signingAki.Length == 0 || + !subjectAki.SequenceEqual(signingAki)) + { + throw new AuthTokenParseException( + "Signing certificate is not issued by the same issuing authority as the authentication certificate"); + } + } + + private static void ValidateSigningCertificateValidity(X509Certificate2 cert) + { + try + { + var bcCert = cert.ToBouncyCastle(); + bcCert.ValidateCertificateExpiry(DateTime.UtcNow, "signing_certificate"); + } + catch (Exception ex) + { + throw new AuthTokenParseException( + "Signing certificate is not valid: " + ex.Message, ex); + } + } + + private static void ValidateSigningCertificateKeyUsage(X509Certificate2 cert) + { + var keyUsage = cert.Extensions.OfType().FirstOrDefault(); + if (keyUsage == null || + !keyUsage.KeyUsages.HasFlag(X509KeyUsageFlags.NonRepudiation)) + { + throw new AuthTokenParseException( + "Signing certificate key usage extension missing or does not contain non-repudiation bit required for digital signatures"); + } + } + + private async Task ValidateSigningCertificateChainAsync(X509Certificate2 signingCertificate) + { + try + { + var trustValidators = SubjectCertificateValidatorBatch.ForTrustValidation( + configuration, + configuration.TrustedCaCertificates, + ocspClient, + ocspServiceProvider, + logger + ); + + await trustValidators.ExecuteFor(signingCertificate); + } + catch (Exception ex) + { + throw new AuthTokenParseException( + "Signing certificate chain validation failed", + ex + ); + } + } + + private static bool SubjectsMatch(X509Certificate2 subjectCert, X509Certificate2 signingCert) + { + var authRaw = subjectCert.SubjectName.RawData; + var signRaw = signingCert.SubjectName.RawData; + + var authAsn1 = Asn1Object.FromByteArray(authRaw); + var signAsn1 = Asn1Object.FromByteArray(signRaw); + + var authName = X509Name.GetInstance(authAsn1); + var signName = X509Name.GetInstance(signAsn1); + + return authName.Equivalent(signName); + } + + private static byte[] GetAuthorityKeyIdentifier(X509Certificate2 cert) + { + try + { + var akiExt = cert.Extensions["2.5.29.35"]; + if (akiExt == null) + { + return []; + } + + var akiObj = Asn1Object.FromByteArray(akiExt.RawData); + var aki = AuthorityKeyIdentifier.GetInstance(akiObj); + + return aki.GetKeyIdentifier() ?? []; + } + catch (Exception ex) + { + throw new AuthTokenParseException("Failed to parse Authority Key Identifier", ex); + } + } + } +} diff --git a/src/WebEid.Security/Validator/VersionValidators/AuthTokenVersion1Validator.cs b/src/WebEid.Security/Validator/VersionValidators/AuthTokenVersion1Validator.cs new file mode 100644 index 00000000..3e9c8728 --- /dev/null +++ b/src/WebEid.Security/Validator/VersionValidators/AuthTokenVersion1Validator.cs @@ -0,0 +1,117 @@ +/* + * 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.Validator.VersionValidators +{ + using System.Security.Cryptography.X509Certificates; + using System.Threading.Tasks; + using AuthToken; + using CertValidators; + using Exceptions; + using Microsoft.Extensions.Logging; + using Ocsp; + using Util; + + /// + /// Validator for token formats with major version 1, e.g. web-eid:1 and web-eid:1.0. + /// + public partial class AuthTokenVersion1Validator : IAuthTokenVersionValidator + { + internal const int SupportedExactMajorVersion = 1; + private const int SupportedMinimalMinorVersion = 0; + + private readonly SubjectCertificateValidatorBatch simpleSubjectCertificateValidators; + private readonly AuthTokenSignatureValidator signatureValidator; + private readonly AuthTokenValidationConfiguration configuration; + private readonly IOcspClient ocspClient; + private readonly OcspServiceProvider ocspServiceProvider; + private readonly ILogger logger; + + /// + /// Initializes a validator for Web eID authentication tokens with token format major version 1. + /// + internal AuthTokenVersion1Validator( + SubjectCertificateValidatorBatch simpleSubjectCertificateValidators, + AuthTokenSignatureValidator signatureValidator, + AuthTokenValidationConfiguration configuration, + IOcspClient ocspClient, + OcspServiceProvider ocspServiceProvider, + ILogger logger = null) + { + this.simpleSubjectCertificateValidators = simpleSubjectCertificateValidators; + this.signatureValidator = signatureValidator; + this.configuration = configuration; + this.ocspClient = ocspClient; + this.ocspServiceProvider = ocspServiceProvider; + this.logger = logger; + } + + /// + /// Determines whether this validator supports the specified token format. + /// + public virtual bool Supports(string format) => + AuthTokenVersion.Supports(format, SupportedExactMajorVersion, SupportedMinimalMinorVersion); + + /// + /// Validates a Web eID authentication token and returns the authenticated user's certificate. + /// + public virtual async Task Validate(WebEidAuthToken authToken, string currentChallengeNonce) + { + if (AuthTokenVersion.SupportsExactly(authToken.Format, SupportedExactMajorVersion, SupportedMinimalMinorVersion) && + authToken.UnverifiedSigningCertificates != null) + { + throw new AuthTokenParseException($"'unverifiedSigningCertificates' field is not allowed for format '{authToken.Format}'"); + } + + if (string.IsNullOrEmpty(authToken.UnverifiedCertificate)) + { + throw new AuthTokenParseException("'unverifiedCertificate' field is missing, null or empty"); + } + + var subjectCertificate = X509CertificateExtensions.ParseCertificate( + authToken.UnverifiedCertificate, + "unverifiedCertificate" + ); + + await simpleSubjectCertificateValidators.ExecuteFor(subjectCertificate); + + var trustValidators = SubjectCertificateValidatorBatch.ForTrustValidation( + configuration, + configuration.TrustedCaCertificates, + ocspClient, + ocspServiceProvider, + logger + ); + + await trustValidators.ExecuteFor(subjectCertificate); + + signatureValidator.Validate( + authToken.Algorithm, + subjectCertificate.GetAsymmetricPublicKey() + .CreateSecurityKeyWithoutCachingSignatureProviders(), + authToken.Signature, + currentChallengeNonce + ); + + return subjectCertificate; + } + } +} diff --git a/src/WebEid.Security/Validator/VersionValidators/AuthTokenVersionValidatorFactory.cs b/src/WebEid.Security/Validator/VersionValidators/AuthTokenVersionValidatorFactory.cs new file mode 100644 index 00000000..d71f66cc --- /dev/null +++ b/src/WebEid.Security/Validator/VersionValidators/AuthTokenVersionValidatorFactory.cs @@ -0,0 +1,123 @@ +/* + * 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.Validator.VersionValidators +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Security.Cryptography; + using CertValidators; + using Exceptions; + using Microsoft.Extensions.Logging; + using Ocsp; + using Ocsp.Service; + + /// + /// Provides a factory for selecting the correct authentication token validator + /// based on the token format version. + /// + /// + /// Creates a new instance of the class. + /// + public sealed class AuthTokenVersionValidatorFactory(IList validators) + { + private readonly IReadOnlyList validators = validators?.ToList() + ?? throw new ArgumentNullException(nameof(validators)); + + /// + /// Determines whether any registered validator supports the specified token format. + /// + public bool Supports(string format) => + validators.Any(v => v.Supports(format)); + + /// + /// Returns the validator that supports the given token format. + /// + public IAuthTokenVersionValidator GetValidatorFor(string format) + { + var validator = validators.FirstOrDefault(v => v.Supports(format)) ?? throw new AuthTokenParseException( + $"Token format version '{format}' is currently not supported"); + + return validator; + } + + /// + /// Creates an instance configured + /// with version-specific Web eID authentication token validators. + /// + public static AuthTokenVersionValidatorFactory Create( + AuthTokenValidationConfiguration configuration, + IOcspClient ocspClient, + ILogger logger = null) + { + ArgumentNullException.ThrowIfNull(configuration); + + var validationConfig = configuration.Copy(); + var trustedCaCertificates = validationConfig.TrustedCaCertificates; + + var simpleSubjectValidators = SubjectCertificateValidatorBatch.CreateFrom( + new SubjectCertificatePurposeValidator(logger), + new SubjectCertificatePolicyValidator( + [.. validationConfig.DisallowedSubjectCertificatePolicies.Select(x => new Oid(x))], + logger) + ); + + OcspServiceProvider ocspServiceProvider = null; + + if (validationConfig.IsUserCertificateRevocationCheckWithOcspEnabled && ocspClient != null) + { + ocspServiceProvider = new OcspServiceProvider( + validationConfig.DesignatedOcspServiceConfiguration, + new AiaOcspServiceConfiguration( + validationConfig.NonceDisabledOcspUrls, + trustedCaCertificates + ) + ); + } + + var signatureValidator = new AuthTokenSignatureValidator(validationConfig.SiteOrigin); + var validatorV10 = new AuthTokenVersion1Validator( + simpleSubjectValidators, + signatureValidator, + validationConfig, + ocspClient, + ocspServiceProvider, + logger + ); + + var validatorV11 = new AuthTokenVersion11Validator( + simpleSubjectValidators, + signatureValidator, + validationConfig, + ocspClient, + ocspServiceProvider, + logger + ); + + return new AuthTokenVersionValidatorFactory( + [ + validatorV11, + validatorV10 + ]); + } + } +} diff --git a/src/WebEid.Security/Validator/VersionValidators/IAuthTokenVersionValidator.cs b/src/WebEid.Security/Validator/VersionValidators/IAuthTokenVersionValidator.cs new file mode 100644 index 00000000..4815399d --- /dev/null +++ b/src/WebEid.Security/Validator/VersionValidators/IAuthTokenVersionValidator.cs @@ -0,0 +1,50 @@ +/* + * 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.Validator.VersionValidators +{ + using System.Security.Cryptography.X509Certificates; + using System.Threading.Tasks; + using AuthToken; + using Exceptions; + + /// + /// Version-specific Web eID authentication token validator. + /// + public interface IAuthTokenVersionValidator + { + /// + /// Whether this validator supports the specified token format, + /// e.g. "web-eid:1.0" or "web-eid:1.1". + /// + public bool Supports(string format); + + /// + /// Validates the Web eID authentication token according to the + /// version-specific rules and returns the authenticated user's certificate. + /// + /// Parsed Web eID authentication token. + /// Server-issued challenge nonce. + /// Validated authentication certificate. + /// If validation fails. + public Task Validate(WebEidAuthToken authToken, string currentChallengeNonce); + } +} diff --git a/src/WebEid.Security/WebEid.Security.csproj b/src/WebEid.Security/WebEid.Security.csproj index ffddef13..eef7e891 100644 --- a/src/WebEid.Security/WebEid.Security.csproj +++ b/src/WebEid.Security/WebEid.Security.csproj @@ -4,7 +4,7 @@ Web eID authentication token validation library true Release - netstandard2.1 + net10.0 WebEid.Security WebEid.Security True @@ -17,16 +17,12 @@ - + - + - - 8.0 - - <_Parameter1>$(MSBuildProjectName).Tests From 6b39ba0d8a63323d20ed9a3b8f9e1591a6b2399c Mon Sep 17 00:00:00 2001 From: Sander Kondratjev Date: Tue, 31 Mar 2026 11:20:11 +0300 Subject: [PATCH 2/3] NFC-149 Readme-s update. --- README.md | 225 +++++++++++++++++++++++++++++++++++++++++----- example/README.md | 43 +++++++-- 2 files changed, 240 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index a58e265e..36424a09 100644 --- a/README.md +++ b/README.md @@ -250,23 +250,24 @@ When using standard [ASP.NET cookie authentication](https://docs.microsoft.com/e ``` - create a REST endpoint that deals with authentication and creates an authentication cookie: ```cs - using System; - using System.Collections.Generic; - using System.Security.Claims; - using System.Text.Json.Serialization; - using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Mvc; - using WebEid.Security.Util; - using WebEid.Security.Validator; + using Security.Util; + using Security.Validator; + using System.Collections.Generic; + using System.Security.Claims; + using System.Threading.Tasks; + using Security.Challenge; + using WebEid.AspNetCore.Example.Dto; + using System; [Route("[controller]")] [ApiController] - public class AuthController : ControllerBase + public class AuthController : BaseController { private readonly IAuthTokenValidator authTokenValidator; - + private readonly IChallengeNonceStore challengeNonceStore; public AuthController(IAuthTokenValidator authTokenValidator, IChallengeNonceStore challengeNonceStore) { this.authTokenValidator = authTokenValidator; @@ -274,18 +275,15 @@ When using standard [ASP.NET cookie authentication](https://docs.microsoft.com/e } [HttpPost] - [ValidateAntiForgeryToken] [Route("login")] public async Task Login([FromBody] AuthenticateRequestDto authToken) { var certificate = await this.authTokenValidator.Validate(authToken.AuthToken, this.challengeNonceStore.GetAndRemove().Base64EncodedNonce); - var claims = new List - { - new Claim(ClaimTypes.GivenName, certificate.GetSubjectGivenName()), - new Claim(ClaimTypes.Surname, certificate.GetSubjectSurname()), - new Claim(ClaimTypes.NameIdentifier, certificate.GetSubjectIdCode()) - }; - + List claims = new(); + AddNewClaimIfCertificateHasData(claims, ClaimTypes.GivenName, certificate.GetSubjectGivenName); + AddNewClaimIfCertificateHasData(claims, ClaimTypes.Surname, certificate.GetSubjectSurname); + AddNewClaimIfCertificateHasData(claims, ClaimTypes.NameIdentifier, certificate.GetSubjectIdCode); + AddNewClaimIfCertificateHasData(claims, ClaimTypes.Name, certificate.GetSubjectCn); var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); var authProperties = new AuthenticationProperties @@ -297,18 +295,99 @@ When using standard [ASP.NET cookie authentication](https://docs.microsoft.com/e CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity), authProperties); + SetUniqueIdInSession(); } [HttpGet] - [ValidateAntiForgeryToken] [Route("logout")] public async Task Logout() { - await HttpContext.SignOutAsync( - CookieAuthenticationDefaults.AuthenticationScheme); + RemoveUserContainerFile(); + await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); + } + + private static void AddNewClaimIfCertificateHasData(List claims, string claimType, Func dataGetter) + { + var claimData = dataGetter(); + if (!string.IsNullOrEmpty(claimData)) + { + claims.Add(new Claim(claimType, claimData)); + } } } ``` +- Similarly, the `MobileAuthInitController` generates a challenge nonce and returns the authentication request link (OS-verified App Link / Universal Link) for starting the Web eID mobile authentication flow, and the `AuthController` handles the mobile login request by validating the returned authentication token and creating the authentication cookie. + ```cs + using System; + using System.Text; + using Microsoft.AspNetCore.Mvc; + using Microsoft.Extensions.Options; + using System.Text.Json; + using System.Text.Json.Serialization; + using Options; + using Services; + using Security.Challenge; + + [ApiController] + [Route("auth/mobile")] + public class MobileAuthInitController( + IChallengeNonceGenerator nonceGenerator, + IOptions mobileOptions, + MobileRequestUriBuilder uriBuilder + ) : ControllerBase + { + private const string WebEidMobileAuthPath = "auth"; + private const string MobileLoginPath = "/auth/mobile/login"; + + [HttpPost("init")] + public IActionResult Init() + { + var challenge = nonceGenerator.GenerateAndStoreNonce(TimeSpan.FromMinutes(5)); + var challengeBase64 = challenge.Base64EncodedNonce; + + var loginUri = $"{Request.Scheme}://{Request.Host}{MobileLoginPath}"; + + var payload = new AuthPayload + { + Challenge = challengeBase64, + LoginUri = loginUri, + GetSigningCertificate = mobileOptions.Value.RequestSigningCert ? true : null + }; + + var json = JsonSerializer.Serialize(payload); + var encodedPayload = Convert.ToBase64String(Encoding.UTF8.GetBytes(json)); + + var authUri = uriBuilder.Build(WebEidMobileAuthPath, encodedPayload); + + return Ok(new AuthUri + { + AuthUriValue = authUri + }); + } + + private sealed record AuthPayload + { + [JsonInclude] + [JsonPropertyName("challenge")] + public required string Challenge { get; init; } + + [JsonInclude] + [JsonPropertyName("loginUri")] + public required string LoginUri { get; init; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("getSigningCertificate")] + public bool? GetSigningCertificate { get; init; } + } + + private sealed record AuthUri + { + [JsonInclude] + [JsonPropertyName("authUri")] + public required string AuthUriValue { get; init; } + } + } + ``` - Similarly, the `MobileAuthInitController` generates a challenge nonce and returns the mobile deep-link for starting the Web eID Mobile authentication flow, and the `MobileAuthLoginController` handles the mobile login request by validating the returned authentication token and creating the authentication cookie. ```cs @@ -422,7 +501,9 @@ When using standard [ASP.NET cookie authentication](https://docs.microsoft.com/e # Table of contents +* [Quickstart](#quickstart) * [Introduction](#introduction) +* [Authentication token format](#authentication-token-format) * [Authentication token validation](#authentication-token-validation) * [Basic usage](#basic-usage) * [Extended configuration](#extended-configuration) @@ -432,6 +513,7 @@ When using standard [ASP.NET cookie authentication](https://docs.microsoft.com/e * [Stateful and stateless authentication](#stateful-and-stateless-authentication) * [Challenge nonce generation](#challenge-nonce-generation) * [Basic usage](#basic-usage-1) +* [Authentication token format versions](#authentication-token-format-versions) # Introduction @@ -439,6 +521,95 @@ The Web eID authentication token validation library for .NET contains the implem The authentication protocol, validation requirements, authentication token format and nonce usage are described in more detail in the [Web eID system architecture document](https://github.com/web-eid/web-eid-system-architecture-doc#authentication-1). +# Authentication token format + +In the following, + +- **origin** is defined as the website origin, the URL serving the web application, +- **challenge nonce** (or challenge) is defined as a cryptographic nonce, a large random number that can be used only once, with at least 256 bits of entropy. + +The Web eID authentication token (format **`web-eid:1.0`**) is a JSON data structure that looks like the following example: + +```json +{ + "unverifiedCertificate": "MIIFozCCA4ugAwIBAgIQHFpdK-zCQsFW4...", + "algorithm": "RS256", + "signature": "HBjNXIaUskXbfhzYQHvwjKDUWfNu4yxXZha...", + "format": "web-eid:1.0", + "appVersion": "https://web-eid.eu/web-eid-app/releases/v2.0.0" +} +``` + +It contains the following fields: + +- `unverifiedCertificate`: the base64-encoded DER-encoded authentication certificate of the eID user; the public key contained in this certificate should be used to verify the signature; the certificate cannot be trusted as it is received from client side and the client can submit a malicious certificate; to establish trust, it must be verified that the certificate is signed by a trusted certificate authority, + +- `algorithm`: the signature algorithm used to produce the signature; the allowed values are the algorithms specified in [JWA RFC](https://www.ietf.org/rfc/rfc7518.html) sections 3.3, 3.4 and 3.5: + + ``` + "ES256", "ES384", "ES512", // ECDSA + "PS256", "PS384", "PS512", // RSASSA-PSS + "RS256", "RS384", "RS512" // RSASSA-PKCS1-v1_5 + ``` + +- `signature`: the base64-encoded signature of the token (see the description below), + +- `format`: the type identifier and version of the token format separated by a colon character '`:`', `web-eid:1.0` as of now; the version number consists of the major and minor number separated by a dot, major version changes are incompatible with previous versions, minor version changes are backwards-compatible within the given major version, + +- `appVersion`: the URL identifying the name and version of the application that issued the token; informative purpose, can be used to identify the affected application in case of faulty tokens. + +The value that is signed by the user’s authentication private key and included in the `signature` field is `hash(origin)+hash(challenge)`. The hash function is used before concatenation to ensure field separation as the hash of a value is guaranteed to have a fixed length. Otherwise the origin `example.com` with challenge nonce `.eu1234` and another origin `example.com.eu` with challenge nonce `1234` would result in the same value after concatenation. The hash function `hash` is the same hash function that is used in the signature algorithm, for example SHA256 in case of RS256. + +The Web eID authentication token (format **`web-eid:1.1`**) is a JSON data structure that looks like the following example: + +```json +{ + "unverifiedCertificate": "MIIFozCCA4ugAwIBAgIQHFpdK-zCQsFW4...", + "algorithm": "RS256", + "signature": "HBjNXIaUskXbfhzYQHvwjKDUWfNu4yxXZha...", + "unverifiedSigningCertificates": [ + { + "certificate": "MIIFikACB3ugAwASAgIHHFrtdZ-zeQsas1...", + "supportedSignatureAlgorithms": [ + { + "cryptoAlgorithm": "ECC", + "hashFunction": "SHA-384", + "paddingScheme": "NONE" + } + ] + } + ], + "format": "web-eid:1.1", + "appVersion": "https://web-eid.eu/web-eid-app/releases/v2.0.0" +} +``` +It contains the following fields: + +- `unverifiedSigningCertificates`: an array of objects containing signing certificate information. + +Each object inside `unverifiedSigningCertificates` contains: + +- `certificate`: base64-encoded DER-encoded signing certificate, + +- `supportedSignatureAlgorithms`: list of supported algorithms in the following format: + + - `cryptoAlgorithm`: the cryptographic algorithm used for the key, + + - `hashFunction`: the hashing algorithm used, + + - `paddingScheme`: the padding scheme used (if applicable). + + +Allowed values are: + + cryptoAlgorithm: "ECC", "RSA" + + hashFunction: + "SHA-224", "SHA-256", "SHA-384", "SHA-512", + "SHA3-224", "SHA3-256", "SHA3-384", "SHA3-512" + + paddingScheme: "NONE", "PKCS1.5", "PSS" + # Authentication token validation The authentication token validation process consists of two stages: @@ -562,6 +733,20 @@ To format the library code, run: dotnet format src/WebEid.Security.sln --no-restore ``` +# Authentication Token Format Versions + +The Web eID authentication protocol defines two token formats currently supported by this library: + +- **Format v1.0** – Used in desktop Web eID authentication flows with traditional smart card readers. + +- **Format v1.1** – An extended token format introduced for broader device compatibility and improved interoperability. + In addition to the fields present in v1.0, it includes: + - `unverifiedSigningCertificates` – an array of signing certificate entries. Each entry contains: + - `certificate` – a base64-encoded DER-encoded signing certificate; + - `supportedSignatureAlgorithms` – a list of supported signature algorithms associated with that certificate; + +Both token formats follow the same validation principles, differing only in the structure of embedded certificates and the additional verification steps required for v1.1. + ## 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 8b8a3f6f..49b22bad 100644 --- a/example/README.md +++ b/example/README.md @@ -6,13 +6,6 @@ This project is an example ASP.NET web application that shows how to implement s More information about the Web eID project is available on the project [website](https://web-eid.eu/). -The ASP.NET web application makes use of the following technologies: - -- ASP.NET MVC, -- the Web eID authentication token validation library [_web-eid-authtoken-validation-dotnet_](https://github.com/web-eid/web-eid-authtoken-validation-dotnet), -- the Web eID JavaScript library [_web-eid.js_](https://github.com/web-eid/web-eid.js), -- the digital signing library [_libdigidocpp_](https://github.com/open-eid/libdigidocpp/tree/master/examples/DigiDocCSharp). - ## Quickstart Complete the steps below to run the example application in order to test authentication and digital signing with Web eID. @@ -160,7 +153,31 @@ This will activate the `https` profile in the `launchSettings.json` and launch t When the application has started, open your preferred web browser on the address defined in `launchSettings.json` on the `applicationUrl` field at `https` profile and follow instructions on the front page. By default the address is https://localhost:44391. -## Overview of the source code +## Table of contents + +* [Quickstart](#quickstart) +* [Overview of the project](#overview-of-the-project) + + [Overview of the source code](#overview-of-the-source-code) + + [Requesting the signing certificate in a separate step](#requesting-the-signing-certificate-in-a-separate-step) +* [More information](#more-information) + + [Frequently asked questions](#frequently-asked-questions) + - [Why do I get the `System.ApplicationException: Failed to verify OCSP Responder certificate` error during signing?](#why-do-i-get-the-systemapplicationexception-failed-to-verify-ocsp-responder-certificate-error-during-signing) +* [Building and running with Docker on Ubuntu Linux](#building-and-running-with-docker-on-ubuntu-linux) + + [Prerequisites](#prerequisites) + + [Building the application](#building-the-application) + + [Building the Docker image](#building-the-docker-image) +* [Running the Docker container with HTTPS support](#running-the-docker-container-with-https-support) + +## Overview of the project + +The ASP.NET web application makes use of the following technologies: + +- ASP.NET MVC, +- the Web eID authentication token validation library [_web-eid-authtoken-validation-dotnet_](https://github.com/web-eid/web-eid-authtoken-validation-dotnet), +- the Web eID JavaScript library [_web-eid.js_](https://github.com/web-eid/web-eid.js), +- the digital signing library [_libdigidocpp_](https://github.com/open-eid/libdigidocpp/tree/master/examples/DigiDocCSharp). + +### Overview of the source code The `src\WebEid.AspNetCore.Example` directory contains the ASP.NET application source code and resources. The subdirectories therein have the following purpose: - `wwwroot`: web server static content, including CSS and JavaScript files, @@ -174,6 +191,16 @@ The `src\WebEid.AspNetCore.Example` directory contains the ASP.NET application s - `Services`: Web eID signing service implementation that uses `libdigidocpp`. - `Options`: strongly-typed configuration classes for mobile Web eID settings such as `BaseRequestUri` and `RequestSigningCert` (when set to false, initiates a separate signing-certificate flow to demo requesting the certificate without prior authentication, as the signing certificate normally comes from the authentication flow). +### Requesting the signing certificate in a separate step + +In some deployments, the signing certificate is not reused from the authentication flow. Instead, it is retrieved directly from the user’s ID-card during the signing process itself. + +This approach is useful when the signing process is performed without a prior authentication step. For example, in a mobile flow, the user may start signing directly without authenticating beforehand. In such cases, the signing certificate must be requested separately from the user’s ID-card before the signature can be created. + +When this mode is enabled in the configuration, the backend issues a separate request for the signing certificate using the `MobileSigningService`. The service communicates with the client to obtain the certificate before the signing container is prepared, ensuring that the correct certificate chain is available for the signature. + +This behavior is controlled by the `RequestSigningCert` flag in the `appsettings.json` configuration files (`appsettings.json`, `appsettings.Development.json`). When the flag is set to **false**, the application explicitly requests the signing certificate during the signing process, demonstrating the separate signing certificate retrieval flow. When set to **true**, the signing uses the signing certificate that was already obtained during authentication, and no additional request is made. + ## More information See the [Web eID Java example application documentation](https://github.com/web-eid/web-eid-spring-boot-example) for more information, including answers to questions not answered below. From 1bca486bcd8f2d617ac9610f7084b6ce46e57f19 Mon Sep 17 00:00:00 2001 From: Mart Aarma Date: Mon, 27 Jul 2026 16:18:05 +0300 Subject: [PATCH 3/3] NFC-149 Readme-s update Signed-off-by: Mart Aarma --- README.md | 255 +++++++++++++++++----------------------------- example/README.md | 9 +- 2 files changed, 102 insertions(+), 162 deletions(-) diff --git a/README.md b/README.md index 36424a09..488ac322 100644 --- a/README.md +++ b/README.md @@ -196,8 +196,9 @@ A REST endpoint that issues challenge nonces is required for authentication. The In the following example, we are using the [ASP.NET Web APIs RESTful Web Services framework](https://dotnet.microsoft.com/apps/aspnet/apis) to implement the endpoint, see also full implementation [here](https://github.com/web-eid/web-eid-authtoken-validation-dotnet/blob/main/example/src/WebEid.AspNetCore.Example/Controllers/Api/AuthController.cs). ```cs +using System; using Microsoft.AspNetCore.Mvc; -using WebEid.Security.Nonce; +using WebEid.Security.Challenge; [ApiController] [Route("auth")] @@ -250,60 +251,97 @@ When using standard [ASP.NET cookie authentication](https://docs.microsoft.com/e ``` - create a REST endpoint that deals with authentication and creates an authentication cookie: ```cs - using Microsoft.AspNetCore.Authentication; - using Microsoft.AspNetCore.Authentication.Cookies; - using Microsoft.AspNetCore.Mvc; - 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 Security.Challenge; + using Microsoft.AspNetCore.Authentication; + using Microsoft.AspNetCore.Authentication.Cookies; + using Microsoft.AspNetCore.Mvc; using WebEid.AspNetCore.Example.Dto; - using System; + using WebEid.Security.AuthToken; + using WebEid.Security.Challenge; + using WebEid.Security.Exceptions; + using WebEid.Security.Util; + using WebEid.Security.Validator; [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) + private readonly IAuthTokenValidator authTokenValidator = authTokenValidator; + private readonly IChallengeNonceStore challengeNonceStore = challengeNonceStore; + + [HttpPost("login")] + public async Task Login([FromBody] AuthenticateRequestDto dto) { - this.authTokenValidator = authTokenValidator; - this.challengeNonceStore = challengeNonceStore; + if (dto?.AuthToken is null) + { + return BadRequest(new { error = "Missing auth_token" }); + } + + try + { + await SignInUser(dto.AuthToken); + return Ok(); + } + catch (Exception ex) when (ex is ChallengeNonceNotFoundException or ChallengeNonceExpiredException) + { + return Unauthorized(new { error = "challenge_nonce_not_found_or_expired" }); + } + catch (AuthTokenException) + { + return Unauthorized(new { error = "authentication_failed" }); + } + } + + [HttpPost("logout")] + public async Task Logout() + { + if (HasActiveSession()) + { + RemoveUserContainerFile(); + } + + await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); } - [HttpPost] - [Route("login")] - public async Task Login([FromBody] AuthenticateRequestDto authToken) + private async Task SignInUser(WebEidAuthToken authToken) { - var certificate = await this.authTokenValidator.Validate(authToken.AuthToken, this.challengeNonceStore.GetAndRemove().Base64EncodedNonce); - List claims = new(); + var certificate = await authTokenValidator.Validate(authToken, challengeNonceStore.GetAndRemove().Base64EncodedNonce); + var claims = new List(); + AddNewClaimIfCertificateHasData(claims, ClaimTypes.GivenName, certificate.GetSubjectGivenName); AddNewClaimIfCertificateHasData(claims, ClaimTypes.Surname, certificate.GetSubjectSurname); AddNewClaimIfCertificateHasData(claims, ClaimTypes.NameIdentifier, certificate.GetSubjectIdCode); AddNewClaimIfCertificateHasData(claims, ClaimTypes.Name, certificate.GetSubjectCn); - var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); - var authProperties = new AuthenticationProperties + var signingCertificate = authToken.UnverifiedSigningCertificates != null && + authToken.UnverifiedSigningCertificates.Count > 0 + ? authToken.UnverifiedSigningCertificates[0] + : null; + + if (signingCertificate != null && !string.IsNullOrEmpty(signingCertificate.Certificate)) { - AllowRefresh = true - }; + claims.Add(new Claim("signingCertificate", signingCertificate.Certificate)); + } + + if (signingCertificate?.SupportedSignatureAlgorithms != null) + { + claims.Add(new Claim( + "supportedSignatureAlgorithms", + JsonSerializer.Serialize(signingCertificate.SupportedSignatureAlgorithms))); + } + + var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); await HttpContext.SignInAsync( CookieAuthenticationDefaults.AuthenticationScheme, - new ClaimsPrincipal(claimsIdentity), - authProperties); - SetUniqueIdInSession(); - } + new ClaimsPrincipal(identity), + new AuthenticationProperties { AllowRefresh = true }); - [HttpGet] - [Route("logout")] - public async Task Logout() - { - RemoveUserContainerFile(); - await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); + SetUniqueIdInSession(); } private static void AddNewClaimIfCertificateHasData(List claims, string claimType, Func dataGetter) @@ -320,20 +358,22 @@ When using standard [ASP.NET cookie authentication](https://docs.microsoft.com/e ```cs using System; using System.Text; - using Microsoft.AspNetCore.Mvc; - using Microsoft.Extensions.Options; using System.Text.Json; using System.Text.Json.Serialization; - using Options; - using Services; - using Security.Challenge; + using Microsoft.AspNetCore.Mvc; + using Microsoft.Extensions.Configuration; + using Microsoft.Extensions.Options; + using WebEid.AspNetCore.Example.Options; + using WebEid.AspNetCore.Example.Services; + using WebEid.Security.Challenge; [ApiController] [Route("auth/mobile")] public class MobileAuthInitController( IChallengeNonceGenerator nonceGenerator, IOptions mobileOptions, - MobileRequestUriBuilder uriBuilder + MobileRequestUriBuilder uriBuilder, + IConfiguration configuration ) : ControllerBase { private const string WebEidMobileAuthPath = "auth"; @@ -345,7 +385,7 @@ When using standard [ASP.NET cookie authentication](https://docs.microsoft.com/e var challenge = nonceGenerator.GenerateAndStoreNonce(TimeSpan.FromMinutes(5)); var challengeBase64 = challenge.Base64EncodedNonce; - var loginUri = $"{Request.Scheme}://{Request.Host}{MobileLoginPath}"; + var loginUri = $"{configuration["OriginUrl"]}{MobileLoginPath}"; var payload = new AuthPayload { @@ -389,116 +429,6 @@ When using standard [ASP.NET cookie authentication](https://docs.microsoft.com/e } ``` -- Similarly, the `MobileAuthInitController` generates a challenge nonce and returns the mobile deep-link for starting the Web eID Mobile authentication flow, and the `MobileAuthLoginController` handles the mobile login request by validating the returned authentication token and creating the authentication cookie. - ```cs - using System; - using System.Text; - using Microsoft.AspNetCore.Mvc; - using Microsoft.Extensions.Options; - using System.Text.Json; - using System.Text.Json.Serialization; - using Options; - using Security.Challenge; - - [ApiController] - [Route("auth/mobile")] - public class MobileAuthInitController( - IChallengeNonceGenerator nonceGenerator, - IOptions mobileOptions - ) : ControllerBase - { - private const string WebEidMobileAuthPath = "auth"; - private const string MobileLoginPath = "/auth/mobile/login"; - - [HttpPost("init")] - public IActionResult Init() - { - var challenge = nonceGenerator.GenerateAndStoreNonce(TimeSpan.FromMinutes(5)); - var challengeBase64 = challenge.Base64EncodedNonce; - - var loginUri = $"{Request.Scheme}://{Request.Host}{MobileLoginPath}"; - - var payload = new AuthPayload - { - Challenge = challengeBase64, - LoginUri = loginUri, - GetSigningCertificate = mobileOptions.Value.RequestSigningCert ? true : null - }; - - var json = JsonSerializer.Serialize(payload); - var encodedPayload = Convert.ToBase64String(Encoding.UTF8.GetBytes(json)); - - var authUri = BuildAuthUri(encodedPayload); - - return Ok(new AuthUri - { - AuthUriValue = authUri - }); - } - ``` - - ```cs - using Microsoft.AspNetCore.Mvc; - using System.Text.Json; - using Dto; - using Security.Challenge; - using Security.Validator; - using System.Security.Claims; - using System.Threading.Tasks; - using Microsoft.AspNetCore.Authentication; - using Microsoft.AspNetCore.Authentication.Cookies; - using Security.Util; - - [ApiController] - [Route("auth/mobile")] - public class MobileAuthLoginController( - IAuthTokenValidator authTokenValidator, - IChallengeNonceStore challengeNonceStore - ) : ControllerBase - { - [HttpPost("login")] - public async Task MobileLogin([FromBody] AuthenticateRequestDto dto) - { - if (dto?.AuthToken == null) - { - return BadRequest(new { error = "Missing auth_token" }); - } - - var parsedToken = dto.AuthToken; - var certificate = await authTokenValidator.Validate( - parsedToken, - challengeNonceStore.GetAndRemove().Base64EncodedNonce); - - var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme); - - identity.AddClaim(new Claim(ClaimTypes.GivenName, certificate.GetSubjectGivenName())); - identity.AddClaim(new Claim(ClaimTypes.Surname, certificate.GetSubjectSurname())); - identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, certificate.GetSubjectIdCode())); - identity.AddClaim(new Claim(ClaimTypes.Name, certificate.GetSubjectCn())); - - if (!string.IsNullOrEmpty(parsedToken.UnverifiedSigningCertificate)) - { - identity.AddClaim(new Claim("signingCertificate", parsedToken.UnverifiedSigningCertificate)); - } - - if (parsedToken.SupportedSignatureAlgorithms != null) - { - identity.AddClaim(new Claim( - "supportedSignatureAlgorithms", - JsonSerializer.Serialize(parsedToken.SupportedSignatureAlgorithms))); - } - - await HttpContext.SignInAsync( - CookieAuthenticationDefaults.AuthenticationScheme, - new ClaimsPrincipal(identity), - new AuthenticationProperties { IsPersistent = false }); - - return Ok(new { redirect = "/welcome" }); - } - } - ``` - - # Table of contents * [Quickstart](#quickstart) @@ -517,7 +447,7 @@ When using standard [ASP.NET cookie authentication](https://docs.microsoft.com/e # Introduction -The Web eID authentication token validation library for .NET contains the implementation of the Web eID authentication token validation process in its entirety to ensure that the authentication token sent by the Web eID browser extension contains valid, consistent data that has not been modified by a third party. It also implements secure challenge nonce generation as required by the Web eID authentication protocol. It is easy to configure and integrate into your authentication service. +The Web eID authentication token validation library for .NET contains the implementation of the Web eID authentication token validation process in its entirety to ensure that the authentication token sent by the Web eID browser extension or mobile application contains valid, consistent data that has not been modified by a third party. It also implements secure challenge nonce generation as required by the Web eID authentication protocol. It is easy to configure and integrate into your authentication service. The authentication protocol, validation requirements, authentication token format and nonce usage are described in more detail in the [Web eID system architecture document](https://github.com/web-eid/web-eid-system-architecture-doc#authentication-1). @@ -554,7 +484,7 @@ It contains the following fields: - `signature`: the base64-encoded signature of the token (see the description below), -- `format`: the type identifier and version of the token format separated by a colon character '`:`', `web-eid:1.0` as of now; the version number consists of the major and minor number separated by a dot, major version changes are incompatible with previous versions, minor version changes are backwards-compatible within the given major version, +- `format`: the type identifier and version of the token format separated by a colon character '`:`', `web-eid:1.0` or `web-eid:1.1` as of now; the version number consists of the major and minor number separated by a dot, major version changes are incompatible with previous versions, minor version changes are backwards-compatible within the given major version, - `appVersion`: the URL identifying the name and version of the application that issued the token; informative purpose, can be used to identify the affected application in case of faulty tokens. @@ -612,10 +542,16 @@ Allowed values are: # Authentication token validation -The authentication token validation process consists of two stages: +The authentication token validation process consists of the following stages: - First, **user certificate validation**: the validator parses the token and extracts the user certificate from the *unverifiedCertificate* field. Then it checks the certificate expiration, purpose and policies. Next it checks that the certificate is signed by a trusted CA and checks the certificate status with OCSP. - Second, **token signature validation**: the validator validates that the token signature was created using the provided user certificate by reconstructing the signed data `hash(origin)+hash(challenge)` and using the public key from the certificate to verify the signature in the `signature` field. If the signature verification succeeds, then the origin and challenge nonce have been implicitly and correctly verified without the need to implement any additional security checks. +- Additional validation for **Web eID authentication tokens (format v1.1)**: the token must contain the `unverifiedSigningCertificates` field with at least one signing certificate entry. Each entry's `supportedSignatureAlgorithms` are validated against the set of allowed cryptographic algorithms, hash functions, and padding schemes. For each signing certificate, the following checks are performed: + - The subject must match the subject of the authentication certificate, ensuring both certificates belong to the same user. + - The issuing authority must match that of the authentication certificate, verified via the Authority Key Identifier (AKI) extension. + - The certificate must be within its validity period. + - The certificate must contain the non-repudiation key usage bit required for digital signatures. + - The certificate chain must validate against the configured trusted certificate authorities. The website backend must lookup the challenge nonce from its local store using an identifier specific to the browser session, to guarantee that the authentication token was received from the same browser to which the corresponding challenge nonce was issued. The website backend must guarantee that the challenge nonce lifetime is limited and that its expiration is checked, and that it can be used only once by removing it from the store during validation. @@ -703,7 +639,7 @@ A common alternative to stateful authentication is stateless authentication with # Challenge nonce generation -The authentication protocol requires support for generating challenge nonces, large random numbers that can be used only once, and storing them for later use during token validation. The validation library uses the *System.Security.Cryptography.RandomNumberGenerator* API as the secure random source and provides *WebEid.Security.Cache.ICache* interface for storing issued challenge nonces. +The authentication protocol requires support for generating challenge nonces, large random numbers that can be used only once, and storing them for later use during token validation. The validation library uses the *System.Security.Cryptography.RandomNumberGenerator* API as the secure random source and the `IChallengeNonceStore` interface for storing issued challenge nonces. The authentication protocol requires a REST endpoint that issues challenge nonces as described in section *[6. Add a REST endpoint for issuing challenge nonces](#6-add-a-rest-endpoint-for-issuing-challenge-nonces)*. @@ -733,17 +669,16 @@ To format the library code, run: dotnet format src/WebEid.Security.sln --no-restore ``` -# Authentication Token Format Versions +# Authentication token format versions The Web eID authentication protocol defines two token formats currently supported by this library: - **Format v1.0** – Used in desktop Web eID authentication flows with traditional smart card readers. -- **Format v1.1** – An extended token format introduced for broader device compatibility and improved interoperability. - In addition to the fields present in v1.0, it includes: - - `unverifiedSigningCertificates` – an array of signing certificate entries. Each entry contains: - - `certificate` – a base64-encoded DER-encoded signing certificate; - - `supportedSignatureAlgorithms` – a list of supported signature algorithms associated with that certificate; +- **Format v1.1** – An extended authentication token format that allows signing certificate information to be included in the authentication response. + - `unverifiedSigningCertificates` – an array of signing certificate entries. Each entry contains: + - `certificate` – a base64-encoded DER-encoded signing certificate; + - `supportedSignatureAlgorithms` – a list of supported signature algorithms associated with that certificate; Both token formats follow the same validation principles, differing only in the structure of embedded certificates and the additional verification steps required for v1.1. diff --git a/example/README.md b/example/README.md index 49b22bad..6ae12e8e 100644 --- a/example/README.md +++ b/example/README.md @@ -156,13 +156,14 @@ By default the address is https://localhost:44391. ## Table of contents * [Quickstart](#quickstart) +* [Setup for Development](#setup-for-development) * [Overview of the project](#overview-of-the-project) + [Overview of the source code](#overview-of-the-source-code) + [Requesting the signing certificate in a separate step](#requesting-the-signing-certificate-in-a-separate-step) * [More information](#more-information) + [Frequently asked questions](#frequently-asked-questions) - [Why do I get the `System.ApplicationException: Failed to verify OCSP Responder certificate` error during signing?](#why-do-i-get-the-systemapplicationexception-failed-to-verify-ocsp-responder-certificate-error-during-signing) -* [Building and running with Docker on Ubuntu Linux](#building-and-running-with-docker-on-ubuntu-linux) +* [Building and running example web application with Docker on Ubuntu Linux](#building-and-running-example-web-application-with-docker-on-ubuntu-linux) + [Prerequisites](#prerequisites) + [Building the application](#building-the-application) + [Building the Docker image](#building-the-docker-image) @@ -187,8 +188,12 @@ The `src\WebEid.AspNetCore.Example` directory contains the ASP.NET application s - logging in, - digital signing, - `DigiDoc`: contains the C# binding files of the `libdigidocpp` library; these files must be copied from the `libdigidocpp` installation directory `\include\digidocpp_csharp`, +- `Dto`: data transfer objects used by the Web API endpoints, - `Pages`: Razor pages, -- `Services`: Web eID signing service implementation that uses `libdigidocpp`. +- `Services`: helper services for cleaning up signing containers and for building the mobile authentication and signing request URIs, +- `Signing`: Web eID signing service implementation that uses `libdigidocpp`, + - `SigningService`: prepares signing containers and finalizes signatures, + - `MobileSigningService`: orchestrates the mobile signing flow (builds mobile signing requests/responses) and supports requesting the signing certificate in a separate step when enabled by configuration, - `Options`: strongly-typed configuration classes for mobile Web eID settings such as `BaseRequestUri` and `RequestSigningCert` (when set to false, initiates a separate signing-certificate flow to demo requesting the certificate without prior authentication, as the signing certificate normally comes from the authentication flow). ### Requesting the signing certificate in a separate step