Skip to content

Commit

Permalink
Added support for SASL EXTERNAL
Browse files Browse the repository at this point in the history
A user can now generate a x509 certificate, register it with a server,
and provide the PEM file to tiny for use over TLS.

Closes osa1#196
  • Loading branch information
trevarj committed Nov 12, 2021
1 parent f9da8d2 commit 8d5d95d
Show file tree
Hide file tree
Showing 9 changed files with 213 additions and 54 deletions.
33 changes: 21 additions & 12 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions crates/libtiny_client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ libtiny_common = { path = "../libtiny_common" }
libtiny_wire = { path = "../libtiny_wire" }
log = "0.4"
native-tls = { version = "0.2", optional = true }
rustls-native-certs = { version = "0.5", optional = true }
rustls-native-certs = { version = "0.6", optional = true }
tokio = { version = "1.6.1", default-features = false, features = ["net", "rt", "io-util", "macros"] }
tokio-native-tls = { version = "0.3", optional = true }
tokio-rustls = { version = "0.22", optional = true }
tokio-rustls = { version = "0.23", optional = true }
tokio-stream = { version = "0.1.6" }
28 changes: 23 additions & 5 deletions crates/libtiny_client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,19 @@ pub struct ServerInfo {
pub sasl_auth: Option<SASLAuth>,
}

