/*
 *
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.
 *
 */

/*
 * SChannel is designed to encrypt and decrypt data in place.  So a
 * given buffer is expected to sometimes contain encrypted data,
 * sometimes decrypted data, and occasionally both.  Outgoing buffers
 * need to reserve space for the TLS header and trailer.  Read
 * operations need to ignore the same headers and trailers from
 * incoming buffers.  Outgoing is simple because we choose record
 * boundaries.  Incoming is complicated by handling incomplete TLS
 * records, and buffering contiguous data for the app layer that may
 * span many records.  A lazy double buffering system is used for
 * the latter.
 */

#include <proton/ssl.h>
#include <proton/engine.h>
#include "core/engine-internal.h"
#include "platform/platform.h"
#include "core/util.h"
#include "core/autodetect.h"

#include <assert.h>

// security.h needs to see this to distinguish from kernel use.
#include <windows.h>
#define SECURITY_WIN32
#include <security.h>
#include <Schnlsp.h>
#include <WinInet.h>
#undef SECURITY_WIN32


/** @file
 * SSL/TLS support API.
 *
 * This file contains an SChannel-based implemention of the SSL/TLS API for Windows platforms.
 */

static void ssl_log_error(const char *fmt, ...);
static void ssl_log(pn_transport_t *transport, const char *fmt, ...);
static void ssl_log_error_status(HRESULT status, const char *fmt, ...);
static HCERTSTORE open_cert_db(const char *store_name, const char *passwd, int *error);

// Thread support.  Some SChannel objects are shared or ref-counted.
// Consistent with the rest of Proton, we assume a connection (and
// therefore its pn_ssl_t) will not be accessed concurrently by
// multiple threads.
class csguard {
  public:
    csguard(CRITICAL_SECTION *cs) : cs_(cs), set_(true) { EnterCriticalSection(cs_); }
    ~csguard() { if (set_) LeaveCriticalSection(cs_); }
    void release() {
        if (set_) {
            set_ = false;
            LeaveCriticalSection(cs_);
        }
    }
  private:
    LPCRITICAL_SECTION cs_;
    bool set_;
};

/*
 * win_credential_t: SChannel context that must accompany TLS connections.
 *
 * SChannel attempts session resumption for shared CredHandle objects.
 * To mimic openssl behavior, server CredHandle handles must be shared
 * by derived connections, client CredHandle handles must be unique
 * when app's session_id is null and tracked for reuse otherwise
 * (TODO).
 *
 * Ref counted by parent ssl_domain_t and each derived connection.
 */
struct win_credential_t {
  CRITICAL_SECTION cslock;
  pn_ssl_mode_t mode;
  PCCERT_CONTEXT cert_context;  // Particulars of the certificate (if any)
  CredHandle cred_handle;       // Bound session parameters, certificate, CAs, verification_mode
  HCERTSTORE trust_store;       // Store of root CAs for validating
  HCERTSTORE server_CA_certs;   // CAs advertised by server (may be a duplicate of the trust_store)
  char *trust_store_name;
};

#define win_credential_compare NULL
#define win_credential_inspect NULL
#define win_credential_hashcode NULL

static void win_credential_initialize(void *object)
{
  win_credential_t *c = (win_credential_t *) object;
  InitializeCriticalSectionAndSpinCount(&c->cslock, 4000);
  SecInvalidateHandle(&c->cred_handle);
  c->cert_context = 0;
  c->trust_store = 0;
  c->server_CA_certs = 0;
  c->trust_store_name = 0;
}

static void win_credential_finalize(void *object)
{
  win_credential_t *c = (win_credential_t *) object;
  if (SecIsValidHandle(&c->cred_handle))
    FreeCredentialsHandle(&c->cred_handle);
  if (c->cert_context)
    CertFreeCertificateContext(c->cert_context);
  if (c->trust_store)
    CertCloseStore(c->trust_store, 0);
  if (c->server_CA_certs)
    CertCloseStore(c->server_CA_certs, 0);
  DeleteCriticalSection(&c->cslock);
  free(c->trust_store_name);
}

static win_credential_t *win_credential(pn_ssl_mode_t m)
{
  static const pn_cid_t CID_win_credential = CID_pn_void;
  static const pn_class_t clazz = PN_CLASS(win_credential);
  win_credential_t *c = (win_credential_t *) pn_class_new(&clazz, sizeof(win_credential_t));
  c->mode = m;
  csguard g(&c->cslock);
  pn_incref(c);  // See next comment regarding refcounting and locks
  return c;
}

// Hack strategy for Proton object refcounting.  Just hold the lock for incref.
// Use the next two functions for decref, one with, the other without the lock.
// The refcount is artificially bumped by one in win_credential() so that we
// can use refcount == 1 to actually delete (by calling decref one last time).
static bool win_credential_decref(win_credential_t *c)
{
  // Call with lock held.  Caller MUST call win_credential_delete if this returns true.
  return pn_decref(c) == 1;
}

static void win_credential_delete(win_credential_t *c)
{
  // Call without lock held.
  assert(pn_refcount(c) == 1);
  pn_decref(c);
}

static int win_credential_load_cert(win_credential_t *cred, const char *store_name, const char *cert_name, const char *passwd)
{
  if (!store_name)
    return -2;

  int ec = 0;
  HCERTSTORE cert_store = open_cert_db(store_name, passwd, &ec);
  if (!cert_store)
    return ec;

  // find friendly name that matches cert_name, or sole certificate
  PCCERT_CONTEXT tmpctx = NULL;
  PCCERT_CONTEXT found_ctx = NULL;
  int cert_count = 0;
  int name_len = cert_name ? strlen(cert_name) : 0;
  char *fn = name_len ? (char *) malloc(name_len + 1) : 0;
  while (tmpctx = CertEnumCertificatesInStore(cert_store, tmpctx)) {
    cert_count++;
    if (cert_name && *cert_name) {
      DWORD len = CertGetNameString(tmpctx, CERT_NAME_FRIENDLY_DISPLAY_TYPE,
                                    0, NULL, NULL, 0);
      if (len != name_len + 1)
        continue;
      CertGetNameString(tmpctx, CERT_NAME_FRIENDLY_DISPLAY_TYPE,
                        0, NULL, fn, len);
      if (!strcmp(cert_name, fn)) {
        found_ctx = tmpctx;
        tmpctx= NULL;
        break;
      }
    } else {
      // Test for single certificate
      if (cert_count == 1) {
        found_ctx = CertDuplicateCertificateContext(tmpctx);
      } else {
        ssl_log_error("Multiple certificates to choose from certificate store %s\n", store_name);
        found_ctx = NULL;
        break;
      }
    }
  }

  if (tmpctx) {
    CertFreeCertificateContext(tmpctx);
    tmpctx = false;
  }
  if (!found_ctx && cert_name && cert_count == 1)
    ssl_log_error("Could not find certificate %s in store %s\n", cert_name, store_name);
  cred->cert_context = found_ctx;

  free(fn);
  CertCloseStore(cert_store, 0);
  return found_ctx ? 0 : -8;
}


// call with win_credential lock held
static CredHandle win_credential_cred_handle(win_credential_t *cred, pn_ssl_verify_mode_t verify_mode,
                                             const char *session_id, SECURITY_STATUS *status)
{
  if (cred->mode == PN_SSL_MODE_SERVER && SecIsValidHandle(&cred->cred_handle)) {
    *status = SEC_E_OK;
    return cred->cred_handle;  // Server always reuses cached value
  }
  // TODO: if (is_client && session_id != NULL) create or use cached value based on
  // session_id+server_host_name (per domain? reclaimed after X hours?)

  CredHandle tmp_handle;
  SecInvalidateHandle(&tmp_handle);
  TimeStamp expiry;  // Not used
  SCHANNEL_CRED descriptor;
  memset(&descriptor, 0, sizeof(descriptor));

  descriptor.dwVersion = SCHANNEL_CRED_VERSION;
  descriptor.dwFlags = SCH_CRED_NO_DEFAULT_CREDS | SCH_CRED_MANUAL_CRED_VALIDATION;
  if (cred->cert_context != NULL) {
    // assign the certificate into the credentials
    descriptor.paCred = &cred->cert_context;
    descriptor.cCreds = 1;
  }

  if (cred->mode == PN_SSL_MODE_SERVER) {
    descriptor.dwFlags |= SCH_CRED_NO_SYSTEM_MAPPER;
    if (cred->server_CA_certs) {
      descriptor.hRootStore = cred->server_CA_certs;
    }
  }

  ULONG direction = (cred->mode == PN_SSL_MODE_SERVER) ? SECPKG_CRED_INBOUND : SECPKG_CRED_OUTBOUND;
  *status = AcquireCredentialsHandle(NULL, UNISP_NAME, direction, NULL,
                                               &descriptor, NULL, NULL, &tmp_handle, &expiry);
  if (cred->mode == PN_SSL_MODE_SERVER && *status == SEC_E_OK)
    cred->cred_handle = tmp_handle;

  return tmp_handle;
}

static bool win_credential_has_certificate(win_credential_t *cred)
{
  if (!cred) return false;
  return (cred->cert_context != NULL);
}

#define SSL_DATA_SIZE 16384
#define SSL_BUF_SIZE (SSL_DATA_SIZE + 5 + 2048 + 32)

typedef enum { UNKNOWN_CONNECTION, SSL_CONNECTION, CLEAR_CONNECTION } connection_mode_t;
typedef struct pn_ssl_session_t pn_ssl_session_t;

struct pn_ssl_domain_t {
  CRITICAL_SECTION cslock;
  int ref_count;
  pn_ssl_mode_t mode;
  bool has_ca_db;       // true when CA database configured
  pn_ssl_verify_mode_t verify_mode;
  bool allow_unsecured;
  win_credential_t *cred;
};

typedef enum { CREATED, CLIENT_HELLO, NEGOTIATING,
               RUNNING, SHUTTING_DOWN, SSL_CLOSED } ssl_state_t;

struct pni_ssl_t {
  pn_ssl_domain_t  *domain;
  const char    *session_id;
  const char *peer_hostname;
  ssl_state_t state;

  bool protocol_detected;
  bool queued_shutdown;
  bool ssl_closed;            // shutdown complete, or SSL error
  ssize_t app_input_closed;   // error code returned by upper layer process input
  ssize_t app_output_closed;  // error code returned by upper layer process output

  // OpenSSL hides the protocol envelope bytes, SChannel has them in-line.
  char *sc_outbuf;     // SChannel output buffer
  size_t sc_out_size;
  size_t sc_out_count;
  char *network_outp;   // network ready bytes within sc_outbuf
  size_t network_out_pending;

  char *sc_inbuf;      // SChannel input buffer
  size_t sc_in_size;
  size_t sc_in_count;
  bool sc_in_incomplete;

  char *inbuf_extra;    // Still encrypted data from following Record(s)
  size_t extra_count;

  char *in_data;          // Just the plaintext data part of sc_inbuf, decrypted in place
  size_t in_data_size;
  size_t in_data_count;
  bool decrypting;
  size_t max_data_size;  // computed in the handshake

  pn_bytes_t app_inbytes; // Virtual decrypted datastream, presented to app layer

  pn_buffer_t *inbuf2;    // Second input buf if longer contiguous bytes needed
  bool double_buffered;

  bool sc_input_shutdown;

