diff --git a/.github/ISSUE_TEMPLATE/01_bug.md b/.github/ISSUE_TEMPLATE/01_bug.md new file mode 100644 index 00000000..0c12e2dc --- /dev/null +++ b/.github/ISSUE_TEMPLATE/01_bug.md @@ -0,0 +1,43 @@ +--- +name: "\U0001F6A8 Bug" +about: Did you come across a bug or unexpected behaviour differing from the docs? +labels: bug +--- + + + +## Describe the bug + + + +## Expected behaviour + + + +## Steps to reproduce the issue + + + + + +## Technical details + +- Host Machine OS (Windows/Linux/Mac): + +## Possible Fix + + + +## Additional context + + diff --git a/.github/ISSUE_TEMPLATE/02_feature_request.md b/.github/ISSUE_TEMPLATE/02_feature_request.md new file mode 100644 index 00000000..ad11b6b4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/02_feature_request.md @@ -0,0 +1,31 @@ +--- +name: "\U0001F381 Feature Request" +about: Do you have an idea for a new feature? +labels: feature request +--- + + + +## Feature description + + + +## Problem and motivation + + + +## Is this something you're interested in working on + + diff --git a/.github/ISSUE_TEMPLATE/03_enhancement.md b/.github/ISSUE_TEMPLATE/03_enhancement.md new file mode 100644 index 00000000..bbdc3b75 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/03_enhancement.md @@ -0,0 +1,30 @@ +--- +name: "\u23F1\uFE0F Enhancement" +about: Do you have an idea for an enhancement? +labels: enhancement +--- + + + +## Current Implementation + + + +## Suggested Enhancement + + + +## Expected Benefits + + diff --git a/.github/ISSUE_TEMPLATE/04_question.md b/.github/ISSUE_TEMPLATE/04_question.md new file mode 100644 index 00000000..2be9b929 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/04_question.md @@ -0,0 +1,20 @@ +--- +name: "\U00002753 Question" +about: If you have questions about pieces of the code or documentation for this component, please post them here. +labels: question +--- + + + +## Your Question + + + +* Source File: +* Line(s): +* Question: diff --git a/.github/workflows/ci-dependency-check.yml b/.github/workflows/ci-dependency-check.yml new file mode 100644 index 00000000..e43fa93b --- /dev/null +++ b/.github/workflows/ci-dependency-check.yml @@ -0,0 +1,36 @@ +name: Dependency Check (OWASP) +on: + schedule: + - cron: '48 02 * * 0' # Each Sunday at 02:48 UTC + push: + branches: + - main + workflow_dispatch: + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/setup-java@v2 + with: + java-version: 11 + distribution: adopt + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - uses: actions/cache@v2 + with: + path: | + ~/.m2/repository + key: ${{ runner.os }}-${{ hashFiles('**/pom.xml') }} + - name: mvn + run: >- + mvn dependency-check:check + --batch-mode + --file ./pom.xml + --settings ./settings.xml + --define app.packages.username="${APP_PACKAGES_USERNAME}" + --define app.packages.password="${APP_PACKAGES_PASSWORD}"; + env: + APP_PACKAGES_USERNAME: ${{ github.actor }} + APP_PACKAGES_PASSWORD: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/ci-main.yml b/.github/workflows/ci-main.yml new file mode 100644 index 00000000..874e618e --- /dev/null +++ b/.github/workflows/ci-main.yml @@ -0,0 +1,148 @@ +name: ci-main +on: + push: + branches: + - main +jobs: + build: + runs-on: ubuntu-20.04 + steps: + - uses: actions/setup-java@v2 + with: + java-version: 11 + distribution: adopt + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - uses: actions/cache@v2 + with: + path: | + ~/.m2/repository + key: ${{ runner.os }}-${{ hashFiles('**/pom.xml') }} + - name: version + run: >- + APP_SHA=$(git rev-parse --short ${GITHUB_SHA}); + APP_REV=$(git rev-list --tags --max-count=1); + APP_TAG=$(git describe --tags ${APP_REV} 2> /dev/null || echo 0.0.0); + APP_VERSION=${APP_TAG}-${APP_SHA}; + echo "APP_SHA=${APP_SHA}" >> ${GITHUB_ENV}; + echo "APP_TAG=${APP_TAG}" >> ${GITHUB_ENV}; + echo "APP_VERSION=${APP_VERSION}" >> ${GITHUB_ENV}; + - name: mvn + run: >- + mvn versions:set + --batch-mode + --file ./pom.xml + --settings ./settings.xml + --define newVersion="${APP_VERSION}"; + mvn clean package + --batch-mode + --file ./pom.xml + --settings ./settings.xml + --define app.packages.username="${APP_PACKAGES_USERNAME}" + --define app.packages.password="${APP_PACKAGES_PASSWORD}"; + env: + APP_PACKAGES_USERNAME: ${{ github.actor }} + APP_PACKAGES_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + - name: Upload ZIP for TST + uses: actions/upload-artifact@v2 + with: + name: DGCG001_TST + path: target/DGCG001_TST* + - name: Upload ZIP for ACC + uses: actions/upload-artifact@v2 + with: + name: DGCG001_ACC + path: target/DGCG001_ACC* + - name: Upload ZIP for PRD + uses: actions/upload-artifact@v2 + with: + name: DGCG001_PRD + path: target/DGCG001_PRD* + build-docker: + runs-on: ubuntu-20.04 + steps: + - uses: actions/setup-java@v2 + with: + java-version: 11 + distribution: adopt + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - uses: actions/cache@v2 + with: + path: | + ~/.m2/repository + key: ${{ runner.os }}-${{ hashFiles('**/pom.xml') }} + - name: version + run: >- + APP_SHA=$(git rev-parse --short ${GITHUB_SHA}); + APP_REV=$(git rev-list --tags --max-count=1); + APP_TAG=$(git describe --tags ${APP_REV} 2> /dev/null || echo 0.0.0); + APP_VERSION=${APP_TAG}-${APP_SHA}; + echo "APP_SHA=${APP_SHA}" >> ${GITHUB_ENV}; + echo "APP_TAG=${APP_TAG}" >> ${GITHUB_ENV}; + echo "APP_VERSION=${APP_VERSION}" >> ${GITHUB_ENV}; + - name: mvn + run: >- + mvn versions:set + --batch-mode + --file ./pom.xml + --settings ./settings.xml + --define newVersion="${APP_VERSION}"; + mvn clean install -P docker + --batch-mode + --file ./pom.xml + --settings ./settings.xml + --define app.packages.username="${APP_PACKAGES_USERNAME}" + --define app.packages.password="${APP_PACKAGES_PASSWORD}"; + env: + APP_PACKAGES_USERNAME: ${{ github.actor }} + APP_PACKAGES_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + - name: docker + run: >- + echo "${APP_PACKAGES_PASSWORD}" | + docker login "${APP_PACKAGES_URL}" + --username "${APP_PACKAGES_USERNAME}" + --password-stdin; + docker build . + --file ./Dockerfile + --tag "${APP_PACKAGES_URL}:${APP_VERSION}"; + docker push "${APP_PACKAGES_URL}:${APP_VERSION}"; + env: + APP_PACKAGES_URL: docker.pkg.github.com/${{ github.repository }}/dgc-gateway + APP_PACKAGES_USERNAME: ${{ github.actor }} + APP_PACKAGES_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + license: + runs-on: ubuntu-20.04 + steps: + - uses: actions/setup-java@v2 + with: + java-version: 11 + distribution: adopt + - uses: actions/checkout@v2 + with: + token: ${{ secrets.GPR_TOKEN_F11H }} + - uses: actions/cache@v2 + with: + path: | + ~/.m2/repository + key: ${{ runner.os }}-${{ hashFiles('**/pom.xml') }} + - name: mvn + run: >- + mvn license:update-file-header license:add-third-party + --batch-mode + --file ./pom.xml + --settings ./settings.xml + --define app.packages.username="${APP_PACKAGES_USERNAME}" + --define app.packages.password="${APP_PACKAGES_PASSWORD}"; + env: + APP_PACKAGES_USERNAME: ${{ github.actor }} + APP_PACKAGES_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + - name: Commit and Push changes + run: | + git config user.name github-actions + git config user.email github-actions@github.com + git add . + git diff --quiet && git diff --staged --quiet || git commit -m "Update License Header and Third Party Notices" + git push diff --git a/.github/workflows/ci-openapi.yml b/.github/workflows/ci-openapi.yml new file mode 100644 index 00000000..af396736 --- /dev/null +++ b/.github/workflows/ci-openapi.yml @@ -0,0 +1,67 @@ +name: ci-openapi +on: + workflow_dispatch: + release: + types: + - created +jobs: + release: + runs-on: ubuntu-20.04 + steps: + - uses: actions/setup-java@v2 + with: + java-version: 11 + distribution: adopt + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - uses: actions/cache@v2 + with: + path: | + ~/.m2/repository + key: ${{ runner.os }}-${{ hashFiles('**/pom.xml') }} + - name: version + run: >- + APP_SHA=$(git rev-parse --short ${GITHUB_SHA}); + APP_TAG=${GITHUB_REF/refs\/tags\/} + APP_VERSION=${APP_TAG}; + echo "APP_SHA=${APP_SHA}" >> ${GITHUB_ENV}; + echo "APP_TAG=${APP_TAG}" >> ${GITHUB_ENV}; + echo "APP_VERSION=${APP_VERSION}" >> ${GITHUB_ENV}; + - name: mvn + run: >- + mvn versions:set + --batch-mode + --file ./pom.xml + --settings ./settings.xml + --define newVersion="${APP_VERSION}"; + mvn clean verify + --batch-mode + --file ./pom.xml + --settings ./settings.xml + --define app.packages.username="${APP_PACKAGES_USERNAME}" + --define app.packages.password="${APP_PACKAGES_PASSWORD}"; + env: + APP_PACKAGES_USERNAME: ${{ github.actor }} + APP_PACKAGES_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + - name: Upload Artifact + uses: actions/upload-artifact@v2 + with: + name: openapi.json + path: target/openapi.json + - name: Checkout OpenApi Doc Branch + uses: actions/checkout@v2 + with: + ref: openapi-doc + - name: Delete existing openapi.json + run: rm -f openapi.json + - name: Download openapi.json + uses: actions/download-artifact@v2 + with: + name: openapi.json + - name: Commit and Push changes + run: | + git config user.name github-actions + git config user.email github-actions@github.com + git commit -a --allow-empty -m "Update OpenAPI JSON" + git push diff --git a/.github/workflows/ci-pull-request.yml b/.github/workflows/ci-pull-request.yml new file mode 100644 index 00000000..c9233c81 --- /dev/null +++ b/.github/workflows/ci-pull-request.yml @@ -0,0 +1,45 @@ +name: ci-pull-request +on: + pull_request: + types: + - opened + - synchronize + - reopened +jobs: + build: + runs-on: ubuntu-20.04 + steps: + - uses: actions/setup-java@v2 + with: + java-version: 11 + distribution: adopt + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - uses: actions/cache@v2 + with: + path: | + ~/.m2/repository + key: ${{ runner.os }}-${{ hashFiles('**/pom.xml') }} + - name: mvn + run: >- + mvn clean package + --batch-mode + --file ./pom.xml + --settings ./settings.xml + --define app.packages.username="${APP_PACKAGES_USERNAME}" + --define app.packages.password="${APP_PACKAGES_PASSWORD}"; + mvn clean package -P docker + --batch-mode + --file ./pom.xml + --settings ./settings.xml + --define app.packages.username="${APP_PACKAGES_USERNAME}" + --define app.packages.password="${APP_PACKAGES_PASSWORD}" + -DskipTests; + env: + APP_PACKAGES_USERNAME: ${{ github.actor }} + APP_PACKAGES_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + - name: docker + run: >- + docker build . + --file ./Dockerfile; diff --git a/.github/workflows/ci-release-notes.yml b/.github/workflows/ci-release-notes.yml new file mode 100644 index 00000000..b12c3216 --- /dev/null +++ b/.github/workflows/ci-release-notes.yml @@ -0,0 +1,25 @@ +name: ci-release-notes +on: + release: + types: + - created +jobs: + release-notes: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: version + run: >- + APP_SHA=$(git rev-parse --short ${GITHUB_SHA}); + APP_TAG=${GITHUB_REF/refs\/tags\/} + APP_VERSION=${APP_TAG}; + echo "APP_SHA=${APP_SHA}" >> ${GITHUB_ENV}; + echo "APP_TAG=${APP_TAG}" >> ${GITHUB_ENV}; + echo "APP_VERSION=${APP_VERSION}" >> ${GITHUB_ENV}; + - name: release-notes + run: npx github-release-notes release --override --tags ${APP_TAG} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GREN_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/ci-release.yml b/.github/workflows/ci-release.yml new file mode 100644 index 00000000..f28aebf6 --- /dev/null +++ b/.github/workflows/ci-release.yml @@ -0,0 +1,79 @@ +name: ci-release +on: + release: + types: + - created +jobs: + release: + runs-on: ubuntu-20.04 + steps: + - uses: actions/setup-java@v2 + with: + java-version: 11 + distribution: adopt + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - uses: actions/cache@v2 + with: + path: | + ~/.m2/repository + key: ${{ runner.os }}-${{ hashFiles('**/pom.xml') }} + - name: version + run: >- + APP_SHA=$(git rev-parse --short ${GITHUB_SHA}); + APP_TAG=${GITHUB_REF/refs\/tags\/} + APP_VERSION=${APP_TAG}; + echo "APP_SHA=${APP_SHA}" >> ${GITHUB_ENV}; + echo "APP_TAG=${APP_TAG}" >> ${GITHUB_ENV}; + echo "APP_VERSION=${APP_VERSION}" >> ${GITHUB_ENV}; + - name: mvn + run: >- + mvn versions:set + --batch-mode + --file ./pom.xml + --settings ./settings.xml + --define newVersion="${APP_VERSION}"; + mvn clean verify + --batch-mode + --file ./pom.xml + --settings ./settings.xml + --define app.packages.username="${APP_PACKAGES_USERNAME}" + --define app.packages.password="${APP_PACKAGES_PASSWORD}"; + env: + APP_PACKAGES_USERNAME: ${{ github.actor }} + APP_PACKAGES_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + - name: Upload Artifacts + run: | + for f in target/*.zip; do gh release upload ${APP_TAG} --clobber $f; done + gh release upload ${APP_TAG} --clobber target/openapi.json + gh release upload ${APP_TAG} --clobber src/main/resources/validation-rule.schema.json + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: mvn docker + run: >- + mvn clean package -P docker + --batch-mode + --file ./pom.xml + --settings ./settings.xml + --define app.packages.username="${APP_PACKAGES_USERNAME}" + --define app.packages.password="${APP_PACKAGES_PASSWORD}"; + env: + APP_PACKAGES_USERNAME: ${{ github.actor }} + APP_PACKAGES_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + - name: docker + run: >- + echo "${APP_PACKAGES_PASSWORD}" | + docker login "${APP_PACKAGES_URL}" + --username "${APP_PACKAGES_USERNAME}" + --password-stdin; + docker build . + --file ./Dockerfile + --tag "${APP_PACKAGES_URL}:latest" + --tag "${APP_PACKAGES_URL}:${APP_VERSION}"; + docker push "${APP_PACKAGES_URL}:latest"; + docker push "${APP_PACKAGES_URL}:${APP_VERSION}"; + env: + APP_PACKAGES_URL: docker.pkg.github.com/${{ github.repository }}/container + APP_PACKAGES_USERNAME: ${{ github.actor }} + APP_PACKAGES_PASSWORD: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/ci-sonar.yml b/.github/workflows/ci-sonar.yml new file mode 100644 index 00000000..90171d45 --- /dev/null +++ b/.github/workflows/ci-sonar.yml @@ -0,0 +1,39 @@ +name: ci-sonar +on: + push: + branches: + - main + pull_request: + types: + - opened + - synchronize + - reopened +jobs: + sonar: + runs-on: ubuntu-20.04 + steps: + - uses: actions/setup-java@v2 + with: + java-version: 11 + distribution: adopt + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - uses: actions/cache@v2 + with: + path: | + ~/.m2/repository + key: ${{ runner.os }}-${{ hashFiles('**/pom.xml') }} + - name: mvn + run: >- + mvn verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar + --batch-mode + --file ./pom.xml + --settings ./settings.xml + --define app.packages.username="${APP_PACKAGES_USERNAME}" + --define app.packages.password="${APP_PACKAGES_PASSWORD}"; + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + APP_PACKAGES_USERNAME: ${{ github.actor }} + APP_PACKAGES_PASSWORD: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..dd5934d9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,45 @@ +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 +.jpb + +### NetBeans ### +/nbproject/ +/nbbuild/ +/dist/ +/.nb-gradle/ +build/ + +### VS Code ### +.vscode/ + +### Others ### +~$*.docx +*.b64 +/testdata/ +*.log + +/keystore + +/tools/* +!/tools/*.bat +!/tools/*.sh + +certs/* diff --git a/.grenrc.js b/.grenrc.js new file mode 100644 index 00000000..e50821ee --- /dev/null +++ b/.grenrc.js @@ -0,0 +1,30 @@ +module.exports = { + "dataSource": "prs", + "prefix": "", + "onlyMilestones": false, + "groupBy": { + "Enhancements": [ + "enhancement", + "internal" + ], + "Bug Fixes": [ + "bug" + ], + "Documentation": [ + "documentation" + ], + "Others": [ + "other" + ] + }, + "changelogFilename": "CHANGELOG.md", + "template": { + commit: ({ message, url, author, name }) => `- [${message}](${url}) - ${author ? `@${author}` : name}`, + issue: "- {{name}} [{{text}}]({{url}})", + noLabel: "other", + group: "\n#### {{heading}}\n", + changelogTitle: "# Changelog\n\n", + release: "## {{release}} ({{date}})\n{{body}}", + releaseSeparator: "\n---\n\n" + } +} diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 00000000..94e2fb2a --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,8 @@ +# This file provides an overview of code owners in this repository. + +# Each line is a file pattern followed by one or more owners. +# The last matching pattern has the most precedence. +# For more details, read the following article on GitHub: https://help.github.com/articles/about-codeowners/. + +# These are the default owners for the whole content of this repository. The default owners are automatically added as reviewers when you open a pull request, unless different owners are specified in the file. +* @eu-digital-green-certificates/dgc-gateway-members diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..e1811e07 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,130 @@ + +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +[opensource@telekom.de](mailto:opensource@telekom.de). +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..d97f3776 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,75 @@ +# Contributing + +## Code of conduct + +All members of the project community must abide by the [Contributor Covenant, version 2.0](CODE_OF_CONDUCT.md). +Only by respecting each other can we develop a productive, collaborative community. +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting [opensource@telekom.de](mailto:opensource@telekom.de) and/or a project maintainer. + +We appreciate your courtesy of avoiding political questions here. Issues which are not related to the project itself will be closed by our community managers. + +## Engaging in our project + +We use GitHub to manage reviews of pull requests. + +* If you are a new contributor, see: [Steps to Contribute](#steps-to-contribute) + +* If you have a trivial fix or improvement, go ahead and create a pull request, addressing (with `@...`) a suitable maintainer of this repository (see [CODEOWNERS](CODEOWNERS) of the repository you want to contribute to) in the description of the pull request. + +* If you plan to do something more involved, please reach out to us and send an [email](mailto:opensource@telekom.de). This will avoid unnecessary work and surely give you and us a good deal of inspiration. + +* Relevant coding style guidelines are available in the respective sub-repositories as they are programming language-dependent. + +## Steps to Contribute + +Should you wish to work on an issue, please claim it first by commenting on the GitHub issue that you want to work on. This is to prevent duplicated efforts from other contributors on the same issue. + +If you have questions about one of the issues, please comment on them, and one of the maintainers will clarify. + +We kindly ask you to follow the [Pull Request Checklist](#Pull-Request-Checklist) to ensure reviews can happen accordingly. + +## Contributing Code + +You are welcome to contribute code in order to fix a bug or to implement a new feature. + +The following rule governs code contributions: + +* Contributions must be licensed under the [Apache 2.0 License](./LICENSE) +* Newly created files must be opened by an instantiated version of the file 'templates/file-header.txt' +* At least if you add a new file to the repository, add your name into the contributor section of the file NOTICE (please respect the preset entry structure) + +## Contributing Documentation + +You are welcome to contribute documentation to the project. + +The following rule governs documentation contributions: + +* Contributions must be licensed under the same license as code, the [Apache 2.0 License](./LICENSE) + +## Pull Request Checklist + +* Branch from the main branch and, if needed, rebase to the current main branch before submitting your pull request. If it doesn't merge cleanly with main you may be asked to rebase your changes. + +* Commits should be as small as possible while ensuring that each commit is correct independently (i.e., each commit should compile and pass tests). + +* Test your changes as thoroughly as possible before you commit them. Preferably, automate your test by unit/integration tests. If tested manually, provide information about the test scope in the PR description (e.g. “Test passed: Upgrade version from 0.42 to 0.42.23.”). + +* Create _Work In Progress [WIP]_ pull requests only if you need clarification or an explicit review before you can continue your work item. + +* If your patch is not getting reviewed or you need a specific person to review it, you can @-reply a reviewer asking for a review in the pull request or a comment, or you can ask for a review by contacting us via [email](mailto:opensource@telekom.de). + +* Post review: + * If a review requires you to change your commit(s), please test the changes again. + * Amend the affected commit(s) and force push onto your branch. + * Set respective comments in your GitHub review to resolved. + * Create a general PR comment to notify the reviewers that your amendments are ready for another round of review. + +## Issues and Planning + +* We use GitHub issues to track bugs and enhancement requests. + +* Please provide as much context as possible when you open an issue. The information you provide must be comprehensive enough to reproduce that issue for the assignee. Therefore, contributors may use but aren't restricted to the issue template provided by the project maintainers. + +* When creating an issue, try using one of our issue templates which already contain some guidelines on which content is expected to process the issue most efficiently. If no template applies, you can of course also create an issue from scratch. + +* Please apply one or more applicable [labels](/../../labels) to your issue so that all community members are able to cluster the issues better. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..e95e0f80 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +FROM adoptopenjdk:11-jre-hotspot + +WORKDIR / + +COPY [ "./target/docker/dgcg.jar", "/dgcg.jar" ] + +ENV JAVA_OPTS="$JAVA_OPTS -Xms256M -Xmx1G" + +EXPOSE 8080 + +ENTRYPOINT [ "sh", "-c", "java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /dgcg.jar" ] diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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 + + http://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. diff --git a/NOTICE b/NOTICE new file mode 100644 index 00000000..6519b830 --- /dev/null +++ b/NOTICE @@ -0,0 +1,11 @@ +Copyright (c) 2021 T-Systems International GmbH and all other contributors. + +This project is licensed under Apache License, Version 2.0; +you may not use them except in compliance with the License. + +Contributors: +------------- + +Daniel Eder [daniel-eder], T-Mobile International Austria GmbH +Andreas Scheibal [ascheibal], T-Systems International GmbH +Felix Dittrich [f11h], T-Systems International GmbH \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 00000000..50e8f098 --- /dev/null +++ b/README.md @@ -0,0 +1,356 @@ +

+ EU Digital COVID Certificate Gateway +

+ +

+ + + + + + + + + + + + + + + +

+ +

+ About • + Development • + Documentation • + Support • + Contribute • + Contributors • + Licensing +

+ +## About + +This repository contains the source code of the EU Digital COVID Certificate Gateway (DGCG). + +DGCG is used to share validation and verification information across all national backend servers. By using DGCG, +backend-to-backend integration is facilitated, and countries can onboard incrementally, while the national backends +retain flexibility and can control data processing of their users. + +## Development + +### Prerequisites + +- OpenJDK 11 (with installed ```keytool``` CLI) +- Maven +- Authenticate to [Github Packages](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-apache-maven-registry) + +#### Authenticating to GitHub Packages + +As some of the required libraries (and/or versions are pinned/available only from GitHub Packages) You need to authenticate +to [GitHub Packages](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-apache-maven-registry) +The following steps need to be followed + +- Create [PAT](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) with scopes: + - `read:packages` for downloading packages + +##### GitHub Maven + +- Copy/Augment `~/.m2/settings.xml` with the contents of `settings.xml` present in this repository + - Replace `${app.packages.username}` with your github username + - Replace `${app.packages.password}` with the generated PAT + +##### GitHub Docker Registry + +- Run `docker login docker.pkg.github.com/eu-digital-green-certificates` before running further docker commands. + - Use your GitHub username as username + - Use the generated PAT as password + +#### Additional Tools for starting Gateway locally + +- OpenSSL (with installed CLI) +- DGC-CLI (https://github.com/eu-digital-green-certificates/dgc-cli) + +### Build + +Whether you cloned or downloaded the 'zipped' sources you will either find the sources in the chosen checkout-directory +or get a zip file with the source code, which you can expand to a folder of your choice. + +In either case open a terminal pointing to the directory you put the sources in. The local build process is described +afterwards depending on the way you choose. + +#### Maven based build for Tomcat WAR-File + +``` +mvn clean install +``` + +#### Maven based build for Docker Image + +``` +mvn clean install -P docker +docker-compose build +``` + +### Start Local + +In order to start the gateway on your local computer you have to follow these steps: + +* Create TrustAnchor +* Create Database +* Start Gateway +* Insert Trusted Parties + +#### Create TrustAnchor + +The TrustAnchor is used to sign TrustedParty entries in the DB. To validate these signatures the gateway needs to public +key of the TrustAnchor. + +To create a TrustAnchor you can execute the following OpenSSL command: + +``` +openssl req -x509 -newkey rsa:4096 -keyout key_ta.pem -out cert_ta.pem -days 365 -nodes +``` + +afterwards the PublicKey has to be exported in a Java KeyStore. + +``` +keytool -importcert -alias dgcg_trust_anchor -file cert_ta.pem -keystore ta.jks -storepass dgcg-p4ssw0rd +``` + +Put the created ta.jks file in the "certs" directory of dgc-gateway. If you are using the Docker image then this folder must +be in the root directory of your local workspace (on the same level as this readme file). Create directory it does not already exist. + +#### Create Database + +DGC Gateway needs a database to persist data. For local deployment a MySQL is recommended. A MySQL DB will be started +when docker-compose file is started, so no additional tasks are required. + +#### Start Gateway + +To start the Gateway just start the docker-compose file. Please assure that the project was build for Docker build +before. + +``` +docker-compose up --build +``` + +#### Common issues + +`ERROR: for dgc-gateway_dgc-gateway_1 Cannot create container for service dgc-gateway` + +This error occurs in Docker-for-Windows if Docker does not have access to the gateway folder. In Docker-for-Windows, +go to `Settings > Resources > File Sharing` and add the root directory of the repository, then restart Docker-for-Windows. + +#### Insert Trusted Parties + +The data structure in the database should be now be created by DGC Gateway. In order to access the DGC Gateway it is +required to onboard some certificates. You will need AUTHENTICATION, UPLOAD and CSCA certificates. + +The certificates can be created with OpenSSL: + +``` +openssl req -x509 -newkey rsa:4096 -keyout key_auth.pem -out cert_auth.pem -days 365 -nodes +openssl req -x509 -newkey rsa:4096 -keyout key_csca.pem -out cert_csca.pem -days 365 -nodes +openssl req -x509 -newkey rsa:4096 -keyout key_upload.pem -out cert_upload.pem -days 365 -nodes +``` + +To sign them with TrustAnchor you can use DGC-CLI: + +``` +dgc ta sign -c cert_ta.pem -k key_ta.pem -i cert_auth.pem +dgc ta sign -c cert_ta.pem -k key_ta.pem -i cert_csca.pem +dgc ta sign -c cert_ta.pem -k key_ta.pem -i cert_upload.pem +``` + +Afterwards you can create a new entry in the `trusted_parties` table and fill all of the fields with the data produced by the above commands. + +##### Inserting Trusted Parties into the Database + +Log on to the mysql container (using the docker commands or opening a shell with the docker UI) and open mysql cli like this: + +``` +mysql --user=root --password=admin dgc +``` + +To show the available tables: + +``` + select * from INFORMATION_SCHEMA.tables where table_schema='dgc'; +``` + +We're interested in the table `trusted_party`; you can see the structure of it by using this command: + +``` +describe trusted_party; +``` + +To insert your certificates you can do this (replacing this with your own information from the `dgc` command): + +``` +INSERT INTO trusted_party (created_at, country, thumbprint, raw_data, signature, certificate_type) +SELECT + NOW() as created_at, + 'NL' as country, + '{Certificate_Thumbprint}' as thumbprint, + '{Certificate_Raw_Data}' as raw_data, + '{TrustAnchor_Signature}' as signature, + '{AUTHENTICATION|UPLOAD|CSCA}' as certificate_type; +``` + +Here is a set of example queries including all of the data: + +``` +-- Authentication certificate +insert into trusted_party (created_at, country, thumbprint, raw_data, signature, certificate_type) +select + now() as created_at, + 'NL' as country, + '397da9eb17467a2b3b83704ab6490a540bef43e84f06a6bd885e6621572da401' as thumbprint, + 'MIIFrzCCA5egAwIBAgIUPg0bGwARBnhfTWmOTpOOdYMKESMwDQYJKoZIhvcNAQELBQAwZzELMAkGA1UEBhMCTkwxDDAKBgNVBAgMA1RMUzEMMAoGA1UEBwwDVExTMQwwCgYDVQQKDANUTFMxDDAKBgNVBAsMA1RMUzEMMAoGA1UEAwwDVExTMRIwEAYJKoZIhvcNAQkBFgNUTFMwHhcNMjEwNTA1MTAxOTMwWhcNMjIwNTA1MTAxOTMwWjBnMQswCQYDVQQGEwJOTDEMMAoGA1UECAwDVExTMQwwCgYDVQQHDANUTFMxDDAKBgNVBAoMA1RMUzEMMAoGA1UECwwDVExTMQwwCgYDVQQDDANUTFMxEjAQBgkqhkiG9w0BCQEWA1RMUzCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALiYJTlNwVftS7k794t/Zog54HQGTPrjreDa4c4eQT3fzjjFF7QnyLn1yERggBX3v0pVP8skTxTMbYc9uLFNYNcpMT6H6eNQDKLmyGIoh8Lq4HQ6vLUGse1IjOreJNtCyFB5z4hFeY/QmJykBza9HE+Pfw9O/otOqO2Jpupk1r+dxlL0+kugRFB+vepmeNMocbFT6mPzQdzToNdMMvuNKNxP/2NeDTzpxVdDQTHvqCK6bQuVcBj6NkLMTkdx2h0iPy7Xwoq8t5Wui/AF4c8lkdIu9/OlLMSCGTX6LaB9zxXEQVCZKml6TZ9snNe9T6OTEuFAGjKr+rpgSL3zNxfo0FurO/Rs+H1w7424yKGPL4WOBtXR9EHZz1/l8YR9tXCGlqarsFjzmZIsUvOFdRFCVAxzYsWRUdWn5wZ9YpG5wbUjzImnLm0nCBdyrnEHBhWHPXS6uXHueEuKJb5gg0Y6+owD9tMYZ7y9tgH6JaYHMYbHKDoOa0cpbUQVjhGA2ce7axIQMo/mSvd5CxXapH5N0Zope7yDjUiyNdRsHJj24r/LpxXBm7eMtIWAlsaXBL1OYF3TJXnVFXWacxaZqKam1orJrmWwyWmd2qwq/ycvs2cDjSipMNKC8WYPI11jnjLC1YcaBEVPRr2BG2mDMFZu7HGxGPIRyWC/IJn7D1LEzywVAgMBAAGjUzBRMB0GA1UdDgQWBBQw3Wsw2XD3R0MgcPpnXSvdnAJedDAfBgNVHSMEGDAWgBQw3Wsw2XD3R0MgcPpnXSvdnAJedDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQCNfqF1DmFLOCOtNF/I4ZMbS4gZymEe6v3dUw9Z4eGcQWGgz19bmpeh8l21PNU+c+KgemQAoa7XMnrfySpjnVDqJ7+k6Dba4KY8ImwIDkY/RYXQxgqXGudxMiPUul0CGpjzvo385Z5VppNWu3ZgRcTcAUCe+42mWpfmvQGEJailRSoIY5K7GceaP62dRgkAmVnq8tP/EtLvnrAlAo1xk00sVLJUnxpBl/J5yOua1qM10ROo/6Md3IB820L7jOUki0rLmH2FzbdEu15PPwnxtHyjgDIr+JbY1BYnCihHI8635HBS1pv+hAj0M0cBY+ChuD5V5yfCCM+QxZM4q4HNd9Fb3hIyfod5KHKTzFxYkpG54KKYUgyPHdEMuj3RFzcIYcYlAdyfc1Q0Ms0YyqeV6Xu0HjibeV16wfZ/+0SLK6WkzMOutLL7L73xVAo1AnIkXUXjQnDOjGusttH7RbMC6BdiC/SevQQuCsFO1b0dx1OQxehNe0wiaFj63ZPXjUFz5QhCPqhZJKjEXmK55RLBUpkYOGNdcS96t+8vI+HucZAqR+2Vu00K6od3cAqjTPV37PxQQY47BnNqIjOzWqvykZNLovQ28iccZWn3R1OkWXbN44+ehGoB2ELFrdu5B3GoWXEP0RSe7+Jo2unoq77rIq4qVoKsyG7+6YDjDy6qV5HdJQ==' as raw_data, + 'MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgEFADCABgkqhkiG9w0BBwEAAKCAMIIFazCCA1OgAwIBAgIUaBQf8hLCSFET3Uik+TXvvStwuqUwDQYJKoZIhvcNAQELBQAwRTELMAkGA1UEBhMCTkwxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMTA1MDUwODU1NDZaFw0yMjA1MDUwODU1NDZaMEUxCzAJBgNVBAYTAk5MMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCzENLq/3uFSodOB5xExWzgxU/6ypHfePqOpxifg/ZVcg9Z69EdCRE0oNXJyBR8GzlDQW5X0SYFb+EvACxZlK5SDiGtXIQ195QWeqh2p7m6c463cYe7L3AYNZ/2BqEe/0RMXsYvObDGpgMNIriUVSfL+wBlNqFY/CkVm0/dBs0EWq3gss8pQViRfA7O2YgjuoocxVjeTtwhQdUFq+vO7tWzcGueCapOzb19rwz6nHnIO0Zy9kg7SVEUD1nte9eIbwm4wq+h7r7ifYLmzMguTk5L54eIlcnjzD+2vxoRq8B/sUOMnHOdpAoWyJLz4auPr10zNd8Sk2PmStCjnFBCo9O5xrcbKeFvnx/K8YkkQlhrUgrYWgzgxuPtSyFx6rkB02CkjvRD2PyoXr4qXoW1aH/u/+k8vlXLnKkhyahdg8XF7kHb3P55NklvPj1hoUkK9HpyhxQoFud5wggShXKF84kk24EqeJeE0gZ6UWxcwjlBQzwhJZWaU8OYAi2AWJWrhRIwU9aamTEX+YLjQeRPE3YYV6yQ1thFACC1yn4LjwV8dOt9Js3HALf3GxGRjusKclntEj5MxNyc6Ehokf+TPJePLp8SafyG4NC+u76FwVJ/W7IoNbVqZUesD6AcCO6hMnLITdfnF9t9obGd6/K6MQoE37e8cU9tp/j8Ug2YlCvlRwIDAQABo1MwUTAdBgNVHQ4EFgQUyVtRWVxHILbKVhrqE/c06w35oOAwHwYDVR0jBBgwFoAUyVtRWVxHILbKVhrqE/c06w35oOAwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAo67nDOetqsHMS2nf/990eA4pbqTsCUsS9/PoT8FVLf+t3qYypSizVi2XspVas3bs1t217oOPHRVGrzEWiLgS0oHOtEq1eKYW2Q2BMcSsxOlJ+2MnBpo+La5/B9kZPBVlqeGUZH065Xn37BMiVY9sA1608QtZNp+NCxTn8Ir92i4sH4mjVOFjaoi29QlmUrn309TaEcan/SQDAXzgCtnNWXzKIxNhPi1RmKtA/2ns0KM28xAvmedCGeT4Io2ax0XgL/6AA1FPoPC2/rHqy9Kx4WBhbuzm3xtwatBPmP/D8fPfcmmF/mbiiTEt1TFNiaLaA0rPDeOQ9AUZgV5XD2HRXERcMIjMGs/qawfRp7uKOD7Tohlcc5pOe+LTB2VMNCSGqAqNgF03Q1R/8rmpYUgbDp1j+E3DGMmD77EFxZp/iJ/kQ/IvB7rJp7XSDNiTDw69IQXrgXJwCBQ7wdfh2qZ/Qq/2LBjVijVO7PgSS0DqwmTRj5uXdApvQP+kUQRuNB+SM/SgaF7nDG/4BvS85Hi7m1cueySji6+waWQl9An9hjMjoYKdKvCucY6Z56OGUND/ReFG4JdTnCrzLNdGOrhTrXXIjtYVCsaF4vYy8UDl/3bxC+/pfccATS6S9Iyndf1Vc/yj76bbAbxSTHN5ahq6EknEq1Tx7hiRLu4y0oizAHcAADGCAyMwggMfAgEBMF0wRTELMAkGA1UEBhMCTkwxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZAIUaBQf8hLCSFET3Uik+TXvvStwuqUwDQYJYIZIAWUDBAIBBQCggZgwGAYJKoZIhvcNAQkDMQsGCSqGSIb3DQEHATAcBgkqhkiG9w0BCQUxDxcNMjEwNTA1MTEyMDQ4WjAtBgkqhkiG9w0BCTQxIDAeMA0GCWCGSAFlAwQCAQUAoQ0GCSqGSIb3DQEBCwUAMC8GCSqGSIb3DQEJBDEiBCA5fanrF0Z6KzuDcEq2SQpUC+9D6E8Gpr2IXmYhVy2kATANBgkqhkiG9w0BAQsFAASCAgAen192W2aNcD/QSTAa0zlFPYN/vdKnzmt3c9ZdecHQLFHzw0qU/5DNlynW4kuDwFzC3uCVCRv0IkT/3uldvsNh+vpqRX2dy2DQVfC4D5wohnyoz72cqlbkKR2PQ1YESM5A2+0VUKYQ8e/hGkn+qo7cs+3BoXnxy7+2aTlKBY37vzqzpGNQ3pXTcMNDIaXI81pk2pXg4MioXuom0b6X80EIGsO/f+UA7pkQDSGlfiAbuhryYpjCHcQr7RFwDaSlM5isgspfIN03LkCbzoSDCWE/ehZB7eYfohWvfOXM4qhVo04WnQa9aPZIAiDF6xOZdfZuB3UBeCfyNkc1jfrIed+gaGc8nOQFAngIOiLuSBKKYSlgPDcQq3n9H7LUutMYvAaRAUii5TrDbbHUPFzZRo4Q7QsjdCsYLB/R2RV6toldVrdCOj2acYHia3Z+/ajs+A5JoHBH9J8VH6Iph6gw6eZSbydkmQyESGATY4Wf8RAoQ8E8iWhm3Qg1p8EGzX3fT3BHHaJTr5zlJ2yZFu5+xvPUfTCvXFDoe+eJ0O3EGg5v4uSv/r4g8e9jMKnWD/3Azvcm/dEIXl5X9VGLE3om4Pxk4jkZidYFK2rHKPCvTcOUTpSXUuB5aybSTfPr1e985tQKQCRvsXa8nAY6ExqAhpwBH19IHKg79M+lR6kTF2gnlQAAAAAAAA==' as signature, + 'AUTHENTICATION' as certificate_type; + +-- Upload certificate +insert into trusted_party (created_at, country, thumbprint, raw_data, signature, certificate_type) +select + now() as created_at, + 'NL' as country, + '7a58cc85a1bcfecb1bc69822cc2a72dfb4fbc9fe23d588fa9b0660b929d368f9' as thumbprint, + 'MIIF0zCCA7ugAwIBAgIUZiCTld5e+Bhk5ott4lejMfphwAMwDQYJKoZIhvcNAQELBQAweTELMAkGA1UEBhMCTkwxDzANBgNVBAgMBlVQTE9BRDEPMA0GA1UEBwwGVVBMT0FEMQ8wDQYDVQQKDAZVUExPQUQxDzANBgNVBAsMBlVQTE9BRDEPMA0GA1UEAwwGVVBMT0FEMRUwEwYJKoZIhvcNAQkBFgZVUExPQUQwHhcNMjEwNTA1MTAyNTI1WhcNMjIwNTA1MTAyNTI1WjB5MQswCQYDVQQGEwJOTDEPMA0GA1UECAwGVVBMT0FEMQ8wDQYDVQQHDAZVUExPQUQxDzANBgNVBAoMBlVQTE9BRDEPMA0GA1UECwwGVVBMT0FEMQ8wDQYDVQQDDAZVUExPQUQxFTATBgkqhkiG9w0BCQEWBlVQTE9BRDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMCrQqfrlprHDAGsaa7eVfBLxBmJ94+N+rNZ51Jlq3zkYV0nNzPpzac3TgNu7Vz6LeWdKBf07ozEQRNM6ojFBT+1Af+3jbT43sqs+qnRJLaD0vO0U0JPtBjk1OGkRaSJFwRId6XQWm0qjeE9Z5F8XjmA+2pSKDja37G7u4zOxnQ7qC1tI3Vs4rahonOk7npXi02o//v1VVaBlKiF4HTZhFaAIGWtoz9SDtLxPJiTGvwx/5NTJlWia1Y0t0Br+kCfuLsAnM20HnwY3CO2RPhkSC2eEDSZ6jFYaah1ggfmSanlHTlwkGzyx6P7aNlcOYCiqERYG61yjVHC5Rd8+aQeGcmF1kXF91Fz0w+LWMZ1FaRQ32bHYGv1M62BZrH58cor9eVc98iKGmlKh8VJ6Qr1bNlijD5BONfFQeKgwFGIdMJrZtbYFMDra+7RmIA+SMf3SaQsYzngBDHiQSjyTHjO3dg7PM5ZDYI79onM9SF3W3Ogj8CM+SgE67kxbvMS92zLbPB7UwJjd4j4JMDM8Z4yf9Kq/cE0mcuZVUs+9ow8LUmPGvQmRZvIpAg3m+XRnMOziUhukx3vI6NzbiqTd4rIR5RBIlgNnTxCwlb5L+6Td/C26HjpKzTTkico2vd8ux61KeG9M7nlsOU+T+w4Ff2Tcpc8eEJZkV0/hIjTIVj1hvmjAgMBAAGjUzBRMB0GA1UdDgQWBBQ9eygTuQWztj0o0b4OCqQNtqPoqzAfBgNVHSMEGDAWgBQ9eygTuQWztj0o0b4OCqQNtqPoqzAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQCZ/48PbHIFUDbNdqGvcw/UgT2ZlF7GhrWB667iP2XEi8m58eYvYLASV0ujhfVEhS3/Sr41fW2hApv47xU2uqkvJSMZ0bNePQv5kakVUaF/a3CtPXoYo29vXBCX1DebNoSjHMBjZRe4f6TZEY7sD9Za9Nvcvpy6Q6ly1tSyqYU/0V2DwmvDKndaF2ejNBwuc9o/FcYWPi3bGjPexbYhqjqp8ZrMbITkKibP6CXFFikAx0xVT8cHU2yBhAjclnVJMfYnzECYmO3Cpuf7r5HK204nWBkG5mnoVb6D7qtjiLImDJGMTGi2RY9AlyD968QNbh7/PcCWptVZdUOrAOOd/yYo+YucVZNcxgSNfkVjE7YCDYQr4Lf0dV47MNPXe/QOFB6fKmueQtRMl2Hn1ht+cojoG3i+qiSEwJKh2hSNGimZBT6AEd93/XA3hmsWA7UcX/YV5HZdPpc9T38vE08f4bB/JBq2yJ58LOpDpUMaVA6wkzmwXDRHBpKeMEDz2JgDZJN+Ud3mo16z7mFEgIqYNGVJvvfeRTrLOoyy+39Ge9amzeArcEqjbQ75qb/cHwJAyElKPQNh6Iet8g5o33yxDsP5LTju3s6ssU4F/CXlZ35QectNvLTx0ewfjxnDFCiAs0zy0I4jf8tb/Obx+awXrRblgGGcHjIcFUk5E3gVY5CDjg==' as raw_data, + 'MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgEFADCABgkqhkiG9w0BBwEAAKCAMIIFazCCA1OgAwIBAgIUaBQf8hLCSFET3Uik+TXvvStwuqUwDQYJKoZIhvcNAQELBQAwRTELMAkGA1UEBhMCTkwxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMTA1MDUwODU1NDZaFw0yMjA1MDUwODU1NDZaMEUxCzAJBgNVBAYTAk5MMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCzENLq/3uFSodOB5xExWzgxU/6ypHfePqOpxifg/ZVcg9Z69EdCRE0oNXJyBR8GzlDQW5X0SYFb+EvACxZlK5SDiGtXIQ195QWeqh2p7m6c463cYe7L3AYNZ/2BqEe/0RMXsYvObDGpgMNIriUVSfL+wBlNqFY/CkVm0/dBs0EWq3gss8pQViRfA7O2YgjuoocxVjeTtwhQdUFq+vO7tWzcGueCapOzb19rwz6nHnIO0Zy9kg7SVEUD1nte9eIbwm4wq+h7r7ifYLmzMguTk5L54eIlcnjzD+2vxoRq8B/sUOMnHOdpAoWyJLz4auPr10zNd8Sk2PmStCjnFBCo9O5xrcbKeFvnx/K8YkkQlhrUgrYWgzgxuPtSyFx6rkB02CkjvRD2PyoXr4qXoW1aH/u/+k8vlXLnKkhyahdg8XF7kHb3P55NklvPj1hoUkK9HpyhxQoFud5wggShXKF84kk24EqeJeE0gZ6UWxcwjlBQzwhJZWaU8OYAi2AWJWrhRIwU9aamTEX+YLjQeRPE3YYV6yQ1thFACC1yn4LjwV8dOt9Js3HALf3GxGRjusKclntEj5MxNyc6Ehokf+TPJePLp8SafyG4NC+u76FwVJ/W7IoNbVqZUesD6AcCO6hMnLITdfnF9t9obGd6/K6MQoE37e8cU9tp/j8Ug2YlCvlRwIDAQABo1MwUTAdBgNVHQ4EFgQUyVtRWVxHILbKVhrqE/c06w35oOAwHwYDVR0jBBgwFoAUyVtRWVxHILbKVhrqE/c06w35oOAwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAo67nDOetqsHMS2nf/990eA4pbqTsCUsS9/PoT8FVLf+t3qYypSizVi2XspVas3bs1t217oOPHRVGrzEWiLgS0oHOtEq1eKYW2Q2BMcSsxOlJ+2MnBpo+La5/B9kZPBVlqeGUZH065Xn37BMiVY9sA1608QtZNp+NCxTn8Ir92i4sH4mjVOFjaoi29QlmUrn309TaEcan/SQDAXzgCtnNWXzKIxNhPi1RmKtA/2ns0KM28xAvmedCGeT4Io2ax0XgL/6AA1FPoPC2/rHqy9Kx4WBhbuzm3xtwatBPmP/D8fPfcmmF/mbiiTEt1TFNiaLaA0rPDeOQ9AUZgV5XD2HRXERcMIjMGs/qawfRp7uKOD7Tohlcc5pOe+LTB2VMNCSGqAqNgF03Q1R/8rmpYUgbDp1j+E3DGMmD77EFxZp/iJ/kQ/IvB7rJp7XSDNiTDw69IQXrgXJwCBQ7wdfh2qZ/Qq/2LBjVijVO7PgSS0DqwmTRj5uXdApvQP+kUQRuNB+SM/SgaF7nDG/4BvS85Hi7m1cueySji6+waWQl9An9hjMjoYKdKvCucY6Z56OGUND/ReFG4JdTnCrzLNdGOrhTrXXIjtYVCsaF4vYy8UDl/3bxC+/pfccATS6S9Iyndf1Vc/yj76bbAbxSTHN5ahq6EknEq1Tx7hiRLu4y0oizAHcAADGCAyMwggMfAgEBMF0wRTELMAkGA1UEBhMCTkwxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZAIUaBQf8hLCSFET3Uik+TXvvStwuqUwDQYJYIZIAWUDBAIBBQCggZgwGAYJKoZIhvcNAQkDMQsGCSqGSIb3DQEHATAcBgkqhkiG9w0BCQUxDxcNMjEwNTA1MTI1MzM2WjAtBgkqhkiG9w0BCTQxIDAeMA0GCWCGSAFlAwQCAQUAoQ0GCSqGSIb3DQEBCwUAMC8GCSqGSIb3DQEJBDEiBCB6WMyFobz+yxvGmCLMKnLftPvJ/iPViPqbBmC5KdNo+TANBgkqhkiG9w0BAQsFAASCAgAD3PIekxk9r3Mnp6C1JV0p5mrRsZTjGyJksN82KketMSQYi3CFelpsUnBbkvI9IRl2NMyh3gvh0S5+CuehwXa/OSXQ2Trq/NSEzK3XZ1sVsOwmPvn1uUVnEw7heS8mK7/vsO2AwQlRuOjOgNSoGByPCceVrChSphy1kP7ZGbpDocTBRHxiVA9wuPQLu+0ffpXC7VX7YqtjdhrqL1+X0dcmqnGK5sX2++7vDF467lqStUkRBqDtqi+KoGC45n9z25ouKAnKAmV1nMuVf9cvwm3U8pIaHI8IMCXAAoGBTSfd0SWutS1aVegp7REigco91YUNjwq3YYLuwdrNvyZ6cR1Mkauy0+DvzghlSLTxWePJWAuVgWwELVqh4SFdi45vH9MckbG2dOd8JipCKotBogYjxsFdTRGHTfzS+OO9RCm8ZnEDrhD6K5ZlBqvQWD5aTGDJ2Uyys5UwPLRXYxO6RTgJ++hK64dAu+QnKxOckCH4yBjamnT4bFYf48GuWBZmNPWMgpFFJm5Bum56auUMdANoV6yO5NYiUenyo92DJzW5w21qYVV++YiXxJHnVkGfPcCqlPFGPd/b1wZuYjBg4ActRdEfJ7wunmEqyqLvNQX8fBVNiqs1itFreZihe0thGFm4ILIyapuABGrjsnT/FAbHVSbkdunb0UKc2YYFPkym8AAAAAAAAA==' as signature, + 'UPLOAD' as certificate_type; + +-- CSCA certificate +insert into trusted_party (created_at, country, thumbprint, raw_data, signature, certificate_type) +select + now() as created_at, + 'NL' as country, + 'a5d441bfa7fdbc2b64b73fb1d78e801bc131d670f6e97218a1625098b3ced707' as thumbprint, + 'MIIFuzCCA6OgAwIBAgIURm8BBiv9BHHG479oKAOOg0kGOvgwDQYJKoZIhvcNAQELBQAwbTELMAkGA1UEBhMCTkwxDTALBgNVBAgMBENTQ0ExDTALBgNVBAcMBENTQ0ExDTALBgNVBAoMBENTQ0ExDTALBgNVBAsMBENTQ0ExDTALBgNVBAMMBENTQ0ExEzARBgkqhkiG9w0BCQEWBENTQ0EwHhcNMjEwNTA1MTAxOTU1WhcNMjIwNTA1MTAxOTU1WjBtMQswCQYDVQQGEwJOTDENMAsGA1UECAwEQ1NDQTENMAsGA1UEBwwEQ1NDQTENMAsGA1UECgwEQ1NDQTENMAsGA1UECwwEQ1NDQTENMAsGA1UEAwwEQ1NDQTETMBEGCSqGSIb3DQEJARYEQ1NDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKXXPXyaDm2GQ9Cs5+MM7jvtOQODwRI4s5kIB2iyG4gOfADEOVe6DYNRm1lvqAdTCrgUw9/quGKPEpfP/4kbit8auU1MY2haicOBYDHcJDAa5UHXCOhEvCF45diehCIy582kNP0fxEQJM7KBe+XsJDyH9joA50V3JKXhih6Nx4iqAq/JyNg29E25FUC3Ml2SPZmE6g/IlidT8+B8NDPVHgGjH5a9+vOjCAUVoIcON+Ez8H/Yop87AfMGhjtSeuJCJ6F4lVfnsSQ4wbAEHKR7YKyyPAm5NAiWQ22FyM4UFS+vNijmWfLcS4uyKfxVk8gBuBOqszZrqmL5VQhFiRwz9MNtj1rUb4ZOFm2laecDXj15oVUTgw7mNLX8MB4jCyjrxUeOQ9XrVdmWQCUm3Sdf9eBwX60J0tkiuPJauaIV115r5CzXxQ3D8y+6B9mDS+7lciIaX/SzFMqI79BwJ1Klc3A8MNt0lIAYhDPSh1BGs7JAGzNbF88Sh1RWF4yJhdfIKl+e5uKXgtlzK6MqbWpr8T0lsV/DccMei9TgXiSwbPQ8DT83WRvDsPyYTWJmfCtCjTE8hWMXiStmpQaYf6fsPMdNW/8l03kpmYwmHL3ToU9e5N6cEyuUGNMjIZB8zMwta4ZdUinG62rvgEK/e6+adE4UG9hfWvs/CcXbYwt7UGK/AgMBAAGjUzBRMB0GA1UdDgQWBBSsY6WtCh6zKpcWm9jLr8pSlwxSfDAfBgNVHSMEGDAWgBSsY6WtCh6zKpcWm9jLr8pSlwxSfDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQBCvDxMoziwQueLaBwDIe+UnXahaoNdw7Rw6wp4fMqccRs6mCRYYX5h2W9ukTX71BcuSyPGUdewHKndvG33odp9Vwm3a/63LiBJqk0+TGfbj5brD8DN5E0+EL3PNxEBGe81Nz2UctAr1rjHuKfCeHR+xzZZTKnDEg1lzs6VV13K9iN4Q9f9HeCvSqHNcbQEweFKZfk2tEJHjjlBwDwYWWfFraVA4FykzYrmZPBtMqrG+UWQJI6B/FjkC3urmotAP3MLjTwhhtIayzkCNpmnkvvbrY/pVnWPEzEptSqdWp3w+jTCEzd0VJeJlH9kOYxi0Mg5KaCONxCQrwI+iKoQnEwjF9cvYo+wVmslYiHMgT+0Ik4jopIiVCinKCeGjt2Ol7eGETfpg1mzjMta4+Abq1N9U36iD3qi/PhSSc6ApCr7ddfLxPLEeDu4Pt/BxsGyPNWPI7gtXLom2+gvbbMRDGpBqDHB/crE4OAAWe0DIrhaOFmSNH+yy8gEkSXfUn1FupVVeOAAOVLpRPQQaC6SxdNXufsO/mzMco5DQUovhrj1HyNM261tpupJgoR0JC8fNIAYAmdy/57ibSn/i48J+PcsaCskzzcYDA18XThImvodGlAWlFa6qhCEhJ/cbrFp75e6pU3CrP6/gf+ssAmbDWfLbNOAI1U4GY1q6+MxhH+bdA==' as raw_data, + 'MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgEFADCABgkqhkiG9w0BBwEAAKCAMIIFazCCA1OgAwIBAgIUaBQf8hLCSFET3Uik+TXvvStwuqUwDQYJKoZIhvcNAQELBQAwRTELMAkGA1UEBhMCTkwxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMTA1MDUwODU1NDZaFw0yMjA1MDUwODU1NDZaMEUxCzAJBgNVBAYTAk5MMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCzENLq/3uFSodOB5xExWzgxU/6ypHfePqOpxifg/ZVcg9Z69EdCRE0oNXJyBR8GzlDQW5X0SYFb+EvACxZlK5SDiGtXIQ195QWeqh2p7m6c463cYe7L3AYNZ/2BqEe/0RMXsYvObDGpgMNIriUVSfL+wBlNqFY/CkVm0/dBs0EWq3gss8pQViRfA7O2YgjuoocxVjeTtwhQdUFq+vO7tWzcGueCapOzb19rwz6nHnIO0Zy9kg7SVEUD1nte9eIbwm4wq+h7r7ifYLmzMguTk5L54eIlcnjzD+2vxoRq8B/sUOMnHOdpAoWyJLz4auPr10zNd8Sk2PmStCjnFBCo9O5xrcbKeFvnx/K8YkkQlhrUgrYWgzgxuPtSyFx6rkB02CkjvRD2PyoXr4qXoW1aH/u/+k8vlXLnKkhyahdg8XF7kHb3P55NklvPj1hoUkK9HpyhxQoFud5wggShXKF84kk24EqeJeE0gZ6UWxcwjlBQzwhJZWaU8OYAi2AWJWrhRIwU9aamTEX+YLjQeRPE3YYV6yQ1thFACC1yn4LjwV8dOt9Js3HALf3GxGRjusKclntEj5MxNyc6Ehokf+TPJePLp8SafyG4NC+u76FwVJ/W7IoNbVqZUesD6AcCO6hMnLITdfnF9t9obGd6/K6MQoE37e8cU9tp/j8Ug2YlCvlRwIDAQABo1MwUTAdBgNVHQ4EFgQUyVtRWVxHILbKVhrqE/c06w35oOAwHwYDVR0jBBgwFoAUyVtRWVxHILbKVhrqE/c06w35oOAwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAo67nDOetqsHMS2nf/990eA4pbqTsCUsS9/PoT8FVLf+t3qYypSizVi2XspVas3bs1t217oOPHRVGrzEWiLgS0oHOtEq1eKYW2Q2BMcSsxOlJ+2MnBpo+La5/B9kZPBVlqeGUZH065Xn37BMiVY9sA1608QtZNp+NCxTn8Ir92i4sH4mjVOFjaoi29QlmUrn309TaEcan/SQDAXzgCtnNWXzKIxNhPi1RmKtA/2ns0KM28xAvmedCGeT4Io2ax0XgL/6AA1FPoPC2/rHqy9Kx4WBhbuzm3xtwatBPmP/D8fPfcmmF/mbiiTEt1TFNiaLaA0rPDeOQ9AUZgV5XD2HRXERcMIjMGs/qawfRp7uKOD7Tohlcc5pOe+LTB2VMNCSGqAqNgF03Q1R/8rmpYUgbDp1j+E3DGMmD77EFxZp/iJ/kQ/IvB7rJp7XSDNiTDw69IQXrgXJwCBQ7wdfh2qZ/Qq/2LBjVijVO7PgSS0DqwmTRj5uXdApvQP+kUQRuNB+SM/SgaF7nDG/4BvS85Hi7m1cueySji6+waWQl9An9hjMjoYKdKvCucY6Z56OGUND/ReFG4JdTnCrzLNdGOrhTrXXIjtYVCsaF4vYy8UDl/3bxC+/pfccATS6S9Iyndf1Vc/yj76bbAbxSTHN5ahq6EknEq1Tx7hiRLu4y0oizAHcAADGCAyMwggMfAgEBMF0wRTELMAkGA1UEBhMCTkwxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZAIUaBQf8hLCSFET3Uik+TXvvStwuqUwDQYJYIZIAWUDBAIBBQCggZgwGAYJKoZIhvcNAQkDMQsGCSqGSIb3DQEHATAcBgkqhkiG9w0BCQUxDxcNMjEwNTA1MTI0MjAzWjAtBgkqhkiG9w0BCTQxIDAeMA0GCWCGSAFlAwQCAQUAoQ0GCSqGSIb3DQEBCwUAMC8GCSqGSIb3DQEJBDEiBCCl1EG/p/28K2S3P7HXjoAbwTHWcPbpchihYlCYs87XBzANBgkqhkiG9w0BAQsFAASCAgBeWXAxkiOgRLVdURZJlY01iPgL0ui5ZuexET+DL2lHKdiVOnMilgNHKv2Dk5kVPRk96j3liEejJVQ0sWIILyXYH8CGOAOJ5s5O5PQr1OlUZhc5GrAtBg9Fl7misSM9qYOQzGMUpwz/D4OqcQroMsTxyHBu54rb6jiCdnRH1TksMFYXR62oZBTVU4B2Uu4b0oPAZhvF8DWLz8JrxHCMYQu6Q+sUmcwhRVk5pn//MZ7Fxev3d5VhCYi6BipC/+2km61rWnCCht9psAOfKsoP5x78mqMzpBzA2MDTh11A2VPQK4GKHcTHUS2VZqcwHOWB9bdxIBHOtY+HN4UjbT6IVHt/sX/GkpcJFHQjouePzpm/FekQlfZKkiiUnmaxMZegovBeOO1qSJsQft20yNjkCRKQLcBg5G9cHyqwgYUAKvufmDMeb4a9dsamMNO39iEChSjgZZ7W0XbxtU89ddhc5WfOH3nKgckuNXLcFDsR/4KxWNR8hRFfAsM5T7M3mbdz19YLBap8t86tSi8DqAxvkqZgFOw/Q3cXKOiAgpcecpxsVynUNkI8GL2/H/BzLGqQGkwlBCOhTsomqKW3HHF/EL92mc/r8Irz293OXvRbA8jNwJHEU6mH1bg1uynlaPT61rB1MEt3i++sk7TjVt889u1AFRJp0f63jEMLIJ51ZUeQngAAAAAAAA==' as signature, + 'CSCA' as certificate_type; +``` + +#### Testing that everything works + +You can test that everything works quickly by using this curl: + +``` +curl -X GET http://localhost:8080/trustList -H "accept: application/json" -H "X-SSL-Client-SHA256: 397da9eb17467a2b3b83704ab6490a540bef43e84f06a6bd885e6621572da401" -H "X-SSL-Client-DN: C=NL" +``` + +* Replace the example SHA with that of your own test certificate in the `X-SSL-Client-SHA256` header +* Replace the example country with your own country in the `X-SSL-Client-DN` header (i.e. US, CN, ZA) + +That command will return something looking like this (but with large base64 strings) + +``` +[ + { + "kid":"OX2p6xdGeis=", + "timestamp":"2021-05-05T12:54:49Z", + "country":"NL", + "certificateType":"AUTHENTICATION", + "thumbprint":"397da9eb17467a2b3b83704ab6490a540bef43e84f06a6bd885e6621572da401", + "signature":"", + "rawData":"" + }, + { + "kid":"eljMhaG8/ss=", + "timestamp":"2021-05-05T12:57:26Z", + "country":"NL", + "certificateType":"UPLOAD", + "thumbprint":"7a58cc85a1bcfecb1bc69822cc2a72dfb4fbc9fe23d588fa9b0660b929d368f9", + "signature":"", + "rawData":"" + }, + { + "kid":"pdRBv6f9vCs=", + "timestamp":"2021-05-05T12:57:36Z", + "country":"NL", + "certificateType":"CSCA", + "thumbprint":"a5d441bfa7fdbc2b64b73fb1d78e801bc131d670f6e97218a1625098b3ced707", + "signature":"", + "rawData":"" + } +] +``` + +NOTE: the url uses mixed cases; it's `trustList` not `trustlist`! + +If something goes wrong, the best place to look is in the logging. + +Docker users can read the logs by copying them to their machine; use `docker ps` to get the ID of the running containers +and `docker cp [CONTAINER_ID]:/logs/dgcg.log .` to copy the log file to the current directory. + +#### Send requests + +DGC Gateway does not do any mTLS termination. To simulate the LoadBalancer on your local deployment you have to send +HTTP requests to the gateway and set two HTTP-Headers: + +X-SSL-Client-SHA256: Containing the SHA-256 Hash of the AUTHENTICATION certificate (thumbprint from dgc ta command +output) +X-SSL-Client-DN: Containing the Distinguish Name (Subject) of the AUTHENTICATION certificate. (Must only contain Country +Property, e.g. C=EU) + +#### Coverting the certificate/private key into PKCS12 + +Windows users may wish to convert their certificate/private keys into a PKCS12 package so that it can be imported into the +machine's certificate store. Thankfully that is pretty simple using openssl. + +For example to convert the test authentication certificate created earlier: + +``` + openssl pkcs12 -export -out auth.pfx -inkey key_auth.pem -in cert_auth.pem +``` + +## Documentation + +### OpenAPI Spec + +The latest OpenAPI specification can always be found here: https://eu-digital-green-certificates.github.io/dgc-gateway/ + +It is also possible to access OpenAPI when DGC Gateway is deployed on your local computer when Spring-Profile "dev" or " +local" is enabled. In order to set authentication headers for authentication without a mTLS terminating LoadBalancer at +least the profile "local" +should be enabled. Then both headers can be set in Swagger UI. + +http://localhost:8090/swagger-ui/index.html + +### Other Documentation + +* [Software Design](docs/software-design-dgc-gateway.md) +* [Onboarding Document](https://github.com/eu-digital-green-certificates/dgc-participating-countries/blob/main/gateway/OnboardingChecklist.md) + +## Support and feedback + +The following channels are available for discussions, feedback, and support requests: + +| Type | Channel | +| ------------------------ | ------------------------------------------------------ | +| **Gateway issues** | | +| **Other requests** | | + +## How to contribute + +Contribution and feedback is encouraged and always welcome. For more information about how to contribute, the project structure, +as well as additional contribution information, see our [Contribution Guidelines](./CONTRIBUTING.md). By participating in this +project, you agree to abide by its [Code of Conduct](./CODE_OF_CONDUCT.md) at all times. + +## Contributors + +Our commitment to open source means that we are enabling -in fact encouraging- all interested parties to contribute and become part of its developer community. + +## Licensing + +Copyright (C) 2021 T-Systems International GmbH and all other contributors + +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](./LICENSE) for the specific +language governing permissions and limitations under the License. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..f494bb23 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,7 @@ +# Reporting Security Vulnerabilities + +This software is built with security and data privacy in mind to ensure your data is safe. We are grateful for security researchers and users reporting a vulnerability to us, first. To ensure that your request is handled in a timely manner and non-disclosure of vulnerabilities can be assured, please follow the below guideline. + +**Please do not report security vulnerabilities directly on GitHub. GitHub Issues can be publicly seen and therefore would result in a direct disclosure.** + +* Please address questions about data privacy, security concepts, and other media requests to the cert@telekom.de mailbox. diff --git a/THIRD-PARTY.md b/THIRD-PARTY.md new file mode 100644 index 00000000..16989ce4 --- /dev/null +++ b/THIRD-PARTY.md @@ -0,0 +1,163 @@ +ThirdPartyNotices +----------------- +This project uses third-party software or other resources that +may be distributed under licenses different from this software. +In the event that we overlooked to list a required notice, please bring this +to our attention by contacting us via this email: +opensource@telekom.de + +ThirdParty Licenses +----------------- + +| Dependency | License | +| --- | --- | +| antlr:antlr:2.7.7 | BSD License | +| ch.qos.logback:logback-classic:1.2.3 | Eclipse Public License - v 1.0 GNU Lesser General Public License | +| ch.qos.logback:logback-core:1.2.3 | Eclipse Public License - v 1.0 GNU Lesser General Public License | +| com.damnhandy:handy-uri-templates:2.1.8 | The Apache Software License, Version 2.0 | +| com.fasterxml:classmate:1.5.1 | Apache License, Version 2.0 | +| com.fasterxml.jackson.core:jackson-annotations:2.12.3 | The Apache Software License, Version 2.0 | +| com.fasterxml.jackson.core:jackson-core:2.12.3 | The Apache Software License, Version 2.0 | +| com.fasterxml.jackson.core:jackson-databind:2.12.3 | The Apache Software License, Version 2.0 | +| com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.12.3 | The Apache Software License, Version 2.0 | +| com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.12.3 | The Apache Software License, Version 2.0 | +| com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.12.3 | The Apache Software License, Version 2.0 | +| com.fasterxml.jackson.module:jackson-module-parameter-names:2.12.3 | The Apache Software License, Version 2.0 | +| com.github.everit-org.json-schema:org.everit.json.schema:1.13.0 | Apache License, Version 2.0 | +| com.github.peteroupc:numbers:1.8.0 | CC0-1.0 | +| com.google.re2j:re2j:1.3 | Go License | +| com.h2database:h2:1.4.200 | MPL 2.0 or EPL 1.0 | +| com.jayway.jsonpath:json-path:2.5.0 | The Apache Software License, Version 2.0 | +| com.sun.activation:jakarta.activation:1.2.2 | EDL 1.0 | +| com.sun.istack:istack-commons-runtime:3.0.12 | Eclipse Distribution License - v 1.0 | +| com.upokecenter:cbor:4.4.4 | CC0-1.0 | +| com.vaadin.external.google:android-json:0.0.20131108.vaadin1 | Apache License 2.0 | +| com.vdurmont:semver4j:3.1.0 | The MIT License | +| com.zaxxer:HikariCP:4.0.3 | The Apache Software License, Version 2.0 | +| commons-codec:commons-codec:1.15 | Apache License, Version 2.0 | +| commons-collections:commons-collections:3.2.2 | Apache License, Version 2.0 | +| commons-digester:commons-digester:1.8.1 | The Apache Software License, Version 2.0 | +| commons-fileupload:commons-fileupload:1.4 | Apache License, Version 2.0 | +| commons-io:commons-io:2.11.0 | Apache License, Version 2.0 | +| commons-logging:commons-logging:1.2 | The Apache Software License, Version 2.0 | +| commons-validator:commons-validator:1.6 | Apache License, Version 2.0 | +| io.github.classgraph:classgraph:4.8.69 | The MIT License (MIT) | +| io.github.openfeign:feign-core:10.12 | The Apache Software License, Version 2.0 | +| io.github.openfeign:feign-httpclient:10.12 | The Apache Software License, Version 2.0 | +| io.github.openfeign:feign-slf4j:10.12 | The Apache Software License, Version 2.0 | +| io.github.openfeign.form:feign-form:3.8.0 | The Apache Software License, Version 2.0 | +| io.github.openfeign.form:feign-form-spring:3.8.0 | The Apache Software License, Version 2.0 | +| io.micrometer:micrometer-core:1.7.1 | The Apache Software License, Version 2.0 | +| io.swagger.core.v3:swagger-annotations:2.1.10 | Apache License 2.0 | +| io.swagger.core.v3:swagger-core:2.1.10 | Apache License 2.0 | +| io.swagger.core.v3:swagger-integration:2.1.10 | Apache License 2.0 | +| io.swagger.core.v3:swagger-models:2.1.10 | Apache License 2.0 | +| jakarta.activation:jakarta.activation-api:1.2.2 | EDL 1.0 | +| jakarta.annotation:jakarta.annotation-api:1.3.5 | EPL 2.0 GPL2 w/ CPE | +| jakarta.persistence:jakarta.persistence-api:2.2.3 | Eclipse Distribution License v. 1.0 Eclipse Public License v. 2.0 | +| jakarta.transaction:jakarta.transaction-api:1.3.3 | EPL 2.0 GPL2 w/ CPE | +| jakarta.validation:jakarta.validation-api:2.0.2 | Apache License 2.0 | +| jakarta.xml.bind:jakarta.xml.bind-api:2.3.3 | Eclipse Distribution License - v 1.0 | +| javax.activation:javax.activation-api:1.2.0 | CDDL/GPLv2+CE | +| javax.xml.bind:jaxb-api:2.3.1 | CDDL 1.1 GPL2 w/ CPE | +| joda-time:joda-time:2.10.2 | Apache 2 | +| mysql:mysql-connector-java:8.0.25 | The GNU General Public License, v2 with FOSS exception | +| net.bytebuddy:byte-buddy:1.10.22 | Apache License, Version 2.0 | +| net.bytebuddy:byte-buddy-agent:1.10.22 | Apache License, Version 2.0 | +| net.javacrumbs.shedlock:shedlock-core:4.25.0 | The Apache Software License, Version 2.0 | +| net.javacrumbs.shedlock:shedlock-provider-jdbc-template:4.25.0 | The Apache Software License, Version 2.0 | +| net.javacrumbs.shedlock:shedlock-spring:4.25.0 | The Apache Software License, Version 2.0 | +| net.minidev:accessors-smart:2.4.7 | The Apache Software License, Version 2.0 | +| net.minidev:json-smart:2.4.7 | The Apache Software License, Version 2.0 | +| org.apache.commons:commons-lang3:3.12.0 | Apache License, Version 2.0 | +| org.apache.httpcomponents:httpclient:4.5.13 | Apache License, Version 2.0 | +| org.apache.httpcomponents:httpcore:4.4.14 | Apache License, Version 2.0 | +| org.apache.logging.log4j:log4j-api:2.14.1 | Apache License, Version 2.0 | +| org.apache.logging.log4j:log4j-to-slf4j:2.14.1 | Apache License, Version 2.0 | +| org.apache.tomcat.embed:tomcat-embed-core:9.0.48 | Apache License, Version 2.0 | +| org.apache.tomcat.embed:tomcat-embed-el:9.0.48 | Apache License, Version 2.0 | +| org.apache.tomcat.embed:tomcat-embed-websocket:9.0.48 | Apache License, Version 2.0 | +| org.apiguardian:apiguardian-api:1.1.0 | The Apache License, Version 2.0 | +| org.aspectj:aspectjweaver:1.9.6 | Eclipse Public License - v 1.0 | +| org.assertj:assertj-core:3.19.0 | Apache License, Version 2.0 | +| org.bouncycastle:bcpkix-jdk15on:1.69 | Bouncy Castle Licence | +| org.bouncycastle:bcprov-jdk15on:1.69 | Bouncy Castle Licence | +| org.bouncycastle:bcutil-jdk15on:1.69 | Bouncy Castle Licence | +| org.dom4j:dom4j:2.1.3 | BSD 3-clause New License | +| org.glassfish.jaxb:jaxb-runtime:2.3.4 | Eclipse Distribution License - v 1.0 | +| org.glassfish.jaxb:txw2:2.3.4 | Eclipse Distribution License - v 1.0 | +| org.hamcrest:hamcrest:2.2 | BSD License 3 | +| org.hdrhistogram:HdrHistogram:2.1.12 | BSD-2-Clause Public Domain, per Creative Commons CC0 | +| org.hibernate:hibernate-core:5.4.32.Final | GNU Library General Public License v2.1 or later | +| org.hibernate.common:hibernate-commons-annotations:5.1.2.Final | GNU Library General Public License v2.1 or later | +| org.hibernate.validator:hibernate-validator:6.2.0.Final | Apache License 2.0 | +| org.javassist:javassist:3.27.0-GA | Apache License 2.0 LGPL 2.1 MPL 1.1 | +| org.jboss:jandex:2.2.3.Final | Apache License, Version 2.0 | +| org.jboss.logging:jboss-logging:3.4.2.Final | Apache License, version 2.0 | +| org.json:json:20201115 | The JSON License | +| org.junit.jupiter:junit-jupiter:5.7.2 | Eclipse Public License v2.0 | +| org.junit.jupiter:junit-jupiter-api:5.7.2 | Eclipse Public License v2.0 | +| org.junit.jupiter:junit-jupiter-engine:5.7.2 | Eclipse Public License v2.0 | +| org.junit.jupiter:junit-jupiter-params:5.7.2 | Eclipse Public License v2.0 | +| org.junit.platform:junit-platform-commons:1.7.2 | Eclipse Public License v2.0 | +| org.junit.platform:junit-platform-engine:1.7.2 | Eclipse Public License v2.0 | +| org.latencyutils:LatencyUtils:2.0.3 | Public Domain, per Creative Commons CC0 | +| org.liquibase:liquibase-core:4.4.2 | Apache License, Version 2.0 | +| org.mapstruct:mapstruct:1.4.2.Final | The Apache Software License, Version 2.0 | +| org.mockito:mockito-core:3.11.2 | The MIT License | +| org.mockito:mockito-junit-jupiter:3.11.2 | The MIT License | +| org.objenesis:objenesis:3.2 | Apache License, Version 2.0 | +| org.opentest4j:opentest4j:1.2.0 | The Apache License, Version 2.0 | +| org.ow2.asm:asm:9.1 | BSD-3-Clause | +| org.projectlombok:lombok:1.18.20 | The MIT License | +| org.skyscreamer:jsonassert:1.5.0 | The Apache Software License, Version 2.0 | +| org.slf4j:jul-to-slf4j:1.7.31 | MIT License | +| org.slf4j:slf4j-api:1.7.31 | MIT License | +| org.springdoc:springdoc-openapi-common:1.5.10 | The Apache License, Version 2.0 | +| org.springdoc:springdoc-openapi-ui:1.5.10 | The Apache License, Version 2.0 | +| org.springdoc:springdoc-openapi-webmvc-core:1.5.10 | The Apache License, Version 2.0 | +| org.springframework:spring-aop:5.3.8 | Apache License, Version 2.0 | +| org.springframework:spring-aspects:5.3.8 | Apache License, Version 2.0 | +| org.springframework:spring-beans:5.3.8 | Apache License, Version 2.0 | +| org.springframework:spring-context:5.3.8 | Apache License, Version 2.0 | +| org.springframework:spring-core:5.3.8 | Apache License, Version 2.0 | +| org.springframework:spring-expression:5.3.8 | Apache License, Version 2.0 | +| org.springframework:spring-jcl:5.3.8 | Apache License, Version 2.0 | +| org.springframework:spring-jdbc:5.3.8 | Apache License, Version 2.0 | +| org.springframework:spring-orm:5.3.8 | Apache License, Version 2.0 | +| org.springframework:spring-test:5.3.8 | Apache License, Version 2.0 | +| org.springframework:spring-tx:5.3.8 | Apache License, Version 2.0 | +| org.springframework:spring-web:5.3.8 | Apache License, Version 2.0 | +| org.springframework:spring-webmvc:5.3.8 | Apache License, Version 2.0 | +| org.springframework.boot:spring-boot:2.5.2 | Apache License, Version 2.0 | +| org.springframework.boot:spring-boot-actuator:2.5.2 | Apache License, Version 2.0 | +| org.springframework.boot:spring-boot-actuator-autoconfigure:2.5.2 | Apache License, Version 2.0 | +| org.springframework.boot:spring-boot-autoconfigure:2.5.2 | Apache License, Version 2.0 | +| org.springframework.boot:spring-boot-starter:2.5.2 | Apache License, Version 2.0 | +| org.springframework.boot:spring-boot-starter-actuator:2.5.2 | Apache License, Version 2.0 | +| org.springframework.boot:spring-boot-starter-aop:2.5.2 | Apache License, Version 2.0 | +| org.springframework.boot:spring-boot-starter-data-jpa:2.5.2 | Apache License, Version 2.0 | +| org.springframework.boot:spring-boot-starter-jdbc:2.5.2 | Apache License, Version 2.0 | +| org.springframework.boot:spring-boot-starter-json:2.5.2 | Apache License, Version 2.0 | +| org.springframework.boot:spring-boot-starter-logging:2.5.2 | Apache License, Version 2.0 | +| org.springframework.boot:spring-boot-starter-test:2.5.2 | Apache License, Version 2.0 | +| org.springframework.boot:spring-boot-starter-tomcat:2.5.2 | Apache License, Version 2.0 | +| org.springframework.boot:spring-boot-starter-validation:2.5.2 | Apache License, Version 2.0 | +| org.springframework.boot:spring-boot-starter-web:2.5.2 | Apache License, Version 2.0 | +| org.springframework.boot:spring-boot-test:2.5.2 | Apache License, Version 2.0 | +| org.springframework.boot:spring-boot-test-autoconfigure:2.5.2 | Apache License, Version 2.0 | +| org.springframework.cloud:spring-cloud-commons:3.0.3 | Apache License, Version 2.0 | +| org.springframework.cloud:spring-cloud-context:3.0.3 | Apache License, Version 2.0 | +| org.springframework.cloud:spring-cloud-openfeign-core:3.0.3 | Apache License, Version 2.0 | +| org.springframework.cloud:spring-cloud-starter:3.0.3 | Apache License, Version 2.0 | +| org.springframework.cloud:spring-cloud-starter-openfeign:3.0.3 | Apache License, Version 2.0 | +| org.springframework.data:spring-data-commons:2.5.2 | Apache License, Version 2.0 | +| org.springframework.data:spring-data-jpa:2.5.2 | Apache License, Version 2.0 | +| org.springframework.security:spring-security-core:5.5.1 | Apache License, Version 2.0 | +| org.springframework.security:spring-security-crypto:5.5.1 | Apache License, Version 2.0 | +| org.springframework.security:spring-security-rsa:1.0.10.RELEASE | Apache 2.0 | +| org.springframework.security:spring-security-web:5.5.1 | Apache License, Version 2.0 | +| org.webjars:swagger-ui:3.51.1 | Apache 2.0 | +| org.webjars:webjars-locator-core:0.46 | MIT | +| org.xmlunit:xmlunit-core:2.8.2 | The Apache Software License, Version 2.0 | +| org.yaml:snakeyaml:1.28 | Apache License, Version 2.0 | diff --git a/codestyle/checkstyle.xml b/codestyle/checkstyle.xml new file mode 100644 index 00000000..ce3b135e --- /dev/null +++ b/codestyle/checkstyle.xml @@ -0,0 +1,318 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..12b2a20d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,40 @@ +version: '3' + +services: + mysql: + image: mysql/mysql-server:5.7 + ports: + - 3306:3306 + environment: + - MYSQL_DATABASE=dgc + - MYSQL_ROOT_PASSWORD=admin + - MYSQL_USER=dgc_adm + - MYSQL_PASSWORD=admin + networks: + persistence: + + dgc-gateway: + build: . + image: eu-digital-green-certificates/dgc-gateway + volumes: + - ./certs:/ec/prod/app/san/dgc + ports: + - 8080:8080 + environment: + - SERVER_PORT=8080 + - SPRING_PROFILES_ACTIVE=mysql,docker + - SPRING_DATASOURCE_URL=jdbc:mysql://mysql:3306/dgc + - SPRING_DATASOURCE_USERNAME=dgc_adm + - SPRING_DATASOURCE_PASSWORD=admin + - DGC_TRUSTANCHOR_KEYSTOREPATH=/ec/prod/app/san/dgc/ta.jks + - DGC_TRUSTANCHOR_KEYSTOREPASS=dgcg-p4ssw0rd + - DGC_TRUSTANCHOR_CERTIFICATEALIAS=dgcg_trust_anchor + depends_on: + - mysql + networks: + backend: + persistence: + +networks: + persistence: + backend: diff --git a/docs/DGCG-Overview.png b/docs/DGCG-Overview.png new file mode 100644 index 00000000..53caf498 Binary files /dev/null and b/docs/DGCG-Overview.png differ diff --git a/docs/DGCG-deployment-template.png b/docs/DGCG-deployment-template.png new file mode 100644 index 00000000..a2adf638 Binary files /dev/null and b/docs/DGCG-deployment-template.png differ diff --git a/docs/Nationalbackends.png b/docs/Nationalbackends.png new file mode 100644 index 00000000..3cb008cd Binary files /dev/null and b/docs/Nationalbackends.png differ diff --git a/docs/Validation-Rules-Additional-Doc.md b/docs/Validation-Rules-Additional-Doc.md new file mode 100644 index 00000000..942f46e1 --- /dev/null +++ b/docs/Validation-Rules-Additional-Doc.md @@ -0,0 +1,65 @@ +### Validation Rules + +The ValidationRules API contains some complex logic. This documents helps to understand it. + +## Validation Rules Download + +The download is executed per Country (2 Digit Country Code) +You will get a Map with Rule Identifier as Key and a List of ValidationRules with this Identifier and different versions +as Value. + +If the last uploaded Validation Rule's ValidFrom property is already in the past, the list will only contain this rule. +If the last uploaded Validation Rule's ValidFrom property is in the future, all versions of this ValidationRule which +will are currently valid or will be valid in future will be in the list. + +**Example**:\ +The Database Contains 5 Rules with Identifier IR-EU-0001 with the following ValidFrom Values: + +1. 21.06.2021 12:00:00 +2. 21.06.2021 14:00:00 +3. 23.06.2021 18:00:00 +4. 24.06.2021 09:00:00 +5. 25.06.2021 10:00:00 + +Current Timestamp is 18.06.2021 22:00:00\ +GET /rules/EU --> List with Rules 1, 2, 3, 4 and 5 + +Current Timestamp is 23.06.2021 22:00:00\ +GET /rules/EU --> List with Rules 3, 4 and 5 + +Current Timestamp is 27.06.2021 22:00:00\ +GET /rules/EU --> List only with Rule 5 + +## Validation Rules Validation + +This documents describes the validation which will be executed when uploading a new Validation Rule to the gateway. + +### Signing + +The JSON file containing the ValidationRule has to be uploaded as a signed CMS. The signed CMS can be created with +the [SignedStringMessageBuilder](https://github.com/eu-digital-green-certificates/dgc-lib/blob/cdd10ea33df19e702828a2e7acc4cd563da1f6ea/src/main/java/eu/europa/ec/dgc/signing/SignedStringMessageBuilder.java) +of [dgc-lib](https://github.com/eu-digital-green-certificates/dgc-lib) +To sign the CMS a valid (onboarded) upload certificate must used. + +### Syntax + +The uploaded JSON file will be checked if it aligns to +the [JSON-Schema for ValidationRules](../src/main/resources/validation-rule.schema.json) + +### Content Checks + +In addition the content of the fields of the ValidationRule will be checked. + +| Field | Concerns To | Validation | Possible Error Message | +| --- | --- | --- | --- | +| Identifier | Acceptance Rules | Identifier must start with GR, VR, TR or RR | 400, Invalid RuleID | +| Identifier | Invalidation Rules | Identifier must start with IR | 400, Invalid RuleID | +| Identifier | All | Country in Identifier must be equal to Country of your authentication certificate | 403, Invalid Country sent | +| Country | All | Must be equal to Country of your authentication certificate | 403, Invalid Country sent | +| Version | Rules with previous version | Version of uploaded Rule must be higher than version of the last uploaded rule | 400, Invalid Version | +| ValidFrom | All | Value of ValidFrom must be before value of ValidTo | 400, Invalid Timestamp(s) | +| ValidFrom | All | Value of ValidFrom must be within 2 weeks from today | 400, Invalid Timestamp(s) | +| ValidFrom | Acceptance Rules | Value of ValidFrom must be at least 48h in future from today | 400, Invalid Timestamp(s) | +| ValidFrom | Invalidation Rules | Value of ValidFrom must be in future from today | 400, Invalid Timestamp(s) | +| ValidFrom | Rules with previous version | Value of ValidFrom must be after or equal to ValidFrom from ValidationRule of previous version | 400, Invalid Timestamp(s) | +| ValidTo | All | Value of ValidTo must be after value of ValidFrom | 400, Invalid Timestamp(s) | diff --git a/docs/api.png b/docs/api.png new file mode 100644 index 00000000..476b52de Binary files /dev/null and b/docs/api.png differ diff --git a/docs/distribution_of_signing.png b/docs/distribution_of_signing.png new file mode 100644 index 00000000..fd48fcb3 Binary files /dev/null and b/docs/distribution_of_signing.png differ diff --git a/docs/software-design-dgc-gateway.md b/docs/software-design-dgc-gateway.md new file mode 100644 index 00000000..fe7bf9b1 --- /dev/null +++ b/docs/software-design-dgc-gateway.md @@ -0,0 +1,500 @@ +# Software Design EU Digital Green Certificates Gateway +by Michael Schulte (m.schulte@t-systems.com) + +## Introduction +This documents describes detailed aspects of the implementation of the +EU-digital-green-certificates Gateway. It is closely related to the document [trust-framework_interoperability_certificates](https://ec.europa.eu/health/sites/health/files/ehealth/docs/trust-framework_interoperability_certificates_en.pdf), +to which describes the overarching framework and structure. The [European Digital Green Certificate Gateway](https://ec.europa.eu/health/sites/health/files/ehealth/docs/digital-green-certificates_v2_en.pdf) defines the gateway structure defines the high level architecture. + +Target audience for this document are software engineers who want to get a better understanding of the insight of the implementation to be able to contribute. + +This document is not finished, feedback is welcome and will change its content. + + +# Overview +## Purpose of the Software System +The Digital Green Certificate Gateway (DGCG) has the purpose to support the EU trust framework. +It provides the operability to securely share validation and verification across the connected national backends. +With the usage of DGCG Each national backend is free to distribute the keys via any preferred technology to support the national verification devices in the best way. +If the Digital Green Certificate is in a correctly formatted 2D code, each verifier device can verify each code from other countries, if the verifier is connected to the backend (online verification) or if it has downloaded and stored the necessary public keys beforehand (offline verification). + +## Core Entities +|Entity| Definition| +| ------------- |:-------------:| +| trusted_party | stores the certificate for the trusted parties| +| signer_information | stores the certificate for the signer | +| audit_event | stores all events happening in the system | + +# Context View +The diagram below shows the api endpoints from the DGC Gateway and the dataflow from and to national backends. +![Data Flow View](DGCG-Overview.png "API Overview") +National Health Authorities acting the certificate management process. + +# Software Design + +## Communication +This is a condesed overview of the comminication of the DGCG +### Triangle of Trust +The triangle of trust is the blueprint for Green Certificate interoperability: +-**Holder**: A Green Certificate (DGC) owner (i.e., a citizen with a vaccination, negative PCR test result, or positive anti-body test result)—note that the Green Certificate can be held digitally within a wallet app or on paper (or both) +-**Issuer**: A national authority +-**Verifier**: An offline/online verifier (e.g., customs officers, police, or hotel staff) +![triangle_of_trust.png](triangle_of_trust.png) +How does the verifier know which issuer is trustworthy? In a personal relationship, one would decide by experience. In this architecture, the DGCG tells the verifier which issuers are trustworthy by providing cryptographically anchored information. +### Distribution of Verification Information +Exactly how each national app communicates with the corresponding national backend -whether via CDN, active push, or otherwise - is left to each country. Important here is the cryptographically secured E2E protection between the member states. +![distribution_of_signing.png](distribution_of_signing.png) +### Communication ways +- Device-to-device communication is built on a standardized 2D code and verifier format defined by the EU Trust Framework. +- A direct backend-to-backend communication is not necessary, because the main purpose of the DGCG solution is to provide verification information. +### Trust +To ensure that just data from trusted parties are accepted. The system contains a trust list which is signed entry by entry air gapped by an official signer. This signer, signs with his private key each request of onboarding and provides this signed information to the DGCG operator which can set this entry on the trust list. This guarantees that no external attacker or another party than the trusted signer can create valid records for the trust list. The public key of the trusted signer is shared out of band to the other parties, to establish an effective trust anchoring. +![trust.png](trust.png) + +## Interfaces +DGCG provides a simple REST API with common upload and download functionality for trusted information. +![api.png](api.png) +The are described further with a OpenAPI doc and in the document [European Digital Green Certificate Gateway](https://ec.europa.eu/health/sites/health/files/ehealth/docs/digital-green-certificates_v2_en.pdf) + +## Database Design + +###Trusted Party Table +| Field | Description | Data Type | +| -------------- | ------------------------------------------------ | ------------------------------------------------ | +| Id | Primary key | Long | +| Timestamp | Timestamp of the Record | Timestamp | +| Country | Country Code | varchar(2) | +| Sha256Fingerprint | SHA256-fingerprint of the certificate | varchar(*) | +| Certificate Type | Type of the certificate (Authentication, Signing, Issuer, Client, CSCA) | varchar(*) | +| RawData | Raw Data of the certificate | binary| +| Signature | Signature of the Trust Anchor | varchar(*) | +The cerificate type is one of the following +- **Authentication** Certificate which the member state is using to authenticate at DGCG (NBTLS) +- **Upload** Certificate which the member state is using to sign the uploaded information’s (NBUS) +- **CSCA** Country Signing Certificate Authority certificate (NBCSCA) + +###Signer Information Table +| Field | Description | Data Type | +| -------------- | ------------------------------------------------ | ------------------------------------------------ | +| Id | Primary key | Long | +| Timestamp | Timestamp of the Record | Timestamp | +| Country | Country Code | varchar(2) | +| Sha256Fingerprint | SHA256-fingerprint of the certificate | varchar(*) | +| Certificate Type | Type of the certificate (Authentication, Signing, Issuer, Client, CSCA) | varchar(*) | +| RawData | Raw Data of the certificate | binary| +| Signature | Signature of the Trust Anchor | varchar(*) | +The cerificate type is one of the following +- **DSC** Certificate which the member state is using to sign documents (NBDSC) +###Audit Event Table +| Field | Description | Data Type | +| -------------- | ------------------------------------------------ | ------------------------------------------------ | +| Id | Primary key | Long | +| Timestamp | Timestamp of the Record | Timestamp | +| Country | Country Code | varchar(2) | +| Uploader Sha256Fingerprint | SHA256-fingerprint of the certificate | varchar(*) | +| Authentication Sha256Fingerprint | SHA256-fingerprint of the certificate | varchar(*) | +| Event | Event which occurs | binary| +| Description | Description of the Event | varchar(*) | +The Rights on the table are restricted to insert only for the application user to restrict manipulation of the audit events. +The following table will contain all Audit Events. It is currently under implementation, so the list will be filled after. + +| Event | Description | +| -------------- | ------------------------------------------------ | +| EventID | Description | + +###Problem Report +This is a List of all Possible Problem Reports that can be returned. + +| Code | Problem | Send Value | Details | +| ----- | --------- | ----------- | --------------------- | +| 0x001 | Validation Error | Not Available | Contains the exception message | +| 0x002 | You cant upload an existing certificate. | Parameters send in the request | Contains the exception message | +| 0x003 | Upload of Signer Certificate failed | Parameters send in the request | Contains the exception message | +| 0x004 | Possible reasons: Wrong Format no CMS, not the correct signing alg missing attributes, invalid signature, certificate not signed by known CA | Parameters send in the request | Contains the exception message | +| 0x005 | The certificate doesn't exists in the database | Parameters send in the request | Contains the exception message | +| 0x006 | Upload of Signer Certificate failed | Parameters send in the request | Contains the exception message | +| 0x007 | Possible reasons: Wrong Format no CMS, not the correct signing alg missing attributes, invalid signature, certificate not signed by known CA | Parameters send in the request | Contains the exception message | +| 0x008 | Internal Server Error | Not Available | Not Available | +| 0x100 | Valueset not found | The requested valueset ID | Not available | +| 0x200 | Invalid JSON | Not Available | Detailed JSON Parse Error Report | +| 0x210 | Invalid Country sent | Not Available | Detailed information which field is invalid | +| 0x220 | Invalid Version | Not Available | Detailed information what is wrong about the provided version | +| 0x230 | Invalid Upload Cert | Upload Cert Subject | Hash of Upload Cert and authenticated Country Code | +| 0x240 | Invalid Timestamp(s) | Not available | Detailed information which timestamp is wrong and what is expected | +| 0x250 | Invalid Rule ID | Not available | Detailed information what is wrong with the used Rule ID | +| 0x260 | CMS Signature is Invalid | Not available | Details about expected CMS | +| 0x270 | Validation Rule does not exist | Requested ValidationRule ID | You can only delete existing rules. | +| 0x299 | Unexpected Error | Not available | Ask Support for help | + +## Monitoring +## Audit Logging +The purpose of the audit logging is to track the usage of the system. +The audit events will be additionally logged into the application log. +### Log File Structure + +The target environment for this service is an Apache Tomcat Server. So all log output will be written to stdout +which is redirected to `catalina.out` log file. So the content of this file needs to be collected by the Ops team. + +### Log Message Structure + +All log messages are following one format. The log format is inspired by the Splunk best practices document ([link](https://dev.splunk.com/enterprise/docs/developapps/addsupport/logging/loggingbestpractices/)) + +Each log message contains key value pairs which will represent the required data. +All of these log messages are consisting of mandatory and additional fields. The mandatory fields are always at the beginning of a log message. + The key value pairs are connected by a "=" and seperated by "," followed by a space. If the value consists of more than one word, the value will be wrapped within double quotes. + Multiple log messages are seperated by a new line. + The following mandatory fields will be sent with each log message: + +| Field | Content | Example Value | +| ---------- | ------------------------------------------------ | -------------------------------------- | +| timestamp | ISO-8601 formatted timestamp (always UTC) | 2020-08-04T16:44:45.999Z | +| level | Log Level | INFO | +| hostname | The hostname of the current node | srv01 | +| pid | Process ID | 44929 | +| traceId | Correlation ID for tracing | d058309145b9f7a3 | +| spanId | Span ID for tracing | d058309145b9f7a3 | +| thread | ID of the thread | main | +| class | The class from which the message is coming from | e.i.f.service.SignerInformationService | +| message | Information about what has happened | Uploaded certificate already exist | +| exception | Stack Trace, if available | org.springframew... | + +Example: +``` +timestamp="2020-08-04 17:19:46.038", level=INFO, pid=44929, traceId=e7d394f3b0431c68, spanId=e7d394f3b0431c68, thread=scheduling-1, class=e.i.f.service.SignerInformationService, message="Uploaded certificate already exist", exception="" +``` + +*exception field will only be written to log file. In console stack traces will be printed directly. + +These key-value-pairs can be followed by additional attributes. The additional attributes are individual for each log message. + +### Log messages + +| Event | Log Level | Log Message | Additional attributes | +| ----- | --------- | ----------- | --------------------- | +| **Authentication** +| Authentication failed, no thumbprint or distinguish name provided | ERROR | No thumbprint or distinguish name | n/a | +| Authentication failed, country property not present in distinguish name | ERROR | Country property is missing | dnString, thumbprint | +| Authentication failed, client has used unkown cert for authentication | ERROR | Unknown client certificate | dnString, thumbprint | +| Authentication failed, normalization of hash failed (load balancer config error) | ERROR | Could not normalize certificate hash. | +| Successful Authentication | INFO | Successful Authentication | dnString, thumbprint | +| **Certificate Integrity Check** +| Certificate integrity check failed: Calculated thumbprint does not match stored thumbprint in database. (data manipulation!) | ERROR | Thumbprint in database does not match thumbprint of stored certificate. | certVerifyThumbprint | +| Certificate integrity check failed: Certificate signature is not issued by TrustAnchor or signature is corrupted (data manipulation!) | ERROR | Verification of certificate signature failed! | certVerifyThumbprint | +| Certificate integrity check failed: Certificate entity does not contain raw certificate or certificate signature. (Onboarding failure) | ERROR | Certificate entity does not contain raw certificate or certificate signature. | certVerifyThumbprint | +| Certificate integrity check failed: Raw certificate data does not contain a valid x509Certificate. (parsing error) | ERROR | Raw certificate data does not contain a valid x509Certificate. | certVerifyThumbprint, exception | +| Certificate integrity check failed: Could not load DGCG-TrustAnchor from KeyStore. (initialization error) | ERROR | Could not load DGCG-TrustAnchor from KeyStore. | certVerifyThumbprint | +| Certificate integrity check failed: Could not use public key to initialize verifier. (initialization error) | ERROR | Could not use public key to initialize verifier. | certVerifyThumbprint | +| Certificate integrity check failed: Signature verifier is not initialized (initialization error) | ERROR | Signature verifier is not initialized | certVerifyThumbprint | +| Certificate integrity check failed: Unknown signing algorithm used by DGCG Trust Anchor. (initialization error) | ERROR | Unknown signing algorithm used by EFGS Trust Anchor. | certVerifyThumbprint | +| Certificate integrity check failed: Parsing of signature results in error. See Parser State for more information. | ERROR | TrustAnchor Verification failed. | parserState | +| Certificate integrity check failed: Parsing of signature results in error. Signature of CMS is not matching contained certificate (data manipulation!) | ERROR | TrustAnchor Verification failed: Signature is not matching signed certificate | certVerifyThumbprint | +| Certificate integrity check failed: Parsing of signature results in error. Certificate is signed but not by TrustAnchor (data manipulation!) | ERROR | TrustAnchor Verification failed: Certificate was not signed by known TrustAnchor | certVerifyThumbprint | +| **Certificate Upload Check** +| Verifier for certificate could not be instantiated. | ERROR | Failed to instantiate JcaContentVerifierProvider from cert | certHash | +| Certificate Issuer Check has failed | ERROR | Could not verify certificate issuance. | exception | +| Check of uploaded certificate has failed when revoking a certificate | ERROR | Verification certificate delete failed | verificationFailureReason, verificationFailureMessage | +| Check of uploaded certificate has failed when uploading a certificate | ERROR | Verification certificate upload failed | verificationFailureReason, verificationFailureReasonMessage | +| Revoking Certificate | INFO | Revoking verification certificate | signerCertSubject, payloadCertSubject | +| Uploading Certificate | INFO | Uploading new verification certificate | signerCertSubject, payloadCertSubject | +| Saving Certificate into DB (All checks passed) | INFO | Saving new SignerInformation Entity | uploadCertThumbprint, cscaCertThumbprint | +| Revoking Certificate from DB (All checks passed) | INFO | Revoking SignerInformation Entity | uploadCertThumbprint | +| **Audit Service** +| Created new AuditEvent (id = event type) | INFO | Created AuditEvent | auditId, country | +| **General** +| Uncaught Exception was thrown in DGCG | ERROR | Uncaught exception | exception | +| **Download Interface** +| Trust List was downloaded by a country | INFO | Downloaded TrustList | downloadedKeys (Number of Keys), downloadedKeysCountry (Downloader Country), downloadedKeysType (optional) | +| **Validation Rule** +| A Member State is trying to upload a new ValidationRule | INFO | Rule Upload Request | n/a | +| Upload of ValidationRule failed | ERROR | Rule Upload Failed | validationRuleUploadError, validationRuleUploadReason | +| A Member State has uploaded a new ValidationRule | INFO | Rule Upload Success | n/a | +| A Member State is trying to delete a ValidationRule | INFO | Rule Delete Request | n/a | +| A Member State has deleted ValidationRules | INFO | Rule Delete Success | validationDeleteAmount, validationDownloadId | +| A Member State is downloading ValidationRules | INFO | Rule Download Request | n/a | +| A Member State has downloaded ValidationRules | INFO |Rule Download Success | validationDownloadAmount, validationDownloadRequester, validationDownloadRequested | + +# Integration into Data Center Infrastructure + +## Load Balancer Integration + +The load balancer terminates TLS, executes the mutual TLS authentication and forwards the http request to a worker node. + +The IP of the load balancer is assigned to registered domain name. + +To allow authentication of the http request the load balancer adds header + attributes containing meta information about the client certificate used to + authenticate the request. + + +## Reverse Proxy +The reverse proxy distributes load over the tomcat instances. +The main purpose for EDGCGS is to provide fail over behavior in case a tomcat instance is not available anymore. + +## Database +The database is implemented as mySQL 5.7 + +## Log Analytics/Monitoring Integration + +## Secret Management +Environment specific secrets are managed as part of the tomcat configuration. JDBC connections are provided as tomcat resources. + +# Security + +In this section, we define the security concept and security requirements for the DGCG Gateway. The meaning of the words "MUST", "MAY", and "SHOULD" is defined in [RFC 2119](https://tools.ietf.org/html/rfc2119). To each requirement, an identifier, in the format "SecReq-{Number}", is assigned. + +## 1. Definitions + +**Client**: It refers to a National Backend (see [DGCG Gateway Architecture Specification](https://ec.europa.eu/health/sites/health/files/ehealth/docs/trust-framework_interoperability_certificates_en.pdf)) that uploads or downloads to/from the DGCG Gateway. In the section "Client Authentication", Client and National Backend are used interchangeably. + +**DGCG Gateway Components** + +* **Load Balancer**: The component that receives the clients' requests (e.g., signerCertificate , trustList or audit) and forwards them to the DGCG Gateway Service after successful execution of the TLS protocol. + +* **Service**: The component that processes the clients' requests (e.g., signerCertificate , trustList or audit) after successful client authentication. + +* **Database**: The component where the information (e.g., thumbprint) of the clients' certificates is stored. + +**Certificates** + +- **Authentication** Certificate which the member state is using to authenticate at DGCG (NBTLS) +- **Upload** Certificate which the member state is using to sign the uploaded information’s (NBUS) +- **CSCA** Country Signing Certificate Authority certificate (NBCSCA) +- **DSC** Certificate which the member state is using to sign documents (NBDSC) + +**Batch Signature**: A [PKCS#7](https://tools.ietf.org/html/rfc5652) object containing, among others, the signature of a diagnosis key batch and the Signing Certificate. + +**Client Authentication**: The process in which a Client is authenticated (using its Authentication Certificate) and authorized to request signerCertificate , trustList or audit. + +**Certificate Thumbprint/Fingerprint**: Hash value of a certificate. We have defined the SHA-256 hash function for calculation of the fingerprint. In this document, certificate hash, certificate fingerprint, and certificate thumbprint are used interchangeably. + +##Client Authentication + +As shown in the figure below, the Ditital Green Certificate Gateway Load Balancer authenticates the Clients (National Databases) via mTLS. Then, the clients' requests are forwarded to the DGCG , which validates the Client Authentication Certificate against a whitelist stored in the database. Once the certificate has been successfully verified, the DGCG passes the requests to the corresponding endpoints (e.g., signerCertificate , trustList or audit). + +**SecReq-001** All the clients' requests (e.g., upload diagnostic key batch) MUST be authenticated. + +###Load Balancer + +**SecReq-002** The Load Balancer MUST perform mutual TLS (mTLS) with the clients (national backends). + +**SecReq-003** The Load Balancer MUST implement TLS termination. + +####Certificate Validation + +**SecReq-004** If the client's certificate is not sent during the TLS handshake protocol, the Load Balancer MUST reject the client's request. + +**SecReq-005** If the client's certificate has expired, the Load Balancer MUST reject the client's request. The expiration is determined by the “notAfter” field (see [RFC 5280](https://tools.ietf.org/html/rfc5280#page-22)) of the certificate. + +**SecReq-006** The Load Balancer MUST maintain a bundle containing the root CA certificates or intermediate CA certificates needed to verify (trust) the clients' authentication certificates. If a national backend uses a self-signed client authentication certificate, this certificate MUST be added to the CA bundle. + +**SecReq-007** The Load Balancer MUST validate the client's certificate chain using its CA bundle (SecReq-006). If validation fails, the Load Balancer MUST reject the client's request. + +**SecReq-008** The Load Balancer MAY maintain a Certificate Revocation List (CRL) (see [RFC 5280](https://tools.ietf.org/html/rfc5280#page-54)). + +**SecReq-009** If SecReq-008 is fulfilled, the Load Balancer MUST reject a request, if the client's certificate is present in the CRL. + +####Request Forwarding + +**SecReq-010** If the client's certificate was successfully validated, the Load Balancer MUST forward the corresponding request to the DGCG Service via HTTP. + +**SecReq-011** When a client's request is forwarded to the DGCG Service (See SecReq-010), the Load Balancer MUST add the following HTTP headers to the request: + +| HTTP Header | Description | +|---------------------|-------------| +| X-SSL-Client-SHA256 | SHA-256 hash value of the DER encoded client's certificate. The so-called certificate fingerprint or thumbprint. (base64 encoded bytes, not base64 encoded hexadecimal string representation) | +| X-SSL-Client-DN | The subject Distinguished Name (DN) of the client's certificate (see [RFC 5280](https://tools.ietf.org/html/rfc5280#page-23) and [RFC 1719](https://tools.ietf.org/html/rfc1779#page-6)). The DN MUST contain the Country (C) attribute. (it is possible to transmit DN string URL encoded) | + +###Ditital Green Certificate Gateway Service + +**SecReq-012** The Ditital Green Certificate Gateway (DGCG) Service MUST authenticate the clients' requests using the information sent in the HTTP requests (see SecReq-011) and the certificate information stored in the DGCG Database. + +**SecReq-013** To authenticate a client, the DGCG Service MUST perform the following steps: + +1. Extract the value of the *X-SSL-Client-SHA256* and *X-SSL-Client-DN* headers from the HTTP request forwarded by the Load Balancer (see SecReq-011). + +2. Extract the Country (C) attribute from the X-SSL-Client-DN value. + +3. Query the DGCG Database using the X-SSL-Client-SHA256 value and the Country (C) attribute. Also, the certificate type (see SecReq-019) MUST be used in the query. In this case, the type is: AUTHENTICATION. + + 1. If the query does not return any record, the DGCG Service MUST reject the client's request. + + 2. If the query returns a record, the DGCG Service MUST check whether the certificate has not been revoked. If the certificate was already revoked, the DGCG Service MUST reject the request. Otherwise continue with step 4. + +4. If the client’s request was authenticated successfully, the DGCG Service MUST forward the request to the corresponding endpoint (e.g., download or upload endpoint). + +####Logging + +**SecReq-014** The DGCG Service MUST log each authentication attempt using the information of the X-SSL-Client-DN header. + +**SecReq-015** The DGCG Service MUST use the log format defined by the Cyber Defense Center (CDC) **TODO:TBD**. + +###Storing Secrets +The service has two secrets which need special handling during storage +- private key of DGCGTLS for outgoing TLS connections (for call back), to allow mTLS authentication +- public key of DGCGTA Trust Anchor + +These keys need to be stored seperate from the database. They are stored in two different Java KeyStore (https://en.wikipedia.org/wiki/Java_KeyStore) and deployed manually to the Tomcat instances. The keystores are protected with a password, the password is set as JVM property. + +### Certificate Verification during OnBoarding + +Note that the onboarding process is *not* part of the DGCG Gateway (software). It is included here to inform the future operators of the EDGCGS and the operators of the member-states of key technical steps. The entire onboarding process will be defined separately as part of the overall e-Health network process. + +**SecReq-023** The Ditital Green Certificate Gateway (DGCG) upload endpoint MUST validate the Signing Certificate, which is sent in the PKCS#7 object (see SecReq-017), based on the requirements specified below. The file format is PKCS#12 (pfx) with a password. The password is communicated by to the DGCG by the Designated Country Technical Contact (DCTC) during a verification call where the DGCG contacts the DCTC to verify the authenticity of the upload and get the password. + +**SecReq-###** The Relative Distinguished Name(RDN) 'C' in the Distinguished Name (DN) must match the country of the the Country. + +**SecReq-###** The RDN 'emailAddress' in the Distinguished Name (DN) must match the 24x7 email address of the Country. + +**SecReq-###** The RNDs CN, O and (optional OU) should be populated with a set of human readable and operationally correct set of values. Such as '/CN=DGCGS Netherlands/OU=National Health Institute/O=Ministry of Public Health/C=NL'. + +**SecReq-###** The PKCS#12 (pfx) Should contain the complete chain, where applicable. + +**SecReq-###** If the Signing Certificate should be valid for at least 3 (more) month. The expiration is determined by the "notAfter" field (see [RFC 5280](https://tools.ietf.org/html/rfc5280#page-22)) of the certificate. + +**SecReq-###** The DGCG upload endpoint MUST verify the signature of the Signing Certificate. If validation failed, the DGCG upload endpoint MUST abort Onboarding.. + +**SecReq-###** In order to ensure maximum interoperability in a short timeline fields such as the Key Usage, Extended Key Usage will be operationally *ignored*. + +**SecReq-###** The X.509 certificate will be of version X.509 v3 (RFC5280). + +**SecReq-###** The key-lengths will meet or exceed the BSI Recommendations(2020) and the ECRYPT-CSA Recommendations(2018) for near term production: 3072 bits (RSA) or 256 bits (EC) and SHA256. + +### Certificate Verification during subsequent use and Upload + +-Digital Green Certificate Gateway (DGCG) upload endpoint MUST validate the Signing Certificate. + +**SecReq-###** If the Signing Certificate has expired, the DGCG upload endpoint MUST reject the upload request. The expiration is determined by the "notAfter" field (see [RFC 5280](https://tools.ietf.org/html/rfc5280#page-22)) of the certificate. + +**SecReq-###** The DGCG upload endpoint MUST verify the signature of the Signing Certificate. If validation failed, the DGCG upload endpoint MUST reject the upload request. + +**SecReq-026** To verify whether a Signing Certificate is whitelisted, the DGCG upload endpoint MUST execute the next steps: + +1. Extract the *Origin* value from the + +2. Extract the *Country (C)* attribute from the X-SSL-Client-DN request header (see SecReq-011). + +3. Compare the *Origin* with the *Country*. + + 1. If the Origin is not equal to Country, the upload endpoint MUST reject the signature, and thus, reject the upload request. Otherwise, continue with step 4. + +4. Extract the signing certificate (DER encoded) from the PKCS#7 object. + +5. Calculate the SHA-256 value of the extracted signing certificate. + +6. Query the DGCG Database using the calculated SHA-256 value and the Country (C) attribute. Also, the certificate type (see SecReq-028) MUST be used in the query. In this case, the type is: SIGNING. + + 1. If the query does not return any record, the upload endpoint MUST reject the signature, and thus, reject the upload request. + + 2. If the query returns a record, the upload endpoint MUST verify that the certificate has not been revoked. If the certificate was already revoked, the upload endpoint MUST reject the signature, and thus, reject the upload request. + + +## Certificate Requirements + +**SecReq-033** All certificates MUST be complied with the X.509 version 3 certificate standard (see [RFC 5280](https://tools.ietf.org/html/rfc5280)). + +**SecReq-034** All certificates MUST contain a Distinguished Name (DN) in the subject field. + +**SecReq-035** The Distinguished Name (DN) MUST have the Country (C) attribute, containing the [country code](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2#Officially_assigned_code_elements) (e.g., NL) of the National Backend. + +-The Signing Certificates, which are used to verify the batch signature, CAN be self-signed. (this subject is likely to change) + +**SecReq-037** The Signing Certificates SHOULD set the Key Usage extension to "digitalSignature" (see [RFC 5280](https://tools.ietf.org/html/rfc5280#section-4.2.1.3)). + +-The Authentication Certificates, which are used to authenticate the National Backends, SHOULD set the Key Extended Usage extension to "clientAuth" (see [RFC 5280](https://tools.ietf.org/html/rfc5280#section-4.2.1.12)). + +###Cryptographic Requirements + +**SecReq-042** The cryptographic operations performed with the National Backends certificates MUST fulfill the following requirements: + +| Signature Algorithm | Minimum Key Length | Hash Algorithm | +|---------------------|--------------------|----------------| +| RSA | 2024 | SHA-256
SHA-384
SHA-512 | +| ECDSA | 250 | SHA-256
SHA-384
SHA-512 | + +The above requirements were defined based on the [BSI recommendations](https://www.bsi.bund.de/SharedDocs/Downloads/DE/BSI/Publikationen/TechnischeRichtlinien/TR02102/BSI-TR-02102.pdf?__blob=publicationFile&v=10) for cryptographic algorithms and key lengths. + + +# Deployment View +The system contains different stages which reflect the different perspectives +to the deployed software. + +Stages: +- DEV +- TEST +- ACC +- PROD + +## Generic Deployment View +This view is a generic view which outlines the structure, but does not contain +any specifics related to a stage. + +![Generic Deployment View](DGCG-deployment-template.png "Generic Deployment View") + + + +## General Software Versions and config + +Tomcat hosting service – tomcat instances that are controlled together. Deployment is performed using Nexus artefact repository. We confirmed in the meantime that the rolling upgrade is possible, but we still need to analyse the requirements against the service capabilities +- Tomcat version: Tomcat 9.0.37 +- JDK version : JDK 11 (OpenJDK) +- Heap Size (MB): 8GB +- Meta Space Size (MB): dynamic/default (currently we are not able to specify this) +- Direct Memory Size (MB): default (currently we are not able to specify this) + +MySQL – Supported version: 5.7 +- Required information to create the instance +- Character Set : utf8|latin1|utf16|utf32|other>: utf8 +- Estimated DB Size: 10 GB +- Required capacity of the VM (GB of memory and number of vCPU) - 4 cores 16 GB RAM +- Number of concurrent users: 1 User for the application with max 28 sessions to store data + + +## Stage DEV - Development +As per beginning of the project a dev environment exists in the OTC allowing quick +and easy access for developer. + +Scaling Level +- single worker node + + +Security Level +- full security + +Test Data +- has a number of countries preloaded + +### Sizing TEST +Proposal +- Worker Nodes 3x [4 x Cores, 16 GB RAM, 10 GB free Storage] +- Database equivalent to 2x [4 Cores and 16 GB RAM] + + +## Stage TEST +Scaling Level +- fully scaled + +Security Level +- full security + +Test Data +- has a number of countries preloaded + +### Sizing TEST +Proposal +- Worker Nodes 3x [4 x Cores, 16 GB RAM, 10 GB free Storage] +- Database equivalent to 2x [4 Cores and 16 GB RAM] + + +## Stage ACC +## Stage PROD +### Sizing PROD + +Proposal +- Worker Nodes 3x [4 x Cores, 16 GB RAM, 10 GB free Storage] +- Database equivalent to 2x [4 Cores and 16 GB RAM] + +# Data Deletion +The data base stores + +# Other Constraints and Conditions +Timezone all times and dates are interpreted as timestamps in UTC (https://en.wikipedia.org/wiki/Coordinated_Universal_Time) diff --git a/docs/triangle_of_trust.png b/docs/triangle_of_trust.png new file mode 100644 index 00000000..c605792a Binary files /dev/null and b/docs/triangle_of_trust.png differ diff --git a/docs/trust.png b/docs/trust.png new file mode 100644 index 00000000..a4c42402 Binary files /dev/null and b/docs/trust.png differ diff --git a/owasp/suppressions.xml b/owasp/suppressions.xml new file mode 100644 index 00000000..8f2dc9f9 --- /dev/null +++ b/owasp/suppressions.xml @@ -0,0 +1,22 @@ + + + + see https://github.com/jeremylong/DependencyCheck/issues/1827> + CVE-2018-1258 + + + see https://github.com/jeremylong/DependencyCheck/issues/2952 + CVE-2011-2732 + CVE-2011-2731 + CVE-2012-5055 + + + see https://tomcat.apache.org/security-9.html#Apache_Tomcat_9.x_vulnerabilities vulnerability is fixed in tomcat 9.0.38 + CVE-2020-13943 + + + see https://nvd.nist.gov/vuln/detail/CVE-2020-10693 vulnerability is fixed in hibernate validator 6.0.20/ 6.1.5 - we are using 6.2.0.FINAL + CVE-2020-10693 + + + diff --git a/pom.xml b/pom.xml new file mode 100644 index 00000000..a0f930d2 --- /dev/null +++ b/pom.xml @@ -0,0 +1,566 @@ + + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 2.5.2 + + + + eu.europa.ec.dgc + dgc-gateway + latest + ${packaging.format} + + dgc-gateway + European Digital Green Certificate Gateway Service project. + + T-Systems International GmbH + + + + + dgc-github + https://maven.pkg.github.com/${github.organization}/* + + + jitpack.io + https://jitpack.io + + + + + war + + 11 + 11 + 11 + + UTF-8 + UTF-8 + + 6.1.6 + 2.5.3 + 5.3.9 + 5.5.1 + 1.18.20 + 4.4.2 + 1.5.10 + 5.7.2 + 1.4.2.Final + 3.11.2 + 1.69 + 3.1.0 + 1.13.0 + 4.25.0 + 2020.0.3 + + 3.3.0 + 3.1.2 + 3.9.0.2155 + 0.8.7 + 1.7.0 + 1.7.2 + 3.0.0-M5 + + EU Digital Green Certificate Gateway Service / dgc-gateway + 2021 + apache_v2 + + eu-digital-green-certificates + dgc-gateway + + eu-digital-green-certificates + ${sonar.organization}_${project.artifactId} + https://sonarcloud.io + + + https://github.com/eu-digital-green-certificates/dgc-gateway + + https://github.com/eu-digital-green-certificates/dgc-gateway/actions?query=workflow%3Aci + + + https://github.com/eu-digital-green-certificates/dgc-gateway/issues + + + https://github.com/eu-digital-green-certificates/dgc-gateway + + + + + docker + + docker + jar + + + + + org.springframework.boot + spring-boot-maven-plugin + + ${project.build.directory}/docker + dgcg + + + + maven-assembly-plugin + + + make-zip-ACC + none + + + make-zip-test + none + + + make-zip-PRD + none + + + + + + + + + + + dgc-github + https://maven.pkg.github.com/${github.organization}/${github.project} + + + + + + + org.springframework.cloud + spring-cloud-dependencies + ${spring.cloud.version} + pom + import + + + + + + + eu.europa.ec.dgc + dgc-lib + 1.1.3 + + + com.vdurmont + semver4j + ${semver4j.version} + + + com.github.everit-org.json-schema + org.everit.json.schema + ${json-schema.version} + + + mysql + mysql-connector-java + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-tomcat + provided + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.cloud + spring-cloud-starter-openfeign + + + io.github.openfeign + feign-httpclient + + + org.springframework.boot + spring-boot-starter-test + test + + + org.liquibase + liquibase-core + + + org.projectlombok + lombok + provided + + + org.springdoc + springdoc-openapi-ui + ${springdoc.version} + + + com.h2database + h2 + runtime + + + org.mapstruct + mapstruct + ${mapstruct.version} + + + org.bouncycastle + bcpkix-jdk15on + ${bcpkix.version} + + + org.mockito + mockito-core + ${mockito.version} + test + + + org.springframework.security + spring-security-web + ${spring.security.version} + + + net.javacrumbs.shedlock + shedlock-provider-jdbc-template + ${shedlock.version} + + + net.javacrumbs.shedlock + shedlock-spring + ${shedlock.version} + + + + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + ${plugin.checkstyle.version} + + + org.sonarsource.scanner.maven + sonar-maven-plugin + ${plugin.sonar.version} + + + org.jacoco + jacoco-maven-plugin + ${plugin.jacoco.version} + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + + org.codehaus.mojo + license-maven-plugin + 2.0.0 + + + org.apache.maven.plugins + maven-war-plugin + 3.3.1 + + + org.apache.maven.plugins + maven-resources-plugin + 3.2.0 + + + org.apache.maven.plugins + maven-assembly-plugin + ${plugin.maven-assembly.version} + + + + + + org.owasp + dependency-check-maven + ${owasp.version} + + ./owasp/suppressions.xml + true + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring.boot.version} + + dev + 5000 + 30 + + + + pre-integration-test + + start + + + + post-integration-test + + stop + + + + + repackage + build-info + + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + + codestyle/checkstyle.xml + target/**/* + UTF-8 + true + true + warning + true + false + + + + validate + validate + + check + + + + + + org.jacoco + jacoco-maven-plugin + + + **/DgcGatewayApplication.java + **/restapi/dto/* + **/restapi/mapper/* + **/repository/* + **/model/* + **/entity/* + **/config/* + **/entity/* + + + + + + prepare-agent + + + + report + + report + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.projectlombok + lombok + ${lombok.version} + + + org.mapstruct + mapstruct-processor + ${mapstruct.version} + + + + + + org.codehaus.mojo + license-maven-plugin + + **/*.java + ${project.organization.name} and all other contributors + ---license-start + ---license-end + --- + false + true + true + . + THIRD-PARTY.md + templates/third-party.ftl + dgc-lib + + + + org.apache.maven.plugins + maven-war-plugin + + ${project.basedir}/src/WEB-INF/web.xml + + + + org.apache.maven.plugins + maven-resources-plugin + + ${project.build.sourceEncoding} + + + + net.ju-n.maven.plugins + checksum-maven-plugin + 1.4 + + + generate-artifact-checksum + install + + files + + + + generate-artifact-checksum-deploy + package + + files + + + + + + + ${project.build.directory} + + dgc* + + + + + SHA-1 + + + + + maven-assembly-plugin + + + make-zip-package + install + + single + + + DGCG001_ACC-${project.version} + false + + src/assembly/assembly.xml + + false + + + + make-zip-ACC + package + + single + + + DGCG001_ACC-${project.version} + false + + src/assembly/assembly.xml + + false + + + + make-zip-PRD + package + + single + + + DGCG001_PRD-${project.version} + false + + src/assembly/assembly.xml + + false + + + + make-zip-test + package + + single + + + DGCG001_TST-${project.version} + false + + src/assembly/assembly.xml + + false + + + + + + org.springdoc + springdoc-openapi-maven-plugin + 1.3 + + http://localhost:8090/api/docs + + + + integration-test + + generate + + + + + + + + diff --git a/settings.xml b/settings.xml new file mode 100644 index 00000000..9552ce4b --- /dev/null +++ b/settings.xml @@ -0,0 +1,12 @@ + + + false + + + dgc-github + ${app.packages.username} + ${app.packages.password} + + + diff --git a/src/WEB-INF/web.xml b/src/WEB-INF/web.xml new file mode 100644 index 00000000..d80081d1 --- /dev/null +++ b/src/WEB-INF/web.xml @@ -0,0 +1,6 @@ + + + \ No newline at end of file diff --git a/src/assembly/assembly.xml b/src/assembly/assembly.xml new file mode 100644 index 00000000..847d80d0 --- /dev/null +++ b/src/assembly/assembly.xml @@ -0,0 +1,46 @@ + + zip + + zip + + + + src/main/resources/db/changelog + classpathFILE + + *.* + + + + src/main/resources/db/changelog + classpathPOST + + *.* + + + + src/main/resources/db/changelog + classpathPRE + + *.* + + + + + + ${project.basedir}/target/${artifactId}-${version}.${packaging} + webapps + dgcg.war + + + ${project.basedir}/target/${artifactId}-${version}.${packaging}.sha1 + webapps + dgcg.war.sha1 + + + false + + diff --git a/src/main/java/eu/europa/ec/dgc/gateway/DgcGatewayApplication.java b/src/main/java/eu/europa/ec/dgc/gateway/DgcGatewayApplication.java new file mode 100644 index 00000000..d36e783c --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/DgcGatewayApplication.java @@ -0,0 +1,46 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway; + +import eu.europa.ec.dgc.gateway.config.DgcConfigProperties; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; +import org.springframework.cloud.openfeign.EnableFeignClients; + +/** + * The Application class. + */ +@SpringBootApplication +@EnableFeignClients +@EnableConfigurationProperties(DgcConfigProperties.class) +public class DgcGatewayApplication extends SpringBootServletInitializer { + + /** + * The main Method. + * + * @param args the args for the main method + */ + public static void main(String[] args) { + SpringApplication.run(DgcGatewayApplication.class, args); + } +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/client/JrcClient.java b/src/main/java/eu/europa/ec/dgc/gateway/client/JrcClient.java new file mode 100644 index 00000000..8e72f104 --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/client/JrcClient.java @@ -0,0 +1,42 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.client; + +import eu.europa.ec.dgc.gateway.model.JrcRatValuesetResponse; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; + +@FeignClient( + name = "jrcClient", + url = "${dgc.jrc.url}", + configuration = JrcClientConfig.class) +public interface JrcClient { + + /** + * This method gets a the RAT values from JRC API. + * + * @return List of RAT values. + */ + @GetMapping(value = "", produces = MediaType.APPLICATION_JSON_VALUE + ) + JrcRatValuesetResponse downloadRatValues(); +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/client/JrcClientConfig.java b/src/main/java/eu/europa/ec/dgc/gateway/client/JrcClientConfig.java new file mode 100644 index 00000000..c65815cc --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/client/JrcClientConfig.java @@ -0,0 +1,94 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.client; + +import eu.europa.ec.dgc.gateway.config.DgcConfigProperties; +import feign.Client; +import feign.httpclient.ApacheHttpClient; +import java.security.NoSuchAlgorithmException; +import javax.net.ssl.SSLContext; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.http.HttpHost; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.CredentialsProvider; +import org.apache.http.conn.ssl.DefaultHostnameVerifier; +import org.apache.http.impl.client.BasicCredentialsProvider; +import org.apache.http.impl.client.HttpClientBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@RequiredArgsConstructor +@Slf4j +public class JrcClientConfig { + + private final DgcConfigProperties config; + + /** + * Configure the client depending on the ssl properties. + * + * @return an Apache Http Client with or without SSL features + */ + @Bean + public Client jrcClient() throws NoSuchAlgorithmException { + HttpClientBuilder httpClientBuilder = HttpClientBuilder.create(); + + httpClientBuilder.setSSLContext(SSLContext.getDefault()); + httpClientBuilder.setSSLHostnameVerifier(new DefaultHostnameVerifier()); + + if (config.getJrc().getProxy().getHost() != null + && config.getJrc().getProxy().getPort() != -1 + && !config.getJrc().getProxy().getHost().isEmpty()) { + log.info("Using Proxy for JRC Connection"); + // Set proxy + httpClientBuilder.setProxy(new HttpHost( + config.getJrc().getProxy().getHost(), + config.getJrc().getProxy().getPort() + )); + + // Set proxy authentication + if (config.getJrc().getProxy().getUsername() != null + && config.getJrc().getProxy().getPassword() != null + && !config.getJrc().getProxy().getUsername().isEmpty() + && !config.getJrc().getProxy().getPassword().isEmpty()) { + + log.info("Using Proxy with Authentication for JRC Connection"); + + CredentialsProvider credentialsProvider = new BasicCredentialsProvider(); + credentialsProvider.setCredentials( + new AuthScope( + config.getJrc().getProxy().getHost(), + config.getJrc().getProxy().getPort()), + new UsernamePasswordCredentials( + config.getJrc().getProxy().getUsername(), + config.getJrc().getProxy().getPassword())); + + httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider); + } + } else { + log.info("Using no proxy for JRC Connection"); + } + + return new ApacheHttpClient(httpClientBuilder.build()); + } +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/config/BouncyCastleConfig.java b/src/main/java/eu/europa/ec/dgc/gateway/config/BouncyCastleConfig.java new file mode 100644 index 00000000..c927668b --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/config/BouncyCastleConfig.java @@ -0,0 +1,36 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.config; + +import java.security.Security; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class BouncyCastleConfig { + + /** + * Adds BouncyCastle as Provider for JavaSecurity. + */ + public BouncyCastleConfig() { + Security.addProvider(new BouncyCastleProvider()); + } +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/config/DgcConfigProperties.java b/src/main/java/eu/europa/ec/dgc/gateway/config/DgcConfigProperties.java new file mode 100644 index 00000000..2239c14e --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/config/DgcConfigProperties.java @@ -0,0 +1,80 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.config; + +import java.util.List; +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@Getter +@Setter +@ConfigurationProperties("dgc") +public class DgcConfigProperties { + + private final CertAuth certAuth = new CertAuth(); + private final TrustAnchor trustAnchor = new TrustAnchor(); + + private String validationRuleSchema; + + private JrcConfig jrc = new JrcConfig(); + + @Getter + @Setter + public static class JrcConfig { + private String url; + private Integer interval = 21_600_000; + private ProxyConfig proxy = new ProxyConfig(); + } + + @Getter + @Setter + public static class ProxyConfig { + + private String host; + private int port = -1; + private String username; + private String password; + } + + @Getter + @Setter + public static class TrustAnchor { + private String keyStorePath; + private String keyStorePass; + private String certificateAlias; + } + + @Getter + @Setter + public static class CertAuth { + + private final HeaderFields headerFields = new HeaderFields(); + private List certWhitelist; + + @Getter + @Setter + public static class HeaderFields { + private String thumbprint; + private String distinguishedName; + } + } +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/config/DgcKeyStore.java b/src/main/java/eu/europa/ec/dgc/gateway/config/DgcKeyStore.java new file mode 100644 index 00000000..bb9e3bae --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/config/DgcKeyStore.java @@ -0,0 +1,96 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.config; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@RequiredArgsConstructor +@Slf4j +public class DgcKeyStore { + + private final DgcConfigProperties dgcConfigProperties; + + /** + * Creates a KeyStore instance with keys for DGC TrustAnchor. + * + * @return KeyStore Instance + * @throws KeyStoreException if no implementation for the specified type found + * @throws IOException if there is an I/O or format problem with the keystore data + * @throws CertificateException if any of the certificates in the keystore could not be loaded + * @throws NoSuchAlgorithmException if the algorithm used to check the integrity of the keystore cannot be found + */ + @Bean + public KeyStore trustAnchorKeyStore() throws KeyStoreException, IOException, + CertificateException, NoSuchAlgorithmException { + KeyStore keyStore = KeyStore.getInstance("JKS"); + + loadKeyStore( + keyStore, + dgcConfigProperties.getTrustAnchor().getKeyStorePath(), + dgcConfigProperties.getTrustAnchor().getKeyStorePass().toCharArray()); + + return keyStore; + } + + private void loadKeyStore(KeyStore keyStore, String path, char[] password) + throws CertificateException, NoSuchAlgorithmException, IOException { + + InputStream fileStream; + + if (path.startsWith("classpath:")) { + String resourcePath = path.substring(10); + fileStream = getClass().getClassLoader().getResourceAsStream(resourcePath); + } else { + File file = new File(path); + fileStream = file.exists() ? getStream(path) : null; + } + + if (fileStream != null && fileStream.available() > 0) { + keyStore.load(fileStream, password); + fileStream.close(); + } else { + keyStore.load(null); + log.info("Could not find Keystore {}", path); + } + + } + + private InputStream getStream(String path) { + try { + return new FileInputStream(path); + } catch (IOException e) { + log.info("Could not find Keystore {}", path); + } + return null; + } +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/config/ErrorHandler.java b/src/main/java/eu/europa/ec/dgc/gateway/config/ErrorHandler.java new file mode 100644 index 00000000..b89f1189 --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/config/ErrorHandler.java @@ -0,0 +1,80 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.config; + +import eu.europa.ec.dgc.gateway.exception.DgcgResponseException; +import eu.europa.ec.dgc.gateway.restapi.dto.ProblemReportDto; +import javax.validation.ConstraintViolationException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.server.ResponseStatusException; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +@ControllerAdvice +@Configuration +@RequiredArgsConstructor +@Slf4j +public class ErrorHandler extends ResponseEntityExceptionHandler { + + /** + * Handles {@link ConstraintViolationException} when a validation failed. + * + * @param e the thrown {@link ConstraintViolationException} + * @return A ResponseEntity with a ErrorMessage inside. + */ + @ExceptionHandler(ConstraintViolationException.class) + public ResponseEntity handleException(ConstraintViolationException e) { + return ResponseEntity + .status(HttpStatus.BAD_REQUEST) + .contentType(MediaType.APPLICATION_JSON) + .body(new ProblemReportDto("0x001", "Validation Error", "", e.getMessage())); + } + + /** + * Global Exception Handler to wrap exceptions into a readable JSON Object. + * + * @param e the thrown exception + * @return ResponseEntity with readable data. + */ + @ExceptionHandler(Exception.class) + public ResponseEntity handleException(Exception e) { + + if (e instanceof DgcgResponseException) { + DgcgResponseException de = (DgcgResponseException) e; + return ResponseEntity + .status(((ResponseStatusException) e).getStatus()) + .contentType(MediaType.APPLICATION_JSON) + .body(new ProblemReportDto(de.getCode(), de.getProblem(), de.getSentValues(), de.getDetails())); + } else { + log.error("Uncaught exception", e); + return ResponseEntity + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .contentType(MediaType.APPLICATION_JSON) + .body(new ProblemReportDto("0x008", "Internal Server Error", "", e.getMessage())); + } + } +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/config/MdcCleanupInterceptor.java b/src/main/java/eu/europa/ec/dgc/gateway/config/MdcCleanupInterceptor.java new file mode 100644 index 00000000..2b5989cd --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/config/MdcCleanupInterceptor.java @@ -0,0 +1,37 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.config; + +import eu.europa.ec.dgc.gateway.utils.DgcMdc; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.springframework.web.servlet.HandlerInterceptor; + +public class MdcCleanupInterceptor implements HandlerInterceptor { + + @Override + public void afterCompletion( + HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { + + // Clean Up MDC after each Request. + DgcMdc.clear(); + } +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/config/OpenApiConfig.java b/src/main/java/eu/europa/ec/dgc/gateway/config/OpenApiConfig.java new file mode 100644 index 00000000..279a5a08 --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/config/OpenApiConfig.java @@ -0,0 +1,106 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.config; + +import eu.europa.ec.dgc.gateway.restapi.dto.ValidationRuleDto; +import io.swagger.v3.core.converter.AnnotatedType; +import io.swagger.v3.core.converter.ModelConverters; +import io.swagger.v3.core.converter.ResolvedSchema; +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.License; +import io.swagger.v3.oas.models.media.ArraySchema; +import io.swagger.v3.oas.models.media.ObjectSchema; +import io.swagger.v3.oas.models.security.SecurityScheme; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.info.BuildProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; + +@Configuration +@RequiredArgsConstructor +public class OpenApiConfig { + + private final Optional buildProperties; + + private final DgcConfigProperties configProperties; + + private final Environment environment; + + public static final String SECURITY_SCHEMA_HASH = "Authentication Certificate Hash"; + public static final String SECURITY_SCHEMA_DISTINGUISH_NAME = "Authentication Certificate Distinguish Name"; + + @Bean + OpenAPI openApiInfo() { + String version; + + if (buildProperties.isPresent()) { + version = buildProperties.get().getVersion(); + } else { + version = "Development Build"; + } + + Components components = new Components(); + + // Add authorization if "local" Profile is enabled. + List activeProfiles = Arrays.asList(environment.getActiveProfiles()); + if (activeProfiles.contains("local")) { + components = new Components() + .addSecuritySchemes(SECURITY_SCHEMA_HASH, new SecurityScheme() + .type(SecurityScheme.Type.APIKEY) + .in(SecurityScheme.In.HEADER) + .name(configProperties.getCertAuth().getHeaderFields().getThumbprint()) + .description("SHA256 Hash of Authentication Certificate (HEX encoded, " + + "e.g. e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855)")) + .addSecuritySchemes(SECURITY_SCHEMA_DISTINGUISH_NAME, new SecurityScheme() + .type(SecurityScheme.Type.APIKEY) + .in(SecurityScheme.In.HEADER) + .name(configProperties.getCertAuth().getHeaderFields().getDistinguishedName()) + .description(SECURITY_SCHEMA_DISTINGUISH_NAME + + "Should contain at least country property. (e.g. C=EU)")); + } + + ResolvedSchema validationRuleSchema = ModelConverters.getInstance().resolveAsResolvedSchema( + new AnnotatedType(ValidationRuleDto.class).resolveAsRef(false)); + + ArraySchema validationRuleArraySchema = new ArraySchema(); + validationRuleArraySchema.setItems(validationRuleSchema.schema); + + components.addSchemas(validationRuleSchema.schema.getName(), validationRuleSchema.schema); + components.addSchemas("ValidationRuleDownloadResponse", + new ObjectSchema().additionalProperties(validationRuleArraySchema)); + + return new OpenAPI() + .info(new Info() + .version(version) + .title("Digital Green Certificate Gateway") + .description("The API defines how to exchange verification information for digital green certificates.") + .license(new License() + .name("Apache 2.0") + .url("http://www.apache.org/licenses/LICENSE-2.0"))) + .components(components); + } +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/config/SchedulerConfig.java b/src/main/java/eu/europa/ec/dgc/gateway/config/SchedulerConfig.java new file mode 100644 index 00000000..4d9a27ce --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/config/SchedulerConfig.java @@ -0,0 +1,33 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.config; + +import net.javacrumbs.shedlock.spring.annotation.EnableSchedulerLock; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.scheduling.annotation.EnableScheduling; + +@Configuration +@Profile("!test") +@EnableScheduling +@EnableSchedulerLock(defaultLockAtMostFor = "PT30S") +public class SchedulerConfig { +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/config/ShedLockConfig.java b/src/main/java/eu/europa/ec/dgc/gateway/config/ShedLockConfig.java new file mode 100644 index 00000000..a5487595 --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/config/ShedLockConfig.java @@ -0,0 +1,53 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.config; + +import static net.javacrumbs.shedlock.provider.jdbctemplate.JdbcTemplateLockProvider.Configuration.builder; + +import javax.sql.DataSource; +import lombok.RequiredArgsConstructor; +import net.javacrumbs.shedlock.core.LockProvider; +import net.javacrumbs.shedlock.provider.jdbctemplate.JdbcTemplateLockProvider; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jdbc.core.JdbcTemplate; + +@Configuration +@RequiredArgsConstructor +public class ShedLockConfig { + + private final DataSource dataSource; + + /** + * Creates a LockProvider for ShedLock. + * + * @return LockProvider + */ + @Bean + public LockProvider lockProvider() { + return new JdbcTemplateLockProvider(builder() + .withTableName("shedlock") + .withJdbcTemplate(new JdbcTemplate(dataSource)) + .usingDbTime() + .build() + ); + } +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/config/ValidationRuleSchemaProvider.java b/src/main/java/eu/europa/ec/dgc/gateway/config/ValidationRuleSchemaProvider.java new file mode 100644 index 00000000..44d05382 --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/config/ValidationRuleSchemaProvider.java @@ -0,0 +1,62 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.config; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import javax.annotation.PostConstruct; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.everit.json.schema.Schema; +import org.everit.json.schema.loader.SchemaLoader; +import org.json.JSONObject; +import org.json.JSONTokener; +import org.springframework.context.annotation.Configuration; +import org.springframework.stereotype.Service; +import org.springframework.util.ResourceUtils; + +@Service +@Configuration +@RequiredArgsConstructor +public class ValidationRuleSchemaProvider { + + private final DgcConfigProperties configProperties; + + @Getter + private Schema validationRuleSchema; + + @PostConstruct + void setup() throws FileNotFoundException, IOException { + InputStream schemaInputStream = ResourceUtils.getURL(configProperties.getValidationRuleSchema()).openStream(); + + try { + validationRuleSchema = SchemaLoader.builder() + .schemaJson(new JSONObject(new JSONTokener(schemaInputStream))) + .draftV7Support() + .build().load().build(); + } finally { + schemaInputStream.close(); + } + } + +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/config/WebMvcConfig.java b/src/main/java/eu/europa/ec/dgc/gateway/config/WebMvcConfig.java new file mode 100644 index 00000000..df8be263 --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/config/WebMvcConfig.java @@ -0,0 +1,34 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebMvcConfig implements WebMvcConfigurer { + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(new MdcCleanupInterceptor()); + } +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/entity/AuditEventEntity.java b/src/main/java/eu/europa/ec/dgc/gateway/entity/AuditEventEntity.java new file mode 100644 index 00000000..2f81f63d --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/entity/AuditEventEntity.java @@ -0,0 +1,85 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.entity; + +import java.time.ZonedDateTime; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@Entity +@Table(name = "audit_event") +@AllArgsConstructor +@NoArgsConstructor +public class AuditEventEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + + /** + * Timestamp of the Record. + */ + @Column(name = "timestamp", nullable = false) + private ZonedDateTime createdAt = ZonedDateTime.now(); + + /** + * ISO 3166 Alpha-2 Country Code. + * (plus code "EU" for administrative European Union entries). + */ + @Column(name = "country", nullable = false, length = 2) + private String country; + + /** + * uploader_sha256_fingerprint SHA256-fingerprint of the certificate. + */ + @Column(name = "uploader_sha256_fingerprint", nullable = false, length = 64) + private String uploaderSha256Fingerprint; + + /** + * uploader_sha256_fingerprint SHA256-fingerprint of the certificate. + */ + @Column(name = "authentication_sha256_fingerprint", nullable = false, length = 64) + private String authenticationSha256Fingerprint; + + /** + * ID of the event that was recorded. + */ + @Column(name = "event", nullable = false, length = 64) + private String event; + + /** + * Description of the recorded event. + */ + @Column(name = "description", nullable = false, length = 64) + private String description; + +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/entity/SignerInformationEntity.java b/src/main/java/eu/europa/ec/dgc/gateway/entity/SignerInformationEntity.java new file mode 100644 index 00000000..521fe813 --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/entity/SignerInformationEntity.java @@ -0,0 +1,97 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.entity; + +import java.time.ZonedDateTime; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@Entity +@Table(name = "signer_information") +@AllArgsConstructor +@NoArgsConstructor +public class SignerInformationEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + + /** + * Timestamp of the Record. + */ + @Column(name = "created_at", nullable = false) + private ZonedDateTime createdAt = ZonedDateTime.now(); + + /** + * ISO 3166 Alpha-2 Country Code + * (plus code "EU" for administrative European Union entries). + */ + @Column(name = "country", nullable = false, length = 2) + private String country; + + /** + * SHA-256 Thumbprint of the certificate (hex encoded). + */ + @Column(name = "thumbprint", nullable = false, length = 64, unique = true) + private String thumbprint; + + /** + * Base64 encoded certificate raw data. + */ + @Column(name = "raw_data", nullable = false, length = 4096) + String rawData; + + /** + * Signature of the TrustAnchor. + */ + @Column(name = "signature", nullable = false, length = 6000) + String signature; + + /** + * Type of the certificate (currently only DSC). + */ + @Column(name = "certificate_type", nullable = false) + @Enumerated(EnumType.STRING) + CertificateType certificateType; + + public enum CertificateType { + + /** + * Certificate which the member state is using to sign documents (NBDSC). + */ + DSC + + } + +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/entity/TrustedPartyEntity.java b/src/main/java/eu/europa/ec/dgc/gateway/entity/TrustedPartyEntity.java new file mode 100644 index 00000000..9f0efd7d --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/entity/TrustedPartyEntity.java @@ -0,0 +1,100 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.entity; + +import java.time.ZonedDateTime; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Entity +@Table(name = "trusted_party") +public class TrustedPartyEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + + /** + * Timestamp of the Record. + */ + @Column(name = "created_at", nullable = false) + private ZonedDateTime createdAt = ZonedDateTime.now(); + + /** + * ISO 3166 Alpha-2 Country Code + * (plus code "EU" for administrative European Union entries). + */ + @Column(name = "country", nullable = false, length = 2) + private String country; + + /** + * SHA-256 Thumbprint of the certificate (hex encoded). + */ + @Column(name = "thumbprint", nullable = false, length = 64, unique = true) + private String thumbprint; + + /** + * Base64 encoded certificate raw data. + */ + @Column(name = "raw_data", nullable = false, length = 4096) + String rawData; + + /** + * Signature of the TrustAnchor. + */ + @Column(name = "signature", nullable = false, length = 6000) + String signature; + + /** + * Type of the certificate (Authentication, Upload, CSCA). + */ + @Column(name = "certificate_type", nullable = false) + @Enumerated(EnumType.STRING) + CertificateType certificateType; + + public enum CertificateType { + /** + * Certificate which the member state is using to authenticate at DGC Gateway (NBTLS). + */ + AUTHENTICATION, + + /** + * Certificate which the member state is using to sign the uploaded information (NBUS). + */ + UPLOAD, + + /** + * Country Signing Certificate Authority certificate (NBCSCA). + */ + CSCA + } +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/entity/ValidationRuleEntity.java b/src/main/java/eu/europa/ec/dgc/gateway/entity/ValidationRuleEntity.java new file mode 100644 index 00000000..60c5b682 --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/entity/ValidationRuleEntity.java @@ -0,0 +1,108 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.entity; + +import java.time.ZonedDateTime; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Entity +@Table(name = "validation_rule", uniqueConstraints = {@UniqueConstraint(columnNames = {"rule_id", "version"})}) +public class ValidationRuleEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + + /** + * Timestamp of the Record. + */ + @Column(name = "created_at", nullable = false) + private ZonedDateTime createdAt = ZonedDateTime.now(); + + /** + * Identifier of the Rule. + * Needs to be a non ID column because Rule ID is not unique. + */ + @Column(name = "rule_id", nullable = false, length = 100) + private String ruleId; + + /** + * CMS containing the whole JSON validation rule. + */ + @Column(name = "signature", nullable = false, length = 10000) + private String cms; + + /** + * Date from when a rule is valid. + */ + @Column(name = "validFrom", nullable = false) + private ZonedDateTime validFrom; + + /** + * Date until a rule is valid. + */ + @Column(name = "validTo", nullable = false) + private ZonedDateTime validTo; + + /** + * Version of the rule. + */ + @Column(name = "version", nullable = false, length = 30) + private String version; + + /** + * 2-Digit Country Code of origin of the rule. + */ + @Column(name = "country", nullable = false, length = 2) + private String country; + + /** + * Type of the certificate (Authentication, Upload, CSCA). + */ + @Column(name = "type", nullable = false) + @Enumerated(EnumType.STRING) + ValidationRuleType validationRuleType; + + public enum ValidationRuleType { + /** + * Rule is used to validate a certificate. + */ + ACCEPTANCE, + + /** + * Rule is used to invalidate a certificate. + */ + INVALIDATION + } +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/entity/ValuesetEntity.java b/src/main/java/eu/europa/ec/dgc/gateway/entity/ValuesetEntity.java new file mode 100644 index 00000000..45a680b2 --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/entity/ValuesetEntity.java @@ -0,0 +1,52 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.entity; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Lob; +import javax.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@Entity +@Table(name = "valueset") +@AllArgsConstructor +@NoArgsConstructor +public class ValuesetEntity { + + @Id + @Column(name = "id", length = 100) + String id; + + /** + * Signature of the TrustAnchor. + */ + @Column(name = "json", nullable = false, length = 1024000) + @Lob + String json; + +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/exception/DgcgResponseException.java b/src/main/java/eu/europa/ec/dgc/gateway/exception/DgcgResponseException.java new file mode 100644 index 00000000..926abcca --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/exception/DgcgResponseException.java @@ -0,0 +1,51 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.exception; + +import lombok.Getter; +import org.springframework.http.HttpStatus; +import org.springframework.web.server.ResponseStatusException; + +@Getter +public class DgcgResponseException extends ResponseStatusException { + + private final String code; + private final String details; + private final String sentValues; + private final String problem; + + /** + * All Args constructor for DgcgResponseException. + * + * @param status the HTTP Status. + * @param code the error code. + * @param details the details of the problem. + * @param sentValues the values sent to cause the error. + * @param problem short problem description. + */ + public DgcgResponseException(HttpStatus status, String code, String problem, String sentValues, String details) { + super(status); + this.code = code; + this.details = details; + this.sentValues = sentValues; + this.problem = problem; + } +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/model/JrcRatValueset.java b/src/main/java/eu/europa/ec/dgc/gateway/model/JrcRatValueset.java new file mode 100644 index 00000000..f57aafa9 --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/model/JrcRatValueset.java @@ -0,0 +1,83 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.model; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.time.ZonedDateTime; +import java.util.List; +import lombok.Data; + +@Data +public class JrcRatValueset { + + @JsonProperty("id_device") + String idDevice; + + @JsonProperty("commercial_name") + String commercialName; + + @JsonProperty("manufacturer") + Manufacturer manufacturer; + + @JsonProperty("hsc_common_list") + Boolean hscCommonList; + + @JsonProperty("hsc_mutual_recognition") + Boolean hscMutualRecognition; + + @JsonProperty("last_updated") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss z") + ZonedDateTime lastUpdated; + + @JsonProperty("hsc_list_history") + List hscListHistory; + + @Data + public static class HscListHistory { + + @JsonProperty("list_date") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss z") + ZonedDateTime listDate; + + @JsonProperty("in_common_list") + Boolean inCommonList; + + @JsonProperty("in_mutual_recognition") + Boolean inMutualRecognition; + } + + @Data + public static class Manufacturer { + + @JsonProperty("id_manufacturer") + String id; + + @JsonProperty("name") + String name; + + @JsonProperty("country") + String country; + + @JsonProperty("website") + String website; + } +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/model/JrcRatValuesetResponse.java b/src/main/java/eu/europa/ec/dgc/gateway/model/JrcRatValuesetResponse.java new file mode 100644 index 00000000..d6789c33 --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/model/JrcRatValuesetResponse.java @@ -0,0 +1,38 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.model; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.time.ZonedDateTime; +import java.util.List; +import lombok.Data; + +@Data +public class JrcRatValuesetResponse { + + @JsonProperty("extracted_on") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss z") + ZonedDateTime extractedOn; + + @JsonProperty("deviceList") + List deviceList; +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/model/ParsedValidationRule.java b/src/main/java/eu/europa/ec/dgc/gateway/model/ParsedValidationRule.java new file mode 100644 index 00000000..1cb0ee10 --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/model/ParsedValidationRule.java @@ -0,0 +1,56 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.time.ZonedDateTime; +import lombok.Data; + +/** + * This class only represents by DGCG required properties of the Validation Rule JSON. + */ +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class ParsedValidationRule { + + @JsonProperty("Identifier") + String identifier; + + @JsonProperty("Type") + String type; + + @JsonProperty("Country") + String country; + + @JsonProperty("Version") + String version; + + @JsonProperty("CertificateType") + String certificateType; + + @JsonProperty("ValidFrom") + ZonedDateTime validFrom; + + @JsonProperty("ValidTo") + ZonedDateTime validTo; + +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/model/RatValueset.java b/src/main/java/eu/europa/ec/dgc/gateway/model/RatValueset.java new file mode 100644 index 00000000..44191663 --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/model/RatValueset.java @@ -0,0 +1,50 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.model; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.time.ZonedDateTime; +import lombok.Data; + +@Data +public class RatValueset { + + @JsonProperty("display") + String display; + + @JsonProperty("lang") + String lang = "en"; + + @JsonProperty("active") + Boolean active; + + @JsonProperty("system") + String system = "https://covid-19-diagnostics.jrc.ec.europa.eu/devices"; + + @JsonProperty("version") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss z", locale = "en-en") + ZonedDateTime version; + + @JsonProperty("validUntil") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss z", locale = "en-en") + ZonedDateTime validUntil; +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/model/TrustList.java b/src/main/java/eu/europa/ec/dgc/gateway/model/TrustList.java new file mode 100644 index 00000000..fb7574f5 --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/model/TrustList.java @@ -0,0 +1,44 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.model; + +import java.time.ZonedDateTime; +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class TrustList { + + private String kid; + + private ZonedDateTime timestamp; + + private String country; + + private TrustListType certificateType; + + private String thumbprint; + + private String signature; + + private String rawData; +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/model/TrustListType.java b/src/main/java/eu/europa/ec/dgc/gateway/model/TrustListType.java new file mode 100644 index 00000000..8ad1f7ff --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/model/TrustListType.java @@ -0,0 +1,29 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.model; + +public enum TrustListType { + + DSC, + UPLOAD, + CSCA, + AUTHENTICATION +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/model/Valueset.java b/src/main/java/eu/europa/ec/dgc/gateway/model/Valueset.java new file mode 100644 index 00000000..900fa5c2 --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/model/Valueset.java @@ -0,0 +1,41 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.time.LocalDate; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class Valueset { + + @JsonProperty("valueSetId") + String id; + + @JsonProperty("valueSetDate") + LocalDate date; + + @JsonProperty("valueSetValues") + Map value; +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/repository/AuditEventRepository.java b/src/main/java/eu/europa/ec/dgc/gateway/repository/AuditEventRepository.java new file mode 100644 index 00000000..410175bb --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/repository/AuditEventRepository.java @@ -0,0 +1,27 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.repository; + +import eu.europa.ec.dgc.gateway.entity.AuditEventEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface AuditEventRepository extends JpaRepository { +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/repository/SignerInformationRepository.java b/src/main/java/eu/europa/ec/dgc/gateway/repository/SignerInformationRepository.java new file mode 100644 index 00000000..e4c06131 --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/repository/SignerInformationRepository.java @@ -0,0 +1,43 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.repository; + +import eu.europa.ec.dgc.gateway.entity.SignerInformationEntity; +import java.util.List; +import java.util.Optional; +import javax.transaction.Transactional; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface SignerInformationRepository extends JpaRepository { + + Optional getFirstByThumbprint(String thumbprint); + + Optional getFirstByThumbprintStartsWith(String thumbprintStart); + + @Transactional + void deleteByThumbprint(String thumbprint); + + List getByCertificateType(SignerInformationEntity.CertificateType type); + + List getByCertificateTypeAndCountry( + SignerInformationEntity.CertificateType type, String countryCode); + +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/repository/TrustedPartyRepository.java b/src/main/java/eu/europa/ec/dgc/gateway/repository/TrustedPartyRepository.java new file mode 100644 index 00000000..903892c2 --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/repository/TrustedPartyRepository.java @@ -0,0 +1,44 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.repository; + +import eu.europa.ec.dgc.gateway.entity.TrustedPartyEntity; +import java.util.List; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +public interface TrustedPartyRepository extends JpaRepository { + + List getByCountryAndCertificateType(String country, TrustedPartyEntity.CertificateType type); + + List getByCertificateType(TrustedPartyEntity.CertificateType type); + + Optional getFirstByThumbprintAndCountryAndCertificateType( + String thumbprint, String country, TrustedPartyEntity.CertificateType type); + + Optional getFirstByThumbprintAndCertificateType( + String thumbprint, TrustedPartyEntity.CertificateType type); + + @Query("SELECT DISTINCT t.country FROM TrustedPartyEntity t") + List getCountryCodeList(); + +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/repository/ValidationRuleRepository.java b/src/main/java/eu/europa/ec/dgc/gateway/repository/ValidationRuleRepository.java new file mode 100644 index 00000000..7dc84f3d --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/repository/ValidationRuleRepository.java @@ -0,0 +1,60 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.repository; + +import eu.europa.ec.dgc.gateway.entity.ValidationRuleEntity; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Optional; +import javax.transaction.Transactional; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +@Repository +@Transactional +public interface ValidationRuleRepository extends JpaRepository { + + Optional getFirstByRuleIdOrderByIdDesc(String ruleId); + + @Query("SELECT v.id FROM ValidationRuleEntity v WHERE " + + "v.validFrom <= :threshold AND v.ruleId = :ruleId ORDER BY v.id DESC") + List getIdByValidFromIsBeforeAndRuleIdIs( + @Param("threshold") ZonedDateTime threshold, @Param("ruleId") String ruleId, Pageable pageable); + + List getByRuleIdAndValidFromIsGreaterThanEqualOrderByIdDesc( + String ruleId, ZonedDateTime threshold); + + @Query("SELECT max(v.id) FROM ValidationRuleEntity v WHERE v.country = :country GROUP BY v.ruleId") + List getLatestIds(@Param("country") String countryCode); + + List getByIdIsGreaterThanEqualAndRuleIdIsOrderByIdDesc(Long minimumId, String ruleId); + + @Modifying + @Query("DELETE FROM ValidationRuleEntity v WHERE v.ruleId = :ruleId") + int deleteByRuleId(@Param("ruleId") String ruleId); + + Optional getByRuleIdAndVersion(String ruleId, String version); + +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/repository/ValuesetRepository.java b/src/main/java/eu/europa/ec/dgc/gateway/repository/ValuesetRepository.java new file mode 100644 index 00000000..c5af55a5 --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/repository/ValuesetRepository.java @@ -0,0 +1,35 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.repository; + +import eu.europa.ec.dgc.gateway.entity.ValuesetEntity; +import java.util.List; +import javax.transaction.Transactional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +@Transactional +public interface ValuesetRepository extends JpaRepository { + + @Query("SELECT v.id FROM ValuesetEntity v") + List getIds(); + +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/restapi/controller/CountryListController.java b/src/main/java/eu/europa/ec/dgc/gateway/restapi/controller/CountryListController.java new file mode 100644 index 00000000..83410927 --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/restapi/controller/CountryListController.java @@ -0,0 +1,82 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.restapi.controller; + +import eu.europa.ec.dgc.gateway.config.OpenApiConfig; +import eu.europa.ec.dgc.gateway.restapi.dto.ProblemReportDto; +import eu.europa.ec.dgc.gateway.restapi.filter.CertificateAuthenticationRequired; +import eu.europa.ec.dgc.gateway.service.TrustedPartyService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/countrylist") +@RequiredArgsConstructor +@Validated +@Slf4j +public class CountryListController { + + private final TrustedPartyService trustedPartyService; + + /** + * Countrylist download endpoint. + */ + @CertificateAuthenticationRequired + @GetMapping(path = "", produces = MediaType.APPLICATION_JSON_VALUE) + @Operation( + security = { + @SecurityRequirement(name = OpenApiConfig.SECURITY_SCHEMA_HASH), + @SecurityRequirement(name = OpenApiConfig.SECURITY_SCHEMA_DISTINGUISH_NAME) + }, + summary = "Returns the full list of onboarded countries.", + tags = {"Country List"}, + responses = { + @ApiResponse( + responseCode = "200", + description = "Returns the full list of onboarded countries.", + content = @Content( + mediaType = MediaType.APPLICATION_JSON_VALUE, + array = @ArraySchema(schema = @Schema(example = "EU")))), + @ApiResponse( + responseCode = "401", + description = "Unauthorized. No Access to the system. (Client Certificate not present or whitelisted)", + content = @Content( + mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = ProblemReportDto.class) + )) + }) + public ResponseEntity> downloadCountryList() { + return ResponseEntity.ok(trustedPartyService.getCountryList()); + } +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/restapi/controller/SignerCertificateController.java b/src/main/java/eu/europa/ec/dgc/gateway/restapi/controller/SignerCertificateController.java new file mode 100644 index 00000000..fd2d41a1 --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/restapi/controller/SignerCertificateController.java @@ -0,0 +1,383 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.restapi.controller; + +import eu.europa.ec.dgc.gateway.config.OpenApiConfig; +import eu.europa.ec.dgc.gateway.exception.DgcgResponseException; +import eu.europa.ec.dgc.gateway.restapi.converter.CmsCertificateMessageConverter; +import eu.europa.ec.dgc.gateway.restapi.dto.ProblemReportDto; +import eu.europa.ec.dgc.gateway.restapi.dto.SignedCertificateDto; +import eu.europa.ec.dgc.gateway.restapi.filter.CertificateAuthenticationFilter; +import eu.europa.ec.dgc.gateway.restapi.filter.CertificateAuthenticationRequired; +import eu.europa.ec.dgc.gateway.service.AuditService; +import eu.europa.ec.dgc.gateway.service.SignerInformationService; +import eu.europa.ec.dgc.gateway.utils.DgcMdc; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestAttribute; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/signerCertificate") +@Slf4j +@RequiredArgsConstructor +public class SignerCertificateController { + + private final SignerInformationService signerInformationService; + + private final AuditService auditService; + + private static final String MDC_VERIFICATION_ERROR_REASON = "verificationFailureReason"; + private static final String MDC_VERIFICATION_ERROR_MESSAGE = "verificationFailureMessage"; + + /** + * VerificationInformation Upload Controller. + */ + @CertificateAuthenticationRequired + @PostMapping(path = "", consumes = CmsCertificateMessageConverter.CONTENT_TYPE_CMS_VALUE) + @Operation( + security = { + @SecurityRequirement(name = OpenApiConfig.SECURITY_SCHEMA_HASH), + @SecurityRequirement(name = OpenApiConfig.SECURITY_SCHEMA_DISTINGUISH_NAME) + }, + summary = "Uploads Signer Certificate of a trusted Issuer", + tags = {"Signer Information"}, + parameters = { + @Parameter( + in = ParameterIn.HEADER, + name = HttpHeaders.CONTENT_TYPE, + required = true, + schema = @Schema(type = "string"), + example = CmsCertificateMessageConverter.CONTENT_TYPE_CMS_VALUE), + @Parameter( + in = ParameterIn.HEADER, + name = HttpHeaders.CONTENT_ENCODING, + required = true, + schema = @Schema(type = "string"), + example = "base64") + }, + requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody( + required = true, + description = "Request body with payload. (limited)", + content = @Content( + mediaType = CmsCertificateMessageConverter.CONTENT_TYPE_CMS_VALUE, + schema = @Schema(implementation = SignedCertificateDto.class)) + ), + responses = { + @ApiResponse( + responseCode = "201", + description = "Verification Information was created successfully."), + @ApiResponse( + responseCode = "400", + description = "Bad request. Possible reasons: Wrong Format, no CMS, not the correct signing alg," + + " missing attributes, invalid signature, certificate not signed by known CA", + content = @Content( + mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = ProblemReportDto.class))), + @ApiResponse( + responseCode = "401", + description = "Unauthorized. No Access to the system. (Client Certificate not present or whitelisted)", + content = @Content( + mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = ProblemReportDto.class) + )), + @ApiResponse( + responseCode = "409", + description = "Conflict. Chosen UUID is already used. Please choose another one.", + content = @Content( + mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = ProblemReportDto.class))) + } + ) + public ResponseEntity postVerificationInformation( + @RequestBody SignedCertificateDto cms, + @RequestAttribute(CertificateAuthenticationFilter.REQUEST_PROP_COUNTRY) String countryCode, + @RequestAttribute(CertificateAuthenticationFilter.REQUEST_PROP_THUMBPRINT) String authThumbprint + ) { + + DgcMdc.put("signerCertSubject", cms.getSignerCertificate().getSubject().toString()); + DgcMdc.put("payloadCertSubject", cms.getPayloadCertificate().getSubject().toString()); + + log.info("Uploading new verification certificate"); + + if (!cms.isVerified()) { + throw new DgcgResponseException( + HttpStatus.BAD_REQUEST, + "0x009", + "Invalid CMS Signature", + "", + "Signature of CMS signed certificate is not validating content of CMS package"); + } + + try { + signerInformationService.addSignerCertificate( + cms.getPayloadCertificate(), + cms.getSignerCertificate(), + cms.getSignature(), + countryCode); + } catch (SignerInformationService.SignerCertCheckException e) { + DgcMdc.put(MDC_VERIFICATION_ERROR_REASON, e.getReason().toString()); + DgcMdc.put(MDC_VERIFICATION_ERROR_MESSAGE, e.getMessage()); + log.error("Verification certificate upload failed"); + + String sentValues = String.format("{%s} country:{%s}", cms, countryCode); + if (e.getReason() == SignerInformationService.SignerCertCheckException.Reason.ALREADY_EXIST_CHECK_FAILED) { + throw new DgcgResponseException(HttpStatus.CONFLICT, "0x002", + "You cant upload an existing certificate.", + sentValues, e.getMessage()); + } else if (e.getReason() == SignerInformationService.SignerCertCheckException.Reason.UPLOAD_FAILED) { + auditService.addAuditEvent( + countryCode, + cms.getSignerCertificate(), + authThumbprint, + "UPLOAD_FAILED", + "postVerificationInformation triggered UPLOAD_FAILED"); + + throw new DgcgResponseException(HttpStatus.INTERNAL_SERVER_ERROR, + "0x003", "Upload of Signer Certificate failed", sentValues, e.getMessage()); + } else { + auditService.addAuditEvent( + countryCode, + cms.getSignerCertificate(), + authThumbprint, + "BAD_REQUEST", + "postVerificationInformation triggered BAD_REQUEST"); + + throw new DgcgResponseException(HttpStatus.BAD_REQUEST, "0x004", "Possible reasons: Wrong Format," + + " no CMS, not the correct signing alg missing attributes, invalid signature, certificate not " + + "signed by known CA", sentValues, e.getMessage()); + } + } + auditService.addAuditEvent( + countryCode, + cms.getSignerCertificate(), + authThumbprint, + "SUCCESS", + "postVerificationInformation successful executed"); + return ResponseEntity.status(201).build(); + } + + /** + * Http Method for deleting signer certificate. + */ + @CertificateAuthenticationRequired + @DeleteMapping(path = "", consumes = CmsCertificateMessageConverter.CONTENT_TYPE_CMS_VALUE) + @Operation( + security = { + @SecurityRequirement(name = OpenApiConfig.SECURITY_SCHEMA_HASH), + @SecurityRequirement(name = OpenApiConfig.SECURITY_SCHEMA_DISTINGUISH_NAME) + }, + summary = "Deletes Signer Certificate of a trusted Issuer", + tags = {"Signer Information"}, + parameters = { + @Parameter( + in = ParameterIn.HEADER, + name = HttpHeaders.CONTENT_TYPE, + required = true, + schema = @Schema(type = "string"), + example = CmsCertificateMessageConverter.CONTENT_TYPE_CMS_VALUE), + @Parameter( + in = ParameterIn.HEADER, + name = HttpHeaders.CONTENT_ENCODING, + required = true, + schema = @Schema(type = "string"), + example = "base64") + }, + requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody( + required = true, + description = "Request body with payload. (limited)", + content = @Content( + mediaType = CmsCertificateMessageConverter.CONTENT_TYPE_CMS_VALUE, + schema = @Schema(implementation = SignedCertificateDto.class)) + ), + responses = { + @ApiResponse( + responseCode = "204", + description = "Certificate was deleted successfully."), + @ApiResponse( + responseCode = "400", + description = "Bad request. Possible reasons: Wrong Format, no CMS, not the correct signing alg," + + " missing attributes, invalid signature, certificate not signed by known CA", + content = @Content( + mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = ProblemReportDto.class))), + @ApiResponse( + responseCode = "401", + description = "Unauthorized. No Access to the system. (Client Certificate not present or whitelisted)", + content = @Content( + mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = ProblemReportDto.class) + )) + } + ) + public ResponseEntity deleteVerificationInformation( + @RequestBody SignedCertificateDto cms, + @RequestAttribute(CertificateAuthenticationFilter.REQUEST_PROP_COUNTRY) String countryCode, + @RequestAttribute(CertificateAuthenticationFilter.REQUEST_PROP_THUMBPRINT) String authThumbprint + ) { + + DgcMdc.put("signerCertSubject", cms.getSignerCertificate().getSubject().toString()); + DgcMdc.put("payloadCertSubject", cms.getPayloadCertificate().getSubject().toString()); + + log.info("Revoking verification certificate"); + + if (!cms.isVerified()) { + throw new DgcgResponseException( + HttpStatus.BAD_REQUEST, + "0x009", + "Invalid CMS Signature", + "", + "Signature of CMS signed certificate is not validating content of CMS package"); + } + + try { + signerInformationService.deleteSignerCertificate( + cms.getPayloadCertificate(), + cms.getSignerCertificate(), + countryCode); + } catch (SignerInformationService.SignerCertCheckException e) { + DgcMdc.put(MDC_VERIFICATION_ERROR_REASON, e.getReason().toString()); + DgcMdc.put(MDC_VERIFICATION_ERROR_MESSAGE, e.getMessage()); + log.error("Verification certificate delete failed"); + + String sentValues = String.format("{%s} country:{%s}", cms, countryCode); + if (e.getReason() == SignerInformationService.SignerCertCheckException.Reason.EXIST_CHECK_FAILED) { + auditService.addAuditEvent( + countryCode, + cms.getSignerCertificate(), + authThumbprint, + "EXIST_CHECK_FAILED", + "revokeVerificationInformation triggered EXIST_CHECK_FAILED"); + + throw new DgcgResponseException(HttpStatus.NOT_FOUND, "0x005", + "The certificate doesn't exists in the database.", + sentValues, e.getMessage()); + } else if (e.getReason() == SignerInformationService.SignerCertCheckException.Reason.UPLOAD_FAILED) { + auditService.addAuditEvent( + countryCode, + cms.getSignerCertificate(), + authThumbprint, + "DELETE_FAILED", + "revokeVerificationInformation triggered UPLOAD_FAILED"); + + throw new DgcgResponseException(HttpStatus.INTERNAL_SERVER_ERROR, + "0x006", "Delete of Signer Certificate failed", sentValues, e.getMessage()); + } else { + auditService.addAuditEvent( + countryCode, + cms.getSignerCertificate(), + authThumbprint, + "BAD_REQUEST", + "revokeVerificationInformation triggered BAD_REQUEST"); + + throw new DgcgResponseException(HttpStatus.BAD_REQUEST, "0x007", "Possible reasons: Wrong Format," + + " no CMS, not the correct signing alg missing attributes, invalid signature, certificate not " + + "signed by known CA", sentValues, e.getMessage()); + } + } + auditService.addAuditEvent( + countryCode, + cms.getSignerCertificate(), + authThumbprint, + "SUCCESS", + "revokeVerificationInformation triggered SUCCESS"); + + return ResponseEntity.status(HttpStatus.NO_CONTENT).build(); + } + + /** + * Alias Method for deleting signer certificate. + */ + @CertificateAuthenticationRequired + @PostMapping(path = "/delete", consumes = CmsCertificateMessageConverter.CONTENT_TYPE_CMS_VALUE) + @Operation( + security = { + @SecurityRequirement(name = OpenApiConfig.SECURITY_SCHEMA_HASH), + @SecurityRequirement(name = OpenApiConfig.SECURITY_SCHEMA_DISTINGUISH_NAME) + }, + summary = "Deletes Signer Certificate of a trusted Issuer", + description = "This endpoint is a workaround alias endpoint. This should only be used if it is not possible" + + " to send http payloads with DELETE requests.", + tags = {"Signer Information"}, + parameters = { + @Parameter( + in = ParameterIn.HEADER, + name = HttpHeaders.CONTENT_TYPE, + required = true, + schema = @Schema(type = "string"), + example = CmsCertificateMessageConverter.CONTENT_TYPE_CMS_VALUE), + @Parameter( + in = ParameterIn.HEADER, + name = HttpHeaders.CONTENT_ENCODING, + required = true, + schema = @Schema(type = "string"), + example = "base64") + }, + requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody( + required = true, + description = "Request body with payload. (limited)", + content = @Content( + mediaType = CmsCertificateMessageConverter.CONTENT_TYPE_CMS_VALUE, + schema = @Schema(implementation = SignedCertificateDto.class)) + ), + responses = { + @ApiResponse( + responseCode = "204", + description = "Certificate was deleted successfully."), + @ApiResponse( + responseCode = "400", + description = "Bad request. Possible reasons: Wrong Format, no CMS, not the correct signing alg," + + " missing attributes, invalid signature, certificate not signed by known CA", + content = @Content( + mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = ProblemReportDto.class))), + @ApiResponse( + responseCode = "401", + description = "Unauthorized. No Access to the system. (Client Certificate not present or whitelisted)", + content = @Content( + mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = ProblemReportDto.class) + )) + } + ) + public ResponseEntity deleteVerificationInformationAlias( + @RequestBody SignedCertificateDto cms, + @RequestAttribute(CertificateAuthenticationFilter.REQUEST_PROP_COUNTRY) String countryCode, + @RequestAttribute(CertificateAuthenticationFilter.REQUEST_PROP_THUMBPRINT) String authThumbprint + ) { + return deleteVerificationInformation(cms, countryCode, authThumbprint); + } +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/restapi/controller/TrustListController.java b/src/main/java/eu/europa/ec/dgc/gateway/restapi/controller/TrustListController.java new file mode 100644 index 00000000..5a2b48d9 --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/restapi/controller/TrustListController.java @@ -0,0 +1,242 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.restapi.controller; + +import eu.europa.ec.dgc.gateway.config.OpenApiConfig; +import eu.europa.ec.dgc.gateway.model.TrustListType; +import eu.europa.ec.dgc.gateway.restapi.dto.CertificateTypeDto; +import eu.europa.ec.dgc.gateway.restapi.dto.ProblemReportDto; +import eu.europa.ec.dgc.gateway.restapi.dto.TrustListDto; +import eu.europa.ec.dgc.gateway.restapi.filter.CertificateAuthenticationFilter; +import eu.europa.ec.dgc.gateway.restapi.filter.CertificateAuthenticationRequired; +import eu.europa.ec.dgc.gateway.restapi.mapper.GwTrustListMapper; +import eu.europa.ec.dgc.gateway.service.TrustListService; +import eu.europa.ec.dgc.gateway.utils.DgcMdc; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import java.util.List; +import java.util.Locale; +import javax.validation.Valid; +import javax.validation.constraints.Size; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestAttribute; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/trustList") +@RequiredArgsConstructor +@Validated +@Slf4j +public class TrustListController { + + private final TrustListService trustListService; + + private final GwTrustListMapper trustListMapper; + + private static final String MDC_PROP_DOWNLOAD_KEYS_COUNT = "downloadedKeys"; + private static final String MDC_PROP_DOWNLOAD_KEYS_TYPE = "downloadedKeysType"; + private static final String MDC_PROP_DOWNLOAD_KEYS_COUNTRY = "downloadedKeysCountry"; + private static final String DOWNLOADED_TRUSTLIST_LOG_MESSAGE = "Downloaded TrustList"; + + /** + * TrustList Download Controller. + */ + @CertificateAuthenticationRequired + @GetMapping(path = "", produces = MediaType.APPLICATION_JSON_VALUE) + @Operation( + security = { + @SecurityRequirement(name = OpenApiConfig.SECURITY_SCHEMA_HASH), + @SecurityRequirement(name = OpenApiConfig.SECURITY_SCHEMA_DISTINGUISH_NAME) + }, + summary = "Returns the full list of trusted certificates.", + tags = {"Trust Lists"}, + responses = { + @ApiResponse( + responseCode = "200", + description = "Returns the full list of trusted parties.", + content = @Content( + mediaType = MediaType.APPLICATION_JSON_VALUE, + array = @ArraySchema(schema = @Schema(implementation = TrustListDto.class)))), + @ApiResponse( + responseCode = "401", + description = "Unauthorized. No Access to the system. (Client Certificate not present or whitelisted)", + content = @Content( + mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = ProblemReportDto.class) + )) + }) + public ResponseEntity> downloadTrustList( + @RequestAttribute(CertificateAuthenticationFilter.REQUEST_PROP_COUNTRY) String downloaderCountryCode + ) { + List trustList = trustListMapper.trustListToTrustListDto(trustListService.getTrustList()); + + DgcMdc.put(MDC_PROP_DOWNLOAD_KEYS_COUNT, trustList.size()); + DgcMdc.put(MDC_PROP_DOWNLOAD_KEYS_COUNTRY, downloaderCountryCode); + + log.info(DOWNLOADED_TRUSTLIST_LOG_MESSAGE); + + return ResponseEntity.ok(trustList); + } + + /** + * TrustList Download Controller (filtered by type). + */ + @CertificateAuthenticationRequired + @GetMapping(path = "/{type}", produces = MediaType.APPLICATION_JSON_VALUE) + @Operation( + security = { + @SecurityRequirement(name = OpenApiConfig.SECURITY_SCHEMA_HASH), + @SecurityRequirement(name = OpenApiConfig.SECURITY_SCHEMA_DISTINGUISH_NAME) + }, + summary = "Returns a filtered list of trusted certificates.", + tags = {"Trust Lists"}, + parameters = { + @Parameter( + in = ParameterIn.PATH, + name = "type", + description = "Certificate Type to filter for", + required = true, + schema = @Schema(implementation = CertificateTypeDto.class)) + }, + responses = { + @ApiResponse( + responseCode = "200", + description = "Returns a filtered list of trusted certificates.", + content = @Content( + mediaType = MediaType.APPLICATION_JSON_VALUE, + array = @ArraySchema(schema = @Schema(implementation = TrustListDto.class)))), + @ApiResponse( + responseCode = "400", + description = "Bad request. Unknown Certificate Type.", + content = @Content( + mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = ProblemReportDto.class) + )), + @ApiResponse( + responseCode = "401", + description = "Unauthorized. No Access to the system. (Client Certificate not present or whitelisted)", + content = @Content( + mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = ProblemReportDto.class) + )) + }) + public ResponseEntity> downloadTrustListFilteredByType( + @Valid @PathVariable("type") CertificateTypeDto type, + @RequestAttribute(CertificateAuthenticationFilter.REQUEST_PROP_COUNTRY) String downloaderCountryCode + ) { + + TrustListType mappedType = trustListMapper.certificateTypeDtoToTrustListType(type); + + List trustList = trustListMapper.trustListToTrustListDto( + trustListService.getTrustList(mappedType)); + + DgcMdc.put(MDC_PROP_DOWNLOAD_KEYS_COUNT, trustList.size()); + DgcMdc.put(MDC_PROP_DOWNLOAD_KEYS_TYPE, type.name()); + DgcMdc.put(MDC_PROP_DOWNLOAD_KEYS_COUNTRY, downloaderCountryCode); + + log.info(DOWNLOADED_TRUSTLIST_LOG_MESSAGE); + + return ResponseEntity.ok(trustList); + } + + /** + * TrustList Download Controller (filtered by type and country). + */ + @CertificateAuthenticationRequired + @GetMapping(path = "/{type}/{country}", produces = MediaType.APPLICATION_JSON_VALUE) + @Operation( + security = { + @SecurityRequirement(name = OpenApiConfig.SECURITY_SCHEMA_HASH), + @SecurityRequirement(name = OpenApiConfig.SECURITY_SCHEMA_DISTINGUISH_NAME) + }, + summary = "Returns a filtered list of trusted certificates.", + tags = {"Trust Lists"}, + parameters = { + @Parameter( + in = ParameterIn.PATH, + name = "type", + description = "Certificate Type to filter for", + required = true, + schema = @Schema(implementation = CertificateTypeDto.class)), + @Parameter( + in = ParameterIn.PATH, + name = "country", + description = "2-Digit Country Code to filter for", + example = "EU", + required = true) + }, + responses = { + @ApiResponse( + responseCode = "200", + description = "Returns a filtered list of trusted certificates.", + content = @Content( + mediaType = MediaType.APPLICATION_JSON_VALUE, + array = @ArraySchema(schema = @Schema(implementation = TrustListDto.class)))), + @ApiResponse( + responseCode = "400", + description = "Bad request. Unknown Certificate Type or invalid country code.", + content = @Content( + mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = ProblemReportDto.class) + )), + @ApiResponse( + responseCode = "401", + description = "Unauthorized. No Access to the system. (Client Certificate not present or whitelisted)", + content = @Content( + mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = ProblemReportDto.class) + )) + }) + public ResponseEntity> downloadTrustListFilteredByCountryAndType( + @Valid @PathVariable("type") CertificateTypeDto type, + @Valid @Size(max = 2, min = 2) @PathVariable("country") String countryCode, + @RequestAttribute(CertificateAuthenticationFilter.REQUEST_PROP_COUNTRY) String downloaderCountryCode + ) { + + TrustListType mappedType = trustListMapper.certificateTypeDtoToTrustListType(type); + countryCode = countryCode.toUpperCase(Locale.ROOT); + + List trustList = trustListMapper.trustListToTrustListDto( + trustListService.getTrustList(mappedType, countryCode)); + + DgcMdc.put(MDC_PROP_DOWNLOAD_KEYS_COUNT, trustList.size()); + DgcMdc.put(MDC_PROP_DOWNLOAD_KEYS_TYPE, type.name()); + DgcMdc.put(MDC_PROP_DOWNLOAD_KEYS_COUNTRY, downloaderCountryCode); + + log.info(DOWNLOADED_TRUSTLIST_LOG_MESSAGE); + + return ResponseEntity.ok(trustList); + } + +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/restapi/controller/ValidationRuleController.java b/src/main/java/eu/europa/ec/dgc/gateway/restapi/controller/ValidationRuleController.java new file mode 100644 index 00000000..cb8da86c --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/restapi/controller/ValidationRuleController.java @@ -0,0 +1,378 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.restapi.controller; + +import eu.europa.ec.dgc.gateway.config.OpenApiConfig; +import eu.europa.ec.dgc.gateway.entity.ValidationRuleEntity; +import eu.europa.ec.dgc.gateway.exception.DgcgResponseException; +import eu.europa.ec.dgc.gateway.restapi.converter.CmsStringMessageConverter; +import eu.europa.ec.dgc.gateway.restapi.dto.ProblemReportDto; +import eu.europa.ec.dgc.gateway.restapi.dto.SignedStringDto; +import eu.europa.ec.dgc.gateway.restapi.dto.ValidationRuleDto; +import eu.europa.ec.dgc.gateway.restapi.filter.CertificateAuthenticationFilter; +import eu.europa.ec.dgc.gateway.restapi.filter.CertificateAuthenticationRequired; +import eu.europa.ec.dgc.gateway.restapi.mapper.GwValidationRuleMapper; +import eu.europa.ec.dgc.gateway.service.AuditService; +import eu.europa.ec.dgc.gateway.service.ValidationRuleService; +import eu.europa.ec.dgc.gateway.utils.DgcMdc; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.parameters.RequestBody; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.hibernate.validator.constraints.Length; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestAttribute; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/rules") +@Slf4j +@RequiredArgsConstructor +public class ValidationRuleController { + + private final ValidationRuleService validationRuleService; + + private final AuditService auditService; + + private final GwValidationRuleMapper validationRuleMapper; + + private static final String MDC_VALIDATION_RULE_DOWNLOAD_AMOUNT = "validationDownloadAmount"; + private static final String MDC_VALIDATION_RULE_DOWNLOAD_REQUESTER = "validationDownloadRequester"; + private static final String MDC_VALIDATION_RULE_DOWNLOAD_REQUESTED = "validationDownloadRequested"; + private static final String MDC_VALIDATION_RULE_DELETE_ID = "validationDownloadId"; + private static final String MDC_VALIDATION_RULE_DELETE_AMOUNT = "validationDeleteAmount"; + + /** + * Endpoint to download a Validation Rule. + */ + @CertificateAuthenticationRequired + @GetMapping(path = "/{country}", produces = MediaType.APPLICATION_JSON_VALUE) + @Operation( + security = { + @SecurityRequirement(name = OpenApiConfig.SECURITY_SCHEMA_HASH), + @SecurityRequirement(name = OpenApiConfig.SECURITY_SCHEMA_DISTINGUISH_NAME) + }, + summary = "Download all rules of country.", + tags = {"Validation Rules"}, + parameters = { + @Parameter( + in = ParameterIn.PATH, + name = "country", + required = true, + example = "EU") + }, + responses = { + @ApiResponse( + responseCode = "200", + description = "Download successful.", + content = @Content( + mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(ref = "#/components/schemas/ValidationRuleDownloadResponse") + ) + ) + } + ) + public ResponseEntity>> downloadValidationRules( + @Valid @PathVariable("country") @Length(max = 2, min = 2) String requestedCountryCode, + @RequestAttribute(CertificateAuthenticationFilter.REQUEST_PROP_COUNTRY) String requesterCountryCode + ) { + + log.info("Rule Download Request"); + + List validationRuleEntities = + validationRuleService.getActiveValidationRules(requestedCountryCode); + + Map> map = new HashMap<>(); + + validationRuleEntities.forEach(validationRuleEntitiy -> + map.computeIfAbsent(validationRuleEntitiy.getRuleId(), k -> new ArrayList<>()) + .add(validationRuleMapper.entityToDto(validationRuleEntitiy))); + + DgcMdc.put(MDC_VALIDATION_RULE_DOWNLOAD_AMOUNT, validationRuleEntities.size()); + DgcMdc.put(MDC_VALIDATION_RULE_DOWNLOAD_REQUESTER, requesterCountryCode); + DgcMdc.put(MDC_VALIDATION_RULE_DOWNLOAD_REQUESTED, requestedCountryCode); + log.info("Rule Download Success"); + + return ResponseEntity.ok(map); + } + + /** + * Endpoint to upload a Validation Rule. + */ + @CertificateAuthenticationRequired + @PostMapping(path = "", consumes = { + CmsStringMessageConverter.CONTENT_TYPE_CMS_TEXT_VALUE, CmsStringMessageConverter.CONTENT_TYPE_CMS_VALUE}) + @Operation( + security = { + @SecurityRequirement(name = OpenApiConfig.SECURITY_SCHEMA_HASH), + @SecurityRequirement(name = OpenApiConfig.SECURITY_SCHEMA_DISTINGUISH_NAME) + }, + summary = "Create a new versions of a rule with id", + tags = {"Validation Rules"}, + requestBody = @RequestBody( + required = true, + description = "CMS Signed String with Validation Rule. Needs to be signed with valid Upload Certificate" + ), + responses = { + @ApiResponse( + responseCode = "201", + description = "Created successful." + ), + @ApiResponse( + responseCode = "400", + description = "Bad data submitted. See ProblemReport for more details.", + content = @Content(schema = @Schema(implementation = ProblemReportDto.class)) + ), + @ApiResponse( + responseCode = "403", + description = "You are not allowed to create this validation rules.", + content = @Content(schema = @Schema(implementation = ProblemReportDto.class)) + ) + } + ) + public ResponseEntity uploadValidationRule( + @org.springframework.web.bind.annotation.RequestBody SignedStringDto signedJson, + @RequestAttribute(CertificateAuthenticationFilter.REQUEST_PROP_COUNTRY) String authenticatedCountryCode, + @RequestAttribute(CertificateAuthenticationFilter.REQUEST_PROP_THUMBPRINT) String thumbprint + ) { + + log.info("Rule Upload Request"); + + if (!signedJson.isVerified()) { + throw new DgcgResponseException(HttpStatus.BAD_REQUEST, "0x260", "CMS signature is invalid", "", + "Submitted string needs to be signed by a valid upload certificate"); + } + + ValidationRuleEntity createdValidationRule; + + try { + createdValidationRule = validationRuleService.addValidationRule( + signedJson.getPayloadString(), + signedJson.getSignerCertificate(), + signedJson.getRawMessage(), + authenticatedCountryCode); + } catch (ValidationRuleService.ValidationRuleCheckException e) { + DgcMdc.put("validationRuleUploadError", e.getMessage()); + DgcMdc.put("validationRuleUploadReason", e.getReason().toString()); + log.error("Rule Upload Failed"); + + switch (e.getReason()) { + case INVALID_JSON: + throw new DgcgResponseException( + HttpStatus.BAD_REQUEST, "0x200", "Invalid JSON", "", e.getMessage()); + case INVALID_COUNTRY: + throw new DgcgResponseException(HttpStatus.FORBIDDEN, "0x210", "Invalid Country sent", "", + e.getMessage()); + case INVALID_VERSION: + throw new DgcgResponseException(HttpStatus.BAD_REQUEST, "0x220", "Invalid Version", "", + e.getMessage()); + case UPLOADER_CERT_CHECK_FAILED: + throw new DgcgResponseException(HttpStatus.BAD_REQUEST, "0x230", "Invalid Upload Cert", + signedJson.getSignerCertificate().getSubject().toString(), e.getMessage()); + case INVALID_TIMESTAMP: + throw new DgcgResponseException(HttpStatus.BAD_REQUEST, "0x240", "Invalid Timestamp(s)", + "", e.getMessage()); + case INVALID_RULE_ID: + throw new DgcgResponseException(HttpStatus.BAD_REQUEST, "0x250", "Invalid RuleID", + "", e.getMessage()); + default: + throw new DgcgResponseException(HttpStatus.INTERNAL_SERVER_ERROR, "0x299", "Unexpected Error", + "", ""); + } + } + + + log.info("Rule Upload Success"); + + auditService.addAuditEvent( + authenticatedCountryCode, + signedJson.getSignerCertificate(), + thumbprint, + "CREATED", + String.format("Created Validation Rule with ID %s (%s)", + createdValidationRule.getRuleId(), createdValidationRule.getVersion())); + + return ResponseEntity.status(HttpStatus.CREATED).build(); + } + + /** + * Endpoint to delete a Validation Rule. + */ + @CertificateAuthenticationRequired + @DeleteMapping(path = "", consumes = { + CmsStringMessageConverter.CONTENT_TYPE_CMS_TEXT_VALUE, CmsStringMessageConverter.CONTENT_TYPE_CMS_VALUE}) + @Operation( + security = { + @SecurityRequirement(name = OpenApiConfig.SECURITY_SCHEMA_HASH), + @SecurityRequirement(name = OpenApiConfig.SECURITY_SCHEMA_DISTINGUISH_NAME) + }, + summary = "Delete all versions of a rule with id", + tags = {"Validation Rules"}, + requestBody = @RequestBody( + required = true, + description = "CMS Signed String representing the Rule ID. Needs to be signed with valid Upload Certificate" + ), + responses = { + @ApiResponse( + responseCode = "204", + description = "Delete successful." + ), + @ApiResponse( + responseCode = "400", + description = "Bad data submitted. See ProblemReport for more details.", + content = @Content(schema = @Schema(implementation = ProblemReportDto.class)) + ), + @ApiResponse( + responseCode = "403", + description = "You are not allowed to delete these validation rules.", + content = @Content(schema = @Schema(implementation = ProblemReportDto.class)) + ), + @ApiResponse( + responseCode = "404", + description = "Validation rule not found.", + content = @Content(schema = @Schema(implementation = ProblemReportDto.class)) + ) + } + ) + public ResponseEntity deleteValidationRules( + @org.springframework.web.bind.annotation.RequestBody SignedStringDto signedString, + @RequestAttribute(CertificateAuthenticationFilter.REQUEST_PROP_COUNTRY) String authenticatedCountryCode, + @RequestAttribute(CertificateAuthenticationFilter.REQUEST_PROP_THUMBPRINT) String thumbprint + ) { + + log.info("Rule Delete Request"); + + if (!signedString.isVerified()) { + throw new DgcgResponseException(HttpStatus.BAD_REQUEST, "0x260", "CMS signature is invalid", "", + "Submitted string needs to be signed by a valid upload certificate"); + } + + try { + validationRuleService.contentCheckUploaderCertificate( + signedString.getSignerCertificate(), authenticatedCountryCode); + } catch (ValidationRuleService.ValidationRuleCheckException e) { + throw new DgcgResponseException(HttpStatus.BAD_REQUEST, "0x230", "Invalid Upload certificate", "", + "You have to use a onboarded upload certificate to sign the string"); + } + + String countryCodeFromIdString = + validationRuleService.getCountryCodeFromIdString(signedString.getPayloadString()); + + if (countryCodeFromIdString == null) { + throw new DgcgResponseException(HttpStatus.BAD_REQUEST, "0x250", "ID-String is invalid", + signedString.getPayloadString(), "Example valid ID-String: GR-EU-11100"); + } + + if (!countryCodeFromIdString.equals(authenticatedCountryCode)) { + throw new DgcgResponseException(HttpStatus.FORBIDDEN, "0x210", "Invalid country in ID-String", + String.format( + "Your authenticated country code: %s, Your requested country code: %s", + authenticatedCountryCode, countryCodeFromIdString), + "ID-String needs to contain your Country Code."); + } + + int deleted = validationRuleService.deleteByRuleId(signedString.getPayloadString()); + + if (deleted == 0) { + throw new DgcgResponseException(HttpStatus.NOT_FOUND, "0x270", "Validation Rule does not exist", + String.format("Validation-Rule Id: %s", signedString.getPayloadString()), + "You can only delete existing validation rules."); + } + + DgcMdc.put(MDC_VALIDATION_RULE_DELETE_AMOUNT, deleted); + DgcMdc.put(MDC_VALIDATION_RULE_DELETE_ID, signedString.getPayloadString()); + log.info("Rule Delete Success"); + + auditService.addAuditEvent( + authenticatedCountryCode, + signedString.getSignerCertificate(), + thumbprint, + "DELETED", + "Deleted Validation Rule with ID " + signedString.getPayloadString()); + + return ResponseEntity.status(HttpStatus.NO_CONTENT).build(); + } + + /** + * Alias endpoint to delete a Validation Rule. + */ + @CertificateAuthenticationRequired + @PostMapping(path = "/delete", consumes = { + CmsStringMessageConverter.CONTENT_TYPE_CMS_TEXT_VALUE, CmsStringMessageConverter.CONTENT_TYPE_CMS_VALUE}) + @Operation( + security = { + @SecurityRequirement(name = OpenApiConfig.SECURITY_SCHEMA_HASH), + @SecurityRequirement(name = OpenApiConfig.SECURITY_SCHEMA_DISTINGUISH_NAME) + }, + summary = "Delete all versions of a rule with id (Alias Endpoint for DELETE)", + tags = {"Validation Rules"}, + requestBody = @RequestBody( + required = true, + description = "CMS Signed String representing the Rule ID. Needs to be signed with valid Upload Certificate" + ), + responses = { + @ApiResponse( + responseCode = "204", + description = "Delete successful." + ), + @ApiResponse( + responseCode = "400", + description = "Bad data submitted. See ProblemReport for more details.", + content = @Content(schema = @Schema(implementation = ProblemReportDto.class)) + ), + @ApiResponse( + responseCode = "403", + description = "You are not allowed to delete these validation rules.", + content = @Content(schema = @Schema(implementation = ProblemReportDto.class)) + ), + @ApiResponse( + responseCode = "404", + description = "Validation rule not found.", + content = @Content(schema = @Schema(implementation = ProblemReportDto.class)) + ) + } + ) + public ResponseEntity deleteValidationRulesAliasEndpoint( + @org.springframework.web.bind.annotation.RequestBody SignedStringDto signedString, + @RequestAttribute(CertificateAuthenticationFilter.REQUEST_PROP_COUNTRY) String authenticatedCountryCode, + @RequestAttribute(CertificateAuthenticationFilter.REQUEST_PROP_THUMBPRINT) String thumbprint + ) { + return deleteValidationRules(signedString, authenticatedCountryCode, thumbprint); + } +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/restapi/controller/ValuesetController.java b/src/main/java/eu/europa/ec/dgc/gateway/restapi/controller/ValuesetController.java new file mode 100644 index 00000000..1bbe1e06 --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/restapi/controller/ValuesetController.java @@ -0,0 +1,141 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.restapi.controller; + +import eu.europa.ec.dgc.gateway.config.OpenApiConfig; +import eu.europa.ec.dgc.gateway.exception.DgcgResponseException; +import eu.europa.ec.dgc.gateway.restapi.dto.ProblemReportDto; +import eu.europa.ec.dgc.gateway.restapi.filter.CertificateAuthenticationRequired; +import eu.europa.ec.dgc.gateway.service.ValuesetService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import java.util.List; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/valuesets") +@Slf4j +@RequiredArgsConstructor +public class ValuesetController { + + private final ValuesetService valuesetService; + + /** + * Controller to get valueset ids. + */ + @CertificateAuthenticationRequired + @GetMapping(path = "") + @Operation( + security = { + @SecurityRequirement(name = OpenApiConfig.SECURITY_SCHEMA_HASH), + @SecurityRequirement(name = OpenApiConfig.SECURITY_SCHEMA_DISTINGUISH_NAME) + }, + summary = "Gets a list of available valuesets.", + tags = {"Valueset"}, + responses = { + @ApiResponse( + responseCode = "200", + description = "List of valueset ids", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = String.class))) + ), + @ApiResponse( + responseCode = "401", + description = "Unauthorized. No Access to the system. (Client Certificate not present or whitelisted)", + content = @Content( + mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = ProblemReportDto.class) + )) + } + ) + public ResponseEntity> getValuesetIds() { + return ResponseEntity.ok(valuesetService.getValuesetIds()); + } + + /** + * Controller to get a specific valueset. + */ + @CertificateAuthenticationRequired + @GetMapping(path = "/{id}") + @Operation( + security = { + @SecurityRequirement(name = OpenApiConfig.SECURITY_SCHEMA_HASH), + @SecurityRequirement(name = OpenApiConfig.SECURITY_SCHEMA_DISTINGUISH_NAME) + }, + summary = "Requests a specific valueset by its id.", + tags = {"Valueset"}, + parameters = @Parameter(in = ParameterIn.PATH, name = "id", description = "Valueset ID"), + responses = { + @ApiResponse( + responseCode = "200", + description = "Valueset JSON Object", + content = @Content(schema = @Schema(implementation = String.class)) + ), + @ApiResponse( + responseCode = "401", + description = "Unauthorized. No Access to the system. (Client Certificate not present or whitelisted)", + content = @Content( + mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = ProblemReportDto.class) + )), + @ApiResponse( + responseCode = "404", + description = "Valueset not found", + content = @Content( + mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = ProblemReportDto.class) + )) + } + ) + public ResponseEntity getValueset(@PathVariable("id") String id) { + + Optional valueset = valuesetService.getValueSetById(id); + + if (valueset.isEmpty()) { + throw new DgcgResponseException( + HttpStatus.NOT_FOUND, + "0x100", + "Valueset not found", + String.format("Requested valueset id %s", id), + "Use the GET /valuesets endpoint to get a list of available valueset ids."); + } + + return ResponseEntity + .status(HttpStatus.OK) + .contentType(MediaType.APPLICATION_JSON) + .body(valueset.get()); + } + +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/restapi/converter/CertificateTypeEnumConverter.java b/src/main/java/eu/europa/ec/dgc/gateway/restapi/converter/CertificateTypeEnumConverter.java new file mode 100644 index 00000000..1c3043c3 --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/restapi/converter/CertificateTypeEnumConverter.java @@ -0,0 +1,41 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.restapi.converter; + +import eu.europa.ec.dgc.gateway.restapi.dto.CertificateTypeDto; +import java.util.Locale; +import org.springframework.core.convert.converter.Converter; +import org.springframework.stereotype.Component; + +@Component +public class CertificateTypeEnumConverter implements Converter { + + /** + * Converts a string into {@link CertificateTypeDto} (case insensitive). + * + * @param source String to convert + * @return value of {@link CertificateTypeDto} + */ + @Override + public CertificateTypeDto convert(String source) { + return CertificateTypeDto.valueOf(source.toUpperCase(Locale.ROOT)); + } +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/restapi/converter/CmsCertificateMessageConverter.java b/src/main/java/eu/europa/ec/dgc/gateway/restapi/converter/CmsCertificateMessageConverter.java new file mode 100644 index 00000000..d8f79d7f --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/restapi/converter/CmsCertificateMessageConverter.java @@ -0,0 +1,97 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.restapi.converter; + +import eu.europa.ec.dgc.gateway.exception.DgcgResponseException; +import eu.europa.ec.dgc.gateway.restapi.dto.SignedCertificateDto; +import eu.europa.ec.dgc.signing.SignedCertificateMessageParser; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpInputMessage; +import org.springframework.http.HttpOutputMessage; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.converter.AbstractHttpMessageConverter; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.http.converter.HttpMessageNotWritableException; +import org.springframework.stereotype.Component; + +@Component +@Slf4j +public class CmsCertificateMessageConverter extends AbstractHttpMessageConverter { + + public static final MediaType CONTENT_TYPE_CMS = new MediaType("application", "cms"); + public static final String CONTENT_TYPE_CMS_VALUE = "application/cms"; + + public CmsCertificateMessageConverter() { + super(CONTENT_TYPE_CMS); + } + + @Override + protected boolean supports(Class clazz) { + return SignedCertificateDto.class.isAssignableFrom(clazz); + } + + @Override + protected SignedCertificateDto readInternal( + Class clazz, + HttpInputMessage inputMessage + ) throws IOException, HttpMessageNotReadableException { + + byte[] inputBytes = inputMessage.getBody().readAllBytes(); + SignedCertificateMessageParser certificateParser = new SignedCertificateMessageParser(inputBytes); + + switch (certificateParser.getParserState()) { + case FAILURE_INVALID_BASE64: + throw badRequest("Invalid Base64 CMS Message"); + case FAILURE_INVALID_CMS: + throw badRequest("Could not parse CMS Message"); + case FAILURE_INVALID_CMS_BODY: + throw badRequest("CMS Message needs to have binary data as body."); + case FAILURE_CMS_SIGNING_CERT_INVALID: + throw badRequest("CMS Message needs to contain exactly one X509 certificate"); + case FAILURE_CMS_SIGNER_INFO: + throw badRequest("CMS Message needs to have exactly 1 signer information."); + case FAILURE_CMS_BODY_PARSING_FAILED: + throw badRequest("CMS Message payload needs to be a DER encoded X509 certificate"); + default: + } + + return SignedCertificateDto.builder() + .payloadCertificate(certificateParser.getPayload()) + .signerCertificate(certificateParser.getSigningCertificate()) + .rawMessage(new String(inputBytes, StandardCharsets.UTF_8)) + .signature(certificateParser.getSignature()) + .verified(certificateParser.isSignatureVerified()) + .build(); + } + + @Override + protected void writeInternal(SignedCertificateDto signedCertificateDto, HttpOutputMessage outputMessage) + throws HttpMessageNotWritableException { + throw new HttpMessageNotWritableException("Outbound Usage of CMS Messages is currently not supported!"); + } + + private DgcgResponseException badRequest(String message) { + return new DgcgResponseException(HttpStatus.BAD_REQUEST, "0x402", message, "", ""); + } +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/restapi/converter/CmsStringMessageConverter.java b/src/main/java/eu/europa/ec/dgc/gateway/restapi/converter/CmsStringMessageConverter.java new file mode 100644 index 00000000..535695a4 --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/restapi/converter/CmsStringMessageConverter.java @@ -0,0 +1,99 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.restapi.converter; + +import eu.europa.ec.dgc.gateway.exception.DgcgResponseException; +import eu.europa.ec.dgc.gateway.restapi.dto.SignedStringDto; +import eu.europa.ec.dgc.signing.SignedStringMessageParser; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpInputMessage; +import org.springframework.http.HttpOutputMessage; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.converter.AbstractHttpMessageConverter; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.http.converter.HttpMessageNotWritableException; +import org.springframework.stereotype.Component; + +@Component +@Slf4j +public class CmsStringMessageConverter extends AbstractHttpMessageConverter { + + public static final MediaType CONTENT_TYPE_CMS_TEXT = new MediaType("application", "cms-text"); + public static final String CONTENT_TYPE_CMS_TEXT_VALUE = "application/cms-text"; + public static final MediaType CONTENT_TYPE_CMS = new MediaType("application", "cms"); + public static final String CONTENT_TYPE_CMS_VALUE = "application/cms"; + + public CmsStringMessageConverter() { + super(CONTENT_TYPE_CMS_TEXT, CONTENT_TYPE_CMS); + } + + @Override + protected boolean supports(Class clazz) { + return SignedStringDto.class.isAssignableFrom(clazz); + } + + @Override + protected SignedStringDto readInternal( + Class clazz, + HttpInputMessage inputMessage + ) throws IOException, HttpMessageNotReadableException { + + byte[] inputBytes = inputMessage.getBody().readAllBytes(); + SignedStringMessageParser parser = new SignedStringMessageParser(inputBytes); + + switch (parser.getParserState()) { + case FAILURE_INVALID_BASE64: + throw badRequest("Invalid Base64 CMS Message"); + case FAILURE_INVALID_CMS: + throw badRequest("Could not parse CMS Message"); + case FAILURE_INVALID_CMS_BODY: + throw badRequest("CMS Message needs to have binary data as body."); + case FAILURE_CMS_SIGNING_CERT_INVALID: + throw badRequest("CMS Message needs to contain exactly one X509 certificate"); + case FAILURE_CMS_SIGNER_INFO: + throw badRequest("CMS Message needs to have exactly 1 signer information."); + case FAILURE_CMS_BODY_PARSING_FAILED: + throw badRequest("CMS Message payload needs to be a String"); + default: + } + + return SignedStringDto.builder() + .payloadString(parser.getPayload()) + .signerCertificate(parser.getSigningCertificate()) + .rawMessage(new String(inputBytes, StandardCharsets.UTF_8)) + .signature(parser.getSignature()) + .verified(parser.isSignatureVerified()) + .build(); + } + + @Override + protected void writeInternal(SignedStringDto signedStringDto, HttpOutputMessage outputMessage) + throws HttpMessageNotWritableException { + throw new HttpMessageNotWritableException("Outbound Usage of CMS Messages is currently not supported!"); + } + + private DgcgResponseException badRequest(String message) { + return new DgcgResponseException(HttpStatus.BAD_REQUEST, "0x402", message, "", ""); + } +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/restapi/dto/CertificateTypeDto.java b/src/main/java/eu/europa/ec/dgc/gateway/restapi/dto/CertificateTypeDto.java new file mode 100644 index 00000000..67c662e6 --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/restapi/dto/CertificateTypeDto.java @@ -0,0 +1,28 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.restapi.dto; + +public enum CertificateTypeDto { + AUTHENTICATION, + UPLOAD, + CSCA, + DSC +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/restapi/dto/ProblemReportDto.java b/src/main/java/eu/europa/ec/dgc/gateway/restapi/dto/ProblemReportDto.java new file mode 100644 index 00000000..8bc46394 --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/restapi/dto/ProblemReportDto.java @@ -0,0 +1,46 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.restapi.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; + +@Schema( + name = "ProblemReport", + type = "object" +) +@Data +@AllArgsConstructor +public class ProblemReportDto { + + @Schema(example = "0x001") + private String code; + + @Schema(example = "Signer Certificate is unknown.") + private String problem; + + @Schema(example = "Certificate Thumbprint: 2342424f24c242f42f4b24...") + private String sendValue; + + @Schema(example = "Use a known upload certificate to upload signer information.") + private String details; +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/restapi/dto/SignedCertificateDto.java b/src/main/java/eu/europa/ec/dgc/gateway/restapi/dto/SignedCertificateDto.java new file mode 100644 index 00000000..114d339a --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/restapi/dto/SignedCertificateDto.java @@ -0,0 +1,60 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.restapi.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import org.bouncycastle.cert.X509CertificateHolder; + +@Schema( + name = "Signed Certificate (CMS)", + type = "string", + example = "MIICyDCCAbCgAwIBAgIGAXR3DZUUMA0GCSqGSIb3DQEBBQUAMBwxCzAJBgNVBAYT" + + "AkRFMQ0wCwYDVQQDDARkZW1vMB4XDTIwMDgyNzA4MDY1MloXDTIxMDkxMDA4MDY1" + + "MlowHDELMAkGA1UEBhMCREUxDTALBgNVBAMMBGRlbW8wggEiMA0GCSqGSIb3DQEB" + + "AQUAA4IBDwAwggEKAoIBAQCKR0TEJOO4z0ks4OMAovcyxuPpeZuR1JykNNFd3OR+" + + "vFWJLJtDYgRjtuqSuKCghLa/ci+0yIs3OeitGtajqFIukYksvX2LxOZDYDUbnpGQ" + + "DPNMVmpEavDBbvKON8C8K036pC41bNvwkTrfUyZ8iE+hV2+kj1SHUyw7jweEUoiw" + + "NmMiaXXPiMIOj7D0qnmM+iTGN9g/DrJ/IvvsgiGpK3QlQ5pnHs2BvzrSw4LFAZ8c" + + "SQfWKheZVHfQf26mJFdEzowrzfzForDdeFAPIIirhufE3jWFxj1thfztu+VSMj84" + + "sDqodEt2VJOY+DvLB1Ls/26LSmFtMnCEuBAhkbQ1E0tbAgMBAAGjEDAOMAwGA1Ud" + + "EwEB/wQCMAAwDQYJKoZIhvcNAQEFBQADggEBABaMEQz4Gbj+G0SZGZaIDoUFDB6n" + + "1R6iUS0zTBgsV8pSpFhwPryRiLdeNzIzsDdQ1ack1NfQ6YPn3/yOJ/SvnXs6n+vO" + + "WQW2KsuiymPSd/wjeywRRMfCysHjrmE+m+8lrFDrKuPnrACwQIsX9PDEsRRBnpSy" + + "5NKUZn6u3iPV9x6rwYCdCa/8VDGLqVb3eEE5dbFaYG9uW02cSbmsiZm8KmW8b6BF" + + "eIwHVRAH6Cs1VZI8UIrdVGCE111tUo/0957rF+/doFyJcwX+4ESH0m2MsHFjXDfG" + + "U8yTjiUh/b2Erk4TCmrJpux30QRhsNZwkmEYSbRv+vp5/obgH1mL5ouoV5I=" +) +@Data +@Builder +@AllArgsConstructor +public class SignedCertificateDto { + + private final X509CertificateHolder payloadCertificate; + private final X509CertificateHolder signerCertificate; + private final String rawMessage; + private final String signature; + + private final boolean verified; + +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/restapi/dto/SignedStringDto.java b/src/main/java/eu/europa/ec/dgc/gateway/restapi/dto/SignedStringDto.java new file mode 100644 index 00000000..38aa07da --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/restapi/dto/SignedStringDto.java @@ -0,0 +1,60 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.restapi.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import org.bouncycastle.cert.X509CertificateHolder; + +@Schema( + name = "Signed String (CMS)", + type = "string", + example = "MIICyDCCAbCgAwIBAgIGAXR3DZUUMA0GCSqGSIb3DQEBBQUAMBwxCzAJBgNVBAYT" + + "AkRFMQ0wCwYDVQQDDARkZW1vMB4XDTIwMDgyNzA4MDY1MloXDTIxMDkxMDA4MDY1" + + "MlowHDELMAkGA1UEBhMCREUxDTALBgNVBAMMBGRlbW8wggEiMA0GCSqGSIb3DQEB" + + "AQUAA4IBDwAwggEKAoIBAQCKR0TEJOO4z0ks4OMAovcyxuPpeZuR1JykNNFd3OR+" + + "vFWJLJtDYgRjtuqSuKCghLa/ci+0yIs3OeitGtajqFIukYksvX2LxOZDYDUbnpGQ" + + "DPNMVmpEavDBbvKON8C8K036pC41bNvwkTrfUyZ8iE+hV2+kj1SHUyw7jweEUoiw" + + "NmMiaXXPiMIOj7D0qnmM+iTGN9g/DrJ/IvvsgiGpK3QlQ5pnHs2BvzrSw4LFAZ8c" + + "SQfWKheZVHfQf26mJFdEzowrzfzForDdeFAPIIirhufE3jWFxj1thfztu+VSMj84" + + "sDqodEt2VJOY+DvLB1Ls/26LSmFtMnCEuBAhkbQ1E0tbAgMBAAGjEDAOMAwGA1Ud" + + "EwEB/wQCMAAwDQYJKoZIhvcNAQEFBQADggEBABaMEQz4Gbj+G0SZGZaIDoUFDB6n" + + "1R6iUS0zTBgsV8pSpFhwPryRiLdeNzIzsDdQ1ack1NfQ6YPn3/yOJ/SvnXs6n+vO" + + "WQW2KsuiymPSd/wjeywRRMfCysHjrmE+m+8lrFDrKuPnrACwQIsX9PDEsRRBnpSy" + + "5NKUZn6u3iPV9x6rwYCdCa/8VDGLqVb3eEE5dbFaYG9uW02cSbmsiZm8KmW8b6BF" + + "eIwHVRAH6Cs1VZI8UIrdVGCE111tUo/0957rF+/doFyJcwX+4ESH0m2MsHFjXDfG" + + "U8yTjiUh/b2Erk4TCmrJpux30QRhsNZwkmEYSbRv+vp5/obgH1mL5ouoV5I=" +) +@Data +@Builder +@AllArgsConstructor +public class SignedStringDto { + + private final String payloadString; + private final X509CertificateHolder signerCertificate; + private final String rawMessage; + private final String signature; + + private final boolean verified; + +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/restapi/dto/TrustListDto.java b/src/main/java/eu/europa/ec/dgc/gateway/restapi/dto/TrustListDto.java new file mode 100644 index 00000000..d1c12619 --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/restapi/dto/TrustListDto.java @@ -0,0 +1,59 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.restapi.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.ZonedDateTime; +import lombok.Getter; +import lombok.Setter; + +@Schema( + name = "TrustList", + type = "object" +) +@Getter +@Setter +public class TrustListDto { + + @Schema(example = "qroU+hDDovs=") + private String kid; + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ssXXX") + private ZonedDateTime timestamp; + + @Schema(example = "EU") + private String country; + + private CertificateTypeDto certificateType; + + @Schema(example = "aaba14fa10c3a2fb441a28af0ec1bb4128153b9ddc796b66bfa04b02ea3e103e") + private String thumbprint; + + @Schema(example = "o53CbAa77LyIMFc5Gz+B2Jc275Gdg/SdLayw7gx0GrTcinR95zfTLr8nNHgJMYlX3rD8Y11zB/Osyt0 ..." + + " W+VIrYRGSEmgjGy2EwzvA5nVhsaA+/udnmbyQw9LjAOQ==") + private String signature; + + @Schema(example = "MIICyDCCAbCgAwIBAgIGAXR3DZUUMA0GCSqGSIb3DQEBBQUAMBwxCzAJB ..." + + " Jpux30QRhsNZwkmEYSbRv+vp5/obgH1mL5ouoV5I=") + private String rawData; + +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/restapi/dto/ValidationRuleDto.java b/src/main/java/eu/europa/ec/dgc/gateway/restapi/dto/ValidationRuleDto.java new file mode 100644 index 00000000..61e467f6 --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/restapi/dto/ValidationRuleDto.java @@ -0,0 +1,48 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.restapi.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.ZonedDateTime; +import lombok.Getter; +import lombok.Setter; + +@Schema(name = "ValidationRule") +@Setter +@Getter +public class ValidationRuleDto { + + @Schema(description = "Version of the Rule (Semver)", example = "1.0.0") + String version; + + @Schema(description = "Rule is valid from") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ssXXX") + ZonedDateTime validFrom; + + @Schema(description = "Rule is valid to") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ssXXX") + ZonedDateTime validTo; + + @Schema(description = "CMS containing the signed JSON Object with the rule itself") + String cms; + +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/restapi/filter/CertificateAuthenticationFilter.java b/src/main/java/eu/europa/ec/dgc/gateway/restapi/filter/CertificateAuthenticationFilter.java new file mode 100644 index 00000000..7d780f0b --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/restapi/filter/CertificateAuthenticationFilter.java @@ -0,0 +1,202 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.restapi.filter; + +import eu.europa.ec.dgc.gateway.config.DgcConfigProperties; +import eu.europa.ec.dgc.gateway.entity.TrustedPartyEntity; +import eu.europa.ec.dgc.gateway.exception.DgcgResponseException; +import eu.europa.ec.dgc.gateway.service.TrustedPartyService; +import eu.europa.ec.dgc.gateway.utils.DgcMdc; +import java.io.IOException; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Base64; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.bouncycastle.util.encoders.DecoderException; +import org.bouncycastle.util.encoders.Hex; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerExceptionResolver; +import org.springframework.web.servlet.HandlerExecutionChain; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; + +@Slf4j +@Component +@AllArgsConstructor +public class CertificateAuthenticationFilter extends OncePerRequestFilter { + + public static final String REQUEST_PROP_COUNTRY = "reqPropCountry"; + public static final String REQUEST_PROP_THUMBPRINT = "reqPropCertThumbprint"; + + private final RequestMappingHandlerMapping requestMap; + + private final DgcConfigProperties properties; + + private final TrustedPartyService trustedPartyService; + + private final HandlerExceptionResolver handlerExceptionResolver; + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) { + try { + HandlerExecutionChain handlerExecutionChain = requestMap.getHandler(request); + + if (handlerExecutionChain == null) { + return true; + } else { + return !((HandlerMethod) handlerExecutionChain.getHandler()).getMethod() + .isAnnotationPresent(CertificateAuthenticationRequired.class); + } + } catch (Exception e) { + handlerExceptionResolver.resolveException(request, null, null, e); + return true; + } + } + + private String normalizeCertificateHash(String inputString) { + if (inputString == null) { + return null; + } + + boolean isHexString; + // check if it is a hex string + try { + Hex.decode(inputString); + isHexString = true; + } catch (DecoderException ignored) { + isHexString = false; + } + + // We can assume that the given string is hex encoded SHA-256 hash when length is 64 and string is hex encoded + if (inputString.length() == 64 && isHexString) { + return inputString; + } else { + try { + String hexString; + if (inputString.contains("%")) { // only url decode input string if it contains none base64 characters + inputString = URLDecoder.decode(inputString, StandardCharsets.UTF_8); + } + hexString = Hex.toHexString(Base64.getDecoder().decode(inputString)); + return hexString; + } catch (IllegalArgumentException ignored) { + log.error("Could not normalize certificate hash."); + return null; + } + } + } + + @Override + protected void doFilterInternal( + HttpServletRequest httpServletRequest, + HttpServletResponse httpServletResponse, + FilterChain filterChain + ) throws ServletException, IOException { + logger.debug("Checking request for auth headers"); + + String headerDistinguishedName = + httpServletRequest.getHeader(properties.getCertAuth().getHeaderFields().getDistinguishedName()); + + String headerCertThumbprint = normalizeCertificateHash( + httpServletRequest.getHeader(properties.getCertAuth().getHeaderFields().getThumbprint())); + + if (headerDistinguishedName == null || headerCertThumbprint == null) { + log.error("No thumbprint or distinguish name"); + handlerExceptionResolver.resolveException( + httpServletRequest, + httpServletResponse, + null, + new DgcgResponseException( + HttpStatus.UNAUTHORIZED, + "0x400", + "No thumbprint or distinguish name", + "", "")); + return; + } + + headerDistinguishedName = URLDecoder.decode(headerDistinguishedName, StandardCharsets.UTF_8); + + DgcMdc.put("dnString", headerDistinguishedName); + DgcMdc.put("thumbprint", headerCertThumbprint); + + Map distinguishNameMap = parseDistinguishNameString(headerDistinguishedName); + + if (!distinguishNameMap.containsKey("C")) { + log.error("Country property is missing"); + handlerExceptionResolver.resolveException( + httpServletRequest, httpServletResponse, null, + new DgcgResponseException( + HttpStatus.BAD_REQUEST, + "0x401", + "Client Certificate must contain country property", + headerDistinguishedName, "")); + return; + } + + Optional certFromDb = trustedPartyService.getCertificate( + headerCertThumbprint, + distinguishNameMap.get("C"), + TrustedPartyEntity.CertificateType.AUTHENTICATION + ); + + if (certFromDb.isEmpty()) { + log.error("Unknown client certificate"); + handlerExceptionResolver.resolveException( + httpServletRequest, httpServletResponse, null, + new DgcgResponseException( + HttpStatus.UNAUTHORIZED, + "0x402", + "Client is not authorized to access the service", + "", "")); + + return; + } + + log.info("Successful Authentication"); + httpServletRequest.setAttribute(REQUEST_PROP_COUNTRY, distinguishNameMap.get("C")); + httpServletRequest.setAttribute(REQUEST_PROP_THUMBPRINT, headerCertThumbprint); + + filterChain.doFilter(httpServletRequest, httpServletResponse); + } + + /** + * Parses a given Distinguish Name string (e.g. "C=DE,OU=Test Unit,O=Test Company"). + * + * @param dnString the DN string to parse. + * @return Map with properties of the DN string. + */ + private Map parseDistinguishNameString(String dnString) { + return Arrays.stream(dnString.split(",")) + .map(part -> part.split("=")) + .filter(entry -> entry.length == 2) + .collect(Collectors.toMap(arr -> arr[0].toUpperCase().trim(), arr -> arr[1].trim(), (s, s2) -> s)); + } +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/restapi/filter/CertificateAuthenticationRequired.java b/src/main/java/eu/europa/ec/dgc/gateway/restapi/filter/CertificateAuthenticationRequired.java new file mode 100644 index 00000000..5ddc1a20 --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/restapi/filter/CertificateAuthenticationRequired.java @@ -0,0 +1,31 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.restapi.filter; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface CertificateAuthenticationRequired { +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/restapi/mapper/GwTrustListMapper.java b/src/main/java/eu/europa/ec/dgc/gateway/restapi/mapper/GwTrustListMapper.java new file mode 100644 index 00000000..e016da0c --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/restapi/mapper/GwTrustListMapper.java @@ -0,0 +1,38 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.restapi.mapper; + +import eu.europa.ec.dgc.gateway.model.TrustList; +import eu.europa.ec.dgc.gateway.model.TrustListType; +import eu.europa.ec.dgc.gateway.restapi.dto.CertificateTypeDto; +import eu.europa.ec.dgc.gateway.restapi.dto.TrustListDto; +import java.util.List; +import org.mapstruct.Mapper; + +@Mapper(componentModel = "spring") +public interface GwTrustListMapper { + + TrustListDto trustListToTrustListDto(TrustList trustList); + + List trustListToTrustListDto(List trustList); + + TrustListType certificateTypeDtoToTrustListType(CertificateTypeDto certificateTypeDto); +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/restapi/mapper/GwValidationRuleMapper.java b/src/main/java/eu/europa/ec/dgc/gateway/restapi/mapper/GwValidationRuleMapper.java new file mode 100644 index 00000000..983d9e87 --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/restapi/mapper/GwValidationRuleMapper.java @@ -0,0 +1,32 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.restapi.mapper; + +import eu.europa.ec.dgc.gateway.entity.ValidationRuleEntity; +import eu.europa.ec.dgc.gateway.restapi.dto.ValidationRuleDto; +import org.mapstruct.Mapper; + +@Mapper(componentModel = "spring") +public interface GwValidationRuleMapper { + + ValidationRuleDto entityToDto(ValidationRuleEntity entity); + +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/service/AuditService.java b/src/main/java/eu/europa/ec/dgc/gateway/service/AuditService.java new file mode 100644 index 00000000..b292b1d7 --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/service/AuditService.java @@ -0,0 +1,89 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.service; + +import eu.europa.ec.dgc.gateway.entity.AuditEventEntity; +import eu.europa.ec.dgc.gateway.repository.AuditEventRepository; +import eu.europa.ec.dgc.gateway.utils.DgcMdc; +import eu.europa.ec.dgc.utils.CertificateUtils; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.bouncycastle.cert.X509CertificateHolder; +import org.springframework.stereotype.Service; + +@Service +@Slf4j +@RequiredArgsConstructor +public class AuditService { + + private final AuditEventRepository auditEventRepository; + + private final CertificateUtils certificateUtils; + + private static final String MDC_PROP_AUDIT_ID = "auditId"; + private static final String MDC_PROP_AUDIT_COUNTRY = "country"; + + /** + * Method to create an audit Event. + * + * @param countryCode 2-digit country Code + * @param uploaderCertificate the uploader cert + * @param authenticationSha256Fingerprint fingerprint of the authentication cert + * @param auditEvent Event ID + * @param auditEventDescription EventDescription + */ + public void addAuditEvent(String countryCode, X509CertificateHolder uploaderCertificate, + String authenticationSha256Fingerprint, + String auditEvent, String auditEventDescription) { + addAuditEvent( + countryCode, + certificateUtils.getCertThumbprint(uploaderCertificate), + authenticationSha256Fingerprint, + auditEvent, + auditEventDescription + ); + } + + /** + * Method to create an audit Event. + * + * @param countryCode 2-digit country Code + * @param uploaderSha256Fingerprint fingerprint of the uploader cert + * @param authenticationSha256Fingerprint fingerprint of the authentication cert + * @param auditEvent Event ID + * @param auditEventDescription EventDescription + */ + public void addAuditEvent(String countryCode, String uploaderSha256Fingerprint, + String authenticationSha256Fingerprint, String auditEvent, String auditEventDescription) { + AuditEventEntity auditEventEntity = new AuditEventEntity(); + auditEventEntity.setEvent(auditEvent); + auditEventEntity.setDescription(auditEventDescription); + auditEventEntity.setCountry(countryCode); + auditEventEntity.setAuthenticationSha256Fingerprint(authenticationSha256Fingerprint); + auditEventEntity.setUploaderSha256Fingerprint(uploaderSha256Fingerprint); + log.debug("Created AuditEvent with ID {} for Country {} with uploader {} authentication{}", auditEvent, + countryCode, uploaderSha256Fingerprint, authenticationSha256Fingerprint); + DgcMdc.put(MDC_PROP_AUDIT_COUNTRY, countryCode); + DgcMdc.put(MDC_PROP_AUDIT_ID, auditEvent); + log.info("Created AuditEvent"); + auditEventRepository.save(auditEventEntity); + } +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/service/RatValuesetUpdateService.java b/src/main/java/eu/europa/ec/dgc/gateway/service/RatValuesetUpdateService.java new file mode 100644 index 00000000..14dc35fe --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/service/RatValuesetUpdateService.java @@ -0,0 +1,157 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import eu.europa.ec.dgc.gateway.client.JrcClient; +import eu.europa.ec.dgc.gateway.model.JrcRatValueset; +import eu.europa.ec.dgc.gateway.model.JrcRatValuesetResponse; +import eu.europa.ec.dgc.gateway.model.RatValueset; +import eu.europa.ec.dgc.gateway.model.Valueset; +import eu.europa.ec.dgc.gateway.utils.DgcMdc; +import feign.FeignException; +import java.time.LocalDate; +import java.time.ZonedDateTime; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Optional; +import javax.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import net.javacrumbs.shedlock.spring.annotation.SchedulerLock; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +@Slf4j +public class RatValuesetUpdateService { + + private static final String RAT_VALUESET_ID = "covid-19-lab-test-manufacturer-and-name"; + + private static final TypeReference> typeReference = new TypeReference<>() { + }; + + private final ValuesetService valuesetService; + + private final ObjectMapper objectMapper; + + private final JrcClient jrcClient; + + /** + * Setup ObjectMapper to keep Timezone when deserializing. + */ + @PostConstruct + public void setup() { + objectMapper.disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE); + } + + /** + * Updates the ValueSet for Rapid Antigen Tests. + */ + @Scheduled(fixedDelayString = "${dgc.jrc.interval:21600000}") + @SchedulerLock(name = "rat_valueset_update") + public void update() { + log.info("Starting RAT Valueset update"); + + Optional valueSetJson = valuesetService.getValueSetById(RAT_VALUESET_ID); + Valueset parsedValueset = + new Valueset<>(RAT_VALUESET_ID, LocalDate.now(), new HashMap<>()); + + if (valueSetJson.isPresent()) { + try { + parsedValueset = objectMapper.readValue(valueSetJson.get(), typeReference); + } catch (JsonProcessingException e) { + log.error("Could not parse RatValueSet", e); + } + } + + JrcRatValuesetResponse jrcResponse; + try { + jrcResponse = jrcClient.downloadRatValues(); + } catch (FeignException e) { + log.error("Failed to download RatValueset from JRC", e); + return; + } + + for (JrcRatValueset device : jrcResponse.getDeviceList()) { + JrcRatValueset.HscListHistory latestHistoryEntryNotInFuture = null; + JrcRatValueset.HscListHistory latestHistoryEntry = null; + long now = ZonedDateTime.now().toEpochSecond(); + + if (device.getHscListHistory() != null) { + + latestHistoryEntryNotInFuture = device.getHscListHistory().stream() + .sorted(Comparator + .comparing((JrcRatValueset.HscListHistory x) -> x.getListDate().toEpochSecond()) + .reversed()) + .dropWhile(x -> x.getListDate().toEpochSecond() > now) + .findFirst() + .orElse(null); + + latestHistoryEntry = device.getHscListHistory().stream() + .max(Comparator.comparing(x -> x.getListDate().toEpochSecond())) + .orElse(null); + } + + if (latestHistoryEntry == null) { + DgcMdc.put("valuesetId", device.getIdDevice()); + log.error("Valueset Entry has no history information. Skipping entry."); + DgcMdc.remove("valuesetId"); + } else { + RatValueset valuesetInDb = + parsedValueset.getValue().computeIfAbsent(device.getIdDevice(), s -> new RatValueset()); + + valuesetInDb.setDisplay( + String.format("%s, %s", device.getManufacturer().getName(), device.getCommercialName())); + + if (latestHistoryEntryNotInFuture != null) { + valuesetInDb.setActive(latestHistoryEntryNotInFuture.getInCommonList()); + valuesetInDb.setVersion(latestHistoryEntryNotInFuture.getListDate()); + } else { + valuesetInDb.setActive(null); + valuesetInDb.setVersion(null); + } + + if (latestHistoryEntry.getListDate().toEpochSecond() < now) { + valuesetInDb.setValidUntil(null); + } else { + valuesetInDb.setValidUntil(latestHistoryEntry.getListDate()); + } + } + } + + parsedValueset.setDate(LocalDate.now()); + String updatedValuesetJson; + try { + updatedValuesetJson = objectMapper.writeValueAsString(parsedValueset); + } catch (JsonProcessingException e) { + log.error("Failed to write updated RAT Valueset as String", e); + return; + } + + valuesetService.updateValueSet(RAT_VALUESET_ID, updatedValuesetJson); + log.info("Updating RAT Valueset finished."); + } +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/service/SignerInformationService.java b/src/main/java/eu/europa/ec/dgc/gateway/service/SignerInformationService.java new file mode 100644 index 00000000..bb5f99ab --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/service/SignerInformationService.java @@ -0,0 +1,328 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.service; + +import eu.europa.ec.dgc.gateway.entity.SignerInformationEntity; +import eu.europa.ec.dgc.gateway.entity.TrustedPartyEntity; +import eu.europa.ec.dgc.gateway.repository.SignerInformationRepository; +import eu.europa.ec.dgc.gateway.utils.DgcMdc; +import eu.europa.ec.dgc.utils.CertificateUtils; +import java.io.IOException; +import java.security.cert.X509Certificate; +import java.util.Base64; +import java.util.List; +import java.util.Optional; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.bouncycastle.asn1.x500.RDN; +import org.bouncycastle.asn1.x509.X509ObjectIdentifiers; +import org.bouncycastle.cert.CertException; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.operator.ContentVerifierProvider; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.RuntimeOperatorException; +import org.bouncycastle.operator.jcajce.JcaContentVerifierProviderBuilder; +import org.springframework.stereotype.Service; + +@Service +@Slf4j +@RequiredArgsConstructor +public class SignerInformationService { + + private final TrustedPartyService trustedPartyService; + + private final CertificateUtils certificateUtils; + + private final SignerInformationRepository signerInformationRepository; + + private static final String MDC_PROP_UPLOAD_CERT_THUMBPRINT = "uploadCertThumbprint"; + private static final String MDC_PROP_CSCA_CERT_THUMBPRINT = "cscaCertThumbprint"; + + /** + * Method to query persistence layer for all stored SignerInformation. + * + * @return List of SignerInformation + */ + public List getSignerInformation() { + return signerInformationRepository.findAll(); + } + + /** + * Method to query persistence layer for SignerInformation filtered by Type. + * + * @param type type to filter for + * @return List of SignerInformation + */ + public List getSignerInformation(SignerInformationEntity.CertificateType type) { + return signerInformationRepository.getByCertificateType(type); + } + + /** + * Method to query persistence layer for SignerInformation filtered by Type and Country. + * + * @param countryCode 2-digit country Code to filter for. + * @param type type to filter for + * @return List of SignerInformation + */ + public List getSignerInformation( + String countryCode, + SignerInformationEntity.CertificateType type) { + return signerInformationRepository.getByCertificateTypeAndCountry(type, countryCode); + } + + /** + * Adds a new Trusted Signer Certificate to TrustStore DB. + * + * @param uploadedCertificate the certificate to add + * @param signerCertificate the certificate which was used to sign the message + * @param signature the detached signature of cms message + * @param authenticatedCountryCode the country code of the uploader country from cert authentication + * @throws SignerCertCheckException if validation check has failed. The exception contains + * a reason property with detailed information why the validation has failed. + */ + public SignerInformationEntity addSignerCertificate( + X509CertificateHolder uploadedCertificate, + X509CertificateHolder signerCertificate, + String signature, + String authenticatedCountryCode + ) throws SignerCertCheckException { + + contentCheckUploaderCertificate(signerCertificate, authenticatedCountryCode); + contentCheckCountryOfOrigin(uploadedCertificate, authenticatedCountryCode); + contentCheckCsca(uploadedCertificate, authenticatedCountryCode); + contentCheckAlreadyExists(uploadedCertificate); + contentCheckKidAlreadyExists(uploadedCertificate); + + // All checks passed --> Save to DB + byte[] certRawData; + try { + certRawData = uploadedCertificate.getEncoded(); + } catch (IOException e) { + throw new SignerCertCheckException(SignerCertCheckException.Reason.UPLOAD_FAILED, "Internal Server Error"); + } + + SignerInformationEntity newSignerInformation = new SignerInformationEntity(); + newSignerInformation.setCountry(authenticatedCountryCode); + newSignerInformation.setRawData(Base64.getEncoder().encodeToString(certRawData)); + newSignerInformation.setThumbprint(certificateUtils.getCertThumbprint(uploadedCertificate)); + newSignerInformation.setCertificateType(SignerInformationEntity.CertificateType.DSC); + newSignerInformation.setSignature(signature); + + log.info("Saving new SignerInformation Entity"); + + newSignerInformation = signerInformationRepository.save(newSignerInformation); + + DgcMdc.remove(MDC_PROP_UPLOAD_CERT_THUMBPRINT); + DgcMdc.remove(MDC_PROP_CSCA_CERT_THUMBPRINT); + + return newSignerInformation; + } + + /** + * Deletes a Trusted Signer Certificate from TrustStore DB. + * + * @param uploadedCertificate the certificate to delete + * @param signerCertificate the certificate which was used to sign the message + * @param authenticatedCountryCode the country code of the uploader country from cert authentication + * @throws SignerCertCheckException if validation check has failed. The exception contains + * a reason property with detailed information why the validation has failed. + */ + public void deleteSignerCertificate( + X509CertificateHolder uploadedCertificate, + X509CertificateHolder signerCertificate, + String authenticatedCountryCode + ) throws SignerCertCheckException { + + contentCheckUploaderCertificate(signerCertificate, authenticatedCountryCode); + contentCheckCountryOfOrigin(uploadedCertificate, authenticatedCountryCode); + contentCheckExists(uploadedCertificate); + + log.info("Revoking SignerInformation Entity"); + + // All checks passed --> Delete from DB + signerInformationRepository.deleteByThumbprint(certificateUtils.getCertThumbprint(uploadedCertificate)); + + DgcMdc.remove(MDC_PROP_UPLOAD_CERT_THUMBPRINT); + } + + private void contentCheckUploaderCertificate( + X509CertificateHolder signerCertificate, + String authenticatedCountryCode) throws SignerCertCheckException { + // Content Check Step 1: Uploader Certificate + String signerCertThumbprint = certificateUtils.getCertThumbprint(signerCertificate); + Optional certFromDb = trustedPartyService.getCertificate( + signerCertThumbprint, + authenticatedCountryCode, + TrustedPartyEntity.CertificateType.UPLOAD + ); + + if (certFromDb.isEmpty()) { + throw new SignerCertCheckException(SignerCertCheckException.Reason.UPLOADER_CERT_CHECK_FAILED, + "Could not find upload certificate with hash %s and country %s", + signerCertThumbprint, authenticatedCountryCode); + } + + DgcMdc.put(MDC_PROP_UPLOAD_CERT_THUMBPRINT, signerCertThumbprint); + } + + private void contentCheckCountryOfOrigin(X509CertificateHolder uploadedCertificate, + String authenticatedCountryCode) throws SignerCertCheckException { + + // Content Check Step 2: Country of Origin check + RDN[] uploadedCertCountryProperties = + uploadedCertificate.getSubject().getRDNs(X509ObjectIdentifiers.countryName); + + if (uploadedCertCountryProperties.length > 1) { + throw new SignerCertCheckException(SignerCertCheckException.Reason.COUNTRY_OF_ORIGIN_CHECK_FAILED, + "Uploaded certificate contains more than one country property."); + } else if (uploadedCertCountryProperties.length < 1) { + throw new SignerCertCheckException(SignerCertCheckException.Reason.COUNTRY_OF_ORIGIN_CHECK_FAILED, + "Uploaded certificate contains no country property."); + } + + if (!uploadedCertCountryProperties[0].getFirst().getValue().toString().equals(authenticatedCountryCode)) { + throw new SignerCertCheckException(SignerCertCheckException.Reason.COUNTRY_OF_ORIGIN_CHECK_FAILED, + "Uploaded certificate is not issued for uploader country."); + } + } + + private void contentCheckCsca(X509CertificateHolder uploadedCertificate, + String authenticatedCountryCode) throws SignerCertCheckException { + + // Content Check Step 3: CSCA Check + List trustedCas = + trustedPartyService.getCertificates(authenticatedCountryCode, TrustedPartyEntity.CertificateType.CSCA); + + if (trustedCas.isEmpty()) { + throw new SignerCertCheckException(SignerCertCheckException.Reason.CSCA_CHECK_FAILED, + "CSCA list for country %s is empty", authenticatedCountryCode); + } + + Optional matchingCa = trustedCas.stream() + .dropWhile(ca -> !certificateSignedByCa(uploadedCertificate, ca)) + .findFirst(); + + if (matchingCa.isEmpty()) { + throw new SignerCertCheckException(SignerCertCheckException.Reason.CSCA_CHECK_FAILED, + "Could not verify uploaded certificate was signed by valid CSCA."); + } else { + DgcMdc.put(MDC_PROP_CSCA_CERT_THUMBPRINT, matchingCa.get().getThumbprint()); + } + } + + private void contentCheckAlreadyExists(X509CertificateHolder uploadedCertificate) throws SignerCertCheckException { + + String uploadedCertificateThumbprint = certificateUtils.getCertThumbprint(uploadedCertificate); + Optional signerInformationEntity = + signerInformationRepository.getFirstByThumbprint(uploadedCertificateThumbprint); + + if (signerInformationEntity.isPresent()) { + throw new SignerCertCheckException(SignerCertCheckException.Reason.ALREADY_EXIST_CHECK_FAILED, + "Uploaded certificate already exists"); + } + } + + private void contentCheckKidAlreadyExists(X509CertificateHolder uploadedCertificate) + throws SignerCertCheckException { + + String uploadedCertificateThumbprint = certificateUtils.getCertThumbprint(uploadedCertificate); + // KID is the first 8 byte of hash. So we take the first 16 characters of the hash + String thumbprintKidPart = uploadedCertificateThumbprint.substring(0, 16); + + Optional signerInformationEntity = + signerInformationRepository.getFirstByThumbprintStartsWith(thumbprintKidPart); + + if (signerInformationEntity.isPresent()) { + throw new SignerCertCheckException(SignerCertCheckException.Reason.KID_CHECK_FAILED, + "A certificate with KID of uploaded certificate already exists"); + } + } + + private void contentCheckExists(X509CertificateHolder uploadedCertificate) throws SignerCertCheckException { + + String uploadedCertificateThumbprint = certificateUtils.getCertThumbprint(uploadedCertificate); + Optional signerInformationEntity = + signerInformationRepository.getFirstByThumbprint(uploadedCertificateThumbprint); + + if (signerInformationEntity.isEmpty()) { + throw new SignerCertCheckException(SignerCertCheckException.Reason.EXIST_CHECK_FAILED, + "Uploaded certificate does not exists"); + } + } + + private boolean certificateSignedByCa(X509CertificateHolder certificate, TrustedPartyEntity caCertificateEntity) { + X509Certificate caCertificate = trustedPartyService.getX509CertificateFromEntity(caCertificateEntity); + + ContentVerifierProvider verifier; + try { + verifier = new JcaContentVerifierProviderBuilder().build(caCertificate); + } catch (OperatorCreationException e) { + DgcMdc.put("certHash", caCertificateEntity.getThumbprint()); + log.error("Failed to instantiate JcaContentVerifierProvider from cert"); + return false; + } + + try { + return certificate.isSignatureValid(verifier); + } catch (CertException | RuntimeOperatorException e) { + return false; + } + } + + /** + * Extracts X509Certificate from {@link SignerInformationEntity}. + * + * @param signerInformationEntity entity from which the certificate should be extracted. + * @return X509Certificate representation. + */ + public X509Certificate getX509CertificateFromEntity(SignerInformationEntity signerInformationEntity) { + try { + byte[] rawDataBytes = Base64.getDecoder().decode(signerInformationEntity.getRawData()); + return certificateUtils.convertCertificate(new X509CertificateHolder(rawDataBytes)); + } catch (Exception e) { + log.error("Failed to parse Certificate from SignerInformationEntity", e); + } + + return null; + } + + public static class SignerCertCheckException extends Exception { + + @Getter + private final Reason reason; + + public SignerCertCheckException(Reason reason, String message, Object... args) { + super(String.format(message, args)); + this.reason = reason; + } + + public enum Reason { + UPLOADER_CERT_CHECK_FAILED, + COUNTRY_OF_ORIGIN_CHECK_FAILED, + CSCA_CHECK_FAILED, + ALREADY_EXIST_CHECK_FAILED, + KID_CHECK_FAILED, + EXIST_CHECK_FAILED, + UPLOAD_FAILED + } + } +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/service/TrustListService.java b/src/main/java/eu/europa/ec/dgc/gateway/service/TrustListService.java new file mode 100644 index 00000000..0c40791e --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/service/TrustListService.java @@ -0,0 +1,145 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.service; + +import eu.europa.ec.dgc.gateway.entity.SignerInformationEntity; +import eu.europa.ec.dgc.gateway.entity.TrustedPartyEntity; +import eu.europa.ec.dgc.gateway.model.TrustList; +import eu.europa.ec.dgc.gateway.model.TrustListType; +import eu.europa.ec.dgc.utils.CertificateUtils; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +@RequiredArgsConstructor +public class TrustListService { + + private final TrustedPartyService trustedPartyService; + + private final SignerInformationService signerInformationService; + + private final CertificateUtils certificateUtils; + + /** + * Get a TrustList with TrustList Entries of all type and all country. + * + * @return List of {@link TrustList} ordered by KID + */ + public List getTrustList() { + return mergeAndConvert( + trustedPartyService.getCertificates(), + signerInformationService.getSignerInformation() + ); + } + + /** + * Get a TrustList with TrustList Entries of all countries filtered by type. + * + * @param type the type to filter for. + * @return List of {@link TrustList} ordered by KID + */ + public List getTrustList(TrustListType type) { + if (type == TrustListType.DSC) { + return mergeAndConvert( + Collections.emptyList(), + signerInformationService.getSignerInformation(SignerInformationEntity.CertificateType.DSC) + ); + } else { + return mergeAndConvert( + trustedPartyService.getCertificates(map(type)), + Collections.emptyList() + ); + } + } + + /** + * Get a TrustList with TrustList Entries filtered by countriy and type. + * + * @param type the type to filter for. + * @param countryCode the 2-Digit country code to filter for. + * @return List of {@link TrustList} ordered by KID + */ + public List getTrustList(TrustListType type, String countryCode) { + if (type == TrustListType.DSC) { + return mergeAndConvert( + Collections.emptyList(), + signerInformationService.getSignerInformation(countryCode, SignerInformationEntity.CertificateType.DSC) + ); + } else { + return mergeAndConvert( + trustedPartyService.getCertificates(countryCode, map(type)), + Collections.emptyList() + ); + } + } + + private List mergeAndConvert( + List trustedPartyList, + List signerInformationList) { + + return Stream.concat( + trustedPartyList.stream().map(this::convert), + signerInformationList.stream().map(this::convert) + ) + .sorted(Comparator.comparing(TrustList::getKid)) + .collect(Collectors.toList()); + } + + private TrustList convert(TrustedPartyEntity trustedPartyEntity) { + return new TrustList( + certificateUtils.getCertKid(trustedPartyService.getX509CertificateFromEntity(trustedPartyEntity)), + trustedPartyEntity.getCreatedAt(), + trustedPartyEntity.getCountry(), + map(trustedPartyEntity.getCertificateType()), + trustedPartyEntity.getThumbprint(), + trustedPartyEntity.getSignature(), + trustedPartyEntity.getRawData() + ); + } + + private TrustList convert(SignerInformationEntity signerInformationEntity) { + return new TrustList( + certificateUtils.getCertKid(signerInformationService.getX509CertificateFromEntity(signerInformationEntity)), + signerInformationEntity.getCreatedAt(), + signerInformationEntity.getCountry(), + map(signerInformationEntity.getCertificateType()), + signerInformationEntity.getThumbprint(), + signerInformationEntity.getSignature(), + signerInformationEntity.getRawData() + ); + } + + private TrustedPartyEntity.CertificateType map(TrustListType type) { + return TrustedPartyEntity.CertificateType.valueOf(type.name()); + } + + private TrustListType map(Enum type) { + return TrustListType.valueOf(type.name()); + } + +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/service/TrustedPartyService.java b/src/main/java/eu/europa/ec/dgc/gateway/service/TrustedPartyService.java new file mode 100644 index 00000000..7d55b5f3 --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/service/TrustedPartyService.java @@ -0,0 +1,203 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.service; + +import eu.europa.ec.dgc.gateway.config.DgcConfigProperties; +import eu.europa.ec.dgc.gateway.entity.TrustedPartyEntity; +import eu.europa.ec.dgc.gateway.repository.TrustedPartyRepository; +import eu.europa.ec.dgc.gateway.utils.DgcMdc; +import eu.europa.ec.dgc.signing.SignedCertificateMessageParser; +import eu.europa.ec.dgc.signing.SignedMessageParser; +import eu.europa.ec.dgc.utils.CertificateUtils; +import java.io.IOException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.util.Base64; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.bouncycastle.cert.X509CertificateHolder; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +@RequiredArgsConstructor +public class TrustedPartyService { + + private static final String MDC_PROP_CERT_THUMBPRINT = "certVerifyThumbprint"; + private static final String MDC_PROP_PARSER_STATE = "parserState"; + private final TrustedPartyRepository trustedPartyRepository; + private final KeyStore trustAnchorKeyStore; + private final DgcConfigProperties dgcConfigProperties; + private final CertificateUtils certificateUtils; + + /** + * Method to query the db for all certificates. + * + * @return List holding the found certificates. + */ + public List getCertificates() { + + return trustedPartyRepository.findAll() + .stream() + .filter(this::validateCertificateIntegrity) + .collect(Collectors.toList()); + } + + /** + * Method to query the db for certificates by type. + * + * @param type type to filter for. + * @return List holding the found certificates. + */ + public List getCertificates(TrustedPartyEntity.CertificateType type) { + + return trustedPartyRepository.getByCertificateType(type) + .stream() + .filter(this::validateCertificateIntegrity) + .collect(Collectors.toList()); + } + + /** + * Method to query the db for certificates. + * + * @param country country of certificate. + * @param type type of certificate. + * @return List holding the found certificates. + */ + public List getCertificates(String country, TrustedPartyEntity.CertificateType type) { + + return trustedPartyRepository.getByCountryAndCertificateType(country, type) + .stream() + .filter(this::validateCertificateIntegrity) + .collect(Collectors.toList()); + } + + /** + * Method to query the db for a certificate. + * + * @param thumbprint RSA-256 thumbprint of certificate. + * @param country country of certificate. + * @param type type of certificate. + * @return Optional holding the certificate if found. + */ + public Optional getCertificate( + String thumbprint, String country, TrustedPartyEntity.CertificateType type) { + + return trustedPartyRepository.getFirstByThumbprintAndCountryAndCertificateType(thumbprint, country, type) + .map(trustedPartyEntity -> validateCertificateIntegrity(trustedPartyEntity) ? trustedPartyEntity : null); + } + + /** + * Returns a list of onboarded countries. + * + * @return List of String. + */ + public List getCountryList() { + return trustedPartyRepository.getCountryCodeList(); + } + + private boolean validateCertificateIntegrity(TrustedPartyEntity trustedPartyEntity) { + + DgcMdc.put(MDC_PROP_CERT_THUMBPRINT, trustedPartyEntity.getThumbprint()); + + // check if entity has signature and certificate information + if (trustedPartyEntity.getSignature() == null || trustedPartyEntity.getSignature().isEmpty() + || trustedPartyEntity.getRawData() == null || trustedPartyEntity.getRawData().isEmpty()) { + log.error("Certificate entity does not contain raw certificate or certificate signature."); + return false; + } + + // check if raw data contains a x509 certificate + X509Certificate x509Certificate = getX509CertificateFromEntity(trustedPartyEntity); + if (x509Certificate == null) { + log.error("Raw certificate data does not contain a valid x509Certificate."); + return false; + } + + // verify if thumbprint in database matches the certificate in raw data + if (!verifyThumbprintMatchesCertificate(trustedPartyEntity, x509Certificate)) { + log.error("Thumbprint in database does not match thumbprint of stored certificate."); + return false; + } + + // load DGCG Trust Anchor PublicKey from KeyStore + X509CertificateHolder trustAnchor = null; + try { + trustAnchor = certificateUtils.convertCertificate((X509Certificate) trustAnchorKeyStore.getCertificate( + dgcConfigProperties.getTrustAnchor().getCertificateAlias())); + } catch (KeyStoreException | CertificateEncodingException | IOException e) { + log.error("Could not load DGCG-TrustAnchor from KeyStore.", e); + return false; + } + + // verify certificate signature + SignedCertificateMessageParser parser = + new SignedCertificateMessageParser(trustedPartyEntity.getSignature(), trustedPartyEntity.getRawData()); + + if (parser.getParserState() != SignedMessageParser.ParserState.SUCCESS) { + DgcMdc.put(MDC_PROP_PARSER_STATE, parser.getParserState().name()); + log.error("TrustAnchor Verification failed."); + return false; + } + + if (!parser.isSignatureVerified()) { + log.error("TrustAnchor Verification failed: Signature is not matching signed certificate"); + return false; + } + + if (!parser.getSigningCertificate().equals(trustAnchor)) { + log.error("TrustAnchor Verification failed: Certificate was not signed by known TrustAnchor"); + return false; + } + + return true; + } + + + /** + * Extracts X509Certificate from {@link TrustedPartyEntity}. + * + * @param trustedPartyEntity entity from which the certificate should be extraced. + * @return X509Certificate representation. + */ + public X509Certificate getX509CertificateFromEntity(TrustedPartyEntity trustedPartyEntity) { + try { + byte[] rawDataBytes = Base64.getDecoder().decode(trustedPartyEntity.getRawData()); + return certificateUtils.convertCertificate(new X509CertificateHolder(rawDataBytes)); + } catch (Exception e) { + log.error("Raw certificate data does not contain a valid x509Certificate", e); + } + + return null; + } + + private boolean verifyThumbprintMatchesCertificate( + TrustedPartyEntity trustedPartyEntity, X509Certificate certificate) { + String certHash = certificateUtils.getCertThumbprint(certificate); + + return certHash != null && certHash.equals(trustedPartyEntity.getThumbprint()); + } +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/service/ValidationRuleService.java b/src/main/java/eu/europa/ec/dgc/gateway/service/ValidationRuleService.java new file mode 100644 index 00000000..09cb0c1b --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/service/ValidationRuleService.java @@ -0,0 +1,405 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.vdurmont.semver4j.Semver; +import eu.europa.ec.dgc.gateway.config.ValidationRuleSchemaProvider; +import eu.europa.ec.dgc.gateway.entity.TrustedPartyEntity; +import eu.europa.ec.dgc.gateway.entity.ValidationRuleEntity; +import eu.europa.ec.dgc.gateway.model.ParsedValidationRule; +import eu.europa.ec.dgc.gateway.repository.ValidationRuleRepository; +import eu.europa.ec.dgc.gateway.utils.DgcMdc; +import eu.europa.ec.dgc.utils.CertificateUtils; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.bouncycastle.cert.X509CertificateHolder; +import org.everit.json.schema.Schema; +import org.everit.json.schema.ValidationException; +import org.json.JSONException; +import org.json.JSONObject; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Service; + +@Service +@Slf4j +@RequiredArgsConstructor +public class ValidationRuleService { + + private final ValidationRuleRepository validationRuleRepository; + + private final CertificateUtils certificateUtils; + + private final TrustedPartyService trustedPartyService; + + private final ValidationRuleSchemaProvider validationRuleSchemaProvider; + + private final ObjectMapper objectMapper; + + private static final String MDC_PROP_UPLOAD_CERT_THUMBPRINT = "uploadCertThumbprint"; + + /** + * Queries the database for active Validation Rules filtered by country. + * If the latest Validation Rule's validity is in future the whole version history of rules between the currently + * active and the latest rule will be returned. + * + * @param country 2 Digit Country Code + * @return List of ValidationRule Entities. + */ + public List getActiveValidationRules(String country) { + + // Getting the IDs of the latest ValidationRuleVersion of each Rule ID. + return validationRuleRepository.getLatestIds(country).stream() + // Getting the corresponding entity + .map(validationRuleRepository::findById) + // Resolve Optional + .map(Optional::get) + // Check if version history is needed for each rule + .map(rule -> { + if (ZonedDateTime.now().isAfter(rule.getValidFrom())) { + // rule already valid - only return this one + return Collections.singletonList(rule); + } else { + // Rule is valid in future, history of this rule is required + + // Get the ID of the first Entity with this RuleId and ValidFrom is Before now. + List ids = validationRuleRepository.getIdByValidFromIsBeforeAndRuleIdIs( + ZonedDateTime.now(), rule.getRuleId(), PageRequest.of(0, 1)); + + if (ids.isEmpty()) { + // Rule has no older but currently valid version + // --> return all rules with valid from is greater than today + + return validationRuleRepository.getByRuleIdAndValidFromIsGreaterThanEqualOrderByIdDesc( + rule.getRuleId(), + ZonedDateTime.now() + ); + } else { + // Return al previous versions and rule itself. + return validationRuleRepository + .getByIdIsGreaterThanEqualAndRuleIdIsOrderByIdDesc(ids.get(0), rule.getRuleId()); + } + } + }) + // flatten the 2 dimensional list + .flatMap(Collection::stream) + // return as one dimensional list + .collect(Collectors.toList()); + } + + /** + * Deletes all rules with given ID. + * + * @param ruleId to delete + * @return amount of deleted entities. + */ + public int deleteByRuleId(String ruleId) { + return validationRuleRepository.deleteByRuleId(ruleId); + } + + /** + * Gets the 2 Digit Country Code from a ID String (e.g. GR-EU-13330 -> EU) + * + * @param idString the idString to parse + * @return the 2 digit country code or null if parsing has failed. + */ + public String getCountryCodeFromIdString(String idString) { + String[] parts = idString.split("-"); + + if (parts.length != 3) { + return null; + } + + if (parts[1].length() != 2) { + return null; + } + + return parts[1]; + } + + /** + * Adds a new Validation Rule DB. + * + * @param uploadedRule the JSON String with the uploaded rule. + * @param signerCertificate the certificate which was used to sign the message + * @param cms the cms containing the JSON + * @param authenticatedCountryCode the country code of the uploader country from cert authentication + * @throws ValidationRuleCheckException if validation check has failed. The exception contains + * a reason property with detailed information why the validation has failed. + */ + public ValidationRuleEntity addValidationRule( + String uploadedRule, + X509CertificateHolder signerCertificate, + String cms, + String authenticatedCountryCode + ) throws ValidationRuleCheckException { + + contentCheckUploaderCertificate(signerCertificate, authenticatedCountryCode); + ParsedValidationRule parsedValidationRule = contentCheckValidJson(uploadedRule); + + ValidationRuleEntity.ValidationRuleType validationRuleType = + parsedValidationRule.getType().equals("Acceptance") ? ValidationRuleEntity.ValidationRuleType.ACCEPTANCE + : ValidationRuleEntity.ValidationRuleType.INVALIDATION; + + contentCheckRuleIdPrefixMatchCertificateType(parsedValidationRule); + contentCheckRuleIdPrefixMatchType(parsedValidationRule, validationRuleType); + contentCheckUploaderCountry(parsedValidationRule, authenticatedCountryCode); + Optional latestValidationRule = contentCheckVersion(parsedValidationRule); + contentCheckTimestamps(parsedValidationRule, validationRuleType, latestValidationRule); + + // All checks passed --> Save to DB + ValidationRuleEntity newValidationRule = new ValidationRuleEntity(); + newValidationRule.setValidationRuleType(validationRuleType); + newValidationRule.setCountry(parsedValidationRule.getCountry()); + newValidationRule.setRuleId(parsedValidationRule.getIdentifier()); + newValidationRule.setValidTo(parsedValidationRule.getValidTo()); + newValidationRule.setValidFrom(parsedValidationRule.getValidFrom()); + newValidationRule.setCms(cms); + newValidationRule.setVersion(parsedValidationRule.getVersion()); + + log.info("Saving new ValidationRule Entity"); + + newValidationRule = validationRuleRepository.save(newValidationRule); + + DgcMdc.remove(MDC_PROP_UPLOAD_CERT_THUMBPRINT); + + return newValidationRule; + } + + private void contentCheckRuleIdPrefixMatchCertificateType(ParsedValidationRule parsedValidationRule) + throws ValidationRuleCheckException { + + Map mapping = Map.of( + "TR", "Test", + "VR", "Vaccination", + "RR", "Recovery", + "GR", "General" + ); + + for (Map.Entry entry : mapping.entrySet()) { + if (parsedValidationRule.getIdentifier().startsWith(entry.getKey()) + && !parsedValidationRule.getCertificateType().equals(entry.getValue())) { + + throw new ValidationRuleCheckException(ValidationRuleCheckException.Reason.INVALID_RULE_ID, + String.format("ID must start with %s for %s Rules", entry.getKey(), entry.getValue())); + } + } + } + + private void contentCheckRuleIdPrefixMatchType( + ParsedValidationRule parsedValidationRule, ValidationRuleEntity.ValidationRuleType type) + throws ValidationRuleCheckException { + + if (parsedValidationRule.getIdentifier().startsWith("IR") + && type == ValidationRuleEntity.ValidationRuleType.ACCEPTANCE) { + throw new ValidationRuleCheckException(ValidationRuleCheckException.Reason.INVALID_RULE_ID, + "Acceptance Rule Rule-ID requires prefix other than IR."); + } + + if (!parsedValidationRule.getIdentifier().startsWith("IR") + && type == ValidationRuleEntity.ValidationRuleType.INVALIDATION) { + throw new ValidationRuleCheckException(ValidationRuleCheckException.Reason.INVALID_RULE_ID, + "Invalidation Rule Rule-ID requires IR prefix."); + } + } + + private Optional contentCheckVersion(ParsedValidationRule parsedValidationRule) + throws ValidationRuleCheckException { + // Get latest version in DB + Optional latestValidationRule = + validationRuleRepository.getFirstByRuleIdOrderByIdDesc(parsedValidationRule.getIdentifier()); + + if (latestValidationRule.isEmpty()) { + return latestValidationRule; + } + + Semver latestVersion = new Semver(latestValidationRule.get().getVersion()); + Semver uploadedVersion = new Semver(parsedValidationRule.getVersion()); + + if (uploadedVersion.isLowerThanOrEqualTo(latestVersion)) { + throw new ValidationRuleCheckException( + ValidationRuleCheckException.Reason.INVALID_VERSION, + "Version of new rule (%s) needs to be greater then old version (%s)", uploadedVersion, latestVersion + ); + } + + return latestValidationRule; + } + + private void contentCheckTimestamps( + ParsedValidationRule parsedValidationRule, + ValidationRuleEntity.ValidationRuleType type, + Optional latestValidationRule) throws ValidationRuleCheckException { + + if (!parsedValidationRule.getValidTo().isAfter(parsedValidationRule.getValidFrom())) { + throw new ValidationRuleCheckException( + ValidationRuleCheckException.Reason.INVALID_TIMESTAMP, + "ValidFrom (%s) needs to be before ValidTo (%s).", + parsedValidationRule.getValidFrom().toString(), + parsedValidationRule.getValidTo().toString()); + } + + if (parsedValidationRule.getValidFrom().isAfter(ZonedDateTime.now().plus(2, ChronoUnit.WEEKS))) { + throw new ValidationRuleCheckException( + ValidationRuleCheckException.Reason.INVALID_TIMESTAMP, + "ValidFrom (%s) cannot be more than 2 weeks in future.", + parsedValidationRule.getValidFrom().toString()); + } + + if (type == ValidationRuleEntity.ValidationRuleType.ACCEPTANCE + && parsedValidationRule.getValidFrom() + .isBefore(ZonedDateTime.now().plus(48, ChronoUnit.HOURS))) { + + throw new ValidationRuleCheckException( + ValidationRuleCheckException.Reason.INVALID_TIMESTAMP, + "ValidFrom (%s) needs to be at least 48h in future for Acceptance Validation Rules", + parsedValidationRule.getValidFrom().toString()); + } + + if (type == ValidationRuleEntity.ValidationRuleType.INVALIDATION + && parsedValidationRule.getValidFrom().isBefore(ZonedDateTime.now())) { + + throw new ValidationRuleCheckException( + ValidationRuleCheckException.Reason.INVALID_TIMESTAMP, + "ValidFrom (%s) needs to be in future for Invalidation Rules", + parsedValidationRule.getValidFrom().toString()); + } + + if (parsedValidationRule.getValidFrom().plus(3, ChronoUnit.DAYS) + .isAfter(parsedValidationRule.getValidTo())) { + + throw new ValidationRuleCheckException( + ValidationRuleCheckException.Reason.INVALID_TIMESTAMP, + "Rule Validity must be at least 72h but is %dh", + ChronoUnit.HOURS.between(parsedValidationRule.getValidFrom(), parsedValidationRule.getValidTo())); + } + + if (latestValidationRule.isPresent() + && parsedValidationRule.getValidFrom().isBefore(latestValidationRule.get().getValidFrom())) { + + throw new ValidationRuleCheckException( + ValidationRuleCheckException.Reason.INVALID_TIMESTAMP, + "ValidFrom (%s) needs to be after or equal to ValidFrom (%s) of previous version of the rule.", + parsedValidationRule.getValidFrom().toString(), latestValidationRule.get().getValidFrom().toString()); + } + } + + private void contentCheckUploaderCountry(ParsedValidationRule parsedValidationRule, String countryCode) + throws ValidationRuleCheckException { + if (!parsedValidationRule.getCountry().equals(countryCode)) { + throw new ValidationRuleCheckException( + ValidationRuleCheckException.Reason.INVALID_COUNTRY, + "Country does not match your authentication."); + } + + if (!getCountryCodeFromIdString(parsedValidationRule.getIdentifier()).equals(countryCode)) { + throw new ValidationRuleCheckException( + ValidationRuleCheckException.Reason.INVALID_COUNTRY, + "Country Code in Identifier does not match country."); + } + } + + private ParsedValidationRule contentCheckValidJson(String json) throws ValidationRuleCheckException { + Schema validationSchema = validationRuleSchemaProvider.getValidationRuleSchema(); + + try { + JSONObject jsonObject = new JSONObject(json); + validationSchema.validate(jsonObject); + } catch (JSONException e) { + throw new ValidationRuleCheckException( + ValidationRuleCheckException.Reason.INVALID_JSON, + "JSON could not be parsed"); + } catch (ValidationException validationException) { + throw new ValidationRuleCheckException( + ValidationRuleCheckException.Reason.INVALID_JSON, + "JSON does not align to Validation Rule Schema: %s", + String.join(", ", validationException.getAllMessages())); + } + + try { + objectMapper.configure(DeserializationFeature.FAIL_ON_TRAILING_TOKENS, true); + return objectMapper.readValue(json, ParsedValidationRule.class); + } catch (JsonProcessingException e) { + throw new ValidationRuleCheckException( + ValidationRuleCheckException.Reason.INVALID_JSON, + "JSON could not be parsed"); + } + } + + /** + * Checks a given UploadCertificate if it exists in the database and is assigned to given CountryCode. + * + * @param signerCertificate Upload Certificate + * @param authenticatedCountryCode Country Code. + * @throws ValidationRuleCheckException if Validation fails. + */ + public void contentCheckUploaderCertificate( + X509CertificateHolder signerCertificate, + String authenticatedCountryCode) throws ValidationRuleCheckException { + // Content Check Step 1: Uploader Certificate + String signerCertThumbprint = certificateUtils.getCertThumbprint(signerCertificate); + Optional certFromDb = trustedPartyService.getCertificate( + signerCertThumbprint, + authenticatedCountryCode, + TrustedPartyEntity.CertificateType.UPLOAD + ); + + if (certFromDb.isEmpty()) { + throw new ValidationRuleCheckException(ValidationRuleCheckException.Reason.UPLOADER_CERT_CHECK_FAILED, + "Could not find upload certificate with hash %s and country %s", + signerCertThumbprint, authenticatedCountryCode); + } + + DgcMdc.put(MDC_PROP_UPLOAD_CERT_THUMBPRINT, signerCertThumbprint); + } + + public static class ValidationRuleCheckException extends Exception { + + @Getter + private final Reason reason; + + public ValidationRuleCheckException(Reason reason, String message, Object... args) { + super(String.format(message, args)); + this.reason = reason; + } + + public enum Reason { + INVALID_JSON, + INVALID_COUNTRY, + INVALID_TIMESTAMP, + INVALID_VERSION, + INVALID_RULE_ID, + UPLOADER_CERT_CHECK_FAILED, + } + } + +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/service/ValuesetService.java b/src/main/java/eu/europa/ec/dgc/gateway/service/ValuesetService.java new file mode 100644 index 00000000..084c4abc --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/service/ValuesetService.java @@ -0,0 +1,84 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.service; + +import eu.europa.ec.dgc.gateway.entity.ValuesetEntity; +import eu.europa.ec.dgc.gateway.repository.ValuesetRepository; +import eu.europa.ec.dgc.gateway.utils.DgcMdc; +import java.util.List; +import java.util.Optional; +import javax.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@Service +@Slf4j +@RequiredArgsConstructor +public class ValuesetService { + + private final ValuesetRepository valuesetRepository; + + /** + * Gets a list of existing valueset IDs. + * + * @return List of Strings + */ + public List getValuesetIds() { + log.info("Getting ValueSet IDs"); + + return valuesetRepository.getIds(); + } + + /** + * Gets the content of a valueset by its id. + * + * @param id id of the valueset + * @return the json content + */ + public Optional getValueSetById(String id) { + DgcMdc.put("valueSetId", id); + log.info("Requesting Value Set."); + + return valuesetRepository.findById(id).map(ValuesetEntity::getJson); + } + + /** + * Sets the JSON-Value of a ValueSet. + * If a Valueset with the given ID does not exists it will be created. + * + * @param id ValueSet-ID + * @param value JSON String containing Valueset. + */ + @Transactional + public void updateValueSet(String id, String value) { + log.info("Updating Valueset."); + + Optional valuesetEntityOptional = valuesetRepository.findById(id); + + ValuesetEntity valuesetEntity = valuesetEntityOptional.orElse(new ValuesetEntity()); + valuesetEntity.setId(id); + valuesetEntity.setJson(value); + + valuesetRepository.save(valuesetEntity); + } + +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/utils/DgcMdc.java b/src/main/java/eu/europa/ec/dgc/gateway/utils/DgcMdc.java new file mode 100644 index 00000000..db7312d7 --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/utils/DgcMdc.java @@ -0,0 +1,112 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.utils; + +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Date; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.slf4j.MDC; + +/** + * Wrapper for MDC to escape values for better log files. + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class DgcMdc { + + /** + * Put a diagnostic context value (the value parameter) as identified with the + * key parameter into the current thread's diagnostic context map. The + * key parameter cannot be null. The value parameter + * can be null only if the underlying implementation supports it. + * + *

This method delegates all work to the MDC of the underlying logging system. + * + * @param key non-null key + * @param value value to put in the map + * @throws IllegalArgumentException in case the "key" parameter is null + */ + public static void put(String key, String value) { + value = value == null ? ">>null<<" : value; + + value = value.replace("\"", "'"); + MDC.put(key, "\"" + value + "\""); + } + + /** + * Put a diagnostic context value (the value parameter) as identified with the + * key parameter into the current thread's diagnostic context map. The + * key parameter cannot be null. The value parameter + * can be null only if the underlying implementation supports it. + * + *

This method delegates all work to the MDC of the underlying logging system. + * + * @param key non-null key + * @param value a numeric value to put in the map + * @throws IllegalArgumentException in case the "key" parameter is null + */ + public static void put(String key, long value) { + put(key, String.valueOf(value)); + } + + /** + * Put a diagnostic context value (the value parameter) as identified with the + * key parameter into the current thread's diagnostic context map. The + * key parameter cannot be null. The value parameter + * can be null only if the underlying implementation supports it. + * + *

This method delegates all work to the MDC of the underlying logging system. + * + * @param key non-null key + * @param value a date value to put in the map + * @throws IllegalArgumentException in case the "key" parameter is null + */ + public static void put(String key, Date value) { + ZonedDateTime timestamp = ZonedDateTime.ofInstant(value.toInstant(), ZoneOffset.UTC); + + put( + key, + timestamp.format(DateTimeFormatter.ISO_INSTANT) + ); + } + + /** + * Remove the diagnostic context identified by the key parameter using + * the underlying system's MDC implementation. The key parameter + * cannot be null. This method does nothing if there is no previous value + * associated with key. + * + * @param key non-null key + * @throws IllegalArgumentException in case the "key" parameter is null + */ + public static void remove(String key) { + MDC.remove(key); + } + + /** + * Clear all entries in the MDC of the underlying implementation. + */ + public static void clear() { + MDC.clear(); + } +} diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml new file mode 100644 index 00000000..d8c6d0f8 --- /dev/null +++ b/src/main/resources/application-dev.yml @@ -0,0 +1,14 @@ +spring: + h2: + console: + enabled: true + path: /h2-console +springdoc: + api-docs: + path: /api/docs + enabled: true + swagger-ui: + path: /swagger +dgc: + trustAnchor: + keyStorePath: classpath:keystore/dgc-ta.jks diff --git a/src/main/resources/application-h2.yml b/src/main/resources/application-h2.yml new file mode 100644 index 00000000..a25b168f --- /dev/null +++ b/src/main/resources/application-h2.yml @@ -0,0 +1,9 @@ +spring: + datasource: + jndi-name: false + driver-class-name: org.h2.Driver + url: jdbc:h2:mem:dgc;DB_CLOSE_ON_EXIT=FALSE;DB_CLOSE_DELAY=-1; + username: sa + password: '' + jpa: + database-platform: org.hibernate.dialect.H2Dialect diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml new file mode 100644 index 00000000..d8a239f6 --- /dev/null +++ b/src/main/resources/application-local.yml @@ -0,0 +1,6 @@ +springdoc: + api-docs: + path: /api/docs + enabled: true + swagger-ui: + path: /swagger diff --git a/src/main/resources/application-mysql.yml b/src/main/resources/application-mysql.yml new file mode 100644 index 00000000..8fdafc86 --- /dev/null +++ b/src/main/resources/application-mysql.yml @@ -0,0 +1,9 @@ +spring: + datasource: + url: jdbc:mysql://localhost:3306/dgc + username: sa + password: sa + driver-class-name: com.mysql.cj.jdbc.Driver + jndi-name: false + jpa: + database-platform: org.hibernate.dialect.MySQL5InnoDBDialect diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 00000000..558d7801 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,48 @@ +server: + port: 8090 +spring: + profiles: + group: + "dev": "h2" + application: + name: eu-digital-green-certificates-gateway + datasource: + jndi-name: jdbc/dgc + jpa: + database-platform: org.hibernate.dialect.MySQL5InnoDBDialect + hibernate: + ddl-auto: validate + liquibase: + enabled: true + change-log: classpath:db/changelog.xml + task: + scheduling: + pool: + size: 5 +management: + endpoints: + web: + exposure: + exclude: "*" + server: + port: -1 +dgc: + jrc: + url: https://covid-19-diagnostics.jrc.ec.europa.eu/devices/hsc-common-recognition-rat + proxy: + host: ${https.proxyHost:} + port: ${https.proxyPort:-1} + username: ${https.proxyUser:} + password: ${https.proxyPassword:} + validationRuleSchema: classpath:validation-rule.schema.json + trustAnchor: + keyStorePath: /ec/prod/app/san/dgc/dgc-ta.jks + keyStorePass: dgc-p4ssw0rd + certificateAlias: dgc_trust_anchor + cert-auth: + header-fields: + thumbprint: X-SSL-Client-SHA256 + distinguished-name: X-SSL-Client-DN +springdoc: + api-docs: + enabled: false diff --git a/src/main/resources/db/changelog.xml b/src/main/resources/db/changelog.xml new file mode 100644 index 00000000..25ee75b7 --- /dev/null +++ b/src/main/resources/db/changelog.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/db/changelog/add-shedlock-table.xml b/src/main/resources/db/changelog/add-shedlock-table.xml new file mode 100644 index 00000000..3f9095d6 --- /dev/null +++ b/src/main/resources/db/changelog/add-shedlock-table.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/db/changelog/add-unique-constraints.xml b/src/main/resources/db/changelog/add-unique-constraints.xml new file mode 100644 index 00000000..8d454a0f --- /dev/null +++ b/src/main/resources/db/changelog/add-unique-constraints.xml @@ -0,0 +1,16 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/db/changelog/add-validation-rule-table.xml b/src/main/resources/db/changelog/add-validation-rule-table.xml new file mode 100644 index 00000000..8ac6ff23 --- /dev/null +++ b/src/main/resources/db/changelog/add-validation-rule-table.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/db/changelog/add-valueset-table.xml b/src/main/resources/db/changelog/add-valueset-table.xml new file mode 100644 index 00000000..4568bfa1 --- /dev/null +++ b/src/main/resources/db/changelog/add-valueset-table.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/db/changelog/create-audit-table.xml b/src/main/resources/db/changelog/create-audit-table.xml new file mode 100644 index 00000000..630a6fc0 --- /dev/null +++ b/src/main/resources/db/changelog/create-audit-table.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/db/changelog/fix-certificate-thumbprints.xml b/src/main/resources/db/changelog/fix-certificate-thumbprints.xml new file mode 100644 index 00000000..ce51f139 --- /dev/null +++ b/src/main/resources/db/changelog/fix-certificate-thumbprints.xml @@ -0,0 +1,20 @@ + + + + + + UPDATE trusted_party SET thumbprint = concat('00', thumbprint) WHERE length(thumbprint) = 62; + + + + UPDATE signer_information SET thumbprint = concat('00', thumbprint) WHERE length(thumbprint) = 62; + + + + + \ No newline at end of file diff --git a/src/main/resources/db/changelog/increase-column-size-for-valueset.xml b/src/main/resources/db/changelog/increase-column-size-for-valueset.xml new file mode 100644 index 00000000..605cf2c3 --- /dev/null +++ b/src/main/resources/db/changelog/increase-column-size-for-valueset.xml @@ -0,0 +1,12 @@ + + + + + + + \ No newline at end of file diff --git a/src/main/resources/db/changelog/init-tables.xml b/src/main/resources/db/changelog/init-tables.xml new file mode 100644 index 00000000..0709924b --- /dev/null +++ b/src/main/resources/db/changelog/init-tables.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/db/snapshot.json b/src/main/resources/db/snapshot.json new file mode 100644 index 00000000..03c99253 --- /dev/null +++ b/src/main/resources/db/snapshot.json @@ -0,0 +1,731 @@ +{ + "snapshot": { + "created": "2021-06-28T15:18:17.131", + "database": { + "productVersion": "2021.1.2", + "shortName": "intellijPsiClass", + "majorVersion": "0", + "minorVersion": "0", + "user": "A34636994", + "productName": "JPA Buddy Intellij", + "url": "jpab?generationContext=60d05a16-aca2-4aeb-9823-8d67f6b942f1" + }, + "metadata": { + "generationContext": { + "dbmsType": "mysql" + } + }, + "objects": { + "liquibase.structure.core.Catalog": [ + { + "catalog": { + "default": true, + "name": "JPA_BUDDY", + "snapshotId": "ff23152" + } + } + ], + "liquibase.structure.core.Column": [ + { + "column": { + "certainDataType": false, + "name": "authentication_sha256_fingerprint", + "nullable": false, + "relation": "liquibase.structure.core.Table#ff23153", + "snapshotId": "ff23159", + "type": { + "columnSize": "64!{java.lang.Integer}", + "typeName": "VARCHAR" + } + } + }, + { + "column": { + "certainDataType": false, + "name": "certificate_type", + "nullable": false, + "relation": "liquibase.structure.core.Table#ff23163", + "snapshotId": "ff23172", + "type": { + "columnSize": "255!{java.lang.Integer}", + "typeName": "VARCHAR" + } + } + }, + { + "column": { + "certainDataType": false, + "name": "certificate_type", + "nullable": false, + "relation": "liquibase.structure.core.Table#ff23191", + "snapshotId": "ff23200", + "type": { + "columnSize": "255!{java.lang.Integer}", + "typeName": "VARCHAR" + } + } + }, + { + "column": { + "certainDataType": false, + "name": "country", + "nullable": false, + "relation": "liquibase.structure.core.Table#ff23153", + "snapshotId": "ff23157", + "type": { + "columnSize": "2!{java.lang.Integer}", + "typeName": "VARCHAR" + } + } + }, + { + "column": { + "certainDataType": false, + "name": "country", + "nullable": false, + "relation": "liquibase.structure.core.Table#ff23163", + "snapshotId": "ff23169", + "type": { + "columnSize": "2!{java.lang.Integer}", + "typeName": "VARCHAR" + } + } + }, + { + "column": { + "certainDataType": false, + "name": "country", + "nullable": false, + "relation": "liquibase.structure.core.Table#ff23191", + "snapshotId": "ff23197", + "type": { + "columnSize": "2!{java.lang.Integer}", + "typeName": "VARCHAR" + } + } + }, + { + "column": { + "certainDataType": false, + "name": "country", + "nullable": false, + "relation": "liquibase.structure.core.Table#ff23174", + "snapshotId": "ff23183", + "type": { + "columnSize": "2!{java.lang.Integer}", + "typeName": "VARCHAR" + } + } + }, + { + "column": { + "certainDataType": false, + "name": "created_at", + "nullable": false, + "relation": "liquibase.structure.core.Table#ff23163", + "snapshotId": "ff23168", + "type": { + "typeName": "DATETIME" + } + } + }, + { + "column": { + "certainDataType": false, + "name": "created_at", + "nullable": false, + "relation": "liquibase.structure.core.Table#ff23191", + "snapshotId": "ff23196", + "type": { + "typeName": "DATETIME" + } + } + }, + { + "column": { + "certainDataType": false, + "name": "created_at", + "nullable": false, + "relation": "liquibase.structure.core.Table#ff23174", + "snapshotId": "ff23177", + "type": { + "typeName": "DATETIME" + } + } + }, + { + "column": { + "certainDataType": false, + "name": "description", + "nullable": false, + "relation": "liquibase.structure.core.Table#ff23153", + "snapshotId": "ff23161", + "type": { + "columnSize": "64!{java.lang.Integer}", + "typeName": "VARCHAR" + } + } + }, + { + "column": { + "certainDataType": false, + "name": "event", + "nullable": false, + "relation": "liquibase.structure.core.Table#ff23153", + "snapshotId": "ff23160", + "type": { + "columnSize": "64!{java.lang.Integer}", + "typeName": "VARCHAR" + } + } + }, + { + "column": { + "autoIncrementInformation": { + "incrementBy": "1!{java.math.BigInteger}", + "startWith": "1!{java.math.BigInteger}" + }, + "certainDataType": false, + "name": "id", + "nullable": false, + "relation": "liquibase.structure.core.Table#ff23153", + "snapshotId": "ff23155", + "type": { + "typeName": "BIGINT" + } + } + }, + { + "column": { + "autoIncrementInformation": { + "incrementBy": "1!{java.math.BigInteger}", + "startWith": "1!{java.math.BigInteger}" + }, + "certainDataType": false, + "name": "id", + "nullable": false, + "relation": "liquibase.structure.core.Table#ff23163", + "snapshotId": "ff23167", + "type": { + "typeName": "BIGINT" + } + } + }, + { + "column": { + "autoIncrementInformation": { + "incrementBy": "1!{java.math.BigInteger}", + "startWith": "1!{java.math.BigInteger}" + }, + "certainDataType": false, + "name": "id", + "nullable": false, + "relation": "liquibase.structure.core.Table#ff23191", + "snapshotId": "ff23195", + "type": { + "typeName": "BIGINT" + } + } + }, + { + "column": { + "autoIncrementInformation": { + "incrementBy": "1!{java.math.BigInteger}", + "startWith": "1!{java.math.BigInteger}" + }, + "certainDataType": false, + "name": "id", + "nullable": false, + "relation": "liquibase.structure.core.Table#ff23174", + "snapshotId": "ff23176", + "type": { + "typeName": "BIGINT" + } + } + }, + { + "column": { + "certainDataType": false, + "name": "id", + "nullable": false, + "relation": "liquibase.structure.core.Table#ff23186", + "snapshotId": "ff23188", + "type": { + "columnSize": "100!{java.lang.Integer}", + "typeName": "VARCHAR" + } + } + }, + { + "column": { + "certainDataType": false, + "name": "json", + "nullable": false, + "relation": "liquibase.structure.core.Table#ff23186", + "snapshotId": "ff23189", + "type": { + "columnSize": "1024000!{java.lang.Integer}", + "typeName": "VARCHAR" + } + } + }, + { + "column": { + "certainDataType": false, + "name": "raw_data", + "nullable": false, + "relation": "liquibase.structure.core.Table#ff23163", + "snapshotId": "ff23170", + "type": { + "columnSize": "4096!{java.lang.Integer}", + "typeName": "VARCHAR" + } + } + }, + { + "column": { + "certainDataType": false, + "name": "raw_data", + "nullable": false, + "relation": "liquibase.structure.core.Table#ff23191", + "snapshotId": "ff23198", + "type": { + "columnSize": "4096!{java.lang.Integer}", + "typeName": "VARCHAR" + } + } + }, + { + "column": { + "certainDataType": false, + "name": "rule_id", + "nullable": false, + "relation": "liquibase.structure.core.Table#ff23174", + "snapshotId": "ff23178", + "type": { + "columnSize": "100!{java.lang.Integer}", + "typeName": "VARCHAR" + } + } + }, + { + "column": { + "certainDataType": false, + "name": "signature", + "nullable": false, + "relation": "liquibase.structure.core.Table#ff23163", + "snapshotId": "ff23171", + "type": { + "columnSize": "6000!{java.lang.Integer}", + "typeName": "VARCHAR" + } + } + }, + { + "column": { + "certainDataType": false, + "name": "signature", + "nullable": false, + "relation": "liquibase.structure.core.Table#ff23191", + "snapshotId": "ff23199", + "type": { + "columnSize": "6000!{java.lang.Integer}", + "typeName": "VARCHAR" + } + } + }, + { + "column": { + "certainDataType": false, + "name": "signature", + "nullable": false, + "relation": "liquibase.structure.core.Table#ff23174", + "snapshotId": "ff23179", + "type": { + "columnSize": "10000!{java.lang.Integer}", + "typeName": "VARCHAR" + } + } + }, + { + "column": { + "certainDataType": false, + "name": "thumbprint", + "nullable": false, + "relation": "liquibase.structure.core.Table#ff23163", + "snapshotId": "ff23165", + "type": { + "columnSize": "64!{java.lang.Integer}", + "typeName": "VARCHAR" + } + } + }, + { + "column": { + "certainDataType": false, + "name": "thumbprint", + "nullable": false, + "relation": "liquibase.structure.core.Table#ff23191", + "snapshotId": "ff23193", + "type": { + "columnSize": "64!{java.lang.Integer}", + "typeName": "VARCHAR" + } + } + }, + { + "column": { + "certainDataType": false, + "name": "timestamp", + "nullable": false, + "relation": "liquibase.structure.core.Table#ff23153", + "snapshotId": "ff23156", + "type": { + "typeName": "DATETIME" + } + } + }, + { + "column": { + "certainDataType": false, + "name": "type", + "nullable": false, + "relation": "liquibase.structure.core.Table#ff23174", + "snapshotId": "ff23184", + "type": { + "columnSize": "255!{java.lang.Integer}", + "typeName": "VARCHAR" + } + } + }, + { + "column": { + "certainDataType": false, + "name": "uploader_sha256_fingerprint", + "nullable": false, + "relation": "liquibase.structure.core.Table#ff23153", + "snapshotId": "ff23158", + "type": { + "columnSize": "64!{java.lang.Integer}", + "typeName": "VARCHAR" + } + } + }, + { + "column": { + "certainDataType": false, + "name": "valid_from", + "nullable": false, + "relation": "liquibase.structure.core.Table#ff23174", + "snapshotId": "ff23180", + "type": { + "typeName": "DATETIME" + } + } + }, + { + "column": { + "certainDataType": false, + "name": "valid_to", + "nullable": false, + "relation": "liquibase.structure.core.Table#ff23174", + "snapshotId": "ff23181", + "type": { + "typeName": "DATETIME" + } + } + }, + { + "column": { + "certainDataType": false, + "name": "version", + "nullable": false, + "relation": "liquibase.structure.core.Table#ff23174", + "snapshotId": "ff23182", + "type": { + "columnSize": "30!{java.lang.Integer}", + "typeName": "VARCHAR" + } + } + } + ], + "liquibase.structure.core.Index": [ + { + "index": { + "columns": [ + "liquibase.structure.core.Column#ff23155" + ], + "name": "IX_PK_AUDIT_EVENT", + "snapshotId": "ff23154", + "table": "liquibase.structure.core.Table#ff23153", + "unique": true + } + }, + { + "index": { + "columns": [ + "liquibase.structure.core.Column#ff23167" + ], + "name": "IX_PK_SIGNER_INFORMATION", + "snapshotId": "ff23166", + "table": "liquibase.structure.core.Table#ff23163", + "unique": true + } + }, + { + "index": { + "columns": [ + "liquibase.structure.core.Column#ff23195" + ], + "name": "IX_PK_TRUSTED_PARTY", + "snapshotId": "ff23194", + "table": "liquibase.structure.core.Table#ff23191", + "unique": true + } + }, + { + "index": { + "columns": [ + "liquibase.structure.core.Column#ff23176" + ], + "name": "IX_PK_VALIDATION_RULE", + "snapshotId": "ff23175", + "table": "liquibase.structure.core.Table#ff23174", + "unique": true + } + }, + { + "index": { + "columns": [ + "liquibase.structure.core.Column#ff23188" + ], + "name": "IX_PK_VALUESET", + "snapshotId": "ff23187", + "table": "liquibase.structure.core.Table#ff23186", + "unique": true + } + } + ], + "liquibase.structure.core.PrimaryKey": [ + { + "primaryKey": { + "backingIndex": "liquibase.structure.core.Index#ff23154", + "columns": [ + "liquibase.structure.core.Column#ff23155" + ], + "name": "PK_AUDIT_EVENT", + "snapshotId": "ff23162", + "table": "liquibase.structure.core.Table#ff23153" + } + }, + { + "primaryKey": { + "backingIndex": "liquibase.structure.core.Index#ff23166", + "columns": [ + "liquibase.structure.core.Column#ff23167" + ], + "name": "PK_SIGNER_INFORMATION", + "snapshotId": "ff23173", + "table": "liquibase.structure.core.Table#ff23163" + } + }, + { + "primaryKey": { + "backingIndex": "liquibase.structure.core.Index#ff23194", + "columns": [ + "liquibase.structure.core.Column#ff23195" + ], + "name": "PK_TRUSTED_PARTY", + "snapshotId": "ff23201", + "table": "liquibase.structure.core.Table#ff23191" + } + }, + { + "primaryKey": { + "backingIndex": "liquibase.structure.core.Index#ff23175", + "columns": [ + "liquibase.structure.core.Column#ff23176" + ], + "name": "PK_VALIDATION_RULE", + "snapshotId": "ff23185", + "table": "liquibase.structure.core.Table#ff23174" + } + }, + { + "primaryKey": { + "backingIndex": "liquibase.structure.core.Index#ff23187", + "columns": [ + "liquibase.structure.core.Column#ff23188" + ], + "name": "PK_VALUESET", + "snapshotId": "ff23190", + "table": "liquibase.structure.core.Table#ff23186" + } + } + ], + "liquibase.structure.core.Schema": [ + { + "schema": { + "catalog": "liquibase.structure.core.Catalog#ff23152", + "default": true, + "name": "JPA_BUDDY", + "snapshotId": "ff23151" + } + } + ], + "liquibase.structure.core.Table": [ + { + "table": { + "columns": [ + "liquibase.structure.core.Column#ff23155", + "liquibase.structure.core.Column#ff23156", + "liquibase.structure.core.Column#ff23157", + "liquibase.structure.core.Column#ff23158", + "liquibase.structure.core.Column#ff23159", + "liquibase.structure.core.Column#ff23160", + "liquibase.structure.core.Column#ff23161" + ], + "indexes": [ + "liquibase.structure.core.Index#ff23154" + ], + "name": "audit_event", + "primaryKey": "liquibase.structure.core.PrimaryKey#ff23162", + "schema": "liquibase.structure.core.Schema#ff23151", + "snapshotId": "ff23153" + } + }, + { + "table": { + "columns": [ + "liquibase.structure.core.Column#ff23167", + "liquibase.structure.core.Column#ff23168", + "liquibase.structure.core.Column#ff23169", + "liquibase.structure.core.Column#ff23165", + "liquibase.structure.core.Column#ff23170", + "liquibase.structure.core.Column#ff23171", + "liquibase.structure.core.Column#ff23172" + ], + "indexes": [ + "liquibase.structure.core.Index#ff23166" + ], + "name": "signer_information", + "primaryKey": "liquibase.structure.core.PrimaryKey#ff23173", + "schema": "liquibase.structure.core.Schema#ff23151", + "snapshotId": "ff23163", + "uniqueConstraints": [ + "liquibase.structure.core.UniqueConstraint#ff23164" + ] + } + }, + { + "table": { + "columns": [ + "liquibase.structure.core.Column#ff23195", + "liquibase.structure.core.Column#ff23196", + "liquibase.structure.core.Column#ff23197", + "liquibase.structure.core.Column#ff23193", + "liquibase.structure.core.Column#ff23198", + "liquibase.structure.core.Column#ff23199", + "liquibase.structure.core.Column#ff23200" + ], + "indexes": [ + "liquibase.structure.core.Index#ff23194" + ], + "name": "trusted_party", + "primaryKey": "liquibase.structure.core.PrimaryKey#ff23201", + "schema": "liquibase.structure.core.Schema#ff23151", + "snapshotId": "ff23191", + "uniqueConstraints": [ + "liquibase.structure.core.UniqueConstraint#ff23192" + ] + } + }, + { + "table": { + "columns": [ + "liquibase.structure.core.Column#ff23176", + "liquibase.structure.core.Column#ff23177", + "liquibase.structure.core.Column#ff23178", + "liquibase.structure.core.Column#ff23179", + "liquibase.structure.core.Column#ff23180", + "liquibase.structure.core.Column#ff23181", + "liquibase.structure.core.Column#ff23182", + "liquibase.structure.core.Column#ff23183", + "liquibase.structure.core.Column#ff23184" + ], + "indexes": [ + "liquibase.structure.core.Index#ff23175" + ], + "name": "validation_rule", + "primaryKey": "liquibase.structure.core.PrimaryKey#ff23185", + "schema": "liquibase.structure.core.Schema#ff23151", + "snapshotId": "ff23174" + } + }, + { + "table": { + "columns": [ + "liquibase.structure.core.Column#ff23188", + "liquibase.structure.core.Column#ff23189" + ], + "indexes": [ + "liquibase.structure.core.Index#ff23187" + ], + "name": "valueset", + "primaryKey": "liquibase.structure.core.PrimaryKey#ff23190", + "schema": "liquibase.structure.core.Schema#ff23151", + "snapshotId": "ff23186" + } + } + ], + "liquibase.structure.core.UniqueConstraint": [ + { + "uniqueConstraint": { + "clustered": false, + "columns": [ + "liquibase.structure.core.Column#ff23165" + ], + "deferrable": false, + "disabled": false, + "initiallyDeferred": false, + "name": "UC_SIGNER_INFORMATION_THUMBPRINT", + "snapshotId": "ff23164", + "table": "liquibase.structure.core.Table#ff23163", + "validate": true + } + }, + { + "uniqueConstraint": { + "clustered": false, + "columns": [ + "liquibase.structure.core.Column#ff23193" + ], + "deferrable": false, + "disabled": false, + "initiallyDeferred": false, + "name": "UC_TRUSTED_PARTY_THUMBPRINT", + "snapshotId": "ff23192", + "table": "liquibase.structure.core.Table#ff23191", + "validate": true + } + } + ] + }, + "snapshotControl": { + "snapshotControl": { + "includedType": [ + "liquibase.structure.core.Catalog", + "liquibase.structure.core.Column", + "liquibase.structure.core.ForeignKey", + "liquibase.structure.core.Index", + "liquibase.structure.core.PrimaryKey", + "liquibase.structure.core.Schema", + "liquibase.structure.core.Sequence", + "liquibase.structure.core.Table", + "liquibase.structure.core.UniqueConstraint", + "liquibase.structure.core.View" + ] + } + } + } +} diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml new file mode 100644 index 00000000..8a6a2211 --- /dev/null +++ b/src/main/resources/logback-spring.xml @@ -0,0 +1,57 @@ + + + + + + + + + DEBUG + + + + + timestamp="%d{"yyyy-MM-dd'T'HH:mm:ss.SSSXXX", UTC}", level="%level", hostname="${HOSTNAME}", pid="${PID:-}", thread="%thread", class="%logger{40}", message="%replace(%replace(%m){'[\r\n]+', ', '}){'"', '\''}", trace="%X{traceId}", span="%X{spanId}", %X%n + + + utf8 + + + + + ${catalina.base:-.}/logs/dgcg.log + + ${catalina.base:-.}/logs/dgcg-%d{yyyy-MM-dd}.log + 90 + + true + true + + + + timestamp="%d{"yyyy-MM-dd'T'HH:mm:ss.SSSXXX", UTC}", level="%level", hostname="${HOSTNAME}", pid="${PID:-}", thread="%thread", class="%logger{40}", message="%replace(%replace(%m){'[\r\n]+', ','}){'"', '\''}", exception="%replace(%ex){'[\r\n]+', ', '}", trace="%X{traceId}", span="%X{spanId}", %X%n%nopex + + + utf8 + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/validation-rule.schema.json b/src/main/resources/validation-rule.schema.json new file mode 100644 index 00000000..454740ef --- /dev/null +++ b/src/main/resources/validation-rule.schema.json @@ -0,0 +1,146 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://webgate.acceptance.ec.europa.eu/dgcg-json-api/validation-rule.schema.json", + "title": "EU DCC Validation Rule", + "description": "Rule to validate an issued EU Digital Covid Certificate.", + "type": "object", + "additionalProperties": false, + "required": [ + "AffectedFields", + "Country", + "CertificateType", + "Description", + "Engine", + "EngineVersion", + "Identifier", + "Logic", + "SchemaVersion", + "Type", + "ValidFrom", + "ValidTo", + "Version" + ], + "properties": { + "Identifier": { + "type": "string", + "description": "The unique rule name", + "pattern": "^(GR|VR|TR|RR|IR)-[A-Z]{2}-\\d{4}$" + }, + "Type": { + "type": "string", + "description": "Type of the rule", + "enum": [ + "Acceptance", + "Invalidation" + ] + }, + "Country": { + "type": "string", + "description": "ISO Country Code of rule owner", + "pattern": "^[A-Z]{2}$" + }, + "Region": { + "type": "string", + "description": "Additional Region property to precise country property.", + "pattern": "^[A-Z0-9]{0,5}$" + }, + "Version": { + "type": "string", + "description": "Version of the rule (Semver)", + "pattern": "^\\d+\\.\\d+\\.\\d+$" + }, + "SchemaVersion": { + "type": "string", + "description": "Version of the used schema (Semver)", + "pattern": "^\\d+\\.\\d+\\.\\d+$" + }, + "Engine": { + "type": "string", + "description": "Type of the RuleEngine" + }, + "EngineVersion": { + "type": "string", + "description": "Version of the used engine (Semver)", + "pattern": "^\\d+\\.\\d+\\.\\d+$" + }, + "CertificateType": { + "type": "string", + "description": "Type of the certificate", + "enum": [ + "General", + "Test", + "Vaccination", + "Recovery" + ] + }, + "Description": { + "type": "array", + "description": "Array of human readable description of the rule", + "items": { + "type": "object", + "required": [ + "lang", + "desc" + ], + "properties": { + "lang": { + "type": "string", + "description": "Language of the description", + "pattern": "^([a-z]{2}|[a-z]{2}-[a-z]{2})$" + }, + "desc": { + "type": "string", + "description": "Description of this rule in specified language", + "minLength": 20 + } + } + }, + "contains": { + "type": "object", + "required": [ + "lang", + "desc" + ], + "properties": { + "lang": { + "type": "string", + "description": "Language of the description", + "pattern": "^en$" + }, + "desc": { + "type": "string", + "description": "Human readable description of this rule in English language", + "minLength": 20 + } + } + }, + "minItems": 1 + }, + "ValidFrom": { + "type": "string", + "description": "Start validity of the rule as ISO 8601 Timestamp (without ms, with timezone)", + "format": "date-time", + "pattern": "^\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d([-+][0-2]\\d:[0-5]\\d|Z)$" + }, + "ValidTo": { + "type": "string", + "description": "End validity of the rule as ISO 8601 Timestamp (without ms, with timezone)", + "format": "date-time", + "pattern": "^\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d([-+][0-2]\\d:[0-5]\\d|Z)$" + }, + "AffectedFields": { + "type": "array", + "description": "Fields of the payload which are used by the rule.", + "items": { + "type": "string", + "description": "Affected field of payload" + }, + "minItems": 1 + }, + "Logic": { + "type": "object", + "description": "The logic payload in JSON", + "minProperties": 1 + } + } +} \ No newline at end of file diff --git a/src/test/java/eu/europa/ec/dgc/gateway/restapi/controller/CountryListIntegrationTest.java b/src/test/java/eu/europa/ec/dgc/gateway/restapi/controller/CountryListIntegrationTest.java new file mode 100644 index 00000000..3b6ac86c --- /dev/null +++ b/src/test/java/eu/europa/ec/dgc/gateway/restapi/controller/CountryListIntegrationTest.java @@ -0,0 +1,88 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.restapi.controller; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import eu.europa.ec.dgc.gateway.config.DgcConfigProperties; +import eu.europa.ec.dgc.gateway.entity.TrustedPartyEntity; +import eu.europa.ec.dgc.gateway.repository.TrustedPartyRepository; +import eu.europa.ec.dgc.gateway.testdata.TrustedPartyTestHelper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +@SpringBootTest +@AutoConfigureMockMvc +class CountryListIntegrationTest { + + @Autowired + TrustedPartyTestHelper trustedPartyTestHelper; + + @Autowired + TrustedPartyRepository trustedPartyRepository; + + @Autowired + DgcConfigProperties dgcConfigProperties; + + private static final String countryCode = "EU"; + private static final String authCertSubject = "C=EU"; + + @Autowired + private MockMvc mockMvc; + + @BeforeEach + void testData() { + trustedPartyRepository.deleteAll(); + } + + @Test + void testGetTrustedParties() throws Exception { + // Insert some Certificates for random Countries + trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, "AA"); + trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, "AB"); + trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, "AC"); + trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, "AD"); + + String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode); + + mockMvc.perform(get("/countrylist") + .accept(MediaType.APPLICATION_JSON_VALUE) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.length()").value(5)) + .andExpect(jsonPath("$[0]").value("AA")) + .andExpect(jsonPath("$[1]").value("AB")) + .andExpect(jsonPath("$[2]").value("AC")) + .andExpect(jsonPath("$[3]").value("AD")) + .andExpect(jsonPath("$[4]").value(countryCode)); + } +} diff --git a/src/test/java/eu/europa/ec/dgc/gateway/restapi/controller/SignerCertificateIntegrationTest.java b/src/test/java/eu/europa/ec/dgc/gateway/restapi/controller/SignerCertificateIntegrationTest.java new file mode 100644 index 00000000..c49bf4db --- /dev/null +++ b/src/test/java/eu/europa/ec/dgc/gateway/restapi/controller/SignerCertificateIntegrationTest.java @@ -0,0 +1,326 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.restapi.controller; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import eu.europa.ec.dgc.gateway.config.DgcConfigProperties; +import eu.europa.ec.dgc.gateway.entity.SignerInformationEntity; +import eu.europa.ec.dgc.gateway.entity.TrustedPartyEntity; +import eu.europa.ec.dgc.gateway.repository.AuditEventRepository; +import eu.europa.ec.dgc.gateway.repository.SignerInformationRepository; +import eu.europa.ec.dgc.gateway.testdata.CertificateTestUtils; +import eu.europa.ec.dgc.gateway.testdata.DgcTestKeyStore; +import eu.europa.ec.dgc.gateway.testdata.TrustedPartyTestHelper; +import eu.europa.ec.dgc.signing.SignedCertificateMessageBuilder; +import eu.europa.ec.dgc.signing.SignedCertificateMessageParser; +import eu.europa.ec.dgc.utils.CertificateUtils; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.Base64; +import java.util.Optional; +import org.bouncycastle.cert.X509CertificateHolder; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.web.servlet.MockMvc; + +@SpringBootTest +@AutoConfigureMockMvc +class SignerCertificateIntegrationTest { + + @Autowired + DgcConfigProperties dgcConfigProperties; + + @Autowired + CertificateUtils certificateUtils; + + @Autowired + DgcTestKeyStore dgcTestKeyStore; + + @Autowired + TrustedPartyTestHelper trustedPartyTestHelper; + + @Autowired + SignerInformationRepository signerInformationRepository; + + @Autowired + AuditEventRepository auditEventRepository; + @Autowired + private MockMvc mockMvc; + + private static final String countryCode = "EU"; + private static final String authCertSubject = "C=" + countryCode; + + @Test + void testSuccessfulUpload() throws Exception { + long signerInformationEntitiesInDb = signerInformationRepository.count(); + long auditEventEntitiesInDb = auditEventRepository.count(); + X509Certificate signerCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + PrivateKey signerPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + + X509Certificate cscaCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.CSCA, countryCode); + PrivateKey cscaPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.CSCA, countryCode); + + KeyPair payloadKeyPair = KeyPairGenerator.getInstance("ec").generateKeyPair(); + X509Certificate payloadCertificate = CertificateTestUtils.generateCertificate(payloadKeyPair, countryCode, "Payload Cert", cscaCertificate, cscaPrivateKey); + + String payload = new SignedCertificateMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey) + .withPayload(new X509CertificateHolder(payloadCertificate.getEncoded())) + .buildAsString(); + + // immediately parse the message to get the signature from the signed message + String signature = new SignedCertificateMessageParser(payload).getSignature(); + + String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode); + + mockMvc.perform(post("/signerCertificate/") + .content(payload) + .contentType("application/cms") + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isCreated()); + + Assertions.assertEquals(signerInformationEntitiesInDb + 1, signerInformationRepository.count()); + Optional createdSignerInformationEntity = + signerInformationRepository.getFirstByThumbprint(certificateUtils.getCertThumbprint(payloadCertificate)); + + Assertions.assertTrue(createdSignerInformationEntity.isPresent()); + + Assertions.assertEquals(auditEventEntitiesInDb+1, auditEventRepository.count()); + Assertions.assertEquals(SignerInformationEntity.CertificateType.DSC, createdSignerInformationEntity.get().getCertificateType()); + Assertions.assertEquals(countryCode, createdSignerInformationEntity.get().getCountry()); + Assertions.assertEquals(signature, createdSignerInformationEntity.get().getSignature()); + Assertions.assertEquals(Base64.getEncoder().encodeToString(payloadCertificate.getEncoded()), createdSignerInformationEntity.get().getRawData()); + } + + @Test + void testUploadFailedConflict() throws Exception { + long signerInformationEntitiesInDb = signerInformationRepository.count(); + long auditEventEntitiesInDb = auditEventRepository.count(); + + X509Certificate signerCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + PrivateKey signerPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + + X509Certificate cscaCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.CSCA, countryCode); + PrivateKey cscaPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.CSCA, countryCode); + + KeyPair payloadKeyPair = KeyPairGenerator.getInstance("ec").generateKeyPair(); + X509Certificate payloadCertificate = CertificateTestUtils.generateCertificate(payloadKeyPair, countryCode, "Payload Cert", cscaCertificate, cscaPrivateKey); + + String payload = new SignedCertificateMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey) + .withPayload(new X509CertificateHolder(payloadCertificate.getEncoded())) + .buildAsString(); + + String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode); + + mockMvc.perform(post("/signerCertificate/") + .content(payload) + .contentType("application/cms") + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isCreated()); + + mockMvc.perform(post("/signerCertificate/") + .content(payload) + .contentType("application/cms") + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isConflict()); + Assertions.assertEquals(auditEventEntitiesInDb+1, auditEventRepository.count()); + Assertions.assertEquals(signerInformationEntitiesInDb + 1, signerInformationRepository.count()); + } + + @Test + void testUploadFailedInvalidCSCA() throws Exception { + long signerInformationEntitiesInDb = signerInformationRepository.count(); + long auditEventEntitiesInDb = auditEventRepository.count(); + + X509Certificate signerCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + PrivateKey signerPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + + // sign with TrustAnchor + X509Certificate cscaCertificate = dgcTestKeyStore.getTrustAnchor(); + PrivateKey cscaPrivateKey = dgcTestKeyStore.getTrustAnchorPrivateKey(); + + KeyPair payloadKeyPair = KeyPairGenerator.getInstance("ec").generateKeyPair(); + X509Certificate payloadCertificate = CertificateTestUtils.generateCertificate(payloadKeyPair, countryCode, "Payload Cert", cscaCertificate, cscaPrivateKey); + + String payload = new SignedCertificateMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey) + .withPayload(new X509CertificateHolder(payloadCertificate.getEncoded())) + .buildAsString(); + + String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode); + + mockMvc.perform(post("/signerCertificate/") + .content(payload) + .contentType("application/cms") + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isBadRequest()); + Assertions.assertEquals(auditEventEntitiesInDb+1, auditEventRepository.count()); + Assertions.assertEquals(signerInformationEntitiesInDb, signerInformationRepository.count()); + } + + @Test + void testUploadFailedInvalidCSCAWrongCountryCode() throws Exception { + long signerInformationEntitiesInDb = signerInformationRepository.count(); + long auditEventEntitiesInDb = auditEventRepository.count(); + + X509Certificate signerCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + PrivateKey signerPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + + // sign with CSCA from another country + X509Certificate cscaCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.CSCA, "XX"); + PrivateKey cscaPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.CSCA, "XX"); + + + KeyPair payloadKeyPair = KeyPairGenerator.getInstance("ec").generateKeyPair(); + X509Certificate payloadCertificate = CertificateTestUtils.generateCertificate(payloadKeyPair, countryCode, "Payload Cert", cscaCertificate, cscaPrivateKey); + + String payload = new SignedCertificateMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey) + .withPayload(new X509CertificateHolder(payloadCertificate.getEncoded())) + .buildAsString(); + + String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode); + + mockMvc.perform(post("/signerCertificate/") + .content(payload) + .contentType("application/cms") + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isBadRequest()); + Assertions.assertEquals(auditEventEntitiesInDb+1, auditEventRepository.count()); + Assertions.assertEquals(signerInformationEntitiesInDb, signerInformationRepository.count()); + } + + @Test + void testUploadFailedPayloadCertCountryWrong() throws Exception { + long signerInformationEntitiesInDb = signerInformationRepository.count(); + long auditEventEntitiesInDb = auditEventRepository.count(); + + X509Certificate signerCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + PrivateKey signerPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + + X509Certificate cscaCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.CSCA, countryCode); + PrivateKey cscaPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.CSCA, countryCode); + + KeyPair payloadKeyPair = KeyPairGenerator.getInstance("ec").generateKeyPair(); + X509Certificate payloadCertificate = CertificateTestUtils.generateCertificate(payloadKeyPair, "XX", "Payload Cert", cscaCertificate, cscaPrivateKey); + + String payload = new SignedCertificateMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey) + .withPayload(new X509CertificateHolder(payloadCertificate.getEncoded())) + .buildAsString(); + + String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode); + + mockMvc.perform(post("/signerCertificate/") + .content(payload) + .contentType("application/cms") + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isBadRequest()); + Assertions.assertEquals(auditEventEntitiesInDb+1, auditEventRepository.count()); + Assertions.assertEquals(signerInformationEntitiesInDb, signerInformationRepository.count()); + } + + @Test + void testUploadFailedWrongSignerCertificate() throws Exception { + long signerInformationEntitiesInDb = signerInformationRepository.count(); + long auditEventEntitiesInDb = auditEventRepository.count(); + + X509Certificate signerCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, "XX"); + PrivateKey signerPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.UPLOAD, "XX"); + + X509Certificate cscaCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.CSCA, countryCode); + PrivateKey cscaPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.CSCA, countryCode); + + KeyPair payloadKeyPair = KeyPairGenerator.getInstance("ec").generateKeyPair(); + X509Certificate payloadCertificate = CertificateTestUtils.generateCertificate(payloadKeyPair, countryCode, "Payload Cert", cscaCertificate, cscaPrivateKey); + + String payload = new SignedCertificateMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey) + .withPayload(new X509CertificateHolder(payloadCertificate.getEncoded())) + .buildAsString(); + + String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode); + + mockMvc.perform(post("/signerCertificate/") + .content(payload) + .contentType("application/cms") + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isBadRequest()); + Assertions.assertEquals(auditEventEntitiesInDb+1, auditEventRepository.count()); + Assertions.assertEquals(signerInformationEntitiesInDb, signerInformationRepository.count()); + } + + @Test + void testUploadFailedInvalidCmsMessage() throws Exception { + long signerInformationEntitiesInDb = signerInformationRepository.count(); + + X509Certificate signerCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + PrivateKey signerPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + + X509Certificate cscaCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.CSCA, countryCode); + PrivateKey cscaPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.CSCA, countryCode); + + KeyPair payloadKeyPair = KeyPairGenerator.getInstance("ec").generateKeyPair(); + X509Certificate payloadCertificate = CertificateTestUtils.generateCertificate(payloadKeyPair, countryCode, "Payload Cert", cscaCertificate, cscaPrivateKey); + + String payload = new SignedCertificateMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey) + .withPayload(new X509CertificateHolder(payloadCertificate.getEncoded())) + .buildAsString(); + + // randomly play a little bit inside the base64 string + payload = payload.replace(payload.substring(10, 50), payload.substring(80, 120)); + + String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode); + + mockMvc.perform(post("/signerCertificate/") + .content(payload) + .contentType("application/cms") + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isBadRequest()); + Assertions.assertEquals(signerInformationEntitiesInDb, signerInformationRepository.count()); + } + +} diff --git a/src/test/java/eu/europa/ec/dgc/gateway/restapi/controller/TrustListIntegrationTest.java b/src/test/java/eu/europa/ec/dgc/gateway/restapi/controller/TrustListIntegrationTest.java new file mode 100644 index 00000000..40affd9a --- /dev/null +++ b/src/test/java/eu/europa/ec/dgc/gateway/restapi/controller/TrustListIntegrationTest.java @@ -0,0 +1,490 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.restapi.controller; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import eu.europa.ec.dgc.gateway.config.DgcConfigProperties; +import eu.europa.ec.dgc.gateway.entity.SignerInformationEntity; +import eu.europa.ec.dgc.gateway.entity.TrustedPartyEntity; +import eu.europa.ec.dgc.gateway.repository.SignerInformationRepository; +import eu.europa.ec.dgc.gateway.repository.TrustedPartyRepository; +import eu.europa.ec.dgc.gateway.restapi.dto.CertificateTypeDto; +import eu.europa.ec.dgc.gateway.restapi.dto.TrustListDto; +import eu.europa.ec.dgc.gateway.testdata.CertificateTestUtils; +import eu.europa.ec.dgc.gateway.testdata.DgcTestKeyStore; +import eu.europa.ec.dgc.gateway.testdata.TrustedPartyTestHelper; +import eu.europa.ec.dgc.utils.CertificateUtils; +import java.io.UnsupportedEncodingException; +import java.security.KeyPairGenerator; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.time.ZonedDateTime; +import java.util.Base64; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; + +@SpringBootTest +@AutoConfigureMockMvc +class TrustListIntegrationTest { + + @Autowired + SignerInformationRepository signerInformationRepository; + + @Autowired + TrustedPartyRepository trustedPartyRepository; + + @Autowired + TrustedPartyTestHelper trustedPartyTestHelper; + + @Autowired + DgcConfigProperties dgcConfigProperties; + + @Autowired + CertificateUtils certificateUtils; + + @Autowired + DgcTestKeyStore dgcTestKeyStore; + + @Autowired + private MockMvc mockMvc; + + private static final String countryCode = "EU"; + private static final String authCertSubject = "C=" + countryCode; + + X509Certificate certUploadDe, certUploadEu, certCscaDe, certCscaEu, certAuthDe, certAuthEu, certDscDe, certDscEu; + + @BeforeEach + void testData() throws Exception { + trustedPartyRepository.deleteAll(); + signerInformationRepository.deleteAll(); + + certUploadDe = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, "DE"); + certUploadEu = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, "EU"); + certCscaDe = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.CSCA, "DE"); + certCscaEu = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.CSCA, "EU"); + certAuthDe = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.AUTHENTICATION, "DE"); + certAuthEu = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.AUTHENTICATION, "EU"); + + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("ec"); + certDscDe = CertificateTestUtils.generateCertificate(keyPairGenerator.generateKeyPair(), "DE", "Test"); + certDscEu = CertificateTestUtils.generateCertificate(keyPairGenerator.generateKeyPair(), "EU", "Test"); + + signerInformationRepository.save(new SignerInformationEntity( + null, + ZonedDateTime.now(), + "DE", + certificateUtils.getCertThumbprint(certDscDe), + Base64.getEncoder().encodeToString(certDscDe.getEncoded()), + "sig1", + SignerInformationEntity.CertificateType.DSC + )); + + signerInformationRepository.save(new SignerInformationEntity( + null, + ZonedDateTime.now(), + "EU", + certificateUtils.getCertThumbprint(certDscEu), + Base64.getEncoder().encodeToString(certDscEu.getEncoded()), + "sig2", + SignerInformationEntity.CertificateType.DSC + )); + } + + @Test + void testTrustListDownloadNoFilter() throws Exception { + String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode); + + mockMvc.perform(get("/trustList") + .accept(MediaType.APPLICATION_JSON_VALUE) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(c -> assertTrustListItem(c, certDscDe, "DE", CertificateTypeDto.DSC, "sig1")) + .andExpect(c -> assertTrustListItem(c, certDscEu, "EU", CertificateTypeDto.DSC, "sig2")) + .andExpect(c -> assertTrustListItem(c, certCscaDe, "DE", CertificateTypeDto.CSCA, null)) + .andExpect(c -> assertTrustListItem(c, certCscaEu, "EU", CertificateTypeDto.CSCA, null)) + .andExpect(c -> assertTrustListItem(c, certUploadDe, "DE", CertificateTypeDto.UPLOAD, null)) + .andExpect(c -> assertTrustListItem(c, certUploadEu, "EU", CertificateTypeDto.UPLOAD, null)) + .andExpect(c -> assertTrustListItem(c, certAuthDe, "DE", CertificateTypeDto.AUTHENTICATION, null)) + .andExpect(c -> assertTrustListItem(c, certAuthEu, "EU", CertificateTypeDto.AUTHENTICATION, null)) + .andExpect(c -> assertTrustListLength(c, 8)); + } + + @Test + void testTrustListDownloadFilterByType() throws Exception { + String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode); + + mockMvc.perform(get("/trustList/AUTHENTICATION") + .accept(MediaType.APPLICATION_JSON_VALUE) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(c -> assertTrustListItem(c, certAuthDe, "DE", CertificateTypeDto.AUTHENTICATION, null)) + .andExpect(c -> assertTrustListItem(c, certAuthEu, "EU", CertificateTypeDto.AUTHENTICATION, null)) + .andExpect(c -> assertTrustListLength(c, 2)); + + mockMvc.perform(get("/trustList/UPLOAD") + .accept(MediaType.APPLICATION_JSON_VALUE) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(c -> assertTrustListItem(c, certUploadDe, "DE", CertificateTypeDto.UPLOAD, null)) + .andExpect(c -> assertTrustListItem(c, certUploadEu, "EU", CertificateTypeDto.UPLOAD, null)) + .andExpect(c -> assertTrustListLength(c, 2)); + + mockMvc.perform(get("/trustList/CSCA") + .accept(MediaType.APPLICATION_JSON_VALUE) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(c -> assertTrustListItem(c, certCscaDe, "DE", CertificateTypeDto.CSCA, null)) + .andExpect(c -> assertTrustListItem(c, certCscaEu, "EU", CertificateTypeDto.CSCA, null)) + .andExpect(c -> assertTrustListLength(c, 2)); + + mockMvc.perform(get("/trustList/DSC") + .accept(MediaType.APPLICATION_JSON_VALUE) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(c -> assertTrustListItem(c, certDscDe, "DE", CertificateTypeDto.DSC, "sig1")) + .andExpect(c -> assertTrustListItem(c, certDscEu, "EU", CertificateTypeDto.DSC, "sig2")) + .andExpect(c -> assertTrustListLength(c, 2)); + } + + @Test + void testTrustListDownloadFilterByTypeCaseInsensitive() throws Exception { + String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode); + + mockMvc.perform(get("/trustList/aUtHeNtiCaTiOn") + .accept(MediaType.APPLICATION_JSON_VALUE) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(c -> assertTrustListItem(c, certAuthDe, "DE", CertificateTypeDto.AUTHENTICATION, null)) + .andExpect(c -> assertTrustListItem(c, certAuthEu, "EU", CertificateTypeDto.AUTHENTICATION, null)) + .andExpect(c -> assertTrustListLength(c, 2)); + + mockMvc.perform(get("/trustList/uploAd") + .accept(MediaType.APPLICATION_JSON_VALUE) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(c -> assertTrustListItem(c, certUploadDe, "DE", CertificateTypeDto.UPLOAD, null)) + .andExpect(c -> assertTrustListItem(c, certUploadEu, "EU", CertificateTypeDto.UPLOAD, null)) + .andExpect(c -> assertTrustListLength(c, 2)); + + mockMvc.perform(get("/trustList/csCA") + .accept(MediaType.APPLICATION_JSON_VALUE) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(c -> assertTrustListItem(c, certCscaDe, "DE", CertificateTypeDto.CSCA, null)) + .andExpect(c -> assertTrustListItem(c, certCscaEu, "EU", CertificateTypeDto.CSCA, null)) + .andExpect(c -> assertTrustListLength(c, 2)); + + mockMvc.perform(get("/trustList/dsc") + .accept(MediaType.APPLICATION_JSON_VALUE) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(c -> assertTrustListItem(c, certDscDe, "DE", CertificateTypeDto.DSC, "sig1")) + .andExpect(c -> assertTrustListItem(c, certDscEu, "EU", CertificateTypeDto.DSC, "sig2")) + .andExpect(c -> assertTrustListLength(c, 2)); + } + + @Test + void testTrustListDownloadFilterByTypeAndCountry() throws Exception { + String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode); + + mockMvc.perform(get("/trustList/AUTHENTICATION/DE") + .accept(MediaType.APPLICATION_JSON_VALUE) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(c -> assertTrustListItem(c, certAuthDe, "DE", CertificateTypeDto.AUTHENTICATION, null)) + .andExpect(c -> assertTrustListLength(c, 1)); + + mockMvc.perform(get("/trustList/AUTHENTICATION/EU") + .accept(MediaType.APPLICATION_JSON_VALUE) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(c -> assertTrustListItem(c, certAuthEu, "EU", CertificateTypeDto.AUTHENTICATION, null)) + .andExpect(c -> assertTrustListLength(c, 1)); + + mockMvc.perform(get("/trustList/UPLOAD/DE") + .accept(MediaType.APPLICATION_JSON_VALUE) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(c -> assertTrustListItem(c, certUploadDe, "DE", CertificateTypeDto.UPLOAD, null)) + .andExpect(c -> assertTrustListLength(c, 1)); + + mockMvc.perform(get("/trustList/UPLOAD/EU") + .accept(MediaType.APPLICATION_JSON_VALUE) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(c -> assertTrustListItem(c, certUploadEu, "EU", CertificateTypeDto.UPLOAD, null)) + .andExpect(c -> assertTrustListLength(c, 1)); + + mockMvc.perform(get("/trustList/CSCA/DE") + .accept(MediaType.APPLICATION_JSON_VALUE) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(c -> assertTrustListItem(c, certCscaDe, "DE", CertificateTypeDto.CSCA, null)) + .andExpect(c -> assertTrustListLength(c, 1)); + + mockMvc.perform(get("/trustList/CSCA/EU") + .accept(MediaType.APPLICATION_JSON_VALUE) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(c -> assertTrustListItem(c, certCscaEu, "EU", CertificateTypeDto.CSCA, null)) + .andExpect(c -> assertTrustListLength(c, 1)); + + mockMvc.perform(get("/trustList/DSC/DE") + .accept(MediaType.APPLICATION_JSON_VALUE) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(c -> assertTrustListItem(c, certDscDe, "DE", CertificateTypeDto.DSC, "sig1")) + .andExpect(c -> assertTrustListLength(c, 1)); + + mockMvc.perform(get("/trustList/DSC/EU") + .accept(MediaType.APPLICATION_JSON_VALUE) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(c -> assertTrustListItem(c, certDscEu, "EU", CertificateTypeDto.DSC, "sig2")) + .andExpect(c -> assertTrustListLength(c, 1)); + } + + @Test + void testTrustListDownloadFilterByTypeAndCountryLowercase() throws Exception { + String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode); + + mockMvc.perform(get("/trustList/AUTHENTICATION/de") + .accept(MediaType.APPLICATION_JSON_VALUE) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(c -> assertTrustListItem(c, certAuthDe, "DE", CertificateTypeDto.AUTHENTICATION, null)) + .andExpect(c -> assertTrustListLength(c, 1)); + + mockMvc.perform(get("/trustList/AUTHENTICATION/eu") + .accept(MediaType.APPLICATION_JSON_VALUE) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(c -> assertTrustListItem(c, certAuthEu, "EU", CertificateTypeDto.AUTHENTICATION, null)) + .andExpect(c -> assertTrustListLength(c, 1)); + + mockMvc.perform(get("/trustList/UPLOAD/de") + .accept(MediaType.APPLICATION_JSON_VALUE) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(c -> assertTrustListItem(c, certUploadDe, "DE", CertificateTypeDto.UPLOAD, null)) + .andExpect(c -> assertTrustListLength(c, 1)); + + mockMvc.perform(get("/trustList/UPLOAD/eu") + .accept(MediaType.APPLICATION_JSON_VALUE) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(c -> assertTrustListItem(c, certUploadEu, "EU", CertificateTypeDto.UPLOAD, null)) + .andExpect(c -> assertTrustListLength(c, 1)); + + mockMvc.perform(get("/trustList/CSCA/de") + .accept(MediaType.APPLICATION_JSON_VALUE) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(c -> assertTrustListItem(c, certCscaDe, "DE", CertificateTypeDto.CSCA, null)) + .andExpect(c -> assertTrustListLength(c, 1)); + + mockMvc.perform(get("/trustList/CSCA/eu") + .accept(MediaType.APPLICATION_JSON_VALUE) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(c -> assertTrustListItem(c, certCscaEu, "EU", CertificateTypeDto.CSCA, null)) + .andExpect(c -> assertTrustListLength(c, 1)); + + mockMvc.perform(get("/trustList/DSC/de") + .accept(MediaType.APPLICATION_JSON_VALUE) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(c -> assertTrustListItem(c, certDscDe, "DE", CertificateTypeDto.DSC, "sig1")) + .andExpect(c -> assertTrustListLength(c, 1)); + + mockMvc.perform(get("/trustList/DSC/eu") + .accept(MediaType.APPLICATION_JSON_VALUE) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(c -> assertTrustListItem(c, certDscEu, "EU", CertificateTypeDto.DSC, "sig2")) + .andExpect(c -> assertTrustListLength(c, 1)); + } + + @Test + void testTrustListDownloadEmptyList() throws Exception { + String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode); + + signerInformationRepository.deleteAll(); + + mockMvc.perform(get("/trustList/DSC") + .accept(MediaType.APPLICATION_JSON_VALUE) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json("[]")); + } + + @Test + void testTrustListWrongType() throws Exception { + String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode); + + mockMvc.perform(get("/trustList/XXX") + .accept(MediaType.APPLICATION_JSON_VALUE) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isBadRequest()); + } + + @Test + void testTrustListWrongCountryCode() throws Exception { + String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode); + + mockMvc.perform(get("/trustList/DSC/XXX") + .accept(MediaType.APPLICATION_JSON_VALUE) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isBadRequest()); + } + + private void assertTrustListItem(MvcResult result, X509Certificate certificate, String country, CertificateTypeDto certificateTypeDto, String signature) throws CertificateEncodingException, UnsupportedEncodingException, JsonProcessingException { + ObjectMapper objectMapper = new ObjectMapper() + .registerModule(new JavaTimeModule()); + List trustList = objectMapper.readValue(result.getResponse().getContentAsString(), new TypeReference<>() { + }); + + Optional trustListOptional = trustList + .stream() + .filter(tl -> tl.getKid().equals(certificateUtils.getCertKid(certificate))) + .findFirst(); + + Assertions.assertTrue(trustListOptional.isPresent()); + + TrustListDto trustListItem = trustListOptional.get(); + + Assertions.assertEquals(certificateUtils.getCertKid(certificate), trustListItem.getKid()); + Assertions.assertEquals(country, trustListItem.getCountry()); + Assertions.assertEquals(certificateTypeDto, trustListItem.getCertificateType()); + Assertions.assertEquals(certificateUtils.getCertThumbprint(certificate), trustListItem.getThumbprint()); + Assertions.assertEquals(Base64.getEncoder().encodeToString(certificate.getEncoded()), trustListItem.getRawData()); + + if (signature != null) { + Assertions.assertEquals(signature, trustListItem.getSignature()); + } + } + + private void assertTrustListLength(MvcResult result, int expectedLength) throws UnsupportedEncodingException, JsonProcessingException { + ObjectMapper objectMapper = new ObjectMapper() + .registerModule(new JavaTimeModule()); + List trustList = objectMapper.readValue(result.getResponse().getContentAsString(), new TypeReference<>() { + }); + Assertions.assertEquals(expectedLength, trustList.size()); + } +} diff --git a/src/test/java/eu/europa/ec/dgc/gateway/restapi/controller/ValidationRuleIntegrationTest.java b/src/test/java/eu/europa/ec/dgc/gateway/restapi/controller/ValidationRuleIntegrationTest.java new file mode 100644 index 00000000..ea759c3c --- /dev/null +++ b/src/test/java/eu/europa/ec/dgc/gateway/restapi/controller/ValidationRuleIntegrationTest.java @@ -0,0 +1,1215 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.restapi.controller; + +import static eu.europa.ec.dgc.gateway.testdata.CertificateTestUtils.assertEquals; +import static eu.europa.ec.dgc.gateway.testdata.CertificateTestUtils.getDummyValidationRule; +import static java.time.format.DateTimeFormatter.ISO_LOCAL_DATE; +import static java.time.temporal.ChronoField.HOUR_OF_DAY; +import static java.time.temporal.ChronoField.MINUTE_OF_HOUR; +import static java.time.temporal.ChronoField.SECOND_OF_MINUTE; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.datatype.jsr310.ser.ZonedDateTimeSerializer; +import eu.europa.ec.dgc.gateway.config.DgcConfigProperties; +import eu.europa.ec.dgc.gateway.connector.model.ValidationRule; +import eu.europa.ec.dgc.gateway.entity.TrustedPartyEntity; +import eu.europa.ec.dgc.gateway.entity.ValidationRuleEntity; +import eu.europa.ec.dgc.gateway.repository.AuditEventRepository; +import eu.europa.ec.dgc.gateway.repository.ValidationRuleRepository; +import eu.europa.ec.dgc.gateway.testdata.TrustedPartyTestHelper; +import eu.europa.ec.dgc.signing.SignedStringMessageBuilder; +import eu.europa.ec.dgc.signing.SignedStringMessageParser; +import eu.europa.ec.dgc.utils.CertificateUtils; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.time.temporal.ChronoUnit; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +@SpringBootTest +@AutoConfigureMockMvc +@Slf4j +class ValidationRuleIntegrationTest { + + @Autowired + DgcConfigProperties dgcConfigProperties; + + @Autowired + CertificateUtils certificateUtils; + + @Autowired + TrustedPartyTestHelper trustedPartyTestHelper; + + @Autowired + AuditEventRepository auditEventRepository; + + @Autowired + ValidationRuleRepository validationRuleRepository; + + ObjectMapper objectMapper; + + @Autowired + private MockMvc mockMvc; + + private static final DateTimeFormatter formatter = new DateTimeFormatterBuilder() + .parseCaseInsensitive() + .append(ISO_LOCAL_DATE) + .appendLiteral('T') + .appendValue(HOUR_OF_DAY, 2) + .appendLiteral(':') + .appendValue(MINUTE_OF_HOUR, 2) + .optionalStart() + .appendLiteral(':') + .appendValue(SECOND_OF_MINUTE, 2) + .appendOffsetId() + .toFormatter(); + + private static final String countryCode = "EU"; + private static final String authCertSubject = "C=" + countryCode; + + @BeforeEach + public void setup() { + validationRuleRepository.deleteAll(); + auditEventRepository.deleteAll(); + + objectMapper = new ObjectMapper(); + + JavaTimeModule javaTimeModule = new JavaTimeModule(); + javaTimeModule.addSerializer(ZonedDateTime.class, new ZonedDateTimeSerializer( + new DateTimeFormatterBuilder().appendPattern("yyyy-MM-dd'T'HH:mm:ssXXX").toFormatter() + )); + + objectMapper.registerModule(javaTimeModule); + objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + + } + + @Test + void testSuccessfulUpload() throws Exception { + long validationRulesInDb = validationRuleRepository.count(); + long auditEventEntitiesInDb = auditEventRepository.count(); + + X509Certificate signerCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + PrivateKey signerPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + + ValidationRule validationRule = getDummyValidationRule(); + + String payload = new SignedStringMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey) + .withPayload(objectMapper.writeValueAsString(validationRule)) + .buildAsString(); + + String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode); + + mockMvc.perform(post("/rules") + .content(payload) + .contentType("application/cms") + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isCreated()); + + Assertions.assertEquals(validationRulesInDb + 1, validationRuleRepository.count()); + Optional createdValidationRule = + validationRuleRepository.getByRuleIdAndVersion(validationRule.getIdentifier(), validationRule.getVersion()); + + Assertions.assertTrue(createdValidationRule.isPresent()); + + Assertions.assertEquals(auditEventEntitiesInDb + 1, auditEventRepository.count()); + Assertions.assertEquals(validationRule.getValidFrom().toEpochSecond(), createdValidationRule.get().getValidFrom().toEpochSecond()); + Assertions.assertEquals(validationRule.getValidTo().toEpochSecond(), createdValidationRule.get().getValidTo().toEpochSecond()); + Assertions.assertEquals(validationRule.getCountry(), createdValidationRule.get().getCountry()); + Assertions.assertEquals(validationRule.getType().toUpperCase(Locale.ROOT), createdValidationRule.get().getValidationRuleType().toString()); + + SignedStringMessageParser parser = new SignedStringMessageParser(createdValidationRule.get().getCms()); + ValidationRule parsedValidationRule = objectMapper.readValue(parser.getPayload(), ValidationRule.class); + + assertEquals(validationRule, parsedValidationRule); + } + + @Test + void testSuccessfulUploadWithoutRegionProperty() throws Exception { + long validationRulesInDb = validationRuleRepository.count(); + long auditEventEntitiesInDb = auditEventRepository.count(); + + X509Certificate signerCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + PrivateKey signerPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + + ValidationRule validationRule = getDummyValidationRule(); + validationRule.setRegion(null); + String json = objectMapper.writeValueAsString(validationRule); + json = json.replace("\"Region\":null,", ""); + + String payload = new SignedStringMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey) + .withPayload(json) + .buildAsString(); + + String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode); + + mockMvc.perform(post("/rules") + .content(payload) + .contentType("application/cms") + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isCreated()); + + Assertions.assertEquals(validationRulesInDb + 1, validationRuleRepository.count()); + Optional createdValidationRule = + validationRuleRepository.getByRuleIdAndVersion(validationRule.getIdentifier(), validationRule.getVersion()); + + Assertions.assertTrue(createdValidationRule.isPresent()); + + Assertions.assertEquals(auditEventEntitiesInDb + 1, auditEventRepository.count()); + Assertions.assertEquals(validationRule.getValidFrom().toEpochSecond(), createdValidationRule.get().getValidFrom().toEpochSecond()); + Assertions.assertEquals(validationRule.getValidTo().toEpochSecond(), createdValidationRule.get().getValidTo().toEpochSecond()); + Assertions.assertEquals(validationRule.getCountry(), createdValidationRule.get().getCountry()); + Assertions.assertEquals(validationRule.getType().toUpperCase(Locale.ROOT), createdValidationRule.get().getValidationRuleType().toString()); + + SignedStringMessageParser parser = new SignedStringMessageParser(createdValidationRule.get().getCms()); + ValidationRule parsedValidationRule = objectMapper.readValue(parser.getPayload(), ValidationRule.class); + + assertEquals(validationRule, parsedValidationRule); + } + + @Test + void testInputOnlyContainsJson() throws Exception { + long validationRulesInDb = validationRuleRepository.count(); + + X509Certificate signerCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + PrivateKey signerPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + + ValidationRule validationRule = getDummyValidationRule(); + + String payload = new SignedStringMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey) + .withPayload(objectMapper.writeValueAsString(validationRule) + "\n" + objectMapper.writeValueAsString(validationRule)) + .buildAsString(); + + String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode); + + mockMvc.perform(post("/rules") + .content(payload) + .contentType("application/cms") + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isBadRequest()); + + payload = new SignedStringMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey) + .withPayload(objectMapper.writeValueAsString(validationRule) + "x") + .buildAsString(); + + authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode); + + mockMvc.perform(post("/rules") + .content(payload) + .contentType("application/cms") + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isBadRequest()); + + Assertions.assertEquals(validationRulesInDb, validationRuleRepository.count()); + } + + @Test + void testJsonSchemaValidation() throws Exception { + X509Certificate signerCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + PrivateKey signerPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode); + + Map invalidValidationRules = new HashMap<>(); + + ValidationRule validationRule = getDummyValidationRule(); + validationRule.setIdentifier("XXXXXXXX"); + invalidValidationRules.put("Invalid ID Pattern", validationRule); + + validationRule = getDummyValidationRule(); + validationRule.setType("XXXXX"); + invalidValidationRules.put("Invalid Type", validationRule); + + validationRule = getDummyValidationRule(); + validationRule.setCountry("EUX"); + invalidValidationRules.put("Invalid Country Pattern", validationRule); + + validationRule = getDummyValidationRule(); + validationRule.setRegion("XXXXXX"); + invalidValidationRules.put("Invalid Region Pattern", validationRule); + + validationRule = getDummyValidationRule(); + validationRule.setVersion("1.0.0.0"); + invalidValidationRules.put("Invalid Version Pattern", validationRule); + + validationRule = getDummyValidationRule(); + validationRule.setSchemaVersion("1.0.0.0"); + invalidValidationRules.put("Invalid Schema Version Pattern", validationRule); + + validationRule = getDummyValidationRule(); + validationRule.setEngineVersion("1.2.3.aaaaa"); + invalidValidationRules.put("Invalid EngineVersion Pattern", validationRule); + + validationRule = getDummyValidationRule(); + validationRule.setDescription(List.of(new ValidationRule.DescriptionItem("xx", "1".repeat(20)))); + invalidValidationRules.put("Missing Description EN", validationRule); + + validationRule = getDummyValidationRule(); + validationRule.getDescription().get(0).setDescription("shorttext"); + invalidValidationRules.put("Description to short", validationRule); + + validationRule = getDummyValidationRule(); + validationRule.setAffectedFields(Collections.emptyList()); + invalidValidationRules.put("AffectedFields No Values", validationRule); + + validationRule = getDummyValidationRule(); + validationRule.setLogic(JsonNodeFactory.instance.objectNode()); + invalidValidationRules.put("Logic Empty", validationRule); + + for (String ruleKey : invalidValidationRules.keySet()) { + log.info("JSON Schema Check: {}", ruleKey); + + String payload = new SignedStringMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey) + .withPayload(objectMapper.writeValueAsString(invalidValidationRules.get(ruleKey))) + .buildAsString(); + + mockMvc.perform(post("/rules") + .content(payload) + .contentType("application/cms") + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value("0x200")); + } + } + + @Test + void testValidationCountry() throws Exception { + X509Certificate signerCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + PrivateKey signerPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode); + + ValidationRule validationRule = getDummyValidationRule(); + validationRule.setIdentifier("GR-DE-0001"); + validationRule.setCountry("DE"); + + String payload = new SignedStringMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey) + .withPayload(objectMapper.writeValueAsString(validationRule)) + .buildAsString(); + + mockMvc.perform(post("/rules") + .content(payload) + .contentType("application/cms") + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.code").value("0x210")); + + validationRule.setCountry("EU"); + + payload = new SignedStringMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey) + .withPayload(objectMapper.writeValueAsString(validationRule)) + .buildAsString(); + + mockMvc.perform(post("/rules") + .content(payload) + .contentType("application/cms") + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.code").value("0x210")); + } + + @Test + void testValidationVersion() throws Exception { + X509Certificate signerCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + PrivateKey signerPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + + ValidationRule validationRule = getDummyValidationRule(); + + String payload = new SignedStringMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey) + .withPayload(objectMapper.writeValueAsString(validationRule)) + .buildAsString(); + + String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode); + + mockMvc.perform(post("/rules") + .content(payload) + .contentType("application/cms") + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isCreated()); + + mockMvc.perform(post("/rules") + .content(payload) + .contentType("application/cms") + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value("0x220")); + + validationRule.setVersion("0.9.0"); + + payload = new SignedStringMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey) + .withPayload(objectMapper.writeValueAsString(validationRule)) + .buildAsString(); + + mockMvc.perform(post("/rules") + .content(payload) + .contentType("application/cms") + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value("0x220")); + } + + @Test + void testValidationUploadCert() throws Exception { + ValidationRule validationRule = getDummyValidationRule(); + + String payload = new SignedStringMessageBuilder() + .withSigningCertificate( + certificateUtils.convertCertificate(trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.CSCA, "EU")), + trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.CSCA, "EU")) + .withPayload(objectMapper.writeValueAsString(validationRule)) + .buildAsString(); + + String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode); + + mockMvc.perform(post("/rules") + .content(payload) + .contentType("application/cms") + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value("0x230")); + } + + @Test + void testValidationTimestamps() throws Exception { + X509Certificate signerCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + PrivateKey signerPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode); + + ValidationRule validationRule = getDummyValidationRule(); + validationRule.setValidFrom(ZonedDateTime.now().plus(1, ChronoUnit.DAYS)); + validationRule.setValidTo(ZonedDateTime.now().minus(1, ChronoUnit.DAYS)); + + String payload = new SignedStringMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey) + .withPayload(objectMapper.writeValueAsString(validationRule)) + .buildAsString(); + + mockMvc.perform(post("/rules") + .content(payload) + .contentType("application/cms") + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value("0x240")); + + validationRule = getDummyValidationRule(); + + payload = new SignedStringMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey) + .withPayload(objectMapper.writeValueAsString(validationRule)) + .buildAsString(); + + mockMvc.perform(post("/rules") + .content(payload) + .contentType("application/cms") + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isCreated()); + + validationRule.setVersion("1.0.1"); + validationRule.setValidFrom(validationRule.getValidFrom().minus(1, ChronoUnit.SECONDS)); + + payload = new SignedStringMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey) + .withPayload(objectMapper.writeValueAsString(validationRule)) + .buildAsString(); + + mockMvc.perform(post("/rules") + .content(payload) + .contentType("application/cms") + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value("0x240")); + + validationRule.setValidFrom(ZonedDateTime.now().plus(4, ChronoUnit.WEEKS)); + + payload = new SignedStringMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey) + .withPayload(objectMapper.writeValueAsString(validationRule)) + .buildAsString(); + + mockMvc.perform(post("/rules") + .content(payload) + .contentType("application/cms") + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value("0x240")); + } + + @Test + void testValidationTimestamps2() throws Exception { + X509Certificate signerCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + PrivateKey signerPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode); + + ValidationRule validationRule = getDummyValidationRule(); + validationRule.setIdentifier("IR-EU-0001"); + validationRule.setType("Invalidation"); + validationRule.setValidFrom(ZonedDateTime.now().plus(1, ChronoUnit.SECONDS)); + + String payload = new SignedStringMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey) + .withPayload(objectMapper.writeValueAsString(validationRule)) + .buildAsString(); + + mockMvc.perform(post("/rules") + .content(payload) + .contentType("application/cms") + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isCreated()); + + validationRule = getDummyValidationRule(); + validationRule.setValidFrom(ZonedDateTime.now()); + + payload = new SignedStringMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey) + .withPayload(objectMapper.writeValueAsString(validationRule)) + .buildAsString(); + + mockMvc.perform(post("/rules") + .content(payload) + .contentType("application/cms") + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value("0x240")); + } + + @Test + void testValidationTimestamps3() throws Exception { + X509Certificate signerCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + PrivateKey signerPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode); + + ValidationRule validationRule = getDummyValidationRule(); + validationRule.setValidFrom(ZonedDateTime.now().plus(3, ChronoUnit.DAYS)); + validationRule.setValidTo(ZonedDateTime.now() + .plus(6, ChronoUnit.DAYS) + .minus(1, ChronoUnit.SECONDS)); + + String payload = new SignedStringMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey) + .withPayload(objectMapper.writeValueAsString(validationRule)) + .buildAsString(); + + mockMvc.perform(post("/rules") + .content(payload) + .contentType("application/cms") + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value("0x240")); + } + + @Test + void testValidationRuleId() throws Exception { + X509Certificate signerCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + PrivateKey signerPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode); + + ValidationRule validationRule = getDummyValidationRule(); + validationRule.setIdentifier("GR-EU-0001"); + validationRule.setType("Invalidation"); + + String payload = new SignedStringMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey) + .withPayload(objectMapper.writeValueAsString(validationRule)) + .buildAsString(); + + mockMvc.perform(post("/rules") + .content(payload) + .contentType("application/cms") + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value("0x250")); + + validationRule.setIdentifier("IR-EU-0001"); + validationRule.setType("Acceptance"); + + payload = new SignedStringMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey) + .withPayload(objectMapper.writeValueAsString(validationRule)) + .buildAsString(); + + mockMvc.perform(post("/rules") + .content(payload) + .contentType("application/cms") + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value("0x250")); + + validationRule.setIdentifier("IR-EU-0001"); + validationRule.setType("Invalidation"); + + payload = new SignedStringMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey) + .withPayload(objectMapper.writeValueAsString(validationRule)) + .buildAsString(); + + mockMvc.perform(post("/rules") + .content(payload) + .contentType("application/cms") + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isCreated()); + + validationRule.setIdentifier("GR-EU-0001"); + validationRule.setType("Acceptance"); + + payload = new SignedStringMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey) + .withPayload(objectMapper.writeValueAsString(validationRule)) + .buildAsString(); + + mockMvc.perform(post("/rules") + .content(payload) + .contentType("application/cms") + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isCreated()); + } + + @Test + void testValidationRuleInvalidIdPrefix() throws Exception { + X509Certificate signerCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + PrivateKey signerPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode); + + ValidationRule validationRule = getDummyValidationRule(); + validationRule.setIdentifier("TR-EU-0001"); + validationRule.setCertificateType("Vaccination"); + + String payload = new SignedStringMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey) + .withPayload(objectMapper.writeValueAsString(validationRule)) + .buildAsString(); + + mockMvc.perform(post("/rules") + .content(payload) + .contentType("application/cms") + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value("0x250")); + + validationRule.setIdentifier("VR-EU-0001"); + validationRule.setCertificateType("Test"); + + payload = new SignedStringMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey) + .withPayload(objectMapper.writeValueAsString(validationRule)) + .buildAsString(); + + mockMvc.perform(post("/rules") + .content(payload) + .contentType("application/cms") + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value("0x250")); + + validationRule.setIdentifier("RR-EU-0001"); + validationRule.setCertificateType("Vaccination"); + + payload = new SignedStringMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey) + .withPayload(objectMapper.writeValueAsString(validationRule)) + .buildAsString(); + + mockMvc.perform(post("/rules") + .content(payload) + .contentType("application/cms") + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value("0x250")); + + validationRule.setIdentifier("GR-EU-0001"); + validationRule.setCertificateType("Vaccination"); + + payload = new SignedStringMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey) + .withPayload(objectMapper.writeValueAsString(validationRule)) + .buildAsString(); + + mockMvc.perform(post("/rules") + .content(payload) + .contentType("application/cms") + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value("0x250")); + } + + @Test + void testDelete() throws Exception { + long validationRulesInDb = validationRuleRepository.count(); + + X509Certificate signerCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + PrivateKey signerPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + + ValidationRule validationRule = getDummyValidationRule(); + + String payload = new SignedStringMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey) + .withPayload(objectMapper.writeValueAsString(validationRule)) + .buildAsString(); + + String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode); + + mockMvc.perform(post("/rules") + .content(payload) + .contentType("application/cms") + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isCreated()); + + validationRule.setVersion("1.0.1"); + + payload = new SignedStringMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey) + .withPayload(objectMapper.writeValueAsString(validationRule)) + .buildAsString(); + + mockMvc.perform(post("/rules") + .content(payload) + .contentType("application/cms") + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isCreated()); + + Assertions.assertEquals(validationRulesInDb + 2, validationRuleRepository.count()); + + String deletePayload = new SignedStringMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey) + .withPayload(validationRule.getIdentifier()) + .buildAsString(); + + mockMvc.perform(delete("/rules") + .content(deletePayload) + .contentType("application/cms") + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isNoContent()); + + Assertions.assertEquals(validationRulesInDb, validationRuleRepository.count()); + } + + @Test + void testDeleteFailNotFound() throws Exception { + X509Certificate signerCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + PrivateKey signerPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + + String payload = new SignedStringMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey) + .withPayload(objectMapper.writeValueAsString("IR-EU-0001")) + .buildAsString(); + + String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode); + + mockMvc.perform(delete("/rules") + .content(payload) + .contentType("application/cms") + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isNotFound()); + } + + @Test + void testDeleteFailInvalidUploadCertificate() throws Exception { + X509Certificate signerCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.CSCA, countryCode); + PrivateKey signerPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.CSCA, countryCode); + + String payload = new SignedStringMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey) + .withPayload(objectMapper.writeValueAsString("IR-EU-0001")) + .buildAsString(); + + String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode); + + mockMvc.perform(delete("/rules") + .content(payload) + .contentType("application/cms") + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value("0x230")); + } + + @Test + void testDeleteFailInvalidIdString() throws Exception { + X509Certificate signerCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + PrivateKey signerPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + + String payload = new SignedStringMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey) + .withPayload(objectMapper.writeValueAsString("XXXX-TESST-!!!!!")) + .buildAsString(); + + String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode); + + mockMvc.perform(delete("/rules") + .content(payload) + .contentType("application/cms") + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value("0x250")); + } + + @Test + void testDeleteFailInvalidCountryCode() throws Exception { + X509Certificate signerCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + PrivateKey signerPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + + String payload = new SignedStringMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey) + .withPayload(objectMapper.writeValueAsString("IR-DE-0001")) + .buildAsString(); + + String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode); + + mockMvc.perform(delete("/rules") + .content(payload) + .contentType("application/cms") + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.code").value("0x210")); + } + + @Test + void testDownloadReturnAll() throws Exception { + X509Certificate signerCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + PrivateKey signerPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + + ValidationRule validationRule1 = getDummyValidationRule(); + validationRule1.setValidFrom(ZonedDateTime.now().minus(1, ChronoUnit.DAYS)); + + String payload1 = new SignedStringMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey) + .withPayload(objectMapper.writeValueAsString(validationRule1)) + .buildAsString(); + + ValidationRuleEntity vr1 = new ValidationRuleEntity(); + vr1.setRuleId(validationRule1.getIdentifier()); + vr1.setValidationRuleType(ValidationRuleEntity.ValidationRuleType.valueOf(validationRule1.getType().toUpperCase(Locale.ROOT))); + vr1.setValidTo(validationRule1.getValidTo()); + vr1.setValidFrom(validationRule1.getValidFrom()); + vr1.setCountry(validationRule1.getCountry()); + vr1.setCms(payload1); + vr1.setVersion(validationRule1.getVersion()); + + validationRuleRepository.save(vr1); + + ValidationRule validationRule2 = getDummyValidationRule(); + validationRule2.setValidFrom(ZonedDateTime.now().plus(2, ChronoUnit.DAYS)); + validationRule2.setVersion("1.0.1"); + + String payload2 = new SignedStringMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey) + .withPayload(objectMapper.writeValueAsString(validationRule2)) + .buildAsString(); + + ValidationRuleEntity vr2 = new ValidationRuleEntity(); + vr2.setRuleId(validationRule2.getIdentifier()); + vr2.setValidationRuleType(ValidationRuleEntity.ValidationRuleType.valueOf(validationRule2.getType().toUpperCase(Locale.ROOT))); + vr2.setValidTo(validationRule2.getValidTo()); + vr2.setValidFrom(validationRule2.getValidFrom()); + vr2.setCountry(validationRule2.getCountry()); + vr2.setCms(payload2); + vr2.setVersion(validationRule2.getVersion()); + + validationRuleRepository.save(vr2); + + ValidationRule validationRule3 = getDummyValidationRule(); + validationRule3.setIdentifier("GR-EU-0002"); + + String payload3 = new SignedStringMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey) + .withPayload(objectMapper.writeValueAsString(validationRule3)) + .buildAsString(); + + ValidationRuleEntity vr3 = new ValidationRuleEntity(); + vr3.setRuleId(validationRule3.getIdentifier()); + vr3.setValidationRuleType(ValidationRuleEntity.ValidationRuleType.valueOf(validationRule3.getType().toUpperCase(Locale.ROOT))); + vr3.setValidTo(validationRule3.getValidTo()); + vr3.setValidFrom(validationRule3.getValidFrom()); + vr3.setCountry(validationRule3.getCountry()); + vr3.setCms(payload3); + vr3.setVersion(validationRule3.getVersion()); + + validationRuleRepository.save(vr3); + + + String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode); + + mockMvc.perform(get("/rules/EU") + .accept(MediaType.APPLICATION_JSON_VALUE) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.['GR-EU-0001'].length()").value(2)) + .andExpect(jsonPath("$.['GR-EU-0001'][0].version").value(vr2.getVersion())) + .andExpect(jsonPath("$.['GR-EU-0001'][0].cms").value(vr2.getCms())) + .andExpect(jsonPath("$.['GR-EU-0001'][0].validTo").value(vr2.getValidTo().format(formatter))) + .andExpect(jsonPath("$.['GR-EU-0001'][0].validFrom").value(vr2.getValidFrom().format(formatter))) + .andExpect(jsonPath("$.['GR-EU-0001'][1].version").value(vr1.getVersion())) + .andExpect(jsonPath("$.['GR-EU-0001'][1].cms").value(vr1.getCms())) + .andExpect(jsonPath("$.['GR-EU-0001'][1].validTo").value(vr1.getValidTo().format(formatter))) + .andExpect(jsonPath("$.['GR-EU-0001'][1].validFrom").value(vr1.getValidFrom().format(formatter))) + .andExpect(jsonPath("$.['GR-EU-0002'].length()").value(1)) + .andExpect(jsonPath("$.['GR-EU-0002'][0].version").value(vr3.getVersion())) + .andExpect(jsonPath("$.['GR-EU-0002'][0].cms").value(vr3.getCms())) + .andExpect(jsonPath("$.['GR-EU-0002'][0].validTo").value(vr3.getValidTo().format(formatter))) + .andExpect(jsonPath("$.['GR-EU-0002'][0].validFrom").value(vr3.getValidFrom().format(formatter))); + } + + @Test + void testDownloadReturnOnlyValid() throws Exception { + X509Certificate signerCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + PrivateKey signerPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + + ValidationRule validationRule1 = getDummyValidationRule(); + validationRule1.setValidFrom(ZonedDateTime.now().minus(4, ChronoUnit.DAYS)); + + String payload1 = new SignedStringMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey) + .withPayload(objectMapper.writeValueAsString(validationRule1)) + .buildAsString(); + + ValidationRuleEntity vr1 = new ValidationRuleEntity(); + vr1.setRuleId(validationRule1.getIdentifier()); + vr1.setValidationRuleType(ValidationRuleEntity.ValidationRuleType.valueOf(validationRule1.getType().toUpperCase(Locale.ROOT))); + vr1.setValidTo(validationRule1.getValidTo()); + vr1.setValidFrom(validationRule1.getValidFrom()); + vr1.setCountry(validationRule1.getCountry()); + vr1.setCms(payload1); + vr1.setVersion(validationRule1.getVersion()); + + validationRuleRepository.save(vr1); + + ValidationRule validationRule2 = getDummyValidationRule(); + validationRule2.setValidFrom(ZonedDateTime.now().minus(2, ChronoUnit.DAYS)); + validationRule2.setVersion("1.0.1"); + + String payload2 = new SignedStringMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey) + .withPayload(objectMapper.writeValueAsString(validationRule2)) + .buildAsString(); + + ValidationRuleEntity vr2 = new ValidationRuleEntity(); + vr2.setRuleId(validationRule2.getIdentifier()); + vr2.setValidationRuleType(ValidationRuleEntity.ValidationRuleType.valueOf(validationRule2.getType().toUpperCase(Locale.ROOT))); + vr2.setValidTo(validationRule2.getValidTo()); + vr2.setValidFrom(validationRule2.getValidFrom()); + vr2.setCountry(validationRule2.getCountry()); + vr2.setCms(payload2); + vr2.setVersion(validationRule2.getVersion()); + + validationRuleRepository.save(vr2); + + String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode); + + mockMvc.perform(get("/rules/EU") + .accept(MediaType.APPLICATION_JSON_VALUE) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.['GR-EU-0001'].length()").value(1)) + .andExpect(jsonPath("$.['GR-EU-0001'][0].version").value(vr2.getVersion())) + .andExpect(jsonPath("$.['GR-EU-0001'][0].cms").value(vr2.getCms())) + .andExpect(jsonPath("$.['GR-EU-0001'][0].validTo").value(vr2.getValidTo().format(formatter))) + .andExpect(jsonPath("$.['GR-EU-0001'][0].validFrom").value(vr2.getValidFrom().format(formatter))); + } + + @Test + void testDownloadDbContainsOnlyRulesValidInFutureShouldReturnAll() throws Exception { + X509Certificate signerCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + PrivateKey signerPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + + ValidationRule validationRule1 = getDummyValidationRule(); + validationRule1.setValidFrom(ZonedDateTime.now().plus(1, ChronoUnit.DAYS)); + + String payload1 = new SignedStringMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey) + .withPayload(objectMapper.writeValueAsString(validationRule1)) + .buildAsString(); + + ValidationRuleEntity vr1 = new ValidationRuleEntity(); + vr1.setRuleId(validationRule1.getIdentifier()); + vr1.setValidationRuleType(ValidationRuleEntity.ValidationRuleType.valueOf(validationRule1.getType().toUpperCase(Locale.ROOT))); + vr1.setValidTo(validationRule1.getValidTo()); + vr1.setValidFrom(validationRule1.getValidFrom()); + vr1.setCountry(validationRule1.getCountry()); + vr1.setCms(payload1); + vr1.setVersion(validationRule1.getVersion()); + + validationRuleRepository.save(vr1); + + ValidationRule validationRule2 = getDummyValidationRule(); + validationRule2.setValidFrom(ZonedDateTime.now().plus(2, ChronoUnit.DAYS)); + validationRule2.setVersion("1.0.1"); + + String payload2 = new SignedStringMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey) + .withPayload(objectMapper.writeValueAsString(validationRule2)) + .buildAsString(); + + ValidationRuleEntity vr2 = new ValidationRuleEntity(); + vr2.setRuleId(validationRule2.getIdentifier()); + vr2.setValidationRuleType(ValidationRuleEntity.ValidationRuleType.valueOf(validationRule2.getType().toUpperCase(Locale.ROOT))); + vr2.setValidTo(validationRule2.getValidTo()); + vr2.setValidFrom(validationRule2.getValidFrom()); + vr2.setCountry(validationRule2.getCountry()); + vr2.setCms(payload2); + vr2.setVersion(validationRule2.getVersion()); + + validationRuleRepository.save(vr2); + + ValidationRule validationRule3 = getDummyValidationRule(); + validationRule3.setValidFrom(ZonedDateTime.now().plus(3, ChronoUnit.DAYS)); + validationRule3.setVersion("1.1.0"); + + String payload3 = new SignedStringMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey) + .withPayload(objectMapper.writeValueAsString(validationRule3)) + .buildAsString(); + + ValidationRuleEntity vr3 = new ValidationRuleEntity(); + vr3.setRuleId(validationRule3.getIdentifier()); + vr3.setValidationRuleType(ValidationRuleEntity.ValidationRuleType.valueOf(validationRule3.getType().toUpperCase(Locale.ROOT))); + vr3.setValidTo(validationRule3.getValidTo()); + vr3.setValidFrom(validationRule3.getValidFrom()); + vr3.setCountry(validationRule3.getCountry()); + vr3.setCms(payload3); + vr3.setVersion(validationRule3.getVersion()); + + validationRuleRepository.save(vr3); + + + String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode); + + mockMvc.perform(get("/rules/EU") + .accept(MediaType.APPLICATION_JSON_VALUE) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.['GR-EU-0001'].length()").value(3)) + .andExpect(jsonPath("$.['GR-EU-0001'][0].version").value(vr3.getVersion())) + .andExpect(jsonPath("$.['GR-EU-0001'][0].cms").value(vr3.getCms())) + .andExpect(jsonPath("$.['GR-EU-0001'][0].validTo").value(vr3.getValidTo().format(formatter))) + .andExpect(jsonPath("$.['GR-EU-0001'][0].validFrom").value(vr3.getValidFrom().format(formatter))) + .andExpect(jsonPath("$.['GR-EU-0001'][1].version").value(vr2.getVersion())) + .andExpect(jsonPath("$.['GR-EU-0001'][1].cms").value(vr2.getCms())) + .andExpect(jsonPath("$.['GR-EU-0001'][1].validTo").value(vr2.getValidTo().format(formatter))) + .andExpect(jsonPath("$.['GR-EU-0001'][1].validFrom").value(vr2.getValidFrom().format(formatter))) + .andExpect(jsonPath("$.['GR-EU-0001'][2].version").value(vr1.getVersion())) + .andExpect(jsonPath("$.['GR-EU-0001'][2].cms").value(vr1.getCms())) + .andExpect(jsonPath("$.['GR-EU-0001'][2].validTo").value(vr1.getValidTo().format(formatter))) + .andExpect(jsonPath("$.['GR-EU-0001'][2].validFrom").value(vr1.getValidFrom().format(formatter))); + + } + + @Test + void testDeleteAliasEndpoint() throws Exception { + long validationRulesInDb = validationRuleRepository.count(); + + X509Certificate signerCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + PrivateKey signerPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + + ValidationRule validationRule = getDummyValidationRule(); + + String payload = new SignedStringMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey) + .withPayload(objectMapper.writeValueAsString(validationRule)) + .buildAsString(); + + String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode); + + mockMvc.perform(post("/rules") + .content(payload) + .contentType("application/cms") + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isCreated()); + + validationRule.setVersion("1.0.1"); + + payload = new SignedStringMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey) + .withPayload(objectMapper.writeValueAsString(validationRule)) + .buildAsString(); + + mockMvc.perform(post("/rules") + .content(payload) + .contentType("application/cms") + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isCreated()); + + Assertions.assertEquals(validationRulesInDb + 2, validationRuleRepository.count()); + + String deletePayload = new SignedStringMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey) + .withPayload(validationRule.getIdentifier()) + .buildAsString(); + + mockMvc.perform(post("/rules/delete") + .content(deletePayload) + .contentType("application/cms") + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isNoContent()); + + Assertions.assertEquals(validationRulesInDb, validationRuleRepository.count()); + } + + @Test + void testSuccessfulUploadWithOldContentType() throws Exception { + long validationRulesInDb = validationRuleRepository.count(); + long auditEventEntitiesInDb = auditEventRepository.count(); + + X509Certificate signerCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + PrivateKey signerPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + + ValidationRule validationRule = getDummyValidationRule(); + + String payload = new SignedStringMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey) + .withPayload(objectMapper.writeValueAsString(validationRule)) + .buildAsString(); + + String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode); + + mockMvc.perform(post("/rules") + .content(payload) + .contentType("application/cms-text") + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isCreated()); + + Assertions.assertEquals(validationRulesInDb + 1, validationRuleRepository.count()); + Optional createdValidationRule = + validationRuleRepository.getByRuleIdAndVersion(validationRule.getIdentifier(), validationRule.getVersion()); + + Assertions.assertTrue(createdValidationRule.isPresent()); + + Assertions.assertEquals(auditEventEntitiesInDb + 1, auditEventRepository.count()); + Assertions.assertEquals(validationRule.getValidFrom().toEpochSecond(), createdValidationRule.get().getValidFrom().toEpochSecond()); + Assertions.assertEquals(validationRule.getValidTo().toEpochSecond(), createdValidationRule.get().getValidTo().toEpochSecond()); + Assertions.assertEquals(validationRule.getCountry(), createdValidationRule.get().getCountry()); + Assertions.assertEquals(validationRule.getType().toUpperCase(Locale.ROOT), createdValidationRule.get().getValidationRuleType().toString()); + + SignedStringMessageParser parser = new SignedStringMessageParser(createdValidationRule.get().getCms()); + ValidationRule parsedValidationRule = objectMapper.readValue(parser.getPayload(), ValidationRule.class); + + assertEquals(validationRule, parsedValidationRule); + } +} diff --git a/src/test/java/eu/europa/ec/dgc/gateway/restapi/controller/ValuesetIntegrationTest.java b/src/test/java/eu/europa/ec/dgc/gateway/restapi/controller/ValuesetIntegrationTest.java new file mode 100644 index 00000000..9d5e40c9 --- /dev/null +++ b/src/test/java/eu/europa/ec/dgc/gateway/restapi/controller/ValuesetIntegrationTest.java @@ -0,0 +1,137 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.restapi.controller; + +import static org.hamcrest.Matchers.equalTo; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import eu.europa.ec.dgc.gateway.config.DgcConfigProperties; +import eu.europa.ec.dgc.gateway.entity.TrustedPartyEntity; +import eu.europa.ec.dgc.gateway.entity.ValuesetEntity; +import eu.europa.ec.dgc.gateway.repository.ValuesetRepository; +import eu.europa.ec.dgc.gateway.testdata.TrustedPartyTestHelper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +@SpringBootTest +@AutoConfigureMockMvc +class ValuesetIntegrationTest { + + @Autowired + ValuesetRepository valuesetRepository; + + @Autowired + TrustedPartyTestHelper trustedPartyTestHelper; + + @Autowired + DgcConfigProperties dgcConfigProperties; + + @Autowired + private MockMvc mockMvc; + + private static final String countryCode = "EU"; + private static final String authCertSubject = "C=" + countryCode; + + private static final ValuesetEntity valuesetEntity1 = + new ValuesetEntity("vs-dummy-1", "{ \"key1\": \"content1\" }"); + private static final ValuesetEntity valuesetEntity2 = + new ValuesetEntity("vs-dummy-2", "{ \"key2\": \"content2\" }"); + private static final ValuesetEntity valuesetEntity3 = + new ValuesetEntity("vs-dummy-3", "{ \"key3\": \"content3\" }"); + + @BeforeEach + void testData() { + valuesetRepository.deleteAll(); + + valuesetRepository.save(valuesetEntity1); + valuesetRepository.save(valuesetEntity2); + valuesetRepository.save(valuesetEntity3); + } + + @Test + void testGetValuesetIds() throws Exception { + String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode); + + mockMvc.perform(get("/valuesets") + .accept(MediaType.APPLICATION_JSON_VALUE) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.length()").value(equalTo(3))) + .andExpect(jsonPath("$[0]").value(equalTo(valuesetEntity1.getId()))) + .andExpect(jsonPath("$[1]").value(equalTo(valuesetEntity2.getId()))) + .andExpect(jsonPath("$[2]").value(equalTo(valuesetEntity3.getId()))); + } + + @Test + void testGetValueset() throws Exception { + String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode); + + mockMvc.perform(get("/valuesets/" + valuesetEntity1.getId()) + .accept(MediaType.APPLICATION_JSON_VALUE) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.key1").value(equalTo("content1"))); + + mockMvc.perform(get("/valuesets/" + valuesetEntity2.getId()) + .accept(MediaType.APPLICATION_JSON_VALUE) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.key2").value(equalTo("content2"))); + + mockMvc.perform(get("/valuesets/" + valuesetEntity3.getId()) + .accept(MediaType.APPLICATION_JSON_VALUE) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.key3").value(equalTo("content3"))); + } + + @Test + void testGetValuesetNotFound() throws Exception { + String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode); + + mockMvc.perform(get("/valuesets/randomId") + .accept(MediaType.APPLICATION_JSON_VALUE) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isNotFound()); + } +} diff --git a/src/test/java/eu/europa/ec/dgc/gateway/restapi/filter/CertAuthFilterTest.java b/src/test/java/eu/europa/ec/dgc/gateway/restapi/filter/CertAuthFilterTest.java new file mode 100644 index 00000000..83e1c195 --- /dev/null +++ b/src/test/java/eu/europa/ec/dgc/gateway/restapi/filter/CertAuthFilterTest.java @@ -0,0 +1,213 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.restapi.filter; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import eu.europa.ec.dgc.gateway.config.DgcConfigProperties; +import eu.europa.ec.dgc.gateway.entity.TrustedPartyEntity; +import eu.europa.ec.dgc.gateway.testdata.TrustedPartyTestHelper; +import java.math.BigInteger; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.web.servlet.MockMvc; + +@SpringBootTest +@AutoConfigureMockMvc +class CertAuthFilterTest { + + @Autowired + private DgcConfigProperties properties; + + @Autowired + private TrustedPartyTestHelper trustedPartyTestHelper; + + @Autowired + MockMvc mockMvc; + + private final String countryCode = "EU"; + private final String authDn = "C=" + countryCode; + + @Test + void testRequestShouldFailIfDNHeaderIsMissing() throws Exception { + String certHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode); + + mockMvc.perform(post("/signerCertificate/") + .contentType("application/cms") + .header(properties.getCertAuth().getHeaderFields().getThumbprint(), certHash) + ).andExpect(status().isUnauthorized()); + } + + @Test + void testRequestShouldFailIfThumbprintHeaderIsMissing() throws Exception { + mockMvc.perform(post("/signerCertificate/") + .contentType("application/cms") + .header(properties.getCertAuth().getHeaderFields().getDistinguishedName(), authDn) + ).andExpect(status().isUnauthorized()); + } + + @Test + void testRequestShouldFailIfCertHeadersAreMissing() throws Exception { + mockMvc.perform(post("/signerCertificate/") + .contentType("application/cms") + ).andExpect(status().isUnauthorized()); + } + + @Test + void testRequestShouldFailIfCertIsNotOnWhitelist() throws Exception { + mockMvc.perform(post("/signerCertificate/") + .contentType("application/cms") + .header(properties.getCertAuth().getHeaderFields().getThumbprint(), "randomString") + .header(properties.getCertAuth().getHeaderFields().getDistinguishedName(), authDn) + ).andExpect(status().isUnauthorized()); + } + + @Test + void testFilterShouldAppendCountryAndThumbprintToRequestObject() throws Exception { + String certHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode); + + mockMvc.perform(post("/signerCertificate/") + .contentType("application/cms") + .header(properties.getCertAuth().getHeaderFields().getThumbprint(), certHash) + .header(properties.getCertAuth().getHeaderFields().getDistinguishedName(), authDn) + ).andExpect(mvcResult -> { + Assertions.assertEquals("EU", mvcResult.getRequest().getAttribute(CertificateAuthenticationFilter.REQUEST_PROP_COUNTRY)); + Assertions.assertEquals( + certHash, + mvcResult.getRequest().getAttribute(CertificateAuthenticationFilter.REQUEST_PROP_THUMBPRINT) + ); + }); + } + + @Test + void testFilterShouldDecodeDnString() throws Exception { + String encodedDnString = "ST%3dSome-State%2c%20C%3dEU%2c%20O%3dInternet%20Widgits%20Pty%20Ltd%2c%20CN%3dTest%20Cert"; + + String certHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode); + + mockMvc.perform(post("/signerCertificate/") + .contentType("application/cms") + .header(properties.getCertAuth().getHeaderFields().getThumbprint(), certHash) + .header(properties.getCertAuth().getHeaderFields().getDistinguishedName(), encodedDnString) + ).andExpect(mvcResult -> { + Assertions.assertEquals("EU", mvcResult.getRequest().getAttribute(CertificateAuthenticationFilter.REQUEST_PROP_COUNTRY)); + Assertions.assertEquals( + certHash, + mvcResult.getRequest().getAttribute(CertificateAuthenticationFilter.REQUEST_PROP_THUMBPRINT) + ); + }); + } + + @Test + void testFilterShouldDecodeBase64AndUrlEncodedCertThumbprint() throws Exception { + String certHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode); + byte[] certHashBytes = new BigInteger(certHash, 16).toByteArray(); + + if (certHashBytes[0] == 0) { + byte[] truncatedCertHashBytes = new byte[certHashBytes.length - 1]; + System.arraycopy(certHashBytes, 1, truncatedCertHashBytes, 0, truncatedCertHashBytes.length); + certHashBytes = truncatedCertHashBytes; + } + + String encodedThumbprint = + URLEncoder.encode(Base64.getEncoder().encodeToString(certHashBytes), StandardCharsets.UTF_8); + + mockMvc.perform(post("/signerCertificate/") + .contentType("application/cms") + .header(properties.getCertAuth().getHeaderFields().getThumbprint(), encodedThumbprint) + .header(properties.getCertAuth().getHeaderFields().getDistinguishedName(), "O=Test Firma GmbH,C=EU,U=,TR,TT=43") + ).andExpect(mvcResult -> { + Assertions.assertEquals("EU", mvcResult.getRequest().getAttribute(CertificateAuthenticationFilter.REQUEST_PROP_COUNTRY)); + Assertions.assertEquals( + certHash, + mvcResult.getRequest().getAttribute(CertificateAuthenticationFilter.REQUEST_PROP_THUMBPRINT) + ); + }); + } + + @Test + void testFilterShouldDecodeBase64EncodedCertThumbprint() throws Exception { + String certHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode); + byte[] certHashBytes = new BigInteger(certHash, 16).toByteArray(); + + if (certHashBytes[0] == 0) { + byte[] truncatedCertHashBytes = new byte[certHashBytes.length - 1]; + System.arraycopy(certHashBytes, 1, truncatedCertHashBytes, 0, truncatedCertHashBytes.length); + certHashBytes = truncatedCertHashBytes; + } + + String encodedThumbprint = Base64.getEncoder().encodeToString(certHashBytes); + + mockMvc.perform(post("/signerCertificate/") + .contentType("application/cms") + .header(properties.getCertAuth().getHeaderFields().getThumbprint(), encodedThumbprint) + .header(properties.getCertAuth().getHeaderFields().getDistinguishedName(), "O=Test Firma GmbH,C=EU,U=,TR,TT=43") + ).andExpect(mvcResult -> { + Assertions.assertEquals("EU", mvcResult.getRequest().getAttribute(CertificateAuthenticationFilter.REQUEST_PROP_COUNTRY)); + Assertions.assertEquals( + certHash, + mvcResult.getRequest().getAttribute(CertificateAuthenticationFilter.REQUEST_PROP_THUMBPRINT) + ); + }); + } + + + @Test + void testRequestShouldFailIfCountryIsNotPresentInDnString() throws Exception { + String certHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode); + + mockMvc.perform(post("/signerCertificate/") + .contentType("application/cms") + .header(properties.getCertAuth().getHeaderFields().getThumbprint(), certHash) + .header(properties.getCertAuth().getHeaderFields().getDistinguishedName(), "O=Test Firma GmbH,U=Abteilung XYZ,TR=test") + ).andExpect(status().isBadRequest()); + } + + @Test + void testFilterShouldFindCountryEvenOnMalformedDnString() throws Exception { + String certHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode); + + mockMvc.perform(post("/signerCertificate/") + .contentType("application/cms") + .header(properties.getCertAuth().getHeaderFields().getThumbprint(), certHash) + .header(properties.getCertAuth().getHeaderFields().getDistinguishedName(), "O=Test Firma GmbH,C=EU,U=,TR,TT=43") + ).andExpect(mvcResult -> Assertions.assertEquals("EU", mvcResult.getRequest().getAttribute(CertificateAuthenticationFilter.REQUEST_PROP_COUNTRY))); + } + + @Test + void testRequestShouldNotFailIfDnStringContainsDuplicatedKeys() throws Exception { + String certHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode); + + mockMvc.perform(post("/signerCertificate/") + .contentType("application/cms") + .header(properties.getCertAuth().getHeaderFields().getThumbprint(), certHash) + .header(properties.getCertAuth().getHeaderFields().getDistinguishedName(), "O=Test Firma GmbH,O=XXX,C=EU,U=Abteilung XYZ,TR=test") + ).andExpect(mvcResult -> Assertions.assertEquals("EU", mvcResult.getRequest().getAttribute(CertificateAuthenticationFilter.REQUEST_PROP_COUNTRY))); + } +} + diff --git a/src/test/java/eu/europa/ec/dgc/gateway/service/AuditServiceTest.java b/src/test/java/eu/europa/ec/dgc/gateway/service/AuditServiceTest.java new file mode 100644 index 00000000..35e4ebcd --- /dev/null +++ b/src/test/java/eu/europa/ec/dgc/gateway/service/AuditServiceTest.java @@ -0,0 +1,93 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.service; + +import eu.europa.ec.dgc.gateway.entity.AuditEventEntity; +import eu.europa.ec.dgc.gateway.entity.TrustedPartyEntity; +import eu.europa.ec.dgc.gateway.repository.AuditEventRepository; +import eu.europa.ec.dgc.gateway.testdata.TrustedPartyTestHelper; +import eu.europa.ec.dgc.utils.CertificateUtils; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class AuditServiceTest { + + @Autowired + AuditService auditService; + + @Autowired + AuditEventRepository auditEventRepository; + + @Autowired + TrustedPartyTestHelper trustedPartyTestHelper; + + @Autowired + CertificateUtils certificateUtils; + + private static final String countryCode = "EU"; + private static final String dummySignature = "randomStringAsSignatureWhichIsNotValidatedInServiceLevel"; + + @Test + void testSuccessfulCreateAuditEvent() { + auditEventRepository.deleteAll(); + String exampleEvent = "postVerificationInformation_ALREADY_EXIST_CHECK_FAILED"; + String exampleEventDescription = "ALREADY_EXIST_CHECK_FAILED"; + + auditService.addAuditEvent(countryCode, dummySignature, + dummySignature, exampleEvent, exampleEventDescription); + + Assertions.assertEquals(1, auditEventRepository.count()); + AuditEventEntity auditEvent = auditEventRepository.findAll().get(0); + + Assertions.assertEquals(countryCode, auditEvent.getCountry()); + Assertions.assertEquals(dummySignature, auditEvent.getAuthenticationSha256Fingerprint()); + Assertions.assertEquals(dummySignature, auditEvent.getUploaderSha256Fingerprint()); + Assertions.assertEquals(exampleEvent, auditEvent.getEvent()); + Assertions.assertEquals(exampleEventDescription, auditEvent.getDescription()); + } + + @Test + void testSuccessfulCreateAuditEventWithCertificate() throws Exception { + auditEventRepository.deleteAll(); + String exampleEvent = "postVerificationInformation_ALREADY_EXIST_CHECK_FAILED"; + String exampleEventDescription = "ALREADY_EXIST_CHECK_FAILED"; + + auditService.addAuditEvent( + countryCode, + certificateUtils.convertCertificate( + trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, countryCode)), + dummySignature, + exampleEvent, + exampleEventDescription); + + Assertions.assertEquals(1, auditEventRepository.count()); + AuditEventEntity auditEvent = auditEventRepository.findAll().get(0); + + Assertions.assertEquals(countryCode, auditEvent.getCountry()); + Assertions.assertEquals(dummySignature, auditEvent.getAuthenticationSha256Fingerprint()); + Assertions.assertEquals(trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.UPLOAD, countryCode), auditEvent.getUploaderSha256Fingerprint()); + Assertions.assertEquals(exampleEvent, auditEvent.getEvent()); + Assertions.assertEquals(exampleEventDescription, auditEvent.getDescription()); + } +} diff --git a/src/test/java/eu/europa/ec/dgc/gateway/service/RatValuesetUpdateServiceTest.java b/src/test/java/eu/europa/ec/dgc/gateway/service/RatValuesetUpdateServiceTest.java new file mode 100644 index 00000000..fc73b5e6 --- /dev/null +++ b/src/test/java/eu/europa/ec/dgc/gateway/service/RatValuesetUpdateServiceTest.java @@ -0,0 +1,475 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.service; + +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.when; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import eu.europa.ec.dgc.gateway.client.JrcClient; +import eu.europa.ec.dgc.gateway.entity.ValuesetEntity; +import eu.europa.ec.dgc.gateway.model.JrcRatValueset; +import eu.europa.ec.dgc.gateway.model.JrcRatValuesetResponse; +import eu.europa.ec.dgc.gateway.model.RatValueset; +import eu.europa.ec.dgc.gateway.model.Valueset; +import eu.europa.ec.dgc.gateway.repository.ValuesetRepository; +import feign.FeignException; +import feign.Request; +import feign.RequestTemplate; +import java.time.LocalDate; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Assertions; +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.boot.test.mock.mockito.MockBean; + +@SpringBootTest +class RatValuesetUpdateServiceTest { + + @MockBean + JrcClient jrcClientMock; + + @Autowired + RatValuesetUpdateService ratValuesetUpdateService; + + @Autowired + ValuesetRepository valuesetRepository; + + @Autowired + ValuesetService valuesetService; + + @Autowired + ObjectMapper objectMapper; + + private static final String RAT_VALUESET_ID = "covid-19-lab-test-manufacturer-and-name"; + + private RatValueset rat1, rat2; + private final static String RAT1_ID = "1234"; + private final static String RAT2_ID = "5678"; + private ValuesetEntity otherValuesetEntity, valuesetEntity; + private Valueset valueset; + private static final TypeReference> typeReference = new TypeReference<>() { + }; + public static final Request dummyRequest = + Request.create(Request.HttpMethod.GET, "url", new HashMap<>(), null, new RequestTemplate()); + + @BeforeEach + void setup() throws JsonProcessingException { + valuesetRepository.deleteAll(); + + // Create a dummy valueset which should not be touched + otherValuesetEntity = new ValuesetEntity( + "other-valueset-with-different-id", + "this-should-not-be-changes" + ); + otherValuesetEntity = valuesetRepository.save(otherValuesetEntity); + + // Create a RAT valueset which should be updated + rat1 = new RatValueset(); + rat1.setActive(true); + rat1.setVersion(ZonedDateTime.now().minus(5, ChronoUnit.DAYS)); + rat1.setDisplay("RAT 1"); + + rat2 = new RatValueset(); + rat2.setActive(true); + rat2.setVersion(ZonedDateTime.now().minus(6, ChronoUnit.DAYS)); + rat2.setDisplay("RAT 2"); + + valueset = new Valueset<>( + RAT_VALUESET_ID, + LocalDate.now().minus(1, ChronoUnit.DAYS), + Map.of( + RAT1_ID, rat1, + RAT2_ID, rat2 + ) + ); + + valuesetEntity = new ValuesetEntity( + RAT_VALUESET_ID, + objectMapper.writeValueAsString(valueset) + ); + valuesetEntity = valuesetRepository.save(valuesetEntity); + } + + @Test + void testRatValuesetUpdateActiveFalse() throws JsonProcessingException { + JrcRatValueset.Manufacturer manufacturer = new JrcRatValueset.Manufacturer(); + manufacturer.setId("1111"); + manufacturer.setCountry("eu"); + manufacturer.setName("Manufacturer Name"); + manufacturer.setWebsite("https://example.org"); + + JrcRatValueset.HscListHistory history1 = new JrcRatValueset.HscListHistory(); + history1.setInCommonList(true); + history1.setInMutualRecognition(true); + history1.setListDate(ZonedDateTime.now().minus(3, ChronoUnit.DAYS)); + + JrcRatValueset.HscListHistory history2 = new JrcRatValueset.HscListHistory(); + history2.setInCommonList(false); + history2.setInMutualRecognition(true); + history2.setListDate(ZonedDateTime.now().minus(1, ChronoUnit.DAYS)); + + JrcRatValueset jrcValueset = new JrcRatValueset(); + jrcValueset.setIdDevice(RAT1_ID); + jrcValueset.setCommercialName("New Com Name"); + jrcValueset.setManufacturer(manufacturer); + jrcValueset.setHscListHistory(List.of(history1, history2)); + + JrcRatValuesetResponse jrcResponse = new JrcRatValuesetResponse(); + jrcResponse.setExtractedOn(ZonedDateTime.now()); + jrcResponse.setDeviceList(List.of(jrcValueset)); + + when(jrcClientMock.downloadRatValues()).thenReturn(jrcResponse); + + ratValuesetUpdateService.update(); + + String updatedValuesetJson = valuesetService.getValueSetById(RAT_VALUESET_ID).orElseThrow(); + Valueset updatedValueset = objectMapper.readValue(updatedValuesetJson, typeReference); + + Assertions.assertEquals(LocalDate.now(), updatedValueset.getDate(), "Valueset Date was not updated."); + Assertions.assertEquals(2, updatedValueset.getValue().size(), "Valueset List size has been changed"); + Assertions.assertFalse(updatedValueset.getValue().get(RAT1_ID).getActive()); + Assertions.assertEquals(String.format("%s, %s", manufacturer.getName(), jrcValueset.getCommercialName()), updatedValueset.getValue().get(RAT1_ID).getDisplay()); + Assertions.assertEquals(history2.getListDate().toEpochSecond(), updatedValueset.getValue().get(RAT1_ID).getVersion().toEpochSecond()); + } + + @Test + void testRatValuesetUpdateActiveTrue() throws JsonProcessingException { + JrcRatValueset.Manufacturer manufacturer = new JrcRatValueset.Manufacturer(); + manufacturer.setId("1111"); + manufacturer.setCountry("eu"); + manufacturer.setName("Manufacturer Name"); + manufacturer.setWebsite("https://example.org"); + + JrcRatValueset.HscListHistory history1 = new JrcRatValueset.HscListHistory(); + history1.setInCommonList(false); + history1.setInMutualRecognition(true); + history1.setListDate(ZonedDateTime.now().minus(3, ChronoUnit.DAYS)); + + JrcRatValueset.HscListHistory history2 = new JrcRatValueset.HscListHistory(); + history2.setInCommonList(true); + history2.setInMutualRecognition(true); + history2.setListDate(ZonedDateTime.now().minus(1, ChronoUnit.DAYS)); + + JrcRatValueset jrcValueset = new JrcRatValueset(); + jrcValueset.setIdDevice(RAT1_ID); + jrcValueset.setCommercialName("New Com Name"); + jrcValueset.setManufacturer(manufacturer); + jrcValueset.setHscListHistory(List.of(history1, history2)); + + JrcRatValuesetResponse jrcResponse = new JrcRatValuesetResponse(); + jrcResponse.setExtractedOn(ZonedDateTime.now()); + jrcResponse.setDeviceList(List.of(jrcValueset)); + + when(jrcClientMock.downloadRatValues()).thenReturn(jrcResponse); + + ratValuesetUpdateService.update(); + + String updatedValuesetJson = valuesetService.getValueSetById(RAT_VALUESET_ID).orElseThrow(); + Valueset updatedValueset = objectMapper.readValue(updatedValuesetJson, typeReference); + + Assertions.assertEquals(LocalDate.now(), updatedValueset.getDate(), "Valueset Date was not updated."); + Assertions.assertEquals(2, updatedValueset.getValue().size(), "Valueset List size has been changed"); + Assertions.assertTrue(updatedValueset.getValue().get(RAT1_ID).getActive()); + Assertions.assertEquals(String.format("%s, %s", manufacturer.getName(), jrcValueset.getCommercialName()), updatedValueset.getValue().get(RAT1_ID).getDisplay()); + Assertions.assertEquals(history2.getListDate().toEpochSecond(), updatedValueset.getValue().get(RAT1_ID).getVersion().toEpochSecond()); + } + + @Test + void testRatValuesetInsertedIfNotExist() throws JsonProcessingException { + valuesetRepository.deleteAll(); + + JrcRatValueset.Manufacturer manufacturer = new JrcRatValueset.Manufacturer(); + manufacturer.setId("1111"); + manufacturer.setCountry("eu"); + manufacturer.setName("Manufacturer Name"); + manufacturer.setWebsite("https://example.org"); + + JrcRatValueset.HscListHistory history1 = new JrcRatValueset.HscListHistory(); + history1.setInCommonList(false); + history1.setInMutualRecognition(true); + history1.setListDate(ZonedDateTime.now().minus(3, ChronoUnit.DAYS)); + + JrcRatValueset.HscListHistory history2 = new JrcRatValueset.HscListHistory(); + history2.setInCommonList(true); + history2.setInMutualRecognition(true); + history2.setListDate(ZonedDateTime.now().minus(1, ChronoUnit.DAYS)); + + JrcRatValueset jrcValueset = new JrcRatValueset(); + jrcValueset.setIdDevice(RAT1_ID); + jrcValueset.setCommercialName("New Com Name"); + jrcValueset.setManufacturer(manufacturer); + jrcValueset.setHscListHistory(List.of(history1, history2)); + + JrcRatValuesetResponse jrcResponse = new JrcRatValuesetResponse(); + jrcResponse.setExtractedOn(ZonedDateTime.now()); + jrcResponse.setDeviceList(List.of(jrcValueset)); + + when(jrcClientMock.downloadRatValues()).thenReturn(jrcResponse); + + ratValuesetUpdateService.update(); + + String updatedValuesetJson = valuesetService.getValueSetById(RAT_VALUESET_ID).orElseThrow(); + Valueset updatedValueset = objectMapper.readValue(updatedValuesetJson, typeReference); + + Assertions.assertEquals(LocalDate.now(), updatedValueset.getDate(), "Valueset Date was not updated."); + Assertions.assertEquals(1, updatedValueset.getValue().size(), "Valueset List size has been changed"); + Assertions.assertTrue(updatedValueset.getValue().get(RAT1_ID).getActive()); + Assertions.assertEquals(String.format("%s, %s", manufacturer.getName(), jrcValueset.getCommercialName()), updatedValueset.getValue().get(RAT1_ID).getDisplay()); + Assertions.assertEquals(history2.getListDate().toEpochSecond(), updatedValueset.getValue().get(RAT1_ID).getVersion().toEpochSecond()); + } + + @Test + void testRatValuesetUpdatedIfJsonInDbIsInvalid() throws JsonProcessingException { + valuesetEntity.setJson("blablabla"); + valuesetRepository.save(valuesetEntity); + + JrcRatValueset.Manufacturer manufacturer = new JrcRatValueset.Manufacturer(); + manufacturer.setId("1111"); + manufacturer.setCountry("eu"); + manufacturer.setName("Manufacturer Name"); + manufacturer.setWebsite("https://example.org"); + + JrcRatValueset.HscListHistory history1 = new JrcRatValueset.HscListHistory(); + history1.setInCommonList(false); + history1.setInMutualRecognition(true); + history1.setListDate(ZonedDateTime.now().minus(3, ChronoUnit.DAYS)); + + JrcRatValueset.HscListHistory history2 = new JrcRatValueset.HscListHistory(); + history2.setInCommonList(true); + history2.setInMutualRecognition(true); + history2.setListDate(ZonedDateTime.now().minus(1, ChronoUnit.DAYS)); + + JrcRatValueset jrcValueset = new JrcRatValueset(); + jrcValueset.setIdDevice(RAT1_ID); + jrcValueset.setCommercialName("New Com Name"); + jrcValueset.setManufacturer(manufacturer); + jrcValueset.setHscListHistory(List.of(history1, history2)); + + JrcRatValuesetResponse jrcResponse = new JrcRatValuesetResponse(); + jrcResponse.setExtractedOn(ZonedDateTime.now()); + jrcResponse.setDeviceList(List.of(jrcValueset)); + + when(jrcClientMock.downloadRatValues()).thenReturn(jrcResponse); + + ratValuesetUpdateService.update(); + + String updatedValuesetJson = valuesetService.getValueSetById(RAT_VALUESET_ID).orElseThrow(); + Valueset updatedValueset = objectMapper.readValue(updatedValuesetJson, typeReference); + + Assertions.assertEquals(LocalDate.now(), updatedValueset.getDate(), "Valueset Date was not set."); + Assertions.assertEquals(1, updatedValueset.getValue().size()); + Assertions.assertTrue(updatedValueset.getValue().get(RAT1_ID).getActive()); + Assertions.assertEquals(String.format("%s, %s", manufacturer.getName(), jrcValueset.getCommercialName()), updatedValueset.getValue().get(RAT1_ID).getDisplay()); + Assertions.assertEquals(history2.getListDate().toEpochSecond(), updatedValueset.getValue().get(RAT1_ID).getVersion().toEpochSecond()); + } + + @Test + void testRatValuesetUpdatedSkipIfHistoryEmpty() throws JsonProcessingException { + JrcRatValueset.Manufacturer manufacturer = new JrcRatValueset.Manufacturer(); + manufacturer.setId("1111"); + manufacturer.setCountry("eu"); + manufacturer.setName("Manufacturer Name"); + manufacturer.setWebsite("https://example.org"); + + JrcRatValueset jrcValueset = new JrcRatValueset(); + jrcValueset.setIdDevice(RAT1_ID); + jrcValueset.setCommercialName("New Com Name"); + jrcValueset.setManufacturer(manufacturer); + jrcValueset.setHscListHistory(Collections.emptyList()); + + JrcRatValuesetResponse jrcResponse = new JrcRatValuesetResponse(); + jrcResponse.setExtractedOn(ZonedDateTime.now()); + jrcResponse.setDeviceList(List.of(jrcValueset)); + + when(jrcClientMock.downloadRatValues()).thenReturn(jrcResponse); + + ratValuesetUpdateService.update(); + + String updatedValuesetJson = valuesetService.getValueSetById(RAT_VALUESET_ID).orElseThrow(); + Valueset updatedValueset = objectMapper.readValue(updatedValuesetJson, typeReference); + + Assertions.assertEquals(LocalDate.now(), updatedValueset.getDate(), "Valueset Date was not set."); + Assertions.assertEquals(2, updatedValueset.getValue().size()); + Assertions.assertTrue(updatedValueset.getValue().get(RAT1_ID).getActive()); + assertEquals(rat1, updatedValueset.getValue().get(RAT1_ID)); + } + + @Test + void testRatValuesetUpdatedSkipIfHistoryNull() throws JsonProcessingException { + JrcRatValueset.Manufacturer manufacturer = new JrcRatValueset.Manufacturer(); + manufacturer.setId("1111"); + manufacturer.setCountry("eu"); + manufacturer.setName("Manufacturer Name"); + manufacturer.setWebsite("https://example.org"); + + JrcRatValueset jrcValueset = new JrcRatValueset(); + jrcValueset.setIdDevice(RAT1_ID); + jrcValueset.setCommercialName("New Com Name"); + jrcValueset.setManufacturer(manufacturer); + jrcValueset.setHscListHistory(null); + + JrcRatValuesetResponse jrcResponse = new JrcRatValuesetResponse(); + jrcResponse.setExtractedOn(ZonedDateTime.now()); + jrcResponse.setDeviceList(List.of(jrcValueset)); + + when(jrcClientMock.downloadRatValues()).thenReturn(jrcResponse); + + ratValuesetUpdateService.update(); + + String updatedValuesetJson = valuesetService.getValueSetById(RAT_VALUESET_ID).orElseThrow(); + Valueset updatedValueset = objectMapper.readValue(updatedValuesetJson, typeReference); + + Assertions.assertEquals(LocalDate.now(), updatedValueset.getDate(), "Valueset Date was not set."); + Assertions.assertEquals(2, updatedValueset.getValue().size()); + Assertions.assertTrue(updatedValueset.getValue().get(RAT1_ID).getActive()); + assertEquals(rat1, updatedValueset.getValue().get(RAT1_ID)); + } + + @Test + void testRatValuesetUpdateShouldNotUpdateWhenRequestFails() throws JsonProcessingException { + + doThrow(new FeignException.Unauthorized("", dummyRequest, null)) + .when(jrcClientMock).downloadRatValues(); + + ratValuesetUpdateService.update(); + + String updatedValuesetJson = valuesetService.getValueSetById(RAT_VALUESET_ID).orElseThrow(); + Valueset updatedValueset = objectMapper.readValue(updatedValuesetJson, typeReference); + + Assertions.assertEquals(LocalDate.now().minus(1, ChronoUnit.DAYS), updatedValueset.getDate(), "Valueset Date has been updated."); + Assertions.assertEquals(2, updatedValueset.getValue().size(), "Valueset List size has been changed"); + assertEquals(rat1, updatedValueset.getValue().get(RAT1_ID)); + assertEquals(rat2, updatedValueset.getValue().get(RAT2_ID)); + } + + @Test + void testRatValuesetUpdateLatestAllHistoryEntriesAreInFuture() throws JsonProcessingException { + JrcRatValueset.Manufacturer manufacturer = new JrcRatValueset.Manufacturer(); + manufacturer.setId("1111"); + manufacturer.setCountry("eu"); + manufacturer.setName("Manufacturer Name"); + manufacturer.setWebsite("https://example.org"); + + JrcRatValueset.HscListHistory history1 = new JrcRatValueset.HscListHistory(); + history1.setInCommonList(false); + history1.setInMutualRecognition(true); + history1.setListDate(ZonedDateTime.now().plus(3, ChronoUnit.DAYS)); + + JrcRatValueset.HscListHistory history2 = new JrcRatValueset.HscListHistory(); + history2.setInCommonList(true); + history2.setInMutualRecognition(true); + history2.setListDate(ZonedDateTime.now().plus(1, ChronoUnit.DAYS)); + + JrcRatValueset jrcValueset = new JrcRatValueset(); + jrcValueset.setIdDevice(RAT1_ID); + jrcValueset.setCommercialName("New Com Name"); + jrcValueset.setManufacturer(manufacturer); + jrcValueset.setHscListHistory(List.of(history1, history2)); + + JrcRatValuesetResponse jrcResponse = new JrcRatValuesetResponse(); + jrcResponse.setExtractedOn(ZonedDateTime.now()); + jrcResponse.setDeviceList(List.of(jrcValueset)); + + when(jrcClientMock.downloadRatValues()).thenReturn(jrcResponse); + + ratValuesetUpdateService.update(); + + String updatedValuesetJson = valuesetService.getValueSetById(RAT_VALUESET_ID).orElseThrow(); + Valueset updatedValueset = objectMapper.readValue(updatedValuesetJson, typeReference); + + Assertions.assertEquals(LocalDate.now(), updatedValueset.getDate(), "Valueset Date was not updated."); + Assertions.assertEquals(2, updatedValueset.getValue().size(), "Valueset List size has been changed"); + Assertions.assertNull(updatedValueset.getValue().get(RAT1_ID).getActive()); + Assertions.assertEquals(String.format("%s, %s", manufacturer.getName(), jrcValueset.getCommercialName()), updatedValueset.getValue().get(RAT1_ID).getDisplay()); + Assertions.assertNull(updatedValueset.getValue().get(RAT1_ID).getVersion()); + assertEquals(history1.getListDate(), updatedValueset.getValue().get(RAT1_ID).getValidUntil()); + } + + @Test + void testRatValuesetUpdateLatestHistoryEntryNotInFuture() throws JsonProcessingException { + JrcRatValueset.Manufacturer manufacturer = new JrcRatValueset.Manufacturer(); + manufacturer.setId("1111"); + manufacturer.setCountry("eu"); + manufacturer.setName("Manufacturer Name"); + manufacturer.setWebsite("https://example.org"); + + JrcRatValueset.HscListHistory history1 = new JrcRatValueset.HscListHistory(); + history1.setInCommonList(false); + history1.setInMutualRecognition(true); + history1.setListDate(ZonedDateTime.now().minus(3, ChronoUnit.DAYS)); + + JrcRatValueset.HscListHistory history2 = new JrcRatValueset.HscListHistory(); + history2.setInCommonList(true); + history2.setInMutualRecognition(true); + history2.setListDate(ZonedDateTime.now().minus(1, ChronoUnit.DAYS)); + + JrcRatValueset.HscListHistory history3 = new JrcRatValueset.HscListHistory(); + history3.setInCommonList(true); + history3.setInMutualRecognition(true); + history3.setListDate(ZonedDateTime.now().plus(1, ChronoUnit.DAYS)); + + JrcRatValueset jrcValueset = new JrcRatValueset(); + jrcValueset.setIdDevice(RAT1_ID); + jrcValueset.setCommercialName("New Com Name"); + jrcValueset.setManufacturer(manufacturer); + jrcValueset.setHscListHistory(List.of(history1, history2, history3)); + + JrcRatValuesetResponse jrcResponse = new JrcRatValuesetResponse(); + jrcResponse.setExtractedOn(ZonedDateTime.now()); + jrcResponse.setDeviceList(List.of(jrcValueset)); + + when(jrcClientMock.downloadRatValues()).thenReturn(jrcResponse); + + ratValuesetUpdateService.update(); + + String updatedValuesetJson = valuesetService.getValueSetById(RAT_VALUESET_ID).orElseThrow(); + Valueset updatedValueset = objectMapper.readValue(updatedValuesetJson, typeReference); + + Assertions.assertEquals(LocalDate.now(), updatedValueset.getDate(), "Valueset Date was not updated."); + Assertions.assertEquals(2, updatedValueset.getValue().size(), "Valueset List size has been changed"); + Assertions.assertEquals(history2.getInCommonList(), updatedValueset.getValue().get(RAT1_ID).getActive()); + Assertions.assertEquals(String.format("%s, %s", manufacturer.getName(), jrcValueset.getCommercialName()), updatedValueset.getValue().get(RAT1_ID).getDisplay()); + assertEquals(history2.getListDate(), updatedValueset.getValue().get(RAT1_ID).getVersion()); + assertEquals(history3.getListDate(), updatedValueset.getValue().get(RAT1_ID).getValidUntil()); + } + + void assertEquals(ZonedDateTime expected, ZonedDateTime given) { + Assertions.assertEquals(expected.toEpochSecond(), given.toEpochSecond()); + } + + void assertEquals(RatValueset expected, RatValueset given) { + Assertions.assertEquals(expected.getVersion().toEpochSecond(), given.getVersion().toEpochSecond()); + Assertions.assertEquals(expected.getActive(), given.getActive()); + Assertions.assertEquals(expected.getDisplay(), given.getDisplay()); + Assertions.assertEquals(expected.getLang(), given.getLang()); + Assertions.assertEquals(expected.getSystem(), given.getSystem()); + } +} diff --git a/src/test/java/eu/europa/ec/dgc/gateway/service/SignerInformationServiceTest.java b/src/test/java/eu/europa/ec/dgc/gateway/service/SignerInformationServiceTest.java new file mode 100644 index 00000000..dc134e03 --- /dev/null +++ b/src/test/java/eu/europa/ec/dgc/gateway/service/SignerInformationServiceTest.java @@ -0,0 +1,360 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.service; + +import eu.europa.ec.dgc.gateway.config.DgcConfigProperties; +import eu.europa.ec.dgc.gateway.entity.SignerInformationEntity; +import eu.europa.ec.dgc.gateway.entity.TrustedPartyEntity; +import eu.europa.ec.dgc.gateway.repository.SignerInformationRepository; +import eu.europa.ec.dgc.gateway.testdata.CertificateTestUtils; +import eu.europa.ec.dgc.gateway.testdata.DgcTestKeyStore; +import eu.europa.ec.dgc.gateway.testdata.TrustedPartyTestHelper; +import eu.europa.ec.dgc.utils.CertificateUtils; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.Base64; +import java.util.Optional; +import org.bouncycastle.cert.X509CertificateHolder; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class SignerInformationServiceTest { + + @Autowired + DgcConfigProperties dgcConfigProperties; + + @Autowired + CertificateUtils certificateUtils; + + @Autowired + DgcTestKeyStore dgcTestKeyStore; + + @Autowired + TrustedPartyTestHelper trustedPartyTestHelper; + + @Autowired + SignerInformationRepository signerInformationRepository; + + @Autowired + SignerInformationService signerInformationService; + + private static final String countryCode = "EU"; + private static final String dummySignature = "randomStringAsSignatureWhichIsNotValidatedInServiceLevel"; + + @Test + void testSuccessfulAddingNewSignerInformationAndDelete() throws Exception { + long signerInformationEntitiesInDb = signerInformationRepository.count(); + + X509Certificate signerCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + + X509Certificate cscaCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.CSCA, countryCode); + PrivateKey cscaPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.CSCA, countryCode); + + KeyPair payloadKeyPair = KeyPairGenerator.getInstance("ec").generateKeyPair(); + X509Certificate payloadCertificate = CertificateTestUtils.generateCertificate(payloadKeyPair, countryCode, "Payload Cert", cscaCertificate, cscaPrivateKey); + + signerInformationService.addSignerCertificate( + new X509CertificateHolder(payloadCertificate.getEncoded()), + new X509CertificateHolder(signerCertificate.getEncoded()), + dummySignature, + countryCode + ); + + Assertions.assertEquals(signerInformationEntitiesInDb + 1, signerInformationRepository.count()); + Optional createdSignerInformationEntity = + signerInformationRepository.getFirstByThumbprint(certificateUtils.getCertThumbprint(payloadCertificate)); + + Assertions.assertTrue(createdSignerInformationEntity.isPresent()); + + Assertions.assertEquals(SignerInformationEntity.CertificateType.DSC, createdSignerInformationEntity.get().getCertificateType()); + Assertions.assertEquals(countryCode, createdSignerInformationEntity.get().getCountry()); + Assertions.assertEquals(dummySignature, createdSignerInformationEntity.get().getSignature()); + Assertions.assertEquals(Base64.getEncoder().encodeToString(payloadCertificate.getEncoded()), createdSignerInformationEntity.get().getRawData()); + + signerInformationService.deleteSignerCertificate( + new X509CertificateHolder(payloadCertificate.getEncoded()), + new X509CertificateHolder(signerCertificate.getEncoded()), + countryCode + ); + + Optional deletedSignerInformationEntity = + signerInformationRepository.getFirstByThumbprint(certificateUtils.getCertThumbprint(payloadCertificate)); + + Assertions.assertTrue(deletedSignerInformationEntity.isEmpty()); + Assertions.assertEquals(signerInformationEntitiesInDb, signerInformationRepository.count()); + } + + @Test + void testAddingFailedConflict() throws Exception { + long signerInformationEntitiesInDb = signerInformationRepository.count(); + + X509Certificate signerCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + + X509Certificate cscaCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.CSCA, countryCode); + PrivateKey cscaPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.CSCA, countryCode); + + KeyPair payloadKeyPair = KeyPairGenerator.getInstance("ec").generateKeyPair(); + X509Certificate payloadCertificate = CertificateTestUtils.generateCertificate(payloadKeyPair, countryCode, "Payload Cert", cscaCertificate, cscaPrivateKey); + + signerInformationService.addSignerCertificate( + new X509CertificateHolder(payloadCertificate.getEncoded()), + new X509CertificateHolder(signerCertificate.getEncoded()), + dummySignature, + countryCode + ); + + try { + signerInformationService.addSignerCertificate( + new X509CertificateHolder(payloadCertificate.getEncoded()), + new X509CertificateHolder(signerCertificate.getEncoded()), + dummySignature, + countryCode + ); + } catch (SignerInformationService.SignerCertCheckException e) { + Assertions.assertEquals(SignerInformationService.SignerCertCheckException.Reason.ALREADY_EXIST_CHECK_FAILED, e.getReason()); + } + + Assertions.assertEquals(signerInformationEntitiesInDb + 1, signerInformationRepository.count()); + } + + @Test + void testAddingFailedKidConflict() throws Exception { + long signerInformationEntitiesInDb = signerInformationRepository.count(); + + X509Certificate signerCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + + X509Certificate cscaCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.CSCA, countryCode); + PrivateKey cscaPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.CSCA, countryCode); + + KeyPair payloadKeyPair = KeyPairGenerator.getInstance("ec").generateKeyPair(); + X509Certificate payloadCertificate = CertificateTestUtils.generateCertificate(payloadKeyPair, countryCode, "Payload Cert", cscaCertificate, cscaPrivateKey); + + signerInformationService.addSignerCertificate( + new X509CertificateHolder(payloadCertificate.getEncoded()), + new X509CertificateHolder(signerCertificate.getEncoded()), + dummySignature, + countryCode + ); + + Optional certInDbOptional = signerInformationRepository.getFirstByThumbprint(certificateUtils.getCertThumbprint(payloadCertificate)); + + Assertions.assertTrue(certInDbOptional.isPresent()); + + SignerInformationEntity certInDb = certInDbOptional.get(); + certInDb.setThumbprint(certInDb.getThumbprint().substring(0, 40) + "x".repeat(24)); // Generate new Hash with first 40 chars from ogirinal hash and add 24 x + + signerInformationRepository.save(certInDb); + + try { + signerInformationService.addSignerCertificate( + new X509CertificateHolder(payloadCertificate.getEncoded()), + new X509CertificateHolder(signerCertificate.getEncoded()), + dummySignature, + countryCode + ); + } catch (SignerInformationService.SignerCertCheckException e) { + Assertions.assertEquals(SignerInformationService.SignerCertCheckException.Reason.KID_CHECK_FAILED, e.getReason()); + } + + Assertions.assertEquals(signerInformationEntitiesInDb + 1, signerInformationRepository.count()); + } + + @Test + void testUploadFailedInvalidCSCA() throws Exception { + long signerInformationEntitiesInDb = signerInformationRepository.count(); + + X509Certificate signerCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + + // sign with TrustAnchor + X509Certificate cscaCertificate = dgcTestKeyStore.getTrustAnchor(); + PrivateKey cscaPrivateKey = dgcTestKeyStore.getTrustAnchorPrivateKey(); + + KeyPair payloadKeyPair = KeyPairGenerator.getInstance("ec").generateKeyPair(); + X509Certificate payloadCertificate = CertificateTestUtils.generateCertificate(payloadKeyPair, countryCode, "Payload Cert", cscaCertificate, cscaPrivateKey); + + try { + signerInformationService.addSignerCertificate( + new X509CertificateHolder(payloadCertificate.getEncoded()), + new X509CertificateHolder(signerCertificate.getEncoded()), + dummySignature, + countryCode + ); + } catch (SignerInformationService.SignerCertCheckException e) { + Assertions.assertEquals(SignerInformationService.SignerCertCheckException.Reason.CSCA_CHECK_FAILED, e.getReason()); + } + + Assertions.assertEquals(signerInformationEntitiesInDb, signerInformationRepository.count()); + } + + @Test + void testUploadFailedInvalidCSCAWrongCountryCode() throws Exception { + long signerInformationEntitiesInDb = signerInformationRepository.count(); + + X509Certificate signerCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + + // sign with CSCA from another country + X509Certificate cscaCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.CSCA, "XX"); + PrivateKey cscaPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.CSCA, "XX"); + + + KeyPair payloadKeyPair = KeyPairGenerator.getInstance("ec").generateKeyPair(); + X509Certificate payloadCertificate = CertificateTestUtils.generateCertificate(payloadKeyPair, countryCode, "Payload Cert", cscaCertificate, cscaPrivateKey); + + try { + signerInformationService.addSignerCertificate( + new X509CertificateHolder(payloadCertificate.getEncoded()), + new X509CertificateHolder(signerCertificate.getEncoded()), + dummySignature, + countryCode + ); + } catch (SignerInformationService.SignerCertCheckException e) { + Assertions.assertEquals(SignerInformationService.SignerCertCheckException.Reason.CSCA_CHECK_FAILED, e.getReason()); + } + + Assertions.assertEquals(signerInformationEntitiesInDb, signerInformationRepository.count()); + } + + @Test + void testUploadFailedPayloadCertCountryWrong() throws Exception { + long signerInformationEntitiesInDb = signerInformationRepository.count(); + + X509Certificate signerCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + + X509Certificate cscaCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.CSCA, countryCode); + PrivateKey cscaPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.CSCA, countryCode); + + KeyPair payloadKeyPair = KeyPairGenerator.getInstance("ec").generateKeyPair(); + X509Certificate payloadCertificate = CertificateTestUtils.generateCertificate(payloadKeyPair, "XX", "Payload Cert", cscaCertificate, cscaPrivateKey); + + try { + signerInformationService.addSignerCertificate( + new X509CertificateHolder(payloadCertificate.getEncoded()), + new X509CertificateHolder(signerCertificate.getEncoded()), + dummySignature, + countryCode + ); + } catch (SignerInformationService.SignerCertCheckException e) { + Assertions.assertEquals(SignerInformationService.SignerCertCheckException.Reason.COUNTRY_OF_ORIGIN_CHECK_FAILED, e.getReason()); + } + + Assertions.assertEquals(signerInformationEntitiesInDb, signerInformationRepository.count()); + } + + @Test + void testUploadFailedWrongSignerCertificate() throws Exception { + long signerInformationEntitiesInDb = signerInformationRepository.count(); + + X509Certificate signerCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, "XX"); + + X509Certificate cscaCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.CSCA, countryCode); + PrivateKey cscaPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.CSCA, countryCode); + + KeyPair payloadKeyPair = KeyPairGenerator.getInstance("ec").generateKeyPair(); + X509Certificate payloadCertificate = CertificateTestUtils.generateCertificate(payloadKeyPair, countryCode, "Payload Cert", cscaCertificate, cscaPrivateKey); + + try { + signerInformationService.addSignerCertificate( + new X509CertificateHolder(payloadCertificate.getEncoded()), + new X509CertificateHolder(signerCertificate.getEncoded()), + dummySignature, + countryCode + ); + } catch (SignerInformationService.SignerCertCheckException e) { + Assertions.assertEquals(SignerInformationService.SignerCertCheckException.Reason.UPLOADER_CERT_CHECK_FAILED, e.getReason()); + } + + Assertions.assertEquals(signerInformationEntitiesInDb, signerInformationRepository.count()); + } + + @Test + void testDeleteFailedNotExists() throws Exception { + X509Certificate signerCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + X509Certificate cscaCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.CSCA, countryCode); + PrivateKey cscaPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.CSCA, countryCode); + + KeyPair payloadKeyPair = KeyPairGenerator.getInstance("ec").generateKeyPair(); + X509Certificate payloadCertificate = CertificateTestUtils.generateCertificate(payloadKeyPair, countryCode, "Payload Cert", cscaCertificate, cscaPrivateKey); + + try { + signerInformationService.deleteSignerCertificate( + new X509CertificateHolder(payloadCertificate.getEncoded()), + new X509CertificateHolder(signerCertificate.getEncoded()), + countryCode + ); + } catch (SignerInformationService.SignerCertCheckException e) { + Assertions.assertEquals(SignerInformationService.SignerCertCheckException.Reason.EXIST_CHECK_FAILED, e.getReason()); + } + } + + @Test + void testDeleteFailedPayloadCertCountryWrong() throws Exception { + long signerInformationEntitiesInDb = signerInformationRepository.count(); + + X509Certificate signerCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + X509Certificate cscaCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.CSCA, countryCode); + PrivateKey cscaPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.CSCA, countryCode); + + KeyPair payloadKeyPair = KeyPairGenerator.getInstance("ec").generateKeyPair(); + X509Certificate payloadCertificate = CertificateTestUtils.generateCertificate(payloadKeyPair, "XX", "Payload Cert", cscaCertificate, cscaPrivateKey); + + try { + signerInformationService.deleteSignerCertificate( + new X509CertificateHolder(payloadCertificate.getEncoded()), + new X509CertificateHolder(signerCertificate.getEncoded()), + countryCode + ); + } catch (SignerInformationService.SignerCertCheckException e) { + Assertions.assertEquals(SignerInformationService.SignerCertCheckException.Reason.COUNTRY_OF_ORIGIN_CHECK_FAILED, e.getReason()); + } + + Assertions.assertEquals(signerInformationEntitiesInDb, signerInformationRepository.count()); + } + + @Test + void testDeleteFailedWrongSignerCertificate() throws Exception { + long signerInformationEntitiesInDb = signerInformationRepository.count(); + + X509Certificate signerCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, "XX"); + + X509Certificate cscaCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.CSCA, countryCode); + PrivateKey cscaPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.CSCA, countryCode); + + KeyPair payloadKeyPair = KeyPairGenerator.getInstance("ec").generateKeyPair(); + X509Certificate payloadCertificate = CertificateTestUtils.generateCertificate(payloadKeyPair, countryCode, "Payload Cert", cscaCertificate, cscaPrivateKey); + + try { + signerInformationService.deleteSignerCertificate( + new X509CertificateHolder(payloadCertificate.getEncoded()), + new X509CertificateHolder(signerCertificate.getEncoded()), + countryCode + ); + } catch (SignerInformationService.SignerCertCheckException e) { + Assertions.assertEquals(SignerInformationService.SignerCertCheckException.Reason.UPLOADER_CERT_CHECK_FAILED, e.getReason()); + } + + Assertions.assertEquals(signerInformationEntitiesInDb, signerInformationRepository.count()); + } +} diff --git a/src/test/java/eu/europa/ec/dgc/gateway/service/TrustListServiceTest.java b/src/test/java/eu/europa/ec/dgc/gateway/service/TrustListServiceTest.java new file mode 100644 index 00000000..c75f6452 --- /dev/null +++ b/src/test/java/eu/europa/ec/dgc/gateway/service/TrustListServiceTest.java @@ -0,0 +1,193 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.service; + +import eu.europa.ec.dgc.gateway.entity.SignerInformationEntity; +import eu.europa.ec.dgc.gateway.entity.TrustedPartyEntity; +import eu.europa.ec.dgc.gateway.model.TrustList; +import eu.europa.ec.dgc.gateway.model.TrustListType; +import eu.europa.ec.dgc.gateway.repository.SignerInformationRepository; +import eu.europa.ec.dgc.gateway.repository.TrustedPartyRepository; +import eu.europa.ec.dgc.gateway.testdata.CertificateTestUtils; +import eu.europa.ec.dgc.gateway.testdata.TrustedPartyTestHelper; +import eu.europa.ec.dgc.utils.CertificateUtils; +import java.security.KeyPairGenerator; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.time.ZonedDateTime; +import java.util.Base64; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.Assertions; +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; + +@SpringBootTest +class TrustListServiceTest { + + @Autowired + SignerInformationRepository signerInformationRepository; + + @Autowired + TrustedPartyRepository trustedPartyRepository; + + @Autowired + TrustedPartyTestHelper trustedPartyTestHelper; + + @Autowired + TrustListService trustListService; + + @Autowired + CertificateUtils certificateUtils; + + X509Certificate certUploadDe, certUploadEu, certCscaDe, certCscaEu, certAuthDe, certAuthEu, certDscDe, certDscEu; + + @BeforeEach + void testData() throws Exception { + trustedPartyRepository.deleteAll(); + signerInformationRepository.deleteAll(); + + certUploadDe = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, "DE"); + certUploadEu = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, "EU"); + certCscaDe = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.CSCA, "DE"); + certCscaEu = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.CSCA, "EU"); + certAuthDe = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.AUTHENTICATION, "DE"); + certAuthEu = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.AUTHENTICATION, "EU"); + + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("ec"); + certDscDe = CertificateTestUtils.generateCertificate(keyPairGenerator.generateKeyPair(), "DE", "Test"); + certDscEu = CertificateTestUtils.generateCertificate(keyPairGenerator.generateKeyPair(), "EU", "Test"); + + signerInformationRepository.save(new SignerInformationEntity( + null, + ZonedDateTime.now(), + "DE", + certificateUtils.getCertThumbprint(certDscDe), + Base64.getEncoder().encodeToString(certDscDe.getEncoded()), + "sig1", + SignerInformationEntity.CertificateType.DSC + )); + + signerInformationRepository.save(new SignerInformationEntity( + null, + ZonedDateTime.now(), + "EU", + certificateUtils.getCertThumbprint(certDscEu), + Base64.getEncoder().encodeToString(certDscEu.getEncoded()), + "sig2", + SignerInformationEntity.CertificateType.DSC + )); + } + + @Test + void testTrustListWithoutFilter() throws Exception { + List trustList = trustListService.getTrustList(); + + Assertions.assertEquals(8, trustList.size()); + + assertTrustListItem(trustList, certDscDe, "DE", TrustListType.DSC, "sig1"); + assertTrustListItem(trustList, certDscEu, "EU", TrustListType.DSC, "sig2"); + assertTrustListItem(trustList, certCscaDe, "DE", TrustListType.CSCA, null); + assertTrustListItem(trustList, certCscaEu, "EU", TrustListType.CSCA, null); + assertTrustListItem(trustList, certUploadDe, "DE", TrustListType.UPLOAD, null); + assertTrustListItem(trustList, certUploadEu, "EU", TrustListType.UPLOAD, null); + assertTrustListItem(trustList, certAuthDe, "DE", TrustListType.AUTHENTICATION, null); + assertTrustListItem(trustList, certAuthEu, "EU", TrustListType.AUTHENTICATION, null); + } + + @Test + void testTrustListFilterByType() throws Exception { + List trustList = trustListService.getTrustList(TrustListType.DSC); + Assertions.assertEquals(2, trustList.size()); + assertTrustListItem(trustList, certDscDe, "DE", TrustListType.DSC, "sig1"); + assertTrustListItem(trustList, certDscEu, "EU", TrustListType.DSC, "sig2"); + + trustList = trustListService.getTrustList(TrustListType.CSCA); + Assertions.assertEquals(2, trustList.size()); + assertTrustListItem(trustList, certCscaDe, "DE", TrustListType.CSCA, null); + assertTrustListItem(trustList, certCscaEu, "EU", TrustListType.CSCA, null); + + trustList = trustListService.getTrustList(TrustListType.UPLOAD); + Assertions.assertEquals(2, trustList.size()); + assertTrustListItem(trustList, certUploadDe, "DE", TrustListType.UPLOAD, null); + assertTrustListItem(trustList, certUploadEu, "EU", TrustListType.UPLOAD, null); + + trustList = trustListService.getTrustList(TrustListType.AUTHENTICATION); + Assertions.assertEquals(2, trustList.size()); + assertTrustListItem(trustList, certAuthDe, "DE", TrustListType.AUTHENTICATION, null); + assertTrustListItem(trustList, certAuthEu, "EU", TrustListType.AUTHENTICATION, null); + } + + @Test + void testTrustListFilterByTypeAndCountry() throws Exception { + List trustList = trustListService.getTrustList(TrustListType.DSC, "DE"); + Assertions.assertEquals(1, trustList.size()); + assertTrustListItem(trustList, certDscDe, "DE", TrustListType.DSC, "sig1"); + trustList = trustListService.getTrustList(TrustListType.DSC, "EU"); + Assertions.assertEquals(1, trustList.size()); + assertTrustListItem(trustList, certDscEu, "EU", TrustListType.DSC, "sig2"); + + trustList = trustListService.getTrustList(TrustListType.CSCA, "DE"); + Assertions.assertEquals(1, trustList.size()); + assertTrustListItem(trustList, certCscaDe, "DE", TrustListType.CSCA, null); + trustList = trustListService.getTrustList(TrustListType.CSCA, "EU"); + Assertions.assertEquals(1, trustList.size()); + assertTrustListItem(trustList, certCscaEu, "EU", TrustListType.CSCA, null); + + trustList = trustListService.getTrustList(TrustListType.UPLOAD, "DE"); + Assertions.assertEquals(1, trustList.size()); + assertTrustListItem(trustList, certUploadDe, "DE", TrustListType.UPLOAD, null); + trustList = trustListService.getTrustList(TrustListType.UPLOAD, "EU"); + Assertions.assertEquals(1, trustList.size()); + assertTrustListItem(trustList, certUploadEu, "EU", TrustListType.UPLOAD, null); + + trustList = trustListService.getTrustList(TrustListType.AUTHENTICATION, "DE"); + Assertions.assertEquals(1, trustList.size()); + assertTrustListItem(trustList, certAuthDe, "DE", TrustListType.AUTHENTICATION, null); + trustList = trustListService.getTrustList(TrustListType.AUTHENTICATION, "EU"); + Assertions.assertEquals(1, trustList.size()); + assertTrustListItem(trustList, certAuthEu, "EU", TrustListType.AUTHENTICATION, null); + } + + private void assertTrustListItem(List trustList, X509Certificate certificate, String country, TrustListType trustListType, String signature) throws CertificateEncodingException { + Optional trustListOptional = trustList + .stream() + .filter(tl -> tl.getKid().equals(certificateUtils.getCertKid(certificate))) + .findFirst(); + + Assertions.assertTrue(trustListOptional.isPresent()); + + TrustList trustListItem = trustListOptional.get(); + + Assertions.assertEquals(certificateUtils.getCertKid(certificate), trustListItem.getKid()); + Assertions.assertEquals(country, trustListItem.getCountry()); + Assertions.assertEquals(trustListType, trustListItem.getCertificateType()); + Assertions.assertEquals(certificateUtils.getCertThumbprint(certificate), trustListItem.getThumbprint()); + Assertions.assertEquals(Base64.getEncoder().encodeToString(certificate.getEncoded()), trustListItem.getRawData()); + + if (signature != null) { + Assertions.assertEquals(signature, trustListItem.getSignature()); + } + } + +} diff --git a/src/test/java/eu/europa/ec/dgc/gateway/service/TrustedPartyServiceTest.java b/src/test/java/eu/europa/ec/dgc/gateway/service/TrustedPartyServiceTest.java new file mode 100644 index 00000000..51c6b28d --- /dev/null +++ b/src/test/java/eu/europa/ec/dgc/gateway/service/TrustedPartyServiceTest.java @@ -0,0 +1,162 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.service; + +import eu.europa.ec.dgc.gateway.entity.TrustedPartyEntity; +import eu.europa.ec.dgc.gateway.repository.TrustedPartyRepository; +import eu.europa.ec.dgc.gateway.testdata.DgcTestKeyStore; +import eu.europa.ec.dgc.gateway.testdata.TrustedPartyTestHelper; +import eu.europa.ec.dgc.signing.SignedCertificateMessageBuilder; +import java.util.Optional; +import org.bouncycastle.cert.X509CertificateHolder; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class TrustedPartyServiceTest { + + @Autowired + TrustedPartyRepository trustedPartyRepository; + + @Autowired + TrustedPartyService trustedPartyService; + + @Autowired + TrustedPartyTestHelper trustedPartyTestHelper; + + @Autowired + DgcTestKeyStore dgcTestKeyStore; + + private static final String countryCode = "EU"; + + @AfterEach + void cleanUp() { + // We have to delete all certs after each test because some tests are manipulating certs in DB. + trustedPartyRepository.deleteAll(); + } + + @Test + void trustedPartyServiceShouldReturnCertificate() throws Exception { + String hash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + Optional certOptional = trustedPartyService.getCertificate(hash, countryCode, TrustedPartyEntity.CertificateType.UPLOAD); + Assertions.assertTrue(certOptional.isPresent()); + Assertions.assertEquals(hash, certOptional.get().getThumbprint()); + + hash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.CSCA, countryCode); + certOptional = trustedPartyService.getCertificate(hash, countryCode, TrustedPartyEntity.CertificateType.CSCA); + Assertions.assertTrue(certOptional.isPresent()); + Assertions.assertEquals(hash, certOptional.get().getThumbprint()); + + hash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode); + certOptional = trustedPartyService.getCertificate(hash, countryCode, TrustedPartyEntity.CertificateType.AUTHENTICATION); + Assertions.assertTrue(certOptional.isPresent()); + Assertions.assertEquals(hash, certOptional.get().getThumbprint()); + } + + @Test + void trustedPartyServiceShouldNotReturnCertificateIfIntegrityOfRawDataIsViolated() throws Exception { + Optional certOptional = trustedPartyService.getCertificate( + trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.CSCA, countryCode), countryCode, TrustedPartyEntity.CertificateType.CSCA); + + Optional anotherCertOptional = trustedPartyService.getCertificate( + trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode), countryCode, TrustedPartyEntity.CertificateType.AUTHENTICATION); + + Assertions.assertTrue(certOptional.isPresent()); + Assertions.assertTrue(anotherCertOptional.isPresent()); + + TrustedPartyEntity cert = certOptional.get(); + cert.setRawData(anotherCertOptional.get().getRawData()); + + trustedPartyRepository.save(cert); + + certOptional = trustedPartyService.getCertificate( + trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.CSCA, countryCode), countryCode, TrustedPartyEntity.CertificateType.CSCA); + Assertions.assertTrue(certOptional.isEmpty()); + } + + @Test + void trustedPartyServiceShouldNotReturnCertificateIfIntegrityOfSignatureIsViolated() throws Exception { + Optional certOptional = trustedPartyService.getCertificate( + trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.CSCA, countryCode), countryCode, TrustedPartyEntity.CertificateType.CSCA); + + Optional anotherCertOptional = trustedPartyService.getCertificate( + trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode), countryCode, TrustedPartyEntity.CertificateType.AUTHENTICATION); + + Assertions.assertTrue(certOptional.isPresent()); + Assertions.assertTrue(anotherCertOptional.isPresent()); + + TrustedPartyEntity cert = certOptional.get(); + cert.setSignature(anotherCertOptional.get().getSignature()); + + trustedPartyRepository.save(cert); + + certOptional = trustedPartyService.getCertificate( + trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.CSCA, countryCode), countryCode, TrustedPartyEntity.CertificateType.CSCA); + Assertions.assertTrue(certOptional.isEmpty()); + } + + @Test + void trustedPartyServiceShouldNotReturnCertificateIfIntegrityOfThumbprintIsViolated() throws Exception { + Optional certOptional = trustedPartyService.getCertificate( + trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.CSCA, countryCode), countryCode, TrustedPartyEntity.CertificateType.CSCA); + + Optional anotherCertOptional = trustedPartyService.getCertificate( + trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode), countryCode, TrustedPartyEntity.CertificateType.AUTHENTICATION); + + Assertions.assertTrue(certOptional.isPresent()); + Assertions.assertTrue(anotherCertOptional.isPresent()); + + trustedPartyRepository.delete(anotherCertOptional.get()); + + TrustedPartyEntity cert = certOptional.get(); + cert.setThumbprint(anotherCertOptional.get().getThumbprint()); + + trustedPartyRepository.save(cert); + + certOptional = trustedPartyService.getCertificate( + cert.getThumbprint(), countryCode, TrustedPartyEntity.CertificateType.CSCA); + Assertions.assertTrue(certOptional.isEmpty()); + } + + @Test + void trustedPartyServiceShouldNotReturnCertificateIfSignatureIsFromUnknownTrustAnchor() throws Exception { + Optional certOptional = trustedPartyService.getCertificate( + trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.CSCA, countryCode), countryCode, TrustedPartyEntity.CertificateType.CSCA); + + // Create new signature with a random non TrustAnchor certificate + String newSignature = new SignedCertificateMessageBuilder() + .withSigningCertificate(new X509CertificateHolder(trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, "XX").getEncoded()), trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.UPLOAD, "XX")) + .withPayload(new X509CertificateHolder(trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.CSCA, countryCode).getEncoded())) + .buildAsString(true); + + Assertions.assertTrue(certOptional.isPresent()); + + TrustedPartyEntity trustedPartyEntity = certOptional.get(); + trustedPartyEntity.setSignature(newSignature); + trustedPartyRepository.save(trustedPartyEntity); + + certOptional = trustedPartyService.getCertificate(trustedPartyEntity.getThumbprint(), countryCode, TrustedPartyEntity.CertificateType.CSCA); + Assertions.assertTrue(certOptional.isEmpty()); + } +} diff --git a/src/test/java/eu/europa/ec/dgc/gateway/service/ValuesetServiceTest.java b/src/test/java/eu/europa/ec/dgc/gateway/service/ValuesetServiceTest.java new file mode 100644 index 00000000..e5883bde --- /dev/null +++ b/src/test/java/eu/europa/ec/dgc/gateway/service/ValuesetServiceTest.java @@ -0,0 +1,85 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.service; + +import eu.europa.ec.dgc.gateway.entity.ValuesetEntity; +import eu.europa.ec.dgc.gateway.repository.ValuesetRepository; +import eu.europa.ec.dgc.gateway.testdata.TrustedPartyTestHelper; +import eu.europa.ec.dgc.utils.CertificateUtils; +import java.util.List; +import org.junit.jupiter.api.Assertions; +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; + +@SpringBootTest +class ValuesetServiceTest { + + @Autowired + ValuesetService valuesetService; + + @Autowired + ValuesetRepository valuesetRepository; + + @Autowired + TrustedPartyTestHelper trustedPartyTestHelper; + + @Autowired + CertificateUtils certificateUtils; + + @BeforeEach + void setup() { + valuesetRepository.deleteAll(); + } + + @Test + void testGetValuesetIds() { + ValuesetEntity valuesetEntity1 = new ValuesetEntity("vs-dummy-1", "content1"); + ValuesetEntity valuesetEntity2 = new ValuesetEntity("vs-dummy-2", "content2"); + ValuesetEntity valuesetEntity3 = new ValuesetEntity("vs-dummy-3", "content3"); + + valuesetRepository.save(valuesetEntity1); + valuesetRepository.save(valuesetEntity2); + valuesetRepository.save(valuesetEntity3); + + + List valuesetIds = valuesetService.getValuesetIds(); + Assertions.assertEquals(3, valuesetService.getValuesetIds().size()); + Assertions.assertTrue(valuesetIds.contains("vs-dummy-1")); + Assertions.assertTrue(valuesetIds.contains("vs-dummy-2")); + Assertions.assertTrue(valuesetIds.contains("vs-dummy-3")); + } + + @Test + void testGetValueset() { + ValuesetEntity valuesetEntity1 = new ValuesetEntity("vs-dummy-1", "content1"); + ValuesetEntity valuesetEntity2 = new ValuesetEntity("vs-dummy-2", "content2"); + + valuesetRepository.save(valuesetEntity1); + valuesetRepository.save(valuesetEntity2); + + Assertions.assertEquals(valuesetEntity1.getJson(), valuesetService.getValueSetById(valuesetEntity1.getId()).orElseThrow()); + Assertions.assertEquals(valuesetEntity2.getJson(), valuesetService.getValueSetById(valuesetEntity2.getId()).orElseThrow()); + } + + +} diff --git a/src/test/java/eu/europa/ec/dgc/gateway/testdata/CertificateTestUtils.java b/src/test/java/eu/europa/ec/dgc/gateway/testdata/CertificateTestUtils.java new file mode 100644 index 00000000..63b27c03 --- /dev/null +++ b/src/test/java/eu/europa/ec/dgc/gateway/testdata/CertificateTestUtils.java @@ -0,0 +1,142 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.testdata; + +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import eu.europa.ec.dgc.gateway.connector.model.ValidationRule; +import java.math.BigInteger; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.time.Instant; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; +import java.util.Date; +import java.util.List; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x500.X500NameBuilder; +import org.bouncycastle.asn1.x509.BasicConstraints; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.X509ObjectIdentifiers; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.junit.jupiter.api.Assertions; + +public class CertificateTestUtils { + + public static ValidationRule getDummyValidationRule() { + ValidationRule validationRule = new ValidationRule(); + + JsonNodeFactory jsonNodeFactory = JsonNodeFactory.instance; + + validationRule.setLogic(jsonNodeFactory.objectNode().set("field1", jsonNodeFactory.textNode("value1"))); + validationRule.setValidTo(ZonedDateTime.now().plus(1, ChronoUnit.WEEKS)); + validationRule.setValidFrom(ZonedDateTime.now().plus(3, ChronoUnit.DAYS)); + validationRule.setCertificateType("General"); + validationRule.setDescription(List.of(new ValidationRule.DescriptionItem("en", "de".repeat(10)))); + validationRule.setEngine("CERTLOGIC"); + validationRule.setEngineVersion("1.0.0"); + validationRule.setVersion("1.0.0"); + validationRule.setAffectedFields(List.of("AB", "DE")); + validationRule.setRegion("BW"); + validationRule.setSchemaVersion("1.0.0"); + validationRule.setType("Acceptance"); + validationRule.setIdentifier("GR-EU-0001"); + validationRule.setCountry("EU"); + + return validationRule; + } + + public static X509Certificate generateCertificate(KeyPair keyPair, String country, String commonName) throws Exception { + Date validFrom = Date.from(Instant.now().minus(1, ChronoUnit.DAYS)); + Date validTo = Date.from(Instant.now().plus(365, ChronoUnit.DAYS)); + + return generateCertificate(keyPair, country, commonName, validFrom, validTo); + } + + public static X509Certificate generateCertificate(KeyPair keyPair, String country, String commonName, X509Certificate ca, PrivateKey caKey) throws Exception { + Date validFrom = Date.from(Instant.now().minus(1, ChronoUnit.DAYS)); + Date validTo = Date.from(Instant.now().plus(365, ChronoUnit.DAYS)); + + return generateCertificate(keyPair, country, commonName, validFrom, validTo, ca, caKey); + } + + public static X509Certificate generateCertificate(KeyPair keyPair, String country, String commonName, Date validFrom, Date validTo) throws Exception { + X500Name subject = new X500NameBuilder() + .addRDN(X509ObjectIdentifiers.countryName, country) + .addRDN(X509ObjectIdentifiers.commonName, commonName) + .build(); + + BigInteger certSerial = new BigInteger(Long.toString(System.currentTimeMillis())); + + ContentSigner contentSigner = new JcaContentSignerBuilder("SHA256withECDSA").build(keyPair.getPrivate()); + + JcaX509v3CertificateBuilder certBuilder = + new JcaX509v3CertificateBuilder(subject, certSerial, validFrom, validTo, subject, keyPair.getPublic()); + + BasicConstraints basicConstraints = new BasicConstraints(false); + certBuilder.addExtension(Extension.basicConstraints, true, basicConstraints); + + return new JcaX509CertificateConverter().getCertificate(certBuilder.build(contentSigner)); + } + + public static X509Certificate generateCertificate(KeyPair keyPair, String country, String commonName, Date validFrom, Date validTo, X509Certificate ca, PrivateKey caKey) throws Exception { + X500Name subject = new X500NameBuilder() + .addRDN(X509ObjectIdentifiers.countryName, country) + .addRDN(X509ObjectIdentifiers.commonName, commonName) + .build(); + + X500Name issuer = new X509CertificateHolder(ca.getEncoded()).getSubject(); + + BigInteger certSerial = new BigInteger(Long.toString(System.currentTimeMillis())); + + ContentSigner contentSigner = new JcaContentSignerBuilder("SHA256withECDSA").build(caKey); + + JcaX509v3CertificateBuilder certBuilder = + new JcaX509v3CertificateBuilder(issuer, certSerial, validFrom, validTo, subject, keyPair.getPublic()); + + BasicConstraints basicConstraints = new BasicConstraints(false); + certBuilder.addExtension(Extension.basicConstraints, true, basicConstraints); + + return new JcaX509CertificateConverter().getCertificate(certBuilder.build(contentSigner)); + } + + public static void assertEquals(ValidationRule v1, ValidationRule v2) { + Assertions.assertEquals(v1.getIdentifier(), v2.getIdentifier()); + Assertions.assertEquals(v1.getType(), v2.getType()); + Assertions.assertEquals(v1.getCountry(), v2.getCountry()); + Assertions.assertEquals(v1.getRegion(), v2.getRegion()); + Assertions.assertEquals(v1.getVersion(), v2.getVersion()); + Assertions.assertEquals(v1.getSchemaVersion(), v2.getSchemaVersion()); + Assertions.assertEquals(v1.getEngine(), v2.getEngine()); + Assertions.assertEquals(v1.getEngineVersion(), v2.getEngineVersion()); + Assertions.assertEquals(v1.getCertificateType(), v2.getCertificateType()); + Assertions.assertEquals(v1.getDescription(), v2.getDescription()); + Assertions.assertEquals(v1.getValidFrom().toEpochSecond(), v2.getValidFrom().toEpochSecond()); + Assertions.assertEquals(v1.getValidTo().toEpochSecond(), v2.getValidTo().toEpochSecond()); + Assertions.assertEquals(v1.getAffectedFields(), v2.getAffectedFields()); + Assertions.assertEquals(v1.getLogic(), v2.getLogic()); + } + +} diff --git a/src/test/java/eu/europa/ec/dgc/gateway/testdata/DgcTestKeyStore.java b/src/test/java/eu/europa/ec/dgc/gateway/testdata/DgcTestKeyStore.java new file mode 100644 index 00000000..330865d4 --- /dev/null +++ b/src/test/java/eu/europa/ec/dgc/gateway/testdata/DgcTestKeyStore.java @@ -0,0 +1,79 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.testdata; + +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; + +import eu.europa.ec.dgc.gateway.config.DgcConfigProperties; +import java.io.IOException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.KeyStoreSpi; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import lombok.Getter; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; + +@TestConfiguration +public class DgcTestKeyStore { + + private final DgcConfigProperties configProperties; + + @Getter + private final X509Certificate trustAnchor; + + @Getter + private final PrivateKey trustAnchorPrivateKey; + + public DgcTestKeyStore(DgcConfigProperties configProperties) throws Exception { + this.configProperties = configProperties; + + KeyPair keyPair = KeyPairGenerator.getInstance("ec").generateKeyPair(); + trustAnchorPrivateKey = keyPair.getPrivate(); + + trustAnchor = CertificateTestUtils.generateCertificate(keyPair, "DE", "DGCG Test TrustAnchor"); + + } + + /** + * Creates a KeyStore instance with keys for DGC. + */ + @Bean + @Primary + public KeyStore testKeyStore() throws IOException, CertificateException, NoSuchAlgorithmException { + KeyStoreSpi keyStoreSpiMock = mock(KeyStoreSpi.class); + KeyStore keyStoreMock = new KeyStore(keyStoreSpiMock, null, "test") { + }; + keyStoreMock.load(null); + + doAnswer((x) -> trustAnchor) + .when(keyStoreSpiMock).engineGetCertificate(configProperties.getTrustAnchor().getCertificateAlias()); + + return keyStoreMock; + } + +} diff --git a/src/test/java/eu/europa/ec/dgc/gateway/testdata/TrustedPartyTestHelper.java b/src/test/java/eu/europa/ec/dgc/gateway/testdata/TrustedPartyTestHelper.java new file mode 100644 index 00000000..8b54cffd --- /dev/null +++ b/src/test/java/eu/europa/ec/dgc/gateway/testdata/TrustedPartyTestHelper.java @@ -0,0 +1,126 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.testdata; + +import eu.europa.ec.dgc.gateway.entity.TrustedPartyEntity; +import eu.europa.ec.dgc.gateway.repository.TrustedPartyRepository; +import eu.europa.ec.dgc.signing.SignedCertificateMessageBuilder; +import eu.europa.ec.dgc.utils.CertificateUtils; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import org.bouncycastle.cert.X509CertificateHolder; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class TrustedPartyTestHelper { + + private final Map> hashMap = Map.of( + TrustedPartyEntity.CertificateType.AUTHENTICATION, new HashMap<>(), + TrustedPartyEntity.CertificateType.CSCA, new HashMap<>(), + TrustedPartyEntity.CertificateType.UPLOAD, new HashMap<>() + ); + + private final Map> certificateMap = Map.of( + TrustedPartyEntity.CertificateType.AUTHENTICATION, new HashMap<>(), + TrustedPartyEntity.CertificateType.CSCA, new HashMap<>(), + TrustedPartyEntity.CertificateType.UPLOAD, new HashMap<>() + ); + + private final Map> privateKeyMap = Map.of( + TrustedPartyEntity.CertificateType.AUTHENTICATION, new HashMap<>(), + TrustedPartyEntity.CertificateType.CSCA, new HashMap<>(), + TrustedPartyEntity.CertificateType.UPLOAD, new HashMap<>() + ); + + private final TrustedPartyRepository trustedPartyRepository; + + private final CertificateUtils certificateUtils; + + private final DgcTestKeyStore testKeyStore; + + public String getHash(TrustedPartyEntity.CertificateType type, String countryCode) throws Exception { + prepareTestCert(type, countryCode); + return hashMap.get(type).get(countryCode); + } + + public X509Certificate getCert(TrustedPartyEntity.CertificateType type, String countryCode) throws Exception { + prepareTestCert(type, countryCode); + return certificateMap.get(type).get(countryCode); + } + + public PrivateKey getPrivateKey(TrustedPartyEntity.CertificateType type, String countryCode) throws Exception { + prepareTestCert(type, countryCode); + return privateKeyMap.get(type).get(countryCode); + } + + private void prepareTestCert(TrustedPartyEntity.CertificateType type, String countryCode) throws Exception { + // Check if a test certificate already exists + if (!hashMap.get(type).containsKey(countryCode)) { + createAndInsertCert(type, countryCode); + } + + // Check if generated certificate is (still) present in DB + if (trustedPartyRepository.getFirstByThumbprintAndCertificateType( + hashMap.get(type).get(countryCode), type + ).isEmpty()) { + insertTestCert(type, countryCode); + } + } + + private void createAndInsertCert(TrustedPartyEntity.CertificateType type, String countryCode) throws Exception { + KeyPair keyPair = KeyPairGenerator.getInstance("ec").generateKeyPair(); + X509Certificate authCertificate = + CertificateTestUtils.generateCertificate(keyPair, countryCode, "DGC Test " + type.name() + " Cert"); + String certHash = certificateUtils.getCertThumbprint(authCertificate); + + certificateMap.get(type).put(countryCode, authCertificate); + hashMap.get(type).put(countryCode, certHash); + privateKeyMap.get(type).put(countryCode, keyPair.getPrivate()); + + insertTestCert(type, countryCode); + } + + private void insertTestCert(TrustedPartyEntity.CertificateType type, String countryCode) throws Exception { + String certRawData = Base64.getEncoder().encodeToString( + certificateMap.get(type).get(countryCode).getEncoded()); + + String signature = new SignedCertificateMessageBuilder() + .withPayload(new X509CertificateHolder(certificateMap.get(type).get(countryCode).getEncoded())) + .withSigningCertificate(new X509CertificateHolder(testKeyStore.getTrustAnchor().getEncoded()), testKeyStore.getTrustAnchorPrivateKey()) + .buildAsString(true); + + TrustedPartyEntity trustedPartyEntity = new TrustedPartyEntity(); + trustedPartyEntity.setCertificateType(type); + trustedPartyEntity.setCountry(countryCode); + trustedPartyEntity.setSignature(signature); + trustedPartyEntity.setRawData(certRawData); + trustedPartyEntity.setThumbprint(hashMap.get(type).get(countryCode)); + + trustedPartyRepository.save(trustedPartyEntity); + } +} diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml new file mode 100644 index 00000000..ad16a75e --- /dev/null +++ b/src/test/resources/application.yml @@ -0,0 +1,36 @@ +server: + port: ${SERVER_PORT:8090} +spring: + profiles: + active: + - test + include: + - dev + application: + name: eu-interop-federation-gateway + liquibase: + enabled: true + change-log: classpath:db/changelog.xml + main: + allow-bean-definition-overriding: true +springdoc: + api-docs: + path: /api/docs + swagger-ui: + path: /swagger + +dgc: + jrc: + url: https://covid-19-diagnostics.jrc.ec.europa.eu/devices/hsc-common-recognition-rat + validationRuleSchema: classpath:validation-rule.schema.json + dbencryption: + initVector: Ho^RDYDuGt0Ki`\x + password: G&B3zSk|fNE!.Pa9+Xv2kUYRx2zp|@=| + trustAnchor: + keyStorePath: keystore/dgc-ta.jks + keyStorePass: dgc-p4ssw0rd + certificateAlias: dgc_trust_anchor + cert-auth: + header-fields: + thumbprint: X-SSL-Client-SHA256 + distinguished-name: X-SSL-Client-DN diff --git a/templates/file-header.txt b/templates/file-header.txt new file mode 100644 index 00000000..9af5493d --- /dev/null +++ b/templates/file-header.txt @@ -0,0 +1,19 @@ +/*- + * ---license-start + * eu-digital-green-certificates / dgc-gateway + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * 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 + * + * http://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. + * ---license-end + */ diff --git a/templates/third-party.ftl b/templates/third-party.ftl new file mode 100644 index 00000000..03befca6 --- /dev/null +++ b/templates/third-party.ftl @@ -0,0 +1,30 @@ +<#function artifactFormat p> + <#return p.groupId + ":" + p.artifactId + ":" + p.version> + +<#function licenseFormat licenses> + <#assign result = ""/> + <#list licenses as license> + <#assign result = result + license + " "/> + + <#return result> + +ThirdPartyNotices +----------------- +This project uses third-party software or other resources that +may be distributed under licenses different from this software. +In the event that we overlooked to list a required notice, please bring this +to our attention by contacting us via this email: +opensource@telekom.de + +ThirdParty Licenses +----------------- + +| Dependency | License | +| --- | --- | +<#compress> + <#list dependencyMap as e> + <#assign project = e.getKey() /> + <#assign license = e.getValue() /> + | ${artifactFormat(project)} | ${licenseFormat(license)} | + + \ No newline at end of file