Skip to main content

appcore_security/
secret_file.rs

1//! Hardened deployment-local file secret resolution.
2
3use crate::{SecretBytes, SecretResolver, SecurityError, SecurityResult, SecuritySecretRef};
4use std::fs::{self, File, OpenOptions};
5use std::io::Read;
6use std::path::{Component, Path, PathBuf};
7
8const MAX_FILE_SECRET_BYTES: u64 = 65_536;
9
10/// Resolves relative security references below one owner-only root.
11#[derive(Debug, Clone)]
12pub struct FileSecretResolver {
13    root: PathBuf,
14}
15
16impl FileSecretResolver {
17    /// Creates a resolver rooted at `root`.
18    pub fn new(root: impl Into<PathBuf>) -> Self {
19        Self { root: root.into() }
20    }
21}
22
23impl SecretResolver for FileSecretResolver {
24    fn resolve(&self, reference: &SecuritySecretRef) -> SecurityResult<SecretBytes> {
25        let relative = validate_relative_reference(&reference.0)?;
26        validate_private_directory(&self.root)?;
27        let canonical_root = fs::canonicalize(&self.root).map_err(unavailable)?;
28        let path = self.root.join(relative);
29        reject_symlink_components(&self.root, relative)?;
30        let canonical_path = fs::canonicalize(&path).map_err(unavailable)?;
31        if !canonical_path.starts_with(&canonical_root) {
32            return Err(SecurityError::InvalidSecretRef);
33        }
34        let mut file = open_no_follow(&path)?;
35        validate_private_file(&file)?;
36        let length = file.metadata().map_err(unavailable)?.len();
37        if length == 0 || length > MAX_FILE_SECRET_BYTES {
38            return Err(SecurityError::SecretUnavailable);
39        }
40        let mut value = Vec::with_capacity(length as usize);
41        file.read_to_end(&mut value).map_err(unavailable)?;
42        Ok(SecretBytes::new(value))
43    }
44}
45
46fn validate_relative_reference(value: &str) -> SecurityResult<&Path> {
47    let path = Path::new(value);
48    if value.is_empty()
49        || path.components().any(|component| {
50            matches!(
51                component,
52                Component::ParentDir | Component::RootDir | Component::Prefix(_)
53            )
54        })
55    {
56        return Err(SecurityError::InvalidSecretRef);
57    }
58    Ok(path)
59}
60
61fn reject_symlink_components(root: &Path, relative: &Path) -> SecurityResult<()> {
62    let mut current = root.to_path_buf();
63    for component in relative.components() {
64        current.push(component);
65        let metadata = fs::symlink_metadata(&current).map_err(unavailable)?;
66        if metadata.file_type().is_symlink() || is_reparse_point(&metadata) {
67            return Err(SecurityError::InvalidSecretRef);
68        }
69    }
70    Ok(())
71}
72
73#[cfg(windows)]
74fn is_reparse_point(metadata: &fs::Metadata) -> bool {
75    use std::os::windows::fs::MetadataExt;
76    use windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_REPARSE_POINT;
77    metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0
78}
79
80#[cfg(not(windows))]
81fn is_reparse_point(_metadata: &fs::Metadata) -> bool {
82    false
83}
84
85#[cfg(unix)]
86fn open_no_follow(path: &Path) -> SecurityResult<File> {
87    use std::os::unix::fs::OpenOptionsExt;
88    OpenOptions::new()
89        .read(true)
90        .custom_flags(libc::O_NOFOLLOW)
91        .open(path)
92        .map_err(unavailable)
93}
94
95#[cfg(windows)]
96fn open_no_follow(path: &Path) -> SecurityResult<File> {
97    use std::os::windows::fs::OpenOptionsExt;
98    use windows_sys::Win32::Storage::FileSystem::FILE_FLAG_OPEN_REPARSE_POINT;
99    OpenOptions::new()
100        .read(true)
101        .custom_flags(FILE_FLAG_OPEN_REPARSE_POINT)
102        .open(path)
103        .map_err(unavailable)
104}
105
106#[cfg(all(not(unix), not(windows)))]
107fn open_no_follow(_path: &Path) -> SecurityResult<File> {
108    Err(SecurityError::SecretUnavailable)
109}
110
111#[cfg(unix)]
112fn validate_private_directory(path: &Path) -> SecurityResult<()> {
113    use std::os::unix::fs::{MetadataExt, PermissionsExt};
114    let metadata = fs::symlink_metadata(path).map_err(unavailable)?;
115    if metadata.file_type().is_symlink()
116        || !metadata.is_dir()
117        || metadata.permissions().mode() & 0o077 != 0
118        || metadata.uid() != unsafe { libc::geteuid() }
119    {
120        return Err(SecurityError::InvalidSecretRef);
121    }
122    Ok(())
123}
124
125#[cfg(windows)]
126fn validate_private_directory(path: &Path) -> SecurityResult<()> {
127    let metadata = fs::symlink_metadata(path).map_err(unavailable)?;
128    if metadata.file_type().is_symlink() || is_reparse_point(&metadata) || !metadata.is_dir() {
129        return Err(SecurityError::InvalidSecretRef);
130    }
131    windows_acl::validate_path_owner_acl(path)
132}
133
134#[cfg(all(not(unix), not(windows)))]
135fn validate_private_directory(_path: &Path) -> SecurityResult<()> {
136    Err(SecurityError::SecretUnavailable)
137}
138
139#[cfg(unix)]
140fn validate_private_file(file: &File) -> SecurityResult<()> {
141    use std::os::unix::fs::{MetadataExt, PermissionsExt};
142    let metadata = file.metadata().map_err(unavailable)?;
143    if !metadata.is_file()
144        || metadata.permissions().mode() & 0o077 != 0
145        || metadata.uid() != unsafe { libc::geteuid() }
146    {
147        return Err(SecurityError::InvalidSecretRef);
148    }
149    Ok(())
150}
151
152#[cfg(windows)]
153fn validate_private_file(file: &File) -> SecurityResult<()> {
154    let metadata = file.metadata().map_err(unavailable)?;
155    if !metadata.is_file() || is_reparse_point(&metadata) {
156        return Err(SecurityError::InvalidSecretRef);
157    }
158    windows_acl::validate_file_owner_acl(file)
159}
160
161#[cfg(all(not(unix), not(windows)))]
162fn validate_private_file(_file: &File) -> SecurityResult<()> {
163    Err(SecurityError::SecretUnavailable)
164}
165
166fn unavailable<T>(_error: T) -> SecurityError {
167    SecurityError::SecretUnavailable
168}
169
170#[cfg(windows)]
171mod windows_acl {
172    use super::*;
173    use std::ffi::c_void;
174    use std::os::windows::ffi::OsStrExt;
175    use std::os::windows::io::AsRawHandle;
176    use std::ptr;
177    use windows_sys::Win32::Foundation::{CloseHandle, LocalFree, ERROR_SUCCESS, HANDLE};
178    use windows_sys::Win32::Security::Authorization::{
179        GetNamedSecurityInfoW, GetSecurityInfo, SE_FILE_OBJECT,
180    };
181    use windows_sys::Win32::Security::{
182        EqualSid, GetAce, GetTokenInformation, TokenUser, ACCESS_ALLOWED_ACE, ACL,
183        DACL_SECURITY_INFORMATION, OWNER_SECURITY_INFORMATION, PSECURITY_DESCRIPTOR, PSID,
184        TOKEN_QUERY, TOKEN_USER,
185    };
186    use windows_sys::Win32::System::SystemServices::{
187        ACCESS_ALLOWED_ACE_TYPE, ACCESS_ALLOWED_CALLBACK_ACE_TYPE,
188        ACCESS_ALLOWED_CALLBACK_OBJECT_ACE_TYPE, ACCESS_ALLOWED_OBJECT_ACE_TYPE,
189    };
190    use windows_sys::Win32::System::Threading::{GetCurrentProcess, OpenProcessToken};
191
192    pub(super) fn validate_path_owner_acl(path: &Path) -> SecurityResult<()> {
193        let wide = path
194            .as_os_str()
195            .encode_wide()
196            .chain(std::iter::once(0))
197            .collect::<Vec<_>>();
198        let mut owner = ptr::null_mut();
199        let mut dacl = ptr::null_mut();
200        let mut descriptor = ptr::null_mut();
201        let result = unsafe {
202            GetNamedSecurityInfoW(
203                wide.as_ptr(),
204                SE_FILE_OBJECT,
205                OWNER_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION,
206                &mut owner,
207                ptr::null_mut(),
208                &mut dacl,
209                ptr::null_mut(),
210                &mut descriptor,
211            )
212        };
213        validate_security_result(result, descriptor, owner, dacl)
214    }
215
216    pub(super) fn validate_file_owner_acl(file: &File) -> SecurityResult<()> {
217        let mut owner = ptr::null_mut();
218        let mut dacl = ptr::null_mut();
219        let mut descriptor = ptr::null_mut();
220        let result = unsafe {
221            GetSecurityInfo(
222                file.as_raw_handle() as HANDLE,
223                SE_FILE_OBJECT,
224                OWNER_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION,
225                &mut owner,
226                ptr::null_mut(),
227                &mut dacl,
228                ptr::null_mut(),
229                &mut descriptor,
230            )
231        };
232        validate_security_result(result, descriptor, owner, dacl)
233    }
234
235    fn validate_security_result(
236        result: u32,
237        descriptor: PSECURITY_DESCRIPTOR,
238        owner: PSID,
239        dacl: *mut ACL,
240    ) -> SecurityResult<()> {
241        if result != ERROR_SUCCESS || descriptor.is_null() {
242            return Err(SecurityError::SecretUnavailable);
243        }
244        let descriptor = SecurityDescriptor(descriptor);
245        let validation = validate_owner_and_acl(owner, dacl);
246        drop(descriptor);
247        validation
248    }
249
250    fn validate_owner_and_acl(owner: PSID, dacl: *mut ACL) -> SecurityResult<()> {
251        if owner.is_null() || dacl.is_null() {
252            return Err(SecurityError::InvalidSecretRef);
253        }
254        let user = current_user()?;
255        if unsafe { EqualSid(owner, user.sid()?) } == 0 {
256            return Err(SecurityError::InvalidSecretRef);
257        }
258        validate_owner_only_acl(dacl, owner)
259    }
260
261    fn validate_owner_only_acl(dacl: *mut ACL, owner: PSID) -> SecurityResult<()> {
262        let mut owner_allowed = false;
263        let ace_count = unsafe { (*dacl).AceCount };
264        for index in 0..u32::from(ace_count) {
265            let mut raw_ace: *mut c_void = ptr::null_mut();
266            if unsafe { GetAce(dacl, index, &mut raw_ace) } == 0 || raw_ace.is_null() {
267                return Err(SecurityError::SecretUnavailable);
268            }
269            let ace_type = unsafe { (*(raw_ace.cast::<ACCESS_ALLOWED_ACE>())).Header.AceType };
270            if ace_type == ACCESS_ALLOWED_ACE_TYPE as u8 {
271                let ace = raw_ace.cast::<ACCESS_ALLOWED_ACE>();
272                let sid = unsafe { ptr::addr_of_mut!((*ace).SidStart).cast::<c_void>() };
273                if unsafe { EqualSid(owner, sid) } == 0 {
274                    return Err(SecurityError::InvalidSecretRef);
275                }
276                owner_allowed = true;
277            } else if is_other_allow_ace(ace_type) {
278                return Err(SecurityError::InvalidSecretRef);
279            }
280        }
281        if owner_allowed {
282            Ok(())
283        } else {
284            Err(SecurityError::InvalidSecretRef)
285        }
286    }
287
288    fn is_other_allow_ace(ace_type: u8) -> bool {
289        [
290            ACCESS_ALLOWED_OBJECT_ACE_TYPE,
291            ACCESS_ALLOWED_CALLBACK_ACE_TYPE,
292            ACCESS_ALLOWED_CALLBACK_OBJECT_ACE_TYPE,
293        ]
294        .contains(&u32::from(ace_type))
295    }
296
297    fn current_user() -> SecurityResult<TokenUser> {
298        let mut token: HANDLE = ptr::null_mut();
299        if unsafe { OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token) } == 0 {
300            return Err(SecurityError::SecretUnavailable);
301        }
302        let token = TokenHandle(token);
303        let mut required = 0;
304        unsafe {
305            GetTokenInformation(token.0, TokenUser, ptr::null_mut(), 0, &mut required);
306        }
307        if required == 0 {
308            return Err(SecurityError::SecretUnavailable);
309        }
310        let mut buffer = vec![0_u8; required as usize];
311        if unsafe {
312            GetTokenInformation(
313                token.0,
314                TokenUser,
315                buffer.as_mut_ptr().cast(),
316                required,
317                &mut required,
318            )
319        } == 0
320        {
321            return Err(SecurityError::SecretUnavailable);
322        }
323        Ok(TokenUser { buffer })
324    }
325
326    struct TokenUser {
327        buffer: Vec<u8>,
328    }
329
330    impl TokenUser {
331        fn sid(&self) -> SecurityResult<PSID> {
332            if self.buffer.len() < std::mem::size_of::<TOKEN_USER>() {
333                return Err(SecurityError::SecretUnavailable);
334            }
335            // SAFETY: GetTokenInformation initialized at least TOKEN_USER bytes.
336            // The byte buffer has no TOKEN_USER alignment guarantee, so use an
337            // unaligned value read instead of constructing a misaligned reference.
338            let user = unsafe { self.buffer.as_ptr().cast::<TOKEN_USER>().read_unaligned() };
339            Ok(user.User.Sid)
340        }
341    }
342
343    struct TokenHandle(HANDLE);
344
345    impl Drop for TokenHandle {
346        fn drop(&mut self) {
347            unsafe {
348                CloseHandle(self.0);
349            }
350        }
351    }
352
353    struct SecurityDescriptor(PSECURITY_DESCRIPTOR);
354
355    impl Drop for SecurityDescriptor {
356        fn drop(&mut self) {
357            unsafe {
358                LocalFree(self.0);
359            }
360        }
361    }
362}