/*
 * Copyright 2009-present MongoDB, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <mongoc/mongoc-config.h>

#ifdef MONGOC_ENABLE_SSL_SECURE_CHANNEL

#include <bson/bson.h>

#include <mongoc/mongoc-log.h>
#include <mongoc/mongoc-trace-private.h>
#include <mongoc/mongoc-ssl.h>
#include <mongoc/mongoc-stream-tls.h>
#include <mongoc/mongoc-stream-tls-private.h>
#include <mongoc/mongoc-secure-channel-private.h>
#include <mongoc/mongoc-stream-tls-secure-channel-private.h>
#include <mongoc/mongoc-errno-private.h>
#include <mongoc/mongoc-error.h>
#include <common-string-private.h>
#include <common-cmp-private.h>


#undef MONGOC_LOG_DOMAIN
#define MONGOC_LOG_DOMAIN "stream-secure-channel"

/* mingw doesn't define this */
#ifndef SECBUFFER_ALERT
#define SECBUFFER_ALERT 17
#endif


PCCERT_CONTEXT
mongoc_secure_channel_setup_certificate_from_file (const char *filename)
{
   char *pem;
   FILE *file;
   bool success;
   HCRYPTKEY hKey;
   long pem_length;
   HCRYPTPROV provider;
   CERT_BLOB public_blob;
   const char *pem_public;
   const char *pem_private;
   LPBYTE blob_private = NULL;
   PCCERT_CONTEXT cert = NULL;
   DWORD blob_private_len = 0;
   DWORD encrypted_private_len = 0;
   LPBYTE encrypted_private = NULL;


   file = fopen (filename, "rb");
   if (!file) {
      MONGOC_ERROR ("Couldn't open file '%s'", filename);
      return NULL;
   }

   fseek (file, 0, SEEK_END);
   pem_length = ftell (file);
   fseek (file, 0, SEEK_SET);
   if (pem_length < 1) {
      MONGOC_ERROR ("Couldn't determine file size of '%s'", filename);
      return NULL;
   }

   pem = (char *) bson_malloc0 (pem_length);
   fread ((void *) pem, 1, pem_length, file);
   fclose (file);

   pem_public = strstr (pem, "-----BEGIN CERTIFICATE-----");
   pem_private = strstr (pem, "-----BEGIN ENCRYPTED PRIVATE KEY-----");

   if (pem_private) {
      MONGOC_ERROR ("Detected unsupported encrypted private key");
      goto fail;
   }

   pem_private = strstr (pem, "-----BEGIN RSA PRIVATE KEY-----");
   if (!pem_private) {
      pem_private = strstr (pem, "-----BEGIN PRIVATE KEY-----");
   }

   if (!pem_private) {
      MONGOC_ERROR ("Can't find private key in '%s'", filename);
      goto fail;
   }

   public_blob.cbData = (DWORD) strlen (pem_public);
   public_blob.pbData = (BYTE *) pem_public;

   /* https://msdn.microsoft.com/en-us/library/windows/desktop/aa380264%28v=vs.85%29.aspx
    */
   CryptQueryObject (CERT_QUERY_OBJECT_BLOB,      /* dwObjectType, blob or file */
                     &public_blob,                /* pvObject, Unicode filename */
                     CERT_QUERY_CONTENT_FLAG_ALL, /* dwExpectedContentTypeFlags */
                     CERT_QUERY_FORMAT_FLAG_ALL,  /* dwExpectedFormatTypeFlags */
                     0,                           /* dwFlags, reserved for "future use" */
                     NULL,                        /* pdwMsgAndCertEncodingType, OUT, unused */
                     NULL,                        /* pdwContentType (dwExpectedContentTypeFlags), OUT, unused */
                     NULL,                        /* pdwFormatType (dwExpectedFormatTypeFlags,), OUT, unused */
                     NULL,                        /* phCertStore, OUT, HCERTSTORE.., unused, for now */
                     NULL,                        /* phMsg, OUT, HCRYPTMSG, only for PKC7, unused */
                     (const void **) &cert        /* ppvContext, OUT, the Certificate Context */
   );

   if (!cert) {
      MONGOC_ERROR ("Failed to extract public key from '%s'. Error 0x%.8X", filename, (unsigned int) GetLastError ());
      goto fail;
   }

   /* https://msdn.microsoft.com/en-us/library/windows/desktop/aa380285%28v=vs.85%29.aspx
    */
   success = CryptStringToBinaryA (pem_private,               /* pszString */
                                   0,                         /* cchString */
                                   CRYPT_STRING_BASE64HEADER, /* dwFlags */
                                   NULL,                      /* pbBinary */
                                   &encrypted_private_len,    /* pcBinary, IN/OUT */
                                   NULL,                      /* pdwSkip */
                                   NULL);                     /* pdwFlags */
   if (!success) {
      MONGOC_ERROR ("Failed to convert base64 private key. Error 0x%.8X", (unsigned int) GetLastError ());
      goto fail;
   }

   encrypted_private = (LPBYTE) bson_malloc0 (encrypted_private_len);
   success = CryptStringToBinaryA (
      pem_private, 0, CRYPT_STRING_BASE64HEADER, encrypted_private, &encrypted_private_len, NULL, NULL);
   if (!success) {
      MONGOC_ERROR ("Failed to convert base64 private key. Error 0x%.8X", (unsigned int) GetLastError ());
      goto fail;
   }

   /* https://msdn.microsoft.com/en-us/library/windows/desktop/aa379912%28v=vs.85%29.aspx
    */
   success = CryptDecodeObjectEx (X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, /* dwCertEncodingType */
                                  PKCS_RSA_PRIVATE_KEY,                    /* lpszStructType */
                                  encrypted_private,                       /* pbEncoded */
                                  encrypted_private_len,                   /* cbEncoded */
                                  0,                                       /* dwFlags */
                                  NULL,                                    /* pDecodePara */
                                  NULL,                                    /* pvStructInfo */
                                  &blob_private_len);                      /* pcbStructInfo */
   if (!success) {
      LPTSTR msg = NULL;
      FormatMessage (FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_ARGUMENT_ARRAY,
                     NULL,
                     GetLastError (),
                     LANG_NEUTRAL,
                     (LPTSTR) &msg,
                     0,
                     NULL);
      MONGOC_ERROR ("Failed to parse private key. %s (0x%.8X)", msg, (unsigned int) GetLastError ());
      LocalFree (msg);
      goto fail;
   }

   blob_private = (LPBYTE) bson_malloc0 (blob_private_len);
   success = CryptDecodeObjectEx (X509_ASN_ENCODING | PKCS_7_ASN_ENCODING,
                                  PKCS_RSA_PRIVATE_KEY,
                                  encrypted_private,
                                  encrypted_private_len,
                                  0,
                                  NULL,
                                  blob_private,
                                  &blob_private_len);
   if (!success) {
      MONGOC_ERROR ("Failed to parse private key. Error 0x%.8X", (unsigned int) GetLastError ());
      goto fail;
   }

   /* https://msdn.microsoft.com/en-us/library/windows/desktop/aa379886%28v=vs.85%29.aspx
    */
   success = CryptAcquireContext (&provider,            /* phProv */
                                  NULL,                 /* pszContainer */
                                  MS_ENHANCED_PROV,     /* pszProvider */
                                  PROV_RSA_FULL,        /* dwProvType */
                                  CRYPT_VERIFYCONTEXT); /* dwFlags */
   if (!success) {
      MONGOC_ERROR ("CryptAcquireContext failed with error 0x%.8X", (unsigned int) GetLastError ());
      goto fail;
   }

   /* https://msdn.microsoft.com/en-us/library/windows/desktop/aa380207%28v=vs.85%29.aspx
    */
   success = CryptImportKey (provider,         /* hProv */
                             blob_private,     /* pbData */
                             blob_private_len, /* dwDataLen */
                             0,                /* hPubKey */
                             0,                /* dwFlags */
                             &hKey);           /* phKey, OUT */
   if (!success) {
      MONGOC_ERROR ("CryptImportKey for private key failed with error 0x%.8X", (unsigned int) GetLastError ());
      goto fail;
   }

   /* https://msdn.microsoft.com/en-us/library/windows/desktop/aa376573%28v=vs.85%29.aspx
    */
   success = CertSetCertificateContextProperty (cert,                         /* pCertContext */
                                                CERT_KEY_PROV_HANDLE_PROP_ID, /* dwPropId */
                                                0,                            /* dwFlags */
                                                (const void *) provider);     /* pvData */
   if (success) {
      TRACE ("%s", "Successfully loaded client certificate");
      return cert;
   }

   MONGOC_ERROR ("Can't associate private key with public key: 0x%.8X", (unsigned int) GetLastError ());

fail:
   SecureZeroMemory (pem, pem_length);
   bson_free (pem);
   if (encrypted_private) {
      SecureZeroMemory (encrypted_private, encrypted_private_len);
      bson_free (encrypted_private);
   }

   if (blob_private) {
      SecureZeroMemory (blob_private, blob_private_len);
      bson_free (blob_private);
   }

   return NULL;
}