  CredHandle cred_handle;
  CtxtHandle ctxt_handle;
  SecPkgContext_StreamSizes sc_sizes;
  pn_ssl_verify_mode_t verify_mode;
  win_credential_t *cred;
  char *subject;
};

static inline pn_transport_t *get_transport_internal(pn_ssl_t *ssl)
{
  // The external pn_sasl_t is really a pointer to the internal pni_transport_t
  return ((pn_transport_t *)ssl);
}

static inline pni_ssl_t *get_ssl_internal(pn_ssl_t *ssl)
{
  // The external pn_sasl_t is really a pointer to the internal pni_transport_t
  return ssl ? ((pn_transport_t *)ssl)->ssl : NULL;
}

struct pn_ssl_session_t {
  const char       *id;
// TODO
  pn_ssl_session_t *ssn_cache_next;
  pn_ssl_session_t *ssn_cache_prev;
};


static ssize_t process_input_ssl( pn_transport_t *transport, unsigned int layer, const char *input_data, size_t len);
static ssize_t process_output_ssl( pn_transport_t *transport, unsigned int layer, char *input_data, size_t len);
static ssize_t process_input_done(pn_transport_t *transport, unsigned int layer, const char *input_data, size_t len);
static ssize_t process_output_done(pn_transport_t *transport, unsigned int layer, char *input_data, size_t len);
static pn_ssl_session_t *ssn_cache_find( pn_ssl_domain_t *, const char * );
static void ssl_session_free( pn_ssl_session_t *);
static size_t buffered_output( pn_transport_t *transport );
static void start_ssl_shutdown(pn_transport_t *transport);
static void rewind_sc_inbuf(pni_ssl_t *ssl);
static bool grow_inbuf2(pn_transport_t *ssl, size_t minimum_size);
static HRESULT verify_peer(pni_ssl_t *ssl, HCERTSTORE root_store, const char *server_name, bool tracing);

// @todo: used to avoid littering the code with calls to printf...
static void ssl_log_error(const char *fmt, ...)
{
  va_list ap;
  va_start(ap, fmt);
  vfprintf(stderr, fmt, ap);
  va_end(ap);
  fflush(stderr);
}

// @todo: used to avoid littering the code with calls to printf...
static void ssl_log(pn_transport_t *transport, const char *fmt, ...)
{
  if (PN_TRACE_DRV & transport->trace) {
    va_list ap;
    va_start(ap, fmt);
    vfprintf(stderr, fmt, ap);
    va_end(ap);
    fflush(stderr);
  }
}

static void ssl_log_error_status(HRESULT status, const char *fmt, ...)
{
  char buf[512];
  va_list ap;

  if (fmt) {
    va_start(ap, fmt);
    vfprintf(stderr, fmt, ap);
    va_end(ap);
  }

  if (FormatMessage(FORMAT_MESSAGE_MAX_WIDTH_MASK | FORMAT_MESSAGE_FROM_SYSTEM,
                    0, status, 0, buf, sizeof(buf), 0))
    ssl_log_error(" : %s\n", buf);
  else
    fprintf(stderr, "pn internal Windows error: %x for %x\n", GetLastError(), status);

  fflush(stderr);
}

static void ssl_log_clear_data(pn_transport_t *transport, const char *data, size_t len)
{
  if (PN_TRACE_RAW & transport->trace) {
    fprintf(stderr, "SSL decrypted data: \"");
    pn_fprint_data( stderr, data, len );
    fprintf(stderr, "\"\n");
  }
}

static size_t _pni_min(size_t a, size_t b)
{
  return (a < b) ? a : b;
}

// unrecoverable SSL failure occured, notify transport and generate error code.
static int ssl_failed(pn_transport_t *transport, const char *reason)
{
  char buf[512] = "Unknown error.";
  if (!reason) {
    HRESULT status = GetLastError();

    FormatMessage(FORMAT_MESSAGE_MAX_WIDTH_MASK | FORMAT_MESSAGE_FROM_SYSTEM,
                  0, status, 0, buf, sizeof(buf), 0);
    reason = buf;
  }
  pni_ssl_t *ssl = transport->ssl;
  ssl->ssl_closed = true;
  ssl->app_input_closed = ssl->app_output_closed = PN_EOS;
  ssl->state = SSL_CLOSED;
  pn_do_error(transport, "amqp:connection:framing-error", "SSL Failure: %s", reason);
  return PN_EOS;
}

static pn_ssl_session_t *ssn_cache_find( pn_ssl_domain_t *domain, const char *id )
{
// TODO:
  return NULL;
}

static void ssl_session_free( pn_ssl_session_t *ssn)
{
  if (ssn) {
    if (ssn->id) free( (void *)ssn->id );
    free( ssn );
  }
}


/** Public API - visible to application code */

bool pn_ssl_present(void)
{
  return true;
}

pn_ssl_domain_t *pn_ssl_domain( pn_ssl_mode_t mode )
{
  pn_ssl_domain_t *domain = (pn_ssl_domain_t *) calloc(1, sizeof(pn_ssl_domain_t));
  if (!domain) return NULL;

  InitializeCriticalSectionAndSpinCount(&domain->cslock, 4000);
  csguard(&domain->cslock);
  domain->ref_count = 1;
  domain->mode = mode;
  switch(mode) {
  case PN_SSL_MODE_CLIENT:
  case PN_SSL_MODE_SERVER:
    break;

  default:
    ssl_log_error("Invalid mode for pn_ssl_mode_t: %d\n", mode);
    free(domain);
    return NULL;
  }
  domain->cred = win_credential(mode);
  return domain;
}

// call with no locks
void pn_ssl_domain_free( pn_ssl_domain_t *domain )
{
  if (!domain) return;
  {
    csguard g(&domain->cslock);
    if (--domain->ref_count)
      return;
  }
  {
    csguard g2(&domain->cred->cslock);
    if (win_credential_decref(domain->cred)) {
      g2.release();
      win_credential_delete(domain->cred);
    }
  }
  DeleteCriticalSection(&domain->cslock);
  free(domain);
}


int pn_ssl_domain_set_credentials( pn_ssl_domain_t *domain,
                               const char *certificate_file,
                               const char *private_key_file,
                               const char *password)
{
  if (!domain) return -1;
  csguard g(&domain->cslock);
  csguard g2(&domain->cred->cslock);

  if (win_credential_has_certificate(domain->cred)) {
    // Need a new win_credential_t to hold new certificate
    if (win_credential_decref(domain->cred)) {
      g2.release();
      win_credential_delete(domain->cred);
    }
    domain->cred = win_credential(domain->mode);
    if (!domain->cred)
      return -1;
  }
  return win_credential_load_cert(domain->cred, certificate_file, private_key_file, password);
}


int pn_ssl_domain_set_trusted_ca_db(pn_ssl_domain_t *domain,
                                    const char *certificate_db)
{
  if (!domain || !certificate_db) return -1;
  csguard g(&domain->cslock);

  int ec = 0;
  HCERTSTORE store = open_cert_db(certificate_db, NULL, &ec);
  if (!store)
    return ec;

  if (domain->has_ca_db) {
    csguard g2(&domain->cred->cslock);
    win_credential_t *new_cred = win_credential(domain->mode);
    if (!new_cred) {
      CertCloseStore(store, 0);
      return -1;
    }
    new_cred->cert_context = CertDuplicateCertificateContext(domain->cred->cert_context);
    if (win_credential_decref(domain->cred)) {
      g2.release();
      win_credential_delete(domain->cred);
    }
    domain->cred = new_cred;
  }

  csguard g3(&domain->cred->cslock);
  domain->cred->trust_store = store;
  domain->cred->trust_store_name = pn_strdup(certificate_db);
  domain->has_ca_db = true;
  return 0;
}


int pn_ssl_domain_set_peer_authentication(pn_ssl_domain_t *domain,
                                          const pn_ssl_verify_mode_t mode,
                                          const char *trusted_CAs)
{
  if (!domain) return -1;
  csguard g(&domain->cslock);
  csguard g2(&domain->cred->cslock);

  if (!domain->has_ca_db && (mode == PN_SSL_VERIFY_PEER || mode == PN_SSL_VERIFY_PEER_NAME)) {
    ssl_log_error("Error: cannot verify peer without a trusted CA configured.\n"
                  "       Use pn_ssl_domain_set_trusted_ca_db()\n");
    return -1;
  }

  HCERTSTORE store = 0;
  bool changed = domain->verify_mode && mode != domain->verify_mode;

  switch (mode) {
  case PN_SSL_VERIFY_PEER:
  case PN_SSL_VERIFY_PEER_NAME:
    if (domain->mode == PN_SSL_MODE_SERVER) {
      if (!trusted_CAs) {
        ssl_log_error("Error: a list of trusted CAs must be provided.");
        return -1;
      }
      if (!win_credential_has_certificate(domain->cred)) {
        ssl_log_error("Error: Server cannot verify peer without configuring a certificate.\n"
                   "       Use pn_ssl_domain_set_credentials()");
        return -1;
      }
      int ec = 0;
      if (!strcmp(trusted_CAs, domain->cred->trust_store_name)) {
        changed = true;
        store = open_cert_db(trusted_CAs, NULL, &ec);
        if (!store)
          return ec;
      }
    }
    break;

  case PN_SSL_ANONYMOUS_PEER:   // hippie free love mode... :)
    break;

  default:
    ssl_log_error("Invalid peer authentication mode given.\n");
    return -1;
  }

  if (changed) {
    win_credential_t *new_cred = win_credential(domain->mode);
    if (!new_cred) {
      if (store)
        CertCloseStore(store, 0);
      return -1;
    }
    new_cred->cert_context = CertDuplicateCertificateContext(domain->cred->cert_context);
    new_cred->trust_store = CertDuplicateStore(domain->cred->trust_store);
    new_cred->trust_store_name = pn_strdup(domain->cred->trust_store_name);
    if (win_credential_decref(domain->cred)) {
      g2.release();
      win_credential_delete(domain->cred);
    }
    domain->cred = new_cred;
    domain->cred->server_CA_certs = store;
  }

  domain->verify_mode = mode;

  return 0;
}

int pn_ssl_domain_set_ciphers(pn_ssl_domain_t *domain, const char *ciphers)
{
  return PN_ERR;
}

int pn_ssl_domain_set_protocols(pn_ssl_domain_t *domain, const char *protocols)
{
  return PN_ERR;
}

const pn_io_layer_t ssl_layer = {
    process_input_ssl,
    process_output_ssl,
    NULL,
    NULL,
    buffered_output
};

const pn_io_layer_t ssl_input_closed_layer = {
    process_input_done,
    process_output_ssl,
    NULL,
    NULL,
    buffered_output
};

const pn_io_layer_t ssl_output_closed_layer = {
    process_input_ssl,
    process_output_done,
    NULL,
    NULL,
    buffered_output
};

const pn_io_layer_t ssl_closed_layer = {
    process_input_done,
    process_output_done,
    NULL,
    NULL,
    buffered_output
};

