Implement SPAKE2+ and its integration in TLS 1.3

This change adds an implementation of SPAKE2+ using the P-256,
SHA256, HKDF-SHA256, and HMAC-SHA256 configuration, as specified
in RFC9383. It also integrates this algorithm into the TLS 1.3
handshake following the I-D specification available at
https://chris-wood.github.io/draft-bmw-tls-pake13/draft-bmw-tls-pake13.html

Change-Id: Ifc81ba974ddef014ea9dcbc7380ecf4db909225c
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/72427
Reviewed-by: Adam Langley <agl@google.com>
Reviewed-by: Bob Beck <bbe@google.com>
Commit-Queue: David Benjamin <davidben@google.com>
diff --git a/crypto/err/ssl.errordata b/crypto/err/ssl.errordata
index bcd7327..d8eb3a4 100644
--- a/crypto/err/ssl.errordata
+++ b/crypto/err/ssl.errordata
@@ -94,6 +94,7 @@
 SSL,251,INVALID_OUTER_RECORD_TYPE
 SSL,269,INVALID_SCT_LIST
 SSL,295,INVALID_SIGNATURE_ALGORITHM
+SSL,324,INVALID_SPAKE2PLUSV1_VALUE
 SSL,160,INVALID_SSL_SESSION
 SSL,161,INVALID_TICKET_KEYS_LENGTH
 SSL,302,KEY_USAGE_BIT_INCORRECT
@@ -135,10 +136,13 @@
 SSL,268,OLD_SESSION_PRF_HASH_MISMATCH
 SSL,188,OLD_SESSION_VERSION_NOT_RETURNED
 SSL,189,OUTPUT_ALIASES_INPUT
+SSL,1122,PAKE_AND_KEY_SHARE_NOT_ALLOWED
+SSL,325,PAKE_EXHAUSTED
 SSL,190,PARSE_TLSEXT
 SSL,191,PATH_TOO_LONG
 SSL,192,PEER_DID_NOT_RETURN_A_CERTIFICATE
 SSL,193,PEER_ERROR_UNSUPPORTED_CERTIFICATE_TYPE
+SSL,326,PEER_PAKE_MISMATCH
 SSL,267,PRE_SHARED_KEY_MUST_BE_LAST
 SSL,287,PRIVATE_KEY_OPERATION_FAILED
 SSL,194,PROTOCOL_IS_SHUTDOWN
@@ -238,6 +242,7 @@
 SSL,236,UNSAFE_LEGACY_RENEGOTIATION_DISABLED
 SSL,237,UNSUPPORTED_CIPHER
 SSL,238,UNSUPPORTED_COMPRESSION_ALGORITHM
+SSL,327,UNSUPPORTED_CREDENTIAL_LIST
 SSL,312,UNSUPPORTED_ECH_SERVER_CONFIG
 SSL,239,UNSUPPORTED_ELLIPTIC_CURVE
 SSL,240,UNSUPPORTED_PROTOCOL
diff --git a/gen/crypto/err_data.cc b/gen/crypto/err_data.cc
index 7ad0781..87f8d23 100644
--- a/gen/crypto/err_data.cc
+++ b/gen/crypto/err_data.cc
@@ -198,51 +198,51 @@
     0x283500f7,
     0x28358c81,
     0x2836099a,
-    0x2c323305,
+    0x2c32337d,
     0x2c3293a3,
-    0x2c333313,
-    0x2c33b325,
-    0x2c343339,
-    0x2c34b34b,
-    0x2c353366,
-    0x2c35b378,
-    0x2c3633a8,
+    0x2c33338b,
+    0x2c33b39d,
+    0x2c3433b1,
+    0x2c34b3c3,
+    0x2c3533de,
+    0x2c35b3f0,
+    0x2c363420,
     0x2c36833a,
-    0x2c3733b5,
-    0x2c37b3e1,
-    0x2c38341f,
-    0x2c38b436,
-    0x2c393454,
-    0x2c39b464,
-    0x2c3a3476,
-    0x2c3ab48a,
-    0x2c3b349b,
-    0x2c3bb4ba,
+    0x2c37342d,
+    0x2c37b459,
+    0x2c383497,
+    0x2c38b4ae,
+    0x2c3934cc,
+    0x2c39b4dc,
+    0x2c3a34ee,
+    0x2c3ab502,
+    0x2c3b3513,
+    0x2c3bb532,
     0x2c3c13b5,
     0x2c3c93cb,
-    0x2c3d34ff,
+    0x2c3d3577,
     0x2c3d93e4,
-    0x2c3e3529,
-    0x2c3eb537,
-    0x2c3f354f,
-    0x2c3fb567,
-    0x2c403591,
+    0x2c3e35a1,
+    0x2c3eb5af,
+    0x2c3f35c7,
+    0x2c3fb5df,
+    0x2c403609,
     0x2c409298,
-    0x2c4135a2,
-    0x2c41b5b5,
+    0x2c41361a,
+    0x2c41b62d,
     0x2c42125e,
-    0x2c42b5c6,
+    0x2c42b63e,
     0x2c43076d,
-    0x2c43b4ac,
-    0x2c4433f4,
-    0x2c44b574,
-    0x2c45338b,
-    0x2c45b3c7,
-    0x2c463444,
-    0x2c46b4ce,
-    0x2c4734e3,
-    0x2c47b51c,
-    0x2c483406,
+    0x2c43b524,
+    0x2c44346c,
+    0x2c44b5ec,
+    0x2c453403,
+    0x2c45b43f,
+    0x2c4634bc,
+    0x2c46b546,
+    0x2c47355b,
+    0x2c47b594,
+    0x2c48347e,
     0x30320000,
     0x30328015,
     0x3033001f,
@@ -442,156 +442,156 @@
     0x404ea0be,
     0x404f216f,
     0x404fa1e5,
-    0x40502254,
-    0x4050a268,
-    0x4051229b,
-    0x405222ab,
-    0x4052a2cf,
-    0x405322e7,
-    0x4053a2fa,
-    0x4054230f,
-    0x4054a332,
-    0x4055235d,
-    0x4055a39a,
-    0x405623bf,
-    0x4056a3d8,
-    0x405723f0,
-    0x4057a403,
-    0x40582418,
-    0x4058a43f,
-    0x4059246e,
-    0x4059a4ae,
-    0x405aa4c2,
-    0x405b24da,
-    0x405ba4eb,
-    0x405c24fe,
-    0x405ca53d,
-    0x405d254a,
-    0x405da56f,
-    0x405e25ad,
+    0x4050226f,
+    0x4050a283,
+    0x405122b6,
+    0x405222c6,
+    0x4052a2ea,
+    0x40532302,
+    0x4053a315,
+    0x4054232a,
+    0x4054a34d,
+    0x40552378,
+    0x4055a3b5,
+    0x405623da,
+    0x4056a3f3,
+    0x4057240b,
+    0x4057a41e,
+    0x40582433,
+    0x4058a45a,
+    0x40592489,
+    0x4059a4c9,
+    0x405aa4dd,
+    0x405b24f5,
+    0x405ba506,
+    0x405c2519,
+    0x405ca558,
+    0x405d2565,
+    0x405da58a,
+    0x405e25c8,
     0x405e8afe,
-    0x405f25ce,
-    0x405fa5db,
-    0x406025e9,
-    0x4060a60b,
-    0x4061266c,
-    0x4061a6a4,
-    0x406226bb,
-    0x4062a6cc,
-    0x40632719,
-    0x4063a72e,
-    0x40642745,
-    0x4064a771,
-    0x4065278c,
-    0x4065a7a3,
-    0x406627bb,
-    0x4066a7e5,
-    0x40672810,
-    0x4067a855,
-    0x4068289d,
-    0x4068a8be,
-    0x406928f0,
-    0x4069a91e,
-    0x406a293f,
-    0x406aa95f,
-    0x406b2ae7,
-    0x406bab0a,
-    0x406c2b20,
-    0x406cae2a,
-    0x406d2e59,
-    0x406dae81,
-    0x406e2eaf,
-    0x406eaefc,
-    0x406f2f55,
-    0x406faf8d,
-    0x40702fa0,
-    0x4070afbd,
+    0x405f2617,
+    0x405fa624,
+    0x40602632,
+    0x4060a654,
+    0x406126c8,
+    0x4061a700,
+    0x40622717,
+    0x4062a728,
+    0x40632775,
+    0x4063a78a,
+    0x406427a1,
+    0x4064a7cd,
+    0x406527e8,
+    0x4065a7ff,
+    0x40662817,
+    0x4066a841,
+    0x4067286c,
+    0x4067a8b1,
+    0x406828f9,
+    0x4068a91a,
+    0x4069294c,
+    0x4069a97a,
+    0x406a299b,
+    0x406aa9bb,
+    0x406b2b43,
+    0x406bab66,
+    0x406c2b7c,
+    0x406cae86,
+    0x406d2eb5,
+    0x406daedd,
+    0x406e2f0b,
+    0x406eaf58,
+    0x406f2fb1,
+    0x406fafe9,
+    0x40702ffc,
+    0x4070b019,
     0x4071084d,
-    0x4071afcf,
-    0x40722fe2,
-    0x4072b018,
-    0x40733030,
+    0x4071b02b,
+    0x4072303e,
+    0x4072b074,
+    0x4073308c,
     0x407395cd,
-    0x40743044,
-    0x4074b05e,
-    0x4075306f,
-    0x4075b083,
-    0x40763091,
+    0x407430a0,
+    0x4074b0ba,
+    0x407530cb,
+    0x4075b0df,
+    0x407630ed,
     0x4076935b,
-    0x407730b6,
-    0x4077b0f6,
-    0x40783111,
-    0x4078b14a,
-    0x40793161,
-    0x4079b177,
-    0x407a31a3,
-    0x407ab1b6,
-    0x407b31cb,
-    0x407bb1dd,
-    0x407c320e,
-    0x407cb217,
-    0x407d28d9,
+    0x40773112,
+    0x4077b16e,
+    0x40783189,
+    0x4078b1c2,
+    0x407931d9,
+    0x4079b1ef,
+    0x407a321b,
+    0x407ab22e,
+    0x407b3243,
+    0x407bb255,
+    0x407c3286,
+    0x407cb28f,
+    0x407d2935,
     0x407da20d,
-    0x407e3126,
-    0x407ea44f,
+    0x407e319e,
+    0x407ea46a,
     0x407f1e32,
     0x407fa005,
     0x4080217f,
     0x40809e5a,
-    0x408122bd,
+    0x408122d8,
     0x4081a10c,
-    0x40822e9a,
+    0x40822ef6,
     0x40829bad,
-    0x4083242a,
-    0x4083a756,
+    0x40832445,
+    0x4083a7b2,
     0x40841e6e,
-    0x4084a487,
-    0x4085250f,
-    0x4085a633,
-    0x4086258f,
+    0x4084a4a2,
+    0x4085252a,
+    0x4085a68f,
+    0x408625aa,
     0x4086a227,
-    0x40872ee0,
-    0x4087a681,
+    0x40872f3c,
+    0x4087a6dd,
     0x40881beb,
-    0x4088a868,
+    0x4088a8c4,
     0x40891c3a,
     0x40899bc7,
-    0x408a2b58,
+    0x408a2bb4,
     0x408a99e5,
-    0x408b31f2,
-    0x408baf6a,
-    0x408c251f,
+    0x408b326a,
+    0x408bafc6,
+    0x408c253a,
     0x408d1f56,
     0x408d9ea0,
     0x408e2086,
-    0x408ea37a,
-    0x408f287c,
-    0x408fa64f,
-    0x40902831,
-    0x4090a561,
-    0x40912b40,
+    0x408ea395,
+    0x408f28d8,
+    0x408fa6ab,
+    0x4090288d,
+    0x4090a57c,
+    0x40912b9c,
     0x40919a1d,
     0x40921c87,
-    0x4092af1b,
-    0x40932ffb,
+    0x4092af77,
+    0x40933057,
     0x4093a238,
     0x40941e82,
-    0x4094ab71,
-    0x409526dd,
-    0x4095b183,
-    0x40962ec7,
+    0x4094abcd,
+    0x40952739,
+    0x4095b1fb,
+    0x40962f23,
     0x4096a198,
-    0x40972283,
+    0x4097229e,
     0x4097a0d5,
     0x40981ce7,
-    0x4098a6f1,
-    0x40992f37,
-    0x4099a3a7,
-    0x409a2340,
+    0x4098a74d,
+    0x40992f93,
+    0x4099a3c2,
+    0x409a235b,
     0x409a9a01,
     0x409b1edc,
     0x409b9f07,
-    0x409c30d8,
+    0x409c3150,
     0x409c9f2f,
     0x409d2154,
     0x409da122,
@@ -602,41 +602,46 @@
     0x40a021f5,
     0x40a0a0ef,
     0x40a1213d,
-    0x40a1a49b,
-    0x41f42a12,
-    0x41f92aa4,
-    0x41fe2997,
-    0x41feac4d,
-    0x41ff2d7b,
-    0x42032a2b,
-    0x42082a4d,
-    0x4208aa89,
-    0x4209297b,
-    0x4209aac3,
-    0x420a29d2,
-    0x420aa9b2,
-    0x420b29f2,
-    0x420baa6b,
-    0x420c2d97,
-    0x420cab81,
-    0x420d2c34,
-    0x420dac6b,
-    0x42122c9e,
-    0x42172d5e,
-    0x4217ace0,
-    0x421c2d02,
-    0x421f2cbd,
-    0x42212e0f,
-    0x42262d41,
-    0x422b2ded,
-    0x422bac0f,
-    0x422c2dcf,
-    0x422cabc2,
-    0x422d2b9b,
-    0x422dadae,
-    0x422e2bee,
-    0x42302d1d,
-    0x4230ac85,
+    0x40a1a4b6,
+    0x40a22254,
+    0x40a2a608,
+    0x40a3267c,
+    0x40a3b134,
+    0x41f42a6e,
+    0x41f92b00,
+    0x41fe29f3,
+    0x41feaca9,
+    0x41ff2dd7,
+    0x42032a87,
+    0x42082aa9,
+    0x4208aae5,
+    0x420929d7,
+    0x4209ab1f,
+    0x420a2a2e,
+    0x420aaa0e,
+    0x420b2a4e,
+    0x420baac7,
+    0x420c2df3,
+    0x420cabdd,
+    0x420d2c90,
+    0x420dacc7,
+    0x42122cfa,
+    0x42172dba,
+    0x4217ad3c,
+    0x421c2d5e,
+    0x421f2d19,
+    0x42212e6b,
+    0x42262d9d,
+    0x422b2e49,
+    0x422bac6b,
+    0x422c2e2b,
+    0x422cac1e,
+    0x422d2bf7,
+    0x422dae0a,
+    0x422e2c4a,
+    0x42302d79,
+    0x4230ace1,
+    0x423125e9,
     0x44320778,
     0x44328787,
     0x44330793,
@@ -692,71 +697,71 @@
     0x4c4194ad,
     0x4c421616,
     0x4c4293f5,
-    0x503235d8,
-    0x5032b5e7,
-    0x503335f2,
-    0x5033b602,
-    0x5034361b,
-    0x5034b635,
-    0x50353643,
-    0x5035b659,
-    0x5036366b,
-    0x5036b681,
-    0x5037369a,
-    0x5037b6ad,
-    0x503836c5,
-    0x5038b6d6,
-    0x503936eb,
-    0x5039b6ff,
-    0x503a371f,
-    0x503ab735,
-    0x503b374d,
-    0x503bb75f,
-    0x503c377b,
-    0x503cb792,
-    0x503d37ab,
-    0x503db7c1,
-    0x503e37ce,
-    0x503eb7e4,
-    0x503f37f6,
+    0x50323650,
+    0x5032b65f,
+    0x5033366a,
+    0x5033b67a,
+    0x50343693,
+    0x5034b6ad,
+    0x503536bb,
+    0x5035b6d1,
+    0x503636e3,
+    0x5036b6f9,
+    0x50373712,
+    0x5037b725,
+    0x5038373d,
+    0x5038b74e,
+    0x50393763,
+    0x5039b777,
+    0x503a3797,
+    0x503ab7ad,
+    0x503b37c5,
+    0x503bb7d7,
+    0x503c37f3,
+    0x503cb80a,
+    0x503d3823,
+    0x503db839,
+    0x503e3846,
+    0x503eb85c,
+    0x503f386e,
     0x503f83b3,
-    0x50403809,
-    0x5040b819,
-    0x50413833,
-    0x5041b842,
-    0x5042385c,
-    0x5042b879,
-    0x50433889,
-    0x5043b899,
-    0x504438b6,
+    0x50403881,
+    0x5040b891,
+    0x504138ab,
+    0x5041b8ba,
+    0x504238d4,
+    0x5042b8f1,
+    0x50433901,
+    0x5043b911,
+    0x5044392e,
     0x50448469,
-    0x504538ca,
-    0x5045b8e8,
-    0x504638fb,
-    0x5046b911,
-    0x50473923,
-    0x5047b938,
-    0x5048395e,
-    0x5048b96c,
-    0x5049397f,
-    0x5049b994,
-    0x504a39aa,
-    0x504ab9ba,
-    0x504b39da,
-    0x504bb9ed,
-    0x504c3a10,
-    0x504cba3e,
-    0x504d3a6b,
-    0x504dba88,
-    0x504e3aa3,
-    0x504ebabf,
-    0x504f3ad1,
-    0x504fbae8,
-    0x50503af7,
+    0x50453942,
+    0x5045b960,
+    0x50463973,
+    0x5046b989,
+    0x5047399b,
+    0x5047b9b0,
+    0x504839d6,
+    0x5048b9e4,
+    0x504939f7,
+    0x5049ba0c,
+    0x504a3a22,
+    0x504aba32,
+    0x504b3a52,
+    0x504bba65,
+    0x504c3a88,
+    0x504cbab6,
+    0x504d3ae3,
+    0x504dbb00,
+    0x504e3b1b,
+    0x504ebb37,
+    0x504f3b49,
+    0x504fbb60,
+    0x50503b6f,
     0x50508729,
-    0x50513b0a,
-    0x5051b8a8,
-    0x50523a50,
+    0x50513b82,
+    0x5051b920,
+    0x50523ac8,
     0x58320fd1,
     0x68320f93,
     0x68328ceb,
@@ -801,19 +806,19 @@
     0x7c321274,
     0x803214c0,
     0x80328090,
-    0x803332d4,
+    0x8033334c,
     0x803380b9,
-    0x803432e3,
-    0x8034b24b,
-    0x80353269,
-    0x8035b2f7,
-    0x803632ab,
-    0x8036b25a,
-    0x8037329d,
-    0x8037b238,
-    0x803832be,
-    0x8038b27a,
-    0x8039328f,
+    0x8034335b,
+    0x8034b2c3,
+    0x803532e1,
+    0x8035b36f,
+    0x80363323,
+    0x8036b2d2,
+    0x80373315,
+    0x8037b2b0,
+    0x80383336,
+    0x8038b2f2,
+    0x80393307,
 };
 
 extern const size_t kOpenSSLReasonValuesLen;
@@ -1249,6 +1254,7 @@
     "INVALID_OUTER_RECORD_TYPE\0"
     "INVALID_SCT_LIST\0"
     "INVALID_SIGNATURE_ALGORITHM\0"
+    "INVALID_SPAKE2PLUSV1_VALUE\0"
     "INVALID_SSL_SESSION\0"
     "INVALID_TICKET_KEYS_LENGTH\0"
     "KEY_USAGE_BIT_INCORRECT\0"
@@ -1289,10 +1295,13 @@
     "OLD_SESSION_CIPHER_NOT_RETURNED\0"
     "OLD_SESSION_PRF_HASH_MISMATCH\0"
     "OLD_SESSION_VERSION_NOT_RETURNED\0"
+    "PAKE_AND_KEY_SHARE_NOT_ALLOWED\0"
+    "PAKE_EXHAUSTED\0"
     "PARSE_TLSEXT\0"
     "PATH_TOO_LONG\0"
     "PEER_DID_NOT_RETURN_A_CERTIFICATE\0"
     "PEER_ERROR_UNSUPPORTED_CERTIFICATE_TYPE\0"
+    "PEER_PAKE_MISMATCH\0"
     "PRE_SHARED_KEY_MUST_BE_LAST\0"
     "PRIVATE_KEY_OPERATION_FAILED\0"
     "PROTOCOL_IS_SHUTDOWN\0"
@@ -1389,6 +1398,7 @@
     "UNKNOWN_STATE\0"
     "UNSAFE_LEGACY_RENEGOTIATION_DISABLED\0"
     "UNSUPPORTED_COMPRESSION_ALGORITHM\0"
+    "UNSUPPORTED_CREDENTIAL_LIST\0"
     "UNSUPPORTED_ECH_SERVER_CONFIG\0"
     "UNSUPPORTED_ELLIPTIC_CURVE\0"
     "UNSUPPORTED_PROTOCOL\0"
diff --git a/include/openssl/ssl.h b/include/openssl/ssl.h
index b6de7f2..ae8cb84 100644
--- a/include/openssl/ssl.h
+++ b/include/openssl/ssl.h
@@ -3492,6 +3492,120 @@
     SSL_CREDENTIAL *cred, CRYPTO_BUFFER *dc);
 
 
