Skip to content

Commit 0a82863

Browse files
committed
openssl: add from_tls_config constructor
Adds a convenience constructor that creates a `MakeTlsConnector` that matches the semantics of Postgres clients. Check out https://www.postgresql.org/docs/current/libpq-ssl.html for more details on the expected behavior.
1 parent 609d0f1 commit 0a82863

File tree

3 files changed

+191
-5
lines changed

3 files changed

+191
-5
lines changed

postgres-openssl/src/lib.rs

Lines changed: 69 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,30 +43,41 @@
4343

4444
#[cfg(feature = "runtime")]
4545
use openssl::error::ErrorStack;
46-
use openssl::hash::MessageDigest;
47-
use openssl::nid::Nid;
4846
#[cfg(feature = "runtime")]
4947
use openssl::ssl::SslConnector;
50-
use openssl::ssl::{self, ConnectConfiguration, SslRef};
48+
use openssl::ssl::{self, ConnectConfiguration, SslFiletype, SslRef};
5149
use openssl::x509::X509VerifyResult;
52-
use std::error::Error;
50+
use openssl::{hash::MessageDigest, ssl::SslMethod};
51+
use openssl::{nid::Nid, ssl::SslVerifyMode};
5352
use std::fmt::{self, Debug};
5453
use std::future::Future;
5554
use std::io;
5655
use std::pin::Pin;
5756
#[cfg(feature = "runtime")]
5857
use std::sync::Arc;
5958
use std::task::{Context, Poll};
59+
use std::{error::Error, path::PathBuf};
6060
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
6161
use tokio_openssl::SslStream;
62-
use tokio_postgres::tls;
6362
#[cfg(feature = "runtime")]
6463
use tokio_postgres::tls::MakeTlsConnect;
6564
use tokio_postgres::tls::{ChannelBinding, TlsConnect};
65+
use tokio_postgres::{config::SslMode, tls};
6666

6767
#[cfg(test)]
6868
mod test;
6969

70+
/// TLS configuration.
71+
#[cfg(feature = "runtime")]
72+
pub struct TlsConfig {
73+
/// SSL mode (`sslmode`).
74+
pub mode: SslMode,
75+
/// Location of the client cert and key (`sslcert`, `sslkey`).
76+
pub client_cert: Option<(PathBuf, PathBuf)>,
77+
/// Location of the root certificate (`sslrootcert`).
78+
pub root_cert: Option<PathBuf>,
79+
}
80+
7081
/// A `MakeTlsConnect` implementation using the `openssl` crate.
7182
///
7283
/// Requires the `runtime` Cargo feature (enabled by default).
@@ -87,6 +98,59 @@ impl MakeTlsConnector {
8798
}
8899
}
89100

101+
/// Creates a new connector from the provided [`TlsConfig`].
102+
///
103+
/// The returned [`MakeTlsConnector`] will be configured to mimick libpq-ssl behavior.
104+
pub fn from_tls_config(tls_config: TlsConfig) -> Result<MakeTlsConnector, ErrorStack> {
105+
let mut builder = SslConnector::builder(SslMethod::tls_client())?;
106+
// The mode dictates whether we verify peer certs and hostnames. By default, Postgres is
107+
// pretty relaxed and recommends SslMode::VerifyCa or SslMode::VerifyFull for security.
108+
//
109+
// For more details, check out Table 33.1. SSL Mode Descriptions in
110+
// https://postgresql.org/docs/current/libpq-ssl.html#LIBPQ-SSL-PROTECTION.
111+
let (verify_mode, verify_hostname) = match tls_config.mode {
112+
SslMode::Disable | SslMode::Prefer => (SslVerifyMode::NONE, false),
113+
SslMode::Require => match tls_config.root_cert {
114+
// If a root CA file exists, the behavior of sslmode=require will be the same as
115+
// that of verify-ca, meaning the server certificate is validated against the CA.
116+
//
117+
// For more details, check out the note about backwards compatibility in
118+
// https://postgresql.org/docs/current/libpq-ssl.html#LIBQ-SSL-CERTIFICATES.
119+
Some(_) => (SslVerifyMode::PEER, false),
120+
None => (SslVerifyMode::NONE, false),
121+
},
122+
SslMode::VerifyCa => (SslVerifyMode::PEER, false),
123+
SslMode::VerifyFull => (SslVerifyMode::PEER, true),
124+
_ => panic!("unexpected sslmode {:?}", tls_config.mode),
125+
};
126+
127+
// Configure peer verification
128+
builder.set_verify(verify_mode);
129+
130+
// Configure certificates
131+
if tls_config.client_cert.is_some() {
132+
let (cert, key) = tls_config.client_cert.unwrap();
133+
builder.set_certificate_file(cert, SslFiletype::PEM)?;
134+
builder.set_private_key_file(key, SslFiletype::PEM)?;
135+
}
136+
if tls_config.root_cert.is_some() {
137+
builder.set_ca_file(tls_config.root_cert.unwrap())?;
138+
}
139+
140+
let mut tls_connector = MakeTlsConnector::new(builder.build());
141+
142+
// Configure hostname verification
143+
match (verify_mode, verify_hostname) {
144+
(SslVerifyMode::PEER, false) => tls_connector.set_callback(|connect, _| {
145+
connect.set_verify_hostname(false);
146+
Ok(())
147+
}),
148+
_ => {}
149+
}
150+
151+
Ok(tls_connector)
152+
}
153+
90154
/// Sets a callback used to apply per-connection configuration.
91155
///
92156
/// The the callback is provided the domain name along with the `ConnectConfiguration`.

