Skip to content

Commit

Permalink
schannel: add support for CURLOPT_CAINFO
Browse files Browse the repository at this point in the history
- Move verify_certificate functionality in schannel.c into a new
  file called schannel_verify.c. Additionally, some structure defintions
  from schannel.c have been moved to schannel.h to allow them to be
  used in schannel_verify.c.

- Make verify_certificate functionality for Schannel available on
  all versions of Windows instead of just Windows CE. verify_certificate
  will be invoked on Windows CE or when the user specifies
  CURLOPT_CAINFO and CURLOPT_SSL_VERIFYPEER.

- In verify_certificate, create a custom certificate chain engine that
  exclusively trusts the certificate store backed by the CURLOPT_CAINFO
  file.

- HAVE_EXCLUSIVE_TRUST_MODE macro defined in schannel.h for
  Windows 7+ to control enabling this feature that relies on Windows 7+
  functionality.

- doc updates of --cacert/CAINFO support for schannel

- Use CERT_NAME_SEARCH_ALL_NAMES_FLAG when invoking CertGetNameString
  when available. This implements a TODO in schannel.c to improve
  handling of multiple SANs in a certificate. In particular, all SANs
  will now be searched instead of just the first name.

- Update tool_operate.c to not search for the curl-ca-bundle.crt file
  when using Schannel to maintain backward compatibility. Previously,
  any curl-ca-bundle.crt file found in that search would have been
  ignored by Schannel. But, with CAINFO support, the file found by
  that search would have been used as the certificate store and
  could cause issues for any users that have curl-ca-bundle.crt in
  the search path.

- Add new test cases 3000 and 3001. These test cases check that the first
  and last SAN, respectively, matches the connection hostname. New test
  certificates have been added for these cases. For 3000, the certificate
  prefix is Server-localhost-firstSAN and for 3001, the certificate
  prefix is Server-localhost-secondSAN.

- Remove TODO 15.2 (Add support for custom server certificate
  validation), this commit addresses it.

Closes curl#1325
  • Loading branch information
mcnulty committed Mar 28, 2018
1 parent bea18c7 commit 46831c5
Show file tree
Hide file tree
Showing 34 changed files with 1,366 additions and 245 deletions.
12 changes: 0 additions & 12 deletions docs/TODO
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,6 @@

15. WinSSL/SChannel
15.1 Add support for client certificate authentication
15.2 Add support for custom server certificate validation
15.3 Add support for the --ciphers option

16. SASL
Expand Down Expand Up @@ -823,17 +822,6 @@ that doesn't exist on the server, just like --ftp-create-dirs.
- Getting a Certificate for Schannel
https://msdn.microsoft.com/en-us/library/windows/desktop/aa375447.aspx

15.2 Add support for custom server certificate validation

WinSSL/SChannel currently makes use of the OS-level system and user
certificate trust store. This does not allow the application or user to
customize the server certificate validation process using curl or libcurl.

Therefore support for the existing --cacert or --capath options should be
implemented by supplying a custom certificate to the SChannel APIs, see:
- Getting a Certificate for Schannel
https://msdn.microsoft.com/en-us/library/windows/desktop/aa375447.aspx

15.3 Add support for the --ciphers option

The cipher suites used by WinSSL/SChannel are configured on an OS-level
Expand Down
8 changes: 8 additions & 0 deletions docs/cmdline-opts/cacert.d
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,12 @@ should not be set. If the option is not set, then curl will use the
certificates in the system and user Keychain to verify the peer, which is the
preferred method of verifying the peer's certificate chain.

(Windows only) If curl 7.60.0+ is built against the native SChannel SSL library
(aka WinSSL) then this option may be available: If the build target was Windows
7 or later then it is available, and if it was for an earlier version of
Windows then it's not and SSL certificate validation with this option will
fail. This option is for backward compatibility with other SSL engines; instead
it is recommended to use Windows' store of root certificates (the default for
WinSSL).

If this option is used several times, the last one will be used.
8 changes: 8 additions & 0 deletions docs/libcurl/opts/CURLOPT_CAINFO.3
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,14 @@ should not be set. If the option is not set, then curl will use the
certificates in the system and user Keychain to verify the peer, which is the
preferred method of verifying the peer's certificate chain.

