为SSL Socket启用哪些密码套件?

21
我正在使用Java的SSLSocket来保护客户端和服务器程序之间的通信。服务器程序还提供来自Web浏览器的HTTPS请求。
根据《Beginning Cryptography with Java》第371页的说法,您应该始终在SSLSocket/SSLServerSocket上调用setEnabledCipherSuites,以确保最终协商的密码套件足够强大,适合您的目的。
话虽如此,对我的SSLSocketFactorygetDefaultCipherSuites方法的调用会产生约180个选项。这些选项范围从TLS_RSA_WITH_AES_256_CBC_SHA(我认为相当安全)到SSL_RSA_WITH_RC4_128_MD5(考虑到MD5的当前状态,不确定是否安全)再到SSL_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA(不完全确定其作用)。
那么,有哪些合理的密码套件列表可以限制套接字呢?
请注意,客户端和服务器都可以访问Bouncy Castle服务提供程序,并且它们可能已经安装了无限制的加密策略文件。

TLS_RSA_WITH_AES_256_CBC_SHA... - 你应该避免使用RSA密钥传输方案(或将它们放在你的广告列表底部)。相反,更喜欢短暂的密钥交换,比如DHE,以实现前向保密。事实上,TLS 1.3正在讨论删除它们,因为它们缺乏这种属性。 - jww
SSL_RSA_WITH_RC4_128_MD5 ... - RC4现在是个问题儿童。请参见关于TLS和WPA中RC4的安全性。攻击者可能无法在网络的2MSL时间窗口内伪造HMAC-MD5签名。然而,攻击者可以统计相关密码流中的位。(而MD5对于长期使用,如证书和数字签名,已经死亡)。 - jww
SSL_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA - 烧掉它。像瘟疫一样避免它。 - jww
3个回答

19
以下是我用来实施密码套件和协议的Java类。在使用SSLSocketFactoryEx之前,当我可以访问SSLSocket时,我会修改它们的属性。Java社区在Stack Overflow上帮助了我,所以现在我很高兴能在这里发布它。 SSLSocketFactoryEx更喜欢更强的密码套件(例如ECDHEDHE),并省略了较弱和不安全的密码套件(例如RC4MD5)。但是,在TLS 1.2不可用时,它必须启用四个RSA密钥传输密码套件,以与Google和Microsoft进行交互。它们是TLS_RSA_WITH_AES_256_CBC_SHA256TLS_RSA_WITH_AES_256_CBC_SHA和两个伙伴。如果可能,应该删除TLS_RSA_*密钥传输方案。
请将密码套件列表保持尽可能小。如果您广告所有可用密码(类似于Flaschen的列表),那么您的列表将超过80个。这在ClientHello中占用了160个字节,可能会导致一些设备失败,因为它们具有用于处理ClientHello的小型固定大小缓冲区。无法正常工作的设备包括F5和Ironport。

实际上,当首选列表与Java支持的密码套件相交时,下面代码中的列表将被减少到10或15个密码套件。例如,当我准备使用无限制的JCE策略连接microsoft.com或google.com时,以下是我得到的列表:

TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384 TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384 TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256 TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256 TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 TLS_DHE_DSS_WITH_AES_256_GCM_SHA384 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 TLS_DHE_DSS_WITH_AES_128_GCM_SHA256 TLS_DHE_DSS_WITH_AES_256_CBC_SHA256 TLS_DHE_RSA_WITH_AES_128_CBC_SHA TLS_DHE_DSS_WITH_AES_128_CBC_SHA TLS_RSA_WITH_AES_256_CBC_SHA256 TLS_RSA_WITH_AES_256_CBC_SHA TLS_RSA_WITH_AES_128_CBC_SHA256 TLS_RSA_WITH_AES_128_CBC_SHA

这个列表省略了一些弱的/受伤的算法,例如RC4和MD5。如果它们被启用,那么您可能会偶尔收到来自浏览器的过时的加密警告

使用默认的JCE策略,列表会更小,因为该策略删除了AES-256和其他一些算法。我认为受限策略下的密码套件大约有7个。

