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
2 changes: 2 additions & 0 deletions .github/workflows/windows-platform.yml
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ jobs:
-p semwright-daemon
- name: Secure Windows Driver workspace grants
run: cargo test --locked -p semwright-driver-host --test windows_secure_host -- secure_windows_driver_workspace --nocapture
- name: Secure Windows Driver secret grants
run: cargo test --locked -p semwright-driver-host --test windows_secure_host -- secure_windows_driver_secret --nocapture
- name: Secure Windows Plugin Host round-trip
run: cargo test --locked -p semwright-plugin-host --features test-tools --test windows_secure_host -- --nocapture
- name: Secure Windows external MCP round-trip
Expand Down
47 changes: 45 additions & 2 deletions crates/driver-host/src/bin/fixture.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use async_trait::async_trait;
use semwright_driver_sdk::{
Capability, Driver, DriverExecutionContext, DriverInterfaces, descriptor_digest, serve,
system_config_mount, workspace_mount,
Capability, Driver, DriverExecutionContext, DriverInterfaces, descriptor_digest, secret_mount,
serve, system_config_mount, workspace_mount,
};
use semwright_types::{
CommandDescriptor, Error, ErrorCode, Idempotency, JobArtifact, JobProgress, Result, Risk,
Expand Down Expand Up @@ -131,6 +131,36 @@ fn disconnect_capability() -> Capability {
}
}

fn secret_capability() -> Capability {
Capability {
descriptor: CommandDescriptor {
name: "driver.fixture.secret_probe".into(),
version: "1".into(),
description: "Read an owner-granted secret and probe mutation authority".into(),
input_schema: json!({"type":"object","additionalProperties":false}),
output_schema: json!({
"type":"object",
"properties":{
"read":{"type":"string"},
"write_ok":{"type":"boolean"}
},
"required":["read","write_ok"],
"additionalProperties":false
}),
requires: vec!["driver:fixture".into()],
risk: Risk::MutatingReversible,
idempotency: Idempotency::Idempotent,
timeout_ms: 2_000,
dry_run: false,
interactive_consent: false,
backends: vec!["driver:fixture".into()],
},
aliases: vec!["secret_probe".into()],
tags: vec!["fixture".into(), "conformance".into(), "secret".into()],
object_types: vec![],
}
}