int pn_ssl_init(pn_ssl_t *ssl0, pn_ssl_domain_t *domain, const char *session_id)
{
  pn_transport_t *transport = get_transport_internal(ssl0);
  pni_ssl_t *ssl = transport->ssl;
  if (!ssl || !domain || ssl->domain) return -1;
  if (ssl->state != CREATED) return -1;

  csguard g(&domain->cslock);
  csguard g2(&domain->cred->cslock);
  ssl->domain = domain;
  domain->ref_count++;
  if (session_id && domain->mode == PN_SSL_MODE_CLIENT)
    ssl->session_id = pn_strdup(session_id);

  // If SSL doesn't specifically allow skipping encryption, require SSL
  // TODO: This is a probably a stop-gap until allow_unsecured is removed
  if (!domain->allow_unsecured) transport->encryption_required = true;

  ssl->cred = domain->cred;
  pn_incref(domain->cred);

  SECURITY_STATUS status = SEC_E_OK;
  ssl->cred_handle = win_credential_cred_handle(ssl->cred, ssl->verify_mode,
                                                ssl->session_id, &status);
  if (status != SEC_E_OK) {
    ssl_log_error_status(status, "Credentials handle failure");
    return -1;
  }

  ssl->state = (domain->mode == PN_SSL_MODE_CLIENT) ? CLIENT_HELLO : NEGOTIATING;
  ssl->verify_mode = domain->verify_mode;
  return 0;
}


int pn_ssl_domain_allow_unsecured_client(pn_ssl_domain_t *domain)
{
  if (!domain) return -1;
  if (domain->mode != PN_SSL_MODE_SERVER) {
    ssl_log_error("Cannot permit unsecured clients - not a server.\n");
    return -1;
  }
  domain->allow_unsecured = true;
  return 0;
}


// TODO: This is just an untested guess
int pn_ssl_get_ssf(pn_ssl_t *ssl0)
{
  SecPkgContext_ConnectionInfo info;

  pni_ssl_t *ssl = get_ssl_internal(ssl0);
  if (ssl &&
      ssl->state == RUNNING &&
      SecIsValidHandle(&ssl->ctxt_handle) &&
      QueryContextAttributes(&ssl->ctxt_handle, SECPKG_ATTR_CONNECTION_INFO, &info) == SEC_E_OK) {
    return info.dwCipherStrength;
  }
  return 0;
}

bool pn_ssl_get_cipher_name(pn_ssl_t *ssl0, char *buffer, size_t size )
{
  pni_ssl_t *ssl = get_ssl_internal(ssl0);
  *buffer = '\0';
  if (ssl->state != RUNNING || !SecIsValidHandle(&ssl->ctxt_handle))
    return false;
  SecPkgContext_ConnectionInfo info;
  if (QueryContextAttributes(&ssl->ctxt_handle, SECPKG_ATTR_CONNECTION_INFO, &info) == SEC_E_OK) {
    // TODO: come up with string for all permutations?
    pni_snprintf( buffer, size, "%x_%x:%x_%x:%x_%x",
              info.aiExch, info.dwExchStrength,
              info.aiCipher, info.dwCipherStrength,
              info.aiHash, info.dwHashStrength);
    return true;
  }
  return false;
}

bool pn_ssl_get_protocol_name(pn_ssl_t *ssl0, char *buffer, size_t size )
{
  pni_ssl_t *ssl = get_ssl_internal(ssl0);
  *buffer = '\0';
  if (ssl->state != RUNNING || !SecIsValidHandle(&ssl->ctxt_handle))
    return false;
  SecPkgContext_ConnectionInfo info;
  if (QueryContextAttributes(&ssl->ctxt_handle, SECPKG_ATTR_CONNECTION_INFO, &info) == SEC_E_OK) {
    if (info.dwProtocol & (SP_PROT_TLS1_CLIENT | SP_PROT_TLS1_SERVER))
      pni_snprintf(buffer, size, "%s", "TLSv1");
    // TLSV1.1 and TLSV1.2 are supported as of XP-SP3, but not defined until VS2010
    else if ((info.dwProtocol & 0x300))
      pni_snprintf(buffer, size, "%s", "TLSv1.1");
    else if ((info.dwProtocol & 0xC00))
      pni_snprintf(buffer, size, "%s", "TLSv1.2");
    else {
      ssl_log_error("unexpected protocol %x\n", info.dwProtocol);
      return false;
    }
    return true;
  }
  return false;
}


void pn_ssl_free( pn_transport_t *transport)
{
  pni_ssl_t *ssl = transport->ssl;
  if (!ssl) return;
  ssl_log( transport, "SSL socket freed.\n" );
  // clean up Windows per TLS session data before releasing the domain count
  csguard g(&ssl->domain->cslock);
  csguard g2(&ssl->cred->cslock);
  if (SecIsValidHandle(&ssl->ctxt_handle))
    DeleteSecurityContext(&ssl->ctxt_handle);
  if (ssl->cred) {
    if (ssl->domain->mode == PN_SSL_MODE_CLIENT && ssl->session_id == NULL) {
      // Responsible for unshared handle
      if (SecIsValidHandle(&ssl->cred_handle))
        FreeCredentialsHandle(&ssl->cred_handle);
    }
    if (win_credential_decref(ssl->cred)) {
      g2.release();
      win_credential_delete(ssl->cred);
    }
  }

  g2.release();
  g.release();
  pn_ssl_domain_free(ssl->domain);

  if (ssl->session_id) free((void *)ssl->session_id);
  if (ssl->peer_hostname) free((void *)ssl->peer_hostname);
  if (ssl->sc_inbuf) free((void *)ssl->sc_inbuf);
  if (ssl->sc_outbuf) free((void *)ssl->sc_outbuf);
  if (ssl->inbuf2) pn_buffer_free(ssl->inbuf2);
  if (ssl->subject) free(ssl->subject);

  free(ssl);
}

pn_ssl_t *pn_ssl(pn_transport_t *transport)
{
  if (!transport) return NULL;
  if (transport->ssl) return (pn_ssl_t *)transport;

  pni_ssl_t *ssl = (pni_ssl_t *) calloc(1, sizeof(pni_ssl_t));
  if (!ssl) return NULL;
  ssl->sc_out_size = ssl->sc_in_size = SSL_BUF_SIZE;

  ssl->sc_outbuf = (char *)malloc(ssl->sc_out_size);
  if (!ssl->sc_outbuf) {
    free(ssl);
    return NULL;
  }
  ssl->sc_inbuf = (char *)malloc(ssl->sc_in_size);
  if (!ssl->sc_inbuf) {
    free(ssl->sc_outbuf);
    free(ssl);
    return NULL;
  }

  ssl->inbuf2 = pn_buffer(0);
  if (!ssl->inbuf2) {
    free(ssl->sc_inbuf);
    free(ssl->sc_outbuf);
    free(ssl);
    return NULL;
  }

  transport->ssl = ssl;

  // Set up hostname from any bound connection
  if (transport->connection) {
    if (pn_string_size(transport->connection->hostname)) {
      pn_ssl_set_peer_hostname((pn_ssl_t *) transport, pn_string_get(transport->connection->hostname));
    }
  }

  SecInvalidateHandle(&ssl->cred_handle);
  SecInvalidateHandle(&ssl->ctxt_handle);
  ssl->state = CREATED;
  ssl->decrypting = true;

  return (pn_ssl_t *)transport;
}


pn_ssl_resume_status_t pn_ssl_resume_status( pn_ssl_t *ssl )
{
  // TODO
  return PN_SSL_RESUME_UNKNOWN;
}


int pn_ssl_set_peer_hostname( pn_ssl_t *ssl0, const char *hostname )
{
  pni_ssl_t *ssl = get_ssl_internal(ssl0);
  if (!ssl) return -1;

  if (ssl->peer_hostname) free((void *)ssl->peer_hostname);
  ssl->peer_hostname = NULL;
  if (hostname) {
    ssl->peer_hostname = pn_strdup(hostname);
    if (!ssl->peer_hostname) return -2;
  }
  return 0;
}

int pn_ssl_get_peer_hostname( pn_ssl_t *ssl0, char *hostname, size_t *bufsize )
{
  pni_ssl_t *ssl = get_ssl_internal(ssl0);
  if (!ssl) return -1;
  if (!ssl->peer_hostname) {
    *bufsize = 0;
    if (hostname) *hostname = '\0';
    return 0;
  }
  unsigned len = strlen(ssl->peer_hostname);
  if (hostname) {
    if (len >= *bufsize) return -1;
    strcpy( hostname, ssl->peer_hostname );
  }
  *bufsize = len;
  return 0;
}

const char* pn_ssl_get_remote_subject(pn_ssl_t *ssl0)
{
  // RFC 2253 compliant, but differs from openssl's subject string with space separators and
  // ordering of multicomponent RDNs.  Made to work as similarly as possible with choice of flags.
  pni_ssl_t *ssl = get_ssl_internal(ssl0);
  if (!ssl || !SecIsValidHandle(&ssl->ctxt_handle))
    return NULL;
  if (!ssl->subject) {
    SECURITY_STATUS status;
    PCCERT_CONTEXT peer_cc = 0;
    status = QueryContextAttributes(&ssl->ctxt_handle, SECPKG_ATTR_REMOTE_CERT_CONTEXT, &peer_cc);
    if (status != SEC_E_OK) {
      ssl_log_error_status(status, "can't obtain remote certificate subject");
      return NULL;
    }
    DWORD flags = CERT_X500_NAME_STR | CERT_NAME_STR_REVERSE_FLAG;
    DWORD strlen = CertNameToStr(peer_cc->dwCertEncodingType, &peer_cc->pCertInfo->Subject,
                                 flags, NULL, 0);
    if (strlen > 0) {
      ssl->subject = (char*) malloc(strlen);
      if (ssl->subject) {
        DWORD len = CertNameToStr(peer_cc->dwCertEncodingType, &peer_cc->pCertInfo->Subject,
                                  flags, ssl->subject, strlen);
        if (len != strlen) {
          free(ssl->subject);
          ssl->subject = NULL;
          ssl_log_error("pn_ssl_get_remote_subject failure in CertNameToStr");
        }
      }
    }
    CertFreeCertificateContext(peer_cc);
  }
  return ssl->subject;
}


/** SChannel specific: */

const char *tls_version_check(pni_ssl_t *ssl)
{
  SecPkgContext_ConnectionInfo info;
  QueryContextAttributes(&ssl->ctxt_handle, SECPKG_ATTR_CONNECTION_INFO, &info);
  // Ascending bit patterns denote newer SSL/TLS protocol versions.
  // SP_PROT_TLS1_0_SERVER is not defined until VS2010.
  return (info.dwProtocol < SP_PROT_TLS1_SERVER) ?
    "peer does not support TLS 1.0 security" : NULL;
}

static void ssl_encrypt(pn_transport_t *transport, char *app_data, size_t count)
{
  pni_ssl_t *ssl = transport->ssl;

  // Get SChannel to encrypt exactly one Record.
  SecBuffer buffs[4];
  buffs[0].cbBuffer = ssl->sc_sizes.cbHeader;
  buffs[0].BufferType = SECBUFFER_STREAM_HEADER;
  buffs[0].pvBuffer = ssl->sc_outbuf;
  buffs[1].cbBuffer = count;
  buffs[1].BufferType = SECBUFFER_DATA;
  buffs[1].pvBuffer = app_data;
  buffs[2].cbBuffer = ssl->sc_sizes.cbTrailer;
  buffs[2].BufferType = SECBUFFER_STREAM_TRAILER;
  buffs[2].pvBuffer = &app_data[count];
  buffs[3].cbBuffer = 0;
  buffs[3].BufferType = SECBUFFER_EMPTY;
  buffs[3].pvBuffer = 0;
  SecBufferDesc buff_desc;
  buff_desc.ulVersion = SECBUFFER_VERSION;
  buff_desc.cBuffers = 4;
  buff_desc.pBuffers = buffs;
  SECURITY_STATUS status = EncryptMessage(&ssl->ctxt_handle, 0, &buff_desc, 0);
  assert(status == SEC_E_OK);

  // EncryptMessage encrypts the data in place. The header and trailer
  // areas were reserved previously and must now be included in the updated
  // count of bytes to write to the peer.
  ssl->sc_out_count = buffs[0].cbBuffer + buffs[1].cbBuffer + buffs[2].cbBuffer;
  ssl->network_outp = ssl->sc_outbuf;
  ssl->network_out_pending = ssl->sc_out_count;
  ssl_log(transport, "ssl_encrypt %d network bytes\n", ssl->network_out_pending);
}