SSLSocketFactoryEx类还确保使用TLS 1.0及以上协议。Java 8之前的Java客户端禁用TLS 1.1和1.2。 SSLContext.getInstance("TLS")也会悄悄地插入SSLv3(即使在Java 8中),因此必须采取措施将其移除。

最后,下面的类是TLS 1.3感知的,所以当提供者提供时它应该可以工作。如果可用,*_CHACHA20_POLY1305密码套件是首选,因为它们比一些当前的套件快得多,并且具有更好的安全性能。Google已经在其服务器上推出了它。我不确定Oracle何时会提供它们。OpenSSL将在OpenSSL 1.1.0中提供它们。

您可以像这样使用:

URL url = new URL("https://www.google.com:443");
HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();

SSLSocketFactoryEx factory = new SSLSocketFactoryEx();
connection.setSSLSocketFactory(factory);
connection.setRequestProperty("charset", "utf-8");

InputStream input = connection.getInputStream();
InputStreamReader reader = new InputStreamReader(input, "utf-8");
BufferedReader buffer = new BufferedReader(reader);
...

class SSLSocketFactoryEx extends SSLSocketFactory
{
    public SSLSocketFactoryEx() throws NoSuchAlgorithmException, KeyManagementException
    {
        initSSLSocketFactoryEx(null,null,null);
    }

    public SSLSocketFactoryEx(KeyManager[] km, TrustManager[] tm, SecureRandom random) throws NoSuchAlgorithmException, KeyManagementException
    {
        initSSLSocketFactoryEx(km, tm, random);
    }

    public SSLSocketFactoryEx(SSLContext ctx) throws NoSuchAlgorithmException, KeyManagementException
    {
        initSSLSocketFactoryEx(ctx);
    }

    public String[] getDefaultCipherSuites()
    {
        return m_ciphers;
    }

    public String[] getSupportedCipherSuites()
    {
        return m_ciphers;
    }

    public String[] getDefaultProtocols()
    {
        return m_protocols;
    }

    public String[] getSupportedProtocols()
    {
        return m_protocols;
    }

    public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException
    {
        SSLSocketFactory factory = m_ctx.getSocketFactory();
        SSLSocket ss = (SSLSocket)factory.createSocket(s, host, port, autoClose);

        ss.setEnabledProtocols(m_protocols);
        ss.setEnabledCipherSuites(m_ciphers);

        return ss;
    }

    public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException
    {
        SSLSocketFactory factory = m_ctx.getSocketFactory();
        SSLSocket ss = (SSLSocket)factory.createSocket(address, port, localAddress, localPort);

        ss.setEnabledProtocols(m_protocols);
        ss.setEnabledCipherSuites(m_ciphers);

        return ss;
    }

    public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException
    {
        SSLSocketFactory factory = m_ctx.getSocketFactory();
        SSLSocket ss = (SSLSocket)factory.createSocket(host, port, localHost, localPort);

        ss.setEnabledProtocols(m_protocols);
        ss.setEnabledCipherSuites(m_ciphers);

        return ss;
    }

    public Socket createSocket(InetAddress host, int port) throws IOException
    {
        SSLSocketFactory factory = m_ctx.getSocketFactory();
        SSLSocket ss = (SSLSocket)factory.createSocket(host, port);

        ss.setEnabledProtocols(m_protocols);
        ss.setEnabledCipherSuites(m_ciphers);

        return ss;
    }

    public Socket createSocket(String host, int port) throws IOException
    {
        SSLSocketFactory factory = m_ctx.getSocketFactory();
        SSLSocket ss = (SSLSocket)factory.createSocket(host, port);

        ss.setEnabledProtocols(m_protocols);
        ss.setEnabledCipherSuites(m_ciphers);

        return ss;
    }

    private void initSSLSocketFactoryEx(KeyManager[] km, TrustManager[] tm, SecureRandom random)
    throws NoSuchAlgorithmException, KeyManagementException
    {
        m_ctx = SSLContext.getInstance("TLS");
        m_ctx.init(km, tm, random);

        m_protocols = GetProtocolList();
        m_ciphers = GetCipherList();
    }

