1
1
mirror of https://github.com/wez/wezterm.git synced 2024-11-30 14:49:26 +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::config::*;
use crate::SshParameters;
#[derive(Default, Debug, Clone, Deserialize)] #[derive(Default, Debug, Clone, Deserialize)]
pub struct TlsDomainServer { pub struct TlsDomainServer {
@ -30,6 +31,11 @@ pub struct TlsDomainClient {
/// all types of domain in the configuration file. /// all types of domain in the configuration file.
pub name: String, 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. /// identifies the host:port pair of the remote server.
pub remote_address: String, pub remote_address: String,
@ -74,3 +80,11 @@ pub struct TlsDomainClient {
#[serde(default = "default_write_timeout")] #[serde(default = "default_write_timeout")]
pub write_timeout: Duration, 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")] #[structopt(name = "proxy", about = "start rpc proxy pipe")]
Proxy, Proxy,
#[structopt(name = "tlscreds", about = "obtain tls credentials")]
TlsCreds,
} }
#[derive(Debug, StructOpt, Clone)] #[derive(Debug, StructOpt, Clone)]
@ -300,9 +303,9 @@ pub fn running_under_wsl() -> bool {
false false
} }
struct SshParameters { pub struct SshParameters {
username: String, pub username: String,
host_and_port: String, pub host_and_port: String,
} }
fn username_from_env() -> anyhow::Result<String> { fn username_from_env() -> anyhow::Result<String> {
@ -827,6 +830,11 @@ fn run() -> anyhow::Result<()> {
}); });
front_end.run_forever()?; 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(()) Ok(())
} }

View File

