Enforce LoginGraceTime in wolfsshd on Windows and make the grace flag per-connection

pull/1043/head
Yosuke Shimizu 2026-06-15 14:50:42 +09:00 committed by John Safranek
parent 09cbe92daa
commit 62639efd35
5 changed files with 322 additions and 44 deletions

View File

@ -43,13 +43,12 @@ jobs:
working-directory: ${{env.GITHUB_WORKSPACE}}wolfssl
run: nuget restore ${{env.WOLFSSL_SOLUTION_FILE_PATH}}
- name: updated user_settings.h for sshd and x509
- name: Enable wolfSSH options (sshd, sftp, x509) in user_settings.h
working-directory: ${{env.GITHUB_WORKSPACE}}
run: cp ${{env.USER_SETTINGS_H_NEW}} ${{env.USER_SETTINGS_H}}
- name: replace wolfSSL user_settings.h with wolfSSH user_settings.h
working-directory: ${{env.GITHUB_WORKSPACE}}
run: get-content ${{env.USER_SETTINGS_H_NEW}} | %{$_ -replace "if 0","if 1"}
shell: bash
run: |
sed -i 's/#if 0/#if 1/g' ${{env.USER_SETTINGS_H_NEW}}
cp ${{env.USER_SETTINGS_H_NEW}} ${{env.USER_SETTINGS_H}}
- name: Build wolfssl library
working-directory: ${{env.GITHUB_WORKSPACE}}wolfssl
@ -65,6 +64,30 @@ jobs:
# See https://docs.microsoft.com/visualstudio/msbuild/msbuild-command-line-reference
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: Locate wolfsshd.exe and stage wolfssl.dll
working-directory: ${{ github.workspace }}\wolfssh
shell: pwsh
run: |
$sshdExe = Get-ChildItem -Path "${{ github.workspace }}\wolfssh" -Recurse -Filter "wolfsshd.exe" -ErrorAction SilentlyContinue |
Where-Object { $_.FullName -like "*Release*" } | Select-Object -First 1
if (-not $sshdExe) {
Write-Host "ERROR: wolfsshd.exe not found"
exit 1
}
Add-Content -Path $env:GITHUB_ENV -Value "SSHD_PATH=$($sshdExe.FullName)"
$sshdDir = Split-Path -Parent $sshdExe.FullName
$wolfsslDll = Get-ChildItem -Path "${{ github.workspace }}\wolfssl" -Recurse -Filter "wolfssl.dll" -ErrorAction SilentlyContinue | Select-Object -First 1
if ($wolfsslDll -and -not (Test-Path (Join-Path $sshdDir "wolfssl.dll"))) {
Copy-Item -Path $wolfsslDll.FullName -Destination (Join-Path $sshdDir "wolfssl.dll") -Force
}
- name: Test LoginGraceTime enforcement on Windows
working-directory: ${{ github.workspace }}\wolfssh\apps\wolfsshd\test
shell: pwsh
timeout-minutes: 2
run: .\sshd_login_grace_test.ps1 -SshdExe "$env:SSHD_PATH"
# Build and run the self-contained unit tests with the MSVC AddressSanitizer.
# This is the only job that executes wolfSSH tests under a sanitizer on
# Windows, where the USE_WINDOWS_API console code (e.g. wolfSSH_DoOSC) is

View File

@ -370,7 +370,6 @@ else
#Github actions needs resolved for these test cases
#run_test "error_return.sh"
#run_test "sshd_login_grace_test.sh"
# add additional tests here, check on var USING_LOCAL_HOST if can make sshd
# server start/restart with changes
@ -397,9 +396,10 @@ else
run_test "sshd_window_full_test.sh"
run_test "sshd_empty_password_test.sh"
run_strictmodes_negative_test
run_test "sshd_login_grace_test.sh"
else
printf "Skipping tests that need to setup local SSHD\n"
SKIPPED=$((SKIPPED+4))
SKIPPED=$((SKIPPED+5))
fi
# these tests run with X509 sshd-config loaded