    private void initSSLSocketFactoryEx(SSLContext ctx)
    throws NoSuchAlgorithmException, KeyManagementException
    {
        m_ctx = ctx;

        m_protocols = GetProtocolList();
        m_ciphers = GetCipherList();
    }

    protected String[] GetProtocolList()
    {
        String[] preferredProtocols = { "TLSv1", "TLSv1.1", "TLSv1.2", "TLSv1.3" };
        String[] availableProtocols = null;

        SSLSocket socket = null;

        try
        {
            SSLSocketFactory factory = m_ctx.getSocketFactory();
            socket = (SSLSocket)factory.createSocket();

            availableProtocols = socket.getSupportedProtocols();
            Arrays.sort(availableProtocols);
        }
        catch(Exception e)
        {
            return new String[]{ "TLSv1" };
        }
        finally
        {
            if(socket != null)
                socket.close();
        }

        List<String> aa = new ArrayList<String>();
        for(int i = 0; i < preferredProtocols.length; i++)
        {
            int idx = Arrays.binarySearch(availableProtocols, preferredProtocols[i]);
            if(idx >= 0)
                aa.add(preferredProtocols[i]);
        }

        return aa.toArray(new String[0]);
    }

    protected String[] GetCipherList()
    {
        String[] preferredCiphers = {

            // *_CHACHA20_POLY1305 are 3x to 4x faster than existing cipher suites.
            //   http://googleonlinesecurity.blogspot.com/2014/04/speeding-up-and-strengthening-https.html
            // Use them if available. Normative names can be found at (TLS spec depends on IPSec spec):
            //   http://tools.ietf.org/html/draft-nir-ipsecme-chacha20-poly1305-01
            //   http://tools.ietf.org/html/draft-mavrogiannopoulos-chacha-tls-02
            "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305",
            "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305",
            "TLS_ECDHE_ECDSA_WITH_CHACHA20_SHA",
            "TLS_ECDHE_RSA_WITH_CHACHA20_SHA",

            "TLS_DHE_RSA_WITH_CHACHA20_POLY1305",
            "TLS_RSA_WITH_CHACHA20_POLY1305",
            "TLS_DHE_RSA_WITH_CHACHA20_SHA",
            "TLS_RSA_WITH_CHACHA20_SHA",

            // Done with bleeding edge, back to TLS v1.2 and below
            "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384",
            "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384",
            "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256",
            "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256",

            "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
            "TLS_DHE_DSS_WITH_AES_256_GCM_SHA384",
            "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
            "TLS_DHE_DSS_WITH_AES_128_GCM_SHA256",

            // TLS v1.0 (with some SSLv3 interop)
            "TLS_DHE_RSA_WITH_AES_256_CBC_SHA384",
            "TLS_DHE_DSS_WITH_AES_256_CBC_SHA256",
            "TLS_DHE_RSA_WITH_AES_128_CBC_SHA",
            "TLS_DHE_DSS_WITH_AES_128_CBC_SHA",

            "TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA",
            "TLS_DHE_DSS_WITH_3DES_EDE_CBC_SHA",
            "SSL_DH_RSA_WITH_3DES_EDE_CBC_SHA",
            "SSL_DH_DSS_WITH_3DES_EDE_CBC_SHA",

            // RSA key transport sucks, but they are needed as a fallback.
            // For example, microsoft.com fails under all versions of TLS
            // if they are not included. If only TLS 1.0 is available at
            // the client, then google.com will fail too. TLS v1.3 is
            // trying to deprecate them, so it will be interesteng to see
            // what happens.
            "TLS_RSA_WITH_AES_256_CBC_SHA256",
            "TLS_RSA_WITH_AES_256_CBC_SHA",
            "TLS_RSA_WITH_AES_128_CBC_SHA256",
            "TLS_RSA_WITH_AES_128_CBC_SHA"
        };

        String[] availableCiphers = null;

        try
        {
            SSLSocketFactory factory = m_ctx.getSocketFactory();
            availableCiphers = factory.getSupportedCipherSuites();
            Arrays.sort(availableCiphers);
        }
        catch(Exception e)
        {
            return new String[] {
                "TLS_DHE_DSS_WITH_AES_128_CBC_SHA",
                "TLS_DHE_DSS_WITH_AES_256_CBC_SHA",
                "TLS_DHE_RSA_WITH_AES_128_CBC_SHA",
                "TLS_DHE_RSA_WITH_AES_256_CBC_SHA",
                "TLS_RSA_WITH_AES_256_CBC_SHA256",
                "TLS_RSA_WITH_AES_256_CBC_SHA",
                "TLS_RSA_WITH_AES_128_CBC_SHA256",
                "TLS_RSA_WITH_AES_128_CBC_SHA",
                "TLS_EMPTY_RENEGOTIATION_INFO_SCSV"
            };
        }

        List<String> aa = new ArrayList<String>();
        for(int i = 0; i < preferredCiphers.length; i++)
        {
            int idx = Arrays.binarySearch(availableCiphers, preferredCiphers[i]);
            if(idx >= 0)
                aa.add(preferredCiphers[i]);
        }

        aa.add("TLS_EMPTY_RENEGOTIATION_INFO_SCSV");

        return aa.toArray(new String[0]);
    }

