Skip to content
Merged
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
6 changes: 4 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -616,8 +616,10 @@ jobs:
- name: Run make for rust
run: make -C lib/rs

- name: Run make check for rust
run: make -C lib/rs check
- name: Run make checks for rust
run: |
make -C lib/rs check
make -C lib/rs check-rustls

- name: Run make test for rust
run: make -C lib/rs/test check
Expand Down
13 changes: 8 additions & 5 deletions doc/thrift-threat-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -1308,10 +1308,13 @@ The claim is correct for Go: validation is on by default, and the system store i
InsecureSkipVerify: true in their *tls.Config, which is outside Thrift's control.

---
Rust — No TLS support
Rust — Validation is controlled by the supplied rustls configuration

The Rust library (lib/rs/src/transport/socket.rs) implements only plain TCP via TcpStream. There is no TSSLSocket type, no TLS transport, and no SSL dependency. The claim is
inapplicable.
When the optional `rustls` feature is enabled, `TTlsClientChannel` requires the caller to supply an `Arc<ClientConfig>` and a `ServerName`. `connect()` completes the TLS
handshake according to that configuration before returning. Chain validation and server-name verification are controlled by the configuration's verifier. `TTlsServerChannel`
and `TServer::listen_tls()` similarly require an `Arc<ServerConfig>`; client-certificate authentication is whatever that configuration selects. Thrift does not construct a
default configuration or automatically load system roots, so there is no implicit trust-store fallback. Applications may also supply rustls custom verifiers; their behavior is
outside Thrift's control.