(Windows only) If curl 7.57.1+ is built against the native SChannel SSL library
(aka WinSSL) then this option may be available: If the build target was Windows
7 or later then it is available, and if it was for an earlier version of
Windows then it's not and SSL certificate validation with this option will
fail. This option is for backward compatibility with other SSL engines; instead
it is recommended to use Windows' store of root certificates (the default for
WinSSL).

The application does not have to keep the string around after setting this
option.
.SH DEFAULT
Expand Down
4 changes: 2 additions & 2 deletions lib/Makefile.inc
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ LIB_VAUTH_HFILES = vauth/vauth.h vauth/digest.h vauth/ntlm.h

LIB_VTLS_CFILES = vtls/openssl.c vtls/gtls.c vtls/vtls.c vtls/nss.c \
vtls/polarssl.c vtls/polarssl_threadlock.c vtls/axtls.c \
vtls/cyassl.c vtls/schannel.c vtls/darwinssl.c vtls/gskit.c \
vtls/mbedtls.c
vtls/cyassl.c vtls/schannel.c vtls/schannel_verify.c \
vtls/darwinssl.c vtls/gskit.c vtls/mbedtls.c

LIB_VTLS_HFILES = vtls/openssl.h vtls/vtls.h vtls/gtls.h \
vtls/nssg.h vtls/polarssl.h vtls/polarssl_threadlock.h vtls/axtls.h \
Expand Down
2 changes: 1 addition & 1 deletion lib/hostcheck.c
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
#if defined(USE_OPENSSL) \
|| defined(USE_AXTLS) \
|| defined(USE_GSKIT) \
|| (defined(USE_SCHANNEL) && defined(_WIN32_WCE))
|| defined(USE_SCHANNEL)
/* these backends use functions from this file */

#ifdef HAVE_NETINET_IN_H
Expand Down
231 changes: 31 additions & 200 deletions lib/vtls/schannel.c
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,6 @@
# error "Can't compile SCHANNEL support without SSPI."
#endif

#include <schnlsp.h>
#include <schannel.h>
#include "curl_sspi.h"
#include "schannel.h"
#include "vtls.h"
#include "sendf.h"
Expand All @@ -61,7 +58,6 @@
#include "x509asn1.h"
#include "curl_printf.h"
#include "system_win32.h"
#include "hostcheck.h"

/* The last #include file should be: */
#include "curl_memory.h"
Expand Down Expand Up @@ -133,49 +129,12 @@
# define CALG_SHA_256 0x0000800c
#endif

/* Structs to store Schannel handles */
struct curl_schannel_cred {
CredHandle cred_handle;
TimeStamp time_stamp;
int refcount;
};

struct curl_schannel_ctxt {
CtxtHandle ctxt_handle;
TimeStamp time_stamp;
};

struct ssl_backend_data {
struct curl_schannel_cred *cred;
struct curl_schannel_ctxt *ctxt;
SecPkgContext_StreamSizes stream_sizes;
size_t encdata_length, decdata_length;
size_t encdata_offset, decdata_offset;
unsigned char *encdata_buffer, *decdata_buffer;
/* encdata_is_incomplete: if encdata contains only a partial record that
can't be decrypted without another Curl_read_plain (that is, status is
SEC_E_INCOMPLETE_MESSAGE) then set this true. after Curl_read_plain writes
more bytes into encdata then set this back to false. */
bool encdata_is_incomplete;
unsigned long req_flags, ret_flags;
CURLcode recv_unrecoverable_err; /* schannel_recv had an unrecoverable err */
bool recv_sspi_close_notify; /* true if connection closed by close_notify */
bool recv_connection_closed; /* true if connection closed, regardless how */
bool use_alpn; /* true if ALPN is used for this connection */
};

#define BACKEND connssl->backend

static Curl_recv schannel_recv;
static Curl_send schannel_send;

static CURLcode pkp_pin_peer_pubkey(struct connectdata *conn, int sockindex,
const char *pinnedpubkey);

#ifdef _WIN32_WCE
static CURLcode verify_certificate(struct connectdata *conn, int sockindex);
#endif

