1
1
mirror of https://github.com/wez/wezterm.git synced 2024-11-27 12:23:46 +03:00

mux: tls: auto-bootstrap via ssh

This makes the tls channel much easier to use; the config can now be as
simple as this on the server side:

```toml
[[tls_servers]]
bind_address = "192.168.1.8:8080"
```

and this on the client side:

```
[[tls_clients]]
name = "hostname"
bootstrap_via_ssh = "192.168.1.8"
remote_address = "hostname:8080"
```

and then `wezterm connect hostname` will use ssh to connect to the
host, start the mux server, request the CA and client certs and
then connect to it over TLS.

This is implemented only for openssl at the moment.
This commit is contained in:
Wez Furlong 2020-02-02 08:01:05 -08:00
parent d2080a4e90
commit 9b02089849
8 changed files with 170 additions and 42 deletions

View File

@ -1,4 +1,5 @@
use crate::config::*;
use crate::SshParameters;
#[derive(Default, Debug, Clone, Deserialize)]
pub struct TlsDomainServer {
@ -30,6 +31,11 @@ pub struct TlsDomainClient {
/// all types of domain in the configuration file.
pub name: String,
/// If set, use ssh to connect, start the server, and obtain
/// a certificate.
/// The value is "user@host:port", just like "wezterm ssh" accepts.
pub bootstrap_via_ssh: Option<String>,
/// identifies the host:port pair of the remote server.
pub remote_address: String,
@ -74,3 +80,11 @@ pub struct TlsDomainClient {
#[serde(default = "default_write_timeout")]
pub write_timeout: Duration,
}
impl TlsDomainClient {
pub fn ssh_parameters(&self) -> Option<anyhow::Result<SshParameters>> {
self.bootstrap_via_ssh
.as_ref()
.map(|user_at_host_and_port| SshParameters::parse(user_at_host_and_port))
}
}

View File

@ -156,6 +156,9 @@ enum CliSubCommand {
#[structopt(name = "proxy", about = "start rpc proxy pipe")]
Proxy,
#[structopt(name = "tlscreds", about = "obtain tls credentials")]
TlsCreds,
}
#[derive(Debug, StructOpt, Clone)]
@ -300,9 +303,9 @@ pub fn running_under_wsl() -> bool {
false
}
struct SshParameters {
username: String,
host_and_port: String,
pub struct SshParameters {
pub username: String,
pub host_and_port: String,
}
fn username_from_env() -> anyhow::Result<String> {
@ -827,6 +830,11 @@ fn run() -> anyhow::Result<()> {
});
front_end.run_forever()?;
}
CliSubCommand::TlsCreds => {
let creds = block_on(client.get_tls_creds())?;
crate::server::codec::Pdu::GetTlsCredsResponse(creds)
.encode(std::io::stdout().lock(), 0)?;
}
}
Ok(())
}

View File

