Re: [TLS] Possible blocking of Encrypted SNI extension in China

David Fifield <david@bamsoftware.com> Fri, 07 August 2020 23:56 UTC

Return-Path: <david@bamsoftware.com>
X-Original-To: tls@ietfa.amsl.com
Delivered-To: tls@ietfa.amsl.com
Received: from localhost (localhost [127.0.0.1]) by ietfa.amsl.com (Postfix) with ESMTP id 675693A0A36 for <tls@ietfa.amsl.com>; Fri, 7 Aug 2020 16:56:37 -0700 (PDT)
X-Virus-Scanned: amavisd-new at amsl.com
X-Spam-Flag: NO
X-Spam-Score: -2.099
X-Spam-Level:
X-Spam-Status: No, score=-2.099 tagged_above=-999 required=5 tests=[BAYES_00=-1.9, DKIM_SIGNED=0.1, DKIM_VALID=-0.1, DKIM_VALID_AU=-0.1, DKIM_VALID_EF=-0.1, SPF_HELO_NONE=0.001, SPF_PASS=-0.001, URIBL_BLOCKED=0.001] autolearn=ham autolearn_force=no
Authentication-Results: ietfa.amsl.com (amavisd-new); dkim=pass (1024-bit key) header.d=bamsoftware.com
Received: from mail.ietf.org ([4.31.198.44]) by localhost (ietfa.amsl.com [127.0.0.1]) (amavisd-new, port 10024) with ESMTP id 2O5npI2Y8do2 for <tls@ietfa.amsl.com>; Fri, 7 Aug 2020 16:56:35 -0700 (PDT)
Received: from melchior.bamsoftware.com (melchior.bamsoftware.com [IPv6:2600:3c00:e000:128:de39:20ee:9704:752d]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by ietfa.amsl.com (Postfix) with ESMTPS id 9D0FA3A0788 for <tls@ietf.org>; Fri, 7 Aug 2020 16:56:35 -0700 (PDT)
DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=bamsoftware.com; s=mail; h=In-Reply-To:Content-Transfer-Encoding: Content-Type:MIME-Version:References:Message-ID:Subject:To:From:Date:Sender: Reply-To:Cc:Content-ID:Content-Description:Resent-Date:Resent-From: Resent-Sender:Resent-To:Resent-Cc:Resent-Message-ID:List-Id:List-Help: List-Unsubscribe:List-Subscribe:List-Post:List-Owner:List-Archive; bh=JUQeyC5b43nB+tnD9p/q2IMHmJIBCgsiBmzEGuQN0j8=; b=iQBCnnDrBQJWLmWxqu4L7xOT21 u9l7UmAgA9HS0k7OFVoQGRd+xpRnxiJ8lJj68ETNDLIsGppMY+s0Co4Kc4WdkB7NIjYg2olrN01Dh 4knwJyt3oZY8vsO1RgN6RKY2emawknvnRwxqd+loF+dH8ct7IWD/SpBHJ0J1Q3UHGQpY=;
Date: Fri, 7 Aug 2020 17:56:30 -0600
From: David Fifield <david@bamsoftware.com>
To: tls@ietf.org
Message-ID: <20200807235630.cw7obeisyvhq6cbe@bamsoftware.com>
Mail-Followup-To: tls@ietf.org
References: <uGJxvVQRPcgn2GZKsKuuVN4SyTe7EOiV3iEK3Cq3Izo0ZstAh1LxEzMKrDZ_0VTrLqeYXQb4k1Qy5uJmEy04zNgngoHBONhVZnvddYYybt8=@iyouport.org>
MIME-Version: 1.0
Content-Type: text/plain; charset=utf-8
Content-Disposition: inline
Content-Transfer-Encoding: 8bit
In-Reply-To: <uGJxvVQRPcgn2GZKsKuuVN4SyTe7EOiV3iEK3Cq3Izo0ZstAh1LxEzMKrDZ_0VTrLqeYXQb4k1Qy5uJmEy04zNgngoHBONhVZnvddYYybt8=@iyouport.org>
User-Agent: NeoMutt/20180716
Archived-At: <https://mailarchive.ietf.org/arch/msg/tls/Dae-cukKMqfzmTT4Ksh1Bzlx7ws>
Subject: Re: [TLS] Possible blocking of Encrypted SNI extension in China
X-BeenThere: tls@ietf.org
X-Mailman-Version: 2.1.29
Precedence: list
List-Id: "This is the mailing list for the Transport Layer Security working group of the IETF." <tls.ietf.org>
List-Unsubscribe: <https://www.ietf.org/mailman/options/tls>, <mailto:tls-request@ietf.org?subject=unsubscribe>
List-Archive: <https://mailarchive.ietf.org/arch/browse/tls/>
List-Post: <mailto:tls@ietf.org>
List-Help: <mailto:tls-request@ietf.org?subject=help>
List-Subscribe: <https://www.ietf.org/mailman/listinfo/tls>, <mailto:tls-request@ietf.org?subject=subscribe>
X-List-Received-Date: Fri, 07 Aug 2020 23:56:37 -0000

On Thu, Jul 30, 2020 at 03:45:48PM +0000, onoketa wrote:
> The Great Firewall of China may have identified and blocked
> Cloudflare's ESNI implementation.
> 
> I have found that when using a TLS client hello with ESNI extension to
> connect to servers behind Cloudflare's CDN, the connection will be cut
> off after the whole TLS handshake is done. And then that IP address
> will be blocked at the TCP level for several minutes.

There is now a detailed written report on the new phenomenon of ESNI
blocking in China. It was produced by a collaboration of researchers
from Geneva (https://censorship.ai/), GFW Report (https://gfw.report/),
and iYouPort (https://www.iyouport.org/).

https://geneva.cs.umd.edu/posts/china-censors-esni/esni/    (English)
https://geneva.cs.umd.edu/zh/posts/china-censors-esni/esni/ (Chinese)

Here are some of the points most likely to be of interest to this group:
 * The detector is not merely matching on the lack of plaintext SNI; it
   is specifically looking for the ESNI extension 0xffce.
 * The ESNI detector only matches the ESNI encrypted_server_name
   extension 0xffce (draft-ietf-tls-esni-00 through -06), not the ECH
   extensions encrypted_client_hello 0xff02, ech_nonce 0xff03,
   outer_extension 0xff04 (draft-ietf-tls-esni-07).
 * The encrypted_server_name extension has to be syntactically correct;
   the detector is not just looking for the byte patter ff cc.
 * Once an ESNI-containing ClientHello is detected, the firewall drops
   packets in the client→server direction for 120 or 180 seconds.
 * The detector runs on all TCP ports, not just 443.

This short payload is sufficient to trigger blocking:
	160303003b0100003703035b72616e646f6d72616e646f6d72616e646f6d7261
	6e646f6d72616e646f6d5d0000000100000effce000a53754772000000000000
Python code to generate this payload is appended to this message.

Most of the functions of the Great Firewall work bidirectionally, and
the ESNI detection and blocking are no exception. Sending an
ESNI-containing ClientHello from *outside* of China to a server
*inside* results in temporary blocking, just the same as sending one
from the inside to the outside. This makes it easy to experiment with,
even if you don't control a host in China.

To experience ESNI blocking for yourself, choose a responsive TCP port
in China (doesn't have to be port 443), for example
www.tsinghua.edu.cn:80. Begin a TCP ping to the port, for example using
one of these commands:
	hping3 -S www.tsinghua.edu.cn -p 80
	nping -4 -c 0 --tcp-connect www.tsinghua.edu.cn -p 80
Then send the trigger payload:
	printf '\x16\x03\x03\x00\x3b\x01\x00\x00\x37\x03\x03[randomrandomrandomrandomrandom]\x00\x00\x00\x01\x00\x00\x0e\xff\xce\x00\nSuGr\x00\x00\x00\x00\x00\x00' | nc -4 -v www.tsinghua.edu.cn 80
The TCP pings will stop receiving replies for 120 or 180 seconds, then
will start back up again.

----

#!/usr/bin/env python3

# Generates a small TLS ClientHello that trigger's the GFW's ESNI detector.
# Writes output to the file minimal.bin.

import struct

from scapy.all import *
load_layer("tls")
from scapy.layers.tls.all import *

# https://tools.ietf.org/html/rfc8446#section-3.4
def var(ceiling, data):
    if ceiling < 256:
        fmt = ">B"
    elif ceiling < 65536:
        fmt = ">H"
    else:
        raise ValueError(ceiling)
    return struct.pack(fmt, len(data)) + data

# https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-01#section-5
def encrypted_server_name(suite, group, key_exchange, record_digest, encrypted_sni):
    return struct.pack(">HH", suite, group) \
        + var(65535, key_exchange) \
        + var(65535, record_digest) \
        + var(65535, encrypted_sni)

clienthello = TLS(
    msg = TLSClientHello(
        gmt_unix_time = 0x5b72616e, # "[ran"
        random_bytes = b"domrandomrandomrandomrandom]",
        ciphers = [],
        ext = [
            # The GFW detector requires a syntactically valid
            # server_name_extension, but the actual values it contains may be
            # nonsense. Here we use a CipherSuite of 0x5375 ("Su"), a NamedGroup
            # of 0x4772 ("Gr"), and zero-length key_exchange, record_digest, and
            # encrypted_sni.
            TLS_Ext_Unknown(type=0xffce, val=encrypted_server_name(0x5375, 0x4772, b"", b"", b"")),
        ],
    )
)

TLS(bytes(clienthello)).show()
print(bytes(clienthello))

FILENAME = "minimal.bin"
open(FILENAME, "wb").write(bytes(clienthello))
print("output written to {}".format(FILENAME))