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