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: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [mqtt5 0.32.2] - 2026-05-20

### Changed

- **Drop unmaintained `rustls-pemfile` dependency** - migrated all PEM parsing to `rustls-pki-types::pem::PemObject` (already implemented by `CertificateDer` and `PrivateKeyDer`). Closes [RUSTSEC-2025-0134](https://rustsec.org/advisories/RUSTSEC-2025-0134.html). As a side effect, private-key loading now accepts PKCS#8, PKCS#1 (RSA), and SEC1 (EC) sections in one pass instead of three sequential format probes, and respects file order when multiple key types are present (issue #82, PR #83).

## [mqtt5 0.32.1] - 2026-05-17

### Fixed
Expand Down
3 changes: 1 addition & 2 deletions crates/mqtt5/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "mqtt5"
version = "0.32.1"
version = "0.32.2"
edition.workspace = true
rust-version.workspace = true
authors.workspace = true
Expand Down Expand Up @@ -144,7 +144,6 @@ http = "1.4.0"
humantime-serde = "1.1"
ring = "0.17.14"
rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"] }
rustls-pemfile = "2.2"
rustls-pki-types = "1.13.0"
tokio = { version = "1.47", features = ["full"] }
tokio-rustls = { version = "0.26", default-features = false, features = ["ring", "tls12"] }
Expand Down
31 changes: 4 additions & 27 deletions crates/mqtt5/src/broker/tls_acceptor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
//! allowing secure connections on a dedicated TLS port (typically 8883).

use crate::error::{MqttError, Result};
use rustls::pki_types::pem::PemObject;
use rustls::pki_types::{CertificateDer, PrivateKeyDer};
use rustls::server::WebPkiClientVerifier;
use rustls::{RootCertStore, ServerConfig};
Expand Down Expand Up @@ -53,7 +54,7 @@ impl TlsAcceptorConfig {
path: impl AsRef<Path>,
) -> Result<Vec<CertificateDer<'static>>> {
let cert_pem = tokio::fs::read(path.as_ref()).await?;
let certs: Vec<CertificateDer<'static>> = rustls_pemfile::certs(&mut &cert_pem[..])
let certs: Vec<CertificateDer<'static>> = CertificateDer::pem_slice_iter(&cert_pem)
.filter_map(std::result::Result::ok)
.collect();

Expand All @@ -75,32 +76,8 @@ impl TlsAcceptorConfig {
path: impl AsRef<Path>,
) -> Result<PrivateKeyDer<'static>> {
let key_pem = tokio::fs::read(path.as_ref()).await?;

// Try PKCS#8 format first
let mut keys: Vec<PrivateKeyDer<'static>> =
rustls_pemfile::pkcs8_private_keys(&mut &key_pem[..])
.filter_map(std::result::Result::ok)
.map(PrivateKeyDer::from)
.collect();

// Try RSA format if PKCS#8 didn't work
if keys.is_empty() {
keys = rustls_pemfile::rsa_private_keys(&mut &key_pem[..])
.filter_map(std::result::Result::ok)
.map(PrivateKeyDer::from)
.collect();
}

// Try EC format if RSA didn't work
if keys.is_empty() {
keys = rustls_pemfile::ec_private_keys(&mut &key_pem[..])
.filter_map(std::result::Result::ok)
.map(PrivateKeyDer::from)
.collect();
}

keys.into_iter()
.next()
PrivateKeyDer::pem_slice_iter(&key_pem)
.find_map(std::result::Result::ok)
.ok_or_else(|| MqttError::Configuration("No private keys found in file".to_string()))
}

