From 9997e895c2e04b73182d429c4c245c26f4818539 Mon Sep 17 00:00:00 2001 From: Felix Dittrich <31076102+f11h@users.noreply.github.com> Date: Wed, 30 Mar 2022 15:53:14 +0200 Subject: [PATCH] Feat: Publication (#179) * Implementation of Publication Archive * Finetuning for Publication Unit Test for Publication Archive * Fix Typo * Add ability to switch off publication * Update URLs in Readme and License File * Add ability to switch off synchronize call --- docker-compose.yml | 3 + lombok.config | 1 + .../gateway/client/AssetManagerClient.java | 71 ++++ .../client/AssetManagerClientConfig.java | 73 ++++ .../gateway/config/DgcConfigProperties.java | 22 +- .../ec/dgc/gateway/config/DgcKeyStore.java | 67 ++-- .../AssetManagerSynchronizeResponseDto.java | 76 ++++ .../gateway/service/PublishingService.java | 328 ++++++++++++++++++ .../gateway/service/TrustedIssuerService.java | 8 + .../gateway/service/TrustedPartyService.java | 6 + src/main/resources/application.yml | 15 + src/main/resources/publication/License.txt | 44 +++ src/main/resources/publication/Readme.txt | 63 ++++ .../publishing/ArchivePublishingTest.java | 295 ++++++++++++++++ .../publishing/PublishingDisabledTest.java | 51 +++ .../dgc/gateway/testdata/DgcTestKeyStore.java | 39 ++- .../testdata/TrustedIssuerTestHelper.java | 2 + .../testdata/TrustedPartyTestHelper.java | 2 + src/test/resources/application.yml | 18 +- 19 files changed, 1143 insertions(+), 41 deletions(-) create mode 100644 lombok.config create mode 100644 src/main/java/eu/europa/ec/dgc/gateway/client/AssetManagerClient.java create mode 100644 src/main/java/eu/europa/ec/dgc/gateway/client/AssetManagerClientConfig.java create mode 100644 src/main/java/eu/europa/ec/dgc/gateway/model/AssetManagerSynchronizeResponseDto.java create mode 100644 src/main/java/eu/europa/ec/dgc/gateway/service/PublishingService.java create mode 100644 src/main/resources/publication/License.txt create mode 100644 src/main/resources/publication/Readme.txt create mode 100644 src/test/java/eu/europa/ec/dgc/gateway/publishing/ArchivePublishingTest.java create mode 100644 src/test/java/eu/europa/ec/dgc/gateway/publishing/PublishingDisabledTest.java diff --git a/docker-compose.yml b/docker-compose.yml index 12b2a20d..5a8b6ca1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -29,6 +29,9 @@ services: - DGC_TRUSTANCHOR_KEYSTOREPATH=/ec/prod/app/san/dgc/ta.jks - DGC_TRUSTANCHOR_KEYSTOREPASS=dgcg-p4ssw0rd - DGC_TRUSTANCHOR_CERTIFICATEALIAS=dgcg_trust_anchor + - DGC_PUBLICATION_KEYSTORE_KEYSTOREPATH=/ec/prod/app/san/dgc/publication-signer-tst.jks + - DGC_PUBLICATION_KEYSTORE_KEYSTOREPASS=dgc-p4ssw0rd + - DGC_PUBLICATION_KEYSTORE_CERTIFICATEALIAS=dgc_tst_publication depends_on: - mysql networks: diff --git a/lombok.config b/lombok.config new file mode 100644 index 00000000..53a4a723 --- /dev/null +++ b/lombok.config @@ -0,0 +1 @@ +lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Qualifier diff --git a/src/main/java/eu/europa/ec/dgc/gateway/client/AssetManagerClient.java b/src/main/java/eu/europa/ec/dgc/gateway/client/AssetManagerClient.java new file mode 100644 index 00000000..e5249f8a --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/client/AssetManagerClient.java @@ -0,0 +1,71 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 - 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.client; + +import eu.europa.ec.dgc.gateway.model.AssetManagerSynchronizeResponseDto; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; + +@FeignClient( + name = "assetManagerClient", + url = "${dgc.publication.url}", + configuration = AssetManagerClientConfig.class) +@ConditionalOnProperty("dgc.publication.enabled") +public interface AssetManagerClient { + + @PutMapping( + value = "/remote.php/dav/files/{uid}/{path}/{filename}", + consumes = MediaType.APPLICATION_OCTET_STREAM_VALUE, + produces = MediaType.ALL_VALUE) + ResponseEntity uploadFile(@RequestHeader(HttpHeaders.AUTHORIZATION) String authHeader, + @PathVariable("uid") String uid, + @PathVariable("path") String path, + @PathVariable("filename") String filename, + @RequestBody byte[] file); + + @PostMapping( + value = "/ocs/v2.php/apps/files/api/v2/synchronize", + consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, + produces = MediaType.APPLICATION_JSON_VALUE + ) + ResponseEntity synchronize( + @RequestHeader(HttpHeaders.AUTHORIZATION) String authHeader, + @RequestHeader("OCS-APIRequest") String ocsApiRequest, + @RequestBody SynchronizeFormData formData); + + @Getter + @AllArgsConstructor + class SynchronizeFormData { + private String path; + private String nodeList; + private String notifyEmails; + } +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/client/AssetManagerClientConfig.java b/src/main/java/eu/europa/ec/dgc/gateway/client/AssetManagerClientConfig.java new file mode 100644 index 00000000..57ae3d23 --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/client/AssetManagerClientConfig.java @@ -0,0 +1,73 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 - 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.client; + +import eu.europa.ec.dgc.gateway.config.DgcConfigProperties; +import feign.Client; +import feign.codec.Encoder; +import feign.form.spring.SpringFormEncoder; +import feign.httpclient.ApacheHttpClient; +import java.security.NoSuchAlgorithmException; +import javax.net.ssl.SSLContext; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.http.conn.ssl.DefaultHostnameVerifier; +import org.apache.http.impl.client.HttpClientBuilder; +import org.springframework.beans.factory.ObjectFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.http.HttpMessageConverters; +import org.springframework.cloud.openfeign.support.SpringEncoder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@RequiredArgsConstructor +@Slf4j +@ConditionalOnProperty("dgc.publication.enabled") +public class AssetManagerClientConfig { + + private final ObjectFactory messageConverters; + + private final DgcConfigProperties config; + + /** + * Form Encoder for Multipart File Upload. + */ + @Bean + public Encoder feignFormEncoder() { + return new SpringFormEncoder(new SpringEncoder(messageConverters)); + } + + /** + * Configure the client depending on the ssl properties. + * + * @return an Apache Http Client with or without SSL features + */ + @Bean + public Client assetManagerClient() throws NoSuchAlgorithmException { + HttpClientBuilder httpClientBuilder = HttpClientBuilder.create(); + + httpClientBuilder.setSSLContext(SSLContext.getDefault()); + httpClientBuilder.setSSLHostnameVerifier(new DefaultHostnameVerifier()); + + return new ApacheHttpClient(httpClientBuilder.build()); + } +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/config/DgcConfigProperties.java b/src/main/java/eu/europa/ec/dgc/gateway/config/DgcConfigProperties.java index 898d0ea1..781f2fe0 100644 --- a/src/main/java/eu/europa/ec/dgc/gateway/config/DgcConfigProperties.java +++ b/src/main/java/eu/europa/ec/dgc/gateway/config/DgcConfigProperties.java @@ -20,6 +20,7 @@ package eu.europa.ec.dgc.gateway.config; +import java.util.ArrayList; import java.util.List; import lombok.Getter; import lombok.Setter; @@ -31,7 +32,8 @@ public class DgcConfigProperties { private final CertAuth certAuth = new CertAuth(); - private final TrustAnchor trustAnchor = new TrustAnchor(); + private final KeyStoreWithAlias trustAnchor = new KeyStoreWithAlias(); + private final Publication publication = new Publication(); private String validationRuleSchema; @@ -41,6 +43,22 @@ public class DgcConfigProperties { private SignerInformation signerInformation = new SignerInformation(); + @Getter + @Setter + public static class Publication { + private KeyStoreWithAlias keystore = new KeyStoreWithAlias(); + private Boolean enabled; + private Boolean synchronizeEnabled; + private String url; + private String amngrUid; + private String path; + private String user; + private String password; + private String archiveFilename; + private String signatureFilename; + private List notifyEmails = new ArrayList<>(); + } + @Getter @Setter public static class JrcConfig { @@ -61,7 +79,7 @@ public static class ProxyConfig { @Getter @Setter - public static class TrustAnchor { + public static class KeyStoreWithAlias { private String keyStorePath; private String keyStorePass; private String certificateAlias; diff --git a/src/main/java/eu/europa/ec/dgc/gateway/config/DgcKeyStore.java b/src/main/java/eu/europa/ec/dgc/gateway/config/DgcKeyStore.java index 98f7f975..d9e6461f 100644 --- a/src/main/java/eu/europa/ec/dgc/gateway/config/DgcKeyStore.java +++ b/src/main/java/eu/europa/ec/dgc/gateway/config/DgcKeyStore.java @@ -22,20 +22,21 @@ import java.io.File; import java.io.FileInputStream; -import java.io.IOException; +import java.io.FileNotFoundException; import java.io.InputStream; import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.cert.CertificateException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; @Configuration @RequiredArgsConstructor @Slf4j +@Profile("!test") public class DgcKeyStore { private final DgcConfigProperties dgcConfigProperties; @@ -44,14 +45,10 @@ public class DgcKeyStore { * Creates a KeyStore instance with keys for DGC TrustAnchor. * * @return KeyStore Instance - * @throws KeyStoreException if no implementation for the specified type found - * @throws IOException if there is an I/O or format problem with the keystore data - * @throws CertificateException if any of the certificates in the keystore could not be loaded - * @throws NoSuchAlgorithmException if the algorithm used to check the integrity of the keystore cannot be found */ @Bean - public KeyStore trustAnchorKeyStore() throws KeyStoreException, IOException, - CertificateException, NoSuchAlgorithmException { + @Qualifier("trustAnchor") + public KeyStore trustAnchorKeyStore() throws Exception { KeyStore keyStore = KeyStore.getInstance("JKS"); loadKeyStore( @@ -62,35 +59,41 @@ public KeyStore trustAnchorKeyStore() throws KeyStoreException, IOException, return keyStore; } - private void loadKeyStore(KeyStore keyStore, String path, char[] password) - throws CertificateException, NoSuchAlgorithmException, IOException { + /** + * Creates a KeyStore instance with keys for DGC Publication Feature. + * + * @return KeyStore Instance + */ + @Bean + @Qualifier("publication") + @ConditionalOnProperty("dgc.publication.enabled") + public KeyStore publicationKeyStore() throws Exception { + KeyStore keyStore = KeyStore.getInstance("JKS"); - InputStream fileStream; + loadKeyStore( + keyStore, + dgcConfigProperties.getPublication().getKeystore().getKeyStorePath(), + dgcConfigProperties.getPublication().getKeystore().getKeyStorePass().toCharArray()); - if (path.startsWith("classpath:")) { - String resourcePath = path.substring(10); - fileStream = getClass().getClassLoader().getResourceAsStream(resourcePath); - } else { - File file = new File(path); - fileStream = file.exists() ? getStream(path) : null; - } + return keyStore; + } - if (fileStream != null && fileStream.available() > 0) { + private void loadKeyStore(KeyStore keyStore, String path, char[] password) throws Exception { + try (InputStream fileStream = getStream(path)) { keyStore.load(fileStream, password); - fileStream.close(); - } else { - keyStore.load(null); - log.info("Could not find Keystore {}", path); + } catch (Exception e) { + log.error("Could not load Keystore {}", path); + throw e; } - } - private InputStream getStream(String path) { - try { - return new FileInputStream(path); - } catch (IOException e) { - log.info("Could not find Keystore {}", path); + private InputStream getStream(String path) throws FileNotFoundException { + if (path.startsWith("classpath:")) { + String resourcePath = path.substring(10); + return getClass().getClassLoader().getResourceAsStream(resourcePath); + } else { + File file = new File(path); + return file.exists() ? new FileInputStream(path) : null; } - return null; } } diff --git a/src/main/java/eu/europa/ec/dgc/gateway/model/AssetManagerSynchronizeResponseDto.java b/src/main/java/eu/europa/ec/dgc/gateway/model/AssetManagerSynchronizeResponseDto.java new file mode 100644 index 00000000..4271676a --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/model/AssetManagerSynchronizeResponseDto.java @@ -0,0 +1,76 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 - 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.model; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +public class AssetManagerSynchronizeResponseDto { + + /** + * Initialize Dto with values for embedded sub-classed. + */ + public AssetManagerSynchronizeResponseDto( + String status, int statusCode, String message, String path, String token) { + Ocs.Data data = new Ocs.Data(statusCode, message, token, path); + Ocs.Meta meta = new Ocs.Meta(status, statusCode, message); + + ocs = new Ocs(meta, data); + } + + private Ocs ocs; + + @Getter + @Setter + @NoArgsConstructor + @AllArgsConstructor + public static class Ocs { + + private Meta meta; + private Data data; + + @Getter + @Setter + @NoArgsConstructor + @AllArgsConstructor + public static class Meta { + private String status; + private Integer statuscode; + private String message; + } + + @Getter + @Setter + @NoArgsConstructor + @AllArgsConstructor + public static class Data { + private Integer statusCode; + private String statusMessage; + private String token; + private String path; + } + } +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/service/PublishingService.java b/src/main/java/eu/europa/ec/dgc/gateway/service/PublishingService.java new file mode 100644 index 00000000..0fb3d963 --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/service/PublishingService.java @@ -0,0 +1,328 @@ +package eu.europa.ec.dgc.gateway.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import eu.europa.ec.dgc.gateway.client.AssetManagerClient; +import eu.europa.ec.dgc.gateway.config.DgcConfigProperties; +import eu.europa.ec.dgc.gateway.entity.SignerInformationEntity; +import eu.europa.ec.dgc.gateway.entity.TrustedPartyEntity; +import eu.europa.ec.dgc.gateway.model.AssetManagerSynchronizeResponseDto; +import eu.europa.ec.dgc.signing.SignedByteArrayMessageBuilder; +import eu.europa.ec.dgc.utils.CertificateUtils; +import feign.FeignException; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Base64; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import net.javacrumbs.shedlock.spring.annotation.SchedulerLock; +import org.bouncycastle.cert.X509CertificateHolder; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.http.ResponseEntity; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.util.ResourceUtils; + +@Service +@RequiredArgsConstructor +@Slf4j +@ConditionalOnProperty("dgc.publication.enabled") +public class PublishingService { + + private final TrustedPartyService trustedPartyService; + + private final SignerInformationService signerInformationService; + + private final CertificateUtils certificateUtils; + + private final DgcConfigProperties properties; + + private final AssetManagerClient assetManagerClient; + + private final ObjectMapper objectMapper; + + @Qualifier("publication") + private final KeyStore publicationKeyStore; + + private static final String PEM_BEGIN = "-----BEGIN CERTIFICATE-----"; + private static final String PEM_END = "-----END CERTIFICATE-----"; + private static final String LINE_SEPERATOR = "\n"; + + /** + * Method to generate and upload an archive with all onboarded CSCA and DSC. + */ + @Scheduled(cron = "0 0 3 * * *") + @SchedulerLock(name = "publishing_generate_zip") + public void publishGatewayData() { + log.info("Start publishing of packed Gateway data"); + + byte[] zip = generateArchive(); + byte[] signature = calculateSignature(zip); + uploadGatewayData(zip, signature); + + log.info("Finished publishing of packed Gateway data"); + } + + private byte[] generateArchive() { + log.debug("Generating Archive for Certificate Publication"); + + log.debug("Fetching TrustedParty CSCA Certificates"); + List cscaTrustedParties = + trustedPartyService.getCertificates(TrustedPartyEntity.CertificateType.CSCA); + log.debug("Got {} trustedParty CSCA Certificates", cscaTrustedParties.size()); + + log.debug("Fetching SignerInformation"); + List signerInformationList = signerInformationService.getSignerInformation(); + log.debug("Fetched {} trusted SignerInformation", signerInformationList.size()); + + try ( + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + ZipOutputStream zipOutputStream = new ZipOutputStream(byteArrayOutputStream) + ) { + /* + * Add Static Files + */ + addFileToZip(zipOutputStream, "Readme.txt", getClasspathFileContent("Readme.txt")); + addFileToZip(zipOutputStream, "License.txt", getClasspathFileContent("License.txt")); + addFileToZip(zipOutputStream, "Version.txt", getVersionFileContent()); + + /* + * Add DSC + */ + addDirectoryToZip(zipOutputStream, "DSC/"); + addDirectoryToZip(zipOutputStream, "DSC/DCC/"); + signerInformationList.stream() + .map(SignerInformationEntity::getCountry) + .distinct() + .forEach(country -> addDirectoryToZip(zipOutputStream, "DSC/DCC/" + country + "/")); + + signerInformationList.forEach(signerInformation -> { + X509Certificate cert = signerInformationService.getX509CertificateFromEntity(signerInformation); + String thumbprint = certificateUtils.getCertThumbprint(cert); + byte[] pem = getPemBytes(Base64.getDecoder().decode(signerInformation.getRawData())); + String filename = "DSC/DCC/" + signerInformation.getCountry() + "/" + thumbprint + ".pem"; + addFileToZip(zipOutputStream, filename, pem); + }); + + /* + * Add CSCA + */ + addDirectoryToZip(zipOutputStream, "CSCA/"); + addDirectoryToZip(zipOutputStream, "CSCA/DCC/"); + cscaTrustedParties.stream() + .map(TrustedPartyEntity::getCountry) + .distinct() + .forEach(country -> addDirectoryToZip(zipOutputStream, "CSCA/DCC/" + country + "/")); + + cscaTrustedParties.forEach(trustedPartyEntity -> { + X509Certificate cert = trustedPartyService.getX509CertificateFromEntity(trustedPartyEntity); + String thumbprint = certificateUtils.getCertThumbprint(cert); + byte[] pem = getPemBytes(Base64.getDecoder().decode(trustedPartyEntity.getRawData())); + String filename = "CSCA/DCC/" + trustedPartyEntity.getCountry() + "/" + thumbprint + ".pem"; + addFileToZip(zipOutputStream, filename, pem); + }); + + log.info("Generated Publication Archive with {} CSCA and {} DSC certificates", + cscaTrustedParties.size(), signerInformationList.size()); + + zipOutputStream.finish(); + + return byteArrayOutputStream.toByteArray(); + } catch (IOException e) { + log.error("Failed to create ZIP Archive."); + log.debug("Failed to create ZIP Archive.", e); + return null; + } + } + + private byte[] calculateSignature(byte[] zip) { + log.debug("Signing created zip archive"); + PrivateKey privateKey; + X509CertificateHolder signingCertificate; + + try { + privateKey = (PrivateKey) publicationKeyStore.getKey( + properties.getPublication().getKeystore().getCertificateAlias(), + properties.getPublication().getKeystore().getKeyStorePass().toCharArray() + ); + + signingCertificate = certificateUtils.convertCertificate( + (X509Certificate) publicationKeyStore.getCertificate( + properties.getPublication().getKeystore().getCertificateAlias())); + } catch (Exception e) { + log.error("Failed to load Publication Signing KeyPair from KeyStore: {}", e.getClass().getName()); + log.debug("Failed to load Publication Signing KeyPair from KeyStore", e); + return null; + } + + return Base64.getEncoder().encode(new SignedByteArrayMessageBuilder() + .withPayload(zip) + .withSigningCertificate(signingCertificate, privateKey) + .build(true)); + } + + private void uploadGatewayData(byte[] zip, byte[] signature) { + String archiveFilename = properties.getPublication().getArchiveFilename(); + String signatureFilename = properties.getPublication().getSignatureFilename(); + + log.info("Uploading DGCG Publication Archive: {}, {}", archiveFilename, signatureFilename); + + try { + ResponseEntity zipUploadResponse = assetManagerClient.uploadFile(getAuthHeader(), + properties.getPublication().getAmngrUid(), properties.getPublication().getPath(), archiveFilename, zip); + + if (zipUploadResponse.getStatusCode().is2xxSuccessful()) { + log.info("Upload of ZIP Archive was successful."); + } else { + log.error("Failed to Upload ZIP Archive: {}", zipUploadResponse.getStatusCode()); + return; + } + } catch (FeignException.FeignServerException e) { + log.error("Failed to Upload ZIP Archive: {}", e.status()); + return; + } + + if (signature != null) { + try { + ResponseEntity signatureUploadResponse = assetManagerClient.uploadFile(getAuthHeader(), + properties.getPublication().getAmngrUid(), properties.getPublication().getPath(), signatureFilename, + signature); + + if (signatureUploadResponse.getStatusCode().is2xxSuccessful()) { + log.info("Upload of Signature file was successful."); + } else { + log.error("Failed to Upload Signature file: {}", signatureUploadResponse.getStatusCode()); + return; + } + } catch (FeignException.FeignServerException e) { + log.error("Failed to Upload Signature file: {}", e.status()); + return; + } + } else { + log.info("Skipping Upload of Signature because it could not be created."); + } + + log.info("All files uploaded, start synchronize process"); + + if (!properties.getPublication().getSynchronizeEnabled()) { + log.info("Synchronizing Files is disabled."); + return; + } + + try { + ResponseEntity synchronizeResponse = assetManagerClient.synchronize( + getAuthHeader(), "true", + new AssetManagerClient.SynchronizeFormData( + properties.getPublication().getPath(), + String.join(",", archiveFilename, signatureFilename), + String.join(",", properties.getPublication().getNotifyEmails()))); + + if (synchronizeResponse.getBody() != null && synchronizeResponse.getStatusCode().is2xxSuccessful()) { + if (synchronizeResponse.getBody().getOcs().getData().getStatusCode() == 200 + && synchronizeResponse.getBody().getOcs().getMeta().getStatuscode() == 200) { + + log.info("Successfully triggered synchronization from acc to prd."); + } else { + log.error("Failed to trigger synchronization from acc to prd: {}, {}, {}", + synchronizeResponse.getStatusCode(), + synchronizeResponse.getBody().getOcs().getData().getStatusMessage(), + synchronizeResponse.getBody().getOcs().getMeta().getMessage()); + } + } else { + log.error("Failed to trigger synchronization from acc to prd: {}, {}, {}", + synchronizeResponse.getStatusCode(), synchronizeResponse.getBody(), + objectMapper.writeValueAsString(synchronizeResponse.getBody())); + return; + } + } catch (FeignException e) { + log.error("Failed to trigger synchronization from acc to prd: {}", e.status()); + return; + } catch (JsonProcessingException e) { + log.error("Failed to trigger synchronization from acc to prd: {}", e.getMessage()); + return; + } + + log.info("Upload and Synchronize successful"); + } + + private String getAuthHeader() { + String header = "Basic "; + header += Base64.getEncoder().encodeToString((properties.getPublication().getUser() + ":" + + properties.getPublication().getPassword()).getBytes(StandardCharsets.UTF_8)); + return header; + } + + private byte[] getVersionFileContent() { + String fileContent = + "DGCG Data Export" + + LINE_SEPERATOR + LINE_SEPERATOR + + "Export Timestamp: " + + ZonedDateTime.now().format(DateTimeFormatter.ISO_OFFSET_DATE_TIME) + + LINE_SEPERATOR; + + return fileContent.getBytes(StandardCharsets.UTF_8); + } + + private byte[] getClasspathFileContent(String filename) { + log.debug("Reading file {} from classpath", filename); + File file; + try { + file = ResourceUtils.getFile("classpath:publication/" + filename); + } catch (IOException e) { + log.error("Failed to get file {} from classpath.", filename); + log.debug("Failed to get file {} from classpath.", filename, e); + return new byte[0]; + } + + try (FileInputStream fileInputStream = new FileInputStream(file)) { + return fileInputStream.readAllBytes(); + } catch (IOException e) { + log.error("Failed to read content from file {} from classpath", filename); + log.debug("Failed to read content from file {} from classpath.", filename, e); + return new byte[0]; + } + } + + private void addDirectoryToZip(ZipOutputStream zipOutputStream, String directory) { + log.debug("Adding directory {} to publication archive", directory); + try { + zipOutputStream.putNextEntry(new ZipEntry(directory)); + zipOutputStream.closeEntry(); + } catch (IOException e) { + log.error("Failed to add directory {} to publication archive.", directory); + } + } + + private void addFileToZip(ZipOutputStream zipOutputStream, String filename, byte[] bytes) { + log.debug("Adding file {} ({} Bytes) to publication archive", filename, bytes.length); + try { + zipOutputStream.putNextEntry(new ZipEntry(filename)); + zipOutputStream.write(bytes); + zipOutputStream.closeEntry(); + } catch (IOException e) { + log.error("Failed to add file {} to publication archive.", filename); + } + } + + private byte[] getPemBytes(byte[] certRawData) { + String pem = PEM_BEGIN + LINE_SEPERATOR; + pem += Base64.getMimeEncoder(64, LINE_SEPERATOR.getBytes(StandardCharsets.UTF_8)) + .encodeToString(certRawData); + pem += LINE_SEPERATOR + PEM_END + LINE_SEPERATOR; + + return pem.getBytes(StandardCharsets.UTF_8); + } + +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/service/TrustedIssuerService.java b/src/main/java/eu/europa/ec/dgc/gateway/service/TrustedIssuerService.java index 74163317..2879559d 100644 --- a/src/main/java/eu/europa/ec/dgc/gateway/service/TrustedIssuerService.java +++ b/src/main/java/eu/europa/ec/dgc/gateway/service/TrustedIssuerService.java @@ -40,6 +40,7 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.bouncycastle.cert.X509CertificateHolder; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Service; @Slf4j @@ -48,11 +49,18 @@ public class TrustedIssuerService { private static final String MDC_PROP_ISSUER_UUID = "issuerUuid"; + private static final String MDC_PROP_PARSER_STATE = "parserState"; + private static final String HASH_SEPARATOR = ";"; + private final TrustedIssuerRepository trustedIssuerRepository; + + @Qualifier("trustAnchor") private final KeyStore trustAnchorKeyStore; + private final DgcConfigProperties dgcConfigProperties; + private final CertificateUtils certificateUtils; /** diff --git a/src/main/java/eu/europa/ec/dgc/gateway/service/TrustedPartyService.java b/src/main/java/eu/europa/ec/dgc/gateway/service/TrustedPartyService.java index 1f2e6066..9d5d4c61 100644 --- a/src/main/java/eu/europa/ec/dgc/gateway/service/TrustedPartyService.java +++ b/src/main/java/eu/europa/ec/dgc/gateway/service/TrustedPartyService.java @@ -41,6 +41,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.bouncycastle.cert.X509CertificateHolder; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Service; @Slf4j @@ -50,9 +51,14 @@ public class TrustedPartyService { private static final String MDC_PROP_CERT_THUMBPRINT = "certVerifyThumbprint"; private static final String MDC_PROP_PARSER_STATE = "parserState"; + private final TrustedPartyRepository trustedPartyRepository; + + @Qualifier("trustAnchor") private final KeyStore trustAnchorKeyStore; + private final DgcConfigProperties dgcConfigProperties; + private final CertificateUtils certificateUtils; /** diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index d1b249f3..76ffa734 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -39,6 +39,21 @@ dgc: keyStorePath: /ec/prod/app/san/dgc/dgc-ta.jks keyStorePass: dgc-p4ssw0rd certificateAlias: dgc_trust_anchor + publication: + enbaled: false + synchronizeEnabled: false + keystore: + keyStorePath: /ec/prod/app/san/dgcg/dgc-publication.jks + keyStorePass: dgc-p4ssw0rd + certificateAlias: dgc_publication + url: https://example.org/asset-manager + amngr-uid: 00000000-0000-0000-0000-000000000000 + path: /assets + user: ${https.proxyUser} + password: ${https.proxyPassword} + notifyEmails: [ ] + archiveFilename: dcc_trustlist.zip + signatureFilename: dcc_trustlist.zip.sig.txt cert-auth: header-fields: thumbprint: X-SSL-Client-SHA256 diff --git a/src/main/resources/publication/License.txt b/src/main/resources/publication/License.txt new file mode 100644 index 00000000..bfff2005 --- /dev/null +++ b/src/main/resources/publication/License.txt @@ -0,0 +1,44 @@ +DATA LICENSE for the database of public keys of digital signer certificates, originally +available from https://ec.europa.eu/assets/eu-dcc/dcc_database.zip + +1. This data license shall govern the use of the database of public keys of digital signer certificates, originally +available from https://ec.europa.eu/assets/eu-dcc/dcc_database.zip. The use of the copyright and database right +material provided in this database (the "Data") indicates that you accept the terms and conditions of this license. +You may not amend this license under any circumstances. + +2. Licensor of this database is the European Commission. The Licensor grants you a worldwide, royalty-free, perpetual, +non-exclusive licence to use the Data subject to the conditions below. + +3. This license grants the exercise of the following rights regarding the Data, subject to the observation of the +conditions in sections 4: + + a) to copy, publish, distribute and transmit the Data; + b) to incorporate the data in commercially and non-commercially available products and/or services. + +These rights are restricted to the Data itself and shall not be construed to extend beyond implicitly. + +4. Any exercise of the rights granted in section 3 shall be conditional upon the observance and fulfilment of the +following obligations: + + a) to acknowledge the use of the database and attribute the copyright and database rights of the European + Commission in an appropriate manner and with an appropriate notice; + b) to acknowledge and to notify in an appropriate manner and with an appropriate notice that the use of the + database does not constitute endorsement of any products and/or services by the European commission; + c) to provide the text of this license in an appropriate manner and with an appropriate notice to any intended + recipient of the Data. + +You may use the following notices at your discretion: + + "Contains data provided by the European Commission under the following license: + https://ec.europa.eu/assets/eu-dcc/dcc_database.zip/license.txt. + The European Commission does not endorse any products and/or services by providing this data." + +5. Foremost, this license is governed by the law of the European Union and additionally by the law of the Member State +of the European Union you or an intended recipient resides in. If you or any intended recipient do not reside in a +Member State of the European Union, the laws of the Kingdom of Belgium shall apply in addition to the law of the +European Union. + +6. The European Commission shall not be liable in any capacity for any use of the Data by third parties. It shall only +be liable for any deliberate and intentional damage caused by a deliberate and intentional default of the Data under any +statutory law applicable according to section 5. Any further liability, representation, warranty or obligation of +the European Commission is explicitly excluded. diff --git a/src/main/resources/publication/Readme.txt b/src/main/resources/publication/Readme.txt new file mode 100644 index 00000000..ed1627ab --- /dev/null +++ b/src/main/resources/publication/Readme.txt @@ -0,0 +1,63 @@ +EU Digital Covid Certificates Signer Certificates Archive + +The archive is published under the license described in License.txt - Please be aware of this license when distributing +this archive or contents of it. + + +Content: + + 1. Intention + 2. Structure of archive + 3. How to verify integrity of DCC + 4. How to verify integrity of this archive + +1. Intention + The content of this archive can be used to verify that a Digital Covid Certificate (DCC) was issued by an authorized + issuer. + +2. Structure of archive + This archive contains two different certificate types: Digital Signer Certificate (DSC) and Country Signing + Certificate Authority (CSCA). The archive is structured by certificate type (DSC or CSCA), domain (currently just + DCC) and the 2-digit country code. The certificates are encoded as PKCS#8 saved in pem files named by there + certificate SHA-256 thumbprint. + + CSCA + ∟ DCC + ∟ CC + ∟ 6d3644ee122d1263267c6f42974c42acc3ca1a08675264fe34360239b5605e0e.pem + DSC + ∟ DCC + ∟ CC + ∟ 6493815d2ecfdbab6507e541a5f53e68b03d057b45e16d39b35b91ee61f78ab0.pem + +3. How to verify integrity of DCC + A. Extract Signature from DCC + B. Get KID from DCC, Convert Base64 string to hex, search for DSC file starting with the resulting hex string + C. Verify that DCC was signed by the DSC + D. Verify that the matching DSC was issued by one of the CSCA + +4. How to verify integrity of this archive + This archive and all of its contents are signed by a certificate of the European Commission. + The signature file will be seperatly distributed. You can find it on the same download page as this archive + (https://ec.europa.eu/assets/eu-dcc/dcc_database.zip.sig.txt). The signature file contains a base64 encoded + CMS-Message with detached payload (PKCS#7). + + There are two options to verify the integrity of the archive: + + A: DGC-CLI (recommended, needs DGC-CLI to be installed) + - Install DGC-CLI: https://github.com/eu-digital-green-certificates/dgc-cli#installation + - Verify integrity + dgc signing validate-file -i dcc_database.zip.sig.txt -p dcc_database.zip + + The command will output only the verification result and the subject and thumbprint of the signer certificate. + The thumbprint should be checked against the published signer certificate. + + B: OpenSSL (Needs OpenSSL CLI to be installed) + - Convert signature file from base64 encoded to plain DER file + openssl base64 -a -A -d -in dcc_database.zip.sig.txt -out dcc_database.zip.sig.der + - Verify integrity + openssl cms -verify -in dcc_database.zip.sig.der -inform DER -content dcc_database.zip -binary -CAfile eu_signer.pem + + The output of the verify command contains the whole binary data of the zip file. + At the end of the output you should find: "Verification successful" + diff --git a/src/test/java/eu/europa/ec/dgc/gateway/publishing/ArchivePublishingTest.java b/src/test/java/eu/europa/ec/dgc/gateway/publishing/ArchivePublishingTest.java new file mode 100644 index 00000000..0c732149 --- /dev/null +++ b/src/test/java/eu/europa/ec/dgc/gateway/publishing/ArchivePublishingTest.java @@ -0,0 +1,295 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 - 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.publishing; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import eu.europa.ec.dgc.gateway.client.AssetManagerClient; +import eu.europa.ec.dgc.gateway.config.DgcConfigProperties; +import eu.europa.ec.dgc.gateway.entity.TrustedPartyEntity; +import eu.europa.ec.dgc.gateway.model.AssetManagerSynchronizeResponseDto; +import eu.europa.ec.dgc.gateway.repository.SignerInformationRepository; +import eu.europa.ec.dgc.gateway.repository.TrustedPartyRepository; +import eu.europa.ec.dgc.gateway.service.PublishingService; +import eu.europa.ec.dgc.gateway.testdata.CertificateTestUtils; +import eu.europa.ec.dgc.gateway.testdata.DgcTestKeyStore; +import eu.europa.ec.dgc.gateway.testdata.SignerInformationTestHelper; +import eu.europa.ec.dgc.gateway.testdata.TrustedPartyTestHelper; +import eu.europa.ec.dgc.signing.SignedByteArrayMessageParser; +import eu.europa.ec.dgc.signing.SignedMessageParser; +import eu.europa.ec.dgc.utils.CertificateUtils; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.security.KeyPairGenerator; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.FileUtils; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.openssl.PEMParser; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; +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; +import org.springframework.util.ResourceUtils; + +@SpringBootTest(properties = { + "dgc.publication.enabled=true", + "dgc.publication.synchronizeEnabled=true", + "dgc.publication.user=user", + "dgc.publication.password=pass", + "dgc.publication.amngruid=uid", + "dgc.publication.path=path/a/b", + "dgc.publication.archiveFilename=db.zip", + "dgc.publication.signatureFilename=db.zip.sig.txt", + "dgc.publication.notifyEmails[0]=u1@c1.de", + "dgc.publication.notifyEmails[1]=u1@c2.de" +}) +@Slf4j +public class ArchivePublishingTest { + + @MockBean + AssetManagerClient assetManagerClientMock; + + @Autowired + TrustedPartyRepository trustedPartyRepository; + + @Autowired + SignerInformationRepository signerInformationRepository; + + @Autowired + PublishingService publishingService; + + @Autowired + AssetManagerClient assetManagerClient; + + @Autowired + TrustedPartyTestHelper trustedPartyTestHelper; + + @Autowired + SignerInformationTestHelper signerInformationTestHelper; + + @Autowired + DgcTestKeyStore dgcTestKeyStore; + + @Autowired + CertificateUtils certificateUtils; + + @Autowired + DgcConfigProperties properties; + + private static final String expectedAuthHeader = + "Basic " + Base64.getEncoder().encodeToString("user:pass".getBytes(StandardCharsets.UTF_8)); + private static final String expectedUid = "uid"; + private static final String expectedPath = "path/a/b"; + private static final String expectedArchiveName = "db.zip"; + private static final String expectedSignatureName = "db.zip.sig.txt"; + + private X509Certificate csca1, csca2, csca3, csca4; + private X509Certificate dsc1, dsc2, dsc3, dsc4; + + @BeforeEach + public void setup() throws Exception { + trustedPartyRepository.deleteAll(); + signerInformationRepository.deleteAll(); + + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("ec"); + + csca1 = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.CSCA, "C1"); + csca2 = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.CSCA, "C2"); + csca3 = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.CSCA, "C3"); + csca4 = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.CSCA, "C4"); + + dsc1 = CertificateTestUtils.generateCertificate(keyPairGenerator.generateKeyPair(), "C1", + "DSC C1", csca1, trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.CSCA, "C1")); + dsc2 = CertificateTestUtils.generateCertificate(keyPairGenerator.generateKeyPair(), "C2", + "DSC C2", csca2, trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.CSCA, "C2")); + dsc3 = CertificateTestUtils.generateCertificate(keyPairGenerator.generateKeyPair(), "C3", + "DSC C3", csca3, trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.CSCA, "C3")); + dsc4 = CertificateTestUtils.generateCertificate(keyPairGenerator.generateKeyPair(), "C4", + "DSC C4", csca4, trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.CSCA, "C4")); + + signerInformationTestHelper.createSignerInformationInDB("C1", "XXX", dsc1, ZonedDateTime.now()); + signerInformationTestHelper.createSignerInformationInDB("C2", "XXX", dsc2, ZonedDateTime.now()); + signerInformationTestHelper.createSignerInformationInDB("C3", "XXX", dsc3, ZonedDateTime.now()); + signerInformationTestHelper.createSignerInformationInDB("C4", "XXX", dsc4, ZonedDateTime.now()); + } + + @Test + public void testArchiveContainsRequiredFiles() throws Exception { + + ArgumentCaptor uploadArchiveArgumentCaptor = ArgumentCaptor.forClass(byte[].class); + ArgumentCaptor uploadSignatureArgumentCaptor = ArgumentCaptor.forClass(byte[].class); + ArgumentCaptor synchronizeFormDataArgumentCaptor = ArgumentCaptor.forClass(AssetManagerClient.SynchronizeFormData.class); + + when(assetManagerClientMock.uploadFile(eq(expectedAuthHeader), eq(expectedUid), eq(expectedPath), eq(expectedArchiveName), uploadArchiveArgumentCaptor.capture())) + .thenReturn(ResponseEntity.ok(null)); + + when(assetManagerClientMock.uploadFile(eq(expectedAuthHeader), eq(expectedUid), eq(expectedPath), eq(expectedSignatureName), uploadSignatureArgumentCaptor.capture())) + .thenReturn(ResponseEntity.ok(null)); + + when(assetManagerClientMock.synchronize(eq(expectedAuthHeader), eq("true"), synchronizeFormDataArgumentCaptor.capture())) + .thenReturn(ResponseEntity.ok(new AssetManagerSynchronizeResponseDto("OK", 200, "Message", expectedPath, "token"))); + + publishingService.publishGatewayData(); + + verify(assetManagerClientMock).uploadFile(eq(expectedAuthHeader), eq(expectedUid), eq(expectedPath), eq(expectedArchiveName), any()); + verify(assetManagerClientMock).uploadFile(eq(expectedAuthHeader), eq(expectedUid), eq(expectedPath), eq(expectedSignatureName), any()); + verify(assetManagerClientMock).synchronize(eq(expectedAuthHeader), eq("true"), any()); + + Assertions.assertNotNull(uploadArchiveArgumentCaptor.getValue()); + Assertions.assertNotNull(uploadSignatureArgumentCaptor.getValue()); + Assertions.assertNotNull(synchronizeFormDataArgumentCaptor.getValue()); + + Assertions.assertEquals(expectedPath, synchronizeFormDataArgumentCaptor.getValue().getPath()); + Assertions.assertArrayEquals(new String[]{expectedArchiveName, expectedSignatureName}, synchronizeFormDataArgumentCaptor.getValue().getNodeList().split(",")); + Assertions.assertArrayEquals(new String[]{"u1@c1.de", "u1@c2.de"}, synchronizeFormDataArgumentCaptor.getValue().getNotifyEmails().split(",")); + + + Map archiveContent = readZipFile(uploadArchiveArgumentCaptor.getValue()); + Assertions.assertEquals(11, archiveContent.size()); + + /* + * Check for Static files. + */ + Assertions.assertTrue(archiveContent.containsKey("Readme.txt")); + Assertions.assertArrayEquals(FileUtils.readFileToByteArray(ResourceUtils.getFile("classpath:publication/Readme.txt")), archiveContent.get("Readme.txt")); + + Assertions.assertTrue(archiveContent.containsKey("License.txt")); + Assertions.assertArrayEquals(FileUtils.readFileToByteArray(ResourceUtils.getFile("classpath:publication/License.txt")), archiveContent.get("License.txt")); + + /* + * Check for Version file + */ + Assertions.assertTrue(archiveContent.containsKey("Version.txt")); + String versionFileContent = new String(archiveContent.get("Version.txt"), StandardCharsets.UTF_8); + ZonedDateTime parsedTimestamp = ZonedDateTime.parse(versionFileContent.substring(versionFileContent.indexOf(":") + 2).trim(), DateTimeFormatter.ISO_OFFSET_DATE_TIME); + Assertions.assertTrue(ZonedDateTime.now().until(parsedTimestamp, ChronoUnit.SECONDS) < 10); + + /* + * Check for CSCA + */ + Assertions.assertTrue((archiveContent.containsKey("CSCA/DCC/C1/" + certificateUtils.getCertThumbprint(csca1) + ".pem"))); + checkPemFile(csca1, archiveContent.get("CSCA/DCC/C1/" + certificateUtils.getCertThumbprint(csca1) + ".pem")); + + Assertions.assertTrue((archiveContent.containsKey("CSCA/DCC/C2/" + certificateUtils.getCertThumbprint(csca2) + ".pem"))); + checkPemFile(csca2, archiveContent.get("CSCA/DCC/C2/" + certificateUtils.getCertThumbprint(csca2) + ".pem")); + + Assertions.assertTrue((archiveContent.containsKey("CSCA/DCC/C3/" + certificateUtils.getCertThumbprint(csca3) + ".pem"))); + checkPemFile(csca3, archiveContent.get("CSCA/DCC/C3/" + certificateUtils.getCertThumbprint(csca3) + ".pem")); + + Assertions.assertTrue((archiveContent.containsKey("CSCA/DCC/C4/" + certificateUtils.getCertThumbprint(csca4) + ".pem"))); + checkPemFile(csca4, archiveContent.get("CSCA/DCC/C4/" + certificateUtils.getCertThumbprint(csca4) + ".pem")); + + /* + * Check for DSC + */ + Assertions.assertTrue((archiveContent.containsKey("DSC/DCC/C1/" + certificateUtils.getCertThumbprint(dsc1) + ".pem"))); + checkPemFile(dsc1, archiveContent.get("DSC/DCC/C1/" + certificateUtils.getCertThumbprint(dsc1) + ".pem")); + + Assertions.assertTrue((archiveContent.containsKey("DSC/DCC/C2/" + certificateUtils.getCertThumbprint(dsc2) + ".pem"))); + checkPemFile(dsc2, archiveContent.get("DSC/DCC/C2/" + certificateUtils.getCertThumbprint(dsc2) + ".pem")); + + Assertions.assertTrue((archiveContent.containsKey("DSC/DCC/C3/" + certificateUtils.getCertThumbprint(dsc3) + ".pem"))); + checkPemFile(dsc3, archiveContent.get("DSC/DCC/C3/" + certificateUtils.getCertThumbprint(dsc3) + ".pem")); + + Assertions.assertTrue((archiveContent.containsKey("DSC/DCC/C4/" + certificateUtils.getCertThumbprint(dsc4) + ".pem"))); + checkPemFile(dsc4, archiveContent.get("DSC/DCC/C4/" + certificateUtils.getCertThumbprint(dsc4) + ".pem")); + + /* + * Check Signature + */ + SignedByteArrayMessageParser parser = new SignedByteArrayMessageParser(uploadSignatureArgumentCaptor.getValue(), Base64.getEncoder().encode(uploadArchiveArgumentCaptor.getValue())); + Assertions.assertEquals(SignedMessageParser.ParserState.SUCCESS, parser.getParserState()); + Assertions.assertArrayEquals(dgcTestKeyStore.getPublicationSigner().getEncoded(), parser.getSigningCertificate().getEncoded()); + Assertions.assertTrue(parser.isSignatureVerified()); + } + + @Test + public void testSynchronizeDisabled() { + + when(assetManagerClientMock.uploadFile(eq(expectedAuthHeader), eq(expectedUid), eq(expectedPath), eq(expectedArchiveName), any())) + .thenReturn(ResponseEntity.ok(null)); + + when(assetManagerClientMock.uploadFile(eq(expectedAuthHeader), eq(expectedUid), eq(expectedPath), eq(expectedSignatureName), any())) + .thenReturn(ResponseEntity.ok(null)); + + properties.getPublication().setSynchronizeEnabled(false); + + publishingService.publishGatewayData(); + + properties.getPublication().setSynchronizeEnabled(true); + + verify(assetManagerClientMock).uploadFile(eq(expectedAuthHeader), eq(expectedUid), eq(expectedPath), eq(expectedArchiveName), any()); + verify(assetManagerClientMock).uploadFile(eq(expectedAuthHeader), eq(expectedUid), eq(expectedPath), eq(expectedSignatureName), any()); + verify(assetManagerClientMock, Mockito.never()).synchronize(eq(expectedAuthHeader), eq("true"), any()); + } + + private void checkPemFile(X509Certificate expected, byte[] pemFile) throws IOException, CertificateEncodingException { + try ( + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(pemFile); + InputStreamReader inputStreamReader = new InputStreamReader(byteArrayInputStream); + PEMParser pemParser = new PEMParser(inputStreamReader) + ) { + Object object = pemParser.readObject(); + Assertions.assertTrue(object instanceof X509CertificateHolder); + + X509CertificateHolder cert = (X509CertificateHolder) object; + Assertions.assertArrayEquals(expected.getEncoded(), cert.getEncoded()); + } + } + + private Map readZipFile(byte[] zipFile) throws IOException { + Map fileMap = new HashMap<>(); + + try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(zipFile); + ZipInputStream zipInputStream = new ZipInputStream(byteArrayInputStream)) { + + ZipEntry zipEntry; + while ((zipEntry = zipInputStream.getNextEntry()) != null) { + if (!zipEntry.isDirectory()) { + fileMap.put(zipEntry.getName(), zipInputStream.readAllBytes()); + } + } + } + + return fileMap; + } + +} diff --git a/src/test/java/eu/europa/ec/dgc/gateway/publishing/PublishingDisabledTest.java b/src/test/java/eu/europa/ec/dgc/gateway/publishing/PublishingDisabledTest.java new file mode 100644 index 00000000..01454103 --- /dev/null +++ b/src/test/java/eu/europa/ec/dgc/gateway/publishing/PublishingDisabledTest.java @@ -0,0 +1,51 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-gateway + * --- + * Copyright (C) 2021 - 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.publishing; + +import eu.europa.ec.dgc.gateway.client.AssetManagerClient; +import eu.europa.ec.dgc.gateway.client.AssetManagerClientConfig; +import eu.europa.ec.dgc.gateway.service.PublishingService; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ApplicationContext; + +@SpringBootTest(properties = { + "dgc.publication.enabled=false" +}) +@Slf4j +public class PublishingDisabledTest { + + @Autowired + ApplicationContext applicationContext; + + @ParameterizedTest + @ValueSource(classes = {PublishingService.class, AssetManagerClientConfig.class, AssetManagerClient.class}) + void testBeansAreNotCreated(Class clazz) { + Assertions.assertThrows(NoSuchBeanDefinitionException.class, + () -> applicationContext.getAutowireCapableBeanFactory().getBean(clazz)); + } + +} diff --git a/src/test/java/eu/europa/ec/dgc/gateway/testdata/DgcTestKeyStore.java b/src/test/java/eu/europa/ec/dgc/gateway/testdata/DgcTestKeyStore.java index 3c284635..eb3cf99c 100644 --- a/src/test/java/eu/europa/ec/dgc/gateway/testdata/DgcTestKeyStore.java +++ b/src/test/java/eu/europa/ec/dgc/gateway/testdata/DgcTestKeyStore.java @@ -20,6 +20,8 @@ package eu.europa.ec.dgc.gateway.testdata; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; @@ -31,13 +33,17 @@ import java.security.KeyStoreSpi; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; +import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import lombok.Getter; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Primary; +import org.springframework.context.annotation.Profile; +@Profile("!int-test") @TestConfiguration public class DgcTestKeyStore { @@ -49,14 +55,23 @@ public class DgcTestKeyStore { @Getter private final PrivateKey trustAnchorPrivateKey; + @Getter + private final X509Certificate publicationSigner; + + @Getter + private final PrivateKey publicationSignerPrivateKey; + public DgcTestKeyStore(DgcConfigProperties configProperties) throws Exception { this.configProperties = configProperties; KeyPair keyPair = KeyPairGenerator.getInstance("ec").generateKeyPair(); trustAnchorPrivateKey = keyPair.getPrivate(); - trustAnchor = CertificateTestUtils.generateCertificate(keyPair, "DE", "DGCG Test TrustAnchor"); + KeyPair keyPairPublication = KeyPairGenerator.getInstance("ec").generateKeyPair(); + publicationSignerPrivateKey = keyPairPublication.getPrivate(); + publicationSigner = CertificateTestUtils.generateCertificate(keyPairPublication, "DE", "DGCG Test Publication"); + } /** @@ -64,6 +79,7 @@ public DgcTestKeyStore(DgcConfigProperties configProperties) throws Exception { */ @Bean @Primary + @Qualifier("trustAnchor") public KeyStore testKeyStore() throws IOException, CertificateException, NoSuchAlgorithmException { KeyStoreSpi keyStoreSpiMock = mock(KeyStoreSpi.class); KeyStore keyStoreMock = new KeyStore(keyStoreSpiMock, null, "test") { @@ -76,4 +92,25 @@ public KeyStore testKeyStore() throws IOException, CertificateException, NoSuchA return keyStoreMock; } + /** + * Creates a KeyStore instance with keys for DGC. + */ + @Bean + @Primary + @Qualifier("publication") + public KeyStore testPublicationKeyStore() throws IOException, CertificateException, NoSuchAlgorithmException, UnrecoverableKeyException { + KeyStoreSpi keyStoreSpiMock = mock(KeyStoreSpi.class); + KeyStore keyStoreMock = new KeyStore(keyStoreSpiMock, null, "test") { + }; + keyStoreMock.load(null); + + doAnswer((x) -> publicationSigner) + .when(keyStoreSpiMock).engineGetCertificate(configProperties.getPublication().getKeystore().getCertificateAlias()); + + doAnswer((x) -> publicationSignerPrivateKey) + .when(keyStoreSpiMock).engineGetKey(eq(configProperties.getPublication().getKeystore().getCertificateAlias()), any()); + + return keyStoreMock; + } + } diff --git a/src/test/java/eu/europa/ec/dgc/gateway/testdata/TrustedIssuerTestHelper.java b/src/test/java/eu/europa/ec/dgc/gateway/testdata/TrustedIssuerTestHelper.java index 8dae186b..9a6ce805 100644 --- a/src/test/java/eu/europa/ec/dgc/gateway/testdata/TrustedIssuerTestHelper.java +++ b/src/test/java/eu/europa/ec/dgc/gateway/testdata/TrustedIssuerTestHelper.java @@ -23,10 +23,12 @@ import eu.europa.ec.dgc.gateway.entity.TrustedIssuerEntity; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; @Service @RequiredArgsConstructor +@Profile("!int-test") public class TrustedIssuerTestHelper { @Autowired diff --git a/src/test/java/eu/europa/ec/dgc/gateway/testdata/TrustedPartyTestHelper.java b/src/test/java/eu/europa/ec/dgc/gateway/testdata/TrustedPartyTestHelper.java index 257ba19c..29df2dae 100644 --- a/src/test/java/eu/europa/ec/dgc/gateway/testdata/TrustedPartyTestHelper.java +++ b/src/test/java/eu/europa/ec/dgc/gateway/testdata/TrustedPartyTestHelper.java @@ -36,10 +36,12 @@ import java.util.Map; import lombok.RequiredArgsConstructor; import org.bouncycastle.cert.X509CertificateHolder; +import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; @Service @RequiredArgsConstructor +@Profile("!int-test") public class TrustedPartyTestHelper { private final Map> hashMap = Map.of( diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index ad16a75e..c2ef5c0d 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -23,13 +23,19 @@ dgc: jrc: url: https://covid-19-diagnostics.jrc.ec.europa.eu/devices/hsc-common-recognition-rat validationRuleSchema: classpath:validation-rule.schema.json - dbencryption: - initVector: Ho^RDYDuGt0Ki`\x - password: G&B3zSk|fNE!.Pa9+Xv2kUYRx2zp|@=| trustAnchor: - keyStorePath: keystore/dgc-ta.jks - keyStorePass: dgc-p4ssw0rd - certificateAlias: dgc_trust_anchor + keyStorePath: classpath:ta_tst.jks + keyStorePass: dgcg-p4ssw0rd + certificateAlias: ta_tst + publication: + enabled: true + keystore: + keyStorePath: keystore/dgc-signer.jks + keyStorePass: dgc-p4ssw0rd + certificateAlias: dgc_tst_publication + url: https://example.org/asset-manager + user: user + password: password cert-auth: header-fields: thumbprint: X-SSL-Client-SHA256