diff --git a/.circleci/config.yml b/.circleci/config.yml index 522683a5a6..ae131f7cda 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -15,6 +15,9 @@ jobs: - restore_cache: key: cwa-server-{{ checksum "~/pom-checksum" }} - run: mvn --batch-mode dependency:go-offline + - run: + name: Analyze on SonarCloud + command: mvn --batch-mode verify sonar:sonar --fail-never - save_cache: paths: - ~/.m2 @@ -30,9 +33,6 @@ jobs: path: ~/test-results - store_artifacts: path: ~/test-results/junit - - run: - name: Analyze on SonarCloud - command: mvn --batch-mode verify sonar:sonar --fail-never workflows: main: diff --git a/.env b/.env index 4a61e7ca47..a9398e2884 100644 --- a/.env +++ b/.env @@ -16,4 +16,3 @@ OBJECTSTORE_SECRETKEY=verySecretKey1 # Docker Compose Secrets settings SECRET_PRIVATE=file:/secrets/private.pem -SECRET_CERTIFICATE=file:/secrets/certificate.crt diff --git a/.gitignore b/.gitignore index f0c8b61d35..07b224c1fc 100644 --- a/.gitignore +++ b/.gitignore @@ -38,5 +38,7 @@ out/ .settings .project .classpath -pom.xml.versionsBackup .factorypath + +pom.xml.versionsBackup +**/.flattened-pom.xml diff --git a/.mvn/maven.config b/.mvn/maven.config new file mode 100644 index 0000000000..152676d49f --- /dev/null +++ b/.mvn/maven.config @@ -0,0 +1 @@ +-Drevision=0.5.1 diff --git a/README.md b/README.md index 5ce13a512b..42d309588b 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ +

Corona-Warn-App Server

