Skip to content

Commit

Permalink
Added an option to use Post-Quantum secure algorithms
Browse files Browse the repository at this point in the history
Added an option to use Post-Quantum secure algorithms for establishing TLS
connections. This option is hidden under a new `--experiment` flag that is
described in README.md.

Closes #15
  • Loading branch information
ameshkov committed Sep 23, 2023
1 parent ab6e52b commit 312c05b
Show file tree
Hide file tree
Showing 11 changed files with 342 additions and 130 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ adheres to [Semantic Versioning][semver].

## [Unreleased]

### Added

* Added an option to use Post-Quantum secure algorithms for establishing TLS
connections. This option is hidden under a new `--experiment` flag that is
described in README.md. ([#15][#15])

### Fixed

* Fixed an issue with `--http2` not being able to work together with `--ech`. In
Expand All @@ -20,6 +26,8 @@ adheres to [Semantic Versioning][semver].

[#14]: https://github.com/ameshkov/gocurl/issues/14

[#15]: https://github.com/ameshkov/gocurl/issues/15

[unreleased]: https://github.com/ameshkov/gocurl/compare/v1.2.0...HEAD

## [1.2.0] - 2023-09-22
Expand Down
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ Also, you can use some new stuff that is not supported by curl.
* `gocurl --dns-servers="tls://dns.google" https://httpbin.agrd.workers.dev/get`
uses custom DNS-over-TLS server to resolve hostnames. More on this
[below](#dns).
* `gocurl --experiment=pq https://pq.cloudflareresearch.com/` enables
post-quantum cryptography support for the request. More on this [below][#exp].

<a id="ech"></a>

Expand Down Expand Up @@ -185,6 +187,28 @@ gocurl \
https://example.org/
```
#### Experimental flags
Experimental flags are added to `gocurl` whenever there's a feature that may be
completely changed or removed in the future. Experiments can be enabled using
the `--experiment=<name[:value]>` argument where `name` is the experiment name
and `value` is an optional string value (the need for it depends on the actual
experiment).

##### Post-quantum cryptography

Post-quantum (PQ) cryptography has been designed to be secure against the
threat of quantum computers. You can learn more about it from Cloudflare's
[blog post][postquantum]. `gocurl` supports it via the `--experiment=pq` flag.
Note, that it is not available for `--http3` at the moment.
```shell
gocurl --experiment=pq https://pq.cloudflareresearch.com/
```
[postquantum]: https://blog.cloudflare.com/post-quantum-for-all/
## All command-line arguments
```shell
Expand Down
110 changes: 110 additions & 0 deletions internal/client/cfcrypto/cfcrypto.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// Package cfcrypto is a package that uses Cloudflare's TLS fork to provide
// features missing in crypto/tls.
package cfcrypto

import (
"crypto/tls"
"net"
"slices"

ctls "github.com/ameshkov/cfcrypto/tls"
"github.com/ameshkov/gocurl/internal/config"
"github.com/ameshkov/gocurl/internal/output"
"github.com/ameshkov/gocurl/internal/resolve"
)

// Handshake attempts to establish a TLS connection using Cloudflare's TLS fork.
//
// Depending on the arguments, it may do the following:
//
// - Encrypted ClientHello.
// - Post-quantum cryptography.
//
// # Arguments
//
// - conn is the underlying network connection that should already be
// established.
// - tlsConfig is the original tls.Config, its properties will be copied to
// the ctls.Config used by this method.
// - resolver is specified enables ECH support.
// - cfg is the *config.Config configuration object.
// - out is the *output.Output object that is used to write logs.
//
// # Encrypted ClientHello
//
// It is used if enabled in the cfg argument. A few things about the tlsConfig
// that is passed to it:
//
// - ServerName will be used in the inner ClientHello. For the outer
// ClientHello it will attempt to use the "public name" field of the ECH
// configuration.
// - Regarding the multiple ECHConfig passed, it chooses the first with
// a suitable cipher suite which effectively means that it will almost
// always simply use the first ECHConfig from the slice.
//
// # Post-quantum cryptography
//
// This basically means that new curves will be added to CurvePreferences.
func Handshake(
conn net.Conn,
tlsConfig *tls.Config,
resolver *resolve.Resolver,
cfg *config.Config,
out *output.Output,
) (tlsConn net.Conn, err error) {
out.Debug("Attempting to establish a TLS connection")

var echConfigs []ctls.ECHConfig
if cfg.ECH {
echConfigs, err = resolver.LookupECHConfigs(tlsConfig.ServerName)
if err != nil {
return nil, err
}
}

_, postQuantum := cfg.Experiments[config.ExpPostQuantum]

// Copying the original tls config fields to ECH-enabled one.
conf := &ctls.Config{
ServerName: tlsConfig.ServerName,
MinVersion: tlsConfig.MinVersion,
MaxVersion: tlsConfig.MaxVersion,
InsecureSkipVerify: tlsConfig.InsecureSkipVerify,
NextProtos: tlsConfig.NextProtos,
}

// In the case of regular http.Transport it can handle h2 upgrade with the
// regular tls.Conn only so remove h2 from NextProtos in this case.
//
// TODO(ameshkov): remove this when transport is reworked to dial first.
if slices.Contains(tlsConfig.NextProtos, "http/1.1") &&
slices.Contains(tlsConfig.NextProtos, "h2") {
conf.NextProtos = []string{"http/1.1"}
}

if len(echConfigs) > 0 {
conf.ECHEnabled = true
conf.ClientECHConfigs = echConfigs
}

if postQuantum {
conf.CurvePreferences = []ctls.CurveID{
ctls.X25519Kyber768Draft00,
ctls.X25519,
ctls.CurveP256,
}
}

c := ctls.Client(conn, conf)
err = c.Handshake()

if err != nil {
return nil, err
}

out.Debug("TLS connection has been established successfully")

return &connWrapper{
baseConn: c,
}, nil
}
119 changes: 119 additions & 0 deletions internal/client/cfcrypto/cfcrypto_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// TODO(ameshkov): tests depend on third-party services, rework this.
package cfcrypto_test

import (
"context"
"crypto/tls"
"fmt"
"io"
"net"
"net/http"
"testing"

"github.com/ameshkov/gocurl/internal/client/cfcrypto"
"github.com/ameshkov/gocurl/internal/config"
"github.com/ameshkov/gocurl/internal/output"
"github.com/ameshkov/gocurl/internal/resolve"
"github.com/stretchr/testify/require"
)

func TestHandshake_encryptedClientHello(t *testing.T) {
const relayDomain = "crypto.cloudflare.com"
const privateDomain = "cloudflare.com"
const path = "cdn-cgi/trace"

out, err := output.NewOutput("", false)
require.NoError(t, err)

cfg := &config.Config{ECH: true}

r, err := resolve.NewResolver(cfg, out)
require.NoError(t, err)

echConfigs, err := r.LookupECHConfigs("crypto.cloudflare.com")
require.NoError(t, err)
require.NotEmpty(t, echConfigs)

// Make sure that the resolved ECH configs will be used.
cfg.ECHConfigs = echConfigs

conn, err := net.Dial("tcp", fmt.Sprintf("%s:443", relayDomain))
require.NoError(t, err)

tlsConf := &tls.Config{
ServerName: privateDomain,
NextProtos: []string{"http/1.1"},
}

tlsConn, err := cfcrypto.Handshake(conn, tlsConf, r, cfg, out)
require.NoError(t, err)

u := fmt.Sprintf("https://%s/%s", privateDomain, path)
req, err := http.NewRequest(http.MethodGet, u, nil)
require.NoError(t, err)

transport := &http.Transport{
DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return tlsConn, nil
},
}
resp, err := transport.RoundTrip(req)
require.NoError(t, err)
defer func(Body io.ReadCloser) {
_ = Body.Close()
}(resp.Body)

body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.NotEmpty(t, body)

bodyStr := string(body)
require.Contains(t, bodyStr, "sni=encrypted")
}

func TestHandshake_postQuantum(t *testing.T) {
const domainName = "pq.cloudflareresearch.com"

out, err := output.NewOutput("", false)
require.NoError(t, err)

conn, err := net.Dial("tcp", fmt.Sprintf("%s:443", domainName))
require.NoError(t, err)

tlsConf := &tls.Config{
ServerName: "pq.cloudflareresearch.com",
NextProtos: []string{"http/1.1"},
}

cfg := &config.Config{
Experiments: map[config.Experiment]string{
config.ExpPostQuantum: "",
},
}

tlsConn, err := cfcrypto.Handshake(conn, tlsConf, nil, cfg, out)
require.NoError(t, err)

u := fmt.Sprintf("https://%s/", domainName)
req, err := http.NewRequest(http.MethodGet, u, nil)
require.NoError(t, err)

transport := &http.Transport{
DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return tlsConn, nil
},
}

resp, err := transport.RoundTrip(req)
require.NoError(t, err)
defer func(Body io.ReadCloser) {
_ = Body.Close()
}(resp.Body)

body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.NotEmpty(t, body)

bodyStr := string(body)
require.Contains(t, bodyStr, "You are using <em>X25519Kyber768Draft00</em>")
}
Original file line number Diff line number Diff line change
@@ -1,57 +1,13 @@
// Package ech is responsible for implementing the Encrypted ClientHello logic.
package ech
package cfcrypto

import (
"crypto/tls"
"net"
"time"

ctls "github.com/ameshkov/cfcrypto/tls"
"github.com/ameshkov/gocurl/internal/output"
)

// HandshakeECH attempts to establish a ECH-enabled connection using the
// specified echConfigs.
//
// A few things about tlsConfig that is passed to it:
// ServerName will be used in the inner ClientHello. For the outer ClientHello
// it will attempt to use the "public name" field of the ECH configuration.
// Regarding the multiple ECHConfig passed, it chooses the first with a suitable
// cipher suite which effectively means that it will almost always simply use
// the first ECHConfig from the slice.
func HandshakeECH(
conn net.Conn,
echConfigs []ctls.ECHConfig,
tlsConfig *tls.Config,
out *output.Output,
) (tlsConn net.Conn, err error) {
out.Debug("Attempting to establish a ECH-enabled connection")

// Copying the original tls config fields to ECH-enabled one.
conf := &ctls.Config{
ServerName: tlsConfig.ServerName,
MinVersion: tlsConfig.MinVersion,
MaxVersion: tlsConfig.MaxVersion,
InsecureSkipVerify: tlsConfig.InsecureSkipVerify,
NextProtos: tlsConfig.NextProtos,
ECHEnabled: true,
ClientECHConfigs: echConfigs,
}

c := ctls.Client(conn, conf)
err = c.Handshake()

if err != nil {
return nil, err
}

out.Debug("ECH-enabled connection has been established successfully")

return &connWrapper{
baseConn: c,
}, nil
}

// tlsConnectionStater is an interface that declares ConnectionState function
// of tls.Conn. The reason for implementing this is to allow HTTP client to
// get access to the TLS connection state and expose it via http.Response.TLS
Expand Down
20 changes: 9 additions & 11 deletions internal/client/clientdialer.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import (
"fmt"
"net"

"github.com/ameshkov/gocurl/internal/client/cfcrypto"
"github.com/ameshkov/gocurl/internal/client/connectto"
"github.com/ameshkov/gocurl/internal/client/dialer"
"github.com/ameshkov/gocurl/internal/client/ech"
"github.com/ameshkov/gocurl/internal/client/proxy"
"github.com/ameshkov/gocurl/internal/client/splittls"
"github.com/ameshkov/gocurl/internal/config"
Expand Down Expand Up @@ -64,8 +64,9 @@ func (d *clientDialer) DialTLSContext(_ context.Context, network, addr string) (
return nil, err
}

if d.cfg.ECH {
d.conn, err = d.handshakeECH(conn)
_, postQuantum := d.cfg.Experiments[config.ExpPostQuantum]
if d.cfg.ECH || postQuantum {
d.conn, err = d.handshakeCTLS(conn)
} else {
d.conn, err = d.handshakeTLS(conn)
}
Expand Down Expand Up @@ -119,14 +120,11 @@ func (d *clientDialer) handshakeTLS(conn net.Conn) (tlsConn net.Conn, err error)
return tlsClient, nil
}

// handshakeECH attempts to establish a ECH-enabled TLS connection.
func (d *clientDialer) handshakeECH(conn net.Conn) (tlsConn net.Conn, err error) {
echConfigs, err := d.resolver.LookupECHConfigs(d.tlsConfig.ServerName)
if err != nil {
return nil, err
}

return ech.HandshakeECH(conn, echConfigs, d.tlsConfig, d.out)
// handshakeCTLS attempts to establish a TLS connection using Cloudflare's fork
// of crypto/tls. This is necessary to enable some features missing from the
// standard library like ECH or post-quantum cryptography.
func (d *clientDialer) handshakeCTLS(conn net.Conn) (tlsConn net.Conn, err error) {
return cfcrypto.Handshake(conn, d.tlsConfig, d.resolver, d.cfg, d.out)
}

// createDialFunc creates dialFunc that implements all the logic configured by
Expand Down
Loading

0 comments on commit 312c05b

Please sign in to comment.