diff --git a/.github/workflows/ci-main.yml b/.github/workflows/ci-main.yml index 4dd73cce..f5d7a949 100644 --- a/.github/workflows/ci-main.yml +++ b/.github/workflows/ci-main.yml @@ -1,7 +1,6 @@ name: ci-main on: workflow_dispatch: - push: branches: - main diff --git a/.gitignore b/.gitignore index dd5934d9..f5d73387 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,4 @@ build/ !/tools/*.sh certs/* +.DS_Store diff --git a/THIRD-PARTY.md b/THIRD-PARTY.md index e0bc36dc..9c865dcb 100644 --- a/THIRD-PARTY.md +++ b/THIRD-PARTY.md @@ -18,7 +18,7 @@ ThirdParty Licenses | com.fasterxml:classmate:1.5.1 | Apache License, Version 2.0 | | com.fasterxml.jackson.core:jackson-annotations:2.13.2 | The Apache Software License, Version 2.0 | | com.fasterxml.jackson.core:jackson-core:2.13.2 | The Apache Software License, Version 2.0 | -| com.fasterxml.jackson.core:jackson-databind:2.13.2.2 | The Apache Software License, Version 2.0 | +| com.fasterxml.jackson.core:jackson-databind:2.13.2.1 | The Apache Software License, Version 2.0 | | com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.13.2 | The Apache Software License, Version 2.0 | | com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.13.2 | The Apache Software License, Version 2.0 | | com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.2 | The Apache Software License, Version 2.0 | @@ -47,7 +47,7 @@ ThirdParty Licenses | io.github.openfeign:feign-slf4j:11.8 | 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.8.4 | The Apache Software License, Version 2.0 | +| io.micrometer:micrometer-core:1.8.5 | The Apache Software License, Version 2.0 | | io.swagger.core.v3:swagger-annotations:2.1.12 | Apache License 2.0 | | io.swagger.core.v3:swagger-core:2.1.12 | Apache License 2.0 | | io.swagger.core.v3:swagger-models:2.1.12 | Apache License 2.0 | @@ -74,9 +74,9 @@ ThirdParty Licenses | org.apache.httpcomponents:httpcore:4.4.15 | Apache License, Version 2.0 | | org.apache.logging.log4j:log4j-api:2.17.2 | Apache License, Version 2.0 | | org.apache.logging.log4j:log4j-to-slf4j:2.17.2 | Apache License, Version 2.0 | -| org.apache.tomcat.embed:tomcat-embed-core:9.0.60 | Apache License, Version 2.0 | -| org.apache.tomcat.embed:tomcat-embed-el:9.0.60 | Apache License, Version 2.0 | -| org.apache.tomcat.embed:tomcat-embed-websocket:9.0.60 | Apache License, Version 2.0 | +| org.apache.tomcat.embed:tomcat-embed-core:9.0.62 | Apache License, Version 2.0 | +| org.apache.tomcat.embed:tomcat-embed-el:9.0.62 | Apache License, Version 2.0 | +| org.apache.tomcat.embed:tomcat-embed-websocket:9.0.62 | Apache License, Version 2.0 | | org.apiguardian:apiguardian-api:1.1.2 | The Apache License, Version 2.0 | | org.aspectj:aspectjweaver:1.9.7 | Eclipse Public License - v 2.0 | | org.assertj:assertj-core:3.21.0 | Apache License, Version 2.0 | @@ -115,45 +115,45 @@ ThirdParty Licenses | org.springdoc:springdoc-openapi-common:1.6.6 | The Apache License, Version 2.0 | | org.springdoc:springdoc-openapi-ui:1.6.6 | The Apache License, Version 2.0 | | org.springdoc:springdoc-openapi-webmvc-core:1.6.6 | The Apache License, Version 2.0 | -| org.springframework:spring-aop:5.3.18 | Apache License, Version 2.0 | -| org.springframework:spring-aspects:5.3.18 | Apache License, Version 2.0 | -| org.springframework:spring-beans:5.3.18 | Apache License, Version 2.0 | -| org.springframework:spring-context:5.3.18 | Apache License, Version 2.0 | -| org.springframework:spring-core:5.3.18 | Apache License, Version 2.0 | -| org.springframework:spring-expression:5.3.18 | Apache License, Version 2.0 | -| org.springframework:spring-jcl:5.3.18 | Apache License, Version 2.0 | -| org.springframework:spring-jdbc:5.3.18 | Apache License, Version 2.0 | -| org.springframework:spring-orm:5.3.18 | Apache License, Version 2.0 | -| org.springframework:spring-test:5.3.18 | Apache License, Version 2.0 | -| org.springframework:spring-tx:5.3.18 | Apache License, Version 2.0 | -| org.springframework:spring-web:5.3.18 | Apache License, Version 2.0 | -| org.springframework:spring-webmvc:5.3.18 | Apache License, Version 2.0 | -| org.springframework.boot:spring-boot:2.6.6 | Apache License, Version 2.0 | -| org.springframework.boot:spring-boot-actuator:2.6.6 | Apache License, Version 2.0 | -| org.springframework.boot:spring-boot-actuator-autoconfigure:2.6.6 | Apache License, Version 2.0 | -| org.springframework.boot:spring-boot-autoconfigure:2.6.6 | Apache License, Version 2.0 | -| org.springframework.boot:spring-boot-starter:2.6.6 | Apache License, Version 2.0 | -| org.springframework.boot:spring-boot-starter-actuator:2.6.6 | Apache License, Version 2.0 | -| org.springframework.boot:spring-boot-starter-aop:2.6.6 | Apache License, Version 2.0 | -| org.springframework.boot:spring-boot-starter-data-jpa:2.6.6 | Apache License, Version 2.0 | -| org.springframework.boot:spring-boot-starter-jdbc:2.6.6 | Apache License, Version 2.0 | -| org.springframework.boot:spring-boot-starter-json:2.6.6 | Apache License, Version 2.0 | -| org.springframework.boot:spring-boot-starter-logging:2.6.6 | Apache License, Version 2.0 | -| org.springframework.boot:spring-boot-starter-test:2.6.6 | Apache License, Version 2.0 | -| org.springframework.boot:spring-boot-starter-tomcat:2.6.6 | Apache License, Version 2.0 | -| org.springframework.boot:spring-boot-starter-validation:2.6.6 | Apache License, Version 2.0 | -| org.springframework.boot:spring-boot-starter-web:2.6.6 | Apache License, Version 2.0 | -| org.springframework.boot:spring-boot-test:2.6.6 | Apache License, Version 2.0 | -| org.springframework.boot:spring-boot-test-autoconfigure:2.6.6 | Apache License, Version 2.0 | +| org.springframework:spring-aop:5.3.19 | Apache License, Version 2.0 | +| org.springframework:spring-aspects:5.3.19 | Apache License, Version 2.0 | +| org.springframework:spring-beans:5.3.19 | Apache License, Version 2.0 | +| org.springframework:spring-context:5.3.19 | Apache License, Version 2.0 | +| org.springframework:spring-core:5.3.19 | Apache License, Version 2.0 | +| org.springframework:spring-expression:5.3.19 | Apache License, Version 2.0 | +| org.springframework:spring-jcl:5.3.19 | Apache License, Version 2.0 | +| org.springframework:spring-jdbc:5.3.19 | Apache License, Version 2.0 | +| org.springframework:spring-orm:5.3.19 | Apache License, Version 2.0 | +| org.springframework:spring-test:5.3.19 | Apache License, Version 2.0 | +| org.springframework:spring-tx:5.3.19 | Apache License, Version 2.0 | +| org.springframework:spring-web:5.3.19 | Apache License, Version 2.0 | +| org.springframework:spring-webmvc:5.3.19 | Apache License, Version 2.0 | +| org.springframework.boot:spring-boot:2.6.7 | Apache License, Version 2.0 | +| org.springframework.boot:spring-boot-actuator:2.6.7 | Apache License, Version 2.0 | +| org.springframework.boot:spring-boot-actuator-autoconfigure:2.6.7 | Apache License, Version 2.0 | +| org.springframework.boot:spring-boot-autoconfigure:2.6.7 | Apache License, Version 2.0 | +| org.springframework.boot:spring-boot-starter:2.6.7 | Apache License, Version 2.0 | +| org.springframework.boot:spring-boot-starter-actuator:2.6.7 | Apache License, Version 2.0 | +| org.springframework.boot:spring-boot-starter-aop:2.6.7 | Apache License, Version 2.0 | +| org.springframework.boot:spring-boot-starter-data-jpa:2.6.7 | Apache License, Version 2.0 | +| org.springframework.boot:spring-boot-starter-jdbc:2.6.7 | Apache License, Version 2.0 | +| org.springframework.boot:spring-boot-starter-json:2.6.7 | Apache License, Version 2.0 | +| org.springframework.boot:spring-boot-starter-logging:2.6.7 | Apache License, Version 2.0 | +| org.springframework.boot:spring-boot-starter-test:2.6.7 | Apache License, Version 2.0 | +| org.springframework.boot:spring-boot-starter-tomcat:2.6.7 | Apache License, Version 2.0 | +| org.springframework.boot:spring-boot-starter-validation:2.6.7 | Apache License, Version 2.0 | +| org.springframework.boot:spring-boot-starter-web:2.6.7 | Apache License, Version 2.0 | +| org.springframework.boot:spring-boot-test:2.6.7 | Apache License, Version 2.0 | +| org.springframework.boot:spring-boot-test-autoconfigure:2.6.7 | Apache License, Version 2.0 | | org.springframework.cloud:spring-cloud-commons:3.1.1 | Apache License, Version 2.0 | | org.springframework.cloud:spring-cloud-context:3.1.1 | Apache License, Version 2.0 | | org.springframework.cloud:spring-cloud-openfeign-core:3.1.1 | Apache License, Version 2.0 | | org.springframework.cloud:spring-cloud-starter:3.1.1 | Apache License, Version 2.0 | | org.springframework.cloud:spring-cloud-starter-openfeign:3.1.1 | Apache License, Version 2.0 | -| org.springframework.data:spring-data-commons:2.6.3 | Apache License, Version 2.0 | -| org.springframework.data:spring-data-jpa:2.6.3 | Apache License, Version 2.0 | -| org.springframework.security:spring-security-core:5.6.2 | Apache License, Version 2.0 | -| org.springframework.security:spring-security-crypto:5.6.2 | Apache License, Version 2.0 | +| org.springframework.data:spring-data-commons:2.6.4 | Apache License, Version 2.0 | +| org.springframework.data:spring-data-jpa:2.6.4 | Apache License, Version 2.0 | +| org.springframework.security:spring-security-core:5.6.3 | Apache License, Version 2.0 | +| org.springframework.security:spring-security-crypto:5.6.3 | 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.6.2 | Apache License, Version 2.0 | | org.webjars:swagger-ui:4.5.0 | Apache 2.0 | diff --git a/pom.xml b/pom.xml index 1ffd88ce..8b1ff19e 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ org.springframework.boot spring-boot-starter-parent - 2.6.6 + 2.6.7 @@ -210,6 +210,11 @@ spring-boot-starter-test test + + org.junit.jupiter + junit-jupiter-params + test + org.liquibase liquibase-core diff --git a/src/main/java/eu/europa/ec/dgc/gateway/client/AssetManagerClient.java b/src/main/java/eu/europa/ec/dgc/gateway/client/AssetManagerClient.java index e5249f8a..70be55bf 100644 --- a/src/main/java/eu/europa/ec/dgc/gateway/client/AssetManagerClient.java +++ b/src/main/java/eu/europa/ec/dgc/gateway/client/AssetManagerClient.java @@ -28,6 +28,7 @@ import org.springframework.http.HttpHeaders; 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.PostMapping; import org.springframework.web.bind.annotation.PutMapping; @@ -52,14 +53,22 @@ ResponseEntity uploadFile(@RequestHeader(HttpHeaders.AUTHORIZATION) String @RequestBody byte[] file); @PostMapping( - value = "/ocs/v2.php/apps/files/api/v2/synchronize", - consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, - produces = MediaType.APPLICATION_JSON_VALUE + value = "/ocs/v2.php/apps/files/api/v2/synchronize", + consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, + produces = MediaType.APPLICATION_JSON_VALUE ) ResponseEntity synchronize( - @RequestHeader(HttpHeaders.AUTHORIZATION) String authHeader, - @RequestHeader("OCS-APIRequest") String ocsApiRequest, - @RequestBody SynchronizeFormData formData); + @RequestHeader(HttpHeaders.AUTHORIZATION) String authHeader, + @RequestHeader("OCS-APIRequest") String ocsApiRequest, + @RequestBody SynchronizeFormData formData); + + @GetMapping( + value = "/remote.php/dav/files/{uid}/{path}/{filename}", + produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) + ResponseEntity downloadFile(@RequestHeader(HttpHeaders.AUTHORIZATION) String authHeader, + @PathVariable("uid") String uid, + @PathVariable("path") String path, + @PathVariable("filename") String filename); @Getter @AllArgsConstructor 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 index 781f2fe0..6b2c2e13 100644 --- a/src/main/java/eu/europa/ec/dgc/gateway/config/DgcConfigProperties.java +++ b/src/main/java/eu/europa/ec/dgc/gateway/config/DgcConfigProperties.java @@ -49,6 +49,8 @@ public static class Publication { private KeyStoreWithAlias keystore = new KeyStoreWithAlias(); private Boolean enabled; private Boolean synchronizeEnabled; + private Boolean downloadEnabled; + private String downloadPath; private String url; private String amngrUid; private String path; diff --git a/src/main/java/eu/europa/ec/dgc/gateway/service/PublishingService.java b/src/main/java/eu/europa/ec/dgc/gateway/service/PublishingService.java index fe768bdc..fa3e226e 100644 --- a/src/main/java/eu/europa/ec/dgc/gateway/service/PublishingService.java +++ b/src/main/java/eu/europa/ec/dgc/gateway/service/PublishingService.java @@ -33,8 +33,11 @@ import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; +import java.io.FileOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; import java.security.KeyStore; import java.security.PrivateKey; import java.security.cert.X509Certificate; @@ -92,6 +95,11 @@ public void publishGatewayData() { byte[] signature = calculateSignature(zip); uploadGatewayData(zip, signature); + if (Boolean.TRUE.equals(properties.getPublication().getDownloadEnabled())) { + downloadFile(properties.getPublication().getArchiveFilename()); + downloadFile(properties.getPublication().getSignatureFilename()); + } + log.info("Finished publishing of packed Gateway data"); } @@ -277,10 +285,52 @@ private void uploadGatewayData(byte[] zip, byte[] signature) { log.info("Upload and Synchronize successful"); } + private void downloadFile(String filename) { + log.info("Downloading uploaded DGCG Publication File: {}", filename); + + ResponseEntity downloadResponse; + try { + downloadResponse = assetManagerClient.downloadFile(getAuthHeader(), + properties.getPublication().getAmngrUid(), properties.getPublication().getPath(), filename); + + if (downloadResponse.getStatusCode().is2xxSuccessful()) { + log.info("Download of file {} was successful.", filename); + } else { + log.error("Failed to download file: {}", downloadResponse.getStatusCode()); + } + } catch (FeignException.FeignServerException e) { + log.error("Failed to Download file: {}", e.status()); + return; + } + + File targetFile = Paths.get(properties.getPublication().getDownloadPath(), filename).toFile(); + + try { + Files.deleteIfExists(targetFile.toPath()); + } catch (IOException e) { + log.error("Failed to delete existing file: {}, {}", targetFile.getAbsolutePath(), e.getMessage()); + return; + } + + if (downloadResponse.hasBody() && downloadResponse.getBody() != null) { + try (FileOutputStream fileOutputStream = new FileOutputStream(targetFile)) { + fileOutputStream.write(downloadResponse.getBody()); + log.info("Saved file {} to {} ({} Bytes)", + filename, targetFile.getAbsolutePath(), downloadResponse.getBody().length); + } catch (IOException e) { + log.error("Failed to write downloaded file to disk: {}, {}", + targetFile.getAbsolutePath(), e.getMessage()); + } + } else { + log.error("Download Response does not contain any body"); + } + + } + private String getAuthHeader() { String header = "Basic "; header += Base64.getEncoder().encodeToString((properties.getPublication().getUser() + ":" - + properties.getPublication().getPassword()).getBytes(StandardCharsets.UTF_8)); + + properties.getPublication().getPassword()).getBytes(StandardCharsets.UTF_8)); return header; } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 76ffa734..91e4bcbe 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -42,6 +42,7 @@ dgc: publication: enbaled: false synchronizeEnabled: false + downloadEnabled: false keystore: keyStorePath: /ec/prod/app/san/dgcg/dgc-publication.jks keyStorePass: dgc-p4ssw0rd diff --git a/src/main/resources/publication/License.txt b/src/main/resources/publication/License.txt index bfff2005..85d68d40 100644 --- a/src/main/resources/publication/License.txt +++ b/src/main/resources/publication/License.txt @@ -1,44 +1,45 @@ -DATA LICENSE for the database of public keys of digital signer certificates, originally -available from https://ec.europa.eu/assets/eu-dcc/dcc_database.zip - -1. This data license shall govern the use of the database of public keys of digital signer certificates, originally -available from https://ec.europa.eu/assets/eu-dcc/dcc_database.zip. The use of the copyright and database right -material provided in this database (the "Data") indicates that you accept the terms and conditions of this license. -You may not amend this license under any circumstances. - -2. Licensor of this database is the European Commission. The Licensor grants you a worldwide, royalty-free, perpetual, -non-exclusive licence to use the Data subject to the conditions below. - -3. This license grants the exercise of the following rights regarding the Data, subject to the observation of the -conditions in sections 4: - - a) to copy, publish, distribute and transmit the Data; - b) to incorporate the data in commercially and non-commercially available products and/or services. - -These rights are restricted to the Data itself and shall not be construed to extend beyond implicitly. - -4. Any exercise of the rights granted in section 3 shall be conditional upon the observance and fulfilment of the -following obligations: - - a) to acknowledge the use of the database and attribute the copyright and database rights of the European - Commission in an appropriate manner and with an appropriate notice; - b) to acknowledge and to notify in an appropriate manner and with an appropriate notice that the use of the - database does not constitute endorsement of any products and/or services by the European commission; - c) to provide the text of this license in an appropriate manner and with an appropriate notice to any intended - recipient of the Data. - -You may use the following notices at your discretion: - - "Contains data provided by the European Commission under the following license: - https://ec.europa.eu/assets/eu-dcc/dcc_database.zip/license.txt. - The European Commission does not endorse any products and/or services by providing this data." - -5. Foremost, this license is governed by the law of the European Union and additionally by the law of the Member State -of the European Union you or an intended recipient resides in. If you or any intended recipient do not reside in a -Member State of the European Union, the laws of the Kingdom of Belgium shall apply in addition to the law of the -European Union. - -6. The European Commission shall not be liable in any capacity for any use of the Data by third parties. It shall only -be liable for any deliberate and intentional damage caused by a deliberate and intentional default of the Data under any -statutory law applicable according to section 5. Any further liability, representation, warranty or obligation of -the European Commission is explicitly excluded. +DATA LICENSE for the database of public keys of digital signer certificates, originally +available from https://ec.europa.eu/health/ehealth/covid-19_en + +1. This DATA LICENSE shall govern the use of the database of public keys of digital signer certificates. +The use of the copyright and database right material provided in this database (the "Data") indicates that +you accept the terms and conditions of this license. You may not amend this license under any circumstances. + +2. Licensor of this database is the European Commission. The Licensor grants you a worldwide, royalty-free, perpetual, +non-exclusive license to use the Data subject to the conditions below. + +3. This license grants the exercise of the following rights regarding the Data, subject to the observation of the +conditions in section 4: + + a) to copy, publish, distribute and transmit the Data; + b) to incorporate the data in commercially and non-commercially available products and/or services. + +These rights are restricted to the Data itself and shall not be construed to extend beyond implicitly. + +4. Any exercise of the rights granted in section 3 shall be conditional upon the observance and fulfillment of the +following obligations: + + a) to acknowledge the use of the database and attribute the copyright and database rights of the European + Commission in an appropriate manner and with appropriate notice; + b) to acknowledge and notify in an appropriate manner and with an appropriate notice that the use of the + the database does not constitute endorsement of any products and/or services by the European Commission; + c) to provide the text of this license in an appropriate manner and with appropriate notice to any intended + recipient of the Data. + +You may use the following notices at your discretion: + + "Contains data provided by the European Commission under the DATA LICENSE. The European Commission + does not endorse any products and/or services by providing this data." + +5. Foremost, this license is governed by the law of the European Union and additionally by the law of the Member State +of the European Union you or an intended recipient resides in. If you or any intended recipient do not reside in a +Member State of the European Union, the laws of the Kingdom of Belgium shall apply in addition to the law of the +European Union. + +6. The European Commission shall not be liable in any capacity for any use of the Data by third parties. It shall only +be liable for any deliberate and intentional damage caused by a deliberate and intentional default of the Data under any +statutory law applicable according to section 5. Any further liability, representation, warranty, or obligation of +the European Commission is explicitly excluded. + +7. This license does not constitute a legal basis pursuant to Article 6 of Regulation (EU) 2016/679 (GDPR) to process +the personal data contained in an EU Digital COVID Certificate. diff --git a/src/main/resources/publication/Readme.txt b/src/main/resources/publication/Readme.txt index ed1627ab..01ac3e1b 100644 --- a/src/main/resources/publication/Readme.txt +++ b/src/main/resources/publication/Readme.txt @@ -1,63 +1,63 @@ -EU Digital Covid Certificates Signer Certificates Archive - -The archive is published under the license described in License.txt - Please be aware of this license when distributing -this archive or contents of it. - - -Content: - - 1. Intention - 2. Structure of archive - 3. How to verify integrity of DCC - 4. How to verify integrity of this archive - -1. Intention - The content of this archive can be used to verify that a Digital Covid Certificate (DCC) was issued by an authorized - issuer. - -2. Structure of archive - This archive contains two different certificate types: Digital Signer Certificate (DSC) and Country Signing - Certificate Authority (CSCA). The archive is structured by certificate type (DSC or CSCA), domain (currently just - DCC) and the 2-digit country code. The certificates are encoded as PKCS#8 saved in pem files named by there - certificate SHA-256 thumbprint. - - CSCA - ∟ DCC - ∟ CC - ∟ 6d3644ee122d1263267c6f42974c42acc3ca1a08675264fe34360239b5605e0e.pem - DSC - ∟ DCC - ∟ CC - ∟ 6493815d2ecfdbab6507e541a5f53e68b03d057b45e16d39b35b91ee61f78ab0.pem - -3. How to verify integrity of DCC - A. Extract Signature from DCC - B. Get KID from DCC, Convert Base64 string to hex, search for DSC file starting with the resulting hex string - C. Verify that DCC was signed by the DSC - D. Verify that the matching DSC was issued by one of the CSCA - -4. How to verify integrity of this archive - This archive and all of its contents are signed by a certificate of the European Commission. - The signature file will be seperatly distributed. You can find it on the same download page as this archive - (https://ec.europa.eu/assets/eu-dcc/dcc_database.zip.sig.txt). The signature file contains a base64 encoded - CMS-Message with detached payload (PKCS#7). - - There are two options to verify the integrity of the archive: - - A: DGC-CLI (recommended, needs DGC-CLI to be installed) - - Install DGC-CLI: https://github.com/eu-digital-green-certificates/dgc-cli#installation - - Verify integrity - dgc signing validate-file -i dcc_database.zip.sig.txt -p dcc_database.zip - - The command will output only the verification result and the subject and thumbprint of the signer certificate. - The thumbprint should be checked against the published signer certificate. - - B: OpenSSL (Needs OpenSSL CLI to be installed) - - Convert signature file from base64 encoded to plain DER file - openssl base64 -a -A -d -in dcc_database.zip.sig.txt -out dcc_database.zip.sig.der - - Verify integrity - openssl cms -verify -in dcc_database.zip.sig.der -inform DER -content dcc_database.zip -binary -CAfile eu_signer.pem - - The output of the verify command contains the whole binary data of the zip file. - At the end of the output you should find: "Verification successful" - +EU Digital Covid Certificates Signer Certificates Archive + +The archive is published under the license described in License.txt - Please be aware of this license when distributing +this archive or contents of it. + + +Content: + + 1. Intention + 2. Structure of archive + 3. How to verify integrity of DCC + 4. How to verify integrity of this archive + +1. Intention + The content of this archive can be used to verify that a Digital Covid Certificate (DCC) was issued by an authorized + issuer. Note that in order to lawfully process the personal data contained in a DCC, verifiers need a legal basis pursuant + to Article 6 of Regulation (EU) 2016/679 (GDPR). + +2. Structure of archive + This archive contains two different certificate types: Digital Signer Certificate (DSC) and Country Signing Certificate + Authority (CSCA). The archive is structured by certificate type (DSC or CSCA), domain (currently just DCC) and the + 2-digit country code. + The certificates are encoded as PKCS#8 saved in pem files named by there certificate SHA-256 thumbprint. + + CSCA + ∟ DCC + ∟ CC + ∟ 6d3644ee122d1263267c6f42974c42acc3ca1a08675264fe34360239b5605e0e.pem + DSC + ∟ DCC + ∟ CC + ∟ 6493815d2ecfdbab6507e541a5f53e68b03d057b45e16d39b35b91ee61f78ab0.pem + +3. How to verify integrity of DCC + A. Extract Signature from DCC + B. Get KID from DCC, Convert Base64 string to hex, search for DSC file starting with the resulting hex string + C. Verify that DCC was signed by the DSC + D. Verify that the matching DSC was issued by one of the CSCA + +4. How to verify integrity of this archive + This archive and all of its contents are signed by a certificate of the European Commission. + The signature file will be seperatly distributed. You can find it on the same download page as this archive ([URL]). + The signature file contains a base64 encoded CMS-Message with detached payload (PKCS#7). + + There are two options to verify the integrity of the archive: + + A: DGC-CLI (recommended, needs DGC-CLI to be installed) + - Install DGC-CLI: https://github.com/eu-digital-green-certificates/dgc-cli#installation + - Verify integrity + dgc signing validate-file -i dcc_database.zip.sig.txt -p dcc_database.zip + + The command will output only the verification result and the subject and thumbprint of the signer certificate. + The thumbprint should be checked against the published signer certificate. + + B: OpenSSL (Needs OpenSSL CLI to be installed) + - Convert signature file from base64 encoded to plain DER file + openssl base64 -a -A -d -in dcc_database.zip.sig.txt -out dcc_database.zip.sig.der + - Verify integrity + openssl cms -verify -in dcc_database.zip.sig.der -inform DER -content dcc_database.zip -binary -CAfile eu_signer.pem + + The output of the verify command contains the whole binary data of the zip file. + At the end of the output you should find: "Verification successful" + diff --git a/src/test/java/eu/europa/ec/dgc/gateway/publishing/ArchivePublishingTest.java b/src/test/java/eu/europa/ec/dgc/gateway/publishing/ArchivePublishingTest.java index 0c732149..f0981d41 100644 --- a/src/test/java/eu/europa/ec/dgc/gateway/publishing/ArchivePublishingTest.java +++ b/src/test/java/eu/europa/ec/dgc/gateway/publishing/ArchivePublishingTest.java @@ -20,11 +20,6 @@ package eu.europa.ec.dgc.gateway.publishing; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - import eu.europa.ec.dgc.gateway.client.AssetManagerClient; import eu.europa.ec.dgc.gateway.config.DgcConfigProperties; import eu.europa.ec.dgc.gateway.entity.TrustedPartyEntity; @@ -40,9 +35,11 @@ import eu.europa.ec.dgc.signing.SignedMessageParser; import eu.europa.ec.dgc.utils.CertificateUtils; import java.io.ByteArrayInputStream; +import java.io.File; import java.io.IOException; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; +import java.nio.file.Paths; import java.security.KeyPairGenerator; import java.security.cert.CertificateEncodingException; import java.security.cert.X509Certificate; @@ -61,8 +58,13 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import org.mockito.ArgumentCaptor; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import org.mockito.Mockito; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; @@ -70,16 +72,17 @@ import org.springframework.util.ResourceUtils; @SpringBootTest(properties = { - "dgc.publication.enabled=true", - "dgc.publication.synchronizeEnabled=true", - "dgc.publication.user=user", - "dgc.publication.password=pass", - "dgc.publication.amngruid=uid", - "dgc.publication.path=path/a/b", - "dgc.publication.archiveFilename=db.zip", - "dgc.publication.signatureFilename=db.zip.sig.txt", - "dgc.publication.notifyEmails[0]=u1@c1.de", - "dgc.publication.notifyEmails[1]=u1@c2.de" + "dgc.publication.enabled=true", + "dgc.publication.synchronizeEnabled=true", + "dgc.publication.downloadEnabled=true", + "dgc.publication.user=user", + "dgc.publication.password=pass", + "dgc.publication.amngruid=uid", + "dgc.publication.path=path/a/b", + "dgc.publication.archiveFilename=db.zip", + "dgc.publication.signatureFilename=db.zip.sig.txt", + "dgc.publication.notifyEmails[0]=u1@c1.de", + "dgc.publication.notifyEmails[1]=u1@c2.de" }) @Slf4j public class ArchivePublishingTest { @@ -114,8 +117,11 @@ public class ArchivePublishingTest { @Autowired DgcConfigProperties properties; + @TempDir + File tempDir; + private static final String expectedAuthHeader = - "Basic " + Base64.getEncoder().encodeToString("user:pass".getBytes(StandardCharsets.UTF_8)); + "Basic " + Base64.getEncoder().encodeToString("user:pass".getBytes(StandardCharsets.UTF_8)); private static final String expectedUid = "uid"; private static final String expectedPath = "path/a/b"; private static final String expectedArchiveName = "db.zip"; @@ -126,6 +132,8 @@ public class ArchivePublishingTest { @BeforeEach public void setup() throws Exception { + properties.getPublication().setDownloadPath(tempDir.getAbsolutePath()); + trustedPartyRepository.deleteAll(); signerInformationRepository.deleteAll(); @@ -157,21 +165,31 @@ public void testArchiveContainsRequiredFiles() throws Exception { ArgumentCaptor uploadArchiveArgumentCaptor = ArgumentCaptor.forClass(byte[].class); ArgumentCaptor uploadSignatureArgumentCaptor = ArgumentCaptor.forClass(byte[].class); ArgumentCaptor synchronizeFormDataArgumentCaptor = ArgumentCaptor.forClass(AssetManagerClient.SynchronizeFormData.class); + byte[] dummyByteArrayArchive = new byte[]{0xd, 0xe, 0xa, 0xd, 0xb, 0xe, 0xe, 0xf}; + byte[] dummyByteArraySignature = new byte[]{0xa, 0xa, 0xa, 0xa, 0xa, 0xa, 0xa, 0xa}; when(assetManagerClientMock.uploadFile(eq(expectedAuthHeader), eq(expectedUid), eq(expectedPath), eq(expectedArchiveName), uploadArchiveArgumentCaptor.capture())) - .thenReturn(ResponseEntity.ok(null)); + .thenReturn(ResponseEntity.ok(null)); when(assetManagerClientMock.uploadFile(eq(expectedAuthHeader), eq(expectedUid), eq(expectedPath), eq(expectedSignatureName), uploadSignatureArgumentCaptor.capture())) - .thenReturn(ResponseEntity.ok(null)); + .thenReturn(ResponseEntity.ok(null)); when(assetManagerClientMock.synchronize(eq(expectedAuthHeader), eq("true"), synchronizeFormDataArgumentCaptor.capture())) - .thenReturn(ResponseEntity.ok(new AssetManagerSynchronizeResponseDto("OK", 200, "Message", expectedPath, "token"))); + .thenReturn(ResponseEntity.ok(new AssetManagerSynchronizeResponseDto("OK", 200, "Message", expectedPath, "token"))); + + when(assetManagerClientMock.downloadFile(expectedAuthHeader, expectedUid, expectedPath, expectedArchiveName)) + .thenReturn(ResponseEntity.ok(dummyByteArrayArchive)); + + when(assetManagerClientMock.downloadFile(expectedAuthHeader, expectedUid, expectedPath, expectedSignatureName)) + .thenReturn(ResponseEntity.ok(dummyByteArraySignature)); publishingService.publishGatewayData(); verify(assetManagerClientMock).uploadFile(eq(expectedAuthHeader), eq(expectedUid), eq(expectedPath), eq(expectedArchiveName), any()); verify(assetManagerClientMock).uploadFile(eq(expectedAuthHeader), eq(expectedUid), eq(expectedPath), eq(expectedSignatureName), any()); verify(assetManagerClientMock).synchronize(eq(expectedAuthHeader), eq("true"), any()); + verify(assetManagerClientMock).downloadFile(expectedAuthHeader, expectedUid, expectedPath, expectedArchiveName); + verify(assetManagerClientMock).downloadFile(expectedAuthHeader, expectedUid, expectedPath, expectedSignatureName); Assertions.assertNotNull(uploadArchiveArgumentCaptor.getValue()); Assertions.assertNotNull(uploadSignatureArgumentCaptor.getValue()); @@ -239,16 +257,33 @@ public void testArchiveContainsRequiredFiles() throws Exception { Assertions.assertEquals(SignedMessageParser.ParserState.SUCCESS, parser.getParserState()); Assertions.assertArrayEquals(dgcTestKeyStore.getPublicationSigner().getEncoded(), parser.getSigningCertificate().getEncoded()); Assertions.assertTrue(parser.isSignatureVerified()); + + /* + * Check Downloaded files + */ + byte[] downloadedArchiveFile = FileUtils.readFileToByteArray( + Paths.get(tempDir.getAbsolutePath(), properties.getPublication().getArchiveFilename()).toFile()); + Assertions.assertArrayEquals(dummyByteArrayArchive, downloadedArchiveFile); + + byte[] downloadedSignatureFile = FileUtils.readFileToByteArray( + Paths.get(tempDir.getAbsolutePath(), properties.getPublication().getSignatureFilename()).toFile()); + Assertions.assertArrayEquals(dummyByteArraySignature, downloadedSignatureFile); } @Test public void testSynchronizeDisabled() { when(assetManagerClientMock.uploadFile(eq(expectedAuthHeader), eq(expectedUid), eq(expectedPath), eq(expectedArchiveName), any())) - .thenReturn(ResponseEntity.ok(null)); + .thenReturn(ResponseEntity.ok(null)); when(assetManagerClientMock.uploadFile(eq(expectedAuthHeader), eq(expectedUid), eq(expectedPath), eq(expectedSignatureName), any())) - .thenReturn(ResponseEntity.ok(null)); + .thenReturn(ResponseEntity.ok(null)); + + when(assetManagerClientMock.downloadFile(expectedAuthHeader, expectedUid, expectedPath, expectedArchiveName)) + .thenReturn(ResponseEntity.ok(new byte[]{})); + + when(assetManagerClientMock.downloadFile(expectedAuthHeader, expectedUid, expectedPath, expectedSignatureName)) + .thenReturn(ResponseEntity.ok(new byte[]{})); properties.getPublication().setSynchronizeEnabled(false);