authfs: Support binder-backed file source
This change adds remote file support to authfs. This allows a process to
read a remote file through a local path with transparent fs-verity
verification.
This is supposed to work across VM boundary, but before the remote
binder is ready, this change uses local binder.
Test: Shell #1
$ adb shell 'exec
9</system/bin/sh
8</data/local/tmp/input.4m
7</data/local/tmp/input.4m.merkle_dump
6</data/local/tmp/input.4m.fsv_sig
fd_server
--ro-fds 9
--ro-fds 8:7:6'`
Shell #2
$ adb push tools/device-test.sh /data/local/tmp/ && \
adb shell /data/local/tmp/device-test.sh
Change-Id: Ia69fae548b83ff3ba572f4a496a7cbcca518cbef
diff --git a/authfs/Android.bp b/authfs/Android.bp
index 3fc4504..9f7be93 100644
--- a/authfs/Android.bp
+++ b/authfs/Android.bp
@@ -10,6 +10,7 @@
],
edition: "2018",
rustlibs: [
+ "authfs_aidl_interface-rust",
"libanyhow",
"libauthfs_crypto_bindgen",
"libcfg_if",
diff --git a/authfs/src/fusefs.rs b/authfs/src/fusefs.rs
index 484aad4..0dfd0af 100644
--- a/authfs/src/fusefs.rs
+++ b/authfs/src/fusefs.rs
@@ -32,6 +32,7 @@
use crate::common::{divide_roundup, COMMON_PAGE_SIZE};
use crate::fsverity::FsverityChunkedFileReader;
use crate::reader::{ChunkedFileReader, ReadOnlyDataByChunk};
+use crate::remote_file::{RemoteChunkedFileReader, RemoteFsverityMerkleTreeReader};
// We're reading the backing file by chunk, so setting the block size to be the same.
const BLOCK_SIZE: usize = COMMON_PAGE_SIZE as usize;
@@ -41,6 +42,9 @@
pub type Inode = u64;
type Handle = u64;
+type RemoteFsverityChunkedFileReader =
+ FsverityChunkedFileReader<RemoteChunkedFileReader, RemoteFsverityMerkleTreeReader>;
+
// A debug only type where everything are stored as local files.
type FileBackedFsverityChunkedFileReader =
FsverityChunkedFileReader<ChunkedFileReader, ChunkedFileReader>;
@@ -48,6 +52,8 @@
pub enum FileConfig {
LocalVerifiedFile(FileBackedFsverityChunkedFileReader, u64),
LocalUnverifiedFile(ChunkedFileReader, u64),
+ RemoteVerifiedFile(RemoteFsverityChunkedFileReader, u64),
+ RemoteUnverifiedFile(RemoteChunkedFileReader, u64),
}
struct AuthFs {
@@ -204,7 +210,9 @@
let inode = num.parse::<Inode>().map_err(|_| io::Error::from_raw_os_error(libc::ENOENT))?;
let st = match self.get_file_config(&inode)? {
FileConfig::LocalVerifiedFile(_, file_size)
- | FileConfig::LocalUnverifiedFile(_, file_size) => create_stat(inode, *file_size)?,
+ | FileConfig::LocalUnverifiedFile(_, file_size)
+ | FileConfig::RemoteUnverifiedFile(_, file_size)
+ | FileConfig::RemoteVerifiedFile(_, file_size) => create_stat(inode, *file_size)?,
};
Ok(Entry {
inode,
@@ -224,7 +232,9 @@
Ok((
match self.get_file_config(&inode)? {
FileConfig::LocalVerifiedFile(_, file_size)
- | FileConfig::LocalUnverifiedFile(_, file_size) => create_stat(inode, *file_size)?,
+ | FileConfig::LocalUnverifiedFile(_, file_size)
+ | FileConfig::RemoteUnverifiedFile(_, file_size)
+ | FileConfig::RemoteVerifiedFile(_, file_size) => create_stat(inode, *file_size)?,
},
DEFAULT_METADATA_TIMEOUT,
))
@@ -239,13 +249,13 @@
// Since file handle is not really used in later operations (which use Inode directly),
// return None as the handle..
match self.get_file_config(&inode)? {
- FileConfig::LocalVerifiedFile(_, _) => {
+ FileConfig::LocalVerifiedFile(_, _) | FileConfig::RemoteVerifiedFile(_, _) => {
check_access_mode(flags, libc::O_RDONLY)?;
// Once verified, and only if verified, the file content can be cached. This is not
// really needed for a local file, but is the behavior of RemoteVerifiedFile later.
Ok((None, fuse::sys::OpenOptions::KEEP_CACHE))
}
- FileConfig::LocalUnverifiedFile(_, _) => {
+ FileConfig::LocalUnverifiedFile(_, _) | FileConfig::RemoteUnverifiedFile(_, _) => {
check_access_mode(flags, libc::O_RDONLY)?;
// Do not cache the content. This type of file is supposed to be verified using
// dm-verity. The filesystem mount over dm-verity already is already cached, so use
@@ -273,6 +283,12 @@
FileConfig::LocalUnverifiedFile(file, file_size) => {
read_chunks(w, file, *file_size, offset, size)
}
+ FileConfig::RemoteVerifiedFile(file, file_size) => {
+ read_chunks(w, file, *file_size, offset, size)
+ }
+ FileConfig::RemoteUnverifiedFile(file, file_size) => {
+ read_chunks(w, file, *file_size, offset, size)
+ }
}
}
}
diff --git a/authfs/src/main.rs b/authfs/src/main.rs
index 46e6fd8..74553f5 100644
--- a/authfs/src/main.rs
+++ b/authfs/src/main.rs
@@ -27,11 +27,12 @@
//! Regardless of the actual file name, the exposed file names through AuthFS are currently integer,
//! e.g. /mountpoint/42.
-use anyhow::{bail, Result};
+use anyhow::{anyhow, bail, Result};
use std::collections::BTreeMap;
use std::fs::File;
use std::io::Read;
use std::path::PathBuf;
+use std::sync::{Arc, Mutex};
use structopt::StructOpt;
mod auth;
@@ -40,18 +41,35 @@
mod fsverity;
mod fusefs;
mod reader;
+mod remote_file;
use auth::FakeAuthenticator;
use fsverity::FsverityChunkedFileReader;
use fusefs::{FileConfig, Inode};
use reader::ChunkedFileReader;
+use remote_file::{RemoteChunkedFileReader, RemoteFsverityMerkleTreeReader};
#[derive(StructOpt)]
-struct Options {
+struct Args {
/// Mount point of AuthFS.
#[structopt(parse(from_os_str))]
mount_point: PathBuf,
+ /// A verifiable read-only file. Can be multiple.
+ ///
+ /// For example, `--remote-verified-file 5:10:1234:/path/to/cert` tells the filesystem to
+ /// associate entry 5 with a remote file 10 of size 1234 bytes, and need to be verified against
+ /// the /path/to/cert.
+ #[structopt(long, parse(try_from_str = parse_remote_verified_file_option))]
+ remote_verified_file: Vec<RemoteVerifiedFileConfig>,
+
+ /// An unverifiable read-only file. Can be multiple.
+ ///
+ /// For example, `--remote-unverified-file 5:10:1234` tells the filesystem to associate entry 5
+ /// with a remote file 10 of size 1234 bytes.
+ #[structopt(long, parse(try_from_str = parse_remote_unverified_file_option))]
+ remote_unverified_file: Vec<RemoteUnverifiedFileConfig>,
+
/// Debug only. A readonly file to be protected by fs-verity. Can be multiple.
#[structopt(long, parse(try_from_str = parse_local_verified_file_option))]
local_verified_file: Vec<LocalVerifiedFileConfig>,
@@ -61,28 +79,91 @@
local_unverified_file: Vec<LocalUnverifiedFileConfig>,
}
+struct RemoteVerifiedFileConfig {
+ ino: Inode,
+
+ /// ID to refer to the remote file.
+ remote_id: i32,
+
+ /// Expected size of the remote file. Necessary for signature check and Merkle tree
+ /// verification.
+ file_size: u64,
+
+ /// Certificate to verify the authenticity of the file's fs-verity signature.
+ /// TODO(170494765): Implement PKCS#7 signature verification.
+ _certificate_path: PathBuf,
+}
+
+struct RemoteUnverifiedFileConfig {
+ ino: Inode,
+
+ /// ID to refer to the remote file.
+ remote_id: i32,
+
+ /// Expected size of the remote file.
+ file_size: u64,
+}
+
struct LocalVerifiedFileConfig {
ino: Inode,
+
+ /// Local path of the backing file.
file_path: PathBuf,
+
+ /// Local path of the backing file's fs-verity Merkle tree dump.
merkle_tree_dump_path: PathBuf,
+
+ /// Local path of fs-verity signature for the backing file.
signature_path: PathBuf,
+
+ /// Certificate to verify the authenticity of the file's fs-verity signature.
+ /// TODO(170494765): Implement PKCS#7 signature verification.
+ _certificate_path: PathBuf,
}
struct LocalUnverifiedFileConfig {
ino: Inode,
+
+ /// Local path of the backing file.
file_path: PathBuf,
}
-fn parse_local_verified_file_option(option: &str) -> Result<LocalVerifiedFileConfig> {
+fn parse_remote_verified_file_option(option: &str) -> Result<RemoteVerifiedFileConfig> {
let strs: Vec<&str> = option.split(':').collect();
if strs.len() != 4 {
bail!("Invalid option: {}", option);
}
+ Ok(RemoteVerifiedFileConfig {
+ ino: strs[0].parse::<Inode>()?,
+ remote_id: strs[1].parse::<i32>()?,
+ file_size: strs[2].parse::<u64>()?,
+ _certificate_path: PathBuf::from(strs[3]),
+ })
+}
+
+fn parse_remote_unverified_file_option(option: &str) -> Result<RemoteUnverifiedFileConfig> {
+ let strs: Vec<&str> = option.split(':').collect();
+ if strs.len() != 3 {
+ bail!("Invalid option: {}", option);
+ }
+ Ok(RemoteUnverifiedFileConfig {
+ ino: strs[0].parse::<Inode>()?,
+ remote_id: strs[1].parse::<i32>()?,
+ file_size: strs[2].parse::<u64>()?,
+ })
+}
+
+fn parse_local_verified_file_option(option: &str) -> Result<LocalVerifiedFileConfig> {
+ let strs: Vec<&str> = option.split(':').collect();
+ if strs.len() != 5 {
+ bail!("Invalid option: {}", option);
+ }
Ok(LocalVerifiedFileConfig {
- ino: strs[0].parse::<Inode>().unwrap(),
+ ino: strs[0].parse::<Inode>()?,
file_path: PathBuf::from(strs[1]),
merkle_tree_dump_path: PathBuf::from(strs[2]),
signature_path: PathBuf::from(strs[3]),
+ _certificate_path: PathBuf::from(strs[4]),
})
}
@@ -92,11 +173,39 @@
bail!("Invalid option: {}", option);
}
Ok(LocalUnverifiedFileConfig {
- ino: strs[0].parse::<Inode>().unwrap(),
+ ino: strs[0].parse::<Inode>()?,
file_path: PathBuf::from(strs[1]),
})
}
+fn new_config_remote_verified_file(remote_id: i32, file_size: u64) -> Result<FileConfig> {
+ let service = remote_file::server::get_local_service();
+ let signature = service
+ .readFsveritySignature(remote_id)
+ .map_err(|e| anyhow!("Failed to read signature: {}", e.get_description()))?;
+
+ let service = Arc::new(Mutex::new(service));
+ let authenticator = FakeAuthenticator::always_succeed();
+ Ok(FileConfig::RemoteVerifiedFile(
+ FsverityChunkedFileReader::new(
+ &authenticator,
+ RemoteChunkedFileReader::new(Arc::clone(&service), remote_id),
+ file_size,
+ signature,
+ RemoteFsverityMerkleTreeReader::new(Arc::clone(&service), remote_id),
+ )?,
+ file_size,
+ ))
+}
+
+fn new_config_remote_unverified_file(remote_id: i32, file_size: u64) -> Result<FileConfig> {
+ let file_reader = RemoteChunkedFileReader::new(
+ Arc::new(Mutex::new(remote_file::server::get_local_service())),
+ remote_id,
+ );
+ Ok(FileConfig::RemoteUnverifiedFile(file_reader, file_size))
+}
+
fn new_config_local_verified_file(
protected_file: &PathBuf,
merkle_tree_dump: &PathBuf,
@@ -125,9 +234,23 @@
Ok(FileConfig::LocalUnverifiedFile(file_reader, file_size))
}
-fn prepare_file_pool(args: &Options) -> Result<BTreeMap<Inode, FileConfig>> {
+fn prepare_file_pool(args: &Args) -> Result<BTreeMap<Inode, FileConfig>> {
let mut file_pool = BTreeMap::new();
+ for config in &args.remote_verified_file {
+ file_pool.insert(
+ config.ino,
+ new_config_remote_verified_file(config.remote_id, config.file_size)?,
+ );
+ }
+
+ for config in &args.remote_unverified_file {
+ file_pool.insert(
+ config.ino,
+ new_config_remote_unverified_file(config.remote_id, config.file_size)?,
+ );
+ }
+
for config in &args.local_verified_file {
file_pool.insert(
config.ino,
@@ -147,8 +270,8 @@
}
fn main() -> Result<()> {
- let args = Options::from_args();
+ let args = Args::from_args();
let file_pool = prepare_file_pool(&args)?;
fusefs::loop_forever(file_pool, &args.mount_point)?;
- Ok(())
+ bail!("Unexpected exit after the handler loop")
}
diff --git a/authfs/src/remote_file.rs b/authfs/src/remote_file.rs
new file mode 100644
index 0000000..7c3d12e
--- /dev/null
+++ b/authfs/src/remote_file.rs
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 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.
+ */
+
+use std::convert::TryFrom;
+use std::io;
+use std::io::Write;
+use std::sync::{Arc, Mutex};
+
+use crate::reader::ReadOnlyDataByChunk;
+
+use authfs_aidl_interface::aidl::com::android::virt::fs::IVirtFdService;
+use authfs_aidl_interface::binder::Strong;
+
+type VirtFdService = Strong<dyn IVirtFdService::IVirtFdService>;
+
+pub mod server {
+ // TODO(victorhsieh): use remote binder.
+ pub fn get_local_service() -> super::VirtFdService {
+ let service_name = "authfs_fd_server";
+ authfs_aidl_interface::binder::get_interface(&service_name)
+ .expect("Cannot reach authfs_fd_server binder service")
+ }
+}
+
+pub struct RemoteChunkedFileReader {
+ // This needs to have Sync trait to be used in fuse::worker::start_message_loop.
+ service: Arc<Mutex<VirtFdService>>,
+ file_fd: i32,
+}
+
+impl RemoteChunkedFileReader {
+ pub fn new(service: Arc<Mutex<VirtFdService>>, file_fd: i32) -> Self {
+ RemoteChunkedFileReader { service, file_fd }
+ }
+}
+
+impl ReadOnlyDataByChunk for RemoteChunkedFileReader {
+ fn read_chunk(&self, chunk_index: u64, mut buf: &mut [u8]) -> io::Result<usize> {
+ let offset = i64::try_from(chunk_index * Self::CHUNK_SIZE)
+ .map_err(|_| io::Error::from_raw_os_error(libc::EOVERFLOW))?;
+
+ let service = Arc::clone(&self.service);
+ let chunk = service
+ .lock()
+ .unwrap()
+ .readFile(self.file_fd, offset, buf.len() as i32)
+ .map_err(|e| io::Error::new(io::ErrorKind::Other, e.get_description()))?;
+ buf.write(&chunk)
+ }
+}
+
+pub struct RemoteFsverityMerkleTreeReader {
+ // This needs to be a Sync to be used in fuse::worker::start_message_loop.
+ // TODO(victorhsieh): change to Strong<> once binder supports it.
+ service: Arc<Mutex<VirtFdService>>,
+ file_fd: i32,
+}
+
+impl RemoteFsverityMerkleTreeReader {
+ pub fn new(service: Arc<Mutex<VirtFdService>>, file_fd: i32) -> Self {
+ RemoteFsverityMerkleTreeReader { service, file_fd }
+ }
+}
+
+impl ReadOnlyDataByChunk for RemoteFsverityMerkleTreeReader {
+ fn read_chunk(&self, chunk_index: u64, mut buf: &mut [u8]) -> io::Result<usize> {
+ let offset = i64::try_from(chunk_index * Self::CHUNK_SIZE)
+ .map_err(|_| io::Error::from_raw_os_error(libc::EOVERFLOW))?;
+
+ let service = Arc::clone(&self.service);
+ let chunk = service
+ .lock()
+ .unwrap()
+ .readFsverityMerkleTree(self.file_fd, offset, buf.len() as i32)
+ .map_err(|e| io::Error::new(io::ErrorKind::Other, e.get_description()))?;
+ buf.write(&chunk)
+ }
+}
diff --git a/authfs/tools/device-test.sh b/authfs/tools/device-test.sh
new file mode 100755
index 0000000..5cf5f10
--- /dev/null
+++ b/authfs/tools/device-test.sh
@@ -0,0 +1,60 @@
+#!/system/bin/sh
+
+# TODO(victorhsieh): Create a standard Android test for continuous integration.
+#
+# How to run this test:
+#
+# Setup:
+# $ adb push testdata/input.4m* /data/local/tmp
+#
+# Shell 1:
+# $ adb shell 'cd /data/local/tmp && exec 9</system/bin/sh 8<input.4m 7<input.4m.merkle_dump 6<input.4m.fsv_sig 5<input.4m 4<input.4m.merkle_dump.bad 3<input.4m.fsv_sig fd_server --ro-fds 9 --ro-fds 8:7:6 --ro-fds 5:4:3'
+#
+# Shell 2:
+# $ adb push tools/device-test.sh /data/local/tmp/ && adb shell /data/local/tmp/device-test.sh
+
+# Run with -u to enter new namespace.
+if [[ $1 == "-u" ]]; then
+ exec unshare -mUr $0
+fi
+
+cd /data/local/tmp
+
+MOUNTPOINT=/data/local/tmp/authfs
+trap "umount ${MOUNTPOINT}" EXIT;
+mkdir -p ${MOUNTPOINT}
+
+size=$(du -b /system/bin/sh |awk '{print $1}')
+size2=$(du -b input.4m |awk '{print $1}')
+
+echo "Mounting authfs in background ..."
+
+# TODO(170494765): Replace /dev/null (currently not used) with a valid
+# certificate.
+authfs \
+ ${MOUNTPOINT} \
+ --local-verified-file 2:input.4m:input.4m.merkle_dump:input.4m.fsv_sig:/dev/null \
+ --local-verified-file 3:input.4k1:input.4k1.merkle_dump:input.4k1.fsv_sig:/dev/null \
+ --local-verified-file 4:input.4k:input.4k.merkle_dump:input.4k.fsv_sig:/dev/null \
+ --local-unverified-file 5:/system/bin/sh \
+ --remote-unverified-file 6:9:${size} \
+ --remote-verified-file 7:8:${size2}:/dev/null \
+ --remote-verified-file 8:5:${size2}:/dev/null \
+ &
+sleep 0.1
+
+echo "Accessing files in authfs ..."
+md5sum ${MOUNTPOINT}/2 input.4m
+echo
+md5sum ${MOUNTPOINT}/3 input.4k1
+echo
+md5sum ${MOUNTPOINT}/4 input.4k
+echo
+md5sum ${MOUNTPOINT}/5 /system/bin/sh
+md5sum ${MOUNTPOINT}/6
+echo
+md5sum ${MOUNTPOINT}/7 input.4m
+echo
+echo Checking error cases...
+cat /data/local/tmp/authfs/8 2>&1 |grep -q ": I/O error" || echo "Failed to catch the problem"
+echo "Done!"