private/functions/Test-AxisCertificateAuthorityCommand.ps1
|
function Test-AxisCertificateAuthorityCommand { <# .SYNOPSIS Checks that a Type='Command' certificate authority is usable. .DESCRIPTION Used by New-AxisCertificateAuthority. Verifies the authority definition (SignCommand present, tokens valid, SshKeyFile exists, WorkingDirectory exists for local use) and then runs $Authority.TestCommand when one is configured, locally through the platform shell or through "ssh <host> sh -s" when SshHost is set. Without a TestCommand an SSH authority still gets a connection check ("ssh <host> true"); a local authority without TestCommand is only validated statically. Never throws; returns [pscustomobject]@{ Reachable; Message }. Example TestCommand for an OpenSSL CA: 'openssl ca -config /etc/pki/ca/openssl.cnf -status 00 || test -r /etc/pki/ca/index.txt' or simply 'test -r /etc/pki/ca/index.txt'. .PARAMETER Authority The authority hashtable from $Config.CertificateAuthorities. .PARAMETER TimeoutSec Overrides $Authority.TimeoutSec (default 120). #> [cmdletbinding()] Param( [Parameter(Mandatory)] [Hashtable]$Authority, [Parameter()] [Int]$TimeoutSec ) if(!$TimeoutSec) { $TimeoutSec = 120 if($Authority.TimeoutSec) { $TimeoutSec = [Int]$Authority.TimeoutSec } } $IsWin = ($PSVersionTable.PSVersion.Major -lt 6) -or $IsWindows $UseSsh = [bool]$Authority.SshHost #Static checks if(!$Authority.SignCommand) { return [pscustomobject]@{ Reachable = $false; Message = "SignCommand is not set" } } if($Authority.SignCommand -notmatch "\{CertFile\}") { return [pscustomobject]@{ Reachable = $false; Message = "SignCommand does not contain the {CertFile} token" } } $Known = @("CsrFile","CertFile","Serial","Reason","Device","Subject") ForEach($Template in @($Authority.SignCommand,$Authority.RevokeCommand,$Authority.TestCommand)) { if(!$Template) { continue } ForEach($Match in [regex]::Matches($Template,"\{(\w+)\}")) { if($Known -notcontains $Match.Groups[1].Value) { return [pscustomobject]@{ Reachable = $false; Message = "Unknown token $($Match.Value) in command template" } } } } if($Authority.SshKeyFile -and !(Test-Path $Authority.SshKeyFile)) { return [pscustomobject]@{ Reachable = $false; Message = "SshKeyFile '$($Authority.SshKeyFile)' not found" } } if(!$UseSsh -and $Authority.WorkingDirectory -and !(Test-Path $Authority.WorkingDirectory)) { return [pscustomobject]@{ Reachable = $false; Message = "WorkingDirectory '$($Authority.WorkingDirectory)' not found" } } if($UseSsh -and !(Get-Command ssh -ErrorAction SilentlyContinue)) { return [pscustomobject]@{ Reachable = $false; Message = "ssh client not found in PATH" } } $Command = $Authority.TestCommand if(!$Command -and !$UseSsh) { return [pscustomobject]@{ Reachable = $true; Message = "Definition valid (no TestCommand configured, command not executed)" } } if(!$Command) { $Command = "true" } $Command = $Command.Replace("{Device}","").Replace("{Subject}","").Replace("{Serial}","").Replace("{Reason}","") $Command = $Command.Replace("{CsrFile}","").Replace("{CertFile}","") $Id = [guid]::NewGuid().ToString("N") $TempDir = Join-Path ([System.IO.Path]::GetTempPath()) "AxisPowershell-$Id" $ScriptFile = Join-Path $TempDir $(if($IsWin -and !$UseSsh) { "run.cmd" } else { "run.sh" }) $StdOutFile = Join-Path $TempDir "stdout.txt" $StdErrFile = Join-Path $TempDir "stderr.txt" Try { $null = New-Item -ItemType Directory -Path $TempDir -Force $Param = @{ PassThru = $true RedirectStandardOutput = $StdOutFile RedirectStandardError = $StdErrFile } if($IsWin) { $Param.Add('WindowStyle','Hidden') } else { $Param.Add('NoNewWindow',$true) } if($UseSsh) { $Script = "set -e`n" if($Authority.WorkingDirectory) { $Script += "cd '$($Authority.WorkingDirectory)'`n" } $Script += "$Command`n" [System.IO.File]::WriteAllText($ScriptFile, $Script, [System.Text.Encoding]::ASCII) $SshTarget = $Authority.SshHost if($Authority.SshUser) { $SshTarget = "$($Authority.SshUser)@$($Authority.SshHost)" } $Arguments = @("-o","BatchMode=yes","-o","ConnectTimeout=15") if($Authority.SshKeyFile) { $Arguments += @("-i",$Authority.SshKeyFile) } $Arguments += @("-T",$SshTarget,"sh","-s") $Param.Add('FilePath',"ssh") $Param.Add('ArgumentList',$Arguments) $Param.Add('RedirectStandardInput',$ScriptFile) $What = "Test command on $($Authority.SshHost)" } elseif($IsWin) { [System.IO.File]::WriteAllText($ScriptFile, "@echo off`r`n$Command`r`n", [System.Text.Encoding]::ASCII) $Shell = "$env:ComSpec" if(!$Shell) { $Shell = "cmd.exe" } $Param.Add('FilePath',$Shell) $Param.Add('ArgumentList',@("/d","/c","`"$ScriptFile`"")) if($Authority.WorkingDirectory) { $Param.Add('WorkingDirectory',$Authority.WorkingDirectory) } $What = "Test command" } else { [System.IO.File]::WriteAllText($ScriptFile, "set -e`n$Command`n", [System.Text.Encoding]::ASCII) $Param.Add('FilePath',"/bin/sh") $Param.Add('ArgumentList',@($ScriptFile)) if($Authority.WorkingDirectory) { $Param.Add('WorkingDirectory',$Authority.WorkingDirectory) } $What = "Test command" } Write-Verbose "Running: $($Param.FilePath) $($Param.ArgumentList -join ' ')" $Process = Start-Process @Param $null = $Process.Handle if(!$Process.WaitForExit($TimeoutSec * 1000)) { Try { $Process.Kill() } Catch { } return [pscustomobject]@{ Reachable = $false; Message = "$What timed out after $TimeoutSec seconds" } } $StdOut = "" $StdErr = "" if(Test-Path $StdOutFile) { $StdOut = "$(Get-Content $StdOutFile -Raw)" } if(Test-Path $StdErrFile) { $StdErr = "$(Get-Content $StdErrFile -Raw)" } if($Process.ExitCode -ne 0) { $Detail = $StdErr.Trim() if(!$Detail) { $Detail = $StdOut.Trim() } return [pscustomobject]@{ Reachable = $false; Message = "$What failed with exit code $($Process.ExitCode): $Detail" } } $Message = "$What succeeded" if($StdOut.Trim()) { $Message += ": $($StdOut.Trim())" } return [pscustomobject]@{ Reachable = $true; Message = $Message } } Catch { return [pscustomobject]@{ Reachable = $false; Message = "$_" } } Finally { if(Test-Path $TempDir) { Remove-Item $TempDir -Recurse -Force -ErrorAction SilentlyContinue } } } |