mirror of
https://github.com/rustfs/rustfs.git
synced 2026-10-04 04:21:35 +00:00
fix(connect): secure new offline state roots (#8218)
This commit is contained in:
@@ -28,6 +28,11 @@
|
||||
//! key-store abstraction that would have to describe both lifecycles.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
#[cfg(unix)]
|
||||
use std::{
|
||||
fs, io,
|
||||
os::unix::fs::{DirBuilderExt as _, MetadataExt as _, PermissionsExt as _},
|
||||
};
|
||||
|
||||
use super::super::identity::DeviceIdentity;
|
||||
use super::super::identity_store::{IdentityStore, StoreError};
|
||||
@@ -39,13 +44,18 @@ const OFFLINE_DIRECTORY: &str = "offline";
|
||||
/// The offline enrolment key of one deployment.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct OfflineKeyStore {
|
||||
#[cfg(unix)]
|
||||
state_root: PathBuf,
|
||||
inner: IdentityStore,
|
||||
}
|
||||
|
||||
impl OfflineKeyStore {
|
||||
pub fn new(directory: impl AsRef<Path>) -> Self {
|
||||
let state_root = directory.as_ref().to_path_buf();
|
||||
Self {
|
||||
inner: IdentityStore::new(directory.as_ref().join(OFFLINE_DIRECTORY)),
|
||||
inner: IdentityStore::new(state_root.join(OFFLINE_DIRECTORY)),
|
||||
#[cfg(unix)]
|
||||
state_root,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,8 +75,106 @@ impl OfflineKeyStore {
|
||||
/// A second enrolment attempt returns the original key rather than minting a
|
||||
/// replacement: the operator may already be carrying a response for it, and
|
||||
/// two keys would mean the response and the device disagree about which one
|
||||
/// Connect pinned.
|
||||
/// Connect pinned. The state root must also satisfy the service IPC's
|
||||
/// owner-only 0700 boundary; an existing unsafe root is never repaired here.
|
||||
pub fn load_or_create(&self) -> Result<DeviceIdentity, StoreError> {
|
||||
#[cfg(unix)]
|
||||
ensure_private_state_root(&self.state_root)?;
|
||||
self.inner.load_or_create()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn ensure_private_state_root(path: &Path) -> Result<(), StoreError> {
|
||||
let mut builder = fs::DirBuilder::new();
|
||||
builder.recursive(true).mode(0o700);
|
||||
builder.create(path).map_err(|source| StoreError::Io {
|
||||
path: path.to_path_buf(),
|
||||
source,
|
||||
})?;
|
||||
|
||||
let metadata = fs::symlink_metadata(path).map_err(|source| StoreError::Io {
|
||||
path: path.to_path_buf(),
|
||||
source,
|
||||
})?;
|
||||
if metadata.file_type().is_symlink()
|
||||
|| !metadata.is_dir()
|
||||
|| metadata.uid() != process_uid()
|
||||
|| metadata.permissions().mode() & 0o7777 != 0o700
|
||||
{
|
||||
return Err(StoreError::Io {
|
||||
path: path.to_path_buf(),
|
||||
source: io::Error::new(
|
||||
io::ErrorKind::PermissionDenied,
|
||||
"offline state root must be a real owner-owned directory with mode 0700",
|
||||
),
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[allow(unsafe_code)]
|
||||
fn process_uid() -> u32 {
|
||||
// SAFETY: geteuid has no pointer arguments or caller preconditions.
|
||||
unsafe { libc::geteuid() }
|
||||
}
|
||||
|
||||
#[cfg(all(test, unix))]
|
||||
mod tests {
|
||||
use std::{
|
||||
fs,
|
||||
os::unix::fs::{PermissionsExt as _, symlink},
|
||||
};
|
||||
|
||||
use super::OfflineKeyStore;
|
||||
|
||||
#[test]
|
||||
fn first_enrollment_creates_private_state_root() {
|
||||
let temporary = tempfile::tempdir().unwrap();
|
||||
let state = temporary.path().join("connect");
|
||||
let store = OfflineKeyStore::new(&state);
|
||||
|
||||
store.load_or_create().unwrap();
|
||||
|
||||
assert_eq!(fs::metadata(&state).unwrap().permissions().mode() & 0o7777, 0o700);
|
||||
assert_eq!(fs::metadata(store.key_path()).unwrap().permissions().mode() & 0o7777, 0o600);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn existing_public_state_root_is_rejected_without_creating_or_replacing_a_key() {
|
||||
let temporary = tempfile::tempdir().unwrap();
|
||||
let state = temporary.path().join("connect");
|
||||
fs::create_dir(&state).unwrap();
|
||||
fs::set_permissions(&state, fs::Permissions::from_mode(0o755)).unwrap();
|
||||
let store = OfflineKeyStore::new(&state);
|
||||
|
||||
let error = store.load_or_create().err().expect("public state root must fail closed");
|
||||
assert!(error.to_string().contains("mode 0700"));
|
||||
assert!(!store.key_path().exists());
|
||||
assert_eq!(fs::metadata(&state).unwrap().permissions().mode() & 0o7777, 0o755);
|
||||
|
||||
fs::set_permissions(&state, fs::Permissions::from_mode(0o700)).unwrap();
|
||||
store.load_or_create().unwrap();
|
||||
let original = fs::read(store.key_path()).unwrap();
|
||||
fs::set_permissions(&state, fs::Permissions::from_mode(0o755)).unwrap();
|
||||
|
||||
assert!(store.load_or_create().is_err());
|
||||
assert_eq!(fs::read(store.key_path()).unwrap(), original);
|
||||
assert_eq!(fs::metadata(&state).unwrap().permissions().mode() & 0o7777, 0o755);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn symbolic_state_root_is_rejected_before_key_generation() {
|
||||
let temporary = tempfile::tempdir().unwrap();
|
||||
let target = temporary.path().join("target");
|
||||
fs::create_dir(&target).unwrap();
|
||||
fs::set_permissions(&target, fs::Permissions::from_mode(0o700)).unwrap();
|
||||
let state = temporary.path().join("connect");
|
||||
symlink(&target, &state).unwrap();
|
||||
let store = OfflineKeyStore::new(&state);
|
||||
|
||||
assert!(store.load_or_create().is_err());
|
||||
assert!(!target.join("offline/device.key").exists());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user