Skip to content

Commit

Permalink
Added downloader for trusted issuer
Browse files Browse the repository at this point in the history
  • Loading branch information
slaurenz committed Mar 24, 2022
1 parent 5beab0c commit 4dd0997
Show file tree
Hide file tree
Showing 8 changed files with 464 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,17 @@
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.gateway.connector.dto.TrustedIssuerDto;
import eu.europa.ec.dgc.gateway.connector.mapper.TrustedIssuerMapper;
import eu.europa.ec.dgc.gateway.connector.model.QueryParameter;
import eu.europa.ec.dgc.gateway.connector.model.TrustedIssuer;
import eu.europa.ec.dgc.signing.SignedCertificateMessageParser;
import eu.europa.ec.dgc.signing.SignedStringMessageParser;
import eu.europa.ec.dgc.utils.CertificateUtils;
import feign.FeignException;
import java.io.IOException;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.Security;
Expand All @@ -41,6 +48,7 @@
import java.util.Objects;
import java.util.stream.Collectors;
import javax.annotation.PostConstruct;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
Expand Down Expand Up @@ -69,12 +77,16 @@ class DgcGatewayConnectorUtils {

private final DgcGatewayConnectorConfigProperties properties;

private final TrustedIssuerMapper trustedIssuerMapper;

@Qualifier("trustAnchor")
private final KeyStore trustAnchorKeyStore;

@Setter
private List<X509CertificateHolder> trustAnchors;

private static final String HASH_SEPARATOR = ";";


@PostConstruct
void init() throws KeyStoreException, CertificateEncodingException, IOException {
Expand Down Expand Up @@ -158,6 +170,22 @@ boolean checkTrustAnchorSignature(TrustListItemDto trustListItem, List<X509Certi
return trustAnchors.stream().anyMatch(trustAnchor -> parser.getSigningCertificate().equals(trustAnchor));
}

boolean checkTrustAnchorSignature(TrustedIssuerDto trustedIssuer, List<X509CertificateHolder> trustAnchors) {
SignedStringMessageParser parser = new SignedStringMessageParser(trustedIssuer.getSignature(),
Base64.getEncoder().encodeToString(getHashData(trustedIssuer).getBytes(StandardCharsets.UTF_8)));

if (parser.getParserState() != SignedCertificateMessageParser.ParserState.SUCCESS) {
log.error("Could not parse trustedIssuer signature. ParserState: {}", parser.getParserState());
return false;
} else if (!parser.isSignatureVerified()) {
log.error("Could not verify trustedIssuer Signature, Country: {}, URL: {}",
trustedIssuer.getCountry(), trustedIssuer.getUrl());
return false;
}

return trustAnchors.stream().anyMatch(trustAnchor -> parser.getSigningCertificate().equals(trustAnchor));
}

X509CertificateHolder getCertificateFromTrustListItem(TrustListItemDto trustListItem) {
byte[] decodedBytes = Base64.getDecoder().decode(trustListItem.getRawData());

Expand Down Expand Up @@ -193,6 +221,39 @@ public List<X509CertificateHolder> fetchCertificatesAndVerifyByTrustAnchor(Certi
.collect(Collectors.toList());
}

public List<TrustedIssuer> fetchTrustedIssuersAndVerifyByTrustAnchor(
Map<QueryParameter<? extends Serializable>, List<? extends Serializable>> queryParameterMap
)
throws DgcGatewayConnectorUtils.DgcGatewayConnectorException {
log.info("Fetching TrustedIssuers from DGCG");

ResponseEntity<List<TrustedIssuerDto>> responseEntity;
try {
responseEntity = dgcGatewayConnectorRestClient.downloadTrustedIssuers(
convertQueryParams(queryParameterMap)
);
} catch (FeignException e) {
throw new DgcGatewayConnectorUtils.DgcGatewayConnectorException(
e.status(), "Download of TrustedIssuers failed.");
}

List<TrustedIssuerDto> downloadedTrustedIssuers = responseEntity.getBody();

if (responseEntity.getStatusCode() != HttpStatus.OK || downloadedTrustedIssuers == null) {
throw new DgcGatewayConnectorUtils.DgcGatewayConnectorException(
responseEntity.getStatusCodeValue(), "Download of TrustedIssuers failed.");
} else {
log.info("Got Response from DGCG, Downloaded TrustedIssuers: {}",
downloadedTrustedIssuers.size());
}

return downloadedTrustedIssuers.stream()
.filter(c -> this.checkTrustAnchorSignature(c, trustAnchors))
.map(trustedIssuerMapper::map)
.collect(Collectors.toList());

}

private boolean checkThumbprintIntegrity(TrustListItemDto trustListItem) {
byte[] certificateRawData = Base64.getDecoder().decode(trustListItem.getRawData());
try {
Expand All @@ -204,4 +265,34 @@ private boolean checkThumbprintIntegrity(TrustListItemDto trustListItem) {
return false;
}
}

protected Map<String, String> convertQueryParams(
Map<QueryParameter<? extends Serializable>, List<? extends Serializable>> queryParameterMap) {

return queryParameterMap.entrySet()
.stream()
.map(mapEntry -> {
String queryKey = mapEntry.getKey().getQueryParamName();
String queryValue = mapEntry.getValue().stream()
.map(Serializable::toString)
.collect(Collectors.joining(","));
return Map.entry(queryKey, queryValue);
})
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}

private String getHashData(TrustedIssuerDto trustedIssuerDto) {
return trustedIssuerDto.getUuid() + HASH_SEPARATOR
+ trustedIssuerDto.getCountry() + HASH_SEPARATOR
+ trustedIssuerDto.getName() + HASH_SEPARATOR
+ trustedIssuerDto.getUrl() + HASH_SEPARATOR
+ trustedIssuerDto.getType().name() + HASH_SEPARATOR;
}

@RequiredArgsConstructor
@Getter
public static class DgcGatewayConnectorException extends Exception {
private final int httpStatusCode;
private final String message;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
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.mapper.TrustListMapper;
import eu.europa.ec.dgc.gateway.connector.mapper.TrustedIssuerMapper;
import eu.europa.ec.dgc.gateway.connector.mapper.TrustedIssuerMapperImpl;
import eu.europa.ec.dgc.gateway.connector.springbootworkaroundforks.DgcFeignClientBuilder;
import eu.europa.ec.dgc.gateway.connector.springbootworkaroundforks.DgcFeignClientFactoryBean;
import eu.europa.ec.dgc.utils.CertificateUtils;
Expand Down Expand Up @@ -75,6 +77,7 @@ public class DgcGatewayDownloadConnectorBuilder {
private static final CertificateUtils certificateUtils = new CertificateUtils();
private final ApplicationContext springBootContext;
private final TrustListMapper trustListMapper;
private final TrustedIssuerMapper trustedIssuerMapper = new TrustedIssuerMapperImpl();

/**
* Builder parameters.
Expand Down Expand Up @@ -292,7 +295,7 @@ public DgcGatewayDownloadConnector build() throws DgcGatewayDownloadConnectorBui
.build();

DgcGatewayConnectorUtils connectorUtils =
new DgcGatewayConnectorUtils(certificateUtils, restClient, null, null);
new DgcGatewayConnectorUtils(certificateUtils, restClient, properties, trustedIssuerMapper, null);
connectorUtils.setTrustAnchors(trustAnchors);

return new DgcGatewayDownloadConnector(connectorUtils, restClient, properties, trustListMapper);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/*-
* ---license-start
* WHO Digital Documentation Covid Certificate Gateway Service / ddcc-gateway-lib
* ---
* Copyright (C) 2022 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.config.DgcGatewayConnectorConfigProperties;
import eu.europa.ec.dgc.gateway.connector.model.QueryParameter;
import eu.europa.ec.dgc.gateway.connector.model.TrustedIssuer;
import java.io.Serializable;
import java.security.Security;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
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.scheduling.annotation.EnableScheduling;
import org.springframework.stereotype.Service;

@ConditionalOnProperty("dgc.gateway.connector.enabled")
@Lazy
@Service
@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
@RequiredArgsConstructor
@EnableScheduling
@Slf4j
public class DgcGatewayTrustedIssuerDownloadConnector {

private final DgcGatewayConnectorUtils connectorUtils;

private final DgcGatewayConnectorConfigProperties properties;


@Getter
private String status = null;

@Getter
private LocalDateTime lastUpdated = null;

private List<TrustedIssuer> trustedIssuers = new ArrayList<>();

private final HashMap<QueryParameter<? extends Serializable>, List<? extends Serializable>> queryParameterMap =
new HashMap<>();

@PostConstruct
void init() {
Security.addProvider(new BouncyCastleProvider());
}


/**
* Set Query Params to filter requests to Gateway. If an entry for given Key already exists it will be overridden.
*
* @param queryParameter The Query Parameter
* @param value Values to filter for.
*/
public <T extends Serializable> void setQueryParameter(QueryParameter<T> queryParameter, T value) {
setQueryParameter(queryParameter, List.of(value));
}

/**
* Set Query Params to filter requests to Gateway. If an entry for given Key already exists it will be overridden.
*
* @param queryParameter The Query Parameter
* @param values List of values (filtering is additive, e.g.: Providing values "CSCA" and "UPLOAD" will
* result in a list of "CSCA" and "UPLOAD" certificates with at least one matching property.
*/
public <T extends Serializable> void setQueryParameter(QueryParameter<T> queryParameter, List<T> values) {
if (!queryParameter.getArrayValue() && values.size() > 1) {
throw new IllegalArgumentException("Only one value is allowed for non-array query parameters.");
}

// Check if Key will be added or value has changed if key already exists
if ((!queryParameterMap.containsKey(queryParameter))
|| queryParameterMap.containsKey(queryParameter)
&& queryParameterMap.get(queryParameter).hashCode() != values.hashCode()) {

// value has changed, invalidate cache
lastUpdated = null;
}

queryParameterMap.put(queryParameter, values);
}

/**
* Resets the Query Params. Cache will also be invalidated.
*/
public void resetQueryParameter() {
queryParameterMap.clear();
lastUpdated = null;
}


/**
* Gets the list of downloaded and validated TrustedIssuers.
* 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 TrustedIssuer}
*/
public List<TrustedIssuer> getTrustedIssuers() {
updateIfRequired();
return Collections.unmodifiableList(trustedIssuers);
}


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.");

try {
// Fetching TrustedIssuers
trustedIssuers = connectorUtils.fetchTrustedIssuersAndVerifyByTrustAnchor(queryParameterMap);
log.info("TrustedIssuers contains {} entries", trustedIssuers.size());
status = null;
} catch (DgcGatewayConnectorUtils.DgcGatewayConnectorException e) {
log.error("Failed to Download Trusted Certificates: {} - {}", e.getHttpStatusCode(), e.getMessage());
status = "Download Failed: " + e.getHttpStatusCode() + " - " + e.getMessage();
}
} else {
log.debug("Cache needs no refresh.");
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import eu.europa.ec.dgc.gateway.connector.dto.CertificateTypeDto;
import eu.europa.ec.dgc.gateway.connector.dto.RevocationBatchListDto;
import eu.europa.ec.dgc.gateway.connector.dto.TrustListItemDto;
import eu.europa.ec.dgc.gateway.connector.dto.TrustedIssuerDto;
import eu.europa.ec.dgc.gateway.connector.dto.ValidationRuleDto;
import java.util.List;
import java.util.Map;
Expand All @@ -37,6 +38,7 @@
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestParam;

@ConditionalOnProperty("dgc.gateway.connector.enabled")
@FeignClient(
Expand Down Expand Up @@ -154,4 +156,12 @@ ResponseEntity<RevocationBatchListDto> downloadRevocationList(
*/
@DeleteMapping(value = "/revocation-list", consumes = "application/cms-text")
ResponseEntity<Void> deleteBatch(@RequestBody String batch);

/**
* Download all trusted issuers.
*
* @return List of Trusted Issuers
*/
@GetMapping(value = "/trustList/issuers", produces = MediaType.APPLICATION_JSON_VALUE)
ResponseEntity<List<TrustedIssuerDto>> downloadTrustedIssuers(@RequestParam Map<String, String> queryParams);
}
Loading

0 comments on commit 4dd0997

Please sign in to comment.