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();