@ -20,6 +20,7 @@ use std::convert::TryInto;
use std::io::{Read, Write}; use std::io::{Read, Write};
use std::net::TcpStream; use std::net::TcpStream;
use std::path::Path; use std::path::Path;
use std::path::PathBuf;
use std::thread; use std::thread;
use std::time::Duration; use std::time::Duration;
@ -207,6 +208,7 @@ pub fn unix_connect_with_retry(path: &Path) -> Result<UnixStream, std::io::Error
struct Reconnectable { struct Reconnectable {
config: ClientDomainConfig, config: ClientDomainConfig,
stream: Option<Box<dyn ReadAndWrite>>, stream: Option<Box<dyn ReadAndWrite>>,
tls_creds: Option<GetTlsCredsResponse>,
} }
struct SshStream { struct SshStream {
@ -290,7 +292,25 @@ impl ReadAndWrite for SshStream {
impl Reconnectable { impl Reconnectable {
fn new(config: ClientDomainConfig, stream: Option<Box<dyn ReadAndWrite>>) -> Self { 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 // 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<()> { fn connect(&mut self, initial: bool, ui: &mut ConnectionUI) -> anyhow::Result<()> {
match self.config.clone() { match self.config.clone() {
ClientDomainConfig::Unix(unix_dom) => self.unix_connect(unix_dom, initial, ui), 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), 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( fn ssh_connect(
&mut self, &mut self,
ssh_dom: SshDomain, ssh_dom: SshDomain,
@ -336,13 +367,7 @@ impl Reconnectable {
let mut chan = sess.channel_session()?; let mut chan = sess.channel_session()?;
let proxy_bin = if !configuration().use_local_build_for_proxy { let proxy_bin = Self::wezterm_bin_path();
"wezterm"
} else if cfg!(debug_assertions) {
"/home/wez/wez-personal/wezterm/target/debug/wezterm"
} else {
"/home/wez/wez-personal/wezterm/target/release/wezterm"
};
let cmd = if initial { let cmd = if initial {
format!("{} cli proxy", proxy_bin) format!("{} cli proxy", proxy_bin)
@ -427,6 +452,7 @@ impl Reconnectable {
pub fn tls_connect( pub fn tls_connect(
&mut self, &mut self,
tls_client: TlsDomainClient, tls_client: TlsDomainClient,
_initial: bool,
ui: &mut ConnectionUI, ui: &mut ConnectionUI,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
use openssl::ssl::{SslConnector, SslFiletype, SslMethod}; 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())?; let mut connector = SslConnector::builder(SslMethod::tls())?;
if let Some(cert_file) = tls_client.pem_cert.as_ref() { let cert_file = match tls_client.pem_cert.clone() {
connector Some(cert) => cert,
.set_certificate_file(&cert_file, SslFiletype::PEM) None if self.tls_creds.is_some() => self.tls_creds_cert_path()?,
.context(format!( None => bail!("no pem_cert configured"),
"set_certificate_file to {} for TLS client", };
cert_file.display()
))?; 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() { if let Some(chain_file) = tls_client.pem_ca.as_ref() {
connector connector
.set_certificate_chain_file(&chain_file) .set_certificate_chain_file(&chain_file)
@ -461,14 +521,19 @@ impl Reconnectable {
chain_file.display() chain_file.display()
))?; ))?;
} }
if let Some(key_file) = tls_client.pem_private_key.as_ref() {
connector let key_file = match tls_client.pem_private_key.clone() {
.set_private_key_file(&key_file, SslFiletype::PEM) Some(key) => key,
.context(format!( None if self.tls_creds.is_some() => self.tls_creds_cert_path()?,
"set_private_key_file to {} for TLS client", None => bail!("no pem_private_key configured"),
key_file.display() };
))?; 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> { fn load_cert(name: &Path) -> anyhow::Result<X509> {
let cert_bytes = std::fs::read(name)?; let cert_bytes = std::fs::read(name)?;
log::trace!("loaded {}", name.display()); 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.build();
let connector = connector let connector = connector
.configure()? .configure()?
@ -524,6 +595,7 @@ impl Reconnectable {
pub fn tls_connect( pub fn tls_connect(
&mut self, &mut self,
tls_client: TlsDomainClient, tls_client: TlsDomainClient,
_initial: bool,
ui: &mut ConnectionUI, ui: &mut ConnectionUI,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
use crate::server::listener::IdentitySource; use crate::server::listener::IdentitySource;
@ -717,4 +789,5 @@ impl Client {
rpc!(get_tab_render_changes, GetTabRenderChanges, UnitResponse); rpc!(get_tab_render_changes, GetTabRenderChanges, UnitResponse);
rpc!(get_lines, GetLines, GetLinesResponse); rpc!(get_lines, GetLines, GetLinesResponse);
rpc!(get_codec_version, GetCodecVersion, GetCodecVersionResponse); rpc!(get_codec_version, GetCodecVersion, GetCodecVersionResponse);
rpc!(get_tls_creds, GetTlsCreds = (), GetTlsCredsResponse);
} }

View File

@ -265,6 +265,8 @@ pdu! {
GetTabRenderChangesResponse: 25, GetTabRenderChangesResponse: 25,
GetCodecVersion: 26, GetCodecVersion: 26,
GetCodecVersionResponse: 27, GetCodecVersionResponse: 27,
GetTlsCreds: 28,
GetTlsCredsResponse: 29,
} }
impl Pdu { impl Pdu {
@ -363,6 +365,20 @@ pub struct Ping {}
#[derive(Deserialize, Serialize, PartialEq, Debug)] #[derive(Deserialize, Serialize, PartialEq, Debug)]
pub struct Pong {} 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)] #[derive(Deserialize, Serialize, PartialEq, Debug)]
pub struct ListTabs {} pub struct ListTabs {}

View File

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

View File

@ -16,6 +16,10 @@ mod ossl;
mod pki; mod pki;
mod umask; 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)))] #[cfg(not(any(feature = "openssl", unix)))]
use not_ossl as tls_impl; use not_ossl as tls_impl;
#[cfg(any(feature = "openssl", unix))] #[cfg(any(feature = "openssl", unix))]

View File

@ -101,15 +101,12 @@ impl OpenSSLNetListener {
pub fn spawn_tls_listener(tls_server: &TlsDomainServer) -> Result<(), Error> { pub fn spawn_tls_listener(tls_server: &TlsDomainServer) -> Result<(), Error> {
openssl::init(); openssl::init();
let pki = super::pki::Pki::init()?;
pki.generate_client_cert()?;
let mut acceptor = SslAcceptor::mozilla_modern(SslMethod::tls())?; let mut acceptor = SslAcceptor::mozilla_modern(SslMethod::tls())?;
let cert_file = tls_server let cert_file = tls_server
.pem_cert .pem_cert
.clone() .clone()
.unwrap_or_else(|| pki.server_pem()); .unwrap_or_else(|| PKI.server_pem());
acceptor acceptor
.set_certificate_file(&cert_file, SslFiletype::PEM) .set_certificate_file(&cert_file, SslFiletype::PEM)
.context(format!( .context(format!(
@ -129,7 +126,7 @@ pub fn spawn_tls_listener(tls_server: &TlsDomainServer) -> Result<(), Error> {
let key_file = tls_server let key_file = tls_server
.pem_private_key .pem_private_key
.clone() .clone()
.unwrap_or_else(|| pki.server_pem()); .unwrap_or_else(|| PKI.server_pem());
acceptor acceptor
.set_private_key_file(&key_file, SslFiletype::PEM) .set_private_key_file(&key_file, SslFiletype::PEM)
.context(format!( .context(format!(
@ -156,7 +153,7 @@ pub fn spawn_tls_listener(tls_server: &TlsDomainServer) -> Result<(), Error> {
acceptor acceptor
.cert_store_mut() .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); 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"))?, .map_err(|_| anyhow!("hostname is not representable as unicode"))?,
"localhost".to_owned(), "localhost".to_owned(),
]; ];
let unix_name = std::env::var("USER")?; let unix_name = crate::username_from_env()?;
// Create the CA certificate // Create the CA certificate
let mut ca_params = CertificateParams::new(alt_names.clone()); let mut ca_params = CertificateParams::new(alt_names.clone());
@ -60,7 +60,7 @@ impl Pki {
} }
pub fn generate_client_cert(&self) -> anyhow::Result<String> { 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 params = CertificateParams::new(vec![unix_name.clone()]);
let mut dn = DistinguishedName::new(); let mut dn = DistinguishedName::new();
@ -72,15 +72,15 @@ impl Pki {
let key_bits = client_cert.get_key_pair().serialize_pem(); let key_bits = client_cert.get_key_pair().serialize_pem();
signed_cert.push_str(&key_bits); 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) 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 { pub fn ca_pem(&self) -> PathBuf {
self.pki_dir.join("ca.pem") self.pki_dir.join("ca.pem")
} }