diff --git a/certs/signing.jks b/certs/signing.jks new file mode 100644 index 0000000..bbd29aa Binary files /dev/null and b/certs/signing.jks differ 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..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 @@ -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..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 @@ -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/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/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/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..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 @@ -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) { + throw 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..62cf73f --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/businessrule/service/JksSigningService.java @@ -0,0 +1,92 @@ +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); + } + } + + @Override + public String getPublicKey() { + return Base64.getEncoder().encodeToString(cert.getPublicKey().getEncoded()); + } +} 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..c7ca83f --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/businessrule/service/ListSigningService.java @@ -0,0 +1,65 @@ +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; + + /** + * 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); + 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..058f333 --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/businessrule/service/SigningService.java @@ -0,0 +1,17 @@ +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 hash + * @return ans1 base64 coded signature + */ + String computeSignature(String hash); + + /** + * get signing public key . + * @return base64 der encoded key + */ + String getPublicKey(); +} 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/restapi/controller/ValueSetControllerIntegrationTest.java b/src/test/java/eu/europa/ec/dgc/businessrule/restapi/controller/ValueSetControllerIntegrationTest.java index c134c59..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 @@ -20,7 +20,14 @@ 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; import eu.europa.ec.dgc.gateway.connector.DgcGatewayValidationRuleDownloadConnector; @@ -62,11 +69,19 @@ class ValueSetControllerIntegrationTest { @Autowired private MockMvc mockMvc; + @Autowired + private ListSigningService listSigningService; + @Autowired + private ValueSetService valueSetService; + + @Autowired + private SignedListRepository signedListRepository; @BeforeEach void clearRepositoryData() { valueSetRepository.deleteAll(); + signedListRepository.deleteAll(); } @Test @@ -95,6 +110,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)) 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 +