// Returns true if decryption succeeded (even for empty content)
static bool ssl_decrypt(pn_transport_t *transport)
{
  pni_ssl_t *ssl = transport->ssl;
  // Get SChannel to decrypt input.  May have an incomplete Record,
  // exactly one, or more than one.  Check also for session ending,
  // session renegotiation.

  SecBuffer recv_buffs[4];
  recv_buffs[0].cbBuffer = ssl->sc_in_count;
  recv_buffs[0].BufferType = SECBUFFER_DATA;
  recv_buffs[0].pvBuffer = ssl->sc_inbuf;
  recv_buffs[1].BufferType = SECBUFFER_EMPTY;
  recv_buffs[2].BufferType = SECBUFFER_EMPTY;
  recv_buffs[3].BufferType = SECBUFFER_EMPTY;
  SecBufferDesc buff_desc;
  buff_desc.ulVersion = SECBUFFER_VERSION;
  buff_desc.cBuffers = 4;
  buff_desc.pBuffers = recv_buffs;
  SECURITY_STATUS status = DecryptMessage(&ssl->ctxt_handle, &buff_desc, 0, NULL);

  if (status == SEC_E_INCOMPLETE_MESSAGE) {
    // Less than a full Record, come back later with more network data
    ssl->sc_in_incomplete = true;
    return false;
  }

  ssl->decrypting = false;

  if (status != SEC_E_OK) {
    rewind_sc_inbuf(ssl);
    switch (status) {
    case SEC_I_CONTEXT_EXPIRED:
      // TLS shutdown alert record.  Ignore all subsequent input.
      ssl->state = SHUTTING_DOWN;
      ssl->sc_input_shutdown = true;
      return false;

    case SEC_I_RENEGOTIATE:
      ssl_log_error("unexpected TLS renegotiation\n");
      // TODO.  Fall through for now.
    default:
      ssl_failed(transport, 0);
      return false;
    }
  }

  ssl->decrypting = false;
  // have a decrypted Record and possible (still-encrypted) data of
  // one (or more) later Recordss.  Adjust pointers accordingly.
  for (int i = 0; i < 4; i++) {
    switch (recv_buffs[i].BufferType) {
    case SECBUFFER_DATA:
      ssl->in_data = (char *) recv_buffs[i].pvBuffer;
      ssl->in_data_size = ssl->in_data_count = recv_buffs[i].cbBuffer;
      break;
    case SECBUFFER_EXTRA:
      ssl->inbuf_extra = (char *)recv_buffs[i].pvBuffer;
      ssl->extra_count = recv_buffs[i].cbBuffer;
      break;
    default:
      // SECBUFFER_STREAM_HEADER:
      // SECBUFFER_STREAM_TRAILER:
      break;
    }
  }
  return true;
}

static void client_handshake_init(pn_transport_t *transport)
{
  pni_ssl_t *ssl = transport->ssl;
  // Tell SChannel to create the first handshake token (ClientHello)
  // and place it in sc_outbuf
  SEC_CHAR *host = (SEC_CHAR *)(ssl->peer_hostname);
  ULONG ctxt_requested = ISC_REQ_STREAM | ISC_REQ_USE_SUPPLIED_CREDS | ISC_REQ_EXTENDED_ERROR;
  ULONG ctxt_attrs;

  SecBuffer send_buffs[2];
  send_buffs[0].cbBuffer = ssl->sc_out_size;
  send_buffs[0].BufferType = SECBUFFER_TOKEN;
  send_buffs[0].pvBuffer = ssl->sc_outbuf;
  send_buffs[1].cbBuffer = 0;
  send_buffs[1].BufferType = SECBUFFER_EMPTY;
  send_buffs[1].pvBuffer = 0;
  SecBufferDesc send_buff_desc;
  send_buff_desc.ulVersion = SECBUFFER_VERSION;
  send_buff_desc.cBuffers = 2;
  send_buff_desc.pBuffers = send_buffs;
  csguard g(&ssl->cred->cslock);
  SECURITY_STATUS status = InitializeSecurityContext(&ssl->cred_handle,
                               NULL, host, ctxt_requested, 0, 0, NULL, 0,
                               &ssl->ctxt_handle, &send_buff_desc,
                               &ctxt_attrs, NULL);

  if (status == SEC_I_CONTINUE_NEEDED) {
    ssl->sc_out_count = send_buffs[0].cbBuffer;
    ssl->network_out_pending = ssl->sc_out_count;
    // the token is the whole quantity to send
    ssl->network_outp = ssl->sc_outbuf;
    ssl_log(transport, "Sending client hello %d bytes\n", ssl->network_out_pending);
  } else {
    ssl_log_error_status(status, "InitializeSecurityContext failed");
    ssl_failed(transport, 0);
  }
}

static void client_handshake( pn_transport_t* transport) {
  pni_ssl_t *ssl = transport->ssl;
  // Feed SChannel ongoing responses from the server until the handshake is complete.
  SEC_CHAR *host = (SEC_CHAR *)(ssl->peer_hostname);
  ULONG ctxt_requested = ISC_REQ_STREAM | ISC_REQ_USE_SUPPLIED_CREDS;
  ULONG ctxt_attrs;
  size_t max = 0;

  // token_buffs describe the buffer that's coming in. It should have
  // a token from the SSL server, or empty if sending final shutdown alert.
  bool shutdown = ssl->state == SHUTTING_DOWN;
  SecBuffer token_buffs[2];
  token_buffs[0].cbBuffer = shutdown ? 0 : ssl->sc_in_count;
  token_buffs[0].BufferType = SECBUFFER_TOKEN;
  token_buffs[0].pvBuffer = shutdown ? 0 : ssl->sc_inbuf;
  token_buffs[1].cbBuffer = 0;
  token_buffs[1].BufferType = SECBUFFER_EMPTY;
  token_buffs[1].pvBuffer = 0;
  SecBufferDesc token_buff_desc;
  token_buff_desc.ulVersion = SECBUFFER_VERSION;
  token_buff_desc.cBuffers = 2;
  token_buff_desc.pBuffers = token_buffs;

  // send_buffs will hold information to forward to the peer.
  SecBuffer send_buffs[2];
  send_buffs[0].cbBuffer = ssl->sc_out_size;
  send_buffs[0].BufferType = SECBUFFER_TOKEN;
  send_buffs[0].pvBuffer = ssl->sc_outbuf;
  send_buffs[1].cbBuffer = 0;
  send_buffs[1].BufferType = SECBUFFER_EMPTY;
  send_buffs[1].pvBuffer = 0;
  SecBufferDesc send_buff_desc;
  send_buff_desc.ulVersion = SECBUFFER_VERSION;
  send_buff_desc.cBuffers = 2;
  send_buff_desc.pBuffers = send_buffs;

  SECURITY_STATUS status;
  {
    csguard g(&ssl->cred->cslock);
    status = InitializeSecurityContext(&ssl->cred_handle,
                               &ssl->ctxt_handle, host, ctxt_requested, 0, 0,
                               &token_buff_desc, 0, NULL, &send_buff_desc,
                               &ctxt_attrs, NULL);
  }

  switch (status) {
  case SEC_E_INCOMPLETE_MESSAGE:
    // Not enough - get more data from the server then try again.
    // Leave input buffers untouched.
    ssl_log(transport, "client handshake: incomplete record\n");
    ssl->sc_in_incomplete = true;
    return;

  case SEC_I_CONTINUE_NEEDED:
    // Successful handshake step, requiring data to be sent to peer.
    ssl->sc_out_count = send_buffs[0].cbBuffer;
    // the token is the whole quantity to send
    ssl->network_out_pending = ssl->sc_out_count;
    ssl->network_outp = ssl->sc_outbuf;
    ssl_log(transport, "client handshake token %d bytes\n", ssl->network_out_pending);
    break;

  case SEC_E_OK:
    // Handshake complete.
    if (shutdown) {
      if (send_buffs[0].cbBuffer > 0) {
        ssl->sc_out_count = send_buffs[0].cbBuffer;
        // the token is the whole quantity to send
        ssl->network_out_pending = ssl->sc_out_count;
        ssl->network_outp = ssl->sc_outbuf;
        ssl_log(transport, "client shutdown token %d bytes\n", ssl->network_out_pending);
      } else {
        ssl->state = SSL_CLOSED;
      }
      // we didn't touch sc_inbuf, no need to reset
      return;
    }
    if (send_buffs[0].cbBuffer != 0) {
      ssl_failed(transport, "unexpected final server token");
      break;
    }
    if (const char *err = tls_version_check(ssl)) {
      ssl_failed(transport, err);
      break;
    }
    if (ssl->verify_mode == PN_SSL_VERIFY_PEER || ssl->verify_mode == PN_SSL_VERIFY_PEER_NAME) {
      bool tracing = PN_TRACE_DRV & transport->trace;
      HRESULT ec = verify_peer(ssl, ssl->cred->trust_store, ssl->peer_hostname, tracing);
      if (ec) {
        if (ssl->peer_hostname)
          ssl_log_error_status(ec, "certificate verification failed for host %s\n", ssl->peer_hostname);
        else
          ssl_log_error_status(ec, "certificate verification failed\n");
        ssl_failed(transport, "TLS certificate verification error");
        break;
      }
    }

    if (token_buffs[1].BufferType == SECBUFFER_EXTRA && token_buffs[1].cbBuffer > 0) {
      // This seems to work but not documented, plus logic differs from decrypt message
      // since the pvBuffer value is not set.  Grrr.
      ssl->extra_count = token_buffs[1].cbBuffer;
      ssl->inbuf_extra = ssl->sc_inbuf + (ssl->sc_in_count - ssl->extra_count);
    }

    QueryContextAttributes(&ssl->ctxt_handle,
                             SECPKG_ATTR_STREAM_SIZES, &ssl->sc_sizes);
    max = ssl->sc_sizes.cbMaximumMessage + ssl->sc_sizes.cbHeader + ssl->sc_sizes.cbTrailer;
    if (max > ssl->sc_out_size) {
      ssl_log_error("Buffer size mismatch have %d, need %d\n", (int) ssl->sc_out_size, (int) max);
      ssl->state = SHUTTING_DOWN;
      ssl->app_input_closed = ssl->app_output_closed = PN_ERR;
      start_ssl_shutdown(transport);
      pn_do_error(transport, "amqp:connection:framing-error", "SSL Failure: buffer size");
      break;
    }

    ssl->state = RUNNING;
    ssl->max_data_size = max - ssl->sc_sizes.cbHeader - ssl->sc_sizes.cbTrailer;
    ssl_log(transport, "client handshake successful %d max record size\n", max);
    break;

  case SEC_I_CONTEXT_EXPIRED:
    // ended before we got going
  default:
    ssl_log(transport, "client handshake failed %d\n", (int) status);
    ssl_failed(transport, 0);
    break;
  }

  if (token_buffs[1].BufferType == SECBUFFER_EXTRA && token_buffs[1].cbBuffer > 0 &&
      !ssl->ssl_closed) {
    // remaining data after the consumed TLS record(s)
    ssl->extra_count = token_buffs[1].cbBuffer;
    ssl->inbuf_extra = ssl->sc_inbuf + (ssl->sc_in_count - ssl->extra_count);
  }

  ssl->decrypting = false;
  rewind_sc_inbuf(ssl);
}


