diff --git a/core/src/main/java/com/predic8/membrane/core/security/KeyStoreUtil.java b/core/src/main/java/com/predic8/membrane/core/security/KeyStoreUtil.java new file mode 100644 index 0000000000..647f592b38 --- /dev/null +++ b/core/src/main/java/com/predic8/membrane/core/security/KeyStoreUtil.java @@ -0,0 +1,162 @@ +/* Copyright 2024 predic8 GmbH, www.predic8.com + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. */ +package com.predic8.membrane.core.security; + +import com.predic8.membrane.core.config.security.Store; +import com.predic8.membrane.core.resolver.ResolverMap; +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.security.*; +import java.security.cert.Certificate; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.util.Enumeration; +import java.util.Optional; + +public class KeyStoreUtil { + /** + * Filters a KeyStore by a specific alias, creating a new KeyStore + * containing only the key and certificate chain associated with + * the provided alias. + * + * @param ks the original KeyStore to filter + * @param keyPass the password for accessing the key + * @param keyAlias the alias of the key to filter + * @return a new KeyStore containing only the key and certificate + * chain associated with the specified alias + * @throws KeyStoreException if the KeyStore cannot be initialized + * @throws IOException if there is an I/O error during the operation + * @throws NoSuchAlgorithmException if the algorithm for recovering + * the key cannot be found + * @throws CertificateException if any of the certificates in the + * chain are invalid + * @throws UnrecoverableKeyException if the key cannot be recovered + * using the given password + */ + public static KeyStore filterKeyStoreByAlias(KeyStore ks, char[] keyPass, String keyAlias) throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException, UnrecoverableKeyException { + KeyStore filteredKeyStore = KeyStore.getInstance(ks.getType()); + filteredKeyStore.load(null, keyPass); + + Key key = ks.getKey(keyAlias, keyPass); + Certificate[] certificateChain = ks.getCertificateChain(keyAlias); + filteredKeyStore.setKeyEntry(keyAlias, key, keyPass, certificateChain); + return filteredKeyStore; + } + + /** + * Generates an SHA-256 digest of the certificate associated with the given alias in the KeyStore. + * + * @param ks The KeyStore containing the certificate. + * @param alias The alias of the certificate in the KeyStore. + * @return A String representation of the SHA-256 digest, with bytes separated by colons. + * @throws CertificateEncodingException If there's an error encoding the certificate. + * @throws KeyStoreException If there's an error accessing the KeyStore. + * @throws NoSuchAlgorithmException If the SHA-256 algorithm is not available. + */ + public static @org.jetbrains.annotations.NotNull String getDigest(KeyStore ks, String alias) throws CertificateEncodingException, KeyStoreException, NoSuchAlgorithmException { + byte[] pkeEnc = ks.getCertificate(alias).getEncoded(); + MessageDigest md = MessageDigest.getInstance("SHA-256"); + md.update(pkeEnc); + byte[] mdbytes = md.digest(); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < mdbytes.length; i++) { + if (i > 0) + sb.append(':'); + sb.append(Integer.toString((mdbytes[i] & 0xff) + 0x100, 16).substring(1)); + } + return sb.toString(); + } + + /** + * Retrieves and loads a KeyStore based on the provided parameters. + * + * @param store The Store object containing information about the keystore. + * @param resourceResolver The ResolverMap used to resolve the keystore location. + * @param baseLocation The base location to be combined with the store's location. + * @param type The type of the KeyStore (e.g., "JKS", "PKCS12"). + * @param password The password used to check the integrity of the keystore. + * @return A loaded KeyStore instance. + * @throws KeyStoreException If there's an error accessing the keystore. + * @throws NoSuchProviderException If the specified provider is not found. + * @throws IOException If there's an I/O error while loading the keystore. + * @throws NoSuchAlgorithmException If the algorithm used to check the integrity of the keystore cannot be found. + * @throws CertificateException If any of the certificates in the keystore could not be loaded. + */ + public static @NotNull KeyStore getAndLoadKeyStore(Store store, ResolverMap resourceResolver, String baseLocation, String type, char[] password) throws KeyStoreException, NoSuchProviderException, IOException, NoSuchAlgorithmException, CertificateException { + KeyStore ks; + if (store.getProvider() != null) + ks = KeyStore.getInstance(type, store.getProvider()); + else + ks = KeyStore.getInstance(type); + ks.load(resourceResolver.resolve(ResolverMap.combine(baseLocation, store.getLocation())), password); + return ks; + } + + /** + * Retrieves the first certificate alias from the provided key store. + * + * @param ks the key store from which to retrieve the certificate alias + * @return the first certificate alias as a String + * @throws KeyStoreException if there is an error accessing the key store + * @throws RuntimeException if no certificate is available in the key store + */ + public static String firstAliasOrThrow(KeyStore ks) throws KeyStoreException { + String keyAlias; + Optional alias = getFirstCertAlias(ks); + if (alias.isPresent()) { + keyAlias = alias.get(); + } else { + throw new RuntimeException("No certificate available in key store."); + } + return keyAlias; + } + + /** + * Returns alias if it was found within the specified key store, else throws. + * + * @param ks the KeyStore which will be queried + * @param alias the alias of the certificate to be queried + * @return the alias of the certificate if it exists + * @throws KeyStoreException if the key store has not been initialized + * @throws RuntimeException if the certificate with the specified alias is not present in the key store + */ + public static String aliasOrThrow(KeyStore ks, String alias) throws KeyStoreException { + String keyAlias; + if (!ks.isKeyEntry(alias)) { + throw new RuntimeException("Certificate of alias " + alias + " not present in key store."); + } else { + keyAlias = alias; + } + return keyAlias; + } + + /** + * Retrieves the first alias of a key entry from the specified KeyStore. + * + * @param keystore the key store to search for key entries + * @return Optional containing the first alias of a key entry, or an empty Optional if none is found + * @throws KeyStoreException is thrown if key store has not been initialized + */ + public static Optional getFirstCertAlias(KeyStore keystore) throws KeyStoreException { + Enumeration aliases = keystore.aliases(); + while (aliases.hasMoreElements()) { + String alias = aliases.nextElement(); + if (keystore.isKeyEntry(alias)) { + return Optional.of(alias); + } + } + return Optional.empty(); + } +} \ No newline at end of file diff --git a/core/src/main/java/com/predic8/membrane/core/transport/ssl/StaticSSLContext.java b/core/src/main/java/com/predic8/membrane/core/transport/ssl/StaticSSLContext.java index d10df898b4..2f1a737d05 100644 --- a/core/src/main/java/com/predic8/membrane/core/transport/ssl/StaticSSLContext.java +++ b/core/src/main/java/com/predic8/membrane/core/transport/ssl/StaticSSLContext.java @@ -14,12 +14,12 @@ package com.predic8.membrane.core.transport.ssl; -import com.google.common.base.Objects; import com.predic8.membrane.core.config.security.SSLParser; import com.predic8.membrane.core.config.security.Store; import com.predic8.membrane.core.resolver.ResolverMap; import com.predic8.membrane.core.transport.TrustManagerWrapper; import com.predic8.membrane.core.transport.http2.Http2TlsSupport; +import org.apache.commons.lang3.NotImplementedException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -27,26 +27,24 @@ import javax.crypto.Cipher; import javax.net.ssl.*; import javax.validation.constraints.NotNull; -import java.io.FileNotFoundException; import java.io.IOException; import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.ServerSocket; import java.net.Socket; import java.security.*; -import java.security.cert.*; import java.security.cert.Certificate; -import java.security.interfaces.RSAPrivateCrtKey; -import java.security.interfaces.RSAPublicKey; +import java.security.cert.*; import java.util.*; +import static com.predic8.membrane.core.security.KeyStoreUtil.*; + public class StaticSSLContext extends SSLContext { private static final String DEFAULT_CERTIFICATE_SHA256 = "c7:e3:fd:97:2f:d3:b9:4f:38:87:9c:45:32:70:b3:d8:c1:9f:d1:64:39:fc:48:5f:f4:a1:6a:95:b5:ca:08:f7"; private static final Logger log = LoggerFactory.getLogger(StaticSSLContext.class.getName()); - private static boolean default_certificate_warned = false; + private static boolean defaultCertificateWarned = false; private static boolean limitedStrength; static { @@ -69,46 +67,38 @@ public class StaticSSLContext extends SSLContext { private final SSLParser sslParser; private List dnsNames; - private javax.net.ssl.SSLContext sslc; - private long validFrom, validUntil; + private long validFrom; + private long validUntil; public StaticSSLContext(SSLParser sslParser, ResolverMap resourceResolver, String baseLocation) { this.sslParser = sslParser; try { - String algorihm = KeyManagerFactory.getDefaultAlgorithm(); - if (sslParser.getAlgorithm() != null) - algorihm = sslParser.getAlgorithm(); + String algorihm = getAlgorithm(sslParser); KeyManagerFactory kmf = null; String keyStoreType = "PKCS12"; if (sslParser.getKeyStore() != null) { - if (sslParser.getKeyStore().getKeyAlias() != null) - throw new InvalidParameterException("keyAlias is not yet supported."); - char[] keyPass = "changeit".toCharArray(); - if (sslParser.getKeyStore().getKeyPassword() != null) - keyPass = sslParser.getKeyStore().getKeyPassword().toCharArray(); + char[] keyPass = getKeyPass(sslParser); if (sslParser.getKeyStore().getType() != null) keyStoreType = sslParser.getKeyStore().getType(); KeyStore ks = openKeyStore(sslParser.getKeyStore(), "PKCS12", keyPass, resourceResolver, baseLocation); + + String paramAlias = sslParser.getKeyStore().getKeyAlias(); + String keyAlias = (paramAlias != null) ? aliasOrThrow(ks, paramAlias) : firstAliasOrThrow(ks); + + KeyStore filteredKeyStore = filterKeyStoreByAlias(ks, keyPass, keyAlias); + kmf = KeyManagerFactory.getInstance(algorihm); - kmf.init(ks, keyPass); - - Enumeration aliases = ks.aliases(); - while (aliases.hasMoreElements()) { - String alias = aliases.nextElement(); - if (ks.isKeyEntry(alias)) { - // first key is used by the KeyManagerFactory - dnsNames = getDNSNames(ks.getCertificate(alias)); - List certs = Arrays.asList(ks.getCertificateChain(alias)); - validUntil = getMinimumValidity(certs); - validFrom = getValidFrom(certs); - break; - } - } + kmf.init(filteredKeyStore, keyPass); + + dnsNames = extractDnsNames(ks.getCertificate(keyAlias)); + List certs = Arrays.asList(ks.getCertificateChain(keyAlias)); + validUntil = getMinimumValidity(certs); + validFrom = getValidFrom(certs); } if (sslParser.getKey() != null) { if (kmf != null) @@ -121,22 +111,18 @@ public StaticSSLContext(SSLParser sslParser, ResolverMap resourceResolver, Strin for (com.predic8.membrane.core.config.security.Certificate cert : sslParser.getKey().getCertificates()) certs.add(PEMSupport.getInstance().parseCertificate(cert.get(resourceResolver, baseLocation))); - if (certs.size() == 0) + if (certs.isEmpty()) throw new RuntimeException("At least one //ssl/key/certificate is required."); - dnsNames = getDNSNames(certs.get(0)); + dnsNames = extractDnsNames(certs.get(0)); checkChainValidity(certs); - Object key = PEMSupport.getInstance().parseKey(sslParser.getKey().getPrivate().get(resourceResolver, baseLocation)); - Key k = key instanceof Key ? (Key) key : ((KeyPair)key).getPrivate(); + Key k = getKey(sslParser, resourceResolver, baseLocation); checkKeyMatchesCert(k, certs); ks.setKeyEntry("inlinePemKeyAndCertificate", k, "".toCharArray(), certs.toArray(new Certificate[certs.size()])); kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); - String keyPassword = ""; - if (sslParser.getKey().getPassword() != null) - keyPassword = sslParser.getKey().getPassword(); - kmf.init(ks, keyPassword.toCharArray()); + kmf.init(ks, getKeyPassword(sslParser)); validUntil = getMinimumValidity(certs); validFrom = getValidFrom(certs); } @@ -201,36 +187,71 @@ public StaticSSLContext(SSLParser sslParser, ResolverMap resourceResolver, Strin init(sslParser, sslc); } + private static Key getKey(SSLParser sslParser, ResolverMap resourceResolver, String baseLocation) throws IOException { + Object key = PEMSupport.getInstance().parseKey(sslParser.getKey().getPrivate().get(resourceResolver, baseLocation)); + Key k = key instanceof Key ? (Key) key : ((KeyPair)key).getPrivate(); + return k; + } + + private static char[] getKeyPassword(SSLParser sslParser) { + if (sslParser.getKey().getPassword() != null) + return sslParser.getKey().getPassword().toCharArray(); + return "".toCharArray(); + } + + private static char @org.jetbrains.annotations.NotNull [] getKeyPass(SSLParser sslParser) { + char[] keyPass = "changeit".toCharArray(); + if (sslParser.getKeyStore().getKeyPassword() != null) + keyPass = sslParser.getKeyStore().getKeyPassword().toCharArray(); + return keyPass; + } + + private static String getAlgorithm(SSLParser sslParser) { + String algorihm = KeyManagerFactory.getDefaultAlgorithm(); + if (sslParser.getAlgorithm() != null) + algorihm = sslParser.getAlgorithm(); + return algorihm; + } + public StaticSSLContext(SSLParser sslParser, javax.net.ssl.SSLContext sslc) { this.sslParser = sslParser; this.sslc = sslc; init(sslParser, sslc); } - private List getDNSNames(Certificate certificate) throws CertificateParsingException { - ArrayList dnsNames = new ArrayList<>(); - if (certificate instanceof X509Certificate) { - X509Certificate x = (X509Certificate) certificate; - - Collection> subjectAlternativeNames = x.getSubjectAlternativeNames(); + /** + * Retrieves the DNS names from the specified certificate. + * + * @param certificate the certificate from which to extract DNS names + * @return a list of DNS names found in the certificate + * @throws CertificateParsingException if there is an error parsing the certificate + */ + private List extractDnsNames(Certificate certificate) throws CertificateParsingException { + ArrayList names = new ArrayList<>(); + if (certificate instanceof X509Certificate cert) { + Collection> subjectAlternativeNames = cert.getSubjectAlternativeNames(); if (subjectAlternativeNames != null) for (List l : subjectAlternativeNames) { - if (l.get(0) instanceof Integer && ((Integer)l.get(0) == 2)) - dnsNames.add(l.get(1).toString()); + if (l.get(0) instanceof Integer && ((Integer) l.get(0) == 2)) + names.add(l.get(1).toString()); } } - return dnsNames; + return names; } @Override public boolean equals(Object obj) { - if (!(obj instanceof StaticSSLContext)) + if (!(obj instanceof StaticSSLContext other)) return false; - StaticSSLContext other = (StaticSSLContext)obj; - return Objects.equal(sslParser, other.sslParser); + return sslParser.hashCode() == other.sslParser.hashCode(); } - private KeyStore openKeyStore(Store store, String defaultType, char[] keyPass, ResolverMap resourceResolver, String baseLocation) throws NoSuchAlgorithmException, CertificateException, FileNotFoundException, IOException, KeyStoreException, NoSuchProviderException { + @Override + public int hashCode() { + return java.util.Objects.hash(sslParser, dnsNames, sslc, validFrom, validUntil); + } + + public static KeyStore openKeyStore(Store store, String defaultType, char[] keyPass, ResolverMap resourceResolver, String baseLocation) throws NoSuchAlgorithmException, CertificateException, IOException, KeyStoreException, NoSuchProviderException { String type = store.getType(); if (type == null) type = defaultType; @@ -239,27 +260,12 @@ private KeyStore openKeyStore(Store store, String defaultType, char[] keyPass, R password = store.getPassword().toCharArray(); if (password == null) throw new InvalidParameterException("Password for key store is not set."); - KeyStore ks; - if (store.getProvider() != null) - ks = KeyStore.getInstance(type, store.getProvider()); - else - ks = KeyStore.getInstance(type); - ks.load(resourceResolver.resolve(ResolverMap.combine(baseLocation, store.getLocation())), password); - if (!default_certificate_warned && ks.getCertificate("membrane") != null) { - byte[] pkeEnc = ks.getCertificate("membrane").getEncoded(); - MessageDigest md = MessageDigest.getInstance("SHA-256"); - md.update(pkeEnc); - byte[] mdbytes = md.digest(); - StringBuffer sb = new StringBuffer(); - for (int i = 0; i < mdbytes.length; i++) { - if (i > 0) - sb.append(':'); - sb.append(Integer.toString((mdbytes[i] & 0xff) + 0x100, 16).substring(1)); - } - if (sb.toString().equals(DEFAULT_CERTIFICATE_SHA256)) { + KeyStore ks = getAndLoadKeyStore(store, resourceResolver, baseLocation, type, password); + if (!defaultCertificateWarned && ks.getCertificate("membrane") != null) { + if (getDigest(ks, "membrane").equals(DEFAULT_CERTIFICATE_SHA256)) { log.warn("Using Membrane with the default certificate. This is highly discouraged! " + "Please run the generate-ssl-keys script in the conf directory."); - default_certificate_warned = true; + defaultCertificateWarned = true; } } return ks; diff --git a/core/src/test/java/com/predic8/membrane/core/security/KeyStoreUtilTest.java b/core/src/test/java/com/predic8/membrane/core/security/KeyStoreUtilTest.java new file mode 100644 index 0000000000..e0347f8ced --- /dev/null +++ b/core/src/test/java/com/predic8/membrane/core/security/KeyStoreUtilTest.java @@ -0,0 +1,128 @@ +/* Copyright 2024 predic8 GmbH, www.predic8.com + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. */ +package com.predic8.membrane.core.security; + +import com.predic8.membrane.core.HttpRouter; +import com.predic8.membrane.core.Router; +import com.predic8.membrane.core.config.security.KeyStore; +import com.predic8.membrane.core.config.security.SSLParser; +import com.predic8.membrane.core.resolver.ResolverMap; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.util.Optional; + +import static com.predic8.membrane.core.security.KeyStoreUtil.filterKeyStoreByAlias; +import static com.predic8.membrane.core.security.KeyStoreUtil.getAndLoadKeyStore; +import static org.junit.jupiter.api.Assertions.*; + +class KeyStoreUtilTest { + + private static Router router; + private static java.security.KeyStore keyStore; + private static final String ALIAS = "key1"; + private static final String KEYSTORE_PASSWORD = "secret"; + private static final String EXPECTED_DIGEST = "96:ec:da:3d:2a:a3:a9:7e:3b:40:56:46:86:d7:1c:d2:a9:e1:69:3f:99:6b:8d:57:4a:4c:bb:7a:24:55:18:ed"; + + @BeforeAll + static void setUp() throws Exception { + router = new HttpRouter(); + SSLParser sslParser = new SSLParser(); + sslParser.setKeyStore(new KeyStore()); + sslParser.getKeyStore().setLocation("classpath:/alias-keystore.p12"); + sslParser.getKeyStore().setKeyPassword(KEYSTORE_PASSWORD); + keyStore = getAndLoadKeyStore(sslParser.getKeyStore(), router.getResolverMap(), router.getBaseLocation(), "PKCS12", KEYSTORE_PASSWORD.toCharArray()); + } + + @AfterAll + static void tearDown() { + router.stop(); + } + + @Test + void testFilterKeyStoreByAlias() throws KeyStoreException, NoSuchProviderException, IOException, NoSuchAlgorithmException, CertificateException, UnrecoverableKeyException { + KeyStore store = new KeyStore(); + store.setLocation("classpath:/alias-keystore.p12"); + store.setKeyPassword(KEYSTORE_PASSWORD); + java.security.KeyStore loadedKeyStore = getAndLoadKeyStore(store, router.getResolverMap(), router.getBaseLocation(), "PKCS12", KEYSTORE_PASSWORD.toCharArray()); + assertNotNull(loadedKeyStore); + assertEquals(2, loadedKeyStore.size()); + java.security.KeyStore filteredKeyStore = filterKeyStoreByAlias(loadedKeyStore, "secret".toCharArray(), "key1"); + assertEquals(1, filteredKeyStore.size()); + } + + @Test + void testGetDigest() throws CertificateEncodingException, KeyStoreException, NoSuchAlgorithmException { + String digest = KeyStoreUtil.getDigest(keyStore, ALIAS); + assertEquals(EXPECTED_DIGEST, digest); + } + + @Test + void testGetAndLoadKeyStore() throws KeyStoreException, NoSuchProviderException, IOException, NoSuchAlgorithmException, CertificateException { + KeyStore store = new KeyStore(); + store.setLocation("classpath:/alias-keystore.p12"); + store.setKeyPassword(KEYSTORE_PASSWORD); + java.security.KeyStore loadedKeyStore = getAndLoadKeyStore(store, router.getResolverMap(), router.getBaseLocation(), "PKCS12", KEYSTORE_PASSWORD.toCharArray()); + assertNotNull(loadedKeyStore); + assertTrue(loadedKeyStore.size() > 0); + assertTrue(loadedKeyStore.containsAlias(ALIAS)); + } + + @Test + void testFirstAliasOrThrowFound() throws KeyStoreException { + String alias = KeyStoreUtil.firstAliasOrThrow(keyStore); + assertEquals(ALIAS, alias); + } + + @Test + void testFirstAliasOrThrowNotFound() throws KeyStoreException, IOException, CertificateException, NoSuchAlgorithmException { + java.security.KeyStore emptyKeyStore = java.security.KeyStore.getInstance("PKCS12"); + emptyKeyStore.load(null, null); + assertThrows(RuntimeException.class, () -> KeyStoreUtil.firstAliasOrThrow(emptyKeyStore)); + } + + @Test + void testAliasOrThrowFound() throws KeyStoreException { + String alias = KeyStoreUtil.aliasOrThrow(keyStore, ALIAS); + assertEquals(ALIAS, alias); + } + + @Test + void testAliasOrThrowNotFound() { + assertThrows(RuntimeException.class, () -> KeyStoreUtil.aliasOrThrow(keyStore, "nonexistent")); + } + + @Test + void testGetFirstCertAliasFound() throws KeyStoreException { + Optional alias = KeyStoreUtil.getFirstCertAlias(keyStore); + assertTrue(alias.isPresent()); + assertEquals(ALIAS, alias.get()); + } + + @Test + void testGetFirstCertAliasNotFound() throws KeyStoreException, IOException, CertificateException, NoSuchAlgorithmException { + java.security.KeyStore emptyKeyStore = java.security.KeyStore.getInstance("PKCS12"); + emptyKeyStore.load(null, null); + Optional alias = KeyStoreUtil.getFirstCertAlias(emptyKeyStore); + assertFalse(alias.isPresent()); + } +} \ No newline at end of file diff --git a/core/src/test/java/com/predic8/membrane/core/transport/ssl/SSLContextTest.java b/core/src/test/java/com/predic8/membrane/core/transport/ssl/SSLContextTest.java index 4e03c8dd50..67f955a561 100644 --- a/core/src/test/java/com/predic8/membrane/core/transport/ssl/SSLContextTest.java +++ b/core/src/test/java/com/predic8/membrane/core/transport/ssl/SSLContextTest.java @@ -11,22 +11,29 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ - package com.predic8.membrane.core.transport.ssl; -import com.predic8.membrane.core.*; -import com.predic8.membrane.core.config.security.*; -import org.junit.jupiter.api.*; +import com.predic8.membrane.core.HttpRouter; +import com.predic8.membrane.core.Router; +import com.predic8.membrane.core.config.security.KeyStore; +import com.predic8.membrane.core.config.security.SSLParser; +import com.predic8.membrane.core.config.security.TrustStore; +import org.apache.commons.lang3.NotImplementedException; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.function.Executable; import org.junit.platform.commons.util.UnrecoverableExceptions; -import javax.net.ssl.*; -import java.io.*; -import java.net.*; +import javax.net.ssl.SSLHandshakeException; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketException; -import static java.lang.String.format; -import static org.junit.jupiter.api.AssertionFailureBuilder.assertionFailure; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; public class SSLContextTest { @@ -49,10 +56,15 @@ public SSLContextBuilder() { sslParser.setEndpointIdentificationAlgorithm(""); } - public SSLContext build() { + public StaticSSLContext build() { return new StaticSSLContext(sslParser, router.getResolverMap(), router.getBaseLocation()); } + private SSLContextBuilder byKeyAlias(String alias) { + sslParser.getKeyStore().setKeyAlias(alias); + return this; + } + private SSLContextBuilder needClientAuth() { sslParser.setClientAuth("need"); return this; @@ -86,6 +98,47 @@ public void simpleConfig() { }); } + @Test + public void selectFirstKeyAndPass() throws Exception { + SSLContext server = cb().withKeyStore("classpath:/alias-keystore.p12").byKeyAlias("key1").build(); + SSLContext client = cb().withTrustStore("classpath:/alias-truststore.p12").build(); + testCombination(server, client); + } + + @Test + public void selectFirstKeyTrustFail() { + assertThrows2(SocketException.class, SSLHandshakeException.class, () -> { + SSLContext server = cb().withKeyStore("classpath:/alias-keystore.p12").byKeyAlias("key2").build(); + SSLContext client = cb().withTrustStore("classpath:/alias-truststore.p12").build(); + testCombination(server, client); + }); + } + + @Test + public void selectSecondKeyAndPass() throws Exception { + SSLContext server = cb().withKeyStore("classpath:/alias-keystore.p12").byKeyAlias("key2").build(); + SSLContext client = cb().withTrustStore("classpath:/alias-truststore2.p12").build(); + testCombination(server, client); + } + + @Test + public void selectSecondKeyTrustFail() { + assertThrows2(SocketException.class, SSLHandshakeException.class, () -> { + SSLContext server = cb().withKeyStore("classpath:/alias-keystore.p12").byKeyAlias("key1").build(); + SSLContext client = cb().withTrustStore("classpath:/alias-truststore2.p12").build(); + testCombination(server, client); + }); + } + + @Test + public void invalidAlias() { + assertThrows(RuntimeException.class, () -> { + SSLContext server = cb().withKeyStore("classpath:/alias-keystore.p12").byKeyAlias("key999").build(); + SSLContext client = cb().withTrustStore("classpath:/alias-truststore.p12").build(); + testCombination(server, client); + }); + } + @Test public void serverKeyOnlyWithoutClientTrust() { assertThrows(Exception.class, () -> { @@ -111,22 +164,6 @@ public void serverKeyOnlyWithInvalidClientTrust() { }); } - public static void assertThrows2(Class expectedType1, Class expectedType2, Executable executable) { - try { - executable.execute(); - } catch (Throwable actualException) { - if (expectedType1.isInstance(actualException)) { - return; - } else if (expectedType2.isInstance(actualException)) { - return; - } else { - UnrecoverableExceptions.rethrowIfUnrecoverable(actualException); - throw new RuntimeException("Unexpected exception type thrown"); - } - } - throw new RuntimeException("Expected exception to be thrown, but nothing was thrown."); - } - @Test public void serverAndClientCertificates() throws Exception { SSLContext server = cb().withKeyStore("classpath:/ssl-rsa.keystore").withTrustStore("classpath:/ssl-rsa-pub2.keystore").needClientAuth().build(); @@ -143,6 +180,21 @@ public void serverAndClientCertificatesWithoutServerTrust() { }); } + public static void assertThrows2(Class expectedType1, Class expectedType2, Executable executable) { + try { + executable.execute(); + } catch (Throwable actualException) { + if (expectedType1.isInstance(actualException)) { + return; + } else if (expectedType2.isInstance(actualException)) { + return; + } else { + UnrecoverableExceptions.rethrowIfUnrecoverable(actualException); + throw new RuntimeException("Unexpected exception type thrown"); + } + } + throw new RuntimeException("Expected exception to be thrown, but nothing was thrown."); + } private void testCombination(SSLContext server, final SSLContext client) throws Exception { ServerSocket ss = server.createServerSocket(3020, 50, null); @@ -174,5 +226,4 @@ private void testCombination(SSLContext server, final SSLContext client) throws if (ex[0] != null) throw ex[0]; } - } diff --git a/core/src/test/resources/alias-keystore.p12 b/core/src/test/resources/alias-keystore.p12 new file mode 100644 index 0000000000..2cdadc80a7 Binary files /dev/null and b/core/src/test/resources/alias-keystore.p12 differ diff --git a/core/src/test/resources/alias-truststore.p12 b/core/src/test/resources/alias-truststore.p12 new file mode 100644 index 0000000000..67235c1b06 Binary files /dev/null and b/core/src/test/resources/alias-truststore.p12 differ diff --git a/core/src/test/resources/alias-truststore2.p12 b/core/src/test/resources/alias-truststore2.p12 new file mode 100644 index 0000000000..e02d9327e5 Binary files /dev/null and b/core/src/test/resources/alias-truststore2.p12 differ