dgciEntityOpt = dgciRepository.findById(dgciId);
if (dgciEntityOpt.isPresent()) {
var dgciEntity = dgciEntityOpt.get();
- String tan = tanService.generateNewTan();
- dgciEntity.setHashedTan(tanService.hashTan(tan));
+ Tan tan = Tan.create();
+ dgciEntity.setHashedTan(tan.getHashedTan());
dgciEntity.setCertHash(issueData.getHash());
dgciRepository.saveAndFlush(dgciEntity);
log.info("signed for " + dgciId);
String signatureBase64 = certificateService.signHash(issueData.getHash());
- return new SignatureData(tan, signatureBase64);
+ return new SignatureData(tan.getRawTan(), signatureBase64);
} else {
log.warn("can not find dgci with id " + dgciId);
throw new DgciNotFound("dgci with id " + dgciId + " not found");
@@ -264,13 +254,13 @@ public ClaimResponse claim(ClaimRequest claimRequest) {
dgciEntity.setClaimed(true);
dgciEntity.setRetryCounter(dgciEntity.getRetryCounter() + 1);
dgciEntity.setPublicKey(asJwk(claimRequest.getPublicKey()));
- String newTan = tanService.generateNewTan();
- dgciEntity.setHashedTan(tanService.hashTan(newTan));
+ Tan newTan = Tan.create();
+ dgciEntity.setHashedTan(newTan.getHashedTan());
dgciEntity.setRetryCounter(0);
log.info("dgci {} claimed", dgciEntity.getDgci());
dgciRepository.saveAndFlush(dgciEntity);
ClaimResponse claimResponse = new ClaimResponse();
- claimResponse.setTan(newTan);
+ claimResponse.setTan(newTan.getRawTan());
return claimResponse;
} else {
log.info("can not find dgci {}", claimRequest.getDgci());
@@ -376,14 +366,14 @@ public EgdcCodeData createEdgc(Eudgc eudgc) {
EgdcCodeData egdcCodeData = new EgdcCodeData();
egdcCodeData.setQrcCode(chainResult.getStep5Prefixed());
egdcCodeData.setDgci(dgci);
- String tan = tanService.generateNewTan();
- egdcCodeData.setTan(tan);
+ Tan ta = Tan.create();
+ egdcCodeData.setTan(ta.getRawTan());
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.setDgciHash(HashUtil.sha256Base64(dgci));
+ dgciEntity.setHashedTan(ta.getHashedTan());
dgciEntity.setGreenCertificateType(greenCertificateType);
dgciEntity.setCreatedAt(ZonedDateTime.now());
dgciEntity.setExpiresAt(ZonedDateTime.now().plus(expirationService.expirationForType(greenCertificateType)));
diff --git a/src/main/java/eu/europa/ec/dgc/issuance/service/Tan.java b/src/main/java/eu/europa/ec/dgc/issuance/service/Tan.java
new file mode 100644
index 0000000..2894789
--- /dev/null
+++ b/src/main/java/eu/europa/ec/dgc/issuance/service/Tan.java
@@ -0,0 +1,55 @@
+package eu.europa.ec.dgc.issuance.service;
+
+import eu.europa.ec.dgc.issuance.utils.HashUtil;
+import java.security.SecureRandom;
+import org.apache.commons.lang3.RandomStringUtils;
+
+public final class Tan {
+
+ private static final int TAN_LENGTH = 8;
+ private static final String HASH_ALGORITHM = "SHA-256";
+ private static final char[] CHAR_SET_FOR_TAN = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789".toCharArray();
+
+ private String rawTan;
+ private String hashedTan;
+
+ private Tan() {
+ }
+
+ /**
+ * Create new TAN object with a TAN and the hash of the TAN. The TAN is constructed from a charset consisting
+ * of A-Z (exclcuding I and O) and 2-9.
+ *
+ * @return the newly created TAN object
+ */
+ public static Tan create() {
+ Tan retVal = new Tan();
+ retVal.rawTan = retVal.generateNewTan();
+ retVal.hashedTan = HashUtil.sha256Base64(retVal.rawTan);
+ return retVal;
+ }
+
+ private String generateNewTan() {
+ SecureRandom random = new SecureRandom();
+ long rnd = random.nextLong();
+ int radixLen = CHAR_SET_FOR_TAN.length;
+ StringBuilder tan = new StringBuilder();
+ while (tan.length() < TAN_LENGTH) {
+ if (rnd == 0) {
+ rnd = random.nextLong();
+ continue;
+ }
+ tan.append(CHAR_SET_FOR_TAN[Math.abs((int) (rnd % radixLen))]);
+ rnd /= radixLen;
+ }
+ return tan.toString();
+ }
+
+ public String getRawTan() {
+ return rawTan;
+ }
+
+ public String getHashedTan() {
+ return hashedTan;
+ }
+}
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
deleted file mode 100644
index f29f6f6..0000000
--- a/src/main/java/eu/europa/ec/dgc/issuance/service/TanService.java
+++ /dev/null
@@ -1,91 +0,0 @@
-/*-
- * ---license-start
- * EU Digital Green Certificate Issuance Service / dgca-issuance-service
- * ---
- * Copyright (C) 2021 T-Systems International GmbH and all other contributors
- * ---
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- * ---license-end
- */
-
-package eu.europa.ec.dgc.issuance.service;
-
-import java.nio.charset.StandardCharsets;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.security.SecureRandom;
-import java.util.Base64;
-import java.util.Random;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.stereotype.Component;
-
-@Slf4j
-@Component
-public class TanService {
-
- private final Random random = new SecureRandom();
- private final char[] charSet;
- private static final int TAN_LENGTH = 8;
-
- /**
- * Constructs TanService with Whitelist of allowed TAN-Chars.
- */
- public TanService() {
- StringBuilder chars = new StringBuilder();
- for (char i = '0'; i <= '9'; i++) {
- chars.append(i);
- }
- for (char i = 'A'; i <= 'Z'; i++) {
- if (i != 'I' && i != '0') {
- chars.append(i);
- }
- }
- charSet = chars.toString().toCharArray();
- }
-
- /**
- * Generates a new TAN.
- * The TAN has a length of 8 characters. The generated TAN does not include letter I and O.
- *
- * @return TAN String.
- */
- public String generateNewTan() {
- long rnd = random.nextLong();
- int radixLen = charSet.length;
- StringBuilder tan = new StringBuilder();
- while (tan.length() < TAN_LENGTH) {
- if (rnd == 0) {
- rnd = random.nextLong();
- continue;
- }
- tan.append(charSet[Math.abs((int) (rnd % radixLen))]);
- rnd /= radixLen;
- }
- return tan.toString();
- }
-
- /**
- * compute tan hash.
- *
- * @return TAN hash
- */
- public String hashTan(String tan) {
- try {
- final MessageDigest digest = MessageDigest.getInstance("SHA-256");
- final byte[] hashBytes = digest.digest(tan.getBytes(StandardCharsets.UTF_8));
- return Base64.getEncoder().encodeToString(hashBytes);
- } catch (NoSuchAlgorithmException e) {
- throw new IllegalArgumentException(e);
- }
- }
-}
diff --git a/src/main/java/eu/europa/ec/dgc/issuance/service/impl/BtpAbstractKeyProvider.java b/src/main/java/eu/europa/ec/dgc/issuance/service/impl/BtpAbstractKeyProvider.java
new file mode 100644
index 0000000..6b93866
--- /dev/null
+++ b/src/main/java/eu/europa/ec/dgc/issuance/service/impl/BtpAbstractKeyProvider.java
@@ -0,0 +1,71 @@
+package eu.europa.ec.dgc.issuance.service.impl;
+
+import eu.europa.ec.dgc.issuance.service.CertificatePrivateKeyProvider;
+import eu.europa.ec.dgc.issuance.utils.btp.CredentialStore;
+import eu.europa.ec.dgc.issuance.utils.btp.SapCredential;
+import java.io.ByteArrayInputStream;
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.util.Base64;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Abstract class with interfaces to the SAP BTP {@link CredentialStore}. It provides methods to get certificates
+ * as well as private keys from the credential store. Implementations of {@link CertificatePrivateKeyProvider}
+ * inheriting from this abstract class do not have to implement a connection to the credential store themselves.
+ * Note: Keys in the credential store are supposed to be in X.509 or RSA format and base64 encoded. Raw keys
+ * will be stripped off line breaks and -----BEGIN / END KEY-----
phrases.
+ */
+public abstract class BtpAbstractKeyProvider implements CertificatePrivateKeyProvider {
+
+ private static final Logger log = LoggerFactory.getLogger(BtpAbstractKeyProvider.class);
+
+ protected final CredentialStore credentialStore;
+
+ public BtpAbstractKeyProvider(CredentialStore credentialStore) {
+ this.credentialStore = credentialStore;
+ }
+
+ protected Certificate getCertificateFromStore(String certName) {
+ SapCredential cert = credentialStore.getKeyByName(certName);
+ String certContent = cleanKeyString(cert.getValue());
+
+ try {
+ byte[] certDecoded = Base64.getDecoder().decode(certContent);
+ CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
+ return certFactory.generateCertificate(new ByteArrayInputStream(certDecoded));
+ } catch (CertificateException e) {
+ log.error("Error building certificate: {}.", e.getMessage());
+ throw new RuntimeException(e);
+ }
+ }
+
+ protected PrivateKey getPrivateKeyFromStore(String keyName) {
+ SapCredential key = credentialStore.getKeyByName(keyName);
+ String keyContent = cleanKeyString(key.getValue());
+
+ try {
+ KeyFactory kf = KeyFactory.getInstance("RSA");
+ PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(Base64.getDecoder().decode(keyContent));
+ return kf.generatePrivate(pkcs8EncodedKeySpec);
+ } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
+ log.error("Error building private key: {}", e.getMessage());
+ throw new RuntimeException(e);
+ }
+ }
+
+ private String cleanKeyString(String rawKey) {
+ return rawKey.replaceAll("\\n", "")
+ .replace("-----BEGIN PRIVATE KEY-----", "")
+ .replace("-----BEGIN PUBLIC KEY-----", "")
+ .replace("-----END PUBLIC KEY-----", "")
+ .replace("-----END PRIVATE KEY-----", "");
+ }
+}
diff --git a/src/main/java/eu/europa/ec/dgc/issuance/service/impl/BtpCertKeyPublisherServiceImpl.java b/src/main/java/eu/europa/ec/dgc/issuance/service/impl/BtpCertKeyPublisherServiceImpl.java
new file mode 100644
index 0000000..2b1ec6c
--- /dev/null
+++ b/src/main/java/eu/europa/ec/dgc/issuance/service/impl/BtpCertKeyPublisherServiceImpl.java
@@ -0,0 +1,96 @@
+package eu.europa.ec.dgc.issuance.service.impl;
+
+import com.sap.cloud.sdk.cloudplatform.connectivity.DestinationAccessor;
+import com.sap.cloud.sdk.cloudplatform.connectivity.HttpClientAccessor;
+import com.sap.cloud.sdk.cloudplatform.connectivity.HttpDestination;
+import eu.europa.ec.dgc.issuance.service.CertKeyPublisherService;
+import eu.europa.ec.dgc.issuance.service.CertificatePrivateKeyProvider;
+import eu.europa.ec.dgc.signing.SignedCertificateMessageBuilder;
+import eu.europa.ec.dgc.utils.CertificateUtils;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.X509Certificate;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.client.methods.RequestBuilder;
+import org.apache.http.entity.StringEntity;
+import org.bouncycastle.cert.X509CertificateHolder;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.context.annotation.Profile;
+import org.springframework.stereotype.Component;
+
+/**
+ * Publishes the issuer's public certificate to the DGC gateway. The public certificate will be signed with the upload
+ * key provided by the upload key provider.
+ *
+ * @see BtpUploadKeyProviderImpl
+ */
+@Component
+@Profile("btp")
+@Slf4j
+public class BtpCertKeyPublisherServiceImpl implements CertKeyPublisherService {
+
+ private static final String DGCG_DESTINATION = "dgcg-destination";
+ private static final String DGCG_UPLOAD_ENDPOINT = "/signerCertificate";
+
+ private final CertificatePrivateKeyProvider uploadKeyProvider;
+ private final CertificatePrivateKeyProvider issuerKeyProvider;
+ private final CertificateUtils certificateUtils;
+
+ /**
+ * Initializes the publisher service with all key provider and utilities needed for uploading certificates to
+ * the gateway.
+ *
+ * @param uploadKeyProvider the upload certificate needed to sign the request
+ * @param issuerKeyProvider the issuer certificate beeing uploaded
+ * @param certificateUtils utilities to convert different certificate formats
+ */
+ public BtpCertKeyPublisherServiceImpl(
+ @Qualifier("uploadKeyProvider") CertificatePrivateKeyProvider uploadKeyProvider,
+ @Qualifier("issuerKeyProvider") CertificatePrivateKeyProvider issuerKeyProvider,
+ CertificateUtils certificateUtils) {
+ this.uploadKeyProvider = uploadKeyProvider;
+ this.issuerKeyProvider = issuerKeyProvider;
+ this.certificateUtils = certificateUtils;
+ }
+
+ @Override
+ public void publishKey() {
+ log.debug("Uploading key to gateway.");
+ HttpDestination httpDestination = DestinationAccessor.getDestination(DGCG_DESTINATION).asHttp();
+ HttpClient httpClient = HttpClientAccessor.getHttpClient(httpDestination);
+
+ try {
+ X509CertificateHolder issuerCertHolder = certificateUtils
+ .convertCertificate((X509Certificate) issuerKeyProvider.getCertificate());
+ X509CertificateHolder uploadCertHolder = certificateUtils
+ .convertCertificate((X509Certificate) uploadKeyProvider.getCertificate());
+
+ String payload = new SignedCertificateMessageBuilder()
+ .withPayloadCertificate(issuerCertHolder)
+ .withSigningCertificate(uploadCertHolder, uploadKeyProvider.getPrivateKey()).buildAsString();
+
+ HttpUriRequest postRequest = RequestBuilder.post(DGCG_UPLOAD_ENDPOINT)
+ .addHeader("Content-type", "application/cms")
+ .setEntity(new StringEntity(payload, StandardCharsets.UTF_8))
+ .build();
+
+ HttpResponse response = httpClient.execute(postRequest);
+ if (response.getStatusLine().getStatusCode() == HttpStatus.SC_CREATED) {
+ log.info("Successfully upload certificate to gateway.");
+ } else {
+ log.warn("Gateway returned 'HTTP {}: {}'.", response.getStatusLine().getStatusCode(),
+ response.getStatusLine().getReasonPhrase());
+ }
+
+ } catch (CertificateEncodingException | IOException e) {
+ log.error("Error while upload certificate to gateway: '{}'.", e.getMessage());
+ throw new RuntimeException(e);
+ }
+ }
+
+}
diff --git a/src/main/java/eu/europa/ec/dgc/issuance/service/impl/BtpIssuerKeyProviderImpl.java b/src/main/java/eu/europa/ec/dgc/issuance/service/impl/BtpIssuerKeyProviderImpl.java
new file mode 100644
index 0000000..35f2cab
--- /dev/null
+++ b/src/main/java/eu/europa/ec/dgc/issuance/service/impl/BtpIssuerKeyProviderImpl.java
@@ -0,0 +1,34 @@
+package eu.europa.ec.dgc.issuance.service.impl;
+
+import eu.europa.ec.dgc.issuance.utils.btp.CredentialStore;
+import java.security.PrivateKey;
+import java.security.cert.Certificate;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Profile;
+import org.springframework.stereotype.Component;
+
+@Slf4j
+@Component("issuerKeyProvider")
+@Profile("btp")
+public class BtpIssuerKeyProviderImpl extends BtpAbstractKeyProvider {
+
+ private static final String ISSUER_KEY_NAME = "issuer-key";
+ private static final String ISSUER_CERT_NAME = "issuer-cert";
+
+ @Autowired
+ public BtpIssuerKeyProviderImpl(CredentialStore credentialStore) {
+ super(credentialStore);
+ }
+
+ @Override
+ public Certificate getCertificate() {
+ return this.getCertificateFromStore(ISSUER_CERT_NAME);
+ }
+
+ @Override
+ public PrivateKey getPrivateKey() {
+ return getPrivateKeyFromStore(ISSUER_KEY_NAME);
+ }
+
+}
diff --git a/src/main/java/eu/europa/ec/dgc/issuance/service/impl/BtpUploadKeyProviderImpl.java b/src/main/java/eu/europa/ec/dgc/issuance/service/impl/BtpUploadKeyProviderImpl.java
new file mode 100644
index 0000000..14581a1
--- /dev/null
+++ b/src/main/java/eu/europa/ec/dgc/issuance/service/impl/BtpUploadKeyProviderImpl.java
@@ -0,0 +1,34 @@
+package eu.europa.ec.dgc.issuance.service.impl;
+
+import eu.europa.ec.dgc.issuance.utils.btp.CredentialStore;
+import java.security.PrivateKey;
+import java.security.cert.Certificate;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Profile;
+import org.springframework.stereotype.Component;
+
+@Component("uploadKeyProvider")
+@Profile("btp")
+@Slf4j
+public class BtpUploadKeyProviderImpl extends BtpAbstractKeyProvider {
+
+ private static final String UPLOAD_KEY_NAME = "upload-key";
+ private static final String UPLOAD_CERT_NAME = "upload-cert";
+
+ @Autowired
+ public BtpUploadKeyProviderImpl(CredentialStore credentialStore) {
+ super(credentialStore);
+ }
+
+ @Override
+ public Certificate getCertificate() {
+ return getCertificateFromStore(UPLOAD_CERT_NAME);
+ }
+
+ @Override
+ public PrivateKey getPrivateKey() {
+ return getPrivateKeyFromStore(UPLOAD_KEY_NAME);
+ }
+
+}
diff --git a/src/main/java/eu/europa/ec/dgc/issuance/service/impl/CertificatePrivateKeyProviderImpl.java b/src/main/java/eu/europa/ec/dgc/issuance/service/impl/CertificatePrivateKeyProviderImpl.java
index 662e150..8fb2ff9 100644
--- a/src/main/java/eu/europa/ec/dgc/issuance/service/impl/CertificatePrivateKeyProviderImpl.java
+++ b/src/main/java/eu/europa/ec/dgc/issuance/service/impl/CertificatePrivateKeyProviderImpl.java
@@ -22,9 +22,11 @@
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
-@Component
+@Component("issuerKeyProvider")
+@Profile("!btp")
@Slf4j
@RequiredArgsConstructor
public class CertificatePrivateKeyProviderImpl implements CertificatePrivateKeyProvider {
diff --git a/src/main/java/eu/europa/ec/dgc/issuance/utils/DgciUtil.java b/src/main/java/eu/europa/ec/dgc/issuance/utils/DgciUtil.java
new file mode 100644
index 0000000..75b9e1b
--- /dev/null
+++ b/src/main/java/eu/europa/ec/dgc/issuance/utils/DgciUtil.java
@@ -0,0 +1,24 @@
+package eu.europa.ec.dgc.issuance.utils;
+
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import java.util.UUID;
+
+public class DgciUtil {
+
+ /**
+ * Encode UUID to charset of A-Z and 0-9.
+ *
+ * @param uuid the UUID to hash
+ * @return the hashed UUID
+ */
+ public static String encodeDgci(UUID uuid) {
+ 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');
+ return bint.toString(radix).toUpperCase();
+ }
+
+}
diff --git a/src/main/java/eu/europa/ec/dgc/issuance/utils/HashUtil.java b/src/main/java/eu/europa/ec/dgc/issuance/utils/HashUtil.java
new file mode 100644
index 0000000..d2b20a1
--- /dev/null
+++ b/src/main/java/eu/europa/ec/dgc/issuance/utils/HashUtil.java
@@ -0,0 +1,26 @@
+package eu.europa.ec.dgc.issuance.utils;
+
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Base64;
+
+public class HashUtil {
+
+ /**
+ * Generates a SHA-256 hash and returns it as Base64 encoded string.
+ *
+ * @param raw the raw input
+ * @return the Base64 encode hash
+ */
+ public static String sha256Base64(String raw) {
+ try {
+ final MessageDigest digest = MessageDigest.getInstance("SHA-256");
+ final byte[] hashBytes = digest.digest(raw.getBytes(StandardCharsets.UTF_8));
+ return Base64.getEncoder().encodeToString(hashBytes);
+ } catch (NoSuchAlgorithmException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+}
diff --git a/src/main/java/eu/europa/ec/dgc/issuance/utils/btp/CredentialStore.java b/src/main/java/eu/europa/ec/dgc/issuance/utils/btp/CredentialStore.java
new file mode 100644
index 0000000..f966fa0
--- /dev/null
+++ b/src/main/java/eu/europa/ec/dgc/issuance/utils/btp/CredentialStore.java
@@ -0,0 +1,45 @@
+package eu.europa.ec.dgc.issuance.utils.btp;
+
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Profile;
+import org.springframework.stereotype.Component;
+import org.springframework.web.client.RestTemplate;
+
+@Component
+@Profile("btp")
+public class CredentialStore {
+
+ private static final Logger log = LoggerFactory.getLogger(CredentialStore.class);
+
+ @Value("${sap.btp.credstore.url}")
+ private String url;
+
+ private final CredentialStoreCryptoUtil cryptoUtil;
+
+ private final RestTemplate restTemplate;
+
+ @Autowired
+ public CredentialStore(CredentialStoreCryptoUtil cryptoUtil, RestTemplate restTemplate) {
+ this.cryptoUtil = cryptoUtil;
+ this.restTemplate = restTemplate;
+ }
+
+ /**
+ * Return the key located under the given name in the credential store.
+ *
+ * @param name the name of the key
+ * @return the key from the credential store
+ */
+ public SapCredential getKeyByName(String name) {
+ log.debug("Querying key with name '{}'.", name);
+ String response = restTemplate.getForEntity(url + "/key?name=" + URLEncoder.encode(name,
+ StandardCharsets.UTF_8), String.class).getBody();
+ return SapCredential.fromJson(cryptoUtil.decrypt(response));
+ }
+
+}
diff --git a/src/main/java/eu/europa/ec/dgc/issuance/utils/btp/CredentialStoreConfig.java b/src/main/java/eu/europa/ec/dgc/issuance/utils/btp/CredentialStoreConfig.java
new file mode 100644
index 0000000..304bc3f
--- /dev/null
+++ b/src/main/java/eu/europa/ec/dgc/issuance/utils/btp/CredentialStoreConfig.java
@@ -0,0 +1,42 @@
+package eu.europa.ec.dgc.issuance.utils.btp;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.web.client.RestTemplateBuilder;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Profile;
+import org.springframework.web.client.RestTemplate;
+
+@Configuration
+@Profile("btp")
+public class CredentialStoreConfig {
+
+ @Value("${sap.btp.credstore.username}")
+ private String username;
+
+ @Value("${sap.btp.credstore.password}")
+ private String password;
+
+ @Value("${sap.btp.credstore.namespace}")
+ private String namespace;
+
+ @Bean
+ RestTemplate restTemplate(RestTemplateBuilder builder) {
+ RestTemplate restTemplate = builder.build();
+ restTemplate.getInterceptors().add((request, body, execution) -> {
+ request.getHeaders().set("Authorization", "Basic " + getAuthToken());
+ request.getHeaders().set("sapcp-credstore-namespace", namespace);
+ return execution.execute(request, body);
+ });
+
+ return restTemplate;
+ }
+
+ private String getAuthToken() {
+ String authHeader = username + ":" + password;
+ return Base64.getEncoder().encodeToString(authHeader.getBytes(StandardCharsets.UTF_8));
+ }
+
+}
diff --git a/src/main/java/eu/europa/ec/dgc/issuance/utils/btp/CredentialStoreCryptoUtil.java b/src/main/java/eu/europa/ec/dgc/issuance/utils/btp/CredentialStoreCryptoUtil.java
new file mode 100644
index 0000000..2715126
--- /dev/null
+++ b/src/main/java/eu/europa/ec/dgc/issuance/utils/btp/CredentialStoreCryptoUtil.java
@@ -0,0 +1,80 @@
+package eu.europa.ec.dgc.issuance.utils.btp;
+
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JWEObject;
+import com.nimbusds.jose.Payload;
+import com.nimbusds.jose.crypto.RSADecrypter;
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.security.spec.X509EncodedKeySpec;
+import java.text.ParseException;
+import java.util.Base64;
+import javax.annotation.PostConstruct;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.NotImplementedException;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Profile;
+import org.springframework.stereotype.Component;
+
+@Slf4j
+@Component
+@Profile("btp")
+public class CredentialStoreCryptoUtil {
+
+ @Value("${sap.btp.credstore.clientPrivateKey}")
+ private String clientPrivateKeyBase64;
+
+ @Value("${sap.btp.credstore.serverPublicKey}")
+ private String serverPublicKeyBase64;
+
+ @Value("${sap.btp.credstore.encrypted}")
+ private boolean encryptionEnabled;
+
+ private PrivateKey ownPrivateKey;
+
+ private PublicKey serverPublicKey;
+
+ @PostConstruct
+ private void prepare() throws NoSuchAlgorithmException, InvalidKeySpecException {
+ if (!encryptionEnabled) {
+ return;
+ }
+
+ KeyFactory rsaKeyFactory = KeyFactory.getInstance("RSA");
+ PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(Base64.getDecoder()
+ .decode(clientPrivateKeyBase64));
+ this.ownPrivateKey = rsaKeyFactory.generatePrivate(pkcs8EncodedKeySpec);
+
+ X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(Base64.getDecoder()
+ .decode(serverPublicKeyBase64));
+ this.serverPublicKey = rsaKeyFactory.generatePublic(x509EncodedKeySpec);
+ }
+
+ protected void encrypt() {
+ throw new NotImplementedException("Encryption is still to be implemented yet.");
+ }
+
+ protected String decrypt(String jweResponse) {
+ if (!encryptionEnabled) {
+ return jweResponse;
+ }
+
+ JWEObject jweObject;
+
+ try {
+ RSADecrypter rsaDecrypter = new RSADecrypter(ownPrivateKey);
+ jweObject = JWEObject.parse(jweResponse);
+ jweObject.decrypt(rsaDecrypter);
+
+ Payload payload = jweObject.getPayload();
+ return payload.toString();
+ } catch (ParseException | JOSEException e) {
+ log.error("Failed to parse JWE response: {}.", e.getMessage());
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/src/main/java/eu/europa/ec/dgc/issuance/utils/btp/SapCredential.java b/src/main/java/eu/europa/ec/dgc/issuance/utils/btp/SapCredential.java
new file mode 100644
index 0000000..1e5861b
--- /dev/null
+++ b/src/main/java/eu/europa/ec/dgc/issuance/utils/btp/SapCredential.java
@@ -0,0 +1,31 @@
+package eu.europa.ec.dgc.issuance.utils.btp;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import java.util.Date;
+import lombok.Data;
+
+@Data
+public class SapCredential {
+
+ private String id;
+ private String name;
+ private Date modifiedAt;
+ private String value;
+ private String status;
+ private String username;
+ private String format;
+ private String category;
+ private String type;
+
+ public static SapCredential fromJson(String rawJson) {
+ return gson().fromJson(rawJson, SapCredential.class);
+ }
+
+ private static Gson gson() {
+ return new GsonBuilder()
+ .enableComplexMapKeySerialization()
+ .create();
+ }
+
+}
diff --git a/src/main/resources/META-INF/spring.factories b/src/main/resources/META-INF/spring.factories
new file mode 100644
index 0000000..3178d86
--- /dev/null
+++ b/src/main/resources/META-INF/spring.factories
@@ -0,0 +1 @@
+io.pivotal.cfenv.spring.boot.CfEnvProcessor=eu.europa.ec.dgc.issuance.config.btp.SapCredentialStoreCfEnvProcessor
diff --git a/src/main/resources/application-btp.yml b/src/main/resources/application-btp.yml
new file mode 100644
index 0000000..b46af5d
--- /dev/null
+++ b/src/main/resources/application-btp.yml
@@ -0,0 +1,10 @@
+sap:
+ btp:
+ credstore:
+ namespace: DgcaIssuerServiceCredentialStore
+ encrypted: false
+ username:
+ password:
+ url:
+ clientPrivateKey:
+ serverPublicKey:
diff --git a/src/test/java/eu/europa/ec/dgc/issuance/Sh256HashTest.java b/src/test/java/eu/europa/ec/dgc/issuance/EncodingTest.java
similarity index 51%
rename from src/test/java/eu/europa/ec/dgc/issuance/Sh256HashTest.java
rename to src/test/java/eu/europa/ec/dgc/issuance/EncodingTest.java
index fd0bb86..3a1ae15 100644
--- a/src/test/java/eu/europa/ec/dgc/issuance/Sh256HashTest.java
+++ b/src/test/java/eu/europa/ec/dgc/issuance/EncodingTest.java
@@ -20,34 +20,32 @@
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 eu.europa.ec.dgc.issuance.utils.DgciUtil;
+import eu.europa.ec.dgc.issuance.utils.HashUtil;
import java.util.UUID;
import org.junit.Test;
-public class Sh256HashTest {
+import static org.junit.Assert.assertEquals;
+
+public class EncodingTest {
+
+ public static final String TEST_TAN = "U7ULCYZY";
+ public static final String TEST_TAN_HASHED = "avmGz38ugM7uBePwKKlvh3IB8+7O+WFhQEbjIxhTxgY=";
+
+ public static final String TEST_UUID = "cd7737d4-51ca-45f8-9f74-3a173b9a1f47";
+ public static final String TEST_DGCI_REP = "NW393C1D87A44870V7TTFQMYC";
+
@Test
public void testCreateSHA256Hash() throws Exception {
- final MessageDigest digest = MessageDigest.getInstance("SHA-256");
- final byte[] hashbytes = digest.digest(
- "some_data".getBytes(StandardCharsets.UTF_8));
- System.out.println(Base64.getEncoder().encodeToString(hashbytes));
+ String output = HashUtil.sha256Base64(TEST_TAN);
+ assertEquals(TEST_TAN_HASHED, output);
}
@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());
+ UUID uuid = UUID.fromString(TEST_UUID);
+ String dgciRep = DgciUtil.encodeDgci(uuid);
+ assertEquals(25, dgciRep.length());
+ assertEquals(TEST_DGCI_REP, dgciRep);
}
}
diff --git a/src/test/java/eu/europa/ec/dgc/issuance/GenerateWalletRequestTest.java b/src/test/java/eu/europa/ec/dgc/issuance/GenerateWalletRequestTest.java
index 40ba0f4..797949e 100644
--- a/src/test/java/eu/europa/ec/dgc/issuance/GenerateWalletRequestTest.java
+++ b/src/test/java/eu/europa/ec/dgc/issuance/GenerateWalletRequestTest.java
@@ -24,11 +24,11 @@
import com.fasterxml.jackson.databind.SerializationFeature;
import eu.europa.ec.dgc.issuance.restapi.dto.ClaimRequest;
import eu.europa.ec.dgc.issuance.restapi.dto.PublicKey;
-import eu.europa.ec.dgc.issuance.service.TanService;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
+import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.Signature;
@@ -40,15 +40,13 @@ public class GenerateWalletRequestTest {
// This can be used to generate valid json structure for claim
@Test
public void testGenerateWalletRequest() throws Exception {
- TanService tanService = new TanService();
-
// Please adapt this to your certificate (the values can be get from browser network log
// see POST /dgci
// and PUT /dgci/{id}
String dgci = "dgci:V1:DE:2e974b3b-d932-4bc9-bbae-d387f93f8bf3:edbcb873196f24be";
String certHash = "mfg0MI7wPFexNkOa4n9OKojrzhe9a9lcim4JzJO3WtY=";
String tan = "U7ULCYZY";
- String tanHash = tanService.hashTan(tan);
+ String tanHash = "avmGz38ugM7uBePwKKlvh3IB8+7O+WFhQEbjIxhTxgY=";
KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA");
keyPairGen.initialize(2048);
@@ -72,6 +70,16 @@ public void testGenerateWalletRequest() throws Exception {
System.out.println(objectMapper.writeValueAsString(claimRequest));
}
+ public static void main(String[] args) {
+ try {
+ final MessageDigest digest = MessageDigest.getInstance("SHA-256");
+ final byte[] hashBytes = digest.digest("U7ULCYZY".getBytes(StandardCharsets.UTF_8));
+ System.out.println(Base64.getEncoder().encodeToString(hashBytes));
+ } catch (NoSuchAlgorithmException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
private void createClaimSignature(ClaimRequest claimRequest, PrivateKey privateKey, String sigAlg) throws InvalidKeyException, NoSuchAlgorithmException, SignatureException {
StringBuilder sigValue = new StringBuilder();
sigValue.append(claimRequest.getTanHash())
@@ -86,15 +94,13 @@ private void createClaimSignature(ClaimRequest claimRequest, PrivateKey privateK
@Test
public void testGenerateWalletRequestEC() throws Exception {
- TanService tanService = new TanService();
-
// Please adapt this to your certificate (the values can be get from browser network log
// see POST /dgci
// and PUT /dgci/{id}
String dgci = "dgci:V1:DE:2e974b3b-d932-4bc9-bbae-d387f93f8bf3:edbcb873196f24be";
String certHash = "mfg0MI7wPFexNkOa4n9OKojrzhe9a9lcim4JzJO3WtY=";
String tan = "U7ULCYZY";
- String tanHash = tanService.hashTan(tan);
+ String tanHash = "avmGz38ugM7uBePwKKlvh3IB8+7O+WFhQEbjIxhTxgY=";
KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("EC");
keyPairGen.initialize(256);
diff --git a/src/test/java/eu/europa/ec/dgc/issuance/service/TanServiceTest.java b/src/test/java/eu/europa/ec/dgc/issuance/service/TanServiceTest.java
index b351530..16f35a2 100644
--- a/src/test/java/eu/europa/ec/dgc/issuance/service/TanServiceTest.java
+++ b/src/test/java/eu/europa/ec/dgc/issuance/service/TanServiceTest.java
@@ -23,13 +23,15 @@
import org.junit.Test;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
public class TanServiceTest {
+
@Test
public void testGenerateTan() throws Exception {
- TanService tanService = new TanService();
- String tan = tanService.generateNewTan();
- System.out.println(tan);
- assertEquals(8,tan.length());
+ Tan tan = Tan.create();
+ assertNotNull(tan);
+ assertEquals(8, tan.getRawTan().length());
}
+
}
diff --git a/src/test/java/eu/europa/ec/dgc/issuance/utils/btp/SapCredentialParserTest.java b/src/test/java/eu/europa/ec/dgc/issuance/utils/btp/SapCredentialParserTest.java
new file mode 100644
index 0000000..de28046
--- /dev/null
+++ b/src/test/java/eu/europa/ec/dgc/issuance/utils/btp/SapCredentialParserTest.java
@@ -0,0 +1,26 @@
+package eu.europa.ec.dgc.issuance.utils.btp;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import org.junit.Assert;
+import org.junit.Test;
+import org.springframework.core.io.ClassPathResource;
+
+public class SapCredentialParserTest {
+
+ private static final String EXPECTED_ID = "5f5fa34e-be21-4c7a-8548-4a538b7156ed";
+ private static final String EXPECTED_VALUE = "SoMeBaSe64EnCoDeDkEy==";
+ private static final String EXPECTED_NAME = "key-name";
+
+ @Test
+ public void testJsonParsing() throws IOException {
+ ClassPathResource jsonResource = new ClassPathResource("/data/fromCredStore.json");
+ String json = Files.readString(jsonResource.getFile().toPath(), StandardCharsets.UTF_8);
+
+ SapCredential sapCredential = SapCredential.fromJson(json);
+ Assert.assertEquals(EXPECTED_ID, sapCredential.getId());
+ Assert.assertEquals(EXPECTED_VALUE, sapCredential.getValue());
+ Assert.assertEquals(EXPECTED_NAME, sapCredential.getName());
+ }
+}
diff --git a/src/test/resources/data/fromCredStore.json b/src/test/resources/data/fromCredStore.json
new file mode 100644
index 0000000..5801a5d
--- /dev/null
+++ b/src/test/resources/data/fromCredStore.json
@@ -0,0 +1,11 @@
+{
+ "id": "5f5fa34e-be21-4c7a-8548-4a538b7156ed",
+ "name": "key-name",
+ "modifiedAt": "2021-05-12T22:16:55.806Z",
+ "value": "SoMeBaSe64EnCoDeDkEy==",
+ "status": "enabled",
+ "username": "key-name",
+ "format": "X.509",
+ "category": "someCategory",
+ "type": "someType"
+}