Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion crates/agent-artifact-admission/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ pub fn load_config(path: &Path) -> Result<AdmissionServiceConfig, ConfigError> {

/// Load the bounded credentials document and return its validated bearer token.
pub fn load_admin_token(path: &Path) -> Result<String, ConfigError> {
let bytes = read_bounded(path, MAX_CREDENTIAL_FILE_BYTES)?;
let bytes = read_credential_bounded(path, MAX_CREDENTIAL_FILE_BYTES)?;
let credential: CredentialFile =
serde_json::from_slice(&bytes).map_err(|_| ConfigError::InvalidJson)?;
validate_admin_token(&credential.admin_token)?;
Expand Down Expand Up @@ -244,8 +244,38 @@ fn valid_executable(value: &str) -> bool {
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.' | b'+'))
}

fn read_credential_bounded(path: &Path, maximum_bytes: u64) -> Result<Vec<u8>, ConfigError> {
let file = File::open(path).map_err(|_| ConfigError::Io)?;
validate_credential_file_permissions(&file)?;
read_open_file_bounded(file, maximum_bytes)
}

#[cfg(unix)]
fn validate_credential_file_permissions(file: &File) -> Result<(), ConfigError> {
use std::os::unix::fs::PermissionsExt;

let mode = file
.metadata()
.map_err(|_| ConfigError::Io)?
.permissions()
.mode();
if mode & 0o077 != 0 {
return Err(ConfigError::InvalidCredential);
}
Ok(())
}

#[cfg(not(unix))]
fn validate_credential_file_permissions(_file: &File) -> Result<(), ConfigError> {
Err(ConfigError::InvalidCredential)
}

fn read_bounded(path: &Path, maximum_bytes: u64) -> Result<Vec<u8>, ConfigError> {
let file = File::open(path).map_err(|_| ConfigError::Io)?;
read_open_file_bounded(file, maximum_bytes)
}

fn read_open_file_bounded(file: File, maximum_bytes: u64) -> Result<Vec<u8>, ConfigError> {
let mut bytes = Vec::new();
file.take(maximum_bytes + 1)
.read_to_end(&mut bytes)
Expand Down
38 changes: 38 additions & 0 deletions crates/agent-artifact-admission/tests/cli_contract.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use std::fs;
use std::time::{SystemTime, UNIX_EPOCH};

#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;

use wardnet_agent_artifact_admission::{
AdmissionPolicy, AdmissionServiceConfig, ApprovedArtifact, ApprovedManifest, CredentialFile,
load_admin_token, load_config, parse_cli_args, validate_service_config,
Expand Down Expand Up @@ -182,6 +185,9 @@ fn loaders_are_bounded_strict_and_do_not_accept_short_credentials() {
serde_json::to_vec(&credential).expect("credential must serialize"),
)
.expect("credential fixture must write");
#[cfg(unix)]
fs::set_permissions(&credential_path, fs::Permissions::from_mode(0o600))
.expect("credential fixture must be owner-only");
assert_eq!(
load_admin_token(&credential_path).expect("valid credential must load"),
credential.admin_token
Expand All @@ -204,3 +210,35 @@ fn loaders_are_bounded_strict_and_do_not_accept_short_credentials() {
let _ = fs::remove_file(config_path);
let _ = fs::remove_file(credential_path);
}

#[cfg(unix)]
#[test]
fn credential_loader_rejects_group_or_other_permissions() {
let credential_path = temp_path("credential-permissions");
let credential = CredentialFile {
admin_token: "0123456789abcdef0123456789abcdef".to_string(),
};
let encoded = serde_json::to_vec(&credential).expect("credential must serialize");
fs::write(&credential_path, &encoded).expect("credential fixture must write");

for secure_mode in [0o600, 0o400] {
fs::set_permissions(&credential_path, fs::Permissions::from_mode(secure_mode))
.expect("secure credential mode must apply");
assert_eq!(
load_admin_token(&credential_path).expect("owner-only credential must load"),
credential.admin_token,
"mode {secure_mode:o}"
);
}

for unsafe_mode in [0o640, 0o604, 0o620, 0o602, 0o610, 0o601] {
fs::set_permissions(&credential_path, fs::Permissions::from_mode(unsafe_mode))
.expect("unsafe credential mode must apply");
assert!(
load_admin_token(&credential_path).is_err(),
"credential mode {unsafe_mode:o} exposed authority outside the owner boundary"
);
}

let _ = fs::remove_file(credential_path);
}
Loading