mirror of
https://git.tartarus.org/simon/putty.git
synced 2025-01-09 01:18:00 +00:00
98200d1bfe
DIT, for 'Data-Independent Timing', is a bit you can set in the processor state on sufficiently new Arm CPUs, which promises that a long list of instructions will deliberately avoid varying their timing based on the input register values. Just what you want for keeping your constant-time crypto primitives constant-time. As far as I'm aware, no CPU has _yet_ implemented any data-dependent optimisations, so DIT is a safety precaution against them doing so in future. It would be embarrassing to be caught without it if a future CPU does do that, so we now turn on DIT in the PuTTY process state. I've put a call to the new enable_dit() function at the start of every main() and WinMain() belonging to a program that might do cryptography (even testcrypt, in case someone uses it for something!), and in case I missed one there, also added a second call at the first moment that any cryptography-using part of the code looks as if it might become active: when an instance of the SSH protocol object is configured, when the system PRNG is initialised, and when selecting any cryptographic authentication protocol in an HTTP or SOCKS proxy connection. With any luck those precautions between them should ensure it's on whenever we need it. Arm's own recommendation is that you should carefully choose the granularity at which you enable and disable DIT: there's a potential time cost to turning it on and off (I'm not sure what, but plausibly something of the order of a pipeline flush), so it's a performance hit to do it _inside_ each individual crypto function, but if CPUs start supporting significant data-dependent optimisation in future, then it will also become a noticeable performance hit to just leave it on across the whole process. So you'd like to do it somewhere in the middle: for example, you might turn on DIT once around the whole process of verifying and decrypting an SSH packet, instead of once for decryption and once for MAC. With all respect to that recommendation as a strategy for maximum performance, I'm not following it here. I turn on DIT at the start of the PuTTY process, and then leave it on. Rationale: 1. PuTTY is not otherwise a performance-critical application: it's not likely to max out your CPU for any purpose _other_ than cryptography. The most CPU-intensive non-cryptographic thing I can imagine a PuTTY process doing is the complicated computation of font rendering in the terminal, and that will normally be cached (you don't recompute each glyph from its outline and hints for every time you display it). 2. I think a bigger risk lies in accidental side channels from having DIT turned off when it should have been on. I can imagine lots of causes for that. Missing a crypto operation in some unswept corner of the code; confusing control flow (like my coroutine macros) jumping with DIT clear into the middle of a region of code that expected DIT to have been set at the beginning; having a reference counter of DIT requests and getting it out of sync. In a more sophisticated programming language, it might be possible to avoid the risk in #2 by cleverness with the type system. For example, in Rust, you could have a zero-sized type that acts as a proof token for DIT being enabled (it would be constructed by a function that also sets DIT, have a Drop implementation that clears DIT, and be !Send so you couldn't use it in a thread other than the one where DIT was set), and then you could require all the actual crypto functions to take a DitToken as an extra parameter, at zero runtime cost. Then "oops I forgot to set DIT around this piece of crypto" would become a compile error. Even so, you'd have to take some care with coroutine-structured code (what happens if a Rust async function yields while holding a DIT token?) and with nesting (if you have two DIT tokens, you don't want dropping the inner one to clear DIT while the outer one is still there to wrongly convince callees that it's set). Maybe in Rust you could get this all to work reliably. But not in C! DIT is an optional feature of the Arm architecture, so we must first test to see if it's supported. This is done the same way as we already do for the various Arm crypto accelerators: on ELF-based systems, check the appropriate bit in the 'hwcap' words in the ELF aux vector; on Mac, look for an appropriate sysctl flag. On Windows I don't know of a way to query the DIT feature, _or_ of a way to write the necessary enabling instruction in an MSVC-compatible way. I've _heard_ that it might not be necessary, because Windows might just turn on DIT unconditionally and leave it on, in an even more extreme version of my own strategy. I don't have a source for that - I heard it by word of mouth - but I _hope_ it's true, because that would suit me very well! Certainly I can't write code to enable DIT without knowing (a) how to do it, (b) how to know if it's safe. Nonetheless, I've put the enable_dit() call in all the right places in the Windows main programs as well as the Unix and cross-platform code, so that if I later find out that I _can_ put in an explicit enable of DIT in some way, I'll only have to arrange to set HAVE_ARM_DIT and compile the enable_dit() function appropriately.
974 lines
32 KiB
C
974 lines
32 KiB
C
/*
|
|
* SSH server for Unix: main program.
|
|
*
|
|
* ======================================================================
|
|
*
|
|
* This server is NOT SECURE!
|
|
*
|
|
* DO NOT DEPLOY IT IN A HOSTILE-FACING ENVIRONMENT!
|
|
*
|
|
* Its purpose is to speak the server end of everything PuTTY speaks
|
|
* on the client side, so that I can test that I haven't broken PuTTY
|
|
* when I reorganise its code, even things like RSA key exchange or
|
|
* chained auth methods which it's hard to find a server that speaks
|
|
* at all.
|
|
*
|
|
* It has no interaction with the OS's authentication system: the
|
|
* authentications it will accept are configurable by command-line
|
|
* option, and once you authenticate, it will run the connection
|
|
* protocol - including all subprocesses and shells - under the same
|
|
* Unix user id you started it under.
|
|
*
|
|
* It really is only suitable for testing the actual SSH protocol.
|
|
* Don't use it for anything more serious!
|
|
*
|
|
* ======================================================================
|
|
*/
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <errno.h>
|
|
#include <assert.h>
|
|
#include <stdarg.h>
|
|
#include <signal.h>
|
|
#include <unistd.h>
|
|
#include <fcntl.h>
|
|
#include <termios.h>
|
|
#include <pwd.h>
|
|
#include <sys/ioctl.h>
|
|
#include <sys/time.h>
|
|
|
|
#include "putty.h"
|
|
#include "mpint.h"
|
|
#include "ssh.h"
|
|
#include "ssh/server.h"
|
|
|
|
void modalfatalbox(const char *p, ...)
|
|
{
|
|
va_list ap;
|
|
fprintf(stderr, "FATAL ERROR: ");
|
|
va_start(ap, p);
|
|
vfprintf(stderr, p, ap);
|
|
va_end(ap);
|
|
fputc('\n', stderr);
|
|
exit(1);
|
|
}
|
|
void nonfatal(const char *p, ...)
|
|
{
|
|
va_list ap;
|
|
fprintf(stderr, "ERROR: ");
|
|
va_start(ap, p);
|
|
vfprintf(stderr, p, ap);
|
|
va_end(ap);
|
|
fputc('\n', stderr);
|
|
}
|
|
|
|
char *platform_default_s(const char *name)
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
bool platform_default_b(const char *name, bool def)
|
|
{
|
|
return def;
|
|
}
|
|
|
|
int platform_default_i(const char *name, int def)
|
|
{
|
|
return def;
|
|
}
|
|
|
|
FontSpec *platform_default_fontspec(const char *name)
|
|
{
|
|
return fontspec_new_default();
|
|
}
|
|
|
|
Filename *platform_default_filename(const char *name)
|
|
{
|
|
return filename_from_str("");
|
|
}
|
|
|
|
char *x_get_default(const char *key)
|
|
{
|
|
return NULL; /* this is a stub */
|
|
}
|
|
|
|
void old_keyfile_warning(void) { }
|
|
|
|
void timer_change_notify(unsigned long next)
|
|
{
|
|
}
|
|
|
|
char *platform_get_x_display(void) { return NULL; }
|
|
|
|
void make_unix_sftp_filehandle_key(void *data, size_t size)
|
|
{
|
|
random_read(data, size);
|
|
}
|
|
|
|
static bool verbose;
|
|
|
|
struct server_config;
|
|
|
|
struct AuthPolicyShared {
|
|
struct AuthPolicy_ssh1_pubkey *ssh1keys;
|
|
struct AuthPolicy_ssh2_pubkey *ssh2keys;
|
|
};
|
|
|
|
struct AuthPolicy {
|
|
struct AuthPolicyShared *shared;
|
|
int kbdint_state;
|
|
};
|
|
|
|
struct server_instance {
|
|
unsigned id;
|
|
AuthPolicy ap;
|
|
LogPolicy logpolicy;
|
|
struct server_config *cfg;
|
|
};
|
|
|
|
struct server_config {
|
|
unsigned config_id;
|
|
|
|
Conf *conf;
|
|
const SshServerConfig *ssc;
|
|
|
|
ssh_key **hostkeys;
|
|
int nhostkeys;
|
|
|
|
RSAKey *hostkey1;
|
|
|
|
unsigned auth_methods;
|
|
|
|
struct AuthPolicyShared *ap_shared;
|
|
|
|
Socket *listening_socket;
|
|
Plug listening_plug;
|
|
};
|
|
|
|
static unsigned next_id = 0;
|
|
|
|
static void log_to_stderr(unsigned id, const char *msg)
|
|
{
|
|
if (id != (unsigned)-1)
|
|
fprintf(stderr, "conn#%u: ", id);
|
|
fputs(msg, stderr);
|
|
fputc('\n', stderr);
|
|
fflush(stderr);
|
|
}
|
|
|
|
static void server_eventlog(LogPolicy *lp, const char *event)
|
|
{
|
|
struct server_instance *inst = container_of(
|
|
lp, struct server_instance, logpolicy);
|
|
if (verbose)
|
|
log_to_stderr(inst->id, event);
|
|
}
|
|
|
|
static void server_logging_error(LogPolicy *lp, const char *event)
|
|
{
|
|
struct server_instance *inst = container_of(
|
|
lp, struct server_instance, logpolicy);
|
|
log_to_stderr(inst->id, event); /* unconditional */
|
|
}
|
|
|
|
static int server_askappend(
|
|
LogPolicy *lp, Filename *filename,
|
|
void (*callback)(void *ctx, int result), void *ctx)
|
|
{
|
|
return 2; /* always overwrite (FIXME: could make this a cmdline option) */
|
|
}
|
|
|
|
static const LogPolicyVtable server_logpolicy_vt = {
|
|
.eventlog = server_eventlog,
|
|
.askappend = server_askappend,
|
|
.logging_error = server_logging_error,
|
|
.verbose = null_lp_verbose_no,
|
|
};
|
|
|
|
struct AuthPolicy_ssh1_pubkey {
|
|
RSAKey key;
|
|
struct AuthPolicy_ssh1_pubkey *next;
|
|
};
|
|
struct AuthPolicy_ssh2_pubkey {
|
|
ptrlen public_blob;
|
|
struct AuthPolicy_ssh2_pubkey *next;
|
|
};
|
|
|
|
unsigned auth_methods(AuthPolicy *ap)
|
|
{
|
|
struct server_instance *inst = container_of(
|
|
ap, struct server_instance, ap);
|
|
return inst->cfg->auth_methods;
|
|
}
|
|
bool auth_none(AuthPolicy *ap, ptrlen username)
|
|
{
|
|
struct server_instance *inst = container_of(
|
|
ap, struct server_instance, ap);
|
|
if (inst->cfg->auth_methods & AUTHMETHOD_NONE)
|
|
return true;
|
|
return false;
|
|
}
|
|
int auth_password(AuthPolicy *ap, ptrlen username, ptrlen password,
|
|
ptrlen *new_password_opt)
|
|
{
|
|
const char *PHONY_GOOD_PASSWORD = "weasel";
|
|
const char *PHONY_BAD_PASSWORD = "ferret";
|
|
|
|
if (!new_password_opt) {
|
|
/* Accept login with our preconfigured good password */
|
|
if (ptrlen_eq_string(password, PHONY_GOOD_PASSWORD))
|
|
return 1;
|
|
/* Don't outright reject the bad password, but insist on a change */
|
|
if (ptrlen_eq_string(password, PHONY_BAD_PASSWORD))
|
|
return 2;
|
|
/* Reject anything else */
|
|
return 0;
|
|
} else {
|
|
/* In a password-change request, expect the bad password as input */
|
|
if (!ptrlen_eq_string(password, PHONY_BAD_PASSWORD))
|
|
return 0;
|
|
/* Accept a request to change it to the good password */
|
|
if (ptrlen_eq_string(*new_password_opt, PHONY_GOOD_PASSWORD))
|
|
return 1;
|
|
/* Outright reject a request to change it to the same password
|
|
* as it already 'was' */
|
|
if (ptrlen_eq_string(*new_password_opt, PHONY_BAD_PASSWORD))
|
|
return 0;
|
|
/* Anything else, pretend the new pw wasn't good enough, and
|
|
* re-request a change */
|
|
return 2;
|
|
}
|
|
}
|
|
bool auth_publickey(AuthPolicy *ap, ptrlen username, ptrlen public_blob)
|
|
{
|
|
struct AuthPolicy_ssh2_pubkey *iter;
|
|
for (iter = ap->shared->ssh2keys; iter; iter = iter->next) {
|
|
if (ptrlen_eq_ptrlen(public_blob, iter->public_blob))
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
RSAKey *auth_publickey_ssh1(
|
|
AuthPolicy *ap, ptrlen username, mp_int *rsa_modulus)
|
|
{
|
|
struct AuthPolicy_ssh1_pubkey *iter;
|
|
for (iter = ap->shared->ssh1keys; iter; iter = iter->next) {
|
|
if (mp_cmp_eq(rsa_modulus, iter->key.modulus))
|
|
return &iter->key;
|
|
}
|
|
return NULL;
|
|
}
|
|
AuthKbdInt *auth_kbdint_prompts(AuthPolicy *ap, ptrlen username)
|
|
{
|
|
AuthKbdInt *aki;
|
|
|
|
switch (ap->kbdint_state) {
|
|
case 0:
|
|
aki = snew(AuthKbdInt);
|
|
aki->title = dupstr("Initial double prompt");
|
|
aki->instruction =
|
|
dupstr("First prompt should echo, second should not");
|
|
aki->nprompts = 2;
|
|
aki->prompts = snewn(aki->nprompts, AuthKbdIntPrompt);
|
|
aki->prompts[0].prompt = dupstr("Echoey prompt: ");
|
|
aki->prompts[0].echo = true;
|
|
aki->prompts[1].prompt = dupstr("Silent prompt: ");
|
|
aki->prompts[1].echo = false;
|
|
return aki;
|
|
case 1: {
|
|
struct server_instance *inst = container_of(
|
|
ap, struct server_instance, ap);
|
|
aki = snew(AuthKbdInt);
|
|
if (inst->cfg->ssc->stunt_allow_trivial_ki_auth) {
|
|
aki->title = dupstr("");
|
|
aki->instruction = dupstr("");
|
|
} else {
|
|
aki->title = dupstr("Zero-prompt step");
|
|
aki->instruction = dupstr("Shouldn't see any prompts this time");
|
|
}
|
|
aki->nprompts = 0;
|
|
aki->prompts = NULL;
|
|
return aki;
|
|
}
|
|
default:
|
|
ap->kbdint_state = 0;
|
|
return NULL;
|
|
}
|
|
}
|
|
int auth_kbdint_responses(AuthPolicy *ap, const ptrlen *responses)
|
|
{
|
|
switch (ap->kbdint_state) {
|
|
case 0:
|
|
if (ptrlen_eq_string(responses[0], "stoat") &&
|
|
ptrlen_eq_string(responses[1], "weasel")) {
|
|
ap->kbdint_state++;
|
|
return 0; /* those are the expected responses */
|
|
} else {
|
|
ap->kbdint_state = 0;
|
|
return -1;
|
|
}
|
|
break;
|
|
case 1:
|
|
return +1; /* succeed after the zero-prompt step */
|
|
default:
|
|
ap->kbdint_state = 0;
|
|
return -1;
|
|
}
|
|
}
|
|
char *auth_ssh1int_challenge(AuthPolicy *ap, unsigned method, ptrlen username)
|
|
{
|
|
/* FIXME: test returning a challenge string without \n, and ensure
|
|
* it gets printed as a prompt in its own right, without PuTTY
|
|
* making up a "Response: " prompt to follow it */
|
|
return dupprintf("This is a dummy %s challenge!\n",
|
|
(method == AUTHMETHOD_TIS ? "TIS" : "CryptoCard"));
|
|
}
|
|
bool auth_ssh1int_response(AuthPolicy *ap, ptrlen response)
|
|
{
|
|
return ptrlen_eq_string(response, "otter");
|
|
}
|
|
bool auth_successful(AuthPolicy *ap, ptrlen username, unsigned method)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
static void safety_warning(FILE *fp)
|
|
{
|
|
fputs(" =================================================\n"
|
|
" THIS SSH SERVER IS NOT WRITTEN TO BE SECURE!\n"
|
|
" DO NOT DEPLOY IT IN A HOSTILE-FACING ENVIRONMENT!\n"
|
|
" =================================================\n", fp);
|
|
}
|
|
|
|
static void show_help(FILE *fp)
|
|
{
|
|
safety_warning(fp);
|
|
fputs("\n"
|
|
"usage: uppity [options] [--and <options>...]\n"
|
|
"options: --listen [PORT|PATH] listen to a port on localhost, or Unix socket\n"
|
|
" --listen-once (with --listen) stop after one "
|
|
"connection\n"
|
|
" --hostkey KEY SSH host key (need at least one)\n"
|
|
" --rsakexkey KEY key for SSH-2 RSA key exchange "
|
|
"(in SSH-1 format)\n"
|
|
" --userkey KEY public key"
|
|
" acceptable for user authentication\n"
|
|
" --sessiondir DIR cwd for session subprocess (default $HOME)\n"
|
|
" --bannertext TEXT send TEXT as SSH-2 auth banner\n"
|
|
" --bannerfile FILE send contents of FILE as SSH-2 auth "
|
|
"banner\n"
|
|
" --kexinit-kex STR override list of SSH-2 KEX methods\n"
|
|
" --kexinit-hostkey STR override list of SSH-2 host key "
|
|
"types\n"
|
|
" --kexinit-cscipher STR override list of SSH-2 "
|
|
"client->server ciphers\n"
|
|
" --kexinit-sccipher STR override list of SSH-2 "
|
|
"server->client ciphers\n"
|
|
" --kexinit-csmac STR override list of SSH-2 "
|
|
"client->server MACs\n"
|
|
" --kexinit-scmac STR override list of SSH-2 "
|
|
"server->client MACs\n"
|
|
" --kexinit-cscomp STR override list of SSH-2 "
|
|
"c->s compression types\n"
|
|
" --kexinit-sccomp STR override list of SSH-2 "
|
|
"s->c compression types\n"
|
|
" --ssh1-ciphers STR override list of SSH-1 ciphers\n"
|
|
" --ssh1-no-compression forbid compression in SSH-1\n"
|
|
" --deny-auth METHOD forbid a userauth method\n"
|
|
" --allow-auth METHOD allow a userauth method\n"
|
|
" (METHOD = none/password/publickey/kbdint/tis/"
|
|
"cryptocard)\n"
|
|
" --exitsignum send buggy numeric \"exit-signal\" "
|
|
"message\n"
|
|
" --verbose print event log messages to standard "
|
|
"error\n"
|
|
" --sshlog FILE write SSH packet log to FILE\n"
|
|
" --sshrawlog FILE write SSH packets + raw data log"
|
|
" to FILE\n"
|
|
" --and run a separate server on another port\n"
|
|
"also: uppity --help show this text\n"
|
|
" uppity --version show version information\n"
|
|
"\n", fp);
|
|
safety_warning(fp);
|
|
}
|
|
|
|
static void show_version_and_exit(void)
|
|
{
|
|
char *buildinfo_text = buildinfo("\n");
|
|
printf("%s: %s\n%s\n", appname, ver, buildinfo_text);
|
|
sfree(buildinfo_text);
|
|
exit(0);
|
|
}
|
|
|
|
const bool buildinfo_gtk_relevant = false;
|
|
|
|
static bool listening = false, listen_once = false;
|
|
static bool finished = false;
|
|
void server_instance_terminated(LogPolicy *lp)
|
|
{
|
|
struct server_instance *inst = container_of(
|
|
lp, struct server_instance, logpolicy);
|
|
|
|
if (listening && !listen_once) {
|
|
log_to_stderr(inst->id, "connection terminated");
|
|
} else {
|
|
finished = true;
|
|
}
|
|
|
|
sfree(inst);
|
|
}
|
|
|
|
static bool longoptarg(const char *arg, const char *expected,
|
|
const char **val, int *argcp, char ***argvp)
|
|
{
|
|
int len = strlen(expected);
|
|
if (memcmp(arg, expected, len))
|
|
return false;
|
|
if (arg[len] == '=') {
|
|
*val = arg + len + 1;
|
|
return true;
|
|
} else if (arg[len] == '\0') {
|
|
if (--*argcp > 0) {
|
|
*val = *++*argvp;
|
|
return true;
|
|
} else {
|
|
fprintf(stderr, "%s: option %s expects an argument\n",
|
|
appname, expected);
|
|
exit(1);
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static bool longoptnoarg(const char *arg, const char *expected)
|
|
{
|
|
int len = strlen(expected);
|
|
if (memcmp(arg, expected, len))
|
|
return false;
|
|
if (arg[len] == '=') {
|
|
fprintf(stderr, "%s: option %s expects no argument\n",
|
|
appname, expected);
|
|
exit(1);
|
|
} else if (arg[len] == '\0') {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static Plug *server_conn_plug(
|
|
struct server_config *cfg, struct server_instance **inst_out)
|
|
{
|
|
struct server_instance *inst = snew(struct server_instance);
|
|
|
|
memset(inst, 0, sizeof(*inst));
|
|
|
|
inst->id = next_id++;
|
|
inst->ap.shared = cfg->ap_shared;
|
|
if (cfg->ssc->stunt_allow_trivial_ki_auth)
|
|
inst->ap.kbdint_state = 1;
|
|
inst->logpolicy.vt = &server_logpolicy_vt;
|
|
inst->cfg = cfg;
|
|
|
|
if (inst_out)
|
|
*inst_out = inst;
|
|
|
|
return ssh_server_plug(
|
|
cfg->conf, cfg->ssc, cfg->hostkeys, cfg->nhostkeys, cfg->hostkey1,
|
|
&inst->ap, &inst->logpolicy, &unix_live_sftpserver_vt);
|
|
}
|
|
|
|
static void server_log(Plug *plug, Socket *s, PlugLogType type, SockAddr *addr,
|
|
int port, const char *error_msg, int error_code)
|
|
{
|
|
log_to_stderr((unsigned)-1, error_msg);
|
|
}
|
|
|
|
static void server_closing(Plug *plug, PlugCloseType type,
|
|
const char *error_msg)
|
|
{
|
|
if (type != PLUGCLOSE_NORMAL)
|
|
log_to_stderr((unsigned)-1, error_msg);
|
|
}
|
|
|
|
static int server_accepting(Plug *p, accept_fn_t constructor, accept_ctx_t ctx)
|
|
{
|
|
struct server_config *cfg = container_of(
|
|
p, struct server_config, listening_plug);
|
|
Socket *s;
|
|
const char *err;
|
|
|
|
struct server_instance *inst;
|
|
|
|
if (listen_once) {
|
|
if (!cfg->listening_socket) /* in case of rapid double-accept */
|
|
return 1;
|
|
sk_close(cfg->listening_socket);
|
|
cfg->listening_socket = NULL;
|
|
}
|
|
|
|
unsigned old_next_id = next_id;
|
|
|
|
Plug *plug = server_conn_plug(cfg, &inst);
|
|
s = constructor(ctx, plug);
|
|
if ((err = sk_socket_error(s)) != NULL)
|
|
return 1;
|
|
|
|
SocketEndpointInfo *pi = sk_peer_info(s);
|
|
|
|
if (pi->addressfamily != ADDRTYPE_LOCAL && !sk_peer_trusted(s)) {
|
|
fprintf(stderr, "rejected connection to serv#%u "
|
|
"from %s (untrustworthy peer)\n",
|
|
cfg->config_id, pi->log_text);
|
|
sk_free_endpoint_info(pi);
|
|
sk_close(s);
|
|
next_id = old_next_id;
|
|
return 1;
|
|
}
|
|
|
|
char *msg = dupprintf("new connection to serv#%u from %s",
|
|
cfg->config_id, pi->log_text);
|
|
log_to_stderr(inst->id, msg);
|
|
sfree(msg);
|
|
sk_free_endpoint_info(pi);
|
|
|
|
sk_set_frozen(s, false);
|
|
ssh_server_start(plug, s);
|
|
return 0;
|
|
}
|
|
|
|
static const PlugVtable server_plugvt = {
|
|
.log = server_log,
|
|
.closing = server_closing,
|
|
.accepting = server_accepting,
|
|
};
|
|
|
|
static unsigned auth_method_from_name(const char *name)
|
|
{
|
|
if (!strcmp(name, "none"))
|
|
return AUTHMETHOD_NONE;
|
|
if (!strcmp(name, "tis"))
|
|
return AUTHMETHOD_TIS;
|
|
if (!strcmp(name, "cryptocard") || !strcmp(name, "ccard"))
|
|
return AUTHMETHOD_CRYPTOCARD;
|
|
if (!strcmp(name, "keyboard-interactive") || !strcmp(name, "k-i") ||
|
|
!strcmp(name, "kbdint") || !strcmp(name, "ki"))
|
|
return AUTHMETHOD_KBDINT;
|
|
if (!strcmp(name, "publickey") || !strcmp(name, "pubkey") ||
|
|
!strcmp(name, "pk"))
|
|
return AUTHMETHOD_PUBLICKEY;
|
|
if (!strcmp(name, "password") || !strcmp(name, "pw"))
|
|
return AUTHMETHOD_PASSWORD;
|
|
return 0;
|
|
}
|
|
|
|
struct cmdline_instance {
|
|
int listen_port;
|
|
const char *listen_socket;
|
|
ssh_key **hostkeys;
|
|
size_t nhostkeys, hostkeysize;
|
|
RSAKey *hostkey1;
|
|
unsigned auth_methods;
|
|
struct AuthPolicyShared aps;
|
|
SshServerConfig ssc;
|
|
Conf *conf;
|
|
};
|
|
|
|
static void init_cmdline_instance(struct cmdline_instance *ci)
|
|
{
|
|
ci->listen_port = -1;
|
|
ci->listen_socket = NULL;
|
|
|
|
ci->hostkeys = NULL;
|
|
ci->nhostkeys = ci->hostkeysize = 0;
|
|
ci->hostkey1 = NULL;
|
|
|
|
ci->conf = make_ssh_server_conf();
|
|
|
|
ci->aps.ssh1keys = NULL;
|
|
ci->aps.ssh2keys = NULL;
|
|
|
|
memset(&ci->ssc, 0, sizeof(ci->ssc));
|
|
|
|
ci->ssc.application_name = "Uppity";
|
|
ci->ssc.session_starting_dir = getenv("HOME");
|
|
ci->ssc.ssh1_cipher_mask = SSH1_SUPPORTED_CIPHER_MASK;
|
|
ci->ssc.ssh1_allow_compression = true;
|
|
|
|
ci->auth_methods = (AUTHMETHOD_PUBLICKEY | AUTHMETHOD_PASSWORD |
|
|
AUTHMETHOD_KBDINT | AUTHMETHOD_TIS |
|
|
AUTHMETHOD_CRYPTOCARD);
|
|
}
|
|
|
|
static void cmdline_instance_start(struct cmdline_instance *ci)
|
|
{
|
|
static unsigned next_server_config_id = 0;
|
|
|
|
struct server_config *scfg = snew(struct server_config);
|
|
scfg->config_id = next_server_config_id++;
|
|
scfg->conf = ci->conf;
|
|
scfg->ssc = &ci->ssc;
|
|
scfg->hostkeys = ci->hostkeys;
|
|
scfg->nhostkeys = ci->nhostkeys;
|
|
scfg->hostkey1 = ci->hostkey1;
|
|
scfg->ap_shared = &ci->aps;
|
|
scfg->auth_methods = ci->auth_methods;
|
|
|
|
if (ci->listen_port >= 0 || ci->listen_socket) {
|
|
listening = true;
|
|
scfg->listening_plug.vt = &server_plugvt;
|
|
char *msg;
|
|
if (ci->listen_port >= 0) {
|
|
scfg->listening_socket = sk_newlistener(
|
|
NULL, ci->listen_port, &scfg->listening_plug, true,
|
|
ADDRTYPE_UNSPEC);
|
|
msg = dupprintf("serv#%u: listening on port %d",
|
|
scfg->config_id, ci->listen_port);
|
|
} else {
|
|
SockAddr *addr = unix_sock_addr(ci->listen_socket);
|
|
scfg->listening_socket = new_unix_listener(
|
|
addr, &scfg->listening_plug);
|
|
msg = dupprintf("serv#%u: listening on Unix socket %s",
|
|
scfg->config_id, ci->listen_socket);
|
|
}
|
|
|
|
log_to_stderr(-1, msg);
|
|
sfree(msg);
|
|
} else {
|
|
struct server_instance *inst;
|
|
Plug *plug = server_conn_plug(scfg, &inst);
|
|
ssh_server_start(plug, make_fd_socket(0, 1, -1, NULL, 0, plug));
|
|
log_to_stderr(inst->id, "speaking SSH on stdio");
|
|
}
|
|
}
|
|
|
|
int main(int argc, char **argv)
|
|
{
|
|
size_t ninstances = 0, instancessize = 8;
|
|
struct cmdline_instance *instances = snewn(
|
|
instancessize, struct cmdline_instance);
|
|
struct cmdline_instance *ci = &instances[ninstances++];
|
|
init_cmdline_instance(ci);
|
|
|
|
enable_dit();
|
|
|
|
if (argc <= 1) {
|
|
/*
|
|
* We're going to terminate with an error message below,
|
|
* because there are no host keys. But we'll display the help
|
|
* as additional standard-error output, if nothing else so
|
|
* that people see the giant safety warning.
|
|
*/
|
|
show_help(stderr);
|
|
fputc('\n', stderr);
|
|
}
|
|
|
|
while (--argc > 0) {
|
|
const char *arg = *++argv;
|
|
const char *val;
|
|
|
|
if (!strcmp(arg, "--help")) {
|
|
show_help(stdout);
|
|
exit(0);
|
|
} else if (longoptnoarg(arg, "--version")) {
|
|
show_version_and_exit();
|
|
} else if (longoptnoarg(arg, "--verbose") || !strcmp(arg, "-v")) {
|
|
verbose = true;
|
|
} else if (longoptnoarg(arg, "--and")) {
|
|
sgrowarray(instances, instancessize, ninstances);
|
|
ci = &instances[ninstances++];
|
|
init_cmdline_instance(ci);
|
|
} else if (longoptarg(arg, "--listen", &val, &argc, &argv)) {
|
|
if (val[0] == '/') {
|
|
ci->listen_port = -1;
|
|
ci->listen_socket = val;
|
|
} else {
|
|
ci->listen_port = atoi(val);
|
|
ci->listen_socket = NULL;
|
|
}
|
|
} else if (!strcmp(arg, "--listen-once")) {
|
|
listen_once = true;
|
|
} else if (longoptarg(arg, "--hostkey", &val, &argc, &argv)) {
|
|
Filename *keyfile;
|
|
int keytype;
|
|
const char *error;
|
|
|
|
keyfile = filename_from_str(val);
|
|
keytype = key_type(keyfile);
|
|
|
|
if (keytype == SSH_KEYTYPE_SSH2) {
|
|
ssh2_userkey *uk;
|
|
ssh_key *key;
|
|
uk = ppk_load_f(keyfile, NULL, &error);
|
|
filename_free(keyfile);
|
|
if (!uk || !uk->key) {
|
|
fprintf(stderr, "%s: unable to load host key '%s': "
|
|
"%s\n", appname, val, error);
|
|
exit(1);
|
|
}
|
|
char *invalid = ssh_key_invalid(uk->key, 0);
|
|
if (invalid) {
|
|
fprintf(stderr, "%s: host key '%s' is unusable: "
|
|
"%s\n", appname, val, invalid);
|
|
exit(1);
|
|
}
|
|
key = uk->key;
|
|
sfree(uk->comment);
|
|
sfree(uk);
|
|
|
|
for (int i = 0; i < ci->nhostkeys; i++)
|
|
if (ssh_key_alg(ci->hostkeys[i]) == ssh_key_alg(key)) {
|
|
fprintf(stderr, "%s: host key '%s' duplicates key "
|
|
"type %s\n", appname, val,
|
|
ssh_key_alg(key)->ssh_id);
|
|
exit(1);
|
|
}
|
|
|
|
sgrowarray(ci->hostkeys, ci->hostkeysize, ci->nhostkeys);
|
|
ci->hostkeys[ci->nhostkeys++] = key;
|
|
} else if (keytype == SSH_KEYTYPE_SSH1) {
|
|
if (ci->hostkey1) {
|
|
fprintf(stderr, "%s: host key '%s' is a redundant "
|
|
"SSH-1 host key\n", appname, val);
|
|
exit(1);
|
|
}
|
|
ci->hostkey1 = snew(RSAKey);
|
|
if (!rsa1_load_f(keyfile, ci->hostkey1, NULL, &error)) {
|
|
fprintf(stderr, "%s: unable to load host key '%s': "
|
|
"%s\n", appname, val, error);
|
|
exit(1);
|
|
}
|
|
} else {
|
|
fprintf(stderr, "%s: '%s' is not loadable as a "
|
|
"private key (%s)", appname, val,
|
|
key_type_to_str(keytype));
|
|
exit(1);
|
|
}
|
|
} else if (longoptarg(arg, "--rsakexkey", &val, &argc, &argv)) {
|
|
Filename *keyfile;
|
|
int keytype;
|
|
const char *error;
|
|
|
|
keyfile = filename_from_str(val);
|
|
keytype = key_type(keyfile);
|
|
|
|
if (keytype != SSH_KEYTYPE_SSH1) {
|
|
fprintf(stderr, "%s: '%s' is not loadable as an SSH-1 format "
|
|
"private key (%s)", appname, val,
|
|
key_type_to_str(keytype));
|
|
exit(1);
|
|
}
|
|
|
|
if (ci->ssc.rsa_kex_key) {
|
|
freersakey(ci->ssc.rsa_kex_key);
|
|
} else {
|
|
ci->ssc.rsa_kex_key = snew(RSAKey);
|
|
}
|
|
|
|
if (!rsa1_load_f(keyfile, ci->ssc.rsa_kex_key, NULL, &error)) {
|
|
fprintf(stderr, "%s: unable to load RSA kex key '%s': "
|
|
"%s\n", appname, val, error);
|
|
exit(1);
|
|
}
|
|
|
|
ci->ssc.rsa_kex_key->sshk.vt = &ssh_rsa;
|
|
} else if (longoptarg(arg, "--userkey", &val, &argc, &argv)) {
|
|
Filename *keyfile;
|
|
int keytype;
|
|
const char *error;
|
|
|
|
keyfile = filename_from_str(val);
|
|
keytype = key_type(keyfile);
|
|
|
|
if (keytype == SSH_KEYTYPE_SSH2_PUBLIC_RFC4716 ||
|
|
keytype == SSH_KEYTYPE_SSH2_PUBLIC_OPENSSH) {
|
|
strbuf *sb = strbuf_new();
|
|
struct AuthPolicy_ssh2_pubkey *node;
|
|
void *blob;
|
|
|
|
if (!ppk_loadpub_f(keyfile, NULL, BinarySink_UPCAST(sb),
|
|
NULL, &error)) {
|
|
fprintf(stderr, "%s: unable to load user key '%s': "
|
|
"%s\n", appname, val, error);
|
|
exit(1);
|
|
}
|
|
|
|
node = snew_plus(struct AuthPolicy_ssh2_pubkey, sb->len);
|
|
blob = snew_plus_get_aux(node);
|
|
memcpy(blob, sb->u, sb->len);
|
|
node->public_blob = make_ptrlen(blob, sb->len);
|
|
|
|
node->next = ci->aps.ssh2keys;
|
|
ci->aps.ssh2keys = node;
|
|
|
|
strbuf_free(sb);
|
|
} else if (keytype == SSH_KEYTYPE_SSH1_PUBLIC) {
|
|
strbuf *sb = strbuf_new();
|
|
BinarySource src[1];
|
|
struct AuthPolicy_ssh1_pubkey *node;
|
|
|
|
if (!rsa1_loadpub_f(keyfile, BinarySink_UPCAST(sb),
|
|
NULL, &error)) {
|
|
fprintf(stderr, "%s: unable to load user key '%s': "
|
|
"%s\n", appname, val, error);
|
|
exit(1);
|
|
}
|
|
|
|
node = snew(struct AuthPolicy_ssh1_pubkey);
|
|
BinarySource_BARE_INIT(src, sb->u, sb->len);
|
|
get_rsa_ssh1_pub(src, &node->key, RSA_SSH1_EXPONENT_FIRST);
|
|
|
|
node->next = ci->aps.ssh1keys;
|
|
ci->aps.ssh1keys = node;
|
|
|
|
strbuf_free(sb);
|
|
} else {
|
|
fprintf(stderr, "%s: '%s' is not loadable as a public key "
|
|
"(%s)\n", appname, val, key_type_to_str(keytype));
|
|
exit(1);
|
|
}
|
|
} else if (longoptarg(arg, "--bannerfile", &val, &argc, &argv)) {
|
|
FILE *fp = fopen(val, "r");
|
|
if (!fp) {
|
|
fprintf(stderr, "%s: %s: open: %s\n", appname,
|
|
val, strerror(errno));
|
|
exit(1);
|
|
}
|
|
strbuf *sb = strbuf_new();
|
|
if (!read_file_into(BinarySink_UPCAST(sb), fp)) {
|
|
fprintf(stderr, "%s: %s: read: %s\n", appname,
|
|
val, strerror(errno));
|
|
exit(1);
|
|
}
|
|
fclose(fp);
|
|
ci->ssc.banner = ptrlen_from_strbuf(sb);
|
|
} else if (longoptarg(arg, "--bannertext", &val, &argc, &argv)) {
|
|
ci->ssc.banner = ptrlen_from_asciz(val);
|
|
} else if (longoptarg(arg, "--sessiondir", &val, &argc, &argv)) {
|
|
ci->ssc.session_starting_dir = val;
|
|
} else if (longoptarg(arg, "--kexinit-kex", &val, &argc, &argv)) {
|
|
ci->ssc.kex_override[KEXLIST_KEX] = ptrlen_from_asciz(val);
|
|
} else if (longoptarg(arg, "--kexinit-hostkey", &val, &argc, &argv)) {
|
|
ci->ssc.kex_override[KEXLIST_HOSTKEY] = ptrlen_from_asciz(val);
|
|
} else if (longoptarg(arg, "--kexinit-cscipher", &val, &argc, &argv)) {
|
|
ci->ssc.kex_override[KEXLIST_CSCIPHER] = ptrlen_from_asciz(val);
|
|
} else if (longoptarg(arg, "--kexinit-csmac", &val, &argc, &argv)) {
|
|
ci->ssc.kex_override[KEXLIST_CSMAC] = ptrlen_from_asciz(val);
|
|
} else if (longoptarg(arg, "--kexinit-cscomp", &val, &argc, &argv)) {
|
|
ci->ssc.kex_override[KEXLIST_CSCOMP] = ptrlen_from_asciz(val);
|
|
} else if (longoptarg(arg, "--kexinit-sccipher", &val, &argc, &argv)) {
|
|
ci->ssc.kex_override[KEXLIST_SCCIPHER] = ptrlen_from_asciz(val);
|
|
} else if (longoptarg(arg, "--kexinit-scmac", &val, &argc, &argv)) {
|
|
ci->ssc.kex_override[KEXLIST_SCMAC] = ptrlen_from_asciz(val);
|
|
} else if (longoptarg(arg, "--kexinit-sccomp", &val, &argc, &argv)) {
|
|
ci->ssc.kex_override[KEXLIST_SCCOMP] = ptrlen_from_asciz(val);
|
|
} else if (longoptarg(arg, "--allow-auth", &val, &argc, &argv)) {
|
|
unsigned method = auth_method_from_name(val);
|
|
if (!method) {
|
|
fprintf(stderr, "%s: unrecognised auth method '%s'\n",
|
|
appname, val);
|
|
exit(1);
|
|
}
|
|
ci->auth_methods |= method;
|
|
} else if (longoptarg(arg, "--deny-auth", &val, &argc, &argv)) {
|
|
unsigned method = auth_method_from_name(val);
|
|
if (!method) {
|
|
fprintf(stderr, "%s: unrecognised auth method '%s'\n",
|
|
appname, val);
|
|
exit(1);
|
|
}
|
|
ci->auth_methods &= ~method;
|
|
} else if (longoptarg(arg, "--ssh1-ciphers", &val, &argc, &argv)) {
|
|
ptrlen list = ptrlen_from_asciz(val);
|
|
ptrlen word;
|
|
unsigned long mask = 0;
|
|
while (word = ptrlen_get_word(&list, ","), word.len != 0) {
|
|
|
|
#define SSH1_CIPHER_CASE(bitpos, name) \
|
|
if (ptrlen_eq_string(word, name)) { \
|
|
mask |= 1U << bitpos; \
|
|
continue; \
|
|
}
|
|
SSH1_SUPPORTED_CIPHER_LIST(SSH1_CIPHER_CASE);
|
|
#undef SSH1_CIPHER_CASE
|
|
|
|
fprintf(stderr, "%s: unrecognised SSH-1 cipher '%.*s'\n",
|
|
appname, PTRLEN_PRINTF(word));
|
|
exit(1);
|
|
}
|
|
ci->ssc.ssh1_cipher_mask = mask;
|
|
} else if (longoptnoarg(arg, "--ssh1-no-compression")) {
|
|
ci->ssc.ssh1_allow_compression = false;
|
|
} else if (longoptnoarg(arg, "--exitsignum")) {
|
|
ci->ssc.exit_signal_numeric = true;
|
|
} else if (longoptarg(arg, "--sshlog", &val, &argc, &argv) ||
|
|
longoptarg(arg, "-sshlog", &val, &argc, &argv)) {
|
|
Filename *logfile = filename_from_str(val);
|
|
conf_set_filename(ci->conf, CONF_logfilename, logfile);
|
|
filename_free(logfile);
|
|
conf_set_int(ci->conf, CONF_logtype, LGTYP_PACKETS);
|
|
conf_set_int(ci->conf, CONF_logxfovr, LGXF_OVR);
|
|
} else if (longoptarg(arg, "--sshrawlog", &val, &argc, &argv) ||
|
|
longoptarg(arg, "-sshrawlog", &val, &argc, &argv)) {
|
|
Filename *logfile = filename_from_str(val);
|
|
conf_set_filename(ci->conf, CONF_logfilename, logfile);
|
|
filename_free(logfile);
|
|
conf_set_int(ci->conf, CONF_logtype, LGTYP_SSHRAW);
|
|
conf_set_int(ci->conf, CONF_logxfovr, LGXF_OVR);
|
|
} else if (!strcmp(arg, "--pretend-to-accept-any-pubkey")) {
|
|
ci->ssc.stunt_pretend_to_accept_any_pubkey = true;
|
|
} else if (!strcmp(arg, "--open-unconditional-agent-socket")) {
|
|
ci->ssc.stunt_open_unconditional_agent_socket = true;
|
|
} else if (!strcmp(arg, "--allow-none-auth")) {
|
|
/* backwards-compatibility synonym for --allow-auth=none */
|
|
ci->auth_methods |= AUTHMETHOD_NONE;
|
|
} else if (!strcmp(arg, "--allow-trivial-ki-auth")) {
|
|
ci->ssc.stunt_allow_trivial_ki_auth = true;
|
|
} else if (!strcmp(arg, "--return-success-to-pubkey-offer")) {
|
|
ci->ssc.stunt_return_success_to_pubkey_offer = true;
|
|
} else if (!strcmp(arg, "--close-after-banner")) {
|
|
ci->ssc.stunt_close_after_banner = true;
|
|
} else {
|
|
fprintf(stderr, "%s: unrecognised option '%s'\n", appname, arg);
|
|
exit(1);
|
|
}
|
|
}
|
|
|
|
if (ninstances > 1 && listen_once) {
|
|
fprintf(stderr, "%s: cannot listen once only with multiple server "
|
|
"instances\n", appname);
|
|
exit(1);
|
|
}
|
|
for (size_t i = 0; i < ninstances; i++) {
|
|
ci = &instances[i];
|
|
if (ci->nhostkeys == 0 && !ci->hostkey1) {
|
|
fprintf(stderr, "%s: specify at least one host key\n", appname);
|
|
exit(1);
|
|
}
|
|
if (ninstances > 1 && !(ci->listen_port >= 0 || ci->listen_socket)) {
|
|
fprintf(stderr, "%s: cannot talk to stdio with multiple server "
|
|
"instances\n", appname);
|
|
exit(1);
|
|
}
|
|
}
|
|
|
|
random_ref();
|
|
|
|
/*
|
|
* Block SIGPIPE, so that we'll get EPIPE individually on
|
|
* particular network connections that go wrong.
|
|
*/
|
|
putty_signal(SIGPIPE, SIG_IGN);
|
|
|
|
sk_init();
|
|
uxsel_init();
|
|
|
|
for (size_t i = 0; i < ninstances; i++)
|
|
cmdline_instance_start(&instances[i]);
|
|
|
|
cli_main_loop(cliloop_no_pw_setup, cliloop_no_pw_check,
|
|
cliloop_always_continue, NULL);
|
|
|
|
return 0;
|
|
}
|