static void server_handshake(pn_transport_t* transport)
{
  pni_ssl_t *ssl = transport->ssl;
  if (!ssl->protocol_detected) {
    // SChannel fails less aggressively than openssl on client hello, causing hangs
    // waiting for more bytes.  Help out here.
    pni_protocol_type_t type = pni_sniff_header(ssl->sc_inbuf, ssl->sc_in_count);
    if (type == PNI_PROTOCOL_INSUFFICIENT) {
      ssl_log(transport, "server handshake: incomplete record\n");
      ssl->sc_in_incomplete = true;
      return;
    } else {
      ssl->protocol_detected = true;
      if (type != PNI_PROTOCOL_SSL) {
        ssl_failed(transport, "bad client hello");
        ssl->decrypting = false;
        rewind_sc_inbuf(ssl);
        return;
      }
    }
  }

  // Feed SChannel ongoing handshake records from the client until the handshake is complete.
  ULONG ctxt_requested = ASC_REQ_STREAM | ASC_REQ_EXTENDED_ERROR;
  if (ssl->verify_mode == PN_SSL_VERIFY_PEER || ssl->verify_mode == PN_SSL_VERIFY_PEER_NAME)
    ctxt_requested |= ASC_REQ_MUTUAL_AUTH;
  ULONG ctxt_attrs;
  size_t max = 0;

  // token_buffs describe the buffer that's coming in. It should have
  // a token from the SSL client except if shutting down or renegotiating.
  bool shutdown = ssl->state == SHUTTING_DOWN;
  SecBuffer token_buffs[2];
  token_buffs[0].cbBuffer = shutdown ? 0 : ssl->sc_in_count;
  token_buffs[0].BufferType = SECBUFFER_TOKEN;
  token_buffs[0].pvBuffer = shutdown ? 0 : ssl->sc_inbuf;
  token_buffs[1].cbBuffer = 0;
  token_buffs[1].BufferType = SECBUFFER_EMPTY;
  token_buffs[1].pvBuffer = 0;
  SecBufferDesc token_buff_desc;
  token_buff_desc.ulVersion = SECBUFFER_VERSION;
  token_buff_desc.cBuffers = 2;
  token_buff_desc.pBuffers = token_buffs;

  // send_buffs will hold information to forward to the peer.
  SecBuffer send_buffs[2];
  send_buffs[0].cbBuffer = ssl->sc_out_size;
  send_buffs[0].BufferType = SECBUFFER_TOKEN;
  send_buffs[0].pvBuffer = ssl->sc_outbuf;
  send_buffs[1].cbBuffer = 0;
  send_buffs[1].BufferType = SECBUFFER_EMPTY;
  send_buffs[1].pvBuffer = 0;
  SecBufferDesc send_buff_desc;
  send_buff_desc.ulVersion = SECBUFFER_VERSION;
  send_buff_desc.cBuffers = 2;
  send_buff_desc.pBuffers = send_buffs;
  PCtxtHandle ctxt_handle_ptr = (SecIsValidHandle(&ssl->ctxt_handle)) ? &ssl->ctxt_handle : 0;

  SECURITY_STATUS status;
  {
    csguard g(&ssl->cred->cslock);
    status = AcceptSecurityContext(&ssl->cred_handle, ctxt_handle_ptr,
                               &token_buff_desc, ctxt_requested, 0, &ssl->ctxt_handle,
                               &send_buff_desc, &ctxt_attrs, NULL);
  }
  bool outbound_token = false;
  switch(status) {
  case SEC_E_INCOMPLETE_MESSAGE:
    // Not enough - get more data from the client then try again.
    // Leave input buffers untouched.
    ssl_log(transport, "server handshake: incomplete record\n");
    ssl->sc_in_incomplete = true;
    return;

  case SEC_I_CONTINUE_NEEDED:
    outbound_token = true;
    break;

  case SEC_E_OK:
    // Handshake complete.
    if (shutdown) {
      if (send_buffs[0].cbBuffer > 0) {
        ssl->sc_out_count = send_buffs[0].cbBuffer;
        // the token is the whole quantity to send
        ssl->network_out_pending = ssl->sc_out_count;
        ssl->network_outp = ssl->sc_outbuf;
        ssl_log(transport, "server shutdown token %d bytes\n", ssl->network_out_pending);
      } else {
        ssl->state = SSL_CLOSED;
      }
      // we didn't touch sc_inbuf, no need to reset
      return;
    }
    if (const char *err = tls_version_check(ssl)) {
      ssl_failed(transport, err);
      break;
    }
    // Handshake complete.

    if (ssl->verify_mode == PN_SSL_VERIFY_PEER || ssl->verify_mode == PN_SSL_VERIFY_PEER_NAME) {
      bool tracing = PN_TRACE_DRV & transport->trace;
      HRESULT ec = verify_peer(ssl, ssl->cred->trust_store, NULL, tracing);
      if (ec) {
        ssl_log_error_status(ec, "certificate verification failed\n");
        ssl_failed(transport, "certificate verification error");
        break;
      }
    }

    QueryContextAttributes(&ssl->ctxt_handle,
                             SECPKG_ATTR_STREAM_SIZES, &ssl->sc_sizes);
    max = ssl->sc_sizes.cbMaximumMessage + ssl->sc_sizes.cbHeader + ssl->sc_sizes.cbTrailer;
    if (max > ssl->sc_out_size) {
      ssl_log_error("Buffer size mismatch have %d, need %d\n", (int) ssl->sc_out_size, (int) max);
      ssl->state = SHUTTING_DOWN;
      ssl->app_input_closed = ssl->app_output_closed = PN_ERR;
      start_ssl_shutdown(transport);
      pn_do_error(transport, "amqp:connection:framing-error", "SSL Failure: buffer size");
      break;
    }

    if (send_buffs[0].cbBuffer != 0)
      outbound_token = true;

    ssl->state = RUNNING;
    ssl->max_data_size = max - ssl->sc_sizes.cbHeader - ssl->sc_sizes.cbTrailer;
    ssl_log(transport, "server handshake successful %d max record size\n", max);
    break;

  case SEC_I_CONTEXT_EXPIRED:
    // ended before we got going
  default:
    ssl_log(transport, "server handshake failed %d\n", (int) status);
    ssl_failed(transport, 0);
    break;
  }

  if (outbound_token) {
    // Successful handshake step, requiring data to be sent to peer.
    assert(ssl->network_out_pending == 0);
    ssl->sc_out_count = send_buffs[0].cbBuffer;
    // the token is the whole quantity to send
    ssl->network_out_pending = ssl->sc_out_count;
    ssl->network_outp = ssl->sc_outbuf;
    ssl_log(transport, "server handshake token %d bytes\n", ssl->network_out_pending);
  }

  if (token_buffs[1].BufferType == SECBUFFER_EXTRA && token_buffs[1].cbBuffer > 0 &&
      !ssl->ssl_closed) {
    // remaining data after the consumed TLS record(s)
    ssl->extra_count = token_buffs[1].cbBuffer;
    ssl->inbuf_extra = ssl->sc_inbuf + (ssl->sc_in_count - ssl->extra_count);
  }

  ssl->decrypting = false;
  rewind_sc_inbuf(ssl);
}

static void ssl_handshake(pn_transport_t* transport) {
  if (transport->ssl->domain->mode == PN_SSL_MODE_CLIENT)
    client_handshake(transport);
  else {
    server_handshake(transport);
  }
}

static bool grow_inbuf2(pn_transport_t *transport, size_t minimum_size) {
  pni_ssl_t *ssl = transport->ssl;
  size_t old_capacity = pn_buffer_capacity(ssl->inbuf2);
  size_t new_capacity = old_capacity ? old_capacity * 2 : 1024;

  while (new_capacity < minimum_size)
    new_capacity *= 2;

  uint32_t max_frame = pn_transport_get_max_frame(transport);
  if (max_frame != 0) {
    if (old_capacity >= max_frame) {
      //  already big enough
      ssl_log(transport, "Application expecting %d bytes (> negotiated maximum frame)\n", new_capacity);
      ssl_failed(transport, "TLS: transport maximum frame size error");
      return false;
    }
  }

  size_t extra_bytes = new_capacity - pn_buffer_size(ssl->inbuf2);
  int err = pn_buffer_ensure(ssl->inbuf2, extra_bytes);
  if (err) {
    ssl_log(transport, "TLS memory allocation failed for %d bytes\n", max_frame);
    ssl_failed(transport, "TLS memory allocation failed");
    return false;
  }
  return true;
}


// Peer initiated a session end by sending us a shutdown alert (and we should politely
// reciprocate), or else we are initiating the session end (and will not bother to wait
// for the peer shutdown alert). Stop processing input immediately, and stop processing
// output once this is sent.

static void start_ssl_shutdown(pn_transport_t *transport)
{
  pni_ssl_t *ssl = transport->ssl;
  assert(ssl->network_out_pending == 0);
  if (ssl->queued_shutdown)
    return;
  ssl->queued_shutdown = true;
  ssl_log(transport, "Shutting down SSL connection...\n");

  DWORD shutdown = SCHANNEL_SHUTDOWN;
  SecBuffer shutBuff;
  shutBuff.cbBuffer = sizeof(DWORD);
  shutBuff.BufferType = SECBUFFER_TOKEN;
  shutBuff.pvBuffer = &shutdown;
  SecBufferDesc desc;
  desc.ulVersion = SECBUFFER_VERSION;
  desc.cBuffers = 1;
  desc.pBuffers = &shutBuff;
  ApplyControlToken(&ssl->ctxt_handle, &desc);

  // Next handshake will generate the shutdown alert token
  ssl_handshake(transport);
}

static void rewind_sc_inbuf(pni_ssl_t *ssl)
{
  // Decrypted bytes have been drained or double buffered.  Prepare for the next SSL Record.
  assert(ssl->in_data_count == 0);
  if (ssl->decrypting)
    return;
  ssl->decrypting = true;
  if (ssl->inbuf_extra) {
    // A previous read picked up more than one Record.  Move it to the beginning.
    memmove(ssl->sc_inbuf, ssl->inbuf_extra, ssl->extra_count);
    ssl->sc_in_count = ssl->extra_count;
    ssl->inbuf_extra = 0;
    ssl->extra_count = 0;
  } else {
    ssl->sc_in_count = 0;
  }
}

static void app_inbytes_add(pn_transport_t *transport)
{
  pni_ssl_t *ssl = transport->ssl;
  if (!ssl->app_inbytes.start) {
    ssl->app_inbytes.start = ssl->in_data;
    ssl->app_inbytes.size = ssl->in_data_count;
    return;
  }

  if (ssl->double_buffered) {
    if (pn_buffer_available(ssl->inbuf2) == 0) {
      if (!grow_inbuf2(transport, 1024))
        // could not add room
        return;
    }
    size_t count = _pni_min(ssl->in_data_count, pn_buffer_available(ssl->inbuf2));
    pn_buffer_append(ssl->inbuf2, ssl->in_data, count);
    ssl->in_data += count;
    ssl->in_data_count -= count;
    ssl->app_inbytes = pn_buffer_bytes(ssl->inbuf2);
  } else {
    assert(ssl->app_inbytes.size == 0);
    ssl->app_inbytes.start = ssl->in_data;
    ssl->app_inbytes.size = ssl->in_data_count;
  }
}


