This commit is contained in:
Matt Johnston 2022-03-22 16:17:47 +08:00
commit be4f9ce8e7
10 changed files with 142 additions and 29 deletions

View File

@ -65,6 +65,8 @@ jobs:
# configure_flags: --enable-fuzz --disable-harden --enable-bundled-libtom --enable-werror # configure_flags: --enable-fuzz --disable-harden --enable-bundled-libtom --enable-werror
# ldflags: -fsanitize=address # ldflags: -fsanitize=address
# extracflags: -fsanitize=address # extracflags: -fsanitize=address
# # -fsanitize=address prevents aslr, don't test it
# pytest_addopts: -k "not aslr"
# fuzz: True # fuzz: True
# cc: clang # cc: clang
@ -74,6 +76,7 @@ jobs:
# ldflags: -fsanitize=undefined # ldflags: -fsanitize=undefined
# # don't fail with alignment due to https://github.com/libtom/libtomcrypt/issues/549 # # don't fail with alignment due to https://github.com/libtom/libtomcrypt/issues/549
# extracflags: -fsanitize=undefined -fno-sanitize-recover=undefined -fsanitize-recover=alignment # extracflags: -fsanitize=undefined -fno-sanitize-recover=undefined -fsanitize-recover=alignment
# pytest_addopts: -k "not aslr"
# fuzz: True # fuzz: True
# cc: clang # cc: clang
@ -86,6 +89,10 @@ jobs:
# for fuzzing # for fuzzing
CXX: clang++ CXX: clang++
RANLIB: ${{ matrix.ranlib || 'ranlib' }} RANLIB: ${{ matrix.ranlib || 'ranlib' }}
# pytest in "make check" recognises this for extra arguments
PYTEST_ADDOPTS: ${{ matrix.pytest_addopts }}
# some pytests depend on special setup from this file. see authorized_keys below.
DBTEST_IN_ACTION: true
steps: steps:
- name: deps - name: deps
@ -128,7 +135,14 @@ jobs:
- name: keys - name: keys
run: | run: |
mkdir -p ~/.ssh mkdir -p ~/.ssh
# remove old files so we can rerun in-place with "act -r" during test development
rm -vf ~/.ssh/id_dropbear*
~/inst/bin/dropbearkey -t ecdsa -f ~/.ssh/id_dropbear | grep ^ecdsa > ~/.ssh/authorized_keys ~/inst/bin/dropbearkey -t ecdsa -f ~/.ssh/id_dropbear | grep ^ecdsa > ~/.ssh/authorized_keys
# to test setting SSH_PUBKEYINFO, replace the trailing comment
~/inst/bin/dropbearkey -t ecdsa -f ~/.ssh/id_dropbear_key2 | grep ^ecdsa | sed 's/[^ ]*$/key2 extra/' >> ~/.ssh/authorized_keys
~/inst/bin/dropbearkey -t ecdsa -f ~/.ssh/id_dropbear_key3 | grep ^ecdsa | sed 's/[^ ]*$/key3%char/' >> ~/.ssh/authorized_keys
~/inst/bin/dropbearkey -t ecdsa -f ~/.ssh/id_dropbear_key4 | grep ^ecdsa | sed 's/[^ ]*$/key4,char/' >> ~/.ssh/authorized_keys
chmod 700 ~ ~/.ssh ~/.ssh/authorized_keys chmod 700 ~ ~/.ssh ~/.ssh/authorized_keys
ls -ld ~ ~/.ssh ~/.ssh/authorized_keys ls -ld ~ ~/.ssh ~/.ssh/authorized_keys

1
auth.h
View File

@ -125,6 +125,7 @@ struct AuthState {
char *pw_passwd; char *pw_passwd;
#if DROPBEAR_SVR_PUBKEY_OPTIONS_BUILT #if DROPBEAR_SVR_PUBKEY_OPTIONS_BUILT
struct PubKeyOptions* pubkey_options; struct PubKeyOptions* pubkey_options;
char *pubkey_info;
#endif #endif
}; };

View File

@ -85,31 +85,32 @@ void recv_msg_userauth_banner() {
banner = buf_getstring(ses.payload, &bannerlen); banner = buf_getstring(ses.payload, &bannerlen);
buf_eatstring(ses.payload); /* The language string */ buf_eatstring(ses.payload); /* The language string */
if (bannerlen > MAX_BANNER_SIZE) { if (cli_opts.quiet == 0) {
TRACE(("recv_msg_userauth_banner: bannerlen too long: %d", bannerlen)) if (bannerlen > MAX_BANNER_SIZE) {
truncated = 1; TRACE(("recv_msg_userauth_banner: bannerlen too long: %d", bannerlen))
} else { truncated = 1;
cleantext(banner); } else {
cleantext(banner);
/* Limit to 24 lines */ /* Limit to 24 lines */
linecount = 1; linecount = 1;
for (i = 0; i < bannerlen; i++) { for (i = 0; i < bannerlen; i++) {
if (banner[i] == '\n') { if (banner[i] == '\n') {
if (linecount >= MAX_BANNER_LINES) { if (linecount >= MAX_BANNER_LINES) {
banner[i] = '\0'; banner[i] = '\0';
truncated = 1; truncated = 1;
break; break;
}
linecount++;
} }
linecount++;
} }
fprintf(stderr, "%s\n", banner);
} }
fprintf(stderr, "%s\n", banner);
}
if (truncated) { if (truncated) {
fprintf(stderr, "[Banner from the server is too long]\n"); fprintf(stderr, "[Banner from the server is too long]\n");
}
} }
m_free(banner); m_free(banner);
TRACE(("leave recv_msg_userauth_banner")) TRACE(("leave recv_msg_userauth_banner"))
} }

View File

@ -62,6 +62,7 @@ static void printhelp() {
"-T Don't allocate a pty\n" "-T Don't allocate a pty\n"
"-N Don't run a remote command\n" "-N Don't run a remote command\n"
"-f Run in background after auth\n" "-f Run in background after auth\n"
"-q quiet, don't show remote banner\n"
"-y Always accept remote host key if unknown\n" "-y Always accept remote host key if unknown\n"
"-y -y Don't perform any remote host key checking (caution)\n" "-y -y Don't perform any remote host key checking (caution)\n"
"-s Request a subsystem (use by external sftp)\n" "-s Request a subsystem (use by external sftp)\n"
@ -141,6 +142,7 @@ void cli_getopts(int argc, char ** argv) {
cli_opts.username = NULL; cli_opts.username = NULL;
cli_opts.cmd = NULL; cli_opts.cmd = NULL;
cli_opts.no_cmd = 0; cli_opts.no_cmd = 0;
cli_opts.quiet = 0;
cli_opts.backgrounded = 0; cli_opts.backgrounded = 0;
cli_opts.wantpty = 9; /* 9 means "it hasn't been touched", gets set later */ cli_opts.wantpty = 9; /* 9 means "it hasn't been touched", gets set later */
cli_opts.always_accept_key = 0; cli_opts.always_accept_key = 0;
@ -214,6 +216,9 @@ void cli_getopts(int argc, char ** argv) {
} }
cli_opts.always_accept_key = 1; cli_opts.always_accept_key = 1;
break; break;
case 'q': /* quiet */
cli_opts.quiet = 1;
break;
case 'p': /* remoteport */ case 'p': /* remoteport */
next = (char**)&cli_opts.remoteport; next = (char**)&cli_opts.remoteport;
break; break;
@ -540,6 +545,12 @@ multihop_passthrough_args() {
ret = m_malloc(len); ret = m_malloc(len);
total = 0; total = 0;
if (cli_opts.quiet)
{
int written = snprintf(ret+total, len-total, "-q ");
total += written;
}
if (cli_opts.no_hostkey_check) if (cli_opts.no_hostkey_check)
{ {
int written = snprintf(ret+total, len-total, "-y -y "); int written = snprintf(ret+total, len-total, "-y -y ");

View File

@ -92,7 +92,6 @@ typedef struct svr_runopts {
/* whether to print the MOTD */ /* whether to print the MOTD */
int domotd; int domotd;
#endif #endif
int norootlogin; int norootlogin;
#ifdef HAVE_GETGROUPLIST #ifdef HAVE_GETGROUPLIST
@ -155,6 +154,7 @@ typedef struct cli_runopts {
int always_accept_key; int always_accept_key;
int no_hostkey_check; int no_hostkey_check;
int no_cmd; int no_cmd;
int quiet;
int backgrounded; int backgrounded;
int is_subsystem; int is_subsystem;
#if DROPBEAR_CLI_PUBKEY_AUTH #if DROPBEAR_CLI_PUBKEY_AUTH

View File

@ -257,11 +257,15 @@ static void send_msg_userauth_pk_ok(const char* sigalgo, unsigned int sigalgolen
} }
/* Content for SSH_PUBKEYINFO is optionally returned malloced in ret_info (will be
freed if already set */
static int checkpubkey_line(buffer* line, int line_num, const char* filename, static int checkpubkey_line(buffer* line, int line_num, const char* filename,
const char* algo, unsigned int algolen, const char* algo, unsigned int algolen,
const unsigned char* keyblob, unsigned int keybloblen) { const unsigned char* keyblob, unsigned int keybloblen,
char ** ret_info) {
buffer *options_buf = NULL; buffer *options_buf = NULL;
unsigned int pos, len; char *info_str = NULL;
unsigned int pos, len, infopos, infolen;
int ret = DROPBEAR_FAILURE; int ret = DROPBEAR_FAILURE;
if (line->len < MIN_AUTHKEYS_LINE || line->len > MAX_AUTHKEYS_LINE) { if (line->len < MIN_AUTHKEYS_LINE || line->len > MAX_AUTHKEYS_LINE) {
@ -339,11 +343,36 @@ static int checkpubkey_line(buffer* line, int line_num, const char* filename,
goto out; goto out;
} }
/* truncate the line at the space after the base64 data */ /* find the length of base64 data */
pos = line->pos; pos = line->pos;
for (len = 0; line->pos < line->len; len++) { for (len = 0; line->pos < line->len; len++) {
if (buf_getbyte(line) == ' ') break; if (buf_getbyte(line) == ' ') {
} break;
}
}
/* find out the length of the public key info, stop at the first space */
infopos = line->pos;
for (infolen = 0; line->pos < line->len; infolen++) {
const char c = buf_getbyte(line);
if (c == ' ') {
break;
}
/* We have an allowlist - authorized_keys lines can't be fully trusted,
some shell scripts may do unsafe things with env var values */
if (!(isalnum(c) || strchr(".,_-+@", c))) {
TRACE(("Not setting SSH_PUBKEYINFO, special characters"))
infolen = 0;
break;
}
}
if (infolen > 0) {
info_str = m_malloc(infolen + 1);
buf_setpos(line, infopos);
strncpy(info_str, buf_getptr(line, infolen), infolen);
}
/* truncate to base64 data length */
buf_setpos(line, pos); buf_setpos(line, pos);
buf_setlen(line, line->pos + len); buf_setlen(line, line->pos + len);
@ -351,14 +380,30 @@ static int checkpubkey_line(buffer* line, int line_num, const char* filename,
ret = cmp_base64_key(keyblob, keybloblen, (const unsigned char *) algo, algolen, line, NULL); ret = cmp_base64_key(keyblob, keybloblen, (const unsigned char *) algo, algolen, line, NULL);
if (ret == DROPBEAR_SUCCESS && options_buf) { /* free pubkey_info if it is filled */
ret = svr_add_pubkey_options(options_buf, line_num, filename); if (ret_info && *ret_info) {
m_free(*ret_info);
*ret_info = NULL;
}
if (ret == DROPBEAR_SUCCESS) {
if (options_buf) {
ret = svr_add_pubkey_options(options_buf, line_num, filename);
}
if (ret_info) {
/* take the (optional) public key information */
*ret_info = info_str;
info_str = NULL;
}
} }
out: out:
if (options_buf) { if (options_buf) {
buf_free(options_buf); buf_free(options_buf);
} }
if (info_str) {
m_free(info_str);
}
return ret; return ret;
} }
@ -431,7 +476,8 @@ static int checkpubkey(const char* keyalgo, unsigned int keyalgolen,
} }
line_num++; line_num++;
ret = checkpubkey_line(line, line_num, filename, keyalgo, keyalgolen, keyblob, keybloblen); ret = checkpubkey_line(line, line_num, filename, keyalgo, keyalgolen,
keyblob, keybloblen, &ses.authstate.pubkey_info);
if (ret == DROPBEAR_SUCCESS) { if (ret == DROPBEAR_SUCCESS) {
break; break;
} }
@ -548,7 +594,7 @@ static int checkfileperm(char * filename) {
int fuzz_checkpubkey_line(buffer* line, int line_num, char* filename, int fuzz_checkpubkey_line(buffer* line, int line_num, char* filename,
const char* algo, unsigned int algolen, const char* algo, unsigned int algolen,
const unsigned char* keyblob, unsigned int keybloblen) { const unsigned char* keyblob, unsigned int keybloblen) {
return checkpubkey_line(line, line_num, filename, algo, algolen, keyblob, keybloblen); return checkpubkey_line(line, line_num, filename, algo, algolen, keyblob, keybloblen, NULL);
} }
#endif #endif

View File

@ -115,6 +115,9 @@ void svr_pubkey_options_cleanup() {
} }
m_free(ses.authstate.pubkey_options); m_free(ses.authstate.pubkey_options);
} }
if (ses.authstate.pubkey_info) {
m_free(ses.authstate.pubkey_info);
}
} }
/* helper for svr_add_pubkey_options. returns DROPBEAR_SUCCESS if the option is matched, /* helper for svr_add_pubkey_options. returns DROPBEAR_SUCCESS if the option is matched,

View File

@ -1030,6 +1030,9 @@ static void execchild(const void *user_data) {
if (chansess->original_command) { if (chansess->original_command) {
addnewvar("SSH_ORIGINAL_COMMAND", chansess->original_command); addnewvar("SSH_ORIGINAL_COMMAND", chansess->original_command);
} }
if (ses.authstate.pubkey_info != NULL) {
addnewvar("SSH_PUBKEYINFO", ses.authstate.pubkey_info);
}
/* change directory */ /* change directory */
if (chdir(ses.authstate.pw_dir) < 0) { if (chdir(ses.authstate.pw_dir) < 0) {

View File

@ -72,6 +72,10 @@ def own_venv_command():
return f"source {venv}/bin/activate" return f"source {venv}/bin/activate"
class HandleTcp(socketserver.ThreadingMixIn, socketserver.TCPServer): class HandleTcp(socketserver.ThreadingMixIn, socketserver.TCPServer):
# override TCPServer's default, avoids TIME_WAIT
allow_reuse_addr = True
""" Listens for a single incoming request, sends a response if given, """ Listens for a single incoming request, sends a response if given,
and returns the inbound data. and returns the inbound data.
Reponse can be a queue object, in which case each item in the queue will Reponse can be a queue object, in which case each item in the queue will

30
test/test_svrauth.py Normal file
View File

@ -0,0 +1,30 @@
from test_dropbear import *
import signal
import queue
import socket
import os
from pathlib import Path
# Tests for server side authentication
# Requires keyfile and authorized_keys set up in github action build.yml
@pytest.mark.skipif('DBTEST_IN_ACTION' not in os.environ, reason="DBTEST_IN_ACTION not set")
def test_pubkeyinfo(request, dropbear):
kf = str(Path.home() / ".ssh/id_dropbear_key2")
r = dbclient(request, "-i", kf, "echo -n $SSH_PUBKEYINFO", capture_output=True)
# stop at first space
assert r.stdout.decode() == "key2"
@pytest.mark.skipif('DBTEST_IN_ACTION' not in os.environ, reason="DBTEST_IN_ACTION not set")
def test_pubkeyinfo_special(request, dropbear):
kf = str(Path.home() / ".ssh/id_dropbear_key3")
r = dbclient(request, "-i", kf, "echo -n $SSH_PUBKEYINFO", capture_output=True)
# comment contains special characters so the SSH_PUBKEYINFO should not be set
assert r.stdout.decode() == ""
@pytest.mark.skipif('DBTEST_IN_ACTION' not in os.environ, reason="DBTEST_IN_ACTION not set")
def test_pubkeyinfo_okchar(request, dropbear):
kf = str(Path.home() / ".ssh/id_dropbear_key4")
r = dbclient(request, "-i", kf, "echo -n $SSH_PUBKEYINFO", capture_output=True)
# comment contains special characters so the SSH_PUBKEYINFO should not be set
assert r.stdout.decode() == "key4,char"