blob: 3056890c3f82944197730312e70695ec757b1e3b [file] [log] [blame]
/*
*
* 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 "core/autodetect.h"
#include "core/engine-internal.h"
#include "core/logger_private.h"
#include "core/util.h"
#include "platform/platform.h"
#include <proton/ssl.h>
#include <proton/engine.h>
#include <assert.h>
#include <stdio.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
extern "C" {
/** @file
* SSL/TLS support API.
*
* This file contains an SChannel-based implemention of the SSL/TLS API for Windows platforms.
*/
static void ssl_log(pn_transport_t *transport, pn_log_level_t sev, const char *fmt, ...);
static void ssl_log_error(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", 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", 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, 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;
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 {
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_mode_t mode;
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_vlog(pn_transport_t *transport, pn_log_level_t sev, const char *fmt, va_list ap)
{
pn_logger_t *logger = transport ? &transport->logger : pn_default_logger();
if (PN_SHOULD_LOG(logger, PN_SUBSYSTEM_SSL, sev)) {
pni_logger_vlogf(logger, PN_SUBSYSTEM_SSL, sev, fmt, ap);
}
}
static void ssl_log(pn_transport_t *transport, pn_log_level_t sev, const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
ssl_vlog(transport, sev, fmt, ap);
va_end(ap);
}
// @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);
ssl_vlog(NULL, PN_LEVEL_ERROR, fmt, ap);
va_end(ap);
}
static void ssl_log_error_status(HRESULT status, const char *fmt, ...)
{
char buf[512];
va_list ap;
if (fmt) {
va_start(ap, fmt);
ssl_vlog(NULL, PN_LEVEL_ERROR, 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", buf);
else
ssl_log_error("Internal Windows error: %x for %x", GetLastError(), status);
}
static void ssl_log_clear_data(pn_transport_t *transport, const char *data, size_t len)
{
PN_LOG_DATA(&transport->logger, PN_SUBSYSTEM_SSL, PN_LEVEL_RAW, "decrypted data", data, len );
}
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 )
{
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", mode);
return NULL;
}
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;
domain->cred = win_credential(mode);
}
if (pn_ssl_domain_set_trusted_ca_db(domain, "ss:root") != 0) {
pn_ssl_domain_free(domain);
return NULL;
}
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;
csguard g2(&domain->cred->cslock);
if (domain->cred->trust_store) {
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;
}
domain->cred->trust_store = store;
domain->cred->trust_store_name = pn_strdup(certificate_db);
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);
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, 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.");
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
};
static pn_ssl_domain_t *default_client_domain = 0;
static pn_ssl_domain_t *default_server_domain = 0;
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) return -1;
if (ssl->state != CREATED) return -1;
if (!domain) {
if (transport->server) {
if (!default_server_domain) {
default_server_domain = pn_ssl_domain(PN_SSL_MODE_SERVER);
}
domain = default_server_domain;
}
else {
if (!default_client_domain) {
default_client_domain = pn_ssl_domain(PN_SSL_MODE_CLIENT);
pn_ssl_domain_set_peer_authentication(default_client_domain, PN_SSL_VERIFY_PEER_NAME, NULL);
}
domain = default_client_domain;
}
}
csguard g(&domain->cslock);
csguard g2(&domain->cred->cslock);
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->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;
ssl->mode = domain->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.");
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?
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))
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))
snprintf(buffer, size, "%s", "TLSv1.1");
else if ((info.dwProtocol & 0xC00))
snprintf(buffer, size, "%s", "TLSv1.2");
else {
ssl_log_error("unexpected protocol %x", 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, PN_LEVEL_DEBUG, "SSL socket freed." );
// clean up Windows per TLS session data before releasing the domain count
csguard g(&ssl->cred->cslock);
if (SecIsValidHandle(&ssl->ctxt_handle))
DeleteSecurityContext(&ssl->ctxt_handle);
if (ssl->cred) {
if (ssl->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)) {
g.release();
win_credential_delete(ssl->cred);
}
}
g.release();
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, PN_LEVEL_TRACE, "ssl_encrypt %d network bytes", 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");
// 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, PN_LEVEL_TRACE, "Sending client hello %d bytes", 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, PN_LEVEL_TRACE, "client handshake: incomplete record");
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, PN_LEVEL_DEBUG, "client handshake token %d bytes", 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, PN_LEVEL_DEBUG, "client shutdown token %d bytes", 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_SHOULD_LOG(&transport->logger, PN_SUBSYSTEM_SSL, PN_LEVEL_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", ssl->peer_hostname);
else
ssl_log_error_status(ec, "certificate verification failed");
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", (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, PN_LEVEL_DEBUG, "client handshake successful %d max record size", max);
break;
case SEC_I_CONTEXT_EXPIRED:
// ended before we got going
default:
ssl_log(transport, PN_LEVEL_ERROR, "client handshake failed %d", (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, PN_LEVEL_DEBUG, "server handshake: incomplete record");
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, PN_LEVEL_DEBUG, "server handshake: incomplete record");
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, PN_LEVEL_DEBUG, "server shutdown token %d bytes", 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_SHOULD_LOG(&transport->logger, PN_SUBSYSTEM_SSL, PN_LEVEL_TRACE);
HRESULT ec = verify_peer(ssl, ssl->cred->trust_store, NULL, tracing);
if (ec) {
ssl_log_error_status(ec, "certificate verification failed");
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", (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, PN_LEVEL_DEBUG, "server handshake successful %d max record size", max);
break;
case SEC_E_ALGORITHM_MISMATCH:
ssl_log(transport, PN_LEVEL_WARNING, "server handshake failed: no common algorithm");
ssl_failed(transport, "server handshake failed: no common algorithm");
break;
case SEC_I_CONTEXT_EXPIRED:
// ended before we got going
default:
ssl_log(transport, PN_LEVEL_ERROR, "server handshake failed %d", (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, PN_LEVEL_DEBUG, "server handshake token %d bytes", 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->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, PN_LEVEL_ERROR, "Application expecting %d bytes (> negotiated maximum frame)", 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, PN_LEVEL_ERROR, "TLS memory allocation failed for %d bytes", 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, PN_LEVEL_INFO, "Shutting down SSL connection...");
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, PN_LEVEL_TRACE, "process_input_ssl( data size=%d )",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, PN_LEVEL_TRACE, "Next decryption, %d left over", 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, PN_LEVEL_TRACE, "Application consumed %d bytes from peer", (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, PN_LEVEL_WARNING, "Application layer closed its input, error=%d (discarding %d bytes)",
(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, PN_LEVEL_WARNING, "Input closed discard %d bytes",
(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, PN_LEVEL_TRACE, "process_input_ssl() returning %d, forwarded %d", (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, PN_LEVEL_TRACE, "process_output_ssl( max_len=%d )",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, PN_LEVEL_TRACE, "Gathered %d bytes from app to send to peer", app_bytes );
} else {
if (app_bytes < 0) {
ssl_log(transport, PN_LEVEL_WARNING, "Application layer closed its output, error=%d (%d bytes pending send)",
(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, PN_LEVEL_WARNING, "Application layer blocked on input, closing");
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, PN_LEVEL_TRACE, "process_output_ssl() returning %d", (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", 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", 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",
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", 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", 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");
if (flags & CERT_STORE_TIME_VALIDITY_FLAG)
ssl_log_error("root certificate time validity failure");
}
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", 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");
// 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;
}
}