static void InitSecBuffer(SecBuffer *buffer, unsigned long BufType,
void *BufDataPtr, unsigned long BufByteSize)
{
Expand Down Expand Up @@ -275,6 +234,24 @@ schannel_connect_step1(struct connectdata *conn, int sockindex)
BACKEND->use_alpn = false;
#endif

#ifdef _WIN32_WCE
/* certificate validation on CE doesn't seem to work right; we'll
* do it following a more manual process. */
BACKEND->use_manual_cred_validation = true;
#elif defined(HAVE_EXCLUSIVE_TRUST_MODE)
/* Using an application-specific certificate store is possible, manual
* credential validation will be performed if requested by the SSL config */
BACKEND->use_manual_cred_validation = SSL_CONN_CONFIG(CAfile) != NULL;
#else
/* Using an application-specific certificate store is not possible, do
* not perform manual credential validation */
BACKEND->use_manual_cred_validation = false;
if(SSL_CONN_CONFIG(CAfile))
infof(data, "schannel: the WinSSL version does not support using the "
"CAINFO option, falling back to using the default WinSSL "
"certificate store configuration\n");
#endif

BACKEND->cred = NULL;

/* check for an existing re-usable credential handle */
Expand All @@ -298,26 +275,23 @@ schannel_connect_step1(struct connectdata *conn, int sockindex)
schannel_cred.dwVersion = SCHANNEL_CRED_VERSION;

if(conn->ssl_config.verifypeer) {
#ifdef _WIN32_WCE
/* certificate validation on CE doesn't seem to work right; we'll
do it following a more manual process. */
schannel_cred.dwFlags = SCH_CRED_MANUAL_CRED_VALIDATION |
SCH_CRED_IGNORE_NO_REVOCATION_CHECK |
SCH_CRED_IGNORE_REVOCATION_OFFLINE;
#else
schannel_cred.dwFlags = SCH_CRED_AUTO_CRED_VALIDATION;
if(BACKEND->use_manual_cred_validation)
schannel_cred.dwFlags = SCH_CRED_MANUAL_CRED_VALIDATION;
else
schannel_cred.dwFlags = SCH_CRED_AUTO_CRED_VALIDATION;

/* TODO s/data->set.ssl.no_revoke/SSL_SET_OPTION(no_revoke)/g */
if(data->set.ssl.no_revoke)
if(data->set.ssl.no_revoke) {
schannel_cred.dwFlags |= SCH_CRED_IGNORE_NO_REVOCATION_CHECK |
SCH_CRED_IGNORE_REVOCATION_OFFLINE;
else
schannel_cred.dwFlags |= SCH_CRED_REVOCATION_CHECK_CHAIN;
#endif
if(data->set.ssl.no_revoke)

infof(data, "schannel: disabled server certificate revocation "
"checks\n");
else
}
else {
schannel_cred.dwFlags |= SCH_CRED_REVOCATION_CHECK_CHAIN;
infof(data, "schannel: checking server certificate revocation\n");
}
}
else {
schannel_cred.dwFlags = SCH_CRED_MANUAL_CRED_VALIDATION |
Expand Down Expand Up @@ -780,12 +754,9 @@ schannel_connect_step2(struct connectdata *conn, int sockindex)
}
}

#ifdef _WIN32_WCE
/* Windows CE doesn't do any server certificate validation.
We have to do it manually. */
if(conn->ssl_config.verifypeer)
if(conn->ssl_config.verifypeer && BACKEND->use_manual_cred_validation) {
return verify_certificate(conn, sockindex);
#endif
}

return CURLE_OK;
}
Expand Down Expand Up @@ -1750,146 +1721,6 @@ static CURLcode pkp_pin_peer_pubkey(struct connectdata *conn, int sockindex,
return result;
}

