From d7a63f7826c1bb189afda87ec905e51f3f5fd30c Mon Sep 17 00:00:00 2001
From: Artur T <81235055+a-trzewik@users.noreply.github.com>
Date: Fri, 7 May 2021 10:18:30 +0200
Subject: [PATCH] updates claim, dgci gen, dgci status (#44)
* feat/dgcistatus
* dgci reduced length
* multiple claim on post endpoint
* update dgc-lib version t 0.4.0
---
pom.xml | 2 +-
.../ec/dgc/issuance/entity/DgciEntity.java | 3 +
.../issuance/repository/DgciRepository.java | 1 +
.../restapi/controller/DgciController.java | 43 +++++++++++---
.../restapi/controller/WalletController.java | 16 +----
.../issuance/restapi/dto/ClaimResponse.java | 2 +-
.../dgc/issuance/service/DgciGenerator.java | 28 +++++----
.../ec/dgc/issuance/service/DgciService.java | 59 +++++++++++++++----
.../resources/db/changelog/init-tables.xml | 7 +++
.../europa/ec/dgc/issuance/Sh256HashTest.java | 17 ++++++
.../dgc/issuance/service/DgciServiceTest.java | 26 +++++++-
11 files changed, 158 insertions(+), 46 deletions(-)
diff --git a/pom.xml b/pom.xml
index 0a5c3cd..5294701 100644
--- a/pom.xml
+++ b/pom.xml
@@ -24,7 +24,7 @@
UTF-8
UTF-8
- 0.3.1
+ 0.4.0
6.1.1
2.4.4
2020.0.2
diff --git a/src/main/java/eu/europa/ec/dgc/issuance/entity/DgciEntity.java b/src/main/java/eu/europa/ec/dgc/issuance/entity/DgciEntity.java
index 028c0d5..3bbdb9a 100644
--- a/src/main/java/eu/europa/ec/dgc/issuance/entity/DgciEntity.java
+++ b/src/main/java/eu/europa/ec/dgc/issuance/entity/DgciEntity.java
@@ -44,6 +44,9 @@ public class DgciEntity {
@Column(name = "dgci", nullable = false, unique = true)
private String dgci;
+ @Column(name = "dgci_hash", nullable = false, unique = true, length = 512)
+ private String dgciHash;
+
@Column(name = "created_at", nullable = false)
private ZonedDateTime createdAt = ZonedDateTime.now();
diff --git a/src/main/java/eu/europa/ec/dgc/issuance/repository/DgciRepository.java b/src/main/java/eu/europa/ec/dgc/issuance/repository/DgciRepository.java
index e0183c8..d6ae2e9 100644
--- a/src/main/java/eu/europa/ec/dgc/issuance/repository/DgciRepository.java
+++ b/src/main/java/eu/europa/ec/dgc/issuance/repository/DgciRepository.java
@@ -28,4 +28,5 @@ public interface DgciRepository extends JpaRepository {
Optional findByDgci(String dgci);
+ Optional findByDgciHash(String dgciHash);
}
diff --git a/src/main/java/eu/europa/ec/dgc/issuance/restapi/controller/DgciController.java b/src/main/java/eu/europa/ec/dgc/issuance/restapi/controller/DgciController.java
index ce8a8fd..0119248 100644
--- a/src/main/java/eu/europa/ec/dgc/issuance/restapi/controller/DgciController.java
+++ b/src/main/java/eu/europa/ec/dgc/issuance/restapi/controller/DgciController.java
@@ -33,6 +33,7 @@
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import javax.validation.Valid;
import lombok.AllArgsConstructor;
+import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
@@ -41,6 +42,7 @@
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@RestController
@@ -116,20 +118,43 @@ public ResponseEntity createEdgc(@Valid @RequestBody Eudgc eudgc)
}
@Operation(
- summary = "Returns a DID document"
+ summary = "Returns a DID document",
+ description = "Return a DID document"
)
- @GetMapping(value = "/{hash}")
- public ResponseEntity getDidDocument(@PathVariable String opaque, String hash) {
- return ResponseEntity.ok(dgciService.getDidDocument(hash));
+ @GetMapping(value = "/{dgciHash}")
+ public ResponseEntity getDidDocument(@PathVariable String dgciHash) {
+ return ResponseEntity.ok(dgciService.getDidDocument(dgciHash));
}
+ /**
+ * dgci status.
+ * @param dgciHash hash
+ * @return response
+ */
@Operation(
summary = "Checks the status of DGCI",
- description = "Produce status message"
+ description = "Produce status HTTP code message"
)
- @GetMapping(value = "/status")
- public ResponseEntity status() {
- // not was not really specified what it is good for
- return ResponseEntity.ok("fine");
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "204", description = "dgci exists"),
+ @ApiResponse(responseCode = "424", description = "dgci locked"),
+ @ApiResponse(responseCode = "404", description = "dgci not found")})
+ @RequestMapping(value = "/{dgciHash}",method = RequestMethod.HEAD)
+ public ResponseEntity dgciStatus(@PathVariable String dgciHash) {
+ HttpStatus httpStatus;
+ switch (dgciService.checkDgciStatus(dgciHash)) {
+ case EXISTS:
+ httpStatus = HttpStatus.NO_CONTENT;
+ break;
+ case LOCKED:
+ httpStatus = HttpStatus.LOCKED;
+ break;
+ case NOT_EXISTS:
+ httpStatus = HttpStatus.NOT_FOUND;
+ break;
+ default:
+ throw new IllegalArgumentException("unknown dgci status");
+ }
+ return ResponseEntity.status(httpStatus).build();
}
}
diff --git a/src/main/java/eu/europa/ec/dgc/issuance/restapi/controller/WalletController.java b/src/main/java/eu/europa/ec/dgc/issuance/restapi/controller/WalletController.java
index 2eaa6a6..6197ab3 100644
--- a/src/main/java/eu/europa/ec/dgc/issuance/restapi/controller/WalletController.java
+++ b/src/main/java/eu/europa/ec/dgc/issuance/restapi/controller/WalletController.java
@@ -36,7 +36,6 @@
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
-import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -57,18 +56,7 @@ public class WalletController {
@ApiResponse(responseCode = "404", description = "dgci not found"),
@ApiResponse(responseCode = "400", description = "wrong claim data")})
@PostMapping(value = "/claim", consumes = MediaType.APPLICATION_JSON_VALUE)
- public ResponseEntity claim(@Valid @RequestBody ClaimRequest claimRequest)
- throws IOException, NoSuchAlgorithmException, SignatureException, InvalidKeySpecException, InvalidKeyException {
- dgciService.claim(claimRequest);
- return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
+ public ResponseEntity claim(@Valid @RequestBody ClaimRequest claimRequest) {
+ return ResponseEntity.ok(dgciService.claim(claimRequest));
}
-
- @Operation(
- summary = "Claims the DGCI for a new TAN and certificate Holder"
- )
- @PatchMapping(value = "/claim", consumes = MediaType.APPLICATION_JSON_VALUE)
- public ResponseEntity claimUpdate(@Valid @RequestBody ClaimRequest claimRequest) {
- return ResponseEntity.ok(dgciService.claimUpdate(claimRequest));
- }
-
}
diff --git a/src/main/java/eu/europa/ec/dgc/issuance/restapi/dto/ClaimResponse.java b/src/main/java/eu/europa/ec/dgc/issuance/restapi/dto/ClaimResponse.java
index 652903d..c7fcea3 100644
--- a/src/main/java/eu/europa/ec/dgc/issuance/restapi/dto/ClaimResponse.java
+++ b/src/main/java/eu/europa/ec/dgc/issuance/restapi/dto/ClaimResponse.java
@@ -24,5 +24,5 @@
@Data
public class ClaimResponse {
- // TODO Claim Response properties
+ String tan;
}
diff --git a/src/main/java/eu/europa/ec/dgc/issuance/service/DgciGenerator.java b/src/main/java/eu/europa/ec/dgc/issuance/service/DgciGenerator.java
index 32ead3c..77d4806 100644
--- a/src/main/java/eu/europa/ec/dgc/issuance/service/DgciGenerator.java
+++ b/src/main/java/eu/europa/ec/dgc/issuance/service/DgciGenerator.java
@@ -21,12 +21,11 @@
package eu.europa.ec.dgc.issuance.service;
import eu.europa.ec.dgc.issuance.config.IssuanceConfigProperties;
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
-import org.bouncycastle.util.encoders.Hex;
import org.springframework.stereotype.Component;
@Component
@@ -41,19 +40,28 @@ public class DgciGenerator {
*/
public String newDgci() {
StringBuilder sb = new StringBuilder();
- sb.append(issuanceConfigProperties.getDgciPrefix()).append(':').append(UUID.randomUUID());
+ sb.append(issuanceConfigProperties.getDgciPrefix()).append(':');
+ // use uuid but encode to 0-9A-Z charset
+ UUID uuid = UUID.randomUUID();
+ ByteBuffer bb = ByteBuffer.wrap(new byte[16]);
+ bb.putLong(uuid.getMostSignificantBits());
+ bb.putLong(uuid.getLeastSignificantBits());
+ BigInteger bint = new BigInteger(1, bb.array());
+ int radix = 10 + ('Z' - 'A');
+ String randomUuidEncoded = bint.toString(radix).toUpperCase();
+ sb.append(randomUuidEncoded);
String checkSum = createDgciCheckSum(sb.toString());
sb.append(':').append(checkSum);
return sb.toString();
}
private String createDgciCheckSum(String dgciRaw) {
- try {
- final MessageDigest digest = MessageDigest.getInstance("SHA-256");
- final byte[] hashBytes = digest.digest(dgciRaw.getBytes(StandardCharsets.UTF_8));
- return Hex.toHexString(hashBytes, 0, 8);
- } catch (NoSuchAlgorithmException e) {
- throw new IllegalArgumentException(e);
+ BigInteger dgciRawAsNumber = new BigInteger(1, dgciRaw.getBytes(StandardCharsets.UTF_8));
+ BigInteger modValue = dgciRawAsNumber.mod(BigInteger.valueOf(97));
+ String checkSum = modValue.toString();
+ if (checkSum.length() == 1) {
+ checkSum = '0' + checkSum;
}
+ return checkSum;
}
}
diff --git a/src/main/java/eu/europa/ec/dgc/issuance/service/DgciService.java b/src/main/java/eu/europa/ec/dgc/issuance/service/DgciService.java
index 15d914c..14ff142 100644
--- a/src/main/java/eu/europa/ec/dgc/issuance/service/DgciService.java
+++ b/src/main/java/eu/europa/ec/dgc/issuance/service/DgciService.java
@@ -71,8 +71,10 @@
@Component
@RequiredArgsConstructor
public class DgciService {
+
+ public enum DgciStatus { EXISTS, NOT_EXISTS, LOCKED }
+
private final DgciRepository dgciRepository;
- private final EhdCryptoService ehdCryptoService;
private final TanService tanService;
private final CertificateService certificateService;
private final IssuanceConfigProperties issuanceConfigProperties;
@@ -100,6 +102,7 @@ public DgciIdentifier initDgci(DgciInit dgciInit) {
String dgci = generateDgci();
dgciEntity.setDgci(dgci);
+ dgciEntity.setDgciHash(dgciHash(dgci));
dgciEntity.setGreenCertificateType(dgciInit.getGreenCertificateType());
dgciRepository.saveAndFlush(dgciEntity);
@@ -119,6 +122,16 @@ public DgciIdentifier initDgci(DgciInit dgciInit) {
);
}
+ private String dgciHash(String dgci) {
+ try {
+ final MessageDigest digest = MessageDigest.getInstance("SHA-256");
+ final byte[] hashBytes = digest.digest(dgci.getBytes(StandardCharsets.UTF_8));
+ return Base64.getEncoder().encodeToString(hashBytes);
+ } catch (NoSuchAlgorithmException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
private long expirationForType(GreenCertificateType greenCertificateType) {
// TODO compute expiration dependend on certificate type and probably config
return EXPIRATION_PERIOD_SEC;
@@ -215,18 +228,16 @@ public ClaimResponse claimUpdate(ClaimRequest claimRequest) {
/**
* claim dgci to wallet app.
* means bind dgci with some public key from wallet app
+ *
* @param claimRequest claim request
*/
- public void claim(ClaimRequest claimRequest) {
+ public ClaimResponse claim(ClaimRequest claimRequest) {
if (!verifySignature(claimRequest)) {
throw new WrongRequest("signature verification failed");
}
Optional dgciEntityOptional = dgciRepository.findByDgci(claimRequest.getDgci());
if (dgciEntityOptional.isPresent()) {
DgciEntity dgciEntity = dgciEntityOptional.get();
- if (dgciEntity.isClaimed()) {
- throw new WrongRequest("already claimed");
- }
if (dgciEntity.getRetryCounter() > MAX_CLAIM_RETRY_TAN) {
throw new WrongRequest("claim max try exceeded");
}
@@ -238,17 +249,24 @@ public void claim(ClaimRequest claimRequest) {
dgciRepository.saveAndFlush(dgciEntity);
throw new WrongRequest("tan mismatch");
}
- ZonedDateTime tanExpireTime = dgciEntity.getCreatedAt()
- .plus(Duration.ofHours(issuanceConfigProperties.getTanExpirationHours()));
- if (tanExpireTime.isBefore(ZonedDateTime.now())) {
- throw new WrongRequest("tan expired");
+ if (!dgciEntity.isClaimed()) {
+ ZonedDateTime tanExpireTime = dgciEntity.getCreatedAt()
+ .plus(Duration.ofHours(issuanceConfigProperties.getTanExpirationHours()));
+ if (tanExpireTime.isBefore(ZonedDateTime.now())) {
+ throw new WrongRequest("tan expired");
+ }
}
dgciEntity.setClaimed(true);
dgciEntity.setRetryCounter(dgciEntity.getRetryCounter() + 1);
dgciEntity.setPublicKey(claimRequest.getPublicKey().getValue());
- dgciEntity.setHashedTan(null);
+ String newTan = tanService.generateNewTan();
+ dgciEntity.setHashedTan(tanService.hashTan(newTan));
+ dgciEntity.setRetryCounter(0);
log.info("dgci {} claimed", dgciEntity.getDgci());
dgciRepository.saveAndFlush(dgciEntity);
+ ClaimResponse claimResponse = new ClaimResponse();
+ claimResponse.setTan(newTan);
+ return claimResponse;
} else {
log.info("can not find dgci {}", claimRequest.getDgci());
throw new DgciNotFound("can not find dgci: " + claimRequest.getDgci());
@@ -336,4 +354,25 @@ public EgdcCodeData createEdgc(Eudgc eudgc) {
return egdcCodeData;
}
+
+ /**
+ * Check if dgci exists.
+ *
+ * @param dgciHash dgci hash
+ * @return DgciStatus
+ */
+ public DgciStatus checkDgciStatus(String dgciHash) {
+ DgciStatus dgciStatus;
+ Optional dgciEntity = dgciRepository.findByDgciHash(dgciHash);
+ if (dgciEntity.isPresent()) {
+ if (dgciEntity.get().isLocked()) {
+ dgciStatus = DgciStatus.LOCKED;
+ } else {
+ dgciStatus = DgciStatus.EXISTS;
+ }
+ } else {
+ dgciStatus = DgciStatus.NOT_EXISTS;
+ }
+ return dgciStatus;
+ }
}
diff --git a/src/main/resources/db/changelog/init-tables.xml b/src/main/resources/db/changelog/init-tables.xml
index 6833da0..29a96f2 100644
--- a/src/main/resources/db/changelog/init-tables.xml
+++ b/src/main/resources/db/changelog/init-tables.xml
@@ -30,4 +30,11 @@
+
+
+
+
+
+
+
diff --git a/src/test/java/eu/europa/ec/dgc/issuance/Sh256HashTest.java b/src/test/java/eu/europa/ec/dgc/issuance/Sh256HashTest.java
index b84f856..fd0bb86 100644
--- a/src/test/java/eu/europa/ec/dgc/issuance/Sh256HashTest.java
+++ b/src/test/java/eu/europa/ec/dgc/issuance/Sh256HashTest.java
@@ -20,9 +20,12 @@
package eu.europa.ec.dgc.issuance;
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Base64;
+import java.util.UUID;
import org.junit.Test;
public class Sh256HashTest {
@@ -33,4 +36,18 @@ public void testCreateSHA256Hash() throws Exception {
"some_data".getBytes(StandardCharsets.UTF_8));
System.out.println(Base64.getEncoder().encodeToString(hashbytes));
}
+
+ @Test
+ public void dgciEncoding() throws Exception {
+ UUID uuid = UUID.randomUUID();
+ System.out.println(uuid.toString());
+ ByteBuffer bb = ByteBuffer.wrap(new byte[16]);
+ bb.putLong(uuid.getMostSignificantBits());
+ bb.putLong(uuid.getLeastSignificantBits());
+ BigInteger bint = new BigInteger(1, bb.array());
+ int radix = 10+('Z'-'A');
+ String dgciRep = bint.toString(radix).toUpperCase();
+ System.out.println(dgciRep);
+ System.out.println(dgciRep.length());
+ }
}
diff --git a/src/test/java/eu/europa/ec/dgc/issuance/service/DgciServiceTest.java b/src/test/java/eu/europa/ec/dgc/issuance/service/DgciServiceTest.java
index 809f5fc..3dcd8e5 100644
--- a/src/test/java/eu/europa/ec/dgc/issuance/service/DgciServiceTest.java
+++ b/src/test/java/eu/europa/ec/dgc/issuance/service/DgciServiceTest.java
@@ -10,6 +10,7 @@
import eu.europa.ec.dgc.issuance.entity.GreenCertificateType;
import eu.europa.ec.dgc.issuance.repository.DgciRepository;
import eu.europa.ec.dgc.issuance.restapi.dto.ClaimRequest;
+import eu.europa.ec.dgc.issuance.restapi.dto.ClaimResponse;
import eu.europa.ec.dgc.issuance.restapi.dto.DgciIdentifier;
import eu.europa.ec.dgc.issuance.restapi.dto.DgciInit;
import eu.europa.ec.dgc.issuance.restapi.dto.EgcDecodeResult;
@@ -128,11 +129,19 @@ void testWalletClaim() throws Exception {
ClaimRequest claimRequest = generateClaimRequest(Hex.decode(decodeResult.getCoseHex()),
egdcCodeData.getDgci(),tanHash, certHash,
"RSA","SHA256WithRSA");
- dgciService.claim(claimRequest);
+ ClaimResponse claimResponse = dgciService.claim(claimRequest);
dgciEnitiyOpt = dgciRepository.findByDgci(egdcCodeData.getDgci());
assertTrue(dgciEnitiyOpt.isPresent());
assertTrue(dgciEnitiyOpt.get().isClaimed());
+
+ // new claim
+ String newTanHash = Base64.getEncoder().encodeToString(
+ MessageDigest.getInstance("SHA-256").digest(claimResponse.getTan().getBytes(StandardCharsets.UTF_8)));
+ ClaimRequest newClaimRequest = generateClaimRequest(Hex.decode(decodeResult.getCoseHex()),
+ egdcCodeData.getDgci(),newTanHash, certHash,
+ "RSA","SHA256WithRSA");
+ dgciService.claim(newClaimRequest);
}
@Test
@@ -254,6 +263,21 @@ private static byte[] convertConcatToDer(byte[] concat) throws CoseException {
return ASN1.EncodeSignature(r, s);
}
+ @Test
+ void checkDgciExists() throws Exception {
+ DgciInit dgciInit = new DgciInit();
+ dgciInit.setGreenCertificateType(GreenCertificateType.Vaccination);
+ DgciIdentifier initResult = dgciService.initDgci(dgciInit);
+ String dgciHash = Base64.getEncoder().encodeToString(
+ MessageDigest.getInstance("SHA256")
+ .digest(initResult.getDgci().getBytes(StandardCharsets.UTF_8)));
+ assertEquals(DgciService.DgciStatus.EXISTS,dgciService.checkDgciStatus(dgciHash));
+ String dgciHashNotExsits = Base64.getEncoder().encodeToString(
+ MessageDigest.getInstance("SHA256")
+ .digest("not exists".getBytes(StandardCharsets.UTF_8)));
+ assertEquals(DgciService.DgciStatus.NOT_EXISTS,dgciService.checkDgciStatus(dgciHashNotExsits));
+ }
+
}