Raw Public Keys: Process and verify received RPKs

This implements the parsing and verification of the peer's Raw Public
Key received in the Certificate payload for TLS 1.3 as described in RFC
8446, and for TLS 1.2 as described in RFC 7250.

Verification of an RPK is handled solely by custom_verify_callback. If
there isn't one set, the verification fails.

Also fills in test runner machinery for testing resumption with RPKs.

This completes the implementation of RPKs in TLS.

Bug: 467663225
Change-Id: I462b0cdd020904ffcdfe9b80bcca6d9e6a6a6964
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/92427
Reviewed-by: David Benjamin <davidben@google.com>
Commit-Queue: Lily Chen <chlily@google.com>
diff --git a/crypto/err/ssl.errordata b/crypto/err/ssl.errordata
index dea6df5..a2c591f 100644
--- a/crypto/err/ssl.errordata
+++ b/crypto/err/ssl.errordata
@@ -96,6 +96,7 @@
 SSL,320,INVALID_OUTER_EXTENSION
 SSL,251,INVALID_OUTER_RECORD_TYPE
 SSL,331,INVALID_PSK_FOR_CONNECTION
+SSL,336,INVALID_RAW_PUBLIC_KEY
 SSL,269,INVALID_SCT_LIST
 SSL,295,INVALID_SIGNATURE_ALGORITHM
 SSL,324,INVALID_SPAKE2PLUSV1_VALUE
diff --git a/gen/crypto/err_data.cc b/gen/crypto/err_data.cc
index 2fb0d38..a45514d 100644
--- a/gen/crypto/err_data.cc
+++ b/gen/crypto/err_data.cc
@@ -205,51 +205,51 @@
     0x283500f7,
     0x28358cc1,
     0x2836099a,
-    0x2c3234ce,
+    0x2c3234e5,
     0x2c32943b,
-    0x2c3334dc,
-    0x2c33b4ee,
-    0x2c343502,
-    0x2c34b514,
-    0x2c35352f,
-    0x2c35b541,
-    0x2c363571,
+    0x2c3334f3,
+    0x2c33b505,
+    0x2c343519,
+    0x2c34b52b,
+    0x2c353546,
+    0x2c35b558,
+    0x2c363588,
     0x2c36833a,
-    0x2c37357e,
-    0x2c37b5aa,
-    0x2c3835e8,
-    0x2c38b5ff,
-    0x2c39361d,
-    0x2c39b62d,
-    0x2c3a363f,
-    0x2c3ab653,
-    0x2c3b3664,
-    0x2c3bb683,
+    0x2c373595,
+    0x2c37b5c1,
+    0x2c3835ff,
+    0x2c38b616,
+    0x2c393634,
+    0x2c39b644,
+    0x2c3a3656,
+    0x2c3ab66a,
+    0x2c3b367b,
+    0x2c3bb69a,
     0x2c3c144d,
     0x2c3c9463,
-    0x2c3d36c8,
+    0x2c3d36df,
     0x2c3d947c,
-    0x2c3e36f2,
-    0x2c3eb700,
-    0x2c3f3718,
-    0x2c3fb730,
-    0x2c40375a,
+    0x2c3e3709,
+    0x2c3eb717,
+    0x2c3f372f,
+    0x2c3fb747,
+    0x2c403771,
     0x2c409330,
-    0x2c41376b,
-    0x2c41b77e,
+    0x2c413782,
+    0x2c41b795,
     0x2c4212f6,
-    0x2c42b78f,
+    0x2c42b7a6,
     0x2c43076d,
-    0x2c43b675,
-    0x2c4435bd,
-    0x2c44b73d,
-    0x2c453554,
-    0x2c45b590,
-    0x2c46360d,
-    0x2c46b697,
-    0x2c4736ac,
-    0x2c47b6e5,
-    0x2c4835cf,
+    0x2c43b68c,
+    0x2c4435d4,
+    0x2c44b754,
+    0x2c45356b,
+    0x2c45b5a7,
+    0x2c463624,
+    0x2c46b6ae,
+    0x2c4736c3,
+    0x2c47b6fc,
+    0x2c4835e6,
     0x30320000,
     0x30328015,
     0x3033001f,
@@ -449,156 +449,156 @@
     0x404ea166,
     0x404f2251,
     0x404fa2c7,
-    0x4050236c,
-    0x4050a380,
-    0x405123cd,
-    0x405223dd,
-    0x4052a40d,
-    0x40532425,
-    0x4053a438,
-    0x4054244d,
-    0x4054a470,
-    0x4055249b,
-    0x4055a4d8,
-    0x405624fd,
-    0x4056a516,
-    0x4057252e,
-    0x4057a541,
-    0x40582556,
-    0x4058a57d,
-    0x405925ac,
-    0x4059a5ec,
-    0x405aa600,
-    0x405b2618,
-    0x405ba629,
-    0x405c263c,
-    0x405ca691,
-    0x405d269e,
-    0x405da6c3,
-    0x405e2701,
+    0x40502383,
+    0x4050a397,
+    0x405123e4,
+    0x405223f4,
+    0x4052a424,
+    0x4053243c,
+    0x4053a44f,
+    0x40542464,
+    0x4054a487,
+    0x405524b2,
+    0x4055a4ef,
+    0x40562514,
+    0x4056a52d,
+    0x40572545,
+    0x4057a558,
+    0x4058256d,
+    0x4058a594,
+    0x405925c3,
+    0x4059a603,
+    0x405aa617,
+    0x405b262f,
+    0x405ba640,
+    0x405c2653,
+    0x405ca6a8,
+    0x405d26b5,
+    0x405da6da,
+    0x405e2718,
     0x405e8afe,
-    0x405f2750,
-    0x405fa75d,
-    0x4060276b,
-    0x4060a78d,
-    0x40612801,
-    0x4061a839,
-    0x40622850,
-    0x4062a861,
-    0x406328ae,
-    0x4063a8c3,
-    0x406428da,
-    0x4064a906,
-    0x40652921,
-    0x4065a938,
-    0x40662950,
-    0x4066a97a,
-    0x406729a5,
-    0x4067a9ea,
-    0x40682a32,
-    0x4068aa53,
-    0x40692a85,
-    0x4069aab3,
-    0x406a2ad4,
-    0x406aaaf4,
-    0x406b2c7c,
-    0x406bac9f,
-    0x406c2cb5,
-    0x406cafbf,
-    0x406d2fee,
-    0x406db016,
-    0x406e3044,
-    0x406eb091,
-    0x406f30ea,
-    0x406fb122,
-    0x40703135,
-    0x4070b152,
+    0x405f2767,
+    0x405fa774,
+    0x40602782,
+    0x4060a7a4,
+    0x40612818,
+    0x4061a850,
+    0x40622867,
+    0x4062a878,
+    0x406328c5,
+    0x4063a8da,
+    0x406428f1,
+    0x4064a91d,
+    0x40652938,
+    0x4065a94f,
+    0x40662967,
+    0x4066a991,
+    0x406729bc,
+    0x4067aa01,
+    0x40682a49,
+    0x4068aa6a,
+    0x40692a9c,
+    0x4069aaca,
+    0x406a2aeb,
+    0x406aab0b,
+    0x406b2c93,
+    0x406bacb6,
+    0x406c2ccc,
+    0x406cafd6,
+    0x406d3005,
+    0x406db02d,
+    0x406e305b,
+    0x406eb0a8,
+    0x406f3101,
+    0x406fb139,
+    0x4070314c,
+    0x4070b169,
     0x4071084d,
-    0x4071b164,
-    0x40723177,
-    0x4072b1ad,
-    0x407331c5,
+    0x4071b17b,
+    0x4072318e,
+    0x4072b1c4,
+    0x407331dc,
     0x40739665,
-    0x407431d9,
-    0x4074b1f3,
-    0x40753204,
-    0x4075b218,
-    0x40763226,
+    0x407431f0,
+    0x4074b20a,
+    0x4075321b,
+    0x4075b22f,
+    0x4076323d,
     0x407693f3,
-    0x40773263,
-    0x4077b2bf,
-    0x407832da,
-    0x4078b313,
-    0x4079332a,
-    0x4079b340,
-    0x407a336c,
-    0x407ab37f,
-    0x407b3394,
-    0x407bb3a6,
-    0x407c33d7,
-    0x407cb3e0,
-    0x407d2a6e,
+    0x4077327a,
+    0x4077b2d6,
+    0x407832f1,
+    0x4078b32a,
+    0x40793341,
+    0x4079b357,
+    0x407a3383,
+    0x407ab396,
+    0x407b33ab,
+    0x407bb3bd,
+    0x407c33ee,
+    0x407cb3f7,
+    0x407d2a85,
     0x407da2ef,
-    0x407e32ef,
-    0x407ea58d,
+    0x407e3306,
+    0x407ea5a4,
     0x407f1eca,
     0x407fa0ad,
     0x40802261,
     0x40809ef2,
-    0x408123fb,
+    0x40812412,
     0x4081a1b4,
-    0x4082302f,
+    0x40823046,
     0x40829c45,
-    0x40832568,
-    0x4083a8eb,
+    0x4083257f,
+    0x4083a902,
     0x40841f16,
-    0x4084a5c5,
-    0x4085264d,
-    0x4085a7c8,
-    0x408626e3,
-    0x4086a324,
-    0x40873075,
-    0x4087a816,
+    0x4084a5dc,
+    0x40852664,
+    0x4085a7df,
+    0x408626fa,
+    0x4086a33b,
+    0x4087308c,
+    0x4087a82d,
     0x40881c83,
-    0x4088a9fd,
+    0x4088aa14,
     0x40891cd2,
     0x40899c5f,
-    0x408a2ced,
+    0x408a2d04,
     0x408a9a7d,
-    0x408b33bb,
-    0x408bb0ff,
-    0x408c2673,
+    0x408b33d2,
+    0x408bb116,
+    0x408c268a,
     0x408d1ffe,
     0x408d9f48,
     0x408e212e,
-    0x408ea4b8,
-    0x408f2a11,
-    0x408fa7e4,
-    0x409029c6,
-    0x4090a6b5,
-    0x40912cd5,
+    0x408ea4cf,
+    0x408f2a28,
+    0x408fa7fb,
+    0x409029dd,
+    0x4090a6cc,
+    0x40912cec,
     0x40919ab5,
     0x40921d1f,
-    0x4092b0b0,
-    0x40933190,
-    0x4093a335,
+    0x4092b0c7,
+    0x409331a7,
+    0x4093a34c,
     0x40941f2a,
-    0x4094ad06,
-    0x40952872,
-    0x4095b34c,
-    0x4096305c,
+    0x4094ad1d,
+    0x40952889,
+    0x4095b363,
+    0x40963073,
     0x4096a27a,
-    0x409723b5,
+    0x409723cc,
     0x4097a17d,
     0x40981d7f,
-    0x4098a886,
-    0x409930cc,
-    0x4099a4e5,
-    0x409a247e,
+    0x4098a89d,
+    0x409930e3,
+    0x4099a4fc,
+    0x409a2495,
     0x409a9a99,
     0x409b1f84,
     0x409b9faf,
-    0x409c32a1,
+    0x409c32b8,
     0x409c9fd7,
     0x409d2236,
     0x409da1ca,
@@ -609,54 +609,55 @@
     0x40a022d7,
     0x40a0a197,
     0x40a121e5,
-    0x40a1a5d9,
-    0x40a22351,
-    0x40a2a741,
-    0x40a327b5,
-    0x40a3b285,
-    0x40a4239b,
+    0x40a1a5f0,
+    0x40a22368,
+    0x40a2a758,
+    0x40a327cc,
+    0x40a3b29c,
+    0x40a423b2,
     0x40a4a1fc,
     0x40a51f06,
     0x40a5a309,
-    0x40a6265d,
+    0x40a62674,
     0x40a6a21e,
