mirror of https://github.com/wolfSSL/wolfssh.git
1717 lines
79 KiB
YAML
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"
|