diff --git a/owasp/suppressions.xml b/owasp/suppressions.xml
index 8f2dc9f9..6aa06da1 100644
--- a/owasp/suppressions.xml
+++ b/owasp/suppressions.xml
@@ -11,12 +11,20 @@
CVE-2012-5055
- see https://tomcat.apache.org/security-9.html#Apache_Tomcat_9.x_vulnerabilities vulnerability is fixed in tomcat 9.0.38
+ see https://tomcat.apache.org/security-9.html#Apache_Tomcat_9.x_vulnerabilities vulnerability is fixed in
+ tomcat 9.0.38
+
CVE-2020-13943
- see https://nvd.nist.gov/vuln/detail/CVE-2020-10693 vulnerability is fixed in hibernate validator 6.0.20/ 6.1.5 - we are using 6.2.0.FINAL
+ see https://nvd.nist.gov/vuln/detail/CVE-2020-10693 vulnerability is fixed in hibernate validator 6.0.20/
+ 6.1.5 - we are using 6.2.0.FINAL
+
CVE-2020-10693
-
+
+ H2 is only used for Unit Testing. Version 2.x includes major breaking changes.
+ CVE-2021-23463
+
+
diff --git a/pom.xml b/pom.xml
index 25ba413d..c60bc428 100644
--- a/pom.xml
+++ b/pom.xml
@@ -7,7 +7,7 @@
org.springframework.boot
spring-boot-starter-parent
- 2.5.7
+ 2.6.1
@@ -43,31 +43,26 @@
UTF-8
UTF-8
- 6.1.6
- 2.5.7
- 5.3.9
- 5.5.1
- 1.18.20
- 4.4.2
- 1.5.10
- 5.7.2
+ 6.5.0
+ 5.6.0
+ 1.18.22
+ 4.6.2
+ 1.6.0
1.4.2.Final
- 3.11.2
- 1.69
+ 4.1.0
+ 1.70
3.1.0
- 1.13.0
- 4.25.0
- 2020.0.3
+ 1.14.0
+ 4.30.0
+ 2021.0.0
1.7.32
- 2.15.0
+ 2.16.0
3.3.0
3.1.2
- 3.9.0.2155
+ 3.9.1.2184
0.8.7
1.7.0
- 1.7.2
- 3.0.0-M5
EU Digital Green Certificate Gateway Service / dgc-gateway
2021
@@ -154,7 +149,7 @@
eu.europa.ec.dgc
dgc-lib
- 1.1.3
+ 1.1.7
com.vdurmont
@@ -318,7 +313,7 @@
org.springframework.boot
spring-boot-maven-plugin
- ${spring.boot.version}
+ ${project.parent.version}
dev
5000
@@ -375,6 +370,7 @@
**/DgcGatewayApplication.java
**/restapi/dto/*
+ **/restapi/dto/**/*
**/restapi/mapper/*
**/repository/*
**/model/*
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 2239c14e..47fa3bc0 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
@@ -37,6 +37,8 @@ public class DgcConfigProperties {
private JrcConfig jrc = new JrcConfig();
+ private Revocation revocation = new Revocation();
+
@Getter
@Setter
public static class JrcConfig {
@@ -77,4 +79,10 @@ public static class HeaderFields {
private String distinguishedName;
}
}
+
+ @Getter
+ @Setter
+ public static class Revocation {
+ private int deleteThreshold = 14;
+ }
}
diff --git a/src/main/java/eu/europa/ec/dgc/gateway/entity/RevocationBatchEntity.java b/src/main/java/eu/europa/ec/dgc/gateway/entity/RevocationBatchEntity.java
new file mode 100644
index 00000000..d1508b55
--- /dev/null
+++ b/src/main/java/eu/europa/ec/dgc/gateway/entity/RevocationBatchEntity.java
@@ -0,0 +1,112 @@
+/*-
+ * ---license-start
+ * EU Digital Green Certificate Gateway Service / dgc-gateway
+ * ---
+ * 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.entity;
+
+import java.time.ZonedDateTime;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.EnumType;
+import javax.persistence.Enumerated;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.Index;
+import javax.persistence.Lob;
+import javax.persistence.Table;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+@Getter
+@Setter
+@Entity
+@Table(name = "revocation_batch", indexes = @Index(columnList = "batchId"))
+@AllArgsConstructor
+@NoArgsConstructor
+public class RevocationBatchEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "id")
+ private Long id;
+
+ /**
+ * ID of the Batch.
+ */
+ @Column(name = "batchId", nullable = false, length = 36, unique = true)
+ private String batchId;
+
+ /**
+ * ISO 3166 Alpha-2 Country Code.
+ * (plus code "EU" for administrative European Union entries).
+ */
+ @Column(name = "country", nullable = false, length = 2)
+ private String country;
+
+ /**
+ * Timestamp of the Batch when it was added or deleted.
+ */
+ @Column(name = "changed", nullable = false)
+ private ZonedDateTime changed = ZonedDateTime.now();
+
+ /**
+ * Timestamp when the Batch will expire.
+ */
+ @Column(name = "expires", nullable = false)
+ private ZonedDateTime expires;
+
+ /**
+ * Flag that indicates whether this batch was already deleted.
+ */
+ @Column(name = "deleted", nullable = false)
+ private Boolean deleted = false;
+
+ /**
+ * Type of Revocation Hashes.
+ */
+ @Column(name = "type", nullable = false)
+ @Enumerated(EnumType.STRING)
+ private RevocationHashType type;
+
+ /**
+ * The KID of the Key used to sign the CMS.
+ */
+ @Column(name = "kid", length = 12)
+ private String kid;
+
+ /**
+ * The Signed CMS with the batch.
+ */
+ @Column(name = "signed_batch", length = 1_024_000)
+ @Lob
+ private String signedBatch;
+
+ /**
+ * Available types of Hash.
+ */
+ public enum RevocationHashType {
+ SIGNATURE,
+ UCI,
+ COUNTRYCODEUCI
+ }
+
+}
diff --git a/src/main/java/eu/europa/ec/dgc/gateway/entity/RevocationBatchProjection.java b/src/main/java/eu/europa/ec/dgc/gateway/entity/RevocationBatchProjection.java
new file mode 100644
index 00000000..c5d306b8
--- /dev/null
+++ b/src/main/java/eu/europa/ec/dgc/gateway/entity/RevocationBatchProjection.java
@@ -0,0 +1,35 @@
+/*-
+ * ---license-start
+ * EU Digital Green Certificate Gateway Service / dgc-gateway
+ * ---
+ * 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.entity;
+
+import java.time.ZonedDateTime;
+
+public interface RevocationBatchProjection {
+
+ String getBatchId();
+
+ String getCountry();
+
+ ZonedDateTime getChanged();
+
+ Boolean getDeleted();
+
+}
diff --git a/src/main/java/eu/europa/ec/dgc/gateway/model/RevocationBatchDownload.java b/src/main/java/eu/europa/ec/dgc/gateway/model/RevocationBatchDownload.java
new file mode 100644
index 00000000..9e5830fc
--- /dev/null
+++ b/src/main/java/eu/europa/ec/dgc/gateway/model/RevocationBatchDownload.java
@@ -0,0 +1,35 @@
+/*-
+ * ---license-start
+ * EU Digital Green Certificate Gateway Service / dgc-gateway
+ * ---
+ * 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.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class RevocationBatchDownload {
+
+ private String batchId;
+
+ private String signedCms;
+}
diff --git a/src/main/java/eu/europa/ec/dgc/gateway/model/RevocationBatchList.java b/src/main/java/eu/europa/ec/dgc/gateway/model/RevocationBatchList.java
new file mode 100644
index 00000000..6ee1f364
--- /dev/null
+++ b/src/main/java/eu/europa/ec/dgc/gateway/model/RevocationBatchList.java
@@ -0,0 +1,47 @@
+/*-
+ * ---license-start
+ * EU Digital Green Certificate Gateway Service / dgc-gateway
+ * ---
+ * 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.model;
+
+import java.time.ZonedDateTime;
+import java.util.List;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+
+@Data
+public class RevocationBatchList {
+
+ private Boolean more;
+
+ private List batches;
+
+ @Data
+ @AllArgsConstructor
+ public static class RevocationBatchListItem {
+
+ private String batchId;
+
+ private String country;
+
+ private ZonedDateTime date;
+
+ private Boolean deleted;
+ }
+}
diff --git a/src/main/java/eu/europa/ec/dgc/gateway/repository/RevocationBatchRepository.java b/src/main/java/eu/europa/ec/dgc/gateway/repository/RevocationBatchRepository.java
new file mode 100644
index 00000000..40d02b10
--- /dev/null
+++ b/src/main/java/eu/europa/ec/dgc/gateway/repository/RevocationBatchRepository.java
@@ -0,0 +1,62 @@
+/*-
+ * ---license-start
+ * EU Digital Green Certificate Gateway Service / dgc-gateway
+ * ---
+ * 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.repository;
+
+import eu.europa.ec.dgc.gateway.entity.RevocationBatchEntity;
+import eu.europa.ec.dgc.gateway.entity.RevocationBatchProjection;
+import java.time.ZonedDateTime;
+import java.util.List;
+import java.util.Optional;
+import javax.transaction.Transactional;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Modifying;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+import org.springframework.stereotype.Repository;
+
+@Repository
+@Transactional
+public interface RevocationBatchRepository extends JpaRepository {
+
+ @Modifying
+ @Query("DELETE FROM RevocationBatchEntity r WHERE r.batchId = :batchId")
+ int deleteByBatchId(@Param("batchId") String batchId);
+
+ Optional getByBatchId(String batchId);
+
+ @Modifying
+ @Query("UPDATE RevocationBatchEntity r SET r.signedBatch = null, r.deleted = true, "
+ + "r.changed = current_timestamp WHERE r.batchId = :batchId")
+ int markBatchAsDeleted(@Param("batchId") String batchId);
+
+ @Modifying
+ @Query("UPDATE RevocationBatchEntity r SET r.signedBatch = null, r.deleted = true, "
+ + "r.changed = current_timestamp WHERE r.deleted = false AND r.expires < :threshold")
+ int markExpiredBatchesAsDeleted(@Param("threshold") ZonedDateTime threshold);
+
+ @Modifying
+ @Query("DELETE FROM RevocationBatchEntity r WHERE r.deleted = true AND r.changed < :threshold")
+ int deleteDeletedBatchesOlderThan(@Param("threshold") ZonedDateTime threshold);
+
+ List getAllByChangedGreaterThanEqualOrderByChangedAsc(ZonedDateTime date, Pageable page);
+
+}
diff --git a/src/main/java/eu/europa/ec/dgc/gateway/restapi/controller/CertificateRevocationListController.java b/src/main/java/eu/europa/ec/dgc/gateway/restapi/controller/CertificateRevocationListController.java
new file mode 100644
index 00000000..633b1346
--- /dev/null
+++ b/src/main/java/eu/europa/ec/dgc/gateway/restapi/controller/CertificateRevocationListController.java
@@ -0,0 +1,347 @@
+/*-
+ * ---license-start
+ * EU Digital Green Certificate Gateway Service / dgc-gateway
+ * ---
+ * 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.restapi.controller;
+
+import eu.europa.ec.dgc.gateway.config.OpenApiConfig;
+import eu.europa.ec.dgc.gateway.exception.DgcgResponseException;
+import eu.europa.ec.dgc.gateway.model.RevocationBatchDownload;
+import eu.europa.ec.dgc.gateway.restapi.converter.CmsStringMessageConverter;
+import eu.europa.ec.dgc.gateway.restapi.dto.SignedStringDto;
+import eu.europa.ec.dgc.gateway.restapi.dto.revocation.RevocationBatchDeleteRequestDto;
+import eu.europa.ec.dgc.gateway.restapi.dto.revocation.RevocationBatchDto;
+import eu.europa.ec.dgc.gateway.restapi.dto.revocation.RevocationBatchListDto;
+import eu.europa.ec.dgc.gateway.restapi.filter.CertificateAuthenticationFilter;
+import eu.europa.ec.dgc.gateway.restapi.filter.CertificateAuthenticationRequired;
+import eu.europa.ec.dgc.gateway.restapi.mapper.RevocationBatchMapper;
+import eu.europa.ec.dgc.gateway.service.RevocationListService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.enums.ParameterIn;
+import io.swagger.v3.oas.annotations.headers.Header;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.security.SecurityRequirement;
+import java.time.ZonedDateTime;
+import javax.validation.Valid;
+import javax.validation.constraints.Pattern;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.format.annotation.DateTimeFormat;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestAttribute;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestHeader;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/revocation-list")
+@RequiredArgsConstructor
+@Validated
+@Slf4j
+public class CertificateRevocationListController {
+
+ private final RevocationListService revocationListService;
+
+ private final RevocationBatchMapper revocationBatchMapper;
+
+ public static final String UUID_REGEX =
+ "^[0-9a-f]{8}\\b-[0-9a-f]{4}\\b-[0-9a-f]{4}\\b-[0-9a-f]{4}\\b-[0-9a-f]{12}$";
+
+ /**
+ * Endpoint to download Revocation Batch List.
+ */
+ @CertificateAuthenticationRequired
+ @GetMapping(path = "", produces = MediaType.APPLICATION_JSON_VALUE)
+ @Operation(
+ security = {
+ @SecurityRequirement(name = OpenApiConfig.SECURITY_SCHEMA_HASH),
+ @SecurityRequirement(name = OpenApiConfig.SECURITY_SCHEMA_DISTINGUISH_NAME)
+ },
+ tags = {"Revocation"},
+ summary = "Download Batch List",
+ description = "Returning a list of batches with a small wrapper providing metadata."
+ + " The batches are sorted by date in ascending (chronological) order.",
+ parameters = {
+ @Parameter(
+ in = ParameterIn.HEADER,
+ name = HttpHeaders.IF_MODIFIED_SINCE,
+ description = "This header contains the last downloaded date to get just the latest results. "
+ + "On the initial call the header should be the set to ‘2021-06-01T00:00:00Z’",
+ required = true)
+ },
+ responses = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "Response contains the batch list.",
+ content = @Content(schema = @Schema(implementation = RevocationBatchListDto.class))),
+ @ApiResponse(
+ responseCode = "204",
+ description = "No Content if no data is available later than provided If-Modified-Since header.")
+ }
+ )
+ public ResponseEntity downloadBatchList(
+ @Valid @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
+ @RequestHeader(HttpHeaders.IF_MODIFIED_SINCE) ZonedDateTime ifModifiedSince) {
+
+ if (ifModifiedSince.isAfter(ZonedDateTime.now())) {
+ throw new DgcgResponseException(HttpStatus.BAD_REQUEST, "", "IfModifiedSince must be in past", "", "");
+ }
+
+ RevocationBatchListDto revocationBatchListDto =
+ revocationBatchMapper.toDto(revocationListService.getRevocationBatchList(ifModifiedSince));
+
+ if (revocationBatchListDto.getBatches().isEmpty()) {
+ return ResponseEntity.noContent().build();
+ } else {
+ return ResponseEntity.ok(revocationBatchListDto);
+ }
+ }
+
+ /**
+ * Endpoint to download Revocation Batch.
+ */
+ @CertificateAuthenticationRequired
+ @GetMapping(value = "/{batchId}", produces = {
+ CmsStringMessageConverter.CONTENT_TYPE_CMS_TEXT_VALUE, CmsStringMessageConverter.CONTENT_TYPE_CMS_VALUE})
+ @Operation(
+ security = {
+ @SecurityRequirement(name = OpenApiConfig.SECURITY_SCHEMA_HASH),
+ @SecurityRequirement(name = OpenApiConfig.SECURITY_SCHEMA_DISTINGUISH_NAME)
+ },
+ tags = {"Revocation"},
+ summary = "Download Batch",
+ description = "Returning a batch with hashes of revoked certificates by its Batch ID.",
+ parameters = {
+ @Parameter(
+ in = ParameterIn.PATH,
+ name = "batchId",
+ description = "ID of the batch to download",
+ schema = @Schema(implementation = String.class, format = "UUID", pattern = UUID_REGEX),
+ required = true)
+ },
+ responses = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "Response contains the batch.",
+ content = @Content(schema = @Schema(implementation = RevocationBatchDto.class)),
+ headers = @Header(name = HttpHeaders.ETAG, description = "Batch ID")),
+ @ApiResponse(
+ responseCode = "404",
+ description = "Batch does not exist."),
+ @ApiResponse(
+ responseCode = "410",
+ description = "Batch already deleted.")
+ }
+ )
+ public ResponseEntity downloadBatch(
+ @Valid @PathVariable("batchId") @Pattern(regexp = UUID_REGEX) String batchId) {
+
+ try {
+ RevocationBatchDownload download = revocationListService.getRevocationBatch(batchId);
+
+ return ResponseEntity
+ .ok()
+ .header(HttpHeaders.ETAG, download.getBatchId())
+ .body(download.getSignedCms());
+
+ } catch (RevocationListService.RevocationBatchServiceException e) {
+ switch (e.getReason()) {
+ case GONE:
+ throw new DgcgResponseException(HttpStatus.GONE, "0x000", "Batch already deleted.", "",
+ e.getMessage());
+ case NOT_FOUND:
+ throw new DgcgResponseException(HttpStatus.NOT_FOUND, "0x000", "Batch does not exist.", "",
+ e.getMessage());
+ default:
+ throw new DgcgResponseException(HttpStatus.INTERNAL_SERVER_ERROR, "0x000", "Unexpected Error",
+ "", "");
+
+ }
+ }
+ }
+
+ /**
+ * Endpoint to upload Revocation Batch.
+ */
+ @CertificateAuthenticationRequired
+ @PostMapping(value = "", consumes = {
+ CmsStringMessageConverter.CONTENT_TYPE_CMS_TEXT_VALUE, CmsStringMessageConverter.CONTENT_TYPE_CMS_VALUE})
+ @Operation(
+ security = {
+ @SecurityRequirement(name = OpenApiConfig.SECURITY_SCHEMA_HASH),
+ @SecurityRequirement(name = OpenApiConfig.SECURITY_SCHEMA_DISTINGUISH_NAME)
+ },
+ tags = {"Revocation"},
+ summary = "Upload a new Batch",
+ description = "Endpoint to upload a new Batch of certificate hashes for revocation.",
+ requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(
+ required = true,
+ content = @Content(schema = @Schema(implementation = RevocationBatchDto.class))
+ ),
+ responses = {
+ @ApiResponse(
+ responseCode = "201",
+ description = "Batch created."),
+ @ApiResponse(
+ responseCode = "409",
+ description = "Batch already exists.")
+ }
+ )
+ public ResponseEntity uploadBatch(
+ @RequestBody SignedStringDto batch,
+ @RequestAttribute(CertificateAuthenticationFilter.REQUEST_PROP_COUNTRY) String countryCode) {
+
+ if (!batch.isVerified()) {
+ throw new DgcgResponseException(HttpStatus.BAD_REQUEST, "0x260", "CMS signature is invalid", "",
+ "Submitted string needs to be signed by a valid upload certificate");
+ }
+
+ try {
+ revocationListService.addRevocationBatch(
+ batch.getPayloadString(),
+ batch.getSignerCertificate(),
+ batch.getRawMessage(),
+ countryCode
+ );
+ } catch (RevocationListService.RevocationBatchServiceException e) {
+ log.error("Upload of Revocation Batch failed: {}, {}", e.getReason(), e.getMessage());
+
+ switch (e.getReason()) {
+ case INVALID_JSON:
+ throw new DgcgResponseException(HttpStatus.BAD_REQUEST, "0x000", "JSON Could not be parsed", "",
+ e.getMessage());
+ case INVALID_JSON_VALUES:
+ throw new DgcgResponseException(HttpStatus.BAD_REQUEST, "0x000", "Batch has invalid values.", "",
+ e.getMessage());
+ case INVALID_COUNTRY:
+ throw new DgcgResponseException(HttpStatus.FORBIDDEN, "0x000", "Invalid Country sent", "",
+ e.getMessage());
+ case UPLOADER_CERT_CHECK_FAILED:
+ throw new DgcgResponseException(HttpStatus.FORBIDDEN, "0x000", "Invalid Upload Certificate",
+ batch.getSignerCertificate().getSubject().toString(), "Certificate used to sign the batch "
+ + "is not a valid/ allowed upload certificate for your country.");
+ default:
+ throw new DgcgResponseException(HttpStatus.INTERNAL_SERVER_ERROR, "0x000", "Unexpected Error",
+ "", "");
+ }
+ }
+
+ return ResponseEntity.status(HttpStatus.CREATED).build();
+ }
+
+ /**
+ * Endpoint to delete Revocation Batch.
+ */
+ @CertificateAuthenticationRequired
+ @DeleteMapping(value = "", consumes = {
+ CmsStringMessageConverter.CONTENT_TYPE_CMS_TEXT_VALUE, CmsStringMessageConverter.CONTENT_TYPE_CMS_VALUE})
+ @Operation(
+ security = {
+ @SecurityRequirement(name = OpenApiConfig.SECURITY_SCHEMA_HASH),
+ @SecurityRequirement(name = OpenApiConfig.SECURITY_SCHEMA_DISTINGUISH_NAME)
+ },
+ tags = {"Revocation"},
+ summary = "Delete a Batch",
+ description = "Deletes a batch of hashes for certificate revocation. "
+ + "Batch will be marked as Deleted and deletion will follow up within 7 days.",
+ requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(
+ required = true,
+ description = "The Batch ID as signed CMS.",
+ content = @Content(schema = @Schema(implementation = RevocationBatchDeleteRequestDto.class))
+ ),
+ responses = {
+ @ApiResponse(
+ responseCode = "204",
+ description = "Batch deleted."),
+ @ApiResponse(
+ responseCode = "404",
+ description = "Batch does not exist.")
+ }
+ )
+ public ResponseEntity deleteBatch(
+ @RequestBody SignedStringDto batchDeleteRequest,
+ @RequestAttribute(CertificateAuthenticationFilter.REQUEST_PROP_COUNTRY) String countryCode) {
+
+ if (!batchDeleteRequest.isVerified()) {
+ throw new DgcgResponseException(HttpStatus.BAD_REQUEST, "0x260", "CMS signature is invalid", "",
+ "Submitted string needs to be signed by a valid upload certificate");
+ }
+
+ try {
+ revocationListService.deleteRevocationBatch(
+ batchDeleteRequest.getPayloadString(),
+ batchDeleteRequest.getSignerCertificate(),
+ countryCode);
+ } catch (RevocationListService.RevocationBatchServiceException e) {
+ switch (e.getReason()) {
+ case INVALID_JSON:
+ throw new DgcgResponseException(HttpStatus.BAD_REQUEST, "0x000", "JSON Could not be parsed", "",
+ e.getMessage());
+ case INVALID_JSON_VALUES:
+ throw new DgcgResponseException(HttpStatus.BAD_REQUEST, "0x000",
+ "Delete Request has invalid values.", "",
+ e.getMessage());
+ case INVALID_COUNTRY:
+ throw new DgcgResponseException(HttpStatus.FORBIDDEN, "0x000", "Invalid Country sent", "",
+ e.getMessage());
+ case NOT_FOUND:
+ throw new DgcgResponseException(HttpStatus.NOT_FOUND, "0x000", "Batch does not exists.", "",
+ e.getMessage());
+ case GONE:
+ throw new DgcgResponseException(HttpStatus.GONE, "0x000", "Batch is already deleted.", "",
+ e.getMessage());
+ case UPLOADER_CERT_CHECK_FAILED:
+ throw new DgcgResponseException(HttpStatus.FORBIDDEN, "0x000", "Invalid Upload Certificate",
+ batchDeleteRequest.getSignerCertificate().getSubject().toString(),
+ "Certificate used to sign the batch is not a valid/ allowed"
+ + " upload certificate for your country.");
+ default:
+ throw new DgcgResponseException(HttpStatus.INTERNAL_SERVER_ERROR, "0x000", "Unexpected Error",
+ "", "");
+ }
+ }
+
+ return ResponseEntity.noContent().build();
+ }
+
+ /**
+ * Alternative endpoint to delete revocation batches.
+ */
+ @CertificateAuthenticationRequired
+ @PostMapping(value = "/delete", consumes = {
+ CmsStringMessageConverter.CONTENT_TYPE_CMS_TEXT_VALUE, CmsStringMessageConverter.CONTENT_TYPE_CMS_VALUE})
+ public ResponseEntity deleteBatchAlternativeEndpoint(
+ @RequestBody SignedStringDto batchDeleteRequest,
+ @RequestAttribute(CertificateAuthenticationFilter.REQUEST_PROP_COUNTRY) String countryCode) {
+
+ return deleteBatch(batchDeleteRequest, countryCode);
+ }
+}
diff --git a/src/main/java/eu/europa/ec/dgc/gateway/restapi/dto/revocation/RevocationBatchDeleteRequestDto.java b/src/main/java/eu/europa/ec/dgc/gateway/restapi/dto/revocation/RevocationBatchDeleteRequestDto.java
new file mode 100644
index 00000000..c1ae569d
--- /dev/null
+++ b/src/main/java/eu/europa/ec/dgc/gateway/restapi/dto/revocation/RevocationBatchDeleteRequestDto.java
@@ -0,0 +1,39 @@
+/*-
+ * ---license-start
+ * EU Digital Green Certificate Gateway Service / dgc-gateway
+ * ---
+ * 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.restapi.dto.revocation;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import javax.validation.constraints.Pattern;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Schema(description = "Object to identify a batch to delete.")
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class RevocationBatchDeleteRequestDto {
+
+ @Schema(description = "Unique Identifier of the Batch", format = "UUID")
+ @Pattern(regexp = "^[0-9a-f]{8}\\b-[0-9a-f]{4}\\b-[0-9a-f]{4}\\b-[0-9a-f]{4}\\b-[0-9a-f]{12}$")
+ private String batchId;
+
+}
diff --git a/src/main/java/eu/europa/ec/dgc/gateway/restapi/dto/revocation/RevocationBatchDto.java b/src/main/java/eu/europa/ec/dgc/gateway/restapi/dto/revocation/RevocationBatchDto.java
new file mode 100644
index 00000000..ddfe2c55
--- /dev/null
+++ b/src/main/java/eu/europa/ec/dgc/gateway/restapi/dto/revocation/RevocationBatchDto.java
@@ -0,0 +1,70 @@
+/*-
+ * ---license-start
+ * EU Digital Green Certificate Gateway Service / dgc-gateway
+ * ---
+ * 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.restapi.dto.revocation;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import java.time.ZonedDateTime;
+import java.util.List;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Size;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.hibernate.validator.constraints.Length;
+
+@Schema(description = "Batch entry with list of revoked certificates.")
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class RevocationBatchDto {
+
+ @Schema(description = "ISO 3166 2-Digit Country Code")
+ @Length(min = 2, max = 2)
+ @NotNull
+ private String country;
+
+ @Schema(description = "Date when the item can be removed")
+ @NotNull
+ private ZonedDateTime expires;
+
+
+ @Schema(description = "Base64 encoded KID of the DSC used to sign the Batch. Use UNKNOWN_KID if kid is not known.")
+ @Length(min = 11, max = 12)
+ private String kid;
+
+ @Schema(description = "Hash Type of the provided entries")
+ private RevocationHashTypeDto hashType;
+
+ @Schema(description = "List of revoked certificate hashes")
+ @Size(min = 1, max = 1000)
+ private List entries;
+
+ @Data
+ @NoArgsConstructor
+ @AllArgsConstructor
+ public static class BatchEntryDto {
+
+ @Schema(description = "Base64 encoded first 128 Bits of the hash of the Entry")
+ @Length(min = 24, max = 24)
+ private String hash;
+
+ }
+}
diff --git a/src/main/java/eu/europa/ec/dgc/gateway/restapi/dto/revocation/RevocationBatchListDto.java b/src/main/java/eu/europa/ec/dgc/gateway/restapi/dto/revocation/RevocationBatchListDto.java
new file mode 100644
index 00000000..feb22055
--- /dev/null
+++ b/src/main/java/eu/europa/ec/dgc/gateway/restapi/dto/revocation/RevocationBatchListDto.java
@@ -0,0 +1,62 @@
+/*-
+ * ---license-start
+ * EU Digital Green Certificate Gateway Service / dgc-gateway
+ * ---
+ * 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.restapi.dto.revocation;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import java.time.ZonedDateTime;
+import java.util.List;
+import javax.validation.constraints.Pattern;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.hibernate.validator.constraints.Length;
+
+@Data
+public class RevocationBatchListDto {
+
+ @Schema(description = "The result is limited by default to 10K. If the flag ‘more’ is set to true, "
+ + "the response indicates that more batches are available for download. "
+ + "To download more items the client must set the If-Modified-Since header")
+ private Boolean more;
+
+ @Schema(description = "The List of batches available since the provided date")
+ private List batches;
+
+ @Data
+ @AllArgsConstructor
+ @NoArgsConstructor
+ public static class RevocationBatchListItemDto {
+
+ @Schema(description = "Unique Identifier of the Batch", format = "UUID")
+ @Pattern(regexp = "^[0-9a-f]{8}\\b-[0-9a-f]{4}\\b-[0-9a-f]{4}\\b-[0-9a-f]{4}\\b-[0-9a-f]{12}$")
+ private String batchId;
+
+ @Schema(description = "2-Digit ISO 3166 Country Code")
+ @Length(min = 2, max = 2)
+ private String country;
+
+ @Schema(description = "Date corresponding to the lastEvent")
+ private ZonedDateTime date;
+
+ @Schema(description = "When true, the entry will be finally removed from the query results after 7 days.")
+ private Boolean deleted;
+ }
+}
diff --git a/src/main/java/eu/europa/ec/dgc/gateway/restapi/dto/revocation/RevocationHashTypeDto.java b/src/main/java/eu/europa/ec/dgc/gateway/restapi/dto/revocation/RevocationHashTypeDto.java
new file mode 100644
index 00000000..633664eb
--- /dev/null
+++ b/src/main/java/eu/europa/ec/dgc/gateway/restapi/dto/revocation/RevocationHashTypeDto.java
@@ -0,0 +1,19 @@
+package eu.europa.ec.dgc.gateway.restapi.dto.revocation;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+@Schema(description = "Type of hash for revocation lists")
+public enum RevocationHashTypeDto {
+
+ @Schema(description = "The hash is calculated over the UCI string encoded in "
+ + "UTF-8 and converted to a byte array.")
+ UCI,
+
+ @Schema(description = "The hash is calculated over the bytes of the COSE_SIGN1 signature from the CWT")
+ SIGNATURE,
+
+ @Schema(description = "The CountryCode encoded as a UTF-8 string concatenated with the UCI encoded with a"
+ + " UTF-8 string. This is then converted to a byte array and used as input to the hash function.")
+ COUNTRYCODEUCI
+
+}
diff --git a/src/main/java/eu/europa/ec/dgc/gateway/restapi/filter/FilterConfig.java b/src/main/java/eu/europa/ec/dgc/gateway/restapi/filter/FilterConfig.java
new file mode 100644
index 00000000..5dd6a123
--- /dev/null
+++ b/src/main/java/eu/europa/ec/dgc/gateway/restapi/filter/FilterConfig.java
@@ -0,0 +1,39 @@
+/*-
+ * ---license-start
+ * EU Digital Green Certificate Gateway Service / dgc-gateway
+ * ---
+ * 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.restapi.filter;
+
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.filter.ServletRequestPathFilter;
+
+@Configuration
+public class FilterConfig {
+
+ /**
+ * Add {@link ServletRequestPathFilter} to enable PathParsing which is
+ * required by {@link CertificateAuthenticationFilter}.
+ */
+ @Bean
+ FilterRegistrationBean servletRequestPathFilter() {
+ return new FilterRegistrationBean<>(new ServletRequestPathFilter());
+ }
+}
diff --git a/src/main/java/eu/europa/ec/dgc/gateway/restapi/mapper/RevocationBatchMapper.java b/src/main/java/eu/europa/ec/dgc/gateway/restapi/mapper/RevocationBatchMapper.java
new file mode 100644
index 00000000..b9cd8fcf
--- /dev/null
+++ b/src/main/java/eu/europa/ec/dgc/gateway/restapi/mapper/RevocationBatchMapper.java
@@ -0,0 +1,34 @@
+/*-
+ * ---license-start
+ * EU Digital Green Certificate Gateway Service / dgc-gateway
+ * ---
+ * 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.restapi.mapper;
+
+import eu.europa.ec.dgc.gateway.model.RevocationBatchList;
+import eu.europa.ec.dgc.gateway.restapi.dto.revocation.RevocationBatchListDto;
+import org.mapstruct.Mapper;
+
+@Mapper(componentModel = "spring")
+public interface RevocationBatchMapper {
+
+ RevocationBatchListDto toDto(RevocationBatchList batchList);
+
+ RevocationBatchList.RevocationBatchListItem toDto(RevocationBatchListDto.RevocationBatchListItemDto batchListItem);
+
+}
diff --git a/src/main/java/eu/europa/ec/dgc/gateway/service/RevocationListCleanUpService.java b/src/main/java/eu/europa/ec/dgc/gateway/service/RevocationListCleanUpService.java
new file mode 100644
index 00000000..7daacd70
--- /dev/null
+++ b/src/main/java/eu/europa/ec/dgc/gateway/service/RevocationListCleanUpService.java
@@ -0,0 +1,58 @@
+/*-
+ * ---license-start
+ * EU Digital Green Certificate Gateway Service / dgc-gateway
+ * ---
+ * 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.service;
+
+import eu.europa.ec.dgc.gateway.config.DgcConfigProperties;
+import eu.europa.ec.dgc.gateway.repository.RevocationBatchRepository;
+import java.time.ZonedDateTime;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import net.javacrumbs.shedlock.spring.annotation.SchedulerLock;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Service;
+
+@Service
+@RequiredArgsConstructor
+@Slf4j
+public class RevocationListCleanUpService {
+
+ private final RevocationBatchRepository revocationBatchRepository;
+
+ private final DgcConfigProperties configProperties;
+
+ /**
+ * Delete Revocation Batches which expiry date is reached.
+ */
+ @Scheduled(cron = "0 0 4 * * *")
+ @SchedulerLock(name = "revocation_batch_cleanup")
+ public void cleanup() {
+ log.info("Starting Revocation List Cleanup Job.");
+
+ int affectedRowsMarkAsDeleted = revocationBatchRepository.markExpiredBatchesAsDeleted(ZonedDateTime.now());
+ log.info("Marked {} Revocation Batches as deleted.", affectedRowsMarkAsDeleted);
+
+ int affectedRowsDeleted = revocationBatchRepository.deleteDeletedBatchesOlderThan(
+ ZonedDateTime.now().minusDays(configProperties.getRevocation().getDeleteThreshold()));
+ log.info("Deleted {} Revocation Batches.", affectedRowsDeleted);
+
+ log.info("Completed Revocation List Cleanup Job.");
+ }
+}
diff --git a/src/main/java/eu/europa/ec/dgc/gateway/service/RevocationListService.java b/src/main/java/eu/europa/ec/dgc/gateway/service/RevocationListService.java
new file mode 100644
index 00000000..a5e4df22
--- /dev/null
+++ b/src/main/java/eu/europa/ec/dgc/gateway/service/RevocationListService.java
@@ -0,0 +1,370 @@
+/*-
+ * ---license-start
+ * EU Digital Green Certificate Gateway Service / dgc-gateway
+ * ---
+ * 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.service;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import eu.europa.ec.dgc.gateway.entity.RevocationBatchEntity;
+import eu.europa.ec.dgc.gateway.entity.RevocationBatchProjection;
+import eu.europa.ec.dgc.gateway.entity.TrustedPartyEntity;
+import eu.europa.ec.dgc.gateway.model.RevocationBatchDownload;
+import eu.europa.ec.dgc.gateway.model.RevocationBatchList;
+import eu.europa.ec.dgc.gateway.repository.RevocationBatchRepository;
+import eu.europa.ec.dgc.gateway.restapi.dto.revocation.RevocationBatchDeleteRequestDto;
+import eu.europa.ec.dgc.gateway.restapi.dto.revocation.RevocationBatchDto;
+import eu.europa.ec.dgc.gateway.utils.DgcMdc;
+import eu.europa.ec.dgc.utils.CertificateUtils;
+import java.time.ZonedDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.stream.Collectors;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.bouncycastle.cert.X509CertificateHolder;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.stereotype.Service;
+import org.springframework.validation.BeanPropertyBindingResult;
+import org.springframework.validation.Errors;
+import org.springframework.validation.FieldError;
+import org.springframework.validation.Validator;
+
+@Service
+@Slf4j
+@RequiredArgsConstructor
+public class RevocationListService {
+
+ private static final int MAX_BATCH_LIST_SIZE = 1000;
+
+ private final RevocationBatchRepository revocationBatchRepository;
+
+ private final CertificateUtils certificateUtils;
+
+ private final TrustedPartyService trustedPartyService;
+
+ private final ObjectMapper objectMapper;
+
+ private final Validator validator;
+
+ private final AuditService auditService;
+
+ private static final String MDC_PROP_UPLOAD_CERT_THUMBPRINT = "uploadCertThumbprint";
+
+ /**
+ * Deletes batch with given batchId.
+ *
+ * @param batchId to delete
+ * @return amount of deleted entities.
+ */
+ public int deleteRevocationBatchByBatchId(String batchId) {
+ return revocationBatchRepository.deleteByBatchId(batchId);
+ }
+
+ /**
+ * Adds a new Validation Rule DB.
+ *
+ * @param uploadedRevocationBatch the JSON String with the uploaded batch.
+ * @param signerCertificate the certificate which was used to sign the message
+ * @param cms the cms containing the JSON
+ * @param authenticatedCountryCode the country code of the uploader country from cert authentication
+ * @throws RevocationBatchServiceException if validation check has failed. The exception contains a reason property
+ * with detailed information why the validation has failed.
+ */
+ public RevocationBatchEntity addRevocationBatch(
+ String uploadedRevocationBatch,
+ X509CertificateHolder signerCertificate,
+ String cms,
+ String authenticatedCountryCode
+ ) throws RevocationBatchServiceException {
+
+ contentCheckUploaderCertificate(signerCertificate, authenticatedCountryCode);
+ RevocationBatchDto parsedBatch = contentCheckValidJson(uploadedRevocationBatch, RevocationBatchDto.class);
+ contentCheckValidValues(parsedBatch);
+ contentCheckUploaderCountry(parsedBatch, authenticatedCountryCode);
+
+
+ // All checks passed --> Save to DB
+ RevocationBatchEntity newRevocationBatchEntity = new RevocationBatchEntity();
+ newRevocationBatchEntity.setBatchId(UUID.randomUUID().toString());
+ newRevocationBatchEntity.setCountry(parsedBatch.getCountry());
+ newRevocationBatchEntity.setExpires(parsedBatch.getExpires());
+ newRevocationBatchEntity.setKid(parsedBatch.getKid());
+ newRevocationBatchEntity.setType(
+ RevocationBatchEntity.RevocationHashType.valueOf(parsedBatch.getHashType().name()));
+ newRevocationBatchEntity.setChanged(ZonedDateTime.now());
+ newRevocationBatchEntity.setDeleted(false);
+ newRevocationBatchEntity.setSignedBatch(cms);
+
+ log.info("Saving new Revocation Batch Entity with id {}", newRevocationBatchEntity.getBatchId());
+
+ auditService.addAuditEvent(
+ authenticatedCountryCode,
+ signerCertificate,
+ authenticatedCountryCode,
+ "CREATED",
+ String.format("Uploaded Revocation Batch (%s)", newRevocationBatchEntity.getBatchId())
+ );
+
+ newRevocationBatchEntity = revocationBatchRepository.save(newRevocationBatchEntity);
+
+ DgcMdc.remove(MDC_PROP_UPLOAD_CERT_THUMBPRINT);
+
+ return newRevocationBatchEntity;
+ }
+
+ /**
+ * Deletes a Revocation Batch from DB.
+ *
+ * @param batchIdJson the JSON String with the id of the batch to delete.
+ * @param signerCertificate the certificate which was used to sign the message
+ * @param authenticatedCountryCode the country code of the uploader country from cert authentication
+ * @throws RevocationBatchServiceException if validation check has failed. The exception contains a reason property
+ * with detailed information why the validation has failed.
+ */
+ public void deleteRevocationBatch(
+ String batchIdJson,
+ X509CertificateHolder signerCertificate,
+ String authenticatedCountryCode
+ ) throws RevocationBatchServiceException {
+
+ contentCheckUploaderCertificate(signerCertificate, authenticatedCountryCode);
+ RevocationBatchDeleteRequestDto parsedDeleteRequest =
+ contentCheckValidJson(batchIdJson, RevocationBatchDeleteRequestDto.class);
+ contentCheckValidValuesForDeletion(parsedDeleteRequest);
+
+ Optional entityInDb =
+ revocationBatchRepository.getByBatchId(parsedDeleteRequest.getBatchId());
+
+ if (entityInDb.isEmpty()) {
+ throw new RevocationBatchServiceException(RevocationBatchServiceException.Reason.NOT_FOUND,
+ "Revocation Batch does not exists");
+ }
+
+ if (!entityInDb.get().getCountry().equals(authenticatedCountryCode)) {
+ throw new RevocationBatchServiceException(RevocationBatchServiceException.Reason.INVALID_COUNTRY,
+ "Revocation Batch does not belong to your country");
+ }
+
+ if (entityInDb.get().getDeleted()) {
+ throw new RevocationBatchServiceException(RevocationBatchServiceException.Reason.GONE,
+ "Revocation Batch is already deleted.");
+ }
+
+ log.info("Deleting Revocation Batch with Batch ID {} from DB", parsedDeleteRequest.getBatchId());
+
+ revocationBatchRepository.markBatchAsDeleted(parsedDeleteRequest.getBatchId());
+
+ auditService.addAuditEvent(
+ authenticatedCountryCode,
+ signerCertificate,
+ authenticatedCountryCode,
+ "DELETED",
+ String.format("Deleted Revocation Batch (%s)", parsedDeleteRequest.getBatchId())
+ );
+
+ DgcMdc.remove(MDC_PROP_UPLOAD_CERT_THUMBPRINT);
+ }
+
+ /**
+ * Returns a RevocationBatchList with Revocation Batches later than given threshold date.
+ * Result is limited to 1000 entries.
+ *
+ * @param thresholdDate date to filter the entries.
+ * @return RevocationBatchList with entries and more flag set to true if more batches exists.
+ */
+ public RevocationBatchList getRevocationBatchList(ZonedDateTime thresholdDate) {
+
+ RevocationBatchList batchList = new RevocationBatchList();
+
+ List entityList =
+ revocationBatchRepository.getAllByChangedGreaterThanEqualOrderByChangedAsc(
+ thresholdDate, PageRequest.ofSize(MAX_BATCH_LIST_SIZE + 1));
+
+ batchList.setMore(entityList.size() > MAX_BATCH_LIST_SIZE);
+ batchList.setBatches(entityList.stream()
+ .limit(MAX_BATCH_LIST_SIZE)
+ .map(revocationBatchEntity -> new RevocationBatchList.RevocationBatchListItem(
+ revocationBatchEntity.getBatchId(),
+ revocationBatchEntity.getCountry(),
+ revocationBatchEntity.getChanged(),
+ revocationBatchEntity.getDeleted()
+ ))
+ .collect(Collectors.toList())
+ );
+
+ return batchList;
+ }
+
+ /**
+ * Download a single revocation batch with the corresponding CMS.
+ *
+ * @param batchId Batch ID of the Batch to download.
+ * @return Object holding the CMS and BatchID.
+ * @throws RevocationBatchServiceException if Download fails with information about the reason.
+ */
+ public RevocationBatchDownload getRevocationBatch(String batchId) throws RevocationBatchServiceException {
+ Optional entity = revocationBatchRepository.getByBatchId(batchId);
+
+ if (entity.isEmpty()) {
+ throw new RevocationBatchServiceException(
+ RevocationBatchServiceException.Reason.NOT_FOUND, "Batch not found");
+ }
+
+ if (entity.get().getDeleted()) {
+ throw new RevocationBatchServiceException(
+ RevocationBatchServiceException.Reason.GONE, "Batch already deleted.");
+ }
+
+ return new RevocationBatchDownload(entity.get().getBatchId(), entity.get().getSignedBatch());
+ }
+
+ private void contentCheckUploaderCountry(RevocationBatchDto parsedBatch, String countryCode)
+ throws RevocationBatchServiceException {
+ if (!parsedBatch.getCountry().equals(countryCode)) {
+ throw new RevocationBatchServiceException(
+ RevocationBatchServiceException.Reason.INVALID_COUNTRY,
+ "Country does not match your authentication.");
+ }
+ }
+
+ private T contentCheckValidJson(String json, Class clazz) throws RevocationBatchServiceException {
+
+ try {
+ objectMapper.configure(DeserializationFeature.FAIL_ON_TRAILING_TOKENS, true);
+ return objectMapper.readValue(json, clazz);
+ } catch (JsonProcessingException e) {
+ throw new RevocationBatchServiceException(
+ RevocationBatchServiceException.Reason.INVALID_JSON,
+ "JSON could not be parsed");
+ }
+ }
+
+
+ private void contentCheckValidValues(RevocationBatchDto parsedBatch) throws RevocationBatchServiceException {
+
+ ArrayList errorMessages = new ArrayList<>();
+
+ Errors errors = new BeanPropertyBindingResult(parsedBatch, RevocationBatchDto.class.getName());
+ validator.validate(parsedBatch, errors);
+
+ if (errors.hasErrors()) {
+ errors.getFieldErrors()
+ .forEach(error -> {
+ errorMessages.add(error.getField() + ": " + error.getDefaultMessage());
+ });
+ }
+
+ for (int i = 0; i < parsedBatch.getEntries().size(); i++) {
+ Errors batchEntryErrors = new BeanPropertyBindingResult(parsedBatch.getEntries().get(i),
+ RevocationBatchDto.BatchEntryDto.class.getName());
+
+ validator.validate(parsedBatch.getEntries().get(i), batchEntryErrors);
+
+ if (batchEntryErrors.hasErrors()) {
+ for (FieldError error : batchEntryErrors.getFieldErrors()) {
+ errorMessages.add("Batch Entry " + i + ": " + error.getField() + ": " + error.getDefaultMessage());
+ }
+ }
+ }
+
+ if (!errorMessages.isEmpty()) {
+ throw new RevocationBatchServiceException(
+ RevocationBatchServiceException.Reason.INVALID_JSON_VALUES,
+ String.join(", ", errorMessages)
+ );
+ }
+ }
+
+ private void contentCheckValidValuesForDeletion(RevocationBatchDeleteRequestDto parsedDeleteRequest)
+ throws RevocationBatchServiceException {
+
+ ArrayList errorMessages = new ArrayList<>();
+
+ Errors errors =
+ new BeanPropertyBindingResult(parsedDeleteRequest, RevocationBatchDeleteRequestDto.class.getName());
+ validator.validate(parsedDeleteRequest, errors);
+
+ if (errors.hasErrors()) {
+ errors.getFieldErrors()
+ .forEach(error -> {
+ errorMessages.add(error.getField() + ": " + error.getDefaultMessage());
+ });
+ }
+
+ if (!errorMessages.isEmpty()) {
+ throw new RevocationBatchServiceException(
+ RevocationBatchServiceException.Reason.INVALID_JSON_VALUES,
+ String.join(", ", errorMessages)
+ );
+ }
+ }
+
+ /**
+ * Checks a given UploadCertificate if it exists in the database and is assigned to given CountryCode.
+ *
+ * @param signerCertificate Upload Certificate
+ * @param authenticatedCountryCode Country Code.
+ * @throws RevocationBatchServiceException if Validation fails.
+ */
+ public void contentCheckUploaderCertificate(
+ X509CertificateHolder signerCertificate,
+ String authenticatedCountryCode) throws RevocationBatchServiceException {
+ // Content Check Step 1: Uploader Certificate
+ String signerCertThumbprint = certificateUtils.getCertThumbprint(signerCertificate);
+ Optional certFromDb = trustedPartyService.getCertificate(
+ signerCertThumbprint,
+ authenticatedCountryCode,
+ TrustedPartyEntity.CertificateType.UPLOAD
+ );
+
+ if (certFromDb.isEmpty()) {
+ throw new RevocationBatchServiceException(RevocationBatchServiceException.Reason.UPLOADER_CERT_CHECK_FAILED,
+ "Could not find upload certificate with hash %s and country %s",
+ signerCertThumbprint, authenticatedCountryCode);
+ }
+
+ DgcMdc.put(MDC_PROP_UPLOAD_CERT_THUMBPRINT, signerCertThumbprint);
+ }
+
+ public static class RevocationBatchServiceException extends Exception {
+
+ @Getter
+ private final Reason reason;
+
+ public RevocationBatchServiceException(Reason reason, String message, Object... args) {
+ super(String.format(message, args));
+ this.reason = reason;
+ }
+
+ public enum Reason {
+ INVALID_JSON,
+ INVALID_JSON_VALUES,
+ INVALID_COUNTRY,
+ UPLOADER_CERT_CHECK_FAILED,
+ NOT_FOUND,
+ GONE
+ }
+ }
+
+}
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index 558d7801..e5a86f2a 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -43,6 +43,8 @@ dgc:
header-fields:
thumbprint: X-SSL-Client-SHA256
distinguished-name: X-SSL-Client-DN
+ revocation:
+ delete-threshold: 14
springdoc:
api-docs:
enabled: false
diff --git a/src/main/resources/db/changelog.xml b/src/main/resources/db/changelog.xml
index 25ee75b7..8d2049ac 100644
--- a/src/main/resources/db/changelog.xml
+++ b/src/main/resources/db/changelog.xml
@@ -12,4 +12,5 @@
-
\ No newline at end of file
+
+
diff --git a/src/main/resources/db/changelog/add-revocation-batch-table.xml b/src/main/resources/db/changelog/add-revocation-batch-table.xml
new file mode 100644
index 00000000..33ae2006
--- /dev/null
+++ b/src/main/resources/db/changelog/add-revocation-batch-table.xml
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/db/snapshot.json b/src/main/resources/db/snapshot.json
index 03c99253..abec0eb6 100644
--- a/src/main/resources/db/snapshot.json
+++ b/src/main/resources/db/snapshot.json
@@ -1,14 +1,14 @@
{
"snapshot": {
- "created": "2021-06-28T15:18:17.131",
+ "created": "2021-12-14T17:40:13.849",
"database": {
- "productVersion": "2021.1.2",
+ "productVersion": "2021.2.2",
"shortName": "intellijPsiClass",
"majorVersion": "0",
"minorVersion": "0",
"user": "A34636994",
"productName": "JPA Buddy Intellij",
- "url": "jpab?generationContext=60d05a16-aca2-4aeb-9823-8d67f6b942f1"
+ "url": "jpab?generationContext=4d2a525d-6d08-4fbe-8d56-c9ef225eda29"
},
"metadata": {
"generationContext": {
@@ -21,7 +21,7 @@
"catalog": {
"default": true,
"name": "JPA_BUDDY",
- "snapshotId": "ff23152"
+ "snapshotId": "c0fc100"
}
}
],
@@ -31,21 +31,34 @@
"certainDataType": false,
"name": "authentication_sha256_fingerprint",
"nullable": false,
- "relation": "liquibase.structure.core.Table#ff23153",
- "snapshotId": "ff23159",
+ "relation": "liquibase.structure.core.Table#c0fc116",
+ "snapshotId": "c0fc122",
"type": {
"columnSize": "64!{java.lang.Integer}",
"typeName": "VARCHAR"
}
}
},
+ {
+ "column": {
+ "certainDataType": false,
+ "name": "batch_id",
+ "nullable": false,
+ "relation": "liquibase.structure.core.Table#c0fc102",
+ "snapshotId": "c0fc104",
+ "type": {
+ "columnSize": "36!{java.lang.Integer}",
+ "typeName": "VARCHAR"
+ }
+ }
+ },
{
"column": {
"certainDataType": false,
"name": "certificate_type",
"nullable": false,
- "relation": "liquibase.structure.core.Table#ff23163",
- "snapshotId": "ff23172",
+ "relation": "liquibase.structure.core.Table#c0fc126",
+ "snapshotId": "c0fc135",
"type": {
"columnSize": "255!{java.lang.Integer}",
"typeName": "VARCHAR"
@@ -57,21 +70,33 @@
"certainDataType": false,
"name": "certificate_type",
"nullable": false,
- "relation": "liquibase.structure.core.Table#ff23191",
- "snapshotId": "ff23200",
+ "relation": "liquibase.structure.core.Table#c0fc155",
+ "snapshotId": "c0fc164",
"type": {
"columnSize": "255!{java.lang.Integer}",
"typeName": "VARCHAR"
}
}
},
+ {
+ "column": {
+ "certainDataType": false,
+ "name": "changed",
+ "nullable": false,
+ "relation": "liquibase.structure.core.Table#c0fc102",
+ "snapshotId": "c0fc109",
+ "type": {
+ "typeName": "DATETIME"
+ }
+ }
+ },
{
"column": {
"certainDataType": false,
"name": "country",
"nullable": false,
- "relation": "liquibase.structure.core.Table#ff23153",
- "snapshotId": "ff23157",
+ "relation": "liquibase.structure.core.Table#c0fc116",
+ "snapshotId": "c0fc120",
"type": {
"columnSize": "2!{java.lang.Integer}",
"typeName": "VARCHAR"
@@ -83,8 +108,8 @@
"certainDataType": false,
"name": "country",
"nullable": false,
- "relation": "liquibase.structure.core.Table#ff23163",
- "snapshotId": "ff23169",
+ "relation": "liquibase.structure.core.Table#c0fc102",
+ "snapshotId": "c0fc108",
"type": {
"columnSize": "2!{java.lang.Integer}",
"typeName": "VARCHAR"
@@ -96,8 +121,8 @@
"certainDataType": false,
"name": "country",
"nullable": false,
- "relation": "liquibase.structure.core.Table#ff23191",
- "snapshotId": "ff23197",
+ "relation": "liquibase.structure.core.Table#c0fc126",
+ "snapshotId": "c0fc132",
"type": {
"columnSize": "2!{java.lang.Integer}",
"typeName": "VARCHAR"
@@ -109,8 +134,21 @@
"certainDataType": false,
"name": "country",
"nullable": false,
- "relation": "liquibase.structure.core.Table#ff23174",
- "snapshotId": "ff23183",
+ "relation": "liquibase.structure.core.Table#c0fc155",
+ "snapshotId": "c0fc161",
+ "type": {
+ "columnSize": "2!{java.lang.Integer}",
+ "typeName": "VARCHAR"
+ }
+ }
+ },
+ {
+ "column": {
+ "certainDataType": false,
+ "name": "country",
+ "nullable": false,
+ "relation": "liquibase.structure.core.Table#c0fc137",
+ "snapshotId": "c0fc147",
"type": {
"columnSize": "2!{java.lang.Integer}",
"typeName": "VARCHAR"
@@ -122,8 +160,8 @@
"certainDataType": false,
"name": "created_at",
"nullable": false,
- "relation": "liquibase.structure.core.Table#ff23163",
- "snapshotId": "ff23168",
+ "relation": "liquibase.structure.core.Table#c0fc126",
+ "snapshotId": "c0fc131",
"type": {
"typeName": "DATETIME"
}
@@ -134,8 +172,8 @@
"certainDataType": false,
"name": "created_at",
"nullable": false,
- "relation": "liquibase.structure.core.Table#ff23191",
- "snapshotId": "ff23196",
+ "relation": "liquibase.structure.core.Table#c0fc155",
+ "snapshotId": "c0fc160",
"type": {
"typeName": "DATETIME"
}
@@ -146,20 +184,32 @@
"certainDataType": false,
"name": "created_at",
"nullable": false,
- "relation": "liquibase.structure.core.Table#ff23174",
- "snapshotId": "ff23177",
+ "relation": "liquibase.structure.core.Table#c0fc137",
+ "snapshotId": "c0fc143",
"type": {
"typeName": "DATETIME"
}
}
},
+ {
+ "column": {
+ "certainDataType": false,
+ "name": "deleted",
+ "nullable": false,
+ "relation": "liquibase.structure.core.Table#c0fc102",
+ "snapshotId": "c0fc111",
+ "type": {
+ "typeName": "BOOLEAN"
+ }
+ }
+ },
{
"column": {
"certainDataType": false,
"name": "description",
"nullable": false,
- "relation": "liquibase.structure.core.Table#ff23153",
- "snapshotId": "ff23161",
+ "relation": "liquibase.structure.core.Table#c0fc116",
+ "snapshotId": "c0fc124",
"type": {
"columnSize": "64!{java.lang.Integer}",
"typeName": "VARCHAR"
@@ -171,14 +221,26 @@
"certainDataType": false,
"name": "event",
"nullable": false,
- "relation": "liquibase.structure.core.Table#ff23153",
- "snapshotId": "ff23160",
+ "relation": "liquibase.structure.core.Table#c0fc116",
+ "snapshotId": "c0fc123",
"type": {
"columnSize": "64!{java.lang.Integer}",
"typeName": "VARCHAR"
}
}
},
+ {
+ "column": {
+ "certainDataType": false,
+ "name": "expires",
+ "nullable": false,
+ "relation": "liquibase.structure.core.Table#c0fc102",
+ "snapshotId": "c0fc110",
+ "type": {
+ "typeName": "DATETIME"
+ }
+ }
+ },
{
"column": {
"autoIncrementInformation": {
@@ -188,8 +250,8 @@
"certainDataType": false,
"name": "id",
"nullable": false,
- "relation": "liquibase.structure.core.Table#ff23153",
- "snapshotId": "ff23155",
+ "relation": "liquibase.structure.core.Table#c0fc116",
+ "snapshotId": "c0fc118",
"type": {
"typeName": "BIGINT"
}
@@ -204,8 +266,8 @@
"certainDataType": false,
"name": "id",
"nullable": false,
- "relation": "liquibase.structure.core.Table#ff23163",
- "snapshotId": "ff23167",
+ "relation": "liquibase.structure.core.Table#c0fc102",
+ "snapshotId": "c0fc106",
"type": {
"typeName": "BIGINT"
}
@@ -220,8 +282,8 @@
"certainDataType": false,
"name": "id",
"nullable": false,
- "relation": "liquibase.structure.core.Table#ff23191",
- "snapshotId": "ff23195",
+ "relation": "liquibase.structure.core.Table#c0fc126",
+ "snapshotId": "c0fc130",
"type": {
"typeName": "BIGINT"
}
@@ -236,8 +298,8 @@
"certainDataType": false,
"name": "id",
"nullable": false,
- "relation": "liquibase.structure.core.Table#ff23174",
- "snapshotId": "ff23176",
+ "relation": "liquibase.structure.core.Table#c0fc155",
+ "snapshotId": "c0fc159",
"type": {
"typeName": "BIGINT"
}
@@ -245,11 +307,27 @@
},
{
"column": {
+ "autoIncrementInformation": {
+ "incrementBy": "1!{java.math.BigInteger}",
+ "startWith": "1!{java.math.BigInteger}"
+ },
"certainDataType": false,
"name": "id",
"nullable": false,
- "relation": "liquibase.structure.core.Table#ff23186",
- "snapshotId": "ff23188",
+ "relation": "liquibase.structure.core.Table#c0fc137",
+ "snapshotId": "c0fc142",
+ "type": {
+ "typeName": "BIGINT"
+ }
+ }
+ },
+ {
+ "column": {
+ "certainDataType": false,
+ "name": "id",
+ "nullable": false,
+ "relation": "liquibase.structure.core.Table#c0fc150",
+ "snapshotId": "c0fc152",
"type": {
"columnSize": "100!{java.lang.Integer}",
"typeName": "VARCHAR"
@@ -261,10 +339,22 @@
"certainDataType": false,
"name": "json",
"nullable": false,
- "relation": "liquibase.structure.core.Table#ff23186",
- "snapshotId": "ff23189",
+ "relation": "liquibase.structure.core.Table#c0fc150",
+ "snapshotId": "c0fc153",
"type": {
- "columnSize": "1024000!{java.lang.Integer}",
+ "typeName": "CLOB"
+ }
+ }
+ },
+ {
+ "column": {
+ "certainDataType": false,
+ "name": "kid",
+ "nullable": true,
+ "relation": "liquibase.structure.core.Table#c0fc102",
+ "snapshotId": "c0fc113",
+ "type": {
+ "columnSize": "12!{java.lang.Integer}",
"typeName": "VARCHAR"
}
}
@@ -274,8 +364,8 @@
"certainDataType": false,
"name": "raw_data",
"nullable": false,
- "relation": "liquibase.structure.core.Table#ff23163",
- "snapshotId": "ff23170",
+ "relation": "liquibase.structure.core.Table#c0fc126",
+ "snapshotId": "c0fc133",
"type": {
"columnSize": "4096!{java.lang.Integer}",
"typeName": "VARCHAR"
@@ -287,8 +377,8 @@
"certainDataType": false,
"name": "raw_data",
"nullable": false,
- "relation": "liquibase.structure.core.Table#ff23191",
- "snapshotId": "ff23198",
+ "relation": "liquibase.structure.core.Table#c0fc155",
+ "snapshotId": "c0fc162",
"type": {
"columnSize": "4096!{java.lang.Integer}",
"typeName": "VARCHAR"
@@ -300,8 +390,8 @@
"certainDataType": false,
"name": "rule_id",
"nullable": false,
- "relation": "liquibase.structure.core.Table#ff23174",
- "snapshotId": "ff23178",
+ "relation": "liquibase.structure.core.Table#c0fc137",
+ "snapshotId": "c0fc139",
"type": {
"columnSize": "100!{java.lang.Integer}",
"typeName": "VARCHAR"
@@ -313,8 +403,8 @@
"certainDataType": false,
"name": "signature",
"nullable": false,
- "relation": "liquibase.structure.core.Table#ff23163",
- "snapshotId": "ff23171",
+ "relation": "liquibase.structure.core.Table#c0fc126",
+ "snapshotId": "c0fc134",
"type": {
"columnSize": "6000!{java.lang.Integer}",
"typeName": "VARCHAR"
@@ -326,8 +416,8 @@
"certainDataType": false,
"name": "signature",
"nullable": false,
- "relation": "liquibase.structure.core.Table#ff23191",
- "snapshotId": "ff23199",
+ "relation": "liquibase.structure.core.Table#c0fc155",
+ "snapshotId": "c0fc163",
"type": {
"columnSize": "6000!{java.lang.Integer}",
"typeName": "VARCHAR"
@@ -339,21 +429,33 @@
"certainDataType": false,
"name": "signature",
"nullable": false,
- "relation": "liquibase.structure.core.Table#ff23174",
- "snapshotId": "ff23179",
+ "relation": "liquibase.structure.core.Table#c0fc137",
+ "snapshotId": "c0fc144",
"type": {
"columnSize": "10000!{java.lang.Integer}",
"typeName": "VARCHAR"
}
}
},
+ {
+ "column": {
+ "certainDataType": false,
+ "name": "signed_batch",
+ "nullable": true,
+ "relation": "liquibase.structure.core.Table#c0fc102",
+ "snapshotId": "c0fc114",
+ "type": {
+ "typeName": "CLOB"
+ }
+ }
+ },
{
"column": {
"certainDataType": false,
"name": "thumbprint",
"nullable": false,
- "relation": "liquibase.structure.core.Table#ff23163",
- "snapshotId": "ff23165",
+ "relation": "liquibase.structure.core.Table#c0fc126",
+ "snapshotId": "c0fc128",
"type": {
"columnSize": "64!{java.lang.Integer}",
"typeName": "VARCHAR"
@@ -365,8 +467,8 @@
"certainDataType": false,
"name": "thumbprint",
"nullable": false,
- "relation": "liquibase.structure.core.Table#ff23191",
- "snapshotId": "ff23193",
+ "relation": "liquibase.structure.core.Table#c0fc155",
+ "snapshotId": "c0fc157",
"type": {
"columnSize": "64!{java.lang.Integer}",
"typeName": "VARCHAR"
@@ -378,8 +480,8 @@
"certainDataType": false,
"name": "timestamp",
"nullable": false,
- "relation": "liquibase.structure.core.Table#ff23153",
- "snapshotId": "ff23156",
+ "relation": "liquibase.structure.core.Table#c0fc116",
+ "snapshotId": "c0fc119",
"type": {
"typeName": "DATETIME"
}
@@ -390,8 +492,21 @@
"certainDataType": false,
"name": "type",
"nullable": false,
- "relation": "liquibase.structure.core.Table#ff23174",
- "snapshotId": "ff23184",
+ "relation": "liquibase.structure.core.Table#c0fc102",
+ "snapshotId": "c0fc112",
+ "type": {
+ "columnSize": "255!{java.lang.Integer}",
+ "typeName": "VARCHAR"
+ }
+ }
+ },
+ {
+ "column": {
+ "certainDataType": false,
+ "name": "type",
+ "nullable": false,
+ "relation": "liquibase.structure.core.Table#c0fc137",
+ "snapshotId": "c0fc148",
"type": {
"columnSize": "255!{java.lang.Integer}",
"typeName": "VARCHAR"
@@ -403,8 +518,8 @@
"certainDataType": false,
"name": "uploader_sha256_fingerprint",
"nullable": false,
- "relation": "liquibase.structure.core.Table#ff23153",
- "snapshotId": "ff23158",
+ "relation": "liquibase.structure.core.Table#c0fc116",
+ "snapshotId": "c0fc121",
"type": {
"columnSize": "64!{java.lang.Integer}",
"typeName": "VARCHAR"
@@ -416,8 +531,8 @@
"certainDataType": false,
"name": "valid_from",
"nullable": false,
- "relation": "liquibase.structure.core.Table#ff23174",
- "snapshotId": "ff23180",
+ "relation": "liquibase.structure.core.Table#c0fc137",
+ "snapshotId": "c0fc145",
"type": {
"typeName": "DATETIME"
}
@@ -428,8 +543,8 @@
"certainDataType": false,
"name": "valid_to",
"nullable": false,
- "relation": "liquibase.structure.core.Table#ff23174",
- "snapshotId": "ff23181",
+ "relation": "liquibase.structure.core.Table#c0fc137",
+ "snapshotId": "c0fc146",
"type": {
"typeName": "DATETIME"
}
@@ -440,8 +555,8 @@
"certainDataType": false,
"name": "version",
"nullable": false,
- "relation": "liquibase.structure.core.Table#ff23174",
- "snapshotId": "ff23182",
+ "relation": "liquibase.structure.core.Table#c0fc137",
+ "snapshotId": "c0fc140",
"type": {
"columnSize": "30!{java.lang.Integer}",
"typeName": "VARCHAR"
@@ -453,123 +568,155 @@
{
"index": {
"columns": [
- "liquibase.structure.core.Column#ff23155"
+ "liquibase.structure.core.Column#c0fc118"
],
- "name": "IX_PK_AUDIT_EVENT",
- "snapshotId": "ff23154",
- "table": "liquibase.structure.core.Table#ff23153",
+ "name": "IX_pk_audit_event",
+ "snapshotId": "c0fc117",
+ "table": "liquibase.structure.core.Table#c0fc116",
"unique": true
}
},
{
"index": {
"columns": [
- "liquibase.structure.core.Column#ff23167"
+ "liquibase.structure.core.Column#c0fc106"
],
- "name": "IX_PK_SIGNER_INFORMATION",
- "snapshotId": "ff23166",
- "table": "liquibase.structure.core.Table#ff23163",
+ "name": "IX_pk_revocation_batch",
+ "snapshotId": "c0fc105",
+ "table": "liquibase.structure.core.Table#c0fc102",
"unique": true
}
},
{
"index": {
"columns": [
- "liquibase.structure.core.Column#ff23195"
+ "liquibase.structure.core.Column#c0fc130"
],
- "name": "IX_PK_TRUSTED_PARTY",
- "snapshotId": "ff23194",
- "table": "liquibase.structure.core.Table#ff23191",
+ "name": "IX_pk_signer_information",
+ "snapshotId": "c0fc129",
+ "table": "liquibase.structure.core.Table#c0fc126",
"unique": true
}
},
{
"index": {
"columns": [
- "liquibase.structure.core.Column#ff23176"
+ "liquibase.structure.core.Column#c0fc159"
],
- "name": "IX_PK_VALIDATION_RULE",
- "snapshotId": "ff23175",
- "table": "liquibase.structure.core.Table#ff23174",
+ "name": "IX_pk_trusted_party",
+ "snapshotId": "c0fc158",
+ "table": "liquibase.structure.core.Table#c0fc155",
"unique": true
}
},
{
"index": {
"columns": [
- "liquibase.structure.core.Column#ff23188"
+ "liquibase.structure.core.Column#c0fc142"
],
- "name": "IX_PK_VALUESET",
- "snapshotId": "ff23187",
- "table": "liquibase.structure.core.Table#ff23186",
+ "name": "IX_pk_validation_rule",
+ "snapshotId": "c0fc141",
+ "table": "liquibase.structure.core.Table#c0fc137",
"unique": true
}
+ },
+ {
+ "index": {
+ "columns": [
+ "liquibase.structure.core.Column#c0fc152"
+ ],
+ "name": "IX_pk_valueset",
+ "snapshotId": "c0fc151",
+ "table": "liquibase.structure.core.Table#c0fc150",
+ "unique": true
+ }
+ },
+ {
+ "index": {
+ "columns": [
+ "liquibase.structure.core.Column#c0fc104"
+ ],
+ "name": "idx_6bd7e9b8e4d29f7ed8d5d4bb2",
+ "snapshotId": "c0fc107",
+ "table": "liquibase.structure.core.Table#c0fc102",
+ "unique": false
+ }
}
],
"liquibase.structure.core.PrimaryKey": [
{
"primaryKey": {
- "backingIndex": "liquibase.structure.core.Index#ff23154",
+ "backingIndex": "liquibase.structure.core.Index#c0fc117",
+ "columns": [
+ "liquibase.structure.core.Column#c0fc118"
+ ],
+ "name": "pk_audit_event",
+ "snapshotId": "c0fc125",
+ "table": "liquibase.structure.core.Table#c0fc116"
+ }
+ },
+ {
+ "primaryKey": {
+ "backingIndex": "liquibase.structure.core.Index#c0fc105",
"columns": [
- "liquibase.structure.core.Column#ff23155"
+ "liquibase.structure.core.Column#c0fc106"
],
- "name": "PK_AUDIT_EVENT",
- "snapshotId": "ff23162",
- "table": "liquibase.structure.core.Table#ff23153"
+ "name": "pk_revocation_batch",
+ "snapshotId": "c0fc115",
+ "table": "liquibase.structure.core.Table#c0fc102"
}
},
{
"primaryKey": {
- "backingIndex": "liquibase.structure.core.Index#ff23166",
+ "backingIndex": "liquibase.structure.core.Index#c0fc129",
"columns": [
- "liquibase.structure.core.Column#ff23167"
+ "liquibase.structure.core.Column#c0fc130"
],
- "name": "PK_SIGNER_INFORMATION",
- "snapshotId": "ff23173",
- "table": "liquibase.structure.core.Table#ff23163"
+ "name": "pk_signer_information",
+ "snapshotId": "c0fc136",
+ "table": "liquibase.structure.core.Table#c0fc126"
}
},
{
"primaryKey": {
- "backingIndex": "liquibase.structure.core.Index#ff23194",
+ "backingIndex": "liquibase.structure.core.Index#c0fc158",
"columns": [
- "liquibase.structure.core.Column#ff23195"
+ "liquibase.structure.core.Column#c0fc159"
],
- "name": "PK_TRUSTED_PARTY",
- "snapshotId": "ff23201",
- "table": "liquibase.structure.core.Table#ff23191"
+ "name": "pk_trusted_party",
+ "snapshotId": "c0fc165",
+ "table": "liquibase.structure.core.Table#c0fc155"
}
},
{
"primaryKey": {
- "backingIndex": "liquibase.structure.core.Index#ff23175",
+ "backingIndex": "liquibase.structure.core.Index#c0fc141",
"columns": [
- "liquibase.structure.core.Column#ff23176"
+ "liquibase.structure.core.Column#c0fc142"
],
- "name": "PK_VALIDATION_RULE",
- "snapshotId": "ff23185",
- "table": "liquibase.structure.core.Table#ff23174"
+ "name": "pk_validation_rule",
+ "snapshotId": "c0fc149",
+ "table": "liquibase.structure.core.Table#c0fc137"
}
},
{
"primaryKey": {
- "backingIndex": "liquibase.structure.core.Index#ff23187",
+ "backingIndex": "liquibase.structure.core.Index#c0fc151",
"columns": [
- "liquibase.structure.core.Column#ff23188"
+ "liquibase.structure.core.Column#c0fc152"
],
- "name": "PK_VALUESET",
- "snapshotId": "ff23190",
- "table": "liquibase.structure.core.Table#ff23186"
+ "name": "pk_valueset",
+ "snapshotId": "c0fc154",
+ "table": "liquibase.structure.core.Table#c0fc150"
}
}
],
"liquibase.structure.core.Schema": [
{
"schema": {
- "catalog": "liquibase.structure.core.Catalog#ff23152",
+ "catalog": "liquibase.structure.core.Catalog#c0fc100",
"default": true,
- "name": "JPA_BUDDY",
- "snapshotId": "ff23151"
+ "snapshotId": "c0fc101"
}
}
],
@@ -577,104 +724,133 @@
{
"table": {
"columns": [
- "liquibase.structure.core.Column#ff23155",
- "liquibase.structure.core.Column#ff23156",
- "liquibase.structure.core.Column#ff23157",
- "liquibase.structure.core.Column#ff23158",
- "liquibase.structure.core.Column#ff23159",
- "liquibase.structure.core.Column#ff23160",
- "liquibase.structure.core.Column#ff23161"
+ "liquibase.structure.core.Column#c0fc118",
+ "liquibase.structure.core.Column#c0fc119",
+ "liquibase.structure.core.Column#c0fc120",
+ "liquibase.structure.core.Column#c0fc121",
+ "liquibase.structure.core.Column#c0fc122",
+ "liquibase.structure.core.Column#c0fc123",
+ "liquibase.structure.core.Column#c0fc124"
],
"indexes": [
- "liquibase.structure.core.Index#ff23154"
+ "liquibase.structure.core.Index#c0fc117"
],
"name": "audit_event",
- "primaryKey": "liquibase.structure.core.PrimaryKey#ff23162",
- "schema": "liquibase.structure.core.Schema#ff23151",
- "snapshotId": "ff23153"
+ "primaryKey": "liquibase.structure.core.PrimaryKey#c0fc125",
+ "schema": "liquibase.structure.core.Schema#c0fc101",
+ "snapshotId": "c0fc116"
}
},
{
"table": {
"columns": [
- "liquibase.structure.core.Column#ff23167",
- "liquibase.structure.core.Column#ff23168",
- "liquibase.structure.core.Column#ff23169",
- "liquibase.structure.core.Column#ff23165",
- "liquibase.structure.core.Column#ff23170",
- "liquibase.structure.core.Column#ff23171",
- "liquibase.structure.core.Column#ff23172"
+ "liquibase.structure.core.Column#c0fc106",
+ "liquibase.structure.core.Column#c0fc104",
+ "liquibase.structure.core.Column#c0fc108",
+ "liquibase.structure.core.Column#c0fc109",
+ "liquibase.structure.core.Column#c0fc110",
+ "liquibase.structure.core.Column#c0fc111",
+ "liquibase.structure.core.Column#c0fc112",
+ "liquibase.structure.core.Column#c0fc113",
+ "liquibase.structure.core.Column#c0fc114"
],
"indexes": [
- "liquibase.structure.core.Index#ff23166"
+ "liquibase.structure.core.Index#c0fc105",
+ "liquibase.structure.core.Index#c0fc107"
+ ],
+ "name": "revocation_batch",
+ "primaryKey": "liquibase.structure.core.PrimaryKey#c0fc115",
+ "schema": "liquibase.structure.core.Schema#c0fc101",
+ "snapshotId": "c0fc102",
+ "uniqueConstraints": [
+ "liquibase.structure.core.UniqueConstraint#c0fc103"
+ ]
+ }
+ },
+ {
+ "table": {
+ "columns": [
+ "liquibase.structure.core.Column#c0fc130",
+ "liquibase.structure.core.Column#c0fc131",
+ "liquibase.structure.core.Column#c0fc132",
+ "liquibase.structure.core.Column#c0fc128",
+ "liquibase.structure.core.Column#c0fc133",
+ "liquibase.structure.core.Column#c0fc134",
+ "liquibase.structure.core.Column#c0fc135"
+ ],
+ "indexes": [
+ "liquibase.structure.core.Index#c0fc129"
],
"name": "signer_information",
- "primaryKey": "liquibase.structure.core.PrimaryKey#ff23173",
- "schema": "liquibase.structure.core.Schema#ff23151",
- "snapshotId": "ff23163",
+ "primaryKey": "liquibase.structure.core.PrimaryKey#c0fc136",
+ "schema": "liquibase.structure.core.Schema#c0fc101",
+ "snapshotId": "c0fc126",
"uniqueConstraints": [
- "liquibase.structure.core.UniqueConstraint#ff23164"
+ "liquibase.structure.core.UniqueConstraint#c0fc127"
]
}
},
{
"table": {
"columns": [
- "liquibase.structure.core.Column#ff23195",
- "liquibase.structure.core.Column#ff23196",
- "liquibase.structure.core.Column#ff23197",
- "liquibase.structure.core.Column#ff23193",
- "liquibase.structure.core.Column#ff23198",
- "liquibase.structure.core.Column#ff23199",
- "liquibase.structure.core.Column#ff23200"
+ "liquibase.structure.core.Column#c0fc159",
+ "liquibase.structure.core.Column#c0fc160",
+ "liquibase.structure.core.Column#c0fc161",
+ "liquibase.structure.core.Column#c0fc157",
+ "liquibase.structure.core.Column#c0fc162",
+ "liquibase.structure.core.Column#c0fc163",
+ "liquibase.structure.core.Column#c0fc164"
],
"indexes": [
- "liquibase.structure.core.Index#ff23194"
+ "liquibase.structure.core.Index#c0fc158"
],
"name": "trusted_party",
- "primaryKey": "liquibase.structure.core.PrimaryKey#ff23201",
- "schema": "liquibase.structure.core.Schema#ff23151",
- "snapshotId": "ff23191",
+ "primaryKey": "liquibase.structure.core.PrimaryKey#c0fc165",
+ "schema": "liquibase.structure.core.Schema#c0fc101",
+ "snapshotId": "c0fc155",
"uniqueConstraints": [
- "liquibase.structure.core.UniqueConstraint#ff23192"
+ "liquibase.structure.core.UniqueConstraint#c0fc156"
]
}
},
{
"table": {
"columns": [
- "liquibase.structure.core.Column#ff23176",
- "liquibase.structure.core.Column#ff23177",
- "liquibase.structure.core.Column#ff23178",
- "liquibase.structure.core.Column#ff23179",
- "liquibase.structure.core.Column#ff23180",
- "liquibase.structure.core.Column#ff23181",
- "liquibase.structure.core.Column#ff23182",
- "liquibase.structure.core.Column#ff23183",
- "liquibase.structure.core.Column#ff23184"
+ "liquibase.structure.core.Column#c0fc142",
+ "liquibase.structure.core.Column#c0fc143",
+ "liquibase.structure.core.Column#c0fc139",
+ "liquibase.structure.core.Column#c0fc144",
+ "liquibase.structure.core.Column#c0fc145",
+ "liquibase.structure.core.Column#c0fc146",
+ "liquibase.structure.core.Column#c0fc140",
+ "liquibase.structure.core.Column#c0fc147",
+ "liquibase.structure.core.Column#c0fc148"
],
"indexes": [
- "liquibase.structure.core.Index#ff23175"
+ "liquibase.structure.core.Index#c0fc141"
],
"name": "validation_rule",
- "primaryKey": "liquibase.structure.core.PrimaryKey#ff23185",
- "schema": "liquibase.structure.core.Schema#ff23151",
- "snapshotId": "ff23174"
+ "primaryKey": "liquibase.structure.core.PrimaryKey#c0fc149",
+ "schema": "liquibase.structure.core.Schema#c0fc101",
+ "snapshotId": "c0fc137",
+ "uniqueConstraints": [
+ "liquibase.structure.core.UniqueConstraint#c0fc138"
+ ]
}
},
{
"table": {
"columns": [
- "liquibase.structure.core.Column#ff23188",
- "liquibase.structure.core.Column#ff23189"
+ "liquibase.structure.core.Column#c0fc152",
+ "liquibase.structure.core.Column#c0fc153"
],
"indexes": [
- "liquibase.structure.core.Index#ff23187"
+ "liquibase.structure.core.Index#c0fc151"
],
"name": "valueset",
- "primaryKey": "liquibase.structure.core.PrimaryKey#ff23190",
- "schema": "liquibase.structure.core.Schema#ff23151",
- "snapshotId": "ff23186"
+ "primaryKey": "liquibase.structure.core.PrimaryKey#c0fc154",
+ "schema": "liquibase.structure.core.Schema#c0fc101",
+ "snapshotId": "c0fc150"
}
}
],
@@ -683,14 +859,45 @@
"uniqueConstraint": {
"clustered": false,
"columns": [
- "liquibase.structure.core.Column#ff23165"
+ "liquibase.structure.core.Column#c0fc139",
+ "liquibase.structure.core.Column#c0fc140"
+ ],
+ "deferrable": false,
+ "disabled": false,
+ "initiallyDeferred": false,
+ "name": "uc_16f88905e309ddbd1fb7b128d",
+ "snapshotId": "c0fc138",
+ "table": "liquibase.structure.core.Table#c0fc137",
+ "validate": true
+ }
+ },
+ {
+ "uniqueConstraint": {
+ "clustered": false,
+ "columns": [
+ "liquibase.structure.core.Column#c0fc104"
+ ],
+ "deferrable": false,
+ "disabled": false,
+ "initiallyDeferred": false,
+ "name": "uc_revocation_batch_batchid",
+ "snapshotId": "c0fc103",
+ "table": "liquibase.structure.core.Table#c0fc102",
+ "validate": true
+ }
+ },
+ {
+ "uniqueConstraint": {
+ "clustered": false,
+ "columns": [
+ "liquibase.structure.core.Column#c0fc128"
],
"deferrable": false,
"disabled": false,
"initiallyDeferred": false,
- "name": "UC_SIGNER_INFORMATION_THUMBPRINT",
- "snapshotId": "ff23164",
- "table": "liquibase.structure.core.Table#ff23163",
+ "name": "uc_signer_information_thumbprint",
+ "snapshotId": "c0fc127",
+ "table": "liquibase.structure.core.Table#c0fc126",
"validate": true
}
},
@@ -698,14 +905,14 @@
"uniqueConstraint": {
"clustered": false,
"columns": [
- "liquibase.structure.core.Column#ff23193"
+ "liquibase.structure.core.Column#c0fc157"
],
"deferrable": false,
"disabled": false,
"initiallyDeferred": false,
- "name": "UC_TRUSTED_PARTY_THUMBPRINT",
- "snapshotId": "ff23192",
- "table": "liquibase.structure.core.Table#ff23191",
+ "name": "uc_trusted_party_thumbprint",
+ "snapshotId": "c0fc156",
+ "table": "liquibase.structure.core.Table#c0fc155",
"validate": true
}
}
diff --git a/src/test/java/eu/europa/ec/dgc/gateway/restapi/controller/CertificateRevocationListIntegrationTest.java b/src/test/java/eu/europa/ec/dgc/gateway/restapi/controller/CertificateRevocationListIntegrationTest.java
new file mode 100644
index 00000000..4923dc62
--- /dev/null
+++ b/src/test/java/eu/europa/ec/dgc/gateway/restapi/controller/CertificateRevocationListIntegrationTest.java
@@ -0,0 +1,953 @@
+/*-
+ * ---license-start
+ * EU Digital Green Certificate Gateway Service / dgc-gateway
+ * ---
+ * 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.restapi.controller;
+
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import com.fasterxml.jackson.datatype.jsr310.ser.ZonedDateTimeSerializer;
+import eu.europa.ec.dgc.gateway.config.DgcConfigProperties;
+import eu.europa.ec.dgc.gateway.entity.RevocationBatchEntity;
+import eu.europa.ec.dgc.gateway.entity.TrustedPartyEntity;
+import eu.europa.ec.dgc.gateway.repository.AuditEventRepository;
+import eu.europa.ec.dgc.gateway.repository.RevocationBatchRepository;
+import eu.europa.ec.dgc.gateway.restapi.dto.revocation.RevocationBatchDeleteRequestDto;
+import eu.europa.ec.dgc.gateway.restapi.dto.revocation.RevocationBatchDto;
+import eu.europa.ec.dgc.gateway.restapi.dto.revocation.RevocationBatchListDto;
+import eu.europa.ec.dgc.gateway.restapi.dto.revocation.RevocationHashTypeDto;
+import eu.europa.ec.dgc.gateway.testdata.TrustedPartyTestHelper;
+import eu.europa.ec.dgc.signing.SignedMessageParser;
+import eu.europa.ec.dgc.signing.SignedStringMessageBuilder;
+import eu.europa.ec.dgc.signing.SignedStringMessageParser;
+import eu.europa.ec.dgc.utils.CertificateUtils;
+import java.io.UnsupportedEncodingException;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+import java.time.OffsetDateTime;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatterBuilder;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+import lombok.extern.slf4j.Slf4j;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.http.HttpHeaders;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.test.web.servlet.MockMvc;
+
+@SpringBootTest
+@AutoConfigureMockMvc
+@Slf4j
+public class CertificateRevocationListIntegrationTest {
+
+ @Autowired
+ DgcConfigProperties dgcConfigProperties;
+
+ @Autowired
+ CertificateUtils certificateUtils;
+
+ @Autowired
+ TrustedPartyTestHelper trustedPartyTestHelper;
+
+ @Autowired
+ AuditEventRepository auditEventRepository;
+
+ @Autowired
+ RevocationBatchRepository revocationBatchRepository;
+
+ ObjectMapper objectMapper;
+
+ @Autowired
+ private MockMvc mockMvc;
+
+ private static final String countryCode = "EU";
+ private static final String authCertSubject = "C=" + countryCode;
+
+ @BeforeEach
+ public void setup() {
+ revocationBatchRepository.deleteAll();
+ auditEventRepository.deleteAll();
+
+ objectMapper = new ObjectMapper();
+
+ JavaTimeModule javaTimeModule = new JavaTimeModule();
+ javaTimeModule.addSerializer(ZonedDateTime.class, new ZonedDateTimeSerializer(
+ new DateTimeFormatterBuilder().appendPattern("yyyy-MM-dd'T'HH:mm:ssXXX").toFormatter()
+ ));
+
+ objectMapper.registerModule(javaTimeModule);
+ objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
+
+ }
+
+ @Test
+ void testSuccessfulUpload() throws Exception {
+ long revocationBatchesInDb = revocationBatchRepository.count();
+ long auditEventEntitiesInDb = auditEventRepository.count();
+
+ X509Certificate signerCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, countryCode);
+ PrivateKey signerPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.UPLOAD, countryCode);
+
+ RevocationBatchDto revocationBatchDto = new RevocationBatchDto();
+ revocationBatchDto.setCountry(countryCode);
+ revocationBatchDto.setExpires(ZonedDateTime.now().plusDays(7));
+ revocationBatchDto.setHashType(RevocationHashTypeDto.SIGNATURE);
+ revocationBatchDto.setKid("UNKNOWN_KID");
+ revocationBatchDto.setEntries(List.of(
+ new RevocationBatchDto.BatchEntryDto("aaaaaaaaaaaaaaaaaaaaaaaa"),
+ new RevocationBatchDto.BatchEntryDto("bbbbbbbbbbbbbbbbbbbbbbbb"),
+ new RevocationBatchDto.BatchEntryDto("cccccccccccccccccccccccc"),
+ new RevocationBatchDto.BatchEntryDto("dddddddddddddddddddddddd"),
+ new RevocationBatchDto.BatchEntryDto("eeeeeeeeeeeeeeeeeeeeeeee")
+ ));
+
+ String payload = new SignedStringMessageBuilder()
+ .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey)
+ .withPayload(objectMapper.writeValueAsString(revocationBatchDto))
+ .buildAsString();
+
+ String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode);
+
+ mockMvc.perform(post("/revocation-list")
+ .content(payload)
+ .contentType("application/cms")
+ .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash)
+ .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject)
+ )
+ .andExpect(status().isCreated());
+
+ Assertions.assertEquals(revocationBatchesInDb + 1, revocationBatchRepository.count());
+ Optional createdRevocationBatch =
+ revocationBatchRepository.findAll().stream().findFirst();
+
+ Assertions.assertTrue(createdRevocationBatch.isPresent());
+
+ Assertions.assertEquals(auditEventEntitiesInDb + 1, auditEventRepository.count());
+ Assertions.assertEquals(revocationBatchDto.getExpires().toEpochSecond(), createdRevocationBatch.get().getExpires().toEpochSecond());
+ Assertions.assertTrue(
+ ZonedDateTime.now().toEpochSecond() - 2 < createdRevocationBatch.get().getChanged().toEpochSecond()
+ && ZonedDateTime.now().toEpochSecond() + 2 > createdRevocationBatch.get().getChanged().toEpochSecond());
+ Assertions.assertEquals(countryCode, createdRevocationBatch.get().getCountry());
+ Assertions.assertEquals(revocationBatchDto.getHashType().name(), createdRevocationBatch.get().getType().name());
+ Assertions.assertEquals(revocationBatchDto.getKid(), createdRevocationBatch.get().getKid());
+ Assertions.assertEquals(36, createdRevocationBatch.get().getBatchId().length());
+
+ SignedStringMessageParser parser = new SignedStringMessageParser(createdRevocationBatch.get().getSignedBatch());
+ RevocationBatchDto parsedRevocationBatch = objectMapper.readValue(parser.getPayload(), RevocationBatchDto.class);
+
+ assertEquals(revocationBatchDto, parsedRevocationBatch);
+ }
+
+ @Test
+ void testUploadFailedInvalidJson() throws Exception {
+ long revocationBatchesInDb = revocationBatchRepository.count();
+ long auditEventEntitiesInDb = auditEventRepository.count();
+
+ X509Certificate signerCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, countryCode);
+ PrivateKey signerPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.UPLOAD, countryCode);
+
+ String payload = new SignedStringMessageBuilder()
+ .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey)
+ .withPayload("randomBadString")
+ .buildAsString();
+
+ String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode);
+
+ mockMvc.perform(post("/revocation-list")
+ .content(payload)
+ .contentType("application/cms")
+ .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash)
+ .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject)
+ )
+ .andExpect(status().isBadRequest());
+
+ Assertions.assertEquals(revocationBatchesInDb, revocationBatchRepository.count());
+ Assertions.assertEquals(auditEventEntitiesInDb, auditEventRepository.count());
+ }
+
+ @Test
+ void testUploadFailedInvalidJsonValues() throws Exception {
+ long revocationBatchesInDb = revocationBatchRepository.count();
+ long auditEventEntitiesInDb = auditEventRepository.count();
+
+ X509Certificate signerCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, countryCode);
+ PrivateKey signerPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.UPLOAD, countryCode);
+
+ RevocationBatchDto revocationBatchDto = new RevocationBatchDto();
+ revocationBatchDto.setCountry(countryCode);
+ revocationBatchDto.setExpires(ZonedDateTime.now().plusDays(7));
+ revocationBatchDto.setHashType(RevocationHashTypeDto.SIGNATURE);
+ revocationBatchDto.setKid("KIDWHICHISWAYTOLONGTOPASS");
+ revocationBatchDto.setEntries(List.of(
+ new RevocationBatchDto.BatchEntryDto("aaaaaaaaaaaaaaaaaaaaaaaa"),
+ new RevocationBatchDto.BatchEntryDto("bbbbbbbbbbbbbbbbbbbbbbbb"),
+ new RevocationBatchDto.BatchEntryDto("cccccccccccccccccccccccc"),
+ new RevocationBatchDto.BatchEntryDto("dddddddddddddddddddddddd"),
+ new RevocationBatchDto.BatchEntryDto("eeeeeeeeeeeeeeeeeeeeeeee")
+ ));
+
+ String payload = new SignedStringMessageBuilder()
+ .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey)
+ .withPayload(objectMapper.writeValueAsString(revocationBatchDto))
+ .buildAsString();
+
+ String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode);
+
+ mockMvc.perform(post("/revocation-list")
+ .content(payload)
+ .contentType("application/cms")
+ .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash)
+ .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject)
+ )
+ .andExpect(status().isBadRequest());
+
+ Assertions.assertEquals(revocationBatchesInDb, revocationBatchRepository.count());
+ Assertions.assertEquals(auditEventEntitiesInDb, auditEventRepository.count());
+ }
+
+ @Test
+ void testUploadFailedInvalidJsonValuesInHashEntries() throws Exception {
+ long revocationBatchesInDb = revocationBatchRepository.count();
+ long auditEventEntitiesInDb = auditEventRepository.count();
+
+ X509Certificate signerCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, countryCode);
+ PrivateKey signerPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.UPLOAD, countryCode);
+
+ RevocationBatchDto revocationBatchDto = new RevocationBatchDto();
+ revocationBatchDto.setCountry(countryCode);
+ revocationBatchDto.setExpires(ZonedDateTime.now().plusDays(7));
+ revocationBatchDto.setHashType(RevocationHashTypeDto.SIGNATURE);
+ revocationBatchDto.setKid("UNKNOWN_KID");
+ revocationBatchDto.setEntries(List.of(
+ new RevocationBatchDto.BatchEntryDto("aaaaaaaaaaaaaaaaaaaaaaaa"),
+ new RevocationBatchDto.BatchEntryDto("bbbbbbbbbbbbbbbbbbbbbbbb"),
+ new RevocationBatchDto.BatchEntryDto("ccccccccccccccccccccccccA"),
+ new RevocationBatchDto.BatchEntryDto("dddddddddddddddddddddddd"),
+ new RevocationBatchDto.BatchEntryDto("eeeeeeeeeeeeeeeeeeeeeeee")
+ ));
+
+ String payload = new SignedStringMessageBuilder()
+ .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey)
+ .withPayload(objectMapper.writeValueAsString(revocationBatchDto))
+ .buildAsString();
+
+ String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode);
+
+ mockMvc.perform(post("/revocation-list")
+ .content(payload)
+ .contentType("application/cms")
+ .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash)
+ .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject)
+ )
+ .andExpect(status().isBadRequest());
+
+ Assertions.assertEquals(revocationBatchesInDb, revocationBatchRepository.count());
+ Assertions.assertEquals(auditEventEntitiesInDb, auditEventRepository.count());
+ }
+
+ @Test
+ void testUploadFailedInvalidCountry() throws Exception {
+ long revocationBatchesInDb = revocationBatchRepository.count();
+ long auditEventEntitiesInDb = auditEventRepository.count();
+
+ X509Certificate signerCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, countryCode);
+ PrivateKey signerPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.UPLOAD, countryCode);
+
+ RevocationBatchDto revocationBatchDto = new RevocationBatchDto();
+ revocationBatchDto.setCountry("XX");
+ revocationBatchDto.setExpires(ZonedDateTime.now().plusDays(7));
+ revocationBatchDto.setHashType(RevocationHashTypeDto.SIGNATURE);
+ revocationBatchDto.setKid("UNKNOWN_KID");
+ revocationBatchDto.setEntries(List.of(
+ new RevocationBatchDto.BatchEntryDto("aaaaaaaaaaaaaaaaaaaaaaaa"),
+ new RevocationBatchDto.BatchEntryDto("bbbbbbbbbbbbbbbbbbbbbbbb"),
+ new RevocationBatchDto.BatchEntryDto("cccccccccccccccccccccccc"),
+ new RevocationBatchDto.BatchEntryDto("dddddddddddddddddddddddd"),
+ new RevocationBatchDto.BatchEntryDto("eeeeeeeeeeeeeeeeeeeeeeee")
+ ));
+
+ String payload = new SignedStringMessageBuilder()
+ .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey)
+ .withPayload(objectMapper.writeValueAsString(revocationBatchDto))
+ .buildAsString();
+
+ String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode);
+
+ mockMvc.perform(post("/revocation-list")
+ .content(payload)
+ .contentType("application/cms")
+ .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash)
+ .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject)
+ )
+ .andExpect(status().isForbidden());
+
+ Assertions.assertEquals(revocationBatchesInDb, revocationBatchRepository.count());
+ Assertions.assertEquals(auditEventEntitiesInDb, auditEventRepository.count());
+ }
+
+ @Test
+ void testDeleteRevocationBatch() throws Exception {
+ RevocationBatchEntity entity = new RevocationBatchEntity();
+ entity.setType(RevocationBatchEntity.RevocationHashType.SIGNATURE);
+ entity.setBatchId(UUID.randomUUID().toString());
+ entity.setDeleted(false);
+ entity.setKid("UNKNOWN_KID");
+ entity.setSignedBatch("Batch1234");
+ entity.setExpires(ZonedDateTime.now().plusDays(5));
+ entity.setChanged(ZonedDateTime.now().minusDays(5));
+ entity.setCountry(countryCode);
+ revocationBatchRepository.save(entity);
+
+
+ long revocationBatchesInDb = revocationBatchRepository.count();
+ long auditEventEntitiesInDb = auditEventRepository.count();
+
+ X509Certificate signerCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, countryCode);
+ PrivateKey signerPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.UPLOAD, countryCode);
+
+ RevocationBatchDeleteRequestDto deleteRequestDto = new RevocationBatchDeleteRequestDto(entity.getBatchId());
+
+ String payload = new SignedStringMessageBuilder()
+ .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey)
+ .withPayload(objectMapper.writeValueAsString(deleteRequestDto))
+ .buildAsString();
+
+ String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode);
+
+ mockMvc.perform(delete("/revocation-list")
+ .content(payload)
+ .contentType("application/cms")
+ .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash)
+ .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject)
+ )
+ .andExpect(status().isNoContent());
+
+ Assertions.assertEquals(revocationBatchesInDb, revocationBatchRepository.count());
+ Assertions.assertEquals(auditEventEntitiesInDb + 1, auditEventRepository.count());
+
+ RevocationBatchEntity deletedBatch = revocationBatchRepository.findAll().get(0);
+ Assertions.assertNull(deletedBatch.getSignedBatch());
+ Assertions.assertTrue(deletedBatch.getDeleted());
+ Assertions.assertTrue(deletedBatch.getChanged().toEpochSecond() > ZonedDateTime.now().minusSeconds(2).toEpochSecond());
+ Assertions.assertTrue(deletedBatch.getChanged().toEpochSecond() < ZonedDateTime.now().plusSeconds(2).toEpochSecond());
+ }
+
+
+ @Test
+ void testDeleteRevocationBatchAlternativeEndpoint() throws Exception {
+ RevocationBatchEntity entity = new RevocationBatchEntity();
+ entity.setType(RevocationBatchEntity.RevocationHashType.SIGNATURE);
+ entity.setBatchId(UUID.randomUUID().toString());
+ entity.setDeleted(false);
+ entity.setKid("UNKNOWN_KID");
+ entity.setSignedBatch("Batch1234");
+ entity.setExpires(ZonedDateTime.now().plusDays(5));
+ entity.setChanged(ZonedDateTime.now().minusDays(5));
+ entity.setCountry(countryCode);
+ revocationBatchRepository.save(entity);
+
+
+ long revocationBatchesInDb = revocationBatchRepository.count();
+ long auditEventEntitiesInDb = auditEventRepository.count();
+
+ X509Certificate signerCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, countryCode);
+ PrivateKey signerPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.UPLOAD, countryCode);
+
+ RevocationBatchDeleteRequestDto deleteRequestDto = new RevocationBatchDeleteRequestDto(entity.getBatchId());
+
+ String payload = new SignedStringMessageBuilder()
+ .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey)
+ .withPayload(objectMapper.writeValueAsString(deleteRequestDto))
+ .buildAsString();
+
+ String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode);
+
+ mockMvc.perform(post("/revocation-list/delete")
+ .content(payload)
+ .contentType("application/cms")
+ .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash)
+ .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject)
+ )
+ .andExpect(status().isNoContent());
+
+ Assertions.assertEquals(revocationBatchesInDb, revocationBatchRepository.count());
+ Assertions.assertEquals(auditEventEntitiesInDb + 1, auditEventRepository.count());
+
+ RevocationBatchEntity deletedBatch = revocationBatchRepository.findAll().get(0);
+ Assertions.assertNull(deletedBatch.getSignedBatch());
+ Assertions.assertTrue(deletedBatch.getDeleted());
+ Assertions.assertTrue(deletedBatch.getChanged().toEpochSecond() > ZonedDateTime.now().minusSeconds(2).toEpochSecond());
+ Assertions.assertTrue(deletedBatch.getChanged().toEpochSecond() < ZonedDateTime.now().plusSeconds(2).toEpochSecond());
+ }
+
+ @Test
+ void testDeleteRevocationBatchFailedInvalidJson() throws Exception {
+ RevocationBatchEntity entity = new RevocationBatchEntity();
+ entity.setType(RevocationBatchEntity.RevocationHashType.SIGNATURE);
+ entity.setBatchId(UUID.randomUUID().toString());
+ entity.setDeleted(false);
+ entity.setKid("UNKNOWN_KID");
+ entity.setSignedBatch("Batch1234");
+ entity.setExpires(ZonedDateTime.now().plusDays(5));
+ entity.setChanged(ZonedDateTime.now().minusDays(5));
+ entity.setCountry(countryCode);
+ revocationBatchRepository.save(entity);
+
+ long auditEventEntitiesInDb = auditEventRepository.count();
+
+ X509Certificate signerCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, countryCode);
+ PrivateKey signerPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.UPLOAD, countryCode);
+
+ String payload = new SignedStringMessageBuilder()
+ .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey)
+ .withPayload("randomString")
+ .buildAsString();
+
+ String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode);
+
+ mockMvc.perform(delete("/revocation-list")
+ .content(payload)
+ .contentType("application/cms")
+ .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash)
+ .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject)
+ )
+ .andExpect(status().isBadRequest());
+
+ Assertions.assertEquals(auditEventEntitiesInDb, auditEventRepository.count());
+
+ RevocationBatchEntity entityAfterDelete = revocationBatchRepository.findById(entity.getId()).orElseThrow();
+ assertEquals(entity, entityAfterDelete);
+ }
+
+ @Test
+ void testDeleteRevocationBatchFailedInvalidJsonValue() throws Exception {
+ RevocationBatchEntity entity = new RevocationBatchEntity();
+ entity.setType(RevocationBatchEntity.RevocationHashType.SIGNATURE);
+ entity.setBatchId(UUID.randomUUID().toString());
+ entity.setDeleted(false);
+ entity.setKid("UNKNOWN_KID");
+ entity.setSignedBatch("Batch1234");
+ entity.setExpires(ZonedDateTime.now().plusDays(5));
+ entity.setChanged(ZonedDateTime.now().minusDays(5));
+ entity.setCountry(countryCode);
+ revocationBatchRepository.save(entity);
+
+ long auditEventEntitiesInDb = auditEventRepository.count();
+
+ X509Certificate signerCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, countryCode);
+ PrivateKey signerPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.UPLOAD, countryCode);
+
+ String payload = new SignedStringMessageBuilder()
+ .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey)
+ .withPayload(objectMapper.writeValueAsString(new RevocationBatchDeleteRequestDto("ThisIsNotAnUUID")))
+ .buildAsString();
+
+ String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode);
+
+ mockMvc.perform(delete("/revocation-list")
+ .content(payload)
+ .contentType("application/cms")
+ .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash)
+ .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject)
+ )
+ .andExpect(status().isBadRequest());
+
+ Assertions.assertEquals(auditEventEntitiesInDb, auditEventRepository.count());
+
+ RevocationBatchEntity entityAfterDelete = revocationBatchRepository.findById(entity.getId()).orElseThrow();
+ assertEquals(entity, entityAfterDelete);
+ }
+
+ @Test
+ void testDeleteRevocationBatchFailedBatchNotFound() throws Exception {
+ RevocationBatchEntity entity = new RevocationBatchEntity();
+ entity.setType(RevocationBatchEntity.RevocationHashType.SIGNATURE);
+ entity.setBatchId(UUID.randomUUID().toString());
+ entity.setDeleted(false);
+ entity.setKid("UNKNOWN_KID");
+ entity.setSignedBatch("Batch1234");
+ entity.setExpires(ZonedDateTime.now().plusDays(5));
+ entity.setChanged(ZonedDateTime.now().minusDays(5));
+ entity.setCountry(countryCode);
+ revocationBatchRepository.save(entity);
+
+ long auditEventEntitiesInDb = auditEventRepository.count();
+
+ X509Certificate signerCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, countryCode);
+ PrivateKey signerPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.UPLOAD, countryCode);
+
+ String payload = new SignedStringMessageBuilder()
+ .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey)
+ .withPayload(objectMapper.writeValueAsString(new RevocationBatchDeleteRequestDto(UUID.randomUUID().toString())))
+ .buildAsString();
+
+ String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode);
+
+ mockMvc.perform(delete("/revocation-list")
+ .content(payload)
+ .contentType("application/cms")
+ .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash)
+ .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject)
+ )
+ .andExpect(status().isNotFound());
+
+ Assertions.assertEquals(auditEventEntitiesInDb, auditEventRepository.count());
+
+ RevocationBatchEntity entityAfterDelete = revocationBatchRepository.findById(entity.getId()).orElseThrow();
+ assertEquals(entity, entityAfterDelete);
+ }
+
+ @Test
+ void testDeleteRevocationBatchFailedInvalidCountry() throws Exception {
+ RevocationBatchEntity entity = new RevocationBatchEntity();
+ entity.setType(RevocationBatchEntity.RevocationHashType.SIGNATURE);
+ entity.setBatchId(UUID.randomUUID().toString());
+ entity.setDeleted(false);
+ entity.setKid("UNKNOWN_KID");
+ entity.setSignedBatch("Batch1234");
+ entity.setExpires(ZonedDateTime.now().plusDays(5));
+ entity.setChanged(ZonedDateTime.now().minusDays(5));
+ entity.setCountry(countryCode);
+ revocationBatchRepository.save(entity);
+
+ long auditEventEntitiesInDb = auditEventRepository.count();
+
+ X509Certificate signerCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, "XX");
+ PrivateKey signerPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.UPLOAD, "XX");
+
+ RevocationBatchDeleteRequestDto deleteRequestDto = new RevocationBatchDeleteRequestDto(entity.getBatchId());
+
+ String payload = new SignedStringMessageBuilder()
+ .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey)
+ .withPayload(objectMapper.writeValueAsString(deleteRequestDto))
+ .buildAsString();
+
+ String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, "XX");
+
+ mockMvc.perform(delete("/revocation-list")
+ .content(payload)
+ .contentType("application/cms")
+ .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash)
+ .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), "C=XX")
+ )
+ .andExpect(status().isForbidden());
+
+ Assertions.assertEquals(auditEventEntitiesInDb, auditEventRepository.count());
+
+ RevocationBatchEntity entityAfterDelete = revocationBatchRepository.findById(entity.getId()).orElseThrow();
+ assertEquals(entity, entityAfterDelete);
+ }
+
+ @Test
+ void testDeleteRevocationBatchFailedUploadDoesNotMatchAuth() throws Exception {
+ RevocationBatchEntity entity = new RevocationBatchEntity();
+ entity.setType(RevocationBatchEntity.RevocationHashType.SIGNATURE);
+ entity.setBatchId(UUID.randomUUID().toString());
+ entity.setDeleted(false);
+ entity.setKid("UNKNOWN_KID");
+ entity.setSignedBatch("Batch1234");
+ entity.setExpires(ZonedDateTime.now().plusDays(5));
+ entity.setChanged(ZonedDateTime.now().minusDays(5));
+ entity.setCountry(countryCode);
+ revocationBatchRepository.save(entity);
+
+ long auditEventEntitiesInDb = auditEventRepository.count();
+
+ X509Certificate signerCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, "XX");
+ PrivateKey signerPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.UPLOAD, "XX");
+
+ RevocationBatchDeleteRequestDto deleteRequestDto = new RevocationBatchDeleteRequestDto(entity.getBatchId());
+
+ String payload = new SignedStringMessageBuilder()
+ .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey)
+ .withPayload(objectMapper.writeValueAsString(deleteRequestDto))
+ .buildAsString();
+
+ String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode);
+
+ mockMvc.perform(delete("/revocation-list")
+ .content(payload)
+ .contentType("application/cms")
+ .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash)
+ .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject)
+ )
+ .andExpect(status().isForbidden());
+
+ Assertions.assertEquals(auditEventEntitiesInDb, auditEventRepository.count());
+
+ RevocationBatchEntity entityAfterDelete = revocationBatchRepository.findById(entity.getId()).orElseThrow();
+ assertEquals(entity, entityAfterDelete);
+ }
+
+ @Test
+ void testDeleteRevocationBatchFailedInvalidCmsSignature() throws Exception {
+ RevocationBatchEntity entity = new RevocationBatchEntity();
+ entity.setType(RevocationBatchEntity.RevocationHashType.SIGNATURE);
+ entity.setBatchId(UUID.randomUUID().toString());
+ entity.setDeleted(false);
+ entity.setKid("UNKNOWN_KID");
+ entity.setSignedBatch("Batch1234");
+ entity.setExpires(ZonedDateTime.now().plusDays(5));
+ entity.setChanged(ZonedDateTime.now().minusDays(5));
+ entity.setCountry(countryCode);
+ revocationBatchRepository.save(entity);
+
+ long auditEventEntitiesInDb = auditEventRepository.count();
+
+ X509Certificate signerCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, countryCode);
+ PrivateKey signerPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.UPLOAD, "XX");
+
+ RevocationBatchDeleteRequestDto deleteRequestDto = new RevocationBatchDeleteRequestDto(entity.getBatchId());
+
+ String payload = new SignedStringMessageBuilder()
+ .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey)
+ .withPayload(objectMapper.writeValueAsString(deleteRequestDto))
+ .buildAsString();
+
+ String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode);
+
+ mockMvc.perform(delete("/revocation-list")
+ .content(payload)
+ .contentType("application/cms")
+ .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash)
+ .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject)
+ )
+ .andExpect(status().isBadRequest());
+
+ Assertions.assertEquals(auditEventEntitiesInDb, auditEventRepository.count());
+
+ RevocationBatchEntity entityAfterDelete = revocationBatchRepository.findById(entity.getId()).orElseThrow();
+ assertEquals(entity, entityAfterDelete);
+ }
+
+ @Test
+ void testDeleteRevocationBatchFailedGone() throws Exception {
+ RevocationBatchEntity entity = new RevocationBatchEntity();
+ entity.setType(RevocationBatchEntity.RevocationHashType.SIGNATURE);
+ entity.setBatchId(UUID.randomUUID().toString());
+ entity.setDeleted(true);
+ entity.setKid("UNKNOWN_KID");
+ entity.setSignedBatch("Batch1234");
+ entity.setExpires(ZonedDateTime.now().plusDays(5));
+ entity.setChanged(ZonedDateTime.now().minusDays(5));
+ entity.setCountry(countryCode);
+ revocationBatchRepository.save(entity);
+
+ long auditEventEntitiesInDb = auditEventRepository.count();
+
+ X509Certificate signerCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, countryCode);
+ PrivateKey signerPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.UPLOAD, countryCode);
+
+ RevocationBatchDeleteRequestDto deleteRequestDto = new RevocationBatchDeleteRequestDto(entity.getBatchId());
+
+ String payload = new SignedStringMessageBuilder()
+ .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey)
+ .withPayload(objectMapper.writeValueAsString(deleteRequestDto))
+ .buildAsString();
+
+ String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode);
+
+ mockMvc.perform(delete("/revocation-list")
+ .content(payload)
+ .contentType("application/cms")
+ .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash)
+ .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject)
+ )
+ .andExpect(status().isGone());
+
+ Assertions.assertEquals(auditEventEntitiesInDb, auditEventRepository.count());
+
+ RevocationBatchEntity entityAfterDelete = revocationBatchRepository.findById(entity.getId()).orElseThrow();
+ assertEquals(entity, entityAfterDelete);
+ }
+
+ @Test
+ void testDownloadBatchList() throws Exception {
+
+ ArrayList entities = new ArrayList<>();
+
+ for (int i = 5500; i > 0; i--) {
+ RevocationBatchEntity entity = new RevocationBatchEntity();
+ entity.setType(RevocationBatchEntity.RevocationHashType.SIGNATURE);
+ entity.setBatchId(UUID.randomUUID().toString());
+ entity.setDeleted(i % 2 == 0);
+ entity.setKid("UNKNOWN_KID");
+ entity.setSignedBatch("Batch1234");
+ entity.setExpires(ZonedDateTime.now().plusDays(5));
+ entity.setChanged(ZonedDateTime.now().minusMinutes(i));
+ entity.setCountry(countryCode);
+ entities.add(revocationBatchRepository.save(entity));
+ }
+
+ String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode);
+
+ mockMvc.perform(get("/revocation-list")
+ .accept("application/json")
+ .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash)
+ .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject)
+ .header(HttpHeaders.IF_MODIFIED_SINCE, entities.get(0).getChanged().minusSeconds(1).toOffsetDateTime().toString())
+ )
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.more").value(true))
+ .andExpect(jsonPath("$.batches.length()").value(1000))
+ .andDo(r -> evaluateDownloadedBatchList(r.getResponse(), entities.subList(0, 1000)));
+
+ mockMvc.perform(get("/revocation-list")
+ .accept("application/json")
+ .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash)
+ .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject)
+ .header(HttpHeaders.IF_MODIFIED_SINCE, entities.get(1000).getChanged().minusSeconds(1).toOffsetDateTime().toString())
+ )
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.more").value(true))
+ .andExpect(jsonPath("$.batches.length()").value(1000))
+ .andDo(r -> evaluateDownloadedBatchList(r.getResponse(), entities.subList(1000, 2000)));
+
+ mockMvc.perform(get("/revocation-list")
+ .accept("application/json")
+ .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash)
+ .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject)
+ .header(HttpHeaders.IF_MODIFIED_SINCE, entities.get(2000).getChanged().minusSeconds(1).toOffsetDateTime().toString())
+ )
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.more").value(true))
+ .andExpect(jsonPath("$.batches.length()").value(1000))
+ .andDo(r -> evaluateDownloadedBatchList(r.getResponse(), entities.subList(2000, 3000)));
+
+ mockMvc.perform(get("/revocation-list")
+ .accept("application/json")
+ .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash)
+ .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject)
+ .header(HttpHeaders.IF_MODIFIED_SINCE, entities.get(3000).getChanged().minusSeconds(1).toOffsetDateTime().toString())
+ )
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.more").value(true))
+ .andExpect(jsonPath("$.batches.length()").value(1000))
+ .andDo(r -> evaluateDownloadedBatchList(r.getResponse(), entities.subList(3000, 4000)));
+
+ mockMvc.perform(get("/revocation-list")
+ .accept("application/json")
+ .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash)
+ .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject)
+ .header(HttpHeaders.IF_MODIFIED_SINCE, entities.get(4000).getChanged().minusSeconds(1).toOffsetDateTime().toString())
+ )
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.more").value(true))
+ .andExpect(jsonPath("$.batches.length()").value(1000))
+ .andDo(r -> evaluateDownloadedBatchList(r.getResponse(), entities.subList(4000, 5000)));
+
+ mockMvc.perform(get("/revocation-list")
+ .accept("application/json")
+ .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash)
+ .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject)
+ .header(HttpHeaders.IF_MODIFIED_SINCE, entities.get(5000).getChanged().minusSeconds(1).toOffsetDateTime().toString())
+ )
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.more").value(false))
+ .andExpect(jsonPath("$.batches.length()").value(500))
+ .andDo(r -> evaluateDownloadedBatchList(r.getResponse(), entities.subList(5000, 5500)));
+
+ mockMvc.perform(get("/revocation-list")
+ .accept("application/json")
+ .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash)
+ .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject)
+ .header(HttpHeaders.IF_MODIFIED_SINCE, entities.get(5499).getChanged().plusSeconds(1).toOffsetDateTime().toString())
+ )
+ .andExpect(status().isNoContent());
+ }
+
+ @Test
+ void testDownloadBatchListFailedNoIfModifiedSince() throws Exception {
+
+ String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode);
+
+ mockMvc.perform(get("/revocation-list")
+ .accept("application/json")
+ .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash)
+ .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject)
+ )
+ .andExpect(status().isBadRequest());
+ }
+
+ @Test
+ void testDownloadBatchListFailedIfModifiedSinceInFuture() throws Exception {
+
+ String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode);
+
+ mockMvc.perform(get("/revocation-list")
+ .accept("application/json")
+ .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash)
+ .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject)
+ .header(HttpHeaders.IF_MODIFIED_SINCE, OffsetDateTime.now().plusSeconds(1).toString())
+ )
+ .andExpect(status().isBadRequest());
+ }
+
+ @Test
+ void testDownloadRevocationBatch() throws Exception {
+ X509Certificate signerCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, countryCode);
+ PrivateKey signerPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.UPLOAD, countryCode);
+
+ RevocationBatchDto batchDto = new RevocationBatchDto();
+ batchDto.setCountry(countryCode);
+ batchDto.setExpires(ZonedDateTime.now().plusDays(5));
+ batchDto.setHashType(RevocationHashTypeDto.SIGNATURE);
+ batchDto.setKid("UNKNOWN_KID");
+ batchDto.setEntries(List.of(new RevocationBatchDto.BatchEntryDto("abcd")));
+
+ String signedBatch = new SignedStringMessageBuilder()
+ .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey)
+ .withPayload(objectMapper.writeValueAsString(batchDto))
+ .buildAsString();
+
+ RevocationBatchEntity entity = new RevocationBatchEntity();
+ entity.setType(RevocationBatchEntity.RevocationHashType.SIGNATURE);
+ entity.setBatchId(UUID.randomUUID().toString());
+ entity.setDeleted(false);
+ entity.setKid("UNKNOWN_KID");
+ entity.setSignedBatch(signedBatch);
+ entity.setExpires(batchDto.getExpires());
+ entity.setChanged(ZonedDateTime.now().minusDays(5));
+ entity.setCountry(countryCode);
+ revocationBatchRepository.save(entity);
+
+ String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode);
+
+ mockMvc.perform(get("/revocation-list/" + entity.getBatchId())
+ .accept("application/cms")
+ .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash)
+ .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject)
+ )
+ .andExpect(status().isOk())
+ .andDo(result -> {
+ SignedStringMessageParser parser = new SignedStringMessageParser(result.getResponse().getContentAsString());
+
+ Assertions.assertEquals(SignedMessageParser.ParserState.SUCCESS, parser.getParserState());
+ Assertions.assertTrue(parser.isSignatureVerified());
+ Assertions.assertArrayEquals(signerCertificate.getEncoded(), parser.getSigningCertificate().getEncoded());
+
+ RevocationBatchDto parsedBatch = objectMapper.readValue(parser.getPayload(), RevocationBatchDto.class);
+
+ assertEquals(batchDto, parsedBatch);
+ });
+ }
+
+ @Test
+ void testDownloadRevocationBatchInvalidBatchId() throws Exception {
+ String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode);
+
+ mockMvc.perform(get("/revocation-list/thisIsNotAnUUID")
+ .accept("application/cms")
+ .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash)
+ .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject)
+ )
+ .andExpect(status().isBadRequest());
+ }
+
+ @Test
+ void testDownloadRevocationBatchGone() throws Exception {
+
+ RevocationBatchEntity entity = new RevocationBatchEntity();
+ entity.setType(RevocationBatchEntity.RevocationHashType.SIGNATURE);
+ entity.setBatchId(UUID.randomUUID().toString());
+ entity.setDeleted(true);
+ entity.setKid("UNKNOWN_KID");
+ entity.setSignedBatch("abcd");
+ entity.setExpires(ZonedDateTime.now().plusDays(5));
+ entity.setChanged(ZonedDateTime.now().minusDays(5));
+ entity.setCountry(countryCode);
+ revocationBatchRepository.save(entity);
+
+ String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode);
+
+ mockMvc.perform(get("/revocation-list/" + entity.getBatchId())
+ .accept("application/cms")
+ .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash)
+ .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject)
+ )
+ .andExpect(status().isGone());
+ }
+
+ @Test
+ void testDownloadRevocationBatchNotFound() throws Exception {
+
+ String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode);
+
+ mockMvc.perform(get("/revocation-list/" + UUID.randomUUID())
+ .accept("application/cms")
+ .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash)
+ .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject)
+ )
+ .andExpect(status().isNotFound());
+ }
+
+ private void evaluateDownloadedBatchList(MockHttpServletResponse mockResponse, List expectedBatches) throws UnsupportedEncodingException, JsonProcessingException {
+ RevocationBatchListDto revocationBatchListDto =
+ objectMapper.readValue(mockResponse.getContentAsString(), RevocationBatchListDto.class);
+
+ Assertions.assertEquals(expectedBatches.size(), revocationBatchListDto.getBatches().size());
+
+ for (int i = 0; i < revocationBatchListDto.getBatches().size(); i++) {
+ assertEquals(expectedBatches.get(i), revocationBatchListDto.getBatches().get(i));
+ }
+ }
+
+ private static void assertEquals(RevocationBatchEntity expected, RevocationBatchListDto.RevocationBatchListItemDto actual) {
+ Assertions.assertEquals(expected.getBatchId(), actual.getBatchId());
+ Assertions.assertEquals(expected.getChanged().toEpochSecond(), actual.getDate().toEpochSecond());
+ Assertions.assertEquals(expected.getCountry(), actual.getCountry());
+ Assertions.assertEquals(expected.getDeleted(), actual.getDeleted());
+ }
+
+
+ private static void assertEquals(RevocationBatchDto expected, RevocationBatchDto actual) {
+ Assertions.assertEquals(expected.getKid(), actual.getKid());
+ Assertions.assertEquals(expected.getExpires().toEpochSecond(), actual.getExpires().toEpochSecond());
+ Assertions.assertEquals(expected.getHashType(), actual.getHashType());
+ Assertions.assertEquals(expected.getCountry(), actual.getCountry());
+ Assertions.assertEquals(expected.getEntries(), actual.getEntries());
+ }
+
+ public static void assertEquals(RevocationBatchEntity expected, RevocationBatchEntity actual) {
+ Assertions.assertEquals(expected.getBatchId(), actual.getBatchId());
+ Assertions.assertEquals(expected.getCountry(), actual.getCountry());
+ Assertions.assertEquals(expected.getKid(), actual.getKid());
+ Assertions.assertEquals(expected.getType(), actual.getType());
+ Assertions.assertEquals(expected.getExpires().toEpochSecond(), actual.getExpires().toEpochSecond());
+ Assertions.assertEquals(expected.getChanged().toEpochSecond(), actual.getChanged().toEpochSecond());
+ Assertions.assertEquals(expected.getDeleted(), actual.getDeleted());
+ Assertions.assertEquals(expected.getId(), expected.getId());
+ Assertions.assertEquals(expected.getSignedBatch(), expected.getSignedBatch());
+ }
+}
diff --git a/src/test/java/eu/europa/ec/dgc/gateway/service/CertificateRevocationListCleanupTest.java b/src/test/java/eu/europa/ec/dgc/gateway/service/CertificateRevocationListCleanupTest.java
new file mode 100644
index 00000000..3aa10a44
--- /dev/null
+++ b/src/test/java/eu/europa/ec/dgc/gateway/service/CertificateRevocationListCleanupTest.java
@@ -0,0 +1,96 @@
+/*-
+ * ---license-start
+ * EU Digital Green Certificate Gateway Service / dgc-gateway
+ * ---
+ * 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.service;
+
+import eu.europa.ec.dgc.gateway.entity.RevocationBatchEntity;
+import eu.europa.ec.dgc.gateway.repository.RevocationBatchRepository;
+import eu.europa.ec.dgc.gateway.restapi.controller.CertificateRevocationListIntegrationTest;
+import java.time.ZonedDateTime;
+import lombok.extern.slf4j.Slf4j;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+
+@SpringBootTest(properties = "dgc.revocation.delete-threshold=14")
+@Slf4j
+class CertificateRevocationListCleanupTest {
+
+ @Autowired
+ RevocationBatchRepository revocationBatchRepository;
+
+ @Autowired
+ RevocationListCleanUpService cleanUpService;
+
+ @BeforeEach
+ public void setup() {
+ revocationBatchRepository.deleteAll();
+ }
+
+ @Test
+ public void testCleanup() {
+
+ // Batch which expired 5 days ago --> marked as deleted
+ RevocationBatchEntity e1 = new RevocationBatchEntity(
+ null, "batchId1", "EU", ZonedDateTime.now(), ZonedDateTime.now().minusDays(5),
+ false, RevocationBatchEntity.RevocationHashType.SIGNATURE, "UNKNOWN_KID", "cms");
+ e1 = revocationBatchRepository.save(e1);
+
+ // Batch which will expire within 2 days --> don't touch
+ RevocationBatchEntity e2 = new RevocationBatchEntity(
+ null, "batchId2", "EU", ZonedDateTime.now(), ZonedDateTime.now().plusDays(2),
+ false, RevocationBatchEntity.RevocationHashType.SIGNATURE, "UNKNOWN_KID", "cms");
+ e2 = revocationBatchRepository.save(e2);
+
+ // Batch which is expired 5 days ago, marked as deleted 5 days ago --> don't touch
+ RevocationBatchEntity e3 = new RevocationBatchEntity(
+ null, "batchId3", "EU", ZonedDateTime.now().minusDays(5), ZonedDateTime.now().minusDays(5),
+ true, RevocationBatchEntity.RevocationHashType.SIGNATURE, "UNKNOWN_KID", null);
+ e3 = revocationBatchRepository.save(e3);
+
+ // Batch which is expired 16 days ago, marked as deleted 16 days ago --> delete
+ RevocationBatchEntity e4 = new RevocationBatchEntity(
+ null, "batchId4", "EU", ZonedDateTime.now().minusDays(16), ZonedDateTime.now().minusDays(16),
+ true, RevocationBatchEntity.RevocationHashType.SIGNATURE, "UNKNOWN_KID", null);
+ e4 = revocationBatchRepository.save(e4);
+
+
+ cleanUpService.cleanup();
+
+
+ RevocationBatchEntity newE1 = revocationBatchRepository.getByBatchId(e1.getBatchId()).orElseThrow();
+ Assertions.assertTrue(newE1.getDeleted());
+ Assertions.assertNull(newE1.getSignedBatch());
+ Assertions.assertTrue(newE1.getChanged().toEpochSecond() < ZonedDateTime.now().plusSeconds(2).toEpochSecond());
+ Assertions.assertTrue(newE1.getChanged().toEpochSecond() > ZonedDateTime.now().minusSeconds(2).toEpochSecond());
+
+ RevocationBatchEntity newE2 = revocationBatchRepository.getByBatchId(e2.getBatchId()).orElseThrow();
+ CertificateRevocationListIntegrationTest.assertEquals(e2, newE2);
+
+ RevocationBatchEntity newE3 = revocationBatchRepository.getByBatchId(e3.getBatchId()).orElseThrow();
+ CertificateRevocationListIntegrationTest.assertEquals(e3, newE3);
+
+ Assertions.assertTrue(revocationBatchRepository.getByBatchId(e4.getBatchId()).isEmpty());
+ }
+
+
+}
diff --git a/src/test/java/eu/europa/ec/dgc/gateway/service/RatValuesetUpdateServiceTest.java b/src/test/java/eu/europa/ec/dgc/gateway/service/RatValuesetUpdateServiceTest.java
index fc73b5e6..d0950e64 100644
--- a/src/test/java/eu/europa/ec/dgc/gateway/service/RatValuesetUpdateServiceTest.java
+++ b/src/test/java/eu/europa/ec/dgc/gateway/service/RatValuesetUpdateServiceTest.java
@@ -356,7 +356,7 @@ void testRatValuesetUpdatedSkipIfHistoryNull() throws JsonProcessingException {
@Test
void testRatValuesetUpdateShouldNotUpdateWhenRequestFails() throws JsonProcessingException {
- doThrow(new FeignException.Unauthorized("", dummyRequest, null))
+ doThrow(new FeignException.Unauthorized("", dummyRequest, null, null))
.when(jrcClientMock).downloadRatValues();
ratValuesetUpdateService.update();