Add some SSLv2 ClientHello tests
authorMatt Caswell <matt@openssl.org>
Tue, 2 Aug 2016 16:24:54 +0000 (17:24 +0100)
committerMatt Caswell <matt@openssl.org>
Mon, 15 Aug 2016 22:14:30 +0000 (23:14 +0100)
Test that we handle a TLS ClientHello in an SSLv2 record correctly.

Reviewed-by: Tim Hudson <tjh@openssl.org>
test/recipes/70-test_sslrecords.t
util/TLSProxy/Message.pm
util/TLSProxy/Record.pm
util/TLSProxy/ServerHello.pm

index 0ae018a..8af236f 100644 (file)
@@ -38,7 +38,7 @@ my $proxy = TLSProxy::Proxy->new(
 my $content_type = TLSProxy::Record::RT_APPLICATION_DATA;
 my $inject_recs_num = 1;
 $proxy->start() or plan skip_all => "Unable to start up Proxy for tests";
-plan tests => 4;
+plan tests => 9;
 ok(TLSProxy::Message->fail(), "Out of context empty records test");
 
 #Test 2: Injecting in context empty records should succeed
@@ -62,6 +62,52 @@ $proxy->filter(\&add_frag_alert_filter);
 $proxy->start();
 ok(!TLSProxy::Message->end(), "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
+};
+#Test 5: Inject an SSLv2 style record format for a TLSv1.2 ClientHello
+#my $sslv2testtype = TLSV1_2_IN_SSLV2;
+my $sslv2testtype = TLSV1_2_IN_SSLV2;
+$proxy->clear();
+$proxy->filter(\&add_sslv2_filter);
+$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->start();
+ok(!TLSProxy::Message->end(), "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->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->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->start();
+ok(TLSProxy::Message->fail(), "Alert before SSLv2 ClientHello test");
 sub add_empty_recs_filter
 {
     my $proxy = shift;
@@ -79,6 +125,7 @@ sub add_empty_recs_filter
             0,
             0,
             0,
+            0,
             "",
             ""
         );
@@ -117,6 +164,7 @@ sub add_frag_alert_filter
         TLSProxy::Record::RT_ALERT,
         TLSProxy::Record::VERS_TLS_1_2,
         1,
+        0,
         1,
         1,
         $byte,
@@ -131,6 +179,7 @@ sub add_frag_alert_filter
         TLSProxy::Record::RT_ALERT,
         TLSProxy::Record::VERS_TLS_1_2,
         1,
+        0,
         1,
         1,
         $byte,
@@ -138,3 +187,150 @@ sub add_frag_alert_filter
     );
     push @{$proxy->record_list}, $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;
+    }
+
+}
index b8db22f..321e080 100644 (file)
@@ -37,7 +37,8 @@ use constant {
 #Alert descriptions
 use constant {
     AL_DESC_CLOSE_NOTIFY => 0,
-    AL_DESC_UNEXPECTED_MESSAGE => 10
+    AL_DESC_UNEXPECTED_MESSAGE => 10,
+    AL_DESC_NO_RENEGOTIATION => 100
 };
 
 my %message_type = (
index 2a605e3..423bad3 100644 (file)
@@ -98,6 +98,7 @@ sub get_records
                 $content_type,
                 $version,
                 $len,
+                0,
                 $len_real,
                 $decrypt_len,
                 substr($packet, TLS_RECORD_HEADER_LENGTH, $len_real),
@@ -167,6 +168,7 @@ sub new
         $content_type,
         $version,
         $len,
+        $sslv2,
         $len_real,
         $decrypt_len,
         $data,
@@ -177,6 +179,7 @@ sub new
         content_type => $content_type,
         version => $version,
         len => $len,
+        sslv2 => $sslv2,
         len_real => $len_real,
         decrypt_len => $decrypt_len,
         data => $data,
@@ -247,7 +250,11 @@ sub reconstruct_record
     my $self = shift;
     my $data;
 
-    $data = pack('Cnn', $self->content_type, $self->version, $self->len);
+    if ($self->sslv2) {
+        $data = pack('n', $self->len | 0x8000);
+    } else {
+        $data = pack('Cnn', $self->content_type, $self->version, $self->len);
+    }
     $data .= $self->data;
 
     return $data;
@@ -269,6 +276,11 @@ sub version
     my $self = shift;
     return $self->{version};
 }
+sub sslv2
+{
+    my $self = shift;
+    return $self->{sslv2};
+}
 sub len_real
 {
     my $self = shift;
index ee2fd72..79a8be9 100644 (file)
@@ -56,13 +56,25 @@ sub parse
     my $comp_meth = unpack('C', substr($self->data, $ptr));
     $ptr++;
     my $extensions_len = unpack('n', substr($self->data, $ptr));
-    $ptr += 2;
+    if (!defined $extensions_len) {
+        $extensions_len = 0;
+    } else {
+        $ptr += 2;
+    }
     #For now we just deal with this as a block of data. In the future we will
     #want to parse this
-    my $extension_data = substr($self->data, $ptr);
+    my $extension_data;
+    if ($extensions_len != 0) {
+        $extension_data = substr($self->data, $ptr);
     
-    if (length($extension_data) != $extensions_len) {
-        die "Invalid extension length\n";
+        if (length($extension_data) != $extensions_len) {
+            die "Invalid extension length\n";
+        }
+    } else {
+        if (length($self->data) != $ptr) {
+            die "Invalid extension length\n";
+        }
+        $extension_data = "";
     }
     my %extensions = ();
     while (length($extension_data) >= 4) {