PCCERT_CONTEXT
mongoc_secure_channel_setup_certificate (mongoc_stream_tls_secure_channel_t *secure_channel, mongoc_ssl_opt_t *opt)
{
   return mongoc_secure_channel_setup_certificate_from_file (opt->pem_file);
}


bool
mongoc_secure_channel_setup_ca (mongoc_stream_tls_secure_channel_t *secure_channel, mongoc_ssl_opt_t *opt)
{
   FILE *file;
   long length;
   const char *pem_key;
   HCERTSTORE cert_store = NULL;
   PCCERT_CONTEXT cert = NULL;
   DWORD encrypted_cert_len = 0;
   LPBYTE encrypted_cert = NULL;

   file = fopen (opt->ca_file, "rb");
   if (!file) {
      MONGOC_ERROR ("Couldn't open file '%s'", opt->ca_file);
      return false;
   }

   fseek (file, 0, SEEK_END);
   length = ftell (file);
   fseek (file, 0, SEEK_SET);
   if (length < 1) {
      MONGOC_WARNING ("Couldn't determine file size of '%s'", opt->ca_file);
      return false;
   }

   pem_key = (const char *) bson_malloc0 (length);
   fread ((void *) pem_key, 1, length, file);
   fclose (file);

   /* If we have private keys or other fuzz, seek to the good stuff */
   pem_key = strstr (pem_key, "-----BEGIN CERTIFICATE-----");
   /*printf ("%s\n", pem_key);*/

   if (!pem_key) {
      MONGOC_WARNING ("Couldn't find certificate in '%s'", opt->ca_file);
      return false;
   }

   if (!CryptStringToBinaryA (pem_key, 0, CRYPT_STRING_BASE64HEADER, NULL, &encrypted_cert_len, NULL, NULL)) {
      MONGOC_ERROR ("Failed to convert BASE64 public key. Error 0x%.8X", (unsigned int) GetLastError ());
      return false;
   }

   encrypted_cert = (LPBYTE) LocalAlloc (0, encrypted_cert_len);
   if (!CryptStringToBinaryA (pem_key, 0, CRYPT_STRING_BASE64HEADER, encrypted_cert, &encrypted_cert_len, NULL, NULL)) {
      MONGOC_ERROR ("Failed to convert BASE64 public key. Error 0x%.8X", (unsigned int) GetLastError ());
      return false;
   }

   cert = CertCreateCertificateContext (X509_ASN_ENCODING, encrypted_cert, encrypted_cert_len);
   if (!cert) {
      MONGOC_WARNING ("Could not convert certificate");
      return false;
   }


   cert_store = CertOpenStore (CERT_STORE_PROV_SYSTEM,                  /* provider */
                               X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, /* certificate encoding */
                               0,                                       /* unused */
                               CERT_SYSTEM_STORE_LOCAL_MACHINE,         /* dwFlags */
                               L"Root");                                /* system store name. "My" or "Root" */

   if (cert_store == NULL) {
      MONGOC_ERROR ("Error opening certificate store");
      return false;
   }

   if (CertAddCertificateContextToStore (cert_store, cert, CERT_STORE_ADD_USE_EXISTING, NULL)) {
      TRACE ("%s", "Added the certificate !");
      CertCloseStore (cert_store, 0);
      return true;
   }
   MONGOC_WARNING ("Failed adding the cert");
   CertCloseStore (cert_store, 0);

   return false;
}