---
Summary table
Expand All @@ -1333,7 +1336,7 @@ Summary table
│ Go │ crypto/tls enforces (default │ System root CA pool if RootCAs=nil │ Yes (system pool) │ Caller can bypass by setting InsecureSkipVerify: │
│ │ InsecureSkipVerify=false) │ │ │ true │
├─────────┼─────────────────────────────────────────┼───────────────────────────────────────────┼────────────────────┼─────────────────────────────────────────────────────┤
│ Rust │ N/A │ N/A │ N/A │ No TLS transport exists
│ Rust │ Caller-configured rustls verifier │ Caller-supplied ClientConfig │ No No default config or implicit trust-store loading
└─────────┴─────────────────────────────────────────┴───────────────────────────────────────────┴────────────────────┴─────────────────────────────────────────────────────┘
```

Expand Down Expand Up @@ -1503,7 +1506,7 @@ not recommended. THRIFT-5926 (crash on None initial DIGEST-MD5 response) is a co
mechanism removal. If the crash is reachable pre-authentication, it qualifies as a remotely-triggerable DoS and will be treated accordingly.

**Q42.**
Thrift sets no project-wide TLS version floor. Each binding delegates version negotiation to its underlying TLS library (OpenSSL, JSSE, crypto/tls, etc.).
Thrift sets no project-wide TLS version floor. Each binding delegates version negotiation to its underlying TLS library (OpenSSL, JSSE, crypto/tls, rustls, etc.).
THRIFT-5743/5876 added TLS 1.3 capability where it was absent. Operators are responsible for configuring a version floor in the underlying library; TLS 1.2
minimum is recommended. A finding that TLS 1.0 or 1.1 is negotiable is a deployment or library-configuration concern, not a Thrift vulnerability. (Note: verify
whether the C++ binding explicitly disables SSLv2/v3 via SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3; if so, state that floor explicitly for the C++ binding.)
Expand Down
3 changes: 3 additions & 0 deletions lib/rs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,13 @@ uuid = "1"
log = {version = "0.4", optional = true}
ordered-float = "3.0"
threadpool = {version = "1.7", optional = true}
rustls = { version = "0.23.42", default-features = false, features = ["std", "tls12"], optional = true }

[features]
default = ["server"]
server = ["threadpool", "log"]
rustls = ["dep:rustls"]

[dev-dependencies]
uuid = { version = "1", features = ["v4"] }
rustls = { version = "0.23.42", default-features = false, features = ["ring", "std", "tls12"] }
8 changes: 7 additions & 1 deletion lib/rs/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ all-local:
$(CARGO) clippy --all -- -D warnings
$(CARGO) build

.PHONY: check-rustls
check-rustls:
$(CARGO) clippy --all --all-features -- -D warnings
$(CARGO) check --no-default-features --features rustls
$(CARGO) test --all-features

clean-local:
$(CARGO) clean
-$(RM) Cargo.lock
Expand All @@ -50,11 +56,11 @@ distdir:

EXTRA_DIST = \
src \
tests \
Cargo.toml \
README.md \
release.sh \
test/fuzz/.gitignore \
NOTICE \
LICENSE \
RELEASING.md

23 changes: 21 additions & 2 deletions lib/rs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,33 @@ types and services by writing their own code on top.
Add `thrift = "x.y.z"` to your `Cargo.toml`, where `x.y.z` is the version of the
Thrift compiler you're using.

### TLS

TLS client and server channels are available through the optional `rustls`
feature:

```toml
[dependencies]
thrift = { version = "x.y.z", features = ["rustls"] }
rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"] }
```

Applications construct a rustls `ClientConfig` or `ServerConfig` and pass it to
`TTlsClientChannel::connect` or `TServer::listen_tls`. Thrift does not choose a
crypto provider, load a system trust store, or read certificate files. This
keeps trust anchors, client authentication, protocol versions, and certificate
selection under application control.

## API Documentation

Full [Rustdoc](https://docs.rs/thrift/)

## Compatibility

The Rust library and auto-generated code targets Rust versions 1.28+.
It does not currently use any Rust 2021 features.
The Rust library and auto-generated code target Rust versions 1.65+. The
minimum was raised from Rust 1.28 by THRIFT-5158, when the library and generator
moved to Rust 2021-only output. Enabling the optional `rustls` feature requires
Rust 1.71+.

### Breaking Changes

Expand Down
40 changes: 40 additions & 0 deletions lib/rs/src/server/threaded.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ use std::net::{TcpListener, ToSocketAddrs};
use std::sync::Arc;
use threadpool::ThreadPool;

#[cfg(feature = "rustls")]
use rustls::ServerConfig;

#[cfg(unix)]
use std::os::unix::net::UnixListener;
#[cfg(unix)]
Expand All @@ -29,6 +32,8 @@ use std::path::Path;
use crate::protocol::{
TInputProtocol, TInputProtocolFactory, TOutputProtocol, TOutputProtocolFactory,
};
#[cfg(feature = "rustls")]
use crate::transport::TTlsServerChannel;
use crate::transport::{TIoChannel, TReadTransportFactory, TTcpChannel, TWriteTransportFactory};
use crate::{ApplicationError, ApplicationErrorKind};

Expand Down Expand Up @@ -199,6 +204,41 @@ where
}))
}

/// Listen for incoming TLS connections on `listen_address`.
///
/// `config` controls certificate selection, client authentication, crypto
/// provider, and protocol policy. Accepted connections are handed to a
/// worker before the TLS handshake begins, so the accept loop does not
/// block on a peer that connects without completing a handshake.
#[cfg(feature = "rustls")]
pub fn listen_tls<A: ToSocketAddrs>(
&mut self,
listen_address: A,
config: Arc<ServerConfig>,
) -> crate::Result<()> {
let listener = TcpListener::bind(listen_address)?;
for stream in listener.incoming() {
match stream {
Ok(stream) => {
stream.set_nodelay(true).ok();
let channel = TTlsServerChannel::with_stream(stream, Arc::clone(&config))?;
self.handle_stream(channel)?;
}
Err(error) => {
warn!(
"failed to accept remote TLS connection with error {:?}",
error
);
}
}
}

Err(crate::Error::Application(ApplicationError {
kind: ApplicationErrorKind::Unknown,
message: "aborted TLS listen loop".into(),
}))
}

/// Listen for incoming connections on `listen_path`.
///
/// `listen_path` should implement `AsRef<Path>` trait.
Expand Down
6 changes: 6 additions & 0 deletions lib/rs/src/transport/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,10 @@ macro_rules! assert_eq_transport_written_bytes {
mod buffered;
mod framed;
mod mem;
mod shared;
mod socket;
#[cfg(feature = "rustls")]
mod tls;

pub use self::buffered::{
TBufferedReadTransport, TBufferedReadTransportFactory, TBufferedWriteTransport,
Expand All @@ -55,7 +58,10 @@ pub use self::framed::{
TFramedWriteTransportFactory,
};
pub use self::mem::TBufferChannel;
pub use self::shared::TSharedChannel;
pub use self::socket::TTcpChannel;
#[cfg(feature = "rustls")]
pub use self::tls::{TTlsClientChannel, TTlsServerChannel};

/// Identifies a transport used by a `TInputProtocol` to receive bytes.
pub trait TReadTransport: Read {}
Expand Down
88 changes: 88 additions & 0 deletions lib/rs/src/transport/shared.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

use std::io::{self, Read, Write};
use std::sync::{Arc, Mutex, MutexGuard};

use super::{ReadHalf, TIoChannel, WriteHalf};

/// A cloneable channel that serializes access to an underlying I/O stream.
///
/// This adapter allows a bidirectional stream that cannot be cloned, such as a
/// TLS session, to implement [`TIoChannel`]. Every read, write, and flush holds
/// the same lock for the duration of that operation. It is intended for
/// synchronous request-response traffic, where a caller writes and flushes a
/// complete request before reading its response.
///
/// The shared lock makes access memory-safe across threads, but it does not
/// provide full-duplex progress: a blocking read holds the lock and prevents a
/// concurrent write until that read finishes.
#[derive(Debug)]
pub struct TSharedChannel<C> {
inner: Arc<Mutex<C>>,
}

impl<C> TSharedChannel<C> {
/// Wrap `inner` in a shared channel.
pub fn new(inner: C) -> Self {
Self {
inner: Arc::new(Mutex::new(inner)),
}
}

// io::Error::other requires Rust 1.74.
#[allow(unknown_lints)]
#[allow(clippy::io_other_error)]
pub(crate) fn lock(&self) -> io::Result<MutexGuard<'_, C>> {
self.inner
.lock()
.map_err(|_| io::Error::new(io::ErrorKind::Other, "shared channel lock is poisoned"))
}
}

impl<C> Clone for TSharedChannel<C> {
fn clone(&self) -> Self {
Self {
inner: Arc::clone(&self.inner),
}
}
}

impl<C: Read> Read for TSharedChannel<C> {
fn read(&mut self, buffer: &mut [u8]) -> io::Result<usize> {
self.lock()?.read(buffer)
}
}

impl<C: Write> Write for TSharedChannel<C> {
fn write(&mut self, buffer: &[u8]) -> io::Result<usize> {
self.lock()?.write(buffer)
}

fn flush(&mut self) -> io::Result<()> {
self.lock()?.flush()
}
}

impl<C: Read + Write> TIoChannel for TSharedChannel<C> {
fn split(self) -> crate::Result<(ReadHalf<Self>, WriteHalf<Self>)>
where
Self: Sized,
{
Ok((ReadHalf::new(self.clone()), WriteHalf::new(self)))
}
}
Loading
Loading