View File

@ -0,0 +1,136 @@
#!/usr/bin/env pwsh
#
# Windows regression test for wolfsshd LoginGraceTime enforcement.
#
# Opens a raw TCP connection that never authenticates and verifies that
# wolfsshd drops it at the login grace deadline. No Windows user account or
# authorized key is required, because the connection is closed before
# authentication ever completes - this exercises the pre-auth grace timer only.
#
# The positive path - a successful authentication calling CancelLoginTimer so
# the session survives past the grace deadline - is deliberately not covered
# here, as it would require provisioning a real Windows user account and key.
# That cancel-on-success path is exercised on POSIX by sshd_login_grace_test.sh.
#
# Enforcement is checked behaviorally (the server closes the connection at the
# grace deadline), not by reading the daemon log, so the test does not depend on
# debug logging being compiled into the wolfSSH Windows build.
#
# Usage:
# pwsh sshd_login_grace_test.ps1 -SshdExe <path-to-wolfsshd.exe> [-Port N] [-Grace N]
# (SshdExe also accepts the SSHD_PATH environment variable.)
param(
[string]$SshdExe = $env:SSHD_PATH,
[int]$Port = 22224,
[int]$Grace = 5
)
$ErrorActionPreference = "Stop"
$exitCode = 1
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$repoRoot = (Resolve-Path (Join-Path $scriptDir "..\..\..")).Path
$keyPath = (Resolve-Path (Join-Path $repoRoot "keys\server-key.pem")).Path
$confFile = Join-Path $scriptDir "sshd_config_test_login_grace"
$authFile = Join-Path $scriptDir "authorized_keys_test"
if (-not $SshdExe -or -not (Test-Path $SshdExe)) {
Write-Host "ERROR: wolfsshd.exe not found (pass -SshdExe or set SSHD_PATH)"
exit 1
}
@"
Port $Port
Protocol 2
LoginGraceTime $Grace
PermitRootLogin yes
PasswordAuthentication yes
PermitEmptyPasswords no
UseDNS no
HostKey $keyPath
AuthorizedKeysFile $authFile
"@ | Out-File -FilePath $confFile -Encoding ASCII
"" | Out-File -FilePath $authFile -Encoding ASCII
# Run wolfsshd in the foreground (-D selects the non-service path on Windows).
$sshd = Start-Process -FilePath $SshdExe `
-ArgumentList "-D", "-f", "`"$confFile`"", "-p", "$Port" `
-NoNewWindow -PassThru
try {
# Wait for the listener to accept connections.
$up = $false
for ($i = 0; $i -lt 20; $i++) {
try {
$probe = New-Object System.Net.Sockets.TcpClient
$probe.Connect("127.0.0.1", $Port)
$probe.Close()
$up = $true
break
}
catch {
Start-Sleep -Milliseconds 500
}
}
if (-not $up) {
# throw rather than exit so the finally block still stops the daemon
throw "wolfsshd did not start listening on port $Port"
}
# Open a raw TCP connection and never authenticate. The server sends its
# banner, waits for ours, and must close the connection once the login grace
# time expires. Block on Read (with a timeout well past the grace time) and
# measure when the server closes the connection.
$stall = New-Object System.Net.Sockets.TcpClient
$stall.Connect("127.0.0.1", $Port)
$stream = $stall.GetStream()
$stream.ReadTimeout = ($Grace + 5) * 1000
$buf = New-Object byte[] 4096
$dropped = $false
$sw = [System.Diagnostics.Stopwatch]::StartNew()
try {
while ($true) {
$n = $stream.Read($buf, 0, $buf.Length)
if ($n -le 0) {
$dropped = $true # server closed the connection
break
}
# otherwise the server sent its banner; keep waiting for the drop
}
}
catch [System.IO.IOException] {
# A server close can surface as a reset (IOException), not a graceful
# EOF. Decide by elapsed time like the .sh test: a throw before the read
# timeout means the server closed the connection.
$dropped = $sw.Elapsed.TotalSeconds -lt ($Grace + 4)
}
$elapsed = [math]::Round($sw.Elapsed.TotalSeconds, 1)
$stall.Close()
Write-Host "connection closed=$dropped after ${elapsed}s (grace=$Grace)"
if ($dropped -and ($elapsed -ge ($Grace - 1)) -and ($elapsed -le ($Grace + 4))) {
Write-Host "PASS: unauthenticated connection dropped at login grace deadline"
$exitCode = 0
}
elseif ($dropped) {
Write-Host "FAIL: connection closed at ${elapsed}s, not near the grace deadline ($Grace s)"
}
else {
Write-Host "FAIL: connection still open past the grace time (not enforced)"
}
}
catch {
Write-Host "ERROR: $_"
$exitCode = 1
}
finally {
if ($sshd -and -not $sshd.HasExited) {
Stop-Process -Id $sshd.Id -Force -ErrorAction SilentlyContinue
}
}
exit $exitCode

