diff options
Diffstat (limited to 'test/recipes/70-test_sslrecords.t')
-rw-r--r-- | test/recipes/70-test_sslrecords.t | 721 |
1 files changed, 721 insertions, 0 deletions
diff --git a/test/recipes/70-test_sslrecords.t b/test/recipes/70-test_sslrecords.t new file mode 100644 index 000000000000..318c9235b0c9 --- /dev/null +++ b/test/recipes/70-test_sslrecords.t @@ -0,0 +1,721 @@ +#! /usr/bin/env perl +# Copyright 2016-2021 The OpenSSL Project Authors. All Rights Reserved. +# +# Licensed under the Apache License 2.0 (the "License"). You may not use +# this file except in compliance with the License. You can obtain a copy +# in the file LICENSE in the source distribution or at +# https://www.openssl.org/source/license.html + +use strict; +use feature 'state'; + +use OpenSSL::Test qw/:DEFAULT cmdstr srctop_file bldtop_dir/; +use OpenSSL::Test::Utils; +use TLSProxy::Proxy; + +my $test_name = "test_sslrecords"; +setup($test_name); + +plan skip_all => "TLSProxy isn't usable on $^O" + if $^O =~ /^(VMS)$/; + +plan skip_all => "$test_name needs the dynamic engine feature enabled" + if disabled("engine") || disabled("dynamic-engine"); + +plan skip_all => "$test_name needs the sock feature enabled" + if disabled("sock"); + +plan skip_all => "$test_name needs TLSv1.2 enabled" + if disabled("tls1_2"); + +$ENV{OPENSSL_ia32cap} = '~0x200000200000000'; +my $proxy = TLSProxy::Proxy->new( + \&add_empty_recs_filter, + cmdstr(app(["openssl"]), display => 1), + srctop_file("apps", "server.pem"), + (!$ENV{HARNESS_ACTIVE} || $ENV{HARNESS_VERBOSE}) +); + +my $boundary_test_type; +my $fatal_alert = 0; # set by filters at expected fatal alerts + +#Test 1: Injecting out of context empty records should fail +my $content_type = TLSProxy::Record::RT_APPLICATION_DATA; +my $inject_recs_num = 1; +$proxy->serverflags("-tls1_2"); +$proxy->clientflags("-no_tls1_3"); +$proxy->start() or plan skip_all => "Unable to start up Proxy for tests"; +plan tests => 20; +ok($fatal_alert, "Out of context empty records test"); + +#Test 2: Injecting in context empty records should succeed +$proxy->clear(); +$content_type = TLSProxy::Record::RT_HANDSHAKE; +$proxy->serverflags("-tls1_2"); +$proxy->clientflags("-no_tls1_3"); +$proxy->start(); +ok(TLSProxy::Message->success(), "In context empty records test"); + +#Test 3: Injecting too many in context empty records should fail +$fatal_alert = 0; +$proxy->clear(); +#We allow 32 consecutive in context empty records +$inject_recs_num = 33; +$proxy->serverflags("-tls1_2"); +$proxy->clientflags("-no_tls1_3"); +$proxy->start(); +ok($fatal_alert, "Too many in context empty records test"); + +#Test 4: Injecting a fragmented fatal alert should fail. We expect the server to +# send back an alert of its own because it cannot handle fragmented +# alerts +$fatal_alert = 0; +$proxy->clear(); +$proxy->filter(\&add_frag_alert_filter); +$proxy->serverflags("-tls1_2"); +$proxy->clientflags("-no_tls1_3"); +$proxy->start(); +ok($fatal_alert, "Fragmented alert records test"); + +#Run some SSLv2 ClientHello tests + +use constant { + TLSV1_2_IN_SSLV2 => 0, + SSLV2_IN_SSLV2 => 1, + FRAGMENTED_IN_TLSV1_2 => 2, + FRAGMENTED_IN_SSLV2 => 3, + ALERT_BEFORE_SSLV2 => 4 +}; + +# The TLSv1.2 in SSLv2 ClientHello need to run at security level 0 +# because in a SSLv2 ClientHello we can't send extentions to indicate +# which signature algorithm we want to use, and the default is SHA1. + +#Test 5: Inject an SSLv2 style record format for a TLSv1.2 ClientHello +my $sslv2testtype = TLSV1_2_IN_SSLV2; +$proxy->clear(); +$proxy->filter(\&add_sslv2_filter); +$proxy->serverflags("-tls1_2"); +$proxy->clientflags("-no_tls1_3 -legacy_renegotiation"); +$proxy->ciphers("AES128-SHA:\@SECLEVEL=0"); +$proxy->start(); +ok(TLSProxy::Message->success(), "TLSv1.2 in SSLv2 ClientHello test"); + +#Test 6: Inject an SSLv2 style record format for an SSLv2 ClientHello. We don't +# support this so it should fail. We actually treat it as an unknown +# protocol so we don't even send an alert in this case. +$sslv2testtype = SSLV2_IN_SSLV2; +$proxy->clear(); +$proxy->serverflags("-tls1_2"); +$proxy->clientflags("-no_tls1_3"); +$proxy->ciphers("AES128-SHA:\@SECLEVEL=0"); +$proxy->start(); +ok(TLSProxy::Message->fail(), "SSLv2 in SSLv2 ClientHello test"); + +#Test 7: Sanity check ClientHello fragmentation. This isn't really an SSLv2 test +# at all, but it gives us confidence that Test 8 fails for the right +# reasons +$sslv2testtype = FRAGMENTED_IN_TLSV1_2; +$proxy->clear(); +$proxy->serverflags("-tls1_2"); +$proxy->clientflags("-no_tls1_3"); +$proxy->ciphers("AES128-SHA:\@SECLEVEL=0"); +$proxy->start(); +ok(TLSProxy::Message->success(), "Fragmented ClientHello in TLSv1.2 test"); + +#Test 8: Fragment a TLSv1.2 ClientHello across a TLS1.2 record; an SSLv2 +# record; and another TLS1.2 record. This isn't allowed so should fail +$sslv2testtype = FRAGMENTED_IN_SSLV2; +$proxy->clear(); +$proxy->serverflags("-tls1_2"); +$proxy->clientflags("-no_tls1_3"); +$proxy->ciphers("AES128-SHA:\@SECLEVEL=0"); +$proxy->start(); +ok(TLSProxy::Message->fail(), "Fragmented ClientHello in TLSv1.2/SSLv2 test"); + +#Test 9: Send a TLS warning alert before an SSLv2 ClientHello. This should +# fail because an SSLv2 ClientHello must be the first record. +$sslv2testtype = ALERT_BEFORE_SSLV2; +$proxy->clear(); +$proxy->serverflags("-tls1_2"); +$proxy->clientflags("-no_tls1_3"); +$proxy->ciphers("AES128-SHA:\@SECLEVEL=0"); +$proxy->start(); +ok(TLSProxy::Message->fail(), "Alert before SSLv2 ClientHello test"); + +#Unrecognised record type tests + +#Test 10: Sending an unrecognised record type in TLS1.2 should fail +$fatal_alert = 0; +$proxy->clear(); +$proxy->serverflags("-tls1_2"); +$proxy->clientflags("-no_tls1_3"); +$proxy->filter(\&add_unknown_record_type); +$proxy->start(); +ok($fatal_alert, "Unrecognised record type in TLS1.2"); + +SKIP: { + skip "TLSv1.1 disabled", 1 if disabled("tls1_1"); + + #Test 11: Sending an unrecognised record type in TLS1.1 should fail + $fatal_alert = 0; + $proxy->clear(); + $proxy->clientflags("-tls1_1 -cipher DEFAULT:\@SECLEVEL=0"); + $proxy->ciphers("AES128-SHA:\@SECLEVEL=0"); + $proxy->start(); + ok($fatal_alert, "Unrecognised record type in TLS1.1"); +} + +#Test 12: Sending a different record version in TLS1.2 should fail +$fatal_alert = 0; +$proxy->clear(); +$proxy->clientflags("-tls1_2"); +$proxy->filter(\&change_version); +$proxy->start(); +ok($fatal_alert, "Changed record version in TLS1.2"); + +#TLS1.3 specific tests +SKIP: { + skip "TLSv1.3 disabled", 8 + if disabled("tls1_3") || (disabled("ec") && disabled("dh")); + + #Test 13: Sending a different record version in TLS1.3 should fail + $proxy->clear(); + $proxy->filter(\&change_version); + $proxy->start(); + ok(TLSProxy::Message->fail(), "Changed record version in TLS1.3"); + + #Test 14: Sending an unrecognised record type in TLS1.3 should fail + $fatal_alert = 0; + $proxy->clear(); + $proxy->filter(\&add_unknown_record_type); + $proxy->start(); + ok($fatal_alert, "Unrecognised record type in TLS1.3"); + + #Test 15: Sending an outer record type other than app data once encrypted + #should fail + $fatal_alert = 0; + $proxy->clear(); + $proxy->filter(\&change_outer_record_type); + $proxy->start(); + ok($fatal_alert, "Wrong outer record type in TLS1.3"); + + use constant { + DATA_AFTER_SERVER_HELLO => 0, + DATA_AFTER_FINISHED => 1, + DATA_AFTER_KEY_UPDATE => 2, + DATA_BETWEEN_KEY_UPDATE => 3, + NO_DATA_BETWEEN_KEY_UPDATE => 4, + }; + + #Test 16: Sending a ServerHello which doesn't end on a record boundary + # should fail + $fatal_alert = 0; + $proxy->clear(); + $boundary_test_type = DATA_AFTER_SERVER_HELLO; + $proxy->filter(\¬_on_record_boundary); + $proxy->start(); + ok($fatal_alert, "Record not on boundary in TLS1.3 (ServerHello)"); + + #Test 17: Sending a Finished which doesn't end on a record boundary + # should fail + $fatal_alert = 0; + $proxy->clear(); + $boundary_test_type = DATA_AFTER_FINISHED; + $proxy->start(); + ok($fatal_alert, "Record not on boundary in TLS1.3 (Finished)"); + + #Test 18: Sending a KeyUpdate which doesn't end on a record boundary + # should fail + $fatal_alert = 0; + $proxy->clear(); + $boundary_test_type = DATA_AFTER_KEY_UPDATE; + $proxy->start(); + ok($fatal_alert, "Record not on boundary in TLS1.3 (KeyUpdate)"); + + #Test 19: Sending application data in the middle of a fragmented KeyUpdate + # should fail. Strictly speaking this is not a record boundary test + # but we use the same filter. + $fatal_alert = 0; + $proxy->clear(); + $boundary_test_type = DATA_BETWEEN_KEY_UPDATE; + $proxy->start(); + ok($fatal_alert, "Data between KeyUpdate"); + + #Test 20: Fragmented KeyUpdate. This should succeed. Strictly speaking this + # is not a record boundary test but we use the same filter. + $proxy->clear(); + $boundary_test_type = NO_DATA_BETWEEN_KEY_UPDATE; + $proxy->start(); + ok(TLSProxy::Message->success(), "No data between KeyUpdate"); + } + + +sub add_empty_recs_filter +{ + my $proxy = shift; + my $records = $proxy->record_list; + + # We're only interested in the initial ClientHello + if ($proxy->flight != 0) { + $fatal_alert = 1 if @{$records}[-1]->is_fatal_alert(1) == 10; + return; + } + + for (my $i = 0; $i < $inject_recs_num; $i++) { + my $record = TLSProxy::Record->new( + 0, + $content_type, + TLSProxy::Record::VERS_TLS_1_2, + 0, + 0, + 0, + 0, + "", + "" + ); + push @{$records}, $record; + } +} + +sub add_frag_alert_filter +{ + my $proxy = shift; + my $records = $proxy->record_list; + my $byte; + + # We're only interested in the initial ClientHello + if ($proxy->flight != 0) { + $fatal_alert = 1 if @{$records}[-1]->is_fatal_alert(1) == 10; + return; + } + + # Add a zero length fragment first + #my $record = TLSProxy::Record->new( + # 0, + # TLSProxy::Record::RT_ALERT, + # TLSProxy::Record::VERS_TLS_1_2, + # 0, + # 0, + # 0, + # "", + # "" + #); + #push @{$proxy->record_list}, $record; + + # Now add the alert level (Fatal) as a separate record + $byte = pack('C', TLSProxy::Message::AL_LEVEL_FATAL); + my $record = TLSProxy::Record->new( + 0, + TLSProxy::Record::RT_ALERT, + TLSProxy::Record::VERS_TLS_1_2, + 1, + 0, + 1, + 1, + $byte, + $byte + ); + push @{$records}, $record; + + # And finally the description (Unexpected message) in a third record + $byte = pack('C', TLSProxy::Message::AL_DESC_UNEXPECTED_MESSAGE); + $record = TLSProxy::Record->new( + 0, + TLSProxy::Record::RT_ALERT, + TLSProxy::Record::VERS_TLS_1_2, + 1, + 0, + 1, + 1, + $byte, + $byte + ); + push @{$records}, $record; +} + +sub add_sslv2_filter +{ + my $proxy = shift; + my $clienthello; + my $record; + + # We're only interested in the initial ClientHello + if ($proxy->flight != 0) { + return; + } + + # Ditch the real ClientHello - we're going to replace it with our own + shift @{$proxy->record_list}; + + if ($sslv2testtype == ALERT_BEFORE_SSLV2) { + my $alert = pack('CC', TLSProxy::Message::AL_LEVEL_FATAL, + TLSProxy::Message::AL_DESC_NO_RENEGOTIATION); + my $alertlen = length $alert; + $record = TLSProxy::Record->new( + 0, + TLSProxy::Record::RT_ALERT, + TLSProxy::Record::VERS_TLS_1_2, + $alertlen, + 0, + $alertlen, + $alertlen, + $alert, + $alert + ); + + push @{$proxy->record_list}, $record; + } + + if ($sslv2testtype == ALERT_BEFORE_SSLV2 + || $sslv2testtype == TLSV1_2_IN_SSLV2 + || $sslv2testtype == SSLV2_IN_SSLV2) { + # This is an SSLv2 format ClientHello + $clienthello = + pack "C44", + 0x01, # ClientHello + 0x03, 0x03, #TLSv1.2 + 0x00, 0x03, # Ciphersuites len + 0x00, 0x00, # Session id len + 0x00, 0x20, # Challenge len + 0x00, 0x00, 0x2f, #AES128-SHA + 0x01, 0x18, 0x9F, 0x76, 0xEC, 0x57, 0xCE, 0xE5, 0xB3, 0xAB, 0x79, 0x90, + 0xAD, 0xAC, 0x6E, 0xD1, 0x58, 0x35, 0x03, 0x97, 0x16, 0x10, 0x82, 0x56, + 0xD8, 0x55, 0xFF, 0xE1, 0x8A, 0xA3, 0x2E, 0xF6; # Challenge + + if ($sslv2testtype == SSLV2_IN_SSLV2) { + # Set the version to "real" SSLv2 + vec($clienthello, 1, 8) = 0x00; + vec($clienthello, 2, 8) = 0x02; + } + + my $chlen = length $clienthello; + + $record = TLSProxy::Record->new( + 0, + TLSProxy::Record::RT_HANDSHAKE, + TLSProxy::Record::VERS_TLS_1_2, + $chlen, + 1, #SSLv2 + $chlen, + $chlen, + $clienthello, + $clienthello + ); + + push @{$proxy->record_list}, $record; + } else { + # For this test we're using a real TLS ClientHello + $clienthello = + pack "C49", + 0x01, # ClientHello + 0x00, 0x00, 0x2D, # Message length + 0x03, 0x03, # TLSv1.2 + 0x01, 0x18, 0x9F, 0x76, 0xEC, 0x57, 0xCE, 0xE5, 0xB3, 0xAB, 0x79, 0x90, + 0xAD, 0xAC, 0x6E, 0xD1, 0x58, 0x35, 0x03, 0x97, 0x16, 0x10, 0x82, 0x56, + 0xD8, 0x55, 0xFF, 0xE1, 0x8A, 0xA3, 0x2E, 0xF6, # Random + 0x00, # Session id len + 0x00, 0x04, # Ciphersuites len + 0x00, 0x2f, # AES128-SHA + 0x00, 0xff, # Empty reneg info SCSV + 0x01, # Compression methods len + 0x00, # Null compression + 0x00, 0x00; # Extensions len + + # Split this into 3: A TLS record; a SSLv2 record and a TLS record. + # We deliberately split the second record prior to the Challenge/Random + # and set the first byte of the random to 1. This makes the second SSLv2 + # record look like an SSLv2 ClientHello + my $frag1 = substr $clienthello, 0, 6; + my $frag2 = substr $clienthello, 6, 32; + my $frag3 = substr $clienthello, 38; + + my $fraglen = length $frag1; + $record = TLSProxy::Record->new( + 0, + TLSProxy::Record::RT_HANDSHAKE, + TLSProxy::Record::VERS_TLS_1_2, + $fraglen, + 0, + $fraglen, + $fraglen, + $frag1, + $frag1 + ); + push @{$proxy->record_list}, $record; + + $fraglen = length $frag2; + my $recvers; + if ($sslv2testtype == FRAGMENTED_IN_SSLV2) { + $recvers = 1; + } else { + $recvers = 0; + } + $record = TLSProxy::Record->new( + 0, + TLSProxy::Record::RT_HANDSHAKE, + TLSProxy::Record::VERS_TLS_1_2, + $fraglen, + $recvers, + $fraglen, + $fraglen, + $frag2, + $frag2 + ); + push @{$proxy->record_list}, $record; + + $fraglen = length $frag3; + $record = TLSProxy::Record->new( + 0, + TLSProxy::Record::RT_HANDSHAKE, + TLSProxy::Record::VERS_TLS_1_2, + $fraglen, + 0, + $fraglen, + $fraglen, + $frag3, + $frag3 + ); + push @{$proxy->record_list}, $record; + } + +} + +sub add_unknown_record_type +{ + my $proxy = shift; + my $records = $proxy->record_list; + state $added_record; + + # We'll change a record after the initial version neg has taken place + if ($proxy->flight == 0) { + $added_record = 0; + return; + } elsif ($proxy->flight != 1 || $added_record) { + $fatal_alert = 1 if @{$records}[-1]->is_fatal_alert(0) == 10; + return; + } + + my $record = TLSProxy::Record->new( + 1, + TLSProxy::Record::RT_UNKNOWN, + @{$records}[-1]->version(), + 1, + 0, + 1, + 1, + "X", + "X" + ); + + #Find ServerHello record and insert after that + my $i; + for ($i = 0; ${$proxy->record_list}[$i]->flight() < 1; $i++) { + next; + } + $i++; + + splice @{$proxy->record_list}, $i, 0, $record; + $added_record = 1; +} + +sub change_version +{ + my $proxy = shift; + my $records = $proxy->record_list; + + # We'll change a version after the initial version neg has taken place + if ($proxy->flight != 1) { + $fatal_alert = 1 if @{$records}[-1]->is_fatal_alert(0) == 70; + return; + } + + if ($#{$records} > 1) { + # ... typically in ServerHelloDone + @{$records}[-1]->version(TLSProxy::Record::VERS_TLS_1_1); + } +} + +sub change_outer_record_type +{ + my $proxy = shift; + my $records = $proxy->record_list; + + # We'll change a record after the initial version neg has taken place + if ($proxy->flight != 1) { + $fatal_alert = 1 if @{$records}[-1]->is_fatal_alert(0) == 10; + return; + } + + # Find CCS record and change record after that + my $i = 0; + foreach my $record (@{$records}) { + last if $record->content_type == TLSProxy::Record::RT_CCS; + $i++; + } + if (defined(${$records}[++$i])) { + ${$records}[$i]->outer_content_type(TLSProxy::Record::RT_HANDSHAKE); + } +} + +sub not_on_record_boundary +{ + my $proxy = shift; + my $records = $proxy->record_list; + my $data; + + #Find server's first flight + if ($proxy->flight != 1) { + $fatal_alert = 1 if @{$records}[-1]->is_fatal_alert(0) == 10; + return; + } + + if ($boundary_test_type == DATA_AFTER_SERVER_HELLO) { + #Merge the ServerHello and EncryptedExtensions records into one + my $i = 0; + foreach my $record (@{$records}) { + if ($record->content_type == TLSProxy::Record::RT_HANDSHAKE) { + $record->{sent} = 1; # pretend it's sent already + last; + } + $i++; + } + + if (defined(${$records}[$i+1])) { + $data = ${$records}[$i]->data(); + $data .= ${$records}[$i+1]->decrypt_data(); + ${$records}[$i+1]->data($data); + ${$records}[$i+1]->len(length $data); + + #Delete the old ServerHello record + splice @{$records}, $i, 1; + } + } elsif ($boundary_test_type == DATA_AFTER_FINISHED) { + return if @{$proxy->{message_list}}[-1]->{mt} + != TLSProxy::Message::MT_FINISHED; + + my $last_record = @{$records}[-1]; + $data = $last_record->decrypt_data; + + #Add a KeyUpdate message onto the end of the Finished record + my $keyupdate = pack "C5", + 0x18, # KeyUpdate + 0x00, 0x00, 0x01, # Message length + 0x00; # Update not requested + + $data .= $keyupdate; + + #Add content type and tag + $data .= pack("C", TLSProxy::Record::RT_HANDSHAKE).("\0"x16); + + #Update the record + $last_record->data($data); + $last_record->len(length $data); + } elsif ($boundary_test_type == DATA_AFTER_KEY_UPDATE) { + return if @{$proxy->{message_list}}[-1]->{mt} + != TLSProxy::Message::MT_FINISHED; + + #KeyUpdates must end on a record boundary + + my $record = TLSProxy::Record->new( + 1, + TLSProxy::Record::RT_APPLICATION_DATA, + TLSProxy::Record::VERS_TLS_1_2, + 0, + 0, + 0, + 0, + "", + "" + ); + + #Add two KeyUpdate messages into a single record + my $keyupdate = pack "C5", + 0x18, # KeyUpdate + 0x00, 0x00, 0x01, # Message length + 0x00; # Update not requested + + $data = $keyupdate.$keyupdate; + + #Add content type and tag + $data .= pack("C", TLSProxy::Record::RT_HANDSHAKE).("\0"x16); + + $record->data($data); + $record->len(length $data); + push @{$records}, $record; + } else { + return if @{$proxy->{message_list}}[-1]->{mt} + != TLSProxy::Message::MT_FINISHED; + + my $record = TLSProxy::Record->new( + 1, + TLSProxy::Record::RT_APPLICATION_DATA, + TLSProxy::Record::VERS_TLS_1_2, + 0, + 0, + 0, + 0, + "", + "" + ); + + #Add a partial KeyUpdate message into the record + $data = pack "C1", + 0x18; # KeyUpdate message type. Omit the rest of the message header + + #Add content type and tag + $data .= pack("C", TLSProxy::Record::RT_HANDSHAKE).("\0"x16); + + $record->data($data); + $record->len(length $data); + push @{$records}, $record; + + if ($boundary_test_type == DATA_BETWEEN_KEY_UPDATE) { + #Now add an app data record + $record = TLSProxy::Record->new( + 1, + TLSProxy::Record::RT_APPLICATION_DATA, + TLSProxy::Record::VERS_TLS_1_2, + 0, + 0, + 0, + 0, + "", + "" + ); + + #Add an empty app data record (just content type and tag) + $data = pack("C", TLSProxy::Record::RT_APPLICATION_DATA).("\0"x16); + + $record->data($data); + $record->len(length $data); + push @{$records}, $record; + } + + #Now add the rest of the KeyUpdate message + $record = TLSProxy::Record->new( + 1, + TLSProxy::Record::RT_APPLICATION_DATA, + TLSProxy::Record::VERS_TLS_1_2, + 0, + 0, + 0, + 0, + "", + "" + ); + + #Add the last 4 bytes of the KeyUpdate record + $data = pack "C4", + 0x00, 0x00, 0x01, # Message length + 0x00; # Update not requested + + #Add content type and tag + $data .= pack("C", TLSProxy::Record::RT_HANDSHAKE).("\0"x16); + + $record->data($data); + $record->len(length $data); + push @{$records}, $record; + + } +} |