Skip to content

Commit

Permalink
feat: connection to gateway and credential store on BTP
Browse files Browse the repository at this point in the history
closes #18, closes #45, closes #55
  • Loading branch information
jurosens committed May 14, 2021
1 parent 57fb6a3 commit f34e053
Show file tree
Hide file tree
Showing 19 changed files with 617 additions and 35 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
Original file line number Diff line number Diff line change
@@ -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;

/**
* <p>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.</p>
* <p><b>Note: </b>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 <code>-----BEGIN / END KEY-----</code> phrases.</p>
*/
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-----", "");
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}

}
Loading

0 comments on commit f34e053

Please sign in to comment.