diff --git a/src/main/java/eu/europa/ec/dgc/gateway/connector/DgcGatewayConnectorUtils.java b/src/main/java/eu/europa/ec/dgc/gateway/connector/DgcGatewayConnectorUtils.java index dc8f8d0..84b44a5 100644 --- a/src/main/java/eu/europa/ec/dgc/gateway/connector/DgcGatewayConnectorUtils.java +++ b/src/main/java/eu/europa/ec/dgc/gateway/connector/DgcGatewayConnectorUtils.java @@ -20,26 +20,73 @@ package eu.europa.ec.dgc.gateway.connector; +import eu.europa.ec.dgc.gateway.connector.client.DgcGatewayConnectorRestClient; +import eu.europa.ec.dgc.gateway.connector.config.DgcGatewayConnectorConfigProperties; +import eu.europa.ec.dgc.gateway.connector.dto.CertificateTypeDto; import eu.europa.ec.dgc.gateway.connector.dto.TrustListItemDto; import eu.europa.ec.dgc.signing.SignedCertificateMessageParser; +import eu.europa.ec.dgc.utils.CertificateUtils; +import feign.FeignException; import java.io.IOException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.Security; +import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; import java.util.Base64; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import javax.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.bouncycastle.cert.CertException; import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.operator.ContentVerifierProvider; import org.bouncycastle.operator.OperatorCreationException; import org.bouncycastle.operator.RuntimeOperatorException; import org.bouncycastle.operator.jcajce.JcaContentVerifierProviderBuilder; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; @Service @Slf4j @ConditionalOnProperty("dgc.gateway.connector.enabled") +@RequiredArgsConstructor class DgcGatewayConnectorUtils { + private final CertificateUtils certificateUtils; + + private final DgcGatewayConnectorRestClient dgcGatewayConnectorRestClient; + + private final DgcGatewayConnectorConfigProperties properties; + + @Qualifier("trustAnchor") + private final KeyStore trustAnchorKeyStore; + + private X509CertificateHolder trustAnchor; + + + @PostConstruct + void init() throws KeyStoreException, CertificateEncodingException, IOException { + Security.addProvider(new BouncyCastleProvider()); + + String trustAnchorAlias = properties.getTrustAnchor().getAlias(); + X509Certificate trustAnchorCert = (X509Certificate) trustAnchorKeyStore.getCertificate(trustAnchorAlias); + + if (trustAnchorCert == null) { + log.error("Could not find TrustAnchor Certificate in Keystore"); + throw new KeyStoreException("Could not find TrustAnchor Certificate in Keystore"); + } + trustAnchor = certificateUtils.convertCertificate(trustAnchorCert); + } + public boolean trustListItemSignedByCa(TrustListItemDto certificate, X509CertificateHolder ca) { ContentVerifierProvider verifier; try { @@ -95,4 +142,39 @@ X509CertificateHolder getCertificateFromTrustListItem(TrustListItemDto trustList return null; } } + + public List fetchCertificatesAndVerifyByTrustAnchor(CertificateTypeDto type) { + ResponseEntity> downloadedCertificates; + try { + downloadedCertificates = dgcGatewayConnectorRestClient.getTrustedCertificates(type); + } catch (FeignException e) { + log.error("Failed to Download certificates from DGC Gateway. Type: {}, status code: {}", type, e.status()); + return Collections.emptyList(); + } + + if (downloadedCertificates.getStatusCode() != HttpStatus.OK || downloadedCertificates.getBody() == null) { + log.error("Failed to Download certificates from DGC Gateway, Type: {}, Status Code: {}", + type, downloadedCertificates.getStatusCodeValue()); + return Collections.emptyList(); + } + + return downloadedCertificates.getBody().stream() + .filter(this::checkThumbprintIntegrity) + .filter(c -> this.checkTrustAnchorSignature(c, trustAnchor)) + .map(this::getCertificateFromTrustListItem) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + private boolean checkThumbprintIntegrity(TrustListItemDto trustListItem) { + byte[] certificateRawData = Base64.getDecoder().decode(trustListItem.getRawData()); + try { + return trustListItem.getThumbprint().equals( + certificateUtils.getCertThumbprint(new X509CertificateHolder(certificateRawData))); + + } catch (IOException e) { + log.error("Could not parse certificate raw data"); + return false; + } + } } diff --git a/src/main/java/eu/europa/ec/dgc/gateway/connector/DgcGatewayCountryListDownloadConnector.java b/src/main/java/eu/europa/ec/dgc/gateway/connector/DgcGatewayCountryListDownloadConnector.java new file mode 100644 index 0000000..7b7e0d3 --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/connector/DgcGatewayCountryListDownloadConnector.java @@ -0,0 +1,121 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-lib + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.connector; + +import eu.europa.ec.dgc.gateway.connector.client.DgcGatewayConnectorRestClient; +import eu.europa.ec.dgc.gateway.connector.config.DgcGatewayConnectorConfigProperties; +import feign.FeignException; +import java.security.Security; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import javax.annotation.PostConstruct; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Lazy; +import org.springframework.context.annotation.Scope; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.stereotype.Service; + +@ConditionalOnProperty("dgc.gateway.connector.enabled") +@Lazy +@Service +@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON) +@RequiredArgsConstructor +@EnableScheduling +@Slf4j +public class DgcGatewayCountryListDownloadConnector { + + private final DgcGatewayConnectorRestClient dgcGatewayConnectorRestClient; + + private final DgcGatewayConnectorConfigProperties properties; + + @Getter + private LocalDateTime lastUpdated = null; + + private List countryList = new ArrayList<>(); + + @PostConstruct + void init() { + Security.addProvider(new BouncyCastleProvider()); + } + + /** + * Gets the list of downloaded Country Codes. + * This call will return a cached list if caching is enabled. + * If cache is outdated a refreshed list will be returned. + * + * @return List of {@link String} + */ + public List getCountryList() { + updateIfRequired(); + return Collections.unmodifiableList(countryList); + } + + private synchronized void updateIfRequired() { + if (lastUpdated == null + || ChronoUnit.SECONDS.between(lastUpdated, LocalDateTime.now()) >= properties.getMaxCacheAge()) { + log.info("Maximum age of cache reached. Fetching new CountryList from DGCG."); + + countryList = new ArrayList<>(); + fetchCountryList(); + log.info("CountryList contains {} country codes.", countryList.size()); + } else { + log.debug("Cache needs no refresh."); + } + } + + private void fetchCountryList() { + log.info("Fetching CountryList from DGCG"); + + ResponseEntity> responseEntity; + try { + responseEntity = dgcGatewayConnectorRestClient.downloadCountryList(); + } catch (FeignException e) { + log.error("Download of CountryList failed. DGCG responded with status code: {}", + e.status()); + return; + } + + List downloadedCountries = responseEntity.getBody(); + + if (responseEntity.getStatusCode() != HttpStatus.OK || downloadedCountries == null) { + log.error("Download of CountryList failed. DGCG responded with status code: {}", + responseEntity.getStatusCode()); + return; + } else { + log.info("Got Response from DGCG, Downloaded Countries: {}", downloadedCountries.size()); + } + + countryList = downloadedCountries; + + lastUpdated = LocalDateTime.now(); + log.info("Put {} country codes CountryList", countryList.size()); + } +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/connector/DgcGatewayDownloadConnector.java b/src/main/java/eu/europa/ec/dgc/gateway/connector/DgcGatewayDownloadConnector.java index 446d3be..7116481 100644 --- a/src/main/java/eu/europa/ec/dgc/gateway/connector/DgcGatewayDownloadConnector.java +++ b/src/main/java/eu/europa/ec/dgc/gateway/connector/DgcGatewayDownloadConnector.java @@ -27,21 +27,13 @@ import eu.europa.ec.dgc.gateway.connector.mapper.TrustListMapper; import eu.europa.ec.dgc.gateway.connector.model.TrustListItem; import eu.europa.ec.dgc.signing.SignedCertificateMessageParser; -import eu.europa.ec.dgc.utils.CertificateUtils; import feign.FeignException; -import java.io.IOException; -import java.security.KeyStore; -import java.security.KeyStoreException; import java.security.Security; -import java.security.cert.CertificateEncodingException; -import java.security.cert.X509Certificate; import java.time.LocalDateTime; import java.time.temporal.ChronoUnit; import java.util.ArrayList; -import java.util.Base64; import java.util.Collections; import java.util.List; -import java.util.Objects; import java.util.stream.Collectors; import javax.annotation.PostConstruct; import lombok.Getter; @@ -49,7 +41,6 @@ import lombok.extern.slf4j.Slf4j; import org.bouncycastle.cert.X509CertificateHolder; import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Lazy; @@ -70,19 +61,12 @@ public class DgcGatewayDownloadConnector { private final DgcGatewayConnectorUtils connectorUtils; - private final CertificateUtils certificateUtils; - private final DgcGatewayConnectorRestClient dgcGatewayConnectorRestClient; private final DgcGatewayConnectorConfigProperties properties; private final TrustListMapper trustListMapper; - @Qualifier("trustAnchor") - private final KeyStore trustAnchorKeyStore; - - private X509CertificateHolder trustAnchor; - @Getter private LocalDateTime lastUpdated = null; @@ -92,17 +76,8 @@ public class DgcGatewayDownloadConnector { private List trustedUploadCertificates = new ArrayList<>(); @PostConstruct - void init() throws KeyStoreException, CertificateEncodingException, IOException { + void init() { Security.addProvider(new BouncyCastleProvider()); - - String trustAnchorAlias = properties.getTrustAnchor().getAlias(); - X509Certificate trustAnchorCert = (X509Certificate) trustAnchorKeyStore.getCertificate(trustAnchorAlias); - - if (trustAnchorCert == null) { - log.error("Could not find TrustAnchor Certificate in Keystore"); - throw new KeyStoreException("Could not find TrustAnchor Certificate in Keystore"); - } - trustAnchor = certificateUtils.convertCertificate(trustAnchorCert); } /** @@ -122,10 +97,11 @@ private synchronized void updateIfRequired() { || ChronoUnit.SECONDS.between(lastUpdated, LocalDateTime.now()) >= properties.getMaxCacheAge()) { log.info("Maximum age of cache reached. Fetching new TrustList from DGCG."); - trustedCscaCertificates = fetchCertificatesAndVerifyByTrustAnchor(CertificateTypeDto.CSCA); + trustedCscaCertificates = connectorUtils.fetchCertificatesAndVerifyByTrustAnchor(CertificateTypeDto.CSCA); log.info("CSCA TrustStore contains {} trusted certificates.", trustedCscaCertificates.size()); - trustedUploadCertificates = fetchCertificatesAndVerifyByTrustAnchor(CertificateTypeDto.UPLOAD); + trustedUploadCertificates = + connectorUtils.fetchCertificatesAndVerifyByTrustAnchor(CertificateTypeDto.UPLOAD); log.info("Upload TrustStore contains {} trusted certificates.", trustedUploadCertificates.size()); fetchTrustListAndVerifyByCscaAndUpload(); @@ -135,29 +111,6 @@ private synchronized void updateIfRequired() { } } - private List fetchCertificatesAndVerifyByTrustAnchor(CertificateTypeDto type) { - ResponseEntity> downloadedCertificates; - try { - downloadedCertificates = dgcGatewayConnectorRestClient.getTrustedCertificates(type); - } catch (FeignException e) { - log.error("Failed to Download certificates from DGC Gateway. Type: {}, status code: {}", type, e.status()); - return Collections.emptyList(); - } - - if (downloadedCertificates.getStatusCode() != HttpStatus.OK || downloadedCertificates.getBody() == null) { - log.error("Failed to Download certificates from DGC Gateway, Type: {}, Status Code: {}", - type, downloadedCertificates.getStatusCodeValue()); - return Collections.emptyList(); - } - - return downloadedCertificates.getBody().stream() - .filter(this::checkThumbprintIntegrity) - .filter(c -> connectorUtils.checkTrustAnchorSignature(c, trustAnchor)) - .map(connectorUtils::getCertificateFromTrustListItem) - .filter(Objects::nonNull) - .collect(Collectors.toList()); - } - private void fetchTrustListAndVerifyByCscaAndUpload() { log.info("Fetching TrustList from DGCG"); @@ -190,18 +143,6 @@ private void fetchTrustListAndVerifyByCscaAndUpload() { log.info("Put {} trusted certificates into TrustList", trustedCertificates.size()); } - private boolean checkThumbprintIntegrity(TrustListItemDto trustListItem) { - byte[] certificateRawData = Base64.getDecoder().decode(trustListItem.getRawData()); - try { - return trustListItem.getThumbprint().equals( - certificateUtils.getCertThumbprint(new X509CertificateHolder(certificateRawData))); - - } catch (IOException e) { - log.error("Could not parse certificate raw data"); - return false; - } - } - private boolean checkCscaCertificate(TrustListItemDto trustListItem) { boolean result = trustedCscaCertificates .stream() diff --git a/src/main/java/eu/europa/ec/dgc/gateway/connector/DgcGatewayUploadConnector.java b/src/main/java/eu/europa/ec/dgc/gateway/connector/DgcGatewayUploadConnector.java index 8f760b3..e1cf0b6 100644 --- a/src/main/java/eu/europa/ec/dgc/gateway/connector/DgcGatewayUploadConnector.java +++ b/src/main/java/eu/europa/ec/dgc/gateway/connector/DgcGatewayUploadConnector.java @@ -121,7 +121,7 @@ public void uploadTrustedCertificate(X509Certificate certificate) throws DgcCert public void uploadTrustedCertificate(X509CertificateHolder certificate) throws DgcCertificateUploadException { String payload = new SignedCertificateMessageBuilder() - .withPayloadCertificate(certificate) + .withPayload(certificate) .withSigningCertificate(uploadCertificateHolder, uploadCertificatePrivateKey) .buildAsString(); @@ -169,7 +169,7 @@ public void deleteTrustedCertificate(X509Certificate certificate) throws DgcCert public void deleteTrustedCertificate(X509CertificateHolder certificate) throws DgcCertificateUploadException { String payload = new SignedCertificateMessageBuilder() - .withPayloadCertificate(certificate) + .withPayload(certificate) .withSigningCertificate(uploadCertificateHolder, uploadCertificatePrivateKey) .buildAsString(); diff --git a/src/main/java/eu/europa/ec/dgc/gateway/connector/DgcGatewayValidationRuleDownloadConnector.java b/src/main/java/eu/europa/ec/dgc/gateway/connector/DgcGatewayValidationRuleDownloadConnector.java new file mode 100644 index 0000000..f3343c8 --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/connector/DgcGatewayValidationRuleDownloadConnector.java @@ -0,0 +1,181 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-lib + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.connector; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import eu.europa.ec.dgc.gateway.connector.client.DgcGatewayConnectorRestClient; +import eu.europa.ec.dgc.gateway.connector.config.DgcGatewayConnectorConfigProperties; +import eu.europa.ec.dgc.gateway.connector.dto.CertificateTypeDto; +import eu.europa.ec.dgc.gateway.connector.dto.ValidationRuleDto; +import eu.europa.ec.dgc.gateway.connector.model.ValidationRule; +import eu.europa.ec.dgc.gateway.connector.model.ValidationRulesByCountry; +import eu.europa.ec.dgc.signing.SignedMessageParser; +import eu.europa.ec.dgc.signing.SignedStringMessageParser; +import feign.FeignException; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.bouncycastle.cert.X509CertificateHolder; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.ldap.embedded.EmbeddedLdapProperties; +import org.springframework.context.annotation.Lazy; +import org.springframework.context.annotation.Scope; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.stereotype.Service; + +@ConditionalOnProperty("dgc.gateway.connector.enabled") +@Lazy +@Service +@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON) +@RequiredArgsConstructor +@EnableScheduling +@Slf4j +public class DgcGatewayValidationRuleDownloadConnector { + + private final DgcGatewayConnectorUtils connectorUtils; + + private final DgcGatewayConnectorRestClient dgcGatewayConnectorRestClient; + + private final DgcGatewayConnectorConfigProperties properties; + + private final DgcGatewayCountryListDownloadConnector countryListDownloadConnector; + + private final ObjectMapper objectMapper; + + @Getter + private LocalDateTime lastUpdated = null; + + private ValidationRulesByCountry validationRules = new ValidationRulesByCountry(); + + private List trustedUploadCertificates = new ArrayList<>(); + + /** + * Gets the list of downloaded and validated validation rules. + * This call will return a cached list if caching is enabled. + * If cache is outdated a refreshed list will be returned. + * + * @return {@link ValidationRulesByCountry} + */ + public ValidationRulesByCountry getValidationRules() { + updateIfRequired(); + return validationRules; + } + + private synchronized void updateIfRequired() { + if (lastUpdated == null + || ChronoUnit.SECONDS.between(lastUpdated, LocalDateTime.now()) >= properties.getMaxCacheAge()) { + log.info("Maximum age of cache reached. Fetching new TrustList from DGCG."); + + validationRules = new ValidationRulesByCountry(); + + trustedUploadCertificates = + connectorUtils.fetchCertificatesAndVerifyByTrustAnchor(CertificateTypeDto.UPLOAD); + log.info("Upload TrustStore contains {} trusted certificates.", trustedUploadCertificates.size()); + + List countryCodes = countryListDownloadConnector.getCountryList(); + log.info("Downloaded Countrylist"); + + countryCodes.forEach(this::fetchValidationRulesAndVerify); + log.info("ValidationRule Cache contains {} ValidationRules.", validationRules.size()); + } else { + log.debug("Cache needs no refresh."); + } + } + + private void fetchValidationRulesAndVerify(String countryCode) { + log.info("Fetching ValidationRules from DGCG for Country {}", countryCode); + + ResponseEntity>> responseEntity; + try { + responseEntity = dgcGatewayConnectorRestClient.downloadValidationRule(countryCode); + } catch (FeignException e) { + log.error("Download of ValidationRules for country {} failed. DGCG responded with status code: {}", + countryCode, e.status()); + return; + } + + Map> downloadedValidationRules = responseEntity.getBody(); + + if (responseEntity.getStatusCode() != HttpStatus.OK || downloadedValidationRules == null) { + log.error("Download of ValidationRules for country {} failed. DGCG responded with status code: {}", + countryCode, responseEntity.getStatusCode()); + return; + } else { + log.info("Got Response from DGCG, Downloaded ValidationRules: {}", downloadedValidationRules.size()); + } + + downloadedValidationRules.values().stream() + .flatMap(Collection::stream) + .filter(this::checkCmsSignature) + .filter(this::checkUploadCertificate) + .map(this::map) + .filter(Objects::nonNull) + .forEach(rule -> validationRules.set(countryCode, rule.getIdentifier(), rule.getVersion(), rule)); + + lastUpdated = LocalDateTime.now(); + } + + private boolean checkCmsSignature(ValidationRuleDto validationRuleDto) { + SignedStringMessageParser parser = + new SignedStringMessageParser(validationRuleDto.getCms()); + + return parser.getParserState() == SignedMessageParser.ParserState.SUCCESS && parser.isSignatureVerified(); + } + + private ValidationRule map(ValidationRuleDto dto) { + SignedStringMessageParser parser = + new SignedStringMessageParser(dto.getCms()); + + try { + ValidationRule parsedRule = objectMapper.readValue(parser.getPayload(), ValidationRule.class); + parsedRule.setRawJson(parser.getPayload()); + return parsedRule; + } catch (JsonProcessingException e) { + log.error("Failed to parse Validation Rule JSON: {}", e.getMessage()); + return null; + } + } + + private boolean checkUploadCertificate(ValidationRuleDto validationRule) { + SignedStringMessageParser parser = + new SignedStringMessageParser(validationRule.getCms()); + X509CertificateHolder uploadCertificate = parser.getSigningCertificate(); + + if (uploadCertificate == null) { + return false; + } + + return trustedUploadCertificates + .stream() + .anyMatch(uploadCertificate::equals); + } +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/connector/DgcGatewayValueSetDownloadConnector.java b/src/main/java/eu/europa/ec/dgc/gateway/connector/DgcGatewayValueSetDownloadConnector.java new file mode 100644 index 0000000..90d946a --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/connector/DgcGatewayValueSetDownloadConnector.java @@ -0,0 +1,143 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-lib + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.connector; + +import eu.europa.ec.dgc.gateway.connector.client.DgcGatewayConnectorRestClient; +import eu.europa.ec.dgc.gateway.connector.config.DgcGatewayConnectorConfigProperties; +import eu.europa.ec.dgc.gateway.connector.model.ValidationRulesByCountry; +import feign.FeignException; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Lazy; +import org.springframework.context.annotation.Scope; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.stereotype.Service; + +@ConditionalOnProperty("dgc.gateway.connector.enabled") +@Lazy +@Service +@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON) +@RequiredArgsConstructor +@EnableScheduling +@Slf4j +public class DgcGatewayValueSetDownloadConnector { + + private final DgcGatewayConnectorRestClient dgcGatewayConnectorRestClient; + + private final DgcGatewayConnectorConfigProperties properties; + + @Getter + private LocalDateTime lastUpdated = null; + + private final Map valueSets = new HashMap<>(); + + /** + * Gets the list of downloaded ValueSets. + * Map Containing Key ValueSetId and Value is the JSON String + * This call will return a cached list if caching is enabled. + * If cache is outdated a refreshed list will be returned. + * + * @return {@link ValidationRulesByCountry} + */ + public Map getValueSets() { + updateIfRequired(); + return valueSets; + } + + private synchronized void updateIfRequired() { + if (lastUpdated == null + || ChronoUnit.SECONDS.between(lastUpdated, LocalDateTime.now()) >= properties.getMaxCacheAge()) { + log.info("Maximum age of cache reached. Fetching new ValueSets from DGCG."); + + valueSets.clear(); + + List valueSetIds = fetchValueSetIds(); + log.info("Got List of ValueSet Ids"); + + valueSetIds.forEach(this::fetchValueSet); + log.info("ValueSet Cache contains {} ValueSets.", valueSets.size()); + } else { + log.debug("Cache needs no refresh."); + } + } + + private List fetchValueSetIds() { + log.info("Fetching ValueSet IDs from DGCG"); + + ResponseEntity> responseEntity; + try { + responseEntity = dgcGatewayConnectorRestClient.downloadValueSetIds(); + } catch (FeignException e) { + log.error("Download of ValueSet IDs failed. DGCG responded with status code: {}", + e.status()); + return Collections.emptyList(); + } + + List valueSetIds = responseEntity.getBody(); + + if (responseEntity.getStatusCode() != HttpStatus.OK || valueSetIds == null) { + log.error("Download of ValueSet IDs failed. DGCG responded with status code: {}", + responseEntity.getStatusCode()); + return Collections.emptyList(); + } else { + log.info("Got Response from DGCG, downloaded {} ValueSet IDs.", valueSetIds.size()); + } + + return valueSetIds; + } + + private void fetchValueSet(String id) { + log.info("Fetching ValueSet from DGCG with Id {}", id); + + ResponseEntity responseEntity; + try { + responseEntity = dgcGatewayConnectorRestClient.downloadValueSet(id); + } catch (FeignException e) { + log.error("Download of ValueSet with ID {} failed. DGCG responded with status code: {}", + id, e.status()); + return; + } + + String valueSet = responseEntity.getBody(); + + if (responseEntity.getStatusCode() != HttpStatus.OK || valueSet == null) { + log.error("Download of ValueSet with ID {} failed. DGCG responded with status code: {}", + id, responseEntity.getStatusCode()); + return; + } else { + log.info("Got Response from DGCG, ValueSet downloaded."); + } + + valueSets.put(id, valueSet); + lastUpdated = LocalDateTime.now(); + } +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/connector/client/DgcGatewayConnectorRestClient.java b/src/main/java/eu/europa/ec/dgc/gateway/connector/client/DgcGatewayConnectorRestClient.java index 7a72ba1..c4423e5 100644 --- a/src/main/java/eu/europa/ec/dgc/gateway/connector/client/DgcGatewayConnectorRestClient.java +++ b/src/main/java/eu/europa/ec/dgc/gateway/connector/client/DgcGatewayConnectorRestClient.java @@ -22,7 +22,9 @@ import eu.europa.ec.dgc.gateway.connector.dto.CertificateTypeDto; import eu.europa.ec.dgc.gateway.connector.dto.TrustListItemDto; +import eu.europa.ec.dgc.gateway.connector.dto.ValidationRuleDto; import java.util.List; +import java.util.Map; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.http.MediaType; @@ -68,4 +70,53 @@ public interface DgcGatewayConnectorRestClient { @DeleteMapping(value = "/signerCertificate", consumes = "application/cms") ResponseEntity deleteSignerInformation(@RequestBody String cmsSignedCertificate); + /** + * Downloads the Countrylist from digital green certificate gateway. + * + * @return List of Strings (2 Digit Country Codes) + */ + @GetMapping(value = "/countrylist", produces = MediaType.APPLICATION_JSON_VALUE) + ResponseEntity> downloadCountryList(); + + /** + * Downloads the list of available ValueSets from digital green certificate gateway. + * + * @return List of Strings + */ + @GetMapping(value = "/valuesets", produces = MediaType.APPLICATION_JSON_VALUE) + ResponseEntity> downloadValueSetIds(); + + /** + * Downloads a ValueSet by its id from digital green certificate gateway. + * + * @return the JSON Representation of the ValueSet. + */ + @GetMapping(value = "/valuesets/{id}", produces = MediaType.APPLICATION_JSON_VALUE) + ResponseEntity downloadValueSet(@PathVariable("id") String id); + + /** + * Uploads a new ValidationRule to digital green certificate gateway. + * + * @param validationRule the CMS signed ValidationRule JSON. + */ + @PostMapping(value = "/rules", consumes = "application/cms-text") + ResponseEntity uploadValidationRule(@RequestBody String validationRule); + + /** + * Deletes a ValidationRule from digital green certificate gateway. + * + * @param validationRuleId the CMS signed ValidationRule Identifier. + */ + @DeleteMapping(value = "/rules", consumes = "application/cms-text") + ResponseEntity deleteValidationRule(@RequestBody String validationRuleId); + + /** + * Downloads a Validation Rule from digital green certificate gateway. + * + * @param countryCode of the Validation Rule + * @return JSON Structure containing relevant versions of Validation Rule + */ + @GetMapping(value = "/rules/{cc}", produces = MediaType.APPLICATION_JSON_VALUE) + ResponseEntity>> downloadValidationRule(@PathVariable("cc") String countryCode); + } diff --git a/src/main/java/eu/europa/ec/dgc/gateway/connector/config/ObjectMapperConfig.java b/src/main/java/eu/europa/ec/dgc/gateway/connector/config/ObjectMapperConfig.java new file mode 100644 index 0000000..5e6267a --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/connector/config/ObjectMapperConfig.java @@ -0,0 +1,36 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-lib + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.connector.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class ObjectMapperConfig { + + @Bean + ObjectMapper objectMapper() { + return new ObjectMapper() + .registerModule(new JavaTimeModule()); + } +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/connector/dto/ValidationRuleDto.java b/src/main/java/eu/europa/ec/dgc/gateway/connector/dto/ValidationRuleDto.java new file mode 100644 index 0000000..d6c1319 --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/connector/dto/ValidationRuleDto.java @@ -0,0 +1,39 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-lib + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.connector.dto; + +import java.time.ZonedDateTime; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class ValidationRuleDto { + + String version; + + ZonedDateTime validFrom; + + ZonedDateTime validTo; + + String cms; + +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/connector/model/ValidationRule.java b/src/main/java/eu/europa/ec/dgc/gateway/connector/model/ValidationRule.java new file mode 100644 index 0000000..4c82611 --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/connector/model/ValidationRule.java @@ -0,0 +1,91 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-lib + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.connector.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; +import java.time.ZonedDateTime; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +public class ValidationRule { + + @JsonProperty("Identifier") + String identifier; + + @JsonProperty("Type") + String type; + + @JsonProperty("Country") + String country; + + @JsonProperty("Region") + String region; + + @JsonProperty("Version") + String version; + + @JsonProperty("SchemaVersion") + String schemaVersion; + + @JsonProperty("Engine") + String engine; + + @JsonProperty("EngineVersion") + String engineVersion; + + @JsonProperty("CertificateType") + String certificateType; + + @JsonProperty("Description") + List description; + + @JsonProperty("ValidFrom") + ZonedDateTime validFrom; + + @JsonProperty("ValidTo") + ZonedDateTime validTo; + + @JsonProperty("AffectedFields") + List affectedFields; + + @JsonProperty("Logic") + JsonNode logic; + + @JsonIgnore + String rawJson; + + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class DescriptionItem { + + @JsonProperty("lang") + String language; + + @JsonProperty("desc") + String description; + } +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/connector/model/ValidationRulesByCountry.java b/src/main/java/eu/europa/ec/dgc/gateway/connector/model/ValidationRulesByCountry.java new file mode 100644 index 0000000..22bce5c --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/connector/model/ValidationRulesByCountry.java @@ -0,0 +1,124 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-lib + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.connector.model; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import lombok.Getter; + +public class ValidationRulesByCountry { + + @Getter + private final Map map = new HashMap<>(); + + /** + * Sets a ValidationRule Entry in the ValidationRuleSet. + * + * @param country Country Code + * @param id ValidationRule Identifier + * @param version Version of Validation Rule + * @param rule The Rule to be added. + */ + public void set(String country, String id, String version, ValidationRule rule) { + map + .computeIfAbsent(country, s -> new ValidationRulesByIdentifier()) + .getMap() + .computeIfAbsent(id, s -> new ValidationRulesByVersion()) + .getMap().put(version, rule); + } + + /** + * Gets a ValidationRule from the map. + * + * @param country Country Code + * @param id ValidationRule Identifier + * @param version Version of Validation Rule + * @return the ValidationRule or null if it does not exist. + */ + public ValidationRule get(String country, String id, String version) { + return map + .getOrDefault(country, new ValidationRulesByIdentifier()) + .getMap() + .getOrDefault(id, new ValidationRulesByVersion()) + .getMap().getOrDefault(version, null); + } + + /** + * Returns a pure Map-structure of this data type. + * This should be used when serializing this Object into JSON. + */ + public Map>> pure() { + return map.entrySet().stream() + .map(e -> Map.entry(e.getKey(), e.getValue().getMap().entrySet().stream() + .map(ee -> Map.entry(ee.getKey(), ee.getValue().getMap())) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)))) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + /** + * Gets a flat list of all Validation Rules. + * + * @return List of {@link ValidationRule} + */ + public List flat() { + List allRules = new ArrayList<>(); + + for (ValidationRulesByIdentifier v : map.values()) { + for (ValidationRulesByVersion vv : v.getMap().values()) { + allRules.addAll(vv.getMap().values()); + } + } + + return allRules; + } + + /** + * Gets the amount of all ValidationRules within this structure. + * + * @return amount of ValidationRules. + */ + public int size() { + int count = 0; + + for (ValidationRulesByIdentifier v : map.values()) { + for (ValidationRulesByVersion vv : v.getMap().values()) { + count += vv.getMap().size(); + } + } + + return count; + } + + public static class ValidationRulesByIdentifier { + + @Getter + private final Map map = new HashMap<>(); + } + + public static class ValidationRulesByVersion { + + @Getter + private final Map map = new HashMap<>(); + } +} diff --git a/src/test/java/eu/europa/ec/dgc/gateway/connector/CountryListDownloadConnectorTest.java b/src/test/java/eu/europa/ec/dgc/gateway/connector/CountryListDownloadConnectorTest.java new file mode 100644 index 0000000..0241e5a --- /dev/null +++ b/src/test/java/eu/europa/ec/dgc/gateway/connector/CountryListDownloadConnectorTest.java @@ -0,0 +1,84 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-lib + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.connector; + +import eu.europa.ec.dgc.gateway.connector.client.DgcGatewayConnectorRestClient; +import eu.europa.ec.dgc.testdata.DgcTestKeyStore; +import eu.europa.ec.dgc.utils.CertificateUtils; +import feign.FeignException; +import feign.Request; +import feign.RequestTemplate; +import java.util.HashMap; +import java.util.List; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.when; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.ResponseEntity; + +@SpringBootTest +@Slf4j +class CountryListDownloadConnectorTest { + + @MockBean + DgcGatewayConnectorRestClient restClientMock; + + @Autowired + DgcGatewayCountryListDownloadConnector connector; + + @Test + void testDownloadOfCountryList() { + + when(restClientMock.downloadCountryList()) + .thenReturn(ResponseEntity.ok(List.of("EU", "DE"))); + + List result = connector.getCountryList(); + Assertions.assertEquals(2, result.size()); + Assertions.assertEquals("EU", result.get(0)); + Assertions.assertEquals("DE", result.get(1)); + Assertions.assertNotNull(connector.getLastUpdated()); + } + + @Test + void shouldReturnEmptyListWhenDownloadFails() { + when(restClientMock.downloadCountryList()) + .thenReturn(ResponseEntity.status(500).build()); + + Assertions.assertEquals(0, connector.getCountryList().size()); + + doThrow(new FeignException.InternalServerError("", dummyRequest(), null)) + .when(restClientMock).downloadCountryList(); + + Assertions.assertEquals(0, connector.getCountryList().size()); + } + + /** + * Method to create dummy request which is required to throw FeignExceptions. + */ + private Request dummyRequest() { + return Request.create(Request.HttpMethod.GET, "url", new HashMap<>(), null, new RequestTemplate()); + } + +} diff --git a/src/test/java/eu/europa/ec/dgc/gateway/connector/DownloadConnectorTest.java b/src/test/java/eu/europa/ec/dgc/gateway/connector/DownloadConnectorTest.java index 4e75b5d..b1ff31d 100644 --- a/src/test/java/eu/europa/ec/dgc/gateway/connector/DownloadConnectorTest.java +++ b/src/test/java/eu/europa/ec/dgc/gateway/connector/DownloadConnectorTest.java @@ -33,7 +33,6 @@ import feign.RequestTemplate; import java.security.KeyPair; import java.security.KeyPairGenerator; -import java.security.KeyStoreException; import java.security.cert.X509Certificate; import java.time.ZonedDateTime; import java.util.Base64; @@ -424,17 +423,6 @@ void testDownloadOfCertificatesShouldFailWrongTrustAnchorSignatureForUpload() th Assertions.assertNotNull(connector.getLastUpdated()); } - @Test - void shouldThrowExceptionOnInitWhenNoTrustAnchorIsPresent() { - - X509Certificate trustAnchorBackup = testKeyStore.getTrustAnchor(); - testKeyStore.setTrustAnchor(null); - - Assertions.assertThrows(KeyStoreException.class, connector::init); - - testKeyStore.setTrustAnchor(trustAnchorBackup); - } - @Test void shouldReturnEmptyListWhenCscaDownloadFails() { when(restClientMock.getTrustedCertificates(CertificateTypeDto.CSCA)) diff --git a/src/test/java/eu/europa/ec/dgc/gateway/connector/DownloadConnectorUtilsTest.java b/src/test/java/eu/europa/ec/dgc/gateway/connector/DownloadConnectorUtilsTest.java new file mode 100644 index 0000000..00550cf --- /dev/null +++ b/src/test/java/eu/europa/ec/dgc/gateway/connector/DownloadConnectorUtilsTest.java @@ -0,0 +1,198 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-lib + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.connector; + +import eu.europa.ec.dgc.gateway.connector.client.DgcGatewayConnectorRestClient; +import eu.europa.ec.dgc.gateway.connector.dto.CertificateTypeDto; +import eu.europa.ec.dgc.gateway.connector.dto.TrustListItemDto; +import eu.europa.ec.dgc.signing.SignedCertificateMessageBuilder; +import eu.europa.ec.dgc.testdata.CertificateTestUtils; +import eu.europa.ec.dgc.testdata.DgcTestKeyStore; +import eu.europa.ec.dgc.utils.CertificateUtils; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.KeyStoreException; +import java.security.cert.X509Certificate; +import java.time.ZonedDateTime; +import java.util.Base64; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; + +@SpringBootTest +@Slf4j +class DownloadConnectorUtilsTest { + + @MockBean + DgcGatewayConnectorRestClient restClientMock; + + @Autowired + DgcGatewayConnectorUtils connectorUtils; + + @Autowired + DgcTestKeyStore testKeyStore; + + @Autowired + CertificateUtils certificateUtils; + + @Test + void shouldThrowExceptionOnInitWhenNoTrustAnchorIsPresent() { + + X509Certificate trustAnchorBackup = testKeyStore.getTrustAnchor(); + testKeyStore.setTrustAnchor(null); + + Assertions.assertThrows(KeyStoreException.class, connectorUtils::init); + + testKeyStore.setTrustAnchor(trustAnchorBackup); + } + + @Test + void testTrustListItemSignedByCa() throws Exception { + + KeyPair keyPair = KeyPairGenerator.getInstance("ec").generateKeyPair(); + X509Certificate csca = CertificateTestUtils.generateCertificate(keyPair, "EU", "CSCA"); + + KeyPair keyPairDsc = KeyPairGenerator.getInstance("ec").generateKeyPair(); + X509Certificate dsc = CertificateTestUtils.generateCertificate(keyPairDsc, "EU", "DSC", csca, keyPair.getPrivate()); + + KeyPair keyPairUpload = KeyPairGenerator.getInstance("ec").generateKeyPair(); + X509Certificate upload = CertificateTestUtils.generateCertificate(keyPairUpload, "EU", "UPLOAD"); + + String dscSignature = new SignedCertificateMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(upload), keyPairUpload.getPrivate()) + .withPayload(certificateUtils.convertCertificate(dsc)) + .buildAsString(true); + + TrustListItemDto dscTrustListItem = new TrustListItemDto(); + dscTrustListItem.setCountry("EU"); + dscTrustListItem.setKid("KID_EU_DSC"); + dscTrustListItem.setCertificateType(CertificateTypeDto.DSC); + dscTrustListItem.setTimestamp(ZonedDateTime.now()); + dscTrustListItem.setSignature(dscSignature); + dscTrustListItem.setThumbprint(certificateUtils.getCertThumbprint(dsc)); + dscTrustListItem.setRawData(Base64.getEncoder().encodeToString(dsc.getEncoded())); + + Assertions.assertTrue(connectorUtils.trustListItemSignedByCa(dscTrustListItem, certificateUtils.convertCertificate(csca))); + } + + @Test + void testTrustListItemSignedByCaFailed() throws Exception { + + KeyPair keyPair = KeyPairGenerator.getInstance("ec").generateKeyPair(); + X509Certificate csca = CertificateTestUtils.generateCertificate(keyPair, "EU", "CSCA"); + + KeyPair keyPairUpload = KeyPairGenerator.getInstance("ec").generateKeyPair(); + X509Certificate upload = CertificateTestUtils.generateCertificate(keyPairUpload, "EU", "UPLOAD"); + + // Sign DSC with Upload certificate + KeyPair keyPairDsc = KeyPairGenerator.getInstance("ec").generateKeyPair(); + X509Certificate dsc = CertificateTestUtils.generateCertificate(keyPairDsc, "EU", "DSC", upload, keyPairUpload.getPrivate()); + + String dscSignature = new SignedCertificateMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(upload), keyPairUpload.getPrivate()) + .withPayload(certificateUtils.convertCertificate(dsc)) + .buildAsString(true); + + TrustListItemDto dscTrustListItem = new TrustListItemDto(); + dscTrustListItem.setCountry("EU"); + dscTrustListItem.setKid("KID_EU_DSC"); + dscTrustListItem.setCertificateType(CertificateTypeDto.DSC); + dscTrustListItem.setTimestamp(ZonedDateTime.now()); + dscTrustListItem.setSignature(dscSignature); + dscTrustListItem.setThumbprint(certificateUtils.getCertThumbprint(dsc)); + dscTrustListItem.setRawData(Base64.getEncoder().encodeToString(dsc.getEncoded())); + + Assertions.assertFalse(connectorUtils.trustListItemSignedByCa(dscTrustListItem, certificateUtils.convertCertificate(csca))); + } + + @Test + void testTrustListItemSignedByTrustAnchor() throws Exception { + + KeyPair keyPair = KeyPairGenerator.getInstance("ec").generateKeyPair(); + X509Certificate csca = CertificateTestUtils.generateCertificate(keyPair, "EU", "CSCA"); + + String cscaSignature = new SignedCertificateMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(testKeyStore.getTrustAnchor()), testKeyStore.getTrustAnchorPrivateKey()) + .withPayload(certificateUtils.convertCertificate(csca)) + .buildAsString(true); + + TrustListItemDto cscaTrustListItem = new TrustListItemDto(); + cscaTrustListItem.setCountry("EU"); + cscaTrustListItem.setKid("KID_EU"); + cscaTrustListItem.setCertificateType(CertificateTypeDto.CSCA); + cscaTrustListItem.setTimestamp(ZonedDateTime.now()); + cscaTrustListItem.setSignature(cscaSignature); + cscaTrustListItem.setThumbprint(certificateUtils.getCertThumbprint(csca)); + cscaTrustListItem.setRawData(Base64.getEncoder().encodeToString(csca.getEncoded())); + + Assertions.assertTrue(connectorUtils.checkTrustAnchorSignature(cscaTrustListItem, certificateUtils.convertCertificate(testKeyStore.getTrustAnchor()))); + } + + @Test + void testTrustListItemSignedByTrustAnchorFailed() throws Exception { + + KeyPair keyPair = KeyPairGenerator.getInstance("ec").generateKeyPair(); + X509Certificate csca = CertificateTestUtils.generateCertificate(keyPair, "EU", "CSCA"); + + String cscaSignature = new SignedCertificateMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(csca), keyPair.getPrivate()) + .withPayload(certificateUtils.convertCertificate(csca)) + .buildAsString(true); + + TrustListItemDto cscaTrustListItem = new TrustListItemDto(); + cscaTrustListItem.setCountry("EU"); + cscaTrustListItem.setKid("KID_EU"); + cscaTrustListItem.setCertificateType(CertificateTypeDto.CSCA); + cscaTrustListItem.setTimestamp(ZonedDateTime.now()); + cscaTrustListItem.setSignature(cscaSignature); + cscaTrustListItem.setThumbprint(certificateUtils.getCertThumbprint(csca)); + cscaTrustListItem.setRawData(Base64.getEncoder().encodeToString(csca.getEncoded())); + + Assertions.assertFalse(connectorUtils.checkTrustAnchorSignature(cscaTrustListItem, certificateUtils.convertCertificate(testKeyStore.getTrustAnchor()))); + } + + @Test + void testGetCertificateFromTrustListItem() throws Exception { + + KeyPair keyPair = KeyPairGenerator.getInstance("ec").generateKeyPair(); + X509Certificate csca = CertificateTestUtils.generateCertificate(keyPair, "EU", "CSCA"); + + String cscaSignature = new SignedCertificateMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(csca), keyPair.getPrivate()) + .withPayload(certificateUtils.convertCertificate(csca)) + .buildAsString(true); + + TrustListItemDto cscaTrustListItem = new TrustListItemDto(); + cscaTrustListItem.setCountry("EU"); + cscaTrustListItem.setKid("KID_EU"); + cscaTrustListItem.setCertificateType(CertificateTypeDto.CSCA); + cscaTrustListItem.setTimestamp(ZonedDateTime.now()); + cscaTrustListItem.setSignature(cscaSignature); + cscaTrustListItem.setThumbprint(certificateUtils.getCertThumbprint(csca)); + cscaTrustListItem.setRawData(Base64.getEncoder().encodeToString(csca.getEncoded())); + + Assertions.assertEquals(certificateUtils.convertCertificate(csca), connectorUtils.getCertificateFromTrustListItem(cscaTrustListItem)); + } + +} diff --git a/src/test/java/eu/europa/ec/dgc/gateway/connector/ValidationRuleDownloadConnectorTest.java b/src/test/java/eu/europa/ec/dgc/gateway/connector/ValidationRuleDownloadConnectorTest.java new file mode 100644 index 0000000..4198189 --- /dev/null +++ b/src/test/java/eu/europa/ec/dgc/gateway/connector/ValidationRuleDownloadConnectorTest.java @@ -0,0 +1,485 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-lib + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.connector; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import eu.europa.ec.dgc.gateway.connector.client.DgcGatewayConnectorRestClient; +import eu.europa.ec.dgc.gateway.connector.dto.CertificateTypeDto; +import eu.europa.ec.dgc.gateway.connector.dto.TrustListItemDto; +import eu.europa.ec.dgc.gateway.connector.dto.ValidationRuleDto; +import eu.europa.ec.dgc.gateway.connector.model.TrustListItem; +import eu.europa.ec.dgc.gateway.connector.model.ValidationRule; +import eu.europa.ec.dgc.gateway.connector.model.ValidationRulesByCountry; +import eu.europa.ec.dgc.signing.SignedCertificateMessageBuilder; +import eu.europa.ec.dgc.signing.SignedStringMessageBuilder; +import eu.europa.ec.dgc.testdata.CertificateTestUtils; +import eu.europa.ec.dgc.testdata.DgcTestKeyStore; +import eu.europa.ec.dgc.utils.CertificateUtils; +import feign.FeignException; +import feign.Request; +import feign.RequestTemplate; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.cert.X509Certificate; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; +import java.util.Base64; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.when; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.ResponseEntity; + +@SpringBootTest +@Slf4j +class ValidationRuleDownloadConnectorTest { + + @MockBean + DgcGatewayConnectorRestClient restClientMock; + + @Autowired + DgcGatewayValidationRuleDownloadConnector connector; + + @Autowired + CertificateUtils certificateUtils; + + @Autowired + DgcTestKeyStore testKeyStore; + + @Autowired + ObjectMapper objectMapper; + + ValidationRule validationRule; + + @BeforeEach + void setup() { + validationRule = new ValidationRule(); + validationRule.setCountry("EU"); + validationRule.setIdentifier("IR-EU-0001"); + validationRule.setType("Invalidation"); + validationRule.setRegion("BW"); + validationRule.setVersion("1.0.0"); + validationRule.setSchemaVersion("1.0.0"); + validationRule.setEngine("CERTLOGIC"); + validationRule.setEngine("1.0.0"); + validationRule.setCertificateType("Vaccination"); + validationRule.setDescription(List.of(new ValidationRule.DescriptionItem("en", "ab".repeat(10)))); + validationRule.setValidFrom(ZonedDateTime.now().plus(1, ChronoUnit.DAYS)); + validationRule.setValidTo(ZonedDateTime.now().plus(3, ChronoUnit.DAYS)); + validationRule.setAffectedFields(List.of("aa", "bb", "cc")); + validationRule.setLogic(JsonNodeFactory.instance.objectNode()); + } + + @Test + void testDownloadOfValidationRules() throws Exception { + + KeyPair keyPairUpload = KeyPairGenerator.getInstance("ec").generateKeyPair(); + X509Certificate upload = CertificateTestUtils.generateCertificate(keyPairUpload, "EU", "UPLOAD"); + + String uploadSignature = new SignedCertificateMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(testKeyStore.getTrustAnchor()), testKeyStore.getTrustAnchorPrivateKey()) + .withPayload(certificateUtils.convertCertificate(upload)) + .buildAsString(true); + + TrustListItemDto uploadTrustListItem = new TrustListItemDto(); + uploadTrustListItem.setCountry("EU"); + uploadTrustListItem.setKid("KID_EU_UPLOAD"); + uploadTrustListItem.setCertificateType(CertificateTypeDto.UPLOAD); + uploadTrustListItem.setTimestamp(ZonedDateTime.now()); + uploadTrustListItem.setSignature(uploadSignature); + uploadTrustListItem.setThumbprint(certificateUtils.getCertThumbprint(upload)); + uploadTrustListItem.setRawData(Base64.getEncoder().encodeToString(upload.getEncoded())); + + String ruleSignature = new SignedStringMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(upload), keyPairUpload.getPrivate()) + .withPayload(objectMapper.writeValueAsString(validationRule)) + .buildAsString(false); + + ValidationRuleDto validationRuleDto = new ValidationRuleDto(); + validationRuleDto.setValidTo(validationRule.getValidTo()); + validationRuleDto.setValidFrom(validationRule.getValidFrom()); + validationRuleDto.setVersion(validationRule.getVersion()); + validationRuleDto.setCms(ruleSignature); + + when(restClientMock.getTrustedCertificates(CertificateTypeDto.UPLOAD)) + .thenReturn(ResponseEntity.ok(Collections.singletonList(uploadTrustListItem))); + + when(restClientMock.downloadCountryList()) + .thenReturn(ResponseEntity.ok(Collections.singletonList("EU"))); + + Map> response = Map.of(validationRule.getIdentifier(), List.of(validationRuleDto)); + + when(restClientMock.downloadValidationRule("EU")) + .thenReturn(ResponseEntity.ok(response)); + + ValidationRulesByCountry result = connector.getValidationRules(); + Assertions.assertEquals(1, result.size()); + assertEquals(validationRule, result.get("EU", validationRule.getIdentifier(), validationRule.getVersion())); + assertEquals(validationRule, result.getMap().get("EU").getMap().get(validationRule.getIdentifier()).getMap().get(validationRule.getVersion())); + Assertions.assertEquals(1, result.getMap().get("EU").getMap().size()); + Assertions.assertEquals(1, result.getMap().get("EU").getMap().get(validationRule.getIdentifier()).getMap().size()); + Assertions.assertEquals(1, result.flat().size()); + Assertions.assertEquals(1, result.size()); + assertEquals(validationRule, result.flat().get(0)); + assertEquals(validationRule, result.pure().get("EU").get(validationRule.getIdentifier()).get(validationRule.getVersion())); + Assertions.assertNotNull(connector.getLastUpdated()); + } + + @Test + void testThumbprintIntegrityCheck() throws Exception { + + KeyPair keyPair = KeyPairGenerator.getInstance("ec").generateKeyPair(); + X509Certificate another = CertificateTestUtils.generateCertificate(keyPair, "EU", "Another"); + + KeyPair keyPairUpload = KeyPairGenerator.getInstance("ec").generateKeyPair(); + X509Certificate upload = CertificateTestUtils.generateCertificate(keyPairUpload, "EU", "UPLOAD"); + + String uploadSignature = new SignedCertificateMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(testKeyStore.getTrustAnchor()), testKeyStore.getTrustAnchorPrivateKey()) + .withPayload(certificateUtils.convertCertificate(upload)) + .buildAsString(true); + + TrustListItemDto uploadTrustListItem = new TrustListItemDto(); + uploadTrustListItem.setCountry("EU"); + uploadTrustListItem.setKid("KID_EU_UPLOAD"); + uploadTrustListItem.setCertificateType(CertificateTypeDto.UPLOAD); + uploadTrustListItem.setTimestamp(ZonedDateTime.now()); + uploadTrustListItem.setSignature(uploadSignature); + // set thumbprint from another cert + uploadTrustListItem.setThumbprint(certificateUtils.getCertThumbprint(another)); + uploadTrustListItem.setRawData(Base64.getEncoder().encodeToString(upload.getEncoded())); + + String ruleSignature = new SignedStringMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(upload), keyPairUpload.getPrivate()) + .withPayload(objectMapper.writeValueAsString(validationRule)) + .buildAsString(false); + + ValidationRuleDto validationRuleDto = new ValidationRuleDto(); + validationRuleDto.setValidTo(validationRule.getValidTo()); + validationRuleDto.setValidFrom(validationRule.getValidFrom()); + validationRuleDto.setVersion(validationRule.getVersion()); + validationRuleDto.setCms(ruleSignature); + + when(restClientMock.getTrustedCertificates(CertificateTypeDto.UPLOAD)) + .thenReturn(ResponseEntity.ok(Collections.singletonList(uploadTrustListItem))); + + when(restClientMock.downloadCountryList()) + .thenReturn(ResponseEntity.ok(Collections.singletonList("EU"))); + + Map> response = Map.of(validationRule.getIdentifier(), List.of(validationRuleDto)); + + when(restClientMock.downloadValidationRule("EU")) + .thenReturn(ResponseEntity.ok(response)); + + ValidationRulesByCountry result = connector.getValidationRules(); + Assertions.assertEquals(0, result.size()); + Assertions.assertNotNull(connector.getLastUpdated()); + } + + + @Test + void testThumbprintIntegrityCheckInvalidRawData() throws Exception { + + KeyPair keyPairUpload = KeyPairGenerator.getInstance("ec").generateKeyPair(); + X509Certificate upload = CertificateTestUtils.generateCertificate(keyPairUpload, "EU", "UPLOAD"); + + String uploadSignature = new SignedCertificateMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(testKeyStore.getTrustAnchor()), testKeyStore.getTrustAnchorPrivateKey()) + .withPayload(certificateUtils.convertCertificate(upload)) + .buildAsString(true); + + TrustListItemDto uploadTrustListItem = new TrustListItemDto(); + uploadTrustListItem.setCountry("EU"); + uploadTrustListItem.setKid("KID_EU_UPLOAD"); + uploadTrustListItem.setCertificateType(CertificateTypeDto.UPLOAD); + uploadTrustListItem.setTimestamp(ZonedDateTime.now()); + uploadTrustListItem.setSignature(uploadSignature); + uploadTrustListItem.setThumbprint(certificateUtils.getCertThumbprint(upload)); + uploadTrustListItem.setRawData(Base64.getEncoder().encodeToString(new byte[]{})); + + String ruleSignature = new SignedStringMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(upload), keyPairUpload.getPrivate()) + .withPayload(objectMapper.writeValueAsString(validationRule)) + .buildAsString(false); + + ValidationRuleDto validationRuleDto = new ValidationRuleDto(); + validationRuleDto.setValidTo(validationRule.getValidTo()); + validationRuleDto.setValidFrom(validationRule.getValidFrom()); + validationRuleDto.setVersion(validationRule.getVersion()); + validationRuleDto.setCms(ruleSignature); + + when(restClientMock.getTrustedCertificates(CertificateTypeDto.UPLOAD)) + .thenReturn(ResponseEntity.ok(Collections.singletonList(uploadTrustListItem))); + + when(restClientMock.downloadCountryList()) + .thenReturn(ResponseEntity.ok(Collections.singletonList("EU"))); + + Map> response = Map.of(validationRule.getIdentifier(), List.of(validationRuleDto)); + + when(restClientMock.downloadValidationRule("EU")) + .thenReturn(ResponseEntity.ok(response)); + + + ValidationRulesByCountry result = connector.getValidationRules(); + Assertions.assertEquals(0, result.size()); + Assertions.assertNotNull(connector.getLastUpdated()); + } + + @Test + void testDownloadOfValidationRulesShouldFailWrongTrustAnchorSignatureForUpload() throws Exception { + + KeyPair fakeTrustAnchorKeyPair = KeyPairGenerator.getInstance("ec").generateKeyPair(); + X509Certificate fakeTrustAnchor = CertificateTestUtils.generateCertificate(fakeTrustAnchorKeyPair, "EU", "TA"); + + KeyPair keyPairUpload = KeyPairGenerator.getInstance("ec").generateKeyPair(); + X509Certificate upload = CertificateTestUtils.generateCertificate(keyPairUpload, "EU", "UPLOAD"); + + String uploadSignature = new SignedCertificateMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(fakeTrustAnchor), fakeTrustAnchorKeyPair.getPrivate()) + .withPayload(certificateUtils.convertCertificate(upload)) + .buildAsString(true); + + TrustListItemDto uploadTrustListItem = new TrustListItemDto(); + uploadTrustListItem.setCountry("EU"); + uploadTrustListItem.setKid("KID_EU_UPLOAD"); + uploadTrustListItem.setCertificateType(CertificateTypeDto.UPLOAD); + uploadTrustListItem.setTimestamp(ZonedDateTime.now()); + uploadTrustListItem.setSignature(uploadSignature); + uploadTrustListItem.setThumbprint(certificateUtils.getCertThumbprint(upload)); + uploadTrustListItem.setRawData(Base64.getEncoder().encodeToString(upload.getEncoded())); + + String ruleSignature = new SignedStringMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(upload), keyPairUpload.getPrivate()) + .withPayload(objectMapper.writeValueAsString(validationRule)) + .buildAsString(false); + + ValidationRuleDto validationRuleDto = new ValidationRuleDto(); + validationRuleDto.setValidTo(validationRule.getValidTo()); + validationRuleDto.setValidFrom(validationRule.getValidFrom()); + validationRuleDto.setVersion(validationRule.getVersion()); + validationRuleDto.setCms(ruleSignature); + + when(restClientMock.getTrustedCertificates(CertificateTypeDto.UPLOAD)) + .thenReturn(ResponseEntity.ok(Collections.singletonList(uploadTrustListItem))); + + when(restClientMock.downloadCountryList()) + .thenReturn(ResponseEntity.ok(Collections.singletonList("EU"))); + + Map> response = Map.of(validationRule.getIdentifier(), List.of(validationRuleDto)); + + when(restClientMock.downloadValidationRule("EU")) + .thenReturn(ResponseEntity.ok(response)); + + Assertions.assertEquals(0, connector.getValidationRules().size()); + Assertions.assertNotNull(connector.getLastUpdated()); + } + + @Test + void shouldReturnEmptyListWhenUploadCertDownloadFails() throws Exception { + KeyPair keyPairUpload = KeyPairGenerator.getInstance("ec").generateKeyPair(); + X509Certificate upload = CertificateTestUtils.generateCertificate(keyPairUpload, "EU", "UPLOAD"); + + String ruleSignature = new SignedStringMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(upload), keyPairUpload.getPrivate()) + .withPayload(objectMapper.writeValueAsString(validationRule)) + .buildAsString(false); + + ValidationRuleDto validationRuleDto = new ValidationRuleDto(); + validationRuleDto.setValidTo(validationRule.getValidTo()); + validationRuleDto.setValidFrom(validationRule.getValidFrom()); + validationRuleDto.setVersion(validationRule.getVersion()); + validationRuleDto.setCms(ruleSignature); + + Map> response = Map.of(validationRule.getIdentifier(), List.of(validationRuleDto)); + + when(restClientMock.downloadValidationRule("EU")) + .thenReturn(ResponseEntity.ok(response)); + + when(restClientMock.downloadCountryList()) + .thenReturn(ResponseEntity.ok(Collections.singletonList("EU"))); + + when(restClientMock.getTrustedCertificates(CertificateTypeDto.UPLOAD)) + .thenReturn(ResponseEntity.status(500).build()); + + Assertions.assertEquals(0, connector.getValidationRules().size()); + + when(restClientMock.downloadCountryList()) + .thenReturn(ResponseEntity.ok(Collections.singletonList("EU"))); + + doThrow(new FeignException.InternalServerError("", dummyRequest(), null)) + .when(restClientMock).getTrustedCertificates(CertificateTypeDto.UPLOAD); + + Assertions.assertEquals(0, connector.getValidationRules().size()); + } + + @Test + void shouldReturnEmptyListWhenDscDownloadFails() { + when(restClientMock.downloadCountryList()) + .thenReturn(ResponseEntity.ok(Collections.singletonList("EU"))); + + when(restClientMock.getTrustedCertificates(CertificateTypeDto.UPLOAD)) + .thenReturn(ResponseEntity.ok(Collections.emptyList())); + + when(restClientMock.downloadValidationRule("EU")) + .thenReturn(ResponseEntity.status(500).build()); + + Assertions.assertEquals(0, connector.getValidationRules().size()); + + when(restClientMock.downloadCountryList()) + .thenReturn(ResponseEntity.ok(Collections.singletonList("EU"))); + + when(restClientMock.getTrustedCertificates(CertificateTypeDto.UPLOAD)) + .thenReturn(ResponseEntity.ok(Collections.emptyList())); + + doThrow(new FeignException.InternalServerError("", dummyRequest(), null)) + .when(restClientMock).getTrustedCertificates(CertificateTypeDto.DSC); + + Assertions.assertEquals(0, connector.getValidationRules().size()); + } + + @Test + void testTrustAnchorCheckInvalidSignatureFormat() throws Exception { + + KeyPair keyPairUpload = KeyPairGenerator.getInstance("ec").generateKeyPair(); + X509Certificate upload = CertificateTestUtils.generateCertificate(keyPairUpload, "EU", "UPLOAD"); + + TrustListItemDto uploadTrustListItem = new TrustListItemDto(); + uploadTrustListItem.setCountry("EU"); + uploadTrustListItem.setKid("KID_EU_UPLOAD"); + uploadTrustListItem.setCertificateType(CertificateTypeDto.UPLOAD); + uploadTrustListItem.setTimestamp(ZonedDateTime.now()); + uploadTrustListItem.setSignature("BADSIGNATURE"); + uploadTrustListItem.setThumbprint(certificateUtils.getCertThumbprint(upload)); + uploadTrustListItem.setRawData(Base64.getEncoder().encodeToString(upload.getEncoded())); + + String ruleSignature = new SignedStringMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(upload), keyPairUpload.getPrivate()) + .withPayload(objectMapper.writeValueAsString(validationRule)) + .buildAsString(false); + + ValidationRuleDto validationRuleDto = new ValidationRuleDto(); + validationRuleDto.setValidTo(validationRule.getValidTo()); + validationRuleDto.setValidFrom(validationRule.getValidFrom()); + validationRuleDto.setVersion(validationRule.getVersion()); + validationRuleDto.setCms(ruleSignature); + + when(restClientMock.getTrustedCertificates(CertificateTypeDto.UPLOAD)) + .thenReturn(ResponseEntity.ok(Collections.singletonList(uploadTrustListItem))); + + when(restClientMock.downloadCountryList()) + .thenReturn(ResponseEntity.ok(Collections.singletonList("EU"))); + + Map> response = Map.of(validationRule.getIdentifier(), List.of(validationRuleDto)); + + when(restClientMock.downloadValidationRule("EU")) + .thenReturn(ResponseEntity.ok(response)); + + Assertions.assertEquals(0, connector.getValidationRules().size()); + Assertions.assertNotNull(connector.getLastUpdated()); + } + + @Test + void testTrustAnchorCheckWrongSignature() throws Exception { + + KeyPair anotherKeyPair = KeyPairGenerator.getInstance("ec").generateKeyPair(); + X509Certificate anotherCert = CertificateTestUtils.generateCertificate(anotherKeyPair, "EU", "another"); + + KeyPair keyPairUpload = KeyPairGenerator.getInstance("ec").generateKeyPair(); + X509Certificate upload = CertificateTestUtils.generateCertificate(keyPairUpload, "EU", "UPLOAD"); + + String wrongSignature = new SignedCertificateMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(testKeyStore.getTrustAnchor()), testKeyStore.getTrustAnchorPrivateKey()) + .withPayload(certificateUtils.convertCertificate(anotherCert)) + .buildAsString(true); + + TrustListItemDto uploadTrustListItem = new TrustListItemDto(); + uploadTrustListItem.setCountry("EU"); + uploadTrustListItem.setKid("KID_EU_UPLOAD"); + uploadTrustListItem.setCertificateType(CertificateTypeDto.UPLOAD); + uploadTrustListItem.setTimestamp(ZonedDateTime.now()); + uploadTrustListItem.setSignature(wrongSignature); + uploadTrustListItem.setThumbprint(certificateUtils.getCertThumbprint(upload)); + uploadTrustListItem.setRawData(Base64.getEncoder().encodeToString(upload.getEncoded())); + + String ruleSignature = new SignedStringMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(upload), keyPairUpload.getPrivate()) + .withPayload(objectMapper.writeValueAsString(validationRule)) + .buildAsString(false); + + ValidationRuleDto validationRuleDto = new ValidationRuleDto(); + validationRuleDto.setValidTo(validationRule.getValidTo()); + validationRuleDto.setValidFrom(validationRule.getValidFrom()); + validationRuleDto.setVersion(validationRule.getVersion()); + validationRuleDto.setCms(ruleSignature); + + when(restClientMock.getTrustedCertificates(CertificateTypeDto.UPLOAD)) + .thenReturn(ResponseEntity.ok(Collections.singletonList(uploadTrustListItem))); + + when(restClientMock.downloadCountryList()) + .thenReturn(ResponseEntity.ok(Collections.singletonList("EU"))); + + Map> response = Map.of(validationRule.getIdentifier(), List.of(validationRuleDto)); + + when(restClientMock.downloadValidationRule("EU")) + .thenReturn(ResponseEntity.ok(response)); + + Assertions.assertEquals(0, connector.getValidationRules().size()); + Assertions.assertNotNull(connector.getLastUpdated()); + } + + /** + * Method to create dummy request which is required to throw FeignExceptions. + */ + private Request dummyRequest() { + return Request.create(Request.HttpMethod.GET, "url", new HashMap<>(), null, new RequestTemplate()); + } + + private void assertEquals(ValidationRule v1, ValidationRule v2) { + Assertions.assertEquals(v1.getIdentifier(), v2.getIdentifier()); + Assertions.assertEquals(v1.getType(), v2.getType()); + Assertions.assertEquals(v1.getCountry(), v2.getCountry()); + Assertions.assertEquals(v1.getRegion(), v2.getRegion()); + Assertions.assertEquals(v1.getVersion(), v2.getVersion()); + Assertions.assertEquals(v1.getSchemaVersion(), v2.getSchemaVersion()); + Assertions.assertEquals(v1.getEngine(), v2.getEngine()); + Assertions.assertEquals(v1.getEngineVersion(), v2.getEngineVersion()); + Assertions.assertEquals(v1.getCertificateType(), v2.getCertificateType()); + Assertions.assertEquals(v1.getDescription(), v2.getDescription()); + Assertions.assertEquals(v1.getValidFrom().toEpochSecond(), v2.getValidFrom().toEpochSecond()); + Assertions.assertEquals(v1.getValidTo().toEpochSecond(), v2.getValidTo().toEpochSecond()); + Assertions.assertEquals(v1.getAffectedFields(), v2.getAffectedFields()); + Assertions.assertEquals(v1.getLogic(), v2.getLogic()); + } + +} diff --git a/src/test/java/eu/europa/ec/dgc/gateway/connector/ValueSetDownloadConnectorTest.java b/src/test/java/eu/europa/ec/dgc/gateway/connector/ValueSetDownloadConnectorTest.java new file mode 100644 index 0000000..aa3c0e6 --- /dev/null +++ b/src/test/java/eu/europa/ec/dgc/gateway/connector/ValueSetDownloadConnectorTest.java @@ -0,0 +1,113 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-lib + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.connector; + +import eu.europa.ec.dgc.gateway.connector.client.DgcGatewayConnectorRestClient; +import feign.FeignException; +import feign.Request; +import feign.RequestTemplate; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.when; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.ResponseEntity; + +@SpringBootTest +@Slf4j +class ValueSetDownloadConnectorTest { + + @MockBean + DgcGatewayConnectorRestClient restClientMock; + + @Autowired + DgcGatewayValueSetDownloadConnector connector; + + @Test + void testDownloadOfValueSets() { + + when(restClientMock.downloadValueSetIds()) + .thenReturn(ResponseEntity.ok(List.of("VS1", "VS2"))); + + when(restClientMock.downloadValueSet("VS1")) + .thenReturn(ResponseEntity.ok("VS1CONTENT")); + + when(restClientMock.downloadValueSet("VS2")) + .thenReturn(ResponseEntity.ok("VS2CONTENT")); + + Map result = connector.getValueSets(); + Assertions.assertEquals(2, result.size()); + Assertions.assertEquals("VS1CONTENT", result.get("VS1")); + Assertions.assertEquals("VS2CONTENT", result.get("VS2")); + Assertions.assertNotNull(connector.getLastUpdated()); + } + + @Test + void shouldReturnEmptyListWhenDownloadOfValueSetIdsFails() { + when(restClientMock.downloadValueSetIds()) + .thenReturn(ResponseEntity.status(500).build()); + + Assertions.assertEquals(0, connector.getValueSets().size()); + + doThrow(new FeignException.InternalServerError("", dummyRequest(), null)) + .when(restClientMock).downloadValueSetIds(); + + Assertions.assertEquals(0, connector.getValueSets().size()); + } + + @Test + void shouldReturnPartialListWhenDownloadOfOneValueSetFails() { + when(restClientMock.downloadValueSetIds()) + .thenReturn(ResponseEntity.ok(List.of("VS1", "VS2"))); + + when(restClientMock.downloadValueSet("VS1")) + .thenReturn(ResponseEntity.status(500).build()); + + when(restClientMock.downloadValueSet("VS2")) + .thenReturn(ResponseEntity.ok("VS2CONTENT")); + + Map valueSets = connector.getValueSets(); + Assertions.assertEquals(1, valueSets.size()); + Assertions.assertEquals("VS2CONTENT", valueSets.get("VS2")); + + doThrow(new FeignException.InternalServerError("", dummyRequest(), null)) + .when(restClientMock).downloadValueSet("VS1"); + + valueSets = connector.getValueSets(); + Assertions.assertEquals(1, valueSets.size()); + Assertions.assertEquals("VS2CONTENT", valueSets.get("VS2")); + } + + /** + * Method to create dummy request which is required to throw FeignExceptions. + */ + private Request dummyRequest() { + return Request.create(Request.HttpMethod.GET, "url", new HashMap<>(), null, new RequestTemplate()); + } + +}