bool
mongoc_secure_channel_setup_crl (mongoc_stream_tls_secure_channel_t *secure_channel, mongoc_ssl_opt_t *opt)
{
   HCERTSTORE cert_store = NULL;
   PCCERT_CONTEXT cert = NULL;
   LPWSTR str;
   int chars;

   chars = MultiByteToWideChar (CP_ACP, 0, opt->crl_file, -1, NULL, 0);
   if (chars < 1) {
      MONGOC_WARNING ("Can't determine opt->crl_file length");
      return false;
   }
   str = (LPWSTR) bson_malloc0 (chars * sizeof (*str));
   MultiByteToWideChar (CP_ACP, 0, opt->crl_file, -1, str, chars);


   /* https://msdn.microsoft.com/en-us/library/windows/desktop/aa380264%28v=vs.85%29.aspx
    */
   CryptQueryObject (CERT_QUERY_OBJECT_FILE,      /* dwObjectType, blob or file */
                     str,                         /* pvObject, Unicode filename */
                     CERT_QUERY_CONTENT_FLAG_CRL, /* dwExpectedContentTypeFlags */
                     CERT_QUERY_FORMAT_FLAG_ALL,  /* dwExpectedFormatTypeFlags */
                     0,                           /* dwFlags, reserved for "future use" */
                     NULL,                        /* pdwMsgAndCertEncodingType, OUT, unused */
                     NULL,                        /* pdwContentType (dwExpectedContentTypeFlags), OUT, unused */
                     NULL,                        /* pdwFormatType (dwExpectedFormatTypeFlags,), OUT, unused */
                     NULL,                        /* phCertStore, OUT, HCERTSTORE.., unused, for now */
                     NULL,                        /* phMsg, OUT, HCRYPTMSG, only for PKC7, unused */
                     (const void **) &cert        /* ppvContext, OUT, the Certificate Context */
   );
   bson_free (str);

   if (!cert) {
      MONGOC_WARNING ("Can't extract CRL from '%s'", opt->crl_file);
      return false;
   }


   cert_store = CertOpenStore (CERT_STORE_PROV_SYSTEM,                  /* provider */
                               X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, /* certificate encoding */
                               0,                                       /* unused */
                               CERT_SYSTEM_STORE_LOCAL_MACHINE,         /* dwFlags */
                               L"Root");                                /* system store name. "My" or "Root" */

   if (cert_store == NULL) {
      MONGOC_ERROR ("Error opening certificate store");
      CertFreeCertificateContext (cert);
      return false;
   }

   if (CertAddCertificateContextToStore (cert_store, cert, CERT_STORE_ADD_USE_EXISTING, NULL)) {
      TRACE ("%s", "Added the certificate !");
      CertFreeCertificateContext (cert);
      CertCloseStore (cert_store, 0);
      return true;
   }

   MONGOC_WARNING ("Failed adding the cert");
   CertFreeCertificateContext (cert);
   CertCloseStore (cert_store, 0);

   return false;
}