Expand Down
76 changes: 15 additions & 61 deletions crates/mqtt5/src/transport/tls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use crate::crypto::NoVerification;
use crate::error::{MqttError, Result};
use crate::time::Duration;
use crate::Transport;
use rustls::pki_types::pem::PemObject;
use rustls::pki_types::{CertificateDer, PrivateKeyDer, ServerName};
use rustls::{ClientConfig, RootCertStore};
use std::net::SocketAddr;
Expand Down Expand Up @@ -175,13 +176,9 @@ impl TlsConfig {
/// # Errors
///
/// Returns an error if the file cannot be read or parsed
///
/// # Errors
///
/// Returns an error if the operation fails
pub fn load_client_cert_pem(&mut self, cert_path: &str) -> Result<()> {
let cert_pem = std::fs::read(cert_path)?;
let certs: Vec<CertificateDer<'static>> = rustls_pemfile::certs(&mut &cert_pem[..])
let certs: Vec<CertificateDer<'static>> = CertificateDer::pem_slice_iter(&cert_pem)
.filter_map(std::result::Result::ok)
.collect();
if certs.is_empty() {
Expand All @@ -198,34 +195,12 @@ impl TlsConfig {
/// # Errors
///
/// Returns an error if the file cannot be read or parsed
///
/// # Errors
///
/// Returns an error if the operation fails
pub fn load_client_key_pem(&mut self, key_path: &str) -> Result<()> {
let key_pem = std::fs::read(key_path)?;
let mut keys: Vec<PrivateKeyDer<'static>> =
rustls_pemfile::pkcs8_private_keys(&mut &key_pem[..])
.filter_map(std::result::Result::ok)
.map(PrivateKeyDer::from)
.collect();

if keys.is_empty() {
// Try RSA keys if PKCS8 didn't work
keys = rustls_pemfile::rsa_private_keys(&mut &key_pem[..])
.filter_map(std::result::Result::ok)
.map(PrivateKeyDer::from)
.collect();
}

if keys.is_empty() {
return Err(MqttError::ProtocolError(
"No private keys found in file".to_string(),
));
}
self.client_key = Some(keys.into_iter().next().ok_or_else(|| {
MqttError::ProtocolError("Keys vector unexpectedly empty".to_string())
})?);
let key = PrivateKeyDer::pem_slice_iter(&key_pem)
.find_map(std::result::Result::ok)
.ok_or_else(|| MqttError::ProtocolError("No private keys found in file".to_string()))?;
self.client_key = Some(key);
Ok(())
}

Expand All @@ -234,13 +209,9 @@ impl TlsConfig {
/// # Errors
///
/// Returns an error if the file cannot be read or parsed
///
/// # Errors
///
/// Returns an error if the operation fails
pub fn load_ca_cert_pem(&mut self, ca_path: &str) -> Result<()> {
let ca_pem = std::fs::read(ca_path)?;
let ca_certs: Vec<CertificateDer<'static>> = rustls_pemfile::certs(&mut &ca_pem[..])
let ca_certs: Vec<CertificateDer<'static>> = CertificateDer::pem_slice_iter(&ca_pem)
.filter_map(std::result::Result::ok)
.collect();
if ca_certs.is_empty() {
Expand All @@ -258,7 +229,7 @@ impl TlsConfig {
///
/// Returns an error if the bytes cannot be parsed as PEM certificate
pub fn load_client_cert_pem_bytes(&mut self, cert_pem: &[u8]) -> Result<()> {
let certs: Vec<CertificateDer<'static>> = rustls_pemfile::certs(&mut &cert_pem[..])
let certs: Vec<CertificateDer<'static>> = CertificateDer::pem_slice_iter(cert_pem)
.filter_map(std::result::Result::ok)
.collect();
if certs.is_empty() {
Expand All @@ -276,28 +247,12 @@ impl TlsConfig {
///
/// Returns an error if the bytes cannot be parsed as PEM private key
pub fn load_client_key_pem_bytes(&mut self, key_pem: &[u8]) -> Result<()> {
let mut keys: Vec<PrivateKeyDer<'static>> =
rustls_pemfile::pkcs8_private_keys(&mut &key_pem[..])
.filter_map(std::result::Result::ok)
.map(PrivateKeyDer::from)
.collect();

if keys.is_empty() {
// Try RSA keys if PKCS8 didn't work
keys = rustls_pemfile::rsa_private_keys(&mut &key_pem[..])
.filter_map(std::result::Result::ok)
.map(PrivateKeyDer::from)
.collect();
}

if keys.is_empty() {
return Err(MqttError::ProtocolError(
"No private keys found in PEM bytes".to_string(),
));
}
self.client_key = Some(keys.into_iter().next().ok_or_else(|| {
MqttError::ProtocolError("Keys vector unexpectedly empty".to_string())
})?);
let key = PrivateKeyDer::pem_slice_iter(key_pem)
.find_map(std::result::Result::ok)
.ok_or_else(|| {
MqttError::ProtocolError("No private keys found in PEM bytes".to_string())
})?;
self.client_key = Some(key);
Ok(())
}

Expand All @@ -307,7 +262,7 @@ impl TlsConfig {
///
/// Returns an error if the bytes cannot be parsed as PEM certificate
pub fn load_ca_cert_pem_bytes(&mut self, ca_pem: &[u8]) -> Result<()> {
let ca_certs: Vec<CertificateDer<'static>> = rustls_pemfile::certs(&mut &ca_pem[..])
let ca_certs: Vec<CertificateDer<'static>> = CertificateDer::pem_slice_iter(ca_pem)
.filter_map(std::result::Result::ok)
.collect();
if ca_certs.is_empty() {
Expand Down Expand Up @@ -336,7 +291,6 @@ impl TlsConfig {
///
/// Returns an error if the bytes are not valid DER-encoded private key
pub fn load_client_key_der_bytes(&mut self, key_der: &[u8]) -> Result<()> {
// Assume PKCS#8 format for DER keys - this is the most common format
use rustls::pki_types::PrivatePkcs8KeyDer;
let key = PrivateKeyDer::from(PrivatePkcs8KeyDer::from(key_der.to_vec()));
self.client_key = Some(key);
Expand Down
5 changes: 3 additions & 2 deletions crates/mqtt5/tests/wss_alpn_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ use common::{find_workspace_root, TestBroker};
use mqtt5::broker::config::{
BrokerConfig, StorageBackend, StorageConfig, TlsConfig as BrokerTlsConfig, WebSocketConfig,
};
use rustls::pki_types::ServerName;
use rustls::pki_types::pem::PemObject;
use rustls::pki_types::{CertificateDer, ServerName};
use std::net::SocketAddr;
use std::sync::atomic::{AtomicU16, Ordering};
use std::sync::Arc;
Expand Down Expand Up @@ -48,7 +49,7 @@ fn build_tls_connector(alpn_protocols: Vec<Vec<u8>>) -> tokio_rustls::TlsConnect
let mut root_store = rustls::RootCertStore::empty();
let workspace_root = find_workspace_root();
let ca_pem = std::fs::read(workspace_root.join("test_certs/ca.pem")).unwrap();
let ca_certs: Vec<_> = rustls_pemfile::certs(&mut &ca_pem[..])
let ca_certs: Vec<CertificateDer<'static>> = CertificateDer::pem_slice_iter(&ca_pem)
.filter_map(std::result::Result::ok)
.collect();
for cert in ca_certs {
Expand Down
Loading