Hi Steffan, Thank you for the review and the ack.
On Sun, Jan 21, 2018 at 5:58 AM, Steffan Karger <steffan.kar...@fox-it.com> wrote: > Hi, > > On 20-01-18 05:52, selva.n...@gmail.com wrote: >> From: Selva Nair <selva.n...@gmail.com> >> >> - If an NCRYPT handle for the private key can be obtained, use >> NCryptSignHash from the Cryptography NG API to sign the hash. >> >> This should work for all keys in the Windows certifiate stores >> but may fail for keys in a legacy token, for example. In such >> cases, we disable TLS v1.2 and fall back to the current >> behaviour. A warning is logged unless TLS version is already >> restricted to <= 1.1 >> >> Signed-off-by: Selva Nair <selva.n...@gmail.com> >> --- >> >> Depends on patches: >> patch 200: https://patchwork.openvpn.net/patch/200/ >> patch 201: https://patchwork.openvpn.net/patch/201/ >> >> v2: Based on Stefan's review: >> - Replace SSL_CTX_get(set)_option with SSL_CTX_get(set)_max_proto_version >> - Wrap some lines to 80 chars >> - replace M_INFO by D_LOW in a low level debug message >> >> Also removed some defines added in v1 that are actually present in mingw >> versions that we target >> >> src/openvpn/Makefile.am | 2 +- >> src/openvpn/cryptoapi.c | 85 >> ++++++++++++++++++++++++++++++++++++++++++++++--- >> src/openvpn/options.c | 18 ----------- >> 3 files changed, 81 insertions(+), 24 deletions(-) >> >> diff --git a/src/openvpn/Makefile.am b/src/openvpn/Makefile.am >> index fcc22d6..1a2f42e 100644 >> --- a/src/openvpn/Makefile.am >> +++ b/src/openvpn/Makefile.am >> @@ -132,5 +132,5 @@ openvpn_LDADD = \ >> $(OPTIONAL_DL_LIBS) >> if WIN32 >> openvpn_SOURCES += openvpn_win32_resources.rc block_dns.c block_dns.h >> -openvpn_LDADD += -lgdi32 -lws2_32 -lwininet -lcrypt32 -liphlpapi -lwinmm >> -lfwpuclnt -lrpcrt4 >> +openvpn_LDADD += -lgdi32 -lws2_32 -lwininet -lcrypt32 -liphlpapi -lwinmm >> -lfwpuclnt -lrpcrt4 -lncrypt >> endif >> diff --git a/src/openvpn/cryptoapi.c b/src/openvpn/cryptoapi.c >> index 4f2c636..f155123 100644 >> --- a/src/openvpn/cryptoapi.c >> +++ b/src/openvpn/cryptoapi.c >> @@ -42,6 +42,7 @@ >> #include <openssl/err.h> >> #include <windows.h> >> #include <wincrypt.h> >> +#include <ncrypt.h> >> #include <stdio.h> >> #include <ctype.h> >> #include <assert.h> >> @@ -83,6 +84,7 @@ >> #define CRYPTOAPI_F_CRYPT_SIGN_HASH 106 >> #define CRYPTOAPI_F_LOAD_LIBRARY 107 >> #define CRYPTOAPI_F_GET_PROC_ADDRESS 108 >> +#define CRYPTOAPI_F_NCRYPT_SIGN_HASH 109 >> >> static ERR_STRING_DATA CRYPTOAPI_str_functs[] = { >> { ERR_PACK(ERR_LIB_CRYPTOAPI, 0, 0), >> "microsoft cryptoapi"}, >> @@ -95,12 +97,13 @@ static ERR_STRING_DATA CRYPTOAPI_str_functs[] = { >> { ERR_PACK(0, CRYPTOAPI_F_CRYPT_SIGN_HASH, 0), >> "CryptSignHash" }, >> { ERR_PACK(0, CRYPTOAPI_F_LOAD_LIBRARY, 0), >> "LoadLibrary" }, >> { ERR_PACK(0, CRYPTOAPI_F_GET_PROC_ADDRESS, 0), >> "GetProcAddress" }, >> + { ERR_PACK(0, CRYPTOAPI_F_NCRYPT_SIGN_HASH, 0), >> "NCryptSignHash" }, >> { 0, NULL } >> }; >> >> typedef struct _CAPI_DATA { >> const CERT_CONTEXT *cert_context; >> - HCRYPTPROV crypt_prov; >> + HCRYPTPROV_OR_NCRYPT_KEY_HANDLE crypt_prov; >> DWORD key_spec; >> BOOL free_crypt_prov; >> } CAPI_DATA; >> @@ -210,6 +213,41 @@ rsa_pub_dec(int flen, const unsigned char *from, >> unsigned char *to, RSA *rsa, in >> return 0; >> } >> >> +/** >> + * Sign the hash in 'from' using NCryptSignHash(). This requires an NCRYPT >> + * key handle in cd->crypt_prov. On return the signature is in 'to'. Returns >> + * the length of the signature or 0 on error. >> + * For now we support only RSA and the padding is assumed to be PKCS1 v1.5 >> + */ >> +static int >> +priv_enc_CNG(const CAPI_DATA *cd, const unsigned char *from, int flen, >> + unsigned char *to, int tlen, int padding) >> +{ >> + NCRYPT_KEY_HANDLE hkey = cd->crypt_prov; >> + DWORD len; >> + ASSERT(cd->key_spec == CERT_NCRYPT_KEY_SPEC); >> + >> + msg(D_LOW, "Signing hash using CNG: data size = %d", flen); >> + >> + /* The hash OID is already in 'from'. So set the hash algorithm >> + * in the padding info struct to NULL. >> + */ >> + BCRYPT_PKCS1_PADDING_INFO padinfo = {NULL}; >> + DWORD status; >> + >> + status = NCryptSignHash(hkey, padding? &padinfo : NULL, (BYTE*) from, >> flen, >> + to, tlen, &len, padding? BCRYPT_PAD_PKCS1 : 0); >> + if (status != ERROR_SUCCESS) >> + { >> + SetLastError(status); >> + CRYPTOAPIerr(CRYPTOAPI_F_NCRYPT_SIGN_HASH); >> + len = 0; >> + } >> + >> + /* Unlike CAPI, CNG signature is in big endian order. No reversing >> needed. */ >> + return len; >> +} >> + >> /* sign arbitrary data */ >> static int >> rsa_priv_enc(int flen, const unsigned char *from, unsigned char *to, RSA >> *rsa, int padding) >> @@ -230,6 +268,11 @@ rsa_priv_enc(int flen, const unsigned char *from, >> unsigned char *to, RSA *rsa, i >> RSAerr(RSA_F_RSA_OSSL_PRIVATE_ENCRYPT, RSA_R_UNKNOWN_PADDING_TYPE); >> return 0; >> } >> + if (cd->key_spec == CERT_NCRYPT_KEY_SPEC) >> + { >> + return priv_enc_CNG(cd, from, flen, to, RSA_size(rsa), padding); >> + } >> + >> /* Unfortunately, there is no "CryptSign()" function in CryptoAPI, that >> would >> * be way to straightforward for M$, I guess... So we have to do it this >> * tricky way instead, by creating a "Hash", and load the already-made >> hash >> @@ -322,7 +365,14 @@ finish(RSA *rsa) >> } >> if (cd->crypt_prov && cd->free_crypt_prov) >> { >> - CryptReleaseContext(cd->crypt_prov, 0); >> + if (cd->key_spec == CERT_NCRYPT_KEY_SPEC) >> + { >> + NCryptFreeObject(cd->crypt_prov); >> + } >> + else >> + { >> + CryptReleaseContext(cd->crypt_prov, 0); >> + } >> } >> if (cd->cert_context) >> { >> @@ -458,8 +508,11 @@ SSL_CTX_use_CryptoAPI_certificate(SSL_CTX *ssl_ctx, >> const char *cert_prop) >> } >> >> /* set up stuff to use the private key */ >> - if (!CryptAcquireCertificatePrivateKey(cd->cert_context, >> CRYPT_ACQUIRE_COMPARE_KEY_FLAG, >> - NULL, &cd->crypt_prov, >> &cd->key_spec, &cd->free_crypt_prov)) >> + /* We prefer to get an NCRYPT key handle so that TLS1.2 can be >> supported */ >> + DWORD flags = CRYPT_ACQUIRE_COMPARE_KEY_FLAG >> + | CRYPT_ACQUIRE_PREFER_NCRYPT_KEY_FLAG; >> + if (!CryptAcquireCertificatePrivateKey(cd->cert_context, flags, NULL, >> + &cd->crypt_prov, &cd->key_spec, &cd->free_crypt_prov)) >> { >> /* if we don't have a smart card reader here, and we try to access a >> * smart card certificate, we get: >> @@ -470,6 +523,21 @@ SSL_CTX_use_CryptoAPI_certificate(SSL_CTX *ssl_ctx, >> const char *cert_prop) >> /* here we don't need to do CryptGetUserKey() or anything; all >> necessary key >> * info is in cd->cert_context, and then, in cd->crypt_prov. */ >> >> + /* if we do not have an NCRYPT key handle restrict TLS to v1.1 or lower >> */ >> + int max_version = SSL_CTX_get_max_proto_version(ssl_ctx); >> + if ((!max_version || max_version > TLS1_1_VERSION) >> + && cd->key_spec != CERT_NCRYPT_KEY_SPEC) >> + { >> + msg(M_WARN,"WARNING: cryptoapicert: private key is in a legacy >> store." >> + " Restricting TLS version to 1.1"); >> + if (!SSL_CTX_set_max_proto_version(ssl_ctx, TLS1_1_VERSION)) >> + { >> + msg(M_NONFATAL,"ERROR: cryptoapicert: unable to set max TLS >> version" >> + " to 1.1. Try config option --tls-version-min 1.1"); >> + goto err; >> + } >> + } >> + >> my_rsa_method = RSA_meth_new("Microsoft Cryptography API RSA Method", >> RSA_METHOD_FLAG_NO_CHECK); >> check_malloc_return(my_rsa_method); >> @@ -550,7 +618,14 @@ err: >> { >> if (cd->free_crypt_prov && cd->crypt_prov) >> { >> - CryptReleaseContext(cd->crypt_prov, 0); >> + if (cd->key_spec == CERT_NCRYPT_KEY_SPEC) >> + { >> + NCryptFreeObject(cd->crypt_prov); >> + } >> + else >> + { >> + CryptReleaseContext(cd->crypt_prov, 0); >> + } >> } >> if (cd->cert_context) >> { >> diff --git a/src/openvpn/options.c b/src/openvpn/options.c >> index b240e2e..220c2e5 100644 >> --- a/src/openvpn/options.c >> +++ b/src/openvpn/options.c >> @@ -3018,24 +3018,6 @@ options_postprocess_mutate(struct options *o) >> } >> #endif >> >> -#ifdef ENABLE_CRYPTOAPI >> - if (o->cryptoapi_cert) >> - { >> - const int tls_version_max = >> - (o->ssl_flags >> SSLF_TLS_VERSION_MAX_SHIFT) >> - &SSLF_TLS_VERSION_MAX_MASK; >> - >> - if (tls_version_max == TLS_VER_UNSPEC || tls_version_max > >> TLS_VER_1_1) >> - { >> - msg(M_WARN, "Warning: cryptapicert used, setting maximum TLS " >> - "version to 1.1."); >> - o->ssl_flags &= ~(SSLF_TLS_VERSION_MAX_MASK >> - <<SSLF_TLS_VERSION_MAX_SHIFT); >> - o->ssl_flags |= (TLS_VER_1_1 << SSLF_TLS_VERSION_MAX_SHIFT); >> - } >> - } >> -#endif /* ENABLE_CRYPTOAPI */ >> - >> #if P2MP >> /* >> * Save certain parms before modifying options via --pull >> > > Code looks good, and now nicely connects using TLS 1.2 with a > certificate from the windows certificate store, nice! > > I don't know how to trigger the 'legacy store' code path. Have you > tested that yourself? If you can confirm that you've tested it: Yes, tested again using an old e-token which triggers this: Sun Jan 21 09:39:23 2018 us=632921 WARNING: cryptoapicert: private key is in a legacy store. Restricting TLS version to 1.1 (on Windows 10). and connects with Sun Jan 21 09:39:53 2018 us=955878 Control Channel: TLSv1.1, cipher TLSv1.0 ECDHE-RSA-AES256-SHA, 1024 bit RSA If the patch-author is allowed to say Tested-by: Selva Nair <selva.n...@gmail.com> > > Acked-by: Steffan Karger <steffan.kar...@fox-it.com> > Thanks, Selva ------------------------------------------------------------------------------ Check out the vibrant tech community on one of the world's most engaging tech sites, Slashdot.org! http://sdm.link/slashdot _______________________________________________ Openvpn-devel mailing list Openvpn-devel@lists.sourceforge.net https://lists.sourceforge.net/lists/listinfo/openvpn-devel