Keystore libselinux rust bindings.
Provide safe wrappers around the libselinux API needed for keystore.
* getcon
* selinux_check_acces
* selabel_lookup
Test: keystore2_selinux_test
Test: keystore2_selinux_rust_bindings_host_test
Bug: 159466840
Change-Id: I73b4aa2e1da9b477965b10927eba069e6346ce6e
diff --git a/keystore2/selinux/src/lib.rs b/keystore2/selinux/src/lib.rs
new file mode 100644
index 0000000..30a235a
--- /dev/null
+++ b/keystore2/selinux/src/lib.rs
@@ -0,0 +1,419 @@
+// Copyright 2020, The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// #![allow(missing_docs)]
+
+//! This crate provides some safe wrappers around the libselinux API. It is currently limited
+//! to the API surface that Keystore 2.0 requires to perform permission checks against
+//! the SEPolicy. Notably, it provides wrappers for:
+//! * getcon
+//! * selinux_check_access
+//! * selabel_lookup for the keystore2_key backend.
+//! And it provides an owning wrapper around context strings `Context`.
+
+use std::ffi::{CStr, CString};
+use std::fmt;
+use std::io;
+pub use std::ops::Deref;
+use std::os::raw::c_char;
+use std::ptr;
+use std::sync;
+
+use selinux_bindgen as selinux;
+
+use anyhow::Context as AnyhowContext;
+use anyhow::{anyhow, Result};
+
+use selinux::SELABEL_CTX_ANDROID_KEYSTORE2_KEY;
+use selinux::SELINUX_CB_LOG;
+
+static SELINUX_LOG_INIT: sync::Once = sync::Once::new();
+
+fn redirect_selinux_logs_to_logcat() {
+ // `selinux_set_callback` assigned the static lifetime function pointer
+ // `selinux_log_callback` to a static lifetime variable.
+ let cb = selinux::selinux_callback { func_log: Some(selinux::selinux_log_callback) };
+ unsafe {
+ selinux::selinux_set_callback(SELINUX_CB_LOG as i32, cb);
+ }
+}
+
+// This function must be called before any entrypoint into lib selinux.
+// Or leave a comment reasoning why calling this macro is not necessary
+// for a given entry point.
+fn init_logger_once() {
+ SELINUX_LOG_INIT.call_once(redirect_selinux_logs_to_logcat)
+}
+
+/// Selinux Error code.
+#[derive(thiserror::Error, Debug, PartialEq)]
+pub enum Error {
+ /// Indicates that an access check yielded no access.
+ #[error("Permission Denied")]
+ PermissionDenied,
+ /// Indicates an unexpected system error. Nested string provides some details.
+ #[error("Selinux SystemError: {0}")]
+ SystemError(String),
+}
+
+impl Error {
+ /// Constructs a `PermissionDenied` error.
+ pub fn perm() -> Self {
+ Error::PermissionDenied
+ }
+ fn sys<T: Into<String>>(s: T) -> Self {
+ Error::SystemError(s.into())
+ }
+}
+
+/// Context represents an SELinux context string. It can take ownership of a raw
+/// s-string as allocated by `getcon` or `selabel_lookup`. In this case it uses
+/// `freecon` to free the resources when dropped. In its second variant it stores
+/// an `std::ffi::CString` that can be initialized from a Rust string slice.
+pub enum Context {
+ /// Wraps a raw context c-string as returned by libselinux.
+ Raw(*mut ::std::os::raw::c_char),
+ /// Stores a context string as `std::ffi::CString`.
+ CString(CString),
+}
+
+impl fmt::Display for Context {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "{}", (**self).to_str().unwrap_or("Invalid context"))
+ }
+}
+
+impl Drop for Context {
+ fn drop(&mut self) {
+ if let Self::Raw(p) = self {
+ // No need to initialize the logger here, because
+ // `freecon` cannot run unless `Backend::lookup` or `getcon`
+ // has run.
+ unsafe { selinux::freecon(*p) };
+ }
+ }
+}
+
+impl Deref for Context {
+ type Target = CStr;
+
+ fn deref(&self) -> &Self::Target {
+ match self {
+ Self::Raw(p) => unsafe { CStr::from_ptr(*p) },
+ Self::CString(cstr) => &cstr,
+ }
+ }
+}
+
+impl Context {
+ /// Initializes the `Context::CString` variant from a Rust string slice.
+ pub fn new(con: &str) -> Result<Self> {
+ Ok(Self::CString(
+ CString::new(con)
+ .with_context(|| format!("Failed to create Context with \"{}\"", con))?,
+ ))
+ }
+}
+
+/// The backend trait provides a uniform interface to all libselinux context backends.
+/// Currently, we only implement the KeystoreKeyBackend though.
+pub trait Backend {
+ /// Implementers use libselinux `selabel_lookup` to lookup the context for the given `key`.
+ fn lookup(&self, key: &str) -> Result<Context>;
+}
+
+/// Keystore key backend takes onwnership of the SELinux context handle returned by
+/// `selinux_android_keystore2_key_context_handle` and uses `selabel_close` to free
+/// the handle when dropped.
+/// It implements `Backend` to provide keystore_key label lookup functionality.
+pub struct KeystoreKeyBackend {
+ handle: *mut selinux::selabel_handle,
+}
+
+impl KeystoreKeyBackend {
+ const BACKEND_TYPE: i32 = SELABEL_CTX_ANDROID_KEYSTORE2_KEY as i32;
+
+ /// Creates a new instance representing an SELinux context handle as returned by
+ /// `selinux_android_keystore2_key_context_handle`.
+ pub fn new() -> Result<Self> {
+ init_logger_once();
+ let handle = unsafe { selinux::selinux_android_keystore2_key_context_handle() };
+ if handle.is_null() {
+ return Err(anyhow!(Error::sys("Failed to open KeystoreKeyBackend")));
+ }
+ Ok(KeystoreKeyBackend { handle })
+ }
+}
+
+impl Drop for KeystoreKeyBackend {
+ fn drop(&mut self) {
+ // No need to initialize the logger here because it cannot be called unless
+ // KeystoreKeyBackend::new has run.
+ unsafe { selinux::selabel_close(self.handle) };
+ }
+}
+
+impl Backend for KeystoreKeyBackend {
+ fn lookup(&self, key: &str) -> Result<Context> {
+ let mut con: *mut c_char = ptr::null_mut();
+ let c_key = CString::new(key).with_context(|| {
+ format!("selabel_lookup: Failed to convert key \"{}\" to CString.", key)
+ })?;
+ match unsafe {
+ // No need to initialize the logger here because it cannot run unless
+ // KeystoreKeyBackend::new has run.
+ selinux::selabel_lookup(self.handle, &mut con, c_key.as_ptr(), Self::BACKEND_TYPE)
+ } {
+ 0 => {
+ if !con.is_null() {
+ Ok(Context::Raw(con))
+ } else {
+ Err(anyhow!(Error::sys(format!(
+ "selabel_lookup returned a NULL context for key \"{}\"",
+ key
+ ))))
+ }
+ }
+ _ => Err(anyhow!(io::Error::last_os_error()))
+ .with_context(|| format!("selabel_lookup failed for key \"{}\"", key)),
+ }
+ }
+}
+
+/// Safe wrapper around libselinux `getcon`. It initializes the `Context::Raw` variant of the
+/// returned `Context`.
+///
+/// ## Return
+/// * Ok(Context::Raw()) if successful.
+/// * Err(Error::sys()) if getcon succeeded but returned a NULL pointer.
+/// * Err(io::Error::last_os_error()) if getcon failed.
+pub fn getcon() -> Result<Context> {
+ init_logger_once();
+ let mut con: *mut c_char = ptr::null_mut();
+ match unsafe { selinux::getcon(&mut con) } {
+ 0 => {
+ if !con.is_null() {
+ Ok(Context::Raw(con))
+ } else {
+ Err(anyhow!(Error::sys("getcon returned a NULL context")))
+ }
+ }
+ _ => Err(anyhow!(io::Error::last_os_error())).context("getcon failed"),
+ }
+}
+
+/// Safe wrapper around selinux_check_access.
+///
+/// ## Return
+/// * Ok(()) iff the requested access was granted.
+/// * Err(anyhow!(Error::perm()))) if the permission was denied.
+/// * Err(anyhow!(ioError::last_os_error())) if any other error occurred while performing
+/// the access check.
+pub fn check_access(source: &Context, target: &Context, tclass: &str, perm: &str) -> Result<()> {
+ init_logger_once();
+ let c_tclass = CString::new(tclass).with_context(|| {
+ format!("check_access: Failed to convert tclass \"{}\" to CString.", tclass)
+ })?;
+ let c_perm = CString::new(perm).with_context(|| {
+ format!("check_access: Failed to convert perm \"{}\" to CString.", perm)
+ })?;
+
+ match unsafe {
+ selinux::selinux_check_access(
+ source.as_ptr(),
+ target.as_ptr(),
+ c_tclass.as_ptr(),
+ c_perm.as_ptr(),
+ ptr::null_mut(),
+ )
+ } {
+ 0 => Ok(()),
+ _ => {
+ let e = io::Error::last_os_error();
+ match e.kind() {
+ io::ErrorKind::PermissionDenied => Err(anyhow!(Error::perm())),
+ _ => Err(anyhow!(e)),
+ }
+ .with_context(|| {
+ format!(
+ concat!(
+ "check_access: Failed with sctx: {} tctx: {}",
+ " with target class: \"{}\" perm: \"{}\""
+ ),
+ source, target, tclass, perm
+ )
+ })
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use anyhow::Result;
+
+ /// The su_key namespace as defined in su.te and keystore_key_contexts of the
+ /// SePolicy (system/sepolicy).
+ static SU_KEY_NAMESPACE: &str = "0";
+ /// The shell_key namespace as defined in shell.te and keystore_key_contexts of the
+ /// SePolicy (system/sepolicy).
+ static SHELL_KEY_NAMESPACE: &str = "1";
+
+ fn check_context() -> Result<(Context, &'static str, bool)> {
+ let context = getcon()?;
+ match context.to_str().unwrap() {
+ "u:r:su:s0" => Ok((context, SU_KEY_NAMESPACE, true)),
+ "u:r:shell:s0" => Ok((context, SHELL_KEY_NAMESPACE, false)),
+ c => Err(anyhow!(format!(
+ "This test must be run as \"su\" or \"shell\". Current context: \"{}\"",
+ c
+ ))),
+ }
+ }
+
+ #[test]
+ fn test_getcon() -> Result<()> {
+ check_context()?;
+ Ok(())
+ }
+
+ #[test]
+ fn test_label_lookup() -> Result<()> {
+ let (_context, namespace, is_su) = check_context()?;
+ let backend = crate::KeystoreKeyBackend::new()?;
+ let context = backend.lookup(namespace)?;
+ if is_su {
+ assert_eq!(context.to_str(), Ok("u:object_r:su_key:s0"));
+ } else {
+ assert_eq!(context.to_str(), Ok("u:object_r:shell_key:s0"));
+ }
+ Ok(())
+ }
+
+ #[test]
+ fn context_from_string() -> Result<()> {
+ let tctx = Context::new("u:object_r:keystore:s0").unwrap();
+ let sctx = Context::new("u:r:system_server:s0").unwrap();
+ check_access(&sctx, &tctx, "keystore2_key", "use")?;
+ Ok(())
+ }
+
+ mod perm {
+ use super::super::*;
+ use super::*;
+ use anyhow::Result;
+
+ /// check_key_perm(perm, privileged, priv_domain)
+ /// `perm` is a permission of the keystore2_key class and `privileged` is a boolean
+ /// indicating whether the permission is considered privileged.
+ /// Privileged permissions are expeced to be denied to `shell` users but granted
+ /// to the given priv_domain.
+ macro_rules! check_key_perm {
+ // "use" is a keyword and cannot be used as an identifier, but we must keep
+ // the permission string intact. So we map the identifier name on use_ while using
+ // the permission string "use". In all other cases we can simply use the stringified
+ // identifier as permission string.
+ (use, $privileged:expr) => {
+ check_key_perm!(use_, $privileged, "use");
+ };
+ ($perm:ident, $privileged:expr) => {
+ check_key_perm!($perm, $privileged, stringify!($perm));
+ };
+ ($perm:ident, $privileged:expr, $p_str:expr) => {
+ #[test]
+ fn $perm() -> Result<()> {
+ android_logger::init_once(
+ android_logger::Config::default()
+ .with_tag("keystore_selinux_tests")
+ .with_min_level(log::Level::Debug),
+ );
+ let scontext = Context::new("u:r:shell:s0")?;
+ let backend = KeystoreKeyBackend::new()?;
+ let tcontext = backend.lookup(SHELL_KEY_NAMESPACE)?;
+
+ if $privileged {
+ assert_eq!(
+ Some(&Error::perm()),
+ check_access(
+ &scontext,
+ &tcontext,
+ "keystore2_key",
+ $p_str
+ )
+ .err()
+ .unwrap()
+ .root_cause()
+ .downcast_ref::<Error>()
+ );
+ } else {
+ assert!(check_access(
+ &scontext,
+ &tcontext,
+ "keystore2_key",
+ $p_str
+ )
+ .is_ok());
+ }
+ Ok(())
+ }
+ };
+ }
+
+ check_key_perm!(manage_blob, true);
+ check_key_perm!(delete, false);
+ check_key_perm!(use_dev_id, true);
+ check_key_perm!(req_forced_op, true);
+ check_key_perm!(gen_unique_id, true);
+ check_key_perm!(grant, true);
+ check_key_perm!(get_info, false);
+ check_key_perm!(list, false);
+ check_key_perm!(rebind, false);
+ check_key_perm!(update, false);
+ check_key_perm!(use, false);
+
+ macro_rules! check_keystore_perm {
+ ($perm:ident) => {
+ #[test]
+ fn $perm() -> Result<()> {
+ let ks_context = Context::new("u:object_r:keystore:s0")?;
+ let priv_context = Context::new("u:r:system_server:s0")?;
+ let unpriv_context = Context::new("u:r:shell:s0")?;
+ assert!(check_access(
+ &priv_context,
+ &ks_context,
+ "keystore2",
+ stringify!($perm)
+ )
+ .is_ok());
+ assert_eq!(
+ Some(&Error::perm()),
+ check_access(&unpriv_context, &ks_context, "keystore2", stringify!($perm))
+ .err()
+ .unwrap()
+ .root_cause()
+ .downcast_ref::<Error>()
+ );
+ Ok(())
+ }
+ };
+ }
+
+ check_keystore_perm!(add_auth);
+ check_keystore_perm!(clear_ns);
+ check_keystore_perm!(get_state);
+ check_keystore_perm!(lock);
+ check_keystore_perm!(reset);
+ check_keystore_perm!(unlock);
+ }
+}