summaryrefslogtreecommitdiff
path: root/keystore2/test_utils
diff options
context:
space:
mode:
authorJanis Danisevskis <jdanis@google.com>2021-09-20 15:44:06 -0700
committerJanis Danisevskis <jdanis@google.com>2021-09-21 13:29:39 -0700
commita578d3998f5dd0d3f0b81e7d62badd2735f9228a (patch)
tree3b82cc75957903aba289cd3544abf13b92201704 /keystore2/test_utils
parent1efb6663520ad3d5e38cbaf3f17d2192646e570c (diff)
downloadsecurity-a578d3998f5dd0d3f0b81e7d62badd2735f9228a.tar.gz
Keystore 2.0: Add run_as to keystore2_test_utils
The run_as function allows a test with sufficient privileges to run a closure as different identity given by a tuple of UID, GID, and SELinux context. This is infrastructure in preparation for the keystore2 vts test. Test: keystore2_test_utils_test Bug: 182508302 Change-Id: Ic1923028e5bc4ca4b1112e34669d52687450fd14
Diffstat (limited to 'keystore2/test_utils')
-rw-r--r--keystore2/test_utils/AndroidTest.xml32
-rw-r--r--keystore2/test_utils/lib.rs2
-rw-r--r--keystore2/test_utils/run_as.rs191
3 files changed, 225 insertions, 0 deletions
diff --git a/keystore2/test_utils/AndroidTest.xml b/keystore2/test_utils/AndroidTest.xml
new file mode 100644
index 00000000..24e277a2
--- /dev/null
+++ b/keystore2/test_utils/AndroidTest.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+
+<configuration description="Config to run keystore2_test_utils_test device tests.">
+ <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer" />
+
+ <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
+ <option name="cleanup" value="true" />
+ <option
+ name="push"
+ value="keystore2_test_utils_test->/data/local/tmp/keystore2_test_utils_test"
+ />
+ </target_preparer>
+
+ <test class="com.android.tradefed.testtype.rust.RustBinaryTest" >
+ <option name="test-device-path" value="/data/local/tmp" />
+ <option name="module-name" value="keystore2_test_utils_test" />
+ </test>
+</configuration> \ No newline at end of file
diff --git a/keystore2/test_utils/lib.rs b/keystore2/test_utils/lib.rs
index 627af20c..a355544b 100644
--- a/keystore2/test_utils/lib.rs
+++ b/keystore2/test_utils/lib.rs
@@ -19,6 +19,8 @@ use std::io::ErrorKind;
use std::path::{Path, PathBuf};
use std::{env::temp_dir, ops::Deref};
+pub mod run_as;
+
/// Represents the lifecycle of a temporary directory for testing.
#[derive(Debug)]
pub struct TempDir {
diff --git a/keystore2/test_utils/run_as.rs b/keystore2/test_utils/run_as.rs
new file mode 100644
index 00000000..d42303d7
--- /dev/null
+++ b/keystore2/test_utils/run_as.rs
@@ -0,0 +1,191 @@
+// Copyright 2021, 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.
+
+//! This module is intended for testing access control enforcement of services such as keystore2,
+//! by assuming various identities with varying levels of privilege. Consequently, appropriate
+//! privileges are required, or the attempt will fail causing a panic.
+//! The `run_as` module provides the function `run_as`, which takes a UID, GID, an SELinux
+//! context, and a closure. The return type of the closure, which is also the return type of
+//! `run_as`, must implement `serde::Serialize` and `serde::Deserialize`.
+//! `run_as` forks, transitions to the given identity, and executes the closure in the newly
+//! forked process. If the closure returns, i.e., does not panic, the forked process exits with
+//! a status of `0`, and the return value is serialized and sent through a pipe to the parent where
+//! it gets deserialized and returned. The STDIO is not changed and the parent's panic handler
+//! remains unchanged. So if the closure panics, the panic message is printed on the parent's STDERR
+//! and the exit status is set to a non `0` value. The latter causes the parent to panic as well,
+//! and if run in a test context, the test to fail.
+
+use keystore2_selinux as selinux;
+use nix::sys::wait::{waitpid, WaitStatus};
+use nix::unistd::{
+ close, fork, pipe as nix_pipe, read as nix_read, setgid, setuid, write as nix_write,
+ ForkResult, Gid, Uid,
+};
+use serde::{de::DeserializeOwned, Serialize};
+use std::os::unix::io::RawFd;
+
+fn transition(se_context: selinux::Context, uid: Uid, gid: Gid) {
+ setgid(gid).expect("Failed to set GID. This test might need more privileges.");
+ setuid(uid).expect("Failed to set UID. This test might need more privileges.");
+
+ selinux::setcon(&se_context)
+ .expect("Failed to set SELinux context. This test might need more privileges.");
+}
+
+/// PipeReader is a simple wrapper around raw pipe file descriptors.
+/// It takes ownership of the file descriptor and closes it on drop. It provides `read_all`, which
+/// reads from the pipe into an expending vector, until no more data can be read.
+struct PipeReader(RawFd);
+
+impl PipeReader {
+ pub fn read_all(&self) -> Result<Vec<u8>, nix::Error> {
+ let mut buffer = [0u8; 128];
+ let mut result = Vec::<u8>::new();
+ loop {
+ let bytes = nix_read(self.0, &mut buffer)?;
+ if bytes == 0 {
+ return Ok(result);
+ }
+ result.extend_from_slice(&buffer[0..bytes]);
+ }
+ }
+}
+
+impl Drop for PipeReader {
+ fn drop(&mut self) {
+ close(self.0).expect("Failed to close reader pipe fd.");
+ }
+}
+
+/// PipeWriter is a simple wrapper around raw pipe file descriptors.
+/// It takes ownership of the file descriptor and closes it on drop. It provides `write`, which
+/// writes the given buffer into the pipe, returning the number of bytes written.
+struct PipeWriter(RawFd);
+
+impl PipeWriter {
+ pub fn write(&self, data: &[u8]) -> Result<usize, nix::Error> {
+ nix_write(self.0, data)
+ }
+}
+
+impl Drop for PipeWriter {
+ fn drop(&mut self) {
+ close(self.0).expect("Failed to close writer pipe fd.");
+ }
+}
+
+fn pipe() -> Result<(PipeReader, PipeWriter), nix::Error> {
+ let (read_fd, write_fd) = nix_pipe()?;
+ Ok((PipeReader(read_fd), PipeWriter(write_fd)))
+}
+
+/// Run the given closure in a new process running with the new identity given as
+/// `uid`, `gid`, and `se_context`.
+pub fn run_as<F, R>(se_context: &str, uid: Uid, gid: Gid, f: F) -> R
+where
+ R: Serialize + DeserializeOwned,
+ F: 'static + Send + FnOnce() -> R,
+{
+ let se_context =
+ selinux::Context::new(se_context).expect("Unable to construct selinux::Context.");
+ let (reader, writer) = pipe().expect("Failed to create pipe.");
+
+ match unsafe { fork() } {
+ Ok(ForkResult::Parent { child, .. }) => {
+ drop(writer);
+ let status = waitpid(child, None).expect("Failed while waiting for child.");
+ if let WaitStatus::Exited(_, 0) = status {
+ // Child exited successfully.
+ // Read the result from the pipe.
+ let serialized_result =
+ reader.read_all().expect("Failed to read result from child.");
+
+ // Deserialize the result and return it.
+ serde_cbor::from_slice(&serialized_result).expect("Failed to deserialize result.")
+ } else {
+ panic!("Child did not exit as expected {:?}", status);
+ }
+ }
+ Ok(ForkResult::Child) => {
+ // This will panic on error or insufficient privileges.
+ transition(se_context, uid, gid);
+
+ // Run the closure.
+ let result = f();
+
+ // Serialize the result of the closure.
+ let vec = serde_cbor::to_vec(&result).expect("Result serialization failed");
+
+ // Send the result to the parent using the pipe.
+ writer.write(&vec).expect("Failed to send serialized result to parent.");
+
+ // Set exit status to `0`.
+ std::process::exit(0);
+ }
+ Err(errno) => {
+ panic!("Failed to fork: {:?}", errno);
+ }
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+ use keystore2_selinux as selinux;
+ use nix::unistd::{getgid, getuid};
+ use serde::{Deserialize, Serialize};
+
+ /// This test checks that the closure does not produce an exit status of `0` when run inside a
+ /// test and the closure panics. This would mask test failures as success.
+ #[test]
+ #[should_panic]
+ fn test_run_as_panics_on_closure_panic() {
+ run_as(selinux::getcon().unwrap().to_str().unwrap(), getuid(), getgid(), || {
+ panic!("Closure must panic.")
+ });
+ }
+
+ static TARGET_UID: Uid = Uid::from_raw(10020);
+ static TARGET_GID: Gid = Gid::from_raw(10020);
+ static TARGET_CTX: &str = "u:r:untrusted_app:s0:c91,c256,c10,c20";
+
+ /// Tests that the closure is running as the target identity.
+ #[test]
+ fn test_transition_to_untrusted_app() {
+ run_as(TARGET_CTX, TARGET_UID, TARGET_GID, || {
+ assert_eq!(TARGET_UID, getuid());
+ assert_eq!(TARGET_GID, getgid());
+ assert_eq!(TARGET_CTX, selinux::getcon().unwrap().to_str().unwrap());
+ });
+ }
+
+ #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+ struct SomeResult {
+ a: u32,
+ b: u64,
+ c: String,
+ }
+
+ #[test]
+ fn test_serialized_result() {
+ let test_result = SomeResult {
+ a: 5,
+ b: 0xffffffffffffffff,
+ c: "supercalifragilisticexpialidocious".to_owned(),
+ };
+ let test_result_clone = test_result.clone();
+ let result = run_as(TARGET_CTX, TARGET_UID, TARGET_GID, || test_result_clone);
+ assert_eq!(test_result, result);
+ }
+}