]> git.ipfire.org Git - thirdparty/gnutls.git/commitdiff
record: ignore any ChangeCipherSpec messages under TLS1.3 handshake
authorNikos Mavrogiannopoulos <nmav@redhat.com>
Thu, 22 Feb 2018 15:12:55 +0000 (16:12 +0100)
committerNikos Mavrogiannopoulos <nmav@redhat.com>
Thu, 8 Mar 2018 11:53:57 +0000 (12:53 +0100)
Also send ChangeCipherSpec messages under TLS1.3 handshake.

This is a draft-ietf-tls-tls13-22 change.

Resolves #395

Signed-off-by: Nikos Mavrogiannopoulos <nmav@redhat.com>
lib/gnutls_int.h
lib/handshake-tls13.c
lib/handshake.c
lib/handshake.h
lib/record.c
tests/Makefile.am
tests/tls13/change_cipher_spec.c [new file with mode: 0644]

index baa9c14589383372159bc3d61797fd0cc2238cb0..c4d8524a278a515b1c4ea745240a0b93d9b58080 100644 (file)
@@ -134,6 +134,9 @@ typedef struct {
 #define GNUTLS_MASTER_SIZE 48
 #define GNUTLS_RANDOM_SIZE 32
 
+/* Enable: Appendix D4.  Middlebox Compatibility Mode */
+#define TLS13_APPENDIX_D4 1
+
 /* DTLS */
 #define DTLS_RETRANS_TIMEOUT 1000
 
@@ -254,9 +257,10 @@ typedef enum handshake_state_t { STATE0 = 0, STATE1, STATE2,
        STATE15, STATE16, STATE17, STATE18, STATE19,
        STATE20 = 20, STATE21, STATE22,
        STATE30 = 30, STATE31, STATE40 = 40, STATE41, STATE50 = 50,
-       STATE90=90, STATE91, STATE92, STATE93,
+       STATE90=90, STATE91, STATE92, STATE93, STATE99=99,
        STATE100=100, STATE101, STATE102, STATE103, STATE104,
        STATE105, STATE106, STATE107, STATE108, STATE109, STATE110,
+       STATE111,
        STATE150 /* key update */
 } handshake_state_t;
 
index 721f334eca938091c4cbdecfd33d571aa59f7969..edb6e8057466d5c72da0faeb502f3f8a0b5d435b 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2017 Red Hat, Inc.
+ * Copyright (C) 2017-2018 Red Hat, Inc.
  *
  * Author: Nikos Mavrogiannopoulos
  *
@@ -78,62 +78,72 @@ int _gnutls13_handshake_client(gnutls_session_t session)
        int ret = 0;
 
        switch (STATE) {
+       case STATE99:
        case STATE100:
+#ifdef TLS13_APPENDIX_D4
+               /* We send it before keys are generated. That works because CCS
+                * is always being cached and queued and not being sent directly */
+               ret = _gnutls_send_change_cipher_spec(session, AGAIN(STATE100));
+               STATE = STATE100;
+               IMED_RET("send change cipher spec", ret, 0);
+#endif
+               /* fall through */
+       case STATE101:
                ret =
                    generate_hs_traffic_keys(session);
-               STATE = STATE100;
+               STATE = STATE101;
                IMED_RET("generate session keys", ret, 0);
                /* fall through */
-       case STATE101:
+       case STATE102:
                ret = _gnutls13_recv_encrypted_extensions(session);
-               STATE = STATE101;
+               STATE = STATE102;
                IMED_RET("recv encrypted extensions", ret, 0);
                /* fall through */
-       case STATE102:
+       case STATE103:
                ret = _gnutls13_recv_certificate_request(session);
-               STATE = STATE102;
+               STATE = STATE103;
                IMED_RET("recv certificate request", ret, 0);
                /* fall through */
-       case STATE103:
+       case STATE104:
                ret = _gnutls13_recv_certificate(session);
-               STATE = STATE103;
+               STATE = STATE104;
                IMED_RET("recv certificate", ret, 0);
                /* fall through */
-       case STATE104:
+       case STATE105:
                ret = _gnutls13_recv_certificate_verify(session);
-               STATE = STATE104;
+               STATE = STATE105;
                IMED_RET("recv server certificate verify", ret, 0);
                /* fall through */
-       case STATE105:
+       case STATE106:
                ret = _gnutls_run_verify_callback(session, GNUTLS_CLIENT);
-               STATE = STATE105;
+               STATE = STATE106;
                if (ret < 0)
                        return gnutls_assert_val(ret);
                FALLTHROUGH;
-       case STATE106:
-               ret = _gnutls13_recv_finished(session);
-               STATE = STATE106;
-               IMED_RET("recv finished", ret, 0);
-               /* fall through */
        case STATE107:
-               ret = _gnutls13_send_certificate(session, AGAIN(STATE107));
+               ret = _gnutls13_recv_finished(session);
                STATE = STATE107;
-               IMED_RET("send certificate", ret, 0);
+               IMED_RET("recv finished", ret, 0);
                /* fall through */
        case STATE108:
-               ret = _gnutls13_send_certificate_verify(session, AGAIN(STATE108));
+               ret = _gnutls13_send_certificate(session, AGAIN(STATE108));
                STATE = STATE108;
-               IMED_RET("send certificate verify", ret, 0);
+               IMED_RET("send certificate", ret, 0);
                /* fall through */
        case STATE109:
-               ret = _gnutls13_send_finished(session, AGAIN(STATE109));
+               ret = _gnutls13_send_certificate_verify(session, AGAIN(STATE109));
                STATE = STATE109;
-               IMED_RET("send finished", ret, 0);
+               IMED_RET("send certificate verify", ret, 0);
                /* fall through */
        case STATE110:
+               ret = _gnutls13_send_finished(session, AGAIN(STATE110));
+               STATE = STATE110;
+               IMED_RET("send finished", ret, 0);
+               /* fall through */
+       case STATE111:
                ret =
                    generate_ap_traffic_keys(session);
-               STATE = STATE110;
+               STATE = STATE111;
                IMED_RET("generate app keys", ret, 0);
 
                STATE = STATE0;
@@ -243,7 +253,7 @@ int _gnutls13_handshake_server(gnutls_session_t session)
                        /* this is triggered by post_client_hello, and instructs the
                         * handshake to proceed but be put on hold */
                        ret = GNUTLS_E_INTERRUPTED;
-                       STATE = STATE100; /* hello already parsed -> move on */
+                       STATE = STATE99; /* hello already parsed -> move on */
                } else {
                        STATE = STATE92;
                }
@@ -255,62 +265,70 @@ int _gnutls13_handshake_server(gnutls_session_t session)
                STATE = STATE93;
                IMED_RET("send hello", ret, 0);
                /* fall through */
+       case STATE99:
        case STATE100:
-               ret =
-                   generate_hs_traffic_keys(session);
+#ifdef TLS13_APPENDIX_D4
+               ret = _gnutls_send_change_cipher_spec(session, AGAIN(STATE100));
                STATE = STATE100;
-               IMED_RET("generate session keys", ret, 0);
+               IMED_RET("send change cipher spec", ret, 0);
+#endif
                /* fall through */
        case STATE101:
-               ret = _gnutls13_send_encrypted_extensions(session, AGAIN(STATE101));
+               ret =
+                   generate_hs_traffic_keys(session);
                STATE = STATE101;
-               IMED_RET("send encrypted extensions", ret, 0);
+               IMED_RET("generate session keys", ret, 0);
                /* fall through */
        case STATE102:
-               ret = _gnutls13_send_certificate_request(session, AGAIN(STATE102));
+               ret = _gnutls13_send_encrypted_extensions(session, AGAIN(STATE102));
                STATE = STATE102;
-               IMED_RET("send certificate request", ret, 0);
+               IMED_RET("send encrypted extensions", ret, 0);
                /* fall through */
        case STATE103:
-               ret = _gnutls13_send_certificate(session, AGAIN(STATE103));
+               ret = _gnutls13_send_certificate_request(session, AGAIN(STATE103));
                STATE = STATE103;
-               IMED_RET("send certificate", ret, 0);
+               IMED_RET("send certificate request", ret, 0);
                /* fall through */
        case STATE104:
-               ret = _gnutls13_send_certificate_verify(session, AGAIN(STATE104));
+               ret = _gnutls13_send_certificate(session, AGAIN(STATE104));
                STATE = STATE104;
-               IMED_RET("send certificate verify", ret, 0);
+               IMED_RET("send certificate", ret, 0);
                /* fall through */
        case STATE105:
-               ret = _gnutls13_send_finished(session, AGAIN(STATE105));
+               ret = _gnutls13_send_certificate_verify(session, AGAIN(STATE105));
                STATE = STATE105;
-               IMED_RET("send finished", ret, 0);
+               IMED_RET("send certificate verify", ret, 0);
                /* fall through */
        case STATE106:
-               ret = _gnutls13_recv_certificate(session);
+               ret = _gnutls13_send_finished(session, AGAIN(STATE106));
                STATE = STATE106;
-               IMED_RET("recv certificate", ret, 0);
+               IMED_RET("send finished", ret, 0);
                /* fall through */
        case STATE107:
-               ret = _gnutls13_recv_certificate_verify(session);
+               ret = _gnutls13_recv_certificate(session);
                STATE = STATE107;
-               IMED_RET("recv certificate verify", ret, 0);
+               IMED_RET("recv certificate", ret, 0);
                /* fall through */
        case STATE108:
-               ret = _gnutls_run_verify_callback(session, GNUTLS_CLIENT);
+               ret = _gnutls13_recv_certificate_verify(session);
                STATE = STATE108;
+               IMED_RET("recv certificate verify", ret, 0);
+               /* fall through */
+       case STATE109:
+               ret = _gnutls_run_verify_callback(session, GNUTLS_CLIENT);
+               STATE = STATE109;
                if (ret < 0)
                        return gnutls_assert_val(ret);
                /* fall through */
-       case STATE109:
+       case STATE110:
                ret = _gnutls13_recv_finished(session);
-               STATE = STATE109;
+               STATE = STATE110;
                IMED_RET("recv finished", ret, 0);
                /* fall through */
-       case STATE110:
+       case STATE111:
                ret =
                    generate_ap_traffic_keys(session);
-               STATE = STATE110;
+               STATE = STATE111;
                IMED_RET("generate app keys", ret, 0);
 
                STATE = STATE0;
index 179fcb8009b8c495dbb468b3804642af839bc169..9b3fe6f648547094cda8737be313f319c771d913 100644 (file)
@@ -1,6 +1,6 @@
 /*
  * Copyright (C) 2000-2016 Free Software Foundation, Inc.
- * Copyright (C) 2015-2017 Red Hat, Inc.
+ * Copyright (C) 2015-2018 Red Hat, Inc.
  *
  * Author: Nikos Mavrogiannopoulos
  *
@@ -1068,9 +1068,22 @@ inline
                if ((session->internals.h_type == type
                     || session->internals.h_type == GNUTLS_HANDSHAKE_ANY)
                    && (session->internals.h_post == post
-                       || session->internals.h_post == GNUTLS_HOOK_BOTH))
+                       || session->internals.h_post == GNUTLS_HOOK_BOTH)) {
+
+                       /* internal API for testing: when we are expected to
+                        * wait for GNUTLS_HANDSHAKE_CHANGE_CIPHER_SPEC, we
+                        * do so, but not when doing for all messages. The
+                        * reason is that change cipher specs are not handshake
+                        * messages, and we don't support waiting for them
+                        * consistently (only sending is tracked, not receiving).
+                        */
+                       if (type == GNUTLS_HANDSHAKE_CHANGE_CIPHER_SPEC &&
+                           session->internals.h_type != GNUTLS_HANDSHAKE_CHANGE_CIPHER_SPEC)
+                               return 0;
+
                        return session->internals.h_hook(session, type,
                                                         post, incoming, &msg);
+               }
        }
        return 0;
 }
