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 index 5729a846..74a5a02d 100644 --- a/src/main/java/eu/europa/ec/dgc/gateway/repository/RevocationBatchRepository.java +++ b/src/main/java/eu/europa/ec/dgc/gateway/repository/RevocationBatchRepository.java @@ -59,4 +59,5 @@ public interface RevocationBatchRepository extends JpaRepository getAllByChangedGreaterThanOrderByChangedAsc(ZonedDateTime date, Pageable page); + List getAllByCountry(String country); } diff --git a/src/main/java/eu/europa/ec/dgc/gateway/repository/ValidationRuleRepository.java b/src/main/java/eu/europa/ec/dgc/gateway/repository/ValidationRuleRepository.java index 25d7d836..2f38175f 100644 --- a/src/main/java/eu/europa/ec/dgc/gateway/repository/ValidationRuleRepository.java +++ b/src/main/java/eu/europa/ec/dgc/gateway/repository/ValidationRuleRepository.java @@ -57,4 +57,6 @@ List getByRuleIdAndValidFromIsGreaterThanEqualOrderByIdDes Optional getByRuleIdAndVersion(String ruleId, String version); + List getAllByCountry(String country); + } diff --git a/src/main/java/eu/europa/ec/dgc/gateway/restapi/controller/CertificateMigrationController.java b/src/main/java/eu/europa/ec/dgc/gateway/restapi/controller/CertificateMigrationController.java new file mode 100644 index 00000000..92051c5e --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/restapi/controller/CertificateMigrationController.java @@ -0,0 +1,242 @@ +package eu.europa.ec.dgc.gateway.restapi.controller; + +import static eu.europa.ec.dgc.gateway.utils.CmsUtils.getSignedString; +import static eu.europa.ec.dgc.gateway.utils.CmsUtils.getSignerCertificate; + +import eu.europa.ec.dgc.gateway.config.OpenApiConfig; +import eu.europa.ec.dgc.gateway.exception.DgcgResponseException; +import eu.europa.ec.dgc.gateway.restapi.dto.CmsPackageDto; +import eu.europa.ec.dgc.gateway.restapi.dto.SignedCertificateDto; +import eu.europa.ec.dgc.gateway.restapi.dto.SignedStringDto; +import eu.europa.ec.dgc.gateway.restapi.filter.CertificateAuthenticationFilter; +import eu.europa.ec.dgc.gateway.restapi.filter.CertificateAuthenticationRequired; +import eu.europa.ec.dgc.gateway.service.RevocationListService; +import eu.europa.ec.dgc.gateway.service.SignerInformationService; +import eu.europa.ec.dgc.gateway.service.ValidationRuleService; +import eu.europa.ec.dgc.gateway.utils.DgcMdc; +import io.swagger.v3.oas.annotations.Operation; +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.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +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.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + + +@RestController +@RequestMapping("/cms-migration") +@Slf4j +@RequiredArgsConstructor +public class CertificateMigrationController { + + public static final String SENT_VALUES_FORMAT = "{%s} country:{%s}"; + public static final String X_004 = "0x004"; + public static final String DEFAULT_ERROR_MESSAGE = "Possible reasons: Wrong Format," + + " no CMS, not the correct signing alg missing attributes, invalid signature, " + + "certificate not signed by known CA"; + + private final SignerInformationService signerInformationService; + + private final RevocationListService revocationListService; + + private final ValidationRuleService validationRuleService; + + private static final String MDC_VERIFICATION_ERROR_REASON = "verificationFailureReason"; + private static final String MDC_VERIFICATION_ERROR_MESSAGE = "verificationFailureMessage"; + + /** + * Get CMS Packages for country. + */ + @CertificateAuthenticationRequired + @GetMapping + @Operation( + security = { + @SecurityRequirement(name = OpenApiConfig.SECURITY_SCHEMA_HASH), + @SecurityRequirement(name = OpenApiConfig.SECURITY_SCHEMA_DISTINGUISH_NAME) + }, + summary = "Get all cms packages for a country identified by certificate.", + tags = {"CMS Migration"}, + responses = { + @ApiResponse( + responseCode = "200", + description = "Download successful.", + content = @Content( + mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = CmsPackageDto.class) + ) + ) + } + ) + public ResponseEntity> getCmsPackages( + @RequestAttribute(CertificateAuthenticationFilter.REQUEST_PROP_COUNTRY) String countryCode + ) { + + log.info("Getting cms packages for {}", countryCode); + + List listItems = signerInformationService.getCmsPackage(countryCode); + log.info("Found {} signerInformation DSC entries", listItems.size()); + + List revocationList = revocationListService.getCmsPackage(countryCode); + log.info("Found {} revocation entries", revocationList.size()); + listItems.addAll(revocationList); + + List validationList = validationRuleService.getCmsPackage(countryCode); + log.info("Found {} validation rule entries", validationList.size()); + listItems.addAll(validationList); + + return ResponseEntity.ok(listItems); + } + + /** + * Update a CMS Package. + * + */ + @CertificateAuthenticationRequired + @PostMapping + @Operation( + security = { + @SecurityRequirement(name = OpenApiConfig.SECURITY_SCHEMA_HASH), + @SecurityRequirement(name = OpenApiConfig.SECURITY_SCHEMA_DISTINGUISH_NAME) + }, + tags = {"CMS Migration"}, + summary = "Update an existing CMS Package", + description = "Endpoint to update an existing CMS pacakage.", + requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody( + required = true, + content = @Content(schema = @Schema(implementation = CmsPackageDto.class)) + ), + responses = { + @ApiResponse( + responseCode = "204", + description = "Update applied."), + @ApiResponse( + responseCode = "409", + description = "CMS Package does not exist."), + @ApiResponse( + responseCode = "400", + description = "Invalid CMS input.") + } + ) + public ResponseEntity updateCmsPackage( + @RequestBody CmsPackageDto cmsPackageDto, + @RequestAttribute(CertificateAuthenticationFilter.REQUEST_PROP_COUNTRY) String countryCode, + @RequestAttribute(CertificateAuthenticationFilter.REQUEST_PROP_THUMBPRINT) String authThumbprint + ) { + + if (CmsPackageDto.CmsPackageTypeDto.DSC == cmsPackageDto.getType()) { + SignedCertificateDto signedCertificateDto = getSignerCertificate(cmsPackageDto.getCms()); + if (!signedCertificateDto.isVerified()) { + throw new DgcgResponseException(HttpStatus.BAD_REQUEST, "0x260", "CMS signature is invalid", "", + "Submitted package needs to be signed by a valid upload certificate"); + } + + try { + signerInformationService.updateSignerCertificate(cmsPackageDto.getEntityId(), + signedCertificateDto.getPayloadCertificate(), signedCertificateDto.getSignerCertificate(), + signedCertificateDto.getSignature(), countryCode); + } catch (SignerInformationService.SignerCertCheckException e) { + handleSignerCertException(cmsPackageDto, countryCode, e); + } + } else { + SignedStringDto signedStringDto = getSignedString(cmsPackageDto.getCms()); + + if (!signedStringDto.isVerified()) { + throw new DgcgResponseException(HttpStatus.BAD_REQUEST, "0x260", "CMS signature is invalid", "", + "Submitted package needs to be signed by a valid upload certificate"); + } + try { + if (CmsPackageDto.CmsPackageTypeDto.REVOCATION_LIST == cmsPackageDto.getType()) { + revocationListService.updateRevocationBatchCertificate(cmsPackageDto.getEntityId(), + signedStringDto.getPayloadString(), signedStringDto.getSignerCertificate(), + signedStringDto.getRawMessage(), countryCode); + } else if (CmsPackageDto.CmsPackageTypeDto.VALIDATION_RULE == cmsPackageDto.getType()) { + validationRuleService.updateValidationRuleCertificate(cmsPackageDto.getEntityId(), + signedStringDto.getPayloadString(), signedStringDto.getSignerCertificate(), + signedStringDto.getRawMessage(), countryCode); + } + } catch (RevocationListService.RevocationBatchServiceException e) { + handleRevocationBatchException(cmsPackageDto, countryCode, e); + } catch (ValidationRuleService.ValidationRuleCheckException e) { + handleValidationRuleExcepetion(cmsPackageDto, countryCode, e); + } + } + + return ResponseEntity.noContent().build(); + } + + private void updateMdc(String s, String message) { + DgcMdc.put(MDC_VERIFICATION_ERROR_REASON, s); + DgcMdc.put(MDC_VERIFICATION_ERROR_MESSAGE, message); + log.error("CMS migration failed"); + } + + private void handleSignerCertException(CmsPackageDto cmsPackageDto, String countryCode, + SignerInformationService.SignerCertCheckException e) { + updateMdc(e.getReason().toString(), e.getMessage()); + String sentValues = String.format(SENT_VALUES_FORMAT, cmsPackageDto, countryCode); + switch (e.getReason()) { + case EXIST_CHECK_FAILED: + throw new DgcgResponseException(HttpStatus.CONFLICT, "0x010", + "Certificate to be updated does not exist.", + sentValues, e.getMessage()); + case UPLOAD_FAILED: + throw new DgcgResponseException(HttpStatus.INTERNAL_SERVER_ERROR, + "0x011", "Upload of new Signer Certificate failed", sentValues, e.getMessage()); + default: + throw new DgcgResponseException(HttpStatus.BAD_REQUEST, X_004, DEFAULT_ERROR_MESSAGE, sentValues, + e.getMessage()); + } + } + + private void handleRevocationBatchException(CmsPackageDto cmsPackageDto, String countryCode, + RevocationListService.RevocationBatchServiceException e) { + updateMdc(e.getReason().toString(), e.getMessage()); + String sentValues = String.format(SENT_VALUES_FORMAT, cmsPackageDto, countryCode); + switch (e.getReason()) { + case NOT_FOUND: + throw new DgcgResponseException(HttpStatus.CONFLICT, "0x020", + "RevocationBatch to be updated does not exist.", + sentValues, e.getMessage()); + case INVALID_COUNTRY: + throw new DgcgResponseException(HttpStatus.BAD_REQUEST, + "0x021", "Invalid country", sentValues, e.getMessage()); + case INVALID_JSON_VALUES: + throw new DgcgResponseException(HttpStatus.BAD_REQUEST, + "0x022", "Json Payload invalid", sentValues, e.getMessage()); + default: + throw new DgcgResponseException(HttpStatus.BAD_REQUEST, X_004, DEFAULT_ERROR_MESSAGE, sentValues, + e.getMessage()); + } + } + + private void handleValidationRuleExcepetion(CmsPackageDto cmsPackageDto, String countryCode, + ValidationRuleService.ValidationRuleCheckException e) { + updateMdc(e.getReason().toString(), e.getMessage()); + String sentValues = String.format(SENT_VALUES_FORMAT, cmsPackageDto, countryCode); + switch (e.getReason()) { + case NOT_FOUND: + throw new DgcgResponseException(HttpStatus.CONFLICT, "0x030", + "ValidationRule to be updated does not exist.", + sentValues, e.getMessage()); + case INVALID_COUNTRY: + throw new DgcgResponseException(HttpStatus.BAD_REQUEST, + "0x031", "Invalid country", sentValues, e.getMessage()); + case INVALID_JSON: + throw new DgcgResponseException(HttpStatus.BAD_REQUEST, "0x032", "Json Payload invalid", sentValues, + e.getMessage()); + default: + throw new DgcgResponseException(HttpStatus.BAD_REQUEST, X_004, DEFAULT_ERROR_MESSAGE, sentValues, + e.getMessage()); + } + } +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/restapi/dto/CmsPackageDto.java b/src/main/java/eu/europa/ec/dgc/gateway/restapi/dto/CmsPackageDto.java new file mode 100644 index 00000000..2c93865c --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/restapi/dto/CmsPackageDto.java @@ -0,0 +1,26 @@ +package eu.europa.ec.dgc.gateway.restapi.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; + +@Schema(name = "CmsPackage") +@Data +@AllArgsConstructor +public class CmsPackageDto { + + @Schema(description = "CMS containing the signed String or certificate") + private String cms; + + @Schema(description = "Internal ID of the package") + private Long entityId; + + @Schema(description = "Type of the CMS package") + private CmsPackageTypeDto type; + + public enum CmsPackageTypeDto { + DSC, + REVOCATION_LIST, + VALIDATION_RULE + } +} 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 index c15593c3..17b8de7d 100644 --- a/src/main/java/eu/europa/ec/dgc/gateway/service/RevocationListService.java +++ b/src/main/java/eu/europa/ec/dgc/gateway/service/RevocationListService.java @@ -20,6 +20,8 @@ package eu.europa.ec.dgc.gateway.service; +import static eu.europa.ec.dgc.gateway.utils.CmsUtils.getSignedString; + import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; @@ -29,6 +31,8 @@ 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.CmsPackageDto; +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.utils.DgcMdc; @@ -239,12 +243,77 @@ public RevocationBatchDownload getRevocationBatch(String batchId) throws Revocat return new RevocationBatchDownload(entity.get().getBatchId(), entity.get().getSignedBatch()); } + /** + * Get CMS packages for country. + * + * @param country The country + * @return A list of all CMS Packages. + */ + public List getCmsPackage(String country) { + List revocationBatchEntities = revocationBatchRepository.getAllByCountry(country); + return revocationBatchEntities.stream() + .map(it -> new CmsPackageDto(it.getSignedBatch(), it.getId(), + CmsPackageDto.CmsPackageTypeDto.REVOCATION_LIST)) + .collect(Collectors.toList()); + } + + /** + * Update Revocation Batch with new cms in db. + * + * @param id the id of the entity. + * @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 updateRevocationBatchCertificate( + Long id, + String payloadRevocationBatch, + X509CertificateHolder signerCertificate, + String cms, + String authenticatedCountryCode + ) throws RevocationBatchServiceException { + + final RevocationBatchEntity revocationBatchEntity = revocationBatchRepository.findById(id).orElseThrow( + () -> new RevocationBatchServiceException(RevocationBatchServiceException.Reason.NOT_FOUND, + "RevocationBatch not present.")); + + contentCheckUploaderCertificate(signerCertificate, authenticatedCountryCode); + contentCheckUploaderCountry(revocationBatchEntity.getCountry(), authenticatedCountryCode); + contentCheckMigrateCms(payloadRevocationBatch, revocationBatchEntity.getSignedBatch()); + + revocationBatchEntity.setSignedBatch(cms); + revocationBatchEntity.setChanged(ZonedDateTime.now()); + + log.info("Updating cms of Revocation Batch Entity with id {}", revocationBatchEntity.getBatchId()); + + auditService.addAuditEvent( + authenticatedCountryCode, + signerCertificate, + authenticatedCountryCode, + "UPDATED", + String.format("Updated Revocation Batch (%s)", revocationBatchEntity.getBatchId()) + ); + + RevocationBatchEntity updatedEntity = revocationBatchRepository.save(revocationBatchEntity); + + DgcMdc.remove(MDC_PROP_UPLOAD_CERT_THUMBPRINT); + + return updatedEntity; + } + private void contentCheckUploaderCountry(RevocationBatchDto parsedBatch, String countryCode) throws RevocationBatchServiceException { - if (!parsedBatch.getCountry().equals(countryCode)) { + contentCheckUploaderCountry(parsedBatch.getCountry(), countryCode); + } + + private void contentCheckUploaderCountry(String batchCountryCode, String countryCode) + throws RevocationBatchServiceException { + if (!batchCountryCode.equals(countryCode)) { throw new RevocationBatchServiceException( - RevocationBatchServiceException.Reason.INVALID_COUNTRY, - "Country does not match your authentication."); + RevocationBatchServiceException.Reason.INVALID_COUNTRY, + "Country does not match your authentication."); } } @@ -320,6 +389,17 @@ private void contentCheckValidValuesForDeletion(RevocationBatchDeleteRequestDto } } + private void contentCheckMigrateCms(String payload, String entityCms) + throws RevocationBatchServiceException { + SignedStringDto signedStringDto = getSignedString(entityCms); + if (!payload.equals(signedStringDto.getPayloadString())) { + throw new RevocationBatchServiceException( + RevocationBatchServiceException.Reason.INVALID_JSON_VALUES, + "New cms payload does not match present payload." + ); + } + } + /** * Checks a given UploadCertificate if it exists in the database and is assigned to given CountryCode. * diff --git a/src/main/java/eu/europa/ec/dgc/gateway/service/SignerInformationService.java b/src/main/java/eu/europa/ec/dgc/gateway/service/SignerInformationService.java index f2427443..f033d5e6 100644 --- a/src/main/java/eu/europa/ec/dgc/gateway/service/SignerInformationService.java +++ b/src/main/java/eu/europa/ec/dgc/gateway/service/SignerInformationService.java @@ -23,6 +23,7 @@ import eu.europa.ec.dgc.gateway.entity.SignerInformationEntity; import eu.europa.ec.dgc.gateway.entity.TrustedPartyEntity; import eu.europa.ec.dgc.gateway.repository.SignerInformationRepository; +import eu.europa.ec.dgc.gateway.restapi.dto.CmsPackageDto; import eu.europa.ec.dgc.gateway.utils.DgcMdc; import eu.europa.ec.dgc.utils.CertificateUtils; import java.io.IOException; @@ -30,6 +31,7 @@ import java.util.Base64; import java.util.List; import java.util.Optional; +import java.util.stream.Collectors; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -137,6 +139,56 @@ public SignerInformationEntity addSignerCertificate( return newSignerInformation; } + + /** + * Update a CMS package. + * + * @param id The entity to update + * @param uploadedCertificate the certificate to add + * @param signerCertificate the certificate which was used to sign the message + * @param signature the detached signature of cms message + * @param authenticatedCountryCode the country code of the uploader country from cert authentication + * @throws SignerCertCheckException if validation check has failed. The exception contains + * a reason property with detailed information why the validation has failed. + */ + public SignerInformationEntity updateSignerCertificate( + Long id, + X509CertificateHolder uploadedCertificate, + X509CertificateHolder signerCertificate, + String signature, + String authenticatedCountryCode + ) throws SignerCertCheckException { + + final SignerInformationEntity signerInformationEntity = signerInformationRepository.findById(id).orElseThrow( + () -> new SignerCertCheckException(SignerCertCheckException.Reason.EXIST_CHECK_FAILED, + "Uploaded certificate does not exists")); + + contentCheckUploaderCertificate(signerCertificate, authenticatedCountryCode); + contentCheckCountryOfOrigin(uploadedCertificate, authenticatedCountryCode); + contentCheckCsca(uploadedCertificate, authenticatedCountryCode); + + byte[] certRawData; + try { + certRawData = uploadedCertificate.getEncoded(); + } catch (IOException e) { + throw new SignerCertCheckException(SignerCertCheckException.Reason.UPLOAD_FAILED, "Internal Server Error"); + } + + signerInformationEntity.setRawData(Base64.getEncoder().encodeToString(certRawData)); + signerInformationEntity.setThumbprint(certificateUtils.getCertThumbprint(uploadedCertificate)); + signerInformationEntity.setCertificateType(SignerInformationEntity.CertificateType.DSC); + signerInformationEntity.setSignature(signature); + + log.info("Updating SignerInformation Entity"); + + SignerInformationEntity updatedEntity = signerInformationRepository.save(signerInformationEntity); + + DgcMdc.remove(MDC_PROP_UPLOAD_CERT_THUMBPRINT); + DgcMdc.remove(MDC_PROP_CSCA_CERT_THUMBPRINT); + + return updatedEntity; + } + /** * Deletes a Trusted Signer Certificate from TrustStore DB. * @@ -164,6 +216,20 @@ public void deleteSignerCertificate( DgcMdc.remove(MDC_PROP_UPLOAD_CERT_THUMBPRINT); } + /** + * Get CMS packages for country. + * + * @param country The country + * @return A list of all CMS Packages. + */ + public List getCmsPackage(String country) { + return signerInformationRepository + .getByCertificateTypeAndCountry(SignerInformationEntity.CertificateType.DSC, country) + .stream() + .map(it -> new CmsPackageDto(it.getRawData(), it.getId(), CmsPackageDto.CmsPackageTypeDto.DSC)) + .collect(Collectors.toList()); + } + private void contentCheckUploaderCertificate( X509CertificateHolder signerCertificate, String authenticatedCountryCode) throws SignerCertCheckException { diff --git a/src/main/java/eu/europa/ec/dgc/gateway/service/ValidationRuleService.java b/src/main/java/eu/europa/ec/dgc/gateway/service/ValidationRuleService.java index e67ed669..39e7977d 100644 --- a/src/main/java/eu/europa/ec/dgc/gateway/service/ValidationRuleService.java +++ b/src/main/java/eu/europa/ec/dgc/gateway/service/ValidationRuleService.java @@ -20,6 +20,8 @@ package eu.europa.ec.dgc.gateway.service; +import static eu.europa.ec.dgc.gateway.utils.CmsUtils.getSignedString; + import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; @@ -29,6 +31,8 @@ import eu.europa.ec.dgc.gateway.entity.ValidationRuleEntity; import eu.europa.ec.dgc.gateway.model.ParsedValidationRule; import eu.europa.ec.dgc.gateway.repository.ValidationRuleRepository; +import eu.europa.ec.dgc.gateway.restapi.dto.CmsPackageDto; +import eu.europa.ec.dgc.gateway.restapi.dto.SignedStringDto; import eu.europa.ec.dgc.gateway.utils.DgcMdc; import eu.europa.ec.dgc.utils.CertificateUtils; import java.time.ZonedDateTime; @@ -195,6 +199,56 @@ public ValidationRuleEntity addValidationRule( return newValidationRule; } + /** + * Get CMS packages for country. + * + * @param country The country + * @return A list of all CMS Packages. + */ + public List getCmsPackage(String country) { + List validationRuleEntities = validationRuleRepository.getAllByCountry(country); + return validationRuleEntities.stream() + .map(it -> new CmsPackageDto(it.getCms(), it.getId(), CmsPackageDto.CmsPackageTypeDto.VALIDATION_RULE)) + .collect(Collectors.toList()); + } + + /** + * Update the cms of a Validation Rule DB. + * + * @param id the id of the entity. + * @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 ValidationRuleCheckException if validation check has failed. The exception contains + * a reason property with detailed information why the validation has failed. + */ + public ValidationRuleEntity updateValidationRuleCertificate( + Long id, + String payloadValidationRule, + X509CertificateHolder signerCertificate, + String cms, + String authenticatedCountryCode + ) throws ValidationRuleCheckException { + + ValidationRuleEntity validationRuleEntity = validationRuleRepository.findById(id).orElseThrow( + () -> new ValidationRuleCheckException(ValidationRuleCheckException.Reason.NOT_FOUND, + "Validation rule not found.")); + + contentCheckUploaderCertificate(signerCertificate, authenticatedCountryCode); + contentCheckUploaderCountry(validationRuleEntity.getCountry(), authenticatedCountryCode); + contentCheckMigrateCms(payloadValidationRule, validationRuleEntity.getCms()); + + validationRuleEntity.setCms(cms); + + log.info("Updating ValidationRule Entity"); + + validationRuleEntity = validationRuleRepository.save(validationRuleEntity); + + DgcMdc.remove(MDC_PROP_UPLOAD_CERT_THUMBPRINT); + + return validationRuleEntity; + } + private void contentCheckRuleIdPrefixMatchCertificateType(ParsedValidationRule parsedValidationRule) throws ValidationRuleCheckException { @@ -315,11 +369,8 @@ private void contentCheckTimestamps( private void contentCheckUploaderCountry(ParsedValidationRule parsedValidationRule, String countryCode) throws ValidationRuleCheckException { - if (!parsedValidationRule.getCountry().equals(countryCode)) { - throw new ValidationRuleCheckException( - ValidationRuleCheckException.Reason.INVALID_COUNTRY, - "Country does not match your authentication."); - } + + contentCheckUploaderCountry(parsedValidationRule.getCountry(), countryCode); if (!getCountryCodeFromIdString(parsedValidationRule.getIdentifier()).equals(countryCode)) { throw new ValidationRuleCheckException( @@ -328,6 +379,15 @@ private void contentCheckUploaderCountry(ParsedValidationRule parsedValidationRu } } + private void contentCheckUploaderCountry(String validationRuleCountryCode, String countryCode) + throws ValidationRuleCheckException { + if (!validationRuleCountryCode.equals(countryCode)) { + throw new ValidationRuleCheckException( + ValidationRuleCheckException.Reason.INVALID_COUNTRY, + "Country does not match your authentication."); + } + } + private ParsedValidationRule contentCheckValidJson(String json) throws ValidationRuleCheckException { Schema validationSchema = validationRuleSchemaProvider.getValidationRuleSchema(); @@ -355,6 +415,16 @@ private ParsedValidationRule contentCheckValidJson(String json) throws Validatio } } + void contentCheckMigrateCms(String payload, String entityCms) + throws ValidationRuleCheckException { + SignedStringDto signedStringDto = getSignedString(entityCms); + if (!payload.equals(signedStringDto.getPayloadString())) { + throw new ValidationRuleCheckException(ValidationRuleCheckException.Reason.INVALID_JSON, + "New cms payload does not match present payload." + ); + } + } + /** * Checks a given UploadCertificate if it exists in the database and is assigned to given CountryCode. * @@ -398,6 +468,7 @@ public enum Reason { INVALID_TIMESTAMP, INVALID_VERSION, INVALID_RULE_ID, + NOT_FOUND, UPLOADER_CERT_CHECK_FAILED, } } diff --git a/src/main/java/eu/europa/ec/dgc/gateway/utils/CmsUtils.java b/src/main/java/eu/europa/ec/dgc/gateway/utils/CmsUtils.java new file mode 100644 index 00000000..d6ed12c3 --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/utils/CmsUtils.java @@ -0,0 +1,41 @@ +package eu.europa.ec.dgc.gateway.utils; + +import eu.europa.ec.dgc.gateway.restapi.dto.SignedCertificateDto; +import eu.europa.ec.dgc.gateway.restapi.dto.SignedStringDto; +import eu.europa.ec.dgc.signing.SignedCertificateMessageParser; +import eu.europa.ec.dgc.signing.SignedStringMessageParser; + +public class CmsUtils { + + private CmsUtils() { + //Utility class + } + + /** + * Get a signed payload String with signerCertificate and signature. + */ + public static SignedStringDto getSignedString(final String cms) { + SignedStringMessageParser messageParser = new SignedStringMessageParser(cms); + return SignedStringDto.builder() + .payloadString(messageParser.getPayload()) + .signerCertificate(messageParser.getSigningCertificate()) + .rawMessage(cms) + .signature(messageParser.getSignature()) + .verified(messageParser.isSignatureVerified()) + .build(); + } + + /** + * Get a signed payload certificate with signerCertificate and signature. + */ + public static SignedCertificateDto getSignerCertificate(final String cms) { + SignedCertificateMessageParser certificateParser = new SignedCertificateMessageParser(cms); + return SignedCertificateDto.builder() + .payloadCertificate(certificateParser.getPayload()) + .signerCertificate(certificateParser.getSigningCertificate()) + .rawMessage(cms) + .signature(certificateParser.getSignature()) + .verified(certificateParser.isSignatureVerified()) + .build(); + } +} diff --git a/src/test/java/eu/europa/ec/dgc/gateway/restapi/controller/CertificateMigrationControllerTest.java b/src/test/java/eu/europa/ec/dgc/gateway/restapi/controller/CertificateMigrationControllerTest.java new file mode 100644 index 00000000..0a17f96d --- /dev/null +++ b/src/test/java/eu/europa/ec/dgc/gateway/restapi/controller/CertificateMigrationControllerTest.java @@ -0,0 +1,653 @@ +package eu.europa.ec.dgc.gateway.restapi.controller; + +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.connector.model.ValidationRule; +import eu.europa.ec.dgc.gateway.entity.RevocationBatchEntity; +import eu.europa.ec.dgc.gateway.entity.SignerInformationEntity; +import eu.europa.ec.dgc.gateway.entity.TrustedPartyEntity; +import eu.europa.ec.dgc.gateway.entity.ValidationRuleEntity; +import eu.europa.ec.dgc.gateway.repository.RevocationBatchRepository; +import eu.europa.ec.dgc.gateway.repository.SignerInformationRepository; +import eu.europa.ec.dgc.gateway.repository.ValidationRuleRepository; +import eu.europa.ec.dgc.gateway.restapi.dto.CmsPackageDto; +import eu.europa.ec.dgc.gateway.restapi.dto.revocation.RevocationBatchDto; +import eu.europa.ec.dgc.gateway.restapi.dto.revocation.RevocationHashTypeDto; +import eu.europa.ec.dgc.gateway.testdata.CertificateTestUtils; +import eu.europa.ec.dgc.gateway.testdata.TrustedPartyTestHelper; +import eu.europa.ec.dgc.signing.SignedCertificateMessageBuilder; +import eu.europa.ec.dgc.signing.SignedCertificateMessageParser; +import eu.europa.ec.dgc.signing.SignedStringMessageBuilder; +import eu.europa.ec.dgc.utils.CertificateUtils; +import org.bouncycastle.cert.X509CertificateHolder; +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.test.web.servlet.MockMvc; + +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatterBuilder; +import java.util.Base64; +import java.util.List; +import java.util.Optional; + +import static eu.europa.ec.dgc.gateway.testdata.CertificateTestUtils.getDummyValidationRule; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +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.*; + +@SpringBootTest +@AutoConfigureMockMvc +class CertificateMigrationControllerTest { + + + @Autowired + DgcConfigProperties dgcConfigProperties; + + @Autowired + TrustedPartyTestHelper trustedPartyTestHelper; + + @Autowired + RevocationBatchRepository revocationBatchRepository; + + @Autowired + SignerInformationRepository signerInformationRepository; + + @Autowired + ValidationRuleRepository validationRuleRepository; + + @Autowired + private MockMvc mockMvc; + + @Autowired + CertificateUtils certificateUtils; + + + ObjectMapper objectMapper; + + private static final String countryCode = "EU"; + private static final String authCertSubject = "C=" + countryCode; + + @BeforeEach + void setUp() { + signerInformationRepository.deleteAll(); + revocationBatchRepository.deleteAll(); + validationRuleRepository.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 testAllCertTypes() throws Exception { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("ec"); + X509Certificate certDscEu = CertificateTestUtils.generateCertificate(keyPairGenerator.generateKeyPair(), countryCode, "Test"); + String cmsBase64 = Base64.getEncoder().encodeToString(certDscEu.getEncoded()); + + createSignerInfo(cmsBase64, certDscEu); + createRevocation(cmsBase64); + createValidationEntry(cmsBase64); + + String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode); + + mockMvc.perform(get("/cms-migration") + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject)) + .andExpect(jsonPath("$", hasSize(3))) + .andExpect(jsonPath("$[0].type", is(CmsPackageDto.CmsPackageTypeDto.DSC.name()))) + .andExpect(jsonPath("$[1].type", is(CmsPackageDto.CmsPackageTypeDto.REVOCATION_LIST.name()))) + .andExpect(jsonPath("$[2].type", is(CmsPackageDto.CmsPackageTypeDto.VALIDATION_RULE.name()))); + } + + @Test + void testNoneForCountry() throws Exception { + String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode); + + mockMvc.perform(get("/cms-migration") + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject)) + .andExpect(jsonPath("$", hasSize(0))); + } + + @Test + void testUpdateDSC() throws Exception { + X509Certificate signerCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + PrivateKey signerPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + X509Certificate cscaCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.CSCA, countryCode); + PrivateKey cscaPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.CSCA, countryCode); + + KeyPair payloadKeyPair = KeyPairGenerator.getInstance("ec").generateKeyPair(); + X509Certificate payloadCertificate = CertificateTestUtils.generateCertificate(payloadKeyPair, countryCode, "Payload Cert", cscaCertificate, cscaPrivateKey); + + String existingPayload = new SignedCertificateMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey) + .withPayload(new X509CertificateHolder(payloadCertificate.getEncoded())) + .buildAsString(); + SignedCertificateMessageParser parser = new SignedCertificateMessageParser(existingPayload); + SignerInformationEntity existingEntity = createSignerInfoEntity(existingPayload, parser.getSignature(), certificateUtils.getCertThumbprint(payloadCertificate)); + + trustedPartyTestHelper.clear(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + trustedPartyTestHelper.clear(TrustedPartyEntity.CertificateType.CSCA, countryCode); + + X509Certificate signerCertificateUpdate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + PrivateKey signerPrivateKeyUpdate = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + X509Certificate cscaCertificateUpdate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.CSCA, countryCode); + PrivateKey cscaPrivateKeyUpdate = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.CSCA, countryCode); + + KeyPair payloadKeyPairUpdate = KeyPairGenerator.getInstance("ec").generateKeyPair(); + X509Certificate payloadCertificateUpdate = CertificateTestUtils.generateCertificate(payloadKeyPairUpdate, countryCode, "Payload Cert", cscaCertificateUpdate, cscaPrivateKeyUpdate); + + String updatePayload = new SignedCertificateMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificateUpdate), signerPrivateKeyUpdate) + .withPayload(new X509CertificateHolder(payloadCertificateUpdate.getEncoded())) + .buildAsString(); + String updatedSignature = new SignedCertificateMessageParser(updatePayload).getSignature(); + CmsPackageDto dto = new CmsPackageDto(updatePayload, existingEntity.getId(), CmsPackageDto.CmsPackageTypeDto.DSC); + + + String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode); + + mockMvc.perform(post("/cms-migration") + .contentType("application/json") + .content(objectMapper.writeValueAsString(dto)) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isNoContent()); + + Optional updatedCert = signerInformationRepository.findById(existingEntity.getId()); + + Assertions.assertTrue(updatedCert.isPresent()); + Assertions.assertEquals(Base64.getEncoder().encodeToString(payloadCertificateUpdate.getEncoded()), updatedCert.get().getRawData()); + Assertions.assertEquals(updatedSignature, updatedCert.get().getSignature()); + } + + @Test + void testUpdateDSCNotFound() throws Exception { + X509Certificate signerCertificateUpdate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + PrivateKey signerPrivateKeyUpdate = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + X509Certificate cscaCertificateUpdate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.CSCA, countryCode); + PrivateKey cscaPrivateKeyUpdate = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.CSCA, countryCode); + + KeyPair payloadKeyPairUpdate = KeyPairGenerator.getInstance("ec").generateKeyPair(); + X509Certificate payloadCertificateUpdate = CertificateTestUtils.generateCertificate(payloadKeyPairUpdate, countryCode, "Payload Cert", cscaCertificateUpdate, cscaPrivateKeyUpdate); + + String updatePayload = new SignedCertificateMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificateUpdate), signerPrivateKeyUpdate) + .withPayload(new X509CertificateHolder(payloadCertificateUpdate.getEncoded())) + .buildAsString(); + CmsPackageDto dto = new CmsPackageDto(updatePayload, 404L, CmsPackageDto.CmsPackageTypeDto.DSC); + + String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode); + + mockMvc.perform(post("/cms-migration") + .contentType("application/json") + .content(objectMapper.writeValueAsString(dto)) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isConflict()) + .andExpect(jsonPath("$.code", is("0x010"))); + } + + @Test + void testUpdateDSCCMSinvalid() throws Exception { + X509Certificate signerCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + PrivateKey signerPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + X509Certificate cscaCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.CSCA, countryCode); + PrivateKey cscaPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.CSCA, countryCode); + + KeyPair payloadKeyPair = KeyPairGenerator.getInstance("ec").generateKeyPair(); + X509Certificate payloadCertificate = CertificateTestUtils.generateCertificate(payloadKeyPair, countryCode, "Payload Cert", cscaCertificate, cscaPrivateKey); + + String existingPayload = new SignedCertificateMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey) + .withPayload(new X509CertificateHolder(payloadCertificate.getEncoded())) + .buildAsString(); + SignedCertificateMessageParser parser = new SignedCertificateMessageParser(existingPayload); + SignerInformationEntity existingEntity = createSignerInfoEntity(existingPayload, parser.getSignature(), certificateUtils.getCertThumbprint(payloadCertificate)); + + CmsPackageDto dto = new CmsPackageDto("invalidCMS", existingEntity.getId(), CmsPackageDto.CmsPackageTypeDto.DSC); + + String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode); + + mockMvc.perform(post("/cms-migration") + .contentType("application/json") + .content(objectMapper.writeValueAsString(dto)) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code", is("0x260"))); + } + + @Test + void testUpdateValidationRule() throws Exception { + X509Certificate signerCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + PrivateKey signerPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + trustedPartyTestHelper.clear(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + X509Certificate signerCertificate2 = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + PrivateKey signerPrivateKey2 = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode); + + ValidationRule validationRule = getDummyValidationRule(); + + String payload = new SignedStringMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey) + .withPayload(objectMapper.writeValueAsString(validationRule)) + .buildAsString(); + ValidationRuleEntity entity = createValidationEntry(payload); + String updatePayload = new SignedStringMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate2), signerPrivateKey2) + .withPayload(objectMapper.writeValueAsString(validationRule)) + .buildAsString(); + CmsPackageDto dto = new CmsPackageDto(updatePayload, entity.getId(), CmsPackageDto.CmsPackageTypeDto.VALIDATION_RULE); + + mockMvc.perform(post("/cms-migration") + .contentType("application/json") + .content(objectMapper.writeValueAsString(dto)) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isNoContent()); + + Optional updatedRule = + validationRuleRepository.findById(entity.getId()); + + Assertions.assertTrue(updatedRule.isPresent()); + Assertions.assertEquals(updatePayload, updatedRule.get().getCms()); + } + + @Test + void testUpdateValidationRulePayloadNotMatching() throws Exception { + X509Certificate signerCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + PrivateKey signerPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + trustedPartyTestHelper.clear(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + X509Certificate signerCertificate2 = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + PrivateKey signerPrivateKey2 = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode); + + ValidationRule validationRule = getDummyValidationRule(); + + String existingPayload = new SignedStringMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey) + .withPayload(objectMapper.writeValueAsString(validationRule)) + .buildAsString(); + ValidationRuleEntity entity = createValidationEntry(existingPayload); + + validationRule.setIdentifier("MISMATCH"); + String updatePayload = new SignedStringMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate2), signerPrivateKey2) + .withPayload(objectMapper.writeValueAsString(validationRule)) + .buildAsString(); + CmsPackageDto dto = new CmsPackageDto(updatePayload, entity.getId(), CmsPackageDto.CmsPackageTypeDto.VALIDATION_RULE); + + mockMvc.perform(post("/cms-migration") + .contentType("application/json") + .content(objectMapper.writeValueAsString(dto)) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code", is("0x032"))); + + Optional updatedRule = + validationRuleRepository.findById(entity.getId()); + + Assertions.assertTrue(updatedRule.isPresent()); + Assertions.assertEquals(existingPayload, updatedRule.get().getCms()); + } + + @Test + void testUpdateValidationRulePayloadNotFound() throws Exception { + X509Certificate signerCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + PrivateKey signerPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode); + + ValidationRule validationRule = getDummyValidationRule(); + + validationRule.setIdentifier("MISMATCH"); + String updatePayload = new SignedStringMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey) + .withPayload(objectMapper.writeValueAsString(validationRule)) + .buildAsString(); + CmsPackageDto dto = new CmsPackageDto(updatePayload, 404L, CmsPackageDto.CmsPackageTypeDto.VALIDATION_RULE); + + mockMvc.perform(post("/cms-migration") + .contentType("application/json") + .content(objectMapper.writeValueAsString(dto)) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isConflict()) + .andExpect(jsonPath("$.code", is("0x030"))); + } + + @Test + void testUpdateValidationRuleInvalidCMS() throws Exception { + X509Certificate signerCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + PrivateKey signerPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode); + + ValidationRule validationRule = getDummyValidationRule(); + + String existingPayload = new SignedStringMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey) + .withPayload(objectMapper.writeValueAsString(validationRule)) + .buildAsString(); + ValidationRuleEntity entity = createValidationEntry(existingPayload); + CmsPackageDto dto = new CmsPackageDto("invalidCms", entity.getId(), CmsPackageDto.CmsPackageTypeDto.VALIDATION_RULE); + + mockMvc.perform(post("/cms-migration") + .contentType("application/json") + .content(objectMapper.writeValueAsString(dto)) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code", is("0x260"))); + } + + @Test + void testUpdateValidationRuleWrongCountry() throws Exception { + X509Certificate signerCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, "DE"); + PrivateKey signerPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.UPLOAD, "DE"); + trustedPartyTestHelper.clear(TrustedPartyEntity.CertificateType.UPLOAD, "DE"); + X509Certificate signerCertificate2 = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + PrivateKey signerPrivateKey2 = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode); + + ValidationRule validationRule = getDummyValidationRule(); + + String existingPayload = new SignedStringMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey) + .withPayload(objectMapper.writeValueAsString(validationRule)) + .buildAsString(); + ValidationRuleEntity entity = createValidationEntry(existingPayload, "DE"); + + String updatePayload = new SignedStringMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate2), signerPrivateKey2) + .withPayload(objectMapper.writeValueAsString(validationRule)) + .buildAsString(); + CmsPackageDto dto = new CmsPackageDto(updatePayload, entity.getId(), CmsPackageDto.CmsPackageTypeDto.VALIDATION_RULE); + + mockMvc.perform(post("/cms-migration") + .contentType("application/json") + .content(objectMapper.writeValueAsString(dto)) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code", is("0x031"))); + + Optional updatedRule = + validationRuleRepository.findById(entity.getId()); + + Assertions.assertTrue(updatedRule.isPresent()); + Assertions.assertEquals(existingPayload, updatedRule.get().getCms()); + } + + @Test + void testUpdateRevocation() throws Exception { + X509Certificate signerCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + PrivateKey signerPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + trustedPartyTestHelper.clear(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + X509Certificate signerCertificate2 = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + PrivateKey signerPrivateKey2 = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + String authCertHash2 = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode); + + RevocationBatchDto revocationBatch = createRevocationBatch("kid1"); + + String existingPayload = new SignedStringMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey) + .withPayload(objectMapper.writeValueAsString(revocationBatch)) + .buildAsString(); + RevocationBatchEntity entity = createRevocationBatchEntity(existingPayload); + + String updatePayload = new SignedStringMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate2), signerPrivateKey2) + .withPayload(objectMapper.writeValueAsString(revocationBatch)) + .buildAsString(); + CmsPackageDto dto = new CmsPackageDto(updatePayload, entity.getId(), CmsPackageDto.CmsPackageTypeDto.REVOCATION_LIST); + + + mockMvc.perform(post("/cms-migration") + .contentType("application/json") + .content(objectMapper.writeValueAsString(dto)) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash2) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isNoContent()); + + Optional updatedBatch = + revocationBatchRepository.findById(entity.getId()); + + Assertions.assertTrue(updatedBatch.isPresent()); + Assertions.assertEquals(updatePayload, updatedBatch.get().getSignedBatch()); + } + + @Test + void testUpdateRevocationPayloadNotMatching() throws Exception { + X509Certificate signerCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + PrivateKey signerPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + trustedPartyTestHelper.clear(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + X509Certificate signerCertificate2 = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + PrivateKey signerPrivateKey2 = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + String authCertHash2 = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode); + + RevocationBatchDto revocationBatch = createRevocationBatch("kid1"); + + String existingPayload = new SignedStringMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey) + .withPayload(objectMapper.writeValueAsString(revocationBatch)) + .buildAsString(); + RevocationBatchEntity entity = createRevocationBatchEntity(existingPayload); + + RevocationBatchDto revocationBatchUnmatch = createRevocationBatch("kid2"); + + String updatePayload = new SignedStringMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate2), signerPrivateKey2) + .withPayload(objectMapper.writeValueAsString(revocationBatchUnmatch)) + .buildAsString(); + CmsPackageDto dto = new CmsPackageDto(updatePayload, entity.getId(), CmsPackageDto.CmsPackageTypeDto.REVOCATION_LIST); + + + mockMvc.perform(post("/cms-migration") + .contentType("application/json") + .content(objectMapper.writeValueAsString(dto)) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash2) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code", is("0x022"))); + + Optional updatedBatch = + revocationBatchRepository.findById(entity.getId()); + + Assertions.assertTrue(updatedBatch.isPresent()); + Assertions.assertEquals(existingPayload, updatedBatch.get().getSignedBatch()); + } + + @Test + void testUpdateRevocationPayloadNotFound() throws Exception { + X509Certificate signerCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + PrivateKey signerPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode); + + RevocationBatchDto revocationBatch = createRevocationBatch("kid1"); + + String updatePayload = new SignedStringMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey) + .withPayload(objectMapper.writeValueAsString(revocationBatch)) + .buildAsString(); + CmsPackageDto dto = new CmsPackageDto(updatePayload, 404L, CmsPackageDto.CmsPackageTypeDto.REVOCATION_LIST); + + + mockMvc.perform(post("/cms-migration") + .contentType("application/json") + .content(objectMapper.writeValueAsString(dto)) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isConflict()) + .andExpect(jsonPath("$.code", is("0x020"))); + } + + @Test + void testUpdateRevocationInvalidCMS() throws Exception { + X509Certificate signerCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + PrivateKey signerPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + String authCertHash = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode); + + RevocationBatchDto revocationBatch = createRevocationBatch("kid1"); + + String existingPayload = new SignedStringMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey) + .withPayload(objectMapper.writeValueAsString(revocationBatch)) + .buildAsString(); + RevocationBatchEntity entity = createRevocationBatchEntity(existingPayload); + + CmsPackageDto dto = new CmsPackageDto("invalidCms", entity.getId(), CmsPackageDto.CmsPackageTypeDto.REVOCATION_LIST); + + mockMvc.perform(post("/cms-migration") + .contentType("application/json") + .content(objectMapper.writeValueAsString(dto)) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code", is("0x260"))); + + Optional updatedBatch = + revocationBatchRepository.findById(entity.getId()); + + Assertions.assertTrue(updatedBatch.isPresent()); + Assertions.assertEquals(existingPayload, updatedBatch.get().getSignedBatch()); + } + + @Test + void testUpdateRevocationWrongCountry() throws Exception { + X509Certificate signerCertificate = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, "DE"); + PrivateKey signerPrivateKey = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.UPLOAD, "DE"); + trustedPartyTestHelper.clear(TrustedPartyEntity.CertificateType.UPLOAD, "DE"); + X509Certificate signerCertificate2 = trustedPartyTestHelper.getCert(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + PrivateKey signerPrivateKey2 = trustedPartyTestHelper.getPrivateKey(TrustedPartyEntity.CertificateType.UPLOAD, countryCode); + String authCertHash2 = trustedPartyTestHelper.getHash(TrustedPartyEntity.CertificateType.AUTHENTICATION, countryCode); + + RevocationBatchDto revocationBatch = createRevocationBatch("kid1"); + revocationBatch.setCountry("DE"); + + String existingPayload = new SignedStringMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate), signerPrivateKey) + .withPayload(objectMapper.writeValueAsString(revocationBatch)) + .buildAsString(); + RevocationBatchEntity entity = createRevocationBatchEntity(existingPayload, "DE"); + + String updatePayload = new SignedStringMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(signerCertificate2), signerPrivateKey2) + .withPayload(objectMapper.writeValueAsString(revocationBatch)) + .buildAsString(); + CmsPackageDto dto = new CmsPackageDto(updatePayload, entity.getId(), CmsPackageDto.CmsPackageTypeDto.REVOCATION_LIST); + + mockMvc.perform(post("/cms-migration") + .contentType("application/json") + .content(objectMapper.writeValueAsString(dto)) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getThumbprint(), authCertHash2) + .header(dgcConfigProperties.getCertAuth().getHeaderFields().getDistinguishedName(), authCertSubject) + ) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code", is("0x021"))); + } + + private void createSignerInfo(final String cmsBase64, final X509Certificate certDscEu) { + signerInformationRepository.save(new SignerInformationEntity( + null, ZonedDateTime.now(), countryCode, certificateUtils.getCertThumbprint(certDscEu), + cmsBase64, "sig1", SignerInformationEntity.CertificateType.DSC + )); + } + + private RevocationBatchEntity createRevocation(final String cmsBase64) { + RevocationBatchEntity revocationBatchEntity = new RevocationBatchEntity( + null, "batchId", countryCode, ZonedDateTime.now(), ZonedDateTime.now().plusDays(2), + false, RevocationBatchEntity.RevocationHashType.SIGNATURE, "UNKNOWN_KID", cmsBase64); + return revocationBatchRepository.save(revocationBatchEntity); + } + + private SignerInformationEntity createSignerInfoEntity(final String cms, final String signature, final String thumbprint) { + SignerInformationEntity signerInformationEntity = new SignerInformationEntity(); + signerInformationEntity.setCertificateType(SignerInformationEntity.CertificateType.DSC); + signerInformationEntity.setRawData(cms); + signerInformationEntity.setSignature(signature); + signerInformationEntity.setCountry(countryCode); + signerInformationEntity.setCreatedAt(ZonedDateTime.now()); + signerInformationEntity.setThumbprint(thumbprint); + return signerInformationRepository.save(signerInformationEntity); + } + + private ValidationRuleEntity createValidationEntry(final String cms) { + return createValidationEntry(cms, countryCode); + } + + private ValidationRuleEntity createValidationEntry(final String cms, final String countryCode) { + ValidationRuleEntity validationRuleEntity = new ValidationRuleEntity(); + validationRuleEntity.setRuleId("rule1"); + validationRuleEntity.setValidationRuleType(ValidationRuleEntity.ValidationRuleType.ACCEPTANCE); + validationRuleEntity.setValidFrom(ZonedDateTime.now()); + validationRuleEntity.setValidTo(ZonedDateTime.now().plusDays(5)); + validationRuleEntity.setCountry(countryCode); + validationRuleEntity.setCms(cms); + validationRuleEntity.setVersion("1"); + validationRuleEntity.setCreatedAt(ZonedDateTime.now()); + return validationRuleRepository.save(validationRuleEntity); + } + + private RevocationBatchEntity createRevocationBatchEntity(final String cms) { + return createRevocationBatchEntity(cms, countryCode); + } + + private RevocationBatchEntity createRevocationBatchEntity(final String cms, final String countryCode) { + RevocationBatchEntity revocationBatchEntity = new RevocationBatchEntity(); + revocationBatchEntity.setBatchId("batch1"); + revocationBatchEntity.setCountry(countryCode); + revocationBatchEntity.setKid("KID1"); + revocationBatchEntity.setChanged(ZonedDateTime.now()); + revocationBatchEntity.setExpires(ZonedDateTime.now().plusDays(5)); + revocationBatchEntity.setType(RevocationBatchEntity.RevocationHashType.SIGNATURE); + revocationBatchEntity.setSignedBatch(cms); + return revocationBatchRepository.save(revocationBatchEntity); + } + + private RevocationBatchDto createRevocationBatch(String kid) { + RevocationBatchDto revocationBatchDto = new RevocationBatchDto(); + revocationBatchDto.setCountry(countryCode); + revocationBatchDto.setExpires(ZonedDateTime.now().plusDays(7)); + revocationBatchDto.setHashType(RevocationHashTypeDto.SIGNATURE); + revocationBatchDto.setKid(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") + )); + return revocationBatchDto; + } +} \ No newline at end of file diff --git a/src/test/java/eu/europa/ec/dgc/gateway/restapi/controller/SignerCertificateIntegrationTest.java b/src/test/java/eu/europa/ec/dgc/gateway/restapi/controller/SignerCertificateIntegrationTest.java index 95588f38..219dc308 100644 --- a/src/test/java/eu/europa/ec/dgc/gateway/restapi/controller/SignerCertificateIntegrationTest.java +++ b/src/test/java/eu/europa/ec/dgc/gateway/restapi/controller/SignerCertificateIntegrationTest.java @@ -20,7 +20,9 @@ package eu.europa.ec.dgc.gateway.restapi.controller; +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.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import eu.europa.ec.dgc.gateway.config.DgcConfigProperties; diff --git a/src/test/java/eu/europa/ec/dgc/gateway/testdata/TrustedPartyTestHelper.java b/src/test/java/eu/europa/ec/dgc/gateway/testdata/TrustedPartyTestHelper.java index 7ef82312..d91a8120 100644 --- a/src/test/java/eu/europa/ec/dgc/gateway/testdata/TrustedPartyTestHelper.java +++ b/src/test/java/eu/europa/ec/dgc/gateway/testdata/TrustedPartyTestHelper.java @@ -90,6 +90,12 @@ public void setRoles(String countryCode, TrustedPartyEntity.CertificateRoles... trustedPartyRepository.save(entity); } + public void clear(TrustedPartyEntity.CertificateType type, String countryCode) { + certificateMap.get(type).remove(countryCode); + hashMap.get(type).remove(countryCode); + privateKeyMap.get(type).remove(countryCode); + } + private void prepareTestCert(TrustedPartyEntity.CertificateType type, String countryCode) throws Exception { // Check if a test certificate already exists if (!hashMap.get(type).containsKey(countryCode)) {