1
0
mirror of https://git.tartarus.org/simon/putty.git synced 2025-01-09 17:38:00 +00:00
putty-source/unix/psusan.c
Simon Tatham 98200d1bfe Arm: turn on PSTATE.DIT if available and needed.
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.
2024-12-19 08:52:47 +00:00

424 lines
12 KiB
C

/*
* 'psusan': Pseudo Ssh for Untappable, Separately Authenticated Networks
*
* This is a standalone application that speaks on its standard I/O
* (or a listening Unix-domain socket) the server end of the bare
* ssh-connection protocol used by PuTTY's connection sharing.
*
* The idea of this tool is that you can use it to communicate across
* any 8-bit-clean data channel between two inconveniently separated
* domains, provided the channel is already (as the name suggests)
* adequately secured against eavesdropping and modification and
* already authenticated as the right user.
*
* If you're sitting at one end of such a channel and want to type
* commands into the other end, the most obvious thing to do is to run
* a terminal session directly over it. But if you run psusan at one
* end, and a PuTTY (or compatible) client at the other end, then you
* not only get a single terminal session: you get all the other SSH
* amenities, like the ability to spawn extra terminal sessions,
* forward ports or X11 connections, even forward an SSH agent.
*
* There are a surprising number of channels of that kind; see the man
* page for some examples.
*/
#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 *vdata, size_t size)
{
/* psusan runs without a random number generator, so we can't make
* this up by random_read. Fortunately, psusan is also
* non-adversarial, so it's safe to generate this trivially. */
unsigned char *data = (unsigned char *)vdata;
for (size_t i = 0; i < size; i++)
data[i] = (unsigned)rand() / ((unsigned)RAND_MAX / 256);
}
static bool verbose;
struct server_instance {
unsigned id;
LogPolicy logpolicy;
};
static void log_to_stderr(unsigned id, const char *msg)
{
if (!verbose)
return;
if (id != (unsigned)-1)
fprintf(stderr, "#%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,
};
static void show_help(FILE *fp)
{
fputs("usage: psusan [options]\n"
"options: --listen SOCKETPATH listen for connections on a Unix-domain socket\n"
" --listen-once (with --listen) stop after one connection\n"
" --verbose print log messages to standard error\n"
" --sessiondir DIR cwd for session subprocess (default $HOME)\n"
" --sshlog FILE write ssh-connection packet log to FILE\n"
" --sshrawlog FILE write packets and raw data log to FILE\n"
"also: psusan --help show this text\n"
" psusan --version show version information\n", 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);
}
bool psusan_continue(void *ctx, bool fd, bool cb)
{
return !finished;
}
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;
}
struct server_config {
Conf *conf;
const SshServerConfig *ssc;
unsigned next_id;
Socket *listening_socket;
Plug listening_plug;
};
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 = cfg->next_id++;
inst->logpolicy.vt = &server_logpolicy_vt;
if (inst_out)
*inst_out = inst;
return ssh_server_plug(
cfg->conf, cfg->ssc, NULL, 0, NULL, NULL,
&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(-1, error_msg);
}
static void server_closing(Plug *plug, PlugCloseType type,
const char *error_msg)
{
if (type != PLUGCLOSE_NORMAL)
log_to_stderr(-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;
}
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);
char *msg = dupprintf("new connection from %s", 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,
};
unsigned auth_methods(AuthPolicy *ap)
{ return 0; }
bool auth_none(AuthPolicy *ap, ptrlen username)
{ return false; }
int auth_password(AuthPolicy *ap, ptrlen username, ptrlen password,
ptrlen *new_password_opt)
{ return 0; }
bool auth_publickey(AuthPolicy *ap, ptrlen username, ptrlen public_blob)
{ return false; }
RSAKey *auth_publickey_ssh1(
AuthPolicy *ap, ptrlen username, mp_int *rsa_modulus)
{ return NULL; }
AuthKbdInt *auth_kbdint_prompts(AuthPolicy *ap, ptrlen username)
{ return NULL; }
int auth_kbdint_responses(AuthPolicy *ap, const ptrlen *responses)
{ return -1; }
char *auth_ssh1int_challenge(AuthPolicy *ap, unsigned method, ptrlen username)
{ return NULL; }
bool auth_ssh1int_response(AuthPolicy *ap, ptrlen response)
{ return false; }
bool auth_successful(AuthPolicy *ap, ptrlen username, unsigned method)
{ return false; }
int main(int argc, char **argv)
{
const char *listen_socket = NULL;
SshServerConfig ssc;
Conf *conf = make_ssh_server_conf();
enable_dit();
memset(&ssc, 0, sizeof(ssc));
ssc.application_name = "PSUSAN";
ssc.session_starting_dir = getenv("HOME");
ssc.bare_connection = true;
while (--argc > 0) {
const char *arg = *++argv;
const char *val;
if (longoptnoarg(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 (longoptarg(arg, "--sessiondir", &val, &argc, &argv)) {
ssc.session_starting_dir = val;
} else if (longoptarg(arg, "--sshlog", &val, &argc, &argv) ||
longoptarg(arg, "-sshlog", &val, &argc, &argv)) {
Filename *logfile = filename_from_str(val);
conf_set_filename(conf, CONF_logfilename, logfile);
filename_free(logfile);
conf_set_int(conf, CONF_logtype, LGTYP_PACKETS);
conf_set_int(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(conf, CONF_logfilename, logfile);
filename_free(logfile);
conf_set_int(conf, CONF_logtype, LGTYP_SSHRAW);
conf_set_int(conf, CONF_logxfovr, LGXF_OVR);
} else if (longoptarg(arg, "--listen", &val, &argc, &argv)) {
listen_socket = val;
} else if (!strcmp(arg, "--listen-once")) {
listen_once = true;
} else {
fprintf(stderr, "%s: unrecognised option '%s'\n", appname, arg);
exit(1);
}
}
sk_init();
uxsel_init();
struct server_config scfg;
scfg.conf = conf;
scfg.ssc = &ssc;
scfg.next_id = 0;
if (listen_socket) {
listening = true;
scfg.listening_plug.vt = &server_plugvt;
SockAddr *addr = unix_sock_addr(listen_socket);
scfg.listening_socket = new_unix_listener(addr, &scfg.listening_plug);
char *msg = dupprintf("listening on Unix socket %s", 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, "running directly on stdio");
}
cli_main_loop(cliloop_no_pw_setup, cliloop_no_pw_check,
psusan_continue, NULL);
return 0;
}