Skip to content

Commit

Permalink
Merge branch 'main' into Feature-domain-PH4H-696
Browse files Browse the repository at this point in the history
  • Loading branch information
roopesh-palankar authored Aug 22, 2024
2 parents b68ae6a + 4045fa7 commit 24378c3
Show file tree
Hide file tree
Showing 5 changed files with 145 additions and 65 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
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.filter.CertificateAuthenticationRole;
Expand All @@ -45,14 +44,11 @@
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Pattern;
import java.time.ZonedDateTime;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
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;
Expand All @@ -61,7 +57,6 @@
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;

Expand All @@ -70,7 +65,9 @@
@RequiredArgsConstructor
@Validated
@Slf4j
@ConditionalOnProperty(name = "dgc.revocation.enabled", havingValue = "true")
@ConditionalOnExpression(
"'${dgc.revocation.enabled}' == 'true' and '${dgc.revocation.hidden.endpoints}' == 'true'"
)
public class CertificateRevocationListController {

private final RevocationListService revocationListService;
Expand All @@ -84,56 +81,6 @@ public class CertificateRevocationListController {
private static final String MDC_DOWNLOADED_COUNTRY = "downloadedCountry";
private static final String MDC_DOWNLOADED_BATCH_ID = "downloadedBatchId";

/**
* Endpoint to download Revocation Batch List.
*/
@CertificateAuthenticationRequired(requiredRoles = CertificateAuthenticationRole.RevocationListReader)
@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<RevocationBatchListDto> 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.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*-
* ---license-start
* WHO Digital Documentation Covid Certificate Gateway Service / ddcc-gateway
* ---
* Copyright (C) 2022 T-Systems International GmbH and all other contributors
* ---
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* ---license-end
*/

package eu.europa.ec.dgc.gateway.restapi.controller;

import eu.europa.ec.dgc.gateway.config.OpenApiConfig;
import eu.europa.ec.dgc.gateway.exception.DgcgResponseException;
import eu.europa.ec.dgc.gateway.restapi.dto.revocation.RevocationBatchListDto;
import eu.europa.ec.dgc.gateway.restapi.filter.CertificateAuthenticationRequired;
import eu.europa.ec.dgc.gateway.restapi.filter.CertificateAuthenticationRole;
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.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 jakarta.validation.Valid;
import java.time.ZonedDateTime;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
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.GetMapping;
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
@ConditionalOnProperty(name = "dgc.revocation.enabled", havingValue = "true")
public class CertificateRevocationListDefaultController {

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}$";

private static final String MDC_DOWNLOADER_COUNTRY = "downloaderCountry";
private static final String MDC_DOWNLOADED_COUNTRY = "downloadedCountry";
private static final String MDC_DOWNLOADED_BATCH_ID = "downloadedBatchId";

/**
* Endpoint to download Revocation Batch List.
*/
@CertificateAuthenticationRequired(requiredRoles = CertificateAuthenticationRole.RevocationListReader)
@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<RevocationBatchListDto> 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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import com.apicatalog.jsonld.document.JsonDocument;
import com.danubetech.keyformats.crypto.ByteSigner;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nimbusds.jose.util.Base64URL;
import eu.europa.ec.dgc.gateway.config.DgcConfigProperties;
import eu.europa.ec.dgc.gateway.entity.SignerInformationEntity;
import eu.europa.ec.dgc.gateway.entity.TrustedIssuerEntity;
Expand Down Expand Up @@ -262,7 +263,7 @@ private void addTrustListEntry(DidTrustListDto trustList,
+ SEPARATOR_COLON
+ getCountryAsLowerCaseAlpha3(cert.getCountry())
+ SEPARATOR_FRAGMENT
+ URLEncoder.encode(cert.getKid(), StandardCharsets.UTF_8));
+ getEncodedKid(cert.getKid()));;
trustListEntry.setController(configProperties.getDid().getTrustListControllerPrefix()
+ SEPARATOR_COLON + getCountryAsLowerCaseAlpha3(cert.getCountry()));
trustListEntry.setPublicKeyJwk(publicKeyJwk);
Expand All @@ -282,4 +283,8 @@ private Optional<TrustedCertificateTrustList> searchForIssuer(TrustedCertificate
.equals(cert.getParsedCertificate().getIssuerX500Principal()))
.findFirst();
}