ssize_t
mongoc_secure_channel_read (mongoc_stream_tls_t *tls, void *data, size_t data_length)
{
   BSON_ASSERT_PARAM (tls);

   if (BSON_UNLIKELY (!mcommon_in_range_signed (int32_t, tls->timeout_msec))) {
      // CDRIVER-4589
      MONGOC_ERROR ("timeout_msec value %" PRId64 " exceeds supported 32-bit range", tls->timeout_msec);
      return -1;
   }

   errno = 0;
   TRACE ("Wanting to read: %zu, timeout is %" PRId64, data_length, tls->timeout_msec);
   /* 4th argument is minimum bytes, while the data_length is the
    * size of the buffer. We are totally fine with just one TLS record (few
    *bytes)
    **/
   const ssize_t length = mongoc_stream_read (tls->base_stream, data, data_length, 0, (int32_t) tls->timeout_msec);

   TRACE ("Got %zd", length);

   if (length > 0) {
      return length;
   }

   return 0;
}

ssize_t
mongoc_secure_channel_write (mongoc_stream_tls_t *tls, const void *data, size_t data_length)
{
   BSON_ASSERT_PARAM (tls);

   if (BSON_UNLIKELY (!mcommon_in_range_signed (int32_t, tls->timeout_msec))) {
      // CDRIVER-4589
      MONGOC_ERROR ("timeout_msec value %" PRId64 " exceeds supported 32-bit range", tls->timeout_msec);
      return -1;
   }

   errno = 0;
   TRACE ("Wanting to write: %zu", data_length);
   const ssize_t length =
      mongoc_stream_write (tls->base_stream, (void *) data, data_length, (int32_t) tls->timeout_msec);
   TRACE ("Wrote: %zd", length);

   return length;
}

void
mongoc_secure_channel_realloc_buf (size_t *size, uint8_t **buf, size_t new_size)
{
   *size = bson_next_power_of_two (new_size);
   *buf = bson_realloc (*buf, *size);
}