    private SSLContext m_ctx;

    private String[] m_ciphers;
    private String[] m_protocols;
}

感谢分享代码。我有两个问题。首先,我不明白的是为什么代码包含2个createSocket函数,每个函数都有不同的参数?因为这个原因我得到了一个错误。如果我更改其中一个的名称,我不知道在代码中调用的实际上是哪一个。其次,在List<String> aa = new ArrayList<String>();中,我得到了一个错误,说“类型List不是泛型;它不能使用参数<String>进行参数化”。 - user2192774
1
此外,这段代码是否使用了Bouncy Castle提供程序?因为Java加密库不包含诸如“CHACHA20”之类的算法。我不知道在我的代码中应该调用Bouncy Castle提供程序的哪个部分。另一个问题是,我在代码中看到了像“TLS_ECDHE_ECDSA_WITH_CHACHA20_SHA”和“TLS_ECDHE_RSA_WITH_CHACHA20_SHA”这样的密码,这是SHA256吗?因为我实际需要的是CHACHA256。 - user2192774
@user2192774 - "为什么这段代码包含了两个不同参数的createSocket函数?" - 实际上有4或5个。因为这个类是Java的SSLSocketFactory的替代品,所以必须提供该原始类的所有方法。 - jww
@jww:讲解得很好。@user2192774:“我不知道在代码中应该在哪里调用Bouncy Castle提供程序......”我已经在我的类文件中导入了import org.bouncycastle.jce.provider.BouncyCastleProvider,并在构建自定义sslContext之前包含了这一行Security.addProvider(new BouncyCastleProvider()),这帮助我获得所需的密码,但为了排除不必要的密码,上述是很好的解释/示例!! - whoami - fakeFaceTrueSoul
谢谢你,你救了我! - SeikoTheWiz
如何构建自定义的sslContext? - Sila Siebert