static void app_inbytes_progress(pn_transport_t *transport, size_t minimum)
{
  pni_ssl_t *ssl = transport->ssl;
  // Make more decrypted data available, if possible.  Otherwise, move
  // unread bytes to front of inbuf2 to make room for next bulk decryption.
  // SSL may have chopped up data that app layer expects to be
  // contiguous.  Start, continue or stop double buffering here.
  if (ssl->double_buffered) {
    if (ssl->app_inbytes.size == 0) {
      // no straggler bytes, optimistically stop for now
      ssl->double_buffered = false;
      pn_buffer_clear(ssl->inbuf2);
      ssl->app_inbytes.start = ssl->in_data;
      ssl->app_inbytes.size = ssl->in_data_count;
    } else {
      pn_bytes_t ib2 = pn_buffer_bytes(ssl->inbuf2);
      assert(ssl->app_inbytes.size <= ib2.size);
      size_t consumed = ib2.size - ssl->app_inbytes.size;
      if (consumed > 0) {
        memmove((void *)ib2.start, ib2.start + consumed, ssl->app_inbytes.size);
        pn_buffer_trim(ssl->inbuf2, 0, consumed);
      }
      if (!pn_buffer_available(ssl->inbuf2)) {
        if (!grow_inbuf2(transport, minimum))
          // could not add room
          return;
      }
      size_t count = _pni_min(ssl->in_data_count, pn_buffer_available(ssl->inbuf2));
      pn_buffer_append(ssl->inbuf2, ssl->in_data, count);
      ssl->in_data += count;
      ssl->in_data_count -= count;
      ssl->app_inbytes = pn_buffer_bytes(ssl->inbuf2);
    }
  } else {
    if (ssl->app_inbytes.size) {
      // start double buffering the left over bytes
      ssl->double_buffered = true;
      pn_buffer_clear(ssl->inbuf2);
      if (!pn_buffer_available(ssl->inbuf2)) {
        if (!grow_inbuf2(transport, minimum))
          // could not add room
          return;
      }
      size_t count = _pni_min(ssl->in_data_count, pn_buffer_available(ssl->inbuf2));
      pn_buffer_append(ssl->inbuf2, ssl->in_data, count);
      ssl->in_data += count;
      ssl->in_data_count -= count;
      ssl->app_inbytes = pn_buffer_bytes(ssl->inbuf2);
    } else {
      // already pointing at all available bytes until next decrypt
    }
  }
  if (ssl->in_data_count == 0)
    rewind_sc_inbuf(ssl);
}


static void app_inbytes_advance(pn_transport_t *transport, size_t consumed)
{
  pni_ssl_t *ssl = transport->ssl;
  if (consumed == 0) {
    // more contiguous bytes required
    app_inbytes_progress(transport, ssl->app_inbytes.size + 1);
    return;
  }
  assert(consumed <= ssl->app_inbytes.size);
  ssl->app_inbytes.start += consumed;
  ssl->app_inbytes.size -= consumed;
  if (!ssl->double_buffered) {
    ssl->in_data += consumed;
    ssl->in_data_count -= consumed;
  }
  if (ssl->app_inbytes.size == 0)
    app_inbytes_progress(transport, 0);
}

static void read_closed(pn_transport_t *transport, unsigned int layer, ssize_t error)
{
  pni_ssl_t *ssl = transport->ssl;
  if (ssl->app_input_closed)
    return;
  if (ssl->state == RUNNING && !error) {
    // Signal end of stream
    ssl->app_input_closed = transport->io_layers[layer+1]->process_input(transport, layer+1, ssl->app_inbytes.start, 0);
  }
  if (!ssl->app_input_closed)
    ssl->app_input_closed = error ? error : PN_ERR;

  if (ssl->app_output_closed) {
    // both sides of app closed, and no more app output pending:
    ssl->state = SHUTTING_DOWN;
    if (ssl->network_out_pending == 0 && !ssl->queued_shutdown) {
      start_ssl_shutdown(transport);
    }
  }
}


// Read up to "available" bytes from the network, decrypt it and pass plaintext to application.

static ssize_t process_input_ssl(pn_transport_t *transport, unsigned int layer, const char *input_data, size_t available)
{
  pni_ssl_t *ssl = transport->ssl;
  ssl_log( transport, "process_input_ssl( data size=%d )\n",available );
  ssize_t consumed = 0;
  ssize_t forwarded = 0;
  bool new_app_input;

  if (available == 0) {
    // No more inbound network data
    read_closed(transport, layer, 0);
    return 0;
  }

  do {
    if (ssl->sc_input_shutdown) {
      // TLS protocol shutdown detected on input, so we are done.
      read_closed(transport, layer, 0);
      return PN_EOS;
    }

    // sc_inbuf should be ready for new or additional network encrypted bytes.
    // i.e. no straggling decrypted bytes pending.
    assert(ssl->in_data_count == 0 && ssl->decrypting);
    new_app_input = false;
    size_t count;

    if (ssl->state != RUNNING) {
      count = _pni_min(ssl->sc_in_size - ssl->sc_in_count, available);
    } else {
      // look for TLS record boundaries
      if (ssl->sc_in_count < 5) {
        ssl->sc_in_incomplete = true;
        size_t hc = _pni_min(available, 5 - ssl->sc_in_count);
        memmove(ssl->sc_inbuf + ssl->sc_in_count, input_data, hc);
        ssl->sc_in_count += hc;
        input_data += hc;
        available -= hc;
        consumed += hc;
        if (ssl->sc_in_count < 5 || available == 0)
          break;
      }

      // Top up sc_inbuf from network input_data hoping for a complete TLS Record
      // We try to guess the length as an optimization, but let SChannel
      // ultimately decide if there is spoofing going on.
      unsigned char low = (unsigned char) ssl->sc_inbuf[4];
      unsigned char high = (unsigned char) ssl->sc_inbuf[3];
      size_t rec_len = high * 256 + low + 5;
      if (rec_len < 5 || rec_len == ssl->sc_in_count || rec_len > ssl->sc_in_size)
        rec_len = ssl->sc_in_size;

      count = _pni_min(rec_len - ssl->sc_in_count, available);
    }

    if (count > 0) {
      memmove(ssl->sc_inbuf + ssl->sc_in_count, input_data, count);
      ssl->sc_in_count += count;
      input_data += count;
      available -= count;
      consumed += count;
      ssl->sc_in_incomplete = false;
    }

    // Try to decrypt another TLS Record.

    if (ssl->sc_in_count > 0 && ssl->state <= SHUTTING_DOWN) {
      if (ssl->state == NEGOTIATING) {
        ssl_handshake(transport);
      } else {
        if (ssl_decrypt(transport)) {
          // Ignore TLS Record with 0 length data (does not mean EOS)
          if (ssl->in_data_size > 0) {
            new_app_input = true;
            app_inbytes_add(transport);
          } else {
            assert(ssl->decrypting == false);
            rewind_sc_inbuf(ssl);
          }
        }
        ssl_log(transport, "Next decryption, %d left over\n", available);
      }
    }

    if (ssl->state == SHUTTING_DOWN) {
      if (ssl->network_out_pending == 0 && !ssl->queued_shutdown) {
        start_ssl_shutdown(transport);
      }
    } else if (ssl->state == SSL_CLOSED) {
      return PN_EOS;
    }

    // Consume or discard the decrypted bytes
    if (new_app_input && (ssl->state == RUNNING || ssl->state == SHUTTING_DOWN)) {
      // present app_inbytes to io_next only if it has new content
      while (ssl->app_inbytes.size > 0) {
        if (!ssl->app_input_closed) {
          ssize_t count = transport->io_layers[layer+1]->process_input(transport, layer+1, ssl->app_inbytes.start, ssl->app_inbytes.size);
          if (count > 0) {
            forwarded += count;
            // advance() can increase app_inbytes.size if double buffered
            app_inbytes_advance(transport, count);
            ssl_log(transport, "Application consumed %d bytes from peer\n", (int) count);
          } else if (count == 0) {
            size_t old_size = ssl->app_inbytes.size;
            app_inbytes_advance(transport, 0);
            if (ssl->app_inbytes.size == old_size) {
              break;  // no additional contiguous decrypted data available, get more network data
            }
          } else {
            // count < 0
            ssl_log(transport, "Application layer closed its input, error=%d (discarding %d bytes)\n",
                 (int) count, (int)ssl->app_inbytes.size);
            app_inbytes_advance(transport, ssl->app_inbytes.size);    // discard
            read_closed(transport, layer, count);
          }
        } else {
          ssl_log(transport, "Input closed discard %d bytes\n",
               (int)ssl->app_inbytes.size);
          app_inbytes_advance(transport, ssl->app_inbytes.size);      // discard
        }
      }
    }
  } while (available || (ssl->sc_in_count && !ssl->sc_in_incomplete));

  if (ssl->state >= SHUTTING_DOWN) {
    if (ssl->app_input_closed || ssl->sc_input_shutdown) {
      // Next layer doesn't want more bytes, or it can't process without more data than it has seen so far
      // but the ssl stream has ended
      consumed = ssl->app_input_closed ? ssl->app_input_closed : PN_EOS;
      if (transport->io_layers[layer]==&ssl_output_closed_layer) {
        transport->io_layers[layer] = &ssl_closed_layer;
      } else {
        transport->io_layers[layer] = &ssl_input_closed_layer;
      }
    }
  }
  ssl_log(transport, "process_input_ssl() returning %d, forwarded %d\n", (int) consumed, (int) forwarded);
  return consumed;
}