+// Password Authenticated Key Exchange (PAKE).
+//
+// Password Authenticated Key Exchange protocols allow client and server to
+// mutually authenticate one another using knowledge of a password or other
+// low-entropy secret. While the TLS 1.3 pre-shared key (PSK) mechanism can
+// authenticate a high-entropy secret, it cannot be used with low-entropy
+// secrets as the PSK binder values can be used to mount a dictionary attack on
+// a low-entropy PSK. Using TLS 1.3 with a PAKE limits an attacker to confirming
+// one password guess per handshake attempt.
+//
+// WARNING: The PAKE mode in TLS is not a general-purpose authentication scheme.
+// As the underlying secret is still low-entropy, callers must limit brute force
+// attacks across multiple connections, especially in multi-connection protocols
+// such as HTTP. The |error_limit| and |rate_limit| parameters in the functions
+// below may be used to implement this, provided the same |SSL_CREDENTIAL|
+// object is used across connections. Applications using multiple connections
+// should use the PAKE credential only once to authenticate a high-entropy
+// secret, e.g. exporting a PSK from |SSL_export_keying_material|, and use the
+// high-entropy secret for subsequent connections.
+//
+// TODO(crbug.com/369963041): Implement RFC 9258 so one can actually do that.
+//
+// WARNING: PAKE support in TLS is still experimental and may change as the
+// standard evolves. See
+// https://chris-wood.github.io/draft-bmw-tls-pake13/draft-bmw-tls-pake13.html
+//
+// Currently, only the SPAKE2PLUS_V1 named PAKE algorithm is implemented; see
+// https://chris-wood.github.io/draft-bmw-tls-pake13/draft-bmw-tls-pake13.html#section-8.1.
+
+// SSL_PAKE_SPAKE2PLUSV1 is the codepoint for SPAKE2PLUS_V1. See
+// https://chris-wood.github.io/draft-bmw-tls-pake13/draft-bmw-tls-pake13.html#name-named-pake-registry.
+#define SSL_PAKE_SPAKE2PLUSV1 0x7d96
+
+// SSL_spake2plusv1_register computes the values that the client (w0,
+// w1) and server (w0, registration_record) require to run SPAKE2+. These values
+// can be used when calling |SSL_CREDENTIAL_new_spake2plusv1_client| and
+// |SSL_CREDENTIAL_new_spake2plusv1_server|. The client and server identities
+// must match the values passed to those functions.
+//
+// Returns one on success and zero on error.
+OPENSSL_EXPORT int SSL_spake2plusv1_register(
+    uint8_t out_w0[32], uint8_t out_w1[32], uint8_t out_registration_record[65],
+    const uint8_t *password, size_t password_len,
+    const uint8_t *client_identity, size_t client_identity_len,
+    const uint8_t *server_identity, size_t server_identity_len);
+
+// SSL_CREDENTIAL_new_spake2plusv1_client creates a new |SSL_CREDENTIAL| that
+// authenticates using SPAKE2+. It is to be used with a TLS client.
+//
+// The |context|, |client_identity|, and |server_identity| fields serve to
+// identity the SPAKE2+ settings and both sides of a connection must agree on
+// these values. If |context| is |NULL|, a default value will be used.
+//
+// |error_limit| is the number of failed handshakes allowed on the credential.
+// After the limit is reached, using the credential will fail. Ideally this
+// value is set to 1. Setting it to a higher value allows an attacker to have
+// that many attempts at guessing the password using this |SSL_CREDENTIAL|.
+// (Assuming that multiple TLS connections are allowed.)
+//
+// |w0| and |w1| come from calling |SSL_spake2plusv1_register|.
+//
+// Unlike most |SSL_CREDENTIAL|s, PAKE client credentials must be the only
+// credential configured on the connection. BoringSSL does not currently support
+// configuring multiple PAKE credentials as a client, or configuring a mix of
+// PAKE and non-PAKE credentials. Once a PAKE credential is configured, the
+// connection will require the server to authenticate with the same secret, so a
+// successful connection then implies that the server supported the PAKE and
+// knew the password.
+OPENSSL_EXPORT SSL_CREDENTIAL *SSL_CREDENTIAL_new_spake2plusv1_client(
+    const uint8_t *context, size_t context_len, const uint8_t *client_identity,
+    size_t client_identity_len, const uint8_t *server_identity,
+    size_t server_identity_len, uint32_t error_limit, const uint8_t *w0,
+    size_t w0_len, const uint8_t *w1, size_t w1_len);
+
+// SSL_CREDENTIAL_new_spake2plusv1_server creates a new |SSL_CREDENTIAL| that
+// authenticates using SPAKE2+. It is to be used with a TLS server.
+//
+// The |context|, |client_identity|, and |server_identity| fields serve to
+// identity the SPAKE2+ settings and both sides of a connection must agree on
+// these values. If |context| is |NULL|, a default value will be used.
+//
+// |rate_limit| is the number of failed or unfinished handshakes allowed on the
+// credential. After the limit is reached, using the credential will fail.
+// Ideally this value is set to 1. Setting it to a higher value allows an
+// attacker to have that many attempts at guessing the password using this
+// |SSL_CREDENTIAL|. (Assuming that multiple TLS connections are allowed.)
+//
+// WARNING: |rate_limit| differs from the client's |error_limit| parameter.
+// Server PAKE credentials must temporarily deduct incomplete handshakes from
+// the limit, until the peer completes the handshake correctly. Thus
+// applications use that multiple connections in parallel may need a higher
+// limit, and thus higher attacker exposure, to avoid failures. Such
+// applications should instead use one PAKE-based connection to established a
+// high-entropy secret (e.g. with |SSL_export_keying_material|) instead of
+// repeating the PAKE exchange for each connection.
+//
+// |w0| and |registration_record| come from calling |SSL_spake2plusv1_register|,
+// which may be computed externally so that the server does not know the
+// password, or a password-equivalent secret.
+//
+// A server wishing to support a PAKE should install one of these credentials.
+// It is also possible to install certificate-based credentials, in which case
+// both PAKE and non-PAKE clients can be supported. However, if only a PAKE
+// credential is installed then the server knows that any successfully-connected
+// clients also knows the password. Otherwise, the server must be careful to
+// inspect the credential used for a connection before assuming that.
+OPENSSL_EXPORT SSL_CREDENTIAL *SSL_CREDENTIAL_new_spake2plusv1_server(
+    const uint8_t *context, size_t context_len, const uint8_t *client_identity,
+    size_t client_identity_len, const uint8_t *server_identity,
+    size_t server_identity_len, uint32_t rate_limit, const uint8_t *w0,
+    size_t w0_len, const uint8_t *registration_record,
+    size_t registration_record_len);
+
+
 // QUIC integration.
 //
 // QUIC acts as an underlying transport for the TLS 1.3 handshake. The following
@@ -5545,7 +5659,7 @@
   // other than by the supported signature algorithms. But WPA3's "192-bit"
   // mode requires at least P-384 or 3072-bit along the chain. The caller must
   // enforce this themselves on the verified chain using functions such as
-  // `X509_STORE_CTX_get0_chain`.
+  // |X509_STORE_CTX_get0_chain|.
   //
   // Note that this setting is less secure than the default. The
   // implementation risks of using a more obscure primitive like P-384
@@ -6068,6 +6182,10 @@
 #define SSL_R_INCONSISTENT_ECH_NEGOTIATION 321
 #define SSL_R_INVALID_ALPS_CODEPOINT 322
 #define SSL_R_NO_MATCHING_ISSUER 323
+#define SSL_R_INVALID_SPAKE2PLUSV1_VALUE 324
+#define SSL_R_PAKE_EXHAUSTED 325
+#define SSL_R_PEER_PAKE_MISMATCH 326
+#define SSL_R_UNSUPPORTED_CREDENTIAL_LIST 327
 #define SSL_R_SSLV3_ALERT_CLOSE_NOTIFY 1000
 #define SSL_R_SSLV3_ALERT_UNEXPECTED_MESSAGE 1010
 #define SSL_R_SSLV3_ALERT_BAD_RECORD_MAC 1020
@@ -6102,5 +6220,6 @@
 #define SSL_R_TLSV1_ALERT_CERTIFICATE_REQUIRED 1116
 #define SSL_R_TLSV1_ALERT_NO_APPLICATION_PROTOCOL 1120
 #define SSL_R_TLSV1_ALERT_ECH_REQUIRED 1121
+#define SSL_R_PAKE_AND_KEY_SHARE_NOT_ALLOWED 1122
 
 #endif  // OPENSSL_HEADER_SSL_H
diff --git a/include/openssl/tls1.h b/include/openssl/tls1.h
index 99d48c5..ed196ce 100644
--- a/include/openssl/tls1.h
+++ b/include/openssl/tls1.h
@@ -14,7 +14,7 @@
 
 #include <openssl/base.h>
 
-#ifdef  __cplusplus
+#ifdef __cplusplus
 extern "C" {
 #endif
 
@@ -114,6 +114,10 @@
 #define TLSEXT_TYPE_encrypted_client_hello 0xfe0d
 #define TLSEXT_TYPE_ech_outer_extensions 0xfd00
 
+// ExtensionType values from draft-bmw-tls-pake13. This is not an IANA defined
+// extension number.
+#define TLSEXT_TYPE_pake 0x8a3b
+
 // ExtensionType value from RFC 6962
 #define TLSEXT_TYPE_certificate_timestamp 18
 
@@ -153,14 +157,14 @@
 #define TLSEXT_MAXLEN_host_name 255
 
 // PSK ciphersuites from 4279
-#define TLS1_CK_PSK_WITH_RC4_128_SHA                    0x0300008A
-#define TLS1_CK_PSK_WITH_3DES_EDE_CBC_SHA               0x0300008B
-#define TLS1_CK_PSK_WITH_AES_128_CBC_SHA                0x0300008C
-#define TLS1_CK_PSK_WITH_AES_256_CBC_SHA                0x0300008D
+#define TLS1_CK_PSK_WITH_RC4_128_SHA 0x0300008A
+#define TLS1_CK_PSK_WITH_3DES_EDE_CBC_SHA 0x0300008B
+#define TLS1_CK_PSK_WITH_AES_128_CBC_SHA 0x0300008C
+#define TLS1_CK_PSK_WITH_AES_256_CBC_SHA 0x0300008D
 
 // PSK ciphersuites from RFC 5489
-#define TLS1_CK_ECDHE_PSK_WITH_AES_128_CBC_SHA          0x0300C035
-#define TLS1_CK_ECDHE_PSK_WITH_AES_256_CBC_SHA          0x0300C036
+#define TLS1_CK_ECDHE_PSK_WITH_AES_128_CBC_SHA 0x0300C035
+#define TLS1_CK_ECDHE_PSK_WITH_AES_256_CBC_SHA 0x0300C036
 
 // Additional TLS ciphersuites from expired Internet Draft
 // draft-ietf-tls-56-bit-ciphersuites-01.txt
@@ -518,7 +522,7 @@
 #define TLS_MD_MAX_CONST_SIZE 20
 
 
-#ifdef  __cplusplus
+#ifdef __cplusplus
 }  // extern C
 #endif
 
diff --git a/ssl/extensions.cc b/ssl/extensions.cc
index eb15914..c8545fd 100644
--- a/ssl/extensions.cc
+++ b/ssl/extensions.cc
@@ -31,6 +31,7 @@
 #include <openssl/rand.h>
 
 #include "../crypto/internal.h"
+#include "../crypto/spake2plus/internal.h"
 #include "internal.h"
 
 
@@ -917,6 +918,10 @@
   if (hs->max_version < TLS1_2_VERSION) {
     return true;
   }
+  // In PAKE mode, signature_algorithms is not used.
+  if (hs->pake_prover != nullptr) {
+    return true;
+  }
 
   CBB contents, sigalgs_cbb;
   if (!CBB_add_u16(out_compressible, TLSEXT_TYPE_signature_algorithms) ||
@@ -1969,6 +1974,11 @@
   if (hs->max_version < TLS1_3_VERSION) {
     return true;
   }
+  // We do not support resumption with PAKEs, so do not offer any PSK key
+  // exchange modes, to signal the server not to send a ticket.
+  if (hs->pake_prover != nullptr) {
+    return true;
+  }
 
   CBB contents, ke_modes;
   if (!CBB_add_u16(out_compressible, TLSEXT_TYPE_psk_key_exchange_modes) ||
@@ -2116,7 +2126,9 @@
   hs->key_shares[1].reset();
   hs->key_share_bytes.Reset();
 
-  if (hs->max_version < TLS1_3_VERSION) {
+  // If offering a PAKE, do not set up key shares. We do not currently support
+  // clients offering both PAKE and non-PAKE modes, including resumption.
+  if (hs->max_version < TLS1_3_VERSION || hs->pake_prover) {
     return true;
   }
 
@@ -2181,7 +2193,9 @@
 static bool ext_key_share_add_clienthello(const SSL_HANDSHAKE *hs, CBB *out,
                                           CBB *out_compressible,
                                           ssl_client_hello_type_t type) {
-  if (hs->max_version < TLS1_3_VERSION) {
+  // If offering a PAKE, do not set up key shares. We do not currently support
+  // clients offering both PAKE and non-PAKE modes, including resumption.
+  if (hs->max_version < TLS1_3_VERSION || hs->pake_prover) {
     return true;
   }
 
@@ -2202,6 +2216,14 @@
 bool ssl_ext_key_share_parse_serverhello(SSL_HANDSHAKE *hs,
                                          Array<uint8_t> *out_secret,
                                          uint8_t *out_alert, CBS *contents) {
+  if (hs->key_shares[0] == nullptr) {
+    // If we did not offer key shares, the extension should have been rejected
+    // as unsolicited.
+    OPENSSL_PUT_ERROR(SSL, ERR_R_INTERNAL_ERROR);
+    *out_alert = SSL_AD_INTERNAL_ERROR;
+    return false;
+  }
+
   CBS ciphertext;
   uint16_t group_id;
   if (!CBS_get_u16(contents, &group_id) ||
@@ -2237,7 +2259,8 @@
                                          Span<const uint8_t> *out_peer_key,
                                          uint8_t *out_alert,
                                          const SSL_CLIENT_HELLO *client_hello) {
-  // We only support connections that include an ECDHE key exchange.
+  // We only support connections that include an ECDHE key exchange, or use a
+  // PAKE.
   CBS contents;
   if (!ssl_client_hello_get_extension(client_hello, &contents,
                                       TLSEXT_TYPE_key_share)) {
@@ -2286,7 +2309,30 @@
   return true;
 }
 
+bool ssl_ext_pake_add_serverhello(SSL_HANDSHAKE *hs, CBB *out) {
+  if (hs->pake_share_bytes.empty()) {
+    return true;
+  }
+
+  CBB pake_ext, pake_msg;
+  if (!CBB_add_u16(out, TLSEXT_TYPE_pake) ||
+      !CBB_add_u16_length_prefixed(out, &pake_ext) ||
+      !CBB_add_u16(&pake_ext, SSL_PAKE_SPAKE2PLUSV1) ||
+      !CBB_add_u16_length_prefixed(&pake_ext, &pake_msg) ||
+      !CBB_add_bytes(&pake_msg, hs->pake_share_bytes.data(),
+                     hs->pake_share_bytes.size()) ||
+      !CBB_flush(out)) {
+    return false;
+  }
+  return true;
+}
+
 bool ssl_ext_key_share_add_serverhello(SSL_HANDSHAKE *hs, CBB *out) {
+  if (hs->pake_verifier) {
+    // We don't add the key share extension if a PAKE is offered.
+    return true;
+  }
+
   CBB entry, ciphertext;
   if (!CBB_add_u16(out, TLSEXT_TYPE_key_share) ||
       !CBB_add_u16_length_prefixed(out, &entry) ||
@@ -2378,6 +2424,11 @@
                                                  CBB *out_compressible,
                                                  ssl_client_hello_type_t type) {
   const SSL *const ssl = hs->ssl;
+  // In PAKE mode, supported_groups and key_share are not used.
+  if (hs->pake_prover != nullptr) {
+    return true;
+  }
+
   CBB contents, groups_bytes;
   if (!CBB_add_u16(out_compressible, TLSEXT_TYPE_supported_groups) ||
       !CBB_add_u16_length_prefixed(out_compressible, &contents) ||
@@ -2818,6 +2869,220 @@
   return true;
 }
 
+// PAKEs
+//
+// See
+// https://chris-wood.github.io/draft-bmw-tls-pake13/draft-bmw-tls-pake13.html
+
+bool ssl_setup_pake_shares(SSL_HANDSHAKE *hs) {
+  hs->pake_share_bytes.Reset();
+  if (hs->max_version < TLS1_3_VERSION) {
+    return true;
+  }
+
+  Array<SSL_CREDENTIAL *> creds;
+  if (!ssl_get_credential_list(hs, &creds)) {
+    return false;
+  }
+
+  if (std::none_of(creds.begin(), creds.end(), [](SSL_CREDENTIAL *cred) {
+        return cred->type == SSLCredentialType::kSPAKE2PlusV1Client;
+      })) {
+    // If there were no configured PAKE credentials, proceed without filling
+    // in the PAKE extension.
+    return true;
+  }
+
+  // We currently do not support multiple PAKE credentials, or a mix of PAKE and
+  // non-PAKE credentials.
+  if (creds.size() != 1u) {
+    OPENSSL_PUT_ERROR(SSL, SSL_R_UNSUPPORTED_CREDENTIAL_LIST);
+    return false;
+  }
+  SSL_CREDENTIAL *cred = creds[0];
+  assert(cred->type == SSLCredentialType::kSPAKE2PlusV1Client);
+
+  hs->pake_prover = MakeUnique<spake2plus::Prover>();
+  uint8_t prover_share[spake2plus::kShareSize];
+  if (hs->pake_prover == nullptr ||
+      !hs->pake_prover->Init(cred->pake_context, cred->client_identity,
+                             cred->server_identity, cred->password_verifier_w0,
+                             cred->password_verifier_w1) ||
+      !hs->pake_prover->GenerateShare(prover_share)) {
+    return false;
+  }
+
+  hs->credential = UpRef(cred);
+
+  bssl::ScopedCBB cbb;
+  CBB shares, client_identity, server_identity, pake_message;
+  if (!CBB_init(cbb.get(), 64) ||
+      !CBB_add_u16_length_prefixed(cbb.get(), &client_identity) ||
+      !CBB_add_bytes(&client_identity, cred->client_identity.data(),
+                     cred->client_identity.size()) ||
+      !CBB_add_u16_length_prefixed(cbb.get(), &server_identity) ||
+      !CBB_add_bytes(&server_identity, cred->server_identity.data(),
+                     cred->server_identity.size()) ||
+      !CBB_add_u16_length_prefixed(cbb.get(), &shares) ||
+      !CBB_add_u16(&shares, SSL_PAKE_SPAKE2PLUSV1) ||
+      !CBB_add_u16_length_prefixed(&shares, &pake_message) ||
+      !CBB_add_bytes(&pake_message, prover_share, sizeof(prover_share))) {
+    return false;
+  }
+
+  return CBBFinishArray(cbb.get(), &hs->pake_share_bytes);
+}
+
+static bool ext_pake_add_clienthello(const SSL_HANDSHAKE *hs, CBB *out,
+                                     CBB *out_compressible,
+                                     ssl_client_hello_type_t type) {
+  if (hs->pake_share_bytes.empty()) {
+    return true;
+  }
+
+  CBB pake_share_bytes;
+  if (!CBB_add_u16(out_compressible, TLSEXT_TYPE_pake) ||
+      !CBB_add_u16_length_prefixed(out_compressible, &pake_share_bytes) ||
+      !CBB_add_bytes(&pake_share_bytes, hs->pake_share_bytes.data(),
+                     hs->pake_share_bytes.size()) ||
+      !CBB_flush(out_compressible)) {
+    return false;
+  }
+
+  return true;
+}
+
+bool ssl_ext_pake_parse_serverhello(SSL_HANDSHAKE *hs,
+                                    Array<uint8_t> *out_secret,
+                                    uint8_t *out_alert, CBS *contents) {
+  *out_alert = SSL_AD_DECODE_ERROR;
+
+  if (!hs->pake_prover) {
+    // If we did not offer a PAKE, the extension should have been rejected as
+    // unsolicited.
+    OPENSSL_PUT_ERROR(SSL, ERR_R_INTERNAL_ERROR);
+    *out_alert = SSL_AD_INTERNAL_ERROR;
+    return false;
+  }
+
+  CBS pake_msg;
+  uint16_t named_pake;
+  if (!CBS_get_u16(contents, &named_pake) ||
+      !CBS_get_u16_length_prefixed(contents, &pake_msg) ||
+      CBS_len(contents) != 0 ||  //
+      named_pake != SSL_PAKE_SPAKE2PLUSV1) {
+    OPENSSL_PUT_ERROR(SSL, SSL_R_DECODE_ERROR);
+    return false;
+  }
+
+  // Check that the server's PAKE share consists of the right number of
+  // bytes for a PAKE share and a key confirmation message.
+  if (CBS_len(&pake_msg) != spake2plus::kShareSize + spake2plus::kConfirmSize) {
+    OPENSSL_PUT_ERROR(SSL, SSL_R_DECODE_ERROR);
+    *out_alert = SSL_AD_ILLEGAL_PARAMETER;
+    return false;
+  }
+  Span<const uint8_t> pake_msg_span = pake_msg;
+
+  // Releasing the result of |ComputeConfirmation| lets the client confirm one
+  // PAKE guess. If all failures are used up, no more guesses are allowed.
+  if (!hs->credential->HasPAKEAttempts()) {
+    OPENSSL_PUT_ERROR(SSL, SSL_R_PAKE_EXHAUSTED);
+    *out_alert = SSL_AD_INTERNAL_ERROR;
+    return false;
+  }
+
+  uint8_t prover_confirm[spake2plus::kConfirmSize];
+  uint8_t prover_secret[spake2plus::kSecretSize];
+  if (!hs->pake_prover->ComputeConfirmation(
+          prover_confirm, prover_secret,
+          pake_msg_span.subspan(0, spake2plus::kShareSize),
+          pake_msg_span.subspan(spake2plus::kShareSize))) {
+    // Record a failure before releasing the answer to the client.
+    hs->credential->ClaimPAKEAttempt();
+    OPENSSL_PUT_ERROR(SSL, SSL_R_DECODE_ERROR);
+    *out_alert = SSL_AD_ILLEGAL_PARAMETER;
+    return false;
+  }
+
+  Array<uint8_t> secret;
+  if (!secret.CopyFrom(prover_secret)) {
+    OPENSSL_PUT_ERROR(SSL, ERR_R_INTERNAL_ERROR);
+    *out_alert = SSL_AD_INTERNAL_ERROR;
+    return false;
+  }
+
+  *out_secret = std::move(secret);
+  return true;
+}
+
+static bool ext_pake_parse_clienthello(SSL_HANDSHAKE *hs, uint8_t *out_alert,
+                                       CBS *contents) {
+  if (contents == nullptr) {
+    return true;
+  }
+
+  // struct {
+  //     opaque    client_identity<0..2^16-1>;
+  //     opaque    server_identity<0..2^16-1>;
+  //     PAKEShare client_shares<0..2^16-1>;
+  // } PAKEClientHello;
+  //
+  // struct {
+  //     NamedPAKE   named_pake;
+  //     opaque      pake_message<1..2^16-1>;
+  // } PAKEShare;
+
+  *out_alert = SSL_AD_DECODE_ERROR;
+  CBS client_identity, server_identity, shares;
+  if (!CBS_get_u16_length_prefixed(contents, &client_identity) ||
+      !CBS_get_u16_length_prefixed(contents, &server_identity) ||
+      !CBS_get_u16_length_prefixed(contents, &shares) ||
+      CBS_len(contents) != 0) {
+    OPENSSL_PUT_ERROR(SSL, SSL_R_DECODE_ERROR);
+    return false;
+  }
+
+  uint16_t last_named_pake = 0;
+  for (size_t i = 0; CBS_len(&shares) > 0; i++) {
+    uint16_t pake_id;
+    CBS message;
+    if (!CBS_get_u16(&shares, &pake_id) ||
+        !CBS_get_u16_length_prefixed(&shares, &message)) {
+      OPENSSL_PUT_ERROR(SSL, SSL_R_DECODE_ERROR);
+      return false;
+    }
+
+    // PAKEs must be sent in strictly monotonic order.
+    if (i > 0 && last_named_pake >= pake_id) {
+      OPENSSL_PUT_ERROR(SSL, SSL_R_DECODE_ERROR);
+      return false;
+    }
+    last_named_pake = pake_id;
+
+    // We only support one PAKE.
+    if (pake_id != SSL_PAKE_SPAKE2PLUSV1) {
+      continue;
+    }
+
+    // Save the PAKE share for the handshake logic to pick up later.
+    // TODO(crbug.com/391393404): It would be nice if the callback did not have
+    // to copy this.
+    hs->pake_share = MakeUnique<SSLPAKEShare>();
+    if (hs->pake_share == nullptr ||
+        !hs->pake_share->client_identity.CopyFrom(client_identity) ||
+        !hs->pake_share->server_identity.CopyFrom(server_identity) ||
+        !hs->pake_share->pake_message.CopyFrom(message)) {
+      OPENSSL_PUT_ERROR(SSL, ERR_R_INTERNAL_ERROR);
+      return false;
+    }
+    hs->pake_share->named_pake = pake_id;
+  }
+
+  return true;
+}
+
+
 // Application-level Protocol Settings
 //
 // https://tools.ietf.org/html/draft-vvv-tls-alps-01
@@ -3221,6 +3486,15 @@
         ext_certificate_authorities_parse_clienthello,
         dont_add_serverhello,
     },
+    {
+        TLSEXT_TYPE_pake,
+        ext_pake_add_clienthello,
+        // This extension is unencrypted and so adding and parsing it from the
+        // ServerHello is handled elsewhere.
+        forbid_parse_serverhello,
+        ext_pake_parse_clienthello,
+        dont_add_serverhello,
+    },
 };
 
 #define kNumExtensions (sizeof(kExtensions) / sizeof(struct tls_extension))
diff --git a/ssl/handshake_client.cc b/ssl/handshake_client.cc
index 509fdcb..0da7314 100644
--- a/ssl/handshake_client.cc
+++ b/ssl/handshake_client.cc
@@ -332,6 +332,7 @@
   hs->ech_client_outer.Reset();
   hs->cookie.Reset();
   hs->key_share_bytes.Reset();
+  hs->pake_share_bytes.Reset();
 }
 
 static enum ssl_hs_wait_t do_start_connect(SSL_HANDSHAKE *hs) {
@@ -363,6 +364,10 @@
         hs->max_version >= TLS1_2_VERSION ? TLS1_2_VERSION : hs->max_version;
   }
 
+  if (!ssl_setup_pake_shares(hs)) {
+    return ssl_hs_error;
+  }
+
   // If the configured session has expired or is not usable, drop it. We also do
   // not offer sessions on renegotiation.
   SSLSessionType session_type = SSLSessionType::kNotResumable;
@@ -379,6 +384,10 @@
         // Don't offer TLS 1.2 tickets if disabled.
         (session_type == SSLSessionType::kTicket &&
          (SSL_get_options(ssl) & SSL_OP_NO_TICKET)) ||
+        // Don't offer sessions and PAKEs at the same time. We do not currently
+        // support resumption with PAKEs. (Offering both together would need
+        // more logic to conditionally send the key_share extension.)
+        hs->pake_prover != nullptr ||
         !ssl_session_is_time_valid(ssl, ssl->session.get()) ||
         SSL_is_quic(ssl) != int{ssl->session->is_quic} ||
         ssl->s3->initial_handshake_complete) {
@@ -642,6 +651,14 @@
     return ssl_hs_ok;
   }
 
+  // If this client is configured to use a PAKE, then the server must support
+  // TLS 1.3.
+  if (hs->pake_prover) {
+    OPENSSL_PUT_ERROR(SSL, SSL_R_UNSUPPORTED_PROTOCOL);
+    ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_PROTOCOL_VERSION);
+    return ssl_hs_error;
+  }
+
   // Clear some TLS 1.3 state that no longer needs to be retained.
   hs->key_shares[0].reset();
   hs->key_shares[1].reset();
diff --git a/ssl/internal.h b/ssl/internal.h
index fec94ec..9e9c4dd 100644
--- a/ssl/internal.h
+++ b/ssl/internal.h
@@ -17,6 +17,7 @@
 #include <stdlib.h>
 
 #include <algorithm>
+#include <atomic>
 #include <bitset>
 #include <initializer_list>
 #include <limits>
@@ -38,6 +39,7 @@
 #include "../crypto/err/internal.h"
 #include "../crypto/internal.h"
 #include "../crypto/lhash/internal.h"
+#include "../crypto/spake2plus/internal.h"
 
 
 #if defined(OPENSSL_WINDOWS)
@@ -1818,6 +1820,8 @@
 enum class SSLCredentialType {
   kX509,
   kDelegated,
+  kSPAKE2PlusV1Client,
+  kSPAKE2PlusV1Server,
 };
 
 BSSL_NAMESPACE_END
@@ -1912,6 +1916,27 @@
   // OCSP response to be sent to the client, if requested.
   bssl::UniquePtr<CRYPTO_BUFFER> ocsp_response;
 
+  // SPAKE2+-specific information.
+  bssl::Array<uint8_t> pake_context;
+  bssl::Array<uint8_t> client_identity;
+  bssl::Array<uint8_t> server_identity;
+  bssl::Array<uint8_t> password_verifier_w0;
+  bssl::Array<uint8_t> password_verifier_w1;  // server-only
+  bssl::Array<uint8_t> registration_record;   // client-only
+  mutable std::atomic<uint32_t> pake_limit;
+
+  // Checks whether there are still permitted PAKE attempts remaining, without
+  // changing the counter.
+  bool HasPAKEAttempts() const;
+
+  // Atomically decrement |pake_limit|. Return true if successful and false if
+  // |pake_limit| is already zero.
+  bool ClaimPAKEAttempt() const;
+
+  // Atomically increment |pake_limit|. This must be paired with a
+  // |ClaimPAKEAttempt| call.
+  void RestorePAKEAttempt() const;
+
   CRYPTO_EX_DATA ex_data;
 
   // must_match_issuer is a flag indicating that this credential should be
@@ -2062,6 +2087,14 @@
   bool ignore_ticket = false;
 };
 
+struct SSLPAKEShare {
+  static constexpr bool kAllowUniquePtr = true;
+  uint16_t named_pake;
+  Array<uint8_t> client_identity;
+  Array<uint8_t> server_identity;
+  Array<uint8_t> pake_message;
+};
+
 struct SSL_HANDSHAKE {
   explicit SSL_HANDSHAKE(SSL *ssl);
   ~SSL_HANDSHAKE();
@@ -2392,6 +2425,18 @@
 
   // grease_seed is the entropy for GREASE values.
   uint8_t grease_seed[ssl_grease_last_index + 1] = {0};
+
+  // pake_share is the PAKE message received over the wire, if any.
+  UniquePtr<SSLPAKEShare> pake_share;
+
+  // pake_share_bytes are the bytes of the PAKEShare to send, if any.
+  Array<uint8_t> pake_share_bytes;
+
+  // pake_prover is the PAKE context for a client.
+  UniquePtr<spake2plus::Prover> pake_prover;
+
+  // pake_verifier is the PAKE context for a server.
+  UniquePtr<spake2plus::Verifier> pake_verifier;
 };
 
 // kMaxTickets is the maximum number of tickets to send immediately after the
@@ -2464,6 +2509,10 @@
 // a single key share of the specified group.
 bool ssl_setup_key_shares(SSL_HANDSHAKE *hs, uint16_t override_group_id);
 
+// ssl_setup_pake_shares computes the client PAKE shares and saves them in |hs|.
+// It returns true on success and false on failure.
+bool ssl_setup_pake_shares(SSL_HANDSHAKE *hs);
+
 bool ssl_ext_key_share_parse_serverhello(SSL_HANDSHAKE *hs,
                                          Array<uint8_t> *out_secret,
                                          uint8_t *out_alert, CBS *contents);
@@ -2471,8 +2520,13 @@
                                          Span<const uint8_t> *out_peer_key,
                                          uint8_t *out_alert,
                                          const SSL_CLIENT_HELLO *client_hello);
+bool ssl_ext_pake_add_serverhello(SSL_HANDSHAKE *hs, CBB *out);
 bool ssl_ext_key_share_add_serverhello(SSL_HANDSHAKE *hs, CBB *out);
 
+bool ssl_ext_pake_parse_serverhello(SSL_HANDSHAKE *hs,
+                                    Array<uint8_t> *out_secret,
+                                    uint8_t *out_alert, CBS *contents);
+
 bool ssl_ext_pre_shared_key_parse_serverhello(SSL_HANDSHAKE *hs,
                                               uint8_t *out_alert,
                                               CBS *contents);
diff --git a/ssl/ssl_credential.cc b/ssl/ssl_credential.cc
index aa6236f..64c49a6 100644
--- a/ssl/ssl_credential.cc
+++ b/ssl/ssl_credential.cc
@@ -19,6 +19,7 @@
 #include <openssl/span.h>
 
 #include "../crypto/internal.h"
+#include "../crypto/spake2plus/internal.h"
 #include "internal.h"
 
 
@@ -141,15 +142,27 @@
 }
 
 bool ssl_credential_st::UsesX509() const {
-  // Currently, all credential types use X.509. However, we may add other
-  // certificate types in the future. Add the checks in the setters now, so we
-  // don't forget.
-  return true;
+  switch (type) {
+    case SSLCredentialType::kX509:
+    case SSLCredentialType::kDelegated:
+      return true;
+    case SSLCredentialType::kSPAKE2PlusV1Client:
+    case SSLCredentialType::kSPAKE2PlusV1Server:
+      return false;
+  }
+  abort();
 }
 
 bool ssl_credential_st::UsesPrivateKey() const {
-  // Currently, all credential types use private keys. However, we may add PSK
-  return true;
+  switch (type) {
+    case SSLCredentialType::kX509:
+    case SSLCredentialType::kDelegated:
+      return true;
+    case SSLCredentialType::kSPAKE2PlusV1Client:
+    case SSLCredentialType::kSPAKE2PlusV1Server:
+      return false;
+  }
+  abort();
 }
 
 bool ssl_credential_st::IsComplete() const {
@@ -258,6 +271,30 @@
   return false;
 }
 
+bool ssl_credential_st::HasPAKEAttempts() const {
+  return pake_limit.load() != 0;
+}
+
+bool ssl_credential_st::ClaimPAKEAttempt() const {
+  uint32_t current = pake_limit.load(std::memory_order_relaxed);
+  for (;;) {
+    if (current == 0) {
+      return false;
+    }
+    if (pake_limit.compare_exchange_weak(current, current - 1)) {
+      break;
+    }
+  }
+
+  return true;
+}
+
+void ssl_credential_st::RestorePAKEAttempt() const {
+  // This should not overflow because it will only be paired with
+  // ClaimPAKEAttempt.
+  pake_limit.fetch_add(1);
+}
+
 bool ssl_credential_st::AppendIntermediateCert(UniquePtr<CRYPTO_BUFFER> cert) {
   if (!UsesX509()) {
     OPENSSL_PUT_ERROR(SSL, ERR_R_SHOULD_NOT_HAVE_BEEN_CALLED);
@@ -425,6 +462,97 @@
   return 1;
 }
 
+int SSL_spake2plusv1_register(uint8_t out_w0[32], uint8_t out_w1[32],
+                              uint8_t out_registration_record[65],
+                              const uint8_t *password, size_t password_len,
+                              const uint8_t *client_identity,
+                              size_t client_identity_len,
+                              const uint8_t *server_identity,
+                              size_t server_identity_len) {
+  return spake2plus::Register(
+      Span(out_w0, 32), Span(out_w1, 32), Span(out_registration_record, 65),
+      Span(password, password_len), Span(client_identity, client_identity_len),
+      Span(server_identity, server_identity_len));
+}
+
+static UniquePtr<SSL_CREDENTIAL> ssl_credential_new_spake2plusv1(
+    SSLCredentialType type, Span<const uint8_t> context,
+    Span<const uint8_t> client_identity, Span<const uint8_t> server_identity,
+    uint32_t limit) {
+  assert(type == SSLCredentialType::kSPAKE2PlusV1Client ||
+         type == SSLCredentialType::kSPAKE2PlusV1Server);
+  auto cred = MakeUnique<SSL_CREDENTIAL>(type);
+  if (cred == nullptr) {
+    return nullptr;
+  }
+
+  if (!cred->pake_context.CopyFrom(context) ||
+      !cred->client_identity.CopyFrom(client_identity) ||
+      !cred->server_identity.CopyFrom(server_identity)) {
+    return nullptr;
+  }
+
+  cred->pake_limit.store(limit);
+  return cred;
+}
+
+SSL_CREDENTIAL *SSL_CREDENTIAL_new_spake2plusv1_client(
+    const uint8_t *context, size_t context_len, const uint8_t *client_identity,
+    size_t client_identity_len, const uint8_t *server_identity,
+    size_t server_identity_len, uint32_t error_limit, const uint8_t *w0,
+    size_t w0_len, const uint8_t *w1, size_t w1_len) {
+  if (w0_len != spake2plus::kVerifierSize ||
+      w1_len != spake2plus::kVerifierSize ||
+      (context == nullptr && context_len != 0)) {
+    OPENSSL_PUT_ERROR(SSL, SSL_R_INVALID_SPAKE2PLUSV1_VALUE);
+    return nullptr;
+  }
+
+  UniquePtr<SSL_CREDENTIAL> cred = ssl_credential_new_spake2plusv1(
+      SSLCredentialType::kSPAKE2PlusV1Client, Span(context, context_len),
+      Span(client_identity, client_identity_len),
+      Span(server_identity, server_identity_len), error_limit);
+  if (!cred) {
+    return nullptr;
+  }
+
+  if (!cred->password_verifier_w0.CopyFrom(Span(w0, w0_len)) ||
+      !cred->password_verifier_w1.CopyFrom(Span(w1, w1_len))) {
+    return nullptr;
+  }
+
+  return cred.release();
+}
+
+SSL_CREDENTIAL *SSL_CREDENTIAL_new_spake2plusv1_server(
+    const uint8_t *context, size_t context_len, const uint8_t *client_identity,
+    size_t client_identity_len, const uint8_t *server_identity,
+    size_t server_identity_len, uint32_t rate_limit, const uint8_t *w0,
+    size_t w0_len, const uint8_t *registration_record,
+    size_t registration_record_len) {
+  if (w0_len != spake2plus::kVerifierSize ||
+      registration_record_len != spake2plus::kRegistrationRecordSize ||
+      (context == nullptr && context_len != 0)) {
+    return nullptr;
+  }
+
+  UniquePtr<SSL_CREDENTIAL> cred = ssl_credential_new_spake2plusv1(
+      SSLCredentialType::kSPAKE2PlusV1Server, Span(context, context_len),
+      Span(client_identity, client_identity_len),
+      Span(server_identity, server_identity_len), rate_limit);
+  if (!cred) {
+    return nullptr;
+  }
+
+  if (!cred->password_verifier_w0.CopyFrom(Span(w0, w0_len)) ||
+      !cred->registration_record.CopyFrom(
+          Span(registration_record, registration_record_len))) {
+    return nullptr;
+  }
+
+  return cred.release();
+}
+
 int SSL_CTX_add1_credential(SSL_CTX *ctx, SSL_CREDENTIAL *cred) {
   if (!cred->IsComplete()) {
     OPENSSL_PUT_ERROR(SSL, ERR_R_SHOULD_NOT_HAVE_BEEN_CALLED);
diff --git a/ssl/ssl_test.cc b/ssl/ssl_test.cc
index 7bbdb13..b3764c9 100644
--- a/ssl/ssl_test.cc
+++ b/ssl/ssl_test.cc
@@ -3780,7 +3780,6 @@
                                      true /* changed */));
 
   // Verify ticket resumption actually works.
-  bssl::UniquePtr<SSL> client, server;
   bssl::UniquePtr<SSL_SESSION> session =
       CreateClientSession(client_ctx_.get(), server_ctx_.get());
   ASSERT_TRUE(session);
@@ -9857,5 +9856,296 @@
   }
 }
 
