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
.