Skip to content

Commit 3002b4d

Browse files
authored
Strict SSL verification (#9)
Fixes #8
1 parent f22867d commit 3002b4d

File tree

11 files changed

+171
-75
lines changed

11 files changed

+171
-75
lines changed

Cargo.lock

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,5 +85,9 @@ fn main() -> nut_client::Result<()> {
8585
You can turn on SSL support by adding `.with_ssl(true)` in the `ConfigBuilder`. This requires the `ssl` feature, which
8686
uses `rustls` under the hood.
8787

88-
Note that this crate turns off all certificate validation at the moment, effectively giving a false sense of security.
89-
If you'd like to contribute to this, see issue #8.
88+
Note that, by default, `.with_ssl(true)` will enable **strict** verification. This means it will verify the server
89+
certificate's DNS entries, check for revocation, and verify the chain using the local root trust. You must also ensure
90+
that the connection hostname is a valid DNS name (e.g. `localhost`, not `127.0.0.1`).
91+
92+
If the server is using a self-signed certificate, and you'd like to ignore the strict validation, you can add
93+
`.with_insecure_ssl(true)` along with `.with_ssl(true)`.

nut-client/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ license = "MIT"
1717
shell-words = "1.0.0"
1818
rustls = { version = "0.19", optional = true, features = ["dangerous_configuration"] }
1919
webpki = { version = "0.21", optional = true }
20+
webpki-roots = { version = "0.21", optional = true }
2021

2122
[features]
22-
ssl = ["rustls", "webpki"]
23+
ssl = ["rustls", "webpki", "webpki-roots"]

nut-client/examples/blocking.rs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,23 @@
11
use std::env;
2-
use std::net::ToSocketAddrs;
32

43
use nut_client::blocking::Connection;
5-
use nut_client::{Auth, ConfigBuilder, Host};
4+
use nut_client::{Auth, ConfigBuilder};
5+
use std::convert::TryInto;
66

77
fn main() -> nut_client::Result<()> {
8-
let addr = env::var("NUT_ADDR")
9-
.unwrap_or_else(|_| "localhost:3493".into())
10-
.to_socket_addrs()
11-
.unwrap()
12-
.next()
13-
.unwrap();
8+
let host = env::var("NUT_HOST").unwrap_or_else(|_| "localhost".into());
9+
let port = env::var("NUT_PORT")
10+
.ok()
11+
.map(|s| s.parse::<u16>().ok())
12+
.flatten()
13+
.unwrap_or(3493);
1414

1515
let username = env::var("NUT_USER").ok();
1616
let password = env::var("NUT_PASSWORD").ok();
1717
let auth = username.map(|username| Auth::new(username, password));
1818

1919
let config = ConfigBuilder::new()
20-
.with_host(Host::Tcp(addr))
20+
.with_host((host, port).try_into().unwrap_or_default())
2121
.with_auth(auth)
2222
.with_debug(false) // Turn this on for debugging network chatter
2323
.build();

nut-client/src/blocking/mod.rs

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,7 @@ impl Connection {
1717
/// Initializes a connection to a NUT server (upsd).
1818
pub fn new(config: &Config) -> crate::Result<Self> {
1919
match &config.host {
20-
Host::Tcp(socket_addr) => {
21-
Ok(Self::Tcp(TcpConnection::new(config.clone(), socket_addr)?))
22-
}
20+
Host::Tcp(host) => Ok(Self::Tcp(TcpConnection::new(config.clone(), &host.addr)?)),
2321
}
2422
}
2523

@@ -62,7 +60,7 @@ impl Connection {
6260
/// A blocking TCP NUT client connection.
6361
pub struct TcpConnection {
6462
config: Config,
65-
pipeline: ConnectionStream,
63+
stream: ConnectionStream,
6664
}
6765

6866
impl TcpConnection {
@@ -71,7 +69,7 @@ impl TcpConnection {
7169
let tcp_stream = TcpStream::connect_timeout(socket_addr, config.timeout)?;
7270
let mut connection = Self {
7371
config,
74-
pipeline: ConnectionStream::Plain(tcp_stream),
72+
stream: ConnectionStream::Plain(tcp_stream),
7573
};
7674

7775
// Initialize SSL connection
@@ -98,19 +96,37 @@ impl TcpConnection {
9896
})?
9997
.expect_ok()?;
10098

101-
let mut config = rustls::ClientConfig::new();
102-
config
103-
.dangerous()
104-
.set_certificate_verifier(std::sync::Arc::new(
105-
crate::ssl::NutCertificateValidator::new(&self.config),
106-
));
99+
let mut ssl_config = rustls::ClientConfig::new();
100+
let sess = if self.config.ssl_insecure {
101+
ssl_config
102+
.dangerous()
103+
.set_certificate_verifier(std::sync::Arc::new(
104+
crate::ssl::InsecureCertificateValidator::new(&self.config),
105+
));
106+
107+
let dns_name = webpki::DNSNameRef::try_from_ascii_str("www.google.com").unwrap();
108+
109+
rustls::ClientSession::new(&std::sync::Arc::new(ssl_config), dns_name)
110+
} else {
111+
// Try to get hostname as given (e.g. localhost can be used for strict SSL, but not 127.0.0.1)
112+
let hostname = self
113+
.config
114+
.host
115+
.hostname()
116+
.ok_or(ClientError::Nut(NutError::SslInvalidHostname))?;
117+
118+
let dns_name = webpki::DNSNameRef::try_from_ascii_str(&hostname)
119+
.map_err(|_| ClientError::Nut(NutError::SslInvalidHostname))?;
120+
121+
ssl_config
122+
.root_store
123+
.add_server_trust_anchors(&webpki_roots::TLS_SERVER_ROOTS);
107124

108-
// todo: this DNS name is temporary; should get from connection hostname? (#8)
109-
let dns_name = webpki::DNSNameRef::try_from_ascii_str("www.google.com").unwrap();
110-
let sess = rustls::ClientSession::new(&std::sync::Arc::new(config), dns_name);
125+
rustls::ClientSession::new(&std::sync::Arc::new(ssl_config), dns_name)
126+
};
111127

112128
// Wrap and override the TCP stream
113-
self.pipeline = self.pipeline.upgrade_ssl(sess)?;
129+
self.stream = self.stream.upgrade_ssl(sess)?;
114130

115131
// Send a test command
116132
self.get_network_version()?;
@@ -179,8 +195,8 @@ impl TcpConnection {
179195
if self.config.debug {
180196
eprint!("DEBUG -> {}", line);
181197
}
182-
self.pipeline.write_all(line.as_bytes())?;
183-
self.pipeline.flush()?;
198+
self.stream.write_all(line.as_bytes())?;
199+
self.stream.flush()?;
184200
Ok(())
185201
}
186202

@@ -203,19 +219,19 @@ impl TcpConnection {
203219
}
204220

205221
fn read_response(&mut self) -> crate::Result<Response> {
206-
let mut reader = BufReader::new(&mut self.pipeline);
222+
let mut reader = BufReader::new(&mut self.stream);
207223
let args = Self::parse_line(&mut reader, self.config.debug)?;
208224
Response::from_args(args)
209225
}
210226

211227
fn read_plain_response(&mut self) -> crate::Result<String> {
212-
let mut reader = BufReader::new(&mut self.pipeline);
228+
let mut reader = BufReader::new(&mut self.stream);
213229
let args = Self::parse_line(&mut reader, self.config.debug)?;
214230
Ok(args.join(" "))
215231
}
216232

217233
fn read_list(&mut self, query: &[&str]) -> crate::Result<Vec<Response>> {
218-
let mut reader = BufReader::new(&mut self.pipeline);
234+
let mut reader = BufReader::new(&mut self.stream);
219235
let args = Self::parse_line(&mut reader, self.config.debug)?;
220236

221237
Response::from_args(args)?.expect_begin_list(query)?;

nut-client/src/config.rs

Lines changed: 70 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,66 @@
11
use core::fmt;
2+
use std::convert::{TryFrom, TryInto};
23
use std::net::{SocketAddr, ToSocketAddrs};
34
use std::time::Duration;
45

6+
use crate::ClientError;
7+
58
/// A host specification.
69
#[derive(Clone, Debug)]
710
pub enum Host {
8-
/// A TCP hostname and port.
9-
Tcp(SocketAddr),
11+
/// A TCP hostname, and address (IP + port).
12+
Tcp(TcpHost),
1013
// TODO: Support Unix socket streams.
1114
}
1215

16+
impl Host {
17+
/// Returns the hostname as given, if any.
18+
pub fn hostname(&self) -> Option<String> {
19+
match self {
20+
Host::Tcp(host) => Some(host.hostname.to_owned()),
21+
// _ => None,
22+
}
23+
}
24+
}
25+
1326
impl Default for Host {
1427
fn default() -> Self {
15-
let addr = (String::from("127.0.0.1"), 3493)
16-
.to_socket_addrs()
17-
.expect("Failed to create local UPS socket address. This is a bug.")
18-
.next()
19-
.expect("Failed to create local UPS socket address. This is a bug.");
20-
Self::Tcp(addr)
28+
(String::from("localhost"), 3493)
29+
.try_into()
30+
.expect("Failed to parse local hostname; this is a bug.")
2131
}
2232
}
2333

2434
impl From<SocketAddr> for Host {
2535
fn from(addr: SocketAddr) -> Self {
26-
Self::Tcp(addr)
36+
let hostname = addr.ip().to_string();
37+
Self::Tcp(TcpHost { hostname, addr })
38+
}
39+
}
40+
41+
/// A TCP address, preserving the original DNS hostname if any.
42+
#[derive(Clone, Debug)]
43+
pub struct TcpHost {
44+
pub(crate) hostname: String,
45+
pub(crate) addr: SocketAddr,
46+
}
47+
48+
impl TryFrom<(String, u16)> for Host {
49+
type Error = ClientError;
50+
51+
fn try_from(hostname_port: (String, u16)) -> Result<Self, Self::Error> {
52+
let (hostname, _) = hostname_port.clone();
53+
let addr = hostname_port
54+
.to_socket_addrs()
55+
.map_err(ClientError::Io)?
56+
.next()
57+
.ok_or_else(|| {
58+
ClientError::Io(std::io::Error::new(
59+
std::io::ErrorKind::AddrNotAvailable,
60+
"no address given",
61+
))
62+
})?;
63+
Ok(Host::Tcp(TcpHost { hostname, addr }))
2764
}
2865
}
2966

@@ -59,17 +96,26 @@ pub struct Config {
5996
pub(crate) auth: Option<Auth>,
6097
pub(crate) timeout: Duration,
6198
pub(crate) ssl: bool,
99+
pub(crate) ssl_insecure: bool,
62100
pub(crate) debug: bool,
63101
}
64102

65103
impl Config {
66104
/// Creates a connection configuration.
67-
pub fn new(host: Host, auth: Option<Auth>, timeout: Duration, ssl: bool, debug: bool) -> Self {
105+
pub fn new(
106+
host: Host,
107+
auth: Option<Auth>,
108+
timeout: Duration,
109+
ssl: bool,
110+
ssl_insecure: bool,
111+
debug: bool,
112+
) -> Self {
68113
Config {
69114
host,
70115
auth,
71116
timeout,
72117
ssl,
118+
ssl_insecure,
73119
debug,
74120
}
75121
}
@@ -82,6 +128,7 @@ pub struct ConfigBuilder {
82128
auth: Option<Auth>,
83129
timeout: Option<Duration>,
84130
ssl: Option<bool>,
131+
ssl_insecure: Option<bool>,
85132
debug: Option<bool>,
86133
}
87134

@@ -111,12 +158,24 @@ impl ConfigBuilder {
111158
}
112159

113160
/// Enables SSL on the connection.
161+
///
162+
/// This will enable strict SSL verification (including hostname),
163+
/// unless `.with_insecure_ssl` is also set to `true`.
114164
#[cfg(feature = "ssl")]
115165
pub fn with_ssl(mut self, ssl: bool) -> Self {
116166
self.ssl = Some(ssl);
117167
self
118168
}
119169

170+
/// Turns off SSL verification.
171+
///
172+
/// Note: you must still use `.with_ssl(true)` to turn on SSL.
173+
#[cfg(feature = "ssl")]
174+
pub fn with_insecure_ssl(mut self, ssl_insecure: bool) -> Self {
175+
self.ssl_insecure = Some(ssl_insecure);
176+
self
177+
}
178+
120179
/// Enables debugging network calls by printing to stderr.
121180
pub fn with_debug(mut self, debug: bool) -> Self {
122181
self.debug = Some(debug);
@@ -130,6 +189,7 @@ impl ConfigBuilder {
130189
self.auth,
131190
self.timeout.unwrap_or_else(|| Duration::from_secs(5)),
132191
self.ssl.unwrap_or(false),
192+
self.ssl_insecure.unwrap_or(false),
133193
self.debug.unwrap_or(false),
134194
)
135195
}

nut-client/src/error.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ pub enum NutError {
1515
/// Occurs when attempting to use SSL in a transport that doesn't support it, or
1616
/// if the server is not configured for it.
1717
SslNotSupported,
18+
/// Occurs when trying to initialize a strict SSL connection with an invalid hostname.
19+
SslInvalidHostname,
1820
/// Occurs when the client used a feature that is disabled by the server.
1921
FeatureNotConfigured,
2022
/// Generic (usually internal) client error.
@@ -29,6 +31,10 @@ impl fmt::Display for NutError {
2931
Self::UnexpectedResponse => write!(f, "Unexpected server response content"),
3032
Self::UnknownResponseType(ty) => write!(f, "Unknown response type: {}", ty),
3133
Self::SslNotSupported => write!(f, "SSL not supported by server or transport"),
34+
Self::SslInvalidHostname => write!(
35+
f,
36+
"Given hostname cannot be used for a strict SSL connection"
37+
),
3238
Self::FeatureNotConfigured => write!(f, "Feature not configured by server"),
3339
Self::Generic(msg) => write!(f, "Internal client error: {}", msg),
3440
}

0 commit comments

Comments
 (0)