http://git-wip-us.apache.org/repos/asf/qpid-proton/blob/37136940/c/src/ssl/schannel.c ---------------------------------------------------------------------- diff --git a/c/src/ssl/schannel.c b/c/src/ssl/schannel.c new file mode 100644 index 0000000..b541c96 --- /dev/null +++ b/c/src/ssl/schannel.c @@ -0,0 +1,2326 @@ +/* + * + * 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; +}
http://git-wip-us.apache.org/repos/asf/qpid-proton/blob/37136940/c/src/ssl/ssl-internal.h ---------------------------------------------------------------------- diff --git a/c/src/ssl/ssl-internal.h b/c/src/ssl/ssl-internal.h new file mode 100644 index 0000000..d3205ea --- /dev/null +++ b/c/src/ssl/ssl-internal.h @@ -0,0 +1,36 @@ +#ifndef PROTON_SSL_INTERNAL_H +#define PROTON_SSL_INTERNAL_H 1 +/* + * + * 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. + * + */ + +#include "proton/ssl.h" + +/** @file + * Internal API for SSL/TLS support in the Driver Layer. + * + * Generic API to abstract the implementation of SSL/TLS from the Driver codebase. + * + */ + +// release the SSL context +void pn_ssl_free(pn_transport_t *transport); + +#endif /* ssl-internal.h */ http://git-wip-us.apache.org/repos/asf/qpid-proton/blob/37136940/c/src/ssl/ssl_stub.c ---------------------------------------------------------------------- diff --git a/c/src/ssl/ssl_stub.c b/c/src/ssl/ssl_stub.c new file mode 100644 index 0000000..0a6a13f --- /dev/null +++ b/c/src/ssl/ssl_stub.c @@ -0,0 +1,172 @@ +/* + * + * 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. + * + */ + +#include <proton/ssl.h> +#include <proton/error.h> +#include <proton/transport.h> +#include "core/engine-internal.h" + + +/** @file + * SSL/TLS support API. + * + * This file contains stub implementations of the SSL/TLS API. This implementation is + * used if there is no SSL/TLS support in the system's environment. + */ + +bool pn_ssl_present(void) +{ + return false; +} + +pn_ssl_t *pn_ssl(pn_transport_t *transport) +{ + return NULL; +} + +int pn_ssl_init(pn_ssl_t *ssl, pn_ssl_domain_t *domain, + const char *session_id) +{ + return -1; +} + +void pn_ssl_free( pn_ssl_t *ssl) +{ +} + +void pn_ssl_trace(pn_ssl_t *ssl, pn_trace_t trace) +{ +} + +ssize_t pn_ssl_input(pn_transport_t *transport, unsigned int layer, const char *bytes, size_t available) +{ + return PN_EOS; +} + +ssize_t pn_ssl_output(pn_transport_t *transport, unsigned int layer, char *buffer, size_t max_size) +{ + return PN_EOS; +} + +const pn_io_layer_t ssl_layer = { + pn_ssl_input, + pn_ssl_output, + NULL, + NULL +}; + +bool pn_ssl_get_cipher_name(pn_ssl_t *ssl, char *buffer, size_t size) +{ + *buffer = '\0'; + return false; +} + +bool pn_ssl_get_protocol_name(pn_ssl_t *ssl, char *buffer, size_t size) +{ + *buffer = '\0'; + return false; +} + +pn_ssl_domain_t *pn_ssl_domain( pn_ssl_mode_t mode) +{ + return NULL; +} + +void pn_ssl_domain_free( pn_ssl_domain_t *d ) +{ +} + +int pn_ssl_domain_set_credentials( pn_ssl_domain_t *domain, + const char *certificate_file, + const char *private_key_file, + const char *password) +{ + return -1; +} + +int pn_ssl_domain_set_trusted_ca_db(pn_ssl_domain_t *domain, + const char *certificate_db) +{ + return -1; +} + +int pn_ssl_domain_set_peer_authentication(pn_ssl_domain_t *domain, + const pn_ssl_verify_mode_t mode, + const char *trusted_CAs) +{ + return -1; +} + +int pn_ssl_domain_allow_unsecured_client(pn_ssl_domain_t *domain) +{ + return -1; +} + +int pn_ssl_domain_set_ciphers(pn_ssl_domain_t *domain, const char *ciphers) +{ + return -1; +} + +int pn_ssl_domain_set_protocols(pn_ssl_domain_t* domain, const char* protocols) +{ + return -1; +} + +bool pn_ssl_allow_unsecured(pn_ssl_t *ssl) +{ + return true; +} + +pn_ssl_resume_status_t pn_ssl_resume_status( pn_ssl_t *s ) +{ + return PN_SSL_RESUME_UNKNOWN; +} + +int pn_ssl_set_peer_hostname( pn_ssl_t *ssl, const char *hostname) +{ + return -1; +} + +int pn_ssl_get_peer_hostname( pn_ssl_t *ssl, char *hostname, size_t *bufsize ) +{ + return -1; +} + +const char* pn_ssl_get_remote_subject(pn_ssl_t *ssl) +{ + return NULL; +} + +int pn_ssl_get_ssf(pn_ssl_t *ssl) +{ + return 0; +} + +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; +} + +const char* pn_ssl_get_remote_subject_subfield(pn_ssl_t *ssl0, pn_ssl_cert_subject_subfield field) +{ + return NULL; +} --------------------------------------------------------------------- To unsubscribe, e-mail: commits-unsubscr...@qpid.apache.org For additional commands, e-mail: commits-h...@qpid.apache.org