From 9070639dbdc0e80c16516e367ba21098a1907720 Mon Sep 17 00:00:00 2001 From: Mihkel Kivisild Date: Wed, 27 Nov 2024 16:20:04 +0200 Subject: [PATCH] Merged web-eid-spring-boot-example into the web-eid-authtoken-validation-java repository. WE2-932 Signed-off-by: Mihkel Kivisild --- .github/workflows/maven-build-example.yml | 30 + README.md | 4 + example/.gitattributes | 19 + example/.gitignore | 34 + .../.mvn/wrapper/MavenWrapperDownloader.java | 121 +++ example/.mvn/wrapper/maven-wrapper.jar | Bin 0 -> 50710 bytes example/.mvn/wrapper/maven-wrapper.properties | 2 + example/.prettierrc.yaml | 7 + example/AUTHORS.md | 7 + example/LICENSE | 21 + example/README.md | 283 +++++++ example/docker-compose.yml | 10 + example/mvnw | 310 +++++++ example/mvnw.cmd | 182 ++++ example/pom.xml | 137 +++ example/scripts/deployment/.gitignore | 2 + example/scripts/deployment/README.md | 20 + example/scripts/deployment/fab.sh | 15 + example/scripts/deployment/fabfile.py | 18 + example/scripts/deployment/requirements.txt | 1 + .../WebEidSpringbootExampleApplication.java | 34 + .../config/ApplicationConfiguration.java | 62 ++ .../config/SameSiteCookieConfiguration.java | 42 + .../SessionBackedChallengeNonceStore.java | 57 ++ .../config/ValidationConfiguration.java | 144 ++++ .../eu/webeid/example/config/YAMLConfig.java | 78 ++ .../AuthTokenDTOAuthenticationProvider.java | 90 ++ .../WebEidAjaxLoginProcessingFilter.java | 93 +++ .../security/WebEidAuthentication.java | 80 ++ .../AjaxAuthenticationFailureHandler.java | 55 ++ .../AjaxAuthenticationSuccessHandler.java | 80 ++ .../example/security/dto/AuthTokenDTO.java | 39 + .../example/service/SigningService.java | 209 +++++ .../example/service/dto/CertificateDTO.java | 70 ++ .../example/service/dto/ChallengeDTO.java | 35 + .../webeid/example/service/dto/DigestDTO.java | 44 + .../webeid/example/service/dto/FileDTO.java | 81 ++ .../service/dto/SignatureAlgorithmDTO.java | 82 ++ .../example/service/dto/SignatureDTO.java | 41 + .../webeid/example/web/IndexController.java | 37 + .../webeid/example/web/WelcomeController.java | 49 ++ .../example/web/rest/ChallengeController.java | 47 ++ .../example/web/rest/SigningController.java | 87 ++ .../src/main/resources/application-dev.yaml | 4 + .../src/main/resources/application-prod.yaml | 8 + .../src/main/resources/application.properties | 2 + example/src/main/resources/application.yaml | 19 + .../resources/certs/dev/TEST_ORG_2021E.cer | 22 + .../resources/certs/dev/TEST_ORG_2021R.cer | 39 + .../certs/dev/TEST_of_ESTEID2018.cer | Bin 0 -> 1408 bytes .../certs/dev/TEST_of_KLASS3-SK_2016.cer | Bin 0 -> 1741 bytes .../certs/dev/eID-TEST-EC-Citizen-CA.cer | 19 + .../certs/dev/eID-TEST-EC-Root-CA.cer | 14 + .../certs/dev/marts-klass3-sk-2010.cer | Bin 0 -> 1133 bytes .../main/resources/certs/prod/ESTEID2018.cer | Bin 0 -> 1371 bytes .../certs/prod/trusted_certificates.jks | Bin 0 -> 1345 bytes .../resources/static/css/bootstrap.min.css | 7 + .../src/main/resources/static/css/main.css | 69 ++ .../static/files/Web eID privacy policy.pdf | Bin 0 -> 76371 bytes .../static/files/example-for-signing.txt | 1 + .../resources/static/img/eu-fund-flags.svg | 787 ++++++++++++++++++ .../src/main/resources/static/js/errors.js | 59 ++ .../src/main/resources/static/js/web-eid.js | 423 ++++++++++ .../static/scripts/install-web-eid.sh | 232 ++++++ .../src/main/resources/templates/index.html | 310 +++++++ .../welcome-with-file-upload-support.html | 130 +++ .../src/main/resources/templates/welcome.html | 135 +++ .../AuthenticationRestControllerTest.java | 49 ++ .../eu/webeid/example/WebApplicationTest.java | 135 +++ .../WebEidAjaxLoginProcessingFilterTest.java | 39 + .../security/WebEidAuthenticationTest.java | 23 + .../eu/webeid/example/testutil/Dates.java | 48 ++ .../webeid/example/testutil/HttpHelper.java | 105 +++ .../webeid/example/testutil/ObjectMother.java | 160 ++++ .../src/test/resources/application-dev.yaml | 4 + example/src/test/resources/signout.p12 | Bin 0 -> 2890 bytes 76 files changed, 5702 insertions(+) create mode 100644 .github/workflows/maven-build-example.yml create mode 100644 example/.gitattributes create mode 100644 example/.gitignore create mode 100644 example/.mvn/wrapper/MavenWrapperDownloader.java create mode 100644 example/.mvn/wrapper/maven-wrapper.jar create mode 100644 example/.mvn/wrapper/maven-wrapper.properties create mode 100644 example/.prettierrc.yaml create mode 100644 example/AUTHORS.md create mode 100644 example/LICENSE create mode 100644 example/README.md create mode 100644 example/docker-compose.yml create mode 100644 example/mvnw create mode 100644 example/mvnw.cmd create mode 100644 example/pom.xml create mode 100644 example/scripts/deployment/.gitignore create mode 100644 example/scripts/deployment/README.md create mode 100644 example/scripts/deployment/fab.sh create mode 100644 example/scripts/deployment/fabfile.py create mode 100644 example/scripts/deployment/requirements.txt create mode 100644 example/src/main/java/eu/webeid/example/WebEidSpringbootExampleApplication.java create mode 100644 example/src/main/java/eu/webeid/example/config/ApplicationConfiguration.java create mode 100644 example/src/main/java/eu/webeid/example/config/SameSiteCookieConfiguration.java create mode 100644 example/src/main/java/eu/webeid/example/config/SessionBackedChallengeNonceStore.java create mode 100644 example/src/main/java/eu/webeid/example/config/ValidationConfiguration.java create mode 100644 example/src/main/java/eu/webeid/example/config/YAMLConfig.java create mode 100644 example/src/main/java/eu/webeid/example/security/AuthTokenDTOAuthenticationProvider.java create mode 100644 example/src/main/java/eu/webeid/example/security/WebEidAjaxLoginProcessingFilter.java create mode 100644 example/src/main/java/eu/webeid/example/security/WebEidAuthentication.java create mode 100644 example/src/main/java/eu/webeid/example/security/ajax/AjaxAuthenticationFailureHandler.java create mode 100644 example/src/main/java/eu/webeid/example/security/ajax/AjaxAuthenticationSuccessHandler.java create mode 100644 example/src/main/java/eu/webeid/example/security/dto/AuthTokenDTO.java create mode 100644 example/src/main/java/eu/webeid/example/service/SigningService.java create mode 100644 example/src/main/java/eu/webeid/example/service/dto/CertificateDTO.java create mode 100644 example/src/main/java/eu/webeid/example/service/dto/ChallengeDTO.java create mode 100644 example/src/main/java/eu/webeid/example/service/dto/DigestDTO.java create mode 100644 example/src/main/java/eu/webeid/example/service/dto/FileDTO.java create mode 100644 example/src/main/java/eu/webeid/example/service/dto/SignatureAlgorithmDTO.java create mode 100644 example/src/main/java/eu/webeid/example/service/dto/SignatureDTO.java create mode 100644 example/src/main/java/eu/webeid/example/web/IndexController.java create mode 100644 example/src/main/java/eu/webeid/example/web/WelcomeController.java create mode 100644 example/src/main/java/eu/webeid/example/web/rest/ChallengeController.java create mode 100644 example/src/main/java/eu/webeid/example/web/rest/SigningController.java create mode 100644 example/src/main/resources/application-dev.yaml create mode 100644 example/src/main/resources/application-prod.yaml create mode 100644 example/src/main/resources/application.properties create mode 100644 example/src/main/resources/application.yaml create mode 100644 example/src/main/resources/certs/dev/TEST_ORG_2021E.cer create mode 100644 example/src/main/resources/certs/dev/TEST_ORG_2021R.cer create mode 100644 example/src/main/resources/certs/dev/TEST_of_ESTEID2018.cer create mode 100644 example/src/main/resources/certs/dev/TEST_of_KLASS3-SK_2016.cer create mode 100644 example/src/main/resources/certs/dev/eID-TEST-EC-Citizen-CA.cer create mode 100644 example/src/main/resources/certs/dev/eID-TEST-EC-Root-CA.cer create mode 100644 example/src/main/resources/certs/dev/marts-klass3-sk-2010.cer create mode 100644 example/src/main/resources/certs/prod/ESTEID2018.cer create mode 100644 example/src/main/resources/certs/prod/trusted_certificates.jks create mode 100644 example/src/main/resources/static/css/bootstrap.min.css create mode 100644 example/src/main/resources/static/css/main.css create mode 100644 example/src/main/resources/static/files/Web eID privacy policy.pdf create mode 100644 example/src/main/resources/static/files/example-for-signing.txt create mode 100644 example/src/main/resources/static/img/eu-fund-flags.svg create mode 100644 example/src/main/resources/static/js/errors.js create mode 100644 example/src/main/resources/static/js/web-eid.js create mode 100644 example/src/main/resources/static/scripts/install-web-eid.sh create mode 100644 example/src/main/resources/templates/index.html create mode 100644 example/src/main/resources/templates/welcome-with-file-upload-support.html create mode 100644 example/src/main/resources/templates/welcome.html create mode 100644 example/src/test/java/eu/webeid/example/AuthenticationRestControllerTest.java create mode 100644 example/src/test/java/eu/webeid/example/WebApplicationTest.java create mode 100644 example/src/test/java/eu/webeid/example/security/WebEidAjaxLoginProcessingFilterTest.java create mode 100644 example/src/test/java/eu/webeid/example/security/WebEidAuthenticationTest.java create mode 100644 example/src/test/java/eu/webeid/example/testutil/Dates.java create mode 100644 example/src/test/java/eu/webeid/example/testutil/HttpHelper.java create mode 100644 example/src/test/java/eu/webeid/example/testutil/ObjectMother.java create mode 100644 example/src/test/resources/application-dev.yaml create mode 100644 example/src/test/resources/signout.p12 diff --git a/.github/workflows/maven-build-example.yml b/.github/workflows/maven-build-example.yml new file mode 100644 index 0000000..7c14bb9 --- /dev/null +++ b/.github/workflows/maven-build-example.yml @@ -0,0 +1,30 @@ +name: Maven build for example + +on: [ push, pull_request ] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-java@v4 + with: + distribution: zulu + java-version: 17 + + - name: Cache Maven packages + uses: actions/cache@v4 + with: + path: ~/.m2 + key: ${{ runner.os }}-m2-v17-${{ secrets.CACHE_VERSION }}-${{ hashFiles('**/pom.xml') }} + restore-keys: ${{ runner.os }}-m2-v17-${{ secrets.CACHE_VERSION }} + + - name: Build + run: mvn --batch-mode compile + working-directory: ./example + + - name: Test and package + run: mvn --batch-mode package + working-directory: ./example diff --git a/README.md b/README.md index ed41dd9..bbc70db 100644 --- a/README.md +++ b/README.md @@ -201,6 +201,7 @@ try { - [Basic usage](#basic-usage-1) - [Extended configuration](#extended-configuration-1) - [Differences between version 1 and version 2](#differences-between-version-1-and-version-2) +- [Example using Spring Boot](#example-using-spring-boot) # Introduction @@ -380,3 +381,6 @@ NonceGenerator generator = new NonceGeneratorBuilder() In version 1, the generated challenge nonces were stored in a JSR107 compatible cache. The goal of using a cache was to support stateful and stateless authentication with a universal API that uses the same underlying mechanism. However, in case the website had a CSRF vulnerability, this made the solution vulnerable to [forged login attacks](https://en.wikipedia.org/wiki/Cross-site_request_forgery#Forging_login_requests) (the attacker could trick the victim to submit the authentication token with the attacker's challenge nonce to the website using a CSRF attack, so that the victim was authenticated to the website as the attacker). To mitigate this attack, in version 2 the requirement is that the library adopter must guarantee that the authentication token is received from the same browser to which the corresponding challenge nonce was issued. The recommended solution is to use a session-backed challenge nonce store, as in the code examples above. The library no longer uses the JSR107 cache API and provides a `ChallengeNonceStore` interface instead. In the internal implementation, the Web eID authentication token format changed in version 2. In version 1, the authentication token was in the OpenID X509 ID Token (JWT) format in order to be compatible with the standard OpenID Connect ID Token specification. During independent security review it was pointed out that any similarities of the Web eID authentication token to the JWT format are actually undesirable, as they would imply that the claims presented in the Web eID authentication token can be trusted and processed, while in fact they must be ignored, as they can be manipulated at the client side. The presence of the claims in the authentication token introduces a risk of vulnerabilities in case the authentication implementer decides to rely on any of them for making security critical decisions or decides to apply the same standard validation workflow that is applied to standard JWTs. Since there does not exist a standardized format for an authentication proof that corresponds to the requirements of the Web eID authentication protocol, a special purpose JSON-based format for the Web eID authentication token was adopted in version 2. The format is described in detail in the section *[Authentication token format](#authentication-token-format)*, and the full analysis of the format change is available in [this article](https://web-eid.github.io/web-eid-system-architecture-doc/web-eid-auth-token-v2-format-spec.pdf). + +# Example using Spring Boot +See the [example documentation](example/README.md). \ No newline at end of file diff --git a/example/.gitattributes b/example/.gitattributes new file mode 100644 index 0000000..9115584 --- /dev/null +++ b/example/.gitattributes @@ -0,0 +1,19 @@ +* text=auto +*.java text eol=lf +*.xml text eol=lf +*.pl text eol=lf +*.py text eol=lf +*.html text eol=lf +*.scss text eol=lf +*.css text eol=lf +*.js text eol=lf +*.bat text eol=crlf +*.cmd text eol=crlf +MANIFEST.MF text eol=lf +commit-msg text eol=lf +.gitattributes text eol=lf +.gitignore text eol=lf +*.deb filter=lfs diff=lfs merge=lfs -text +*.pkg filter=lfs diff=lfs merge=lfs -text +*.msi filter=lfs diff=lfs merge=lfs -text +*.zip filter=lfs diff=lfs merge=lfs -text diff --git a/example/.gitignore b/example/.gitignore new file mode 100644 index 0000000..535d7b5 --- /dev/null +++ b/example/.gitignore @@ -0,0 +1,34 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/** +!**/src/test/** + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ + +### VS Code ### +.vscode/ + +### Vim ### +*.swp diff --git a/example/.mvn/wrapper/MavenWrapperDownloader.java b/example/.mvn/wrapper/MavenWrapperDownloader.java new file mode 100644 index 0000000..bb16fac --- /dev/null +++ b/example/.mvn/wrapper/MavenWrapperDownloader.java @@ -0,0 +1,121 @@ +/* + * Copyright 2007-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import java.io.*; +import java.net.*; +import java.nio.channels.*; +import java.util.Properties; + +public class MavenWrapperDownloader { + private static final String WRAPPER_VERSION = "0.5.6"; + /** + * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. + */ + private static final String DEFAULT_DOWNLOAD_URL = + "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" + + WRAPPER_VERSION + + "/maven-wrapper-" + + WRAPPER_VERSION + + ".jar"; + + /** + * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to + * use instead of the default one. + */ + private static final String MAVEN_WRAPPER_PROPERTIES_PATH = ".mvn/wrapper/maven-wrapper.properties"; + + /** + * Path where the maven-wrapper.jar will be saved to. + */ + private static final String MAVEN_WRAPPER_JAR_PATH = ".mvn/wrapper/maven-wrapper.jar"; + + /** + * Name of the property which should be used to override the default download url for the wrapper. + */ + private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; + + public static void main(String args[]) { + System.out.println("- Downloader started"); + File baseDirectory = new File(args[0]); + System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); + + // If the maven-wrapper.properties exists, read it and check if it contains a custom + // wrapperUrl parameter. + File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); + String url = DEFAULT_DOWNLOAD_URL; + if (mavenWrapperPropertyFile.exists()) { + FileInputStream mavenWrapperPropertyFileInputStream = null; + try { + mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); + Properties mavenWrapperProperties = new Properties(); + mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); + url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); + } catch (IOException e) { + System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); + } finally { + try { + if (mavenWrapperPropertyFileInputStream != null) { + mavenWrapperPropertyFileInputStream.close(); + } + } catch (IOException e) { + // Ignore ... + } + } + } + System.out.println("- Downloading from: " + url); + + File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); + if (!outputFile.getParentFile().exists()) { + if (!outputFile.getParentFile().mkdirs()) { + System.out.println( + "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'" + ); + } + } + System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); + try { + downloadFileFromURL(url, outputFile); + System.out.println("Done"); + System.exit(0); + } catch (Throwable e) { + System.out.println("- Error downloading"); + e.printStackTrace(); + System.exit(1); + } + } + + private static void downloadFileFromURL(String urlString, File destination) throws Exception { + if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { + String username = System.getenv("MVNW_USERNAME"); + char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); + Authenticator.setDefault( + new Authenticator() { + + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(username, password); + } + } + ); + } + URL website = new URL(urlString); + ReadableByteChannel rbc; + rbc = Channels.newChannel(website.openStream()); + FileOutputStream fos = new FileOutputStream(destination); + fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); + fos.close(); + rbc.close(); + } +} diff --git a/example/.mvn/wrapper/maven-wrapper.jar b/example/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..2cc7d4a55c0cd0092912bf49ae38b3a9e3fd0054 GIT binary patch literal 50710 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 EfAhh=DF6Tf literal 0 HcmV?d00001 diff --git a/example/.mvn/wrapper/maven-wrapper.properties b/example/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..642d572 --- /dev/null +++ b/example/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar diff --git a/example/.prettierrc.yaml b/example/.prettierrc.yaml new file mode 100644 index 0000000..04450cb --- /dev/null +++ b/example/.prettierrc.yaml @@ -0,0 +1,7 @@ +trailingComma: "none" +useTabs: false +tabWidth: 4 +semi: true +singleQuote: false +printWidth: 120 + diff --git a/example/AUTHORS.md b/example/AUTHORS.md new file mode 100644 index 0000000..4eb1014 --- /dev/null +++ b/example/AUTHORS.md @@ -0,0 +1,7 @@ +# Contributors + +Here is the list of people involved in creating the example application. + + Juri Letberg + Mart Sõmermaa + Martin Ott diff --git a/example/LICENSE b/example/LICENSE new file mode 100644 index 0000000..326ac32 --- /dev/null +++ b/example/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020-2023 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. diff --git a/example/README.md b/example/README.md new file mode 100644 index 0000000..169abb2 --- /dev/null +++ b/example/README.md @@ -0,0 +1,283 @@ +# Web eID Spring Boot example + +![European Regional Development Fund](https://github.com/open-eid/DigiDoc4-Client/blob/master/client/images/EL_Regionaalarengu_Fond.png) + +This project is an example Spring Boot web application that shows how to implement strong authentication +and digital signing with electronic ID smart cards using Web eID. + +More information about the Web eID project is available on the project [website](https://web-eid.eu/), which is served by this application. + +## Quickstart + +Complete the steps below to run the example application in order to test authentication and digital signing with Web eID. + +### 1. Setup HTTPS + +Web eID only works over a HTTPS connection with a trusted HTTPS certificate. +You can either setup a reverse HTTPS proxy during development or, alternatively, configure +HTTPS support directly in the bundled web server. HTTPS configuration is described in more detail in section _[HTTPS support](#https-support)_ below. + +You can use solutions like [_ngrok_](https://ngrok.com/), [_localtunnel_](https://theboroer.github.io/localtunnel-www/), or any other reverse HTTPS proxy tool. For example, with _ngrok_, download and run it in a terminal window by providing the protocol and the Spring Boot application port arguments as follows: + + ngrok http 8080 + +### 2. Configure the origin URL + +One crucial step of the Web eID authentication token validation algorithm is verifying the token signature. The value that is signed contains the site origin URL (the URL serving the web application) to protect against man-in-the-middle attacks. Hence the site origin URL must be configured in application settings. + +To configure the origin URL, copy and paste the HTTPS URL from _ngrok_ output in step 1 into the `local-origin` field in the profile-specific settings file `src/main/resources/application-{dev,prod}.yaml` as follows: + +```yaml +web-eid-auth-token: + validation: + local-origin: "https://<>" +``` + +**Note that the origin URL must not end with a slash `/`**. + +### 3. Configure the trusted certificate authority certificates + +The algorithm, which performs the validation of the Web eID authentication token, needs to know which intermediate certificate authorities (CA) are trusted to issue the eID authentication certificates. CA certificates are loaded either from `.cer` files in the profile-specific subdirectory of the [`certs` resource directory](src/main/resources/certs) or the [truststore file](src/main/resources/certs/prod/trusted_certificates.jks). By default, Estonian eID test CA certificates are included in the `dev` profile and production CA certificates in the `prod` profile. + +In case you need to provide your own CA certificates, either add the `.cer` files to the `src/main/resources/certs/{dev,prod}` profile-specific directory or add the certificates to the truststore file. + +### 4. Choose either the `dev` or `prod` profile + +If you have a test eID card, use the `dev` profile. In this case access to paid services is not required, but you need to upload the authentication and signing certificates of the test card to the test OCSP responder database as described in section _[Using DigiDoc4j in test mode with the `dev` profile](#using-digidoc4j-in-test-mode-with-the-dev-profile)_ below. The `dev` profile is activated by default. + +If you only have a production eID card, use the `prod` profile. You can still test authentication without further configuration; however, for digital signing to work, you need access to a paid timestamping service as described in section [_Using DigiDoc4j in production mode with the `prod` profile_](#using-digidoc4j-in-production-mode-with-the-prod-profile) below. + +You can specify the profile as a command-line argument to the Maven wrapper command `./mvnw`, for example `./mvnw -Pprod ...`. + +### 5. Run the application + +Spring Boot web applications can be run from the command-line. You need to have the Java Development Kit 17 installed for building the application package and running the application. + +Build and run the application with the following command in a terminal window: + +```sh +./mvnw spring-boot:run +``` + +This will activate the default `dev` profile and launch the built-in Tomcat web server on port 8080 that was forwarded using _ngrok_ in step 1. + +If you want to use the `prod` profile, build and run the application with the following command: + +```sh +./mvnw -Pprod -Dspring-boot.run.profiles=prod spring-boot:run +``` + +When the application has started, open the _ngrok_ HTTPS URL in your preferred web browser and follow instructions on the front page. + +## Table of contents + +* [Quickstart](#quickstart) +* [Overview of the project](#overview-of-the-project) + + [Overview of the source code](#overview-of-the-source-code) + + [Configuration](#configuration) + + [Integration with Web eID components](#integration-with-web-eid-components) + - [Using local version of the validation library](#using-local-version-of-the-validation-library) + + [Integration with DigiDoc4j components](#integration-with-digidoc4j-components) + - [Using the Certificates' *Authority Information Access* (AIA) extension in DigiDoc4j](#using-the-certificates-authority-information-access-aia-extension-in-digidoc4j) + - [Using DigiDoc4j in test mode with the `dev` profile](#using-digidoc4j-in-test-mode-with-the-dev-profile) + - [Using DigiDoc4j in production mode with the `prod` profile](#using-digidoc4j-in-production-mode-with-the-prod-profile) + + [Stateful and stateless authentication](#stateful-and-stateless-authentication) + + [Assuring that the signing and authentication certificate subjects match](#assuring-that-the-signing-and-authentication-certificate-subjects-match) +* [HTTPS support](#https-support) + + [How to verify that HTTPS is configured properly](#how-to-verify-that-https-is-configured-properly) +* [Deployment](#deployment) +* [Frequently asked questions](#frequently-asked-questions) + + [Why do I sometimes get the 403 Forbidden response during authentication?](#why-do-i-sometimes-get-the-403-forbidden-response-during-authentication) + + [Why do I get the "Access denied to TSP service" error?](#why-do-i-get-the-access-denied-to-tsp-service-error) + + [Why can I not use my test ID card?](#why-can-i-not-use-my-test-id-card) + + [Why do I get the 401 Unauthorized "Authentication failed: Web eID token validation failed" response during authentication?](#why-do-i-get-the-401-unauthorized-authentication-failed-web-eid-token-validation-failed-response-during-authentication) + +## Overview of the project + +This repository contains the code of a minimal Spring Boot web application that demonstrates how to use Web eID for strong authentication and digital signing. It makes use of the following technologies: + +- Spring Web MVC with REST support, +- the Thymeleaf template engine, +- Spring Security, +- the Web eID authentication token validation library [_web-eid-authtoken-validation-java_](https://github.com/web-eid/web-eid-authtoken-validation-java), +- the Web eID JavaScript library [_web-eid.js_](https://github.com/web-eid/web-eid.js), +- the digital signing library [_DigiDoc4j_](https://github.com/open-eid/digidoc4j). + +The project uses Maven for managing the dependencies and building the application. Maven project configuration file `pom.xml` is in the root of the project. + +There is also a Docker Compose configuration file `docker-compose.yml` in the root of the project for building the application Docker image as described in section [_Deployment_](#deployment) below. + +### Overview of the source code + +The source code folder `src` contains the application source code and resources in the `main` subdirectory and tests in the `test` subdirectory. + +The `src/main/java/eu/webeid/example` directory contains the Spring Boot application Java class and the following subdirectories: + +- `config`: Spring and HTTP security configuration, Web eID authentication token validation library configuration, trusted CA certificates loading etc, +- `security`: Web eID authentication token validation library integration with Spring Security via an `AuthenticationProvider` and `AuthenticationProcessingFilter`, +- `service`: Web eID signing service implementation that uses DigiDoc4j, and DigiDoc4j runtime configuration, +- `web`: Spring Web MVC controller for the welcome page and Spring Web REST controllers that provide endpoints + - for getting the challenge nonce used by the authentication token validation library, + - for digital signing. + +The `src/resources` directory contains the resources used by the application: + +- `application.yaml`: main configuration file that is shared by all profiles, +- `application-{dev,prod}.yaml`: profile-specific configuration files, +- `application.properties`: activates the `dev` profile by default, +- `certs`: CA certificates in profile-specific subdirectories, +- `static`: web server static content, including CSS and JavaScript files, +- `templates`: Thymeleaf templates. + +The `src/tests` directory contains the application test suite. The most important test is the `WebApplicationTest.testHappyFlow_LoginPrepareSignDownload()` method that tests the full authentication and digital signing workflow. + +### Configuration + +As described in section [_4. Choose either the `dev` or `prod` profile_](#4-choose-either-the-dev-or-prod-profile) above, the application has two different configuration profiles: `dev` profile for running the application in development mode and `prod` profile for production mode. The `dev` profile is activated by default. + +The profile-specific configuration files `src/main/resources/application-{dev,prod}.yaml` contain the `web-eid-auth-token.validation.use-digidoc4j-prod-configuration` setting that configures DigiDoc4j either in test or production mode, and a setting for configuring the origin URL as described in section [_2. Configure the origin URL_](#2-configure-the-origin-url) above. Additionally, the `web-eid-auth-token.validation.truststore-password` setting specifies the truststore password used in the `prod` profile. + +The main configuration file `src/main/resources/application.yaml` is shared by all profiles and contains logging configuration and settings that make the session cookie secure behind a reverse proxy as described in section [_HTTPS support_](#https-support) below. + +Besides configuration settings, the trusted certificate authority certificates may need to be configured as described in section [_3. Configure the trusted certificate authority certificates_](#3-configure-the-trusted-certificate-authority-certificates) above. + +Spring Security has CSRF protection enabled by default. Web eID requires CSRF protection. + +### Integration with Web eID components + +Detailed overview of Java code changes required for integrating Web eID authentication token validation is available in the [_web-eid-authtoken-validation-java_ library README](../README.md). There are instructions for configuring the nonce generator, trusted certificate authority certificates, authentication token validator, Spring Security authentication integration and REST endpoints. The corresponding Java code is in the `src/main/java/eu/webeid/example/{config,security,web/rest}` directories. + +A similar overview of JavaScript and HTML code changes required for authentication and digital signing with Web eID is available in the [web-eid.js library README](https://github.com/web-eid/web-eid.js/blob/main/README.md). The corresponding JavaScript and HTML code is in the `src/resources/{static,templates}` directories. + +#### Using local version of the validation library +If you want to run the application with the local version of the `authtoken-validation` library you have to first build it and ensure that you have the .jar file in the `./target` folder. +Then, install the `authtoken-validation` library as `authtoken-validation-local` into the Maven local repository with the following command: +```bash +.\mvnw install:install-file -Dfile="..\target\authtoken-validation-.jar" -DgroupId="eu.webeid.security" -DartifactId=authtoken-validation-local -Dversion="" -Dpackaging=jar +``` +Remember to change the `` to the appropriate version number of the .jar package. +Then change the `pom.xml` to use the `authtoken-validation-local` version instead by changing the dependency as follows: +```xml + + eu.webeid.security + authtoken-validation-local + ${webeid.version} + +``` +This way, you can switch back to the official release by removing the `-local` part from the `artifactId` without requiring to remove the package from the Maven local repository. + +### Integration with DigiDoc4j components + +Java code examples that show how to create and sign data containers that hold signed file objects and digital signatures is available in the [DigiDoc4j wiki](https://github.com/open-eid/digidoc4j/wiki/Examples-of-using-it). Further information and links to the API documentation is available in the project [README](https://github.com/open-eid/digidoc4j/blob/master/README.md). The corresponding Java code is in the `src/main/java/eu/webeid/example/{service,web/rest}` directories. + +#### Using the Certificates' _Authority Information Access_ (AIA) extension in DigiDoc4j + +In the `SigningService` constructor we have configured DigiDoc4j to use the AIA extension that contains the certificates’ OCSP service location with `signingConfiguration.setPreferAiaOcsp(true)`. Note that there may be limitations to using AIA URLs during signing as the services behind these URLs provide different security and SLA guarantees than dedicated OCSP services, so you should consider using a dedicated OCSP service instead. See the instructions in DigiDoc4j documentation and also the [corresponding section in _web-eid-authtoken-validation-java_ README](https://github.com/web-eid/web-eid-authtoken-validation-java/blob/main/README.md#certificates-authority-information-access-aia-extension). + +#### Using DigiDoc4j in test mode with the `dev` profile + +When using DigiDoc4j in test mode with a test eID card, you need to upload the corresponding authentication and signing certificates to the _demo.sk.ee_ OCSP responder database at https://demo.sk.ee/upload_cert/index.php, according to instructions on the webpage. You can choose the certificate status during upload, so you can also test bad statuses. + +#### Using DigiDoc4j in production mode with the `prod` profile + +When using DigiDoc4j in production mode, you need access to a paid timestamping service by registering your server's IP address with the service provider. Besides that, you may want to use a paid OCSP service for legal and reliability reasons as described above. + +### Stateful and stateless authentication + +In the current example we use the classical stateful Spring Security session cookie-based authentication mechanism, where a cookie that contains the user session ID is set during successful login and session data is stored at sever side. Cookie-based authentication must be protected against cross-site request forgery (CSRF) attacks and extra measures must be taken to secure the cookies by serving them only over HTTPS and setting the _HttpOnly_, _Secure_ and _SameSite_ attributes. + +A common alternative to stateful authentication is stateless authentication with JSON Web Tokens (JWT) or secure cookie sessions where the session data resides at client side browser and is either signed or encrypted. Secure cookie sessions are described in [RFC 6896](https://datatracker.ietf.org/doc/html/rfc6896) and in the following [article about secure cookie-based Spring Security sessions](https://www.innoq.com/en/blog/cookie-based-spring-security-session/). Usage of both an anonymous session and a cache is required to store the challenge nonce and the time it was issued before the user is authenticated. The anonymous session must be used for protection against [forged login attacks](https://en.wikipedia.org/wiki/Cross-site_request_forgery#Forging_login_requests) by guaranteeing that the authentication token is received from the same browser to which the corresponding challenge nonce was issued. The cache must be used for protection against replay attacks by guaranteeing that each authentication token can be used exactly once. + +### Assuring that the signing and authentication certificate subjects match + +It is usually required to verify that the signing certificate subject matches the authentication certificate subject by assuring that both ID codes match. This check is implemented at the beginning of the `SigningService.prepareContainer()` method. + +## HTTPS support + +There are two ways of adding HTTPS support to a Spring Boot application: + +1. enable HTTPS support directly in the bundled Tomcat server by configuring + the `server.ssl.*` properties, + +2. use a reverse proxy server that handles TLS termination and communicates + with the Spring Boot application over a local HTTP socket. + +The first approach is straightforward and documented in the [official documentation](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#howto.webserver.configure-ssl). + +The second approach, running behind a reverse proxy, is common in enterprise +deployments and the configuration is more complex. We assume this setup in the +current project and document it in this section to facilitate production deployment. + +Enabling HTTPS support when running behind a reverse proxy server requires +configuration of both the proxy server and the Spring Boot application. + +The proxy server must pass the `Host:` line from the incoming request to the +proxied application and set the `X-Forwarded-*` headers to inform the +application that it runs behind a reverse proxy. Here is example configuration +for the Apache web server: + + + ProxyPreserveHost On + ProxyPass http://localhost:8380/ + ProxyPassReverse http://localhost:8380/ + RequestHeader set X-Forwarded-Proto https + RequestHeader set X-Forwarded-Port 443 + + +The Spring Boot application turns on proxied HTTPS support in the bundled +Tomcat web server automatically if it detects the presence of the +`X-Forwarded-*` headers and the following settings are configured in +`application.properties`: + + server.forward-headers-strategy=native + server.tomcat.remote-ip-header=x-forwarded-for + server.tomcat.protocol-header=x-forwarded-proto + +These settings are already enabled in the main configuration file `application.yaml`. See chapter +[9.3.12](https://docs.spring.io/spring-boot/docs/2.2.5.RELEASE/reference/htmlsingle/#howto-use-behind-a-proxy-server) +and +[9.14.3](https://docs.spring.io/spring-boot/docs/2.2.5.RELEASE/reference/htmlsingle/#howto-enable-https) +in the official documentation for further details. + +### How to verify that HTTPS is configured properly + +When HTTPS support is configured properly, all responses will have the HTTP +Strict Transport Security (HSTS) header and the `JSESSIONID` session cookie has the +`Secure` attribute set. + +## Deployment + +A Docker Compose configuration file `docker-compose.yml` is available in the root of the project for packaging the application in a Docker image so that it can be deployed with a container enginge. + +Build the Docker image with [Jib](https://github.com/GoogleContainerTools/jib) as follows: + +```sh +./mvnw -Pprod package com.google.cloud.tools:jib-maven-plugin:dockerBuild +``` + +and deploy with Docker Compose as follows: + +```sh +docker-compose up -d +``` + +The application will run in production mode with `prod` profile settings. + +## Frequently asked questions + +### Why do I sometimes get the `403 Forbidden` response during authentication? + +This is most likely caused by an expired CSRF token. Please refresh the page and try again. + +### Why do I get the `"Access denied to TSP service"` error? + +This error means that access was denied to the timestamping service. You are running in production mode with the `prod` profile and need to register for paid access to the timestamping service as described in section [_Using DigiDoc4j in production mode with the `prod` profile_](#using-digidoc4j-in-production-mode-with-the-prod-profile) above. + +### Why can I not use my test ID card? + +When running the application with the `dev` profile in test mode, you need to upload the corresponding authentication and signing certificates to the _demo.sk.ee_ OCSP responder database at , according to instructions on the webpage. + +### Why do I get the `401 Unauthorized "Authentication failed: Web eID token validation failed"` response during authentication? + +One possible reason is that you are using the test ID card on a site that is running in production mode or, vice-versa, a real ID card on a site that is running in test mode; or any other ID card whose certificate authority has not been added to the list of trusted certificate authorities. There will be a `CertificateNotTrustedException` in the logs in this case. + diff --git a/example/docker-compose.yml b/example/docker-compose.yml new file mode 100644 index 0000000..239d19a --- /dev/null +++ b/example/docker-compose.yml @@ -0,0 +1,10 @@ +version: '2' +services: + web-eid-springboot-example: + image: web-eid-springboot-example:3.1.0 + restart: always + environment: + JAVA_TOOL_OPTIONS: '-Dspring.profiles.active=prod' + ports: + - '127.0.0.1:8380:8080' + diff --git a/example/mvnw b/example/mvnw new file mode 100644 index 0000000..a16b543 --- /dev/null +++ b/example/mvnw @@ -0,0 +1,310 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + if [ -n "$MVNW_REPOURL" ]; then + jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + fi + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + fi + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget "$jarUrl" -O "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl -o "$wrapperJarPath" "$jarUrl" -f + else + curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + fi + + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaClass=`cygpath --path --windows "$javaClass"` + fi + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/example/mvnw.cmd b/example/mvnw.cmd new file mode 100644 index 0000000..c8d4337 --- /dev/null +++ b/example/mvnw.cmd @@ -0,0 +1,182 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + +FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/example/pom.xml b/example/pom.xml new file mode 100644 index 0000000..f5a6f03 --- /dev/null +++ b/example/pom.xml @@ -0,0 +1,137 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.3.4 + + + eu.webeid.example + web-eid-springboot-example + 3.1.0 + web-eid-springboot-example + Example Spring Boot application that demonstrates how to use Web eID for authentication and digital + signing + + + + 17 + 3.2.5 + 3.1.0 + 5.3.1 + 1.44 + 3.4.2 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + + org.digidoc4j + digidoc4j + ${digidoc4j.version} + + + eu.webeid.security + authtoken-validation + + + ${webeid.version} + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + org.jmockit + jmockit + ${jmockit.version} + test + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + maven-surefire-plugin + ${maven-surefire-plugin.version} + + + -javaagent:${settings.localRepository}/org/jmockit/jmockit/${jmockit.version}/jmockit-${jmockit.version}.jar + + true + + + + com.google.cloud.tools + jib-maven-plugin + ${jib.version} + + + eclipse-temurin:${java.version}-jre-jammy + + + + + + + + + dev + + true + + + dev + + + + prod + + prod + + + + + + + gitlab + https://gitlab.com/api/v4/projects/19948337/packages/maven + + + + + diff --git a/example/scripts/deployment/.gitignore b/example/scripts/deployment/.gitignore new file mode 100644 index 0000000..af2ecbd --- /dev/null +++ b/example/scripts/deployment/.gitignore @@ -0,0 +1,2 @@ +*.swp +venv/ diff --git a/example/scripts/deployment/README.md b/example/scripts/deployment/README.md new file mode 100644 index 0000000..333b1e2 --- /dev/null +++ b/example/scripts/deployment/README.md @@ -0,0 +1,20 @@ +## Deploy new version to web-eid.eu server + +In the local machine: + +1. Setup virtualenv and install [Fabric](https://www.fabfile.org/) + + python3 -m venv venv + . venv/bin/activate # . venv/Scripts/activate on Windows + pip install -r requirements.txt + +2. Check the project path, server hostname, user and port in `fab.sh` + +4. Verify that Fabric is able to connect to the server by running `uname` + + ./fab.sh uname + +7. Deploy the project + + ./fab.sh deploy + diff --git a/example/scripts/deployment/fab.sh b/example/scripts/deployment/fab.sh new file mode 100644 index 0000000..85af577 --- /dev/null +++ b/example/scripts/deployment/fab.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +set -eu + +cd "$( dirname "$0" )" + +SERVER_ADDRESS=web-eid.eu +SERVER_USER=baron +SERVER_PORT=22 + +export WEBEID_DIR='web-eid/web-eid-springboot-example' +export WEBEID_DIR='web-eid/test-web-eid-springboot-example' + +. venv/bin/activate +fab -e -H ${SERVER_USER}@${SERVER_ADDRESS}:${SERVER_PORT} "$@" diff --git a/example/scripts/deployment/fabfile.py b/example/scripts/deployment/fabfile.py new file mode 100644 index 0000000..c4612a3 --- /dev/null +++ b/example/scripts/deployment/fabfile.py @@ -0,0 +1,18 @@ +import os + +from fabric import task + + +@task +def uname(c): + c.run('uname -a') + + +@task +def deploy(c): + with c.cd(os.getenv('WEBEID_DIR')): + c.run('git pull') + c.run('mvn clean package com.google.cloud.tools:jib-maven-plugin:dockerBuild') + c.run('docker-compose down') + c.run('docker-compose up -d') + diff --git a/example/scripts/deployment/requirements.txt b/example/scripts/deployment/requirements.txt new file mode 100644 index 0000000..50d35bb --- /dev/null +++ b/example/scripts/deployment/requirements.txt @@ -0,0 +1 @@ +fabric < 3 diff --git a/example/src/main/java/eu/webeid/example/WebEidSpringbootExampleApplication.java b/example/src/main/java/eu/webeid/example/WebEidSpringbootExampleApplication.java new file mode 100644 index 0000000..f82bac0 --- /dev/null +++ b/example/src/main/java/eu/webeid/example/WebEidSpringbootExampleApplication.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2020-2024 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. + */ + +package eu.webeid.example; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class WebEidSpringbootExampleApplication { + + public static void main(String[] args) { + SpringApplication.run(WebEidSpringbootExampleApplication.class, args); + } +} diff --git a/example/src/main/java/eu/webeid/example/config/ApplicationConfiguration.java b/example/src/main/java/eu/webeid/example/config/ApplicationConfiguration.java new file mode 100644 index 0000000..d93c942 --- /dev/null +++ b/example/src/main/java/eu/webeid/example/config/ApplicationConfiguration.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2020-2024 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. + */ + +package eu.webeid.example.config; + +import eu.webeid.example.security.AuthTokenDTOAuthenticationProvider; +import eu.webeid.example.security.WebEidAjaxLoginProcessingFilter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler; +import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +@EnableWebSecurity +@EnableMethodSecurity(securedEnabled = true) +public class ApplicationConfiguration implements WebMvcConfigurer { + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http, AuthTokenDTOAuthenticationProvider authTokenDTOAuthenticationProvider, AuthenticationConfiguration authConfig) throws Exception { + return http + .authenticationProvider(authTokenDTOAuthenticationProvider) + .addFilterBefore(new WebEidAjaxLoginProcessingFilter("/auth/login", authConfig.getAuthenticationManager()), + UsernamePasswordAuthenticationFilter.class) + .logout(logout -> logout.logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler())) + .headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin)) + .build(); + } + + @Override + public void addViewControllers(ViewControllerRegistry registry) { + registry.addViewController("/").setViewName("index"); + registry.addViewController("/welcome").setViewName("welcome"); + } + +} diff --git a/example/src/main/java/eu/webeid/example/config/SameSiteCookieConfiguration.java b/example/src/main/java/eu/webeid/example/config/SameSiteCookieConfiguration.java new file mode 100644 index 0000000..7940165 --- /dev/null +++ b/example/src/main/java/eu/webeid/example/config/SameSiteCookieConfiguration.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2020-2024 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. + */ + +package eu.webeid.example.config; + +import org.apache.tomcat.util.http.Rfc6265CookieProcessor; +import org.springframework.boot.web.embedded.tomcat.TomcatContextCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class SameSiteCookieConfiguration implements WebMvcConfigurer { + + @Bean + public TomcatContextCustomizer configureSameSiteCookies() { + return context -> { + final Rfc6265CookieProcessor cookieProcessor = new Rfc6265CookieProcessor(); + cookieProcessor.setSameSiteCookies("strict"); + context.setCookieProcessor(cookieProcessor); + }; + } +} diff --git a/example/src/main/java/eu/webeid/example/config/SessionBackedChallengeNonceStore.java b/example/src/main/java/eu/webeid/example/config/SessionBackedChallengeNonceStore.java new file mode 100644 index 0000000..cb4654d --- /dev/null +++ b/example/src/main/java/eu/webeid/example/config/SessionBackedChallengeNonceStore.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2020-2024 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. + */ + +package eu.webeid.example.config; + +import org.springframework.beans.factory.ObjectFactory; +import eu.webeid.security.challenge.ChallengeNonce; +import eu.webeid.security.challenge.ChallengeNonceStore; + +import jakarta.servlet.http.HttpSession; + +public class SessionBackedChallengeNonceStore implements ChallengeNonceStore { + + private static final String CHALLENGE_NONCE_KEY = "challenge-nonce"; + + final ObjectFactory httpSessionFactory; + + public SessionBackedChallengeNonceStore(ObjectFactory httpSessionFactory) { + this.httpSessionFactory = httpSessionFactory; + } + + @Override + public void put(ChallengeNonce challengeNonce) { + currentSession().setAttribute(CHALLENGE_NONCE_KEY, challengeNonce); + } + + @Override + public ChallengeNonce getAndRemoveImpl() { + final ChallengeNonce challengeNonce = (ChallengeNonce) currentSession().getAttribute(CHALLENGE_NONCE_KEY); + currentSession().removeAttribute(CHALLENGE_NONCE_KEY); + return challengeNonce; + } + + private HttpSession currentSession() { + return httpSessionFactory.getObject(); + } + +} diff --git a/example/src/main/java/eu/webeid/example/config/ValidationConfiguration.java b/example/src/main/java/eu/webeid/example/config/ValidationConfiguration.java new file mode 100644 index 0000000..dbe21ee --- /dev/null +++ b/example/src/main/java/eu/webeid/example/config/ValidationConfiguration.java @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2020-2024 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. + */ + +package eu.webeid.example.config; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.ObjectFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import eu.webeid.security.challenge.ChallengeNonceGenerator; +import eu.webeid.security.challenge.ChallengeNonceGeneratorBuilder; +import eu.webeid.security.challenge.ChallengeNonceStore; +import eu.webeid.security.exceptions.JceException; +import eu.webeid.security.validator.AuthTokenValidator; +import eu.webeid.security.validator.AuthTokenValidatorBuilder; + +import jakarta.servlet.http.HttpSession; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; + +@Configuration +public class ValidationConfiguration { + + private static final Logger LOG = LoggerFactory.getLogger(ValidationConfiguration.class); + + private static final long CHALLENGE_NONCE_TTL_MINUTES = 5; + private static final String CERTS_RESOURCE_PATH = "/certs/"; + public static final String TRUSTED_CERTIFICATES_JKS = "trusted_certificates.jks"; + + @Value("${spring.profiles.active}") + private String activeProfile; + + @Bean + public ChallengeNonceStore sessionBasedChallengeNonceStore(ObjectFactory httpSessionFactory) { + return new SessionBackedChallengeNonceStore(httpSessionFactory); + } + + @Bean + public ChallengeNonceGenerator generator(ChallengeNonceStore challengeNonceStore) { + return new ChallengeNonceGeneratorBuilder() + .withNonceTtl(Duration.ofMinutes(CHALLENGE_NONCE_TTL_MINUTES)) + .withChallengeNonceStore(challengeNonceStore) + .build(); + } + + @Bean + public X509Certificate[] loadTrustedCACertificatesFromCerFiles() { + List caCertificates = new ArrayList<>(); + + try { + CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); + + PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); + Resource[] resources = resolver.getResources(CERTS_RESOURCE_PATH + activeProfile + "/*.cer"); + + for (Resource resource : resources) { + X509Certificate caCertificate = (X509Certificate) certFactory.generateCertificate(resource.getInputStream()); + caCertificates.add(caCertificate); + } + + } catch (CertificateException | IOException e) { + throw new RuntimeException("Error initializing trusted CA certificates.", e); + } + + return caCertificates.toArray(new X509Certificate[0]); + } + + @Bean + public X509Certificate[] loadTrustedCACertificatesFromTrustStore() { + List caCertificates = new ArrayList<>(); + + try (InputStream is = ValidationConfiguration.class.getResourceAsStream(CERTS_RESOURCE_PATH + activeProfile + "/" + TRUSTED_CERTIFICATES_JKS)) { + if (is == null) { + LOG.info("Truststore file {} not found for {} profile", TRUSTED_CERTIFICATES_JKS, activeProfile); + return new X509Certificate[0]; + } + KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType()); + keystore.load(is, yamlConfig().getTrustStorePassword().toCharArray()); + Enumeration aliases = keystore.aliases(); + while (aliases.hasMoreElements()) { + String alias = aliases.nextElement(); + X509Certificate certificate = (X509Certificate) keystore.getCertificate(alias); + caCertificates.add(certificate); + } + } catch (IOException | CertificateException | KeyStoreException | NoSuchAlgorithmException e) { + throw new RuntimeException("Error initializing trusted CA certificates from trust store.", e); + } + + return caCertificates.toArray(new X509Certificate[0]); + } + + @Bean + public AuthTokenValidator validator() { + try { + return new AuthTokenValidatorBuilder() + .withSiteOrigin(URI.create(yamlConfig().getLocalOrigin())) + .withTrustedCertificateAuthorities(loadTrustedCACertificatesFromCerFiles()) + .withTrustedCertificateAuthorities(loadTrustedCACertificatesFromTrustStore()) + .build(); + } catch (JceException e) { + throw new RuntimeException("Error building the Web eID auth token validator.", e); + } + } + + @Bean + public YAMLConfig yamlConfig() { + return new YAMLConfig(); + } + +} diff --git a/example/src/main/java/eu/webeid/example/config/YAMLConfig.java b/example/src/main/java/eu/webeid/example/config/YAMLConfig.java new file mode 100644 index 0000000..35905f0 --- /dev/null +++ b/example/src/main/java/eu/webeid/example/config/YAMLConfig.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2020-2024 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. + */ + +package eu.webeid.example.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableConfigurationProperties +@ConfigurationProperties(prefix = "web-eid-auth-token.validation") +public class YAMLConfig { + + @Value("local-origin") + private String localOrigin; + + @Value("site-cert-hash") + private String siteCertHash; + + @Value("truststore-password") + private String trustStorePassword; + + @Value("#{new Boolean('${web-eid-auth-token.validation.use-digidoc4j-prod-configuration}'.trim())}") + private Boolean useDigiDoc4jProdConfiguration; + + public String getLocalOrigin() { + return localOrigin; + } + + public void setLocalOrigin(String localOrigin) { + this.localOrigin = localOrigin; + } + + public String getSiteCertHash() { + return siteCertHash; + } + + public void setSiteCertHash(String siteCertHash) { + this.siteCertHash = siteCertHash; + } + + public String getTrustStorePassword() { + return trustStorePassword; + } + + public void setTrustStorePassword(String trustStorePassword) { + this.trustStorePassword = trustStorePassword; + } + + public boolean getUseDigiDoc4jProdConfiguration() { + return useDigiDoc4jProdConfiguration; + } + + public void setUseDigiDoc4jProdConfiguration(boolean useDigiDoc4jProdConfiguration) { + this.useDigiDoc4jProdConfiguration = useDigiDoc4jProdConfiguration; + } +} diff --git a/example/src/main/java/eu/webeid/example/security/AuthTokenDTOAuthenticationProvider.java b/example/src/main/java/eu/webeid/example/security/AuthTokenDTOAuthenticationProvider.java new file mode 100644 index 0000000..9965ff3 --- /dev/null +++ b/example/src/main/java/eu/webeid/example/security/AuthTokenDTOAuthenticationProvider.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2020-2024 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. + */ + +package eu.webeid.example.security; + +import eu.webeid.example.security.dto.AuthTokenDTO; +import eu.webeid.security.authtoken.WebEidAuthToken; +import eu.webeid.security.challenge.ChallengeNonceStore; +import eu.webeid.security.exceptions.AuthTokenException; +import eu.webeid.security.validator.AuthTokenValidator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.AuthenticationServiceException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; +import org.springframework.stereotype.Component; + +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.util.Collections; +import java.util.List; + +/** + * Parses JWT from token string inside AuthTokenDTO and attempts authentication. + */ +@Component +public class AuthTokenDTOAuthenticationProvider implements AuthenticationProvider { + public static final String ROLE_USER = "ROLE_USER"; + private static final GrantedAuthority USER_ROLE = new SimpleGrantedAuthority(ROLE_USER); + + private static final Logger LOG = LoggerFactory.getLogger(AuthTokenDTOAuthenticationProvider.class); + + private final AuthTokenValidator tokenValidator; + private final ChallengeNonceStore challengeNonceStore; + + public AuthTokenDTOAuthenticationProvider(AuthTokenValidator tokenValidator, ChallengeNonceStore challengeNonceStore) { + this.tokenValidator = tokenValidator; + this.challengeNonceStore = challengeNonceStore; + } + + @Override + public Authentication authenticate(Authentication auth) throws AuthenticationException { + LOG.info("authenticate(): {}", auth); + + final PreAuthenticatedAuthenticationToken authentication = (PreAuthenticatedAuthenticationToken) auth; + final WebEidAuthToken authToken = ((AuthTokenDTO) authentication.getCredentials()).getToken(); + + final List authorities = Collections.singletonList(USER_ROLE); + + try { + final String nonce = challengeNonceStore.getAndRemove().getBase64EncodedNonce(); + final X509Certificate userCertificate = tokenValidator.validate(authToken, nonce); + return WebEidAuthentication.fromCertificate(userCertificate, authorities); + } catch (AuthTokenException e) { + throw new AuthenticationServiceException("Web eID token validation failed", e); + } catch (CertificateEncodingException e) { + throw new AuthenticationServiceException("Web eID token has incorrect certificate subject fields", e); + } + } + + @Override + public boolean supports(Class authentication) { + LOG.info("supports(): {}", authentication); + return PreAuthenticatedAuthenticationToken.class.equals(authentication); + } + +} diff --git a/example/src/main/java/eu/webeid/example/security/WebEidAjaxLoginProcessingFilter.java b/example/src/main/java/eu/webeid/example/security/WebEidAjaxLoginProcessingFilter.java new file mode 100644 index 0000000..cc47f86 --- /dev/null +++ b/example/src/main/java/eu/webeid/example/security/WebEidAjaxLoginProcessingFilter.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2020-2024 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. + */ + +package eu.webeid.example.security; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; +import eu.webeid.example.security.ajax.AjaxAuthenticationFailureHandler; +import eu.webeid.example.security.ajax.AjaxAuthenticationSuccessHandler; +import eu.webeid.example.security.dto.AuthTokenDTO; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.AuthenticationServiceException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; +import org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy; +import org.springframework.security.web.context.HttpSessionSecurityContextRepository; +import org.springframework.security.web.context.SecurityContextRepository; + +import java.io.IOException; + +public class WebEidAjaxLoginProcessingFilter extends AbstractAuthenticationProcessingFilter { + private static final Logger LOG = LoggerFactory.getLogger(WebEidAjaxLoginProcessingFilter.class); + private final ObjectReader OBJECT_READER = new ObjectMapper().readerFor(AuthTokenDTO.class); + private final SecurityContextRepository securityContextRepository; + + public WebEidAjaxLoginProcessingFilter( + String defaultFilterProcessesUrl, + AuthenticationManager authenticationManager + ) { + super(defaultFilterProcessesUrl); + this.setAuthenticationManager(authenticationManager); + this.setAuthenticationSuccessHandler(new AjaxAuthenticationSuccessHandler()); + this.setAuthenticationFailureHandler(new AjaxAuthenticationFailureHandler()); + setSessionAuthenticationStrategy(new SessionFixationProtectionStrategy()); + this.securityContextRepository = new HttpSessionSecurityContextRepository(); + } + + @Override + public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) + throws AuthenticationException, IOException { + if (!HttpMethod.POST.name().equals(request.getMethod())) { + LOG.warn("HttpMethod not supported: {}", request.getMethod()); + throw new AuthenticationServiceException("HttpMethod not supported: " + request.getMethod()); + } + final String contentType = request.getHeader("Content-type"); + if (contentType == null || !contentType.startsWith("application/json")) { + LOG.warn("Content type not supported: {}", contentType); + throw new AuthenticationServiceException("Content type not supported: " + contentType); + } + + LOG.info("attemptAuthentication(): Reading request body"); + final AuthTokenDTO authTokenDTO = OBJECT_READER.readValue(request.getReader()); + LOG.info("attemptAuthentication(): Creating token"); + final PreAuthenticatedAuthenticationToken token = new PreAuthenticatedAuthenticationToken(null, authTokenDTO); + LOG.info("attemptAuthentication(): Calling authentication manager"); + return getAuthenticationManager().authenticate(token); + } + + @Override + protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { + super.successfulAuthentication(request, response, chain, authResult); + securityContextRepository.saveContext(SecurityContextHolder.getContext(), request, response); + } +} diff --git a/example/src/main/java/eu/webeid/example/security/WebEidAuthentication.java b/example/src/main/java/eu/webeid/example/security/WebEidAuthentication.java new file mode 100644 index 0000000..c039007 --- /dev/null +++ b/example/src/main/java/eu/webeid/example/security/WebEidAuthentication.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2020-2024 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. + */ + +package eu.webeid.example.security; + +import eu.webeid.security.certificate.CertificateData; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; + +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +public class WebEidAuthentication extends PreAuthenticatedAuthenticationToken implements Authentication { + + private final String idCode; + + public static Authentication fromCertificate(X509Certificate userCertificate, List authorities) throws CertificateEncodingException { + final String principalName = getPrincipalNameFromCertificate(userCertificate); + final String idCode = CertificateData.getSubjectIdCode(userCertificate) + .orElseThrow(() -> new CertificateEncodingException("Certificate does not contain subject ID code")); + return new WebEidAuthentication(principalName, idCode, authorities); + } + + public String getIdCode() { + return idCode; + } + + private WebEidAuthentication(String principalName, String idCode, List authorities) { + super(principalName, idCode, authorities); + this.idCode = idCode; + } + + private static String getPrincipalNameFromCertificate(X509Certificate userCertificate) throws CertificateEncodingException { + final Optional givenName = CertificateData.getSubjectGivenName(userCertificate); + final Optional surname = CertificateData.getSubjectSurname(userCertificate); + + if (givenName.isPresent() && surname.isPresent()) { + return givenName.get() + ' ' + surname.get(); + } else { + // Organization certificates do not have given name and surname fields. + return CertificateData.getSubjectCN(userCertificate) + .orElseThrow(() -> new CertificateEncodingException("Certificate does not contain subject CN")); + } + } + + @Override + public boolean equals(Object o) { + if (!super.equals(o)) return false; + WebEidAuthentication that = (WebEidAuthentication) o; + return Objects.equals(idCode, that.idCode); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), idCode); + } +} diff --git a/example/src/main/java/eu/webeid/example/security/ajax/AjaxAuthenticationFailureHandler.java b/example/src/main/java/eu/webeid/example/security/ajax/AjaxAuthenticationFailureHandler.java new file mode 100644 index 0000000..647698f --- /dev/null +++ b/example/src/main/java/eu/webeid/example/security/ajax/AjaxAuthenticationFailureHandler.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2020-2024 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. + */ + +package eu.webeid.example.security.ajax; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import java.io.IOException; + +public class AjaxAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler { + private static final Logger LOG = LoggerFactory.getLogger(AjaxAuthenticationFailureHandler.class); + + public static final String AUTHENTICATION_FAILED = "Authentication failed: "; + + @Override + public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, + AuthenticationException exception) throws IOException { + final String message = AUTHENTICATION_FAILED + exception.getMessage(); + LOG.warn("onAuthenticationFailure(): exception {}, returning {} {}", + exception, + HttpServletResponse.SC_UNAUTHORIZED, + message); + final HttpSession session = request.getSession(false); + if (session != null) { + LOG.info("Invalidating session"); + session.invalidate(); + } + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, message); + } +} diff --git a/example/src/main/java/eu/webeid/example/security/ajax/AjaxAuthenticationSuccessHandler.java b/example/src/main/java/eu/webeid/example/security/ajax/AjaxAuthenticationSuccessHandler.java new file mode 100644 index 0000000..b545422 --- /dev/null +++ b/example/src/main/java/eu/webeid/example/security/ajax/AjaxAuthenticationSuccessHandler.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2020-2024 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. + */ + +package eu.webeid.example.security.ajax; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectWriter; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +/** + * Write custom response on having user successfully authenticated. + *

+ * This is not required in production application, but to demonstrate that + * authentication and authorization steps have been passed. + */ +@Component +public class AjaxAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { + private static final Logger LOG = LoggerFactory.getLogger(AjaxAuthenticationSuccessHandler.class); + + @Override + public void onAuthenticationSuccess( + HttpServletRequest request, + HttpServletResponse response, + Authentication authentication + ) + throws IOException { + LOG.info("onAuthenticationSuccess(): {}", authentication); + + response.setStatus(HttpServletResponse.SC_OK); + response.setHeader("Content-Type", "application/json; charset=utf-8"); + + response.getWriter().write(AuthSuccessDTO.asJson(authentication)); + } + + public static class AuthSuccessDTO { + private static final ObjectWriter OBJECT_WRITER = new ObjectMapper().writerFor(AuthSuccessDTO.class); + + @JsonProperty("sub") + private String sub; + + @JsonProperty("auth") + private String auth; + + public static String asJson(Authentication authentication) throws JsonProcessingException { + final AuthSuccessDTO dto = new AuthSuccessDTO(); + dto.sub = authentication.getName(); + dto.auth = authentication.getAuthorities().toString(); + return OBJECT_WRITER.writeValueAsString(dto); + } + } +} diff --git a/example/src/main/java/eu/webeid/example/security/dto/AuthTokenDTO.java b/example/src/main/java/eu/webeid/example/security/dto/AuthTokenDTO.java new file mode 100644 index 0000000..9321c4c --- /dev/null +++ b/example/src/main/java/eu/webeid/example/security/dto/AuthTokenDTO.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2020-2024 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. + */ + +package eu.webeid.example.security.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import eu.webeid.security.authtoken.WebEidAuthToken; + +public class AuthTokenDTO { + @JsonProperty("auth-token") + private WebEidAuthToken token; + + public WebEidAuthToken getToken() { + return token; + } + + public void setToken(WebEidAuthToken token) { + this.token = token; + } +} diff --git a/example/src/main/java/eu/webeid/example/service/SigningService.java b/example/src/main/java/eu/webeid/example/service/SigningService.java new file mode 100644 index 0000000..507b4b3 --- /dev/null +++ b/example/src/main/java/eu/webeid/example/service/SigningService.java @@ -0,0 +1,209 @@ +/* + * Copyright (c) 2020-2024 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. + */ + +package eu.webeid.example.service; + +import eu.webeid.example.config.YAMLConfig; +import eu.webeid.example.security.WebEidAuthentication; +import eu.webeid.example.service.dto.CertificateDTO; +import eu.webeid.example.service.dto.DigestDTO; +import eu.webeid.example.service.dto.FileDTO; +import eu.webeid.example.service.dto.SignatureDTO; +import eu.webeid.security.certificate.CertificateData; +import jakarta.servlet.http.HttpSession; +import jakarta.xml.bind.DatatypeConverter; +import org.apache.commons.io.FilenameUtils; +import org.digidoc4j.Configuration; +import org.digidoc4j.Container; +import org.digidoc4j.ContainerBuilder; +import org.digidoc4j.DataFile; +import org.digidoc4j.DataToSign; +import org.digidoc4j.DigestAlgorithm; +import org.digidoc4j.Signature; +import org.digidoc4j.SignatureBuilder; +import org.digidoc4j.SignatureProfile; +import org.digidoc4j.utils.TokenAlgorithmSupport; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.ObjectFactory; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.security.access.annotation.Secured; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.io.InputStream; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Objects; + +import static eu.webeid.example.security.AuthTokenDTOAuthenticationProvider.ROLE_USER; + +@Service +@Secured(ROLE_USER) +public class SigningService { + + private static final String SESSION_ATTR_FILE = "file-to-sign"; + private static final String SESSION_ATTR_CONTAINER = "container-to-sign"; + private static final String SESSION_ATTR_DATA = "data-to-sign"; + private static final Logger LOG = LoggerFactory.getLogger(SigningService.class); + private final Configuration signingConfiguration; + + private final ObjectFactory httpSessionFactory; + + public SigningService(ObjectFactory httpSessionFactory, YAMLConfig yamlConfig) { + this.httpSessionFactory = httpSessionFactory; + signingConfiguration = Configuration.of(yamlConfig.getUseDigiDoc4jProdConfiguration() ? + Configuration.Mode.PROD : Configuration.Mode.TEST); + // Use automatic AIA OCSP URL selection from certificate for signatures. + signingConfiguration.setPreferAiaOcsp(true); + } + + private HttpSession currentSession() { + return httpSessionFactory.getObject(); + } + + /** + * Prepares given container {@link Container} for the signature process. + * + * @param certificateDTO user's X.509 certificate + * @param authentication authenticated principal + * @return data to be signed + */ + public DigestDTO prepareContainer(CertificateDTO certificateDTO, WebEidAuthentication authentication) throws CertificateException, NoSuchAlgorithmException, IOException { + final X509Certificate certificate = certificateDTO.toX509Certificate(); + final String signingIdCode = CertificateData.getSubjectIdCode(certificate) + .orElseThrow(() -> new RuntimeException("Certificate does not contain subject ID code")); + if (!signingIdCode.equals(authentication.getIdCode())) { + throw new IllegalArgumentException("Authenticated subject ID code differs from " + + "signing certificate subject ID code"); + } + + final FileDTO fileDTO = FileDTO.getExampleForSigningFromResources(); + final Container containerToSign = getContainerToSign(fileDTO); + final String containerName = generateContainerName(fileDTO.getName()); + + currentSession().setAttribute(SESSION_ATTR_CONTAINER, containerToSign); + currentSession().setAttribute(SESSION_ATTR_FILE, fileDTO); + + LOG.info("Preparing container for signing for file '{}'", containerName); + + final DigestAlgorithm signatureDigestAlgorithm = TokenAlgorithmSupport.determineSignatureDigestAlgorithm(certificate); + final String digestAlgorithmName = signatureDigestAlgorithm.uri().getRef() + .toUpperCase().replace("SHA", "SHA-"); // SHA256 -> SHA-256 + if (!certificateDTO.getSupportedHashFunctionNames().contains(digestAlgorithmName)) { + throw new IllegalArgumentException("Determined signature digest algorithm '" + digestAlgorithmName + + "' is not supported. Supported algorithms are: " + String.join(", ", certificateDTO.getSupportedHashFunctionNames())); + } + + final DataToSign dataToSign = SignatureBuilder + .aSignature(containerToSign) + .withSignatureProfile(SignatureProfile.LT) // AIA OCSP is supported for signatures with LT or LTA profile. + .withSigningCertificate(certificate) + .withSignatureDigestAlgorithm(signatureDigestAlgorithm) + .buildDataToSign(); + + currentSession().setAttribute(SESSION_ATTR_DATA, dataToSign); + + LOG.info("Successfully prepared container for signing for file '{}'", containerName); + + final byte[] digest = signatureDigestAlgorithm.getDssDigestAlgorithm().getMessageDigest() + .digest(dataToSign.getDataToSign()); + + final DigestDTO digestDTO = new DigestDTO(); + digestDTO.setHash(DatatypeConverter.printBase64Binary(digest)); + digestDTO.setHashFunction(digestAlgorithmName); + + return digestDTO; + } + + /** + * Signs a {@link Container} using given {@link SignatureDTO}. + * Container to sign is taken from the current session. + * + * @param signatureDTO signature DTO + * @return fileDTO + */ + public FileDTO signContainer(SignatureDTO signatureDTO) { + FileDTO fileDTO = (FileDTO) Objects.requireNonNull(currentSession().getAttribute(SESSION_ATTR_FILE)); + Container containerToSign = (Container) Objects.requireNonNull(currentSession().getAttribute(SESSION_ATTR_CONTAINER)); + DataToSign dataToSign = (DataToSign) Objects.requireNonNull(currentSession().getAttribute(SESSION_ATTR_DATA)); + + byte[] signatureBytes = DatatypeConverter.parseBase64Binary(signatureDTO.getBase64Signature()); + Signature signature = dataToSign.finalize(signatureBytes); + containerToSign.addSignature(signature); + currentSession().setAttribute(SESSION_ATTR_CONTAINER, containerToSign); + + return new FileDTO(generateContainerName(fileDTO.getName())); + } + + public String getContainerName() { + FileDTO fileDTO = (FileDTO) Objects.requireNonNull(currentSession().getAttribute(SESSION_ATTR_FILE)); + return generateContainerName(fileDTO.getName()); + } + + public ByteArrayResource getSignedContainerAsResource() throws IOException { + Container signedContainer = (Container) Objects.requireNonNull(currentSession().getAttribute(SESSION_ATTR_CONTAINER)); + try (final InputStream stream = signedContainer.saveAsStream()) { + return new ByteArrayResource(stream.readAllBytes()); + } + } + + private Container getContainerToSign(FileDTO fileDTO) { + LOG.info("Creating container for file '{}'", fileDTO.getName()); + + final DataFile dataFile = new DataFile(fileDTO.getContentBytes(), fileDTO.getName(), fileDTO.getContentType()); + return ContainerBuilder + .aContainer(Container.DocumentType.ASICE) + .withDataFile(dataFile) + .withConfiguration(signingConfiguration) + .build(); + } + + private String generateContainerName(String fileName) { + return FilenameUtils.removeExtension(fileName) + ".asice"; + } + + /* Here is an example method that demonstrates how to handle file uploads. + * See also SigningController.upload(). + * + * Creates a {@link Container} using given name and files. + * + * @param fileDTO container file + * @return new {@link Container} instance + public FileDTO createContainer(FileDTO fileDTO) { + + Container containerToSign = getContainerToSign(fileDTO); + + currentSession().setAttribute(SESSION_ATTR_CONTAINER, containerToSign); + currentSession().setAttribute(SESSION_ATTR_FILE, fileDTO); + + FileDTO newFileDTO = new FileDTO(fileDTO.getName()); + return newFileDTO; + } + */ + /* When using uploaded files, retrieve file information and container from the session in prepareContainer(). + + FileDTO fileDTO = (FileDTO) Objects.requireNonNull(currentSession().getAttribute(SESSION_ATTR_FILE)); + Container containerToSign = (Container) Objects.requireNonNull(currentSession().getAttribute(SESSION_ATTR_CONTAINER)); + */ +} diff --git a/example/src/main/java/eu/webeid/example/service/dto/CertificateDTO.java b/example/src/main/java/eu/webeid/example/service/dto/CertificateDTO.java new file mode 100644 index 0000000..6050c85 --- /dev/null +++ b/example/src/main/java/eu/webeid/example/service/dto/CertificateDTO.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2020-2024 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. + */ + +package eu.webeid.example.service.dto; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; +import java.util.stream.Collectors; + +public class CertificateDTO { + + private String certificate; + private List supportedSignatureAlgorithms; + + public String getCertificate() { + return certificate; + } + + public void setCertificate(String certificate) { + this.certificate = certificate; + } + + public List getSupportedSignatureAlgorithms() { + return supportedSignatureAlgorithms; + } + + public void setSupportedSignatureAlgorithms(List supportedSignatureAlgorithms) { + this.supportedSignatureAlgorithms = supportedSignatureAlgorithms; + } + + public X509Certificate toX509Certificate() throws CertificateException { + byte[] certificateBytes = Base64.getDecoder().decode(certificate); + InputStream inStream = new ByteArrayInputStream(certificateBytes); + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + return (X509Certificate) cf.generateCertificate(inStream); + } + + public List getSupportedHashFunctionNames() { + return supportedSignatureAlgorithms == null ? new ArrayList<>() : supportedSignatureAlgorithms + .stream() + .map(SignatureAlgorithmDTO::getHashFunction) + .distinct() + .collect(Collectors.toList()); + } +} diff --git a/example/src/main/java/eu/webeid/example/service/dto/ChallengeDTO.java b/example/src/main/java/eu/webeid/example/service/dto/ChallengeDTO.java new file mode 100644 index 0000000..dd95d42 --- /dev/null +++ b/example/src/main/java/eu/webeid/example/service/dto/ChallengeDTO.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2020-2024 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. + */ + +package eu.webeid.example.service.dto; + +public class ChallengeDTO { + private String nonce; + + public String getNonce() { + return nonce; + } + + public void setNonce(String nonce) { + this.nonce = nonce; + } +} diff --git a/example/src/main/java/eu/webeid/example/service/dto/DigestDTO.java b/example/src/main/java/eu/webeid/example/service/dto/DigestDTO.java new file mode 100644 index 0000000..4e56d36 --- /dev/null +++ b/example/src/main/java/eu/webeid/example/service/dto/DigestDTO.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2020-2024 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. + */ + +package eu.webeid.example.service.dto; + +public class DigestDTO { + private String hash; + private String hashFunction; + + public String getHash() { + return hash; + } + + public void setHash(String hash) { + this.hash = hash; + } + + public String getHashFunction() { + return hashFunction; + } + + public void setHashFunction(String hashFunction) { + this.hashFunction = hashFunction; + } +} diff --git a/example/src/main/java/eu/webeid/example/service/dto/FileDTO.java b/example/src/main/java/eu/webeid/example/service/dto/FileDTO.java new file mode 100644 index 0000000..3a65edc --- /dev/null +++ b/example/src/main/java/eu/webeid/example/service/dto/FileDTO.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2020-2024 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. + */ + +package eu.webeid.example.service.dto; + +import org.springframework.core.io.ClassPathResource; +import org.springframework.util.MimeTypeUtils; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.io.Serializable; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Objects; + +public class FileDTO implements Serializable { + private static final String EXAMPLE_FILENAME = "example-for-signing.txt"; + + private final String name; + private String contentType; + private byte[] contentBytes; + + public FileDTO(String name) { + this.name = name; + } + + public FileDTO(String name, String contentType, byte[] contentBytes) { + this.name = name; + this.contentType = contentType; + this.contentBytes = contentBytes; + } + + public static FileDTO fromMultipartFile(MultipartFile file) throws IOException { + return new FileDTO( + Objects.requireNonNull(file.getOriginalFilename()), + Objects.requireNonNull(file.getContentType()), + Objects.requireNonNull(file.getBytes()) + ); + } + + public static FileDTO getExampleForSigningFromResources() throws IOException { + final URI resourceUri = new ClassPathResource("/static/files/" + EXAMPLE_FILENAME).getURI(); + return new FileDTO( + EXAMPLE_FILENAME, + MimeTypeUtils.TEXT_PLAIN_VALUE, + Files.readAllBytes(Paths.get(resourceUri)) + ); + } + + public String getName() { + return name; + } + + public String getContentType() { + return contentType; + } + + public byte[] getContentBytes() { + return contentBytes; + } +} diff --git a/example/src/main/java/eu/webeid/example/service/dto/SignatureAlgorithmDTO.java b/example/src/main/java/eu/webeid/example/service/dto/SignatureAlgorithmDTO.java new file mode 100644 index 0000000..bef5ba4 --- /dev/null +++ b/example/src/main/java/eu/webeid/example/service/dto/SignatureAlgorithmDTO.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2020-2024 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. + */ + +package eu.webeid.example.service.dto; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +public class SignatureAlgorithmDTO { + + // See https://github.com/web-eid/web-eid-app/blob/main/src/controller/command-handlers/signauthutils.cpp#L121-L127 + private static final Set SUPPORTED_CRYPTO_ALGOS = new HashSet<>(Arrays.asList( + "ECC", "RSA" + )); + private static final Set SUPPORTED_PADDING_SCHEMES = new HashSet<>(Arrays.asList( + "NONE", "PKCS1.5", "PSS" + )); + // See https://github.com/web-eid/libelectronic-id/tree/main/src/electronic-id.cpp#L131 + private static final Set SUPPORTED_HASH_FUNCTIONS = new HashSet<>(Arrays.asList( + "SHA-224", "SHA-256", "SHA-384", "SHA-512", + "SHA3-224", "SHA3-256", "SHA3-384", "SHA3-512" + )); + + private String cryptoAlgorithm; + + private String hashFunction; + + private String paddingScheme; + + public String getCryptoAlgorithm() { + return cryptoAlgorithm; + } + + public void setCryptoAlgorithm(String cryptoAlgorithm) { + if (!SUPPORTED_CRYPTO_ALGOS.contains(cryptoAlgorithm)) { + throw new IllegalArgumentException("The provided crypto algorithm is not supported"); + } + this.cryptoAlgorithm = cryptoAlgorithm; + } + + public String getHashFunction() { + return hashFunction; + } + + public void setHashFunction(String hashFunction) { + if (!SUPPORTED_HASH_FUNCTIONS.contains(hashFunction)) { + throw new IllegalArgumentException("The provided hash function is not supported"); + } + this.hashFunction = hashFunction; + } + + public String getPaddingScheme() { + return paddingScheme; + } + + public void setPaddingScheme(String paddingScheme) { + if (!SUPPORTED_PADDING_SCHEMES.contains(paddingScheme)) { + throw new IllegalArgumentException("The provided padding scheme is not supported"); + } + this.paddingScheme = paddingScheme; + } +} diff --git a/example/src/main/java/eu/webeid/example/service/dto/SignatureDTO.java b/example/src/main/java/eu/webeid/example/service/dto/SignatureDTO.java new file mode 100644 index 0000000..68742fc --- /dev/null +++ b/example/src/main/java/eu/webeid/example/service/dto/SignatureDTO.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2020-2024 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. + */ + +package eu.webeid.example.service.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class SignatureDTO { + + @JsonProperty("signature") + private String base64Signature; + + public String getBase64Signature() { + return base64Signature; + } + + public void setBase64Signature(String base64Signature) { + this.base64Signature = base64Signature; + } +} diff --git a/example/src/main/java/eu/webeid/example/web/IndexController.java b/example/src/main/java/eu/webeid/example/web/IndexController.java new file mode 100644 index 0000000..e464a50 --- /dev/null +++ b/example/src/main/java/eu/webeid/example/web/IndexController.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2020-2024 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. + */ + +package eu.webeid.example.web; + +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; + +@Controller +public class IndexController { + @GetMapping("/") + public String welcome(Model model, HttpServletRequest request) { + model.addAttribute("serverName", request.getServerName()); + return "index"; + } +} diff --git a/example/src/main/java/eu/webeid/example/web/WelcomeController.java b/example/src/main/java/eu/webeid/example/web/WelcomeController.java new file mode 100644 index 0000000..0db6fc7 --- /dev/null +++ b/example/src/main/java/eu/webeid/example/web/WelcomeController.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2020-2024 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. + */ + +package eu.webeid.example.web; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.access.annotation.Secured; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; + +import java.security.Principal; +import java.util.Objects; + +import static eu.webeid.example.security.AuthTokenDTOAuthenticationProvider.ROLE_USER; + +@Controller +@Secured(ROLE_USER) +public class WelcomeController { + private static final Logger LOG = LoggerFactory.getLogger(WelcomeController.class); + + @GetMapping("welcome") + public String welcome(Model model, Principal principal) { + Objects.requireNonNull(principal); + LOG.info("Showing welcome page, logged in as principal={}", principal.getName()); + model.addAttribute("principalName", principal.getName()); + return "welcome"; + } +} diff --git a/example/src/main/java/eu/webeid/example/web/rest/ChallengeController.java b/example/src/main/java/eu/webeid/example/web/rest/ChallengeController.java new file mode 100644 index 0000000..ecc3ee4 --- /dev/null +++ b/example/src/main/java/eu/webeid/example/web/rest/ChallengeController.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2020-2024 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. + */ + +package eu.webeid.example.web.rest; + +import eu.webeid.example.service.dto.ChallengeDTO; +import eu.webeid.security.challenge.ChallengeNonceGenerator; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("auth") +public class ChallengeController { + + private final ChallengeNonceGenerator challengeNonceGenerator; + + public ChallengeController(ChallengeNonceGenerator challengeNonceGenerator) { + this.challengeNonceGenerator = challengeNonceGenerator; + } + + @GetMapping("challenge") + public ChallengeDTO challenge() { + final ChallengeDTO challenge = new ChallengeDTO(); + challenge.setNonce(challengeNonceGenerator.generateAndStoreNonce().getBase64EncodedNonce()); + return challenge; + } +} diff --git a/example/src/main/java/eu/webeid/example/web/rest/SigningController.java b/example/src/main/java/eu/webeid/example/web/rest/SigningController.java new file mode 100644 index 0000000..4f935be --- /dev/null +++ b/example/src/main/java/eu/webeid/example/web/rest/SigningController.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2020-2024 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. + */ + +package eu.webeid.example.web.rest; + +import eu.webeid.example.security.WebEidAuthentication; +import eu.webeid.example.service.SigningService; +import eu.webeid.example.service.dto.CertificateDTO; +import eu.webeid.example.service.dto.DigestDTO; +import eu.webeid.example.service.dto.FileDTO; +import eu.webeid.example.service.dto.SignatureDTO; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.Resource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.annotation.Secured; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.io.IOException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; + +import static eu.webeid.example.security.AuthTokenDTOAuthenticationProvider.ROLE_USER; + +@RestController +@RequestMapping("sign") +@Secured(ROLE_USER) +public class SigningController { + + private final SigningService signingService; + + public SigningController(SigningService signingService) { + this.signingService = signingService; + } + + @PostMapping("prepare") + public DigestDTO prepare(@RequestBody CertificateDTO data, WebEidAuthentication authentication) throws CertificateException, NoSuchAlgorithmException, IOException { + return signingService.prepareContainer(data, authentication); + } + + @PostMapping("sign") + public FileDTO sign(@RequestBody SignatureDTO data) { + return signingService.signContainer(data); + } + + @GetMapping(value = "download", produces = "application/vnd.etsi.asic-e+zip") + public ResponseEntity download() throws IOException { + + ByteArrayResource resource = signingService.getSignedContainerAsResource(); + return ResponseEntity.ok() + .contentLength(resource.contentLength()) + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + signingService.getContainerName()) + .body(resource); + } + + /* Here is an example endpoint that demonstrates how to handle file uploads. + See also resources/templates/welcome-with-file-upload-support.html. + + @PostMapping(value = "upload", produces = "application/json") + public FileDTO upload(@RequestParam("file") MultipartFile multipartFile) throws IOException { + return signingService.createContainer(FileDTO.fromMultipartFile(multipartFile)); + } + */ +} diff --git a/example/src/main/resources/application-dev.yaml b/example/src/main/resources/application-dev.yaml new file mode 100644 index 0000000..e480fb3 --- /dev/null +++ b/example/src/main/resources/application-dev.yaml @@ -0,0 +1,4 @@ +web-eid-auth-token: + validation: + use-digidoc4j-prod-configuration: false + local-origin: "https://7911-90-191-75-155.ngrok-free.app" diff --git a/example/src/main/resources/application-prod.yaml b/example/src/main/resources/application-prod.yaml new file mode 100644 index 0000000..3868f35 --- /dev/null +++ b/example/src/main/resources/application-prod.yaml @@ -0,0 +1,8 @@ +web-eid-auth-token: + validation: + use-digidoc4j-prod-configuration: true + local-origin: "https://web-eid.eu" + truststore-password: "changeit" +spring: + thymeleaf: + cache: true diff --git a/example/src/main/resources/application.properties b/example/src/main/resources/application.properties new file mode 100644 index 0000000..7d70ac4 --- /dev/null +++ b/example/src/main/resources/application.properties @@ -0,0 +1,2 @@ +spring.profiles.active=dev +server.servlet.session.cookie.name=__Host-JSESSIONID \ No newline at end of file diff --git a/example/src/main/resources/application.yaml b/example/src/main/resources/application.yaml new file mode 100644 index 0000000..df3117c --- /dev/null +++ b/example/src/main/resources/application.yaml @@ -0,0 +1,19 @@ +spring: + main: + allow-circular-references: true + +# Make session cookie secure behind a reverse proxy, see +# - https://docs.spring.io/spring-boot/docs/2.2.5.RELEASE/reference/htmlsingle/#howto-use-behind-a-proxy-server +# - https://docs.spring.io/spring-boot/docs/2.2.5.RELEASE/reference/htmlsingle/#howto-enable-https +# Note that `server.servlet.session.cookie.secure` is implicitly true when HTTPS is detected. +server: + forward-headers-strategy: native + tomcat: + remote-ip-header: x-forwarded-for + protocol-header: x-forwarded-proto + +logging: + level: + eu.webeid.security: DEBUG + eu.webeid.example: DEBUG + org.springframework.security.web.csrf.CsrfFilter: DEBUG diff --git a/example/src/main/resources/certs/dev/TEST_ORG_2021E.cer b/example/src/main/resources/certs/dev/TEST_ORG_2021E.cer new file mode 100644 index 0000000..bf399a9 --- /dev/null +++ b/example/src/main/resources/certs/dev/TEST_ORG_2021E.cer @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDoDCCAwOgAwIBAgIQcyqWqJYuHMpg+S/gNJArMTAKBggqhkjOPQQDAzBuMQsw +CQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRh +DA5OVFJFRS0xMDc0NzAxMzEpMCcGA1UEAwwgVEVTVCBvZiBTSyBJRCBTb2x1dGlv +bnMgUk9PVCBHMUUwHhcNMjEwNzIyMDg0NDE2WhcNMzYwNzIyMDg0NDE2WjBvMQsw +CQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRh +DA5OVFJFRS0xMDc0NzAxMzEqMCgGA1UEAwwhVEVTVCBvZiBTSyBJRCBTb2x1dGlv +bnMgT1JHIDIwMjFFMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEPN17jh+wlx7U0fND +5Vd+xix//r/XXBrK2JPgzXPT3ApkHQwBuMMcRAT3xTZ0UtiRbfo6mClvtZdyEcyf +qw8xUd7rZ/jJHkHE+Ea/5x8sZtgBRRBdga932N40gkRuTfdEo4IBYzCCAV8wHwYD +VR0jBBgwFoAU4hzeY9y++IR+ATsuS4Cx4X/V8eYwHQYDVR0OBBYEFEmnHNBblRgz +FcEe8pie53tLDoZpMA4GA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAGAQH/AgEA +MGwGCCsGAQUFBwEBBGAwXjAiBggrBgEFBQcwAYYWaHR0cDovL2RlbW8uc2suZWUv +b2NzcDA4BggrBgEFBQcwAoYsaHR0cDovL2Muc2suZWUvVEVTVF9TS19ST09UX0cx +XzIwMjFFLmRlci5jcnQwOQYDVR0fBDIwMDAuoCygKoYoaHR0cDovL2Muc2suZWUv +VEVTVF9TS19ST09UX0cxXzIwMjFFLmNybDBQBgNVHSAESTBHMEUGBFUdIAAwPTA7 +BggrBgEFBQcCARYvaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9lbi9yZXBv +c2l0b3J5L0NQUy8wCgYIKoZIzj0EAwMDgYoAMIGGAkFeLgoCd2OgKM/9YM2eizwD +Mdb8DIE2aX9a5czWUXypSa+fA5HXry4oAFqY3ed7TWrgaX+RgD4ejuHDJUN3fVmb +zwJBHI1S8XxeM2+c0YUJQMU1SY2VVcVax6110r1aBDr90RAF2a9H8sKRVYtz21lC +bM/wb9Lg61nkctY91DVjvWMj138= +-----END CERTIFICATE----- diff --git a/example/src/main/resources/certs/dev/TEST_ORG_2021R.cer b/example/src/main/resources/certs/dev/TEST_ORG_2021R.cer new file mode 100644 index 0000000..6eb8091 --- /dev/null +++ b/example/src/main/resources/certs/dev/TEST_ORG_2021R.cer @@ -0,0 +1,39 @@ +-----BEGIN CERTIFICATE----- +MIIGzDCCBLSgAwIBAgIQPadGvT+ZiMRhHPzivWSWBDANBgkqhkiG9w0BAQwFADBu +MQswCQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYD +VQRhDA5OVFJFRS0xMDc0NzAxMzEpMCcGA1UEAwwgVEVTVCBvZiBTSyBJRCBTb2x1 +dGlvbnMgUk9PVCBHMVIwHhcNMjEwODE4MTIyODE4WhcNMzYwODE4MTIyODE4WjBv +MQswCQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYD +VQRhDA5OVFJFRS0xMDc0NzAxMzEqMCgGA1UEAwwhVEVTVCBvZiBTSyBJRCBTb2x1 +dGlvbnMgT1JHIDIwMjFSMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA +vRcXPfY+T3pZl0ou0HQ76GEcOSqa8LmktDjFlWRlessGR+A70AiNw1k6s98Ic6wv +u+KUnRSdXCu+QANnkJVr75+W4heWAgJL0aOwDsBRIqn/aFiaW49+7Vw8zdlH6LnU +8YFDTvjy7tgX1ZyqFI9s31MJ84p9YH+DOwEYB8VMdkwa9wVBIu3oDh9hCTVUZtZF +G/yOI0/Tu9eKRbLSK6Je/op2IvEXqZwZVnWpR/yAAfyYOsY6/t9iieCknvQtWXnu +pxMm9HybUoOqHQ6QKz0nTY4As0sDuNom0aVmHeNR7W+ebJtiU8H/zqr+yR46qauD +QSrVqBlKfR7ZGLGPxvW38qfQpoQ2okaR+bUrrAcscJidwT3+0n1+2t4jSX6OTXu2 +HsPjk7dmcrGhTtQh2BcjbhHit+nOS3BuB1QOJ8tXsdj1nsaHPae6ZeH/KbMetBeh +BCwpXcNP3aR+AfVJWzgLObZSrqeCzGsfSfBQvracTjZzJp9NGXTZBe9MzcYomr2d +I7mqt/wa5N6i7eDa75cv+FncGrRKJkQ1JRRPynGcQK/dOVrmjjdeJGf6vgqWP/WI +KTuXYdovCnvXhO+PuOifRiP0scnWN+YXR7R3EXT50rk79lNrOOzcqmft3B2EVsVI +BPJTwjnSiNXZRHSoedOPJEInLQ7sP9uDWbqMd04/yYUCAwEAAaOCAWMwggFfMB8G +A1UdIwQYMBaAFMaIk1tHkln1YUO+2L6I5FaPti5RMB0GA1UdDgQWBBSJNpEi++WV +9vJUbwBlF9+7AA6VxTAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIB +ADBsBggrBgEFBQcBAQRgMF4wIgYIKwYBBQUHMAGGFmh0dHA6Ly9kZW1vLnNrLmVl +L29jc3AwOAYIKwYBBQUHMAKGLGh0dHA6Ly9jLnNrLmVlL1RFU1RfU0tfUk9PVF9H +MV8yMDIxUi5kZXIuY3J0MDkGA1UdHwQyMDAwLqAsoCqGKGh0dHA6Ly9jLnNrLmVl +L1RFU1RfU0tfUk9PVF9HMV8yMDIxUi5jcmwwUAYDVR0gBEkwRzBFBgRVHSAAMD0w +OwYIKwYBBQUHAgEWL2h0dHBzOi8vd3d3LnNraWRzb2x1dGlvbnMuZXUvZW4vcmVw +b3NpdG9yeS9DUFMvMA0GCSqGSIb3DQEBDAUAA4ICAQCInhE2mlWOwF5ZgXEgBP4G +rVAp7SLyqOoeeIB1PkTX9zZTN7zm+G1T9jYBYAbskIKsIQYz8Rg/HaXkg1MDl1lf +7M9CYbWTZanOPzSfAUS1u49pnyKr74DIRCo2bdD4e2ofS5y/uMu3qE1wClzO6TJo +oJtX1PGTptqKclZywxAVeoCisT1f48PW6dq0b5i6LTtDTDSSKGUgvdm18af0TSMn +kozsxIWzY06TmDfJvlPB12h8vIlzBZXHoVH89wBtffV1uVpc8btyZqqYnqPivXSM +clQMuTmJSZjZoOQ695JqN92kSy8aDc9Bxqb+XsBmmjr26nvvW54y68TUZSz1D29n +eBPy0DB60ZAFRgtyfanlPZL8KymBan45u88rE5S3GOl0kbkYOmsIoCv4BHILxlQl +ddFY+BGkDwM8bEQ480AERTXcp+4rXUj80kXIsa5NR5z2+LaDvy2j3tVcY7iRkdIU +nVWYwxEEgU0dbzxl+LlSx/ad9dGzbhvhsEiq93jz2qJdpsC6EiBlBGCRyfwEgv8a +CqDTAJNqnY3+UzTzYds1HlMfxSyzoi01zyfN4DtWl2RTCslBZ0/XLnn4HaTSU5Bq +wESseW9CPJRaO+W3ERsSRAW9tjDuEuBxWAN91OWlkk6GpLJkr3/4UcO85pVgNJf3 +Q4sZkdHlzV6yY/Cq1S+b9Q== +-----END CERTIFICATE----- diff --git a/example/src/main/resources/certs/dev/TEST_of_ESTEID2018.cer b/example/src/main/resources/certs/dev/TEST_of_ESTEID2018.cer new file mode 100644 index 0000000000000000000000000000000000000000..6a96fb083686e6210a1c5f14cfd0cf7780227843 GIT binary patch literal 1408 zcmXqLVy!V~V!6A3nTe5!Nx)3vv*-7+0I%o?Zz}}*_*@OR*f_M>JkHs&FtZpW8FCwN zvN4CUun9A{x*AFwNP;+AJVL?V3Z5{`00ChPtj}Y7q!6B}mE?^fl&NdKWV+VVhiII&}yOD)KnYpoz zfrasC_kxE9wGJgI|1P6utBF!f7_Lfs2h?))#ovL01yOs^$8Hv4%@O;YUWqT3NO zc01ipG{_6Q?x*_ZR`nF|^=l*;`K~|cT_1Pn&}An#yMmB?fjr6EuO+pba)uqLp4KBV zbzw`_sx^`u6{oYmyS??z>|@{7i7fJYeYVWxt@OOJ^I09@Uo387HZo{p)-#X?2BNGo zi-dt#gGfD3!Hq9Z`x(pM9naO&-1+6?pR5B0vLFR~EMhDo2U=!o9`T!R&1CXz=0mP9 z1`bWTY6CuyG(RKbe-;*C%(EE?f%w88K8FDtkYZwFFlb^t3lddeX<|GAIU}Ixp z=&xdA0wx1?7KZLO4nU?68@DzaBMW2mL`6m>MgwI7MK%tg04pmy6Qh`DMoCFQv6a4l zd3m{BakgG+s=jkTumLZ!9wsDiJ;(?YSyogo8!DF_mCKCEWkKa~pmI4;xm>7RZd5K0 zDwh|P%ZJM4N978jas^SjLa1C}RIUgT7Z^BbVTJ5ppe(X~fn4OE19I66v_R3R3W{U} z136eMF>}IMOmG&9K{<@g$jFjykZGU?;~OxxDS=`Y7^#VwiFzrix%tqTO-YJqFS)41Kmw+bk(q_ZfD4l7`rH^94J<&; zmuE3F&^ORspuIq|O`RBXlZ$eYOEKog9tMNP4kjl?h4}8(+C`ah8qZmUX4i_II#8o~ zb?t&zpUu`zXpvhh^-pISU-1u>%8&21R0?L9i~O!U&hV!~!#3N5J*-aOZ}+OJOim1q zA0B_1P*dL@UEOxMjI-jUmtkuae6w;WAqE!h}6>W^kMq_}4-9!+C5{ifg zVSLqE1QEqL9nk?BN_D_EMBWNIDx&pK5%B?3MFF4f21RWDbUHit?tb4r_wG68p7VhW zW+TX8lo!)!FpcIA!xATV`n4ZeE_PF_E#@Uq(fW#r_xw5(EKkI_3e~ z-^b=O5?jPVp+qv>UlPIP11u0^&?bJGLYM*3GiYNVinpa@&|s(;8CSYc9aCX_Uko>G zU--JNJ{ZZ1sBG;TxYe_XY`WLeu`Ssvq^zPJb*O8Uvho)=*gN+wVRVR|q=!93jHVf; z7YS8|_^W?)4GrkRUi%NtQSPYSn)cFpMP1!)}<#%6%m@c;Ytnw*%n<6R8zNRkv(@ld-=kKd-72e%h%{qNHrMo^}TtC*G zxGWBaQ};M_XA|B(hbQUh+<+es&<=Xms$2U~U+h2r=tjJ>CM?Opt|jx4psitEy*52X zG0iA0AKIJgzfsL|t(HpBb3CwQ!bGm2Ix$nZo@^va|fI{fN%CjAwkr-37F@JKAEOKUwuS0rdoIG}oN&m8; zu-y7AciEKmmW}#3MJ1@RUmMzP5}@1nCT%Vooadi0;hamz ztGLpNs&R`I>~E^g-ghJ0BQ8%;loUp<-8DYWya^gSS{pMfVCP=BZ|BWq-t9>%ANaT| zK&L>D(t53M^H0JVeg#8rSFRT3y{m|*sP;<0d=?p|78z)C7=jOF&@ieZ{J~hNnA{y< z_%Fm7FsT<+Ab@S*##lInYSB7i7_bmr#Lx!}kAdM^ri~vHM&Jt`R&=nDYDa4dG8k4g z*om`Vr_;_v(Zs|=-f;V%axDoWKU&bh7Zc_}LWqR)kd7K8ghSlnnV_%=Qc^<=$*4yM z@u(FMq=5)XJ3L!Bi}ieHF2n=YOy)WRi_#B(j=Dnw7IpY9cc`rhl%EX4l&n}GP)<)6 z^*KUXjgs`?PIG4?P6&tRntvKb1w+>XlC$dUvsF_%uMG}2vHK*1(vEbFQD9qGiaMO@ z_GH(X%kg=x^o{R;A$!Cb7)CaO_DYa_XIekxtS}nq_tgHLbpF)hX5rpT^P864 zIsKsVNxXGuo`8O`t(ClU1f6{mY$&OX4*&Xfd(oDO>pe6crMGs*w-C=biwc(JmT)=8 zv(GQe?=qC$>$G`_$+9j!_qGkaw13;t*62&)bM=$h?jin1H=PYz-`#e2#%s`PSas_7 zZPx>N{HoH+o>$qNra$~T%x^;7`f4-dOgB!fcz3Kk@>0%#xt<@MRsCZ84EN$=Y0sh7 v-An4f;d)=}dt;pn$K*2liKZ%-^RCr5+e2UK2e$SXUB6&gwTE64x6=7|3$xO>EDNao-%FN9y&Q2}PE-g0HFi-=j zVdjx{byaYNs7X#N$;{6K^72ZGQWb*o^Gg(*9StQ6#Nf7ZGKz5)WM?`QXX~Y=8pw(B z8X6iH8=4s!m{=N_MTzqonILfua~0Q4dg-2;ue-bcXvr@YF=q^YKoz_fhgGJ z!hGI7j={mky20KGMh1ok22G480mI13z}&>h&j1wXVrpV!WLQ0QnP7bOpKt!`s25ULP^1S0y`7 zmCM{f;PeH9Z=dW+|7W)pslQ=KyPeh}S@$FSb;`O5{nv-z*lc>(#(3d{?W7A8t7hl$ zNc?NkSpVl>e?@xl=8{_l(|z;zZ{IXC*zWSFDdw+t{(kkKZn4Shzuwml{j!}J+MmY} zdSzWd19L@&g;Qs##?z$@vz`anXe3=#ZOJSCwCB3iqRaC*&p+7or_7%5=#$AwDaWEb z7q~tvdh+C$@qP#Y>$&%rxlDY%dVTw@&5Jb8cpaS3&$xc>&)XO6+&-|`o*kGZ4a$INj6<6ZnEcq87{x>~N=gc>^!3Zj%k{u%LqEBo*dPe5g2}*5 z&V|8|p_rkRp#+GF8B!S(z+@4SpUIF0#MumqKv=?{2b6JU$Y&^GC;*cnF_3Cc1{VfB z16h!p_*lePL}E)t1bemqRKkyT~^rr8D&p-)kEufk;ZpJiL~ z@uTX=mEKoGW}@XBU;<%e$Wki0a9F6K-{Gp_A;k@EXKGC_&%5`1T937Rb%$n0Va+vJ z=VJ$A%3r<=xxa4qDSes0xqZHUS6IGRh8m~Lxx)FgWUs|1k2Q%4rxi%8@2Kl@RsZ?* z{GsF3EuGiS-j>|r$$EE5+m?(~ig z`TDK!#e}$}xqJTXO^G|aqV+}pPKJk)rQiRbV|=N(?p7T0-j6R% za literal 0 HcmV?d00001 diff --git a/example/src/main/resources/certs/prod/ESTEID2018.cer b/example/src/main/resources/certs/prod/ESTEID2018.cer new file mode 100644 index 0000000000000000000000000000000000000000..6200181ffbe32047257a19c847ed230d095e953b GIT binary patch literal 1371 zcmXqLVhuNFV%f2PnTe5!NubpI*BX%$@9yZuX;=QGg}yc5V&l+i^EhYA!pve2Wyo#7 z$;KSY!Y0h*>S`!$APM4d@dyQbD|osn1n1|JmSpDV6)QLf8;To|jqaF|x60H?lA&GdH#|urMCCkj#JU zbxl}#{4EZD8=UiQ7|m7nS=nAVC+d`}>%&(UQ<|iY@8C_jVX|U&gv&Fph3r#v zmY6TgXKhvyo)F2{r~Fs;A0VE5xKdh;C6i7z9~NzJlW^I=z_`GSz1B{d?0Cl zM#ldvEWns%GY|stg+Y7{12!PV#K>UK#CR4Ys=(63cm&8fV8Fq~#=_8F#mEGVc6Jtq z?l%rVrV$&rHX9=gWAj8sMn*;hWdlVv4xj)lD?1aTm}o{xNkOrdzJ7Umxn6O$UTUho zb3m{GFR~scByK&(2ozaXR4yATmmQVMjLKy}<#M2MIZ?S>s9bJTE)Ob~7nRG0%H>Dp z3ZQZYQMp2>TwzqM2oe_|dZPvVVbG6UH}SY?B4WDlk$LGZUeanwqMco?n)n2+l$JaHULb%21`rP=&~u zTrVZHNH4jl#6SY3fsvVo$AAlx&idSdNx=Z*VtE!#19bz{1ryv>4iIF*z|RuVU`CR9u&?WpdKyHoH@>(wk*DibsxH vHN+Il?|g7yGCo*=t>@%SyV|wW>~DXbCmgq(g@^f~dx!()8JmCmqDx}|!-qnr literal 0 HcmV?d00001 diff --git a/example/src/main/resources/certs/prod/trusted_certificates.jks b/example/src/main/resources/certs/prod/trusted_certificates.jks new file mode 100644 index 0000000000000000000000000000000000000000..0276f80a9d40486ac22b88b73f38cd5bf513e9ed GIT binary patch literal 1345 zcmezO_TO6u1_mY|W(3nbsj0f@`DMw8Mh1o!K*17~*)={4tPy&q29^vAEPo7|SbhL; z=1>+kVJ25s zLums^5QmFLDA-%U(?uaTKc}=LGe56b!7|k#)F|x60 zH?lA&GdH#|urM5#{(G~bFv>nZZL`3aU7TNc*EpDIJv_xd_wZYdP_s{;AF?WQUb)_T z+y2q}+FmaSLH0S*o`#+|nIJHwbe?lr#jDed(+urw*q&5$TvKy7nIE7T9w)@JzTI@1 z&&6ZCvByiNo7IfrJ$1IZ->Nt=HwVmSU);pB$DoO6 zhk+0<*kpwn8UM3z7_b2;CPqdBK9CqcNQ?y-e{2ROAigSyuVtXY#-Yu|$jZvj%n4^P z!C5SD7NdbINEaWA7>h`qX4rF+NfmzO5A*emwPq9^{A)7bKprHm%pzeR)_`3>6B7eS zi2_R#<9CB5#?J;EY-}tH{Z))iz~sWt!qEN30mw9B;|98ig|T^}A|uf0$_9!sConOJ ziDs0P6ck(O>z9|8>lJ6~rKajT2Lv1NBI{v7;?{$VK#^rd<+7o2*-^R7s9Y9QE(a=? z6P3$_%H>Ao@}P2gQMr7mTz*up04i4yl`DkG6-MQXAaQ|#gBDiE{sqb+`xnSX4mu#0 z&A^b23l>#eM8=c^EHW9HS$GV%AW5Ll4H!#EB?xn44}(Et2a^-yfM!fRmaZ38ZS>}Fc%JrmMb(`b3{qB=J6`cV{c0W w0#2|9f1bD@Zq7c%U;NjCce=elexl`<`}>(IGj_5!BoukZADy|;Cn>`Q0M189!Tcode{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 diff --git a/example/src/main/resources/static/css/main.css b/example/src/main/resources/static/css/main.css new file mode 100644 index 0000000..3de3421 --- /dev/null +++ b/example/src/main/resources/static/css/main.css @@ -0,0 +1,69 @@ +body { + font-family: "Inv Maison Neue","Maison Neue",-apple-system,BlinkMacSystemFont,"Open Sans",open-sans,sans-serif; +} + +.has-advanced-upload { + outline: 2px dashed #92b0b3; + outline-offset: -10px; + + -webkit-transition: outline-offset .25s ease-in-out, background-color .25s linear; + transition: outline-offset .25s ease-in-out, background-color .25s linear; +} + +.adding-signature { + height: 14px; + color: #000000; + font-size: 32px; + font-weight: bold; + letter-spacing: 0; + line-height: 14px; +} + +.welcome-line { + height: 40px; + color: #000000; + font-size: 14px; + letter-spacing: 0; + line-height: 20px; + margin-top: 1rem; +} + +#webeid-logout-button { + height: 2.5rem; +} + +#file-drop-area { + margin: 0 10rem; + padding-top: 2rem; + box-sizing: border-box; + outline: 2px dashed #92b0b3; + outline-offset: -10px; + border-radius: 3px; + background-color: #F5F5F5; +} + +.is-dragover { + background-color: #b7dbde !important; + outline-offset: -20px !important; + outline-color: #ffffff !important; +} + +#file-name, #file-drop-area, .welcome-line { + text-align: center; +} + +.eu-logo-fixed { + display: block; + position: fixed; + background: #fff; + bottom: 0; + left: 0; + margin: 0; + padding: 5px 20px; + text-align: center; + z-index: 1000; +} + +.eu-logo-fixed img { + height: 86px; +} diff --git a/example/src/main/resources/static/files/Web eID privacy policy.pdf b/example/src/main/resources/static/files/Web eID privacy policy.pdf new file mode 100644 index 0000000000000000000000000000000000000000..d5c03c67bc609655c7a16eaa6ea9e63bfaf08aa0 GIT binary patch literal 76371 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 XZz9n9el{XBaPz`JLPBTs)j|IQPi<*H literal 0 HcmV?d00001 diff --git a/example/src/main/resources/static/files/example-for-signing.txt b/example/src/main/resources/static/files/example-for-signing.txt new file mode 100644 index 0000000..07a5be0 --- /dev/null +++ b/example/src/main/resources/static/files/example-for-signing.txt @@ -0,0 +1 @@ +This is an example text file for testing digital signing. \ No newline at end of file diff --git a/example/src/main/resources/static/img/eu-fund-flags.svg b/example/src/main/resources/static/img/eu-fund-flags.svg new file mode 100644 index 0000000..0e99b0d --- /dev/null +++ b/example/src/main/resources/static/img/eu-fund-flags.svg @@ -0,0 +1,787 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/src/main/resources/static/js/errors.js b/example/src/main/resources/static/js/errors.js new file mode 100644 index 0000000..95220bb --- /dev/null +++ b/example/src/main/resources/static/js/errors.js @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2020-2024 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. + */ + +"use strict"; + +const alertUi = { + alert: document.querySelector("#error-message"), + alertMessage: document.querySelector("#error-message .message"), + alertDetails: document.querySelector("#error-message .details") +}; + +export function hideErrorMessage() { + alertUi.alert.style.display = "none"; +} + +export function showErrorMessage(error) { + const message = "Authentication failed"; + const details = + `[Code]\n${error.code}` + + `\n\n[Message]\n${error.message}` + + (error.response ? `\n\n[response]\n${JSON.stringify(error.response, null, " ")}` : ""); + + alertUi.alertMessage.innerText = message; + alertUi.alertDetails.innerText = details; + alertUi.alert.style.display = "block"; +} + +export async function checkHttpError(response) { + if (!response.ok) { + let body; + try { + body = await response.text(); + } catch (error) { + body = "<>"; + } + const error = new Error("Server error: " + body); + error.code = response.status; + throw error; + } +} \ No newline at end of file diff --git a/example/src/main/resources/static/js/web-eid.js b/example/src/main/resources/static/js/web-eid.js new file mode 100644 index 0000000..520fa88 --- /dev/null +++ b/example/src/main/resources/static/js/web-eid.js @@ -0,0 +1,423 @@ +/** + * MIT License + * + * Copyright (c) 2020-2023 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. + */ + +var Action; +(function (Action) { + Action["WARNING"] = "web-eid:warning"; + Action["STATUS"] = "web-eid:status"; + Action["STATUS_ACK"] = "web-eid:status-ack"; + Action["STATUS_SUCCESS"] = "web-eid:status-success"; + Action["STATUS_FAILURE"] = "web-eid:status-failure"; + Action["AUTHENTICATE"] = "web-eid:authenticate"; + Action["AUTHENTICATE_ACK"] = "web-eid:authenticate-ack"; + Action["AUTHENTICATE_SUCCESS"] = "web-eid:authenticate-success"; + Action["AUTHENTICATE_FAILURE"] = "web-eid:authenticate-failure"; + Action["GET_SIGNING_CERTIFICATE"] = "web-eid:get-signing-certificate"; + Action["GET_SIGNING_CERTIFICATE_ACK"] = "web-eid:get-signing-certificate-ack"; + Action["GET_SIGNING_CERTIFICATE_SUCCESS"] = "web-eid:get-signing-certificate-success"; + Action["GET_SIGNING_CERTIFICATE_FAILURE"] = "web-eid:get-signing-certificate-failure"; + Action["SIGN"] = "web-eid:sign"; + Action["SIGN_ACK"] = "web-eid:sign-ack"; + Action["SIGN_SUCCESS"] = "web-eid:sign-success"; + Action["SIGN_FAILURE"] = "web-eid:sign-failure"; +})(Action || (Action = {})); +var Action$1 = Action; + +var ErrorCode; +(function (ErrorCode) { + ErrorCode["ERR_WEBEID_ACTION_TIMEOUT"] = "ERR_WEBEID_ACTION_TIMEOUT"; + ErrorCode["ERR_WEBEID_USER_TIMEOUT"] = "ERR_WEBEID_USER_TIMEOUT"; + ErrorCode["ERR_WEBEID_VERSION_MISMATCH"] = "ERR_WEBEID_VERSION_MISMATCH"; + ErrorCode["ERR_WEBEID_VERSION_INVALID"] = "ERR_WEBEID_VERSION_INVALID"; + ErrorCode["ERR_WEBEID_EXTENSION_UNAVAILABLE"] = "ERR_WEBEID_EXTENSION_UNAVAILABLE"; + ErrorCode["ERR_WEBEID_NATIVE_UNAVAILABLE"] = "ERR_WEBEID_NATIVE_UNAVAILABLE"; + ErrorCode["ERR_WEBEID_UNKNOWN_ERROR"] = "ERR_WEBEID_UNKNOWN_ERROR"; + ErrorCode["ERR_WEBEID_CONTEXT_INSECURE"] = "ERR_WEBEID_CONTEXT_INSECURE"; + ErrorCode["ERR_WEBEID_USER_CANCELLED"] = "ERR_WEBEID_USER_CANCELLED"; + ErrorCode["ERR_WEBEID_NATIVE_INVALID_ARGUMENT"] = "ERR_WEBEID_NATIVE_INVALID_ARGUMENT"; + ErrorCode["ERR_WEBEID_NATIVE_FATAL"] = "ERR_WEBEID_NATIVE_FATAL"; + ErrorCode["ERR_WEBEID_ACTION_PENDING"] = "ERR_WEBEID_ACTION_PENDING"; + ErrorCode["ERR_WEBEID_MISSING_PARAMETER"] = "ERR_WEBEID_MISSING_PARAMETER"; +})(ErrorCode || (ErrorCode = {})); +var ErrorCode$1 = ErrorCode; + +class MissingParameterError extends Error { + constructor(message) { + super(message); + this.name = this.constructor.name; + this.code = ErrorCode$1.ERR_WEBEID_MISSING_PARAMETER; + } +} + +class ActionPendingError extends Error { + constructor(message = "same action for Web-eID browser extension is already pending") { + super(message); + this.name = this.constructor.name; + this.code = ErrorCode$1.ERR_WEBEID_ACTION_PENDING; + } +} + +class ActionTimeoutError extends Error { + constructor(message = "extension message timeout") { + super(message); + this.name = this.constructor.name; + this.code = ErrorCode$1.ERR_WEBEID_ACTION_TIMEOUT; + } +} + +const SECURE_CONTEXTS_INFO_URL = "https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts"; +class ContextInsecureError extends Error { + constructor(message = "Secure context required, see " + SECURE_CONTEXTS_INFO_URL) { + super(message); + this.name = this.constructor.name; + this.code = ErrorCode$1.ERR_WEBEID_CONTEXT_INSECURE; + } +} + +class ExtensionUnavailableError extends Error { + constructor(message = "Web-eID extension is not available") { + super(message); + this.name = this.constructor.name; + this.code = ErrorCode$1.ERR_WEBEID_EXTENSION_UNAVAILABLE; + } +} + +var config = Object.freeze({ + VERSION: "2.0.2", + EXTENSION_HANDSHAKE_TIMEOUT: 1000, + NATIVE_APP_HANDSHAKE_TIMEOUT: 5 * 1000, + DEFAULT_USER_INTERACTION_TIMEOUT: 2 * 60 * 1000, + MAX_EXTENSION_LOAD_DELAY: 1000, +}); + +class NativeFatalError extends Error { + constructor(message = "native application terminated with a fatal error") { + super(message); + this.name = this.constructor.name; + this.code = ErrorCode$1.ERR_WEBEID_NATIVE_FATAL; + } +} + +class NativeInvalidArgumentError extends Error { + constructor(message = "native application received an invalid argument") { + super(message); + this.name = this.constructor.name; + this.code = ErrorCode$1.ERR_WEBEID_NATIVE_INVALID_ARGUMENT; + } +} + +class NativeUnavailableError extends Error { + constructor(message = "Web-eID native application is not available") { + super(message); + this.name = this.constructor.name; + this.code = ErrorCode$1.ERR_WEBEID_NATIVE_UNAVAILABLE; + } +} + +class UnknownError extends Error { + constructor(message = "an unknown error occurred") { + super(message); + this.name = this.constructor.name; + this.code = ErrorCode$1.ERR_WEBEID_UNKNOWN_ERROR; + } +} + +class UserCancelledError extends Error { + constructor(message = "request was cancelled by the user") { + super(message); + this.name = this.constructor.name; + this.code = ErrorCode$1.ERR_WEBEID_USER_CANCELLED; + } +} + +class UserTimeoutError extends Error { + constructor(message = "user failed to respond in time") { + super(message); + this.name = this.constructor.name; + this.code = ErrorCode$1.ERR_WEBEID_USER_TIMEOUT; + } +} + +class VersionInvalidError extends Error { + constructor(message = "invalid version string") { + super(message); + this.name = this.constructor.name; + this.code = ErrorCode$1.ERR_WEBEID_VERSION_INVALID; + } +} + +function tmpl(strings, requiresUpdate) { + return `Update required for Web-eID ${requiresUpdate}`; +} +class VersionMismatchError extends Error { + constructor(message, versions, requiresUpdate) { + if (!message) { + if (!requiresUpdate) { + message = "requiresUpdate not provided"; + } + else if (requiresUpdate.extension && requiresUpdate.nativeApp) { + message = tmpl `${"extension and native app"}`; + } + else if (requiresUpdate.extension) { + message = tmpl `${"extension"}`; + } + else if (requiresUpdate.nativeApp) { + message = tmpl `${"native app"}`; + } + } + super(message); + this.name = this.constructor.name; + this.code = ErrorCode$1.ERR_WEBEID_VERSION_MISMATCH; + this.requiresUpdate = requiresUpdate; + if (versions) { + const { library, extension, nativeApp } = versions; + Object.assign(this, { library, extension, nativeApp }); + } + } +} + +const errorCodeToErrorClass = { + [ErrorCode$1.ERR_WEBEID_ACTION_PENDING]: ActionPendingError, + [ErrorCode$1.ERR_WEBEID_ACTION_TIMEOUT]: ActionTimeoutError, + [ErrorCode$1.ERR_WEBEID_CONTEXT_INSECURE]: ContextInsecureError, + [ErrorCode$1.ERR_WEBEID_EXTENSION_UNAVAILABLE]: ExtensionUnavailableError, + [ErrorCode$1.ERR_WEBEID_NATIVE_INVALID_ARGUMENT]: NativeInvalidArgumentError, + [ErrorCode$1.ERR_WEBEID_NATIVE_FATAL]: NativeFatalError, + [ErrorCode$1.ERR_WEBEID_NATIVE_UNAVAILABLE]: NativeUnavailableError, + [ErrorCode$1.ERR_WEBEID_USER_CANCELLED]: UserCancelledError, + [ErrorCode$1.ERR_WEBEID_USER_TIMEOUT]: UserTimeoutError, + [ErrorCode$1.ERR_WEBEID_VERSION_INVALID]: VersionInvalidError, + [ErrorCode$1.ERR_WEBEID_VERSION_MISMATCH]: VersionMismatchError, +}; +function deserializeError(errorObject) { + let error; + if (typeof errorObject.code == "string" && errorObject.code in errorCodeToErrorClass) { + const CustomError = errorCodeToErrorClass[errorObject.code]; + error = new CustomError(); + } + else { + error = new UnknownError(); + } + for (const [key, value] of Object.entries(errorObject)) { + error[key] = value; + } + return error; +} + +class WebExtensionService { + constructor() { + this.loggedWarnings = []; + this.queue = []; + window.addEventListener("message", (event) => this.receive(event)); + } + receive(event) { + var _a, _b, _c, _d, _e, _f; + if (!/^web-eid:/.test((_a = event.data) === null || _a === void 0 ? void 0 : _a.action)) + return; + const message = event.data; + const suffix = (_c = (_b = message.action) === null || _b === void 0 ? void 0 : _b.match(/success$|failure$|ack$/)) === null || _c === void 0 ? void 0 : _c[0]; + const initialAction = this.getInitialAction(message.action); + const pending = this.getPendingMessage(initialAction); + if (message.action === Action$1.WARNING) { + (_d = message.warnings) === null || _d === void 0 ? void 0 : _d.forEach((warning) => { + if (!this.loggedWarnings.includes(warning)) { + this.loggedWarnings.push(warning); + console.warn(warning.replace(/\n|\r/g, "")); + } + }); + } + else if (pending) { + switch (suffix) { + case "ack": { + clearTimeout(pending.ackTimer); + break; + } + case "success": { + this.removeFromQueue(initialAction); + (_e = pending.resolve) === null || _e === void 0 ? void 0 : _e.call(pending, message); + break; + } + case "failure": { + const failureMessage = message; + this.removeFromQueue(initialAction); + (_f = pending.reject) === null || _f === void 0 ? void 0 : _f.call(pending, failureMessage.error ? deserializeError(failureMessage.error) : failureMessage); + break; + } + } + } + } + send(message, timeout) { + if (this.getPendingMessage(message.action)) { + return Promise.reject(new ActionPendingError()); + } + else if (!window.isSecureContext) { + return Promise.reject(new ContextInsecureError()); + } + else { + const pending = { message }; + this.queue.push(pending); + pending.promise = new Promise((resolve, reject) => { + pending.resolve = resolve; + pending.reject = reject; + }); + pending.ackTimer = window.setTimeout(() => this.onAckTimeout(pending), config.EXTENSION_HANDSHAKE_TIMEOUT); + pending.replyTimer = window.setTimeout(() => this.onReplyTimeout(pending), timeout); + window.postMessage(message, "*"); + return pending.promise; + } + } + onReplyTimeout(pending) { + var _a; + this.removeFromQueue(pending.message.action); + (_a = pending.reject) === null || _a === void 0 ? void 0 : _a.call(pending, new ActionTimeoutError()); + } + onAckTimeout(pending) { + var _a; + clearTimeout(pending.replyTimer); + this.removeFromQueue(pending.message.action); + (_a = pending.reject) === null || _a === void 0 ? void 0 : _a.call(pending, new ExtensionUnavailableError()); + } + getPendingMessage(action) { + return this.queue.find((pm) => { + return pm.message.action === action; + }); + } + getInitialAction(action) { + return action.replace(/-success$|-failure$|-ack$/, ""); + } + removeFromQueue(action) { + const pending = this.getPendingMessage(action); + clearTimeout(pending === null || pending === void 0 ? void 0 : pending.replyTimer); + this.queue = this.queue.filter((pending) => (pending.message.action !== action)); + } +} + +/** + * Sleeps for a specified time before resolving the returned promise. + * + * @param milliseconds Time in milliseconds until the promise is resolved + * + * @returns Empty promise + */ +function sleep(milliseconds) { + return new Promise((resolve) => { + setTimeout(() => resolve(), milliseconds); + }); +} + +const webExtensionService = new WebExtensionService(); +const initializationTime = +new Date(); +async function extensionLoadDelay() { + const now = +new Date(); + await sleep(initializationTime + config.MAX_EXTENSION_LOAD_DELAY - now); +} +async function status() { + await extensionLoadDelay(); + const timeout = config.EXTENSION_HANDSHAKE_TIMEOUT + config.NATIVE_APP_HANDSHAKE_TIMEOUT; + const message = { + action: Action$1.STATUS, + libraryVersion: config.VERSION, + }; + try { + const { library, extension, nativeApp, } = await webExtensionService.send(message, timeout); + return { + library, + extension, + nativeApp, + }; + } + catch (error) { + error.library = config.VERSION; + throw error; + } +} +async function authenticate(challengeNonce, options) { + await extensionLoadDelay(); + if (!challengeNonce) { + throw new MissingParameterError("authenticate function requires a challengeNonce"); + } + const timeout = (config.EXTENSION_HANDSHAKE_TIMEOUT + + config.NATIVE_APP_HANDSHAKE_TIMEOUT + + ((options === null || options === void 0 ? void 0 : options.userInteractionTimeout) || config.DEFAULT_USER_INTERACTION_TIMEOUT)); + const message = { + action: Action$1.AUTHENTICATE, + libraryVersion: config.VERSION, + challengeNonce, + options, + }; + const { unverifiedCertificate, algorithm, signature, format, appVersion, } = await webExtensionService.send(message, timeout); + return { + unverifiedCertificate, + algorithm, + signature, + format, + appVersion, + }; +} +async function getSigningCertificate(options) { + await extensionLoadDelay(); + const timeout = (config.EXTENSION_HANDSHAKE_TIMEOUT + + config.NATIVE_APP_HANDSHAKE_TIMEOUT + + ((options === null || options === void 0 ? void 0 : options.userInteractionTimeout) || config.DEFAULT_USER_INTERACTION_TIMEOUT) * 2); + const message = { + action: Action$1.GET_SIGNING_CERTIFICATE, + libraryVersion: config.VERSION, + options, + }; + const { certificate, supportedSignatureAlgorithms, } = await webExtensionService.send(message, timeout); + return { + certificate, + supportedSignatureAlgorithms, + }; +} +async function sign(certificate, hash, hashFunction, options) { + await extensionLoadDelay(); + if (!certificate) { + throw new MissingParameterError("sign function requires a certificate as parameter"); + } + if (!hash) { + throw new MissingParameterError("sign function requires a hash as parameter"); + } + if (!hashFunction) { + throw new MissingParameterError("sign function requires a hashFunction as parameter"); + } + const timeout = (config.EXTENSION_HANDSHAKE_TIMEOUT + + config.NATIVE_APP_HANDSHAKE_TIMEOUT + + ((options === null || options === void 0 ? void 0 : options.userInteractionTimeout) || config.DEFAULT_USER_INTERACTION_TIMEOUT) * 2); + const message = { + action: Action$1.SIGN, + libraryVersion: config.VERSION, + certificate, + hash, + hashFunction, + options, + }; + const { signature, signatureAlgorithm, } = await webExtensionService.send(message, timeout); + return { + signature, + signatureAlgorithm, + }; +} + +export { Action$1 as Action, ErrorCode$1 as ErrorCode, authenticate, config, getSigningCertificate, sign, status }; diff --git a/example/src/main/resources/static/scripts/install-web-eid.sh b/example/src/main/resources/static/scripts/install-web-eid.sh new file mode 100644 index 0000000..c8c5ea7 --- /dev/null +++ b/example/src/main/resources/static/scripts/install-web-eid.sh @@ -0,0 +1,232 @@ +#!/bin/sh +# This script configures .deb based Linux repositories +# License: public domain +# Script https://github.com/open-eid/linux-installer +# See wiki https://github.com/open-eid/linux-installer/wiki/Linux-Packages +set -e + +# Key used for signing releases +RIA_KEY="""-----BEGIN PGP PUBLIC KEY BLOCK----- +Comment: GPGTools - https://gpgtools.org + +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 +K2czbpReKw== +=aSyh +-----END PGP PUBLIC KEY BLOCK----- +""" + +add_key() { + # keystring=`echo "$RIA_KEY" | gpg` # XXX: can't be automated, gpg always creates files on disk + keystring="0xC6C83D68 'RIA Software Signing Key '" + echo "Adding key to trusted key set" + echo "$keystring" + echo "$RIA_KEY" | gpg --dearmor | sudo tee /usr/share/keyrings/ria-repository.gpg > /dev/null +} + +test_sudo() { + if ! command -v sudo>/dev/null; then + make_fail "You must have sudo and be in sudo group\nAs root do: apt install sudo && adduser $USER sudo" + fi +} + +test_root() { + if test $(id -u) -eq 0; then + echo "You run this script as root. DO NOT RUN RANDOM SCRIPTS AS ROOT." + exit 2 + fi +} + +# add the given repository into /etc/apt/sources.list.d +add_repository() { + umask 0022 + echo "Adding RIA repository to APT sources list (/etc/apt/sources.list.d/ria-repository.list)" + echo "deb [signed-by=/usr/share/keyrings/ria-repository.gpg] https://installer.id.ee/media/ubuntu/ $1 main" | sudo tee /etc/apt/sources.list.d/ria-repository.list +} + +make_install() { + echo "Installing software (apt update && apt install web-eid)" + sudo apt update + sudo apt install "$@" +} + +make_fail() { + echo "$1" + exit 3 +} + +make_warn() { + echo "### $1" + echo "Press ENTER to continue, CTRL-C to cancel" + read -r dummy +} + +### Install Estonian ID card software + +# check for Debian derivative. +if ! command -v lsb_release>/dev/null; then + make_fail "# Not a Debian Linux :(" +fi + +# we use sudo +test_root +test_sudo + +# version name LTS supported until +# 20.04 focal LTS 2025-04 +# 22.04 jammy LTS 2027-04 +# 24.04 noble LTS 2029-04 +# 24.10 oracular - 2025-07 +LATEST_SUPPORTED_UBUNTU_CODENAME='oracular' + +# check if Debian or Ubuntu +distro=$(lsb_release -is | tr '[:upper:]' '[:lower:]') +release=$(lsb_release -rs) +codename=$(lsb_release -cs) + +case $distro in + debian) + make_warn "Debian is not officially supported" + echo "### Installing possibly missing https support for APT (apt install apt-transport-https)" + # Debian lacks https support for apt, by default + sudo apt install apt-transport-https + case "$codename" in + bullseye) + make_warn "Debian $codename is not officially supported" + make_warn "Installing from ubuntu-focal repository" + add_repository focal + ;; + bookworm) + make_warn "Debian $codename is not officially supported" + make_warn "Installing from ubuntu-jammy repository" + add_repository jammy + ;; + *) + make_fail "Debian $codename is not officially supported" + ;; + esac + ;; + ubuntu|neon|zorin) + case $distro in + neon) make_warn "Neon is not officially supported; assuming that it is equivalent to Ubuntu" ;; + *) ;; + esac + case $codename in + utopic|vivid|wily|trusty|artful|cosmic|disco|xenial|eoan|groovy|hirsute|impish|bionic|zorin|kinetic|lunar|mantic) + make_fail "Ubuntu $codename is not officially supported" + ;; + focal|jammy|noble|oracular) + add_repository $codename + ;; + *) + make_warn "Ubuntu $codename is not officially supported" + make_warn "Trying to install package for Ubuntu ${LATEST_SUPPORTED_UBUNTU_CODENAME}" + add_repository ${LATEST_SUPPORTED_UBUNTU_CODENAME} + ;; + esac + ;; + linuxmint) + case $release in + 22*) + make_warn "Linux Mint 22 is not officially supported" + add_repository noble + ;; + 21*) + make_warn "Linux Mint 21 is not officially supported" + add_repository jammy + ;; + 20*) + make_warn "Linux Mint 20 is not officially supported" + add_repository focal + ;; + *) + make_fail "Linux Mint $release is not officially supported" + ;; + esac + ;; + elementary*os|elementary) + case $release in + 7*) + make_warn "Elementary OS 7 is not officially supported" + add_repository jammy + ;; + *) + make_fail "Elementary OS $release is not officially supported" + ;; + esac + ;; + pop) + case $codename in + artful|cosmic|disco|eoan|bionic) + make_fail "Pop!_OS $codename is not officially supported" + ;; + focal|jammy) + make_warn "Pop!_OS $codename is not officially supported" + add_repository $codename + ;; + *) + make_warn "Pop!_OS $codename is not officially supported" + make_warn "Trying to install package for Pop!_OS ${LATEST_SUPPORTED_UBUNTU_CODENAME}" + add_repository ${LATEST_SUPPORTED_UBUNTU_CODENAME} + ;; + esac + ;; + *) + make_fail "$distro is not supported :(" + ;; +esac + +add_key +make_install web-eid + +echo +echo "Thank you for using Estonian ID card!" +read -p "Would you like to read instructions on how to configure browsers for using ID-card? (Y/n): " instructions +case $instructions in + [Yy]*|"" ) xdg-open "https://www.id.ee/en/article/ubuntu-id-software-installation-updating-and-removal/#removing-mozilla-firefox";; + * ) ;; +esac diff --git a/example/src/main/resources/templates/index.html b/example/src/main/resources/templates/index.html new file mode 100644 index 0000000..b13f2cb --- /dev/null +++ b/example/src/main/resources/templates/index.html @@ -0,0 +1,310 @@ + + + + + + + + + + Web eID: electronic ID smart cards on the Web + + + + +

+
+
+

Web eID: electronic ID smart cards on the Web

+

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

+

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

+

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

+ +
+

Table of contents

+ + +
+

Usage

+

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 12 or later, for Firefox and Chrome from here, +
    • +
    • on macOS 12 or later, for Safari, install the extension from App Store, +
    • +
    • on Windows 10, Windows 11, Windows Server 2016, Windows Server 2019, Windows Server + 2022, + 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. +

+ +
+

Uninstallation

+

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.

+ +

Debugging and logs

+
    +
  • + 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. +
  • +
+ +
+

Documentation

+

+ 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. +

+
+

For developers

+

+ 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. +

+
+
+
+ +
+ EU fund flags +
+ + + + diff --git a/example/src/main/resources/templates/welcome-with-file-upload-support.html b/example/src/main/resources/templates/welcome-with-file-upload-support.html new file mode 100644 index 0000000..f13ab58 --- /dev/null +++ b/example/src/main/resources/templates/welcome-with-file-upload-support.html @@ -0,0 +1,130 @@ + + + + + + Welcome! + + + + +
+
+
+
+

Sign a document

+

+
+
+

+ + +

+
+ +
+
+ + + + + diff --git a/example/src/main/resources/templates/welcome.html b/example/src/main/resources/templates/welcome.html new file mode 100644 index 0000000..67b3131 --- /dev/null +++ b/example/src/main/resources/templates/welcome.html @@ -0,0 +1,135 @@ + + + + + + + + + + Welcome! + + + + +
+
+
+
+

Digital signing

+

+
+ +
+ +
+

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

+ +
+ + +

+ + +

+
+ +
+
+ + + + diff --git a/example/src/test/java/eu/webeid/example/AuthenticationRestControllerTest.java b/example/src/test/java/eu/webeid/example/AuthenticationRestControllerTest.java new file mode 100644 index 0000000..aa6f5df --- /dev/null +++ b/example/src/test/java/eu/webeid/example/AuthenticationRestControllerTest.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2020-2024 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. + */ + +package eu.webeid.example; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import eu.webeid.example.web.rest.ChallengeController; + +import static org.assertj.core.api.Assertions.assertThat; +import static eu.webeid.security.challenge.ChallengeNonceGenerator.NONCE_LENGTH; + +@SpringBootTest +class AuthenticationRestControllerTest { + + @Autowired + ChallengeController authRestController; + + @Test + void testChallengeNonceLength() { + assertThat(authRestController.challenge().getNonce().length()) + .isEqualTo(nonceGeneratorNonceBase64Length()); + } + + private int nonceGeneratorNonceBase64Length() { + return (NONCE_LENGTH * 8 + 6 - 1) / 6 + 1; + } + +} diff --git a/example/src/test/java/eu/webeid/example/WebApplicationTest.java b/example/src/test/java/eu/webeid/example/WebApplicationTest.java new file mode 100644 index 0000000..e28e8fa --- /dev/null +++ b/example/src/test/java/eu/webeid/example/WebApplicationTest.java @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2020-2024 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. + */ + +package eu.webeid.example; + +import eu.webeid.example.testutil.Dates; +import eu.webeid.example.testutil.HttpHelper; +import eu.webeid.example.testutil.ObjectMother; +import mockit.Mock; +import mockit.MockUp; +import org.digidoc4j.impl.asic.AsicSignatureFinalizer; +import org.digidoc4j.impl.asic.xades.XadesSignature; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpStatus; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockHttpSession; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; +import eu.webeid.example.service.dto.DigestDTO; +import eu.webeid.security.challenge.ChallengeNonce; +import eu.webeid.security.util.DateAndTime; +import eu.webeid.security.validator.certvalidators.SubjectCertificateNotRevokedValidator; + +import java.security.cert.X509Certificate; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; + +@SpringBootTest +@WebAppConfiguration +public class WebApplicationTest { + + @Autowired + private WebApplicationContext context; + + @Autowired + private jakarta.servlet.Filter[] springSecurityFilterChain; + + private static DefaultMockMvcBuilder mvcBuilder; + + @BeforeEach + public void setup() { + mvcBuilder = MockMvcBuilders.webAppContextSetup(context).addFilters(springSecurityFilterChain); + } + + @Test + public void testRoot() throws Exception { + // @formatter:off + MockHttpServletResponse response = mvcBuilder + .build() + .perform(get("/")) + .andReturn() + .getResponse(); + // @formatter:on + assertEquals(HttpStatus.OK.value(), response.getStatus()); + System.out.println(response.getContentAsString()); + } + + @Test + public void testHappyFlow_LoginPrepareSignDownload() throws Exception { + + // Arrange + new MockUp() { + @Mock + public void validateCertificateNotRevoked(X509Certificate subjectCertificate) { + // Do not call real OCSP service in tests. + } + }; + + new MockUp() { + @Mock + public void validateOcspResponse(XadesSignature xadesSignature) { + // Do not call real OCSP service in tests. + } + }; + + MockHttpSession session = new MockHttpSession(); + session.setAttribute("challenge-nonce", new ChallengeNonce(ObjectMother.VALID_CHALLENGE_NONCE, DateAndTime.utcNow().plusMinutes(1))); + + Dates.setMockedSignatureDate(Dates.getSigningDateTime()); + + // Act and assert + mvcBuilder.build().perform(get("/auth/challenge")); + + MvcResult result = HttpHelper.login(mvcBuilder, session, ObjectMother.mockAuthToken()); + session = (MockHttpSession) result.getRequest().getSession(); + MockHttpServletResponse response = result.getResponse(); + assertEquals("{\"sub\":\"JAAK-KRISTJAN JÕEORG\",\"auth\":\"[ROLE_USER]\"}", response.getContentAsString()); + + /* Example how to test file upload. + response = HttpHelper.upload(mvcBuilder, session, mockMultipartFile()); + assertEquals(HttpStatus.OK.value(), response.getStatus()); + public static MockMultipartFile mockMultipartFile() { + return new MockMultipartFile("file", "test-file.txt", "text/plain", "some xml".getBytes()); + } + */ + + response = HttpHelper.prepare(mvcBuilder, session, ObjectMother.mockPrepareRequest()); + assertEquals(HttpStatus.OK.value(), response.getStatus()); + + DigestDTO digestDTO = ObjectMother.jsonStringToBean(response.getContentAsString(), DigestDTO.class); + + response = HttpHelper.sign(mvcBuilder, session, ObjectMother.mockSignRequest(digestDTO.getHash())); + assertEquals(HttpStatus.OK.value(), response.getStatus()); + + response = HttpHelper.download(mvcBuilder, session); + assertEquals(HttpStatus.OK.value(), response.getStatus()); + assertEquals("attachment; filename=example-for-signing.asice", response.getHeader("Content-Disposition")); + } +} diff --git a/example/src/test/java/eu/webeid/example/security/WebEidAjaxLoginProcessingFilterTest.java b/example/src/test/java/eu/webeid/example/security/WebEidAjaxLoginProcessingFilterTest.java new file mode 100644 index 0000000..cb95073 --- /dev/null +++ b/example/src/test/java/eu/webeid/example/security/WebEidAjaxLoginProcessingFilterTest.java @@ -0,0 +1,39 @@ +package eu.webeid.example.security; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.AuthenticationManager; + +import java.io.BufferedReader; +import java.io.StringReader; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class WebEidAjaxLoginProcessingFilterTest { + + private static final String AUTH_TOKEN = "{\"auth-token\":" + + "{\"algorithm\":\"ES384\"," + + "\"certificate\":\"MIIEAzCCA2WgAwIBAgIQHWbVWxCkcYxbzz9nBzGrDzAKBggqhkjOPQQDBDBgMQswCQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRhDA5OVFJFRS0xMDc0NzAxMzEbMBkGA1UEAwwSVEVTVCBvZiBFU1RFSUQyMDE4MB4XDTE4MTAyMzE1MzM1OVoXDTIzMTAyMjIxNTk1OVowfzELMAkGA1UEBhMCRUUxKjAoBgNVBAMMIUrDlUVPUkcsSkFBSy1LUklTVEpBTiwzODAwMTA4NTcxODEQMA4GA1UEBAwHSsOVRU9SRzEWMBQGA1UEKgwNSkFBSy1LUklTVEpBTjEaMBgGA1UEBRMRUE5PRUUtMzgwMDEwODU3MTgwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQ/u+9IncarVpgrACN6aRgUiT9lWC9H7llnxoEXe8xoCI982Md8YuJsVfRdeG5jwVfXe0N6KkHLFRARspst8qnACULkqFNat/Kj+XRwJ2UANeJ3Gl5XBr+tnLNuDf/UiR6jggHDMIIBvzAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIDiDBHBgNVHSAEQDA+MDIGCysGAQQBg5EhAQIBMCMwIQYIKwYBBQUHAgEWFWh0dHBzOi8vd3d3LnNrLmVlL0NQUzAIBgYEAI96AQIwHwYDVR0RBBgwFoEUMzgwMDEwODU3MThAZWVzdGkuZWUwHQYDVR0OBBYEFOTddHnA9rJtbLwhBNyn0xZTQGCMMGEGCCsGAQUFBwEDBFUwUzBRBgYEAI5GAQUwRzBFFj9odHRwczovL3NrLmVlL2VuL3JlcG9zaXRvcnkvY29uZGl0aW9ucy1mb3ItdXNlLW9mLWNlcnRpZmljYXRlcy8TAkVOMCAGA1UdJQEB/wQWMBQGCCsGAQUFBwMCBggrBgEFBQcDBDAfBgNVHSMEGDAWgBTAhJkpxE6fOwI09pnhClYACCk+ezBzBggrBgEFBQcBAQRnMGUwLAYIKwYBBQUHMAGGIGh0dHA6Ly9haWEuZGVtby5zay5lZS9lc3RlaWQyMDE4MDUGCCsGAQUFBzAChilodHRwOi8vYy5zay5lZS9UZXN0X29mX0VTVEVJRDIwMTguZGVyLmNydDAKBggqhkjOPQQDBAOBiwAwgYcCQgHYElkX4vn821JR41akI/lpexCnJFUf4GiOMbTfzAxpZma333R8LNrmI4zbzDp03hvMTzH49g1jcbGnaCcbboS8DAJBObenUp++L5VqldHwKAps61nM4V+TiLqD0jILnTzl+pV+LexNL3uGzUfvvDNLHnF9t6ygi8+Bsjsu3iHHyM1haKM=\"," + + "\"issuerApp\":\"https://web-eid.eu/web-eid-app/releases/2.0.0+976\"," + + "\"signature\":\"Z+r6IIx0lmXNTHrlJOTknVaOYszXba5ko1e8rjWJXd4nzIVsxeTps/Revg+tuKREkkIuIKhwvOnARdWV6vBmhjlpRUZn9aNPSDRP98T/0sPZgDp31hwsWfCAYnPzzYC9\"," + + "\"version\":\"web-eid:1\"}}"; + + @Test + void testAttemptAuthentication() throws Exception { + final HttpServletRequest request = mock(HttpServletRequest.class); + final HttpServletResponse response = mock(HttpServletResponse.class); + when(request.getMethod()).thenReturn(HttpMethod.POST.name()); + when(request.getHeader("Content-type")).thenReturn("application/json"); + when(request.getReader()).thenReturn(new BufferedReader(new StringReader(AUTH_TOKEN))); + + final AuthenticationManager authenticationManager = mock(AuthenticationManager.class); + + assertDoesNotThrow(() -> + new WebEidAjaxLoginProcessingFilter("/auth/login", authenticationManager) + .attemptAuthentication(request, response)); + } +} \ No newline at end of file diff --git a/example/src/test/java/eu/webeid/example/security/WebEidAuthenticationTest.java b/example/src/test/java/eu/webeid/example/security/WebEidAuthenticationTest.java new file mode 100644 index 0000000..630cf49 --- /dev/null +++ b/example/src/test/java/eu/webeid/example/security/WebEidAuthenticationTest.java @@ -0,0 +1,23 @@ +package eu.webeid.example.security; + +import eu.webeid.security.certificate.CertificateLoader; +import org.junit.jupiter.api.Test; +import org.springframework.security.core.Authentication; + +import java.security.cert.X509Certificate; +import java.util.Collections; + +import static org.assertj.core.api.Assertions.assertThat; + +class WebEidAuthenticationTest { + + private static final String ORGANIZATION_CERT = "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"; + + @Test + void whenOrganizationCertificate_thenSucceeds() throws Exception { + final X509Certificate certificate = CertificateLoader.decodeCertificateFromBase64(ORGANIZATION_CERT); + final Authentication authentication = WebEidAuthentication.fromCertificate(certificate, Collections.emptyList()); + assertThat(authentication.getPrincipal()).isEqualTo("Testijad.ee isikutuvastus"); + } + +} \ No newline at end of file diff --git a/example/src/test/java/eu/webeid/example/testutil/Dates.java b/example/src/test/java/eu/webeid/example/testutil/Dates.java new file mode 100644 index 0000000..c44118d --- /dev/null +++ b/example/src/test/java/eu/webeid/example/testutil/Dates.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2020-2024 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. + */ + +package eu.webeid.example.testutil; + +import eu.europa.esig.dss.model.BLevelParameters; +import mockit.Mock; +import mockit.MockUp; + +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.Date; + +public final class Dates { + + public static ZonedDateTime getSigningDateTime() { + return ZonedDateTime.of(2020, 4, 14, 13, 36, 49, 0, + ZoneId.of("Europe/Tallinn")); + } + + public static void setMockedSignatureDate(ZonedDateTime mockedDateTime) { + new MockUp() { + @Mock + public Date getSigningDate() { + return Date.from(mockedDateTime.toInstant()); + } + }; + } +} diff --git a/example/src/test/java/eu/webeid/example/testutil/HttpHelper.java b/example/src/test/java/eu/webeid/example/testutil/HttpHelper.java new file mode 100644 index 0000000..fec2621 --- /dev/null +++ b/example/src/test/java/eu/webeid/example/testutil/HttpHelper.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2020-2024 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. + */ + +package eu.webeid.example.testutil; + +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockHttpSession; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder; +import eu.webeid.example.security.dto.AuthTokenDTO; +import eu.webeid.example.service.dto.CertificateDTO; +import eu.webeid.example.service.dto.SignatureDTO; + +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; + +public class HttpHelper { + + public static MvcResult login(DefaultMockMvcBuilder mvcBuilder, MockHttpSession session, AuthTokenDTO authTokenDTO) throws Exception { + // @formatter:off + return mvcBuilder + .build() + .perform(post("/auth/login") + .session(session) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(ObjectMother.toJson(authTokenDTO))) + .andReturn(); + // @formatter:on + } + + public static MockHttpServletResponse upload(DefaultMockMvcBuilder mvcBuilder, MockHttpSession session, MockMultipartFile mockMultipartFile) throws Exception { + // @formatter:off + return mvcBuilder + .build() + .perform(MockMvcRequestBuilders.multipart("/sign/upload") + .file(mockMultipartFile) + .session(session)) + .andReturn() + .getResponse(); + // @formatter:on + } + + public static MockHttpServletResponse prepare(DefaultMockMvcBuilder mvcBuilder, MockHttpSession session, CertificateDTO certificateDTO) throws Exception { + // @formatter:off + return mvcBuilder + .build() + .perform(post("/sign/prepare") + .session(session) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(ObjectMother.toJson(certificateDTO))) + .andReturn() + .getResponse(); + // @formatter:on + } + + public static MockHttpServletResponse sign(DefaultMockMvcBuilder mvcBuilder, MockHttpSession session, SignatureDTO signatureDTO) throws Exception { + // @formatter:off + return mvcBuilder + .build() + .perform(post("/sign/sign") + .session(session) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(ObjectMother.toJson(signatureDTO))) + .andReturn() + .getResponse(); + // @formatter:on + } + + public static MockHttpServletResponse download(DefaultMockMvcBuilder mvcBuilder, MockHttpSession session) throws Exception { + // @formatter:off + return mvcBuilder + .build() + .perform(get("/sign/download") + .session(session)) + .andReturn() + .getResponse(); + // @formatter:on + } +} diff --git a/example/src/test/java/eu/webeid/example/testutil/ObjectMother.java b/example/src/test/java/eu/webeid/example/testutil/ObjectMother.java new file mode 100644 index 0000000..f6103d5 --- /dev/null +++ b/example/src/test/java/eu/webeid/example/testutil/ObjectMother.java @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2020-2024 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. + */ + +package eu.webeid.example.testutil; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import eu.webeid.example.service.dto.SignatureAlgorithmDTO; +import eu.webeid.security.authtoken.WebEidAuthToken; +import org.apache.commons.lang3.ArrayUtils; +import org.digidoc4j.DigestAlgorithm; +import org.digidoc4j.exceptions.DigiDoc4JException; +import eu.webeid.example.security.dto.AuthTokenDTO; +import eu.webeid.example.service.dto.CertificateDTO; +import eu.webeid.example.service.dto.SignatureDTO; + +import jakarta.xml.bind.DatatypeConverter; +import java.io.FileInputStream; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.Base64; +import java.util.List; + +public class ObjectMother { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + private static final String TEST_PKI_CONTAINER = "src/test/resources/signout.p12"; + private static final String TEST_PKI_CONTAINER_PASSWORD = "test"; + private static final WebEidAuthToken VALID_AUTH_TOKEN; + + static { + try { + VALID_AUTH_TOKEN = MAPPER.readValue( + "{\"algorithm\":\"ES384\"," + + "\"unverifiedCertificate\":\"MIIEBDCCA2WgAwIBAgIQY5OGshxoPMFg+Wfc0gFEaTAKBggqhkjOPQQDBDBgMQswCQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRhDA5OVFJFRS0xMDc0NzAxMzEbMBkGA1UEAwwSVEVTVCBvZiBFU1RFSUQyMDE4MB4XDTIxMDcyMjEyNDMwOFoXDTI2MDcwOTIxNTk1OVowfzELMAkGA1UEBhMCRUUxKjAoBgNVBAMMIUrDlUVPUkcsSkFBSy1LUklTVEpBTiwzODAwMTA4NTcxODEQMA4GA1UEBAwHSsOVRU9SRzEWMBQGA1UEKgwNSkFBSy1LUklTVEpBTjEaMBgGA1UEBRMRUE5PRUUtMzgwMDEwODU3MTgwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQmwEKsJTjaMHSaZj19hb9EJaJlwbKc5VFzmlGMFSJVk4dDy+eUxa5KOA7tWXqzcmhh5SYdv+MxcaQKlKWLMa36pfgv20FpEDb03GCtLqjLTRZ7649PugAQ5EmAqIic29CjggHDMIIBvzAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIDiDBHBgNVHSAEQDA+MDIGCysGAQQBg5EhAQIBMCMwIQYIKwYBBQUHAgEWFWh0dHBzOi8vd3d3LnNrLmVlL0NQUzAIBgYEAI96AQIwHwYDVR0RBBgwFoEUMzgwMDEwODU3MThAZWVzdGkuZWUwHQYDVR0OBBYEFPlp/ceABC52itoqppEmbf71TJz6MGEGCCsGAQUFBwEDBFUwUzBRBgYEAI5GAQUwRzBFFj9odHRwczovL3NrLmVlL2VuL3JlcG9zaXRvcnkvY29uZGl0aW9ucy1mb3ItdXNlLW9mLWNlcnRpZmljYXRlcy8TAkVOMCAGA1UdJQEB/wQWMBQGCCsGAQUFBwMCBggrBgEFBQcDBDAfBgNVHSMEGDAWgBTAhJkpxE6fOwI09pnhClYACCk+ezBzBggrBgEFBQcBAQRnMGUwLAYIKwYBBQUHMAGGIGh0dHA6Ly9haWEuZGVtby5zay5lZS9lc3RlaWQyMDE4MDUGCCsGAQUFBzAChilodHRwOi8vYy5zay5lZS9UZXN0X29mX0VTVEVJRDIwMTguZGVyLmNydDAKBggqhkjOPQQDBAOBjAAwgYgCQgDCAgybz0u3W+tGI+AX+PiI5CrE9ptEHO5eezR1Jo4j7iGaO0i39xTGUB+NSC7P6AQbyE/ywqJjA1a62jTLcS9GHAJCARxN4NO4eVdWU3zVohCXm8WN3DWA7XUcn9TZiLGQ29P4xfQZOXJi/z4PNRRsR4plvSNB3dfyBvZn31HhC7my8woi\"," + + "\"appVersion\":\"https://web-eid.eu/web-eid-app/releases/2.5.0+0\"," + + "\"signature\":\"0Ov7ME6pTY1K2GXMj8Wxov/o2fGIMEds8OMY5dKdkB0nrqQX7fG1E5mnsbvyHpMDecMUH6Yg+p1HXdgB/lLqOcFZjt/OVXPjAAApC5d1YgRYATDcxsR1zqQwiNcHdmWn\"," + + "\"format\":\"web-eid:1.0\"}", + WebEidAuthToken.class); + } catch (JsonProcessingException e) { + throw new RuntimeException("Token parsing failed"); + } + } + + public static final String VALID_CHALLENGE_NONCE = "12345678123456781234567812345678912356789123"; + + public static AuthTokenDTO mockAuthToken() { + AuthTokenDTO authToken = new AuthTokenDTO(); + authToken.setToken(VALID_AUTH_TOKEN); + return authToken; + } + + public static String toJson(Object object) throws JsonProcessingException { + return MAPPER.writeValueAsString(object); + } + + public static String mockCertificateInBase64() { + try { + X509Certificate certificate = getSigningCert(); + byte[] derEncodedCertificate = certificate.getEncoded(); + return DatatypeConverter.printBase64Binary(derEncodedCertificate); + } catch (Exception e) { + throw new RuntimeException("Certificate loading failed", e); + } + } + + public static String mockSignatureInBase64(String digestToSign) { + return signDigest(DatatypeConverter.parseBase64Binary(digestToSign)); + } + + public static T jsonStringToBean(String jsonString, Class valueType) throws JsonProcessingException { + return MAPPER.readValue(jsonString, valueType); + } + + public static CertificateDTO mockPrepareRequest() { + CertificateDTO certificateDTO = new CertificateDTO(); + certificateDTO.setCertificate(mockCertificateInBase64()); + final SignatureAlgorithmDTO signatureAlgorithmDTO = new SignatureAlgorithmDTO(); + signatureAlgorithmDTO.setCryptoAlgorithm("RSA"); + signatureAlgorithmDTO.setHashFunction("SHA-256"); + signatureAlgorithmDTO.setPaddingScheme("PKCS1.5"); + certificateDTO.setSupportedSignatureAlgorithms(List.of(signatureAlgorithmDTO)); + return certificateDTO; + } + + public static SignatureDTO mockSignRequest(String digestToSign) { + SignatureDTO signatureDTO = new SignatureDTO(); + signatureDTO.setBase64Signature(mockSignatureInBase64(digestToSign)); + return signatureDTO; + } + + private static X509Certificate getSigningCert() { + try { + KeyStore keyStore = KeyStore.getInstance("PKCS12"); + try (FileInputStream stream = new FileInputStream(TEST_PKI_CONTAINER)) { + keyStore.load(stream, TEST_PKI_CONTAINER_PASSWORD.toCharArray()); + } + return (X509Certificate) keyStore.getCertificate("1"); + } catch (Exception e) { + throw new RuntimeException("Loading signer cert failed"); + } + } + + private static byte[] sign(byte[] dataToSign) { + try { + KeyStore keyStore = KeyStore.getInstance("PKCS12"); + try (FileInputStream stream = new FileInputStream(TEST_PKI_CONTAINER)) { + keyStore.load(stream, TEST_PKI_CONTAINER_PASSWORD.toCharArray()); + } + PrivateKey privateKey = (PrivateKey) keyStore.getKey("1", TEST_PKI_CONTAINER_PASSWORD.toCharArray()); + final String javaSignatureAlgorithm = "NONEwith" + privateKey.getAlgorithm(); + + return encrypt(javaSignatureAlgorithm, privateKey, addOID(dataToSign)); + } catch (Exception e) { + throw new DigiDoc4JException("Loading private key failed"); + } + } + + private static byte[] addOID(byte[] digest) { + return ArrayUtils.addAll(DigestAlgorithm.SHA256.digestInfoPrefix(), digest); + } + + private static byte[] encrypt(final String javaSignatureAlgorithm, final PrivateKey privateKey, final byte[] bytes) { + try { + java.security.Signature signature = java.security.Signature.getInstance(javaSignatureAlgorithm); + signature.initSign(privateKey); + signature.update(bytes); + return signature.sign(); + } catch (GeneralSecurityException e) { + throw new RuntimeException(e); + } + } + + private static String signDigest(byte[] digestToSign) { + byte[] signatureValue = sign(digestToSign); + return Base64.getEncoder().encodeToString(signatureValue); + } + +} diff --git a/example/src/test/resources/application-dev.yaml b/example/src/test/resources/application-dev.yaml new file mode 100644 index 0000000..5e60766 --- /dev/null +++ b/example/src/test/resources/application-dev.yaml @@ -0,0 +1,4 @@ +web-eid-auth-token: + validation: + use-digidoc4j-prod-configuration: false + local-origin: "https://ria.ee" diff --git a/example/src/test/resources/signout.p12 b/example/src/test/resources/signout.p12 new file mode 100644 index 0000000000000000000000000000000000000000..3fd499be5f210e1b691bfd189db564e0049eacc8 GIT binary patch literal 2890 zcmY+EXE+-Q7snG4F=~_;wH0kp1QnyDN*g5htd$nEM~s?vix{P9&ng;QV^%0ywbiPc zFIsyxW@u5lDc;`adEa}V`{6w2d4B(M&WH2i2gflXX@InF920tAO z4?sA^T{w<$6OIF8{uO}@K{zn_H`e}LT69eR+q%FAq`~7rf5LH~V>pbC@&EYuab6HK z>d`xDd$n}pC6fLkpE%d2X~wpTG&CRp2nXudH3+7!2+Uqdk+roqY&Eo(5(C2p52r#W zUGA(WE>Ons=k6Jf26Fm`ZS8`?#a~dxVni=N5VsB!`jj>(;qa(IXd$|##s}M_3L~8F zpeUA_L9>>et4D2j2b2_=Xxcwc2TbH8^|dARe+hqXhzK~am3k|pqSPzP3GazH3}mWh zT8QD!b`lr)ib-s_0KfRfpR_OF`)T-Mu6i2JsK&EOiHXA+oK|b~l6f=0LMk=O$&c&% zTE}9MK!UCA;yuLu9!%RGiu7PrNzj@WyM!6FXivKxZAp&#%mj2D{WU8n`WPa{ICZLC z@i1+i3{2>rgzyKJ`$X6Yym2?fuM7xiXVk@^`giZ&`Z)EUwB|2t6^_md+ z_5kENtTc)LX|m&u!v5Z+Y%7t|5?d4D7!wgQCEb;eibKh-i4s#ke?K=2&6fO$G8B+` z9&7pV1<(mh@g~f_5^M^l5x`EdAHP1eAX?E2p zhwQKavX}6;hV|0BxkwPB*hdeaQxnZoWjO!LfekV{Rt834mW*rXk}q+HmAzjY>vMdv zr+RXxDQ>y@6xGyAn4aU5&);gJkGyxUB;1c@&N~f?x(XG?Um>8S)))j^1tKvD3gPyA zHN5JDcjVLaZdE-}!?QWm=sOi%?ile6zL(qkf+UJ zdb6&wWrAACVTcwp$wNi7|JEE4HMQsVY^ygM8;E&f_>Vug@o}Q_PYSo0?8~+6&&L7J za$f|zH{)cEeS7p~uv9a0uRV(uE0u#FT0Nn%qx4*Uea|*)1&`4iSFePN$esWtePkqy zBP=rAB&)Z)P2Rzn$uO!hkI?ysil37=TsUGIJ}wP-=1TA=(PBx!hf3M5Jg#Bh64^LY zSQ|1%(|9mYC&gU2p72R`#yH*Y6IYA*B|v%F>^HpcG`H}5GMHRF;l6t#=;l?)f=FxI z<3dcp_-KyV34&Y4C6wiss?h>lXA2r~y#%mhTmRsrIZzJAv}#k`A*~z#4#vK0brs<2 z#Xk4!d9H%sTTr*AvmU4TDwc;TQc&q zaVK&J_{G0X$pDoD3DN-M;3EGZPB|!u-OSC)Nmvdck5H0RQB+Yb#y8R~mg)W^OuYQYjgU$_Rw}h0c zCW~LtCbso<0Ofe(L9FQB4U*=IC0AFm*-Rhi_-u!*wzw~08M4Ls6t#X9gNy#n0=gRL zd-9Fai`PGdjpqAq5B4@soWr<;@ARcsx|qD&$ZiVO3$5sv@upJEMy#Jl((j)m_OiJ{ zl2gh|sVSF&lb-0Ox(LxJ#R|^_l`1!P_Z8`>CFWWKH9wz3p)p6A1ScCZW}0YiSNfxK z4U2?`QoR;inhF9AYxsgm*A}27UYMPgAC0dOMOaEiAR#qn{ASu5Pg8@3{Wttl_8J6R z-cVHJ&fBorK23Kl1c@Fok-I`Cdk4lS?89)H)a-LJ_hFfw^2T=ksQtKzLoW>=0Od#Y9wxkqJ_R$p@_jhlPBpb`}+Ns7XNb=mLR9S&KngQ;mr>nA* zW1$E2L0wx-MOoi&KTDSP5aaGQcp_B=bF|ViPS{pg$tP)?&1v`b#YePc9a5)J`5@CX=vWIxY+BtMyc@L5XM`k zyK0`P4Sx&^{(KfmmpVNPRzQkVD7UuceuiIS>EnAvA6RylUQMO`iEY$`L%E zpm_4U&r@nXFI1+v|v>d43I{%+mdn55{ir07_cb_KK zN!31GkGp(@OCpEg6x3M?6&@j=FvsR@9OAEq5reB$m13sevO5P>ilk2$(yVzQy1Ka7 zWki>5)x`%xQUsosMD`sit?&3y*T2cT2 literal 0 HcmV?d00001