Skip to content

Commit 05dafb6

Browse files
authored
Merge pull request #3823 from rina23q/feature/3807/support-local-client-username-password-authentication
feat: Support username/password for local broker authentication
2 parents 2952453 + eb289af commit 05dafb6

File tree

13 files changed

+450
-154
lines changed

13 files changed

+450
-154
lines changed

crates/common/mqtt_channel/src/config.rs

Lines changed: 117 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@ use crate::MqttMessage;
22
use crate::TopicFilter;
33
use certificate::parse_root_certificate;
44
use certificate::CertificateError;
5-
use log::debug;
65
use rumqttc::tokio_rustls::rustls;
76
use rumqttc::tokio_rustls::rustls::pki_types::CertificateDer;
87
use rumqttc::LastWill;
98
use std::fmt::Debug;
109
use std::fmt::Formatter;
10+
use std::fs::File;
11+
use std::io::BufRead;
12+
use std::io::BufReader;
1113
use std::ops::Deref;
1214
use std::path::Path;
1315
use std::sync::Arc;
@@ -74,7 +76,7 @@ pub struct BrokerConfig {
7476
/// Default: 1883
7577
pub port: u16,
7678

77-
/// Certificate authentication configuration
79+
/// Authentication configuration
7880
pub authentication: Option<AuthenticationConfig>,
7981
}
8082

