From 255bb9497a3571742656c23974f6c0f58f2259a5 Mon Sep 17 00:00:00 2001 From: JacobBarthelmeh Date: Tue, 12 Sep 2023 14:15:23 -0700 Subject: [PATCH 01/10] add TrustedSystemCAKeys sshd option for system CA load --- apps/wolfsshd/configuration.c | 46 ++++++++++++++++++++++++++++++++++- apps/wolfsshd/configuration.h | 2 ++ apps/wolfsshd/wolfsshd.c | 33 +++++++++++++++++++++++++ src/certman.c | 21 +++++++++++++++- wolfssh/certman.h | 4 +++ wolfssh/test.h | 5 +++- 6 files changed, 108 insertions(+), 3 deletions(-) diff --git a/apps/wolfsshd/configuration.c b/apps/wolfsshd/configuration.c index 61ce9b1c1..6923554b5 100644 --- a/apps/wolfsshd/configuration.c +++ b/apps/wolfsshd/configuration.c @@ -104,6 +104,7 @@ struct WOLFSSHD_CONFIG { byte permitEmptyPasswords:1; byte authKeysFileSet:1; /* if not set then no explicit authorized keys */ byte strictModes:1; /* enforce file permission/ownership checks */ + byte useSystemCA:1; }; /* Maximum depth of nested Include directives. Bounds the recursion @@ -425,9 +426,10 @@ enum { OPT_PUBKEY_AUTH = 24, OPT_STRICT_MODES = 25, OPT_AUTHORIZED_UPN_DOMAINS = 26, + OPT_TRUSTED_SYSTEM_CA_KEYS = 27, }; enum { - NUM_OPTIONS = 27 + NUM_OPTIONS = 28 }; static const CONFIG_OPTION options[NUM_OPTIONS] = { @@ -454,6 +456,7 @@ static const CONFIG_OPTION options[NUM_OPTIONS] = { {OPT_FORCE_CMD, "ForceCommand"}, {OPT_HOST_CERT, "HostCertificate"}, {OPT_TRUSTED_USER_CA_KEYS, "TrustedUserCAKeys"}, + {OPT_TRUSTED_SYSTEM_CA_KEYS, "TrustedSystemCAKeys"}, {OPT_PIDFILE, "PidFile"}, {OPT_BANNER, "Banner"}, {OPT_STRICT_MODES, "StrictModes"}, @@ -1311,6 +1314,9 @@ static int HandleConfigOption(WOLFSSHD_CONFIG** conf, int opt, /* TODO: Add logic to check if file exists? */ ret = wolfSSHD_ConfigSetUserCAKeysFile(*conf, value); break; + case OPT_TRUSTED_SYSTEM_CA_KEYS: + ret = wolfSSHD_ConfigSetSystemCA(*conf, value); + break; case OPT_PIDFILE: ret = SetFileString(&(*conf)->pidFile, value, (*conf)->heap); break; @@ -1668,6 +1674,44 @@ char* wolfSSHD_ConfigGetHostCertFile(const WOLFSSHD_CONFIG* conf) return ret; } + +/* getter function for if using system CAs + * return 1 if true and 0 if false */ +int wolfSSHD_ConfigGetSystemCA(const WOLFSSHD_CONFIG* conf) +{ + if (conf != NULL) { + return conf->useSystemCA; + } + return 0; +} + + +/* setter function for if using system CAs + * 'yes' if true and 'no' if false + * returns WS_SUCCESS on success */ +int wolfSSHD_ConfigSetSystemCA(WOLFSSHD_CONFIG* conf, const char* value) +{ + int ret = WS_SUCCESS; + + if (conf != NULL) { + if (WSTRCMP(value, "yes") == 0) { + wolfSSH_Log(WS_LOG_INFO, "[SSHD] System CAs enabled"); + conf->useSystemCA = 1; + } + else if (WSTRCMP(value, "no") == 0) { + wolfSSH_Log(WS_LOG_INFO, "[SSHD] System CAs disabled"); + conf->useSystemCA = 0; + } + else { + wolfSSH_Log(WS_LOG_INFO, "[SSHD] System CAs unexpected flag"); + ret = WS_FATAL_ERROR; + } + } + + return ret; +} + + char* wolfSSHD_ConfigGetUserCAKeysFile(const WOLFSSHD_CONFIG* conf) { char* ret = NULL; diff --git a/apps/wolfsshd/configuration.h b/apps/wolfsshd/configuration.h index 5792b4e89..c15c5b2dc 100644 --- a/apps/wolfsshd/configuration.h +++ b/apps/wolfsshd/configuration.h @@ -64,6 +64,8 @@ char* wolfSSHD_ConfigGetUserCAKeysFile(const WOLFSSHD_CONFIG* conf); char* wolfSSHD_ConfigGetAuthorizedUPNDomains(const WOLFSSHD_CONFIG* conf); int wolfSSHD_ConfigSetHostKeyFile(WOLFSSHD_CONFIG* conf, const char* file); int wolfSSHD_ConfigSetHostCertFile(WOLFSSHD_CONFIG* conf, const char* file); +int wolfSSHD_ConfigSetSystemCA(WOLFSSHD_CONFIG* conf, const char* value); +int wolfSSHD_ConfigGetSystemCA(const WOLFSSHD_CONFIG* conf); int wolfSSHD_ConfigSetUserCAKeysFile(WOLFSSHD_CONFIG* conf, const char* file); word16 wolfSSHD_ConfigGetPort(const WOLFSSHD_CONFIG* conf); char* wolfSSHD_ConfigGetAuthKeysFile(const WOLFSSHD_CONFIG* conf); diff --git a/apps/wolfsshd/wolfsshd.c b/apps/wolfsshd/wolfsshd.c index fb8111deb..c0a91e9aa 100644 --- a/apps/wolfsshd/wolfsshd.c +++ b/apps/wolfsshd/wolfsshd.c @@ -526,6 +526,39 @@ static int SetupCTX(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX** ctx, #endif /* WOLFSSH_OSSH_CERTS || WOLFSSH_CERTS */ #ifdef WOLFSSH_CERTS + /* check if loading in system CA certs */ + if (ret == WS_SUCCESS && wolfSSHD_ConfigGetSystemCA(conf)) { + WOLFSSL_CTX* sslCtx; + + wolfSSH_Log(WS_LOG_INFO, "[SSHD] Using system CAs"); + sslCtx = wolfSSL_CTX_new(wolfSSLv23_method()); + if (sslCtx == NULL) { + wolfSSH_Log(WS_LOG_INFO, "[SSHD] Unable to create temporary CTX"); + ret = WS_FATAL_ERROR; + } + + if (ret == WS_SUCCESS) { + if (wolfSSL_CTX_load_system_CA_certs(sslCtx) != WOLFSSL_SUCCESS) { + wolfSSH_Log(WS_LOG_INFO, "[SSHD] Issue loading system CAs"); + ret = WS_FATAL_ERROR; + } + } + + if (ret == WS_SUCCESS) { + if (wolfSSH_SetCertManager(*ctx, + wolfSSL_CTX_GetCertManager(sslCtx)) != WS_SUCCESS) { + wolfSSH_Log(WS_LOG_INFO, + "[SSHD] Issue copying over system CAs"); + ret = WS_FATAL_ERROR; + } + } + + if (sslCtx != NULL) { + wolfSSL_CTX_free(sslCtx); + } + } + + /* load in CA certs from file set */ if (ret == WS_SUCCESS) { char* caCert = wolfSSHD_ConfigGetUserCAKeysFile(conf); if (caCert != NULL) { diff --git a/src/certman.c b/src/certman.c index 2674ca2f7..922c0d04e 100644 --- a/src/certman.c +++ b/src/certman.c @@ -36,7 +36,6 @@ #endif -#include #include #include #include @@ -85,6 +84,26 @@ struct WOLFSSH_CERTMAN { }; +/* used to import an external cert manager, frees and replaces existing manager + * returns WS_SUCCESS on success + */ +int wolfSSH_SetCertManager(WOLFSSH_CTX* ctx, WOLFSSL_CERT_MANAGER* cm) +{ + if (ctx == NULL || cm == NULL) { + return WS_BAD_ARGUMENT; + } + + /* free up existing cm if present */ + if (ctx->certMan != NULL && ctx->certMan->cm != NULL) { + wolfSSL_CertManagerFree(ctx->certMan->cm); + } + wolfSSL_CertManager_up_ref(cm); + ctx->certMan->cm = cm; + + return WS_SUCCESS; +} + + static WOLFSSH_CERTMAN* _CertMan_init(WOLFSSH_CERTMAN* cm, void* heap) { WOLFSSH_CERTMAN* ret = NULL; diff --git a/wolfssh/certman.h b/wolfssh/certman.h index f80735550..854b15e8c 100644 --- a/wolfssh/certman.h +++ b/wolfssh/certman.h @@ -30,6 +30,7 @@ #include #include +#include /* included for WOLFSSL_CERT_MANAGER struct */ #ifdef __cplusplus extern "C" { @@ -40,6 +41,9 @@ struct WOLFSSH_CERTMAN; typedef struct WOLFSSH_CERTMAN WOLFSSH_CERTMAN; +WOLFSSH_API +int wolfSSH_SetCertManager(WOLFSSH_CTX* ctx, WOLFSSL_CERT_MANAGER* cm); + WOLFSSH_API WOLFSSH_CERTMAN* wolfSSH_CERTMAN_new(void* heap); diff --git a/wolfssh/test.h b/wolfssh/test.h index bfadb703d..d156d7f9b 100644 --- a/wolfssh/test.h +++ b/wolfssh/test.h @@ -1150,6 +1150,7 @@ static INLINE void build_addr_ipv6(struct sockaddr_in6* addr, const char* peer, #define BAD 0xFF +#ifndef WOLFSSL_BASE16 static const byte hexDecode[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, @@ -1219,7 +1220,9 @@ static int Base16_Decode(const byte* in, word32 inLen, *outLen = outIdx; return 0; } - +#else + #include +#endif /* !WOLFSSL_BASE16 */ static void FreeBins(byte* b1, byte* b2, byte* b3, byte* b4) { From 58705cc3cefbdd453a9c162d5797c7b99a1f70e7 Mon Sep 17 00:00:00 2001 From: JacobBarthelmeh Date: Tue, 26 Sep 2023 16:27:34 -0600 Subject: [PATCH 02/10] add macro guard for system ca certs load --- apps/wolfsshd/wolfsshd.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/wolfsshd/wolfsshd.c b/apps/wolfsshd/wolfsshd.c index c0a91e9aa..d1085af64 100644 --- a/apps/wolfsshd/wolfsshd.c +++ b/apps/wolfsshd/wolfsshd.c @@ -527,6 +527,7 @@ static int SetupCTX(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX** ctx, #ifdef WOLFSSH_CERTS /* check if loading in system CA certs */ + #ifdef WOLFSSL_SYS_CA_CERTS if (ret == WS_SUCCESS && wolfSSHD_ConfigGetSystemCA(conf)) { WOLFSSL_CTX* sslCtx; @@ -557,6 +558,7 @@ static int SetupCTX(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX** ctx, wolfSSL_CTX_free(sslCtx); } } + #endif /* load in CA certs from file set */ if (ret == WS_SUCCESS) { From 51468f5833bd02f600dbcdf4f658753f369b3ad9 Mon Sep 17 00:00:00 2001 From: Kareem Date: Fri, 11 Oct 2024 15:19:48 -0700 Subject: [PATCH 03/10] Add support for loading user CA certs from a configurable Windows cert store. --- apps/wolfsshd/configuration.c | 145 +++++++++++++++++++++++++++++++++- apps/wolfsshd/configuration.h | 8 ++ apps/wolfsshd/wolfsshd.c | 25 ++++-- 3 files changed, 170 insertions(+), 8 deletions(-) diff --git a/apps/wolfsshd/configuration.c b/apps/wolfsshd/configuration.c index 6923554b5..531bfd139 100644 --- a/apps/wolfsshd/configuration.c +++ b/apps/wolfsshd/configuration.c @@ -94,6 +94,9 @@ struct WOLFSSHD_CONFIG { char* forceCmd; char* pidFile; char* authorizedUPNDomains; /* allowlist of UPN realms for cert auth */ + char* winUserStores; + char* winUserDwFlags; + char* winUserPvPara; WOLFSSHD_CONFIG* next; /* next config in list */ long loginTimer; word16 port; @@ -105,6 +108,7 @@ struct WOLFSSHD_CONFIG { byte authKeysFileSet:1; /* if not set then no explicit authorized keys */ byte strictModes:1; /* enforce file permission/ownership checks */ byte useSystemCA:1; + byte useUserCAStore:1; }; /* Maximum depth of nested Include directives. Bounds the recursion @@ -385,6 +389,9 @@ void wolfSSHD_ConfigFree(WOLFSSHD_CONFIG* conf) FreeString(¤t->authorizedUPNDomains, heap); FreeString(¤t->usrAppliesTo, heap); FreeString(¤t->groupAppliesTo, heap); + FreeString(¤t->winUserStores, heap); + FreeString(¤t->winUserDwFlags, heap); + FreeString(¤t->winUserPvPara, heap); WFREE(current, heap, DYNTYPE_SSHD); current = next; @@ -425,11 +432,15 @@ enum { OPT_BANNER = 23, OPT_PUBKEY_AUTH = 24, OPT_STRICT_MODES = 25, - OPT_AUTHORIZED_UPN_DOMAINS = 26, - OPT_TRUSTED_SYSTEM_CA_KEYS = 27, + OPT_TRUSTED_SYSTEM_CA_KEYS = 26, + OPT_TRUSTED_USER_CA_STORE = 27, + OPT_WIN_USER_STORES = 28, + OPT_WIN_USER_DW_FLAGS = 29, + OPT_WIN_USER_PV_PARA = 30, + OPT_AUTHORIZED_UPN_DOMAINS = 31 }; enum { - NUM_OPTIONS = 28 + NUM_OPTIONS = 32 }; static const CONFIG_OPTION options[NUM_OPTIONS] = { @@ -460,6 +471,10 @@ static const CONFIG_OPTION options[NUM_OPTIONS] = { {OPT_PIDFILE, "PidFile"}, {OPT_BANNER, "Banner"}, {OPT_STRICT_MODES, "StrictModes"}, + {OPT_TRUSTED_USER_CA_STORE, "TrustedUserCaStore"}, + {OPT_WIN_USER_STORES, "WinUserStores"}, + {OPT_WIN_USER_DW_FLAGS, "WinUserDwFlags"}, + {OPT_WIN_USER_PV_PARA, "WinUserPvPara"}, {OPT_AUTHORIZED_UPN_DOMAINS, "AuthorizedUPNDomains"}, }; @@ -1325,6 +1340,17 @@ static int HandleConfigOption(WOLFSSHD_CONFIG** conf, int opt, break; case OPT_STRICT_MODES: ret = HandleStrictModes(*conf, value); + case OPT_TRUSTED_USER_CA_STORE: + ret = wolfSSHD_ConfigSetUserCAStore(*conf, value); + break; + case OPT_WIN_USER_STORES: + ret = wolfSSHD_ConfigSetWinUserStores(*conf, value); + break; + case OPT_WIN_USER_DW_FLAGS: + ret = wolfSSHD_ConfigSetWinUserDwFlags(*conf, value); + break; + case OPT_WIN_USER_PV_PARA: + ret = wolfSSHD_ConfigSetWinUserPvPara(*conf, value); break; case OPT_AUTHORIZED_UPN_DOMAINS: ret = SetListString(&(*conf)->authorizedUPNDomains, full, fullSz, @@ -1711,6 +1737,119 @@ int wolfSSHD_ConfigSetSystemCA(WOLFSSHD_CONFIG* conf, const char* value) return ret; } +/* getter function for if using user CA store + * return 1 if true and 0 if false */ +int wolfSSHD_ConfigGetUserCAStore(const WOLFSSHD_CONFIG* conf) +{ + if (conf != NULL) { + return conf->useUserCAStore; + } + return 0; +} + + +/* setter function for if using user CA store + * 'yes' if true and 'no' if false + * returns WS_SUCCESS on success */ +int wolfSSHD_ConfigSetUserCAStore(WOLFSSHD_CONFIG* conf, const char* value) +{ + int ret = WS_SUCCESS; + + if (conf != NULL) { + if (WSTRCMP(value, "yes") == 0) { + wolfSSH_Log(WS_LOG_INFO, "[SSHD] User CA store enabled. Note this " + "is currently only supported on Windows."); + conf->useUserCAStore = 1; + } + else if (WSTRCMP(value, "no") == 0) { + wolfSSH_Log(WS_LOG_INFO, "[SSHD] User CA store disabled"); + conf->useUserCAStore = 0; + } + else { + wolfSSH_Log(WS_LOG_INFO, "[SSHD] User CA store unexpected flag"); + ret = WS_FATAL_ERROR; + } + } + + return ret; +} + +char* wolfSSHD_ConfigGetWinUserStores(WOLFSSHD_CONFIG* conf) { + if (conf != NULL) { + if (conf->winUserStores == NULL) { + /* If no value was specified, default to CERT_STORE_PROV_SYSTEM */ + CreateString(&conf->winUserStores, "CERT_STORE_PROV_SYSTEM", + (int)WSTRLEN("CERT_STORE_PROV_SYSTEM"), conf->heap); + } + + return conf->winUserStores; + } + + return NULL; +} + +int wolfSSHD_ConfigSetWinUserStores(WOLFSSHD_CONFIG* conf, const char* value) { + int ret = WS_SUCCESS; + + if (conf == NULL) { + ret = WS_BAD_ARGUMENT; + } + + ret = CreateString(&conf->winUserStores, value, (int)WSTRLEN(value), conf->heap); + + return ret; +} + +char* wolfSSHD_ConfigGetWinUserDwFlags(WOLFSSHD_CONFIG* conf) { + if (conf != NULL) { + if (conf->winUserDwFlags == NULL) { + /* If no value was specified, default to CERT_SYSTEM_STORE_CURRENT_USER */ + CreateString(&conf->winUserDwFlags, "CERT_SYSTEM_STORE_CURRENT_USER", + (int)WSTRLEN("CERT_SYSTEM_STORE_CURRENT_USER"), conf->heap); + } + + return conf->winUserDwFlags; + } + + return NULL; +} + +int wolfSSHD_ConfigSetWinUserDwFlags(WOLFSSHD_CONFIG* conf, const char* value) { + int ret = WS_SUCCESS; + + if (conf == NULL) { + ret = WS_BAD_ARGUMENT; + } + + ret = CreateString(&conf->winUserDwFlags, value, (int)WSTRLEN(value), conf->heap); + + return ret; +} + +char* wolfSSHD_ConfigGetWinUserPvPara(WOLFSSHD_CONFIG* conf) { + if (conf != NULL) { + if (conf->winUserPvPara == NULL) { + /* If no value was specified, default to MY */ + CreateString(&conf->winUserPvPara, "MY", (int)WSTRLEN("MY"), conf->heap); + } + + return conf->winUserPvPara; + } + + return NULL; +} + +int wolfSSHD_ConfigSetWinUserPvPara(WOLFSSHD_CONFIG* conf, const char* value) { + int ret = WS_SUCCESS; + + if (conf == NULL) { + ret = WS_BAD_ARGUMENT; + } + + ret = CreateString(&conf->winUserPvPara, value, (int)WSTRLEN(value), conf->heap); + + return ret; +} char* wolfSSHD_ConfigGetUserCAKeysFile(const WOLFSSHD_CONFIG* conf) { diff --git a/apps/wolfsshd/configuration.h b/apps/wolfsshd/configuration.h index c15c5b2dc..71cd9c263 100644 --- a/apps/wolfsshd/configuration.h +++ b/apps/wolfsshd/configuration.h @@ -66,6 +66,14 @@ int wolfSSHD_ConfigSetHostKeyFile(WOLFSSHD_CONFIG* conf, const char* file); int wolfSSHD_ConfigSetHostCertFile(WOLFSSHD_CONFIG* conf, const char* file); int wolfSSHD_ConfigSetSystemCA(WOLFSSHD_CONFIG* conf, const char* value); int wolfSSHD_ConfigGetSystemCA(const WOLFSSHD_CONFIG* conf); +int wolfSSHD_ConfigSetUserCAStore(WOLFSSHD_CONFIG* conf, const char* value); +int wolfSSHD_ConfigGetUserCAStore(const WOLFSSHD_CONFIG* conf); +char* wolfSSHD_ConfigGetWinUserStores(WOLFSSHD_CONFIG* conf); +int wolfSSHD_ConfigSetWinUserStores(WOLFSSHD_CONFIG* conf, const char* value); +char* wolfSSHD_ConfigGetWinUserDwFlags(WOLFSSHD_CONFIG* conf); +int wolfSSHD_ConfigSetWinUserDwFlags(WOLFSSHD_CONFIG* conf, const char* value); +char* wolfSSHD_ConfigGetWinUserPvPara(WOLFSSHD_CONFIG* conf); +int wolfSSHD_ConfigSetWinUserPvPara(WOLFSSHD_CONFIG* conf, const char* value); int wolfSSHD_ConfigSetUserCAKeysFile(WOLFSSHD_CONFIG* conf, const char* file); word16 wolfSSHD_ConfigGetPort(const WOLFSSHD_CONFIG* conf); char* wolfSSHD_ConfigGetAuthKeysFile(const WOLFSSHD_CONFIG* conf); diff --git a/apps/wolfsshd/wolfsshd.c b/apps/wolfsshd/wolfsshd.c index d1085af64..cd95fbdec 100644 --- a/apps/wolfsshd/wolfsshd.c +++ b/apps/wolfsshd/wolfsshd.c @@ -526,9 +526,10 @@ static int SetupCTX(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX** ctx, #endif /* WOLFSSH_OSSH_CERTS || WOLFSSH_CERTS */ #ifdef WOLFSSH_CERTS - /* check if loading in system CA certs */ + /* check if loading in system and/or user CA certs */ #ifdef WOLFSSL_SYS_CA_CERTS - if (ret == WS_SUCCESS && wolfSSHD_ConfigGetSystemCA(conf)) { + if (ret == WS_SUCCESS && (wolfSSHD_ConfigGetSystemCA(conf) + || wolfSSHD_ConfigGetUserCAStore(conf))) { WOLFSSL_CTX* sslCtx; wolfSSH_Log(WS_LOG_INFO, "[SSHD] Using system CAs"); @@ -539,9 +540,23 @@ static int SetupCTX(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX** ctx, } if (ret == WS_SUCCESS) { - if (wolfSSL_CTX_load_system_CA_certs(sslCtx) != WOLFSSL_SUCCESS) { - wolfSSH_Log(WS_LOG_INFO, "[SSHD] Issue loading system CAs"); - ret = WS_FATAL_ERROR; + if (wolfSSHD_ConfigGetSystemCA(conf)) { + if (wolfSSL_CTX_load_system_CA_certs(sslCtx) != WOLFSSL_SUCCESS) { + wolfSSH_Log(WS_LOG_INFO, "[SSHD] Issue loading system CAs"); + ret = WS_FATAL_ERROR; + } + } + } + + if (ret == WS_SUCCESS) { + if (wolfSSHD_ConfigGetUserCAStore(conf)) { + if (wolfSSL_CTX_load_windows_user_CA_certs(sslCtx, + wolfSSHD_ConfigGetWinUserStores(conf), + wolfSSHD_ConfigGetWinUserDwFlags(conf), + wolfSSHD_ConfigGetWinUserPvPara(conf)) != WOLFSSL_SUCCESS) { + wolfSSH_Log(WS_LOG_INFO, "[SSHD] Issue loading user CAs"); + ret = WS_FATAL_ERROR; + } } } From febb79a37cdba2bd597c680c5ab59699a9b6ff15 Mon Sep 17 00:00:00 2001 From: Kareem Date: Fri, 15 Nov 2024 16:02:42 -0700 Subject: [PATCH 04/10] Prefix wolfSSH specific options with wolfSSH_. --- apps/wolfsshd/configuration.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/wolfsshd/configuration.c b/apps/wolfsshd/configuration.c index 531bfd139..9127ff043 100644 --- a/apps/wolfsshd/configuration.c +++ b/apps/wolfsshd/configuration.c @@ -467,14 +467,14 @@ static const CONFIG_OPTION options[NUM_OPTIONS] = { {OPT_FORCE_CMD, "ForceCommand"}, {OPT_HOST_CERT, "HostCertificate"}, {OPT_TRUSTED_USER_CA_KEYS, "TrustedUserCAKeys"}, - {OPT_TRUSTED_SYSTEM_CA_KEYS, "TrustedSystemCAKeys"}, {OPT_PIDFILE, "PidFile"}, {OPT_BANNER, "Banner"}, {OPT_STRICT_MODES, "StrictModes"}, - {OPT_TRUSTED_USER_CA_STORE, "TrustedUserCaStore"}, - {OPT_WIN_USER_STORES, "WinUserStores"}, - {OPT_WIN_USER_DW_FLAGS, "WinUserDwFlags"}, - {OPT_WIN_USER_PV_PARA, "WinUserPvPara"}, + {OPT_TRUSTED_SYSTEM_CA_KEYS, "wolfSSH_TrustedSystemCAKeys"}, + {OPT_TRUSTED_USER_CA_STORE, "wolfSSH_TrustedUserCaStore"}, + {OPT_WIN_USER_STORES, "wolfSSH_WinUserStores"}, + {OPT_WIN_USER_DW_FLAGS, "wolfSSH_WinUserDwFlags"}, + {OPT_WIN_USER_PV_PARA, "wolfSSH_WinUserPvPara"}, {OPT_AUTHORIZED_UPN_DOMAINS, "AuthorizedUPNDomains"}, }; From 1577ea0fff22c8b0568458c08ccb32d314febf2f Mon Sep 17 00:00:00 2001 From: JacobBarthelmeh Date: Mon, 9 Feb 2026 15:27:04 -0700 Subject: [PATCH 05/10] add Windows cert store use with signing and add example arguments add Windows cert store test case make windows cert feature default disabled and simplify macro guard additional unit tests, advertise x509 and pubkey, use CN to match username, build check for WOLFSSL_SYS_CA_CERTS, fix for CM ref count additional build test, uniform enum name, fail on unkown cert store ecc curve, tie in of loading whole cert store for sys CA's --- .github/workflows/windows-cert-store-test.yml | 731 ++++++++++++++++ apps/wolfsshd/auth.c | 23 +- apps/wolfsshd/configuration.c | 200 ++++- apps/wolfsshd/configuration.h | 7 + apps/wolfsshd/wolfsshd.c | 474 ++++++++--- configure.ac | 14 + examples/client/common.c | 98 ++- examples/client/common.h | 5 + examples/echoserver/echoserver.c | 77 +- examples/sftpclient/sftpclient.c | 111 ++- ide/winvs/api-test/api-test.vcxproj | 18 +- ide/winvs/client/client.vcxproj | 16 +- ide/winvs/echoserver/echoserver.vcxproj | 16 +- ide/winvs/unit-test/unit-test.vcxproj | 18 +- .../wolfsftp-client/wolfsftp-client.vcxproj | 16 +- ide/winvs/wolfssh/wolfssh.vcxproj | 8 +- ide/winvs/wolfsshd/wolfsshd.vcxproj | 6 +- src/certman.c | 133 ++- src/internal.c | 797 ++++++++++++++++-- src/ssh.c | 379 +++++++++ tests/unit.c | 204 ++++- wolfssh/certman.h | 9 + wolfssh/internal.h | 28 + wolfssh/ssh.h | 10 + 24 files changed, 3107 insertions(+), 291 deletions(-) create mode 100644 .github/workflows/windows-cert-store-test.yml diff --git a/.github/workflows/windows-cert-store-test.yml b/.github/workflows/windows-cert-store-test.yml new file mode 100644 index 000000000..eb4fcb84f --- /dev/null +++ b/.github/workflows/windows-cert-store-test.yml @@ -0,0 +1,731 @@ +name: Windows Certificate Store Test + +# Tests MS Certificate Store integration for wolfSSH. The matrix covers +# server host keys and client user keys coming from the cert store, from +# X.509 cert/key files, or both, plus an ECDSA cert store host key. +# +# Test flow per matrix entry: +# 1. Create testuser client cert (renewcerts.sh) and, for store cases, +# import/create certificates in the Windows certificate store. +# 2. If the server key comes from the store: run echoserver with -W and +# connect with the SFTP client. +# 3. Run wolfsshd as a Windows service and connect with the SFTP client. + +on: + push: + branches: [ 'master', 'main', 'release/**' ] + pull_request: + branches: [ '*' ] + +env: + WOLFSSL_SOLUTION_FILE_PATH: wolfssl64.sln + SOLUTION_FILE_PATH: wolfssh.sln + USER_SETTINGS_H_NEW: wolfssh/ide/winvs/user_settings.h + USER_SETTINGS_H: wolfssl/IDE/WIN/user_settings.h + INCLUDE_DIR: wolfssh + WOLFSSL_BUILD_CONFIGURATION: Release + WOLFSSH_BUILD_CONFIGURATION: Release + BUILD_PLATFORM: x64 + TARGET_PLATFORM: 10 + TEST_PORT: 22222 + +jobs: + build: + runs-on: windows-latest + + steps: + - uses: actions/checkout@v4 + with: + repository: wolfssl/wolfssl + path: wolfssl + + - uses: actions/checkout@v4 + with: + path: wolfssh + + - name: Add MSBuild to PATH + uses: microsoft/setup-msbuild@v1 + + - name: Restore wolfSSL NuGet packages + working-directory: ${{ github.workspace }}\wolfssl + run: nuget restore ${{env.WOLFSSL_SOLUTION_FILE_PATH}} + + - name: updated user_settings.h for sshd and x509 + working-directory: ${{ github.workspace }} + shell: bash + run: | + # Enable SSHD, SFTP, and X509 support (including WOLFSSH_NO_FPKI) + sed -i 's/#if 0/#if 1/g' ${{env.USER_SETTINGS_H_NEW}} + # Enable the Windows cert store API (not in the repo user_settings.h). + # Appended to wolfssh/ide/winvs/user_settings.h, which the VS projects + # put on the include path before wolfssl/IDE/WIN. + printf '\n/* Appended by windows-cert-store-test CI */\n#define WOLFSSH_WINDOWS_CERT_STORE\n' >> ${{env.USER_SETTINGS_H_NEW}} + cp ${{env.USER_SETTINGS_H_NEW}} ${{env.USER_SETTINGS_H}} + + - name: Build wolfssl library + working-directory: ${{ github.workspace }}\wolfssl + run: msbuild /m /p:PlatformToolset=v142 /p:Platform=${{env.BUILD_PLATFORM}} /p:Configuration=${{env.WOLFSSL_BUILD_CONFIGURATION}} /t:wolfssl ${{env.WOLFSSL_SOLUTION_FILE_PATH}} + + - name: Upload wolfSSL build artifacts + uses: actions/upload-artifact@v4 + with: + name: wolfssl-windows-build + if-no-files-found: warn + retention-days: 1 + path: | + wolfssl/IDE/WIN/${{env.WOLFSSL_BUILD_CONFIGURATION}}/${{env.BUILD_PLATFORM}}/** + wolfssl/IDE/WIN/${{env.WOLFSSL_BUILD_CONFIGURATION}}/** + wolfssl/${{env.WOLFSSL_BUILD_CONFIGURATION}}/${{env.BUILD_PLATFORM}}/** + wolfssl/${{env.WOLFSSL_BUILD_CONFIGURATION}}/** + + - name: Restore NuGet packages + working-directory: ${{ github.workspace }}\wolfssh\ide\winvs + run: nuget restore ${{env.SOLUTION_FILE_PATH}} + + - name: Build wolfssh + working-directory: ${{ github.workspace }}\wolfssh\ide\winvs + run: msbuild /m /p:PlatformToolset=v142 /p:Platform=${{env.BUILD_PLATFORM}} /p:WindowsTargetPlatformVersion=${{env.TARGET_PLATFORM}} /p:Configuration=${{env.WOLFSSH_BUILD_CONFIGURATION}} ${{env.SOLUTION_FILE_PATH}} + + - name: Upload wolfSSH build artifacts + uses: actions/upload-artifact@v4 + with: + name: wolfssh-windows-build + if-no-files-found: error + path: | + wolfssh/ide/winvs/**/Release/** + + # Compile-only check of the WOLFSSL_SYS_CA_CERTS paths in wolfsshd, which + # the functional matrix never defines and so never builds. + build-sys-ca-certs: + runs-on: windows-latest + + steps: + - uses: actions/checkout@v4 + with: + repository: wolfssl/wolfssl + path: wolfssl + + - uses: actions/checkout@v4 + with: + path: wolfssh + + - name: Add MSBuild to PATH + uses: microsoft/setup-msbuild@v1 + + - name: Restore wolfSSL NuGet packages + working-directory: ${{ github.workspace }}\wolfssl + run: nuget restore ${{env.WOLFSSL_SOLUTION_FILE_PATH}} + + - name: user_settings.h with sshd, x509, cert store, and system CA certs + working-directory: ${{ github.workspace }} + shell: bash + run: | + sed -i 's/#if 0/#if 1/g' ${{env.USER_SETTINGS_H_NEW}} + printf '\n#define WOLFSSH_WINDOWS_CERT_STORE\n#define WOLFSSL_SYS_CA_CERTS\n' >> ${{env.USER_SETTINGS_H_NEW}} + cp ${{env.USER_SETTINGS_H_NEW}} ${{env.USER_SETTINGS_H}} + + - name: Build wolfssl library + working-directory: ${{ github.workspace }}\wolfssl + run: msbuild /m /p:PlatformToolset=v142 /p:Platform=${{env.BUILD_PLATFORM}} /p:Configuration=${{env.WOLFSSL_BUILD_CONFIGURATION}} /t:wolfssl ${{env.WOLFSSL_SOLUTION_FILE_PATH}} + + - name: Restore NuGet packages + working-directory: ${{ github.workspace }}\wolfssh\ide\winvs + run: nuget restore ${{env.SOLUTION_FILE_PATH}} + + - name: Build wolfssh (compile check) + working-directory: ${{ github.workspace }}\wolfssh\ide\winvs + run: msbuild /m /p:PlatformToolset=v142 /p:Platform=${{env.BUILD_PLATFORM}} /p:WindowsTargetPlatformVersion=${{env.TARGET_PLATFORM}} /p:Configuration=${{env.WOLFSSH_BUILD_CONFIGURATION}} ${{env.SOLUTION_FILE_PATH}} + + test: + needs: build + runs-on: windows-latest + strategy: + fail-fast: false + matrix: + include: + - server_key_source: file + client_key_source: x509 + key_algorithm: rsa + test_name: "Server-File-Client-X509" + - server_key_source: store + client_key_source: x509 + key_algorithm: rsa + test_name: "Server-Store-Client-X509" + - server_key_source: file + client_key_source: store + key_algorithm: rsa + test_name: "Server-File-Client-Store" + - server_key_source: store + client_key_source: store + key_algorithm: rsa + test_name: "Server-Store-Client-Store" + - server_key_source: store + client_key_source: x509 + key_algorithm: ecdsa + test_name: "Server-Store-Client-X509-ECDSA" + + steps: + - uses: actions/checkout@v4 + with: + path: wolfssh + + - name: Download wolfSSH build artifacts + uses: actions/download-artifact@v4 + with: + name: wolfssh-windows-build + path: . + + - name: Download wolfSSL build artifacts + uses: actions/download-artifact@v4 + with: + name: wolfssl-windows-build + path: . + + - name: Create testuser client certificate - ${{ matrix.test_name }} + working-directory: ${{ github.workspace }}\wolfssh + shell: bash + env: + # Disable MSYS path conversion - Git Bash converts /C=US/... to C:/Program Files/Git/C=US/... + MSYS_NO_PATHCONV: 1 + MSYS2_ARG_CONV_EXCL: "*" + run: | + # Create an X509 certificate for testuser, signed by the test CA, + # using renewcerts.sh (like sshd_x509_test.sh does). Used directly + # for x509 clients and imported into the store for store clients. + cd keys + bash renewcerts.sh testuser + cd .. + + if [[ ! -f "keys/testuser-cert.der" || ! -f "keys/testuser-key.der" ]]; then + echo "ERROR: renewcerts.sh did not create testuser-cert.der/testuser-key.der" + ls -la keys/ + exit 1 + fi + echo "CLIENT_CERT_FILE=keys/testuser-cert.der" >> $GITHUB_ENV + echo "CLIENT_KEY_FILE=keys/testuser-key.der" >> $GITHUB_ENV + + - name: Set up cert store certificates + working-directory: ${{ github.workspace }}\wolfssh + shell: pwsh + run: | + # Server host key: self-signed cert in LocalMachine\My so the + # wolfsshd service (LocalSystem) can access it. + if ("${{ matrix.server_key_source }}" -eq "store") { + if ("${{ matrix.key_algorithm }}" -eq "ecdsa") { + $serverCert = New-SelfSignedCertificate ` + -Subject "CN=wolfSSH-Test-Server" ` + -KeyAlgorithm ECDSA_nistP256 ` + -CertStoreLocation "Cert:\LocalMachine\My" ` + -KeyExportPolicy Exportable ` + -NotAfter (Get-Date).AddYears(1) ` + -KeyUsage DigitalSignature + } else { + $serverCert = New-SelfSignedCertificate ` + -Subject "CN=wolfSSH-Test-Server" ` + -KeyAlgorithm RSA ` + -KeyLength 2048 ` + -CertStoreLocation "Cert:\LocalMachine\My" ` + -KeyExportPolicy Exportable ` + -NotAfter (Get-Date).AddYears(1) ` + -KeyUsage DigitalSignature, KeyEncipherment + } + Write-Host "Server cert created: $($serverCert.Subject) ($($serverCert.Thumbprint))" + + # Grant LocalSystem access to the private key file. Required for + # the wolfsshd service running as LocalSystem; without this, + # CryptAcquireCertificatePrivateKey fails. + if ("${{ matrix.key_algorithm }}" -eq "ecdsa") { + $privKey = [System.Security.Cryptography.X509Certificates.ECDsaCertificateExtensions]::GetECDsaPrivateKey($serverCert) + } else { + $privKey = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($serverCert) + } + $keyName = $privKey.Key.UniqueName + $keyFile = @( + "$env:ProgramData\Microsoft\Crypto\Keys\$keyName", + "$env:ProgramData\Microsoft\Crypto\RSA\MachineKeys\$keyName", + "$env:ProgramData\Microsoft\Crypto\SystemKeys\$keyName" + ) | Where-Object { Test-Path $_ } | Select-Object -First 1 + if (-not $keyFile) { + Write-Host "ERROR: Private key file not found for $keyName" + exit 1 + } + $acl = Get-Acl $keyFile + $rule = New-Object System.Security.AccessControl.FileSystemAccessRule ` + "NT AUTHORITY\SYSTEM", "FullControl", "Allow" + $acl.SetAccessRule($rule) + Set-Acl $keyFile $acl + Write-Host "Granted SYSTEM FullControl on private key: $keyFile" + + # Export the CN (without "CN=") for HostKeyStoreSubject + $subject = $serverCert.Subject + if ($subject -match "^CN=(.+)$") { $subject = $matches[1] } + Add-Content -Path $env:GITHUB_ENV -Value "SERVER_CERT_SUBJECT=$subject" + } + + # Client user key: import the CA-signed testuser cert+key into + # CurrentUser\My (via PFX; openssl converts the DER files). + if ("${{ matrix.client_key_source }}" -eq "store") { + $userCertPath = (Resolve-Path $env:CLIENT_CERT_FILE).Path + $userKeyPath = (Resolve-Path $env:CLIENT_KEY_FILE).Path + $userCertPem = Join-Path $env:TEMP "testuser-cert.pem" + $userKeyPem = Join-Path $env:TEMP "testuser-key.pem" + $pfxPath = Join-Path $env:TEMP "testuser-client.pfx" + $pfxPassword = "TempP@ss123" + + & openssl x509 -inform DER -in $userCertPath -out $userCertPem + if ($LASTEXITCODE -ne 0) { Write-Host "ERROR: cert DER to PEM failed"; exit 1 } + & openssl rsa -inform DER -in $userKeyPath -out $userKeyPem 2>$null + if ($LASTEXITCODE -ne 0) { + & openssl ec -inform DER -in $userKeyPath -out $userKeyPem + if ($LASTEXITCODE -ne 0) { Write-Host "ERROR: key DER to PEM failed (tried RSA and ECC)"; exit 1 } + } + & openssl pkcs12 -export -out $pfxPath -inkey $userKeyPem -in $userCertPem -password "pass:$pfxPassword" -nodes + if ($LASTEXITCODE -ne 0) { Write-Host "ERROR: PFX creation failed"; exit 1 } + + Import-PfxCertificate -FilePath $pfxPath -CertStoreLocation "Cert:\CurrentUser\My" ` + -Password (ConvertTo-SecureString -String $pfxPassword -Force -AsPlainText) | Out-Null + Remove-Item -Path $pfxPath, $userCertPem, $userKeyPem -ErrorAction SilentlyContinue + + $importedCert = Get-ChildItem -Path "Cert:\CurrentUser\My" | + Where-Object { $_.Subject -match "testuser" } | Select-Object -First 1 + if (-not $importedCert) { + Write-Host "ERROR: imported testuser cert not found in CurrentUser\My" + exit 1 + } + Write-Host "Client cert imported: $($importedCert.Subject) ($($importedCert.Thumbprint))" + + # Export the CN for the client cert store lookup. The full X.500 + # DN contains commas which break command-line argument parsing. + $cn = $importedCert.Subject + if ($cn -match 'CN=([^,]+)') { $cn = $matches[1].Trim() } + Add-Content -Path $env:GITHUB_ENV -Value "CLIENT_CERT_SUBJECT=$cn" + } + + - name: Create Windows user testuser + shell: pwsh + run: | + $homeDir = "C:\Users\testuser" + $sshDir = "$homeDir\.ssh" + $authKeysFile = "$sshDir\authorized_keys" + # Password: <=14 chars to avoid net user "Windows 2000" prompt; mixed case, number, special. + # This is a test user and not a sensitive password. + $pw = 'T3stP@ss!xY9' + + New-Item -ItemType Directory -Path $homeDir -Force | Out-Null + New-Item -ItemType Directory -Path $sshDir -Force | Out-Null + + # Create local user testuser (net user avoids New-LocalUser password policy issues in CI) + $o = net user testuser $pw /add /homedir:$homeDir 2>&1 + if ($LASTEXITCODE -ne 0) { + if ($o -match "already exists") { + net user testuser /homedir:$homeDir 2>$null + } else { + Write-Host "net user failed: $o" + exit 1 + } + } + + # X509 auth verifies the client cert against the CA; authorized_keys + # is not used but the file should exist. + "" | Out-File -FilePath $authKeysFile -Encoding ASCII -NoNewline + icacls $authKeysFile /grant "testuser:R" /q + + # Set ProfileImagePath so SHGetKnownFolderPath(FOLDERID_Profile) returns $homeDir + # for testuser (GetHomeDirectory in wolfsshd uses that; otherwise it can fail for new users). + $sid = (New-Object System.Security.Principal.NTAccount("testuser")).Translate([System.Security.Principal.SecurityIdentifier]).Value + $profKey = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\$sid" + if (-not (Test-Path $profKey)) { New-Item -Path $profKey -Force | Out-Null } + Set-ItemProperty -Path $profKey -Name "ProfileImagePath" -Value $homeDir -Force + + - name: Create wolfSSHd config file + working-directory: ${{ github.workspace }}\wolfssh + shell: pwsh + run: | + $configContent = @" + Port ${{env.TEST_PORT}} + PasswordAuthentication yes + PermitRootLogin yes + "@ + + # Server verifies client X509 certs against the test CA (PEM format, + # as per apps/wolfsshd/test/create_sshd_config.sh) + $caCertPath = (Resolve-Path "keys\ca-cert-ecc.pem").Path + $configContent += @" + + TrustedUserCAKeys $caCertPath + "@ + + if ("${{ matrix.server_key_source }}" -eq "store") { + # The certificate is part of the store entry; do NOT specify + # HostCertificate separately. + $configContent += @" + + HostKeyStore My + HostKeyStoreSubject $env:SERVER_CERT_SUBJECT + HostKeyStoreFlags LOCAL_MACHINE + "@ + } else { + $keyPath = (Resolve-Path "keys\server-key.pem").Path + $certPath = (Resolve-Path "keys\server-cert.pem").Path + $configContent += @" + + HostKey $keyPath + HostCertificate $certPath + "@ + } + + $configContent | Out-File -FilePath sshd_config_test -Encoding ASCII + Write-Host "=== wolfSSHd Config ===" + Get-Content sshd_config_test + + - name: Find wolfSSH executables + working-directory: ${{ github.workspace }}\wolfssh + shell: pwsh + run: | + $searchRoot = "${{ github.workspace }}" + + $sshdExe = Get-ChildItem -Path $searchRoot -Recurse -Filter "wolfsshd.exe" -ErrorAction SilentlyContinue | + Where-Object { $_.FullName -like "*Release*" -or $_.FullName -like "*Debug*" } | + Select-Object -First 1 + if (-not $sshdExe) { + Write-Host "ERROR: wolfsshd.exe not found" + Get-ChildItem -Path $searchRoot -Recurse -Filter "*.exe" -ErrorAction SilentlyContinue | Select-Object FullName + exit 1 + } + Write-Host "wolfsshd.exe: $($sshdExe.FullName)" + Add-Content -Path $env:GITHUB_ENV -Value "SSHD_PATH=$($sshdExe.FullName)" + + # SFTP client (project name is often wolfsftp-client) + $sftpExe = Get-ChildItem -Path $searchRoot -Recurse -Filter "wolfsftp.exe" -ErrorAction SilentlyContinue | + Where-Object { $_.FullName -like "*Release*" -or $_.FullName -like "*Debug*" } | + Select-Object -First 1 + if (-not $sftpExe) { + $sftpExe = Get-ChildItem -Path $searchRoot -Recurse -Filter "wolfsftp-client.exe" -ErrorAction SilentlyContinue | + Where-Object { $_.FullName -like "*Release*" -or $_.FullName -like "*Debug*" } | + Select-Object -First 1 + } + if (-not $sftpExe) { + Write-Host "ERROR: SFTP client exe not found (wolfsftp.exe or wolfsftp-client.exe)" + Get-ChildItem -Path $searchRoot -Recurse -Filter "*.exe" -ErrorAction SilentlyContinue | Select-Object FullName + exit 1 + } + Write-Host "SFTP client: $($sftpExe.FullName)" + Add-Content -Path $env:GITHUB_ENV -Value "SFTP_PATH=$($sftpExe.FullName)" + + # echoserver (used for the cert store host key test) + $echoserverExe = Get-ChildItem -Path $searchRoot -Recurse -Filter "echoserver.exe" -ErrorAction SilentlyContinue | + Where-Object { $_.FullName -like "*Release*" -or $_.FullName -like "*Debug*" } | + Select-Object -First 1 + if ($echoserverExe) { + Write-Host "echoserver.exe: $($echoserverExe.FullName)" + Add-Content -Path $env:GITHUB_ENV -Value "ECHOSERVER_PATH=$($echoserverExe.FullName)" + } elseif ("${{ matrix.server_key_source }}" -eq "store") { + Write-Host "ERROR: echoserver.exe not found (required for cert store server test)" + exit 1 + } + + - name: Copy wolfSSL DLL to executable directory (if dynamic build) + working-directory: ${{ github.workspace }} + shell: pwsh + run: | + $sshdDir = Split-Path -Parent $env:SSHD_PATH + + # If wolfssl.lib is next to wolfsshd.exe, it's a static build - no DLL needed + if (Test-Path (Join-Path $sshdDir "wolfssl.lib")) { + Write-Host "wolfssl.lib present beside wolfsshd.exe - static build; wolfssl.dll not required" + exit 0 + } + + $wolfsslDll = Get-ChildItem -Path "${{ github.workspace }}\wolfssl" -Recurse -Filter "wolfssl.dll" -ErrorAction SilentlyContinue | + Select-Object -First 1 + if ($wolfsslDll) { + Copy-Item -Path $wolfsslDll.FullName -Destination (Join-Path $sshdDir "wolfssl.dll") -Force + Write-Host "Copied wolfssl.dll to $sshdDir" + } else { + Write-Host "wolfssl.dll not found; if build is static (wolfssl.lib in output), this is OK" + } + + - name: Grant service (LocalSystem) access to config, keys, and executable + working-directory: ${{ github.workspace }}\wolfssh + shell: pwsh + run: | + # wolfsshd runs as LocalSystem; it must be able to read the config + # and key files and run the exe (and load wolfssl.dll if dynamic). + # /T = apply to existing files and subdirs; (OI)(CI) = inherit to new objects + $wolfsshRoot = (Get-Location).Path + icacls $wolfsshRoot /grant "NT AUTHORITY\SYSTEM:(OI)(CI)RX" /T /q + if ($LASTEXITCODE -ne 0) { + Write-Host "ERROR: icacls failed on $wolfsshRoot" + exit 1 + } + $sshdDir = (Resolve-Path (Split-Path -Parent $env:SSHD_PATH)).Path + icacls $sshdDir /grant "NT AUTHORITY\SYSTEM:(OI)(CI)RX" /T /q + + - name: Start echoserver with cert store host key + if: matrix.server_key_source == 'store' + working-directory: ${{ github.workspace }}\wolfssh + shell: pwsh + run: | + # Exercise the cert store host key (-W Store:Subject:Location) in the + # echoserver before the wolfsshd service test. Start it detached (via + # cmd start /B) so it survives after this step ends. + $echoserverPath = $env:ECHOSERVER_PATH + $exeDir = Split-Path -Parent $echoserverPath + $port = ${{env.TEST_PORT}} + $spec = "My:wolfSSH-Test-Server:LOCAL_MACHINE" + $wolfsshRoot = "${{ github.workspace }}\wolfssh" + + # -a : verify client X.509 certs + # -K testuser:: register testuser with the auth callback + $caCertPem = Join-Path $wolfsshRoot "keys\ca-cert-ecc.pem" + $clientCert = (Resolve-Path (Join-Path $wolfsshRoot $env:CLIENT_CERT_FILE)).Path + $echoArgs = @("-W", $spec, "-p", $port, "-a", $caCertPem, "-K", "testuser:$clientCert") + + $argStr = $echoArgs -join " " + $echoLogFile = Join-Path $wolfsshRoot "echoserver_debug.log" + Add-Content -Path $env:GITHUB_ENV -Value "ECHOSERVER_LOG=$echoLogFile" + Write-Host "Command: $echoserverPath $argStr" + $cmdLine = "`"$echoserverPath`" $argStr > `"$echoLogFile`" 2>&1" + Start-Process -FilePath "cmd.exe" ` + -ArgumentList "/c", "start", "/B", "cmd", "/c", $cmdLine ` + -WorkingDirectory $exeDir -NoNewWindow -Wait:$false + Start-Sleep -Seconds 2 + $proc = Get-Process -Name "echoserver" -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($proc) { + Add-Content -Path $env:GITHUB_ENV -Value "ECHOSERVER_PID=$($proc.Id)" + Write-Host "echoserver started with PID $($proc.Id)" + } + + # Wait for the port to be listening + $timeout = 15 + $elapsed = 0 + while ($elapsed -lt $timeout) { + Start-Sleep -Seconds 1 + $elapsed++ + try { + $conn = New-Object System.Net.Sockets.TcpClient("127.0.0.1", $port) + if ($conn.Connected) { $conn.Close(); break } + } catch {} + if (-not (Get-Process -Name "echoserver" -ErrorAction SilentlyContinue)) { + Write-Host "ERROR: echoserver exited before port was ready" + if (Test-Path $echoLogFile) { Get-Content $echoLogFile } + exit 1 + } + } + if ($elapsed -ge $timeout) { + Write-Host "ERROR: Port $port not listening after ${timeout}s" + if (Test-Path $echoLogFile) { Get-Content $echoLogFile } + exit 1 + } + Write-Host "echoserver is listening on port $port" + + - name: Test SFTP against echoserver (cert store host key) + if: matrix.server_key_source == 'store' + working-directory: ${{ github.workspace }}\wolfssh + shell: pwsh + run: | + $testPort = ${{env.TEST_PORT}} + $sftpPath = $env:SFTP_PATH + + @" + pwd + ls + quit + "@ | Out-File -FilePath sftp_echo_commands.txt -Encoding ASCII + + $sftpArgs = @("-u", "testuser", "-h", "localhost", "-p", "$testPort") + $caCertDer = (Resolve-Path "keys\ca-cert-ecc.der").Path + if ("${{ matrix.client_key_source }}" -eq "store") { + $sftpArgs += "-W", "My:$($env:CLIENT_CERT_SUBJECT):CURRENT_USER" + } else { + $sftpArgs += "-J", (Resolve-Path $env:CLIENT_CERT_FILE).Path + $sftpArgs += "-i", (Resolve-Path $env:CLIENT_KEY_FILE).Path + } + # -A: CA cert for host verification; -X: ignore peer IP vs cert checks + $sftpArgs += "-A", $caCertDer, "-X" + + Write-Host "Running: $sftpPath $($sftpArgs -join ' ')" + $process = Start-Process -FilePath $sftpPath ` + -ArgumentList $sftpArgs ` + -RedirectStandardInput "sftp_echo_commands.txt" ` + -RedirectStandardOutput "sftp_echo_output.txt" ` + -RedirectStandardError "sftp_echo_error.txt" ` + -Wait -NoNewWindow -PassThru + + Write-Host "SFTP (echoserver) exit code: $($process.ExitCode)" + Write-Host "=== SFTP Output ===" + if (Test-Path sftp_echo_output.txt) { Get-Content sftp_echo_output.txt } + Write-Host "=== SFTP Error ===" + if (Test-Path sftp_echo_error.txt) { Get-Content sftp_echo_error.txt } + + if ($process.ExitCode -ne 0) { + $echoLog = $env:ECHOSERVER_LOG + if (-not [string]::IsNullOrEmpty($echoLog) -and (Test-Path $echoLog)) { + Write-Host "=== Echoserver Log ===" + Get-Content $echoLog + } + Write-Host "ERROR: SFTP against echoserver failed" + exit 1 + } + Write-Host "SFTP against echoserver succeeded" + + - name: Stop echoserver before wolfsshd test + if: matrix.server_key_source == 'store' + shell: pwsh + run: | + $echoserverPid = $env:ECHOSERVER_PID + if (-not [string]::IsNullOrEmpty($echoserverPid)) { + Stop-Process -Id $echoserverPid -Force -ErrorAction SilentlyContinue + Start-Sleep -Seconds 2 + } + # Also kill by name in case PID tracking missed it + Get-Process -Name "echoserver" -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue + # Clear the env var so cleanup step doesn't try again + Add-Content -Path $env:GITHUB_ENV -Value "ECHOSERVER_PID=" + + - name: Start wolfSSHd as Windows service + working-directory: ${{ github.workspace }}\wolfssh + shell: pwsh + run: | + $sshdPathFull = (Resolve-Path $env:SSHD_PATH).Path + $configPathFull = (Resolve-Path "sshd_config_test").Path + $serviceName = "wolfsshd" + + # Remove service if it already exists + $existingService = Get-Service -Name $serviceName -ErrorAction SilentlyContinue + if ($existingService) { + if ($existingService.Status -eq 'Running') { + Stop-Service -Name $serviceName -Force -ErrorAction SilentlyContinue + Start-Sleep -Seconds 2 + } + sc.exe delete $serviceName | Out-Null + Start-Sleep -Seconds 2 + } + + # We do NOT include -E here because LocalSystem only has RX on + # the wolfssh directory and cannot create a log file. Debug output + # from the service goes to OutputDebugString. + $binPath = "`"$sshdPathFull`" -f `"$configPathFull`" -p ${{env.TEST_PORT}}" + Write-Host "Creating service with binpath: $binPath" + $createResult = sc.exe create $serviceName binPath= $binPath + if ($LASTEXITCODE -ne 0) { + Write-Host "ERROR: Failed to create service" + Write-Host $createResult + exit 1 + } + + $startResult = sc.exe start $serviceName + if ($LASTEXITCODE -ne 0) { + Write-Host "ERROR: Failed to start service" + Write-Host $startResult + sc.exe query $serviceName + exit 1 + } + + Start-Sleep -Seconds 5 + $service = Get-Service -Name $serviceName -ErrorAction SilentlyContinue + if (-not $service -or $service.Status -ne 'Running') { + Write-Host "ERROR: Service is not running. Status: $($service.Status)" + sc.exe query $serviceName + Get-WinEvent -FilterHashtable @{LogName='System'; ProviderName='Service Control Manager'} -MaxEvents 20 -ErrorAction SilentlyContinue | + Where-Object { $_.Message -like "*$serviceName*" } | + Select-Object TimeCreated, LevelDisplayName, Message | Format-List + exit 1 + } + + Write-Host "wolfSSHd service is running" + Add-Content -Path $env:GITHUB_ENV -Value "SSHD_SERVICE_NAME=$serviceName" + + - name: Test SFTP connection against wolfsshd + working-directory: ${{ github.workspace }}\wolfssh + shell: pwsh + run: | + $testPort = ${{env.TEST_PORT}} + $sftpPath = $env:SFTP_PATH + + # Verify the server is listening before running the client + try { + $tcpClient = New-Object System.Net.Sockets.TcpClient + $connect = $tcpClient.BeginConnect("localhost", $testPort, $null, $null) + $wait = $connect.AsyncWaitHandle.WaitOne(3000, $false) + if ($wait) { + $tcpClient.EndConnect($connect) + $tcpClient.Close() + } else { + Write-Host "ERROR: TCP connection timeout - server may not be listening on port $testPort" + exit 1 + } + } catch { + Write-Host "ERROR: TCP connection failed: $_" + exit 1 + } + + @" + pwd + ls + quit + "@ | Out-File -FilePath sftp_commands.txt -Encoding ASCII + + $sftpArgs = @("-u", "testuser", "-h", "localhost", "-p", "$testPort") + $caCertDer = (Resolve-Path "keys\ca-cert-ecc.der").Path + if ("${{ matrix.client_key_source }}" -eq "store") { + $sftpArgs += "-W", "My:$($env:CLIENT_CERT_SUBJECT):CURRENT_USER" + } else { + $sftpArgs += "-J", (Resolve-Path $env:CLIENT_CERT_FILE).Path + $sftpArgs += "-i", (Resolve-Path $env:CLIENT_KEY_FILE).Path + } + # -A: CA cert for host verification; -X: ignore peer IP vs cert checks + $sftpArgs += "-A", $caCertDer, "-X" + + Write-Host "Running: $sftpPath $($sftpArgs -join ' ')" + Write-Host "Test matrix: server=${{ matrix.server_key_source }}, client=${{ matrix.client_key_source }}" + $process = Start-Process -FilePath $sftpPath ` + -ArgumentList $sftpArgs ` + -RedirectStandardInput "sftp_commands.txt" ` + -RedirectStandardOutput "sftp_output.txt" ` + -RedirectStandardError "sftp_error.txt" ` + -Wait -NoNewWindow -PassThru + + Write-Host "SFTP exit code: $($process.ExitCode)" + Write-Host "=== SFTP Output ===" + if (Test-Path sftp_output.txt) { Get-Content sftp_output.txt } + Write-Host "=== SFTP Error ===" + if (Test-Path sftp_error.txt) { Get-Content sftp_error.txt } + + if ($process.ExitCode -ne 0) { + Write-Host "ERROR: SFTP client exited with code $($process.ExitCode)" + exit 1 + } + Write-Host "Test completed - key exchange and SFTP connection succeeded" + + - name: Cleanup + if: always() + shell: pwsh + run: | + # Stop echoserver if it is still running + $echoserverPid = $env:ECHOSERVER_PID + if (-not [string]::IsNullOrEmpty($echoserverPid)) { + Stop-Process -Id $echoserverPid -Force -ErrorAction SilentlyContinue + } + Get-Process -Name "echoserver" -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue + + # Stop and remove wolfSSHd service + $serviceName = $env:SSHD_SERVICE_NAME + if ([string]::IsNullOrEmpty($serviceName)) { $serviceName = "wolfsshd" } + $service = Get-Service -Name $serviceName -ErrorAction SilentlyContinue + if ($service) { + if ($service.Status -eq 'Running') { + Stop-Service -Name $serviceName -Force -ErrorAction SilentlyContinue + Start-Sleep -Seconds 2 + } + sc.exe delete $serviceName | Out-Null + } + + # Remove test certificates from the stores + Get-ChildItem -Path "Cert:\CurrentUser\My" | Where-Object { + $_.Subject -like "*wolfSSH-Test*" -or $_.Subject -like "*testuser*" + } | Remove-Item -Force -ErrorAction SilentlyContinue + Get-ChildItem -Path "Cert:\LocalMachine\My" | Where-Object { + $_.Subject -like "*wolfSSH-Test*" + } | Remove-Item -Force -ErrorAction SilentlyContinue + Write-Host "Cleaned up test certificates" diff --git a/apps/wolfsshd/auth.c b/apps/wolfsshd/auth.c index 9c5909184..b9baa3e38 100644 --- a/apps/wolfsshd/auth.c +++ b/apps/wolfsshd/auth.c @@ -54,7 +54,9 @@ #include #include -#ifdef WOLFSSL_FPKI +#if defined(WOLFSSL_FPKI) || defined(_WIN32) +/* Used to bind a client certificate to the requested user name: by UPN + * with FPKI, by subject CN on Windows builds without FPKI. */ #include #endif @@ -2083,10 +2085,11 @@ static int RequestAuthentication(WS_UserAuthData* authData, ret = WOLFSSH_USERAUTH_REJECTED; } - #ifdef WOLFSSL_FPKI + #if defined(WOLFSSL_FPKI) || defined(_WIN32) if (ret == WOLFSSH_USERAUTH_SUCCESS && authData->type == WOLFSSH_USERAUTH_PUBLICKEY) { - /* compare user name to UPN in certificate */ + /* Bind the certificate to the requested user name via UPN with FPKI or + * CN without FPKI. */ if (authData->sf.publicKey.isCert) { DecodedCert* dCert; #ifdef WOLFSSH_SMALL_STACK @@ -2111,6 +2114,7 @@ static int RequestAuthentication(WS_UserAuthData* authData, } else { int usrMatch = 0; + #ifdef WOLFSSL_FPKI int upnRealmUnchecked = 0; DNS_entry* current = dCert->altNames; const char* upnDomains = @@ -2139,6 +2143,15 @@ static int RequestAuthentication(WS_UserAuthData* authData, wolfSSH_Log(WS_LOG_WARN, "[SSHD] AuthorizedUPNDomains " "not set; certificate UPN domain is not checked"); } + #else + /* Without FPKI compare subject CN with user name */ + if (dCert->subjectCN != NULL && + (int)XSTRLEN(usr) == dCert->subjectCNLen && + XSTRNCMP(usr, dCert->subjectCN, + (size_t)dCert->subjectCNLen) == 0) { + usrMatch = 1; + } + #endif if (usrMatch == 0) { wolfSSH_Log(WS_LOG_ERROR, "[SSHD] incorrect user cert " @@ -2176,7 +2189,9 @@ static int RequestAuthentication(WS_UserAuthData* authData, } else { #ifdef _WIN32 - /* Still need to get users token on Windows */ + /* The UPN/CN-vs-username check above already bound the + * certificate to the requested user. Still need to get + * the users token on Windows. */ wolfSSH_Log(WS_LOG_INFO, "[SSHD] Relying on CA for public key check"); rc = SetupUserTokenWin(usr, &authData->sf.publicKey, diff --git a/apps/wolfsshd/configuration.c b/apps/wolfsshd/configuration.c index 9127ff043..ebc6322b1 100644 --- a/apps/wolfsshd/configuration.c +++ b/apps/wolfsshd/configuration.c @@ -87,6 +87,11 @@ struct WOLFSSHD_CONFIG { char* hostKeyFile; char* hostCertFile; char* userCAKeysFile; +#ifdef WOLFSSH_WINDOWS_CERT_STORE + char* hostKeyStore; + char* hostKeyStoreSubject; + char* hostKeyStoreFlags; +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ char* hostKeyAlgos; char* kekAlgos; char* listenAddress; @@ -94,9 +99,11 @@ struct WOLFSSHD_CONFIG { char* forceCmd; char* pidFile; char* authorizedUPNDomains; /* allowlist of UPN realms for cert auth */ +#ifdef USE_WINDOWS_API char* winUserStores; char* winUserDwFlags; char* winUserPvPara; +#endif /* USE_WINDOWS_API */ WOLFSSHD_CONFIG* next; /* next config in list */ long loginTimer; word16 port; @@ -389,9 +396,16 @@ void wolfSSHD_ConfigFree(WOLFSSHD_CONFIG* conf) FreeString(¤t->authorizedUPNDomains, heap); FreeString(¤t->usrAppliesTo, heap); FreeString(¤t->groupAppliesTo, heap); - FreeString(¤t->winUserStores, heap); - FreeString(¤t->winUserDwFlags, heap); - FreeString(¤t->winUserPvPara, heap); +#ifdef WOLFSSH_WINDOWS_CERT_STORE + FreeString(¤t->hostKeyStore, heap); + FreeString(¤t->hostKeyStoreSubject, heap); + FreeString(¤t->hostKeyStoreFlags, heap); +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ +#ifdef USE_WINDOWS_API + FreeString(¤t->winUserStores, heap); + FreeString(¤t->winUserDwFlags, heap); + FreeString(¤t->winUserPvPara, heap); +#endif /* USE_WINDOWS_API */ WFREE(current, heap, DYNTYPE_SSHD); current = next; @@ -418,6 +432,9 @@ enum { OPT_PROTOCOL = 9, OPT_LOGIN_GRACE_TIME = 10, OPT_HOST_KEY = 11, + OPT_HOST_KEY_STORE = 50, + OPT_HOST_KEY_STORE_SUBJECT = 51, + OPT_HOST_KEY_STORE_FLAGS = 52, OPT_PASSWORD_AUTH = 12, OPT_PORT = 13, OPT_PERMIT_ROOT = 14, @@ -434,16 +451,14 @@ enum { OPT_STRICT_MODES = 25, OPT_TRUSTED_SYSTEM_CA_KEYS = 26, OPT_TRUSTED_USER_CA_STORE = 27, +#ifdef USE_WINDOWS_API OPT_WIN_USER_STORES = 28, OPT_WIN_USER_DW_FLAGS = 29, OPT_WIN_USER_PV_PARA = 30, +#endif /* USE_WINDOWS_API */ OPT_AUTHORIZED_UPN_DOMAINS = 31 }; -enum { - NUM_OPTIONS = 32 -}; - -static const CONFIG_OPTION options[NUM_OPTIONS] = { +static const CONFIG_OPTION options[] = { {OPT_AUTH_KEYS_FILE, "AuthorizedKeysFile"}, {OPT_PRIV_SEP, "UsePrivilegeSeparation"}, {OPT_PERMIT_EMPTY_PW, "PermitEmptyPasswords"}, @@ -455,6 +470,14 @@ static const CONFIG_OPTION options[NUM_OPTIONS] = { {OPT_ACCEPT_ENV, "AcceptEnv"}, {OPT_PROTOCOL, "Protocol"}, {OPT_LOGIN_GRACE_TIME, "LoginGraceTime"}, + /* The config parser uses strncmp with the option-name length, so longer + * option names that share a common prefix MUST appear before the shorter + * one. HostKeyStoreSubject/HostKeyStoreFlags before HostKeyStore, + * and all HostKeyStore* before HostKey. Kept unconditional so + * "HostKeyStore" never prefix-matches "HostKey" on non-store builds. */ + {OPT_HOST_KEY_STORE_SUBJECT, "HostKeyStoreSubject"}, + {OPT_HOST_KEY_STORE_FLAGS, "HostKeyStoreFlags"}, + {OPT_HOST_KEY_STORE, "HostKeyStore"}, {OPT_HOST_KEY, "HostKey"}, {OPT_PASSWORD_AUTH, "PasswordAuthentication"}, {OPT_PUBKEY_AUTH, "PubkeyAuthentication"}, @@ -471,12 +494,15 @@ static const CONFIG_OPTION options[NUM_OPTIONS] = { {OPT_BANNER, "Banner"}, {OPT_STRICT_MODES, "StrictModes"}, {OPT_TRUSTED_SYSTEM_CA_KEYS, "wolfSSH_TrustedSystemCAKeys"}, - {OPT_TRUSTED_USER_CA_STORE, "wolfSSH_TrustedUserCaStore"}, + {OPT_TRUSTED_USER_CA_STORE, "wolfSSH_TrustedUserCAStore"}, +#ifdef USE_WINDOWS_API {OPT_WIN_USER_STORES, "wolfSSH_WinUserStores"}, {OPT_WIN_USER_DW_FLAGS, "wolfSSH_WinUserDwFlags"}, {OPT_WIN_USER_PV_PARA, "wolfSSH_WinUserPvPara"}, +#endif /* USE_WINDOWS_API */ {OPT_AUTHORIZED_UPN_DOMAINS, "AuthorizedUPNDomains"}, }; +#define NUM_OPTIONS ((int)(sizeof(options) / sizeof(*options))) /* returns WS_SUCCESS on success */ static int HandlePrivSep(WOLFSSHD_CONFIG* conf, const char* value) @@ -1340,9 +1366,11 @@ static int HandleConfigOption(WOLFSSHD_CONFIG** conf, int opt, break; case OPT_STRICT_MODES: ret = HandleStrictModes(*conf, value); + break; case OPT_TRUSTED_USER_CA_STORE: ret = wolfSSHD_ConfigSetUserCAStore(*conf, value); break; + #ifdef USE_WINDOWS_API case OPT_WIN_USER_STORES: ret = wolfSSHD_ConfigSetWinUserStores(*conf, value); break; @@ -1352,10 +1380,39 @@ static int HandleConfigOption(WOLFSSHD_CONFIG** conf, int opt, case OPT_WIN_USER_PV_PARA: ret = wolfSSHD_ConfigSetWinUserPvPara(*conf, value); break; + #endif /* USE_WINDOWS_API */ case OPT_AUTHORIZED_UPN_DOMAINS: ret = SetListString(&(*conf)->authorizedUPNDomains, full, fullSz, (*conf)->heap); break; + #ifdef WOLFSSH_WINDOWS_CERT_STORE + case OPT_HOST_KEY_STORE: + wolfSSH_Log(WS_LOG_INFO, + "[SSHD] Parsed HostKeyStore = '%s'", value); + ret = SetFileString(&(*conf)->hostKeyStore, value, (*conf)->heap); + break; + case OPT_HOST_KEY_STORE_SUBJECT: + wolfSSH_Log(WS_LOG_INFO, + "[SSHD] Parsed HostKeyStoreSubject = '%s'", value); + ret = SetFileString(&(*conf)->hostKeyStoreSubject, value, + (*conf)->heap); + break; + case OPT_HOST_KEY_STORE_FLAGS: + wolfSSH_Log(WS_LOG_INFO, + "[SSHD] Parsed HostKeyStoreFlags = '%s'", value); + ret = SetFileString(&(*conf)->hostKeyStoreFlags, value, + (*conf)->heap); + break; + #else + case OPT_HOST_KEY_STORE: + case OPT_HOST_KEY_STORE_SUBJECT: + case OPT_HOST_KEY_STORE_FLAGS: + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] HostKeyStore* options require a " + "WOLFSSH_WINDOWS_CERT_STORE build"); + ret = WS_NOT_COMPILED; + break; + #endif /* WOLFSSH_WINDOWS_CERT_STORE */ default: break; } @@ -1719,7 +1776,11 @@ int wolfSSHD_ConfigSetSystemCA(WOLFSSHD_CONFIG* conf, const char* value) { int ret = WS_SUCCESS; - if (conf != NULL) { + if (conf == NULL || value == NULL) { + ret = WS_BAD_ARGUMENT; + } + + if (ret == WS_SUCCESS) { if (WSTRCMP(value, "yes") == 0) { wolfSSH_Log(WS_LOG_INFO, "[SSHD] System CAs enabled"); conf->useSystemCA = 1; @@ -1755,7 +1816,11 @@ int wolfSSHD_ConfigSetUserCAStore(WOLFSSHD_CONFIG* conf, const char* value) { int ret = WS_SUCCESS; - if (conf != NULL) { + if (conf == NULL || value == NULL) { + ret = WS_BAD_ARGUMENT; + } + + if (ret == WS_SUCCESS) { if (WSTRCMP(value, "yes") == 0) { wolfSSH_Log(WS_LOG_INFO, "[SSHD] User CA store enabled. Note this " "is currently only supported on Windows."); @@ -1774,12 +1839,19 @@ int wolfSSHD_ConfigSetUserCAStore(WOLFSSHD_CONFIG* conf, const char* value) return ret; } -char* wolfSSHD_ConfigGetWinUserStores(WOLFSSHD_CONFIG* conf) { +#ifdef USE_WINDOWS_API +char* wolfSSHD_ConfigGetWinUserStores(WOLFSSHD_CONFIG* conf) +{ if (conf != NULL) { if (conf->winUserStores == NULL) { /* If no value was specified, default to CERT_STORE_PROV_SYSTEM */ - CreateString(&conf->winUserStores, "CERT_STORE_PROV_SYSTEM", - (int)WSTRLEN("CERT_STORE_PROV_SYSTEM"), conf->heap); + if (CreateString(&conf->winUserStores, "CERT_STORE_PROV_SYSTEM", + (int)WSTRLEN("CERT_STORE_PROV_SYSTEM"), conf->heap) + != WS_SUCCESS) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Unable to create default winUserStores"); + return NULL; + } } return conf->winUserStores; @@ -1788,24 +1860,38 @@ char* wolfSSHD_ConfigGetWinUserStores(WOLFSSHD_CONFIG* conf) { return NULL; } -int wolfSSHD_ConfigSetWinUserStores(WOLFSSHD_CONFIG* conf, const char* value) { +int wolfSSHD_ConfigSetWinUserStores(WOLFSSHD_CONFIG* conf, const char* value) +{ int ret = WS_SUCCESS; - if (conf == NULL) { + if (conf == NULL || value == NULL) { ret = WS_BAD_ARGUMENT; } - ret = CreateString(&conf->winUserStores, value, (int)WSTRLEN(value), conf->heap); + if (ret == WS_SUCCESS) { + /* free any previously set value before replacing it */ + FreeString(&conf->winUserStores, conf->heap); + ret = CreateString(&conf->winUserStores, value, + (int)WSTRLEN(value), conf->heap); + } return ret; } -char* wolfSSHD_ConfigGetWinUserDwFlags(WOLFSSHD_CONFIG* conf) { +char* wolfSSHD_ConfigGetWinUserDwFlags(WOLFSSHD_CONFIG* conf) +{ if (conf != NULL) { if (conf->winUserDwFlags == NULL) { - /* If no value was specified, default to CERT_SYSTEM_STORE_CURRENT_USER */ - CreateString(&conf->winUserDwFlags, "CERT_SYSTEM_STORE_CURRENT_USER", - (int)WSTRLEN("CERT_SYSTEM_STORE_CURRENT_USER"), conf->heap); + /* If no value was specified, default to + * CERT_SYSTEM_STORE_CURRENT_USER */ + if (CreateString(&conf->winUserDwFlags, + "CERT_SYSTEM_STORE_CURRENT_USER", + (int)WSTRLEN("CERT_SYSTEM_STORE_CURRENT_USER"), + conf->heap) != WS_SUCCESS) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Unable to create default winUserDwFlags"); + return NULL; + } } return conf->winUserDwFlags; @@ -1814,23 +1900,35 @@ char* wolfSSHD_ConfigGetWinUserDwFlags(WOLFSSHD_CONFIG* conf) { return NULL; } -int wolfSSHD_ConfigSetWinUserDwFlags(WOLFSSHD_CONFIG* conf, const char* value) { +int wolfSSHD_ConfigSetWinUserDwFlags(WOLFSSHD_CONFIG* conf, const char* value) +{ int ret = WS_SUCCESS; - if (conf == NULL) { + if (conf == NULL || value == NULL) { ret = WS_BAD_ARGUMENT; } - ret = CreateString(&conf->winUserDwFlags, value, (int)WSTRLEN(value), conf->heap); + if (ret == WS_SUCCESS) { + /* free any previously set value before replacing it */ + FreeString(&conf->winUserDwFlags, conf->heap); + ret = CreateString(&conf->winUserDwFlags, value, + (int)WSTRLEN(value), conf->heap); + } return ret; } -char* wolfSSHD_ConfigGetWinUserPvPara(WOLFSSHD_CONFIG* conf) { +char* wolfSSHD_ConfigGetWinUserPvPara(WOLFSSHD_CONFIG* conf) +{ if (conf != NULL) { if (conf->winUserPvPara == NULL) { /* If no value was specified, default to MY */ - CreateString(&conf->winUserPvPara, "MY", (int)WSTRLEN("MY"), conf->heap); + if (CreateString(&conf->winUserPvPara, "MY", + (int)WSTRLEN("MY"), conf->heap) != WS_SUCCESS) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Unable to create default winUserPvPara"); + return NULL; + } } return conf->winUserPvPara; @@ -1839,17 +1937,24 @@ char* wolfSSHD_ConfigGetWinUserPvPara(WOLFSSHD_CONFIG* conf) { return NULL; } -int wolfSSHD_ConfigSetWinUserPvPara(WOLFSSHD_CONFIG* conf, const char* value) { +int wolfSSHD_ConfigSetWinUserPvPara(WOLFSSHD_CONFIG* conf, const char* value) +{ int ret = WS_SUCCESS; - if (conf == NULL) { + if (conf == NULL || value == NULL) { ret = WS_BAD_ARGUMENT; } - ret = CreateString(&conf->winUserPvPara, value, (int)WSTRLEN(value), conf->heap); + if (ret == WS_SUCCESS) { + /* free any previously set value before replacing it */ + FreeString(&conf->winUserPvPara, conf->heap); + ret = CreateString(&conf->winUserPvPara, value, + (int)WSTRLEN(value), conf->heap); + } return ret; } +#endif /* USE_WINDOWS_API */ char* wolfSSHD_ConfigGetUserCAKeysFile(const WOLFSSHD_CONFIG* conf) { @@ -1895,6 +2000,43 @@ static int SetFileString(char** dst, const char* src, void* heap) return ret; } +#ifdef WOLFSSH_WINDOWS_CERT_STORE +char* wolfSSHD_ConfigGetHostKeyStore(const WOLFSSHD_CONFIG* conf) +{ + char* ret = NULL; + + if (conf != NULL) { + ret = conf->hostKeyStore; + } + + return ret; +} + + +char* wolfSSHD_ConfigGetHostKeyStoreSubject(const WOLFSSHD_CONFIG* conf) +{ + char* ret = NULL; + + if (conf != NULL) { + ret = conf->hostKeyStoreSubject; + } + + return ret; +} + + +char* wolfSSHD_ConfigGetHostKeyStoreFlags(const WOLFSSHD_CONFIG* conf) +{ + char* ret = NULL; + + if (conf != NULL) { + ret = conf->hostKeyStoreFlags; + } + + return ret; +} +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ + int wolfSSHD_ConfigSetHostKeyFile(WOLFSSHD_CONFIG* conf, const char* file) { int ret = WS_SUCCESS; diff --git a/apps/wolfsshd/configuration.h b/apps/wolfsshd/configuration.h index 71cd9c263..554aeba50 100644 --- a/apps/wolfsshd/configuration.h +++ b/apps/wolfsshd/configuration.h @@ -64,16 +64,23 @@ char* wolfSSHD_ConfigGetUserCAKeysFile(const WOLFSSHD_CONFIG* conf); char* wolfSSHD_ConfigGetAuthorizedUPNDomains(const WOLFSSHD_CONFIG* conf); int wolfSSHD_ConfigSetHostKeyFile(WOLFSSHD_CONFIG* conf, const char* file); int wolfSSHD_ConfigSetHostCertFile(WOLFSSHD_CONFIG* conf, const char* file); +#ifdef WOLFSSH_WINDOWS_CERT_STORE +char* wolfSSHD_ConfigGetHostKeyStore(const WOLFSSHD_CONFIG* conf); +char* wolfSSHD_ConfigGetHostKeyStoreSubject(const WOLFSSHD_CONFIG* conf); +char* wolfSSHD_ConfigGetHostKeyStoreFlags(const WOLFSSHD_CONFIG* conf); +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ int wolfSSHD_ConfigSetSystemCA(WOLFSSHD_CONFIG* conf, const char* value); int wolfSSHD_ConfigGetSystemCA(const WOLFSSHD_CONFIG* conf); int wolfSSHD_ConfigSetUserCAStore(WOLFSSHD_CONFIG* conf, const char* value); int wolfSSHD_ConfigGetUserCAStore(const WOLFSSHD_CONFIG* conf); +#ifdef USE_WINDOWS_API char* wolfSSHD_ConfigGetWinUserStores(WOLFSSHD_CONFIG* conf); int wolfSSHD_ConfigSetWinUserStores(WOLFSSHD_CONFIG* conf, const char* value); char* wolfSSHD_ConfigGetWinUserDwFlags(WOLFSSHD_CONFIG* conf); int wolfSSHD_ConfigSetWinUserDwFlags(WOLFSSHD_CONFIG* conf, const char* value); char* wolfSSHD_ConfigGetWinUserPvPara(WOLFSSHD_CONFIG* conf); int wolfSSHD_ConfigSetWinUserPvPara(WOLFSSHD_CONFIG* conf, const char* value); +#endif /* USE_WINDOWS_API */ int wolfSSHD_ConfigSetUserCAKeysFile(WOLFSSHD_CONFIG* conf, const char* file); word16 wolfSSHD_ConfigGetPort(const WOLFSSHD_CONFIG* conf); char* wolfSSHD_ConfigGetAuthKeysFile(const WOLFSSHD_CONFIG* conf); diff --git a/apps/wolfsshd/wolfsshd.c b/apps/wolfsshd/wolfsshd.c index cd95fbdec..6a127a3bb 100644 --- a/apps/wolfsshd/wolfsshd.c +++ b/apps/wolfsshd/wolfsshd.c @@ -38,6 +38,18 @@ #include #include +#ifdef WOLFSSH_WINDOWS_CERT_STORE + #include + #include + #include + #ifndef CERT_SYSTEM_STORE_CURRENT_USER + #define CERT_SYSTEM_STORE_CURRENT_USER 0x00010000 + #endif + #ifndef CERT_SYSTEM_STORE_LOCAL_MACHINE + #define CERT_SYSTEM_STORE_LOCAL_MACHINE 0x00020000 + #endif +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ + #define WOLFSSH_TEST_SERVER #include @@ -342,6 +354,129 @@ static void CleanupCTX(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX** ctx, (void)conf; } +#if defined(WOLFSSH_CERTS) && defined(WOLFSSH_WINDOWS_CERT_STORE) +/* Add every certificate in the configured Windows store (winUserPvPara name, + * winUserDwFlags location) as a trusted root CA. Returns WS_SUCCESS on + * success. */ +static int LoadUserCACertsFromStore(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX* ctx, + void* heap) +{ + int ret = WS_SUCCESS; + char* storeNameStr; + char* dwFlagsStr; + char* providerStr; + word32 dwFlags = CERT_SYSTEM_STORE_CURRENT_USER; + wchar_t* wStoreName = NULL; + int wStoreNameLen; + HCERTSTORE hStore = NULL; + PCCERT_CONTEXT pCertContext = NULL; + word32 loaded = 0; + + storeNameStr = wolfSSHD_ConfigGetWinUserPvPara(conf); + dwFlagsStr = wolfSSHD_ConfigGetWinUserDwFlags(conf); + providerStr = wolfSSHD_ConfigGetWinUserStores(conf); + if (storeNameStr == NULL) { + wolfSSH_Log(WS_LOG_ERROR, "[SSHD] No user CA store name configured"); + return WS_BAD_ARGUMENT; + } + + /* Only the system-store provider is supported here. */ + if (providerStr != NULL && + WSTRCMP(providerStr, "CERT_STORE_PROV_SYSTEM") != 0) { + wolfSSH_Log(WS_LOG_INFO, + "[SSHD] wolfSSH_WinUserStores='%s' ignored; only " + "CERT_STORE_PROV_SYSTEM is supported", providerStr); + } + + if (dwFlagsStr != NULL) { + if (WSTRCMP(dwFlagsStr, "CURRENT_USER") == 0 || + WSTRCMP(dwFlagsStr, "CERT_SYSTEM_STORE_CURRENT_USER") == 0) { + dwFlags = CERT_SYSTEM_STORE_CURRENT_USER; + } + else if (WSTRCMP(dwFlagsStr, "LOCAL_MACHINE") == 0 || + WSTRCMP(dwFlagsStr, "CERT_SYSTEM_STORE_LOCAL_MACHINE") == 0) { + dwFlags = CERT_SYSTEM_STORE_LOCAL_MACHINE; + } + else { + /* fall back to a raw numeric value; a result of 0 means the string + * was not a recognized name or valid number, which is never a + * usable store-location flag */ + dwFlags = (word32)atoi(dwFlagsStr); + if (dwFlags == 0) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Unrecognized user CA store flags '%s'", dwFlagsStr); + return WS_BAD_ARGUMENT; + } + } + } + + wStoreNameLen = MultiByteToWideChar(CP_UTF8, 0, storeNameStr, -1, NULL, 0); + if (wStoreNameLen == 0) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Failed to convert user CA store name to wide characters"); + return WS_BAD_ARGUMENT; + } + wStoreName = (wchar_t*)WMALLOC(wStoreNameLen * sizeof(wchar_t), heap, + DYNTYPE_SSHD); + if (wStoreName == NULL) { + return WS_MEMORY_E; + } + MultiByteToWideChar(CP_UTF8, 0, storeNameStr, -1, wStoreName, + wStoreNameLen); + + hStore = CertOpenStore(CERT_STORE_PROV_SYSTEM_W, 0, (HCRYPTPROV_LEGACY)0, + dwFlags | CERT_STORE_OPEN_EXISTING_FLAG | CERT_STORE_READONLY_FLAG, + wStoreName); + if (hStore == NULL) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Unable to open user CA cert store '%s', error %lu", + storeNameStr, (unsigned long)GetLastError()); + WFREE(wStoreName, heap, DYNTYPE_SSHD); + return WS_FATAL_ERROR; + } + + /* Passing the previous context frees it and advances the enumeration. */ + for (;;) { + pCertContext = CertEnumCertificatesInStore(hStore, pCertContext); + if (pCertContext == NULL) { + break; + } + if (pCertContext->pbCertEncoded == NULL || + pCertContext->cbCertEncoded == 0) { + continue; + } + if (wolfSSH_CTX_AddRootCert_buffer(ctx, + (const byte*)pCertContext->pbCertEncoded, + (word32)pCertContext->cbCertEncoded, + WOLFSSH_FORMAT_ASN1) != WS_SUCCESS) { + /* Skip certs wolfSSH cannot use as a trust anchor. */ + wolfSSH_Log(WS_LOG_INFO, + "[SSHD] Skipping a cert in store '%s' that could not be " + "loaded as a root CA", storeNameStr); + continue; + } + loaded++; + } + + CertCloseStore(hStore, 0); + WFREE(wStoreName, heap, DYNTYPE_SSHD); + + if (loaded == 0) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] No usable CA certificates found in store '%s'", + storeNameStr); + ret = WS_FATAL_ERROR; + } + else { + wolfSSH_Log(WS_LOG_INFO, + "[SSHD] Loaded %u CA certificate(s) from store '%s'", + loaded, storeNameStr); + } + + return ret; +} +#endif /* WOLFSSH_CERTS && WOLFSSH_WINDOWS_CERT_STORE */ + /* Initializes and sets up the WOLFSSH_CTX struct based on the configure options * return WS_SUCCESS on success */ @@ -383,98 +518,208 @@ static int SetupCTX(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX** ctx, /* Load in host private key */ if (ret == WS_SUCCESS) { +#ifdef WOLFSSH_WINDOWS_CERT_STORE + char* hostKeyStore = wolfSSHD_ConfigGetHostKeyStore(conf); + char* hostKeyStoreSubject = wolfSSHD_ConfigGetHostKeyStoreSubject(conf); + char* hostKeyStoreFlags = wolfSSHD_ConfigGetHostKeyStoreFlags(conf); - char* hostKey = wolfSSHD_ConfigGetHostKeyFile(conf); + wolfSSH_Log(WS_LOG_INFO, + "[SSHD] Cert store code compiled in. " + "hostKeyStore=%s, hostKeyStoreSubject=%s, hostKeyStoreFlags=%s", + hostKeyStore ? hostKeyStore : "(null)", + hostKeyStoreSubject ? hostKeyStoreSubject : "(null)", + hostKeyStoreFlags ? hostKeyStoreFlags : "(null)"); - if (hostKey == NULL) { - wolfSSH_Log(WS_LOG_ERROR, "[SSHD] No host private key set"); + if (hostKeyStore != NULL && hostKeyStoreSubject == NULL) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] HostKeyStore set but HostKeyStoreSubject is missing"); ret = WS_BAD_ARGUMENT; } - else { - byte* data; - word32 dataSz = 0; - /* The host private key is a secret trust anchor: refuse a symlink, - * an unsafe owner or path, or a group/world readable/writable - * file. */ - data = getBufferFromFile(hostKey, &dataSz, heap, - WOLFSSHD_LOAD_SECRET); - if (data == NULL) { - /* NULL means the secure gate rejected the file (bad owner, - * symlink, group/world writable/readable; reason already - * logged) or the read failed, so report a file error rather - * than a memory error. */ + if (ret == WS_SUCCESS && + hostKeyStore != NULL && hostKeyStoreSubject != NULL) { + /* Use cert store host key */ + wchar_t* wStoreName = NULL; + wchar_t* wSubjectName = NULL; + word32 dwFlags = CERT_SYSTEM_STORE_CURRENT_USER; + int storeNameLen, subjectNameLen; + + /* Parse flags if provided */ + if (hostKeyStoreFlags != NULL) { + if (WSTRCMP(hostKeyStoreFlags, "CURRENT_USER") == 0) { + dwFlags = CERT_SYSTEM_STORE_CURRENT_USER; + } else if (WSTRCMP(hostKeyStoreFlags, "LOCAL_MACHINE") == 0) { + dwFlags = CERT_SYSTEM_STORE_LOCAL_MACHINE; + } else { + /* fall back to a raw numeric value; a result of 0 means the + * string was not a recognized name or valid number, which + * is never a usable store-location flag */ + dwFlags = (word32)atoi(hostKeyStoreFlags); + if (dwFlags == 0) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Unrecognized host key store flags '%s'", + hostKeyStoreFlags); + ret = WS_BAD_ARGUMENT; + } + } + } + + /* Convert to wide strings */ + storeNameLen = MultiByteToWideChar(CP_UTF8, 0, hostKeyStore, -1, + NULL, 0); + subjectNameLen = MultiByteToWideChar(CP_UTF8, 0, + hostKeyStoreSubject, -1, NULL, 0); + + if (ret != WS_SUCCESS) { + /* flag parsing failed; error already logged */ + } + else if (storeNameLen == 0 || subjectNameLen == 0) { wolfSSH_Log(WS_LOG_ERROR, - "[SSHD] Error reading host key file."); - ret = WS_BAD_FILE_E; + "[SSHD] Failed to convert cert store strings to wchar"); + ret = WS_BAD_ARGUMENT; + } + else { + wStoreName = (wchar_t*)WMALLOC( + storeNameLen * sizeof(wchar_t), heap, DYNTYPE_SSHD); + wSubjectName = (wchar_t*)WMALLOC( + subjectNameLen * sizeof(wchar_t), heap, DYNTYPE_SSHD); + + if (wStoreName == NULL || wSubjectName == NULL) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Memory allocation failed for cert store strings"); + ret = WS_MEMORY_E; + } + else { + MultiByteToWideChar(CP_UTF8, 0, hostKeyStore, -1, + wStoreName, storeNameLen); + MultiByteToWideChar(CP_UTF8, 0, hostKeyStoreSubject, -1, + wSubjectName, subjectNameLen); + + ret = wolfSSH_CTX_UsePrivateKey_fromStore(*ctx, wStoreName, + dwFlags, wSubjectName); + if (ret != WS_SUCCESS) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Failed to load host key from certificate store"); + } + } + if (wStoreName != NULL) { + WFREE(wStoreName, heap, DYNTYPE_SSHD); + } + if (wSubjectName != NULL) { + WFREE(wSubjectName, heap, DYNTYPE_SSHD); + } } + } + else if (ret == WS_SUCCESS) +#elif defined(WOLFSSH_CERTS) + wolfSSH_Log(WS_LOG_INFO, + "[SSHD] WOLFSSH_WINDOWS_CERT_STORE not defined - cert store support disabled"); +#else + wolfSSH_Log(WS_LOG_INFO, + "[SSHD] WOLFSSH_CERTS not defined - cert store support disabled"); +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ + { + char* hostKey = wolfSSHD_ConfigGetHostKeyFile(conf); - if (ret == WS_SUCCESS) { - /* Host keys may be PEM or DER. Detect by content: a DER key is - * an ASN.1 SEQUENCE (leading 0x30); anything else is treated as - * PEM text and decoded with wc_KeyPemToDer(), which handles - * PKCS#1, SEC1 and PKCS#8 "PRIVATE KEY" bodies. - * - * The previous code used wc_PemToDer(..., PRIVATEKEY_TYPE, ...), - * which only recognizes the classic "RSA/EC PRIVATE KEY" PEM - * headers. On a PKCS#8 body (how ML-DSA host keys are emitted) - * it returns *success* but yields a malformed body (leading - * 0x04, not a 0x30 SEQUENCE), which - * wolfSSH_CTX_UsePrivateKey_buffer() then rejects with - * WS_BAD_FILETYPE_E, so ML-DSA PEM host keys could not load. */ - byte* keyDer = NULL; - - if (dataSz == 0) { - /* An empty (0-byte) file passes the NULL check above but - * carries no key material. Handle it explicitly as a file - * error instead of falling into the PEM path, where - * WMALLOC(0) is implementation-defined (may return NULL and - * be misreported as WS_MEMORY_E). */ - wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Host key file is empty."); + wolfSSH_Log(WS_LOG_INFO, + "[SSHD] File-based host key path entered. hostKey=%s", + hostKey ? hostKey : "(null)"); + + if (hostKey == NULL) { + wolfSSH_Log(WS_LOG_ERROR, "[SSHD] No host private key set"); + ret = WS_BAD_ARGUMENT; + } + else { + byte* data; + word32 dataSz = 0; + + /* The host private key is a secret trust anchor: refuse a + * symlink, an unsafe owner or path, or a group/world + * readable/writable file. */ + data = getBufferFromFile(hostKey, &dataSz, heap, + WOLFSSHD_LOAD_SECRET); + if (data == NULL) { + /* NULL means the secure gate rejected the file (bad owner, + * symlink, group/world writable/readable; reason already + * logged) or the read failed, so report a file error rather + * than a memory error. */ + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Error reading host key file."); ret = WS_BAD_FILE_E; + } - else if (data[0] == 0x30) { - privBuf = data; - privBufSz = dataSz; - } - else { - keyDer = (byte*)WMALLOC(dataSz, heap, DYNTYPE_SSHD); - if (keyDer == NULL) { - ret = WS_MEMORY_E; + + if (ret == WS_SUCCESS) { + /* Host keys may be PEM or DER. Detect by content: a DER key + * is an ASN.1 SEQUENCE (leading 0x30); anything else is + * treated as PEM text and decoded with wc_KeyPemToDer(), + * which handles PKCS#1, SEC1 and PKCS#8 "PRIVATE KEY" + * bodies. + * + * The previous code used wc_PemToDer(..., PRIVATEKEY_TYPE, + * ...), which only recognizes the classic "RSA/EC PRIVATE + * KEY" PEM headers. On a PKCS#8 body (how ML-DSA host keys + * are emitted) it returns *success* but yields a malformed + * body (leading 0x04, not a 0x30 SEQUENCE), which + * wolfSSH_CTX_UsePrivateKey_buffer() then rejects with + * WS_BAD_FILETYPE_E, so ML-DSA PEM host keys could not + * load. */ + byte* keyDer = NULL; + + if (dataSz == 0) { + /* An empty (0-byte) file passes the NULL check above + * but carries no key material. Handle it explicitly as + * a file error instead of falling into the PEM path, + * where WMALLOC(0) is implementation-defined (may + * return NULL and be misreported as WS_MEMORY_E). */ + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Host key file is empty."); + ret = WS_BAD_FILE_E; + } + else if (data[0] == 0x30) { + privBuf = data; + privBufSz = dataSz; } else { - int keyDerSz = wc_KeyPemToDer(data, dataSz, keyDer, - (int)dataSz, NULL); - if (keyDerSz <= 0) { - wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Failed to convert " - "host private key from PEM."); - ret = WS_BAD_FILE_E; + keyDer = (byte*)WMALLOC(dataSz, heap, DYNTYPE_SSHD); + if (keyDer == NULL) { + ret = WS_MEMORY_E; } else { - privBuf = keyDer; - privBufSz = (word32)keyDerSz; + int keyDerSz = wc_KeyPemToDer(data, dataSz, keyDer, + (int)dataSz, NULL); + if (keyDerSz <= 0) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Failed to convert host private key " + "from PEM."); + ret = WS_BAD_FILE_E; + } + else { + privBuf = keyDer; + privBufSz = (word32)keyDerSz; + } } } - } - if (ret == WS_SUCCESS - && wolfSSH_CTX_UsePrivateKey_buffer(*ctx, privBuf, - privBufSz, WOLFSSH_FORMAT_ASN1) < 0) { - wolfSSH_Log(WS_LOG_ERROR, - "[SSHD] Failed to use host private key."); - ret = WS_BAD_ARGUMENT; - } + if (ret == WS_SUCCESS + && wolfSSH_CTX_UsePrivateKey_buffer(*ctx, privBuf, + privBufSz, WOLFSSH_FORMAT_ASN1) < 0) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Failed to use host private key."); + ret = WS_BAD_ARGUMENT; + } - if (keyDer != NULL) { - WS_FORCEZERO(keyDer, dataSz); - WFREE(keyDer, heap, DYNTYPE_SSHD); + if (keyDer != NULL) { + WS_FORCEZERO(keyDer, dataSz); + WFREE(keyDer, heap, DYNTYPE_SSHD); + } + /* data held the raw private key — the DER bytes, or the PEM + * text decoded into keyDer above. Zeroize before freeing so + * key material does not linger in the heap after use. */ + WS_FORCEZERO(data, dataSz); + freeBufferFromFile(data, heap); } - /* data held the raw private key — the DER bytes, or the PEM - * text decoded into keyDer above. Zeroize before freeing so key - * material does not linger in the heap after use. */ - WS_FORCEZERO(data, dataSz); - freeBufferFromFile(data, heap); } } } @@ -526,37 +771,23 @@ static int SetupCTX(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX** ctx, #endif /* WOLFSSH_OSSH_CERTS || WOLFSSH_CERTS */ #ifdef WOLFSSH_CERTS - /* check if loading in system and/or user CA certs */ + /* Load system CA certs from the OS trust store via wolfSSL into a + * temporary WOLFSSL_CTX, then import its cert manager. */ #ifdef WOLFSSL_SYS_CA_CERTS - if (ret == WS_SUCCESS && (wolfSSHD_ConfigGetSystemCA(conf) - || wolfSSHD_ConfigGetUserCAStore(conf))) { + if (ret == WS_SUCCESS && wolfSSHD_ConfigGetSystemCA(conf)) { WOLFSSL_CTX* sslCtx; wolfSSH_Log(WS_LOG_INFO, "[SSHD] Using system CAs"); - sslCtx = wolfSSL_CTX_new(wolfSSLv23_method()); + sslCtx = wolfSSL_CTX_new(wolfSSLv23_server_method()); if (sslCtx == NULL) { wolfSSH_Log(WS_LOG_INFO, "[SSHD] Unable to create temporary CTX"); ret = WS_FATAL_ERROR; } if (ret == WS_SUCCESS) { - if (wolfSSHD_ConfigGetSystemCA(conf)) { - if (wolfSSL_CTX_load_system_CA_certs(sslCtx) != WOLFSSL_SUCCESS) { - wolfSSH_Log(WS_LOG_INFO, "[SSHD] Issue loading system CAs"); - ret = WS_FATAL_ERROR; - } - } - } - - if (ret == WS_SUCCESS) { - if (wolfSSHD_ConfigGetUserCAStore(conf)) { - if (wolfSSL_CTX_load_windows_user_CA_certs(sslCtx, - wolfSSHD_ConfigGetWinUserStores(conf), - wolfSSHD_ConfigGetWinUserDwFlags(conf), - wolfSSHD_ConfigGetWinUserPvPara(conf)) != WOLFSSL_SUCCESS) { - wolfSSH_Log(WS_LOG_INFO, "[SSHD] Issue loading user CAs"); - ret = WS_FATAL_ERROR; - } + if (wolfSSL_CTX_load_system_CA_certs(sslCtx) != WOLFSSL_SUCCESS) { + wolfSSH_Log(WS_LOG_INFO, "[SSHD] Issue loading system CAs"); + ret = WS_FATAL_ERROR; } } @@ -573,7 +804,32 @@ static int SetupCTX(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX** ctx, wolfSSL_CTX_free(sslCtx); } } - #endif + #else + /* The system CA directive is parsed unconditionally. Fail startup if it + * was set but wolfSSL was built without WOLFSSL_SYS_CA_CERTS, rather than + * silently running without the configured trust anchors. */ + if (ret == WS_SUCCESS && wolfSSHD_ConfigGetSystemCA(conf)) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] wolfSSH_TrustedSystemCAKeys set but wolfSSL was built " + "without WOLFSSL_SYS_CA_CERTS."); + ret = WS_NOT_COMPILED; + } + #endif /* WOLFSSL_SYS_CA_CERTS */ + + /* Load user CA certs (trust anchors used to verify client X.509 certs) + * directly from a Windows certificate store into the cert manager. */ + #ifdef WOLFSSH_WINDOWS_CERT_STORE + if (ret == WS_SUCCESS && wolfSSHD_ConfigGetUserCAStore(conf)) { + ret = LoadUserCACertsFromStore(conf, *ctx, heap); + } + #else + if (ret == WS_SUCCESS && wolfSSHD_ConfigGetUserCAStore(conf)) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] wolfSSH_TrustedUserCAStore set but " + "WOLFSSH_WINDOWS_CERT_STORE is not compiled in."); + ret = WS_NOT_COMPILED; + } + #endif /* WOLFSSH_WINDOWS_CERT_STORE */ /* load in CA certs from file set */ if (ret == WS_SUCCESS) { @@ -620,6 +876,14 @@ static int SetupCTX(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX** ctx, } } } +#else + if (ret == WS_SUCCESS && (wolfSSHD_ConfigGetSystemCA(conf) + || wolfSSHD_ConfigGetUserCAStore(conf))) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] wolfSSH_TrustedSystemCAKeys/wolfSSH_TrustedUserCAStore set " + "but wolfSSH was built without WOLFSSH_CERTS."); + ret = WS_NOT_COMPILED; + } #endif if (ret == WS_SUCCESS) { @@ -2865,6 +3129,24 @@ static int StartSSHD(int argc, char** argv) } } + if (logFile == NULL) { + logFile = stderr; + } +#ifdef _WIN32 + /* The early -D detection (wide-string comparison of cmdArgs before + * conversion) may have set ServiceDebugCb even when -D was supplied. + * Now that mygetopt has been processed, restore the file-based + * callback in any case where output should go to logFile: + * - isDaemon==0 → running interactively, logs to logFile (stderr) + * - isDaemon==1 but -E was used → logs to the specified file + * This must happen BEFORE config/SetupCTX so their log messages are + * captured in the file (or stderr) rather than lost to + * OutputDebugString. */ + if (!isDaemon || logFile != stderr) { + wolfSSH_SetLoggingCb(wolfSSHDLoggingCb); + } +#endif + if (ret == WS_SUCCESS) { ret = wolfSSHD_ConfigLoad(conf, configFile); if (ret != WS_SUCCESS) { @@ -2896,10 +3178,6 @@ static int StartSSHD(int argc, char** argv) } } - if (logFile == NULL) { - logFile = stderr; - } - /* run as a daemon or service */ #ifndef WIN32 if (ret == WS_SUCCESS && isDaemon) { diff --git a/configure.ac b/configure.ac index a98a70212..d7c2e3353 100644 --- a/configure.ac +++ b/configure.ac @@ -220,6 +220,12 @@ AC_ARG_ENABLE([ossh-certs], [AS_HELP_STRING([--enable-ossh-certs],[Enable OpenSSH certificate user auth (default: disabled)])], [ENABLED_OSSH_CERTS=$enableval],[ENABLED_OSSH_CERTS=no]) +# Windows certificate store (host/client keys) +AC_ARG_ENABLE([windows-cert-store], + [AS_HELP_STRING([--enable-windows-cert-store],[Enable Windows certificate store integration for keys (default: disabled)])], + [ENABLED_WINDOWS_CERT_STORE=$enableval], + [ENABLED_WINDOWS_CERT_STORE=no]) + # TPM 2.0 Support AC_ARG_ENABLE([tpm], [AS_HELP_STRING([--enable-tpm],[Enable TPM 2.0 support (default: disabled)])], @@ -288,6 +294,13 @@ AS_IF([test "x$ENABLED_CERTS" = "xyes"], [AM_CPPFLAGS="$AM_CPPFLAGS -DWOLFSSH_CERTS"]) AS_IF([test "x$ENABLED_OSSH_CERTS" = "xyes"], [AM_CPPFLAGS="$AM_CPPFLAGS -DWOLFSSH_OSSH_CERTS"]) +AS_IF([test "x$ENABLED_WINDOWS_CERT_STORE" = "xyes"], + [AS_IF([test "x$ENABLED_CERTS" != "xyes"], + [AC_MSG_ERROR([--enable-windows-cert-store requires X.509 cert support (--enable-certs)])]) + AM_CPPFLAGS="$AM_CPPFLAGS -DWOLFSSH_WINDOWS_CERT_STORE" + AS_CASE([$host], + [*mingw*|*msys*|*cygwin*],[LIBS="$LIBS -lcrypt32 -lncrypt"], + [AC_MSG_ERROR([--enable-windows-cert-store is only supported on Windows hosts (mingw/msys/cygwin)])])]) AS_IF([test "x$ENABLED_SMALLSTACK" = "xyes"], [AM_CPPFLAGS="$AM_CPPFLAGS -DWOLFSSH_SMALL_STACK"]) AS_IF([test "x$ENABLED_NONE_CIPHER" = "xyes"], @@ -399,4 +412,5 @@ AS_ECHO([" * TPM 2.0 support: $ENABLED_TPM"]) AS_ECHO([" * TCP/IP Forwarding: $ENABLED_FWD"]) AS_ECHO([" * X.509 Certs: $ENABLED_CERTS"]) AS_ECHO([" * OpenSSH Certs: $ENABLED_OSSH_CERTS"]) +AS_ECHO([" * Windows cert store: $ENABLED_WINDOWS_CERT_STORE"]) AS_ECHO([" * Examples: $ENABLED_EXAMPLES"]) diff --git a/examples/client/common.c b/examples/client/common.c index b9df3416e..704bc7f9c 100644 --- a/examples/client/common.c +++ b/examples/client/common.c @@ -48,11 +48,17 @@ #ifdef WOLFSSH_CERTS #include + #ifdef WOLFSSH_WINDOWS_CERT_STORE + #include + #include + #include + #endif /* WOLFSSH_WINDOWS_CERT_STORE */ #endif static byte userPublicKeyBuf[512]; static byte* userPublicKey = userPublicKeyBuf; static byte userPublicKeyAlloc = 0; +static int userPublicKeyCtxOwned = 0; /* userPublicKey aliases CTX memory */ static const byte* userPublicKeyType = NULL; static byte userPassword[256]; static const byte* userPrivateKeyType = NULL; @@ -1158,7 +1164,14 @@ void ClientFreeBuffers(const char* pubKeyName, const char* privKeyName, * name being given. */ (void)pubKeyName; - if (userPublicKeyAlloc && userPublicKey != NULL) { + if (userPublicKeyCtxOwned) { + /* Aliases CTX-owned memory; the CTX frees it, not us. */ + userPublicKey = userPublicKeyBuf; + userPublicKeySz = 0; + userPublicKeyCtxOwned = 0; + userPublicKeyAlloc = 0; + } + else if (userPublicKeyAlloc && userPublicKey != NULL) { WFREE(userPublicKey, heap, DYNTYPE_PRIVKEY); userPublicKey = userPublicKeyBuf; userPublicKeySz = 0; @@ -1201,3 +1214,86 @@ void ClientFreeBuffers(const char* pubKeyName, const char* privKeyName, wc_ForceZero(userPassword, sizeof(userPassword)); pubKeyLoaded = 0; } + +#ifdef WOLFSSH_WINDOWS_CERT_STORE +int ClientSetPrivateKeyFromStore(WOLFSSH_CTX* ctx, + const wchar_t* storeName, word32 dwFlags, const wchar_t* subjectName) +{ + int ret = WS_SUCCESS; + + if (ctx == NULL || storeName == NULL || subjectName == NULL) { + return WS_BAD_ARGUMENT; + } + + ret = wolfSSH_CTX_UsePrivateKey_fromStore(ctx, storeName, dwFlags, subjectName); + if (ret != WS_SUCCESS) { + fprintf(stderr, "Error loading private key from certificate store: %d\n", ret); + } + + return ret; +} + + +/* After loading a cert store key, populate the global auth callback variables + * (userPublicKeyType, userPublicKey, etc.) so that ClientUserAuth can present + * the certificate for public key authentication. + * For x509 cert auth the "public key" is the DER certificate, and the type + * is the x509v3 name that matches the key algorithm. */ +int ClientSetupCertStoreAuth(WOLFSSH_CTX* ctx) +{ + word32 i; + + if (ctx == NULL) + return WS_BAD_ARGUMENT; + + for (i = 0; i < ctx->privateKeyCount && i < WOLFSSH_MAX_PVT_KEYS; i++) { + WOLFSSH_PVT_KEY* pvtKey = &ctx->privateKey[i]; + if (!pvtKey->useCertStore) + continue; + + /* Point userPublicKey at the DER certificate stored in the CTX. + * This is safe because the CTX outlives the auth callback. The + * ctx-owned flag stops ClientFreeBuffers from freeing CTX memory. */ + userPublicKey = pvtKey->cert; + userPublicKeySz = pvtKey->certSz; + userPublicKeyCtxOwned = 1; + + /* Map the internal key format to the x509v3 SSH type name. */ + switch (pvtKey->publicKeyFmt) { + case ID_SSH_RSA: + case ID_X509V3_SSH_RSA: + case ID_RSA_SHA2_256: + case ID_RSA_SHA2_512: + userPublicKeyType = (const byte*)"x509v3-ssh-rsa"; + break; + case ID_ECDSA_SHA2_NISTP256: + case ID_X509V3_ECDSA_SHA2_NISTP256: + userPublicKeyType = (const byte*)"x509v3-ecdsa-sha2-nistp256"; + break; + case ID_ECDSA_SHA2_NISTP384: + case ID_X509V3_ECDSA_SHA2_NISTP384: + userPublicKeyType = (const byte*)"x509v3-ecdsa-sha2-nistp384"; + break; + case ID_ECDSA_SHA2_NISTP521: + case ID_X509V3_ECDSA_SHA2_NISTP521: + userPublicKeyType = (const byte*)"x509v3-ecdsa-sha2-nistp521"; + break; + default: + fprintf(stderr, "Unsupported cert store key type: %d\n", + pvtKey->publicKeyFmt); + return WS_BAD_ARGUMENT; + } + userPublicKeyTypeSz = (word32)WSTRLEN((const char*)userPublicKeyType); + + /* No in-memory private key — signing goes through the cert store. */ + userPrivateKey = NULL; + userPrivateKeySz = 0; + + pubKeyLoaded = 1; + return WS_SUCCESS; + } + + fprintf(stderr, "No cert store key found in CTX\n"); + return WS_BAD_ARGUMENT; +} +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ diff --git a/examples/client/common.h b/examples/client/common.h index 6ea330c2e..ffb97638f 100644 --- a/examples/client/common.h +++ b/examples/client/common.h @@ -35,6 +35,11 @@ void ClientFreeBuffers(const char* pubKeyName, const char* privKeyName, #ifdef WOLFSSH_TPM int ClientSetTpm(WOLFSSH* ssh); #endif +#ifdef WOLFSSH_WINDOWS_CERT_STORE +int ClientSetPrivateKeyFromStore(WOLFSSH_CTX* ctx, + const wchar_t* storeName, word32 dwFlags, const wchar_t* subjectName); +int ClientSetupCertStoreAuth(WOLFSSH_CTX* ctx); +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ #endif /* WOLFSSH_COMMON_H */ diff --git a/examples/echoserver/echoserver.c b/examples/echoserver/echoserver.c index 4bb178bcf..32691ee8e 100644 --- a/examples/echoserver/echoserver.c +++ b/examples/echoserver/echoserver.c @@ -41,6 +41,7 @@ #include #include #include +#include #include #include #include @@ -117,6 +118,16 @@ #define SOCKET_EWOULDBLOCK WSAEWOULDBLOCK #endif +#ifdef WOLFSSH_WINDOWS_CERT_STORE + #include + #include + #ifndef CERT_SYSTEM_STORE_CURRENT_USER + #define CERT_SYSTEM_STORE_CURRENT_USER 0x00010000 + #endif + #ifndef CERT_SYSTEM_STORE_LOCAL_MACHINE + #define CERT_SYSTEM_STORE_LOCAL_MACHINE 0x00020000 + #endif +#endif #ifndef NO_WOLFSSH_SERVER @@ -2878,6 +2889,9 @@ static void ShowUsage(void) printf(" -x set the comma separated list of key exchange algos " "to use\n"); printf(" -m set the comma separated list of mac algos to use\n"); +#ifdef WOLFSSH_WINDOWS_CERT_STORE + printf(" -W Windows cert store: \"store:subject:flags\" (e.g. My:CN=Server:CURRENT_USER)\n"); +#endif printf(" -b test user auth would block\n"); printf(" -H set test highwater callback\n"); } @@ -2992,6 +3006,9 @@ THREAD_RETURN WOLFSSH_THREAD echoserver_test(void* args) #ifdef WOLFSSH_CERTS char* caCert = NULL; #endif + #ifdef WOLFSSH_WINDOWS_CERT_STORE + const char* certStoreSpec = NULL; + #endif int argc = serverArgs->argc; char** argv = serverArgs->argv; @@ -3000,8 +3017,11 @@ THREAD_RETURN WOLFSSH_THREAD echoserver_test(void* args) kbAuthData.promptCount = 0; #endif + #ifdef WOLFSSH_WINDOWS_CERT_STORE + certStoreSpec = getenv("WOLFSSH_CERT_STORE"); + #endif if (argc > 0) { - const char* optlist = "?1a:d:efEp:R:Ni:j:i:I:J:K:P:k:b:x:m:c:s:G:H"; + const char* optlist = "?1a:d:efEp:R:Ni:j:i:I:J:K:P:k:b:x:m:c:s:G:HW:"; myoptind = 0; while ((ch = mygetopt(argc, argv, optlist)) != -1) { switch (ch) { @@ -3126,6 +3146,12 @@ THREAD_RETURN WOLFSSH_THREAD echoserver_test(void* args) useCustomHighWaterCb = 1; break; + #ifdef WOLFSSH_WINDOWS_CERT_STORE + case 'W': + certStoreSpec = myoptarg; + break; + #endif + default: ShowUsage(); serverArgs->return_code = MY_EX_USAGE; @@ -3325,6 +3351,31 @@ THREAD_RETURN WOLFSSH_THREAD echoserver_test(void* args) } #endif + #ifdef WOLFSSH_WINDOWS_CERT_STORE + if (certStoreSpec != NULL) { + /* Load host key from Windows certificate store */ + wchar_t* wStoreName = NULL; + wchar_t* wSubjectName = NULL; + word32 dwFlags = 0; + int ret; + + ret = wolfSSH_ParseCertStoreSpec(certStoreSpec, &wStoreName, + &wSubjectName, &dwFlags, NULL); + if (ret != WS_SUCCESS) { + ES_ERROR("Invalid cert store spec. Use: store:subject:flags\n"); + } + + ret = wolfSSH_CTX_UsePrivateKey_fromStore(ctx, wStoreName, + dwFlags, wSubjectName); + WFREE(wStoreName, NULL, DYNTYPE_TEMP); + WFREE(wSubjectName, NULL, DYNTYPE_TEMP); + if (ret != WS_SUCCESS) { + ES_ERROR("Couldn't load host key from certificate store.\n"); + } + loadDefaultHostKeys = 0; + } + #endif + if (loadDefaultHostKeys) { bufSz = load_key(peerEcc, keyLoadBuf, bufSz); if (bufSz == 0) { @@ -3729,7 +3780,29 @@ int wolfSSH_Echoserver(int argc, char** argv) #endif #if !defined(WOLFSSL_NUCLEUS) && !defined(INTEGRITY) && !defined(__INTEGRITY) - ChangeToWolfSshRoot(); + { + int useStore = 0; + #ifdef WOLFSSH_WINDOWS_CERT_STORE + /* When using the Windows certificate store for host keys, the + * echoserver does not need file-based keys, so skip the root + * directory search that looks for ./keys/server-key-rsa.pem. */ + if (getenv("WOLFSSH_CERT_STORE") != NULL) { + useStore = 1; + } + else { + int i; + for (i = 1; i < argc; i++) { + if (WSTRNCMP(argv[i], "-W", 2) == 0) { + useStore = 1; + break; + } + } + } + #endif + if (!useStore) { + ChangeToWolfSshRoot(); + } + } #endif #ifndef NO_WOLFSSH_SERVER echoserver_test(&args); diff --git a/examples/sftpclient/sftpclient.c b/examples/sftpclient/sftpclient.c index e892a71e1..694967593 100644 --- a/examples/sftpclient/sftpclient.c +++ b/examples/sftpclient/sftpclient.c @@ -33,6 +33,7 @@ #include #include #include +#include #include #include #include @@ -46,6 +47,17 @@ #ifdef WOLFSSH_CERTS #include + #ifdef WOLFSSH_WINDOWS_CERT_STORE + #include + #include + #include + #ifndef CERT_SYSTEM_STORE_CURRENT_USER + #define CERT_SYSTEM_STORE_CURRENT_USER 0x00010000 + #endif + #ifndef CERT_SYSTEM_STORE_LOCAL_MACHINE + #define CERT_SYSTEM_STORE_LOCAL_MACHINE 0x00020000 + #endif + #endif /* WOLFSSH_WINDOWS_CERT_STORE */ #endif #if defined(WOLFSSH_SFTP) && !defined(NO_WOLFSSH_CLIENT) @@ -398,6 +410,10 @@ static void ShowUsage(void) printf(" -g put local filename as remote filename\n"); printf(" -G get remote filename as local filename\n"); printf(" -i filename for the user's private key\n"); +#ifdef WOLFSSH_WINDOWS_CERT_STORE + printf(" -W Windows cert store: \"store:subject:flags\"\n"); + printf(" Example: -W \"My:CN=MyCert:CURRENT_USER\"\n"); +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ #ifdef WOLFSSH_CERTS printf(" -J filename for DER certificate to use\n"); printf(" Certificate example : client -u orange \\\n"); @@ -1566,13 +1582,20 @@ THREAD_RETURN WOLFSSH_THREAD sftpclient_test(void* args) char* pubKeyName = NULL; char* certName = NULL; char* caCert = NULL; +#ifdef WOLFSSH_WINDOWS_CERT_STORE + const char* certStoreSpec = NULL; /* Format: "store:subject:flags" */ +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ SFTPC_HEAP_HINT* heap = NULL; int argc = ((func_args*)args)->argc; char** argv = ((func_args*)args)->argv; ((func_args*)args)->return_code = 0; - while ((ch = mygetopt(argc, argv, "?d:gh:i:j:l:p:r:u:EGNP:J:A:X")) != -1) { + while ((ch = mygetopt(argc, argv, "?d:gh:i:j:l:p:r:u:EGNP:J:A:X" +#ifdef WOLFSSH_WINDOWS_CERT_STORE + "W:" +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ + )) != -1) { switch (ch) { case 'd': defaultSftpPath = myoptarg; @@ -1650,6 +1673,12 @@ THREAD_RETURN WOLFSSH_THREAD sftpclient_test(void* args) #endif #endif +#ifdef WOLFSSH_WINDOWS_CERT_STORE + case 'W': + certStoreSpec = myoptarg; + break; +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ + case '?': ShowUsage(); exit(EXIT_SUCCESS); @@ -1696,26 +1725,72 @@ THREAD_RETURN WOLFSSH_THREAD sftpclient_test(void* args) } #endif /* WOLFSSH_STATIC_MEMORY */ - ret = ClientSetPrivateKey(privKeyName, userEcc, heap, NULL); - if (ret != 0) { - err_sys("Error setting private key"); - } -#ifdef WOLFSSH_CERTS - /* passed in certificate to use */ - if (certName) { - ret = ClientUseCert(certName, heap); - } - else -#endif +#ifdef WOLFSSH_WINDOWS_CERT_STORE + if (certStoreSpec != NULL) { + wchar_t* wStoreName = NULL; + wchar_t* wSubjectName = NULL; + word32 dwFlags = 0; + + ret = wolfSSH_ParseCertStoreSpec(certStoreSpec, &wStoreName, + &wSubjectName, &dwFlags, NULL); + if (ret != WS_SUCCESS) { + err_sys("Invalid cert store spec. Use: store:subject:flags"); + } + + /* Create context first */ + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, heap); + if (ctx == NULL) { + WFREE(wStoreName, NULL, DYNTYPE_TEMP); + WFREE(wSubjectName, NULL, DYNTYPE_TEMP); + err_sys("Couldn't create wolfSSH client context."); + } + + /* Set private key from cert store */ + ret = ClientSetPrivateKeyFromStore(ctx, wStoreName, dwFlags, + wSubjectName); + if (ret != WS_SUCCESS) { + WFREE(wStoreName, NULL, DYNTYPE_TEMP); + WFREE(wSubjectName, NULL, DYNTYPE_TEMP); + err_sys("Error setting private key from certificate store"); + } + + /* Set up auth callback globals (public key type, cert DER) so + * that ClientUserAuth presents the certificate for public key + * authentication. */ + ret = ClientSetupCertStoreAuth(ctx); + if (ret != WS_SUCCESS) { + WFREE(wStoreName, NULL, DYNTYPE_TEMP); + WFREE(wSubjectName, NULL, DYNTYPE_TEMP); + err_sys("Error setting up cert store auth"); + } + + WFREE(wStoreName, NULL, DYNTYPE_TEMP); + WFREE(wSubjectName, NULL, DYNTYPE_TEMP); + } else +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ { - ret = ClientUsePubKey(pubKeyName, 0, heap); - } - if (ret != 0) { - err_sys("Error setting public key"); - } + ret = ClientSetPrivateKey(privKeyName, userEcc, heap, NULL); + if (ret != 0) { + err_sys("Error setting private key"); + } - ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, heap); + #ifdef WOLFSSH_CERTS + /* passed in certificate to use */ + if (certName) { + ret = ClientUseCert(certName, heap); + } + else + #endif + { + ret = ClientUsePubKey(pubKeyName, 0, heap); + } + if (ret != 0) { + err_sys("Error setting public key"); + } + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, heap); + } if (ctx == NULL) err_sys("Couldn't create wolfSSH client context."); diff --git a/ide/winvs/api-test/api-test.vcxproj b/ide/winvs/api-test/api-test.vcxproj index b0289307d..2524860b7 100644 --- a/ide/winvs/api-test/api-test.vcxproj +++ b/ide/winvs/api-test/api-test.vcxproj @@ -1,4 +1,4 @@ - + @@ -346,7 +346,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDebug32) @@ -382,7 +382,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDllDebug32) @@ -418,7 +418,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDebug64) @@ -454,7 +454,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDllDebug64) @@ -491,7 +491,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptRelease32) @@ -531,7 +531,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptDllRelease32) @@ -571,7 +571,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptRelease64) @@ -611,7 +611,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptDllRelease64) diff --git a/ide/winvs/client/client.vcxproj b/ide/winvs/client/client.vcxproj index ce9887b3b..d8d0d838c 100644 --- a/ide/winvs/client/client.vcxproj +++ b/ide/winvs/client/client.vcxproj @@ -346,7 +346,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDebug32) @@ -382,7 +382,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDllDebug32) @@ -418,7 +418,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDebug64) @@ -454,7 +454,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDllDebug64) @@ -491,7 +491,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptRelease32) @@ -531,7 +531,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptDllRelease32) @@ -571,7 +571,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptRelease64) @@ -611,7 +611,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptDllRelease64) diff --git a/ide/winvs/echoserver/echoserver.vcxproj b/ide/winvs/echoserver/echoserver.vcxproj index e220247c7..c5715bc14 100644 --- a/ide/winvs/echoserver/echoserver.vcxproj +++ b/ide/winvs/echoserver/echoserver.vcxproj @@ -345,7 +345,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDebug32) @@ -381,7 +381,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDllDebug32) @@ -417,7 +417,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDebug64) @@ -453,7 +453,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDllDebug64) @@ -490,7 +490,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptRelease32) @@ -530,7 +530,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptDllRelease32) @@ -570,7 +570,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptRelease64) @@ -610,7 +610,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptDllRelease64) diff --git a/ide/winvs/unit-test/unit-test.vcxproj b/ide/winvs/unit-test/unit-test.vcxproj index 383de1ee9..cf1e70a18 100644 --- a/ide/winvs/unit-test/unit-test.vcxproj +++ b/ide/winvs/unit-test/unit-test.vcxproj @@ -1,4 +1,4 @@ - + @@ -345,7 +345,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDebug32) @@ -381,7 +381,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDllDebug32) @@ -417,7 +417,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDebug64) @@ -453,7 +453,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDllDebug64) @@ -490,7 +490,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptRelease32) @@ -530,7 +530,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptDllRelease32) @@ -570,7 +570,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptRelease64) @@ -610,7 +610,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptDllRelease64) diff --git a/ide/winvs/wolfsftp-client/wolfsftp-client.vcxproj b/ide/winvs/wolfsftp-client/wolfsftp-client.vcxproj index 8ed347f93..26125b088 100644 --- a/ide/winvs/wolfsftp-client/wolfsftp-client.vcxproj +++ b/ide/winvs/wolfsftp-client/wolfsftp-client.vcxproj @@ -346,7 +346,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDebug32) @@ -400,7 +400,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDllDebug32) @@ -418,7 +418,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDebug64) @@ -472,7 +472,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDllDebug64) @@ -491,7 +491,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptRelease32) @@ -531,7 +531,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptDllRelease32) @@ -571,7 +571,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptRelease64) @@ -611,7 +611,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptDllRelease64) diff --git a/ide/winvs/wolfssh/wolfssh.vcxproj b/ide/winvs/wolfssh/wolfssh.vcxproj index 0808a12e2..c5821eefd 100644 --- a/ide/winvs/wolfssh/wolfssh.vcxproj +++ b/ide/winvs/wolfssh/wolfssh.vcxproj @@ -365,7 +365,7 @@ Windows true $(wolfCryptDllDebug32) - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) @@ -428,7 +428,7 @@ Windows true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) $(wolfCryptDllDebug64) @@ -502,7 +502,7 @@ true true $(wolfCryptDllRelease32) - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) @@ -577,7 +577,7 @@ true true true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) $(wolfCryptDllRelease64) diff --git a/ide/winvs/wolfsshd/wolfsshd.vcxproj b/ide/winvs/wolfsshd/wolfsshd.vcxproj index 2b14feaa2..ea006b8c0 100644 --- a/ide/winvs/wolfsshd/wolfsshd.vcxproj +++ b/ide/winvs/wolfsshd/wolfsshd.vcxproj @@ -337,7 +337,7 @@ Console true ..\..\..\..\wolfssl\Debug\x64;..\Debug\x64 - wolfssl.lib;ws2_32.lib;secur32.lib;userenv.lib;$(CoreLibraryDependencies);%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;secur32.lib;userenv.lib;crypt32.lib;ncrypt.lib;$(CoreLibraryDependencies);%(AdditionalDependencies) @@ -385,7 +385,7 @@ true true true - wolfssl.lib;ws2_32.lib;secur32.lib;userenv.lib;$(CoreLibraryDependencies);%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;secur32.lib;userenv.lib;crypt32.lib;ncrypt.lib;$(CoreLibraryDependencies);%(AdditionalDependencies) ..\..\..\..\wolfssl\Release\x64;..\Release\x64 @@ -417,7 +417,7 @@ Level3 - wolfssl.lib;ws2_32.lib;secur32.lib;userenv.lib;$(CoreLibraryDependencies);%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;secur32.lib;userenv.lib;crypt32.lib;ncrypt.lib;$(CoreLibraryDependencies);%(AdditionalDependencies) $(wolfCryptDLLRelease64) true true diff --git a/src/certman.c b/src/certman.c index 922c0d04e..cb0db2567 100644 --- a/src/certman.c +++ b/src/certman.c @@ -44,6 +44,16 @@ #include #include +#ifdef WOLFSSH_WINDOWS_CERT_STORE + #include + #include + #ifndef CERT_SYSTEM_STORE_CURRENT_USER + #define CERT_SYSTEM_STORE_CURRENT_USER 0x00010000 + #endif + #ifndef CERT_SYSTEM_STORE_LOCAL_MACHINE + #define CERT_SYSTEM_STORE_LOCAL_MACHINE 0x00020000 + #endif +#endif #ifdef WOLFSSH_CERTS @@ -89,15 +99,24 @@ struct WOLFSSH_CERTMAN { */ int wolfSSH_SetCertManager(WOLFSSH_CTX* ctx, WOLFSSL_CERT_MANAGER* cm) { - if (ctx == NULL || cm == NULL) { + if (ctx == NULL || cm == NULL || ctx->certMan == NULL) { return WS_BAD_ARGUMENT; } + /* importing the manager already in use is a no-op */ + if (ctx->certMan->cm == cm) { + return WS_SUCCESS; + } + + if (wolfSSL_CertManager_up_ref(cm) != WOLFSSL_SUCCESS) { + WLOG(WS_LOG_CERTMAN, "Failed to increment cert manager reference"); + return WS_FATAL_ERROR; + } + /* free up existing cm if present */ - if (ctx->certMan != NULL && ctx->certMan->cm != NULL) { + if (ctx->certMan->cm != NULL) { wolfSSL_CertManagerFree(ctx->certMan->cm); } - wolfSSL_CertManager_up_ref(cm); ctx->certMan->cm = cm; return WS_SUCCESS; @@ -663,4 +682,112 @@ static int CheckProfile(DecodedCert* cert, int profile) } #endif /* WOLFSSH_NO_FPKI */ + +#ifdef WOLFSSH_WINDOWS_CERT_STORE +/* Parse a cert store spec string "store:subject:flags" into wide-string + * components. Allocates wStoreName and wSubjectName via WMALLOC; caller + * must WFREE them. dwFlags is set to the parsed flags value. + * Returns WS_SUCCESS on success. */ +int wolfSSH_ParseCertStoreSpec(const char* spec, + wchar_t** wStoreName, wchar_t** wSubjectName, + word32* dwFlags, void* heap) +{ + char* specCopy = NULL; + char* storeName = NULL; + char* subjectName = NULL; + char* flagsStr = NULL; + int wStoreNameLen, wSubjectNameLen; + size_t specLen; + + if (spec == NULL || wStoreName == NULL || wSubjectName == NULL || + dwFlags == NULL) { + return WS_BAD_ARGUMENT; + } + + *wStoreName = NULL; + *wSubjectName = NULL; + *dwFlags = CERT_SYSTEM_STORE_CURRENT_USER; + + specLen = WSTRLEN(spec) + 1; + specCopy = (char*)WMALLOC(specLen, heap, DYNTYPE_TEMP); + if (specCopy == NULL) + return WS_MEMORY_E; + WSTRNCPY(specCopy, spec, specLen); + + /* Parse "store:subject:flags" */ + storeName = specCopy; + subjectName = WSTRCHR(storeName, ':'); + if (subjectName != NULL) { + *subjectName++ = '\0'; + flagsStr = WSTRCHR(subjectName, ':'); + if (flagsStr != NULL) { + *flagsStr++ = '\0'; + if (*flagsStr == '\0') { + WFREE(specCopy, heap, DYNTYPE_TEMP); + return WS_BAD_ARGUMENT; + } + if (WSTRCMP(flagsStr, "CURRENT_USER") == 0) { + *dwFlags = CERT_SYSTEM_STORE_CURRENT_USER; + } + else if (WSTRCMP(flagsStr, "LOCAL_MACHINE") == 0) { + *dwFlags = CERT_SYSTEM_STORE_LOCAL_MACHINE; + } + else { + /* fall back to a raw numeric value; a result of 0 means the + * string was not a recognized name or valid number, which is + * never a usable store-location flag */ + *dwFlags = (word32)atoi(flagsStr); + if (*dwFlags == 0) { + WFREE(specCopy, heap, DYNTYPE_TEMP); + return WS_BAD_ARGUMENT; + } + } + } + } + + if (storeName == NULL || subjectName == NULL || *storeName == '\0' || + *subjectName == '\0') { + WFREE(specCopy, heap, DYNTYPE_TEMP); + return WS_BAD_ARGUMENT; + } + + /* Convert to wide strings */ + wStoreNameLen = MultiByteToWideChar(CP_UTF8, 0, storeName, -1, NULL, 0); + wSubjectNameLen = MultiByteToWideChar(CP_UTF8, 0, subjectName, -1, + NULL, 0); + + if (wStoreNameLen == 0 || wSubjectNameLen == 0) { + WFREE(specCopy, heap, DYNTYPE_TEMP); + return WS_FATAL_ERROR; + } + + *wStoreName = (wchar_t*)WMALLOC(wStoreNameLen * sizeof(wchar_t), + heap, DYNTYPE_TEMP); + *wSubjectName = (wchar_t*)WMALLOC(wSubjectNameLen * sizeof(wchar_t), + heap, DYNTYPE_TEMP); + + if (*wStoreName == NULL || *wSubjectName == NULL) { + if (*wStoreName != NULL) { + WFREE(*wStoreName, heap, DYNTYPE_TEMP); + *wStoreName = NULL; + } + if (*wSubjectName != NULL) { + WFREE(*wSubjectName, heap, DYNTYPE_TEMP); + *wSubjectName = NULL; + } + WFREE(specCopy, heap, DYNTYPE_TEMP); + return WS_MEMORY_E; + } + + MultiByteToWideChar(CP_UTF8, 0, storeName, -1, + *wStoreName, wStoreNameLen); + MultiByteToWideChar(CP_UTF8, 0, subjectName, -1, + *wSubjectName, wSubjectNameLen); + + WFREE(specCopy, heap, DYNTYPE_TEMP); + return WS_SUCCESS; +} +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ + + #endif /* WOLFSSH_CERTS */ diff --git a/src/internal.c b/src/internal.c index 72169832c..61e4b6ee9 100644 --- a/src/internal.c +++ b/src/internal.c @@ -65,6 +65,27 @@ #include #endif +#ifdef WOLFSSH_WINDOWS_CERT_STORE + #include + #include + #include + #ifndef CERT_SYSTEM_STORE_CURRENT_USER + #define CERT_SYSTEM_STORE_CURRENT_USER 0x00010000 + #endif + #ifndef CERT_SYSTEM_STORE_LOCAL_MACHINE + #define CERT_SYSTEM_STORE_LOCAL_MACHINE 0x00020000 + #endif + #ifndef CERT_NCRYPT_KEY_SPEC + #define CERT_NCRYPT_KEY_SPEC 0x00000003 + #endif + #ifndef BCRYPT_PAD_PKCS1 + #define BCRYPT_PAD_PKCS1 0x00000002 + #endif + +static int ExtractPubKeyDerFromCert(const byte* certDer, word32 certDerSz, + byte** outDer, word32* outDerSz, void* heap); +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ + #ifdef NO_INLINE #include #else @@ -1248,6 +1269,40 @@ WOLFSSH_CTX* CtxInit(WOLFSSH_CTX* ctx, byte side, void* heap) } +#ifdef WOLFSSH_WINDOWS_CERT_STORE +/* Release any MS Certificate Store state held by a private key slot and reset + * the cert-store fields so the slot is no longer treated as cert-store backed. + * Safe to call on a slot that never held cert-store state. */ +static void ClearCertStoreKey(WOLFSSH_CTX* ctx, WOLFSSH_PVT_KEY* pvtKey) +{ + if (pvtKey->certStoreContext != NULL) { + CertFreeCertificateContext((PCCERT_CONTEXT)pvtKey->certStoreContext); + pvtKey->certStoreContext = NULL; + } + if (pvtKey->storeName != NULL) { + WFREE(pvtKey->storeName, ctx->heap, DYNTYPE_STRING); + pvtKey->storeName = NULL; + } + if (pvtKey->subjectName != NULL) { + WFREE(pvtKey->subjectName, ctx->heap, DYNTYPE_STRING); + pvtKey->subjectName = NULL; + } + pvtKey->useCertStore = 0; +} + + +/* Returns 1 if the slot is genuinely backed by the MS Certificate Store. + * Requires a live cert context and no in-memory private key, so a slot that + * was later overwritten by a file-based key (which clears these) is not + * mistaken for a cert-store key. */ +static INLINE int IsCertStoreKey(const WOLFSSH_PVT_KEY* pvtKey) +{ + return pvtKey != NULL && pvtKey->useCertStore + && pvtKey->certStoreContext != NULL && pvtKey->key == NULL; +} +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ + + void CtxResourceFree(WOLFSSH_CTX* ctx) { WLOG(WS_LOG_DEBUG, "Entering CtxResourceFree()"); @@ -1268,6 +1323,9 @@ void CtxResourceFree(WOLFSSH_CTX* ctx) ctx->privateKey[i].cert = NULL; ctx->privateKey[i].certSz = 0; } +#ifdef WOLFSSH_WINDOWS_CERT_STORE + ClearCertStoreKey(ctx, &ctx->privateKey[i]); +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ #endif ctx->privateKey[i].publicKeyFmt = ID_NONE; } @@ -2139,7 +2197,7 @@ static int IdentifyCert(const byte* in, word32 inSz, void* heap) #endif /* WOLFSSH_CERTS */ -static void RefreshPublicKeyAlgo(WOLFSSH_CTX* ctx) +void RefreshPublicKeyAlgo(WOLFSSH_CTX* ctx) { WOLFSSH_PVT_KEY* key; byte* publicKeyAlgo = ctx->publicKeyAlgo; @@ -2185,7 +2243,7 @@ static void RefreshPublicKeyAlgo(WOLFSSH_CTX* ctx) #ifdef WOLFSSH_CERTS -static INLINE byte CertTypeForId(byte id) +WOLFSSH_LOCAL byte CertTypeForId(byte id) { switch (id) { #ifndef WOLFSSH_NO_SSH_RSA_SHA1 @@ -2365,6 +2423,12 @@ static int SetHostCertificate(WOLFSSH_CTX* ctx, pvtKey->publicKeyFmt = certId; } + #ifdef WOLFSSH_WINDOWS_CERT_STORE + /* A file-based certificate is replacing this slot's contents; drop + * any cert-store state so it is not mistaken for a cert-store key. */ + ClearCertStoreKey(ctx, pvtKey); + #endif + pvtKey->cert = der; pvtKey->certSz = derSz; @@ -2419,6 +2483,13 @@ static int SetHostPrivateKey(WOLFSSH_CTX* ctx, pvtKey->publicKeyFmt = keyId; } + #ifdef WOLFSSH_WINDOWS_CERT_STORE + /* This slot is now backed by an in-memory key; drop any cert-store + * state it may have carried so signing/K_S do not use a stale + * certificate context. */ + ClearCertStoreKey(ctx, pvtKey); + #endif + pvtKey->key = der; pvtKey->keySz = derSz; #ifdef WOLFSSH_TPM @@ -12825,6 +12896,9 @@ struct wolfSSH_sigKeyBlockFull { word32 pubKeyNameSz; const char *pubKeyFmtName; word32 pubKeyFmtNameSz; +#ifdef WOLFSSH_WINDOWS_CERT_STORE + const WOLFSSH_PVT_KEY* pvtKey; /* Pointer to private key for cert store support */ +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ union { #ifndef WOLFSSH_NO_RSA struct { @@ -13186,6 +13260,10 @@ static int SendKexGetSigningKey(WOLFSSH* ssh, #ifdef WOLFSSH_TPM ssh->handshake->useTpm = ssh->ctx->privateKey[keyIdx].isTpm; #endif +#ifdef WOLFSSH_WINDOWS_CERT_STORE + /* Set pointer to private key for cert store support */ + sigKeyBlock_ptr->pvtKey = &ssh->ctx->privateKey[keyIdx]; +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ /* Dispatches on pubKeyFmtId to sync with SendKexDhReply's free chain. * ID_RSA_SHA2_256/512 already collapse to ID_SSH_RSA. */ @@ -13197,26 +13275,75 @@ static int SendKexGetSigningKey(WOLFSSH* ssh, FALL_THROUGH; #endif case ID_SSH_RSA: - /* Decode the user-configured RSA private key. */ - sigKeyBlock_ptr->sk.rsa.eSz = - (word32)sizeof(sigKeyBlock_ptr->sk.rsa.e); - sigKeyBlock_ptr->sk.rsa.nSz = - (word32)sizeof(sigKeyBlock_ptr->sk.rsa.n); - ret = wc_InitRsaKey(&sigKeyBlock_ptr->sk.rsa.key, heap); - #ifdef WOLFSSH_TPM - if (ret == 0 && ssh->ctx->privateKey[keyIdx].isTpm) { - /* No private key in RAM; take the public key from the TPM. */ - ret = wolfTPM2_RsaKey_TpmToWolf(ssh->ctx->tpmDev, - ssh->ctx->tpmKey, &sigKeyBlock_ptr->sk.rsa.key); - if (ret != 0) - ret = WS_RSA_E; +#ifdef WOLFSSH_WINDOWS_CERT_STORE + /* Check if this is a cert store key */ + if (IsCertStoreKey(&ssh->ctx->privateKey[keyIdx])) { + /* For cert store keys, extract the RSA public key from the + * DER certificate so that wc_RsaFlattenPublicKey (below) + * can produce the correct e/n for the key-exchange hash, + * and so that wolfSSH_RsaVerify can self-check the + * signature. Signing will still use the cert store. */ + const byte* certDer = + ssh->ctx->privateKey[keyIdx].cert; + word32 certDerSz = + ssh->ctx->privateKey[keyIdx].certSz; + + sigKeyBlock_ptr->sk.rsa.eSz = + (word32)sizeof(sigKeyBlock_ptr->sk.rsa.e); + sigKeyBlock_ptr->sk.rsa.nSz = + (word32)sizeof(sigKeyBlock_ptr->sk.rsa.n); + ret = wc_InitRsaKey(&sigKeyBlock_ptr->sk.rsa.key, heap); + + if (ret == 0 && certDer != NULL && certDerSz > 0) { + byte* pubKeyDer = NULL; + word32 pubKeyDerSz = 0; + + ret = ExtractPubKeyDerFromCert(certDer, certDerSz, + &pubKeyDer, &pubKeyDerSz, heap); + if (ret == 0) { + word32 idx2 = 0; + ret = wc_RsaPublicKeyDecode(pubKeyDer, &idx2, + &sigKeyBlock_ptr->sk.rsa.key, pubKeyDerSz); + } + if (pubKeyDer != NULL) + WFREE(pubKeyDer, heap, DYNTYPE_PUBKEY); + + if (ret != 0) { + WLOG(WS_LOG_DEBUG, + "SendKexDhReply: cert store RSA pubkey " + "decode failed %d", ret); + ret = WS_CRYPTO_FAILED; + } + } + else if (ret == 0) { + WLOG(WS_LOG_DEBUG, + "SendKexDhReply: cert store key has no cert DER"); + ret = WS_BAD_ARGUMENT; + } + } else +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ + { + /* Decode the user-configured RSA private key. */ + sigKeyBlock_ptr->sk.rsa.eSz = + (word32)sizeof(sigKeyBlock_ptr->sk.rsa.e); + sigKeyBlock_ptr->sk.rsa.nSz = + (word32)sizeof(sigKeyBlock_ptr->sk.rsa.n); + ret = wc_InitRsaKey(&sigKeyBlock_ptr->sk.rsa.key, heap); + #ifdef WOLFSSH_TPM + if (ret == 0 && ssh->ctx->privateKey[keyIdx].isTpm) { + /* No private key in RAM; take the public key from the TPM. */ + ret = wolfTPM2_RsaKey_TpmToWolf(ssh->ctx->tpmDev, + ssh->ctx->tpmKey, &sigKeyBlock_ptr->sk.rsa.key); + if (ret != 0) + ret = WS_RSA_E; + } + else + #endif /* WOLFSSH_TPM */ + if (ret == 0) + ret = wc_RsaPrivateKeyDecode(ssh->ctx->privateKey[keyIdx].key, + &scratch, &sigKeyBlock_ptr->sk.rsa.key, + (int)ssh->ctx->privateKey[keyIdx].keySz); } - else - #endif /* WOLFSSH_TPM */ - if (ret == 0) - ret = wc_RsaPrivateKeyDecode(ssh->ctx->privateKey[keyIdx].key, - &scratch, &sigKeyBlock_ptr->sk.rsa.key, - (int)ssh->ctx->privateKey[keyIdx].keySz); /* hash in usual public key if not RFC6187 style cert use */ if (!isCert) { @@ -13332,6 +13459,45 @@ static int SendKexGetSigningKey(WOLFSSH* ssh, } else #endif /* WOLFSSH_TPM */ +#ifdef WOLFSSH_WINDOWS_CERT_STORE + if (ret == 0 && IsCertStoreKey(&ssh->ctx->privateKey[keyIdx])) { + /* For cert store keys, extract the ECC public key from the + * DER certificate. Signing uses the cert store handle via + * SignHEcdsa's cert-store branch. */ + const byte* certDer = + ssh->ctx->privateKey[keyIdx].cert; + word32 certDerSz = + ssh->ctx->privateKey[keyIdx].certSz; + + if (certDer != NULL && certDerSz > 0) { + byte* pubKeyDer = NULL; + word32 pubKeyDerSz = 0; + + ret = ExtractPubKeyDerFromCert(certDer, certDerSz, + &pubKeyDer, &pubKeyDerSz, heap); + if (ret == 0) { + word32 idx2 = 0; + ret = wc_EccPublicKeyDecode(pubKeyDer, &idx2, + &sigKeyBlock_ptr->sk.ecc.key, pubKeyDerSz); + } + if (pubKeyDer != NULL) + WFREE(pubKeyDer, heap, DYNTYPE_PUBKEY); + + if (ret != 0) { + WLOG(WS_LOG_DEBUG, + "SendKexDhReply: cert store ECC pubkey " + "decode failed %d", ret); + ret = WS_CRYPTO_FAILED; + } + } + else { + WLOG(WS_LOG_DEBUG, + "SendKexDhReply: cert store key has no cert DER"); + ret = WS_BAD_ARGUMENT; + } + } + else +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ if (ret == 0) ret = wc_EccPrivateKeyDecode(ssh->ctx->privateKey[keyIdx].key, &scratch, &sigKeyBlock_ptr->sk.ecc.key, @@ -14367,6 +14533,261 @@ static int KeyAgreeEcdhMlKem_server(WOLFSSH* ssh, byte hashId, #endif /* ML-KEM variants */ +#ifdef WOLFSSH_WINDOWS_CERT_STORE +/* Extract DER-encoded public key from a DER certificate. + * Caller must WFREE(*outDer, heap, DYNTYPE_PUBKEY) on success. + * Returns 0 on success. */ +static int ExtractPubKeyDerFromCert(const byte* certDer, word32 certDerSz, + byte** outDer, word32* outDerSz, void* heap) +{ + struct DecodedCert dCert; + byte* pubKeyDer = NULL; + word32 pubKeyDerSz = 0; + int ret; + + if (certDer == NULL || certDerSz == 0 || outDer == NULL || + outDerSz == NULL) { + return WS_BAD_ARGUMENT; + } + + wc_InitDecodedCert(&dCert, certDer, certDerSz, heap); + ret = wc_ParseCert(&dCert, CERT_TYPE, 0, NULL); + if (ret == 0) { + ret = wc_GetPubKeyDerFromCert(&dCert, NULL, &pubKeyDerSz); + if (ret == LENGTH_ONLY_E) { + ret = 0; + pubKeyDer = (byte*)WMALLOC(pubKeyDerSz, heap, DYNTYPE_PUBKEY); + if (pubKeyDer == NULL) + ret = WS_MEMORY_E; + } + } + if (ret == 0) + ret = wc_GetPubKeyDerFromCert(&dCert, pubKeyDer, &pubKeyDerSz); + wc_FreeDecodedCert(&dCert); + + if (ret == 0) { + *outDer = pubKeyDer; + *outDerSz = pubKeyDerSz; + } + else { + if (pubKeyDer != NULL) + WFREE(pubKeyDer, heap, DYNTYPE_PUBKEY); + } + + return ret; +} + + +#ifdef WOLFSSH_CERTS +/* Map a public key algorithm ID to the base key format ID stored in a + * private key slot's publicKeyFmt. The RSA signature variants and the + * X509 form collapse to ID_SSH_RSA, and the X509 ECDSA forms collapse to + * the matching plain curve ID. */ +static byte CertStoreBaseKeyId(byte id) +{ + byte baseId; + + baseId = id; + switch (id) { + case ID_RSA_SHA2_256: + case ID_RSA_SHA2_512: + case ID_X509V3_SSH_RSA: + baseId = ID_SSH_RSA; + break; + case ID_X509V3_ECDSA_SHA2_NISTP256: + baseId = ID_ECDSA_SHA2_NISTP256; + break; + case ID_X509V3_ECDSA_SHA2_NISTP384: + baseId = ID_ECDSA_SHA2_NISTP384; + break; + case ID_X509V3_ECDSA_SHA2_NISTP521: + baseId = ID_ECDSA_SHA2_NISTP521; + break; + } + + return baseId; +} + + +/* Find the cert-store-backed private key slot whose key type matches the + * public key algorithm keyId being used, so that a config holding both an + * RSA and an ECC cert-store key selects the correct slot. Returns NULL + * when no cert-store slot matches. */ +static const WOLFSSH_PVT_KEY* FindCertStoreKey(const WOLFSSH_CTX* ctx, + byte keyId) +{ + const WOLFSSH_PVT_KEY* pvtKey; + byte baseId; + word32 i; + + baseId = CertStoreBaseKeyId(keyId); + for (i = 0; i < ctx->privateKeyCount; i++) { + pvtKey = &ctx->privateKey[i]; + if (IsCertStoreKey(pvtKey) && + CertStoreBaseKeyId(pvtKey->publicKeyFmt) == baseId) { + return pvtKey; + } + } + + return NULL; +} +#endif /* WOLFSSH_CERTS */ + + +#ifndef WOLFSSH_NO_ECDSA +/* Convert an ECDSA signature from NCryptSignHash, which is raw r||s with + * each component exactly half of sigSz (not DER), into separate minimal + * mpint components with leading zeros trimmed. On input rSz and sSz hold + * the capacities of r and s; on output they hold the trimmed sizes. */ +static int CertStoreEccSigToRs(const byte* sig, word32 sigSz, + byte* r, word32* rSz, byte* s, word32* sSz) +{ + word32 halfSz; + word32 rOff, sOff; + int ret; + + halfSz = 0; + rOff = 0; + sOff = 0; + ret = WS_SUCCESS; + + if (sigSz < 2 || (sigSz & 1) != 0) { + WLOG(WS_LOG_DEBUG, "CertStoreEccSigToRs: Invalid signature size"); + ret = WS_ECC_E; + } + if (ret == WS_SUCCESS) { + halfSz = sigSz / 2; + if (halfSz > *rSz || halfSz > *sSz) { + WLOG(WS_LOG_DEBUG, "CertStoreEccSigToRs: Signature too large"); + ret = WS_ECC_E; + } + } + if (ret == WS_SUCCESS) { + /* Trim leading zeros so r and s are minimal mpints. */ + while (rOff < halfSz - 1 && sig[rOff] == 0) + rOff++; + while (sOff < halfSz - 1 && sig[halfSz + sOff] == 0) + sOff++; + WMEMCPY(r, sig + rOff, halfSz - rOff); + *rSz = halfSz - rOff; + WMEMCPY(s, sig + halfSz + sOff, halfSz - sOff); + *sSz = halfSz - sOff; + } + + return ret; +} +#endif /* !WOLFSSH_NO_ECDSA */ + + +/* Signing abstraction for MS Certificate Store support + * This function provides a clean abstraction for signing that can use + * either traditional keys or keys from the MS Certificate Store. + * For RSA, expects encoded signature (digest + OID) in digest parameter. + * For ECDSA, expects raw hash in digest parameter. + */ +static int SignWithCertStoreKey(WOLFSSH* ssh, + const WOLFSSH_PVT_KEY* pvtKey, + const byte* data, word32 dataSz, + enum wc_HashType hashId, + byte* sig, word32* sigSz) +{ + int ret = WS_SUCCESS; + PCCERT_CONTEXT pCertContext = NULL; + HCRYPTPROV_OR_NCRYPT_KEY_HANDLE hCryptProv = 0; + DWORD dwKeySpec = 0; + BOOL fCallerFreeProv = FALSE; + DWORD dwSigLen = 0; + SECURITY_STATUS nCryptRet = 0; + + WLOG(WS_LOG_DEBUG, "Entering SignWithCertStoreKey()"); + + /* hashId is no longer needed now that only the NCRYPT signing path + * (which derives the algorithm from the key/DigestInfo) is used. */ + WOLFSSH_UNUSED(ssh); + WOLFSSH_UNUSED(hashId); + + if (pvtKey == NULL || !pvtKey->useCertStore || + pvtKey->certStoreContext == NULL) { + WLOG(WS_LOG_DEBUG, "SignWithCertStoreKey: Not a cert store key"); + return WS_BAD_ARGUMENT; + } + + pCertContext = (PCCERT_CONTEXT)pvtKey->certStoreContext; + + /* Get the private key handle from the certificate. Only CNG/NCRYPT keys + * are supported (targets are Windows 10 and newer); legacy CryptoAPI/CSP + * keys are rejected here. */ + if (!CryptAcquireCertificatePrivateKey(pCertContext, + CRYPT_ACQUIRE_ONLY_NCRYPT_KEY_FLAG | CRYPT_ACQUIRE_SILENT_FLAG, + NULL, &hCryptProv, &dwKeySpec, &fCallerFreeProv)) { + DWORD dwErr = GetLastError(); + WLOG(WS_LOG_DEBUG, "SignWithCertStoreKey: Failed to acquire NCRYPT private key, error: %lu", dwErr); + return WS_CRYPTO_FAILED; + } + + /* Sign using CNG (Next Generation Crypto API). Only NCRYPT keys are + * acquired above, so dwKeySpec is always CERT_NCRYPT_KEY_SPEC here. */ + { + DWORD cbSignature = *sigSz; + + /* Determine padding and algorithm based on key type */ + if (pvtKey->publicKeyFmt == ID_SSH_RSA || + pvtKey->publicKeyFmt == ID_RSA_SHA2_256 || + pvtKey->publicKeyFmt == ID_RSA_SHA2_512 || + pvtKey->publicKeyFmt == ID_X509V3_SSH_RSA) { + /* RSA PKCS1 padding. + * The caller (SignHRsa) passes a DER-encoded DigestInfo + * (OID + hash) via wc_EncodeSignature(). Setting pszAlgId + * to NULL tells NCryptSignHash that the data is already a + * complete DigestInfo and should be placed directly into + * the PKCS#1 v1.5 block without further wrapping. + * If pszAlgId were non-NULL, NCryptSignHash would expect + * a raw hash and would construct DigestInfo internally, + * causing NTE_INVALID_PARAMETER (0x80090027). */ + BCRYPT_PKCS1_PADDING_INFO paddingInfo; + + WMEMSET(&paddingInfo, 0, sizeof(paddingInfo)); + paddingInfo.pszAlgId = NULL; + + nCryptRet = NCryptSignHash(hCryptProv, &paddingInfo, + (PBYTE)data, dataSz, sig, cbSignature, &dwSigLen, + BCRYPT_PAD_PKCS1); + } else if (pvtKey->publicKeyFmt == ID_ECDSA_SHA2_NISTP256 || + pvtKey->publicKeyFmt == ID_ECDSA_SHA2_NISTP384 || + pvtKey->publicKeyFmt == ID_ECDSA_SHA2_NISTP521 || + pvtKey->publicKeyFmt == ID_X509V3_ECDSA_SHA2_NISTP256 || + pvtKey->publicKeyFmt == ID_X509V3_ECDSA_SHA2_NISTP384 || + pvtKey->publicKeyFmt == ID_X509V3_ECDSA_SHA2_NISTP521) { + /* ECDSA - no padding */ + nCryptRet = NCryptSignHash(hCryptProv, NULL, + (PBYTE)data, dataSz, sig, cbSignature, &dwSigLen, 0); + } else { + WLOG(WS_LOG_DEBUG, "SignWithCertStoreKey: Unsupported key type"); + ret = WS_BAD_ARGUMENT; + } + + if (ret == WS_SUCCESS) { + if (nCryptRet != 0) { + WLOG(WS_LOG_DEBUG, "SignWithCertStoreKey: NCryptSignHash failed, error: 0x%08x", nCryptRet); + ret = WS_CRYPTO_FAILED; + } else { + *sigSz = dwSigLen; + ret = WS_SUCCESS; + } + } + } + + /* Free the key handle if we acquired it */ + if (fCallerFreeProv) { + NCryptFreeObject(hCryptProv); + } + + WLOG(WS_LOG_DEBUG, "Leaving SignWithCertStoreKey(), ret = %d", ret); + return ret; +} +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ + + static int SignHRsa(WOLFSSH* ssh, byte* sig, word32* sigSz, struct wolfSSH_sigKeyBlockFull *sigKey) #ifndef WOLFSSH_NO_RSA @@ -14427,23 +14848,41 @@ static int SignHRsa(WOLFSSH* ssh, byte* sig, word32* sigSz, ret = wolfTPM2_SignHashScheme(ssh->ctx->tpmDev, ssh->ctx->tpmKey, digest, (int)digestSz, sig, (int*)sigSz, TPM_ALG_RSASSA, TPM2_GetTpmHashType(hashId)); - if (ret == 0) - ret = (int)*sigSz; - else + if (ret == 0) { + ret = WS_SUCCESS; + } + else { + WLOG(WS_LOG_DEBUG, "SignHRsa: Bad TPM Sign"); ret = WS_RSA_E; + } } else #endif /* WOLFSSH_TPM */ - ret = wc_RsaSSL_Sign(encSig, encSigSz, sig, - KEX_SIG_SIZE, &sigKey->sk.rsa.key, - ssh->rng); - if (ret <= 0) { - WLOG(WS_LOG_DEBUG, "SignHRsa: Bad RSA Sign"); - ret = WS_RSA_E; + #ifdef WOLFSSH_WINDOWS_CERT_STORE + /* Check if this is a cert store key */ + if (IsCertStoreKey(sigKey->pvtKey)) { + /* Use cert store signing abstraction */ + ret = SignWithCertStoreKey(ssh, sigKey->pvtKey, encSig, encSigSz, + hashId, sig, sigSz); + if (ret != WS_SUCCESS) { + WLOG(WS_LOG_DEBUG, "SignHRsa: Cert store sign failed"); + } } - else { - *sigSz = (word32)ret; - ret = WS_SUCCESS; + else + #endif /* WOLFSSH_WINDOWS_CERT_STORE */ + { + /* Use traditional key signing */ + ret = wc_RsaSSL_Sign(encSig, encSigSz, sig, + KEX_SIG_SIZE, &sigKey->sk.rsa.key, + ssh->rng); + if (ret <= 0) { + WLOG(WS_LOG_DEBUG, "SignHRsa: Bad RSA Sign"); + ret = WS_RSA_E; + } + else { + *sigSz = (word32)ret; + ret = WS_SUCCESS; + } } } @@ -14452,8 +14891,23 @@ static int SignHRsa(WOLFSSH* ssh, byte* sig, word32* sigSz, && !ssh->handshake->useTpm #endif ) { - ret = wolfSSH_RsaVerify(sig, *sigSz, encSig, encSigSz, - &sigKey->sk.rsa.key, heap, "SignHRsa"); +#ifdef WOLFSSH_WINDOWS_CERT_STORE + /* For cert store keys the private key lives in the Windows cert + * store and the in-memory RsaKey may only contain the public + * half extracted from the certificate. The self-verify step + * still works because the public key was decoded from the cert + * in SendKexDhReply. */ + if (IsCertStoreKey(sigKey->pvtKey)) { + /* Verify using the public-key-only RsaKey decoded from + * the cert store certificate. */ + ret = wolfSSH_RsaVerify(sig, *sigSz, encSig, encSigSz, + &sigKey->sk.rsa.key, heap, "SignHRsa(certStore)"); + } else +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ + { + ret = wolfSSH_RsaVerify(sig, *sigSz, encSig, encSigSz, + &sigKey->sk.rsa.key, heap, "SignHRsa"); + } } WS_FORCEZERO(digest, sizeof(digest)); @@ -14525,20 +14979,48 @@ static int SignHEcdsa(WOLFSSH* ssh, byte* sig, word32* sigSz, WLOG(WS_LOG_INFO, "Signing hash with %s.", IdToName(ssh->handshake->pubKeyId)); #ifdef WOLFSSH_TPM - if (useTpm) + if (useTpm) { ret = wolfTPM2_SignHashScheme(ssh->ctx->tpmDev, ssh->ctx->tpmKey, digest, (int)digestSz, rawSig, (int*)&rawSigSz, TPM_ALG_ECDSA, TPM2_GetTpmHashType(hashId)); + if (ret != 0) { + WLOG(WS_LOG_DEBUG, "SignHEcdsa: Bad TPM Sign"); + ret = WS_ECC_E; + } + else { + ret = WS_SUCCESS; + } + } else #endif /* WOLFSSH_TPM */ - ret = wc_ecc_sign_hash(digest, digestSz, sig, sigSz, ssh->rng, - &sigKey->sk.ecc.key); - if (ret != 0) { - WLOG(WS_LOG_DEBUG, "SignHEcdsa: Bad ECDSA Sign"); - ret = WS_ECC_E; + #ifdef WOLFSSH_WINDOWS_CERT_STORE + /* Check if this is a cert store key */ + if (IsCertStoreKey(sigKey->pvtKey)) { + /* Use cert store signing abstraction - ECDSA uses raw hash. + * Note: unlike the RSA path, ECDSA does not self-verify here + * because NCryptSignHash returns raw r||s (not DER), and + * converting back for wc_ecc_verify_hash would add complexity. + * The key exchange hash comparison by the peer serves as + * the primary verification. */ + ret = SignWithCertStoreKey(ssh, sigKey->pvtKey, digest, digestSz, + hashId, sig, sigSz); + if (ret != WS_SUCCESS) { + WLOG(WS_LOG_DEBUG, "SignHEcdsa: Cert store sign failed"); + } } - else { - ret = WS_SUCCESS; + else + #endif /* WOLFSSH_WINDOWS_CERT_STORE */ + { + /* Use traditional key signing */ + ret = wc_ecc_sign_hash(digest, digestSz, sig, sigSz, ssh->rng, + &sigKey->sk.ecc.key); + if (ret != MP_OKAY) { + WLOG(WS_LOG_DEBUG, "SignHEcdsa: Bad ECDSA Sign"); + ret = WS_ECC_E; + } + else { + ret = WS_SUCCESS; + } } } @@ -14579,10 +15061,18 @@ static int SignHEcdsa(WOLFSSH* ssh, byte* sig, word32* sigSz, } else #endif /* WOLFSSH_TPM */ - ret = wc_ecc_sig_to_rs(sig, *sigSz, r, &rSz, s, &sSz); - - if (ret != 0) { - ret = WS_ECC_E; + #ifdef WOLFSSH_WINDOWS_CERT_STORE + /* NCryptSignHash for ECDSA returns raw r||s (each half of sigSz), + * NOT DER-encoded. Split directly. */ + if (IsCertStoreKey(sigKey->pvtKey)) { + ret = CertStoreEccSigToRs(sig, *sigSz, r, &rSz, s, &sSz); + } else +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ + { + ret = wc_ecc_sig_to_rs(sig, *sigSz, r, &rSz, s, &sSz); + if (ret != 0) { + ret = WS_ECC_E; + } } } @@ -16827,6 +17317,32 @@ static int PrepareUserAuthRequestRsaCert(WOLFSSH* ssh, word32* payloadSz, authData->sf.publicKey.publicKeySz); else #endif /* WOLFSSH_AGENT */ +#ifdef WOLFSSH_WINDOWS_CERT_STORE + /* Note: already inside #ifdef WOLFSSH_CERTS */ + if (authData->sf.publicKey.privateKey == NULL) { + /* Cert store: decode public key from the stored certificate */ + const WOLFSSH_PVT_KEY* pvtKey; + + pvtKey = FindCertStoreKey(ssh->ctx, keySig->keyId); + if (pvtKey == NULL || pvtKey->cert == NULL) { + ret = WS_BAD_ARGUMENT; + } + else { + byte* pubKeyDer = NULL; + word32 pubKeyDerSz = 0; + + ret = ExtractPubKeyDerFromCert(pvtKey->cert, pvtKey->certSz, + &pubKeyDer, &pubKeyDerSz, ssh->ctx->heap); + if (ret == 0) { + idx = 0; + ret = wc_RsaPublicKeyDecode(pubKeyDer, &idx, + &keySig->ks.rsa.key, pubKeyDerSz); + } + if (pubKeyDer != NULL) + WFREE(pubKeyDer, ssh->ctx->heap, DYNTYPE_PUBKEY); + } + } else +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ ret = wc_RsaPrivateKeyDecode(authData->sf.publicKey.privateKey, &idx, &keySig->ks.rsa.key, authData->sf.publicKey.privateKeySz); @@ -16954,17 +17470,59 @@ static int BuildUserAuthRequestRsaCert(WOLFSSH* ssh, if (ret == WS_SUCCESS) { int sigSz; WLOG(WS_LOG_INFO, "Signing hash with RSA."); - sigSz = wc_RsaSSL_Sign(encDigest, encDigestSz, - output + begin, keySig->sigSz, - &keySig->ks.rsa.key, ssh->rng); - if (sigSz <= 0 || (word32)sigSz != keySig->sigSz) { - WLOG(WS_LOG_DEBUG, "SUAR: Bad RSA Sign"); - ret = WS_RSA_E; - } - else { - ret = wolfSSH_RsaVerify(output + begin, keySig->sigSz, - encDigest, encDigestSz, &keySig->ks.rsa.key, - ssh->ctx->heap, "SUAR"); +#ifdef WOLFSSH_WINDOWS_CERT_STORE + if (authData->sf.publicKey.privateKey == NULL) { + /* Cert store: sign with NCryptSignHash via + * SignWithCertStoreKey (pszAlgId=NULL, data is + * the already-encoded DigestInfo). */ + const WOLFSSH_PVT_KEY* pvtKey; + + pvtKey = FindCertStoreKey(ssh->ctx, keySig->keyId); + if (pvtKey != NULL) { + word32 outSigSz = keySig->sigSz; + ret = SignWithCertStoreKey(ssh, pvtKey, + encDigest, encDigestSz, hashId, + output + begin, &outSigSz); + if (ret == WS_SUCCESS) { + sigSz = (int)outSigSz; + if (sigSz <= 0 || + (word32)sigSz != keySig->sigSz) { + WLOG(WS_LOG_DEBUG, + "SUAR: Cert store RSA sig length mismatch"); + ret = WS_RSA_E; + } + else { + ret = wolfSSH_RsaVerify(output + begin, + outSigSz, encDigest, encDigestSz, + &keySig->ks.rsa.key, ssh->ctx->heap, + "SUAR(certStore)"); + } + } else { + WLOG(WS_LOG_DEBUG, + "SUAR: Cert store RSA sign failed"); + ret = WS_RSA_E; + } + } else { + WLOG(WS_LOG_DEBUG, + "SUAR: Cert store key not found for RSA"); + ret = WS_BAD_ARGUMENT; + } + } else +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ + { + sigSz = wc_RsaSSL_Sign(encDigest, encDigestSz, + output + begin, keySig->sigSz, + &keySig->ks.rsa.key, ssh->rng); + if (sigSz <= 0 || (word32)sigSz != keySig->sigSz) { + WLOG(WS_LOG_DEBUG, "SUAR: Bad RSA Sign"); + ret = WS_RSA_E; + } + else { + ret = wolfSSH_RsaVerify(output + begin, + keySig->sigSz, encDigest, encDigestSz, + &keySig->ks.rsa.key, ssh->ctx->heap, + "SUAR"); + } } } @@ -17289,29 +17847,60 @@ static int PrepareUserAuthRequestEccCert(WOLFSSH* ssh, word32* payloadSz, if (ret == WS_SUCCESS) { word32 idx = 0; +#ifdef WOLFSSH_WINDOWS_CERT_STORE + /* Note: already inside #ifdef WOLFSSH_CERTS. + * Cert store: no in-memory private key — decode public key from + * the DER certificate that UsePrivateKey_fromStore saved. */ + if (authData->sf.publicKey.privateKey == NULL) { + const WOLFSSH_PVT_KEY* pvtKey; + + pvtKey = FindCertStoreKey(ssh->ctx, keySig->keyId); + if (pvtKey == NULL || pvtKey->cert == NULL) { + ret = WS_BAD_ARGUMENT; + } + else { + byte* pubKeyDer = NULL; + word32 pubKeyDerSz = 0; + + ret = ExtractPubKeyDerFromCert(pvtKey->cert, pvtKey->certSz, + &pubKeyDer, &pubKeyDerSz, ssh->ctx->heap); + if (ret == 0) { + idx = 0; + ret = wc_EccPublicKeyDecode(pubKeyDer, &idx, + &keySig->ks.ecc.key, pubKeyDerSz); + } + if (pubKeyDer != NULL) + WFREE(pubKeyDer, ssh->ctx->heap, DYNTYPE_PUBKEY); + } + } else +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ + { #if 0 #ifdef WOLFSSH_AGENT - if (ssh->agentEnabled) { - word32 sz; - const byte* c = (const byte*)authData->sf.publicKey.publicKey; - - ato32(c + idx, &sz); - idx += LENGTH_SZ + sz; - ato32(c + idx, &sz); - idx += LENGTH_SZ + sz; - ato32(c + idx, &sz); - idx += LENGTH_SZ; - c += idx; - idx = 0; + if (ssh->agentEnabled) { + word32 sz; + const byte* c = + (const byte*)authData->sf.publicKey.publicKey; + + ato32(c + idx, &sz); + idx += LENGTH_SZ + sz; + ato32(c + idx, &sz); + idx += LENGTH_SZ + sz; + ato32(c + idx, &sz); + idx += LENGTH_SZ; + c += idx; + idx = 0; - ret = wc_ecc_import_x963(c, sz, &keySig->ks.ecc.key); - } - else + ret = wc_ecc_import_x963(c, sz, &keySig->ks.ecc.key); + } + else #endif #endif - ret = wc_EccPrivateKeyDecode(authData->sf.publicKey.privateKey, - &idx, &keySig->ks.ecc.key, - authData->sf.publicKey.privateKeySz); + ret = wc_EccPrivateKeyDecode( + authData->sf.publicKey.privateKey, + &idx, &keySig->ks.ecc.key, + authData->sf.publicKey.privateKeySz); + } } if (ret == WS_SUCCESS) { @@ -17412,22 +18001,58 @@ static int BuildUserAuthRequestEccCert(WOLFSSH* ssh, ret = HashUpdate(&hash, hashId, checkData, checkDataSz); if (ret == WS_SUCCESS) ret = wc_HashFinal(&hash, hashId, digest); - if (ret == WS_SUCCESS) - ret = wc_ecc_sign_hash(digest, digestSz, sig, &sigSz, - ssh->rng, &keySig->ks.ecc.key); + wc_HashFree(&hash, hashId); + } + } + +#ifdef WOLFSSH_WINDOWS_CERT_STORE + /* Cert store signing: NCryptSignHash returns raw r||s */ + if (ret == WS_SUCCESS && + authData->sf.publicKey.privateKey == NULL) { + const WOLFSSH_PVT_KEY* pvtKey; + + pvtKey = FindCertStoreKey(ssh->ctx, keySig->keyId); + if (pvtKey != NULL) { + ret = SignWithCertStoreKey(ssh, pvtKey, + digest, digestSz, hashId, sig, &sigSz); + if (ret == WS_SUCCESS) { + /* NCryptSignHash ECDSA output is raw r||s, each + * component is half the total signature size. */ + rSz = sSz = (word32)sizeof(rs) / 2; + r = rs; + s = rs + rSz; + ret = CertStoreEccSigToRs(sig, sigSz, r, &rSz, s, &sSz); + if (ret != WS_SUCCESS) { + WLOG(WS_LOG_DEBUG, + "SUAR: Bad cert store ECC signature"); + } + } else { + WLOG(WS_LOG_DEBUG, "SUAR: Cert store ECC sign failed"); + ret = WS_ECC_E; + } + } else { + WLOG(WS_LOG_DEBUG, + "SUAR: Cert store key not found for ECC"); + ret = WS_BAD_ARGUMENT; + } + } else +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ + { + if (ret == WS_SUCCESS) { + ret = wc_ecc_sign_hash(digest, digestSz, sig, &sigSz, + ssh->rng, &keySig->ks.ecc.key); if (ret != WS_SUCCESS) { WLOG(WS_LOG_DEBUG, "SUAR: Bad ECC Cert Sign"); ret = WS_ECC_E; } - wc_HashFree(&hash, hashId); } - } - if (ret == WS_SUCCESS) { - rSz = sSz = (word32)sizeof(rs) / 2; - r = rs; - s = rs + rSz; - ret = wc_ecc_sig_to_rs(sig, sigSz, r, &rSz, s, &sSz); + if (ret == WS_SUCCESS) { + rSz = sSz = (word32)sizeof(rs) / 2; + r = rs; + s = rs + rSz; + ret = wc_ecc_sig_to_rs(sig, sigSz, r, &rSz, s, &sSz); + } } if (ret == WS_SUCCESS) { diff --git a/src/ssh.c b/src/ssh.c index 44d17b816..ced0e24b3 100644 --- a/src/ssh.c +++ b/src/ssh.c @@ -35,6 +35,17 @@ #include #include +#ifdef WOLFSSH_WINDOWS_CERT_STORE + #include + #include + #include + #include + #include + #ifndef CERT_NCRYPT_KEY_SPEC + #define CERT_NCRYPT_KEY_SPEC 0x00000003 + #endif +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ + #ifdef NO_INLINE #include #else @@ -2829,6 +2840,374 @@ int wolfSSH_CTX_AddRootCert_buffer(WOLFSSH_CTX* ctx, return ret; } +#ifdef WOLFSSH_WINDOWS_CERT_STORE +/* Find the certificate in hStore whose Common Name exactly matches + * subjectName. subjectName may include a leading "CN=" prefix. + * CERT_FIND_SUBJECT_STR_W is only used as a substring pre-filter to + * enumerate candidates; each candidate's CN is then compared exactly so + * that a lookup for "server1" does not select "server1.example" or + * "myserver1". Returns the certificate context (caller must free with + * CertFreeCertificateContext) or NULL when no exact match exists. */ +static PCCERT_CONTEXT FindCertByExactCN(HCERTSTORE hStore, + const wchar_t* subjectName) +{ + PCCERT_CONTEXT pCertContext; + const wchar_t* cn; + wchar_t* certCn; + DWORD certCnSz; + int match; + + /* Strip an optional "CN=" prefix from the requested name. */ + cn = subjectName; + if (wcslen(cn) > 3 && + (wcsncmp(cn, L"CN=", 3) == 0 || wcsncmp(cn, L"cn=", 3) == 0)) { + cn = cn + 3; + } + + pCertContext = NULL; + for (;;) { + /* Passing the previous context frees it and continues the search. */ + pCertContext = CertFindCertificateInStore(hStore, + X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, + 0, CERT_FIND_SUBJECT_STR_W, cn, pCertContext); + if (pCertContext == NULL) { + break; + } + certCnSz = CertGetNameStringW(pCertContext, CERT_NAME_ATTR_TYPE, 0, + (void*)szOID_COMMON_NAME, NULL, 0); + if (certCnSz <= 1) { + continue; + } + certCn = (wchar_t*)WMALLOC(certCnSz * sizeof(wchar_t), NULL, + DYNTYPE_TEMP); + if (certCn == NULL) { + CertFreeCertificateContext(pCertContext); + pCertContext = NULL; + break; + } + certCnSz = CertGetNameStringW(pCertContext, CERT_NAME_ATTR_TYPE, 0, + (void*)szOID_COMMON_NAME, certCn, certCnSz); + match = (certCnSz > 1 && wcscmp(certCn, cn) == 0); + WFREE(certCn, NULL, DYNTYPE_TEMP); + if (match) { + break; + } + } + + return pCertContext; +} + + +/* Fill the private key slot for keyId with cert-store backed state. Any + * existing file-based or cert-store resources in the slot are replaced. + * The slot takes its own reference on pCertContext and its own copies of + * the name strings and certificate DER so that every slot can be freed + * independently by CtxResourceFree. On failure the slot and + * ctx->privateKeyCount are left unchanged. + * Returns WS_SUCCESS on success. */ +static int UseCertStoreSlot(WOLFSSH_CTX* ctx, byte keyId, + PCCERT_CONTEXT pCertContext, const wchar_t* storeName, + const wchar_t* subjectName, word32 dwFlags) +{ + WOLFSSH_PVT_KEY* pvtKey; + PCCERT_CONTEXT slotContext; + wchar_t* storeNameCopy; + wchar_t* subjectNameCopy; + byte* certBuf; + size_t storeNameLen; + size_t subjectNameLen; + word32 certSz; + word32 keyIdx; + word32 i; + void* heap; + + heap = ctx->heap; + + /* Find an existing slot of the same type or an available new slot */ + keyIdx = WOLFSSH_MAX_PVT_KEYS; + for (i = 0; i < ctx->privateKeyCount && i < WOLFSSH_MAX_PVT_KEYS; i++) { + if (ctx->privateKey[i].publicKeyFmt == keyId) { + keyIdx = i; + break; + } + } + if (keyIdx == WOLFSSH_MAX_PVT_KEYS + && ctx->privateKeyCount >= WOLFSSH_MAX_PVT_KEYS) { + WLOG(WS_LOG_DEBUG, "UseCertStoreSlot: No available key slot"); + return WS_CTX_KEY_COUNT_E; + } + + /* Allocate every new resource before modifying the slot so a failure + * leaves the context untouched. */ + storeNameLen = wcslen(storeName) + 1; + subjectNameLen = wcslen(subjectName) + 1; + certSz = pCertContext->cbCertEncoded; + storeNameCopy = (wchar_t*)WMALLOC(storeNameLen * sizeof(wchar_t), + heap, DYNTYPE_STRING); + subjectNameCopy = (wchar_t*)WMALLOC(subjectNameLen * sizeof(wchar_t), + heap, DYNTYPE_STRING); + certBuf = (byte*)WMALLOC(certSz, heap, DYNTYPE_CERT); + if (storeNameCopy == NULL || subjectNameCopy == NULL || certBuf == NULL) { + if (storeNameCopy != NULL) + WFREE(storeNameCopy, heap, DYNTYPE_STRING); + if (subjectNameCopy != NULL) + WFREE(subjectNameCopy, heap, DYNTYPE_STRING); + if (certBuf != NULL) + WFREE(certBuf, heap, DYNTYPE_CERT); + WLOG(WS_LOG_DEBUG, "UseCertStoreSlot: Memory allocation failed"); + return WS_MEMORY_E; + } + WMEMCPY(storeNameCopy, storeName, storeNameLen * sizeof(wchar_t)); + WMEMCPY(subjectNameCopy, subjectName, subjectNameLen * sizeof(wchar_t)); + WMEMCPY(certBuf, pCertContext->pbCertEncoded, certSz); + + /* Each slot holds its own reference on the certificate context */ + slotContext = CertDuplicateCertificateContext(pCertContext); + if (slotContext == NULL) { + WFREE(storeNameCopy, heap, DYNTYPE_STRING); + WFREE(subjectNameCopy, heap, DYNTYPE_STRING); + WFREE(certBuf, heap, DYNTYPE_CERT); + WLOG(WS_LOG_DEBUG, "Failed CertDuplicateCertificateContext"); + return WS_FATAL_ERROR; + } + + /* if no existing matching key id was found append the key to the end */ + if (keyIdx == WOLFSSH_MAX_PVT_KEYS) { + keyIdx = ctx->privateKeyCount; + ctx->privateKeyCount++; + } + pvtKey = &ctx->privateKey[keyIdx]; + + /* Free existing resources if replacing an existing slot. The slot may + * previously have held either a cert-store key or a file-based + * key/cert, so clear both kinds of resources. */ + if (pvtKey->certStoreContext != NULL) { + CertFreeCertificateContext( + (PCCERT_CONTEXT)pvtKey->certStoreContext); + pvtKey->certStoreContext = NULL; + } + if (pvtKey->storeName != NULL) { + WFREE(pvtKey->storeName, heap, DYNTYPE_STRING); + pvtKey->storeName = NULL; + } + if (pvtKey->subjectName != NULL) { + WFREE(pvtKey->subjectName, heap, DYNTYPE_STRING); + pvtKey->subjectName = NULL; + } + if (pvtKey->key != NULL) { + WS_FORCEZERO(pvtKey->key, pvtKey->keySz); + WFREE(pvtKey->key, heap, DYNTYPE_PRIVKEY); + pvtKey->key = NULL; + pvtKey->keySz = 0; + } + if (pvtKey->cert != NULL) { + WFREE(pvtKey->cert, heap, DYNTYPE_CERT); + pvtKey->cert = NULL; + pvtKey->certSz = 0; + } + + /* Set up the private key structure */ + pvtKey->publicKeyFmt = keyId; + pvtKey->useCertStore = 1; + pvtKey->certStoreContext = (void*)slotContext; + pvtKey->storeName = storeNameCopy; + pvtKey->subjectName = subjectNameCopy; + pvtKey->dwFlags = dwFlags; + pvtKey->cert = certBuf; + pvtKey->certSz = certSz; + + return WS_SUCCESS; +} + + +/* Load a private key from MS Certificate Store + * storeName: Certificate store name (e.g., L"My", L"Root") + * dwFlags: Certificate store flags (e.g., CERT_SYSTEM_STORE_CURRENT_USER) + * subjectName: Certificate subject Common Name for lookup, with or without + * a "CN=" prefix. The CN must match exactly; thumbprint lookup is not + * currently implemented. + * The key is registered both as its plain key type and, mirroring the + * file-based HostKey plus HostCertificate pairing, as the matching + * RFC6187 x509v3-* type so the store certificate itself can be sent as + * the public host key to peers that negotiate certificate algorithms. + * returns WS_SUCCESS on success + */ +int wolfSSH_CTX_UsePrivateKey_fromStore(WOLFSSH_CTX* ctx, + const wchar_t* storeName, word32 dwFlags, + const wchar_t* subjectName) +{ + int ret = WS_SUCCESS; + HCERTSTORE hStore = NULL; + PCCERT_CONTEXT pCertContext = NULL; + byte keyId = ID_NONE; + PCERT_PUBLIC_KEY_INFO pPubKeyInfo = NULL; + + WLOG(WS_LOG_DEBUG, "Entering wolfSSH_CTX_UsePrivateKey_fromStore()"); + + if (ctx == NULL || storeName == NULL || subjectName == NULL) { + WLOG(WS_LOG_DEBUG, "wolfSSH_CTX_UsePrivateKey_fromStore: Bad argument"); + return WS_BAD_ARGUMENT; + } + + /* Open the certificate store */ + hStore = CertOpenStore(CERT_STORE_PROV_SYSTEM_W, 0, (HCRYPTPROV_LEGACY)0, + (DWORD)dwFlags | CERT_STORE_OPEN_EXISTING_FLAG, storeName); + if (hStore == NULL) { + DWORD dwErr = GetLastError(); + WLOG(WS_LOG_DEBUG, "wolfSSH_CTX_UsePrivateKey_fromStore: Failed to open store, error: %lu", dwErr); + return WS_FATAL_ERROR; + } + + /* Find the certificate by exact Common Name match. */ + pCertContext = FindCertByExactCN(hStore, subjectName); + + if (pCertContext == NULL) { + CertCloseStore(hStore, 0); + WLOG(WS_LOG_ERROR, "wolfSSH_CTX_UsePrivateKey_fromStore: Certificate " + "not found with subject '%ls'", subjectName); + return WS_FATAL_ERROR; + } + + /* Determine key type from certificate */ + /* Get the public key info to determine algorithm */ + pPubKeyInfo = &pCertContext->pCertInfo->SubjectPublicKeyInfo; + + /* Check algorithm OID to determine key type */ + if (pPubKeyInfo->Algorithm.pszObjId != NULL) { + /* Compare OID strings (they are ASCII, not wide) */ + if (strcmp(pPubKeyInfo->Algorithm.pszObjId, szOID_RSA_RSA) == 0 || + strcmp(pPubKeyInfo->Algorithm.pszObjId, szOID_RSA_ENCRYPT) == 0) { + keyId = ID_SSH_RSA; + } + else if (strcmp(pPubKeyInfo->Algorithm.pszObjId, szOID_ECC_PUBLIC_KEY) == 0) { + /* Decode the curve OID from the algorithm parameters to select + * the correct ECDSA key type. The Parameters field contains + * a DER-encoded OID identifying the named curve. */ + char* curveOid = NULL; + DWORD curveOidSz = 0; + + if (pPubKeyInfo->Algorithm.Parameters.cbData > 0 && + CryptDecodeObjectEx(X509_ASN_ENCODING, + X509_OBJECT_IDENTIFIER, + pPubKeyInfo->Algorithm.Parameters.pbData, + pPubKeyInfo->Algorithm.Parameters.cbData, + CRYPT_DECODE_ALLOC_FLAG, NULL, + &curveOid, &curveOidSz)) { + /* Compare against well-known curve OIDs */ + if (strcmp(curveOid, "1.2.840.10045.3.1.7") == 0) { + keyId = ID_ECDSA_SHA2_NISTP256; + } + else if (strcmp(curveOid, "1.3.132.0.34") == 0) { + keyId = ID_ECDSA_SHA2_NISTP384; + } + else if (strcmp(curveOid, "1.3.132.0.35") == 0) { + keyId = ID_ECDSA_SHA2_NISTP521; + } + else { + WLOG(WS_LOG_DEBUG, + "wolfSSH_CTX_UsePrivateKey_fromStore: " + "Unrecognized ECC curve OID: %s, " + "defaulting to P-256", curveOid); + keyId = ID_ECDSA_SHA2_NISTP256; + } + LocalFree(curveOid); + } + else { + WLOG(WS_LOG_DEBUG, + "wolfSSH_CTX_UsePrivateKey_fromStore: " + "Failed to decode ECC curve parameters, " + "defaulting to P-256"); + keyId = ID_ECDSA_SHA2_NISTP256; + } + } + else { + CertFreeCertificateContext(pCertContext); + CertCloseStore(hStore, 0); + WLOG(WS_LOG_DEBUG, "wolfSSH_CTX_UsePrivateKey_fromStore: Unsupported key algorithm: %s", pPubKeyInfo->Algorithm.pszObjId); + return WS_BAD_ARGUMENT; + } + } + else { + CertFreeCertificateContext(pCertContext); + CertCloseStore(hStore, 0); + WLOG(WS_LOG_DEBUG, "wolfSSH_CTX_UsePrivateKey_fromStore: No algorithm OID"); + return WS_BAD_ARGUMENT; + } + + /* Verify private key is accessible before registering the key. + * This catches permission issues early (e.g., LocalSystem service + * cannot access the private key) rather than failing later during + * SSH handshake signing. */ + { + HCRYPTPROV_OR_NCRYPT_KEY_HANDLE hKey = 0; + DWORD dwKeySpec = 0; + BOOL fCallerFree = FALSE; + + /* Require a CNG/NCRYPT key. Legacy CryptoAPI/CSP keys are not + * supported; targets are Windows 10 and newer. */ + if (!CryptAcquireCertificatePrivateKey(pCertContext, + CRYPT_ACQUIRE_ONLY_NCRYPT_KEY_FLAG | CRYPT_ACQUIRE_SILENT_FLAG, + NULL, &hKey, &dwKeySpec, &fCallerFree)) { + DWORD dwErr = GetLastError(); + WLOG(WS_LOG_ERROR, "wolfSSH_CTX_UsePrivateKey_fromStore: Cannot " + "access private key, error: %lu. Check that the current user " + "or service account has permission to access the key.", dwErr); + CertFreeCertificateContext(pCertContext); + CertCloseStore(hStore, 0); + return WS_CRYPTO_FAILED; + } + /* Release the key handle since we just needed to verify access */ + if (fCallerFree) { + if (dwKeySpec == CERT_NCRYPT_KEY_SPEC) { + NCryptFreeObject(hKey); + } + else { + CryptReleaseContext(hKey, 0); + } + } + WLOG(WS_LOG_DEBUG, "wolfSSH_CTX_UsePrivateKey_fromStore: Private key " + "access verified successfully"); + } + + /* Register the key under its plain type so peers without RFC6187 + * support get a raw public key, and under the matching X.509 type so + * the store certificate can be sent as K_S when a peer negotiates an + * x509v3-* algorithm. On failure of the second registration the first + * slot stays in the context; it is fully owned by the context and is + * released by CtxResourceFree. */ + ret = UseCertStoreSlot(ctx, keyId, pCertContext, storeName, subjectName, + dwFlags); + if (ret == WS_SUCCESS) { + byte certId; + + certId = CertTypeForId(keyId); + /* CertTypeForId returns keyId unchanged when no X509 equivalent was + * found; skip adding the X509 ID slot in that case. */ + if (certId != keyId) { + ret = UseCertStoreSlot(ctx, certId, pCertContext, storeName, + subjectName, dwFlags); + } + } + + /* Each registered slot holds its own reference on the certificate + * context for later signing operations, so release the lookup + * reference from CertFindCertificateInStore. Closing the store does + * not invalidate the slot contexts. + * Note: if the certificate is removed from the store while we hold + * these contexts, CryptAcquireCertificatePrivateKey may fail at + * signing time. */ + CertFreeCertificateContext(pCertContext); + CertCloseStore(hStore, 0); + + if (ret == WS_SUCCESS) { + /* Refresh public key algorithm list */ + RefreshPublicKeyAlgo(ctx); + } + + WLOG(WS_LOG_DEBUG, "Leaving wolfSSH_CTX_UsePrivateKey_fromStore(), ret = %d", ret); + return ret; +} +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ #endif /* WOLFSSH_CERTS */ diff --git a/tests/unit.c b/tests/unit.c index f4693ed57..ec29f7690 100644 --- a/tests/unit.c +++ b/tests/unit.c @@ -56,12 +56,29 @@ !defined(NO_FILESYSTEM) #define WOLFSSH_TEST_CERTMAN_PROMOTE /* The certman helpers use malloc/free and LONG_MAX; pull these in here so - * the tests build even when the SCP block below is not compiled. */ + * the tests build even when the SCP block below is not compiled. + * certman.h itself comes from the WOLFSSH_CERTS block below. */ #include #include #include #include +#endif + +#ifdef WOLFSSH_CERTS #include + #include +#endif + +#ifdef WOLFSSH_WINDOWS_CERT_STORE + #include + #include + #include + #ifndef CERT_SYSTEM_STORE_CURRENT_USER + #define CERT_SYSTEM_STORE_CURRENT_USER 0x00010000 + #endif + #ifndef CERT_SYSTEM_STORE_LOCAL_MACHINE + #define CERT_SYSTEM_STORE_LOCAL_MACHINE 0x00020000 + #endif #endif #ifdef WOLFSSH_SFTP @@ -9986,6 +10003,178 @@ static int test_CertMan_PromoteValidCaIntermediate(void) #endif /* WOLFSSH_TEST_CERTMAN_PROMOTE */ +#ifdef WOLFSSH_CERTS +/* wolfSSH_SetCertManager imports a WOLFSSL_CERT_MANAGER by reference into + * the wolfSSH context. Test argument checking, importing the same manager + * twice, replacing an already-imported manager, and the reference count + * that keeps the manager alive after the WOLFSSL_CTX that created it is + * freed (a missing reference shows up as a use-after-free/double-free + * under the sanitizer builds). */ +static int test_SetCertManager(void) +{ + int result = 0; + WOLFSSH_CTX* ctx = NULL; + WOLFSSL_CTX* sslCtx = NULL; + WOLFSSL_CTX* sslCtx2 = NULL; + WOLFSSL_CERT_MANAGER* cm = NULL; + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + if (ctx == NULL) + result = -1; + + if (result == 0) { + sslCtx = wolfSSL_CTX_new(wolfSSLv23_server_method()); + if (sslCtx == NULL) + result = -2; + } + + /* bad arguments */ + if (result == 0) { + cm = wolfSSL_CTX_GetCertManager(sslCtx); + if (cm == NULL) + result = -3; + } + if (result == 0 && wolfSSH_SetCertManager(NULL, cm) != WS_BAD_ARGUMENT) + result = -4; + if (result == 0 && wolfSSH_SetCertManager(ctx, NULL) != WS_BAD_ARGUMENT) + result = -5; + + /* import, then import the same manager again */ + if (result == 0 && wolfSSH_SetCertManager(ctx, cm) != WS_SUCCESS) + result = -6; + if (result == 0 && wolfSSH_SetCertManager(ctx, cm) != WS_SUCCESS) + result = -7; + + /* the context must hold its own reference: freeing the WOLFSSL_CTX + * that created the manager must leave the imported manager usable */ + if (result == 0) { + wolfSSL_CTX_free(sslCtx); + sslCtx = NULL; + } + + /* replace the imported manager with one from a second WOLFSSL_CTX, + * releasing the reference on the first manager */ + if (result == 0) { + sslCtx2 = wolfSSL_CTX_new(wolfSSLv23_server_method()); + if (sslCtx2 == NULL) + result = -8; + } + if (result == 0) { + cm = wolfSSL_CTX_GetCertManager(sslCtx2); + if (cm == NULL) + result = -9; + else if (wolfSSH_SetCertManager(ctx, cm) != WS_SUCCESS) + result = -10; + } + + if (sslCtx != NULL) + wolfSSL_CTX_free(sslCtx); + if (sslCtx2 != NULL) + wolfSSL_CTX_free(sslCtx2); + if (ctx != NULL) + wolfSSH_CTX_free(ctx); + + return result; +} +#endif /* WOLFSSH_CERTS */ + +#ifdef WOLFSSH_WINDOWS_CERT_STORE +/* Check one wolfSSH_ParseCertStoreSpec call against expected results. + * expRet is the expected return value; the name/flag expectations are only + * checked when expRet is WS_SUCCESS. */ +static int certStoreSpecCheck(const char* spec, int expRet, + const wchar_t* expStore, const wchar_t* expSubject, word32 expFlags) +{ + int ret; + int result = 0; + wchar_t* wStoreName = NULL; + wchar_t* wSubjectName = NULL; + word32 dwFlags = 0; + + ret = wolfSSH_ParseCertStoreSpec(spec, &wStoreName, &wSubjectName, + &dwFlags, NULL); + if (ret != expRet) { + printf("ParseCertStoreSpec(%s): ret %d, expected %d\n", + spec != NULL ? spec : "(null)", ret, expRet); + result = -1; + } + if (result == 0 && ret == WS_SUCCESS) { + if (wcscmp(wStoreName, expStore) != 0) + result = -2; + else if (wcscmp(wSubjectName, expSubject) != 0) + result = -3; + else if (dwFlags != expFlags) + result = -4; + } + /* on failure the parser must not hand back allocations */ + if (result == 0 && ret != WS_SUCCESS && + (wStoreName != NULL || wSubjectName != NULL)) { + result = -5; + } + + if (wStoreName != NULL) + WFREE(wStoreName, NULL, DYNTYPE_TEMP); + if (wSubjectName != NULL) + WFREE(wSubjectName, NULL, DYNTYPE_TEMP); + + return result; +} + + +static int test_ParseCertStoreSpec(void) +{ + int result; + wchar_t* wStoreName = NULL; + wchar_t* wSubjectName = NULL; + word32 dwFlags = 0; + + /* bad arguments */ + result = certStoreSpecCheck(NULL, WS_BAD_ARGUMENT, NULL, NULL, 0); + if (result == 0 && wolfSSH_ParseCertStoreSpec("My:server", NULL, + &wSubjectName, &dwFlags, NULL) != WS_BAD_ARGUMENT) + result = -10; + if (result == 0 && wolfSSH_ParseCertStoreSpec("My:server", &wStoreName, + NULL, &dwFlags, NULL) != WS_BAD_ARGUMENT) + result = -11; + if (result == 0 && wolfSSH_ParseCertStoreSpec("My:server", &wStoreName, + &wSubjectName, NULL, NULL) != WS_BAD_ARGUMENT) + result = -12; + + /* full spec with named flag values */ + if (result == 0) + result = certStoreSpecCheck("My:server:LOCAL_MACHINE", WS_SUCCESS, + L"My", L"server", CERT_SYSTEM_STORE_LOCAL_MACHINE); + if (result == 0) + result = certStoreSpecCheck("My:server:CURRENT_USER", WS_SUCCESS, + L"My", L"server", CERT_SYSTEM_STORE_CURRENT_USER); + + /* flags default to CURRENT_USER when not given */ + if (result == 0) + result = certStoreSpecCheck("My:server", WS_SUCCESS, + L"My", L"server", CERT_SYSTEM_STORE_CURRENT_USER); + + /* numeric flags value */ + if (result == 0) + result = certStoreSpecCheck("My:server:12345", WS_SUCCESS, + L"My", L"server", 12345); + + /* missing or empty fields are rejected */ + if (result == 0) + result = certStoreSpecCheck("My", WS_BAD_ARGUMENT, NULL, NULL, 0); + if (result == 0) + result = certStoreSpecCheck("My:", WS_BAD_ARGUMENT, NULL, NULL, 0); + if (result == 0) + result = certStoreSpecCheck(":server", WS_BAD_ARGUMENT, NULL, NULL, 0); + if (result == 0) + result = certStoreSpecCheck("My:server:", WS_BAD_ARGUMENT, NULL, NULL, + 0); + if (result == 0) + result = certStoreSpecCheck("", WS_BAD_ARGUMENT, NULL, NULL, 0); + + return result; +} +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ + /* Tests below install a custom allocator via wolfSSL_SetAllocators. The * wolfSSL_Malloc_cb / wolfSSL_Free_cb / wolfSSL_Realloc_cb typedefs gain * extra parameters when wolfSSL is built with WOLFSSL_STATIC_MEMORY or @@ -13360,6 +13549,19 @@ int wolfSSH_UnitTest(int argc, char** argv) #endif +#if defined(WOLFSSH_TEST_INTERNAL) && defined(WOLFSSH_CERTS) + unitResult = test_SetCertManager(); + printf("SetCertManager: %s\n", (unitResult == 0 ? "SUCCESS" : "FAILED")); + testResult = testResult || unitResult; +#endif + +#if defined(WOLFSSH_TEST_INTERNAL) && defined(WOLFSSH_WINDOWS_CERT_STORE) + unitResult = test_ParseCertStoreSpec(); + printf("ParseCertStoreSpec: %s\n", + (unitResult == 0 ? "SUCCESS" : "FAILED")); + testResult = testResult || unitResult; +#endif + #ifdef WOLFSSH_TEST_CERTMAN_PROMOTE unitResult = test_CertMan_NoPromoteNonCaIntermediate(); printf("CertMan_NoPromoteNonCaIntermediate: %s\n", diff --git a/wolfssh/certman.h b/wolfssh/certman.h index 854b15e8c..fe68aeaf5 100644 --- a/wolfssh/certman.h +++ b/wolfssh/certman.h @@ -30,6 +30,7 @@ #include #include +#include /* included for WOLFSSH_CTX */ #include /* included for WOLFSSL_CERT_MANAGER struct */ #ifdef __cplusplus @@ -59,6 +60,14 @@ int wolfSSH_CERTMAN_VerifyCerts_buffer(WOLFSSH_CERTMAN* cm, const unsigned char* cert, word32 certSz, word32 certCount); +#ifdef WOLFSSH_WINDOWS_CERT_STORE +WOLFSSH_API +int wolfSSH_ParseCertStoreSpec(const char* spec, + wchar_t** wStoreName, wchar_t** wSubjectName, + word32* dwFlags, void* heap); +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ + + #ifdef __cplusplus } #endif diff --git a/wolfssh/internal.h b/wolfssh/internal.h index 2967b05cc..6e217e1cf 100644 --- a/wolfssh/internal.h +++ b/wolfssh/internal.h @@ -57,6 +57,15 @@ #include #endif /* WOLFSSH_CERTS */ +#ifdef WOLFSSH_WINDOWS_CERT_STORE + #ifndef WOLFSSH_CERTS + #error "WOLFSSH_WINDOWS_CERT_STORE requires WOLFSSH_CERTS" + #endif + #ifndef _WIN32 + #error "WOLFSSH_WINDOWS_CERT_STORE requires a Windows (_WIN32) target" + #endif +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ + #ifdef WOLFSSH_TPM #include #endif /* WOLFSSH_TPM */ @@ -743,6 +752,21 @@ typedef struct WOLFSSH_PVT_KEY { /* When set, the host key material lives in the TPM and key/keySz are * unused; signing and the public K_S come from ctx->tpmKey. */ #endif +#ifdef WOLFSSH_WINDOWS_CERT_STORE + byte useCertStore:1; + /* Flag indicating if this key is from MS Certificate Store. */ + void* certStoreContext; + /* Windows certificate context (PCCERT_CONTEXT) for MS Certificate Store. + * Owned by CTX, must be freed with CertFreeCertificateContext. */ + wchar_t* storeName; + /* Certificate store name (e.g., "My", "Root"). Owned by CTX. */ + wchar_t* subjectName; + /* Certificate subject name for lookup. Owned by CTX. */ + word32 dwFlags; + /* Certificate store flags (e.g., CERT_SYSTEM_STORE_CURRENT_USER). + * Kept as word32 so this header does not depend on Windows + * typedefs; converted to DWORD at the CertOpenStore call. */ +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ } WOLFSSH_PVT_KEY; @@ -1264,6 +1288,7 @@ WOLFSSH_LOCAL int ChannelPutData(WOLFSSH_CHANNEL* channel, byte* data, word32 dataSz); WOLFSSH_LOCAL int ChannelCreditWindow(WOLFSSH* ssh, WOLFSSH_CHANNEL* channel, word32 amount); +WOLFSSH_LOCAL void RefreshPublicKeyAlgo(WOLFSSH_CTX* ctx); WOLFSSH_LOCAL int wolfSSH_ProcessBuffer(WOLFSSH_CTX* ctx, const byte* in, word32 inSz, int format, int type); @@ -1435,6 +1460,9 @@ WOLFSSH_LOCAL int GenerateKey(byte hashId, byte keyId, byte* key, WOLFSSH_LOCAL int wcPrimeForId(byte id); #endif WOLFSSH_LOCAL enum wc_HashType HashForId(byte id); +#ifdef WOLFSSH_CERTS +WOLFSSH_LOCAL byte CertTypeForId(byte id); +#endif enum AcceptStates { diff --git a/wolfssh/ssh.h b/wolfssh/ssh.h index 57e8f2452..7dc826c0f 100644 --- a/wolfssh/ssh.h +++ b/wolfssh/ssh.h @@ -43,6 +43,11 @@ #include #endif +#ifdef WOLFSSH_WINDOWS_CERT_STORE +/* The Windows certificate store API below uses wchar_t strings. */ +#include +#endif + #ifdef __cplusplus extern "C" { #endif @@ -494,6 +499,11 @@ WOLFSSH_API int wolfSSH_CTX_UsePrivateKey_buffer(WOLFSSH_CTX* ctx, const byte* cert, word32 certSz, int format); WOLFSSH_API int wolfSSH_CTX_AddRootCert_buffer(WOLFSSH_CTX* ctx, const byte* cert, word32 certSz, int format); +#ifdef WOLFSSH_WINDOWS_CERT_STORE + WOLFSSH_API int wolfSSH_CTX_UsePrivateKey_fromStore(WOLFSSH_CTX* ctx, + const wchar_t* storeName, word32 dwFlags, + const wchar_t* subjectName); +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ #endif /* WOLFSSH_CERTS */ WOLFSSH_API int wolfSSH_CTX_SetWindowPacketSize(WOLFSSH_CTX* ctx, word32 windowSz, word32 maxPacketSz); From e8297ac107a20dd45e53bb5b5a1b2625348a32e6 Mon Sep 17 00:00:00 2001 From: JacobBarthelmeh Date: Mon, 3 Aug 2026 23:09:49 -0600 Subject: [PATCH 06/10] reset of tpm flag, handling of duplicate expiered cert in store, non-case sensitive name match --- .github/workflows/windows-cert-store-test.yml | 57 ++++++++ apps/wolfsshd/auth.c | 12 +- apps/wolfsshd/configuration.c | 38 +++++- apps/wolfsshd/wolfsshd.c | 32 +++-- configure.ac | 4 +- examples/client/common.c | 9 +- examples/sftpclient/sftpclient.c | 15 ++- ide/winvs/api-test/api-test.vcxproj | 16 +-- ide/winvs/client/client.vcxproj | 16 +-- ide/winvs/echoserver/echoserver.vcxproj | 16 +-- ide/winvs/unit-test/unit-test.vcxproj | 16 +-- .../wolfsftp-client/wolfsftp-client.vcxproj | 16 +-- ide/winvs/wolfssh/wolfssh.vcxproj | 8 +- ide/winvs/wolfsshd/wolfsshd.vcxproj | 22 ++-- src/certman.c | 11 +- src/internal.c | 8 ++ src/ssh.c | 122 +++++++++++------- tests/unit.c | 13 +- 18 files changed, 300 insertions(+), 131 deletions(-) diff --git a/.github/workflows/windows-cert-store-test.yml b/.github/workflows/windows-cert-store-test.yml index eb4fcb84f..88cbcff88 100644 --- a/.github/workflows/windows-cert-store-test.yml +++ b/.github/workflows/windows-cert-store-test.yml @@ -260,6 +260,10 @@ jobs: $subject = $serverCert.Subject if ($subject -match "^CN=(.+)$") { $subject = $matches[1] } Add-Content -Path $env:GITHUB_ENV -Value "SERVER_CERT_SUBJECT=$subject" + + # Export the (self-signed) server cert as DER so the client can use + # it as the trust anchor when negotiating an x509v3-* host key. + Export-Certificate -Cert $serverCert -FilePath "server-store-cert.der" | Out-Null } # Client user key: import the CA-signed testuser cert+key into @@ -569,6 +573,59 @@ jobs: } Write-Host "SFTP against echoserver succeeded" + - name: Test SFTP against echoserver with x509v3 host key + if: matrix.server_key_source == 'store' && matrix.key_algorithm == 'ecdsa' + working-directory: ${{ github.workspace }}\wolfssh + shell: pwsh + run: | + # Force the x509v3 host key algorithm so the cert store certificate + # itself is sent as K_S and verified by the client, exercising the + # X.509 host-key slot instead of the plain-key slot. The server cert + # is self-signed, so it is its own trust anchor (-A). + $testPort = ${{env.TEST_PORT}} + $sftpPath = $env:SFTP_PATH + + @" + pwd + ls + quit + "@ | Out-File -FilePath sftp_x509_commands.txt -Encoding ASCII + + $sftpArgs = @("-u", "testuser", "-h", "localhost", "-p", "$testPort") + if ("${{ matrix.client_key_source }}" -eq "store") { + $sftpArgs += "-W", "My:$($env:CLIENT_CERT_SUBJECT):CURRENT_USER" + } else { + $sftpArgs += "-J", (Resolve-Path $env:CLIENT_CERT_FILE).Path + $sftpArgs += "-i", (Resolve-Path $env:CLIENT_KEY_FILE).Path + } + $sftpArgs += "-A", (Resolve-Path "server-store-cert.der").Path, "-X" + $sftpArgs += "-k", "x509v3-ecdsa-sha2-nistp256" + + Write-Host "Running: $sftpPath $($sftpArgs -join ' ')" + $process = Start-Process -FilePath $sftpPath ` + -ArgumentList $sftpArgs ` + -RedirectStandardInput "sftp_x509_commands.txt" ` + -RedirectStandardOutput "sftp_x509_output.txt" ` + -RedirectStandardError "sftp_x509_error.txt" ` + -Wait -NoNewWindow -PassThru + + Write-Host "SFTP (x509v3 host key) exit code: $($process.ExitCode)" + Write-Host "=== SFTP Output ===" + if (Test-Path sftp_x509_output.txt) { Get-Content sftp_x509_output.txt } + Write-Host "=== SFTP Error ===" + if (Test-Path sftp_x509_error.txt) { Get-Content sftp_x509_error.txt } + + if ($process.ExitCode -ne 0) { + $echoLog = $env:ECHOSERVER_LOG + if (-not [string]::IsNullOrEmpty($echoLog) -and (Test-Path $echoLog)) { + Write-Host "=== Echoserver Log ===" + Get-Content $echoLog + } + Write-Host "ERROR: SFTP with x509v3 host key failed" + exit 1 + } + Write-Host "SFTP with x509v3 host key succeeded" + - name: Stop echoserver before wolfsshd test if: matrix.server_key_source == 'store' shell: pwsh diff --git a/apps/wolfsshd/auth.c b/apps/wolfsshd/auth.c index b9baa3e38..f8e030fc0 100644 --- a/apps/wolfsshd/auth.c +++ b/apps/wolfsshd/auth.c @@ -2144,11 +2144,19 @@ static int RequestAuthentication(WS_UserAuthData* authData, "not set; certificate UPN domain is not checked"); } #else - /* Without FPKI compare subject CN with user name */ + /* Without FPKI compare subject CN with user name. + * Windows account names are case-insensitive, so match + * the CN the same way there. */ if (dCert->subjectCN != NULL && (int)XSTRLEN(usr) == dCert->subjectCNLen && + #ifdef _WIN32 + WSTRNCASECMP(usr, dCert->subjectCN, + (size_t)dCert->subjectCNLen) == 0 + #else XSTRNCMP(usr, dCert->subjectCN, - (size_t)dCert->subjectCNLen) == 0) { + (size_t)dCert->subjectCNLen) == 0 + #endif + ) { usrMatch = 1; } #endif diff --git a/apps/wolfsshd/configuration.c b/apps/wolfsshd/configuration.c index ebc6322b1..50978793d 100644 --- a/apps/wolfsshd/configuration.c +++ b/apps/wolfsshd/configuration.c @@ -1265,6 +1265,20 @@ static int SetListString(char** dst, const char* value, int valueSz, return ret; } +/* CA trust sources are loaded once at startup from the global config; a + * Match-scoped setting would be silently ignored at authentication time. + * Reject such options at parse time instead of failing open. Returns + * WS_SUCCESS when conf is the global config. */ +static int CheckNotInMatch(const WOLFSSHD_CONFIG* conf, const char* option) +{ + if (conf->usrAppliesTo != NULL || conf->groupAppliesTo != NULL) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Option %s is not supported inside a Match block", option); + return WS_BAD_ARGUMENT; + } + return WS_SUCCESS; +} + /* returns WS_SUCCESS on success */ /* NOLINTNEXTLINE(misc-no-recursion): bounded by WOLFSSHD_MAX_INCLUDE_DEPTH */ static int HandleConfigOption(WOLFSSHD_CONFIG** conf, int opt, @@ -1356,7 +1370,9 @@ static int HandleConfigOption(WOLFSSHD_CONFIG** conf, int opt, ret = wolfSSHD_ConfigSetUserCAKeysFile(*conf, value); break; case OPT_TRUSTED_SYSTEM_CA_KEYS: - ret = wolfSSHD_ConfigSetSystemCA(*conf, value); + ret = CheckNotInMatch(*conf, "wolfSSH_TrustedSystemCAKeys"); + if (ret == WS_SUCCESS) + ret = wolfSSHD_ConfigSetSystemCA(*conf, value); break; case OPT_PIDFILE: ret = SetFileString(&(*conf)->pidFile, value, (*conf)->heap); @@ -1368,17 +1384,25 @@ static int HandleConfigOption(WOLFSSHD_CONFIG** conf, int opt, ret = HandleStrictModes(*conf, value); break; case OPT_TRUSTED_USER_CA_STORE: - ret = wolfSSHD_ConfigSetUserCAStore(*conf, value); + ret = CheckNotInMatch(*conf, "wolfSSH_TrustedUserCAStore"); + if (ret == WS_SUCCESS) + ret = wolfSSHD_ConfigSetUserCAStore(*conf, value); break; #ifdef USE_WINDOWS_API case OPT_WIN_USER_STORES: - ret = wolfSSHD_ConfigSetWinUserStores(*conf, value); + ret = CheckNotInMatch(*conf, "wolfSSH_WinUserStores"); + if (ret == WS_SUCCESS) + ret = wolfSSHD_ConfigSetWinUserStores(*conf, value); break; case OPT_WIN_USER_DW_FLAGS: - ret = wolfSSHD_ConfigSetWinUserDwFlags(*conf, value); + ret = CheckNotInMatch(*conf, "wolfSSH_WinUserDwFlags"); + if (ret == WS_SUCCESS) + ret = wolfSSHD_ConfigSetWinUserDwFlags(*conf, value); break; case OPT_WIN_USER_PV_PARA: - ret = wolfSSHD_ConfigSetWinUserPvPara(*conf, value); + ret = CheckNotInMatch(*conf, "wolfSSH_WinUserPvPara"); + if (ret == WS_SUCCESS) + ret = wolfSSHD_ConfigSetWinUserPvPara(*conf, value); break; #endif /* USE_WINDOWS_API */ case OPT_AUTHORIZED_UPN_DOMAINS: @@ -1394,7 +1418,9 @@ static int HandleConfigOption(WOLFSSHD_CONFIG** conf, int opt, case OPT_HOST_KEY_STORE_SUBJECT: wolfSSH_Log(WS_LOG_INFO, "[SSHD] Parsed HostKeyStoreSubject = '%s'", value); - ret = SetFileString(&(*conf)->hostKeyStoreSubject, value, + /* use the full line remainder so a CN containing spaces is + * kept instead of being cut at the first token */ + ret = SetListString(&(*conf)->hostKeyStoreSubject, full, fullSz, (*conf)->heap); break; case OPT_HOST_KEY_STORE_FLAGS: diff --git a/apps/wolfsshd/wolfsshd.c b/apps/wolfsshd/wolfsshd.c index 6a127a3bb..f97323575 100644 --- a/apps/wolfsshd/wolfsshd.c +++ b/apps/wolfsshd/wolfsshd.c @@ -380,12 +380,15 @@ static int LoadUserCACertsFromStore(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX* ctx, return WS_BAD_ARGUMENT; } - /* Only the system-store provider is supported here. */ + /* Only the system-store provider is supported here. Fail rather than + * silently load trust anchors from a different provider than the one + * configured. */ if (providerStr != NULL && WSTRCMP(providerStr, "CERT_STORE_PROV_SYSTEM") != 0) { - wolfSSH_Log(WS_LOG_INFO, - "[SSHD] wolfSSH_WinUserStores='%s' ignored; only " + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] wolfSSH_WinUserStores='%s' is not supported; only " "CERT_STORE_PROV_SYSTEM is supported", providerStr); + return WS_BAD_ARGUMENT; } if (dwFlagsStr != NULL) { @@ -398,11 +401,13 @@ static int LoadUserCACertsFromStore(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX* ctx, dwFlags = CERT_SYSTEM_STORE_LOCAL_MACHINE; } else { - /* fall back to a raw numeric value; a result of 0 means the string - * was not a recognized name or valid number, which is never a - * usable store-location flag */ + /* Fall back to a raw numeric value, but only accept system-store + * location bits. Anything else is either not a location or a + * control flag (e.g. CERT_STORE_DELETE_FLAG) that would make + * CertOpenStore destructive. */ dwFlags = (word32)atoi(dwFlagsStr); - if (dwFlags == 0) { + if ((dwFlags & (word32)CERT_SYSTEM_STORE_LOCATION_MASK) == 0 || + (dwFlags & ~(word32)CERT_SYSTEM_STORE_LOCATION_MASK) != 0) { wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Unrecognized user CA store flags '%s'", dwFlagsStr); return WS_BAD_ARGUMENT; @@ -551,11 +556,16 @@ static int SetupCTX(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX** ctx, } else if (WSTRCMP(hostKeyStoreFlags, "LOCAL_MACHINE") == 0) { dwFlags = CERT_SYSTEM_STORE_LOCAL_MACHINE; } else { - /* fall back to a raw numeric value; a result of 0 means the - * string was not a recognized name or valid number, which - * is never a usable store-location flag */ + /* Fall back to a raw numeric value, but only accept + * system-store location bits. Anything else is either not + * a location or a control flag (e.g. + * CERT_STORE_DELETE_FLAG) that would make CertOpenStore + * destructive. */ dwFlags = (word32)atoi(hostKeyStoreFlags); - if (dwFlags == 0) { + if ((dwFlags & + (word32)CERT_SYSTEM_STORE_LOCATION_MASK) == 0 || + (dwFlags & + ~(word32)CERT_SYSTEM_STORE_LOCATION_MASK) != 0) { wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Unrecognized host key store flags '%s'", hostKeyStoreFlags); diff --git a/configure.ac b/configure.ac index d7c2e3353..bf183cf71 100644 --- a/configure.ac +++ b/configure.ac @@ -299,8 +299,8 @@ AS_IF([test "x$ENABLED_WINDOWS_CERT_STORE" = "xyes"], [AC_MSG_ERROR([--enable-windows-cert-store requires X.509 cert support (--enable-certs)])]) AM_CPPFLAGS="$AM_CPPFLAGS -DWOLFSSH_WINDOWS_CERT_STORE" AS_CASE([$host], - [*mingw*|*msys*|*cygwin*],[LIBS="$LIBS -lcrypt32 -lncrypt"], - [AC_MSG_ERROR([--enable-windows-cert-store is only supported on Windows hosts (mingw/msys/cygwin)])])]) + [*mingw*|*msys*],[LIBS="$LIBS -lcrypt32 -lncrypt"], + [AC_MSG_ERROR([--enable-windows-cert-store is only supported on _WIN32 Windows hosts (mingw/msys)])])]) AS_IF([test "x$ENABLED_SMALLSTACK" = "xyes"], [AM_CPPFLAGS="$AM_CPPFLAGS -DWOLFSSH_SMALL_STACK"]) AS_IF([test "x$ENABLED_NONE_CIPHER" = "xyes"], diff --git a/examples/client/common.c b/examples/client/common.c index 704bc7f9c..aa114fc00 100644 --- a/examples/client/common.c +++ b/examples/client/common.c @@ -1285,8 +1285,13 @@ int ClientSetupCertStoreAuth(WOLFSSH_CTX* ctx) } userPublicKeyTypeSz = (word32)WSTRLEN((const char*)userPublicKeyType); - /* No in-memory private key — signing goes through the cert store. */ - userPrivateKey = NULL; + /* No in-memory private key — signing goes through the cert store. + * Keep the static-buffer invariant (only replace the pointer when it + * is not a heap allocation) so a later key load or + * ClientFreeBuffers call remains valid. */ + if (!userPrivateKeyAlloc) { + userPrivateKey = userPrivateKeyBuf; + } userPrivateKeySz = 0; pubKeyLoaded = 1; diff --git a/examples/sftpclient/sftpclient.c b/examples/sftpclient/sftpclient.c index 694967593..1a3a4bc0f 100644 --- a/examples/sftpclient/sftpclient.c +++ b/examples/sftpclient/sftpclient.c @@ -410,6 +410,8 @@ static void ShowUsage(void) printf(" -g put local filename as remote filename\n"); printf(" -G get remote filename as local filename\n"); printf(" -i filename for the user's private key\n"); + printf(" -k set the comma separated list of public key " + "algos to offer\n"); #ifdef WOLFSSH_WINDOWS_CERT_STORE printf(" -W Windows cert store: \"store:subject:flags\"\n"); printf(" Example: -W \"My:CN=MyCert:CURRENT_USER\"\n"); @@ -1582,6 +1584,7 @@ THREAD_RETURN WOLFSSH_THREAD sftpclient_test(void* args) char* pubKeyName = NULL; char* certName = NULL; char* caCert = NULL; + const char* keyList = NULL; #ifdef WOLFSSH_WINDOWS_CERT_STORE const char* certStoreSpec = NULL; /* Format: "store:subject:flags" */ #endif /* WOLFSSH_WINDOWS_CERT_STORE */ @@ -1591,7 +1594,7 @@ THREAD_RETURN WOLFSSH_THREAD sftpclient_test(void* args) char** argv = ((func_args*)args)->argv; ((func_args*)args)->return_code = 0; - while ((ch = mygetopt(argc, argv, "?d:gh:i:j:l:p:r:u:EGNP:J:A:X" + while ((ch = mygetopt(argc, argv, "?d:gh:i:j:k:l:p:r:u:EGNP:J:A:X" #ifdef WOLFSSH_WINDOWS_CERT_STORE "W:" #endif /* WOLFSSH_WINDOWS_CERT_STORE */ @@ -1657,6 +1660,10 @@ THREAD_RETURN WOLFSSH_THREAD sftpclient_test(void* args) pubKeyName = myoptarg; break; + case 'k': + keyList = myoptarg; + break; + #ifdef WOLFSSH_CERTS case 'J': certName = myoptarg; @@ -1794,6 +1801,12 @@ THREAD_RETURN WOLFSSH_THREAD sftpclient_test(void* args) if (ctx == NULL) err_sys("Couldn't create wolfSSH client context."); + if (keyList) { + if (wolfSSH_CTX_SetAlgoListKey(ctx, keyList) != WS_SUCCESS) { + err_sys("Error setting key list."); + } + } + if (((func_args*)args)->user_auth == NULL) wolfSSH_SetUserAuth(ctx, ClientUserAuth); else diff --git a/ide/winvs/api-test/api-test.vcxproj b/ide/winvs/api-test/api-test.vcxproj index 2524860b7..bbeb3dc6b 100644 --- a/ide/winvs/api-test/api-test.vcxproj +++ b/ide/winvs/api-test/api-test.vcxproj @@ -364,7 +364,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDebug32FIPS) @@ -400,7 +400,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDllDebug32FIPS) @@ -436,7 +436,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDebug64FIPS) @@ -472,7 +472,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDllDebug64FIPS) @@ -511,7 +511,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptRelease32FIPS) @@ -551,7 +551,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptDllRelease32FIPS) @@ -591,7 +591,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptRelease64FIPS) @@ -631,7 +631,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptDllRelease64FIPS) diff --git a/ide/winvs/client/client.vcxproj b/ide/winvs/client/client.vcxproj index d8d0d838c..1a2eed004 100644 --- a/ide/winvs/client/client.vcxproj +++ b/ide/winvs/client/client.vcxproj @@ -364,7 +364,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDebug32FIPS) @@ -400,7 +400,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDllDebug32FIPS) @@ -436,7 +436,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDebug64FIPS) @@ -472,7 +472,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDllDebug64FIPS) @@ -511,7 +511,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptRelease32FIPS) @@ -551,7 +551,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptDllRelease32FIPS) @@ -591,7 +591,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptRelease64FIPS) @@ -631,7 +631,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptDllRelease64FIPS) diff --git a/ide/winvs/echoserver/echoserver.vcxproj b/ide/winvs/echoserver/echoserver.vcxproj index c5715bc14..b32603fee 100644 --- a/ide/winvs/echoserver/echoserver.vcxproj +++ b/ide/winvs/echoserver/echoserver.vcxproj @@ -363,7 +363,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDebug32FIPS) @@ -399,7 +399,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDllDebug32FIPS) @@ -435,7 +435,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDebug64FIPS) @@ -471,7 +471,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDllDebug64FIPS) @@ -510,7 +510,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptRelease32FIPS) @@ -550,7 +550,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptDllRelease32FIPS) @@ -590,7 +590,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptRelease64FIPS) @@ -630,7 +630,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptDllRelease64FIPS) diff --git a/ide/winvs/unit-test/unit-test.vcxproj b/ide/winvs/unit-test/unit-test.vcxproj index cf1e70a18..f88807fc8 100644 --- a/ide/winvs/unit-test/unit-test.vcxproj +++ b/ide/winvs/unit-test/unit-test.vcxproj @@ -363,7 +363,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDebug32FIPS) @@ -399,7 +399,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDllDebug32FIPS) @@ -435,7 +435,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDebug64FIPS) @@ -471,7 +471,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDllDebug64FIPS) @@ -510,7 +510,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptRelease32FIPS) @@ -550,7 +550,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptDllRelease32FIPS) @@ -590,7 +590,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptRelease64FIPS) @@ -630,7 +630,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptDllRelease64FIPS) diff --git a/ide/winvs/wolfsftp-client/wolfsftp-client.vcxproj b/ide/winvs/wolfsftp-client/wolfsftp-client.vcxproj index 26125b088..4c0e39dea 100644 --- a/ide/winvs/wolfsftp-client/wolfsftp-client.vcxproj +++ b/ide/winvs/wolfsftp-client/wolfsftp-client.vcxproj @@ -364,7 +364,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDllDebug32FIPS) @@ -382,7 +382,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDebug32FIPS) @@ -436,7 +436,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDllDebug64FIPS) @@ -454,7 +454,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDebug64FIPS) @@ -511,7 +511,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptRelease32FIPS) @@ -551,7 +551,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptDllRelease32FIPS) @@ -591,7 +591,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptRelease64FIPS) @@ -631,7 +631,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptDllRelease64FIPS) diff --git a/ide/winvs/wolfssh/wolfssh.vcxproj b/ide/winvs/wolfssh/wolfssh.vcxproj index c5821eefd..25b2d7764 100644 --- a/ide/winvs/wolfssh/wolfssh.vcxproj +++ b/ide/winvs/wolfssh/wolfssh.vcxproj @@ -382,7 +382,7 @@ Windows true $(wolfCryptDllDebug32FIPS) - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) @@ -445,7 +445,7 @@ Windows true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) $(wolfCryptDllDebug64FIPS) @@ -522,7 +522,7 @@ true true $(wolfCryptDllRelease32FIPS) - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) @@ -597,7 +597,7 @@ true true true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) $(wolfCryptDllRelease64FIPS) diff --git a/ide/winvs/wolfsshd/wolfsshd.vcxproj b/ide/winvs/wolfsshd/wolfsshd.vcxproj index ea006b8c0..fede8c151 100644 --- a/ide/winvs/wolfsshd/wolfsshd.vcxproj +++ b/ide/winvs/wolfsshd/wolfsshd.vcxproj @@ -254,7 +254,7 @@ Console true ..\..\..\..\wolfssl\Debug\Win32;..\Debug\Win32 - wolfssl.lib;ws2_32.lib;secur32.lib;userenv.lib;$(CoreLibraryDependencies);%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;secur32.lib;userenv.lib;crypt32.lib;ncrypt.lib;$(CoreLibraryDependencies);%(AdditionalDependencies) @@ -269,7 +269,7 @@ Console true ..\..\..\..\wolfssl\IDE\WIN10\Debug\Win32;..\Debug\Win32 - wolfssl-fips.lib;ws2_32.lib;secur32.lib;userenv.lib;$(CoreLibraryDependencies);%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;secur32.lib;userenv.lib;crypt32.lib;ncrypt.lib;$(CoreLibraryDependencies);%(AdditionalDependencies) @@ -284,7 +284,7 @@ Console true ..\..\..\..\wolfssl\IDE\WIN10\Debug\Win32;..\Debug\Win32 - wolfssl-fips.lib;ws2_32.lib;secur32.lib;userenv.lib;$(CoreLibraryDependencies);%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;secur32.lib;userenv.lib;crypt32.lib;ncrypt.lib;$(CoreLibraryDependencies);%(AdditionalDependencies) @@ -303,7 +303,7 @@ true true ..\..\..\..\wolfssl\Release\Win32;..\Release\Win32 - wolfssl.lib;ws2_32.lib;secur32.lib;userenv.lib;$(CoreLibraryDependencies);%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;secur32.lib;userenv.lib;crypt32.lib;ncrypt.lib;$(CoreLibraryDependencies);%(AdditionalDependencies) @@ -322,7 +322,7 @@ true true ..\..\..\..\wolfssl\IDE\WIN10\Release\Win32;..\Release\Win32 - wolfssl-fips.lib;ws2_32.lib;secur32.lib;userenv.lib;$(CoreLibraryDependencies);%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;secur32.lib;userenv.lib;crypt32.lib;ncrypt.lib;$(CoreLibraryDependencies);%(AdditionalDependencies) @@ -352,7 +352,7 @@ Console true ..\..\..\..\wolfssl\IDE\WIN10\Debug\x64;..\Debug\x64 - wolfssl-fips.lib;ws2_32.lib;secur32.lib;userenv.lib;$(CoreLibraryDependencies);%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;secur32.lib;userenv.lib;crypt32.lib;ncrypt.lib;$(CoreLibraryDependencies);%(AdditionalDependencies) @@ -367,7 +367,7 @@ Console true ..\..\..\..\wolfssl\IDE\WIN10\Debug\x64;..\Debug\x64 - wolfssl-fips.lib;ws2_32.lib;secur32.lib;userenv.lib;$(CoreLibraryDependencies);%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;secur32.lib;userenv.lib;crypt32.lib;ncrypt.lib;$(CoreLibraryDependencies);%(AdditionalDependencies) @@ -404,7 +404,7 @@ true true true - wolfssl-fips.lib;ws2_32.lib;secur32.lib;userenv.lib;$(CoreLibraryDependencies);%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;secur32.lib;userenv.lib;crypt32.lib;ncrypt.lib;$(CoreLibraryDependencies);%(AdditionalDependencies) ..\..\..\..\wolfssl\IDE\WIN10\Release\x64;..\Release\x64 @@ -433,7 +433,7 @@ Level3 - wolfssl-fips.lib;ws2_32.lib;secur32.lib;userenv.lib;$(CoreLibraryDependencies);%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;secur32.lib;userenv.lib;crypt32.lib;ncrypt.lib;$(CoreLibraryDependencies);%(AdditionalDependencies) $(wolfCryptDLLRelease64FIPS) true true @@ -442,7 +442,7 @@ - wolfssl-fips.lib;ws2_32.lib;secur32.lib;userenv.lib;$(CoreLibraryDependencies);%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;secur32.lib;userenv.lib;crypt32.lib;ncrypt.lib;$(CoreLibraryDependencies);%(AdditionalDependencies) $(wolfCryptDLLRelease32FIPS) @@ -456,7 +456,7 @@ ..;..\..\..;$(wolfCryptDir);..\..\..\apps\wolfsshd\;%(AdditionalIncludeDirectories) - wolfssl.lib;ws2_32.lib;secur32.lib;userenv.lib;$(CoreLibraryDependencies);%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;secur32.lib;userenv.lib;crypt32.lib;ncrypt.lib;$(CoreLibraryDependencies);%(AdditionalDependencies) $(wolfCryptDLLRelease32FIPS) diff --git a/src/certman.c b/src/certman.c index cb0db2567..25e9f1d95 100644 --- a/src/certman.c +++ b/src/certman.c @@ -733,11 +733,14 @@ int wolfSSH_ParseCertStoreSpec(const char* spec, *dwFlags = CERT_SYSTEM_STORE_LOCAL_MACHINE; } else { - /* fall back to a raw numeric value; a result of 0 means the - * string was not a recognized name or valid number, which is - * never a usable store-location flag */ + /* Fall back to a raw numeric value, but only accept + * system-store location bits. Anything else is either not a + * location or a control flag (e.g. CERT_STORE_DELETE_FLAG) + * that would make CertOpenStore destructive. */ *dwFlags = (word32)atoi(flagsStr); - if (*dwFlags == 0) { + if ((*dwFlags & (word32)CERT_SYSTEM_STORE_LOCATION_MASK) == 0 + || (*dwFlags & + ~(word32)CERT_SYSTEM_STORE_LOCATION_MASK) != 0) { WFREE(specCopy, heap, DYNTYPE_TEMP); return WS_BAD_ARGUMENT; } diff --git a/src/internal.c b/src/internal.c index 61e4b6ee9..446cb39c2 100644 --- a/src/internal.c +++ b/src/internal.c @@ -2546,6 +2546,11 @@ int wolfSSH_SetHostTpmKey(WOLFSSH_CTX* ctx, byte keyId) pvtKey->key = NULL; pvtKey->keySz = 0; pvtKey->isTpm = 1; + #ifdef WOLFSSH_WINDOWS_CERT_STORE + /* The slot is now TPM backed; drop any cert-store state so signing + * and K_S do not use a stale certificate context. */ + ClearCertStoreKey(ctx, pvtKey); + #endif #ifdef WOLFSSH_CERTS /* Mark the matching certificate slot TPM-backed so certificate KEX @@ -2562,6 +2567,9 @@ int wolfSSH_SetHostTpmKey(WOLFSSH_CTX* ctx, byte keyId) ctx->privateKey[certIdx].keySz = 0; } ctx->privateKey[certIdx].isTpm = 1; + #ifdef WOLFSSH_WINDOWS_CERT_STORE + ClearCertStoreKey(ctx, &ctx->privateKey[certIdx]); + #endif break; } } diff --git a/src/ssh.c b/src/ssh.c index ced0e24b3..2feed21b7 100644 --- a/src/ssh.c +++ b/src/ssh.c @@ -2846,12 +2846,15 @@ int wolfSSH_CTX_AddRootCert_buffer(WOLFSSH_CTX* ctx, * CERT_FIND_SUBJECT_STR_W is only used as a substring pre-filter to * enumerate candidates; each candidate's CN is then compared exactly so * that a lookup for "server1" does not select "server1.example" or - * "myserver1". Returns the certificate context (caller must free with - * CertFreeCertificateContext) or NULL when no exact match exists. */ + * "myserver1". A currently time-valid match is preferred over an expired + * one so a renewal's leftover certificate is not selected. Returns the + * certificate context (caller must free with CertFreeCertificateContext) + * or NULL when no exact match exists. */ static PCCERT_CONTEXT FindCertByExactCN(HCERTSTORE hStore, const wchar_t* subjectName) { PCCERT_CONTEXT pCertContext; + PCCERT_CONTEXT firstMatch; const wchar_t* cn; wchar_t* certCn; DWORD certCnSz; @@ -2865,6 +2868,7 @@ static PCCERT_CONTEXT FindCertByExactCN(HCERTSTORE hStore, } pCertContext = NULL; + firstMatch = NULL; for (;;) { /* Passing the previous context frees it and continues the search. */ pCertContext = CertFindCertificateInStore(hStore, @@ -2890,10 +2894,23 @@ static PCCERT_CONTEXT FindCertByExactCN(HCERTSTORE hStore, match = (certCnSz > 1 && wcscmp(certCn, cn) == 0); WFREE(certCn, NULL, DYNTYPE_TEMP); if (match) { - break; + if (CertVerifyTimeValidity(NULL, pCertContext->pCertInfo) == 0) { + break; + } + if (firstMatch == NULL) { + firstMatch = CertDuplicateCertificateContext(pCertContext); + } } } + /* No time-valid match; fall back to the first exact match, if any. */ + if (pCertContext == NULL) { + pCertContext = firstMatch; + } + else if (firstMatch != NULL) { + CertFreeCertificateContext(firstMatch); + } + return pCertContext; } @@ -3008,6 +3025,10 @@ static int UseCertStoreSlot(WOLFSSH_CTX* ctx, byte keyId, /* Set up the private key structure */ pvtKey->publicKeyFmt = keyId; +#ifdef WOLFSSH_TPM + /* A stale TPM mark would route signing through the TPM. */ + pvtKey->isTpm = 0; +#endif pvtKey->useCertStore = 1; pvtKey->certStoreContext = (void*)slotContext; pvtKey->storeName = storeNameCopy; @@ -3072,65 +3093,76 @@ int wolfSSH_CTX_UsePrivateKey_fromStore(WOLFSSH_CTX* ctx, /* Get the public key info to determine algorithm */ pPubKeyInfo = &pCertContext->pCertInfo->SubjectPublicKeyInfo; - /* Check algorithm OID to determine key type */ + /* Check algorithm OID to determine key type. Only algorithms and + * curves compiled into this build are accepted; anything else leaves + * keyId as ID_NONE and is rejected below rather than registering a + * host key type that cannot be used for signing. */ if (pPubKeyInfo->Algorithm.pszObjId != NULL) { /* Compare OID strings (they are ASCII, not wide) */ if (strcmp(pPubKeyInfo->Algorithm.pszObjId, szOID_RSA_RSA) == 0 || strcmp(pPubKeyInfo->Algorithm.pszObjId, szOID_RSA_ENCRYPT) == 0) { + #ifndef WOLFSSH_NO_RSA keyId = ID_SSH_RSA; + #else + WLOG(WS_LOG_ERROR, "wolfSSH_CTX_UsePrivateKey_fromStore: " + "RSA is not compiled in"); + #endif } else if (strcmp(pPubKeyInfo->Algorithm.pszObjId, szOID_ECC_PUBLIC_KEY) == 0) { - /* Decode the curve OID from the algorithm parameters to select - * the correct ECDSA key type. The Parameters field contains - * a DER-encoded OID identifying the named curve. */ - char* curveOid = NULL; - DWORD curveOidSz = 0; - - if (pPubKeyInfo->Algorithm.Parameters.cbData > 0 && - CryptDecodeObjectEx(X509_ASN_ENCODING, - X509_OBJECT_IDENTIFIER, - pPubKeyInfo->Algorithm.Parameters.pbData, - pPubKeyInfo->Algorithm.Parameters.cbData, - CRYPT_DECODE_ALLOC_FLAG, NULL, - &curveOid, &curveOidSz)) { - /* Compare against well-known curve OIDs */ - if (strcmp(curveOid, "1.2.840.10045.3.1.7") == 0) { - keyId = ID_ECDSA_SHA2_NISTP256; - } - else if (strcmp(curveOid, "1.3.132.0.34") == 0) { - keyId = ID_ECDSA_SHA2_NISTP384; - } - else if (strcmp(curveOid, "1.3.132.0.35") == 0) { - keyId = ID_ECDSA_SHA2_NISTP521; - } - else { - WLOG(WS_LOG_DEBUG, - "wolfSSH_CTX_UsePrivateKey_fromStore: " - "Unrecognized ECC curve OID: %s, " - "defaulting to P-256", curveOid); - keyId = ID_ECDSA_SHA2_NISTP256; - } - LocalFree(curveOid); + /* The algorithm parameters hold the DER-encoded named-curve + * OID; match its raw bytes to select the ECDSA key type. */ + static const byte oidP256[] = { + 0x06, 0x08, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07 + }; + static const byte oidP384[] = { + 0x06, 0x05, 0x2B, 0x81, 0x04, 0x00, 0x22 + }; + static const byte oidP521[] = { + 0x06, 0x05, 0x2B, 0x81, 0x04, 0x00, 0x23 + }; + const byte* params = pPubKeyInfo->Algorithm.Parameters.pbData; + DWORD paramsSz = pPubKeyInfo->Algorithm.Parameters.cbData; + + if (params == NULL) { + paramsSz = 0; } - else { - WLOG(WS_LOG_DEBUG, - "wolfSSH_CTX_UsePrivateKey_fromStore: " - "Failed to decode ECC curve parameters, " - "defaulting to P-256"); + #ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP256 + if (paramsSz == sizeof(oidP256) && + WMEMCMP(params, oidP256, sizeof(oidP256)) == 0) { keyId = ID_ECDSA_SHA2_NISTP256; } + #endif + #ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP384 + if (paramsSz == sizeof(oidP384) && + WMEMCMP(params, oidP384, sizeof(oidP384)) == 0) { + keyId = ID_ECDSA_SHA2_NISTP384; + } + #endif + #ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP521 + if (paramsSz == sizeof(oidP521) && + WMEMCMP(params, oidP521, sizeof(oidP521)) == 0) { + keyId = ID_ECDSA_SHA2_NISTP521; + } + #endif + if (keyId == ID_NONE) { + WLOG(WS_LOG_ERROR, "wolfSSH_CTX_UsePrivateKey_fromStore: " + "Unsupported ECC curve parameters"); + } } else { - CertFreeCertificateContext(pCertContext); - CertCloseStore(hStore, 0); - WLOG(WS_LOG_DEBUG, "wolfSSH_CTX_UsePrivateKey_fromStore: Unsupported key algorithm: %s", pPubKeyInfo->Algorithm.pszObjId); - return WS_BAD_ARGUMENT; + WLOG(WS_LOG_ERROR, "wolfSSH_CTX_UsePrivateKey_fromStore: " + "Unsupported key algorithm: %s", + pPubKeyInfo->Algorithm.pszObjId); } } else { + WLOG(WS_LOG_ERROR, + "wolfSSH_CTX_UsePrivateKey_fromStore: No algorithm OID"); + } + + if (keyId == ID_NONE) { CertFreeCertificateContext(pCertContext); CertCloseStore(hStore, 0); - WLOG(WS_LOG_DEBUG, "wolfSSH_CTX_UsePrivateKey_fromStore: No algorithm OID"); return WS_BAD_ARGUMENT; } diff --git a/tests/unit.c b/tests/unit.c index ec29f7690..688ec7575 100644 --- a/tests/unit.c +++ b/tests/unit.c @@ -10153,10 +10153,17 @@ static int test_ParseCertStoreSpec(void) result = certStoreSpecCheck("My:server", WS_SUCCESS, L"My", L"server", CERT_SYSTEM_STORE_CURRENT_USER); - /* numeric flags value */ + /* numeric flags: only system-store location bits are accepted */ if (result == 0) - result = certStoreSpecCheck("My:server:12345", WS_SUCCESS, - L"My", L"server", 12345); + result = certStoreSpecCheck("My:server:393216", WS_SUCCESS, + L"My", L"server", CERT_SYSTEM_STORE_USERS); + if (result == 0) + result = certStoreSpecCheck("My:server:12345", WS_BAD_ARGUMENT, + NULL, NULL, 0); + /* location plus a control flag (CERT_STORE_DELETE_FLAG) is rejected */ + if (result == 0) + result = certStoreSpecCheck("My:server:65552", WS_BAD_ARGUMENT, + NULL, NULL, 0); /* missing or empty fields are rejected */ if (result == 0) From d26a556a3c36c9ec4a1f240829698fc384a915c3 Mon Sep 17 00:00:00 2001 From: JacobBarthelmeh Date: Tue, 4 Aug 2026 09:25:07 -0600 Subject: [PATCH 07/10] fix for flags handling, ocsp case, macro guards, unused variable, changed default from MY to required to be set --- .github/workflows/windows-cert-store-test.yml | 244 ++++++++++- apps/wolfsshd/auth.c | 60 ++- apps/wolfsshd/configuration.c | 158 ++++--- apps/wolfsshd/configuration.h | 10 +- apps/wolfsshd/wolfsshd.c | 273 ++++++++---- configure.ac | 6 +- examples/client/common.c | 52 ++- examples/client/common.h | 3 + examples/echoserver/echoserver.c | 64 ++- examples/sftpclient/sftpclient.c | 37 +- ide/winvs/api-test/api-test.vcxproj | 2 +- ide/winvs/testsuite/testsuite.vcxproj | 32 +- ide/winvs/unit-test/unit-test.vcxproj | 2 +- src/certman.c | 98 ++++- src/internal.c | 358 ++++++++++------ src/ssh.c | 400 ++++++++++++------ tests/unit.c | 116 +++-- wolfssh/certman.h | 17 +- wolfssh/internal.h | 2 + wolfssh/ssh.h | 9 + wolfssh/test.h | 6 +- 21 files changed, 1375 insertions(+), 574 deletions(-) diff --git a/.github/workflows/windows-cert-store-test.yml b/.github/workflows/windows-cert-store-test.yml index 88cbcff88..07176365c 100644 --- a/.github/workflows/windows-cert-store-test.yml +++ b/.github/workflows/windows-cert-store-test.yml @@ -16,6 +16,7 @@ on: branches: [ 'master', 'main', 'release/**' ] pull_request: branches: [ '*' ] + workflow_dispatch: env: WOLFSSL_SOLUTION_FILE_PATH: wolfssl64.sln @@ -32,6 +33,7 @@ env: jobs: build: runs-on: windows-latest + timeout-minutes: 30 steps: - uses: actions/checkout@v4 @@ -98,6 +100,7 @@ jobs: # the functional matrix never defines and so never builds. build-sys-ca-certs: runs-on: windows-latest + timeout-minutes: 30 steps: - uses: actions/checkout@v4 @@ -132,21 +135,68 @@ jobs: working-directory: ${{ github.workspace }}\wolfssh\ide\winvs run: nuget restore ${{env.SOLUTION_FILE_PATH}} + # Fails the build if the defines never reach wolfsshd.c, which otherwise + # compiles its #else branch and silently degrades to a duplicate of build. + - name: Guard that the defines reach wolfsshd.c + working-directory: ${{ github.workspace }}\wolfssh + shell: bash + run: | + printf '\n#if !defined(WOLFSSL_SYS_CA_CERTS) || !defined(WOLFSSH_WINDOWS_CERT_STORE) || !defined(WOLFSSH_SSHD)\n#error "CI: expected defines did not reach wolfsshd.c"\n#endif\n' >> apps/wolfsshd/wolfsshd.c + - name: Build wolfssh (compile check) working-directory: ${{ github.workspace }}\wolfssh\ide\winvs run: msbuild /m /p:PlatformToolset=v142 /p:Platform=${{env.BUILD_PLATFORM}} /p:WindowsTargetPlatformVersion=${{env.TARGET_PLATFORM}} /p:Configuration=${{env.WOLFSSH_BUILD_CONFIGURATION}} ${{env.SOLUTION_FILE_PATH}} + # Autotools coverage for --enable-windows-cert-store: the mingw link + # libraries and both error paths. Configure only, so no cross-built wolfSSL + # is needed; the wolfssl link test is satisfied from the autoconf cache. + configure-windows-cert-store: + runs-on: ubuntu-latest + timeout-minutes: 10 + + env: + WOLFSSL_CACHE: ac_cv_lib_wolfssl_wolfCrypt_Init=yes + + steps: + - uses: actions/checkout@v4 + + - name: Install mingw toolchain and autotools + run: | + sudo apt-get update + sudo apt-get install -y gcc-mingw-w64-x86-64 autoconf automake libtool + + - name: Generate configure + run: ./autogen.sh + + - name: mingw host links crypt32 and ncrypt + run: | + ./configure --host=x86_64-w64-mingw32 --enable-certs \ + --enable-windows-cert-store $WOLFSSL_CACHE + grep -q -- '-lcrypt32' Makefile + grep -q -- '-lncrypt' Makefile + + - name: Rejects a non-Windows host and a missing --enable-certs + run: | + ! ./configure --enable-certs --enable-windows-cert-store $WOLFSSL_CACHE + ! ./configure --host=x86_64-w64-mingw32 --enable-windows-cert-store \ + $WOLFSSL_CACHE + test: needs: build runs-on: windows-latest + timeout-minutes: 30 strategy: fail-fast: false matrix: include: + # user_ca_source: store replaces the file-based TrustedUserCAKeys + # with wolfSSH_TrustedUserCAStore, so the store is the only trust + # anchor for the client certificate. - server_key_source: file client_key_source: x509 key_algorithm: rsa - test_name: "Server-File-Client-X509" + user_ca_source: store + test_name: "Server-File-Client-X509-UserCAStore" - server_key_source: store client_key_source: x509 key_algorithm: rsa @@ -163,6 +213,11 @@ jobs: client_key_source: x509 key_algorithm: ecdsa test_name: "Server-Store-Client-X509-ECDSA" + - server_key_source: file + client_key_source: store + key_algorithm: rsa + client_key_algorithm: ecdsa + test_name: "Server-File-Client-Store-ECDSA" steps: - uses: actions/checkout@v4 @@ -194,6 +249,24 @@ jobs: # for x509 clients and imported into the store for store clients. cd keys bash renewcerts.sh testuser + + # renewcerts.sh always gives testuser fred's RSA key. Re-issue it with + # an EC key when the client store entry is meant to be ECDSA. + if [ "${{ matrix.client_key_algorithm }}" = "ecdsa" ]; then + touch index.txt + sed 's/fred/testuser/g' renewcerts.cnf > renewcerts-testuser.cnf + openssl ecparam -name prime256v1 -genkey -noout -out testuser-key.pem + openssl req -subj "/C=US/ST=WA/L=Seattle/O=wolfSSL Inc/OU=Development/CN=testuser/emailAddress=testuser@example.com" \ + -key testuser-key.pem -out testuser-cert.csr \ + -config renewcerts-testuser.cnf -new -nodes + openssl x509 -req -in testuser-cert.csr -days 3650 \ + -extfile renewcerts-testuser.cnf -extensions v3_testuser \ + -CA ca-cert-ecc.pem -CAkey ca-key-ecc.pem -out testuser-cert.pem \ + -set_serial 7 + openssl x509 -in testuser-cert.pem -outform DER -out testuser-cert.der + openssl ec -in testuser-key.pem -outform DER -out testuser-key.der + rm -f renewcerts-testuser.cnf testuser-cert.csr index.* + fi cd .. if [[ ! -f "keys/testuser-cert.der" || ! -f "keys/testuser-key.der" ]]; then @@ -305,6 +378,37 @@ jobs: Add-Content -Path $env:GITHUB_ENV -Value "CLIENT_CERT_SUBJECT=$cn" } + - name: Import test CA into a Windows store + if: matrix.user_ca_source == 'store' + working-directory: ${{ github.workspace }}\wolfssh + shell: pwsh + run: | + # LocalMachine so the wolfsshd service (LocalSystem) can read it. + # certutil creates the store if it does not already exist. + $caDer = (Resolve-Path "keys\ca-cert-ecc.der").Path + certutil -addstore -f wolfSSHTestCA $caDer + if ($LASTEXITCODE -ne 0) { + Write-Host "ERROR: certutil failed to add the CA to wolfSSHTestCA" + exit 1 + } + $caInStore = Get-ChildItem -Path "Cert:\LocalMachine\wolfSSHTestCA" -ErrorAction SilentlyContinue + if (-not $caInStore) { + Write-Host "ERROR: no certificate present in LocalMachine\wolfSSHTestCA" + exit 1 + } + Write-Host "CA imported: $($caInStore[0].Subject)" + + # An existing but empty store for the negative startup test. Adding + # then removing the CA leaves the store itself in place, so the + # failure is "no usable CA" and not "store not found". + certutil -addstore -f wolfSSHEmptyCA $caDer + if ($LASTEXITCODE -ne 0) { Write-Host "ERROR: certutil -addstore wolfSSHEmptyCA"; exit 1 } + Get-ChildItem -Path "Cert:\LocalMachine\wolfSSHEmptyCA" | Remove-Item -Force + if (Get-ChildItem -Path "Cert:\LocalMachine\wolfSSHEmptyCA" -ErrorAction SilentlyContinue) { + Write-Host "ERROR: wolfSSHEmptyCA is not empty" + exit 1 + } + - name: Create Windows user testuser shell: pwsh run: | @@ -333,6 +437,19 @@ jobs: # is not used but the file should exist. "" | Out-File -FilePath $authKeysFile -Encoding ASCII -NoNewline icacls $authKeysFile /grant "testuser:R" /q + if ($LASTEXITCODE -ne 0) { + Write-Host "ERROR: icacls failed on $authKeysFile" + exit 1 + } + + # wolfsshd serves SFTP from the home directory while impersonating + # testuser; the SFTP tests assert this name appears in the listing. + "marker" | Out-File -FilePath "$homeDir\wolfssh_sftp_marker.txt" -Encoding ASCII + icacls $homeDir /grant "testuser:(OI)(CI)RX" /T /q + if ($LASTEXITCODE -ne 0) { + Write-Host "ERROR: icacls failed on $homeDir" + exit 1 + } # Set ProfileImagePath so SHGetKnownFolderPath(FOLDERID_Profile) returns $homeDir # for testuser (GetHomeDirectory in wolfsshd uses that; otherwise it can fail for new users). @@ -351,13 +468,25 @@ jobs: PermitRootLogin yes "@ - # Server verifies client X509 certs against the test CA (PEM format, - # as per apps/wolfsshd/test/create_sshd_config.sh) - $caCertPath = (Resolve-Path "keys\ca-cert-ecc.pem").Path - $configContent += @" + # Server verifies client X509 certs against the test CA. Either from a + # PEM file (as per apps/wolfsshd/test/create_sshd_config.sh) or from + # the Windows store the CA was imported into, never both, so a + # successful client auth pins down which one supplied the anchor. + if ("${{ matrix.user_ca_source }}" -eq "store") { + $configContent += @" + + wolfSSH_TrustedUserCAStore yes + wolfSSH_WinUserStores CERT_STORE_PROV_SYSTEM + wolfSSH_WinUserPvPara wolfSSHTestCA + wolfSSH_WinUserDwFlags LOCAL_MACHINE + "@ + } else { + $caCertPath = (Resolve-Path "keys\ca-cert-ecc.pem").Path + $configContent += @" TrustedUserCAKeys $caCertPath "@ + } if ("${{ matrix.server_key_source }}" -eq "store") { # The certificate is part of the store entry; do NOT specify @@ -432,21 +561,25 @@ jobs: working-directory: ${{ github.workspace }} shell: pwsh run: | + # This job has no wolfssl checkout; the artifact unpacks at the + # workspace root, so search there rather than under wolfssl\. $sshdDir = Split-Path -Parent $env:SSHD_PATH + $searchRoot = "${{ github.workspace }}" - # If wolfssl.lib is next to wolfsshd.exe, it's a static build - no DLL needed - if (Test-Path (Join-Path $sshdDir "wolfssl.lib")) { - Write-Host "wolfssl.lib present beside wolfsshd.exe - static build; wolfssl.dll not required" + $wolfsslDll = Get-ChildItem -Path $searchRoot -Recurse -Filter "wolfssl.dll" -ErrorAction SilentlyContinue | + Select-Object -First 1 + if ($wolfsslDll) { + Copy-Item -Path $wolfsslDll.FullName -Destination (Join-Path $sshdDir "wolfssl.dll") -Force + Write-Host "Copied $($wolfsslDll.FullName) to $sshdDir" exit 0 } - $wolfsslDll = Get-ChildItem -Path "${{ github.workspace }}\wolfssl" -Recurse -Filter "wolfssl.dll" -ErrorAction SilentlyContinue | + $wolfsslLib = Get-ChildItem -Path $searchRoot -Recurse -Filter "wolfssl.lib" -ErrorAction SilentlyContinue | Select-Object -First 1 - if ($wolfsslDll) { - Copy-Item -Path $wolfsslDll.FullName -Destination (Join-Path $sshdDir "wolfssl.dll") -Force - Write-Host "Copied wolfssl.dll to $sshdDir" + if ($wolfsslLib) { + Write-Host "Static build ($($wolfsslLib.FullName)); wolfssl.dll not required" } else { - Write-Host "wolfssl.dll not found; if build is static (wolfssl.lib in output), this is OK" + Write-Host "WARNING: neither wolfssl.dll nor wolfssl.lib found under $searchRoot" } - name: Grant service (LocalSystem) access to config, keys, and executable @@ -464,6 +597,10 @@ jobs: } $sshdDir = (Resolve-Path (Split-Path -Parent $env:SSHD_PATH)).Path icacls $sshdDir /grant "NT AUTHORITY\SYSTEM:(OI)(CI)RX" /T /q + if ($LASTEXITCODE -ne 0) { + Write-Host "ERROR: icacls failed on $sshdDir" + exit 1 + } - name: Start echoserver with cert store host key if: matrix.server_key_source == 'store' @@ -485,6 +622,10 @@ jobs: $clientCert = (Resolve-Path (Join-Path $wolfsshRoot $env:CLIENT_CERT_FILE)).Path $echoArgs = @("-W", $spec, "-p", $port, "-a", $caCertPem, "-K", "testuser:$clientCert") + # echoserver serves SFTP from its working directory; the SFTP tests + # assert this name appears in the remote listing. + "marker" | Out-File -FilePath (Join-Path $exeDir "wolfssh_sftp_marker.txt") -Encoding ASCII + $argStr = $echoArgs -join " " $echoLogFile = Join-Path $wolfsshRoot "echoserver_debug.log" Add-Content -Path $env:GITHUB_ENV -Value "ECHOSERVER_LOG=$echoLogFile" @@ -503,12 +644,13 @@ jobs: # Wait for the port to be listening $timeout = 15 $elapsed = 0 - while ($elapsed -lt $timeout) { + $ready = $false + while ($elapsed -lt $timeout -and -not $ready) { Start-Sleep -Seconds 1 $elapsed++ try { $conn = New-Object System.Net.Sockets.TcpClient("127.0.0.1", $port) - if ($conn.Connected) { $conn.Close(); break } + if ($conn.Connected) { $conn.Close(); $ready = $true; continue } } catch {} if (-not (Get-Process -Name "echoserver" -ErrorAction SilentlyContinue)) { Write-Host "ERROR: echoserver exited before port was ready" @@ -516,7 +658,7 @@ jobs: exit 1 } } - if ($elapsed -ge $timeout) { + if (-not $ready) { Write-Host "ERROR: Port $port not listening after ${timeout}s" if (Test-Path $echoLogFile) { Get-Content $echoLogFile } exit 1 @@ -527,7 +669,11 @@ jobs: if: matrix.server_key_source == 'store' working-directory: ${{ github.workspace }}\wolfssh shell: pwsh + timeout-minutes: 3 run: | + # The plain host key algorithm wins negotiation here, so this covers + # the plain key slot and user auth; the x509v3 slot is covered by the + # next step. $testPort = ${{env.TEST_PORT}} $sftpPath = $env:SFTP_PATH @@ -571,17 +717,23 @@ jobs: Write-Host "ERROR: SFTP against echoserver failed" exit 1 } + if ((Get-Content sftp_echo_output.txt -Raw) -notmatch "wolfssh_sftp_marker.txt") { + Write-Host "ERROR: remote listing did not contain the marker file" + exit 1 + } Write-Host "SFTP against echoserver succeeded" - name: Test SFTP against echoserver with x509v3 host key - if: matrix.server_key_source == 'store' && matrix.key_algorithm == 'ecdsa' + if: matrix.server_key_source == 'store' working-directory: ${{ github.workspace }}\wolfssh shell: pwsh + timeout-minutes: 3 run: | # Force the x509v3 host key algorithm so the cert store certificate # itself is sent as K_S and verified by the client, exercising the # X.509 host-key slot instead of the plain-key slot. The server cert - # is self-signed, so it is its own trust anchor (-A). + # is self-signed, so it is its own trust anchor (-A), which also means + # a fallback to a file-based host key could not pass this step. $testPort = ${{env.TEST_PORT}} $sftpPath = $env:SFTP_PATH @@ -599,7 +751,11 @@ jobs: $sftpArgs += "-i", (Resolve-Path $env:CLIENT_KEY_FILE).Path } $sftpArgs += "-A", (Resolve-Path "server-store-cert.der").Path, "-X" - $sftpArgs += "-k", "x509v3-ecdsa-sha2-nistp256" + if ("${{ matrix.key_algorithm }}" -eq "ecdsa") { + $sftpArgs += "-k", "x509v3-ecdsa-sha2-nistp256" + } else { + $sftpArgs += "-k", "x509v3-ssh-rsa" + } Write-Host "Running: $sftpPath $($sftpArgs -join ' ')" $process = Start-Process -FilePath $sftpPath ` @@ -624,6 +780,10 @@ jobs: Write-Host "ERROR: SFTP with x509v3 host key failed" exit 1 } + if ((Get-Content sftp_x509_output.txt -Raw) -notmatch "wolfssh_sftp_marker.txt") { + Write-Host "ERROR: remote listing did not contain the marker file" + exit 1 + } Write-Host "SFTP with x509v3 host key succeeded" - name: Stop echoserver before wolfsshd test @@ -640,6 +800,41 @@ jobs: # Clear the env var so cleanup step doesn't try again Add-Content -Path $env:GITHUB_ENV -Value "ECHOSERVER_PID=" + - name: wolfSSHd refuses to start with an empty user CA store + if: matrix.user_ca_source == 'store' + working-directory: ${{ github.workspace }}\wolfssh + shell: pwsh + timeout-minutes: 3 + run: | + # -D -t runs the config load and CTX setup in the foreground and then + # returns without listening. Windows main() always returns 0, so the + # log is what is asserted on. + (Get-Content sshd_config_test) -replace 'wolfSSHTestCA', 'wolfSSHEmptyCA' | + Out-File -FilePath sshd_config_empty_ca -Encoding ASCII + $configPathFull = (Resolve-Path "sshd_config_empty_ca").Path + + Start-Process -FilePath (Resolve-Path $env:SSHD_PATH).Path ` + -ArgumentList @("-D", "-d", "-t", "-f", $configPathFull) ` + -RedirectStandardOutput "sshd_empty_ca_out.txt" ` + -RedirectStandardError "sshd_empty_ca_err.txt" ` + -Wait -NoNewWindow + + $log = "" + foreach ($f in @("sshd_empty_ca_out.txt", "sshd_empty_ca_err.txt")) { + if (Test-Path $f) { $log += (Get-Content $f -Raw) } + } + Write-Host "=== wolfsshd output ===" + Write-Host $log + # Windows may prune the registry key once the last cert is removed, in + # which case the store fails to open instead of enumerating empty. + # Either way startup must not succeed. + if ($log -notmatch "No usable CA certificates found in store" -and + $log -notmatch "Unable to open user CA cert store") { + Write-Host "ERROR: wolfsshd did not reject the empty user CA store" + exit 1 + } + Write-Host "wolfsshd rejected the empty user CA store" + - name: Start wolfSSHd as Windows service working-directory: ${{ github.workspace }}\wolfssh shell: pwsh @@ -696,6 +891,7 @@ jobs: - name: Test SFTP connection against wolfsshd working-directory: ${{ github.workspace }}\wolfssh shell: pwsh + timeout-minutes: 3 run: | $testPort = ${{env.TEST_PORT}} $sftpPath = $env:SFTP_PATH @@ -753,6 +949,12 @@ jobs: Write-Host "ERROR: SFTP client exited with code $($process.ExitCode)" exit 1 } + # ls discards errors and doCmds always returns success, so assert on + # the listing itself rather than on the exit code alone. + if ((Get-Content sftp_output.txt -Raw) -notmatch "wolfssh_sftp_marker.txt") { + Write-Host "ERROR: remote listing did not contain the marker file" + exit 1 + } Write-Host "Test completed - key exchange and SFTP connection succeeded" - name: Cleanup @@ -785,4 +987,8 @@ jobs: Get-ChildItem -Path "Cert:\LocalMachine\My" | Where-Object { $_.Subject -like "*wolfSSH-Test*" } | Remove-Item -Force -ErrorAction SilentlyContinue + foreach ($s in @("wolfSSHTestCA", "wolfSSHEmptyCA")) { + Get-ChildItem -Path "Cert:\LocalMachine\$s" -ErrorAction SilentlyContinue | + Remove-Item -Force -ErrorAction SilentlyContinue + } Write-Host "Cleaned up test certificates" diff --git a/apps/wolfsshd/auth.c b/apps/wolfsshd/auth.c index f8e030fc0..cef2669d5 100644 --- a/apps/wolfsshd/auth.c +++ b/apps/wolfsshd/auth.c @@ -54,7 +54,7 @@ #include #include -#if defined(WOLFSSL_FPKI) || defined(_WIN32) +#if defined(WOLFSSH_CERTS) && (defined(WOLFSSL_FPKI) || defined(_WIN32)) /* Used to bind a client certificate to the requested user name: by UPN * with FPKI, by subject CN on Windows builds without FPKI. */ #include @@ -1883,7 +1883,8 @@ static int CAKeysFileDiffers(const char* a, const char* b) /* Returns 1 when the certificate UPN @ in name[0..nameSz) * authorizes login as 'usr'. allowList is a whitespace/comma list of permitted * realms; NULL/empty matches the local part only, else domain must be listed. */ -#if defined(WOLFSSL_FPKI) || defined(WOLFSSHD_UNIT_TEST) +#if (defined(WOLFSSL_FPKI) && defined(WOLFSSH_CERTS)) || \ + defined(WOLFSSHD_UNIT_TEST) WOLFSSHD_STATIC int MatchUPNToUser(const char* usr, const char* name, int nameSz, const char* allowList) { @@ -1941,7 +1942,7 @@ WOLFSSHD_STATIC int MatchUPNToUser(const char* usr, const char* name, return ret; } -#endif /* WOLFSSL_FPKI || WOLFSSHD_UNIT_TEST */ +#endif /* (WOLFSSL_FPKI && WOLFSSH_CERTS) || WOLFSSHD_UNIT_TEST */ /* @@ -2085,12 +2086,15 @@ static int RequestAuthentication(WS_UserAuthData* authData, ret = WOLFSSH_USERAUTH_REJECTED; } - #if defined(WOLFSSL_FPKI) || defined(_WIN32) + #if defined(WOLFSSH_CERTS) && (defined(WOLFSSL_FPKI) || defined(_WIN32)) if (ret == WOLFSSH_USERAUTH_SUCCESS && authData->type == WOLFSSH_USERAUTH_PUBLICKEY) { /* Bind the certificate to the requested user name via UPN with FPKI or - * CN without FPKI. */ - if (authData->sf.publicKey.isCert) { + * CN without FPKI. Only done when relying on the CA; an + * AuthorizedKeysFile entry is itself an explicit user to cert binding + * and is checked below. */ + if (authData->sf.publicKey.isCert && + !wolfSSHD_ConfigGetAuthKeysFileSet(usrConf)) { DecodedCert* dCert; #ifdef WOLFSSH_SMALL_STACK dCert = (DecodedCert*)WMALLOC(sizeof(DecodedCert), NULL, @@ -2144,12 +2148,18 @@ static int RequestAuthentication(WS_UserAuthData* authData, "not set; certificate UPN domain is not checked"); } #else - /* Without FPKI compare subject CN with user name. - * Windows account names are case-insensitive, so match - * the CN the same way there. */ - if (dCert->subjectCN != NULL && + /* Without FPKI compare subject CN with user name. Only + * reachable on Windows, where account names are + * case-insensitive, so match the CN the same way when the + * Windows string API is available. + * + * This is a name match only. There is no analogue of + * AuthorizedUPNDomains here, so any CA in the trust store + * may assert any CN; the trusted user CA set is the whole + * of the issuer policy. */ + if (dCert->subjectCN != NULL && dCert->subjectCNLen > 0 && (int)XSTRLEN(usr) == dCert->subjectCNLen && - #ifdef _WIN32 + #ifdef USE_WINDOWS_API WSTRNCASECMP(usr, dCert->subjectCN, (size_t)dCert->subjectCNLen) == 0 #else @@ -2158,12 +2168,18 @@ static int RequestAuthentication(WS_UserAuthData* authData, #endif ) { usrMatch = 1; + /* warn per auth attempt so the weaker binding is + * visible, no shared state */ + wolfSSH_Log(WS_LOG_WARN, "[SSHD] certificate bound to " + "user by subject CN only; no issuer constraint is " + "applied, keep the trusted user CA set narrow"); } #endif if (usrMatch == 0) { wolfSSH_Log(WS_LOG_ERROR, "[SSHD] incorrect user cert " - "sent"); + "sent; certificate identity does not match the " + "requested user (user=%s)", usr); ret = WOLFSSH_USERAUTH_INVALID_PUBLICKEY; } } @@ -2196,10 +2212,11 @@ static int RequestAuthentication(WS_UserAuthData* authData, ret = WOLFSSH_USERAUTH_REJECTED; } else { - #ifdef _WIN32 - /* The UPN/CN-vs-username check above already bound the - * certificate to the requested user. Still need to get - * the users token on Windows. */ + #if defined(WOLFSSH_CERTS) && defined(_WIN32) + /* Bound to the requested user above by certificate UPN with + * FPKI, or by subject CN otherwise; which of the two is in + * force is fixed by the wolfSSL build, not by configuration. + * Still need to get the users token on Windows. */ wolfSSH_Log(WS_LOG_INFO, "[SSHD] Relying on CA for public key check"); rc = SetupUserTokenWin(usr, &authData->sf.publicKey, @@ -2213,7 +2230,7 @@ static int RequestAuthentication(WS_UserAuthData* authData, "[SSHD] Error getting users token."); ret = WOLFSSH_USERAUTH_FAILURE; } - #elif defined(WOLFSSL_FPKI) + #elif defined(WOLFSSH_CERTS) && defined(WOLFSSL_FPKI) /* The UPN-vs-username check above already bound the certificate * to the requested user, so the CA-verified chain is * sufficient. */ @@ -2221,10 +2238,11 @@ static int RequestAuthentication(WS_UserAuthData* authData, "[SSHD] Relying on CA for public key check"); ret = WOLFSSH_USERAUTH_SUCCESS; #else - /* Without FPKI the certificate UPN/principal cannot be read, so - * the requested user cannot be bound to the certificate. Fail - * closed: require AuthorizedKeysFile (per-user key/cert mapping) - * or a wolfSSL build with FPKI. */ + /* No binding ran above: either the certificate UPN/principal + * cannot be read without FPKI, or this build has no + * certificate support at all. Fail closed: require + * AuthorizedKeysFile (per-user key/cert mapping) or a wolfSSL + * build with FPKI. */ wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Certificate authentication cannot bind the requested " "user without FPKI or AuthorizedKeysFile; rejecting " diff --git a/apps/wolfsshd/configuration.c b/apps/wolfsshd/configuration.c index 50978793d..9c6af9acf 100644 --- a/apps/wolfsshd/configuration.c +++ b/apps/wolfsshd/configuration.c @@ -99,11 +99,11 @@ struct WOLFSSHD_CONFIG { char* forceCmd; char* pidFile; char* authorizedUPNDomains; /* allowlist of UPN realms for cert auth */ -#ifdef USE_WINDOWS_API +#ifdef WOLFSSH_WINDOWS_CERT_STORE char* winUserStores; char* winUserDwFlags; char* winUserPvPara; -#endif /* USE_WINDOWS_API */ +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ WOLFSSHD_CONFIG* next; /* next config in list */ long loginTimer; word16 port; @@ -350,6 +350,46 @@ static WOLFSSHD_CONFIG* wolfSSHD_ConfigCopy(WOLFSSHD_CONFIG* conf) newConf->heap); } +#ifdef WOLFSSH_WINDOWS_CERT_STORE + if (ret == WS_SUCCESS && conf->hostKeyStore) { + ret = CreateString(&newConf->hostKeyStore, conf->hostKeyStore, + (int)WSTRLEN(conf->hostKeyStore), + newConf->heap); + } + + if (ret == WS_SUCCESS && conf->hostKeyStoreSubject) { + ret = CreateString(&newConf->hostKeyStoreSubject, + conf->hostKeyStoreSubject, + (int)WSTRLEN(conf->hostKeyStoreSubject), + newConf->heap); + } + + if (ret == WS_SUCCESS && conf->hostKeyStoreFlags) { + ret = CreateString(&newConf->hostKeyStoreFlags, + conf->hostKeyStoreFlags, + (int)WSTRLEN(conf->hostKeyStoreFlags), + newConf->heap); + } + + if (ret == WS_SUCCESS && conf->winUserStores) { + ret = CreateString(&newConf->winUserStores, conf->winUserStores, + (int)WSTRLEN(conf->winUserStores), + newConf->heap); + } + + if (ret == WS_SUCCESS && conf->winUserDwFlags) { + ret = CreateString(&newConf->winUserDwFlags, conf->winUserDwFlags, + (int)WSTRLEN(conf->winUserDwFlags), + newConf->heap); + } + + if (ret == WS_SUCCESS && conf->winUserPvPara) { + ret = CreateString(&newConf->winUserPvPara, conf->winUserPvPara, + (int)WSTRLEN(conf->winUserPvPara), + newConf->heap); + } +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ + if (ret == WS_SUCCESS) { newConf->loginTimer = conf->loginTimer; newConf->port = conf->port; @@ -360,6 +400,8 @@ static WOLFSSHD_CONFIG* wolfSSHD_ConfigCopy(WOLFSSHD_CONFIG* conf) newConf->permitEmptyPasswords = conf->permitEmptyPasswords; newConf->authKeysFileSet = conf->authKeysFileSet; newConf->strictModes = conf->strictModes; + newConf->useSystemCA = conf->useSystemCA; + newConf->useUserCAStore = conf->useUserCAStore; } else { wolfSSHD_ConfigFree(newConf); @@ -400,12 +442,10 @@ void wolfSSHD_ConfigFree(WOLFSSHD_CONFIG* conf) FreeString(¤t->hostKeyStore, heap); FreeString(¤t->hostKeyStoreSubject, heap); FreeString(¤t->hostKeyStoreFlags, heap); -#endif /* WOLFSSH_WINDOWS_CERT_STORE */ -#ifdef USE_WINDOWS_API FreeString(¤t->winUserStores, heap); FreeString(¤t->winUserDwFlags, heap); FreeString(¤t->winUserPvPara, heap); -#endif /* USE_WINDOWS_API */ +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ WFREE(current, heap, DYNTYPE_SSHD); current = next; @@ -451,11 +491,9 @@ enum { OPT_STRICT_MODES = 25, OPT_TRUSTED_SYSTEM_CA_KEYS = 26, OPT_TRUSTED_USER_CA_STORE = 27, -#ifdef USE_WINDOWS_API OPT_WIN_USER_STORES = 28, OPT_WIN_USER_DW_FLAGS = 29, OPT_WIN_USER_PV_PARA = 30, -#endif /* USE_WINDOWS_API */ OPT_AUTHORIZED_UPN_DOMAINS = 31 }; static const CONFIG_OPTION options[] = { @@ -495,11 +533,9 @@ static const CONFIG_OPTION options[] = { {OPT_STRICT_MODES, "StrictModes"}, {OPT_TRUSTED_SYSTEM_CA_KEYS, "wolfSSH_TrustedSystemCAKeys"}, {OPT_TRUSTED_USER_CA_STORE, "wolfSSH_TrustedUserCAStore"}, -#ifdef USE_WINDOWS_API {OPT_WIN_USER_STORES, "wolfSSH_WinUserStores"}, {OPT_WIN_USER_DW_FLAGS, "wolfSSH_WinUserDwFlags"}, {OPT_WIN_USER_PV_PARA, "wolfSSH_WinUserPvPara"}, -#endif /* USE_WINDOWS_API */ {OPT_AUTHORIZED_UPN_DOMAINS, "AuthorizedUPNDomains"}, }; #define NUM_OPTIONS ((int)(sizeof(options) / sizeof(*options))) @@ -1388,7 +1424,7 @@ static int HandleConfigOption(WOLFSSHD_CONFIG** conf, int opt, if (ret == WS_SUCCESS) ret = wolfSSHD_ConfigSetUserCAStore(*conf, value); break; - #ifdef USE_WINDOWS_API + #ifdef WOLFSSH_WINDOWS_CERT_STORE case OPT_WIN_USER_STORES: ret = CheckNotInMatch(*conf, "wolfSSH_WinUserStores"); if (ret == WS_SUCCESS) @@ -1404,30 +1440,40 @@ static int HandleConfigOption(WOLFSSHD_CONFIG** conf, int opt, if (ret == WS_SUCCESS) ret = wolfSSHD_ConfigSetWinUserPvPara(*conf, value); break; - #endif /* USE_WINDOWS_API */ + #else + case OPT_WIN_USER_STORES: + case OPT_WIN_USER_DW_FLAGS: + case OPT_WIN_USER_PV_PARA: + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] wolfSSH_WinUser* options require a " + "WOLFSSH_WINDOWS_CERT_STORE build"); + ret = WS_NOT_COMPILED; + break; + #endif /* WOLFSSH_WINDOWS_CERT_STORE */ case OPT_AUTHORIZED_UPN_DOMAINS: ret = SetListString(&(*conf)->authorizedUPNDomains, full, fullSz, (*conf)->heap); break; #ifdef WOLFSSH_WINDOWS_CERT_STORE case OPT_HOST_KEY_STORE: - wolfSSH_Log(WS_LOG_INFO, - "[SSHD] Parsed HostKeyStore = '%s'", value); - ret = SetFileString(&(*conf)->hostKeyStore, value, (*conf)->heap); + ret = CheckNotInMatch(*conf, "HostKeyStore"); + if (ret == WS_SUCCESS) + ret = SetFileString(&(*conf)->hostKeyStore, value, + (*conf)->heap); break; case OPT_HOST_KEY_STORE_SUBJECT: - wolfSSH_Log(WS_LOG_INFO, - "[SSHD] Parsed HostKeyStoreSubject = '%s'", value); + ret = CheckNotInMatch(*conf, "HostKeyStoreSubject"); /* use the full line remainder so a CN containing spaces is * kept instead of being cut at the first token */ - ret = SetListString(&(*conf)->hostKeyStoreSubject, full, fullSz, - (*conf)->heap); + if (ret == WS_SUCCESS) + ret = SetListString(&(*conf)->hostKeyStoreSubject, full, + fullSz, (*conf)->heap); break; case OPT_HOST_KEY_STORE_FLAGS: - wolfSSH_Log(WS_LOG_INFO, - "[SSHD] Parsed HostKeyStoreFlags = '%s'", value); - ret = SetFileString(&(*conf)->hostKeyStoreFlags, value, - (*conf)->heap); + ret = CheckNotInMatch(*conf, "HostKeyStoreFlags"); + if (ret == WS_SUCCESS) + ret = SetFileString(&(*conf)->hostKeyStoreFlags, value, + (*conf)->heap); break; #else case OPT_HOST_KEY_STORE: @@ -1865,25 +1911,18 @@ int wolfSSHD_ConfigSetUserCAStore(WOLFSSHD_CONFIG* conf, const char* value) return ret; } -#ifdef USE_WINDOWS_API -char* wolfSSHD_ConfigGetWinUserStores(WOLFSSHD_CONFIG* conf) +#ifdef WOLFSSH_WINDOWS_CERT_STORE +/* Returns the configured store provider, or NULL when not configured. The + * caller decides what an unset value means. */ +char* wolfSSHD_ConfigGetWinUserStores(const WOLFSSHD_CONFIG* conf) { - if (conf != NULL) { - if (conf->winUserStores == NULL) { - /* If no value was specified, default to CERT_STORE_PROV_SYSTEM */ - if (CreateString(&conf->winUserStores, "CERT_STORE_PROV_SYSTEM", - (int)WSTRLEN("CERT_STORE_PROV_SYSTEM"), conf->heap) - != WS_SUCCESS) { - wolfSSH_Log(WS_LOG_ERROR, - "[SSHD] Unable to create default winUserStores"); - return NULL; - } - } + char* ret = NULL; - return conf->winUserStores; + if (conf != NULL) { + ret = conf->winUserStores; } - return NULL; + return ret; } int wolfSSHD_ConfigSetWinUserStores(WOLFSSHD_CONFIG* conf, const char* value) @@ -1904,26 +1943,16 @@ int wolfSSHD_ConfigSetWinUserStores(WOLFSSHD_CONFIG* conf, const char* value) return ret; } -char* wolfSSHD_ConfigGetWinUserDwFlags(WOLFSSHD_CONFIG* conf) +/* Returns the configured store location, or NULL when not configured. */ +char* wolfSSHD_ConfigGetWinUserDwFlags(const WOLFSSHD_CONFIG* conf) { - if (conf != NULL) { - if (conf->winUserDwFlags == NULL) { - /* If no value was specified, default to - * CERT_SYSTEM_STORE_CURRENT_USER */ - if (CreateString(&conf->winUserDwFlags, - "CERT_SYSTEM_STORE_CURRENT_USER", - (int)WSTRLEN("CERT_SYSTEM_STORE_CURRENT_USER"), - conf->heap) != WS_SUCCESS) { - wolfSSH_Log(WS_LOG_ERROR, - "[SSHD] Unable to create default winUserDwFlags"); - return NULL; - } - } + char* ret = NULL; - return conf->winUserDwFlags; + if (conf != NULL) { + ret = conf->winUserDwFlags; } - return NULL; + return ret; } int wolfSSHD_ConfigSetWinUserDwFlags(WOLFSSHD_CONFIG* conf, const char* value) @@ -1944,23 +1973,18 @@ int wolfSSHD_ConfigSetWinUserDwFlags(WOLFSSHD_CONFIG* conf, const char* value) return ret; } -char* wolfSSHD_ConfigGetWinUserPvPara(WOLFSSHD_CONFIG* conf) +/* Returns the configured store name, or NULL when not configured. There is + * deliberately no default: this store is a trust anchor source for client + * certificate auth and must be picked by the administrator. */ +char* wolfSSHD_ConfigGetWinUserPvPara(const WOLFSSHD_CONFIG* conf) { - if (conf != NULL) { - if (conf->winUserPvPara == NULL) { - /* If no value was specified, default to MY */ - if (CreateString(&conf->winUserPvPara, "MY", - (int)WSTRLEN("MY"), conf->heap) != WS_SUCCESS) { - wolfSSH_Log(WS_LOG_ERROR, - "[SSHD] Unable to create default winUserPvPara"); - return NULL; - } - } + char* ret = NULL; - return conf->winUserPvPara; + if (conf != NULL) { + ret = conf->winUserPvPara; } - return NULL; + return ret; } int wolfSSHD_ConfigSetWinUserPvPara(WOLFSSHD_CONFIG* conf, const char* value) @@ -1980,7 +2004,7 @@ int wolfSSHD_ConfigSetWinUserPvPara(WOLFSSHD_CONFIG* conf, const char* value) return ret; } -#endif /* USE_WINDOWS_API */ +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ char* wolfSSHD_ConfigGetUserCAKeysFile(const WOLFSSHD_CONFIG* conf) { diff --git a/apps/wolfsshd/configuration.h b/apps/wolfsshd/configuration.h index 554aeba50..367c92cec 100644 --- a/apps/wolfsshd/configuration.h +++ b/apps/wolfsshd/configuration.h @@ -73,14 +73,14 @@ int wolfSSHD_ConfigSetSystemCA(WOLFSSHD_CONFIG* conf, const char* value); int wolfSSHD_ConfigGetSystemCA(const WOLFSSHD_CONFIG* conf); int wolfSSHD_ConfigSetUserCAStore(WOLFSSHD_CONFIG* conf, const char* value); int wolfSSHD_ConfigGetUserCAStore(const WOLFSSHD_CONFIG* conf); -#ifdef USE_WINDOWS_API -char* wolfSSHD_ConfigGetWinUserStores(WOLFSSHD_CONFIG* conf); +#ifdef WOLFSSH_WINDOWS_CERT_STORE +char* wolfSSHD_ConfigGetWinUserStores(const WOLFSSHD_CONFIG* conf); int wolfSSHD_ConfigSetWinUserStores(WOLFSSHD_CONFIG* conf, const char* value); -char* wolfSSHD_ConfigGetWinUserDwFlags(WOLFSSHD_CONFIG* conf); +char* wolfSSHD_ConfigGetWinUserDwFlags(const WOLFSSHD_CONFIG* conf); int wolfSSHD_ConfigSetWinUserDwFlags(WOLFSSHD_CONFIG* conf, const char* value); -char* wolfSSHD_ConfigGetWinUserPvPara(WOLFSSHD_CONFIG* conf); +char* wolfSSHD_ConfigGetWinUserPvPara(const WOLFSSHD_CONFIG* conf); int wolfSSHD_ConfigSetWinUserPvPara(WOLFSSHD_CONFIG* conf, const char* value); -#endif /* USE_WINDOWS_API */ +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ int wolfSSHD_ConfigSetUserCAKeysFile(WOLFSSHD_CONFIG* conf, const char* file); word16 wolfSSHD_ConfigGetPort(const WOLFSSHD_CONFIG* conf); char* wolfSSHD_ConfigGetAuthKeysFile(const WOLFSSHD_CONFIG* conf); diff --git a/apps/wolfsshd/wolfsshd.c b/apps/wolfsshd/wolfsshd.c index f97323575..2dff553f0 100644 --- a/apps/wolfsshd/wolfsshd.c +++ b/apps/wolfsshd/wolfsshd.c @@ -37,17 +37,29 @@ #include #include #include +#ifdef WOLFSSH_CERTS + #include +#endif #ifdef WOLFSSH_WINDOWS_CERT_STORE #include #include #include + #include + #include + #include + #ifndef CERT_SYSTEM_STORE_LOCATION_MASK + #define CERT_SYSTEM_STORE_LOCATION_MASK 0x00FF0000 + #endif #ifndef CERT_SYSTEM_STORE_CURRENT_USER #define CERT_SYSTEM_STORE_CURRENT_USER 0x00010000 #endif #ifndef CERT_SYSTEM_STORE_LOCAL_MACHINE #define CERT_SYSTEM_STORE_LOCAL_MACHINE 0x00020000 #endif + #ifndef CERT_SYSTEM_STORE_USERS + #define CERT_SYSTEM_STORE_USERS 0x00060000 + #endif #endif /* WOLFSSH_WINDOWS_CERT_STORE */ #define WOLFSSH_TEST_SERVER @@ -355,11 +367,86 @@ static void CleanupCTX(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX** ctx, } #if defined(WOLFSSH_CERTS) && defined(WOLFSSH_WINDOWS_CERT_STORE) -/* Add every certificate in the configured Windows store (winUserPvPara name, - * winUserDwFlags location) as a trusted root CA. Returns WS_SUCCESS on +/* Parse a Windows system store location, given either as a CERT_SYSTEM_STORE_* + * name (long or short form) or as a number in strtoul() base 0 form, so both + * 65536 and 0x00010000 work. Only location bits are accepted; anything else is + * either not a location or a control flag (e.g. CERT_STORE_DELETE_FLAG) that + * would make CertOpenStore destructive. Returns WS_SUCCESS on success. */ +static int ParseCertStoreLocation(const char* in, word32* out) +{ + int ret = WS_SUCCESS; + unsigned long val; + char* end; + + if (in == NULL || out == NULL || *in == '\0') { + return WS_BAD_ARGUMENT; + } + + if (WSTRCMP(in, "CURRENT_USER") == 0 || + WSTRCMP(in, "CERT_SYSTEM_STORE_CURRENT_USER") == 0) { + *out = (word32)CERT_SYSTEM_STORE_CURRENT_USER; + } + else if (WSTRCMP(in, "LOCAL_MACHINE") == 0 || + WSTRCMP(in, "CERT_SYSTEM_STORE_LOCAL_MACHINE") == 0) { + *out = (word32)CERT_SYSTEM_STORE_LOCAL_MACHINE; + } + else if (WSTRCMP(in, "USERS") == 0 || + WSTRCMP(in, "CERT_SYSTEM_STORE_USERS") == 0) { + *out = (word32)CERT_SYSTEM_STORE_USERS; + } + else { + end = NULL; + errno = 0; + val = strtoul(in, &end, 0); + if (end == in || *end != '\0' || errno == ERANGE) { + ret = WS_BAD_ARGUMENT; + } + else if ((val & (unsigned long)CERT_SYSTEM_STORE_LOCATION_MASK) == 0 || + (val & ~(unsigned long)CERT_SYSTEM_STORE_LOCATION_MASK) != 0) { + ret = WS_BAD_ARGUMENT; + } + else { + *out = (word32)val; + } + } + + return ret; +} + +/* Returns 1 when der holds an X.509 certificate with basicConstraints + * CA:TRUE, 0 otherwise. */ +static int CertIsCA(const byte* der, word32 derSz) +{ + DecodedCert* dCert; + int isCA = 0; +#ifdef WOLFSSH_SMALL_STACK + dCert = (DecodedCert*)WMALLOC(sizeof(DecodedCert), NULL, DYNTYPE_CERT); + if (dCert == NULL) { + return 0; + } +#else + DecodedCert sdCert; + + dCert = &sdCert; +#endif + + wc_InitDecodedCert(dCert, der, derSz, NULL); + if (wc_ParseCert(dCert, CERT_TYPE, NO_VERIFY, NULL) == 0) { + isCA = (dCert->isCA != 0); + } + FreeDecodedCert(dCert); +#ifdef WOLFSSH_SMALL_STACK + WFREE(dCert, NULL, DYNTYPE_CERT); +#endif + + return isCA; +} + +/* Add every CA certificate in the configured Windows store (winUserPvPara + * name, winUserDwFlags location) as a trusted root CA. Returns WS_SUCCESS on * success. */ -static int LoadUserCACertsFromStore(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX* ctx, - void* heap) +static int LoadUserCACertsFromStore(const WOLFSSHD_CONFIG* conf, + WOLFSSH_CTX* ctx, void* heap) { int ret = WS_SUCCESS; char* storeNameStr; @@ -371,18 +458,27 @@ static int LoadUserCACertsFromStore(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX* ctx, HCERTSTORE hStore = NULL; PCCERT_CONTEXT pCertContext = NULL; word32 loaded = 0; + word32 skipped = 0; storeNameStr = wolfSSHD_ConfigGetWinUserPvPara(conf); dwFlagsStr = wolfSSHD_ConfigGetWinUserDwFlags(conf); providerStr = wolfSSHD_ConfigGetWinUserStores(conf); + + /* Every certificate in this store becomes a trust anchor for client + * authentication, so the administrator must name it. There is no default: + * guessing one silently would pick a store the administrator never + * reviewed. */ if (storeNameStr == NULL) { - wolfSSH_Log(WS_LOG_ERROR, "[SSHD] No user CA store name configured"); + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] wolfSSH_TrustedUserCAStore is enabled but no store name " + "is configured. Set wolfSSH_WinUserPvPara to the store holding " + "the client CA certificates to trust, e.g. 'Root' or 'CA'."); return WS_BAD_ARGUMENT; } - /* Only the system-store provider is supported here. Fail rather than - * silently load trust anchors from a different provider than the one - * configured. */ + /* Only the system-store provider is supported here. NULL means the option + * was not given, which is that same provider. Fail rather than silently + * load trust anchors from a different provider than the one configured. */ if (providerStr != NULL && WSTRCMP(providerStr, "CERT_STORE_PROV_SYSTEM") != 0) { wolfSSH_Log(WS_LOG_ERROR, @@ -391,28 +487,13 @@ static int LoadUserCACertsFromStore(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX* ctx, return WS_BAD_ARGUMENT; } - if (dwFlagsStr != NULL) { - if (WSTRCMP(dwFlagsStr, "CURRENT_USER") == 0 || - WSTRCMP(dwFlagsStr, "CERT_SYSTEM_STORE_CURRENT_USER") == 0) { - dwFlags = CERT_SYSTEM_STORE_CURRENT_USER; - } - else if (WSTRCMP(dwFlagsStr, "LOCAL_MACHINE") == 0 || - WSTRCMP(dwFlagsStr, "CERT_SYSTEM_STORE_LOCAL_MACHINE") == 0) { - dwFlags = CERT_SYSTEM_STORE_LOCAL_MACHINE; - } - else { - /* Fall back to a raw numeric value, but only accept system-store - * location bits. Anything else is either not a location or a - * control flag (e.g. CERT_STORE_DELETE_FLAG) that would make - * CertOpenStore destructive. */ - dwFlags = (word32)atoi(dwFlagsStr); - if ((dwFlags & (word32)CERT_SYSTEM_STORE_LOCATION_MASK) == 0 || - (dwFlags & ~(word32)CERT_SYSTEM_STORE_LOCATION_MASK) != 0) { - wolfSSH_Log(WS_LOG_ERROR, - "[SSHD] Unrecognized user CA store flags '%s'", dwFlagsStr); - return WS_BAD_ARGUMENT; - } - } + /* An unset location keeps the CERT_SYSTEM_STORE_CURRENT_USER default set + * above. */ + if (dwFlagsStr != NULL && + ParseCertStoreLocation(dwFlagsStr, &dwFlags) != WS_SUCCESS) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Unrecognized user CA store flags '%s'", dwFlagsStr); + return WS_BAD_ARGUMENT; } wStoreNameLen = MultiByteToWideChar(CP_UTF8, 0, storeNameStr, -1, NULL, 0); @@ -450,6 +531,16 @@ static int LoadUserCACertsFromStore(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX* ctx, pCertContext->cbCertEncoded == 0) { continue; } + /* wolfSSL does not enforce basicConstraints CA:TRUE for user-loaded + * trust anchors, so an end-entity certificate sitting in the store + * would become a login authority. Filter it out here. */ + if (!CertIsCA(pCertContext->pbCertEncoded, + (word32)pCertContext->cbCertEncoded)) { + skipped++; + wolfSSH_Log(WS_LOG_INFO, + "[SSHD] Skipping a non-CA cert in store '%s'", storeNameStr); + continue; + } if (wolfSSH_CTX_AddRootCert_buffer(ctx, (const byte*)pCertContext->pbCertEncoded, (word32)pCertContext->cbCertEncoded, @@ -468,14 +559,15 @@ static int LoadUserCACertsFromStore(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX* ctx, if (loaded == 0) { wolfSSH_Log(WS_LOG_ERROR, - "[SSHD] No usable CA certificates found in store '%s'", - storeNameStr); + "[SSHD] No usable CA certificates found in store '%s' (%u non-CA " + "cert(s) skipped)", storeNameStr, skipped); ret = WS_FATAL_ERROR; } else { - wolfSSH_Log(WS_LOG_INFO, - "[SSHD] Loaded %u CA certificate(s) from store '%s'", - loaded, storeNameStr); + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Trusting %u CA certificate(s) from store '%s' " + "(location 0x%08lx) for client authentication, %u non-CA cert(s) " + "skipped", loaded, storeNameStr, (unsigned long)dwFlags, skipped); } return ret; @@ -523,23 +615,23 @@ static int SetupCTX(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX** ctx, /* Load in host private key */ if (ret == WS_SUCCESS) { -#ifdef WOLFSSH_WINDOWS_CERT_STORE +#if defined(WOLFSSH_CERTS) && defined(WOLFSSH_WINDOWS_CERT_STORE) char* hostKeyStore = wolfSSHD_ConfigGetHostKeyStore(conf); char* hostKeyStoreSubject = wolfSSHD_ConfigGetHostKeyStoreSubject(conf); char* hostKeyStoreFlags = wolfSSHD_ConfigGetHostKeyStoreFlags(conf); - wolfSSH_Log(WS_LOG_INFO, - "[SSHD] Cert store code compiled in. " - "hostKeyStore=%s, hostKeyStoreSubject=%s, hostKeyStoreFlags=%s", - hostKeyStore ? hostKeyStore : "(null)", - hostKeyStoreSubject ? hostKeyStoreSubject : "(null)", - hostKeyStoreFlags ? hostKeyStoreFlags : "(null)"); - if (hostKeyStore != NULL && hostKeyStoreSubject == NULL) { wolfSSH_Log(WS_LOG_ERROR, "[SSHD] HostKeyStore set but HostKeyStoreSubject is missing"); ret = WS_BAD_ARGUMENT; } + else if (hostKeyStore == NULL && + (hostKeyStoreSubject != NULL || hostKeyStoreFlags != NULL)) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] HostKeyStoreSubject/HostKeyStoreFlags set but " + "HostKeyStore is missing"); + ret = WS_BAD_ARGUMENT; + } if (ret == WS_SUCCESS && hostKeyStore != NULL && hostKeyStoreSubject != NULL) { @@ -549,29 +641,14 @@ static int SetupCTX(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX** ctx, word32 dwFlags = CERT_SYSTEM_STORE_CURRENT_USER; int storeNameLen, subjectNameLen; - /* Parse flags if provided */ - if (hostKeyStoreFlags != NULL) { - if (WSTRCMP(hostKeyStoreFlags, "CURRENT_USER") == 0) { - dwFlags = CERT_SYSTEM_STORE_CURRENT_USER; - } else if (WSTRCMP(hostKeyStoreFlags, "LOCAL_MACHINE") == 0) { - dwFlags = CERT_SYSTEM_STORE_LOCAL_MACHINE; - } else { - /* Fall back to a raw numeric value, but only accept - * system-store location bits. Anything else is either not - * a location or a control flag (e.g. - * CERT_STORE_DELETE_FLAG) that would make CertOpenStore - * destructive. */ - dwFlags = (word32)atoi(hostKeyStoreFlags); - if ((dwFlags & - (word32)CERT_SYSTEM_STORE_LOCATION_MASK) == 0 || - (dwFlags & - ~(word32)CERT_SYSTEM_STORE_LOCATION_MASK) != 0) { - wolfSSH_Log(WS_LOG_ERROR, - "[SSHD] Unrecognized host key store flags '%s'", - hostKeyStoreFlags); - ret = WS_BAD_ARGUMENT; - } - } + /* An unset location keeps the CURRENT_USER default set above. */ + if (hostKeyStoreFlags != NULL && + ParseCertStoreLocation(hostKeyStoreFlags, &dwFlags) + != WS_SUCCESS) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Unrecognized host key store flags '%s'", + hostKeyStoreFlags); + ret = WS_BAD_ARGUMENT; } /* Convert to wide strings */ @@ -622,20 +699,10 @@ static int SetupCTX(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX** ctx, } } else if (ret == WS_SUCCESS) -#elif defined(WOLFSSH_CERTS) - wolfSSH_Log(WS_LOG_INFO, - "[SSHD] WOLFSSH_WINDOWS_CERT_STORE not defined - cert store support disabled"); -#else - wolfSSH_Log(WS_LOG_INFO, - "[SSHD] WOLFSSH_CERTS not defined - cert store support disabled"); -#endif /* WOLFSSH_WINDOWS_CERT_STORE */ +#endif /* WOLFSSH_CERTS && WOLFSSH_WINDOWS_CERT_STORE */ { char* hostKey = wolfSSHD_ConfigGetHostKeyFile(conf); - wolfSSH_Log(WS_LOG_INFO, - "[SSHD] File-based host key path entered. hostKey=%s", - hostKey ? hostKey : "(null)"); - if (hostKey == NULL) { wolfSSH_Log(WS_LOG_ERROR, "[SSHD] No host private key set"); ret = WS_BAD_ARGUMENT; @@ -782,12 +849,29 @@ static int SetupCTX(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX** ctx, #ifdef WOLFSSH_CERTS /* Load system CA certs from the OS trust store via wolfSSL into a - * temporary WOLFSSL_CTX, then import its cert manager. */ + * temporary WOLFSSL_CTX, then import its cert manager. That cert manager + * verifies *client* certificates during user authentication, so every CA + * in the OS trust store becomes a login authority for this daemon. On a + * public trust store that is every commercial root CA, and the only + * remaining binding to an account is the certificate subject. Intended for + * a store that holds nothing but the organization's own CA. */ #ifdef WOLFSSL_SYS_CA_CERTS if (ret == WS_SUCCESS && wolfSSHD_ConfigGetSystemCA(conf)) { WOLFSSL_CTX* sslCtx; - wolfSSH_Log(WS_LOG_INFO, "[SSHD] Using system CAs"); + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] WARNING: wolfSSH_TrustedSystemCAKeys makes every CA in " + "the OS trust store an SSH user authentication authority. Any " + "certificate issued by any of them whose subject matches a local " + "account name can log in as that account. Use this only when the " + "OS trust store holds solely your organization's CA."); + #ifdef WOLFSSH_NO_FPKI + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] WARNING: built without FPKI profile checking, so peer " + "certificates are not required to carry a client authentication " + "EKU. A TLS server, S/MIME or code signing certificate with a " + "matching subject is accepted for login."); + #endif sslCtx = wolfSSL_CTX_new(wolfSSLv23_server_method()); if (sslCtx == NULL) { wolfSSH_Log(WS_LOG_INFO, "[SSHD] Unable to create temporary CTX"); @@ -828,7 +912,7 @@ static int SetupCTX(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX** ctx, /* Load user CA certs (trust anchors used to verify client X.509 certs) * directly from a Windows certificate store into the cert manager. */ - #ifdef WOLFSSH_WINDOWS_CERT_STORE + #if defined(WOLFSSH_CERTS) && defined(WOLFSSH_WINDOWS_CERT_STORE) if (ret == WS_SUCCESS && wolfSSHD_ConfigGetUserCAStore(conf)) { ret = LoadUserCACertsFromStore(conf, *ctx, heap); } @@ -841,6 +925,19 @@ static int SetupCTX(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX** ctx, } #endif /* WOLFSSH_WINDOWS_CERT_STORE */ + /* State the cert-to-user binding once at startup. Which one is in force is + * fixed by the wolfSSL build, not by configuration, so this cannot be + * derived from the config file. */ + #if defined(WOLFSSH_CERTS) && !defined(WOLFSSL_FPKI) && defined(_WIN32) + if (ret == WS_SUCCESS) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] WARNING: client certificates are bound to an account by " + "subject CN only. Any CA in the trusted user CA set may assert " + "any CN, so keep that set narrow. Build wolfSSL with FPKI for " + "UPN binding and AuthorizedUPNDomains."); + } + #endif + /* load in CA certs from file set */ if (ret == WS_SUCCESS) { char* caCert = wolfSSHD_ConfigGetUserCAKeysFile(conf); @@ -3171,8 +3268,20 @@ static int StartSSHD(int argc, char** argv) } /* check if host key file was passed in */ - if (hostKeyFile != NULL) { - wolfSSHD_ConfigSetHostKeyFile(conf, hostKeyFile); + if (ret == WS_SUCCESS && hostKeyFile != NULL) { + #if defined(WOLFSSH_CERTS) && defined(WOLFSSH_WINDOWS_CERT_STORE) + /* The store branch in SetupCTX() wins over the file path, so an + * accepted -h here would be silently discarded. */ + if (wolfSSHD_ConfigGetHostKeyStore(conf) != NULL) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] -h host key file conflicts with the configured " + "HostKeyStore. Use one or the other."); + ret = WS_BAD_ARGUMENT; + } + #endif + if (ret == WS_SUCCESS) { + wolfSSHD_ConfigSetHostKeyFile(conf, hostKeyFile); + } } if (ret == WS_SUCCESS) { diff --git a/configure.ac b/configure.ac index bf183cf71..93ae41e06 100644 --- a/configure.ac +++ b/configure.ac @@ -298,9 +298,11 @@ AS_IF([test "x$ENABLED_WINDOWS_CERT_STORE" = "xyes"], [AS_IF([test "x$ENABLED_CERTS" != "xyes"], [AC_MSG_ERROR([--enable-windows-cert-store requires X.509 cert support (--enable-certs)])]) AM_CPPFLAGS="$AM_CPPFLAGS -DWOLFSSH_WINDOWS_CERT_STORE" + dnl Only the mingw triplets define _WIN32, which wolfssh/internal.h + dnl requires. MSYS/Cygwin toolchains do not. AS_CASE([$host], - [*mingw*|*msys*],[LIBS="$LIBS -lcrypt32 -lncrypt"], - [AC_MSG_ERROR([--enable-windows-cert-store is only supported on _WIN32 Windows hosts (mingw/msys)])])]) + [*mingw*],[LIBS="$LIBS -lcrypt32 -lncrypt"], + [AC_MSG_ERROR([--enable-windows-cert-store is only supported on _WIN32 Windows hosts (mingw)])])]) AS_IF([test "x$ENABLED_SMALLSTACK" = "xyes"], [AM_CPPFLAGS="$AM_CPPFLAGS -DWOLFSSH_SMALL_STACK"]) AS_IF([test "x$ENABLED_NONE_CIPHER" = "xyes"], diff --git a/examples/client/common.c b/examples/client/common.c index aa114fc00..bf8b34b15 100644 --- a/examples/client/common.c +++ b/examples/client/common.c @@ -1219,7 +1219,7 @@ void ClientFreeBuffers(const char* pubKeyName, const char* privKeyName, int ClientSetPrivateKeyFromStore(WOLFSSH_CTX* ctx, const wchar_t* storeName, word32 dwFlags, const wchar_t* subjectName) { - int ret = WS_SUCCESS; + int ret; if (ctx == NULL || storeName == NULL || subjectName == NULL) { return WS_BAD_ARGUMENT; @@ -1241,6 +1241,7 @@ int ClientSetPrivateKeyFromStore(WOLFSSH_CTX* ctx, * is the x509v3 name that matches the key algorithm. */ int ClientSetupCertStoreAuth(WOLFSSH_CTX* ctx) { + const byte* keyType; word32 i; if (ctx == NULL) @@ -1251,47 +1252,58 @@ int ClientSetupCertStoreAuth(WOLFSSH_CTX* ctx) if (!pvtKey->useCertStore) continue; - /* Point userPublicKey at the DER certificate stored in the CTX. - * This is safe because the CTX outlives the auth callback. The - * ctx-owned flag stops ClientFreeBuffers from freeing CTX memory. */ - userPublicKey = pvtKey->cert; - userPublicKeySz = pvtKey->certSz; - userPublicKeyCtxOwned = 1; - - /* Map the internal key format to the x509v3 SSH type name. */ + /* Map the internal key format to the x509v3 SSH type name. Resolve + * it before touching the globals so a failure leaves them alone. */ switch (pvtKey->publicKeyFmt) { case ID_SSH_RSA: case ID_X509V3_SSH_RSA: case ID_RSA_SHA2_256: case ID_RSA_SHA2_512: - userPublicKeyType = (const byte*)"x509v3-ssh-rsa"; + keyType = (const byte*)"x509v3-ssh-rsa"; break; case ID_ECDSA_SHA2_NISTP256: case ID_X509V3_ECDSA_SHA2_NISTP256: - userPublicKeyType = (const byte*)"x509v3-ecdsa-sha2-nistp256"; + keyType = (const byte*)"x509v3-ecdsa-sha2-nistp256"; break; case ID_ECDSA_SHA2_NISTP384: case ID_X509V3_ECDSA_SHA2_NISTP384: - userPublicKeyType = (const byte*)"x509v3-ecdsa-sha2-nistp384"; + keyType = (const byte*)"x509v3-ecdsa-sha2-nistp384"; break; case ID_ECDSA_SHA2_NISTP521: case ID_X509V3_ECDSA_SHA2_NISTP521: - userPublicKeyType = (const byte*)"x509v3-ecdsa-sha2-nistp521"; + keyType = (const byte*)"x509v3-ecdsa-sha2-nistp521"; break; default: fprintf(stderr, "Unsupported cert store key type: %d\n", pvtKey->publicKeyFmt); return WS_BAD_ARGUMENT; } - userPublicKeyTypeSz = (word32)WSTRLEN((const char*)userPublicKeyType); - /* No in-memory private key — signing goes through the cert store. - * Keep the static-buffer invariant (only replace the pointer when it - * is not a heap allocation) so a later key load or - * ClientFreeBuffers call remains valid. */ - if (!userPrivateKeyAlloc) { - userPrivateKey = userPrivateKeyBuf; + /* Drop anything an earlier file based load left behind, the cert + * store key replaces it. */ + if (userPublicKeyAlloc && userPublicKey != NULL) { + WFREE(userPublicKey, ctx->heap, DYNTYPE_PRIVKEY); + userPublicKeyAlloc = 0; } + if (userPrivateKeyAlloc && userPrivateKey != NULL) { + wc_ForceZero(userPrivateKey, userPrivateKeySz); + WFREE(userPrivateKey, ctx->heap, DYNTYPE_PRIVKEY); + userPrivateKeyAlloc = 0; + } + + /* Point userPublicKey at the DER certificate stored in the CTX. The + * ctx-owned flag stops ClientFreeBuffers from freeing CTX memory. + * The alias is only valid while the slot keeps its certificate: + * re-loading a host key onto this slot frees it, so do not mix this + * with the file-key loaders on the same CTX. */ + userPublicKey = pvtKey->cert; + userPublicKeySz = pvtKey->certSz; + userPublicKeyCtxOwned = 1; + userPublicKeyType = keyType; + userPublicKeyTypeSz = (word32)WSTRLEN((const char*)keyType); + + /* No in-memory private key, signing goes through the cert store. */ + userPrivateKey = userPrivateKeyBuf; userPrivateKeySz = 0; pubKeyLoaded = 1; diff --git a/examples/client/common.h b/examples/client/common.h index ffb97638f..79e3354a9 100644 --- a/examples/client/common.h +++ b/examples/client/common.h @@ -38,6 +38,9 @@ int ClientSetTpm(WOLFSSH* ssh); #ifdef WOLFSSH_WINDOWS_CERT_STORE int ClientSetPrivateKeyFromStore(WOLFSSH_CTX* ctx, const wchar_t* storeName, word32 dwFlags, const wchar_t* subjectName); +/* Supersedes ClientUseCert()/ClientUsePubKey()/ClientSetPrivateKey(), any key + * they loaded is released. Call ClientFreeBuffers() before wolfSSH_CTX_free(), + * the auth globals alias memory owned by ctx. */ int ClientSetupCertStoreAuth(WOLFSSH_CTX* ctx); #endif /* WOLFSSH_WINDOWS_CERT_STORE */ diff --git a/examples/echoserver/echoserver.c b/examples/echoserver/echoserver.c index 32691ee8e..0c76fef9e 100644 --- a/examples/echoserver/echoserver.c +++ b/examples/echoserver/echoserver.c @@ -41,7 +41,9 @@ #include #include #include -#include +#ifdef WOLFSSH_WINDOWS_CERT_STORE + #include +#endif #include #include #include @@ -129,6 +131,9 @@ #endif #endif +/* Shared by echoserver_test() and the -W pre-scan in wolfSSH_Echoserver(). */ +#define ES_OPTLIST "?1a:d:efEp:R:Ni:j:i:I:J:K:P:k:b:x:m:c:s:G:HW:" + #ifndef NO_WOLFSSH_SERVER static const char echoserverBanner[] = "wolfSSH Example Echo Server\n"; @@ -2890,7 +2895,12 @@ static void ShowUsage(void) "to use\n"); printf(" -m set the comma separated list of mac algos to use\n"); #ifdef WOLFSSH_WINDOWS_CERT_STORE - printf(" -W Windows cert store: \"store:subject:flags\" (e.g. My:CN=Server:CURRENT_USER)\n"); + printf(" -W Windows cert store: \"store:subject:flags\" " + "(e.g. My:CN=Server:CURRENT_USER)\n"); + printf(" also read from the WOLFSSH_CERT_STORE environment " + "variable\n"); + printf(" with either set, file names are relative to the " + "current directory\n"); #endif printf(" -b test user auth would block\n"); printf(" -H set test highwater callback\n"); @@ -3017,11 +3027,8 @@ THREAD_RETURN WOLFSSH_THREAD echoserver_test(void* args) kbAuthData.promptCount = 0; #endif - #ifdef WOLFSSH_WINDOWS_CERT_STORE - certStoreSpec = getenv("WOLFSSH_CERT_STORE"); - #endif if (argc > 0) { - const char* optlist = "?1a:d:efEp:R:Ni:j:i:I:J:K:P:k:b:x:m:c:s:G:HW:"; + const char* optlist = ES_OPTLIST; myoptind = 0; while ((ch = mygetopt(argc, argv, optlist)) != -1) { switch (ch) { @@ -3146,11 +3153,14 @@ THREAD_RETURN WOLFSSH_THREAD echoserver_test(void* args) useCustomHighWaterCb = 1; break; - #ifdef WOLFSSH_WINDOWS_CERT_STORE case 'W': - certStoreSpec = myoptarg; + #ifdef WOLFSSH_WINDOWS_CERT_STORE + certStoreSpec = myoptarg; + #else + ES_ERROR("-W requires wolfSSH built with " + "WOLFSSH_WINDOWS_CERT_STORE\n"); + #endif break; - #endif default: ShowUsage(); @@ -3160,6 +3170,17 @@ THREAD_RETURN WOLFSSH_THREAD echoserver_test(void* args) } } myoptind = 0; /* reset for test cases */ + + #ifdef WOLFSSH_WINDOWS_CERT_STORE + /* -W takes priority over the environment. */ + if (certStoreSpec == NULL) { + certStoreSpec = getenv("WOLFSSH_CERT_STORE"); + if (certStoreSpec != NULL) { + printf("Taking the host key from the WOLFSSH_CERT_STORE " + "environment variable\n"); + } + } + #endif wc_InitMutex(&doneLock); #ifdef WOLFSSH_TEST_BLOCK @@ -3360,16 +3381,24 @@ THREAD_RETURN WOLFSSH_THREAD echoserver_test(void* args) int ret; ret = wolfSSH_ParseCertStoreSpec(certStoreSpec, &wStoreName, - &wSubjectName, &dwFlags, NULL); + &wSubjectName, &dwFlags, heap); if (ret != WS_SUCCESS) { + #ifdef WOLFSSH_SMALL_STACK + wc_ForceZero(keyLoadBuf, EXAMPLE_KEYLOAD_BUFFER_SZ); + WFREE(keyLoadBuf, NULL, 0); + #endif ES_ERROR("Invalid cert store spec. Use: store:subject:flags\n"); } ret = wolfSSH_CTX_UsePrivateKey_fromStore(ctx, wStoreName, dwFlags, wSubjectName); - WFREE(wStoreName, NULL, DYNTYPE_TEMP); - WFREE(wSubjectName, NULL, DYNTYPE_TEMP); + WFREE(wStoreName, heap, DYNTYPE_TEMP); + WFREE(wSubjectName, heap, DYNTYPE_TEMP); if (ret != WS_SUCCESS) { + #ifdef WOLFSSH_SMALL_STACK + wc_ForceZero(keyLoadBuf, EXAMPLE_KEYLOAD_BUFFER_SZ); + WFREE(keyLoadBuf, NULL, 0); + #endif ES_ERROR("Couldn't load host key from certificate store.\n"); } loadDefaultHostKeys = 0; @@ -3790,13 +3819,18 @@ int wolfSSH_Echoserver(int argc, char** argv) useStore = 1; } else { - int i; - for (i = 1; i < argc; i++) { - if (WSTRNCMP(argv[i], "-W", 2) == 0) { + int ch; + + /* Parse rather than match on argv, an option value could + * start with "-W" too. */ + myoptind = 0; + while ((ch = mygetopt(argc, argv, ES_OPTLIST)) != -1) { + if (ch == 'W') { useStore = 1; break; } } + myoptind = 0; } #endif if (!useStore) { diff --git a/examples/sftpclient/sftpclient.c b/examples/sftpclient/sftpclient.c index 1a3a4bc0f..9c8d7d53c 100644 --- a/examples/sftpclient/sftpclient.c +++ b/examples/sftpclient/sftpclient.c @@ -33,7 +33,9 @@ #include #include #include -#include +#ifdef WOLFSSH_WINDOWS_CERT_STORE + #include +#endif #include #include #include @@ -415,6 +417,8 @@ static void ShowUsage(void) #ifdef WOLFSSH_WINDOWS_CERT_STORE printf(" -W Windows cert store: \"store:subject:flags\"\n"); printf(" Example: -W \"My:CN=MyCert:CURRENT_USER\"\n"); + printf(" supplies both keys, can not be used with " + "-i, -j or -J\n"); #endif /* WOLFSSH_WINDOWS_CERT_STORE */ #ifdef WOLFSSH_CERTS printf(" -J filename for DER certificate to use\n"); @@ -1700,6 +1704,14 @@ THREAD_RETURN WOLFSSH_THREAD sftpclient_test(void* args) if (username == NULL) err_sys("client requires a username parameter."); +#ifdef WOLFSSH_WINDOWS_CERT_STORE + if (certStoreSpec != NULL && (privKeyName != NULL || pubKeyName != NULL || + certName != NULL)) { + err_sys("-W provides both keys, it can not be used with -i, -j " + "or -J."); + } +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ + if ((pubKeyName == NULL && certName == NULL) && privKeyName != NULL) { err_sys("If setting priv key, need pub key."); } @@ -1740,7 +1752,7 @@ THREAD_RETURN WOLFSSH_THREAD sftpclient_test(void* args) word32 dwFlags = 0; ret = wolfSSH_ParseCertStoreSpec(certStoreSpec, &wStoreName, - &wSubjectName, &dwFlags, NULL); + &wSubjectName, &dwFlags, heap); if (ret != WS_SUCCESS) { err_sys("Invalid cert store spec. Use: store:subject:flags"); } @@ -1748,8 +1760,8 @@ THREAD_RETURN WOLFSSH_THREAD sftpclient_test(void* args) /* Create context first */ ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, heap); if (ctx == NULL) { - WFREE(wStoreName, NULL, DYNTYPE_TEMP); - WFREE(wSubjectName, NULL, DYNTYPE_TEMP); + WFREE(wStoreName, heap, DYNTYPE_TEMP); + WFREE(wSubjectName, heap, DYNTYPE_TEMP); err_sys("Couldn't create wolfSSH client context."); } @@ -1757,8 +1769,8 @@ THREAD_RETURN WOLFSSH_THREAD sftpclient_test(void* args) ret = ClientSetPrivateKeyFromStore(ctx, wStoreName, dwFlags, wSubjectName); if (ret != WS_SUCCESS) { - WFREE(wStoreName, NULL, DYNTYPE_TEMP); - WFREE(wSubjectName, NULL, DYNTYPE_TEMP); + WFREE(wStoreName, heap, DYNTYPE_TEMP); + WFREE(wSubjectName, heap, DYNTYPE_TEMP); err_sys("Error setting private key from certificate store"); } @@ -1767,13 +1779,13 @@ THREAD_RETURN WOLFSSH_THREAD sftpclient_test(void* args) * authentication. */ ret = ClientSetupCertStoreAuth(ctx); if (ret != WS_SUCCESS) { - WFREE(wStoreName, NULL, DYNTYPE_TEMP); - WFREE(wSubjectName, NULL, DYNTYPE_TEMP); + WFREE(wStoreName, heap, DYNTYPE_TEMP); + WFREE(wSubjectName, heap, DYNTYPE_TEMP); err_sys("Error setting up cert store auth"); } - WFREE(wStoreName, NULL, DYNTYPE_TEMP); - WFREE(wSubjectName, NULL, DYNTYPE_TEMP); + WFREE(wStoreName, heap, DYNTYPE_TEMP); + WFREE(wSubjectName, heap, DYNTYPE_TEMP); } else #endif /* WOLFSSH_WINDOWS_CERT_STORE */ { @@ -1949,13 +1961,16 @@ THREAD_RETURN WOLFSSH_THREAD sftpclient_test(void* args) WCLOSESOCKET(sockFd); wolfSSH_free(ssh); + + /* Clear the auth globals before the CTX, with a cert store key they + * alias memory owned by the CTX. */ + ClientFreeBuffers(pubKeyName, privKeyName, heap); wolfSSH_CTX_free(ctx); if (ret != WS_SUCCESS) { printf("error %d encountered\n", ret); ((func_args*)args)->return_code = ret; } - ClientFreeBuffers(pubKeyName, privKeyName, heap); #if !defined(WOLFSSH_NO_ECC) && defined(FP_ECC) && defined(HAVE_THREAD_LS) wc_ecc_fp_free(); /* free per thread cache */ #endif diff --git a/ide/winvs/api-test/api-test.vcxproj b/ide/winvs/api-test/api-test.vcxproj index bbeb3dc6b..8b40665e9 100644 --- a/ide/winvs/api-test/api-test.vcxproj +++ b/ide/winvs/api-test/api-test.vcxproj @@ -1,4 +1,4 @@ - + diff --git a/ide/winvs/testsuite/testsuite.vcxproj b/ide/winvs/testsuite/testsuite.vcxproj index a97835917..7d13d5c7c 100644 --- a/ide/winvs/testsuite/testsuite.vcxproj +++ b/ide/winvs/testsuite/testsuite.vcxproj @@ -348,7 +348,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDebug32) @@ -366,7 +366,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDebug32FIPS) @@ -384,7 +384,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDllDebug32) @@ -402,7 +402,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDllDebug32FIPS) @@ -420,7 +420,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDebug64) @@ -438,7 +438,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDebug64FIPS) @@ -456,7 +456,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDllDebug64) @@ -474,7 +474,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDllDebug64FIPS) @@ -493,7 +493,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptRelease32) @@ -513,7 +513,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptRelease32FIPS) @@ -533,7 +533,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptDllRelease32) @@ -553,7 +553,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptDllRelease32FIPS) @@ -573,7 +573,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptRelease64) @@ -593,7 +593,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptRelease64FIPS) @@ -613,7 +613,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptDllRelease64) @@ -633,7 +633,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptDllRelease64FIPS) diff --git a/ide/winvs/unit-test/unit-test.vcxproj b/ide/winvs/unit-test/unit-test.vcxproj index f88807fc8..f716a0433 100644 --- a/ide/winvs/unit-test/unit-test.vcxproj +++ b/ide/winvs/unit-test/unit-test.vcxproj @@ -1,4 +1,4 @@ - + diff --git a/src/certman.c b/src/certman.c index 25e9f1d95..5f4fbd6a8 100644 --- a/src/certman.c +++ b/src/certman.c @@ -36,6 +36,7 @@ #endif +#include #include #include #include @@ -45,14 +46,22 @@ #include #ifdef WOLFSSH_WINDOWS_CERT_STORE + #include + #include #include #include + #ifndef CERT_SYSTEM_STORE_LOCATION_MASK + #define CERT_SYSTEM_STORE_LOCATION_MASK 0x00FF0000 + #endif #ifndef CERT_SYSTEM_STORE_CURRENT_USER #define CERT_SYSTEM_STORE_CURRENT_USER 0x00010000 #endif #ifndef CERT_SYSTEM_STORE_LOCAL_MACHINE #define CERT_SYSTEM_STORE_LOCAL_MACHINE 0x00020000 #endif + #ifndef CERT_SYSTEM_STORE_USERS + #define CERT_SYSTEM_STORE_USERS 0x00060000 + #endif #endif #ifdef WOLFSSH_CERTS @@ -94,11 +103,22 @@ struct WOLFSSH_CERTMAN { }; +/* wolfSSL_CertManager_up_ref() was added in wolfSSL 4.6.0 */ +#define WOLFSSL_V4_6_0 0x04006000 + + /* used to import an external cert manager, frees and replaces existing manager * returns WS_SUCCESS on success */ int wolfSSH_SetCertManager(WOLFSSH_CTX* ctx, WOLFSSL_CERT_MANAGER* cm) { +#if LIBWOLFSSL_VERSION_HEX < WOLFSSL_V4_6_0 + WOLFSSH_UNUSED(ctx); + WOLFSSH_UNUSED(cm); + + WLOG(WS_LOG_CERTMAN, "Importing a cert manager needs wolfSSL 4.6.0"); + return WS_NOT_COMPILED; +#else if (ctx == NULL || cm == NULL || ctx->certMan == NULL) { return WS_BAD_ARGUMENT; } @@ -113,6 +133,17 @@ int wolfSSH_SetCertManager(WOLFSSH_CTX* ctx, WOLFSSL_CERT_MANAGER* cm) return WS_FATAL_ERROR; } +#ifdef HAVE_OCSP + /* an imported manager gets the same policy _CertMan_init() applies, and + * is rejected if it can't, rather than silently skipping revocation */ + if (wolfSSL_CertManagerEnableOCSP(cm, WOLFSSL_OCSP_CHECKALL) + != WOLFSSL_SUCCESS) { + WLOG(WS_LOG_CERTMAN, "Couldn't enable OCSP on imported cert manager"); + wolfSSL_CertManagerFree(cm); + return WS_FATAL_ERROR; + } +#endif + /* free up existing cm if present */ if (ctx->certMan->cm != NULL) { wolfSSL_CertManagerFree(ctx->certMan->cm); @@ -120,6 +151,7 @@ int wolfSSH_SetCertManager(WOLFSSH_CTX* ctx, WOLFSSL_CERT_MANAGER* cm) ctx->certMan->cm = cm; return WS_SUCCESS; +#endif } @@ -684,9 +716,10 @@ static int CheckProfile(DecodedCert* cert, int profile) #ifdef WOLFSSH_WINDOWS_CERT_STORE -/* Parse a cert store spec string "store:subject:flags" into wide-string - * components. Allocates wStoreName and wSubjectName via WMALLOC; caller - * must WFREE them. dwFlags is set to the parsed flags value. +/* Parse a cert store spec string "store:subject[:flags]" into wide-string + * components. The subject may not contain ':'. Allocates wStoreName and + * wSubjectName via WMALLOC; caller must WFREE them. On success dwFlags is + * set to the parsed flags value, on failure it is left alone. * Returns WS_SUCCESS on success. */ int wolfSSH_ParseCertStoreSpec(const char* spec, wchar_t** wStoreName, wchar_t** wSubjectName, @@ -696,6 +729,10 @@ int wolfSSH_ParseCertStoreSpec(const char* spec, char* storeName = NULL; char* subjectName = NULL; char* flagsStr = NULL; + char* flagsEnd = NULL; + unsigned long flagsVal; + unsigned long locationMask; + word32 flags; int wStoreNameLen, wSubjectNameLen; size_t specLen; @@ -706,7 +743,8 @@ int wolfSSH_ParseCertStoreSpec(const char* spec, *wStoreName = NULL; *wSubjectName = NULL; - *dwFlags = CERT_SYSTEM_STORE_CURRENT_USER; + flags = CERT_SYSTEM_STORE_CURRENT_USER; + locationMask = (unsigned long)CERT_SYSTEM_STORE_LOCATION_MASK; specLen = WSTRLEN(spec) + 1; specCopy = (char*)WMALLOC(specLen, heap, DYNTYPE_TEMP); @@ -726,24 +764,50 @@ int wolfSSH_ParseCertStoreSpec(const char* spec, WFREE(specCopy, heap, DYNTYPE_TEMP); return WS_BAD_ARGUMENT; } - if (WSTRCMP(flagsStr, "CURRENT_USER") == 0) { - *dwFlags = CERT_SYSTEM_STORE_CURRENT_USER; + if (WSTRCHR(flagsStr, ':') != NULL) { + WLOG(WS_LOG_CERTMAN, + "Cert store subject may not contain a ':'"); + WFREE(specCopy, heap, DYNTYPE_TEMP); + return WS_BAD_ARGUMENT; + } + /* Accept the same spellings as wolfsshd's HostKeyStoreFlags and + * wolfSSH_WinUserDwFlags so one name works everywhere. */ + if (WSTRCMP(flagsStr, "CURRENT_USER") == 0 + || WSTRCMP(flagsStr, + "CERT_SYSTEM_STORE_CURRENT_USER") == 0) { + flags = CERT_SYSTEM_STORE_CURRENT_USER; + } + else if (WSTRCMP(flagsStr, "LOCAL_MACHINE") == 0 + || WSTRCMP(flagsStr, + "CERT_SYSTEM_STORE_LOCAL_MACHINE") == 0) { + flags = CERT_SYSTEM_STORE_LOCAL_MACHINE; } - else if (WSTRCMP(flagsStr, "LOCAL_MACHINE") == 0) { - *dwFlags = CERT_SYSTEM_STORE_LOCAL_MACHINE; + else if (WSTRCMP(flagsStr, "USERS") == 0 + || WSTRCMP(flagsStr, "CERT_SYSTEM_STORE_USERS") == 0) { + flags = CERT_SYSTEM_STORE_USERS; } else { - /* Fall back to a raw numeric value, but only accept - * system-store location bits. Anything else is either not a - * location or a control flag (e.g. CERT_STORE_DELETE_FLAG) - * that would make CertOpenStore destructive. */ - *dwFlags = (word32)atoi(flagsStr); - if ((*dwFlags & (word32)CERT_SYSTEM_STORE_LOCATION_MASK) == 0 - || (*dwFlags & - ~(word32)CERT_SYSTEM_STORE_LOCATION_MASK) != 0) { + /* Fall back to a raw numeric value, decimal or 0x hex, that + * has to be consumed whole. Only system-store location bits + * are accepted. Anything else is either not a location or a + * control flag (e.g. CERT_STORE_DELETE_FLAG) that would make + * CertOpenStore destructive. */ + errno = 0; + flagsVal = strtoul(flagsStr, &flagsEnd, 0); + if (flagsEnd == flagsStr || *flagsEnd != '\0' + || errno == ERANGE) { + WLOG(WS_LOG_CERTMAN, "Malformed cert store flags value"); WFREE(specCopy, heap, DYNTYPE_TEMP); return WS_BAD_ARGUMENT; } + if ((flagsVal & locationMask) == 0 + || (flagsVal & ~locationMask) != 0) { + WLOG(WS_LOG_CERTMAN, + "Cert store flags are not a store location"); + WFREE(specCopy, heap, DYNTYPE_TEMP); + return WS_BAD_ARGUMENT; + } + flags = (word32)flagsVal; } } } @@ -787,6 +851,8 @@ int wolfSSH_ParseCertStoreSpec(const char* spec, MultiByteToWideChar(CP_UTF8, 0, subjectName, -1, *wSubjectName, wSubjectNameLen); + *dwFlags = flags; + WFREE(specCopy, heap, DYNTYPE_TEMP); return WS_SUCCESS; } diff --git a/src/internal.c b/src/internal.c index 446cb39c2..f2527e113 100644 --- a/src/internal.c +++ b/src/internal.c @@ -76,7 +76,7 @@ #define CERT_SYSTEM_STORE_LOCAL_MACHINE 0x00020000 #endif #ifndef CERT_NCRYPT_KEY_SPEC - #define CERT_NCRYPT_KEY_SPEC 0x00000003 + #define CERT_NCRYPT_KEY_SPEC 0xFFFFFFFF #endif #ifndef BCRYPT_PAD_PKCS1 #define BCRYPT_PAD_PKCS1 0x00000002 @@ -1272,9 +1272,16 @@ WOLFSSH_CTX* CtxInit(WOLFSSH_CTX* ctx, byte side, void* heap) #ifdef WOLFSSH_WINDOWS_CERT_STORE /* Release any MS Certificate Store state held by a private key slot and reset * the cert-store fields so the slot is no longer treated as cert-store backed. - * Safe to call on a slot that never held cert-store state. */ + * The certificate DER copied out of the store is released as well, so a slot + * can never pair a store certificate with a key from another source. Only a + * slot that is cert-store backed is touched, so a file certificate installed + * on the slot is left alone. */ static void ClearCertStoreKey(WOLFSSH_CTX* ctx, WOLFSSH_PVT_KEY* pvtKey) { + if (!pvtKey->useCertStore) { + return; + } + if (pvtKey->certStoreContext != NULL) { CertFreeCertificateContext((PCCERT_CONTEXT)pvtKey->certStoreContext); pvtKey->certStoreContext = NULL; @@ -1287,6 +1294,13 @@ static void ClearCertStoreKey(WOLFSSH_CTX* ctx, WOLFSSH_PVT_KEY* pvtKey) WFREE(pvtKey->subjectName, ctx->heap, DYNTYPE_STRING); pvtKey->subjectName = NULL; } +#ifdef WOLFSSH_CERTS + if (pvtKey->cert != NULL) { + WFREE(pvtKey->cert, ctx->heap, DYNTYPE_CERT); + pvtKey->cert = NULL; + pvtKey->certSz = 0; + } +#endif pvtKey->useCertStore = 0; } @@ -2194,16 +2208,48 @@ static int IdentifyCert(const byte* in, word32 inSz, void* heap) return ret; } + + +/* Returns 1 when id names an x509v3 host key algorithm. */ +static int IsCertKeyId(byte id) +{ + int ret; + + switch (id) { + case ID_X509V3_SSH_RSA: + case ID_X509V3_ECDSA_SHA2_NISTP256: + case ID_X509V3_ECDSA_SHA2_NISTP384: + case ID_X509V3_ECDSA_SHA2_NISTP521: + #ifndef WOLFSSH_NO_MLDSA + case ID_X509V3_MLDSA44: + case ID_X509V3_MLDSA65: + case ID_X509V3_MLDSA87: + #endif + ret = 1; + break; + default: + ret = 0; + } + + return ret; +} #endif /* WOLFSSH_CERTS */ -void RefreshPublicKeyAlgo(WOLFSSH_CTX* ctx) +WOLFSSH_LOCAL void RefreshPublicKeyAlgo(WOLFSSH_CTX* ctx) { WOLFSSH_PVT_KEY* key; byte* publicKeyAlgo = ctx->publicKeyAlgo; word32 keyCount = ctx->privateKeyCount, publicKeyAlgoCount = 0, idx; for (idx = 0, key = ctx->privateKey; idx < keyCount; idx++, key++) { + #ifdef WOLFSSH_CERTS + /* An x509v3 slot whose certificate was dropped cannot produce a K_S, + * so do not advertise it. */ + if (IsCertKeyId(key->publicKeyFmt) && key->cert == NULL) { + continue; + } + #endif if (key->publicKeyFmt == ID_SSH_RSA) { #ifndef WOLFSSH_NO_RSA_SHA2_512 if (publicKeyAlgoCount < WOLFSSH_MAX_PUB_KEY_ALGO) { @@ -2329,14 +2375,20 @@ static int UpdateHostCertificates(WOLFSSH_CTX* ctx, if (HINTISSET(keyHint) && HINTISSET(certHint)) { byte* key = NULL; word32 keySz; + int copyKey; #ifdef WOLFSSH_TPM int keyIsTpm = ctx->privateKey[keyHint].isTpm; #endif + /* A cert-store or TPM slot has no software key bytes to copy. */ + copyKey = ctx->privateKey[keyHint].key != NULL + && ctx->privateKey[keyHint].keySz > 0; + #ifdef WOLFSSH_TPM /* A TPM-backed key has no software bytes to copy; clear any stale * software key on the certificate slot and mark it TPM-backed. */ if (keyIsTpm) { + copyKey = 0; if (ctx->privateKey[certHint].key != NULL) { WS_FORCEZERO(ctx->privateKey[certHint].key, ctx->privateKey[certHint].keySz); @@ -2348,11 +2400,7 @@ static int UpdateHostCertificates(WOLFSSH_CTX* ctx, ctx->privateKey[certHint].isTpm = 1; } #endif - if (ret == WS_SUCCESS -#ifdef WOLFSSH_TPM - && !keyIsTpm -#endif - ) { + if (ret == WS_SUCCESS && copyKey) { keySz = ctx->privateKey[keyHint].keySz; key = (byte*)WMALLOC(keySz, ctx->heap, DYNTYPE_PRIVKEY); if (key == NULL) { @@ -2367,6 +2415,12 @@ static int UpdateHostCertificates(WOLFSSH_CTX* ctx, WFREE(ctx->privateKey[certHint].key, ctx->heap, DYNTYPE_PRIVKEY); } + #ifdef WOLFSSH_WINDOWS_CERT_STORE + /* The slot's key material and its cert-store state change + * together, so the store certificate is never sent as K_S + * with a signature made by this software key. */ + ClearCertStoreKey(ctx, &ctx->privateKey[certHint]); + #endif ctx->privateKey[certHint].key = key; ctx->privateKey[certHint].keySz = keySz; #ifdef WOLFSSH_TPM @@ -2406,12 +2460,25 @@ static int SetHostCertificate(WOLFSSH_CTX* ctx, } } + /* Replace the existing certificate slot when there is one, rather than + * appending a second slot with the same publicKeyFmt. */ + if (HINTISSET(certIdx)) { + destIdx = certIdx; + } + if (destIdx >= WOLFSSH_MAX_PVT_KEYS) { ret = WS_CTX_KEY_COUNT_E; } else { WOLFSSH_PVT_KEY* pvtKey = ctx->privateKey + destIdx; + #ifdef WOLFSSH_WINDOWS_CERT_STORE + /* A file-based certificate is replacing this slot's contents; drop + * any cert-store state, including the store's certificate DER, so + * the slot is not mistaken for a cert-store key. */ + ClearCertStoreKey(ctx, pvtKey); + #endif + if (pvtKey->publicKeyFmt == certId) { if (pvtKey->cert != NULL) { WFREE(pvtKey->cert, ctx->heap, dynamicType); @@ -2423,12 +2490,6 @@ static int SetHostCertificate(WOLFSSH_CTX* ctx, pvtKey->publicKeyFmt = certId; } - #ifdef WOLFSSH_WINDOWS_CERT_STORE - /* A file-based certificate is replacing this slot's contents; drop - * any cert-store state so it is not mistaken for a cert-store key. */ - ClearCertStoreKey(ctx, pvtKey); - #endif - pvtKey->cert = der; pvtKey->certSz = derSz; @@ -14544,24 +14605,37 @@ static int KeyAgreeEcdhMlKem_server(WOLFSSH* ssh, byte hashId, #ifdef WOLFSSH_WINDOWS_CERT_STORE /* Extract DER-encoded public key from a DER certificate. * Caller must WFREE(*outDer, heap, DYNTYPE_PUBKEY) on success. - * Returns 0 on success. */ + * Returns WS_SUCCESS on success, otherwise a WS_ error code. */ static int ExtractPubKeyDerFromCert(const byte* certDer, word32 certDerSz, byte** outDer, word32* outDerSz, void* heap) { - struct DecodedCert dCert; + struct DecodedCert* dCert = NULL; +#ifndef WOLFSSH_SMALL_STACK + struct DecodedCert dCert_s; +#endif byte* pubKeyDer = NULL; word32 pubKeyDerSz = 0; - int ret; + int ret = 0; if (certDer == NULL || certDerSz == 0 || outDer == NULL || outDerSz == NULL) { return WS_BAD_ARGUMENT; } - wc_InitDecodedCert(&dCert, certDer, certDerSz, heap); - ret = wc_ParseCert(&dCert, CERT_TYPE, 0, NULL); +#ifndef WOLFSSH_SMALL_STACK + dCert = &dCert_s; +#else + dCert = (struct DecodedCert*)WMALLOC(sizeof(struct DecodedCert), + heap, DYNTYPE_CERT); + if (dCert == NULL) { + return WS_MEMORY_E; + } +#endif + + wc_InitDecodedCert(dCert, certDer, certDerSz, heap); + ret = wc_ParseCert(dCert, CERT_TYPE, 0, NULL); if (ret == 0) { - ret = wc_GetPubKeyDerFromCert(&dCert, NULL, &pubKeyDerSz); + ret = wc_GetPubKeyDerFromCert(dCert, NULL, &pubKeyDerSz); if (ret == LENGTH_ONLY_E) { ret = 0; pubKeyDer = (byte*)WMALLOC(pubKeyDerSz, heap, DYNTYPE_PUBKEY); @@ -14570,16 +14644,23 @@ static int ExtractPubKeyDerFromCert(const byte* certDer, word32 certDerSz, } } if (ret == 0) - ret = wc_GetPubKeyDerFromCert(&dCert, pubKeyDer, &pubKeyDerSz); - wc_FreeDecodedCert(&dCert); + ret = wc_GetPubKeyDerFromCert(dCert, pubKeyDer, &pubKeyDerSz); + wc_FreeDecodedCert(dCert); +#ifdef WOLFSSH_SMALL_STACK + WFREE(dCert, heap, DYNTYPE_CERT); +#endif if (ret == 0) { *outDer = pubKeyDer; *outDerSz = pubKeyDerSz; + ret = WS_SUCCESS; } else { if (pubKeyDer != NULL) WFREE(pubKeyDer, heap, DYNTYPE_PUBKEY); + /* Keep wolfCrypt codes out of the wolfSSH error space. */ + if (ret != WS_MEMORY_E) + ret = WS_CRYPTO_FAILED; } return ret; @@ -14628,6 +14709,10 @@ static const WOLFSSH_PVT_KEY* FindCertStoreKey(const WOLFSSH_CTX* ctx, byte baseId; word32 i; + if (ctx == NULL) { + return NULL; + } + baseId = CertStoreBaseKeyId(keyId); for (i = 0; i < ctx->privateKeyCount; i++) { pvtKey = &ctx->privateKey[i]; @@ -14639,6 +14724,29 @@ static const WOLFSSH_PVT_KEY* FindCertStoreKey(const WOLFSSH_CTX* ctx, return NULL; } + + +/* Resolve the cert-store slot to sign a client user-auth request with. The + * slot must hold the exact certificate being offered, so a credential the + * application supplied itself is never silently signed with a store key. + * Returns NULL when the request is not a cert-store request, in which case + * the caller falls back to the in-memory key. */ +static const WOLFSSH_PVT_KEY* FindCertStoreAuthKey(const WOLFSSH_CTX* ctx, + byte keyId, const byte* cert, word32 certSz) +{ + const WOLFSSH_PVT_KEY* pvtKey; + + pvtKey = FindCertStoreKey(ctx, keyId); + if (pvtKey != NULL) { + if (pvtKey->cert == NULL || cert == NULL || certSz == 0 || + pvtKey->certSz != certSz || + WMEMCMP(pvtKey->cert, cert, certSz) != 0) { + pvtKey = NULL; + } + } + + return pvtKey; +} #endif /* WOLFSSH_CERTS */ @@ -14728,8 +14836,8 @@ static int SignWithCertStoreKey(WOLFSSH* ssh, if (!CryptAcquireCertificatePrivateKey(pCertContext, CRYPT_ACQUIRE_ONLY_NCRYPT_KEY_FLAG | CRYPT_ACQUIRE_SILENT_FLAG, NULL, &hCryptProv, &dwKeySpec, &fCallerFreeProv)) { - DWORD dwErr = GetLastError(); - WLOG(WS_LOG_DEBUG, "SignWithCertStoreKey: Failed to acquire NCRYPT private key, error: %lu", dwErr); + WLOG(WS_LOG_DEBUG, "SignWithCertStoreKey: Failed to acquire NCRYPT " + "private key, error: %lu", (unsigned long)GetLastError()); return WS_CRYPTO_FAILED; } @@ -14776,7 +14884,8 @@ static int SignWithCertStoreKey(WOLFSSH* ssh, if (ret == WS_SUCCESS) { if (nCryptRet != 0) { - WLOG(WS_LOG_DEBUG, "SignWithCertStoreKey: NCryptSignHash failed, error: 0x%08x", nCryptRet); + WLOG(WS_LOG_DEBUG, "SignWithCertStoreKey: NCryptSignHash " + "failed, error: 0x%08lx", (unsigned long)nCryptRet); ret = WS_CRYPTO_FAILED; } else { *sigSz = dwSigLen; @@ -14899,23 +15008,11 @@ static int SignHRsa(WOLFSSH* ssh, byte* sig, word32* sigSz, && !ssh->handshake->useTpm #endif ) { -#ifdef WOLFSSH_WINDOWS_CERT_STORE - /* For cert store keys the private key lives in the Windows cert - * store and the in-memory RsaKey may only contain the public - * half extracted from the certificate. The self-verify step - * still works because the public key was decoded from the cert - * in SendKexDhReply. */ - if (IsCertStoreKey(sigKey->pvtKey)) { - /* Verify using the public-key-only RsaKey decoded from - * the cert store certificate. */ - ret = wolfSSH_RsaVerify(sig, *sigSz, encSig, encSigSz, - &sigKey->sk.rsa.key, heap, "SignHRsa(certStore)"); - } else -#endif /* WOLFSSH_WINDOWS_CERT_STORE */ - { - ret = wolfSSH_RsaVerify(sig, *sigSz, encSig, encSigSz, - &sigKey->sk.rsa.key, heap, "SignHRsa"); - } + /* For a cert store key the RsaKey holds only the public half + * decoded from the certificate by SendKexGetSigningKey(), which + * is all the self-verify needs. */ + ret = wolfSSH_RsaVerify(sig, *sigSz, encSig, encSigSz, + &sigKey->sk.rsa.key, heap, "SignHRsa"); } WS_FORCEZERO(digest, sizeof(digest)); @@ -15074,8 +15171,9 @@ static int SignHEcdsa(WOLFSSH* ssh, byte* sig, word32* sigSz, * NOT DER-encoded. Split directly. */ if (IsCertStoreKey(sigKey->pvtKey)) { ret = CertStoreEccSigToRs(sig, *sigSz, r, &rSz, s, &sSz); - } else -#endif /* WOLFSSH_WINDOWS_CERT_STORE */ + } + else + #endif /* WOLFSSH_WINDOWS_CERT_STORE */ { ret = wc_ecc_sig_to_rs(sig, *sigSz, r, &rSz, s, &sSz); if (ret != 0) { @@ -17318,6 +17416,14 @@ static int PrepareUserAuthRequestRsaCert(WOLFSSH* ssh, word32* payloadSz, if (ret == WS_SUCCESS) { word32 idx = 0; +#ifdef WOLFSSH_WINDOWS_CERT_STORE + /* Note: already inside #ifdef WOLFSSH_CERTS */ + const WOLFSSH_PVT_KEY* pvtKey; + + pvtKey = FindCertStoreAuthKey(ssh->ctx, keySig->keyId, + authData->sf.publicKey.publicKey, + authData->sf.publicKey.publicKeySz); +#endif #ifdef WOLFSSH_AGENT if (ssh->agentEnabled) ret = wc_RsaPublicKeyDecode(authData->sf.publicKey.publicKey, @@ -17326,30 +17432,22 @@ static int PrepareUserAuthRequestRsaCert(WOLFSSH* ssh, word32* payloadSz, else #endif /* WOLFSSH_AGENT */ #ifdef WOLFSSH_WINDOWS_CERT_STORE - /* Note: already inside #ifdef WOLFSSH_CERTS */ - if (authData->sf.publicKey.privateKey == NULL) { + if (pvtKey != NULL) { /* Cert store: decode public key from the stored certificate */ - const WOLFSSH_PVT_KEY* pvtKey; - - pvtKey = FindCertStoreKey(ssh->ctx, keySig->keyId); - if (pvtKey == NULL || pvtKey->cert == NULL) { - ret = WS_BAD_ARGUMENT; - } - else { - byte* pubKeyDer = NULL; - word32 pubKeyDerSz = 0; + byte* pubKeyDer = NULL; + word32 pubKeyDerSz = 0; - ret = ExtractPubKeyDerFromCert(pvtKey->cert, pvtKey->certSz, - &pubKeyDer, &pubKeyDerSz, ssh->ctx->heap); - if (ret == 0) { - idx = 0; - ret = wc_RsaPublicKeyDecode(pubKeyDer, &idx, - &keySig->ks.rsa.key, pubKeyDerSz); - } - if (pubKeyDer != NULL) - WFREE(pubKeyDer, ssh->ctx->heap, DYNTYPE_PUBKEY); + ret = ExtractPubKeyDerFromCert(pvtKey->cert, pvtKey->certSz, + &pubKeyDer, &pubKeyDerSz, ssh->ctx->heap); + if (ret == WS_SUCCESS) { + idx = 0; + ret = wc_RsaPublicKeyDecode(pubKeyDer, &idx, + &keySig->ks.rsa.key, pubKeyDerSz); } - } else + if (pubKeyDer != NULL) + WFREE(pubKeyDer, ssh->ctx->heap, DYNTYPE_PUBKEY); + } + else #endif /* WOLFSSH_WINDOWS_CERT_STORE */ ret = wc_RsaPrivateKeyDecode(authData->sf.publicKey.privateKey, &idx, &keySig->ks.rsa.key, @@ -17477,45 +17575,46 @@ static int BuildUserAuthRequestRsaCert(WOLFSSH* ssh, } if (ret == WS_SUCCESS) { int sigSz; +#ifdef WOLFSSH_WINDOWS_CERT_STORE + const WOLFSSH_PVT_KEY* pvtKey; +#endif + WLOG(WS_LOG_INFO, "Signing hash with RSA."); #ifdef WOLFSSH_WINDOWS_CERT_STORE - if (authData->sf.publicKey.privateKey == NULL) { + pvtKey = FindCertStoreAuthKey(ssh->ctx, keySig->keyId, + authData->sf.publicKey.publicKey, + authData->sf.publicKey.publicKeySz); + if (pvtKey != NULL) { /* Cert store: sign with NCryptSignHash via * SignWithCertStoreKey (pszAlgId=NULL, data is * the already-encoded DigestInfo). */ - const WOLFSSH_PVT_KEY* pvtKey; - - pvtKey = FindCertStoreKey(ssh->ctx, keySig->keyId); - if (pvtKey != NULL) { - word32 outSigSz = keySig->sigSz; - ret = SignWithCertStoreKey(ssh, pvtKey, - encDigest, encDigestSz, hashId, - output + begin, &outSigSz); - if (ret == WS_SUCCESS) { - sigSz = (int)outSigSz; - if (sigSz <= 0 || - (word32)sigSz != keySig->sigSz) { - WLOG(WS_LOG_DEBUG, - "SUAR: Cert store RSA sig length mismatch"); - ret = WS_RSA_E; - } - else { - ret = wolfSSH_RsaVerify(output + begin, - outSigSz, encDigest, encDigestSz, - &keySig->ks.rsa.key, ssh->ctx->heap, - "SUAR(certStore)"); - } - } else { + word32 outSigSz; + + outSigSz = keySig->sigSz; + ret = SignWithCertStoreKey(ssh, pvtKey, + encDigest, encDigestSz, hashId, + output + begin, &outSigSz); + if (ret == WS_SUCCESS) { + sigSz = (int)outSigSz; + if (sigSz <= 0 || (word32)sigSz != keySig->sigSz) { WLOG(WS_LOG_DEBUG, - "SUAR: Cert store RSA sign failed"); + "SUAR: Cert store RSA sig length mismatch"); ret = WS_RSA_E; } - } else { + else { + ret = wolfSSH_RsaVerify(output + begin, + outSigSz, encDigest, encDigestSz, + &keySig->ks.rsa.key, ssh->ctx->heap, + "SUAR(certStore)"); + } + } + else { WLOG(WS_LOG_DEBUG, - "SUAR: Cert store key not found for RSA"); - ret = WS_BAD_ARGUMENT; + "SUAR: Cert store RSA sign failed"); + ret = WS_RSA_E; } - } else + } + else #endif /* WOLFSSH_WINDOWS_CERT_STORE */ { sigSz = wc_RsaSSL_Sign(encDigest, encDigestSz, @@ -17856,31 +17955,29 @@ static int PrepareUserAuthRequestEccCert(WOLFSSH* ssh, word32* payloadSz, if (ret == WS_SUCCESS) { word32 idx = 0; #ifdef WOLFSSH_WINDOWS_CERT_STORE - /* Note: already inside #ifdef WOLFSSH_CERTS. - * Cert store: no in-memory private key — decode public key from - * the DER certificate that UsePrivateKey_fromStore saved. */ - if (authData->sf.publicKey.privateKey == NULL) { - const WOLFSSH_PVT_KEY* pvtKey; + /* Note: already inside #ifdef WOLFSSH_CERTS */ + const WOLFSSH_PVT_KEY* pvtKey; - pvtKey = FindCertStoreKey(ssh->ctx, keySig->keyId); - if (pvtKey == NULL || pvtKey->cert == NULL) { - ret = WS_BAD_ARGUMENT; - } - else { - byte* pubKeyDer = NULL; - word32 pubKeyDerSz = 0; + pvtKey = FindCertStoreAuthKey(ssh->ctx, keySig->keyId, + authData->sf.publicKey.publicKey, + authData->sf.publicKey.publicKeySz); + /* Cert store: no in-memory private key, decode the public key from + * the DER certificate that UsePrivateKey_fromStore saved. */ + if (pvtKey != NULL) { + byte* pubKeyDer = NULL; + word32 pubKeyDerSz = 0; - ret = ExtractPubKeyDerFromCert(pvtKey->cert, pvtKey->certSz, - &pubKeyDer, &pubKeyDerSz, ssh->ctx->heap); - if (ret == 0) { - idx = 0; - ret = wc_EccPublicKeyDecode(pubKeyDer, &idx, - &keySig->ks.ecc.key, pubKeyDerSz); - } - if (pubKeyDer != NULL) - WFREE(pubKeyDer, ssh->ctx->heap, DYNTYPE_PUBKEY); + ret = ExtractPubKeyDerFromCert(pvtKey->cert, pvtKey->certSz, + &pubKeyDer, &pubKeyDerSz, ssh->ctx->heap); + if (ret == WS_SUCCESS) { + idx = 0; + ret = wc_EccPublicKeyDecode(pubKeyDer, &idx, + &keySig->ks.ecc.key, pubKeyDerSz); } - } else + if (pubKeyDer != NULL) + WFREE(pubKeyDer, ssh->ctx->heap, DYNTYPE_PUBKEY); + } + else #endif /* WOLFSSH_WINDOWS_CERT_STORE */ { #if 0 @@ -18002,6 +18099,13 @@ static int BuildUserAuthRequestEccCert(WOLFSSH* ssh, #endif #endif { +#ifdef WOLFSSH_WINDOWS_CERT_STORE + const WOLFSSH_PVT_KEY* pvtKey; + + pvtKey = FindCertStoreAuthKey(ssh->ctx, keySig->keyId, + authData->sf.publicKey.publicKey, + authData->sf.publicKey.publicKeySz); +#endif if (ret == WS_SUCCESS) { WLOG(WS_LOG_INFO, "Signing hash with ECDSA cert."); ret = wc_HashInit(&hash, hashId); @@ -18011,16 +18115,16 @@ static int BuildUserAuthRequestEccCert(WOLFSSH* ssh, ret = wc_HashFinal(&hash, hashId, digest); wc_HashFree(&hash, hashId); } + if (ret != WS_SUCCESS) { + WLOG(WS_LOG_DEBUG, "SUAR: Bad ECC Cert Hash"); + ret = WS_ECC_E; + } } #ifdef WOLFSSH_WINDOWS_CERT_STORE /* Cert store signing: NCryptSignHash returns raw r||s */ - if (ret == WS_SUCCESS && - authData->sf.publicKey.privateKey == NULL) { - const WOLFSSH_PVT_KEY* pvtKey; - - pvtKey = FindCertStoreKey(ssh->ctx, keySig->keyId); - if (pvtKey != NULL) { + if (pvtKey != NULL) { + if (ret == WS_SUCCESS) { ret = SignWithCertStoreKey(ssh, pvtKey, digest, digestSz, hashId, sig, &sigSz); if (ret == WS_SUCCESS) { @@ -18034,16 +18138,14 @@ static int BuildUserAuthRequestEccCert(WOLFSSH* ssh, WLOG(WS_LOG_DEBUG, "SUAR: Bad cert store ECC signature"); } - } else { + } + else { WLOG(WS_LOG_DEBUG, "SUAR: Cert store ECC sign failed"); ret = WS_ECC_E; } - } else { - WLOG(WS_LOG_DEBUG, - "SUAR: Cert store key not found for ECC"); - ret = WS_BAD_ARGUMENT; } - } else + } + else #endif /* WOLFSSH_WINDOWS_CERT_STORE */ { if (ret == WS_SUCCESS) { diff --git a/src/ssh.c b/src/ssh.c index 2feed21b7..7b935e029 100644 --- a/src/ssh.c +++ b/src/ssh.c @@ -41,8 +41,22 @@ #include #include #include + /* Fallbacks for SDKs that predate these wincrypt.h definitions. The + * values must match wincrypt.h exactly. */ #ifndef CERT_NCRYPT_KEY_SPEC - #define CERT_NCRYPT_KEY_SPEC 0x00000003 + #define CERT_NCRYPT_KEY_SPEC 0xFFFFFFFF + #endif + #ifndef CERT_SYSTEM_STORE_LOCATION_MASK + #define CERT_SYSTEM_STORE_LOCATION_MASK 0x00FF0000 + #endif + #ifndef CERT_SYSTEM_STORE_CURRENT_USER + #define CERT_SYSTEM_STORE_CURRENT_USER 0x00010000 + #endif + #ifndef CERT_SYSTEM_STORE_LOCAL_MACHINE + #define CERT_SYSTEM_STORE_LOCAL_MACHINE 0x00020000 + #endif + #ifndef CERT_SYSTEM_STORE_USERS + #define CERT_SYSTEM_STORE_USERS 0x00060000 #endif #endif /* WOLFSSH_WINDOWS_CERT_STORE */ @@ -2841,34 +2855,48 @@ int wolfSSH_CTX_AddRootCert_buffer(WOLFSSH_CTX* ctx, } #ifdef WOLFSSH_WINDOWS_CERT_STORE -/* Find the certificate in hStore whose Common Name exactly matches - * subjectName. subjectName may include a leading "CN=" prefix. +/* Find the certificate in hStore whose Common Name matches subjectName. + * subjectName may include a leading "CN=" prefix. * CERT_FIND_SUBJECT_STR_W is only used as a substring pre-filter to - * enumerate candidates; each candidate's CN is then compared exactly so + * enumerate candidates; each candidate's CN is then compared in full so * that a lookup for "server1" does not select "server1.example" or - * "myserver1". A currently time-valid match is preferred over an expired - * one so a renewal's leftover certificate is not selected. Returns the - * certificate context (caller must free with CertFreeCertificateContext) - * or NULL when no exact match exists. */ -static PCCERT_CONTEXT FindCertByExactCN(HCERTSTORE hStore, - const wchar_t* subjectName) + * "myserver1". The compare is case insensitive, matching both the + * pre-filter and X.500 name semantics. A candidate that is currently + * time-valid and has a private key is preferred, so that neither a + * renewal's leftover certificate nor a public-only duplicate ends the + * search. The selected certificate is stored in out, and is NULL when no + * match exists. The caller frees it with CertFreeCertificateContext. + * Returns WS_SUCCESS on success. */ +static int FindCertByExactCN(void* heap, HCERTSTORE hStore, + const wchar_t* subjectName, PCCERT_CONTEXT* out) { PCCERT_CONTEXT pCertContext; - PCCERT_CONTEXT firstMatch; + PCCERT_CONTEXT validMatch; + PCCERT_CONTEXT expiredMatch; const wchar_t* cn; wchar_t* certCn; DWORD certCnSz; + DWORD propSz; int match; + int hasKey; + int ret; + + *out = NULL; + ret = WS_SUCCESS; /* Strip an optional "CN=" prefix from the requested name. */ cn = subjectName; - if (wcslen(cn) > 3 && - (wcsncmp(cn, L"CN=", 3) == 0 || wcsncmp(cn, L"cn=", 3) == 0)) { + if (wcslen(cn) >= 3 && _wcsnicmp(cn, L"CN=", 3) == 0) { cn = cn + 3; } + if (*cn == L'\0') { + WLOG(WS_LOG_ERROR, "FindCertByExactCN: Empty common name requested"); + return WS_BAD_ARGUMENT; + } pCertContext = NULL; - firstMatch = NULL; + validMatch = NULL; + expiredMatch = NULL; for (;;) { /* Passing the previous context frees it and continues the search. */ pCertContext = CertFindCertificateInStore(hStore, @@ -2882,65 +2910,93 @@ static PCCERT_CONTEXT FindCertByExactCN(HCERTSTORE hStore, if (certCnSz <= 1) { continue; } - certCn = (wchar_t*)WMALLOC(certCnSz * sizeof(wchar_t), NULL, + certCn = (wchar_t*)WMALLOC(certCnSz * sizeof(wchar_t), heap, DYNTYPE_TEMP); if (certCn == NULL) { CertFreeCertificateContext(pCertContext); pCertContext = NULL; + ret = WS_MEMORY_E; break; } certCnSz = CertGetNameStringW(pCertContext, CERT_NAME_ATTR_TYPE, 0, (void*)szOID_COMMON_NAME, certCn, certCnSz); - match = (certCnSz > 1 && wcscmp(certCn, cn) == 0); - WFREE(certCn, NULL, DYNTYPE_TEMP); - if (match) { - if (CertVerifyTimeValidity(NULL, pCertContext->pCertInfo) == 0) { + match = (certCnSz > 1 && _wcsicmp(certCn, cn) == 0); + WFREE(certCn, heap, DYNTYPE_TEMP); + if (!match) { + continue; + } + + /* A duplicate that cannot sign must not end the search. */ + propSz = 0; + hasKey = CertGetCertificateContextProperty(pCertContext, + CERT_KEY_PROV_INFO_PROP_ID, NULL, &propSz); + if (CertVerifyTimeValidity(NULL, pCertContext->pCertInfo) == 0) { + if (hasKey) { break; } - if (firstMatch == NULL) { - firstMatch = CertDuplicateCertificateContext(pCertContext); + if (validMatch == NULL) { + validMatch = CertDuplicateCertificateContext(pCertContext); } } + else if (expiredMatch == NULL) { + expiredMatch = CertDuplicateCertificateContext(pCertContext); + } } - /* No time-valid match; fall back to the first exact match, if any. */ - if (pCertContext == NULL) { - pCertContext = firstMatch; + /* An allocation failure is reported as such rather than falling back + * to a candidate the enumeration had already rejected. */ + if (ret == WS_SUCCESS && pCertContext == NULL) { + if (validMatch != NULL) { + WLOG(WS_LOG_WARN, "FindCertByExactCN: No match with a private " + "key, using '%ls' anyway", subjectName); + pCertContext = validMatch; + validMatch = NULL; + } + else if (expiredMatch != NULL) { + WLOG(WS_LOG_WARN, "FindCertByExactCN: No time-valid match, " + "using an expired '%ls'", subjectName); + pCertContext = expiredMatch; + expiredMatch = NULL; + } } - else if (firstMatch != NULL) { - CertFreeCertificateContext(firstMatch); + if (validMatch != NULL) { + CertFreeCertificateContext(validMatch); + } + if (expiredMatch != NULL) { + CertFreeCertificateContext(expiredMatch); + } + + if (ret == WS_MEMORY_E) { + WLOG(WS_LOG_ERROR, "FindCertByExactCN: Memory allocation failed"); + } + else { + *out = pCertContext; } - return pCertContext; + return ret; } -/* Fill the private key slot for keyId with cert-store backed state. Any - * existing file-based or cert-store resources in the slot are replaced. - * The slot takes its own reference on pCertContext and its own copies of - * the name strings and certificate DER so that every slot can be freed - * independently by CtxResourceFree. On failure the slot and - * ctx->privateKeyCount are left unchanged. - * Returns WS_SUCCESS on success. */ -static int UseCertStoreSlot(WOLFSSH_CTX* ctx, byte keyId, - PCCERT_CONTEXT pCertContext, const wchar_t* storeName, - const wchar_t* subjectName, word32 dwFlags) -{ - WOLFSSH_PVT_KEY* pvtKey; - PCCERT_CONTEXT slotContext; - wchar_t* storeNameCopy; - wchar_t* subjectNameCopy; - byte* certBuf; - size_t storeNameLen; - size_t subjectNameLen; +/* Resources for one cert-store backed private key slot. Everything is + * allocated before any slot is modified so that registering the plain key + * type and the matching X.509 type is all or nothing. */ +typedef struct CertStoreSlot { + PCCERT_CONTEXT context; + wchar_t* storeName; + wchar_t* subjectName; + byte* cert; word32 certSz; word32 keyIdx; - word32 i; - void* heap; + byte keyId; +} CertStoreSlot; - heap = ctx->heap; - /* Find an existing slot of the same type or an available new slot */ +/* Index of the slot holding keyId, WOLFSSH_MAX_PVT_KEYS when not found. */ +static word32 FindKeySlot(WOLFSSH_CTX* ctx, byte keyId) +{ + word32 i; + word32 keyIdx; + keyIdx = WOLFSSH_MAX_PVT_KEYS; for (i = 0; i < ctx->privateKeyCount && i < WOLFSSH_MAX_PVT_KEYS; i++) { if (ctx->privateKey[i].publicKeyFmt == keyId) { @@ -2948,68 +3004,90 @@ static int UseCertStoreSlot(WOLFSSH_CTX* ctx, byte keyId, break; } } - if (keyIdx == WOLFSSH_MAX_PVT_KEYS - && ctx->privateKeyCount >= WOLFSSH_MAX_PVT_KEYS) { - WLOG(WS_LOG_DEBUG, "UseCertStoreSlot: No available key slot"); - return WS_CTX_KEY_COUNT_E; + + return keyIdx; +} + + +/* Release resources of a slot that was prepared but never committed. */ +static void FreeCertStoreSlot(void* heap, CertStoreSlot* slot) +{ + if (slot->context != NULL) { + CertFreeCertificateContext(slot->context); + } + if (slot->storeName != NULL) { + WFREE(slot->storeName, heap, DYNTYPE_STRING); + } + if (slot->subjectName != NULL) { + WFREE(slot->subjectName, heap, DYNTYPE_STRING); } + if (slot->cert != NULL) { + WFREE(slot->cert, heap, DYNTYPE_CERT); + } + WMEMSET(slot, 0, sizeof(*slot)); +} + + +/* Allocate the resources slot keyIdx needs, without modifying the + * context. The slot takes its own reference on pCertContext and its own + * copies of the name strings and certificate DER so that every slot can + * be freed independently by CtxResourceFree. + * Returns WS_SUCCESS on success. */ +static int PrepCertStoreSlot(void* heap, byte keyId, word32 keyIdx, + PCCERT_CONTEXT pCertContext, const wchar_t* storeName, + const wchar_t* subjectName, CertStoreSlot* slot) +{ + size_t storeNameLen; + size_t subjectNameLen; + + WMEMSET(slot, 0, sizeof(*slot)); + slot->keyId = keyId; + slot->keyIdx = keyIdx; + slot->certSz = pCertContext->cbCertEncoded; - /* Allocate every new resource before modifying the slot so a failure - * leaves the context untouched. */ storeNameLen = wcslen(storeName) + 1; subjectNameLen = wcslen(subjectName) + 1; - certSz = pCertContext->cbCertEncoded; - storeNameCopy = (wchar_t*)WMALLOC(storeNameLen * sizeof(wchar_t), + slot->storeName = (wchar_t*)WMALLOC(storeNameLen * sizeof(wchar_t), heap, DYNTYPE_STRING); - subjectNameCopy = (wchar_t*)WMALLOC(subjectNameLen * sizeof(wchar_t), + slot->subjectName = (wchar_t*)WMALLOC(subjectNameLen * sizeof(wchar_t), heap, DYNTYPE_STRING); - certBuf = (byte*)WMALLOC(certSz, heap, DYNTYPE_CERT); - if (storeNameCopy == NULL || subjectNameCopy == NULL || certBuf == NULL) { - if (storeNameCopy != NULL) - WFREE(storeNameCopy, heap, DYNTYPE_STRING); - if (subjectNameCopy != NULL) - WFREE(subjectNameCopy, heap, DYNTYPE_STRING); - if (certBuf != NULL) - WFREE(certBuf, heap, DYNTYPE_CERT); - WLOG(WS_LOG_DEBUG, "UseCertStoreSlot: Memory allocation failed"); + slot->cert = (byte*)WMALLOC(slot->certSz, heap, DYNTYPE_CERT); + slot->context = CertDuplicateCertificateContext(pCertContext); + if (slot->storeName == NULL || slot->subjectName == NULL + || slot->cert == NULL || slot->context == NULL) { + FreeCertStoreSlot(heap, slot); + WLOG(WS_LOG_ERROR, "PrepCertStoreSlot: Memory allocation failed"); return WS_MEMORY_E; } - WMEMCPY(storeNameCopy, storeName, storeNameLen * sizeof(wchar_t)); - WMEMCPY(subjectNameCopy, subjectName, subjectNameLen * sizeof(wchar_t)); - WMEMCPY(certBuf, pCertContext->pbCertEncoded, certSz); - - /* Each slot holds its own reference on the certificate context */ - slotContext = CertDuplicateCertificateContext(pCertContext); - if (slotContext == NULL) { - WFREE(storeNameCopy, heap, DYNTYPE_STRING); - WFREE(subjectNameCopy, heap, DYNTYPE_STRING); - WFREE(certBuf, heap, DYNTYPE_CERT); - WLOG(WS_LOG_DEBUG, "Failed CertDuplicateCertificateContext"); - return WS_FATAL_ERROR; - } + WMEMCPY(slot->storeName, storeName, storeNameLen * sizeof(wchar_t)); + WMEMCPY(slot->subjectName, subjectName, subjectNameLen * sizeof(wchar_t)); + WMEMCPY(slot->cert, pCertContext->pbCertEncoded, slot->certSz); + + return WS_SUCCESS; +} - /* if no existing matching key id was found append the key to the end */ - if (keyIdx == WOLFSSH_MAX_PVT_KEYS) { - keyIdx = ctx->privateKeyCount; - ctx->privateKeyCount++; - } - pvtKey = &ctx->privateKey[keyIdx]; - /* Free existing resources if replacing an existing slot. The slot may - * previously have held either a cert-store key or a file-based - * key/cert, so clear both kinds of resources. */ +/* Move the prepared resources into the context. The slot may previously + * have held either a cert-store key or a file-based key/cert, so clear + * both kinds of resources. Cannot fail. */ +static void CommitCertStoreSlot(WOLFSSH_CTX* ctx, CertStoreSlot* slot, + word32 dwFlags) +{ + WOLFSSH_PVT_KEY* pvtKey; + void* heap; + + heap = ctx->heap; + pvtKey = &ctx->privateKey[slot->keyIdx]; + if (pvtKey->certStoreContext != NULL) { CertFreeCertificateContext( (PCCERT_CONTEXT)pvtKey->certStoreContext); - pvtKey->certStoreContext = NULL; } if (pvtKey->storeName != NULL) { WFREE(pvtKey->storeName, heap, DYNTYPE_STRING); - pvtKey->storeName = NULL; } if (pvtKey->subjectName != NULL) { WFREE(pvtKey->subjectName, heap, DYNTYPE_STRING); - pvtKey->subjectName = NULL; } if (pvtKey->key != NULL) { WS_FORCEZERO(pvtKey->key, pvtKey->keySz); @@ -3019,38 +3097,41 @@ static int UseCertStoreSlot(WOLFSSH_CTX* ctx, byte keyId, } if (pvtKey->cert != NULL) { WFREE(pvtKey->cert, heap, DYNTYPE_CERT); - pvtKey->cert = NULL; - pvtKey->certSz = 0; } - /* Set up the private key structure */ - pvtKey->publicKeyFmt = keyId; + pvtKey->publicKeyFmt = slot->keyId; #ifdef WOLFSSH_TPM /* A stale TPM mark would route signing through the TPM. */ pvtKey->isTpm = 0; #endif pvtKey->useCertStore = 1; - pvtKey->certStoreContext = (void*)slotContext; - pvtKey->storeName = storeNameCopy; - pvtKey->subjectName = subjectNameCopy; + pvtKey->certStoreContext = (void*)slot->context; + pvtKey->storeName = slot->storeName; + pvtKey->subjectName = slot->subjectName; pvtKey->dwFlags = dwFlags; - pvtKey->cert = certBuf; - pvtKey->certSz = certSz; + pvtKey->cert = slot->cert; + pvtKey->certSz = slot->certSz; - return WS_SUCCESS; + /* Ownership moved to the context. */ + WMEMSET(slot, 0, sizeof(*slot)); } /* Load a private key from MS Certificate Store * storeName: Certificate store name (e.g., L"My", L"Root") - * dwFlags: Certificate store flags (e.g., CERT_SYSTEM_STORE_CURRENT_USER) + * dwFlags: Certificate store location, and only a location (e.g. + * CERT_SYSTEM_STORE_CURRENT_USER). Control flags such as + * CERT_STORE_DELETE_FLAG would make CertOpenStore destructive and are + * rejected. The store is opened read-only. * subjectName: Certificate subject Common Name for lookup, with or without - * a "CN=" prefix. The CN must match exactly; thumbprint lookup is not - * currently implemented. + * a "CN=" prefix. The CN must match in full, case insensitively; + * thumbprint lookup is not currently implemented. * The key is registered both as its plain key type and, mirroring the * file-based HostKey plus HostCertificate pairing, as the matching * RFC6187 x509v3-* type so the store certificate itself can be sent as * the public host key to peers that negotiate certificate algorithms. + * The x509v3-* registration is skipped when the build has no such + * algorithm for the key type, e.g. RSA with SHA-1 disabled. * returns WS_SUCCESS on success */ int wolfSSH_CTX_UsePrivateKey_fromStore(WOLFSSH_CTX* ctx, @@ -3061,7 +3142,13 @@ int wolfSSH_CTX_UsePrivateKey_fromStore(WOLFSSH_CTX* ctx, HCERTSTORE hStore = NULL; PCCERT_CONTEXT pCertContext = NULL; byte keyId = ID_NONE; + byte certId = ID_NONE; PCERT_PUBLIC_KEY_INFO pPubKeyInfo = NULL; + CertStoreSlot keySlot; + CertStoreSlot certSlot; + word32 keyIdx; + word32 certIdx; + word32 newCount; WLOG(WS_LOG_DEBUG, "Entering wolfSSH_CTX_UsePrivateKey_fromStore()"); @@ -3070,18 +3157,34 @@ int wolfSSH_CTX_UsePrivateKey_fromStore(WOLFSSH_CTX* ctx, return WS_BAD_ARGUMENT; } - /* Open the certificate store */ + /* Only accept system-store location bits. Anything else is either not + * a location or a control flag (e.g. CERT_STORE_DELETE_FLAG) that + * would make CertOpenStore destructive. */ + if ((dwFlags & (word32)CERT_SYSTEM_STORE_LOCATION_MASK) == 0 || + (dwFlags & ~(word32)CERT_SYSTEM_STORE_LOCATION_MASK) != 0) { + WLOG(WS_LOG_ERROR, "wolfSSH_CTX_UsePrivateKey_fromStore: Store " + "flags are not a system store location"); + return WS_BAD_ARGUMENT; + } + + /* Open the certificate store. Read-only, both because nothing here + * writes to it and because a read/write open of a LOCAL_MACHINE store + * fails for a non-administrator service account. */ hStore = CertOpenStore(CERT_STORE_PROV_SYSTEM_W, 0, (HCRYPTPROV_LEGACY)0, - (DWORD)dwFlags | CERT_STORE_OPEN_EXISTING_FLAG, storeName); + (DWORD)dwFlags | CERT_STORE_OPEN_EXISTING_FLAG + | CERT_STORE_READONLY_FLAG, storeName); if (hStore == NULL) { - DWORD dwErr = GetLastError(); - WLOG(WS_LOG_DEBUG, "wolfSSH_CTX_UsePrivateKey_fromStore: Failed to open store, error: %lu", dwErr); + WLOG(WS_LOG_ERROR, "wolfSSH_CTX_UsePrivateKey_fromStore: Failed to " + "open store, error: %lu", (unsigned long)GetLastError()); return WS_FATAL_ERROR; } - /* Find the certificate by exact Common Name match. */ - pCertContext = FindCertByExactCN(hStore, subjectName); - + /* Find the certificate by full Common Name match. */ + ret = FindCertByExactCN(ctx->heap, hStore, subjectName, &pCertContext); + if (ret != WS_SUCCESS) { + CertCloseStore(hStore, 0); + return ret; + } if (pCertContext == NULL) { CertCloseStore(hStore, 0); WLOG(WS_LOG_ERROR, "wolfSSH_CTX_UsePrivateKey_fromStore: Certificate " @@ -3099,8 +3202,7 @@ int wolfSSH_CTX_UsePrivateKey_fromStore(WOLFSSH_CTX* ctx, * host key type that cannot be used for signing. */ if (pPubKeyInfo->Algorithm.pszObjId != NULL) { /* Compare OID strings (they are ASCII, not wide) */ - if (strcmp(pPubKeyInfo->Algorithm.pszObjId, szOID_RSA_RSA) == 0 || - strcmp(pPubKeyInfo->Algorithm.pszObjId, szOID_RSA_ENCRYPT) == 0) { + if (strcmp(pPubKeyInfo->Algorithm.pszObjId, szOID_RSA_RSA) == 0) { #ifndef WOLFSSH_NO_RSA keyId = ID_SSH_RSA; #else @@ -3180,15 +3282,17 @@ int wolfSSH_CTX_UsePrivateKey_fromStore(WOLFSSH_CTX* ctx, if (!CryptAcquireCertificatePrivateKey(pCertContext, CRYPT_ACQUIRE_ONLY_NCRYPT_KEY_FLAG | CRYPT_ACQUIRE_SILENT_FLAG, NULL, &hKey, &dwKeySpec, &fCallerFree)) { - DWORD dwErr = GetLastError(); WLOG(WS_LOG_ERROR, "wolfSSH_CTX_UsePrivateKey_fromStore: Cannot " "access private key, error: %lu. Check that the current user " - "or service account has permission to access the key.", dwErr); + "or service account has permission to access the key.", + (unsigned long)GetLastError()); CertFreeCertificateContext(pCertContext); CertCloseStore(hStore, 0); return WS_CRYPTO_FAILED; } - /* Release the key handle since we just needed to verify access */ + /* Release the key handle since we just needed to verify access. + * CRYPT_ACQUIRE_ONLY_NCRYPT_KEY_FLAG makes the CNG case the only + * reachable one; the CSP release is kept for the flags changing. */ if (fCallerFree) { if (dwKeySpec == CERT_NCRYPT_KEY_SPEC) { NCryptFreeObject(hKey); @@ -3204,21 +3308,57 @@ int wolfSSH_CTX_UsePrivateKey_fromStore(WOLFSSH_CTX* ctx, /* Register the key under its plain type so peers without RFC6187 * support get a raw public key, and under the matching X.509 type so * the store certificate can be sent as K_S when a peer negotiates an - * x509v3-* algorithm. On failure of the second registration the first - * slot stays in the context; it is fully owned by the context and is - * released by CtxResourceFree. */ - ret = UseCertStoreSlot(ctx, keyId, pCertContext, storeName, subjectName, - dwFlags); + * x509v3-* algorithm. Both slots are located and prepared before + * either is committed, so a failure leaves the context, including any + * host key already in these slots, exactly as it was. */ + WMEMSET(&keySlot, 0, sizeof(keySlot)); + WMEMSET(&certSlot, 0, sizeof(certSlot)); + newCount = ctx->privateKeyCount; + keyIdx = FindKeySlot(ctx, keyId); + if (keyIdx == WOLFSSH_MAX_PVT_KEYS) { + keyIdx = newCount++; + } + + /* CertTypeForId returns keyId unchanged when the build has no X509 + * equivalent; skip the X509 ID slot in that case. */ + certId = CertTypeForId(keyId); + certIdx = WOLFSSH_MAX_PVT_KEYS; + if (certId != keyId) { + certIdx = FindKeySlot(ctx, certId); + if (certIdx == WOLFSSH_MAX_PVT_KEYS) { + certIdx = newCount++; + } + } + else { + WLOG(WS_LOG_INFO, "wolfSSH_CTX_UsePrivateKey_fromStore: No x509v3 " + "algorithm for key type %d in this build, registering the " + "plain host key only", keyId); + } + + if (newCount > WOLFSSH_MAX_PVT_KEYS) { + WLOG(WS_LOG_ERROR, "wolfSSH_CTX_UsePrivateKey_fromStore: No " + "available key slot"); + ret = WS_CTX_KEY_COUNT_E; + } if (ret == WS_SUCCESS) { - byte certId; - - certId = CertTypeForId(keyId); - /* CertTypeForId returns keyId unchanged when no X509 equivalent was - * found; skip adding the X509 ID slot in that case. */ - if (certId != keyId) { - ret = UseCertStoreSlot(ctx, certId, pCertContext, storeName, - subjectName, dwFlags); + ret = PrepCertStoreSlot(ctx->heap, keyId, keyIdx, pCertContext, + storeName, subjectName, &keySlot); + } + if (ret == WS_SUCCESS && certIdx != WOLFSSH_MAX_PVT_KEYS) { + ret = PrepCertStoreSlot(ctx->heap, certId, certIdx, pCertContext, + storeName, subjectName, &certSlot); + } + + if (ret == WS_SUCCESS) { + CommitCertStoreSlot(ctx, &keySlot, dwFlags); + if (certIdx != WOLFSSH_MAX_PVT_KEYS) { + CommitCertStoreSlot(ctx, &certSlot, dwFlags); } + ctx->privateKeyCount = newCount; + } + else { + FreeCertStoreSlot(ctx->heap, &keySlot); + FreeCertStoreSlot(ctx->heap, &certSlot); } /* Each registered slot holds its own reference on the certificate diff --git a/tests/unit.c b/tests/unit.c index 688ec7575..ebad3f8e5 100644 --- a/tests/unit.c +++ b/tests/unit.c @@ -55,13 +55,20 @@ defined(WOLFSSL_CERT_GEN) && !defined(WOLFSSH_NO_ECDSA) && \ !defined(NO_FILESYSTEM) #define WOLFSSH_TEST_CERTMAN_PROMOTE + #include + #include +#endif + +/* Cert manager tests that read a test cert off disk. A superset of the + * WOLFSSH_TEST_CERTMAN_PROMOTE conditions above. */ +#if defined(WOLFSSH_CERTS) && !defined(WOLFSSH_NO_ECDSA) && \ + !defined(NO_FILESYSTEM) + #define WOLFSSH_TEST_CERTMAN_ROOTCA /* The certman helpers use malloc/free and LONG_MAX; pull these in here so * the tests build even when the SCP block below is not compiled. * certman.h itself comes from the WOLFSSH_CERTS block below. */ #include #include - #include - #include #endif #ifdef WOLFSSH_CERTS @@ -79,6 +86,9 @@ #ifndef CERT_SYSTEM_STORE_LOCAL_MACHINE #define CERT_SYSTEM_STORE_LOCAL_MACHINE 0x00020000 #endif + #ifndef CERT_SYSTEM_STORE_USERS + #define CERT_SYSTEM_STORE_USERS 0x00060000 + #endif #endif #ifdef WOLFSSH_SFTP @@ -9541,7 +9551,13 @@ static int test_IdentifyAsn1Key(void) return result; } -#ifdef WOLFSSH_TEST_CERTMAN_PROMOTE +#endif /* WOLFSSH_TEST_INTERNAL */ + +/* The cert manager tests below use only public API, so they are outside the + * WOLFSSH_TEST_INTERNAL section. Each carries its own feature guard; + * WOLFSSH_TEST_CERTMAN_PROMOTE still implies WOLFSSH_TEST_INTERNAL. */ + +#ifdef WOLFSSH_TEST_CERTMAN_ROOTCA /* Read a whole file into a freshly malloc'd buffer. Caller frees *buf. */ static int certmanLoadFile(const char* fn, byte** buf, word32* bufSz) @@ -9590,6 +9606,10 @@ static int certmanLoadFile(const char* fn, byte** buf, word32* bufSz) return 0; } +#endif /* WOLFSSH_TEST_CERTMAN_ROOTCA */ + +#ifdef WOLFSSH_TEST_CERTMAN_PROMOTE + /* Forge an end-entity cert whose issuer is the supplied cert and which is * signed with the supplied (non-CA) key. Fills der/derSz on success. */ static int certmanForgeChild(const byte* issuerCert, word32 issuerCertSz, @@ -10006,34 +10026,42 @@ static int test_CertMan_PromoteValidCaIntermediate(void) #ifdef WOLFSSH_CERTS /* wolfSSH_SetCertManager imports a WOLFSSL_CERT_MANAGER by reference into * the wolfSSH context. Test argument checking, importing the same manager - * twice, replacing an already-imported manager, and the reference count - * that keeps the manager alive after the WOLFSSL_CTX that created it is - * freed (a missing reference shows up as a use-after-free/double-free - * under the sanitizer builds). */ + * twice, replacing an already-imported manager, and the reference counting + * on both sides of the import: the imported manager must outlive the + * reference it was created with, and must outlive the wolfSSH context. Each + * of those is checked by loading a root CA through the manager afterwards, + * so a missing up_ref shows up as a failure or as a use-after-free under + * the sanitizer builds. */ static int test_SetCertManager(void) { int result = 0; WOLFSSH_CTX* ctx = NULL; - WOLFSSL_CTX* sslCtx = NULL; - WOLFSSL_CTX* sslCtx2 = NULL; WOLFSSL_CERT_MANAGER* cm = NULL; + WOLFSSL_CERT_MANAGER* cm2 = NULL; +#ifdef WOLFSSH_TEST_CERTMAN_ROOTCA + byte* root = NULL; + word32 rootSz = 0; - ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); - if (ctx == NULL) + /* run from the source root so ./keys resolves */ + if (certmanLoadFile("./keys/ca-cert-ecc.der", &root, &rootSz) != 0) { + printf("SetCertManager: can't load root cert\n"); result = -1; + } +#endif if (result == 0) { - sslCtx = wolfSSL_CTX_new(wolfSSLv23_server_method()); - if (sslCtx == NULL) + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + if (ctx == NULL) result = -2; } - /* bad arguments */ if (result == 0) { - cm = wolfSSL_CTX_GetCertManager(sslCtx); + cm = wolfSSL_CertManagerNew(); if (cm == NULL) result = -3; } + + /* bad arguments */ if (result == 0 && wolfSSH_SetCertManager(NULL, cm) != WS_BAD_ARGUMENT) result = -4; if (result == 0 && wolfSSH_SetCertManager(ctx, NULL) != WS_BAD_ARGUMENT) @@ -10045,34 +10073,50 @@ static int test_SetCertManager(void) if (result == 0 && wolfSSH_SetCertManager(ctx, cm) != WS_SUCCESS) result = -7; - /* the context must hold its own reference: freeing the WOLFSSL_CTX - * that created the manager must leave the imported manager usable */ + /* the context holds its own reference: dropping the creating reference + * must leave the imported manager usable */ if (result == 0) { - wolfSSL_CTX_free(sslCtx); - sslCtx = NULL; + wolfSSL_CertManagerFree(cm); + cm = NULL; } +#ifdef WOLFSSH_TEST_CERTMAN_ROOTCA + if (result == 0 && wolfSSH_CTX_AddRootCert_buffer(ctx, root, rootSz, + WOLFSSH_FORMAT_ASN1) != WS_SUCCESS) + result = -8; +#endif - /* replace the imported manager with one from a second WOLFSSL_CTX, - * releasing the reference on the first manager */ - if (result == 0) { - sslCtx2 = wolfSSL_CTX_new(wolfSSLv23_server_method()); - if (sslCtx2 == NULL) - result = -8; - } + /* replacing the imported manager releases the reference on the old one */ if (result == 0) { - cm = wolfSSL_CTX_GetCertManager(sslCtx2); - if (cm == NULL) + cm2 = wolfSSL_CertManagerNew(); + if (cm2 == NULL) result = -9; - else if (wolfSSH_SetCertManager(ctx, cm) != WS_SUCCESS) + else if (wolfSSH_SetCertManager(ctx, cm2) != WS_SUCCESS) result = -10; } +#ifdef WOLFSSH_TEST_CERTMAN_ROOTCA + if (result == 0 && wolfSSH_CTX_AddRootCert_buffer(ctx, root, rootSz, + WOLFSSH_FORMAT_ASN1) != WS_SUCCESS) + result = -11; +#endif - if (sslCtx != NULL) - wolfSSL_CTX_free(sslCtx); - if (sslCtx2 != NULL) - wolfSSL_CTX_free(sslCtx2); + /* the caller's reference outlives the context that imported it */ if (ctx != NULL) wolfSSH_CTX_free(ctx); +#ifdef WOLFSSH_TEST_CERTMAN_ROOTCA + if (result == 0 && cm2 != NULL && + wolfSSL_CertManagerLoadCABuffer(cm2, root, rootSz, + WOLFSSL_FILETYPE_ASN1) != WOLFSSL_SUCCESS) + result = -12; +#endif + + if (cm != NULL) + wolfSSL_CertManagerFree(cm); + if (cm2 != NULL) + wolfSSL_CertManagerFree(cm2); +#ifdef WOLFSSH_TEST_CERTMAN_ROOTCA + if (root != NULL) + free(root); +#endif return result; } @@ -10182,6 +10226,8 @@ static int test_ParseCertStoreSpec(void) } #endif /* WOLFSSH_WINDOWS_CERT_STORE */ +#ifdef WOLFSSH_TEST_INTERNAL + /* Tests below install a custom allocator via wolfSSL_SetAllocators. The * wolfSSL_Malloc_cb / wolfSSL_Free_cb / wolfSSL_Realloc_cb typedefs gain * extra parameters when wolfSSL is built with WOLFSSL_STATIC_MEMORY or @@ -13556,13 +13602,13 @@ int wolfSSH_UnitTest(int argc, char** argv) #endif -#if defined(WOLFSSH_TEST_INTERNAL) && defined(WOLFSSH_CERTS) +#ifdef WOLFSSH_CERTS unitResult = test_SetCertManager(); printf("SetCertManager: %s\n", (unitResult == 0 ? "SUCCESS" : "FAILED")); testResult = testResult || unitResult; #endif -#if defined(WOLFSSH_TEST_INTERNAL) && defined(WOLFSSH_WINDOWS_CERT_STORE) +#ifdef WOLFSSH_WINDOWS_CERT_STORE unitResult = test_ParseCertStoreSpec(); printf("ParseCertStoreSpec: %s\n", (unitResult == 0 ? "SUCCESS" : "FAILED")); diff --git a/wolfssh/certman.h b/wolfssh/certman.h index fe68aeaf5..54a2b4007 100644 --- a/wolfssh/certman.h +++ b/wolfssh/certman.h @@ -30,8 +30,10 @@ #include #include -#include /* included for WOLFSSH_CTX */ -#include /* included for WOLFSSL_CERT_MANAGER struct */ +#ifdef WOLFSSH_CERTS + #include /* included for WOLFSSH_CTX */ + #include /* included for WOLFSSL_CERT_MANAGER struct */ +#endif #ifdef __cplusplus extern "C" { @@ -42,8 +44,12 @@ struct WOLFSSH_CERTMAN; typedef struct WOLFSSH_CERTMAN WOLFSSH_CERTMAN; +#ifdef WOLFSSH_CERTS +/* Replaces the CTX's cert manager with cm, taking a reference on it and + * applying wolfSSH's revocation policy. */ WOLFSSH_API int wolfSSH_SetCertManager(WOLFSSH_CTX* ctx, WOLFSSL_CERT_MANAGER* cm); +#endif /* WOLFSSH_CERTS */ WOLFSSH_API WOLFSSH_CERTMAN* wolfSSH_CERTMAN_new(void* heap); @@ -60,12 +66,15 @@ int wolfSSH_CERTMAN_VerifyCerts_buffer(WOLFSSH_CERTMAN* cm, const unsigned char* cert, word32 certSz, word32 certCount); -#ifdef WOLFSSH_WINDOWS_CERT_STORE +#if defined(WOLFSSH_CERTS) && defined(WOLFSSH_WINDOWS_CERT_STORE) +/* Splits "store:subject[:flags]", where flags is CURRENT_USER, + * LOCAL_MACHINE, or a decimal or 0x hex CERT_SYSTEM_STORE_* location, and + * defaults to CURRENT_USER. The subject may not contain a ':'. */ WOLFSSH_API int wolfSSH_ParseCertStoreSpec(const char* spec, wchar_t** wStoreName, wchar_t** wSubjectName, word32* dwFlags, void* heap); -#endif /* WOLFSSH_WINDOWS_CERT_STORE */ +#endif /* WOLFSSH_CERTS && WOLFSSH_WINDOWS_CERT_STORE */ #ifdef __cplusplus diff --git a/wolfssh/internal.h b/wolfssh/internal.h index 6e217e1cf..5576bb1f3 100644 --- a/wolfssh/internal.h +++ b/wolfssh/internal.h @@ -64,6 +64,8 @@ #ifndef _WIN32 #error "WOLFSSH_WINDOWS_CERT_STORE requires a Windows (_WIN32) target" #endif + /* the cert store fields below are wchar_t strings */ + #include #endif /* WOLFSSH_WINDOWS_CERT_STORE */ #ifdef WOLFSSH_TPM diff --git a/wolfssh/ssh.h b/wolfssh/ssh.h index 7dc826c0f..beb35ca86 100644 --- a/wolfssh/ssh.h +++ b/wolfssh/ssh.h @@ -500,6 +500,15 @@ WOLFSSH_API int wolfSSH_CTX_UsePrivateKey_buffer(WOLFSSH_CTX* ctx, WOLFSSH_API int wolfSSH_CTX_AddRootCert_buffer(WOLFSSH_CTX* ctx, const byte* cert, word32 certSz, int format); #ifdef WOLFSSH_WINDOWS_CERT_STORE + /* Use the certificate with Common Name subjectName, and its private + * key, from the storeName system certificate store as the host key. + * subjectName may carry a "CN=" prefix and matches in full, case + * insensitively. dwFlags selects the store location and must hold + * only CERT_SYSTEM_STORE_* location bits, e.g. + * CERT_SYSTEM_STORE_CURRENT_USER; control flags such as + * CERT_STORE_DELETE_FLAG are rejected with WS_BAD_ARGUMENT. The store + * is opened read-only. Returns WS_SUCCESS on success; on any failure + * the context is left unchanged. */ WOLFSSH_API int wolfSSH_CTX_UsePrivateKey_fromStore(WOLFSSH_CTX* ctx, const wchar_t* storeName, word32 dwFlags, const wchar_t* subjectName); diff --git a/wolfssh/test.h b/wolfssh/test.h index d156d7f9b..c720c9fc8 100644 --- a/wolfssh/test.h +++ b/wolfssh/test.h @@ -1148,9 +1148,10 @@ static INLINE void build_addr_ipv6(struct sockaddr_in6* addr, const char* peer, #ifdef WOLFSSH_TEST_HEX2BIN +#ifndef WOLFSSL_BASE16 + #define BAD 0xFF -#ifndef WOLFSSL_BASE16 static const byte hexDecode[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, @@ -1220,6 +1221,9 @@ static int Base16_Decode(const byte* in, word32 inLen, *outLen = outIdx; return 0; } + +#undef BAD + #else #include #endif /* !WOLFSSL_BASE16 */ From 51629a72702434cdef0e47b8b4dce0eb5ba9a7fb Mon Sep 17 00:00:00 2001 From: JacobBarthelmeh Date: Tue, 4 Aug 2026 09:40:49 -0600 Subject: [PATCH 08/10] enable SHA1 with windows cert store test case --- .github/workflows/windows-cert-store-test.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/windows-cert-store-test.yml b/.github/workflows/windows-cert-store-test.yml index 07176365c..b1af9c3ac 100644 --- a/.github/workflows/windows-cert-store-test.yml +++ b/.github/workflows/windows-cert-store-test.yml @@ -61,7 +61,11 @@ jobs: # Enable the Windows cert store API (not in the repo user_settings.h). # Appended to wolfssh/ide/winvs/user_settings.h, which the VS projects # put on the include path before wolfssl/IDE/WIN. - printf '\n/* Appended by windows-cert-store-test CI */\n#define WOLFSSH_WINDOWS_CERT_STORE\n' >> ${{env.USER_SETTINGS_H_NEW}} + # WC_SIG_MIN_HASH_TYPE: x509v3-ssh-rsa signs H with SHA-1 (RFC 6187 + # names no other RSA form), which wc_SignatureVerify rejects at its + # default SHA-256 floor. Needed for the RSA x509v3 host key step; see + # the same define in tpm-ssh.yml. + printf '\n/* Appended by windows-cert-store-test CI */\n#define WOLFSSH_WINDOWS_CERT_STORE\n#define WC_SIG_MIN_HASH_TYPE WC_HASH_TYPE_SHA\n' >> ${{env.USER_SETTINGS_H_NEW}} cp ${{env.USER_SETTINGS_H_NEW}} ${{env.USER_SETTINGS_H}} - name: Build wolfssl library From cca116e78010f51177a8a03201fa65396969bb61 Mon Sep 17 00:00:00 2001 From: JacobBarthelmeh Date: Tue, 4 Aug 2026 21:32:00 -0600 Subject: [PATCH 09/10] refactoring, expand test cases, adjust to authorized key file, minor dead code adjustments --- .github/workflows/windows-cert-store-test.yml | 97 +++++- apps/wolfsshd/auth.c | 53 ++- apps/wolfsshd/configuration.c | 12 +- apps/wolfsshd/wolfsshd.c | 221 +++++++++--- examples/client/common.c | 105 +++--- examples/client/common.h | 8 +- examples/echoserver/echoserver.c | 2 +- examples/sftpclient/sftpclient.c | 22 +- ide/winvs/user_settings.h | 16 + src/certman.c | 26 +- src/internal.c | 69 +++- src/ssh.c | 319 ++++++++++++------ tests/unit.c | 34 +- wolfssh/certman.h | 12 +- wolfssh/internal.h | 14 +- 15 files changed, 723 insertions(+), 287 deletions(-) diff --git a/.github/workflows/windows-cert-store-test.yml b/.github/workflows/windows-cert-store-test.yml index b1af9c3ac..ac87a10be 100644 --- a/.github/workflows/windows-cert-store-test.yml +++ b/.github/workflows/windows-cert-store-test.yml @@ -61,11 +61,21 @@ jobs: # Enable the Windows cert store API (not in the repo user_settings.h). # Appended to wolfssh/ide/winvs/user_settings.h, which the VS projects # put on the include path before wolfssl/IDE/WIN. - # WC_SIG_MIN_HASH_TYPE: x509v3-ssh-rsa signs H with SHA-1 (RFC 6187 - # names no other RSA form), which wc_SignatureVerify rejects at its - # default SHA-256 floor. Needed for the RSA x509v3 host key step; see - # the same define in tpm-ssh.yml. - printf '\n/* Appended by windows-cert-store-test CI */\n#define WOLFSSH_WINDOWS_CERT_STORE\n#define WC_SIG_MIN_HASH_TYPE WC_HASH_TYPE_SHA\n' >> ${{env.USER_SETTINGS_H_NEW}} + # RFC 6187 names only one RSA X.509 algorithm, x509v3-ssh-rsa, and it + # signs with SHA-1, so both SHA-1 gates have to come down for any RSA + # certificate to negotiate: + # WC_SIG_MIN_HASH_TYPE - wc_SignatureVerify otherwise rejects + # SHA-1 at its SHA-256 floor (see the + # same define in tpm-ssh.yml). + # WOLFSSH_NO_SHA1_SOFT_DISABLE - x509v3-ssh-rsa is otherwise absent + # from cannedKeyAlgoNames, so the server + # never lists it in server-sig-algs and + # the client's RSA certificate fails + # PrepareUserAuthRequestPublicKey() with + # WS_MATCH_KEY_ALGO_E. + # Both lists put their SHA-1 entries last, so the ECDSA entries in the + # matrix still negotiate the same SHA-2 algorithms as before. + printf '\n/* Appended by windows-cert-store-test CI */\n#define WOLFSSH_WINDOWS_CERT_STORE\n#define WOLFSSH_NO_SHA1_SOFT_DISABLE\n#define WC_SIG_MIN_HASH_TYPE WC_HASH_TYPE_SHA\n' >> ${{env.USER_SETTINGS_H_NEW}} cp ${{env.USER_SETTINGS_H_NEW}} ${{env.USER_SETTINGS_H}} - name: Build wolfssl library @@ -180,10 +190,19 @@ jobs: grep -q -- '-lncrypt' Makefile - name: Rejects a non-Windows host and a missing --enable-certs + # Each assertion exits explicitly: bash errexit exempts a command + # inverted with '!', and the step status comes from the last command. run: | - ! ./configure --enable-certs --enable-windows-cert-store $WOLFSSL_CACHE - ! ./configure --host=x86_64-w64-mingw32 --enable-windows-cert-store \ - $WOLFSSL_CACHE + if ./configure --enable-certs --enable-windows-cert-store \ + $WOLFSSL_CACHE; then + echo 'ERROR: configure should have failed on a non-Windows host' + exit 1 + fi + if ./configure --host=x86_64-w64-mingw32 \ + --enable-windows-cert-store $WOLFSSL_CACHE; then + echo 'ERROR: configure should have failed without --enable-certs' + exit 1 + fi test: needs: build @@ -205,23 +224,32 @@ jobs: client_key_source: x509 key_algorithm: rsa test_name: "Server-Store-Client-X509" + # key_algorithm is the server host key; client_key_algorithm is the + # testuser client certificate key. Both are stated explicitly on the + # store-client entries so neither depends on which key renewcerts.sh + # happens to copy. - server_key_source: file client_key_source: store key_algorithm: rsa + client_key_algorithm: ecdsa test_name: "Server-File-Client-Store" - server_key_source: store client_key_source: store key_algorithm: rsa + client_key_algorithm: ecdsa test_name: "Server-Store-Client-Store" - server_key_source: store client_key_source: x509 key_algorithm: ecdsa test_name: "Server-Store-Client-X509-ECDSA" + # RSA client certificate, covering the x509v3-ssh-rsa user-auth and + # client-side RSA cert store signing paths that the ECDSA entries + # above cannot reach. - server_key_source: file client_key_source: store key_algorithm: rsa - client_key_algorithm: ecdsa - test_name: "Server-File-Client-Store-ECDSA" + client_key_algorithm: rsa + test_name: "Server-File-Client-Store-RSA" steps: - uses: actions/checkout@v4 @@ -254,12 +282,20 @@ jobs: cd keys bash renewcerts.sh testuser - # renewcerts.sh always gives testuser fred's RSA key. Re-issue it with - # an EC key when the client store entry is meant to be ECDSA. - if [ "${{ matrix.client_key_algorithm }}" = "ecdsa" ]; then + # renewcerts.sh copies fred's key, which is EC prime256v1, so testuser + # comes out ECDSA. Re-issue it explicitly for whichever algorithm the + # matrix entry asks for, rather than inheriting whatever fred's key + # happens to be. + ALG="${{ matrix.client_key_algorithm }}" + if [ -n "$ALG" ]; then touch index.txt sed 's/fred/testuser/g' renewcerts.cnf > renewcerts-testuser.cnf - openssl ecparam -name prime256v1 -genkey -noout -out testuser-key.pem + if [ "$ALG" = "rsa" ]; then + openssl genrsa -out testuser-key.pem 2048 + else + openssl ecparam -name prime256v1 -genkey -noout \ + -out testuser-key.pem + fi openssl req -subj "/C=US/ST=WA/L=Seattle/O=wolfSSL Inc/OU=Development/CN=testuser/emailAddress=testuser@example.com" \ -key testuser-key.pem -out testuser-cert.csr \ -config renewcerts-testuser.cnf -new -nodes @@ -268,7 +304,11 @@ jobs: -CA ca-cert-ecc.pem -CAkey ca-key-ecc.pem -out testuser-cert.pem \ -set_serial 7 openssl x509 -in testuser-cert.pem -outform DER -out testuser-cert.der - openssl ec -in testuser-key.pem -outform DER -out testuser-key.der + if [ "$ALG" = "rsa" ]; then + openssl rsa -in testuser-key.pem -outform DER -out testuser-key.der + else + openssl ec -in testuser-key.pem -outform DER -out testuser-key.der + fi rm -f renewcerts-testuser.cnf testuser-cert.csr index.* fi cd .. @@ -278,6 +318,29 @@ jobs: ls -la keys/ exit 1 fi + + # Assert the key really is the algorithm this entry asked for, so a + # change to renewcerts.sh cannot silently turn an entry into a + # duplicate of another one. Unset means whatever renewcerts.sh gives, + # which is fred's EC key. + EXPECT="${{ matrix.client_key_algorithm }}" + [ -n "$EXPECT" ] || EXPECT=ecdsa + if openssl rsa -inform DER -in keys/testuser-key.der -noout 2>/dev/null + then + ACTUAL=rsa + elif openssl ec -inform DER -in keys/testuser-key.der -noout 2>/dev/null + then + ACTUAL=ecdsa + else + echo "ERROR: testuser-key.der is neither RSA nor EC" + exit 1 + fi + if [ "$ACTUAL" != "$EXPECT" ]; then + echo "ERROR: testuser client key is $ACTUAL, expected $EXPECT" + exit 1 + fi + echo "testuser client key algorithm: $ACTUAL" + echo "CLIENT_CERT_FILE=keys/testuser-cert.der" >> $GITHUB_ENV echo "CLIENT_KEY_FILE=keys/testuser-key.der" >> $GITHUB_ENV @@ -493,8 +556,8 @@ jobs: } if ("${{ matrix.server_key_source }}" -eq "store") { - # The certificate is part of the store entry; do NOT specify - # HostCertificate separately. + # The certificate is part of the store entry. HostKey and + # HostCertificate alongside HostKeyStore are rejected at startup. $configContent += @" HostKeyStore My diff --git a/apps/wolfsshd/auth.c b/apps/wolfsshd/auth.c index cef2669d5..055ead8f6 100644 --- a/apps/wolfsshd/auth.c +++ b/apps/wolfsshd/auth.c @@ -643,6 +643,46 @@ static int IsAbsoluteAuthKeysPath(const char* path) return ret; } +#if defined(WOLFSSH_CERTS) && (defined(WOLFSSL_FPKI) || defined(_WIN32)) +/* True when the AuthorizedKeysFile pattern is guaranteed to resolve to a + * different file for every account, which is what makes an entry in it an + * implicit user-to-credential binding. A relative pattern resolves under the + * account's home directory, and an absolute one qualifies only when it carries + * a %u or %h token. An absolute pattern with neither (e.g. + * "/etc/ssh/authorized_keys_all") is one shared file for every account and + * binds a credential to nothing. */ +static int IsPerUserAuthKeysPattern(const char* pattern) +{ + word32 i; + word32 patSz; + + if (pattern == NULL || *pattern == '\0') { + /* the built-in ~/.ssh/authorized_keys default */ + return 1; + } + + if (!IsAbsoluteAuthKeysPath(pattern)) { + return 1; + } + + patSz = (word32)WSTRLEN(pattern); + for (i = 0; (i + 1) < patSz; i++) { + if (pattern[i] != '%') { + continue; + } + if (pattern[i + 1] == 'u' || pattern[i + 1] == 'h') { + return 1; + } + /* "%%" is a literal percent, step over both characters */ + if (pattern[i + 1] == '%') { + i++; + } + } + + return 0; +} +#endif /* WOLFSSH_CERTS && (WOLFSSL_FPKI || _WIN32) */ + /* Resolve the authorized keys file path for a user. The pattern is passed in * explicitly so concurrent authentications cannot race on it, and its tokens * are expanded so each user resolves to a distinct path. */ @@ -2090,11 +2130,16 @@ static int RequestAuthentication(WS_UserAuthData* authData, if (ret == WOLFSSH_USERAUTH_SUCCESS && authData->type == WOLFSSH_USERAUTH_PUBLICKEY) { /* Bind the certificate to the requested user name via UPN with FPKI or - * CN without FPKI. Only done when relying on the CA; an - * AuthorizedKeysFile entry is itself an explicit user to cert binding - * and is checked below. */ + * CN without FPKI. Skipped only when a per-user AuthorizedKeysFile is + * configured, because such an entry is itself an explicit user to cert + * binding and is checked below. A shared AuthorizedKeysFile (an + * absolute pattern with no %u or %h) resolves to one file for every + * account and binds the certificate to nothing, so the identity check + * still has to run. */ if (authData->sf.publicKey.isCert && - !wolfSSHD_ConfigGetAuthKeysFileSet(usrConf)) { + !(wolfSSHD_ConfigGetAuthKeysFileSet(usrConf) && + IsPerUserAuthKeysPattern( + wolfSSHD_ConfigGetAuthKeysFile(usrConf)))) { DecodedCert* dCert; #ifdef WOLFSSH_SMALL_STACK dCert = (DecodedCert*)WMALLOC(sizeof(DecodedCert), NULL, diff --git a/apps/wolfsshd/configuration.c b/apps/wolfsshd/configuration.c index 9c6af9acf..3d6555121 100644 --- a/apps/wolfsshd/configuration.c +++ b/apps/wolfsshd/configuration.c @@ -1862,8 +1862,10 @@ int wolfSSHD_ConfigSetSystemCA(WOLFSSHD_CONFIG* conf, const char* value) conf->useSystemCA = 0; } else { - wolfSSH_Log(WS_LOG_INFO, "[SSHD] System CAs unexpected flag"); - ret = WS_FATAL_ERROR; + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] wolfSSH_TrustedSystemCAKeys: expected 'yes' or 'no', " + "got '%s'", value); + ret = WS_BAD_ARGUMENT; } } @@ -1903,8 +1905,10 @@ int wolfSSHD_ConfigSetUserCAStore(WOLFSSHD_CONFIG* conf, const char* value) conf->useUserCAStore = 0; } else { - wolfSSH_Log(WS_LOG_INFO, "[SSHD] User CA store unexpected flag"); - ret = WS_FATAL_ERROR; + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] wolfSSH_TrustedUserCAStore: expected 'yes' or 'no', " + "got '%s'", value); + ret = WS_BAD_ARGUMENT; } } diff --git a/apps/wolfsshd/wolfsshd.c b/apps/wolfsshd/wolfsshd.c index 2dff553f0..51b93fe16 100644 --- a/apps/wolfsshd/wolfsshd.c +++ b/apps/wolfsshd/wolfsshd.c @@ -252,8 +252,11 @@ static void interruptCatch(int in) /* redirect logging to a specific file and add the PID value */ static void wolfSSHDLoggingCb(enum wolfSSH_LogLevel lvl, const char *const str) { - /* always log errors and optionally log other info/debug level messages */ - if (lvl == WS_LOG_ERROR || debugMode) { + /* Always log errors and warnings, and optionally log other info/debug + * level messages. Warnings carry the security relevant notices, e.g. that + * a certificate was bound to an account by subject CN alone, so they must + * not depend on -d. */ + if (lvl == WS_LOG_ERROR || lvl == WS_LOG_WARN || debugMode) { fprintf(logFile, "[PID %d]: %s\n", WGETPID(), str); /* flush so each line is visible immediately, e.g. to a consumer * reading the log file while the daemon is still running */ @@ -367,6 +370,24 @@ static void CleanupCTX(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX** ctx, } #if defined(WOLFSSH_CERTS) && defined(WOLFSSH_WINDOWS_CERT_STORE) +/* Returns 1 when val is one of the defined CERT_SYSTEM_STORE_* location + * codes. The mask alone is not enough: values such as 0x000A0000 sit inside + * it but name no store, and CertOpenStore would fail with an opaque error + * instead of the configuration reporting a bad value. The location id lives + * in the high half; 1, 2 and 4 through 9 are assigned, 3 is not. */ +static int IsCertStoreLocation(unsigned long val) +{ + unsigned long id; + + if (val == 0 || + (val & ~(unsigned long)CERT_SYSTEM_STORE_LOCATION_MASK) != 0) { + return 0; + } + id = val >> 16; + + return id == 1 || id == 2 || (id >= 4 && id <= 9); +} + /* Parse a Windows system store location, given either as a CERT_SYSTEM_STORE_* * name (long or short form) or as a number in strtoul() base 0 form, so both * 65536 and 0x00010000 work. Only location bits are accepted; anything else is @@ -401,8 +422,7 @@ static int ParseCertStoreLocation(const char* in, word32* out) if (end == in || *end != '\0' || errno == ERANGE) { ret = WS_BAD_ARGUMENT; } - else if ((val & (unsigned long)CERT_SYSTEM_STORE_LOCATION_MASK) == 0 || - (val & ~(unsigned long)CERT_SYSTEM_STORE_LOCATION_MASK) != 0) { + else if (!IsCertStoreLocation(val)) { ret = WS_BAD_ARGUMENT; } else { @@ -413,16 +433,25 @@ static int ParseCertStoreLocation(const char* in, word32* out) return ret; } -/* Returns 1 when der holds an X.509 certificate with basicConstraints - * CA:TRUE, 0 otherwise. */ +/* Result of CertIsCA(). Kept distinct so the caller can tell a certificate + * that is genuinely not a CA from one that could not be examined at all. */ +enum { + CERT_CA_NO = 0, /* parsed, basicConstraints CA is not TRUE */ + CERT_CA_YES = 1, /* parsed, basicConstraints CA:TRUE */ + CERT_CA_UNKNOWN = -1 /* could not parse or could not allocate */ +}; + +/* Returns CERT_CA_YES when der holds an X.509 certificate with + * basicConstraints CA:TRUE, CERT_CA_NO when it parses but is not a CA, and + * CERT_CA_UNKNOWN when it could not be examined. */ static int CertIsCA(const byte* der, word32 derSz) { DecodedCert* dCert; - int isCA = 0; + int isCA = CERT_CA_UNKNOWN; #ifdef WOLFSSH_SMALL_STACK dCert = (DecodedCert*)WMALLOC(sizeof(DecodedCert), NULL, DYNTYPE_CERT); if (dCert == NULL) { - return 0; + return CERT_CA_UNKNOWN; } #else DecodedCert sdCert; @@ -432,9 +461,9 @@ static int CertIsCA(const byte* der, word32 derSz) wc_InitDecodedCert(dCert, der, derSz, NULL); if (wc_ParseCert(dCert, CERT_TYPE, NO_VERIFY, NULL) == 0) { - isCA = (dCert->isCA != 0); + isCA = (dCert->isCA != 0) ? CERT_CA_YES : CERT_CA_NO; } - FreeDecodedCert(dCert); + wc_FreeDecodedCert(dCert); #ifdef WOLFSSH_SMALL_STACK WFREE(dCert, NULL, DYNTYPE_CERT); #endif @@ -452,13 +481,15 @@ static int LoadUserCACertsFromStore(const WOLFSSHD_CONFIG* conf, char* storeNameStr; char* dwFlagsStr; char* providerStr; - word32 dwFlags = CERT_SYSTEM_STORE_CURRENT_USER; + word32 dwFlags = 0; wchar_t* wStoreName = NULL; int wStoreNameLen; HCERTSTORE hStore = NULL; PCCERT_CONTEXT pCertContext = NULL; word32 loaded = 0; word32 skipped = 0; + word32 rejected = 0; + int isCA; storeNameStr = wolfSSHD_ConfigGetWinUserPvPara(conf); dwFlagsStr = wolfSSHD_ConfigGetWinUserDwFlags(conf); @@ -467,12 +498,27 @@ static int LoadUserCACertsFromStore(const WOLFSSHD_CONFIG* conf, /* Every certificate in this store becomes a trust anchor for client * authentication, so the administrator must name it. There is no default: * guessing one silently would pick a store the administrator never - * reviewed. */ + * reviewed. Name a store created for this purpose, e.g. 'SSH_UserCA'. + * The Windows 'Root', 'AuthRoot' and 'CA' stores must not be used: they + * are populated by the Microsoft Trusted Root Program, so pointing at one + * makes every public commercial CA an SSH login authority. */ if (storeNameStr == NULL) { wolfSSH_Log(WS_LOG_ERROR, "[SSHD] wolfSSH_TrustedUserCAStore is enabled but no store name " - "is configured. Set wolfSSH_WinUserPvPara to the store holding " - "the client CA certificates to trust, e.g. 'Root' or 'CA'."); + "is configured. Set wolfSSH_WinUserPvPara to a store holding " + "nothing but the client CA certificates to trust, e.g. " + "'SSH_UserCA'. Do not use the public 'Root', 'AuthRoot' or 'CA' " + "stores."); + return WS_BAD_ARGUMENT; + } + + if (WSTRCMP(storeNameStr, "Root") == 0 || + WSTRCMP(storeNameStr, "AuthRoot") == 0 || + WSTRCMP(storeNameStr, "CA") == 0) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] wolfSSH_WinUserPvPara='%s' names a Windows public trust " + "store. Every CA it holds would become an SSH login authority. " + "Use a store created for this purpose instead.", storeNameStr); return WS_BAD_ARGUMENT; } @@ -487,14 +533,28 @@ static int LoadUserCACertsFromStore(const WOLFSSHD_CONFIG* conf, return WS_BAD_ARGUMENT; } - /* An unset location keeps the CERT_SYSTEM_STORE_CURRENT_USER default set - * above. */ - if (dwFlagsStr != NULL && - ParseCertStoreLocation(dwFlagsStr, &dwFlags) != WS_SUCCESS) { + /* The location is mandatory for the same reason the store name is. The + * per-user hive is writable by the account the daemon runs as, without + * elevation, so silently defaulting to CURRENT_USER would let anything + * running as that account add a trust anchor. */ + if (dwFlagsStr == NULL) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] wolfSSH_TrustedUserCAStore is enabled but no store " + "location is configured. Set wolfSSH_WinUserDwFlags, normally to " + "LOCAL_MACHINE."); + return WS_BAD_ARGUMENT; + } + if (ParseCertStoreLocation(dwFlagsStr, &dwFlags) != WS_SUCCESS) { wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Unrecognized user CA store flags '%s'", dwFlagsStr); return WS_BAD_ARGUMENT; } + if (dwFlags == (word32)CERT_SYSTEM_STORE_CURRENT_USER) { + wolfSSH_Log(WS_LOG_WARN, + "[SSHD] wolfSSH_WinUserDwFlags selects the per-user store hive, " + "which the daemon's own account can write to without elevation. " + "LOCAL_MACHINE is the safer location for trust anchors."); + } wStoreNameLen = MultiByteToWideChar(CP_UTF8, 0, storeNameStr, -1, NULL, 0); if (wStoreNameLen == 0) { @@ -534,11 +594,14 @@ static int LoadUserCACertsFromStore(const WOLFSSHD_CONFIG* conf, /* wolfSSL does not enforce basicConstraints CA:TRUE for user-loaded * trust anchors, so an end-entity certificate sitting in the store * would become a login authority. Filter it out here. */ - if (!CertIsCA(pCertContext->pbCertEncoded, - (word32)pCertContext->cbCertEncoded)) { + isCA = CertIsCA(pCertContext->pbCertEncoded, + (word32)pCertContext->cbCertEncoded); + if (isCA != CERT_CA_YES) { skipped++; - wolfSSH_Log(WS_LOG_INFO, - "[SSHD] Skipping a non-CA cert in store '%s'", storeNameStr); + wolfSSH_Log(WS_LOG_WARN, + "[SSHD] Skipping a cert in store '%s': %s", storeNameStr, + isCA == CERT_CA_NO ? "not a CA (no basicConstraints CA:TRUE)" + : "could not be parsed"); continue; } if (wolfSSH_CTX_AddRootCert_buffer(ctx, @@ -546,8 +609,9 @@ static int LoadUserCACertsFromStore(const WOLFSSHD_CONFIG* conf, (word32)pCertContext->cbCertEncoded, WOLFSSH_FORMAT_ASN1) != WS_SUCCESS) { /* Skip certs wolfSSH cannot use as a trust anchor. */ - wolfSSH_Log(WS_LOG_INFO, - "[SSHD] Skipping a cert in store '%s' that could not be " + rejected++; + wolfSSH_Log(WS_LOG_WARN, + "[SSHD] Skipping a CA cert in store '%s' that could not be " "loaded as a root CA", storeNameStr); continue; } @@ -559,15 +623,22 @@ static int LoadUserCACertsFromStore(const WOLFSSHD_CONFIG* conf, if (loaded == 0) { wolfSSH_Log(WS_LOG_ERROR, - "[SSHD] No usable CA certificates found in store '%s' (%u non-CA " - "cert(s) skipped)", storeNameStr, skipped); + "[SSHD] No usable CA certificates found in store '%s' (%u not a " + "CA, %u rejected as a root CA)", storeNameStr, skipped, rejected); ret = WS_FATAL_ERROR; } else { - wolfSSH_Log(WS_LOG_ERROR, + wolfSSH_Log(WS_LOG_INFO, "[SSHD] Trusting %u CA certificate(s) from store '%s' " - "(location 0x%08lx) for client authentication, %u non-CA cert(s) " - "skipped", loaded, storeNameStr, (unsigned long)dwFlags, skipped); + "(location 0x%08lx) for client authentication (%u not a CA, %u " + "rejected as a root CA)", loaded, storeNameStr, + (unsigned long)dwFlags, skipped, rejected); + if (rejected > 0) { + wolfSSH_Log(WS_LOG_WARN, + "[SSHD] %u CA certificate(s) in store '%s' could not be " + "loaded; the trust anchor set is incomplete", rejected, + storeNameStr); + } } return ret; @@ -632,6 +703,27 @@ static int SetupCTX(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX** ctx, "HostKeyStore is missing"); ret = WS_BAD_ARGUMENT; } + /* The store branch below wins over the file path, so a HostKey line + * left in place would be silently discarded. StartSSHD() already + * rejects the same conflict expressed with -h. */ + else if (hostKeyStore != NULL && + wolfSSHD_ConfigGetHostKeyFile(conf) != NULL) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] HostKey conflicts with the configured HostKeyStore. " + "Use one or the other."); + ret = WS_BAD_ARGUMENT; + } + /* A store host key carries its own certificate. A HostCertificate + * file would land on the same x509v3 slot and leave it advertised + * with no signing material behind it. */ + else if (hostKeyStore != NULL && + wolfSSHD_ConfigGetHostCertFile(conf) != NULL) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] HostCertificate conflicts with the configured " + "HostKeyStore, which supplies its own certificate. Use one or " + "the other."); + ret = WS_BAD_ARGUMENT; + } if (ret == WS_SUCCESS && hostKeyStore != NULL && hostKeyStoreSubject != NULL) { @@ -652,20 +744,20 @@ static int SetupCTX(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX** ctx, } /* Convert to wide strings */ - storeNameLen = MultiByteToWideChar(CP_UTF8, 0, hostKeyStore, -1, - NULL, 0); - subjectNameLen = MultiByteToWideChar(CP_UTF8, 0, - hostKeyStoreSubject, -1, NULL, 0); + if (ret == WS_SUCCESS) { + storeNameLen = MultiByteToWideChar(CP_UTF8, 0, hostKeyStore, + -1, NULL, 0); + subjectNameLen = MultiByteToWideChar(CP_UTF8, 0, + hostKeyStoreSubject, -1, NULL, 0); - if (ret != WS_SUCCESS) { - /* flag parsing failed; error already logged */ - } - else if (storeNameLen == 0 || subjectNameLen == 0) { - wolfSSH_Log(WS_LOG_ERROR, - "[SSHD] Failed to convert cert store strings to wchar"); - ret = WS_BAD_ARGUMENT; + if (storeNameLen == 0 || subjectNameLen == 0) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Failed to convert cert store strings to wchar"); + ret = WS_BAD_ARGUMENT; + } } - else { + + if (ret == WS_SUCCESS) { wStoreName = (wchar_t*)WMALLOC( storeNameLen * sizeof(wchar_t), heap, DYNTYPE_SSHD); wSubjectName = (wchar_t*)WMALLOC( @@ -859,14 +951,14 @@ static int SetupCTX(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX** ctx, if (ret == WS_SUCCESS && wolfSSHD_ConfigGetSystemCA(conf)) { WOLFSSL_CTX* sslCtx; - wolfSSH_Log(WS_LOG_ERROR, + wolfSSH_Log(WS_LOG_WARN, "[SSHD] WARNING: wolfSSH_TrustedSystemCAKeys makes every CA in " "the OS trust store an SSH user authentication authority. Any " "certificate issued by any of them whose subject matches a local " "account name can log in as that account. Use this only when the " "OS trust store holds solely your organization's CA."); #ifdef WOLFSSH_NO_FPKI - wolfSSH_Log(WS_LOG_ERROR, + wolfSSH_Log(WS_LOG_WARN, "[SSHD] WARNING: built without FPKI profile checking, so peer " "certificates are not required to carry a client authentication " "EKU. A TLS server, S/MIME or code signing certificate with a " @@ -874,13 +966,15 @@ static int SetupCTX(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX** ctx, #endif sslCtx = wolfSSL_CTX_new(wolfSSLv23_server_method()); if (sslCtx == NULL) { - wolfSSH_Log(WS_LOG_INFO, "[SSHD] Unable to create temporary CTX"); + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Unable to create temporary CTX for the system CAs"); ret = WS_FATAL_ERROR; } if (ret == WS_SUCCESS) { if (wolfSSL_CTX_load_system_CA_certs(sslCtx) != WOLFSSL_SUCCESS) { - wolfSSH_Log(WS_LOG_INFO, "[SSHD] Issue loading system CAs"); + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Issue loading system CAs"); ret = WS_FATAL_ERROR; } } @@ -888,7 +982,7 @@ static int SetupCTX(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX** ctx, if (ret == WS_SUCCESS) { if (wolfSSH_SetCertManager(*ctx, wolfSSL_CTX_GetCertManager(sslCtx)) != WS_SUCCESS) { - wolfSSH_Log(WS_LOG_INFO, + wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Issue copying over system CAs"); ret = WS_FATAL_ERROR; } @@ -913,6 +1007,21 @@ static int SetupCTX(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX** ctx, /* Load user CA certs (trust anchors used to verify client X.509 certs) * directly from a Windows certificate store into the cert manager. */ #if defined(WOLFSSH_CERTS) && defined(WOLFSSH_WINDOWS_CERT_STORE) + /* Mirror the HostKeyStore* validation: the wolfSSH_WinUser* group only + * has an effect through LoadUserCACertsFromStore(), so accepting it + * without the store enabled would start the daemon with no client CA + * trust anchors and no indication why logins fail. */ + if (ret == WS_SUCCESS && !wolfSSHD_ConfigGetUserCAStore(conf) && + (wolfSSHD_ConfigGetWinUserPvPara(conf) != NULL || + wolfSSHD_ConfigGetWinUserDwFlags(conf) != NULL || + wolfSSHD_ConfigGetWinUserStores(conf) != NULL)) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] wolfSSH_WinUserPvPara/wolfSSH_WinUserDwFlags/" + "wolfSSH_WinUserStores are set but wolfSSH_TrustedUserCAStore is " + "not enabled, so they would have no effect."); + ret = WS_BAD_ARGUMENT; + } + if (ret == WS_SUCCESS && wolfSSHD_ConfigGetUserCAStore(conf)) { ret = LoadUserCACertsFromStore(conf, *ctx, heap); } @@ -929,8 +1038,11 @@ static int SetupCTX(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX** ctx, * fixed by the wolfSSL build, not by configuration, so this cannot be * derived from the config file. */ #if defined(WOLFSSH_CERTS) && !defined(WOLFSSL_FPKI) && defined(_WIN32) - if (ret == WS_SUCCESS) { - wolfSSH_Log(WS_LOG_ERROR, + if (ret == WS_SUCCESS && + (wolfSSHD_ConfigGetUserCAKeysFile(conf) != NULL || + wolfSSHD_ConfigGetUserCAStore(conf) || + wolfSSHD_ConfigGetSystemCA(conf))) { + wolfSSH_Log(WS_LOG_WARN, "[SSHD] WARNING: client certificates are bound to an account by " "subject CN only. Any CA in the trusted user CA set may assert " "any CN, so keep that set narrow. Build wolfSSL with FPKI for " @@ -938,6 +1050,19 @@ static int SetupCTX(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX** ctx, } #endif + /* A per-user AuthorizedKeysFile is its own user-to-certificate binding, so + * the certificate identity check, and with it the UPN realm allowlist, is + * not run for those accounts. Say so rather than leaving the directive + * looking enforced. */ + if (ret == WS_SUCCESS && + wolfSSHD_ConfigGetAuthorizedUPNDomains(conf) != NULL && + wolfSSHD_ConfigGetAuthKeysFileSet(conf)) { + wolfSSH_Log(WS_LOG_WARN, + "[SSHD] AuthorizedUPNDomains is not enforced for accounts with a " + "per-user AuthorizedKeysFile; those certificates are bound by " + "their authorized_keys entry instead."); + } + /* load in CA certs from file set */ if (ret == WS_SUCCESS) { char* caCert = wolfSSHD_ConfigGetUserCAKeysFile(conf); diff --git a/examples/client/common.c b/examples/client/common.c index bf8b34b15..e5c7b6a6f 100644 --- a/examples/client/common.c +++ b/examples/client/common.c @@ -49,9 +49,8 @@ #ifdef WOLFSSH_CERTS #include #ifdef WOLFSSH_WINDOWS_CERT_STORE + /* windows.h pulls in wincrypt.h; no NCrypt API is called from here. */ #include - #include - #include #endif /* WOLFSSH_WINDOWS_CERT_STORE */ #endif @@ -765,6 +764,8 @@ int ClientUseCert(const char* certName, void* heap) userPublicKeyTypeSz = (word32)WSTRLEN((const char*)publicKeyType); pubKeyLoaded = 1; userPublicKeyAlloc = 1; + /* this buffer is ours now, not the CTX's */ + userPublicKeyCtxOwned = 0; } else { /* Defensive: load_der_file() clears its output pointer on the @@ -967,6 +968,8 @@ static int wolfSSH_TPM_InitKey(WOLFTPM2_DEV* dev, const char* name, if (rc == 0) { userPublicKey = p; userPublicKeyAlloc = 1; + /* this buffer is ours now, not the CTX's */ + userPublicKeyCtxOwned = 0; } else { WLOG(WS_LOG_DEBUG, "Reading public key failed, rc: %d", rc); } @@ -1110,6 +1113,8 @@ int ClientUsePubKey(const char* pubKeyName, int userEcc, void* heap) if (ret == 0) { pubKeyLoaded = 1; userPublicKeyAlloc = 1; + /* this buffer is ours now, not the CTX's */ + userPublicKeyCtxOwned = 0; } else { userPublicKey = userPublicKeyBuf; @@ -1239,78 +1244,86 @@ int ClientSetPrivateKeyFromStore(WOLFSSH_CTX* ctx, * the certificate for public key authentication. * For x509 cert auth the "public key" is the DER certificate, and the type * is the x509v3 name that matches the key algorithm. */ -int ClientSetupCertStoreAuth(WOLFSSH_CTX* ctx) +int ClientSetupCertStoreAuth(WOLFSSH_CTX* ctx, void* heap) { - const byte* keyType; + const byte* keyType = NULL; + WOLFSSH_PVT_KEY* pvtKey = NULL; word32 i; if (ctx == NULL) return WS_BAD_ARGUMENT; + /* wolfSSH_CTX_UsePrivateKey_fromStore() registers a store key under its + * plain key type and, when the build has one, under the matching x509v3 + * type. Only the x509v3 slot can be offered for certificate user auth, + * and it only exists when that algorithm is compiled in, so select on it + * rather than trusting slot ordering. */ for (i = 0; i < ctx->privateKeyCount && i < WOLFSSH_MAX_PVT_KEYS; i++) { - WOLFSSH_PVT_KEY* pvtKey = &ctx->privateKey[i]; - if (!pvtKey->useCertStore) + WOLFSSH_PVT_KEY* cur = &ctx->privateKey[i]; + + if (!cur->useCertStore || cur->cert == NULL || cur->certSz == 0) continue; /* Map the internal key format to the x509v3 SSH type name. Resolve * it before touching the globals so a failure leaves them alone. */ - switch (pvtKey->publicKeyFmt) { - case ID_SSH_RSA: + switch (cur->publicKeyFmt) { case ID_X509V3_SSH_RSA: - case ID_RSA_SHA2_256: - case ID_RSA_SHA2_512: keyType = (const byte*)"x509v3-ssh-rsa"; break; - case ID_ECDSA_SHA2_NISTP256: case ID_X509V3_ECDSA_SHA2_NISTP256: keyType = (const byte*)"x509v3-ecdsa-sha2-nistp256"; break; - case ID_ECDSA_SHA2_NISTP384: case ID_X509V3_ECDSA_SHA2_NISTP384: keyType = (const byte*)"x509v3-ecdsa-sha2-nistp384"; break; - case ID_ECDSA_SHA2_NISTP521: case ID_X509V3_ECDSA_SHA2_NISTP521: keyType = (const byte*)"x509v3-ecdsa-sha2-nistp521"; break; default: - fprintf(stderr, "Unsupported cert store key type: %d\n", - pvtKey->publicKeyFmt); - return WS_BAD_ARGUMENT; - } - - /* Drop anything an earlier file based load left behind, the cert - * store key replaces it. */ - if (userPublicKeyAlloc && userPublicKey != NULL) { - WFREE(userPublicKey, ctx->heap, DYNTYPE_PRIVKEY); - userPublicKeyAlloc = 0; - } - if (userPrivateKeyAlloc && userPrivateKey != NULL) { - wc_ForceZero(userPrivateKey, userPrivateKeySz); - WFREE(userPrivateKey, ctx->heap, DYNTYPE_PRIVKEY); - userPrivateKeyAlloc = 0; + /* the plain twin of the same store key, keep looking */ + continue; } + pvtKey = cur; + break; + } - /* Point userPublicKey at the DER certificate stored in the CTX. The - * ctx-owned flag stops ClientFreeBuffers from freeing CTX memory. - * The alias is only valid while the slot keeps its certificate: - * re-loading a host key onto this slot frees it, so do not mix this - * with the file-key loaders on the same CTX. */ - userPublicKey = pvtKey->cert; - userPublicKeySz = pvtKey->certSz; - userPublicKeyCtxOwned = 1; - userPublicKeyType = keyType; - userPublicKeyTypeSz = (word32)WSTRLEN((const char*)keyType); - - /* No in-memory private key, signing goes through the cert store. */ - userPrivateKey = userPrivateKeyBuf; - userPrivateKeySz = 0; + if (pvtKey == NULL) { + fprintf(stderr, "No cert store key with an x509v3 algorithm found in " + "CTX. RSA cert store keys need SHA-1 enabled " + "(WOLFSSH_NO_SHA1_SOFT_DISABLE) on both ends.\n"); + return WS_BAD_ARGUMENT; + } - pubKeyLoaded = 1; - return WS_SUCCESS; + /* Drop anything an earlier file based load left behind, the cert + * store key replaces it. Freed with the same heap the loaders in this + * file allocate with. */ + if (userPublicKeyAlloc && userPublicKey != NULL) { + WFREE(userPublicKey, heap, DYNTYPE_PRIVKEY); + userPublicKey = userPublicKeyBuf; + userPublicKeyAlloc = 0; } + if (userPrivateKeyAlloc && userPrivateKey != NULL) { + wc_ForceZero(userPrivateKey, userPrivateKeySz); + WFREE(userPrivateKey, heap, DYNTYPE_PRIVKEY); + userPrivateKeyAlloc = 0; + } + + /* Point userPublicKey at the DER certificate stored in the CTX. The + * ctx-owned flag stops ClientFreeBuffers from freeing CTX memory. + * The alias is only valid while the slot keeps its certificate: + * re-loading a host key onto this slot frees it, so do not mix this + * with the file-key loaders on the same CTX. */ + userPublicKey = pvtKey->cert; + userPublicKeySz = pvtKey->certSz; + userPublicKeyCtxOwned = 1; + userPublicKeyType = keyType; + userPublicKeyTypeSz = (word32)WSTRLEN((const char*)keyType); + + /* No in-memory private key, signing goes through the cert store. */ + userPrivateKey = userPrivateKeyBuf; + userPrivateKeySz = 0; - fprintf(stderr, "No cert store key found in CTX\n"); - return WS_BAD_ARGUMENT; + pubKeyLoaded = 1; + return WS_SUCCESS; } #endif /* WOLFSSH_WINDOWS_CERT_STORE */ diff --git a/examples/client/common.h b/examples/client/common.h index 79e3354a9..21b4a0e44 100644 --- a/examples/client/common.h +++ b/examples/client/common.h @@ -20,6 +20,12 @@ #ifndef WOLFSSH_COMMON_H #define WOLFSSH_COMMON_H + +#include +#ifdef WOLFSSH_WINDOWS_CERT_STORE + #include +#endif + int ClientLoadCA(WOLFSSH_CTX* ctx, const char* caCert); int ClientUsePubKey(const char* pubKeyName, int userEcc, void* heap); int ClientSetPrivateKey(const char* privKeyName, int userEcc, @@ -41,7 +47,7 @@ int ClientSetPrivateKeyFromStore(WOLFSSH_CTX* ctx, /* Supersedes ClientUseCert()/ClientUsePubKey()/ClientSetPrivateKey(), any key * they loaded is released. Call ClientFreeBuffers() before wolfSSH_CTX_free(), * the auth globals alias memory owned by ctx. */ -int ClientSetupCertStoreAuth(WOLFSSH_CTX* ctx); +int ClientSetupCertStoreAuth(WOLFSSH_CTX* ctx, void* heap); #endif /* WOLFSSH_WINDOWS_CERT_STORE */ #endif /* WOLFSSH_COMMON_H */ diff --git a/examples/echoserver/echoserver.c b/examples/echoserver/echoserver.c index 0c76fef9e..0b7445f1a 100644 --- a/examples/echoserver/echoserver.c +++ b/examples/echoserver/echoserver.c @@ -132,7 +132,7 @@ #endif /* Shared by echoserver_test() and the -W pre-scan in wolfSSH_Echoserver(). */ -#define ES_OPTLIST "?1a:d:efEp:R:Ni:j:i:I:J:K:P:k:b:x:m:c:s:G:HW:" +#define ES_OPTLIST "?1a:d:efEp:R:Ni:j:I:J:K:P:k:b:x:m:c:s:G:HW:" #ifndef NO_WOLFSSH_SERVER diff --git a/examples/sftpclient/sftpclient.c b/examples/sftpclient/sftpclient.c index 9c8d7d53c..951015c52 100644 --- a/examples/sftpclient/sftpclient.c +++ b/examples/sftpclient/sftpclient.c @@ -1765,27 +1765,26 @@ THREAD_RETURN WOLFSSH_THREAD sftpclient_test(void* args) err_sys("Couldn't create wolfSSH client context."); } - /* Set private key from cert store */ + /* Set private key from cert store. The names are only needed for + * this call, so release them here and leave the error paths with + * nothing to clean up. */ ret = ClientSetPrivateKeyFromStore(ctx, wStoreName, dwFlags, wSubjectName); + WFREE(wStoreName, heap, DYNTYPE_TEMP); + wStoreName = NULL; + WFREE(wSubjectName, heap, DYNTYPE_TEMP); + wSubjectName = NULL; if (ret != WS_SUCCESS) { - WFREE(wStoreName, heap, DYNTYPE_TEMP); - WFREE(wSubjectName, heap, DYNTYPE_TEMP); err_sys("Error setting private key from certificate store"); } /* Set up auth callback globals (public key type, cert DER) so * that ClientUserAuth presents the certificate for public key * authentication. */ - ret = ClientSetupCertStoreAuth(ctx); + ret = ClientSetupCertStoreAuth(ctx, heap); if (ret != WS_SUCCESS) { - WFREE(wStoreName, heap, DYNTYPE_TEMP); - WFREE(wSubjectName, heap, DYNTYPE_TEMP); err_sys("Error setting up cert store auth"); } - - WFREE(wStoreName, heap, DYNTYPE_TEMP); - WFREE(wSubjectName, heap, DYNTYPE_TEMP); } else #endif /* WOLFSSH_WINDOWS_CERT_STORE */ { @@ -1876,8 +1875,11 @@ THREAD_RETURN WOLFSSH_THREAD sftpclient_test(void* args) ret = wolfSSH_SFTP_connect(ssh); else ret = NonBlockSSH_connect(); - if (ret != WS_SUCCESS) + if (ret != WS_SUCCESS) { + fprintf(stderr, "wolfSSH_SFTP_connect failed: %d, %s\n", ret, + wolfSSH_ErrorToName(ret)); err_sys("Couldn't connect SFTP"); + } { /* get current working directory */ diff --git a/ide/winvs/user_settings.h b/ide/winvs/user_settings.h index d923629bf..b47ca41db 100644 --- a/ide/winvs/user_settings.h +++ b/ide/winvs/user_settings.h @@ -58,6 +58,22 @@ #define WOLFSSH_CERTS #endif +/* Host and user keys held in the MS Certificate Store. Needs WOLFSSH_CERTS + * above, and the projects link crypt32.lib and ncrypt.lib for it. Left + * commented rather than behind an "#if 0" so enabling the X.509 block above + * does not silently pull it in. + * + * #undef WOLFSSH_WINDOWS_CERT_STORE + * #define WOLFSSH_WINDOWS_CERT_STORE + * + * An RSA certificate store key is offered as "x509v3-ssh-rsa", which RFC 6187 + * signs with SHA-1, so that combination also needs + * #define WOLFSSH_NO_SHA1_SOFT_DISABLE + * here and, in wolfSSL's user_settings.h, + * #define WC_SIG_MIN_HASH_TYPE WC_HASH_TYPE_SHA + * ECDSA certificate store keys need neither. + */ + /* default SSHD options */ #if 0 diff --git a/src/certman.c b/src/certman.c index 5f4fbd6a8..d799a4a18 100644 --- a/src/certman.c +++ b/src/certman.c @@ -103,10 +103,6 @@ struct WOLFSSH_CERTMAN { }; -/* wolfSSL_CertManager_up_ref() was added in wolfSSL 4.6.0 */ -#define WOLFSSL_V4_6_0 0x04006000 - - /* used to import an external cert manager, frees and replaces existing manager * returns WS_SUCCESS on success */ @@ -717,9 +713,13 @@ static int CheckProfile(DecodedCert* cert, int profile) #ifdef WOLFSSH_WINDOWS_CERT_STORE /* Parse a cert store spec string "store:subject[:flags]" into wide-string - * components. The subject may not contain ':'. Allocates wStoreName and - * wSubjectName via WMALLOC; caller must WFREE them. On success dwFlags is - * set to the parsed flags value, on failure it is left alone. + * components. The spec is split at the first ':' for the store name and at + * the next one for the flags, so neither the store name nor the subject may + * contain a ':'; a spec with a third ':' is rejected. "My:CN=host:65536" is + * therefore store "My", subject "CN=host", flags 65536, never a two-field + * spec with a ':' in the subject. Allocates wStoreName and wSubjectName via + * WMALLOC; caller must WFREE them. On success dwFlags is set to the parsed + * flags value, on failure it is left alone. * Returns WS_SUCCESS on success. */ int wolfSSH_ParseCertStoreSpec(const char* spec, wchar_t** wStoreName, wchar_t** wSubjectName, @@ -761,12 +761,16 @@ int wolfSSH_ParseCertStoreSpec(const char* spec, if (flagsStr != NULL) { *flagsStr++ = '\0'; if (*flagsStr == '\0') { + WLOG(WS_LOG_CERTMAN, + "Cert store spec has an empty flags field; expected " + "store:subject[:flags]"); WFREE(specCopy, heap, DYNTYPE_TEMP); return WS_BAD_ARGUMENT; } if (WSTRCHR(flagsStr, ':') != NULL) { WLOG(WS_LOG_CERTMAN, - "Cert store subject may not contain a ':'"); + "Cert store spec has too many ':'-separated fields; " + "expected store:subject[:flags]"); WFREE(specCopy, heap, DYNTYPE_TEMP); return WS_BAD_ARGUMENT; } @@ -796,7 +800,9 @@ int wolfSSH_ParseCertStoreSpec(const char* spec, flagsVal = strtoul(flagsStr, &flagsEnd, 0); if (flagsEnd == flagsStr || *flagsEnd != '\0' || errno == ERANGE) { - WLOG(WS_LOG_CERTMAN, "Malformed cert store flags value"); + WLOG(WS_LOG_CERTMAN, "Malformed cert store flags value " + "'%s'; expected store:subject[:flags] with a " + "CERT_SYSTEM_STORE_* name or number", flagsStr); WFREE(specCopy, heap, DYNTYPE_TEMP); return WS_BAD_ARGUMENT; } @@ -812,7 +818,7 @@ int wolfSSH_ParseCertStoreSpec(const char* spec, } } - if (storeName == NULL || subjectName == NULL || *storeName == '\0' || + if (subjectName == NULL || *storeName == '\0' || *subjectName == '\0') { WFREE(specCopy, heap, DYNTYPE_TEMP); return WS_BAD_ARGUMENT; diff --git a/src/internal.c b/src/internal.c index f2527e113..7ea43c3cd 100644 --- a/src/internal.c +++ b/src/internal.c @@ -1078,6 +1078,12 @@ static const char cannedKeyAlgoNames[] = #ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP256 "x509v3-ecdsa-sha2-nistp256," #endif /* WOLFSSH_NO_ECDSA_SHA2_NISTP256 */ + #ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP384 + "x509v3-ecdsa-sha2-nistp384," + #endif /* WOLFSSH_NO_ECDSA_SHA2_NISTP384 */ + #ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP521 + "x509v3-ecdsa-sha2-nistp521," + #endif /* WOLFSSH_NO_ECDSA_SHA2_NISTP521 */ #ifdef WOLFSSH_NO_SHA1_SOFT_DISABLE "x509v3-ssh-rsa," #endif /* WOLFSSH_NO_SHA1_SOFT_DISABLE */ @@ -1157,6 +1163,12 @@ static const char cannedKeyAlgoNamesHostKey[] = #ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP256 "x509v3-ecdsa-sha2-nistp256," #endif /* WOLFSSH_NO_ECDSA_SHA2_NISTP256 */ + #ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP384 + "x509v3-ecdsa-sha2-nistp384," + #endif /* WOLFSSH_NO_ECDSA_SHA2_NISTP384 */ + #ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP521 + "x509v3-ecdsa-sha2-nistp521," + #endif /* WOLFSSH_NO_ECDSA_SHA2_NISTP521 */ #ifdef WOLFSSH_NO_SHA1_SOFT_DISABLE "x509v3-ssh-rsa," #endif /* WOLFSSH_NO_SHA1_SOFT_DISABLE */ @@ -1286,14 +1298,6 @@ static void ClearCertStoreKey(WOLFSSH_CTX* ctx, WOLFSSH_PVT_KEY* pvtKey) CertFreeCertificateContext((PCCERT_CONTEXT)pvtKey->certStoreContext); pvtKey->certStoreContext = NULL; } - if (pvtKey->storeName != NULL) { - WFREE(pvtKey->storeName, ctx->heap, DYNTYPE_STRING); - pvtKey->storeName = NULL; - } - if (pvtKey->subjectName != NULL) { - WFREE(pvtKey->subjectName, ctx->heap, DYNTYPE_STRING); - pvtKey->subjectName = NULL; - } #ifdef WOLFSSH_CERTS if (pvtKey->cert != NULL) { WFREE(pvtKey->cert, ctx->heap, DYNTYPE_CERT); @@ -2250,6 +2254,21 @@ WOLFSSH_LOCAL void RefreshPublicKeyAlgo(WOLFSSH_CTX* ctx) continue; } #endif + /* A slot with no signing source at all cannot answer a KEXDH_INIT. + * This happens when a file HostCertificate lands on a slot whose + * paired key is TPM or cert-store backed, leaving a certificate with + * no key behind it. Advertising it would abort the handshake instead + * of falling back to an algorithm that does work. */ + if (key->key == NULL + #ifdef WOLFSSH_TPM + && !key->isTpm + #endif + #ifdef WOLFSSH_WINDOWS_CERT_STORE + && !IsCertStoreKey(key) + #endif + ) { + continue; + } if (key->publicKeyFmt == ID_SSH_RSA) { #ifndef WOLFSSH_NO_RSA_SHA2_512 if (publicKeyAlgoCount < WOLFSSH_MAX_PUB_KEY_ALGO) { @@ -2467,8 +2486,24 @@ static int SetHostCertificate(WOLFSSH_CTX* ctx, } if (destIdx >= WOLFSSH_MAX_PVT_KEYS) { + /* der not taken on this path; free it to avoid a leak */ + WFREE(der, ctx->heap, dynamicType); ret = WS_CTX_KEY_COUNT_E; } + #ifdef WOLFSSH_WINDOWS_CERT_STORE + /* A file certificate cannot be paired with a cert-store host key: the + * store slot holds no software key to copy onto the certificate slot, + * and clearing the store state below would tear down the only signing + * source this algorithm has. Report the misconfiguration instead. */ + else if (IsCertStoreKey(ctx->privateKey + destIdx) + || (HINTISSET(keyIdx) && IsCertStoreKey(ctx->privateKey + keyIdx))) { + WLOG(WS_LOG_ERROR, "SetHostCertificate: The host key for this " + "algorithm comes from the certificate store, which supplies its " + "own certificate; do not also load a host certificate file"); + WFREE(der, ctx->heap, dynamicType); + ret = WS_BAD_ARGUMENT; + } + #endif else { WOLFSSH_PVT_KEY* pvtKey = ctx->privateKey + destIdx; @@ -14636,7 +14671,7 @@ static int ExtractPubKeyDerFromCert(const byte* certDer, word32 certDerSz, ret = wc_ParseCert(dCert, CERT_TYPE, 0, NULL); if (ret == 0) { ret = wc_GetPubKeyDerFromCert(dCert, NULL, &pubKeyDerSz); - if (ret == LENGTH_ONLY_E) { + if (ret == WC_NO_ERR_TRACE(LENGTH_ONLY_E)) { ret = 0; pubKeyDer = (byte*)WMALLOC(pubKeyDerSz, heap, DYNTYPE_PUBKEY); if (pubKeyDer == NULL) @@ -14822,11 +14857,14 @@ static int SignWithCertStoreKey(WOLFSSH* ssh, WOLFSSH_UNUSED(ssh); WOLFSSH_UNUSED(hashId); - if (pvtKey == NULL || !pvtKey->useCertStore || - pvtKey->certStoreContext == NULL) { + if (!IsCertStoreKey(pvtKey)) { WLOG(WS_LOG_DEBUG, "SignWithCertStoreKey: Not a cert store key"); return WS_BAD_ARGUMENT; } + if (sig == NULL || sigSz == NULL || *sigSz == 0) { + WLOG(WS_LOG_DEBUG, "SignWithCertStoreKey: Bad signature buffer"); + return WS_BAD_ARGUMENT; + } pCertContext = (PCCERT_CONTEXT)pvtKey->certStoreContext; @@ -14965,7 +15003,9 @@ static int SignHRsa(WOLFSSH* ssh, byte* sig, word32* sigSz, ret = wolfTPM2_SignHashScheme(ssh->ctx->tpmDev, ssh->ctx->tpmKey, digest, (int)digestSz, sig, (int*)sigSz, TPM_ALG_RSASSA, TPM2_GetTpmHashType(hashId)); - if (ret == 0) { + /* The self-check below is skipped for the TPM, so a zero-length + * signature would otherwise be emitted in the KEXDH_REPLY. */ + if (ret == 0 && *sigSz > 0) { ret = WS_SUCCESS; } else { @@ -14981,6 +15021,11 @@ static int SignHRsa(WOLFSSH* ssh, byte* sig, word32* sigSz, /* Use cert store signing abstraction */ ret = SignWithCertStoreKey(ssh, sigKey->pvtKey, encSig, encSigSz, hashId, sig, sigSz); + if (ret == WS_SUCCESS && *sigSz == 0) { + WLOG(WS_LOG_DEBUG, "SignHRsa: Cert store sign gave no " + "signature"); + ret = WS_RSA_E; + } if (ret != WS_SUCCESS) { WLOG(WS_LOG_DEBUG, "SignHRsa: Cert store sign failed"); } diff --git a/src/ssh.c b/src/ssh.c index 7b935e029..b8224c0eb 100644 --- a/src/ssh.c +++ b/src/ssh.c @@ -2855,34 +2855,72 @@ int wolfSSH_CTX_AddRootCert_buffer(WOLFSSH_CTX* ctx, } #ifdef WOLFSSH_WINDOWS_CERT_STORE +/* Returns 1 when the certificate's private key can actually be used for + * signing. CERT_KEY_PROV_INFO_PROP_ID is not enough: it is also set for + * legacy CryptoAPI/CSP keys, which CRYPT_ACQUIRE_ONLY_NCRYPT_KEY_FLAG + * rejects. Use the same acquisition the signing path performs so a + * candidate that cannot sign is not chosen. */ +static int CertKeyCanSign(PCCERT_CONTEXT pCertContext) +{ + HCRYPTPROV_OR_NCRYPT_KEY_HANDLE hKey = 0; + DWORD dwKeySpec = 0; + BOOL fCallerFree = FALSE; + + if (!CryptAcquireCertificatePrivateKey(pCertContext, + CRYPT_ACQUIRE_ONLY_NCRYPT_KEY_FLAG | CRYPT_ACQUIRE_SILENT_FLAG, + NULL, &hKey, &dwKeySpec, &fCallerFree)) { + return 0; + } + + if (fCallerFree) { + if (dwKeySpec == CERT_NCRYPT_KEY_SPEC) { + NCryptFreeObject(hKey); + } + else { + CryptReleaseContext(hKey, 0); + } + } + + return 1; +} + + /* Find the certificate in hStore whose Common Name matches subjectName. * subjectName may include a leading "CN=" prefix. * CERT_FIND_SUBJECT_STR_W is only used as a substring pre-filter to * enumerate candidates; each candidate's CN is then compared in full so * that a lookup for "server1" does not select "server1.example" or * "myserver1". The compare is case insensitive, matching both the - * pre-filter and X.500 name semantics. A candidate that is currently - * time-valid and has a private key is preferred, so that neither a - * renewal's leftover certificate nor a public-only duplicate ends the - * search. The selected certificate is stored in out, and is NULL when no - * match exists. The caller frees it with CertFreeCertificateContext. + * pre-filter and X.500 name semantics. Candidates are ranked by how + * usable they are: time-valid with a usable key, then any candidate with + * a usable key, then time-valid without one, then the rest. A key that + * can sign outranks time validity, because a certificate with no usable + * key can never produce a signature, so neither a renewal's leftover + * certificate nor a public-only duplicate ends the search. The selected + * certificate is stored in out, and is NULL when no match exists. The + * caller frees it with CertFreeCertificateContext. * Returns WS_SUCCESS on success. */ static int FindCertByExactCN(void* heap, HCERTSTORE hStore, const wchar_t* subjectName, PCCERT_CONTEXT* out) { PCCERT_CONTEXT pCertContext; + PCCERT_CONTEXT keyedMatch; PCCERT_CONTEXT validMatch; PCCERT_CONTEXT expiredMatch; const wchar_t* cn; wchar_t* certCn; DWORD certCnSz; - DWORD propSz; int match; int hasKey; + int timeValidity; + int keyedEarly; + int expiredEarly; int ret; *out = NULL; ret = WS_SUCCESS; + keyedEarly = 0; + expiredEarly = 0; /* Strip an optional "CN=" prefix from the requested name. */ cn = subjectName; @@ -2895,6 +2933,7 @@ static int FindCertByExactCN(void* heap, HCERTSTORE hStore, } pCertContext = NULL; + keyedMatch = NULL; validMatch = NULL; expiredMatch = NULL; for (;;) { @@ -2926,39 +2965,74 @@ static int FindCertByExactCN(void* heap, HCERTSTORE hStore, continue; } - /* A duplicate that cannot sign must not end the search. */ - propSz = 0; - hasKey = CertGetCertificateContextProperty(pCertContext, - CERT_KEY_PROV_INFO_PROP_ID, NULL, &propSz); - if (CertVerifyTimeValidity(NULL, pCertContext->pCertInfo) == 0) { - if (hasKey) { - break; + /* A duplicate that cannot sign must not end the search. + * CertVerifyTimeValidity returns -1 before the validity period and + * +1 after it. */ + hasKey = CertKeyCanSign(pCertContext); + timeValidity = CertVerifyTimeValidity(NULL, pCertContext->pCertInfo); + if (hasKey && timeValidity == 0) { + break; + } + + if (hasKey) { + if (keyedMatch == NULL) { + keyedMatch = CertDuplicateCertificateContext(pCertContext); + keyedEarly = (timeValidity < 0); + if (keyedMatch == NULL) { + ret = WS_MEMORY_E; + } } + } + else if (timeValidity == 0) { if (validMatch == NULL) { validMatch = CertDuplicateCertificateContext(pCertContext); + if (validMatch == NULL) { + ret = WS_MEMORY_E; + } } } else if (expiredMatch == NULL) { expiredMatch = CertDuplicateCertificateContext(pCertContext); + expiredEarly = (timeValidity < 0); + if (expiredMatch == NULL) { + ret = WS_MEMORY_E; + } + } + + if (ret != WS_SUCCESS) { + CertFreeCertificateContext(pCertContext); + pCertContext = NULL; + break; } } /* An allocation failure is reported as such rather than falling back * to a candidate the enumeration had already rejected. */ if (ret == WS_SUCCESS && pCertContext == NULL) { - if (validMatch != NULL) { - WLOG(WS_LOG_WARN, "FindCertByExactCN: No match with a private " - "key, using '%ls' anyway", subjectName); + if (keyedMatch != NULL) { + WLOG(WS_LOG_WARN, "FindCertByExactCN: No time-valid match, using " + "a %s '%ls' that has a private key", + keyedEarly ? "not yet valid" : "expired", subjectName); + pCertContext = keyedMatch; + keyedMatch = NULL; + } + else if (validMatch != NULL) { + WLOG(WS_LOG_WARN, "FindCertByExactCN: No match with a usable " + "private key, using '%ls' anyway", subjectName); pCertContext = validMatch; validMatch = NULL; } else if (expiredMatch != NULL) { - WLOG(WS_LOG_WARN, "FindCertByExactCN: No time-valid match, " - "using an expired '%ls'", subjectName); + WLOG(WS_LOG_WARN, "FindCertByExactCN: No time-valid match and " + "none with a usable private key, using a %s '%ls'", + expiredEarly ? "not yet valid" : "expired", subjectName); pCertContext = expiredMatch; expiredMatch = NULL; } } + if (keyedMatch != NULL) { + CertFreeCertificateContext(keyedMatch); + } if (validMatch != NULL) { CertFreeCertificateContext(validMatch); } @@ -2982,8 +3056,6 @@ static int FindCertByExactCN(void* heap, HCERTSTORE hStore, * type and the matching X.509 type is all or nothing. */ typedef struct CertStoreSlot { PCCERT_CONTEXT context; - wchar_t* storeName; - wchar_t* subjectName; byte* cert; word32 certSz; word32 keyIdx; @@ -3015,12 +3087,6 @@ static void FreeCertStoreSlot(void* heap, CertStoreSlot* slot) if (slot->context != NULL) { CertFreeCertificateContext(slot->context); } - if (slot->storeName != NULL) { - WFREE(slot->storeName, heap, DYNTYPE_STRING); - } - if (slot->subjectName != NULL) { - WFREE(slot->subjectName, heap, DYNTYPE_STRING); - } if (slot->cert != NULL) { WFREE(slot->cert, heap, DYNTYPE_CERT); } @@ -3030,37 +3096,32 @@ static void FreeCertStoreSlot(void* heap, CertStoreSlot* slot) /* Allocate the resources slot keyIdx needs, without modifying the * context. The slot takes its own reference on pCertContext and its own - * copies of the name strings and certificate DER so that every slot can - * be freed independently by CtxResourceFree. + * copy of the certificate DER so that every slot can be freed + * independently by CtxResourceFree. * Returns WS_SUCCESS on success. */ static int PrepCertStoreSlot(void* heap, byte keyId, word32 keyIdx, - PCCERT_CONTEXT pCertContext, const wchar_t* storeName, - const wchar_t* subjectName, CertStoreSlot* slot) + PCCERT_CONTEXT pCertContext, CertStoreSlot* slot) { - size_t storeNameLen; - size_t subjectNameLen; + /* A zero-length certificate would be committed to the slot and later + * advertised as an x509v3 host key with an empty K_S. */ + if (pCertContext->pbCertEncoded == NULL + || pCertContext->cbCertEncoded == 0) { + WLOG(WS_LOG_ERROR, "PrepCertStoreSlot: Store certificate is empty"); + return WS_BAD_ARGUMENT; + } WMEMSET(slot, 0, sizeof(*slot)); slot->keyId = keyId; slot->keyIdx = keyIdx; slot->certSz = pCertContext->cbCertEncoded; - storeNameLen = wcslen(storeName) + 1; - subjectNameLen = wcslen(subjectName) + 1; - slot->storeName = (wchar_t*)WMALLOC(storeNameLen * sizeof(wchar_t), - heap, DYNTYPE_STRING); - slot->subjectName = (wchar_t*)WMALLOC(subjectNameLen * sizeof(wchar_t), - heap, DYNTYPE_STRING); slot->cert = (byte*)WMALLOC(slot->certSz, heap, DYNTYPE_CERT); slot->context = CertDuplicateCertificateContext(pCertContext); - if (slot->storeName == NULL || slot->subjectName == NULL - || slot->cert == NULL || slot->context == NULL) { + if (slot->cert == NULL || slot->context == NULL) { FreeCertStoreSlot(heap, slot); WLOG(WS_LOG_ERROR, "PrepCertStoreSlot: Memory allocation failed"); return WS_MEMORY_E; } - WMEMCPY(slot->storeName, storeName, storeNameLen * sizeof(wchar_t)); - WMEMCPY(slot->subjectName, subjectName, subjectNameLen * sizeof(wchar_t)); WMEMCPY(slot->cert, pCertContext->pbCertEncoded, slot->certSz); return WS_SUCCESS; @@ -3070,25 +3131,24 @@ static int PrepCertStoreSlot(void* heap, byte keyId, word32 keyIdx, /* Move the prepared resources into the context. The slot may previously * have held either a cert-store key or a file-based key/cert, so clear * both kinds of resources. Cannot fail. */ -static void CommitCertStoreSlot(WOLFSSH_CTX* ctx, CertStoreSlot* slot, - word32 dwFlags) +static void CommitCertStoreSlot(WOLFSSH_CTX* ctx, CertStoreSlot* slot) { WOLFSSH_PVT_KEY* pvtKey; void* heap; heap = ctx->heap; + if (slot->keyIdx >= WOLFSSH_MAX_PVT_KEYS) { + /* Unreachable: the caller bounds checks before preparing a slot. */ + WLOG(WS_LOG_ERROR, "CommitCertStoreSlot: Slot index out of range"); + FreeCertStoreSlot(heap, slot); + return; + } pvtKey = &ctx->privateKey[slot->keyIdx]; if (pvtKey->certStoreContext != NULL) { CertFreeCertificateContext( (PCCERT_CONTEXT)pvtKey->certStoreContext); } - if (pvtKey->storeName != NULL) { - WFREE(pvtKey->storeName, heap, DYNTYPE_STRING); - } - if (pvtKey->subjectName != NULL) { - WFREE(pvtKey->subjectName, heap, DYNTYPE_STRING); - } if (pvtKey->key != NULL) { WS_FORCEZERO(pvtKey->key, pvtKey->keySz); WFREE(pvtKey->key, heap, DYNTYPE_PRIVKEY); @@ -3106,9 +3166,6 @@ static void CommitCertStoreSlot(WOLFSSH_CTX* ctx, CertStoreSlot* slot, #endif pvtKey->useCertStore = 1; pvtKey->certStoreContext = (void*)slot->context; - pvtKey->storeName = slot->storeName; - pvtKey->subjectName = slot->subjectName; - pvtKey->dwFlags = dwFlags; pvtKey->cert = slot->cert; pvtKey->certSz = slot->certSz; @@ -3117,6 +3174,27 @@ static void CommitCertStoreSlot(WOLFSSH_CTX* ctx, CertStoreSlot* slot, } +#ifndef WOLFSSH_NO_ECDSA +/* DER-encoded named-curve OIDs as they appear in a certificate's + * SubjectPublicKeyInfo algorithm parameters. */ +#ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP256 +static const byte certStoreOidP256[] = { + 0x06, 0x08, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07 +}; +#endif +#ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP384 +static const byte certStoreOidP384[] = { + 0x06, 0x05, 0x2B, 0x81, 0x04, 0x00, 0x22 +}; +#endif +#ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP521 +static const byte certStoreOidP521[] = { + 0x06, 0x05, 0x2B, 0x81, 0x04, 0x00, 0x23 +}; +#endif +#endif /* WOLFSSH_NO_ECDSA */ + + /* Load a private key from MS Certificate Store * storeName: Certificate store name (e.g., L"My", L"Root") * dwFlags: Certificate store location, and only a location (e.g. @@ -3149,6 +3227,14 @@ int wolfSSH_CTX_UsePrivateKey_fromStore(WOLFSSH_CTX* ctx, word32 keyIdx; word32 certIdx; word32 newCount; + byte haveCertSlot; + HCRYPTPROV_OR_NCRYPT_KEY_HANDLE hKey = 0; + DWORD dwKeySpec = 0; + BOOL fCallerFree = FALSE; +#ifndef WOLFSSH_NO_ECDSA + const byte* params = NULL; + DWORD paramsSz = 0; +#endif WLOG(WS_LOG_DEBUG, "Entering wolfSSH_CTX_UsePrivateKey_fromStore()"); @@ -3203,53 +3289,64 @@ int wolfSSH_CTX_UsePrivateKey_fromStore(WOLFSSH_CTX* ctx, if (pPubKeyInfo->Algorithm.pszObjId != NULL) { /* Compare OID strings (they are ASCII, not wide) */ if (strcmp(pPubKeyInfo->Algorithm.pszObjId, szOID_RSA_RSA) == 0) { - #ifndef WOLFSSH_NO_RSA + /* An RSA slot is useless without an RSA signature algorithm to + * negotiate, so require one the way wolfSSH_CTX_UseTpmHostKey does + * rather than consuming a slot RefreshPublicKeyAlgo will not + * advertise. */ + #if !defined(WOLFSSH_NO_RSA) && \ + (!defined(WOLFSSH_NO_RSA_SHA2_256) || \ + !defined(WOLFSSH_NO_RSA_SHA2_512) || \ + (defined(WOLFSSH_NO_SHA1_SOFT_DISABLE) && \ + !defined(WOLFSSH_NO_SSH_RSA_SHA1))) keyId = ID_SSH_RSA; #else WLOG(WS_LOG_ERROR, "wolfSSH_CTX_UsePrivateKey_fromStore: " - "RSA is not compiled in"); + "No usable RSA signature algorithm is compiled in"); #endif } - else if (strcmp(pPubKeyInfo->Algorithm.pszObjId, szOID_ECC_PUBLIC_KEY) == 0) { + else if (strcmp(pPubKeyInfo->Algorithm.pszObjId, + szOID_ECC_PUBLIC_KEY) == 0) { + #ifndef WOLFSSH_NO_ECDSA /* The algorithm parameters hold the DER-encoded named-curve * OID; match its raw bytes to select the ECDSA key type. */ - static const byte oidP256[] = { - 0x06, 0x08, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07 - }; - static const byte oidP384[] = { - 0x06, 0x05, 0x2B, 0x81, 0x04, 0x00, 0x22 - }; - static const byte oidP521[] = { - 0x06, 0x05, 0x2B, 0x81, 0x04, 0x00, 0x23 - }; - const byte* params = pPubKeyInfo->Algorithm.Parameters.pbData; - DWORD paramsSz = pPubKeyInfo->Algorithm.Parameters.cbData; + params = pPubKeyInfo->Algorithm.Parameters.pbData; + paramsSz = pPubKeyInfo->Algorithm.Parameters.cbData; if (params == NULL) { paramsSz = 0; } #ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP256 - if (paramsSz == sizeof(oidP256) && - WMEMCMP(params, oidP256, sizeof(oidP256)) == 0) { + if (paramsSz == sizeof(certStoreOidP256) && + WMEMCMP(params, certStoreOidP256, + sizeof(certStoreOidP256)) == 0) { keyId = ID_ECDSA_SHA2_NISTP256; } + else #endif #ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP384 - if (paramsSz == sizeof(oidP384) && - WMEMCMP(params, oidP384, sizeof(oidP384)) == 0) { + if (paramsSz == sizeof(certStoreOidP384) && + WMEMCMP(params, certStoreOidP384, + sizeof(certStoreOidP384)) == 0) { keyId = ID_ECDSA_SHA2_NISTP384; } + else #endif #ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP521 - if (paramsSz == sizeof(oidP521) && - WMEMCMP(params, oidP521, sizeof(oidP521)) == 0) { + if (paramsSz == sizeof(certStoreOidP521) && + WMEMCMP(params, certStoreOidP521, + sizeof(certStoreOidP521)) == 0) { keyId = ID_ECDSA_SHA2_NISTP521; } + else #endif - if (keyId == ID_NONE) { + { WLOG(WS_LOG_ERROR, "wolfSSH_CTX_UsePrivateKey_fromStore: " "Unsupported ECC curve parameters"); } + #else + WLOG(WS_LOG_ERROR, "wolfSSH_CTX_UsePrivateKey_fromStore: " + "ECDSA is not compiled in"); + #endif /* WOLFSSH_NO_ECDSA */ } else { WLOG(WS_LOG_ERROR, "wolfSSH_CTX_UsePrivateKey_fromStore: " @@ -3271,39 +3368,33 @@ int wolfSSH_CTX_UsePrivateKey_fromStore(WOLFSSH_CTX* ctx, /* Verify private key is accessible before registering the key. * This catches permission issues early (e.g., LocalSystem service * cannot access the private key) rather than failing later during - * SSH handshake signing. */ - { - HCRYPTPROV_OR_NCRYPT_KEY_HANDLE hKey = 0; - DWORD dwKeySpec = 0; - BOOL fCallerFree = FALSE; - - /* Require a CNG/NCRYPT key. Legacy CryptoAPI/CSP keys are not - * supported; targets are Windows 10 and newer. */ - if (!CryptAcquireCertificatePrivateKey(pCertContext, - CRYPT_ACQUIRE_ONLY_NCRYPT_KEY_FLAG | CRYPT_ACQUIRE_SILENT_FLAG, - NULL, &hKey, &dwKeySpec, &fCallerFree)) { - WLOG(WS_LOG_ERROR, "wolfSSH_CTX_UsePrivateKey_fromStore: Cannot " - "access private key, error: %lu. Check that the current user " - "or service account has permission to access the key.", - (unsigned long)GetLastError()); - CertFreeCertificateContext(pCertContext); - CertCloseStore(hStore, 0); - return WS_CRYPTO_FAILED; + * SSH handshake signing. + * Require a CNG/NCRYPT key. Legacy CryptoAPI/CSP keys are not + * supported; targets are Windows 10 and newer. */ + if (!CryptAcquireCertificatePrivateKey(pCertContext, + CRYPT_ACQUIRE_ONLY_NCRYPT_KEY_FLAG | CRYPT_ACQUIRE_SILENT_FLAG, + NULL, &hKey, &dwKeySpec, &fCallerFree)) { + WLOG(WS_LOG_ERROR, "wolfSSH_CTX_UsePrivateKey_fromStore: Cannot " + "access private key, error: %lu. Check that the current user " + "or service account has permission to access the key.", + (unsigned long)GetLastError()); + CertFreeCertificateContext(pCertContext); + CertCloseStore(hStore, 0); + return WS_CRYPTO_FAILED; + } + /* Release the key handle since we just needed to verify access. + * CRYPT_ACQUIRE_ONLY_NCRYPT_KEY_FLAG makes the CNG case the only + * reachable one; the CSP release is kept for the flags changing. */ + if (fCallerFree) { + if (dwKeySpec == CERT_NCRYPT_KEY_SPEC) { + NCryptFreeObject(hKey); } - /* Release the key handle since we just needed to verify access. - * CRYPT_ACQUIRE_ONLY_NCRYPT_KEY_FLAG makes the CNG case the only - * reachable one; the CSP release is kept for the flags changing. */ - if (fCallerFree) { - if (dwKeySpec == CERT_NCRYPT_KEY_SPEC) { - NCryptFreeObject(hKey); - } - else { - CryptReleaseContext(hKey, 0); - } + else { + CryptReleaseContext(hKey, 0); } - WLOG(WS_LOG_DEBUG, "wolfSSH_CTX_UsePrivateKey_fromStore: Private key " - "access verified successfully"); } + WLOG(WS_LOG_DEBUG, "wolfSSH_CTX_UsePrivateKey_fromStore: Private key " + "access verified successfully"); /* Register the key under its plain type so peers without RFC6187 * support get a raw public key, and under the matching X.509 type so @@ -3320,14 +3411,18 @@ int wolfSSH_CTX_UsePrivateKey_fromStore(WOLFSSH_CTX* ctx, } /* CertTypeForId returns keyId unchanged when the build has no X509 - * equivalent; skip the X509 ID slot in that case. */ + * equivalent; skip the X509 ID slot in that case. haveCertSlot rather + * than a certIdx sentinel, so the "skip" marker cannot be confused with + * an index a full table legitimately computes. */ certId = CertTypeForId(keyId); - certIdx = WOLFSSH_MAX_PVT_KEYS; + certIdx = 0; + haveCertSlot = 0; if (certId != keyId) { certIdx = FindKeySlot(ctx, certId); if (certIdx == WOLFSSH_MAX_PVT_KEYS) { certIdx = newCount++; } + haveCertSlot = 1; } else { WLOG(WS_LOG_INFO, "wolfSSH_CTX_UsePrivateKey_fromStore: No x509v3 " @@ -3342,17 +3437,17 @@ int wolfSSH_CTX_UsePrivateKey_fromStore(WOLFSSH_CTX* ctx, } if (ret == WS_SUCCESS) { ret = PrepCertStoreSlot(ctx->heap, keyId, keyIdx, pCertContext, - storeName, subjectName, &keySlot); + &keySlot); } - if (ret == WS_SUCCESS && certIdx != WOLFSSH_MAX_PVT_KEYS) { + if (ret == WS_SUCCESS && haveCertSlot) { ret = PrepCertStoreSlot(ctx->heap, certId, certIdx, pCertContext, - storeName, subjectName, &certSlot); + &certSlot); } if (ret == WS_SUCCESS) { - CommitCertStoreSlot(ctx, &keySlot, dwFlags); - if (certIdx != WOLFSSH_MAX_PVT_KEYS) { - CommitCertStoreSlot(ctx, &certSlot, dwFlags); + CommitCertStoreSlot(ctx, &keySlot); + if (haveCertSlot) { + CommitCertStoreSlot(ctx, &certSlot); } ctx->privateKeyCount = newCount; } diff --git a/tests/unit.c b/tests/unit.c index ebad3f8e5..ab44341a1 100644 --- a/tests/unit.c +++ b/tests/unit.c @@ -10023,7 +10023,14 @@ static int test_CertMan_PromoteValidCaIntermediate(void) #endif /* WOLFSSH_TEST_CERTMAN_PROMOTE */ -#ifdef WOLFSSH_CERTS +/* wolfSSH_SetCertManager() is an unconditional WS_NOT_COMPILED stub before + * wolfSSL 4.6.0, which is where wolfSSL_CertManager_up_ref() landed. Skip the + * test there rather than failing on behaviour that is by design. */ +#if defined(WOLFSSH_CERTS) && (LIBWOLFSSL_VERSION_HEX >= WOLFSSL_V4_6_0) + #define WOLFSSH_TEST_SET_CERTMAN +#endif + +#ifdef WOLFSSH_TEST_SET_CERTMAN /* wolfSSH_SetCertManager imports a WOLFSSL_CERT_MANAGER by reference into * the wolfSSH context. Test argument checking, importing the same manager * twice, replacing an already-imported manager, and the reference counting @@ -10041,11 +10048,16 @@ static int test_SetCertManager(void) #ifdef WOLFSSH_TEST_CERTMAN_ROOTCA byte* root = NULL; word32 rootSz = 0; + /* ./keys only resolves when run from the source root, and + * keys/ca-cert-ecc.der is not linked into an out-of-tree build tree. The + * argument and reference-count checks below need no file, so treat a + * missing cert as a skip of the root-CA half rather than a failure. */ + int haveRoot = (certmanLoadFile("./keys/ca-cert-ecc.der", &root, &rootSz) + == 0); - /* run from the source root so ./keys resolves */ - if (certmanLoadFile("./keys/ca-cert-ecc.der", &root, &rootSz) != 0) { - printf("SetCertManager: can't load root cert\n"); - result = -1; + if (!haveRoot) { + printf("SetCertManager: skipping root cert checks, " + "./keys/ca-cert-ecc.der not readable\n"); } #endif @@ -10080,7 +10092,8 @@ static int test_SetCertManager(void) cm = NULL; } #ifdef WOLFSSH_TEST_CERTMAN_ROOTCA - if (result == 0 && wolfSSH_CTX_AddRootCert_buffer(ctx, root, rootSz, + if (result == 0 && haveRoot && + wolfSSH_CTX_AddRootCert_buffer(ctx, root, rootSz, WOLFSSH_FORMAT_ASN1) != WS_SUCCESS) result = -8; #endif @@ -10094,7 +10107,8 @@ static int test_SetCertManager(void) result = -10; } #ifdef WOLFSSH_TEST_CERTMAN_ROOTCA - if (result == 0 && wolfSSH_CTX_AddRootCert_buffer(ctx, root, rootSz, + if (result == 0 && haveRoot && + wolfSSH_CTX_AddRootCert_buffer(ctx, root, rootSz, WOLFSSH_FORMAT_ASN1) != WS_SUCCESS) result = -11; #endif @@ -10103,7 +10117,7 @@ static int test_SetCertManager(void) if (ctx != NULL) wolfSSH_CTX_free(ctx); #ifdef WOLFSSH_TEST_CERTMAN_ROOTCA - if (result == 0 && cm2 != NULL && + if (result == 0 && haveRoot && cm2 != NULL && wolfSSL_CertManagerLoadCABuffer(cm2, root, rootSz, WOLFSSL_FILETYPE_ASN1) != WOLFSSL_SUCCESS) result = -12; @@ -10120,7 +10134,7 @@ static int test_SetCertManager(void) return result; } -#endif /* WOLFSSH_CERTS */ +#endif /* WOLFSSH_TEST_SET_CERTMAN */ #ifdef WOLFSSH_WINDOWS_CERT_STORE /* Check one wolfSSH_ParseCertStoreSpec call against expected results. @@ -13602,7 +13616,7 @@ int wolfSSH_UnitTest(int argc, char** argv) #endif -#ifdef WOLFSSH_CERTS +#ifdef WOLFSSH_TEST_SET_CERTMAN unitResult = test_SetCertManager(); printf("SetCertManager: %s\n", (unitResult == 0 ? "SUCCESS" : "FAILED")); testResult = testResult || unitResult; diff --git a/wolfssh/certman.h b/wolfssh/certman.h index 54a2b4007..771358452 100644 --- a/wolfssh/certman.h +++ b/wolfssh/certman.h @@ -46,7 +46,11 @@ typedef struct WOLFSSH_CERTMAN WOLFSSH_CERTMAN; #ifdef WOLFSSH_CERTS /* Replaces the CTX's cert manager with cm, taking a reference on it and - * applying wolfSSH's revocation policy. */ + * applying wolfSSH's revocation policy. The caller retains ownership, but + * note the policy is applied to the shared object: in an HAVE_OCSP build + * this enables WOLFSSL_OCSP_CHECKALL on cm, so a caller that keeps using + * the same manager for TLS will find every chain requiring an OCSP + * response. */ WOLFSSH_API int wolfSSH_SetCertManager(WOLFSSH_CTX* ctx, WOLFSSL_CERT_MANAGER* cm); #endif /* WOLFSSH_CERTS */ @@ -68,8 +72,10 @@ int wolfSSH_CERTMAN_VerifyCerts_buffer(WOLFSSH_CERTMAN* cm, #if defined(WOLFSSH_CERTS) && defined(WOLFSSH_WINDOWS_CERT_STORE) /* Splits "store:subject[:flags]", where flags is CURRENT_USER, - * LOCAL_MACHINE, or a decimal or 0x hex CERT_SYSTEM_STORE_* location, and - * defaults to CURRENT_USER. The subject may not contain a ':'. */ + * LOCAL_MACHINE, USERS, or a decimal or 0x hex CERT_SYSTEM_STORE_* location, + * and defaults to CURRENT_USER. The spec is split at the first two ':', so + * neither the store name nor the subject may contain one and a third ':' is + * rejected. */ WOLFSSH_API int wolfSSH_ParseCertStoreSpec(const char* spec, wchar_t** wStoreName, wchar_t** wSubjectName, diff --git a/wolfssh/internal.h b/wolfssh/internal.h index 5576bb1f3..34b5c0c71 100644 --- a/wolfssh/internal.h +++ b/wolfssh/internal.h @@ -117,6 +117,10 @@ extern "C" { #define WOLFSSH_NO_DH #endif +#ifndef WOLFSSL_V4_6_0 + /* wolfSSL_CertManager_up_ref() was added in wolfSSL 4.6.0 */ + #define WOLFSSL_V4_6_0 0x04006000 +#endif #define WOLFSSL_V5_0_0 0x05000000 #define WOLFSSL_V5_7_0 0x05007000 #define WOLFSSL_V5_7_2 0x05007002 @@ -755,19 +759,11 @@ typedef struct WOLFSSH_PVT_KEY { * unused; signing and the public K_S come from ctx->tpmKey. */ #endif #ifdef WOLFSSH_WINDOWS_CERT_STORE - byte useCertStore:1; + byte useCertStore; /* Flag indicating if this key is from MS Certificate Store. */ void* certStoreContext; /* Windows certificate context (PCCERT_CONTEXT) for MS Certificate Store. * Owned by CTX, must be freed with CertFreeCertificateContext. */ - wchar_t* storeName; - /* Certificate store name (e.g., "My", "Root"). Owned by CTX. */ - wchar_t* subjectName; - /* Certificate subject name for lookup. Owned by CTX. */ - word32 dwFlags; - /* Certificate store flags (e.g., CERT_SYSTEM_STORE_CURRENT_USER). - * Kept as word32 so this header does not depend on Windows - * typedefs; converted to DWORD at the CertOpenStore call. */ #endif /* WOLFSSH_WINDOWS_CERT_STORE */ } WOLFSSH_PVT_KEY; From 97482a26ecfc713f8deb9a7ef3b3f9163e1350ad Mon Sep 17 00:00:00 2001 From: JacobBarthelmeh Date: Wed, 5 Aug 2026 10:53:31 -0600 Subject: [PATCH 10/10] expanding test cases and minor refactors --- .github/workflows/windows-cert-store-test.yml | 114 +++++++-- apps/wolfsshd/auth.c | 54 ++-- apps/wolfsshd/auth.h | 1 + apps/wolfsshd/configuration.c | 44 ++++ apps/wolfsshd/configuration.h | 3 + apps/wolfsshd/test/test_configuration.c | 24 +- apps/wolfsshd/wolfsshd.c | 202 ++++++++++++--- configure.ac | 6 +- examples/client/common.c | 44 ++-- examples/client/common.h | 4 +- examples/echoserver/echoserver.c | 27 +- examples/scpclient/scpclient.c | 2 +- examples/sftpclient/sftpclient.c | 23 +- src/certman.c | 62 ++++- src/internal.c | 242 ++++++++++++------ src/ssh.c | 89 +++---- tests/unit.c | 51 ++-- wolfssh/certman.h | 20 +- wolfssh/internal.h | 14 +- wolfssh/ssh.h | 11 +- wolfssh/test.h | 6 +- 21 files changed, 737 insertions(+), 306 deletions(-) diff --git a/.github/workflows/windows-cert-store-test.yml b/.github/workflows/windows-cert-store-test.yml index ac87a10be..ea109a8bb 100644 --- a/.github/workflows/windows-cert-store-test.yml +++ b/.github/workflows/windows-cert-store-test.yml @@ -59,8 +59,10 @@ jobs: # Enable SSHD, SFTP, and X509 support (including WOLFSSH_NO_FPKI) sed -i 's/#if 0/#if 1/g' ${{env.USER_SETTINGS_H_NEW}} # Enable the Windows cert store API (not in the repo user_settings.h). - # Appended to wolfssh/ide/winvs/user_settings.h, which the VS projects - # put on the include path before wolfssl/IDE/WIN. + # Inserted into wolfssh/ide/winvs/user_settings.h, which the VS + # projects put on the include path before wolfssl/IDE/WIN. The insert + # lands before the closing include-guard #endif so the defines stay + # inside the guard. # RFC 6187 names only one RSA X.509 algorithm, x509v3-ssh-rsa, and it # signs with SHA-1, so both SHA-1 gates have to come down for any RSA # certificate to negotiate: @@ -75,7 +77,12 @@ jobs: # WS_MATCH_KEY_ALGO_E. # Both lists put their SHA-1 entries last, so the ECDSA entries in the # matrix still negotiate the same SHA-2 algorithms as before. - printf '\n/* Appended by windows-cert-store-test CI */\n#define WOLFSSH_WINDOWS_CERT_STORE\n#define WOLFSSH_NO_SHA1_SOFT_DISABLE\n#define WC_SIG_MIN_HASH_TYPE WC_HASH_TYPE_SHA\n' >> ${{env.USER_SETTINGS_H_NEW}} + sed -i '/#endif \/\* _WIN_USER_SETTINGS_H_ \*\//i\ + /* Inserted by windows-cert-store-test CI */\ + #define WOLFSSH_WINDOWS_CERT_STORE\ + #define WOLFSSH_NO_SHA1_SOFT_DISABLE\ + #define WC_SIG_MIN_HASH_TYPE WC_HASH_TYPE_SHA' ${{env.USER_SETTINGS_H_NEW}} + grep -q '^#define WOLFSSH_WINDOWS_CERT_STORE' ${{env.USER_SETTINGS_H_NEW}} cp ${{env.USER_SETTINGS_H_NEW}} ${{env.USER_SETTINGS_H}} - name: Build wolfssl library @@ -98,10 +105,37 @@ jobs: working-directory: ${{ github.workspace }}\wolfssh\ide\winvs run: nuget restore ${{env.SOLUTION_FILE_PATH}} + # Fails the build if the defines never reach wolfsshd.c (same guard as the + # build-sys-ca-certs job). + - name: Guard that the defines reach wolfsshd.c + working-directory: ${{ github.workspace }}\wolfssh + shell: bash + run: | + printf '\n#if !defined(WOLFSSH_WINDOWS_CERT_STORE) || !defined(WOLFSSH_NO_SHA1_SOFT_DISABLE) || !defined(WOLFSSH_SSHD)\n#error "CI: expected defines did not reach wolfsshd.c"\n#endif\n' >> apps/wolfsshd/wolfsshd.c + - name: Build wolfssh working-directory: ${{ github.workspace }}\wolfssh\ide\winvs run: msbuild /m /p:PlatformToolset=v142 /p:Platform=${{env.BUILD_PLATFORM}} /p:WindowsTargetPlatformVersion=${{env.TARGET_PLATFORM}} /p:Configuration=${{env.WOLFSSH_BUILD_CONFIGURATION}} ${{env.SOLUTION_FILE_PATH}} + # Run the unit and API tests here, where WOLFSSH_WINDOWS_CERT_STORE is + # defined; no other workflow defines it, so test_ParseCertStoreSpec and + # test_SetCertManager only ever execute in this job. Run from the wolfssh + # checkout root so ./keys/ paths resolve (as in windows-check.yml). The + # solution build writes to $(SolutionDir)$(Configuration)\$(Platform). + - name: Run api-test and unit-test + working-directory: ${{ github.workspace }}\wolfssh + shell: pwsh + run: | + $dir = "ide\winvs\${{env.WOLFSSH_BUILD_CONFIGURATION}}\${{env.BUILD_PLATFORM}}" + $dll = Get-ChildItem -Path "${{ github.workspace }}\wolfssl" -Recurse -Filter "wolfssl.dll" -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($dll) { Copy-Item $dll.FullName $dir -Force } + foreach ($t in @("api-test", "unit-test")) { + $exe = Join-Path $dir "$t.exe" + if (-not (Test-Path $exe)) { throw "$exe not found" } + & $exe + if ($LASTEXITCODE -ne 0) { throw "$t failed (exit $LASTEXITCODE)" } + } + - name: Upload wolfSSH build artifacts uses: actions/upload-artifact@v4 with: @@ -138,7 +172,12 @@ jobs: shell: bash run: | sed -i 's/#if 0/#if 1/g' ${{env.USER_SETTINGS_H_NEW}} - printf '\n#define WOLFSSH_WINDOWS_CERT_STORE\n#define WOLFSSL_SYS_CA_CERTS\n' >> ${{env.USER_SETTINGS_H_NEW}} + # Insert before the closing include-guard #endif, not after it. + sed -i '/#endif \/\* _WIN_USER_SETTINGS_H_ \*\//i\ + /* Inserted by windows-cert-store-test CI */\ + #define WOLFSSH_WINDOWS_CERT_STORE\ + #define WOLFSSL_SYS_CA_CERTS' ${{env.USER_SETTINGS_H_NEW}} + grep -q '^#define WOLFSSH_WINDOWS_CERT_STORE' ${{env.USER_SETTINGS_H_NEW}} cp ${{env.USER_SETTINGS_H_NEW}} ${{env.USER_SETTINGS_H}} - name: Build wolfssl library @@ -192,17 +231,29 @@ jobs: - name: Rejects a non-Windows host and a missing --enable-certs # Each assertion exits explicitly: bash errexit exempts a command # inverted with '!', and the step status comes from the last command. + # The output grep pins each failure to the intended configure.ac error, + # so an unrelated earlier configure failure cannot keep the check green. run: | if ./configure --enable-certs --enable-windows-cert-store \ - $WOLFSSL_CACHE; then + $WOLFSSL_CACHE > conf-host.log 2>&1; then echo 'ERROR: configure should have failed on a non-Windows host' exit 1 fi + if ! grep -q 'only supported on _WIN32 Windows hosts' conf-host.log; then + cat conf-host.log + echo 'ERROR: configure failed, but not with the non-Windows host error' + exit 1 + fi if ./configure --host=x86_64-w64-mingw32 \ - --enable-windows-cert-store $WOLFSSL_CACHE; then + --enable-windows-cert-store $WOLFSSL_CACHE > conf-nocerts.log 2>&1; then echo 'ERROR: configure should have failed without --enable-certs' exit 1 fi + if ! grep -q 'requires X.509 cert support' conf-nocerts.log; then + cat conf-nocerts.log + echo 'ERROR: configure failed, but not with the missing-certs error' + exit 1 + fi test: needs: build @@ -860,10 +911,24 @@ jobs: $echoserverPid = $env:ECHOSERVER_PID if (-not [string]::IsNullOrEmpty($echoserverPid)) { Stop-Process -Id $echoserverPid -Force -ErrorAction SilentlyContinue - Start-Sleep -Seconds 2 } # Also kill by name in case PID tracking missed it Get-Process -Name "echoserver" -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue + # wolfSSH skips SO_REUSEADDR on Windows (wolfssh/test.h), so wolfsshd + # hard-fails if it binds before the port is released. Poll until the + # listener is gone rather than sleeping a fixed interval. + $port = ${{env.TEST_PORT}} + $timeout = 30 + $elapsed = 0 + while (Get-NetTCPConnection -LocalPort $port -State Listen -ErrorAction SilentlyContinue) { + if ($elapsed -ge $timeout) { + Write-Host "ERROR: port $port still listening ${timeout}s after stopping echoserver" + exit 1 + } + Start-Sleep -Seconds 1 + $elapsed++ + } + Write-Host "Port $port released" # Clear the env var so cleanup step doesn't try again Add-Content -Path $env:GITHUB_ENV -Value "ECHOSERVER_PID=" @@ -873,18 +938,20 @@ jobs: shell: pwsh timeout-minutes: 3 run: | - # -D -t runs the config load and CTX setup in the foreground and then - # returns without listening. Windows main() always returns 0, so the - # log is what is asserted on. + # Start wolfsshd for real (no -t test mode) so refusal is observable + # as the process exiting without a listener, not just as a log line. + # Windows main() always returns 0, so the exit code is not asserted. (Get-Content sshd_config_test) -replace 'wolfSSHTestCA', 'wolfSSHEmptyCA' | Out-File -FilePath sshd_config_empty_ca -Encoding ASCII $configPathFull = (Resolve-Path "sshd_config_empty_ca").Path + $port = ${{env.TEST_PORT}} - Start-Process -FilePath (Resolve-Path $env:SSHD_PATH).Path ` - -ArgumentList @("-D", "-d", "-t", "-f", $configPathFull) ` + $proc = Start-Process -FilePath (Resolve-Path $env:SSHD_PATH).Path ` + -ArgumentList @("-D", "-d", "-f", $configPathFull, "-p", $port) ` -RedirectStandardOutput "sshd_empty_ca_out.txt" ` -RedirectStandardError "sshd_empty_ca_err.txt" ` - -Wait -NoNewWindow + -NoNewWindow -PassThru + Start-Sleep -Seconds 5 $log = "" foreach ($f in @("sshd_empty_ca_out.txt", "sshd_empty_ca_err.txt")) { @@ -892,15 +959,29 @@ jobs: } Write-Host "=== wolfsshd output ===" Write-Host $log + + $failed = $false # Windows may prune the registry key once the last cert is removed, in # which case the store fails to open instead of enumerating empty. # Either way startup must not succeed. if ($log -notmatch "No usable CA certificates found in store" -and $log -notmatch "Unable to open user CA cert store") { Write-Host "ERROR: wolfsshd did not reject the empty user CA store" - exit 1 + $failed = $true + } + if (-not $proc.HasExited) { + Write-Host "ERROR: wolfsshd is still running with an empty user CA store" + $failed = $true + } + if (Get-NetTCPConnection -LocalPort $port -State Listen -ErrorAction SilentlyContinue) { + Write-Host "ERROR: wolfsshd is listening on port $port with an empty user CA store" + $failed = $true + } + if (-not $proc.HasExited) { + Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue } - Write-Host "wolfsshd rejected the empty user CA store" + if ($failed) { exit 1 } + Write-Host "wolfsshd refused to start with the empty user CA store" - name: Start wolfSSHd as Windows service working-directory: ${{ github.workspace }}\wolfssh @@ -924,6 +1005,9 @@ jobs: # We do NOT include -E here because LocalSystem only has RX on # the wolfssh directory and cannot create a log file. Debug output # from the service goes to OutputDebugString. + # Single-string binPath with embedded quotes: how pwsh renders it to + # sc.exe depends on $PSNativeCommandArgumentPassing. This works here + # only because CI workspace paths contain no spaces. $binPath = "`"$sshdPathFull`" -f `"$configPathFull`" -p ${{env.TEST_PORT}}" Write-Host "Creating service with binpath: $binPath" $createResult = sc.exe create $serviceName binPath= $binPath diff --git a/apps/wolfsshd/auth.c b/apps/wolfsshd/auth.c index 055ead8f6..db44508d6 100644 --- a/apps/wolfsshd/auth.c +++ b/apps/wolfsshd/auth.c @@ -655,17 +655,31 @@ static int IsPerUserAuthKeysPattern(const char* pattern) { word32 i; word32 patSz; + word32 seg; if (pattern == NULL || *pattern == '\0') { /* the built-in ~/.ssh/authorized_keys default */ return 1; } + /* a ".." component can escape the home directory and collapse to one + * shared file for every account, so it is never per-user */ + patSz = (word32)WSTRLEN(pattern); + seg = 0; + for (i = 0; i <= patSz; i++) { + if (i == patSz || pattern[i] == '/' || pattern[i] == '\\') { + if (i - seg == 2 && pattern[seg] == '.' && + pattern[seg + 1] == '.') { + return 0; + } + seg = i + 1; + } + } + if (!IsAbsoluteAuthKeysPath(pattern)) { return 1; } - patSz = (word32)WSTRLEN(pattern); for (i = 0; (i + 1) < patSz; i++) { if (pattern[i] != '%') { continue; @@ -683,6 +697,21 @@ static int IsPerUserAuthKeysPattern(const char* pattern) } #endif /* WOLFSSH_CERTS && (WOLFSSL_FPKI || _WIN32) */ +/* Exported predicate matching the runtime identity-check skip; on builds + * without that check it always returns 0. */ +int wolfSSHD_AuthKeysPatternIsPerUser(const char* pattern) +{ +#if defined(WOLFSSH_CERTS) && (defined(WOLFSSL_FPKI) || defined(_WIN32)) + if (pattern == NULL) { + return 0; + } + return IsPerUserAuthKeysPattern(pattern); +#else + (void)pattern; + return 0; +#endif +} + /* Resolve the authorized keys file path for a user. The pattern is passed in * explicitly so concurrent authentications cannot race on it, and its tokens * are expanded so each user resolves to a distinct path. */ @@ -2135,9 +2164,11 @@ static int RequestAuthentication(WS_UserAuthData* authData, * binding and is checked below. A shared AuthorizedKeysFile (an * absolute pattern with no %u or %h) resolves to one file for every * account and binds the certificate to nothing, so the identity check - * still has to run. */ + * still has to run. Never skipped when AuthorizedUPNDomains is set, so + * the configured realm allowlist is always enforced. */ if (authData->sf.publicKey.isCert && !(wolfSSHD_ConfigGetAuthKeysFileSet(usrConf) && + wolfSSHD_ConfigGetAuthorizedUPNDomains(usrConf) == NULL && IsPerUserAuthKeysPattern( wolfSSHD_ConfigGetAuthKeysFile(usrConf)))) { DecodedCert* dCert; @@ -2186,17 +2217,16 @@ static int RequestAuthentication(WS_UserAuthData* authData, current = current->next; } - /* a UPN matched but no realm policy is set; warn per auth + /* a UPN matched but no realm policy is set; note per auth * attempt so the opt-in gap is visible, no shared state */ if (upnRealmUnchecked) { - wolfSSH_Log(WS_LOG_WARN, "[SSHD] AuthorizedUPNDomains " + wolfSSH_Log(WS_LOG_INFO, "[SSHD] AuthorizedUPNDomains " "not set; certificate UPN domain is not checked"); } #else /* Without FPKI compare subject CN with user name. Only * reachable on Windows, where account names are - * case-insensitive, so match the CN the same way when the - * Windows string API is available. + * case-insensitive, so match the CN the same way. * * This is a name match only. There is no analogue of * AuthorizedUPNDomains here, so any CA in the trust store @@ -2204,18 +2234,12 @@ static int RequestAuthentication(WS_UserAuthData* authData, * of the issuer policy. */ if (dCert->subjectCN != NULL && dCert->subjectCNLen > 0 && (int)XSTRLEN(usr) == dCert->subjectCNLen && - #ifdef USE_WINDOWS_API WSTRNCASECMP(usr, dCert->subjectCN, - (size_t)dCert->subjectCNLen) == 0 - #else - XSTRNCMP(usr, dCert->subjectCN, - (size_t)dCert->subjectCNLen) == 0 - #endif - ) { + (size_t)dCert->subjectCNLen) == 0) { usrMatch = 1; - /* warn per auth attempt so the weaker binding is + /* note per auth attempt so the weaker binding is * visible, no shared state */ - wolfSSH_Log(WS_LOG_WARN, "[SSHD] certificate bound to " + wolfSSH_Log(WS_LOG_INFO, "[SSHD] certificate bound to " "user by subject CN only; no issuer constraint is " "applied, keep the trusted user CA set narrow"); } diff --git a/apps/wolfsshd/auth.h b/apps/wolfsshd/auth.h index 6d67bfb61..64dfc7c19 100644 --- a/apps/wolfsshd/auth.h +++ b/apps/wolfsshd/auth.h @@ -78,6 +78,7 @@ int wolfSSHD_AuthReducePermissionsUser(WOLFSSHD_AUTH* auth, WUID_T uid, int wolfSSHD_AuthSetGroups(const WOLFSSHD_AUTH* auth, const char* usr, WGID_T gid); long wolfSSHD_AuthGetGraceTime(const WOLFSSHD_AUTH* auth); +int wolfSSHD_AuthKeysPatternIsPerUser(const char* pattern); #ifdef WOLFSSH_OSSH_CERTS void wolfSSHD_AuthSetPeerIp(WOLFSSHD_AUTH* auth, const char* ip); const char* wolfSSHD_AuthGetForcedCmd(const WOLFSSHD_AUTH* auth); diff --git a/apps/wolfsshd/configuration.c b/apps/wolfsshd/configuration.c index 3d6555121..9ded08551 100644 --- a/apps/wolfsshd/configuration.c +++ b/apps/wolfsshd/configuration.c @@ -540,6 +540,37 @@ static const CONFIG_OPTION options[] = { }; #define NUM_OPTIONS ((int)(sizeof(options) / sizeof(*options))) +#ifdef WOLFSSHD_UNIT_TEST +/* Test hook for the option-table ordering invariant: the parser matches with + * WSTRNCMP over the table in order, so an earlier name that is a strict + * prefix of a later one would shadow it. Returns 1 and sets earlier/later on + * a violation, 0 when the table is well ordered. */ +int wolfSSHD_ConfigOptionPrefixShadow(const char** earlier, const char** later) +{ + int i; + int j; + int len; + + for (i = 0; i < NUM_OPTIONS; i++) { + len = (int)WSTRLEN(options[i].name); + for (j = i + 1; j < NUM_OPTIONS; j++) { + if ((int)WSTRLEN(options[j].name) > len && + WSTRNCMP(options[i].name, options[j].name, len) == 0) { + if (earlier != NULL) { + *earlier = options[i].name; + } + if (later != NULL) { + *later = options[j].name; + } + return 1; + } + } + } + + return 0; +} +#endif /* WOLFSSHD_UNIT_TEST */ + /* returns WS_SUCCESS on success */ static int HandlePrivSep(WOLFSSHD_CONFIG* conf, const char* value) { @@ -2032,6 +2063,19 @@ char* wolfSSHD_ConfigGetAuthorizedUPNDomains(const WOLFSSHD_CONFIG* conf) return ret; } +/* returns the next config node in the list (the global config is the head, + * each Match block adds a node) or NULL at the end of the list */ +WOLFSSHD_CONFIG* wolfSSHD_ConfigGetNext(const WOLFSSHD_CONFIG* conf) +{ + WOLFSSHD_CONFIG* ret = NULL; + + if (conf != NULL) { + ret = conf->next; + } + + return ret; +} + static int SetFileString(char** dst, const char* src, void* heap) { int ret = WS_SUCCESS; diff --git a/apps/wolfsshd/configuration.h b/apps/wolfsshd/configuration.h index 367c92cec..cca930c63 100644 --- a/apps/wolfsshd/configuration.h +++ b/apps/wolfsshd/configuration.h @@ -62,6 +62,7 @@ char* wolfSSHD_ConfigGetHostKeyFile(const WOLFSSHD_CONFIG* conf); char* wolfSSHD_ConfigGetHostCertFile(const WOLFSSHD_CONFIG* conf); char* wolfSSHD_ConfigGetUserCAKeysFile(const WOLFSSHD_CONFIG* conf); char* wolfSSHD_ConfigGetAuthorizedUPNDomains(const WOLFSSHD_CONFIG* conf); +WOLFSSHD_CONFIG* wolfSSHD_ConfigGetNext(const WOLFSSHD_CONFIG* conf); int wolfSSHD_ConfigSetHostKeyFile(WOLFSSHD_CONFIG* conf, const char* file); int wolfSSHD_ConfigSetHostCertFile(WOLFSSHD_CONFIG* conf, const char* file); #ifdef WOLFSSH_WINDOWS_CERT_STORE @@ -101,6 +102,8 @@ void wolfSSHD_ConfigSavePID(const WOLFSSHD_CONFIG* conf); #ifdef WOLFSSHD_UNIT_TEST int ParseConfigLine(WOLFSSHD_CONFIG** conf, const char* l, int lSz, int depth); +int wolfSSHD_ConfigOptionPrefixShadow(const char** earlier, + const char** later); #endif #endif /* WOLFSSHD_H */ diff --git a/apps/wolfsshd/test/test_configuration.c b/apps/wolfsshd/test/test_configuration.c index 75708d53a..0ad4970f3 100644 --- a/apps/wolfsshd/test/test_configuration.c +++ b/apps/wolfsshd/test/test_configuration.c @@ -2588,8 +2588,27 @@ static int test_PermitRootLoginModes(void) return ret; } -/* Parses an AuthorizedUPNDomains line and confirms the stored value is returned - * by the getter, locking in the new config option's plumbing. */ +/* The config parser matches option names with WSTRNCMP over the options table + * in order, so no entry may be a strict prefix of a later one (e.g. "HostKey" + * must come after the "HostKeyStore*" names). */ +static int test_ConfigOptionPrefixOrder(void) +{ + int ret = WS_SUCCESS; + const char* earlier = NULL; + const char* later = NULL; + + Log(" Testing scenario: option table prefix ordering."); + if (wolfSSHD_ConfigOptionPrefixShadow(&earlier, &later)) { + Log(" option '%s' shadows later option '%s'.", earlier, later); + ret = WS_FATAL_ERROR; + } + Log(ret == WS_SUCCESS ? " PASSED.\n" : " FAILED.\n"); + + return ret; +} + +/* Parses an AuthorizedUPNDomains line and confirms the stored value is + * returned by the getter, locking in the new config option's plumbing. */ static int test_ConfigParseAuthorizedUPNDomains(void) { int ret = WS_SUCCESS; @@ -4007,6 +4026,7 @@ const TEST_CASE testCases[] = { TEST_DECL(test_ConfigDefaults), TEST_DECL(test_PermitRootProhibitPassword), TEST_DECL(test_ParseConfigLine), + TEST_DECL(test_ConfigOptionPrefixOrder), TEST_DECL(test_ConfigCopy), TEST_DECL(test_GetUserConfMatchOverride), TEST_DECL(test_MatchUnsupportedSelector), diff --git a/apps/wolfsshd/wolfsshd.c b/apps/wolfsshd/wolfsshd.c index 51b93fe16..c6cf6e769 100644 --- a/apps/wolfsshd/wolfsshd.c +++ b/apps/wolfsshd/wolfsshd.c @@ -415,6 +415,26 @@ static int ParseCertStoreLocation(const char* in, word32* out) WSTRCMP(in, "CERT_SYSTEM_STORE_USERS") == 0) { *out = (word32)CERT_SYSTEM_STORE_USERS; } + else if (WSTRCMP(in, "CURRENT_SERVICE") == 0 || + WSTRCMP(in, "CERT_SYSTEM_STORE_CURRENT_SERVICE") == 0) { + *out = (word32)CERT_SYSTEM_STORE_CURRENT_SERVICE; + } + else if (WSTRCMP(in, "SERVICES") == 0 || + WSTRCMP(in, "CERT_SYSTEM_STORE_SERVICES") == 0) { + *out = (word32)CERT_SYSTEM_STORE_SERVICES; + } + else if (WSTRCMP(in, "CURRENT_USER_GROUP_POLICY") == 0 || + WSTRCMP(in, "CERT_SYSTEM_STORE_CURRENT_USER_GROUP_POLICY") == 0) { + *out = (word32)CERT_SYSTEM_STORE_CURRENT_USER_GROUP_POLICY; + } + else if (WSTRCMP(in, "LOCAL_MACHINE_GROUP_POLICY") == 0 || + WSTRCMP(in, "CERT_SYSTEM_STORE_LOCAL_MACHINE_GROUP_POLICY") == 0) { + *out = (word32)CERT_SYSTEM_STORE_LOCAL_MACHINE_GROUP_POLICY; + } + else if (WSTRCMP(in, "LOCAL_MACHINE_ENTERPRISE") == 0 || + WSTRCMP(in, "CERT_SYSTEM_STORE_LOCAL_MACHINE_ENTERPRISE") == 0) { + *out = (word32)CERT_SYSTEM_STORE_LOCAL_MACHINE_ENTERPRISE; + } else { end = NULL; errno = 0; @@ -444,33 +464,67 @@ enum { /* Returns CERT_CA_YES when der holds an X.509 certificate with * basicConstraints CA:TRUE, CERT_CA_NO when it parses but is not a CA, and * CERT_CA_UNKNOWN when it could not be examined. */ -static int CertIsCA(const byte* der, word32 derSz) +static int CertIsCA(const byte* der, word32 derSz, void* heap) { DecodedCert* dCert; int isCA = CERT_CA_UNKNOWN; +#ifndef WOLFSSH_SMALL_STACK + DecodedCert sdCert; +#endif + #ifdef WOLFSSH_SMALL_STACK - dCert = (DecodedCert*)WMALLOC(sizeof(DecodedCert), NULL, DYNTYPE_CERT); + dCert = (DecodedCert*)WMALLOC(sizeof(DecodedCert), heap, DYNTYPE_CERT); if (dCert == NULL) { return CERT_CA_UNKNOWN; } #else - DecodedCert sdCert; - dCert = &sdCert; #endif - wc_InitDecodedCert(dCert, der, derSz, NULL); + wc_InitDecodedCert(dCert, der, derSz, heap); if (wc_ParseCert(dCert, CERT_TYPE, NO_VERIFY, NULL) == 0) { isCA = (dCert->isCA != 0) ? CERT_CA_YES : CERT_CA_NO; } wc_FreeDecodedCert(dCert); #ifdef WOLFSSH_SMALL_STACK - WFREE(dCert, NULL, DYNTYPE_CERT); + WFREE(dCert, heap, DYNTYPE_CERT); #endif return isCA; } +/* Returns 1 when name refers to a Windows store populated by the OS or the + * Microsoft Trusted Root Program rather than the administrator. Store names + * resolve to registry keys, which are case-insensitive, and may carry a + * '\' or '\' prefix, so only the final path component is + * compared, case-insensitively. */ +static int IsWinPublicTrustStoreName(const char* name) +{ + static const char* const deny[] = { + "Root", "AuthRoot", "CA", "Disallowed", "TrustedPublisher", "trust" + }; + const char* base; + const char* sep; + word32 len; + word32 i; + + base = name; + sep = WSTRCHR(base, '\\'); + while (sep != NULL) { + base = sep + 1; + sep = WSTRCHR(base, '\\'); + } + len = (word32)WSTRLEN(base); + for (i = 0; i < (word32)(sizeof(deny) / sizeof(*deny)); i++) { + if (len == (word32)WSTRLEN(deny[i]) && + WSTRNCASECMP(base, deny[i], len) == 0) { + return 1; + } + } + + return 0; +} + /* Add every CA certificate in the configured Windows store (winUserPvPara * name, winUserDwFlags location) as a trusted root CA. Returns WS_SUCCESS on * success. */ @@ -489,6 +543,7 @@ static int LoadUserCACertsFromStore(const WOLFSSHD_CONFIG* conf, word32 loaded = 0; word32 skipped = 0; word32 rejected = 0; + word32 notX509 = 0; int isCA; storeNameStr = wolfSSHD_ConfigGetWinUserPvPara(conf); @@ -512,11 +567,9 @@ static int LoadUserCACertsFromStore(const WOLFSSHD_CONFIG* conf, return WS_BAD_ARGUMENT; } - if (WSTRCMP(storeNameStr, "Root") == 0 || - WSTRCMP(storeNameStr, "AuthRoot") == 0 || - WSTRCMP(storeNameStr, "CA") == 0) { + if (IsWinPublicTrustStoreName(storeNameStr)) { wolfSSH_Log(WS_LOG_ERROR, - "[SSHD] wolfSSH_WinUserPvPara='%s' names a Windows public trust " + "[SSHD] wolfSSH_WinUserPvPara='%s' names a Windows system trust " "store. Every CA it holds would become an SSH login authority. " "Use a store created for this purpose instead.", storeNameStr); return WS_BAD_ARGUMENT; @@ -549,13 +602,26 @@ static int LoadUserCACertsFromStore(const WOLFSSHD_CONFIG* conf, "[SSHD] Unrecognized user CA store flags '%s'", dwFlagsStr); return WS_BAD_ARGUMENT; } - if (dwFlags == (word32)CERT_SYSTEM_STORE_CURRENT_USER) { + /* Every location other than the LOCAL_MACHINE hives (per-user, per-service + * and HKEY_USERS stores) can be written without elevation by the account + * it belongs to. */ + if (dwFlags != (word32)CERT_SYSTEM_STORE_LOCAL_MACHINE && + dwFlags != (word32)CERT_SYSTEM_STORE_LOCAL_MACHINE_GROUP_POLICY && + dwFlags != (word32)CERT_SYSTEM_STORE_LOCAL_MACHINE_ENTERPRISE) { wolfSSH_Log(WS_LOG_WARN, - "[SSHD] wolfSSH_WinUserDwFlags selects the per-user store hive, " - "which the daemon's own account can write to without elevation. " + "[SSHD] wolfSSH_WinUserDwFlags selects a store hive that its own " + "account can write to without elevation. " "LOCAL_MACHINE is the safer location for trust anchors."); } +#ifdef WOLFSSH_NO_FPKI + wolfSSH_Log(WS_LOG_WARN, + "[SSHD] WARNING: built without FPKI profile checking, so peer " + "certificates are not required to carry a client authentication " + "EKU. A TLS server, S/MIME or code signing certificate with a " + "matching subject is accepted for login."); +#endif + wStoreNameLen = MultiByteToWideChar(CP_UTF8, 0, storeNameStr, -1, NULL, 0); if (wStoreNameLen == 0) { wolfSSH_Log(WS_LOG_ERROR, @@ -567,8 +633,13 @@ static int LoadUserCACertsFromStore(const WOLFSSHD_CONFIG* conf, if (wStoreName == NULL) { return WS_MEMORY_E; } - MultiByteToWideChar(CP_UTF8, 0, storeNameStr, -1, wStoreName, - wStoreNameLen); + if (MultiByteToWideChar(CP_UTF8, 0, storeNameStr, -1, wStoreName, + wStoreNameLen) == 0) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Failed to convert user CA store name to wide characters"); + WFREE(wStoreName, heap, DYNTYPE_SSHD); + return WS_BAD_ARGUMENT; + } hStore = CertOpenStore(CERT_STORE_PROV_SYSTEM_W, 0, (HCRYPTPROV_LEGACY)0, dwFlags | CERT_STORE_OPEN_EXISTING_FLAG | CERT_STORE_READONLY_FLAG, @@ -589,13 +660,24 @@ static int LoadUserCACertsFromStore(const WOLFSSHD_CONFIG* conf, } if (pCertContext->pbCertEncoded == NULL || pCertContext->cbCertEncoded == 0) { + notX509++; + wolfSSH_Log(WS_LOG_WARN, + "[SSHD] Skipping an entry in store '%s' with no encoded " + "certificate", storeNameStr); + continue; + } + if ((pCertContext->dwCertEncodingType & X509_ASN_ENCODING) == 0) { + notX509++; + wolfSSH_Log(WS_LOG_WARN, + "[SSHD] Skipping an entry in store '%s' that is not X.509 " + "DER encoded", storeNameStr); continue; } /* wolfSSL does not enforce basicConstraints CA:TRUE for user-loaded * trust anchors, so an end-entity certificate sitting in the store * would become a login authority. Filter it out here. */ isCA = CertIsCA(pCertContext->pbCertEncoded, - (word32)pCertContext->cbCertEncoded); + (word32)pCertContext->cbCertEncoded, heap); if (isCA != CERT_CA_YES) { skipped++; wolfSSH_Log(WS_LOG_WARN, @@ -624,15 +706,16 @@ static int LoadUserCACertsFromStore(const WOLFSSHD_CONFIG* conf, if (loaded == 0) { wolfSSH_Log(WS_LOG_ERROR, "[SSHD] No usable CA certificates found in store '%s' (%u not a " - "CA, %u rejected as a root CA)", storeNameStr, skipped, rejected); + "CA, %u rejected as a root CA, %u not X.509)", storeNameStr, + skipped, rejected, notX509); ret = WS_FATAL_ERROR; } else { wolfSSH_Log(WS_LOG_INFO, "[SSHD] Trusting %u CA certificate(s) from store '%s' " "(location 0x%08lx) for client authentication (%u not a CA, %u " - "rejected as a root CA)", loaded, storeNameStr, - (unsigned long)dwFlags, skipped, rejected); + "rejected as a root CA, %u not X.509)", loaded, storeNameStr, + (unsigned long)dwFlags, skipped, rejected, notX509); if (rejected > 0) { wolfSSH_Log(WS_LOG_WARN, "[SSHD] %u CA certificate(s) in store '%s' could not be " @@ -731,7 +814,8 @@ static int SetupCTX(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX** ctx, wchar_t* wStoreName = NULL; wchar_t* wSubjectName = NULL; word32 dwFlags = CERT_SYSTEM_STORE_CURRENT_USER; - int storeNameLen, subjectNameLen; + int storeNameLen = 0; + int subjectNameLen = 0; /* An unset location keeps the CURRENT_USER default set above. */ if (hostKeyStoreFlags != NULL && @@ -742,6 +826,14 @@ static int SetupCTX(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX** ctx, hostKeyStoreFlags); ret = WS_BAD_ARGUMENT; } + if (ret == WS_SUCCESS && + dwFlags == (word32)CERT_SYSTEM_STORE_CURRENT_USER) { + wolfSSH_Log(WS_LOG_WARN, + "[SSHD] HostKeyStoreFlags selects the per-user store " + "hive, which the daemon's own account can write to " + "without elevation. LOCAL_MACHINE is the safer location " + "for the host key."); + } /* Convert to wide strings */ if (ret == WS_SUCCESS) { @@ -768,12 +860,15 @@ static int SetupCTX(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX** ctx, "[SSHD] Memory allocation failed for cert store strings"); ret = WS_MEMORY_E; } + else if (MultiByteToWideChar(CP_UTF8, 0, hostKeyStore, -1, + wStoreName, storeNameLen) == 0 || + MultiByteToWideChar(CP_UTF8, 0, hostKeyStoreSubject, + -1, wSubjectName, subjectNameLen) == 0) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Failed to convert cert store strings to wchar"); + ret = WS_BAD_ARGUMENT; + } else { - MultiByteToWideChar(CP_UTF8, 0, hostKeyStore, -1, - wStoreName, storeNameLen); - MultiByteToWideChar(CP_UTF8, 0, hostKeyStoreSubject, -1, - wSubjectName, subjectNameLen); - ret = wolfSSH_CTX_UsePrivateKey_fromStore(*ctx, wStoreName, dwFlags, wSubjectName); if (ret != WS_SUCCESS) { @@ -1050,18 +1145,53 @@ static int SetupCTX(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX** ctx, } #endif - /* A per-user AuthorizedKeysFile is its own user-to-certificate binding, so - * the certificate identity check, and with it the UPN realm allowlist, is - * not run for those accounts. Say so rather than leaving the directive - * looking enforced. */ - if (ret == WS_SUCCESS && - wolfSSHD_ConfigGetAuthorizedUPNDomains(conf) != NULL && - wolfSSHD_ConfigGetAuthKeysFileSet(conf)) { - wolfSSH_Log(WS_LOG_WARN, - "[SSHD] AuthorizedUPNDomains is not enforced for accounts with a " - "per-user AuthorizedKeysFile; those certificates are bound by " - "their authorized_keys entry instead."); + /* AuthorizedUPNDomains is only enforced by the FPKI UPN check. Fail + * startup rather than silently ignore a configured realm policy. Check + * every config node since the directive may sit in a Match block. */ + #ifndef WOLFSSL_FPKI + if (ret == WS_SUCCESS) { + const WOLFSSHD_CONFIG* cur; + + cur = conf; + while (cur != NULL) { + if (wolfSSHD_ConfigGetAuthorizedUPNDomains(cur) != NULL) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] AuthorizedUPNDomains is set but wolfSSL was " + "built without WOLFSSL_FPKI, so the UPN realm allowlist " + "cannot be enforced."); + ret = WS_BAD_ARGUMENT; + break; + } + cur = wolfSSHD_ConfigGetNext(cur); + } + } + #endif /* !WOLFSSL_FPKI */ + + /* A per-user AuthorizedKeysFile is its own user-to-certificate binding, + * so the certificate identity check is skipped for those accounts unless + * AuthorizedUPNDomains is set. Warn once at startup, checking every + * config node since AuthorizedKeysFile may be set in a Match block. */ + #if defined(WOLFSSL_FPKI) || defined(_WIN32) + if (ret == WS_SUCCESS) { + const WOLFSSHD_CONFIG* cur; + + cur = conf; + while (cur != NULL) { + if (wolfSSHD_ConfigGetAuthKeysFileSet(cur) && + wolfSSHD_AuthKeysPatternIsPerUser( + wolfSSHD_ConfigGetAuthKeysFile(cur)) && + wolfSSHD_ConfigGetAuthorizedUPNDomains(cur) == NULL) { + wolfSSH_Log(WS_LOG_WARN, + "[SSHD] The certificate identity check is skipped for " + "accounts with a per-user AuthorizedKeysFile; those " + "certificates are bound by their authorized_keys entry " + "instead."); + break; + } + cur = wolfSSHD_ConfigGetNext(cur); + } } + #endif /* WOLFSSL_FPKI || _WIN32 */ /* load in CA certs from file set */ if (ret == WS_SUCCESS) { diff --git a/configure.ac b/configure.ac index 93ae41e06..254d2891d 100644 --- a/configure.ac +++ b/configure.ac @@ -346,7 +346,11 @@ AC_CONFIG_LINKS([keys/gretel-key-rsa.pub:keys/gretel-key-rsa.pub keys/server-key-rsa.der:keys/server-key-rsa.der keys/server-key-ecc.der:keys/server-key-ecc.der keys/server-key-ecc-521.der:keys/server-key-ecc-521.der - keys/server-key-ed25519.der:keys/server-key-ed25519.der]) + keys/server-key-ed25519.der:keys/server-key-ed25519.der + keys/ca-cert-ecc.der:keys/ca-cert-ecc.der + keys/ca-key-ecc.der:keys/ca-key-ecc.der + keys/fred-cert.der:keys/fred-cert.der + keys/fred-key.der:keys/fred-key.der]) # Set the automake conditionals. AM_CONDITIONAL([BUILD_EXAMPLE_SERVERS],[test "x$ENABLED_EXAMPLES" = "xyes"]) diff --git a/examples/client/common.c b/examples/client/common.c index e5c7b6a6f..65bb3d0a0 100644 --- a/examples/client/common.c +++ b/examples/client/common.c @@ -48,16 +48,11 @@ #ifdef WOLFSSH_CERTS #include - #ifdef WOLFSSH_WINDOWS_CERT_STORE - /* windows.h pulls in wincrypt.h; no NCrypt API is called from here. */ - #include - #endif /* WOLFSSH_WINDOWS_CERT_STORE */ #endif static byte userPublicKeyBuf[512]; static byte* userPublicKey = userPublicKeyBuf; static byte userPublicKeyAlloc = 0; -static int userPublicKeyCtxOwned = 0; /* userPublicKey aliases CTX memory */ static const byte* userPublicKeyType = NULL; static byte userPassword[256]; static const byte* userPrivateKeyType = NULL; @@ -764,8 +759,6 @@ int ClientUseCert(const char* certName, void* heap) userPublicKeyTypeSz = (word32)WSTRLEN((const char*)publicKeyType); pubKeyLoaded = 1; userPublicKeyAlloc = 1; - /* this buffer is ours now, not the CTX's */ - userPublicKeyCtxOwned = 0; } else { /* Defensive: load_der_file() clears its output pointer on the @@ -968,8 +961,6 @@ static int wolfSSH_TPM_InitKey(WOLFTPM2_DEV* dev, const char* name, if (rc == 0) { userPublicKey = p; userPublicKeyAlloc = 1; - /* this buffer is ours now, not the CTX's */ - userPublicKeyCtxOwned = 0; } else { WLOG(WS_LOG_DEBUG, "Reading public key failed, rc: %d", rc); } @@ -1113,8 +1104,6 @@ int ClientUsePubKey(const char* pubKeyName, int userEcc, void* heap) if (ret == 0) { pubKeyLoaded = 1; userPublicKeyAlloc = 1; - /* this buffer is ours now, not the CTX's */ - userPublicKeyCtxOwned = 0; } else { userPublicKey = userPublicKeyBuf; @@ -1169,14 +1158,7 @@ void ClientFreeBuffers(const char* pubKeyName, const char* privKeyName, * name being given. */ (void)pubKeyName; - if (userPublicKeyCtxOwned) { - /* Aliases CTX-owned memory; the CTX frees it, not us. */ - userPublicKey = userPublicKeyBuf; - userPublicKeySz = 0; - userPublicKeyCtxOwned = 0; - userPublicKeyAlloc = 0; - } - else if (userPublicKeyAlloc && userPublicKey != NULL) { + if (userPublicKeyAlloc && userPublicKey != NULL) { WFREE(userPublicKey, heap, DYNTYPE_PRIVKEY); userPublicKey = userPublicKeyBuf; userPublicKeySz = 0; @@ -1230,9 +1212,11 @@ int ClientSetPrivateKeyFromStore(WOLFSSH_CTX* ctx, return WS_BAD_ARGUMENT; } - ret = wolfSSH_CTX_UsePrivateKey_fromStore(ctx, storeName, dwFlags, subjectName); + ret = wolfSSH_CTX_UsePrivateKey_fromStore(ctx, storeName, dwFlags, + subjectName); if (ret != WS_SUCCESS) { - fprintf(stderr, "Error loading private key from certificate store: %d\n", ret); + fprintf(stderr, + "Error loading private key from certificate store: %d\n", ret); } return ret; @@ -1248,6 +1232,7 @@ int ClientSetupCertStoreAuth(WOLFSSH_CTX* ctx, void* heap) { const byte* keyType = NULL; WOLFSSH_PVT_KEY* pvtKey = NULL; + byte* certCopy = NULL; word32 i; if (ctx == NULL) @@ -1294,6 +1279,14 @@ int ClientSetupCertStoreAuth(WOLFSSH_CTX* ctx, void* heap) return WS_BAD_ARGUMENT; } + /* Copy the DER certificate before touching the globals so a failure + * leaves them alone. ClientFreeBuffers() frees the copy. */ + certCopy = (byte*)WMALLOC(pvtKey->certSz, heap, DYNTYPE_PRIVKEY); + if (certCopy == NULL) { + return WS_MEMORY_E; + } + WMEMCPY(certCopy, pvtKey->cert, pvtKey->certSz); + /* Drop anything an earlier file based load left behind, the cert * store key replaces it. Freed with the same heap the loaders in this * file allocate with. */ @@ -1308,14 +1301,9 @@ int ClientSetupCertStoreAuth(WOLFSSH_CTX* ctx, void* heap) userPrivateKeyAlloc = 0; } - /* Point userPublicKey at the DER certificate stored in the CTX. The - * ctx-owned flag stops ClientFreeBuffers from freeing CTX memory. - * The alias is only valid while the slot keeps its certificate: - * re-loading a host key onto this slot frees it, so do not mix this - * with the file-key loaders on the same CTX. */ - userPublicKey = pvtKey->cert; + userPublicKey = certCopy; userPublicKeySz = pvtKey->certSz; - userPublicKeyCtxOwned = 1; + userPublicKeyAlloc = 1; userPublicKeyType = keyType; userPublicKeyTypeSz = (word32)WSTRLEN((const char*)keyType); diff --git a/examples/client/common.h b/examples/client/common.h index 21b4a0e44..62618d166 100644 --- a/examples/client/common.h +++ b/examples/client/common.h @@ -45,8 +45,8 @@ int ClientSetTpm(WOLFSSH* ssh); int ClientSetPrivateKeyFromStore(WOLFSSH_CTX* ctx, const wchar_t* storeName, word32 dwFlags, const wchar_t* subjectName); /* Supersedes ClientUseCert()/ClientUsePubKey()/ClientSetPrivateKey(), any key - * they loaded is released. Call ClientFreeBuffers() before wolfSSH_CTX_free(), - * the auth globals alias memory owned by ctx. */ + * they loaded is released. Copies the certificate out of ctx; call + * ClientFreeBuffers() to release the copy. */ int ClientSetupCertStoreAuth(WOLFSSH_CTX* ctx, void* heap); #endif /* WOLFSSH_WINDOWS_CERT_STORE */ diff --git a/examples/echoserver/echoserver.c b/examples/echoserver/echoserver.c index 0b7445f1a..c15dafda1 100644 --- a/examples/echoserver/echoserver.c +++ b/examples/echoserver/echoserver.c @@ -120,17 +120,6 @@ #define SOCKET_EWOULDBLOCK WSAEWOULDBLOCK #endif -#ifdef WOLFSSH_WINDOWS_CERT_STORE - #include - #include - #ifndef CERT_SYSTEM_STORE_CURRENT_USER - #define CERT_SYSTEM_STORE_CURRENT_USER 0x00010000 - #endif - #ifndef CERT_SYSTEM_STORE_LOCAL_MACHINE - #define CERT_SYSTEM_STORE_LOCAL_MACHINE 0x00020000 - #endif -#endif - /* Shared by echoserver_test() and the -W pre-scan in wolfSSH_Echoserver(). */ #define ES_OPTLIST "?1a:d:efEp:R:Ni:j:I:J:K:P:k:b:x:m:c:s:G:HW:" @@ -3172,9 +3161,12 @@ THREAD_RETURN WOLFSSH_THREAD echoserver_test(void* args) myoptind = 0; /* reset for test cases */ #ifdef WOLFSSH_WINDOWS_CERT_STORE - /* -W takes priority over the environment. */ + /* -W takes priority over the environment; empty means unset. */ if (certStoreSpec == NULL) { certStoreSpec = getenv("WOLFSSH_CERT_STORE"); + if (certStoreSpec != NULL && certStoreSpec[0] == '\0') { + certStoreSpec = NULL; + } if (certStoreSpec != NULL) { printf("Taking the host key from the WOLFSSH_CERT_STORE " "environment variable\n"); @@ -3392,8 +3384,7 @@ THREAD_RETURN WOLFSSH_THREAD echoserver_test(void* args) ret = wolfSSH_CTX_UsePrivateKey_fromStore(ctx, wStoreName, dwFlags, wSubjectName); - WFREE(wStoreName, heap, DYNTYPE_TEMP); - WFREE(wSubjectName, heap, DYNTYPE_TEMP); + wolfSSH_FreeCertStoreSpec(wStoreName, wSubjectName, heap); if (ret != WS_SUCCESS) { #ifdef WOLFSSH_SMALL_STACK wc_ForceZero(keyLoadBuf, EXAMPLE_KEYLOAD_BUFFER_SZ); @@ -3812,10 +3803,14 @@ int wolfSSH_Echoserver(int argc, char** argv) { int useStore = 0; #ifdef WOLFSSH_WINDOWS_CERT_STORE + const char* envStore; + /* When using the Windows certificate store for host keys, the * echoserver does not need file-based keys, so skip the root - * directory search that looks for ./keys/server-key-rsa.pem. */ - if (getenv("WOLFSSH_CERT_STORE") != NULL) { + * directory search that looks for ./keys/server-key-rsa.pem. + * An empty WOLFSSH_CERT_STORE means unset. */ + envStore = getenv("WOLFSSH_CERT_STORE"); + if (envStore != NULL && envStore[0] != '\0') { useStore = 1; } else { diff --git a/examples/scpclient/scpclient.c b/examples/scpclient/scpclient.c index 08ee73f10..f2ecde3b2 100644 --- a/examples/scpclient/scpclient.c +++ b/examples/scpclient/scpclient.c @@ -329,6 +329,7 @@ THREAD_RETURN WOLFSSH_THREAD scp_client(void* args) } WCLOSESOCKET(sockFd); wolfSSH_free(ssh); + ClientFreeBuffers(pubKeyName, privKeyName, NULL); wolfSSH_CTX_free(ctx); if (ret != WS_SUCCESS && ret != WS_SOCKET_ERROR_E && ret != WS_CHANNEL_CLOSED) { @@ -336,7 +337,6 @@ THREAD_RETURN WOLFSSH_THREAD scp_client(void* args) "Closing scp stream failed. Connection could have been closed by peer"); } - ClientFreeBuffers(pubKeyName, privKeyName, NULL); #if !defined(WOLFSSH_NO_ECC) && defined(FP_ECC) && defined(HAVE_THREAD_LS) wc_ecc_fp_free(); /* free per thread cache */ #endif diff --git a/examples/sftpclient/sftpclient.c b/examples/sftpclient/sftpclient.c index 951015c52..6093f4cbd 100644 --- a/examples/sftpclient/sftpclient.c +++ b/examples/sftpclient/sftpclient.c @@ -49,17 +49,6 @@ #ifdef WOLFSSH_CERTS #include - #ifdef WOLFSSH_WINDOWS_CERT_STORE - #include - #include - #include - #ifndef CERT_SYSTEM_STORE_CURRENT_USER - #define CERT_SYSTEM_STORE_CURRENT_USER 0x00010000 - #endif - #ifndef CERT_SYSTEM_STORE_LOCAL_MACHINE - #define CERT_SYSTEM_STORE_LOCAL_MACHINE 0x00020000 - #endif - #endif /* WOLFSSH_WINDOWS_CERT_STORE */ #endif #if defined(WOLFSSH_SFTP) && !defined(NO_WOLFSSH_CLIENT) @@ -412,8 +401,8 @@ static void ShowUsage(void) printf(" -g put local filename as remote filename\n"); printf(" -G get remote filename as local filename\n"); printf(" -i filename for the user's private key\n"); - printf(" -k set the comma separated list of public key " - "algos to offer\n"); + printf(" -k set the comma separated list of server host key " + "algos to accept\n"); #ifdef WOLFSSH_WINDOWS_CERT_STORE printf(" -W Windows cert store: \"store:subject:flags\"\n"); printf(" Example: -W \"My:CN=MyCert:CURRENT_USER\"\n"); @@ -1760,8 +1749,7 @@ THREAD_RETURN WOLFSSH_THREAD sftpclient_test(void* args) /* Create context first */ ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, heap); if (ctx == NULL) { - WFREE(wStoreName, heap, DYNTYPE_TEMP); - WFREE(wSubjectName, heap, DYNTYPE_TEMP); + wolfSSH_FreeCertStoreSpec(wStoreName, wSubjectName, heap); err_sys("Couldn't create wolfSSH client context."); } @@ -1770,11 +1758,11 @@ THREAD_RETURN WOLFSSH_THREAD sftpclient_test(void* args) * nothing to clean up. */ ret = ClientSetPrivateKeyFromStore(ctx, wStoreName, dwFlags, wSubjectName); - WFREE(wStoreName, heap, DYNTYPE_TEMP); + wolfSSH_FreeCertStoreSpec(wStoreName, wSubjectName, heap); wStoreName = NULL; - WFREE(wSubjectName, heap, DYNTYPE_TEMP); wSubjectName = NULL; if (ret != WS_SUCCESS) { + wolfSSH_CTX_free(ctx); err_sys("Error setting private key from certificate store"); } @@ -1783,6 +1771,7 @@ THREAD_RETURN WOLFSSH_THREAD sftpclient_test(void* args) * authentication. */ ret = ClientSetupCertStoreAuth(ctx, heap); if (ret != WS_SUCCESS) { + wolfSSH_CTX_free(ctx); err_sys("Error setting up cert store auth"); } } else diff --git a/src/certman.c b/src/certman.c index d799a4a18..fcc22f281 100644 --- a/src/certman.c +++ b/src/certman.c @@ -53,6 +53,9 @@ #ifndef CERT_SYSTEM_STORE_LOCATION_MASK #define CERT_SYSTEM_STORE_LOCATION_MASK 0x00FF0000 #endif + #ifndef CERT_SYSTEM_STORE_LOCATION_SHIFT + #define CERT_SYSTEM_STORE_LOCATION_SHIFT 16 + #endif #ifndef CERT_SYSTEM_STORE_CURRENT_USER #define CERT_SYSTEM_STORE_CURRENT_USER 0x00010000 #endif @@ -712,14 +715,30 @@ static int CheckProfile(DecodedCert* cert, int profile) #ifdef WOLFSSH_WINDOWS_CERT_STORE +/* Returns 1 when dwFlags is exactly one assigned CERT_SYSTEM_STORE_* + * location with no control flags set, 0 otherwise. Location ids 1, 2 and + * 4..9 are assigned in wincrypt.h; 3 and 10..255 are not, and CertOpenStore + * fails opaquely on them. */ +int wolfSSH_CertStoreLocationValid(word32 dwFlags) +{ + word32 id; + + if ((dwFlags & ~(word32)CERT_SYSTEM_STORE_LOCATION_MASK) != 0) { + return 0; + } + id = dwFlags >> CERT_SYSTEM_STORE_LOCATION_SHIFT; + return id == 1 || id == 2 || (id >= 4 && id <= 9); +} + + /* Parse a cert store spec string "store:subject[:flags]" into wide-string * components. The spec is split at the first ':' for the store name and at * the next one for the flags, so neither the store name nor the subject may * contain a ':'; a spec with a third ':' is rejected. "My:CN=host:65536" is * therefore store "My", subject "CN=host", flags 65536, never a two-field - * spec with a ':' in the subject. Allocates wStoreName and wSubjectName via - * WMALLOC; caller must WFREE them. On success dwFlags is set to the parsed - * flags value, on failure it is left alone. + * spec with a ':' in the subject. Allocates wStoreName and wSubjectName; + * caller releases them with wolfSSH_FreeCertStoreSpec(). On success dwFlags + * is set to the parsed flags value, on failure it is left alone. * Returns WS_SUCCESS on success. */ int wolfSSH_ParseCertStoreSpec(const char* spec, wchar_t** wStoreName, wchar_t** wSubjectName, @@ -806,10 +825,10 @@ int wolfSSH_ParseCertStoreSpec(const char* spec, WFREE(specCopy, heap, DYNTYPE_TEMP); return WS_BAD_ARGUMENT; } - if ((flagsVal & locationMask) == 0 - || (flagsVal & ~locationMask) != 0) { + if (!wolfSSH_CertStoreLocationValid((word32)flagsVal)) { WLOG(WS_LOG_CERTMAN, - "Cert store flags are not a store location"); + "Cert store flags are not an assigned store " + "location"); WFREE(specCopy, heap, DYNTYPE_TEMP); return WS_BAD_ARGUMENT; } @@ -852,16 +871,39 @@ int wolfSSH_ParseCertStoreSpec(const char* spec, return WS_MEMORY_E; } - MultiByteToWideChar(CP_UTF8, 0, storeName, -1, - *wStoreName, wStoreNameLen); - MultiByteToWideChar(CP_UTF8, 0, subjectName, -1, - *wSubjectName, wSubjectNameLen); + if (MultiByteToWideChar(CP_UTF8, 0, storeName, -1, + *wStoreName, wStoreNameLen) == 0 || + MultiByteToWideChar(CP_UTF8, 0, subjectName, -1, + *wSubjectName, wSubjectNameLen) == 0) { + WLOG(WS_LOG_CERTMAN, "Cert store spec wide-string conversion failed"); + WFREE(*wStoreName, heap, DYNTYPE_TEMP); + WFREE(*wSubjectName, heap, DYNTYPE_TEMP); + *wStoreName = NULL; + *wSubjectName = NULL; + WFREE(specCopy, heap, DYNTYPE_TEMP); + return WS_FATAL_ERROR; + } *dwFlags = flags; WFREE(specCopy, heap, DYNTYPE_TEMP); return WS_SUCCESS; } + + +/* Releases the wide strings allocated by wolfSSH_ParseCertStoreSpec(). + * Either pointer may be NULL. The heap must match the parse call. */ +void wolfSSH_FreeCertStoreSpec(wchar_t* wStoreName, wchar_t* wSubjectName, + void* heap) +{ + if (wStoreName != NULL) { + WFREE(wStoreName, heap, DYNTYPE_TEMP); + } + if (wSubjectName != NULL) { + WFREE(wSubjectName, heap, DYNTYPE_TEMP); + } + WOLFSSH_UNUSED(heap); +} #endif /* WOLFSSH_WINDOWS_CERT_STORE */ diff --git a/src/internal.c b/src/internal.c index 7ea43c3cd..6e6e9be7b 100644 --- a/src/internal.c +++ b/src/internal.c @@ -2251,6 +2251,8 @@ WOLFSSH_LOCAL void RefreshPublicKeyAlgo(WOLFSSH_CTX* ctx) /* An x509v3 slot whose certificate was dropped cannot produce a K_S, * so do not advertise it. */ if (IsCertKeyId(key->publicKeyFmt) && key->cert == NULL) { + WLOG(WS_LOG_DEBUG, "RefreshPublicKeyAlgo: skipping %s, " + "no certificate", IdToName(key->publicKeyFmt)); continue; } #endif @@ -2267,6 +2269,8 @@ WOLFSSH_LOCAL void RefreshPublicKeyAlgo(WOLFSSH_CTX* ctx) && !IsCertStoreKey(key) #endif ) { + WLOG(WS_LOG_DEBUG, "RefreshPublicKeyAlgo: skipping %s, " + "no signing source", IdToName(key->publicKeyFmt)); continue; } if (key->publicKeyFmt == ID_SSH_RSA) { @@ -2302,6 +2306,10 @@ WOLFSSH_LOCAL void RefreshPublicKeyAlgo(WOLFSSH_CTX* ctx) } } } + if (publicKeyAlgoCount == 0 && keyCount > 0) { + WLOG(WS_LOG_ERROR, "RefreshPublicKeyAlgo: No usable host key; every " + "loaded slot lacks a certificate or signing source"); + } ctx->publicKeyAlgoCount = publicKeyAlgoCount; } @@ -2416,6 +2424,10 @@ static int UpdateHostCertificates(WOLFSSH_CTX* ctx, ctx->privateKey[certHint].key = NULL; ctx->privateKey[certHint].keySz = 0; } + #ifdef WOLFSSH_WINDOWS_CERT_STORE + /* A slot is never both TPM- and cert-store backed. */ + ClearCertStoreKey(ctx, &ctx->privateKey[certHint]); + #endif ctx->privateKey[certHint].isTpm = 1; } #endif @@ -2438,6 +2450,11 @@ static int UpdateHostCertificates(WOLFSSH_CTX* ctx, /* The slot's key material and its cert-store state change * together, so the store certificate is never sent as K_S * with a signature made by this software key. */ + if (IsCertStoreKey(&ctx->privateKey[certHint])) { + WLOG(WS_LOG_ERROR, "UpdateHostCertificates: Dropping " + "the cert-store x509v3 host key; the loaded file " + "key replaces it"); + } ClearCertStoreKey(ctx, &ctx->privateKey[certHint]); #endif ctx->privateKey[certHint].key = key; @@ -2508,9 +2525,9 @@ static int SetHostCertificate(WOLFSSH_CTX* ctx, WOLFSSH_PVT_KEY* pvtKey = ctx->privateKey + destIdx; #ifdef WOLFSSH_WINDOWS_CERT_STORE - /* A file-based certificate is replacing this slot's contents; drop - * any cert-store state, including the store's certificate DER, so - * the slot is not mistaken for a cert-store key. */ + /* Defensive only: the else-if above already rejects a cert-store + * slot, so this can only clear a slot in a state no writer + * currently produces. */ ClearCertStoreKey(ctx, pvtKey); #endif @@ -2583,6 +2600,11 @@ static int SetHostPrivateKey(WOLFSSH_CTX* ctx, /* This slot is now backed by an in-memory key; drop any cert-store * state it may have carried so signing/K_S do not use a stale * certificate context. */ + if (IsCertStoreKey(pvtKey)) { + WLOG(WS_LOG_ERROR, "SetHostPrivateKey: Replacing the " + "certificate store host key for this algorithm with a " + "file key"); + } ClearCertStoreKey(ctx, pvtKey); #endif @@ -6019,8 +6041,30 @@ static int ParseECCPubKeyCert(WOLFSSH *ssh, #ifndef WOLFSSH_NO_ECDSA byte* der = NULL; word32 derSz, idx = 0; + int expectedCurve; int error; + switch (ssh->handshake->pubKeyId) { + #ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP256 + case ID_X509V3_ECDSA_SHA2_NISTP256: + expectedCurve = ECC_SECP256R1; + break; + #endif + #ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP384 + case ID_X509V3_ECDSA_SHA2_NISTP384: + expectedCurve = ECC_SECP384R1; + break; + #endif + #ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP521 + case ID_X509V3_ECDSA_SHA2_NISTP521: + expectedCurve = ECC_SECP521R1; + break; + #endif + default: + expectedCurve = ECC_CURVE_INVALID; + break; + } + ret = ParsePubKeyCert(ssh, pubKey, pubKeySz, &der, &derSz); if (ret == WS_SUCCESS) { error = InitPubKey(sigKeyBlock_ptr, ssh); @@ -6031,6 +6075,15 @@ static int ParseECCPubKeyCert(WOLFSSH *ssh, if (error == 0) error = wc_EccPublicKeyDecode(der, &idx, &sigKeyBlock_ptr->sk.ecc.key, derSz); + /* Bind the certificate's key to the negotiated curve, as + * ParseECCPubKey does for plain keys. */ + if (error == 0 && + wc_ecc_get_curve_id(sigKeyBlock_ptr->sk.ecc.key.idx) != + expectedCurve) { + WLOG(WS_LOG_DEBUG, "ParseECCPubKeyCert: certificate key curve " + "does not match the negotiated algorithm"); + error = WS_INVALID_PRIME_CURVE; + } if (error == 0) { sigKeyBlock_ptr->keySz = (word32)sizeof(sigKeyBlock_ptr->sk.ecc.key); } @@ -12851,6 +12904,16 @@ int SendKexInit(WOLFSSH* ssh) ret = WS_BAD_ARGUMENT; } + /* Loaded slots can all lack a signing source (RefreshPublicKeyAlgo + * skips them); fail here rather than send an empty, RFC 4253 + * violating, server-host-key-algorithms list. */ + if (ret == WS_SUCCESS && ssh->ctx->side == WOLFSSH_ENDPOINT_SERVER && + ssh->algoListKey == NULL && ssh->ctx->publicKeyAlgoCount == 0) { + WLOG(WS_LOG_ERROR, "No usable host key: every loaded slot lacks a " + "certificate or signing source"); + ret = WS_BAD_ARGUMENT; + } + if (ret == WS_SUCCESS) { /* Set self is keying flag since we started sending the KEX init msg */ ssh->isKeying |= WOLFSSH_SELF_IS_KEYING; @@ -14733,64 +14796,65 @@ static byte CertStoreBaseKeyId(byte id) } -/* Find the cert-store-backed private key slot whose key type matches the - * public key algorithm keyId being used, so that a config holding both an - * RSA and an ECC cert-store key selects the correct slot. Returns NULL - * when no cert-store slot matches. */ -static const WOLFSSH_PVT_KEY* FindCertStoreKey(const WOLFSSH_CTX* ctx, - byte keyId) +/* Resolve the cert-store slot to sign a client user-auth request with. The + * slot must match the key type of the public key algorithm keyId AND hold + * the exact certificate being offered, so a credential the application + * supplied itself is never silently signed with a store key, and several + * slots sharing a base key type do not shadow one another. Returns NULL + * when the request is not a cert-store request, in which case the caller + * falls back to the in-memory key. */ +static const WOLFSSH_PVT_KEY* FindCertStoreAuthKey(const WOLFSSH_CTX* ctx, + byte keyId, const byte* cert, word32 certSz) { const WOLFSSH_PVT_KEY* pvtKey; byte baseId; word32 i; - if (ctx == NULL) { + if (ctx == NULL || cert == NULL || certSz == 0) { return NULL; } baseId = CertStoreBaseKeyId(keyId); - for (i = 0; i < ctx->privateKeyCount; i++) { + for (i = 0; i < ctx->privateKeyCount && i < WOLFSSH_MAX_PVT_KEYS; i++) { pvtKey = &ctx->privateKey[i]; if (IsCertStoreKey(pvtKey) && - CertStoreBaseKeyId(pvtKey->publicKeyFmt) == baseId) { + CertStoreBaseKeyId(pvtKey->publicKeyFmt) == baseId && + pvtKey->cert != NULL && pvtKey->certSz == certSz && + WMEMCMP(pvtKey->cert, cert, certSz) == 0) { return pvtKey; } } return NULL; } +#endif /* WOLFSSH_CERTS */ -/* Resolve the cert-store slot to sign a client user-auth request with. The - * slot must hold the exact certificate being offered, so a credential the - * application supplied itself is never silently signed with a store key. - * Returns NULL when the request is not a cert-store request, in which case - * the caller falls back to the in-memory key. */ -static const WOLFSSH_PVT_KEY* FindCertStoreAuthKey(const WOLFSSH_CTX* ctx, - byte keyId, const byte* cert, word32 certSz) +#ifndef WOLFSSH_NO_ECDSA +/* Field size in bytes of the curve behind an ECDSA key id, 0 when the id + * is not an ECDSA type. */ +static word32 CertStoreCurveSzForId(byte id) { - const WOLFSSH_PVT_KEY* pvtKey; - - pvtKey = FindCertStoreKey(ctx, keyId); - if (pvtKey != NULL) { - if (pvtKey->cert == NULL || cert == NULL || certSz == 0 || - pvtKey->certSz != certSz || - WMEMCMP(pvtKey->cert, cert, certSz) != 0) { - pvtKey = NULL; - } + switch (CertStoreBaseKeyId(id)) { + case ID_ECDSA_SHA2_NISTP256: + return 32; + case ID_ECDSA_SHA2_NISTP384: + return 48; + case ID_ECDSA_SHA2_NISTP521: + return 66; } - - return pvtKey; + return 0; } -#endif /* WOLFSSH_CERTS */ -#ifndef WOLFSSH_NO_ECDSA /* Convert an ECDSA signature from NCryptSignHash, which is raw r||s with - * each component exactly half of sigSz (not DER), into separate minimal - * mpint components with leading zeros trimmed. On input rSz and sSz hold - * the capacities of r and s; on output they hold the trimmed sizes. */ -static int CertStoreEccSigToRs(const byte* sig, word32 sigSz, + * each component exactly the curve field size (not DER), into separate + * minimal mpint components with leading zeros trimmed. curveSz is the + * expected field size; a blob of any other length (e.g. a DER SEQUENCE + * from a misbehaving KSP) is rejected rather than split blindly. On input + * rSz and sSz hold the capacities of r and s; on output they hold the + * trimmed sizes. */ +static int CertStoreEccSigToRs(const byte* sig, word32 sigSz, word32 curveSz, byte* r, word32* rSz, byte* s, word32* sSz) { word32 halfSz; @@ -14802,12 +14866,13 @@ static int CertStoreEccSigToRs(const byte* sig, word32 sigSz, sOff = 0; ret = WS_SUCCESS; - if (sigSz < 2 || (sigSz & 1) != 0) { - WLOG(WS_LOG_DEBUG, "CertStoreEccSigToRs: Invalid signature size"); + if (curveSz == 0 || sigSz != curveSz * 2) { + WLOG(WS_LOG_DEBUG, "CertStoreEccSigToRs: Signature size does not " + "match the curve"); ret = WS_ECC_E; } if (ret == WS_SUCCESS) { - halfSz = sigSz / 2; + halfSz = curveSz; if (halfSz > *rSz || halfSz > *sSz) { WLOG(WS_LOG_DEBUG, "CertStoreEccSigToRs: Signature too large"); ret = WS_ECC_E; @@ -14932,9 +14997,15 @@ static int SignWithCertStoreKey(WOLFSSH* ssh, } } - /* Free the key handle if we acquired it */ + /* Free the key handle if we acquired it. Only NCRYPT keys are acquired + * above; the CSP release is kept for the flags changing. */ if (fCallerFreeProv) { - NCryptFreeObject(hCryptProv); + if (dwKeySpec == CERT_NCRYPT_KEY_SPEC) { + NCryptFreeObject(hCryptProv); + } + else { + CryptReleaseContext(hCryptProv, 0); + } } WLOG(WS_LOG_DEBUG, "Leaving SignWithCertStoreKey(), ret = %d", ret); @@ -14996,6 +15067,24 @@ static int SignHRsa(WOLFSSH* ssh, byte* sig, word32* sigSz, if (ret == WS_SUCCESS) { WLOG(WS_LOG_INFO, "Signing hash with %s.", IdToName(ssh->handshake->pubKeyId)); + #ifdef WOLFSSH_WINDOWS_CERT_STORE + /* A slot is never both cert-store and TPM backed; testing the + * cert-store first matches SendKexGetSigningKey()'s dispatch. */ + if (IsCertStoreKey(sigKey->pvtKey)) { + /* Use cert store signing abstraction */ + ret = SignWithCertStoreKey(ssh, sigKey->pvtKey, encSig, encSigSz, + hashId, sig, sigSz); + if (ret == WS_SUCCESS && *sigSz == 0) { + WLOG(WS_LOG_DEBUG, "SignHRsa: Cert store sign gave no " + "signature"); + ret = WS_RSA_E; + } + if (ret != WS_SUCCESS) { + WLOG(WS_LOG_DEBUG, "SignHRsa: Cert store sign failed"); + } + } + else + #endif /* WOLFSSH_WINDOWS_CERT_STORE */ #ifdef WOLFSSH_TPM if (ssh->handshake->useTpm && ssh->ctx->tpmDev != NULL && ssh->ctx->tpmKey != NULL) { @@ -15015,23 +15104,6 @@ static int SignHRsa(WOLFSSH* ssh, byte* sig, word32* sigSz, } else #endif /* WOLFSSH_TPM */ - #ifdef WOLFSSH_WINDOWS_CERT_STORE - /* Check if this is a cert store key */ - if (IsCertStoreKey(sigKey->pvtKey)) { - /* Use cert store signing abstraction */ - ret = SignWithCertStoreKey(ssh, sigKey->pvtKey, encSig, encSigSz, - hashId, sig, sigSz); - if (ret == WS_SUCCESS && *sigSz == 0) { - WLOG(WS_LOG_DEBUG, "SignHRsa: Cert store sign gave no " - "signature"); - ret = WS_RSA_E; - } - if (ret != WS_SUCCESS) { - WLOG(WS_LOG_DEBUG, "SignHRsa: Cert store sign failed"); - } - } - else - #endif /* WOLFSSH_WINDOWS_CERT_STORE */ { /* Use traditional key signing */ ret = wc_RsaSSL_Sign(encSig, encSigSz, sig, @@ -15147,11 +15219,10 @@ static int SignHEcdsa(WOLFSSH* ssh, byte* sig, word32* sigSz, /* Check if this is a cert store key */ if (IsCertStoreKey(sigKey->pvtKey)) { /* Use cert store signing abstraction - ECDSA uses raw hash. - * Note: unlike the RSA path, ECDSA does not self-verify here - * because NCryptSignHash returns raw r||s (not DER), and - * converting back for wc_ecc_verify_hash would add complexity. - * The key exchange hash comparison by the peer serves as - * the primary verification. */ + * Note: unlike the RSA path, the server does not self-verify + * this signature; CertStoreEccSigToRs() below validates the + * blob length against the curve, and only the peer performs a + * cryptographic verification. */ ret = SignWithCertStoreKey(ssh, sigKey->pvtKey, digest, digestSz, hashId, sig, sigSz); if (ret != WS_SUCCESS) { @@ -15212,10 +15283,12 @@ static int SignHEcdsa(WOLFSSH* ssh, byte* sig, word32* sigSz, else #endif /* WOLFSSH_TPM */ #ifdef WOLFSSH_WINDOWS_CERT_STORE - /* NCryptSignHash for ECDSA returns raw r||s (each half of sigSz), - * NOT DER-encoded. Split directly. */ + /* NCryptSignHash for ECDSA returns raw r||s (each half the curve + * size), NOT DER-encoded. Split directly. */ if (IsCertStoreKey(sigKey->pvtKey)) { - ret = CertStoreEccSigToRs(sig, *sigSz, r, &rSz, s, &sSz); + ret = CertStoreEccSigToRs(sig, *sigSz, + CertStoreCurveSzForId(sigKey->pvtKey->publicKeyFmt), + r, &rSz, s, &sSz); } else #endif /* WOLFSSH_WINDOWS_CERT_STORE */ @@ -17494,9 +17567,20 @@ static int PrepareUserAuthRequestRsaCert(WOLFSSH* ssh, word32* payloadSz, } else #endif /* WOLFSSH_WINDOWS_CERT_STORE */ + if (authData->sf.publicKey.privateKey == NULL || + authData->sf.publicKey.privateKeySz == 0) { + /* A cert-store-only client has no in-memory key; a decode of + * the empty buffer would report a misleading wolfCrypt ASN + * error. */ + WLOG(WS_LOG_DEBUG, "PrepareUserAuthRequestRsaCert: No private " + "key; the offered certificate matched no cert-store slot"); + ret = WS_BAD_ARGUMENT; + } + else { ret = wc_RsaPrivateKeyDecode(authData->sf.publicKey.privateKey, &idx, &keySig->ks.rsa.key, authData->sf.publicKey.privateKeySz); + } } if (ret == WS_SUCCESS) { @@ -18046,10 +18130,22 @@ static int PrepareUserAuthRequestEccCert(WOLFSSH* ssh, word32* payloadSz, else #endif #endif - ret = wc_EccPrivateKeyDecode( - authData->sf.publicKey.privateKey, - &idx, &keySig->ks.ecc.key, - authData->sf.publicKey.privateKeySz); + if (authData->sf.publicKey.privateKey == NULL || + authData->sf.publicKey.privateKeySz == 0) { + /* A cert-store-only client has no in-memory key; a + * decode of the empty buffer would report a misleading + * wolfCrypt ASN error. */ + WLOG(WS_LOG_DEBUG, "PrepareUserAuthRequestEccCert: No " + "private key; the offered certificate matched no " + "cert-store slot"); + ret = WS_BAD_ARGUMENT; + } + else { + ret = wc_EccPrivateKeyDecode( + authData->sf.publicKey.privateKey, + &idx, &keySig->ks.ecc.key, + authData->sf.publicKey.privateKeySz); + } } } @@ -18174,11 +18270,13 @@ static int BuildUserAuthRequestEccCert(WOLFSSH* ssh, digest, digestSz, hashId, sig, &sigSz); if (ret == WS_SUCCESS) { /* NCryptSignHash ECDSA output is raw r||s, each - * component is half the total signature size. */ + * component is the curve field size. */ rSz = sSz = (word32)sizeof(rs) / 2; r = rs; s = rs + rSz; - ret = CertStoreEccSigToRs(sig, sigSz, r, &rSz, s, &sSz); + ret = CertStoreEccSigToRs(sig, sigSz, + CertStoreCurveSzForId(pvtKey->publicKeyFmt), + r, &rSz, s, &sSz); if (ret != WS_SUCCESS) { WLOG(WS_LOG_DEBUG, "SUAR: Bad cert store ECC signature"); diff --git a/src/ssh.c b/src/ssh.c index b8224c0eb..301453820 100644 --- a/src/ssh.c +++ b/src/ssh.c @@ -2892,21 +2892,19 @@ static int CertKeyCanSign(PCCERT_CONTEXT pCertContext) * that a lookup for "server1" does not select "server1.example" or * "myserver1". The compare is case insensitive, matching both the * pre-filter and X.500 name semantics. Candidates are ranked by how - * usable they are: time-valid with a usable key, then any candidate with - * a usable key, then time-valid without one, then the rest. A key that - * can sign outranks time validity, because a certificate with no usable - * key can never produce a signature, so neither a renewal's leftover - * certificate nor a public-only duplicate ends the search. The selected - * certificate is stored in out, and is NULL when no match exists. The - * caller frees it with CertFreeCertificateContext. + * usable they are: time-valid with a usable key first, then any candidate + * with a usable key. A candidate with no usable key is never selected -- + * the caller repeats the same key acquisition and would only fail with a + * misleading error -- so a public-only duplicate neither ends the search + * nor is returned. The selected certificate is stored in out, and is NULL + * when no usable match exists. The caller frees it with + * CertFreeCertificateContext. * Returns WS_SUCCESS on success. */ static int FindCertByExactCN(void* heap, HCERTSTORE hStore, const wchar_t* subjectName, PCCERT_CONTEXT* out) { PCCERT_CONTEXT pCertContext; PCCERT_CONTEXT keyedMatch; - PCCERT_CONTEXT validMatch; - PCCERT_CONTEXT expiredMatch; const wchar_t* cn; wchar_t* certCn; DWORD certCnSz; @@ -2914,13 +2912,13 @@ static int FindCertByExactCN(void* heap, HCERTSTORE hStore, int hasKey; int timeValidity; int keyedEarly; - int expiredEarly; + int keylessSeen; int ret; *out = NULL; ret = WS_SUCCESS; keyedEarly = 0; - expiredEarly = 0; + keylessSeen = 0; /* Strip an optional "CN=" prefix from the requested name. */ cn = subjectName; @@ -2934,8 +2932,6 @@ static int FindCertByExactCN(void* heap, HCERTSTORE hStore, pCertContext = NULL; keyedMatch = NULL; - validMatch = NULL; - expiredMatch = NULL; for (;;) { /* Passing the previous context frees it and continues the search. */ pCertContext = CertFindCertificateInStore(hStore, @@ -2983,20 +2979,8 @@ static int FindCertByExactCN(void* heap, HCERTSTORE hStore, } } } - else if (timeValidity == 0) { - if (validMatch == NULL) { - validMatch = CertDuplicateCertificateContext(pCertContext); - if (validMatch == NULL) { - ret = WS_MEMORY_E; - } - } - } - else if (expiredMatch == NULL) { - expiredMatch = CertDuplicateCertificateContext(pCertContext); - expiredEarly = (timeValidity < 0); - if (expiredMatch == NULL) { - ret = WS_MEMORY_E; - } + else { + keylessSeen = 1; } if (ret != WS_SUCCESS) { @@ -3016,32 +3000,20 @@ static int FindCertByExactCN(void* heap, HCERTSTORE hStore, pCertContext = keyedMatch; keyedMatch = NULL; } - else if (validMatch != NULL) { - WLOG(WS_LOG_WARN, "FindCertByExactCN: No match with a usable " - "private key, using '%ls' anyway", subjectName); - pCertContext = validMatch; - validMatch = NULL; - } - else if (expiredMatch != NULL) { - WLOG(WS_LOG_WARN, "FindCertByExactCN: No time-valid match and " - "none with a usable private key, using a %s '%ls'", - expiredEarly ? "not yet valid" : "expired", subjectName); - pCertContext = expiredMatch; - expiredMatch = NULL; + else if (keylessSeen) { + WLOG(WS_LOG_ERROR, "FindCertByExactCN: '%ls' matched only " + "certificates with no usable private key", subjectName); } } if (keyedMatch != NULL) { CertFreeCertificateContext(keyedMatch); } - if (validMatch != NULL) { - CertFreeCertificateContext(validMatch); - } - if (expiredMatch != NULL) { - CertFreeCertificateContext(expiredMatch); - } - if (ret == WS_MEMORY_E) { - WLOG(WS_LOG_ERROR, "FindCertByExactCN: Memory allocation failed"); + if (ret != WS_SUCCESS) { + if (pCertContext != NULL) { + CertFreeCertificateContext(pCertContext); + } + WLOG(WS_LOG_ERROR, "FindCertByExactCN: Failed, ret = %d", ret); } else { *out = pCertContext; @@ -3150,6 +3122,10 @@ static void CommitCertStoreSlot(WOLFSSH_CTX* ctx, CertStoreSlot* slot) (PCCERT_CONTEXT)pvtKey->certStoreContext); } if (pvtKey->key != NULL) { + /* The mirror-image order (a file key loaded after a store key) is + * reported the same way from SetHostPrivateKey(). */ + WLOG(WS_LOG_ERROR, "CommitCertStoreSlot: Replacing the file-based " + "host key for this algorithm with the certificate store key"); WS_FORCEZERO(pvtKey->key, pvtKey->keySz); WFREE(pvtKey->key, heap, DYNTYPE_PRIVKEY); pvtKey->key = NULL; @@ -3243,11 +3219,10 @@ int wolfSSH_CTX_UsePrivateKey_fromStore(WOLFSSH_CTX* ctx, return WS_BAD_ARGUMENT; } - /* Only accept system-store location bits. Anything else is either not - * a location or a control flag (e.g. CERT_STORE_DELETE_FLAG) that - * would make CertOpenStore destructive. */ - if ((dwFlags & (word32)CERT_SYSTEM_STORE_LOCATION_MASK) == 0 || - (dwFlags & ~(word32)CERT_SYSTEM_STORE_LOCATION_MASK) != 0) { + /* Only accept an assigned system-store location. Anything else is + * either not a location or a control flag (e.g. CERT_STORE_DELETE_FLAG) + * that would make CertOpenStore destructive. */ + if (!wolfSSH_CertStoreLocationValid(dwFlags)) { WLOG(WS_LOG_ERROR, "wolfSSH_CTX_UsePrivateKey_fromStore: Store " "flags are not a system store location"); return WS_BAD_ARGUMENT; @@ -3262,7 +3237,7 @@ int wolfSSH_CTX_UsePrivateKey_fromStore(WOLFSSH_CTX* ctx, if (hStore == NULL) { WLOG(WS_LOG_ERROR, "wolfSSH_CTX_UsePrivateKey_fromStore: Failed to " "open store, error: %lu", (unsigned long)GetLastError()); - return WS_FATAL_ERROR; + return WS_BAD_FILE_E; } /* Find the certificate by full Common Name match. */ @@ -3431,8 +3406,9 @@ int wolfSSH_CTX_UsePrivateKey_fromStore(WOLFSSH_CTX* ctx, } if (newCount > WOLFSSH_MAX_PVT_KEYS) { - WLOG(WS_LOG_ERROR, "wolfSSH_CTX_UsePrivateKey_fromStore: No " - "available key slot"); + WLOG(WS_LOG_ERROR, "wolfSSH_CTX_UsePrivateKey_fromStore: Not enough " + "free key slots; a store key needs one for the plain type and " + "one for the x509v3 type"); ret = WS_CTX_KEY_COUNT_E; } if (ret == WS_SUCCESS) { @@ -3471,7 +3447,8 @@ int wolfSSH_CTX_UsePrivateKey_fromStore(WOLFSSH_CTX* ctx, RefreshPublicKeyAlgo(ctx); } - WLOG(WS_LOG_DEBUG, "Leaving wolfSSH_CTX_UsePrivateKey_fromStore(), ret = %d", ret); + WLOG(WS_LOG_DEBUG, "Leaving wolfSSH_CTX_UsePrivateKey_fromStore(), " + "ret = %d", ret); return ret; } #endif /* WOLFSSH_WINDOWS_CERT_STORE */ diff --git a/tests/unit.c b/tests/unit.c index ab44341a1..93e59b63e 100644 --- a/tests/unit.c +++ b/tests/unit.c @@ -47,10 +47,11 @@ #include #include "unit.h" -/* Regression coverage for non-CA intermediate promotion. - * Needs WOLFSSH_TEST_INTERNAL (the test bodies are in that section), the cert - * manager, runtime cert generation to forge the attack cert, ECDSA (the test - * certs are ECC), and a filesystem to load the test certs. */ +/* Regression coverage for non-CA intermediate promotion. The test bodies use + * only public API, but keep WOLFSSH_TEST_INTERNAL: it limits them to the + * autotools test builds, which run with the ./keys certs the tests hard-load. + * Also needs the cert manager, runtime cert generation to forge the attack + * cert, ECDSA (the test certs are ECC), and a filesystem for the certs. */ #if defined(WOLFSSH_TEST_INTERNAL) && defined(WOLFSSH_CERTS) && \ defined(WOLFSSL_CERT_GEN) && !defined(WOLFSSH_NO_ECDSA) && \ !defined(NO_FILESYSTEM) @@ -71,6 +72,13 @@ #include #endif +/* wolfSSH_SetCertManager() is an unconditional WS_NOT_COMPILED stub before + * wolfSSL 4.6.0, which is where wolfSSL_CertManager_up_ref() landed. Skip the + * test there rather than failing on behaviour that is by design. */ +#if defined(WOLFSSH_CERTS) && (LIBWOLFSSL_VERSION_HEX >= WOLFSSL_V4_6_0) + #define WOLFSSH_TEST_SET_CERTMAN +#endif + #ifdef WOLFSSH_CERTS #include #include @@ -9557,7 +9565,11 @@ static int test_IdentifyAsn1Key(void) * WOLFSSH_TEST_INTERNAL section. Each carries its own feature guard; * WOLFSSH_TEST_CERTMAN_PROMOTE still implies WOLFSSH_TEST_INTERNAL. */ -#ifdef WOLFSSH_TEST_CERTMAN_ROOTCA +/* Guard by the actual users -- the promote tests and the root-CA half of + * test_SetCertManager() -- to avoid -Wunused-function. */ +#if defined(WOLFSSH_TEST_CERTMAN_PROMOTE) || \ + (defined(WOLFSSH_TEST_SET_CERTMAN) && \ + defined(WOLFSSH_TEST_CERTMAN_ROOTCA)) /* Read a whole file into a freshly malloc'd buffer. Caller frees *buf. */ static int certmanLoadFile(const char* fn, byte** buf, word32* bufSz) @@ -9606,7 +9618,8 @@ static int certmanLoadFile(const char* fn, byte** buf, word32* bufSz) return 0; } -#endif /* WOLFSSH_TEST_CERTMAN_ROOTCA */ +#endif /* WOLFSSH_TEST_CERTMAN_PROMOTE || + * (WOLFSSH_TEST_SET_CERTMAN && WOLFSSH_TEST_CERTMAN_ROOTCA) */ #ifdef WOLFSSH_TEST_CERTMAN_PROMOTE @@ -10023,13 +10036,6 @@ static int test_CertMan_PromoteValidCaIntermediate(void) #endif /* WOLFSSH_TEST_CERTMAN_PROMOTE */ -/* wolfSSH_SetCertManager() is an unconditional WS_NOT_COMPILED stub before - * wolfSSL 4.6.0, which is where wolfSSL_CertManager_up_ref() landed. Skip the - * test there rather than failing on behaviour that is by design. */ -#if defined(WOLFSSH_CERTS) && (LIBWOLFSSL_VERSION_HEX >= WOLFSSL_V4_6_0) - #define WOLFSSH_TEST_SET_CERTMAN -#endif - #ifdef WOLFSSH_TEST_SET_CERTMAN /* wolfSSH_SetCertManager imports a WOLFSSL_CERT_MANAGER by reference into * the wolfSSH context. Test argument checking, importing the same manager @@ -10048,15 +10054,15 @@ static int test_SetCertManager(void) #ifdef WOLFSSH_TEST_CERTMAN_ROOTCA byte* root = NULL; word32 rootSz = 0; - /* ./keys only resolves when run from the source root, and - * keys/ca-cert-ecc.der is not linked into an out-of-tree build tree. The - * argument and reference-count checks below need no file, so treat a - * missing cert as a skip of the root-CA half rather than a failure. */ + /* Autotools builds link keys/ca-cert-ecc.der into the build tree, but + * other runners (e.g. the VS unit-test.exe) may not run from a tree with + * ./keys. The argument and reference-count checks below need no file, so + * treat a missing cert as a SKIP of the root-CA half, not a failure. */ int haveRoot = (certmanLoadFile("./keys/ca-cert-ecc.der", &root, &rootSz) == 0); if (!haveRoot) { - printf("SetCertManager: skipping root cert checks, " + printf("SetCertManager: SKIP root cert checks, " "./keys/ca-cert-ecc.der not readable\n"); } #endif @@ -10170,10 +10176,7 @@ static int certStoreSpecCheck(const char* spec, int expRet, result = -5; } - if (wStoreName != NULL) - WFREE(wStoreName, NULL, DYNTYPE_TEMP); - if (wSubjectName != NULL) - WFREE(wSubjectName, NULL, DYNTYPE_TEMP); + wolfSSH_FreeCertStoreSpec(wStoreName, wSubjectName, NULL); return result; } @@ -10222,6 +10225,10 @@ static int test_ParseCertStoreSpec(void) if (result == 0) result = certStoreSpecCheck("My:server:65552", WS_BAD_ARGUMENT, NULL, NULL, 0); + /* an unassigned location id (0x00030000) is rejected */ + if (result == 0) + result = certStoreSpecCheck("My:server:0x00030000", WS_BAD_ARGUMENT, + NULL, NULL, 0); /* missing or empty fields are rejected */ if (result == 0) diff --git a/wolfssh/certman.h b/wolfssh/certman.h index 771358452..2ca3b27bc 100644 --- a/wolfssh/certman.h +++ b/wolfssh/certman.h @@ -30,9 +30,12 @@ #include #include +#include /* included for WOLFSSL_CERT_MANAGER struct */ #ifdef WOLFSSH_CERTS #include /* included for WOLFSSH_CTX */ - #include /* included for WOLFSSL_CERT_MANAGER struct */ +#endif +#ifdef WOLFSSH_WINDOWS_CERT_STORE + #include #endif #ifdef __cplusplus @@ -50,7 +53,9 @@ typedef struct WOLFSSH_CERTMAN WOLFSSH_CERTMAN; * note the policy is applied to the shared object: in an HAVE_OCSP build * this enables WOLFSSL_OCSP_CHECKALL on cm, so a caller that keeps using * the same manager for TLS will find every chain requiring an OCSP - * response. */ + * response. Returns WS_NOT_COMPILED for any arguments when built against + * wolfSSL older than 4.6.0 (wolfSSL_CertManager_up_ref() is unavailable + * there). */ WOLFSSH_API int wolfSSH_SetCertManager(WOLFSSH_CTX* ctx, WOLFSSL_CERT_MANAGER* cm); #endif /* WOLFSSH_CERTS */ @@ -75,11 +80,20 @@ int wolfSSH_CERTMAN_VerifyCerts_buffer(WOLFSSH_CERTMAN* cm, * LOCAL_MACHINE, USERS, or a decimal or 0x hex CERT_SYSTEM_STORE_* location, * and defaults to CURRENT_USER. The spec is split at the first two ':', so * neither the store name nor the subject may contain one and a third ':' is - * rejected. */ + * rejected. Returns WS_SUCCESS and gives the caller ownership of the two + * allocated wide strings, which must be released with + * wolfSSH_FreeCertStoreSpec() using the same heap. On failure both + * out-pointers are set to NULL and dwFlags is untouched. */ WOLFSSH_API int wolfSSH_ParseCertStoreSpec(const char* spec, wchar_t** wStoreName, wchar_t** wSubjectName, word32* dwFlags, void* heap); + +/* Frees the strings returned by wolfSSH_ParseCertStoreSpec(). Either + * pointer may be NULL. */ +WOLFSSH_API +void wolfSSH_FreeCertStoreSpec(wchar_t* wStoreName, wchar_t* wSubjectName, + void* heap); #endif /* WOLFSSH_CERTS && WOLFSSH_WINDOWS_CERT_STORE */ diff --git a/wolfssh/internal.h b/wolfssh/internal.h index 34b5c0c71..b6bafe762 100644 --- a/wolfssh/internal.h +++ b/wolfssh/internal.h @@ -64,8 +64,6 @@ #ifndef _WIN32 #error "WOLFSSH_WINDOWS_CERT_STORE requires a Windows (_WIN32) target" #endif - /* the cert store fields below are wchar_t strings */ - #include #endif /* WOLFSSH_WINDOWS_CERT_STORE */ #ifdef WOLFSSH_TPM @@ -117,10 +115,8 @@ extern "C" { #define WOLFSSH_NO_DH #endif -#ifndef WOLFSSL_V4_6_0 - /* wolfSSL_CertManager_up_ref() was added in wolfSSL 4.6.0 */ - #define WOLFSSL_V4_6_0 0x04006000 -#endif +/* wolfSSL_CertManager_up_ref() was added in wolfSSL 4.6.0 */ +#define WOLFSSL_V4_6_0 0x04006000 #define WOLFSSL_V5_0_0 0x05000000 #define WOLFSSL_V5_7_0 0x05007000 #define WOLFSSL_V5_7_2 0x05007002 @@ -767,6 +763,12 @@ typedef struct WOLFSSH_PVT_KEY { #endif /* WOLFSSH_WINDOWS_CERT_STORE */ } WOLFSSH_PVT_KEY; +#ifdef WOLFSSH_WINDOWS_CERT_STORE +/* Returns 1 when the value is exactly one assigned CERT_SYSTEM_STORE_* + * location with no control flags set. Defined in certman.c. */ +WOLFSSH_LOCAL int wolfSSH_CertStoreLocationValid(word32 dwFlags); +#endif + /* our wolfSSH Context */ struct WOLFSSH_CTX { diff --git a/wolfssh/ssh.h b/wolfssh/ssh.h index beb35ca86..e8b71bedc 100644 --- a/wolfssh/ssh.h +++ b/wolfssh/ssh.h @@ -507,8 +507,15 @@ WOLFSSH_API int wolfSSH_CTX_UsePrivateKey_buffer(WOLFSSH_CTX* ctx, * only CERT_SYSTEM_STORE_* location bits, e.g. * CERT_SYSTEM_STORE_CURRENT_USER; control flags such as * CERT_STORE_DELETE_FLAG are rejected with WS_BAD_ARGUMENT. The store - * is opened read-only. Returns WS_SUCCESS on success; on any failure - * the context is left unchanged. */ + * is opened read-only. When no time-valid certificate with a usable + * private key matches, an expired or not-yet-valid one with a usable + * key is still selected with only a logged warning, so a renewal whose + * key is not yet readable can fall back to the previous certificate. + * Returns WS_SUCCESS on success, WS_BAD_FILE_E when the store cannot + * be opened, WS_CRYPTO_FAILED when the private key is inaccessible, + * WS_CTX_KEY_COUNT_E when two key slots are not free, and + * WS_FATAL_ERROR when no certificate matches; on any failure the + * context is left unchanged. */ WOLFSSH_API int wolfSSH_CTX_UsePrivateKey_fromStore(WOLFSSH_CTX* ctx, const wchar_t* storeName, word32 dwFlags, const wchar_t* subjectName); diff --git a/wolfssh/test.h b/wolfssh/test.h index c720c9fc8..a1dbf28e1 100644 --- a/wolfssh/test.h +++ b/wolfssh/test.h @@ -1148,7 +1148,9 @@ static INLINE void build_addr_ipv6(struct sockaddr_in6* addr, const char* peer, #ifdef WOLFSSH_TEST_HEX2BIN -#ifndef WOLFSSL_BASE16 +/* Use the local fallback whenever wolfSSL will not supply Base16_Decode: + * coding.c compiles to nothing under NO_CODING even with WOLFSSL_BASE16. */ +#if !defined(WOLFSSL_BASE16) || defined(NO_CODING) #define BAD 0xFF @@ -1226,7 +1228,7 @@ static int Base16_Decode(const byte* in, word32 inLen, #else #include -#endif /* !WOLFSSL_BASE16 */ +#endif /* !WOLFSSL_BASE16 || NO_CODING */ static void FreeBins(byte* b1, byte* b2, byte* b3, byte* b4) {