From b1fe85cc976c676eb50ff886596c93e04fd71d82 Mon Sep 17 00:00:00 2001 From: Alex Klyubin Date: Tue, 2 Dec 2014 11:15:42 -0800 Subject: Add SSLSocket tests which inspect emitted ClientHello fields. This CL adds basic tests which capture the ClientHello emitted by SSLSocket and assert that the various fields are as expected. In particular, this CL adds tests for: * client protocol version, * cipher suite list, * compression methods, * server_name extension (SNI). Change-Id: I387c44363ad26f064885f9bfa28572da37871078 --- .../libcore/tlswire/handshake/CipherSuite.java | 105 +++++++++----------- .../handshake/ServerNameHelloExtension.java | 2 +- .../tests/util/DelegatingSSLSocketFactory.java | 101 +++++++++++++++++++ .../src/test/java/tests/util/ForEachRunner.java | 54 +++++++++++ support/src/test/java/tests/util/Pair.java | 107 +++++++++++++++++++++ 5 files changed, 311 insertions(+), 58 deletions(-) create mode 100644 support/src/test/java/tests/util/DelegatingSSLSocketFactory.java create mode 100644 support/src/test/java/tests/util/ForEachRunner.java create mode 100644 support/src/test/java/tests/util/Pair.java (limited to 'support/src') diff --git a/support/src/test/java/libcore/tlswire/handshake/CipherSuite.java b/support/src/test/java/libcore/tlswire/handshake/CipherSuite.java index 959379d..88ce2f2 100644 --- a/support/src/test/java/libcore/tlswire/handshake/CipherSuite.java +++ b/support/src/test/java/libcore/tlswire/handshake/CipherSuite.java @@ -27,33 +27,41 @@ public class CipherSuite { // https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml private static final CipherSuite[] CIPHER_SUITES = new CipherSuite[] { new CipherSuite(0x0000, "TLS_NULL_WITH_NULL_NULL"), - new CipherSuite(0x0001, "TLS_RSA_WITH_NULL_MD5"), - new CipherSuite(0x0002, "TLS_RSA_WITH_NULL_SHA"), - new CipherSuite(0x0003, "TLS_RSA_EXPORT_WITH_RC4_40_MD5"), - new CipherSuite(0x0004, "TLS_RSA_WITH_RC4_128_MD5"), - new CipherSuite(0x0005, "TLS_RSA_WITH_RC4_128_SHA"), + new CipherSuite(0x0001, "TLS_RSA_WITH_NULL_MD5", "SSL_RSA_WITH_NULL_MD5"), + new CipherSuite(0x0002, "TLS_RSA_WITH_NULL_SHA", "SSL_RSA_WITH_NULL_SHA"), + new CipherSuite(0x0003, "TLS_RSA_EXPORT_WITH_RC4_40_MD5", "SSL_RSA_EXPORT_WITH_RC4_40_MD5"), + new CipherSuite(0x0004, "TLS_RSA_WITH_RC4_128_MD5", "SSL_RSA_WITH_RC4_128_MD5"), + new CipherSuite(0x0005, "TLS_RSA_WITH_RC4_128_SHA", "SSL_RSA_WITH_RC4_128_SHA"), new CipherSuite(0x0006, "TLS_RSA_EXPORT_WITH_RC2_CBC_40_MD5"), new CipherSuite(0x0007, "TLS_RSA_WITH_IDEA_CBC_SHA"), - new CipherSuite(0x0008, "TLS_RSA_EXPORT_WITH_DES40_CBC_SHA"), - new CipherSuite(0x0009, "TLS_RSA_WITH_DES_CBC_SHA"), - new CipherSuite(0x000a, "TLS_RSA_WITH_3DES_EDE_CBC_SHA"), + new CipherSuite(0x0008, "TLS_RSA_EXPORT_WITH_DES40_CBC_SHA", + "SSL_RSA_EXPORT_WITH_DES40_CBC_SHA"), + new CipherSuite(0x0009, "TLS_RSA_WITH_DES_CBC_SHA", "SSL_RSA_WITH_DES_CBC_SHA"), + new CipherSuite(0x000a, "TLS_RSA_WITH_3DES_EDE_CBC_SHA", "SSL_RSA_WITH_3DES_EDE_CBC_SHA"), new CipherSuite(0x000b, "TLS_DH_DSS_EXPORT_WITH_DES40_CBC_SHA"), new CipherSuite(0x000c, "TLS_DH_DSS_WITH_DES_CBC_SHA"), new CipherSuite(0x000d, "TLS_DH_DSS_WITH_3DES_EDE_CBC_SHA"), new CipherSuite(0x000e, "TLS_DH_RSA_EXPORT_WITH_DES40_CBC_SHA"), new CipherSuite(0x000f, "TLS_DH_RSA_WITH_DES_CBC_SHA"), new CipherSuite(0x0010, "TLS_DH_RSA_WITH_3DES_EDE_CBC_SHA"), - new CipherSuite(0x0011, "TLS_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA"), - new CipherSuite(0x0012, "TLS_DHE_DSS_WITH_DES_CBC_SHA"), - new CipherSuite(0x0013, "TLS_DHE_DSS_WITH_3DES_EDE_CBC_SHA"), - new CipherSuite(0x0014, "TLS_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA"), - new CipherSuite(0x0015, "TLS_DHE_RSA_WITH_DES_CBC_SHA"), - new CipherSuite(0x0016, "TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA"), - new CipherSuite(0x0017, "TLS_DH_anon_EXPORT_WITH_RC4_40_MD5"), - new CipherSuite(0x0018, "TLS_DH_anon_WITH_RC4_128_MD5"), - new CipherSuite(0x0019, "TLS_DH_anon_EXPORT_WITH_DES40_CBC_SHA"), - new CipherSuite(0x001a, "TLS_DH_anon_WITH_DES_CBC_SHA"), - new CipherSuite(0x001b, "TLS_DH_anon_WITH_3DES_EDE_CBC_SHA"), + new CipherSuite(0x0011, "TLS_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA", + "SSL_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA"), + new CipherSuite(0x0012, "TLS_DHE_DSS_WITH_DES_CBC_SHA", "SSL_DHE_DSS_WITH_DES_CBC_SHA"), + new CipherSuite(0x0013, "TLS_DHE_DSS_WITH_3DES_EDE_CBC_SHA", + "SSL_DHE_DSS_WITH_3DES_EDE_CBC_SHA"), + new CipherSuite(0x0014, "TLS_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA", + "SSL_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA"), + new CipherSuite(0x0015, "TLS_DHE_RSA_WITH_DES_CBC_SHA", "SSL_DHE_RSA_WITH_DES_CBC_SHA"), + new CipherSuite(0x0016, "TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA", + "SSL_DHE_RSA_WITH_3DES_EDE_CBC_SHA"), + new CipherSuite(0x0017, "TLS_DH_anon_EXPORT_WITH_RC4_40_MD5", + "SSL_DH_anon_EXPORT_WITH_RC4_40_MD5"), + new CipherSuite(0x0018, "TLS_DH_anon_WITH_RC4_128_MD5", "SSL_DH_anon_WITH_RC4_128_MD5"), + new CipherSuite(0x0019, "TLS_DH_anon_EXPORT_WITH_DES40_CBC_SHA", + "SSL_DH_anon_EXPORT_WITH_DES40_CBC_SHA"), + new CipherSuite(0x001a, "TLS_DH_anon_WITH_DES_CBC_SHA", "SSL_DH_anon_WITH_DES_CBC_SHA"), + new CipherSuite(0x001b, "TLS_DH_anon_WITH_3DES_EDE_CBC_SHA", + "SSL_DH_anon_WITH_3DES_EDE_CBC_SHA"), new CipherSuite(0x001e, "TLS_KRB5_WITH_DES_CBC_SHA"), new CipherSuite(0x001f, "TLS_KRB5_WITH_3DES_EDE_CBC_SHA"), new CipherSuite(0x0020, "TLS_KRB5_WITH_RC4_128_SHA"), @@ -369,59 +377,38 @@ public class CipherSuite { throw new RuntimeException( "Cipher suite multiply defined: " + Integer.toHexString(cipherSuite.code)); } - if (byName.put(cipherSuite.name, cipherSuite) != null) { + String name = cipherSuite.name; + if (byName.put(name, cipherSuite) != null) { throw new RuntimeException( "Cipher suite multiply defined: " + cipherSuite.name); } + String androidName = cipherSuite.getAndroidName(); + if (!name.equals(androidName)) { + if (byName.put(androidName, cipherSuite) != null) { + throw new RuntimeException( + "Cipher suite multiply defined: " + cipherSuite.androidName); + } + } } - // Add alternative names used in Android's platform-default TLS/SSL stack. - addAltName(byName, - "TLS_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA", "SSL_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA"); - addAltName(byName, - "TLS_DHE_DSS_WITH_3DES_EDE_CBC_SHA", "SSL_DHE_DSS_WITH_3DES_EDE_CBC_SHA"); - addAltName(byName, "TLS_DHE_DSS_WITH_DES_CBC_SHA", "SSL_DHE_DSS_WITH_DES_CBC_SHA"); - addAltName(byName, - "TLS_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA", "SSL_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA"); - addAltName(byName, - "TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA", "SSL_DHE_RSA_WITH_3DES_EDE_CBC_SHA"); - addAltName(byName, "TLS_DHE_RSA_WITH_DES_CBC_SHA", "SSL_DHE_RSA_WITH_DES_CBC_SHA"); - addAltName(byName, - "TLS_DH_anon_EXPORT_WITH_DES40_CBC_SHA", "SSL_DH_anon_EXPORT_WITH_DES40_CBC_SHA"); - addAltName(byName, - "TLS_DH_anon_EXPORT_WITH_RC4_40_MD5", "SSL_DH_anon_EXPORT_WITH_RC4_40_MD5"); - addAltName(byName, - "TLS_DH_anon_WITH_3DES_EDE_CBC_SHA", "SSL_DH_anon_WITH_3DES_EDE_CBC_SHA"); - addAltName(byName, "TLS_DH_anon_WITH_DES_CBC_SHA", "SSL_DH_anon_WITH_DES_CBC_SHA"); - addAltName(byName, "TLS_DH_anon_WITH_RC4_128_MD5", "SSL_DH_anon_WITH_RC4_128_MD5"); - addAltName(byName, - "TLS_RSA_EXPORT_WITH_DES40_CBC_SHA", "SSL_RSA_EXPORT_WITH_DES40_CBC_SHA"); - addAltName(byName, "TLS_RSA_EXPORT_WITH_RC4_40_MD5", "SSL_RSA_EXPORT_WITH_RC4_40_MD5"); - addAltName(byName, "TLS_RSA_WITH_3DES_EDE_CBC_SHA", "SSL_RSA_WITH_3DES_EDE_CBC_SHA"); - addAltName(byName, "TLS_RSA_WITH_DES_CBC_SHA", "SSL_RSA_WITH_DES_CBC_SHA"); - addAltName(byName, "TLS_RSA_WITH_NULL_MD5", "SSL_RSA_WITH_NULL_MD5"); - addAltName(byName, "TLS_RSA_WITH_NULL_SHA", "SSL_RSA_WITH_NULL_SHA"); - addAltName(byName, "TLS_RSA_WITH_RC4_128_MD5", "SSL_RSA_WITH_RC4_128_MD5"); - addAltName(byName, "TLS_RSA_WITH_RC4_128_SHA", "SSL_RSA_WITH_RC4_128_SHA"); - CODE_TO_CIPHER_SUITE = byCode; NAME_TO_CIPHER_SUITE = byName; } - private static void addAltName(Map byName, String name, String altName) { - CipherSuite cipherSuite = byName.get(name); - if (cipherSuite == null) { - throw new IllegalArgumentException("Cipher suite not found: " + name); - } - byName.put(altName, cipherSuite); - } - public final int code; public final String name; + private final String androidName; private CipherSuite(int code, String name) { this.code = code; this.name = name; + this.androidName = null; + } + + private CipherSuite(int code, String name, String androidName) { + this.code = code; + this.name = name; + this.androidName = androidName; } public static CipherSuite valueOf(String name) { @@ -440,6 +427,10 @@ public class CipherSuite { return new CipherSuite(code, Integer.toHexString(code)); } + public String getAndroidName() { + return (androidName != null) ? androidName : name; + } + @Override public String toString() { return name; diff --git a/support/src/test/java/libcore/tlswire/handshake/ServerNameHelloExtension.java b/support/src/test/java/libcore/tlswire/handshake/ServerNameHelloExtension.java index afc9cfe..5b06246 100644 --- a/support/src/test/java/libcore/tlswire/handshake/ServerNameHelloExtension.java +++ b/support/src/test/java/libcore/tlswire/handshake/ServerNameHelloExtension.java @@ -29,7 +29,7 @@ import java.util.List; public class ServerNameHelloExtension extends HelloExtension { private static final int TYPE_HOST_NAME = 0; - private List hostnames; + public List hostnames; @Override protected void parseData() throws IOException { diff --git a/support/src/test/java/tests/util/DelegatingSSLSocketFactory.java b/support/src/test/java/tests/util/DelegatingSSLSocketFactory.java new file mode 100644 index 0000000..5513210 --- /dev/null +++ b/support/src/test/java/tests/util/DelegatingSSLSocketFactory.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * 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 tests.util; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.Socket; +import java.net.UnknownHostException; + +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; + +/** + * {@link SSLSocketFactory} which delegates all invocations to the provided delegate + * {@code SSLSocketFactory}. + */ +public class DelegatingSSLSocketFactory extends SSLSocketFactory { + + private final SSLSocketFactory mDelegate; + + public DelegatingSSLSocketFactory(SSLSocketFactory delegate) { + this.mDelegate = delegate; + } + + /** + * Invoked after obtaining a socket from the delegate and before returning it to the caller. + * + *

The default implementation does nothing. + */ + protected void configureSocket(@SuppressWarnings("unused") SSLSocket socket) {} + + @Override + public String[] getDefaultCipherSuites() { + return mDelegate.getDefaultCipherSuites(); + } + + @Override + public String[] getSupportedCipherSuites() { + return mDelegate.getSupportedCipherSuites(); + } + + @Override + public Socket createSocket() throws IOException { + SSLSocket socket = (SSLSocket) mDelegate.createSocket(); + configureSocket(socket); + return socket; + } + + @Override + public Socket createSocket(Socket s, String host, int port, boolean autoClose) + throws IOException { + SSLSocket socket = (SSLSocket) mDelegate.createSocket(s, host, port, autoClose); + configureSocket(socket); + return socket; + } + + @Override + public Socket createSocket(String host, int port) throws IOException, UnknownHostException { + SSLSocket socket = (SSLSocket) mDelegate.createSocket(host, port); + configureSocket(socket); + return socket; + } + + @Override + public Socket createSocket(String host, int port, InetAddress localHost, int localPort) + throws IOException, UnknownHostException { + SSLSocket socket = (SSLSocket) mDelegate.createSocket(host, port, localHost, localPort); + configureSocket(socket); + return socket; + } + + @Override + public Socket createSocket(InetAddress host, int port) throws IOException { + SSLSocket socket = (SSLSocket) mDelegate.createSocket(host, port); + configureSocket(socket); + return socket; + } + + @Override + public Socket createSocket(InetAddress address, int port, InetAddress localAddress, + int localPort) throws IOException { + SSLSocket socket = + (SSLSocket) mDelegate.createSocket(address, port, localAddress, localPort); + configureSocket(socket); + return socket; + } +} diff --git a/support/src/test/java/tests/util/ForEachRunner.java b/support/src/test/java/tests/util/ForEachRunner.java new file mode 100644 index 0000000..2c222b2 --- /dev/null +++ b/support/src/test/java/tests/util/ForEachRunner.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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 tests.util; + +/** + * Runner which executes the provided code under test (via a callback) for each provided input + * value. + */ +public final class ForEachRunner { + + /** + * Callback parameterized with a value. + */ + public interface Callback { + /** + * Invokes the callback for the provided value. + */ + void run(T value) throws Exception; + } + + private ForEachRunner() {} + + /** + * Invokes the provided callback for each of the provided named values. + * + * @param namesAndValues named values represented as name-value pairs. + * + * @param type of value. + */ + public static void runNamed(Callback callback, Iterable> namesAndValues) + throws Exception { + for (Pair nameAndValue : namesAndValues) { + try { + callback.run(nameAndValue.getSecond()); + } catch (Throwable e) { + throw new Exception("Failed for " + nameAndValue.getFirst() + ": " + e.getMessage(), e); + } + } + } +} diff --git a/support/src/test/java/tests/util/Pair.java b/support/src/test/java/tests/util/Pair.java new file mode 100644 index 0000000..9b0906d --- /dev/null +++ b/support/src/test/java/tests/util/Pair.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * 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 tests.util; + +/** + * Pair of typed values. + * + *

Pairs are obtained using {@link #of(Object, Object) of}. + * + * @param type of the first value. + * @param type of the second value. + */ +public class Pair { + private final F mFirst; + private final S mSecond; + + private Pair(F first, S second) { + mFirst = first; + mSecond = second; + } + + /** + * Gets the pair consisting of the two provided values. + * + * @param first first value or {@code null}. + * @param second second value or {@code null}. + */ + public static Pair of(F first, S second) { + return new Pair(first, second); + } + + /** + * Gets the first value from this pair. + * + * @return value or {@code null}. + */ + public F getFirst() { + return mFirst; + } + + /** + * Gets the second value from this pair. + * + * @return value or {@code null}. + */ + public S getSecond() { + return mSecond; + } + + @Override + public String toString() { + return "Pair[" + mFirst + ", " + mSecond + "]"; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((mFirst == null) ? 0 : mFirst.hashCode()); + result = prime * result + ((mSecond == null) ? 0 : mSecond.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + @SuppressWarnings("rawtypes") + Pair other = (Pair) obj; + if (mFirst == null) { + if (other.mFirst != null) { + return false; + } + } else if (!mFirst.equals(other.mFirst)) { + return false; + } + if (mSecond == null) { + if (other.mSecond != null) { + return false; + } + } else if (!mSecond.equals(other.mSecond)) { + return false; + } + return true; + } +} -- cgit v1.1