Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support cipher suite selection #167

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ libc = "0.2"
tempfile = "3.1.0"

[target.'cfg(target_os = "windows")'.dependencies]
schannel = "0.1.16"
schannel = "0.1.19"

[target.'cfg(not(any(target_os = "windows", target_os = "macos", target_os = "ios")))'.dependencies]
log = "0.4.5"
Expand Down
265 changes: 264 additions & 1 deletion src/imp/openssl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,238 @@ use self::openssl::ssl::{
SslVerifyMode,
};
use self::openssl::x509::{store::X509StoreBuilder, X509VerifyResult, X509};
use std::borrow;
use std::collections::HashSet;
use std::error;
use std::fmt;
use std::io;
use std::sync::Once;

use self::openssl::pkey::Private;
use {Protocol, TlsAcceptorBuilder, TlsConnectorBuilder};
use {
CipherSuiteSet, Protocol, TlsAcceptorBuilder, TlsBulkEncryptionAlgorithm, TlsConnectorBuilder,
TlsHashAlgorithm, TlsKeyExchangeAlgorithm, TlsSignatureAlgorithm,
};

const CIPHER_STRING_SUFFIX: &[&str] = &[
"!aNULL",
"!eNULL",
"!IDEA",
"!SEED",
"!SRP",
"!PSK",
"@STRENGTH",
];

fn cartesian_product(
xs: impl IntoIterator<Item = Vec<&'static str>>,
ys: impl IntoIterator<Item = &'static str> + Clone,
) -> Vec<Vec<&'static str>> {
xs.into_iter()
.flat_map(move |x| ys.clone().into_iter().map(move |y| [&x, &[y][..]].concat()))
.collect()
}