-    0x40a7324b,
-    0x40a7a3ef,
-    0x41f42ba7,
-    0x41f92c39,
-    0x41fe2b2c,
-    0x41feade2,
-    0x41ff2f10,
-    0x42032bc0,
-    0x42082be2,
-    0x4208ac1e,
-    0x42092b10,
-    0x4209ac58,
-    0x420a2b67,
-    0x420aab47,
-    0x420b2b87,
-    0x420bac00,
-    0x420c2f2c,
-    0x420cad16,
-    0x420d2dc9,
-    0x420dae00,
-    0x42122e33,
-    0x42172ef3,
-    0x4217ae75,
-    0x421c2e97,
-    0x421f2e52,
-    0x42212fa4,
-    0x42262ed6,
-    0x422b2f82,
-    0x422bada4,
-    0x422c2f64,
-    0x422cad57,
-    0x422d2d30,
-    0x422daf43,
-    0x422e2d83,
-    0x42302eb2,
-    0x4230ae1a,
-    0x42312722,
+    0x40a73262,
+    0x40a7a406,
+    0x40a82324,
+    0x41f42bbe,
+    0x41f92c50,
+    0x41fe2b43,
+    0x41feadf9,
+    0x41ff2f27,
+    0x42032bd7,
+    0x42082bf9,
+    0x4208ac35,
+    0x42092b27,
+    0x4209ac6f,
+    0x420a2b7e,
+    0x420aab5e,
+    0x420b2b9e,
+    0x420bac17,
+    0x420c2f43,
+    0x420cad2d,
+    0x420d2de0,
+    0x420dae17,
+    0x42122e4a,
+    0x42172f0a,
+    0x4217ae8c,
+    0x421c2eae,
+    0x421f2e69,
+    0x42212fbb,
+    0x42262eed,
+    0x422b2f99,
+    0x422badbb,
+    0x422c2f7b,
+    0x422cad6e,
+    0x422d2d47,
+    0x422daf5a,
+    0x422e2d9a,
+    0x42302ec9,
+    0x4230ae31,
+    0x42312739,
     0x44320778,
     0x44328787,
     0x44330793,
@@ -712,71 +713,71 @@
     0x4c419545,
     0x4c4216ae,
     0x4c42948d,
-    0x503237a1,
-    0x5032b7b0,
-    0x503337bb,
-    0x5033b7cb,
-    0x503437e4,
-    0x5034b7fe,
-    0x5035380c,
-    0x5035b822,
-    0x50363834,
-    0x5036b84a,
-    0x50373863,
-    0x5037b876,
-    0x5038388e,
-    0x5038b89f,
-    0x503938b4,
-    0x5039b8c8,
-    0x503a38e8,
-    0x503ab8fe,
-    0x503b3916,
-    0x503bb928,
-    0x503c3944,
-    0x503cb95b,
-    0x503d3974,
-    0x503db98a,
-    0x503e3997,
-    0x503eb9ad,
-    0x503f39bf,
+    0x503237b8,
+    0x5032b7c7,
+    0x503337d2,
+    0x5033b7e2,
+    0x503437fb,
+    0x5034b815,
+    0x50353823,
+    0x5035b839,
+    0x5036384b,
+    0x5036b861,
+    0x5037387a,
+    0x5037b88d,
+    0x503838a5,
+    0x5038b8b6,
+    0x503938cb,
+    0x5039b8df,
+    0x503a38ff,
+    0x503ab915,
+    0x503b392d,
+    0x503bb93f,
+    0x503c395b,
+    0x503cb972,
+    0x503d398b,
+    0x503db9a1,
+    0x503e39ae,
+    0x503eb9c4,
+    0x503f39d6,
     0x503f83b3,
-    0x504039d2,
-    0x5040b9e2,
-    0x504139fc,
-    0x5041ba0b,
-    0x50423a25,
-    0x5042ba42,
-    0x50433a52,
-    0x5043ba62,
-    0x50443a7f,
+    0x504039e9,
+    0x5040b9f9,
+    0x50413a13,
+    0x5041ba22,
+    0x50423a3c,
+    0x5042ba59,
+    0x50433a69,
+    0x5043ba79,
+    0x50443a96,
     0x50448469,
-    0x50453a93,
-    0x5045bab1,
-    0x50463ac4,
-    0x5046bada,
-    0x50473aec,
-    0x5047bb01,
-    0x50483b27,
-    0x5048bb35,
-    0x50493b48,
-    0x5049bb5d,
-    0x504a3b73,
-    0x504abb83,
-    0x504b3ba3,
-    0x504bbbb6,
-    0x504c3bd9,
-    0x504cbc07,
-    0x504d3c34,
-    0x504dbc51,
-    0x504e3c6c,
-    0x504ebc88,
-    0x504f3c9a,
-    0x504fbcb1,
-    0x50503cc0,
+    0x50453aaa,
+    0x5045bac8,
+    0x50463adb,
+    0x5046baf1,
+    0x50473b03,
+    0x5047bb18,
+    0x50483b3e,
+    0x5048bb4c,
+    0x50493b5f,
+    0x5049bb74,
+    0x504a3b8a,
+    0x504abb9a,
+    0x504b3bba,
+    0x504bbbcd,
+    0x504c3bf0,
+    0x504cbc1e,
+    0x504d3c4b,
+    0x504dbc68,
+    0x504e3c83,
+    0x504ebc9f,
+    0x504f3cb1,
+    0x504fbcc8,
+    0x50503cd7,
     0x50508729,
-    0x50513cd3,
-    0x5051ba71,
-    0x50523c19,
+    0x50513cea,
+    0x5051ba88,
+    0x50523c30,
     0x58321011,
     0x68320fd3,
     0x68328d2b,
@@ -821,19 +822,19 @@
     0x7c32130c,
     0x80321558,
     0x80328090,
-    0x8033349d,
+    0x803334b4,
     0x803380b9,
-    0x803434ac,
-    0x8034b414,
-    0x80353432,
-    0x8035b4c0,
-    0x80363474,
-    0x8036b423,
-    0x80373466,
-    0x8037b401,
-    0x80383487,
-    0x8038b443,
-    0x80393458,
+    0x803434c3,
+    0x8034b42b,
+    0x80353449,
+    0x8035b4d7,
+    0x8036348b,
+    0x8036b43a,
+    0x8037347d,
+    0x8037b418,
+    0x8038349e,
+    0x8038b45a,
+    0x8039346f,
     0x84320bb0,
     0x84328bc9,
 };
@@ -1279,6 +1280,7 @@
     "INVALID_OUTER_EXTENSION\0"
     "INVALID_OUTER_RECORD_TYPE\0"
     "INVALID_PSK_FOR_CONNECTION\0"
+    "INVALID_RAW_PUBLIC_KEY\0"
     "INVALID_SCT_LIST\0"
     "INVALID_SIGNATURE_ALGORITHM\0"
     "INVALID_SPAKE2PLUSV1_VALUE\0"
diff --git a/include/openssl/prefix_symbols.h b/include/openssl/prefix_symbols.h
index ec54b36..7c922f0 100644
--- a/include/openssl/prefix_symbols.h
+++ b/include/openssl/prefix_symbols.h
@@ -2156,6 +2156,7 @@
 #pragma redefine_extname SSL_get0_peer_available_trust_anchors BORINGSSL_ADD_USER_LABEL_AND_PREFIX(SSL_get0_peer_available_trust_anchors)
 #pragma redefine_extname SSL_get0_peer_certificates BORINGSSL_ADD_USER_LABEL_AND_PREFIX(SSL_get0_peer_certificates)
 #pragma redefine_extname SSL_get0_peer_delegation_algorithms BORINGSSL_ADD_USER_LABEL_AND_PREFIX(SSL_get0_peer_delegation_algorithms)
+#pragma redefine_extname SSL_get0_peer_rpk BORINGSSL_ADD_USER_LABEL_AND_PREFIX(SSL_get0_peer_rpk)
 #pragma redefine_extname SSL_get0_peer_verify_algorithms BORINGSSL_ADD_USER_LABEL_AND_PREFIX(SSL_get0_peer_verify_algorithms)
 #pragma redefine_extname SSL_get0_selected_credential BORINGSSL_ADD_USER_LABEL_AND_PREFIX(SSL_get0_selected_credential)
 #pragma redefine_extname SSL_get0_server_requested_CAs BORINGSSL_ADD_USER_LABEL_AND_PREFIX(SSL_get0_server_requested_CAs)
@@ -5249,6 +5250,7 @@
 #define SSL_get0_peer_available_trust_anchors BORINGSSL_ADD_PREFIX(SSL_get0_peer_available_trust_anchors)
 #define SSL_get0_peer_certificates BORINGSSL_ADD_PREFIX(SSL_get0_peer_certificates)
 #define SSL_get0_peer_delegation_algorithms BORINGSSL_ADD_PREFIX(SSL_get0_peer_delegation_algorithms)
+#define SSL_get0_peer_rpk BORINGSSL_ADD_PREFIX(SSL_get0_peer_rpk)
 #define SSL_get0_peer_verify_algorithms BORINGSSL_ADD_PREFIX(SSL_get0_peer_verify_algorithms)
 #define SSL_get0_selected_credential BORINGSSL_ADD_PREFIX(SSL_get0_selected_credential)
 #define SSL_get0_server_requested_CAs BORINGSSL_ADD_PREFIX(SSL_get0_server_requested_CAs)
diff --git a/include/openssl/ssl.h b/include/openssl/ssl.h
index c45829c..6cff875 100644
--- a/include/openssl/ssl.h
+++ b/include/openssl/ssl.h
@@ -3897,9 +3897,10 @@
 // When raw public keys are in use, the client_certificate_type and
 // server_certificate_type extensions are sent in the handshake to indicate to
 // the peer which type(s) of certificate(s) can be exchanged.
-
-// TODO(crbug.com/467663225): Implementation is not yet complete. The values
-// configured via functions in this section may not currently be used.
+//
+// To verify a received raw public key, the caller must set a custom
+// verification callback (see |SSL_CTX_set_custom_verify|). If no callback is
+// configured, raw public keys will be rejected by default.
 
 // TLSEXT_cert_type_* are certificate types with values taken from the "TLS
 // Certificate Types" subregistry of the TLS Extensions registry.
@@ -3983,6 +3984,9 @@
 // |TLSEXT_cert_type_x509|.
 OPENSSL_EXPORT int SSL_get_peer_cert_type(const SSL *ssl);
 
+// SSL_get0_peer_rpk returns the peer's raw public key from |ssl|, if the peer
+// has sent one in the handshake. It returns nullptr otherwise.
+OPENSSL_EXPORT EVP_PKEY *SSL_get0_peer_rpk(const SSL *ssl);
 
 // Password Authenticated Key Exchange (PAKE).
 //
@@ -6957,6 +6961,7 @@
 #define SSL_R_INVALID_CERT_TYPES_LIST 333
 #define SSL_R_UNSUPPORTED_CERTIFICATE 334
 #define SSL_R_MISSING_KEY 335
+#define SSL_R_INVALID_RAW_PUBLIC_KEY 336
 #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 74c901c..9eddd24 100644
--- a/ssl/extensions.cc
+++ b/ssl/extensions.cc
@@ -3901,7 +3901,22 @@
 static bool ext_server_cert_type_parse_serverhello(SSL_HANDSHAKE *hs,
                                                    uint8_t *out_alert,
                                                    CBS *contents) {
-  // TODO(crbug.com/467663225): Implement this.
+  assert(!hs->config->accepted_peer_cert_types.empty());
+  // Absence of an extension from the server implies the default, X.509.
+  uint8_t cert_type = kDefaultCertType;
+  if (contents != nullptr &&
+      (!CBS_get_u8(contents, &cert_type) || CBS_len(contents) != 0)) {
+    return false;
+  }
+  // Check that the server's chosen type is among the types we accept.
+  if (std::find(hs->config->accepted_peer_cert_types.begin(),
+                hs->config->accepted_peer_cert_types.end(),
+                cert_type) == hs->config->accepted_peer_cert_types.end()) {
+    OPENSSL_PUT_ERROR(SSL, SSL_R_UNSUPPORTED_CERTIFICATE);
+    *out_alert = SSL_AD_UNSUPPORTED_CERTIFICATE;
+    return false;
+  }
+  hs->peer_cert_type = cert_type;
   return true;
 }
 
