From 7e8bac9bc9a048674f86159b49c60e4732ab7069 Mon Sep 17 00:00:00 2001 From: mnbogner Date: Fri, 17 Oct 2025 13:46:13 -0700 Subject: [PATCH 1/2] Initial draft of java layer of ECH support for Conscrypt. --- .../conscrypt/AbstractConscryptEngine.java | 8 + .../conscrypt/AbstractConscryptSocket.java | 8 + .../main/java/org/conscrypt/Conscrypt.java | 185 +++++++ .../java/org/conscrypt/ConscryptEngine.java | 18 + .../org/conscrypt/ConscryptEngineSocket.java | 20 + .../ConscryptFileDescriptorSocket.java | 21 + .../java/org/conscrypt/EchParameters.java | 29 + .../org/conscrypt/EchRejectedException.java | 17 + .../org/conscrypt/Java8EngineWrapper.java | 18 + .../main/java/org/conscrypt/NativeSsl.java | 13 + .../java/org/conscrypt/SSLParametersImpl.java | 14 + .../src/main/java/org/conscrypt/SSLUtils.java | 5 + .../android/net/module/util/DnsPacket.java | 253 +++++++++ .../org/conscrypt/ConscryptOpenJdkSuite.java | 2 + .../java/org/conscrypt/EchInteropTest.java | 521 ++++++++++++++++++ .../resources/check-tls.akamaized.net.bin | Bin 0 -> 139 bytes .../test/resources/cloudflare-esni.com.bin | Bin 0 -> 26 bytes .../test/resources/cloudflareresearch.com.bin | Bin 0 -> 134 bytes .../test/resources/crypto.cloudflare.com.bin | Bin 0 -> 499 bytes openjdk/src/test/resources/deb.debian.org.bin | Bin 0 -> 131 bytes ...-13.esni.defo.ie_12414-ech-config-list.bin | Bin 0 -> 66 bytes openjdk/src/test/resources/duckduckgo.com.bin | Bin 0 -> 97 bytes .../src/test/resources/en.wikipedia.org.bin | Bin 0 -> 114 bytes .../src/test/resources/enabled.tls13.com.bin | Bin 0 -> 105 bytes .../src/test/resources/mirrors.kernel.org.bin | Bin 0 -> 121 bytes .../src/test/resources/openstreetmap.org.bin | Bin 0 -> 98 bytes openjdk/src/test/resources/tls13.1d.pw.bin | Bin 0 -> 97 bytes openjdk/src/test/resources/www.google.com.bin | Bin 0 -> 82 bytes openjdk/src/test/resources/www.yandex.ru.bin | Bin 0 -> 92 bytes 29 files changed, 1132 insertions(+) create mode 100644 common/src/main/java/org/conscrypt/EchParameters.java create mode 100644 common/src/main/java/org/conscrypt/EchRejectedException.java create mode 100644 common/src/main/java/org/conscrypt/com/android/net/module/util/DnsPacket.java create mode 100644 openjdk/src/test/java/org/conscrypt/EchInteropTest.java create mode 100644 openjdk/src/test/resources/check-tls.akamaized.net.bin create mode 100644 openjdk/src/test/resources/cloudflare-esni.com.bin create mode 100644 openjdk/src/test/resources/cloudflareresearch.com.bin create mode 100644 openjdk/src/test/resources/crypto.cloudflare.com.bin create mode 100644 openjdk/src/test/resources/deb.debian.org.bin create mode 100644 openjdk/src/test/resources/draft-13.esni.defo.ie_12414-ech-config-list.bin create mode 100644 openjdk/src/test/resources/duckduckgo.com.bin create mode 100644 openjdk/src/test/resources/en.wikipedia.org.bin create mode 100644 openjdk/src/test/resources/enabled.tls13.com.bin create mode 100644 openjdk/src/test/resources/mirrors.kernel.org.bin create mode 100644 openjdk/src/test/resources/openstreetmap.org.bin create mode 100644 openjdk/src/test/resources/tls13.1d.pw.bin create mode 100644 openjdk/src/test/resources/www.google.com.bin create mode 100644 openjdk/src/test/resources/www.yandex.ru.bin 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 0000000000000000000000000000000000000000..1ed80b3286cfc163b059df9fd317ac503e3e87e9 GIT binary patch literal 139 zcmbP`*x10p2!;%t$r-81*}5e;#hi)RiMffHRjDb=d8s7~42}$p2Y47*fyx*H<}!$~ zCK_588L_4mCzqSEL6ja)IAG1d22#MF%3#W#X8@Kw;K-GcUtE%#SX`1?1XanLoS(~( Pm|H2#z`*+StIr2!yN*9Bj!&l?5gFT**24r73ASiAAZ*$@#eq42}$p2Y5gv0|Uc6hBgq* z$iU2$VZ^|~z_Dn4yZ@s39sWQAJQ&>m@iI8wWl&&Hm>%5K{qVeY(jwE}uUJ^(%%5gh z$MpUvUfU6Q(LPzyp&qCeWUDa3F5T4Pyi7f?g$!&A3TkZb3=Hf*8-W012P(hA|9~(9 z6VL#rMFk9O%z4Eo2ZYf?%rHbuF+_|pM9hIAzZqnpCJU%AoMuq)1oFVHU{J&@$NuQR jQC#{M6>-aPm>+nFOCJ*_ZaGf#1J`lsW9G&!$I1o(D?V}7 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..05fb082700d6db7a09411adbc80a8e1c1e0aa57f GIT binary patch literal 131 zcmezWxv_zP5eylaQ&N-IfH*TTk2$|6oq@rTf$;zj11nILflq)z3MQAESiqTZ4 ppO?yau0gQ!8fu?F1003=o7Nh_G literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..625edeebc3b34d441655f4df2bc7e0d786cc9e21 GIT binary patch literal 114 zcmeC#*x10p2!;$ysd=2`nc0~IsVSL>%=ty>3=ED8j0boaSV8iEAq;{nDV2GNU^TfQ zH3uXQ7&EW|B^j7>7}S~biVY4Jb7kZgm*gfEm!uXQFs>-?Ph((Mae#qAMu9=7lmVzP F0swMs9XS91 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..912bc1694a3d081ea7ac070087056031877a4881 GIT binary patch literal 105 zcmbPnxUqqO5eR{RJvA>eDJM0BwIrw5(3m+nKbL{Qk%9354~S%7VANr-0@Do4Oc_QD gEDRhO0(0Us1m?ywurVm8vAKiH00WRXDt~S~03x*!Z2$lO literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..21a14017002921e4d4ed23a64add9e8a1eac161a GIT binary patch literal 121 zcmZoHY;0g)1VaY)+{~h){GwvE?9`&X)EwsgqI3oZM+U|NJPfQr6$}hp82Dgnm^WDNF_B3=ED8j0c1m*gz7@EesY+dByC-sYPX}MaAri tMcIjYiABtLsU-}|DS5@rdHEGg#ib18f4L1A7-SR}82AJj7y}zXIsq__7CQg{ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..25ef07d78a5d7cf6751bf17c6a5eea4ccebf63fb GIT binary patch literal 82 zcmdlZ)!4wm$iM)?%;n|fZ0Y&=={c#)$@#eq42}$p2Lu?{!17WIYRq}Xh6e;VQ}T*+ Z6H{_C^9~3|2;|iP^|e4SI|tAl8vyO`5e5JN literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..a1260e08d1e674680427c52527d1b0cb06a36342 GIT binary patch literal 92 zcmZ3pu(5%Gk%0k(naj(|*(wwBQc^3Jib@$692po72r#gLrNbEvne&Pb4+wA+R~9Fx k Date: Thu, 13 Nov 2025 16:05:31 -0800 Subject: [PATCH 2/2] Cleaned up code, replaced resource files for tests, added checks to prevent unintended failures, updated tests so all hosts are tested before failing. --- common/src/jni/main/cpp/conscrypt/jniutil.cc | 14 +- .../jni/main/cpp/conscrypt/native_crypto.cc | 14 +- .../main/java/org/conscrypt/Conscrypt.java | 29 ++- .../java/org/conscrypt/ConscryptEngine.java | 8 +- .../org/conscrypt/ConscryptEngineSocket.java | 8 +- .../ConscryptFileDescriptorSocket.java | 9 +- .../java/org/conscrypt/EchParameters.java | 2 - .../org/conscrypt/Java8EngineWrapper.java | 8 +- .../main/java/org/conscrypt/NativeSsl.java | 14 +- .../java/org/conscrypt/EchInteropTest.java | 230 ++++++++++-------- .../_10413._https.draft-13.esni.defo.ie.bin | Bin 0 -> 120 bytes .../_11413._https.draft-13.esni.defo.ie.bin | Bin 0 -> 120 bytes .../_12413._https.draft-13.esni.defo.ie.bin | Bin 0 -> 120 bytes .../_12414._https.draft-13.esni.defo.ie.bin | Bin 0 -> 120 bytes .../_8413._https.draft-13.esni.defo.ie.bin | Bin 0 -> 119 bytes .../_8414._https.draft-13.esni.defo.ie.bin | Bin 0 -> 119 bytes .../test/resources/cloudflare-esni.com.bin | Bin 26 -> 69 bytes .../test/resources/cloudflareresearch.com.bin | Bin 134 -> 72 bytes openjdk/src/test/resources/tls13.1d.pw.bin | Bin 97 -> 0 bytes openjdk/src/test/resources/web.wechat.com.bin | Bin 0 -> 83 bytes openjdk/src/test/resources/www.yandex.ru.bin | Bin 92 -> 79 bytes 21 files changed, 214 insertions(+), 122 deletions(-) create mode 100644 openjdk/src/test/resources/_10413._https.draft-13.esni.defo.ie.bin create mode 100644 openjdk/src/test/resources/_11413._https.draft-13.esni.defo.ie.bin create mode 100644 openjdk/src/test/resources/_12413._https.draft-13.esni.defo.ie.bin create mode 100644 openjdk/src/test/resources/_12414._https.draft-13.esni.defo.ie.bin create mode 100644 openjdk/src/test/resources/_8413._https.draft-13.esni.defo.ie.bin create mode 100644 openjdk/src/test/resources/_8414._https.draft-13.esni.defo.ie.bin delete mode 100644 openjdk/src/test/resources/tls13.1d.pw.bin create mode 100644 openjdk/src/test/resources/web.wechat.com.bin diff --git a/common/src/jni/main/cpp/conscrypt/jniutil.cc b/common/src/jni/main/cpp/conscrypt/jniutil.cc index 7f106a9ce..3c3d11545 100644 --- a/common/src/jni/main/cpp/conscrypt/jniutil.cc +++ b/common/src/jni/main/cpp/conscrypt/jniutil.cc @@ -159,14 +159,24 @@ void jniRegisterNativeMethods(JNIEnv* env, const char* className, const JNINativ ScopedLocalRef c(env, env->FindClass(className)); if (c.get() == nullptr) { char* msg; - (void)asprintf(&msg, "Native registration unable to find class '%s'; aborting...", + // TEMP - fixes local build issue + int foo = 0; + foo = asprintf(&msg, "Native registration unable to find class '%s'; aborting...", className); + if (foo > 0) { + CONSCRYPT_LOG_VERBOSE("FOO: %d", foo); + } env->FatalError(msg); } if (env->RegisterNatives(c.get(), gMethods, numMethods) < 0) { char* msg; - (void)asprintf(&msg, "RegisterNatives failed for '%s'; aborting...", className); + // TEMP - fixes local build issue + int foo = 0; + foo = asprintf(&msg, "RegisterNatives failed for '%s'; aborting...", className); + if (foo > 0) { + CONSCRYPT_LOG_VERBOSE("FOO: %d", foo); + } env->FatalError(msg); } } diff --git a/common/src/jni/main/cpp/conscrypt/native_crypto.cc b/common/src/jni/main/cpp/conscrypt/native_crypto.cc index ab1f1512a..f46ccf050 100644 --- a/common/src/jni/main/cpp/conscrypt/native_crypto.cc +++ b/common/src/jni/main/cpp/conscrypt/native_crypto.cc @@ -7835,7 +7835,12 @@ static int sslSelect(JNIEnv* env, int type, jobject fdObject, AppData* appData, if (fds[1].revents & POLLIN) { char token; do { - (void)read(appData->fdsEmergency[0], &token, 1); + // TEMP - fixes local build issue + int foo = 0; + foo = read(appData->fdsEmergency[0], &token, 1); + if (foo > 0) { + CONSCRYPT_LOG_VERBOSE("FOO: %d", foo); + } } while (errno == EINTR); } } @@ -7866,7 +7871,12 @@ static void sslNotify(AppData* appData) { char token = '*'; do { errno = 0; - (void)write(appData->fdsEmergency[1], &token, 1); + // TEMP - fixes local build issue + int foo = 0; + foo = write(appData->fdsEmergency[1], &token, 1); + if (foo > 0) { + CONSCRYPT_LOG_VERBOSE("FOO: %d", foo); + } } while (errno == EINTR); errno = errnoBackup; #endif diff --git a/common/src/main/java/org/conscrypt/Conscrypt.java b/common/src/main/java/org/conscrypt/Conscrypt.java index abfddaaa7..3db96a8ca 100644 --- a/common/src/main/java/org/conscrypt/Conscrypt.java +++ b/common/src/main/java/org/conscrypt/Conscrypt.java @@ -573,7 +573,14 @@ public static String getEchNameOverride(SSLSocket socket) { * @param socket the socket (instance of ConscryptSocket) */ public static boolean echAccepted(SSLSocket socket) { - return toConscrypt(socket).echAccepted(); + AbstractConscryptSocket conSocket = toConscrypt(socket); + byte[] echConfig = conSocket.getEchParameters().configList; + // if there is no ECH config, .echAccepted may throw an exception + if (echConfig == null || echConfig.length == 0) { + return false; + } else { + return conSocket.echAccepted(); + } } /** @@ -840,7 +847,8 @@ public static byte[] exportKeyingMaterial( * @param engine the engine * @param enabled Whether to enable TLSv1.3 ECH GREASE * - * @see TLS Encrypted Client Hello 6.2. GREASE ECH + * @see TLS + * Encrypted Client Hello 6.2. GREASE ECH */ public static void setEchParameters(SSLEngine engine, EchParameters parameters) { @@ -856,7 +864,14 @@ public static String getEchNameOverride(SSLEngine engine) { } public static boolean echAccepted(SSLEngine engine) { - return toConscrypt(engine).echAccepted(); + AbstractConscryptEngine conEngine = toConscrypt(engine); + byte[] echConfig = conEngine.getEchParameters().configList; + // if there is no ECH config, .echAccepted may throw an exception + if (echConfig == null || echConfig.length == 0) { + return false; + } else { + return conEngine.echAccepted(); + } } @@ -903,14 +918,14 @@ public static boolean echAccepted(SSLEngine engine) { * using the num_echs output. * * @param rrval is the binary encoded RData - * @return is 1 for success, error otherwise + * @return is a byte array with the copied config or null */ public static byte[] getEchConfigListFromDnsRR(byte[] rrval) { int rv = 0; int binlen = 0; /* the RData */ byte[] binbuf = null; int pos = 0; - int remaining = rrval.length;; + int remaining = rrval.length; String dnsname = null; int plen = 0; boolean done = false; @@ -937,7 +952,7 @@ public static byte[] getEchConfigListFromDnsRR(byte[] rrval) { rv = 1; break; } - for (int i =pos; i < clen; i++) { + for (int i = pos; i < clen; i++) { thename.write(DnsPacket.byteToUnsignedInt(rrval[pos + i])); } thename.write('.'); @@ -968,7 +983,7 @@ public static byte[] getEchConfigListFromDnsRR(byte[] rrval) { if (!done) { return null; } - if (plen <=0) { + if (plen <= 0) { return null; } byte[] ret = new byte[plen]; diff --git a/common/src/main/java/org/conscrypt/ConscryptEngine.java b/common/src/main/java/org/conscrypt/ConscryptEngine.java index ec06943e5..ae80e7b1e 100644 --- a/common/src/main/java/org/conscrypt/ConscryptEngine.java +++ b/common/src/main/java/org/conscrypt/ConscryptEngine.java @@ -413,7 +413,13 @@ public String getEchNameOverride() { @Override public boolean echAccepted() { - return ssl.echAccepted(); + byte[] echConfig = sslParameters.getEchParameters().configList; + // if there is no ECH config, .echAccepted may throw an exception + if (echConfig == null || echConfig.length == 0) { + return false; + } else { + return ssl.echAccepted(); + } } @Override diff --git a/common/src/main/java/org/conscrypt/ConscryptEngineSocket.java b/common/src/main/java/org/conscrypt/ConscryptEngineSocket.java index 458505b49..894acb351 100644 --- a/common/src/main/java/org/conscrypt/ConscryptEngineSocket.java +++ b/common/src/main/java/org/conscrypt/ConscryptEngineSocket.java @@ -490,7 +490,13 @@ public String getEchNameOverride() { @Override public boolean echAccepted() { - return engine.echAccepted(); + byte[] echConfig = engine.getEchParameters().configList; + // if there is no ECH config, .echAccepted may throw an exception + if (echConfig == null || echConfig.length == 0) { + return false; + } else { + return engine.echAccepted(); + } } @Override diff --git a/common/src/main/java/org/conscrypt/ConscryptFileDescriptorSocket.java b/common/src/main/java/org/conscrypt/ConscryptFileDescriptorSocket.java index fc3b58062..5da971b7f 100644 --- a/common/src/main/java/org/conscrypt/ConscryptFileDescriptorSocket.java +++ b/common/src/main/java/org/conscrypt/ConscryptFileDescriptorSocket.java @@ -895,7 +895,6 @@ 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); @@ -913,7 +912,13 @@ public String getEchNameOverride() { @Override public boolean echAccepted() { - return ssl.echAccepted(); + byte[] echConfig = sslParameters.getEchParameters().configList; + // if there is no ECH config, .echAccepted may throw an exception + if (echConfig == null || echConfig.length == 0) { + return false; + } else { + return ssl.echAccepted(); + } } @Override diff --git a/common/src/main/java/org/conscrypt/EchParameters.java b/common/src/main/java/org/conscrypt/EchParameters.java index 9c581569d..97f143543 100644 --- a/common/src/main/java/org/conscrypt/EchParameters.java +++ b/common/src/main/java/org/conscrypt/EchParameters.java @@ -1,7 +1,6 @@ package org.conscrypt; public class EchParameters { - public boolean useEchGrease; public byte[] configList; @@ -25,5 +24,4 @@ public EchParameters(boolean useEchGrease, byte[] configList) { this.useEchGrease = useEchGrease; this.configList = configList; } - } diff --git a/common/src/main/java/org/conscrypt/Java8EngineWrapper.java b/common/src/main/java/org/conscrypt/Java8EngineWrapper.java index 1abd1ea04..a5a9c0857 100644 --- a/common/src/main/java/org/conscrypt/Java8EngineWrapper.java +++ b/common/src/main/java/org/conscrypt/Java8EngineWrapper.java @@ -131,7 +131,13 @@ public String getEchNameOverride() { @Override public boolean echAccepted() { - return delegate.echAccepted(); + byte[] echConfig = delegate.getEchParameters().configList; + // if there is no ECH config, .echAccepted may throw an exception + if (echConfig == null || echConfig.length == 0) { + return false; + } else { + return delegate.echAccepted(); + } } @Override diff --git a/common/src/main/java/org/conscrypt/NativeSsl.java b/common/src/main/java/org/conscrypt/NativeSsl.java index ea2d56611..d3cb2acaf 100644 --- a/common/src/main/java/org/conscrypt/NativeSsl.java +++ b/common/src/main/java/org/conscrypt/NativeSsl.java @@ -279,7 +279,13 @@ String getEchNameOverride() { } boolean echAccepted() { - return NativeCrypto.SSL_ech_accepted(ssl, this); + byte[] echConfig = parameters.getEchParameters().configList; + // if there is no ECH config, .echAccepted may throw an exception + if (echConfig == null || echConfig.length == 0) { + return false; + } else { + return NativeCrypto.SSL_ech_accepted(ssl, this); + } } void initialize(String hostname, OpenSSLKey channelIdPrivateKey) throws IOException { @@ -301,9 +307,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); + 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)) { + && !NativeCrypto.SSL_set1_ech_config_list( + ssl, this, parameters.getEchParameters().configList)) { throw new SSLHandshakeException("Error setting ECHConfigList"); } } else { diff --git a/openjdk/src/test/java/org/conscrypt/EchInteropTest.java b/openjdk/src/test/java/org/conscrypt/EchInteropTest.java index f902def35..034fcdef9 100644 --- a/openjdk/src/test/java/org/conscrypt/EchInteropTest.java +++ b/openjdk/src/test/java/org/conscrypt/EchInteropTest.java @@ -57,30 +57,27 @@ public class EchInteropTest { private static String[] hostsNonEch = { "www.yandex.ru", "en.wikipedia.org", - // TEMP - causes prefetch exception "web.wechat.com", + "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 + "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 (fails to connect) + "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) + "openstreetmap.org", // now supports ECH + "cloudflare-esni.com", // now supports ECH + "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 + // "draft-13.esni.defo.ie:9413", // (array out of bounds) + "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]; @@ -109,7 +106,7 @@ public static void tearDown() throws NoSuchAlgorithmException { public void testConnectSocket() throws IOException { boolean hostFailed = false; for (String h : hosts) { - System.out.println(" = TEST CONNECT SOCKET FOR " + h); + System.out.println(" = RUNNING testConnectSocket FOR " + h); String[] hostPort = h.split(":"); String host = hostPort[0]; int port = 443; @@ -119,43 +116,48 @@ public void testConnectSocket() throws IOException { 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; + 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)); + setUpEch = true; + } else { + Conscrypt.setEchParameters(sslSocket, new EchParameters(true)); + } + } catch (NamingException e) { + System.out.println("getEchConfigListFromDns THREW NamingException FOR " + h); + System.out.println(e.getMessage()); + hostFailed = true; + continue; + } + sslSocket.setSoTimeout(TIMEOUT_MILLISECONDS); + try { + sslSocket.startHandshake(); + } catch (Exception e) { + System.out.println("startHandshake THREW Exception FOR " + h); + System.out.println(e.getMessage()); + hostFailed = true; + continue; + } + assertTrue(sslSocket.isConnected()); + AbstractConscryptSocket abstractConscryptSocket = (AbstractConscryptSocket) sslSocket; + if (setUpEch) { + assertTrue(abstractConscryptSocket.echAccepted()); } else { - Conscrypt.setEchParameters(sslSocket, new EchParameters(true)); - System.out.println("ENABLED ECH GREASE"); + assertFalse(abstractConscryptSocket.echAccepted()); } - } catch (NamingException e) { - System.out.println("GET CONFIG LIST THREW EXCEPTION FOR " + host); + sslSocket.close(); + } catch (ConnectException e) { + System.out.println("createSocket THREW ConnectException FOR " + h); 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); } @@ -165,7 +167,7 @@ public void testConnectSocket() throws IOException { @Test public void testEchConfigOnNonEchHosts() throws IOException { for (String h : hostsNonEch) { - System.out.println(" = TEST ECH CONFIG ON NON ECH HOSTS FOR " + h); + System.out.println(" = RUNNING testEchConfigOnNonEchHosts FOR " + h); String[] hostPort = h.split(":"); String host = hostPort[0]; int port = 443; @@ -197,8 +199,8 @@ public void testEchConfigOnNonEchHosts() throws IOException { public void testConnectHttpsURLConnection() throws IOException { boolean hostFailed = false; for (String host : hosts) { + System.out.println(" = RUNNING testConnectHttpsURLConnection FOR " + host); 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)); @@ -206,13 +208,16 @@ public void testConnectHttpsURLConnection() throws IOException { 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("getEchConfigListFromDns THREW NamingException FOR " + host); + System.out.println(e.getMessage()); + hostFailed = true; + continue; + } catch (ArrayIndexOutOfBoundsException e) { + System.out.println("getEchConfigListFromDns THREW ArrayIndexOutOfBoundsException FOR " + host); System.out.println(e.getMessage()); hostFailed = true; continue; @@ -229,12 +234,20 @@ public void testConnectHttpsURLConnection() throws IOException { 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("getResponseCode/getContentType/getCipherSuite THREW Exception FOR " + host); System.out.println(e.getMessage()); + hostFailed = true; + continue; + } + try { + connection.getContent(); + } catch (ConnectException e) { + System.out.println("getContent THREW ConnectException FOR " + host); + System.out.println(e.getMessage()); + hostFailed = true; + continue; } - connection.getContent(); assertEquals(200, responseCode); String[] options = {"text/html", "text/plain"}; List contentTypes = Arrays.asList(options); @@ -243,14 +256,14 @@ public void testConnectHttpsURLConnection() throws IOException { 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 { + boolean hostFailed = false; for (String h : hosts) { - System.out.println(" = TEST PARSE DNS AND CONNECT FOR " + h); + System.out.println(" = RUNNING testParseDnsAndConnect FOR " + h); String[] hostPort = h.split(":"); String host = hostPort[0]; int port = 443; @@ -261,10 +274,11 @@ public void testParseDnsAndConnect() throws IOException, NamingException { 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("getEchConfigListFromDns THREW Exception FOR " + h); System.out.println(e.getMessage()); + hostFailed = true; + continue; } if (echConfigList != null) { @@ -272,39 +286,43 @@ public void testParseDnsAndConnect() throws IOException, NamingException { 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()); + try { + SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(host, port); + assertTrue(Conscrypt.isConscrypt(sslSocket)); + if (echConfigList != null) { + Conscrypt.setEchParameters(sslSocket, new EchParameters(true, echConfigList)); + } else { + Conscrypt.setEchParameters(sslSocket, new EchParameters(true)); + } + sslSocket.setSoTimeout(TIMEOUT_MILLISECONDS); + sslSocket.startHandshake(); + assertTrue(sslSocket.isConnected()); + AbstractConscryptSocket abstractConscryptSocket = (AbstractConscryptSocket) sslSocket; + if (echConfigList != null) { + assertTrue(abstractConscryptSocket.echAccepted()); + } else { + assertFalse(abstractConscryptSocket.echAccepted()); + } + sslSocket.close(); + } catch (ConnectException e) { + System.out.println("createSocket THREW ConnectException FOR " + h); + System.out.println(e.getMessage()); + hostFailed = true; + continue; } - sslSocket.close(); } + assertFalse(hostFailed); } @Test public void testParseDnsFromFiles() { + boolean hostFailed = false; for (String hostString : hosts) { - System.out.println(" = TEST PARSE DNS FROM FILES FOR " + hostString); + System.out.println(" = RUNNING testParseDnsFromFiles FOR " + hostString); String[] h = hostString.split(":"); String host = h[0]; if (h.length > 1) { @@ -317,19 +335,24 @@ public void testParseDnsFromFiles() { echPbuf("DNS ANSWER", dnsAnswer); try { DnsEchAnswer dnsEchAnswer = new DnsEchAnswer(dnsAnswer); - if (dnsEchAnswer.getEchConfigList() == null) { - System.out.println("ECH CONFIG LIST NULL FOR " + host); - } else { + if (dnsEchAnswer.getEchConfigList() != null) { echPbuf("ECH CONFIG LIST", dnsEchAnswer.getEchConfigList()); } } catch (DnsPacket.ParseException e) { - e.printStackTrace(); + System.out.println("DnsEchAnswer THREW ParseException FOR " + hostString); + System.out.println(e.getMessage()); + hostFailed = true; + continue; } } catch (IOException e) { - e.printStackTrace(); + System.out.println("readTestFile THREW IOException FOR " + hostString); + System.out.println(e.getMessage()); + hostFailed = true; + continue; } } + assertFalse(hostFailed); } static byte[] getEchConfigListFromDns(String hostPort) throws NamingException { @@ -492,30 +515,35 @@ public static void echPbuf(String msg, byte[] buf) { * 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() { + // the reformatted host strings with ports for defo don't return ips + final String actualHost = (host.contains(":")) ? host.split(":")[0] : host; + Thread t = 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); + System.out.println("PREFETCH OK FOR " + host); } catch (NamingException e) { - System.out.println("PREFETCH FAILED FOR " + actualHost + ", GET ECH LIST THREW EXCEPTION"); + System.out.println("getEchConfigListFromDns THREW NamingException FOR " + host); + System.out.println(e.getMessage()); + } catch (ArrayIndexOutOfBoundsException e) { + System.out.println("getEchConfigListFromDns THREW ArrayIndexOutOfBoundsException FOR " + host); System.out.println(e.getMessage()); } catch (UnknownHostException e) { - System.out.println("PREFETCH FAILED FOR " + actualHost + ", IP LOOKUP THREW EXCEPTION"); + System.out.println("getByName THREW UnknownHostException FOR " + host); System.out.println(e.getMessage()); } } - }.start(); + }; + t.start(); + try { + // wait for each prefetch to complete before running actual tests + t.join(); + } catch (InterruptedException e) { + System.out.println("PREFETCH THREAD INTERRUPTED FOR " + host); + } } - System.out.println("========== PREFETCH END =============================================================="); } } diff --git a/openjdk/src/test/resources/_10413._https.draft-13.esni.defo.ie.bin b/openjdk/src/test/resources/_10413._https.draft-13.esni.defo.ie.bin new file mode 100644 index 0000000000000000000000000000000000000000..5ffc445827b482b96e5af22fc71eeb3482dd24ea GIT binary patch literal 120 zcmcBvX>4X-WMBYcj+COrv=UuIW0ut7yiAsq)U0TEf7ZS6q^l!UN=I7H6jC>81n4lk;;KDz@ys#lXPM!N9=W L!oX?O05SmpyX79R literal 0 HcmV?d00001 diff --git a/openjdk/src/test/resources/_11413._https.draft-13.esni.defo.ie.bin b/openjdk/src/test/resources/_11413._https.draft-13.esni.defo.ie.bin new file mode 100644 index 0000000000000000000000000000000000000000..c33f1fa3dec0bcf46112acc0e181260a650b1f81 GIT binary patch literal 120 zcmWe)YHVg;WMBYcj+COrv=UuIW0ut7yiAsq)U0TEf7ZS6q^l!UN=I7H6jC>81n4lk;;KDz@ys#lXPM!N9=W L!oX?O05SmpgVP>3 literal 0 HcmV?d00001 diff --git a/openjdk/src/test/resources/_12413._https.draft-13.esni.defo.ie.bin b/openjdk/src/test/resources/_12413._https.draft-13.esni.defo.ie.bin new file mode 100644 index 0000000000000000000000000000000000000000..79167a8687d023abe32ac2aa7c313549c05a90e2 GIT binary patch literal 120 zcmWgH*4WIz$iM)?94SSKX(hUb#w@ADd6_IJscHEfnWFAO%1rY#=JIfx&?> zk*6p>zeKk zk*6p>zeKk1$xFjcq2guJX&P>nKO$Ul6=jSq1Y}tE@fq|Wafq}V& Kfzzr1WC8#YJs)oX literal 0 HcmV?d00001 diff --git a/openjdk/src/test/resources/_8414._https.draft-13.esni.defo.ie.bin b/openjdk/src/test/resources/_8414._https.draft-13.esni.defo.ie.bin new file mode 100644 index 0000000000000000000000000000000000000000..2f042d1ae5552d9d7f81b6d235075c1d46d57686 GIT binary patch literal 119 zcma#gX>4X-WMBYcj+COrv=UuIW0ut7yiAsq)UZ J6qVl@3IMc=5qAIp literal 134 zcmZQzWME)qU}nlNVqjt5STw&gbkY2-PzF{84+gh?ybMkm3^WDNF_B3=ED8j0c1m*gz7@EesY+dByC-sYPX}MaAri tMcIjYiABtLsU-}|DS5@rdHEGg#ib18f4L1A7-SR}82AJj7y}zXIsq__7CQg{ diff --git a/openjdk/src/test/resources/web.wechat.com.bin b/openjdk/src/test/resources/web.wechat.com.bin new file mode 100644 index 0000000000000000000000000000000000000000..4499bc451e35570a595beb2e3f4081bcdb4c3c32 GIT binary patch literal 83 zcmX?ou(5%Gk%5^32$;)Lli12rlQR-an3MB!8Gz!92Y47*fiweC1Oqz@P~Px>zyTeQ S1Xzegd;V(!WPanLEDr$t!x5nX literal 0 HcmV?d00001 diff --git a/openjdk/src/test/resources/www.yandex.ru.bin b/openjdk/src/test/resources/www.yandex.ru.bin index a1260e08d1e674680427c52527d1b0cb06a36342..ddfd1dc7d3e5bb3132705d591ae22de4a2b32624 100644 GIT binary patch literal 79 zcmdmeps|60k%5^32$;*u%h@Uu^HNePn2JgnfWnLict9kOd7FX7H$ul8na}$Fzb`W1 IH^Mvu0K&=;`2YX_ literal 92 zcmZ3pu(5%Gk%0k(naj(|*(wwBQc^3Jib@$692po72r#gLrNbEvne&Pb4+wA+R~9Fx k