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