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
67 changes: 55 additions & 12 deletions crates/codex-win-engine/src/authenticode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,25 +107,39 @@ fn powershell_exe() -> PathBuf {
.unwrap_or_else(|| PathBuf::from("powershell.exe"))
}

// PowerShell custom-object casts are rejected by ConstrainedLanguage. A plain
// hashtable serializes to the same JSON shape while remaining usable under
// AppLocker / WDAC-managed sessions.
#[cfg(windows)]
pub fn verify_openai_authenticode(path: &Path) -> Result<AuthenticodeReport, EngineError> {
log::info!("Authenticode verification start");
let script = format!(
const AUTHENTICODE_REPORT_PROJECTION: &str = r#"
@{
status = [string]$sig.Status
statusMessage = [string]$sig.StatusMessage
subject = if ($sig.SignerCertificate) { [string]$sig.SignerCertificate.Subject } else { '' }
issuer = if ($sig.SignerCertificate) { [string]$sig.SignerCertificate.Issuer } else { '' }
thumbprint = if ($sig.SignerCertificate) { [string]$sig.SignerCertificate.Thumbprint } else { '' }
} | ConvertTo-Json -Compress
"#;

#[cfg(windows)]
fn authenticode_script(path: &Path) -> String {
format!(
r#"
$ErrorActionPreference = 'Stop'
$securityModule = Join-Path $env:WINDIR 'System32\WindowsPowerShell\v1.0\Modules\Microsoft.PowerShell.Security\Microsoft.PowerShell.Security.psd1'
Import-Module $securityModule -ErrorAction Stop
$sig = Get-AuthenticodeSignature -LiteralPath {path}
[pscustomobject]@{{
status = [string]$sig.Status
statusMessage = [string]$sig.StatusMessage
subject = if ($sig.SignerCertificate) {{ [string]$sig.SignerCertificate.Subject }} else {{ '' }}
issuer = if ($sig.SignerCertificate) {{ [string]$sig.SignerCertificate.Issuer }} else {{ '' }}
thumbprint = if ($sig.SignerCertificate) {{ [string]$sig.SignerCertificate.Thumbprint }} else {{ '' }}
}} | ConvertTo-Json -Compress
{report_projection}
"#,
path = ps_quote(&path.to_string_lossy())
);
path = ps_quote(&path.to_string_lossy()),
report_projection = AUTHENTICODE_REPORT_PROJECTION,
)
}

#[cfg(windows)]
pub fn verify_openai_authenticode(path: &Path) -> Result<AuthenticodeReport, EngineError> {
log::info!("Authenticode verification start");
let script = authenticode_script(path);

let mut command = hidden_command(powershell_exe());
command.args(["-NoProfile", "-NonInteractive", "-Command", &script]);
Expand Down Expand Up @@ -219,4 +233,33 @@ mod tests {
.unwrap();
assert!(!report.is_valid_openai());
}

#[cfg(windows)]
#[test]
fn authenticode_script_runs_in_constrained_language() {
let path = std::env::current_exe().expect("resolve current test executable");
let production_script = super::authenticode_script(&path);
let script = format!(
r#"$ErrorActionPreference = 'Stop'
$ExecutionContext.SessionState.LanguageMode = 'ConstrainedLanguage'
if ($ExecutionContext.SessionState.LanguageMode -ne 'ConstrainedLanguage') {{
throw 'failed to enter ConstrainedLanguage'
}}
{production_script}
"#,
production_script = production_script,
);
let output = super::hidden_command(super::powershell_exe())
.args(["-NoProfile", "-NonInteractive", "-Command", &script])
.output()
.expect("run constrained-language Authenticode script");
assert!(
output.status.success(),
"Authenticode script failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let report = report_from_json(String::from_utf8_lossy(&output.stdout).trim())
.expect("parse constrained-language Authenticode report");
assert!(!report.status.is_empty());
}
}
17 changes: 17 additions & 0 deletions crates/codex-win-engine/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,20 @@ pub enum EngineError {
#[error("io error: {0}")]
Io(String),
}

#[cfg(test)]
mod constrained_language_regressions {
#[test]
fn runtime_powershell_avoids_custom_object_casts() {
let forbidden = ["[pscustom", "object]"].concat();
for (name, source) in [
("authenticode.rs", include_str!("authenticode.rs")),
("sys.rs", include_str!("sys.rs")),
] {
assert!(
!source.to_ascii_lowercase().contains(&forbidden),
"{name} contains a PowerShell custom-object cast that fails in ConstrainedLanguage"
);
}
}
}
16 changes: 8 additions & 8 deletions crates/codex-win-engine/src/portable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ fn request_codex_close_filtered(timeout_secs: u64, root: &Path) -> Result<(), En
let script = format!(
r#"
$RootFilter = {root_filter}
try {{ $RootFilter = [System.IO.Path]::GetFullPath($RootFilter).TrimEnd('\') }} catch {{}}
try {{ $RootFilter = ([string](Convert-Path -LiteralPath $RootFilter -ErrorAction Stop)).TrimEnd('\') }} catch {{}}
function Get-ProcessExePath($p) {{
try {{
$path = [string]$p.Path
Expand All @@ -303,21 +303,20 @@ function Get-ProcessExePath($p) {{
function Test-UnderRoot($p) {{
$path = Get-ProcessExePath $p
if ([string]::IsNullOrWhiteSpace($path) -or [string]::IsNullOrWhiteSpace($RootFilter)) {{ return $false }}
$full = [string]$path
try {{
$full = [System.IO.Path]::GetFullPath($path)
return ($full.Equals($RootFilter, [System.StringComparison]::OrdinalIgnoreCase) -or
$full.StartsWith($RootFilter + '\', [System.StringComparison]::OrdinalIgnoreCase))
}} catch {{
return $false
}}
$resolved = [string](Convert-Path -LiteralPath $path -ErrorAction Stop)
if (-not [string]::IsNullOrWhiteSpace($resolved)) {{ $full = $resolved }}
}} catch {{}}
return ($full.Equals($RootFilter, [System.StringComparison]::OrdinalIgnoreCase) -or
$full.StartsWith($RootFilter + '\', [System.StringComparison]::OrdinalIgnoreCase))
}}
function Get-TargetCodexProcess {{
$all = Get-Process -Name Codex, ChatGPT -ErrorAction SilentlyContinue
foreach ($p in $all) {{
if (Test-UnderRoot $p) {{ $p }}
}}
}}
$deadline = (Get-Date).AddSeconds({timeout})
$procs = @(Get-TargetCodexProcess)
if ($procs.Count -eq 0) {{
'no-targets'
Expand All @@ -329,6 +328,7 @@ foreach ($p in $procs) {{
if ($p.MainWindowHandle -ne 0) {{ [void]$p.CloseMainWindow() }}
}} catch {{}}
}}
$deadline = (Get-Date).AddSeconds({timeout})
while ((Get-Date) -lt $deadline) {{
Start-Sleep -Milliseconds 250
$remaining = @()
Expand Down
Loading
Loading