TLS client example
This example creates a netcat-like client for TLS servers using Sandwich. It does not validate any server-side certificates, and should be used for educational purposes only.
Go implementation
The Go version makes usage of Go channels for an efficient implementation. It can be run with:
Its source code is the following (examples/go/tls_client/main.go
):
// Copyright (c) SandboxAQ. All rights reserved.
// SPDX-License-Identifier: AGPL-3.0-only
package main
import (
"flag"
"fmt"
"io"
"log"
"os"
"os/signal"
"syscall"
swapi "github.com/sandbox-quantum/sandwich/go/proto/sandwich/api/v1"
sw "github.com/sandbox-quantum/sandwich/go"
swio "github.com/sandbox-quantum/sandwich/go/io"
swtunnel "github.com/sandbox-quantum/sandwich/go/tunnel"
)
func createClientConfiguration(cert *string, tls_version *string) *swapi.Configuration {
EmptyVerifier := &swapi.TLSOptions_EmptyVerifier{
EmptyVerifier: &swapi.EmptyVerifier{},
}
x509_verifier := &swapi.TLSOptions_X509Verifier{
X509Verifier: &swapi.X509Verifier{
TrustedCas: []*swapi.Certificate{
{
Source: &swapi.Certificate_Static{
Static: &swapi.ASN1DataSource{
Data: &swapi.DataSource{
Specifier: &swapi.DataSource_Filename{
Filename: *cert,
},
},
Format: swapi.ASN1EncodingFormat_ENCODING_FORMAT_PEM,
},
},
},
},
},
}
config := &swapi.Configuration{
Impl: swapi.Implementation_IMPL_OPENSSL1_1_1_OQS,
Opts: &swapi.Configuration_Client{
Client: &swapi.ClientOptions{
Opts: &swapi.ClientOptions_Tls{
Tls: &swapi.TLSClientOptions{
CommonOptions: &swapi.TLSOptions{
PeerVerifier: &swapi.TLSOptions_EmptyVerifier{
EmptyVerifier: &swapi.EmptyVerifier{},
},
},
},
},
},
},
}
tls13config := &swapi.TLSv13Config{
Compliance: &swapi.Compliance{
ClassicalChoice: swapi.ClassicalAlgoChoice_CLASSICAL_ALGORITHMS_ALLOW,
},
Ke: []string{
"kyber768",
"p256_kyber512",
"prime256v1",
},
}
tls12config := &swapi.TLSv12Config{
Ciphersuite: []string{
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
"TLS_RSA_WITH_AES_256_GCM_SHA384",
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
"TLS_RSA_WITH_AES_128_GCM_SHA256",
},
}
switch *tls_version {
case "tls12":
config.GetClient().GetTls().CommonOptions.Tls12 = tls12config
case "tls13":
config.GetClient().GetTls().CommonOptions.Tls13 = tls13config
default:
log.Fatalln("TLS version is not supported")
}
if len(*cert) == 0 {
config.GetClient().GetTls().CommonOptions.PeerVerifier = EmptyVerifier
} else {
config.GetClient().GetTls().CommonOptions.PeerVerifier = x509_verifier
}
return config
}
func main() {
host := flag.String("host", "", "Host to connect to")
port := flag.Int64("port", 0, "TCP port to connect to")
tls_version := flag.String("tls_version", "", "TLS version: --tls_version tls13 or tls12")
cert := flag.String("server_cert", "", "Server certificates")
flag.Parse()
if *port == 0 || *host == "" {
log.Fatalln("Please provide a client host and port!")
}
if *tls_version == "" {
log.Fatalln("Please provide a TLS protocol version, e.g --tls_version tls13 or tls12")
}
swio, ioerr := swio.IOTCPClient(*host, uint16(*port))
if ioerr != nil {
log.Fatalln("Error connecting to destination:", ioerr)
return
}
sw_lib_ctx := sw.NewSandwich()
ctx, err := swtunnel.NewTunnelContext(sw_lib_ctx, createClientConfiguration(cert, tls_version))
if err != nil {
log.Fatalln("Error create tunnel context:", err)
return
}
tunnel, err := swtunnel.NewTunnelWithReadWriter(ctx, swio, &swapi.TunnelConfiguration{
Verifier: &swapi.TunnelVerifier{
Verifier: &swapi.TunnelVerifier_EmptyVerifier{
EmptyVerifier: &swapi.EmptyVerifier{},
},
},
})
if err != nil {
log.Fatalln(err)
}
err = tunnel.Handshake()
if err != nil {
log.Fatalln(err)
}
errChannel := make(chan error)
// Copy data from stdin to destination using io.Copy
go func() {
_, err := io.Copy(tunnel, os.Stdin)
errChannel <- err
}()
// Copy data from source to stdout using io.Copy
go func() {
_, err := io.Copy(os.Stdout, tunnel)
errChannel <- err
}()
// Handle Ctrl+C signal to gracefully close the connection
interruptChannel := make(chan os.Signal, 1)
signal.Notify(interruptChannel, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-interruptChannel
os.Exit(0)
}()
// Wait for the data copying goroutine to finish
err = <-errChannel
if err != nil && err != io.EOF {
fmt.Println("Error:", err)
}
}
Python implementation
The Python version can be run with:
Its source code is the following (examples/python/tls_client/main.py
):
# Copyright (c) SandboxAQ. All rights reserved.
# SPDX-License-Identifier: AGPL-3.0-only
import selectors
import socket
import sys
from multiprocessing.connection import Connection
from typing import BinaryIO
import pysandwich.proto.api.v1.compliance_pb2 as Compliance
import pysandwich.proto.api.v1.configuration_pb2 as SandwichTunnelProto
import pysandwich.proto.api.v1.verifiers_pb2 as SandwichVerifiers
import pysandwich.errors as SandwichErrors
import pysandwich.io_helpers as SandwichIOHelpers
import pysandwich.tunnel as SandwichTunnel
from pysandwich.proto.api.v1.tunnel_pb2 import TunnelConfiguration
from pysandwich.sandwich import Sandwich
def create_client_conf(tls: str) -> SandwichTunnelProto:
"""Create Client configuration."""
conf = SandwichTunnelProto.Configuration()
conf.impl = SandwichTunnelProto.IMPL_BORINGSSL_OQS
match tls:
case "tls13":
# Sets TLS 1.3 Compliance, Key Establishment (KE), and Ciphersuite.
tls13 = conf.client.tls.common_options.tls13
tls13.ke.append("X25519")
tls13.compliance.classical_choice = Compliance.CLASSICAL_ALGORITHMS_ALLOW
tls13.ciphersuite.extend(["TLS_CHACHA20_POLY1305_SHA256"])
case "tls12":
tls12 = conf.client.tls.common_options.tls12
ciphers = [
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
"TLS_RSA_WITH_AES_256_GCM_SHA384",
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
"TLS_RSA_WITH_AES_128_GCM_SHA256",
]
tls12.ciphersuite.extend(ciphers)
case _:
raise NotImplementedError("TLS version is not supported")
conf.client.tls.common_options.empty_verifier.CopyFrom(
SandwichVerifiers.EmptyVerifier()
)
return conf
def is_localhost(hostname: str):
try:
# Get the IP address for the given hostname
ip_address = socket.gethostbyname(hostname)
# Check if the IP address is a localhost IP address
return ip_address in ("127.0.0.1", "::1")
except socket.gaierror:
# If the hostname cannot be resolved, it's not localhost
return False
def create_client_tun_conf(hostname: str) -> TunnelConfiguration:
tun_conf = TunnelConfiguration()
if not is_localhost(hostname):
tun_conf.server_name_indication = hostname
tun_conf.verifier.empty_verifier.CopyFrom(SandwichVerifiers.EmptyVerifier())
return tun_conf
def run_client(
host: str,
port: int,
input_r: Connection | BinaryIO,
output_w: Connection | BinaryIO,
client_ctx_conf: SandwichTunnel.Context,
):
"""Connect to server with a Context"""
while True:
try:
client_io = socket.create_connection((host, port))
break
except ConnectionRefusedError:
pass
swio = SandwichIOHelpers.io_socket_wrap(client_io)
client_tun_conf = create_client_tun_conf(host)
client = SandwichTunnel.Tunnel(
client_ctx_conf,
swio,
client_tun_conf,
)
assert client is not None
client.handshake()
state = client.state()
assert (
state == client.State.STATE_HANDSHAKE_DONE
), f"Expected state HANDSHAKE_DONE, got {state}"
sel = selectors.DefaultSelector()
sel.register(input_r, selectors.EVENT_READ, data=None)
sel.register(client_io, selectors.EVENT_READ, data=None)
client_io.setblocking(False)
while True:
events = sel.select(timeout=None)
for key, _ in events:
if key.fileobj is client_io:
try:
data = client.read(1024)
except SandwichErrors.RecordPlaneWantReadException:
continue
if not data:
sel.unregister(client_io)
else:
if isinstance(output_w, Connection):
output_w.send_bytes(data)
else:
output_w.write(b">")
output_w.write(data)
output_w.flush()
elif key.fileobj is input_r:
if isinstance(input_r, Connection):
data = input_r.recv_bytes(16)
else:
data = input_r.readline()
if not data:
sel.unregister(input_r)
else:
client_io.setblocking(True)
client.write(data)
client_io.setblocking(False)
client.close()
def main(
hostname: str,
port: int,
tls: str,
input_r: Connection | BinaryIO,
output_w: Connection | BinaryIO,
):
sw = Sandwich()
client_conf = create_client_conf(tls)
client_ctx = SandwichTunnel.Context.from_config(sw, client_conf)
run_client(hostname, port, input_r, output_w, client_ctx)
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(prog="TLS client using Sandwich")
parser.add_argument(
"-p",
"--port",
type=int,
help="Port to connect to (defaults to 443)",
default=443,
required=True,
)
parser.add_argument("--host", type=str, help="Host to connect to", required=True)
parser.add_argument(
"--tls_version",
type=str,
help="TLS version: --tls_version tls13 or tls12",
required=True,
)
args = parser.parse_args()
main(args.host, args.port, args.tls_version, sys.stdin.buffer, sys.stdout.buffer)
Rust implementation
The Rust version can be run with:
It only works for Linux so far, as it's using the epoll
API.
Its source code is the following (examples/rust/tls_client/main.rs
):