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);