Validate server's group

The client wasn't validating the DH group parameters in the KEX DH GEX
Group message. This adds a function to perform the validation of the
prime `p` to verify it is safe. (Prime and that ((p - 1) / 2) is
prime.) Also adds a test to a known unsafe prime and known safe prime
to verify the validate function.

Affected function: DoKexDhGexGroup.
Issue: F-1688
pull/922/head
John Safranek 2026-04-16 13:42:24 -07:00
parent 543a6c2a2f
commit 16ddcd9764
3 changed files with 362 additions and 2 deletions

View File

@ -185,6 +185,10 @@ Flags:
WOLFSSH_NO_CURVE25519_SHA256
Set when Curve25519 or SHA2-256 are disabled in wolfSSL. Set to disable use
of Curve25519 key exchange.
WOLFSSH_MR_ROUNDS
Set the number of Miller-Rabin rounds used when the client checks the
server's prime group when using GEX key exchange. The default is 8. More
rounds are better, but also takes a lot longer.
*/
static const char sshProtoIdStr[] = "SSH-2.0-wolfSSHv"
@ -6284,6 +6288,141 @@ static int DoKexDhGexRequest(WOLFSSH* ssh,
}
/*
* Validate a DH GEX group received from the server against RFC 4419 sec. 3
* requirements, with an additional hardening/policy check:
* - p's bit length falls within the client's requested [minBits, maxBits]
* - p is odd and probably prime
* - (additional hardening requirement) (p-1)/2 is also probably prime,
* i.e. p is a safe prime, which bounds the order of g to q or 2q and
* closes the small-subgroup attack
* Returns WS_SUCCESS if the group is acceptable.
*/
static int ValidateKexDhGexGroup(const byte* primeGroup, word32 primeGroupSz,
const byte* generator, word32 generatorSz,
word32 minBits, word32 maxBits, WC_RNG* rng)
{
mp_int p, g, q;
int pgqInit = 0;
int bits;
int isPrime = MP_NO;
int ret = WS_SUCCESS;
if (primeGroup == NULL || primeGroupSz == 0
|| generator == NULL || generatorSz == 0
|| rng == NULL)
ret = WS_BAD_ARGUMENT;
/*
* Check the bounds on the size of the flat prime group and generator
* values. The prime group shall be LE maxBits. The generator size
* shall be LE prime group size. This is a check on the sizes of values
* sent by the peer before reading them in and checking them as mp_ints.
*/
if (ret == WS_SUCCESS) {
word32 maxBytes = (maxBits / 8) + ((maxBits % 8) ? 1 : 0);
/* Adjust the sizes for signed padding. */
word32 adjPrimeGroupSz = primeGroupSz - ((primeGroup[0] == 0) ? 1 : 0);
word32 adjGeneratorSz = generatorSz - ((generator[0] == 0) ? 1 : 0);
if (adjPrimeGroupSz > maxBytes || adjGeneratorSz > adjPrimeGroupSz) {
ret = WS_DH_SIZE_E;
}
}
if (ret == WS_SUCCESS) {
if (mp_init_multi(&p, &g, &q, NULL, NULL, NULL) != MP_OKAY) {
ret = WS_CRYPTO_FAILED;
}
else {
pgqInit = 1;
}
}
if (ret == WS_SUCCESS) {
if (mp_read_unsigned_bin(&p, primeGroup, primeGroupSz) != MP_OKAY)
ret = WS_CRYPTO_FAILED;
}
if (ret == WS_SUCCESS) {
if (mp_read_unsigned_bin(&g, generator, generatorSz) != MP_OKAY)
ret = WS_CRYPTO_FAILED;
}
if (ret == WS_SUCCESS) {
bits = mp_count_bits(&p);
if (bits < (int)minBits || bits > (int)maxBits) {
WLOG(WS_LOG_DEBUG,
"DH GEX: prime size %d outside requested [%u, %u]",
bits, minBits, maxBits);
ret = WS_DH_SIZE_E;
}
}
if (ret == WS_SUCCESS) {
if (!mp_isodd(&p)) {
WLOG(WS_LOG_DEBUG, "DH GEX: prime is even");
ret = WS_CRYPTO_FAILED;
}
}
/* 2 <= g: reject g == 0 and g == 1. */
if (ret == WS_SUCCESS) {
if (mp_cmp_d(&g, 1) != MP_GT) {
WLOG(WS_LOG_DEBUG, "DH GEX: generator < 2");
ret = WS_CRYPTO_FAILED;
}
}
/* g <= p - 2: reject g == p - 1 (order 2) and anything larger. */
if (ret == WS_SUCCESS) {
if (mp_sub_d(&p, 1, &q) != MP_OKAY)
ret = WS_CRYPTO_FAILED;
else if (mp_cmp(&g, &q) != MP_LT) {
WLOG(WS_LOG_DEBUG, "DH GEX: generator >= p - 1");
ret = WS_CRYPTO_FAILED;
}
}
/* Miller-Rabin: p must be prime. */
if (ret == WS_SUCCESS) {
isPrime = MP_NO;
ret = mp_prime_is_prime_ex(&p, WOLFSSH_MR_ROUNDS, &isPrime, rng);
if (ret != MP_OKAY || isPrime == MP_NO) {
WLOG(WS_LOG_DEBUG, "DH GEX: p is not prime");
ret = WS_CRYPTO_FAILED;
}
else {
ret = WS_SUCCESS;
}
}
/* Safe prime check: q = (p - 1) / 2 must also be prime. */
if (ret == WS_SUCCESS) {
if (mp_sub_d(&p, 1, &q) != MP_OKAY || mp_div_2(&q, &q) != MP_OKAY)
ret = WS_CRYPTO_FAILED;
}
if (ret == WS_SUCCESS) {
isPrime = MP_NO;
ret = mp_prime_is_prime_ex(&q, WOLFSSH_MR_ROUNDS, &isPrime, rng);
if (ret != MP_OKAY || isPrime == MP_NO) {
WLOG(WS_LOG_DEBUG, "DH GEX: (p-1)/2 is not prime, p is not safe");
ret = WS_CRYPTO_FAILED;
}
else {
ret = WS_SUCCESS;
}
}
if (pgqInit) {
mp_clear(&q);
mp_clear(&g);
mp_clear(&p);
}
return ret;
}
static int DoKexDhGexGroup(WOLFSSH* ssh,
byte* buf, word32 len, word32* idx)
{
@ -6300,13 +6439,22 @@ static int DoKexDhGexGroup(WOLFSSH* ssh,
if (ret == WS_SUCCESS) {
begin = *idx;
ret = GetMpint(&primeGroupSz, &primeGroup, buf, len, &begin);
if (ret == WS_SUCCESS && primeGroupSz > (MAX_KEX_KEY_SZ + 1))
if (ret == WS_SUCCESS && primeGroupSz > (MAX_KEX_KEY_SZ + 1)) {
ret = WS_DH_SIZE_E;
}
}
if (ret == WS_SUCCESS)
ret = GetMpint(&generatorSz, &generator, buf, len, &begin);
if (ret == WS_SUCCESS) {
ret = ValidateKexDhGexGroup(primeGroup, primeGroupSz,
generator, generatorSz,
ssh->handshake->dhGexMinSz,
ssh->handshake->dhGexMaxSz,
ssh->rng);
}
if (ret == WS_SUCCESS) {
if (ssh->handshake->primeGroup)
WFREE(ssh->handshake->primeGroup, ssh->ctx->heap, DYNTYPE_MPINT);
@ -6340,7 +6488,17 @@ static int DoKexDhGexGroup(WOLFSSH* ssh,
return ret;
}
#ifdef WOLFSSH_TEST_INTERNAL
int wolfSSH_TestValidateKexDhGexGroup(const byte* primeGroup,
word32 primeGroupSz, const byte* generator, word32 generatorSz,
word32 minBits, word32 maxBits, WC_RNG* rng)
{
return ValidateKexDhGexGroup(primeGroup, primeGroupSz,
generator, generatorSz, minBits, maxBits, rng);
}
#endif
#endif /* !WOLFSSH_NO_DH_GEX_SHA256 */
static int DoIgnore(WOLFSSH* ssh, byte* buf, word32 len, word32* idx)

View File

@ -31,7 +31,10 @@
#include <stdio.h>
#include <wolfssh/ssh.h>
#include <wolfssh/keygen.h>
#include <wolfssh/error.h>
#include <wolfssh/internal.h>
#include <wolfssl/wolfcrypt/random.h>
#include <wolfssl/wolfcrypt/integer.h>
#include <wolfssl/wolfcrypt/hmac.h>
#define WOLFSSH_TEST_HEX2BIN
@ -439,6 +442,190 @@ done:
#endif /* WOLFSSH_TEST_INTERNAL && any HMAC SHA variant enabled */
#if defined(WOLFSSH_TEST_INTERNAL) && !defined(WOLFSSH_NO_DH_GEX_SHA256)
typedef struct {
const char* candidate;
const char* generator;
word32 minBits;
word32 maxBits;
int expectedResult;
} PrimeTestVector;
static const PrimeTestVector primeTestVectors[] = {
{
/*
* For testing the ValidateKexDhGexGroup() function, we need to
* verify that the function detects unsafe primes. The following
* unsafe prime is the prime used with GOST-ECC. (RFC 7836) It is
* prime and fine for its application. It isn't safe for DH, as
* q = (p-1)/2 is not prime.
*/
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
"fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffdc7",
"02",
512, 8192, WS_CRYPTO_FAILED
},
{
/*
* We need to verify that the function detects safe primes. The
* following safePrime is the MODP 2048-bit group from RFC 3526.
*/
"ffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74"
"020bbea63b139b22514a08798e3404ddef9519b3cd3a431b302b0a6df25f1437"
"4fe1356d6d51c245e485b576625e7ec6f44c42e9a637ed6b0bff5cb6f406b7ed"
"ee386bfb5a899fa5ae9f24117c4b1fe649286651ece45b3dc2007cb8a163bf05"
"98da48361c55d39a69163fa8fd24cf5f83655d23dca3ad961c62f356208552bb"
"9ed529077096966d670c354e4abc9804f1746c08ca18217c32905e462e36ce3b"
"e39e772c180e86039b2783a2ec07a28fb5c55df06f4c52c9de2bcbf695581718"
"3995497cea956ae515d2261898fa051015728e5a8aacaa68ffffffffffffffff",
"02",
2048, 8192, WS_SUCCESS
},
{
/*
* This checks for g = p - 1.
*/
"ffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74"
"020bbea63b139b22514a08798e3404ddef9519b3cd3a431b302b0a6df25f1437"
"4fe1356d6d51c245e485b576625e7ec6f44c42e9a637ed6b0bff5cb6f406b7ed"
"ee386bfb5a899fa5ae9f24117c4b1fe649286651ece45b3dc2007cb8a163bf05"
"98da48361c55d39a69163fa8fd24cf5f83655d23dca3ad961c62f356208552bb"
"9ed529077096966d670c354e4abc9804f1746c08ca18217c32905e462e36ce3b"
"e39e772c180e86039b2783a2ec07a28fb5c55df06f4c52c9de2bcbf695581718"
"3995497cea956ae515d2261898fa051015728e5a8aacaa68ffffffffffffffff",
"ffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74"
"020bbea63b139b22514a08798e3404ddef9519b3cd3a431b302b0a6df25f1437"
"4fe1356d6d51c245e485b576625e7ec6f44c42e9a637ed6b0bff5cb6f406b7ed"
"ee386bfb5a899fa5ae9f24117c4b1fe649286651ece45b3dc2007cb8a163bf05"
"98da48361c55d39a69163fa8fd24cf5f83655d23dca3ad961c62f356208552bb"
"9ed529077096966d670c354e4abc9804f1746c08ca18217c32905e462e36ce3b"
"e39e772c180e86039b2783a2ec07a28fb5c55df06f4c52c9de2bcbf695581718"
"3995497cea956ae515d2261898fa051015728e5a8aacaa68fffffffffffffffe",
2048, 8192, WS_CRYPTO_FAILED
},
{
/*
* This checks for g = 1.
*/
"ffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74"
"020bbea63b139b22514a08798e3404ddef9519b3cd3a431b302b0a6df25f1437"
"4fe1356d6d51c245e485b576625e7ec6f44c42e9a637ed6b0bff5cb6f406b7ed"
"ee386bfb5a899fa5ae9f24117c4b1fe649286651ece45b3dc2007cb8a163bf05"
"98da48361c55d39a69163fa8fd24cf5f83655d23dca3ad961c62f356208552bb"
"9ed529077096966d670c354e4abc9804f1746c08ca18217c32905e462e36ce3b"
"e39e772c180e86039b2783a2ec07a28fb5c55df06f4c52c9de2bcbf695581718"
"3995497cea956ae515d2261898fa051015728e5a8aacaa68ffffffffffffffff",
"01",
2048, 8192, WS_CRYPTO_FAILED
},
{
/*
* This checks prime size less than minBits.
*/
"ffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74"
"020bbea63b139b22514a08798e3404ddef9519b3cd3a431b302b0a6df25f1437"
"4fe1356d6d51c245e485b576625e7ec6f44c42e9a637ed6b0bff5cb6f406b7ed"
"ee386bfb5a899fa5ae9f24117c4b1fe649286651ece45b3dc2007cb8a163bf05"
"98da48361c55d39a69163fa8fd24cf5f83655d23dca3ad961c62f356208552bb"
"9ed529077096966d670c354e4abc9804f1746c08ca18217c32905e462e36ce3b"
"e39e772c180e86039b2783a2ec07a28fb5c55df06f4c52c9de2bcbf695581718"
"3995497cea956ae515d2261898fa051015728e5a8aacaa68ffffffffffffffff",
"02",
3072, 8192, WS_DH_SIZE_E
},
{
/*
* This checks prime size greater than maxBits.
*/
"ffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74"
"020bbea63b139b22514a08798e3404ddef9519b3cd3a431b302b0a6df25f1437"
"4fe1356d6d51c245e485b576625e7ec6f44c42e9a637ed6b0bff5cb6f406b7ed"
"ee386bfb5a899fa5ae9f24117c4b1fe649286651ece45b3dc2007cb8a163bf05"
"98da48361c55d39a69163fa8fd24cf5f83655d23dca3ad961c62f356208552bb"
"9ed529077096966d670c354e4abc9804f1746c08ca18217c32905e462e36ce3b"
"e39e772c180e86039b2783a2ec07a28fb5c55df06f4c52c9de2bcbf695581718"
"3995497cea956ae515d2261898fa051015728e5a8aacaa68ffffffffffffffff",
"02",
512, 1024, WS_DH_SIZE_E
},
{
/*
* This checks for even p.
*/
"ffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74"
"020bbea63b139b22514a08798e3404ddef9519b3cd3a431b302b0a6df25f1437"
"4fe1356d6d51c245e485b576625e7ec6f44c42e9a637ed6b0bff5cb6f406b7ed"
"ee386bfb5a899fa5ae9f24117c4b1fe649286651ece45b3dc2007cb8a163bf05"
"98da48361c55d39a69163fa8fd24cf5f83655d23dca3ad961c62f356208552bb"
"9ed529077096966d670c354e4abc9804f1746c08ca18217c32905e462e36ce3b"
"e39e772c180e86039b2783a2ec07a28fb5c55df06f4c52c9de2bcbf695581718"
"3995497cea956ae515d2261898fa051015728e5a8aacaa68fffffffffffffffe",
"02",
2048, 8192, WS_CRYPTO_FAILED
},
{
/*
* A well known composite number that breaks some MR implementations.
* This is calculated by wolfCrypt for one of its prime tests.
*/
"000000000088cbf655be37a612fa535b4a9b81d394854ecbedfe1a4afbecdc7b"
"a6a263549dd3c17882b054329384962576e7c5aa281e04ab5a0e7245584ad324"
"9c7ac4de7caf5663bae95f6bb9e8bec4124e04d82eac54a246bda49a5c5c2a1b"
"366ef8c085fc7c5f87478a55832d1b2184154c24260df67561d17c4359724403",
"02",
512, 8192, WS_CRYPTO_FAILED
},
};
static int test_DhGexGroupValidate(void)
{
WC_RNG rng;
const PrimeTestVector* tv;
byte* candidate;
byte* generator;
word32 candidateSz;
word32 generatorSz;
int tc = (int)(sizeof(primeTestVectors)/sizeof(primeTestVectors[0]));
int result = 0, ret, i;
if (wc_InitRng(&rng) != 0) {
printf("DhGexGroupValidate: wc_InitRng failed\n");
return -110;
}
for (i = 0, tv = primeTestVectors; i < tc && !result; i++, tv++) {
candidate = NULL;
candidateSz = 0;
generator = NULL;
generatorSz = 0;
ret = ConvertHexToBin(tv->candidate, &candidate, &candidateSz,
tv->generator, &generator, &generatorSz,
NULL, NULL, NULL, NULL, NULL, NULL);
if (ret != 0) {
result = -113;
break;
}
ret = wolfSSH_TestValidateKexDhGexGroup(candidate, candidateSz,
generator, generatorSz, tv->minBits, tv->maxBits, &rng);
if (ret != tv->expectedResult) {
printf("DhGexGroupValidate: validator returned %d, expected %d\n",
ret, tv->expectedResult);
result = -121;
}
FreeBins(candidate, generator, NULL, NULL);
}
wc_FreeRng(&rng);
return result;
}
#endif /* WOLFSSH_TEST_INTERNAL && !WOLFSSH_NO_DH_GEX_SHA256 */
/* Error Code And Message Test */
static int test_Errors(void)
@ -520,6 +707,13 @@ int wolfSSH_UnitTest(int argc, char** argv)
testResult = testResult || unitResult;
#endif
#if defined(WOLFSSH_TEST_INTERNAL) && !defined(WOLFSSH_NO_DH_GEX_SHA256)
unitResult = test_DhGexGroupValidate();
printf("DhGexGroupValidate: %s\n",
(unitResult == 0 ? "SUCCESS" : "FAILED"));
testResult = testResult || unitResult;
#endif
#ifdef WOLFSSH_KEYGEN
#ifndef WOLFSSH_NO_RSA
unitResult = test_RsaKeyGen();

View File

@ -476,6 +476,9 @@ enum NameIdType {
#define MAX_KEX_KEY_SZ (WOLFSSH_DEFAULT_GEXDH_MAX / 8)
#endif
#endif
#ifndef WOLFSSH_MR_ROUNDS
#define WOLFSSH_MR_ROUNDS 8
#endif
#ifndef WOLFSSH_MAX_FILE_SIZE
#define WOLFSSH_MAX_FILE_SIZE (1024ul * 1024ul * 4)
#endif
@ -1324,7 +1327,12 @@ enum WS_MessageIdLimits {
WOLFSSH_API int wolfSSH_TestIsMessageAllowed(WOLFSSH* ssh, byte msg,
byte state);
WOLFSSH_API int wolfSSH_TestDoReceive(WOLFSSH* ssh);
#endif
#ifndef WOLFSSH_NO_DH_GEX_SHA256
WOLFSSH_API int wolfSSH_TestValidateKexDhGexGroup(const byte* primeGroup,
word32 primeGroupSz, const byte* generator, word32 generatorSz,
word32 minBits, word32 maxBits, WC_RNG* rng);
#endif /* !WOLFSSH_NO_DH_GEX_SHA256 */
#endif /* WOLFSSH_TEST_INTERNAL */
/* dynamic memory types */
enum WS_DynamicTypes {