+class SSLPAKETest : public testing::Test {
+ public:
+  static Span<const uint8_t> pake_context() {
+    return StringAsBytes("test context");
+  }
+  static Span<const uint8_t> client_identity() {
+    return StringAsBytes("client");
+  }
+  static Span<const uint8_t> server_identity() {
+    return StringAsBytes("client");
+  }
+
+  static UniquePtr<SSL_CTX> NewClientContext(std::string_view password,
+                                             uint32_t attempts) {
+    auto reg = Register(password);
+    if (!reg) {
+      return nullptr;
+    }
+
+    UniquePtr<SSL_CREDENTIAL> cred(SSL_CREDENTIAL_new_spake2plusv1_client(
+        pake_context().data(), pake_context().size(), client_identity().data(),
+        client_identity().size(), server_identity().data(),
+        server_identity().size(), attempts, reg->pw_verifier_w0,
+        sizeof(reg->pw_verifier_w0), reg->pw_verifier_w1,
+        sizeof(reg->pw_verifier_w1)));
+    if (cred == nullptr) {
+      return nullptr;
+    }
+
+    bssl::UniquePtr<SSL_CTX> ctx(SSL_CTX_new(TLS_method()));
+    if (ctx == nullptr || !SSL_CTX_add1_credential(ctx.get(), cred.get())) {
+      return nullptr;
+    }
+    return ctx;
+  }
+
+  static UniquePtr<SSL_CTX> NewServerContext(std::string_view password,
+                                             uint32_t attempts) {
+    auto reg = Register(password);
+    if (!reg) {
+      return nullptr;
+    }
+
+    UniquePtr<SSL_CREDENTIAL> cred(SSL_CREDENTIAL_new_spake2plusv1_server(
+        pake_context().data(), pake_context().size(), client_identity().data(),
+        client_identity().size(), server_identity().data(),
+        server_identity().size(), attempts, reg->pw_verifier_w0,
+        sizeof(reg->pw_verifier_w0), reg->registration_record,
+        sizeof(reg->registration_record)));
+    if (cred == nullptr) {
+      return nullptr;
+    }
+
+    bssl::UniquePtr<SSL_CTX> ctx(SSL_CTX_new(TLS_method()));
+    if (ctx == nullptr || !SSL_CTX_add1_credential(ctx.get(), cred.get())) {
+      return nullptr;
+    }
+    return ctx;
+  }
+
+ private:
+  struct PAKERegistration {
+    uint8_t pw_verifier_w0[32];
+    uint8_t pw_verifier_w1[32];
+    uint8_t registration_record[65];
+  };
+
+  static std::optional<PAKERegistration> Register(std::string_view password) {
+    auto password_bytes = StringAsBytes(password);
+    PAKERegistration ret;
+    if (!SSL_spake2plusv1_register(
+            ret.pw_verifier_w0, ret.pw_verifier_w1, ret.registration_record,
+            password_bytes.data(), password_bytes.size(),
+            client_identity().data(), client_identity().size(),
+            server_identity().data(), server_identity().size())) {
+      return std::nullopt;
+    }
+    return ret;
+  }
+};
+
+TEST_F(SSLPAKETest, SPAKE2PLUS) {
+  UniquePtr<SSL_CTX> client_ctx = NewClientContext("password", 1);
+  ASSERT_TRUE(client_ctx);
+  UniquePtr<SSL_CTX> server_ctx = NewServerContext("password", 1);
+  ASSERT_TRUE(server_ctx);
+  bssl::UniquePtr<SSL> client, server;
+  ASSERT_TRUE(ConnectClientAndServer(&client, &server, client_ctx.get(),
+                                     server_ctx.get()));
+}
+
+TEST_F(SSLPAKETest, ClientLimit) {
+  static constexpr uint32_t kLimit = 5;
+  static constexpr uint32_t kUnlimited = UINT32_MAX;
+
+  UniquePtr<SSL_CTX> client_ctx = NewClientContext("password", kLimit);
+  ASSERT_TRUE(client_ctx);
+  UniquePtr<SSL_CTX> server_ctx_good = NewServerContext("password", kUnlimited);
+  ASSERT_TRUE(server_ctx_good);
+  UniquePtr<SSL_CTX> server_ctx_bad = NewServerContext("wrong", kUnlimited);
+  ASSERT_TRUE(server_ctx_bad);
+
+  // The client sees confirmV before revealing a password confirmation, so
+  // neither successful nor unfinished handshakes contribute to the limit.
+  bssl::UniquePtr<SSL> client, server;
+  for (uint32_t i = 0; i < kLimit * 2; i++) {
+    // Unfinished handshake.
+    ASSERT_TRUE(CreateClientAndServer(&client, &server, client_ctx.get(),
+                                      server_ctx_good.get()));
+    ASSERT_EQ(SSL_do_handshake(client.get()), -1);  // Write ClientHello.
+    ASSERT_EQ(SSL_get_error(client.get(), -1), SSL_ERROR_WANT_READ);
+
+    // Successful handshake.
+    ASSERT_TRUE(ConnectClientAndServer(&client, &server, client_ctx.get(),
+                                       server_ctx_good.get()));
+  }
+
+  // After kLimit - 1 password mismatches, the credential still functions.
+  for (uint32_t i = 0; i < kLimit - 1; i++) {
+    ASSERT_FALSE(ConnectClientAndServer(&client, &server, client_ctx.get(),
+                                        server_ctx_bad.get()));
+  }
+  ASSERT_TRUE(ConnectClientAndServer(&client, &server, client_ctx.get(),
+                                     server_ctx_good.get()));
+
+  // But after one more password mismatch...
+  ASSERT_FALSE(ConnectClientAndServer(&client, &server, client_ctx.get(),
+                                      server_ctx_bad.get()));
+
+  // ...the client should refuse to use the credential at all.
+  ASSERT_FALSE(ConnectClientAndServer(&client, &server, client_ctx.get(),
+                                      server_ctx_good.get()));
+  ASSERT_TRUE(ErrorEquals(ERR_get_error(), ERR_LIB_SSL, SSL_R_PAKE_EXHAUSTED));
+}
+
+TEST_F(SSLPAKETest, ServerLimit) {
+  static constexpr uint32_t kLimit = 5;
+  static constexpr uint32_t kUnlimited = UINT32_MAX;
+
+  UniquePtr<SSL_CTX> server_ctx = NewServerContext("password", kLimit);
+  ASSERT_TRUE(server_ctx);
+  UniquePtr<SSL_CTX> client_ctx_good = NewClientContext("password", kUnlimited);
+  ASSERT_TRUE(client_ctx_good);
+  UniquePtr<SSL_CTX> client_ctx_bad = NewClientContext("wrong", kUnlimited);
+  ASSERT_TRUE(client_ctx_bad);
+
+  // Successful handshakes do not (indefinitely) contribute to the limit. If the
+  // server sees one good handshake at a time, the limit does not impact it.
+  bssl::UniquePtr<SSL> client, server;
+  for (uint32_t i = 0; i < kLimit * 2; i++) {
+    ASSERT_TRUE(ConnectClientAndServer(&client, &server, client_ctx_good.get(),
+                                       server_ctx.get()));
+  }
+
+  // The server sends confirmV before confirming the client knew the password,
+  // so any handshake in between ClientHello and ServerHello counts towards the
+  // limit.
+  struct ClientServerPair {
+    bssl::UniquePtr<SSL> client, server;
+  };
+  std::vector<ClientServerPair> pending;
+  auto handshake_up_to_serverhello = [](ClientServerPair *pair) {
+    // Send ClientHello.
+    ASSERT_EQ(SSL_do_handshake(pair->client.get()), -1);
+    ASSERT_EQ(SSL_get_error(pair->client.get(), -1), SSL_ERROR_WANT_READ);
+    // Send ServerHello..Finished.
+    ASSERT_EQ(SSL_do_handshake(pair->server.get()), -1);
+    ASSERT_EQ(SSL_get_error(pair->server.get(), -1), SSL_ERROR_WANT_READ);
+  };
+
+  // First, go just under the limit.
+  for (uint32_t i = 0; i < kLimit - 1; i++) {
+    ClientServerPair pair;
+    ASSERT_TRUE(CreateClientAndServer(&pair.client, &pair.server,
+                                      client_ctx_good.get(), server_ctx.get()));
+    ASSERT_NO_FATAL_FAILURE(handshake_up_to_serverhello(&pair));
+    pending.push_back(std::move(pair));
+  }
+
+  // The server can still complete a handshake.
+  ASSERT_TRUE(ConnectClientAndServer(&client, &server, client_ctx_good.get(),
+                                     server_ctx.get()));
+
+  // Start one more unfinished handshake.
+  ClientServerPair pair;
+  ASSERT_TRUE(CreateClientAndServer(&pair.client, &pair.server,
+                                    client_ctx_good.get(), server_ctx.get()));
+  ASSERT_NO_FATAL_FAILURE(handshake_up_to_serverhello(&pair));
+  pending.push_back(std::move(pair));
+
+  // The credential is at its limit.
+  ASSERT_FALSE(ConnectClientAndServer(&client, &server, client_ctx_good.get(),
+                                      server_ctx.get()));
+  ASSERT_TRUE(ErrorEquals(ERR_get_error(), ERR_LIB_SSL, SSL_R_PAKE_EXHAUSTED));
+
+  // Complete some of the handshakes. As they complete, the server learns that
+  // the client had the correct guess, so the connections no longer count
+  // towards the brute force limit.
+  static constexpr uint32_t kRemainingLimit = kLimit / 2;
+  for (uint32_t i = 0; i < kRemainingLimit; i++) {
+    ASSERT_TRUE(CompleteHandshakes(pending.back().client.get(),
+                                   pending.back().server.get()));
+    pending.pop_back();
+  }
+
+  // The server can complete a handshake now that some of the limit has been
+  // released.
+  ASSERT_TRUE(ConnectClientAndServer(&client, &server, client_ctx_good.get(),
+                                     server_ctx.get()));
+
+  // Failed handshakes consume the limit. First consume all but one of the newly
+  // released limit.
+  for (uint32_t i = 0; i < kRemainingLimit - 1; i++) {
+    ASSERT_FALSE(ConnectClientAndServer(&client, &server, client_ctx_bad.get(),
+                                        server_ctx.get()));
+  }
+  ASSERT_TRUE(ConnectClientAndServer(&client, &server, client_ctx_good.get(),
+                                     server_ctx.get()));
+
+  // Consume the last of the limit.
+  ASSERT_FALSE(ConnectClientAndServer(&client, &server, client_ctx_bad.get(),
+                                      server_ctx.get()));
+  // The credential is disabled again.
+  ASSERT_FALSE(ConnectClientAndServer(&client, &server, client_ctx_good.get(),
+                                      server_ctx.get()));
+  ASSERT_TRUE(ErrorEquals(ERR_get_error(), ERR_LIB_SSL, SSL_R_PAKE_EXHAUSTED));
+
+  // The unfinished handshakes continue to count toward the limit even if they
+  // are destroyed.
+  pending.clear();
+  ASSERT_FALSE(ConnectClientAndServer(&client, &server, client_ctx_good.get(),
+                                      server_ctx.get()));
+  ASSERT_TRUE(ErrorEquals(ERR_get_error(), ERR_LIB_SSL, SSL_R_PAKE_EXHAUSTED));
+}
+
+#if defined(OPENSSL_THREADS)
+// The PAKE limit mechanism should be thread-safe.
+TEST_F(SSLPAKETest, ClientThreads) {
+  static constexpr uint32_t kLimit = 5;
+  static constexpr uint32_t kUnlimited = UINT32_MAX;
+  static constexpr int kThreads = 10;
+
+  UniquePtr<SSL_CTX> client_ctx = NewClientContext("password", kLimit);
+  ASSERT_TRUE(client_ctx);
+  UniquePtr<SSL_CTX> server_ctx_good = NewServerContext("password", kUnlimited);
+  ASSERT_TRUE(server_ctx_good);
+  UniquePtr<SSL_CTX> server_ctx_bad = NewServerContext("wrong", kUnlimited);
+  ASSERT_TRUE(server_ctx_bad);
+
+  auto connect = [&](SSL_CTX *server_ctx) {
+    bssl::UniquePtr<SSL> client, server;
+    ConnectClientAndServer(&client, &server, client_ctx.get(), server_ctx);
+  };
+
+  std::vector<std::thread> threads;
+  for (int i = 0; i < kThreads; i++) {
+    threads.emplace_back([&] { connect(server_ctx_good.get()); });
+    threads.emplace_back([&] { connect(server_ctx_bad.get()); });
+  }
+  for (auto &thread : threads) {
+    thread.join();
+  }
+}
+TEST_F(SSLPAKETest, ServerThreads) {
+  static constexpr uint32_t kLimit = 5;
+  static constexpr uint32_t kUnlimited = UINT32_MAX;
+  static constexpr int kThreads = 10;
+
+  UniquePtr<SSL_CTX> server_ctx = NewServerContext("password", kLimit);
+  ASSERT_TRUE(server_ctx);
+  UniquePtr<SSL_CTX> client_ctx_good = NewClientContext("password", kUnlimited);
+  ASSERT_TRUE(client_ctx_good);
+  UniquePtr<SSL_CTX> client_ctx_bad = NewClientContext("wrong", kUnlimited);
+  ASSERT_TRUE(client_ctx_bad);
+
+  auto connect = [&](SSL_CTX *client_ctx) {
+    bssl::UniquePtr<SSL> client, server;
+    ConnectClientAndServer(&client, &server, client_ctx, server_ctx.get());
+  };
+
+  std::vector<std::thread> threads;
+  for (int i = 0; i < kThreads; i++) {
+    threads.emplace_back([&] { connect(client_ctx_good.get()); });
+    threads.emplace_back([&] { connect(client_ctx_bad.get()); });
+  }
+  for (auto &thread : threads) {
+    thread.join();
+  }
+}
+#endif  // OPENSSL_THREADS
+
 }  // namespace
 BSSL_NAMESPACE_END
