@@ -4226,7 +4226,78 @@ def client_callback(*args): # pragma: nocover
4226
4226
4227
4227
4228
4228
class TestDTLS (object ):
4229
+ # The way you would expect DTLSv1_listen to work is:
4230
+ #
4231
+ # - it reads packets in a loop
4232
+ # - when it finds a valid ClientHello, it returns
4233
+ # - now the handshake can proceed
4234
+ #
4235
+ # However, on older versions of OpenSSL, it did something "cleverer". The
4236
+ # way it worked is:
4237
+ #
4238
+ # - it "peeks" into the BIO to see the next packet without consuming it
4239
+ # - if *not* a valid ClientHello, then it reads the packet to consume it
4240
+ # and loops around
4241
+ # - if it *is* a valid ClientHello, it *leaves the packet in the BIO*, and
4242
+ # returns
4243
+ # - then the handshake finds the ClientHello in the BIO and reads it a
4244
+ # second time.
4245
+ #
4246
+ # I'm not sure exactly when this switched over. The OpenSSL v1.1.1 in
4247
+ # Ubuntu 18.04 has the old behavior. The OpenSSL v1.1.1 in Ubuntu 20.04 has
4248
+ # the new behavior. There doesn't seem to be any mention of this change in
4249
+ # the OpenSSL v1.1.1 changelog, but presumably it changed in some point
4250
+ # release or another. Presumably in 2025 or so there will be only new
4251
+ # OpenSSLs around we can delete this whole comment and the weird
4252
+ # workaround. If anyone is still using this library by then, which seems
4253
+ # both depressing and inevitable.
4254
+ #
4255
+ # Anyway, why do we care? The reason is that the old strategy has a
4256
+ # problem: the "peek" operation is only defined on "DGRAM BIOs", which are
4257
+ # a special type of object that is different from the more familiar "socket
4258
+ # BIOs" and "memory BIOs". If you *don't* have a DGRAM BIO, and you try to
4259
+ # peek into the BIO... then it silently degrades to a full-fledged "read"
4260
+ # operation that consumes the packet. Which is a problem if your algorithm
4261
+ # depends on leaving the packet in the BIO to be read again later.
4262
+ #
4263
+ # So on old OpenSSL, we have a problem:
4264
+ #
4265
+ # - we can't use a DGRAM BIO, because cryptography/pyopenssl don't wrap the
4266
+ # relevant APIs, nor should they.
4267
+ #
4268
+ # - if we use a socket BIO, then the first time DTLSv1_listen sees an
4269
+ # invalid packet (like for example... the challenge packet that *every
4270
+ # DTLS handshake starts with before the real ClientHello!*), it tries to
4271
+ # first "peek" it, and then "read" it. But since the first "peek"
4272
+ # consumes the packet, the second "read" ends up hanging or consuming
4273
+ # some unrelated packet, which is undesirable. So you can't even get to
4274
+ # the handshake stage successfully.
4275
+ #
4276
+ # - if we use a memory BIO, then DTLSv1_listen works OK on invalid packets
4277
+ # -- first the "peek" consumes them, and then it tries to "read" again to
4278
+ # consume them, which fails immediately, and OpenSSL ignores the failure.
4279
+ # So it works by accident. BUT, when we get a valid ClientHello, we have
4280
+ # a problem: DTLSv1_listen tries to "peek" it and then leave it in the
4281
+ # read BIO for do_handshake to consume. But instead "peek" consumes the
4282
+ # packet, so it's not there where do_handshake is expecting it, and the
4283
+ # handshake fails.
4284
+ #
4285
+ # Fortunately (if that's the word), we can work around the memory BIO
4286
+ # problem. (Which is good, because in real life probably all our users will
4287
+ # be using memory BIOs.) All we have to do is to save the valid ClientHello
4288
+ # before calling DTLSv1_listen, and then after it returns we push *a second
4289
+ # copy of it* of the packet memory BIO before calling do_handshake. This
4290
+ # fakes out OpenSSL and makes it think the "peek" operation worked
4291
+ # correctly, and we can go on with our lives.
4292
+ #
4293
+ # In fact, we push the second copy of the ClientHello unconditionally. On
4294
+ # new versions of OpenSSL, this is unnecessary, but harmless, because the
4295
+ # DTLS state machine treats it like a network hiccup that duplicated a
4296
+ # packet, which DTLS is robust against.
4229
4297
def test_it_works_at_all (self ):
4298
+ # arbitrary number larger than any conceivable handshake volley
4299
+ LARGE_BUFFER = 65536
4300
+
4230
4301
s_ctx = Context (DTLS_METHOD )
4231
4302
4232
4303
def generate_cookie (ssl ):
@@ -4237,40 +4308,91 @@ def verify_cookie(ssl, cookie):
4237
4308
4238
4309
s_ctx .set_cookie_generate_callback (generate_cookie )
4239
4310
s_ctx .set_cookie_verify_callback (verify_cookie )
4240
-
4241
4311
s_ctx .use_privatekey (load_privatekey (FILETYPE_PEM , server_key_pem ))
4242
4312
s_ctx .use_certificate (load_certificate (FILETYPE_PEM , server_cert_pem ))
4243
-
4244
- s_sock = socket_any_family (type = SOCK_DGRAM )
4245
- s_sock .bind ((loopback_address (s_sock ), 0 ))
4246
- s = Connection (s_ctx , s_sock )
4313
+ s_ctx .set_options (OP_NO_QUERY_MTU )
4314
+ s = Connection (s_ctx )
4247
4315
s .set_accept_state ()
4248
4316
4249
- c_sock = socket (s_sock .family , type = SOCK_DGRAM )
4250
- c = loopback_client_factory (c_sock , DTLS_METHOD )
4317
+ c_ctx = Context (DTLS_METHOD )
4318
+ c_ctx .set_options (OP_NO_QUERY_MTU )
4319
+ c = Connection (c_ctx )
4320
+ c .set_connect_state ()
4251
4321
4252
- c_sock .connect (s_sock .getsockname ())
4253
- s_sock .connect (c_sock .getsockname ())
4322
+ # These are mandatory, because openssl can't guess the MTU for a memory bio and
4323
+ # will produce a mysterious error if you make it try.
4324
+ c .set_ciphertext_mtu (1500 )
4325
+ s .set_ciphertext_mtu (1500 )
4254
4326
4255
- def s_handler ():
4256
- s .DTLSv1_listen ()
4257
- s .do_handshake ()
4258
- s .write (b"hello" )
4259
- assert s .read (100 ) == b"goodbye"
4260
- return "ok"
4327
+ latest_client_hello = None
4261
4328
4262
- def c_handler ():
4263
- c .do_handshake ()
4264
- assert c .read (100 ) == b"hello"
4265
- c .write (b"goodbye" )
4266
- return "ok"
4329
+ def pump_membio (label , source , sink ):
4330
+ try :
4331
+ chunk = source .bio_read (LARGE_BUFFER )
4332
+ except WantReadError :
4333
+ return False
4334
+ if not chunk :
4335
+ return False
4336
+ # Gross hack: if this is a ClientHello, save it so we can find it
4337
+ # later. See giant comment above.
4338
+ try :
4339
+ # if ContentType == handshake and HandshakeType == client_hello:
4340
+ if chunk [0 ] == 22 and chunk [13 ] == 1 :
4341
+ nonlocal latest_client_hello
4342
+ latest_client_hello = chunk
4343
+ except IndexError :
4344
+ pass
4345
+ print (f"{ label } : { chunk .hex ()} " )
4346
+ sink .bio_write (chunk )
4347
+ return True
4348
+
4349
+ def pump ():
4350
+ # Raises if there was no data to pump, to avoid infinite loops if we aren't
4351
+ # making progress.
4352
+ assert pump_membio ("s -> c" , s , c ) or pump_membio ("c -> s" , c , s )
4353
+
4354
+ c_handshaking = True
4355
+ s_listening = True
4356
+ s_handshaking = False
4357
+ first = True
4358
+ while c_handshaking or s_listening or s_handshaking :
4359
+ if not first :
4360
+ pump ()
4361
+ first = False
4362
+
4363
+ if c_handshaking :
4364
+ try :
4365
+ c .do_handshake ()
4366
+ except WantReadError :
4367
+ pass
4368
+ else :
4369
+ c_handshaking = False
4267
4370
4268
- with ThreadPoolExecutor (max_workers = 2 ) as executor :
4269
- s_fut = executor .submit (s_handler )
4270
- c_fut = executor .submit (c_handler )
4371
+ if s_listening :
4372
+ try :
4373
+ s .DTLSv1_listen ()
4374
+ except WantReadError :
4375
+ pass
4376
+ else :
4377
+ s_listening = False
4378
+ s_handshaking = True
4379
+ # Write the duplicate ClientHello. See giant comment above.
4380
+ s .bio_write (latest_client_hello )
4271
4381
4272
- assert s_fut .result () == "ok"
4273
- assert c_fut .result () == "ok"
4382
+ if s_handshaking :
4383
+ try :
4384
+ s .do_handshake ()
4385
+ except WantReadError :
4386
+ pass
4387
+ else :
4388
+ s_handshaking = False
4389
+
4390
+ s .write (b"hello" )
4391
+ pump ()
4392
+ assert c .read (100 ) == b"hello"
4393
+ c .write (b"goodbye" )
4394
+ pump ()
4395
+ assert s .read (100 ) == b"goodbye"
4274
4396
4275
4397
# Check that the MTU set/query functions are doing *something*
4276
4398
c .set_ciphertext_mtu (1000 )
0 commit comments