Skip to content

Secure tunnel abstraction

Introduction

Sandwich provides a secure tunnel abstraction. An example of such a tunnel is TLS. Sandwich slices the concept of tunnels into two different states:

  • a handshake plane where a shared key between two peers is generated
  • a record plane where actual protected data is exchanged

Sandwich uses a protobuf-based configuration for setting up such tunnels. Tunnels are either in client or server mode (from the underlying protocol point of view), depending on whether a ClientOptions or ServerOptions object is used in the overall tunnel configuration. Data are transported over an I/O object that must be initialised before creating a tunnel.

The protobuf-based configuration provides various runtime agility:

  • the cryptography backend that is used for protocol and cryptography implementation can be changed
  • the actual protocol (client / server)

It provides runtime agility as only the protobuf-based configuration needs to be changed (with no code modification), and that configuration can be provided at runtime. Closing and relaunching existing tunnels is now not done directly by Sandwich, and need to be handled by the users of the library.

Verifiers

Verifiers are used to verify the identity of the peer a sandwich tunnel is talking with. This is done by passing a TunnelVerifier to the Sandwich tunnel creation API through the TunnelConfiguration message.

For TLS, only SAN (Subject Alternative Name) can be verified for now.

Go example

Let's create a TLS server tunnel in Go. First, let's create a protobuf server configuration:

import (
    swapi "github.com/sandbox-quantum/sandwich/go/proto/sandwich/api/v1"
    sw "github.com/sandbox-quantum/sandwich/go"
    swtunnel "github.com/sandbox-quantum/sandwich/go/tunnel"
)

func createServerConfiguration(certfile *string, keyfile *string) *swapi.Configuration {
    return &swapi.Configuration{
        Impl: swapi.Implementation_IMPL_OPENSSL1_1_1_OQS,
        Opts: &swapi.Configuration_Server{
            Server: &swapi.ServerOptions{
                Opts: &swapi.ServerOptions_Tls{
                    Tls: &swapi.TLSServerOptions{
                        CommonOptions: &swapi.TLSOptions{
                            Tls13: &swapi.TLSv13Config{
                                Compliance: &swapi.Compliance{
                                    ClassicalChoice: swapi.ClassicalAlgoChoice_CLASSICAL_ALGORITHMS_ALLOW,
                                },
                                Ke: []string{
                                    "kyber768",
                                    "p256_kyber512",
                                    "prime256v1",
                                },
                            },
                            Tls12: &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",
                                },
                            },
                            PeerVerifier: &swapi.TLSOptions_EmptyVerifier{
                                EmptyVerifier: &swapi.EmptyVerifier{},
                            },
                            Identity: &swapi.X509Identity{
                                Certificate: &swapi.Certificate{
                                    Source: &swapi.Certificate_Static{
                                        Static: &swapi.ASN1DataSource{
                                            Data: &swapi.DataSource{
                                                Specifier: &swapi.DataSource_Filename{
                                                    Filename: *certfile,
                                                },
                                            },
                                            Format: swapi.ASN1EncodingFormat_ENCODING_FORMAT_PEM,
                                        },
                                    },
                                },
                                PrivateKey: &swapi.PrivateKey{
                                    Source: &swapi.PrivateKey_Static{
                                        Static: &swapi.ASN1DataSource{
                                            Data: &swapi.DataSource{
                                                Specifier: &swapi.DataSource_Filename{
                                                    Filename: *keyfile,
                                                },
                                            },
                                            Format: swapi.ASN1EncodingFormat_ENCODING_FORMAT_PEM,
                                        },
                                    },
                                },
                            },
                        },
                    },
                },
            },
        },
    }
}

That configuration uses a private key and public certificate that are stored on disk, and accepts kyber768, p256_kyber512 and secp256k1 as key exchange mechanisms.

Assuming we have a valid Sandwich I/O object, we can then create a sandwich tunnel:

import (
    swapi "github.com/sandbox-quantum/sandwich/go/proto/sandwich/api/v1"
    sw "github.com/sandbox-quantum/sandwich/go"
    swtunnel "github.com/sandbox-quantum/sandwich/go/tunnel"
)

