All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.dtreskunov.easyssl.spring.EasySslBeans Maven / Gradle / Ivy

package com.github.dtreskunov.easyssl.spring;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.KeyException;
import java.security.KeyPair;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Security;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

import javax.net.ssl.SSLContext;
import javax.net.ssl.X509TrustManager;
import javax.servlet.Filter;

import org.apache.http.ssl.SSLContexts;
import org.apache.http.ssl.TrustStrategy;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.openssl.PEMDecryptorProvider;
import org.bouncycastle.openssl.PEMEncryptedKeyPair;
import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8DecryptorProviderBuilder;
import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder;
import org.bouncycastle.operator.InputDecryptorProvider;
import org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.SslConnectionFactory;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.web.embedded.jetty.ConfigurableJettyWebServerFactory;
import org.springframework.boot.web.embedded.jetty.JettyServerCustomizer;
import org.springframework.boot.web.server.ConfigurableWebServerFactory;
import org.springframework.boot.web.server.Ssl;
import org.springframework.boot.web.server.SslStoreProvider;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.Resource;
import org.springframework.util.StreamUtils;
import org.springframework.web.client.RestTemplate;

import com.github.dtreskunov.easyssl.CRLTrustManager;
import com.github.dtreskunov.easyssl.ClientCertificateCheckingFilter;

