From c9e6118246fd0ded8adff97245bcef5df0616f87 Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Mon, 22 Oct 2018 20:32:58 +0100 Subject: [PATCH] Uppity: add challenge-response auth methods. This adds the server side of the SSH-2 keyboard-interactive protocol, and the pair of very similar SSH-1 methods AUTH_TIS and AUTH_CCARD (which basically differ only in message numbers, and each involve a single challenge from the server and a response from the user). --- ssh1login-server.c | 46 +++++++++++++++++++++++- ssh2userauth-server.c | 81 +++++++++++++++++++++++++++++++++++++++++++ sshserver.h | 28 +++++++++++++++ unix/uxserver.c | 64 +++++++++++++++++++++++++++++++++- 4 files changed, 217 insertions(+), 2 deletions(-) diff --git a/ssh1login-server.c b/ssh1login-server.c index 26f8f730..359dcf98 100644 --- a/ssh1login-server.c +++ b/ssh1login-server.c @@ -152,7 +152,10 @@ static void ssh1_login_server_process_queue(PacketProtocolLayer *ppl) s->supported_auths_mask |= (1U << SSH1_AUTH_PASSWORD); if (s->ap_methods & AUTHMETHOD_PUBLICKEY) s->supported_auths_mask |= (1U << SSH1_AUTH_RSA); - /* FIXME: TIS, CCARD */ + if (s->ap_methods & AUTHMETHOD_TIS) + s->supported_auths_mask |= (1U << SSH1_AUTH_TIS); + if (s->ap_methods & AUTHMETHOD_CRYPTOCARD) + s->supported_auths_mask |= (1U << SSH1_AUTH_CCARD); for (i = 0; i < 8; i++) s->cookie[i] = random_byte(); @@ -349,6 +352,47 @@ static void ssh1_login_server_process_queue(PacketProtocolLayer *ppl) } goto auth_success; + } else if (pktin->type == SSH1_CMSG_AUTH_TIS || + pktin->type == SSH1_CMSG_AUTH_CCARD) { + char *challenge; + unsigned response_type; + ptrlen response; + + s->current_method = (pktin->type == SSH1_CMSG_AUTH_TIS ? + AUTHMETHOD_TIS : AUTHMETHOD_CRYPTOCARD); + if (!(s->ap_methods & s->current_method)) + continue; + + challenge = auth_ssh1int_challenge( + s->authpolicy, s->current_method, s->username); + if (!challenge) + continue; + pktout = ssh_bpp_new_pktout( + s->ppl.bpp, + (s->current_method == AUTHMETHOD_TIS ? + SSH1_SMSG_AUTH_TIS_CHALLENGE : + SSH1_SMSG_AUTH_CCARD_CHALLENGE)); + put_stringz(pktout, challenge); + pq_push(s->ppl.out_pq, pktout); + sfree(challenge); + + crMaybeWaitUntilV((pktin = ssh1_login_server_pop(s)) != NULL); + response_type = (s->current_method == AUTHMETHOD_TIS ? + SSH1_CMSG_AUTH_TIS_RESPONSE : + SSH1_CMSG_AUTH_CCARD_RESPONSE); + if (pktin->type != response_type) { + ssh_proto_error(s->ppl.ssh, "Received unexpected packet in " + "response to %s challenge, type %d (%s)", + (s->current_method == AUTHMETHOD_TIS ? + "TIS" : "CryptoCard"), + pktin->type, ssh1_pkt_type(pktin->type)); + return; + } + + response = get_string(pktin); + + if (auth_ssh1int_response(s->authpolicy, response)) + goto auth_success; } } diff --git a/ssh2userauth-server.c b/ssh2userauth-server.c index 84f3d27c..e642e3de 100644 --- a/ssh2userauth-server.c +++ b/ssh2userauth-server.c @@ -29,6 +29,8 @@ struct ssh2_userauth_server_state { unsigned methods, this_method; int partial_success; + AuthKbdInt *aki; + PacketProtocolLayer ppl; }; @@ -46,6 +48,21 @@ static const struct PacketProtocolLayerVtable ssh2_userauth_server_vtable = { "ssh-userauth", }; +static void free_auth_kbdint(AuthKbdInt *aki) +{ + int i; + + if (!aki) + return; + + sfree(aki->title); + sfree(aki->instruction); + for (i = 0; i < aki->nprompts; i++) + sfree(aki->prompts[i].prompt); + sfree(aki->prompts); + sfree(aki); +} + PacketProtocolLayer *ssh2_userauth_server_new( PacketProtocolLayer *successor_layer, AuthPolicy *authpolicy) { @@ -76,6 +93,8 @@ static void ssh2_userauth_server_free(PacketProtocolLayer *ppl) if (s->successor_layer) ssh_ppl_free(s->successor_layer); + free_auth_kbdint(s->aki); + sfree(s); } @@ -211,6 +230,66 @@ static void ssh2_userauth_server_process_queue(PacketProtocolLayer *ppl) if (!success) goto failure; + } else if (ptrlen_eq_string(s->method, "keyboard-interactive")) { + int i, ok; + unsigned n; + + s->this_method = AUTHMETHOD_KBDINT; + if (!(s->methods & s->this_method)) + goto failure; + + do { + s->aki = auth_kbdint_prompts(s->authpolicy, s->username); + if (!s->aki) + goto failure; + + pktout = ssh_bpp_new_pktout( + s->ppl.bpp, SSH2_MSG_USERAUTH_INFO_REQUEST); + put_stringz(pktout, s->aki->title); + put_stringz(pktout, s->aki->instruction); + put_stringz(pktout, ""); /* language tag */ + put_uint32(pktout, s->aki->nprompts); + for (i = 0; i < s->aki->nprompts; i++) { + put_stringz(pktout, s->aki->prompts[i].prompt); + put_bool(pktout, s->aki->prompts[i].echo); + } + pq_push(s->ppl.out_pq, pktout); + + crMaybeWaitUntilV( + (pktin = ssh2_userauth_server_pop(s)) != NULL); + if (pktin->type != SSH2_MSG_USERAUTH_INFO_RESPONSE) { + ssh_proto_error( + s->ppl.ssh, "Received unexpected packet when " + "expecting USERAUTH_INFO_RESPONSE, type %d (%s)", + pktin->type, + ssh2_pkt_type(s->ppl.bpp->pls->kctx, + s->ppl.bpp->pls->actx, pktin->type)); + return; + } + + n = get_uint32(pktin); + if (n != s->aki->nprompts) { + ssh_proto_error( + s->ppl.ssh, "Received %u keyboard-interactive " + "responses after sending %u prompts", + n, s->aki->nprompts); + return; + } + + { + ptrlen *responses = snewn(s->aki->nprompts, ptrlen); + for (i = 0; i < s->aki->nprompts; i++) + responses[i] = get_string(pktin); + ok = auth_kbdint_responses(s->authpolicy, responses); + sfree(responses); + } + + free_auth_kbdint(s->aki); + s->aki = NULL; + } while (ok == 0); + + if (ok <= 0) + goto failure; } else { goto failure; } @@ -246,6 +325,8 @@ static void ssh2_userauth_server_process_queue(PacketProtocolLayer *ppl) add_to_commasep(list, "password"); if (s->methods & AUTHMETHOD_PUBLICKEY) add_to_commasep(list, "publickey"); + if (s->methods & AUTHMETHOD_KBDINT) + add_to_commasep(list, "keyboard-interactive"); put_stringsb(pktout, list); } put_bool(pktout, s->partial_success); diff --git a/sshserver.h b/sshserver.h index fa0ee87e..7081d0cd 100644 --- a/sshserver.h +++ b/sshserver.h @@ -13,6 +13,9 @@ void platform_logevent(const char *msg); X(NONE) \ X(PASSWORD) \ X(PUBLICKEY) \ + X(KBDINT) \ + X(TIS) \ + X(CRYPTOCARD) \ /* end of list */ #define AUTHMETHOD_BIT_INDEX(name) AUTHMETHOD_BIT_INDEX_##name, @@ -21,6 +24,18 @@ enum { AUTHMETHODS(AUTHMETHOD_BIT_INDEX) AUTHMETHOD_BIT_INDEX_dummy }; AUTHMETHOD_##name = 1 << AUTHMETHOD_BIT_INDEX_##name, enum { AUTHMETHODS(AUTHMETHOD_BIT_VALUE) AUTHMETHOD_BIT_VALUE_dummy }; +typedef struct AuthKbdInt AuthKbdInt; +typedef struct AuthKbdIntPrompt AuthKbdIntPrompt; +struct AuthKbdInt { + char *title, *instruction; /* both need freeing */ + int nprompts; + AuthKbdIntPrompt *prompts; /* the array itself needs freeing */ +}; +struct AuthKbdIntPrompt { + char *prompt; /* needs freeing */ + int echo; +}; + unsigned auth_methods(AuthPolicy *); int auth_none(AuthPolicy *, ptrlen username); int auth_password(AuthPolicy *, ptrlen username, ptrlen password); @@ -28,6 +43,19 @@ int auth_publickey(AuthPolicy *, ptrlen username, ptrlen public_blob); /* auth_publickey_ssh1 must return the whole public key given the modulus, * because the SSH-1 client never transmits the exponent over the wire. * The key remains owned by the AuthPolicy. */ + +AuthKbdInt *auth_kbdint_prompts(AuthPolicy *, ptrlen username); +/* auth_kbdint_prompts returns NULL to trigger auth failure */ +int auth_kbdint_responses(AuthPolicy *, const ptrlen *responses); +/* auth_kbdint_responses returns >0 for success, <0 for failure, and 0 + * to indicate that we haven't decided yet and further prompts are + * coming */ + +/* The very similar SSH-1 TIS and CryptoCard methods are combined into + * a single API for AuthPolicy, which takes a method argument */ +char *auth_ssh1int_challenge(AuthPolicy *, unsigned method, ptrlen username); +int auth_ssh1int_response(AuthPolicy *, ptrlen response); + struct RSAKey *auth_publickey_ssh1( AuthPolicy *ap, ptrlen username, Bignum rsa_modulus); /* auth_successful returns FALSE if further authentication is needed */ diff --git a/unix/uxserver.c b/unix/uxserver.c index 2e68ca89..bbb26ef1 100644 --- a/unix/uxserver.c +++ b/unix/uxserver.c @@ -164,10 +164,12 @@ struct AuthPolicy_ssh2_pubkey { struct AuthPolicy { struct AuthPolicy_ssh1_pubkey *ssh1keys; struct AuthPolicy_ssh2_pubkey *ssh2keys; + int kbdint_state; }; unsigned auth_methods(AuthPolicy *ap) { - return AUTHMETHOD_PUBLICKEY | AUTHMETHOD_PASSWORD; + return (AUTHMETHOD_PUBLICKEY | AUTHMETHOD_PASSWORD | AUTHMETHOD_KBDINT | + AUTHMETHOD_TIS | AUTHMETHOD_CRYPTOCARD); } int auth_none(AuthPolicy *ap, ptrlen username) { @@ -196,6 +198,65 @@ struct RSAKey *auth_publickey_ssh1( } return NULL; } +AuthKbdInt *auth_kbdint_prompts(AuthPolicy *ap, ptrlen username) +{ + AuthKbdInt *aki = snew(AuthKbdInt); + + switch (ap->kbdint_state) { + case 0: + 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: + 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")); +} +int auth_ssh1int_response(AuthPolicy *ap, ptrlen response) +{ + return ptrlen_eq_string(response, "otter"); +} int auth_successful(AuthPolicy *ap, ptrlen username, unsigned method) { return TRUE; @@ -280,6 +341,7 @@ int main(int argc, char **argv) Conf *conf = conf_new(); load_open_settings(NULL, conf); + ap.kbdint_state = 0; ap.ssh1keys = NULL; ap.ssh2keys = NULL;