@@ -1553,7 +1566,7 @@ set_client_ciphersuite(gnutls_session_t session, uint8_t suite[2])
  */
 static int
 client_check_if_resuming(gnutls_session_t session,
-                                uint8_t * session_id, int session_id_len)
+                        uint8_t * session_id, int session_id_len)
 {
        char buf[2 * GNUTLS_MAX_SESSION_ID_SIZE + 1];
        int ret;
@@ -2556,7 +2569,7 @@ static int handshake_client(gnutls_session_t session)
        const version_entry_st *ver;
 
  reset:
-       if (STATE >= STATE100)
+       if (STATE >= STATE99)
                return _gnutls13_handshake_client(session);
 
        switch (STATE) {
@@ -2605,7 +2618,7 @@ static int handshake_client(gnutls_session_t session)
        case STATE4:
                ver = get_version(session);
                if (ver->tls13_sem) { /* TLS 1.3 state machine */
-                       STATE = STATE100;
+                       STATE = STATE99;
                        goto reset;
                }
 
@@ -2778,7 +2791,7 @@ static int handshake_client(gnutls_session_t session)
 /* This function is to be called if the handshake was successfully 
  * completed. This sends a Change Cipher Spec packet to the peer.
  */
-static ssize_t send_change_cipher_spec(gnutls_session_t session, int again)
+ssize_t _gnutls_send_change_cipher_spec(gnutls_session_t session, int again)
 {
        uint8_t *data;
        mbuffer_st *bufel;
@@ -2786,7 +2799,7 @@ static ssize_t send_change_cipher_spec(gnutls_session_t session, int again)
        const version_entry_st *vers;
 
        if (again == 0) {
-               bufel = _gnutls_handshake_alloc(session, 1);
+               bufel = _gnutls_handshake_alloc(session, 3); /* max for DTLS0.9 */
                if (bufel == NULL)
                        return gnutls_assert_val(GNUTLS_E_MEMORY_ERROR);
 
@@ -2809,6 +2822,13 @@ static ssize_t send_change_cipher_spec(gnutls_session_t session, int again)
                        session->internals.dtls.hsk_write_seq++;
                }
 
+               ret = call_hook_func(session, GNUTLS_HANDSHAKE_CHANGE_CIPHER_SPEC, GNUTLS_HOOK_PRE, 0,
+                                    data, 1);
+               if (ret < 0) {
+                       _mbuffer_xfree(&bufel);
+                       return gnutls_assert_val(ret);
+               }
+
                ret =
                    _gnutls_handshake_io_cache_int(session,
                                                   GNUTLS_HANDSHAKE_CHANGE_CIPHER_SPEC,
@@ -2818,6 +2838,12 @@ static ssize_t send_change_cipher_spec(gnutls_session_t session, int again)
                        return gnutls_assert_val(ret);
                }
 
+               ret = call_hook_func(session, GNUTLS_HANDSHAKE_CHANGE_CIPHER_SPEC, GNUTLS_HOOK_POST, 0,
+                                    data, 1);
+               if (ret < 0) {
+                       return gnutls_assert_val(ret);
+               }
+
                _gnutls_handshake_log("REC[%p]: Sent ChangeCipherSpec\n",
                                      session);
        }
@@ -2836,7 +2862,7 @@ static int send_handshake_final(gnutls_session_t session, int init)
        switch (FINAL_STATE) {
        case STATE0:
        case STATE1:
-               ret = send_change_cipher_spec(session, FAGAIN(STATE1));
+               ret = _gnutls_send_change_cipher_spec(session, FAGAIN(STATE1));
                FINAL_STATE = STATE0;
 
                if (ret < 0) {
@@ -3021,7 +3047,7 @@ static int handshake_server(gnutls_session_t session)
 
                ver = get_version(session);
                if (ver->tls13_sem) { /* TLS 1.3 state machine */
-                       STATE = STATE100;
+                       STATE = STATE99;
                        goto reset;
                }
 
index 109f1247c8ee40236de82c24523d7d97679c81b3..0084789bcd6dee1c72094520b6e1cb1bc370555d 100644 (file)
@@ -64,6 +64,8 @@ int _gnutls_generate_session_id(uint8_t * session_id, uint8_t * len);
 int _gnutls_gen_server_random(gnutls_session_t session, int version);
 void _gnutls_set_client_random(gnutls_session_t session, uint8_t * rnd);
 
+ssize_t _gnutls_send_change_cipher_spec(gnutls_session_t session, int again);
+
 int _gnutls_send_server_hello(gnutls_session_t session, int again);
 
 int _gnutls_find_pk_algos_in_ciphersuites(uint8_t * data, int datalen);
index 20dff2b3a73e720b8c4a84bf4fe30c25daed01d5..a8ba45032dac82078eef72c6726d215b303bd4de 100644 (file)
@@ -1196,14 +1196,15 @@ _gnutls_recv_in_buffers(gnutls_session_t session, content_type_t type,
        mbuffer_st *bufel = NULL, *decrypted = NULL;
        gnutls_datum_t t;
        int ret;
-       unsigned int empty_fragments = 0;
+       unsigned int n_retries = 0;
        record_parameters_st *record_params;
        record_state_st *record_state;
        struct tls_record_st record;
+       const version_entry_st *vers = get_version(session);
 
     begin:
+ begin:
 
-       if (empty_fragments > DEFAULT_MAX_EMPTY_RECORDS) {
+       if (n_retries > DEFAULT_MAX_EMPTY_RECORDS) {
                gnutls_assert();
                return GNUTLS_E_TOO_MANY_EMPTY_PACKETS;
        }
@@ -1264,6 +1265,18 @@ _gnutls_recv_in_buffers(gnutls_session_t session, content_type_t type,
        if (bufel == NULL)
                return gnutls_assert_val(GNUTLS_E_INTERNAL_ERROR);
 
+       if (vers && vers->tls13_sem && record.type == GNUTLS_CHANGE_CIPHER_SPEC &&
+           record.length == 1 && session->internals.handshake_in_progress) {
+               _gnutls_read_log("discarding change cipher spec in TLS1.3\n");
+               /* we use the same mechanism to retry as when
+                * receiving multiple empty TLS packets */
+               bufel =
+                   _mbuffer_head_pop_first(&session->internals.
+                                           record_recv_buffer);
+               _mbuffer_xfree(&bufel);
+               n_retries++;
+               goto begin;
+       }
 
        /* We allocate the maximum possible to allow few compressed bytes to expand to a
         * full record. Moreover we add space for any pad and the MAC (in case
@@ -1363,7 +1376,7 @@ _gnutls_recv_in_buffers(gnutls_session_t session, content_type_t type,
  */
        if (_mbuffer_get_udata_size(decrypted) == 0) {
                _mbuffer_xfree(&decrypted);
-               empty_fragments++;
+               n_retries++;
                goto begin;
        }
 
@@ -1388,7 +1401,7 @@ _gnutls_recv_in_buffers(gnutls_session_t session, content_type_t type,
 
        return ret;
 
     discard:
+ discard:
        session->internals.dtls.packets_dropped++;
 
        /* discard the whole received fragment. */
@@ -1398,7 +1411,7 @@ _gnutls_recv_in_buffers(gnutls_session_t session, content_type_t type,
        _mbuffer_xfree(&bufel);
        return gnutls_assert_val(GNUTLS_E_AGAIN);
 
     sanity_check_error:
+ sanity_check_error:
        if (IS_DTLS(session)) {
                session->internals.dtls.packets_dropped++;
                ret = gnutls_assert_val(GNUTLS_E_AGAIN);
@@ -1408,11 +1421,11 @@ _gnutls_recv_in_buffers(gnutls_session_t session, content_type_t type,
        session_unresumable(session);
        session_invalidate(session);
 
     cleanup:
+ cleanup:
        _mbuffer_xfree(&decrypted);
        return ret;
 
     recv_error:
+ recv_error:
        if (ret < 0
            && (gnutls_error_is_fatal(ret) == 0
                || ret == GNUTLS_E_TIMEDOUT))
index 66766b632c22486bc3bc8f02191f501bacddbb3f..36a493cdc70af005af9dec7ce158137881544ad2 100644 (file)
@@ -112,6 +112,8 @@ ctests += tls13/multi-ocsp
 
 ctests += tls13/ocsp-client
 
+ctests += tls13/change_cipher_spec
+
 ctests += mini-record-2 simple gnutls_hmac_fast set_pkcs12_cred cert certuniqueid tls-neg-ext-key \
         mpi certificate_set_x509_crl dn parse_ca x509-dn x509-dn-decode record-sizes \
         hostname-check cve-2008-4989 pkcs12_s2k chainverify record-sizes-range \
diff --git a/tests/tls13/change_cipher_spec.c b/tests/tls13/change_cipher_spec.c
new file mode 100644 (file)
index 0000000..23519d9
--- /dev/null
@@ -0,0 +1,344 @@
+/*
+ * Copyright (C) 2017 Red Hat, Inc.
+ *
+ * Author: Nikos Mavrogiannopoulos
+ *
+ * This file is part of GnuTLS.
+ *
+ * GnuTLS is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * GnuTLS is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <stdlib.h>
+
+#if defined(_WIN32)
+
+int main()
+{
+       exit(77);
+}
+
+#else
+
+#include <string.h>
+#include <sys/types.h>
+#include <netinet/in.h>
+#include <sys/socket.h>
+#include <sys/wait.h>
+#include <arpa/inet.h>
+#include <unistd.h>
+#include <gnutls/gnutls.h>
+#include <gnutls/dtls.h>
+#include <signal.h>
+#include <assert.h>
+#include <errno.h>
+
+#include "cert-common.h"
+#include "utils.h"
+
+/* This program tests whether the ChangeCipherSpec message
+ * is ignored during handshake.
+ */
+
+static void server_log_func(int level, const char *str)
+{
+       fprintf(stderr, "server|<%d>| %s", level, str);
+}
+
+static void client_log_func(int level, const char *str)
+{
+       fprintf(stderr, "client|<%d>| %s", level, str);
+}
+
+static unsigned client_sent_ccs = 0;
+static unsigned server_sent_ccs = 0;
+
+static int cli_hsk_callback(gnutls_session_t session, unsigned int htype,
+       unsigned post, unsigned int incoming, const gnutls_datum_t *msg);
+
+static void client(int fd, unsigned ccs_check)
+{
+       int ret;
+       gnutls_certificate_credentials_t x509_cred;
+       gnutls_session_t session;
+       char buf[64];
+
+       global_init();
+       client_sent_ccs = 0;
+       server_sent_ccs = 0;
+
+       if (debug) {
+               gnutls_global_set_log_function(client_log_func);
+               gnutls_global_set_log_level(7);
+       }
+
+       gnutls_certificate_allocate_credentials(&x509_cred);
+
+       /* Initialize TLS session
+        */
+       gnutls_init(&session, GNUTLS_CLIENT|GNUTLS_POST_HANDSHAKE_AUTH);
+
+       gnutls_session_set_ptr(session, &ccs_check);
+       gnutls_handshake_set_timeout(session, 20 * 1000);
+       if (ccs_check) {
+               gnutls_handshake_set_hook_function(session, GNUTLS_HANDSHAKE_ANY,
+                                                  GNUTLS_HOOK_PRE,
+                                                  cli_hsk_callback);
+       }
+
+       ret = gnutls_priority_set_direct(session, "NORMAL:-VERS-ALL:+VERS-TLS1.3:+VERS-TLS1.2:+VERS-TLS1.0", NULL);
+       if (ret < 0)
+               fail("cannot set TLS 1.3 priorities\n");
+
+
+       gnutls_certificate_set_x509_key_mem(x509_cred, &cli_ca3_cert,
+                                           &cli_ca3_key,
+                                           GNUTLS_X509_FMT_PEM);
+
+       /* put the anonymous credentials to the current session
+        */
+       gnutls_credentials_set(session, GNUTLS_CRD_CERTIFICATE, x509_cred);
+
+       gnutls_transport_set_int(session, fd);
+
+       /* Perform the TLS handshake
+        */
+       do {
+               ret = gnutls_handshake(session);
+       }
+       while (ret < 0 && gnutls_error_is_fatal(ret) == 0);
+
+       if (ret != 0)
+               fail("handshake failed: %s\n", gnutls_strerror(ret));
+       success("client handshake completed\n");
+
+       do {
+               ret = gnutls_record_recv(session, buf, sizeof(buf));
+       } while (ret == GNUTLS_E_AGAIN || ret == GNUTLS_E_INTERRUPTED);
+
+       if (ret < 0)
+               fail("client: recv did not succeed as expected: %s\n", gnutls_strerror(ret));
+
+       close(fd);
+
+       gnutls_deinit(session);
+
+       if (ccs_check) {
+               if (client_sent_ccs != 1) {
+                       fail("client: did not sent CCS\n");
+               }
+       }
+
+       gnutls_certificate_free_credentials(x509_cred);
+
+       gnutls_global_deinit();
+}
+
+static int cli_hsk_callback(gnutls_session_t session, unsigned int htype,
+       unsigned post, unsigned int incoming, const gnutls_datum_t *msg)
+{
+       unsigned *p;
+       unsigned ccs_check;
+       static unsigned hello_received = 0;
+
+       p = gnutls_session_get_ptr(session);
+       ccs_check = *p;
+
+       assert(ccs_check != 0);
+       assert(post == GNUTLS_HOOK_PRE);
+
+       if (htype == GNUTLS_HANDSHAKE_CLIENT_HELLO && !incoming) {
+               hello_received = 1;
+
+               gnutls_handshake_set_hook_function(session, GNUTLS_HANDSHAKE_CHANGE_CIPHER_SPEC,
+                                                  GNUTLS_HOOK_PRE,
+                                                  cli_hsk_callback);
+       }
+
+       if (htype == GNUTLS_HANDSHAKE_CHANGE_CIPHER_SPEC && !incoming && hello_received) {
+               client_sent_ccs++;
+               assert(msg->size == 1 && msg->data[0] == 0x01);
+       }
+
+
+       return 0;
+}
+
+static int hsk_callback(gnutls_session_t session, unsigned int htype,
+       unsigned post, unsigned int incoming, const gnutls_datum_t *msg)
+{
+       int ret;
+       int fd;
+       unsigned *p;
+       unsigned ccs_check;
+
+       p = gnutls_session_get_ptr(session);
+       ccs_check = *p;
+
+       assert(post == GNUTLS_HOOK_PRE);
+
+       if (!ccs_check) {
+               if (!incoming || htype == GNUTLS_HANDSHAKE_CLIENT_HELLO ||
+                   htype == GNUTLS_HANDSHAKE_FINISHED)
+                       return 0;
+
+               fd = gnutls_transport_get_int(session);
+
+               /* send change cipher spec */
+               do {
+                       ret = send(fd, "\x14\x03\x03\x00\x01\x01", 6, 0);
+               } while(ret == -1 && (errno == EINTR || errno == EAGAIN));
+       } else { /* checking whether server received it */
+               if (htype == GNUTLS_HANDSHAKE_CHANGE_CIPHER_SPEC && !incoming) {
+                       server_sent_ccs++;
+                       assert(msg->size == 1 && msg->data[0] == 0x01);
+               }
+       }
+       return 0;
+}
+
+static void server(int fd, unsigned ccs_check)
+{
+       int ret;
+       gnutls_session_t session;
+       gnutls_certificate_credentials_t x509_cred;
+
+       /* this must be called once in the program
+        */
+       global_init();
+
+       client_sent_ccs = 0;
+       server_sent_ccs = 0;
+
+       if (debug) {
+               gnutls_global_set_log_function(server_log_func);
+               gnutls_global_set_log_level(4711);
+       }
+
+       gnutls_certificate_allocate_credentials(&x509_cred);
+       gnutls_certificate_set_x509_key_mem(x509_cred, &server_cert,
+                                           &server_key,
+                                           GNUTLS_X509_FMT_PEM);
+
+       gnutls_init(&session, GNUTLS_SERVER);
+
+       gnutls_handshake_set_timeout(session, 20 * 1000);
+
+       if (ccs_check)
+               gnutls_handshake_set_hook_function(session, GNUTLS_HANDSHAKE_CHANGE_CIPHER_SPEC,
+                                                  GNUTLS_HOOK_PRE,
+                                                  hsk_callback);
+       else
+               gnutls_handshake_set_hook_function(session, GNUTLS_HANDSHAKE_ANY,
+                                                  GNUTLS_HOOK_PRE,
+                                                  hsk_callback);
+
+       /* avoid calling all the priority functions, since the defaults
+        * are adequate.
+        */
+       assert(gnutls_priority_set_direct(session, "NORMAL:+VERS-TLS1.3", NULL) >= 0);
+       gnutls_session_set_ptr(session, &ccs_check);
+
+       gnutls_credentials_set(session, GNUTLS_CRD_CERTIFICATE, x509_cred);
+
+       gnutls_transport_set_int(session, fd);
+
+       do {
+               ret = gnutls_handshake(session);
+       } while (ret < 0 && gnutls_error_is_fatal(ret) == 0);
+
+       if (ret != 0)
+               fail("handshake failed: %s\n", gnutls_strerror(ret));
+
+       success("server handshake completed\n");
+
+       gnutls_certificate_server_set_request(session, GNUTLS_CERT_REQUIRE);
+       /* ask peer for re-authentication */
+       do {
+               ret = gnutls_record_send(session, "\x00", 1);
+       } while (ret == GNUTLS_E_AGAIN || ret == GNUTLS_E_INTERRUPTED);
+
+       if (ret < 0)
+               fail("server: gnutls_record_send did not succeed as expected: %s\n", gnutls_strerror(ret));
+
+       close(fd);
+       gnutls_deinit(session);
+
+       gnutls_certificate_free_credentials(x509_cred);
+
+       if (ccs_check) {
+               if (server_sent_ccs != 1) {
+                       fail("server: did not sent CCS\n");
+               }
+       }
+
+       gnutls_global_deinit();
+
+       if (debug)
+               success("server: client/server hello were verified\n");
+}
+
+static void ch_handler(int sig)
+{
+       int status;
+       wait(&status);
+       check_wait_status(status);
+       return;
+}
+
+static
+void start(unsigned ccs_check)
+{
+       int fd[2];
+       int ret;
+       pid_t child;
+
+       signal(SIGCHLD, ch_handler);
+
+       ret = socketpair(AF_UNIX, SOCK_STREAM, 0, fd);
+       if (ret < 0) {
+               perror("socketpair");
+               exit(1);
+       }
+
+       child = fork();
+       if (child < 0) {
+               perror("fork");
+               fail("fork");
+               exit(1);
+       }
+
+       if (child) {
+               /* parent */
+               close(fd[1]);
+               server(fd[0], ccs_check);
+               kill(child, SIGTERM);
+       } else {
+               close(fd[0]);
+               client(fd[1], ccs_check);
+               exit(0);
+       }
+}
+
+void doit(void)
+{
+       start(0);
+       start(1);
+}
+
+#endif                         /* _WIN32 */