/**
 * Defines Spring beans that are used for mutual SSL. They are:
 * 
    *
  1. {@link #easySslContext} - may be used to configure an SSL-using {@link RestTemplate}
  2. *
  3. {@link #easySslClientCertificateCheckingFilter} - checks that client's certificate has not been revoked
  4. *
  5. {@link #easySslServletContainerCustomizer} - used by Spring Boot to configure Jetty/Tomcat/Undertow to use SSL with client cert auth
  6. *
  7. {@code local.server.protocol} - environment property injectable into managed beans using {@code @Value}
  8. *
*/ @Configuration @EasySslBeans.ConditionalOnEnabled public class EasySslBeans { private static final String PROTOCOL_PROPERTY = "local.server.protocol"; @ConditionalOnProperty(value = "easyssl.enabled", matchIfMissing = true) public static @interface ConditionalOnEnabled {} @ConditionalOnEnabled @ConditionalOnWebApplication @ConditionalOnProperty(value = "easyssl.serverCustomizationEnabled", matchIfMissing = true) public static @interface ConditionalOnServerCustomizationEnabled {} static { Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider()); } /** Java APIs require a password when using a {@link KeyStore}. Hard-coded password is fine since the KeyStore is ephemeral. */ private static final String KEY_PASSWORD = UUID.randomUUID().toString(); // 122 bits of secure random goodness private static final String KEY_ALIAS = "easyssl-key"; private static final X509TrustManager NOOP_TRUST_MANAGER = new X509TrustManager() { @Override public X509Certificate[] getAcceptedIssuers() { return null; } @Override public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { // no-op } @Override public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { // no-op } }; public static SSLContext getSSLContext(EasySslProperties config) throws Exception { X509TrustManager trustManager = getTrustManager(config); TrustStrategy trustStrategy = new TrustStrategy() { @Override public boolean isTrusted(X509Certificate[] chain, String authType) throws CertificateException { trustManager.checkServerTrusted(chain, authType); return false; // still use the underlying JSSE verification } }; return SSLContexts.custom() .loadKeyMaterial(getKeyStore(config.getCertificate(), config.getKey(), config.getKeyPassword()), KEY_PASSWORD.toCharArray()) .loadTrustMaterial(getTrustStore(config.getCaCertificate()), trustStrategy) .build(); } private static List getCertificates(Resource certificate) throws Exception { CertificateFactory cf = CertificateFactory.getInstance("X.509"); // if several certs are concatenated together OpenSSL-style, this will only load the first one! // cf.generateCertificate(certificate.getInputStream()); String[] pems = StreamUtils.copyToString(certificate.getInputStream(), StandardCharsets.UTF_8).split("(?=-----BEGIN CERTIFICATE-----)"); ArrayList certs = new ArrayList<>(pems.length); for (String pem: pems) { certs.add(cf.generateCertificate(new ByteArrayInputStream(pem.getBytes(StandardCharsets.UTF_8)))); } return certs; } private static KeyStore getKeyStore(Resource certificate, Resource key, String keyPassword) throws Exception { PrivateKey privateKey = getPrivateKey(key.getInputStream(), keyPassword); List certs = getCertificates(certificate); KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); keyStore.load(null, null); keyStore.setKeyEntry(KEY_ALIAS, privateKey, KEY_PASSWORD.toCharArray(), certs.toArray(new Certificate[certs.size()])); return keyStore; } private static PrivateKey getPrivateKey(InputStream inputStream, String keyPassword) throws Exception { final Object pemObject; try (PEMParser pemParser = new PEMParser(new InputStreamReader(inputStream, Charset.defaultCharset()))) { pemObject = pemParser.readObject(); } JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC"); final PEMKeyPair pemKeyPair; final KeyPair keyPair; if (pemObject instanceof PEMEncryptedKeyPair) { if (keyPassword == null || keyPassword.isEmpty()) { throw new KeyException("Need a non-empty password for a PEMEncryptedKeyPair"); } PEMDecryptorProvider decryptor = new JcePEMDecryptorProviderBuilder().build(keyPassword.toCharArray()); pemKeyPair = ((PEMEncryptedKeyPair)pemObject).decryptKeyPair(decryptor); } else if (pemObject instanceof PEMKeyPair) { pemKeyPair = (PEMKeyPair) pemObject; } else if (pemObject instanceof PrivateKeyInfo) { return converter.getPrivateKey((PrivateKeyInfo) pemObject); } else if (pemObject instanceof PKCS8EncryptedPrivateKeyInfo) { if (keyPassword == null || keyPassword.isEmpty()) { throw new KeyException("Need a non-empty password for a PKCS8EncryptedPrivateKeyInfo"); } InputDecryptorProvider decryptor = new JceOpenSSLPKCS8DecryptorProviderBuilder().build(keyPassword.toCharArray()); PrivateKeyInfo privateKeyInfo = ((PKCS8EncryptedPrivateKeyInfo)pemObject).decryptPrivateKeyInfo(decryptor); return converter.getPrivateKey(privateKeyInfo); } else { throw new KeyException("Private key is expected to be either a PEMEncryptedKeyPair or a PEMKeyPair, but is actually a " + pemObject.getClass().getSimpleName()); } keyPair = converter.getKeyPair(pemKeyPair); return keyPair.getPrivate(); } private static X509TrustManager getTrustManager(EasySslProperties config) throws Exception { if (config.getCertificateRevocationList() == null) { return NOOP_TRUST_MANAGER; } ArrayList publicKeys = new ArrayList<>(config.getCaCertificate().size()); for (Resource r: config.getCaCertificate()) { for (Certificate c: getCertificates(r)) { publicKeys.add(c.getPublicKey()); } } return new CRLTrustManager( config.getCertificateRevocationList(), publicKeys, config.getCertificateRevocationListCheckTimeoutSeconds(), config.getCertificateRevocationListCheckIntervalSeconds(), TimeUnit.SECONDS ); } private static KeyStore getTrustStore(Collection certificates) throws Exception { CertificateFactory cf = CertificateFactory.getInstance("X.509"); KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); trustStore.load(null, null); int index = 0; for (Resource certificate: certificates) { Certificate ca = cf.generateCertificate(certificate.getInputStream()); trustStore.setCertificateEntry("easyssl-ca-" + index, ca); index++; } return trustStore; } @Bean @ConditionalOnEnabled public EasySslProperties easySslProperties() { return new EasySslProperties(); } @Bean @ConditionalOnEnabled public SSLContext easySslContext(EasySslProperties config) throws Exception { return getSSLContext(config); } @Bean @ConditionalOnServerCustomizationEnabled public Filter easySslClientCertificateCheckingFilter(EasySslProperties config) throws Exception { return new ClientCertificateCheckingFilter(getTrustManager(config)); } @Bean @ConditionalOnServerCustomizationEnabled public WebServerFactoryCustomizer easySslServletContainerCustomizer(EasySslProperties config, @Autowired(required = false) ServerProperties serverProperties) throws Exception { final SslStoreProvider storeProvider = getSslStoreProvider(config); final Ssl sslProperties = getSslProperties(config, serverProperties); return factory -> { factory.setSslStoreProvider(storeProvider); factory.setSsl(sslProperties); }; } /** * Jetty 9.4.15 started doing additional checks on client certificates. This bit of code reverts to the old behavior. * * See eclipse/jetty.project#3454 */ @Bean @ConditionalOnServerCustomizationEnabled public WebServerFactoryCustomizer jettyServerCustomizer() { return container -> { container.addServerCustomizers(new JettyServerCustomizer() { @Override public void customize(Server server) { for (Connector connector: server.getConnectors()) { if (!(connector instanceof ServerConnector)) { continue; } SslConnectionFactory connectionFactory = ((ServerConnector) connector).getConnectionFactory( SslConnectionFactory.class); if (connectionFactory == null) { continue; } SslContextFactory contextFactory = connectionFactory.getSslContextFactory(); contextFactory.setEndpointIdentificationAlgorithm(null); } } }); }; } @Autowired public void setProtocolEnvironmentProperty(ApplicationContext context, @Autowired(required = false) EasySslProperties config) { if (config != null && config.isEnabled() && config.isServerCustomizationEnabled()) { setEnvironmentProperty(context, PROTOCOL_PROPERTY, "https"); } else { setEnvironmentProperty(context, PROTOCOL_PROPERTY, "http"); } } @SuppressWarnings("unchecked") private void setEnvironmentProperty(ApplicationContext context, String propertyName, String propertyValue) { if (!(context instanceof ConfigurableApplicationContext)) { return; } ConfigurableEnvironment environment = ((ConfigurableApplicationContext) context).getEnvironment(); MutablePropertySources sources = environment.getPropertySources(); PropertySource source = sources.get("easyssl"); if (source == null) { source = new MapPropertySource("easyssl", new HashMap()); sources.addFirst(source); } ((Map) source.getSource()).put(propertyName, propertyValue); } private SslStoreProvider getSslStoreProvider(EasySslProperties config) throws Exception { final KeyStore keyStore = getKeyStore(config.getCertificate(), config.getKey(), config.getKeyPassword()); final KeyStore trustStore = getTrustStore(config.getCaCertificate()); return new SslStoreProvider() { @Override public KeyStore getKeyStore() throws Exception { return keyStore; } @Override public KeyStore getTrustStore() throws Exception { return trustStore; } }; } private Ssl getSslProperties(EasySslProperties config, ServerProperties serverProperties) { Ssl properties = (serverProperties == null || serverProperties.getSsl() == null) ? new Ssl() : serverProperties.getSsl(); properties.setEnabled(true); properties.setClientAuth(config.getClientAuth()); properties.setKeyAlias(KEY_ALIAS); properties.setKeyPassword(KEY_PASSWORD); return properties; } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy