mirror of
https://github.com/libssh2/libssh2.git
synced 2026-10-24 03:48:58 +03:00
ssh2_dh_secret() in the Windows CNG backend uses the server's
Diffie-Hellman public value f without the range check the other four
backends apply through ssh2_dh_validate(), so a malicious server can
send f of 0, 1 or p-1 and push the shared secret into a trivial subgroup
that openssl, libgcrypt, mbedtls and os400qc3 all reject with
LIBSSH2_ERROR_PROTO. This adds a WinCNG ssh2_dh_validate() that enforces
1 < f < p-1 and the same minimum-set-bits check on the big-endian value,
and calls it at the top of ssh2_dh_secret() like the other backends do.
The shared test_ssh2_dh_validate() in tests/test_simple.c was stubbed
out on WinCNG only because the function did not exist, so it now runs
against the real implementation too.
Reported-by: yaotushaozhu on github
Ref: GHSA-m9rg-h9jv-9gvh
Follow-up to fcf830788d #2239
Closes #2645
407 lines
12 KiB
C
407 lines
12 KiB
C
/* Copyright (C) The Written Word, Inc.
|
|
* Copyright (C) Simon Josefsson
|
|
* All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions are met:
|
|
*
|
|
* 1. Redistributions of source code must retain the above copyright notice,
|
|
* this list of conditions and the following disclaimer.
|
|
*
|
|
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
* this list of conditions and the following disclaimer in the documentation
|
|
* and/or other materials provided with the distribution.
|
|
*
|
|
* 3. Neither the name of the copyright holder nor the names of its
|
|
* contributors may be used to endorse or promote products derived from this
|
|
* software without specific prior written permission.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
|
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
* POSSIBILITY OF SUCH DAMAGE.
|
|
*
|
|
* SPDX-License-Identifier: BSD-3-Clause
|
|
*/
|
|
|
|
#include "libssh2_priv.h"
|
|
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <stdlib.h> /* for atoi() */
|
|
|
|
static int test_ssh2_base64_decode(LIBSSH2_SESSION *session)
|
|
{
|
|
char *data;
|
|
size_t datalen;
|
|
const char *src = "Zm5vcmQ=";
|
|
int ret;
|
|
|
|
ret = ssh2_base64_decode(session, &data, &datalen, src, strlen(src));
|
|
if(ret)
|
|
return ret;
|
|
|
|
if(datalen != 5 || strcmp(data, "fnord")) {
|
|
fprintf(stderr, "ssh2_base64_decode() failed (%d, %.*s)\n",
|
|
(int)datalen, (int)datalen, data);
|
|
return 1;
|
|
}
|
|
|
|
free(data);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int test_knownhost_ipv6_case(LIBSSH2_SESSION *session,
|
|
const char *stored,
|
|
const char *checked,
|
|
int port,
|
|
int expected)
|
|
{
|
|
static const char key[] = "AAAA";
|
|
const int typemask =
|
|
LIBSSH2_KNOWNHOST_TYPE_PLAIN |
|
|
LIBSSH2_KNOWNHOST_KEYENC_BASE64 |
|
|
LIBSSH2_KNOWNHOST_KEY_SSHRSA;
|
|
LIBSSH2_KNOWNHOSTS *hosts;
|
|
int rc;
|
|
|
|
hosts = libssh2_knownhost_init(session);
|
|
if(!hosts) {
|
|
fprintf(stderr, "libssh2_knownhost_init() failed\n");
|
|
return 1;
|
|
}
|
|
|
|
rc = libssh2_knownhost_addc(hosts, stored, NULL, key, strlen(key),
|
|
NULL, 0, typemask, NULL);
|
|
if(rc) {
|
|
fprintf(stderr, "libssh2_knownhost_addc(%s) failed: %d\n",
|
|
stored, rc);
|
|
libssh2_knownhost_free(hosts);
|
|
return 1;
|
|
}
|
|
|
|
if(port >= 0)
|
|
rc = libssh2_knownhost_checkp(hosts, checked, port, key,
|
|
strlen(key), typemask, NULL);
|
|
else
|
|
rc = libssh2_knownhost_check(hosts, checked, key, strlen(key),
|
|
typemask, NULL);
|
|
|
|
libssh2_knownhost_free(hosts);
|
|
|
|
if(rc != expected) {
|
|
fprintf(stderr,
|
|
"knownhost IPv6: stored=%s checked=%s port=%d "
|
|
"expected=%d got=%d\n",
|
|
stored, checked, port, expected, rc);
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int test_knownhost_ipv6(LIBSSH2_SESSION *session)
|
|
{
|
|
int err = 0;
|
|
|
|
err |= test_knownhost_ipv6_case(
|
|
session,
|
|
"fd5f:4268:2387:97c1:0000:0000:0000:0002",
|
|
"fd5f:4268:2387:97c1::2",
|
|
-1, LIBSSH2_KNOWNHOST_CHECK_MATCH);
|
|
|
|
err |= test_knownhost_ipv6_case(
|
|
session,
|
|
"fd5f:4268:2387:97c1::2",
|
|
"fd5f:4268:2387:97c1:0:0:0:2",
|
|
-1, LIBSSH2_KNOWNHOST_CHECK_MATCH);
|
|
|
|
err |= test_knownhost_ipv6_case(
|
|
session,
|
|
"0:0:0:0:0:ffff:c000:0201",
|
|
"::ffff:192.0.2.1",
|
|
-1, LIBSSH2_KNOWNHOST_CHECK_MATCH);
|
|
|
|
err |= test_knownhost_ipv6_case(
|
|
session,
|
|
"[fd5f:4268:2387:97c1:0000:0000:0000:0002]:2222",
|
|
"fd5f:4268:2387:97c1::2",
|
|
2222, LIBSSH2_KNOWNHOST_CHECK_MATCH);
|
|
|
|
err |= test_knownhost_ipv6_case(
|
|
session,
|
|
"[fd5f:4268:2387:97c1:0000:0000:0000:0002]:2222",
|
|
"fd5f:4268:2387:97c1::2",
|
|
22, LIBSSH2_KNOWNHOST_CHECK_NOTFOUND);
|
|
|
|
err |= test_knownhost_ipv6_case(
|
|
session,
|
|
"fd5f:4268:2387:97c1::2",
|
|
"fd5f:4268:2387:97c1::3",
|
|
-1, LIBSSH2_KNOWNHOST_CHECK_NOTFOUND);
|
|
|
|
err |= test_knownhost_ipv6_case(
|
|
session,
|
|
"fd5f:4268:2387:97c1::2",
|
|
"fd5f:4268:2387:97c1::1::2",
|
|
-1, LIBSSH2_KNOWNHOST_CHECK_NOTFOUND);
|
|
|
|
err |= test_knownhost_ipv6_case(
|
|
session,
|
|
"example.com",
|
|
"example.com",
|
|
-1, LIBSSH2_KNOWNHOST_CHECK_MATCH);
|
|
|
|
err |= test_knownhost_ipv6_case(
|
|
session,
|
|
"example.com",
|
|
"EXAMPLE.COM",
|
|
-1, LIBSSH2_KNOWNHOST_CHECK_NOTFOUND);
|
|
|
|
/* Hexadecimal digits are case-insensitive. */
|
|
err |= test_knownhost_ipv6_case(
|
|
session,
|
|
"2001:0DB8:0000:0000:0000:0000:0000:0001",
|
|
"2001:db8::1",
|
|
-1, LIBSSH2_KNOWNHOST_CHECK_MATCH);
|
|
|
|
/* Compression may represent the complete address. */
|
|
err |= test_knownhost_ipv6_case(
|
|
session,
|
|
"0:0:0:0:0:0:0:0",
|
|
"::",
|
|
-1, LIBSSH2_KNOWNHOST_CHECK_MATCH);
|
|
|
|
/* Compression at the end must expand to the remaining words. */
|
|
err |= test_knownhost_ipv6_case(
|
|
session,
|
|
"2001:db8:1:2:3:4:0:0",
|
|
"2001:db8:1:2:3:4::",
|
|
-1, LIBSSH2_KNOWNHOST_CHECK_MATCH);
|
|
|
|
/* More than eight words is not a valid IPv6 literal. */
|
|
err |= test_knownhost_ipv6_case(
|
|
session,
|
|
"2001:db8::1",
|
|
"1:2:3:4:5:6:7:8:9",
|
|
-1, LIBSSH2_KNOWNHOST_CHECK_NOTFOUND);
|
|
|
|
/* An embedded IPv4 address must contain valid octets. */
|
|
err |= test_knownhost_ipv6_case(
|
|
session,
|
|
"::ffff:192.0.2.1",
|
|
"::ffff:192.0.2.256",
|
|
-1, LIBSSH2_KNOWNHOST_CHECK_NOTFOUND);
|
|
|
|
/* Embedded IPv4 octets with leading zeros are not valid. */
|
|
err |= test_knownhost_ipv6_case(
|
|
session,
|
|
"::ffff:192.168.001.1",
|
|
"::ffff:192.168.1.1",
|
|
-1, LIBSSH2_KNOWNHOST_CHECK_NOTFOUND);
|
|
|
|
/* Identical malformed names retain the historical exact-string match. */
|
|
err |= test_knownhost_ipv6_case(
|
|
session,
|
|
"::ffff:192.168.001.1",
|
|
"::ffff:192.168.001.1",
|
|
-1, LIBSSH2_KNOWNHOST_CHECK_MATCH);
|
|
|
|
/*
|
|
* Preserve the historical exact-string behavior even when a plain host
|
|
* containing colons is not a valid IPv6 literal.
|
|
*/
|
|
err |= test_knownhost_ipv6_case(
|
|
session,
|
|
"fd5f::1::2",
|
|
"fd5f::1::2",
|
|
-1, LIBSSH2_KNOWNHOST_CHECK_MATCH);
|
|
|
|
return err > 0;
|
|
}
|
|
|
|
static int test_ssh2_dh_validate(void)
|
|
{
|
|
struct tbn {
|
|
const char *f; const char *p; int expected;
|
|
};
|
|
static const struct tbn tests[] = {
|
|
{ "2", "10", -3 },
|
|
{ "1", "10", -1 },
|
|
{ "0", "10", -1 },
|
|
{ "9", "10", -2 }, /* f=9; p=10 => p-2=8, f>8 */
|
|
{ "6", "20", -3 }, /* f=6 (110); p=20 => p-2=18, 6<=18 */
|
|
{ "27", "100", 0 }, /* f=27 (11011); p=100 => p-2=98, 27<=98 */
|
|
{ "240", "242", 0 }, /* f=p-2, f=240 (11110000); p=f+2=242 */
|
|
};
|
|
|
|
size_t i;
|
|
int err = 0;
|
|
|
|
for(i = 0; i < SSH2_ARRAYSIZE(tests); i++) {
|
|
struct tbn t = tests[i];
|
|
int got;
|
|
ssh2_bn *f = ssh2_bn_init();
|
|
ssh2_bn *p = ssh2_bn_init();
|
|
if(!f || !p ||
|
|
ssh2_bn_set_word(f, (uint32_t)atoi(t.f)) ||
|
|
ssh2_bn_set_word(p, (uint32_t)atoi(t.p)))
|
|
got = -9;
|
|
else
|
|
got = ssh2_dh_validate(f, p);
|
|
ssh2_bn_free(f);
|
|
ssh2_bn_free(p);
|
|
if(got != t.expected) {
|
|
fprintf(stderr,
|
|
"ssh2_dh_validate/%lu: f=%s p=%s: expected %d got %d\n",
|
|
(unsigned long)i, t.f, t.p, t.expected, got);
|
|
err++;
|
|
}
|
|
}
|
|
|
|
return err > 0;
|
|
}
|
|
|
|
static int test_ssh2_bn_from_bin(void)
|
|
{
|
|
static const struct {
|
|
unsigned char input[5];
|
|
size_t length;
|
|
size_t leading;
|
|
} tests[] = {
|
|
{ { 1 }, 1, 0 },
|
|
{ { 0x7f }, 1, 0 },
|
|
{ { 0x80 }, 1, 0 },
|
|
{ { 0xff, 0x42 }, 2, 0 },
|
|
{ { 0, 1 }, 2, 1 },
|
|
{ { 0, 0x80 }, 2, 1 },
|
|
{ { 0, 0, 0x80, 0x42 }, 4, 2 },
|
|
{ { 0, 0, 0, 0, 1 }, 5, 4 },
|
|
};
|
|
size_t i;
|
|
int rc = 0;
|
|
|
|
for(i = 0; i < SSH2_ARRAYSIZE(tests); i++) {
|
|
unsigned char actual[5] = { 0 };
|
|
size_t length = tests[i].length - tests[i].leading;
|
|
ssh2_bn *bn = NULL;
|
|
if(ssh2_bn_from_bin(&bn, tests[i].input, tests[i].length) ||
|
|
ssh2_bn_bytes(bn) != length ||
|
|
ssh2_bn_to_bin(bn, actual) ||
|
|
memcmp(actual, tests[i].input + tests[i].leading, length)) {
|
|
fprintf(stderr, "ssh2_bn_from_bin case %lu failed\n",
|
|
(unsigned long)i);
|
|
rc = 1;
|
|
}
|
|
ssh2_bn_free(bn);
|
|
}
|
|
return rc;
|
|
}
|
|
|
|
/* Return codes match scp.c (SCP_C_FIELDS_*). */
|
|
static int test_ssh2_scp_parse_c_fields(void)
|
|
{
|
|
long mode = -1;
|
|
libssh2_int64_t size = -1;
|
|
char long_line[SSH2_SCP_RESPONSE_BUFLEN];
|
|
int prefix_len;
|
|
int prc;
|
|
int err = 0;
|
|
|
|
/* Normal complete line with short name */
|
|
prc = ssh2_scp_parse_c_fields("C0644 123 shortname\n", 20, &mode, &size);
|
|
if(prc || mode != 420L || size != 123) { /* 0644 octal == 420 */
|
|
fprintf(stderr, "scp_parse short: prc=%d mode=%ld size=%lu\n",
|
|
prc, mode, (unsigned long)size);
|
|
err++;
|
|
}
|
|
|
|
/* Fields complete without trailing newline (name unfinished) */
|
|
mode = -1;
|
|
size = -1;
|
|
prc = ssh2_scp_parse_c_fields("C0755 42 ", 9, &mode, &size);
|
|
if(prc || mode != 493L || size != 42) { /* 0755 octal == 493 */
|
|
fprintf(stderr, "scp_parse partial-name: prc=%d mode=%ld size=%lu\n",
|
|
prc, mode, (unsigned long)size);
|
|
err++;
|
|
}
|
|
|
|
/* Incomplete size digits still growing */
|
|
prc = ssh2_scp_parse_c_fields("C0644 12", 8, &mode, &size);
|
|
if(prc != 1) {
|
|
fprintf(stderr, "scp_parse incomplete size: prc=%d (want 1)\n", prc);
|
|
err++;
|
|
}
|
|
|
|
/*
|
|
* Fixed buffer full of "Cmode size " + long name without newline. Mode and
|
|
* size must still parse so scp_recv can drain the unused basename.
|
|
*/
|
|
prefix_len = snprintf(long_line, sizeof(long_line), "C0644 99 ");
|
|
if(prefix_len < 0 || (size_t)prefix_len >= sizeof(long_line)) {
|
|
fprintf(stderr, "scp_parse long-name: snprintf failed (%d)\n",
|
|
prefix_len);
|
|
return 1;
|
|
}
|
|
memset(long_line + prefix_len, 'a',
|
|
sizeof(long_line) - (size_t)prefix_len);
|
|
mode = -1;
|
|
size = -1;
|
|
prc = ssh2_scp_parse_c_fields(long_line, sizeof(long_line), &mode, &size);
|
|
if(prc || mode != 420L || size != 99) { /* 0644 octal == 420 */
|
|
fprintf(stderr,
|
|
"scp_parse long-name buffer: prc=%d mode=%ld size=%lu\n",
|
|
prc, mode, (unsigned long)size);
|
|
err++;
|
|
}
|
|
|
|
/* Malformed: bad mode */
|
|
prc = ssh2_scp_parse_c_fields("Cxyz 1 name\n", 12, &mode, &size);
|
|
if(prc != -1) {
|
|
fprintf(stderr, "scp_parse bad mode: prc=%d (want -1)\n", prc);
|
|
err++;
|
|
}
|
|
|
|
return err > 0;
|
|
}
|
|
|
|
int main(int argc, char *argv[])
|
|
{
|
|
LIBSSH2_SESSION *session;
|
|
int rc;
|
|
(void)argv;
|
|
(void)argc;
|
|
|
|
rc = libssh2_init(0);
|
|
if(rc) {
|
|
fprintf(stderr, "libssh2_init() failed: %d\n", rc);
|
|
return 1;
|
|
}
|
|
|
|
session = libssh2_session_init();
|
|
if(!session) {
|
|
fprintf(stderr, "libssh2_session_init() failed\n");
|
|
return 1;
|
|
}
|
|
|
|
rc = test_ssh2_base64_decode(session);
|
|
rc |= test_knownhost_ipv6(session);
|
|
rc |= test_ssh2_dh_validate();
|
|
rc |= test_ssh2_bn_from_bin();
|
|
rc |= test_ssh2_scp_parse_c_fields();
|
|
|
|
libssh2_session_free(session);
|
|
|
|
libssh2_exit();
|
|
|
|
return rc;
|
|
}
|