static ssize_t process_output_ssl( pn_transport_t *transport, unsigned int layer, char *buffer, size_t max_len)
{
  pni_ssl_t *ssl = transport->ssl;
  if (!ssl) return PN_EOS;
  ssl_log( transport, "process_output_ssl( max_len=%d )\n",max_len );

  ssize_t written = 0;
  ssize_t total_app_bytes = 0;
  bool work_pending;

  if (ssl->state == CLIENT_HELLO) {
    // output buffers eclusively for internal handshake use until negotiation complete
    client_handshake_init(transport);
    if (ssl->state == SSL_CLOSED)
      return PN_EOS;
    ssl->state = NEGOTIATING;
  }

  do {
    work_pending = false;

    if (ssl->network_out_pending > 0) {
      size_t wcount = _pni_min(ssl->network_out_pending, max_len);
      memmove(buffer, ssl->network_outp, wcount);
      ssl->network_outp += wcount;
      ssl->network_out_pending -= wcount;
      buffer += wcount;
      max_len -= wcount;
      written += wcount;
    }

    if (ssl->network_out_pending == 0 && ssl->state == RUNNING  && !ssl->app_output_closed) {
      // refill the buffer with app data and encrypt it

      char *app_data = ssl->sc_outbuf + ssl->sc_sizes.cbHeader;
      char *app_outp = app_data;
      size_t remaining = ssl->max_data_size;
      ssize_t app_bytes;
      do {
        app_bytes = transport->io_layers[layer+1]->process_output(transport, layer+1, app_outp, remaining);
        if (app_bytes > 0) {
          app_outp += app_bytes;
          remaining -= app_bytes;
          ssl_log( transport, "Gathered %d bytes from app to send to peer\n", app_bytes );
        } else {
          if (app_bytes < 0) {
            ssl_log(transport, "Application layer closed its output, error=%d (%d bytes pending send)\n",
                 (int) app_bytes, (int) ssl->network_out_pending);
            ssl->app_output_closed = app_bytes;
            if (ssl->app_input_closed)
              ssl->state = SHUTTING_DOWN;
          } else if (total_app_bytes == 0 && ssl->app_input_closed) {
            // We've drained all the App layer can provide
            ssl_log(transport, "Application layer blocked on input, closing\n");
            ssl->state = SHUTTING_DOWN;
            ssl->app_output_closed = PN_ERR;
          }
        }
      } while (app_bytes > 0);
      if (app_outp > app_data) {
        work_pending = (max_len > 0);
        ssl_encrypt(transport, app_data, app_outp - app_data);
      }
    }

    if (ssl->network_out_pending == 0) {
      if (ssl->state == SHUTTING_DOWN) {
        if (!ssl->queued_shutdown) {
          start_ssl_shutdown(transport);
          work_pending = true;
        } else {
          ssl->state = SSL_CLOSED;
        }
      }
      else if (ssl->state == NEGOTIATING && ssl->app_input_closed) {
        ssl->app_output_closed = PN_EOS;
        ssl->state = SSL_CLOSED;
      }
    }
  } while (work_pending);

  if (written == 0 && ssl->state == SSL_CLOSED) {
    written = ssl->app_output_closed ? ssl->app_output_closed : PN_EOS;
    if (transport->io_layers[layer]==&ssl_input_closed_layer) {
      transport->io_layers[layer] = &ssl_closed_layer;
    } else {
      transport->io_layers[layer] = &ssl_output_closed_layer;
    }
  }
  ssl_log(transport, "process_output_ssl() returning %d\n", (int) written);
  return written;
}


static ssize_t process_input_done(pn_transport_t *transport, unsigned int layer, const char *input_data, size_t len)
{
  return PN_EOS;
}

static ssize_t process_output_done(pn_transport_t *transport, unsigned int layer, char *input_data, size_t len)
{
  return PN_EOS;
}

// return # output bytes sitting in this layer
static size_t buffered_output(pn_transport_t *transport)
{
  size_t count = 0;
  pni_ssl_t *ssl = transport->ssl;
  if (ssl) {
    count += ssl->network_out_pending;
    if (count == 0 && ssl->state == SHUTTING_DOWN && ssl->queued_shutdown)
      count++;
  }
  return count;
}

static HCERTSTORE open_cert_db(const char *store_name, const char *passwd, int *error) {
  *error = 0;
  DWORD sys_store_type = 0;
  HCERTSTORE cert_store = 0;

  if (store_name) {
    if (strncmp(store_name, "ss:", 3) == 0) {
      store_name += 3;
      sys_store_type = CERT_SYSTEM_STORE_CURRENT_USER;
    }
    else if (strncmp(store_name, "lmss:", 5) == 0) {
      store_name += 5;
      sys_store_type = CERT_SYSTEM_STORE_LOCAL_MACHINE;
    }
  }

  if (sys_store_type) {
    // Opening a system store, names are not case sensitive.
    // Map confusing GUI name to actual registry store name.
    if (!pn_strcasecmp(store_name, "personal")) store_name= "my";
    cert_store = CertOpenStore(CERT_STORE_PROV_SYSTEM_A, 0, NULL,
                               CERT_STORE_OPEN_EXISTING_FLAG | CERT_STORE_READONLY_FLAG |
                               sys_store_type, store_name);
    if (!cert_store) {
      ssl_log_error_status(GetLastError(), "Failed to open system certificate store %s", store_name);
      *error = -3;
      return NULL;
    }
  } else {
    // PKCS#12 file
    HANDLE cert_file = CreateFile(store_name, GENERIC_READ, 0, NULL, OPEN_EXISTING,
                                  FILE_ATTRIBUTE_NORMAL, NULL);
    if (INVALID_HANDLE_VALUE == cert_file) {
      HRESULT status = GetLastError();
      ssl_log_error_status(status, "Failed to open the file holding the private key: %s", store_name);
      *error = -4;
      return NULL;
    }
    DWORD nread = 0L;
    const DWORD file_size = GetFileSize(cert_file, NULL);
    char *buf = NULL;
    if (INVALID_FILE_SIZE != file_size)
      buf = (char *) malloc(file_size);
    if (!buf || !ReadFile(cert_file, buf, file_size, &nread, NULL)
        || file_size != nread) {
      HRESULT status = GetLastError();
      CloseHandle(cert_file);
      free(buf);
      ssl_log_error_status(status, "Reading the private key from file failed %s", store_name);
      *error = -5;
      return NULL;
    }
    CloseHandle(cert_file);

    CRYPT_DATA_BLOB blob;
    blob.cbData = nread;
    blob.pbData = (BYTE *) buf;

    wchar_t *pwUCS2 = NULL;
    int pwlen = 0;
    if (passwd) {
      // convert passwd to null terminated wchar_t (Windows UCS2)
      pwlen = strlen(passwd);
      pwUCS2 = (wchar_t *) calloc(pwlen + 1, sizeof(wchar_t));
      int nwc = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, passwd, pwlen, &pwUCS2[0], pwlen);
      if (!nwc) {
        ssl_log_error_status(GetLastError(), "Error converting password from UTF8");
        free(buf);
        free(pwUCS2);
        *error = -6;
        return NULL;
      }
    }

    cert_store = PFXImportCertStore(&blob, pwUCS2, 0);
    if (pwUCS2) {
      SecureZeroMemory(pwUCS2, pwlen * sizeof(wchar_t));
      free(pwUCS2);
    }
    if (cert_store == NULL) {
      ssl_log_error_status(GetLastError(), "Failed to import the file based certificate store");
      free(buf);
      *error = -7;
      return NULL;
    }

    free(buf);
  }

  return cert_store;
}

static bool store_contains(HCERTSTORE store, PCCERT_CONTEXT cert)
{
  DWORD find_type = CERT_FIND_EXISTING; // Require exact match
  PCCERT_CONTEXT tcert = CertFindCertificateInStore(store, X509_ASN_ENCODING | PKCS_7_ASN_ENCODING,
                                                    0, find_type, cert, 0);
  if (tcert) {
    CertFreeCertificateContext(tcert);
    return true;
  }
  return false;
}

/* Match the DNS name pattern from the peer certificate against our configured peer
   hostname */
static bool match_dns_pattern(const char *hostname, const char *pattern, int plen)
{
  int slen = (int) strlen(hostname);
  if (memchr( pattern, '*', plen ) == NULL)
    return (plen == slen &&
            pn_strncasecmp( pattern, hostname, plen ) == 0);

  /* dns wildcarded pattern - RFC2818 */
  char plabel[64];   /* max label length < 63 - RFC1034 */
  char slabel[64];

  while (plen > 0 && slen > 0) {
    const char *cptr;
    int len;

    cptr = (const char *) memchr( pattern, '.', plen );
    len = (cptr) ? cptr - pattern : plen;
    if (len > (int) sizeof(plabel) - 1) return false;
    memcpy( plabel, pattern, len );
    plabel[len] = 0;
    if (cptr) ++len;    // skip matching '.'
    pattern += len;
    plen -= len;

    cptr = (const char *) memchr( hostname, '.', slen );
    len = (cptr) ? cptr - hostname : slen;
    if (len > (int) sizeof(slabel) - 1) return false;
    memcpy( slabel, hostname, len );
    slabel[len] = 0;
    if (cptr) ++len;    // skip matching '.'
    hostname += len;
    slen -= len;

    char *star = strchr( plabel, '*' );
    if (!star) {
      if (pn_strcasecmp( plabel, slabel )) return false;
    } else {
      *star = '\0';
      char *prefix = plabel;
      int prefix_len = strlen(prefix);
      char *suffix = star + 1;
      int suffix_len = strlen(suffix);
      if (prefix_len && pn_strncasecmp( prefix, slabel, prefix_len )) return false;
      if (suffix_len && pn_strncasecmp( suffix,
                                     slabel + (strlen(slabel) - suffix_len),
                                     suffix_len )) return false;
    }
  }

  return plen == slen;
}

// Caller must free the returned buffer
static char* wide_to_utf8(LPWSTR wstring)
{
  int len = WideCharToMultiByte(CP_UTF8, 0, wstring, -1, 0, 0, 0, 0);
  if (!len) {
    ssl_log_error_status(GetLastError(), "converting UCS2 to UTF8");
    return NULL;
  }
  char *p = (char *) malloc(len);
  if (!p) return NULL;
  if (WideCharToMultiByte(CP_UTF8, 0, wstring, -1, p, len, 0, 0))
    return p;
  ssl_log_error_status(GetLastError(), "converting UCS2 to UTF8");
  free (p);
  return NULL;
}

static bool server_name_matches(const char *server_name, CERT_EXTENSION *alt_name_ext, PCCERT_CONTEXT cert)
{
  // As for openssl.c: alt names first, then CN
  bool matched = false;

  if (alt_name_ext) {
    CERT_ALT_NAME_INFO* alt_name_info = NULL;
    DWORD size = 0;
    if(!CryptDecodeObjectEx(X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, szOID_SUBJECT_ALT_NAME2,
                            alt_name_ext->Value.pbData, alt_name_ext->Value.cbData,
                            CRYPT_DECODE_ALLOC_FLAG | CRYPT_DECODE_NOCOPY_FLAG,
                            0, &alt_name_info, &size)) {
      ssl_log_error_status(GetLastError(), "Alternative name match internal error");
      return false;
    }

    int name_ct = alt_name_info->cAltEntry;
    for (int i = 0; !matched && i < name_ct; ++i) {
      if (alt_name_info->rgAltEntry[i].dwAltNameChoice == CERT_ALT_NAME_DNS_NAME) {
        char *alt_name = wide_to_utf8(alt_name_info->rgAltEntry[i].pwszDNSName);
        if (alt_name) {
          matched = match_dns_pattern(server_name, (const char *) alt_name, strlen(alt_name));
          free(alt_name);
        }
      }
    }
    LocalFree(&alt_name_info);
  }

  if (!matched) {
    PCERT_INFO info = cert->pCertInfo;
    DWORD len = CertGetNameString(cert, CERT_NAME_ATTR_TYPE, 0, szOID_COMMON_NAME, 0, 0);
    char *name = (char *) malloc(len);
    if (name) {
      int count = CertGetNameString(cert, CERT_NAME_ATTR_TYPE, 0, szOID_COMMON_NAME, name, len);
      if (count)
        matched = match_dns_pattern(server_name, (const char *) name, strlen(name));
      free(name);
    }
  }
  return matched;
}

const char* pn_ssl_get_remote_subject_subfield(pn_ssl_t *ssl0, pn_ssl_cert_subject_subfield field)
{
    return NULL;
}

int pn_ssl_get_cert_fingerprint(pn_ssl_t *ssl0,
                                          char *fingerprint,
                                          size_t fingerprint_length,
                                          pn_ssl_hash_alg hash_alg)
{
    *fingerprint = '\0';
    return -1;
}

