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)