diff --git a/ssl/handshake_client.cc b/ssl/handshake_client.cc
index 62471ba..31cb867 100644
--- a/ssl/handshake_client.cc
+++ b/ssl/handshake_client.cc
@@ -860,8 +860,21 @@
 
   CBS body = msg.body;
   uint8_t alert = SSL_AD_DECODE_ERROR;
-  if (!ssl_parse_cert_chain(&alert, &hs->new_session->certs, &hs->peer_pubkey,
-                            nullptr, &body, ssl->ctx->pool)) {
+
+  bool parse_ok;
+  if (hs->peer_cert_type == TLSEXT_cert_type_rpk) {
+    hs->new_session->peer_cert_type = TLSEXT_cert_type_rpk;
+    parse_ok = ssl_parse_rpk_cert(&alert, &hs->new_session->peer_raw_public_key,
+                                  &hs->peer_pubkey, nullptr, &body);
+  } else {
+    assert(hs->new_session->peer_cert_type == TLSEXT_cert_type_x509);
+    hs->new_session->peer_cert_type = TLSEXT_cert_type_x509;
+    parse_ok =
+        ssl_parse_cert_chain(&alert, &hs->new_session->certs, &hs->peer_pubkey,
+                             nullptr, &body, ssl->ctx->pool);
+  }
+
+  if (!parse_ok) {
     ssl_send_alert(ssl, SSL3_AL_FATAL, alert);
     return ssl_hs_error;
   }
@@ -1380,7 +1393,8 @@
   Array<uint8_t> pms;
   uint32_t alg_k = hs->new_cipher->algorithm_mkey;
   uint32_t alg_a = hs->new_cipher->algorithm_auth;
-  if (ssl_cipher_uses_certificate_auth(hs->new_cipher)) {
+  if (ssl_cipher_uses_certificate_auth(hs->new_cipher) &&
+      hs->new_session->peer_cert_type == TLSEXT_cert_type_x509) {
     const CRYPTO_BUFFER *leaf =
         sk_CRYPTO_BUFFER_value(hs->new_session->certs.get(), 0);
     CBS leaf_cbs;
@@ -1391,6 +1405,8 @@
     // certificates, which we do not support, from ECDSA certificates.
     // Historically, we have not checked RSA key usages, so it is controlled by
     // a flag for now. See https://crbug.com/795089.
+    // Key usage is only checked for X.509 certs. (RPKs have no keyUsage to
+    // enforce.)
     ssl_key_usage_t intended_use = (alg_k & SSL_kRSA)
                                        ? key_usage_encipherment
                                        : key_usage_digital_signature;
diff --git a/ssl/handshake_server.cc b/ssl/handshake_server.cc
index 8f3ac37..1129d11 100644
--- a/ssl/handshake_server.cc
+++ b/ssl/handshake_server.cc
@@ -1235,11 +1235,25 @@
 
   CBS certificate_msg = msg.body;
   uint8_t alert = SSL_AD_DECODE_ERROR;
-  if (!ssl_parse_cert_chain(&alert, &hs->new_session->certs, &hs->peer_pubkey,
-                            hs->config->retain_only_sha256_of_client_certs
-                                ? hs->new_session->peer_sha256
-                                : nullptr,
-                            &certificate_msg, ssl->ctx->pool)) {
+
+  bool parse_ok;
+  uint8_t *const out_sha256 = hs->config->retain_only_sha256_of_client_certs
+                                  ? hs->new_session->peer_sha256
+                                  : nullptr;
+  if (hs->peer_cert_type == TLSEXT_cert_type_rpk) {
+    hs->new_session->peer_cert_type = TLSEXT_cert_type_rpk;
+    parse_ok =
+        ssl_parse_rpk_cert(&alert, &hs->new_session->peer_raw_public_key,
+                           &hs->peer_pubkey, out_sha256, &certificate_msg);
+  } else {
+    assert(hs->peer_cert_type == TLSEXT_cert_type_x509);
+    hs->new_session->peer_cert_type = TLSEXT_cert_type_x509;
+    parse_ok =
+        ssl_parse_cert_chain(&alert, &hs->new_session->certs, &hs->peer_pubkey,
+                             out_sha256, &certificate_msg, ssl->ctx->pool);
+  }
+
+  if (!parse_ok) {
     ssl_send_alert(ssl, SSL3_AL_FATAL, alert);
     return ssl_hs_error;
   }
@@ -1276,7 +1290,7 @@
 }
 
 static enum ssl_hs_wait_t do_verify_client_certificate(SSL_HANDSHAKE *hs) {
-  if (sk_CRYPTO_BUFFER_num(hs->new_session->certs.get()) > 0) {
+  if (ssl_session_has_peer_cred(hs->new_session.get())) {
     switch (ssl_verify_peer_cert(hs)) {
       case ssl_verify_ok:
         break;
@@ -1526,13 +1540,16 @@
     return ssl_hs_error;
   }
 
-  // The peer certificate must be valid for signing.
-  const CRYPTO_BUFFER *leaf =
-      sk_CRYPTO_BUFFER_value(hs->new_session->certs.get(), 0);
-  CBS leaf_cbs;
-  CRYPTO_BUFFER_init_CBS(leaf, &leaf_cbs);
-  if (!ssl_cert_check_key_usage(&leaf_cbs, key_usage_digital_signature)) {
-    return ssl_hs_error;
+  // The X.509 peer certificate must be valid for signing. (RPKs have no
+  // keyUsage to enforce.)
+  if (hs->new_session->peer_cert_type == TLSEXT_cert_type_x509) {
+    const CRYPTO_BUFFER *leaf =
+        sk_CRYPTO_BUFFER_value(hs->new_session->certs.get(), 0);
+    CBS leaf_cbs;
+    CRYPTO_BUFFER_init_CBS(leaf, &leaf_cbs);
+    if (!ssl_cert_check_key_usage(&leaf_cbs, key_usage_digital_signature)) {
+      return ssl_hs_error;
+    }
   }
 
   CBS certificate_verify = msg.body, signature;
diff --git a/ssl/internal.h b/ssl/internal.h
index 5880da1..2fbe2d0 100644
--- a/ssl/internal.h
+++ b/ssl/internal.h
@@ -1090,6 +1090,18 @@
                           uint8_t *out_leaf_sha256, CBS *cbs,
                           CRYPTO_BUFFER_POOL *pool);
 
+// ssl_parse_rpk_cert parses a RawPublicKey certificate from |cbs| in the format
+// used by a TLS 1.2 Certificate message (RFC 7250). On success, it advances
+// |cbs| and returns true, and sets |*out_raw_public_key| to the parsed key, and
+// sets |*out_pubkey| to the same key by incrementing the reference count, and
+// if |out_rpk_sha256| is non-NULL, it writes the SHA-256 hash of the RPK to
+// |out_rpk_sha256|. Otherwise, it returns false and sets |*out_alert| to an
+// alert to send to the peer.
+bool ssl_parse_rpk_cert(uint8_t *out_alert,
+                        UniquePtr<EVP_PKEY> *out_raw_public_key,
+                        UniquePtr<EVP_PKEY> *out_pubkey,
+                        uint8_t *out_rpk_sha256, CBS *cbs);
+
 enum ssl_key_usage_t {
   key_usage_digital_signature = 0,
   key_usage_encipherment = 2,
@@ -4408,7 +4420,6 @@
 
   // peer_raw_public_key, if non-null, is the raw public key received from the
   // peer. This must be null if `certs` is non-null.
-  // TODO(crbug.com/467663225): Populate this field.
   bssl::UniquePtr<EVP_PKEY> peer_raw_public_key;
 
  private:
diff --git a/ssl/ssl_cert.cc b/ssl/ssl_cert.cc
index 0cece5d..2330a95 100644
--- a/ssl/ssl_cert.cc
+++ b/ssl/ssl_cert.cc
@@ -183,6 +183,40 @@
   return true;
 }
 
+bool ssl_parse_rpk_cert(uint8_t *out_alert,
+                        UniquePtr<EVP_PKEY> *out_raw_public_key,
+                        UniquePtr<EVP_PKEY> *out_pubkey,
+                        uint8_t *out_rpk_sha256, CBS *cbs) {
+  out_raw_public_key->reset();
+  out_pubkey->reset();
+  CBS spki;
+  if (!CBS_get_u24_length_prefixed(cbs, &spki) ||  //
+      CBS_len(cbs) != 0) {
+    OPENSSL_PUT_ERROR(SSL, SSL_R_INVALID_RAW_PUBLIC_KEY);
+    *out_alert = SSL_AD_DECODE_ERROR;
+    return false;
+  }
+  // The TLS 1.2 Certificate format for Raw Public Keys in RFC 7250 does not
+  // permit the peer to decline to send a Certificate, which is possible to do
+  // with X.509 Certificates, but we allow a client to do this by sending an
+  // empty RPK SPKI.
+  if (CBS_len(&spki) == 0) {
+    return true;
+  }
+  *out_raw_public_key = ssl_parse_peer_subject_public_key_info(spki);
+  if (*out_raw_public_key == nullptr) {
+    OPENSSL_PUT_ERROR(SSL, SSL_R_INVALID_RAW_PUBLIC_KEY);
+    *out_alert = SSL_AD_DECODE_ERROR;
+    return false;
+  }
+  // Retain the hash of the leaf certificate if requested.
+  if (out_rpk_sha256 != nullptr) {
+    SHA256(CBS_data(&spki), CBS_len(&spki), out_rpk_sha256);
+  }
+  *out_pubkey = UpRef(*out_raw_public_key);
+  return true;
+}
+
 // ssl_cert_skip_to_spki parses a DER-encoded, X.509 certificate from |in| and
 // positions |*out_tbs_cert| to cover the TBSCertificate, starting at the
 // subjectPublicKeyInfo.
diff --git a/ssl/ssl_lib.cc b/ssl/ssl_lib.cc
index be46b95..e3c42a9 100644
--- a/ssl/ssl_lib.cc
+++ b/ssl/ssl_lib.cc
@@ -3736,3 +3736,10 @@
   }
   return kDefaultCertType;
 }
+
+EVP_PKEY *SSL_get0_peer_rpk(const SSL *ssl) {
+  if (const SSL_SESSION *session = SSL_get_session(ssl); session != nullptr) {
+    return session->peer_raw_public_key.get();
+  }
+  return nullptr;
+}
diff --git a/ssl/test/bssl_shim.cc b/ssl/test/bssl_shim.cc
index ea95e3e..b0873ea 100644
--- a/ssl/test/bssl_shim.cc
+++ b/ssl/test/bssl_shim.cc
@@ -670,7 +670,7 @@
   }
 
   if (config->expect_no_peer_cert || !config->psk.empty() || IsTLS13PSK(ssl) ||
-      IsPAKE(ssl)) {
+      IsPAKE(ssl) || !config->expect_peer_rpk_sha256.empty()) {
     if (SSL_get_peer_cert_chain(ssl) != nullptr) {
       fprintf(stderr, "Received unexpected peer certificate.\n");
       return false;
diff --git a/ssl/test/runner/common.go b/ssl/test/runner/common.go
index 093e04d..2d17483 100644
--- a/ssl/test/runner/common.go
+++ b/ssl/test/runner/common.go
@@ -453,6 +453,7 @@
 	localApplicationSettingsOld []byte
 	peerApplicationSettingsOld  []byte
 	resumptionAcrossNames       bool
+	serverRawPublicKey          []byte
 }
 
 // ClientSessionCache is a cache of ClientSessionState objects that can be used
@@ -1531,8 +1532,9 @@
 	// advertise all configured cipher suite values.
 	AdvertiseAllConfiguredCiphers bool
 
-	// EmptyCertificateList, if true, causes the server to send an empty
-	// certificate list in the Certificate message.
+	// EmptyCertificateList, if true, causes the server or client to send an empty
+	// certificate list in the Certificate message. For a TLS 1.2 RawPublicKey
+	// Certificate (RFC 7250), this causes the SubjectPublicKeyInfo to be empty.
 	EmptyCertificateList bool
 
 	// ExpectNewTicket, if true, causes the client to abort if it does not
@@ -2406,11 +2408,24 @@
 	CredentialTypeRawPublicKey
 )
 
+func (c CredentialType) CertificateType() CertificateType {
+	switch c {
+	case CredentialTypeX509, CredentialTypeDelegated:
+		return certTypeX509
+	case CredentialTypeRawPublicKey:
+		return certTypeRawPublicKey
+	default:
+		panic("Unexpected credential type")
+	}
+}
+
 // A Credential is a certificate chain and private key that a TLS endpoint may
 // use to authenticate.
 type Credential struct {
 	Type CredentialType
 	// Certificate is a chain of one or more certificates, leaf first.
+	// For a RawPublicKey credential, this contains exactly one element, which
+	// holds the SubjectPublicKeyInfo data of the raw public key.
 	Certificate [][]byte
 	// RootCertificate is the certificate that issued this chain.
 	RootCertificate []byte
diff --git a/ssl/test/runner/conn.go b/ssl/test/runner/conn.go
index 7e5914d..4a1c11b 100644
--- a/ssl/test/runner/conn.go
+++ b/ssl/test/runner/conn.go
@@ -1648,6 +1648,7 @@
 		localApplicationSettingsOld: c.localApplicationSettingsOld,
 		peerApplicationSettingsOld:  c.peerApplicationSettingsOld,
 		resumptionAcrossNames:       newSessionTicket.flags.hasFlag(flagResumptionAcrossNames),
+		serverRawPublicKey:          c.peerRawPublicKey,
 	}
 
 	if c.config.Bugs.ExpectGREASE && !newSessionTicket.hasGREASEExtension {
@@ -2093,6 +2094,7 @@
 		hasApplicationSettingsOld:   c.hasApplicationSettingsOld,
 		localApplicationSettingsOld: c.localApplicationSettingsOld,
 		peerApplicationSettingsOld:  c.peerApplicationSettingsOld,
+		peerRawPublicKey:            c.peerRawPublicKey,
 	}
 
 	if !c.config.Bugs.SendEmptySessionTicket {
diff --git a/ssl/test/runner/handshake_client.go b/ssl/test/runner/handshake_client.go
index 007bd84..4f26321 100644
--- a/ssl/test/runner/handshake_client.go
+++ b/ssl/test/runner/handshake_client.go
@@ -408,6 +408,11 @@
 			return err
 		}
 
+		err = hs.processServerExtensionsAfterResume(&hs.serverHello.extensions, isResume)
+		if err != nil {
+			return err
+		}
+
 		if isResume {
 			if c.config.Bugs.EarlyChangeCipherSpec == 0 {
 				if err := hs.establishKeys(); err != nil {
@@ -1255,6 +1260,11 @@
 		return err
 	}
 
+	err = hs.processServerExtensionsAfterResume(&encryptedExtensions.extensions, c.didResume)
+	if err != nil {
+		return err
+	}
+
 	var credential *Credential
 	var certReq *certificateRequestMsg
 	if c.didResume {
@@ -1262,6 +1272,7 @@
 		c.peerCertificates = hs.session.serverCertificates
 		c.sctList = hs.session.sctList
 		c.ocspResponse = hs.session.ocspResponse
+		c.peerRawPublicKey = hs.session.serverRawPublicKey
 	} else if hs.pakeContext == nil && psk == nil {
 		msg, err := c.readHandshake()
 		if err != nil {
@@ -1496,11 +1507,14 @@
 			requestContext:    certReq.requestContext,
 		}
 		if credential != nil {
-			for _, certData := range credential.Certificate {
-				certMsg.certificates = append(certMsg.certificates, certificateEntry{
-					data:           certData,
-					extraExtension: c.config.Bugs.SendExtensionOnCertificate,
-				})
+			certMsg.certificateType = credential.Type.CertificateType()
+			if !c.config.Bugs.EmptyCertificateList {
+				for _, certData := range credential.Certificate {
+					certMsg.certificates = append(certMsg.certificates, certificateEntry{
+						data:           certData,
+						extraExtension: c.config.Bugs.SendExtensionOnCertificate,
+					})
+				}
 			}
 		}
 		hs.writeClientHash(certMsg.marshal())
@@ -1779,10 +1793,13 @@
 	if certRequested && !c.config.Bugs.SkipClientCertificate {
 		certMsg := new(certificateMsg)
 		if credential != nil {
-			for _, certData := range credential.Certificate {
-				certMsg.certificates = append(certMsg.certificates, certificateEntry{
-					data: certData,
-				})
+			certMsg.certificateType = credential.Type.CertificateType()
+			if !c.config.Bugs.EmptyCertificateList {
+				for _, certData := range credential.Certificate {
+					certMsg.certificates = append(certMsg.certificates, certificateEntry{
+						data: certData,
+					})
+				}
 			}
 		}
 		hs.writeClientHash(certMsg.marshal())
@@ -2190,18 +2207,6 @@
 		c.peerApplicationSettingsOld = hs.session.peerApplicationSettingsOld
 	}
 
-	if expected := c.config.Bugs.ExpectClientCertificateTypes; expected != nil {
-		if len(expected) > 1 {
-			panic("Expected client_certificate_type must not contain more than 1 value.")
-		}
-		var found []CertificateType
-		if serverExtensions.clientCertificateType != nil {
-			found = []CertificateType{*serverExtensions.clientCertificateType}
-		}
-		if !slices.Equal(found, expected) {
-			return fmt.Errorf("tls: server sent client certificate type %v, but expected %v", found, expected)
-		}
-	}
 	if expected := c.config.Bugs.ExpectServerCertificateTypes; expected != nil {
 		if len(expected) > 1 {
 			panic("Expected server_certificate_type must not contain more than 1 value.")
@@ -2214,12 +2219,35 @@
 			return fmt.Errorf("tls: server sent server certificate type %v, but expected %v", found, expected)
 		}
 	}
-	c.clientCertificateType = serverExtensions.clientCertificateType
 	c.serverCertificateType = serverExtensions.serverCertificateType
 
 	return nil
 }
 
+func (hs *clientHandshakeState) processServerExtensionsAfterResume(serverExtensions *serverExtensions, isResume bool) error {
+	c := hs.c
+
+	if expected := c.config.Bugs.ExpectClientCertificateTypes; expected != nil {
+		if len(expected) > 1 {
+			panic("Expected client_certificate_type must not contain more than 1 value.")
+		}
+		var found []CertificateType
+		if serverExtensions.clientCertificateType != nil {
+			found = []CertificateType{*serverExtensions.clientCertificateType}
+		}
+		// The server should not request a client cert if resuming.
+		if isResume && len(found) > 0 {
+			return errors.New("tls: server sent client_certificate_type on resumption")
+		}
+		if !isResume && !slices.Equal(found, expected) {
+			return fmt.Errorf("tls: server sent client certificate type %v, but expected %v", found, expected)
+		}
+	}
+	c.clientCertificateType = serverExtensions.clientCertificateType
+
+	return nil
+}
+
 func (hs *clientHandshakeState) serverResumedSession() bool {
 	// If the server responded with the same sessionID then it means the
 	// sessionTicket is being used to resume a TLS session.
@@ -2293,6 +2321,7 @@
 		c.extendedMasterSecret = hs.session.extendedMasterSecret
 		c.sctList = hs.session.sctList
 		c.ocspResponse = hs.session.ocspResponse
+		c.peerRawPublicKey = hs.session.serverRawPublicKey
 		hs.finishedHash.discardHandshakeBuffer()
 		return true, nil
 	}
@@ -2346,6 +2375,7 @@
 		sctList:                   c.sctList,
 		ocspResponse:              c.ocspResponse,
 		ticketExpiration:          c.config.time().Add(time.Duration(7 * 24 * time.Hour)),
+		serverRawPublicKey:        c.peerRawPublicKey,
 	}
 
 	if !hs.serverHello.extensions.ticketSupported {
diff --git a/ssl/test/runner/handshake_messages.go b/ssl/test/runner/handshake_messages.go
index 9045004..2efccfd 100644
--- a/ssl/test/runner/handshake_messages.go
+++ b/ssl/test/runner/handshake_messages.go
@@ -2233,13 +2233,19 @@
 	if m.raw != nil {
 		return m.raw
 	}
+	if m.certificateType == certTypeRawPublicKey && len(m.certificates) > 1 {
+		panic("Expected at most 1 certificateEntry for RawPublicKey")
+	}
 
 	certMsg := cryptobyte.NewBuilder(nil)
 	certMsg.AddUint8(typeCertificate)
-	// TODO(crbug.com/467663225): Serialize RawPublicKey Certificates.
 	certMsg.AddUint24LengthPrefixed(func(certificate *cryptobyte.Builder) {
 		if m.hasRequestContext {
 			addUint8LengthPrefixedBytes(certificate, m.requestContext)
+		} else if m.certificateType == certTypeRawPublicKey && len(m.certificates) == 1 {
+			// TLS 1.2 RawPublicKey format.
+			addUint24LengthPrefixedBytes(certificate, m.certificates[0].data)
+			return
 		}
 		certificate.AddUint24LengthPrefixed(func(certificateList *cryptobyte.Builder) {
 			for i, cert := range m.certificates {
diff --git a/ssl/test/runner/handshake_server.go b/ssl/test/runner/handshake_server.go
index 68442ae..636c932 100644
--- a/ssl/test/runner/handshake_server.go
+++ b/ssl/test/runner/handshake_server.go
@@ -42,6 +42,7 @@
 	finishedBytes   []byte
 	echHPKEContext  *hpke.Context
 	echConfigID     uint8
+	rpkFromClient   []byte
 }
 
 // serverHandshake performs a TLS handshake as a server.
@@ -1282,6 +1283,12 @@
 			if _, err := hs.processCertsFromClient(hs.sessionState.certificates); err != nil {
 				return err
 			}
+		} else if len(hs.sessionState.peerRawPublicKey) > 0 {
+			if _, err := hs.processRawPublicKeyFromClient([]certificateEntry{
+				{data: hs.sessionState.peerRawPublicKey},
+			}); err != nil {
+				return err
+			}
 		}
 	}
 
@@ -1805,6 +1812,17 @@
 			c.clientCertificateType = serverExtensions.clientCertificateType
 		}
 	}
+	if sendServerCertType := c.config.Bugs.SendServerCertificateTypes; sendServerCertType != nil {
+		if len(sendServerCertType) > 1 {
+			panic("tls: server_certificate_type must not contain more than 1 value.")
+		}
+		if len(sendServerCertType) == 0 {
+			serverExtensions.serverCertificateType = nil
+		} else {
+			serverExtensions.serverCertificateType = ptrTo(sendServerCertType[0])
+			c.serverCertificateType = serverExtensions.serverCertificateType
+		}
+	}
 
 	return nil
 }
@@ -1858,7 +1876,7 @@
 		return false
 	}
 
-	sessionHasClientCerts := len(hs.sessionState.certificates) != 0
+	sessionHasClientCerts := len(hs.sessionState.certificates) != 0 || len(hs.sessionState.peerRawPublicKey) != 0
 	needClientCerts := c.config.ClientAuth == RequireAnyClientCert || c.config.ClientAuth == RequireAndVerifyClientCert
 	if needClientCerts && !sessionHasClientCerts {
 		return false
@@ -1905,6 +1923,14 @@
 		}
 	}
 
+	if len(hs.sessionState.peerRawPublicKey) > 0 {
+		if _, err := hs.processRawPublicKeyFromClient([]certificateEntry{
+			{data: hs.sessionState.peerRawPublicKey},
+		}); err != nil {
+			return err
+		}
+	}
+
 	hs.masterSecret = hs.sessionState.secret
 	c.extendedMasterSecret = hs.sessionState.extendedMasterSecret
 
@@ -1963,6 +1989,7 @@
 
 	if !isPSK {
 		certMsg := new(certificateMsg)
+		certMsg.certificateType = hs.cert.Type.CertificateType()
 		if !config.Bugs.EmptyCertificateList {
 			for _, certData := range hs.cert.Certificate {
 				certMsg.certificates = append(certMsg.certificates, certificateEntry{
@@ -2217,11 +2244,12 @@
 func (hs *serverHandshakeState) sendSessionTicket() error {
 	c := hs.c
 	state := sessionState{
-		vers:          c.vers,
-		cipherSuite:   hs.suite,
-		secret:        hs.masterSecret,
-		certificates:  hs.certsFromClient,
-		handshakeHash: hs.finishedHash.Sum(),
+		vers:             c.vers,
+		cipherSuite:      hs.suite,
+		secret:           hs.masterSecret,
+		certificates:     hs.certsFromClient,
+		handshakeHash:    hs.finishedHash.Sum(),
+		peerRawPublicKey: hs.rpkFromClient,
 	}
 
 	if !hs.hello.extensions.ticketSupported || hs.c.config.Bugs.SkipNewSessionTicket {
@@ -2371,8 +2399,9 @@
 	return nil, nil
 }
 
-// processRawPublicKeyFromClient takes the list of certificates from the Certificate message,
-// which for a RPK should contain exactly 1 entry, and parses it to return the raw public key.
+// processRawPublicKeyFromClient takes the list of certificates from the session
+// state or a Certificate message, which for a RPK should contain exactly 1
+// entry, and parses it to return the raw public key.
 func (hs *serverHandshakeState) processRawPublicKeyFromClient(certificates []certificateEntry) (crypto.PublicKey, error) {
 	c := hs.c
 
@@ -2386,6 +2415,7 @@
 	}
 
 	c.peerRawPublicKey = certificates[0].data
+	hs.rpkFromClient = certificates[0].data
 
 	rawPublicKey, err := x509.ParsePKIXPublicKey(c.peerRawPublicKey)
 	if err != nil {
diff --git a/ssl/test/runner/raw_public_key_tests.go b/ssl/test/runner/raw_public_key_tests.go
index 0b83e29..3208b59 100644
--- a/ssl/test/runner/raw_public_key_tests.go
+++ b/ssl/test/runner/raw_public_key_tests.go
@@ -15,6 +15,7 @@
 package runner
 
 import (
+	"crypto/sha256"
 	"fmt"
 	"slices"
 	"strconv"
@@ -32,53 +33,264 @@
 	certTypesListRPKUnknown  = []CertificateType{certTypeRawPublicKey, certTypeBogus}
 )
 
+// A malformed RPK credential which is sent on the wire as an empty SubjectPublicKeyInfo.
+var rpkEmpty = Credential{
+	Type:        CredentialTypeRawPublicKey,
+	Certificate: [][]byte{{}},
+}
+
+func addRPKCustomVerifyToFlags(peerCredential *Credential, shimFlags []string) []string {
+	if peerCredential.Type == CredentialTypeRawPublicKey {
+		peerRPKHash := sha256.Sum256(peerCredential.Certificate[0])
+		shimFlags = append(shimFlags, "-expect-peer-rpk-sha256", base64FlagValue(peerRPKHash[:]))
+	}
+	return shimFlags
+}
+
 func addServerCertTypeTests() {
 	for _, ver := range allVersions(tls) {
-		// Tests sending list of accepted server cert types in the ClientHello.
-		// TODO(crbug.com/467663225): Test server response and rest of the handshake.
+		// Tests sending list of accepted server cert types in the ClientHello and
+		// receiving the server's negotiated cert type and credential.
 		for _, test := range []struct {
 			name                         string
 			serverCertTypesAccepted      []CertificateType
 			expectedClientHelloExtension []CertificateType
+			serverHelloExtension         []CertificateType
+			serverCredential             *Credential
+			expectedError                string
+			expectedLocalError           string
 		}{
 			{
-				name:                         "RPKOnly",
+				name:                         "RPKOnly-NegotiatedRPK",
 				serverCertTypesAccepted:      certTypesListRPKOnly,
 				expectedClientHelloExtension: certTypesListRPKOnly,
+				serverHelloExtension:         certTypesListRPKOnly,
+				serverCredential:             &rpkEcdsaP256,
 			},
 			{
-				name:                         "RPKX509",
+				name:                         "RPKX509-NegotiatedRPK",
 				serverCertTypesAccepted:      certTypesListRPKX509,
 				expectedClientHelloExtension: certTypesListRPKX509,
+				serverHelloExtension:         certTypesListRPKOnly,
+				serverCredential:             &rpkEcdsaP256,
 			},
 			{
-				name:                         "X509RPK",
+				name:                         "RPKX509-NegotiatedX509",
+				serverCertTypesAccepted:      certTypesListRPKX509,
+				expectedClientHelloExtension: certTypesListRPKX509,
+				serverHelloExtension:         certTypesListX509Only,
+				serverCredential:             &ecdsaP256Certificate,
+			},
+			{
+				name:                         "RPKX509-NegotiatedX509ByDefault",
+				serverCertTypesAccepted:      certTypesListRPKX509,
+				expectedClientHelloExtension: certTypesListRPKX509,
+				serverHelloExtension:         nil,
+				serverCredential:             &ecdsaP256Certificate,
+			},
+			{
+				name:                         "X509RPK-NegotiatedRPK",
 				serverCertTypesAccepted:      certTypesListX509RPK,
 				expectedClientHelloExtension: certTypesListX509RPK,
+				serverHelloExtension:         certTypesListRPKOnly,
+				serverCredential:             &rpkEcdsaP256,
+			},
+			{
+				name:                         "X509RPK-NegotiatedX509",
+				serverCertTypesAccepted:      certTypesListX509RPK,
+				expectedClientHelloExtension: certTypesListX509RPK,
+				serverHelloExtension:         certTypesListX509Only,
+				serverCredential:             &ecdsaP256Certificate,
+			},
+			{
+				name:                         "X509RPK-NegotiatedX509ByDefault",
+				serverCertTypesAccepted:      certTypesListX509RPK,
+				expectedClientHelloExtension: certTypesListX509RPK,
+				serverHelloExtension:         nil,
+				serverCredential:             &ecdsaP256Certificate,
 			},
 			{
 				// Configuring the default cert type only omits the extension.
 				name:                         "DefaultOnly-Omitted",
 				serverCertTypesAccepted:      certTypesListX509Only,
 				expectedClientHelloExtension: []CertificateType{},
+				serverHelloExtension:         nil,
+				serverCredential:             &ecdsaP256Certificate,
+			},
+			{
+				// Server picked RPK despite client not supporting RPKs. Client should
+				// reject the unsolicited extension.
+				name:                         "DefaultOnly-ServerPickedRPKInError",
+				serverCertTypesAccepted:      certTypesListX509Only,
+				expectedClientHelloExtension: []CertificateType{},
+				serverHelloExtension:         certTypesListRPKOnly,
+				serverCredential:             &rpkEcdsaP256,
+				expectedError:                ":UNEXPECTED_EXTENSION:",
+			},
+			{
+				// Server picked X.509 despite client not supporting X.509. Client
+				// should reject the invalid extension.
+				name:                         "RPKOnly-ServerPickedX509InError",
+				serverCertTypesAccepted:      certTypesListRPKOnly,
+				expectedClientHelloExtension: certTypesListRPKOnly,
+				serverHelloExtension:         certTypesListX509Only,
+				serverCredential:             &ecdsaP256Certificate,
+				expectedError:                ":UNSUPPORTED_CERTIFICATE:",
+			},
+			{
+				// Server picked X.509 despite client not supporting X.509. Client
+				// should reject.
+				name:                         "RPKOnly-ServerPickedX509ByDefaultInError",
+				serverCertTypesAccepted:      certTypesListRPKOnly,
+				expectedClientHelloExtension: certTypesListRPKOnly,
+				serverHelloExtension:         nil,
+				serverCredential:             &ecdsaP256Certificate,
+				expectedError:                ":UNSUPPORTED_CERTIFICATE:",
+			},
+			{
+				// Server sent an X.509 cert despite negotiating RPK. It should fail to
+				// parse.
+				name:                         "RPKOnly-NegotiatedRPK-ServerIncorrectlySentX509",
+				serverCertTypesAccepted:      certTypesListRPKOnly,
+				expectedClientHelloExtension: certTypesListRPKOnly,
+				serverHelloExtension:         certTypesListRPKOnly,
+				serverCredential:             &ecdsaP256Certificate,
+				expectedError:                ":DECODE_ERROR:",
+			},
+			{
+				// Server sent a RPK despite negotiating X.509 (explicitly). It should
+				// fail to parse.
+				name:                         "RPKX509-NegotiatedX509-ServerIncorrectlySentRPK",
+				serverCertTypesAccepted:      certTypesListRPKX509,
+				expectedClientHelloExtension: certTypesListRPKX509,
+				serverHelloExtension:         certTypesListX509Only,
+				serverCredential:             &rpkEcdsaP256,
+				expectedLocalError:           "remote error: error decoding message",
+			},
+			{
+				// Server sent a RPK despite negotiating X.509 (by default). It should
+				// fail to parse.
+				name:                         "RPKX509-NegotiatedX509ByDefault-ServerIncorrectlySentRPK",
+				serverCertTypesAccepted:      certTypesListRPKX509,
+				expectedClientHelloExtension: certTypesListRPKX509,
+				serverHelloExtension:         nil,
+				serverCredential:             &rpkEcdsaP256,
+				expectedLocalError:           "remote error: error decoding message",
 			},
 		} {
+			shimFlags := flagCertTypes("-accepted-peer-cert-types", test.serverCertTypesAccepted)
+			if test.expectedError == "" {
+				shimFlags = append(shimFlags, "-expect-peer-certificate-type", strconv.Itoa(int(test.serverCredential.Type.CertificateType())))
+				shimFlags = addRPKCustomVerifyToFlags(test.serverCredential, shimFlags)
+			}
 			testCases = append(testCases, testCase{
 				testType: clientTest,
 				name:     fmt.Sprintf("ServerCertificateType-Client-Requests%s-%s", test.name, ver.name),
 				config: Config{
 					MinVersion: ver.version,
 					MaxVersion: ver.version,
+					Credential: test.serverCredential,
 					Bugs: ProtocolBugs{
 						ExpectServerCertificateTypes: test.expectedClientHelloExtension,
+						SendServerCertificateTypes:   test.serverHelloExtension,
 					},
 				},
-				flags: flagCertTypes("-accepted-peer-cert-types", test.serverCertTypesAccepted),
-				// TODO(crbug.com/467663225): Test resumption. It doesn't yet work
-				// because the RPK isn't parsed and stored in the session yet.
-				resumeSession: false,
+				flags:              shimFlags,
+				shouldFail:         test.expectedError != "" || test.expectedLocalError != "",
+				expectedError:      test.expectedError,
+				expectedLocalError: test.expectedLocalError,
+				resumeSession:      test.expectedError == "" && test.expectedLocalError == "",
 			})
 		}
+		shimFlags := flagCertTypes("-accepted-peer-cert-types", certTypesListRPKOnly)
+		// The Certificate message contains an RPK with an empty SPKI. In TLS 1.2
+		// this is considered to indicate the lack of a certificate (so the client
+		// will reject), whereas this is illegal for the TLS 1.3 RPK Certificate
+		// format.
+		expectedError := ":INVALID_RAW_PUBLIC_KEY:"
+		if ver.version <= VersionTLS12 {
+			// Parsing the Certificate failed to yield a credential.
+			expectedError = ":DECODE_ERROR:"
+		}
+		testCases = append(testCases, testCase{
+			testType: clientTest,
+			name:     fmt.Sprintf("ServerCertificateType-Client-RequestsRPKOnly-ServerSentEmptyRPK-%s", ver.name),
+			config: Config{
+				MinVersion: ver.version,
+				MaxVersion: ver.version,
+				Credential: &rpkEmpty,
+				Bugs: ProtocolBugs{
+					ExpectServerCertificateTypes: certTypesListRPKOnly,
+					SendServerCertificateTypes:   certTypesListRPKOnly,
+				},
+			},
+			flags:         shimFlags,
+			shouldFail:    true,
+			expectedError: expectedError,
+		})
+		if ver.version >= VersionTLS13 {
+			// If the server sends a Certificate message for an RPK that contains an
+			// empty certificate list, client should reject. (For TLS 1.2 and below,
+			// this test would be equivalent to the one above.)
+			expectedError = ":PEER_DID_NOT_RETURN_A_CERTIFICATE:"
+			testCases = append(testCases, testCase{
+				testType: clientTest,
+				name:     fmt.Sprintf("ServerCertificateType-Client-RequestsRPKOnly-ServerSentEmptyCertificateList-%s", ver.name),
+				config: Config{
+					MinVersion: ver.version,
+					MaxVersion: ver.version,
+					Credential: &rpkEcdsaP256,
+					Bugs: ProtocolBugs{
+						ExpectServerCertificateTypes: certTypesListRPKOnly,
+						SendServerCertificateTypes:   certTypesListRPKOnly,
+						EmptyCertificateList:         true,
+						SkipCertificateVerify:        true,
+					},
+				},
+				flags:         shimFlags,
+				shouldFail:    true,
+				expectedError: expectedError,
+			})
+			// If the server sends an otherwise valid Certificate message with an RPK,
+			// but does not send CertificateVerify, client should reject.
+			shimFlags = addRPKCustomVerifyToFlags(&rpkEcdsaP256, shimFlags)
+			testCases = append(testCases, testCase{
+				testType: clientTest,
+				name:     fmt.Sprintf("ServerCertificateType-Client-RPKWithoutCertificateVerify-%s", ver.name),
+				config: Config{
+					MinVersion: ver.version,
+					MaxVersion: ver.version,
+					Credential: &rpkEcdsaP256,
+					Bugs: ProtocolBugs{
+						ExpectServerCertificateTypes: certTypesListRPKOnly,
+						SendServerCertificateTypes:   certTypesListRPKOnly,
+						SkipCertificateVerify:        true,
+					},
+				},
+				flags:              shimFlags,
+				shouldFail:         true,
+				expectedLocalError: "remote error: unexpected message",
+			})
+		}
+		// Test that RPK server cert verification fails if we force it to fail.
+		shimFlags = append([]string{"-verify-fail"},
+			flagCertTypes("-accepted-peer-cert-types", certTypesListRPKOnly)...)
+		testCases = append(testCases, testCase{
+			testType: clientTest,
+			name:     fmt.Sprintf("ServerCertificateType-Client-RPKVerifyFail-%s", ver.name),
+			config: Config{
+				MinVersion: ver.version,
+				MaxVersion: ver.version,
+				Credential: &rpkEcdsaP256,
+				Bugs: ProtocolBugs{
+					ExpectServerCertificateTypes: certTypesListRPKOnly,
+					SendServerCertificateTypes:   certTypesListRPKOnly,
+				},
+			},
+			flags:         shimFlags,
+			shouldFail:    true,
+			expectedError: ":CERTIFICATE_VERIFY_FAILED:",
+		})
 		// Tests that server can receive a server_certificate_type extension from
 		// the client and select and send its most-preferred shared cert type based
 		// on configured server credentials, and test that server sends credential
@@ -87,14 +299,12 @@
 			name                        string
 			serverCertTypesRequested    []CertificateType
 			serverCredentialsConfigured []*Credential
-			expectedNegotiated          CertificateType
 			expectedCredentialIndex     int
 		}{
 			{
 				name:                        "RPKRequested-RPKAvailable",
 				serverCertTypesRequested:    certTypesListRPKOnly,
 				serverCredentialsConfigured: []*Credential{&rpkEcdsaP256},
-				expectedNegotiated:          certTypeRawPublicKey,
 				expectedCredentialIndex:     0,
 			},
 			{
@@ -104,63 +314,54 @@
 				name:                        "RPKRequested-MultipleRPKsAvailable",
 				serverCertTypesRequested:    certTypesListRPKOnly,
 				serverCredentialsConfigured: []*Credential{&rpkEcdsaP256, &rpkRsa},
-				expectedNegotiated:          certTypeRawPublicKey,
 				expectedCredentialIndex:     0,
 			},
 			{
 				name:                        "RPKX509Requested-RPKAvailable",
 				serverCertTypesRequested:    certTypesListRPKX509,
 				serverCredentialsConfigured: []*Credential{&rpkEcdsaP256},
-				expectedNegotiated:          certTypeRawPublicKey,
 				expectedCredentialIndex:     0,
 			},
 			{
 				name:                        "X509RPKRequested-RPKAvailable",
 				serverCertTypesRequested:    certTypesListX509RPK,
 				serverCredentialsConfigured: []*Credential{&rpkEcdsaP256},
-				expectedNegotiated:          certTypeRawPublicKey,
 				expectedCredentialIndex:     0,
 			},
 			{
 				name:                        "RPKRequested-RPKX509Available",
 				serverCertTypesRequested:    certTypesListRPKOnly,
 				serverCredentialsConfigured: []*Credential{&rpkEcdsaP256, &ecdsaP256Certificate},
-				expectedNegotiated:          certTypeRawPublicKey,
 				expectedCredentialIndex:     0,
 			},
 			{
 				name:                        "RPKX509Requested-RPKX509Available",
 				serverCertTypesRequested:    certTypesListRPKX509,
 				serverCredentialsConfigured: []*Credential{&rpkEcdsaP256, &ecdsaP256Certificate},
-				expectedNegotiated:          certTypeRawPublicKey,
 				expectedCredentialIndex:     0,
 			},
 			{
 				name:                        "X509RPKRequested-RPKX509Available",
 				serverCertTypesRequested:    certTypesListX509RPK,
 				serverCredentialsConfigured: []*Credential{&rpkEcdsaP256, &ecdsaP256Certificate},
-				expectedNegotiated:          certTypeRawPublicKey,
 				expectedCredentialIndex:     0,
 			},
 			{
 				name:                        "RPKRequested-X509RPKAvailable",
 				serverCertTypesRequested:    certTypesListRPKOnly,
 				serverCredentialsConfigured: []*Credential{&ecdsaP256Certificate, &rpkEcdsaP256},
-				expectedNegotiated:          certTypeRawPublicKey,
 				expectedCredentialIndex:     1,
 			},
 			{
 				name:                        "RPKX509Requested-X509RPKAvailable",
 				serverCertTypesRequested:    certTypesListRPKX509,
 				serverCredentialsConfigured: []*Credential{&ecdsaP256Certificate, &rpkEcdsaP256},
-				expectedNegotiated:          certTypeX509,
 				expectedCredentialIndex:     0,
 			},
 			{
 				name:                        "X509RPKRequested-X509RPKAvailable",
 				serverCertTypesRequested:    certTypesListX509RPK,
 				serverCredentialsConfigured: []*Credential{&ecdsaP256Certificate, &rpkEcdsaP256},
-				expectedNegotiated:          certTypeX509,
 				expectedCredentialIndex:     0,
 			},
 			{
@@ -169,7 +370,6 @@
 				name:                        "RPKUnknownRequested-X509RPKAvailable",
 				serverCertTypesRequested:    certTypesListRPKUnknown,
 				serverCredentialsConfigured: []*Credential{&ecdsaP256Certificate, &rpkEcdsaP256},
-				expectedNegotiated:          certTypeRawPublicKey,
 				expectedCredentialIndex:     1,
 			},
 			{
@@ -178,7 +378,6 @@
 				name:                        "UnknownX509Requested-X509RPKAvailable",
 				serverCertTypesRequested:    certTypesListUnknownX509,
 				serverCredentialsConfigured: []*Credential{&rpkEcdsaP256, &ecdsaP256Certificate},
-				expectedNegotiated:          certTypeX509,
 				expectedCredentialIndex:     1,
 			},
 		} {
@@ -194,19 +393,17 @@
 					MaxVersion: ver.version,
 					Bugs: ProtocolBugs{
 						SendServerCertificateTypes:   test.serverCertTypesRequested,
-						ExpectServerCertificateTypes: []CertificateType{test.expectedNegotiated},
+						ExpectServerCertificateTypes: []CertificateType{expectedServerCredential.Type.CertificateType()},
 					},
 				},
 				flags: []string{
-					"-expect-selected-credential", strconv.Itoa(test.expectedCredentialIndex),
+					"-on-initial-expect-selected-credential", strconv.Itoa(test.expectedCredentialIndex),
 				},
 				expectations: connectionExpectations{
 					peerCertificate: expectedServerCredential,
 				},
-				shimCredentials: test.serverCredentialsConfigured,
-				// TODO(crbug.com/467663225): Test resumption. It doesn't yet work
-				// because the RPK isn't parsed and stored in the session yet.
-				resumeSession:      false,
+				shimCredentials:    test.serverCredentialsConfigured,
+				resumeSession:      true,
 				skipSplitHandshake: true,
 			}
 			// Test that the server can defer configuring credentials to the cert
@@ -364,16 +561,14 @@
 				},
 				shimCredentials: test.clientCredentials,
 				flags: []string{
-					"-expect-selected-credential", strconv.Itoa(test.expectedCredentialIndex),
+					"-on-initial-expect-selected-credential", strconv.Itoa(test.expectedCredentialIndex),
 				},
 				expectations: connectionExpectations{
 					peerCertificate: expectedClientCredential,
 				},
 				shouldFail:    test.expectedFailure != "",
 				expectedError: test.expectedFailure,
-				// TODO(crbug.com/467663225): Test resumption. It doesn't yet work
-				// because the RPK isn't parsed and stored in the session yet.
-				resumeSession: false,
+				resumeSession: test.expectedFailure == "",
 			})
 		}
 		// Tests that overriding the default client_certificate_type logic works, and
@@ -547,7 +742,7 @@
 					},
 				},
 				flags: append(
-					[]string{"-expect-selected-credential", strconv.Itoa(test.expectedCredentialIndex)},
+					[]string{"-on-initial-expect-selected-credential", strconv.Itoa(test.expectedCredentialIndex)},
 					flagCertTypes("-available-client-cert-types", test.configuredClientCertTypes)...),
 				shimCredentials: test.clientCredentials,
 				expectations: connectionExpectations{
@@ -555,9 +750,7 @@
 				},
 				shouldFail:    test.expectedFailure != "",
 				expectedError: test.expectedFailure,
-				// TODO(crbug.com/467663225): Test resumption. It doesn't yet work
-				// because the RPK isn't parsed and stored in the session yet.
-				resumeSession: false,
+				resumeSession: test.expectedFailure == "",
 			}
 			// Test that the client can defer configuring credentials to the cert
 			// callback.
@@ -577,7 +770,7 @@
 			clientCertTypesReceived      []CertificateType
 			clientCertTypesAccepted      []CertificateType
 			expectedServerHelloExtension []CertificateType
-			expectedNegotiated           CertificateType
+			clientCredential             *Credential
 			expectedError                string
 			expectedLocalError           string
 		}{
@@ -586,49 +779,49 @@
 				clientCertTypesReceived:      certTypesListRPKOnly,
 				clientCertTypesAccepted:      certTypesListRPKOnly,
 				expectedServerHelloExtension: certTypesListRPKOnly,
-				expectedNegotiated:           certTypeRawPublicKey,
+				clientCredential:             &rpkEcdsaP256,
 			},
 			{
 				name:                         "RPKX509Received-RPKAccepted",
 				clientCertTypesReceived:      certTypesListRPKX509,
 				clientCertTypesAccepted:      certTypesListRPKOnly,
 				expectedServerHelloExtension: certTypesListRPKOnly,
-				expectedNegotiated:           certTypeRawPublicKey,
+				clientCredential:             &rpkEcdsaP256,
 			},
 			{
 				name:                         "X509RPKReceived-RPKAccepted",
 				clientCertTypesReceived:      certTypesListX509RPK,
 				clientCertTypesAccepted:      certTypesListRPKOnly,
 				expectedServerHelloExtension: certTypesListRPKOnly,
-				expectedNegotiated:           certTypeRawPublicKey,
+				clientCredential:             &rpkEcdsaP256,
 			},
 			{
 				name:                         "RPKX509Received-RPKX509Accepted",
 				clientCertTypesReceived:      certTypesListRPKX509,
 				clientCertTypesAccepted:      certTypesListRPKX509,
 				expectedServerHelloExtension: certTypesListRPKOnly,
-				expectedNegotiated:           certTypeRawPublicKey,
+				clientCredential:             &rpkEcdsaP256,
 			},
 			{
 				name:                         "X509RPKReceived-RPKX509Accepted",
 				clientCertTypesReceived:      certTypesListX509RPK,
 				clientCertTypesAccepted:      certTypesListRPKX509,
 				expectedServerHelloExtension: certTypesListRPKOnly,
-				expectedNegotiated:           certTypeRawPublicKey,
+				clientCredential:             &rpkEcdsaP256,
 			},
 			{
 				name:                         "RPKX509Received-X509RPKAccepted",
 				clientCertTypesReceived:      certTypesListRPKX509,
 				clientCertTypesAccepted:      certTypesListX509RPK,
 				expectedServerHelloExtension: certTypesListX509Only,
-				expectedNegotiated:           certTypeX509,
+				clientCredential:             &ecdsaP256Certificate,
 			},
 			{
 				name:                         "X509RPKReceived-X509RPKAccepted",
 				clientCertTypesReceived:      certTypesListX509RPK,
 				clientCertTypesAccepted:      certTypesListX509RPK,
 				expectedServerHelloExtension: certTypesListX509Only,
-				expectedNegotiated:           certTypeX509,
+				clientCredential:             &ecdsaP256Certificate,
 			},
 			{
 				name:                    "RejectsInvalidEmptyExtension",
@@ -671,7 +864,7 @@
 				clientCertTypesReceived:      certTypesListRPKUnknown,
 				clientCertTypesAccepted:      certTypesListX509RPK,
 				expectedServerHelloExtension: certTypesListRPKOnly,
-				expectedNegotiated:           certTypeRawPublicKey,
+				clientCredential:             &rpkEcdsaP256,
 			},
 			{
 				// If the client does not send the extension, the server should treat it
@@ -680,7 +873,7 @@
 				clientCertTypesReceived:      nil,
 				clientCertTypesAccepted:      certTypesListRPKX509,
 				expectedServerHelloExtension: []CertificateType{},
-				expectedNegotiated:           certTypeX509,
+				clientCredential:             &ecdsaP256Certificate,
 			},
 			{
 				// If the client does not send the extension, but the server is
@@ -691,48 +884,144 @@
 				expectedError:           ":UNSUPPORTED_CERTIFICATE:",
 				expectedLocalError:      "remote error: unsupported certificate",
 			},
+			{
+				name:                         "RPKReceived-RPKAccepted-ClientSentX509InError",
+				clientCertTypesReceived:      certTypesListRPKOnly,
+				clientCertTypesAccepted:      certTypesListRPKOnly,
+				expectedServerHelloExtension: certTypesListRPKOnly,
+				clientCredential:             &ecdsaP256Certificate,
+				expectedError:                ":DECODE_ERROR:",
+				expectedLocalError:           "remote error: error decoding message",
+			},
 		} {
-			flags :=
-				append(flagCertTypes("-accepted-peer-cert-types", test.clientCertTypesAccepted),
-					"-require-any-client-certificate")
-			// The handshake currently fails because the rest of the RPK client cert
-			// flow isn't yet implemented.
-			// TODO(crbug.com/467663225): Test client response and rest of the handshake.
-			shouldFail := true
-			expectedError := ":PEER_DID_NOT_RETURN_A_CERTIFICATE:"
-			expectedLocalError := "remote error: handshake failure"
-			if ver.version == VersionTLS13 {
-				expectedLocalError = "remote error: certificate required"
+			for _, verifyMode := range []struct {
+				name string
+				flag string
+			}{
+				{"FailIfNoClientCert", "-require-any-client-certificate"},
+				{"VerifyPeer", "-verify-peer"},
+			} {
+				shimFlags :=
+					append(flagCertTypes("-accepted-peer-cert-types", test.clientCertTypesAccepted),
+						verifyMode.flag)
+				if test.expectedError == "" {
+					shimFlags = append(shimFlags,
+						"-expect-peer-certificate-type", strconv.Itoa(int(test.clientCredential.Type.CertificateType())))
+					shimFlags = addRPKCustomVerifyToFlags(test.clientCredential, shimFlags)
+				}
+				testCases = append(testCases, testCase{
+					testType: serverTest,
+					name:     fmt.Sprintf("ClientCertificateType-Server-%s-%s-%s", test.name, verifyMode.name, ver.name),
+					config: Config{
+						MinVersion: ver.version,
+						MaxVersion: ver.version,
+						Credential: test.clientCredential,
+						Bugs: ProtocolBugs{
+							SendClientCertificateTypes:   test.clientCertTypesReceived,
+							ExpectClientCertificateTypes: test.expectedServerHelloExtension,
+						},
+					},
+					flags:              shimFlags,
+					shouldFail:         test.expectedError != "" || test.expectedLocalError != "",
+					expectedError:      test.expectedError,
+					expectedLocalError: test.expectedLocalError,
+					resumeSession:      test.expectedError == "" && test.expectedLocalError == "",
+					skipSplitHandshake: true,
+				})
 			}
-			if test.expectedError != "" {
-				shouldFail = true
-				expectedError = test.expectedError
-				expectedLocalError = test.expectedLocalError
-			} else {
-				flags = append(flags,
-					"-expect-peer-certificate-type", strconv.Itoa(int(test.expectedNegotiated)))
-			}
+		}
+		// The Certificate message contains an RPK with an empty SPKI. In TLS 1.2
+		// this is considered to indicate the lack of a certificate (so a server
+		// configured to require a client cert will reject), whereas this is
+		// illegal for the TLS 1.3 RPK Certificate format.
+		shimFlags := append([]string{"-require-any-client-certificate"},
+			flagCertTypes("-accepted-peer-cert-types", certTypesListRPKOnly)...)
+		expectedError := ":INVALID_RAW_PUBLIC_KEY:"
+		if ver.version <= VersionTLS12 {
+			expectedError = ":PEER_DID_NOT_RETURN_A_CERTIFICATE:"
+		}
+		testCases = append(testCases, testCase{
+			testType: serverTest,
+			name:     fmt.Sprintf("ClientCertificateType-Server-ClientSentEmptyRPK-%s", ver.name),
+			config: Config{
+				MinVersion: ver.version,
+				MaxVersion: ver.version,
+				Credential: &rpkEmpty,
+				Bugs: ProtocolBugs{
+					SendClientCertificateTypes:   certTypesListRPKOnly,
+					ExpectClientCertificateTypes: certTypesListRPKOnly,
+				},
+			},
+			flags:              shimFlags,
+			skipSplitHandshake: true,
+			shouldFail:         true,
+			expectedError:      expectedError,
+		})
+		shimFlags = append([]string{"-verify-peer"},
+			flagCertTypes("-accepted-peer-cert-types", certTypesListRPKOnly)...)
+		// If the client sends a Certificate message for an RPK that contains an
+		// empty certificate list, and the server isn't configured to require a
+		// client cert, it should proceed without a client cert.
+		testCases = append(testCases, testCase{
+			testType: serverTest,
+			name:     fmt.Sprintf("ClientCertificateType-Server-ClientSentEmptyCertificateList-%s", ver.name),
+			config: Config{
+				MinVersion: ver.version,
+				MaxVersion: ver.version,
+				Credential: &rpkEcdsaP256,
+				Bugs: ProtocolBugs{
+					SendClientCertificateTypes:   certTypesListRPKOnly,
+					ExpectClientCertificateTypes: certTypesListRPKOnly,
+					EmptyCertificateList:         true,
+					SkipCertificateVerify:        true,
+				},
+			},
+			flags:              shimFlags,
+			skipSplitHandshake: true,
+		})
+		if ver.version >= VersionTLS13 {
+			// If the client sends an otherwise valid Certificate message with an RPK,
+			// but does not send CertificateVerify, server should reject.
+			shimFlags = addRPKCustomVerifyToFlags(&rpkEcdsaP256, shimFlags)
 			testCases = append(testCases, testCase{
 				testType: serverTest,
-				name:     fmt.Sprintf("ClientCertificateType-Server-%s-%s", test.name, ver.name),
+				name:     fmt.Sprintf("ClientCertificateType-Server-RPKWithoutCertificateVerify-%s", ver.name),
 				config: Config{
 					MinVersion: ver.version,
 					MaxVersion: ver.version,
+					Credential: &rpkEcdsaP256,
 					Bugs: ProtocolBugs{
-						SendClientCertificateTypes:   test.clientCertTypesReceived,
-						ExpectClientCertificateTypes: test.expectedServerHelloExtension,
+						SendClientCertificateTypes:   certTypesListRPKOnly,
+						ExpectClientCertificateTypes: certTypesListRPKOnly,
+						SkipCertificateVerify:        true,
 					},
 				},
-				flags:              flags,
-				shouldFail:         shouldFail,
-				expectedError:      expectedError,
-				expectedLocalError: expectedLocalError,
-				// TODO(crbug.com/467663225): Test resumption. It doesn't yet work
-				// because the RPK isn't parsed and stored in the session yet.
-				resumeSession:      false,
+				flags:              shimFlags,
+				shouldFail:         true,
+				expectedLocalError: "remote error: unexpected message",
 				skipSplitHandshake: true,
 			})
 		}
+		// Test that RPK client cert verification fails if we force it to fail.
+		shimFlags = append([]string{"-require-any-client-certificate", "-verify-fail"},
+			flagCertTypes("-accepted-peer-cert-types", certTypesListRPKOnly)...)
+		testCases = append(testCases, testCase{
+			testType: serverTest,
+			name:     fmt.Sprintf("ClientCertificateType-Server-RPKVerifyFail-%s", ver.name),
+			config: Config{
+				MinVersion: ver.version,
+				MaxVersion: ver.version,
+				Credential: &rpkEcdsaP256,
+				Bugs: ProtocolBugs{
+					SendClientCertificateTypes:   certTypesListRPKOnly,
+					ExpectClientCertificateTypes: certTypesListRPKOnly,
+				},
+			},
+			flags:              shimFlags,
+			shouldFail:         true,
+			expectedError:      ":CERTIFICATE_VERIFY_FAILED:",
+			skipSplitHandshake: true,
+		})
 	}
 }
 
diff --git a/ssl/test/runner/runner.go b/ssl/test/runner/runner.go
index 491253e..31bffa2 100644
--- a/ssl/test/runner/runner.go
+++ b/ssl/test/runner/runner.go
@@ -312,10 +312,24 @@
 		{&ecdsaP384Key, &rpkEcdsaP384},
 		{&rsa2048Key, &rpkRsa},
 	} {
+		var publicKey crypto.PublicKey
+		switch def.key.(type) {
+		case *rsa.PrivateKey:
+			publicKey = def.key.(*rsa.PrivateKey).Public()
+		case *ecdsa.PrivateKey:
+			publicKey = def.key.(*ecdsa.PrivateKey).Public()
+		default:
+			panic("credential has unsupported key type")
+		}
+		rpkData, err := x509.MarshalPKIXPublicKey(publicKey)
+		if err != nil {
+			panic(err)
+		}
 		*def.out = Credential{
-			Type:       CredentialTypeRawPublicKey,
-			PrivateKey: def.key,
-			KeyPath:    writeTempKeyFile(def.key),
+			Type:        CredentialTypeRawPublicKey,
+			PrivateKey:  def.key,
+			KeyPath:     writeTempKeyFile(def.key),
+			Certificate: [][]byte{rpkData},
 		}
 	}
 }
