tls-server #33

Closed
JakeHillion wants to merge 1 commits from tls-server into main
7 changed files with 774 additions and 0 deletions

150
Cargo.lock generated
View File

@ -28,6 +28,12 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "base64"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
[[package]]
name = "bitflags"
version = "1.3.2"
@ -61,6 +67,12 @@ dependencies = [
"rustc_version",
]
[[package]]
name = "cc"
version = "1.0.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
[[package]]
name = "cfg-if"
version = "1.0.0"
@ -114,7 +126,10 @@ dependencies = [
"ipnetwork",
"libc",
"log",
"mio",
"nix",
"rustls",
"rustls-pemfile",
"serde",
"serde_json",
"tempfile",
@ -389,6 +404,18 @@ dependencies = [
"autocfg",
]
[[package]]
name = "mio"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "713d550d9b44d89174e066b7a6217ae06234c10cb47819a88290d2b353c31799"
dependencies = [
"libc",
"log",
"wasi",
"windows-sys",
]
[[package]]
name = "nix"
version = "0.24.1"
@ -420,6 +447,12 @@ dependencies = [
"libc",
]
[[package]]
name = "once_cell"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b10983b38c53aebdf33f542c6275b0f58a238129d00c4ae0e6fb59738d783ca"
[[package]]
name = "oorandom"
version = "11.1.3"
@ -544,6 +577,21 @@ dependencies = [
"winapi",
]
[[package]]
name = "ring"
version = "0.16.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc"
dependencies = [
"cc",
"libc",
"once_cell",
"spin",
"untrusted",
"web-sys",
"winapi",
]
[[package]]
name = "rustc_version"
version = "0.4.0"
@ -553,6 +601,27 @@ dependencies = [
"semver",
]
[[package]]
name = "rustls"
version = "0.20.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aab8ee6c7097ed6057f43c187a62418d0c05a4bd5f18b3571db50ee0f9ce033"
dependencies = [
"log",
"ring",
"sct",
"webpki",
]
[[package]]
name = "rustls-pemfile"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7522c9de787ff061458fe9a829dc790a3f5b22dc571694fc5883f448b94d9a9"
dependencies = [
"base64",
]
[[package]]
name = "ryu"
version = "1.0.9"
@ -574,6 +643,16 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "sct"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4"
dependencies = [
"ring",
"untrusted",
]
[[package]]
name = "semver"
version = "1.0.6"
@ -621,6 +700,12 @@ dependencies = [
"serde",
]
[[package]]
name = "spin"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
[[package]]
name = "strsim"
version = "0.10.0"
@ -718,6 +803,12 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
[[package]]
name = "untrusted"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
[[package]]
name = "walkdir"
version = "2.3.2"
@ -729,6 +820,12 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm-bindgen"
version = "0.2.79"
@ -793,6 +890,16 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "webpki"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd"
dependencies = [
"ring",
"untrusted",
]
[[package]]
name = "winapi"
version = "0.3.9"
@ -823,3 +930,46 @@ name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-sys"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2"
dependencies = [
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_msvc"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47"
[[package]]
name = "windows_i686_gnu"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6"
[[package]]
name = "windows_i686_msvc"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024"
[[package]]
name = "windows_x86_64_gnu"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1"
[[package]]
name = "windows_x86_64_msvc"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"

View File

@ -25,6 +25,10 @@ tempfile = "3.3"
[dev-dependencies]
criterion = "0.3"
rustls = "0.20"
rustls-pemfile = "1.0.0"
mio = { version = "0.8", features = ["net", "os-poll", "os-ext"] }
[[bench]]
name = "clone3"
harness = false

View File

@ -32,6 +32,16 @@ To run this example:
cargo build --example pipes
target/debug/clone-shim -s examples/pipes/spec.json target/debug/examples/pipes
### examples/tls
The tls example runs a fully privilege separated TLS server under the shim. A TCP listener is handed to a "tcp_listener" process. This spawns separated "tls_handler" processes, which are the only processes with access to the certificate files. Finally, a "request_handler" process is handed shared memory with the "tls_handler" to handle the TLS request.
To run this example:
cargo build
cargo build --example tls
target/debug/clone-shim -s examples/tls/spec.json target/debug/examples/tls
## Debugging the shim
The shim can be debugged as with most processes, but it is exceptionally forky. Breaking before a clone in `rust-gdb` then running `set follow-fork-mode child` is often necessary. The best approach is to go in with a plan of attack.

10
examples/tls/http.rs Normal file
View File

@ -0,0 +1,10 @@
use std::fs::File;
use std::io::Write;
pub(super) fn handler(_request: File, mut response: File) -> i32 {
println!("entered http handler");
response.write_all(b"hello world!").unwrap();
0
}

177
examples/tls/main.rs Normal file
View File

@ -0,0 +1,177 @@
mod http;
mod tls;
use std::fs::File;
use std::net::{TcpListener, TcpStream};
use std::os::unix::io::IntoRawFd;
fn main() {
let mut args = std::env::args();
let _bin = args.next();
let entrypoint = args.next();
match entrypoint {
Some(s) => match s.as_str() {
"connection_listener" => connection_listener_entrypoint(),
"tls_handler" => tls_handler_entrypoint(),
"request_handler" => request_handler_entrypoint(),
_ => unimplemented!(),
},
None => unimplemented!(),
}
}
fn connection_listener_entrypoint() {
// imports
use std::os::unix::io::{FromRawFd, RawFd};
// argument parsing
let mut args = std::env::args();
let _bin = args.next();
let _entrypoint = args.next();
let tcp_listener = args.next();
let tcp_listener: RawFd = tcp_listener
.expect("tcp listener required")
.parse()
.expect("tcp listener should be a file descriptor");
let tcp_listener = unsafe { TcpListener::from_raw_fd(tcp_listener) };
let tls_handler_trigger = args.next();
let tls_handler_trigger: RawFd = tls_handler_trigger
.expect("request handler required")
.parse()
.expect("tcp listener should be a file descriptor");
let tls_handler_trigger = unsafe { File::from_raw_fd(tls_handler_trigger) };
// real function body
fn connection_listener(tls_handler_trigger: File, tcp_listener: TcpListener) -> i32 {
println!("connection_listener entered");
// handle incoming connections
for stream in tcp_listener.incoming() {
let stream = match stream {
Ok(s) => s,
Err(e) => {
println!("connection listener: error: {}", e);
return 1;
}
};
tls_handler(&tls_handler_trigger, stream);
}
0
}
std::process::exit(connection_listener(tls_handler_trigger, tcp_listener))
}
fn tls_handler(trigger_socket: &File, stream: TcpStream) {
// imports
use nix::sys::socket::{sendmsg, ControlMessage, MsgFlags};
use std::os::unix::io::AsRawFd;
// send file descriptor(s)
let sockfd = trigger_socket.as_raw_fd();
let fds = [stream.into_raw_fd()];
sendmsg::<()>(
sockfd,
&[],
&[ControlMessage::ScmRights(&fds)],
MsgFlags::empty(),
None,
)
.unwrap();
}
fn tls_handler_entrypoint() {
// imports
use std::os::unix::io::{FromRawFd, RawFd};
// argument parsing
let mut args = std::env::args();
let _bin = args.next();
let _entrypoint = args.next();
let request_handler_trigger = args.next();
let request_handler_trigger: RawFd = request_handler_trigger
.expect("request handler required")
.parse()
.expect("request handler should be a file descriptor");
let request_handler_trigger = unsafe { File::from_raw_fd(request_handler_trigger) };
let cert = args.next();
let cert: RawFd = cert
.expect("cert required")
.parse()
.expect("cert should be a file descriptor");
let cert = unsafe { File::from_raw_fd(cert) };
let key = args.next();
let key: RawFd = key
.expect("key required")
.parse()
.expect("key should be a file descriptor");
let key = unsafe { File::from_raw_fd(key) };
let stream = args.next();
let stream: RawFd = stream
.expect("tcp stream required")
.parse()
.expect("tcp stream should be a file descriptor");
let stream = unsafe { TcpStream::from_raw_fd(stream) };
std::process::exit(tls::handler(request_handler_trigger, cert, key, stream))
}
fn request_handler(trigger_socket: &File, requestfd: impl IntoRawFd, responsefd: impl IntoRawFd) {
// imports
use nix::sys::socket::{sendmsg, ControlMessage, MsgFlags};
use std::os::unix::io::AsRawFd;
// send file descriptor(s)
let sockfd = trigger_socket.as_raw_fd();
let fds = [requestfd.into_raw_fd(), responsefd.into_raw_fd()];
sendmsg::<()>(
sockfd,
&[],
&[ControlMessage::ScmRights(&fds)],
MsgFlags::empty(),
None,
)
.unwrap();
}
fn request_handler_entrypoint() {
// imports
use std::os::unix::io::{FromRawFd, RawFd};
// argument parsing
let mut args = std::env::args();
let _bin = args.next();
let _entrypoint = args.next();
let request = args.next();
let request: RawFd = request
.expect("request stream required")
.parse()
.expect("request stream should be a file descriptor");
let request = unsafe { File::from_raw_fd(request) };
let response = args.next();
let response: RawFd = response
.expect("response stream required")
.parse()
.expect("response stream should be a file descriptor");
let response = unsafe { File::from_raw_fd(response) };
std::process::exit(http::handler(request, response))
}

117
examples/tls/spec.json Normal file
View File

@ -0,0 +1,117 @@
{
"entrypoints": {
"connection_listener": {
"args": [
"BinaryName",
"Entrypoint",
{
"TcpListener": {
"addr": "0.0.0.0:8443"
}
},
{
"FileSocket": {
"Tx": "tls"
}
}
],
"environment": [
{
"Filesystem": {
"host_path": "/lib/x86_64-linux-gnu/libgcc_s.so.1",
"environment_path": "/lib/libgcc_s.so.1"
}
},
{
"Filesystem": {
"host_path": "/lib/x86_64-linux-gnu/libc.so.6",
"environment_path": "/lib/libc.so.6"
}
},
{
"Filesystem": {
"host_path": "/lib64/ld-linux-x86-64.so.2",
"environment_path": "/lib64/ld-linux-x86-64.so.2"
}
}
]
},
"tls_handler": {
"trigger": {
"FileSocket": "tls"
},
"args": [
"BinaryName",
"Entrypoint",
{
"FileSocket": {
"Tx": "request"
}
},
{
"File": "/etc/ssl/certs/example.com.pem"
},
{
"File": "/etc/ssl/private/example.com.key"
},
"Trigger"
],
"environment": [
{
"Filesystem": {
"host_path": "/lib/x86_64-linux-gnu/libgcc_s.so.1",
"environment_path": "/lib/libgcc_s.so.1"
}
},
{
"Filesystem": {
"host_path": "/lib/x86_64-linux-gnu/libc.so.6",
"environment_path": "/lib/libc.so.6"
}
},
{
"Filesystem": {
"host_path": "/lib64/ld-linux-x86-64.so.2",
"environment_path": "/lib64/ld-linux-x86-64.so.2"
}
}
]
},
"request_handler": {
"trigger": {
"FileSocket": "request"
},
"args": [
"BinaryName",
"Entrypoint",
"Trigger"
],
"environment": [
{
"Filesystem": {
"host_path": "/var/www/html",
"environment_path": "/var/www/html"
}
},
{
"Filesystem": {
"host_path": "/lib/x86_64-linux-gnu/libgcc_s.so.1",
"environment_path": "/lib/libgcc_s.so.1"
}
},
{
"Filesystem": {
"host_path": "/lib/x86_64-linux-gnu/libc.so.6",
"environment_path": "/lib/libc.so.6"
}
},
{
"Filesystem": {
"host_path": "/lib64/ld-linux-x86-64.so.2",
"environment_path": "/lib64/ld-linux-x86-64.so.2"
}
}
]
}
}
}

306
examples/tls/tls.rs Normal file
View File

@ -0,0 +1,306 @@
/* SPDX-License-Identifier: MIT */
//
// This example is adapted from rustls/rustls-mio/examples/tlsserver.rs
// Both pieces of work are under the MIT license. Thanks to the original
// author.
//
// Copyright (c) 2016 Joseph Birr-Pixton <jpixton@gmail.com>
// Copyright (c) 2022 Jake Hillion <jake@hillion.co.uk>
use log::{debug, error};
use super::request_handler;
use std::collections::HashMap;
use std::fs::File;
use std::io;
use std::io::{BufReader, Read, Write};
use std::net;
use std::os::unix::io::{AsRawFd, FromRawFd};
use std::sync::Arc;
use mio::net::{TcpListener, TcpStream};
use mio::unix::pipe;
use rustls;
pub(super) fn handler(
request_handler_trigger: File,
mut cert: File,
mut key: File,
stream: net::TcpStream,
) -> i32 {
println!("tls handler entered");
let config = make_config(&mut cert, &mut key);
let mut stream = TcpStream::from_std(stream);
let mut poll = mio::Poll::new().unwrap();
poll.registry()
.register(&mut stream, TCP_STREAM, mio::Interest::READABLE)
.unwrap();
let (tls_data_in_send, mut tls_data_in_recv) = pipe::new().unwrap();
poll.registry()
.register(&mut tls_data_in_recv, TLS_DATA_IN, mio::Interest::READABLE)
.unwrap();
let (mut tls_data_out_send, tls_data_out_recv) = pipe::new().unwrap();
request_handler(
&request_handler_trigger,
tls_data_out_recv, // request
tls_data_in_send, // response
);
let mut events = mio::Events::with_capacity(256);
let mut read_buf = [0_u8; 1024];
loop {
poll.poll(&mut events, None).unwrap();
for event in events.iter() {
match event.token() {
TCP_STREAM => {}
TLS_DATA_IN => {
let read_bytes = tls_data_in_recv.read(&mut read_buf).unwrap();
// TODO: encrypt me
}
_ => unreachable!("only two tokens are registered"),
}
}
}
0
}
const TCP_STREAM: mio::Token = mio::Token(0);
const TLS_DATA_IN: mio::Token = mio::Token(1);
/// This binds together a TCP listening socket, some outstanding
/// connections, and a TLS server configuration.
struct TlsServer {
server: TcpListener,
connections: HashMap<mio::Token, OpenConnection>,
next_id: usize,
tls_config: Arc<rustls::ServerConfig>,
}
/// This is a connection which has been accepted by the server,
/// and is currently being served.
///
/// It has a TCP-level stream, a TLS-level connection state, and some
/// other state/metadata.
struct OpenConnection {
socket: TcpStream,
token: mio::Token,
closing: bool,
closed: bool,
tls_conn: rustls::ServerConnection,
sent_http_response: bool,
}
/// This used to be conveniently exposed by mio: map EWOULDBLOCK
/// errors to something less-errory.
fn try_read(r: io::Result<usize>) -> io::Result<Option<usize>> {
match r {
Ok(len) => Ok(Some(len)),
Err(e) => {
if e.kind() == io::ErrorKind::WouldBlock {
Ok(None)
} else {
Err(e)
}
}
}
}
impl OpenConnection {
fn new(
socket: TcpStream,
token: mio::Token,
tls_conn: rustls::ServerConnection,
) -> OpenConnection {
OpenConnection {
socket,
token,
closing: false,
closed: false,
tls_conn,
sent_http_response: false,
}
}
/// We're a connection, and we have something to do.
fn ready(&mut self, registry: &mio::Registry, ev: &mio::event::Event) {
// If we're readable: read some TLS. Then
// see if that yielded new plaintext. Then
// see if the backend is readable too.
if ev.is_readable() {
self.do_tls_read();
self.try_plain_read();
}
if ev.is_writable() {
self.do_tls_write_and_handle_error();
}
if self.closing {
let _ = self.socket.shutdown(net::Shutdown::Both);
self.closed = true;
self.deregister(registry);
} else {
self.reregister(registry);
}
}
fn do_tls_read(&mut self) {
// Read some TLS data.
match self.tls_conn.read_tls(&mut self.socket) {
Err(err) => {
if let io::ErrorKind::WouldBlock = err.kind() {
return;
}
error!("read error {:?}", err);
self.closing = true;
return;
}
Ok(0) => {
debug!("eof");
self.closing = true;
return;
}
Ok(_) => {}
};
// Process newly-received TLS messages.
if let Err(err) = self.tls_conn.process_new_packets() {
error!("cannot process packet: {:?}", err);
// last gasp write to send any alerts
self.do_tls_write_and_handle_error();
self.closing = true;
}
}
fn try_plain_read(&mut self) {
// Read and process all available plaintext.
if let Ok(io_state) = self.tls_conn.process_new_packets() {
if io_state.plaintext_bytes_to_read() > 0 {
let mut buf = Vec::new();
buf.resize(io_state.plaintext_bytes_to_read(), 0u8);
self.tls_conn.reader().read_exact(&mut buf).unwrap();
debug!("plaintext read {:?}", buf.len());
self.incoming_plaintext(&buf);
}
}
}
/// Process some amount of received plaintext.
fn incoming_plaintext(&mut self, buf: &[u8]) {
self.tls_conn.writer().write_all(buf).unwrap();
}
fn send_http_response_once(&mut self) {
let response =
b"HTTP/1.0 200 OK\r\nConnection: close\r\n\r\nHello world from rustls tlsserver\r\n";
if !self.sent_http_response {
self.tls_conn.writer().write_all(response).unwrap();
self.sent_http_response = true;
self.tls_conn.send_close_notify();
}
}
fn tls_write(&mut self) -> io::Result<usize> {
self.tls_conn.write_tls(&mut self.socket)
}
fn do_tls_write_and_handle_error(&mut self) {
let rc = self.tls_write();
if rc.is_err() {
error!("write failed {:?}", rc);
self.closing = true;
}
}
fn register(&mut self, registry: &mio::Registry) {
let event_set = self.event_set();
registry
.register(&mut self.socket, self.token, event_set)
.unwrap();
}
fn reregister(&mut self, registry: &mio::Registry) {
let event_set = self.event_set();
registry
.reregister(&mut self.socket, self.token, event_set)
.unwrap();
}
fn deregister(&mut self, registry: &mio::Registry) {
registry.deregister(&mut self.socket).unwrap();
}
/// What IO events we're currently waiting for,
/// based on wants_read/wants_write.
fn event_set(&self) -> mio::Interest {
let rd = self.tls_conn.wants_read();
let wr = self.tls_conn.wants_write();
if rd && wr {
mio::Interest::READABLE | mio::Interest::WRITABLE
} else if wr {
mio::Interest::WRITABLE
} else {
mio::Interest::READABLE
}
}
fn is_closed(&self) -> bool {
self.closed
}
}
fn load_certs(certfile: &mut File) -> Vec<rustls::Certificate> {
let mut reader = BufReader::new(certfile);
rustls_pemfile::certs(&mut reader)
.unwrap()
.iter()
.map(|v| rustls::Certificate(v.clone()))
.collect()
}
fn load_private_key(keyfile: &mut File) -> rustls::PrivateKey {
let mut reader = BufReader::new(keyfile);
loop {
match rustls_pemfile::read_one(&mut reader).expect("cannot parse private key .pem file") {
Some(rustls_pemfile::Item::RSAKey(key)) => return rustls::PrivateKey(key),
Some(rustls_pemfile::Item::PKCS8Key(key)) => return rustls::PrivateKey(key),
Some(rustls_pemfile::Item::ECKey(key)) => return rustls::PrivateKey(key),
None => break,
_ => {}
}
}
panic!("no keys found in supplied file (encrypted keys not supported)");
}
fn make_config(cert: &mut File, key: &mut File) -> Arc<rustls::ServerConfig> {
let certs = load_certs(cert);
let privkey = load_private_key(key);
let config = rustls::ServerConfig::builder()
.with_safe_default_cipher_suites()
.with_safe_default_kx_groups()
.with_safe_default_protocol_versions()
.expect("inconsistent cipher-suites/versions specified")
.with_no_client_auth()
.with_single_cert(certs, privkey)
.expect("bad certificates/private key");
Arc::new(config)
}