Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 135 additions & 22 deletions apps/wolfsshd/auth.c
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
#include <wolfssl/wolfcrypt/wc_port.h>
#include <wolfssl/wolfcrypt/error-crypt.h>
#include <wolfssl/wolfcrypt/coding.h>
#include <wolfssl/wolfcrypt/asn_public.h>

#ifdef WOLFSSL_FPKI
#include <wolfssl/wolfcrypt/asn.h>
Expand Down Expand Up @@ -160,20 +161,53 @@ struct WOLFSSHD_AUTH {
#endif

#ifndef MAX_LINE_SZ
/* Sized to hold the largest authorized_keys entry. */
/* Max authorized_keys entry size. */
#ifndef WOLFSSH_NO_MLDSA
#ifndef WOLFSSH_NO_MLDSA87
#define MAX_LINE_SZ ((WC_MLDSA_87_PUB_KEY_SIZE + 2) / 3 * 4 + 640)
#if defined(WOLFSSH_CERTS)
/* Max size for ML-DSA-87 certs plus headroom. */
#define MAX_LINE_SZ \
((WC_MLDSA_87_PUB_KEY_SIZE + WC_MLDSA_87_SIG_SIZE + \
COMPOSITE_MAX_TRAD_PUB_SZ + 1024 + 2) / 3 * 4 + 640)
#else
#define MAX_LINE_SZ \
((WC_MLDSA_87_PUB_KEY_SIZE + COMPOSITE_MAX_TRAD_PUB_SZ + \
2) / 3 * 4 + 640)
#endif
#elif !defined(WOLFSSH_NO_MLDSA65)
#define MAX_LINE_SZ ((WC_MLDSA_65_PUB_KEY_SIZE + 2) / 3 * 4 + 640)
#if defined(WOLFSSH_CERTS)
#define MAX_LINE_SZ \
((WC_MLDSA_65_PUB_KEY_SIZE + WC_MLDSA_65_SIG_SIZE + \
COMPOSITE_MAX_TRAD_PUB_SZ + 1024 + 2) / 3 * 4 + 640)
#else
#define MAX_LINE_SZ \
((WC_MLDSA_65_PUB_KEY_SIZE + COMPOSITE_MAX_TRAD_PUB_SZ + \
2) / 3 * 4 + 640)
#endif
#else
#define MAX_LINE_SZ ((WC_MLDSA_44_PUB_KEY_SIZE + 2) / 3 * 4 + 640)
#if defined(WOLFSSH_CERTS)
#define MAX_LINE_SZ \
((WC_MLDSA_44_PUB_KEY_SIZE + WC_MLDSA_44_SIG_SIZE + \
COMPOSITE_MAX_TRAD_PUB_SZ + 1024 + 2) / 3 * 4 + 640)
#else
#define MAX_LINE_SZ \
((WC_MLDSA_44_PUB_KEY_SIZE + COMPOSITE_MAX_TRAD_PUB_SZ + \
2) / 3 * 4 + 640)
#endif
#endif
#else
#define MAX_LINE_SZ 900
#endif
#endif

#ifdef WOLFSSHD_UNIT_TEST
/* Expose MAX_LINE_SZ for tests. */
word32 wolfsshd_test_MaxLineSz(void)
{
return (word32)MAX_LINE_SZ;
}
#endif

#if 0
/* this could potentially be useful in a deeply embedded future port */

Expand Down Expand Up @@ -270,6 +304,28 @@ static int CheckAuthKeysLine(char* line, word32 lineSz, const byte* key,
#endif
#endif
#endif
#if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256)
"ssh-mldsa44-es256@wolfssl.com",
#endif
#if !defined(WOLFSSH_NO_MLDSA65) && \
!defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) && !defined(NO_SHA512)
"ssh-mldsa65-es256@wolfssl.com",
#endif
#if !defined(WOLFSSH_NO_MLDSA87) && \
!defined(WOLFSSH_NO_ECDSA_SHA2_NISTP384) && !defined(NO_SHA512)
"ssh-mldsa87-es384@wolfssl.com",
#endif
#if !defined(WOLFSSH_NO_MLDSA44) && !defined(WOLFSSH_NO_ED25519) && \
!defined(NO_SHA512)
"ssh-mldsa44-ed25519@openssh.com",
#endif
#if !defined(WOLFSSH_NO_MLDSA65) && !defined(WOLFSSH_NO_ED25519) && \
!defined(NO_SHA512)
"ssh-mldsa65-ed25519@wolfssl.com",
#endif
#if !defined(WOLFSSH_NO_MLDSA87) && defined(HAVE_ED448)
"ssh-mldsa87-ed448@wolfssl.com",
#endif
};
const int NUM_ALLOWED_TYPES =
(int)(sizeof(allowedTypes) / sizeof(allowedTypes[0]));
Expand Down Expand Up @@ -1342,6 +1398,74 @@ static int SearchKeysFile(const char* keysFilePath, const byte* key,
return ret;
}