/// SASL authentication credentials
/// SASL authentication mechanisms
#[derive(Debug, Clone)]
pub struct SASLAuth {
pub username: String,
pub password: String,
pub enum SASLAuth {
Plain { username: String, password: String },
External(SASLExternal),
}

#[derive(Debug, Clone)]
pub struct SASLExternal {
/// DER-encoded X.509 certificate used for TLS Client Authorization
pub cert: Vec<u8>,
/// DER-encoded Private Key that was used to generate `cert`
pub key: Vec<u8>,
}

/// IRC client events. Returned by `Client` to the users via a channel.
Expand Down Expand Up @@ -388,10 +396,19 @@ async fn main_loop(
// Establish TCP connection to the server
//

let sasl_ext = server_info.sasl_auth.as_ref().and_then(|s| {
if let SASLAuth::External(s) = s {
Some(s)
} else {
None
}
});

let stream = match try_connect(
addrs,
&serv_name,
server_info.tls,
sasl_ext,
&mut rcv_cmd,
&mut snd_ev,
)
Expand Down Expand Up @@ -624,14 +641,15 @@ async fn try_connect<S: StreamExt<Item = Cmd> + Unpin>(
addrs: Vec<SocketAddr>,
serv_name: &str,
use_tls: bool,
sasl: Option<&SASLExternal>,
rcv_cmd: &mut S,
snd_ev: &mut mpsc::Sender<Event>,
) -> TaskResult<Option<Stream>> {
let connect_task = async move {
for addr in addrs {
snd_ev.send(Event::Connecting(addr)).await.unwrap();
let mb_stream = if use_tls {
Stream::new_tls(addr, serv_name).await
Stream::new_tls(addr, serv_name, sasl).await
} else {
Stream::new_tcp(addr).await
};
Expand Down
31 changes: 21 additions & 10 deletions crates/libtiny_client/src/state.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#![allow(clippy::zero_prefixed_literal)]

use crate::utils;
use crate::{utils, SASLAuth};
use crate::{Cmd, Event, ServerInfo};
use libtiny_common::{ChanName, ChanNameRef};
use libtiny_wire as wire;
Expand Down Expand Up @@ -623,7 +623,15 @@ impl StateInner {
match subcommand.as_ref() {
"ACK" => {
if params.iter().any(|cap| cap.as_str() == "sasl") {
snd_irc_msg.try_send(wire::authenticate("PLAIN")).unwrap();
if let Some(sasl) = &self.server_info.sasl_auth {
let msg = match sasl {
SASLAuth::Plain { .. } => "PLAIN",
SASLAuth::External(_) => "+",
};
snd_irc_msg.try_send(wire::authenticate(msg)).unwrap();
} else {
warn!("SASL AUTH not set but got SASL ACK");
}
}
}
"NAK" => {
Expand All @@ -643,15 +651,18 @@ impl StateInner {
AUTHENTICATE { ref param } => {
if param.as_str() == "+" {
// Empty AUTHENTICATE response; server accepted the specified SASL mechanism
// (PLAIN)
if let Some(ref auth) = self.server_info.sasl_auth {
let msg = format!(
"{}\x00{}\x00{}",
auth.username, auth.username, auth.password
);
snd_irc_msg
.try_send(wire::authenticate(&base64::encode(&msg)))
.unwrap();
match auth {
SASLAuth::Plain { username, password } => {
let msg = format!("{}\x00{}\x00{}", username, username, password);
snd_irc_msg
.try_send(wire::authenticate(&base64::encode(&msg)))
.unwrap();
}
SASLAuth::External { .. } => {
snd_irc_msg.try_send(wire::authenticate("+")).unwrap()
}
}
}
}
}
Expand Down
79 changes: 65 additions & 14 deletions crates/libtiny_client/src/stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,53 @@ use tokio_native_tls::TlsStream;
#[cfg(feature = "tls-rustls")]
use tokio_rustls::client::TlsStream;

use crate::SASLExternal;

#[cfg(feature = "tls-native")]
lazy_static! {
static ref TLS_CONNECTOR: tokio_native_tls::TlsConnector =
tokio_native_tls::TlsConnector::from(native_tls::TlsConnector::builder().build().unwrap());
static ref TLS_CONNECTOR: tokio_native_tls::TlsConnector = tls_connector(None);
}

#[cfg(feature = "tls-native")]
fn tls_connector(sasl: Option<&SASLExternal>) -> tokio_native_tls::TlsConnector {
let mut builder = native_tls::TlsConnector::builder();
if let Some(SASLExternal { cert, key }) = sasl {
todo!("Waiting for https://github.com/sfackler/rust-native-tls/pull/209")
}
tokio_native_tls::TlsConnector::from(builder.build().unwrap())
}

#[cfg(feature = "tls-rustls")]
lazy_static! {
static ref TLS_CONNECTOR: tokio_rustls::TlsConnector = {
let mut config = tokio_rustls::rustls::ClientConfig::default();
config.root_store = rustls_native_certs::load_native_certs().unwrap();
tokio_rustls::TlsConnector::from(std::sync::Arc::new(config))
static ref TLS_CONNECTOR: tokio_rustls::TlsConnector = tls_connector(None);
}

#[cfg(feature = "tls-rustls")]
fn tls_connector(sasl: Option<&SASLExternal>) -> tokio_rustls::TlsConnector {
use tokio_rustls::rustls::{Certificate, ClientConfig, PrivateKey, RootCertStore};

let mut roots = RootCertStore::empty();
for cert in rustls_native_certs::load_native_certs().expect("could not load platform certs") {
roots.add(&Certificate(cert.0)).unwrap();
}

let builder = ClientConfig::builder()
.with_safe_defaults()
.with_root_certificates(roots);

let config = if let Some(SASLExternal { cert, key }) = sasl {
builder
.with_single_cert(
vec![Certificate(cert.to_owned())],
PrivateKey(key.to_owned()),
)
.expect("Client auth cert")
} else {
builder.with_no_client_auth()
};
tokio_rustls::TlsConnector::from(std::sync::Arc::new(config))
}

#[derive(Debug)]
// We box the fields to reduce type size. Without boxing the type size is 64 with native-tls and
// 1288 with native-tls. With boxing it's 16 in both. More importantly, there's a large size
// difference between the variants when using rustls, see #189.
Expand All @@ -41,7 +72,7 @@ pub(crate) enum Stream {
#[cfg(feature = "tls-native")]
pub(crate) type TlsError = native_tls::Error;
#[cfg(feature = "tls-rustls")]
pub(crate) type TlsError = tokio_rustls::rustls::TLSError;
pub(crate) type TlsError = tokio_rustls::rustls::Error;

pub(crate) enum StreamError {
TlsError(TlsError),
Expand All @@ -66,18 +97,38 @@ impl Stream {
}

#[cfg(feature = "tls-native")]
pub(crate) async fn new_tls(addr: SocketAddr, host_name: &str) -> Result<Stream, StreamError> {
pub(crate) async fn new_tls(
addr: SocketAddr,
host_name: &str,
sasl: Option<&SASLExternal>,
) -> Result<Stream, StreamError> {
let tcp_stream = TcpStream::connect(addr).await?;
let tls_stream = TLS_CONNECTOR.connect(host_name, tcp_stream).await?;
// If SASL EXTERNAL is enabled create a new TLS connector with client auth cert
let tls_stream = if sasl.is_some() {
tls_connector(sasl).connect(host_name, tcp_stream).await?
} else {
TLS_CONNECTOR.connect(host_name, tcp_stream).await?
};
Ok(Stream::TlsStream(tls_stream.into()))
}

#[cfg(feature = "tls-rustls")]
pub(crate) async fn new_tls(addr: SocketAddr, host_name: &str) -> Result<Stream, StreamError> {
pub(crate) async fn new_tls(
addr: SocketAddr,
host_name: &str,
sasl: Option<&SASLExternal>,
) -> Result<Stream, StreamError> {
use std::convert::TryFrom;
use tokio_rustls::rustls::ServerName;

let tcp_stream = TcpStream::connect(addr).await?;
let name = tokio_rustls::webpki::DNSNameRef::try_from_ascii_str(host_name)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
let tls_stream = TLS_CONNECTOR.connect(name, tcp_stream).await?;
let name = ServerName::try_from(host_name).unwrap();
// If SASL EXTERNAL is enabled create a new TLS connector with client auth cert
let tls_stream = if sasl.is_some() {
tls_connector(sasl).connect(name, tcp_stream).await?
} else {
TLS_CONNECTOR.connect(name, tcp_stream).await?
};
Ok(Stream::TlsStream(tls_stream.into()))
}
}
Expand Down
1 change: 1 addition & 0 deletions crates/tiny/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ libtiny_logger = { path = "../libtiny_logger" }
libtiny_tui = { path = "../libtiny_tui", default-features = false }
libtiny_wire = { path = "../libtiny_wire" }
log = "0.4"
rustls-pemfile = "0.2"
serde = { version = "1.0", features = ["derive"] }
serde_yaml = "0.8"
time = "0.1"
Expand Down
10 changes: 8 additions & 2 deletions crates/tiny/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,20 @@ servers:
# Three authentication methods: pass, sasl, and nickserv_ident
# These are optional and you probably only need one of these, delete
# others.
# For SASL EXTERNAL certificate and fingerprint generation, see server documentation.
# You will need to register the cert's fingerprint with NickServ
# ex. https://www.oftc.net/NickServ/CertFP/

# Server or nick password
# pass: 'hunter2'

# SASL authentication
# sasl:
# username: 'tiny_user'
# password: 'hunter2'
# plain:
# username: 'tiny_user'
# password: 'hunter2'
# external:
# pem: "/home/.config/tiny/oftc.pem"

# Identify nick by sending a message to NickServ:
# (useful when `pass` or `sasl` fields above are not used)
Expand Down
16 changes: 13 additions & 3 deletions crates/tiny/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,19 @@ use std::io::{Read, Write};
use std::path::{Path, PathBuf};

#[derive(Clone, Deserialize, Debug, PartialEq, Eq)]
pub(crate) struct SASLAuth {
pub(crate) username: String,
pub(crate) password: String,
#[serde(rename_all = "snake_case")]
pub(crate) enum SASLAuth {
Plain {
/// Registered username
username: String,
/// Password
password: String,
},
External {
/// Path to PEM file with private key and certificate (PKCS12 format).
/// A fingerprint of the certificate should be registered with NickServ
pem: PathBuf,
},
}

#[derive(Clone, Deserialize)]
Expand Down
Loading

0 comments on commit 8d5d95d

Please sign in to comment.