diff --git a/ssl/test/bssl_shim.cc b/ssl/test/bssl_shim.cc
index 143a705..08a57c6 100644
--- a/ssl/test/bssl_shim.cc
+++ b/ssl/test/bssl_shim.cc
@@ -425,6 +425,12 @@
   return true;
 }
 
+static bool IsPAKE(const SSL *ssl) {
+  int idx = GetTestState(ssl)->selected_credential;
+  return idx >= 0 && GetTestConfig(ssl)->credentials[idx].type ==
+                         CredentialConfigType::kSPAKE2PlusV1;
+}
+
 // CheckHandshakeProperties checks, immediately after |ssl| completes its
 // initial handshake (or False Starts), whether all the properties are
 // consistent with the test configuration and invariants.
@@ -660,6 +666,11 @@
       fprintf(stderr, "Received peer certificate on a PSK cipher.\n");
       return false;
     }
+  } else if (IsPAKE(ssl)) {
+    if (SSL_get_peer_cert_chain(ssl) != nullptr) {
+      fprintf(stderr, "Received peer certificate on a PAKE handshake.\n");
+      return false;
+    }
   } else if (!config->is_server || config->require_any_client_certificate) {
     if (SSL_get_peer_cert_chain(ssl) == nullptr) {
       fprintf(stderr, "Received no peer certificate but expected one.\n");
@@ -1257,7 +1268,7 @@
 
   if (GetProtocolVersion(ssl) >= TLS1_3_VERSION && !config->is_server) {
     bool expect_new_session =
-        !config->expect_no_session && !config->shim_shuts_down;
+        !config->expect_no_session && !config->shim_shuts_down && !IsPAKE(ssl);
     if (expect_new_session != test_state->got_new_session) {
       fprintf(stderr,
               "new session was%s cached, but we expected the opposite\n",
diff --git a/ssl/test/runner/common.go b/ssl/test/runner/common.go
index 36914ad..6a04a2c 100644
--- a/ssl/test/runner/common.go
+++ b/ssl/test/runner/common.go
@@ -133,6 +133,7 @@
 	extensionRenegotiationInfo          uint16 = 0xff01
 	extensionQUICTransportParamsLegacy  uint16 = 0xffa5 // draft-ietf-quic-tls-32 and earlier
 	extensionChannelID                  uint16 = 30032  // not IANA assigned
+	extensionPAKE                       uint16 = 35387  // not IANA assigned
 	extensionDuplicate                  uint16 = 0xffff // not IANA assigned
 	extensionEncryptedClientHello       uint16 = 0xfe0d // not IANA assigned
 	extensionECHOuterExtensions         uint16 = 0xfd00 // not IANA assigned
@@ -264,6 +265,9 @@
 // draft-ietf-tls-esni-13, sections 7.2 and 7.2.1.
 const echAcceptConfirmationLength = 8
 
+// Temporary value; pre RFC.
+const spakeID uint16 = 0x7d96
+
 // ConnectionState records basic TLS details about the connection.
 type ConnectionState struct {
 	Version                    uint16                // TLS version used by the connection (e.g. VersionTLS12)
@@ -1711,7 +1715,8 @@
 	SecondHelloRetryRequest bool
 
 	// SendHelloRetryRequestCurve, if non-zero, causes the server to send
-	// the specified curve in a HelloRetryRequest.
+	// the specified curve in a HelloRetryRequest, even if the client did
+	// not offer key shares at all.
 	SendHelloRetryRequestCurve CurveID
 
 	// SendHelloRetryRequestCipherSuite, if non-zero, causes the server to send
@@ -1778,6 +1783,12 @@
 	// TLS 1.3, even in handshakes where it is not allowed, such as resumption.
 	AlwaysSendCertificate bool
 
+	// UseCertificateCredential, if not nil, is the credential to use as a
+	// server for TLS 1.3 Certificate and CertificateVerify messages. This may
+	// be used with AlwaysSendCertificate to authenticate with a certificate
+	// alongside some non-certificate credential.
+	UseCertificateCredential *Credential
+
 	// SendSNIWarningAlert, if true, causes the server to send an
 	// unrecognized_name alert before the ServerHello.
 	SendSNIWarningAlert bool
@@ -2023,6 +2034,32 @@
 
 	// AllowEpochOverflow allows DTLS epoch numbers to wrap around.
 	AllowEpochOverflow bool
+
+	// SendPAKEInHelloRetryRequest causes the server to send a HelloRetryRequest
+	// message containing a PAKE extension.
+	SendPAKEInHelloRetryRequest bool
+
+	// UnsolicitedPAKE, if non-zero, causes a ServerHello to contain a PAKE
+	// response of the specified algorithm, even if the client didn't request it.
+	UnsolicitedPAKE uint16
+
+	// OfferExtraPAKEs, if not empty, is a list of additional PAKE algorithms to
+	// offer as a client. They cannot be negotiated and should be used in tests
+	// where the server is expected to ignore them.
+	OfferExtraPAKEs []uint16
+
+	// OfferExtraPAKEClientID and OfferExtraPAKEServerID are the PAKE client and
+	// server IDs to send with OfferExtraPAKEs. These may be left unset if
+	// configured with a real PAKE credential.
+	OfferExtraPAKEClientID []byte
+	OfferExtraPAKEServerID []byte
+
+	// TruncatePAKEMessage, if true, causes PAKE messages to be truncated.
+	TruncatePAKEMessage bool
+
+	// CheckClientHello is called on the initial ClientHello received from the
+	// peer, to implement extra checks.
+	CheckClientHello func(*clientHelloMsg) error
 }
 
 func (c *Config) serverInit() {
@@ -2194,6 +2231,7 @@
 const (
 	CredentialTypeX509 CredentialType = iota
 	CredentialTypeDelegated
+	CredentialTypeSPAKE2PlusV1
 )
 
 // A Credential is a certificate chain and private key that a TLS endpoint may
@@ -2233,6 +2271,19 @@
 	// SignSignatureAlgorithms, if not nil, overrides the default set of
 	// supported signature algorithms to sign with.
 	SignSignatureAlgorithms []signatureAlgorithm
+	// The following fields are used for PAKE credentials. For simplicity,
+	// we specify the password directly and expect the shim and runner to
+	// compute the client- and server-specific halves as needed.
+	PAKEContext  []byte
+	PAKEClientID []byte
+	PAKEServerID []byte
+	PAKEPassword []byte
+	// WrongPAKERole, if set, causes the shim to be configured with a
+	// credential of the wrong role.
+	WrongPAKERole bool
+	// OverridePAKECodepoint, if non-zero, causes the runner to send the
+	// specified value instead of the actual PAKE codepoint.
+	OverridePAKECodepoint uint16
 }
 
 func (c *Credential) WithSignatureAlgorithms(sigAlgs ...signatureAlgorithm) *Credential {
diff --git a/ssl/test/runner/handshake_client.go b/ssl/test/runner/handshake_client.go
index 09f6d94..0d259fc 100644
--- a/ssl/test/runner/handshake_client.go
+++ b/ssl/test/runner/handshake_client.go
@@ -22,6 +22,7 @@
 	"time"
 
 	"boringssl.googlesource.com/boringssl/ssl/test/runner/hpke"
+	"boringssl.googlesource.com/boringssl/ssl/test/runner/spake2plus"
 	"golang.org/x/crypto/cryptobyte"
 )
 
@@ -40,6 +41,7 @@
 	session        *ClientSessionState
 	finishedBytes  []byte
 	peerPublicKey  crypto.PublicKey
+	pakeContext    *spake2plus.Context
 }
 
 func mapClientHelloVersion(vers uint16, isDTLS bool) uint16 {
@@ -673,6 +675,41 @@
 		}
 	}
 
+	for _, id := range c.config.Bugs.OfferExtraPAKEs {
+		hello.pakeClientID = c.config.Bugs.OfferExtraPAKEClientID
+		hello.pakeServerID = c.config.Bugs.OfferExtraPAKEServerID
+		hello.pakeShares = append(hello.pakeShares, pakeShare{id: id, msg: []byte{1}})
+	}
+	if cred := c.config.Credential; cred != nil && cred.Type == CredentialTypeSPAKE2PlusV1 {
+		if maxVersion < VersionTLS13 {
+			panic("The PAKE extension is only supported in TLS 1.3")
+		}
+		w0, w1, _, err := spake2plus.Register(cred.PAKEPassword, cred.PAKEClientID, cred.PAKEServerID)
+		if err != nil {
+			return nil, err
+		}
+		hs.pakeContext, err = spake2plus.NewProver(cred.PAKEContext, cred.PAKEClientID, cred.PAKEServerID, w0, w1)
+		if err != nil {
+			return nil, err
+		}
+		share, err := hs.pakeContext.GenerateProverShare()
+		if err != nil {
+			return nil, err
+		}
+		if c.config.Bugs.TruncatePAKEMessage {
+			share = share[:len(share)-1]
+		}
+		hello.pakeClientID = cred.PAKEClientID
+		hello.pakeServerID = cred.PAKEServerID
+		id := spakeID
+		if cred.OverridePAKECodepoint != 0 {
+			id = cred.OverridePAKECodepoint
+		}
+		hello.pakeShares = append(hello.pakeShares, pakeShare{id: id, msg: share})
+		hello.hasKeyShares = false
+		hello.keyShares = nil
+	}
+
 	possibleCipherSuites := c.config.cipherSuites()
 	hello.cipherSuites = make([]uint16, 0, len(possibleCipherSuites))
 
@@ -1086,8 +1123,9 @@
 		return fmt.Errorf("tls: server sent non-matching cipher suite %04x vs %04x", hs.suite.id, hs.serverHello.cipherSuite)
 	}
 
+	// ServerHello must be consistent with HelloRetryRequest, if any.
 	if haveHelloRetryRequest {
-		if helloRetryRequest.hasSelectedGroup && helloRetryRequest.selectedGroup != hs.serverHello.keyShare.group {
+		if helloRetryRequest.hasSelectedGroup && (!hs.serverHello.hasKeyShare || helloRetryRequest.selectedGroup != hs.serverHello.keyShare.group) {
 			c.sendAlert(alertHandshakeFailure)
 			return errors.New("tls: ServerHello parameters did not match HelloRetryRequest")
 		}
@@ -1123,29 +1161,55 @@
 	}
 	hs.finishedHash.addEntropy(pskSecret)
 
-	if !hs.serverHello.hasKeyShare {
-		c.sendAlert(alertUnsupportedExtension)
-		return errors.New("tls: server omitted KeyShare on resumption.")
-	}
-
-	// Resolve ECDHE and compute the handshake secret.
-	ecdheSecret := zeroSecret
-	if !c.config.Bugs.MissingKeyShare && !c.config.Bugs.SecondClientHelloMissingKeyShare {
-		kem, ok := hs.keyShares[hs.serverHello.keyShare.group]
-		if !ok {
-			c.sendAlert(alertHandshakeFailure)
-			return errors.New("tls: server selected an unsupported group")
+	sharedSecret := zeroSecret
+	if len(hs.serverHello.pakeMessage) != 0 {
+		if c.didResume {
+			return errors.New("server resumed and returned a PAKE extension")
 		}
-		c.curveID = hs.serverHello.keyShare.group
+		if hs.pakeContext == nil {
+			return errors.New("server selected a PAKE unexpectedly")
+		}
+		if hs.serverHello.pakeID != spakeID {
+			return errors.New("server selected an unknown PAKE")
+		}
+		if expected := 65 + 32; len(hs.serverHello.pakeMessage) != expected {
+			return fmt.Errorf("wrong length SPAKE2+ message, got %d, want %d", len(hs.serverHello.pakeMessage), expected)
+		}
+		if hs.serverHello.hasKeyShare || hs.serverHello.hasPSKIdentity {
+			return errors.New("server included invalid extension with PAKE extension")
+		}
 
 		var err error
-		ecdheSecret, err = kem.decap(c.config, hs.serverHello.keyShare.keyExchange)
-		if err != nil {
-			return err
+		if _, sharedSecret, err = hs.pakeContext.ComputeProverConfirmation(hs.serverHello.pakeMessage[:65], hs.serverHello.pakeMessage[65:]); err != nil {
+			return fmt.Errorf("while computing SPAKE2+ confirmation: %w", err)
+		}
+	} else if hs.pakeContext != nil {
+		return errors.New("server didn't respond with PAKE message")
+	} else {
+		if !hs.serverHello.hasKeyShare {
+			c.sendAlert(alertUnsupportedExtension)
+			return errors.New("tls: server omitted KeyShare on resumption.")
+		}
+
+		// Resolve ECDHE and compute the handshake secret.
+		if !c.config.Bugs.MissingKeyShare && !c.config.Bugs.SecondClientHelloMissingKeyShare {
+			kem, ok := hs.keyShares[hs.serverHello.keyShare.group]
+			if !ok {
+				c.sendAlert(alertHandshakeFailure)
+				return errors.New("tls: server selected an unsupported group")
+			}
+			c.curveID = hs.serverHello.keyShare.group
+
+			var err error
+			sharedSecret, err = kem.decap(c.config, hs.serverHello.keyShare.keyExchange)
+			if err != nil {
+				return err
+			}
 		}
 	}
+
 	hs.finishedHash.nextSecret()
-	hs.finishedHash.addEntropy(ecdheSecret)
+	hs.finishedHash.addEntropy(sharedSecret)
 	hs.writeServerHash(hs.serverHello.marshal())
 
 	// Derive handshake traffic keys and switch read key to handshake
@@ -1178,6 +1242,8 @@
 		c.peerCertificates = hs.session.serverCertificates
 		c.sctList = hs.session.sctList
 		c.ocspResponse = hs.session.ocspResponse
+	} else if hs.pakeContext != nil {
+		// The PAKE authenticates the connection.
 	} else {
 		msg, err := c.readHandshake()
 		if err != nil {
@@ -1867,8 +1933,7 @@
 func (hs *clientHandshakeState) establishKeys() error {
 	c := hs.c
 
-	clientMAC, serverMAC, clientKey, serverKey, clientIV, serverIV :=
-		keysFromMasterSecret(c.vers, hs.suite, hs.masterSecret, hs.hello.random, hs.serverHello.random, hs.suite.macLen, hs.suite.keyLen, hs.suite.ivLen(c.vers))
+	clientMAC, serverMAC, clientKey, serverKey, clientIV, serverIV := keysFromMasterSecret(c.vers, hs.suite, hs.masterSecret, hs.hello.random, hs.serverHello.random, hs.suite.macLen, hs.suite.keyLen, hs.suite.ivLen(c.vers))
 	var clientCipher, serverCipher any
 	var clientHash, serverHash macFunction
 	if hs.suite.cipher != nil {
diff --git a/ssl/test/runner/handshake_messages.go b/ssl/test/runner/handshake_messages.go
index 9a41b72..5232581 100644
--- a/ssl/test/runner/handshake_messages.go
+++ b/ssl/test/runner/handshake_messages.go
@@ -145,6 +145,11 @@
 	payload  []byte
 }
 
+type pakeShare struct {
+	id  uint16
+	msg []byte
+}
+
 type clientHelloMsg struct {
 	raw                                      []byte
 	isDTLS                                   bool
@@ -197,6 +202,9 @@
 	delegatedCredential                      []signatureAlgorithm
 	alpsProtocols                            []string
 	alpsProtocolsOld                         []string
+	pakeClientID                             []byte
+	pakeServerID                             []byte
+	pakeShares                               []pakeShare
 	outerExtensions                          []uint16
 	reorderOuterExtensionsWithoutCompressing bool
 	prefixExtensions                         []uint16
@@ -537,7 +545,21 @@
 			body: body.BytesOrPanic(),
 		})
 	}
-
+	if len(m.pakeShares) > 0 {
+		body := cryptobyte.NewBuilder(nil)
+		addUint16LengthPrefixedBytes(body, m.pakeClientID)
+		addUint16LengthPrefixedBytes(body, m.pakeServerID)
+		body.AddUint16LengthPrefixed(func(shares *cryptobyte.Builder) {
+			for _, share := range m.pakeShares {
+				shares.AddUint16(share.id)
+				addUint16LengthPrefixedBytes(shares, share.msg)
+			}
+		})
+		extensions = append(extensions, extension{
+			id:   extensionPAKE,
+			body: body.BytesOrPanic(),
+		})
+	}
 	// The PSK extension must be last. See https://tools.ietf.org/html/rfc8446#section-4.2.11
 	if len(m.pskIdentities) > 0 {
 		pskExtension := cryptobyte.NewBuilder(nil)
@@ -759,6 +781,9 @@
 	m.delegatedCredential = nil
 	m.alpsProtocols = nil
 	m.alpsProtocolsOld = nil
+	m.pakeClientID = nil
+	m.pakeServerID = nil
+	m.pakeShares = nil
 
 	if len(reader) == 0 {
 		// ClientHello is optionally followed by extension data
@@ -1057,6 +1082,25 @@
 				}
 				m.alpsProtocolsOld = append(m.alpsProtocolsOld, string(protocol))
 			}
+		case extensionPAKE:
+			var clientId, serverId, shares cryptobyte.String
+			if !body.ReadUint16LengthPrefixed(&clientId) ||
+				!body.ReadUint16LengthPrefixed(&serverId) ||
+				!body.ReadUint16LengthPrefixed(&shares) ||
+				len(body) != 0 {
+				return false
+			}
+			for len(shares) > 0 {
+				var id uint16
+				var msg cryptobyte.String
+				if !shares.ReadUint16(&id) ||
+					!shares.ReadUint16LengthPrefixed(&msg) {
+					return false
+				}
+				m.pakeClientID = []byte(clientId)
+				m.pakeServerID = []byte(serverId)
+				m.pakeShares = append(m.pakeShares, pakeShare{id: id, msg: msg})
+			}
 		}
 
 		if isGREASEValue(extension) {
@@ -1209,6 +1253,8 @@
 	omitExtensions        bool
 	emptyExtensions       bool
 	extensions            serverExtensions
+	pakeID                uint16
+	pakeMessage           []byte
 }
 
 func (m *serverHelloMsg) marshal() []byte {
@@ -1266,6 +1312,13 @@
 						extensions.AddUint16(m.vers)
 					}
 				}
+				if len(m.pakeMessage) != 0 {
+					extensions.AddUint16(extensionPAKE)
+					extensions.AddUint16LengthPrefixed(func(share *cryptobyte.Builder) {
+						share.AddUint16(m.pakeID)
+						addUint16LengthPrefixedBytes(share, m.pakeMessage)
+					})
+				}
 				if len(m.customExtension) > 0 {
 					extensions.AddUint16(extensionCustom)
 					addUint16LengthPrefixedBytes(extensions, []byte(m.customExtension))
@@ -1376,6 +1429,10 @@
 				m.hasPSKIdentity = true
 			case extensionSupportedVersions:
 				// Parsed above.
+			case extensionPAKE:
+				if !body.ReadUint16(&m.pakeID) || !readUint16LengthPrefixedBytes(&body, &m.pakeMessage) {
+					return false
+				}
 			default:
 				// Only allow the 3 extensions that are sent in
 				// the clear in TLS 1.3.
@@ -1630,11 +1687,11 @@
 				return false
 			}
 		case extensionALPN:
-			var protocols, protocol cryptobyte.String
-			if !body.ReadUint16LengthPrefixed(&protocols) ||
+			var pakes, protocol cryptobyte.String
+			if !body.ReadUint16LengthPrefixed(&pakes) ||
 				len(body) != 0 ||
-				!protocols.ReadUint8LengthPrefixed(&protocol) ||
-				len(protocols) != 0 {
+				!pakes.ReadUint8LengthPrefixed(&protocol) ||
+				len(pakes) != 0 {
 				return false
 			}
 			m.alpnProtocol = string(protocol)
@@ -1815,6 +1872,8 @@
 	echConfirmation       []byte
 	echConfirmationOffset int
 	duplicateExtensions   bool
+	pakeID                uint16
+	pakeMessage           []byte
 }
 
 func (m *helloRetryRequestMsg) marshal() []byte {
@@ -1850,6 +1909,13 @@
 					extensions.AddUint16(2) // length
 					extensions.AddUint16(uint16(m.selectedGroup))
 				}
+				if len(m.pakeMessage) != 0 {
+					extensions.AddUint16(extensionPAKE)
+					extensions.AddUint16LengthPrefixed(func(share *cryptobyte.Builder) {
+						share.AddUint16(m.pakeID)
+						addUint16LengthPrefixedBytes(share, m.pakeMessage)
+					})
+				}
 				// m.cookie may be a non-nil empty slice for empty cookie tests.
 				if m.cookie != nil {
 					extensions.AddUint16(extensionCookie)
@@ -2000,7 +2066,6 @@
 				}
 			}
 		})
-
 	})
 
 	m.raw = certMsg.BytesOrPanic()
