[UDM] Added validation for pubkey

a cryptographic vulnerability in the SUCI decryption routines
of Open5GS 5G—specifically Profile B, which uses P-256 (secp256r1)
for its elliptic curve routines.

If a mobile device user passes a public key within its SUCI
that does not correspond to a valid point on the P-256 elliptic curve,
the Open5GS UDM will not check the point
before running elliptic curve operations with it and returning a response
to the mobile device user.

If the public key is not checked to be a valid point, an attacker can leverage
this behavior to extract the Profile B private key from the UDM,
as has been done in other domains
(https://owasp.org/www-pdf-archive/Practical_Invalid_Curve_Attacks_on_TLS-ECDH_-_Juraj_Somorovsky.pdf).

Note that Profile A is not similarly vulnerable to this, as it is impossible
to construct an invalid point on a curve25519 elliptic curve.

There was some work that went into developing a practical proof of concept
of this kind of attack against free5gc last year; it can be found here:

https://www.gsma.com/security/wp-content/uploads/2023/10/0073-invalid_curve.pdf

And here is the free5gc security advisory:

https://github.com/advisories/GHSA-cqvv-r3g3-26rf

To mitigate this issue in Open5GS, the public key of the UE must be validated
by the UDM prior to use. Adding a validation function such as the following
should work:

I designed this code based on information from https://crypto.stackexchange.com/questions/90151/verify-that-a-point-belongs-to-secp256r1.
This commit is contained in:
Sukchan Lee 2024-03-24 14:04:44 +09:00
parent cf4ad1f248
commit 7c14073533
1 changed files with 60 additions and 0 deletions

View File

@ -4,6 +4,8 @@
#include <string.h>
#include "ogs-core.h"
#define NUM_ECC_DIGITS (ECC_BYTES/8)
#define MAX_TRIES 16
@ -82,6 +84,7 @@ static int getRandomNumber(uint64_t *p_vli)
HCRYPTPROV l_prov;
if(!CryptAcquireContext(&l_prov, NULL, NULL, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT))
{
ogs_error("CryptAcquireContext() failed");
return 0;
}
@ -107,9 +110,11 @@ static int getRandomNumber(uint64_t *p_vli)
int l_fd = open("/dev/urandom", O_RDONLY | O_CLOEXEC);
if(l_fd == -1)
{
ogs_error("open(/dev/urandom) failed");
l_fd = open("/dev/random", O_RDONLY | O_CLOEXEC);
if(l_fd == -1)
{
ogs_error("open(/dev/random) failed");
return 0;
}
}
@ -122,6 +127,7 @@ static int getRandomNumber(uint64_t *p_vli)
if(l_read <= 0)
{ // read failed
close(l_fd);
ogs_error("read() failed");
return 0;
}
l_left -= l_read;
@ -1073,6 +1079,7 @@ int ecc_make_key(uint8_t p_publicKey[ECC_BYTES+1], uint8_t p_privateKey[ECC_BYTE
{
if(!getRandomNumber(l_private) || (l_tries++ >= MAX_TRIES))
{
ogs_error("getRandomNumber() failed [%d]", l_tries);
return 0;
}
if(vli_isZero(l_private))
@ -1096,6 +1103,45 @@ int ecc_make_key(uint8_t p_publicKey[ECC_BYTES+1], uint8_t p_privateKey[ECC_BYTE
return 1;
}
#define CURVE_A_32 {0xFFFFFFFFFFFFFFFCull, 0x00000000FFFFFFFFull, 0x0000000000000000ull, 0xFFFFFFFF00000001ull}
static int ecdh_validate_pubkey(EccPoint l_public, uint64_t l_private[NUM_ECC_DIGITS]) {
uint64_t left[NUM_ECC_DIGITS];
uint64_t right[NUM_ECC_DIGITS];
uint64_t curve_a[NUM_ECC_DIGITS] = CURVE_A_32;
/*
* To ensure l_public is a valid point on the curve, we need to check:
* y^2 % p == (x^3 + a * x + b) % p)
*/
/* Compute y^2 % p and store in `left` */
vli_modSquare_fast(left, l_public.y);
/* Compute x^3 and store in `right` */
vli_modSquare_fast(right, l_public.x);
vli_modMult_fast(right, right, l_public.x);
/* Compute a * x and store in `curve_a` */
vli_modMult_fast(curve_a, curve_a, l_public.x);
/* Store ((a * x) + b) % p in `curve_a */
vli_modAdd(curve_a, curve_a, curve_b, curve_p);
/*
* Combine x^3 and ((a * x) + b) to make (x^3 + a * x + b) % p);
* store in `right`
*/
vli_modAdd(right, right, curve_a, curve_p);
int i;
for (i = 0; i < NUM_ECC_DIGITS; i++) {
if (left[i] != right[i]) {
return 0; // y^2 % p != (x^3 + a * x + b) % p)
}
}
return 1;
}
int ecdh_shared_secret(const uint8_t p_publicKey[ECC_BYTES+1], const uint8_t p_privateKey[ECC_BYTES], uint8_t p_secret[ECC_BYTES])
{
EccPoint l_public;
@ -1104,12 +1150,23 @@ int ecdh_shared_secret(const uint8_t p_publicKey[ECC_BYTES+1], const uint8_t p_p
if(!getRandomNumber(l_random))
{
ogs_error("getRandomNumber() failed");
return 0;
}
ecc_point_decompress(&l_public, p_publicKey);
ecc_bytes2native(l_private, p_privateKey);
/*
* Validate received public key `p_publicKey` is a valid point
* on curve P-256
*/
if (!ecdh_validate_pubkey(l_public, l_private))
{
ogs_error("ecdh_validate_pubkey() failed");
return 0;
}
EccPoint l_product;
EccPoint_mult(&l_product, &l_public, l_private, l_random);
@ -1202,6 +1259,7 @@ int ecdsa_sign(const uint8_t p_privateKey[ECC_BYTES], const uint8_t p_hash[ECC_B
{
if(!getRandomNumber(k) || (l_tries++ >= MAX_TRIES))
{
ogs_error("getRandomNumber() failed [%d]", l_tries);
return 0;
}
if(vli_isZero(k))
@ -1256,11 +1314,13 @@ int ecdsa_verify(const uint8_t p_publicKey[ECC_BYTES+1], const uint8_t p_hash[EC
if(vli_isZero(l_r) || vli_isZero(l_s))
{ /* r, s must not be 0. */
ogs_error("r, s must not be 0");
return 0;
}
if(vli_cmp(curve_n, l_r) != 1 || vli_cmp(curve_n, l_s) != 1)
{ /* r, s must be < n. */
ogs_error("r, s must be < n");
return 0;
}