@ -20,6 +20,7 @@ use std::convert::TryInto;
use std::io::{Read, Write};
use std::net::TcpStream;
use std::path::Path;
use std::path::PathBuf;
use std::thread;
use std::time::Duration;
@ -207,6 +208,7 @@ pub fn unix_connect_with_retry(path: &Path) -> Result<UnixStream, std::io::Error
struct Reconnectable {
config: ClientDomainConfig,
stream: Option<Box<dyn ReadAndWrite>>,
tls_creds: Option<GetTlsCredsResponse>,
}
struct SshStream {
@ -290,7 +292,25 @@ impl ReadAndWrite for SshStream {
impl Reconnectable {
fn new(config: ClientDomainConfig, stream: Option<Box<dyn ReadAndWrite>>) -> Self {
Self { config, stream }
Self {
config,
stream,
tls_creds: None,
}
}
fn tls_creds_path(&self) -> anyhow::Result<PathBuf> {
let path = crate::config::pki_dir()?.join(self.config.name());
std::fs::create_dir_all(&path)?;
Ok(path)
}
fn tls_creds_ca_path(&self) -> anyhow::Result<PathBuf> {
Ok(self.tls_creds_path()?.join("ca.pem"))
}
fn tls_creds_cert_path(&self) -> anyhow::Result<PathBuf> {
Ok(self.tls_creds_path()?.join("cert.pem"))
}
// Clippy thinks we should return &ReadAndWrite here, but the caller
@ -320,11 +340,22 @@ impl Reconnectable {
fn connect(&mut self, initial: bool, ui: &mut ConnectionUI) -> anyhow::Result<()> {
match self.config.clone() {
ClientDomainConfig::Unix(unix_dom) => self.unix_connect(unix_dom, initial, ui),
ClientDomainConfig::Tls(tls) => self.tls_connect(tls, ui),
ClientDomainConfig::Tls(tls) => self.tls_connect(tls, initial, ui),
ClientDomainConfig::Ssh(ssh) => self.ssh_connect(ssh, initial, ui),
}
}
/// If debugging on wez's machine, use a path specific to that machine.
fn wezterm_bin_path() -> &'static str {
if !configuration().use_local_build_for_proxy {
"wezterm"
} else if cfg!(debug_assertions) {
"/home/wez/wez-personal/wezterm/target/debug/wezterm"
} else {
"/home/wez/wez-personal/wezterm/target/release/wezterm"
}
}
fn ssh_connect(
&mut self,
ssh_dom: SshDomain,
@ -336,13 +367,7 @@ impl Reconnectable {
let mut chan = sess.channel_session()?;
let proxy_bin = if !configuration().use_local_build_for_proxy {
"wezterm"
} else if cfg!(debug_assertions) {
"/home/wez/wez-personal/wezterm/target/debug/wezterm"
} else {
"/home/wez/wez-personal/wezterm/target/release/wezterm"
};
let proxy_bin = Self::wezterm_bin_path();
let cmd = if initial {
format!("{} cli proxy", proxy_bin)
@ -427,6 +452,7 @@ impl Reconnectable {
pub fn tls_connect(
&mut self,
tls_client: TlsDomainClient,
_initial: bool,
ui: &mut ConnectionUI,
) -> anyhow::Result<()> {
use openssl::ssl::{SslConnector, SslFiletype, SslMethod};
@ -443,16 +469,50 @@ impl Reconnectable {
)
})?;
if let Some(Ok(ssh_params)) = tls_client.ssh_parameters() {
if self.tls_creds.is_none() {
// We need to bootstrap via an ssh session
let sess =
ssh_connect_with_ui(&ssh_params.host_and_port, &ssh_params.username, ui)?;
let mut chan = sess.channel_session()?;
// The `tlscreds` command will start the server if needed and then
// obtain client credentials that we can use for tls.
let cmd = format!("{} cli tlscreds", Self::wezterm_bin_path());
ui.output_str(&format!("Running: {}\n", cmd));
chan.exec(&cmd)?;
let creds = match Pdu::decode(chan)?.pdu {
Pdu::GetTlsCredsResponse(creds) => creds,
_ => bail!("unexpected response to tlscreds"),
};
// Save the credentials to disk, as that is currently the easiest
// way to get them into openssl. Ideally we'd keep these entirely
// in memory.
std::fs::write(&self.tls_creds_ca_path()?, creds.ca_cert_pem.as_bytes())?;
std::fs::write(
&self.tls_creds_cert_path()?,
creds.client_cert_pem.as_bytes(),
)?;
self.tls_creds.replace(creds);
}
}
let mut connector = SslConnector::builder(SslMethod::tls())?;
if let Some(cert_file) = tls_client.pem_cert.as_ref() {
connector
.set_certificate_file(&cert_file, SslFiletype::PEM)
.context(format!(
"set_certificate_file to {} for TLS client",
cert_file.display()
))?;
}
let cert_file = match tls_client.pem_cert.clone() {
Some(cert) => cert,
None if self.tls_creds.is_some() => self.tls_creds_cert_path()?,
None => bail!("no pem_cert configured"),
};
connector
.set_certificate_file(&cert_file, SslFiletype::PEM)
.context(format!(
"set_certificate_file to {} for TLS client",
cert_file.display()
))?;
if let Some(chain_file) = tls_client.pem_ca.as_ref() {
connector
.set_certificate_chain_file(&chain_file)
@ -461,14 +521,19 @@ impl Reconnectable {
chain_file.display()
))?;
}
if let Some(key_file) = tls_client.pem_private_key.as_ref() {
connector
.set_private_key_file(&key_file, SslFiletype::PEM)
.context(format!(
"set_private_key_file to {} for TLS client",
key_file.display()
))?;
}
let key_file = match tls_client.pem_private_key.clone() {
Some(key) => key,
None if self.tls_creds.is_some() => self.tls_creds_cert_path()?,
None => bail!("no pem_private_key configured"),
};
connector
.set_private_key_file(&key_file, SslFiletype::PEM)
.context(format!(
"set_private_key_file to {} for TLS client",
key_file.display()
))?;
fn load_cert(name: &Path) -> anyhow::Result<X509> {
let cert_bytes = std::fs::read(name)?;
log::trace!("loaded {}", name.display());
@ -486,6 +551,12 @@ impl Reconnectable {
}
}
if self.tls_creds.is_some() {
connector
.cert_store_mut()
.add_cert(load_cert(&self.tls_creds_ca_path()?)?)?;
}
let connector = connector.build();
let connector = connector
.configure()?
@ -524,6 +595,7 @@ impl Reconnectable {
pub fn tls_connect(
&mut self,
tls_client: TlsDomainClient,
_initial: bool,
ui: &mut ConnectionUI,
) -> anyhow::Result<()> {
use crate::server::listener::IdentitySource;
@ -717,4 +789,5 @@ impl Client {
rpc!(get_tab_render_changes, GetTabRenderChanges, UnitResponse);
rpc!(get_lines, GetLines, GetLinesResponse);
rpc!(get_codec_version, GetCodecVersion, GetCodecVersionResponse);
rpc!(get_tls_creds, GetTlsCreds = (), GetTlsCredsResponse);
}

View File

@ -265,6 +265,8 @@ pdu! {
GetTabRenderChangesResponse: 25,
GetCodecVersion: 26,
GetCodecVersionResponse: 27,
GetTlsCreds: 28,
GetTlsCredsResponse: 29,
}
impl Pdu {
@ -363,6 +365,20 @@ pub struct Ping {}
#[derive(Deserialize, Serialize, PartialEq, Debug)]
pub struct Pong {}
/// Requests a client certificate to authenticate against
/// the TLS based server
#[derive(Deserialize, Serialize, PartialEq, Debug)]
pub struct GetTlsCreds {}
#[derive(Deserialize, Serialize, PartialEq, Debug)]
pub struct GetTlsCredsResponse {
/// The signing certificate
pub ca_cert_pem: String,
/// A client authentication certificate and private
/// key, PEM encoded
pub client_cert_pem: String,
}
#[derive(Deserialize, Serialize, PartialEq, Debug)]
pub struct ListTabs {}

View File

@ -2,6 +2,7 @@ use crate::mux::renderable::{RenderableDimensions, StableCursorPosition};
use crate::mux::tab::{Tab, TabId};
use crate::mux::{Mux, MuxNotification, MuxSubscriber};
use crate::server::codec::*;
use crate::server::listener::PKI;
use crate::server::pollable::*;
use anyhow::{anyhow, bail, Context, Error};
use crossbeam::channel::TryRecvError;
@ -482,6 +483,20 @@ impl<S: ReadAndWrite> ClientSession<S> {
})))
}
Pdu::GetTlsCreds(_) => {
catch(
move || {
let client_cert_pem = PKI.generate_client_cert()?;
let ca_cert_pem = PKI.ca_pem_string()?;
Ok(Pdu::GetTlsCredsResponse(GetTlsCredsResponse {
client_cert_pem,
ca_cert_pem,
}))
},
send_response,
);
}
Pdu::Invalid { .. } => send_response(Err(anyhow!("invalid PDU {:?}", decoded.pdu))),
Pdu::Pong { .. }
| Pdu::ListTabsResponse { .. }
@ -491,6 +506,7 @@ impl<S: ReadAndWrite> ClientSession<S> {
| Pdu::UnitResponse { .. }
| Pdu::GetLinesResponse { .. }
| Pdu::GetCodecVersionResponse { .. }
| Pdu::GetTlsCredsResponse { .. }
| Pdu::ErrorResponse { .. } => {
send_response(Err(anyhow!("expected a request, got {:?}", decoded.pdu)))
}

View File

@ -16,6 +16,10 @@ mod ossl;
mod pki;
mod umask;
lazy_static::lazy_static! {
static ref PKI: pki::Pki = pki::Pki::init().expect("failed to initialize PKI");
}
#[cfg(not(any(feature = "openssl", unix)))]
use not_ossl as tls_impl;
#[cfg(any(feature = "openssl", unix))]

View File

@ -101,15 +101,12 @@ impl OpenSSLNetListener {
pub fn spawn_tls_listener(tls_server: &TlsDomainServer) -> Result<(), Error> {
openssl::init();
let pki = super::pki::Pki::init()?;
pki.generate_client_cert()?;
let mut acceptor = SslAcceptor::mozilla_modern(SslMethod::tls())?;
let cert_file = tls_server
.pem_cert
.clone()
.unwrap_or_else(|| pki.server_pem());
.unwrap_or_else(|| PKI.server_pem());
acceptor
.set_certificate_file(&cert_file, SslFiletype::PEM)
.context(format!(
@ -129,7 +126,7 @@ pub fn spawn_tls_listener(tls_server: &TlsDomainServer) -> Result<(), Error> {
let key_file = tls_server
.pem_private_key
.clone()
.unwrap_or_else(|| pki.server_pem());
.unwrap_or_else(|| PKI.server_pem());
acceptor
.set_private_key_file(&key_file, SslFiletype::PEM)
.context(format!(
@ -156,7 +153,7 @@ pub fn spawn_tls_listener(tls_server: &TlsDomainServer) -> Result<(), Error> {
acceptor
.cert_store_mut()
.add_cert(load_cert(&pki.ca_pem())?)?;
.add_cert(load_cert(&PKI.ca_pem())?)?;
acceptor.set_verify(SslVerifyMode::PEER | SslVerifyMode::FAIL_IF_NO_PEER_CERT);

View File

@ -30,7 +30,7 @@ impl Pki {
.map_err(|_| anyhow!("hostname is not representable as unicode"))?,
"localhost".to_owned(),
];
let unix_name = std::env::var("USER")?;
let unix_name = crate::username_from_env()?;
// Create the CA certificate
let mut ca_params = CertificateParams::new(alt_names.clone());
@ -60,7 +60,7 @@ impl Pki {
}
pub fn generate_client_cert(&self) -> anyhow::Result<String> {
let unix_name = std::env::var("USER")?;
let unix_name = crate::username_from_env()?;
let mut params = CertificateParams::new(vec![unix_name.clone()]);
let mut dn = DistinguishedName::new();
@ -72,15 +72,15 @@ impl Pki {
let key_bits = client_cert.get_key_pair().serialize_pem();
signed_cert.push_str(&key_bits);
/* TODO: remove the bit that writes out the client pem here;
* this is just so I can test this locally */
let client_pem_path = self.pki_dir.join("client.pem");
std::fs::write(&client_pem_path, signed_cert.as_bytes())
.context(format!("saving {}", client_pem_path.display()))?;
Ok(signed_cert)
}
pub fn ca_pem_string(&self) -> anyhow::Result<String> {
self.ca_cert
.serialize_pem()
.context("Serializing ca cert pem")
}
pub fn ca_pem(&self) -> PathBuf {
self.pki_dir.join("ca.pem")
}