Skip to content

Use of Windows certificate store for authentication #6

Use of Windows certificate store for authentication

Use of Windows certificate store for authentication #6

name: Windows Certificate Store Test
# This workflow tests MS Certificate Store integration for WolfSSH
# It tests 4 combinations:
# 1. Server using cert store key, Client using cert store key
# 2. Server using cert store key, Client using file-based key (interop)
# 3. Server using file-based key, Client using cert store key (interop)
# 4. Server using file-based key, Client using file-based key (baseline)
on:
push:
branches: [ 'master', 'main', 'release/**' ]
pull_request:
branches: [ '*' ]
env:
WOLFSSL_SOLUTION_FILE_PATH: wolfssl64.sln
SOLUTION_FILE_PATH: wolfssh.sln
USER_SETTINGS_H_NEW: wolfssh/ide/winvs/user_settings.h
USER_SETTINGS_H: wolfssl/IDE/WIN/user_settings.h
INCLUDE_DIR: wolfssh
WOLFSSL_BUILD_CONFIGURATION: Release
WOLFSSH_BUILD_CONFIGURATION: Release
BUILD_PLATFORM: x64
TARGET_PLATFORM: 10
TEST_PORT: 22222
jobs:
build:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
with:
repository: wolfssl/wolfssl
path: wolfssl
- uses: actions/checkout@v4
with:
path: wolfssh
- name: Add MSBuild to PATH
uses: microsoft/setup-msbuild@v1
- name: Restore wolfSSL NuGet packages
working-directory: ${{ github.workspace }}\wolfssl
run: nuget restore ${{env.WOLFSSL_SOLUTION_FILE_PATH}}
- name: updated user_settings.h for sshd and x509
working-directory: ${{env.GITHUB_WORKSPACE}}
shell: bash
run: |
# Enable SSHD, SFTP, and X509 support (including WOLFSSH_NO_FPKI)
sed -i 's/#if 0/#if 1/g' ${{env.USER_SETTINGS_H_NEW}}
# Enable Windows cert store API (not in repo user_settings.h).
# Must be appended to wolfssh/ide/winvs/user_settings.h: VS projects put ide/winvs on the
printf '\n/* Appended by windows-cert-store-test CI */\n#define WOLFSSH_WINDOWS_CERT_STORE\n' >> ${{env.USER_SETTINGS_H_NEW}}
cp ${{env.USER_SETTINGS_H_NEW}} ${{env.USER_SETTINGS_H}}
# Verify WOLFSSH_NO_FPKI will be defined
if grep -q "WOLFSSH_NO_FPKI" ${{env.USER_SETTINGS_H}}; then
echo "WOLFSSH_NO_FPKI found in user_settings.h"
else
echo "WARNING: WOLFSSH_NO_FPKI not found in user_settings.h"
fi
- name: Build wolfssl library
working-directory: ${{ github.workspace }}\wolfssl
run: msbuild /m /p:PlatformToolset=v142 /p:Platform=${{env.BUILD_PLATFORM}} /p:Configuration=${{env.WOLFSSL_BUILD_CONFIGURATION}} /t:wolfssl ${{env.WOLFSSL_SOLUTION_FILE_PATH}}
- name: Upload wolfSSL build artifacts
uses: actions/upload-artifact@v4
with:
name: wolfssl-windows-build
if-no-files-found: warn
retention-days: 1
path: |
wolfssl/IDE/WIN/${{env.WOLFSSL_BUILD_CONFIGURATION}}/${{env.BUILD_PLATFORM}}/**
wolfssl/IDE/WIN/${{env.WOLFSSL_BUILD_CONFIGURATION}}/**
wolfssl/${{env.WOLFSSL_BUILD_CONFIGURATION}}/${{env.BUILD_PLATFORM}}/**
wolfssl/${{env.WOLFSSL_BUILD_CONFIGURATION}}/**
- name: Restore NuGet packages
working-directory: ${{ github.workspace }}\wolfssh\ide\winvs
run: nuget restore ${{env.SOLUTION_FILE_PATH}}
- name: Build wolfssh
working-directory: ${{ github.workspace }}\wolfssh\ide\winvs
run: msbuild /m /p:PlatformToolset=v142 /p:Platform=${{env.BUILD_PLATFORM}} /p:WindowsTargetPlatformVersion=${{env.TARGET_PLATFORM}} /p:Configuration=${{env.WOLFSSH_BUILD_CONFIGURATION}} ${{env.SOLUTION_FILE_PATH}}
- name: Upload wolfSSH build artifacts
uses: actions/upload-artifact@v4
with:
name: wolfssh-windows-build
if-no-files-found: error
path: |
wolfssh/ide/winvs/**/Release/**
- name: Create PowerShell script to import cert to store
working-directory: ${{ github.workspace }}\wolfssh
run: |
@"
# Import certificate and key to Windows Certificate Store
param(
[string]$CertPath,
[string]$KeyPath,
[string]$StoreName = "My",
[string]$SubjectName,
[string]$StoreLocation = "CurrentUser"
)
`$ErrorActionPreference = "Stop"
# Convert DER to Base64 for import
`$certBytes = [System.IO.File]::ReadAllBytes(`$CertPath)
`$certBase64 = [System.Convert]::ToBase64String(`$certBytes)
# Create certificate object
`$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2
`$cert.Import([System.Convert]::FromBase64String(`$certBase64))
# If subject name not provided, use CN from certificate
if ([string]::IsNullOrEmpty(`$SubjectName)) {
`$SubjectName = `$cert.Subject
}
# Determine store location
`$storeLocationEnum = [System.Security.Cryptography.X509Certificates.StoreLocation]::CurrentUser
if (`$StoreLocation -eq "LocalMachine") {
`$storeLocationEnum = [System.Security.Cryptography.X509Certificates.StoreLocation]::LocalMachine
}
# Open the certificate store
`$store = New-Object System.Security.Cryptography.X509Certificates.X509Store(`$StoreName, `$storeLocationEnum)
`$store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite)
try {
# Remove existing certificate with same subject if present
`$existingCerts = `$store.Certificates.Find([System.Security.Cryptography.X509Certificates.X509FindType]::FindBySubjectName, `$SubjectName, `$false)
foreach (`$existingCert in `$existingCerts) {
`$store.Remove(`$existingCert)
}
# Import the certificate
# Note: For private key import, we need to use certutil or other methods
# This is a simplified version - in practice, you may need to use
# certutil -importPFX or other methods to import with private key
`$store.Add(`$cert)
Write-Host "Certificate imported successfully to `$StoreName store"
Write-Host "Subject: `$SubjectName"
Write-Host "Thumbprint: `$(`$cert.Thumbprint)"
}
finally {
`$store.Close()
}
return `$cert.Thumbprint
"@ | Out-File -FilePath import-cert.ps1 -Encoding UTF8
- name: Build import script
working-directory: ${{ github.workspace }}\wolfssh
shell: pwsh
run: |
# Note: This step is informational - actual import happens in test job
Write-Host "Keys will be imported to cert store in test job"
test:
needs: build
runs-on: windows-latest
strategy:
fail-fast: false
matrix:
include:
- server_key_source: file
client_key_source: x509
key_algorithm: rsa
test_name: "Server-File-Client-X509"
- server_key_source: store
client_key_source: x509
key_algorithm: rsa
test_name: "Server-Store-Client-X509"
- server_key_source: file
client_key_source: store
key_algorithm: rsa
test_name: "Server-File-Client-Store"
- server_key_source: store
client_key_source: store
key_algorithm: rsa
test_name: "Server-Store-Client-Store"
- server_key_source: store
client_key_source: x509
key_algorithm: ecdsa
test_name: "Server-Store-Client-X509-ECDSA"
steps:
- uses: actions/checkout@v4
with:
path: wolfssh
- name: Download wolfSSH build artifacts
uses: actions/download-artifact@v4
with:
name: wolfssh-windows-build
path: .
- name: Download wolfSSL build artifacts
uses: actions/download-artifact@v4
with:
name: wolfssl-windows-build
path: .
- name: Set up test environment - ${{ matrix.test_name }}
working-directory: ${{ github.workspace }}\wolfssh
shell: bash
env:
# Disable MSYS path conversion - Git Bash converts /C=US/... to C:/Program Files/Git/C=US/...
MSYS_NO_PATHCONV: 1
MSYS2_ARG_CONV_EXCL: "*"
run: |
echo "=== Test Configuration ==="
echo "Server key source: ${{ matrix.server_key_source }}"
echo "Client key source: ${{ matrix.client_key_source }}"
echo "========================="
# Create X509 certificate for testuser using renewcerts.sh (like sshd_x509_test.sh does)
if [[ "${{ matrix.client_key_source }}" == "x509" || "${{ matrix.client_key_source }}" == "store" ]]; then
echo "Creating X509 certificate for testuser using renewcerts.sh..."
# Check required files exist
if [[ ! -f "keys/renewcerts.sh" ]]; then
echo "ERROR: renewcerts.sh not found at keys/renewcerts.sh"
exit 1
fi
if [[ ! -f "keys/fred-key.pem" ]]; then
echo "ERROR: fred-key.pem not found at keys/fred-key.pem"
exit 1
fi
if [[ ! -f "keys/ca-key-ecc.pem" ]]; then
echo "ERROR: ca-key-ecc.pem not found at keys/ca-key-ecc.pem"
exit 1
fi
# Run renewcerts.sh with testuser argument (this creates testuser-cert.der and testuser-key.der)
cd keys
bash renewcerts.sh testuser
cd ..
# Verify certificates were created
if [[ -f "keys/testuser-cert.der" && -f "keys/testuser-key.der" ]]; then
echo "Created testuser-cert.der and testuser-key.der"
# Verify the certificate has the correct CN
certText=$(openssl x509 -in keys/testuser-cert.der -inform DER -text -noout 2>&1)
if echo "$certText" | grep -q "CN.*=.*testuser"; then
echo "Certificate CN verified: testuser"
else
echo "WARNING: Certificate CN may not match testuser"
echo "Certificate subject:"
echo "$certText" | grep "Subject:"
fi
echo "CLIENT_CERT_FILE=keys/testuser-cert.der" >> $GITHUB_ENV
echo "CLIENT_KEY_FILE=keys/testuser-key.der" >> $GITHUB_ENV
else
echo "ERROR: Failed to create certificate files"
echo "Expected: keys/testuser-cert.der and keys/testuser-key.der"
ls -la keys/
exit 1
fi
fi
- name: Set up cert store certificates
working-directory: ${{ github.workspace }}\wolfssh
shell: pwsh
run: |
# For testing, we'll create self-signed certificates in the cert store
# In a production scenario, you would import existing DER keys/certs
# using certutil or other tools that can handle private key import
# Create server certificate in cert store with exportable key
# For server: use LocalMachine so service (LocalSystem) can access it
# For client: use CurrentUser (accessed by testuser)
if ("${{ matrix.server_key_source }}" -eq "store") {
if ("${{ matrix.key_algorithm }}" -eq "ecdsa") {
$serverCert = New-SelfSignedCertificate `
-Subject "CN=wolfSSH-Test-Server" `
-KeyAlgorithm ECDSA_nistP256 `
-CertStoreLocation "Cert:\LocalMachine\My" `
-KeyExportPolicy Exportable `
-NotAfter (Get-Date).AddYears(1) `
-KeyUsage DigitalSignature
} else {
$serverCert = New-SelfSignedCertificate `
-Subject "CN=wolfSSH-Test-Server" `
-KeyAlgorithm RSA `
-KeyLength 2048 `
-CertStoreLocation "Cert:\LocalMachine\My" `
-KeyExportPolicy Exportable `
-NotAfter (Get-Date).AddYears(1) `
-KeyUsage DigitalSignature, KeyEncipherment
}
Write-Host "Server cert created in LocalMachine: $($serverCert.Subject)"
# Grant LocalSystem (NT AUTHORITY\SYSTEM) access to the private key.
# This is required for the wolfsshd service to access the key when running
# as LocalSystem. Without this, CryptAcquireCertificatePrivateKey fails.
Write-Host "=== Granting LocalSystem access to private key ==="
Write-Host "Certificate thumbprint: $($serverCert.Thumbprint)"
Write-Host "Certificate has private key: $($serverCert.HasPrivateKey)"
# Try multiple methods to get the private key info
$keyFound = $false
# Method 1: Try RSACertificateExtensions (RSA keys)
try {
Write-Host "Trying RSACertificateExtensions.GetRSAPrivateKey..."
$rsaKey = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($serverCert)
if ($rsaKey) {
Write-Host " Got RSA key object: $($rsaKey.GetType().FullName)"
# Try to get the key name from CngKey
if ($rsaKey.Key) {
$keyName = $rsaKey.Key.UniqueName
Write-Host " Key unique name: $keyName"
} elseif ($rsaKey -is [System.Security.Cryptography.RSACng]) {
$cngKey = $rsaKey.Key
if ($cngKey) {
$keyName = $cngKey.UniqueName
Write-Host " CNG Key unique name: $keyName"
}
}
}
} catch {
Write-Host " RSACertificateExtensions method failed: $_"
}
# Method 1b: Try ECDsaCertificateExtensions (ECDSA keys)
if (-not $keyName) {
try {
Write-Host "Trying ECDsaCertificateExtensions.GetECDsaPrivateKey..."
$ecdsaKey = [System.Security.Cryptography.X509Certificates.ECDsaCertificateExtensions]::GetECDsaPrivateKey($serverCert)
if ($ecdsaKey) {
Write-Host " Got ECDSA key object: $($ecdsaKey.GetType().FullName)"
if ($ecdsaKey.Key) {
$keyName = $ecdsaKey.Key.UniqueName
Write-Host " ECDSA CNG key unique name: $keyName"
}
}
} catch {
Write-Host " ECDsaCertificateExtensions method failed: $_"
}
}
# Method 2: Try PrivateKey property (older API)
if (-not $keyName) {
try {
Write-Host "Trying PrivateKey property..."
if ($serverCert.PrivateKey) {
$pk = $serverCert.PrivateKey
Write-Host " Got private key: $($pk.GetType().FullName)"
if ($pk.CspKeyContainerInfo) {
$keyName = $pk.CspKeyContainerInfo.UniqueKeyContainerName
Write-Host " CSP Key container name: $keyName"
}
}
} catch {
Write-Host " PrivateKey property method failed: $_"
}
}
# Search for key file in known locations
if ($keyName) {
$keyPaths = @(
"$env:ProgramData\Microsoft\Crypto\Keys\$keyName",
"$env:ProgramData\Microsoft\Crypto\RSA\MachineKeys\$keyName",
"$env:ProgramData\Microsoft\Crypto\SystemKeys\$keyName"
)
foreach ($keyPath in $keyPaths) {
if (Test-Path $keyPath) {
Write-Host "Found key file at: $keyPath"
# Show current ACL
$acl = Get-Acl $keyPath
Write-Host "Current ACL:"
$acl.Access | ForEach-Object { Write-Host " $($_.IdentityReference): $($_.FileSystemRights)" }
# Add SYSTEM access
$permission = "NT AUTHORITY\SYSTEM", "FullControl", "Allow"
$accessRule = New-Object System.Security.AccessControl.FileSystemAccessRule $permission
$acl.SetAccessRule($accessRule)
Set-Acl $keyPath $acl
Write-Host "Granted SYSTEM FullControl to: $keyPath"
# Verify new ACL
$newAcl = Get-Acl $keyPath
Write-Host "New ACL:"
$newAcl.Access | ForEach-Object { Write-Host " $($_.IdentityReference): $($_.FileSystemRights)" }
$keyFound = $true
break
}
}
if (-not $keyFound) {
Write-Host "WARNING: Key file not found in any expected location"
Write-Host "Searching for key files..."
Get-ChildItem "$env:ProgramData\Microsoft\Crypto" -Recurse -File 2>$null | ForEach-Object {
if ($_.Name -like "*$($keyName.Substring(0, [Math]::Min(8, $keyName.Length)))*") {
Write-Host " Possible match: $($_.FullName)"
}
}
}
} else {
Write-Host "WARNING: Could not determine private key name"
}
if (-not $keyFound) {
Write-Host "WARNING: Could not grant SYSTEM access to private key - service may fail"
}
} else {
# Still create it for consistency, but in CurrentUser (not used for server)
$serverCert = New-SelfSignedCertificate `
-Subject "CN=wolfSSH-Test-Server" `
-KeyAlgorithm RSA `
-KeyLength 2048 `
-CertStoreLocation "Cert:\CurrentUser\My" `
-KeyExportPolicy Exportable `
-NotAfter (Get-Date).AddYears(1) `
-KeyUsage DigitalSignature, KeyEncipherment
Write-Host "Server cert created in CurrentUser: $($serverCert.Subject)"
}
Write-Host "Server cert thumbprint: $($serverCert.Thumbprint)"
Write-Host "Server cert full subject: $($serverCert.Subject)"
# Extract just the CN value without "CN=" prefix for CertFindCertificateInStore
# CertFindCertificateInStore with CERT_FIND_SUBJECT_STR searches the formatted name
# which may not include the "CN=" prefix
$subjectForSearch = $serverCert.Subject
if ($subjectForSearch -match "^CN=(.+)$") {
$subjectForSearch = $matches[1]
Write-Host "Using CN value for search: $subjectForSearch"
}
Add-Content -Path $env:GITHUB_ENV -Value "SERVER_CERT_SUBJECT=$subjectForSearch"
Add-Content -Path $env:GITHUB_ENV -Value "SERVER_CERT_STORE=${{ matrix.server_key_source }}"
# Create/import client certificate based on client_key_source
if ("${{ matrix.client_key_source }}" -eq "store") {
# For cert store: import testuser certificate (signed by CA) into cert store
# This ensures the cert is signed by the CA that the server trusts and has CN=testuser
$userCertPath = $env:CLIENT_CERT_FILE
$userKeyPath = $env:CLIENT_KEY_FILE
if ([string]::IsNullOrEmpty($userCertPath)) {
$userCertPath = Join-Path "${{ github.workspace }}" "wolfssh\keys\testuser-cert.der"
}
if ([string]::IsNullOrEmpty($userKeyPath)) {
$userKeyPath = Join-Path "${{ github.workspace }}" "wolfssh\keys\testuser-key.der"
}
# Fallback to fred if testuser certs don't exist
if (-not (Test-Path $userCertPath)) {
Write-Host "WARNING: testuser-cert.der not found, trying fred-cert.der"
$userCertPath = Join-Path "${{ github.workspace }}" "wolfssh\keys\fred-cert.der"
}
if (-not (Test-Path $userKeyPath)) {
Write-Host "WARNING: testuser-key.der not found, trying fred-key.der"
$userKeyPath = Join-Path "${{ github.workspace }}" "wolfssh\keys\fred-key.der"
}
if (-not (Test-Path $userCertPath) -or -not (Test-Path $userKeyPath)) {
Write-Host "ERROR: Client cert or key not found: $userCertPath or $userKeyPath"
exit 1
}
# Convert DER cert+key to PFX for import into cert store using openssl
$pfxPath = Join-Path $env:TEMP "testuser-client.pfx"
$pfxPassword = "TempP@ss123"
# Check if openssl is available
$opensslPath = Get-Command openssl -ErrorAction SilentlyContinue
if ($opensslPath) {
Write-Host "Converting testuser-cert.der + testuser-key.der to PFX using openssl..."
# Convert DER to PEM first, then to PFX
$userCertPem = Join-Path $env:TEMP "testuser-cert.pem"
$userKeyPem = Join-Path $env:TEMP "testuser-key.pem"
# Convert certificate DER to PEM
$certConvert = & openssl x509 -inform DER -in $userCertPath -out $userCertPem 2>&1
if ($LASTEXITCODE -ne 0) {
Write-Host "ERROR: Failed to convert cert DER to PEM: $certConvert"
$importedCert = $null
} else {
# Try to convert key - first try as RSA, then as ECC
$keyConvert = & openssl rsa -inform DER -in $userKeyPath -out $userKeyPem 2>&1
if ($LASTEXITCODE -ne 0) {
Write-Host "Key is not RSA, trying ECC..."
$keyConvert = & openssl ec -inform DER -in $userKeyPath -out $userKeyPem 2>&1
if ($LASTEXITCODE -ne 0) {
Write-Host "ERROR: Failed to convert key DER to PEM (tried RSA and ECC): $keyConvert"
$importedCert = $null
} else {
Write-Host "Successfully converted ECC key to PEM"
}
} else {
Write-Host "Successfully converted RSA key to PEM"
}
if ($importedCert -eq $null -and (Test-Path $userKeyPem)) {
# Create PFX
$pfxConvert = & openssl pkcs12 -export -out $pfxPath -inkey $userKeyPem -in $userCertPem -password "pass:$pfxPassword" -nodes 2>&1
if ($LASTEXITCODE -ne 0) {
Write-Host "ERROR: Failed to create PFX: $pfxConvert"
$importedCert = $null
} elseif (Test-Path $pfxPath) {
Write-Host "PFX created, importing into cert store..."
# Import PFX into cert store
try {
Import-PfxCertificate -FilePath $pfxPath -CertStoreLocation "Cert:\CurrentUser\My" -Password (ConvertTo-SecureString -String $pfxPassword -Force -AsPlainText) -ErrorAction Stop | Out-Null
# Get the imported cert - look for "testuser" in subject or check most recent
$importedCert = Get-ChildItem -Path "Cert:\CurrentUser\My" | Where-Object { $_.Subject -match "testuser" } | Select-Object -First 1
if (-not $importedCert) {
Write-Host "WARNING: Cert imported but not found by subject search, checking most recent..."
# Get the most recently added cert
$importedCert = Get-ChildItem -Path "Cert:\CurrentUser\My" | Sort-Object NotBefore -Descending | Select-Object -First 1
}
if ($importedCert) {
Write-Host "Successfully imported testuser cert: $($importedCert.Subject)"
}
} catch {
Write-Host "ERROR: Failed to import PFX: $_"
$importedCert = $null
}
# Cleanup temp files
Remove-Item -Path $pfxPath, $userCertPem, $userKeyPem -ErrorAction SilentlyContinue
} else {
Write-Host "WARNING: PFX file was not created"
$importedCert = $null
}
}
}
} else {
Write-Host "WARNING: openssl not found, cannot import testuser cert. Creating self-signed cert (may not work with CA verification)"
$importedCert = $null
}
if (-not $importedCert) {
# Fallback: create self-signed cert (won't work with CA verification, but allows test to proceed)
Write-Host "Creating self-signed client cert as fallback..."
$clientCert = New-SelfSignedCertificate `
-Subject "CN=wolfSSH-Test-Client" `
-KeyAlgorithm RSA `
-KeyLength 2048 `
-CertStoreLocation "Cert:\CurrentUser\My" `
-KeyExportPolicy Exportable `
-NotAfter (Get-Date).AddYears(1) `
-KeyUsage DigitalSignature, KeyEncipherment
$importedCert = $clientCert
}
Write-Host "Client cert in store: $($importedCert.Subject)"
Write-Host "Client cert thumbprint: $($importedCert.Thumbprint)"
# Extract CN from the subject for use as a cert store search string.
# The full X.500 DN contains commas which break command-line argument
# parsing, but CertFindCertificateInStore does substring matching so
# the CN alone is sufficient.
$cn = $importedCert.Subject
if ($cn -match 'CN=([^,]+)') {
$cn = $matches[1].Trim()
}
Write-Host "Client cert CN for store lookup: '$cn'"
Add-Content -Path $env:GITHUB_ENV -Value "CLIENT_CERT_SUBJECT=$cn"
} else {
# For file/x509: create a placeholder cert (not used, but keeps env var consistent)
$clientCert = New-SelfSignedCertificate `
-Subject "CN=wolfSSH-Test-Client" `
-KeyAlgorithm RSA `
-KeyLength 2048 `
-CertStoreLocation "Cert:\CurrentUser\My" `
-KeyExportPolicy Exportable `
-NotAfter (Get-Date).AddYears(1) `
-KeyUsage DigitalSignature, KeyEncipherment
Write-Host "Client cert created (placeholder for non-store tests): $($clientCert.Subject)"
Add-Content -Path $env:GITHUB_ENV -Value "CLIENT_CERT_SUBJECT=$($clientCert.Subject)"
}
- name: Create Windows user testuser and authorized_keys
shell: pwsh
run: |
$homeDir = "C:\Users\testuser"
$sshDir = "$homeDir\.ssh"
$authKeysFile = "$sshDir\authorized_keys"
# Password: <=14 chars to avoid net user "Windows 2000" prompt; mixed case, number, special.
# This is a test user and not a sensitive password.
$pw = 'T3stP@ss!xY9'
# Create home dir and .ssh for testuser (default: .ssh/authorized_keys)
New-Item -ItemType Directory -Path $homeDir -Force | Out-Null
New-Item -ItemType Directory -Path $sshDir -Force | Out-Null
Write-Host "Created $homeDir and $sshDir"
# Create local user testuser (net user avoids New-LocalUser password policy issues in CI)
$o = net user testuser $pw /add /homedir:$homeDir 2>&1
if ($LASTEXITCODE -ne 0) {
if ($o -match "already exists") {
Write-Host "User testuser already exists"
net user testuser /homedir:$homeDir 2>$null
} else {
Write-Host "net user failed: $o"
exit 1
}
} else {
Write-Host "Created user testuser"
}
Add-Content -Path $env:GITHUB_ENV -Value "TESTUSER_PASSWORD=$pw"
# For X509: no authorized_keys needed (server verifies cert against CA)
Write-Host "X509 certificate auth (source: ${{ matrix.client_key_source }}): authorized_keys not needed (server uses CA verification)"
# Create empty file - X509 doesn't use authorized_keys, but file should exist
"" | Out-File -FilePath $authKeysFile -Encoding ASCII -NoNewline
icacls $authKeysFile /grant "testuser:R" /q
Write-Host "Created $authKeysFile (empty - X509 uses CA verification)"
# Set ProfileImagePath so SHGetKnownFolderPath(FOLDERID_Profile) returns $homeDir
# for testuser (GetHomeDirectory in wolfsshd uses that; otherwise it can fail for new users).
$sid = (New-Object System.Security.Principal.NTAccount("testuser")).Translate([System.Security.Principal.SecurityIdentifier]).Value
$profKey = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\$sid"
if (-not (Test-Path $profKey)) { New-Item -Path $profKey -Force | Out-Null }
Set-ItemProperty -Path $profKey -Name "ProfileImagePath" -Value $homeDir -Force
Write-Host "Set ProfileImagePath for testuser to $homeDir"
- name: Create wolfSSHd config file
working-directory: ${{ github.workspace }}\wolfssh
shell: pwsh
run: |
$configContent = @"
Port ${{env.TEST_PORT}}
PasswordAuthentication yes
PermitRootLogin yes
"@
# For X509 client auth: configure TrustedUserCAKeys and HostCertificate (server verifies client cert against CA)
# Use PEM format as per apps/wolfsshd/test/create_sshd_config.sh
$caCertPathPem = Join-Path "${{ github.workspace }}" "wolfssh\keys\ca-cert-ecc.pem"
$caCertPathFull = (Resolve-Path $caCertPathPem -ErrorAction SilentlyContinue)
if (-not $caCertPathFull) {
Write-Host "ERROR: CA cert not found at: $caCertPathPem"
exit 1
}
Write-Host "Using CA cert (PEM format) for X509 verification: $($caCertPathFull.Path)"
$configContent += @"
TrustedUserCAKeys $($caCertPathFull.Path)
"@
if ("${{ matrix.server_key_source }}" -eq "store") {
# Get server cert subject from environment
$serverSubject = $env:SERVER_CERT_SUBJECT
if ([string]::IsNullOrEmpty($serverSubject)) {
Write-Host "ERROR: SERVER_CERT_SUBJECT not set"
exit 1
}
Write-Host "Using cert store host key with subject: $serverSubject"
# Server cert is in LocalMachine (service runs as LocalSystem)
# Note: When using cert store, the certificate is part of the store entry
# Do NOT specify HostCertificate separately - it would conflict with the store cert
$configContent += @"
HostKeyStore My
HostKeyStoreSubject $serverSubject
HostKeyStoreFlags LOCAL_MACHINE
"@
} else {
# Use PEM format as per apps/wolfsshd/test/create_sshd_config.sh
$keyPath = Join-Path "${{ github.workspace }}" "wolfssh\keys\server-key.pem"
$keyPathFull = (Resolve-Path $keyPath -ErrorAction SilentlyContinue)
if (-not $keyPathFull) {
Write-Host "ERROR: Host key file not found at: $keyPath"
Write-Host "Checking for key files..."
Get-ChildItem -Path "${{ github.workspace }}\wolfssh\keys" -Filter "server-key*" | Select-Object FullName
exit 1
}
Write-Host "Using file-based host key: $($keyPathFull.Path)"
# Add HostCertificate only for file-based keys (not cert store)
$serverCertPathPem = Join-Path "${{ github.workspace }}" "wolfssh\keys\server-cert.pem"
$serverCertPathFull = (Resolve-Path $serverCertPathPem -ErrorAction SilentlyContinue)
if (-not $serverCertPathFull) {
Write-Host "ERROR: server-cert.pem not found at: $serverCertPathPem (required for X509)"
Write-Host "Checking for server cert files..."
Get-ChildItem -Path "${{ github.workspace }}\wolfssh\keys" -Filter "server-cert*" | Select-Object FullName
exit 1
}
Write-Host "Using server certificate: $($serverCertPathFull.Path)"
$configContent += @"
HostKey $($keyPathFull.Path)
HostCertificate $($serverCertPathFull.Path)
"@
}
$configContent | Out-File -FilePath sshd_config_test -Encoding ASCII
Write-Host "=== wolfSSHd Config ==="
Get-Content sshd_config_test
Write-Host "=== End Config ==="
- name: Find wolfSSH executables
working-directory: ${{ github.workspace }}\wolfssh
shell: pwsh
run: |
$searchRoot = "${{ github.workspace }}"
Write-Host "Searching for built executables under: $searchRoot"
# Find wolfsshd.exe
$sshdExe = Get-ChildItem -Path $searchRoot -Recurse -Filter "wolfsshd.exe" -ErrorAction SilentlyContinue |
Where-Object { $_.FullName -like "*Release*" -or $_.FullName -like "*Debug*" } |
Select-Object -First 1
if ($sshdExe) {
Write-Host "Found wolfsshd.exe at: $($sshdExe.FullName)"
Add-Content -Path $env:GITHUB_ENV -Value "SSHD_PATH=$($sshdExe.FullName)"
} else {
Write-Host "ERROR: wolfsshd.exe not found"
Get-ChildItem -Path $searchRoot -Recurse -Filter "*.exe" -ErrorAction SilentlyContinue | Select-Object FullName
exit 1
}
# Find wolfsftp client exe (project name is often wolfsftp-client)
$sftpExe = Get-ChildItem -Path $searchRoot -Recurse -Filter "wolfsftp.exe" -ErrorAction SilentlyContinue |
Where-Object { $_.FullName -like "*Release*" -or $_.FullName -like "*Debug*" } |
Select-Object -First 1
if (-not $sftpExe) {
$sftpExe = Get-ChildItem -Path $searchRoot -Recurse -Filter "wolfsftp-client.exe" -ErrorAction SilentlyContinue |
Where-Object { $_.FullName -like "*Release*" -or $_.FullName -like "*Debug*" } |
Select-Object -First 1
}
if ($sftpExe) {
Write-Host "Found SFTP client exe at: $($sftpExe.FullName)"
Add-Content -Path $env:GITHUB_ENV -Value "SFTP_PATH=$($sftpExe.FullName)"
} else {
Write-Host "ERROR: SFTP client exe not found (wolfsftp.exe or wolfsftp-client.exe)"
Get-ChildItem -Path $searchRoot -Recurse -Filter "*.exe" -ErrorAction SilentlyContinue | Select-Object FullName
exit 1
}
# Find wolfssh.exe (SSH client) (optional)
$sshExe = Get-ChildItem -Path $searchRoot -Recurse -Filter "wolfssh.exe" -ErrorAction SilentlyContinue |
Where-Object { $_.FullName -like "*Release*" -or $_.FullName -like "*Debug*" } |
Select-Object -First 1
if ($sshExe) {
Write-Host "Found wolfssh.exe at: $($sshExe.FullName)"
Add-Content -Path $env:GITHUB_ENV -Value "SSH_PATH=$($sshExe.FullName)"
} else {
Write-Host "WARNING: wolfssh.exe not found (SSH client test will be skipped)"
}
# Find echoserver.exe (used for cert store server test instead of wolfsshd for better debug logs)
$echoserverExe = Get-ChildItem -Path $searchRoot -Recurse -Filter "echoserver.exe" -ErrorAction SilentlyContinue |
Where-Object { $_.FullName -like "*Release*" -or $_.FullName -like "*Debug*" } |
Select-Object -First 1
if ($echoserverExe) {
Write-Host "Found echoserver.exe at: $($echoserverExe.FullName)"
Add-Content -Path $env:GITHUB_ENV -Value "ECHOSERVER_PATH=$($echoserverExe.FullName)"
} else {
Write-Host "WARNING: echoserver.exe not found (cert store server test will need it)"
}
- name: Copy wolfSSL DLL to executable directory (if dynamic build)
working-directory: ${{ github.workspace }}
shell: pwsh
run: |
$sshdPath = (Get-Content env:SSHD_PATH)
if (-not (Test-Path $sshdPath)) {
Write-Host "ERROR: wolfsshd.exe path not found in environment"
exit 1
}
$sshdDir = Split-Path -Parent $sshdPath
Write-Host "wolfsshd.exe directory: $sshdDir"
# If wolfssl.lib is already next to wolfsshd.exe, it's a static build - no DLL needed
$libInSshdDir = Join-Path $sshdDir "wolfssl.lib"
if (Test-Path $libInSshdDir) {
Write-Host "wolfssl.lib present beside wolfsshd.exe - static build; wolfssl.dll not required"
exit 0
}
# Dynamic build: find and copy wolfssl.dll
$wolfsslRoot = "${{ github.workspace }}\wolfssl"
$buildConfig = "${{env.WOLFSSL_BUILD_CONFIGURATION}}"
$buildPlatform = "${{env.BUILD_PLATFORM}}"
$commonPaths = @(
"$wolfsslRoot\IDE\WIN\$buildConfig\$buildPlatform\wolfssl.dll",
"$wolfsslRoot\IDE\WIN\$buildConfig\wolfssl.dll",
"$wolfsslRoot\$buildConfig\$buildPlatform\wolfssl.dll",
"$wolfsslRoot\$buildConfig\wolfssl.dll"
)
$wolfsslDll = $null
foreach ($path in $commonPaths) {
if (Test-Path $path) { $wolfsslDll = Get-Item $path; break }
}
if (-not $wolfsslDll) {
$wolfsslDll = Get-ChildItem -Path $wolfsslRoot -Recurse -Filter "wolfssl.dll" -ErrorAction SilentlyContinue | Select-Object -First 1
}
if ($wolfsslDll) {
$targetDll = Join-Path $sshdDir "wolfssl.dll"
Copy-Item -Path $wolfsslDll.FullName -Destination $targetDll -Force
Write-Host "Copied wolfssl.dll to $targetDll"
} else {
Write-Host "wolfssl.dll not found; if build is static (wolfssl.lib in output), this is OK"
}
- name: Verify host key configuration
working-directory: ${{ github.workspace }}\wolfssh
shell: pwsh
run: |
Write-Host "=== Verifying Host Key Configuration ==="
$configPath = "sshd_config_test"
if (-not (Test-Path $configPath)) {
Write-Host "ERROR: Config file not found: $configPath"
exit 1
}
$configContent = Get-Content $configPath -Raw
Write-Host "Config file content:"
Write-Host $configContent
# Check if host key is configured
$hasHostKey = $false
if ($configContent -match "HostKey\s+") {
Write-Host "Found HostKey directive (file-based)"
$hasHostKey = $true
# Verify the key file exists
if ($configContent -match "HostKey\s+([^\r\n]+)") {
$keyPath = $matches[1].Trim()
Write-Host "Host key path: $keyPath"
if (Test-Path $keyPath) {
Write-Host "Host key file exists: OK"
} else {
Write-Host "ERROR: Host key file not found: $keyPath"
exit 1
}
}
}
if ($configContent -match "HostKeyStore\s+") {
Write-Host "Found HostKeyStore directive (cert store-based)"
$hasHostKey = $true
# Verify cert store subject is set
if ($configContent -match "HostKeyStoreSubject\s+([^\r\n]+)") {
$subject = $matches[1].Trim()
Write-Host "Host key store subject: $subject"
if ([string]::IsNullOrEmpty($subject)) {
Write-Host "ERROR: HostKeyStoreSubject is empty"
exit 1
}
# Verify cert exists in store (check both LocalMachine and CurrentUser based on flags)
$storeFlags = ""
if ($configContent -match "HostKeyStoreFlags\s+([^\r\n]+)") {
$storeFlags = $matches[1].Trim()
}
$storePath = "Cert:\CurrentUser\My"
if ($storeFlags -eq "LOCAL_MACHINE") {
$storePath = "Cert:\LocalMachine\My"
}
Write-Host "Checking cert store: $storePath"
# Use -like with wildcards for substring match (same as CertFindCertificateInStore)
# The subject might be "wolfSSH-Test-Server" but cert has "CN=wolfSSH-Test-Server"
$cert = Get-ChildItem -Path $storePath -ErrorAction SilentlyContinue | Where-Object { $_.Subject -like "*$subject*" } | Select-Object -First 1
if ($cert) {
Write-Host "Certificate found in store: OK (Thumbprint: $($cert.Thumbprint), Subject: $($cert.Subject))"
# Verify cert has private key accessible
try {
$hasPrivateKey = $cert.HasPrivateKey
Write-Host "Cert has private key: $hasPrivateKey"
if (-not $hasPrivateKey) {
Write-Host "WARNING: Certificate does not have a private key accessible"
}
} catch {
Write-Host "WARNING: Could not verify private key access: $_"
}
} else {
Write-Host "ERROR: Certificate not found in store with subject: $subject"
Write-Host "Available certificates in $storePath :"
Get-ChildItem -Path $storePath -ErrorAction SilentlyContinue | Select-Object Subject, Thumbprint | Format-Table
exit 1
}
} else {
Write-Host "ERROR: HostKeyStoreSubject not found in config"
exit 1
}
}
# For X509 (both x509 file and store): TrustedUserCAKeys is required instead of authorized_keys
if ("${{ matrix.client_key_source }}" -eq "x509" -or "${{ matrix.client_key_source }}" -eq "store") {
if ($configContent -match "TrustedUserCAKeys\s+") {
Write-Host "Found TrustedUserCAKeys directive (X509 CA verification, client source: ${{ matrix.client_key_source }})"
if ($configContent -match "TrustedUserCAKeys\s+([^\r\n]+)") {
$caPath = $matches[1].Trim()
Write-Host "CA cert path: $caPath"
if (Test-Path $caPath) {
Write-Host "CA cert file exists: OK"
$caFileInfo = Get-Item $caPath
Write-Host "CA cert file size: $($caFileInfo.Length) bytes"
} else {
Write-Host "ERROR: CA cert file not found: $caPath"
Write-Host "Current directory: $(Get-Location)"
Write-Host "Files in keys directory:"
if (Test-Path "keys") {
Get-ChildItem "keys" -Filter "*ca-cert*" | Format-Table Name, Length
}
exit 1
}
}
} else {
Write-Host "ERROR: TrustedUserCAKeys not found (required for X509, client source: ${{ matrix.client_key_source }})"
exit 1
}
# For X509: AuthorizedKeysFile should NOT be set (server uses CA verification only)
if ($configContent -match "AuthorizedKeysFile\s+") {
Write-Host "WARNING: AuthorizedKeysFile is set for X509 auth - this may prevent CA verification"
Write-Host "Server will only use CA verification if AuthorizedKeysFile is NOT set"
} else {
Write-Host "AuthorizedKeysFile not set: OK (server will use CA verification)"
}
# Verify client cert files exist (for x509 file case)
if ("${{ matrix.client_key_source }}" -eq "x509") {
$userCertPath = $env:CLIENT_CERT_FILE
$userKeyPath = $env:CLIENT_KEY_FILE
if ([string]::IsNullOrEmpty($userCertPath)) {
$userCertPath = "keys\testuser-cert.der"
}
if ([string]::IsNullOrEmpty($userKeyPath)) {
$userKeyPath = "keys\testuser-key.der"
}
# Fallback to fred if testuser certs don't exist
if (-not (Test-Path $userCertPath)) {
Write-Host "WARNING: testuser-cert.der not found, trying fred-cert.der"
$userCertPath = "keys\fred-cert.der"
}
if (-not (Test-Path $userKeyPath)) {
Write-Host "WARNING: testuser-key.der not found, trying fred-key.der"
$userKeyPath = "keys\fred-key.der"
}
Write-Host "Verifying client cert files for X509 authentication..."
if (-not (Test-Path $userCertPath)) {
Write-Host "ERROR: Client cert not found: $userCertPath"
exit 1
}
if (-not (Test-Path $userKeyPath)) {
Write-Host "ERROR: Client key not found: $userKeyPath"
exit 1
}
Write-Host "Client cert files found: OK"
$certInfo = Get-Item $userCertPath
$keyInfo = Get-Item $userKeyPath
Write-Host " $($certInfo.Name): $($certInfo.Length) bytes"
Write-Host " $($keyInfo.Name): $($keyInfo.Length) bytes"
}
}
if (-not $hasHostKey) {
Write-Host "ERROR: No host key configuration found in config file!"
exit 1
}
Write-Host "Host key configuration verified: OK"
- name: Verify dependencies and environment
working-directory: ${{ github.workspace }}\wolfssh
shell: pwsh
run: |
$sshdPath = (Get-Content env:SSHD_PATH)
$sshdDir = Split-Path -Parent $sshdPath
Write-Host "=== Verifying wolfsshd.exe environment ==="
Write-Host "Executable: $sshdPath"
Write-Host "Directory: $sshdDir"
# Check if DLL is present
$dllPath = Join-Path $sshdDir "wolfssl.dll"
if (Test-Path $dllPath) {
Write-Host "✓ wolfssl.dll found"
$dllInfo = Get-Item $dllPath
Write-Host " Size: $($dllInfo.Length) bytes"
Write-Host " Modified: $($dllInfo.LastWriteTime)"
} else {
Write-Host "✗ wolfssl.dll NOT FOUND in $sshdDir"
Write-Host "Files in directory:"
Get-ChildItem -Path $sshdDir | Select-Object Name, Length | Format-Table
}
# Check config file
$configPath = "sshd_config_test"
if (Test-Path $configPath) {
Write-Host "✓ Config file found: $configPath"
} else {
Write-Host "✗ Config file NOT FOUND: $configPath"
}
# Note: Direct execution will fail with "StartServiceCtrlDispatcher failed"
# This is expected - the executable is built as a service and must run via SCM
Write-Host ""
Write-Host "Note: wolfsshd.exe is built as a Windows service."
Write-Host "Direct execution will fail (this is expected)."
Write-Host "It must be started via Service Control Manager (sc.exe)."
- name: Grant service (LocalSystem) access to config, keys, and executable
working-directory: ${{ github.workspace }}\wolfssh
shell: pwsh
run: |
# wolfsshd runs as LocalSystem; it must be able to read config, key files, and run the exe.
# Grant NT AUTHORITY\SYSTEM read+execute on the entire wolfssh tree so the service can:
# - run wolfsshd.exe (and load wolfssl.dll if dynamic)
# - read sshd_config_test and all files under keys/
# /T = apply to existing files and subdirs; (OI)(CI) = inherit to new objects
$wolfsshRoot = (Get-Location).Path
Write-Host "Granting SYSTEM (RX) on entire wolfssh tree: $wolfsshRoot"
icacls $wolfsshRoot /grant "NT AUTHORITY\SYSTEM:(OI)(CI)RX" /T /q
if ($LASTEXITCODE -ne 0) {
Write-Host "WARNING: icacls on wolfssh root failed, trying config and keys only"
$configPathFull = (Resolve-Path "sshd_config_test" -ErrorAction Stop).Path
$keysDir = (Resolve-Path "keys" -ErrorAction Stop).Path
icacls $configPathFull /grant "NT AUTHORITY\SYSTEM:R" /q
icacls $keysDir /grant "NT AUTHORITY\SYSTEM:(OI)(CI)R" /T /q
$sshdPath = $env:SSHD_PATH
if ($sshdPath -and (Test-Path $sshdPath)) {
$sshdDir = (Resolve-Path (Split-Path -Parent $sshdPath)).Path
icacls $sshdDir /grant "NT AUTHORITY\SYSTEM:(OI)(CI)RX" /T /q
}
}
Write-Host "Done."
- name: Test cert store access as LocalSystem
if: matrix.server_key_source == 'store'
working-directory: ${{ github.workspace }}\wolfssh
shell: pwsh
run: |
# Run the standalone cert store test AS LocalSystem using a scheduled task.
# This verifies that the service account can actually access the private key.
Write-Host "=== Testing cert store access as LocalSystem ==="
$testExe = $env:WINCERTSTORE_TEST_PATH
if (-not $testExe -or -not (Test-Path $testExe)) {
Write-Host "Skipping LocalSystem test (win-cert-store-test.exe not found)"
exit 0
}
$store = "My"
$subject = "wolfSSH-Test-Server"
$location = "LOCAL_MACHINE"
$outputFile = "$env:TEMP\localsystem-cert-test-output.txt"
# Create a batch script to run the test and capture output
$batchScript = @"
"$testExe" $store $subject $location > "$outputFile" 2>&1
echo EXIT_CODE=%ERRORLEVEL% >> "$outputFile"
"@
$batchPath = "$env:TEMP\run-cert-test.cmd"
$batchScript | Out-File -FilePath $batchPath -Encoding ASCII
# Create a scheduled task that runs as SYSTEM
$taskName = "WolfSSH-CertStoreTest"
$action = New-ScheduledTaskAction -Execute "cmd.exe" -Argument "/c `"$batchPath`""
$principal = New-ScheduledTaskPrincipal -UserId "NT AUTHORITY\SYSTEM" -LogonType ServiceAccount -RunLevel Highest
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries
# Remove existing task if present
Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue
# Register and run the task
Register-ScheduledTask -TaskName $taskName -Action $action -Principal $principal -Settings $settings | Out-Null
Start-ScheduledTask -TaskName $taskName
# Wait for task to complete (max 30 seconds)
$timeout = 30
$elapsed = 0
while ($elapsed -lt $timeout) {
Start-Sleep -Seconds 1
$elapsed++
$task = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue
if ($task.State -eq "Ready") {
break
}
}
# Get task result
$taskInfo = Get-ScheduledTaskInfo -TaskName $taskName -ErrorAction SilentlyContinue
Write-Host "Task last run result: $($taskInfo.LastTaskResult)"
# Display output
if (Test-Path $outputFile) {
Write-Host "=== LocalSystem cert store test output ==="
Get-Content $outputFile
Write-Host "=== End output ==="
# Check for success
$content = Get-Content $outputFile -Raw
if ($content -match "EXIT_CODE=0") {
Write-Host "SUCCESS: LocalSystem can access the certificate private key"
} else {
Write-Host "FAILURE: LocalSystem cannot access the certificate private key"
Write-Host "This explains why the wolfsshd service fails to start."
# Don't exit with error - let the service test provide the final verdict
}
} else {
Write-Host "WARNING: No output file generated"
}
# Cleanup
Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue
Remove-Item $batchPath -ErrorAction SilentlyContinue
Remove-Item $outputFile -ErrorAction SilentlyContinue
- name: Test wolfsshd config loading as LocalSystem
if: matrix.server_key_source == 'store'
working-directory: ${{ github.workspace }}\wolfssh
shell: pwsh
run: |
# Try to get more info by testing if wolfsshd can at least parse the config
# We'll create a small test that verifies LocalSystem can read all config-referenced files
Write-Host "=== Testing config file access as LocalSystem ==="
$configPath = (Resolve-Path "sshd_config_test").Path
$outputFile = "$env:TEMP\localsystem-config-test-output.txt"
# Create a PowerShell script to test file access as SYSTEM
$testScript = @'
$ErrorActionPreference = "Continue"
$configPath = $args[0]
$outputPath = $args[1]
$results = @()
$results += "Testing config access as: $([System.Security.Principal.WindowsIdentity]::GetCurrent().Name)"
$results += ""
# Test config file
$results += "Config file: $configPath"
if (Test-Path $configPath) {
$results += " Exists: YES"
try {
$content = Get-Content $configPath -Raw
$results += " Readable: YES"
$results += " Content:"
$content -split "`n" | ForEach-Object { $results += " $_" }
$results += ""
# Extract and test TrustedUserCAKeys
if ($content -match "TrustedUserCAKeys\s+([^\r\n]+)") {
$caPath = $matches[1].Trim()
$results += "TrustedUserCAKeys: $caPath"
if (Test-Path $caPath) {
$results += " Exists: YES"
try {
$caContent = Get-Content $caPath -Raw
$results += " Readable: YES ($($caContent.Length) bytes)"
} catch {
$results += " Readable: NO - $_"
}
} else {
$results += " Exists: NO"
}
}
} catch {
$results += " Readable: NO - $_"
}
} else {
$results += " Exists: NO"
}
$results | Out-File -FilePath $outputPath -Encoding UTF8
'@
$scriptPath = "$env:TEMP\test-config-access.ps1"
$testScript | Out-File -FilePath $scriptPath -Encoding UTF8
# Create scheduled task to run as SYSTEM
$taskName = "WolfSSH-ConfigAccessTest"
$action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-ExecutionPolicy Bypass -File `"$scriptPath`" `"$configPath`" `"$outputFile`""
$principal = New-ScheduledTaskPrincipal -UserId "NT AUTHORITY\SYSTEM" -LogonType ServiceAccount -RunLevel Highest
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries
Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue
Register-ScheduledTask -TaskName $taskName -Action $action -Principal $principal -Settings $settings | Out-Null
Start-ScheduledTask -TaskName $taskName
# Wait for completion
$timeout = 30
$elapsed = 0
while ($elapsed -lt $timeout) {
Start-Sleep -Seconds 1
$elapsed++
$task = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue
if ($task.State -eq "Ready") { break }
}
# Show results
if (Test-Path $outputFile) {
Write-Host "=== LocalSystem config access test results ==="
Get-Content $outputFile
Write-Host "=== End results ==="
} else {
Write-Host "WARNING: No output file generated"
}
# Cleanup
Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue
Remove-Item $scriptPath -ErrorAction SilentlyContinue
Remove-Item $outputFile -ErrorAction SilentlyContinue
- name: Dry-run wolfsshd as LocalSystem (cert store diagnostics)
if: matrix.server_key_source == 'store'
working-directory: ${{ github.workspace }}\wolfssh
shell: pwsh
run: |
# Run wolfsshd in non-daemon test mode AS LOCALSYSTEM via scheduled
# task. This captures the exact error that the service would hit.
$sshdPath = (Get-Content env:SSHD_PATH -ErrorAction SilentlyContinue)
if (-not $sshdPath -or -not (Test-Path $sshdPath)) {
Write-Host "Skipping: wolfsshd.exe not found"
exit 0
}
$sshdPathFull = (Resolve-Path $sshdPath).Path
$configPathFull = (Resolve-Path "sshd_config_test").Path
$port = ${{env.TEST_PORT}}
# Output goes to a temp file that LocalSystem can write to
$outFile = "$env:TEMP\wolfsshd-localsystem-dryrun.txt"
# We wrap the call in cmd /c so stdout+stderr go to the file
$cmdLine = "`"$sshdPathFull`" -D -t -d -f `"$configPathFull`" -p $port"
Write-Host "Will run as SYSTEM: $cmdLine"
# Create scheduled task to run as SYSTEM
$taskName = "WolfSSH-DryRunLocalSystem"
$action = New-ScheduledTaskAction `
-Execute "cmd.exe" `
-Argument "/c $cmdLine > `"$outFile`" 2>&1"
$principal = New-ScheduledTaskPrincipal `
-UserId "NT AUTHORITY\SYSTEM" `
-LogonType ServiceAccount -RunLevel Highest
$settings = New-ScheduledTaskSettingsSet `
-AllowStartIfOnBatteries -DontStopIfGoingOnBatteries
Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue
Register-ScheduledTask -TaskName $taskName -Action $action -Principal $principal -Settings $settings | Out-Null
Start-ScheduledTask -TaskName $taskName
# Wait for completion (wolfsshd -t exits quickly)
$timeout = 30
$elapsed = 0
while ($elapsed -lt $timeout) {
Start-Sleep -Seconds 1
$elapsed++
$task = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue
if ($task.State -eq "Ready") { break }
}
# Get exit code
$taskInfo = Get-ScheduledTaskInfo -TaskName $taskName -ErrorAction SilentlyContinue
if ($taskInfo) {
Write-Host "Scheduled task last result: $($taskInfo.LastTaskResult)"
}
# Show output
Write-Host "=== wolfsshd dry-run as LocalSystem ==="
if (Test-Path $outFile) {
Get-Content $outFile
} else {
Write-Host "(no output file generated)"
}
Write-Host "=== end LocalSystem dry-run ==="
# Cleanup
Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue
Remove-Item $outFile -ErrorAction SilentlyContinue
- name: Start echoserver with cert store (cert store matrix – more debug logs)
if: matrix.server_key_source == 'store'
working-directory: ${{ github.workspace }}\wolfssh
shell: pwsh
run: |
# For cert store we use echoserver instead of wolfsshd for now (better debug logs).
# Start echoserver detached (via cmd start /B) so it survives after this step ends;
# otherwise the runner may kill the process when the step completes.
$echoserverPath = $env:ECHOSERVER_PATH
if (-not $echoserverPath -or -not (Test-Path $echoserverPath)) {
Write-Host "ERROR: echoserver.exe not found (ECHOSERVER_PATH not set or missing)"
Get-ChildItem -Path "${{ github.workspace }}" -Recurse -Filter "echoserver.exe" -ErrorAction SilentlyContinue | Select-Object FullName
exit 1
}
$exeDir = Split-Path -Parent $echoserverPath
$exeName = Split-Path -Leaf $echoserverPath
$store = "My"
$subject = "wolfSSH-Test-Server"
$location = "LOCAL_MACHINE"
$port = ${{env.TEST_PORT}}
$spec = "${store}:${subject}:${location}"
# Build echoserver arguments. Besides the cert-store host key (-W)
# and port, the echoserver needs:
# -a <CA cert> so it can verify client X.509 certs
# -K testuser:<client cert> so the user-auth callback recognises testuser
$echoArgs = @("-W", $spec, "-p", $port)
$wolfsshRoot = "${{ github.workspace }}\wolfssh"
# CA certificate for client-cert verification
$caCertPem = Join-Path $wolfsshRoot "keys\ca-cert-ecc.pem"
if (Test-Path $caCertPem) {
$echoArgs += "-a", $caCertPem
Write-Host "CA cert for client verification: $caCertPem"
} else {
Write-Host "WARNING: CA cert not found at $caCertPem (client cert auth will fail)"
}
# Register testuser with their certificate so the auth callback accepts them
$clientCert = $env:CLIENT_CERT_FILE
if ([string]::IsNullOrEmpty($clientCert)) {
$clientCert = Join-Path $wolfsshRoot "keys\testuser-cert.der"
}
# Resolve to absolute path (CLIENT_CERT_FILE may be relative)
if (Test-Path $clientCert) {
$clientCert = (Resolve-Path $clientCert).Path
$echoArgs += "-K", "testuser:$clientCert"
Write-Host "Registered testuser cert: $clientCert"
} else {
Write-Host "WARNING: Client cert not found at $clientCert (testuser auth will fail)"
}
Write-Host "=== Starting echoserver with cert store (detached, debug logs) ==="
$argStr = ($echoArgs | ForEach-Object { $_ }) -join " "
Write-Host "Command: $echoserverPath $argStr"
# Redirect stdout+stderr to a log file so we can inspect debug output later.
# Use cmd /c with output redirection to run the echoserver detached.
$echoLogFile = Join-Path $wolfsshRoot "echoserver_debug.log"
Add-Content -Path $env:GITHUB_ENV -Value "ECHOSERVER_LOG=$echoLogFile"
Write-Host "Debug log: $echoLogFile"
$cmdLine = "`"$echoserverPath`" $argStr > `"$echoLogFile`" 2>&1"
Start-Process -FilePath "cmd.exe" `
-ArgumentList "/c", "start", "/B", "cmd", "/c", $cmdLine `
-WorkingDirectory $exeDir -NoNewWindow -Wait:$false
Start-Sleep -Seconds 2
$proc = Get-Process -Name "echoserver" -ErrorAction SilentlyContinue | Select-Object -First 1
if ($proc) {
Add-Content -Path $env:GITHUB_ENV -Value "ECHOSERVER_PID=$($proc.Id)"
Write-Host "echoserver started with PID $($proc.Id)"
} else {
Write-Host "WARNING: echoserver process not found by name after start"
}
# Wait for port to be listening (max 15 seconds)
$timeout = 15
$elapsed = 0
while ($elapsed -lt $timeout) {
Start-Sleep -Seconds 1
$elapsed++
try {
$conn = New-Object System.Net.Sockets.TcpClient("127.0.0.1", $port)
if ($conn.Connected) { $conn.Close(); break }
} catch {}
$stillRunning = Get-Process -Name "echoserver" -ErrorAction SilentlyContinue
if ($stillRunning) { continue }
Write-Host "ERROR: echoserver exited before port was ready"
exit 1
}
if ($elapsed -ge $timeout) {
Write-Host "WARNING: Port $port not listening after ${timeout}s (echoserver may still be starting)"
} else {
Write-Host "echoserver is listening on port $port"
}
- name: Test SFTP against echoserver (cert store server – debug logs)
if: matrix.server_key_source == 'store'
working-directory: ${{ github.workspace }}\wolfssh
shell: pwsh
run: |
Write-Host "=== SFTP test against echoserver (cert store host key) ==="
$testPort = ${{env.TEST_PORT}}
$sftpPath = (Get-Content env:SFTP_PATH)
if (-not (Test-Path $sftpPath)) {
Write-Host "ERROR: wolfsftp.exe not found at $sftpPath"
exit 1
}
# Test commands
$testCommands = @"
pwd
ls
quit
"@
$testCommands | Out-File -FilePath sftp_echo_commands.txt -Encoding ASCII
# Build SFTP args (same logic as main SFTP test step)
$sftpArgs = @(
"-u", "testuser",
"-h", "localhost",
"-p", "$testPort"
)
if ("${{ matrix.client_key_source }}" -eq "store") {
$clientSubject = $env:CLIENT_CERT_SUBJECT
if ([string]::IsNullOrEmpty($clientSubject)) {
Write-Host "ERROR: CLIENT_CERT_SUBJECT not set"; exit 1
}
$certStoreSpec = "My:${clientSubject}:CURRENT_USER"
$sftpArgs += "-W", $certStoreSpec
$caCertPath = "keys\ca-cert-ecc.der"
if (Test-Path $caCertPath) {
$sftpArgs += "-A", (Resolve-Path $caCertPath).Path
}
$sftpArgs += "-X"
} elseif ("${{ matrix.client_key_source }}" -eq "x509") {
$certPath = $env:CLIENT_CERT_FILE
$keyPath = $env:CLIENT_KEY_FILE
if ([string]::IsNullOrEmpty($certPath)) { $certPath = "keys\testuser-cert.der" }
if ([string]::IsNullOrEmpty($keyPath)) { $keyPath = "keys\testuser-key.der" }
if (-not (Test-Path $certPath)) { $certPath = "keys\fred-cert.der" }
if (-not (Test-Path $keyPath)) { $keyPath = "keys\fred-key.der" }
$sftpArgs += "-J", (Resolve-Path $certPath).Path
$sftpArgs += "-i", (Resolve-Path $keyPath).Path
$caCertPath = "keys\ca-cert-ecc.der"
if (Test-Path $caCertPath) {
$sftpArgs += "-A", (Resolve-Path $caCertPath).Path
}
$sftpArgs += "-X"
}
Write-Host "Running: $sftpPath $($sftpArgs -join ' ')"
$process = Start-Process -FilePath $sftpPath `
-ArgumentList $sftpArgs `
-RedirectStandardInput "sftp_echo_commands.txt" `
-RedirectStandardOutput "sftp_echo_output.txt" `
-RedirectStandardError "sftp_echo_error.txt" `
-Wait -NoNewWindow -PassThru
Write-Host "SFTP (echoserver) exit code: $($process.ExitCode)"
Write-Host "=== SFTP Output (echoserver) ==="
if (Test-Path sftp_echo_output.txt) { Get-Content sftp_echo_output.txt }
Write-Host "=== SFTP Error (echoserver) ==="
if (Test-Path sftp_echo_error.txt) { Get-Content sftp_echo_error.txt }
# Dump echoserver debug log
$echoLog = $env:ECHOSERVER_LOG
if (-not [string]::IsNullOrEmpty($echoLog) -and (Test-Path $echoLog)) {
Write-Host "=== Echoserver Debug Log ==="
Get-Content $echoLog
Write-Host "=== End Echoserver Debug Log ==="
}
if ($process.ExitCode -ne 0) {
Write-Host "WARNING: SFTP against echoserver failed (exit $($process.ExitCode)) – will continue to wolfsshd test"
} else {
Write-Host "SFTP against echoserver succeeded"
}
- name: Stop echoserver before wolfsshd test
if: matrix.server_key_source == 'store'
shell: pwsh
run: |
$echoserverPid = $env:ECHOSERVER_PID
if (-not [string]::IsNullOrEmpty($echoserverPid)) {
Write-Host "Stopping echoserver (PID $echoserverPid) before starting wolfsshd"
Stop-Process -Id $echoserverPid -Force -ErrorAction SilentlyContinue
Start-Sleep -Seconds 2
}
# Also kill by name in case PID tracking missed it
Get-Process -Name "echoserver" -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
# Clear the env var so cleanup step doesn't try again
Add-Content -Path $env:GITHUB_ENV -Value "ECHOSERVER_PID="
- name: Validate wolfsshd config (non-daemon dry run)
working-directory: ${{ github.workspace }}\wolfssh
shell: pwsh
run: |
# Run wolfsshd in non-daemon test mode (-D -t -d) to validate
# config loading and cert store access. This gives us visible
# stdout/stderr output, unlike the service which logs to
# OutputDebugString.
$sshdPath = (Get-Content env:SSHD_PATH)
if (-not (Test-Path $sshdPath)) {
Write-Host "Skipping dry-run: wolfsshd.exe not found"
exit 0
}
$configPathFull = (Resolve-Path "sshd_config_test").Path
$port = ${{env.TEST_PORT}}
Write-Host "=== wolfsshd dry-run: $sshdPath -D -t -d -f $configPathFull -p $port ==="
$proc = Start-Process -FilePath $sshdPath `
-ArgumentList "-D", "-t", "-d", "-f", $configPathFull, "-p", $port `
-RedirectStandardOutput "wolfsshd_dryrun_out.txt" `
-RedirectStandardError "wolfsshd_dryrun_err.txt" `
-Wait -NoNewWindow -PassThru
Write-Host "Exit code: $($proc.ExitCode)"
Write-Host "=== stdout ==="
if (Test-Path wolfsshd_dryrun_out.txt) { Get-Content wolfsshd_dryrun_out.txt }
Write-Host "=== stderr ==="
if (Test-Path wolfsshd_dryrun_err.txt) { Get-Content wolfsshd_dryrun_err.txt }
Write-Host "=== end dry-run ==="
if ($proc.ExitCode -ne 0) {
Write-Host "WARNING: wolfsshd dry-run failed (exit $($proc.ExitCode))"
Write-Host "The service will likely also fail to start."
} else {
Write-Host "wolfsshd dry-run succeeded - config is valid"
}
- name: Start wolfSSHd as Windows service
working-directory: ${{ github.workspace }}\wolfssh
shell: pwsh
run: |
$sshdPath = (Get-Content env:SSHD_PATH)
if (-not (Test-Path $sshdPath)) {
Write-Host "ERROR: wolfsshd.exe not found at $sshdPath"
exit 1
}
# Get absolute path for service
$sshdPathFull = (Resolve-Path $sshdPath).Path
$configPathFull = (Resolve-Path "sshd_config_test").Path
# Service name
$serviceName = "wolfsshd"
# Remove service if it already exists
$existingService = Get-Service -Name $serviceName -ErrorAction SilentlyContinue
if ($existingService) {
Write-Host "Removing existing $serviceName service"
if ($existingService.Status -eq 'Running') {
Stop-Service -Name $serviceName -Force -ErrorAction SilentlyContinue
Start-Sleep -Seconds 2
}
sc.exe delete $serviceName | Out-Null
Start-Sleep -Seconds 2
}
# Show config file content for debugging
Write-Host "=== Config file content ==="
Get-Content $configPathFull
Write-Host "=== End config ==="
# Verify all files referenced in config are accessible
Write-Host "=== Verifying config file references ==="
$configContent = Get-Content $configPathFull -Raw
# Check TrustedUserCAKeys
if ($configContent -match "TrustedUserCAKeys\s+([^\r\n]+)") {
$caPath = $matches[1].Trim()
Write-Host "TrustedUserCAKeys: $caPath"
if (Test-Path $caPath) {
Write-Host " File exists: YES"
$acl = Get-Acl $caPath
$systemAccess = $acl.Access | Where-Object { $_.IdentityReference -like "*SYSTEM*" }
if ($systemAccess) {
Write-Host " SYSTEM access: $($systemAccess.FileSystemRights)"
} else {
Write-Host " WARNING: No explicit SYSTEM access (may inherit)"
}
} else {
Write-Host " ERROR: File not found!"
}
}
# Check HostKeyStoreSubject
if ($configContent -match "HostKeyStoreSubject\s+([^\r\n]+)") {
$subject = $matches[1].Trim()
Write-Host "HostKeyStoreSubject: '$subject'"
Write-Host " Length: $($subject.Length) chars"
# Show hex dump for debugging
$bytes = [System.Text.Encoding]::UTF8.GetBytes($subject)
$hex = ($bytes | ForEach-Object { '{0:X2}' -f $_ }) -join ' '
Write-Host " UTF-8 hex: $hex"
}
# Pre-service checks
$sshdDir = Split-Path -Parent $sshdPathFull
$dllPath = Join-Path $sshdDir "wolfssl.dll"
$libPath = Join-Path $sshdDir "wolfssl.lib"
Write-Host "=== Pre-service checks ==="
Write-Host "Executable: $sshdPathFull"
Write-Host "Config: $configPathFull"
Write-Host "Working directory (sshd dir): $sshdDir"
# wolfSSL: either DLL (dynamic) or static (.lib linked into exe)
if (Test-Path $dllPath) {
Write-Host "✓ wolfssl.dll found (dynamic build)"
$dllInfo = Get-Item $dllPath
Write-Host " Size: $($dllInfo.Length) bytes"
} elseif (Test-Path $libPath) {
Write-Host "✓ wolfssl.lib present, no wolfssl.dll - static build (no DLL required)"
} else {
Write-Host "WARNING: Neither wolfssl.dll nor wolfssl.lib in $sshdDir"
Write-Host "Files present:"
Get-ChildItem -Path $sshdDir | Select-Object Name, Length | Format-Table
# Continue anyway; service may still start if wolfssl is linked another way
}
if (-not (Test-Path $configPathFull)) {
Write-Host "ERROR: Config file not found: $configPathFull"
exit 1
} else {
Write-Host "✓ Config file found"
# Verify config file is readable
try {
$configContent = Get-Content $configPathFull -Raw
Write-Host " Size: $($configContent.Length) bytes"
} catch {
Write-Host "ERROR: Cannot read config file: $_"
exit 1
}
}
# Create the service with proper binpath
# Note: sc.exe requires the binPath to have the executable path and arguments
# The entire command line goes in binPath, with the exe path in quotes
# We do NOT include -E <log> here because LocalSystem only has RX on
# the wolfssh directory and cannot create a log file. Debug output
# from the service goes to OutputDebugString; for visible diagnostics
# we rely on the "Validate wolfsshd config" dry-run step.
$binPath = "`"$sshdPathFull`" -f `"$configPathFull`" -p ${{env.TEST_PORT}}"
Write-Host "Creating service with binpath: $binPath"
$createResult = sc.exe create $serviceName binPath= $binPath
if ($LASTEXITCODE -ne 0) {
Write-Host "ERROR: Failed to create service"
Write-Host $createResult
exit 1
}
Write-Host "Service created: $createResult"
# Set service to auto-start on failure (for debugging)
# This won't help if it exits cleanly, but might help with crashes
sc.exe failure $serviceName reset= 86400 actions= restart/5000/restart/5000/restart/5000 | Out-Null
# Start the service
Write-Host "Starting $serviceName service"
$startResult = sc.exe start $serviceName
if ($LASTEXITCODE -ne 0) {
Write-Host "ERROR: Failed to start service"
Write-Host $startResult
# Try to get service status for debugging
sc.exe query $serviceName
exit 1
}
Write-Host "Service started: $startResult"
# Wait a bit for service to start
Start-Sleep -Seconds 5
# Check service status
$service = Get-Service -Name $serviceName -ErrorAction SilentlyContinue
if (-not $service) {
Write-Host "ERROR: Service $serviceName not found after creation"
exit 1
}
if ($service.Status -ne 'Running') {
Write-Host "ERROR: Service is not running. Status: $($service.Status)"
# Get detailed service information
Write-Host "=== Service Query ==="
sc.exe query $serviceName
# Get service configuration to see the actual command
Write-Host "=== Service Configuration ==="
sc.exe qc $serviceName
# Check service error code and details
Write-Host "=== Service Error Code ==="
$serviceInfo = Get-CimInstance -ClassName Win32_Service -Filter "Name='$serviceName'" -ErrorAction SilentlyContinue
if ($serviceInfo) {
Write-Host "ExitCode: $($serviceInfo.ExitCode)"
Write-Host "State: $($serviceInfo.State)"
Write-Host "Status: $($serviceInfo.Status)"
Write-Host "PathName: $($serviceInfo.PathName)"
Write-Host "StartMode: $($serviceInfo.StartMode)"
}
# Try to get process exit code if it ran briefly
Write-Host "=== Checking for recent process exit ==="
$recentProcesses = Get-WinEvent -FilterHashtable @{LogName='System'; ID=7034,7035,7036} -MaxEvents 50 -ErrorAction SilentlyContinue |
Where-Object { $_.Message -like "*wolfsshd*" } |
Select-Object -First 5
if ($recentProcesses) {
Write-Host "Recent service events:"
$recentProcesses | ForEach-Object { Write-Host " $($_.TimeCreated): $($_.Message)" }
}
# Check event logs for errors
Write-Host "=== System Event Log (Service Control Manager) ==="
Get-EventLog -LogName System -Source "Service Control Manager" -Newest 20 -ErrorAction SilentlyContinue |
Where-Object { $_.Message -like "*wolfsshd*" -or $_.Message -like "*$serviceName*" } |
Select-Object TimeGenerated, EntryType, Message | Format-List
Write-Host "=== Application Event Log ==="
Get-EventLog -LogName Application -Newest 30 -ErrorAction SilentlyContinue |
Where-Object { $_.Source -like "*wolf*" -or $_.Message -like "*wolf*" } |
Select-Object TimeGenerated, Source, EntryType, Message | Format-List
# Check wolfSSL: DLL (dynamic) or .lib (static)
Write-Host "=== Checking wolfSSL (DLL or static) ==="
$sshdDir = Split-Path -Parent $sshdPathFull
$dllPath = Join-Path $sshdDir "wolfssl.dll"
$libPath = Join-Path $sshdDir "wolfssl.lib"
if (Test-Path $dllPath) {
Write-Host "wolfssl.dll: YES (dynamic build)"
} elseif (Test-Path $libPath) {
Write-Host "wolfssl.dll: NO; wolfssl.lib: YES (static build - OK)"
} else {
Write-Host "wolfssl.dll: NO; wolfssl.lib: NO"
Write-Host "Files in $sshdDir :"
Get-ChildItem -Path $sshdDir | Select-Object Name, Length | Format-Table
}
# Check if process is running
Write-Host "=== Checking if process is running ==="
$processes = Get-Process | Where-Object { $_.ProcessName -like "*wolfsshd*" }
if ($processes) {
Write-Host "Found processes:"
$processes | Format-Table Id, ProcessName, StartTime, Path
} else {
Write-Host "No wolfsshd processes found"
}
exit 1
}
Write-Host "wolfSSHd service is running (Status: $($service.Status))"
Add-Content -Path $env:GITHUB_ENV -Value "SSHD_SERVICE_NAME=$serviceName"
- name: Test SFTP connection against wolfsshd
working-directory: ${{ github.workspace }}\wolfssh
shell: pwsh
run: |
# First, verify server is running and accessible
Write-Host "=== Pre-flight checks (wolfsshd) ==="
$serviceName = $env:SSHD_SERVICE_NAME
$testPort = ${{env.TEST_PORT}}
if ($serviceName) {
$service = Get-Service -Name $serviceName -ErrorAction SilentlyContinue
if ($service) {
Write-Host "Service status: $($service.Status)"
if ($service.Status -ne "Running") {
Write-Host "ERROR: Service is not running!"
exit 1
}
} else {
Write-Host "WARNING: Service $serviceName not found"
}
}
# Test TCP connectivity to the server
Write-Host "Testing TCP connection to localhost:$testPort..."
try {
$tcpClient = New-Object System.Net.Sockets.TcpClient
$connect = $tcpClient.BeginConnect("localhost", $testPort, $null, $null)
$wait = $connect.AsyncWaitHandle.WaitOne(3000, $false)
if ($wait) {
$tcpClient.EndConnect($connect)
Write-Host "TCP connection successful: OK"
$tcpClient.Close()
} else {
Write-Host "ERROR: TCP connection timeout - server may not be listening on port $testPort"
exit 1
}
} catch {
Write-Host "ERROR: TCP connection failed: $_"
Write-Host "Checking if port $testPort is in use..."
$listener = Get-NetTCPConnection -LocalPort $testPort -ErrorAction SilentlyContinue
if ($listener) {
Write-Host "Port $testPort is in use by:"
$listener | Format-Table LocalAddress, LocalPort, State, OwningProcess
} else {
Write-Host "Port $testPort is not in use"
}
exit 1
}
# Verify server config file exists and is readable
if (Test-Path "sshd_config_test") {
Write-Host "Server config file exists: OK"
$configContent = Get-Content "sshd_config_test" -Raw
Write-Host "=== Server Config Content ==="
Write-Host $configContent
Write-Host "=== End Server Config ==="
if ($configContent -match "TrustedUserCAKeys\s+([^\r\n]+)") {
$caPath = $matches[1].Trim()
Write-Host "TrustedUserCAKeys found: $caPath"
if (Test-Path $caPath) {
Write-Host " CA cert file exists: OK"
} else {
Write-Host " ERROR: CA cert file not found: $caPath"
}
} else {
Write-Host "ERROR: TrustedUserCAKeys not found in config"
}
if ($configContent -match "HostCertificate\s+([^\r\n]+)") {
$hostCertPath = $matches[1].Trim()
Write-Host "HostCertificate found: $hostCertPath"
if (Test-Path $hostCertPath) {
Write-Host " Host cert file exists: OK"
} else {
Write-Host " ERROR: Host cert file not found: $hostCertPath"
}
} elseif ($configContent -match "HostKeyStore\s+") {
Write-Host "HostKeyStore configured (cert store) – no HostCertificate file needed"
} else {
Write-Host "ERROR: Neither HostCertificate nor HostKeyStore found in config"
}
if ($configContent -match "HostKey\s+([^\r\n]+)") {
$hostKeyPath = $matches[1].Trim()
Write-Host "HostKey found: $hostKeyPath"
if (Test-Path $hostKeyPath) {
Write-Host " Host key file exists: OK"
} else {
Write-Host " ERROR: Host key file not found: $hostKeyPath"
}
} elseif ($configContent -match "HostKeyStoreSubject\s+([^\r\n]+)") {
Write-Host "HostKeyStoreSubject found: $($matches[1].Trim()) (cert store)"
}
} else {
Write-Host "ERROR: sshd_config_test not found"
exit 1
}
# Verify service is using the correct config
$serviceName = $env:SSHD_SERVICE_NAME
if ($serviceName) {
Write-Host "=== Verifying Service Configuration ==="
$serviceConfig = sc.exe qc $serviceName
Write-Host $serviceConfig
if ($serviceConfig -match "BINARY_PATH_NAME\s*:\s*(.+)") {
$binPath = $matches[1].Trim()
Write-Host "Service binary path: $binPath"
if ($binPath -match "sshd_config_test") {
Write-Host " Config file in service path: OK"
} else {
Write-Host " WARNING: Config file path not found in service binary path"
}
}
}
# First test with SSH client (like sshd_x509_test.sh) to verify basic X509 connection
$sshPath = (Get-Content env:SSH_PATH -ErrorAction SilentlyContinue)
if ($sshPath -and (Test-Path $sshPath)) {
Write-Host "=== Testing X509 connection with SSH client (like sshd_x509_test.sh) ==="
if ("${{ matrix.client_key_source }}" -eq "x509") {
$certPath = "keys\fred-cert.der"
$keyPath = "keys\fred-key.der"
$caCertPath = "keys\ca-cert-ecc.der"
if ((Test-Path $certPath) -and (Test-Path $keyPath) -and (Test-Path $caCertPath)) {
$certPathFull = (Resolve-Path $certPath).Path
$keyPathFull = (Resolve-Path $keyPath).Path
$caCertPathFull = (Resolve-Path $caCertPath).Path
$sshTestArgs = @(
"-u", "testuser",
"-h", "localhost",
"-p", "${{env.TEST_PORT}}",
"-i", $keyPathFull,
"-J", $certPathFull,
"-A", $caCertPathFull,
"-X",
"-c", "pwd"
)
Write-Host "Running SSH client test: $sshPath $($sshTestArgs -join ' ')"
$sshProcess = Start-Process -FilePath $sshPath `
-ArgumentList $sshTestArgs `
-RedirectStandardOutput "ssh_test_output.txt" `
-RedirectStandardError "ssh_test_error.txt" `
-Wait -NoNewWindow -PassThru
Write-Host "SSH client exit code: $($sshProcess.ExitCode)"
if (Test-Path ssh_test_output.txt) {
Write-Host "=== SSH Output ==="
Get-Content ssh_test_output.txt
}
if (Test-Path ssh_test_error.txt) {
Write-Host "=== SSH Error ==="
Get-Content ssh_test_error.txt
}
if ($sshProcess.ExitCode -eq 0) {
Write-Host "✓ SSH client X509 connection successful"
} else {
Write-Host "✗ SSH client X509 connection failed (exit code: $($sshProcess.ExitCode))"
Write-Host "This suggests the X509 authentication itself may be failing"
}
} else {
Write-Host "WARNING: Required cert files not found for SSH client test"
}
}
} else {
Write-Host "WARNING: SSH client not found (SSH_PATH not set or file missing)"
Write-Host "SSH client test skipped - proceeding with SFTP test only"
}
$sftpPath = (Get-Content env:SFTP_PATH)
if (-not (Test-Path $sftpPath)) {
Write-Host "ERROR: wolfsftp.exe not found at $sftpPath"
exit 1
}
# Create test commands file
$testCommands = @"
pwd
ls
quit
"@
$testCommands | Out-File -FilePath sftp_commands.txt -Encoding ASCII
# Build SFTP command arguments
$sftpArgs = @(
"-u", "testuser",
"-h", "localhost",
"-p", "${{env.TEST_PORT}}"
)
if ("${{ matrix.client_key_source }}" -eq "store") {
$clientSubject = $env:CLIENT_CERT_SUBJECT
Write-Host "CLIENT_CERT_SUBJECT = '$clientSubject'"
if ([string]::IsNullOrEmpty($clientSubject)) {
Write-Host "ERROR: CLIENT_CERT_SUBJECT not set"
exit 1
}
$certStoreSpec = "My:${clientSubject}:CURRENT_USER"
Write-Host "Cert store spec: $certStoreSpec"
$sftpArgs += "-W", $certStoreSpec
# CA cert for host verification (use DER format)
$caCertPath = "keys\ca-cert-ecc.der"
if (Test-Path $caCertPath) {
$caCertPathFull = (Resolve-Path $caCertPath).Path
$sftpArgs += "-A", $caCertPathFull
Write-Host "CA cert: $caCertPathFull"
} else {
Write-Host "WARNING: CA cert not found: $caCertPath (host verification may fail)"
}
# Add -X flag to ignore IP checks on peer vs peer certificate
$sftpArgs += "-X"
} elseif ("${{ matrix.client_key_source }}" -eq "x509") {
# X509 certificate authentication: use certificate + private key
# Use testuser certificate (created by renewcerts.sh) to match the username
$certPath = $env:CLIENT_CERT_FILE
$keyPath = $env:CLIENT_KEY_FILE
if ([string]::IsNullOrEmpty($certPath)) {
$certPath = "keys\testuser-cert.der"
}
if ([string]::IsNullOrEmpty($keyPath)) {
$keyPath = "keys\testuser-key.der"
}
# Fallback to fred if testuser certs don't exist
if (-not (Test-Path $certPath)) {
Write-Host "WARNING: $certPath not found, trying fred-cert.der"
$certPath = "keys\fred-cert.der"
}
if (-not (Test-Path $keyPath)) {
Write-Host "WARNING: $keyPath not found, trying fred-key.der"
$keyPath = "keys\fred-key.der"
}
$caCertPath = "keys\ca-cert-ecc.der"
Write-Host "Verifying X509 certificate files..."
Write-Host "Current directory: $(Get-Location)"
if (-not (Test-Path $certPath)) {
Write-Host "ERROR: Client cert not found: $certPath"
Write-Host "Files in keys directory:"
if (Test-Path "keys") {
Get-ChildItem "keys" -Filter "*fred*" | Format-Table Name, Length
}
exit 1
}
if (-not (Test-Path $keyPath)) {
Write-Host "ERROR: Client key not found: $keyPath"
exit 1
}
# Verify file sizes (should not be empty)
$certInfo = Get-Item $certPath
$keyInfo = Get-Item $keyPath
Write-Host "Client cert: $($certInfo.FullName) ($($certInfo.Length) bytes)"
Write-Host "Client key: $($keyInfo.FullName) ($($keyInfo.Length) bytes)"
# Use absolute paths to avoid any path issues
$certPathFull = (Resolve-Path $certPath).Path
$keyPathFull = (Resolve-Path $keyPath).Path
$sftpArgs += "-J", $certPathFull
$sftpArgs += "-i", $keyPathFull
# CA cert for host verification (use DER format as per test script)
if (Test-Path $caCertPath) {
$caCertPathFull = (Resolve-Path $caCertPath).Path
$sftpArgs += "-A", $caCertPathFull
Write-Host "CA cert: $caCertPathFull"
} else {
Write-Host "WARNING: CA cert not found: $caCertPath (host verification may fail)"
}
# Add -X flag to ignore IP checks on peer vs peer certificate (as per test script)
$sftpArgs += "-X"
}
# X509 certificate auth only - no password fallback
Write-Host "Running: $sftpPath $($sftpArgs -join ' ')"
Write-Host "Test matrix: server=${{ matrix.server_key_source }}, client=${{ matrix.client_key_source }}"
# Run SFTP with commands
# Note: This may fail on auth, but we're testing that key exchange works
$process = Start-Process -FilePath $sftpPath `
-ArgumentList $sftpArgs `
-RedirectStandardInput "sftp_commands.txt" `
-RedirectStandardOutput "sftp_output.txt" `
-RedirectStandardError "sftp_error.txt" `
-Wait -NoNewWindow -PassThru
Write-Host "SFTP exit code: $($process.ExitCode)"
Write-Host "=== SFTP Output ==="
if (Test-Path sftp_output.txt) {
Get-Content sftp_output.txt
}
Write-Host "=== SFTP Error ==="
if (Test-Path sftp_error.txt) {
Get-Content sftp_error.txt
}
# Dump echoserver debug log (if running echoserver instead of wolfsshd)
$echoLog = $env:ECHOSERVER_LOG
if (-not [string]::IsNullOrEmpty($echoLog) -and (Test-Path $echoLog)) {
Write-Host "=== Echoserver Debug Log ==="
Get-Content $echoLog
Write-Host "=== End Echoserver Debug Log ==="
}
# For X509 tests: check server logs for certificate verification errors
if ("${{ matrix.client_key_source }}" -eq "x509" -or "${{ matrix.client_key_source }}" -eq "store") {
Write-Host "=== Checking server logs for X509 certificate verification ==="
$serviceName = $env:SSHD_SERVICE_NAME
if ($serviceName) {
# Check Application event log for wolfSSH errors
Get-EventLog -LogName Application -Newest 50 -ErrorAction SilentlyContinue |
Where-Object { $_.Source -like "*wolf*" -or $_.Message -like "*wolf*" -or $_.Message -like "*cert*" -or $_.Message -like "*CA*" } |
Select-Object TimeGenerated, Source, EntryType, Message | Format-List
}
}
# Check if we got past key exchange (connection established)
$output = ""
$errOut = ""
if (Test-Path sftp_output.txt) {
$output = Get-Content sftp_output.txt -Raw
}
if (Test-Path sftp_error.txt) {
$errOut = Get-Content sftp_error.txt -Raw
}
# Failure indicators
if ($output -match "connection.*refused" -or $errOut -match "connection.*refused") {
Write-Host "ERROR: Connection refused - server may not be running"
exit 1
}
if ($output -match "key.*exchange.*fail" -or $errOut -match "key.*exchange.*fail") {
Write-Host "ERROR: Key exchange failed - cert store key may not be working"
exit 1
}
if ($output -match "Couldn't connect" -or $errOut -match "Couldn't connect") {
Write-Host "ERROR: SFTP could not connect"
if ("${{ matrix.client_key_source }}" -eq "x509" -or "${{ matrix.client_key_source }}" -eq "store") {
Write-Host "For X509 auth, check:"
Write-Host " 1. Server has TrustedUserCAKeys configured correctly"
Write-Host " 2. Client cert (testuser-cert.der) is signed by CA (ca-cert-ecc.pem/der) and has CN=testuser"
Write-Host " 3. Server can read the CA cert file"
} else {
Write-Host "Check authorized_keys, user, or server configuration"
}
exit 1
}
if ($process.ExitCode -ne 0) {
Write-Host "ERROR: SFTP client exited with code $($process.ExitCode)"
exit 1
}
Write-Host "Test completed - key exchange and SFTP connection succeeded"
- name: Test SSH client connection
working-directory: ${{ github.workspace }}\wolfssh
shell: pwsh
run: |
$sshPath = (Get-Content env:SSH_PATH -ErrorAction SilentlyContinue)
if (-not $sshPath -or -not (Test-Path $sshPath)) {
Write-Host "WARNING: wolfssh.exe not found, skipping SSH client test"
Write-Host "This is OK if the SSH client wasn't built"
exit 0
}
Write-Host "Found wolfssh.exe at: $sshPath" -ForegroundColor Green
# Build SSH client command arguments
$sshArgs = @(
"-l", "testuser",
"-p", "${{env.TEST_PORT}}",
"localhost"
)
# Set authentication method based on client_key_source (X509 only)
if ("${{ matrix.client_key_source }}" -eq "store") {
$clientSubject = $env:CLIENT_CERT_SUBJECT
$env:WOLFSSH_CERT_STORE = "My:$clientSubject:CURRENT_USER"
Write-Host "Using cert store key via WOLFSSH_CERT_STORE: $env:WOLFSSH_CERT_STORE" -ForegroundColor Yellow
# Add -X flag to ignore IP checks
$sshArgs += "-X"
} elseif ("${{ matrix.client_key_source }}" -eq "x509") {
# X509 certificate authentication: use certificate + private key
# Use testuser certificate (created by renewcerts.sh) to match the username
$certPath = $env:CLIENT_CERT_FILE
$keyPath = $env:CLIENT_KEY_FILE
if ([string]::IsNullOrEmpty($certPath)) {
$certPath = "keys\testuser-cert.der"
}
if ([string]::IsNullOrEmpty($keyPath)) {
$keyPath = "keys\testuser-key.der"
}
# Fallback to fred if testuser certs don't exist
if (-not (Test-Path $certPath)) {
Write-Host "WARNING: $certPath not found, trying fred-cert.der"
$certPath = "keys\fred-cert.der"
}
if (-not (Test-Path $keyPath)) {
Write-Host "WARNING: $keyPath not found, trying fred-key.der"
$keyPath = "keys\fred-key.der"
}
$caCertPath = "keys\ca-cert-ecc.der"
if (-not (Test-Path $certPath) -or -not (Test-Path $keyPath)) {
Write-Host "WARNING: X509 cert/key not found, skipping SSH client test"
exit 0
}
$sshArgs += "-J", $certPath
$sshArgs += "-i", $keyPath
if (Test-Path $caCertPath) {
$sshArgs += "-A", $caCertPath
}
# Add -X flag to ignore IP checks (as per test script)
$sshArgs += "-X"
Write-Host "Using X509 certificate authentication" -ForegroundColor Yellow
}
Write-Host "Running: $sshPath $($sshArgs -join ' ')" -ForegroundColor Yellow
Write-Host "Test matrix: server=${{ matrix.server_key_source }}, client=${{ matrix.client_key_source }}" -ForegroundColor Cyan
# Run SSH client with a simple command (non-interactive)
# Use -N for no command, or -c for a command
$sshArgs += "-N" # No command, just test connection
$process = Start-Process -FilePath $sshPath `
-ArgumentList $sshArgs `
-RedirectStandardOutput "ssh_output.txt" `
-RedirectStandardError "ssh_error.txt" `
-Wait -NoNewWindow -PassThru
Write-Host "SSH client exit code: $($process.ExitCode)" -ForegroundColor $(if ($process.ExitCode -eq 0) { "Green" } else { "Yellow" })
Write-Host "=== SSH Client Output ===" -ForegroundColor Cyan
if (Test-Path ssh_output.txt) {
Get-Content ssh_output.txt
}
Write-Host "=== SSH Client Error ===" -ForegroundColor Cyan
if (Test-Path ssh_error.txt) {
Get-Content ssh_error.txt
}
# Check if we got past key exchange (connection established)
# Auth failure is OK - we're testing cert store key loading
$output = ""
$errOut = ""
if (Test-Path ssh_output.txt) {
$output = Get-Content ssh_output.txt -Raw
}
if (Test-Path ssh_error.txt) {
$errOut = Get-Content ssh_error.txt -Raw
}
# Success indicators: connection established, key exchange completed
# Failure indicators: connection refused, key exchange failed
if ($output -match "connection.*refused" -or $errOut -match "connection.*refused") {
Write-Host "ERROR: Connection refused - server may not be running" -ForegroundColor Red
exit 1
}
if ($output -match "key.*exchange.*fail" -or $errOut -match "key.*exchange.*fail") {
Write-Host "ERROR: Key exchange failed - cert store key may not be working" -ForegroundColor Red
exit 1
}
Write-Host "SSH client test completed - key exchange appears to have worked" -ForegroundColor Green
- name: Cleanup
if: always()
working-directory: ${{ github.workspace }}\wolfssh
shell: pwsh
run: |
# Stop echoserver if we started it (cert store matrix)
$echoserverPid = $env:ECHOSERVER_PID
if (-not [string]::IsNullOrEmpty($echoserverPid)) {
Write-Host "Stopping echoserver (PID $echoserverPid)"
Stop-Process -Id $echoserverPid -Force -ErrorAction SilentlyContinue
Start-Sleep -Seconds 1
}
# Stop and remove wolfSSHd service
$serviceName = $env:SSHD_SERVICE_NAME
if ([string]::IsNullOrEmpty($serviceName)) {
$serviceName = "wolfsshd"
}
$service = Get-Service -Name $serviceName -ErrorAction SilentlyContinue
if ($service) {
if ($service.Status -eq 'Running') {
Write-Host "Stopping $serviceName service"
Stop-Service -Name $serviceName -Force -ErrorAction SilentlyContinue
Start-Sleep -Seconds 2
}
Write-Host "Deleting $serviceName service"
sc.exe delete $serviceName | Out-Null
Start-Sleep -Seconds 1
}
# Remove test certificates from store
Get-ChildItem -Path "Cert:\CurrentUser\My" | Where-Object {
$_.Subject -like "*wolfSSH-Test*"
} | Remove-Item -Force
Write-Host "Cleaned up test certificates"