Skip to content

Commit 620d826

Browse files
newtorkMatKuhrKavithaSiva
authored
Enable PEM file format for ClientCertificateAuthentication (#225)
Co-authored-by: Matthias Kuhr <52661546+MatKuhr@users.noreply.github.com> Co-authored-by: KavithaSiva <32287936+KavithaSiva@users.noreply.github.com>
1 parent c149ee0 commit 620d826

File tree

12 files changed

+341
-216
lines changed

12 files changed

+341
-216
lines changed

cloudplatform/cloudplatform-connectivity/pom.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,14 @@
9090
<groupId>com.auth0</groupId>
9191
<artifactId>java-jwt</artifactId>
9292
</dependency>
93+
<dependency>
94+
<groupId>org.bouncycastle</groupId>
95+
<artifactId>bcprov-jdk18on</artifactId>
96+
</dependency>
97+
<dependency>
98+
<groupId>org.bouncycastle</groupId>
99+
<artifactId>bcpkix-jdk18on</artifactId>
100+
</dependency>
93101
<!-- scope "provided" -->
94102
<dependency>
95103
<groupId>org.projectlombok</groupId>
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/*
2+
* Copyright (c) 2024 SAP SE or an SAP affiliate company. All rights reserved.
3+
*/
4+
5+
package com.sap.cloud.sdk.cloudplatform.connectivity;
6+
7+
import java.io.ByteArrayInputStream;
8+
import java.io.IOException;
9+
import java.io.Reader;
10+
import java.security.KeyStore;
11+
import java.security.KeyStoreException;
12+
import java.security.NoSuchAlgorithmException;
13+
import java.security.PrivateKey;
14+
import java.security.cert.Certificate;
15+
import java.security.cert.CertificateException;
16+
import java.security.cert.CertificateFactory;
17+
import java.util.ArrayList;
18+
import java.util.List;
19+
20+
import javax.annotation.Nonnull;
21+
import javax.annotation.Nullable;
22+
23+
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
24+
import org.bouncycastle.openssl.PEMKeyPair;
25+
import org.bouncycastle.openssl.PEMParser;
26+
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
27+
import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8DecryptorProviderBuilder;
28+
import org.bouncycastle.operator.InputDecryptorProvider;
29+
import org.bouncycastle.operator.OperatorCreationException;
30+
import org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo;
31+
import org.bouncycastle.pkcs.PKCSException;
32+
import org.bouncycastle.util.io.pem.PemObject;
33+
34+
import com.sap.cloud.sdk.cloudplatform.connectivity.exception.DestinationAccessException;
35+
36+
import io.vavr.control.Try;
37+
import lombok.AccessLevel;
38+
import lombok.NoArgsConstructor;
39+
40+
@NoArgsConstructor( access = AccessLevel.PRIVATE )
41+
class KeyStoreReader
42+
{
43+
private static final String MSG_CERT = "Provided certificate data did not contain any valid X.509 certificates.";
44+
private static final String MSG_KEY = "Provided key data did not contain a valid PEM key";
45+
46+
@Nonnull
47+
static KeyStore createKeyStore(
48+
@Nonnull final String alias,
49+
@Nonnull final char[] password,
50+
@Nonnull final Reader certReader,
51+
@Nonnull final Reader keyReader )
52+
throws KeyStoreException,
53+
CertificateException,
54+
IOException,
55+
NoSuchAlgorithmException
56+
{
57+
final Certificate[] clientCertificates =
58+
Try.of(() -> loadCertificates(certReader)).getOrElseThrow(e -> new DestinationAccessException(MSG_CERT, e));
59+
final PrivateKey privateKey =
60+
Try.of(() -> loadKey(keyReader, password)).getOrElseThrow(e -> new DestinationAccessException(MSG_KEY, e));
61+
final KeyStore keyStore = KeyStore.getInstance("JKS");
62+
keyStore.load(null);
63+
keyStore.setKeyEntry(alias, privateKey, password, clientCertificates);
64+
return keyStore;
65+
}
66+
67+
@Nonnull
68+
static Certificate[] loadCertificates( @Nonnull final Reader certReader )
69+
throws CertificateException,
70+
IOException
71+
{
72+
final List<Certificate> certs = new ArrayList<>();
73+
final CertificateFactory factory = CertificateFactory.getInstance("X509");
74+
75+
try( PEMParser pemParser = new PEMParser(certReader) ) {
76+
PemObject object;
77+
while( (object = pemParser.readPemObject()) != null ) {
78+
if( !object.getType().equals("CERTIFICATE") ) {
79+
continue;
80+
}
81+
certs.add(factory.generateCertificate(new ByteArrayInputStream(object.getContent())));
82+
}
83+
}
84+
if( certs.isEmpty() ) {
85+
throw new IllegalArgumentException(
86+
"Provided certificate data did not contain any valid X.509 certificates.");
87+
}
88+
return certs.toArray(new Certificate[0]);
89+
}
90+
91+
@Nonnull
92+
static PrivateKey loadKey( @Nonnull final Reader keyReader, @Nullable final char[] password )
93+
throws IOException,
94+
OperatorCreationException,
95+
PKCSException
96+
{
97+
try( PEMParser pemParser = new PEMParser(keyReader) ) {
98+
final Object raw = pemParser.readObject();
99+
if( raw instanceof PEMKeyPair ) {
100+
return new JcaPEMKeyConverter().getKeyPair((PEMKeyPair) raw).getPrivate();
101+
}
102+
if( raw instanceof PrivateKey ) {
103+
return (PrivateKey) raw;
104+
}
105+
if( raw instanceof PKCS8EncryptedPrivateKeyInfo ) {
106+
final InputDecryptorProvider c = new JceOpenSSLPKCS8DecryptorProviderBuilder().build(password);
107+
final PrivateKeyInfo privateKeyInfo = ((PKCS8EncryptedPrivateKeyInfo) raw).decryptPrivateKeyInfo(c);
108+
return new JcaPEMKeyConverter().getPrivateKey(privateKeyInfo);
109+
}
110+
if( raw instanceof PrivateKeyInfo ) {
111+
return new JcaPEMKeyConverter().getPrivateKey((PrivateKeyInfo) raw);
112+
}
113+
throw new IllegalArgumentException("Provided key data did not contain a valid PEM key.");
114+
}
115+
}
116+
}

cloudplatform/connectivity-apache-httpclient4/pom.xml

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -96,14 +96,6 @@
9696
</exclusion>
9797
</exclusions>
9898
</dependency>
99-
<dependency>
100-
<groupId>org.bouncycastle</groupId>
101-
<artifactId>bcprov-jdk18on</artifactId>
102-
</dependency>
103-
<dependency>
104-
<groupId>org.bouncycastle</groupId>
105-
<artifactId>bcpkix-jdk18on</artifactId>
106-
</dependency>
10799
<dependency>
108100
<groupId>org.apache.commons</groupId>
109101
<artifactId>commons-lang3</artifactId>

cloudplatform/connectivity-apache-httpclient4/src/main/java/com/sap/cloud/sdk/cloudplatform/connectivity/AbstractX509SslContextProvider.java

Lines changed: 5 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -4,29 +4,15 @@
44

55
package com.sap.cloud.sdk.cloudplatform.connectivity;
66

7-
import java.io.ByteArrayInputStream;
8-
import java.io.IOException;
97
import java.io.Reader;
108
import java.io.StringReader;
11-
import java.security.KeyStore;
12-
import java.security.PrivateKey;
13-
import java.security.cert.Certificate;
14-
import java.security.cert.CertificateException;
15-
import java.security.cert.CertificateFactory;
16-
import java.util.ArrayList;
17-
import java.util.List;
189

1910
import javax.annotation.Nonnull;
2011
import javax.net.ssl.SSLContext;
2112

2213
import org.apache.http.ssl.SSLContextBuilder;
23-
import org.bouncycastle.openssl.PEMKeyPair;
24-
import org.bouncycastle.openssl.PEMParser;
25-
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
26-
import org.bouncycastle.util.io.pem.PemObject;
2714

2815
import com.sap.cloud.sdk.cloudplatform.PlatformSslContextProvider;
29-
import com.sap.cloud.sdk.cloudplatform.exception.CloudPlatformException;
3016

3117
import io.vavr.control.Try;
3218

@@ -35,6 +21,9 @@
3521
*/
3622
abstract class AbstractX509SslContextProvider implements PlatformSslContextProvider
3723
{
24+
private static final char[] DEFAULT_PASSWORD = "changeit".toCharArray();
25+
private static final String DEFAULT_ALIAS = "instance-identity";
26+
3827
/**
3928
* Convenience for {@code tryGetContext(new StringReader(cert), new StringReader(key))}
4029
*
@@ -65,66 +54,9 @@ Try<SSLContext> tryGetContext( @Nonnull final String cert, @Nonnull final String
6554
Try<SSLContext> tryGetContext( @Nonnull final Reader certReader, @Nonnull final Reader keyReader )
6655
{
6756
final SSLContextBuilder sslContextBuilder = SSLContextBuilder.create();
68-
69-
final Certificate[] clientCertificates;
70-
final PrivateKey privateKey;
71-
72-
try {
73-
clientCertificates = loadCertificates(certReader);
74-
}
75-
catch( final Exception e ) {
76-
return Try.failure(new CloudPlatformException("Failed to load platform certificate", e));
77-
}
78-
79-
try {
80-
privateKey = loadPrivateKey(keyReader);
81-
}
82-
catch( final Exception e ) {
83-
return Try.failure(new CloudPlatformException("Failed to load platform key", e));
84-
}
85-
8657
return Try
87-
.of(() -> KeyStore.getInstance("JKS"))
88-
.andThenTry(k -> k.load(null))
89-
.andThenTry(
90-
k -> k.setKeyEntry("instance-identity", privateKey, "changeit".toCharArray(), clientCertificates))
91-
.mapTry(k -> sslContextBuilder.loadKeyMaterial(k, "changeit".toCharArray()))
58+
.of(() -> KeyStoreReader.createKeyStore(DEFAULT_ALIAS, DEFAULT_PASSWORD, certReader, keyReader))
59+
.mapTry(k -> sslContextBuilder.loadKeyMaterial(k, DEFAULT_PASSWORD))
9260
.mapTry(SSLContextBuilder::build);
9361
}
94-
95-
@Nonnull
96-
static Certificate[] loadCertificates( @Nonnull final Reader certReader )
97-
throws CertificateException,
98-
IOException
99-
{
100-
final List<Certificate> certs = new ArrayList<>();
101-
final CertificateFactory factory = CertificateFactory.getInstance("X509");
102-
103-
try( PEMParser pemParser = new PEMParser(certReader) ) {
104-
PemObject object;
105-
while( (object = pemParser.readPemObject()) != null ) {
106-
if( !object.getType().equals("CERTIFICATE") ) {
107-
continue;
108-
}
109-
certs.add(factory.generateCertificate(new ByteArrayInputStream(object.getContent())));
110-
}
111-
}
112-
if( certs.isEmpty() ) {
113-
throw new CloudPlatformException("Provided certificate data did not contain any valid X.509 certificates.");
114-
}
115-
return certs.toArray(new Certificate[0]);
116-
}
117-
118-
@Nonnull
119-
static PrivateKey loadPrivateKey( @Nonnull final Reader keyReader )
120-
throws IOException
121-
{
122-
try( PEMParser pemParser = new PEMParser(keyReader) ) {
123-
final PEMKeyPair keyPair = (PEMKeyPair) pemParser.readObject();
124-
if( keyPair == null ) {
125-
throw new CloudPlatformException("Provided key data did not contain a valid PEM key.");
126-
}
127-
return new JcaPEMKeyConverter().getKeyPair(keyPair).getPrivate();
128-
}
129-
}
13062
}

cloudplatform/connectivity-apache-httpclient4/src/test/java/com/sap/cloud/sdk/cloudplatform/connectivity/AbstractX509SslContextProviderTest.java

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ void testCertificateParsing()
3636
CertificateException
3737
{
3838
final FileReader cert = new FileReader(getTestFile("valid_cert.pem"));
39-
final Certificate[] certificates = AbstractX509SslContextProvider.loadCertificates(cert);
39+
final Certificate[] certificates = KeyStoreReader.loadCertificates(cert);
4040

4141
assertThat(certificates).isNotEmpty();
4242
assertThat(certificates[0].getType()).isEqualTo("X.509");
@@ -51,16 +51,16 @@ void testCertificateParsingFailure()
5151
throws FileNotFoundException
5252
{
5353
final FileReader cert = new FileReader(getTestFile("invalid_cert"));
54-
assertThatThrownBy(() -> AbstractX509SslContextProvider.loadCertificates(cert))
55-
.isInstanceOf(CloudPlatformException.class);
54+
assertThatThrownBy(() -> KeyStoreReader.loadCertificates(cert)).isInstanceOf(IllegalArgumentException.class);
5655
}
5756

5857
@Test
5958
void testKeyParsing()
60-
throws IOException
59+
throws Exception
6160
{
6261
final FileReader key = new FileReader(getTestFile("valid_key.pem"));
63-
final PrivateKey privateKey = AbstractX509SslContextProvider.loadPrivateKey(key);
62+
final char[] pw = "changeit".toCharArray();
63+
final PrivateKey privateKey = KeyStoreReader.loadKey(key, pw);
6464

6565
assertThat(privateKey.getAlgorithm()).containsIgnoringCase("RSA");
6666
}
@@ -70,8 +70,8 @@ void testKeyParsingFailure()
7070
throws FileNotFoundException
7171
{
7272
final FileReader key = new FileReader(getTestFile("invalid_key"));
73-
assertThatThrownBy(() -> AbstractX509SslContextProvider.loadPrivateKey(key))
74-
.isInstanceOf(CloudPlatformException.class);
73+
final char[] pw = "changeit".toCharArray();
74+
assertThatThrownBy(() -> KeyStoreReader.loadKey(key, pw)).isInstanceOf(IllegalArgumentException.class);
7575
}
7676

7777
@Test
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright (c) 2024 SAP SE or an SAP affiliate company. All rights reserved.
3+
*/
4+
5+
package com.sap.cloud.sdk.cloudplatform.connectivity;
6+
7+
import static org.assertj.core.api.Assertions.assertThat;
8+
9+
import java.io.FileReader;
10+
import java.security.KeyStore;
11+
import java.security.cert.X509Certificate;
12+
import java.security.interfaces.RSAPrivateCrtKey;
13+
14+
import org.junit.jupiter.api.Test;
15+
16+
class KeyStoreReaderTest
17+
{
18+
private static final String RES =
19+
"src/test/resources/" + ClientCertificateAuthenticationLocalTest.class.getSimpleName();
20+
private static final String CRT_PATH = RES + "/client-cert.crt";
21+
private static final String KEY_PATH = RES + "/client-cert.key";
22+
23+
@Test
24+
void testPem()
25+
throws Exception
26+
{
27+
final String ALIAS = "1";
28+
final FileReader certs = new FileReader(CRT_PATH), key = new FileReader(KEY_PATH);
29+
final KeyStore createdKeystore = KeyStoreReader.createKeyStore(ALIAS, new char[0], certs, key);
30+
31+
assertThat(createdKeystore.getType()).isEqualTo("JKS");
32+
assertThat(createdKeystore.getProvider()).isNotNull();
33+
34+
assertThat(createdKeystore.getCertificateChain(ALIAS)).hasSize(1);
35+
assertThat(createdKeystore.getCertificate(ALIAS))
36+
.isInstanceOf(X509Certificate.class)
37+
.extracting(c -> ((X509Certificate) c).getSubjectX500Principal())
38+
.hasToString("CN=localhost, EMAILADDRESS=cloudsdk@sap.com, O=Potsdam, ST=Brandenburg, C=DE");
39+
40+
assertThat(createdKeystore.getKey(ALIAS, new char[0])).isInstanceOf(RSAPrivateCrtKey.class); // no password
41+
}
42+
}

cloudplatform/connectivity-apache-httpclient5/pom.xml

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -84,14 +84,6 @@
8484
<groupId>org.apache.httpcomponents.core5</groupId>
8585
<artifactId>httpcore5</artifactId>
8686
</dependency>
87-
<dependency>
88-
<groupId>org.bouncycastle</groupId>
89-
<artifactId>bcprov-jdk18on</artifactId>
90-
</dependency>
91-
<dependency>
92-
<groupId>org.bouncycastle</groupId>
93-
<artifactId>bcpkix-jdk18on</artifactId>
94-
</dependency>
9587
<dependency>
9688
<groupId>org.apache.commons</groupId>
9789
<artifactId>commons-lang3</artifactId>

0 commit comments

Comments
 (0)