Repository: qpid-proton Updated Branches: refs/heads/master 6302a89bc -> 8f4e31f6c
PROTON-697: SChannel functionality: Proton-specific trust stores, client certificates Project: http://git-wip-us.apache.org/repos/asf/qpid-proton/repo Commit: http://git-wip-us.apache.org/repos/asf/qpid-proton/commit/8f4e31f6 Tree: http://git-wip-us.apache.org/repos/asf/qpid-proton/tree/8f4e31f6 Diff: http://git-wip-us.apache.org/repos/asf/qpid-proton/diff/8f4e31f6 Branch: refs/heads/master Commit: 8f4e31f6c8766d7c7361d39c185ef97e593ef0b4 Parents: 6302a89 Author: Clifford Jansen <[email protected]> Authored: Wed Dec 17 10:51:34 2014 -0800 Committer: Clifford Jansen <[email protected]> Committed: Wed Dec 17 10:51:34 2014 -0800 ---------------------------------------------------------------------- proton-c/src/windows/schannel.c | 713 ++++++++++++++++++++++++++++------- 1 file changed, 580 insertions(+), 133 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/qpid-proton/blob/8f4e31f6/proton-c/src/windows/schannel.c ---------------------------------------------------------------------- diff --git a/proton-c/src/windows/schannel.c b/proton-c/src/windows/schannel.c index 88b4a07..da457c4 100644 --- a/proton-c/src/windows/schannel.c +++ b/proton-c/src/windows/schannel.c @@ -45,6 +45,7 @@ #define SECURITY_WIN32 #include <security.h> #include <Schnlsp.h> +#include <WinInet.h> #undef SECURITY_WIN32 @@ -57,6 +58,7 @@ 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); /* * win_credential_t: SChannel context that must accompany TLS connections. @@ -73,6 +75,9 @@ struct win_credential_t { 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 @@ -84,6 +89,9 @@ static void win_credential_initialize(void *object) win_credential_t *c = (win_credential_t *) object; 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) @@ -93,6 +101,11 @@ static void win_credential_finalize(void *object) 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); + free(c->trust_store_name); } static win_credential_t *win_credential(pn_ssl_mode_t m) @@ -109,86 +122,10 @@ static int win_credential_load_cert(win_credential_t *cred, const char *store_na if (!store_name) return -2; - char *buf = NULL; - 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 (pni_eq_nocase(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); - return -3; - } - } 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); - return -4; - } - DWORD nread = 0L; - const DWORD file_size = GetFileSize(cert_file, 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); - return -5; - } - 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); - return -6; - } - } - - 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); - return -7; - } - } + 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; @@ -230,7 +167,6 @@ static int win_credential_load_cert(win_credential_t *cred, const char *store_na ssl_log_error("Could not find certificate %s in store %s\n", cert_name, store_name); cred->cert_context = found_ctx; - free(buf); free(fn); CertCloseStore(cert_store, 0); return found_ctx ? 0 : -8; @@ -254,15 +190,20 @@ static CredHandle win_credential_cred_handle(win_credential_t *cred, pn_ssl_veri memset(&descriptor, 0, sizeof(descriptor)); descriptor.dwVersion = SCHANNEL_CRED_VERSION; - descriptor.dwFlags = SCH_CRED_NO_DEFAULT_CREDS; - if (cred->mode == PN_SSL_MODE_CLIENT && verify_mode == PN_SSL_ANONYMOUS_PEER) - descriptor.dwFlags |= SCH_CRED_MANUAL_CRED_VALIDATION; + 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); @@ -338,6 +279,7 @@ struct pni_ssl_t { CredHandle cred_handle; CtxtHandle ctxt_handle; SecPkgContext_StreamSizes sc_sizes; + pn_ssl_verify_mode_t verify_mode; win_credential_t *cred; }; @@ -371,7 +313,7 @@ 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, ...) @@ -410,7 +352,7 @@ static void ssl_log_error_status(HRESULT status, const char *fmt, ...) 0, status, 0, buf, sizeof(buf), 0)) ssl_log_error(" : %s\n", buf); else - fprintf(stderr, "pn internal Windows error: %lu\n", GetLastError()); + fprintf(stderr, "pn internal Windows error: %x for %x\n", GetLastError(), status); fflush(stderr); } @@ -448,15 +390,6 @@ static int ssl_failed(pn_transport_t *transport, const char *reason) return PN_EOS; } -/* 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 ) -{ - return false; // TODO -} - - static pn_ssl_session_t *ssn_cache_find( pn_ssl_domain_t *domain, const char *id ) { // TODO: @@ -532,25 +465,25 @@ int pn_ssl_domain_set_credentials( pn_ssl_domain_t *domain, int pn_ssl_domain_set_trusted_ca_db(pn_ssl_domain_t *domain, const char *certificate_db) { - if (!domain) return -1; + if (!domain || !certificate_db) return -1; - if (certificate_db && !pni_eq_nocase(certificate_db, "ss:root") && - !pni_eq_nocase(certificate_db, "lmss:root")) - // TODO: handle more than just the main system trust store - return -1; - if (!certificate_db && !domain->has_ca_db) - return 0; // No change - if (certificate_db && domain->has_ca_db) - return 0; // NO change + int ec = 0; + HCERTSTORE store = open_cert_db(certificate_db, NULL, &ec); + if (!store) + return ec; - win_credential_t *new_cred = win_credential(domain->mode); - if (!new_cred) - return -1; - new_cred->cert_context = CertDuplicateCertificateContext(domain->cred->cert_context); - pn_decref(domain->cred); - domain->cred = new_cred; + if (domain->has_ca_db) { + win_credential_t *new_cred = win_credential(domain->mode); + if (!new_cred) + return -1; + new_cred->cert_context = CertDuplicateCertificateContext(domain->cred->cert_context); + pn_decref(domain->cred); + domain->cred = new_cred; + } - domain->has_ca_db = (certificate_db != NULL); + domain->cred->trust_store = store; + domain->cred->trust_store_name = pn_strdup(certificate_db); + domain->has_ca_db = true; return 0; } @@ -560,21 +493,53 @@ int pn_ssl_domain_set_peer_authentication(pn_ssl_domain_t *domain, const char *trusted_CAs) { if (!domain) return -1; + 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: - ssl_log_error("Error: optional peer name checking not yet implemented\n"); // But coming soon - return -1; - case PN_SSL_VERIFY_PEER_NAME: - if (!domain->has_ca_db) { - ssl_log_error("Error: cannot verify peer without a trusted CA configured.\n" - " Use pn_ssl_domain_set_trusted_ca_db()\n"); - return -1; - } if (domain->mode == PN_SSL_MODE_SERVER) { - ssl_log_error("Client certificates not yet supported.\n"); // But coming soon - return -1; + 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)) { + store = open_cert_db(trusted_CAs, NULL, &ec); + if (!store) + return ec; + } else { + store = CertDuplicateStore(domain->cred->trust_store); + } + + if (domain->cred->server_CA_certs) { + // Already have one + changed = true; + 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); + new_cred->trust_store = CertDuplicateStore(domain->cred->trust_store); + new_cred->trust_store_name = pn_strdup(domain->cred->trust_store_name); + pn_decref(domain->cred); + domain->cred = new_cred; + } + + domain->cred->server_CA_certs = store; } break; @@ -586,13 +551,21 @@ int pn_ssl_domain_set_peer_authentication(pn_ssl_domain_t *domain, return -1; } + if (changed) { + 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); + new_cred->trust_store = CertDuplicateStore(domain->cred->trust_store); + new_cred->trust_store_name = pn_strdup(domain->cred->trust_store_name); + pn_decref(domain->cred); + domain->cred = new_cred; + } + domain->verify_mode = mode; - win_credential_t *new_cred = win_credential(domain->mode); - if (!new_cred) - return -1; - new_cred->cert_context = CertDuplicateCertificateContext(domain->cred->cert_context); - pn_decref(domain->cred); - domain->cred = new_cred; + domain->cred->server_CA_certs = store; return 0; } @@ -641,7 +614,7 @@ int pn_ssl_init(pn_ssl_t *ssl0, pn_ssl_domain_t *domain, const char *session_id) pn_incref(domain->cred); SECURITY_STATUS status = SEC_E_OK; - ssl->cred_handle = win_credential_cred_handle(ssl->cred, ssl->domain->verify_mode, + 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"); @@ -649,6 +622,7 @@ int pn_ssl_init(pn_ssl_t *ssl0, pn_ssl_domain_t *domain, const char *session_id) } ssl->state = (domain->mode == PN_SSL_MODE_CLIENT) ? CLIENT_HELLO : NEGOTIATING; + ssl->verify_mode = domain->verify_mode; return 0; } @@ -877,6 +851,7 @@ static bool ssl_decrypt(pn_transport_t *transport) return false; case SEC_I_RENEGOTIATE: + ssl_log_error("unexpected TLS renegotiation\n"); // TODO. Fall through for now. default: ssl_failed(transport, 0); @@ -912,7 +887,7 @@ static void client_handshake_init(pn_transport_t *transport) // Tell SChannel to create the first handshake token (ClientHello) // and place it in sc_outbuf SEC_CHAR *host = const_cast<SEC_CHAR *>(ssl->peer_hostname); - ULONG ctxt_requested = ISC_REQ_STREAM | ISC_REQ_USE_SUPPLIED_CREDS; + ULONG ctxt_requested = ISC_REQ_STREAM | ISC_REQ_USE_SUPPLIED_CREDS | ISC_REQ_EXTENDED_ERROR; ULONG ctxt_attrs; SecBuffer send_buffs[2]; @@ -993,7 +968,6 @@ static void client_handshake( pn_transport_t* transport) { case SEC_I_CONTINUE_NEEDED: // Successful handshake step, requiring data to be sent to peer. - // TODO: check if server has requested a client certificate ssl->sc_out_count = send_buffs[0].cbBuffer; // the token is the whole quantity to send ssl->network_out_pending = ssl->sc_out_count; @@ -1024,6 +998,19 @@ static void client_handshake( pn_transport_t* transport) { 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. @@ -1072,8 +1059,9 @@ static void server_handshake(pn_transport_t* transport) { pni_ssl_t *ssl = transport->ssl; // Feed SChannel ongoing handshake records from the client until the handshake is complete. - ULONG ctxt_requested = ASC_REQ_STREAM; - // TODO: if server and verifying client certificate, ctxtRequested |= ASC_REQ_MUTUAL_AUTH; + 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; @@ -1143,7 +1131,17 @@ static void server_handshake(pn_transport_t* transport) break; } // Handshake complete. - // TODO: manual check of certificate chain here, if bad: ssl_failed() + 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, 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; @@ -1652,3 +1650,452 @@ static size_t buffered_output(pn_transport_t *transport) } 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 (pni_eq_nocase(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 && + 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 (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 && strncasecmp( prefix, slabel, prefix_len )) return false; + if (suffix_len && 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; +} + +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; + } + } 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; +} --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