@@ -57,7 +58,7 @@ The docker-compose contains the following services: Service | Description | Endpoint and Default Credentials ------------------|-------------|----------- -submission | The Corona-Warn-App submission service | `http://localhost:8000`
`http://localhost:8005` (for actuator endpoint) +submission | The Corona-Warn-App submission service | `http://localhost:8000`
`http://localhost:8006` (for actuator endpoint) distribution | The Corona-Warn-App distribution service | NO ENDPOINT postgres | A [postgres] database installation | `postgres:8001`
Username: postgres
Password: postgres pgadmin | A [pgadmin](https://www.pgadmin.org/) installation for the postgres database | `http://localhost:8002`
Username: user@domain.com
Password: password @@ -102,9 +103,8 @@ After you made sure that the specified dependencies are running, configure them * Configure the Postgres connection in the [submission config](./services/submission/src/main/resources/application.yaml) and in the [distribution config](./services/distribution/src/main/resources/application.yaml) * Configure the S3 compatible object storage in the [distribution config](./services/distribution/src/main/resources/application.yaml) -* Configure the certificate and private key for the distribution service, the paths need to be prefixed with `file:` +* Configure the private key for the distribution service, the path need to be prefixed with `file:` * `VAULT_FILESIGNING_SECRET` should be the path to the private key, example available in `/docker-compose-test-secrets/private.pem` - * `VAULT_FILESIGNING_CERT` should be the path to the certificate, example available in `/docker-compose-test-secrets/certificate.cert` #### Build @@ -146,7 +146,7 @@ Distribution Service | [services/distribution/api_v1.json)](https://github. Profile | Effect -------------|------------- -`dev` | Turns the log level to `DEBUG`. +`dev` | Turns the log level to `DEBUG` and sets the app package ID in the export packages' signature info to `de.rki.coronawarnapp-dev` so that test certificates (instead of production certificates) will be used for client-side validation. `cloud` | Removes default values for the `datasource` and `objectstore` configurations. `demo` | Includes incomplete days and hours into the distribution run, thus creating aggregates for the current day and the current hour (and including both in the respective indices). When running multiple distributions in one hour with this profile, the date aggregate for today and the hours aggregate for the current hour will be updated and overwritten. `testdata` | Causes test data to be inserted into the database before each distribution run. By default, around 1000 random diagnosis keys will be generated per hour. If there are no diagnosis keys in the database yet, random keys will be generated for every hour from the beginning of the retention period (14 days ago at 00:00 UTC) until one hour before the present hour. If there are already keys in the database, the random keys will be generated for every hour from the latest diagnosis key in the database (by submission timestamp) until one hour before the present hour (or none at all, if the latest diagnosis key in the database was submitted one hour ago or later). diff --git a/SECURITY.md b/SECURITY.md index b150b8786d..deadf034d0 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,16 +1,20 @@ -# Reporting Security Vulnerabilities +# Security Vulnerabilities -The Corona-Warn-App 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. +The Corona-Warn-App is built with security and data privacy in mind to ensure your data is safe. + +## Reporting + +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 corona-warn-app.opensource@sap.com mailbox. -* For reporting a vulnerability, please use the Vulnerability Report Form for Security Researchers on [SAP Trust Center](https://www.sap.com/about/trust-center/incident-management.html). +* For reporting a vulnerability, please use the Vulnerability Report Form for Security Researchers on [SAP Trust Center](https://www.sap.com/about/trust-center/incident-management.html). * Please select "Corona-Warn-App" in the _product_ list. * In the _versions_ field, either note the specific [release version](https://github.com/corona-warn-app/cwa-server/releases) or commit id of the master branch you investigated. - * The affected repository should be mentioned in the _vulnerability description_. + * The affected repository should be mentioned in the _vulnerability description_. * Please use this channel only for reporting vulnerabilities of the _cwa-server_ component and check the security of the respective repositories for other components. -# Disclosure Handling +## Disclosure Handling SAP is committed to timely review and respond to your request. The resolution of code defects will be handled by a dedicated group of security experts and prepared in a private GitHub repository. The project will inform the public about resolved security vulnerabilities. For more information on the disclosure guidelines, please consult [SAP security information page](https://www.sap.com/about/trust-center/security/incident-management.html). diff --git a/THIRD-PARTY-NOTICES b/THIRD-PARTY-NOTICES index fb9642ef02..42dc7de02a 100644 --- a/THIRD-PARTY-NOTICES +++ b/THIRD-PARTY-NOTICES @@ -55,6 +55,11 @@ Licensor: MinIO Inc. Website: https://min.io/ License: Apache License 2.0 +Component: MojoHaus Flatten Maven Plugin +Licensor: MojoHaus +Website: https://www.mojohaus.org/flatten-maven-plugin/ +License: Apache License 2.0 + Component: PostgreSQL Licensor: PostgreSQL Website: https://www.postgresql.org/ @@ -87,7 +92,7 @@ License: Apache License 2.0 -------------------------------------------------------------------------------- Apache License 2.0 (Commons IO, Commons Math 3, flyway, JSON-Simple, -Maven, MinIO Object Storage, snakeyaml, Spring Boot, Zenko CloudServer) +Maven, MinIO Object Storage, MojoHaus Flatten Maven Plugin, snakeyaml, Spring Boot, Zenko CloudServer) Apache License Version 2.0, January 2004 @@ -715,4 +720,4 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --------------------------------------------------------------------------------- \ No newline at end of file +-------------------------------------------------------------------------------- diff --git a/codestyle/.markdownlint.yml b/codestyle/.markdownlint.yml index e07d0d4ebe..3df53cf68d 100644 --- a/codestyle/.markdownlint.yml +++ b/codestyle/.markdownlint.yml @@ -2,4 +2,3 @@ default: true MD013: false #https://github.com/DavidAnson/markdownlint/blob/master/doc/Rules.md#md013 MD033: false #https://github.com/DavidAnson/markdownlint/blob/master/doc/Rules.md#md033 -MD034: false #https://github.com/DavidAnson/markdownlint/blob/master/doc/Rules.md#md034 diff --git a/common/persistence/pom.xml b/common/persistence/pom.xml index fa5239e5e8..31fc6c9d99 100644 --- a/common/persistence/pom.xml +++ b/common/persistence/pom.xml @@ -12,7 +12,7 @@ org.opencwa persistence - 0.5.0 + ${revision} 11 @@ -27,7 +27,7 @@ org.opencwa protocols - 0.5.0 + ${project.version} org.springframework.boot @@ -105,6 +105,28 @@ + + + org.codehaus.mojo + flatten-maven-plugin + 1.1.0 + + true + resolveCiFriendliesOnly + + + + flatten + process-resources + flatten + + + flatten-clean + clean + clean + + + org.apache.maven.plugins maven-checkstyle-plugin diff --git a/common/pom.xml b/common/pom.xml index d1970990d2..94f5e7183c 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -6,7 +6,7 @@ server org.opencwa - 0.5.0 + ${revision} ../pom.xml @@ -17,7 +17,6 @@ 4.0.0 common - 0.5.0 pom diff --git a/common/protocols/pom.xml b/common/protocols/pom.xml index a03483cd8a..82a84b165c 100644 --- a/common/protocols/pom.xml +++ b/common/protocols/pom.xml @@ -5,13 +5,12 @@ common org.opencwa - 0.5.0 + ${revision} ../pom.xml 4.0.0 protocols - 0.5.0 corona-warn-app_cwa-server_common_protocols @@ -53,4 +52,4 @@ - \ No newline at end of file + diff --git a/common/protocols/src/main/proto/app/coronawarn/server/common/protocols/external/exposurenotification/temporary_exposure_key_export.proto b/common/protocols/src/main/proto/app/coronawarn/server/common/protocols/external/exposurenotification/temporary_exposure_key_export.proto index 075614b7ed..38f7e3fd98 100644 --- a/common/protocols/src/main/proto/app/coronawarn/server/common/protocols/external/exposurenotification/temporary_exposure_key_export.proto +++ b/common/protocols/src/main/proto/app/coronawarn/server/common/protocols/external/exposurenotification/temporary_exposure_key_export.proto @@ -21,17 +21,15 @@ message TemporaryExposureKeyExport { message SignatureInfo { // Apple App Store Application Bundle ID optional string app_bundle_id = 1; - // Android App package name - optional string android_package = 2; // Key version for rollovers // Must be in character class [a-zA-Z0-9_] - optional string verification_key_version = 3; + optional string verification_key_version = 2; // Alias with which to identify public key to be used for verification // Must be in character class [a-zA-Z0-9_] - optional string verification_key_id = 4; + optional string verification_key_id = 3; // ASN.1 OID for Algorithm Identifier. Supported algorithms are // either 1.2.840.10045.4.3.2 or 1.2.840.10045.4.3.4 - optional string signature_algorithm = 5; + optional string signature_algorithm = 4; } message TemporaryExposureKey { // Key of infected user diff --git a/docker-compose-test-secrets/certificate.crt b/docker-compose-test-secrets/certificate.crt deleted file mode 100644 index dbb2a3a672..0000000000 --- a/docker-compose-test-secrets/certificate.crt +++ /dev/null @@ -1,18 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIBMjCB2AIBZDAKBggqhkjOPQQDAjAkMSIwIAYDVQQDDBlDV0EgVGVzdCBSb290 -IENlcnRpZmljYXRlMB4XDTIwMDUyMzExMzAzOFoXDTIxMDUyMzExMzAzOFowJjEk -MCIGA1UEAwwbQ1dBIFRlc3QgQ2xpZW50IENlcnRpZmljYXRlMFkwEwYHKoZIzj0C -AQYIKoZIzj0DAQcDQgAEYQJ+sReY1L8z851VFRpLu4PCusj/7Ruvi879KjrQJ12k -KKsfeRWytmrE65Jok1lsYqpFhRWcxG6VV5FX0yG+EjAKBggqhkjOPQQDAgNJADBG -AiEAwP/VKVIhOuiIczrPFg0o4ns39Wu1vBpIXu+/psI/3LECIQD0FAXo5chuUkQy -LeUtwsQaC8v2KA96ew3PfCoTilvU1Q== ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIIBQzCB6QIUN7Z6IofaE0qtz2X1xz/IUDCUXd0wCgYIKoZIzj0EAwIwJDEiMCAG -A1UEAwwZQ1dBIFRlc3QgUm9vdCBDZXJ0aWZpY2F0ZTAeFw0yMDA1MjMxMTMwMzha -Fw0yMTA1MjMxMTMwMzhaMCQxIjAgBgNVBAMMGUNXQSBUZXN0IFJvb3QgQ2VydGlm -aWNhdGUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATBH0/vPsD62wE9jk5JZZd+ -Yf4WXZ3sKZUGbdqJssc4lxjyZJvpPLCirRQT6XWwhmBhoL8mRL5tpZ/93o+jzULe -MAoGCCqGSM49BAMCA0kAMEYCIQCNPEtoC1QtgnncZzV/rgIoO6tktCiAkybBhHMB -1SISZQIhAPguoBWCscYwNHtEgTDx2sQ8UZ79KvWvpHFlwDEyiHAv ------END CERTIFICATE----- diff --git a/docker-compose-test-secrets/private.pem b/docker-compose-test-secrets/private.pem index 053142de82..6d8dd02eff 100644 --- a/docker-compose-test-secrets/private.pem +++ b/docker-compose-test-secrets/private.pem @@ -1,5 +1,5 @@ -----BEGIN EC PRIVATE KEY----- -MHcCAQEEILQRQFlGcfeTAclubtjQ1rBjtmIOB/d7PITZyDe1r81/oAoGCCqGSM49 -AwEHoUQDQgAEYQJ+sReY1L8z851VFRpLu4PCusj/7Ruvi879KjrQJ12kKKsfeRWy -tmrE65Jok1lsYqpFhRWcxG6VV5FX0yG+Eg== +MHcCAQEEINOQcM6jChvjAwf0B3C2ex7Ronsc2VH4qIZK91n1/tgJoAoGCCqGSM49 +AwEHoUQDQgAE9/HUs+ssvOdmv+BZPjubaUiYOWYTd5iRMopbdBzpEPXbyQBSmOFe +sVJ7y3GTU/1ql9FuIrqB7YBkhZZExPEqEw== -----END EC PRIVATE KEY----- diff --git a/docker-compose.yml b/docker-compose.yml index 66361ade9c..92df204215 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,7 +8,7 @@ services: - postgres ports: - "8000:8080" - - "8005:8081" + - "8006:8081" environment: SPRING_PROFILES_ACTIVE: dev POSTGRESQL_SERVICE_PORT: '5432' @@ -38,9 +38,8 @@ services: CWA_OBJECTSTORE_BUCKET: cwa CWA_OBJECTSTORE_PORT: 8000 services.distribution.paths.output: /tmp/distribution - # Settings for cryptographic artefacts + # Settings for cryptographic artifacts VAULT_FILESIGNING_SECRET: ${SECRET_PRIVATE} - VAULT_FILESIGNING_CERT: ${SECRET_CERTIFICATE} volumes: - ./docker-compose-test-secrets:/secrets postgres: @@ -84,10 +83,10 @@ services: environment: - AWS_ACCESS_KEY_ID=${OBJECTSTORE_ACCESSKEY} - AWS_SECRET_ACCESS_KEY=${OBJECTSTORE_SECRETKEY} - entrypoint: ["/root/scripts/wait-for-it/wait-for-it.sh", "objectstore:8000", "-t", "30", "--"] + #entrypoint: ["/root/scripts/wait-for-it/wait-for-it.sh", "objectstore:8000", "-t", "30", "--"] volumes: - ./scripts/wait-for-it:/root/scripts/wait-for-it - command: aws s3api create-bucket --bucket cwa --endpoint-url http://objectstore:8000 --acl public-read + command: s3api create-bucket --bucket cwa --endpoint-url http://objectstore:8000 --acl public-read depends_on: - objectstore verification-fake: diff --git a/pom.xml b/pom.xml index af81a9f668..b7004c9644 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ org.opencwa server - 0.5.0 + ${revision} server CWA Server https://www.coronawarn.app/ @@ -37,6 +37,28 @@ + + + org.codehaus.mojo + flatten-maven-plugin + 1.1.0 + + true + resolveCiFriendliesOnly + + + + flatten + process-resources + flatten + + + flatten-clean + clean + clean + + + org.apache.maven.plugins maven-checkstyle-plugin diff --git a/scripts/.gitignore b/scripts/.gitignore index ba7dc224e6..81bbbae97b 100644 --- a/scripts/.gitignore +++ b/scripts/.gitignore @@ -1 +1 @@ -/certificates/ +keys/ diff --git a/scripts/generate_certificates.sh b/scripts/generate_certificates.sh deleted file mode 100755 index 3490256a8f..0000000000 --- a/scripts/generate_certificates.sh +++ /dev/null @@ -1,89 +0,0 @@ -#!/bin/bash - -# Generates a new self-signed X.509 root certificate and a client certificate. Only to be used for -# testing purposes. This script requires openssl to be installed, see here: -## https://www.openssl.org/source/ - -pushd "$(dirname "${BASH_SOURCE[0]}")" > /dev/null || exit - -rm -rf certificates -mkdir certificates -pushd certificates > /dev/null || exit - -# Generate a private key -# $1 = OUT Private key file -generate_private_key() -{ - openssl ecparam \ - -name prime256v1 \ - -genkey \ - -noout \ - -out "$1" -} - -# Generate certificate signing request -# $1 = IN New certificate private key -# $2 = IN "Subject" of the certificate -# $3 = OUT Certificate signing request file -generate_certificate_signing_request() -{ - openssl req \ - -new \ - -key "$1" \ - -subj "$2" \ - -out "$3" -} - -# Self-sign a certificate request -# $1 = IN Certificate signing request file -# $2 = IN Certificate authority private key file -# $3 = OUT New certificate file -self_sign_certificate_request() -{ - openssl x509 \ - -req \ - -days 365 \ - -in "$1" \ - -signkey "$2" \ - -out "$3" -} - -# Sign a certificate request using a CA certificate + private key -# $1 = IN Certificate signing request file -# $2 = IN Certificate authority certificate file -# $3 = IN Certificate authority private key file -# $4 = OUT New certificate file -ca_sign_certificate_request() -{ - openssl x509 \ - -req \ - -days 365 \ - -set_serial 100 \ - -in "$1" \ - -CA "$2" \ - -CAkey "$3" \ - -out "$4" -} - -# Self-signed root certificate -mkdir root -generate_private_key root/private.pem -generate_certificate_signing_request root/private.pem '/CN=CWA Test Root Certificate' root/request.csr -self_sign_certificate_request root/request.csr root/private.pem root/certificate.crt - -# Client certificate signed by root certificate -mkdir client -generate_private_key client/private.pem -generate_certificate_signing_request client/private.pem '/CN=CWA Test Client Certificate' client/request.csr -ca_sign_certificate_request client/request.csr root/certificate.crt root/private.pem client/certificate.crt - -# Concatenate the root certificate and the client certificate. -# This way, we have the whole certificate chain in a single file. -mkdir chain -cat client/certificate.crt root/certificate.crt >> chain/certificate.crt - -# Final verification of the certificate chain -openssl verify -CAfile root/certificate.crt chain/certificate.crt - -popd > /dev/null || exit -popd > /dev/null || exit \ No newline at end of file diff --git a/scripts/generate_keys.sh b/scripts/generate_keys.sh new file mode 100644 index 0000000000..1f4b73ed31 --- /dev/null +++ b/scripts/generate_keys.sh @@ -0,0 +1,68 @@ +#!/bin/bash + +# Generates a prime256v1 EC private key and extracts the respective public key. +# This script requires openssl to be installed, see here: +## https://www.openssl.org/source/ + +pushd "$(dirname "${BASH_SOURCE[0]}")" > /dev/null || exit + +rm -rf keys +mkdir keys +pushd keys > /dev/null || exit + +# Generate a prime256v1 EC private key +# $1 = OUT Private key file +generate_private_key() +{ + openssl ecparam \ + -name prime256v1 \ + -genkey \ + -out "$1" \ + -noout +} + +# Extract the public key from the private key +# $1 = IN Private key +# $2 = OUT Public key +extract_public_key() +{ + openssl ec \ + -in "$1" \ + -pubout \ + -out "$2" +} + +# Generate certificate signing request +# $1 = IN New certificate private key +# $2 = IN "Subject" of the certificate +# $3 = OUT Certificate signing request file +generate_certificate_signing_request() +{ + openssl req \ + -new \ + -key "$1" \ + -subj "$2" \ + -out "$3" +} + +# Self-sign a certificate request +# $1 = IN Certificate signing request file +# $2 = IN Certificate authority private key file +# $3 = OUT New certificate file +self_sign_certificate_request() +{ + openssl x509 \ + -req \ + -days 365 \ + -in "$1" \ + -signkey "$2" \ + -out "$3" +} + +generate_private_key private.pem +extract_public_key private.pem public.pem +generate_certificate_signing_request private.pem '/CN=CWA Test Certificate' request.csr +self_sign_certificate_request request.csr private.pem certificate.crt + +popd > /dev/null || exit +popd > /dev/null || exit \ No newline at end of file diff --git a/scripts/wait-for-it/README.md b/scripts/wait-for-it/README.md index 4891736fd0..7bd323a636 100644 --- a/scripts/wait-for-it/README.md +++ b/scripts/wait-for-it/README.md @@ -1,10 +1,10 @@ -## wait-for-it +# wait-for-it `wait-for-it.sh` is a pure bash script that will wait on the availability of a host and TCP port. It is useful for synchronizing the spin-up of interdependent services, such as linked docker containers. Since it is a pure bash script, it does not have any external dependencies. ## Usage -``` +```sh wait-for-it.sh host:port [-s] [-t timeout] [-- command args] -h HOST | --host=HOST Host or IP under test -p PORT | --port=PORT TCP port under test @@ -20,7 +20,7 @@ wait-for-it.sh host:port [-s] [-t timeout] [-- command args] For example, let's test to see if we can access port 80 on www.google.com, and if it is available, echo the message `google is up`. -``` +```sh $ ./wait-for-it.sh www.google.com:80 -- echo "google is up" wait-for-it.sh: waiting 15 seconds for www.google.com:80 wait-for-it.sh: www.google.com:80 is available after 0 seconds @@ -29,7 +29,7 @@ google is up You can set your own timeout with the `-t` or `--timeout=` option. Setting the timeout value to 0 will disable the timeout: -``` +```sh $ ./wait-for-it.sh -t 0 www.google.com:80 -- echo "google is up" wait-for-it.sh: waiting for www.google.com:80 without a timeout wait-for-it.sh: www.google.com:80 is available after 0 seconds @@ -38,7 +38,7 @@ google is up The subcommand will be executed regardless if the service is up or not. If you wish to execute the subcommand only if the service is up, add the `--strict` argument. In this example, we will test port 81 on www.google.com which will fail: -``` +```sh $ ./wait-for-it.sh www.google.com:81 --timeout=1 --strict -- echo "google is up" wait-for-it.sh: waiting 1 seconds for www.google.com:81 wait-for-it.sh: timeout occurred after waiting 1 seconds for www.google.com:81 @@ -47,7 +47,7 @@ wait-for-it.sh: strict mode, refusing to execute subprocess If you don't want to execute a subcommand, leave off the `--` argument. This way, you can test the exit condition of `wait-for-it.sh` in your own scripts, and determine how to proceed: -``` +```sh $ ./wait-for-it.sh www.google.com:80 wait-for-it.sh: waiting 15 seconds for www.google.com:80 wait-for-it.sh: www.google.com:80 is available after 0 seconds diff --git a/services/distribution/pom.xml b/services/distribution/pom.xml index 23574e4b34..2c8048e60c 100644 --- a/services/distribution/pom.xml +++ b/services/distribution/pom.xml @@ -5,7 +5,7 @@ services org.opencwa - 0.5.0 + ${revision} ../pom.xml 4.0.0 @@ -52,7 +52,6 @@ distribution - 0.5.0 io.minio diff --git a/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/appconfig/structure/directory/AppConfigurationDirectory.java b/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/appconfig/structure/directory/AppConfigurationDirectory.java index 5b6708d4d5..d3da88d0b3 100644 --- a/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/appconfig/structure/directory/AppConfigurationDirectory.java +++ b/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/appconfig/structure/directory/AppConfigurationDirectory.java @@ -65,7 +65,7 @@ public AppConfigurationDirectory(CryptoProvider cryptoProvider, DistributionServ this.distributionServiceConfig = distributionServiceConfig; countryDirectory = new IndexDirectoryOnDisk<>(distributionServiceConfig.getApi().getCountryPath(), - x -> Set.of(distributionServiceConfig.getApi().getCountryGermany()), Object::toString); + __ -> Set.of(distributionServiceConfig.getApi().getCountryGermany()), Object::toString); addApplicationConfigurationIfValid(); @@ -79,7 +79,7 @@ private void addApplicationConfigurationIfValid() { addArchiveIfMessageValid(distributionServiceConfig.getApi().getAppConfigFileName(), appConfig, validator); } catch (UnableToLoadFileException e) { - logger.error("Exposure configuration will not be published! Unable to read configuration file from disk."); + logger.error("Exposure configuration will not be published! Unable to read configuration file from disk.", e); } } @@ -97,7 +97,7 @@ private void addArchiveIfMessageValid(String archiveName, Message message, Confi ArchiveOnDisk appConfigurationFile = new ArchiveOnDisk(archiveName); appConfigurationFile.addWritable(new FileOnDisk("export.bin", message.toByteArray())); - countryDirectory.addWritableToAll(x -> new AppConfigurationSigningDecorator(appConfigurationFile, cryptoProvider, + countryDirectory.addWritableToAll(__ -> new AppConfigurationSigningDecorator(appConfigurationFile, cryptoProvider, distributionServiceConfig)); } } diff --git a/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/appconfig/validation/RiskScoreClassificationValidator.java b/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/appconfig/validation/RiskScoreClassificationValidator.java index 66ef3cee7d..f04184a992 100644 --- a/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/appconfig/validation/RiskScoreClassificationValidator.java +++ b/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/appconfig/validation/RiskScoreClassificationValidator.java @@ -87,12 +87,10 @@ private void validateRiskScoreValueBounds(int value) { } private void validateUrl(String url) { - if (!url.isBlank()) { - try { - new URL(url); - } catch (MalformedURLException e) { - errors.add(new RiskScoreClassificationValidationError("url", url, INVALID_URL)); - } + try { + new URL(url.trim()); + } catch (MalformedURLException e) { + errors.add(new RiskScoreClassificationValidationError("url", url, INVALID_URL)); } } diff --git a/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/component/CryptoProvider.java b/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/component/CryptoProvider.java index e416995d14..680c56ac83 100644 --- a/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/component/CryptoProvider.java +++ b/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/component/CryptoProvider.java @@ -20,7 +20,6 @@ package app.coronawarn.server.services.distribution.assembly.component; import app.coronawarn.server.services.distribution.config.DistributionServiceConfig; -import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; @@ -28,9 +27,6 @@ import java.security.KeyPair; import java.security.PrivateKey; import java.security.Security; -import java.security.cert.Certificate; -import java.security.cert.CertificateException; -import java.security.cert.CertificateFactory; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.openssl.PEMKeyPair; import org.bouncycastle.openssl.PEMParser; @@ -40,77 +36,43 @@ import org.springframework.stereotype.Component; /** - * Wrapper component for a {@link CryptoProvider#getPrivateKey() private key} and a {@link - * CryptoProvider#getCertificate()} certificate} from the application properties. + * Wrapper component for a {@link CryptoProvider#getPrivateKey() private key} from the application properties. */ @Component public class CryptoProvider { - private final String privateKeyPath; - - private final String certificatePath; - - private final ResourceLoader resourceLoader; - - private PrivateKey privateKey; - private Certificate certificate; + private final PrivateKey privateKey; /** * Creates a CryptoProvider, using {@link BouncyCastleProvider}. */ CryptoProvider(ResourceLoader resourceLoader, DistributionServiceConfig distributionServiceConfig) { - this.resourceLoader = resourceLoader; - this.privateKeyPath = distributionServiceConfig.getPaths().getPrivateKey(); - this.certificatePath = distributionServiceConfig.getPaths().getCertificate(); + privateKey = loadPrivateKey(resourceLoader, distributionServiceConfig); Security.addProvider(new BouncyCastleProvider()); } - private static PrivateKey getPrivateKeyFromStream(final InputStream privateKeyStream) throws IOException { + private static PrivateKey getPrivateKeyFromStream(InputStream privateKeyStream) throws IOException { InputStreamReader privateKeyStreamReader = new InputStreamReader(privateKeyStream); Object parsed = new PEMParser(privateKeyStreamReader).readObject(); KeyPair pair = new JcaPEMKeyConverter().getKeyPair((PEMKeyPair) parsed); return pair.getPrivate(); } - private static Certificate getCertificateFromStream(final InputStream certificateStream) - throws IOException, CertificateException { - return getCertificateFromBytes(certificateStream.readAllBytes()); - } - - private static Certificate getCertificateFromBytes(final byte[] bytes) - throws CertificateException { - CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); - InputStream certificateByteStream = new ByteArrayInputStream(bytes); - return certificateFactory.generateCertificate(certificateByteStream); - } - /** - * Reads and returns the {@link PrivateKey} configured in the application properties. + * Returns the {@link PrivateKey} configured in the application properties. */ public PrivateKey getPrivateKey() { - if (privateKey == null) { - Resource privateKeyResource = resourceLoader.getResource(privateKeyPath); - try (InputStream privateKeyStream = privateKeyResource.getInputStream()) { - privateKey = getPrivateKeyFromStream(privateKeyStream); - } catch (IOException e) { - throw new UncheckedIOException("Failed to load private key from " + privateKeyPath, e); - } - } return privateKey; } - /** - * Reads and returns the {@link Certificate} configured in the application properties. - */ - public Certificate getCertificate() { - if (this.certificate == null) { - Resource certResource = resourceLoader.getResource(certificatePath); - try (InputStream certStream = certResource.getInputStream()) { - this.certificate = getCertificateFromStream(certStream); - } catch (IOException | CertificateException e) { - throw new RuntimeException("Failed to load certificate from " + certificatePath, e); - } + private PrivateKey loadPrivateKey(ResourceLoader resourceLoader, + DistributionServiceConfig distributionServiceConfig) { + String path = distributionServiceConfig.getPaths().getPrivateKey(); + Resource privateKeyResource = resourceLoader.getResource(path); + try (InputStream privateKeyStream = privateKeyResource.getInputStream()) { + return getPrivateKeyFromStream(privateKeyStream); + } catch (IOException e) { + throw new UncheckedIOException("Failed to load private key from " + path, e); } - return certificate; } } diff --git a/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/diagnosiskeys/structure/directory/DiagnosisKeysCountryDirectory.java b/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/diagnosiskeys/structure/directory/DiagnosisKeysCountryDirectory.java index 8fe599d007..4f88461f62 100644 --- a/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/diagnosiskeys/structure/directory/DiagnosisKeysCountryDirectory.java +++ b/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/diagnosiskeys/structure/directory/DiagnosisKeysCountryDirectory.java @@ -47,7 +47,7 @@ public class DiagnosisKeysCountryDirectory extends IndexDirectoryOnDisk */ public DiagnosisKeysCountryDirectory(Collection diagnosisKeys, CryptoProvider cryptoProvider, DistributionServiceConfig distributionServiceConfig) { - super(distributionServiceConfig.getApi().getCountryPath(), x -> + super(distributionServiceConfig.getApi().getCountryPath(), __ -> Set.of(distributionServiceConfig.getApi().getCountryGermany()), Object::toString); this.diagnosisKeys = diagnosisKeys; this.cryptoProvider = cryptoProvider; diff --git a/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/diagnosiskeys/structure/directory/DiagnosisKeysDateDirectory.java b/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/diagnosiskeys/structure/directory/DiagnosisKeysDateDirectory.java index d5936eeb76..dc60756c1c 100644 --- a/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/diagnosiskeys/structure/directory/DiagnosisKeysDateDirectory.java +++ b/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/diagnosiskeys/structure/directory/DiagnosisKeysDateDirectory.java @@ -49,7 +49,7 @@ public class DiagnosisKeysDateDirectory extends IndexDirectoryOnDisk */ public DiagnosisKeysDateDirectory(Collection diagnosisKeys, CryptoProvider cryptoProvider, DistributionServiceConfig distributionServiceConfig) { - super(distributionServiceConfig.getApi().getDatePath(), x -> DateTime.getDates(diagnosisKeys), ISO8601::format); + super(distributionServiceConfig.getApi().getDatePath(), __ -> DateTime.getDates(diagnosisKeys), ISO8601::format); this.cryptoProvider = cryptoProvider; this.diagnosisKeys = diagnosisKeys; this.distributionServiceConfig = distributionServiceConfig; diff --git a/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/diagnosiskeys/structure/file/TemporaryExposureKeyExportFile.java b/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/diagnosiskeys/structure/file/TemporaryExposureKeyExportFile.java index bf6785a27d..bfd399fb35 100644 --- a/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/diagnosiskeys/structure/file/TemporaryExposureKeyExportFile.java +++ b/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/diagnosiskeys/structure/file/TemporaryExposureKeyExportFile.java @@ -103,7 +103,6 @@ private static Set getTemporaryExposureKeysFromDiagnosisKe return diagnosisKeys.stream().map(diagnosisKey -> TemporaryExposureKey.newBuilder() .setKeyData(ByteString.copyFrom(diagnosisKey.getKeyData())) .setTransmissionRiskLevel(diagnosisKey.getTransmissionRiskLevel()) - // TODO cwa-server/#233 Rolling start number and period should be int32 .setRollingStartIntervalNumber(diagnosisKey.getRollingStartIntervalNumber()) .setRollingPeriod(diagnosisKey.getRollingPeriod()) .build()) diff --git a/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/diagnosiskeys/util/DateTime.java b/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/diagnosiskeys/util/DateTime.java index 122a1749ba..01a0074af8 100644 --- a/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/diagnosiskeys/util/DateTime.java +++ b/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/diagnosiskeys/util/DateTime.java @@ -33,7 +33,7 @@ */ public class DateTime { - private DateTime(){ + private DateTime() { } /** diff --git a/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/structure/archive/ArchiveOnDisk.java b/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/structure/archive/ArchiveOnDisk.java index ee0e543fde..43390bb802 100644 --- a/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/structure/archive/ArchiveOnDisk.java +++ b/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/structure/archive/ArchiveOnDisk.java @@ -35,16 +35,12 @@ import java.util.Set; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * An {@link Archive} that can be written to disk as a ZIP archive. */ public class ArchiveOnDisk extends FileOnDisk implements Archive { - private static final Logger logger = LoggerFactory.getLogger(ArchiveOnDisk.class); - private DirectoryOnDisk tempDirectory; /** diff --git a/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/structure/file/FileOnDisk.java b/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/structure/file/FileOnDisk.java index 7979fe08d7..b2af9d6d56 100644 --- a/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/structure/file/FileOnDisk.java +++ b/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/structure/file/FileOnDisk.java @@ -37,8 +37,8 @@ public FileOnDisk(String name, byte[] bytes) { } /** - * Creates a {@link java.io.File} with name {@link Writable#getName} on disk and writes the - * {@link File#getBytes bytes} of this {@link File} into that {@link java.io.File}. + * Creates a {@link java.io.File} with name {@link Writable#getName} on disk and writes the {@link File#getBytes + * bytes} of this {@link File} into that {@link java.io.File}. */ @Override public void write() { @@ -61,8 +61,6 @@ public void setBytes(byte[] bytes) { */ @Override public void prepare(ImmutableStack indices) { - /* - Method override exists here to comply with the implementation rules for the Writable interface. - */ + // Method override exists here to comply with the implementation rules for the Writable interface. } } diff --git a/services/distribution/src/main/java/app/coronawarn/server/services/distribution/config/DistributionServiceConfig.java b/services/distribution/src/main/java/app/coronawarn/server/services/distribution/config/DistributionServiceConfig.java index abe31d1546..d0a4882bc6 100644 --- a/services/distribution/src/main/java/app/coronawarn/server/services/distribution/config/DistributionServiceConfig.java +++ b/services/distribution/src/main/java/app/coronawarn/server/services/distribution/config/DistributionServiceConfig.java @@ -156,7 +156,6 @@ public void setExposuresPerHour(Integer exposuresPerHour) { public static class Paths { private String privateKey; - private String certificate; private String output; public String getPrivateKey() { @@ -167,14 +166,6 @@ public void setPrivateKey(String privateKey) { this.privateKey = privateKey; } - public String getCertificate() { - return certificate; - } - - public void setCertificate(String certificate) { - this.certificate = certificate; - } - public String getOutput() { return output; } @@ -272,7 +263,6 @@ public void setAppConfigFileName(String appConfigFileName) { public static class Signature { private String appBundleId; - private String androidPackage; private String verificationKeyId; private String verificationKeyVersion; private String algorithmOid; @@ -288,14 +278,6 @@ public void setAppBundleId(String appBundleId) { this.appBundleId = appBundleId; } - public String getAndroidPackage() { - return androidPackage; - } - - public void setAndroidPackage(String androidPackage) { - this.androidPackage = androidPackage; - } - public String getVerificationKeyId() { return verificationKeyId; } @@ -348,10 +330,8 @@ public void setSecurityProvider(String securityProvider) { * Returns the static {@link SignatureInfo} configured in the application properties. TODO Enter correct values. */ public SignatureInfo getSignatureInfo() { - // TODO cwa-server#183 cwa-server#207 cwa-server#238 return SignatureInfo.newBuilder() .setAppBundleId(this.getAppBundleId()) - .setAndroidPackage(this.getAndroidPackage()) .setVerificationKeyVersion(this.getVerificationKeyVersion()) .setVerificationKeyId(this.getVerificationKeyId()) .setSignatureAlgorithm(this.getAlgorithmOid()) diff --git a/services/distribution/src/main/java/app/coronawarn/server/services/distribution/objectstore/ObjectStoreAccess.java b/services/distribution/src/main/java/app/coronawarn/server/services/distribution/objectstore/ObjectStoreAccess.java index adec54bb0f..7a58fc15bb 100644 --- a/services/distribution/src/main/java/app/coronawarn/server/services/distribution/objectstore/ObjectStoreAccess.java +++ b/services/distribution/src/main/java/app/coronawarn/server/services/distribution/objectstore/ObjectStoreAccess.java @@ -20,19 +20,17 @@ package app.coronawarn.server.services.distribution.objectstore; import app.coronawarn.server.services.distribution.config.DistributionServiceConfig; -import app.coronawarn.server.services.distribution.config.DistributionServiceConfig.ObjectStore; import app.coronawarn.server.services.distribution.objectstore.publish.LocalFile; import io.minio.MinioClient; import io.minio.PutObjectOptions; import io.minio.Result; -import io.minio.errors.InvalidEndpointException; -import io.minio.errors.InvalidPortException; import io.minio.errors.MinioException; import io.minio.messages.DeleteError; import io.minio.messages.Item; import java.io.IOException; import java.security.GeneralSecurityException; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -58,7 +56,11 @@ public class ObjectStoreAccess { private static final Logger logger = LoggerFactory.getLogger(ObjectStoreAccess.class); - private static final String DEFAULT_REGION = "eu-west-1"; + /** + * Specifies the default maximum amount of time in seconds that a published resource can be considered "fresh" when + * held in a cache. + */ + public static final int DEFAULT_MAX_CACHE_AGE = 300; private final boolean isSetPublicReadAclOnPutObject; @@ -71,14 +73,14 @@ public class ObjectStoreAccess { * bucket. * * @param distributionServiceConfig The config properties + * @param minioClient The client used for interaction with the object store * @throws IOException When there were problems creating the S3 client * @throws GeneralSecurityException When there were problems creating the S3 client * @throws MinioException When there were problems creating the S3 client */ - ObjectStoreAccess(DistributionServiceConfig distributionServiceConfig) + ObjectStoreAccess(DistributionServiceConfig distributionServiceConfig, MinioClient minioClient) throws IOException, GeneralSecurityException, MinioException { - this.client = createClient(distributionServiceConfig.getObjectStore()); - + this.client = minioClient; this.bucket = distributionServiceConfig.getObjectStore().getBucket(); this.isSetPublicReadAclOnPutObject = distributionServiceConfig.getObjectStore().isSetPublicReadAclOnPutObject(); @@ -87,38 +89,25 @@ public class ObjectStoreAccess { } } - private MinioClient createClient(ObjectStore objectStore) - throws InvalidPortException, InvalidEndpointException { - if (isSsl(objectStore)) { - return new MinioClient( - objectStore.getEndpoint(), - objectStore.getPort(), - objectStore.getAccessKey(), objectStore.getSecretKey(), - DEFAULT_REGION, - true - ); - } else { - return new MinioClient( - objectStore.getEndpoint(), - objectStore.getPort(), - objectStore.getAccessKey(), objectStore.getSecretKey() - ); - } - } - - private boolean isSsl(ObjectStore objectStore) { - return objectStore.getEndpoint().startsWith("https://"); + /** + * Stores the target file on the S3 and sets cache control headers according to the default maximum age value. + * + * @param localFile The file to be published. + */ + public void putObject(LocalFile localFile) throws IOException, GeneralSecurityException, MinioException { + putObject(localFile, DEFAULT_MAX_CACHE_AGE); } /** - * Stores the target file on the S3. + * Stores the target file on the S3 and sets cache control headers according to the specified maximum age value. * - * @param localFile the file to be published + * @param localFile The file to be published. + * @param maxAge A cache control parameter that specifies the maximum amount of time in seconds that a resource can + * be considered "fresh" when held in a cache. */ - public void putObject(LocalFile localFile) - throws IOException, GeneralSecurityException, MinioException { + public void putObject(LocalFile localFile, int maxAge) throws IOException, GeneralSecurityException, MinioException { String s3Key = localFile.getS3Key(); - PutObjectOptions options = createOptionsFor(localFile); + PutObjectOptions options = createOptionsFor(localFile, maxAge); logger.info("... uploading {}", s3Key); this.client.putObject(bucket, s3Key, localFile.getFile().toString(), options); @@ -129,7 +118,7 @@ public void putObject(LocalFile localFile) * * @param prefix the prefix, e.g. my/folder/ */ - public List deleteObjectsWithPrefix(String prefix) + public void deleteObjectsWithPrefix(String prefix) throws MinioException, GeneralSecurityException, IOException { List toDelete = getObjectsWithPrefix(prefix) .stream() @@ -144,9 +133,9 @@ public List deleteObjectsWithPrefix(String prefix) errors.add(deleteErrorResult.get()); } - logger.info("Deletion result: {}", errors.size()); - - return errors; + if (!errors.isEmpty()) { + throw new MinioException("Can't delete files, number of errors: " + errors.size()); + } } /** @@ -167,12 +156,14 @@ public List getObjectsWithPrefix(String prefix) return list; } - private PutObjectOptions createOptionsFor(LocalFile file) { + private PutObjectOptions createOptionsFor(LocalFile file, int maxAge) { var options = new PutObjectOptions(file.getFile().toFile().length(), -1); + Map headers = new HashMap<>(Map.of("cache-control", "public,max-age=" + maxAge)); if (this.isSetPublicReadAclOnPutObject) { - options.setHeaders(Map.of("x-amz-acl", "public-read")); + headers.put("x-amz-acl", "public-read"); } + options.setHeaders(headers); return options; } diff --git a/services/distribution/src/main/java/app/coronawarn/server/services/distribution/objectstore/ObjectStoreClientConfig.java b/services/distribution/src/main/java/app/coronawarn/server/services/distribution/objectstore/ObjectStoreClientConfig.java new file mode 100644 index 0000000000..e623d94723 --- /dev/null +++ b/services/distribution/src/main/java/app/coronawarn/server/services/distribution/objectstore/ObjectStoreClientConfig.java @@ -0,0 +1,66 @@ +/* + * Corona-Warn-App + * + * SAP SE and all other contributors / + * copyright owners license this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ + +package app.coronawarn.server.services.distribution.objectstore; + +import app.coronawarn.server.services.distribution.config.DistributionServiceConfig; +import app.coronawarn.server.services.distribution.config.DistributionServiceConfig.ObjectStore; +import io.minio.MinioClient; +import io.minio.errors.InvalidEndpointException; +import io.minio.errors.InvalidPortException; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Manages the instantiation of the {@link MinioClient} bean. + */ +@Configuration +public class ObjectStoreClientConfig { + + private static final String DEFAULT_REGION = "eu-west-1"; + + @Bean + public MinioClient createMinioClient(DistributionServiceConfig distributionServiceConfig) + throws InvalidPortException, InvalidEndpointException { + return createClient(distributionServiceConfig.getObjectStore()); + } + + private MinioClient createClient(ObjectStore objectStore) + throws InvalidPortException, InvalidEndpointException { + if (isSsl(objectStore)) { + return new MinioClient( + objectStore.getEndpoint(), + objectStore.getPort(), + objectStore.getAccessKey(), objectStore.getSecretKey(), + DEFAULT_REGION, + true + ); + } else { + return new MinioClient( + objectStore.getEndpoint(), + objectStore.getPort(), + objectStore.getAccessKey(), objectStore.getSecretKey() + ); + } + } + + private boolean isSsl(ObjectStore objectStore) { + return objectStore.getEndpoint().startsWith("https://"); + } +} diff --git a/services/distribution/src/main/java/app/coronawarn/server/services/distribution/objectstore/S3RetentionPolicy.java b/services/distribution/src/main/java/app/coronawarn/server/services/distribution/objectstore/S3RetentionPolicy.java new file mode 100644 index 0000000000..0ff308600c --- /dev/null +++ b/services/distribution/src/main/java/app/coronawarn/server/services/distribution/objectstore/S3RetentionPolicy.java @@ -0,0 +1,90 @@ +/* + * Corona-Warn-App + * + * SAP SE and all other contributors / + * copyright owners license this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ + +package app.coronawarn.server.services.distribution.objectstore; + +import app.coronawarn.server.services.distribution.config.DistributionServiceConfig; +import app.coronawarn.server.services.distribution.config.DistributionServiceConfig.Api; +import io.minio.errors.MinioException; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.security.GeneralSecurityException; +import java.time.LocalDate; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * Creates an S3RetentionPolicy object, which applies the retention policy to the S3 compatible storage. + */ +@Component +public class S3RetentionPolicy { + + private final ObjectStoreAccess objectStoreAccess; + private final Api api; + + @Autowired + public S3RetentionPolicy(ObjectStoreAccess objectStoreAccess, DistributionServiceConfig distributionServiceConfig) { + this.objectStoreAccess = objectStoreAccess; + this.api = distributionServiceConfig.getApi(); + } + + /** + * Deletes all diagnosis-key files from S3 that are older than retentionDays. + * + * @param retentionDays the number of days, that files should be retained on S3. + */ + public void applyRetentionPolicy(int retentionDays) throws MinioException, GeneralSecurityException, IOException { + List diagnosisKeysObjects = objectStoreAccess.getObjectsWithPrefix("version/v1/" + + api.getDiagnosisKeysPath() + "/" + + api.getCountryPath() + "/" + + api.getCountryGermany() + "/" + + api.getDatePath() + "/"); + final String regex = ".*([0-9]{4}-[0-9]{2}-[0-9]{2}).*"; + final Pattern pattern = Pattern.compile(regex); + + final LocalDate cutOffDate = LocalDate.now(ZoneOffset.UTC).minusDays(retentionDays); + + diagnosisKeysObjects.stream() + .filter(diagnosisKeysObject -> { + Matcher matcher = pattern.matcher(diagnosisKeysObject.getObjectName()); + return matcher.matches() && LocalDate.parse(matcher.group(1), DateTimeFormatter.ISO_LOCAL_DATE) + .isBefore(cutOffDate); + }) + .forEach(this::deleteDiagnosisKey); + } + + /** + * Java stream do not support checked exceptions within streams. This helper method rethrows them as unchecked + * expressions, so they can be passed up to the Retention Policy. + * + * @param diagnosisKey the diagnosis key, that should be deleted. + */ + public void deleteDiagnosisKey(S3Object diagnosisKey) { + try { + objectStoreAccess.deleteObjectsWithPrefix(diagnosisKey.getObjectName()); + } catch (Exception e) { + throw new UncheckedIOException(new IOException(e)); + } + } +} diff --git a/services/distribution/src/main/java/app/coronawarn/server/services/distribution/runner/RetentionPolicy.java b/services/distribution/src/main/java/app/coronawarn/server/services/distribution/runner/RetentionPolicy.java index f9eae32d4a..b81fe3a03c 100644 --- a/services/distribution/src/main/java/app/coronawarn/server/services/distribution/runner/RetentionPolicy.java +++ b/services/distribution/src/main/java/app/coronawarn/server/services/distribution/runner/RetentionPolicy.java @@ -22,6 +22,7 @@ import app.coronawarn.server.common.persistence.service.DiagnosisKeyService; import app.coronawarn.server.services.distribution.Application; import app.coronawarn.server.services.distribution.config.DistributionServiceConfig; +import app.coronawarn.server.services.distribution.objectstore.S3RetentionPolicy; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.ApplicationArguments; @@ -46,27 +47,32 @@ public class RetentionPolicy implements ApplicationRunner { private final Integer retentionDays; + private final S3RetentionPolicy s3RetentionPolicy; + /** * Creates a new RetentionPolicy. */ RetentionPolicy(DiagnosisKeyService diagnosisKeyService, ApplicationContext applicationContext, - DistributionServiceConfig distributionServiceConfig) { + DistributionServiceConfig distributionServiceConfig, + S3RetentionPolicy s3RetentionPolicy) { this.diagnosisKeyService = diagnosisKeyService; this.applicationContext = applicationContext; this.retentionDays = distributionServiceConfig.getRetentionDays(); + this.s3RetentionPolicy = s3RetentionPolicy; } @Override public void run(ApplicationArguments args) { try { diagnosisKeyService.applyRetentionPolicy(retentionDays); + s3RetentionPolicy.applyRetentionPolicy(retentionDays); } catch (Exception e) { logger.error("Application of retention policy failed.", e); Application.killApplication(applicationContext); } - logger.debug("Retention policy applied successfully. Deleted all entries older that {} days.", + logger.debug("Retention policy applied successfully. Deleted all entries older than {} days.", retentionDays); } } diff --git a/services/distribution/src/main/resources/application-dev.yaml b/services/distribution/src/main/resources/application-dev.yaml index e437ef440a..40fae9b692 100644 --- a/services/distribution/src/main/resources/application-dev.yaml +++ b/services/distribution/src/main/resources/application-dev.yaml @@ -6,3 +6,8 @@ logging: web: DEBUG app: coronawarn: DEBUG + +services: + distribution: + signature: + app-bundle-id: de.rki.coronawarnapp-dev \ No newline at end of file diff --git a/services/distribution/src/main/resources/application.yaml b/services/distribution/src/main/resources/application.yaml index 169965787b..d5211b3c73 100644 --- a/services/distribution/src/main/resources/application.yaml +++ b/services/distribution/src/main/resources/application.yaml @@ -16,7 +16,6 @@ services: paths: output: out privatekey: ${VAULT_FILESIGNING_SECRET} - certificate: ${VAULT_FILESIGNING_CERT} tek-export: file-name: export.bin file-header: EK Export v1 @@ -33,9 +32,8 @@ services: app-config-file-name: app_config signature: app-bundle-id: de.rki.coronawarnapp - android-package: de.rki.coronawarnapp - verification-key-id: - verification-key-version: + verification-key-id: 262 + verification-key-version: v1 algorithm-oid: 1.2.840.10045.4.3.2 algorithm-name: SHA256withECDSA file-name: export.sig diff --git a/services/distribution/src/test/java/app/coronawarn/server/services/distribution/assembly/appconfig/validation/RiskScoreClassificationValidatorTest.java b/services/distribution/src/test/java/app/coronawarn/server/services/distribution/assembly/appconfig/validation/RiskScoreClassificationValidatorTest.java index 87f3187ca7..da6f5829fb 100644 --- a/services/distribution/src/test/java/app/coronawarn/server/services/distribution/assembly/appconfig/validation/RiskScoreClassificationValidatorTest.java +++ b/services/distribution/src/test/java/app/coronawarn/server/services/distribution/assembly/appconfig/validation/RiskScoreClassificationValidatorTest.java @@ -39,19 +39,19 @@ class RiskScoreClassificationValidatorTest { private final static int MAX_SCORE = ParameterSpec.RISK_SCORE_MAX; private final static String VALID_LABEL = "myLabel"; - private final static String VALID_URL = ""; + private final static String VALID_URL = "https://www.my.url"; @ParameterizedTest @ValueSource(strings = {"", " "}) void failsForBlankLabels(String invalidLabel) { - var validator = buildValidator(buildRiskClass(invalidLabel, 0, MAX_SCORE, "")); + var validator = buildValidator(buildRiskClass(invalidLabel, 0, MAX_SCORE, VALID_URL)); var expectedResult = buildExpectedResult(buildError("label", invalidLabel, ErrorType.BLANK_LABEL)); assertThat(validator.validate()).isEqualTo(expectedResult); } @ParameterizedTest - @ValueSource(strings = {"invalid.Url", "invalid-url", "$$$://invalid.url"}) + @ValueSource(strings = {"invalid.Url", "invalid-url", "$$$://invalid.url", "", " "}) void failsForInvalidUrl(String invalidUrl) { var validator = buildValidator(buildRiskClass(VALID_LABEL, 0, MAX_SCORE, invalidUrl)); var expectedResult = buildExpectedResult(buildError("url", invalidUrl, ErrorType.INVALID_URL)); @@ -133,10 +133,8 @@ void doesNotFailForValidClassification(RiskScoreClassification validClassificati private static Stream createValidClassifications() { return Stream.of( - // blank url - buildClassification(buildRiskClass(VALID_LABEL, 0, MAX_SCORE, "")), - // legit url - buildClassification(buildRiskClass(VALID_LABEL, 0, MAX_SCORE, "http://www.sap.com")), + // valid url + buildClassification(buildRiskClass(VALID_LABEL, 0, MAX_SCORE, VALID_URL)), // [0:MAX_SCORE/2][MAX_SCORE/2:MAX_SCORE] buildClassification( buildRiskClass(VALID_LABEL, 0, MAX_SCORE / 2, VALID_URL), diff --git a/services/distribution/src/test/java/app/coronawarn/server/services/distribution/assembly/component/CryptoProviderTest.java b/services/distribution/src/test/java/app/coronawarn/server/services/distribution/assembly/component/CryptoProviderTest.java index 5e8f24a3be..84f722f028 100644 --- a/services/distribution/src/test/java/app/coronawarn/server/services/distribution/assembly/component/CryptoProviderTest.java +++ b/services/distribution/src/test/java/app/coronawarn/server/services/distribution/assembly/component/CryptoProviderTest.java @@ -42,6 +42,5 @@ class CryptoProviderTest { @Test void constructorInitializesCryptoArtifacts() { assertThat(cryptoProvider.getPrivateKey()).isNotNull(); - assertThat(cryptoProvider.getCertificate()).isNotNull(); } } diff --git a/services/distribution/src/test/java/app/coronawarn/server/services/distribution/assembly/structure/archive/decorator/signing/SigningDecoratorTest.java b/services/distribution/src/test/java/app/coronawarn/server/services/distribution/assembly/structure/archive/decorator/signing/SigningDecoratorTest.java index 7a4f45e5b8..7b2fa45e56 100644 --- a/services/distribution/src/test/java/app/coronawarn/server/services/distribution/assembly/structure/archive/decorator/signing/SigningDecoratorTest.java +++ b/services/distribution/src/test/java/app/coronawarn/server/services/distribution/assembly/structure/archive/decorator/signing/SigningDecoratorTest.java @@ -15,12 +15,17 @@ import app.coronawarn.server.services.distribution.assembly.structure.file.FileOnDisk; import app.coronawarn.server.services.distribution.assembly.structure.util.ImmutableStack; import app.coronawarn.server.services.distribution.config.DistributionServiceConfig; +import java.io.ByteArrayInputStream; import java.io.IOException; +import java.io.InputStream; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; import java.security.Signature; import java.security.SignatureException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; import java.util.List; import org.junit.Rule; import org.junit.jupiter.api.BeforeEach; @@ -30,6 +35,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.test.context.ConfigFileApplicationContextInitializer; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; @@ -42,6 +49,9 @@ class SigningDecoratorTest { @Autowired CryptoProvider cryptoProvider; + @Autowired + ResourceLoader resourceLoader; + @Autowired DistributionServiceConfig distributionServiceConfig; @@ -95,22 +105,29 @@ void checkSignatureInfo() { SignatureInfo signatureInfo = signatureList.getSignaturesList().get(0).getSignatureInfo(); assertThat(signatureInfo.getAppBundleId()).isEqualTo("de.rki.coronawarnapp"); - assertThat(signatureInfo.getAndroidPackage()).isEqualTo("de.rki.coronawarnapp"); assertThat(signatureInfo.getSignatureAlgorithm()).isEqualTo("1.2.840.10045.4.3.2"); - assertThat(signatureInfo.getVerificationKeyId()).isEmpty(); - assertThat(signatureInfo.getVerificationKeyVersion()).isEmpty(); + assertThat(signatureInfo.getVerificationKeyId()).isEqualTo("262"); + assertThat(signatureInfo.getVerificationKeyVersion()).isEqualTo("v1"); } @Test void checkSignature() - throws NoSuchProviderException, NoSuchAlgorithmException, InvalidKeyException, SignatureException { + throws NoSuchProviderException, NoSuchAlgorithmException, SignatureException, IOException, InvalidKeyException, CertificateException { byte[] fileBytes = fileToSign.getBytes(); byte[] signatureBytes = signatureList.getSignaturesList().get(0).getSignature().toByteArray(); - Signature payloadSignature = Signature.getInstance("SHA256withECDSA", "BC"); - payloadSignature.initVerify(cryptoProvider.getCertificate()); - payloadSignature.update(fileBytes); - assertThat(payloadSignature.verify(signatureBytes)).isTrue(); + Resource certResource = resourceLoader.getResource("classpath:keys/certificate.crt"); + try (InputStream certStream = certResource.getInputStream()) { + byte[] bytes = certStream.readAllBytes(); + CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + InputStream certificateByteStream = new ByteArrayInputStream(bytes); + Certificate certificate = certificateFactory.generateCertificate(certificateByteStream); + + Signature payloadSignature = Signature.getInstance("SHA256withECDSA", "BC"); + payloadSignature.initVerify(certificate); + payloadSignature.update(fileBytes); + assertThat(payloadSignature.verify(signatureBytes)).isTrue(); + } } private static class TestSigningDecorator extends SigningDecoratorOnDisk { diff --git a/services/distribution/src/test/java/app/coronawarn/server/services/distribution/objectstore/ObjectStoreAccessUnitTest.java b/services/distribution/src/test/java/app/coronawarn/server/services/distribution/objectstore/ObjectStoreAccessUnitTest.java new file mode 100644 index 0000000000..ab187dfc66 --- /dev/null +++ b/services/distribution/src/test/java/app/coronawarn/server/services/distribution/objectstore/ObjectStoreAccessUnitTest.java @@ -0,0 +1,113 @@ +/* + * Corona-Warn-App + * + * SAP SE and all other contributors / + * copyright owners license this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ + +package app.coronawarn.server.services.distribution.objectstore; + +import static java.util.Map.entry; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import app.coronawarn.server.services.distribution.config.DistributionServiceConfig; +import app.coronawarn.server.services.distribution.objectstore.publish.LocalFile; +import io.minio.MinioClient; +import io.minio.PutObjectOptions; +import java.io.File; +import java.nio.file.Path; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.ConfigFileApplicationContextInitializer; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +@EnableConfigurationProperties(value = DistributionServiceConfig.class) +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = {MinioClient.class}, initializers = ConfigFileApplicationContextInitializer.class) +class ObjectStoreAccessUnitTest { + + private static final String EXP_S3_KEY = "fooS3Key"; + private static final String EXP_FILE_CONTENT = "barFileContent"; + + private final DistributionServiceConfig distributionServiceConfig; + private final String expBucketName; + private LocalFile testLocalFile; + private ObjectStoreAccess objectStoreAccess; + + @MockBean + private MinioClient minioClient; + + @Autowired + public ObjectStoreAccessUnitTest(DistributionServiceConfig distributionServiceConfig) { + this.distributionServiceConfig = distributionServiceConfig; + this.expBucketName = distributionServiceConfig.getObjectStore().getBucket(); + } + + @BeforeEach + public void setUpMocks() throws Exception { + when(minioClient.bucketExists(any())).thenReturn(true); + this.objectStoreAccess = new ObjectStoreAccess(distributionServiceConfig, minioClient); + this.testLocalFile = setUpLocalFileMock(); + } + + private LocalFile setUpLocalFileMock() { + var testLocalFile = mock(LocalFile.class); + var testPath = mock(Path.class); + + when(testLocalFile.getS3Key()).thenReturn(EXP_S3_KEY); + when(testLocalFile.getFile()).thenReturn(testPath); + when(testPath.toFile()).thenReturn(mock(File.class)); + when(testPath.toString()).thenReturn(EXP_FILE_CONTENT); + + return testLocalFile; + } + + @Test + void testPutObjectSetsDefaultCacheControlHeader() throws Exception { + ArgumentCaptor options = ArgumentCaptor.forClass(PutObjectOptions.class); + var expHeader = entry("cache-control", "public,max-age=" + ObjectStoreAccess.DEFAULT_MAX_CACHE_AGE); + + objectStoreAccess.putObject(testLocalFile); + + verify(minioClient, atLeastOnce()) + .putObject(eq(expBucketName), eq(EXP_S3_KEY), eq(EXP_FILE_CONTENT), options.capture()); + Assertions.assertThat(options.getValue().headers()).contains(expHeader); + } + + @Test + void testPutObjectSetsSpecifiedCacheControlHeader() throws Exception { + ArgumentCaptor options = ArgumentCaptor.forClass(PutObjectOptions.class); + var expMaxAge = 1337; + var expHeader = entry("cache-control", "public,max-age=" + expMaxAge); + + objectStoreAccess.putObject(testLocalFile, expMaxAge); + + verify(minioClient, atLeastOnce()) + .putObject(eq(expBucketName), eq(EXP_S3_KEY), eq(EXP_FILE_CONTENT), options.capture()); + Assertions.assertThat(options.getValue().headers()).contains(expHeader); + } +} diff --git a/services/distribution/src/test/java/app/coronawarn/server/services/distribution/objectstore/S3RetentionPolicyTest.java b/services/distribution/src/test/java/app/coronawarn/server/services/distribution/objectstore/S3RetentionPolicyTest.java new file mode 100644 index 0000000000..4cf5a94424 --- /dev/null +++ b/services/distribution/src/test/java/app/coronawarn/server/services/distribution/objectstore/S3RetentionPolicyTest.java @@ -0,0 +1,92 @@ +/* + * Corona-Warn-App + * + * SAP SE and all other contributors / + * copyright owners license this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ + +package app.coronawarn.server.services.distribution.objectstore; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import app.coronawarn.server.services.distribution.config.DistributionServiceConfig; +import app.coronawarn.server.services.distribution.config.DistributionServiceConfig.ObjectStore; +import io.minio.errors.MinioException; +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.time.LocalDate; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.ConfigFileApplicationContextInitializer; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +@EnableConfigurationProperties(value = DistributionServiceConfig.class) +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = {S3RetentionPolicy.class, ObjectStore.class}, + initializers = ConfigFileApplicationContextInitializer.class) +class S3RetentionPolicyTest { + + @MockBean + private ObjectStoreAccess objectStoreAccess; + + @Autowired + private S3RetentionPolicy s3RetentionPolicy; + + @Autowired DistributionServiceConfig distributionServiceConfig; + + @Test + void shouldDeleteOldFiles() throws IOException, GeneralSecurityException, MinioException { + String expectedFileToBeDeleted = generateFileName(LocalDate.now().minusDays(2)); + + when(objectStoreAccess.getObjectsWithPrefix(any())).thenReturn(List.of( + new S3Object(expectedFileToBeDeleted), + new S3Object(generateFileName(LocalDate.now())), + new S3Object("version/v1/configuration/country/DE/app_config"))); + + s3RetentionPolicy.applyRetentionPolicy(1); + + verify(objectStoreAccess, atLeastOnce()).deleteObjectsWithPrefix(eq(expectedFileToBeDeleted)); + } + + @Test + void shouldNotDeleteFilesIfAllAreValid() throws IOException, GeneralSecurityException, MinioException { + when(objectStoreAccess.getObjectsWithPrefix(any())).thenReturn(List.of( + new S3Object(generateFileName(LocalDate.now().minusDays(1))), + new S3Object(generateFileName(LocalDate.now().plusDays(1))), + new S3Object(generateFileName(LocalDate.now())), + new S3Object("version/v1/configuration/country/DE/app_config"))); + + s3RetentionPolicy.applyRetentionPolicy(1); + + verify(objectStoreAccess, never()).deleteObjectsWithPrefix(any()); + } + + private String generateFileName(LocalDate date) { + var api = distributionServiceConfig.getApi(); + + return "version/v1/" + api.getDiagnosisKeysPath() + "/" + api.getCountryPath() + "/" + + api.getCountryGermany() + "/" + api.getDatePath() + "/" + date.toString() + "/hour/0"; + } +} diff --git a/services/distribution/src/test/resources/application.yaml b/services/distribution/src/test/resources/application.yaml index 95a624d9f1..fc0cfae272 100644 --- a/services/distribution/src/test/resources/application.yaml +++ b/services/distribution/src/test/resources/application.yaml @@ -13,8 +13,7 @@ services: include-incomplete-hours: false paths: output: out - privatekey: classpath:certificates/client/private.pem - certificate: classpath:certificates/chain/certificate.crt + privatekey: classpath:keys/private.pem tek-export: file-name: export.bin file-header: EK Export v1 @@ -31,9 +30,8 @@ services: app-config-file-name: app_config signature: app-bundle-id: de.rki.coronawarnapp - android-package: de.rki.coronawarnapp - verification-key-id: - verification-key-version: + verification-key-id: 262 + verification-key-version: v1 algorithm-oid: 1.2.840.10045.4.3.2 algorithm-name: SHA256withECDSA file-name: export.sig diff --git a/services/distribution/src/test/resources/certificates/chain/certificate.crt b/services/distribution/src/test/resources/certificates/chain/certificate.crt deleted file mode 100644 index dbb2a3a672..0000000000 --- a/services/distribution/src/test/resources/certificates/chain/certificate.crt +++ /dev/null @@ -1,18 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIBMjCB2AIBZDAKBggqhkjOPQQDAjAkMSIwIAYDVQQDDBlDV0EgVGVzdCBSb290 -IENlcnRpZmljYXRlMB4XDTIwMDUyMzExMzAzOFoXDTIxMDUyMzExMzAzOFowJjEk -MCIGA1UEAwwbQ1dBIFRlc3QgQ2xpZW50IENlcnRpZmljYXRlMFkwEwYHKoZIzj0C -AQYIKoZIzj0DAQcDQgAEYQJ+sReY1L8z851VFRpLu4PCusj/7Ruvi879KjrQJ12k -KKsfeRWytmrE65Jok1lsYqpFhRWcxG6VV5FX0yG+EjAKBggqhkjOPQQDAgNJADBG -AiEAwP/VKVIhOuiIczrPFg0o4ns39Wu1vBpIXu+/psI/3LECIQD0FAXo5chuUkQy -LeUtwsQaC8v2KA96ew3PfCoTilvU1Q== ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIIBQzCB6QIUN7Z6IofaE0qtz2X1xz/IUDCUXd0wCgYIKoZIzj0EAwIwJDEiMCAG -A1UEAwwZQ1dBIFRlc3QgUm9vdCBDZXJ0aWZpY2F0ZTAeFw0yMDA1MjMxMTMwMzha -Fw0yMTA1MjMxMTMwMzhaMCQxIjAgBgNVBAMMGUNXQSBUZXN0IFJvb3QgQ2VydGlm -aWNhdGUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATBH0/vPsD62wE9jk5JZZd+ -Yf4WXZ3sKZUGbdqJssc4lxjyZJvpPLCirRQT6XWwhmBhoL8mRL5tpZ/93o+jzULe -MAoGCCqGSM49BAMCA0kAMEYCIQCNPEtoC1QtgnncZzV/rgIoO6tktCiAkybBhHMB -1SISZQIhAPguoBWCscYwNHtEgTDx2sQ8UZ79KvWvpHFlwDEyiHAv ------END CERTIFICATE----- diff --git a/services/distribution/src/test/resources/certificates/client/private.pem b/services/distribution/src/test/resources/certificates/client/private.pem deleted file mode 100644 index 053142de82..0000000000 --- a/services/distribution/src/test/resources/certificates/client/private.pem +++ /dev/null @@ -1,5 +0,0 @@ ------BEGIN EC PRIVATE KEY----- -MHcCAQEEILQRQFlGcfeTAclubtjQ1rBjtmIOB/d7PITZyDe1r81/oAoGCCqGSM49 -AwEHoUQDQgAEYQJ+sReY1L8z851VFRpLu4PCusj/7Ruvi879KjrQJ12kKKsfeRWy -tmrE65Jok1lsYqpFhRWcxG6VV5FX0yG+Eg== ------END EC PRIVATE KEY----- diff --git a/services/distribution/src/test/resources/keys/certificate.crt b/services/distribution/src/test/resources/keys/certificate.crt new file mode 100644 index 0000000000..53ce1c0470 --- /dev/null +++ b/services/distribution/src/test/resources/keys/certificate.crt @@ -0,0 +1,9 @@ +-----BEGIN CERTIFICATE----- +MIIBNzCB3wIUFNO+5pQ8sXzK1SlHWDWdJyDqNPgwCgYIKoZIzj0EAwIwHzEdMBsG +A1UEAwwUQ1dBIFRlc3QgQ2VydGlmaWNhdGUwHhcNMjAwNTI3MTUxNDIxWhcNMjEw +NTI3MTUxNDIxWjAfMR0wGwYDVQQDDBRDV0EgVGVzdCBDZXJ0aWZpY2F0ZTBZMBMG +ByqGSM49AgEGCCqGSM49AwEHA0IABCrLdy73GUzX9IxppsdInYwhv/oVvHIARx2X +TdS+VzuAlH46wKb2l63z1uvZ9GE6iua+y3z6tu/SmAZw8R9rnIswCgYIKoZIzj0E +AwIDRwAwRAIgfyhreHW5Z7TI6hRh2hXF4RnzWY/9OLlJrUveGkV7tZQCIEf1BEXJ +AVEMhsjYJLBd1Ktb8v623WvY9gDFZrr+/whl +-----END CERTIFICATE----- diff --git a/services/distribution/src/test/resources/keys/private.pem b/services/distribution/src/test/resources/keys/private.pem new file mode 100644 index 0000000000..e330a3d3c0 --- /dev/null +++ b/services/distribution/src/test/resources/keys/private.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIMA+2YS0MAGyxH3gzHcR7DPzc4uC54X5+FAr4NWFeomwoAoGCCqGSM49 +AwEHoUQDQgAEKst3LvcZTNf0jGmmx0idjCG/+hW8cgBHHZdN1L5XO4CUfjrApvaX +rfPW69n0YTqK5r7LfPq279KYBnDxH2uciw== +-----END EC PRIVATE KEY----- diff --git a/services/pom.xml b/services/pom.xml index c00047a3af..1ddf53bdab 100644 --- a/services/pom.xml +++ b/services/pom.xml @@ -14,7 +14,7 @@ services org.opencwa - 0.5.0 + ${revision} pom @@ -35,12 +35,12 @@ org.opencwa persistence - 0.5.0 + ${project.version} org.opencwa protocols - 0.5.0 + ${project.version} org.springframework.boot @@ -89,6 +89,28 @@ + + + org.codehaus.mojo + flatten-maven-plugin + 1.1.0 + + true + resolveCiFriendliesOnly + + + + flatten + process-resources + flatten + + + flatten-clean + clean + clean + + + org.springframework.boot spring-boot-maven-plugin @@ -124,4 +146,4 @@ - \ No newline at end of file + diff --git a/services/submission/pom.xml b/services/submission/pom.xml index 7e93f979c0..9b13d6991d 100644 --- a/services/submission/pom.xml +++ b/services/submission/pom.xml @@ -5,13 +5,12 @@ services org.opencwa - 0.5.0 + ${revision} ../pom.xml 4.0.0 submission - 0.5.0 corona-warn-app_cwa-server_services_submission diff --git a/services/submission/src/main/java/app/coronawarn/server/services/submission/verification/TanVerifier.java b/services/submission/src/main/java/app/coronawarn/server/services/submission/verification/TanVerifier.java index f1a0078fc7..65539af9d5 100644 --- a/services/submission/src/main/java/app/coronawarn/server/services/submission/verification/TanVerifier.java +++ b/services/submission/src/main/java/app/coronawarn/server/services/submission/verification/TanVerifier.java @@ -49,12 +49,12 @@ public class TanVerifier { * This class can be used to verify a TAN against a configured verification service. * * @param submissionServiceConfig A submission service configuration - * @param restTemplateBuilder A rest template builder + * @param restTemplateBuilder A rest template builder */ @Autowired public TanVerifier(SubmissionServiceConfig submissionServiceConfig, RestTemplateBuilder restTemplateBuilder) { this.verificationServiceUrl = submissionServiceConfig.getVerificationBaseUrl() - + submissionServiceConfig.getVerificationPath(); + + submissionServiceConfig.getVerificationPath(); this.restTemplate = restTemplateBuilder.build(); this.requestHeader.setContentType(MediaType.APPLICATION_JSON); } @@ -88,6 +88,7 @@ private boolean checkTanSyntax(String tan) { UUID.fromString(tan); return true; } catch (IllegalArgumentException e) { + logger.debug("UUID creation failed for value: {}", tan, e); return false; } } @@ -107,8 +108,8 @@ private boolean verifyWithVerificationService(String tan) { ResponseEntity response = restTemplate.postForEntity(verificationServiceUrl, entity, String.class); return response.getStatusCode().is2xxSuccessful(); } catch (HttpClientErrorException.NotFound e) { - // The validation service returns http status 404 if the TAN is invalid - logger.debug("TAN validation failed for TAN: {}", tan); + // The verification service returns http status 404 if the TAN is invalid + logger.debug("TAN verification failed"); return false; } } diff --git a/services/submission/src/main/resources/application-cloud.yaml b/services/submission/src/main/resources/application-cloud.yaml index 00eaeb11b6..7878484f92 100644 --- a/services/submission/src/main/resources/application-cloud.yaml +++ b/services/submission/src/main/resources/application-cloud.yaml @@ -12,3 +12,12 @@ services: submission: verification: base-url: ${VERIFICATION_BASE_URL} + +server: + ssl: + enabled: true + key-password: ${SSL_PASSWORD} + key-store: ${SSL_KEYSTORE_PATH} + key-store-password: ${SSL_PASSWORD} + key-store-provider: SUN + key-store-type: JKS \ No newline at end of file