fn long_capability() -> Capability {
Capability {
descriptor: CommandDescriptor {
Expand Down Expand Up @@ -188,6 +218,7 @@ impl Driver for Fixture {
capability(),
mount_capability(),
config_capability(),
secret_capability(),
long_capability(),
disconnect_capability(),
])
Expand All @@ -197,6 +228,7 @@ impl Driver for Fixture {
"driver.fixture.ping" => capability(),
"driver.fixture.mount_probe" => mount_capability(),
"driver.fixture.config_probe" => config_capability(),
"driver.fixture.secret_probe" => secret_capability(),
"driver.fixture.disconnect" => disconnect_capability(),
_ => {
return Err(Error::new(
Expand Down Expand Up @@ -233,6 +265,17 @@ impl Driver for Fixture {
let write_ok = std::fs::write(root.join("child.txt"), b"written").is_ok();
return Ok(json!({"read":read,"write_ok":write_ok}));
}
if command == "driver.fixture.secret_probe" {
if args.as_object().is_none_or(|args| !args.is_empty()) {
return Err(Error::invalid(
"fixture secret probe accepts an empty object",
));
}
let path = secret_mount("fixture-secret")?;
let read = std::fs::read_to_string(&path)?;
let write_ok = std::fs::write(&path, b"changed").is_ok();
return Ok(json!({"read":read,"write_ok":write_ok}));
}
if command == "driver.fixture.disconnect" {
if args.as_object().is_none_or(|args| !args.is_empty()) {
return Err(Error::invalid("fixture disconnect accepts an empty object"));
Expand Down
68 changes: 46 additions & 22 deletions crates/driver-host/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -326,32 +326,40 @@ async fn wait_for_operation_cpu_budget(
}

fn validate_secret_source(path: &Path) -> Result<()> {
if std::fs::canonicalize(path)? != path {
return Err(Error::new(
ErrorCode::PolicyDenied,
"Driver secret source must be canonical",
));
}
let metadata = std::fs::symlink_metadata(path)?;
if !metadata.is_file() || metadata.len() == 0 || metadata.len() > 4096 {
return Err(Error::new(
ErrorCode::PolicyDenied,
"Driver secret source must be a small regular file",
));
#[cfg(target_os = "windows")]
{
semwright_platform_services::verify_private_data_file(path, 4096)
}
#[cfg(unix)]

#[cfg(not(target_os = "windows"))]
{
if metadata.uid() != semwright_platform_services::current_uid()
|| metadata.mode() & 0o077 != 0
|| metadata.nlink() != 1
{
if std::fs::canonicalize(path)? != path {
return Err(Error::new(
ErrorCode::PolicyDenied,
"Driver secret source must be canonical",
));
}
let metadata = std::fs::symlink_metadata(path)?;
if !metadata.is_file() || metadata.len() == 0 || metadata.len() > 4096 {
return Err(Error::new(
ErrorCode::PermissionDenied,
"Driver secret source must be owner-only and single-linked",
ErrorCode::PolicyDenied,
"Driver secret source must be a small regular file",
));
}
#[cfg(unix)]
{
if metadata.uid() != semwright_platform_services::current_uid()
|| metadata.mode() & 0o077 != 0
|| metadata.nlink() != 1
{
return Err(Error::new(
ErrorCode::PermissionDenied,
"Driver secret source must be owner-only and single-linked",
));
}
}
Ok(())
}
Ok(())
}

fn validate_owner_permissions(
Expand Down Expand Up @@ -791,10 +799,10 @@ fn sandbox_spec_windows(
Mount, MountClass, ResourceLimits, SandboxKind, SandboxSpec,
};

if !manifest.secrets.is_empty() || !manifest.tools.is_empty() {
if !manifest.tools.is_empty() {
return Err(Error::new(
ErrorCode::SandboxDenied,
"Windows secret and tool grants remain fail-closed until their source-integrity and immutable-executable contracts are proven",
"Windows tool grants remain fail-closed until their immutable-executable contract is proven",
));
}
if manifest.loopback_port.is_some() {
Expand Down Expand Up @@ -852,6 +860,22 @@ fn sandbox_spec_windows(
})
.collect::<Result<Vec<_>>>()?,
);
mounts.extend(
manifest
.secrets
.iter()
.map(|secret| {
let grant = lookup(&secret.root)?;
Ok(Mount {
source: grant.path.clone(),
class: MountClass::Secret,
logical_name: secret.name.clone(),
read_only: true,
execute: false,
})
})
.collect::<Result<Vec<_>>>()?,
);
Ok(SandboxSpec {
kind: SandboxKind::Driver,
staged_executable: staged.into(),
Expand Down
174 changes: 172 additions & 2 deletions crates/driver-host/tests/windows_secure_host.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
use semwright_backend_api::{Context, Provider};
use semwright_driver_host::DriverProvider;
use semwright_driver_sdk::{
ApplicationMatch, DriverInterfaces, DriverMount, DriverResources, Manifest, SystemConfigMount,
Transport,
ApplicationMatch, DriverInterfaces, DriverMount, DriverResources, DriverSecretMount, Manifest,
SystemConfigMount, Transport,
};
use semwright_policy::FilesystemGrant;
use sha2::{Digest, Sha256};
Expand Down Expand Up @@ -139,6 +139,32 @@ fn grant_all_application_packages_modify(path: &Path) {
);
}

fn grant_all_application_packages_file_modify(path: &Path) {
let status = std::process::Command::new("icacls")
.arg(path)
.arg("/grant")
.arg("*S-1-15-2-1:(M)")
.status()
.expect("grant ALL APPLICATION PACKAGES file modify");
assert!(
status.success(),
"broad application-package secret mutation grant must succeed"
);
}

fn grant_all_application_packages_file_read(path: &Path) {
let status = std::process::Command::new("icacls")
.arg(path)
.arg("/grant")
.arg("*S-1-15-2-1:(R)")
.status()
.expect("grant ALL APPLICATION PACKAGES file read");
assert!(
status.success(),
"broad application-package secret read grant must succeed"
);
}

async fn execute_mount_probe(
read_only: bool,
owner_write: bool,
Expand Down Expand Up @@ -291,6 +317,150 @@ async fn secure_windows_driver_system_config_is_read_only() {
assert!(!config.path().join("child.txt").exists());
}

async fn execute_secret_probe() -> (serde_json::Value, tempfile::TempDir) {
let source = PathBuf::from(env!("CARGO_BIN_EXE_semwright-driver-fixture"));
let binary_dir = tempfile::tempdir().expect("fixture directory");
let executable = binary_dir.path().join("driver.exe");
std::fs::copy(&source, &executable).expect("copy driver fixture");
harden_fixture(&executable);

let secret_dir = tempfile::tempdir().expect("secret grant");
let secret_path = secret_dir.path().join("secret.txt");
std::fs::write(&secret_path, b"owner-secret").expect("write secret fixture");
harden_fixture(&secret_path);

let mut manifest = manifest(executable);
manifest.secrets = vec![DriverSecretMount {
root: "fixture-secret-root".into(),
name: "fixture-secret".into(),
}];
let roots = vec![FilesystemGrant {
name: "fixture-secret-root".into(),
path: secret_path.clone(),
read: true,
write: false,
}];

let state = tempfile::tempdir().expect("driver state");
let helper = std::env::current_exe().expect("current test executable");
let provider = DriverProvider::connect(manifest, state.path(), &helper, &roots, false)
.await
.expect("Windows Driver Host secret mount");

let capabilities = Provider::capabilities(provider.as_ref())
.await
.expect("driver capabilities");
let probe = capabilities
.iter()
.find(|capability| capability.descriptor.name == "driver.fixture.secret_probe")
.expect("fixture secret capability")
.descriptor
.clone();
let output = Provider::execute(
provider.as_ref(),
&Context {
session: "windows-secret".into(),
request_id: "windows-secret-read-only".into(),
cancellation: CancellationToken::new(),
},
&probe,
&serde_json::json!({}),
)
.await
.expect("secret probe through LPAC");

Provider::shutdown(provider.as_ref())
.await
.expect("secret Driver Host shutdown");
drop(binary_dir);
(output, secret_dir)
}

#[tokio::test]
async fn secure_windows_driver_secret_is_private_and_read_only() {
let (output, secret_dir) = execute_secret_probe().await;
assert_eq!(output["read"], "owner-secret");
assert_eq!(output["write_ok"], false);
assert_eq!(
std::fs::read(secret_dir.path().join("secret.txt")).expect("read secret after shutdown"),
b"owner-secret"
);
}

#[tokio::test]
async fn secure_windows_driver_secret_rejects_source_with_broad_mutation_acl() {
let source = PathBuf::from(env!("CARGO_BIN_EXE_semwright-driver-fixture"));
let binary_dir = tempfile::tempdir().expect("fixture directory");
let executable = binary_dir.path().join("driver.exe");
std::fs::copy(&source, &executable).expect("copy driver fixture");
harden_fixture(&executable);

let secret_dir = tempfile::tempdir().expect("secret grant");
let secret_path = secret_dir.path().join("secret.txt");
std::fs::write(&secret_path, b"owner-secret").expect("write secret fixture");
harden_fixture(&secret_path);
grant_all_application_packages_file_modify(&secret_path);

let mut candidate = manifest(executable);
candidate.secrets = vec![DriverSecretMount {
root: "fixture-secret-root".into(),
name: "fixture-secret".into(),
}];
let roots = vec![FilesystemGrant {
name: "fixture-secret-root".into(),
path: secret_path,
read: true,
write: false,
}];
let state = tempfile::tempdir().expect("driver state");
let helper = std::env::current_exe().expect("current test executable");

let error = match DriverProvider::connect(candidate, state.path(), &helper, &roots, false).await
{
Ok(provider) => {
let _ = Provider::shutdown(provider.as_ref()).await;
panic!("mutable secret source must be rejected before child launch");
}
Err(error) => error,
};
assert_eq!(error.code, semwright_types::ErrorCode::PermissionDenied);
}

#[tokio::test]
async fn secure_windows_driver_rejects_nonprivate_secret_source_before_spawn() {
let source = PathBuf::from(env!("CARGO_BIN_EXE_semwright-driver-fixture"));
let binary_dir = tempfile::tempdir().expect("fixture directory");
let executable = binary_dir.path().join("driver.exe");
std::fs::copy(&source, &executable).expect("copy driver fixture");
harden_fixture(&executable);

let directory = tempfile::tempdir().expect("secret directory");
let secret = directory.path().join("secret.txt");
std::fs::write(&secret, b"sensitive-secret").expect("write secret");
harden_fixture(&secret);
grant_all_application_packages_file_read(&secret);

let mut candidate = manifest(executable);
candidate.secrets = vec![DriverSecretMount {
root: "fixture-secret-root".into(),
name: "fixture-secret".into(),
}];
let roots = vec![FilesystemGrant {
name: "fixture-secret-root".into(),
path: secret,
read: true,
write: false,
}];
let state = tempfile::tempdir().expect("driver state");
let helper = std::env::current_exe().expect("current test executable");
let error = match DriverProvider::connect(candidate, state.path(), &helper, &roots, false).await
{
Ok(_) => panic!("non-private secret source must be rejected before spawn"),
Err(error) => error,
};
assert_eq!(error.code, semwright_types::ErrorCode::PermissionDenied);
}

#[tokio::test]
async fn secure_windows_driver_operation_cpu_budget_terminates_job() {
let source = PathBuf::from(env!("CARGO_BIN_EXE_semwright-driver-fixture"));
Expand Down
5 changes: 5 additions & 0 deletions crates/driver-sdk/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ pub fn system_config_mount(logical_name: &str) -> Result<PathBuf> {
runtime_mount(MountClass::SystemConfig, logical_name)
}

/// Resolve an owner-granted secret file as materialized by the current platform sandbox.
pub fn secret_mount(logical_name: &str) -> Result<PathBuf> {
runtime_mount(MountClass::Secret, logical_name)
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Transport {
Expand Down
Loading
Loading