diff --git a/pom.xml b/pom.xml
index 5294701..7200545 100644
--- a/pom.xml
+++ b/pom.xml
@@ -241,6 +241,16 @@
kotlin-stdlib-jdk8
1.4.31
+
+ com.fasterxml.jackson.dataformat
+ jackson-dataformat-cbor
+ 2.12.3
+
+
+ com.nimbusds
+ nimbus-jose-jwt
+ 9.9.2
+
io.pivotal.cfenv
java-cfenv-boot
diff --git a/src/main/java/eu/europa/ec/dgc/issuance/config/HcertLibConfig.java b/src/main/java/eu/europa/ec/dgc/issuance/config/HcertLibConfig.java
index f0968f0..431e8de 100644
--- a/src/main/java/eu/europa/ec/dgc/issuance/config/HcertLibConfig.java
+++ b/src/main/java/eu/europa/ec/dgc/issuance/config/HcertLibConfig.java
@@ -1,12 +1,10 @@
package eu.europa.ec.dgc.issuance.config;
import ehn.techiop.hcert.kotlin.chain.Base45Service;
-import ehn.techiop.hcert.kotlin.chain.CborService;
import ehn.techiop.hcert.kotlin.chain.CompressorService;
import ehn.techiop.hcert.kotlin.chain.ContextIdentifierService;
import ehn.techiop.hcert.kotlin.chain.CoseService;
import ehn.techiop.hcert.kotlin.chain.impl.DefaultBase45Service;
-import ehn.techiop.hcert.kotlin.chain.impl.DefaultCborService;
import ehn.techiop.hcert.kotlin.chain.impl.DefaultCompressorService;
import ehn.techiop.hcert.kotlin.chain.impl.DefaultContextIdentifierService;
import ehn.techiop.hcert.kotlin.chain.impl.DefaultCoseService;
@@ -41,9 +39,4 @@ Base45Service base45Service() {
return new DefaultBase45Service();
}
- @Bean
- CborService cborService() {
- return new DefaultCborService();
- }
-
}
diff --git a/src/main/java/eu/europa/ec/dgc/issuance/config/IssuanceConfigProperties.java b/src/main/java/eu/europa/ec/dgc/issuance/config/IssuanceConfigProperties.java
index 7682dca..9e75b71 100644
--- a/src/main/java/eu/europa/ec/dgc/issuance/config/IssuanceConfigProperties.java
+++ b/src/main/java/eu/europa/ec/dgc/issuance/config/IssuanceConfigProperties.java
@@ -20,21 +20,51 @@
package eu.europa.ec.dgc.issuance.config;
+import java.time.Duration;
+import java.time.temporal.ChronoUnit;
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Size;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.convert.DurationUnit;
@Getter
@Setter
@ConfigurationProperties("issuance")
public class IssuanceConfigProperties {
-
+ @NotBlank
+ @Size(max = 20)
private String dgciPrefix;
private String keyStoreFile;
private String keyStorePassword;
private String certAlias;
private String privateKeyPassword;
+ @NotBlank
+ @Size(max = 2)
private String countryCode;
- private int tanExpirationHours = 24;
+ @DurationUnit(ChronoUnit.HOURS)
+ private Duration tanExpirationHours = Duration.ofHours(24);
+ /**
+ * JSON file that is provided to /context endpoint.
+ */
+ private String contextFile;
+ @NotNull
+ private Expiration expiration;
+
+ @Getter
+ @Setter
+ public static class Expiration {
+ @DurationUnit(ChronoUnit.DAYS)
+ @NotNull
+ private Duration vaccination;
+ @DurationUnit(ChronoUnit.DAYS)
+ @NotNull
+ private Duration recovery;
+ @DurationUnit(ChronoUnit.DAYS)
+ @NotNull
+ private Duration test;
+ }
}
diff --git a/src/main/java/eu/europa/ec/dgc/issuance/config/OpenApiConfig.java b/src/main/java/eu/europa/ec/dgc/issuance/config/OpenApiConfig.java
index 884330c..544cf79 100644
--- a/src/main/java/eu/europa/ec/dgc/issuance/config/OpenApiConfig.java
+++ b/src/main/java/eu/europa/ec/dgc/issuance/config/OpenApiConfig.java
@@ -3,6 +3,7 @@
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
+import java.util.Optional;
import lombok.Generated;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.info.BuildProperties;
@@ -14,7 +15,7 @@
@RequiredArgsConstructor
public class OpenApiConfig {
- private final BuildProperties buildProperties;
+ private final Optional buildProperties;
/**
* Configure the OpenApi bean with title and version.
@@ -23,11 +24,18 @@ public class OpenApiConfig {
*/
@Bean
public OpenAPI openApi() {
+ String version;
+ if (buildProperties.isPresent()) {
+ version = buildProperties.get().getVersion();
+ } else {
+ // build properties is not available if starting from IDE without running mvn before (so fake this)
+ version = "dev";
+ }
return new OpenAPI()
.info(new Info()
.title("Digital Green Certificate Issuance")
.description("The API defines Issuance Service for digital green certificates.")
- .version(buildProperties.getVersion())
+ .version(version)
.license(new License()
.name("Apache 2.0")
.url("https://www.apache.org/licenses/LICENSE-2.0")));
diff --git a/src/main/java/eu/europa/ec/dgc/issuance/restapi/controller/CertController.java b/src/main/java/eu/europa/ec/dgc/issuance/restapi/controller/CertController.java
index 33501b6..fea79b7 100644
--- a/src/main/java/eu/europa/ec/dgc/issuance/restapi/controller/CertController.java
+++ b/src/main/java/eu/europa/ec/dgc/issuance/restapi/controller/CertController.java
@@ -69,7 +69,7 @@ public class CertController {
* Controller for creating Vaccination Certificate.
*/
@Operation(
- summary = "create edgc with process step informations, developing tool"
+ summary = "create edgc with process step information, developing tool"
)
@PostMapping(value = "create", consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity createVaccinationCertificate(@RequestBody Eudgc eudgc) {
diff --git a/src/main/java/eu/europa/ec/dgc/issuance/restapi/controller/ContextController.java b/src/main/java/eu/europa/ec/dgc/issuance/restapi/controller/ContextController.java
new file mode 100644
index 0000000..508ae87
--- /dev/null
+++ b/src/main/java/eu/europa/ec/dgc/issuance/restapi/controller/ContextController.java
@@ -0,0 +1,31 @@
+package eu.europa.ec.dgc.issuance.restapi.controller;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import eu.europa.ec.dgc.issuance.service.ContextService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.responses.ApiResponses;
+import lombok.AllArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/context")
+@AllArgsConstructor
+public class ContextController {
+ private final ContextService contextService;
+
+ @Operation(
+ summary = "provide configuration information for wallet app",
+ description = "list of claim endpoints for wallet app"
+ )
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "server list")}
+ )
+ @GetMapping(value = "")
+ public ResponseEntity context() {
+ return ResponseEntity.ok(contextService.getContextDefintion());
+ }
+}
diff --git a/src/main/java/eu/europa/ec/dgc/issuance/restapi/dto/DidAuthentication.java b/src/main/java/eu/europa/ec/dgc/issuance/restapi/dto/DidAuthentication.java
index 0b28cbe..b9f90de 100644
--- a/src/main/java/eu/europa/ec/dgc/issuance/restapi/dto/DidAuthentication.java
+++ b/src/main/java/eu/europa/ec/dgc/issuance/restapi/dto/DidAuthentication.java
@@ -20,13 +20,13 @@
package eu.europa.ec.dgc.issuance.restapi.dto;
+import com.fasterxml.jackson.databind.JsonNode;
import lombok.Data;
@Data
public class DidAuthentication {
private String type;
private String controller;
- // TODO use data type and ISO Date formater
private String expires;
- private String publicKeyBase58;
+ private JsonNode publicKeyJsw;
}
diff --git a/src/main/java/eu/europa/ec/dgc/issuance/service/ConfigurableCborService.java b/src/main/java/eu/europa/ec/dgc/issuance/service/ConfigurableCborService.java
new file mode 100644
index 0000000..84df6d0
--- /dev/null
+++ b/src/main/java/eu/europa/ec/dgc/issuance/service/ConfigurableCborService.java
@@ -0,0 +1,64 @@
+package eu.europa.ec.dgc.issuance.service;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.dataformat.cbor.databind.CBORMapper;
+import com.upokecenter.cbor.CBORObject;
+import ehn.techiop.hcert.data.Eudgc;
+import ehn.techiop.hcert.kotlin.chain.impl.DefaultCborService;
+import eu.europa.ec.dgc.issuance.config.IssuanceConfigProperties;
+import eu.europa.ec.dgc.issuance.entity.GreenCertificateType;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import lombok.RequiredArgsConstructor;
+import org.jetbrains.annotations.NotNull;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+/**
+ * own cbor service.
+ * The default one inject fixed country code and expiration period
+ *
+ */
+@Service
+@RequiredArgsConstructor
+public class ConfigurableCborService extends DefaultCborService {
+ public static final int ISSUER = 1;
+ public static final int ISSUED_AT = 6;
+ public static final int EXPIRATION = 4;
+ public static final int HCERT = -260;
+ public static final int HCERT_VERSION = 1;
+ // Need autowired because there is circular reference
+ @Autowired
+ private DgciService dgciService;
+
+ private final IssuanceConfigProperties issuanceConfigProperties;
+
+ @Override
+ public byte[] encode(@NotNull Eudgc input) {
+ byte[] cbor;
+ try {
+ cbor = new CBORMapper().writeValueAsBytes(input);
+ } catch (JsonProcessingException e) {
+ throw new IllegalArgumentException(e);
+ }
+ GreenCertificateType greenCertificateType;
+ if (input.getT() != null && !input.getT().isEmpty()) {
+ greenCertificateType = GreenCertificateType.Test;
+ } else if (input.getR() != null && !input.getR().isEmpty()) {
+ greenCertificateType = GreenCertificateType.Recovery;
+ } else {
+ greenCertificateType = GreenCertificateType.Vaccination;
+ }
+ long issueTime = Instant.now().getEpochSecond();
+ long expirationTime = issueTime + dgciService.expirationForType(greenCertificateType).get(ChronoUnit.SECONDS);
+ CBORObject coseContainer = CBORObject.NewMap();
+ coseContainer.set(CBORObject.FromObject(ISSUER),
+ CBORObject.FromObject(issuanceConfigProperties.getCountryCode()));
+ coseContainer.set(CBORObject.FromObject(ISSUED_AT),CBORObject.FromObject(issueTime));
+ coseContainer.set(CBORObject.FromObject(EXPIRATION),CBORObject.FromObject(expirationTime));
+ CBORObject hcert = CBORObject.NewMap();
+ hcert.set(CBORObject.FromObject(HCERT_VERSION),CBORObject.DecodeFromBytes(cbor));
+ coseContainer.set(CBORObject.FromObject(HCERT),hcert);
+ return coseContainer.EncodeToBytes();
+ }
+}
diff --git a/src/main/java/eu/europa/ec/dgc/issuance/service/ContextService.java b/src/main/java/eu/europa/ec/dgc/issuance/service/ContextService.java
new file mode 100644
index 0000000..a5e0f0c
--- /dev/null
+++ b/src/main/java/eu/europa/ec/dgc/issuance/service/ContextService.java
@@ -0,0 +1,55 @@
+package eu.europa.ec.dgc.issuance.service;
+
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.JsonNodeFactory;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import eu.europa.ec.dgc.issuance.config.IssuanceConfigProperties;
+import java.io.File;
+import java.io.IOException;
+import javax.annotation.PostConstruct;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+@Component
+@Slf4j
+@RequiredArgsConstructor
+public class ContextService {
+ private final IssuanceConfigProperties issuanceConfigProperties;
+ private JsonNode contextDefinition;
+
+ /**
+ * load json context file.
+ */
+ @PostConstruct
+ public void loadContextFile() {
+ if (issuanceConfigProperties.getContextFile() != null
+ && issuanceConfigProperties.getContextFile().length() > 0) {
+ File contextFile = new File(issuanceConfigProperties.getContextFile());
+ if (!contextFile.isFile()) {
+ throw new IllegalArgumentException("configured context file can not be found: " + contextFile);
+ }
+ ObjectMapper mapper = new ObjectMapper();
+ try {
+ contextDefinition = mapper.readTree(contextFile);
+ log.info("context file loaded from: " + contextFile);
+ } catch (IOException e) {
+ throw new IllegalArgumentException("can not read json context file: " + contextFile, e);
+ }
+ } else {
+ log.warn("the context json file not configured (property: issuance.contextFile)."
+ + " The empty context file is generated instead");
+ JsonNodeFactory jsonNodeFactory = JsonNodeFactory.instance;
+ ObjectNode contextObj = jsonNodeFactory.objectNode();
+ contextObj.set("Origin", jsonNodeFactory.textNode(issuanceConfigProperties.getCountryCode()));
+ contextObj.set("claimEndpoints", jsonNodeFactory.arrayNode());
+ contextDefinition = contextObj;
+ }
+ }
+
+ public JsonNode getContextDefintion() {
+ return contextDefinition;
+ }
+}
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 14ff142..32ea8f7 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
@@ -20,6 +20,12 @@
package eu.europa.ec.dgc.issuance.service;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.nimbusds.jose.jwk.Curve;
+import com.nimbusds.jose.jwk.ECKey;
+import com.nimbusds.jose.jwk.RSAKey;
import com.upokecenter.cbor.CBORObject;
import com.upokecenter.cbor.CBORType;
import ehn.techiop.hcert.data.Eudgc;
@@ -51,14 +57,16 @@
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
+import java.security.interfaces.ECPublicKey;
+import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.time.Duration;
import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Base64;
-import java.util.Date;
import java.util.List;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
@@ -72,7 +80,9 @@
@RequiredArgsConstructor
public class DgciService {
- public enum DgciStatus { EXISTS, NOT_EXISTS, LOCKED }
+ public enum DgciStatus {
+ EXISTS, NOT_EXISTS, LOCKED
+ }
private final DgciRepository dgciRepository;
private final TanService tanService;
@@ -87,10 +97,6 @@ public enum DgciStatus { EXISTS, NOT_EXISTS, LOCKED }
private static final int MAX_CLAIM_RETRY_TAN = 3;
- // one year in seconds
- // TODO: shift to spring configuration
- private static final long EXPIRATION_PERIOD_SEC = 60 * 60 * 24 * 364L;
-
/**
* Initializes new DGCI.
*
@@ -104,13 +110,15 @@ public DgciIdentifier initDgci(DgciInit dgciInit) {
dgciEntity.setDgci(dgci);
dgciEntity.setDgciHash(dgciHash(dgci));
dgciEntity.setGreenCertificateType(dgciInit.getGreenCertificateType());
- dgciRepository.saveAndFlush(dgciEntity);
log.info("init dgci: {} id: {}", dgci, dgciEntity.getId());
- Date now = new Date();
- long sec = now.getTime() / 1000;
- long expiration = sec + expirationForType(dgciInit.getGreenCertificateType());
+ ZonedDateTime now = ZonedDateTime.now();
+ ZonedDateTime expiration = now.plus(expirationForType(dgciInit.getGreenCertificateType()));
+ long expirationSec = expiration.toInstant().getEpochSecond();
+
+ dgciEntity.setExpiresAt(expiration);
+ dgciRepository.saveAndFlush(dgciEntity);
return new DgciIdentifier(
dgciEntity.getId(),
@@ -118,7 +126,7 @@ public DgciIdentifier initDgci(DgciInit dgciInit) {
certificateService.getKidAsBase64(),
certificateService.getAlgorithmIdentifier(),
issuanceConfigProperties.getCountryCode(),
- expiration
+ expirationSec
);
}
@@ -132,9 +140,34 @@ private String dgciHash(String dgci) {
}
}
- private long expirationForType(GreenCertificateType greenCertificateType) {
- // TODO compute expiration dependend on certificate type and probably config
- return EXPIRATION_PERIOD_SEC;
+ /**
+ * expiration duration for given edgc type.
+ * @param greenCertificateType edgc type
+ * @return Duration
+ */
+ public Duration expirationForType(GreenCertificateType greenCertificateType) {
+ Duration duration;
+ if (issuanceConfigProperties.getExpiration() != null) {
+ switch (greenCertificateType) {
+ case Test:
+ duration = issuanceConfigProperties.getExpiration().getTest();
+ break;
+ case Vaccination:
+ duration = issuanceConfigProperties.getExpiration().getVaccination();
+ break;
+ case Recovery:
+ duration = issuanceConfigProperties.getExpiration().getRecovery();
+ break;
+ default:
+ throw new IllegalArgumentException("unsupported cert type for expiration: " + greenCertificateType);
+ }
+ } else {
+ duration = null;
+ }
+ if (duration == null) {
+ duration = Duration.of(365, ChronoUnit.DAYS);
+ }
+ return duration;
}
@NotNull
@@ -169,26 +202,39 @@ public SignatureData finishDgci(long dgciId, IssueData issueData) throws Excepti
/**
* get did document.
*
- * @param hash hash
+ * @param dgciHash dgciHash
* @return didDocument
*/
- public DidDocument getDidDocument(String hash) {
- DidDocument didDocument = new DidDocument();
- didDocument.setContext("https://w3id.org/did/v1");
- // TODO DID fake data
- didDocument.setId("dgc:V1:DE:xxxxxxxxx:34sdfmnn3434fdf89");
- didDocument.setController("did:web:ec.europa.eu/health/dgc/efdv34k34mdmdfj344");
- DidAuthentication didAuthentication = new DidAuthentication();
- didAuthentication.setController("dgc:V1:DE:xxxxxxxxx:34sdfmnn3434fdf89");
- didAuthentication.setType("EcdsaSecp256k1VerificationKey2018");
- didAuthentication.setExpires("2017-02-08T16:02:20Z");
- // TODO use base58 here
- didAuthentication.setPublicKeyBase58(Base64.getEncoder().encodeToString(certificateService.publicKey()));
-
- List didAuthentications = new ArrayList<>();
- didAuthentications.add(didAuthentication);
- didDocument.setAuthentication(didAuthentications);
- return didDocument;
+ public DidDocument getDidDocument(String dgciHash) {
+ Optional dgciEntityOpt = dgciRepository.findByDgciHash(dgciHash);
+ if (dgciEntityOpt.isPresent()) {
+ DgciEntity dgciEntity = dgciEntityOpt.get();
+ DidDocument didDocument = new DidDocument();
+ didDocument.setContext("https://w3id.org/did/v1");
+ didDocument.setId(dgciEntity.getDgci());
+ didDocument.setController(dgciEntity.getDgci());
+ List didAuthentications = new ArrayList<>();
+ if (dgciEntity.isClaimed()) {
+ DidAuthentication didAuthentication = new DidAuthentication();
+ didAuthentication.setController(dgciEntity.getDgci());
+ didAuthentication.setType("EcdsaSecp256k1VerificationKey2018");
+ didAuthentication.setExpires(dgciEntity.getExpiresAt()
+ .toOffsetDateTime().format(
+ DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'")));
+ ObjectMapper objectMapper = new ObjectMapper();
+ try {
+ JsonNode jwkNode = objectMapper.readTree(dgciEntity.getPublicKey());
+ didAuthentication.setPublicKeyJsw(jwkNode);
+ } catch (JsonProcessingException e) {
+ log.error("data error, public key is not jwk json for dgci.id" + dgciEntity.getId());
+ }
+ didAuthentications.add(didAuthentication);
+ }
+ didDocument.setAuthentication(didAuthentications);
+ return didDocument;
+ } else {
+ throw new DgciNotFound("can not find dgci with hash: " + dgciHash);
+ }
}
/**
@@ -215,16 +261,6 @@ public byte[] computeCoseSignHash(byte[] coseMessage) {
}
}
- /**
- * Currently not Implemented.
- */
- public ClaimResponse claimUpdate(ClaimRequest claimRequest) {
- ClaimResponse claimResponse = new ClaimResponse();
- // TODO wallet claim post (update?)
- throw new RuntimeException("not implemented yet");
- // return claimResponse;
- }
-
/**
* claim dgci to wallet app.
* means bind dgci with some public key from wallet app
@@ -251,14 +287,14 @@ public ClaimResponse claim(ClaimRequest claimRequest) {
}
if (!dgciEntity.isClaimed()) {
ZonedDateTime tanExpireTime = dgciEntity.getCreatedAt()
- .plus(Duration.ofHours(issuanceConfigProperties.getTanExpirationHours()));
+ .plus(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.setPublicKey(asJwk(claimRequest.getPublicKey()));
String newTan = tanService.generateNewTan();
dgciEntity.setHashedTan(tanService.hashTan(newTan));
dgciEntity.setRetryCounter(0);
@@ -273,6 +309,37 @@ public ClaimResponse claim(ClaimRequest claimRequest) {
}
}
+ private String asJwk(eu.europa.ec.dgc.issuance.restapi.dto.PublicKey publicKeyClaim) {
+ byte[] keyBytes = Base64.getDecoder().decode(publicKeyClaim.getValue());
+ X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes);
+ KeyFactory kf;
+ try {
+ kf = KeyFactory.getInstance(publicKeyClaim.getType());
+ } catch (NoSuchAlgorithmException e) {
+ throw new WrongRequest("key type not supported: '" + publicKeyClaim.getType()
+ + "', try RSA or EC");
+ }
+ PublicKey publicKey;
+ try {
+ publicKey = kf.generatePublic(spec);
+ } catch (InvalidKeySpecException e) {
+ throw new WrongRequest("invalid key");
+ }
+ String jwkString;
+ if (publicKey instanceof RSAPublicKey) {
+ RSAKey jwkKey = new RSAKey.Builder((RSAPublicKey) publicKey).build();
+ jwkString = jwkKey.toJSONString();
+ } else if (publicKey instanceof ECPublicKey) {
+ ECPublicKey ecPublicKey = (ECPublicKey) publicKey;
+ Curve curve = Curve.forECParameterSpec(ecPublicKey.getParams());
+ ECKey jwkKey = new ECKey.Builder(curve,ecPublicKey).build();
+ jwkString = jwkKey.toJSONString();
+ } else {
+ throw new WrongRequest("unsupported key type");
+ }
+ return jwkString;
+ }
+
private boolean verifySignature(ClaimRequest claimRequest) {
byte[] keyBytes = Base64.getDecoder().decode(claimRequest.getPublicKey().getValue());
X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes);
@@ -346,10 +413,11 @@ public EgdcCodeData createEdgc(Eudgc eudgc) {
DgciEntity dgciEntity = new DgciEntity();
dgciEntity.setDgci(dgci);
dgciEntity.setCertHash(Base64.getEncoder().encodeToString(computeCoseSignHash(chainResult.getStep2Cose())));
+ dgciEntity.setDgciHash(dgciHash(dgci));
dgciEntity.setHashedTan(tanService.hashTan(tan));
dgciEntity.setGreenCertificateType(greenCertificateType);
dgciEntity.setCreatedAt(ZonedDateTime.now());
- dgciEntity.setExpiresAt(ZonedDateTime.now().plus(expirationForType(greenCertificateType), ChronoUnit.SECONDS));
+ dgciEntity.setExpiresAt(ZonedDateTime.now().plus(expirationForType(greenCertificateType)));
dgciRepository.saveAndFlush(dgciEntity);
return egdcCodeData;
diff --git a/src/main/java/eu/europa/ec/dgc/issuance/service/TanService.java b/src/main/java/eu/europa/ec/dgc/issuance/service/TanService.java
index 60f9226..f29f6f6 100644
--- a/src/main/java/eu/europa/ec/dgc/issuance/service/TanService.java
+++ b/src/main/java/eu/europa/ec/dgc/issuance/service/TanService.java
@@ -75,7 +75,9 @@ public String generateNewTan() {
}
/**
- * TODO comment.
+ * compute tan hash.
+ *
+ * @return TAN hash
*/
public String hashTan(String tan) {
try {
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index 758cbfd..ae22706 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -46,7 +46,11 @@ issuance:
certAlias: edgc_dev_ec
privateKeyPassword: dgca
countryCode: DE
- tanExpirationHours: 24
+ tanExpirationHours: 2
+ expiration:
+ vaccination: 365
+ recoverty: 365
+ test: 60
dgc:
gateway:
connector:
diff --git a/src/test/java/eu/europa/ec/dgc/issuance/JwkCoreTest.java b/src/test/java/eu/europa/ec/dgc/issuance/JwkCoreTest.java
new file mode 100644
index 0000000..9c9e8fa
--- /dev/null
+++ b/src/test/java/eu/europa/ec/dgc/issuance/JwkCoreTest.java
@@ -0,0 +1,44 @@
+package eu.europa.ec.dgc.issuance;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.nimbusds.jose.jwk.Curve;
+import com.nimbusds.jose.jwk.ECKey;
+import com.nimbusds.jose.jwk.RSAKey;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.interfaces.ECPublicKey;
+import java.security.interfaces.RSAPublicKey;
+import org.junit.jupiter.api.Test;
+
+class JwkCoreTest {
+ @Test
+ void testRSAgeneration() throws Exception {
+ KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA");
+ keyPairGen.initialize(2048);
+ KeyPair keyPair = keyPairGen.generateKeyPair();
+
+ RSAKey jwkKey = new RSAKey.Builder((RSAPublicKey) keyPair.getPublic()).build();
+ ObjectMapper objectMapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT);
+ String jwkString = jwkKey.toJSONString();
+ JsonNode jwkElem = objectMapper.readTree(jwkString);
+ System.out.println(jwkString);
+ System.out.println(objectMapper.writeValueAsString(jwkElem));
+ }
+
+ @Test
+ void testECgeneration() throws Exception {
+ KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("EC");
+ keyPairGen.initialize(256);
+ KeyPair keyPair = keyPairGen.generateKeyPair();
+ ECPublicKey ecPublicKey = (ECPublicKey) keyPair.getPublic();
+ Curve curve = Curve.forECParameterSpec(ecPublicKey.getParams());
+
+ ECKey jwkKey = new ECKey.Builder(curve,(ECPublicKey) keyPair.getPublic()).build();
+ ObjectMapper objectMapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT);
+ String jwkString = jwkKey.toJSONString();
+ JsonNode jwkElem = objectMapper.readTree(jwkString);
+ System.out.println(objectMapper.writeValueAsString(jwkElem));
+ }
+}
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 3dcd8e5..fc0114d 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
@@ -2,6 +2,7 @@
import COSE.ASN1;
import COSE.CoseException;
+import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import ehn.techiop.hcert.data.Eudgc;
import ehn.techiop.hcert.kotlin.chain.SampleData;
@@ -13,6 +14,7 @@
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.DidDocument;
import eu.europa.ec.dgc.issuance.restapi.dto.EgcDecodeResult;
import eu.europa.ec.dgc.issuance.restapi.dto.EgdcCodeData;
import eu.europa.ec.dgc.issuance.restapi.dto.IssueData;
@@ -88,6 +90,10 @@ void testDGCISign() throws Exception {
assertNotNull(signatureData.getSignature());
assertNotNull(signatureData.getTan());
assertEquals(8,signatureData.getTan().length());
+
+ String dgciHash = sha256(dgciIdentifier.getDgci());
+ DidDocument didDocument = dgciService.getDidDocument(dgciHash);
+ assertNotNull(didDocument);
}
@Test
@@ -101,10 +107,22 @@ void testCreateEdgcBackend() throws Exception {
Optional dgciEnitiyOpt = dgciRepository.findByDgci(egdcCodeData.getDgci());
assertTrue(dgciEnitiyOpt.isPresent());
assertEquals(GreenCertificateType.Vaccination,dgciEnitiyOpt.get().getGreenCertificateType());
+ assertNotNull(dgciEnitiyOpt.get().getCertHash());
+ assertNotNull(dgciEnitiyOpt.get().getDgciHash());
+ assertNotNull(dgciEnitiyOpt.get().getHashedTan());
+ assertNotNull(dgciEnitiyOpt.get().getExpiresAt());
EgcDecodeResult decodeResult = edgcValidator.decodeEdgc(egdcCodeData.getQrcCode());
assertTrue(decodeResult.isValidated());
assertNull(decodeResult.getErrorMessage());
+ JsonNode cborJson = decodeResult.getCborJson();
+ assertNotNull(cborJson);
+ assertEquals(issuanceConfigProperties.getCountryCode(),cborJson.get("1").asText());
+ long createdAt = cborJson.get("1").asLong();
+ long expiredAt = cborJson.get("4").asLong();
+ JsonNode payload = cborJson.get("-260").get("1");
+ assertNotNull(payload);
+ assertTrue(payload.isObject());
}
@Test
@@ -142,6 +160,20 @@ void testWalletClaim() throws Exception {
egdcCodeData.getDgci(),newTanHash, certHash,
"RSA","SHA256WithRSA");
dgciService.claim(newClaimRequest);
+
+ String dgciHash = sha256(egdcCodeData.getDgci());
+ DidDocument didDocument = dgciService.getDidDocument(dgciHash);
+ assertNotNull(didDocument);
+ assertNotNull(didDocument.getAuthentication());
+ assertFalse(didDocument.getAuthentication().isEmpty());
+ System.out.println(objectMapper.writeValueAsString(didDocument.getAuthentication().get(0).getPublicKeyJsw()));
+
+ }
+
+ private String sha256(String toHash) throws NoSuchAlgorithmException {
+ return Base64.getEncoder().encodeToString(
+ MessageDigest.getInstance("SHA256")
+ .digest(toHash.getBytes(StandardCharsets.UTF_8)));
}
@Test
@@ -171,8 +203,17 @@ void testWalletClaimEC() throws Exception {
dgciEnitiyOpt = dgciRepository.findByDgci(egdcCodeData.getDgci());
assertTrue(dgciEnitiyOpt.isPresent());
assertTrue(dgciEnitiyOpt.get().isClaimed());
+
+ String dgciHash = sha256(egdcCodeData.getDgci());
+ DidDocument didDocument = dgciService.getDidDocument(dgciHash);
+ assertNotNull(didDocument);
+ assertNotNull(didDocument.getAuthentication());
+ assertFalse(didDocument.getAuthentication().isEmpty());
+ System.out.println(objectMapper.writeValueAsString(didDocument.getAuthentication().get(0).getPublicKeyJsw()));
}
+
+
private ClaimRequest generateClaimRequest(byte[] coseMessage, String dgci, String tanHash, String certHash64, String keyType, String sigAlg) throws Exception {
ClaimRequest claimRequest = new ClaimRequest();
claimRequest.setDgci(dgci);