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)); + } + }