@@ -2708,8 +2773,7 @@
 	return true
 }
 
-type helloRequestMsg struct {
-}
+type helloRequestMsg struct{}
 
 func (*helloRequestMsg) marshal() []byte {
 	return []byte{typeHelloRequest, 0, 0, 0}
diff --git a/ssl/test/runner/handshake_server.go b/ssl/test/runner/handshake_server.go
index 609ec80..3470e17 100644
--- a/ssl/test/runner/handshake_server.go
+++ b/ssl/test/runner/handshake_server.go
@@ -21,6 +21,7 @@
 	"time"
 
 	"boringssl.googlesource.com/boringssl/ssl/test/runner/hpke"
+	"boringssl.googlesource.com/boringssl/ssl/test/runner/spake2plus"
 	"golang.org/x/crypto/cryptobyte"
 )
 
@@ -171,6 +172,11 @@
 	if err != nil {
 		return err
 	}
+	if config.Bugs.CheckClientHello != nil {
+		if err = config.Bugs.CheckClientHello(hs.clientHello); err != nil {
+			return err
+		}
+	}
 	if size := config.Bugs.RequireClientHelloSize; size != 0 && len(hs.clientHello.raw) != size {
 		return fmt.Errorf("tls: ClientHello record size is %d, but expected %d", len(hs.clientHello.raw), size)
 	}
@@ -672,7 +678,8 @@
 		return errors.New("tls: early data extension received in DTLS")
 	}
 
-	hs.hello.hasKeyShare = true
+	// Decide whether to use key_share.
+	hs.hello.hasKeyShare = config.Credential.Type != CredentialTypeSPAKE2PlusV1 && config.Bugs.UnsolicitedPAKE == 0
 	if hs.sessionState != nil && config.Bugs.NegotiatePSKResumption {
 		hs.hello.hasKeyShare = false
 	}
@@ -680,7 +687,8 @@
 		hs.hello.hasKeyShare = false
 	}
 
-	var sendHelloRetryRequest bool
+	// Decide whether a HelloRetryRequest is needed.
+	sendHelloRetryRequest := config.Bugs.AlwaysSendHelloRetryRequest
 	cipherSuite := hs.suite.id
 	if config.Bugs.SendHelloRetryRequestCipherSuite != 0 {
 		cipherSuite = config.Bugs.SendHelloRetryRequestCipherSuite
@@ -694,10 +702,6 @@
 		duplicateExtensions: config.Bugs.DuplicateHelloRetryRequestExtensions,
 	}
 
-	if config.Bugs.AlwaysSendHelloRetryRequest {
-		sendHelloRetryRequest = true
-	}
-
 	if config.Bugs.SendHelloRetryRequestCookie != nil {
 		sendHelloRetryRequest = true
 		helloRetryRequest.cookie = config.Bugs.SendHelloRetryRequestCookie
@@ -755,6 +759,12 @@
 		sendHelloRetryRequest = false
 	}
 
+	if config.Bugs.SendPAKEInHelloRetryRequest {
+		helloRetryRequest.pakeID = spakeID
+		helloRetryRequest.pakeMessage = []byte{1}
+		sendHelloRetryRequest = true
+	}
+
 	if sendHelloRetryRequest {
 		hs.finishedHash.UpdateForHelloRetryRequest()
 
@@ -1015,6 +1025,51 @@
 				keyExchange: ciphertext,
 			}
 		}
+	} else if hs.cert.Type == CredentialTypeSPAKE2PlusV1 {
+		if len(hs.clientHello.pakeShares) == 0 {
+			return errors.New("tls: client not configured with PAKE")
+		}
+		if !bytes.Equal(hs.clientHello.pakeClientID, hs.cert.PAKEClientID) ||
+			!bytes.Equal(hs.clientHello.pakeServerID, hs.cert.PAKEServerID) {
+			return fmt.Errorf("tls: client configured with different PAKE identities: got (%x, %x), wanted (%x, %x)", hs.clientHello.pakeClientID, hs.clientHello.pakeServerID, hs.cert.PAKEClientID, hs.cert.PAKEServerID)
+		}
+		var pakeMessage []byte
+		for _, pake := range hs.clientHello.pakeShares {
+			if pake.id == spakeID {
+				pakeMessage = pake.msg
+			}
+		}
+		if pakeMessage == nil {
+			return errors.New("tls: client does not support SPAKE2+")
+		}
+		w0, _, registrationRecord, err := spake2plus.Register(hs.cert.PAKEPassword, hs.cert.PAKEClientID, hs.cert.PAKEServerID)
+		if err != nil {
+			return err
+		}
+		pake, err := spake2plus.NewVerifier(hs.cert.PAKEContext, hs.cert.PAKEClientID, hs.cert.PAKEServerID, w0, registrationRecord)
+		if err != nil {
+			return err
+		}
+		share, confirm, sharedSecret, err := pake.ProcessProverShare(pakeMessage)
+		if err != nil {
+			c.sendAlert(alertHandshakeFailure)
+			return fmt.Errorf("while processing SPAKE2+ prover share: %w", err)
+		}
+		hs.finishedHash.nextSecret()
+		hs.finishedHash.addEntropy(sharedSecret)
+		hs.hello.pakeID = spakeID
+		if hs.cert.OverridePAKECodepoint != 0 {
+			hs.hello.pakeID = hs.cert.OverridePAKECodepoint
+		}
+		hs.hello.pakeMessage = slices.Concat(share, confirm)
+		if c.config.Bugs.TruncatePAKEMessage {
+			hs.hello.pakeMessage = hs.hello.pakeMessage[:len(hs.hello.pakeMessage)-1]
+		}
+	} else if config.Bugs.UnsolicitedPAKE != 0 {
+		hs.finishedHash.nextSecret()
+		hs.finishedHash.addEntropy(hs.finishedHash.zeroSecret())
+		hs.hello.pakeID = config.Bugs.UnsolicitedPAKE
+		hs.hello.pakeMessage = []byte{1}
 	} else {
 		hs.finishedHash.nextSecret()
 		hs.finishedHash.addEntropy(hs.finishedHash.zeroSecret())
@@ -1068,7 +1123,10 @@
 		c.writeRecord(recordTypeHandshake, encryptedExtensions.marshal())
 	}
 
