Skip to content

Commit

Permalink
feat: btp integration (#56)
Browse files Browse the repository at this point in the history
* refactor: moved some helper methods to utils in order to reduce code duplication

* feat: connection to gateway and credential store on BTP

closes #18, closes #45, closes #55
  • Loading branch information
jurosens committed May 14, 2021
1 parent 9e7cba2 commit 14f3bd1
Show file tree
Hide file tree
Showing 27 changed files with 774 additions and 189 deletions.
16 changes: 16 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,13 @@
<artifactId>bcpkix-jdk15on</artifactId>
<version>${bcpkix.version}</version>
</dependency>
<dependency>
<groupId>com.sap.cloud.sdk</groupId>
<artifactId>sdk-bom</artifactId>
<version>3.43.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

Expand Down Expand Up @@ -256,6 +263,15 @@
<artifactId>java-cfenv-boot</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.6</version>
</dependency>
<dependency>
<groupId>com.sap.cloud.sdk.cloudplatform</groupId>
<artifactId>scp-cf</artifactId>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package eu.europa.ec.dgc.issuance.config.btp;

import io.pivotal.cfenv.core.CfCredentials;
import io.pivotal.cfenv.core.CfService;
import io.pivotal.cfenv.spring.boot.CfEnvProcessor;
import io.pivotal.cfenv.spring.boot.CfEnvProcessorProperties;
import java.util.Map;

/**
* Custom implementation of {@link CfEnvProcessor} for reading the SAP credential store parameters from the <code>
* VCAP_SERVICES</code> environment variable and making them available as properties in the spring context.
* <br/><br/>
* The following properties are available in the context after the processor is done:
* <code>
* <ul>
* <li>sap.btp.credstore.url</li>
* <li>sap.btp.credstore.password</li>
* <li>sap.btp.credstore.username</li>
* <li>sap.btp.credstore.clientPrivateKey</li>
* <li>sap.btp.credstore.serverPublicKey</li>
* </ul>
* </code>
*
* @see CfEnvProcessor
*/
public class SapCredentialStoreCfEnvProcessor implements CfEnvProcessor {

private static final String CRED_STORE_SCHEME = "credstore";
private static final String CRED_STORE_PROPERTY_PREFIX = "sap.btp.credstore";

@Override
public boolean accept(CfService service) {
return service.existsByTagIgnoreCase("credstore", "securestore", "keystore", "credentials")
|| service.existsByLabelStartsWith("credstore") || service.existsByUriSchemeStartsWith(CRED_STORE_SCHEME);
}

@Override
public void process(CfCredentials cfCredentials, Map<String, Object> properties) {
properties.put(CRED_STORE_PROPERTY_PREFIX + ".url", cfCredentials.getString("url"));
properties.put(CRED_STORE_PROPERTY_PREFIX + ".password", cfCredentials.getString("password"));
properties.put(CRED_STORE_PROPERTY_PREFIX + ".username", cfCredentials.getString("username"));

@SuppressWarnings("unchecked")
Map<String, Object> encryption = (Map<String, Object>) cfCredentials.getMap().get("encryption");
if (encryption == null) {
// Encryption features have been disabled on this BTP instance.
properties.put(CRED_STORE_PROPERTY_PREFIX + ".clientPrivateKey", "encryption-disabled");
properties.put(CRED_STORE_PROPERTY_PREFIX + ".serverPublicKey", "encryption-disabled");
return;
}

String clientPrivateKey = encryption.get("client_private_key").toString();
String serverPublicKey = encryption.get("server_public_key").toString();

properties.put(CRED_STORE_PROPERTY_PREFIX + ".clientPrivateKey", clientPrivateKey);
properties.put(CRED_STORE_PROPERTY_PREFIX + ".serverPublicKey", serverPublicKey);
}

@Override
public CfEnvProcessorProperties getProperties() {
return CfEnvProcessorProperties.builder()
.propertyPrefixes(CRED_STORE_PROPERTY_PREFIX)
.serviceName("CredentialStore")
.build();
}

}
Original file line number Diff line number Diff line change
@@ -1,36 +1,8 @@
package eu.europa.ec.dgc.issuance.service;


import eu.europa.ec.dgc.gateway.connector.DgcGatewayUploadConnector;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

@Component
@Slf4j
@RequiredArgsConstructor
public class CertKeyPublisherService {
private final CertificateService certificateService;
private final Optional<DgcGatewayUploadConnector> dgcGatewayUploadConnector;

public interface CertKeyPublisherService {
/**
* publish signing certificate to gateway.
* Publishes the signing certificate to the DGC gateway.
*/
public void publishKey() {
if (dgcGatewayUploadConnector.isPresent()) {
log.info("start publish certificate to gateway");
DgcGatewayUploadConnector connector = dgcGatewayUploadConnector.get();
try {
connector.uploadTrustedCertificate(certificateService.getCertficate());
log.info("certificate uploaded to gateway");
} catch (DgcGatewayUploadConnector.DgcCertificateUploadException e) {
log.error("can not upload certificate to gateway",e);
throw new DdcGatewayException("error during gateway connector communication",e);
}
} else {
log.warn("can not publish certificate to gateway, because the gateway connector is not enabled");
throw new DdcGatewayException("gateway connector is configured as disabled");
}
}
void publishKey();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package eu.europa.ec.dgc.issuance.service;


import eu.europa.ec.dgc.gateway.connector.DgcGatewayUploadConnector;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;

@Component
@Profile("!btp")
@Slf4j
@RequiredArgsConstructor
public class CertKeyPublisherServiceImpl implements CertKeyPublisherService {
private final CertificateService certificateService;
private final Optional<DgcGatewayUploadConnector> dgcGatewayUploadConnector;

@Override
public void publishKey() {
if (dgcGatewayUploadConnector.isPresent()) {
log.info("start publish certificate to gateway");
DgcGatewayUploadConnector connector = dgcGatewayUploadConnector.get();
try {
connector.uploadTrustedCertificate(certificateService.getCertficate());
log.info("certificate uploaded to gateway");
} catch (DgcGatewayUploadConnector.DgcCertificateUploadException e) {
log.error("can not upload certificate to gateway", e);
throw new DdcGatewayException("error during gateway connector communication", e);
}
} else {
log.warn("can not publish certificate to gateway, because the gateway connector is not enabled");
throw new DdcGatewayException("gateway connector is configured as disabled");
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -31,24 +31,32 @@
import javax.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

@Component
@Slf4j
@RequiredArgsConstructor
public class CertificateService {
private final CertificatePrivateKeyProvider certificatePrivateKeyProvider;
private final SigningService signingService;
private byte[] kid;

@Autowired
public CertificateService(@Qualifier("issuerKeyProvider") CertificatePrivateKeyProvider
certificatePrivateKeyProvider, SigningService signingService) {
this.certificatePrivateKeyProvider = certificatePrivateKeyProvider;
this.signingService = signingService;
}

/**
* compute kid.
* key identifier needed for cose
*/
@PostConstruct
public void computeKid() {
CertificateUtils certificateUtils = new CertificateUtils();
String kidBase64 = certificateUtils.getCertKid((X509Certificate)getCertficate());
String kidBase64 = certificateUtils.getCertKid(getCertficate());
kid = Base64.getDecoder().decode(kidBase64);
}

Expand Down
12 changes: 2 additions & 10 deletions src/main/java/eu/europa/ec/dgc/issuance/service/DgciGenerator.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@
package eu.europa.ec.dgc.issuance.service;

import eu.europa.ec.dgc.issuance.config.IssuanceConfigProperties;
import eu.europa.ec.dgc.issuance.utils.DgciUtil;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
Expand All @@ -41,15 +41,7 @@ public class DgciGenerator {
public String newDgci() {
StringBuilder sb = new StringBuilder();
sb.append(issuanceConfigProperties.getDgciPrefix()).append(':');
// use uuid but encode to 0-9A-Z charset
UUID uuid = UUID.randomUUID();
ByteBuffer bb = ByteBuffer.wrap(new byte[16]);
bb.putLong(uuid.getMostSignificantBits());
bb.putLong(uuid.getLeastSignificantBits());
BigInteger bint = new BigInteger(1, bb.array());
int radix = 10 + ('Z' - 'A');
String randomUuidEncoded = bint.toString(radix).toUpperCase();
sb.append(randomUuidEncoded);
sb.append(DgciUtil.encodeDgci(UUID.randomUUID()));
String checkSum = createDgciCheckSum(sb.toString());
sb.append(':').append(checkSum);
return sb.toString();
Expand Down
36 changes: 13 additions & 23 deletions src/main/java/eu/europa/ec/dgc/issuance/service/DgciService.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
import eu.europa.ec.dgc.issuance.restapi.dto.EgdcCodeData;
import eu.europa.ec.dgc.issuance.restapi.dto.IssueData;
import eu.europa.ec.dgc.issuance.restapi.dto.SignatureData;
import eu.europa.ec.dgc.issuance.utils.HashUtil;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
Expand Down Expand Up @@ -83,7 +84,6 @@ public enum DgciStatus {
}

private final DgciRepository dgciRepository;
private final TanService tanService;
private final CertificateService certificateService;
private final IssuanceConfigProperties issuanceConfigProperties;
private final DgciGenerator dgciGenerator;
Expand All @@ -107,7 +107,7 @@ public DgciIdentifier initDgci(DgciInit dgciInit) {
String dgci = generateDgci();

dgciEntity.setDgci(dgci);
dgciEntity.setDgciHash(dgciHash(dgci));
dgciEntity.setDgciHash(HashUtil.sha256Base64(dgci));
dgciEntity.setGreenCertificateType(dgciInit.getGreenCertificateType());

log.info("init dgci: {} id: {}", dgci, dgciEntity.getId());
Expand All @@ -129,16 +129,6 @@ public DgciIdentifier initDgci(DgciInit dgciInit) {
);
}

private String dgciHash(String dgci) {
try {
final MessageDigest digest = MessageDigest.getInstance("SHA-256");
final byte[] hashBytes = digest.digest(dgci.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(hashBytes);
} catch (NoSuchAlgorithmException e) {
throw new IllegalArgumentException(e);
}
}

@NotNull
private String generateDgci() {
return dgciGenerator.newDgci();
Expand All @@ -151,17 +141,17 @@ private String generateDgci() {
* @param issueData issueData
* @return signature data
*/
public SignatureData finishDgci(long dgciId, IssueData issueData) throws Exception {
public SignatureData finishDgci(long dgciId, IssueData issueData) {
Optional<DgciEntity> 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");
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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)));
Expand Down
55 changes: 55 additions & 0 deletions src/main/java/eu/europa/ec/dgc/issuance/service/Tan.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading

0 comments on commit 14f3bd1

Please sign in to comment.