/**
 * The follow functions comes from one of my favorite project, cURL!
 * Thank you so much for having gone through the Secure Channel pain for me.
 *
 *
 * Copyright (C) 2012 - 2015, Marc Hoersken, <info@marc-hoersken.de>
 * Copyright (C) 2012, Mark Salisbury, <mark.salisbury@hp.com>
 * Copyright (C) 2012 - 2015, Daniel Stenberg, <daniel@haxx.se>, et al.
 *
 * This software is licensed as described in the file COPYING, which
 * you should have received as part of this distribution. The terms
 * are also available at https://curl.haxx.se/docs/copyright.html.
 *
 * You may opt to use, copy, modify, merge, publish, distribute and/or sell
 * copies of the Software, and permit persons to whom the Software is
 * furnished to do so, under the terms of the COPYING file.
 *
 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
 * KIND, either express or implied.
 *
 ***************************************************************************/

/*
 * Based upon the PolarSSL implementation in polarssl.c and polarssl.h:
 *   Copyright (C) 2010, 2011, Hoi-Ho Chan, <hoiho.chan@gmail.com>
 *
 * Based upon the CyaSSL implementation in cyassl.c and cyassl.h:
 *   Copyright (C) 1998 - 2012, Daniel Stenberg, <daniel@haxx.se>, et al.
 *
 * Thanks for code and inspiration!
 */

void
_mongoc_secure_channel_init_sec_buffer (SecBuffer *buffer,
                                        unsigned long buf_type,
                                        void *buf_data_ptr,
                                        unsigned long buf_byte_size)
{
   buffer->cbBuffer = buf_byte_size;
   buffer->BufferType = buf_type;
   buffer->pvBuffer = buf_data_ptr;
}

void
_mongoc_secure_channel_init_sec_buffer_desc (SecBufferDesc *desc, SecBuffer *buffer_array, unsigned long buffer_count)
{
   desc->ulVersion = SECBUFFER_VERSION;
   desc->pBuffers = buffer_array;
   desc->cBuffers = buffer_count;
}


#define MONGOC_LOG_AND_SET_ERROR(ERROR, DOMAIN, CODE, ...) \
   do {                                                    \
      MONGOC_ERROR (__VA_ARGS__);                          \
      bson_set_error (ERROR, DOMAIN, CODE, __VA_ARGS__);   \
   } while (0)

bool
mongoc_secure_channel_handshake_step_1 (mongoc_stream_tls_t *tls, char *hostname, bson_error_t *error)
{
   SecBuffer outbuf;
   ssize_t written = -1;
   SecBufferDesc outbuf_desc;
   SECURITY_STATUS sspi_status = SEC_E_OK;
   mongoc_stream_tls_secure_channel_t *secure_channel = (mongoc_stream_tls_secure_channel_t *) tls->ctx;

   TRACE ("SSL/TLS connection with '%s' (step 1/3)", hostname);

   /* setup output buffer */
   _mongoc_secure_channel_init_sec_buffer (&outbuf, SECBUFFER_EMPTY, NULL, 0);
   _mongoc_secure_channel_init_sec_buffer_desc (&outbuf_desc, &outbuf, 1);

   /* setup request flags */
   secure_channel->req_flags = ISC_REQ_SEQUENCE_DETECT | ISC_REQ_REPLAY_DETECT | ISC_REQ_CONFIDENTIALITY |
                               ISC_REQ_ALLOCATE_MEMORY | ISC_REQ_STREAM;

   /* allocate memory for the security context handle */
   secure_channel->ctxt = (mongoc_secure_channel_ctxt *) bson_malloc0 (sizeof (mongoc_secure_channel_ctxt));

   /* https://msdn.microsoft.com/en-us/library/windows/desktop/aa375924.aspx */
   sspi_status = InitializeSecurityContext (&secure_channel->cred->cred_handle, /* phCredential */
                                            NULL,                               /* phContext */
                                            hostname,                           /* pszTargetName */
                                            secure_channel->req_flags,          /* fContextReq */
                                            0,                                  /* Reserved1, must be 0 */
                                            0,                                  /* TargetDataRep, unused */
                                            NULL,                               /* pInput */
                                            0,                                  /* Reserved2, must be 0 */
                                            &secure_channel->ctxt->ctxt_handle, /* phNewContext OUT param */
                                            &outbuf_desc,                       /* pOutput OUT param */
                                            &secure_channel->ret_flags,         /* pfContextAttr OUT param */
                                            &secure_channel->ctxt->time_stamp   /* ptsExpiry OUT param */
   );

   if (sspi_status != SEC_I_CONTINUE_NEEDED) {
      MONGOC_LOG_AND_SET_ERROR (error,
                                MONGOC_ERROR_STREAM,
                                MONGOC_ERROR_STREAM_SOCKET,
                                "initial InitializeSecurityContext failed: %ld",
                                sspi_status);
      return false;
   }

   TRACE ("sending initial handshake data: sending %lu bytes...", outbuf.cbBuffer);

   /* send initial handshake data which is now stored in output buffer */
   written = mongoc_secure_channel_write (tls, outbuf.pvBuffer, outbuf.cbBuffer);
   FreeContextBuffer (outbuf.pvBuffer);

   if (outbuf.cbBuffer != (size_t) written) {
      MONGOC_LOG_AND_SET_ERROR (error,
                                MONGOC_ERROR_STREAM,
                                MONGOC_ERROR_STREAM_SOCKET,
                                "failed to send initial handshake data: "
                                "sent %zd of %lu bytes",
                                written,
                                outbuf.cbBuffer);
      return false;
   }

   TRACE ("sent initial handshake data: sent %zd bytes", written);

   secure_channel->recv_unrecoverable_err = 0;
   secure_channel->recv_sspi_close_notify = false;
   secure_channel->recv_connection_closed = false;

   /* continue to second handshake step */
   secure_channel->connecting_state = ssl_connect_2;

   return true;
}