@@ -935,13 +949,17 @@
 	}
 
 	if expected := expectations.peerCertificate; expected != nil {
-		if len(connState.PeerCertificates) != len(expected.Certificate) {
-			return fmt.Errorf("expected peer to send %d certificates, but got %d", len(connState.PeerCertificates), len(expected.Certificate))
-		}
-		for i, cert := range connState.PeerCertificates {
-			if !bytes.Equal(cert.Raw, expected.Certificate[i]) {
-				return fmt.Errorf("peer certificate %d did not match", i+1)
+		var peerCerts [][]byte
+		switch expected.Type.CertificateType() {
+		case certTypeX509:
+			for _, cert := range connState.PeerCertificates {
+				peerCerts = append(peerCerts, cert.Raw)
 			}
+		case certTypeRawPublicKey:
+			peerCerts = [][]byte{connState.PeerRawPublicKey}
+		}
+		if !slices.EqualFunc(peerCerts, expected.Certificate, slices.Equal) {
+			return fmt.Errorf("peer certificate did not match expectations (got %v, expected %v)", peerCerts, expected.Certificate)
 		}
 
 		if !bytes.Equal(connState.OCSPResponse, expected.OCSPStaple) {
diff --git a/ssl/test/runner/ticket.go b/ssl/test/runner/ticket.go
index c12209f..4ba2970 100644
--- a/ssl/test/runner/ticket.go
+++ b/ssl/test/runner/ticket.go
@@ -37,6 +37,7 @@
 	hasApplicationSettingsOld   bool
 	localApplicationSettingsOld []byte
 	peerApplicationSettingsOld  []byte
+	peerRawPublicKey            []byte
 }
 
 func (s *sessionState) marshal() []byte {
@@ -81,6 +82,8 @@
 		msg.AddUint8(0)
 	}
 
+	addUint24LengthPrefixedBytes(msg, s.peerRawPublicKey)
+
 	return msg.BytesOrPanic()
 }
 
@@ -168,6 +171,10 @@
 		}
 	}
 
