为了满足安全规范,从http改造成https(见(四)启用HTTPS),然而启用https后就可以高枕无忧了吗?绿盟告诉你:当然不,TLS Client-initiated 重协商攻击(CVE-2011-1473)了解一下。
1. 漏洞
报告是这样的:
详细描述 该漏洞存在于SSL renegotiation的过程中。对于使用SSL重协商功能的服务都会受其影响。特别的,renegotiation被用于浏览器到服务器之间的验证。虽然目前可以在不启用renegotiation进程的情况下使用HTTPS,但很多服务器的默认设置均启用了renegotiation功能。该漏洞只需要一台普通电脑和DSL连接即可轻易攻破SSL服务器。而对于大型服务器集群来说,则需要20台电脑和120Kbps的网络连接即可实现。SSL是银行、网上电子邮件服务和其他用于服务器和用户之间保护私人数据并安全通信必不可少的功能。所以本次拒绝服务漏洞影响范围非常广危害非常大。 解决办法 使用SSL开启重协商的服务都会受该漏洞影响. Apache解决办法: 升级到Apache 2.2.15以后版本 IIS解决办法: IIS 5.0启用SSL服务时,也会受影响。可以升级IIS 6.0到更高的版本。 Lighttpd解决办法: 建议升级到lighttpd 1.4.30或者更高,并设置ssl.disable-client-renegotiation = "enable"。 http://download.lighttpd.net/lighttpd/releases-1.4.x/ Nginx解决办法: 0.7.x升级到nginx 0.7.64 0.8.x升级到 0.8.23 以及更高版本。 http://nginx.org/en/download.html Tomcat解决办法: 1、使用NIO connector代替BIO connector,因为NIO不支持重协商,参考如下配置: <Connector protocol="org.apache.coyote.http11.Http11NioProtocol"> (可能会影响Tomcat性能); 2、配置Nginx反向代理,在Nginx中修复OpenSSL相关问题。 参考链接: https://tomcat.apache.org/tomcat-6.0-doc/ssl-howto.html https://tomcat.apache.org/tomcat-7.0-doc/ssl-howto.html http://tomcat.apache.org/security-7.html#Not_a_vulnerability_in_Tomcat https://tomcat.apache.org/tomcat-6.0-doc/config/http.html#Connector_Comparison Squid解决办法: 升级到3.5.24以及以后版本 http://www.squid-cache.org/Versions/ 其它服务解决方案请联系各应用厂商确认关闭重协商的方法。
然而我的http server用的是netty(netty既支持jdk ssl,也支持open ssl,open ssl的安全性和性能都比jdk ssl来的高,可以参考博文)。
解决方案中并无想要的信息,仅有一个用nginx增加反向代理,这种方式并无法让我感到满意,网上搜了一堆信息,但并没有找到想要的答案,因此只能从源码找起了。
2. 找http server
方式是直接从启动入口,通过find usages往上找。
@Override public void start() throws WebServerException { if (this.nettyContext == null) { try { this.nettyContext = startHttpServer(); } catch (Exception ex) { if (findBindException(ex) != null) { SocketAddress address = this.httpServer.options().getAddress(); if (address instanceof InetSocketAddress) { throw new PortInUseException( ((InetSocketAddress) address).getPort()); } } throw new WebServerException("Unable to start Netty", ex); } NettyWebServer.logger.info("Netty started on port(s): " + getPort()); startDaemonAwaitThread(this.nettyContext); } }
@Override public WebServer getWebServer(HttpHandler httpHandler) { HttpServer httpServer = createHttpServer(); ReactorHttpHandlerAdapter handlerAdapter = new ReactorHttpHandlerAdapter( httpHandler); return new NettyWebServer(httpServer, handlerAdapter, this.lifecycleTimeout); }
private HttpServer createHttpServer() { return HttpServer.builder().options((options) -> { options.listenAddress(getListenAddress()); if (getSsl() != null && getSsl().isEnabled()) { SslServerCustomizer sslServerCustomizer = new SslServerCustomizer( getSsl(), getSslStoreProvider()); sslServerCustomizer.customize(options); } if (getCompression() != null && getCompression().getEnabled()) { CompressionCustomizer compressionCustomizer = new CompressionCustomizer( getCompression()); compressionCustomizer.customize(options); } applyCustomizers(options); }).build(); }
@Override public void customize(HttpServerOptions.Builder builder) { SslContextBuilder sslBuilder = SslContextBuilder .forServer(getKeyManagerFactory(this.ssl, this.sslStoreProvider)) .trustManager(getTrustManagerFactory(this.ssl, this.sslStoreProvider)); if (this.ssl.getEnabledProtocols() != null) { sslBuilder.protocols(this.ssl.getEnabledProtocols()); } if (this.ssl.getCiphers() != null) { sslBuilder = sslBuilder.ciphers(Arrays.asList(this.ssl.getCiphers())); } if (this.ssl.getClientAuth() == Ssl.ClientAuth.NEED) { sslBuilder = sslBuilder.clientAuth(ClientAuth.REQUIRE); } else if (this.ssl.getClientAuth() == Ssl.ClientAuth.WANT) { sslBuilder = sslBuilder.clientAuth(ClientAuth.OPTIONAL); } try { builder.sslContext(sslBuilder.build()); } catch (Exception ex) { throw new IllegalStateException(ex); } }
public SslContext build() throws SSLException { if (forServer) { return SslContext.newServerContextInternal(provider, sslContextProvider, trustCertCollection, trustManagerFactory, keyCertChain, key, keyPassword, keyManagerFactory, ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout, clientAuth, protocols, startTls, enableOcsp); } else { return SslContext.newClientContextInternal(provider, sslContextProvider, trustCertCollection, trustManagerFactory, keyCertChain, key, keyPassword, keyManagerFactory, ciphers, cipherFilter, apn, protocols, sessionCacheSize, sessionTimeout, enableOcsp); } }
static SslContext newServerContextInternal( SslProvider provider, Provider sslContextProvider, X509Certificate[] trustCertCollection, TrustManagerFactory trustManagerFactory, X509Certificate[] keyCertChain, PrivateKey key, String keyPassword, KeyManagerFactory keyManagerFactory, Iterable<String> ciphers, CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apn, long sessionCacheSize, long sessionTimeout, ClientAuth clientAuth, String[] protocols, boolean startTls, boolean enableOcsp) throws SSLException { if (provider == null) { provider = defaultServerProvider(); } switch (provider) { case JDK: if (enableOcsp) { throw new IllegalArgumentException("OCSP is not supported with this SslProvider: " + provider); } return new JdkSslServerContext(sslContextProvider, trustCertCollection, trustManagerFactory, keyCertChain, key, keyPassword, keyManagerFactory, ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout, clientAuth, protocols, startTls); case OPENSSL: verifyNullSslContextProvider(provider, sslContextProvider); return new OpenSslServerContext( trustCertCollection, trustManagerFactory, keyCertChain, key, keyPassword, keyManagerFactory, ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout, clientAuth, protocols, startTls, enableOcsp); case OPENSSL_REFCNT: verifyNullSslContextProvider(provider, sslContextProvider); return new ReferenceCountedOpenSslServerContext( trustCertCollection, trustManagerFactory, keyCertChain, key, keyPassword, keyManagerFactory, ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout, clientAuth, protocols, startTls, enableOcsp); default: throw new Error(provider.toString()); } }
找到这一步会发现provider为null,然后调用defaultServerProvider()方法里
private static SslProvider defaultProvider() { if (OpenSsl.isAvailable()) { return SslProvider.OPENSSL; } else { return SslProvider.JDK; } }
/** * Returns {@code true} if and only if * <a href="http://netty.io/wiki/forked-tomcat-native.html">{@code netty-tcnative}</a> and its OpenSSL support * are available. */ public static boolean isAvailable() { return UNAVAILABILITY_CAUSE == null; }
发现,源码会先找open ssl,如果找不到则启用jdk ssl。找open ssl的源码是这样的:
static { Throwable cause = null; if (SystemPropertyUtil.getBoolean("io.netty.handler.ssl.noOpenSsl", false)) { cause = new UnsupportedOperationException( "OpenSSL was explicit disabled with -Dio.netty.handler.ssl.noOpenSsl=true"); logger.debug( "netty-tcnative explicit disabled; " + OpenSslEngine.class.getSimpleName() + " will be unavailable.", cause); } else { // Test if netty-tcnative is in the classpath first. try { Class.forName("io.netty.internal.tcnative.SSL", false, OpenSsl.class.getClassLoader()); } catch (ClassNotFoundException t) { cause = t; logger.debug( "netty-tcnative not in the classpath; " + OpenSslEngine.class.getSimpleName() + " will be unavailable."); } // If in the classpath, try to load the native library and initialize netty-tcnative. if (cause == null) { try { // The JNI library was not already loaded. Load it now. loadTcNative(); } catch (Throwable t) { cause = t; logger.debug( "Failed to load netty-tcnative; " + OpenSslEngine.class.getSimpleName() + " will be unavailable, unless the " + "application has already loaded the symbols by some other means. " + "See http://netty.io/wiki/forked-tomcat-native.html for more information.", t); } try { String engine = SystemPropertyUtil.get("io.netty.handler.ssl.openssl.engine", null); if (engine == null) { logger.debug("Initialize netty-tcnative using engine: 'default'"); } else { logger.debug("Initialize netty-tcnative using engine: '{}'", engine); } initializeTcNative(engine); // The library was initialized successfully. If loading the library failed above, // reset the cause now since it appears that the library was loaded by some other // means. cause = null; } catch (Throwable t) { if (cause == null) { cause = t; } logger.debug( "Failed to initialize netty-tcnative; " + OpenSslEngine.class.getSimpleName() + " will be unavailable. " + "See http://netty.io/wiki/forked-tomcat-native.html for more information.", t); } } } UNAVAILABILITY_CAUSE = cause; if (cause == null) { logger.debug("netty-tcnative using native library: {}", SSL.versionString()); final List<String> defaultCiphers = new ArrayList<String>(); final Set<String> availableOpenSslCipherSuites = new LinkedHashSet<String>(128); boolean supportsKeyManagerFactory = false; boolean useKeyManagerFactory = false; boolean supportsHostNameValidation = false; try { final long sslCtx = SSLContext.make(SSL.SSL_PROTOCOL_ALL, SSL.SSL_MODE_SERVER); long certBio = 0; SelfSignedCertificate cert = null; try { SSLContext.setCipherSuite(sslCtx, "ALL"); final long ssl = SSL.newSSL(sslCtx, true); try { for (String c: SSL.getCiphers(ssl)) { // Filter out bad input. if (c == null || c.isEmpty() || availableOpenSslCipherSuites.contains(c)) { continue; } availableOpenSslCipherSuites.add(c); } try { SSL.setHostNameValidation(ssl, 0, "netty.io"); supportsHostNameValidation = true; } catch (Throwable ignore) { logger.debug("Hostname Verification not supported."); } try { cert = new SelfSignedCertificate(); certBio = ReferenceCountedOpenSslContext.toBIO(ByteBufAllocator.DEFAULT, cert.cert()); SSL.setCertificateChainBio(ssl, certBio, false); supportsKeyManagerFactory = true; try { useKeyManagerFactory = AccessController.doPrivileged(new PrivilegedAction<Boolean>() { @Override public Boolean run() { return SystemPropertyUtil.getBoolean( "io.netty.handler.ssl.openssl.useKeyManagerFactory", true); } }); } catch (Throwable ignore) { logger.debug("Failed to get useKeyManagerFactory system property."); } } catch (Throwable ignore) { logger.debug("KeyManagerFactory not supported."); } } finally { SSL.freeSSL(ssl); if (certBio != 0) { SSL.freeBIO(certBio); } if (cert != null) { cert.delete(); } } } finally { SSLContext.free(sslCtx); } } catch (Exception e) { logger.warn("Failed to get the list of available OpenSSL cipher suites.", e); } AVAILABLE_OPENSSL_CIPHER_SUITES = Collections.unmodifiableSet(availableOpenSslCipherSuites); final Set<String> availableJavaCipherSuites = new LinkedHashSet<String>( AVAILABLE_OPENSSL_CIPHER_SUITES.size() * 2); for (String cipher: AVAILABLE_OPENSSL_CIPHER_SUITES) { // Included converted but also openssl cipher name availableJavaCipherSuites.add(CipherSuiteConverter.toJava(cipher, "TLS")); availableJavaCipherSuites.add(CipherSuiteConverter.toJava(cipher, "SSL")); } addIfSupported(availableJavaCipherSuites, defaultCiphers, DEFAULT_CIPHER_SUITES); useFallbackCiphersIfDefaultIsEmpty(defaultCiphers, availableJavaCipherSuites); DEFAULT_CIPHERS = Collections.unmodifiableList(defaultCiphers); AVAILABLE_JAVA_CIPHER_SUITES = Collections.unmodifiableSet(availableJavaCipherSuites); final Set<String> availableCipherSuites = new LinkedHashSet<String>( AVAILABLE_OPENSSL_CIPHER_SUITES.size() + AVAILABLE_JAVA_CIPHER_SUITES.size()); availableCipherSuites.addAll(AVAILABLE_OPENSSL_CIPHER_SUITES); availableCipherSuites.addAll(AVAILABLE_JAVA_CIPHER_SUITES); AVAILABLE_CIPHER_SUITES = availableCipherSuites; SUPPORTS_KEYMANAGER_FACTORY = supportsKeyManagerFactory; SUPPORTS_HOSTNAME_VALIDATION = supportsHostNameValidation; USE_KEYMANAGER_FACTORY = useKeyManagerFactory; Set<String> protocols = new LinkedHashSet<String>(6); // Seems like there is no way to explicitly disable SSLv2Hello in openssl so it is always enabled protocols.add(PROTOCOL_SSL_V2_HELLO); if (doesSupportProtocol(SSL.SSL_PROTOCOL_SSLV2, SSL.SSL_OP_NO_SSLv2)) { protocols.add(PROTOCOL_SSL_V2); } if (doesSupportProtocol(SSL.SSL_PROTOCOL_SSLV3, SSL.SSL_OP_NO_SSLv3)) { protocols.add(PROTOCOL_SSL_V3); } if (doesSupportProtocol(SSL.SSL_PROTOCOL_TLSV1, SSL.SSL_OP_NO_TLSv1)) { protocols.add(PROTOCOL_TLS_V1); } if (doesSupportProtocol(SSL.SSL_PROTOCOL_TLSV1_1, SSL.SSL_OP_NO_TLSv1_1)) { protocols.add(PROTOCOL_TLS_V1_1); } if (doesSupportProtocol(SSL.SSL_PROTOCOL_TLSV1_2, SSL.SSL_OP_NO_TLSv1_2)) { protocols.add(PROTOCOL_TLS_V1_2); } SUPPORTED_PROTOCOLS_SET = Collections.unmodifiableSet(protocols); SUPPORTS_OCSP = doesSupportOcsp(); if (logger.isDebugEnabled()) { logger.debug("Supported protocols (OpenSSL): {} ", SUPPORTED_PROTOCOLS_SET); logger.debug("Default cipher suites (OpenSSL): {}", DEFAULT_CIPHERS); } } else { DEFAULT_CIPHERS = Collections.emptyList(); AVAILABLE_OPENSSL_CIPHER_SUITES = Collections.emptySet(); AVAILABLE_JAVA_CIPHER_SUITES = Collections.emptySet(); AVAILABLE_CIPHER_SUITES = Collections.emptySet(); SUPPORTS_KEYMANAGER_FACTORY = false; SUPPORTS_HOSTNAME_VALIDATION = false; USE_KEYMANAGER_FACTORY = false; SUPPORTED_PROTOCOLS_SET = Collections.emptySet(); SUPPORTS_OCSP = false; } }
最终发现是由于io.netty.internal.tcnative.SSL的类找不到。
直到现在ssl启动的顺序思路也比较清晰了,如果系统支持open ssl则启用open ssl,如果不支持则用jdk ssl。那么我们改造的方式也就相应的有2种:
1. 改造jdk ssl,禁用SSL renegotiation
2. 找到相对应的包,支持open ssl
个人倾向第二种,原因就是open ssl的性能和安全性比jdk ssl更加好。针对第一种,翻遍相关资料,均是对jdk1.8以下的,对jdk1.8以上的均没有说明(jdk1.8以下的参考这个)。
3. 支持open ssl
通过上面的源码,可以看到是io.netty.internal.tcnative.SSL找不到,那么我们就只要增加相对应的包就好了。
找了下maven仓库,有以下包可用:
在pom中增加对应的依赖
使用了netty-tcnative,会发现这种方式需要增加平台支持,不然就会报以下错误
Failed to load any of the given libraries: [netty_tcnative_windows_x86_64, netty_tcnative_x86_64, netty_tcnative]
载入的包越来越多
使用netty-tcnative-boringssl-static则没有任何问题(boringssl是google对openssl的一个fork,果然google强大)。
重启后用openssl s_client -connect进行连接,之后输入R进行验证:
[root@devlop gateway]# openssl s_client -connect localhost:443 CONNECTED(00000003) depth=3 C = US, O = "The Go Daddy Group, Inc.", OU = Go Daddy Class 2 Certification Authority verify return:1******中间省略****** Secure Renegotiation IS supported Compression: NONE Expansion: NONE No ALPN negotiated SSL-Session: Protocol : TLSv1.2 Cipher : ECDHE-RSA-AES256-SHA Session-ID: 5C384430A5E50FB18848259D71396E94320454D6828439347F9791D43F32BC63 Session-ID-ctx: Master-Key: 3D7FCF9A38CA624D9B22A79A05CCBBACDED4ED01DF5AD3E1CBF19BE9AEFD9CD08A6FECFC215ECEECF829C004C3D7F2F1 Key-Arg : None Krb5 Principal: None PSK identity: None PSK identity hint: None Start Time: 1547191344 Timeout : 300 (sec) Verify return code: 0 (ok) ---
===改造前===
R RENEGOTIATING depth=3 C = US, O = "The Go Daddy Group, Inc.", OU = Go Daddy Class 2 Certification Authority verify return:1 depth=2 C = US, ST = Arizona, L = Scottsdale, O = "GoDaddy.com, Inc.", CN = Go Daddy Root Certificate Authority - G2 verify return:1 depth=1 C = US, ST = Arizona, L = Scottsdale, O = "GoDaddy.com, Inc.", OU = http://certs.godaddy.com/repository/, CN = Go Daddy Secure Certificate Authority - G2 verify return:1 depth=0 C = CN, L = E69DADE5B79EE5B882, O = E4B8ADE7A7BBEFBC88E69DADE5B79EEFBC89E4BFA1E681AFE68A80E69CAFE69C89E99990E585ACE58FB8, CN = ***** verify return:1 read:errno=0
===改造后===
R
RENEGOTIATING
140137592752032:error:1409444C:SSL routines:ssl3_read_bytes:tlsv1 alert no renegotiation:s3_pkt.c:1493:SSL alert number 100 140137592752032:error:1409E0E5:SSL routines:ssl3_write_bytes:ssl handshake failure:s3_pkt.c:659:
解决问题。