/// AES-GCM ciphersuites aren't included in AES128 or AES256. However, specifying `AESGCM` in the
/// cipher string doesn't allow us to specify the bitwidth of the AES cipher used, nor does it
/// allow us to specify the bitwidth of the SHA algorithm.
fn expand_gcm_algorithms(cipher_suites: &CipherSuiteSet) -> Vec<&'static str> {
let first = cipher_suites
.key_exchange
.iter()
.flat_map(|alg| -> &[&str] {
match alg {
TlsKeyExchangeAlgorithm::Dhe => &[
"DHE-RSA-AES128-GCM-SHA256",
"DHE-RSA-AES256-GCM-SHA384",
"DHE-DSS-AES128-GCM-SHA256",
"DHE-DSS-AES256-GCM-SHA384",
],
TlsKeyExchangeAlgorithm::Ecdhe => &[
"ECDHE-RSA-AES128-GCM-SHA256",
"ECDHE-RSA-AES256-GCM-SHA384",
"ECDHE-ECDSA-AES128-GCM-SHA256",
"ECDHE-ECDSA-AES256-GCM-SHA384",
],
TlsKeyExchangeAlgorithm::Rsa => &["AES128-GCM-SHA256", "AES256-GCM-SHA384"],
TlsKeyExchangeAlgorithm::__NonExhaustive => unreachable!(),
}
})
.copied();
let rest: &[HashSet<_>] = &[
cipher_suites
.signature
.iter()
.flat_map(|alg| -> &[&str] {
match alg {
TlsSignatureAlgorithm::Dss => &[
"DH-DSS-AES128-GCM-SHA256",
"DH-DSS-AES256-GCM-SHA384",
"DHE-DSS-AES128-GCM-SHA256",
"DHE-DSS-AES256-GCM-SHA384",
],
TlsSignatureAlgorithm::Ecdsa => &[
"ECDH-ECDSA-AES128-GCM-SHA256",
"ECDH-ECDSA-AES256-GCM-SHA384",
"ECDHE-ECDSA-AES128-GCM-SHA256",
"ECDHE-ECDSA-AES256-GCM-SHA384",
],
TlsSignatureAlgorithm::Rsa => &[
"AES128-GCM-SHA256",
"AES256-GCM-SHA384",
"DH-RSA-AES128-GCM-SHA256",
"DH-RSA-AES256-GCM-SHA384",
"DHE-RSA-AES128-GCM-SHA256",
"DHE-RSA-AES256-GCM-SHA384",
"ECDH-RSA-AES128-GCM-SHA256",
"ECDH-RSA-AES256-GCM-SHA384",
"ECDHE-RSA-AES128-GCM-SHA256",
"ECDHE-RSA-AES256-GCM-SHA384",
],
TlsSignatureAlgorithm::__NonExhaustive => unreachable!(),
}
})
.copied()
.collect(),
cipher_suites
.bulk_encryption
.iter()
.flat_map(|alg| -> &[&str] {
match alg {
TlsBulkEncryptionAlgorithm::Aes128 => &[
"AES128-GCM-SHA256",
"DH-RSA-AES128-GCM-SHA256",
"DH-DSS-AES128-GCM-SHA256",
"DHE-RSA-AES128-GCM-SHA256",
"DHE-DSS-AES128-GCM-SHA256",
"ECDH-RSA-AES128-GCM-SHA256",
"ECDH-ECDSA-AES128-GCM-SHA256",
"ECDHE-RSA-AES128-GCM-SHA256",
"ECDHE-ECDSA-AES128-GCM-SHA256",
],
TlsBulkEncryptionAlgorithm::Aes256 => &[
"AES256-GCM-SHA384",
"DH-RSA-AES256-GCM-SHA384",
"DH-DSS-AES256-GCM-SHA384",
"DHE-RSA-AES256-GCM-SHA384",
"DHE-DSS-AES256-GCM-SHA384",
"ECDH-RSA-AES256-GCM-SHA384",
"ECDH-ECDSA-AES256-GCM-SHA384",
"ECDHE-RSA-AES256-GCM-SHA384",
"ECDHE-ECDSA-AES256-GCM-SHA384",
],
TlsBulkEncryptionAlgorithm::Des => &[],
TlsBulkEncryptionAlgorithm::Rc2 => &[],
TlsBulkEncryptionAlgorithm::Rc4 => &[],
TlsBulkEncryptionAlgorithm::TripleDes => &[],
TlsBulkEncryptionAlgorithm::__NonExhaustive => unreachable!(),
}
})
.copied()
.collect(),
cipher_suites
.hash
.iter()
.flat_map(|alg| -> &[&str] {
match alg {
TlsHashAlgorithm::Md5 => &[],
TlsHashAlgorithm::Sha1 => &[],
TlsHashAlgorithm::Sha256 => &[
"AES128-GCM-SHA256",
"DH-RSA-AES128-GCM-SHA256",
"DH-DSS-AES128-GCM-SHA256",
"DHE-RSA-AES128-GCM-SHA256",
"DHE-DSS-AES128-GCM-SHA256",
"ECDH-RSA-AES128-GCM-SHA256",
"ECDH-ECDSA-AES128-GCM-SHA256",
"ECDHE-RSA-AES128-GCM-SHA256",
"ECDHE-ECDSA-AES128-GCM-SHA256",
],
TlsHashAlgorithm::Sha384 => &[
"AES256-GCM-SHA384",
"DH-RSA-AES256-GCM-SHA384",
"DH-DSS-AES256-GCM-SHA384",
"DHE-RSA-AES256-GCM-SHA384",
"DHE-DSS-AES256-GCM-SHA384",
"ECDH-RSA-AES256-GCM-SHA384",
"ECDH-ECDSA-AES256-GCM-SHA384",
"ECDHE-RSA-AES256-GCM-SHA384",
"ECDHE-ECDSA-AES256-GCM-SHA384",
],
TlsHashAlgorithm::__NonExhaustive => unreachable!(),
}
})
.copied()
.collect(),
];

first
.filter(|alg| rest.iter().all(|algs| algs.contains(alg)))
.collect()
}

