| /* |
| * libwebsockets - small server side websockets and web server implementation |
| * |
| * Copyright (C) 2010 - 2019 Andy Green <andy@warmcat.com> |
| * |
| * Permission is hereby granted, free of charge, to any person obtaining a copy |
| * of this software and associated documentation files (the "Software"), to |
| * deal in the Software without restriction, including without limitation the |
| * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or |
| * sell copies of the Software, and to permit persons to whom the Software is |
| * furnished to do so, subject to the following conditions: |
| * |
| * The above copyright notice and this permission notice shall be included in |
| * all copies or substantial portions of the Software. |
| * |
| * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING |
| * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS |
| * IN THE SOFTWARE. |
| */ |
| |
| #include "libwebsockets.h" |
| #include "lws-ssh.h" |
| |
| #include <string.h> |
| #include <stdlib.h> |
| |
| void *sshd_zalloc(size_t s) |
| { |
| void *p = malloc(s); |
| |
| if (p) |
| memset(p, 0, s); |
| |
| return p; |
| } |
| |
| uint32_t |
| lws_g32(uint8_t **p) |
| { |
| uint32_t v = 0; |
| |
| v = (v << 8) | *((*p)++); |
| v = (v << 8) | *((*p)++); |
| v = (v << 8) | *((*p)++); |
| v = (v << 8) | *((*p)++); |
| |
| return v; |
| } |
| |
| uint32_t |
| lws_p32(uint8_t *p, uint32_t v) |
| { |
| *p++ = (uint8_t)(v >> 24); |
| *p++ = (uint8_t)(v >> 16); |
| *p++ = (uint8_t)(v >> 8); |
| *p++ = (uint8_t)v; |
| |
| return v; |
| } |
| |
| int |
| lws_cstr(uint8_t **p, const char *s, uint32_t max) |
| { |
| uint32_t n = (uint32_t)strlen(s); |
| |
| if (n > max) |
| return 1; |
| |
| lws_p32(*p, n); |
| *p += 4; |
| strcpy((char *)(*p), s); |
| *p += n; |
| |
| return 0; |
| } |
| |
| int |
| lws_buf(uint8_t **p, void *s, uint32_t len) |
| { |
| lws_p32(*p, len); |
| *p += 4; |
| memcpy((char *)(*p), s, len); |
| *p += len; |
| |
| return 0; |
| } |
| |
| void |
| write_task(struct per_session_data__sshd *pss, struct lws_ssh_channel *ch, |
| int task) |
| { |
| pss->write_task[pss->wt_head] = (uint8_t)task; |
| pss->write_channel[pss->wt_head] = ch; |
| pss->wt_head = (pss->wt_head + 1) & 7; |
| lws_callback_on_writable(pss->wsi); |
| } |
| |
| void |
| write_task_insert(struct per_session_data__sshd *pss, struct lws_ssh_channel *ch, |
| int task) |
| { |
| pss->wt_tail = (pss->wt_tail - 1) & 7; |
| pss->write_task[pss->wt_tail] = (uint8_t)task; |
| pss->write_channel[pss->wt_tail] = ch; |
| lws_callback_on_writable(pss->wsi); |
| } |
| |
| |
| void |
| lws_pad_set_length(struct per_session_data__sshd *pss, void *start, uint8_t **p, |
| struct lws_ssh_keys *keys) |
| { |
| uint32_t len = (uint32_t)lws_ptr_diff(*p, start); |
| uint8_t padc = 4, *bs = start; |
| |
| if (keys->full_length) |
| len -= 4; |
| |
| if ((len + padc) & (uint32_t)(keys->padding_alignment - 1)) |
| padc = (uint8_t)((uint8_t)padc + (uint8_t)(keys->padding_alignment - |
| ((len + padc) & (uint32_t)(keys->padding_alignment - 1)))); |
| |
| bs[4] = padc; |
| len += padc; |
| |
| if (!keys->valid) /* no crypto = pad with 00 */ |
| while (padc--) |
| *((*p)++) = 0; |
| else { /* crypto active = pad with random */ |
| lws_get_random(pss->vhd->context, *p, padc); |
| (*p) += padc; |
| } |
| if (keys->full_length) |
| len += 4; |
| |
| lws_p32(start, len - 4); |
| } |
| |
| static uint32_t |
| offer(struct per_session_data__sshd *pss, uint8_t *p, uint32_t len, int first, |
| int *payload_len) |
| { |
| uint8_t *op = p, *lp, *end = p + len - 1; |
| int n, padc = 4, keylen; |
| char keyt[32]; |
| uint8_t keybuf[256]; |
| |
| keylen = (int)get_gen_server_key_25519(pss, keybuf, (int)sizeof(keybuf)); |
| if (!keylen) { |
| lwsl_notice("get_gen_server_key failed\n"); |
| return 1; |
| } |
| lwsl_info("keylen %d\n", keylen); |
| n = ed25519_key_parse(keybuf, (unsigned int)keylen, |
| keyt, sizeof(keyt), NULL, NULL); |
| if (n) { |
| lwsl_notice("unable to parse server key: %d\n", n); |
| return 1; |
| } |
| |
| /* |
| * byte SSH_MSG_KEXINIT |
| * byte[16] cookie (random bytes) |
| * name-list kex_algorithms |
| * name-list server_host_key_algorithms |
| * name-list encryption_algorithms_client_to_server |
| * name-list encryption_algorithms_server_to_client |
| * name-list mac_algorithms_client_to_server |
| * name-list mac_algorithms_server_to_client |
| * name-list compression_algorithms_client_to_server |
| * name-list compression_algorithms_server_to_client |
| * name-list langua->es_client_to_server |
| * name-list langua->es_server_to_client |
| * boolean first_kex_packet_follows |
| * uint32 0 (reserved for future extension) |
| */ |
| |
| p += 5; /* msg len + padding */ |
| |
| *p++ = SSH_MSG_KEXINIT; |
| lws_get_random(pss->vhd->context, p, 16); |
| p += 16; |
| |
| /* KEX algorithms */ |
| |
| lp = p; |
| p += 4; |
| n = lws_snprintf((char *)p, lws_ptr_diff_size_t(end, p), "curve25519-sha256@libssh.org"); |
| p += lws_p32(lp, (uint32_t)n); |
| |
| /* Server Host Key Algorithms */ |
| |
| lp = p; |
| p += 4; |
| n = lws_snprintf((char *)p, lws_ptr_diff_size_t(end, p), "%s", keyt); |
| p += lws_p32(lp, (uint32_t)n); |
| |
| /* Encryption Algorithms: C -> S */ |
| |
| lp = p; |
| p += 4; |
| // n = lws_snprintf((char *)p, end - p, "aes256-gcm@openssh.com"); |
| n = lws_snprintf((char *)p, lws_ptr_diff_size_t(end, p), "chacha20-poly1305@openssh.com"); |
| p += lws_p32(lp, (uint32_t)n); |
| |
| /* Encryption Algorithms: S -> C */ |
| |
| lp = p; |
| p += 4; |
| // n = lws_snprintf((char *)p, end - p, "aes256-gcm@openssh.com"); |
| n = lws_snprintf((char *)p, lws_ptr_diff_size_t(end, p), "chacha20-poly1305@openssh.com"); |
| p += lws_p32(lp, (uint32_t)n); |
| |
| /* MAC Algorithms: C -> S */ |
| |
| lp = p; |
| p += 4; |
| /* bogus: chacha20 does not use MACs, but 'none' is not offered */ |
| n = lws_snprintf((char *)p, lws_ptr_diff_size_t(end, p), "hmac-sha2-256"); |
| p += lws_p32(lp, (uint32_t)n); |
| |
| /* MAC Algorithms: S -> C */ |
| |
| lp = p; |
| p += 4; |
| /* bogus: chacha20 does not use MACs, but 'none' is not offered */ |
| n = lws_snprintf((char *)p, lws_ptr_diff_size_t(end, p), "hmac-sha2-256"); |
| p += lws_p32(lp, (uint32_t)n); |
| |
| /* Compression Algorithms: C -> S */ |
| |
| lp = p; |
| p += 4; |
| n = lws_snprintf((char *)p, lws_ptr_diff_size_t(end, p), "none"); |
| p += lws_p32(lp, (uint32_t)n); |
| |
| /* Compression Algorithms: S -> C */ |
| |
| lp = p; |
| p += 4; |
| n = lws_snprintf((char *)p, lws_ptr_diff_size_t(end, p), "none"); |
| p += lws_p32(lp, (uint32_t)n); |
| |
| if (p - op < 13 + padc + 8) |
| return 0; |
| |
| /* Languages: C -> S */ |
| |
| *p++ = 0; |
| *p++ = 0; |
| *p++ = 0; |
| *p++ = 0; |
| |
| /* Languages: S -> C */ |
| |
| *p++ = 0; |
| *p++ = 0; |
| *p++ = 0; |
| *p++ = 0; |
| |
| /* First KEX packet coming */ |
| |
| *p++ = !!first; |
| |
| /* Reserved */ |
| |
| *p++ = 0; |
| *p++ = 0; |
| *p++ = 0; |
| *p++ = 0; |
| |
| len = (uint32_t)lws_ptr_diff(p, op); |
| if (payload_len) |
| /* starts at buf + 5 and excludes padding */ |
| *payload_len = (int)(len - 5); |
| |
| /* we must give at least 4 bytes of 00 padding */ |
| |
| if (((int)len + padc) & 7) |
| padc += 8 - (((int)len + padc) & 7); |
| |
| op[4] = (uint8_t)padc; |
| len += (uint32_t)padc; |
| |
| while (padc--) |
| *p++ = 0; |
| |
| /* recorded length does not include the uint32_t len itself */ |
| lws_p32(op, len - 4); |
| |
| return len; |
| } |
| |
| static int |
| handle_name(struct per_session_data__sshd *pss) |
| { |
| struct lws_kex *kex = pss->kex; |
| char keyt[32]; |
| uint8_t keybuf[256]; |
| int n = 0, len; |
| |
| switch (pss->parser_state) { |
| case SSH_KEX_NL_KEX_ALGS: |
| if (!strcmp(pss->name, "curve25519-sha256@libssh.org")) |
| kex->match_bitfield |= 1; |
| break; |
| case SSH_KEX_NL_SHK_ALGS: |
| len = (int)get_gen_server_key_25519(pss, keybuf, (int)sizeof(keybuf)); |
| if (!len) |
| break; |
| if (ed25519_key_parse(keybuf, (unsigned int)len, |
| keyt, sizeof(keyt), |
| NULL, NULL)) { |
| lwsl_err("Unable to parse host key %d\n", n); |
| } else { |
| if (!strcmp(pss->name, keyt)) { |
| kex->match_bitfield |= 2; |
| break; |
| } |
| } |
| break; |
| case SSH_KEX_NL_EACTS_ALGS: |
| if (!strcmp(pss->name, "chacha20-poly1305@openssh.com")) |
| kex->match_bitfield |= 4; |
| break; |
| case SSH_KEX_NL_EASTC_ALGS: |
| if (!strcmp(pss->name, "chacha20-poly1305@openssh.com")) |
| kex->match_bitfield |= 8; |
| break; |
| case SSH_KEX_NL_MACTS_ALGS: |
| if (!strcmp(pss->name, "hmac-sha2-256")) |
| kex->match_bitfield |= 16; |
| break; |
| case SSH_KEX_NL_MASTC_ALGS: |
| if (!strcmp(pss->name, "hmac-sha2-256")) |
| kex->match_bitfield |= 32; |
| break; |
| case SSH_KEX_NL_CACTS_ALGS: |
| if (!strcmp(pss->name, "none")) |
| kex->match_bitfield |= 64; |
| break; |
| case SSH_KEX_NL_CASTC_ALGS: |
| if (!strcmp(pss->name, "none")) |
| kex->match_bitfield |= 128; |
| break; |
| case SSH_KEX_NL_LCTS_ALGS: |
| case SSH_KEX_NL_LSTC_ALGS: |
| break; |
| default: |
| break; |
| } |
| |
| return 0; |
| } |
| |
| |
| static int |
| lws_kex_create(struct per_session_data__sshd *pss) |
| { |
| pss->kex = sshd_zalloc(sizeof(struct lws_kex)); |
| lwsl_info("%s\n", __func__); |
| return !pss->kex; |
| } |
| |
| static void |
| lws_kex_destroy(struct per_session_data__sshd *pss) |
| { |
| if (!pss->kex) |
| return; |
| |
| lwsl_info("Destroying KEX\n"); |
| |
| if (pss->kex->I_C) { |
| free(pss->kex->I_C); |
| pss->kex->I_C = NULL; |
| } |
| if (pss->kex->I_S) { |
| free(pss->kex->I_S); |
| pss->kex->I_S = NULL; |
| } |
| |
| lws_explicit_bzero(pss->kex, sizeof(*pss->kex)); |
| free(pss->kex); |
| pss->kex = NULL; |
| } |
| |
| static void |
| ssh_free(void *p) |
| { |
| if (!p) |
| return; |
| |
| lwsl_debug("%s: FREE %p\n", __func__, p); |
| free(p); |
| } |
| |
| #define ssh_free_set_NULL(x) if (x) { ssh_free(x); (x) = NULL; } |
| |
| static void |
| lws_ua_destroy(struct per_session_data__sshd *pss) |
| { |
| if (!pss->ua) |
| return; |
| |
| lwsl_info("%s\n", __func__); |
| |
| if (pss->ua->username) |
| ssh_free(pss->ua->username); |
| if (pss->ua->service) |
| ssh_free(pss->ua->service); |
| if (pss->ua->alg) |
| ssh_free(pss->ua->alg); |
| if (pss->ua->pubkey) |
| ssh_free(pss->ua->pubkey); |
| if (pss->ua->sig) { |
| lws_explicit_bzero(pss->ua->sig, pss->ua->sig_len); |
| ssh_free(pss->ua->sig); |
| } |
| |
| lws_explicit_bzero(pss->ua, sizeof(*pss->ua)); |
| free(pss->ua); |
| pss->ua = NULL; |
| } |
| |
| |
| static int |
| rsa_hash_alg_from_ident(const char *ident) |
| { |
| if (strcmp(ident, "ssh-rsa") == 0 || |
| strcmp(ident, "ssh-rsa-cert-v01@openssh.com") == 0) |
| return LWS_GENHASH_TYPE_SHA1; |
| if (strcmp(ident, "rsa-sha2-256") == 0) |
| return LWS_GENHASH_TYPE_SHA256; |
| if (strcmp(ident, "rsa-sha2-512") == 0) |
| return LWS_GENHASH_TYPE_SHA512; |
| |
| return -1; |
| } |
| |
| static void |
| state_get_string_alloc(struct per_session_data__sshd *pss, int next) |
| { |
| pss->parser_state = SSHS_GET_STRING_LEN_ALLOC; |
| pss->state_after_string = (char)next; |
| } |
| |
| static void |
| state_get_string(struct per_session_data__sshd *pss, int next) |
| { |
| pss->parser_state = SSHS_GET_STRING_LEN; |
| pss->state_after_string = (char)next; |
| } |
| |
| static void |
| state_get_u32(struct per_session_data__sshd *pss, int next) |
| { |
| pss->parser_state = SSHS_GET_U32; |
| pss->state_after_string = (char)next; |
| } |
| |
| static struct lws_ssh_channel * |
| ssh_get_server_ch(struct per_session_data__sshd *pss, uint32_t chi) |
| { |
| struct lws_ssh_channel *ch = pss->ch_list; |
| |
| while (ch) { |
| if (ch->server_ch == chi) |
| return ch; |
| ch = ch->next; |
| } |
| |
| return NULL; |
| } |
| |
| #if 0 |
| static struct lws_ssh_channel * |
| ssh_get_peer_ch(struct per_session_data__sshd *pss, uint32_t chi) |
| { |
| struct lws_ssh_channel *ch = pss->ch_list; |
| |
| while (ch) { |
| if (ch->sender_ch == chi) |
| return ch; |
| ch = ch->next; |
| } |
| |
| return NULL; |
| } |
| #endif |
| |
| static void |
| ssh_destroy_channel(struct per_session_data__sshd *pss, |
| struct lws_ssh_channel *ch) |
| { |
| lws_start_foreach_llp(struct lws_ssh_channel **, ppch, pss->ch_list) { |
| if (*ppch == ch) { |
| lwsl_info("Deleting ch %p\n", ch); |
| if (pss->vhd && pss->vhd->ops && |
| pss->vhd->ops->channel_destroy) |
| pss->vhd->ops->channel_destroy(ch->priv); |
| *ppch = ch->next; |
| if (ch->sub) |
| free(ch->sub); |
| free(ch); |
| |
| return; |
| } |
| } lws_end_foreach_llp(ppch, next); |
| |
| lwsl_notice("Failed to delete ch\n"); |
| } |
| |
| static void |
| lws_ssh_exec_finish(void *finish_handle, int retcode) |
| { |
| struct lws_ssh_channel *ch = (struct lws_ssh_channel *)finish_handle; |
| struct per_session_data__sshd *pss = ch->pss; |
| |
| ch->retcode = retcode; |
| write_task(pss, ch, SSH_WT_EXIT_STATUS); |
| ch->scheduled_close = 1; |
| write_task(pss, ch, SSH_WT_CH_CLOSE); |
| } |
| |
| static int |
| lws_ssh_parse_plaintext(struct per_session_data__sshd *pss, uint8_t *p, size_t len) |
| { |
| struct lws_gencrypto_keyelem e[LWS_GENCRYPTO_RSA_KEYEL_COUNT]; |
| struct lws_genrsa_ctx ctx; |
| struct lws_ssh_channel *ch; |
| struct lws_subprotocol_scp *scp; |
| uint8_t *pp, *ps, hash[64]; |
| #if !defined(MBEDTLS_VERSION_NUMBER) || MBEDTLS_VERSION_NUMBER < 0x03000000 |
| uint8_t *otmp = NULL; |
| #endif |
| uint32_t m; |
| int n; |
| |
| while (len --) { |
| again: |
| switch(pss->parser_state) { |
| case SSH_INITIALIZE_TRANSIENT: |
| pss->parser_state = SSHS_IDSTRING; |
| pss->ctr = 0; |
| pss->copy_to_I_C = 0; |
| |
| /* fallthru */ |
| case SSHS_IDSTRING: |
| if (*p == 0x0d) { |
| pss->V_C[pss->npos] = '\0'; |
| pss->npos = 0; |
| lwsl_info("peer id: %s\n", pss->V_C); |
| p++; |
| pss->parser_state = SSHS_IDSTRING_CR; |
| break; |
| } |
| if (pss->npos < sizeof(pss->V_C) - 1) |
| pss->V_C[pss->npos++] = (char)*p; |
| p++; |
| break; |
| |
| case SSHS_IDSTRING_CR: |
| if (*p++ != 0x0a) { |
| lwsl_notice("mangled id string\n"); |
| return 1; |
| } |
| pss->ssh_sequence_ctr_cts = 0; |
| pss->parser_state = SSHS_MSG_LEN; |
| break; |
| |
| case SSHS_MSG_LEN: |
| pss->msg_len = (pss->msg_len << 8) | *p++; |
| if (++pss->ctr != 4) |
| break; |
| |
| if (pss->active_keys_cts.valid) { |
| uint8_t b[4]; |
| |
| POKE_U32(b, (uint32_t)pss->msg_len); |
| pss->msg_len = lws_chachapoly_get_length( |
| &pss->active_keys_cts, |
| pss->ssh_sequence_ctr_cts, b); |
| } else |
| pss->ssh_sequence_ctr_cts++; |
| |
| lwsl_info("msg len %d\n", pss->msg_len); |
| |
| pss->parser_state = SSHS_MSG_PADDING; |
| pss->ctr = 0; |
| pss->pos = 4; |
| if (pss->msg_len < 2 + 4) { |
| lwsl_notice("illegal msg size\n"); |
| goto bail; |
| } |
| break; |
| |
| case SSHS_MSG_PADDING: |
| pss->msg_padding = *p++; |
| pss->parser_state = SSHS_MSG_ID; |
| break; |
| |
| case SSHS_MSG_ID: |
| pss->msg_id = *p++; |
| pss->ctr = 0; |
| switch (pss->msg_id) { |
| case SSH_MSG_DISCONNECT: |
| /* |
| * byte SSH_MSG_DISCONNECT |
| * uint32 reason code |
| * string description in ISO-10646 |
| * UTF-8 encoding [RFC3629] |
| * string language tag [RFC3066] |
| */ |
| lwsl_notice("SSH_MSG_DISCONNECT\n"); |
| state_get_u32(pss, SSHS_NVC_DISCONNECT_REASON); |
| break; |
| case SSH_MSG_IGNORE: |
| lwsl_notice("SSH_MSG_IGNORE\n"); |
| break; |
| case SSH_MSG_UNIMPLEMENTED: |
| lwsl_notice("SSH_MSG_UNIMPLEMENTED\n"); |
| break; |
| case SSH_MSG_DEBUG: |
| lwsl_notice("SSH_MSG_DEBUG\n"); |
| break; |
| case SSH_MSG_SERVICE_REQUEST: |
| lwsl_info("SSH_MSG_SERVICE_REQUEST\n"); |
| /* payload is a string */ |
| state_get_string(pss, SSHS_DO_SERVICE_REQUEST); |
| break; |
| case SSH_MSG_SERVICE_ACCEPT: |
| lwsl_notice("SSH_MSG_ACCEPT\n"); |
| break; |
| |
| case SSH_MSG_KEXINIT: |
| if (pss->kex_state != |
| KEX_STATE_EXPECTING_CLIENT_OFFER) { |
| pss->parser_state = SSH_KEX_STATE_SKIP; |
| break; |
| } |
| if (!pss->kex) { |
| lwsl_notice("%s: SSH_MSG_KEXINIT: NULL pss->kex\n", __func__); |
| goto bail; |
| } |
| pss->parser_state = SSH_KEX_STATE_COOKIE; |
| pss->kex->I_C_payload_len = 0; |
| pss->kex->I_C_alloc_len = pss->msg_len; |
| pss->kex->I_C = sshd_zalloc(pss->kex->I_C_alloc_len); |
| if (!pss->kex->I_C) { |
| lwsl_notice("OOM 3\n"); |
| goto bail; |
| } |
| pss->kex->I_C[pss->kex->I_C_payload_len++] = |
| pss->msg_id; |
| pss->copy_to_I_C = 1; |
| break; |
| case SSH_MSG_KEX_ECDH_INIT: |
| pss->parser_state = SSH_KEX_STATE_ECDH_KEYLEN; |
| break; |
| |
| case SSH_MSG_NEWKEYS: |
| if (pss->kex_state != |
| KEX_STATE_REPLIED_TO_OFFER && |
| pss->kex_state != |
| KEX_STATE_CRYPTO_INITIALIZED) { |
| lwsl_notice("unexpected newkeys\n"); |
| |
| goto bail; |
| } |
| /* |
| * it means we should now use the keys we |
| * agreed on |
| */ |
| lwsl_info("Activating CTS keys\n"); |
| pss->active_keys_cts = pss->kex->keys_next_cts; |
| if (lws_chacha_activate(&pss->active_keys_cts)) |
| goto bail; |
| |
| pss->kex->newkeys |= 2; |
| if (pss->kex->newkeys == 3) |
| lws_kex_destroy(pss); |
| |
| if (pss->msg_padding) { |
| pss->copy_to_I_C = 0; |
| pss->parser_state = |
| SSHS_MSG_EAT_PADDING; |
| break; |
| } else { |
| pss->parser_state = SSHS_MSG_LEN; |
| } |
| break; |
| |
| case SSH_MSG_USERAUTH_REQUEST: |
| /* |
| * byte SSH_MSG_USERAUTH_REQUEST |
| * string user name in UTF-8 |
| * encoding [RFC3629] |
| * string service name in US-ASCII |
| * string "publickey" |
| * boolean FALSE |
| * string public key alg |
| * string public key blob |
| */ |
| lwsl_info("SSH_MSG_USERAUTH_REQUEST\n"); |
| if (pss->ua) { |
| lwsl_notice("pss->ua overwrite\n"); |
| |
| goto bail; |
| } |
| |
| pss->ua = sshd_zalloc(sizeof(*pss->ua)); |
| if (!pss->ua) |
| goto bail; |
| |
| state_get_string_alloc(pss, SSHS_DO_UAR_SVC); |
| /* username is destroyed with userauth struct */ |
| if (!pss->sent_banner) { |
| if (pss->vhd->ops->banner) |
| write_task(pss, NULL, |
| SSH_WT_UA_BANNER); |
| pss->sent_banner = 1; |
| } |
| break; |
| case SSH_MSG_USERAUTH_FAILURE: |
| goto bail; |
| case SSH_MSG_USERAUTH_SUCCESS: |
| goto bail; |
| case SSH_MSG_USERAUTH_BANNER: |
| goto bail; |
| |
| case SSH_MSG_CHANNEL_OPEN: |
| state_get_string(pss, SSHS_NVC_CHOPEN_TYPE); |
| break; |
| |
| case SSH_MSG_CHANNEL_REQUEST: |
| /* RFC4254 |
| * |
| * byte SSH_MSG_CHANNEL_REQUEST |
| * uint32 recipient channel |
| * string "pty-req" |
| * boolean want_reply |
| * string TERM environment variable value |
| * (e.g., vt100) |
| * uint32 terminal width, characters |
| * (e.g., 80) |
| * uint32 terminal height, rows (e.g., 24) |
| * uint32 terminal width, px (e.g., 640) |
| * uint32 terminal height, px (e.g., 480) |
| * string encoded terminal modes |
| */ |
| state_get_u32(pss, SSHS_NVC_CHRQ_RECIP); |
| break; |
| |
| case SSH_MSG_CHANNEL_EOF: |
| /* RFC4254 |
| * When a party will no longer send more data |
| * to a channel, it SHOULD send |
| * SSH_MSG_CHANNEL_EOF. |
| * |
| * byte SSH_MSG_CHANNEL_EOF |
| * uint32 recipient channel |
| */ |
| state_get_u32(pss, SSHS_NVC_CH_EOF); |
| break; |
| |
| case SSH_MSG_CHANNEL_CLOSE: |
| /* RFC4254 |
| * |
| * byte SSH_MSG_CHANNEL_CLOSE |
| * uint32 recipient channel |
| * |
| * This message does not consume window space |
| * and can be sent even if no window space is |
| * available. |
| * |
| * It is RECOMMENDED that all data sent before |
| * this message be delivered to the actual |
| * destination, if possible. |
| */ |
| state_get_u32(pss, SSHS_NVC_CH_CLOSE); |
| break; |
| |
| case SSH_MSG_CHANNEL_DATA: |
| /* RFC4254 |
| * |
| * byte SSH_MSG_CHANNEL_DATA |
| * uint32 recipient channel |
| * string data |
| */ |
| state_get_u32(pss, SSHS_NVC_CD_RECIP); |
| break; |
| |
| case SSH_MSG_CHANNEL_WINDOW_ADJUST: |
| /* RFC452 |
| * |
| * byte SSH_MSG_CHANNEL_WINDOW_ADJUST |
| * uint32 recipient channel |
| * uint32 bytes to add |
| */ |
| if (!pss->ch_list) |
| goto bail; |
| |
| state_get_u32(pss, SSHS_NVC_WA_RECIP); |
| break; |
| default: |
| lwsl_notice("unk msg_id %d\n", pss->msg_id); |
| |
| goto bail; |
| } |
| break; |
| |
| case SSH_KEX_STATE_COOKIE: |
| if (pss->msg_len < 16 + 1 + 1 + (10 * 4) + 5) { |
| lwsl_notice("sanity: kex length failed\n"); |
| goto bail; |
| } |
| pss->kex->kex_cookie[pss->ctr++] = *p++; |
| if (pss->ctr != sizeof(pss->kex->kex_cookie)) |
| break; |
| pss->parser_state = SSH_KEX_NL_KEX_ALGS_LEN; |
| pss->ctr = 0; |
| break; |
| case SSH_KEX_NL_KEX_ALGS_LEN: |
| case SSH_KEX_NL_SHK_ALGS_LEN: |
| case SSH_KEX_NL_EACTS_ALGS_LEN: |
| case SSH_KEX_NL_EASTC_ALGS_LEN: |
| case SSH_KEX_NL_MACTS_ALGS_LEN: |
| case SSH_KEX_NL_MASTC_ALGS_LEN: |
| case SSH_KEX_NL_CACTS_ALGS_LEN: |
| case SSH_KEX_NL_CASTC_ALGS_LEN: |
| case SSH_KEX_NL_LCTS_ALGS_LEN: |
| case SSH_KEX_NL_LSTC_ALGS_LEN: |
| case SSH_KEX_STATE_ECDH_KEYLEN: |
| |
| pss->len = (pss->len << 8) | *p++; |
| if (++pss->ctr != 4) |
| break; |
| |
| switch (pss->parser_state) { |
| case SSH_KEX_STATE_ECDH_KEYLEN: |
| pss->parser_state = SSH_KEX_STATE_ECDH_Q_C; |
| break; |
| default: |
| pss->parser_state++; |
| if (pss->len == 0) |
| pss->parser_state++; |
| break; |
| } |
| pss->ctr = 0; |
| pss->npos = 0; |
| if (pss->msg_len - pss->pos < pss->len) { |
| lwsl_notice("sanity: length %d - %d < %d\n", |
| pss->msg_len, pss->pos, pss->len); |
| goto bail; |
| } |
| break; |
| |
| case SSH_KEX_NL_KEX_ALGS: |
| case SSH_KEX_NL_SHK_ALGS: |
| case SSH_KEX_NL_EACTS_ALGS: |
| case SSH_KEX_NL_EASTC_ALGS: |
| case SSH_KEX_NL_MACTS_ALGS: |
| case SSH_KEX_NL_MASTC_ALGS: |
| case SSH_KEX_NL_CACTS_ALGS: |
| case SSH_KEX_NL_CASTC_ALGS: |
| case SSH_KEX_NL_LCTS_ALGS: |
| case SSH_KEX_NL_LSTC_ALGS: |
| if (*p != ',') { |
| if (pss->npos < sizeof(pss->name) - 1) |
| pss->name[pss->npos++] = (char)*p; |
| } else { |
| pss->name[pss->npos] = '\0'; |
| pss->npos = 0; |
| handle_name(pss); |
| } |
| p++; |
| if (!--pss->len) { |
| pss->name[pss->npos] = '\0'; |
| if (pss->npos) |
| handle_name(pss); |
| pss->parser_state++; |
| break; |
| } |
| break; |
| |
| case SSH_KEX_FIRST_PKT: |
| pss->first_coming = !!*p++; |
| pss->parser_state = SSH_KEX_RESERVED; |
| break; |
| |
| case SSH_KEX_RESERVED: |
| pss->len = (pss->len << 8) | *p++; |
| if (++pss->ctr != 4) |
| break; |
| pss->ctr = 0; |
| if (pss->msg_padding) { |
| pss->copy_to_I_C = 0; |
| pss->parser_state = SSHS_MSG_EAT_PADDING; |
| break; |
| } |
| pss->parser_state = SSHS_MSG_LEN; |
| break; |
| |
| case SSH_KEX_STATE_ECDH_Q_C: |
| if (pss->len != 32) { |
| lwsl_notice("wrong key len\n"); |
| goto bail; |
| } |
| pss->kex->Q_C[pss->ctr++] = *p++; |
| if (pss->ctr != 32) |
| break; |
| lwsl_info("Q_C parsed\n"); |
| pss->parser_state = SSHS_MSG_EAT_PADDING; |
| break; |
| |
| case SSH_KEX_STATE_SKIP: |
| if (pss->pos - 4 < pss->msg_len) { |
| p++; |
| break; |
| } |
| lwsl_debug("skip done pos %d, msg_len %d len=%ld, \n", |
| pss->pos, pss->msg_len, (long)len); |
| pss->parser_state = SSHS_MSG_LEN; |
| pss->ctr = 0; |
| break; |
| |
| case SSHS_MSG_EAT_PADDING: |
| p++; |
| if (--pss->msg_padding) |
| break; |
| if (pss->msg_len + 4 != pss->pos) { |
| lwsl_notice("sanity: kex end mismatch %d %d\n", |
| pss->pos, pss->msg_len); |
| goto bail; |
| } |
| |
| switch (pss->msg_id) { |
| case SSH_MSG_KEX_ECDH_INIT: |
| if (pss->kex->match_bitfield != 0xff) { |
| lwsl_notice("unable to negotiate\n"); |
| goto bail; |
| } |
| if (kex_ecdh(pss, pss->kex->kex_r, |
| &pss->kex->kex_r_len)) { |
| lwsl_notice("hex_ecdh failed\n"); |
| goto bail; |
| } |
| write_task(pss, NULL, SSH_WT_OFFER_REPLY); |
| break; |
| } |
| |
| pss->parser_state = SSHS_MSG_LEN; |
| pss->ctr = 0; |
| break; |
| |
| case SSHS_GET_STRING_LEN: |
| pss->npos = 0; |
| pss->len = (pss->len << 8) | *p++; |
| if (++pss->ctr != 4) |
| break; |
| pss->ctr = 0; |
| pss->parser_state = SSHS_GET_STRING; |
| break; |
| |
| case SSHS_GET_STRING: |
| if (pss->npos >= sizeof(pss->name) - 1) { |
| lwsl_notice("non-alloc string too big\n"); |
| goto bail; |
| } |
| pss->name[pss->npos++] = (char)*p++; |
| if (pss->npos != pss->len) |
| break; |
| |
| pss->name[pss->npos] = '\0'; |
| pss->parser_state = pss->state_after_string; |
| goto again; |
| |
| case SSHS_GET_STRING_LEN_ALLOC: |
| pss->npos = 0; |
| pss->len = (pss->len << 8) | *p++; |
| if (++pss->ctr != 4) |
| break; |
| pss->ctr = 0; |
| pss->last_alloc = sshd_zalloc(pss->len + 1); |
| lwsl_debug("SSHS_GET_STRING_LEN_ALLOC: %p, state %d\n", |
| pss->last_alloc, pss->state_after_string); |
| if (!pss->last_alloc) { |
| lwsl_notice("alloc string too big\n"); |
| goto bail; |
| } |
| pss->parser_state = SSHS_GET_STRING_ALLOC; |
| break; |
| |
| case SSHS_GET_STRING_ALLOC: |
| if (pss->npos >= pss->len) |
| goto bail; |
| pss->last_alloc[pss->npos++] = *p++; |
| if (pss->npos != pss->len) |
| break; |
| pss->last_alloc[pss->npos] = '\0'; |
| pss->parser_state = pss->state_after_string; |
| goto again; |
| |
| /* |
| * User Authentication |
| */ |
| |
| case SSHS_DO_SERVICE_REQUEST: |
| pss->okayed_userauth = 1; |
| pss->parser_state = SSHS_MSG_EAT_PADDING; |
| /* |
| * this only 'accepts' that we can negotiate auth for |
| * this service, not accepts the auth |
| */ |
| write_task(pss, NULL, SSH_WT_UA_ACCEPT); |
| break; |
| |
| case SSHS_DO_UAR_SVC: |
| pss->ua->username = (char *)pss->last_alloc; |
| pss->last_alloc = NULL; /* it was adopted */ |
| state_get_string_alloc(pss, SSHS_DO_UAR_PUBLICKEY); |
| /* destroyed with UA struct */ |
| break; |
| |
| case SSHS_DO_UAR_PUBLICKEY: |
| pss->ua->service = (char *)pss->last_alloc; |
| pss->last_alloc = NULL; /* it was adopted */ |
| |
| /* Sect 5, RFC4252 |
| * |
| * The 'user name' and 'service name' are repeated in |
| * every new authentication attempt, and MAY change. |
| * |
| * The server implementation MUST carefully check them |
| * in every message, and MUST flush any accumulated |
| * authentication states if they change. If it is |
| * unable to flush an authentication state, it MUST |
| * disconnect if the 'user name' or 'service name' |
| * changes. |
| */ |
| |
| if (pss->seen_auth_req_before && ( |
| strcmp(pss->ua->username, |
| pss->last_auth_req_username) || |
| strcmp(pss->ua->service, |
| pss->last_auth_req_service))) { |
| lwsl_notice("username / svc changed\n"); |
| |
| goto bail; |
| } |
| |
| pss->seen_auth_req_before = 1; |
| lws_strncpy(pss->last_auth_req_username, |
| pss->ua->username, |
| sizeof(pss->last_auth_req_username)); |
| lws_strncpy(pss->last_auth_req_service, |
| pss->ua->service, |
| sizeof(pss->last_auth_req_service)); |
| |
| if (strcmp(pss->ua->service, "ssh-connection")) |
| goto ua_fail; |
| state_get_string(pss, SSHS_NVC_DO_UAR_CHECK_PUBLICKEY); |
| break; |
| |
| case SSHS_NVC_DO_UAR_CHECK_PUBLICKEY: |
| if (!strcmp(pss->name, "none")) { |
| /* we must fail it */ |
| lwsl_info("got 'none' req, refusing\n"); |
| goto ua_fail; |
| } |
| |
| if (strcmp(pss->name, "publickey")) { |
| lwsl_notice("expected 'publickey' got '%s'\n", |
| pss->name); |
| goto ua_fail; |
| } |
| pss->parser_state = SSHS_DO_UAR_SIG_PRESENT; |
| break; |
| |
| case SSHS_DO_UAR_SIG_PRESENT: |
| lwsl_info("SSHS_DO_UAR_SIG_PRESENT\n"); |
| pss->ua->sig_present = (char)*p++; |
| state_get_string_alloc(pss, SSHS_NVC_DO_UAR_ALG); |
| /* destroyed with UA struct */ |
| break; |
| |
| case SSHS_NVC_DO_UAR_ALG: |
| pss->ua->alg = (char *)pss->last_alloc; |
| pss->last_alloc = NULL; /* it was adopted */ |
| if (rsa_hash_alg_from_ident(pss->ua->alg) < 0) { |
| lwsl_notice("unknown alg\n"); |
| goto ua_fail; |
| } |
| state_get_string_alloc(pss, SSHS_NVC_DO_UAR_PUBKEY_BLOB); |
| /* destroyed with UA struct */ |
| break; |
| |
| case SSHS_NVC_DO_UAR_PUBKEY_BLOB: |
| pss->ua->pubkey = pss->last_alloc; |
| pss->last_alloc = NULL; /* it was adopted */ |
| pss->ua->pubkey_len = pss->npos; |
| /* |
| * RFC4253 |
| * |
| * ssh-rsa |
| * |
| * The structure inside the blob is |
| * |
| * mpint e |
| * mpint n |
| * |
| * Let's see if this key is authorized |
| */ |
| |
| n = 1; |
| if (pss->vhd->ops && pss->vhd->ops->is_pubkey_authorized) |
| n = pss->vhd->ops->is_pubkey_authorized( |
| pss->ua->username, pss->ua->alg, |
| pss->ua->pubkey, (int)pss->ua->pubkey_len); |
| if (n) { |
| lwsl_info("rejecting peer pubkey\n"); |
| goto ua_fail; |
| } |
| |
| if (pss->ua->sig_present) { |
| state_get_string_alloc(pss, SSHS_NVC_DO_UAR_SIG); |
| /* destroyed with UA struct */ |
| break; |
| } |
| |
| /* |
| * This key is at least one we would be prepared |
| * to accept if he really has it... since no sig |
| * client should resend everything with a sig |
| * appended. OK it and delete this initial UA |
| */ |
| write_task(pss, NULL, SSH_WT_UA_PK_OK); |
| pss->parser_state = SSHS_MSG_EAT_PADDING; |
| break; |
| |
| case SSHS_NVC_DO_UAR_SIG: |
| /* |
| * Now the pubkey is coming with a sig |
| */ |
| /* Sect 5.1 RFC4252 |
| * |
| * SSH_MSG_USERAUTH_SUCCESS MUST be sent only once. |
| * When SSH_MSG_USERAUTH_SUCCESS has been sent, any |
| * further authentication requests received after that |
| * SHOULD be silently ignored. |
| */ |
| if (pss->ssh_auth_state == SSH_AUTH_STATE_GAVE_AUTH_IGNORE_REQS) { |
| lwsl_info("Silently ignoring auth req after accepted\n"); |
| goto ua_fail_silently; |
| } |
| lwsl_info("SSHS_DO_UAR_SIG\n"); |
| pss->ua->sig = pss->last_alloc; |
| pss->last_alloc = NULL; /* it was adopted */ |
| pss->ua->sig_len = pss->npos; |
| pss->parser_state = SSHS_MSG_EAT_PADDING; |
| |
| /* |
| * RFC 4252 p9 |
| * |
| * The value of 'signature' is a signature with |
| * the private host key of the following data, in |
| * this order: |
| * |
| * string session identifier |
| * byte SSH_MSG_USERAUTH_REQUEST |
| * string user name |
| * string service name |
| * string "publickey" |
| * boolean TRUE |
| * string public key algorithm name |
| * string public key to be used for auth |
| * |
| * We reproduce the signature plaintext and the |
| * hash, and then decrypt the incoming signed block. |
| * What comes out is some ASN1, in there is the |
| * hash decrypted. We find it and confirm it |
| * matches the hash we computed ourselves. |
| * |
| * First step is generate the sig plaintext |
| */ |
| n = 4 + 32 + |
| 1 + |
| 4 + (int)strlen(pss->ua->username) + |
| 4 + (int)strlen(pss->ua->service) + |
| 4 + 9 + |
| 1 + |
| 4 + (int)strlen(pss->ua->alg) + |
| 4 + (int)pss->ua->pubkey_len; |
| |
| ps = sshd_zalloc((unsigned int)n); |
| if (!ps) { |
| lwsl_notice("OOM 4\n"); |
| goto ua_fail; |
| } |
| |
| pp = ps; |
| lws_buf(&pp, pss->session_id, 32); |
| *pp++ = SSH_MSG_USERAUTH_REQUEST; |
| lws_cstr(&pp, pss->ua->username, 64); |
| lws_cstr(&pp, pss->ua->service, 64); |
| lws_cstr(&pp, "publickey", 64); |
| *pp++ = 1; |
| lws_cstr(&pp, pss->ua->alg, 64); |
| lws_buf(&pp, pss->ua->pubkey, pss->ua->pubkey_len); |
| |
| /* Next hash the plaintext */ |
| |
| if (lws_genhash_init(&pss->ua->hash_ctx, |
| (enum lws_genhash_types)rsa_hash_alg_from_ident(pss->ua->alg))) { |
| lwsl_notice("genhash init failed\n"); |
| free(ps); |
| goto ua_fail; |
| } |
| |
| if (lws_genhash_update(&pss->ua->hash_ctx, ps, lws_ptr_diff_size_t(pp, ps))) { |
| lwsl_notice("genhash update failed\n"); |
| free(ps); |
| goto ua_fail; |
| } |
| lws_genhash_destroy(&pss->ua->hash_ctx, hash); |
| free(ps); |
| |
| /* |
| * Prepare the RSA decryption context: load in |
| * the E and N factors |
| */ |
| |
| memset(e, 0, sizeof(e)); |
| pp = pss->ua->pubkey; |
| m = lws_g32(&pp); |
| pp += m; |
| m = lws_g32(&pp); |
| e[LWS_GENCRYPTO_RSA_KEYEL_E].buf = pp; |
| e[LWS_GENCRYPTO_RSA_KEYEL_E].len = m; |
| pp += m; |
| m = lws_g32(&pp); |
| e[LWS_GENCRYPTO_RSA_KEYEL_N].buf = pp; |
| e[LWS_GENCRYPTO_RSA_KEYEL_N].len = m; |
| |
| if (lws_genrsa_create(&ctx, e, pss->vhd->context, |
| LGRSAM_PKCS1_1_5, |
| LWS_GENHASH_TYPE_UNKNOWN)) |
| goto ua_fail; |
| |
| /* |
| * point to the encrypted signature payload we |
| * were sent |
| */ |
| pp = pss->ua->sig; |
| m = lws_g32(&pp); |
| pp += m; |
| m = lws_g32(&pp); |
| #if !defined(MBEDTLS_VERSION_NUMBER) || MBEDTLS_VERSION_NUMBER < 0x03000000 |
| |
| /* |
| * decrypt it, resulting in an error, or some ASN1 |
| * including the decrypted signature |
| */ |
| otmp = sshd_zalloc(m); |
| if (!otmp) |
| /* ua_fail1 frees bn_e, bn_n and rsa */ |
| goto ua_fail1; |
| |
| n = lws_genrsa_public_decrypt(&ctx, pp, m, otmp, m); |
| if (n > 0) { |
| /* the decrypted sig is in ASN1 format */ |
| m = 0; |
| while ((int)m < n) { |
| /* sig payload */ |
| if (otmp[m] == 0x04 && |
| otmp[m + 1] == lws_genhash_size( |
| pss->ua->hash_ctx.type)) { |
| m = (uint32_t)memcmp(&otmp[m + 2], hash, |
| (unsigned int)lws_genhash_size(pss->ua->hash_ctx.type)); |
| break; |
| } |
| /* go into these */ |
| if (otmp[m] == 0x30) { |
| m += 2; |
| continue; |
| } |
| /* otherwise skip payloads */ |
| m += (uint32_t)(otmp[m + 1] + 2); |
| } |
| } |
| |
| free(otmp); |
| #else |
| ctx.ctx->MBEDTLS_PRIVATE(len) = m; |
| n = lws_genrsa_hash_sig_verify(&ctx, hash, |
| (enum lws_genhash_types)rsa_hash_alg_from_ident(pss->ua->alg), |
| pp, m) == 0 ? 1 : 0; |
| #endif |
| lws_genrsa_destroy(&ctx); |
| |
| /* |
| * if no good, m is nonzero and inform peer |
| */ |
| if (n <= 0) { |
| lwsl_notice("hash sig verify fail: %d\n", m); |
| goto ua_fail; |
| } |
| |
| /* if it checks out, inform peer */ |
| |
| lwsl_info("sig check OK\n"); |
| |
| /* Sect 5.1 RFC4252 |
| * |
| * SSH_MSG_USERAUTH_SUCCESS MUST be sent only once. |
| * When SSH_MSG_USERAUTH_SUCCESS has been sent, any |
| * further authentication requests received after that |
| * SHOULD be silently ignored. |
| */ |
| pss->ssh_auth_state = SSH_AUTH_STATE_GAVE_AUTH_IGNORE_REQS; |
| |
| write_task(pss, NULL, SSH_WT_UA_SUCCESS); |
| lws_ua_destroy(pss); |
| break; |
| |
| /* |
| * Channels |
| */ |
| |
| case SSHS_GET_U32: |
| pss->len = (pss->len << 8) | *p++; |
| if (++pss->ctr != 4) |
| break; |
| pss->ctr = 0; |
| pss->parser_state = pss->state_after_string; |
| goto again; |
| |
| /* |
| * Channel: Disconnect |
| */ |
| |
| case SSHS_NVC_DISCONNECT_REASON: |
| pss->disconnect_reason = pss->len; |
| state_get_string_alloc(pss, SSHS_NVC_DISCONNECT_DESC); |
| break; |
| |
| case SSHS_NVC_DISCONNECT_DESC: |
| pss->disconnect_desc = (char *)pss->last_alloc; |
| pss->last_alloc = NULL; /* it was adopted */ |
| state_get_string(pss, SSHS_NVC_DISCONNECT_LANG); |
| break; |
| |
| case SSHS_NVC_DISCONNECT_LANG: |
| lwsl_notice("SSHS_NVC_DISCONNECT_LANG\n"); |
| if (pss->vhd->ops && pss->vhd->ops->disconnect_reason) |
| pss->vhd->ops->disconnect_reason( |
| pss->disconnect_reason, |
| pss->disconnect_desc, pss->name); |
| ssh_free_set_NULL(pss->last_alloc); |
| break; |
| |
| /* |
| * Channel: Open |
| */ |
| |
| case SSHS_NVC_CHOPEN_TYPE: |
| /* channel open */ |
| if (strcmp(pss->name, "session")) { |
| lwsl_notice("Failing on not session\n"); |
| pss->reason = 3; |
| goto ch_fail; |
| } |
| lwsl_info("SSHS_NVC_CHOPEN_TYPE: creating session\n"); |
| pss->ch_temp = sshd_zalloc(sizeof(*pss->ch_temp)); |
| if (!pss->ch_temp) |
| return -1; |
| |
| pss->ch_temp->type = SSH_CH_TYPE_SESSION; |
| pss->ch_temp->pss = pss; |
| state_get_u32(pss, SSHS_NVC_CHOPEN_SENDER_CH); |
| break; |
| |
| case SSHS_NVC_CHOPEN_SENDER_CH: |
| pss->ch_temp->sender_ch = pss->len; |
| state_get_u32(pss, SSHS_NVC_CHOPEN_WINSIZE); |
| break; |
| case SSHS_NVC_CHOPEN_WINSIZE: |
| lwsl_info("Initial window set to %d\n", pss->len); |
| pss->ch_temp->window = (int32_t)pss->len; |
| state_get_u32(pss, SSHS_NVC_CHOPEN_PKTSIZE); |
| break; |
| case SSHS_NVC_CHOPEN_PKTSIZE: |
| pss->ch_temp->max_pkt = pss->len; |
| pss->ch_temp->peer_window_est = LWS_SSH_INITIAL_WINDOW; |
| pss->ch_temp->server_ch = (uint32_t)pss->next_ch_num++; |
| /* |
| * add us to channel list... leave as ch_temp |
| * as write task needs it and will NULL down |
| */ |
| lwsl_info("creating new session ch\n"); |
| pss->ch_temp->next = pss->ch_list; |
| pss->ch_list = pss->ch_temp; |
| if (pss->vhd->ops && pss->vhd->ops->channel_create) |
| pss->vhd->ops->channel_create(pss->wsi, |
| &pss->ch_temp->priv); |
| write_task(pss, pss->ch_temp, SSH_WT_CH_OPEN_CONF); |
| pss->parser_state = SSHS_MSG_EAT_PADDING; |
| break; |
| |
| /* |
| * SSH_MSG_CHANNEL_REQUEST |
| */ |
| |
| case SSHS_NVC_CHRQ_RECIP: |
| pss->ch_recip = pss->len; |
| state_get_string(pss, SSHS_NVC_CHRQ_TYPE); |
| break; |
| |
| case SSHS_NVC_CHRQ_TYPE: |
| pss->parser_state = SSHS_CHRQ_WANT_REPLY; |
| break; |
| |
| case SSHS_CHRQ_WANT_REPLY: |
| pss->rq_want_reply = *p++; |
| lwsl_info("SSHS_CHRQ_WANT_REPLY: %s, wantrep: %d\n", |
| pss->name, pss->rq_want_reply); |
| |
| pss->ch_temp = ssh_get_server_ch(pss, pss->ch_recip); |
| |
| /* after this they differ by the request */ |
| |
| /* |
| * a PTY for a shell |
| */ |
| if (!strcmp(pss->name, "pty-req")) { |
| state_get_string(pss, SSHS_NVC_CHRQ_TERM); |
| break; |
| } |
| /* |
| * a shell |
| */ |
| if (!strcmp(pss->name, "shell")) { |
| pss->channel_doing_spawn = pss->ch_temp->server_ch; |
| if (pss->vhd->ops && pss->vhd->ops->shell && |
| !pss->vhd->ops->shell(pss->ch_temp->priv, |
| pss->wsi, |
| lws_ssh_exec_finish, pss->ch_temp)) { |
| |
| if (pss->rq_want_reply) |
| write_task_insert(pss, pss->ch_temp, |
| SSH_WT_CHRQ_SUCC); |
| pss->parser_state = SSHS_MSG_EAT_PADDING; |
| break; |
| } |
| |
| goto chrq_fail; |
| } |
| /* |
| * env vars to be set in the shell |
| */ |
| if (!strcmp(pss->name, "env")) { |
| state_get_string(pss, SSHS_NVC_CHRQ_ENV_NAME); |
| break; |
| } |
| |
| /* |
| * exec something |
| */ |
| if (!strcmp(pss->name, "exec")) { |
| state_get_string_alloc(pss, SSHS_NVC_CHRQ_EXEC_CMD); |
| break; |
| } |
| |
| /* |
| * spawn a subsystem |
| */ |
| if (!strcmp(pss->name, "subsystem")) { |
| lwsl_notice("subsystem\n"); |
| state_get_string_alloc(pss, |
| SSHS_NVC_CHRQ_SUBSYSTEM); |
| break; |
| } |
| |
| if (pss->rq_want_reply) |
| goto chrq_fail; |
| |
| pss->parser_state = SSH_KEX_STATE_SKIP; |
| break; |
| |
| /* CHRQ pty-req */ |
| |
| case SSHS_NVC_CHRQ_TERM: |
| memcpy(pss->args.pty.term, pss->name, |
| sizeof(pss->args.pty.term) - 1); |
| state_get_u32(pss, SSHS_NVC_CHRQ_TW); |
| break; |
| case SSHS_NVC_CHRQ_TW: |
| pss->args.pty.width_ch = pss->len; |
| state_get_u32(pss, SSHS_NVC_CHRQ_TH); |
| break; |
| case SSHS_NVC_CHRQ_TH: |
| pss->args.pty.height_ch = pss->len; |
| state_get_u32(pss, SSHS_NVC_CHRQ_TWP); |
| break; |
| case SSHS_NVC_CHRQ_TWP: |
| pss->args.pty.width_px = pss->len; |
| state_get_u32(pss, SSHS_NVC_CHRQ_THP); |
| break; |
| case SSHS_NVC_CHRQ_THP: |
| pss->args.pty.height_px = pss->len; |
| state_get_string_alloc(pss, SSHS_NVC_CHRQ_MODES); |
| break; |
| case SSHS_NVC_CHRQ_MODES: |
| /* modes is a stream of byte-pairs, not a string */ |
| pss->args.pty.modes = (char *)pss->last_alloc; |
| pss->last_alloc = NULL; /* it was adopted */ |
| pss->args.pty.modes_len = pss->npos; |
| n = 0; |
| if (pss->vhd->ops && pss->vhd->ops->pty_req) |
| n = pss->vhd->ops->pty_req(pss->ch_temp->priv, |
| &pss->args.pty); |
| ssh_free_set_NULL(pss->args.pty.modes); |
| if (n) |
| goto chrq_fail; |
| if (pss->rq_want_reply) |
| write_task(pss, pss->ch_temp, SSH_WT_CHRQ_SUCC); |
| pss->parser_state = SSHS_MSG_EAT_PADDING; |
| break; |
| |
| /* CHRQ env */ |
| |
| case SSHS_NVC_CHRQ_ENV_NAME: |
| strcpy(pss->args.aux, pss->name); |
| state_get_string(pss, SSHS_NVC_CHRQ_ENV_VALUE); |
| break; |
| |
| case SSHS_NVC_CHRQ_ENV_VALUE: |
| if (pss->vhd->ops && pss->vhd->ops->set_env) |
| if (pss->vhd->ops->set_env(pss->ch_temp->priv, |
| pss->args.aux, pss->name)) |
| goto chrq_fail; |
| pss->parser_state = SSHS_MSG_EAT_PADDING; |
| break; |
| |
| /* CHRQ exec */ |
| |
| case SSHS_NVC_CHRQ_EXEC_CMD: |
| /* |
| * byte SSH_MSG_CHANNEL_REQUEST |
| * uint32 recipient channel |
| * string "exec" |
| * boolean want reply |
| * string command |
| * |
| * This message will request that the server start the |
| * execution of the given command. The 'command' string |
| * may contain a path. Normal precautions MUST be taken |
| * to prevent the execution of unauthorized commands. |
| * |
| * scp sends "scp -t /path/..." |
| */ |
| lwsl_info("exec cmd: %s %02X\n", pss->last_alloc, *p); |
| |
| pss->channel_doing_spawn = pss->ch_temp->server_ch; |
| |
| if (pss->vhd->ops && pss->vhd->ops->exec && |
| !pss->vhd->ops->exec(pss->ch_temp->priv, pss->wsi, |
| (const char *)pss->last_alloc, |
| lws_ssh_exec_finish, pss->ch_temp)) { |
| ssh_free_set_NULL(pss->last_alloc); |
| if (pss->rq_want_reply) |
| write_task_insert(pss, pss->ch_temp, |
| SSH_WT_CHRQ_SUCC); |
| |
| pss->parser_state = SSHS_MSG_EAT_PADDING; |
| break; |
| } |
| |
| /* |
| * even if he doesn't want to exec it, we know how to |
| * fake scp |
| */ |
| |
| /* we only alloc "exec" of scp for scp destination */ |
| n = 1; |
| if (pss->last_alloc[0] != 's' || |
| pss->last_alloc[1] != 'c' || |
| pss->last_alloc[2] != 'p' || |
| pss->last_alloc[3] != ' ') |
| /* disallow it */ |
| n = 0; |
| |
| ssh_free_set_NULL(pss->last_alloc); |
| if (!n) |
| goto chrq_fail; |
| |
| /* our channel speaks SCP protocol now */ |
| |
| scp = sshd_zalloc(sizeof(*scp)); |
| if (!scp) |
| return -1; |
| |
| pss->ch_temp->type = SSH_CH_TYPE_SCP; |
| pss->ch_temp->sub = (lws_subprotocol *)scp; |
| |
| scp->ips = SSHS_SCP_COLLECTSTR; |
| |
| if (pss->rq_want_reply) |
| write_task(pss, pss->ch_temp, SSH_WT_CHRQ_SUCC); |
| |
| /* we start the scp protocol first by sending an ACK */ |
| write_task(pss, pss->ch_temp, SSH_WT_SCP_ACK_OKAY); |
| |
| pss->parser_state = SSHS_MSG_EAT_PADDING; |
| break; |
| |
| case SSHS_NVC_CHRQ_SUBSYSTEM: |
| lwsl_notice("subsystem: %s", pss->last_alloc); |
| n = 0; |
| #if 0 |
| if (!strcmp(pss->name, "sftp")) { |
| lwsl_notice("SFTP session\n"); |
| pss->ch_temp->type = SSH_CH_TYPE_SFTP; |
| n = 1; |
| } |
| #endif |
| ssh_free_set_NULL(pss->last_alloc); |
| // if (!n) |
| goto ch_fail; |
| #if 0 |
| if (pss->rq_want_reply) |
| write_task(pss, ssh_get_server_ch(pss, |
| pss->ch_recip), SSH_WT_CHRQ_SUCC); |
| pss->parser_state = SSHS_MSG_EAT_PADDING; |
| break; |
| #endif |
| |
| /* SSH_MSG_CHANNEL_DATA */ |
| |
| case SSHS_NVC_CD_RECIP: |
| pss->ch_recip = pss->len; |
| |
| ch = ssh_get_server_ch(pss, pss->ch_recip); |
| ch->peer_window_est -= (int32_t)pss->msg_len; |
| |
| if (pss->msg_len < sizeof(pss->name)) |
| state_get_string(pss, SSHS_NVC_CD_DATA); |
| else |
| state_get_string_alloc(pss, |
| SSHS_NVC_CD_DATA_ALLOC); |
| break; |
| |
| case SSHS_NVC_CD_DATA_ALLOC: |
| case SSHS_NVC_CD_DATA: |
| /* |
| * Actual protocol incoming payload |
| */ |
| if (pss->parser_state == SSHS_NVC_CD_DATA_ALLOC) |
| pp = pss->last_alloc; |
| else |
| pp = (uint8_t *)pss->name; |
| lwsl_info("SSHS_NVC_CD_DATA\n"); |
| |
| ch = ssh_get_server_ch(pss, pss->ch_recip); |
| switch (ch->type) { |
| case SSH_CH_TYPE_SCP: |
| scp = &ch->sub->scp; |
| switch (scp->ips) { |
| case SSHS_SCP_COLLECTSTR: |
| /* gather the ascii-coded headers */ |
| for (n = 0; n < (int)pss->npos; n++) |
| lwsl_notice("0x%02X %c\n", |
| pp[n], pp[n]); |
| |
| /* Header triggers the transfer? */ |
| if (pp[0] == 'C' && pp[pss->npos - 1] == '\x0a') { |
| while (*pp != ' ' && *pp != '\x0a') |
| pp++; |
| if (*pp++ != ' ') { |
| write_task(pss, ch, |
| SSH_WT_SCP_ACK_ERROR); |
| pss->parser_state = SSHS_MSG_EAT_PADDING; |
| break; |
| } |
| scp->len = (uint64_t)atoll((const char *)pp); |
| lwsl_notice("scp payload %llu expected\n", |
| (unsigned long long)scp->len); |
| scp->ips = SSHS_SCP_PAYLOADIN; |
| } |
| /* ack it */ |
| write_task(pss, pss->ch_temp, |
| SSH_WT_SCP_ACK_OKAY); |
| break; |
| case SSHS_SCP_PAYLOADIN: |
| /* the scp file payload */ |
| if (pss->vhd->ops) |
| pss->vhd->ops->rx(ch->priv, |
| pss->wsi, pp, pss->npos); |
| if (scp->len >= pss->npos) |
| scp->len -= pss->npos; |
| else |
| scp->len = 0; |
| if (!scp->len) { |
| lwsl_notice("scp txfer completed\n"); |
| scp->ips = SSHS_SCP_COLLECTSTR; |
| break; |
| } |
| break; |
| } |
| break; |
| default: /* scp payload */ |
| if (pss->vhd->ops) |
| pss->vhd->ops->rx(ch->priv, pss->wsi, |
| pp, pss->npos); |
| break; |
| } |
| if (pss->parser_state == SSHS_NVC_CD_DATA_ALLOC) |
| ssh_free_set_NULL(pss->last_alloc); |
| |
| if (ch->peer_window_est < 32768) { |
| write_task(pss, ch, SSH_WT_WINDOW_ADJUST); |
| ch->peer_window_est += 32768; |
| lwsl_info("extra peer WINDOW_ADJUST (~ %d)\n", |
| ch->peer_window_est); |
| } |
| |
| pss->parser_state = SSHS_MSG_EAT_PADDING; |
| break; |
| |
| case SSHS_NVC_WA_RECIP: |
| pss->ch_recip = pss->len; |
| state_get_u32(pss, SSHS_NVC_WA_ADD); |
| break; |
| |
| case SSHS_NVC_WA_ADD: |
| ch = ssh_get_server_ch(pss, pss->ch_recip); |
| if (ch) { |
| ch->window += (int32_t)pss->len; |
| lwsl_notice("got additional window %d (now %d)\n", |
| pss->len, ch->window); |
| } |
| pss->parser_state = SSHS_MSG_EAT_PADDING; |
| break; |
| |
| /* |
| * channel close |
| */ |
| |
| case SSHS_NVC_CH_EOF: |
| /* |
| * No explicit response is sent to this |
| * message. However, the application may send |
| * EOF to whatever is at the other end of the |
| * channel. Note that the channel remains open |
| * after this message, and more data may still |
| * be sent in the other direction. This message |
| * does not consume window space and can be sent |
| * even if no window space is available. |
| */ |
| lwsl_notice("SSH_MSG_CHANNEL_EOF: %d\n", pss->ch_recip); |
| ch = ssh_get_server_ch(pss, pss->ch_recip); |
| if (!ch) { |
| lwsl_notice("unknown ch %d\n", pss->ch_recip); |
| return -1; |
| } |
| |
| if (!ch->scheduled_close) { |
| lwsl_notice("scheduling CLOSE\n"); |
| ch->scheduled_close = 1; |
| write_task(pss, ch, SSH_WT_CH_CLOSE); |
| } |
| pss->parser_state = SSHS_MSG_EAT_PADDING; |
| break; |
| |
| case SSHS_NVC_CH_CLOSE: |
| /* |
| * When either party wishes to terminate the |
| * channel, it sends SSH_MSG_CHANNEL_CLOSE. |
| * Upon receiving this message, a party MUST |
| * send back an SSH_MSG_CHANNEL_CLOSE unless it |
| * has already sent this message for the |
| * channel. The channel is considered closed |
| * for a party when it has both sent and |
| * received SSH_MSG_CHANNEL_CLOSE, and the |
| * party may then reuse the channel number. |
| * A party MAY send SSH_MSG_CHANNEL_CLOSE |
| * without having sent or received |
| * SSH_MSG_CHANNEL_EOF. |
| */ |
| lwsl_notice("SSH_MSG_CHANNEL_CLOSE ch %d\n", |
| pss->ch_recip); |
| ch = ssh_get_server_ch(pss, pss->ch_recip); |
| if (!ch) |
| goto bail; |
| |
| pss->parser_state = SSHS_MSG_EAT_PADDING; |
| |
| if (ch->sent_close) { |
| /* |
| * This is acking our sent close... |
| * we can destroy the channel with no |
| * further communication. |
| */ |
| ssh_destroy_channel(pss, ch); |
| break; |
| } |
| |
| ch->received_close = 1; |
| ch->scheduled_close = 1; |
| write_task(pss, ch, SSH_WT_CH_CLOSE); |
| break; |
| |
| default: |
| break; |
| |
| chrq_fail: |
| lwsl_notice("chrq_fail\n"); |
| write_task(pss, pss->ch_temp, SSH_WT_CHRQ_FAILURE); |
| pss->parser_state = SSH_KEX_STATE_SKIP; |
| break; |
| |
| ch_fail: |
| if (pss->ch_temp) { |
| free(pss->ch_temp); |
| pss->ch_temp = NULL; |
| } |
| write_task(pss, pss->ch_temp, SSH_WT_CH_FAILURE); |
| pss->parser_state = SSH_KEX_STATE_SKIP; |
| break; |
| |
| #if !defined(MBEDTLS_VERSION_NUMBER) || MBEDTLS_VERSION_NUMBER < 0x03000000 |
| ua_fail1: |
| #endif |
| lws_genrsa_destroy(&ctx); |
| ua_fail: |
| write_task(pss, NULL, SSH_WT_UA_FAILURE); |
| ua_fail_silently: |
| lws_ua_destroy(pss); |
| /* Sect 4, RFC4252 |
| * |
| * Additionally, the implementation SHOULD limit the |
| * number of failed authentication attempts a client |
| * may perform in a single session (the RECOMMENDED |
| * limit is 20 attempts). If the threshold is |
| * exceeded, the server SHOULD disconnect. |
| */ |
| if (pss->count_auth_attempts++ > 20) |
| goto bail; |
| |
| pss->parser_state = SSH_KEX_STATE_SKIP; |
| break; |
| } |
| |
| pss->pos++; |
| } |
| |
| return 0; |
| bail: |
| lws_kex_destroy(pss); |
| lws_ua_destroy(pss); |
| |
| return SSH_DISCONNECT_KEY_EXCHANGE_FAILED; |
| } |
| |
| static int |
| parse(struct per_session_data__sshd *pss, uint8_t *p, size_t len) |
| { |
| while (len--) { |
| |
| if (pss->copy_to_I_C && pss->kex->I_C_payload_len < |
| pss->kex->I_C_alloc_len && |
| pss->parser_state != SSHS_MSG_EAT_PADDING) |
| pss->kex->I_C[pss->kex->I_C_payload_len++] = *p; |
| |
| if (pss->active_keys_cts.valid && |
| pss->parser_state == SSHS_MSG_LEN) |
| /* take a copy for full decrypt */ |
| pss->packet_assembly[pss->pa_pos++] = *p; |
| |
| if (pss->active_keys_cts.valid && |
| pss->parser_state == SSHS_MSG_PADDING && |
| pss->msg_len) { |
| /* we are going to have to decrypt it */ |
| uint32_t cp, l = pss->msg_len + 4 + |
| pss->active_keys_cts.MAC_length; |
| uint8_t pt[2048]; |
| |
| len++; |
| cp = (uint32_t)len; |
| |
| if (cp > l - pss->pa_pos) |
| cp = l - pss->pa_pos; |
| |
| if (cp > sizeof(pss->packet_assembly) - |
| pss->pa_pos) { |
| lwsl_err("Packet is too big to decrypt\n"); |
| |
| goto bail; |
| } |
| if (pss->msg_len < 2 + 4) { |
| lwsl_err("packet too small\n"); |
| |
| goto bail; |
| } |
| |
| memcpy(&pss->packet_assembly[pss->pa_pos], p, cp); |
| pss->pa_pos += cp; |
| len -= cp; |
| p += cp; |
| |
| if (pss->pa_pos != l) |
| return 0; |
| |
| /* decrypt it */ |
| cp = (uint32_t)lws_chacha_decrypt(&pss->active_keys_cts, |
| pss->ssh_sequence_ctr_cts++, |
| pss->packet_assembly, |
| pss->pa_pos, pt); |
| if (cp) { |
| lwsl_notice("Decryption failed: %d\n", cp); |
| goto bail; |
| } |
| |
| if (lws_ssh_parse_plaintext(pss, pt + 4, pss->msg_len)) |
| goto bail; |
| |
| pss->pa_pos = 0; |
| pss->ctr = 0; |
| continue; |
| } |
| |
| if (lws_ssh_parse_plaintext(pss, p, 1)) |
| goto bail; |
| |
| p++; |
| } |
| |
| return 0; |
| |
| bail: |
| lws_kex_destroy(pss); |
| lws_ua_destroy(pss); |
| |
| return SSH_DISCONNECT_KEY_EXCHANGE_FAILED; |
| } |
| |
| static uint32_t |
| pad_and_encrypt(uint8_t *dest, void *ps, uint8_t *pp, |
| struct per_session_data__sshd *pss, int skip_pad) |
| { |
| uint32_t n; |
| |
| if (!skip_pad) |
| lws_pad_set_length(pss, ps, &pp, &pss->active_keys_stc); |
| n = (uint32_t)lws_ptr_diff(pp, ps); |
| |
| if (!pss->active_keys_stc.valid) { |
| memcpy(dest, ps, n); |
| return n; |
| } |
| |
| lws_chacha_encrypt(&pss->active_keys_stc, pss->ssh_sequence_ctr_stc, |
| ps, n, dest); |
| n += pss->active_keys_stc.MAC_length; |
| |
| return n; |
| } |
| |
| static int |
| lws_callback_raw_sshd(struct lws *wsi, enum lws_callback_reasons reason, |
| void *user, void *in, size_t len) |
| { |
| struct per_session_data__sshd *pss = |
| (struct per_session_data__sshd *)user, **p; |
| struct per_vhost_data__sshd *vhd = NULL; |
| uint8_t buf[LWS_PRE + 1024], *pp, *ps = &buf[LWS_PRE + 512], *ps1 = NULL; |
| const struct lws_protocol_vhost_options *pvo; |
| const struct lws_protocols *prot; |
| struct lws_ssh_channel *ch; |
| char lang[10]; |
| int n, m, o; |
| |
| /* |
| * Because we are an abstract protocol plugin, we will get called by |
| * wsi that actually bind to a plugin "on top of us" that calls thru |
| * to our callback. |
| * |
| * Under those circumstances, we can't simply get a pointer to our own |
| * protocol from the wsi. If there's a pss already, we can get it from |
| * there, but the first time for each connection we have to look it up. |
| */ |
| if (pss && pss->vhd) |
| vhd = (struct per_vhost_data__sshd *) |
| lws_protocol_vh_priv_get(lws_get_vhost(wsi), |
| pss->vhd->protocol); |
| else |
| if (lws_get_vhost(wsi)) |
| vhd = (struct per_vhost_data__sshd *) |
| lws_protocol_vh_priv_get(lws_get_vhost(wsi), |
| lws_vhost_name_to_protocol( |
| lws_get_vhost(wsi), "lws-ssh-base")); |
| |
| switch ((int)reason) { |
| case LWS_CALLBACK_PROTOCOL_INIT: |
| vhd = lws_protocol_vh_priv_zalloc(lws_get_vhost(wsi), |
| lws_get_protocol(wsi), |
| sizeof(struct per_vhost_data__sshd)); |
| if (!vhd) |
| return 0; |
| vhd->context = lws_get_context(wsi); |
| vhd->protocol = lws_get_protocol(wsi); |
| vhd->vhost = lws_get_vhost(wsi); |
| |
| pvo = (const struct lws_protocol_vhost_options *)in; |
| while (pvo) { |
| /* |
| * the user code passes the ops struct address to us |
| * using a pvo (per-vhost option) |
| */ |
| if (!strcmp(pvo->name, "ops")) |
| vhd->ops = (const struct lws_ssh_ops *)pvo->value; |
| |
| /* |
| * the user code is telling us to get the ops struct |
| * from another protocol's protocol.user pointer |
| */ |
| if (!strcmp(pvo->name, "ops-from")) { |
| prot = lws_vhost_name_to_protocol(vhd->vhost, |
| pvo->value); |
| if (prot) |
| vhd->ops = (const struct lws_ssh_ops *)prot->user; |
| else |
| lwsl_err("%s: can't find protocol %s\n", |
| __func__, pvo->value); |
| } |
| |
| pvo = pvo->next; |
| } |
| |
| if (!vhd->ops) { |
| lwsl_warn("ssh pvo \"ops\" is mandatory\n"); |
| return 0; |
| } |
| /* |
| * The user code ops api_version has to be current |
| */ |
| if (vhd->ops->api_version != LWS_SSH_OPS_VERSION) { |
| lwsl_err("FATAL ops is api_version v%d but code is v%d\n", |
| vhd->ops->api_version, LWS_SSH_OPS_VERSION); |
| return 1; |
| } |
| break; |
| |
| case LWS_CALLBACK_RAW_ADOPT: |
| lwsl_info("LWS_CALLBACK_RAW_ADOPT\n"); |
| if (!vhd) |
| return -1; |
| pss->next = vhd->live_pss_list; |
| vhd->live_pss_list = pss; |
| pss->parser_state = SSH_INITIALIZE_TRANSIENT; |
| pss->wsi = wsi; |
| pss->vhd = vhd; |
| pss->kex_state = KEX_STATE_EXPECTING_CLIENT_OFFER; |
| pss->active_keys_cts.padding_alignment = 8; |
| pss->active_keys_stc.padding_alignment = 8; |
| if (lws_kex_create(pss)) |
| return -1; |
| write_task(pss, NULL, SSH_WT_VERSION); |
| |
| /* sect 4 RFC4252 |
| * |
| * The server SHOULD have a timeout for authentication and |
| * disconnect if the authentication has not been accepted |
| * within the timeout period. |
| * |
| * The RECOMMENDED timeout period is 10 minutes. |
| */ |
| lws_set_timeout(wsi, (enum pending_timeout) |
| SSH_PENDING_TIMEOUT_CONNECT_TO_SUCCESSFUL_AUTH, 10 * 60); |
| break; |
| |
| case LWS_CALLBACK_RAW_CLOSE: |
| if (!pss) |
| return -1; |
| lwsl_info("LWS_CALLBACK_RAW_CLOSE\n"); |
| lws_kex_destroy(pss); |
| lws_ua_destroy(pss); |
| |
| ssh_free_set_NULL(pss->last_alloc); |
| |
| while (pss->ch_list) |
| ssh_destroy_channel(pss, pss->ch_list); |
| |
| lws_chacha_destroy(&pss->active_keys_cts); |
| lws_chacha_destroy(&pss->active_keys_stc); |
| |
| p = &vhd->live_pss_list; |
| |
| while (*p) { |
| if ((*p) == pss) { |
| *p = pss->next; |
| continue; |
| } |
| p = &((*p)->next); |
| } |
| break; |
| |
| case LWS_CALLBACK_RAW_RX: |
| if (!pss) |
| return -1; |
| if (parse(pss, in, len)) |
| return -1; |
| break; |
| |
| case LWS_CALLBACK_RAW_WRITEABLE: |
| if (!pss) |
| break; |
| n = 0; |
| o = pss->write_task[pss->wt_tail]; |
| ch = pss->write_channel[pss->wt_tail]; |
| |
| if (pss->wt_head == pss->wt_tail) |
| o = SSH_WT_NONE; |
| |
| switch (o) { |
| case SSH_WT_VERSION: |
| if (!pss->vhd) |
| break; |
| n = lws_snprintf((char *)buf + LWS_PRE, |
| sizeof(buf) - LWS_PRE - 1, "%s\r\n", |
| pss->vhd->ops->server_string); |
| write_task(pss, NULL, SSH_WT_OFFER); |
| break; |
| |
| case SSH_WT_OFFER: |
| if (!pss->vhd) |
| break; |
| m = 0; |
| n = (int)offer(pss, buf + LWS_PRE, |
| sizeof(buf) - LWS_PRE, 0, &m); |
| if (n == 0) { |
| lwsl_notice("Too small\n"); |
| |
| return -1; |
| } |
| |
| if (!pss->kex) { |
| lwsl_notice("%s: SSH_WT_OFFER: pss->kex is NULL\n", |
| __func__); |
| return -1; |
| } |
| |
| /* we need a copy of it to generate the hash later */ |
| if (pss->kex->I_S) |
| free(pss->kex->I_S); |
| pss->kex->I_S = sshd_zalloc((unsigned int)m); |
| if (!pss->kex->I_S) { |
| lwsl_notice("OOM 5: %d\n", m); |
| |
| return -1; |
| } |
| /* without length + padcount part */ |
| memcpy(pss->kex->I_S, buf + LWS_PRE + 5, (unsigned int)m); |
| pss->kex->I_S_payload_len = (uint32_t)m; /* without padding */ |
| break; |
| |
| case SSH_WT_OFFER_REPLY: |
| memcpy(ps, pss->kex->kex_r, pss->kex->kex_r_len); |
| n = (int)pad_and_encrypt(&buf[LWS_PRE], ps, |
| ps + pss->kex->kex_r_len, pss, 1); |
| pss->kex_state = KEX_STATE_REPLIED_TO_OFFER; |
| /* afterwards, must do newkeys */ |
| write_task(pss, NULL, SSH_WT_SEND_NEWKEYS); |
| break; |
| |
| case SSH_WT_SEND_NEWKEYS: |
| pp = ps + 5; |
| *pp++ = SSH_MSG_NEWKEYS; |
| goto pac; |
| |
| case SSH_WT_UA_ACCEPT: |
| /* |
| * If the server supports the service (and permits |
| * the client to use it), it MUST respond with the |
| * following: |
| * |
| * byte SSH_MSG_SERVICE_ACCEPT |
| * string service name |
| */ |
| pp = ps + 5; |
| *pp++ = SSH_MSG_SERVICE_ACCEPT; |
| lws_p32(pp, pss->npos); |
| pp += 4; |
| strcpy((char *)pp, pss->name); |
| pp += pss->npos; |
| goto pac; |
| |
| case SSH_WT_UA_FAILURE: |
| pp = ps + 5; |
| *pp++ = SSH_MSG_USERAUTH_FAILURE; |
| lws_p32(pp, 9); |
| pp += 4; |
| strcpy((char *)pp, "publickey"); |
| pp += 9; |
| *pp++ = 0; |
| goto pac; |
| |
| case SSH_WT_UA_BANNER: |
| pp = ps + 5; |
| *pp++ = SSH_MSG_USERAUTH_BANNER; |
| if (pss->vhd && pss->vhd->ops->banner) |
| n = (int)pss->vhd->ops->banner((char *)&buf[650], |
| 150 - 1, |
| lang, (int)sizeof(lang)); |
| lws_p32(pp, (uint32_t)n); |
| pp += 4; |
| strcpy((char *)pp, (char *)&buf[650]); |
| pp += n; |
| if (lws_cstr(&pp, lang, sizeof(lang))) |
| goto bail; |
| goto pac; |
| |
| case SSH_WT_UA_PK_OK: |
| /* |
| * The server MUST respond to this message with |
| * either SSH_MSG_USERAUTH_FAILURE or with the |
| * following: |
| * |
| * byte SSH_MSG_USERAUTH_PK_OK |
| * string public key alg name from the request |
| * string public key blob from the request |
| */ |
| n = 74 + (int)pss->ua->pubkey_len; |
| if (n > (int)sizeof(buf) - LWS_PRE) { |
| lwsl_notice("pubkey too large\n"); |
| goto bail; |
| } |
| ps1 = sshd_zalloc((unsigned int)n); |
| if (!ps1) |
| goto bail; |
| ps = ps1; |
| pp = ps1 + 5; |
| *pp++ = SSH_MSG_USERAUTH_PK_OK; |
| if (lws_cstr(&pp, pss->ua->alg, 64)) { |
| free(ps1); |
| goto bail; |
| } |
| lws_p32(pp, pss->ua->pubkey_len); |
| pp += 4; |
| memcpy(pp, pss->ua->pubkey, pss->ua->pubkey_len); |
| pp += pss->ua->pubkey_len; |
| |
| /* we no longer need the UA now we judged it */ |
| lws_ua_destroy(pss); |
| |
| goto pac; |
| |
| case SSH_WT_UA_SUCCESS: |
| pp = ps + 5; |
| *pp++ = SSH_MSG_USERAUTH_SUCCESS; |
| /* end SSH_PENDING_TIMEOUT_CONNECT_TO_SUCCESSFUL_AUTH */ |
| lws_set_timeout(wsi, NO_PENDING_TIMEOUT, 0); |
| goto pac; |
| |
| case SSH_WT_CH_OPEN_CONF: |
| pp = ps + 5; |
| *pp++ = SSH_MSG_CHANNEL_OPEN_CONFIRMATION; |
| lws_p32(pp, pss->ch_temp->server_ch); |
| pp += 4; |
| lws_p32(pp, pss->ch_temp->sender_ch); |
| pp += 4; |
| /* tx initial window size towards us */ |
| lws_p32(pp, LWS_SSH_INITIAL_WINDOW); |
| pp += 4; |
| /* maximum packet size towards us */ |
| lws_p32(pp, 800); |
| pp += 4; |
| lwsl_info("SSH_WT_CH_OPEN_CONF\n"); |
| /* it's on the linked-list */ |
| pss->ch_temp = NULL; |
| goto pac; |
| |
| case SSH_WT_CH_FAILURE: |
| pp = ps + 5; |
| *pp++ = SSH_MSG_CHANNEL_OPEN_FAILURE; |
| lws_p32(pp, ch->server_ch); |
| pp += 4; |
| lws_p32(pp, ch->sender_ch); |
| pp += 4; |
| lws_cstr(&pp, "reason", 64); |
| lws_cstr(&pp, "en/US", 64); |
| lwsl_info("SSH_WT_CH_FAILURE\n"); |
| goto pac; |
| |
| case SSH_WT_CHRQ_SUCC: |
| pp = ps + 5; |
| *pp++ = SSH_MSG_CHANNEL_SUCCESS; |
| lws_p32(pp, ch->server_ch); |
| lwsl_info("SSH_WT_CHRQ_SUCC\n"); |
| pp += 4; |
| goto pac; |
| |
| case SSH_WT_CHRQ_FAILURE: |
| pp = ps + 5; |
| *pp++ = SSH_MSG_CHANNEL_FAILURE; |
| lws_p32(pp, ch->server_ch); |
| pp += 4; |
| lwsl_info("SSH_WT_CHRQ_FAILURE\n"); |
| goto pac; |
| |
| case SSH_WT_CH_CLOSE: |
| pp = ps + 5; |
| *pp++ = SSH_MSG_CHANNEL_CLOSE; |
| lws_p32(pp, ch->server_ch); |
| lwsl_info("SSH_WT_CH_CLOSE\n"); |
| pp += 4; |
| goto pac; |
| |
| case SSH_WT_CH_EOF: |
| pp = ps + 5; |
| *pp++ = SSH_MSG_CHANNEL_EOF; |
| lws_p32(pp, ch->server_ch); |
| lwsl_info("SSH_WT_CH_EOF\n"); |
| pp += 4; |
| goto pac; |
| |
| case SSH_WT_SCP_ACK_ERROR: |
| case SSH_WT_SCP_ACK_OKAY: |
| pp = ps + 5; |
| *pp++ = SSH_MSG_CHANNEL_DATA; |
| /* ps + 6 */ |
| lws_p32(pp, ch->sender_ch); |
| pp += 4; |
| lws_p32(pp, 1); |
| pp += 4; |
| if (o == SSH_WT_SCP_ACK_ERROR) |
| *pp++ = 2; |
| else |
| *pp++ = 0; |
| lwsl_info("SSH_WT_SCP_ACK_OKAY\n"); |
| goto pac; |
| |
| case SSH_WT_WINDOW_ADJUST: |
| pp = ps + 5; |
| *pp++ = SSH_MSG_CHANNEL_WINDOW_ADJUST; |
| /* ps + 6 */ |
| lws_p32(pp, ch->sender_ch); |
| pp += 4; |
| lws_p32(pp, 32768); |
| pp += 4; |
| lwsl_info("send SSH_MSG_CHANNEL_WINDOW_ADJUST\n"); |
| goto pac; |
| |
| case SSH_WT_EXIT_STATUS: |
| pp = ps + 5; |
| *pp++ = SSH_MSG_CHANNEL_REQUEST; |
| lws_p32(pp, ch->sender_ch); |
| pp += 4; |
| lws_p32(pp, 11); |
| pp += 4; |
| strcpy((char *)pp, "exit-status"); |
| pp += 11; |
| *pp++ = 0; |
| lws_p32(pp, (uint32_t)ch->retcode); |
| pp += 4; |
| lwsl_info("send SSH_MSG_CHANNEL_EXIT_STATUS\n"); |
| goto pac; |
| |
| case SSH_WT_NONE: |
| default: |
| /* sending payload */ |
| |
| ch = ssh_get_server_ch(pss, 0); |
| /* have a channel up to send on? */ |
| if (!ch) |
| break; |
| |
| if (!pss->vhd || !pss->vhd->ops) |
| break; |
| n = pss->vhd->ops->tx_waiting(ch->priv); |
| if (n < 0) |
| return -1; |
| if (!n) |
| /* nothing to send */ |
| break; |
| |
| if (n == (LWS_STDOUT | LWS_STDERR)) { |
| /* pick one using round-robin */ |
| if (pss->serviced_stderr_last) |
| n = LWS_STDOUT; |
| else |
| n = LWS_STDERR; |
| } |
| |
| pss->serviced_stderr_last = !!(n & LWS_STDERR); |
| |
| /* stdout or stderr */ |
| pp = ps + 5; |
| if (n == LWS_STDOUT) |
| *pp++ = SSH_MSG_CHANNEL_DATA; |
| else |
| *pp++ = SSH_MSG_CHANNEL_EXTENDED_DATA; |
| /* ps + 6 */ |
| lws_p32(pp, pss->ch_list->server_ch); |
| m = 14; |
| if (n == LWS_STDERR) { |
| pp += 4; |
| /* data type code... 1 for stderr payload */ |
| lws_p32(pp, SSH_EXTENDED_DATA_STDERR); |
| m = 18; |
| } |
| /* also skip another strlen u32 at + 10 / +14 */ |
| pp += 8; |
| /* ps + 14 / + 18 */ |
| |
| pp += pss->vhd->ops->tx(ch->priv, n, pp, |
| lws_ptr_diff_size_t( |
| &buf[sizeof(buf) - 1], pp)); |
| |
| lws_p32(ps + m - 4, (uint32_t)lws_ptr_diff(pp, (ps + m))); |
| |
| if (pss->vhd->ops->tx_waiting(ch->priv) > 0) |
| lws_callback_on_writable(wsi); |
| |
| ch->window -= lws_ptr_diff(pp, ps) - m; |
| //lwsl_debug("our send window: %d\n", ch->window); |
| |
| /* fallthru */ |
| pac: |
| if (!pss->vhd) |
| break; |
| n = (int)pad_and_encrypt(&buf[LWS_PRE], ps, pp, pss, 0); |
| break; |
| |
| bail: |
| lws_ua_destroy(pss); |
| lws_kex_destroy(pss); |
| |
| return 1; |
| |
| } |
| |
| if (n > 0) { |
| m = lws_write(wsi, (unsigned char *)buf + LWS_PRE, (unsigned int)n, |
| LWS_WRITE_HTTP); |
| |
| switch(o) { |
| case SSH_WT_SEND_NEWKEYS: |
| lwsl_info("Activating STC keys\n"); |
| pss->active_keys_stc = pss->kex->keys_next_stc; |
| lws_chacha_activate(&pss->active_keys_stc); |
| pss->kex_state = KEX_STATE_CRYPTO_INITIALIZED; |
| pss->kex->newkeys |= 1; |
| if (pss->kex->newkeys == 3) |
| lws_kex_destroy(pss); |
| break; |
| case SSH_WT_UA_PK_OK: |
| free(ps1); |
| break; |
| case SSH_WT_CH_CLOSE: |
| if (ch->received_close) { |
| /* |
| * We are sending this at the behest of |
| * the remote peer... |
| * we can destroy the channel with no |
| * further communication. |
| */ |
| ssh_destroy_channel(pss, ch); |
| break; |
| } |
| ch->sent_close = 1; |
| break; |
| } |
| if (m < 0) { |
| lwsl_err("ERR %d from write\n", m); |
| goto bail; |
| } |
| |
| if (o != SSH_WT_VERSION) |
| pss->ssh_sequence_ctr_stc++; |
| |
| if (o != SSH_WT_NONE) |
| pss->wt_tail = |
| (pss->wt_tail + 1) & 7; |
| } else |
| if (o == SSH_WT_UA_PK_OK) /* free it either way */ |
| free(ps1); |
| |
| ch = ssh_get_server_ch(pss, 0); |
| |
| if (pss->wt_head != pss->wt_tail || |
| (ch && ch->priv && pss->vhd && |
| pss->vhd->ops->tx_waiting(ch->priv))) |
| lws_callback_on_writable(wsi); |
| |
| break; |
| |
| case LWS_CALLBACK_SSH_UART_SET_RXFLOW: |
| /* |
| * this is sent to set rxflow state on any connections that |
| * sink on a particular sink. The sink index affected is in len |
| * |
| * More than one protocol may sink to the same uart, and the |
| * protocol may select the sink itself, eg, in the URL used |
| * to set up the connection. |
| */ |
| lwsl_notice("sshd LWS_CALLBACK_SSH_UART_SET_RXFLOW: wsi %p, %d\n", |
| wsi, (int)len & 1); |
| lws_rx_flow_control(wsi, len & 1); |
| break; |
| |
| case LWS_CALLBACK_CGI: |
| if (!pss) |
| break; |
| if (pss->vhd && pss->vhd->ops && |
| pss->vhd->ops->child_process_io && |
| pss->vhd->ops->child_process_io(pss->ch_temp->priv, |
| pss->wsi, (struct lws_cgi_args *)in)) |
| return -1; |
| break; |
| |
| case LWS_CALLBACK_CGI_PROCESS_ATTACH: |
| if (!pss) |
| break; |
| ch = ssh_get_server_ch(pss, pss->channel_doing_spawn); |
| if (ch) { |
| ch->spawn_pid = (uint32_t)len; /* child process PID */ |
| lwsl_notice("associated PID %d to ch %d\n", (int)len, |
| pss->channel_doing_spawn); |
| } |
| break; |
| |
| case LWS_CALLBACK_CGI_TERMINATED: |
| if (!pss) |
| break; |
| if (pss->vhd && pss->vhd->ops && |
| pss->vhd->ops->child_process_terminated) |
| pss->vhd->ops->child_process_terminated(pss->ch_temp->priv, |
| pss->wsi); |
| /* |
| * we have the child PID in len... we need to match it to a |
| * channel that is on the wsi |
| */ |
| ch = pss->ch_list; |
| |
| while (ch) { |
| if (ch->spawn_pid == len) { |
| lwsl_notice("starting close of ch with PID %d\n", |
| (int)len); |
| ch->scheduled_close = 1; |
| write_task(pss, ch, SSH_WT_CH_CLOSE); |
| break; |
| } |
| ch = ch->next; |
| } |
| break; |
| |
| default: |
| break; |
| } |
| |
| return 0; |
| } |
| |
| #define LWS_PLUGIN_PROTOCOL_LWS_RAW_SSHD { \ |
| "lws-ssh-base", \ |
| lws_callback_raw_sshd, \ |
| sizeof(struct per_session_data__sshd), \ |
| 1024, 0, NULL, 900 \ |
| } |
| |
| LWS_VISIBLE const struct lws_protocols lws_ssh_base_protocols[] = { |
| LWS_PLUGIN_PROTOCOL_LWS_RAW_SSHD, |
| { NULL, NULL, 0, 0, 0, NULL, 0 } /* terminator */ |
| }; |
| |
| #if !defined (LWS_PLUGIN_STATIC) |
| |
| LWS_VISIBLE const lws_plugin_protocol_t lws_ssh_base = { |
| .hdr = { |
| "ssh base", |
| "lws_protocol_plugin", |
| LWS_BUILD_HASH, |
| LWS_PLUGIN_API_MAGIC |
| }, |
| |
| .protocols = lws_ssh_base_protocols, |
| .count_protocols = LWS_ARRAY_SIZE(lws_ssh_base_protocols), |
| .extensions = NULL, |
| .count_extensions = 0, |
| }; |
| |
| #endif |