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"] 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..1e4ae94d --- /dev/null +++ b/src/authenticators/unix_peer_credentials_authenticator/mod.rs @@ -0,0 +1,196 @@ +// 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<list_authenticators::AuthenticatorInfo> { + 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<ConnectionMetadata>, + ) -> Result<ApplicationName> { + // 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::domain_socket::peer_credentials; + 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) = ( + peer_credentials::peer_cred(&sock_a).unwrap(), + peer_credentials::peer_cred(&_sock_b).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) = ( + peer_credentials::peer_cred(&sock_a).unwrap(), + peer_credentials::peer_cred(&_sock_b).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) = ( + peer_credentials::peer_cred(&sock_a).unwrap(), + peer_credentials::peer_cred(&_sock_b).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..399801b0 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,22 @@ impl Listen for DomainSocketListener { format_error!("Failed to set stream as blocking", err); None } else { + let ucred = peer_credentials::peer_cred(&stream) + .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, + }), }) } } @@ -248,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<pid_t>, + } + + #[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<UCred> { + let ucred_size = mem::size_of::<ucred>(); + + // Trivial sanity checks. + assert!(mem::size_of::<u32>() <= mem::size_of::<usize>()); + 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::<ucred>() { + 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<UCred> { + 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()) + } + } + } + } +} 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<u32> because not all + /// platforms support retrieving PID via a domain socket. + pid: Option<i32>, + }, + // 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