postgres-openssl/src/test.rs

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,15 @@ where
2525
assert_eq!(rows[0].get::<_, i32>(0), 1);
2626
}
2727

28+
async fn from_tls_config_smoke_test(config: TlsConfig) {
29+
let mut connector = MakeTlsConnector::from_tls_config(config).unwrap();
30+
smoke_test(
31+
"user=ssl_user dbname=postgres",
32+
MakeTlsConnect::<TcpStream>::make_tls_connect(&mut connector, "localhost").unwrap(),
33+
)
34+
.await
35+
}
36+
2837
#[tokio::test]
2938
async fn require() {
3039
let mut builder = SslConnector::builder(SslMethod::tls()).unwrap();
@@ -109,3 +118,97 @@ async fn runtime() {
109118
assert_eq!(rows.len(), 1);
110119
assert_eq!(rows[0].get::<_, i32>(0), 1);
111120
}
121+
122+
#[tokio::test]
123+
async fn from_tls_config_base() {
124+
from_tls_config_smoke_test(TlsConfig {
125+
mode: SslMode::Disable,
126+
client_cert: None,
127+
root_cert: None,
128+
})
129+
.await;
130+
131+
from_tls_config_smoke_test(TlsConfig {
132+
mode: SslMode::Prefer,
133+
client_cert: None,
134+
root_cert: None,
135+
})
136+
.await;
137+
138+
from_tls_config_smoke_test(TlsConfig {
139+
mode: SslMode::Require,
140+
client_cert: None,
141+
root_cert: None,
142+
})
143+
.await;
144+
145+
from_tls_config_smoke_test(TlsConfig {
146+
mode: SslMode::Require,
147+
client_cert: None,
148+
root_cert: Some(PathBuf::from("../test/server.crt")),
149+
})
150+
.await;
151+
152+
from_tls_config_smoke_test(TlsConfig {
153+
mode: SslMode::VerifyCa,
154+
client_cert: None,
155+
root_cert: Some(PathBuf::from("../test/server.crt")),
156+
})
157+
.await;
158+
159+
from_tls_config_smoke_test(TlsConfig {
160+
mode: SslMode::VerifyFull,
161+
client_cert: None,
162+
root_cert: Some(PathBuf::from("../test/server.crt")),
163+
})
164+
.await;
165+
}
166+
167+
#[tokio::test]
168+
#[should_panic(expected = "certificate verify failed")]
169+
async fn from_tls_config_require_with_wrong_root_cert_err() {
170+
from_tls_config_smoke_test(TlsConfig {
171+
mode: SslMode::Require,
172+
client_cert: None,
173+
root_cert: Some(PathBuf::from("../test/other.crt")),
174+
})
175+
.await;
176+
}
177+
178+
#[tokio::test]
179+
#[should_panic(expected = "certificate verify failed")]
180+
async fn from_tls_config_verify_ca_with_wrong_root_cert_err() {
181+
from_tls_config_smoke_test(TlsConfig {
182+
mode: SslMode::VerifyCa,
183+
client_cert: None,
184+
root_cert: Some(PathBuf::from("../test/other.crt")),
185+
})
186+
.await;
187+
}
188+
189+
#[tokio::test]
190+
#[should_panic(expected = "certificate verify failed")]
191+
async fn from_tls_config_verify_full_with_wrong_root_cert_err() {
192+
from_tls_config_smoke_test(TlsConfig {
193+
mode: SslMode::VerifyFull,
194+
client_cert: None,
195+
root_cert: Some(PathBuf::from("../test/other.crt")),
196+
})
197+
.await;
198+
}
199+
200+
#[tokio::test]
201+
#[should_panic(expected = "Hostname mismatch")]
202+
async fn from_tls_config_verify_full_with_wrong_hostname_err() {
203+
let tls_config = TlsConfig {
204+
mode: SslMode::VerifyFull,
205+
client_cert: None,
206+
root_cert: Some(PathBuf::from("../test/server.crt")),
207+
};
208+
let mut connector = MakeTlsConnector::from_tls_config(tls_config).unwrap();
209+
smoke_test(
210+
"user=ssl_user dbname=postgres",
211+
MakeTlsConnect::<TcpStream>::make_tls_connect(&mut connector, "otherhost").unwrap(),
212+
)
213+
.await
214+
}

test/other.crt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIDFDCCAfygAwIBAgIJAMpak9jpV15CMA0GCSqGSIb3DQEBBQUAMA8xDTALBgNV
3+
BAMMBHJvb3QwHhcNMjEwNTEyMDg1NDEyWhcNMzEwNTEwMDg1NDEyWjAPMQ0wCwYD
4+
VQQDDARyb290MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyuPNlaHj
5+
ssg2sQQ0CmbMl9KocZufjAmXdW7GCDvwXGRFSw+HPwKbcOPmmI96/JEO0jf9lExh
6+
pLoMJsi6IuwL1IpXOkwLmZbiNMaJxC7SbQ/J6f3EdVfdKA15YfgCjHZBVomK+nSy
7+
S0AERKypPNO+tv23wtpM7YmUw8O0e08Aqi7PLENyT4McBF6sSSV94OP+o4GOQ2rT
8+
TMM+lrze1t+YQjhwOFjF3sKTrmE9J9F5R5/eN1syPlS2xLyN+bmWaByK64myNfh3
9+
fRcjpiVfhkbePkuitJBo5MfUHgn90q2Y1OtP/7UhsJX5XUgmRFY1iGiIMkB2jmEr
10+
6Ugy8rFuYMuIkwIDAQABo3MwcTAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTL
11+
5f6++2fV0v/Y3WJYo4zkEbv7DzA/BgNVHSMEODA2gBTL5f6++2fV0v/Y3WJYo4zk
12+
Ebv7D6ETpBEwDzENMAsGA1UEAwwEcm9vdIIJAMpak9jpV15CMA0GCSqGSIb3DQEB
13+
BQUAA4IBAQAmOb/aXndz63uk6eSrwkwwM4oAeie333kosG3Bpzo5/EdNEvsZRLs9
14+
xhZRYCoyW/pBOcPs7FAI90G/1OuhoQk8MkvjS5MukZmcNj2Yrew6/1WedfHwwIYV
15+
q9Wt6WaXRNu/pMn2IHrNrBaC1utmHIF5Yj2njVDIUGWbe3xBUl90dxHtt11830fc
16+
N6rqSZiBRMwK8Il2z31VPCCbURX/9BI1TUfgZbOyvEDA43QEUTIDfxTbkZKUC7KF
17+
7ClcHl7Py4kqlPkpkyWKBrnokMNRDKGlSDDYy1+hDO/xObx2U6Z1x/ksv/+G05fI
18+
hfrnzZaanvotLMZf2Fut1Ee5BjsnZE3Y
19+
-----END CERTIFICATE-----

0 commit comments

Comments
 (0)