Implement RFC 9258 as a server

Fixed: 369963041
Change-Id: I30fc886360723658f0354a4ca7ce4e5b2a4c7e2c
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/89547
Auto-Submit: David Benjamin <davidben@google.com>
Reviewed-by: Lily Chen <chlily@google.com>
Commit-Queue: Lily Chen <chlily@google.com>
Commit-Queue: David Benjamin <davidben@google.com>
diff --git a/crypto/err/ssl.errordata b/crypto/err/ssl.errordata
index b2028a4..e0159fe 100644
--- a/crypto/err/ssl.errordata
+++ b/crypto/err/ssl.errordata
@@ -132,6 +132,7 @@
 SSL,183,NO_REQUIRED_DIGEST
 SSL,184,NO_SHARED_CIPHER
 SSL,266,NO_SHARED_GROUP
+SSL,332,NO_SUPPORTED_PSK_MODE
 SSL,280,NO_SUPPORTED_VERSIONS_ENABLED
 SSL,185,NULL_SSL_CTX
 SSL,186,NULL_SSL_METHOD_PASSED
diff --git a/crypto/mem_internal.h b/crypto/mem_internal.h
index 5cc5e5c..ee003f2 100644
--- a/crypto/mem_internal.h
+++ b/crypto/mem_internal.h
@@ -87,7 +87,9 @@
 namespace internal {
 
 // All types with kAllowUniquePtr set may be used with UniquePtr. Other types
-// may be C structs which require a |BORINGSSL_MAKE_DELETER| registration.
+// may be C structs which require a |BORINGSSL_MAKE_DELETER| registration. Where
+// an internal type cannot be annotated (e.g. an alias of std::variant), use
+// |BORINGSSL_MAKE_DELETER(T, Delete)|.
 template <typename T>
 struct DeleterImpl<T, std::enable_if_t<T::kAllowUniquePtr>> {
   static void Free(T *t) { Delete(t); }
diff --git a/gen/crypto/err_data.cc b/gen/crypto/err_data.cc
index 4d7c79a..9d8e2d2 100644
--- a/gen/crypto/err_data.cc
+++ b/gen/crypto/err_data.cc
@@ -202,51 +202,51 @@
     0x283500f7,
     0x28358cc1,
     0x2836099a,
-    0x2c323438,
+    0x2c32344e,
     0x2c3293f7,
-    0x2c333446,
-    0x2c33b458,
-    0x2c34346c,
-    0x2c34b47e,
-    0x2c353499,
-    0x2c35b4ab,
-    0x2c3634db,
+    0x2c33345c,
+    0x2c33b46e,
+    0x2c343482,
+    0x2c34b494,
+    0x2c3534af,
+    0x2c35b4c1,
+    0x2c3634f1,
     0x2c36833a,
-    0x2c3734e8,
-    0x2c37b514,
-    0x2c383552,
-    0x2c38b569,
-    0x2c393587,
-    0x2c39b597,
-    0x2c3a35a9,
-    0x2c3ab5bd,
-    0x2c3b35ce,
-    0x2c3bb5ed,
+    0x2c3734fe,
+    0x2c37b52a,
+    0x2c383568,
+    0x2c38b57f,
+    0x2c39359d,
+    0x2c39b5ad,
+    0x2c3a35bf,
+    0x2c3ab5d3,
+    0x2c3b35e4,
+    0x2c3bb603,
     0x2c3c1409,
     0x2c3c941f,
-    0x2c3d3632,
+    0x2c3d3648,
     0x2c3d9438,
-    0x2c3e365c,
-    0x2c3eb66a,
-    0x2c3f3682,
-    0x2c3fb69a,
-    0x2c4036c4,
+    0x2c3e3672,
+    0x2c3eb680,
+    0x2c3f3698,
+    0x2c3fb6b0,
+    0x2c4036da,
     0x2c4092ec,
-    0x2c4136d5,
-    0x2c41b6e8,
+    0x2c4136eb,
+    0x2c41b6fe,
     0x2c4212b2,
-    0x2c42b6f9,
+    0x2c42b70f,
     0x2c43076d,
-    0x2c43b5df,
-    0x2c443527,
-    0x2c44b6a7,
-    0x2c4534be,
-    0x2c45b4fa,
-    0x2c463577,
-    0x2c46b601,
-    0x2c473616,
-    0x2c47b64f,
-    0x2c483539,
+    0x2c43b5f5,
+    0x2c44353d,
+    0x2c44b6bd,
+    0x2c4534d4,
+    0x2c45b510,
+    0x2c46358d,
+    0x2c46b617,
+    0x2c47362c,
+    0x2c47b665,
+    0x2c48354f,
     0x30320000,
     0x30328015,
     0x3033001f,
@@ -469,74 +469,74 @@
     0x405b25b0,
     0x405ba5c1,
     0x405c25d4,
-    0x405ca613,
-    0x405d2620,
-    0x405da645,
-    0x405e2683,
+    0x405ca629,
+    0x405d2636,
+    0x405da65b,
+    0x405e2699,
     0x405e8afe,
-    0x405f26d2,
-    0x405fa6df,
-    0x406026ed,
-    0x4060a70f,
-    0x40612783,
-    0x4061a7bb,
-    0x406227d2,
-    0x4062a7e3,
-    0x40632830,
-    0x4063a845,
-    0x4064285c,
-    0x4064a888,
-    0x406528a3,
-    0x4065a8ba,
-    0x406628d2,
-    0x4066a8fc,
-    0x40672927,
-    0x4067a96c,
-    0x406829b4,
-    0x4068a9d5,
-    0x40692a07,
-    0x4069aa35,
-    0x406a2a56,
-    0x406aaa76,
-    0x406b2bfe,
-    0x406bac21,
-    0x406c2c37,
-    0x406caf41,
-    0x406d2f70,
-    0x406daf98,
-    0x406e2fc6,
-    0x406eb013,
-    0x406f306c,
-    0x406fb0a4,
-    0x407030b7,
-    0x4070b0d4,
+    0x405f26e8,
+    0x405fa6f5,
+    0x40602703,
+    0x4060a725,
+    0x40612799,
+    0x4061a7d1,
+    0x406227e8,
+    0x4062a7f9,
+    0x40632846,
+    0x4063a85b,
+    0x40642872,
+    0x4064a89e,
+    0x406528b9,
+    0x4065a8d0,
+    0x406628e8,
+    0x4066a912,
+    0x4067293d,
+    0x4067a982,
+    0x406829ca,
+    0x4068a9eb,
+    0x40692a1d,
+    0x4069aa4b,
+    0x406a2a6c,
+    0x406aaa8c,
+    0x406b2c14,
+    0x406bac37,
+    0x406c2c4d,
+    0x406caf57,
+    0x406d2f86,
+    0x406dafae,
+    0x406e2fdc,
+    0x406eb029,
+    0x406f3082,
+    0x406fb0ba,
+    0x407030cd,
+    0x4070b0ea,
     0x4071084d,
-    0x4071b0e6,
-    0x407230f9,
-    0x4072b12f,
-    0x40733147,
+    0x4071b0fc,
+    0x4072310f,
+    0x4072b145,
+    0x4073315d,
     0x40739621,
-    0x4074315b,
-    0x4074b175,
-    0x40753186,
-    0x4075b19a,
-    0x407631a8,
+    0x40743171,
+    0x4074b18b,
+    0x4075319c,
+    0x4075b1b0,
+    0x407631be,
     0x407693af,
-    0x407731cd,
-    0x4077b229,
-    0x40783244,
-    0x4078b27d,
-    0x40793294,
-    0x4079b2aa,
-    0x407a32d6,
-    0x407ab2e9,
-    0x407b32fe,
-    0x407bb310,
-    0x407c3341,
-    0x407cb34a,
-    0x407d29f0,
+    0x407731e3,
+    0x4077b23f,
+    0x4078325a,
+    0x4078b293,
+    0x407932aa,
+    0x4079b2c0,
+    0x407a32ec,
+    0x407ab2ff,
+    0x407b3314,
+    0x407bb326,
+    0x407c3357,
+    0x407cb360,
+    0x407d2a06,
     0x407da293,
-    0x407e3259,
+    0x407e326f,
     0x407ea525,
     0x407f1e86,
     0x407fa069,
@@ -544,58 +544,58 @@
     0x40809eae,
     0x40812393,
     0x4081a170,
-    0x40822fb1,
+    0x40822fc7,
     0x40829c01,
     0x40832500,
-    0x4083a86d,
+    0x4083a883,
     0x40841ed2,
     0x4084a55d,
     0x408525e5,
-    0x4085a74a,
-    0x40862665,
+    0x4085a760,
+    0x4086267b,
     0x4086a2c8,
-    0x40872ff7,
-    0x4087a798,
+    0x4087300d,
+    0x4087a7ae,
     0x40881c3f,
-    0x4088a97f,
+    0x4088a995,
     0x40891c8e,
     0x40899c1b,
-    0x408a2c6f,
+    0x408a2c85,
     0x408a9a39,
-    0x408b3325,
-    0x408bb081,
-    0x408c25f5,
+    0x408b333b,
+    0x408bb097,
+    0x408c260b,
     0x408d1fba,
     0x408d9f04,
     0x408e20ea,
     0x408ea450,
-    0x408f2993,
-    0x408fa766,
-    0x40902948,
-    0x4090a637,
-    0x40912c57,
+    0x408f29a9,
+    0x408fa77c,
+    0x4090295e,
+    0x4090a64d,
+    0x40912c6d,
     0x40919a71,
     0x40921cdb,
-    0x4092b032,
-    0x40933112,
+    0x4092b048,
+    0x40933128,
     0x4093a2d9,
     0x40941ee6,
-    0x4094ac88,
-    0x409527f4,
-    0x4095b2b6,
-    0x40962fde,
+    0x4094ac9e,
+    0x4095280a,
+    0x4095b2cc,
+    0x40962ff4,
     0x4096a21e,
     0x40972359,
     0x4097a139,
     0x40981d3b,
-    0x4098a808,
-    0x4099304e,
+    0x4098a81e,
+    0x40993064,
     0x4099a47d,
     0x409a2416,
     0x409a9a55,
     0x409b1f40,
     0x409b9f6b,
-    0x409c320b,
+    0x409c3221,
     0x409c9f93,
     0x409d21da,
     0x409da186,
@@ -608,48 +608,49 @@
     0x40a121a1,
     0x40a1a571,
     0x40a222f5,
-    0x40a2a6c3,
-    0x40a32737,
-    0x40a3b1ef,
+    0x40a2a6d9,
+    0x40a3274d,
+    0x40a3b205,
     0x40a4233f,
     0x40a4a1b8,
     0x40a51ec2,
     0x40a5a2ad,
-    0x41f42b29,
-    0x41f92bbb,
-    0x41fe2aae,
-    0x41fead64,
-    0x41ff2e92,
-    0x42032b42,
-    0x42082b64,
-    0x4208aba0,
-    0x42092a92,
-    0x4209abda,
-    0x420a2ae9,
-    0x420aaac9,
-    0x420b2b09,
-    0x420bab82,
-    0x420c2eae,
-    0x420cac98,
-    0x420d2d4b,
-    0x420dad82,
-    0x42122db5,
-    0x42172e75,
-    0x4217adf7,
-    0x421c2e19,
-    0x421f2dd4,
-    0x42212f26,
-    0x42262e58,
-    0x422b2f04,
-    0x422bad26,
-    0x422c2ee6,
-    0x422cacd9,
-    0x422d2cb2,
-    0x422daec5,
-    0x422e2d05,
-    0x42302e34,
-    0x4230ad9c,
-    0x423126a4,
+    0x40a625f5,
+    0x41f42b3f,
+    0x41f92bd1,
+    0x41fe2ac4,
+    0x41fead7a,
+    0x41ff2ea8,
+    0x42032b58,
+    0x42082b7a,
+    0x4208abb6,
+    0x42092aa8,
+    0x4209abf0,
+    0x420a2aff,
+    0x420aaadf,
+    0x420b2b1f,
+    0x420bab98,
+    0x420c2ec4,
+    0x420cacae,
+    0x420d2d61,
+    0x420dad98,
+    0x42122dcb,
+    0x42172e8b,
+    0x4217ae0d,
+    0x421c2e2f,
+    0x421f2dea,
+    0x42212f3c,
+    0x42262e6e,
+    0x422b2f1a,
+    0x422bad3c,
+    0x422c2efc,
+    0x422cacef,
+    0x422d2cc8,
+    0x422daedb,
+    0x422e2d1b,
+    0x42302e4a,
+    0x4230adb2,
+    0x423126ba,
     0x44320778,
     0x44328787,
     0x44330793,
@@ -705,71 +706,71 @@
     0x4c419501,
     0x4c42166a,
     0x4c429449,
-    0x5032370b,
-    0x5032b71a,
-    0x50333725,
-    0x5033b735,
-    0x5034374e,
-    0x5034b768,
-    0x50353776,
-    0x5035b78c,
-    0x5036379e,
-    0x5036b7b4,
-    0x503737cd,
-    0x5037b7e0,
-    0x503837f8,
-    0x5038b809,
-    0x5039381e,
-    0x5039b832,
-    0x503a3852,
-    0x503ab868,
-    0x503b3880,
-    0x503bb892,
-    0x503c38ae,
-    0x503cb8c5,
-    0x503d38de,
-    0x503db8f4,
-    0x503e3901,
-    0x503eb917,
-    0x503f3929,
+    0x50323721,
+    0x5032b730,
+    0x5033373b,
+    0x5033b74b,
+    0x50343764,
+    0x5034b77e,
+    0x5035378c,
+    0x5035b7a2,
+    0x503637b4,
+    0x5036b7ca,
+    0x503737e3,
+    0x5037b7f6,
+    0x5038380e,
+    0x5038b81f,
+    0x50393834,
+    0x5039b848,
+    0x503a3868,
+    0x503ab87e,
+    0x503b3896,
+    0x503bb8a8,
+    0x503c38c4,
+    0x503cb8db,
+    0x503d38f4,
+    0x503db90a,
+    0x503e3917,
+    0x503eb92d,
+    0x503f393f,
     0x503f83b3,
-    0x5040393c,
-    0x5040b94c,
-    0x50413966,
-    0x5041b975,
-    0x5042398f,
-    0x5042b9ac,
-    0x504339bc,
-    0x5043b9cc,
-    0x504439e9,
+    0x50403952,
+    0x5040b962,
+    0x5041397c,
+    0x5041b98b,
+    0x504239a5,
+    0x5042b9c2,
+    0x504339d2,
+    0x5043b9e2,
+    0x504439ff,
     0x50448469,
-    0x504539fd,
-    0x5045ba1b,
-    0x50463a2e,
-    0x5046ba44,
-    0x50473a56,
-    0x5047ba6b,
-    0x50483a91,
-    0x5048ba9f,
-    0x50493ab2,
-    0x5049bac7,
-    0x504a3add,
-    0x504abaed,
-    0x504b3b0d,
-    0x504bbb20,
-    0x504c3b43,
-    0x504cbb71,
-    0x504d3b9e,
-    0x504dbbbb,
-    0x504e3bd6,
-    0x504ebbf2,
-    0x504f3c04,
-    0x504fbc1b,
-    0x50503c2a,
+    0x50453a13,
+    0x5045ba31,
+    0x50463a44,
+    0x5046ba5a,
+    0x50473a6c,
+    0x5047ba81,
+    0x50483aa7,
+    0x5048bab5,
+    0x50493ac8,
+    0x5049badd,
+    0x504a3af3,
+    0x504abb03,
+    0x504b3b23,
+    0x504bbb36,
+    0x504c3b59,
+    0x504cbb87,
+    0x504d3bb4,
+    0x504dbbd1,
+    0x504e3bec,
+    0x504ebc08,
+    0x504f3c1a,
+    0x504fbc31,
+    0x50503c40,
     0x50508729,
-    0x50513c3d,
-    0x5051b9db,
-    0x50523b83,
+    0x50513c53,
+    0x5051b9f1,
+    0x50523b99,
     0x58321011,
     0x68320fd3,
     0x68328d2b,
@@ -814,19 +815,19 @@
     0x7c3212c8,
     0x80321514,
     0x80328090,
-    0x80333407,
+    0x8033341d,
     0x803380b9,
-    0x80343416,
-    0x8034b37e,
-    0x8035339c,
-    0x8035b42a,
-    0x803633de,
-    0x8036b38d,
-    0x803733d0,
-    0x8037b36b,
-    0x803833f1,
-    0x8038b3ad,
-    0x803933c2,
+    0x8034342c,
+    0x8034b394,
+    0x803533b2,
+    0x8035b440,
+    0x803633f4,
+    0x8036b3a3,
+    0x803733e6,
+    0x8037b381,
+    0x80383407,
+    0x8038b3c3,
+    0x803933d8,
     0x84320bb0,
     0x84328bc9,
 };
@@ -1305,6 +1306,7 @@
     "NO_REQUIRED_DIGEST\0"
     "NO_SHARED_CIPHER\0"
     "NO_SHARED_GROUP\0"
+    "NO_SUPPORTED_PSK_MODE\0"
     "NO_SUPPORTED_VERSIONS_ENABLED\0"
     "NULL_SSL_CTX\0"
     "NULL_SSL_METHOD_PASSED\0"
diff --git a/include/openssl/ssl.h b/include/openssl/ssl.h
index fcfd67d..96e6c2a 100644
--- a/include/openssl/ssl.h
+++ b/include/openssl/ssl.h
@@ -3755,9 +3755,6 @@
 // In both clients and servers, if a caller configures one or more PSK
 // credentials, and calls no certificate-related functions, the connection will
 // only accept one of those PSKs.
-//
-// TODO(crbug.com/369963041): These credentials are currently only implemented
-// as a client. Implement these as a server as well.
 OPENSSL_EXPORT SSL_CREDENTIAL *SSL_CREDENTIAL_new_pre_shared_key(
     const uint8_t *key, size_t key_len, const uint8_t *id, size_t id_len,
     const EVP_MD *md, const uint8_t *context, size_t context_len);
@@ -3892,10 +3889,9 @@
 // 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.
+// secret. For example, an application may export a PSK from a PAKE connection
+// with |SSL_export_keying_material|, and then pass the result to
+// |SSL_CREDENTIAL_new_pre_shared_key| to authenticate subsequent connections.
 //
 // WARNING: PAKE support in TLS is still experimental and may change as the
 // standard evolves. See
@@ -6820,6 +6816,7 @@
 #define SSL_R_INVALID_CERTIFICATE_PROPERTY_LIST 329
 #define SSL_R_DUPLICATE_GROUP 330
 #define SSL_R_INVALID_PSK_FOR_CONNECTION 331
+#define SSL_R_NO_SUPPORTED_PSK_MODE 332
 #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
diff --git a/ssl/extensions.cc b/ssl/extensions.cc
index 79d89a0..84c80ab 100644
--- a/ssl/extensions.cc
+++ b/ssl/extensions.cc
@@ -2066,82 +2066,134 @@
   return &hs->pre_shared_keys[selected_identity];
 }
 
