diff --git a/cds-feature-auditlog-ng/src/main/java/com/sap/cds/feature/auditlog/ng/CertificateHttpClientConfig.java b/cds-feature-auditlog-ng/src/main/java/com/sap/cds/feature/auditlog/ng/CertificateHttpClientConfig.java index c45e656..d7437cd 100644 --- a/cds-feature-auditlog-ng/src/main/java/com/sap/cds/feature/auditlog/ng/CertificateHttpClientConfig.java +++ b/cds-feature-auditlog-ng/src/main/java/com/sap/cds/feature/auditlog/ng/CertificateHttpClientConfig.java @@ -1,9 +1,7 @@ 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; @@ -11,7 +9,6 @@ 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; @@ -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; @@ -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 { diff --git a/cds-feature-auditlog-ng/src/test/java/com/sap/cds/feature/auditlog/ng/CertificateHttpClientConfigTest.java b/cds-feature-auditlog-ng/src/test/java/com/sap/cds/feature/auditlog/ng/CertificateHttpClientConfigTest.java new file mode 100644 index 0000000..7c4d741 --- /dev/null +++ b/cds-feature-auditlog-ng/src/test/java/com/sap/cds/feature/auditlog/ng/CertificateHttpClientConfigTest.java @@ -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(); + } +}