diff --git a/Cargo.toml b/Cargo.toml index db7c22b9..d1ccacd5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,3 +70,4 @@ pkcs11-provider = ["pkcs11", "picky-asn1-der", "picky-asn1", "picky-asn1-x509", tpm-provider = ["tss-esapi", "picky-asn1-der", "picky-asn1", "picky-asn1-x509"] all-providers = ["tpm-provider", "pkcs11-provider", "mbed-crypto-provider"] docs = ["pkcs11-provider", "tpm-provider", "tss-esapi/docs", "mbed-crypto-provider"] +unix-peer-credentials-authenticator = [] diff --git a/src/authenticators/mod.rs b/src/authenticators/mod.rs index 612e33bf..ec7eea5f 100644 --- a/src/authenticators/mod.rs +++ b/src/authenticators/mod.rs @@ -8,11 +8,12 @@ //! 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; +#[cfg(feature = "unix-peer-credentials-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 +36,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..a4157fa5 --- /dev/null +++ b/src/authenticators/unix_peer_credentials_authenticator/mod.rs @@ -0,0 +1,183 @@ +// 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; + +#[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(); + if expected_uid_bytes.is_empty() { + error!("Expected UID in authentication request, but it is empty."); + return Err(ResponseStatus::AuthenticationError); + } + + const EXPECTED_UID_SIZE_BYTES: usize = 4; + if expected_uid_bytes.len() != EXPECTED_UID_SIZE_BYTES { + error!( + "UID in authentication request is not the right size (expected: {}, got: {}).", + EXPECTED_UID_SIZE_BYTES, + expected_uid_bytes.len() + ); + return Err(ResponseStatus::AuthenticationError); + } + + let boxed_slice = expected_uid_bytes.into_boxed_slice(); + let boxed_array: Box<[u8; 4]> = boxed_slice.try_into().unwrap(); + let expected_uid = u32::from_le_bytes(*boxed_array); + + let meta = meta.ok_or_else(|| { + error!("Authenticator did not receive any metadata; cannot perform authentication."); + ResponseStatus::AuthenticationError + })?; + + let (uid, _gid) = match meta { + ConnectionMetadata::UnixPeerCredentials { uid, gid } => (uid, gid), + // TODO: add wildcard pattern when `ConnectionMetadata` has more possibilities. + }; + + // 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_string().as_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, + }); + + 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_string().as_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, + }); + + 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, + }); + + 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: 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..9f4ddd6f 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, GetMetadata}; use log::error; #[cfg(not(feature = "no-parsec-user-and-clients-group"))] use std::ffi::CString; @@ -16,6 +16,7 @@ use std::io::{Error, ErrorKind, Result}; use std::os::unix::fs::PermissionsExt; use std::os::unix::io::FromRawFd; use std::os::unix::net::UnixListener; +use std::os::unix::net::UnixStream; use std::path::Path; use std::time::Duration; @@ -202,11 +203,10 @@ impl Listen for DomainSocketListener { format_error!("Failed to set stream as blocking", err); None } else { + let metadata = stream.metadata(); 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, }) } } @@ -248,3 +248,26 @@ impl DomainSocketListenerBuilder { })?) } } + +impl GetMetadata for UnixStream { + #[cfg(feature = "unix-peer-credentials-authenticator")] + fn metadata(&self) -> Option { + let ucred = self.peer_cred().or_else(|err| { + format_error!( + "Failed to grab peer credentials metadata from UnixStream", + err + ); + None + })?; + Some(ConnectionMetadata::UnixPeerCredentials { + uid: ucred.uid, + gid: ucred.gid, + }) + } + + // If Unix peer credentials authenticator feature is not in use, return None for the metadata. + #[cfg(not(feature = "unix-peer-credentials-authenticator"))] + fn metadata(&self) -> Option { + None + } +} diff --git a/src/front/listener.rs b/src/front/listener.rs index 914de6f4..fc596e20 100644 --- a/src/front/listener.rs +++ b/src/front/listener.rs @@ -34,7 +34,13 @@ 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, + }, } /// Represents a connection to a single client @@ -70,3 +76,9 @@ pub trait Listen { /// If the listener has not been initialised before, with the `init` method. fn accept(&self) -> Option; } + +/// Get metadata for a particular object. +pub trait GetMetadata { + /// Get the metadata associated with this object. + fn metadata(&self) -> Option; +} diff --git a/src/lib.rs b/src/lib.rs index ee105805..d27f9a41 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -35,6 +35,12 @@ )] // This one is hard to avoid. #![allow(clippy::multiple_crate_versions)] +// TODO: remove this if/when the Unix peer credentials PR gets merged. Link +// here for reference: https://github.com/rust-lang/rust/pull/75148 +#![cfg_attr( + feature = "unix-peer-credentials-authenticator", + feature(peer_credentials_unix_socket) +)] #[allow(unused)] macro_rules! format_error {