static HRESULT verify_peer(pni_ssl_t *ssl, HCERTSTORE root_store, const char *server_name, bool tracing)
{
  // Free/release the following before return:
  PCCERT_CONTEXT peer_cc = 0;
  PCCERT_CONTEXT trust_anchor = 0;
  PCCERT_CHAIN_CONTEXT chain_context = 0;
  wchar_t *nameUCS2 = 0;

  if (server_name && strlen(server_name) > 255) {
    ssl_log_error("invalid server name: %s\n", server_name);
    return WSAENAMETOOLONG;
  }

  // Get peer's certificate.
  SECURITY_STATUS status;
  status = QueryContextAttributes(&ssl->ctxt_handle, SECPKG_ATTR_REMOTE_CERT_CONTEXT, &peer_cc);
  if (status != SEC_E_OK) {
    ssl_log_error_status(status, "can't obtain remote peer certificate information");
    return status;
  }

  // Build the peer's certificate chain.  Multiple chains may be built but we
  // care about rgpChain[0], which is the best.  Custom root stores are not
  // allowed until W8/server 2012: see CERT_CHAIN_ENGINE_CONFIG.  For now, we
  // manually override to taste.

  // Chain verification functions give false reports for CRL if the trust anchor
  // is not in the official root store.  We ignore CRL completely if it doesn't
  // apply to any untrusted certs in the chain, and defer to SChannel's veto
  // otherwise.  To rely on CRL, the CA must be in both the official system
  // trusted root store and the Proton cred->trust_store.  To defeat CRL, the
  // most distal cert with CRL must be placed in the Proton cred->trust_store.
  // Similarly, certificate usage checking is overly strict at times.

  CERT_CHAIN_PARA desc;
  memset(&desc, 0, sizeof(desc));
  desc.cbSize = sizeof(desc);

  LPSTR usages[] = { szOID_PKIX_KP_SERVER_AUTH };
  DWORD n_usages = sizeof(usages) / sizeof(LPSTR);
  desc.RequestedUsage.dwType = USAGE_MATCH_TYPE_OR;
  desc.RequestedUsage.Usage.cUsageIdentifier = n_usages;
  desc.RequestedUsage.Usage.rgpszUsageIdentifier = usages;

  if(!CertGetCertificateChain(0, peer_cc, 0, peer_cc->hCertStore, &desc,
                             CERT_CHAIN_REVOCATION_CHECK_CHAIN_EXCLUDE_ROOT |
                             CERT_CHAIN_CACHE_END_CERT,
                             0, &chain_context)){
    HRESULT st = GetLastError();
    ssl_log_error_status(st, "Basic certificate chain check failed");
    CertFreeCertificateContext(peer_cc);
    return st;
  }
  if (chain_context->cChain < 1 || chain_context->rgpChain[0]->cElement < 1) {
    ssl_log_error("empty chain with status %x %x\n", chain_context->TrustStatus.dwErrorStatus,
                 chain_context->TrustStatus.dwInfoStatus);
    return SEC_E_CERT_UNKNOWN;
  }

  int chain_len = chain_context->rgpChain[0]->cElement;
  PCCERT_CONTEXT leaf_cert = chain_context->rgpChain[0]->rgpElement[0]->pCertContext;
  PCCERT_CONTEXT trunk_cert = chain_context->rgpChain[0]->rgpElement[chain_len - 1]->pCertContext;
  if (tracing)
    // See doc for CERT_CHAIN_POLICY_STATUS for bit field error and info status values
    ssl_log_error("status for complete chain: error bits %x info bits %x\n",
                  chain_context->TrustStatus.dwErrorStatus, chain_context->TrustStatus.dwInfoStatus);

  // Supplement with checks against Proton's trusted_ca_db, custom revocation and usage.
  HRESULT error = 0;
  do {
    // Do not return from this do loop.  Set error = SEC_E_XXX and break.
    bool revocable = false;  // unless we see any untrusted certs that could be
    for (int i = 0; i < chain_len; i++) {
      CERT_CHAIN_ELEMENT *ce = chain_context->rgpChain[0]->rgpElement[i];
      PCCERT_CONTEXT cc = ce->pCertContext;
      if (cc->pCertInfo->dwVersion != CERT_V3) {
        if (tracing)
          ssl_log_error("certificate chain element %d is not version 3\n", i);
        error = SEC_E_CERT_WRONG_USAGE; // A fossil
        break;
      }

      if (!trust_anchor && store_contains(root_store, cc))
        trust_anchor = CertDuplicateCertificateContext(cc);

      int n_ext = cc->pCertInfo->cExtension;
      for (int ii = 0; ii < n_ext && !revocable && !trust_anchor; ii++) {
        CERT_EXTENSION *p = &cc->pCertInfo->rgExtension[ii];
        // rfc 5280 extensions for revocation
        if (!strcmp(p->pszObjId, szOID_AUTHORITY_INFO_ACCESS) ||
            !strcmp(p->pszObjId, szOID_CRL_DIST_POINTS) ||
            !strcmp(p->pszObjId, szOID_FRESHEST_CRL)) {
          revocable = true;
        }
      }

      if (tracing) {
        char name[512];
        const char *is_anchor = (cc == trust_anchor) ? " trust anchor" : "";
        if (!CertNameToStr(cc->dwCertEncodingType, &cc->pCertInfo->Subject,
                           CERT_X500_NAME_STR | CERT_NAME_STR_NO_PLUS_FLAG, name, sizeof(name)))
          strcpy(name, "[too long]");
        ssl_log_error("element %d (name: %s)%s error bits %x info bits %x\n", i, name, is_anchor,
                      ce->TrustStatus.dwErrorStatus, ce->TrustStatus.dwInfoStatus);
      }
    }
    if (error)
      break;

    if (!trust_anchor) {
      // We don't trust any of the certs in the chain, see if the last cert
      // is issued by a Proton trusted CA.
      DWORD flags = CERT_STORE_NO_ISSUER_FLAG || CERT_STORE_SIGNATURE_FLAG ||
        CERT_STORE_TIME_VALIDITY_FLAG;
      trust_anchor = CertGetIssuerCertificateFromStore(root_store, trunk_cert, 0, &flags);
      if (trust_anchor) {
        if (tracing) {
          if (flags & CERT_STORE_SIGNATURE_FLAG)
            ssl_log_error("root certificate signature failure\n");
          if (flags & CERT_STORE_TIME_VALIDITY_FLAG)
            ssl_log_error("root certificate time validity failure\n");
        }
        if (flags) {
          CertFreeCertificateContext(trust_anchor);
          trust_anchor = 0;
        }
      }
    }
    if (!trust_anchor) {
      error = SEC_E_UNTRUSTED_ROOT;
      break;
    }

    bool strict_usage = false;
    CERT_EXTENSION *leaf_alt_names = 0;
    if (leaf_cert != trust_anchor) {
      int n_ext = leaf_cert->pCertInfo->cExtension;
      for (int ii = 0; ii < n_ext; ii++) {
        CERT_EXTENSION *p = &leaf_cert->pCertInfo->rgExtension[ii];
        if (!strcmp(p->pszObjId, szOID_ENHANCED_KEY_USAGE))
          strict_usage = true;
        if (!strcmp(p->pszObjId, szOID_SUBJECT_ALT_NAME2))
          if (p->Value.pbData)
            leaf_alt_names = p;
      }
    }

    if (server_name) {
      int len = strlen(server_name);
      nameUCS2 = (wchar_t *) calloc(len + 1, sizeof(wchar_t));
      int nwc = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, server_name, len, &nameUCS2[0], len);
      if (!nwc) {
        error = GetLastError();
        ssl_log_error_status(error, "Error converting server name from UTF8");
        break;
      }
    }

    // SSL-specific parameters (ExtraPolicy below)
    SSL_EXTRA_CERT_CHAIN_POLICY_PARA ssl_desc;
    memset(&ssl_desc, 0, sizeof(ssl_desc));
    ssl_desc.cbSize = sizeof(ssl_desc);
    ssl_desc.pwszServerName = nameUCS2;
    ssl_desc.dwAuthType = nameUCS2 ? AUTHTYPE_SERVER : AUTHTYPE_CLIENT;
    ssl_desc.fdwChecks = SECURITY_FLAG_IGNORE_UNKNOWN_CA;
    if (server_name)
      ssl_desc.fdwChecks |= SECURITY_FLAG_IGNORE_CERT_CN_INVALID;
    if (!revocable)
      ssl_desc.fdwChecks |= SECURITY_FLAG_IGNORE_REVOCATION;
    if (!strict_usage)
      ssl_desc.fdwChecks |= SECURITY_FLAG_IGNORE_WRONG_USAGE;

    // General certificate chain parameters
    CERT_CHAIN_POLICY_PARA chain_desc;
    memset(&chain_desc, 0, sizeof(chain_desc));
    chain_desc.cbSize = sizeof(chain_desc);
    chain_desc.dwFlags = CERT_CHAIN_POLICY_ALLOW_UNKNOWN_CA_FLAG;
    if (!revocable)
      chain_desc.dwFlags |= CERT_CHAIN_POLICY_IGNORE_ALL_REV_UNKNOWN_FLAGS;
    if (!strict_usage)
      chain_desc.dwFlags |= CERT_CHAIN_POLICY_IGNORE_WRONG_USAGE_FLAG;
    chain_desc.pvExtraPolicyPara = &ssl_desc;

    CERT_CHAIN_POLICY_STATUS chain_status;
    memset(&chain_status, 0, sizeof(chain_status));
    chain_status.cbSize = sizeof(chain_status);

    if (!CertVerifyCertificateChainPolicy(CERT_CHAIN_POLICY_SSL, chain_context,
                                          &chain_desc, &chain_status)) {
      error = GetLastError();
      // Failure to complete the check, does not (in)validate the cert.
      ssl_log_error_status(error, "Supplemental certificate chain check failed");
      break;
    }

    if (chain_status.dwError) {
      error = chain_status.dwError;
      if (tracing) {
        ssl_log_error_status(chain_status.dwError, "Certificate chain verification error");
        if (chain_status.lChainIndex == 0 && chain_status.lElementIndex != -1) {
          int idx = chain_status.lElementIndex;
          CERT_CHAIN_ELEMENT *ce = chain_context->rgpChain[0]->rgpElement[idx];
          ssl_log_error("  chain failure at %d error/info: %x %x\n", idx,
                        ce->TrustStatus.dwErrorStatus, ce->TrustStatus.dwInfoStatus);
        }
      }
      break;
    }

    if (server_name && ssl->verify_mode == PN_SSL_VERIFY_PEER_NAME &&
        !server_name_matches(server_name, leaf_alt_names, leaf_cert)) {
      error = SEC_E_WRONG_PRINCIPAL;
      break;
    }
    else if (ssl->verify_mode == PN_SSL_VERIFY_PEER_NAME && !server_name) {
      ssl_log_error("Error: configuration error: PN_SSL_VERIFY_PEER_NAME configured, but no peer hostname set!");
      error = SEC_E_WRONG_PRINCIPAL;
      break;
    }
  } while (0);

  if (tracing && !error)
    ssl_log_error("peer certificate authenticated\n");

  // Lots to clean up.
  if (peer_cc)
    CertFreeCertificateContext(peer_cc);
  if (trust_anchor)
    CertFreeCertificateContext(trust_anchor);
  if (chain_context)
    CertFreeCertificateChain(chain_context);
  free(nameUCS2);
  return error;
}