/* Detects host private key format. */
int wolfSSHD_DetectPrivKeyFormat(byte* data, word32 dataSz, void* heap,
byte** keyDer, byte** privBuf, word32* privBufSz)
{
int keyFormat = WOLFSSH_FORMAT_ASN1;
byte* der;
int derSz;

if (keyDer != NULL) {
*keyDer = NULL;
}
if (privBuf != NULL) {
*privBuf = NULL;
}
if (privBufSz != NULL) {
*privBufSz = 0;
}

if (data == NULL || dataSz == 0 || keyDer == NULL || privBuf == NULL ||
privBufSz == NULL) {
return WS_BAD_ARGUMENT;
}

der = (byte*)WMALLOC(dataSz, heap, DYNTYPE_SSHD);
if (der == NULL) {
return WS_MEMORY_E;
}

derSz = wc_KeyPemToDer(data, (int)dataSz, der, (int)dataSz, NULL);
if (derSz <= 0) {
WS_FORCEZERO(der, dataSz);
WFREE(der, heap, DYNTYPE_SSHD);

*privBuf = data;
*privBufSz = dataSz;

/* Strict prefix match for OpenSSH magic (35 bytes) */
if (*privBufSz >= 35 &&
WMEMCMP(*privBuf, "-----BEGIN OPENSSH PRIVATE KEY-----", 35) == 0) {
keyFormat = WOLFSSH_FORMAT_OPENSSH;
}
else if (*privBufSz >= sizeof("openssh-key-v1") &&
WMEMCMP(*privBuf, "openssh-key-v1",
sizeof("openssh-key-v1")) == 0) {
/* sizeof() includes the magic's trailing NUL */
keyFormat = WOLFSSH_FORMAT_OPENSSH;
}
else if (data[0] != 0x30) {
wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Failed to convert host private key from PEM.");
*privBuf = NULL;
*privBufSz = 0;
return WS_BAD_FILE_E;
}
}
else {
*keyDer = der;
*privBuf = der;
*privBufSz = (word32)derSz;
/* PEM-decoded result may still be an OpenSSH binary blob */
if (*privBufSz >= sizeof("openssh-key-v1") &&
WMEMCMP(*privBuf, "openssh-key-v1",
sizeof("openssh-key-v1")) == 0) {
keyFormat = WOLFSSH_FORMAT_OPENSSH;
}
}

return keyFormat;
}

WOLFSSHD_STATIC int SearchForPubKey(const char* path,
const char* authKeysFile, const char* user,
Expand Down Expand Up @@ -2107,9 +2231,7 @@ static int CheckPublicKeyWIN(const char* usr,
}
#endif /* _WIN32*/

/* Returns 1 if 'usr' is root-equivalent for PermitRootLogin (any uid 0
* account, or the literal name "root"; name-only on Windows). Shared by
* DoCheckUser and RequestAuthentication so all enforcement points agree. */
/* Check if user is root-equivalent. */
static int IsRootUser(const char* usr)
{
int isRoot = 0;
Expand All @@ -2128,17 +2250,14 @@ static int IsRootUser(const char* usr)
return isRoot;
}

/* Returns 1 if root login is denied outright, i.e. PermitRootLogin no.
* Used by DoCheckUser. */
/* Check if root login denied. */
WOLFSSHD_STATIC int IsRootLoginDenied(int isRoot, WOLFSSHD_CONFIG* usrConf)
{
return (isRoot == 1 &&
wolfSSHD_ConfigGetPermitRoot(usrConf) == WOLFSSHD_PERMIT_ROOT_NO);
}

/* Returns 1 if root password authentication is blocked, i.e.
* PermitRootLogin prohibit-password or forced-commands-only. Used by
* RequestAuthentication for WOLFSSH_USERAUTH_PASSWORD. */
/* Check if root password auth blocked. */
WOLFSSHD_STATIC int IsRootPasswordAuthBlocked(int isRoot,
WOLFSSHD_CONFIG* usrConf)
{
Expand All @@ -2149,9 +2268,7 @@ WOLFSSHD_STATIC int IsRootPasswordAuthBlocked(int isRoot,
WOLFSSHD_PERMIT_ROOT_FORCED_CMD));
}

