Skip to content

Commit

Permalink
updates claim, dgci gen, dgci status (#44)
Browse files Browse the repository at this point in the history
* feat/dgcistatus

* dgci reduced length

* multiple claim on post endpoint

* update dgc-lib version t 0.4.0
  • Loading branch information
a-trzewik authored May 7, 2021
1 parent 8f31788 commit d7a63f7
Show file tree
Hide file tree
Showing 11 changed files with 158 additions and 46 deletions.
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<!-- dependencies -->
<dgclib.version>0.3.1</dgclib.version>
<dgclib.version>0.4.0</dgclib.version>
<owasp.version>6.1.1</owasp.version>
<spring.boot.version>2.4.4</spring.boot.version>
<spring.cloud.version>2020.0.2</spring.cloud.version>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,5 @@ public interface DgciRepository extends JpaRepository<DgciEntity, Long> {

Optional<DgciEntity> findByDgci(String dgci);

Optional<DgciEntity> findByDgciHash(String dgciHash);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -116,20 +118,43 @@ public ResponseEntity<EgdcCodeData> 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<DidDocument> getDidDocument(@PathVariable String opaque, String hash) {
return ResponseEntity.ok(dgciService.getDidDocument(hash));
@GetMapping(value = "/{dgciHash}")
public ResponseEntity<DidDocument> 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<String> 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<Void> 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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<Void> claim(@Valid @RequestBody ClaimRequest claimRequest)
throws IOException, NoSuchAlgorithmException, SignatureException, InvalidKeySpecException, InvalidKeyException {
dgciService.claim(claimRequest);
return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
public ResponseEntity<ClaimResponse> 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<ClaimResponse> claimUpdate(@Valid @RequestBody ClaimRequest claimRequest) {
return ResponseEntity.ok(dgciService.claimUpdate(claimRequest));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,5 @@

@Data
public class ClaimResponse {
// TODO Claim Response properties
String tan;
}
28 changes: 18 additions & 10 deletions src/main/java/eu/europa/ec/dgc/issuance/service/DgciGenerator.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
}
}
59 changes: 49 additions & 10 deletions src/main/java/eu/europa/ec/dgc/issuance/service/DgciService.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);

Expand All @@ -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;
Expand Down Expand Up @@ -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<DgciEntity> 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");
}
Expand All @@ -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());
Expand Down Expand Up @@ -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> 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;
}
}
7 changes: 7 additions & 0 deletions src/main/resources/db/changelog/init-tables.xml
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,11 @@
<changeSet id="dgci-sequence" author="jhagestedt">
<addAutoIncrement tableName="dgci" columnName="id" columnDataType="bigint" startWith="1" incrementBy="1"/>
</changeSet>
<changeSet id="dgci-hash" author="atrzewik">
<addColumn tableName="dgci" >
<column name="dgci_hash" type="varchar(512)">
<constraints unique="true"/>
</column>
</addColumn>
</changeSet>
</databaseChangeLog>
17 changes: 17 additions & 0 deletions src/test/java/eu/europa/ec/dgc/issuance/Sh256HashTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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));
}



}

0 comments on commit d7a63f7

Please sign in to comment.