Implement EC_wpa3_sae_hash_to_curve_p256 This implements the hash-to-curve variant defined in IEEE Std 802.11-2020, Section 12.4.4.2.3. It's similar to the IETF's hash-to-curve, but needlessly different in how it expands the input to field elements. Bug: 525105943 Change-Id: I426bfebf3c2b77d5e6e8dc41030c2ca0066e9d21 Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/97687 Commit-Queue: David Benjamin <davidben@google.com> Reviewed-by: Lily Chen <chlily@google.com>
diff --git a/crypto/ec/hash_to_curve.cc b/crypto/ec/hash_to_curve.cc index dd32dd2..8fae618 100644 --- a/crypto/ec/hash_to_curve.cc +++ b/crypto/ec/hash_to_curve.cc
@@ -16,7 +16,9 @@ #include <openssl/digest.h> #include <openssl/err.h> +#include <openssl/hkdf.h> #include <openssl/nid.h> +#include <openssl/span.h> #include <assert.h> @@ -154,18 +156,25 @@ } // big_endian_to_words decodes `in` as a big-endian integer and writes the -// result to `out`. `num_words` must be large enough to contain the output. -void big_endian_to_words(BN_ULONG *out, size_t num_words, const uint8_t *in, - size_t len) { - assert(len <= num_words * sizeof(BN_ULONG)); +// result to `out`. `out` must be large enough to contain the output. +void big_endian_to_words(Span<BN_ULONG> out, Span<const uint8_t> in) { + BSSL_CHECK(in.size() <= out.size() * sizeof(BN_ULONG)); // Ensure any excess bytes are zeroed. - OPENSSL_memset(out, 0, num_words * sizeof(BN_ULONG)); - uint8_t *out_u8 = (uint8_t *)out; - for (size_t i = 0; i < len; i++) { - out_u8[len - 1 - i] = in[i]; + OPENSSL_memset(out.data(), 0, out.size() * sizeof(BN_ULONG)); + uint8_t *out_u8 = reinterpret_cast<uint8_t *>(out.data()); + for (size_t i = 0; i < in.size(); i++) { + out_u8[in.size() - 1 - i] = in[i]; } } +void big_endian_reduce_to_felem(const EC_GROUP *group, EC_FELEM *out, + Span<const uint8_t> in) { + BN_ULONG words_buf[2 * EC_MAX_WORDS]; + auto words = Span(words_buf).first(2 * group->field.N.width); + big_endian_to_words(words, in); + ec_felem_reduce(group, out, words.data(), words.size()); +} + // hash_to_field implements the operation described in section 5.2 // of RFC 9380, with count = 2. `k` is the security factor. int hash_to_field2(const EC_GROUP *group, const EVP_MD *md, EC_FELEM *out1, @@ -177,12 +186,8 @@ !expand_message_xmd(md, buf, 2 * L, msg, msg_len, dst, dst_len)) { return 0; } - BN_ULONG words[2 * EC_MAX_WORDS]; - size_t num_words = 2 * group->field.N.width; - big_endian_to_words(words, num_words, buf, L); - ec_felem_reduce(group, out1, words, num_words); - big_endian_to_words(words, num_words, buf + L, L); - ec_felem_reduce(group, out2, words, num_words); + big_endian_reduce_to_felem(group, out1, Span(buf).first(L)); + big_endian_reduce_to_felem(group, out2, Span(buf).subspan(L, L)); return 1; } @@ -197,10 +202,7 @@ !expand_message_xmd(md, buf, L, msg, msg_len, dst, dst_len)) { return 0; } - BN_ULONG words[2 * EC_MAX_WORDS]; - size_t num_words = 2 * group->field.N.width; - big_endian_to_words(words, num_words, buf, L); - ec_felem_reduce(group, out, words, num_words); + big_endian_reduce_to_felem(group, out, Span(buf).first(L)); return 1; } @@ -219,7 +221,7 @@ BN_ULONG words[2 * EC_MAX_WORDS]; size_t num_words = 2 * order->width; - big_endian_to_words(words, num_words, buf, L); + big_endian_to_words(Span(words).first(num_words), Span(buf).first(L)); ec_scalar_reduce(group, out, words, num_words); return 1; } @@ -342,15 +344,8 @@ out->Z = tv4; } -int hash_to_curve(const EC_GROUP *group, const EVP_MD *md, const EC_FELEM *Z, - const EC_FELEM *c2, unsigned k, EC_JACOBIAN *out, - const uint8_t *dst, size_t dst_len, const uint8_t *msg, - size_t msg_len) { - EC_FELEM u0, u1; - if (!hash_to_field2(group, md, &u0, &u1, dst, dst_len, k, msg, msg_len)) { - return 0; - } - +int sswu_and_add(const EC_GROUP *group, const EC_FELEM *Z, const EC_FELEM *c2, + EC_JACOBIAN *out, const EC_FELEM *u0, const EC_FELEM *u1) { // Compute `c1` = (p - 3) / 4. BN_ULONG c1[EC_MAX_WORDS]; size_t num_c1 = group->field.N.width; @@ -360,14 +355,23 @@ bn_rshift_words(c1, c1, /*shift=*/2, /*num=*/num_c1); EC_JACOBIAN Q0, Q1; - map_to_curve_simple_swu(group, Z, c1, num_c1, c2, &Q0, &u0); - map_to_curve_simple_swu(group, Z, c1, num_c1, c2, &Q1, &u1); + map_to_curve_simple_swu(group, Z, c1, num_c1, c2, &Q0, u0); + map_to_curve_simple_swu(group, Z, c1, num_c1, c2, &Q1, u1); group->meth->add(group, out, &Q0, &Q1); // R = Q0 + Q1 // All our curves have cofactor one, so `clear_cofactor` is a no-op. return 1; } +int hash_to_curve(const EC_GROUP *group, const EVP_MD *md, const EC_FELEM *Z, + const EC_FELEM *c2, unsigned k, EC_JACOBIAN *out, + const uint8_t *dst, size_t dst_len, const uint8_t *msg, + size_t msg_len) { + EC_FELEM u0, u1; + return hash_to_field2(group, md, &u0, &u1, dst, dst_len, k, msg, msg_len) && + sswu_and_add(group, Z, c2, out, &u0, &u1); +} + int encode_to_curve(const EC_GROUP *group, const EVP_MD *md, const EC_FELEM *Z, const EC_FELEM *c2, unsigned k, EC_JACOBIAN *out, const uint8_t *dst, size_t dst_len, const uint8_t *msg, @@ -554,6 +558,55 @@ msg, msg_len); } +int EC_wpa3_sae_hash_to_curve_p256(const EC_GROUP *group, EC_POINT *out, + const uint8_t *salt, size_t salt_len, + const uint8_t *ikm, size_t ikm_len) { + if (EC_GROUP_cmp(group, out->group, nullptr) != 0) { + OPENSSL_PUT_ERROR(EC, EC_R_INCOMPATIBLE_OBJECTS); + return 0; + } + if (EC_GROUP_get_curve_name(group) != NID_X9_62_prime256v1) { + OPENSSL_PUT_ERROR(EC, EC_R_GROUP_MISMATCH); + return 0; + } + + // WPA3 uses a different process for computing `u0` and `u1` (called `u1` and + // `u2` in 802.11) than RFC 9380. + uint8_t pwd_seed[EVP_MAX_MD_SIZE]; + size_t pwd_seed_len; + if (!HKDF_extract(pwd_seed, &pwd_seed_len, EVP_sha256(), ikm, ikm_len, salt, + salt_len)) { + return 0; + } + + uint8_t pwd_value[48]; // `len` for P-256 is 32 + 32 / 2 + auto label = StringAsBytes("SAE Hash to Element u1 P1"); + if (!HKDF_expand(pwd_value, sizeof(pwd_value), EVP_sha256(), pwd_seed, + pwd_seed_len, label.data(), label.size())) { + return 0; + } + EC_FELEM u1; + big_endian_reduce_to_felem(group, &u1, pwd_value); + + label = StringAsBytes("SAE Hash to Element u2 P2"); + if (!HKDF_expand(pwd_value, sizeof(pwd_value), EVP_sha256(), pwd_seed, + pwd_seed_len, label.data(), label.size())) { + return 0; + } + EC_FELEM u2; + big_endian_reduce_to_felem(group, &u2, pwd_value); + + // Z = -10, c2 = sqrt(10) + EC_FELEM Z, c2; + if (!felem_from_u8(group, &Z, 10) || + !ec_felem_from_bytes(group, &c2, kP256Sqrt10, sizeof(kP256Sqrt10))) { + return 0; + } + ec_felem_neg(group, &Z, &Z); + + return sswu_and_add(group, &Z, &c2, &out->raw, &u1, &u2); +} + int bssl::ec_hash_to_scalar_p384_xmd_sha384(const EC_GROUP *group, EC_SCALAR *out, const uint8_t *dst, size_t dst_len, const uint8_t *msg,
diff --git a/crypto/fipsmodule/ec/ec_test.cc b/crypto/fipsmodule/ec/ec_test.cc index adc969a..f3d66b6 100644 --- a/crypto/fipsmodule/ec/ec_test.cc +++ b/crypto/fipsmodule/ec/ec_test.cc
@@ -1509,6 +1509,46 @@ EC_group_p384(), &raw, nullptr, 0, kMessage, sizeof(kMessage))); } +// Test the WPA3 SAE hash-to-curve construction. Test vector from Appendix J.10 +// of IEEE Std 802.11-2024. +TEST(ECTest, WPA3SAEHashToCurve) { + static const uint8_t kSalt[] = {0x62, 0x79, 0x74, 0x65, 0x6d, 0x65}; + static const uint8_t kIKM[] = {0x6d, 0x65, 0x6b, 0x6d, 0x69, 0x74, 0x61, + 0x73, 0x64, 0x69, 0x67, 0x6f, 0x61, 0x74, + 0x70, 0x73, 0x6b, 0x34, 0x69, 0x6e, 0x74, + 0x65, 0x72, 0x6e, 0x65, 0x74}; + static const uint8_t kExpected[] = { + 0x04, 0xb6, 0xe3, 0x8c, 0x98, 0x75, 0x0c, 0x68, 0x4b, 0x5d, 0x17, + 0xc3, 0xd8, 0xc9, 0xa4, 0x10, 0x0b, 0x39, 0x93, 0x12, 0x79, 0x18, + 0x7c, 0xa6, 0xcc, 0xed, 0x5f, 0x37, 0xef, 0x46, 0xdd, 0xfa, 0x97, + 0x56, 0x87, 0xe9, 0x72, 0xe5, 0x0f, 0x73, 0xe3, 0x89, 0x88, 0x61, + 0xe7, 0xed, 0xad, 0x21, 0xbe, 0xa7, 0xd5, 0xf6, 0x22, 0xdf, 0x88, + 0x24, 0x3b, 0xb8, 0x04, 0x92, 0x0a, 0xe8, 0xe6, 0x47, 0xfa}; + + const EC_GROUP *group = EC_group_p256(); + UniquePtr<EC_POINT> point(EC_POINT_new(group)); + ASSERT_TRUE(point); + ASSERT_TRUE(EC_wpa3_sae_hash_to_curve_p256( + group, point.get(), kSalt, sizeof(kSalt), kIKM, sizeof(kIKM))); + + std::vector<uint8_t> buf; + ASSERT_TRUE( + EncodeECPoint(&buf, group, point.get(), POINT_CONVERSION_UNCOMPRESSED)); + EXPECT_EQ(Bytes(kExpected), Bytes(buf)); + + // The function should check for the wrong group. + UniquePtr<EC_POINT> point_p384(EC_POINT_new(EC_group_p384())); + ASSERT_TRUE(point_p384); + EXPECT_FALSE(EC_wpa3_sae_hash_to_curve_p256(EC_group_p384(), point_p384.get(), + kSalt, sizeof(kSalt), kIKM, + sizeof(kIKM))); + EXPECT_FALSE(EC_wpa3_sae_hash_to_curve_p256(EC_group_p256(), point_p384.get(), + kSalt, sizeof(kSalt), kIKM, + sizeof(kIKM))); + EXPECT_FALSE(EC_wpa3_sae_hash_to_curve_p256( + EC_group_p384(), point.get(), kSalt, sizeof(kSalt), kIKM, sizeof(kIKM))); +} + #if !defined(BORINGSSL_SHARED_LIBRARY) TEST(ECTest, HashToScalar) { struct HashToScalarTest {
diff --git a/include/openssl/ec.h b/include/openssl/ec.h index 5dfc88a..60ccf3e 100644 --- a/include/openssl/ec.h +++ b/include/openssl/ec.h
@@ -336,6 +336,16 @@ const EC_GROUP *group, EC_POINT *out, const uint8_t *dst, size_t dst_len, const uint8_t *msg, size_t msg_len); +// EC_wpa3_sae_hash_to_curve_p256 hashes `salt` and `ikm` to a point on `group` +// and writes the result to `out`, implementing the WPA3 SAE hash-to-curve +// variant for P-256 defined in IEEE Std 802.11-2020, Section 12.4.4.2.3. It +// returns one on success and zero on error. `salt` is the HKDF-Extract salt, +// which is the SSID. `ikm` is the HKDF-Extract input, or password and +// identifier concatenation. +OPENSSL_EXPORT int EC_wpa3_sae_hash_to_curve_p256( + const EC_GROUP *group, EC_POINT *out, const uint8_t *salt, size_t salt_len, + const uint8_t *ikm, size_t ikm_len); + // Deprecated functions.
diff --git a/include/openssl/prefix_symbols.h b/include/openssl/prefix_symbols.h index abfe812..703383c 100644 --- a/include/openssl/prefix_symbols.h +++ b/include/openssl/prefix_symbols.h
@@ -882,6 +882,7 @@ #pragma redefine_extname EC_group_p521 BORINGSSL_ADD_USER_LABEL_AND_PREFIX(EC_group_p521) #pragma redefine_extname EC_hash_to_curve_p256_xmd_sha256_sswu BORINGSSL_ADD_USER_LABEL_AND_PREFIX(EC_hash_to_curve_p256_xmd_sha256_sswu) #pragma redefine_extname EC_hash_to_curve_p384_xmd_sha384_sswu BORINGSSL_ADD_USER_LABEL_AND_PREFIX(EC_hash_to_curve_p384_xmd_sha384_sswu) +#pragma redefine_extname EC_wpa3_sae_hash_to_curve_p256 BORINGSSL_ADD_USER_LABEL_AND_PREFIX(EC_wpa3_sae_hash_to_curve_p256) #pragma redefine_extname ED25519_keypair BORINGSSL_ADD_USER_LABEL_AND_PREFIX(ED25519_keypair) #pragma redefine_extname ED25519_keypair_from_seed BORINGSSL_ADD_USER_LABEL_AND_PREFIX(ED25519_keypair_from_seed) #pragma redefine_extname ED25519_sign BORINGSSL_ADD_USER_LABEL_AND_PREFIX(ED25519_sign) @@ -3999,6 +4000,7 @@ #define EC_group_p521 BORINGSSL_ADD_PREFIX(EC_group_p521) #define EC_hash_to_curve_p256_xmd_sha256_sswu BORINGSSL_ADD_PREFIX(EC_hash_to_curve_p256_xmd_sha256_sswu) #define EC_hash_to_curve_p384_xmd_sha384_sswu BORINGSSL_ADD_PREFIX(EC_hash_to_curve_p384_xmd_sha384_sswu) +#define EC_wpa3_sae_hash_to_curve_p256 BORINGSSL_ADD_PREFIX(EC_wpa3_sae_hash_to_curve_p256) #define ED25519_keypair BORINGSSL_ADD_PREFIX(ED25519_keypair) #define ED25519_keypair_from_seed BORINGSSL_ADD_PREFIX(ED25519_keypair_from_seed) #define ED25519_sign BORINGSSL_ADD_PREFIX(ED25519_sign)