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 8ee993f..8473c94 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 @@ -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; @@ -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; @@ -69,12 +77,16 @@ class DgcGatewayConnectorUtils { private final DgcGatewayConnectorConfigProperties properties; + private final TrustedIssuerMapper trustedIssuerMapper; + @Qualifier("trustAnchor") private final KeyStore trustAnchorKeyStore; @Setter private List trustAnchors; + private static final String HASH_SEPARATOR = ";"; + @PostConstruct void init() throws KeyStoreException, CertificateEncodingException, IOException { @@ -158,6 +170,22 @@ boolean checkTrustAnchorSignature(TrustListItemDto trustListItem, List parser.getSigningCertificate().equals(trustAnchor)); } + boolean checkTrustAnchorSignature(TrustedIssuerDto trustedIssuer, List 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()); @@ -193,6 +221,39 @@ public List fetchCertificatesAndVerifyByTrustAnchor(Certi .collect(Collectors.toList()); } + public List fetchTrustedIssuersAndVerifyByTrustAnchor( + Map, List> queryParameterMap + ) + throws DgcGatewayConnectorUtils.DgcGatewayConnectorException { + log.info("Fetching TrustedIssuers from DGCG"); + + ResponseEntity> responseEntity; + try { + responseEntity = dgcGatewayConnectorRestClient.downloadTrustedIssuers( + convertQueryParams(queryParameterMap) + ); + } catch (FeignException e) { + throw new DgcGatewayConnectorUtils.DgcGatewayConnectorException( + e.status(), "Download of TrustedIssuers failed."); + } + + List 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 { @@ -204,4 +265,34 @@ private boolean checkThumbprintIntegrity(TrustListItemDto trustListItem) { return false; } } + + protected Map convertQueryParams( + Map, List> 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; + } } diff --git a/src/main/java/eu/europa/ec/dgc/gateway/connector/DgcGatewayDownloadConnectorBuilder.java b/src/main/java/eu/europa/ec/dgc/gateway/connector/DgcGatewayDownloadConnectorBuilder.java index 313914b..bffc12c 100644 --- a/src/main/java/eu/europa/ec/dgc/gateway/connector/DgcGatewayDownloadConnectorBuilder.java +++ b/src/main/java/eu/europa/ec/dgc/gateway/connector/DgcGatewayDownloadConnectorBuilder.java @@ -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; @@ -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. @@ -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); diff --git a/src/main/java/eu/europa/ec/dgc/gateway/connector/DgcGatewayTrustedIssuerDownloadConnector.java b/src/main/java/eu/europa/ec/dgc/gateway/connector/DgcGatewayTrustedIssuerDownloadConnector.java new file mode 100644 index 0000000..9338c17 --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/connector/DgcGatewayTrustedIssuerDownloadConnector.java @@ -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 trustedIssuers = new ArrayList<>(); + + private final HashMap, List> 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 void setQueryParameter(QueryParameter 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 void setQueryParameter(QueryParameter queryParameter, List 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 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."); + } + } + +} 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 2cf7a92..78d4afe 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 @@ -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; @@ -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( @@ -154,4 +156,12 @@ ResponseEntity downloadRevocationList( */ @DeleteMapping(value = "/revocation-list", consumes = "application/cms-text") ResponseEntity deleteBatch(@RequestBody String batch); + + /** + * Download all trusted issuers. + * + * @return List of Trusted Issuers + */ + @GetMapping(value = "/trustList/issuers", produces = MediaType.APPLICATION_JSON_VALUE) + ResponseEntity> downloadTrustedIssuers(@RequestParam Map queryParams); } diff --git a/src/main/java/eu/europa/ec/dgc/gateway/connector/dto/TrustedIssuerDto.java b/src/main/java/eu/europa/ec/dgc/gateway/connector/dto/TrustedIssuerDto.java new file mode 100644 index 0000000..0b938a6 --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/connector/dto/TrustedIssuerDto.java @@ -0,0 +1,63 @@ +/*- + * ---license-start + * WHO Digital Documentation Covid Certificate Gateway Service / ddcc-gateway + * --- + * 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.dto; + +import java.time.ZonedDateTime; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class TrustedIssuerDto { + + private String url; + + private UrlTypeDto type; + + private String country; + + private String thumbprint; + + private String sslPublicKey; + + private String keyStorageType; + + private String signature; + + private ZonedDateTime timestamp; + + private String sourceGateway; + + private String uuid; + + private String domain; + + private Long version; + + private String name; + + public enum UrlTypeDto { + HTTP, + DID + } +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/connector/mapper/TrustedIssuerMapper.java b/src/main/java/eu/europa/ec/dgc/gateway/connector/mapper/TrustedIssuerMapper.java new file mode 100644 index 0000000..5ae7b28 --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/connector/mapper/TrustedIssuerMapper.java @@ -0,0 +1,34 @@ +/*- + * ---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.mapper; + +import eu.europa.ec.dgc.gateway.connector.dto.TrustedIssuerDto; +import eu.europa.ec.dgc.gateway.connector.model.TrustedIssuer; +import java.util.List; +import org.mapstruct.Mapper; + +@Mapper(componentModel = "spring") +public interface TrustedIssuerMapper { + + TrustedIssuer map(TrustedIssuerDto trustedIssuerDto); + + List map(List trustedIssuerDto); +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/connector/model/QueryParameter.java b/src/main/java/eu/europa/ec/dgc/gateway/connector/model/QueryParameter.java new file mode 100644 index 0000000..be1ffee --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/connector/model/QueryParameter.java @@ -0,0 +1,53 @@ +/*- + * ---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.model; + +import java.io.Serializable; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +@Getter +public class QueryParameter { + + public static final QueryParameter GROUP = + new QueryParameter<>("group", true, String.class); + + public static final QueryParameter COUNTRY_CODE = + new QueryParameter<>("country", true, String.class); + + public static final QueryParameter DOMAIN = + new QueryParameter<>("domain", true, String.class); + + public static final QueryParameter REFERENCE_TYPE = + new QueryParameter<>("referenceType", true, String.class); + + public static final QueryParameter SIGNATURE_TYPE = + new QueryParameter<>("signatureType", true, String.class); + + public static final QueryParameter WITH_FEDERATION = + new QueryParameter<>("withFederation", false, Boolean.class); + + private final String queryParamName; + private final Boolean arrayValue; + private final Class queryParamType; +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/connector/model/TrustedIssuer.java b/src/main/java/eu/europa/ec/dgc/gateway/connector/model/TrustedIssuer.java new file mode 100644 index 0000000..dcbc966 --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/connector/model/TrustedIssuer.java @@ -0,0 +1,57 @@ +/*- + * ---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.model; + +import java.time.ZonedDateTime; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class TrustedIssuer { + + private String url; + + private UrlType type; + + private String country; + + private String thumbprint; + + private String sslPublicKey; + + private String keyStorageType; + + private String signature; + + private ZonedDateTime timestamp; + + private String name; + + private String uuid; + + private String domain; + + public enum UrlType { + HTTP, + DID + } +}