Skip to content

Commit

Permalink
Implement DGCG Connector Download
Browse files Browse the repository at this point in the history
  • Loading branch information
f11h committed Apr 29, 2021
1 parent 212ec84 commit 2505711
Show file tree
Hide file tree
Showing 14 changed files with 845 additions and 0 deletions.
1 change: 1 addition & 0 deletions lombok.config
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Qualifier
52 changes: 52 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,12 @@
<!-- dependencies -->
<owasp.version>6.1.1</owasp.version>
<spring.boot.version>2.4.4</spring.boot.version>
<spring.cloud.version>3.0.2</spring.cloud.version>
<feign.version>10.10.1</feign.version>
<bcpkix.version>1.68</bcpkix.version>
<lombok.version>1.18.20</lombok.version>
<junit.version>5.7.1</junit.version>
<mapstruct.version>1.4.2.Final</mapstruct.version>

<plugin.checkstyle.version>3.1.2</plugin.checkstyle.version>
<plugin.sonar.version>3.6.1.1688</plugin.sonar.version>
Expand Down Expand Up @@ -66,6 +69,32 @@
<artifactId>spring-boot-starter</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
<version>${spring.boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>${spring.cloud.version}</version>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
<version>${feign.version}</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${mapstruct.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
Expand Down Expand Up @@ -164,6 +193,29 @@
<emptyLineAfterHeader>true</emptyLineAfterHeader>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</path>
<path>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<version>${spring.boot.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>

Expand Down
2 changes: 2 additions & 0 deletions src/main/java/eu/europa/ec/dgc/DgcLibAutoConfiguration.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@

package eu.europa.ec.dgc;

import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan("eu.europa.ec.dgc")
@EnableConfigurationProperties
public class DgcLibAutoConfiguration {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
/*-
* ---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.dto.CertificateTypeDto;
import eu.europa.ec.dgc.gateway.connector.dto.TrustListItemDto;
import eu.europa.ec.dgc.gateway.connector.mapper.TrustListMapper;
import eu.europa.ec.dgc.gateway.connector.model.TrustListItem;
import eu.europa.ec.dgc.signing.SignedCertificateMessageBuilder;
import eu.europa.ec.dgc.utils.CertificateUtils;
import java.io.IOException;
import java.security.KeyStore;
import java.security.KeyStoreException;
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.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import javax.annotation.PostConstruct;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.bouncycastle.cert.X509CertificateHolder;
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.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")
@Service
@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
@RequiredArgsConstructor
@EnableScheduling
@Slf4j
public class DgcGatewayConnector {

private final DgcGatewayConnectorUtils connectorUtils;

private final DgcGatewayConnectorRestClient dgcGatewayConnectorRestClient;

private final DgcGatewayConnectorConfigProperties properties;

private final CertificateUtils certificateUtils;

private final TrustListMapper trustListMapper;

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

private X509CertificateHolder trustAnchor;

@Getter
private LocalDateTime lastUpdated = null;

private List<TrustListItem> trustedCertificates = new ArrayList<>();

private List<X509CertificateHolder> trustedCsca = new ArrayList<>();

@PostConstruct
void init() throws KeyStoreException, CertificateEncodingException, IOException {
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 List<TrustListItem> getTrustedCertificates() {
updateIfRequired();
return Collections.unmodifiableList(trustedCertificates);
}

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

fetchTrustedCscaAndVerifyByTrustAnchor();
fetchTrustListAndVerifyByCsca();
} else {
log.debug("Cache needs no refresh.");
}
}

private void fetchTrustedCscaAndVerifyByTrustAnchor() {
ResponseEntity<List<TrustListItemDto>> downloadedCsca =
dgcGatewayConnectorRestClient.getTrustedCertificates(CertificateTypeDto.CSCA);

if (downloadedCsca.getStatusCode() != HttpStatus.OK || downloadedCsca.getBody() == null) {
log.error("Failed to Download CSCA from DGC Gateway");
return;
}

trustedCsca = downloadedCsca.getBody().stream()
.filter(c -> connectorUtils.checkTrustAnchorSignature(c, trustAnchor))
.map(connectorUtils::getCertificateFromTrustListItem)
.filter(Objects::nonNull)
.collect(Collectors.toList());
}

private void fetchTrustListAndVerifyByCsca() {
log.info("Fetching TrustList from DGCG");

ResponseEntity<List<TrustListItemDto>> responseEntity =
dgcGatewayConnectorRestClient.getTrustedCertificates(CertificateTypeDto.DSC);
List<TrustListItemDto> downloadedDcs = responseEntity.getBody();

if (responseEntity.getStatusCode() != HttpStatus.OK || downloadedDcs == null) {
log.error("Download of TrustListItems failed. DGCG responded with status code: {}",
responseEntity.getStatusCode());
return;
} else {
log.info("Got Response from DGCG, Downloaded Certificates: {}", downloadedDcs.size());
}

trustedCertificates = downloadedDcs.stream()
.filter(dcs -> trustedCsca.stream().anyMatch(ca -> connectorUtils.trustListItemSignedByCa(dcs, ca)))
.map(trustListMapper::map)
.collect(Collectors.toList());

lastUpdated = LocalDateTime.now();
log.info("Put {} trusted certificates into TrustList", trustedCertificates.size());
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*-
* ---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.dto.TrustListItemDto;
import eu.europa.ec.dgc.signing.SignedCertificateMessageParser;
import java.io.IOException;
import java.security.cert.CertificateException;
import java.util.Base64;
import lombok.extern.slf4j.Slf4j;
import org.bouncycastle.cert.CertException;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.operator.ContentVerifierProvider;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaContentVerifierProviderBuilder;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service;

@Service
@Slf4j
@ConditionalOnProperty("dgc.gateway.connector.enabled")
class DgcGatewayConnectorUtils {

public boolean trustListItemSignedByCa(TrustListItemDto certificate, X509CertificateHolder ca) {
ContentVerifierProvider verifier;
try {
verifier = new JcaContentVerifierProviderBuilder().build(ca);
} catch (OperatorCreationException | CertificateException e) {
log.error("Failed to instantiate JcaContentVerifierProvider from cert. KID: {}, Country: {}",
certificate.getKid(), certificate.getCountry());
return false;
}

X509CertificateHolder dcs;
try {
dcs = new X509CertificateHolder(Base64.getDecoder().decode(certificate.getRawData()));
} catch (IOException e) {
log.error("Could not parse certificate. KID: {}, Country: {}",
certificate.getKid(), certificate.getCountry());
return false;
}

try {
return dcs.isSignatureValid(verifier);
} catch (CertException e) {
log.error("Could not verify that certificate was issued by ca. Certificate: {}, CA: {}",
dcs.getSubject().toString(), ca.getSubject().toString());
return false;
}
}

boolean checkTrustAnchorSignature(TrustListItemDto trustListItem, X509CertificateHolder trustAnchor) {
SignedCertificateMessageParser parser = new SignedCertificateMessageParser(
trustListItem.getSignature(), trustListItem.getRawData());

if (parser.getParserState() != SignedCertificateMessageParser.ParserState.SUCCESS) {
log.error("Could not parse trustListItem CMS. ParserState: {}", parser.getParserState());
return false;
} else if (!parser.isSignatureVerified()) {
log.error("Could not verify trustListItem CMS Signature, KID: {}, Country: {}",
trustListItem.getKid(), trustListItem.getCountry());
return false;
}

return parser.getSigningCertificate().equals(trustAnchor);
}

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

try {
return new X509CertificateHolder(decodedBytes);
} catch (IOException e) {
log.error("Failed to parse Certificate Raw Data. KID: {}, Country: {}",
trustListItem.getKid(), trustListItem.getCountry());
return null;
}
}
}
Loading

0 comments on commit 2505711

Please sign in to comment.