@@ -88,35 +90,114 @@ pub struct BrokerConfig {
8890
/// 3. server and client authentication - clients will verify MQTT broker
8991
/// certificate and broker will verify client certificates
9092
///
93+
/// In addition, supporting username/password authentication with any combinations.
94+
///
9195
/// [mosquitto]: https://mosquitto.org/man/mosquitto-conf-5.html#authentication
9296
#[derive(Debug, Clone)]
9397
pub struct AuthenticationConfig {
9498
/// Trusted root certificate store used to verify broker certificate
9599
cert_store: rustls::RootCertStore,
96100

97-
/// Client authentication configuration
98-
client_auth: Option<ClientAuthConfig>,
101+
/// Client certificate and key
102+
cert_config: Option<ClientAuthCertConfig>,
103+
104+
/// Client username
105+
username: Option<String>,
106+
107+
/// Client password: it can be set only when username is set due to the MQTT specification.
108+
/// Therefore, the value can be read only via API.
109+
password: Option<Zeroizing<String>>,
99110
}
100111

101112
impl Default for AuthenticationConfig {
102113
fn default() -> Self {
103114
AuthenticationConfig {
104115
cert_store: rustls::RootCertStore::empty(),
105-
client_auth: None,
116+
cert_config: None,
117+
username: None,
118+
password: None,
119+
}
120+
}
121+
}
122+
123+
impl AuthenticationConfig {
124+
pub fn get_cert_store_mut(&mut self) -> &mut rustls::RootCertStore {
125+
&mut self.cert_store
126+
}
127+
128+
pub fn set_cert_config(
129+
&mut self,
130+
cert_path: impl AsRef<Path>,
131+
key_path: impl AsRef<Path>,
132+
) -> Result<(), CertificateError> {
133+
let cert_config = ClientAuthCertConfig::new(cert_path.as_ref(), key_path.as_ref())?;
134+
self.cert_config = Some(cert_config);
135+
Ok(())
136+
}
137+
138+
pub fn set_username(&mut self, username: String) {
139+
self.username = Some(username);
140+
}
141+
142+
pub fn set_password(&mut self, password: Zeroizing<String>) {
143+
self.password = Some(password);
144+
}
145+
146+
pub fn to_rustls_client_config(&self) -> Result<Option<rustls::ClientConfig>, rustls::Error> {
147+
if self.cert_store.is_empty() {
148+
return Ok(None);
149+
}
150+
151+
let tls_config =
152+
rustls::ClientConfig::builder().with_root_certificates(self.cert_store.clone());
153+
154+
let tls_config = match &self.cert_config {
155+
Some(cert_config) => tls_config.with_client_auth_cert(
156+
cert_config.cert_chain.clone(),
157+
cert_config.key.deref().0.clone_key(),
158+
)?,
159+
None => tls_config.with_no_client_auth(),
160+
};
161+
Ok(Some(tls_config))
162+
}
163+
164+
/// When the password is empty, this returns an empty string.
165+
/// This is because `rumqttc::MqttOptions::set_credentials()` always requires a value for password.
166+
pub fn get_credentials(&self) -> Option<(String, Zeroizing<String>)> {
167+
match &self.username {
168+
Some(username) => {
169+
let password = self.password.clone().unwrap_or_default();
170+
Some((username.to_string(), password))
171+
}
172+
None => None,
106173
}
107174
}
108175
}
109176

110177
#[derive(Clone)]
111-
struct ClientAuthConfig {
178+
struct ClientAuthCertConfig {
112179
cert_chain: Vec<CertificateDer<'static>>,
113180
key: Arc<Zeroizing<PrivateKey>>,
114181
}
115182

116-
impl Debug for ClientAuthConfig {
183+
impl ClientAuthCertConfig {
184+
pub fn new(
185+
cert_path: impl AsRef<Path>,
186+
key_path: impl AsRef<Path>,
187+
) -> Result<Self, CertificateError> {
188+
let cert_chain = parse_root_certificate::read_cert_chain(cert_path)?;
189+
let key = parse_root_certificate::read_pvt_key(key_path)?;
190+
Ok(ClientAuthCertConfig {
191+
cert_chain,
192+
key: Arc::new(Zeroizing::new(PrivateKey(key))),
193+
})
194+
}
195+
}
196+
197+
impl Debug for ClientAuthCertConfig {
117198
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
118-
f.debug_struct("ClientAuthConfig")
119-
.field("cert_chain", &self.cert_chain)
199+
f.debug_struct("ClientAuthCertConfig")
200+
.field("cert_chain", &self)
120201
.finish()
121202
}
122203
}
@@ -270,58 +351,11 @@ impl Config {
270351
}
271352
}
272353

273-
/// Adds all certificates present in `ca_file` file to the trust store.
274-
/// Enables server authentication.
275-
pub fn with_cafile(
354+
pub fn with_client_auth(
276355
&mut self,
277-
ca_file: impl AsRef<Path>,
356+
config: AuthenticationConfig,
278357
) -> Result<&mut Self, certificate::CertificateError> {
279-
debug!(target: "MQTT", "Using CA certificate: {}", ca_file.as_ref().display());
280-
let authentication_config = self.broker.authentication.get_or_insert(Default::default());
281-
let cert_store = &mut authentication_config.cert_store;
282-
283-
parse_root_certificate::add_certs_from_file(cert_store, ca_file)?;
284-
285-
Ok(self)
286-
}
287-
288-
/// Adds all certificate from all files in the directory `ca_dir` to the
289-
/// trust store. Enables server authentication.
290-
pub fn with_cadir(
291-
&mut self,
292-
ca_dir: impl AsRef<Path>,
293-
) -> Result<&mut Self, certificate::CertificateError> {
294-
debug!(target: "MQTT", "Using CA directory: {}", ca_dir.as_ref().display());
295-
let authentication_config = self.broker.authentication.get_or_insert(Default::default());
296-
let cert_store = &mut authentication_config.cert_store;
297-
298-
parse_root_certificate::add_certs_from_directory(cert_store, ca_dir)?;
299-
300-
Ok(self)
301-
}
302-
303-
/// Provide client certificate and private key for authentication. If server
304-
/// authentication was not enabled by previously calling
305-
/// [`Config::with_cafile`] or [`Config::with_cadir`], this method also
306-
/// enables it but initializes an empty root cert store.
307-
pub fn with_client_auth<P: AsRef<Path>>(
308-
&mut self,
309-
cert_file: P,
310-
key_file: P,
311-
) -> Result<&mut Self, CertificateError> {
312-
debug!(target: "MQTT", "Using client certificate: {}", cert_file.as_ref().display());
313-
debug!(target: "MQTT", "Using client private key: {}", key_file.as_ref().display());
314-
let cert_chain = parse_root_certificate::read_cert_chain(cert_file)?;
315-
let key = parse_root_certificate::read_pvt_key(key_file)?;
316-
317-
let client_auth_config = ClientAuthConfig {
318-
cert_chain,
319-
key: Arc::new(Zeroizing::new(PrivateKey(key))),
320-
};
321-
322-
let authentication_config = self.broker.authentication.get_or_insert(Default::default());
323-
authentication_config.client_auth = Some(client_auth_config);
324-
358+
self.broker.authentication.get_or_insert(config);
325359
Ok(self)
326360
}
327361

@@ -347,18 +381,12 @@ impl Config {
347381
}
348382

349383
if let Some(authentication_config) = &broker_config.authentication {
350-
let tls_config = rustls::ClientConfig::builder()
351-
.with_root_certificates(authentication_config.cert_store.clone());
352-
353-
let tls_config = match authentication_config.client_auth.clone() {
354-
Some(client_auth_config) => tls_config.with_client_auth_cert(
355-
client_auth_config.cert_chain,
356-
client_auth_config.key.deref().0.clone_key(),
357-
)?,
358-
None => tls_config.with_no_client_auth(),
359-
};
360-
361-
mqtt_options.set_transport(rumqttc::Transport::tls_with_config(tls_config.into()));
384+
if let Some((username, password)) = authentication_config.get_credentials() {
385+
mqtt_options.set_credentials(username, password.clone().to_string());
386+
}
387+
if let Some(tls_config) = authentication_config.to_rustls_client_config()? {
388+
mqtt_options.set_transport(rumqttc::Transport::tls_with_config(tls_config.into()));
389+
}
362390
}
363391

364392
mqtt_options.set_max_packet_size(MAX_PACKET_SIZE, MAX_PACKET_SIZE);
@@ -376,3 +404,21 @@ impl Config {
376404
Ok(mqtt_options)
377405
}
378406
}
407+
408+
/// Read the first line of the given file and return it.
409+
pub fn read_password(path: impl AsRef<Path>) -> Result<Zeroizing<String>, CertificateError> {
410+
let f = File::open(&path).map_err(|error| CertificateError::IoError {
411+
error,
412+
path: path.as_ref().to_owned(),
413+
})?;
414+
let reader = BufReader::new(f);
415+
416+
match reader.lines().next() {
417+
Some(Ok(password)) => Ok(Zeroizing::new(password)),
418+
Some(Err(error)) => Err(CertificateError::IoError {
419+
error,
420+
path: path.as_ref().to_owned(),
421+
}),
422+
None => Ok(Zeroizing::new("".to_string())),
423+
}
424+
}

crates/common/tedge_config/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ pub use tedge_toml::models;
1111
pub use tedge_toml::tedge_config::TEdgeConfig;
1212
pub use tedge_toml::tedge_config::TEdgeConfigDto;
1313
pub use tedge_toml::tedge_config::TEdgeConfigReader;
14+
pub use tedge_toml::tedge_config::TEdgeMqttClientAuthConfig;
1415
pub use tedge_toml::tedge_config_location::*;
1516

1617
pub use camino::Utf8Path as Path;

crates/common/tedge_config/src/tedge_toml/tedge_config.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,8 @@ use tedge_config_macros::*;
5353
use tracing::error;
5454

5555
mod mqtt_config;
56-
pub use mqtt_config::MqttAuthClientConfig;
57-
pub use mqtt_config::MqttAuthConfig;
5856
pub use mqtt_config::MqttAuthConfigCloudBroker;
57+
pub use mqtt_config::TEdgeMqttClientAuthConfig;
5958

6059
const DEFAULT_ROOT_CERT_PATH: &str = "/etc/ssl/certs";
6160

@@ -725,6 +724,14 @@ define_tedge_config! {
725724
#[tedge_config(example = "/etc/mosquitto/auth_certificates/key.pem")]
726725
#[tedge_config(deprecated_name = "keyfile")]
727726
key_file: AbsolutePath,
727+
728+
/// Client username
729+
#[tedge_config(example = "myuser")]
730+
username: String,
731+
732+
/// Path to the client password file
733+
#[tedge_config(example = "/etc/tedge/.client_password")]
734+
password_file: AbsolutePath,
728735
}
729736
},
730737

0 commit comments

Comments
 (0)