{
    swio := // ...
    tunnel, err := swtunnel.NewTunnelWithReadWriter(ctx, conn, &swapi.TunnelConfiguration{
        Verifier: &swapi.TunnelVerifier{
            Verifier: &swapi.TunnelVerifier_EmptyVerifier{
                EmptyVerifier: &swapi.EmptyVerifier{},
            },
        },
    })
}

The underlying I/O object can come for instance from a TCP listener object that has accepted a new connection. An end-to-end example creating an echo TLS server is available in examples/go/echo_tls_server.

Python example

Let's create a TLS server tunnel in Python. First, let's create a protobuf server configuration:

import pysandwich.proto.api.v1.compliance_pb2 as Compliance
import pysandwich.proto.api.v1.configuration_pb2 as SandwichTunnelProto
import pysandwich.proto.api.v1.encoding_format_pb2 as EncodingFormat
import pysandwich.proto.api.v1.listener_configuration_pb2 as ListenerAPI
import pysandwich.proto.api.v1.verifiers_pb2 as SandwichVerifiers
import pysandwich.io_helpers as SandwichIOHelpers
import pysandwich.tunnel as SandwichTunnel
from pysandwich.proto.api.v1.tunnel_pb2 import TunnelConfiguration
from pysandwich import listener as SandwichListener
from pysandwich.sandwich import Sandwich


def create_server_conf(cert_path: str, key_path: str) -> SandwichTunnelProto:
    conf = SandwichTunnelProto.Configuration()
    conf.impl = SandwichTunnelProto.IMPL_OPENSSL1_1_1_OQS

    # Sets TLS 1.3 Compliance, Key Establishment (KE) and Ciphersuites.
    tls13 = conf.server.tls.common_options.tls13
    tls13.ke.append("X25519")
    tls13.compliance.classical_choice = Compliance.CLASSICAL_ALGORITHMS_ALLOW

    # Sets TLS 1.2 Ciphersuite.
    tls12 = conf.server.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)

    conf.server.tls.common_options.empty_verifier.CopyFrom(
        SandwichVerifiers.EmptyVerifier()
    )
    conf.server.tls.common_options.identity.certificate.static.data.filename = cert_path
    conf.server.tls.common_options.identity.certificate.static.format = (
        EncodingFormat.ENCODING_FORMAT_PEM
    )

    conf.server.tls.common_options.identity.private_key.static.data.filename = key_path
    conf.server.tls.common_options.identity.private_key.static.format = (
        EncodingFormat.ENCODING_FORMAT_PEM
    )

    return conf


def create_server_tun_conf() -> TunnelConfiguration:
    tun_conf = TunnelConfiguration()
    tun_conf.verifier.empty_verifier.CopyFrom(SandwichVerifiers.EmptyVerifier())
    return tun_conf


def create_tcp_listener(hostname: str, port: int) -> SandwichListener.Listener:
    """Creates the configuration for a TCP listener.
    Returns:
        A TCP listener which is listening on hostname:port.
    """
    conf = ListenerAPI.ListenerConfiguration()
    conf.tcp.addr.hostname = hostname
    conf.tcp.addr.port = port
    conf.tcp.blocking_mode = ListenerAPI.BLOCKINGMODE_BLOCKING

    return SandwichListener.Listener(conf)

That configuration uses a private key and public certificate that are stored on disk, and accepts kyber768 and prime256v1 as key exchange mechanisms.

Assuming we have a valid Sandwich I/O object, we can then create a sandwich tunnel:

def my_func():
        server_tun_conf = create_server_tun_conf()
        server = SandwichTunnel.Tunnel(server_ctx_conf, swio, server_tun_conf)

        server.handshake()
        state = server.state()
        assert (
            state == server.State.STATE_HANDSHAKE_DONE
        ), f"Expected state HANDSHAKE_DONE, got {state}"

        while True:
            data = b""
            while True:
                c = server.read(1)
                data += c
                if c == b"\n":
                    break
            server.write(data)

        server.close()
        swio = # ...

An end-to-end example creating an echo TLS server is available in examples/python/echo_tls_server.