/* Returns 1 if root public key login is missing the ForceCommand required by
* PermitRootLogin forced-commands-only. Used by RequestAuthentication for
* WOLFSSH_USERAUTH_PUBLICKEY. */
/* Check if ForceCommand missing for root. */
WOLFSSHD_STATIC int IsRootPubKeyForcedCmdMissing(int isRoot,
WOLFSSHD_CONFIG* usrConf)
{
Expand Down Expand Up @@ -2445,7 +2562,7 @@ static int RequestAuthentication(WS_UserAuthData* authData,
ret = WOLFSSH_USERAUTH_REJECTED;
}
else if (IsRootPasswordAuthBlocked(isRoot, usrConf)) {
/* prohibit-password and forced-commands-only both block this. */
/* Blocked by config. */
wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Password authentication for "
"root not allowed by configuration!");
ret = WOLFSSH_USERAUTH_REJECTED;
Expand Down Expand Up @@ -2510,7 +2627,7 @@ static int RequestAuthentication(WS_UserAuthData* authData,
if (ret == WOLFSSH_USERAUTH_SUCCESS &&
authData->type == WOLFSSH_USERAUTH_PUBLICKEY &&
IsRootPubKeyForcedCmdMissing(isRoot, usrConf)) {
/* forced-commands-only requires a forced command for root pubkey. */
/* Forced command required. */
wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Public key login for root requires "
"a forced command by configuration!");
ret = WOLFSSH_USERAUTH_REJECTED;
Expand Down Expand Up @@ -2840,11 +2957,7 @@ static int SetDefaultUserID(WOLFSSHD_AUTH* auth)

pwInfo = getpwnam(WOLFSSH_USER_STRING(WOLFSSH_SSHD_USER));
#ifdef WOLFSSHD_UNIT_TEST
/* Unit tests run wolfSSHD_AuthCreateUser() outside of a real daemon
* install, where the dedicated "sshd" system account may not exist.
* Fall back to the invoking user so auth-flow tests can exercise
* wolfSSHD_AuthCreateUser() without requiring that account. Never
* enabled in a production build. */
/* Fallback for unit tests. */
if (pwInfo == NULL) {
pwInfo = getpwuid(getuid());
}
Expand Down
7 changes: 7 additions & 0 deletions apps/wolfsshd/auth.h
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,12 @@ int wolfSSHD_GetHomeDirectory(WOLFSSHD_AUTH* auth, WOLFSSH* ssh, WCHAR* out, int
int wolfSSHD_OpenSecureFile(const char* path, WUID_T ownerUid,
int rejectReadable, void* heap, WFILE** out);

/* classifies a loaded host private key buffer as OpenSSH or ASN1/DER.
* *keyDer is a WMALLOC'd (heap, DYNTYPE_SSHD) buffer to WS_FORCEZERO +
* WFREE on a PEM decode, else NULL. */
int wolfSSHD_DetectPrivKeyFormat(byte* data, word32 dataSz, void* heap,
byte** keyDer, byte** privBuf, word32* privBufSz);

#ifdef WOLFSSHD_UNIT_TEST
#ifndef _WIN32
extern int (*wsshd_setregid_cb)(WGID_T, WGID_T);
Expand All @@ -128,6 +134,7 @@ int SearchForPubKey(const char* path, const char* authKeysFile,
const WS_UserAuthData_PublicKey* pubKeyCtx,
WUID_T uid, int strictModes);
#endif
word32 wolfsshd_test_MaxLineSz(void);
#if defined(WOLFSSH_HAVE_LIBCRYPT) || defined(WOLFSSH_HAVE_LIBLOGIN)
int CheckPasswordHashUnix(const char* input, const char* stored);
#endif
Expand Down
14 changes: 14 additions & 0 deletions apps/wolfsshd/test/create_sshd_config.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,20 @@ AuthorizedKeysFile $PWD/authorized_keys_test

EOF

cat <<EOF > sshd_config_test_mldsa
Port 22222
Protocol 2
LoginGraceTime 600
PermitRootLogin yes
PasswordAuthentication yes
PermitEmptyPasswords no
UsePrivilegeSeparation no
UseDNS no
HostKey $PWD/../../../keys/server-key-mldsa87es384
AuthorizedKeysFile $PWD/authorized_keys_test

EOF

cat <<EOF > sshd_config_test_x509
Port 22222
Protocol 2
Expand Down
21 changes: 21 additions & 0 deletions apps/wolfsshd/test/run_all_sshd_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,27 @@ else
stop_wolfsshd
fi

# ML-DSA composite host key test. Runs when we control the local daemon.
# The client side uses an ECC key since we only test the host key here.
# sshd_config_test_mldsa has no other host key, so a build without ML-DSA
# cannot start the daemon at all; check for support out here rather than
# letting the test script skip, which would come too late. ML-DSA comes from
# wolfSSL (HAVE_DILITHIUM) and has no wolfSSH configure option, so probe the
# client's algorithm list. The closed port keeps the probe from connecting.
if [ "$USING_LOCAL_HOST" == 1 ]; then
if ../../../examples/client/client -E -u "$USER" -h 127.0.0.1 -p 1 \
2>/dev/null | grep -q "ssh-mldsa87-es384@wolfssl.com"; then
start_wolfsshd "sshd_config_test_mldsa"
run_test "sshd_mldsa_composite_test.sh"
printf "Shutting down test wolfSSHd\n"
stop_wolfsshd
else
printf "sshd_mldsa_composite_test.sh ... SKIPPED\n"
TOTAL=$((TOTAL+1))
SKIPPED=$((SKIPPED+1))
fi
fi

# OpenSSH certificate user-auth test (self-contained: starts its own
# wolfSSHd; skips when not built with --enable-ossh-certs). Runs the suite
# against the wolfSSH example client and, for interop, the system OpenSSH
Expand Down
4 changes: 2 additions & 2 deletions apps/wolfsshd/test/ssh_kex_algos.sh
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ printf "\n"
# host key algorithms sent.
find_substring_of_algos() {
# Extract the substring between start and end lines
SUBSTRING=$(printf "$OUTPUT" | grep -A20 "Server Host Key Algorithms")
SUBSTRING=$(printf "$SUBSTRING" | grep -v -A15 "DKI: Enc Algorithms")
SUBSTRING=$(printf "$OUTPUT" | grep -A100 "Server Host Key Algorithms")
SUBSTRING=$(printf "$SUBSTRING" | grep -v -A95 "DKI: Enc Algorithms")
}

# take input argument $1 and checks if it is in the SUBSTRING
Expand Down
61 changes: 61 additions & 0 deletions apps/wolfsshd/test/sshd_mldsa_composite_test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
#!/bin/sh

# ML-DSA composite host key test.
#
# The daemon under test is started with sshd_config_test_mldsa, whose only host
# key is keys/server-key-mldsa87es384, an OpenSSH-format ML-DSA-87 + ECDSA-P384
# composite key. Pinning the client's host key algorithm list is what makes this
# an actual assertion about the negotiated algorithm: only the composite name
# may succeed, and any other name must fail to find a common host key algorithm.
#
# ML-DSA support comes from wolfSSL (HAVE_DILITHIUM) and has no wolfSSH
# configure option, so probe the client's algorithm list and skip (77) when the
# composite is not built in.

PWD0=`pwd`
cd ../../..

TEST_CLIENT="./examples/client/client"
USER=`whoami`
PRIVATE_KEY="./keys/hansel-key-ecc.der"
PUBLIC_KEY="./keys/hansel-key-ecc.pub"
ALGO="ssh-mldsa87-es384@wolfssl.com"
OTHER_ALGO="ecdsa-sha2-nistp256"

if [ -z "$1" ] || [ -z "$2" ]; then
echo "expecting host and port as arguments"
echo "./sshd_mldsa_composite_test.sh 127.0.0.1 22222"
exit 1
fi

skip() { echo "$1"; cd $PWD0; exit 77; }

[ -x "$TEST_CLIENT" ] || skip "wolfSSH example client not built, skipping"

# -E lists the compiled-in algorithms before connecting, so point it at a closed
# port to keep the probe from opening a session.
ALGOS=`$TEST_CLIENT -E -u $USER -h 127.0.0.1 -p 1 2>/dev/null`
echo "$ALGOS" | grep -q "$ALGO" || skip "no $ALGO support in this build, skipping"

# Positive: with only the composite offered, a successful login means it is what
# was negotiated.
set -e
echo "$TEST_CLIENT -k $ALGO -c 'ls' -u $USER -i $PRIVATE_KEY -j $PUBLIC_KEY -h \"$1\" -p \"$2\""
$TEST_CLIENT -k "$ALGO" -c 'ls' -u $USER -i $PRIVATE_KEY -j $PUBLIC_KEY -h "$1" -p "$2"
set +e

# Negative control: offering only ECDSA must fail, proving the pass above was
# not some other host key the daemon happened to have loaded.
if echo "$ALGOS" | grep -q "$OTHER_ALGO"; then
echo "$TEST_CLIENT -k $OTHER_ALGO -c 'ls' -u $USER -i $PRIVATE_KEY -j $PUBLIC_KEY -h \"$1\" -p \"$2\""
$TEST_CLIENT -k "$OTHER_ALGO" -c 'ls' -u $USER -i $PRIVATE_KEY -j $PUBLIC_KEY \
-h "$1" -p "$2"
if [ $? -eq 0 ]; then
echo "expected $OTHER_ALGO to fail against the composite host key"
cd $PWD0
exit 1
fi
fi

cd $PWD0
exit 0
Loading
Loading