+	if !readUint24LengthPrefixedBytes(&reader, &s.peerRawPublicKey) {
+		return false
+	}
+
 	if len(reader) > 0 {
 		return false
 	}
diff --git a/ssl/test/test_config.cc b/ssl/test/test_config.cc
index c915f2b..15148ee 100644
--- a/ssl/test/test_config.cc
+++ b/ssl/test/test_config.cc
@@ -637,6 +637,8 @@
                       &TestConfig::available_client_cert_types),
         OptionalIntFlag("-expect-peer-certificate-type",
                         &TestConfig::expect_peer_certificate_type),
+        Base64Flag("-expect-peer-rpk-sha256",
+                   &TestConfig::expect_peer_rpk_sha256),
     };
     std::sort(ret.begin(), ret.end(), FlagNameComparator{});
     return ret;
@@ -2298,6 +2300,30 @@
   return ssl_verify_ok;
 }
 
+static ssl_verify_result_t VerifyRawPublicKeyCallback(SSL *ssl,
+                                                      uint8_t *out_alert) {
+  const TestConfig *config = GetTestConfig(ssl);
+  const EVP_PKEY *peer_rpk = SSL_get0_peer_rpk(ssl);
+  if (peer_rpk == nullptr) {
+    fprintf(stderr, "Expected peer RPK but found none.\n");
+    return ssl_verify_invalid;
+  }
+  ScopedCBB spki_cbb;
+  uint8_t peer_rpk_sha256[SHA256_DIGEST_LENGTH];
+  if (!CBB_init(spki_cbb.get(), 0) ||
+      !EVP_marshal_public_key(spki_cbb.get(), peer_rpk) ||
+      !SHA256(CBB_data(spki_cbb.get()), CBB_len(spki_cbb.get()),
+              peer_rpk_sha256)) {
+    fprintf(stderr, "Error computing sha256 hash of peer RPK.\n");
+    return ssl_verify_invalid;
+  }
+  if (Span(peer_rpk_sha256) != Span(config->expect_peer_rpk_sha256)) {
+    fprintf(stderr, "sha256 hash of peer RPK does not match expectation.\n");
+    return ssl_verify_invalid;
+  }
+  return ssl_verify_ok;
+}
+
 static int CertCallback(SSL *ssl, void *arg) {
   const TestConfig *config = GetTestConfig(ssl);
 
@@ -2360,7 +2386,16 @@
   if (verify_peer) {
     mode = SSL_VERIFY_PEER;
   }
-  if (use_custom_verify_callback) {
+  if (!expect_peer_rpk_sha256.empty()) {
+    if (expect_peer_rpk_sha256.size() != SHA256_DIGEST_LENGTH) {
+      fprintf(stderr,
+              "Invalid -expect-peer-rpk-sha256 length: %d (should be %d).\n",
+              static_cast<int>(expect_peer_rpk_sha256.size()),
+              SHA256_DIGEST_LENGTH);
+      return nullptr;
+    }
+    SSL_set_custom_verify(ssl.get(), mode, VerifyRawPublicKeyCallback);
+  } else if (use_custom_verify_callback) {
     SSL_set_custom_verify(ssl.get(), mode, CustomVerifyCallback);
   } else if (mode != SSL_VERIFY_NONE) {
     SSL_set_verify(ssl.get(), mode, nullptr);
diff --git a/ssl/test/test_config.h b/ssl/test/test_config.h
index a1a3c35..5a8f2f5 100644
--- a/ssl/test/test_config.h
+++ b/ssl/test/test_config.h
@@ -256,6 +256,7 @@
   std::vector<uint8_t> accepted_peer_cert_types;
   std::vector<uint8_t> available_client_cert_types;
   std::optional<uint8_t> expect_peer_certificate_type;
+  std::vector<uint8_t> expect_peer_rpk_sha256;
 
   std::vector<const char *> handshaker_args;
 
diff --git a/ssl/tls13_both.cc b/ssl/tls13_both.cc
index 3b6c973..30b1d8f 100644
--- a/ssl/tls13_both.cc
+++ b/ssl/tls13_both.cc
@@ -180,6 +180,7 @@
     return false;
   }
 
+  // Only used for X.509 certificates.
   UniquePtr<STACK_OF(CRYPTO_BUFFER)> certs(sk_CRYPTO_BUFFER_new_null());
   if (!certs) {
     ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_INTERNAL_ERROR);
@@ -190,56 +191,80 @@
       ssl->server && hs->config->retain_only_sha256_of_client_certs;
   UniquePtr<EVP_PKEY> pkey;
   while (CBS_len(&certificate_list) > 0) {
-    CBS certificate, extensions;
+    CBS certificate;  // For an RPK, this is the subjectPublicKeyInfo.
+    CBS extensions;
     if (!CBS_get_u24_length_prefixed(&certificate_list, &certificate) ||
         !CBS_get_u16_length_prefixed(&certificate_list, &extensions) ||
         CBS_len(&certificate) == 0) {
       ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_DECODE_ERROR);
-      OPENSSL_PUT_ERROR(SSL, SSL_R_CERT_LENGTH_MISMATCH);
+      OPENSSL_PUT_ERROR(SSL, hs->peer_cert_type == TLSEXT_cert_type_rpk
+                                 ? SSL_R_INVALID_RAW_PUBLIC_KEY
+                                 : SSL_R_CERT_LENGTH_MISMATCH);
       return false;
     }
+    const bool is_first_entry = pkey == nullptr;
+    if (is_first_entry) {
+      if (hs->peer_cert_type == TLSEXT_cert_type_rpk) {
+        // Only one CertificateEntry is allowed for RawPublicKey cert type.
+        if (CBS_len(&certificate_list) != 0) {
+          ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_ILLEGAL_PARAMETER);
+          OPENSSL_PUT_ERROR(SSL, SSL_R_INVALID_RAW_PUBLIC_KEY);
+          return false;
+        }
+        pkey = ssl_parse_peer_subject_public_key_info(certificate);
+      } else {
+        assert(hs->peer_cert_type == TLSEXT_cert_type_x509);
+        bool is_leaf_cert = sk_CRYPTO_BUFFER_num(certs.get()) == 0;
+        if (is_leaf_cert) {
+          pkey = ssl_cert_parse_pubkey(&certificate);
+        }
+      }
 
-    const bool is_leaf = sk_CRYPTO_BUFFER_num(certs.get()) == 0;
-    if (is_leaf) {
-      pkey = ssl_cert_parse_pubkey(&certificate);
       if (!pkey) {
         ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_DECODE_ERROR);
         OPENSSL_PUT_ERROR(SSL, SSL_R_DECODE_ERROR);
         return false;
       }
       // TLS 1.3 always uses certificate keys for signing thus the correct
-      // keyUsage is enforced.
-      if (!ssl_cert_check_key_usage(&certificate,
+      // keyUsage is enforced. (RPKs have no keyUsage to enforce.)
+      if (hs->peer_cert_type == TLSEXT_cert_type_x509 &&
+          !ssl_cert_check_key_usage(&certificate,
                                     key_usage_digital_signature)) {
         ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_ILLEGAL_PARAMETER);
         return false;
       }
 
       if (retain_sha256) {
-        // Retain the hash of the leaf certificate if requested.
+        // Retain the hash of the leaf certificate or RPK if requested.
         SHA256(CBS_data(&certificate), CBS_len(&certificate),
                hs->new_session->peer_sha256);
       }
     }
 
-    UniquePtr<CRYPTO_BUFFER> buf(
-        CRYPTO_BUFFER_new_from_CBS(&certificate, ssl->ctx->pool));
-    if (!buf ||  //
-        !PushToStack(certs.get(), std::move(buf))) {
-      ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_INTERNAL_ERROR);
-      return false;
+    if (hs->peer_cert_type == TLSEXT_cert_type_x509) {
+      UniquePtr<CRYPTO_BUFFER> buf(
+          CRYPTO_BUFFER_new_from_CBS(&certificate, ssl->ctx->pool));
+      if (!buf ||  //
+          !PushToStack(certs.get(), std::move(buf))) {
+        ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_INTERNAL_ERROR);
+        return false;
+      }
     }
 
+    // Extensions are not supported with RPKs.
+    const bool cert_extensions_allowed =
+        hs->peer_cert_type == TLSEXT_cert_type_x509;
+
     // Parse out the extensions.
-    SSLExtension status_request(
-        TLSEXT_TYPE_status_request,
-        !ssl->server && hs->config->ocsp_stapling_enabled);
-    SSLExtension sct(
-        TLSEXT_TYPE_certificate_timestamp,
-        !ssl->server && hs->config->signed_cert_timestamps_enabled);
+    SSLExtension status_request(TLSEXT_TYPE_status_request,
+                                cert_extensions_allowed && !ssl->server &&
+                                    hs->config->ocsp_stapling_enabled);
+    SSLExtension sct(TLSEXT_TYPE_certificate_timestamp,
+                     cert_extensions_allowed && !ssl->server &&
+                         hs->config->signed_cert_timestamps_enabled);
     SSLExtension trust_anchors(
         TLSEXT_TYPE_trust_anchors,
-        !ssl->server && is_leaf &&
+        cert_extensions_allowed && !ssl->server && is_first_entry &&
             hs->config->requested_trust_anchors.has_value());
     uint8_t alert = SSL_AD_DECODE_ERROR;
     if (!ssl_parse_extensions(&extensions, &alert,
@@ -300,17 +325,17 @@
     }
   }
 
-  // Store a null certificate list rather than an empty one if the peer didn't
-  // send certificates.
-  if (sk_CRYPTO_BUFFER_num(certs.get()) == 0) {
-    certs.reset();
+  if (pkey != nullptr) {
+    hs->new_session->peer_cert_type = hs->peer_cert_type;
+    hs->peer_pubkey = std::move(pkey);
+    if (hs->peer_cert_type == TLSEXT_cert_type_rpk) {
+      hs->new_session->peer_raw_public_key = UpRef(hs->peer_pubkey);
+    } else {
+      assert(hs->peer_cert_type == TLSEXT_cert_type_x509);
+      hs->new_session->certs = std::move(certs);
+    }
   }
 
-  hs->peer_pubkey = std::move(pkey);
-  hs->new_session->certs = std::move(certs);
-  // TODO(crbug.com/467663225): Parse RPKs and set appropriate session fields.
-  hs->new_session->peer_cert_type = hs->peer_cert_type;
-
   if (!ssl->ctx->x509_method->session_cache_objects(hs->new_session.get())) {
     OPENSSL_PUT_ERROR(SSL, SSL_R_DECODE_ERROR);
     ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_DECODE_ERROR);