View File

@ -33,6 +33,9 @@ AuthorizedKeysFile $PWD/authorized_keys_test
EOF
start_wolfsshd "sshd_config_test_login_grace"
# ensure the daemon is stopped on every exit path, including the failure
# exits below, so a leftover wolfsshd cannot interfere with later tests
trap stop_wolfsshd EXIT
pushd ../../..
TEST_CLIENT="./examples/client/client"
@ -47,22 +50,55 @@ if [ "$RESULT" != 0 ]; then
exit 1
fi
# attempt clearing out stdin from previous echo/grep
read -t 1 -n 1000 discard
# test grace login timeout by stalling on password prompt
timeout --foreground 7 "$TEST_CLIENT" -u "$USER" -h "$TEST_HOST" -p "$TEST_PORT" -t
popd
cat ./log.txt | grep "Failed login within grace period"
RESULT=$?
if [ "$RESULT" != 0 ]; then
echo "FAIL: Grace period not hit"
cat ./log.txt
# Test the grace-time timeout behaviorally: open a raw TCP connection, never
# authenticate, and confirm the server closes it at the grace deadline. This
# asserts the actual behavior rather than scraping the log, matching the Windows
# PowerShell test (and not relying on the daemon log being readable).
GRACE=5
if ! exec 3<>"/dev/tcp/$TEST_HOST/$TEST_PORT"; then
echo "FAIL: could not connect to $TEST_HOST:$TEST_PORT"
exit 1
fi
# The server sends its banner, waits for ours (which never comes), then closes
# the connection once the grace time expires. Read until the server closes the
# connection (EOF) or the per-read timeout elapses, and measure how long it
# took. Use a large read timeout (GRACE + 8) and decide by elapsed time rather
# than read's exit status, which differs across bash versions (timeout returns
# >128 on modern bash but 1 on the macOS bash 3.2).
START=$SECONDS
while true; do
if IFS= read -r -t $((GRACE + 8)) -n 1024 _ <&3; then
: # received banner/data, keep waiting for the server to close
else
break # server closed (EOF) or the read timeout elapsed
fi
done
ELAPSED=$((SECONDS - START))
exec 3>&-
# An exit well before the read timeout means the server closed the connection;
# an exit near GRACE + 8 means it stayed open (not enforced).
if [ "$ELAPSED" -le $((GRACE + 4)) ]; then
DROPPED=1
else
DROPPED=0
fi
echo "connection closed=$DROPPED after ${ELAPSED}s (grace=$GRACE)"
if [ "$DROPPED" = 1 ] && [ "$ELAPSED" -ge $((GRACE - 1)) ]; then
echo "PASS: unauthenticated connection dropped at login grace deadline"
elif [ "$DROPPED" = 1 ]; then
echo "FAIL: connection closed at ${ELAPSED}s, before the grace deadline ($GRACE s)"
exit 1
else
echo "FAIL: connection still open past the grace time (not enforced)"
exit 1
fi
stop_wolfsshd
exit 0

View File

