diff --git a/common/src/main/java/org/conscrypt/AbstractConscryptEngine.java b/common/src/main/java/org/conscrypt/AbstractConscryptEngine.java index 0f1354a93..b9f1fb5ed 100644 --- a/common/src/main/java/org/conscrypt/AbstractConscryptEngine.java +++ b/common/src/main/java/org/conscrypt/AbstractConscryptEngine.java @@ -152,6 +152,14 @@ public abstract SSLEngineResult wrap( @SuppressWarnings("MissingOverride") // For compiling pre Java 9. public abstract String getHandshakeApplicationProtocol(); + public abstract void setEchParameters(EchParameters parameters); + + public abstract EchParameters getEchParameters(); + + public abstract String getEchNameOverride(); + + public abstract boolean echAccepted(); + /** * Sets an application-provided ALPN protocol selector. If provided, this will override * the list of protocols set by {@link #setApplicationProtocols(String[])}. diff --git a/common/src/main/java/org/conscrypt/AbstractConscryptSocket.java b/common/src/main/java/org/conscrypt/AbstractConscryptSocket.java index b177e9d61..6f396b3d4 100644 --- a/common/src/main/java/org/conscrypt/AbstractConscryptSocket.java +++ b/common/src/main/java/org/conscrypt/AbstractConscryptSocket.java @@ -738,4 +738,12 @@ private boolean isDelegating() { */ abstract byte[] exportKeyingMaterial(String label, byte[] context, int length) throws SSLException; + + public abstract void setEchParameters(EchParameters parameters); + + public abstract EchParameters getEchParameters(); + + public abstract String getEchNameOverride(); + + public abstract boolean echAccepted(); } diff --git a/common/src/main/java/org/conscrypt/Conscrypt.java b/common/src/main/java/org/conscrypt/Conscrypt.java index 0450874bc..abfddaaa7 100644 --- a/common/src/main/java/org/conscrypt/Conscrypt.java +++ b/common/src/main/java/org/conscrypt/Conscrypt.java @@ -15,8 +15,10 @@ */ package org.conscrypt; +import org.conscrypt.com.android.net.module.util.DnsPacket; import org.conscrypt.io.IoUtils; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.InvocationTargetException; @@ -533,6 +535,47 @@ public static byte[] exportKeyingMaterial( return toConscrypt(socket).exportKeyingMaterial(label, context, length); } + /** + * Casts a socket to a Conscrypt socket if possible, and sets the parameters + * required to configure ECH for that socket. + * Throws an IllegalArgumentException if the socket is not a ConscryptSocket + * @param socket the socket (instance of ConscryptSocket) + * @param parameters parameters required to configure ECH + */ + public static void setEchParameters(SSLSocket socket, EchParameters parameters) { + toConscrypt(socket).setEchParameters(parameters); + } + + /** + * Casts a socket to a Conscrypt socket if possible, and returns the parameters + * used to configure ECH for that socket. + * Throws an IllegalArgumentException if the socket is not a ConscryptSocket + * @param socket the socket (instance of ConscryptSocket) + */ + public static EchParameters getEchParameters(SSLSocket socket) { + return toConscrypt(socket).getEchParameters(); + } + + /** + * Casts a socket to a Conscrypt socket if possible, and returns the string used + * to replace the hostname as the public name. + * Throws an IllegalArgumentException if the socket is not a ConscryptSocket + * @param socket the socket (instance of ConscryptSocket) + */ + public static String getEchNameOverride(SSLSocket socket) { + return toConscrypt(socket).getEchNameOverride(); + } + + /** + * Casts a socket to a Conscrypt socket if possible, and returns whether the native + * SSL/crypto implementation detects that the connection supports ECH. + * Throws an IllegalArgumentException if the socket is not a ConscryptSocket + * @param socket the socket (instance of ConscryptSocket) + */ + public static boolean echAccepted(SSLSocket socket) { + return toConscrypt(socket).echAccepted(); + } + /** * Indicates whether the given {@link SSLEngine} was created by this distribution of Conscrypt. */ @@ -791,6 +834,148 @@ public static byte[] exportKeyingMaterial( return toConscrypt(engine).exportKeyingMaterial(label, context, length); } + /** + * This method enables or disables Encrypted Client Hello (ECH) GREASE. + * + * @param engine the engine + * @param enabled Whether to enable TLSv1.3 ECH GREASE + * + * @see TLS Encrypted Client Hello 6.2. GREASE ECH + */ + + public static void setEchParameters(SSLEngine engine, EchParameters parameters) { + toConscrypt(engine).setEchParameters(parameters); + } + + public static EchParameters getEchParameters(SSLEngine engine) { + return toConscrypt(engine).getEchParameters(); + } + + public static String getEchNameOverride(SSLEngine engine) { + return toConscrypt(engine).getEchNameOverride(); + } + + public static boolean echAccepted(SSLEngine engine) { + return toConscrypt(engine).echAccepted(); + } + + + /** + * < Max RR value size, as given to API + */ + private static int ECH_MAX_RRVALUE_LEN = 2000; + /** + * < Max for an ECHConfig extension + */ + private static int ECH_MAX_ECHCONFIGEXT_LEN = 100; + /** + * < just for a sanity check + */ + private static int ECH_MIN_ECHCONFIG_LEN = 32; + /** + * < for a sanity check + */ + private static int ECH_MAX_ECHCONFIG_LEN = ECH_MAX_RRVALUE_LEN; + + /** + * One or more catenated binary ECHConfigs + */ + public static int ECH_FMT_BIN = 1; + /** + * < presentation form of HTTPSSVC + */ + public static int ECH_FMT_HTTPSSVC = 4; + /** + * the wire-format code for ECH within an SVCB or HTTPS RData + */ + private static int ECH_PCODE_ECH = 0x0005; + + /** + * Decode SVCB/HTTPS RR value provided as binary or ascii-hex. + *

+ * The rrval may be the catenation of multiple encoded ECHConfigs. + * We internally try decode and handle those and (later) + * use whichever is relevant/best. + *

+ * Note that we "succeed" even if there is no ECHConfigs in the input - some + * callers might download the RR from DNS and pass it here without looking + * inside, and there are valid uses of such RRs. The caller can check though + * using the num_echs output. + * + * @param rrval is the binary encoded RData + * @return is 1 for success, error otherwise + */ + public static byte[] getEchConfigListFromDnsRR(byte[] rrval) { + int rv = 0; + int binlen = 0; /* the RData */ + byte[] binbuf = null; + int pos = 0; + int remaining = rrval.length;; + String dnsname = null; + int plen = 0; + boolean done = false; + + /* + * skip 2 octet priority and TargetName as those are the + * application's responsibility, not the library's + */ + if (remaining <= 2) { + return null; + } + pos += 2; + remaining -= 2; + pos++; + int clen = DnsPacket.byteToUnsignedInt(rrval[pos]); + ByteArrayOutputStream thename = new ByteArrayOutputStream(); + if (clen == 0) { + // special case - return "." as name + thename.write('.'); + rv = 1; + } + while (clen != 0) { + if (clen > remaining) { + rv = 1; + break; + } + for (int i =pos; i < clen; i++) { + thename.write(DnsPacket.byteToUnsignedInt(rrval[pos + i])); + } + thename.write('.'); + pos += clen; + remaining -= clen + 1; + clen = DnsPacket.byteToUnsignedInt(rrval[pos]); + } + if (rv != 1) { + return null; + } + + int echStart = 0; + while (!done && remaining >= 4) { + int pcode = (rrval[pos] << 8) + rrval[pos + 1]; + pos += 2; + plen = (rrval[pos] << 8) + rrval[pos + 1]; + pos += 2; + remaining -= 4; + if (pcode == ECH_PCODE_ECH) { + echStart = pos; + done = true; + } + if (plen != 0 && plen <= remaining) { + pos += plen; + remaining -= plen; + } + } + if (!done) { + return null; + } + if (plen <=0) { + return null; + } + byte[] ret = new byte[plen]; + System.arraycopy(rrval, echStart, ret, 0, plen); + return ret; + } + /** * Indicates whether the given {@link TrustManager} was created by this distribution of * Conscrypt. diff --git a/common/src/main/java/org/conscrypt/ConscryptEngine.java b/common/src/main/java/org/conscrypt/ConscryptEngine.java index 818fa93cc..ec06943e5 100644 --- a/common/src/main/java/org/conscrypt/ConscryptEngine.java +++ b/common/src/main/java/org/conscrypt/ConscryptEngine.java @@ -398,6 +398,24 @@ public int getPeerPort() { return peerInfoProvider.getPort(); } + public void setEchParameters(EchParameters parameters) { + sslParameters.setEchParameters(parameters); + } + + public EchParameters getEchParameters() { + return sslParameters.getEchParameters(); + } + + @Override + public String getEchNameOverride() { + return ssl.getEchNameOverride(); + } + + @Override + public boolean echAccepted() { + return ssl.echAccepted(); + } + @Override public void beginHandshake() throws SSLException { synchronized (ssl) { diff --git a/common/src/main/java/org/conscrypt/ConscryptEngineSocket.java b/common/src/main/java/org/conscrypt/ConscryptEngineSocket.java index c2db73e59..458505b49 100644 --- a/common/src/main/java/org/conscrypt/ConscryptEngineSocket.java +++ b/common/src/main/java/org/conscrypt/ConscryptEngineSocket.java @@ -473,6 +473,26 @@ byte[] exportKeyingMaterial(String label, byte[] context, int length) throws SSL return engine.exportKeyingMaterial(label, context, length); } + @Override + public void setEchParameters(EchParameters parameters) { + engine.setEchParameters(parameters); + } + + @Override + public EchParameters getEchParameters() { + return engine.getEchParameters(); + } + + @Override + public String getEchNameOverride() { + return engine.getEchNameOverride(); + } + + @Override + public boolean echAccepted() { + return engine.echAccepted(); + } + @Override public final boolean getUseClientMode() { return engine.getUseClientMode(); diff --git a/common/src/main/java/org/conscrypt/ConscryptFileDescriptorSocket.java b/common/src/main/java/org/conscrypt/ConscryptFileDescriptorSocket.java index 1f7940ecf..fc3b58062 100644 --- a/common/src/main/java/org/conscrypt/ConscryptFileDescriptorSocket.java +++ b/common/src/main/java/org/conscrypt/ConscryptFileDescriptorSocket.java @@ -895,6 +895,27 @@ byte[] exportKeyingMaterial(String label, byte[] context, int length) throws SSL return ssl.exportKeyingMaterial(label, context, length); } + + @Override + public void setEchParameters(EchParameters parameters) { + sslParameters.setEchParameters(parameters); + } + + @Override + public EchParameters getEchParameters() { + return sslParameters.getEchParameters(); + } + + @Override + public String getEchNameOverride() { + return ssl.getEchNameOverride(); + } + + @Override + public boolean echAccepted() { + return ssl.echAccepted(); + } + @Override public final boolean getUseClientMode() { return sslParameters.getUseClientMode(); diff --git a/common/src/main/java/org/conscrypt/EchParameters.java b/common/src/main/java/org/conscrypt/EchParameters.java new file mode 100644 index 000000000..9c581569d --- /dev/null +++ b/common/src/main/java/org/conscrypt/EchParameters.java @@ -0,0 +1,29 @@ +package org.conscrypt; + +public class EchParameters { + + public boolean useEchGrease; + + public byte[] configList; + + public EchParameters() { + this.useEchGrease = false; + this.configList = null; + } + + public EchParameters(boolean useEchGrease) { + this.useEchGrease = useEchGrease; + this.configList = null; + } + + public EchParameters(byte[] configList) { + this.useEchGrease = false; + this.configList = configList; + } + + public EchParameters(boolean useEchGrease, byte[] configList) { + this.useEchGrease = useEchGrease; + this.configList = configList; + } + +} diff --git a/common/src/main/java/org/conscrypt/EchRejectedException.java b/common/src/main/java/org/conscrypt/EchRejectedException.java new file mode 100644 index 000000000..af91ce859 --- /dev/null +++ b/common/src/main/java/org/conscrypt/EchRejectedException.java @@ -0,0 +1,17 @@ +package org.conscrypt; + +import javax.net.ssl.SSLHandshakeException; + +/** + * The server rejected the ECH Config List, and might have supplied an ECH + * Retry Config. + * + * @see NativeCrypto#SSL_get0_ech_retry_configs(long, NativeSsl) + */ +public class EchRejectedException extends SSLHandshakeException { + private static final long serialVersionUID = 98723498273473923L; + + EchRejectedException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/common/src/main/java/org/conscrypt/Java8EngineWrapper.java b/common/src/main/java/org/conscrypt/Java8EngineWrapper.java index 5cf135d4f..1abd1ea04 100644 --- a/common/src/main/java/org/conscrypt/Java8EngineWrapper.java +++ b/common/src/main/java/org/conscrypt/Java8EngineWrapper.java @@ -116,6 +116,24 @@ public int getPeerPort() { return delegate.getPeerPort(); } + public void setEchParameters(EchParameters parameters) { + delegate.setEchParameters(parameters); + } + + public EchParameters getEchParameters() { + return delegate.getEchParameters(); + } + + @Override + public String getEchNameOverride() { + return delegate.getEchNameOverride(); + } + + @Override + public boolean echAccepted() { + return delegate.echAccepted(); + } + @Override public void beginHandshake() throws SSLException { delegate.beginHandshake(); diff --git a/common/src/main/java/org/conscrypt/NativeSsl.java b/common/src/main/java/org/conscrypt/NativeSsl.java index 406c68f2c..ea2d56611 100644 --- a/common/src/main/java/org/conscrypt/NativeSsl.java +++ b/common/src/main/java/org/conscrypt/NativeSsl.java @@ -274,6 +274,14 @@ byte[] getTlsChannelId() throws SSLException { return NativeCrypto.SSL_get_tls_channel_id(ssl, this); } + String getEchNameOverride() { + return NativeCrypto.SSL_get0_ech_name_override(ssl, this); + } + + boolean echAccepted() { + return NativeCrypto.SSL_ech_accepted(ssl, this); + } + void initialize(String hostname, OpenSSLKey channelIdPrivateKey) throws IOException { boolean enableSessionCreation = parameters.getEnableSessionCreation(); if (!enableSessionCreation) { @@ -293,6 +301,11 @@ void initialize(String hostname, OpenSSLKey channelIdPrivateKey) throws IOExcept if (parameters.isCTVerificationEnabled(hostname)) { NativeCrypto.SSL_enable_signed_cert_timestamps(ssl, this); } + NativeCrypto.SSL_set_enable_ech_grease(ssl, this, parameters.getEchParameters().useEchGrease); + if (parameters.getEchParameters().configList != null + && !NativeCrypto.SSL_set1_ech_config_list(ssl, this, parameters.getEchParameters().configList)) { + throw new SSLHandshakeException("Error setting ECHConfigList"); + } } else { NativeCrypto.SSL_set_accept_state(ssl, this); diff --git a/common/src/main/java/org/conscrypt/SSLParametersImpl.java b/common/src/main/java/org/conscrypt/SSLParametersImpl.java index f2056f2bd..f51e6b174 100644 --- a/common/src/main/java/org/conscrypt/SSLParametersImpl.java +++ b/common/src/main/java/org/conscrypt/SSLParametersImpl.java @@ -109,6 +109,8 @@ final class SSLParametersImpl implements Cloneable { boolean useSessionTickets; private Boolean useSni; + private EchParameters echParameters; + /** * Whether the TLS Channel ID extension is enabled. This field is * server-side only. @@ -235,6 +237,7 @@ private SSLParametersImpl(ClientSessionContext clientSessionContext, this.useSessionTickets = sslParams.useSessionTickets; this.useSni = sslParams.useSni; this.channelIdEnabled = sslParams.channelIdEnabled; + this.echParameters = sslParams.echParameters; } /** @@ -474,6 +477,17 @@ boolean getUseSni() { return useSni != null ? useSni : isSniEnabledByDefault(); } + /* + * Includes parameters for supporting ECH with this SSL connection + */ + void setEchParameters(EchParameters parameters) { + this.echParameters = parameters; + } + + EchParameters getEchParameters() { + return echParameters; + } + /* * For testing only. */ diff --git a/common/src/main/java/org/conscrypt/SSLUtils.java b/common/src/main/java/org/conscrypt/SSLUtils.java index 39eb05a42..c29c5d42e 100644 --- a/common/src/main/java/org/conscrypt/SSLUtils.java +++ b/common/src/main/java/org/conscrypt/SSLUtils.java @@ -352,6 +352,11 @@ static SSLHandshakeException toSSLHandshakeException(Throwable e) { return (SSLHandshakeException) e; } + if (e.getMessage().contains(":ECH_REJECTED ")) { + // TODO should this be implemented in boringssl? + return (SSLHandshakeException) new EchRejectedException(e.getMessage()).initCause(e); + } + return (SSLHandshakeException) new SSLHandshakeException(e.getMessage()).initCause(e); } diff --git a/common/src/main/java/org/conscrypt/com/android/net/module/util/DnsPacket.java b/common/src/main/java/org/conscrypt/com/android/net/module/util/DnsPacket.java new file mode 100644 index 000000000..0052fa382 --- /dev/null +++ b/common/src/main/java/org/conscrypt/com/android/net/module/util/DnsPacket.java @@ -0,0 +1,253 @@ +package org.conscrypt.com.android.net.module.util; + +/* + * Copyright (C) 2019 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. + */ + +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.text.DecimalFormat; +import java.text.FieldPosition; +import java.util.ArrayList; +import java.util.List; + +/** + * Defines basic data for DNS protocol based on RFC 1035. + * Subclasses create the specific format used in DNS packet. + * + * @hide + */ + +/** + * @see original source + * + * Some local changes have been made to provide additional utility + */ + +public abstract class DnsPacket { + /** + * Thrown when parsing packet failed. + */ + public static class ParseException extends RuntimeException { + public String reason; + public ParseException(String reason) { + super(reason); + this.reason = reason; + } + public ParseException(String reason, Throwable cause) { + super(reason, cause); + this.reason = reason; + } + } + /** + * DNS header for DNS protocol based on RFC 1035. + */ + public class DnsHeader { + private static final String TAG = "DnsHeader"; + public final int id; + public final int flags; + public final int rcode; + private final int[] mRecordCount; + /** + * Create a new DnsHeader from a positioned ByteBuffer. + * + * The ByteBuffer must be in network byte order (which is the default). + * Reads the passed ByteBuffer from its current position and decodes a DNS header. + * When this constructor returns, the reading position of the ByteBuffer has been + * advanced to the end of the DNS header record. + * This is meant to chain with other methods reading a DNS response in sequence. + */ + DnsHeader(ByteBuffer buf) throws BufferUnderflowException { + id = Short.toUnsignedInt(buf.getShort()); + flags = Short.toUnsignedInt(buf.getShort()); + rcode = flags & 0xF; + mRecordCount = new int[NUM_SECTIONS]; + for (int i = 0; i < NUM_SECTIONS; ++i) { + mRecordCount[i] = Short.toUnsignedInt(buf.getShort()); + } + } + /** + * Get record count by type. + */ + public int getRecordCount(int type) { + return mRecordCount[type]; + } + } + /** + * Superclass for DNS questions and DNS resource records. + * + * DNS questions (No TTL/RDATA) + * DNS resource records (With TTL/RDATA) + */ + public class DnsRecord { + private static final int MAXNAMESIZE = 255; + private static final int MAXLABELSIZE = 63; + private static final int MAXLABELCOUNT = 128; + public static final int NAME_NORMAL = 0; + public static final int NAME_COMPRESSION = 0xC0; + private final DecimalFormat mByteFormat = new DecimalFormat(); + private final FieldPosition mPos = new FieldPosition(0); + private static final String TAG = "DnsRecord"; + public final String dName; + public final int nsType; + public final int nsClass; + public final long ttl; + private final byte[] mRdata; + /** + * Create a new DnsRecord from a positioned ByteBuffer. + * + * Reads the passed ByteBuffer from its current position and decodes a DNS record. + * When this constructor returns, the reading position of the ByteBuffer has been + * advanced to the end of the DNS header record. + * This is meant to chain with other methods reading a DNS response in sequence. + * + * @param ByteBuffer input of record, must be in network byte order + * (which is the default). + */ + DnsRecord(int recordType, ByteBuffer buf) + throws BufferUnderflowException, ParseException { + dName = parseName(buf, 0 /* Parse depth */); + if (dName.length() > MAXNAMESIZE) { + throw new ParseException( + "Parse name fail, name size is too long: " + dName.length()); + } + nsType = Short.toUnsignedInt(buf.getShort()); + nsClass = Short.toUnsignedInt(buf.getShort()); + if (recordType != QDSECTION) { + ttl = Integer.toUnsignedLong(buf.getInt()); + final int length = Short.toUnsignedInt(buf.getShort()); + mRdata = new byte[length]; + buf.get(mRdata); + } else { + ttl = 0; + mRdata = null; + } + } + /** + * Get a copy of rdata. + */ + public byte[] getRR() { + return (mRdata == null) ? null : mRdata.clone(); + } + /** + * Convert label from {@code byte[]} to {@code String} + * + * Follows the same conversion rules of the native code (ns_name.c in libc) + */ + private String labelToString(byte[] label) { + final StringBuffer sb = new StringBuffer(); + for (int i = 0; i < label.length; ++i) { + int b = Byte.toUnsignedInt(label[i]); + // Control characters and non-ASCII characters. + if (b <= 0x20 || b >= 0x7f) { + // Append the byte as an escaped decimal number, e.g., "\19" for 0x13. + sb.append('\\'); + mByteFormat.format(b, sb, mPos); + } else if (b == '"' || b == '.' || b == ';' || b == '\\' + || b == '(' || b == ')' || b == '@' || b == '$') { + // Append the byte as an escaped character, e.g., "\:" for 0x3a. + sb.append('\\'); + sb.append((char) b); + } else { + // Append the byte as a character, e.g., "a" for 0x61. + sb.append((char) b); + } + } + return sb.toString(); + } + private String parseName(ByteBuffer buf, int depth) throws + BufferUnderflowException, ParseException { + if (depth > MAXLABELCOUNT) { + throw new ParseException("Failed to parse name, too many labels"); + } + final int len = Byte.toUnsignedInt(buf.get()); + final int mask = len & NAME_COMPRESSION; + if (0 == len) { + return ""; + } else if (mask != NAME_NORMAL && mask != NAME_COMPRESSION) { + throw new ParseException("Parse name fail, bad label type"); + } else if (mask == NAME_COMPRESSION) { + // Name compression based on RFC 1035 - 4.1.4 Message compression + final int offset = ((len & ~NAME_COMPRESSION) << 8) + Byte.toUnsignedInt(buf.get()); + final int oldPos = buf.position(); + if (offset >= oldPos - 2) { + throw new ParseException("Parse compression name fail, invalid compression"); + } + buf.position(offset); + final String pointed = parseName(buf, depth + 1); + buf.position(oldPos); + return pointed; + } else { + final byte[] label = new byte[len]; + buf.get(label); + final String head = labelToString(label); + if (head.length() > MAXLABELSIZE) { + throw new ParseException("Parse name fail, invalid label length"); + } + final String tail = parseName(buf, depth + 1); + // return TextUtils.isEmpty(tail) ? head : head + "." + tail; + return (tail == null || tail.isEmpty()) ? head : head + "." + tail; + } + } + } + + /** {@link Byte#toUnsignedInt(byte)} was added to Android in API 26. */ + public static int byteToUnsignedInt(byte b) { + return b & 255; + } + + /** {@link Short#toUnsignedInt(short)} was added to Android in API 26. */ + public static int shortToUnsignedInt(short s) { + return s & '\uffff'; + } + + /** {@link Integer#toUnsignedLong(int)} was added to Android in API 26. */ + public static long integerToUnsignedLong(int i) { + return (long) i & 4294967295L; + } + + public static final int QDSECTION = 0; + public static final int ANSECTION = 1; + public static final int NSSECTION = 2; + public static final int ARSECTION = 3; + private static final int NUM_SECTIONS = ARSECTION + 1; + private static final String TAG = DnsPacket.class.getSimpleName(); + protected final DnsHeader mHeader; + protected final List[] mRecords; + protected DnsPacket(byte[] data) throws ParseException { + if (null == data) throw new ParseException("Parse header failed, null input data"); + final ByteBuffer buffer; + try { + buffer = ByteBuffer.wrap(data); + mHeader = new DnsHeader(buffer); + } catch (BufferUnderflowException e) { + throw new ParseException("Parse Header fail, bad input data", e); + } + mRecords = new ArrayList[NUM_SECTIONS]; + for (int i = 0; i < NUM_SECTIONS; ++i) { + final int count = mHeader.getRecordCount(i); + if (count > 0) { + mRecords[i] = new ArrayList(count); + } + for (int j = 0; j < count; ++j) { + try { + mRecords[i].add(new DnsRecord(i, buffer)); + } catch (BufferUnderflowException e) { + throw new ParseException("Parse record fail", e); + } + } + } + } +} diff --git a/openjdk/src/test/java/org/conscrypt/ConscryptOpenJdkSuite.java b/openjdk/src/test/java/org/conscrypt/ConscryptOpenJdkSuite.java index e338a857f..0abab1201 100644 --- a/openjdk/src/test/java/org/conscrypt/ConscryptOpenJdkSuite.java +++ b/openjdk/src/test/java/org/conscrypt/ConscryptOpenJdkSuite.java @@ -180,6 +180,8 @@ TrustManagerFactoryTest.class, VeryBasicHttpServerTest.class, X509KeyManagerTest.class, + // ech tests + EchInteropTest.class }) public class ConscryptOpenJdkSuite { @BeforeClass diff --git a/openjdk/src/test/java/org/conscrypt/EchInteropTest.java b/openjdk/src/test/java/org/conscrypt/EchInteropTest.java new file mode 100644 index 000000000..f902def35 --- /dev/null +++ b/openjdk/src/test/java/org/conscrypt/EchInteropTest.java @@ -0,0 +1,521 @@ +/* + * Copyright (C) 2021 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 org.conscrypt; + +import org.conscrypt.com.android.net.module.util.DnsPacket; +import org.junit.*; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.io.IOException; +import java.net.*; +import java.security.NoSuchAlgorithmException; +import java.security.Security; +import java.util.Arrays; +import java.util.Hashtable; +import java.util.List; + +import javax.naming.Context; +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.directory.Attribute; +import javax.naming.directory.Attributes; +import javax.naming.directory.DirContext; +import javax.naming.directory.InitialDirContext; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLHandshakeException; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +@RunWith(JUnit4.class) +public class EchInteropTest { + + private static final int TIMEOUT_MILLISECONDS = 30000; + + private static String[] hostsNonEch = { + "www.yandex.ru", + "en.wikipedia.org", + // TEMP - causes prefetch exception "web.wechat.com", + "mirrors.kernel.org", + "www.google.com", + "check-tls.akamaized.net", // uses SNI + "duckduckgo.com", // TLS 1.3 + "deb.debian.org", // TLS 1.3 Fastly + "tls13.1d.pw", // TLS 1.3 only, no ECH + "cloudflareresearch.com", // no ECH + + "enabled.tls13.com", // no longer supports ECH + "crypto.cloudflare.com", // no longer supports ECH + }; + private static String[] hostsEch = { + "openstreetmap.org", // now supports ECH + "cloudflare-esni.com", // now supports ECH + + // TEMP - commented out to avoid issues with unique formatting + //"draft-13.esni.defo.ie:8413", // OpenSSL s_server + //"draft-13.esni.defo.ie:8414", // OpenSSL s_server, likely forces HRR as it only likes P-384 for TLS =09 + // TEMP - causes prefetch exception "draft-13.esni.defo.ie:9413", + //"draft-13.esni.defo.ie:10413", // nginx + //"draft-13.esni.defo.ie:11413", // apache + //"draft-13.esni.defo.ie:12413", // haproxy shared mode (haproxy terminates TLS) + //"draft-13.esni.defo.ie:12414", // haproxy split mode (haproxy only decrypts ECH) + }; + + private static String[] hosts = new String[hostsNonEch.length + hostsEch.length]; + + @BeforeClass + public static void setUp() throws NoSuchAlgorithmException { + System.out.println("========== SETUP BEGIN ==============================================================="); + Security.insertProviderAt(Conscrypt.newProvider(), 1); + assertTrue(Conscrypt.isAvailable()); + assertTrue(Conscrypt.isConscrypt(SSLContext.getInstance("TLSv1.3"))); + System.arraycopy(hostsNonEch, 0, hosts, 0, hostsNonEch.length); + System.arraycopy(hostsEch, 0, hosts, hostsNonEch.length, hostsEch.length); + prefetchDns(hosts); + System.out.println("========== SETUP END ================================================================="); + } + + @AfterClass + public static void tearDown() throws NoSuchAlgorithmException { + System.out.println("========== TEARDOWN BEGIN ============================================================"); + Security.removeProvider("Conscrypt"); + assertFalse(Conscrypt.isConscrypt(SSLContext.getInstance("TLSv1"))); + System.out.println("========== TEARDOWN END =============================================================="); + } + + @Test + public void testConnectSocket() throws IOException { + boolean hostFailed = false; + for (String h : hosts) { + System.out.println(" = TEST CONNECT SOCKET FOR " + h); + String[] hostPort = h.split(":"); + String host = hostPort[0]; + int port = 443; + if (hostPort.length == 2) { + port = Integer.parseInt(hostPort[1]); + } + + SSLSocketFactory sslSocketFactory = (SSLSocketFactory) SSLSocketFactory.getDefault(); + assertTrue(Conscrypt.isConscrypt(sslSocketFactory)); + SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(host, port); + assertTrue(Conscrypt.isConscrypt(sslSocket)); + boolean setUpEch = false; + try { + byte[] echConfigList = getEchConfigListFromDns(h); + if (echConfigList != null) { + Conscrypt.setEchParameters(sslSocket, new EchParameters(true, echConfigList)); + System.out.println("ENABLED ECH GREASE AND CONFIG LIST"); + setUpEch = true; + } else { + Conscrypt.setEchParameters(sslSocket, new EchParameters(true)); + System.out.println("ENABLED ECH GREASE"); + } + } catch (NamingException e) { + System.out.println("GET CONFIG LIST THREW EXCEPTION FOR " + host); + System.out.println(e.getMessage()); + hostFailed = true; + continue; + } + sslSocket.setSoTimeout(TIMEOUT_MILLISECONDS); + try { + sslSocket.startHandshake(); + System.out.println("HANDSHAKE OK FOR " + host); + } catch (Exception e) { + System.out.println("HANDSHAKE THREW EXCEPTION FOR " + host); + System.out.println(e.getMessage()); + } + assertTrue(sslSocket.isConnected()); + AbstractConscryptSocket abstractConscryptSocket = (AbstractConscryptSocket) sslSocket; + if (setUpEch) { + assertTrue(abstractConscryptSocket.echAccepted()); + } else { + assertFalse(abstractConscryptSocket.echAccepted()); + } + sslSocket.close(); + } + System.out.println("TEST FAILED FOR ONE OR MORE HOSTS: " + hostFailed); + assertFalse(hostFailed); + } + + @Rule + public ExpectedException echRejectedExceptionRule = ExpectedException.none(); + + @Test + public void testEchConfigOnNonEchHosts() throws IOException { + for (String h : hostsNonEch) { + System.out.println(" = TEST ECH CONFIG ON NON ECH HOSTS FOR " + h); + String[] hostPort = h.split(":"); + String host = hostPort[0]; + int port = 443; + if (hostPort.length == 2) { + port = Integer.parseInt(hostPort[1]); + } + + SSLSocketFactory sslSocketFactory = (SSLSocketFactory) SSLSocketFactory.getDefault(); + assertTrue(Conscrypt.isConscrypt(sslSocketFactory)); + SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(host, port); + assertTrue(Conscrypt.isConscrypt(sslSocket)); + + // load saved ech config with the expecation that the key mismatch will cause rejection + byte[] echConfigList = TestUtils.readTestFile("draft-13.esni.defo.ie_12414-ech-config-list.bin"); + Conscrypt.setEchParameters(sslSocket, new EchParameters(echConfigList)); + + echRejectedExceptionRule.expect(SSLHandshakeException.class); + echRejectedExceptionRule.expectMessage("ECH_REJECTED"); + sslSocket.setSoTimeout(TIMEOUT_MILLISECONDS); + sslSocket.startHandshake(); + assertTrue(sslSocket.isConnected()); + AbstractConscryptSocket abstractConscryptSocket = (AbstractConscryptSocket) sslSocket; + assertTrue(abstractConscryptSocket.echAccepted()); + sslSocket.close(); + } + } + + @Test + public void testConnectHttpsURLConnection() throws IOException { + boolean hostFailed = false; + for (String host : hosts) { + URL url = new URL("https://" + host); + System.out.println(" = TEST CONNECT HTTPS URL CONNECTION FOR " + url); + HttpsURLConnection connection = (HttpsURLConnection) url.openConnection(); + SSLSocketFactory delegateSocketFactory = connection.getSSLSocketFactory(); + assertTrue(Conscrypt.isConscrypt(delegateSocketFactory)); + try { + byte[] echConfigList = getEchConfigListFromDns(host); + if (echConfigList != null) { + connection.setSSLSocketFactory(new EchSSLSocketFactory(delegateSocketFactory, echConfigList)); + System.out.println("CREATED SOCKET FACTORY WITH ECH GREASE AND CONFIG LIST"); + } else { + connection.setSSLSocketFactory(new EchSSLSocketFactory(delegateSocketFactory, true)); + System.out.println("CREATED SOCKET FACTORY WITH ECH GREASE"); + } + } catch (NamingException e) { + System.out.println("GET CONFIG LIST THREW EXCEPTION FOR " + host); + System.out.println(e.getMessage()); + hostFailed = true; + continue; + } + // Cloudflare will return 403 Forbidden (error code 1010) unless a User Agent is set :-| + connection.setRequestProperty("User-Agent", "Conscrypt EchInteropTest"); + connection.setConnectTimeout(0); // blocking connect with TCP timeout + connection.setReadTimeout(0); + + int responseCode = -1; + String contentType = "error"; + String cipherSuite = "error"; + try { + responseCode = connection.getResponseCode(); + contentType = connection.getContentType().split(";")[0]; + cipherSuite = connection.getCipherSuite(); + System.out.println("GET CONNECTION INFO OK FOR " + url + " -> " + responseCode + " | " + contentType + " | " + cipherSuite); + } catch (Exception e) { + System.out.println("GET CONNECTION INFO THREW EXCEPTION FOR " + url); + System.out.println(e.getMessage()); + } + connection.getContent(); + assertEquals(200, responseCode); + String[] options = {"text/html", "text/plain"}; + List contentTypes = Arrays.asList(options); + // some defo urls have different content types, is this an error? + assertTrue(contentTypes.contains(contentType)); + assertTrue(cipherSuite.startsWith("TLS")); + connection.disconnect(); + } + System.out.println("TEST FAILED FOR ONE OR MORE HOSTS: " + hostFailed); + assertFalse(hostFailed); + } + + @Test + public void testParseDnsAndConnect() throws IOException, NamingException { + for (String h : hosts) { + System.out.println(" = TEST PARSE DNS AND CONNECT FOR " + h); + String[] hostPort = h.split(":"); + String host = hostPort[0]; + int port = 443; + if (hostPort.length > 1) { + port = Integer.parseInt(hostPort[1]); + } + + byte[] echConfigList = null; + try { + echConfigList = getEchConfigListFromDns(h); + System.out.println("ECH CONFIG LIST OK FOR " + h); + } catch (Exception e) { + System.out.println("ECH CONFIG LIST THREW EXCEPTION FOR " + h); + System.out.println(e.getMessage()); + } + + if (echConfigList != null) { + assertEquals("length should match inline declaration", + echConfigList[1] + 2, // leading 0x00 and length bytes + echConfigList.length + ); + } else { + System.out.println("NO ECH CONFIG LIST FOUND IN DNS FOR " + h); + } + + SSLSocketFactory sslSocketFactory = (SSLSocketFactory) SSLSocketFactory.getDefault(); + assertTrue(Conscrypt.isConscrypt(sslSocketFactory)); + SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(host, port); + assertTrue(Conscrypt.isConscrypt(sslSocket)); + if (echConfigList != null) { + Conscrypt.setEchParameters(sslSocket, new EchParameters(true, echConfigList)); + System.out.println("ENABLED ECH GREASE AND CONFIG LIST"); + } else { + Conscrypt.setEchParameters(sslSocket, new EchParameters(true)); + System.out.println("ENABLED ECH GREASE"); + } + sslSocket.setSoTimeout(TIMEOUT_MILLISECONDS); + sslSocket.startHandshake(); + assertTrue(sslSocket.isConnected()); + AbstractConscryptSocket abstractConscryptSocket = (AbstractConscryptSocket) sslSocket; + System.out.println("ECHACCEPTED SET TO " + abstractConscryptSocket.echAccepted() + " FOR " + host); + if (echConfigList != null) { + assertTrue(abstractConscryptSocket.echAccepted()); + } else { + assertFalse(abstractConscryptSocket.echAccepted()); + } + sslSocket.close(); + } + } + + @Test + public void testParseDnsFromFiles() { + for (String hostString : hosts) { + System.out.println(" = TEST PARSE DNS FROM FILES FOR " + hostString); + String[] h = hostString.split(":"); + String host = h[0]; + if (h.length > 1) { + if (!"443".equals(h[1])) { + host = "_" + h[1] + "._https." + h[0]; // query for non-standard port + } + } + try { + byte[] dnsAnswer = TestUtils.readTestFile(host + ".bin"); + echPbuf("DNS ANSWER", dnsAnswer); + try { + DnsEchAnswer dnsEchAnswer = new DnsEchAnswer(dnsAnswer); + if (dnsEchAnswer.getEchConfigList() == null) { + System.out.println("ECH CONFIG LIST NULL FOR " + host); + } else { + echPbuf("ECH CONFIG LIST", dnsEchAnswer.getEchConfigList()); + } + } catch (DnsPacket.ParseException e) { + e.printStackTrace(); + } + } catch (IOException e) { + e.printStackTrace(); + } + + } + } + + static byte[] getEchConfigListFromDns(String hostPort) throws NamingException { + String[] h = hostPort.split(":"); + String dnshost = h[0]; + if (h.length > 1 && !"443".equals(h[1])) { + dnshost = "_" + h[1] + "._https." + h[0]; // query for non-standard port + } + + byte[] echConfigList = null; + Hashtable envProps = + new Hashtable(); + envProps.put(Context.INITIAL_CONTEXT_FACTORY, + "com.sun.jndi.dns.DnsContextFactory"); + DirContext dnsContext = new InitialDirContext(envProps); + Attributes dnsEntries = dnsContext.getAttributes(dnshost, new String[]{"65"}); + NamingEnumeration ae = dnsEntries.getAll(); + while (ae.hasMore()) { + Attribute attr = (Attribute) ae.next(); + // only parse HTTPS/65 (previous included SVCB/64, but why?) + for (int i = 0; i < attr.size(); i++) { + Object rr = attr.get(i); + if (!(rr instanceof byte[])) { + continue; + } else { + echConfigList = Conscrypt.getEchConfigListFromDnsRR((byte[]) rr); + } + } + } + ae.close(); + return echConfigList; + } + + class DnsEchAnswer extends DnsPacket { + private static final String TAG = "DnsResolver.DnsAddressAnswer"; + private static final boolean DBG = true; + + /** + * Service Binding [draft-ietf-dnsop-svcb-https-00] + */ + public static final int TYPE_SVCB = 64; + + /** + * HTTPS Binding [draft-ietf-dnsop-svcb-https-00] + */ + public static final int TYPE_HTTPS = 65; + + private final int mQueryType; + + protected DnsEchAnswer(byte[] data) throws ParseException { + super(data); + if ((mHeader.flags & (1 << 15)) == 0) { + throw new IllegalArgumentException("Not an answer packet"); + } + if (mHeader.getRecordCount(QDSECTION) == 0) { + throw new IllegalArgumentException("No question found"); + } + // Expect only one question in question section. + mQueryType = mRecords[QDSECTION].get(0).nsType; + } + + public byte[] getEchConfigList() { + byte[] results = new byte[0]; + if (mHeader.getRecordCount(ANSECTION) == 0) return results; + + for (final DnsRecord ansSec : mRecords[ANSECTION]) { + // Only support SVCB and HTTPS since only they can have ECH Config Lists + int nsType = ansSec.nsType; + if (nsType != mQueryType || (nsType != TYPE_SVCB && nsType != TYPE_HTTPS)) { + continue; + } + echPbuf("RR", ansSec.getRR()); + results = Conscrypt.getEchConfigListFromDnsRR(ansSec.getRR()); + } + return results; + } + } + + private static class EchSSLSocketFactory extends SSLSocketFactory { + private final SSLSocketFactory delegate; + private final boolean enableEchGrease; + + private byte[] echConfigList; + + public EchSSLSocketFactory(SSLSocketFactory delegate, boolean enableEchGrease) { + this.delegate = delegate; + this.enableEchGrease = enableEchGrease; + } + + public EchSSLSocketFactory(SSLSocketFactory delegate, byte[] echConfigList) { + this.delegate = delegate; + this.enableEchGrease = true; + this.echConfigList = echConfigList; + } + + @Override + public String[] getDefaultCipherSuites() { + return delegate.getDefaultCipherSuites(); + } + + @Override + public String[] getSupportedCipherSuites() { + return delegate.getSupportedCipherSuites(); + } + + @Override + public Socket createSocket(Socket socket, String host, int port, boolean autoClose) + throws IOException { + return setEchSettings(delegate.createSocket(socket, host, port, autoClose)); + } + + @Override + public Socket createSocket(String host, int port) + throws IOException, UnknownHostException { + return setEchSettings(delegate.createSocket(host, port)); + } + + @Override + public Socket createSocket(String host, int port, InetAddress localAddress, int localPort) + throws IOException, UnknownHostException { + return setEchSettings(delegate.createSocket(host, port, localAddress, localPort)); + } + + @Override + public Socket createSocket(InetAddress host, int port) + throws IOException { + return setEchSettings(delegate.createSocket(host, port)); + } + + @Override + public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) + throws IOException { + return setEchSettings(delegate.createSocket(address, port, localAddress, localPort)); + } + + private Socket setEchSettings(Socket socket) { + SSLSocket sslSocket = (SSLSocket) socket; + Conscrypt.setEchParameters(sslSocket, new EchParameters(enableEchGrease, echConfigList)); + return sslSocket; + } + + } + + public static void echPbuf(String msg, byte[] buf) { + if (buf == null) { + System.out.println(msg + " ():\n null"); + return; + } + int blen = buf.length; + System.out.print(msg + " (" + blen + "):\n "); + for (int i = 0; i < blen; i++) { + if ((i != 0) && (i % 16 == 0)) + System.out.print("\n "); + System.out.print(String.format("%02x:", Byte.toUnsignedInt(buf[i]))); + } + System.out.print("\n"); + } + + /** + * Prime the DNS cache with the hosts that are used in these tests. + */ + private static void prefetchDns(String[] hosts) { + System.out.println("========== PREFETCH BEGIN ============================================================"); + for (final String host : hosts) { + new Thread() { + @Override + public void run() { + String actualHost = host; + if (actualHost.contains(":")) { + // the reformatted host strings with ports for defo don't return ips + actualHost = actualHost.split(":")[0]; + } + try { + InetAddress.getByName(actualHost); + getEchConfigListFromDns(host); + System.out.println("PREFETCH OK FOR " + actualHost); + } catch (NamingException e) { + System.out.println("PREFETCH FAILED FOR " + actualHost + ", GET ECH LIST THREW EXCEPTION"); + System.out.println(e.getMessage()); + } catch (UnknownHostException e) { + System.out.println("PREFETCH FAILED FOR " + actualHost + ", IP LOOKUP THREW EXCEPTION"); + System.out.println(e.getMessage()); + } + } + }.start(); + } + System.out.println("========== PREFETCH END =============================================================="); + } +} diff --git a/openjdk/src/test/resources/check-tls.akamaized.net.bin b/openjdk/src/test/resources/check-tls.akamaized.net.bin new file mode 100644 index 000000000..1ed80b328 Binary files /dev/null and b/openjdk/src/test/resources/check-tls.akamaized.net.bin differ diff --git a/openjdk/src/test/resources/cloudflare-esni.com.bin b/openjdk/src/test/resources/cloudflare-esni.com.bin new file mode 100644 index 000000000..d794be2bd Binary files /dev/null and b/openjdk/src/test/resources/cloudflare-esni.com.bin differ diff --git a/openjdk/src/test/resources/cloudflareresearch.com.bin b/openjdk/src/test/resources/cloudflareresearch.com.bin new file mode 100644 index 000000000..522390706 Binary files /dev/null and b/openjdk/src/test/resources/cloudflareresearch.com.bin differ diff --git a/openjdk/src/test/resources/crypto.cloudflare.com.bin b/openjdk/src/test/resources/crypto.cloudflare.com.bin new file mode 100644 index 000000000..081f672af Binary files /dev/null and b/openjdk/src/test/resources/crypto.cloudflare.com.bin differ diff --git a/openjdk/src/test/resources/deb.debian.org.bin b/openjdk/src/test/resources/deb.debian.org.bin new file mode 100644 index 000000000..05fb08270 Binary files /dev/null and b/openjdk/src/test/resources/deb.debian.org.bin differ diff --git a/openjdk/src/test/resources/draft-13.esni.defo.ie_12414-ech-config-list.bin b/openjdk/src/test/resources/draft-13.esni.defo.ie_12414-ech-config-list.bin new file mode 100644 index 000000000..b94202bf3 Binary files /dev/null and b/openjdk/src/test/resources/draft-13.esni.defo.ie_12414-ech-config-list.bin differ diff --git a/openjdk/src/test/resources/duckduckgo.com.bin b/openjdk/src/test/resources/duckduckgo.com.bin new file mode 100644 index 000000000..05089961e Binary files /dev/null and b/openjdk/src/test/resources/duckduckgo.com.bin differ diff --git a/openjdk/src/test/resources/en.wikipedia.org.bin b/openjdk/src/test/resources/en.wikipedia.org.bin new file mode 100644 index 000000000..625edeebc Binary files /dev/null and b/openjdk/src/test/resources/en.wikipedia.org.bin differ diff --git a/openjdk/src/test/resources/enabled.tls13.com.bin b/openjdk/src/test/resources/enabled.tls13.com.bin new file mode 100644 index 000000000..912bc1694 Binary files /dev/null and b/openjdk/src/test/resources/enabled.tls13.com.bin differ diff --git a/openjdk/src/test/resources/mirrors.kernel.org.bin b/openjdk/src/test/resources/mirrors.kernel.org.bin new file mode 100644 index 000000000..21a140170 Binary files /dev/null and b/openjdk/src/test/resources/mirrors.kernel.org.bin differ diff --git a/openjdk/src/test/resources/openstreetmap.org.bin b/openjdk/src/test/resources/openstreetmap.org.bin new file mode 100644 index 000000000..1239566fc Binary files /dev/null and b/openjdk/src/test/resources/openstreetmap.org.bin differ diff --git a/openjdk/src/test/resources/tls13.1d.pw.bin b/openjdk/src/test/resources/tls13.1d.pw.bin new file mode 100644 index 000000000..96ff98d0c Binary files /dev/null and b/openjdk/src/test/resources/tls13.1d.pw.bin differ diff --git a/openjdk/src/test/resources/www.google.com.bin b/openjdk/src/test/resources/www.google.com.bin new file mode 100644 index 000000000..25ef07d78 Binary files /dev/null and b/openjdk/src/test/resources/www.google.com.bin differ diff --git a/openjdk/src/test/resources/www.yandex.ru.bin b/openjdk/src/test/resources/www.yandex.ru.bin new file mode 100644 index 000000000..a1260e08d Binary files /dev/null and b/openjdk/src/test/resources/www.yandex.ru.bin differ