From 2c1d8bb776280d55b4fca924690004305ff3abf1 Mon Sep 17 00:00:00 2001 From: Artur T <81235055+a-trzewik@users.noreply.github.com> Date: Wed, 7 Jul 2021 11:04:17 +0200 Subject: [PATCH 1/7] feat/list-signature jks signing optional --- certs/signing.jks | Bin 0 -> 700 bytes .../DgcBusinessRuleServiceApplication.java | 3 +- .../businessrule/config/JksSigningConfig.java | 15 ++ .../entity/CountryListEntity.java | 5 + .../ec/dgc/businessrule/entity/ListType.java | 5 + .../businessrule/entity/SignedListEntity.java | 36 ++++ .../repository/SignedListRepository.java | 8 + .../controller/BusinessRuleController.java | 22 +- .../controller/CountryListController.java | 13 +- .../controller/ValueSetController.java | 202 ------------------ .../service/BusinessRuleService.java | 19 +- .../service/CountryListService.java | 30 ++- .../GatewayDataDownloadBtpServiceImpl.java | 2 +- .../GatewayDataDownloadServiceImpl.java | 2 +- .../service/JksSigningService.java | 86 ++++++++ .../service/ListSigningService.java | 59 +++++ .../businessrule/service/SigningService.java | 11 + .../businessrule/service/ValueSetService.java | 12 +- .../resources/application-jks-signing.yml | 5 + src/main/resources/db/changelog.xml | 1 + .../resources/db/changelog/add_list_table.xml | 32 +++ .../CountryListControllerIntegrationTest.java | 2 +- .../service/BusinessRuleServiceTest.java | 23 +- src/test/resources/application.yml | 6 + 24 files changed, 373 insertions(+), 226 deletions(-) create mode 100644 certs/signing.jks create mode 100644 src/main/java/eu/europa/ec/dgc/businessrule/config/JksSigningConfig.java create mode 100644 src/main/java/eu/europa/ec/dgc/businessrule/entity/ListType.java create mode 100644 src/main/java/eu/europa/ec/dgc/businessrule/entity/SignedListEntity.java create mode 100644 src/main/java/eu/europa/ec/dgc/businessrule/repository/SignedListRepository.java delete mode 100644 src/main/java/eu/europa/ec/dgc/businessrule/restapi/controller/ValueSetController.java create mode 100644 src/main/java/eu/europa/ec/dgc/businessrule/service/JksSigningService.java create mode 100644 src/main/java/eu/europa/ec/dgc/businessrule/service/ListSigningService.java create mode 100644 src/main/java/eu/europa/ec/dgc/businessrule/service/SigningService.java create mode 100644 src/main/resources/application-jks-signing.yml create mode 100644 src/main/resources/db/changelog/add_list_table.xml diff --git a/certs/signing.jks b/certs/signing.jks new file mode 100644 index 0000000000000000000000000000000000000000..bbd29aac3d59e8097b597282288de0c9d66a3bea GIT binary patch literal 700 zcmezO_TO6u1_mY|W&~rFl=S38Ag8K^G3+By5K}JSa2A0Ns zPY$PSywWsv&$JTx0%a*hPo*b%!cXFk>c!r5dTjTp$;ClbT0LbZ`|Jdlr9pEyOzco< z2sj$s<{#nlk$L%k*_iWFo`o6KWL%#)XC+gSUaj=O!sWeOmM1+QR;E3Y+P=6-{d|mX z(eZADNmE=mylXNI@;#8_W@IE)=J%#oUF7pZyTY$GjMxn7ipySCS?Cp~En#-+JK?c! zanR|#848!vyRJPq>|C@@W`QNI9H;n&&Q~(_I`{MJk9^=wzpbmlz^)DUDQkqDsevWX ztBVbq80Q1A%K~O5MkXefgeOrV23%|$T5TTZY+0C@48je$4LI4DLs{5_nNm^>d4Q5Y z4hIi&UQxLruK_oR$Iio2l3H8><8q;Lxly^y5UzooIIoeRfw_U1p|O#fiAfZYYlOt5 zoyCy`!ffnd|1mK_J<80;&g{g%($A$(c(UnXP{h4}bp~2fe;VvzlK+x<_DGSz_S$=w z?w^#`yj&e3B=~3PCkDG`-M8BwH`$vyWXkHXz7fx3JM?`ia=s_l%ECOc~0F``Qr)(4?)G^=E{M*5p{A8t=xhbDR lrx;IIc5KxmuiAr|xj}3j6!#>aX1Y;W@ha=>gL3c58329?;2Zz| literal 0 HcmV?d00001 diff --git a/src/main/java/eu/europa/ec/dgc/businessrule/DgcBusinessRuleServiceApplication.java b/src/main/java/eu/europa/ec/dgc/businessrule/DgcBusinessRuleServiceApplication.java index 3f32e62..1028ac0 100644 --- a/src/main/java/eu/europa/ec/dgc/businessrule/DgcBusinessRuleServiceApplication.java +++ b/src/main/java/eu/europa/ec/dgc/businessrule/DgcBusinessRuleServiceApplication.java @@ -21,6 +21,7 @@ package eu.europa.ec.dgc.businessrule; import eu.europa.ec.dgc.businessrule.config.DgcConfigProperties; +import eu.europa.ec.dgc.businessrule.config.JksSigningConfig; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -30,7 +31,7 @@ * The Application class. */ @SpringBootApplication -@EnableConfigurationProperties(DgcConfigProperties.class) +@EnableConfigurationProperties({DgcConfigProperties.class, JksSigningConfig.class}) public class DgcBusinessRuleServiceApplication extends SpringBootServletInitializer { /** diff --git a/src/main/java/eu/europa/ec/dgc/businessrule/config/JksSigningConfig.java b/src/main/java/eu/europa/ec/dgc/businessrule/config/JksSigningConfig.java new file mode 100644 index 0000000..20a3faa --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/businessrule/config/JksSigningConfig.java @@ -0,0 +1,15 @@ +package eu.europa.ec.dgc.businessrule.config; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@Getter +@Setter +@ConfigurationProperties("jks-signing") +public class JksSigningConfig { + private String keyStoreFile; + private String keyStorePassword; + private String certAlias; + private String privateKeyPassword; +} diff --git a/src/main/java/eu/europa/ec/dgc/businessrule/entity/CountryListEntity.java b/src/main/java/eu/europa/ec/dgc/businessrule/entity/CountryListEntity.java index 62e1c3a..b2c8b83 100644 --- a/src/main/java/eu/europa/ec/dgc/businessrule/entity/CountryListEntity.java +++ b/src/main/java/eu/europa/ec/dgc/businessrule/entity/CountryListEntity.java @@ -49,4 +49,9 @@ public class CountryListEntity { @Column(name = "raw_data", nullable = false) String rawData; + @Column(name = "hash", length = 64) + private String hash; + + @Column(name = "signature", length = 256) + private String signature; } \ No newline at end of file diff --git a/src/main/java/eu/europa/ec/dgc/businessrule/entity/ListType.java b/src/main/java/eu/europa/ec/dgc/businessrule/entity/ListType.java new file mode 100644 index 0000000..6552025 --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/businessrule/entity/ListType.java @@ -0,0 +1,5 @@ +package eu.europa.ec.dgc.businessrule.entity; + +public enum ListType { + Rules, ValueSets +} diff --git a/src/main/java/eu/europa/ec/dgc/businessrule/entity/SignedListEntity.java b/src/main/java/eu/europa/ec/dgc/businessrule/entity/SignedListEntity.java new file mode 100644 index 0000000..28922ca --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/businessrule/entity/SignedListEntity.java @@ -0,0 +1,36 @@ +package eu.europa.ec.dgc.businessrule.entity; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.Id; +import javax.persistence.Lob; +import javax.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@Entity +@Table(name = "signed_list") +@AllArgsConstructor +@NoArgsConstructor +public class SignedListEntity { + @Id + @Column(name = "list_type", nullable = false) + @Enumerated(EnumType.STRING) + private ListType listType; + + @Column(name = "hash", nullable = false, length = 64) + private String hash; + + @Column(name = "signature", nullable = false, length = 256) + private String signature; + + @Lob + @Column(name = "raw_data", nullable = false) + String rawData; +} diff --git a/src/main/java/eu/europa/ec/dgc/businessrule/repository/SignedListRepository.java b/src/main/java/eu/europa/ec/dgc/businessrule/repository/SignedListRepository.java new file mode 100644 index 0000000..c26b828 --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/businessrule/repository/SignedListRepository.java @@ -0,0 +1,8 @@ +package eu.europa.ec.dgc.businessrule.repository; + +import eu.europa.ec.dgc.businessrule.entity.ListType; +import eu.europa.ec.dgc.businessrule.entity.SignedListEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface SignedListRepository extends JpaRepository { +} diff --git a/src/main/java/eu/europa/ec/dgc/businessrule/restapi/controller/BusinessRuleController.java b/src/main/java/eu/europa/ec/dgc/businessrule/restapi/controller/BusinessRuleController.java index e336689..9d168cf 100644 --- a/src/main/java/eu/europa/ec/dgc/businessrule/restapi/controller/BusinessRuleController.java +++ b/src/main/java/eu/europa/ec/dgc/businessrule/restapi/controller/BusinessRuleController.java @@ -21,6 +21,7 @@ package eu.europa.ec.dgc.businessrule.restapi.controller; import eu.europa.ec.dgc.businessrule.entity.BusinessRuleEntity; +import eu.europa.ec.dgc.businessrule.entity.SignedListEntity; import eu.europa.ec.dgc.businessrule.exception.DgcaBusinessRulesResponseException; import eu.europa.ec.dgc.businessrule.restapi.dto.BusinessRuleListItemDto; import eu.europa.ec.dgc.businessrule.restapi.dto.ProblemReportDto; @@ -35,9 +36,11 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import java.util.List; import java.util.Locale; +import java.util.Optional; import javax.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; @@ -56,7 +59,7 @@ public class BusinessRuleController { private static final String API_VERSION_HEADER = "X-VERSION"; - private static final String X_SIGNATURE_HEADER = "X-SIGNATURE"; + public static final String X_SIGNATURE_HEADER = "X-SIGNATURE"; private final BusinessRuleService businessRuleService; @@ -90,8 +93,21 @@ public class BusinessRuleController { public ResponseEntity> getRules( @RequestHeader(value = API_VERSION_HEADER, required = false) String apiVersion ) { - - return ResponseEntity.ok(businessRuleService.getBusinessRulesList()); + Optional rulesList = businessRuleService.getBusinessRulesSignedList(); + ResponseEntity responseEntity; + if (rulesList.isPresent()) { + ResponseEntity.BodyBuilder respBuilder = ResponseEntity.ok(); + String signature = rulesList.get().getSignature(); + if (signature!=null & signature.length()>0) { + HttpHeaders responseHeaders = new HttpHeaders(); + responseHeaders.set(X_SIGNATURE_HEADER, signature); + respBuilder.headers(responseHeaders); + } + responseEntity = respBuilder.body(rulesList.get().getRawData()); + } else { + responseEntity = ResponseEntity.ok(businessRuleService.getBusinessRulesList()); + } + return responseEntity; } diff --git a/src/main/java/eu/europa/ec/dgc/businessrule/restapi/controller/CountryListController.java b/src/main/java/eu/europa/ec/dgc/businessrule/restapi/controller/CountryListController.java index 45043b0..0f9d39b 100644 --- a/src/main/java/eu/europa/ec/dgc/businessrule/restapi/controller/CountryListController.java +++ b/src/main/java/eu/europa/ec/dgc/businessrule/restapi/controller/CountryListController.java @@ -20,6 +20,7 @@ package eu.europa.ec.dgc.businessrule.restapi.controller; +import eu.europa.ec.dgc.businessrule.entity.CountryListEntity; import eu.europa.ec.dgc.businessrule.service.CountryListService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -31,6 +32,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -83,7 +85,16 @@ public class CountryListController { public ResponseEntity getCountryList( @RequestHeader(value = API_VERSION_HEADER, required = false) String apiVersion ) { - return ResponseEntity.ok(countryListService.getCountryList()); + CountryListEntity countryList = countryListService.getCountryList(); + ResponseEntity responseEntity; + if (countryList.getSignature()!=null) { + HttpHeaders responseHeaders = new HttpHeaders(); + responseHeaders.set(BusinessRuleController.X_SIGNATURE_HEADER, countryList.getSignature()); + responseEntity = ResponseEntity.ok().headers(responseHeaders).body(countryList.getRawData()); + } else { + responseEntity = ResponseEntity.ok(countryList.getRawData()); + } + return responseEntity; } diff --git a/src/main/java/eu/europa/ec/dgc/businessrule/restapi/controller/ValueSetController.java b/src/main/java/eu/europa/ec/dgc/businessrule/restapi/controller/ValueSetController.java deleted file mode 100644 index 6b76a56..0000000 --- a/src/main/java/eu/europa/ec/dgc/businessrule/restapi/controller/ValueSetController.java +++ /dev/null @@ -1,202 +0,0 @@ -/*- - * ---license-start - * eu-digital-green-certificates / dgca-businessrule-service - * --- - * Copyright (C) 2021 T-Systems International GmbH and all other contributors - * --- - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ---license-end - */ - -package eu.europa.ec.dgc.businessrule.restapi.controller; - - -import eu.europa.ec.dgc.businessrule.entity.ValueSetEntity; -import eu.europa.ec.dgc.businessrule.exception.DgcaBusinessRulesResponseException; -import eu.europa.ec.dgc.businessrule.restapi.dto.ProblemReportDto; -import eu.europa.ec.dgc.businessrule.restapi.dto.ValueSetListItemDto; -import eu.europa.ec.dgc.businessrule.service.ValueSetService; -import eu.europa.ec.dgc.businessrule.utils.BusinessRulesUtils; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.enums.ParameterIn; -import io.swagger.v3.oas.annotations.media.ArraySchema; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.ExampleObject; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import java.util.List; -import javax.validation.Valid; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestHeader; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequestMapping("/valuesets") -@Slf4j -@RequiredArgsConstructor -public class ValueSetController { - - private static final String API_VERSION_HEADER = "X-VERSION"; - - private final BusinessRulesUtils businessRulesUtils; - - private final ValueSetService valueSetService; - - - /** - * Http Method for getting the value set list. - */ - @GetMapping(path = "", produces = MediaType.APPLICATION_JSON_VALUE) - @Operation( - summary = "Gets the a list of all value set ids and value set hash values.", - description = "This method returns a list containing the ids and hash values of all value sets. The" - + " hash value can be used to check, if a value set has changed and needs to be updated. The hash value can" - + " also be used to download a specific value set afterwards.", - tags = {"Value Sets"}, - parameters = { - @Parameter( - in = ParameterIn.HEADER, - name = "X-VERSION", - description = "Version of the API. In preparation of changes in the future. Set it to \"1.0\"", - required = true, - schema = @Schema(implementation = String.class)) - }, - responses = { - @ApiResponse( - responseCode = "200", - description = "Returns a list of all value set ids and there hash values.", - content = @Content( - mediaType = MediaType.APPLICATION_JSON_VALUE, - array = @ArraySchema(schema = @Schema(implementation = ValueSetListItemDto.class)), - examples = { - @ExampleObject(value = "[\n" - + " {\n" - + " \"id\": \"country-2-codes\",\n" - + " \"hash\": \"923e4e556fe7936e4a3e92e76cfb3aa87be1bf30000b4df3b755247042eea0e7\"\n" - + " },\n" - + " {\n" - + " \"id\": \"covid-19-lab-result\",\n" - + " \"hash\": \"934e145e9bb1f560d1d3b1ec767ce3a4e9f86ae101260ed04a5cef8c1f5636c4\"\n" - + " },\n" - + " {\n" - + " \"id\": \"covid-19-lab-test-manufacturer-and-name\",\n" - + " \"hash\": \"9da3ed15d036c20339647f8db1cb67bfcfbd04575e10b0c0df8e55a76a173a97\"\n" - + " },\n" - + " {\n" - + " \"id\": \"covid-19-lab-test-type\",\n" - + " \"hash\": \"50ba87d7c774cd9d77e4d82f6ab34871119bc4ad51b5b6fa1100efa687be0094\"\n" - + " },\n" - + " {\n" - + " \"id\": \"disease-agent-targeted\",\n" - + " \"hash\": \"d4bfba1fd9f2eb29dfb2938220468ccb0b481d348f192e6015d36da4b911a83a\"\n" - + " },\n" - + " {\n" - + " \"id\": \"sct-vaccines-covid-19\",\n" - + " \"hash\": \"70505eab33ac1da351f782ee2e78e89451226c47360e7b89b8a6295bbb70eed6\"\n" - + " },\n" - + " {\n" - + " \"id\": \"vaccines-covid-19-auth-holders\",\n" - + " \"hash\": \"55af9c705a95ced1a7d9130043f71a7a01f72e168dbd451d23d1575962518ab6\"\n" - + " },\n" - + " {\n" - + " \"id\": \"vaccines-covid-19-names\",\n" - + " \"hash\": \"8651c3db9ed5332c8fa42943d4656d442a5264debc8482b6d11d4c9176149146\"\n" - + " }\n" - + "]") - })) - } - ) - public ResponseEntity> getValueSetList( - @RequestHeader(value = API_VERSION_HEADER, required = false) String apiVersion - ) { - return ResponseEntity.ok(valueSetService.getValueSetsList()); - } - - /** - * Http Method for getting specific value set . - */ - @GetMapping(path = "/{hash}", produces = MediaType.APPLICATION_JSON_VALUE) - @Operation( - summary = "Gets a specific value set by its hash value.", - description = "This method can be used to download a specific value set. Therefore the hash value of the value " - + "set must be provided as path parameter.", - tags = {"Value Sets"}, - parameters = { - @Parameter( - in = ParameterIn.PATH, - name = "hash", - description = "Hash of the value set to download", - required = true, - schema = @Schema(implementation = String.class)), - @Parameter( - in = ParameterIn.HEADER, - name = "X-VERSION", - description = "Version of the API. In preparation of changes in the future.", - required = true, - schema = @Schema(implementation = String.class)) - }, - responses = { - @ApiResponse( - responseCode = "200", - description = "Returns the specified value set.", - content = @Content( - mediaType = MediaType.APPLICATION_JSON_VALUE, - schema = @Schema(implementation = String.class), - examples = { - @ExampleObject(value = "{\n" - + " \"valueSetId\": \"disease-agent-targeted\",\n" - + " \"valueSetDate\": \"2021-04-27\",\n" - + " \"valueSetValues\": {\n" - + " \"840539006\": {\n" - + " \"display\": \"COVID-19\",\n" - + " \"lang\": \"en\",\n" - + " \"active\": true,\n" - + " \"version\": \"http://snomed.info/sct/900000000000207008/version/20210131\",\n" - + " \"system\": \"http://snomed.info/sct\"\n" - + " }\n" - + " }\n" - + "}") - })), - @ApiResponse( - responseCode = "404", - description = "Value set could not be found for the given hash value.", - content = @Content( - mediaType = MediaType.APPLICATION_JSON_VALUE, - schema = @Schema(implementation = ProblemReportDto.class) - )) - }) - - public ResponseEntity getValueSet( - @RequestHeader(value = API_VERSION_HEADER, required = false) String apiVersion, - @Valid @PathVariable("hash") String hash - ) { - ValueSetEntity vse = valueSetService.getValueSetByHash(hash); - - if (vse == null) { - throw new DgcaBusinessRulesResponseException(HttpStatus.NOT_FOUND, "0x001", "Possible reasons: " - + "The provided hash value is not correct", hash, ""); - } - - return ResponseEntity.ok(vse.getRawData()); - } - - -} diff --git a/src/main/java/eu/europa/ec/dgc/businessrule/service/BusinessRuleService.java b/src/main/java/eu/europa/ec/dgc/businessrule/service/BusinessRuleService.java index 9319c82..0145dff 100644 --- a/src/main/java/eu/europa/ec/dgc/businessrule/service/BusinessRuleService.java +++ b/src/main/java/eu/europa/ec/dgc/businessrule/service/BusinessRuleService.java @@ -20,19 +20,28 @@ package eu.europa.ec.dgc.businessrule.service; +import com.fasterxml.jackson.core.JsonProcessingException; import eu.europa.ec.dgc.businessrule.entity.BusinessRuleEntity; +import eu.europa.ec.dgc.businessrule.entity.ListType; +import eu.europa.ec.dgc.businessrule.entity.SignedListEntity; +import eu.europa.ec.dgc.businessrule.exception.DgcaBusinessRulesResponseException; import eu.europa.ec.dgc.businessrule.model.BusinessRuleItem; import eu.europa.ec.dgc.businessrule.repository.BusinessRuleRepository; +import eu.europa.ec.dgc.businessrule.repository.SignedListRepository; import eu.europa.ec.dgc.businessrule.restapi.dto.BusinessRuleListItemDto; import eu.europa.ec.dgc.businessrule.utils.BusinessRulesUtils; import eu.europa.ec.dgc.gateway.connector.model.ValidationRule; +import eu.europa.ec.dgc.utils.CertificateUtils; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.List; import java.util.Locale; +import java.util.Optional; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -43,6 +52,8 @@ public class BusinessRuleService { private final BusinessRuleRepository businessRuleRepository; + private final ListSigningService listSigningService; + private final SignedListRepository signedListRepository; private final BusinessRulesUtils businessRulesUtils; @@ -56,6 +67,10 @@ public List getBusinessRulesList() { return rulesItems; } + public Optional getBusinessRulesSignedList() { + return signedListRepository.findById(ListType.Rules); + } + /** * Gets list of all business rules ids and hashes for a country. */ @@ -80,7 +95,7 @@ public BusinessRuleEntity getBusinessRuleByCountryAndHash(String country, String * @param businessRules list of actual value sets */ @Transactional - public void updateBusinesRules(List businessRules) { + public void updateBusinessRules(List businessRules) { List ruleHashes = businessRules.stream().map(BusinessRuleItem::getHash).collect(Collectors.toList()); List alreadyStoredRules = getBusinessRulesHashList(); @@ -96,7 +111,7 @@ public void updateBusinesRules(List businessRules) { saveBusinessRule(rule); } } - + listSigningService.updateSignedList(getBusinessRulesList(),ListType.Rules); } /** diff --git a/src/main/java/eu/europa/ec/dgc/businessrule/service/CountryListService.java b/src/main/java/eu/europa/ec/dgc/businessrule/service/CountryListService.java index 2b99a83..965ab26 100644 --- a/src/main/java/eu/europa/ec/dgc/businessrule/service/CountryListService.java +++ b/src/main/java/eu/europa/ec/dgc/businessrule/service/CountryListService.java @@ -21,7 +21,11 @@ package eu.europa.ec.dgc.businessrule.service; import eu.europa.ec.dgc.businessrule.entity.CountryListEntity; +import eu.europa.ec.dgc.businessrule.entity.ListType; import eu.europa.ec.dgc.businessrule.repository.CountryListRepository; +import eu.europa.ec.dgc.businessrule.utils.BusinessRulesUtils; +import java.security.NoSuchAlgorithmException; +import java.util.Optional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @@ -35,19 +39,20 @@ public class CountryListService { private static final Long COUNTRY_LIST_ID = 1L; private final CountryListRepository countryListRepository; + private final Optional signingService; + private final BusinessRulesUtils businessRulesUtils; /** * Gets the actual country list. * @return the country list. */ @Transactional - public String getCountryList() { + public CountryListEntity getCountryList() { CountryListEntity cle = countryListRepository.getFirstById(COUNTRY_LIST_ID); - if (cle != null) { - return cle.getRawData(); - } else { - return "[]"; + if (cle == null) { + cle = new CountryListEntity(COUNTRY_LIST_ID,"[]",null,null); } + return cle; } @@ -57,8 +62,8 @@ public String getCountryList() { */ @Transactional public void updateCountryList(String newCountryListData) { - String oldList = getCountryList(); - if (!newCountryListData.equals(oldList)) { + CountryListEntity oldList = getCountryList(); + if (!newCountryListData.equals(oldList.getRawData())) { saveCountryList(newCountryListData); } } @@ -68,10 +73,17 @@ public void updateCountryList(String newCountryListData) { * Saves a country list by replacing an old one. * @param listData the country list to be saved. */ - @Transactional public void saveCountryList(String listData) { - CountryListEntity cle = new CountryListEntity(COUNTRY_LIST_ID,listData); + CountryListEntity cle = new CountryListEntity(COUNTRY_LIST_ID,listData,null,null); + try { + cle.setHash(businessRulesUtils.calculateHash(listData)); + } catch (NoSuchAlgorithmException e) { + new IllegalArgumentException(e); + } + if (signingService.isPresent()) { + cle.setSignature(signingService.get().computeSignature(cle.getHash())); + } countryListRepository.save(cle); } diff --git a/src/main/java/eu/europa/ec/dgc/businessrule/service/GatewayDataDownloadBtpServiceImpl.java b/src/main/java/eu/europa/ec/dgc/businessrule/service/GatewayDataDownloadBtpServiceImpl.java index 4d97636..7884691 100644 --- a/src/main/java/eu/europa/ec/dgc/businessrule/service/GatewayDataDownloadBtpServiceImpl.java +++ b/src/main/java/eu/europa/ec/dgc/businessrule/service/GatewayDataDownloadBtpServiceImpl.java @@ -84,7 +84,7 @@ public void downloadBusinessRules() { } if (!ruleItems.isEmpty()) { - businessRuleService.updateBusinesRules(ruleItems); + businessRuleService.updateBusinessRules(ruleItems); } else { log.warn("The download of the business rules seems to fail, as the download connector " + "returns an empty list. No data will be changed."); diff --git a/src/main/java/eu/europa/ec/dgc/businessrule/service/GatewayDataDownloadServiceImpl.java b/src/main/java/eu/europa/ec/dgc/businessrule/service/GatewayDataDownloadServiceImpl.java index af62673..087c010 100644 --- a/src/main/java/eu/europa/ec/dgc/businessrule/service/GatewayDataDownloadServiceImpl.java +++ b/src/main/java/eu/europa/ec/dgc/businessrule/service/GatewayDataDownloadServiceImpl.java @@ -74,7 +74,7 @@ public void downloadBusinessRules() { } if (!ruleItems.isEmpty()) { - businessRuleService.updateBusinesRules(ruleItems); + businessRuleService.updateBusinessRules(ruleItems); } else { log.warn("The download of the business rules seems to fail, as the download connector " + "returns an empty business rules list.-> No data was changed."); diff --git a/src/main/java/eu/europa/ec/dgc/businessrule/service/JksSigningService.java b/src/main/java/eu/europa/ec/dgc/businessrule/service/JksSigningService.java new file mode 100644 index 0000000..90c47aa --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/businessrule/service/JksSigningService.java @@ -0,0 +1,86 @@ +package eu.europa.ec.dgc.businessrule.service; + +import eu.europa.ec.dgc.businessrule.config.JksSigningConfig; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.Security; +import java.security.Signature; +import java.security.SignatureException; +import java.security.UnrecoverableEntryException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.util.Base64; +import javax.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +@Slf4j +@Profile("jks-signing") +public class JksSigningService implements SigningService { + private final JksSigningConfig jksSigningConfig; + + private Certificate cert; + private PrivateKey privateKey; + + /** + * PostConstruct method to load KeyStore for issuing certificates. + */ + @PostConstruct + public void loadKeyStore() throws KeyStoreException, IOException, + CertificateException, NoSuchAlgorithmException, UnrecoverableEntryException { + if (jksSigningConfig ==null || jksSigningConfig.getKeyStorePassword()==null) { + throw new IllegalArgumentException("missing configuration jwk-signing.keyStorePassword; can not init jks signing"); + } + final char[] keyStorePassword = jksSigningConfig.getKeyStorePassword().toCharArray(); + final String keyName = jksSigningConfig.getCertAlias(); + + Security.addProvider(new BouncyCastleProvider()); + Security.setProperty("crypto.policy", "unlimited"); + + KeyStore keyStore = KeyStore.getInstance("JKS"); + + File keyFile = new File(jksSigningConfig.getKeyStoreFile()); + if (!keyFile.isFile()) { + log.error("keyfile not found on: {} please adapt the configuration property: jwk-signing.keyStoreFile", + keyFile); + throw new IllegalArgumentException("keyfile not found on: " + keyFile + + " please adapt the configuration property: jwk-signing.keyStoreFile"); + } + try (InputStream is = new FileInputStream(jksSigningConfig.getKeyStoreFile())) { + final char[] privateKeyPassword = jksSigningConfig.getPrivateKeyPassword().toCharArray(); + keyStore.load(is, privateKeyPassword); + KeyStore.PasswordProtection keyPassword = + new KeyStore.PasswordProtection(keyStorePassword); + + KeyStore.PrivateKeyEntry privateKeyEntry = + (KeyStore.PrivateKeyEntry) keyStore.getEntry(keyName, keyPassword); + cert = keyStore.getCertificate(keyName); + privateKey = privateKeyEntry.getPrivateKey(); + } + } + + @Override + public String computeSignature(String hash) { + try { + Signature sig = Signature.getInstance("SHA256withECDSA"); + sig.initSign(privateKey); + sig.update(hash.getBytes(StandardCharsets.UTF_8)); + return Base64.getEncoder().encodeToString(sig.sign()); + } catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) { + throw new IllegalArgumentException("can not compute signature",e); + } + } +} diff --git a/src/main/java/eu/europa/ec/dgc/businessrule/service/ListSigningService.java b/src/main/java/eu/europa/ec/dgc/businessrule/service/ListSigningService.java new file mode 100644 index 0000000..50e0c17 --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/businessrule/service/ListSigningService.java @@ -0,0 +1,59 @@ +package eu.europa.ec.dgc.businessrule.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import eu.europa.ec.dgc.businessrule.entity.ListType; +import eu.europa.ec.dgc.businessrule.entity.SignedListEntity; +import eu.europa.ec.dgc.businessrule.repository.SignedListRepository; +import eu.europa.ec.dgc.businessrule.restapi.dto.BusinessRuleListItemDto; +import eu.europa.ec.dgc.businessrule.utils.BusinessRulesUtils; +import java.security.NoSuchAlgorithmException; +import java.util.List; +import java.util.Optional; +import liquibase.pro.packaged.T; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.stereotype.Service; + +@Slf4j +@RequiredArgsConstructor +@Service +public class ListSigningService { + private final SignedListRepository signedListRepository; + private final MappingJackson2HttpMessageConverter jacksonHttpMessageConverter; + private final Optional signingService; + private final BusinessRulesUtils businessRulesUtils; + + public void updateSignedList(List list,ListType listType) { + try { + String listRaw = jacksonHttpMessageConverter.getObjectMapper().writeValueAsString(list); + String hash = businessRulesUtils.calculateHash(listRaw); + Optional ruleList = signedListRepository.findById(listType); + if (ruleList.isEmpty()) { + SignedListEntity signedListEntity = new SignedListEntity(); + signedListEntity.setListType(listType); + signedListEntity.setHash(hash); + signedListEntity.setRawData(listRaw); + calculateSignature(signedListEntity); + signedListRepository.save(signedListEntity); + } else { + if (!ruleList.get().getHash().equals(hash)) { + ruleList.get().setHash(hash); + calculateSignature(ruleList.get()); + signedListRepository.save(ruleList.get()); + } + } + } catch (JsonProcessingException | NoSuchAlgorithmException e) { + log.error("can not create siglist",e); + } + } + + private void calculateSignature(SignedListEntity signedListEntity) { + if (signingService.isPresent()) { + signedListEntity.setSignature(signingService.get().computeSignature(signedListEntity.getHash())); + } else { + signedListEntity.setSignature(""); + } + } + +} diff --git a/src/main/java/eu/europa/ec/dgc/businessrule/service/SigningService.java b/src/main/java/eu/europa/ec/dgc/businessrule/service/SigningService.java new file mode 100644 index 0000000..d41ec39 --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/businessrule/service/SigningService.java @@ -0,0 +1,11 @@ +package eu.europa.ec.dgc.businessrule.service; + +public interface SigningService { + /** + * compute hash. + * As EC Curve is P256 (with SHA256 or "SHA256WITHECDSA" in Bouncycastle). + * @param hash + * @return ans1 base64 coded signature + */ + String computeSignature(String hash); +} diff --git a/src/main/java/eu/europa/ec/dgc/businessrule/service/ValueSetService.java b/src/main/java/eu/europa/ec/dgc/businessrule/service/ValueSetService.java index 962c7f7..73e4f82 100644 --- a/src/main/java/eu/europa/ec/dgc/businessrule/service/ValueSetService.java +++ b/src/main/java/eu/europa/ec/dgc/businessrule/service/ValueSetService.java @@ -20,8 +20,11 @@ package eu.europa.ec.dgc.businessrule.service; +import eu.europa.ec.dgc.businessrule.entity.ListType; +import eu.europa.ec.dgc.businessrule.entity.SignedListEntity; import eu.europa.ec.dgc.businessrule.entity.ValueSetEntity; import eu.europa.ec.dgc.businessrule.model.ValueSetItem; +import eu.europa.ec.dgc.businessrule.repository.SignedListRepository; import eu.europa.ec.dgc.businessrule.repository.ValueSetRepository; import eu.europa.ec.dgc.businessrule.restapi.dto.ValueSetListItemDto; import eu.europa.ec.dgc.businessrule.utils.BusinessRulesUtils; @@ -29,6 +32,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -43,7 +47,8 @@ public class ValueSetService { private final BusinessRulesUtils businessRulesUtils; private final ValueSetRepository valueSetRepository; - + private final ListSigningService listSigningService; + private final SignedListRepository signedListRepository; /** * Gets list of all value set ids and hashes. @@ -54,6 +59,10 @@ public List getValueSetsList() { return valueSetItems; } + public Optional getValueSetsSignedList() { + return signedListRepository.findById(ListType.ValueSets); + } + /** * Gets a value set by its hash value. @@ -92,6 +101,7 @@ public void updateValueSets(List valueSets) { log.debug("Value set already exists in database. Persisting skipped."); } } + listSigningService.updateSignedList(getValueSetsList(), ListType.ValueSets); } diff --git a/src/main/resources/application-jks-signing.yml b/src/main/resources/application-jks-signing.yml new file mode 100644 index 0000000..8869a88 --- /dev/null +++ b/src/main/resources/application-jks-signing.yml @@ -0,0 +1,5 @@ +jks-signing: + keyStoreFile: certs/signing.jks + keyStorePassword: dgca + certAlias: dgca + privateKeyPassword: dgca \ No newline at end of file diff --git a/src/main/resources/db/changelog.xml b/src/main/resources/db/changelog.xml index bb27197..fd9d6be 100644 --- a/src/main/resources/db/changelog.xml +++ b/src/main/resources/db/changelog.xml @@ -5,5 +5,6 @@ xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.3.xsd"> + \ No newline at end of file diff --git a/src/main/resources/db/changelog/add_list_table.xml b/src/main/resources/db/changelog/add_list_table.xml new file mode 100644 index 0000000..ae7c634 --- /dev/null +++ b/src/main/resources/db/changelog/add_list_table.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/java/eu/europa/ec/dgc/businessrule/restapi/controller/CountryListControllerIntegrationTest.java b/src/test/java/eu/europa/ec/dgc/businessrule/restapi/controller/CountryListControllerIntegrationTest.java index b169ada..038d8d8 100644 --- a/src/test/java/eu/europa/ec/dgc/businessrule/restapi/controller/CountryListControllerIntegrationTest.java +++ b/src/test/java/eu/europa/ec/dgc/businessrule/restapi/controller/CountryListControllerIntegrationTest.java @@ -79,7 +79,7 @@ void getEmptyCountryList() throws Exception { @Test void getCountryList() throws Exception { - CountryListEntity cle = new CountryListEntity(COUNTRY_LIST_ID, TEST_LIST_DATA); + CountryListEntity cle = new CountryListEntity(COUNTRY_LIST_ID, TEST_LIST_DATA,null,null); countryListRepository.save(cle); mockMvc.perform(get("/countrylist")) diff --git a/src/test/java/eu/europa/ec/dgc/businessrule/service/BusinessRuleServiceTest.java b/src/test/java/eu/europa/ec/dgc/businessrule/service/BusinessRuleServiceTest.java index 22fcde8..c3376ba 100644 --- a/src/test/java/eu/europa/ec/dgc/businessrule/service/BusinessRuleServiceTest.java +++ b/src/test/java/eu/europa/ec/dgc/businessrule/service/BusinessRuleServiceTest.java @@ -1,8 +1,11 @@ package eu.europa.ec.dgc.businessrule.service; import eu.europa.ec.dgc.businessrule.entity.BusinessRuleEntity; +import eu.europa.ec.dgc.businessrule.entity.ListType; +import eu.europa.ec.dgc.businessrule.entity.SignedListEntity; import eu.europa.ec.dgc.businessrule.model.BusinessRuleItem; import eu.europa.ec.dgc.businessrule.repository.BusinessRuleRepository; +import eu.europa.ec.dgc.businessrule.repository.SignedListRepository; import eu.europa.ec.dgc.businessrule.testdata.BusinessRulesTestHelper; import eu.europa.ec.dgc.businessrule.utils.BusinessRulesUtils; import eu.europa.ec.dgc.gateway.connector.DgcGatewayCountryListDownloadConnector; @@ -11,6 +14,7 @@ import eu.europa.ec.dgc.gateway.connector.model.ValidationRule; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -18,11 +22,15 @@ import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.ActiveProfiles; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; @SpringBootTest @AutoConfigureMockMvc +@ActiveProfiles("jks-signing") class BusinessRuleServiceTest { @MockBean @@ -37,6 +45,9 @@ class BusinessRuleServiceTest { @Autowired BusinessRuleService businessRuleService; + @Autowired + SignedListRepository signedListRepository; + @Autowired BusinessRuleRepository businessRuleRepository; @@ -68,7 +79,7 @@ void updateBusinessRulesWithExisting() throws Exception { businessRuleItem.setRawData(BusinessRulesTestHelper.BR_DATA_1); businessRuleItems.add(businessRuleItem); - businessRuleService.updateBusinesRules(businessRuleItems); + businessRuleService.updateBusinessRules(businessRuleItems); Assertions.assertEquals(1, businessRuleRepository.count()); } @@ -81,7 +92,7 @@ void updateBusinessRulesWithEmptyList() { List businessRuleItems = new ArrayList<>(); - businessRuleService.updateBusinesRules(businessRuleItems); + businessRuleService.updateBusinessRules(businessRuleItems); Assertions.assertEquals(0, businessRuleRepository.count()); } @@ -112,13 +123,13 @@ void updateBusinessRule() throws Exception { Item2.setRawData(BusinessRulesTestHelper.BR_DATA_2); businessRuleItems.add(Item2); - businessRuleService.updateBusinesRules(businessRuleItems); + businessRuleService.updateBusinessRules(businessRuleItems); Assertions.assertEquals(2, businessRuleRepository.count()); businessRuleItems.remove(0); - businessRuleService.updateBusinesRules(businessRuleItems); + businessRuleService.updateBusinessRules(businessRuleItems); List result = businessRuleRepository.findAll(); Assertions.assertEquals(1, result.size()); @@ -130,6 +141,10 @@ void updateBusinessRule() throws Exception { Assertions.assertEquals(BusinessRulesTestHelper.BR_COUNTRY_2, resultEntity.getCountry()); Assertions.assertEquals(BusinessRulesTestHelper.BR_VERSION_2, resultEntity.getVersion()); Assertions.assertEquals(BusinessRulesTestHelper.BR_DATA_2, resultEntity.getRawData()); + + Optional rules = signedListRepository.findById(ListType.Rules); + assertTrue(rules.isPresent()); + assertNotNull(rules.get().getSignature()); } diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index abd53f6..338cdf0 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -29,3 +29,9 @@ springdoc: swagger-ui: path: /swagger +jks-signing: + keyStoreFile: certs/signing.jks + keyStorePassword: dgca + certAlias: dgca + privateKeyPassword: dgca + From f0bc6be5944bee6f0311a30a407c85734f347c70 Mon Sep 17 00:00:00 2001 From: Artur T <81235055+a-trzewik@users.noreply.github.com> Date: Wed, 7 Jul 2021 11:08:47 +0200 Subject: [PATCH 2/7] fix/undo delete file This reverts commit 2c1d8bb776280d55b4fca924690004305ff3abf1. --- .../controller/ValueSetController.java | 202 ++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100644 src/main/java/eu/europa/ec/dgc/businessrule/restapi/controller/ValueSetController.java diff --git a/src/main/java/eu/europa/ec/dgc/businessrule/restapi/controller/ValueSetController.java b/src/main/java/eu/europa/ec/dgc/businessrule/restapi/controller/ValueSetController.java new file mode 100644 index 0000000..6b76a56 --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/businessrule/restapi/controller/ValueSetController.java @@ -0,0 +1,202 @@ +/*- + * ---license-start + * eu-digital-green-certificates / dgca-businessrule-service + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ---license-end + */ + +package eu.europa.ec.dgc.businessrule.restapi.controller; + + +import eu.europa.ec.dgc.businessrule.entity.ValueSetEntity; +import eu.europa.ec.dgc.businessrule.exception.DgcaBusinessRulesResponseException; +import eu.europa.ec.dgc.businessrule.restapi.dto.ProblemReportDto; +import eu.europa.ec.dgc.businessrule.restapi.dto.ValueSetListItemDto; +import eu.europa.ec.dgc.businessrule.service.ValueSetService; +import eu.europa.ec.dgc.businessrule.utils.BusinessRulesUtils; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import java.util.List; +import javax.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/valuesets") +@Slf4j +@RequiredArgsConstructor +public class ValueSetController { + + private static final String API_VERSION_HEADER = "X-VERSION"; + + private final BusinessRulesUtils businessRulesUtils; + + private final ValueSetService valueSetService; + + + /** + * Http Method for getting the value set list. + */ + @GetMapping(path = "", produces = MediaType.APPLICATION_JSON_VALUE) + @Operation( + summary = "Gets the a list of all value set ids and value set hash values.", + description = "This method returns a list containing the ids and hash values of all value sets. The" + + " hash value can be used to check, if a value set has changed and needs to be updated. The hash value can" + + " also be used to download a specific value set afterwards.", + tags = {"Value Sets"}, + parameters = { + @Parameter( + in = ParameterIn.HEADER, + name = "X-VERSION", + description = "Version of the API. In preparation of changes in the future. Set it to \"1.0\"", + required = true, + schema = @Schema(implementation = String.class)) + }, + responses = { + @ApiResponse( + responseCode = "200", + description = "Returns a list of all value set ids and there hash values.", + content = @Content( + mediaType = MediaType.APPLICATION_JSON_VALUE, + array = @ArraySchema(schema = @Schema(implementation = ValueSetListItemDto.class)), + examples = { + @ExampleObject(value = "[\n" + + " {\n" + + " \"id\": \"country-2-codes\",\n" + + " \"hash\": \"923e4e556fe7936e4a3e92e76cfb3aa87be1bf30000b4df3b755247042eea0e7\"\n" + + " },\n" + + " {\n" + + " \"id\": \"covid-19-lab-result\",\n" + + " \"hash\": \"934e145e9bb1f560d1d3b1ec767ce3a4e9f86ae101260ed04a5cef8c1f5636c4\"\n" + + " },\n" + + " {\n" + + " \"id\": \"covid-19-lab-test-manufacturer-and-name\",\n" + + " \"hash\": \"9da3ed15d036c20339647f8db1cb67bfcfbd04575e10b0c0df8e55a76a173a97\"\n" + + " },\n" + + " {\n" + + " \"id\": \"covid-19-lab-test-type\",\n" + + " \"hash\": \"50ba87d7c774cd9d77e4d82f6ab34871119bc4ad51b5b6fa1100efa687be0094\"\n" + + " },\n" + + " {\n" + + " \"id\": \"disease-agent-targeted\",\n" + + " \"hash\": \"d4bfba1fd9f2eb29dfb2938220468ccb0b481d348f192e6015d36da4b911a83a\"\n" + + " },\n" + + " {\n" + + " \"id\": \"sct-vaccines-covid-19\",\n" + + " \"hash\": \"70505eab33ac1da351f782ee2e78e89451226c47360e7b89b8a6295bbb70eed6\"\n" + + " },\n" + + " {\n" + + " \"id\": \"vaccines-covid-19-auth-holders\",\n" + + " \"hash\": \"55af9c705a95ced1a7d9130043f71a7a01f72e168dbd451d23d1575962518ab6\"\n" + + " },\n" + + " {\n" + + " \"id\": \"vaccines-covid-19-names\",\n" + + " \"hash\": \"8651c3db9ed5332c8fa42943d4656d442a5264debc8482b6d11d4c9176149146\"\n" + + " }\n" + + "]") + })) + } + ) + public ResponseEntity> getValueSetList( + @RequestHeader(value = API_VERSION_HEADER, required = false) String apiVersion + ) { + return ResponseEntity.ok(valueSetService.getValueSetsList()); + } + + /** + * Http Method for getting specific value set . + */ + @GetMapping(path = "/{hash}", produces = MediaType.APPLICATION_JSON_VALUE) + @Operation( + summary = "Gets a specific value set by its hash value.", + description = "This method can be used to download a specific value set. Therefore the hash value of the value " + + "set must be provided as path parameter.", + tags = {"Value Sets"}, + parameters = { + @Parameter( + in = ParameterIn.PATH, + name = "hash", + description = "Hash of the value set to download", + required = true, + schema = @Schema(implementation = String.class)), + @Parameter( + in = ParameterIn.HEADER, + name = "X-VERSION", + description = "Version of the API. In preparation of changes in the future.", + required = true, + schema = @Schema(implementation = String.class)) + }, + responses = { + @ApiResponse( + responseCode = "200", + description = "Returns the specified value set.", + content = @Content( + mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = String.class), + examples = { + @ExampleObject(value = "{\n" + + " \"valueSetId\": \"disease-agent-targeted\",\n" + + " \"valueSetDate\": \"2021-04-27\",\n" + + " \"valueSetValues\": {\n" + + " \"840539006\": {\n" + + " \"display\": \"COVID-19\",\n" + + " \"lang\": \"en\",\n" + + " \"active\": true,\n" + + " \"version\": \"http://snomed.info/sct/900000000000207008/version/20210131\",\n" + + " \"system\": \"http://snomed.info/sct\"\n" + + " }\n" + + " }\n" + + "}") + })), + @ApiResponse( + responseCode = "404", + description = "Value set could not be found for the given hash value.", + content = @Content( + mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = ProblemReportDto.class) + )) + }) + + public ResponseEntity getValueSet( + @RequestHeader(value = API_VERSION_HEADER, required = false) String apiVersion, + @Valid @PathVariable("hash") String hash + ) { + ValueSetEntity vse = valueSetService.getValueSetByHash(hash); + + if (vse == null) { + throw new DgcaBusinessRulesResponseException(HttpStatus.NOT_FOUND, "0x001", "Possible reasons: " + + "The provided hash value is not correct", hash, ""); + } + + return ResponseEntity.ok(vse.getRawData()); + } + + +} From a6962d66d39a9e8b71539fc5c5195a49953e2b64 Mon Sep 17 00:00:00 2001 From: Artur T <81235055+a-trzewik@users.noreply.github.com> Date: Wed, 7 Jul 2021 11:30:24 +0200 Subject: [PATCH 3/7] fix/check style --- .../controller/BusinessRuleController.java | 2 +- .../controller/CountryListController.java | 2 +- .../controller/ValueSetController.java | 19 ++++++++++++++++++- .../service/CountryListService.java | 2 +- .../service/JksSigningService.java | 7 ++++--- .../service/ListSigningService.java | 6 ++++++ .../businessrule/service/SigningService.java | 2 +- 7 files changed, 32 insertions(+), 8 deletions(-) diff --git a/src/main/java/eu/europa/ec/dgc/businessrule/restapi/controller/BusinessRuleController.java b/src/main/java/eu/europa/ec/dgc/businessrule/restapi/controller/BusinessRuleController.java index 9d168cf..a0bacd4 100644 --- a/src/main/java/eu/europa/ec/dgc/businessrule/restapi/controller/BusinessRuleController.java +++ b/src/main/java/eu/europa/ec/dgc/businessrule/restapi/controller/BusinessRuleController.java @@ -98,7 +98,7 @@ public ResponseEntity> getRules( if (rulesList.isPresent()) { ResponseEntity.BodyBuilder respBuilder = ResponseEntity.ok(); String signature = rulesList.get().getSignature(); - if (signature!=null & signature.length()>0) { + if (signature != null & signature.length() > 0) { HttpHeaders responseHeaders = new HttpHeaders(); responseHeaders.set(X_SIGNATURE_HEADER, signature); respBuilder.headers(responseHeaders); diff --git a/src/main/java/eu/europa/ec/dgc/businessrule/restapi/controller/CountryListController.java b/src/main/java/eu/europa/ec/dgc/businessrule/restapi/controller/CountryListController.java index 0f9d39b..5c8169a 100644 --- a/src/main/java/eu/europa/ec/dgc/businessrule/restapi/controller/CountryListController.java +++ b/src/main/java/eu/europa/ec/dgc/businessrule/restapi/controller/CountryListController.java @@ -87,7 +87,7 @@ public ResponseEntity getCountryList( ) { CountryListEntity countryList = countryListService.getCountryList(); ResponseEntity responseEntity; - if (countryList.getSignature()!=null) { + if (countryList.getSignature() != null) { HttpHeaders responseHeaders = new HttpHeaders(); responseHeaders.set(BusinessRuleController.X_SIGNATURE_HEADER, countryList.getSignature()); responseEntity = ResponseEntity.ok().headers(responseHeaders).body(countryList.getRawData()); diff --git a/src/main/java/eu/europa/ec/dgc/businessrule/restapi/controller/ValueSetController.java b/src/main/java/eu/europa/ec/dgc/businessrule/restapi/controller/ValueSetController.java index 6b76a56..5f44252 100644 --- a/src/main/java/eu/europa/ec/dgc/businessrule/restapi/controller/ValueSetController.java +++ b/src/main/java/eu/europa/ec/dgc/businessrule/restapi/controller/ValueSetController.java @@ -21,6 +21,7 @@ package eu.europa.ec.dgc.businessrule.restapi.controller; +import eu.europa.ec.dgc.businessrule.entity.SignedListEntity; import eu.europa.ec.dgc.businessrule.entity.ValueSetEntity; import eu.europa.ec.dgc.businessrule.exception.DgcaBusinessRulesResponseException; import eu.europa.ec.dgc.businessrule.restapi.dto.ProblemReportDto; @@ -36,9 +37,11 @@ import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import java.util.List; +import java.util.Optional; import javax.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; @@ -127,7 +130,21 @@ public class ValueSetController { public ResponseEntity> getValueSetList( @RequestHeader(value = API_VERSION_HEADER, required = false) String apiVersion ) { - return ResponseEntity.ok(valueSetService.getValueSetsList()); + Optional rulesList = valueSetService.getValueSetsSignedList(); + ResponseEntity responseEntity; + if (rulesList.isPresent()) { + ResponseEntity.BodyBuilder respBuilder = ResponseEntity.ok(); + String signature = rulesList.get().getSignature(); + if (signature != null & signature.length() > 0) { + HttpHeaders responseHeaders = new HttpHeaders(); + responseHeaders.set(BusinessRuleController.X_SIGNATURE_HEADER, signature); + respBuilder.headers(responseHeaders); + } + responseEntity = respBuilder.body(rulesList.get().getRawData()); + } else { + responseEntity = ResponseEntity.ok(valueSetService.getValueSetsList()); + } + return responseEntity; } /** diff --git a/src/main/java/eu/europa/ec/dgc/businessrule/service/CountryListService.java b/src/main/java/eu/europa/ec/dgc/businessrule/service/CountryListService.java index 965ab26..6b6be01 100644 --- a/src/main/java/eu/europa/ec/dgc/businessrule/service/CountryListService.java +++ b/src/main/java/eu/europa/ec/dgc/businessrule/service/CountryListService.java @@ -82,7 +82,7 @@ public void saveCountryList(String listData) { new IllegalArgumentException(e); } if (signingService.isPresent()) { - cle.setSignature(signingService.get().computeSignature(cle.getHash())); + cle.setSignature(signingService.get().computeSignature(cle.getHash())); } countryListRepository.save(cle); } diff --git a/src/main/java/eu/europa/ec/dgc/businessrule/service/JksSigningService.java b/src/main/java/eu/europa/ec/dgc/businessrule/service/JksSigningService.java index 90c47aa..34ddd8e 100644 --- a/src/main/java/eu/europa/ec/dgc/businessrule/service/JksSigningService.java +++ b/src/main/java/eu/europa/ec/dgc/businessrule/service/JksSigningService.java @@ -41,8 +41,9 @@ public class JksSigningService implements SigningService { @PostConstruct public void loadKeyStore() throws KeyStoreException, IOException, CertificateException, NoSuchAlgorithmException, UnrecoverableEntryException { - if (jksSigningConfig ==null || jksSigningConfig.getKeyStorePassword()==null) { - throw new IllegalArgumentException("missing configuration jwk-signing.keyStorePassword; can not init jks signing"); + if (jksSigningConfig == null || jksSigningConfig.getKeyStorePassword() == null) { + throw new IllegalArgumentException("missing configuration jwk-signing.keyStorePassword; " + + "can not init jks signing"); } final char[] keyStorePassword = jksSigningConfig.getKeyStorePassword().toCharArray(); final String keyName = jksSigningConfig.getCertAlias(); @@ -80,7 +81,7 @@ public String computeSignature(String hash) { sig.update(hash.getBytes(StandardCharsets.UTF_8)); return Base64.getEncoder().encodeToString(sig.sign()); } catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) { - throw new IllegalArgumentException("can not compute signature",e); + throw new IllegalArgumentException("can not compute signature", e); } } } diff --git a/src/main/java/eu/europa/ec/dgc/businessrule/service/ListSigningService.java b/src/main/java/eu/europa/ec/dgc/businessrule/service/ListSigningService.java index 50e0c17..c7ca83f 100644 --- a/src/main/java/eu/europa/ec/dgc/businessrule/service/ListSigningService.java +++ b/src/main/java/eu/europa/ec/dgc/businessrule/service/ListSigningService.java @@ -24,6 +24,12 @@ public class ListSigningService { private final Optional signingService; private final BusinessRulesUtils businessRulesUtils; + /** + * update or create signed list. + * @param list list of elements + * @param listType type of list + * @param type of list elem + */ public void updateSignedList(List list,ListType listType) { try { String listRaw = jacksonHttpMessageConverter.getObjectMapper().writeValueAsString(list); diff --git a/src/main/java/eu/europa/ec/dgc/businessrule/service/SigningService.java b/src/main/java/eu/europa/ec/dgc/businessrule/service/SigningService.java index d41ec39..5215781 100644 --- a/src/main/java/eu/europa/ec/dgc/businessrule/service/SigningService.java +++ b/src/main/java/eu/europa/ec/dgc/businessrule/service/SigningService.java @@ -4,7 +4,7 @@ public interface SigningService { /** * compute hash. * As EC Curve is P256 (with SHA256 or "SHA256WITHECDSA" in Bouncycastle). - * @param hash + * @param hash hash * @return ans1 base64 coded signature */ String computeSignature(String hash); From 01dc955e924a69381783ea09ef9d483e0f5db2de Mon Sep 17 00:00:00 2001 From: Artur T <81235055+a-trzewik@users.noreply.github.com> Date: Wed, 7 Jul 2021 11:59:32 +0200 Subject: [PATCH 4/7] feat/add publickey endpoint --- .../restapi/controller/SigningController.java | 47 +++++++++++++++++++ .../service/JksSigningService.java | 5 ++ .../businessrule/service/SigningService.java | 6 +++ 3 files changed, 58 insertions(+) create mode 100644 src/main/java/eu/europa/ec/dgc/businessrule/restapi/controller/SigningController.java diff --git a/src/main/java/eu/europa/ec/dgc/businessrule/restapi/controller/SigningController.java b/src/main/java/eu/europa/ec/dgc/businessrule/restapi/controller/SigningController.java new file mode 100644 index 0000000..0c2df2e --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/businessrule/restapi/controller/SigningController.java @@ -0,0 +1,47 @@ +package eu.europa.ec.dgc.businessrule.restapi.controller; + +import eu.europa.ec.dgc.businessrule.service.SigningService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/publickey") +@Slf4j +@RequiredArgsConstructor +public class SigningController { + private final Optional signingService; + + /** + * Http Method for getting the business rules list. + */ + @GetMapping(path = "", produces = MediaType.APPLICATION_JSON_VALUE) + @Operation( + summary = "Gets the signing public key (der base64 encoded)", + description = "Gets the signing public key (der base64 encoded)", + tags = {"Business Rules"}, + responses = { + @ApiResponse( + responseCode = "200", + description = "public key"), + @ApiResponse( + responseCode = "404", + description = "signing not supported"), + } + ) + public ResponseEntity getPublicKey() { + if (signingService.isPresent()) { + return ResponseEntity.ok(signingService.get().getPublicKey()); + } else { + return ResponseEntity.status(HttpStatus.NOT_FOUND).build(); + } + } +} diff --git a/src/main/java/eu/europa/ec/dgc/businessrule/service/JksSigningService.java b/src/main/java/eu/europa/ec/dgc/businessrule/service/JksSigningService.java index 34ddd8e..62cf73f 100644 --- a/src/main/java/eu/europa/ec/dgc/businessrule/service/JksSigningService.java +++ b/src/main/java/eu/europa/ec/dgc/businessrule/service/JksSigningService.java @@ -84,4 +84,9 @@ public String computeSignature(String hash) { throw new IllegalArgumentException("can not compute signature", e); } } + + @Override + public String getPublicKey() { + return Base64.getEncoder().encodeToString(cert.getPublicKey().getEncoded()); + } } diff --git a/src/main/java/eu/europa/ec/dgc/businessrule/service/SigningService.java b/src/main/java/eu/europa/ec/dgc/businessrule/service/SigningService.java index 5215781..058f333 100644 --- a/src/main/java/eu/europa/ec/dgc/businessrule/service/SigningService.java +++ b/src/main/java/eu/europa/ec/dgc/businessrule/service/SigningService.java @@ -8,4 +8,10 @@ public interface SigningService { * @return ans1 base64 coded signature */ String computeSignature(String hash); + + /** + * get signing public key . + * @return base64 der encoded key + */ + String getPublicKey(); } From b4b4d7f077b2a3acd0e631786fea849da12676b6 Mon Sep 17 00:00:00 2001 From: Artur T <81235055+a-trzewik@users.noreply.github.com> Date: Wed, 7 Jul 2021 13:06:30 +0200 Subject: [PATCH 5/7] fix/junit --- .../controller/ValueSetControllerIntegrationTest.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/test/java/eu/europa/ec/dgc/businessrule/restapi/controller/ValueSetControllerIntegrationTest.java b/src/test/java/eu/europa/ec/dgc/businessrule/restapi/controller/ValueSetControllerIntegrationTest.java index c134c59..44822d6 100644 --- a/src/test/java/eu/europa/ec/dgc/businessrule/restapi/controller/ValueSetControllerIntegrationTest.java +++ b/src/test/java/eu/europa/ec/dgc/businessrule/restapi/controller/ValueSetControllerIntegrationTest.java @@ -20,7 +20,11 @@ package eu.europa.ec.dgc.businessrule.restapi.controller; +import eu.europa.ec.dgc.businessrule.entity.ListType; import eu.europa.ec.dgc.businessrule.repository.ValueSetRepository; +import eu.europa.ec.dgc.businessrule.service.BusinessRuleService; +import eu.europa.ec.dgc.businessrule.service.ListSigningService; +import eu.europa.ec.dgc.businessrule.service.ValueSetService; import eu.europa.ec.dgc.businessrule.testdata.BusinessRulesTestHelper; import eu.europa.ec.dgc.gateway.connector.DgcGatewayCountryListDownloadConnector; import eu.europa.ec.dgc.gateway.connector.DgcGatewayValidationRuleDownloadConnector; @@ -62,7 +66,11 @@ class ValueSetControllerIntegrationTest { @Autowired private MockMvc mockMvc; + @Autowired + private ListSigningService listSigningService; + @Autowired + private ValueSetService valueSetService; @BeforeEach void clearRepositoryData() { @@ -95,6 +103,8 @@ void getValueSetList() throws Exception { BusinessRulesTestHelper.VALUESET_IDENTIFIER_2, BusinessRulesTestHelper.VALUESET_DATA_2); + listSigningService.updateSignedList(valueSetService.getValueSetsList(), ListType.ValueSets); + mockMvc.perform(get("/valuesets").header(API_VERSION_HEADER, "1.0")) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) From a6de79fed5f194e5e3789a55298bd66e96100c9f Mon Sep 17 00:00:00 2001 From: Artur T <81235055+a-trzewik@users.noreply.github.com> Date: Wed, 7 Jul 2021 13:29:32 +0200 Subject: [PATCH 6/7] fix junit --- .../controller/ValueSetControllerIntegrationTest.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/test/java/eu/europa/ec/dgc/businessrule/restapi/controller/ValueSetControllerIntegrationTest.java b/src/test/java/eu/europa/ec/dgc/businessrule/restapi/controller/ValueSetControllerIntegrationTest.java index 44822d6..2154a7e 100644 --- a/src/test/java/eu/europa/ec/dgc/businessrule/restapi/controller/ValueSetControllerIntegrationTest.java +++ b/src/test/java/eu/europa/ec/dgc/businessrule/restapi/controller/ValueSetControllerIntegrationTest.java @@ -21,9 +21,12 @@ package eu.europa.ec.dgc.businessrule.restapi.controller; import eu.europa.ec.dgc.businessrule.entity.ListType; +import eu.europa.ec.dgc.businessrule.entity.SignedListEntity; +import eu.europa.ec.dgc.businessrule.repository.SignedListRepository; import eu.europa.ec.dgc.businessrule.repository.ValueSetRepository; import eu.europa.ec.dgc.businessrule.service.BusinessRuleService; import eu.europa.ec.dgc.businessrule.service.ListSigningService; +import eu.europa.ec.dgc.businessrule.service.SigningService; import eu.europa.ec.dgc.businessrule.service.ValueSetService; import eu.europa.ec.dgc.businessrule.testdata.BusinessRulesTestHelper; import eu.europa.ec.dgc.gateway.connector.DgcGatewayCountryListDownloadConnector; @@ -72,9 +75,13 @@ class ValueSetControllerIntegrationTest { @Autowired private ValueSetService valueSetService; + @Autowired + private SignedListRepository signedListRepository; + @BeforeEach void clearRepositoryData() { valueSetRepository.deleteAll(); + signedListRepository.deleteAll(); } @Test From 139ee07cd578aca4bcc46552829a353380afd919 Mon Sep 17 00:00:00 2001 From: Artur T <81235055+a-trzewik@users.noreply.github.com> Date: Wed, 7 Jul 2021 13:34:02 +0200 Subject: [PATCH 7/7] fix findbug --- .../europa/ec/dgc/businessrule/service/CountryListService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/eu/europa/ec/dgc/businessrule/service/CountryListService.java b/src/main/java/eu/europa/ec/dgc/businessrule/service/CountryListService.java index 6b6be01..0e41df8 100644 --- a/src/main/java/eu/europa/ec/dgc/businessrule/service/CountryListService.java +++ b/src/main/java/eu/europa/ec/dgc/businessrule/service/CountryListService.java @@ -79,7 +79,7 @@ public void saveCountryList(String listData) { try { cle.setHash(businessRulesUtils.calculateHash(listData)); } catch (NoSuchAlgorithmException e) { - new IllegalArgumentException(e); + throw new IllegalArgumentException(e); } if (signingService.isPresent()) { cle.setSignature(signingService.get().computeSignature(cle.getHash()));