Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
package com.sap.cds.feature.auditlog.ng;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.StringReader;
import java.security.KeyFactory;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.SecureRandom;
import java.security.Security;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
Expand All @@ -26,6 +23,7 @@
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8DecryptorProviderBuilder;
import org.bouncycastle.operator.InputDecryptorProvider;
import org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo;
Expand Down Expand Up @@ -233,15 +231,13 @@ private static PrivateKey parsePrivateKey(String keyPem, char[] passphrase) thro
try (PEMParser pemParser = new PEMParser(new StringReader(keyPem))) {
Object object = pemParser.readObject();
if (object instanceof PrivateKeyInfo keyInfo) {
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyInfo.getEncoded());
return KeyFactory.getInstance("RSA").generatePrivate(keySpec);
return new JcaPEMKeyConverter().setProvider("BC").getPrivateKey(keyInfo);
} else if (object instanceof PKCS8EncryptedPrivateKeyInfo encInfo) {
try {
InputDecryptorProvider decryptorProvider = new JceOpenSSLPKCS8DecryptorProviderBuilder().build(effectivePassphrase);
PrivateKeyInfo keyInfo = encInfo.decryptPrivateKeyInfo(decryptorProvider);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyInfo.getEncoded());
return KeyFactory.getInstance("RSA").generatePrivate(keySpec);
} catch (IOException e) {
return new JcaPEMKeyConverter().setProvider("BC").getPrivateKey(keyInfo);
} catch (Exception e) {
throw new RuntimeException("Failed to decrypt private key. Check that the passphrase is correct and the key is compatible. Original error: " + e.getMessage(), e);
}
} else {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package com.sap.cds.feature.auditlog.ng;

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertNotNull;

import java.io.StringWriter;
import java.math.BigInteger;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Date;

import javax.security.auth.x500.X500Principal;

import org.apache.http.impl.client.CloseableHttpClient;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
import org.bouncycastle.openssl.jcajce.JcaPKCS8Generator;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.junit.jupiter.api.Test;

/**
* Verifies that {@link CertificateHttpClientConfig} can build an HTTP client from
* certificate/key material regardless of the key algorithm. Historically the private
* key was parsed with a hardcoded RSA {@code KeyFactory}, which rejected EC keys as
* issued by non-BTP certificate providers (e.g. the Zero Trust Identity Service).
*/
class CertificateHttpClientConfigTest {

@Test
void buildsHttpClientFromEcCertificateAndKey() throws Exception {
KeyPairAndPem material = selfSigned("EC", "SHA256withECDSA");

CloseableHttpClient client = CertificateHttpClientConfig.builder()
.certPem(material.certPem)
.keyPem(material.keyPem)
.build()
.getHttpClient();

assertNotNull(client, "EC-keyed binding should produce an HTTP client");
}

@Test
void buildsHttpClientFromRsaCertificateAndKey() throws Exception {
KeyPairAndPem material = selfSigned("RSA", "SHA256withRSA");

assertDoesNotThrow(() -> CertificateHttpClientConfig.builder()
.certPem(material.certPem)
.keyPem(material.keyPem)
.build());
}

private record KeyPairAndPem(String certPem, String keyPem) {
}

private static KeyPairAndPem selfSigned(String keyAlgorithm, String signatureAlgorithm) throws Exception {
KeyPairGenerator kpg = KeyPairGenerator.getInstance(keyAlgorithm);
if ("EC".equals(keyAlgorithm)) {
kpg.initialize(256);
} else {
kpg.initialize(2048);
}
KeyPair keyPair = kpg.generateKeyPair();

X500Principal subject = new X500Principal("CN=test");
Instant now = Instant.now();
JcaX509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(
subject,
BigInteger.valueOf(System.currentTimeMillis()),
Date.from(now.minus(1, ChronoUnit.HOURS)),
Date.from(now.plus(1, ChronoUnit.DAYS)),
subject,
keyPair.getPublic());
ContentSigner signer = new JcaContentSignerBuilder(signatureAlgorithm).build(keyPair.getPrivate());
X509Certificate cert = new JcaX509CertificateConverter().getCertificate(certBuilder.build(signer));

return new KeyPairAndPem(toPem(cert), keyToPkcs8Pem(keyPair.getPrivate()));
}

private static String toPem(Object obj) throws Exception {
StringWriter sw = new StringWriter();
try (JcaPEMWriter writer = new JcaPEMWriter(sw)) {
writer.writeObject(obj);
}
return sw.toString();
}

// Writes the key in PKCS#8 form ("BEGIN PRIVATE KEY"), matching what the ALS
// binding provides and what parsePrivateKey expects.
private static String keyToPkcs8Pem(PrivateKey key) throws Exception {
StringWriter sw = new StringWriter();
try (JcaPEMWriter writer = new JcaPEMWriter(sw)) {
writer.writeObject(new JcaPKCS8Generator(key, null));
}
return sw.toString();
}
}