Skip to content

Commit

Permalink
Merge pull request #214 from joechrisellis/uds_authenticator
Browse files Browse the repository at this point in the history
Add Unix peer credentials authenticator
  • Loading branch information
hug-dev authored Sep 22, 2020
2 parents b80416b + 0b48895 commit 2a57277
Show file tree
Hide file tree
Showing 5 changed files with 355 additions and 8 deletions.
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]

Expand Down
6 changes: 3 additions & 3 deletions src/authenticators/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
///
Expand Down
196 changes: 196 additions & 0 deletions src/authenticators/unix_peer_credentials_authenticator/mod.rs
Original file line number Diff line number Diff line change
@@ -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.
}
}
144 changes: 140 additions & 4 deletions src/front/domain_socket.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
}),
})
}
}
Expand Down Expand Up @@ -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())
}
}
}
}
}
Loading

0 comments on commit 2a57277

Please sign in to comment.