wolfssh/.github/workflows/windows-cert-store-test.yml

1717 lines
79 KiB
YAML

name: Windows Certificate Store Test
# Tests MS Certificate Store integration for wolfSSH. The matrix covers
# server host keys and client user keys coming from the cert store, from
# X.509 cert/key files, or both, plus an ECDSA cert store host key.
#
# Test flow per matrix entry:
# 1. Create testuser client cert (renewcerts.sh) and, for store cases,
# import/create certificates in the Windows certificate store.
# 2. If the server key comes from the store: run echoserver with -W and
# connect with the SFTP client.
# 3. Run wolfsshd as a Windows service and connect with the SFTP client.
#
# Two builds feed the matrix: one with OPENSSL_ALL (wolfSSL FPKI, UPN
# identity binding) and one without (subject CN binding).
on:
push:
branches: [ 'master', 'main', 'release/**' ]
pull_request:
branches: [ '*' ]
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
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
timeout-minutes: 30
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: ${{ 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 the Windows cert store API (not in the repo user_settings.h).
# Inserted into wolfssh/ide/winvs/user_settings.h, which the VS
# projects put on the include path before wolfssl/IDE/WIN. The insert
# lands before the closing include-guard #endif so the defines stay
# inside the guard.
# RFC 6187 names only one RSA X.509 algorithm, x509v3-ssh-rsa, and it
# signs with SHA-1, so both SHA-1 gates have to come down for any RSA
# certificate to negotiate:
# WC_SIG_MIN_HASH_TYPE - wc_SignatureVerify otherwise rejects
# SHA-1 at its SHA-256 floor (see the
# same define in tpm-ssh.yml).
# WOLFSSH_NO_SHA1_SOFT_DISABLE - x509v3-ssh-rsa is otherwise absent
# from cannedKeyAlgoNames, so the server
# never lists it in server-sig-algs and
# the client's RSA certificate fails
# PrepareUserAuthRequestPublicKey() with
# WS_MATCH_KEY_ALGO_E.
# Both lists put their SHA-1 entries last, so the ECDSA entries in the
# matrix still negotiate the same SHA-2 algorithms as before.
sed -i '/#endif \/\* _WIN_USER_SETTINGS_H_ \*\//i\
/* Inserted by windows-cert-store-test CI */\
#define WOLFSSH_WINDOWS_CERT_STORE\
#define WOLFSSH_NO_SHA1_SOFT_DISABLE\
#define WC_SIG_MIN_HASH_TYPE WC_HASH_TYPE_SHA' ${{env.USER_SETTINGS_H_NEW}}
grep -q '^#define WOLFSSH_WINDOWS_CERT_STORE' ${{env.USER_SETTINGS_H_NEW}}
cp ${{env.USER_SETTINGS_H_NEW}} ${{env.USER_SETTINGS_H}}
- 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}}
# Fails the build if the defines never reach wolfsshd.c (same guard as the
# build-sys-ca-certs job).
- name: Guard that the defines reach wolfsshd.c
working-directory: ${{ github.workspace }}\wolfssh
shell: bash
run: |
printf '\n#if !defined(WOLFSSH_WINDOWS_CERT_STORE) || !defined(WOLFSSH_NO_SHA1_SOFT_DISABLE) || !defined(WOLFSSH_SSHD)\n#error "CI: expected defines did not reach wolfsshd.c"\n#endif\n' >> apps/wolfsshd/wolfsshd.c
- 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}}
# Run the unit and API tests here, where WOLFSSH_WINDOWS_CERT_STORE is
# defined; no other workflow defines it, so test_ParseCertStoreSpec and
# test_SetCertManager only ever execute in this job. Run from the wolfssh
# checkout root so ./keys/ paths resolve (as in windows-check.yml). The
# solution build writes to $(SolutionDir)$(Configuration)\$(Platform).
- name: Run api-test and unit-test
working-directory: ${{ github.workspace }}\wolfssh
shell: pwsh
run: |
# Non-zero native exits are handled in-script; without this, pwsh 7.4
# can turn them into terminating errors before the handler runs.
$PSNativeCommandUseErrorActionPreference = $false
$dir = "ide\winvs\${{env.WOLFSSH_BUILD_CONFIGURATION}}\${{env.BUILD_PLATFORM}}"
$dll = Get-ChildItem -Path "${{ github.workspace }}\wolfssl" -Recurse -Filter "wolfssl.dll" -ErrorAction SilentlyContinue | Select-Object -First 1
if ($dll) { Copy-Item $dll.FullName $dir -Force }
foreach ($t in @("api-test", "unit-test")) {
$exe = Join-Path $dir "$t.exe"
if (-not (Test-Path $exe)) { throw "$exe not found" }
& $exe
if ($LASTEXITCODE -ne 0) { throw "$t failed (exit $LASTEXITCODE)" }
}
- 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/**
# The same build against a wolfSSL without OPENSSL_ALL, and so without
# WOLFSSL_FPKI to test CN match instead of UPN.
build-no-fpki:
runs-on: windows-latest
timeout-minutes: 30
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: user_settings.h for sshd, x509 and cert store without OPENSSL_ALL
working-directory: ${{ github.workspace }}
shell: bash
run: |
sed -i 's/#if 0/#if 1/g' ${{env.USER_SETTINGS_H_NEW}}
# Drop OPENSSL_ALL so wolfSSL is built without WOLFSSL_FPKI.
sed -i '/OPENSSL_ALL/d' ${{env.USER_SETTINGS_H_NEW}}
if grep -q 'OPENSSL_ALL' ${{env.USER_SETTINGS_H_NEW}}; then
echo "ERROR: OPENSSL_ALL still present"
exit 1
fi
sed -i '/#endif \/\* _WIN_USER_SETTINGS_H_ \*\//i\
/* Inserted by windows-cert-store-test CI */\
#define WOLFSSH_WINDOWS_CERT_STORE\
#define WOLFSSH_NO_SHA1_SOFT_DISABLE\
#define WC_SIG_MIN_HASH_TYPE WC_HASH_TYPE_SHA' ${{env.USER_SETTINGS_H_NEW}}
grep -q '^#define WOLFSSH_WINDOWS_CERT_STORE' ${{env.USER_SETTINGS_H_NEW}}
cp ${{env.USER_SETTINGS_H_NEW}} ${{env.USER_SETTINGS_H}}
- 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-no-fpki
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}}
# Fails the build unless auth.c really compiles the subject-CN branch:
# WOLFSSL_FPKI must be absent and the cert store defines present.
- name: Guard that the CN binding branch is the one compiled
working-directory: ${{ github.workspace }}\wolfssh
shell: bash
run: |
printf '\n#if defined(WOLFSSL_FPKI) || !defined(WOLFSSH_NO_FPKI)\n#error "CI: expected a non-FPKI wolfSSL for the CN binding build"\n#endif\n' >> apps/wolfsshd/auth.c
printf '\n#if !defined(WOLFSSH_WINDOWS_CERT_STORE) || !defined(WOLFSSH_SSHD)\n#error "CI: expected defines did not reach wolfsshd.c"\n#endif\n' >> apps/wolfsshd/wolfsshd.c
- 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: Run api-test and unit-test
working-directory: ${{ github.workspace }}\wolfssh
shell: pwsh
run: |
$PSNativeCommandUseErrorActionPreference = $false
$dir = "ide\winvs\${{env.WOLFSSH_BUILD_CONFIGURATION}}\${{env.BUILD_PLATFORM}}"
$dll = Get-ChildItem -Path "${{ github.workspace }}\wolfssl" -Recurse -Filter "wolfssl.dll" -ErrorAction SilentlyContinue | Select-Object -First 1
if ($dll) { Copy-Item $dll.FullName $dir -Force }
foreach ($t in @("api-test", "unit-test")) {
$exe = Join-Path $dir "$t.exe"
if (-not (Test-Path $exe)) { throw "$exe not found" }
& $exe
if ($LASTEXITCODE -ne 0) { throw "$t failed (exit $LASTEXITCODE)" }
}
- name: Upload wolfSSH build artifacts
uses: actions/upload-artifact@v4
with:
name: wolfssh-windows-build-no-fpki
if-no-files-found: error
path: |
wolfssh/ide/winvs/**/Release/**
# Compile-only check of the WOLFSSL_SYS_CA_CERTS paths in wolfsshd, which
# the functional matrix never defines and so never builds.
build-sys-ca-certs:
runs-on: windows-latest
timeout-minutes: 30
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: user_settings.h with sshd, x509, cert store, and system CA certs
working-directory: ${{ github.workspace }}
shell: bash
run: |
sed -i 's/#if 0/#if 1/g' ${{env.USER_SETTINGS_H_NEW}}
# Insert before the closing include-guard #endif, not after it.
sed -i '/#endif \/\* _WIN_USER_SETTINGS_H_ \*\//i\
/* Inserted by windows-cert-store-test CI */\
#define WOLFSSH_WINDOWS_CERT_STORE\
#define WOLFSSL_SYS_CA_CERTS' ${{env.USER_SETTINGS_H_NEW}}
grep -q '^#define WOLFSSH_WINDOWS_CERT_STORE' ${{env.USER_SETTINGS_H_NEW}}
cp ${{env.USER_SETTINGS_H_NEW}} ${{env.USER_SETTINGS_H}}
- 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: Restore NuGet packages
working-directory: ${{ github.workspace }}\wolfssh\ide\winvs
run: nuget restore ${{env.SOLUTION_FILE_PATH}}
# Fails the build if the defines never reach wolfsshd.c, which otherwise
# compiles its #else branch and silently degrades to a duplicate of build.
- name: Guard that the defines reach wolfsshd.c
working-directory: ${{ github.workspace }}\wolfssh
shell: bash
run: |
printf '\n#if !defined(WOLFSSL_SYS_CA_CERTS) || !defined(WOLFSSH_WINDOWS_CERT_STORE) || !defined(WOLFSSH_SSHD)\n#error "CI: expected defines did not reach wolfsshd.c"\n#endif\n' >> apps/wolfsshd/wolfsshd.c
- name: Build wolfssh (compile check)
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}}
# Autotools coverage for --enable-windows-cert-store: the mingw link
# libraries and both error paths. Configure only, so no cross-built wolfSSL
# is needed; the wolfssl link test is satisfied from the autoconf cache.
configure-windows-cert-store:
runs-on: ubuntu-latest
timeout-minutes: 20
env:
WOLFSSL_CACHE: ac_cv_lib_wolfssl_wolfCrypt_Init=yes
steps:
- uses: actions/checkout@v4
- name: Install mingw toolchain and autotools
run: |
sudo apt-get update
sudo apt-get install -y gcc-mingw-w64-x86-64 autoconf automake libtool
- name: Generate configure
run: ./autogen.sh
- name: mingw host links crypt32 and ncrypt
run: |
./configure --host=x86_64-w64-mingw32 --enable-certs \
--enable-windows-cert-store $WOLFSSL_CACHE
grep -q -- '-lcrypt32' Makefile
grep -q -- '-lncrypt' Makefile
# Compile the cert-store sources with the mingw cross compiler so a
# GCC/mingw-only break in the new code (header ordering, format checks,
# older-SDK differences from MSVC) is caught, not just configure-tested.
# A cross-built wolfSSL library is not needed to compile these objects:
# only the wolfSSL headers are consumed, generated by configuring a
# wolfSSL source checkout for the same mingw host.
- name: mingw compile check of the cert-store sources
run: |
git clone --depth 1 https://github.com/wolfssl/wolfssl.git wolfssl-src
(cd wolfssl-src && ./autogen.sh > /dev/null && \
./configure --host=x86_64-w64-mingw32 --enable-ssh > /dev/null)
x86_64-w64-mingw32-gcc -fsyntax-only -Wall \
-DWOLFSSH_CERTS -DWOLFSSH_WINDOWS_CERT_STORE -DWOLFSSH_SSHD \
-DHAVE_CONFIG_H -I. -Iwolfssl-src \
src/certman.c src/ssh.c src/internal.c \
apps/wolfsshd/wolfsshd.c apps/wolfsshd/configuration.c \
apps/wolfsshd/auth.c
- name: Rejects a non-Windows host and a missing --enable-certs
# Each assertion exits explicitly: bash errexit exempts a command
# inverted with '!', and the step status comes from the last command.
# The output grep pins each failure to the intended configure.ac error,
# so an unrelated earlier configure failure cannot keep the check green.
run: |
if ./configure --enable-certs --enable-windows-cert-store \
$WOLFSSL_CACHE > conf-host.log 2>&1; then
echo 'ERROR: configure should have failed on a non-Windows host'
exit 1
fi
if ! grep -q 'only supported on _WIN32 Windows hosts' conf-host.log; then
cat conf-host.log
echo 'ERROR: configure failed, but not with the non-Windows host error'
exit 1
fi
if ./configure --host=x86_64-w64-mingw32 \
--enable-windows-cert-store $WOLFSSL_CACHE > conf-nocerts.log 2>&1; then
echo 'ERROR: configure should have failed without --enable-certs'
exit 1
fi
if ! grep -q 'requires X.509 cert support' conf-nocerts.log; then
cat conf-nocerts.log
echo 'ERROR: configure failed, but not with the missing-certs error'
exit 1
fi
test:
needs: [build, build-no-fpki]
runs-on: windows-latest
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
# build_flavor selects the artifact pair: unset for the FPKI build,
# "-no-fpki" for the build without OPENSSL_ALL.
include:
# user_ca_source: store replaces the file-based TrustedUserCAKeys
# with wolfSSH_TrustedUserCAStore, so the store is the only trust
# anchor for the client certificate.
- server_key_source: file
client_key_source: x509
key_algorithm: rsa
user_ca_source: store
test_name: "Server-File-Client-X509-UserCAStore"
- server_key_source: store
client_key_source: x509
key_algorithm: rsa
test_name: "Server-Store-Client-X509"
# key_algorithm is the server host key; client_key_algorithm is the
# testuser client certificate key. Both are stated explicitly on the
# store-client entries so neither depends on which key renewcerts.sh
# happens to copy.
- server_key_source: file
client_key_source: store
key_algorithm: rsa
client_key_algorithm: ecdsa
test_name: "Server-File-Client-Store"
- server_key_source: store
client_key_source: store
key_algorithm: rsa
client_key_algorithm: ecdsa
test_name: "Server-Store-Client-Store"
- server_key_source: store
client_key_source: x509
key_algorithm: ecdsa
test_name: "Server-Store-Client-X509-ECDSA"
# RSA client certificate, covering the x509v3-ssh-rsa user-auth and
# client-side RSA cert store signing paths that the ECDSA entries
# above cannot reach.
- server_key_source: file
client_key_source: store
key_algorithm: rsa
client_key_algorithm: rsa
test_name: "Server-File-Client-Store-RSA"
# The first entry again on the build without WOLFSSL_FPKI, so the
# identity binding step below exercises the subject-CN branch of
# auth.c: matching CN, case-differing account name, mismatched CN.
- server_key_source: file
client_key_source: x509
key_algorithm: rsa
user_ca_source: store
build_flavor: -no-fpki
test_name: "Server-File-Client-X509-UserCAStore-NoFPKI"
steps:
- uses: actions/checkout@v4
with:
path: wolfssh
- name: Download wolfSSH build artifacts
uses: actions/download-artifact@v4
with:
name: wolfssh-windows-build${{ matrix.build_flavor }}
path: .
- name: Download wolfSSL build artifacts
uses: actions/download-artifact@v4
with:
name: wolfssl-windows-build${{ matrix.build_flavor }}
path: .
- name: Create testuser client certificate - ${{ 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: |
# Create an X509 certificate for testuser, signed by the test CA,
# using renewcerts.sh (like sshd_x509_test.sh does). Used directly
# for x509 clients and imported into the store for store clients.
cd keys
bash renewcerts.sh testuser
# renewcerts.sh has no 'set -e' and exits 0 even when an openssl call
# failed, so verify the CA and server cert it silently regenerates
# and that the rest of the matrix depends on.
openssl x509 -in ca-cert-ecc.pem -noout
openssl verify -CAfile ca-cert-ecc.pem server-cert.pem
# renewcerts.sh copies fred's key, which is EC prime256v1, so testuser
# comes out ECDSA. Re-issue it explicitly for whichever algorithm the
# matrix entry asks for, rather than inheriting whatever fred's key
# happens to be.
ALG="${{ matrix.client_key_algorithm }}"
if [ -n "$ALG" ]; then
touch index.txt
sed 's/fred/testuser/g' renewcerts.cnf > renewcerts-testuser.cnf
if [ "$ALG" = "rsa" ]; then
openssl genrsa -out testuser-key.pem 2048
else
openssl ecparam -name prime256v1 -genkey -noout \
-out testuser-key.pem
fi
openssl req -subj "/C=US/ST=WA/L=Seattle/O=wolfSSL Inc/OU=Development/CN=testuser/emailAddress=testuser@example.com" \
-key testuser-key.pem -out testuser-cert.csr \
-config renewcerts-testuser.cnf -new -nodes
openssl x509 -req -in testuser-cert.csr -days 3650 \
-extfile renewcerts-testuser.cnf -extensions v3_testuser \
-CA ca-cert-ecc.pem -CAkey ca-key-ecc.pem -out testuser-cert.pem \
-set_serial 7
openssl x509 -in testuser-cert.pem -outform DER -out testuser-cert.der
if [ "$ALG" = "rsa" ]; then
openssl rsa -in testuser-key.pem -outform DER -out testuser-key.der
else
openssl ec -in testuser-key.pem -outform DER -out testuser-key.der
fi
rm -f renewcerts-testuser.cnf testuser-cert.csr index.*
fi
cd ..
if [[ ! -f "keys/testuser-cert.der" || ! -f "keys/testuser-key.der" ]]; then
echo "ERROR: renewcerts.sh did not create testuser-cert.der/testuser-key.der"
ls -la keys/
exit 1
fi
# Assert the key really is the algorithm this entry asked for, so a
# change to renewcerts.sh cannot silently turn an entry into a
# duplicate of another one. Unset means whatever renewcerts.sh gives,
# which is fred's EC key.
EXPECT="${{ matrix.client_key_algorithm }}"
[ -n "$EXPECT" ] || EXPECT=ecdsa
if openssl rsa -inform DER -in keys/testuser-key.der -noout 2>/dev/null
then
ACTUAL=rsa
elif openssl ec -inform DER -in keys/testuser-key.der -noout 2>/dev/null
then
ACTUAL=ecdsa
else
echo "ERROR: testuser-key.der is neither RSA nor EC"
exit 1
fi
if [ "$ACTUAL" != "$EXPECT" ]; then
echo "ERROR: testuser client key is $ACTUAL, expected $EXPECT"
exit 1
fi
echo "testuser client key algorithm: $ACTUAL"
echo "CLIENT_CERT_FILE=keys/testuser-cert.der" >> $GITHUB_ENV
echo "CLIENT_KEY_FILE=keys/testuser-key.der" >> $GITHUB_ENV
- name: Set up cert store certificates
working-directory: ${{ github.workspace }}\wolfssh
shell: pwsh
run: |
# Non-zero native exits (the openssl RSA-then-EC fallback) are
# handled in-script.
$PSNativeCommandUseErrorActionPreference = $false
# Server host key: self-signed cert in LocalMachine\My so the
# wolfsshd service (LocalSystem) can access it.
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: $($serverCert.Subject) ($($serverCert.Thumbprint))"
# Grant LocalSystem access to the private key file. Required for
# the wolfsshd service running as LocalSystem; without this,
# CryptAcquireCertificatePrivateKey fails.
if ("${{ matrix.key_algorithm }}" -eq "ecdsa") {
$privKey = [System.Security.Cryptography.X509Certificates.ECDsaCertificateExtensions]::GetECDsaPrivateKey($serverCert)
} else {
$privKey = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($serverCert)
}
$keyName = $privKey.Key.UniqueName
$keyFile = @(
"$env:ProgramData\Microsoft\Crypto\Keys\$keyName",
"$env:ProgramData\Microsoft\Crypto\RSA\MachineKeys\$keyName",
"$env:ProgramData\Microsoft\Crypto\SystemKeys\$keyName"
) | Where-Object { Test-Path $_ } | Select-Object -First 1
if (-not $keyFile) {
Write-Host "ERROR: Private key file not found for $keyName"
exit 1
}
$acl = Get-Acl $keyFile
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule `
"NT AUTHORITY\SYSTEM", "FullControl", "Allow"
$acl.SetAccessRule($rule)
Set-Acl $keyFile $acl
Write-Host "Granted SYSTEM FullControl on private key: $keyFile"
# Export the CN (without "CN=") for wolfSSH_HostKeyStoreSubject
$subject = $serverCert.Subject
if ($subject -match "^CN=(.+)$") { $subject = $matches[1] }
Add-Content -Path $env:GITHUB_ENV -Value "SERVER_CERT_SUBJECT=$subject"
# Export the (self-signed) server cert as DER so the client can use
# it as the trust anchor when negotiating an x509v3-* host key.
Export-Certificate -Cert $serverCert -FilePath "server-store-cert.der" | Out-Null
}
# Client user key: import the CA-signed testuser cert+key into
# CurrentUser\My (via PFX; openssl converts the DER files).
if ("${{ matrix.client_key_source }}" -eq "store") {
$userCertPath = (Resolve-Path $env:CLIENT_CERT_FILE).Path
$userKeyPath = (Resolve-Path $env:CLIENT_KEY_FILE).Path
$userCertPem = Join-Path $env:TEMP "testuser-cert.pem"
$userKeyPem = Join-Path $env:TEMP "testuser-key.pem"
$pfxPath = Join-Path $env:TEMP "testuser-client.pfx"
$pfxPassword = "TempP@ss123"
& openssl x509 -inform DER -in $userCertPath -out $userCertPem
if ($LASTEXITCODE -ne 0) { Write-Host "ERROR: cert DER to PEM failed"; exit 1 }
& openssl rsa -inform DER -in $userKeyPath -out $userKeyPem 2>$null
if ($LASTEXITCODE -ne 0) {
& openssl ec -inform DER -in $userKeyPath -out $userKeyPem
if ($LASTEXITCODE -ne 0) { Write-Host "ERROR: key DER to PEM failed (tried RSA and ECC)"; exit 1 }
}
& openssl pkcs12 -export -out $pfxPath -inkey $userKeyPem -in $userCertPem -password "pass:$pfxPassword" -nodes
if ($LASTEXITCODE -ne 0) { Write-Host "ERROR: PFX creation failed"; exit 1 }
Import-PfxCertificate -FilePath $pfxPath -CertStoreLocation "Cert:\CurrentUser\My" `
-Password (ConvertTo-SecureString -String $pfxPassword -Force -AsPlainText) | Out-Null
Remove-Item -Path $pfxPath, $userCertPem, $userKeyPem -ErrorAction SilentlyContinue
$importedCert = Get-ChildItem -Path "Cert:\CurrentUser\My" |
Where-Object { $_.Subject -match "testuser" } | Select-Object -First 1
if (-not $importedCert) {
Write-Host "ERROR: imported testuser cert not found in CurrentUser\My"
exit 1
}
Write-Host "Client cert imported: $($importedCert.Subject) ($($importedCert.Thumbprint))"
# Export the CN for the client cert store lookup. The full X.500
# DN contains commas which break command-line argument parsing.
$cn = $importedCert.Subject
if ($cn -match 'CN=([^,]+)') { $cn = $matches[1].Trim() }
Add-Content -Path $env:GITHUB_ENV -Value "CLIENT_CERT_SUBJECT=$cn"
}
- name: Import test CA into a Windows store
if: matrix.user_ca_source == 'store'
working-directory: ${{ github.workspace }}\wolfssh
shell: pwsh
run: |
# Non-zero certutil exits are handled in-script.
$PSNativeCommandUseErrorActionPreference = $false
# LocalMachine so the wolfsshd service (LocalSystem) can read it.
# certutil creates the store if it does not already exist.
$caDer = (Resolve-Path "keys\ca-cert-ecc.der").Path
certutil -addstore -f wolfSSHTestCA $caDer
if ($LASTEXITCODE -ne 0) {
Write-Host "ERROR: certutil failed to add the CA to wolfSSHTestCA"
exit 1
}
$caInStore = Get-ChildItem -Path "Cert:\LocalMachine\wolfSSHTestCA" -ErrorAction SilentlyContinue
if (-not $caInStore) {
Write-Host "ERROR: no certificate present in LocalMachine\wolfSSHTestCA"
exit 1
}
Write-Host "CA imported: $($caInStore[0].Subject)"
# An existing but empty store for the negative startup test. Adding
# then removing the CA leaves the store itself in place, so the
# failure is "no usable CA" and not "store not found".
certutil -addstore -f wolfSSHEmptyCA $caDer
if ($LASTEXITCODE -ne 0) { Write-Host "ERROR: certutil -addstore wolfSSHEmptyCA"; exit 1 }
Get-ChildItem -Path "Cert:\LocalMachine\wolfSSHEmptyCA" | Remove-Item -Force
if (Get-ChildItem -Path "Cert:\LocalMachine\wolfSSHEmptyCA" -ErrorAction SilentlyContinue) {
Write-Host "ERROR: wolfSSHEmptyCA is not empty"
exit 1
}
# A store holding only an end-entity certificate, for the negative
# test of the CertIsCA basicConstraints filter: with no CA:TRUE cert
# present the daemon must refuse to start rather than promote the
# leaf to a login authority.
$leafDer = (Resolve-Path "keys\testuser-cert.der").Path
certutil -addstore -f wolfSSHLeafOnlyCA $leafDer
if ($LASTEXITCODE -ne 0) { Write-Host "ERROR: certutil -addstore wolfSSHLeafOnlyCA"; exit 1 }
- name: Create Windows user testuser
shell: pwsh
run: |
# net user's "already exists" recovery below inspects $LASTEXITCODE;
# without this, pwsh 7.4 throws at the call site first.
$PSNativeCommandUseErrorActionPreference = $false
$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'
New-Item -ItemType Directory -Path $homeDir -Force | Out-Null
New-Item -ItemType Directory -Path $sshDir -Force | Out-Null
# 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") {
net user testuser /homedir:$homeDir 2>$null
} else {
Write-Host "net user failed: $o"
exit 1
}
}
# X509 auth verifies the client cert against the CA; authorized_keys
# is not used but the file should exist.
"" | Out-File -FilePath $authKeysFile -Encoding ASCII -NoNewline
icacls $authKeysFile /grant "testuser:R" /q
if ($LASTEXITCODE -ne 0) {
Write-Host "ERROR: icacls failed on $authKeysFile"
exit 1
}
# wolfsshd serves SFTP from the home directory while impersonating
# testuser; the SFTP tests assert this name appears in the listing.
"marker" | Out-File -FilePath "$homeDir\wolfssh_sftp_marker.txt" -Encoding ASCII
icacls $homeDir /grant "testuser:(OI)(CI)RX" /T /q
if ($LASTEXITCODE -ne 0) {
Write-Host "ERROR: icacls failed on $homeDir"
exit 1
}
# 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
- name: Create wolfSSHd config file
working-directory: ${{ github.workspace }}\wolfssh
shell: pwsh
run: |
$configContent = @"
Port ${{env.TEST_PORT}}
PasswordAuthentication yes
PermitRootLogin yes
"@
# Server verifies client X509 certs against the test CA. Either from a
# PEM file (as per apps/wolfsshd/test/create_sshd_config.sh) or from
# the Windows store the CA was imported into, never both, so a
# successful client auth pins down which one supplied the anchor.
if ("${{ matrix.user_ca_source }}" -eq "store") {
$configContent += @"
wolfSSH_TrustedUserCAStore yes
wolfSSH_WinUserStores CERT_STORE_PROV_SYSTEM
wolfSSH_WinUserPvPara wolfSSHTestCA
wolfSSH_WinUserDwFlags LOCAL_MACHINE
"@
} else {
$caCertPath = (Resolve-Path "keys\ca-cert-ecc.pem").Path
$configContent += @"
TrustedUserCAKeys $caCertPath
"@
}
if ("${{ matrix.server_key_source }}" -eq "store") {
# The certificate is part of the store entry. HostKey and
# HostCertificate alongside wolfSSH_HostKeyStore are rejected at startup.
$configContent += @"
wolfSSH_HostKeyStore My
wolfSSH_HostKeyStoreSubject $env:SERVER_CERT_SUBJECT
wolfSSH_HostKeyStoreFlags LOCAL_MACHINE
"@
} else {
$keyPath = (Resolve-Path "keys\server-key.pem").Path
$certPath = (Resolve-Path "keys\server-cert.pem").Path
$configContent += @"
HostKey $keyPath
HostCertificate $certPath
"@
}
$configContent | Out-File -FilePath sshd_config_test -Encoding ASCII
Write-Host "=== wolfSSHd Config ==="
Get-Content sshd_config_test
- name: Find wolfSSH executables
working-directory: ${{ github.workspace }}\wolfssh
shell: pwsh
run: |
$searchRoot = "${{ github.workspace }}"
$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 (-not $sshdExe) {
Write-Host "ERROR: wolfsshd.exe not found"
Get-ChildItem -Path $searchRoot -Recurse -Filter "*.exe" -ErrorAction SilentlyContinue | Select-Object FullName
exit 1
}
Write-Host "wolfsshd.exe: $($sshdExe.FullName)"
Add-Content -Path $env:GITHUB_ENV -Value "SSHD_PATH=$($sshdExe.FullName)"
# SFTP client (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 (-not $sftpExe) {
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
}
Write-Host "SFTP client: $($sftpExe.FullName)"
Add-Content -Path $env:GITHUB_ENV -Value "SFTP_PATH=$($sftpExe.FullName)"
# echoserver (used for the cert store host key test)
$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 "echoserver.exe: $($echoserverExe.FullName)"
Add-Content -Path $env:GITHUB_ENV -Value "ECHOSERVER_PATH=$($echoserverExe.FullName)"
} elseif ("${{ matrix.server_key_source }}" -eq "store") {
Write-Host "ERROR: echoserver.exe not found (required for cert store server test)"
exit 1
}
- name: Copy wolfSSL DLL to executable directory (if dynamic build)
working-directory: ${{ github.workspace }}
shell: pwsh
run: |
# This job has no wolfssl checkout; the artifact unpacks at the
# workspace root, so search there rather than under wolfssl\.
$sshdDir = Split-Path -Parent $env:SSHD_PATH
$searchRoot = "${{ github.workspace }}"
$wolfsslDll = Get-ChildItem -Path $searchRoot -Recurse -Filter "wolfssl.dll" -ErrorAction SilentlyContinue |
Select-Object -First 1
if ($wolfsslDll) {
Copy-Item -Path $wolfsslDll.FullName -Destination (Join-Path $sshdDir "wolfssl.dll") -Force
Write-Host "Copied $($wolfsslDll.FullName) to $sshdDir"
exit 0
}
$wolfsslLib = Get-ChildItem -Path $searchRoot -Recurse -Filter "wolfssl.lib" -ErrorAction SilentlyContinue |
Select-Object -First 1
if ($wolfsslLib) {
Write-Host "Static build ($($wolfsslLib.FullName)); wolfssl.dll not required"
} else {
Write-Host "WARNING: neither wolfssl.dll nor wolfssl.lib found under $searchRoot"
}
- name: Grant service (LocalSystem) access to config, keys, and executable
working-directory: ${{ github.workspace }}\wolfssh
shell: pwsh
run: |
# icacls failures are handled in-script via $LASTEXITCODE.
$PSNativeCommandUseErrorActionPreference = $false
# wolfsshd runs as LocalSystem; it must be able to read the config
# and key files and run the exe (and load wolfssl.dll if dynamic).
# /T = apply to existing files and subdirs; (OI)(CI) = inherit to new objects
$wolfsshRoot = (Get-Location).Path
icacls $wolfsshRoot /grant "NT AUTHORITY\SYSTEM:(OI)(CI)RX" /T /q
if ($LASTEXITCODE -ne 0) {
Write-Host "ERROR: icacls failed on $wolfsshRoot"
exit 1
}
$sshdDir = (Resolve-Path (Split-Path -Parent $env:SSHD_PATH)).Path
icacls $sshdDir /grant "NT AUTHORITY\SYSTEM:(OI)(CI)RX" /T /q
if ($LASTEXITCODE -ne 0) {
Write-Host "ERROR: icacls failed on $sshdDir"
exit 1
}
- name: Start echoserver with cert store host key
if: matrix.server_key_source == 'store'
working-directory: ${{ github.workspace }}\wolfssh
shell: pwsh
run: |
# Exercise the cert store host key (-W Store:Subject:Location) in the
# echoserver before the wolfsshd service test. Start it detached (via
# cmd start /B) so it survives after this step ends.
$echoserverPath = $env:ECHOSERVER_PATH
$exeDir = Split-Path -Parent $echoserverPath
$port = ${{env.TEST_PORT}}
# Reuse the exported CN rather than duplicating the constant the
# wolfsshd config path uses.
$spec = "My:$($env:SERVER_CERT_SUBJECT):LOCAL_MACHINE"
$wolfsshRoot = "${{ github.workspace }}\wolfssh"
# -a <CA cert>: verify client X.509 certs
# -K testuser:<client cert>: register testuser with the auth callback
$caCertPem = Join-Path $wolfsshRoot "keys\ca-cert-ecc.pem"
$clientCert = (Resolve-Path (Join-Path $wolfsshRoot $env:CLIENT_CERT_FILE)).Path
$echoArgs = @("-W", $spec, "-p", $port, "-a", $caCertPem, "-K", "testuser:$clientCert")
# echoserver serves SFTP from its working directory; the SFTP tests
# assert this name appears in the remote listing.
"marker" | Out-File -FilePath (Join-Path $exeDir "wolfssh_sftp_marker.txt") -Encoding ASCII
# Quote every element so a path containing a space cannot split an
# argument when the array is flattened for cmd.exe.
$argStr = ($echoArgs | ForEach-Object { '"{0}"' -f $_ }) -join ' '
$echoLogFile = Join-Path $wolfsshRoot "echoserver_debug.log"
Add-Content -Path $env:GITHUB_ENV -Value "ECHOSERVER_LOG=$echoLogFile"
Write-Host "Command: $echoserverPath $argStr"
# Launch via a batch file: passing the quoted command line as one
# Start-Process argument nests quotes, which cmd mangles.
$batFile = Join-Path $wolfsshRoot "start_echoserver.bat"
Set-Content -Path $batFile -Encoding ASCII -Value @(
"@echo off",
"`"$echoserverPath`" $argStr > `"$echoLogFile`" 2>&1"
)
Start-Process -FilePath "cmd.exe" `
-ArgumentList "/c", "start", "/B", "cmd", "/c", $batFile `
-WorkingDirectory $exeDir -NoNewWindow -Wait:$false
# The detached launch goes through two intermediate cmd.exe
# processes, so poll for the echoserver process instead of a fixed
# sleep, and only treat its absence as a crash once it has been seen
# running.
$seenRunning = $false
for ($i = 0; $i -lt 10 -and -not $seenRunning; $i++) {
$proc = Get-Process -Name "echoserver" -ErrorAction SilentlyContinue | Select-Object -First 1
if ($proc) {
$seenRunning = $true
Add-Content -Path $env:GITHUB_ENV -Value "ECHOSERVER_PID=$($proc.Id)"
Write-Host "echoserver started with PID $($proc.Id)"
} else {
Start-Sleep -Seconds 1
}
}
# Wait for the port to be listening
$timeout = 15
$elapsed = 0
$ready = $false
while ($elapsed -lt $timeout -and -not $ready) {
Start-Sleep -Seconds 1
$elapsed++
try {
$conn = New-Object System.Net.Sockets.TcpClient("127.0.0.1", $port)
if ($conn.Connected) { $conn.Close(); $ready = $true; continue }
} catch {}
if ($seenRunning -and
-not (Get-Process -Name "echoserver" -ErrorAction SilentlyContinue)) {
Write-Host "ERROR: echoserver exited before port was ready"
if (Test-Path $echoLogFile) { Get-Content $echoLogFile }
exit 1
}
}
if (-not $ready) {
Write-Host "ERROR: Port $port not listening after ${timeout}s"
if (Test-Path $echoLogFile) { Get-Content $echoLogFile }
exit 1
}
Write-Host "echoserver is listening on port $port"
- name: Test SFTP against echoserver (cert store host key)
if: matrix.server_key_source == 'store'
working-directory: ${{ github.workspace }}\wolfssh
shell: pwsh
timeout-minutes: 3
run: |
# The plain host key algorithm wins negotiation here, so this covers
# the plain key slot and user auth; the x509v3 slot is covered by the
# next step.
$testPort = ${{env.TEST_PORT}}
$sftpPath = $env:SFTP_PATH
@"
pwd
ls
quit
"@ | Out-File -FilePath sftp_echo_commands.txt -Encoding ASCII
$sftpArgs = @("-u", "testuser", "-h", "localhost", "-p", "$testPort")
$caCertDer = (Resolve-Path "keys\ca-cert-ecc.der").Path
if ("${{ matrix.client_key_source }}" -eq "store") {
$sftpArgs += "-W", "My:$($env:CLIENT_CERT_SUBJECT):CURRENT_USER"
} else {
$sftpArgs += "-J", (Resolve-Path $env:CLIENT_CERT_FILE).Path
$sftpArgs += "-i", (Resolve-Path $env:CLIENT_KEY_FILE).Path
}
# -A: CA cert for host verification; -X: ignore peer IP vs cert checks
$sftpArgs += "-A", $caCertDer, "-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 ==="
if (Test-Path sftp_echo_output.txt) { Get-Content sftp_echo_output.txt }
Write-Host "=== SFTP Error ==="
if (Test-Path sftp_echo_error.txt) { Get-Content sftp_echo_error.txt }
if ($process.ExitCode -ne 0) {
$echoLog = $env:ECHOSERVER_LOG
if (-not [string]::IsNullOrEmpty($echoLog) -and (Test-Path $echoLog)) {
Write-Host "=== Echoserver Log ==="
Get-Content $echoLog
}
Write-Host "ERROR: SFTP against echoserver failed"
exit 1
}
if ((Get-Content sftp_echo_output.txt -Raw) -notmatch "wolfssh_sftp_marker.txt") {
Write-Host "ERROR: remote listing did not contain the marker file"
exit 1
}
Write-Host "SFTP against echoserver succeeded"
- name: Test SFTP against echoserver with x509v3 host key
if: matrix.server_key_source == 'store'
working-directory: ${{ github.workspace }}\wolfssh
shell: pwsh
timeout-minutes: 3
run: |
# Force the x509v3 host key algorithm so the cert store certificate
# itself is sent as K_S and verified by the client, exercising the
# X.509 host-key slot instead of the plain-key slot. The server cert
# is self-signed, so it is its own trust anchor (-A), which also means
# a fallback to a file-based host key could not pass this step.
$testPort = ${{env.TEST_PORT}}
$sftpPath = $env:SFTP_PATH
@"
pwd
ls
quit
"@ | Out-File -FilePath sftp_x509_commands.txt -Encoding ASCII
$sftpArgs = @("-u", "testuser", "-h", "localhost", "-p", "$testPort")
if ("${{ matrix.client_key_source }}" -eq "store") {
$sftpArgs += "-W", "My:$($env:CLIENT_CERT_SUBJECT):CURRENT_USER"
} else {
$sftpArgs += "-J", (Resolve-Path $env:CLIENT_CERT_FILE).Path
$sftpArgs += "-i", (Resolve-Path $env:CLIENT_KEY_FILE).Path
}
$sftpArgs += "-A", (Resolve-Path "server-store-cert.der").Path, "-X"
if ("${{ matrix.key_algorithm }}" -eq "ecdsa") {
$sftpArgs += "-k", "x509v3-ecdsa-sha2-nistp256"
} else {
$sftpArgs += "-k", "x509v3-ssh-rsa"
}
Write-Host "Running: $sftpPath $($sftpArgs -join ' ')"
$process = Start-Process -FilePath $sftpPath `
-ArgumentList $sftpArgs `
-RedirectStandardInput "sftp_x509_commands.txt" `
-RedirectStandardOutput "sftp_x509_output.txt" `
-RedirectStandardError "sftp_x509_error.txt" `
-Wait -NoNewWindow -PassThru
Write-Host "SFTP (x509v3 host key) exit code: $($process.ExitCode)"
Write-Host "=== SFTP Output ==="
if (Test-Path sftp_x509_output.txt) { Get-Content sftp_x509_output.txt }
Write-Host "=== SFTP Error ==="
if (Test-Path sftp_x509_error.txt) { Get-Content sftp_x509_error.txt }
if ($process.ExitCode -ne 0) {
$echoLog = $env:ECHOSERVER_LOG
if (-not [string]::IsNullOrEmpty($echoLog) -and (Test-Path $echoLog)) {
Write-Host "=== Echoserver Log ==="
Get-Content $echoLog
}
Write-Host "ERROR: SFTP with x509v3 host key failed"
exit 1
}
if ((Get-Content sftp_x509_output.txt -Raw) -notmatch "wolfssh_sftp_marker.txt") {
Write-Host "ERROR: remote listing did not contain the marker file"
exit 1
}
Write-Host "SFTP with x509v3 host key 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)) {
Stop-Process -Id $echoserverPid -Force -ErrorAction SilentlyContinue
}
# Also kill by name in case PID tracking missed it
Get-Process -Name "echoserver" -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
# wolfSSH skips SO_REUSEADDR on Windows (wolfssh/test.h), so wolfsshd
# hard-fails if it binds before the port is released. Poll until the
# listener is gone rather than sleeping a fixed interval.
$port = ${{env.TEST_PORT}}
$timeout = 30
$elapsed = 0
# -ErrorAction Stop in a try/catch so a cmdlet failure (module or
# WMI hiccup) fails the step loudly instead of reading as "released".
while ($true) {
try {
$listening = Get-NetTCPConnection -LocalPort $port `
-State Listen -ErrorAction Stop
} catch [Microsoft.PowerShell.Cmdletization.Cim.CimJobException] {
# no matching connection: the port is released
$listening = $null
}
if (-not $listening) { break }
if ($elapsed -ge $timeout) {
Write-Host "ERROR: port $port still listening ${timeout}s after stopping echoserver"
exit 1
}
Start-Sleep -Seconds 1
$elapsed++
}
Write-Host "Port $port released"
# Clear the env var so cleanup step doesn't try again
Add-Content -Path $env:GITHUB_ENV -Value "ECHOSERVER_PID="
- name: wolfSSHd refuses to start with an empty user CA store
if: matrix.user_ca_source == 'store'
working-directory: ${{ github.workspace }}\wolfssh
shell: pwsh
timeout-minutes: 3
run: |
# Start wolfsshd for real (no -t test mode) so refusal is observable
# as the process exiting without a listener, not just as a log line.
# Windows main() always returns 0, so the exit code is not asserted.
(Get-Content sshd_config_test) -replace 'wolfSSHTestCA', 'wolfSSHEmptyCA' |
Out-File -FilePath sshd_config_empty_ca -Encoding ASCII
$configPathFull = (Resolve-Path "sshd_config_empty_ca").Path
$port = ${{env.TEST_PORT}}
$proc = Start-Process -FilePath (Resolve-Path $env:SSHD_PATH).Path `
-ArgumentList @("-D", "-d", "-f", $configPathFull, "-p", $port) `
-RedirectStandardOutput "sshd_empty_ca_out.txt" `
-RedirectStandardError "sshd_empty_ca_err.txt" `
-NoNewWindow -PassThru
# Wait for the refusal instead of sleeping a fixed interval; a slow
# cold start on a loaded runner must not read as "still running".
$exited = $proc.WaitForExit(30000)
$log = ""
foreach ($f in @("sshd_empty_ca_out.txt", "sshd_empty_ca_err.txt")) {
if (Test-Path $f) { $log += (Get-Content $f -Raw) }
}
Write-Host "=== wolfsshd output ==="
Write-Host $log
$failed = $false
# Windows may prune the registry key once the last cert is removed, in
# which case the store fails to open instead of enumerating empty.
# Either way startup must not succeed.
if ($log -notmatch "No usable CA certificates found in store" -and
$log -notmatch "Unable to open user CA cert store") {
Write-Host "ERROR: wolfsshd did not reject the empty user CA store"
$failed = $true
}
if (-not $exited) {
Write-Host "ERROR: wolfsshd is still running with an empty user CA store"
$failed = $true
}
# -ErrorAction Stop in a try/catch so a cmdlet failure cannot be read
# as "not listening" and silently pass the assertion.
try {
$listening = Get-NetTCPConnection -LocalPort $port -State Listen `
-ErrorAction Stop
} catch [Microsoft.PowerShell.Cmdletization.Cim.CimJobException] {
$listening = $null
}
if ($listening) {
Write-Host "ERROR: wolfsshd is listening on port $port with an empty user CA store"
$failed = $true
}
if (-not $exited) {
Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue
}
if ($failed) { exit 1 }
Write-Host "wolfsshd refused to start with the empty user CA store"
# Every fail-closed startup validation the cert-store options carry, each
# asserted on its specific error message, the process exiting, and no
# listener appearing. Runs once (the user_ca_source: store entry).
- name: wolfSSHd rejects invalid cert-store configurations
if: matrix.user_ca_source == 'store'
working-directory: ${{ github.workspace }}\wolfssh
shell: pwsh
timeout-minutes: 10
run: |
$port = ${{env.TEST_PORT}}
$sshdPath = (Resolve-Path $env:SSHD_PATH).Path
$hostKey = (Resolve-Path "keys\server-key.pem").Path
$hostCert = (Resolve-Path "keys\server-cert.pem").Path
$caPem = (Resolve-Path "keys\ca-cert-ecc.pem").Path
$script:anyFailed = $false
function Test-SshdRejects {
param($Desc, $ConfigLines, $Expect, $ExtraArgs = @())
Write-Host "--- $Desc"
$cfg = "sshd_config_negative"
$ConfigLines | Out-File -FilePath $cfg -Encoding ASCII
$cfgFull = (Resolve-Path $cfg).Path
$sshdArgs = @("-D", "-d", "-f", $cfgFull, "-p", ${{env.TEST_PORT}})
$sshdArgs += $ExtraArgs
$proc = Start-Process -FilePath $sshdPath -ArgumentList $sshdArgs `
-RedirectStandardOutput "sshd_neg_out.txt" `
-RedirectStandardError "sshd_neg_err.txt" `
-NoNewWindow -PassThru
$exited = $proc.WaitForExit(30000)
$log = ""
foreach ($f in @("sshd_neg_out.txt", "sshd_neg_err.txt")) {
if (Test-Path $f) { $log += (Get-Content $f -Raw) }
}
$ok = $true
if ($log -notmatch $Expect) {
Write-Host "ERROR: expected '$Expect' in output"
Write-Host $log
$ok = $false
}
if (-not $exited) {
Write-Host "ERROR: wolfsshd is still running"
Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue
$ok = $false
}
try {
$listening = Get-NetTCPConnection -LocalPort ${{env.TEST_PORT}} `
-State Listen -ErrorAction Stop
} catch [Microsoft.PowerShell.Cmdletization.Cim.CimJobException] {
$listening = $null
}
if ($listening) {
Write-Host "ERROR: wolfsshd is listening"
$ok = $false
}
if ($ok) { Write-Host "PASSED" } else { $script:anyFailed = $true }
}
$base = @("Port $port", "HostKey $hostKey", "TrustedUserCAKeys $caPem")
Test-SshdRejects "user CA store with no store name" `
@("Port $port", "HostKey $hostKey",
"wolfSSH_TrustedUserCAStore yes",
"wolfSSH_WinUserDwFlags LOCAL_MACHINE") `
"no store name"
Test-SshdRejects "user CA store with no store location" `
@("Port $port", "HostKey $hostKey",
"wolfSSH_TrustedUserCAStore yes",
"wolfSSH_WinUserPvPara wolfSSHTestCA") `
"no store location"
Test-SshdRejects "unsupported store provider" `
@("Port $port", "HostKey $hostKey",
"wolfSSH_TrustedUserCAStore yes",
"wolfSSH_WinUserPvPara wolfSSHTestCA",
"wolfSSH_WinUserDwFlags LOCAL_MACHINE",
"wolfSSH_WinUserStores CERT_STORE_PROV_MEMORY") `
"is not supported"
Test-SshdRejects "unrecognized store location" `
@("Port $port", "HostKey $hostKey",
"wolfSSH_TrustedUserCAStore yes",
"wolfSSH_WinUserPvPara wolfSSHTestCA",
"wolfSSH_WinUserDwFlags NOT_A_LOCATION") `
"Unrecognized user CA store flags"
foreach ($storeName in @("Root", "root", "Root\", "SID\Root")) {
Test-SshdRejects "OS trust store name '$storeName' refused" `
@("Port $port", "HostKey $hostKey",
"wolfSSH_TrustedUserCAStore yes",
"wolfSSH_WinUserPvPara $storeName",
"wolfSSH_WinUserDwFlags LOCAL_MACHINE") `
"names a Windows system"
}
Test-SshdRejects "WinUser options without the store enabled" `
($base + @("wolfSSH_WinUserPvPara wolfSSHTestCA")) `
"wolfSSH_TrustedUserCAStore is not enabled"
Test-SshdRejects "user CA store with only a leaf certificate" `
@("Port $port", "HostKey $hostKey",
"wolfSSH_TrustedUserCAStore yes",
"wolfSSH_WinUserPvPara wolfSSHLeafOnlyCA",
"wolfSSH_WinUserDwFlags LOCAL_MACHINE") `
"No usable CA certificates found in store"
Test-SshdRejects "wolfSSH_HostKeyStore without wolfSSH_HostKeyStoreSubject" `
($base[0..0] + @("TrustedUserCAKeys $caPem", "wolfSSH_HostKeyStore My",
"wolfSSH_HostKeyStoreFlags LOCAL_MACHINE")) `
"wolfSSH_HostKeyStoreSubject is missing"
Test-SshdRejects "wolfSSH_HostKeyStore without wolfSSH_HostKeyStoreFlags" `
($base[0..0] + @("TrustedUserCAKeys $caPem", "wolfSSH_HostKeyStore My",
"wolfSSH_HostKeyStoreSubject wolfSSH-Test-Server")) `
"wolfSSH_HostKeyStoreFlags is missing"
Test-SshdRejects "wolfSSH_HostKeyStoreSubject/Flags without wolfSSH_HostKeyStore" `
($base[0..0] + @("TrustedUserCAKeys $caPem",
"wolfSSH_HostKeyStoreSubject wolfSSH-Test-Server",
"wolfSSH_HostKeyStoreFlags LOCAL_MACHINE")) `
"wolfSSH_HostKeyStore is missing"
Test-SshdRejects "wolfSSH_HostKeyStore conflicts with HostKey" `
($base + @("wolfSSH_HostKeyStore My",
"wolfSSH_HostKeyStoreSubject wolfSSH-Test-Server",
"wolfSSH_HostKeyStoreFlags LOCAL_MACHINE")) `
"HostKey conflicts"
Test-SshdRejects "wolfSSH_HostKeyStore conflicts with HostCertificate" `
($base[0..0] + @("TrustedUserCAKeys $caPem",
"HostCertificate $hostCert", "wolfSSH_HostKeyStore My",
"wolfSSH_HostKeyStoreSubject wolfSSH-Test-Server",
"wolfSSH_HostKeyStoreFlags LOCAL_MACHINE")) `
"HostCertificate conflicts"
Test-SshdRejects "-h conflicts with wolfSSH_HostKeyStore" `
($base[0..0] + @("TrustedUserCAKeys $caPem", "wolfSSH_HostKeyStore My",
"wolfSSH_HostKeyStoreSubject wolfSSH-Test-Server",
"wolfSSH_HostKeyStoreFlags LOCAL_MACHINE")) `
"-h host key file conflicts" @("-h", $hostKey)
# This build has no WOLFSSL_SYS_CA_CERTS, so the system CA directive
# must fail closed rather than run without the configured anchors.
Test-SshdRejects "system CA on a build without WOLFSSL_SYS_CA_CERTS" `
($base + @("wolfSSH_TrustedSystemCAKeys yes")) `
"WOLFSSL_SYS_CA_CERTS"
if ($script:anyFailed) { exit 1 }
Write-Host "All invalid cert-store configurations were rejected"
# The store hive warning must reach the log without -d: the log callback
# writes WS_LOG_WARN unconditionally. CurrentUser has no wolfSSHTestCA
# store, so the daemon exits after warning and nothing is left running.
- name: wolfSSHd logs store hive warning without -d
if: matrix.user_ca_source == 'store'
working-directory: ${{ github.workspace }}\wolfssh
shell: pwsh
timeout-minutes: 3
run: |
$port = ${{env.TEST_PORT}}
$hostKey = (Resolve-Path "keys\server-key.pem").Path
@("Port $port", "HostKey $hostKey",
"wolfSSH_TrustedUserCAStore yes",
"wolfSSH_WinUserPvPara wolfSSHTestCA",
"wolfSSH_WinUserDwFlags CURRENT_USER") |
Out-File -FilePath sshd_config_warn -Encoding ASCII
$cfgFull = (Resolve-Path "sshd_config_warn").Path
$logFile = Join-Path (Get-Location).Path "sshd_warn_log.txt"
$proc = Start-Process -FilePath (Resolve-Path $env:SSHD_PATH).Path `
-ArgumentList @("-D", "-f", $cfgFull, "-p", $port, "-E", $logFile) `
-NoNewWindow -PassThru
$exited = $proc.WaitForExit(30000)
if (-not $exited) {
Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue
}
$log = ""
if (Test-Path $logFile) { $log = Get-Content $logFile -Raw }
Write-Host "=== wolfsshd log ==="
Write-Host $log
if ($log -notmatch "without elevation") {
Write-Host "ERROR: hive warning did not reach the log without -d"
exit 1
}
Write-Host "Hive warning was logged without -d"
- name: Start wolfSSHd as Windows service
working-directory: ${{ github.workspace }}\wolfssh
shell: pwsh
run: |
# sc.exe failures are diagnosed in-script (query + event log dump);
# without this, pwsh 7.4 throws before the diagnostics run.
$PSNativeCommandUseErrorActionPreference = $false
$sshdPathFull = (Resolve-Path $env:SSHD_PATH).Path
$configPathFull = (Resolve-Path "sshd_config_test").Path
$serviceName = "wolfsshd"
# Remove service if it already exists
$existingService = Get-Service -Name $serviceName -ErrorAction SilentlyContinue
if ($existingService) {
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
}
# 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.
# Single-string binPath with embedded quotes: how pwsh renders it to
# sc.exe depends on $PSNativeCommandArgumentPassing. This works here
# only because CI workspace paths contain no spaces.
$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
}
$startResult = sc.exe start $serviceName
if ($LASTEXITCODE -ne 0) {
Write-Host "ERROR: Failed to start service"
Write-Host $startResult
sc.exe query $serviceName
exit 1
}
Start-Sleep -Seconds 5
$service = Get-Service -Name $serviceName -ErrorAction SilentlyContinue
if (-not $service -or $service.Status -ne 'Running') {
Write-Host "ERROR: Service is not running. Status: $($service.Status)"
sc.exe query $serviceName
Get-WinEvent -FilterHashtable @{LogName='System'; ProviderName='Service Control Manager'} -MaxEvents 20 -ErrorAction SilentlyContinue |
Where-Object { $_.Message -like "*$serviceName*" } |
Select-Object TimeCreated, LevelDisplayName, Message | Format-List
exit 1
}
Write-Host "wolfSSHd service is running"
Add-Content -Path $env:GITHUB_ENV -Value "SSHD_SERVICE_NAME=$serviceName"
- name: Test SFTP connection against wolfsshd
working-directory: ${{ github.workspace }}\wolfssh
shell: pwsh
timeout-minutes: 3
run: |
$testPort = ${{env.TEST_PORT}}
$sftpPath = $env:SFTP_PATH
# Verify the server is listening before running the client
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)
$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: $_"
exit 1
}
@"
pwd
ls
quit
"@ | Out-File -FilePath sftp_commands.txt -Encoding ASCII
$sftpArgs = @("-u", "testuser", "-h", "localhost", "-p", "$testPort")
$caCertDer = (Resolve-Path "keys\ca-cert-ecc.der").Path
if ("${{ matrix.client_key_source }}" -eq "store") {
$sftpArgs += "-W", "My:$($env:CLIENT_CERT_SUBJECT):CURRENT_USER"
} else {
$sftpArgs += "-J", (Resolve-Path $env:CLIENT_CERT_FILE).Path
$sftpArgs += "-i", (Resolve-Path $env:CLIENT_KEY_FILE).Path
}
# -A: CA cert for host verification; -X: ignore peer IP vs cert checks
$sftpArgs += "-A", $caCertDer, "-X"
Write-Host "Running: $sftpPath $($sftpArgs -join ' ')"
Write-Host "Test matrix: server=${{ matrix.server_key_source }}, client=${{ matrix.client_key_source }}"
$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 }
if ($process.ExitCode -ne 0) {
Write-Host "ERROR: SFTP client exited with code $($process.ExitCode)"
exit 1
}
# ls discards errors and doCmds always returns success, so assert on
# the listing itself rather than on the exit code alone.
if ((Get-Content sftp_output.txt -Raw) -notmatch "wolfssh_sftp_marker.txt") {
Write-Host "ERROR: remote listing did not contain the marker file"
exit 1
}
Write-Host "Test completed - key exchange and SFTP connection succeeded"
# The certificate identity binding: a client certificate whose identity
# does not match the requested account must be rejected, and the match is
# case-insensitive like Windows account names. On the FPKI build
# (OPENSSL_ALL turns on WOLFSSL_ASN_ALL and with it WOLFSSL_FPKI) the
# binding is the certificate UPN, which renewcerts.cnf sets to
# <user>@example alongside the CN; on the -no-fpki build it is the
# subject CN. The wronguser cert differs in both, so the same three
# connections cover either branch. Runs against the already-running
# service on the user_ca_source: store entries, whose config sets no
# AuthorizedKeysFile so the identity binding is what decides.
- name: Client certificate identity binding is enforced (UPN with FPKI, CN without)
if: matrix.user_ca_source == 'store'
working-directory: ${{ github.workspace }}\wolfssh
shell: pwsh
timeout-minutes: 5
run: |
$PSNativeCommandUseErrorActionPreference = $false
$testPort = ${{env.TEST_PORT}}
$sftpPath = $env:SFTP_PATH
$caCertDer = (Resolve-Path "keys\ca-cert-ecc.der").Path
# Issue a certificate for a CN that names no requested account,
# signed by the same trusted CA. Disable MSYS path conversion so Git
# Bash does not rewrite the leading-slash -subj argument into
# C:/Program Files/Git/C=US/...
$env:MSYS_NO_PATHCONV = "1"
$env:MSYS2_ARG_CONV_EXCL = "*"
Push-Location keys
& bash -c "touch index.txt && sed 's/fred/wronguser/g' renewcerts.cnf > renewcerts-wronguser.cnf && openssl ecparam -name prime256v1 -genkey -noout -out wronguser-key.pem && openssl req -subj '/C=US/ST=WA/L=Seattle/O=wolfSSL Inc/OU=Development/CN=wronguser' -key wronguser-key.pem -out wronguser-cert.csr -config renewcerts-wronguser.cnf -new -nodes && openssl x509 -req -in wronguser-cert.csr -days 3650 -extfile renewcerts-wronguser.cnf -extensions v3_wronguser -CA ca-cert-ecc.pem -CAkey ca-key-ecc.pem -out wronguser-cert.pem -set_serial 8 && openssl x509 -in wronguser-cert.pem -outform DER -out wronguser-cert.der && openssl ec -in wronguser-key.pem -outform DER -out wronguser-key.der"
if ($LASTEXITCODE -ne 0) { Pop-Location; Write-Host "ERROR: wronguser cert creation failed"; exit 1 }
Pop-Location
# Recreate the service with debug logging so a rejection in this step
# is diagnosable: the service logs only to OutputDebugString
# otherwise. C:\Windows\Temp is writable by LocalSystem where the
# workspace is not.
$cnLog = "C:\Windows\Temp\wolfsshd_cn_debug.log"
$sshdPathFull = (Resolve-Path $env:SSHD_PATH).Path
$configPathFull = (Resolve-Path "sshd_config_test").Path
Stop-Service -Name wolfsshd -Force
$binPath = "`"$sshdPathFull`" -f `"$configPathFull`" -p $testPort -d -E `"$cnLog`""
sc.exe config wolfsshd binPath= $binPath | Out-Null
if ($LASTEXITCODE -ne 0) { Write-Host "ERROR: sc config failed"; exit 1 }
Start-Service -Name wolfsshd
Start-Sleep -Seconds 3
$svc = Get-Service -Name wolfsshd
if ($svc.Status -ne 'Running') {
Write-Host "ERROR: service not running after reconfigure"
sc.exe query wolfsshd
exit 1
}
"quit" | Out-File -FilePath sftp_cn_commands.txt -Encoding ASCII
# Run all three connections first, then stop the service and evaluate:
# the service keeps the -E log open without read sharing, so the log
# is only readable once the service has been stopped.
# Control connection: the exact-case user against the reconfigured
# service must still succeed, so a later failure is attributable to
# the case-differing name rather than to service state.
$args0 = @("-u", "testuser", "-h", "localhost", "-p", "$testPort",
"-J", (Resolve-Path $env:CLIENT_CERT_FILE).Path,
"-i", (Resolve-Path $env:CLIENT_KEY_FILE).Path,
"-A", $caCertDer, "-X")
$p = Start-Process -FilePath $sftpPath -ArgumentList $args0 `
-RedirectStandardInput "sftp_cn_commands.txt" `
-RedirectStandardOutput "sftp_cn0_out.txt" `
-RedirectStandardError "sftp_cn0_err.txt" `
-Wait -NoNewWindow -PassThru
$controlExit = $p.ExitCode
# CN=wronguser presented for -u testuser must fail
$args1 = @("-u", "testuser", "-h", "localhost", "-p", "$testPort",
"-J", (Resolve-Path "keys\wronguser-cert.der").Path,
"-i", (Resolve-Path "keys\wronguser-key.der").Path,
"-A", $caCertDer, "-X")
$p = Start-Process -FilePath $sftpPath -ArgumentList $args1 `
-RedirectStandardInput "sftp_cn_commands.txt" `
-RedirectStandardOutput "sftp_cn_out.txt" `
-RedirectStandardError "sftp_cn_err.txt" `
-Wait -NoNewWindow -PassThru
$wrongExit = $p.ExitCode
# CN=testuser presented for -u TESTUSER must succeed: Windows account
# names are case-insensitive and the CN match follows suit
$args2 = @("-u", "TESTUSER", "-h", "localhost", "-p", "$testPort",
"-J", (Resolve-Path $env:CLIENT_CERT_FILE).Path,
"-i", (Resolve-Path $env:CLIENT_KEY_FILE).Path,
"-A", $caCertDer, "-X")
$p = Start-Process -FilePath $sftpPath -ArgumentList $args2 `
-RedirectStandardInput "sftp_cn_commands.txt" `
-RedirectStandardOutput "sftp_cn2_out.txt" `
-RedirectStandardError "sftp_cn2_err.txt" `
-Wait -NoNewWindow -PassThru
$upperExit = $p.ExitCode
# Release the log before reading it
Stop-Service -Name wolfsshd -Force -ErrorAction SilentlyContinue
$srvLog = ""
if (Test-Path $cnLog) { $srvLog = Get-Content $cnLog -Raw }
$failed = $false
if ($controlExit -ne 0) {
Write-Host "ERROR: exact-case control connection failed"
Get-Content sftp_cn0_out.txt, sftp_cn0_err.txt
$failed = $true
} else {
Write-Host "Exact-case control connection accepted"
}
if ($wrongExit -eq 0) {
Write-Host "ERROR: CN=wronguser was accepted for user testuser"
Get-Content sftp_cn_out.txt, sftp_cn_err.txt
$failed = $true
}
elseif ($srvLog -notmatch "incorrect user cert") {
# The rejection must be the CN identity check, not an incidental
# failure earlier or later in the exchange.
Write-Host "ERROR: wronguser was rejected for a reason other than the CN check"
Get-Content sftp_cn_out.txt, sftp_cn_err.txt
$failed = $true
} else {
Write-Host "CN mismatch rejected (exit $wrongExit)"
}
if ($upperExit -ne 0) {
Write-Host "ERROR: case-differing user TESTUSER was rejected"
Get-Content sftp_cn2_out.txt, sftp_cn2_err.txt
$failed = $true
} else {
Write-Host "Case-insensitive CN match accepted"
}
if ($failed) {
if ($srvLog -ne "") {
Write-Host "=== wolfsshd debug log (tail) ==="
$lines = $srvLog -split "`n"
$lines | Select-Object -Last 400
} else {
Write-Host "(no wolfsshd debug log was written)"
}
exit 1
}
# Restore the original binPath and restart the service so any step
# added after this one gets a running, normally-configured daemon.
$origBinPath = "`"$sshdPathFull`" -f `"$configPathFull`" -p $testPort"
sc.exe config wolfsshd binPath= $origBinPath | Out-Null
Start-Service -Name wolfsshd
# -W supplies both keys, so combining it with -i/-j/-J is a usage error
# the client must refuse before connecting.
- name: SFTP client rejects -W combined with -i
if: matrix.client_key_source == 'store'
working-directory: ${{ github.workspace }}\wolfssh
shell: pwsh
timeout-minutes: 2
run: |
$PSNativeCommandUseErrorActionPreference = $false
"quit" | Out-File -FilePath sftp_wconflict_cmd.txt -Encoding ASCII
$conflictArgs = @("-u", "testuser", "-h", "localhost",
"-p", "${{env.TEST_PORT}}",
"-W", "My:$($env:CLIENT_CERT_SUBJECT):CURRENT_USER",
"-i", (Resolve-Path $env:CLIENT_KEY_FILE).Path)
$p = Start-Process -FilePath $env:SFTP_PATH -ArgumentList $conflictArgs `
-RedirectStandardInput "sftp_wconflict_cmd.txt" `
-RedirectStandardOutput "sftp_wconflict_out.txt" `
-RedirectStandardError "sftp_wconflict_err.txt" `
-Wait -NoNewWindow -PassThru
$log = ""
foreach ($f in @("sftp_wconflict_out.txt", "sftp_wconflict_err.txt")) {
if (Test-Path $f) { $log += (Get-Content $f -Raw) }
}
if ($p.ExitCode -eq 0) {
Write-Host "ERROR: -W with -i was accepted"
exit 1
}
if ($log -notmatch "can not be used with") {
Write-Host "ERROR: expected the -W conflict message"
Write-Host $log
exit 1
}
Write-Host "-W with -i rejected as expected"
- name: Cleanup
if: always()
shell: pwsh
run: |
# Stop echoserver if it is still running
$echoserverPid = $env:ECHOSERVER_PID
if (-not [string]::IsNullOrEmpty($echoserverPid)) {
Stop-Process -Id $echoserverPid -Force -ErrorAction SilentlyContinue
}
Get-Process -Name "echoserver" -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
# 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') {
Stop-Service -Name $serviceName -Force -ErrorAction SilentlyContinue
Start-Sleep -Seconds 2
}
sc.exe delete $serviceName | Out-Null
}
# Remove test certificates from the stores
Get-ChildItem -Path "Cert:\CurrentUser\My" | Where-Object {
$_.Subject -like "*wolfSSH-Test*" -or $_.Subject -like "*testuser*"
} | Remove-Item -Force -ErrorAction SilentlyContinue
Get-ChildItem -Path "Cert:\LocalMachine\My" | Where-Object {
$_.Subject -like "*wolfSSH-Test*"
} | Remove-Item -Force -ErrorAction SilentlyContinue
foreach ($s in @("wolfSSHTestCA", "wolfSSHEmptyCA", "wolfSSHLeafOnlyCA")) {
Get-ChildItem -Path "Cert:\LocalMachine\$s" -ErrorAction SilentlyContinue |
Remove-Item -Force -ErrorAction SilentlyContinue
# Remove the store itself (a registry key), not just its
# contents, so nothing persists across runs on a self-hosted
# runner
Remove-Item -Path "HKLM:\SOFTWARE\Microsoft\SystemCertificates\$s" `
-Recurse -Force -ErrorAction SilentlyContinue
}
# Remove generated key material from the checkout (private keys for
# CA-signed identities must not persist on a self-hosted runner)
Remove-Item -Path "$env:GITHUB_WORKSPACE\wolfssh\keys\wronguser-*" `
-Force -ErrorAction SilentlyContinue
Remove-Item -Path "$env:GITHUB_WORKSPACE\wolfssh\keys\testuser-key.*" `
-Force -ErrorAction SilentlyContinue
Write-Host "Cleaned up test certificates"