#ifdef _WIN32_WCE
static CURLcode verify_certificate(struct connectdata *conn, int sockindex)
{
SECURITY_STATUS status;
struct Curl_easy *data = conn->data;
struct ssl_connect_data *connssl = &conn->ssl[sockindex];
CURLcode result = CURLE_OK;
CERT_CONTEXT *pCertContextServer = NULL;
const CERT_CHAIN_CONTEXT *pChainContext = NULL;
const char * const conn_hostname = SSL_IS_PROXY() ?
conn->http_proxy.host.name :
conn->host.name;

status = s_pSecFn->QueryContextAttributes(&BACKEND->ctxt->ctxt_handle,
SECPKG_ATTR_REMOTE_CERT_CONTEXT,
&pCertContextServer);

if((status != SEC_E_OK) || (pCertContextServer == NULL)) {
failf(data, "schannel: Failed to read remote certificate context: %s",
Curl_sspi_strerror(conn, status));
result = CURLE_PEER_FAILED_VERIFICATION;
}

if(result == CURLE_OK) {
CERT_CHAIN_PARA ChainPara;
memset(&ChainPara, 0, sizeof(ChainPara));
ChainPara.cbSize = sizeof(ChainPara);

if(!CertGetCertificateChain(NULL,
pCertContextServer,
NULL,
pCertContextServer->hCertStore,
&ChainPara,
(data->set.ssl.no_revoke ? 0 :
CERT_CHAIN_REVOCATION_CHECK_CHAIN),
NULL,
&pChainContext)) {
failf(data, "schannel: CertGetCertificateChain failed: %s",
Curl_sspi_strerror(conn, GetLastError()));
pChainContext = NULL;
result = CURLE_PEER_FAILED_VERIFICATION;
}

if(result == CURLE_OK) {
CERT_SIMPLE_CHAIN *pSimpleChain = pChainContext->rgpChain[0];
DWORD dwTrustErrorMask = ~(DWORD)(CERT_TRUST_IS_NOT_TIME_NESTED);
dwTrustErrorMask &= pSimpleChain->TrustStatus.dwErrorStatus;
if(dwTrustErrorMask) {
if(dwTrustErrorMask & CERT_TRUST_IS_REVOKED)
failf(data, "schannel: CertGetCertificateChain trust error"
" CERT_TRUST_IS_REVOKED");
else if(dwTrustErrorMask & CERT_TRUST_IS_PARTIAL_CHAIN)
failf(data, "schannel: CertGetCertificateChain trust error"
" CERT_TRUST_IS_PARTIAL_CHAIN");
else if(dwTrustErrorMask & CERT_TRUST_IS_UNTRUSTED_ROOT)
failf(data, "schannel: CertGetCertificateChain trust error"
" CERT_TRUST_IS_UNTRUSTED_ROOT");
else if(dwTrustErrorMask & CERT_TRUST_IS_NOT_TIME_VALID)
failf(data, "schannel: CertGetCertificateChain trust error"
" CERT_TRUST_IS_NOT_TIME_VALID");
else
failf(data, "schannel: CertGetCertificateChain error mask: 0x%08x",
dwTrustErrorMask);
result = CURLE_PEER_FAILED_VERIFICATION;
}
}
}

if(result == CURLE_OK) {
if(conn->ssl_config.verifyhost) {
TCHAR cert_hostname_buff[256];
DWORD len;

/* TODO: Fix this for certificates with multiple alternative names.
Right now we're only asking for the first preferred alternative name.
Instead we'd need to do all via CERT_NAME_SEARCH_ALL_NAMES_FLAG
(if WinCE supports that?) and run this section in a loop for each.
https://msdn.microsoft.com/en-us/library/windows/desktop/aa376086.aspx
curl: (51) schannel: CertGetNameString() certificate hostname
(.google.com) did not match connection (google.com)
*/
len = CertGetNameString(pCertContextServer,
CERT_NAME_DNS_TYPE,
CERT_NAME_DISABLE_IE4_UTF8_FLAG,
NULL,
cert_hostname_buff,
256);
if(len > 0) {
const char *cert_hostname;

/* Comparing the cert name and the connection hostname encoded as UTF-8
* is acceptable since both values are assumed to use ASCII
* (or some equivalent) encoding
*/
cert_hostname = Curl_convert_tchar_to_UTF8(cert_hostname_buff);
if(!cert_hostname) {
result = CURLE_OUT_OF_MEMORY;
}
else{
int match_result;

match_result = Curl_cert_hostcheck(cert_hostname, conn->host.name);
if(match_result == CURL_HOST_MATCH) {
infof(data,
"schannel: connection hostname (%s) validated "
"against certificate name (%s)\n",
conn->host.name,
cert_hostname);
result = CURLE_OK;
}
else{
failf(data,
"schannel: connection hostname (%s) "
"does not match certificate name (%s)",
conn->host.name,
cert_hostname);
result = CURLE_PEER_FAILED_VERIFICATION;
}
Curl_unicodefree(cert_hostname);
}
}
else {
failf(data,
"schannel: CertGetNameString did not provide any "
"certificate name information");
result = CURLE_PEER_FAILED_VERIFICATION;
}
}
}

if(pChainContext)
CertFreeCertificateChain(pChainContext);

if(pCertContextServer)
CertFreeCertificateContext(pCertContextServer);

return result;
}
#endif /* _WIN32_WCE */

static void Curl_schannel_checksum(const unsigned char *input,
size_t inputlen,
unsigned char *checksum,
Expand Down
Loading

0 comments on commit 46831c5

Please sign in to comment.