-	requestClientCert := config.ClientAuth >= RequestClientCert && (hs.sessionState == nil || config.Bugs.AlwaysSendCertificateRequest)
+	var requestClientCert bool
+	if (hs.sessionState == nil && hs.cert.Type != CredentialTypeSPAKE2PlusV1) || config.Bugs.AlwaysSendCertificateRequest {
+		requestClientCert = config.ClientAuth >= RequestClientCert
+	}
 	if requestClientCert {
 		// Request a client certificate
 		certReq := &certificateRequestMsg{
@@ -1094,21 +1152,25 @@
 		c.writeRecord(recordTypeHandshake, certReq.marshal())
 	}
 
-	if hs.sessionState == nil || config.Bugs.AlwaysSendCertificate {
+	if (hs.sessionState == nil && hs.cert.Type != CredentialTypeSPAKE2PlusV1) || config.Bugs.AlwaysSendCertificate {
+		useCert := hs.cert
+		if config.Bugs.UseCertificateCredential != nil {
+			useCert = config.Bugs.UseCertificateCredential
+		}
 		certMsg := &certificateMsg{
 			hasRequestContext: true,
 		}
 		if !config.Bugs.EmptyCertificateList {
-			for i, certData := range hs.cert.Certificate {
+			for i, certData := range useCert.Certificate {
 				cert := certificateEntry{
 					data: certData,
 				}
 				if i == 0 {
 					if hs.clientHello.ocspStapling && !c.config.Bugs.NoOCSPStapling {
-						cert.ocspResponse = hs.cert.OCSPStaple
+						cert.ocspResponse = useCert.OCSPStaple
 					}
 					if hs.clientHello.sctListSupported && !c.config.Bugs.NoSignedCertificateTimestamps {
-						cert.sctList = hs.cert.SignedCertificateTimestampList
+						cert.sctList = useCert.SignedCertificateTimestampList
 					}
 					cert.duplicateExtensions = config.Bugs.SendDuplicateCertExtensions
 					cert.extraExtension = config.Bugs.SendExtensionOnCertificate
@@ -1172,13 +1234,13 @@
 
 		// Determine the hash to sign.
 		var err error
-		certVerify.signatureAlgorithm, err = selectSignatureAlgorithm(c.isClient, c.vers, hs.cert, config, hs.clientHello.signatureAlgorithms)
+		certVerify.signatureAlgorithm, err = selectSignatureAlgorithm(c.isClient, c.vers, useCert, config, hs.clientHello.signatureAlgorithms)
 		if err != nil {
 			c.sendAlert(alertInternalError)
 			return err
 		}
 
-		privKey := hs.cert.PrivateKey
+		privKey := useCert.PrivateKey
 		input := hs.finishedHash.certificateVerifyInput(serverCertificateVerifyContextTLS13)
 		certVerify.signature, err = signMessage(c.isClient, c.vers, privKey, c.config, certVerify.signatureAlgorithm, input)
 		if err != nil {
@@ -2042,8 +2104,7 @@
 func (hs *serverHandshakeState) establishKeys() error {
 	c := hs.c
 
-	clientMAC, serverMAC, clientKey, serverKey, clientIV, serverIV :=
-		keysFromMasterSecret(c.vers, hs.suite, hs.masterSecret, hs.clientHello.random, hs.hello.random, hs.suite.macLen, hs.suite.keyLen, hs.suite.ivLen(c.vers))
+	clientMAC, serverMAC, clientKey, serverKey, clientIV, serverIV := keysFromMasterSecret(c.vers, hs.suite, hs.masterSecret, hs.clientHello.random, hs.hello.random, hs.suite.macLen, hs.suite.keyLen, hs.suite.ivLen(c.vers))
 
 	var clientCipher, serverCipher any
 	var clientHash, serverHash macFunction
diff --git a/ssl/test/runner/runner.go b/ssl/test/runner/runner.go
index b16f43b..31ebc99 100644
--- a/ssl/test/runner/runner.go
+++ b/ssl/test/runner/runner.go
@@ -205,13 +205,17 @@
 
 var channelIDBytes []byte
 
-var testOCSPResponse = []byte{1, 2, 3, 4}
-var testOCSPResponse2 = []byte{5, 6, 7, 8}
-var testSCTList = []byte{0, 6, 0, 4, 5, 6, 7, 8}
-var testSCTList2 = []byte{0, 6, 0, 4, 1, 2, 3, 4}
+var (
+	testOCSPResponse  = []byte{1, 2, 3, 4}
+	testOCSPResponse2 = []byte{5, 6, 7, 8}
+	testSCTList       = []byte{0, 6, 0, 4, 5, 6, 7, 8}
+	testSCTList2      = []byte{0, 6, 0, 4, 1, 2, 3, 4}
+)
 
-var testOCSPExtension = append([]byte{byte(extensionStatusRequest) >> 8, byte(extensionStatusRequest), 0, 8, statusTypeOCSP, 0, 0, 4}, testOCSPResponse...)
-var testSCTExtension = append([]byte{byte(extensionSignedCertificateTimestamp) >> 8, byte(extensionSignedCertificateTimestamp), 0, byte(len(testSCTList))}, testSCTList...)
+var (
+	testOCSPExtension = append([]byte{byte(extensionStatusRequest) >> 8, byte(extensionStatusRequest), 0, 8, statusTypeOCSP, 0, 0, 4}, testOCSPResponse...)
+	testSCTExtension  = append([]byte{byte(extensionSignedCertificateTimestamp) >> 8, byte(extensionSignedCertificateTimestamp), 0, byte(len(testSCTList))}, testSCTList...)
+)
 
 var (
 	rsaCertificate       Credential
@@ -682,7 +686,8 @@
 	// shimCredentials is a list of credentials which should be configured at
 	// the shim. It differs from shimCertificate only in whether the old or
 	// new APIs are used.
-	shimCredentials []*Credential
+	shimCredentials       []*Credential
+	resumeShimCredentials []*Credential
 }
 
 var testCases []testCase
@@ -1071,7 +1076,7 @@
 		// If readWithUnfinishedWrite is set, the shim prefix will be
 		// available later.
 		if shimPrefix != "" && !test.readWithUnfinishedWrite {
-			var buf = make([]byte, len(shimPrefix))
+			buf := make([]byte, len(shimPrefix))
 			_, err := io.ReadFull(tlsConn, buf)
 			if err != nil {
 				return err
@@ -1165,7 +1170,7 @@
 
 		// Consume the shim prefix if needed.
 		if shimPrefix != "" {
-			var buf = make([]byte, len(shimPrefix))
+			buf := make([]byte, len(shimPrefix))
 			_, err := io.ReadFull(tlsConn, buf)
 			if err != nil {
 				return err
@@ -1464,6 +1469,8 @@
 			flags = append(flags, prefix+"-new-x509-credential")
 		case CredentialTypeDelegated:
 			flags = append(flags, prefix+"-new-delegated-credential")
+		case CredentialTypeSPAKE2PlusV1:
+			flags = append(flags, prefix+"-new-spake2plusv1-credential")
 		default:
 			panic(fmt.Errorf("unknown credential type %d", cred.Type))
 		}
@@ -1471,23 +1478,30 @@
 		panic("default credential must be X.509")
 	}
 
+	handleBase64Field := func(flag string, value []byte) {
+		if len(value) != 0 {
+			flags = append(flags, fmt.Sprintf("%s-%s", prefix, flag), base64FlagValue(value))
+		}
+	}
+
 	if len(cred.ChainPath) != 0 {
 		flags = append(flags, prefix+"-cert-file", cred.ChainPath)
 	}
 	if len(cred.KeyPath) != 0 {
 		flags = append(flags, prefix+"-key-file", cred.KeyPath)
 	}
-	if len(cred.OCSPStaple) != 0 {
-		flags = append(flags, prefix+"-ocsp-response", base64FlagValue(cred.OCSPStaple))
-	}
-	if len(cred.SignedCertificateTimestampList) != 0 {
-		flags = append(flags, prefix+"-signed-cert-timestamps", base64FlagValue(cred.SignedCertificateTimestampList))
-	}
+	handleBase64Field("ocsp-response", cred.OCSPStaple)
+	handleBase64Field("signed-cert-timestamps", cred.SignedCertificateTimestampList)
 	for _, sigAlg := range cred.SignatureAlgorithms {
 		flags = append(flags, prefix+"-signing-prefs", strconv.Itoa(int(sigAlg)))
 	}
-	if len(cred.DelegatedCredential) != 0 {
-		flags = append(flags, prefix+"-delegated-credential", base64FlagValue(cred.DelegatedCredential))
+	handleBase64Field("delegated-credential", cred.DelegatedCredential)
+	handleBase64Field("pake-context", cred.PAKEContext)
+	handleBase64Field("pake-client-id", cred.PAKEClientID)
+	handleBase64Field("pake-server-id", cred.PAKEServerID)
+	handleBase64Field("pake-password", cred.PAKEPassword)
+	if cred.WrongPAKERole {
+		flags = append(flags, prefix+"-wrong-pake-role")
 	}
 	return flags
 }
@@ -1511,7 +1525,7 @@
 
 	// Configure the default credential.
 	shimCertificate := test.shimCertificate
-	if shimCertificate == nil && len(test.shimCredentials) == 0 && test.testType == serverTest && len(test.config.PreSharedKey) == 0 {
+	if shimCertificate == nil && len(test.shimCredentials) == 0 && len(test.resumeShimCredentials) == 0 && test.testType == serverTest && len(test.config.PreSharedKey) == 0 {
 		shimCertificate = &rsaCertificate
 	}
 	if shimCertificate != nil {
@@ -1529,6 +1543,9 @@
 	for _, cred := range test.shimCredentials {
 		flags = appendCredentialFlags(flags, cred, "", true)
 	}
+	for _, cred := range test.resumeShimCredentials {
+		flags = appendCredentialFlags(flags, cred, "-on-resume", true)
+	}
 
 	if test.protocol == dtls {
 		flags = append(flags, "-dtls")
@@ -2009,6 +2026,7 @@
 		if test.protocol != tls ||
 			test.testType != serverTest ||
 			len(test.shimCredentials) != 0 ||
+			len(test.resumeShimCredentials) != 0 ||
 			strings.Contains(test.name, "ECH-Server") ||
 			test.skipSplitHandshake {
 			continue
@@ -4572,7 +4590,7 @@
 }
 
 func addCBCSplittingTests() {
-	var cbcCiphers = []struct {
+	cbcCiphers := []struct {
 		name   string
 		cipher uint16
 	}{
@@ -4892,7 +4910,6 @@
 			"-use-client-ca-list", "<EMPTY>",
 		},
 	})
-
 }
 
 func addExtendedMasterSecretTests() {
@@ -7802,7 +7819,7 @@
 						config: Config{
 							MaxVersion:          ver.version,
 							NextProtos:          []string{"proto"},
-							ApplicationSettings: map[string][]byte{"proto": []byte{}},
+							ApplicationSettings: map[string][]byte{"proto": {}},
 							ALPSUseNewCodepoint: alpsCodePoint,
 						},
 						resumeSession: true,
@@ -7822,7 +7839,7 @@
 						config: Config{
 							MaxVersion:          ver.version,
 							NextProtos:          []string{"proto"},
-							ApplicationSettings: map[string][]byte{"proto": []byte{}},
+							ApplicationSettings: map[string][]byte{"proto": {}},
 							ALPSUseNewCodepoint: alpsCodePoint,
 						},
 						resumeSession: true,
@@ -7887,7 +7904,7 @@
 						config: Config{
 							MaxVersion:          ver.version,
 							NextProtos:          []string{"proto"},
-							ApplicationSettings: map[string][]byte{"proto": []byte{}},
+							ApplicationSettings: map[string][]byte{"proto": {}},
 							Bugs:                bugs,
 							ALPSUseNewCodepoint: alpsCodePoint,
 						},
@@ -10467,8 +10484,10 @@
 	{"ECDSA", 0, &ecdsaP256Certificate, CurveP256},
 }
 
-const fakeSigAlg1 signatureAlgorithm = 0x2a01
-const fakeSigAlg2 signatureAlgorithm = 0xff01
+const (
+	fakeSigAlg1 signatureAlgorithm = 0x2a01
+	fakeSigAlg2 signatureAlgorithm = 0xff01
+)
 
 func addSignatureAlgorithmTests() {
 	// Not all ciphers involve a signature. Advertise a list which gives all
@@ -13026,7 +13045,6 @@
 		{"AEAD-AES128-GCM-SHA256", TLS_AES_128_GCM_SHA256},
 		{"AEAD-AES256-GCM-SHA384", TLS_AES_256_GCM_SHA384},
 	} {
-
 		testCases = append(testCases, testCase{
 			name: "ExportTrafficSecrets-" + cipherSuite.name,
 			config: Config{
@@ -18288,7 +18306,7 @@
 		flags: []string{"-max-version", strconv.Itoa(VersionTLS12)},
 	})
 
-	var clientHelloTests = []struct {
+	clientHelloTests := []struct {
 		clientHello []byte
 		isJDK11     bool
 	}{
@@ -18596,7 +18614,8 @@
 	{
 		name:   "HKDF-SHA256-AES-256-GCM",
 		cipher: HPKECipherSuite{KDF: hpke.HKDFSHA256, AEAD: hpke.AES256GCM},
-	}, {
+	},
+	{
 		name:   "HKDF-SHA256-ChaCha20-Poly1305",
 		cipher: HPKECipherSuite{KDF: hpke.HKDFSHA256, AEAD: hpke.ChaCha20Poly1305},
 	},
@@ -18790,7 +18809,8 @@
 				flags: []string{
 					"-ech-server-config", base64FlagValue(echConfig.ECHConfig.Raw),
 					"-ech-server-key", base64FlagValue(echConfig.Key),
-					"-ech-is-retry-config", "1"},
+					"-ech-is-retry-config", "1",
+				},
 				shouldFail:         true,
 				expectedLocalError: "remote error: illegal parameter",
 				expectedError:      ":INVALID_CLIENT_HELLO_INNER:",
@@ -22885,6 +22905,502 @@
 	})
 }
 
+func addPAKETests() {
+	spakeCredential := Credential{
+		Type:         CredentialTypeSPAKE2PlusV1,
+		PAKEContext:  []byte("context"),
+		PAKEClientID: []byte("client"),
+		PAKEServerID: []byte("server"),
+		PAKEPassword: []byte("password"),
+	}
+
+	spakeWrongClientID := spakeCredential
+	spakeWrongClientID.PAKEClientID = []byte("wrong")
+
+	spakeWrongServerID := spakeCredential
+	spakeWrongServerID.PAKEServerID = []byte("wrong")
+
+	spakeWrongPassword := spakeCredential
+	spakeWrongPassword.PAKEPassword = []byte("wrong")
+
+	spakeWrongRole := spakeCredential
+	spakeWrongRole.WrongPAKERole = true
+
+	spakeWrongCodepoint := spakeCredential
+	spakeWrongCodepoint.OverridePAKECodepoint = 1234
+
+	testCases = append(testCases, testCase{
+		name:     "PAKE-No-Server-Support",
+		testType: serverTest,
+		config: Config{
+			MinVersion: VersionTLS13,
+			Credential: &spakeCredential,
+		},
+		shouldFail:    true,
+		expectedError: ":MISSING_KEY_SHARE:",
+	})
+	testCases = append(testCases, testCase{
+		name:     "PAKE-Server",
+		testType: serverTest,
+		config: Config{
+			MinVersion: VersionTLS13,
+			Credential: &spakeCredential,
+			Bugs: ProtocolBugs{
+				// We do not currently support resumption with PAKE, so PAKE
+				// servers should not issue session tickets.
+				ExpectNoNewSessionTicket: true,
+			},
+		},
+		shimCredentials: []*Credential{&spakeCredential},
+	})
+	testCases = append(testCases, testCase{
+		// Send a ClientHello with the wrong PAKE client ID.
+		name:     "PAKE-Server-WrongClientID",
+		testType: serverTest,
+		config: Config{
+			MinVersion: VersionTLS13,
+			Credential: &spakeWrongClientID,
+		},
+		shimCredentials:    []*Credential{&spakeCredential},
+		shouldFail:         true,
+		expectedError:      ":PEER_PAKE_MISMATCH:",
+		expectedLocalError: "remote error: handshake failure",
+	})
+	testCases = append(testCases, testCase{
+		// Send a ClientHello with the wrong PAKE server ID.
+		name:     "PAKE-Server-WrongServerID",
+		testType: serverTest,
+		config: Config{
+			MinVersion: VersionTLS13,
+			Credential: &spakeWrongServerID,
+		},
+		shimCredentials:    []*Credential{&spakeCredential},
+		shouldFail:         true,
+		expectedError:      ":PEER_PAKE_MISMATCH:",
+		expectedLocalError: "remote error: handshake failure",
+	})
+	testCases = append(testCases, testCase{
+		// Send a ClientHello with the wrong PAKE codepoint.
+		name:     "PAKE-Server-WrongCodepoint",
+		testType: serverTest,
+		config: Config{
+			MinVersion: VersionTLS13,
+			Credential: &spakeWrongCodepoint,
+		},
+		shimCredentials:    []*Credential{&spakeCredential},
+		shouldFail:         true,
+		expectedError:      ":PEER_PAKE_MISMATCH:",
+		expectedLocalError: "remote error: handshake failure",
+	})
+	testCases = append(testCases, testCase{
+		// A server configured with a mix of PAKE and non-PAKE
+		// credentials will select the first that matches what the
+		// client offered. In doing so, it should skip unsupported
+		// PAKE algorithms.
+		name:     "PAKE-Server-MultiplePAKEs",
+		testType: serverTest,
+		config: Config{
+			MinVersion: VersionTLS13,
+			Credential: &spakeCredential,
+			Bugs: ProtocolBugs{
+				OfferExtraPAKEs: []uint16{1, 2, 3, 4, 5},
+			},
+		},
+		shimCredentials: []*Credential{&spakeWrongClientID, &spakeWrongServerID, &spakeWrongRole, &spakeCredential, &rsaCertificate},
+		flags:           []string{"-expect-selected-credential", "3"},
+	})
+	testCases = append(testCases, testCase{
+		// A server configured with a certificate credential before a
+		// PAKE credential will consider the certificate credential first.
+		name:     "PAKE-Server-CertificateBeforePAKE",
+		testType: serverTest,
+		config: Config{
+			MinVersion: VersionTLS13,
+			Bugs: ProtocolBugs{
+				// Pretend to offer a matching PAKE share, but expect the
+				// shim to select the credential first and negotiate a
+				// normal handshake.
+				OfferExtraPAKEClientID: spakeCredential.PAKEClientID,
+				OfferExtraPAKEServerID: spakeCredential.PAKEServerID,
+				OfferExtraPAKEs:        []uint16{spakeID},
+			},
+		},
+		shimCredentials: []*Credential{&rsaCertificate, &spakeCredential},
+		flags:           []string{"-expect-selected-credential", "0"},
+	})
+	testCases = append(testCases, testCase{
+		// A server configured with just a PAKE credential should reject normal
+		// clients.
+		name:     "PAKE-Server-NormalClient",
+		testType: serverTest,
+		config: Config{
+			MinVersion: VersionTLS13,
+		},
+		shimCredentials:    []*Credential{&spakeCredential},
+		shouldFail:         true,
+		expectedError:      ":PEER_PAKE_MISMATCH:",
+		expectedLocalError: "remote error: handshake failure",
+	})
+	testCases = append(testCases, testCase{
+		// ... and TLS 1.2 clients.
+		name:     "PAKE-Server-NormalTLS12Client",
+		testType: serverTest,
+		config: Config{
+			MinVersion: VersionTLS12,
+			MaxVersion: VersionTLS12,
+		},
+		shimCredentials:    []*Credential{&spakeCredential},
+		shouldFail:         true,
+		expectedError:      ":NO_SHARED_CIPHER:",
+		expectedLocalError: "remote error: handshake failure",
+	})
+	testCases = append(testCases, testCase{
+		// ... but you can configure a server with both PAKE and certificate-based
+		// SSL_CREDENTIALs and that works.
+		name:     "PAKE-ServerWithCertsToo-NormalClient",
+		testType: serverTest,
+		config: Config{
+			MinVersion: VersionTLS13,
+		},
+		shimCredentials: []*Credential{&spakeCredential, &rsaCertificate},
+		flags:           []string{"-expect-selected-credential", "1"},
+	})
+	testCases = append(testCases, testCase{
+		// ... and for older clients.
+		name:     "PAKE-ServerWithCertsToo-NormalTLS12Client",
+		testType: serverTest,
+		config: Config{
+			MinVersion: VersionTLS12,
+			MaxVersion: VersionTLS12,
+		},
+		shimCredentials: []*Credential{&spakeCredential, &rsaCertificate},
+		flags:           []string{"-expect-selected-credential", "1"},
+	})
+	testCases = append(testCases, testCase{
+		name:     "PAKE-Client",
+		testType: clientTest,
+		config: Config{
+			MinVersion: VersionTLS13,
+			Credential: &spakeCredential,
+			Bugs: ProtocolBugs{
+				CheckClientHello: func(c *clientHelloMsg) error {
+					// PAKE connections don't use the key_share / supported_groups mechanism.
+					if c.hasKeyShares {
+						return errors.New("unexpected key_share extension")
+					}
+					if len(c.supportedCurves) != 0 {
+						return errors.New("unexpected supported_groups extension")
+					}
+					// PAKE connections don't use signature algorithms.
+					if len(c.signatureAlgorithms) != 0 {
+						return errors.New("unexpected signature_algorithms extension")
+					}
+					// We don't support resumption with PAKEs.
+					if len(c.pskKEModes) != 0 {
+						return errors.New("unexpected psk_key_exchange_modes extension")
+					}
+					return nil
+				},
+			},
+		},
+		shimCredentials: []*Credential{&spakeCredential},
+	})
+	testCases = append(testCases, testCase{
+		// Although there is no reason to request new key shares, the PAKE
+		// client should handle cookie requests.
+		name:     "PAKE-Client-HRRCookie",
+		testType: clientTest,
+		config: Config{
+			MinVersion: VersionTLS13,
+			Credential: &spakeCredential,
+			Bugs: ProtocolBugs{
+				SendHelloRetryRequestCookie: []byte("cookie"),
+			},
+		},
+		shimCredentials: []*Credential{&spakeCredential},
+	})
+	testCases = append(testCases, testCase{
+		// A PAKE client will not offer key shares, so the client should
+		// reject a HelloRetryRequest requesting a different key share.
+		name:     "PAKE-Client-HRRKeyShare",
+		testType: clientTest,
+		config: Config{
+			MinVersion: VersionTLS13,
+			Credential: &spakeCredential,
+			Bugs: ProtocolBugs{
+				SendHelloRetryRequestCurve: CurveX25519,
+			},
+		},
+		shimCredentials:    []*Credential{&spakeCredential},
+		shouldFail:         true,
+		expectedError:      ":UNEXPECTED_EXTENSION:",
+		expectedLocalError: "remote error: unsupported extension",
+	})
+	testCases = append(testCases, testCase{
+		// A server cannot reply with an HRR asking for a PAKE if the client didn't
+		// offer a PAKE in the ClientHello.
+		name:     "PAKE-NormalClient-PAKEInHRR",
+		testType: clientTest,
+		config: Config{
+			MinVersion: VersionTLS13,
+			Credential: &spakeCredential,
+			Bugs: ProtocolBugs{
+				AlwaysSendHelloRetryRequest: true,
+				SendPAKEInHelloRetryRequest: true,
+			},
+		},
+		shouldFail:    true,
+		expectedError: ":UNEXPECTED_EXTENSION:",
+	})
+	testCases = append(testCases, testCase{
+		// A PAKE client should not accept an empty ServerHello.
+		name:     "PAKE-Client-EmptyServerHello",
+		testType: clientTest,
+		config: Config{
+			MinVersion: VersionTLS13,
+			Bugs: ProtocolBugs{
+				// Trigger an empty ServerHello by making a normal server skip
+				// the key_share extension.
+				MissingKeyShare: true,
+			},
+		},
+		shimCredentials: []*Credential{&spakeCredential},
+		shouldFail:      true,
+		expectedError:   ":MISSING_EXTENSION:",
+	})
+	testCases = append(testCases, testCase{
+		// A PAKE client should not accept a key_share ServerHello.
+		name:     "PAKE-Client-KeyShareServerHello",
+		testType: clientTest,
+		config: Config{
+			MinVersion: VersionTLS13,
+			Bugs: ProtocolBugs{
+				// Trigger a key_share ServerHello by making a normal server
+				// skip the HelloRetryRequest it would otherwise send in
+				// response to the shim's key_share-less ClientHello.
+				SkipHelloRetryRequest: true,
+				// Ignore the client's lack of supported_groups.
+				IgnorePeerCurvePreferences: true,
+			},
+		},
+		shimCredentials: []*Credential{&spakeCredential},
+		shouldFail:      true,
+		expectedError:   ":UNEXPECTED_EXTENSION:",
+	})
+	testCases = append(testCases, testCase{
+		// A PAKE client should not accept a TLS 1.2 ServerHello.
+		name:     "PAKE-Client-TLS12ServerHello",
+		testType: clientTest,
+		config: Config{
+			MinVersion: VersionTLS12,
+			MaxVersion: VersionTLS12,
+		},
+		shimCredentials: []*Credential{&spakeCredential},
+		shouldFail:      true,
+		expectedError:   ":UNSUPPORTED_PROTOCOL:",
+	})
+	testCases = append(testCases, testCase{
+		// A server cannot send the PAKE extension to a non-PAKE client.
+		name:     "PAKE-NormalClient-UnsolicitedPAKEInServerHello",
+		testType: clientTest,
+		config: Config{
+			Bugs: ProtocolBugs{
+				UnsolicitedPAKE: spakeID,
+			},
+		},
+		shouldFail:    true,
+		expectedError: ":UNEXPECTED_EXTENSION:",
+	})
+	testCases = append(testCases, testCase{
+		// A server cannot reply with a PAKE that the client did not offer.
+		name:     "PAKE-Client-WrongPAKEInServerHello",
+		testType: clientTest,
+		config: Config{
+			Bugs: ProtocolBugs{
+				UnsolicitedPAKE: 1234,
+			},
+		},
+		shimCredentials: []*Credential{&spakeCredential},
+		shouldFail:      true,
+		expectedError:   ":DECODE_ERROR:",
+	})
+	testCases = append(testCases, testCase{
+		name:     "PAKE-Extension-Duplicate",
+		testType: serverTest,
+		config: Config{
+			MinVersion: VersionTLS13,
+			Bugs: ProtocolBugs{
+				OfferExtraPAKEClientID: []byte("client"),
+				OfferExtraPAKEServerID: []byte("server"),
+				OfferExtraPAKEs:        []uint16{1234, 1234},
+			},
+		},
+		shouldFail:    true,
+		expectedError: ":ERROR_PARSING_EXTENSION:",
+	})
+	testCases = append(testCases, testCase{
+		// If the client sees a server with a wrong password, it should
+		// reject the confirmV value in the ServerHello.
+		name:     "PAKE-Client-WrongPassword",
+		testType: clientTest,
+		config: Config{
+			MinVersion: VersionTLS13,
+			Credential: &spakeWrongPassword,
+		},
+		shimCredentials: []*Credential{&spakeCredential},
+		shouldFail:      true,
+		expectedError:   ":DECODE_ERROR:",
+	})
+	testCases = append(testCases, testCase{
+		name:     "PAKE-Client-Truncate",
+		testType: clientTest,
+		config: Config{
+			MinVersion: VersionTLS13,
+			Credential: &spakeCredential,
+			Bugs: ProtocolBugs{
+				TruncatePAKEMessage: true,
+			},
+		},
+		shimCredentials: []*Credential{&spakeCredential},
+		shouldFail:      true,
+		expectedError:   ":DECODE_ERROR:",
+	})
+	testCases = append(testCases, testCase{
+		name:     "PAKE-Server-Truncate",
+		testType: serverTest,
+		config: Config{
+			MinVersion: VersionTLS13,
+			Credential: &spakeCredential,
+			Bugs: ProtocolBugs{
+				TruncatePAKEMessage: true,
+			},
+		},
+		shimCredentials:    []*Credential{&spakeCredential},
+		shouldFail:         true,
+		expectedError:      ":DECODE_ERROR:",
+		expectedLocalError: "remote error: illegal parameter",
+	})
+	testCases = append(testCases, testCase{
+		// Servers may not send CertificateRequest in a PAKE handshake.
+		name:     "PAKE-Client-UnexpectedCertificateRequest",
+		testType: clientTest,
+		config: Config{
+			MinVersion: VersionTLS13,
+			Credential: &spakeCredential,
+			ClientAuth: RequireAnyClientCert,
+			Bugs: ProtocolBugs{
+				AlwaysSendCertificateRequest: true,
+			},
+		},
+		shimCredentials:    []*Credential{&spakeCredential},
+		shouldFail:         true,
+		expectedError:      ":UNEXPECTED_MESSAGE:",
+		expectedLocalError: "remote error: unexpected message",
+	})
+	testCases = append(testCases, testCase{
+		// Servers may not send Certificate in a PAKE handshake.
+		name:     "PAKE-Client-UnexpectedCertificate",
+		testType: clientTest,
+		config: Config{
+			MinVersion: VersionTLS13,
+			Credential: &spakeCredential,
+			Bugs: ProtocolBugs{
+				AlwaysSendCertificate:    true,
+				UseCertificateCredential: &rsaCertificate,
+				// Ignore the client's lack of signature_algorithms.
+				IgnorePeerSignatureAlgorithmPreferences: true,
+			},
+		},
+		shimCredentials:    []*Credential{&spakeCredential},
+		shouldFail:         true,
+		expectedError:      ":UNEXPECTED_MESSAGE:",
+		expectedLocalError: "remote error: unexpected message",
+	})
+	testCases = append(testCases, testCase{
+		// If a server is configured to request client certificates, it should
+		// still not do so when negotiating a PAKE.
+		name:     "PAKE-Server-DoNotRequestClientCertificate",
+		testType: serverTest,
+		config: Config{
+			MinVersion: VersionTLS13,
+			Credential: &spakeCredential,
+		},
+		shimCredentials: []*Credential{&spakeCredential, &rsaCertificate},
+		flags:           []string{"-require-any-client-certificate"},
+	})
+	testCases = append(testCases, testCase{
+		// Clients should ignore server PAKE credentials.
+		name:     "PAKE-Client-WrongRole",
+		testType: clientTest,
+		config: Config{
+			MinVersion: VersionTLS13,
+			Credential: &spakeCredential,
+		},
+		shimCredentials: []*Credential{&spakeWrongRole},
+		shouldFail:      true,
+		// The shim will send a non-PAKE ClientHello.
+		expectedLocalError: "tls: client not configured with PAKE",
+	})
+	testCases = append(testCases, testCase{
+		// Servers should ignore client PAKE credentials.
+		name:     "PAKE-Server-WrongRole",
+		testType: serverTest,
+		config: Config{
+			MinVersion: VersionTLS13,
+			Credential: &spakeCredential,
+		},
+		shimCredentials: []*Credential{&spakeWrongRole},
+		shouldFail:      true,
+		// The shim will fail the handshake because it has no usable credentials
+		// available.
+		expectedError:      ":UNKNOWN_CERTIFICATE_TYPE:",
+		expectedLocalError: "remote error: handshake failure",
+	})
+	testCases = append(testCases, testCase{
+		// On the client, we only support a single PAKE credential.
+		name:            "PAKE-Client-MultiplePAKEs",
+		testType:        clientTest,
+		shimCredentials: []*Credential{&spakeCredential, &spakeWrongPassword},
+		shouldFail:      true,
+		expectedError:   ":UNSUPPORTED_CREDENTIAL_LIST:",
+	})
+	testCases = append(testCases, testCase{
+		// On the client, we only support a single PAKE credential.
+		name:            "PAKE-Client-PAKEAndCertificate",
+		testType:        clientTest,
+		shimCredentials: []*Credential{&spakeCredential, &rsaCertificate},
+		shouldFail:      true,
+		expectedError:   ":UNSUPPORTED_CREDENTIAL_LIST:",
+	})
+	testCases = append(testCases, testCase{
+		// We currently do not support resumption with PAKE. Even if configured
+		// with a session, the client should not offer the session with PAKEs.
+		name:     "PAKE-Client-NoResume",
+		testType: clientTest,
+		// Make two connections. For the first connection, just establish a
+		// session without PAKE, to pick up a session.
+		config: Config{
+			Credential: &rsaCertificate,
+		},
+		// For the second connection, use SPAKE.
+		resumeSession: true,
+		resumeConfig: &Config{
+			Credential: &spakeCredential,
+			Bugs: ProtocolBugs{
+				// Check that the ClientHello does not offer a session, even
+				// though one was configured.
+				ExpectNoTLS13PSK: true,
+				// Respond with an unsolicted PSK extension in ServerHello, to
+				// check that the client rejects it.
+				AlwaysSelectPSKIdentity: true,
+			},
+		},
+		resumeShimCredentials: []*Credential{&spakeCredential},
+		shouldFail:            true,
+		expectedError:         ":UNEXPECTED_EXTENSION:",
+	})
+}
+
 func worker(dispatcher *shimDispatcher, statusChan chan statusMsg, c chan *testCase, shimPath string, wg *sync.WaitGroup) {
 	defer wg.Done()
 
@@ -23137,6 +23653,7 @@
 	addCompliancePolicyTests()
 	addCertificateSelectionTests()
 	addKeyUpdateTests()
+	addPAKETests()
 
 	toAppend, err := convertToSplitHandshakeTests(testCases)
 	if err != nil {
diff --git a/ssl/test/runner/spake2plus/spake2plus.go b/ssl/test/runner/spake2plus/spake2plus.go
new file mode 100644
index 0000000..3c702ab
--- /dev/null
+++ b/ssl/test/runner/spake2plus/spake2plus.go
@@ -0,0 +1,439 @@
+// Copyright 2025 The BoringSSL Authors
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
+// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+// Package spake2plus implements RFC 9383 for testing.
+package spake2plus
+
+import (
+	"bytes"
+	"crypto/elliptic"
+	"crypto/hmac"
+	"crypto/rand"
+	"crypto/sha256"
+	"encoding/binary"
+	"errors"
+	"io"
+	"math/big"
+
+	"golang.org/x/crypto/hkdf"
+	"golang.org/x/crypto/scrypt"
+)
+
+const (
+	verifierSize           = 32 // size of w0, w1 in bytes, for P-256
+	registrationRecordSize = 65 // uncompressed P-256 point size
+	shareSize              = 65
+	confirmSize            = 32
+	keySize                = 32
+	pbkdfOutputSize        = 80
+)
+
+type Role int
+
+const (
+	RoleProver Role = iota
+	RoleVerifier
+)
+
+type state int
+
+const (
+	stateInit state = iota
+	stateShareGenerated
+	stateKeyGenerated
+)
+
+type Context struct {
+	IDProver   []byte
+	IDVerifier []byte
+	Role       Role
+
+	curve   elliptic.Curve
+	context []byte
+	w0      *big.Int
+	w1      *big.Int
+	Lx, Ly  *big.Int // L point
+	Mx, My  *big.Int // M point
+	Nx, Ny  *big.Int // N point
+	Xx, Xy  *big.Int // X point
+	Yx, Yy  *big.Int // Y point
+	Zx, Zy  *big.Int // Z point
+	Vx, Vy  *big.Int // V point
+	x       *big.Int // ephemeral scalar for prover
+	y       *big.Int // ephemeral scalar for verifier
+	share   []byte
+	confirm []byte
+	state   state
+}
+
+// Hardcoded M and N from the RFC (uncompressed)
+var kM = []byte{
+	0x04, 0x88, 0x6e, 0x2f, 0x97, 0xac, 0xe4, 0x6e, 0x55, 0xba, 0x9d,
+	0xd7, 0x24, 0x25, 0x79, 0xf2, 0x99, 0x3b, 0x64, 0xe1, 0x6e, 0xf3,
+	0xdc, 0xab, 0x95, 0xaf, 0xd4, 0x97, 0x33, 0x3d, 0x8f, 0xa1, 0x2f,
+	0x5f, 0xf3, 0x55, 0x16, 0x3e, 0x43, 0xce, 0x22, 0x4e, 0x0b, 0x0e,
+	0x65, 0xff, 0x02, 0xac, 0x8e, 0x5c, 0x7b, 0xe0, 0x94, 0x19, 0xc7,
+	0x85, 0xe0, 0xca, 0x54, 0x7d, 0x55, 0xa1, 0x2e, 0x2d, 0x20,
+}
+
+var kN = []byte{
+	0x04, 0xd8, 0xbb, 0xd6, 0xc6, 0x39, 0xc6, 0x29, 0x37, 0xb0, 0x4d,
+	0x99, 0x7f, 0x38, 0xc3, 0x77, 0x07, 0x19, 0xc6, 0x29, 0xd7, 0x01,
+	0x4d, 0x49, 0xa2, 0x4b, 0x4f, 0x98, 0xba, 0xa1, 0x29, 0x2b, 0x49,
+	0x07, 0xd6, 0x0a, 0xa6, 0xbf, 0xad, 0xe4, 0x50, 0x08, 0xa6, 0x36,
+	0x33, 0x7f, 0x51, 0x68, 0xc6, 0x4d, 0x9b, 0xd3, 0x60, 0x34, 0x80,
+	0x8c, 0xd5, 0x64, 0x49, 0x0b, 0x1e, 0x65, 0x6e, 0xdb, 0xe7,
+}
+
+func Register(
+	pw []byte,
+	idProver []byte,
+	idVerifier []byte,
+) (pwVerifierW0 []byte, pwVerifierW1 []byte, registrationRecord []byte, err error) {
+	mhfBuf := new(bytes.Buffer)
+	if err := binary.Write(mhfBuf, binary.LittleEndian, uint64(len(pw))); err != nil {
+		return nil, nil, nil, err
+	}
+	mhfBuf.Write(pw)
+	if err := binary.Write(mhfBuf, binary.LittleEndian, uint64(len(idProver))); err != nil {
+		return nil, nil, nil, err
+	}
+	mhfBuf.Write(idProver)
+	if err := binary.Write(mhfBuf, binary.LittleEndian, uint64(len(idVerifier))); err != nil {
+		return nil, nil, nil, err
+	}
+	mhfBuf.Write(idVerifier)
+
+	key, err := scrypt.Key(mhfBuf.Bytes(), nil, 32768, 8, 1, pbkdfOutputSize)
+	if err != nil {
+		return nil, nil, nil, err
+	}
+
+	curve := elliptic.P256()
+	N := curve.Params().N
+
+	w0 := new(big.Int).SetBytes(key[:pbkdfOutputSize/2])
+	w0.Mod(w0, N)
+
+	w1 := new(big.Int).SetBytes(key[pbkdfOutputSize/2:])
+	w1.Mod(w1, N)
+
+	pwVerifierW0 = make([]byte, verifierSize)
+	pwVerifierW1 = make([]byte, verifierSize)
+	copy(pwVerifierW0, w0.Bytes())
+	copy(pwVerifierW1, w1.Bytes())
+
+	Lx, Ly := curve.ScalarBaseMult(w1.Bytes())
+	L := elliptic.Marshal(curve, Lx, Ly)
+	registrationRecord = make([]byte, registrationRecordSize)
+	copy(registrationRecord, L)
+
+	return pwVerifierW0, pwVerifierW1, registrationRecord, nil
+}
+
+func newContext(
+	role Role,
+	context []byte,
+	idProver []byte,
+	idVerifier []byte,
+	pwVerifierW0 []byte,
+	pwVerifierW1 []byte,
+	registrationRecord []byte,
+	x *big.Int,
+	y *big.Int,
+) (*Context, error) {
+	curve := elliptic.P256()
+
+	Mx, My := elliptic.Unmarshal(curve, kM)
+	if Mx == nil {
+		return nil, errors.New("invalid M point")
+	}
+	Nx, Ny := elliptic.Unmarshal(curve, kN)
+	if Nx == nil {
+		return nil, errors.New("invalid N point")
+	}
+
+	ctx := &Context{
+		Role:       role,
+		curve:      curve,
+		context:    append([]byte(nil), context...),
+		IDProver:   append([]byte(nil), idProver...),
+		IDVerifier: append([]byte(nil), idVerifier...),
+		Mx:         Mx,
+		My:         My,
+		Nx:         Nx,
+		Ny:         Ny,
+		state:      stateInit,
+	}
+
+	N := curve.Params().N
+
+	if role == RoleProver {
+		if pwVerifierW0 == nil || pwVerifierW1 == nil || y != nil {
+			return nil, errors.New("invalid parameters for prover")
+		}
+
+		ctx.w0 = new(big.Int).SetBytes(pwVerifierW0)
+		ctx.w1 = new(big.Int).SetBytes(pwVerifierW1)
+		ctx.w0.Mod(ctx.w0, N)
+		ctx.w1.Mod(ctx.w1, N)
+
+		if x == nil {
+			xRand, err := randFieldElement(curve)
+			if err != nil {
+				return nil, err
+			}
+			ctx.x = xRand
+		} else {
+			ctx.x = new(big.Int).Set(x)
+		}
+	} else {
+		// Verifier
+		if pwVerifierW0 == nil || registrationRecord == nil || x != nil {
+			return nil, errors.New("invalid parameters for verifier")
+		}
+
+		ctx.w0 = new(big.Int).SetBytes(pwVerifierW0)
+		ctx.w0.Mod(ctx.w0, N)
+
+		// Load L
+		Lx, Ly := elliptic.Unmarshal(curve, registrationRecord)
+		if Lx == nil {
+			return nil, errors.New("invalid L point")
+		}
+		ctx.Lx, ctx.Ly = Lx, Ly
+
+		if y == nil {
+			yRand, err := randFieldElement(curve)
+			if err != nil {
+				return nil, err
+			}
+			ctx.y = yRand
+		} else {
+			ctx.y = new(big.Int).Set(y)
+		}
+	}
+
+	return ctx, nil
+}
+
+func randFieldElement(curve elliptic.Curve) (*big.Int, error) {
+	params := curve.Params()
+	b := make([]byte, (params.BitSize+7)/8)
+	var k *big.Int
+	for {
+		if _, err := rand.Read(b); err != nil {
+			return nil, err
+		}
+		k = new(big.Int).SetBytes(b)
+		if k.Sign() != 0 && k.Cmp(params.N) < 0 {
+			break
+		}
+	}
+	return k, nil
+}
+
+func NewProver(
+	context []byte, idProver []byte, idVerifier []byte,
+	pwVerifierW0 []byte, pwVerifierW1 []byte,
+) (*Context, error) {
+	return newContext(RoleProver, context, idProver, idVerifier,
+		pwVerifierW0, pwVerifierW1, nil, nil, nil)
+}
+
+func NewVerifier(
+	context []byte, idProver []byte, idVerifier []byte,
+	pwVerifierW0 []byte, registrationRecord []byte,
+) (*Context, error) {
+	return newContext(RoleVerifier, context, idProver, idVerifier,
+		pwVerifierW0, nil, registrationRecord, nil, nil)
+}
+
+func (ctx *Context) GenerateProverShare() (share []byte, err error) {
+	if ctx.Role != RoleProver {
+		return nil, errors.New("invalid state for prover share generation")
+	}
+	if ctx.state != stateInit {
+		return ctx.share, nil
+	}
+	curve := ctx.curve
+
+	// l = x * G
+	lx, ly := curve.ScalarBaseMult(ctx.x.Bytes())
+	// r = w0 * M
+	rx, ry := curve.ScalarMult(ctx.Mx, ctx.My, ctx.w0.Bytes())
+	// X = l + r
+	Xx, Xy := curve.Add(lx, ly, rx, ry)
+	ctx.Xx, ctx.Xy = Xx, Xy
+
+	share = elliptic.Marshal(curve, Xx, Xy)
+	ctx.share = append([]byte(nil), share...)
+	ctx.state = stateShareGenerated
+	return share, nil
+}
+
+func updateWithLengthPrefix(h io.Writer, data []byte) {
+	var lenLe [8]byte
+	binary.LittleEndian.PutUint64(lenLe[:], uint64(len(data)))
+	h.Write(lenLe[:])
+	h.Write(data)
+}
+
+func computeTranscriptAndConfirmation(
+	ctx *Context,
+	shareP, shareV []byte,
+) (proverConfirm, verifierConfirm, sharedSecret []byte, err error) {
+	curve := ctx.curve
+	Z := elliptic.Marshal(curve, ctx.Zx, ctx.Zy)
+	V := elliptic.Marshal(curve, ctx.Vx, ctx.Vy)
+
+	h := sha256.New()
+	updateWithLengthPrefix(h, ctx.context)
+	updateWithLengthPrefix(h, ctx.IDProver)
+	updateWithLengthPrefix(h, ctx.IDVerifier)
+	updateWithLengthPrefix(h, kM)
+	updateWithLengthPrefix(h, kN)
+	updateWithLengthPrefix(h, shareP)
+	updateWithLengthPrefix(h, shareV)
+	updateWithLengthPrefix(h, Z)
+	updateWithLengthPrefix(h, V)
+	updateWithLengthPrefix(h, ctx.w0.Bytes())
+	K_main := h.Sum(nil)
+
+	confirmationStr := []byte("ConfirmationKeys")
+	keys := doHKDF(K_main, confirmationStr, keySize*2)
+	secretInfoStr := []byte("SharedKey")
+	sharedSecret = doHKDF(K_main, secretInfoStr, keySize)
+
+	// Prover confirmation = HMAC(keys[:32], shareV)
+	macP := hmac.New(sha256.New, keys[:keySize])
+	macP.Write(shareV)
+	proverConfirm = macP.Sum(nil)
+
+	// Verifier confirmation = HMAC(keys[32:], shareP)
+	macV := hmac.New(sha256.New, keys[keySize:])
+	macV.Write(shareP)
+	verifierConfirm = macV.Sum(nil)
+
+	return
+}
+
+func doHKDF(ikm, info []byte, size int) []byte {
+	h := hkdf.New(sha256.New, ikm, nil, info)
+	out := make([]byte, size)
+	h.Read(out)
+	return out
+}
+
+func (ctx *Context) ProcessProverShare(
+	proverShare []byte,
+) (verifierShare []byte, verifierConfirm []byte, sharedSecret []byte, err error) {
+	if ctx.Role != RoleVerifier || ctx.state != stateInit || len(proverShare) != shareSize {
+		return nil, nil, nil, errors.New("invalid state or share")
+	}
+	curve := ctx.curve
+
+	// Y = y*G + w0*N
+	lx, ly := curve.ScalarBaseMult(ctx.y.Bytes())
+	rx, ry := curve.ScalarMult(ctx.Nx, ctx.Ny, ctx.w0.Bytes())
+	Yx, Yy := curve.Add(lx, ly, rx, ry)
+	ctx.Yx, ctx.Yy = Yx, Yy
+
+	verifierShare = elliptic.Marshal(curve, Yx, Yy)
+	if px, py := elliptic.Unmarshal(curve, proverShare); px == nil {
+		return nil, nil, nil, errors.New("invalid prover share")
+	} else {
+		ctx.Xx, ctx.Xy = px, py
+	}
+
+	// T = X - w0*M
+	mx, my := curve.ScalarMult(ctx.Mx, ctx.My, ctx.w0.Bytes())
+	mx, my = mx, new(big.Int).Neg(my)
+	my.Mod(my, curve.Params().P)
+
+	Tx, Ty := curve.Add(ctx.Xx, ctx.Xy, mx, my)
+	// Z = (y)*T
+	Zx, Zy := curve.ScalarMult(Tx, Ty, ctx.y.Bytes())
+	ctx.Zx, ctx.Zy = Zx, Zy
+	// V = (y)*L
+	Vx, Vy := curve.ScalarMult(ctx.Lx, ctx.Ly, ctx.y.Bytes())
+	ctx.Vx, ctx.Vy = Vx, Vy
+
+	proverConfirm, verifierConfirm, sharedSecret, err := computeTranscriptAndConfirmation(ctx, proverShare, verifierShare)
+	if err != nil {
+		return nil, nil, nil, err
+	}
+
+	ctx.confirm = proverConfirm
+	ctx.state = stateKeyGenerated
+
+	return verifierShare, verifierConfirm, sharedSecret, nil
+}
+
+// computeProverConfirmation (Prover side)
+func (ctx *Context) ComputeProverConfirmation(
+	verifierShare []byte,
+	claimedVerifierConfirm []byte,
+) (proverConfirm []byte, sharedSecret []byte, err error) {
+	if ctx.Role != RoleProver || ctx.state != stateShareGenerated || len(verifierShare) != shareSize || len(claimedVerifierConfirm) != confirmSize {
+		return nil, nil, errors.New("invalid state or input")
+	}
+	curve := ctx.curve
+	vx, vy := elliptic.Unmarshal(curve, verifierShare)
+	if vx == nil {
+		return nil, nil, errors.New("invalid verifier share")
+	}
+	ctx.Yx, ctx.Yy = vx, vy
+
+	// T = Y - w0*N
+	nx, ny := curve.ScalarMult(ctx.Nx, ctx.Ny, ctx.w0.Bytes())
+	ny.Neg(ny)
+	ny.Mod(ny, curve.Params().P)
+
+	Tx, Ty := curve.Add(ctx.Yx, ctx.Yy, nx, ny)
+
+	// Z = x*T
+	Zx, Zy := curve.ScalarMult(Tx, Ty, ctx.x.Bytes())
+	ctx.Zx, ctx.Zy = Zx, Zy
+
+	// V = w1*T
+	Vx, Vy := curve.ScalarMult(Tx, Ty, ctx.w1.Bytes())
+	ctx.Vx, ctx.Vy = Vx, Vy
+
+	// Compute transcript
+	proverConfirm, verifierConfirm, sharedSecret, err := computeTranscriptAndConfirmation(ctx, ctx.share, verifierShare)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	// Check verifier confirm
+	if !hmac.Equal(verifierConfirm, claimedVerifierConfirm) {
+		return nil, nil, errors.New("verifier confirmation mismatch")
+	}
+
+	ctx.state = stateKeyGenerated
+
+	return proverConfirm, sharedSecret, nil
+}
+
+// VerifyProverConfirmation (Verifier side)
+func (ctx *Context) VerifyProverConfirmation(proverConfirm []byte) error {
+	if ctx.Role != RoleVerifier || ctx.state != stateKeyGenerated || len(proverConfirm) != confirmSize {
+		return errors.New("invalid state or input")
+	}
+	if !hmac.Equal(ctx.confirm, proverConfirm) {
+		return errors.New("prover confirmation mismatch")
+	}
+	return nil
+}
diff --git a/ssl/test/runner/spake2plus/spake2plus_test.go b/ssl/test/runner/spake2plus/spake2plus_test.go
new file mode 100644
index 0000000..b0a072c
--- /dev/null
+++ b/ssl/test/runner/spake2plus/spake2plus_test.go
@@ -0,0 +1,266 @@
+// Copyright 2025 The BoringSSL Authors
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
+// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+package spake2plus
+
+import (
+	"bytes"
+	"encoding/hex"
+	"math/big"
+	"testing"
+)
+
+func hexToBytes(h string) []byte {
+	b, err := hex.DecodeString(h)
+	if err != nil {
+		panic(err)
+	}
+	return b
+}
+
+func TestSPAKE2PlusBasicRoundTrip(t *testing.T) {
+	pw := []byte("password")
+	context := []byte("SPAKE2+-P256-SHA256-HKDF-SHA256-HMAC-SHA256 Test Vectors")
+	idProver := []byte("client")
+	idVerifier := []byte("server")
+
+	pwVerifierW0, pwVerifierW1, registrationRecord, err := Register(
+		pw, idProver, idVerifier,
+	)
+	if err != nil {
+		t.Fatalf("Registration failed: %v", err)
+	}
+
+	prover, err := NewProver(
+		context, idProver, idVerifier,
+		pwVerifierW0, pwVerifierW1,
+	)
+	if err != nil {
+		t.Fatalf("Prover context creation failed: %v", err)
+	}
+	verifier, err := NewVerifier(
+		context, idProver, idVerifier,
+		pwVerifierW0, registrationRecord,
+	)
+	if err != nil {
+		t.Fatalf("Verifier context creation failed: %v", err)
+	}
+
+	proverShare, err := prover.GenerateProverShare()
+	if err != nil {
+		t.Fatalf("Prover share generation failed: %v", err)
+	}
+
+	verifierShare, verifierConfirm, verifierSecret, err := verifier.ProcessProverShare(proverShare)
+	if err != nil {
+		t.Fatalf("Verifier failed to process prover's share: %v", err)
+	}
+
+	proverConfirm, proverSecret, err := prover.ComputeProverConfirmation(verifierShare, verifierConfirm)
+	if err != nil {
+		t.Fatalf("Prover failed to compute confirmation: %v", err)
+	}
+
+	if err := verifier.VerifyProverConfirmation(proverConfirm); err != nil {
+		t.Fatalf("Verifier failed to verify prover confirmation: %v", err)
+	}
+
+	if !bytes.Equal(proverSecret, verifierSecret) {
+		t.Fatal("Shared secrets do not match")
+	}
+}
+
+func TestSPAKE2PlusTestVectors(t *testing.T) {
+	// Test Vectors from RFC 9383 Appendix C
+	context := []byte("SPAKE2+-P256-SHA256-HKDF-SHA256-HMAC-SHA256 Test Vectors")
+	idProver := []byte("client")
+	idVerifier := []byte("server")
+
+	w0_str := "bb8e1bbcf3c48f62c08db243652ae55d3e5586053fca77102994f23ad95491b3"
+	w1_str := "7e945f34d78785b8a3ef44d0df5a1a97d6b3b460409a345ca7830387a74b1dba"
+	L_str := "04eb7c9db3d9a9eb1f8adab81b5794c1f13ae3e225efbe91ea487425854c7fc00f00bfedcbd09b2400142d40a14f2064ef31dfaa903b91d1faea7093d835966efd"
+	x_str := "d1232c8e8693d02368976c174e2088851b8365d0d79a9eee709c6a05a2fad539"
+	y_str := "717a72348a182085109c8d3917d6c43d59b224dc6a7fc4f0483232fa6516d8b3"
+	share_p_str := "04ef3bd051bf78a2234ec0df197f7828060fe9856503579bb1733009042c15c0c1de127727f418b5966afadfdd95a6e4591d171056b333dab97a79c7193e341727"
+	share_v_str := "04c0f65da0d11927bdf5d560c69e1d7d939a05b0e88291887d679fcadea75810fb5cc1ca7494db39e82ff2f50665255d76173e09986ab46742c798a9a68437b048"
+	confirm_p_str := "926cc713504b9b4d76c9162ded04b5493e89109f6d89462cd33adc46fda27527"
+	confirm_v_str := "9747bcc4f8fe9f63defee53ac9b07876d907d55047e6ff2def2e7529089d3e68"
+	secret_str := "0c5f8ccd1413423a54f6c1fb26ff01534a87f893779c6e68666d772bfd91f3e7"
+
+	w0 := hexToBytes(w0_str)
+	w1 := hexToBytes(w1_str)
+	L := hexToBytes(L_str)
+	x := hexToBytes(x_str)
+	y := hexToBytes(y_str)
+
+	prover, err := newContext(
+		RoleProver, context, idProver, idVerifier,
+		w0, w1, nil, bytesToBigInt(x), nil,
+	)
+	if err != nil {
+		t.Fatalf("failed to create prover: %v", err)
+	}
+	verifier, err := newContext(
+		RoleVerifier, context, idProver, idVerifier,
+		w0, nil, L, nil, bytesToBigInt(y),
+	)
+	if err != nil {
+		t.Fatalf("failed to create verifier: %v", err)
+	}
+
+	proverShare, err := prover.GenerateProverShare()
+	if err != nil {
+		t.Fatalf("failed to generate prover share: %v", err)
+	}
+	expectedShareP := hexToBytes(share_p_str)
+	if !bytes.Equal(proverShare, expectedShareP) {
+		t.Fatalf("prover share mismatch:\n got  %x\nwant %x", proverShare, expectedShareP)
+	}
+
+	vShare, vConfirm, vSecret, err := verifier.ProcessProverShare(proverShare)
+	if err != nil {
+		t.Fatalf("verifier failed to process prover share: %v", err)
+	}
+	expectedShareV := hexToBytes(share_v_str)
+	if !bytes.Equal(vShare, expectedShareV) {
+		t.Fatalf("verifier share mismatch:\n got  %x\nwant %x", vShare, expectedShareV)
+	}
+	expectedConfirmV := hexToBytes(confirm_v_str)
+	if !bytes.Equal(vConfirm, expectedConfirmV) {
+		t.Fatalf("verifier confirm mismatch:\n got  %x\nwant %x", vConfirm, expectedConfirmV)
+	}
+
+	pConfirm, pSecret, err := prover.ComputeProverConfirmation(vShare, vConfirm)
+	if err != nil {
+		t.Fatalf("prover failed to compute confirmation: %v", err)
+	}
+	expectedConfirmP := hexToBytes(confirm_p_str)
+	if !bytes.Equal(pConfirm, expectedConfirmP) {
+		t.Fatalf("prover confirm mismatch:\n got  %x\nwant %x", pConfirm, expectedConfirmP)
+	}
+
+	if err := verifier.VerifyProverConfirmation(pConfirm); err != nil {
+		t.Fatalf("verifier failed to verify prover confirmation: %v", err)
+	}
+
+	if !bytes.Equal(pSecret, vSecret) {
+		t.Fatal("shared secrets do not match")
+	}
+	expectedSecret := hexToBytes(secret_str)
+	if !bytes.Equal(expectedSecret, vSecret) {
+		t.Fatalf("shared secret mismatch:\n got  %x\nwant %x", vSecret, expectedSecret)
+	}
+}
+
+func TestSPAKE2PlusMultipleRuns(t *testing.T) {
+	pw := []byte("password")
+	context := []byte("Repeated test")
+	idProver := []byte("client")
+	idVerifier := []byte("server")
+
+	for i := 0; i < 5; i++ {
+		pwVerifierW0, pwVerifierW1, registrationRecord, err := Register(
+			pw, idProver, idVerifier)
+		if err != nil {
+			t.Fatalf("registration failed: %v", err)
+		}
+		prover, err := NewProver(context, idProver, idVerifier, pwVerifierW0, pwVerifierW1)
+		if err != nil {
+			t.Fatalf("prover context creation failed: %v", err)
+		}
+		verifier, err := NewVerifier(context, idProver, idVerifier, pwVerifierW0, registrationRecord)
+		if err != nil {
+			t.Fatalf("verifier context creation failed: %v", err)
+		}
+
+		proverShare, err := prover.GenerateProverShare()
+		if err != nil {
+			t.Fatalf("prover share gen failed: %v", err)
+		}
+
+		vShare, vConfirm, vSecret, err := verifier.ProcessProverShare(proverShare)
+		if err != nil {
+			t.Fatalf("verifier process share failed: %v", err)
+		}
+
+		pConfirm, pSecret, err := prover.ComputeProverConfirmation(vShare, vConfirm)
+		if err != nil {
+			t.Fatalf("prover compute confirm failed: %v", err)
+		}
+
+		if err := verifier.VerifyProverConfirmation(pConfirm); err != nil {
+			t.Fatalf("verifier confirm failed: %v", err)
+		}
+
+		if !bytes.Equal(pSecret, vSecret) {
+			t.Fatalf("shared secrets differ")
+		}
+	}
+}
+
+func TestSPAKE2PlusWrongPassword(t *testing.T) {
+	correctPw := []byte("password")
+	wrongPw := []byte("wrongpassword")
+	context := []byte("Wrong password test")
+	idProver := []byte("client")
+	idVerifier := []byte("server")
+
+	// Register with the correct password
+	correctW0, _, registrationRecord, err := Register(
+		correctPw, idProver, idVerifier)
+	if err != nil {
+		t.Fatalf("registration failed: %v", err)
+	}
+
+	// Register with the wrong password
+	wrongW0, wrongW1, _, err := Register(
+		wrongPw, idProver, idVerifier)
+	if err != nil {
+		t.Fatalf("registration failed: %v", err)
+	}
+
+	// Create prover with wrong password verifiers
+	prover, err := NewProver(context, idProver, idVerifier, wrongW0, wrongW1)
+	if err != nil {
+		t.Fatalf("prover context creation failed: %v", err)
+	}
+
+	// Create verifier with correct password verifiers
+	verifier, err := NewVerifier(context, idProver, idVerifier, correctW0, registrationRecord)
+	if err != nil {
+		t.Fatalf("verifier context creation failed: %v", err)
+	}
+
+	proverShare, err := prover.GenerateProverShare()
+	if err != nil {
+		t.Fatalf("prover share gen failed: %v", err)
+	}
+
+	vShare, vConfirm, _, err := verifier.ProcessProverShare(proverShare)
+	if err != nil {
+		t.Fatalf("verifier process share failed: %v", err)
+	}
+
+	_, _, err = prover.ComputeProverConfirmation(vShare, vConfirm)
+	if err == nil {
+		t.Fatalf("expected error computing confirmation, got nil")
+	}
+}
+
+func bytesToBigInt(b []byte) *big.Int {
+	if len(b) == 0 {
+		return nil
+	}
+	return new(big.Int).SetBytes(b)
+}
diff --git a/ssl/test/test_config.cc b/ssl/test/test_config.cc
index ea0c4f7..9f2fe22 100644
--- a/ssl/test/test_config.cc
+++ b/ssl/test/test_config.cc
@@ -509,6 +509,8 @@
         NewCredentialFlag("-new-x509-credential", CredentialConfigType::kX509),
         NewCredentialFlag("-new-delegated-credential",
                           CredentialConfigType::kDelegated),
+        NewCredentialFlag("-new-spake2plusv1-credential",
+                          CredentialConfigType::kSPAKE2PlusV1),
         CredentialFlagWithDefault(
             StringFlag("-cert-file", &TestConfig::cert_file),
             StringFlag("-cert-file", &CredentialConfig::cert_file)),
@@ -528,6 +530,16 @@
                        &TestConfig::signed_cert_timestamps),
             Base64Flag("-signed-cert-timestamps",
                        &CredentialConfig::signed_cert_timestamps)),
+        CredentialFlag(
+            Base64Flag("-pake-context", &CredentialConfig::pake_context)),
+        CredentialFlag(
+            Base64Flag("-pake-client-id", &CredentialConfig::pake_client_id)),
+        CredentialFlag(
+            Base64Flag("-pake-server-id", &CredentialConfig::pake_server_id)),
+        CredentialFlag(
+            Base64Flag("-pake-password", &CredentialConfig::pake_password)),
+        CredentialFlag(
+            BoolFlag("-wrong-pake-role", &CredentialConfig::wrong_pake_role)),
         IntFlag("-private-key-delay-ms", &TestConfig::private_key_delay_ms),
     };
     std::sort(ret.begin(), ret.end(), FlagNameComparator{});
@@ -555,10 +567,8 @@
 
 }  // namespace
 
-bool ParseConfig(int argc, char **argv, bool is_shim,
-                 TestConfig *out_initial,
-                 TestConfig *out_resume,
-                 TestConfig *out_retry) {
+bool ParseConfig(int argc, char **argv, bool is_shim, TestConfig *out_initial,
+                 TestConfig *out_resume, TestConfig *out_retry) {
   for (int i = 0; i < argc; i++) {
     bool skip = false;
     const char *arg = argv[i];
@@ -672,7 +682,7 @@
 static void CredentialInfoExDataFree(void *parent, void *ptr,
                                      CRYPTO_EX_DATA *ad, int index, long argl,
                                      void *argp) {
-  delete static_cast<CredentialInfo*>(ptr);
+  delete static_cast<CredentialInfo *>(ptr);
 }
 
 static int CredentialInfoExDataIndex() {
@@ -1336,6 +1346,43 @@
     case CredentialConfigType::kDelegated:
       cred.reset(SSL_CREDENTIAL_new_delegated());
       break;
+    case CredentialConfigType::kSPAKE2PlusV1: {
+      uint8_t pw_verifier_w0[32];
+      uint8_t pw_verifier_w1[32];
+      uint8_t registration_record[65];
+      if (!SSL_spake2plusv1_register(pw_verifier_w0, pw_verifier_w1,
+                                     registration_record,
+                                     cred_config.pake_password.data(),
+                                     cred_config.pake_password.size(),
+                                     cred_config.pake_client_id.data(),
+                                     cred_config.pake_client_id.size(),
+                                     cred_config.pake_server_id.data(),
+                                     cred_config.pake_server_id.size())) {
+        return nullptr;
+      }
+      bool is_server =
+          cred_config.wrong_pake_role ? !config.is_server : config.is_server;
+      if (is_server) {
+        cred.reset(SSL_CREDENTIAL_new_spake2plusv1_server(
+            cred_config.pake_context.data(), cred_config.pake_context.size(),
+            cred_config.pake_client_id.data(),
+            cred_config.pake_client_id.size(),
+            cred_config.pake_server_id.data(),
+            cred_config.pake_server_id.size(),
+            /*attempts=*/1, pw_verifier_w0, sizeof(pw_verifier_w0),
+            registration_record, sizeof(registration_record)));
+      } else {
+        cred.reset(SSL_CREDENTIAL_new_spake2plusv1_client(
+            cred_config.pake_context.data(), cred_config.pake_context.size(),
+            cred_config.pake_client_id.data(),
+            cred_config.pake_client_id.size(),
+            cred_config.pake_server_id.data(),
+            cred_config.pake_server_id.size(),
+            /*attempts=*/1, pw_verifier_w0, sizeof(pw_verifier_w0),
+            pw_verifier_w1, sizeof(pw_verifier_w1)));
+      }
+      break;
+    }
   }
   if (cred == nullptr) {
     return nullptr;
@@ -1700,16 +1747,13 @@
   // Invoke the rewind before we sanity check SNI because we will
   // end up calling the select_cert_cb twice with two different SNIs.
   if (SSL_ech_accepted(ssl) && config->fail_early_callback_ech_rewind) {
-      return ssl_select_cert_disable_ech;
+    return ssl_select_cert_disable_ech;
   }
 
-  const char *server_name =
-      SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name);
+  const char *server_name = SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name);
 
   if (config->expect_no_server_name && server_name != nullptr) {
-    fprintf(stderr,
-            "Expected no server name but got %s.\n",
-            server_name);
+    fprintf(stderr, "Expected no server name but got %s.\n", server_name);
     return ssl_select_cert_error;
   }
 
@@ -1797,11 +1841,8 @@
 }
 
 static const SSL_QUIC_METHOD g_quic_method = {
-    SetQuicReadSecret,
-    SetQuicWriteSecret,
-    AddQuicHandshakeData,
-    FlushQuicFlight,
-    SendQuicAlert,
+    SetQuicReadSecret, SetQuicWriteSecret, AddQuicHandshakeData,
+    FlushQuicFlight,   SendQuicAlert,
 };
 
 static bool MaybeInstallCertCompressionAlg(
@@ -2219,7 +2260,7 @@
     return nullptr;
   }
   if (wpa_202304 && !SSL_set_compliance_policy(
-                         ssl.get(), ssl_compliance_policy_wpa3_192_202304)) {
+                        ssl.get(), ssl_compliance_policy_wpa3_192_202304)) {
     fprintf(stderr, "SSL_set_compliance_policy failed\n");
     return nullptr;
   }
@@ -2314,8 +2355,7 @@
           ssl.get(), SSL_is_dtls(ssl.get()) ? DTLS1_VERSION : TLS1_VERSION)) {
     return nullptr;
   }
-  if (min_version != 0 &&
-      !SSL_set_min_proto_version(ssl.get(), min_version)) {
+  if (min_version != 0 && !SSL_set_min_proto_version(ssl.get(), min_version)) {
     return nullptr;
   }
   // TODO(crbug.com/42290594): Remove this once DTLS 1.3 is enabled by default.
@@ -2323,8 +2363,7 @@
       !SSL_set_max_proto_version(ssl.get(), DTLS1_3_VERSION)) {
     return nullptr;
   }
-  if (max_version != 0 &&
-      !SSL_set_max_proto_version(ssl.get(), max_version)) {
+  if (max_version != 0 && !SSL_set_max_proto_version(ssl.get(), max_version)) {
     return nullptr;
   }
   if (mtu != 0) {
diff --git a/ssl/test/test_config.h b/ssl/test/test_config.h
index 84dce1b..dd24de9 100644
--- a/ssl/test/test_config.h
+++ b/ssl/test/test_config.h
@@ -25,7 +25,11 @@
 
 #include "test_state.h"
 
-enum class CredentialConfigType { kX509, kDelegated };
+enum class CredentialConfigType {
+  kX509,
+  kDelegated,
+  kSPAKE2PlusV1,
+};
 
 struct CredentialConfig {
   CredentialConfigType type;
@@ -35,6 +39,11 @@
   std::vector<uint8_t> delegated_credential;
   std::vector<uint8_t> ocsp_response;
   std::vector<uint8_t> signed_cert_timestamps;
+  std::vector<uint8_t> pake_context;
+  std::vector<uint8_t> pake_client_id;
+  std::vector<uint8_t> pake_server_id;
+  std::vector<uint8_t> pake_password;
+  bool wrong_pake_role = false;
 };
 
 struct TestConfig {
@@ -225,7 +234,7 @@
   std::vector<CredentialConfig> credentials;
   int private_key_delay_ms = 0;
 
-  std::vector<const char*> handshaker_args;
+  std::vector<const char *> handshaker_args;
 
   bssl::UniquePtr<SSL_CTX> SetupCtx(SSL_CTX *old_ctx) const;
 
diff --git a/ssl/tls13_client.cc b/ssl/tls13_client.cc
index adfb971..bc3fe7d 100644
--- a/ssl/tls13_client.cc
+++ b/ssl/tls13_client.cc
@@ -251,7 +251,8 @@
 
   // The ECH extension, if present, was already parsed by
   // |check_ech_confirmation|.
-  SSLExtension cookie(TLSEXT_TYPE_cookie), key_share(TLSEXT_TYPE_key_share),
+  SSLExtension cookie(TLSEXT_TYPE_cookie),
+      key_share(TLSEXT_TYPE_key_share, !hs->key_share_bytes.empty()),
       supported_versions(TLSEXT_TYPE_supported_versions),
       ech_unused(TLSEXT_TYPE_encrypted_client_hello,
                  hs->selected_ech_config || hs->config->ech_grease_enabled);
@@ -284,6 +285,10 @@
   }
 
   if (key_share.present) {
+    // If offering PAKE, we won't send key_share extensions, in which case we
+    // would have rejected key_share from the peer.
+    assert(!hs->pake_prover);
+
     uint16_t group_id;
     if (!CBS_get_u16(&key_share.data, &group_id) ||
         CBS_len(&key_share.data) != 0) {
@@ -421,12 +426,14 @@
       ssl_session_get_type(ssl->session.get()) ==
           SSLSessionType::kPreSharedKey &&
       ssl->s3->ech_status != ssl_ech_rejected;
-  SSLExtension key_share(TLSEXT_TYPE_key_share),
+  SSLExtension key_share(TLSEXT_TYPE_key_share, hs->key_shares[0] != nullptr),
+      pake_share(TLSEXT_TYPE_pake, hs->pake_prover != nullptr),
       pre_shared_key(TLSEXT_TYPE_pre_shared_key, pre_shared_key_allowed),
       supported_versions(TLSEXT_TYPE_supported_versions);
-  if (!ssl_parse_extensions(&server_hello.extensions, &alert,
-                            {&key_share, &pre_shared_key, &supported_versions},
-                            /*ignore_unknown=*/false)) {
+  if (!ssl_parse_extensions(
+          &server_hello.extensions, &alert,
+          {&key_share, &pre_shared_key, &supported_versions, &pake_share},
+          /*ignore_unknown=*/false)) {
     ssl_send_alert(ssl, SSL3_AL_FATAL, alert);
     return ssl_hs_error;
   }
@@ -442,6 +449,39 @@
     return ssl_hs_error;
   }
 
+  // The combination of ServerHello extensions determines the kind of handshake
+  // that the server selected. Check for invalid combinations.
+
+  // pake replaces key_share and may not be used with pre_shared_key.
+  if (pake_share.present && (key_share.present || pre_shared_key.present)) {
+    OPENSSL_PUT_ERROR(SSL, SSL_R_UNEXPECTED_EXTENSION);
+    ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_UNSUPPORTED_EXTENSION);
+    return ssl_hs_error;
+  }
+  // In PAKE mode, we require a PAKE handshake and do not support resumption.
+  if (hs->pake_prover != nullptr && !pake_share.present) {
+    OPENSSL_PUT_ERROR(SSL, SSL_R_MISSING_EXTENSION);
+    ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_MISSING_EXTENSION);
+    return ssl_hs_error;
+  }
+  // In non-PAKE modes, we require per-connection forward secrecy and do not
+  // support psk_ke.
+  if (hs->pake_prover == nullptr && !key_share.present) {
+    OPENSSL_PUT_ERROR(SSL, SSL_R_MISSING_KEY_SHARE);
+    ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_MISSING_EXTENSION);
+    return ssl_hs_error;
+  }
+  // The above imples only one of three handshake forms will be allowed. The
+  // checks for unsolicited extensions ensure the server did not select
+  // something we cannot respond to.
+  assert(
+      // Full handshake
+      (key_share.present && !pake_share.present && !pre_shared_key.present) ||
+      // PSK/resumption handshake
+      (key_share.present && !pake_share.present && pre_shared_key.present) ||
+      // PAKE handshake
+      (!key_share.present && pake_share.present && !pre_shared_key.present));
+
   alert = SSL_AD_DECODE_ERROR;
   if (pre_shared_key.present) {
     if (!ssl_ext_pre_shared_key_parse_serverhello(hs, &alert,
@@ -500,24 +540,29 @@
     return ssl_hs_error;
   }
 
-  if (!key_share.present) {
-    // We do not support psk_ke and thus always require a key share.
-    OPENSSL_PUT_ERROR(SSL, SSL_R_MISSING_KEY_SHARE);
-    ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_MISSING_EXTENSION);
-    return ssl_hs_error;
-  }
-
-  // Resolve ECDHE and incorporate it into the secret.
-  Array<uint8_t> dhe_secret;
+  // Resolve ECDHE or PAKE and incorporate it into the secret.
+  Array<uint8_t> shared_secret;
   alert = SSL_AD_DECODE_ERROR;
-  if (!ssl_ext_key_share_parse_serverhello(hs, &dhe_secret, &alert,
-                                           &key_share.data)) {
-    ssl_send_alert(ssl, SSL3_AL_FATAL, alert);
+  if (key_share.present) {
+    if (!ssl_ext_key_share_parse_serverhello(hs, &shared_secret, &alert,
+                                             &key_share.data)) {
+      ssl_send_alert(ssl, SSL3_AL_FATAL, alert);
+      return ssl_hs_error;
+    }
+  } else if (pake_share.present) {
+    if (!ssl_ext_pake_parse_serverhello(hs, &shared_secret, &alert,
+                                        &pake_share.data)) {
+      ssl_send_alert(ssl, SSL3_AL_FATAL, alert);
+      return ssl_hs_error;
+    }
+  } else {
+    OPENSSL_PUT_ERROR(SSL, ERR_R_INTERNAL_ERROR);
+    ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_INTERNAL_ERROR);
     return ssl_hs_error;
   }
 
-  if (!tls13_advance_key_schedule(hs, dhe_secret) ||  //
-      !ssl_hash_message(hs, msg) ||                   //
+  if (!tls13_advance_key_schedule(hs, shared_secret) ||  //
+      !ssl_hash_message(hs, msg) ||                      //
       !tls13_derive_handshake_secrets(hs)) {
     return ssl_hs_error;
   }
@@ -638,6 +683,11 @@
     return ssl_hs_ok;
   }
 
+  if (hs->pake_prover) {
+    hs->tls13_state = state_read_server_finished;
+    return ssl_hs_ok;
+  }
+
   SSLMessage msg;
   if (!ssl->method->get_message(ssl, &msg)) {
     return ssl_hs_read_message;
@@ -649,7 +699,6 @@
     return ssl_hs_ok;
   }
 
-
   SSLExtension sigalgs(TLSEXT_TYPE_signature_algorithms),
       ca(TLSEXT_TYPE_certificate_authorities);
   CBS body = msg.body, context, extensions, supported_signature_algorithms;
diff --git a/ssl/tls13_server.cc b/ssl/tls13_server.cc
index a03529a..c98f1cf 100644
--- a/ssl/tls13_server.cc
+++ b/ssl/tls13_server.cc
@@ -43,6 +43,30 @@
 // See RFC 8446, section 8.3.
 static const int32_t kMaxTicketAgeSkewSeconds = 60;
 
+static bool resolve_pake_secret(SSL_HANDSHAKE *hs) {
+  uint8_t verifier_share[spake2plus::kShareSize];
+  uint8_t verifier_confirm[spake2plus::kConfirmSize];
+  uint8_t shared_secret[spake2plus::kSecretSize];
+  if (!hs->pake_verifier->ProcessProverShare(verifier_share, verifier_confirm,
+                                             shared_secret,
+                                             hs->pake_share->pake_message)) {
+    OPENSSL_PUT_ERROR(SSL, SSL_R_DECODE_ERROR);
+    ssl_send_alert(hs->ssl, SSL3_AL_FATAL, SSL_AD_ILLEGAL_PARAMETER);
+    return false;
+  }
+
+  bssl::ScopedCBB cbb;
+  if (!CBB_init(cbb.get(), sizeof(verifier_share) + sizeof(verifier_confirm)) ||
+      !CBB_add_bytes(cbb.get(), verifier_share, sizeof(verifier_share)) ||
+      !CBB_add_bytes(cbb.get(), verifier_confirm, sizeof(verifier_confirm)) ||
+      !CBBFinishArray(cbb.get(), &hs->pake_share_bytes)) {
+    return false;
+  }
+
+  return tls13_advance_key_schedule(
+      hs, MakeConstSpan(shared_secret, sizeof(shared_secret)));
+}
+
 static bool resolve_ecdhe_secret(SSL_HANDSHAKE *hs,
                                  const SSL_CLIENT_HELLO *client_hello) {
   SSL *const ssl = hs->ssl;
@@ -131,7 +155,10 @@
       !hs->accept_psk_mode ||
       // We only implement stateless resumption in TLS 1.3, so skip sending
       // tickets if disabled.
-      (SSL_get_options(ssl) & SSL_OP_NO_TICKET)) {
+      (SSL_get_options(ssl) & SSL_OP_NO_TICKET) ||
+      // Don't send tickets for PAKE connections. We don't support resumption
+      // with PAKEs.
+      hs->pake_verifier != nullptr) {
     *out_sent_tickets = false;
     return true;
   }
@@ -220,8 +247,9 @@
   return true;
 }
 
-static bool check_credential(SSL_HANDSHAKE *hs, const SSL_CREDENTIAL *cred,
-                             uint16_t *out_sigalg) {
+static bool check_signature_credential(SSL_HANDSHAKE *hs,
+                                       const SSL_CREDENTIAL *cred,
+                                       uint16_t *out_sigalg) {
   switch (cred->type) {
     case SSLCredentialType::kX509:
       break;
@@ -234,9 +262,12 @@
         return false;
       }
       break;
+    default:
+      OPENSSL_PUT_ERROR(SSL, SSL_R_UNKNOWN_CERTIFICATE_TYPE);
+      return false;
   }
 
-  // All currently supported credentials require a signature. If |cred| is a
+  // If we reach here then the credential requires a signature. If |cred| is a
   // delegated credential, this also checks that the peer supports delegated
   // credentials and matched |dc_cert_verify_algorithm|.
   if (!tls1_choose_signature_algorithm(hs, cred, out_sigalg)) {
@@ -247,6 +278,21 @@
   return ssl_credential_matches_requested_issuers(hs, cred);
 }
 
+static bool check_pake_credential(SSL_HANDSHAKE *hs,
+                                  const SSL_CREDENTIAL *cred) {
+  assert(cred->type == SSLCredentialType::kSPAKE2PlusV1Server);
+  // Look for a client PAKE share that matches |cred|.
+  if (hs->pake_share == nullptr ||
+      hs->pake_share->named_pake != SSL_PAKE_SPAKE2PLUSV1 ||
+      hs->pake_share->client_identity != Span(cred->client_identity) ||
+      hs->pake_share->server_identity != Span(cred->server_identity)) {
+    OPENSSL_PUT_ERROR(SSL, SSL_R_PEER_PAKE_MISMATCH);
+    return false;
+  }
+
+  return true;
+}
+
 static enum ssl_hs_wait_t do_select_parameters(SSL_HANDSHAKE *hs) {
   // At this point, most ClientHello extensions have already been processed by
   // the common handshake logic. Resolve the remaining non-PSK parameters.
@@ -284,11 +330,27 @@
   // Select the credential to use.
   for (SSL_CREDENTIAL *cred : creds) {
     ERR_clear_error();
-    uint16_t sigalg;
-    if (check_credential(hs, cred, &sigalg)) {
-      hs->credential = UpRef(cred);
-      hs->signature_algorithm = sigalg;
-      break;
+    if (cred->type == SSLCredentialType::kSPAKE2PlusV1Server) {
+      if (check_pake_credential(hs, cred)) {
+        hs->credential = UpRef(cred);
+        hs->pake_verifier = MakeUnique<spake2plus::Verifier>();
+        if (hs->pake_verifier == nullptr ||
+            !hs->pake_verifier->Init(cred->pake_context, cred->client_identity,
+                                     cred->server_identity,
+                                     cred->password_verifier_w0,
+                                     cred->registration_record)) {
+          ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_INTERNAL_ERROR);
+          return ssl_hs_error;
+        }
+        break;
+      }
+    } else {
+      uint16_t sigalg;
+      if (check_signature_credential(hs, cred, &sigalg)) {
+        hs->credential = UpRef(cred);
+        hs->signature_algorithm = sigalg;
+        break;
+      }
     }
   }
   if (hs->credential == nullptr) {
@@ -360,6 +422,12 @@
     return ssl_ticket_aead_ignore_ticket;
   }
 
+  // We do not currently support resumption with PAKEs.
+  if (hs->credential != nullptr &&
+      hs->credential->type == SSLCredentialType::kSPAKE2PlusV1Server) {
+    return ssl_ticket_aead_ignore_ticket;
+  }
+
   // TLS 1.3 session tickets are renewed separately as part of the
   // NewSessionTicket.
   bool unused_renew;
@@ -485,19 +553,24 @@
 
   // Record connection properties in the new session.
   hs->new_session->cipher = hs->new_cipher;
-  if (!tls1_get_shared_group(hs, &hs->new_session->group_id)) {
-    OPENSSL_PUT_ERROR(SSL, SSL_R_NO_SHARED_GROUP);
-    ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_HANDSHAKE_FAILURE);
-    return ssl_hs_error;
-  }
 
-  // Determine if we need HelloRetryRequest.
-  bool found_key_share;
-  if (!ssl_ext_key_share_parse_clienthello(hs, &found_key_share,
-                                           /*out_key_share=*/nullptr, &alert,
-                                           &client_hello)) {
-    ssl_send_alert(ssl, SSL3_AL_FATAL, alert);
-    return ssl_hs_error;
+  // If using key shares, resolve the supported group and determine if we need
+  // HelloRetryRequest.
+  bool need_hrr = false;
+  if (hs->pake_verifier == nullptr) {
+    if (!tls1_get_shared_group(hs, &hs->new_session->group_id)) {
+      OPENSSL_PUT_ERROR(SSL, SSL_R_NO_SHARED_GROUP);
+      ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_HANDSHAKE_FAILURE);
+      return ssl_hs_error;
+    }
+    bool found_key_share;
+    if (!ssl_ext_key_share_parse_clienthello(hs, &found_key_share,
+                                             /*out_key_share=*/nullptr, &alert,
+                                             &client_hello)) {
+      ssl_send_alert(ssl, SSL3_AL_FATAL, alert);
+      return ssl_hs_error;
+    }
+    need_hrr = !found_key_share;
   }
 
   // Determine if we're negotiating 0-RTT.
@@ -527,7 +600,7 @@
     ssl->s3->early_data_reason = ssl_early_data_ticket_age_skew;
   } else if (!quic_ticket_compatible(session.get(), hs->config)) {
     ssl->s3->early_data_reason = ssl_early_data_quic_parameter_mismatch;
-  } else if (!found_key_share) {
+  } else if (need_hrr) {
     ssl->s3->early_data_reason = ssl_early_data_hello_retry_request;
   } else {
     // |ssl_session_is_resumable| forbids cross-cipher resumptions even if the
@@ -593,7 +666,7 @@
     ssl->s3->skip_early_data = true;
   }
 
-  if (!found_key_share) {
+  if (need_hrr) {
     ssl->method->next_message(ssl);
     if (!hs->transcript.UpdateForHelloRetryRequest()) {
       return ssl_hs_error;
@@ -602,8 +675,22 @@
     return ssl_hs_ok;
   }
 
-  if (!resolve_ecdhe_secret(hs, &client_hello)) {
-    return ssl_hs_error;
+  if (hs->pake_verifier) {
+    assert(!ssl->s3->session_reused);
+    // Revealing the PAKE share (notably confirmV) allows the client to confirm
+    // one PAKE guess, so we must deduct from the brute force limit.
+    if (!hs->credential->ClaimPAKEAttempt()) {
+      OPENSSL_PUT_ERROR(SSL, SSL_R_PAKE_EXHAUSTED);
+      ssl_send_alert(hs->ssl, SSL3_AL_FATAL, SSL_AD_INTERNAL_ERROR);
+      return ssl_hs_error;
+    }
+    if (!resolve_pake_secret(hs)) {
+      return ssl_hs_error;
+    }
+  } else {
+    if (!resolve_ecdhe_secret(hs, &client_hello)) {
+      return ssl_hs_error;
+    }
   }
 
   ssl->method->next_message(ssl);
@@ -618,6 +705,9 @@
     return ssl_hs_hints_ready;
   }
 
+  // Although a server could HelloRetryRequest with PAKEs to request a cookie,
+  // we never do so.
+  assert(hs->pake_verifier == nullptr);
   ScopedCBB cbb;
   CBB body, session_id, extensions;
   if (!ssl->method->init_message(ssl, cbb.get(), &body, SSL3_MT_SERVER_HELLO) ||
@@ -775,6 +865,9 @@
     }
   }
 
+  // Although a server could HelloRetryRequest with PAKEs to request a cookie,
+  // we never do so.
+  assert(hs->pake_verifier == nullptr);
   if (!resolve_ecdhe_secret(hs, &client_hello)) {
     return ssl_hs_error;
   }
@@ -832,6 +925,7 @@
       !CBB_add_u8(&body, 0) ||
       !CBB_add_u16_length_prefixed(&body, &extensions) ||
       !ssl_ext_pre_shared_key_add_serverhello(hs, &extensions) ||
+      !ssl_ext_pake_add_serverhello(hs, &extensions) ||
       !ssl_ext_key_share_add_serverhello(hs, &extensions) ||
       !ssl_ext_supported_versions_add_serverhello(hs, &extensions) ||
       !ssl->method->finish_message(ssl, cbb.get(), &server_hello)) {
@@ -882,7 +976,7 @@
     return ssl_hs_error;
   }
 
-  if (!ssl->s3->session_reused) {
+  if (!ssl->s3->session_reused && !hs->pake_verifier) {
     // Determine whether to request a client certificate.
     hs->cert_request = !!(hs->config->verify_mode & SSL_VERIFY_PEER);
     // Only request a certificate if Channel ID isn't negotiated.
@@ -926,7 +1020,7 @@
   }
 
   // Send the server Certificate message, if necessary.
-  if (!ssl->s3->session_reused) {
+  if (!ssl->s3->session_reused && !hs->pake_verifier) {
     if (!tls13_add_certificate(hs)) {
       return ssl_hs_error;
     }
@@ -1274,6 +1368,13 @@
     hs->tls13_state = state13_done;
   }
 
+  if (hs->credential != nullptr &&
+      hs->credential->type == SSLCredentialType::kSPAKE2PlusV1Server) {
+    // The client has now confirmed that it does know the correct password, so
+    // this connection no longer counts towards the brute force limit.
+    hs->credential->RestorePAKEAttempt();
+  }
+
   ssl->method->next_message(ssl);
   if (SSL_is_dtls(ssl)) {
     ssl->method->schedule_ack(ssl);