From d56700ca914e67eaa6d50ac5fb40657c870604ae Mon Sep 17 00:00:00 2001 From: Joe Ellis Date: Mon, 21 Sep 2020 16:23:13 +0100 Subject: [PATCH 1/3] Add rand crate as a dev-dependency Will be required for testing the Unix peer credentials authenticator. Signed-off-by: Joe Ellis --- Cargo.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index c6844f9d..88955daf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,9 @@ picky-asn1-x509 = { version = "0.3.2", optional = true } users = "0.10.0" libc = "0.2.77" +[dev-dependencies] +rand = { version = "0.7.3", features = ["small_rng"] } + [package.metadata.docs.rs] features = ["pkcs11-provider", "tpm-provider", "tss-esapi/docs", "mbed-crypto-provider"] From c02b5ef806ee28c817a1b664826df2355e46210b Mon Sep 17 00:00:00 2001 From: Joe Ellis Date: Thu, 6 Aug 2020 11:05:09 +0100 Subject: [PATCH 2/3] Add peer credentials authenticator This authenticator uses peer credentials for authentication. The specific type of peer credentials in mind at the moment are Unix peer credentials, but this can be extended in the future. Unix peer credentials provide direct access to the (effective) uid/gid on the other end of a domain socket connect, without cooperation between the endpoints. This means that we can trivially determine the uid/gid of the connecting process, which we can then use for authentication. This authenticator: - grabs the (uid, gid) pair of the connecting process. - grabs the self-declared uid sent in the authentication request. - verifies that authentication is successful by checking that the self-declared uid in the authentication request is equal to the actual uid from the peer credentials. - if authentication was successful, creates an `ApplicationName` based on the uid. The authenticator is hidden behind the Cargo feature `peer-credentials-authenticator`. Note that gid is currently unused by the authenticator. Also note that this patch depends on the following PR being merged: https://github.com/rust-lang/rust/pull/75148 At the time of writing, this PR is currently under review and is not merged into the Rust stdlib. This patch therefore will not build with the current a stable/nightly compiler. Signed-off-by: Joe Ellis --- src/authenticators/mod.rs | 6 +- .../mod.rs | 186 ++++++++++++++++++ src/front/domain_socket.rs | 20 +- src/front/listener.rs | 14 +- 4 files changed, 218 insertions(+), 8 deletions(-) create mode 100644 src/authenticators/unix_peer_credentials_authenticator/mod.rs diff --git a/src/authenticators/mod.rs b/src/authenticators/mod.rs index 612e33bf..5ac29025 100644 --- a/src/authenticators/mod.rs +++ b/src/authenticators/mod.rs @@ -8,11 +8,11 @@ //! used throughout the service for identifying the request initiator. The input to an authentication //! is the `RequestAuth` field of a request, which is parsed by the authenticator specified in the header. //! The authentication functionality is abstracted through an `Authenticate` trait. -//! -//! Currently only a simple Direct Authenticator component is implemented. pub mod direct_authenticator; +pub mod unix_peer_credentials_authenticator; + use crate::front::listener::ConnectionMetadata; use parsec_interface::operations::list_authenticators; use parsec_interface::requests::request::RequestAuth; @@ -35,7 +35,7 @@ pub trait Authenticate { /// Authenticates a `RequestAuth` payload and returns the `ApplicationName` if successful. A /// optional `ConnectionMetadata` object is passed in too, since it is sometimes possible to /// perform authentication based on the connection's metadata (i.e. as is the case for UNIX - /// domain sockets with peer credentials). + /// domain sockets with Unix peer credentials). /// /// # Errors /// diff --git a/src/authenticators/unix_peer_credentials_authenticator/mod.rs b/src/authenticators/unix_peer_credentials_authenticator/mod.rs new file mode 100644 index 00000000..a046dca8 --- /dev/null +++ b/src/authenticators/unix_peer_credentials_authenticator/mod.rs @@ -0,0 +1,186 @@ +// Copyright 2020 Contributors to the Parsec project. +// SPDX-License-Identifier: Apache-2.0 +//! Unix peer credentials authenticator +//! +//! The `UnixPeerCredentialsAuthenticator` uses Unix peer credentials to perform authentication. As +//! such, it uses the effective Unix user ID (UID) to authenticate the connecting process. Unix +//! peer credentials also allow us to access the effective Unix group ID (GID) of the connecting +//! process, although this information is currently unused. +//! +//! Currently, the stringified UID is used as the application name. + +use super::ApplicationName; +use super::Authenticate; +use crate::front::listener::ConnectionMetadata; +use log::error; +use parsec_interface::operations::list_authenticators; +use parsec_interface::requests::request::RequestAuth; +use parsec_interface::requests::AuthType; +use parsec_interface::requests::{ResponseStatus, Result}; +use parsec_interface::secrecy::ExposeSecret; +use std::convert::TryInto; + +/// Unix peer credentials authenticator. +#[derive(Copy, Clone, Debug)] +pub struct UnixPeerCredentialsAuthenticator; + +impl Authenticate for UnixPeerCredentialsAuthenticator { + fn describe(&self) -> Result { + Ok(list_authenticators::AuthenticatorInfo { + description: String::from( + "Uses Unix peer credentials to authenticate the client. Verifies that the self-declared \ + Unix user identifier (UID) in the request's authentication header matches that which is \ + found from the peer credentials." + ), + version_maj: 0, + version_min: 1, + version_rev: 0, + id: AuthType::PeerCredentials, + }) + } + + fn authenticate( + &self, + auth: &RequestAuth, + meta: Option, + ) -> Result { + // Parse authentication request. + let expected_uid_bytes = auth.buffer.expose_secret(); + + const EXPECTED_UID_SIZE_BYTES: usize = 4; + let expected_uid: [u8; EXPECTED_UID_SIZE_BYTES] = + expected_uid_bytes.as_slice().try_into().map_err(|_| { + error!( + "UID in authentication request is not the right size (expected: {}, got: {}).", + EXPECTED_UID_SIZE_BYTES, + expected_uid_bytes.len() + ); + ResponseStatus::AuthenticationError + })?; + let expected_uid = u32::from_le_bytes(expected_uid); + + let meta = meta.ok_or_else(|| { + error!("Authenticator did not receive any metadata; cannot perform authentication."); + ResponseStatus::AuthenticationError + })?; + + #[allow(unreachable_patterns)] + let (uid, _gid, _pid) = match meta { + ConnectionMetadata::UnixPeerCredentials { uid, gid, pid } => (uid, gid, pid), + _ => { + error!("Wrong metadata type given to Unix peer credentials authenticator."); + return Err(ResponseStatus::AuthenticationError); + } + }; + + // Authentication is successful if the _actual_ UID from the Unix peer credentials equals + // the self-declared UID in the authentication request. + if uid == expected_uid { + Ok(ApplicationName(uid.to_string())) + } else { + error!("Declared UID in authentication request does not match the process's UID."); + Err(ResponseStatus::AuthenticationError) + } + } +} + +#[cfg(test)] +mod test { + use super::super::Authenticate; + use super::UnixPeerCredentialsAuthenticator; + use crate::front::listener::ConnectionMetadata; + use parsec_interface::requests::request::RequestAuth; + use parsec_interface::requests::ResponseStatus; + use rand::Rng; + use std::os::unix::net::UnixStream; + use users::get_current_uid; + + #[test] + fn successful_authentication() { + // This test should PASS; we are verifying that our username gets set as the application + // secret when using Unix peer credentials authentication with Unix domain sockets. + + // Create two connected sockets. + let (sock_a, _sock_b) = UnixStream::pair().unwrap(); + let (cred_a, _cred_b) = (sock_a.peer_cred().unwrap(), _sock_b.peer_cred().unwrap()); + + let authenticator = UnixPeerCredentialsAuthenticator {}; + + let req_auth_data = cred_a.uid.to_le_bytes().to_vec(); + let req_auth = RequestAuth::new(req_auth_data); + let conn_metadata = Some(ConnectionMetadata::UnixPeerCredentials { + uid: cred_a.uid, + gid: cred_a.gid, + pid: None, + }); + + let auth_name = authenticator + .authenticate(&req_auth, conn_metadata) + .expect("Failed to authenticate"); + + assert_eq!(auth_name.get_name(), get_current_uid().to_string()); + } + + #[test] + fn unsuccessful_authentication_wrong_declared_uid() { + // This test should FAIL; we are trying to authenticate, but we are declaring the wrong + // UID. + + // Create two connected sockets. + let (sock_a, _sock_b) = UnixStream::pair().unwrap(); + let (cred_a, _cred_b) = (sock_a.peer_cred().unwrap(), _sock_b.peer_cred().unwrap()); + + let authenticator = UnixPeerCredentialsAuthenticator {}; + + let wrong_uid = cred_a.uid + 1; + let wrong_req_auth_data = wrong_uid.to_le_bytes().to_vec(); + let req_auth = RequestAuth::new(wrong_req_auth_data); + let conn_metadata = Some(ConnectionMetadata::UnixPeerCredentials { + uid: cred_a.uid, + gid: cred_a.gid, + pid: cred_a.pid, + }); + + let auth_result = authenticator.authenticate(&req_auth, conn_metadata); + assert_eq!(auth_result, Err(ResponseStatus::AuthenticationError)); + } + + #[test] + fn unsuccessful_authentication_garbage_data() { + // This test should FAIL; we are sending garbage (random) data in the request. + + // Create two connected sockets. + let (sock_a, _sock_b) = UnixStream::pair().unwrap(); + let (cred_a, _cred_b) = (sock_a.peer_cred().unwrap(), _sock_b.peer_cred().unwrap()); + + let authenticator = UnixPeerCredentialsAuthenticator {}; + + let garbage_data = rand::thread_rng().gen::<[u8; 32]>().to_vec(); + let req_auth = RequestAuth::new(garbage_data); + let conn_metadata = Some(ConnectionMetadata::UnixPeerCredentials { + uid: cred_a.uid, + gid: cred_a.gid, + pid: cred_a.pid, + }); + + let auth_result = authenticator.authenticate(&req_auth, conn_metadata); + assert_eq!(auth_result, Err(ResponseStatus::AuthenticationError)); + } + + #[test] + fn unsuccessful_authentication_no_metadata() { + let authenticator = UnixPeerCredentialsAuthenticator {}; + let req_auth = RequestAuth::new("secret".into()); + + let conn_metadata = None; + let auth_result = authenticator.authenticate(&req_auth, conn_metadata); + assert_eq!(auth_result, Err(ResponseStatus::AuthenticationError)); + } + + #[test] + fn unsuccessful_authentication_wrong_metadata() { + // TODO(new_metadata_variant): this test needs implementing when we have more than one + // metadata type. At the moment, the compiler just complains with an 'unreachable branch' + // message. + } +} diff --git a/src/front/domain_socket.rs b/src/front/domain_socket.rs index b0e0879d..5c0fdc3b 100644 --- a/src/front/domain_socket.rs +++ b/src/front/domain_socket.rs @@ -5,8 +5,8 @@ //! Expose Parsec functionality using Unix domain sockets as an IPC layer. //! The local socket is created at a predefined location. use super::listener; -use listener::Connection; use listener::Listen; +use listener::{Connection, ConnectionMetadata}; use log::error; #[cfg(not(feature = "no-parsec-user-and-clients-group"))] use std::ffi::CString; @@ -202,11 +202,23 @@ impl Listen for DomainSocketListener { format_error!("Failed to set stream as blocking", err); None } else { + let ucred = stream + .peer_cred() + .map_err(|err| { + format_error!( + "Failed to grab peer credentials metadata from UnixStream", + err + ); + err + }) + .ok()?; Some(Connection { stream: Box::new(stream), - // TODO: when possible, we want to replace this with the (uid, gid, pid) - // triple for peer credentials. See listener.rs. - metadata: None, + metadata: Some(ConnectionMetadata::UnixPeerCredentials { + uid: ucred.uid, + gid: ucred.gid, + pid: ucred.pid, + }), }) } } diff --git a/src/front/listener.rs b/src/front/listener.rs index 914de6f4..cd8b7e04 100644 --- a/src/front/listener.rs +++ b/src/front/listener.rs @@ -34,7 +34,19 @@ pub struct ListenerConfig { /// Specifies metadata associated with a connection, if any. #[derive(Copy, Clone, Debug)] pub enum ConnectionMetadata { - // TODO: nothing here right now. Metadata types will be added as needed. + /// Unix peer credentials metadata for Unix domain sockets. + UnixPeerCredentials { + /// The effective UID of the connecting process. + uid: u32, + /// The effective GID of the connecting process. + gid: u32, + /// The optional PID of the connecting process. This is an Option because not all + /// platforms support retrieving PID via a domain socket. + pid: Option, + }, + // NOTE: there is currently only _one_ variant of the ConnectionMetadata enum. When a second + // variant is added, you will need to update some tests! + // You should grep the tests for `TODO(new_metadata_variant)` and update them accordingly. } /// Represents a connection to a single client From 0b48895c07bb96bbf2660ea0dc78602805da9f2c Mon Sep 17 00:00:00 2001 From: Joe Ellis Date: Wed, 16 Sep 2020 10:27:41 +0100 Subject: [PATCH 3/3] Cherry-pick Rust standard library peer credentials feature into Parsec The code introduced in this patch has been cherry-picked from the following PR: https://github.com/rust-lang/rust/pull/75148 At the time of writing (16/09/20), this patch is in the nightly Rust channel. To avoid needing to use the nightly compiler to build Parsec, this patch includes the change from the standard library to allow us to use this feature 'early'. Once the feature hits the stable branch, it should be safe to revert this commit with `git revert`. Signed-off-by: Joe Ellis --- .../mod.rs | 16 ++- src/front/domain_socket.rs | 128 +++++++++++++++++- 2 files changed, 139 insertions(+), 5 deletions(-) diff --git a/src/authenticators/unix_peer_credentials_authenticator/mod.rs b/src/authenticators/unix_peer_credentials_authenticator/mod.rs index a046dca8..1e4ae94d 100644 --- a/src/authenticators/unix_peer_credentials_authenticator/mod.rs +++ b/src/authenticators/unix_peer_credentials_authenticator/mod.rs @@ -88,6 +88,7 @@ impl Authenticate for UnixPeerCredentialsAuthenticator { mod test { use super::super::Authenticate; use super::UnixPeerCredentialsAuthenticator; + use crate::front::domain_socket::peer_credentials; use crate::front::listener::ConnectionMetadata; use parsec_interface::requests::request::RequestAuth; use parsec_interface::requests::ResponseStatus; @@ -102,7 +103,10 @@ mod test { // Create two connected sockets. let (sock_a, _sock_b) = UnixStream::pair().unwrap(); - let (cred_a, _cred_b) = (sock_a.peer_cred().unwrap(), _sock_b.peer_cred().unwrap()); + let (cred_a, _cred_b) = ( + peer_credentials::peer_cred(&sock_a).unwrap(), + peer_credentials::peer_cred(&_sock_b).unwrap(), + ); let authenticator = UnixPeerCredentialsAuthenticator {}; @@ -128,7 +132,10 @@ mod test { // Create two connected sockets. let (sock_a, _sock_b) = UnixStream::pair().unwrap(); - let (cred_a, _cred_b) = (sock_a.peer_cred().unwrap(), _sock_b.peer_cred().unwrap()); + let (cred_a, _cred_b) = ( + peer_credentials::peer_cred(&sock_a).unwrap(), + peer_credentials::peer_cred(&_sock_b).unwrap(), + ); let authenticator = UnixPeerCredentialsAuthenticator {}; @@ -151,7 +158,10 @@ mod test { // Create two connected sockets. let (sock_a, _sock_b) = UnixStream::pair().unwrap(); - let (cred_a, _cred_b) = (sock_a.peer_cred().unwrap(), _sock_b.peer_cred().unwrap()); + let (cred_a, _cred_b) = ( + peer_credentials::peer_cred(&sock_a).unwrap(), + peer_credentials::peer_cred(&_sock_b).unwrap(), + ); let authenticator = UnixPeerCredentialsAuthenticator {}; diff --git a/src/front/domain_socket.rs b/src/front/domain_socket.rs index 5c0fdc3b..399801b0 100644 --- a/src/front/domain_socket.rs +++ b/src/front/domain_socket.rs @@ -202,8 +202,7 @@ impl Listen for DomainSocketListener { format_error!("Failed to set stream as blocking", err); None } else { - let ucred = stream - .peer_cred() + let ucred = peer_credentials::peer_cred(&stream) .map_err(|err| { format_error!( "Failed to grab peer credentials metadata from UnixStream", @@ -260,3 +259,128 @@ impl DomainSocketListenerBuilder { })?) } } + +// == IMPORTANT NOTE == +// +// The code below has been cherry-picked from the following PR: +// +// https://github.com/rust-lang/rust/pull/75148 +// +// At the time of writing (16/09/20), this patch is in the nightly Rust channel. To avoid needing +// to use the nightly compiler to build Parsec, we have instead opted to cherry-pick the change +// from the patch to allow us to use this feature 'early'. +// +// Once the feature hits stable, it should be safe to revert the commit that introduced the changes +// below with `git revert`. You can find the stabilizing Rust issue here: +// +// https://github.com/rust-lang/rust/issues/42839 + +/// Implementation of peer credentials fetching for Unix domain socket. +pub mod peer_credentials { + use libc::{gid_t, pid_t, uid_t}; + + /// Credentials for a UNIX process for credentials passing. + #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] + pub struct UCred { + /// The UID part of the peer credential. This is the effective UID of the process at the domain + /// socket's endpoint. + pub uid: uid_t, + /// The GID part of the peer credential. This is the effective GID of the process at the domain + /// socket's endpoint. + pub gid: gid_t, + /// The PID part of the peer credential. This field is optional because the PID part of the + /// peer credentials is not supported on every platform. On platforms where the mechanism to + /// discover the PID exists, this field will be populated to the PID of the process at the + /// domain socket's endpoint. Otherwise, it will be set to None. + pub pid: Option, + } + + #[cfg(any(target_os = "android", target_os = "linux"))] + pub use self::impl_linux::peer_cred; + + #[cfg(any( + target_os = "dragonfly", + target_os = "freebsd", + target_os = "ios", + target_os = "macos", + target_os = "openbsd" + ))] + pub use self::impl_bsd::peer_cred; + + #[cfg(any(target_os = "linux", target_os = "android"))] + #[allow(missing_docs, trivial_casts)] // docs not required; only used for selective compilation. + pub mod impl_linux { + use super::UCred; + use libc::{c_void, getsockopt, socklen_t, ucred, SOL_SOCKET, SO_PEERCRED}; + use std::os::unix::io::AsRawFd; + use std::os::unix::net::UnixStream; + use std::{io, mem}; + + pub fn peer_cred(socket: &UnixStream) -> io::Result { + let ucred_size = mem::size_of::(); + + // Trivial sanity checks. + assert!(mem::size_of::() <= mem::size_of::()); + assert!(ucred_size <= u32::MAX as usize); + + let mut ucred_size = ucred_size as socklen_t; + let mut ucred: ucred = ucred { + pid: 1, + uid: 1, + gid: 1, + }; + + unsafe { + let ret = getsockopt( + socket.as_raw_fd(), + SOL_SOCKET, + SO_PEERCRED, + &mut ucred as *mut ucred as *mut c_void, + &mut ucred_size, + ); + + if ret == 0 && ucred_size as usize == mem::size_of::() { + Ok(UCred { + uid: ucred.uid, + gid: ucred.gid, + pid: Some(ucred.pid), + }) + } else { + Err(io::Error::last_os_error()) + } + } + } + } + + #[cfg(any( + target_os = "dragonfly", + target_os = "macos", + target_os = "ios", + target_os = "freebsd", + target_os = "openbsd" + ))] + #[allow(missing_docs)] // docs not required; only used for selective compilation. + pub mod impl_bsd { + use super::UCred; + use std::io; + use std::os::unix::io::AsRawFd; + use std::os::unix::net::UnixStream; + + pub fn peer_cred(socket: &UnixStream) -> io::Result { + let mut cred = UCred { + uid: 1, + gid: 1, + pid: None, + }; + unsafe { + let ret = libc::getpeereid(socket.as_raw_fd(), &mut cred.uid, &mut cred.gid); + + if ret == 0 { + Ok(cred) + } else { + Err(io::Error::last_os_error()) + } + } + } + } +}