Implement POSH and host-meta secure delegation for incoming and outgoing

This commit is contained in:
Travis Burtrum 2022-02-27 02:04:06 -05:00
parent e792da3312
commit 01714522ec
10 changed files with 414 additions and 140 deletions

3
Cargo.lock generated
View File

@ -1607,6 +1607,7 @@ name = "xmpp-proxy"
version = "1.0.0"
dependencies = [
"anyhow",
"data-encoding",
"die",
"env_logger",
"futures",
@ -1616,11 +1617,13 @@ dependencies = [
"quinn",
"rand",
"reqwest",
"ring",
"rustls",
"rustls-native-certs",
"rustls-pemfile",
"serde",
"serde_derive",
"serde_json",
"tokio",
"tokio-rustls",
"tokio-tungstenite",

View File

@ -27,6 +27,8 @@ futures = "0.3"
die = "0.2"
anyhow = "1.0"
tokio = { version = "1.9", features = ["net", "rt", "rt-multi-thread", "macros", "io-util"] }
ring = "0.16"
data-encoding = "2.3"
# logging deps
log = "0.4"
@ -67,3 +69,6 @@ logging = ["rand", "env_logger"]
[package.metadata.cargo-all-features]
skip_optional_dependencies = true
[dev-dependencies]
serde_json = "1.0"

24
contrib/posh.sh Executable file
View File

@ -0,0 +1,24 @@
#!/bin/sh
# these are just examples for how to grab and hash certificates for POSH
# adapted from https://curl.se/libcurl/c/CURLOPT_PINNEDPUBLICKEY.html
# this is for any direct TLS port like xmpps or https
openssl s_client -servername posh.badxmpp.eu -connect posh.badxmpp.eu:443 < /dev/null | sed -n "/-----BEGIN/,/-----END/p" > posh.badxmpp.eu.pem
openssl asn1parse -noout -inform pem -in posh.badxmpp.eu.pem -out posh.badxmpp.eu.der
openssl dgst -sha256 -binary posh.badxmpp.eu.der | openssl base64 | tr -d '\n' > posh.badxmpp.eu.der.sha256
openssl dgst -sha512 -binary posh.badxmpp.eu.der | openssl base64 | tr -d '\n' > posh.badxmpp.eu.der.sha512
openssl base64 < posh.badxmpp.eu.der | tr -d '\n' > posh.badxmpp.eu.der.base64
# this is for any starttls xmpp port
openssl s_client -starttls xmpp -name posh.badxmpp.eu -servername posh.badxmpp.eu -connect snikket2.prosody.im:5222 < /dev/null | sed -n "/-----BEGIN/,/-----END/p" > posh.badxmpp.eu.5222.pem
openssl asn1parse -noout -inform pem -in posh.badxmpp.eu.5222.pem -out posh.badxmpp.eu.5222.der
openssl dgst -sha256 -binary posh.badxmpp.eu.5222.der | openssl base64 | tr -d '\n' > posh.badxmpp.eu.5222.der.sha256
openssl dgst -sha512 -binary posh.badxmpp.eu.5222.der | openssl base64 | tr -d '\n' > posh.badxmpp.eu.5222.der.sha512
openssl base64 < posh.badxmpp.eu.5222.der | tr -d '\n' > posh.badxmpp.eu.5222.der.base64
wget https://posh.badxmpp.eu/.well-known/posh/xmpp-server.json https://posh.badxmpp.eu/.well-known/posh/xmpp-client.json
grep . *.sha*

View File

@ -8,7 +8,6 @@ use std::net::SocketAddr;
pub use log::{debug, error, info, log_enabled, trace};
use rustls::{Certificate, ServerConnection};
use tokio_rustls::webpki::DnsNameRef;
pub fn to_str(buf: &[u8]) -> std::borrow::Cow<'_, str> {
String::from_utf8_lossy(buf)
@ -138,24 +137,10 @@ pub enum ServerCerts {
}
impl ServerCerts {
pub fn valid(&self, dns_name: DnsNameRef) -> bool {
use std::convert::TryFrom;
use tokio_rustls::webpki;
self.first_peer_cert()
.and_then(|c| {
if let Ok(cert) = webpki::EndEntityCert::try_from(c.0.as_ref()) {
cert.verify_is_valid_for_dns_name(dns_name).map(|_| true).ok()
} else {
Some(false)
}
})
.unwrap_or(false)
}
pub fn first_peer_cert(&self) -> Option<Certificate> {
pub fn peer_certificates(&self) -> Option<Vec<Certificate>> {
match self {
ServerCerts::Tls(c) => c.peer_certificates().map(|c| c[0].clone()),
ServerCerts::Quic(c) => c.peer_identity().and_then(|v| v.downcast::<Vec<Certificate>>().ok()).map(|v| v[0].clone()),
ServerCerts::Tls(c) => c.peer_certificates().map(|c| c.to_vec()),
ServerCerts::Quic(c) => c.peer_identity().and_then(|v| v.downcast::<Vec<Certificate>>().ok()).map(|v| v.to_vec()),
}
}

View File

@ -8,6 +8,7 @@ use std::iter::Iterator;
use std::net::SocketAddr;
use std::path::Path;
use std::sync::Arc;
use std::time::SystemTime;
use die::Die;
@ -27,7 +28,7 @@ use tokio_rustls::{
TlsAcceptor, TlsConnector,
};
use anyhow::{bail, Result};
use anyhow::{anyhow, bail, Result};
mod slicesubsequence;
use slicesubsequence::*;
@ -145,44 +146,24 @@ impl Config {
#[cfg(feature = "outgoing")]
fn get_outgoing_cfg(&self) -> OutgoingConfig {
let c2s_config = ClientConfig::builder().with_safe_defaults().with_root_certificates(root_cert_store()).with_no_client_auth();
let s2s_config = match self.certs_key().and_then(|(tls_certs, tls_key)| {
Ok(ClientConfig::builder()
.with_safe_defaults()
.with_root_certificates(root_cert_store())
.with_single_cert(tls_certs, tls_key)?)
}) {
Ok(s) => s,
let certs_key = match self.certs_key() {
Ok((tls_certs, tls_key)) => {
ClientConfig::builder()
.with_safe_defaults()
.with_root_certificates(root_cert_store())
.with_single_cert(tls_certs.clone(), tls_key.clone())
.die("invalid key for certs");
Some((tls_certs, tls_key))
}
Err(e) => {
debug!("invalid key/cert for s2s client auth: {}", e);
c2s_config.clone()
None
}
};
// uncomment to disable cert auth/sasl external
//let s2s_config = c2s_config.clone();
let mut c2s_config_alpn = c2s_config.clone();
let mut s2s_config_alpn = s2s_config.clone();
c2s_config_alpn.alpn_protocols.push(ALPN_XMPP_CLIENT.to_vec());
s2s_config_alpn.alpn_protocols.push(ALPN_XMPP_SERVER.to_vec());
let c2s_config_alpn = Arc::new(c2s_config_alpn);
let s2s_config_alpn = Arc::new(s2s_config_alpn);
let c2s_connector_alpn: TlsConnector = c2s_config_alpn.clone().into();
let s2s_connector_alpn: TlsConnector = s2s_config_alpn.clone().into();
let c2s_connector: TlsConnector = Arc::new(c2s_config).into();
let s2s_connector: TlsConnector = Arc::new(s2s_config).into();
OutgoingConfig {
max_stanza_size_bytes: self.max_stanza_size_bytes,
c2s_config_alpn,
s2s_config_alpn,
c2s_connector_alpn,
s2s_connector_alpn,
c2s_connector,
s2s_connector,
certs_key,
}
}
@ -209,7 +190,7 @@ impl Config {
let mut config = ServerConfig::builder()
.with_safe_defaults()
.with_client_cert_verifier(Arc::new(AllowAnyAnonymousOrAuthenticatedServer))
.with_client_cert_verifier(Arc::new(AllowAnonymousOrAnyCert))
.with_single_cert(tls_certs, tls_key)?;
// todo: will connecting without alpn work then?
config.alpn_protocols.push(ALPN_XMPP_CLIENT.to_vec());
@ -228,43 +209,53 @@ impl Config {
#[cfg(feature = "outgoing")]
pub struct OutgoingConfig {
max_stanza_size_bytes: usize,
c2s_config_alpn: Arc<ClientConfig>,
s2s_config_alpn: Arc<ClientConfig>,
c2s_connector_alpn: TlsConnector,
s2s_connector_alpn: TlsConnector,
c2s_connector: TlsConnector,
s2s_connector: TlsConnector,
certs_key: Option<(Vec<Certificate>, PrivateKey)>,
}
#[cfg(feature = "outgoing")]
impl OutgoingConfig {
pub fn client_cfg_alpn(&self, is_c2s: bool) -> Arc<ClientConfig> {
if is_c2s {
self.c2s_config_alpn.clone()
} else {
self.s2s_config_alpn.clone()
}
}
pub fn with_custom_certificate_verifier(&self, is_c2s: bool, cert_verifier: XmppServerCertVerifier) -> OutgoingVerifierConfig {
let config = match (is_c2s, self.certs_key.as_ref()) {
(false, Some((tls_certs, tls_key))) => ClientConfig::builder()
.with_safe_defaults()
.with_custom_certificate_verifier(Arc::new(cert_verifier))
.with_single_cert(tls_certs.to_vec(), tls_key.to_owned())
.expect("cannot panic because key was checked for validity in OutgoingConfig constructor"),
_ => ClientConfig::builder()
.with_safe_defaults()
.with_custom_certificate_verifier(Arc::new(cert_verifier))
.with_no_client_auth(),
};
pub fn connector_alpn(&self, is_c2s: bool) -> TlsConnector {
if is_c2s {
self.c2s_connector_alpn.clone()
} else {
self.s2s_connector_alpn.clone()
}
}
let mut config_alpn = config.clone();
config_alpn.alpn_protocols.push(if is_c2s { ALPN_XMPP_CLIENT } else { ALPN_XMPP_SERVER }.to_vec());
pub fn connector(&self, is_c2s: bool) -> TlsConnector {
if is_c2s {
self.c2s_connector.clone()
} else {
self.s2s_connector.clone()
let config_alpn = Arc::new(config_alpn);
let connector_alpn: TlsConnector = config_alpn.clone().into();
let connector: TlsConnector = Arc::new(config).into();
OutgoingVerifierConfig {
max_stanza_size_bytes: self.max_stanza_size_bytes,
config_alpn,
connector_alpn,
connector,
}
}
}
#[derive(Clone)]
#[cfg(feature = "outgoing")]
pub struct OutgoingVerifierConfig {
pub max_stanza_size_bytes: usize,
pub config_alpn: Arc<ClientConfig>,
pub connector_alpn: TlsConnector,
pub connector: TlsConnector,
}
async fn shuffle_rd_wr(in_rd: StanzaRead, in_wr: StanzaWrite, config: CloneableConfig, server_certs: ServerCerts, local_addr: SocketAddr, client_addr: &mut Context<'_>) -> Result<()> {
let filter = StanzaFilter::new(config.max_stanza_size_bytes);
shuffle_rd_wr_filter(in_rd, in_wr, config, server_certs, local_addr, client_addr, filter).await
@ -293,14 +284,14 @@ async fn shuffle_rd_wr_filter(
if !is_c2s {
// for s2s we need this
let dns_from = stream_open
let domain = stream_open
.extract_between(b" from='", b"'")
.or_else(|_| stream_open.extract_between(b" from=\"", b"\""))
.and_then(|b| Ok(DnsNameRef::try_from_ascii(b)?))?;
if !server_certs.valid(dns_from) {
// todo: send stream error saying cert is invalid
bail!("server certificate invalid for {:?}", dns_from);
}
.and_then(|b| Ok(std::str::from_utf8(b)?))?;
let (_, cert_verifier) = get_xmpp_connections(domain, is_c2s).await?;
let certs = server_certs.peer_certificates().ok_or_else(|| anyhow!("no client cert auth for s2s incoming from {}", domain))?;
// todo: send stream error saying cert is invalid
cert_verifier.verify_cert(&certs[0], &certs[1..], SystemTime::now())?;
}
drop(server_certs);

View File

@ -6,9 +6,9 @@ use std::{net::SocketAddr, sync::Arc};
use anyhow::Result;
#[cfg(feature = "outgoing")]
pub async fn quic_connect(target: SocketAddr, server_name: &str, is_c2s: bool, config: OutgoingConfig) -> Result<(StanzaWrite, StanzaRead)> {
pub async fn quic_connect(target: SocketAddr, server_name: &str, config: OutgoingVerifierConfig) -> Result<(StanzaWrite, StanzaRead)> {
let bind_addr = "0.0.0.0:0".parse().unwrap();
let client_cfg = config.client_cfg_alpn(is_c2s);
let client_cfg = config.config_alpn;
let mut endpoint = quinn::Endpoint::client(bind_addr)?;
endpoint.set_default_client_config(quinn::ClientConfig::new(client_cfg));

View File

@ -3,11 +3,15 @@
use std::convert::TryFrom;
use std::net::SocketAddr;
use data_encoding::BASE64;
use ring::digest::{Algorithm, Context as DigestContext, SHA256, SHA512};
use trust_dns_resolver::error::ResolveError;
use trust_dns_resolver::lookup::{SrvLookup, TxtLookup};
use trust_dns_resolver::{IntoName, TokioAsyncResolver};
use anyhow::{bail, Result};
use tokio_rustls::webpki::DnsName;
#[cfg(feature = "websocket")]
use tokio_tungstenite::tungstenite::http::Uri;
@ -47,11 +51,10 @@ impl XmppConnection {
pub async fn connect(
&self,
domain: &str,
is_c2s: bool,
stream_open: &[u8],
in_filter: &mut crate::StanzaFilter,
client_addr: &mut Context<'_>,
config: OutgoingConfig,
config: OutgoingVerifierConfig,
) -> Result<(StanzaWrite, StanzaRead, SocketAddr, &'static str)> {
debug!("{} attempting connection to SRV: {:?}", client_addr.log_from(), self);
// todo: need to set options to Ipv4AndIpv6
@ -61,23 +64,23 @@ impl XmppConnection {
debug!("{} trying ip {}", client_addr.log_from(), to_addr);
// todo: for DNSSEC we need to optionally allow target in addition to domain, but what for SNI
match self.conn_type {
XmppConnectionType::StartTLS => match crate::starttls_connect(to_addr, domain, is_c2s, stream_open, in_filter, config.clone()).await {
XmppConnectionType::StartTLS => match crate::starttls_connect(to_addr, domain, stream_open, in_filter, config.clone()).await {
Ok((wr, rd)) => return Ok((wr, rd, to_addr, "starttls-out")),
Err(e) => error!("starttls connection failed to IP {} from SRV {}, error: {}", to_addr, self.target, e),
},
XmppConnectionType::DirectTLS => match crate::tls_connect(to_addr, domain, is_c2s, config.clone()).await {
XmppConnectionType::DirectTLS => match crate::tls_connect(to_addr, domain, config.clone()).await {
Ok((wr, rd)) => return Ok((wr, rd, to_addr, "directtls-out")),
Err(e) => error!("direct tls connection failed to IP {} from SRV {}, error: {}", to_addr, self.target, e),
},
#[cfg(feature = "quic")]
XmppConnectionType::QUIC => match crate::quic_connect(to_addr, domain, is_c2s, config.clone()).await {
XmppConnectionType::QUIC => match crate::quic_connect(to_addr, domain, config.clone()).await {
Ok((wr, rd)) => return Ok((wr, rd, to_addr, "quic-out")),
Err(e) => error!("quic connection failed to IP {} from SRV {}, error: {}", to_addr, self.target, e),
},
#[cfg(feature = "websocket")]
// todo: when websocket is found via DNS, we need to validate cert against domain, *not* target, this is a security problem with XEP-0156, we are doing it the secure but likely unexpected way here for now
XmppConnectionType::WebSocket(ref url, ref origin, ref secure) => {
match crate::websocket_connect(to_addr, if *secure { &self.target } else { domain }, url, origin, is_c2s, config.clone()).await {
match crate::websocket_connect(to_addr, if *secure { &self.target } else { domain }, url, origin, config.clone()).await {
Ok((wr, rd)) => return Ok((wr, rd, to_addr, "websocket-out")),
Err(e) => error!("websocket connection failed to IP {} from TXT {}, error: {}", to_addr, url, e),
}
@ -163,7 +166,8 @@ fn collect_txts(ret: &mut Vec<XmppConnection>, secure_urls: Vec<String>, txt_rec
}
}
pub async fn get_xmpp_connections(domain: &str, is_c2s: bool) -> Result<Vec<XmppConnection>> {
pub async fn get_xmpp_connections(domain: &str, is_c2s: bool) -> Result<(Vec<XmppConnection>, XmppServerCertVerifier)> {
let mut valid_tls_cert_server_names: Vec<DnsName> = vec![DnsNameRef::try_from_ascii_str(domain)?.to_owned()];
let (starttls, direct_tls, quic, websocket_txt, websocket_rel) = if is_c2s {
("_xmpp-client._tcp", "_xmpps-client._tcp", "_xmppq-client._udp", "_xmppconnect", "urn:xmpp:alt-connections:websocket")
} else {
@ -192,8 +196,8 @@ pub async fn get_xmpp_connections(domain: &str, is_c2s: bool) -> Result<Vec<Xmpp
quic,
//#[cfg(feature = "websocket")]
websocket_txt,
websocket_host_meta,
websocket_host_meta_json,
websocket_host,
posh,
) = tokio::join!(
RESOLVER.srv_lookup(starttls),
RESOLVER.srv_lookup(direct_tls),
@ -202,7 +206,7 @@ pub async fn get_xmpp_connections(domain: &str, is_c2s: bool) -> Result<Vec<Xmpp
//#[cfg(feature = "websocket")]
RESOLVER.txt_lookup(websocket_txt),
collect_host_meta(domain, websocket_rel),
collect_host_meta_json(domain, websocket_rel),
collect_posh(domain),
);
let mut ret = Vec::new();
@ -212,17 +216,7 @@ pub async fn get_xmpp_connections(domain: &str, is_c2s: bool) -> Result<Vec<Xmpp
collect_srvs(&mut ret, quic, XmppConnectionType::QUIC);
#[cfg(feature = "websocket")]
{
let mut urls = Vec::new();
match websocket_host_meta {
Ok(mut u) => urls.append(&mut u),
Err(e) => debug!("websocket_host_meta error for domain {}: {}", domain, e),
}
match websocket_host_meta_json {
Ok(mut u) => urls.append(&mut u),
Err(e) => debug!("websocket_host_meta_json error for domain {}: {}", domain, e),
}
urls.sort();
urls.dedup();
let urls = websocket_host.unwrap_or_default();
for url in &urls {
if let Some(url) = wss_to_srv(url, true) {
ret.push(url);
@ -233,6 +227,25 @@ pub async fn get_xmpp_connections(domain: &str, is_c2s: bool) -> Result<Vec<Xmpp
ret.sort_by(|a, b| a.priority.cmp(&b.priority));
// todo: do something with weight
#[allow(clippy::single_match)]
for srv in &ret {
match srv.conn_type {
#[cfg(feature = "websocket")]
XmppConnectionType::WebSocket(_, _, ref secure) => {
if *secure {
if let Ok(target) = DnsNameRef::try_from_ascii_str(srv.target.as_str()) {
let target = target.to_owned();
if !valid_tls_cert_server_names.contains(&target) {
valid_tls_cert_server_names.push(target);
}
}
}
}
_ => {}
}
}
let cert_verifier = XmppServerCertVerifier::new(valid_tls_cert_server_names, posh.ok());
if ret.is_empty() {
// default starttls ports
ret.push(XmppConnection {
@ -274,7 +287,7 @@ pub async fn get_xmpp_connections(domain: &str, is_c2s: bool) -> Result<Vec<Xmpp
debug!("{} records for {}: {:?}", ret.len(), domain, ret);
Ok(ret)
Ok((ret, cert_verifier))
}
pub async fn srv_connect(
@ -285,8 +298,10 @@ pub async fn srv_connect(
client_addr: &mut Context<'_>,
config: OutgoingConfig,
) -> Result<(StanzaWrite, StanzaRead, Vec<u8>)> {
for srv in get_xmpp_connections(domain, is_c2s).await? {
let connect = srv.connect(domain, is_c2s, stream_open, in_filter, client_addr, config.clone()).await;
let (srvs, cert_verifier) = get_xmpp_connections(domain, is_c2s).await?;
let config = config.with_custom_certificate_verifier(is_c2s, cert_verifier);
for srv in srvs {
let connect = srv.connect(domain, stream_open, in_filter, client_addr, config.clone()).await;
if connect.is_err() {
continue;
}
@ -313,13 +328,20 @@ pub async fn srv_connect(
}
#[cfg(not(feature = "websocket"))]
async fn collect_host_meta_json(domain: &str, rel: &str) -> Result<Vec<String>> {
async fn collect_host_meta(domain: &str, rel: &str) -> Result<Vec<String>> {
bail!("websocket disabled")
}
#[cfg(not(feature = "websocket"))]
#[cfg(feature = "websocket")]
async fn collect_host_meta(domain: &str, rel: &str) -> Result<Vec<String>> {
bail!("websocket disabled")
match tokio::join!(collect_host_meta_xml(domain, rel), collect_host_meta_json(domain, rel)) {
(Ok(mut xml), Ok(json)) => {
combine_uniq(&mut xml, json);
Ok(xml)
}
(_, Ok(json)) => Ok(json),
(xml, _) => xml,
}
}
#[cfg(feature = "websocket")]
@ -335,7 +357,7 @@ async fn collect_host_meta_json(domain: &str, rel: &str) -> Result<Vec<String>>
}
let url = format!("https://{}/.well-known/host-meta.json", domain);
let resp = reqwest::get(&url).await?;
let resp = https_get(&url).await?;
if resp.status().is_success() {
let resp = resp.json::<HostMeta>().await?;
// we will only support wss:// (TLS) not ws:// (plain text)
@ -346,7 +368,7 @@ async fn collect_host_meta_json(domain: &str, rel: &str) -> Result<Vec<String>>
}
#[cfg(feature = "websocket")]
async fn parse_host_meta(rel: &str, bytes: &[u8]) -> Result<Vec<String>> {
async fn parse_host_meta_xml(rel: &str, bytes: &[u8]) -> Result<Vec<String>> {
let mut vec = Vec::new();
let mut stanza_reader = StanzaReader(bytes);
let mut filter = StanzaFilter::new(8192);
@ -374,25 +396,212 @@ async fn parse_host_meta(rel: &str, bytes: &[u8]) -> Result<Vec<String>> {
}
#[cfg(feature = "websocket")]
async fn collect_host_meta(domain: &str, rel: &str) -> Result<Vec<String>> {
async fn collect_host_meta_xml(domain: &str, rel: &str) -> Result<Vec<String>> {
let url = format!("https://{}/.well-known/host-meta", domain);
let resp = reqwest::get(&url).await?;
let resp = https_get(&url).await?;
if resp.status().is_success() {
parse_host_meta(rel, resp.bytes().await?.as_ref()).await
parse_host_meta_xml(rel, resp.bytes().await?.as_ref()).await
} else {
bail!("failed with status code {} for url {}", resp.status(), url)
}
}
pub async fn https_get<T: reqwest::IntoUrl>(url: T) -> reqwest::Result<reqwest::Response> {
// todo: resolve URL with our resolver
reqwest::Client::builder().https_only(true).build()?.get(url).send().await
}
// https://datatracker.ietf.org/doc/html/rfc7711
// https://www.iana.org/assignments/posh-service-names/posh-service-names.xhtml
async fn collect_posh(domain: &str) -> Result<Posh> {
match tokio::join!(collect_posh_service(domain, "xmpp-client"), collect_posh_service(domain, "xmpp-server")) {
(Ok(client), Ok(server)) => Ok(client.append(server)),
(_, Ok(server)) => Ok(server),
(client, _) => client,
}
}
async fn collect_posh_service(domain: &str, service_name: &str) -> Result<Posh> {
let url = format!("https://{}/.well-known/posh/{}.json", domain, service_name);
let resp = https_get(&url).await?;
if resp.status().is_success() {
match resp.json::<PoshJson>().await? {
PoshJson::PoshFingerprints { fingerprints, expires } => Posh::new(fingerprints, expires),
PoshJson::PoshRedirect { url, expires } => {
let resp = https_get(&url).await?;
match resp.json::<PoshJson>().await? {
PoshJson::PoshRedirect { .. } => bail!("posh illegal url redirect to another url"),
PoshJson::PoshFingerprints { fingerprints, expires: expires2 } => Posh::new(
fingerprints,
// expires is supposed to be the least of these two
min(expires, expires2),
),
}
}
}
} else {
bail!("failed with status code {} for url {}", resp.status(), url)
}
}
fn combine_uniq(target: &mut Vec<String>, mut other: Vec<String>) {
target.append(&mut other);
target.sort();
target.dedup();
}
fn min(a: u64, b: u64) -> u64 {
if a < b {
a
} else {
b
}
}
#[derive(Deserialize, Debug)]
#[serde(untagged)]
enum PoshJson {
PoshFingerprints { fingerprints: Vec<Fingerprint>, expires: u64 },
PoshRedirect { url: String, expires: u64 },
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "kebab-case")]
struct Fingerprint {
// todo: support more algorithms or no?
sha_256: Option<String>,
sha_512: Option<String>,
}
#[derive(Debug)]
pub struct Posh {
sha_256_fingerprints: Vec<String>,
sha_512_fingerprints: Vec<String>,
expires: u64,
}
impl Posh {
fn new(fingerprints: Vec<Fingerprint>, expires: u64) -> Result<Self> {
if expires == 0 {
bail!("posh expires is 0, ignoring");
}
let mut sha_256_fingerprints = Vec::with_capacity(fingerprints.len());
let mut sha_512_fingerprints = Vec::with_capacity(fingerprints.len());
for f in fingerprints {
if let Some(h) = f.sha_256 {
sha_256_fingerprints.push(h);
}
if let Some(h) = f.sha_512 {
sha_512_fingerprints.push(h);
}
}
Ok(Posh {
sha_256_fingerprints,
sha_512_fingerprints,
expires,
})
}
fn append(mut self, other: Self) -> Self {
combine_uniq(&mut self.sha_256_fingerprints, other.sha_256_fingerprints);
combine_uniq(&mut self.sha_512_fingerprints, other.sha_512_fingerprints);
self.expires = min(self.expires, other.expires);
self
}
pub fn valid_cert(&self, cert: &[u8]) -> bool {
(!self.sha_256_fingerprints.is_empty() && self.sha_256_fingerprints.contains(&digest(&SHA256, cert)))
|| (!self.sha_512_fingerprints.is_empty() && self.sha_512_fingerprints.contains(&digest(&SHA512, cert)))
}
}
fn digest(algorithm: &'static Algorithm, buf: &[u8]) -> String {
let mut context = DigestContext::new(algorithm);
context.update(buf);
let digest = context.finish();
BASE64.encode(digest.as_ref())
}
#[cfg(test)]
mod tests {
use crate::srv::*;
fn valid_posh(posh: &[u8], cert: &[u8]) -> bool {
let posh: PoshJson = serde_json::from_slice(&posh[..]).unwrap();
let cert = BASE64.decode(cert).unwrap();
println!("posh: {:?}", posh);
if let PoshJson::PoshFingerprints { fingerprints, expires } = posh {
let posh = Posh::new(fingerprints, expires).unwrap();
println!("posh: {:?}", posh);
posh.valid_cert(&cert)
} else {
false
}
}
#[test]
fn posh_deserialize() {
assert!(valid_posh(
br###"{"expires":86400,"fingerprints":[{"sha-256":"6sKZUeE0LBwbCXqeoHJsGCjpFLNrL9QF2W6NhDYnV4I="}]}"###,
br###"MIICHDCCAaGgAwIBAgIUQCykdom3fbgtYxbVzk12uY13FqUwCgYIKoZIzj0EAwIwGjEYMBYGA1UEAwwPcG9zaC5iYWR4bXBwLmV1MB4XDTIxMTAxNTE0NDkzMloXDTIyMTAxNTE0NDkzMlowGjEYMBYGA1UEAwwPcG9zaC5iYWR4bXBwLmV1MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEUeyvxJeBihodBTIATT5szGfsgeNE1nNIEjOU+PSDBpfCFEAKw5oIxB35TGyPvOe1MBSBXcaRFXBSKBZ4AkVRPsKsGjEmUa9GpIbEwcsUvw+NTx8OT81tuTEbpjs0QGy0o4GnMIGkMAwGA1UdEwQFMAMBAf8wgZMGA1UdEQSBizCBiKAqBggrBgEFBQcIB6AeFhxfeG1wcC1jbGllbnQucG9zaC5iYWR4bXBwLmV1oCoGCCsGAQUFBwgHoB4WHF94bXBwLXNlcnZlci5wb3NoLmJhZHhtcHAuZXWgHQYIKwYBBQUHCAWgEQwPcG9zaC5iYWR4bXBwLmV1gg9wb3NoLmJhZHhtcHAuZXUwCgYIKoZIzj0EAwIDaQAwZgIxAKLvjCkY9OV9dX7emghbroYgbqqWWBaQuIHLqtOEKpS+R88fOfEJbokViKNinY3ugwIxAPJ/oiK8ekF0gfa4aWmoCscbNv2Ns7HD+iSLm4GcSc/tza9r+uXVsV+0uqJ3UleTFA=="###
));
assert!(valid_posh(
br###"{"expires":86400,"fingerprints":[{"sha-512":"7S7zdev/QvRxHYguWHhD5Thlolj+H4aHo9Qy3Y1R6p7WGKnNBNPxk+tnHRSIs5CJIHIR3M7a6wNkgAC5uLWL/g=="}]}"###,
br###"MIICHDCCAaGgAwIBAgIUQCykdom3fbgtYxbVzk12uY13FqUwCgYIKoZIzj0EAwIwGjEYMBYGA1UEAwwPcG9zaC5iYWR4bXBwLmV1MB4XDTIxMTAxNTE0NDkzMloXDTIyMTAxNTE0NDkzMlowGjEYMBYGA1UEAwwPcG9zaC5iYWR4bXBwLmV1MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEUeyvxJeBihodBTIATT5szGfsgeNE1nNIEjOU+PSDBpfCFEAKw5oIxB35TGyPvOe1MBSBXcaRFXBSKBZ4AkVRPsKsGjEmUa9GpIbEwcsUvw+NTx8OT81tuTEbpjs0QGy0o4GnMIGkMAwGA1UdEwQFMAMBAf8wgZMGA1UdEQSBizCBiKAqBggrBgEFBQcIB6AeFhxfeG1wcC1jbGllbnQucG9zaC5iYWR4bXBwLmV1oCoGCCsGAQUFBwgHoB4WHF94bXBwLXNlcnZlci5wb3NoLmJhZHhtcHAuZXWgHQYIKwYBBQUHCAWgEQwPcG9zaC5iYWR4bXBwLmV1gg9wb3NoLmJhZHhtcHAuZXUwCgYIKoZIzj0EAwIDaQAwZgIxAKLvjCkY9OV9dX7emghbroYgbqqWWBaQuIHLqtOEKpS+R88fOfEJbokViKNinY3ugwIxAPJ/oiK8ekF0gfa4aWmoCscbNv2Ns7HD+iSLm4GcSc/tza9r+uXVsV+0uqJ3UleTFA=="###
));
assert!(!valid_posh(
br###"{"expires":86400,"fingerprints":[{"sha-256":"Dp8REwxYw0vFt2tRAGIAT4nNtXD2wwqL0eF5QdN4Zm4="}]}"###,
br###"MIICHDCCAaGgAwIBAgIUQCykdom3fbgtYxbVzk12uY13FqUwCgYIKoZIzj0EAwIwGjEYMBYGA1UEAwwPcG9zaC5iYWR4bXBwLmV1MB4XDTIxMTAxNTE0NDkzMloXDTIyMTAxNTE0NDkzMlowGjEYMBYGA1UEAwwPcG9zaC5iYWR4bXBwLmV1MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEUeyvxJeBihodBTIATT5szGfsgeNE1nNIEjOU+PSDBpfCFEAKw5oIxB35TGyPvOe1MBSBXcaRFXBSKBZ4AkVRPsKsGjEmUa9GpIbEwcsUvw+NTx8OT81tuTEbpjs0QGy0o4GnMIGkMAwGA1UdEwQFMAMBAf8wgZMGA1UdEQSBizCBiKAqBggrBgEFBQcIB6AeFhxfeG1wcC1jbGllbnQucG9zaC5iYWR4bXBwLmV1oCoGCCsGAQUFBwgHoB4WHF94bXBwLXNlcnZlci5wb3NoLmJhZHhtcHAuZXWgHQYIKwYBBQUHCAWgEQwPcG9zaC5iYWR4bXBwLmV1gg9wb3NoLmJhZHhtcHAuZXUwCgYIKoZIzj0EAwIDaQAwZgIxAKLvjCkY9OV9dX7emghbroYgbqqWWBaQuIHLqtOEKpS+R88fOfEJbokViKNinY3ugwIxAPJ/oiK8ekF0gfa4aWmoCscbNv2Ns7HD+iSLm4GcSc/tza9r+uXVsV+0uqJ3UleTFA=="###
));
assert!(!valid_posh(
br###"{"expires":86400,"fingerprints":[{"sha-512":"GwfqWa8hIYCGt9V9EgdDHg6npGeGhpAwryUJkU1FuP6CNiF2Auv1s1Tp9gSWSlCTbClSmzz+sorNVOfaDW6m3Q=="}]}"###,
br###"MIICHDCCAaGgAwIBAgIUQCykdom3fbgtYxbVzk12uY13FqUwCgYIKoZIzj0EAwIwGjEYMBYGA1UEAwwPcG9zaC5iYWR4bXBwLmV1MB4XDTIxMTAxNTE0NDkzMloXDTIyMTAxNTE0NDkzMlowGjEYMBYGA1UEAwwPcG9zaC5iYWR4bXBwLmV1MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEUeyvxJeBihodBTIATT5szGfsgeNE1nNIEjOU+PSDBpfCFEAKw5oIxB35TGyPvOe1MBSBXcaRFXBSKBZ4AkVRPsKsGjEmUa9GpIbEwcsUvw+NTx8OT81tuTEbpjs0QGy0o4GnMIGkMAwGA1UdEwQFMAMBAf8wgZMGA1UdEQSBizCBiKAqBggrBgEFBQcIB6AeFhxfeG1wcC1jbGllbnQucG9zaC5iYWR4bXBwLmV1oCoGCCsGAQUFBwgHoB4WHF94bXBwLXNlcnZlci5wb3NoLmJhZHhtcHAuZXWgHQYIKwYBBQUHCAWgEQwPcG9zaC5iYWR4bXBwLmV1gg9wb3NoLmJhZHhtcHAuZXUwCgYIKoZIzj0EAwIDaQAwZgIxAKLvjCkY9OV9dX7emghbroYgbqqWWBaQuIHLqtOEKpS+R88fOfEJbokViKNinY3ugwIxAPJ/oiK8ekF0gfa4aWmoCscbNv2Ns7HD+iSLm4GcSc/tza9r+uXVsV+0uqJ3UleTFA=="###
));
let posh = br###"
{
"fingerprints": [
{
"sha-256": "4/mggdlVx8A3pvHAWW5sD+qJyMtUHgiRuPjVC48N0XQ=",
"sha-512": "25N+1hB2Vo42l9lSGqw+n3BKFhDHsyork8ou+D9B43TXeJ1J81mdQEDqm39oR/EHkPBDDG1y5+AG94Kec0xVqA==",
"bla": "woo"
}
],
"expires": 604800
}
"###;
let posh: PoshJson = serde_json::from_slice(&posh[..]).unwrap();
println!("posh: {:?}", posh);
if let PoshJson::PoshFingerprints { fingerprints, expires } = posh {
let posh = Posh::new(fingerprints, expires);
println!("posh: {:?}", posh);
}
let posh = br###"
{
"url":"https://hosting.example.net/.well-known/posh/spice.json",
"expires": 604800
}
"###;
let posh: PoshJson = serde_json::from_slice(&posh[..]).unwrap();
println!("posh: {:?}", posh);
}
//#[tokio::test]
async fn posh() -> Result<()> {
let domain = "posh.badxmpp.eu";
let posh = collect_posh(domain).await.unwrap();
println!("posh for domain {}: {:?}", domain, posh);
Ok(())
}
//#[tokio::test]
async fn srv() -> Result<()> {
let domain = "burtrum.org";
let is_c2s = true;
for srv in get_xmpp_connections(domain, is_c2s).await? {
let (srvs, cert_verifier) = get_xmpp_connections(domain, is_c2s).await?;
println!("cert_verifier: {:?}", cert_verifier);
for srv in srvs {
println!("trying 1 domain {}, SRV: {:?}", domain, srv);
let ips = RESOLVER.lookup_ip(srv.target.clone()).await?;
for ip in ips.iter() {
@ -407,7 +616,7 @@ mod tests {
async fn http() -> Result<()> {
let hosts = collect_host_meta_json("burtrum.org", "urn:xmpp:alt-connections:websocket").await?;
println!("{:?}", hosts);
let hosts = collect_host_meta("burtrum.org", "urn:xmpp:alt-connections:websocket").await?;
let hosts = collect_host_meta_xml("burtrum.org", "urn:xmpp:alt-connections:websocket").await?;
println!("{:?}", hosts);
Ok(())
}
@ -416,22 +625,22 @@ mod tests {
#[tokio::test]
async fn test_parse_host_meta() -> Result<()> {
let xrd = br#"<XRD xmlns='http://docs.oasis-open.org/ns/xri/xrd-1.0'><Link rel='urn:xmpp:alt-connections:xbosh' href='https://burtrum.org/http-bind'/><Link rel='urn:xmpp:alt-connections:websocket' href='wss://burtrum.org/xmpp-websocket'/></XRD>"#;
assert_eq!(parse_host_meta("urn:xmpp:alt-connections:websocket", xrd).await?, vec!["wss://burtrum.org/xmpp-websocket"]);
assert_eq!(parse_host_meta_xml("urn:xmpp:alt-connections:websocket", xrd).await?, vec!["wss://burtrum.org/xmpp-websocket"]);
let xrd = br#"<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"><Link rel="urn:xmpp:alt-connections:xbosh" href="https://burtrum.org/http-bind"/><Link rel="urn:xmpp:alt-connections:websocket" href="wss://burtrum.org/xmpp-websocket"/></XRD>"#;
assert_eq!(parse_host_meta("urn:xmpp:alt-connections:websocket", xrd).await?, vec!["wss://burtrum.org/xmpp-websocket"]);
assert_eq!(parse_host_meta_xml("urn:xmpp:alt-connections:websocket", xrd).await?, vec!["wss://burtrum.org/xmpp-websocket"]);
let xrd = br#"<xrd xmlns='http://docs.oasis-open.org/ns/xri/xrd-1.0'><link rel='urn:xmpp:alt-connections:xbosh' href='https://burtrum.org/http-bind'/><link rel='urn:xmpp:alt-connections:websocket' href='wss://burtrum.org/xmpp-websocket'/></xrd>"#;
assert_eq!(parse_host_meta("urn:xmpp:alt-connections:websocket", xrd).await?, vec!["wss://burtrum.org/xmpp-websocket"]);
assert_eq!(parse_host_meta_xml("urn:xmpp:alt-connections:websocket", xrd).await?, vec!["wss://burtrum.org/xmpp-websocket"]);
let xrd = br#"<xrd xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"><link rel="urn:xmpp:alt-connections:xbosh" href="https://burtrum.org/http-bind"/><link rel="urn:xmpp:alt-connections:websocket" href="wss://burtrum.org/xmpp-websocket"/></xrd>"#;
assert_eq!(parse_host_meta("urn:xmpp:alt-connections:websocket", xrd).await?, vec!["wss://burtrum.org/xmpp-websocket"]);
assert_eq!(parse_host_meta_xml("urn:xmpp:alt-connections:websocket", xrd).await?, vec!["wss://burtrum.org/xmpp-websocket"]);
let xrd = br#"<xrd xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"><link rel="urn:xmpp:alt-connections:xbosh" href="https://burtrum.org/http-bind"/><link rel="urn:xmpp:alt-connections:websocket" href="wss://burtrum.org/xmpp-websocket"/><link rel="urn:xmpp:alt-connections:s2s-websocket" href="wss://burtrum.org/xmpp-websocket-s2s"/></xrd>"#;
assert_eq!(parse_host_meta("urn:xmpp:alt-connections:s2s-websocket", xrd).await?, vec!["wss://burtrum.org/xmpp-websocket-s2s"]);
assert_eq!(parse_host_meta_xml("urn:xmpp:alt-connections:s2s-websocket", xrd).await?, vec!["wss://burtrum.org/xmpp-websocket-s2s"]);
let xrd = br#"<xrd xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"><link rel="urn:xmpp:alt-connections:xbosh" href="https://burtrum.org/http-bind"/><link rel="urn:xmpp:alt-connections:websocket" href="wss://burtrum.org/xmpp-websocket"/><link rel="urn:xmpp:alt-connections:s2s-websocket" href="wss://burtrum.org/xmpp-websocket-s2s"/></xrd>"#;
assert_eq!(parse_host_meta("urn:xmpp:alt-connections:websocket", xrd).await?, vec!["wss://burtrum.org/xmpp-websocket"]);
assert_eq!(parse_host_meta_xml("urn:xmpp:alt-connections:websocket", xrd).await?, vec!["wss://burtrum.org/xmpp-websocket"]);
Ok(())
}

View File

@ -7,16 +7,16 @@ use tokio::io::{AsyncBufReadExt, BufStream};
use tokio_rustls::rustls::ServerName;
#[cfg(feature = "outgoing")]
pub async fn tls_connect(target: SocketAddr, server_name: &str, is_c2s: bool, config: OutgoingConfig) -> Result<(StanzaWrite, StanzaRead)> {
pub async fn tls_connect(target: SocketAddr, server_name: &str, config: OutgoingVerifierConfig) -> Result<(StanzaWrite, StanzaRead)> {
let dnsname = ServerName::try_from(server_name)?;
let stream = tokio::net::TcpStream::connect(target).await?;
let stream = config.connector_alpn(is_c2s).connect(dnsname, stream).await?;
let stream = config.connector_alpn.connect(dnsname, stream).await?;
let (rd, wrt) = tokio::io::split(stream);
Ok((StanzaWrite::new(wrt), StanzaRead::new(rd)))
}
#[cfg(feature = "outgoing")]
pub async fn starttls_connect(target: SocketAddr, server_name: &str, is_c2s: bool, stream_open: &[u8], in_filter: &mut StanzaFilter, config: OutgoingConfig) -> Result<(StanzaWrite, StanzaRead)> {
pub async fn starttls_connect(target: SocketAddr, server_name: &str, stream_open: &[u8], in_filter: &mut StanzaFilter, config: OutgoingVerifierConfig) -> Result<(StanzaWrite, StanzaRead)> {
let dnsname = ServerName::try_from(server_name)?;
let mut stream = tokio::net::TcpStream::connect(target).await?;
let (in_rd, mut in_wr) = stream.split();
@ -54,7 +54,7 @@ pub async fn starttls_connect(target: SocketAddr, server_name: &str, is_c2s: boo
}
debug!("starttls starting TLS {}", server_name);
let stream = config.connector(is_c2s).connect(dnsname, stream).await?;
let stream = config.connector.connect(dnsname, stream).await?;
let (rd, wrt) = tokio::io::split(stream);
Ok((StanzaWrite::new(wrt), StanzaRead::new(rd)))
}

View File

@ -1,8 +1,12 @@
use crate::Posh;
use log::debug;
use rustls::client::{ServerCertVerified, ServerCertVerifier};
use rustls::server::{ClientCertVerified, ClientCertVerifier};
use rustls::{Certificate, DistinguishedNames, Error};
use rustls::{Certificate, DistinguishedNames, Error, ServerName};
use std::convert::TryFrom;
use std::time::SystemTime;
use tokio_rustls::webpki;
use tokio_rustls::webpki::DnsName;
type SignatureAlgorithms = &'static [&'static webpki::SignatureAlgorithm];
@ -33,9 +37,9 @@ pub fn pki_error(error: webpki::Error) -> Error {
}
}
pub struct AllowAnyAnonymousOrAuthenticatedServer;
pub struct AllowAnonymousOrAnyCert;
impl ClientCertVerifier for AllowAnyAnonymousOrAuthenticatedServer {
impl ClientCertVerifier for AllowAnonymousOrAnyCert {
fn offer_client_auth(&self) -> bool {
true
}
@ -49,11 +53,8 @@ impl ClientCertVerifier for AllowAnyAnonymousOrAuthenticatedServer {
}
fn verify_client_cert(&self, end_entity: &Certificate, intermediates: &[Certificate], now: SystemTime) -> Result<ClientCertVerified, Error> {
let (cert, chain) = prepare(end_entity, intermediates)?;
let now = webpki::Time::try_from(now).map_err(|_| Error::FailedToGetCurrentTime)?;
cert.verify_is_valid_tls_server_cert(SUPPORTED_SIG_ALGS, &crate::TLS_SERVER_ROOTS, &chain, now)
.map_err(pki_error)
.map(|_| ClientCertVerified::assertion())
// this is checked only after the first <stream: stanza so we know the from=
Ok(ClientCertVerified::assertion())
}
}
@ -67,3 +68,59 @@ fn prepare<'a, 'b>(end_entity: &'a Certificate, intermediates: &'a [Certificate]
Ok((cert, intermediates))
}
#[derive(Debug)]
pub struct XmppServerCertVerifier {
names: Vec<DnsName>,
posh: Option<Posh>,
}
impl XmppServerCertVerifier {
pub fn new(names: Vec<DnsName>, posh: Option<Posh>) -> Self {
XmppServerCertVerifier { names, posh }
}
pub fn verify_cert(&self, end_entity: &Certificate, intermediates: &[Certificate], now: SystemTime) -> Result<ServerCertVerified, Error> {
if let Some(ref posh) = self.posh {
if posh.valid_cert(end_entity.as_ref()) {
debug!("posh succeeded for {:?}", self.names.first());
return Ok(ServerCertVerified::assertion());
} else {
// per RFC if POSH fails, continue with other methods
debug!("posh failed for {:?}", self.names.first());
}
}
// from WebPkiVerifier, validates CA trusted cert
let (cert, chain) = prepare(end_entity, intermediates)?;
let webpki_now = webpki::Time::try_from(now).map_err(|_| Error::FailedToGetCurrentTime)?;
cert.verify_is_valid_tls_server_cert(SUPPORTED_SIG_ALGS, &crate::TLS_SERVER_ROOTS, &chain, webpki_now)
.map_err(pki_error)?;
for name in &self.names {
if cert.verify_is_valid_for_dns_name(name.as_ref()).is_ok() {
return Ok(ServerCertVerified::assertion());
}
}
Err(Error::InvalidCertificateData(format!("invalid peer certificate: all validation attempts failed: {:?}", end_entity)))
}
}
impl ServerCertVerifier for XmppServerCertVerifier {
fn verify_server_cert(
&self,
end_entity: &Certificate,
intermediates: &[Certificate],
_server_name: &ServerName,
_scts: &mut dyn Iterator<Item = &[u8]>,
_ocsp_response: &[u8],
now: SystemTime,
) -> Result<ServerCertVerified, Error> {
self.verify_cert(end_entity, intermediates, now)
}
fn request_scts(&self) -> bool {
false
}
}

View File

@ -104,14 +104,14 @@ use tokio_tungstenite::tungstenite::http::header::{ORIGIN, SEC_WEBSOCKET_PROTOCO
use tokio_tungstenite::tungstenite::http::Uri;
#[cfg(feature = "outgoing")]
pub async fn websocket_connect(target: SocketAddr, server_name: &str, url: &Uri, origin: &str, is_c2s: bool, config: OutgoingConfig) -> Result<(StanzaWrite, StanzaRead)> {
pub async fn websocket_connect(target: SocketAddr, server_name: &str, url: &Uri, origin: &str, config: OutgoingVerifierConfig) -> Result<(StanzaWrite, StanzaRead)> {
let mut request = url.into_client_request()?;
request.headers_mut().append(SEC_WEBSOCKET_PROTOCOL, "xmpp".parse()?);
request.headers_mut().append(ORIGIN, origin.parse()?);
let dnsname = ServerName::try_from(server_name)?;
let stream = tokio::net::TcpStream::connect(target).await?;
let stream = config.connector(is_c2s).connect(dnsname, stream).await?;
let stream = config.connector.connect(dnsname, stream).await?;
let stream: tokio_rustls::TlsStream<tokio::net::TcpStream> = stream.into();
// todo: tokio_tungstenite seems to have a bug, if the write buffer is non-zero, it'll hang forever, even though we always flush, investigate