-bool ssl_ext_pre_shared_key_parse_clienthello(
-    SSL_HANDSHAKE *hs, CBS *out_ticket, CBS *out_binders,
-    uint32_t *out_obfuscated_ticket_age, uint8_t *out_alert,
-    const SSL_CLIENT_HELLO *client_hello, CBS *contents) {
+std::optional<SSLOfferedPSK> SSLOfferedPSKs::Next() {
+  if (CBS_len(&identities) == 0) {
+    return std::nullopt;
+  }
+  SSLOfferedPSK psk;
+  if (!CBS_get_u16_length_prefixed(&identities, &psk.identity) ||
+      CBS_len(&psk.identity) == 0 ||
+      !CBS_get_u32(&identities, &psk.obfuscated_ticket_age) ||
+      !CBS_get_u8_length_prefixed(&binders, &psk.binder) ||
+      CBS_len(&psk.binder) == 0) {
+    // After a successful parse, this should never happen.
+    return std::nullopt;
+  }
+  return psk;
+}
+
+std::optional<SSLOfferedPSKs> ssl_ext_pre_shared_key_parse_clienthello(
+    SSL_HANDSHAKE *hs, uint8_t *out_alert, const SSL_CLIENT_HELLO *client_hello,
+    CBS *contents) {
   // Verify that the pre_shared_key extension is the last extension in
   // ClientHello.
   if (CBS_data(contents) + CBS_len(contents) !=
       client_hello->extensions + client_hello->extensions_len) {
     OPENSSL_PUT_ERROR(SSL, SSL_R_PRE_SHARED_KEY_MUST_BE_LAST);
     *out_alert = SSL_AD_ILLEGAL_PARAMETER;
-    return false;
+    return std::nullopt;
   }
 
-  // We only process the first PSK identity since we don't support pure PSK.
-  CBS identities, binders;
-  if (!CBS_get_u16_length_prefixed(contents, &identities) ||    //
-      !CBS_get_u16_length_prefixed(&identities, out_ticket) ||  //
-      !CBS_get_u32(&identities, out_obfuscated_ticket_age) ||   //
-      !CBS_get_u16_length_prefixed(contents, &binders) ||       //
-      CBS_len(&binders) == 0 ||                                 //
+  SSLOfferedPSKs psks;
+  if (!CBS_get_u16_length_prefixed(contents, &psks.identities) ||
+      !CBS_get_u16_length_prefixed(contents, &psks.binders) ||
+      CBS_len(&psks.identities) == 0 ||  //
+      CBS_len(&psks.binders) == 0 ||     //
       CBS_len(contents) != 0) {
     OPENSSL_PUT_ERROR(SSL, SSL_R_DECODE_ERROR);
     *out_alert = SSL_AD_DECODE_ERROR;
-    return false;
+    return std::nullopt;
   }
 
-  *out_binders = binders;
-
-  // Check the syntax of the remaining identities, but do not process them.
-  size_t num_identities = 1;
-  while (CBS_len(&identities) != 0) {
-    CBS unused_ticket;
-    uint32_t unused_obfuscated_ticket_age;
-    if (!CBS_get_u16_length_prefixed(&identities, &unused_ticket) ||
-        !CBS_get_u32(&identities, &unused_obfuscated_ticket_age)) {
+  // Check the syntax of the extension.
+  SSLOfferedPSKs copy = psks;
+  while (CBS_len(&copy.identities) != 0 && CBS_len(&copy.binders) != 0) {
+    if (!copy.Next()) {
       OPENSSL_PUT_ERROR(SSL, SSL_R_DECODE_ERROR);
       *out_alert = SSL_AD_DECODE_ERROR;
-      return false;
+      return std::nullopt;
     }
-
-    num_identities++;
   }
 
-  // Check the syntax of the binders. The value will be checked later if
-  // resuming.
-  size_t num_binders = 0;
-  while (CBS_len(&binders) != 0) {
-    CBS binder;
-    if (!CBS_get_u8_length_prefixed(&binders, &binder)) {
-      OPENSSL_PUT_ERROR(SSL, SSL_R_DECODE_ERROR);
-      *out_alert = SSL_AD_DECODE_ERROR;
-      return false;
-    }
-
-    num_binders++;
-  }
-
-  if (num_identities != num_binders) {
+  // We should have run out of identities and binders at the same time.
+  if (CBS_len(&copy.identities) != 0 ||
+      CBS_len(&copy.binders) != 0) {
     OPENSSL_PUT_ERROR(SSL, SSL_R_PSK_IDENTITY_BINDER_COUNT_MISMATCH);
     *out_alert = SSL_AD_ILLEGAL_PARAMETER;
+    return std::nullopt;
+  }
+
+  return psks;
+}
+
+bool ssl_verify_psk_binder(SSL_HANDSHAKE *hs, uint8_t *out_alert,
+                           const SSLPreSharedKey &psk,
+                           const SSL_CLIENT_HELLO &client_hello) {
+  CBS pre_shared_key;
+  if (!ssl_client_hello_get_extension(&client_hello, &pre_shared_key,
+                                      TLSEXT_TYPE_pre_shared_key)) {
+    OPENSSL_PUT_ERROR(SSL, SSL_R_MISSING_EXTENSION);
+    *out_alert = SSL_AD_MISSING_EXTENSION;
     return false;
   }
 
-  return true;
+  std::optional<SSLOfferedPSKs> offered_psks =
+      ssl_ext_pre_shared_key_parse_clienthello(hs, out_alert, &client_hello,
+                                               &pre_shared_key);
+  if (!offered_psks) {
+    return false;
+  }
+  const size_t binders_len =
+      2 /* length prefix */ + CBS_len(&offered_psks->binders);
+
+  Span<const uint8_t> identity = ssl_pre_shared_key_identity(psk);
+  for (uint16_t index = 0; true; index++) {
+    std::optional<SSLOfferedPSK> offered_psk = offered_psks->Next();
+    if (!offered_psk) {
+      OPENSSL_PUT_ERROR(SSL, SSL_R_PSK_IDENTITY_NOT_FOUND);
+      *out_alert = SSL_AD_ILLEGAL_PARAMETER;
+      return false;
+    }
+
+    if (offered_psk->identity != identity) {
+      continue;
+    }
+
+    hs->selected_psk_index = index;
+    uint8_t verify_data[EVP_MAX_MD_SIZE];
+    size_t verify_data_len;
+    if (!tls13_psk_binder(
+            hs, verify_data, &verify_data_len, psk, hs->transcript,
+            Span(client_hello.client_hello, client_hello.client_hello_len),
+            binders_len)) {
+      *out_alert = SSL_AD_INTERNAL_ERROR;
+      return false;
+    }
+
+    bool binder_ok = CBS_len(&offered_psk->binder) == verify_data_len &&
+                     CRYPTO_memcmp(CBS_data(&offered_psk->binder), verify_data,
+                                   verify_data_len) == 0;
+    if (CRYPTO_fuzzer_mode_enabled()) {
+      binder_ok = true;
+    }
+    if (!binder_ok) {
+      OPENSSL_PUT_ERROR(SSL, SSL_R_DIGEST_CHECK_FAILED);
+      *out_alert = SSL_AD_DECRYPT_ERROR;
+      return false;
+    }
+    return true;
+  }
 }
 
 bool ssl_ext_pre_shared_key_add_serverhello(SSL_HANDSHAKE *hs, CBB *out) {
-  if (!hs->ssl->s3->session_reused) {
+  if (!hs->selected_psk_index.has_value()) {
     return true;
   }
 
   CBB contents;
-  if (!CBB_add_u16(out, TLSEXT_TYPE_pre_shared_key) ||  //
-      !CBB_add_u16_length_prefixed(out, &contents) ||   //
-      // We only consider the first identity for resumption
-      !CBB_add_u16(&contents, 0) ||  //
+  if (!CBB_add_u16(out, TLSEXT_TYPE_pre_shared_key) ||
+      !CBB_add_u16_length_prefixed(out, &contents) ||
+      !CBB_add_u16(&contents, *hs->selected_psk_index) ||  //
       !CBB_flush(out)) {
     return false;
   }
@@ -4540,7 +4592,7 @@
 enum ssl_ticket_aead_result_t ssl_process_ticket(
     SSL_HANDSHAKE *hs, UniquePtr<SSL_SESSION> *out_session,
     bool *out_renew_ticket, Span<const uint8_t> ticket,
-    Span<const uint8_t> session_id) {
+    Span<const uint8_t> session_id, bool save_ticket) {
   SSL *const ssl = hs->ssl;
   *out_renew_ticket = false;
   out_session->reset();
@@ -4625,6 +4677,10 @@
     return ssl_ticket_aead_ignore_ticket;
   }
 
+  if (save_ticket && !session->ticket.CopyFrom(ticket)) {
+    return ssl_ticket_aead_error;
+  }
+
   // Envoy's tests expect the session to have a session ID that matches the
   // placeholder used by the client. It's unclear whether this is a good idea,
   // but we maintain it for now.
diff --git a/ssl/internal.h b/ssl/internal.h
index 189a904..0c81546 100644
--- a/ssl/internal.h
+++ b/ssl/internal.h
@@ -1232,7 +1232,16 @@
                                                         uint16_t protocol,
                                                         const EVP_MD *hkdf_md);
 
+// tls13_compare_imported_psk_identity returns whether |id| is equal to |cred|'s
+// imported identity for the specified target protocol and target KDF. This
+// allows matching against PSK identities without deriving imported PSK keys.
+bool tls13_compare_imported_psk_identity(Span<const uint8_t> id,
+                                         const SSL_CREDENTIAL *cred,
+                                         uint16_t protocol,
+                                         const EVP_MD *hkdf_md);
+
 using SSLPreSharedKey = std::variant<SSLImportedPSK, UniquePtr<SSL_SESSION>>;
+BORINGSSL_MAKE_DELETER(SSLPreSharedKey, Delete)
 
 // ssl_pre_shared_key_hash return's |psk|'s hash.
 const EVP_MD *ssl_pre_shared_key_hash(const SSLPreSharedKey &psk);
@@ -1240,6 +1249,9 @@
 // ssl_pre_shared_key_identity return's |psk|'s identity.
 Span<const uint8_t> ssl_pre_shared_key_identity(const SSLPreSharedKey &psk);
 
+// ssl_pre_shared_key_secret return's |psk|'s secret.
+Span<const uint8_t> ssl_pre_shared_key_secret(const SSLPreSharedKey &psk);
+
 // tls13_psk_binder calculates the PSK binder value for |psk| over |transcript|
 // and |client_hello|. On success, it writes the result to |out|, sets
 // |*out_len| to the length, and returns true. Otherwise, it returns false.
@@ -1250,13 +1262,6 @@
                       const SSLTranscript &transcript,
                       Span<const uint8_t> client_hello, size_t binders_len);
 
-// tls13_verify_psk_binder verifies that the handshake transcript, truncated up
-// to the binders has a valid binder for |session|. It returns true on success,
-// and false on failure.
-bool tls13_verify_psk_binder(const SSL_HANDSHAKE *hs,
-                             const SSL_SESSION *session, const SSLMessage &msg,
-                             CBS *binders);
-
 
 // Encrypted ClientHello.
 
@@ -1770,6 +1775,13 @@
   // pre_shared_keys are the pre-shared keys to be offered by the client.
   Vector<SSLPreSharedKey> pre_shared_keys;
 
+  // pre_shared_key is the selected pre-shared key on the server.
+  UniquePtr<SSLPreSharedKey> pre_shared_key;
+
+  // selected_psk_index is the index of the selected pre-shared key on the
+  // server.
+  std::optional<uint16_t> selected_psk_index;
+
   // transcript is the current handshake transcript.
   SSLTranscript transcript;
 
@@ -2164,14 +2176,36 @@
                                     Array<uint8_t> *out_secret,
                                     uint8_t *out_alert, CBS *contents);
 
+struct SSLOfferedPSK {
+  CBS identity, binder;
+  uint32_t obfuscated_ticket_age;
+};
+
+struct SSLOfferedPSKs {
+  CBS identities, binders;
+  std::optional<SSLOfferedPSK> Next();
+};
+
 const SSLPreSharedKey *ssl_ext_pre_shared_key_parse_serverhello(
     SSL_HANDSHAKE *hs, uint8_t *out_alert, CBS *contents);
-bool ssl_ext_pre_shared_key_parse_clienthello(
-    SSL_HANDSHAKE *hs, CBS *out_ticket, CBS *out_binders,
-    uint32_t *out_obfuscated_ticket_age, uint8_t *out_alert,
-    const SSL_CLIENT_HELLO *client_hello, CBS *contents);
+std::optional<SSLOfferedPSKs> ssl_ext_pre_shared_key_parse_clienthello(
+    SSL_HANDSHAKE *hs, uint8_t *out_alert, const SSL_CLIENT_HELLO *client_hello,
+    CBS *contents);
+
+// ssl_verify_psk_binder verifies |client_hello| has a valid binder for |psk|.
+// The binder is computed with |client_hello| and |hs|'s transcript, which
+// should not have |client_hello| in it. On success, it returns true. Otherwise,
+// it returns false and sets |*out_alert| to an alert to send.
+//
+// This function additionally saves the index where |psk| was found in |hs|. It
+// must be called before |ssl_ext_pre_shared_key_add_serverhello|.
+bool ssl_verify_psk_binder(SSL_HANDSHAKE *hs, uint8_t *out_alert,
+                           const SSLPreSharedKey &psk,
+                           const SSL_CLIENT_HELLO &client_hello);
+
 bool ssl_ext_pre_shared_key_add_serverhello(SSL_HANDSHAKE *hs, CBB *out);
 
+
 // ssl_is_sct_list_valid does a shallow parse of the SCT list in |contents| and
 // returns whether it's valid.
 bool ssl_is_sct_list_valid(const CBS *contents);
@@ -3658,10 +3692,13 @@
 //   |ssl_ticket_aead_retry|: the ticket could not be immediately decrypted.
 //       Retry later.
 //   |ssl_ticket_aead_error|: an error occurred that is fatal to the connection.
+//
+// If |save_ticket| is true, |*out_session| will have a copy of the ticket saved
+// in its |ticket| field.
 enum ssl_ticket_aead_result_t ssl_process_ticket(
     SSL_HANDSHAKE *hs, UniquePtr<SSL_SESSION> *out_session,
     bool *out_renew_ticket, Span<const uint8_t> ticket,
-    Span<const uint8_t> session_id);
+    Span<const uint8_t> session_id, bool save_ticket);
 
 // tls1_verify_channel_id processes |msg| as a Channel ID message, and verifies
 // the signature. If the key is valid, it saves the Channel ID and returns true.
diff --git a/ssl/ssl_session.cc b/ssl/ssl_session.cc
index c2a4336..528acea 100644
--- a/ssl/ssl_session.cc
+++ b/ssl/ssl_session.cc
@@ -599,7 +599,8 @@
   if (tickets_supported && CBS_len(&ticket) != 0) {
     switch (ssl_process_ticket(
         hs, &session, &renew_ticket, ticket,
-        Span(client_hello->session_id, client_hello->session_id_len))) {
+        Span(client_hello->session_id, client_hello->session_id_len),
+        /*save_ticket=*/false)) {
       case ssl_ticket_aead_success:
         break;
       case ssl_ticket_aead_ignore_ticket:
diff --git a/ssl/test/runner/common.go b/ssl/test/runner/common.go
index 1eea175..b23fd15 100644
--- a/ssl/test/runner/common.go
+++ b/ssl/test/runner/common.go
@@ -401,6 +401,7 @@
 	HasApplicationSettingsOld  bool                  // whether ALPS old codepoint was negotiated
 	PeerApplicationSettingsOld []byte                // the old application settings received from the peer
 	ECHAccepted                bool                  // whether ECH was accepted on this connection
+	SelectedPSK                *Credential           // the selected PSK, if any
 }
 
 // ClientAuthType declares the policy the server will follow for
@@ -561,7 +562,11 @@
 	Time func() time.Time
 
 	// Credential contains the credential to present to the other side of
-	// the connection. Server configurations must include this field.
+	// the connection. Server configurations must include this field. We only
+	// support one credential because, except for PSKs, offered credentials do
+	// not appear on the wire, and tests already know which credential to
+	// expect to use. For offering multiple PSKs, use the PSKCredentials
+	// field.
 	Credential *Credential
 
 	// RootCAs defines the set of root certificate authorities
@@ -697,13 +702,17 @@
 	RequestChannelID bool
 
 	// PreSharedKey, if not nil, is the pre-shared key to use with
-	// the PSK cipher suites.
+	// TLS 1.2 PSK cipher suites.
 	PreSharedKey []byte
 
 	// PreSharedKeyIdentity, if not empty, is the identity to use
-	// with the PSK cipher suites.
+	// with TLS 1.2 PSK cipher suites.
 	PreSharedKeyIdentity string
 
+	// PSKCredentials, if not empty, is a list of TLS 1.3 PSK credentials to
+	// offer as a client.
+	PSKCredentials []*Credential
+
 	// MaxEarlyDataSize controls the maximum number of bytes that the
 	// server will accept in early data and advertise in a
 	// NewSessionTicketMsg. If 0, no early data will be accepted and
@@ -1940,9 +1949,13 @@
 	// rejected. See RFC 8701.
 	ExpectGREASE bool
 
-	// OmitPSKsOnSecondClientHello, if true, causes the client to omit the
+	// OmitPSKsOnSecondClientHello causes the client to delete the specified
+	// number of PSKs, from the front, on the second ClientHello.
+	OmitPSKsOnSecondClientHello int
+
+	// OmitAllPSKsOnSecondClientHello, if true, causes the client to omit the
 	// PSK extension on the second ClientHello.
-	OmitPSKsOnSecondClientHello bool
+	OmitAllPSKsOnSecondClientHello bool
 
 	// OnlyCorruptSecondPSKBinder, if true, causes the options below to
 	// only apply to the second PSK binder.
@@ -2416,6 +2429,17 @@
 	PSKIdentity  []byte
 	PSKHash      crypto.Hash
 	PSKContext   []byte
+	// ImportTargetPSKHashes, if not empty, causes the PSK to be imported
+	// with the specified set of target PSK hashes, instead of the default
+	// set. To test unknown hashes, zero is interpreted as SHA-256 with the
+	// wrong codepoint.
+	ImportTargetPSKHashes []crypto.Hash
+	// ImportTargetPSKProtocol, if non-zero, causes the imported PSK
+	// identity use the specified value instead of the protocol.
+	ImportTargetPSKProtocol uint16
+	// AppendToImportedPSKIdentity is a byte string that is appended to the
+	// imported PSK identity.
+	AppendToImportedPSKIdentity []byte
 	// TrustAnchorID, if not empty, is the trust anchor ID for the issuer
 	// of the certificate chain.
 	TrustAnchorID []byte
diff --git a/ssl/test/runner/conn.go b/ssl/test/runner/conn.go
index 377011c..54bb7b2 100644
--- a/ssl/test/runner/conn.go
+++ b/ssl/test/runner/conn.go
@@ -155,6 +155,8 @@
 	// echAccepted indicates whether ECH was accepted for this connection.
 	echAccepted bool
 
+	selectedPSK *Credential
+
 	tmp [16]byte
 }
 
@@ -1925,6 +1927,7 @@
 		state.HasApplicationSettingsOld = c.hasApplicationSettingsOld
 		state.PeerApplicationSettingsOld = c.peerApplicationSettingsOld
 		state.ECHAccepted = c.echAccepted
+		state.SelectedPSK = c.selectedPSK
 	}
 
 	return state
diff --git a/ssl/test/runner/fuzzer_mode.json b/ssl/test/runner/fuzzer_mode.json
index f895546..5186df0 100644
--- a/ssl/test/runner/fuzzer_mode.json
+++ b/ssl/test/runner/fuzzer_mode.json
@@ -32,6 +32,7 @@
     "ShimTicketRewritable*": "Fuzzer mode does not encrypt tickets.",
 
     "Resume-Server-*Binder*": "Fuzzer mode does not check binders.",
+    "PSK-Server-*Binder*": "Fuzzer mode does not check binders.",
 
     "SkipEarlyData*": "Trial decryption does not work with the NULL cipher.",
     "EarlyDataChannelID-OfferBoth-Server-*": "Trial decryption does not work with the NULL cipher.",
diff --git a/ssl/test/runner/handshake_client.go b/ssl/test/runner/handshake_client.go
index 48089cb..aafa6e7 100644
--- a/ssl/test/runner/handshake_client.go
+++ b/ssl/test/runner/handshake_client.go
@@ -158,6 +158,24 @@
 	if session != nil && (session.vers.protocolVersion() >= VersionTLS13 || c.config.Bugs.SendBothTickets) {
 		hs.preSharedKeys = append(hs.preSharedKeys, newClientSessionPSK(session))
 	}
+	pskCreds := c.config.PSKCredentials
+	if c.config.Credential != nil && c.config.Credential.Type == CredentialTypePreSharedKey {
+		pskCreds = append(slices.Clone(pskCreds), c.config.Credential)
+	}
+	for _, cred := range pskCreds {
+		targetProtocol := version{VersionTLS13}
+		if c.isDTLS {
+			targetProtocol = version{VersionDTLS13}
+		}
+		if len(cred.ImportTargetPSKHashes) != 0 {
+			for _, targetHash := range cred.ImportTargetPSKHashes {
+				hs.preSharedKeys = append(hs.preSharedKeys, importPSK(cred, targetProtocol, targetHash))
+			}
+		} else {
+			hs.preSharedKeys = append(hs.preSharedKeys, importPSK(cred, targetProtocol, crypto.SHA256))
+			hs.preSharedKeys = append(hs.preSharedKeys, importPSK(cred, targetProtocol, crypto.SHA384))
+		}
+	}
 
 	// Set up ECH parameters.
 	var err error
@@ -1139,30 +1157,32 @@
 
 	// Resolve PSK and compute the early secret.
 	zeroSecret := hs.finishedHash.zeroSecret()
-	pskSecret := zeroSecret
+	var psk *preSharedKey
 	if hs.serverHello.hasPSKIdentity {
-		// We send at most one PSK identity.
-		if hs.session == nil || hs.serverHello.pskIdentity != 0 {
+		if int(hs.serverHello.pskIdentity) >= len(hs.preSharedKeys) {
 			c.sendAlert(alertUnknownPSKIdentity)
 			return errors.New("tls: server sent unknown PSK identity")
 		}
-		if hs.session.vers != c.vers {
+		psk = hs.preSharedKeys[hs.serverHello.pskIdentity]
+		if psk.version != c.vers {
 			c.sendAlert(alertIllegalParameter)
-			return errors.New("tls: server resumed an invalid session for the protocol version")
+			return errors.New("tls: server selected an invalid PSK for the protocol version")
 		}
-		if hs.session.cipherSuite.hash() != hs.suite.hash() {
+		if psk.hash != hs.suite.hash() {
 			c.sendAlert(alertIllegalParameter)
-			return errors.New("tls: server resumed an invalid session for the cipher suite")
+			return errors.New("tls: server selected an invalid PSK for the cipher suite")
 		}
-		pskSecret = hs.session.secret
-		c.didResume = true
+		c.didResume = psk.clientSession != nil
+		c.selectedPSK = psk.credential
+		hs.finishedHash.addEntropy(psk.secret)
+	} else {
+		hs.finishedHash.addEntropy(zeroSecret)
 	}
-	hs.finishedHash.addEntropy(pskSecret)
 
 	sharedSecret := zeroSecret
 	if len(hs.serverHello.pakeMessage) != 0 {
-		if c.didResume {
-			return errors.New("server resumed and returned a PAKE extension")
+		if psk != nil {
+			return errors.New("server selected PSK and PAKE at the same time")
 		}
 		if hs.pakeContext == nil {
 			return errors.New("server selected a PAKE unexpectedly")
@@ -1240,9 +1260,7 @@
 		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 {
+	} else if hs.pakeContext == nil && psk == nil {
 		msg, err := c.readHandshake()
 		if err != nil {
 			return err
@@ -1627,14 +1645,18 @@
 	}
 	// The first ClientHello may have set this due to OnlyCompressSecondClientHelloInner.
 	hello.reorderOuterExtensionsWithoutCompressing = false
-	if c.config.Bugs.OmitPSKsOnSecondClientHello {
+	hello.pskIdentities = hello.pskIdentities[c.config.Bugs.OmitPSKsOnSecondClientHello:]
+	hs.preSharedKeys = hs.preSharedKeys[c.config.Bugs.OmitPSKsOnSecondClientHello:]
+	if c.config.Bugs.OmitAllPSKsOnSecondClientHello {
 		hello.pskIdentities = nil
-		hello.pskBinders = nil
+		hs.preSharedKeys = nil
 	}
 	hello.raw = nil
 
 	if len(hello.pskIdentities) > 0 {
 		hs.generatePSKBinders(hello, firstHelloBytes, helloRetryRequest.marshal())
+	} else {
+		hello.pskBinders = nil
 	}
 
 	if outerHello != nil {
diff --git a/ssl/test/runner/handshake_server.go b/ssl/test/runner/handshake_server.go
index 82054ac..1acab4a 100644
--- a/ssl/test/runner/handshake_server.go
+++ b/ssl/test/runner/handshake_server.go
@@ -625,6 +625,7 @@
 		}
 		hs.hello.hasPSKIdentity = true
 		hs.hello.pskIdentity = uint16(pskIndex)
+		c.selectedPSK = config.Credential
 	}
 
 	if foundKEMode && !config.SessionTicketsDisabled {
diff --git a/ssl/test/runner/prf.go b/ssl/test/runner/prf.go
index 9c5354c..3b1a62a 100644
--- a/ssl/test/runner/prf.go
+++ b/ssl/test/runner/prf.go
@@ -468,23 +468,27 @@
 }
 
 type preSharedKey struct {
-	version      version
-	hash         crypto.Hash
-	identity     []byte
-	secret       []byte
-	binderKey    []byte
-	creationTime time.Time
-	ticketAgeAdd uint32
+	version       version
+	hash          crypto.Hash
+	identity      []byte
+	secret        []byte
+	binderKey     []byte
+	creationTime  time.Time
+	ticketAgeAdd  uint32
+	clientSession *ClientSessionState
+	serverSession *sessionState
+	credential    *Credential
 }
 
 func newClientSessionPSK(session *ClientSessionState) *preSharedKey {
 	psk := &preSharedKey{
-		version:      session.vers,
-		hash:         session.cipherSuite.hash(),
-		identity:     session.sessionTicket,
-		secret:       session.secret,
-		creationTime: session.ticketCreationTime,
-		ticketAgeAdd: session.ticketAgeAdd,
+		version:       session.vers,
+		hash:          session.cipherSuite.hash(),
+		identity:      session.sessionTicket,
+		secret:        session.secret,
+		creationTime:  session.ticketCreationTime,
+		ticketAgeAdd:  session.ticketAgeAdd,
+		clientSession: session,
 	}
 	psk.initBinder(resumptionPSKBinderLabel)
 	return psk
@@ -492,12 +496,13 @@
 
 func newServerSessionPSK(ticket []byte, session *sessionState) *preSharedKey {
 	psk := &preSharedKey{
-		version:      session.vers,
-		hash:         session.cipherSuite.hash(),
-		identity:     ticket,
-		secret:       session.secret,
-		creationTime: session.ticketCreationTime,
-		ticketAgeAdd: session.ticketAgeAdd,
+		version:       session.vers,
+		hash:          session.cipherSuite.hash(),
+		identity:      ticket,
+		secret:        session.secret,
+		creationTime:  session.ticketCreationTime,
+		ticketAgeAdd:  session.ticketAgeAdd,
+		serverSession: session,
 	}
 	psk.initBinder(resumptionPSKBinderLabel)
 	return psk
@@ -532,6 +537,10 @@
 
 	var targetKDF uint16
 	switch targetHash {
+	case 0:
+		// We treat zero as some unrecognized hash value for testing.
+		targetKDF = 0x1234
+		targetHash = crypto.SHA256
 	case crypto.SHA256:
 		targetKDF = kdfHKDFWithSHA256
 	case crypto.SHA384:
@@ -540,13 +549,19 @@
 		panic("unrecogized target HKDF hash")
 	}
 
+	targetProtocolValue := targetProtocol.wire
+	if cred.ImportTargetPSKProtocol != 0 {
+		targetProtocolValue = cred.ImportTargetPSKProtocol
+	}
+
 	// See RFC 9258, Section 5.1.
 	identity := (&importedPSKIdentity{
 		externalIdentity: cred.PSKIdentity,
 		context:          cred.PSKContext,
-		targetProtocol:   targetProtocol.wire,
+		targetProtocol:   targetProtocolValue,
 		targetKDF:        targetKDF,
 	}).marshal()
+	identity = append(identity, cred.AppendToImportedPSKIdentity...)
 
 	h := cred.PSKHash.New()
 	h.Write(identity)
@@ -561,10 +576,11 @@
 	ipskx := hkdfExpandLabel(targetProtocol, cred.PSKHash, epskx, derivedPSKLabel, identityHash, targetHash.Size())
 
 	psk := &preSharedKey{
-		version:  targetProtocol,
-		hash:     targetHash,
-		identity: identity,
-		secret:   ipskx,
+		version:    targetProtocol,
+		hash:       targetHash,
+		identity:   identity,
+		secret:     ipskx,
+		credential: cred,
 	}
 	psk.initBinder(importedPSKBinderLabel)
 	return psk
diff --git a/ssl/test/runner/psk_tests.go b/ssl/test/runner/psk_tests.go
index 36f6de6..ada0a3b 100644
--- a/ssl/test/runner/psk_tests.go
+++ b/ssl/test/runner/psk_tests.go
@@ -46,6 +46,13 @@
 		PSKContext:   []byte("context2"),
 		PSKHash:      crypto.SHA384,
 	}
+	pskSHA256Credential2 := Credential{
+		Type:         CredentialTypePreSharedKey,
+		PreSharedKey: slices.Repeat([]byte{'I', 'J', 'K', 'L'}, 8),
+		PSKIdentity:  []byte("psk3"),
+		PSKContext:   []byte("context3"),
+		PSKHash:      crypto.SHA256,
+	}
 
 	hashToPSK := func(hash crypto.Hash) *Credential {
 		switch hash {
@@ -90,6 +97,23 @@
 					// resumption connections.
 					flags: []string{"-expect-no-peer-cert"},
 				})
+				testCases = append(testCases, testCase{
+					testType: serverTest,
+					protocol: protocol,
+					name:     fmt.Sprintf("PSK-Server-%s-%s-%s", hashToString(pskHash), hashToString(cipherHash), protocol),
+					config: Config{
+						Credential:   psk,
+						MaxVersion:   VersionTLS13,
+						CipherSuites: []uint16{cipher},
+					},
+					shimCredentials: []*Credential{psk},
+					expectations: connectionExpectations{
+						selectedPSK: psk,
+					},
+					// Also test that the resulting session can be reused.
+					resumeSession:      true,
+					resumeExpectations: &connectionExpectations{},
+				})
 
 				// Test with HelloRetryRequest to ensure the client computes
 				// the second ClientHello's binder correctly, and also accounts
@@ -392,5 +416,485 @@
 			expectedError:      ":UNEXPECTED_MESSAGE:",
 			expectedLocalError: "remote error: unexpected message",
 		})
+
+		// If a server is configured to request client certificates, it should
+		// still not do so when negotiating a PSK.
+		testCases = append(testCases, testCase{
+			protocol: protocol,
+			name:     fmt.Sprintf("PSK-Server-DoNotRequestClientCertificate-%s", protocol),
+			testType: serverTest,
+			config: Config{
+				Credential: &pskSHA256Credential,
+				MaxVersion: VersionTLS13,
+			},
+			shimCredentials: []*Credential{&pskSHA256Credential},
+			flags:           []string{"-require-any-client-certificate"},
+		})
+
+		// The server should notice if the second binder is wrong.
+		for _, secondBinder := range []bool{false, true} {
+			binderStr := "FirstBinder"
+			var defaultCurves []CurveID
+			if secondBinder {
+				binderStr = "SecondBinder"
+				// Force a HelloRetryRequest by predicting an empty curve list.
+				defaultCurves = []CurveID{}
+			}
+
+			testCases = append(testCases, testCase{
+				protocol: protocol,
+				testType: serverTest,
+				name:     fmt.Sprintf("PSK-Server-BinderWrongLength-%s-%s", binderStr, protocol),
+				config: Config{
+					MaxVersion:    VersionTLS13,
+					Credential:    &pskSHA256Credential,
+					DefaultCurves: defaultCurves,
+					Bugs: ProtocolBugs{
+						SendShortPSKBinder:         true,
+						OnlyCorruptSecondPSKBinder: secondBinder,
+					},
+				},
+				shimCredentials:    []*Credential{&pskSHA256Credential},
+				shouldFail:         true,
+				expectedLocalError: "remote error: error decrypting message",
+				expectedError:      ":DIGEST_CHECK_FAILED:",
+			})
+
+			testCases = append(testCases, testCase{
+				protocol: protocol,
+				testType: serverTest,
+				name:     fmt.Sprintf("PSK-Server-NoPSKBinder-%s-%s", binderStr, protocol),
+				config: Config{
+					MaxVersion:    VersionTLS13,
+					Credential:    &pskSHA256Credential,
+					DefaultCurves: defaultCurves,
+					Bugs: ProtocolBugs{
+						SendNoPSKBinder:            true,
+						OnlyCorruptSecondPSKBinder: secondBinder,
+					},
+				},
+				shimCredentials:    []*Credential{&pskSHA256Credential},
+				shouldFail:         true,
+				expectedLocalError: "remote error: error decoding message",
+				expectedError:      ":DECODE_ERROR:",
+			})
+
+			testCases = append(testCases, testCase{
+				protocol: protocol,
+				testType: serverTest,
+				name:     fmt.Sprintf("PSK-Server-ExtraPSKBinder-%s-%s", binderStr, protocol),
+				config: Config{
+					MaxVersion:    VersionTLS13,
+					Credential:    &pskSHA256Credential,
+					DefaultCurves: defaultCurves,
+					Bugs: ProtocolBugs{
+						SendExtraPSKBinder:         true,
+						OnlyCorruptSecondPSKBinder: secondBinder,
+					},
+				},
+				shimCredentials:    []*Credential{&pskSHA256Credential},
+				shouldFail:         true,
+				expectedLocalError: "remote error: illegal parameter",
+				expectedError:      ":PSK_IDENTITY_BINDER_COUNT_MISMATCH:",
+			})
+
+			testCases = append(testCases, testCase{
+				protocol: protocol,
+				testType: serverTest,
+				name:     fmt.Sprintf("PSK-Server-ExtraIdentityNoBinder-%s-%s", binderStr, protocol),
+				config: Config{
+					MaxVersion:    VersionTLS13,
+					Credential:    &pskSHA256Credential,
+					DefaultCurves: defaultCurves,
+					Bugs: ProtocolBugs{
+						ExtraPSKIdentity:           true,
+						OnlyCorruptSecondPSKBinder: secondBinder,
+					},
+				},
+				shimCredentials:    []*Credential{&pskSHA256Credential},
+				shouldFail:         true,
+				expectedLocalError: "remote error: illegal parameter",
+				expectedError:      ":PSK_IDENTITY_BINDER_COUNT_MISMATCH:",
+			})
+
+			testCases = append(testCases, testCase{
+				protocol: protocol,
+				testType: serverTest,
+				name:     fmt.Sprintf("PSK-Server-InvalidPSKBinder-%s-%s", binderStr, protocol),
+				config: Config{
+					MaxVersion:    VersionTLS13,
+					Credential:    &pskSHA256Credential,
+					DefaultCurves: defaultCurves,
+					Bugs: ProtocolBugs{
+						SendInvalidPSKBinder:       true,
+						OnlyCorruptSecondPSKBinder: secondBinder,
+					},
+				},
+				shimCredentials:    []*Credential{&pskSHA256Credential},
+				shouldFail:         true,
+				expectedLocalError: "remote error: error decrypting message",
+				expectedError:      ":DIGEST_CHECK_FAILED:",
+			})
+
+			testCases = append(testCases, testCase{
+				protocol: protocol,
+				testType: serverTest,
+				name:     fmt.Sprintf("PSK-Server-PSKBinderFirstExtension-%s-%s", binderStr, protocol),
+				config: Config{
+					MaxVersion:    VersionTLS13,
+					Credential:    &pskSHA256Credential,
+					DefaultCurves: defaultCurves,
+					Bugs: ProtocolBugs{
+						PSKBinderFirst:             true,
+						OnlyCorruptSecondPSKBinder: secondBinder,
+					},
+				},
+				shimCredentials:    []*Credential{&pskSHA256Credential},
+				shouldFail:         true,
+				expectedLocalError: "remote error: illegal parameter",
+				expectedError:      ":PRE_SHARED_KEY_MUST_BE_LAST:",
+			})
+		}
+
+		// The server can defer configuring PSKs to either the early callback
+		// or the SSL_CTX_set_cert_cb callback. (-async causes the shim to defer
+		// installing credentials. -use-early-callback controls which callback
+		// installs it.)
+		testCases = append(testCases, testCase{
+			testType: serverTest,
+			protocol: protocol,
+			name:     fmt.Sprintf("PSK-Server-CertCallback-%s", protocol),
+			config: Config{
+				MaxVersion: VersionTLS13,
+				Credential: &pskSHA256Credential,
+			},
+			shimCredentials: []*Credential{&pskSHA256Credential},
+			flags:           []string{"-async", "-expect-selected-credential", "0"},
+			expectations: connectionExpectations{
+				selectedPSK: &pskSHA256Credential,
+			},
+		})
+		testCases = append(testCases, testCase{
+			testType: serverTest,
+			protocol: protocol,
+			name:     fmt.Sprintf("PSK-Server-EarlyCallback-%s", protocol),
+			config: Config{
+				MaxVersion: VersionTLS13,
+				Credential: &pskSHA256Credential,
+			},
+			shimCredentials: []*Credential{&pskSHA256Credential},
+			flags:           []string{"-async", "-use-early-callback", "-expect-selected-credential", "0"},
+			expectations: connectionExpectations{
+				selectedPSK: &pskSHA256Credential,
+			},
+		})
+
+		// If a server is configured with multiple PSKs, it selects the
+		// first common one.
+		testCases = append(testCases, testCase{
+			testType: serverTest,
+			protocol: protocol,
+			name:     fmt.Sprintf("PSK-Server-ConsiderMultiplePSKs-%s", protocol),
+			config: Config{
+				MaxVersion:     VersionTLS13,
+				PSKCredentials: []*Credential{&pskSHA256Credential, &pskSHA384Credential},
+			},
+			shimCredentials: []*Credential{&pskSHA256Credential2, &pskSHA384Credential},
+			flags:           []string{"-expect-selected-credential", "1"},
+			expectations: connectionExpectations{
+				selectedPSK: &pskSHA384Credential,
+			},
+		})
+
+		// The client and server have no PSKs in common.
+		testCases = append(testCases, testCase{
+			testType: serverTest,
+			protocol: protocol,
+			name:     fmt.Sprintf("PSK-Server-NoCommonPSKs-%s", protocol),
+			config: Config{
+				MaxVersion:     VersionTLS13,
+				PSKCredentials: []*Credential{&pskSHA256Credential, &pskSHA384Credential},
+			},
+			shimCredentials:    []*Credential{&pskSHA256Credential2},
+			shouldFail:         true,
+			expectedError:      ":PSK_IDENTITY_NOT_FOUND:",
+			expectedLocalError: "remote error: handshake failure",
+		})
+
+		// If the server sends HelloRetryRequest, the client may filter its PSK list
+		// based on the selected cipher. The server must send its PSK index based
+		// on the second list, not the first.
+		testCases = append(testCases, testCase{
+			testType: serverTest,
+			protocol: protocol,
+			name:     fmt.Sprintf("PSK-Server-HRR-UpdateIndex-%s", protocol),
+			config: Config{
+				MaxVersion:     VersionTLS13,
+				PSKCredentials: []*Credential{&pskSHA256Credential, &pskSHA384Credential},
+				DefaultCurves:  []CurveID{}, // Trigger HRR
+				Bugs: ProtocolBugs{
+					OmitPSKsOnSecondClientHello: 1,
+				},
+			},
+			shimCredentials: []*Credential{&pskSHA384Credential},
+			flags:           []string{"-expect-selected-credential", "0"},
+			expectations: connectionExpectations{
+				selectedPSK: &pskSHA384Credential,
+			},
+		})
+
+		// If the PSK is missing from the second ClientHello, the server should
+		// reject the connection.
+		testCases = append(testCases, testCase{
+			testType: serverTest,
+			protocol: protocol,
+			name:     fmt.Sprintf("PSK-Server-HRR-PSKMissing-%s", protocol),
+			config: Config{
+				MaxVersion:     VersionTLS13,
+				PSKCredentials: []*Credential{&pskSHA256Credential, &pskSHA384Credential},
+				DefaultCurves:  []CurveID{}, // Trigger HRR
+				Bugs: ProtocolBugs{
+					OmitPSKsOnSecondClientHello: 2, // Delete both SHA-256 and SHA-384 variants.
+				},
+			},
+			shimCredentials:    []*Credential{&pskSHA256Credential},
+			shouldFail:         true,
+			expectedLocalError: "remote error: illegal parameter",
+			expectedError:      ":PSK_IDENTITY_NOT_FOUND:",
+		})
+
+		testCases = append(testCases, testCase{
+			protocol: protocol,
+			testType: serverTest,
+			name:     fmt.Sprintf("PSK-Server-OmitAllPSKsOnSecondClientHello-%s", protocol),
+			config: Config{
+				MaxVersion:    VersionTLS13,
+				Credential:    &pskSHA256Credential,
+				DefaultCurves: []CurveID{}, // Trigger HRR
+				Bugs: ProtocolBugs{
+					OmitAllPSKsOnSecondClientHello: true,
+				},
+			},
+			shimCredentials:    []*Credential{&pskSHA256Credential},
+			shouldFail:         true,
+			expectedLocalError: "remote error: missing extension",
+			expectedError:      ":MISSING_EXTENSION:",
+		})
+
+		// The imported PSK must match exactly, or it is not in common.
+		extraBytes := pskSHA256Credential
+		extraBytes.AppendToImportedPSKIdentity = []byte("extra")
+		wrongHash := pskSHA256Credential
+		wrongHash.ImportTargetPSKHashes = []crypto.Hash{0}
+		wrongProtocol := pskSHA256Credential
+		wrongProtocol.ImportTargetPSKProtocol = 0x1234
+		wrongProtocol2 := pskSHA256Credential
+		wrongProtocol2.ImportTargetPSKProtocol = VersionDTLS13
+		wrongContext := pskSHA256Credential
+		wrongContext.PSKContext = []byte("wrong context")
+		if protocol == dtls {
+			wrongProtocol2.ImportTargetPSKProtocol = VersionTLS13
+		}
+		testCases = append(testCases, testCase{
+			testType: serverTest,
+			protocol: protocol,
+			name:     fmt.Sprintf("PSK-Server-IdentityDoesNotMatch-%s", protocol),
+			config: Config{
+				MaxVersion: VersionTLS13,
+				PSKCredentials: []*Credential{
+					&extraBytes,
+					&wrongHash,
+					&wrongProtocol,
+					&wrongProtocol2,
+					&wrongContext,
+				},
+			},
+			shimCredentials:    []*Credential{&pskSHA256Credential},
+			shouldFail:         true,
+			expectedError:      ":PSK_IDENTITY_NOT_FOUND:",
+			expectedLocalError: "remote error: handshake failure",
+		})
+
+		// If multiple PSKs match, the server's order is used.
+		testCases = append(testCases, testCase{
+			testType: serverTest,
+			protocol: protocol,
+			name:     fmt.Sprintf("PSK-Server-ServerPreferenceOrder-%s", protocol),
+			config: Config{
+				MaxVersion:     VersionTLS13,
+				PSKCredentials: []*Credential{&pskSHA384Credential, &pskSHA256Credential},
+			},
+			shimCredentials: []*Credential{&pskSHA256Credential, &pskSHA384Credential},
+			flags:           []string{"-expect-selected-credential", "0"},
+			expectations: connectionExpectations{
+				selectedPSK: &pskSHA256Credential,
+			},
+		})
+
+		// Servers can be configured with both PSKs and certificates,
+		// in which case they evaluate based on their preference order.
+		testCases = append(testCases, testCase{
+			testType: serverTest,
+			protocol: protocol,
+			name:     fmt.Sprintf("PSK-Server-PSKOrCert-PSK-%s", protocol),
+			config: Config{
+				MaxVersion:     VersionTLS13,
+				PSKCredentials: []*Credential{&pskSHA256Credential},
+			},
+			shimCredentials: []*Credential{
+				&pskSHA256Credential,
+				&rsaCertificate,
+			},
+			// The ClientHello works for both, but the shim should
+			// pick the PSK.
+			flags: []string{"-expect-selected-credential", "0"},
+			expectations: connectionExpectations{
+				selectedPSK: &pskSHA256Credential,
+			},
+		})
+		testCases = append(testCases, testCase{
+			testType: serverTest,
+			protocol: protocol,
+			name:     fmt.Sprintf("PSK-Server-PSKOrCert-Cert-%s", protocol),
+			config: Config{
+				MaxVersion:     VersionTLS13,
+				PSKCredentials: []*Credential{&pskSHA384Credential}, // Wrong PSK
+			},
+			shimCredentials: []*Credential{
+				&pskSHA256Credential,
+				&rsaCertificate,
+			},
+			// The ClientHello is not good for the PSK, so the shim
+			// should pick the certificate.
+			flags: []string{"-expect-selected-credential", "1"},
+			expectations: connectionExpectations{
+				peerCertificate: &rsaCertificate,
+			},
+		})
+		testCases = append(testCases, testCase{
+			testType: serverTest,
+			protocol: protocol,
+			name:     fmt.Sprintf("PSK-Server-CertOrPSK-Cert-%s", protocol),
+			config: Config{
+				MaxVersion:     VersionTLS13,
+				PSKCredentials: []*Credential{&pskSHA256Credential},
+			},
+			shimCredentials: []*Credential{
+				&rsaCertificate,
+				&pskSHA256Credential,
+			},
+			// The ClientHello works for both, but the shim should
+			// pick the certificate.
+			flags: []string{"-expect-selected-credential", "0"},
+			expectations: connectionExpectations{
+				peerCertificate: &rsaCertificate,
+			},
+		})
+		testCases = append(testCases, testCase{
+			testType: serverTest,
+			protocol: protocol,
+			name:     fmt.Sprintf("PSK-Server-CertOrPSK-PSK-%s", protocol),
+			config: Config{
+				MaxVersion:                VersionTLS13,
+				PSKCredentials:            []*Credential{&pskSHA256Credential},
+				VerifySignatureAlgorithms: []signatureAlgorithm{}, // No common algs
+			},
+			shimCredentials: []*Credential{
+				&rsaCertificate,
+				&pskSHA256Credential,
+			},
+			// The ClientHello is not good for the certficate, so the
+			// shim should pick the PSK.
+			flags: []string{"-expect-selected-credential", "1"},
+			expectations: connectionExpectations{
+				selectedPSK: &pskSHA256Credential,
+			},
+		})
+
+		// Clients should import PSKs for each of their supported ciphers.
+		// If one does not, the server should skip any PSKs that do not
+		// work with the chosen cipher.
+		importSHA384Only := pskSHA256Credential
+		importSHA384Only.ImportTargetPSKHashes = []crypto.Hash{crypto.SHA384}
+		testCases = append(testCases, testCase{
+			testType: serverTest,
+			protocol: protocol,
+			name:     fmt.Sprintf("PSK-Server-PartialImport-Match-%s", protocol),
+			config: Config{
+				MaxVersion:     VersionTLS13,
+				CipherSuites:   []uint16{TLS_AES_256_GCM_SHA384},
+				PSKCredentials: []*Credential{&importSHA384Only},
+			},
+			shimCredentials: []*Credential{&pskSHA256Credential},
+			flags:           []string{"-expect-selected-credential", "0"},
+			expectations: connectionExpectations{
+				selectedPSK: &importSHA384Only,
+			},
+		})
+		testCases = append(testCases, testCase{
+			testType: serverTest,
+			protocol: protocol,
+			name:     fmt.Sprintf("PSK-Server-PartialImport-NoMatch-%s", protocol),
+			config: Config{
+				MaxVersion:     VersionTLS13,
+				CipherSuites:   []uint16{TLS_AES_128_GCM_SHA256},
+				PSKCredentials: []*Credential{&importSHA384Only},
+			},
+			shimCredentials:    []*Credential{&pskSHA256Credential},
+			shouldFail:         true,
+			expectedError:      ":PSK_IDENTITY_NOT_FOUND:",
+			expectedLocalError: "remote error: handshake failure",
+		})
+		testCases = append(testCases, testCase{
+			testType: serverTest,
+			protocol: protocol,
+			name:     fmt.Sprintf("PSK-Server-PartialImport-NoMatch-Fallback-%s", protocol),
+			config: Config{
+				MaxVersion:     VersionTLS13,
+				CipherSuites:   []uint16{TLS_AES_128_GCM_SHA256},
+				PSKCredentials: []*Credential{&importSHA384Only},
+			},
+			shimCredentials: []*Credential{&pskSHA256Credential, &rsaCertificate},
+			flags:           []string{"-expect-selected-credential", "1"},
+			expectations: connectionExpectations{
+				peerCertificate: &rsaCertificate,
+			},
+		})
+
+		// We only implement psk_dhe_ke. If the client does not offer it, PSKs
+		// are not eligible.
+		testCases = append(testCases, testCase{
+			testType: serverTest,
+			protocol: protocol,
+			name:     fmt.Sprintf("PSK-Server-MissingPSKMode-NoMatch-%s", protocol),
+			config: Config{
+				MaxVersion:     VersionTLS13,
+				PSKCredentials: []*Credential{&pskSHA256Credential},
+				Bugs: ProtocolBugs{
+					SendPSKKeyExchangeModes: []byte{0x1a},
+				},
+			},
+			shimCredentials:    []*Credential{&pskSHA256Credential},
+			shouldFail:         true,
+			expectedError:      ":NO_SUPPORTED_PSK_MODE:",
+			expectedLocalError: "remote error: handshake failure",
+		})
+		testCases = append(testCases, testCase{
+			testType: serverTest,
+			protocol: protocol,
+			name:     fmt.Sprintf("PSK-Server-MissingPSKMode-NoMatch-Fallback-%s", protocol),
+			config: Config{
+				MaxVersion:     VersionTLS13,
+				PSKCredentials: []*Credential{&pskSHA256Credential},
+				Bugs: ProtocolBugs{
+					SendPSKKeyExchangeModes: []byte{0x1a},
+				},
+			},
+			shimCredentials: []*Credential{&pskSHA256Credential, &rsaCertificate},
+			flags:           []string{"-expect-selected-credential", "1"},
+			expectations: connectionExpectations{
+				peerCertificate: &rsaCertificate,
+			},
+		})
 	}
 }
diff --git a/ssl/test/runner/resumption_tests.go b/ssl/test/runner/resumption_tests.go
index ccb3053..efae49d 100644
--- a/ssl/test/runner/resumption_tests.go
+++ b/ssl/test/runner/resumption_tests.go
@@ -575,18 +575,18 @@
 
 	testCases = append(testCases, testCase{
 		testType:      serverTest,
-		name:          "Resume-Server-OmitPSKsOnSecondClientHello",
+		name:          "Resume-Server-OmitAllPSKsOnSecondClientHello",
 		resumeSession: true,
 		config: Config{
 			MaxVersion:    VersionTLS13,
 			DefaultCurves: []CurveID{},
 			Bugs: ProtocolBugs{
-				OmitPSKsOnSecondClientHello: true,
+				OmitAllPSKsOnSecondClientHello: true,
 			},
 		},
 		shouldFail:         true,
-		expectedLocalError: "remote error: illegal parameter",
-		expectedError:      ":INCONSISTENT_CLIENT_HELLO:",
+		expectedLocalError: "remote error: missing extension",
+		expectedError:      ":MISSING_EXTENSION:",
 	})
 }
 
diff --git a/ssl/test/runner/runner.go b/ssl/test/runner/runner.go
index ebc1f46..99019fa 100644
--- a/ssl/test/runner/runner.go
+++ b/ssl/test/runner/runner.go
@@ -453,6 +453,9 @@
 	// serverNameAck, if not nil, is whether the server should have acknowledged
 	// the server name. This field is only checked in full handshakes.
 	serverNameAck *bool
+	// selectedPSK, if not nil, is the PSK credential that should have been
+	// negotiated.
+	selectedPSK *Credential
 }
 
 type testCase struct {
@@ -945,6 +948,13 @@
 		}
 	}
 
+	if expectations.selectedPSK != nil && expectations.selectedPSK != connState.SelectedPSK {
+		if connState.SelectedPSK == nil {
+			return errors.New("tls: expected a PSK, but none was selected")
+		}
+		return errors.New("tls: connection selected a different PSK from what was expected")
+	}
+
 	if test.exportKeyingMaterial > 0 {
 		actual := make([]byte, test.exportKeyingMaterial)
 		if _, err := io.ReadFull(tlsConn, actual); err != nil {
diff --git a/ssl/tls13_enc.cc b/ssl/tls13_enc.cc
index c24cf58..9d28be5 100644
--- a/ssl/tls13_enc.cc
+++ b/ssl/tls13_enc.cc
@@ -518,6 +518,14 @@
   return std::get<UniquePtr<SSL_SESSION>>(psk)->ticket;
 }
 
+Span<const uint8_t> ssl_pre_shared_key_secret(const SSLPreSharedKey &psk) {
+  if (const auto *imported = std::get_if<SSLImportedPSK>(&psk);
+      imported != nullptr) {
+    return imported->ipskx;
+  }
+  return std::get<UniquePtr<SSL_SESSION>>(psk)->secret;
+}
+
 bool tls13_psk_binder(const SSL_HANDSHAKE *hs, Span<uint8_t> out,
                       size_t *out_len, const SSLPreSharedKey &psk,
                       const SSLTranscript &transcript,
@@ -590,36 +598,16 @@
   return true;
 }
 
-bool tls13_verify_psk_binder(const SSL_HANDSHAKE *hs,
-                             const SSL_SESSION *session, const SSLMessage &msg,
-                             CBS *binders) {
-  uint8_t verify_data[EVP_MAX_MD_SIZE];
-  size_t verify_data_len;
-  CBS binder;
-  // The binders are computed over |msg| with |binders| and its u16 length
-  // prefix removed. The caller is assumed to have parsed |msg|, extracted
-  // |binders|, and verified the PSK extension is last.
-  if (!tls13_psk_binder(hs, verify_data, &verify_data_len,
-                        UpRef(const_cast<SSL_SESSION *>(session)),
-                        hs->transcript, msg.body, 2 + CBS_len(binders)) ||
-      // We only consider the first PSK, so compare against the first binder.
-      !CBS_get_u8_length_prefixed(binders, &binder)) {
-    OPENSSL_PUT_ERROR(SSL, ERR_R_INTERNAL_ERROR);
-    return false;
+static std::optional<uint16_t> hkdf_md_to_kdf_id(const EVP_MD *hkdf_md) {
+  // See Section 10 of RFC 9258.
+  switch (EVP_MD_nid(hkdf_md)) {
+    case NID_sha256:
+      return 0x0001;  // HKDF_SHA256
+    case NID_sha384:
+      return 0x0002;  // HKDF_SHA384
+    default:
+      return std::nullopt;
   }
-
-  bool binder_ok =
-      CBS_len(&binder) == verify_data_len &&
-      CRYPTO_memcmp(CBS_data(&binder), verify_data, verify_data_len) == 0;
-  if (CRYPTO_fuzzer_mode_enabled()) {
-    binder_ok = true;
-  }
-  if (!binder_ok) {
-    OPENSSL_PUT_ERROR(SSL, SSL_R_DIGEST_CHECK_FAILED);
-    return false;
-  }
-
-  return true;
 }
 
 std::optional<SSLImportedPSK> tls13_derive_imported_psk(
@@ -627,18 +615,10 @@
     const EVP_MD *hkdf_md) {
   assert(cred->type == SSLCredentialType::kPreSharedKey);
 
-  // See Section 10 of RFC 9258.
-  uint16_t target_kdf;
-  switch (EVP_MD_nid(hkdf_md)) {
-    case NID_sha256:
-      target_kdf = 0x0001;  // HKDF_SHA256
-      break;
-    case NID_sha384:
-      target_kdf = 0x0002;  // HKDF_SHA384
-      break;
-    default:
-      OPENSSL_PUT_ERROR(SSL, ERR_R_INTERNAL_ERROR);
-      return std::nullopt;
+  std::optional<uint16_t> target_kdf = hkdf_md_to_kdf_id(hkdf_md);
+  if (!target_kdf.has_value()) {
+    OPENSSL_PUT_ERROR(SSL, ERR_R_INTERNAL_ERROR);
+    return std::nullopt;
   }
 
   SSLImportedPSK ret;
@@ -658,7 +638,7 @@
       !CBB_add_bytes(&context, cred->epsk_context.data(),
                      cred->epsk_context.size()) ||
       !CBB_add_u16(imported_id.get(), protocol) ||
-      !CBB_add_u16(imported_id.get(), target_kdf) ||
+      !CBB_add_u16(imported_id.get(), *target_kdf) ||
       !CBBFinishArray(imported_id.get(), &ret.imported_identity)) {
     return std::nullopt;
   }
@@ -684,6 +664,28 @@
   return ret;
 }
 
+bool tls13_compare_imported_psk_identity(Span<const uint8_t> id,
+                                         const SSL_CREDENTIAL *cred,
+                                         uint16_t protocol,
+                                         const EVP_MD *hkdf_md) {
+  assert(cred->type == SSLCredentialType::kPreSharedKey);
+  std::optional<uint16_t> target_kdf = hkdf_md_to_kdf_id(hkdf_md);
+  if (!target_kdf.has_value()) {
+    return false;
+  }
+
+  // See Section 5.1 of RFC 9258.
+  CBS cbs = id, external_identity, context;
+  uint16_t found_protocol, found_kdf;
+  return CBS_get_u16_length_prefixed(&cbs, &external_identity) &&
+         external_identity == Span(cred->epsk_id) &&
+         CBS_get_u16_length_prefixed(&cbs, &context) &&
+         context == Span(cred->epsk_context) &&
+         CBS_get_u16(&cbs, &found_protocol) && found_protocol == protocol &&
+         CBS_get_u16(&cbs, &found_kdf) && found_kdf == *target_kdf &&
+         CBS_len(&cbs) == 0;
+}
+
 size_t ssl_ech_confirmation_signal_hello_offset(const SSL *ssl) {
   static_assert(ECH_CONFIRMATION_SIGNAL_LEN < SSL3_RANDOM_SIZE,
                 "the confirmation signal is a suffix of the random");
diff --git a/ssl/tls13_server.cc b/ssl/tls13_server.cc
index c365c04..4b6d840 100644
--- a/ssl/tls13_server.cc
+++ b/ssl/tls13_server.cc
@@ -307,15 +307,59 @@
   return true;
 }
 
+static bool check_psk_credential(SSL_HANDSHAKE *hs, const SSL_CREDENTIAL *cred,
+                                 const std::optional<SSLOfferedPSKs> &psks) {
+  assert(cred->type == SSLCredentialType::kPreSharedKey);
+  SSL *const ssl = hs->ssl;
+  if (!psks) {
+    OPENSSL_PUT_ERROR(SSL, SSL_R_MISSING_EXTENSION);
+    return false;
+  }
+  if (!hs->accept_psk_mode) {
+    OPENSSL_PUT_ERROR(SSL, SSL_R_NO_SUPPORTED_PSK_MODE);
+    return false;
+  }
+
+  // Look for a matching PSK.
+  const EVP_MD *md =
+      ssl_get_handshake_digest(ssl_protocol_version(ssl), hs->new_cipher);
+  SSLOfferedPSKs copy = *psks;
+  for (;;) {
+    std::optional<SSLOfferedPSK> psk = copy.Next();
+    if (!psk) {
+      OPENSSL_PUT_ERROR(SSL, SSL_R_PSK_IDENTITY_NOT_FOUND);
+      return false;
+    }
+    if (tls13_compare_imported_psk_identity(psk->identity, cred,
+                                            ssl->s3->version, md)) {
+      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.
+  // the common handshake logic. Resolve the remaining non-resumption
+  // parameters. First, parse out another copy of the ClientHello and important
+  // extensions.
   SSL *const ssl = hs->ssl;
   SSLMessage msg;
   SSL_CLIENT_HELLO client_hello;
   if (!hs->GetClientHello(&msg, &client_hello)) {
     return ssl_hs_error;
   }
+  std::optional<SSLOfferedPSKs> psks;
+  CBS psk_ext;
+  uint8_t alert = SSL_AD_DECODE_ERROR;
+  if (ssl_client_hello_get_extension(&client_hello, &psk_ext,
+                                     TLSEXT_TYPE_pre_shared_key)) {
+    psks = ssl_ext_pre_shared_key_parse_clienthello(hs, &alert, &client_hello,
+                                                    &psk_ext);
+    if (!psks) {
+      ssl_send_alert(ssl, SSL3_AL_FATAL, alert);
+      return ssl_hs_error;
+    }
+  }
 
   if (SSL_is_quic(ssl) && client_hello.session_id_len > 0) {
     OPENSSL_PUT_ERROR(SSL, SSL_R_UNEXPECTED_COMPATIBILITY_MODE);
@@ -331,6 +375,15 @@
         Span(client_hello.session_id, client_hello.session_id_len));
   }
 
+  // Negotiate the cipher suite. This must happen before negotiating PSKs.
+  hs->new_cipher = choose_tls13_cipher(ssl, &client_hello);
+  if (hs->new_cipher == nullptr) {
+    OPENSSL_PUT_ERROR(SSL, SSL_R_NO_SHARED_CIPHER);
+    ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_HANDSHAKE_FAILURE);
+    return ssl_hs_error;
+  }
+
+  // Select the credential to use.
   Array<SSL_CREDENTIAL *> creds;
   if (!ssl_get_full_credential_list(hs, &creds)) {
     return ssl_hs_error;
@@ -340,8 +393,6 @@
     ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_INTERNAL_ERROR);
     return ssl_hs_error;
   }
-
-  // Select the credential to use.
   for (SSL_CREDENTIAL *cred : creds) {
     ERR_clear_error();
     if (cred->type == SSLCredentialType::kSPAKE2PlusV1Server) {
@@ -358,6 +409,22 @@
         }
         break;
       }
+    } else if (cred->type == SSLCredentialType::kPreSharedKey) {
+      if (check_psk_credential(hs, cred, psks)) {
+        const EVP_MD *md =
+            ssl_get_handshake_digest(ssl_protocol_version(ssl), hs->new_cipher);
+        std::optional<SSLImportedPSK> imported =
+            tls13_derive_imported_psk(hs, cred, ssl->s3->version, md);
+        if (!imported) {
+          return ssl_hs_error;
+        }
+        hs->credential = UpRef(cred);
+        hs->pre_shared_key = MakeUnique<SSLPreSharedKey>(*std::move(imported));
+        if (hs->pre_shared_key == nullptr) {
+          return ssl_hs_error;
+        }
+        break;
+      }
     } else {
       uint16_t sigalg;
       if (check_signature_credential(hs, cred, &sigalg)) {
@@ -374,17 +441,8 @@
     return ssl_hs_error;
   }
 
-  // Negotiate the cipher suite.
-  hs->new_cipher = choose_tls13_cipher(ssl, &client_hello);
-  if (hs->new_cipher == nullptr) {
-    OPENSSL_PUT_ERROR(SSL, SSL_R_NO_SHARED_CIPHER);
-    ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_HANDSHAKE_FAILURE);
-    return ssl_hs_error;
-  }
-
   // HTTP/2 negotiation depends on the cipher suite, so ALPN negotiation was
   // deferred. Complete it now.
-  uint8_t alert = SSL_AD_DECODE_ERROR;
   if (!ssl_negotiate_alpn(hs, &alert, &client_hello)) {
     ssl_send_alert(ssl, SSL3_AL_FATAL, alert);
     return ssl_hs_error;
@@ -423,11 +481,9 @@
     return ssl_ticket_aead_error;
   }
 
-  CBS ticket, binders;
-  uint32_t client_ticket_age;
-  if (!ssl_ext_pre_shared_key_parse_clienthello(
-          hs, &ticket, &binders, &client_ticket_age, out_alert, client_hello,
-          &pre_shared_key)) {
+  std::optional<SSLOfferedPSKs> psks = ssl_ext_pre_shared_key_parse_clienthello(
+      hs, out_alert, client_hello, &pre_shared_key);
+  if (!psks) {
     return ssl_ticket_aead_error;
   }
 
@@ -442,12 +498,21 @@
     return ssl_ticket_aead_ignore_ticket;
   }
 
-  // TLS 1.3 session tickets are renewed separately as part of the
-  // NewSessionTicket.
+  // We only consider the first PSK for session resumption.
+  std::optional<SSLOfferedPSK> psk = psks->Next();
+  if (!psk) {
+    OPENSSL_PUT_ERROR(SSL, ERR_R_INTERNAL_ERROR);
+    return ssl_ticket_aead_error;
+  }
+
+  // Ignore whether the caller asked for TLS 1.2 ticket renewal. TLS 1.3 session
+  // tickets are renewed separately as part of the NewSessionTicket. Also save
+  // the ticket so we can find the PSK again on the second ClientHello.
   bool unused_renew;
   UniquePtr<SSL_SESSION> session;
   enum ssl_ticket_aead_result_t ret =
-      ssl_process_ticket(hs, &session, &unused_renew, ticket, {});
+      ssl_process_ticket(hs, &session, &unused_renew, psk->identity,
+                         /*session_id=*/{}, /*save_ticket=*/true);
   switch (ret) {
     case ssl_ticket_aead_success:
       break;
@@ -465,7 +530,8 @@
   }
 
   // Recover the client ticket age and convert to seconds.
-  client_ticket_age -= session->ticket_age_add;
+  uint32_t client_ticket_age =
+      psk->obfuscated_ticket_age - session->ticket_age_add;
   client_ticket_age /= 1000;
 
   OPENSSL_timeval now = ssl_ctx_get_current_time(ssl->ctx.get());
@@ -482,13 +548,6 @@
 
   *out_ticket_age_skew = static_cast<int32_t>(client_ticket_age) -
                          static_cast<int32_t>(server_ticket_age);
-
-  // Check the PSK binder.
-  if (!tls13_verify_psk_binder(hs, session.get(), msg, &binders)) {
-    *out_alert = SSL_AD_DECRYPT_ERROR;
-    return ssl_ticket_aead_error;
-  }
-
   *out_session = std::move(session);
   return ssl_ticket_aead_success;
 }
@@ -510,6 +569,10 @@
   return true;
 }
 
+static bool using_certificate(const SSL_HANDSHAKE *hs) {
+  return !hs->pre_shared_key && !hs->pake_verifier;
+}
+
 static enum ssl_hs_wait_t do_select_session(SSL_HANDSHAKE *hs) {
   SSL *const ssl = hs->ssl;
   SSLMessage msg;
@@ -541,12 +604,15 @@
         return ssl_hs_error;
       }
 
-      ssl->s3->session_reused = true;
-      hs->can_release_private_key = true;
-
       // Resumption incorporates fresh key material, so refresh the timeout.
       ssl_session_renew_timeout(ssl, hs->new_session.get(),
                                 ssl->session_ctx->session_psk_dhe_timeout);
+
+      ssl->s3->session_reused = true;
+      hs->pre_shared_key = MakeUnique<SSLPreSharedKey>(UpRef(session));
+      if (hs->pre_shared_key == nullptr) {
+        return ssl_hs_error;
+      }
       break;
 
     case ssl_ticket_aead_error:
@@ -558,6 +624,8 @@
       return ssl_hs_pending_ticket;
   }
 
+  hs->can_release_private_key = !using_certificate(hs);
+
   // Negotiate ALPS now, after ALPN is negotiated and |hs->new_session| is
   // initialized.
   if (!ssl_negotiate_alps(hs, &alert, &client_hello)) {
@@ -661,13 +729,20 @@
     return ssl_hs_error;
   }
 
+  if (hs->pre_shared_key &&
+      !ssl_verify_psk_binder(hs, &alert, *hs->pre_shared_key, client_hello)) {
+    ssl_send_alert(ssl, SSL3_AL_FATAL, alert);
+    return ssl_hs_error;
+  }
+
   size_t hash_len = EVP_MD_size(
       ssl_get_handshake_digest(ssl_protocol_version(ssl), hs->new_cipher));
 
   // Set up the key schedule and incorporate the PSK into the running secret.
-  if (!tls13_init_key_schedule(hs, ssl->s3->session_reused
-                                       ? Span(hs->new_session->secret)
-                                       : Span(kZeroes, hash_len)) ||
+  Span<const uint8_t> psk = hs->pre_shared_key
+                                ? ssl_pre_shared_key_secret(*hs->pre_shared_key)
+                                : Span(kZeroes, hash_len);
+  if (!tls13_init_key_schedule(hs, psk) ||  //
       !ssl_hash_message(hs, msg)) {
     return ssl_hs_error;
   }
@@ -851,33 +926,15 @@
   //
   // We do, however, check the second PSK binder. This covers the client key
   // share, in case we ever send half-RTT data (we currently do not). It is also
-  // a tricky computation, so we enforce the peer handled it correctly.
-  if (ssl->s3->session_reused) {
-    CBS pre_shared_key;
-    if (!ssl_client_hello_get_extension(&client_hello, &pre_shared_key,
-                                        TLSEXT_TYPE_pre_shared_key)) {
-      OPENSSL_PUT_ERROR(SSL, SSL_R_INCONSISTENT_CLIENT_HELLO);
-      ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_ILLEGAL_PARAMETER);
-      return ssl_hs_error;
-    }
-
-    CBS ticket, binders;
-    uint32_t client_ticket_age;
+  // a tricky computation, so we enforce the peer handled it correctly. It is
+  // also necessary to search the PSK list from the second ClientHello because
+  // PSK indices may have changed (RFC 8446 section 4.1.4).
+  if (hs->pre_shared_key) {
     uint8_t alert = SSL_AD_DECODE_ERROR;
-    if (!ssl_ext_pre_shared_key_parse_clienthello(
-            hs, &ticket, &binders, &client_ticket_age, &alert, &client_hello,
-            &pre_shared_key)) {
+    if (!ssl_verify_psk_binder(hs, &alert, *hs->pre_shared_key, client_hello)) {
       ssl_send_alert(ssl, SSL3_AL_FATAL, alert);
       return ssl_hs_error;
     }
-
-    // Note it is important that we do not obtain a new |SSL_SESSION| from
-    // |ticket|. We have already selected parameters based on the first
-    // ClientHello (in the transcript) and must not switch partway through.
-    if (!tls13_verify_psk_binder(hs, hs->new_session.get(), msg, &binders)) {
-      ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_DECRYPT_ERROR);
-      return ssl_hs_error;
-    }
   }
 
   // Although a server could HelloRetryRequest with PAKEs to request a cookie,
@@ -988,7 +1045,7 @@
     return ssl_hs_error;
   }
 
-  if (!ssl->s3->session_reused && !hs->pake_verifier) {
+  if (using_certificate(hs)) {
     // Determine whether to request a client certificate.
     hs->cert_request = !!(hs->config->verify_mode & SSL_VERIFY_PEER);
   }
@@ -1027,7 +1084,7 @@
   }
 
   // Send the server Certificate message, if necessary.
-  if (!ssl->s3->session_reused && !hs->pake_verifier) {
+  if (using_certificate(hs)) {
     if (!tls13_add_certificate(hs)) {
       return ssl_hs_error;
     }