@ -117,6 +117,12 @@ typedef struct WOLFSSHD_CONNECTION {
int listenFd;
char ip[INET6_ADDRSTRLEN];
byte isThreaded;
#ifdef _WIN32
/* set when the login grace time expires before authentication completes;
* POSIX uses the file-scope loginGraceTimedOut flag instead */
volatile byte timeOut;
PTP_TIMER loginTimer; /* threadpool timer enforcing login grace time */
#endif
} WOLFSSHD_CONNECTION;
#ifdef __unix__
@ -217,11 +223,11 @@ static void interruptCatch(int in)
static void wolfSSHDLoggingCb(enum wolfSSH_LogLevel lvl, const char *const str)
{
/* always log errors and optionally log other info/debug level messages */
if (lvl == WS_LOG_ERROR) {
fprintf(logFile, "[PID %d]: %s\n", WGETPID(), str);
}
else if (debugMode) {
if (lvl == WS_LOG_ERROR || debugMode) {
fprintf(logFile, "[PID %d]: %s\n", WGETPID(), str);
/* flush so each line is visible immediately, e.g. to a consumer
* reading the log file while the daemon is still running */
fflush(logFile);
}
}
@ -1946,26 +1952,72 @@ static int SHELL_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh,
#endif
#endif
#ifdef WIN32
static volatile int timeOut = 0;
#ifdef _WIN32
/* Cancels and frees the login grace time threadpool timer for a connection. */
static void CancelLoginTimer(WOLFSSHD_CONNECTION* conn)
{
if (conn != NULL && conn->loginTimer != NULL) {
/* setting the due time to NULL cancels any pending timer */
SetThreadpoolTimer(conn->loginTimer, NULL, 0, 0);
WaitForThreadpoolTimerCallbacks(conn->loginTimer, TRUE);
CloseThreadpoolTimer(conn->loginTimer);
conn->loginTimer = NULL;
}
}
/* threadpool callback marking a connection as timed out at the grace deadline */
static VOID CALLBACK GraceTimeoutCb(PTP_CALLBACK_INSTANCE instance, PVOID ctx,
PTP_TIMER timer)
{
WOLFSSHD_CONNECTION* conn = (WOLFSSHD_CONNECTION*)ctx;
if (conn != NULL) {
/* published with an interlocked write so the connection thread reading
* the flag in LoginGraceExpired() sees it without a data race */
InterlockedExchange8((volatile CHAR*)&conn->timeOut, 1);
}
(void)instance;
(void)timer;
}
#else
static __thread int timeOut = 0;
#endif
/* Set by the SIGALRM handler when the login grace time expires. A signal
* handler may only access a volatile sig_atomic_t object, so the handler sets
* just this flag and the accept loop observes it. */
static volatile sig_atomic_t loginGraceTimedOut = 0;
static void alarmCatch(int signum)
{
timeOut = 1;
loginGraceTimedOut = 1;
(void)signum;
}
#endif
/* Returns non-zero once the login grace time has expired for this connection.
* On Windows the threadpool callback records it on the connection; on POSIX the
* SIGALRM handler sets a sig_atomic_t flag that the accept loop reads here. */
static int LoginGraceExpired(WOLFSSHD_CONNECTION* conn)
{
#ifdef _WIN32
/* interlocked read pairs with the InterlockedExchange8 in GraceTimeoutCb;
* the OR with 0 is a read-modify-write, hence the non-const parameter */
return InterlockedOr8((volatile CHAR*)&conn->timeOut, 0);
#else
(void)conn;
return (int)loginGraceTimedOut;
#endif
}
static int UserAuthResult(byte result,
WS_UserAuthData* authData, void* userAuthResultCtx)
{
(void)authData;
(void)userAuthResultCtx;
if (result == WOLFSSH_USERAUTH_SUCCESS) {
#ifndef WIN32
/* @TODO alarm catch on windows */
/* authentication finished in time, cancel the login grace timer */
#ifdef _WIN32
CancelLoginTimer((WOLFSSHD_CONNECTION*)userAuthResultCtx);
#else
(void)userAuthResultCtx;
alarm(0);
#endif
}
@ -2002,28 +2054,47 @@ static void* HandleConnection(void* arg)
if (ret == WS_SUCCESS) {
int select_ret = 0;
int timedOut = 0;
long graceTime;
wolfSSH_set_fd(ssh, conn->fd);
wolfSSH_SetUserAuthCtx(ssh, conn->auth);
/* let UserAuthResult reach this connection to cancel the grace timer */
wolfSSH_SetUserAuthResultCtx(ssh, conn);
/* set alarm for login grace time */
/* arm the login grace timer */
graceTime = wolfSSHD_AuthGetGraceTime(conn->auth);
if (graceTime > 0) {
#ifdef WIN32
/* LoginGraceTime enforcement is not yet implemented on Windows.
* @TODO implement via CreateWaitableTimer or similar. */
wolfSSH_Log(WS_LOG_WARN, "[SSHD] LoginGraceTime is set but "
"not enforced on this platform");
#ifdef _WIN32
FILETIME due;
ULARGE_INTEGER rel;
/* negative relative time, in 100 ns units */
rel.QuadPart = (ULONGLONG)(-((LONGLONG)graceTime * 10000000LL));
due.dwLowDateTime = rel.LowPart;
due.dwHighDateTime = rel.HighPart;
conn->loginTimer = CreateThreadpoolTimer(GraceTimeoutCb, conn, NULL);
if (conn->loginTimer == NULL) {
/* fail closed: mark the connection as timed out so the accept
* loop exits immediately rather than enforcing no grace time */
InterlockedExchange8((volatile CHAR*)&conn->timeOut, 1);
wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Unable to create login grace "
"timer, closing connection");
}
else {
SetThreadpoolTimer(conn->loginTimer, &due, 0, 0);
}
#else
signal(SIGALRM, alarmCatch);
loginGraceTimedOut = 0; /* clear any stale state before arming */
alarm((unsigned int)graceTime);
#endif
}
ret = wolfSSH_accept(ssh);
error = wolfSSH_get_error(ssh);
while (timeOut == 0 && (ret != WS_SUCCESS
while (LoginGraceExpired(conn) == 0 && (ret != WS_SUCCESS
&& ret != WS_SCP_INIT && ret != WS_SFTP_COMPLETE)
&& (error == WS_WANT_READ || error == WS_WANT_WRITE)) {
@ -2041,16 +2112,24 @@ static void* HandleConnection(void* arg)
error = WS_FATAL_ERROR;
}
wolfSSH_Log(WS_LOG_ERROR,
"[SSHD] grace time = %ld timeout = %d", graceTime, timeOut);
/* read the flag once through the accessor; on Windows this is an
* interlocked read of the value the threadpool callback published, on
* POSIX it is the SIGALRM handler's loginGraceTimedOut flag */
timedOut = LoginGraceExpired(conn);
wolfSSH_Log(WS_LOG_INFO,
"[SSHD] grace time = %ld timeout = %d", graceTime,
timedOut);
if (graceTime > 0) {
if (timeOut) {
/* only report a grace failure when authentication did not also
* complete in the same accept iteration */
if (timedOut && ret != WS_SUCCESS &&
ret != WS_SFTP_COMPLETE && ret != WS_SCP_INIT) {
wolfSSH_Log(WS_LOG_ERROR,
"[SSHD] Failed login within grace period");
}
#ifdef WIN32
/* @TODO SetTimer(NULL, NULL, graceTime, alarmCatch); */
#ifdef _WIN32
CancelLoginTimer(conn); /* cancel any pending timer */
#else
alarm(0); /* cancel any alarm */
#endif
@ -2736,6 +2815,10 @@ static int StartSSHD(int argc, char** argv)
conn->auth = auth;
conn->listenFd = (int)listenFd;
conn->isThreaded = isDaemon;
#ifdef _WIN32
conn->timeOut = 0;
conn->loginTimer = NULL;
#endif
/* wait for a connection */
if (PendingConnection(listenFd)) {