bool
mongoc_secure_channel_handshake_step_2 (mongoc_stream_tls_t *tls, char *hostname, bson_error_t *error)
{
   mongoc_stream_tls_secure_channel_t *secure_channel = (mongoc_stream_tls_secure_channel_t *) tls->ctx;
   SECURITY_STATUS sspi_status = SEC_E_OK;
   ssize_t nread = -1, written = -1;
   SecBufferDesc outbuf_desc;
   SecBufferDesc inbuf_desc;
   SecBuffer outbuf[3];
   SecBuffer inbuf[2];
   bool doread;
   int i;

   doread = (secure_channel->connecting_state != ssl_connect_2_writing) ? true : false;

   TRACE ("%s", "SSL/TLS connection with endpoint (step 2/3)");

   if (!secure_channel->cred || !secure_channel->ctxt) {
      MONGOC_LOG_AND_SET_ERROR (
         error, MONGOC_ERROR_STREAM, MONGOC_ERROR_STREAM_SOCKET, "required TLS credentials or context not provided");

      return false;
   }

   /* grow the buffer if necessary */
   if (secure_channel->encdata_length == secure_channel->encdata_offset) {
      mongoc_secure_channel_realloc_buf (
         &secure_channel->encdata_length, &secure_channel->encdata_buffer, secure_channel->encdata_length + 1);
   }

   for (;;) {
      if (doread) {
         /* read encrypted handshake data from socket */
         nread = mongoc_secure_channel_read (tls,
                                             (char *) (secure_channel->encdata_buffer + secure_channel->encdata_offset),
                                             secure_channel->encdata_length - secure_channel->encdata_offset);

         if (!nread) {
            if (MONGOC_ERRNO_IS_AGAIN (errno)) {
               if (secure_channel->connecting_state != ssl_connect_2_writing) {
                  secure_channel->connecting_state = ssl_connect_2_reading;
               }

               TRACE ("%s", "failed to receive handshake, need more data");
               return true;
            }

            MONGOC_LOG_AND_SET_ERROR (error,
                                      MONGOC_ERROR_STREAM,
                                      MONGOC_ERROR_STREAM_SOCKET,
                                      "failed to receive handshake, SSL/TLS connection failed");

            return false;
         }

         /* increase encrypted data buffer offset */
         secure_channel->encdata_offset += nread;
      }

      TRACE ("encrypted data buffer: offset %d length %d",
             (int) secure_channel->encdata_offset,
             (int) secure_channel->encdata_length);

      /* setup input buffers */
      _mongoc_secure_channel_init_sec_buffer (&inbuf[0],
                                              SECBUFFER_TOKEN,
                                              bson_malloc (secure_channel->encdata_offset),
                                              (unsigned long) (secure_channel->encdata_offset & (size_t) 0xFFFFFFFFUL));
      _mongoc_secure_channel_init_sec_buffer (&inbuf[1], SECBUFFER_EMPTY, NULL, 0);
      _mongoc_secure_channel_init_sec_buffer_desc (&inbuf_desc, inbuf, 2);

      /* setup output buffers */
      _mongoc_secure_channel_init_sec_buffer (&outbuf[0], SECBUFFER_TOKEN, NULL, 0);
      _mongoc_secure_channel_init_sec_buffer (&outbuf[1], SECBUFFER_ALERT, NULL, 0);
      _mongoc_secure_channel_init_sec_buffer (&outbuf[2], SECBUFFER_EMPTY, NULL, 0);
      _mongoc_secure_channel_init_sec_buffer_desc (&outbuf_desc, outbuf, 3);

      if (inbuf[0].pvBuffer == NULL) {
         MONGOC_LOG_AND_SET_ERROR (error, MONGOC_ERROR_STREAM, MONGOC_ERROR_STREAM_SOCKET, "unable to allocate memory");
         return false;
      }

      /* copy received handshake data into input buffer */
      memcpy (inbuf[0].pvBuffer, secure_channel->encdata_buffer, secure_channel->encdata_offset);

      /* https://msdn.microsoft.com/en-us/library/windows/desktop/aa375924.aspx
       */
      sspi_status = InitializeSecurityContext (&secure_channel->cred->cred_handle,
                                               &secure_channel->ctxt->ctxt_handle,
                                               hostname,
                                               secure_channel->req_flags,
                                               0,
                                               0,
                                               &inbuf_desc,
                                               0,
                                               NULL,
                                               &outbuf_desc,
                                               &secure_channel->ret_flags,
                                               &secure_channel->ctxt->time_stamp);

      /* free buffer for received handshake data */
      bson_free (inbuf[0].pvBuffer);

      /* check if the handshake was incomplete */
      if (sspi_status == SEC_E_INCOMPLETE_MESSAGE) {
         secure_channel->connecting_state = ssl_connect_2_reading;
         TRACE ("%s", "received incomplete message, need more data");
         return true;
      }

      /* If the server has requested a client certificate, attempt to continue
       * the handshake without one. This will allow connections to servers which
       * request a client certificate but do not require it. */
      if (sspi_status == SEC_I_INCOMPLETE_CREDENTIALS && !(secure_channel->req_flags & ISC_REQ_USE_SUPPLIED_CREDS)) {
         secure_channel->req_flags |= ISC_REQ_USE_SUPPLIED_CREDS;
         secure_channel->connecting_state = ssl_connect_2_writing;
         TRACE ("%s", "A client certificate has been requested");
         return true;
      }

      /* check if the handshake needs to be continued */
      if (sspi_status == SEC_I_CONTINUE_NEEDED || sspi_status == SEC_E_OK) {
         for (i = 0; i < 3; i++) {
            /* search for handshake tokens that need to be send */
            if (outbuf[i].BufferType == SECBUFFER_TOKEN && outbuf[i].cbBuffer > 0) {
               TRACE ("sending next handshake data: sending %lu bytes...", outbuf[i].cbBuffer);

               /* send handshake token to server */
               written = mongoc_secure_channel_write (tls, outbuf[i].pvBuffer, outbuf[i].cbBuffer);

               if (outbuf[i].cbBuffer != (size_t) written) {
                  MONGOC_LOG_AND_SET_ERROR (error,
                                            MONGOC_ERROR_STREAM,
                                            MONGOC_ERROR_STREAM_SOCKET,
                                            "failed to send next handshake data: "
                                            "sent %zd of %lu bytes",
                                            written,
                                            outbuf[i].cbBuffer);
                  return false;
               }
            }

            /* free obsolete buffer */
            if (outbuf[i].pvBuffer != NULL) {
               FreeContextBuffer (outbuf[i].pvBuffer);
            }
         }
      } else {
         switch (sspi_status) {
         case SEC_E_WRONG_PRINCIPAL:
            MONGOC_LOG_AND_SET_ERROR (error,
                                      MONGOC_ERROR_STREAM,
                                      MONGOC_ERROR_STREAM_SOCKET,
                                      "SSL Certification verification failed: hostname "
                                      "doesn't match certificate");
            break;

         case SEC_E_UNTRUSTED_ROOT:
            MONGOC_LOG_AND_SET_ERROR (error,
                                      MONGOC_ERROR_STREAM,
                                      MONGOC_ERROR_STREAM_SOCKET,
                                      "SSL Certification verification failed: Untrusted "
                                      "root certificate");
            break;

         case SEC_E_CERT_EXPIRED:
            MONGOC_LOG_AND_SET_ERROR (error,
                                      MONGOC_ERROR_STREAM,
                                      MONGOC_ERROR_STREAM_SOCKET,
                                      "SSL Certification verification failed: certificate "
                                      "has expired");
            break;
         case CRYPT_E_NO_REVOCATION_CHECK:
            MONGOC_LOG_AND_SET_ERROR (error,
                                      MONGOC_ERROR_STREAM,
                                      MONGOC_ERROR_STREAM_SOCKET,
                                      "SSL Certification verification failed: certificate "
                                      "does not include revocation check.");
            break;

         case SEC_E_INSUFFICIENT_MEMORY:
         case SEC_E_INTERNAL_ERROR:
         case SEC_E_INVALID_HANDLE:
         case SEC_E_INVALID_TOKEN:
         case SEC_E_LOGON_DENIED:
         case SEC_E_NO_AUTHENTICATING_AUTHORITY:
         case SEC_E_NO_CREDENTIALS:
         case SEC_E_TARGET_UNKNOWN:
         case SEC_E_UNSUPPORTED_FUNCTION:
#ifdef SEC_E_APPLICATION_PROTOCOL_MISMATCH
         /* Not available in VS2010 */
         case SEC_E_APPLICATION_PROTOCOL_MISMATCH:
#endif


         default: {
            LPTSTR msg = NULL;

            FormatMessage (FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_ARGUMENT_ARRAY,
                           NULL,
                           GetLastError (),
                           LANG_NEUTRAL,
                           (LPTSTR) &msg,
                           0,
                           NULL);
            MONGOC_LOG_AND_SET_ERROR (error,
                                      MONGOC_ERROR_STREAM,
                                      MONGOC_ERROR_STREAM_SOCKET,
                                      "Failed to initialize security context, error code: "
                                      "0x%04X%04X: %s",
                                      (unsigned int) (sspi_status >> 16) & 0xffff,
                                      (unsigned int) sspi_status & 0xffff,
                                      msg);
            LocalFree (msg);
         }
         }
         return false;
      }

      /* check if there was additional remaining encrypted data */
      if (inbuf[1].BufferType == SECBUFFER_EXTRA && inbuf[1].cbBuffer > 0) {
         TRACE ("encrypted data length: %lu", inbuf[1].cbBuffer);

         /*
          * There are two cases where we could be getting extra data here:
          * 1) If we're renegotiating a connection and the handshake is already
          * complete (from the server perspective), it can encrypted app data
          * (not handshake data) in an extra buffer at this point.
          * 2) (sspi_status == SEC_I_CONTINUE_NEEDED) We are negotiating a
          * connection and this extra data is part of the handshake.
          * We should process the data immediately; waiting for the socket to
          * be ready may fail since the server is done sending handshake data.
          */
         /* check if the remaining data is less than the total amount
          * and therefore begins after the already processed data */
         if (secure_channel->encdata_offset > inbuf[1].cbBuffer) {
            memmove (secure_channel->encdata_buffer,
                     (secure_channel->encdata_buffer + secure_channel->encdata_offset) - inbuf[1].cbBuffer,
                     inbuf[1].cbBuffer);
            secure_channel->encdata_offset = inbuf[1].cbBuffer;

            if (sspi_status == SEC_I_CONTINUE_NEEDED) {
               doread = FALSE;
               continue;
            }
         }
      } else {
         secure_channel->encdata_offset = 0;
      }

      break;
   }

   /* check if the handshake needs to be continued */
   if (sspi_status == SEC_I_CONTINUE_NEEDED) {
      secure_channel->connecting_state = ssl_connect_2_reading;
      return true;
   }

   /* check if the handshake is complete */
   if (sspi_status == SEC_E_OK) {
      secure_channel->connecting_state = ssl_connect_3;
      TRACE ("%s", "SSL/TLS handshake complete");
   }

   return true;
}

bool
mongoc_secure_channel_handshake_step_3 (mongoc_stream_tls_t *tls, char *hostname, bson_error_t *error)
{
   mongoc_stream_tls_secure_channel_t *secure_channel = (mongoc_stream_tls_secure_channel_t *) tls->ctx;

   BSON_ASSERT (ssl_connect_3 == secure_channel->connecting_state);

   TRACE ("SSL/TLS connection with %s (step 3/3)", hostname);

   if (!secure_channel->cred) {
      MONGOC_LOG_AND_SET_ERROR (
         error, MONGOC_ERROR_STREAM, MONGOC_ERROR_STREAM_SOCKET, "required TLS credentials not provided");
      return false;
   }

   /* check if the required context attributes are met */
   if (secure_channel->ret_flags != secure_channel->req_flags) {
      MONGOC_LOG_AND_SET_ERROR (error, MONGOC_ERROR_STREAM, MONGOC_ERROR_STREAM_SOCKET, "Failed handshake");

      return false;
   }

   secure_channel->connecting_state = ssl_connect_done;

   return true;
}
#endif