private String getEncodedKid(String kid) {
return Base64URL.encode(kid).toString();
}
}
4 changes: 3 additions & 1 deletion src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,9 @@ dgc:
pem: X-SSL-Client-Cert
revocation:
delete-threshold: 14
enabled: false
enabled: true
hidden:
endpoints: false
signer-information:
delete-threshold: 14
trustedCertificates:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import static org.mockito.Mockito.doNothing;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.nimbusds.jose.util.Base64URL;
import eu.europa.ec.dgc.gateway.entity.FederationGatewayEntity;
import eu.europa.ec.dgc.gateway.entity.SignerInformationEntity;
import eu.europa.ec.dgc.gateway.entity.TrustedPartyEntity;
Expand Down Expand Up @@ -220,12 +221,12 @@ void testTrustList(boolean isEcAlgorithm) throws Exception {
Assertions.assertEquals("b", parsed.getController());
Assertions.assertEquals(6, parsed.getVerificationMethod().size());

assertVerificationMethod(getVerificationMethodByKid(parsed.getVerificationMethod(), "c" + ":deu" + "#" + URLEncoder.encode(certDscDeKid, StandardCharsets.UTF_8)),
assertVerificationMethod(getVerificationMethodByKid(parsed.getVerificationMethod(), "c" + ":deu" + "#" + getEncodedKid(certDscDeKid)),
certDscDeKid, certDscDe, certCscaDe, "deu");
assertVerificationMethod(getVerificationMethodByKid(parsed.getVerificationMethod(), "c:xeu#kid2"),
"kid2", certDscEu, certCscaEu, "xeu");
assertVerificationMethod(getVerificationMethodByKid(parsed.getVerificationMethod(), "c:xex#kid3"),
"kid3", federatedCertDscEx, null, "xex");
assertVerificationMethod(getVerificationMethodByKid(parsed.getVerificationMethod(), "c" + ":xeu" + "#" + getEncodedKid("kid2")),
"kid2", certDscEu, certCscaEu, "xeu");
assertVerificationMethod(getVerificationMethodByKid(parsed.getVerificationMethod(), "c" + ":xex" + "#" + getEncodedKid("kid3")),
"kid3", federatedCertDscEx, null, "xex");

Assertions.assertTrue(parsed.getVerificationMethod().contains("did:trusted:DE:issuer"));
Assertions.assertTrue(parsed.getVerificationMethod().contains("did:trusted:EU:issuer"));
Expand Down Expand Up @@ -261,7 +262,7 @@ private void assertVerificationMethod(Object in, String kid, X509Certificate dsc
LinkedHashMap jsonNode = (LinkedHashMap) in;
Assertions.assertEquals("JsonWebKey2020", jsonNode.get("type"));
Assertions.assertEquals("d" + ":" + country, jsonNode.get("controller"));
Assertions.assertEquals("c" + ":" + country + "#" + URLEncoder.encode(kid, StandardCharsets.UTF_8), jsonNode.get("id"));
Assertions.assertEquals("c" + ":" + country + "#" + getEncodedKid(kid), jsonNode.get("id"));

LinkedHashMap publicKeyJwk = (LinkedHashMap) jsonNode.get("publicKeyJwk");

Expand Down Expand Up @@ -313,4 +314,8 @@ private static class LDProof {

}
}

private String getEncodedKid(String kid) {
return Base64URL.encode(kid).toString();
}
}

0 comments on commit 24378c3

Please sign in to comment.