7
不要使用任何带有“export”字样的东西。由于强加密出口限制,这是不完整的软件。
编辑:更改为使用2009年的文档。
2009年NIST建议书recommendation列出了以下内容,包括TLS_RSA_WITH_AES_256_CBC_SHA(你提到的): TLS_RSA_WITH_NULL_SHA(除非你确定不需要任何隐私/保密性,否则不要使用它)。
TLS_RSA_WITH_3DES_EDE_CBC_SHA
TLS_RSA_WITH_AES_128_CBC_SHA
TLS_RSA_WITH_AES_256_CBC_SHA
TLS_DH_DSS_WITH_3DES_EDE_CBC_SHA
TLS_DH_RSA_WITH_3DES_EDE_CBC_SHA
TLS_DHE_DSS_WITH_3DES_EDE_CBC_SHA
TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA
TLS_DH_DSS_WITH_AES_128_CBC_SHA
TLS_DH_RSA_WITH_AES_128_CBC_SHA
TLS_DHE_DSS_WITH_AES_128_CBC_SHA
TLS_DHE_RSA_WITH_AES_128_CBC_SHA
TLS_DH_DSS_WITH_AES_256_CBC_SHA
TLS_DH_RSA_WITH_AES_256_CBC_SHA
TLS_DHE_DSS_WITH_AES_256_CBC_SHA
TLS_DHE_RSA_WITH_AES_256_CBC_SHA
TLS_ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA
TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA
TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA
TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA
TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA
TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA
TLS_ECDH_RSA_WITH_3DES_EDE_CBC_SHA
TLS_ECDH_RSA_WITH_AES_128_CBC_SHA
TLS_ECDH_RSA_WITH_AES_256_CBC_SHA
TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA
TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA
TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA
TLS_PSK_WITH_3DES_EDE_CBC_SHA
TLS_PSK_WITH_AES_128_CBC_SHA
TLS_PSK_WITH_AES_256_CBC_SHA
TLS_DHE_PSK_WITH_3DES_EDE_CBC_SHA
TLS_DHE_PSK_WITH_AES_128_CBC_SHA
TLS_DHE_PSK_WITH_AES_256_CBC_SHA
TLS_RSA_PSK_WITH_3DES_EDE_CBC_SHA
TLS_RSA_PSK_WITH_AES_128_CBC_SHA
TLS_RSA_PSK_WITH_AES_256_CBC_SHA
TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 
TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 
TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256 
TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384 

4
TLS_RSA_WITH_NULL_SHA让您感到有些困惑 - 它是否指定没有加密?在这个上下文中,这是否意味着没有任何加密? - Zarkonnen
2
Zarkonnen,我认为你是正确的,TLS_RSA_WITH_NULL_SHA表示没有加密。我链接的草案第34页说,NULL加密用于“需要完整性保护但不需要加密的情况”。 - Matthew Flaschen
1
我怀疑他不需要像 TLS_PSK_WITH_3DES_EDE_CBC_SHA 这样的预共享密钥(PSK)密码套件。TLS_RSA_WITH_AES_256_CBC_SHA 使用 RSA 密钥传输。他应该使用 DHEECDHE 来实现前向保密。 - jww
这里有一个关于密码套件规范子部分及应避免的内容的良好解释: https://blog.eveoh.nl/2014/02/tls-ssl-ciphers-pfs-tomcat/ - Søren Boisen

1

在这里,您可以检查加密方法的安全性:https://ciphersuite.info/cs/

以下是通过API检查加密方法的方法:

 public static boolean isCipherSecure(String cipher) throws Exception
  {
    URL url = new URL("https://ciphersuite.info/api/cs/");
    URLConnection request = url.openConnection();
    request.connect();

    JsonElement root = JsonParser.parseReader(new InputStreamReader((InputStream) request.getContent())); 
    JsonObject rootobj = root.getAsJsonObject();
    JsonArray suits = rootobj.getAsJsonArray("ciphersuites");
    for (JsonElement el : suits)
    {
      Set<Map.Entry<String, JsonElement>> entries = el.getAsJsonObject().entrySet();
      for (Map.Entry<String, JsonElement> entry : entries)
      {
        if (entry.getKey().contentEquals(cipher) || entry.getValue().getAsJsonObject()
            .getAsJsonPrimitive("openssl_name").getAsString().contentEquals(cipher))
        {
          String sec = entry.getValue().getAsJsonObject().getAsJsonPrimitive("security").getAsString();
          System.out.println(entry.getKey() + ": " + sec);
          if (sec.contentEquals("recommended") || sec.contentEquals("secure"))
          {
            return true;
          }
          else
            return false;
        }
      }
    }
    throw new Exception("Can't find cipher: " + cipher);
  }

这个调用:

 SSLServerSocketFactory ssf = (SSLServerSocketFactory) SSLServerSocketFactory.getDefault();
    for (String cipher : ssf.getSupportedCipherSuites())
    {
        if (isCipherSecure(cipher))
        {
          //your code
        }
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接