fn expand_algorithms(cipher_suites: &CipherSuiteSet) -> String {
let mut cipher_suite_strings: Vec<Vec<&'static str>> = vec![];

cipher_suite_strings.extend(cipher_suites.key_exchange.iter().map(|alg| {
vec![match alg {
TlsKeyExchangeAlgorithm::Dhe => "DHE",
TlsKeyExchangeAlgorithm::Ecdhe => "ECDHE",
TlsKeyExchangeAlgorithm::Rsa => "kRSA",
TlsKeyExchangeAlgorithm::__NonExhaustive => unreachable!(),
}]
}));

cipher_suite_strings = cartesian_product(
cipher_suite_strings,
cipher_suites.signature.iter().map(|alg| match alg {
TlsSignatureAlgorithm::Dss => "aDSS",
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like below, do we really need DSS?

TlsSignatureAlgorithm::Ecdsa => "aECDSA",
TlsSignatureAlgorithm::Rsa => "aRSA",
TlsSignatureAlgorithm::__NonExhaustive => unreachable!(),
}),
);
cipher_suite_strings = cartesian_product(
cipher_suite_strings,
cipher_suites.bulk_encryption.iter().map(|alg| match alg {
TlsBulkEncryptionAlgorithm::Aes128 => "AES128",
TlsBulkEncryptionAlgorithm::Aes256 => "AES256",
TlsBulkEncryptionAlgorithm::Des => "DES",
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need to offer historical curiosities like DES and RC2?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I lean towards not being opinionated about what is available. I can remove them if that's your preference, but I can imagine that someone might have some use for them for connecting to old, insecure servers.

TlsBulkEncryptionAlgorithm::Rc2 => "RC2",
TlsBulkEncryptionAlgorithm::Rc4 => "RC4",
TlsBulkEncryptionAlgorithm::TripleDes => "3DES",
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should include ChaCha20 IMO.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would be nice, but AFAICT Schannel doesn't have any ChaCha20 ciphersuites, and I've just included the GCD of the available algorithms across the different backends.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's fine that schannel doesn't support it yet - many versions of OpenSSL don't either. As long as it's paired with another algorithm that is supported it'll work.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, what's the experience you had in mind, though, if the user chooses to only specify ChaCha20 on Windows? As currently written, nothing here returns a Result, and from some testing, it looks passing no bulk encryption algorithms to the supported_algorithms call in Schannel will result in Windows choosing some (most likely system-wide) defaults.

On Windows 10 19h1 where I'm trying this out, it looks like it chose to allow AES-128, AES-256, and 3DES, which seems consistent with https://docs.microsoft.com/en-us/windows/win32/secauthn/tls-cipher-suites-in-windows-10-v1903, but probably not ideal as far as the behavior of this API.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would need to fail to handshake, yeah. If that's not easily doable though we can leave it out for now.

TlsBulkEncryptionAlgorithm::__NonExhaustive => unreachable!(),
}),
);
cipher_suite_strings = cartesian_product(
cipher_suite_strings,
cipher_suites.hash.iter().map(|alg| match alg {
TlsHashAlgorithm::Md5 => "MD5",
TlsHashAlgorithm::Sha1 => "SHA1",
TlsHashAlgorithm::Sha256 => "SHA256",
TlsHashAlgorithm::Sha384 => "SHA384",
TlsHashAlgorithm::__NonExhaustive => unreachable!(),
}),
);

// GCM first, as `@STRENGTH` sorts purely on bitwidth, and otherwise respects the initial
// ordering. GCM is generally preferred over CBC for performance and security reasons.
expand_gcm_algorithms(cipher_suites)
.into_iter()
.map(borrow::Cow::Borrowed)
.chain(
cipher_suite_strings
.into_iter()
.map(|parts| borrow::Cow::Owned(parts.join("+"))),
)
.chain(
CIPHER_STRING_SUFFIX
.iter()
.map(|s| borrow::Cow::Borrowed(*s)),
)
.collect::<Vec<_>>()
.join(":")
}

#[cfg(have_min_max_version)]
fn supported_protocols(
Expand Down Expand Up @@ -262,6 +487,9 @@ impl TlsConnector {
connector.add_extra_chain_cert(cert.to_owned())?;
}
}
if let Some(ref cipher_suites) = builder.cipher_suites {
connector.set_cipher_list(&expand_algorithms(cipher_suites))?;
}
supported_protocols(builder.min_protocol, builder.max_protocol, &mut connector)?;

if builder.disable_built_in_roots {
Expand Down Expand Up @@ -452,3 +680,38 @@ impl<S: io::Read + io::Write> io::Write for TlsStream<S> {
self.0.flush()
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn expand_algorithms_basic() {
assert_eq!(
expand_algorithms(&CipherSuiteSet {
key_exchange: vec![TlsKeyExchangeAlgorithm::Dhe, TlsKeyExchangeAlgorithm::Ecdhe],
signature: vec![TlsSignatureAlgorithm::Rsa],
bulk_encryption: vec![
TlsBulkEncryptionAlgorithm::Aes128,
TlsBulkEncryptionAlgorithm::Aes256
],
hash: vec![TlsHashAlgorithm::Sha256, TlsHashAlgorithm::Sha384],
}),
"\
DHE-RSA-AES128-GCM-SHA256:\
DHE-RSA-AES256-GCM-SHA384:\
ECDHE-RSA-AES128-GCM-SHA256:\
ECDHE-RSA-AES256-GCM-SHA384:\
DHE+aRSA+AES128+SHA256:\
DHE+aRSA+AES128+SHA384:\
DHE+aRSA+AES256+SHA256:\
DHE+aRSA+AES256+SHA384:\
ECDHE+aRSA+AES128+SHA256:\
ECDHE+aRSA+AES128+SHA384:\
ECDHE+aRSA+AES256+SHA256:\
ECDHE+aRSA+AES256+SHA384:\
!aNULL:!eNULL:!IDEA:!SEED:!SRP:!PSK:@STRENGTH\
",
);
}
}
Loading